commit 8f3069035f4f91c4c5cfe5fb4a110fdf5ac55524 Author: HWLAB Code Queue Date: Thu May 21 14:27:53 2026 +0000 chore: establish hwlab l0 contract skeleton diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8286d08c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.go] +indent_style = tab +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..afadf021 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# OS and editor files +.DS_Store +Thumbs.db +*.swp +*.swo +.idea/ +.vscode/ + +# Logs and local environment +*.log +.env +.env.* +!.env.example + +# Build outputs +bin/ +dist/ +build/ +coverage/ +tmp/ + +# Go +*.test +*.out +vendor/ + +# Node package managers, for future web work +node_modules/ +.pnpm-store/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Deploy/runtime state +*.local.json diff --git a/README.md b/README.md new file mode 100644 index 00000000..224b33ea --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# HWLAB + +HWLAB is the hardware lab runtime and control plane for MVP development. + +This repository is intentionally small at L0. It defines the shared protocol +contract, schema skeletons, deploy manifest schema, and minimal validation +entry points that later service implementations must follow. + +## MVP Freeze + +- MVP scope is DEV only. +- DEV endpoint is fixed at `http://74.48.78.17:6667`. +- PROD endpoint `:6666` is reserved and is not an MVP acceptance target. +- UniDesk may be used only as an external scheduling, CI, or CD base. +- HWLAB runtime services must not be replaced by UniDesk backend, + provider-gateway, or microservice proxy implementations. + +## Frozen Service IDs + +- `hwlab-cloud-api` +- `hwlab-cloud-web` +- `hwlab-agent-mgr` +- `hwlab-agent-worker` +- `hwlab-gateway` +- `hwlab-gateway-simu` +- `hwlab-box-simu` +- `hwlab-patch-panel` +- `hwlab-router` +- `hwlab-tunnel-client` +- `hwlab-edge-proxy` +- `hwlab-cli` +- `hwlab-agent-skills` + +## Frozen Repository Layout + +The MVP branches may add implementation files under these paths: + +- `cmd/...` +- `internal/...` +- `web/hwlab-cloud-web` +- `tools/hwlab-cli` +- `skills` +- `protocol/schemas` +- `deploy/deploy.json` +- `deploy/k8s/base/dev/prod` +- `deploy/master-edge` +- `deploy/frp` +- `scripts/bootstrap-skills.sh` +- `scripts/worker-entrypoint.sh` + +## Contract Index + +- Protocol contract: [protocol/README.md](protocol/README.md) +- JSON-RPC envelope: [protocol/json-rpc.md](protocol/json-rpc.md) +- Error codes: [protocol/errors.md](protocol/errors.md) +- Audit fields: [protocol/audit.md](protocol/audit.md) +- Capability model: [protocol/capabilities.md](protocol/capabilities.md) +- Topology constraints: [protocol/topology.md](protocol/topology.md) +- MVP e2e contract: [protocol/mvp-e2e-contract.md](protocol/mvp-e2e-contract.md) +- JSON Schemas: [protocol/schemas](protocol/schemas) +- Deploy schema: [deploy/deploy.schema.json](deploy/deploy.schema.json) + +## Validation + +Run the lightweight validation suite: + +```sh +npm run check +``` diff --git a/cmd/.gitkeep b/cmd/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/cmd/.gitkeep @@ -0,0 +1 @@ + diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 00000000..f57e7323 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,7 @@ +# Deploy Contract + +`deploy.schema.json` defines the future `deploy/deploy.json` manifest shape. + +The MVP acceptance environment is DEV only, with endpoint +`http://74.48.78.17:6667`. PROD profile fields may be represented in schema for +future compatibility, but PROD deployment is not part of MVP acceptance. diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json new file mode 100644 index 00000000..38856930 --- /dev/null +++ b/deploy/deploy.schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/deploy/deploy.schema.json", + "title": "HWLAB Deploy Manifest", + "type": "object", + "required": ["manifestVersion", "environment", "commitId", "profiles", "services"], + "properties": { + "manifestVersion": { + "type": "string", + "pattern": "^v[0-9]+$" + }, + "environment": { + "type": "string", + "enum": ["dev"] + }, + "commitId": { + "type": "string", + "pattern": "^[a-f0-9]{7,40}$" + }, + "namespace": { + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + }, + "endpoint": { + "type": "string", + "const": "http://74.48.78.17:6667" + }, + "profiles": { + "type": "object", + "required": ["dev"], + "properties": { + "dev": { + "$ref": "#/$defs/profile" + }, + "prod": { + "$ref": "#/$defs/profile" + } + }, + "additionalProperties": false + }, + "services": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/service" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "$defs": { + "profile": { + "type": "object", + "required": ["name", "enabled"], + "properties": { + "name": { + "type": "string", + "enum": ["dev", "prod"] + }, + "enabled": { + "type": "boolean" + }, + "namespace": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "notes": { + "type": "string" + } + }, + "additionalProperties": false + }, + "service": { + "type": "object", + "required": ["serviceId", "image", "namespace", "healthPath", "profile"], + "properties": { + "serviceId": { + "type": "string", + "enum": [ + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-gateway", + "hwlab-gateway-simu", + "hwlab-box-simu", + "hwlab-patch-panel", + "hwlab-router", + "hwlab-tunnel-client", + "hwlab-edge-proxy", + "hwlab-cli", + "hwlab-agent-skills" + ] + }, + "image": { + "type": "string", + "minLength": 1 + }, + "namespace": { + "type": "string", + "minLength": 1 + }, + "healthPath": { + "type": "string", + "pattern": "^/" + }, + "profile": { + "type": "string", + "enum": ["dev", "prod"] + }, + "replicas": { + "type": "integer", + "minimum": 0 + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } +} diff --git a/deploy/frp/.gitkeep b/deploy/frp/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deploy/frp/.gitkeep @@ -0,0 +1 @@ + diff --git a/deploy/k8s/base/.gitkeep b/deploy/k8s/base/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deploy/k8s/base/.gitkeep @@ -0,0 +1 @@ + diff --git a/deploy/k8s/dev/.gitkeep b/deploy/k8s/dev/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deploy/k8s/dev/.gitkeep @@ -0,0 +1 @@ + diff --git a/deploy/k8s/prod/.gitkeep b/deploy/k8s/prod/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deploy/k8s/prod/.gitkeep @@ -0,0 +1 @@ + diff --git a/deploy/master-edge/.gitkeep b/deploy/master-edge/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/deploy/master-edge/.gitkeep @@ -0,0 +1 @@ + diff --git a/internal/protocol/index.mjs b/internal/protocol/index.mjs new file mode 100644 index 00000000..49c857e2 --- /dev/null +++ b/internal/protocol/index.mjs @@ -0,0 +1,127 @@ +export const ENVIRONMENT_DEV = "dev"; +export const DEV_ENDPOINT = "http://74.48.78.17:6667"; +export const JSON_RPC_VERSION = "2.0"; + +export const SERVICE_IDS = Object.freeze([ + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-gateway", + "hwlab-gateway-simu", + "hwlab-box-simu", + "hwlab-patch-panel", + "hwlab-router", + "hwlab-tunnel-client", + "hwlab-edge-proxy", + "hwlab-cli", + "hwlab-agent-skills" +]); + +export const TABLES = Object.freeze([ + "projects", + "gateway_sessions", + "box_resources", + "box_capabilities", + "wiring_configs", + "patch_panel_status", + "hardware_operations", + "audit_events", + "agent_sessions", + "worker_sessions", + "agent_trace_events", + "evidence_records" +]); + +export const ERROR_CODES = Object.freeze({ + parseError: -32700, + invalidRequest: -32600, + methodNotFound: -32601, + invalidParams: -32602, + internalError: -32603, + hwlabUnknown: -32000, + projectNotFound: -32010, + sessionNotFound: -32011, + capabilityUnavailable: -32020, + resourceLocked: -32021, + wiringInvalid: -32022, + operationRejected: -32023, + evidenceMissing: -32030, + auditRequired: -32040 +}); + +const methodPattern = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$/; + +export function isFrozenServiceId(serviceId) { + return SERVICE_IDS.includes(serviceId); +} + +export function validateMeta(meta) { + if (!meta || typeof meta !== "object" || Array.isArray(meta)) { + throw new Error("meta must be an object"); + } + if (!meta.traceId) { + throw new Error("meta.traceId is required"); + } + if (!isFrozenServiceId(meta.serviceId)) { + throw new Error(`unknown serviceId ${JSON.stringify(meta.serviceId)}`); + } + if (meta.environment !== ENVIRONMENT_DEV) { + throw new Error(`unsupported environment ${JSON.stringify(meta.environment)}`); + } +} + +export function validateRequest(envelope) { + if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) { + throw new Error("request must be an object"); + } + if (envelope.jsonrpc !== JSON_RPC_VERSION) { + throw new Error(`jsonrpc must be ${JSON_RPC_VERSION}`); + } + if (envelope.id === undefined || envelope.id === null) { + throw new Error("id is required"); + } + if (!methodPattern.test(envelope.method)) { + throw new Error(`invalid method ${JSON.stringify(envelope.method)}`); + } + if ( + envelope.params !== undefined && + (!envelope.params || typeof envelope.params !== "object" || Array.isArray(envelope.params)) + ) { + throw new Error("params must be an object when present"); + } + validateMeta(envelope.meta); +} + +export function validateResponse(envelope) { + if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) { + throw new Error("response must be an object"); + } + if (envelope.jsonrpc !== JSON_RPC_VERSION) { + throw new Error(`jsonrpc must be ${JSON_RPC_VERSION}`); + } + if (envelope.id === undefined || envelope.id === null) { + throw new Error("id is required"); + } + const hasResult = Object.hasOwn(envelope, "result"); + const hasError = Object.hasOwn(envelope, "error"); + if (hasResult === hasError) { + throw new Error("response must contain exactly one of result or error"); + } + if (hasError) { + validateRpcError(envelope.error); + } + validateMeta(envelope.meta); +} + +export function validateRpcError(error) { + if (!error || typeof error !== "object" || Array.isArray(error)) { + throw new Error("error must be an object"); + } + if (!Number.isInteger(error.code)) { + throw new Error("error.code must be an integer"); + } + if (!error.message || typeof error.message !== "string") { + throw new Error("error.message is required"); + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..5aa4d508 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "hwlab", + "version": "0.0.0-l0", + "private": true, + "type": "module", + "scripts": { + "validate": "node scripts/validate-contract.mjs", + "check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs" + } +} diff --git a/protocol/README.md b/protocol/README.md new file mode 100644 index 00000000..4b42732d --- /dev/null +++ b/protocol/README.md @@ -0,0 +1,42 @@ +# HWLAB Protocol Contract + +The L0 protocol contract defines the common language between cloud services, +gateway services, simulated boxes, agent workers, CLI tooling, and future +hardware integrations. + +All MVP implementations must preserve these invariants: + +- Use JSON-RPC 2.0 envelopes for command-style API calls unless a later + service contract explicitly narrows the transport. +- Emit auditable records for user, system, gateway, worker, and hardware + actions that mutate state or produce evidence. +- Describe box hardware through capabilities, resource state, and wiring + constraints instead of service-specific ad hoc fields. +- Treat DEV as the only MVP acceptance environment. +- Keep service identities within the frozen service ID list in the root + README. + +## Frozen Tables + +The database contract is intentionally named here before migrations exist so +parallel MVP work shares stable terminology: + +- `projects` +- `gateway_sessions` +- `box_resources` +- `box_capabilities` +- `wiring_configs` +- `patch_panel_status` +- `hardware_operations` +- `audit_events` +- `agent_sessions` +- `worker_sessions` +- `agent_trace_events` +- `evidence_records` + +## Schema Status + +Schemas in `protocol/schemas` are draft-2020-12 JSON Schemas. They define L0 +field names, identities, relationships, timestamps, and state enums. Later +service work may tighten validation, but must not silently rename these L0 +contract fields. diff --git a/protocol/audit.md b/protocol/audit.md new file mode 100644 index 00000000..492abed3 --- /dev/null +++ b/protocol/audit.md @@ -0,0 +1,26 @@ +# Audit Fields + +Mutating calls and evidence-producing workflows must carry audit metadata from +the caller through to persisted `audit_events`. + +## Required Fields + +- `auditId`: stable audit event identifier. +- `traceId`: cross-service trace identifier. +- `actorType`: `user`, `service`, `agent`, `worker`, or `system`. +- `actorId`: stable identity for the actor. +- `action`: dotted action name such as `hardware.operation.requested`. +- `targetType`: resource family affected by the action. +- `targetId`: stable target identifier. +- `serviceId`: frozen HWLAB service ID emitting the event. +- `environment`: `dev` for MVP. +- `occurredAt`: RFC 3339 timestamp. + +## Recommended Fields + +- `projectId`: project scope for the action. +- `gatewaySessionId`: gateway session participating in the action. +- `workerSessionId`: worker session participating in the action. +- `operationId`: hardware operation connected to the action. +- `outcome`: `accepted`, `succeeded`, `failed`, `rejected`, or `canceled`. +- `reason`: short machine-readable reason for rejection or failure. diff --git a/protocol/capabilities.md b/protocol/capabilities.md new file mode 100644 index 00000000..c40bb215 --- /dev/null +++ b/protocol/capabilities.md @@ -0,0 +1,44 @@ +# Capability Model + +Box hardware is described with resources and capabilities. + +## Resource + +A box resource is a physical or simulated component that can be addressed by +the gateway: + +- board +- power supply +- serial port +- network port +- relay +- sensor +- simulator endpoint + +Resources carry lifecycle state such as `available`, `reserved`, `running`, +`maintenance`, or `offline`. + +## Capability + +A capability describes an action or measurement exposed by a resource. Examples: + +- `power.cycle` +- `power.set_voltage` +- `serial.console` +- `network.link` +- `firmware.flash` +- `sensor.read` + +Capabilities declare: + +- direction: `input`, `output`, or `bidirectional` +- value type: `boolean`, `integer`, `number`, `string`, `object`, or `binary` +- optional unit +- constraints for allowed values or ranges +- whether the operation mutates hardware state + +## Operation Binding + +Hardware operations reference capabilities by `capabilityId` and resources by +`resourceId`. Implementations must not infer a capability from a free-form +operation name when a capability record exists. diff --git a/protocol/errors.md b/protocol/errors.md new file mode 100644 index 00000000..24d52a56 --- /dev/null +++ b/protocol/errors.md @@ -0,0 +1,24 @@ +# Error Codes + +HWLAB uses JSON-RPC standard errors plus a reserved HWLAB server-error range. + +| Code | Name | Meaning | +| --- | --- | --- | +| `-32700` | `parse_error` | Invalid JSON payload. | +| `-32600` | `invalid_request` | Invalid JSON-RPC envelope. | +| `-32601` | `method_not_found` | Method is unknown to the target service. | +| `-32602` | `invalid_params` | Method parameters failed validation. | +| `-32603` | `internal_error` | Unexpected service failure. | +| `-32000` | `hwlab_unknown` | Unclassified HWLAB runtime error. | +| `-32010` | `project_not_found` | Project ID is not known or not visible. | +| `-32011` | `session_not_found` | Gateway, agent, or worker session is missing. | +| `-32020` | `capability_unavailable` | Requested hardware capability is unavailable. | +| `-32021` | `resource_locked` | Box resource is already reserved or running. | +| `-32022` | `wiring_invalid` | Wiring config violates topology constraints. | +| `-32023` | `operation_rejected` | Hardware operation cannot be accepted. | +| `-32030` | `evidence_missing` | Required evidence record was not produced. | +| `-32040` | `audit_required` | A mutating action was attempted without audit metadata. | + +Error responses must include `error.message`. `error.data` should carry stable +machine-readable fields such as `projectId`, `sessionId`, `resourceId`, or +`operationId` when available. diff --git a/protocol/json-rpc.md b/protocol/json-rpc.md new file mode 100644 index 00000000..1861d897 --- /dev/null +++ b/protocol/json-rpc.md @@ -0,0 +1,70 @@ +# JSON-RPC Envelope + +HWLAB command-style APIs use JSON-RPC 2.0 envelopes. + +## Request + +```json +{ + "jsonrpc": "2.0", + "id": "req_01J00000000000000000000000", + "method": "hardware.operation.request", + "params": { + "projectId": "prj_01J00000000000000000000000" + }, + "meta": { + "traceId": "trc_01J00000000000000000000000", + "actorId": "usr_01J00000000000000000000000", + "serviceId": "hwlab-cloud-api", + "environment": "dev" + } +} +``` + +## Response + +```json +{ + "jsonrpc": "2.0", + "id": "req_01J00000000000000000000000", + "result": { + "accepted": true + }, + "meta": { + "traceId": "trc_01J00000000000000000000000", + "serviceId": "hwlab-agent-mgr", + "environment": "dev" + } +} +``` + +## Error + +```json +{ + "jsonrpc": "2.0", + "id": "req_01J00000000000000000000000", + "error": { + "code": -32020, + "message": "Requested hardware capability is unavailable", + "data": { + "capabilityId": "cap_01J00000000000000000000000" + } + }, + "meta": { + "traceId": "trc_01J00000000000000000000000", + "serviceId": "hwlab-gateway", + "environment": "dev" + } +} +``` + +## Envelope Rules + +- `jsonrpc` is always `2.0`. +- `id` is required for request/response flows and must be echoed unchanged. +- `method` names use dotted lower-case namespaces. +- `params` must be an object when present. +- A response contains exactly one of `result` or `error`. +- `meta.traceId` is required for all cross-service calls. +- `meta.environment` is `dev` for MVP. diff --git a/protocol/mvp-e2e-contract.md b/protocol/mvp-e2e-contract.md new file mode 100644 index 00000000..e0426188 --- /dev/null +++ b/protocol/mvp-e2e-contract.md @@ -0,0 +1,31 @@ +# MVP E2E Contract + +The L0 e2e contract is a sequence contract for future MVP tests. It is not a +heavy e2e test suite. + +## DEV Endpoint + +All MVP acceptance traffic targets: + +```text +http://74.48.78.17:6667 +``` + +## Minimal Flow + +1. Create or select a project. +2. Start a gateway session for `hwlab-gateway` or `hwlab-gateway-simu`. +3. Register box resources and capabilities. +4. Apply a wiring config that passes topology validation. +5. Start an agent session and worker session. +6. Request a hardware operation through JSON-RPC. +7. Emit trace events as the operation is accepted, dispatched, executed, and + completed. +8. Persist an evidence record for the operation. +9. Emit audit events for mutating steps and final outcome. + +## Acceptance Boundary + +For L0, acceptance is limited to parseable schemas, stable contract names, and +minimal in-process validation. PROD deployment, service restarts, and full e2e +execution are explicitly out of scope. diff --git a/protocol/schemas/agent-session.schema.json b/protocol/schemas/agent-session.schema.json new file mode 100644 index 00000000..852040f1 --- /dev/null +++ b/protocol/schemas/agent-session.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/agent-session.schema.json", + "title": "HWLAB Agent Session", + "type": "object", + "required": [ + "agentSessionId", + "projectId", + "serviceId", + "status", + "environment", + "startedAt", + "updatedAt" + ], + "properties": { + "agentSessionId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "serviceId": { + "const": "hwlab-agent-mgr" + }, + "status": { + "type": "string", + "enum": ["starting", "active", "waiting", "completed", "failed", "canceled"] + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "requestedBy": { + "$ref": "common.json#/$defs/id" + }, + "goal": { + "type": "string", + "maxLength": 4000 + }, + "startedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "completedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "updatedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "metadata": { + "$ref": "common.json#/$defs/jsonObject" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/audit-event.schema.json b/protocol/schemas/audit-event.schema.json new file mode 100644 index 00000000..3ce582b1 --- /dev/null +++ b/protocol/schemas/audit-event.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/audit-event.schema.json", + "title": "HWLAB Audit Event", + "type": "object", + "required": [ + "auditId", + "traceId", + "actorType", + "actorId", + "action", + "targetType", + "targetId", + "serviceId", + "environment", + "occurredAt" + ], + "properties": { + "auditId": { + "$ref": "common.json#/$defs/id" + }, + "traceId": { + "$ref": "common.json#/$defs/id" + }, + "actorType": { + "$ref": "common.json#/$defs/actorType" + }, + "actorId": { + "$ref": "common.json#/$defs/id" + }, + "action": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$" + }, + "targetType": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "targetId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "gatewaySessionId": { + "$ref": "common.json#/$defs/id" + }, + "workerSessionId": { + "$ref": "common.json#/$defs/id" + }, + "operationId": { + "$ref": "common.json#/$defs/id" + }, + "serviceId": { + "$ref": "common.json#/$defs/serviceId" + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "outcome": { + "$ref": "common.json#/$defs/auditOutcome" + }, + "reason": { + "type": "string", + "maxLength": 512 + }, + "metadata": { + "$ref": "common.json#/$defs/jsonObject" + }, + "occurredAt": { + "$ref": "common.json#/$defs/timestamp" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/box-capability.schema.json b/protocol/schemas/box-capability.schema.json new file mode 100644 index 00000000..71b8eeaa --- /dev/null +++ b/protocol/schemas/box-capability.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/box-capability.schema.json", + "title": "HWLAB Box Capability", + "type": "object", + "required": [ + "capabilityId", + "resourceId", + "projectId", + "name", + "direction", + "valueType", + "mutatesState", + "createdAt", + "updatedAt" + ], + "properties": { + "capabilityId": { + "$ref": "common.json#/$defs/id" + }, + "resourceId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "name": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$" + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "direction": { + "type": "string", + "enum": ["input", "output", "bidirectional"] + }, + "valueType": { + "type": "string", + "enum": ["boolean", "integer", "number", "string", "object", "binary"] + }, + "unit": { + "type": "string", + "maxLength": 32 + }, + "constraints": { + "$ref": "common.json#/$defs/jsonObject" + }, + "mutatesState": { + "type": "boolean" + }, + "createdAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "updatedAt": { + "$ref": "common.json#/$defs/timestamp" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/box-resource.schema.json b/protocol/schemas/box-resource.schema.json new file mode 100644 index 00000000..ccf47d59 --- /dev/null +++ b/protocol/schemas/box-resource.schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/box-resource.schema.json", + "title": "HWLAB Box Resource", + "type": "object", + "required": [ + "resourceId", + "projectId", + "gatewaySessionId", + "boxId", + "resourceType", + "state", + "environment", + "createdAt", + "updatedAt" + ], + "properties": { + "resourceId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "gatewaySessionId": { + "$ref": "common.json#/$defs/id" + }, + "boxId": { + "$ref": "common.json#/$defs/id" + }, + "resourceType": { + "type": "string", + "enum": [ + "board", + "power_supply", + "serial_port", + "network_port", + "relay", + "sensor", + "simulator_endpoint" + ] + }, + "name": { + "type": "string", + "maxLength": 160 + }, + "state": { + "$ref": "common.json#/$defs/resourceState" + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "metadata": { + "$ref": "common.json#/$defs/jsonObject" + }, + "createdAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "updatedAt": { + "$ref": "common.json#/$defs/timestamp" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/common.json b/protocol/schemas/common.json new file mode 100644 index 00000000..f171f769 --- /dev/null +++ b/protocol/schemas/common.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/common.json", + "title": "HWLAB Common Schema Definitions", + "type": "object", + "$defs": { + "id": { + "type": "string", + "minLength": 3, + "maxLength": 128, + "pattern": "^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "environment": { + "type": "string", + "enum": ["dev"] + }, + "serviceId": { + "type": "string", + "enum": [ + "hwlab-cloud-api", + "hwlab-cloud-web", + "hwlab-agent-mgr", + "hwlab-agent-worker", + "hwlab-gateway", + "hwlab-gateway-simu", + "hwlab-box-simu", + "hwlab-patch-panel", + "hwlab-router", + "hwlab-tunnel-client", + "hwlab-edge-proxy", + "hwlab-cli", + "hwlab-agent-skills" + ] + }, + "lifecycleStatus": { + "type": "string", + "enum": ["pending", "active", "inactive", "failed", "deleted"] + }, + "resourceState": { + "type": "string", + "enum": ["available", "reserved", "running", "maintenance", "offline"] + }, + "operationStatus": { + "type": "string", + "enum": [ + "requested", + "accepted", + "queued", + "running", + "succeeded", + "failed", + "rejected", + "canceled", + "timed_out" + ] + }, + "actorType": { + "type": "string", + "enum": ["user", "service", "agent", "worker", "system"] + }, + "auditOutcome": { + "type": "string", + "enum": ["accepted", "succeeded", "failed", "rejected", "canceled"] + }, + "traceLevel": { + "type": "string", + "enum": ["debug", "info", "warn", "error"] + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string", + "maxLength": 256 + } + }, + "jsonObject": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/protocol/schemas/evidence-record.schema.json b/protocol/schemas/evidence-record.schema.json new file mode 100644 index 00000000..bc4188ed --- /dev/null +++ b/protocol/schemas/evidence-record.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/evidence-record.schema.json", + "title": "HWLAB Evidence Record", + "type": "object", + "required": [ + "evidenceId", + "projectId", + "operationId", + "kind", + "uri", + "sha256", + "serviceId", + "environment", + "createdAt" + ], + "properties": { + "evidenceId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "operationId": { + "$ref": "common.json#/$defs/id" + }, + "agentSessionId": { + "$ref": "common.json#/$defs/id" + }, + "workerSessionId": { + "$ref": "common.json#/$defs/id" + }, + "kind": { + "type": "string", + "enum": ["log", "screenshot", "measurement", "artifact", "report", "trace"] + }, + "uri": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + }, + "mimeType": { + "type": "string", + "maxLength": 128 + }, + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$" + }, + "sizeBytes": { + "type": "integer", + "minimum": 0 + }, + "serviceId": { + "$ref": "common.json#/$defs/serviceId" + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "metadata": { + "$ref": "common.json#/$defs/jsonObject" + }, + "createdAt": { + "$ref": "common.json#/$defs/timestamp" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/gateway-session.schema.json b/protocol/schemas/gateway-session.schema.json new file mode 100644 index 00000000..647200fe --- /dev/null +++ b/protocol/schemas/gateway-session.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/gateway-session.schema.json", + "title": "HWLAB Gateway Session", + "type": "object", + "required": [ + "gatewaySessionId", + "projectId", + "serviceId", + "gatewayId", + "status", + "environment", + "startedAt" + ], + "properties": { + "gatewaySessionId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "serviceId": { + "enum": ["hwlab-gateway", "hwlab-gateway-simu"] + }, + "gatewayId": { + "$ref": "common.json#/$defs/id" + }, + "endpoint": { + "type": "string", + "format": "uri" + }, + "status": { + "type": "string", + "enum": ["starting", "connected", "degraded", "disconnected", "stopped"] + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "startedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "lastSeenAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "stoppedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "labels": { + "$ref": "common.json#/$defs/labels" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/hardware-operation.schema.json b/protocol/schemas/hardware-operation.schema.json new file mode 100644 index 00000000..6383286d --- /dev/null +++ b/protocol/schemas/hardware-operation.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/hardware-operation.schema.json", + "title": "HWLAB Hardware Operation", + "type": "object", + "required": [ + "operationId", + "projectId", + "gatewaySessionId", + "resourceId", + "capabilityId", + "requestedBy", + "status", + "environment", + "requestedAt", + "updatedAt" + ], + "properties": { + "operationId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "gatewaySessionId": { + "$ref": "common.json#/$defs/id" + }, + "agentSessionId": { + "$ref": "common.json#/$defs/id" + }, + "workerSessionId": { + "$ref": "common.json#/$defs/id" + }, + "resourceId": { + "$ref": "common.json#/$defs/id" + }, + "capabilityId": { + "$ref": "common.json#/$defs/id" + }, + "requestedBy": { + "$ref": "common.json#/$defs/id" + }, + "input": true, + "output": true, + "status": { + "$ref": "common.json#/$defs/operationStatus" + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "requestedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "startedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "completedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "updatedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "error": { + "$ref": "common.json#/$defs/jsonObject" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/json-rpc-envelope.schema.json b/protocol/schemas/json-rpc-envelope.schema.json new file mode 100644 index 00000000..f7df8b37 --- /dev/null +++ b/protocol/schemas/json-rpc-envelope.schema.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/json-rpc-envelope.schema.json", + "title": "HWLAB JSON-RPC Envelope", + "oneOf": [ + { + "type": "object", + "required": ["jsonrpc", "id", "method", "meta"], + "properties": { + "jsonrpc": { + "const": "2.0" + }, + "id": { + "type": ["string", "number"] + }, + "method": { + "type": "string", + "pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9_]*)+$" + }, + "params": { + "type": "object", + "additionalProperties": true + }, + "meta": { + "$ref": "#/$defs/meta" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["jsonrpc", "id", "result", "meta"], + "properties": { + "jsonrpc": { + "const": "2.0" + }, + "id": { + "type": ["string", "number"] + }, + "result": true, + "meta": { + "$ref": "#/$defs/meta" + } + }, + "not": { + "required": ["error"] + }, + "additionalProperties": false + }, + { + "type": "object", + "required": ["jsonrpc", "id", "error", "meta"], + "properties": { + "jsonrpc": { + "const": "2.0" + }, + "id": { + "type": ["string", "number", "null"] + }, + "error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string", + "minLength": 1 + }, + "data": true + }, + "additionalProperties": false + }, + "meta": { + "$ref": "#/$defs/meta" + } + }, + "not": { + "required": ["result"] + }, + "additionalProperties": false + } + ], + "$defs": { + "meta": { + "type": "object", + "required": ["traceId", "serviceId", "environment"], + "properties": { + "traceId": { + "$ref": "common.json#/$defs/id" + }, + "actorId": { + "$ref": "common.json#/$defs/id" + }, + "serviceId": { + "$ref": "common.json#/$defs/serviceId" + }, + "environment": { + "$ref": "common.json#/$defs/environment" + } + }, + "additionalProperties": true + } + } +} diff --git a/protocol/schemas/patch-panel-status.schema.json b/protocol/schemas/patch-panel-status.schema.json new file mode 100644 index 00000000..83d43ea1 --- /dev/null +++ b/protocol/schemas/patch-panel-status.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/patch-panel-status.schema.json", + "title": "HWLAB Patch Panel Status", + "type": "object", + "required": [ + "patchPanelStatusId", + "projectId", + "gatewaySessionId", + "serviceId", + "state", + "environment", + "observedAt" + ], + "properties": { + "patchPanelStatusId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "gatewaySessionId": { + "$ref": "common.json#/$defs/id" + }, + "wiringConfigId": { + "$ref": "common.json#/$defs/id" + }, + "serviceId": { + "const": "hwlab-patch-panel" + }, + "state": { + "type": "string", + "enum": ["unknown", "ready", "applying", "active", "faulted", "offline"] + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "activeConnections": { + "type": "array", + "items": { + "type": "object", + "required": ["fromResourceId", "fromPort", "toResourceId", "toPort"], + "properties": { + "fromResourceId": { + "$ref": "common.json#/$defs/id" + }, + "fromPort": { + "type": "string" + }, + "toResourceId": { + "$ref": "common.json#/$defs/id" + }, + "toPort": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "observedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "metadata": { + "$ref": "common.json#/$defs/jsonObject" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/project.schema.json b/protocol/schemas/project.schema.json new file mode 100644 index 00000000..fc612fab --- /dev/null +++ b/protocol/schemas/project.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/project.schema.json", + "title": "HWLAB Project", + "type": "object", + "required": ["projectId", "name", "status", "environment", "createdAt", "updatedAt"], + "properties": { + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "status": { + "$ref": "common.json#/$defs/lifecycleStatus" + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "labels": { + "$ref": "common.json#/$defs/labels" + }, + "createdBy": { + "$ref": "common.json#/$defs/id" + }, + "createdAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "updatedAt": { + "$ref": "common.json#/$defs/timestamp" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/trace-event.schema.json b/protocol/schemas/trace-event.schema.json new file mode 100644 index 00000000..ae52418e --- /dev/null +++ b/protocol/schemas/trace-event.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/trace-event.schema.json", + "title": "HWLAB Agent Trace Event", + "type": "object", + "required": [ + "traceEventId", + "traceId", + "projectId", + "serviceId", + "level", + "message", + "environment", + "occurredAt" + ], + "properties": { + "traceEventId": { + "$ref": "common.json#/$defs/id" + }, + "traceId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "agentSessionId": { + "$ref": "common.json#/$defs/id" + }, + "workerSessionId": { + "$ref": "common.json#/$defs/id" + }, + "operationId": { + "$ref": "common.json#/$defs/id" + }, + "serviceId": { + "$ref": "common.json#/$defs/serviceId" + }, + "level": { + "$ref": "common.json#/$defs/traceLevel" + }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "metadata": { + "$ref": "common.json#/$defs/jsonObject" + }, + "occurredAt": { + "$ref": "common.json#/$defs/timestamp" + } + }, + "additionalProperties": false +} diff --git a/protocol/schemas/wiring-config.schema.json b/protocol/schemas/wiring-config.schema.json new file mode 100644 index 00000000..5bd7cfbf --- /dev/null +++ b/protocol/schemas/wiring-config.schema.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/wiring-config.schema.json", + "title": "HWLAB Wiring Config", + "type": "object", + "required": [ + "wiringConfigId", + "projectId", + "gatewaySessionId", + "status", + "connections", + "createdAt", + "updatedAt" + ], + "properties": { + "wiringConfigId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "gatewaySessionId": { + "$ref": "common.json#/$defs/id" + }, + "name": { + "type": "string", + "maxLength": 160 + }, + "status": { + "type": "string", + "enum": ["draft", "valid", "active", "rejected", "retired"] + }, + "connections": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/connection" + } + }, + "constraints": { + "$ref": "common.json#/$defs/jsonObject" + }, + "createdAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "updatedAt": { + "$ref": "common.json#/$defs/timestamp" + } + }, + "additionalProperties": false, + "$defs": { + "endpoint": { + "type": "object", + "required": ["resourceId", "port"], + "properties": { + "resourceId": { + "$ref": "common.json#/$defs/id" + }, + "port": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "capabilityId": { + "$ref": "common.json#/$defs/id" + } + }, + "additionalProperties": false + }, + "connection": { + "type": "object", + "required": ["from", "to"], + "properties": { + "from": { + "$ref": "#/$defs/endpoint" + }, + "to": { + "$ref": "#/$defs/endpoint" + }, + "mode": { + "type": "string", + "enum": ["exclusive", "shared", "monitor"] + } + }, + "additionalProperties": false + } + } +} diff --git a/protocol/schemas/worker-session.schema.json b/protocol/schemas/worker-session.schema.json new file mode 100644 index 00000000..967f9e67 --- /dev/null +++ b/protocol/schemas/worker-session.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hwlab.pikastech.local/protocol/schemas/worker-session.schema.json", + "title": "HWLAB Worker Session", + "type": "object", + "required": [ + "workerSessionId", + "agentSessionId", + "projectId", + "serviceId", + "status", + "environment", + "startedAt", + "updatedAt" + ], + "properties": { + "workerSessionId": { + "$ref": "common.json#/$defs/id" + }, + "agentSessionId": { + "$ref": "common.json#/$defs/id" + }, + "projectId": { + "$ref": "common.json#/$defs/id" + }, + "serviceId": { + "const": "hwlab-agent-worker" + }, + "gatewaySessionId": { + "$ref": "common.json#/$defs/id" + }, + "status": { + "type": "string", + "enum": ["starting", "active", "blocked", "completed", "failed", "canceled"] + }, + "environment": { + "$ref": "common.json#/$defs/environment" + }, + "startedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "completedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "updatedAt": { + "$ref": "common.json#/$defs/timestamp" + }, + "metadata": { + "$ref": "common.json#/$defs/jsonObject" + } + }, + "additionalProperties": false +} diff --git a/protocol/topology.md b/protocol/topology.md new file mode 100644 index 00000000..26332a8c --- /dev/null +++ b/protocol/topology.md @@ -0,0 +1,32 @@ +# Topology Constraints + +The MVP topology separates cloud, gateway, worker, simulated box, patch panel, +router, tunnel, and edge proxy responsibilities. + +## Constraints + +- `hwlab-cloud-api` is the cloud API boundary for DEV. +- `hwlab-cloud-web` consumes cloud APIs and must not control hardware directly. +- `hwlab-agent-mgr` schedules agent work and creates worker sessions. +- `hwlab-agent-worker` executes scoped work through declared gateway and box + capabilities. +- `hwlab-gateway` is the hardware control boundary. +- `hwlab-gateway-simu` and `hwlab-box-simu` may simulate gateway and box + behavior for MVP validation. +- `hwlab-patch-panel` owns patch panel state. +- `hwlab-router`, `hwlab-tunnel-client`, and `hwlab-edge-proxy` own network + reachability concerns. +- `hwlab-cli` is a client and must not bypass service contracts. +- `hwlab-agent-skills` contains reusable agent skill assets. + +## Wiring + +Wiring configs describe allowed connections between resource ports. A valid +wiring config must: + +- belong to a project +- reference known gateway and box resources +- declare endpoints explicitly +- avoid cycles unless a later capability explicitly allows them +- represent one active config per resource group unless the patch panel reports + an override diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/scripts/.gitkeep @@ -0,0 +1 @@ + diff --git a/scripts/bootstrap-skills.sh b/scripts/bootstrap-skills.sh new file mode 100755 index 00000000..2a4165bd --- /dev/null +++ b/scripts/bootstrap-skills.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +set -eu + +repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +mkdir -p "$repo_root/skills" + +echo "HWLAB skills directory ready: $repo_root/skills" diff --git a/scripts/validate-contract.mjs b/scripts/validate-contract.mjs new file mode 100644 index 00000000..a1eed22e --- /dev/null +++ b/scripts/validate-contract.mjs @@ -0,0 +1,106 @@ +#!/usr/bin/env node +import assert from "node:assert/strict"; +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + ENVIRONMENT_DEV, + SERVICE_IDS, + TABLES, + validateRequest, + validateResponse +} from "../internal/protocol/index.mjs"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); + +async function parseJSONFiles(dir) { + const entries = await readdir(path.join(repoRoot, dir), { withFileTypes: true }); + const parsed = []; + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith(".json")) { + continue; + } + const relativePath = path.join(dir, entry.name); + const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); + const doc = JSON.parse(raw); + assert.ok(doc.$schema, `${relativePath} missing $schema`); + assert.ok(doc.$id, `${relativePath} missing $id`); + parsed.push({ relativePath, doc }); + } + return parsed; +} + +function assertUnique(name, values) { + assert.equal(new Set(values).size, values.length, `${name} must be unique`); +} + +function assertCommonSchema(commonSchema) { + const schemaServiceIds = commonSchema.$defs.serviceId.enum; + assert.deepEqual(schemaServiceIds, SERVICE_IDS, "common service ids must match runtime constants"); + assert.deepEqual(commonSchema.$defs.environment.enum, [ENVIRONMENT_DEV]); +} + +function assertDeploySchema(deploySchema) { + const serviceIds = deploySchema.$defs.service.properties.serviceId.enum; + assert.deepEqual(serviceIds, SERVICE_IDS, "deploy service ids must match runtime constants"); + assert.equal(deploySchema.properties.endpoint.const, "http://74.48.78.17:6667"); +} + +function assertEnvelopeValidation() { + validateRequest({ + jsonrpc: "2.0", + id: "req_01J00000000000000000000000", + method: "hardware.operation.request", + params: { + projectId: "prj_01J00000000000000000000000" + }, + meta: { + traceId: "trc_01J00000000000000000000000", + serviceId: "hwlab-cloud-api", + environment: "dev" + } + }); + + validateResponse({ + jsonrpc: "2.0", + id: "req_01J00000000000000000000000", + result: { + accepted: true + }, + meta: { + traceId: "trc_01J00000000000000000000000", + serviceId: "hwlab-agent-mgr", + environment: "dev" + } + }); + + assert.throws( + () => + validateResponse({ + jsonrpc: "2.0", + id: "req_01J00000000000000000000000", + result: {}, + error: { code: -32000, message: "bad" }, + meta: { + traceId: "trc_01J00000000000000000000000", + serviceId: "hwlab-agent-mgr", + environment: "dev" + } + }), + /exactly one/ + ); +} + +const schemas = await parseJSONFiles("protocol/schemas"); +const deploySchemas = await parseJSONFiles("deploy"); +const docsByName = new Map([...schemas, ...deploySchemas].map((item) => [path.basename(item.relativePath), item.doc])); + +assert.ok(schemas.length >= 13, "expected protocol schemas"); +assert.ok(TABLES.includes("patch_panel_status"), "frozen tables must include patch_panel_status"); +assertUnique("service ids", SERVICE_IDS); +assertUnique("tables", TABLES); +assertCommonSchema(docsByName.get("common.json")); +assertDeploySchema(docsByName.get("deploy.schema.json")); +assertEnvelopeValidation(); + +console.log(`validated ${schemas.length + deploySchemas.length} JSON files and ${SERVICE_IDS.length} service ids`); diff --git a/scripts/worker-entrypoint.sh b/scripts/worker-entrypoint.sh new file mode 100755 index 00000000..2fb7afa7 --- /dev/null +++ b/scripts/worker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +set -eu + +echo "HWLAB worker entrypoint placeholder" +exec "$@" diff --git a/skills/.gitkeep b/skills/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/skills/.gitkeep @@ -0,0 +1 @@ + diff --git a/tools/hwlab-cli/.gitkeep b/tools/hwlab-cli/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tools/hwlab-cli/.gitkeep @@ -0,0 +1 @@ + diff --git a/web/hwlab-cloud-web/.gitkeep b/web/hwlab-cloud-web/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/web/hwlab-cloud-web/.gitkeep @@ -0,0 +1 @@ +