chore: establish hwlab l0 contract skeleton

This commit is contained in:
HWLAB Code Queue
2026-05-21 14:27:53 +00:00
commit 8f3069035f
41 changed files with 1732 additions and 0 deletions
+16
View File
@@ -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
+35
View File
@@ -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
+69
View File
@@ -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
```
+1
View File
@@ -0,0 +1 @@
+7
View File
@@ -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.
+132
View File
@@ -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
}
}
}
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+127
View File
@@ -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");
}
}
+10
View File
@@ -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"
}
}
+42
View File
@@ -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.
+26
View File
@@ -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.
+44
View File
@@ -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.
+24
View File
@@ -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.
+70
View File
@@ -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.
+31
View File
@@ -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.
@@ -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
}
+76
View File
@@ -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
}
@@ -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
}
+63
View File
@@ -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
}
+85
View File
@@ -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
}
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}
}
}
@@ -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
}
+40
View File
@@ -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
}
+57
View File
@@ -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
}
@@ -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
}
}
}
@@ -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
}
+32
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
+7
View File
@@ -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"
+106
View File
@@ -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`);
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
set -eu
echo "HWLAB worker entrypoint placeholder"
exec "$@"
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@