diff --git a/.github/workflows/workspace-ops.yml b/.github/workflows/workspace-ops.yml new file mode 100644 index 0000000..fca8b1c --- /dev/null +++ b/.github/workflows/workspace-ops.yml @@ -0,0 +1,34 @@ +name: workspace-ops + +on: + pull_request: + paths: + - 'schemas/workspace-operation-state.schema.json' + - 'schemas/operation-command.schema.json' + - 'schemas/diagnostics-export.schema.json' + - 'examples/workspace-ops/**' + - 'scripts/validate_workspace_ops.py' + - '.github/workflows/workspace-ops.yml' + push: + branches: + - main + - copilot/expose-local-workspace-operations + paths: + - 'schemas/workspace-operation-state.schema.json' + - 'schemas/operation-command.schema.json' + - 'schemas/diagnostics-export.schema.json' + - 'examples/workspace-ops/**' + - 'scripts/validate_workspace_ops.py' + - '.github/workflows/workspace-ops.yml' + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - run: python -m pip install jsonschema + - run: python scripts/validate_workspace_ops.py diff --git a/docs/workspace-operations.md b/docs/workspace-operations.md new file mode 100644 index 0000000..09a8b37 --- /dev/null +++ b/docs/workspace-operations.md @@ -0,0 +1,109 @@ +# Workspace Operations Contract + +Status: initial contract-capture slice. + +`sourceos-shell` is the SourceOS shell and user-interface plane. This document +defines how the shell surfaces and controls local Workspace Operations — sync, +terminal, browser, local agent-machine, model carry, and workstation state — +without becoming a runtime or policy authority. + +## Boundary + +The shell is a **control surface and projection layer**. It must not create +hidden mutations or policy decisions outside the Operation Plane. + +- The shell reads operation/task state from contracts supplied by the Operation + Plane; it does not author that state. +- The shell routes actions through structured `OperationCommand` records; it + does not execute integrations directly. +- The shell applies redaction policy before emitting diagnostics exports; it + does not decide that policy. + +## Required surfaces + +| Surface | Description | +|---|---| +| Local Operation Tray projection | `operationTrayProjection` in `WorkspaceOperationState` | +| Local Operation Inspector projection | `operationInspectorProjection` in `WorkspaceOperationState` | +| File availability states | `fileAvailabilityState`: `local`, `remote`, `syncing`, `conflicted`, `quarantined` | +| Device identity / trust profile | `deviceIdentity` in `WorkspaceOperationState` | +| Sync status from `sourceos-syncd` | `syncStatus` in `WorkspaceOperationState` | +| TurtleTerm entry point | `integrationEntryPoints.turtleTerm` + `OperationCommand` class `turtleterm-open` / `turtleterm-close` | +| BearBrowser entry point | `integrationEntryPoints.bearBrowser` + `OperationCommand` class `bearbrowser-open` / `bearbrowser-close` | +| Agent-machine entry point | `integrationEntryPoints.agentMachine` + `OperationCommand` class `agentmachine-activate` / `agentmachine-deactivate` | +| Redacted diagnostics export | `DiagnosticsExport` schema with `redactionApplied: true` | + +## Operation states + +The shell must be able to distinguish all of the following `operationState` +values as reported by the Operation Plane: + +| State | Meaning | +|---|---| +| `stored` | Operation received and stored; not yet admitted | +| `quarantined` | Operation held pending policy review | +| `admitted` | Operation admitted to the active workspace | +| `activated` | Operation is running | +| `syncing` | Operation is being synchronised with remote | +| `conflicted` | Operation has a merge or sync conflict | +| `failed` | Operation terminated with an error | + +## File availability states + +| State | Meaning | +|---|---| +| `local` | File is locally available | +| `remote` | File exists only on remote | +| `syncing` | File is being synchronised | +| `conflicted` | File has a sync or merge conflict | +| `quarantined` | File is held pending policy review | + +## OperationCommand routing + +Shell actions route through `OperationCommand` records +(`schemas/operation-command.schema.json`). The shell emits a command record +with a `commandClass` such as `turtleterm-open`, `sync-request`, or +`diagnostics-export`; the Operation Plane acts on it. + +Supported command classes: + +- `turtleterm-open` / `turtleterm-close` +- `bearbrowser-open` / `bearbrowser-close` +- `agentmachine-activate` / `agentmachine-deactivate` +- `sync-request` / `sync-cancel` +- `diagnostics-export` +- `model-carry-initiate` +- `workstation-state-query` + +All commands support `isDryRun: true` for plan/preview without side effects. + +## Diagnostics export + +The `DiagnosticsExport` schema (`schemas/diagnostics-export.schema.json`) +enforces: + +- `redactionApplied: true` — redaction is always applied before export. +- `contentCaptureEnabled: false` — inline content capture is always disabled. +- `payloadMode` — must be `metadata-only`, `summary`, `ref-only`, or + `redacted`. +- `policyDecisionRefs` — non-empty; Policy Fabric decision refs are required. + +## Required integrations + +- `SourceOS-Linux/sourceos-spec#87` +- `SociOS-Linux/workstation-contracts#28` +- `SourceOS-Linux/sourceos-syncd#3` +- `SourceOS-Linux/sourceos-devtools#19` +- `SourceOS-Linux/BearBrowser#20` +- `SourceOS-Linux/agent-machine#18` +- `SocioProphet/prophet-core-contracts#1` +- `SocioProphet/sociosphere#259` + +## Non-goals + +- Shell does not own Policy Fabric or Operation Plane runtime logic. +- Shell does not create hidden mutations outside the Operation Plane. +- Shell does not enable content capture. +- Shell does not export diagnostics without Policy Fabric decision refs. +- Shell does not activate agent-machine integrations without Agent Registry + authority. diff --git a/examples/workspace-ops/diagnostics-export.metadata-only.example.json b/examples/workspace-ops/diagnostics-export.metadata-only.example.json new file mode 100644 index 0000000..92fbe2b --- /dev/null +++ b/examples/workspace-ops/diagnostics-export.metadata-only.example.json @@ -0,0 +1,24 @@ +{ + "exportId": "urn:srcos:diag-export:summary-demo-0001", + "specVersion": "0.1.0", + "exportedAt": "2026-05-07T06:11:00Z", + "actorRef": "urn:srcos:subject:operator-demo", + "sessionRef": "urn:srcos:shell-session:demo-0002", + "workspaceRef": "urn:srcos:workspace:professional-intelligence-demo", + "redactionApplied": true, + "payloadMode": "metadata-only", + "contentCaptureEnabled": false, + "diagnosticClasses": [ + "operation-state", + "sync-status", + "device-identity", + "integration-entry-point" + ], + "artifactRefs": [ + "urn:srcos:evidence:diag-export-summary-demo-0001" + ], + "redactionRefs": [], + "policyDecisionRefs": [ + "urn:srcos:policy-decision:diag-export-summary-demo-0001" + ] +} diff --git a/examples/workspace-ops/diagnostics-export.redacted.example.json b/examples/workspace-ops/diagnostics-export.redacted.example.json new file mode 100644 index 0000000..d776650 --- /dev/null +++ b/examples/workspace-ops/diagnostics-export.redacted.example.json @@ -0,0 +1,23 @@ +{ + "exportId": "urn:srcos:diag-export:redacted-demo-0001", + "specVersion": "0.1.0", + "exportedAt": "2026-05-07T06:10:00Z", + "actorRef": "urn:srcos:subject:operator-demo", + "sessionRef": "urn:srcos:shell-session:demo-0001", + "workspaceRef": "urn:srcos:workspace:professional-intelligence-demo", + "redactionApplied": true, + "payloadMode": "redacted", + "contentCaptureEnabled": false, + "diagnosticClasses": [ + "operation-state", + "sync-status", + "file-availability" + ], + "artifactRefs": [], + "redactionRefs": [ + "urn:srcos:redaction-tombstone:diag-export-demo-credential-boundary-0001" + ], + "policyDecisionRefs": [ + "urn:srcos:policy-decision:diag-export-demo-0001" + ] +} diff --git a/examples/workspace-ops/operation-command.agentmachine-activate.example.json b/examples/workspace-ops/operation-command.agentmachine-activate.example.json new file mode 100644 index 0000000..36eeafd --- /dev/null +++ b/examples/workspace-ops/operation-command.agentmachine-activate.example.json @@ -0,0 +1,17 @@ +{ + "commandId": "urn:srcos:op-command:agentmachine-activate-demo-0001", + "specVersion": "0.1.0", + "issuedAt": "2026-05-07T06:07:00Z", + "actorRef": "urn:srcos:subject:operator-demo", + "sessionRef": "urn:srcos:shell-session:demo-0001", + "commandClass": "agentmachine-activate", + "targetRef": "urn:srcos:workspace-op-state:admitted-syncing-demo-0001", + "payload": { + "agentRegistryRef": "urn:srcos:agent-grant:ops-history-operator-assist-demo", + "inlineMaterialIncluded": false + }, + "isDryRun": false, + "policyDecisionRefs": [ + "urn:srcos:policy-decision:op-command-agentmachine-demo-0001" + ] +} diff --git a/examples/workspace-ops/operation-command.sync-request.example.json b/examples/workspace-ops/operation-command.sync-request.example.json new file mode 100644 index 0000000..9acbbb0 --- /dev/null +++ b/examples/workspace-ops/operation-command.sync-request.example.json @@ -0,0 +1,17 @@ +{ + "commandId": "urn:srcos:op-command:sync-request-demo-0001", + "specVersion": "0.1.0", + "issuedAt": "2026-05-07T06:06:00Z", + "actorRef": "urn:srcos:subject:operator-demo", + "sessionRef": "urn:srcos:shell-session:demo-0001", + "commandClass": "sync-request", + "targetRef": "urn:srcos:workspace:professional-intelligence-demo", + "payload": { + "syncProfileRef": "urn:srcos:sync-profile:default", + "inlineMaterialIncluded": false + }, + "isDryRun": true, + "policyDecisionRefs": [ + "urn:srcos:policy-decision:op-command-sync-demo-0001" + ] +} diff --git a/examples/workspace-ops/operation-command.turtleterm-open.example.json b/examples/workspace-ops/operation-command.turtleterm-open.example.json new file mode 100644 index 0000000..613fba2 --- /dev/null +++ b/examples/workspace-ops/operation-command.turtleterm-open.example.json @@ -0,0 +1,17 @@ +{ + "commandId": "urn:srcos:op-command:turtleterm-open-demo-0001", + "specVersion": "0.1.0", + "issuedAt": "2026-05-07T06:05:00Z", + "actorRef": "urn:srcos:subject:operator-demo", + "sessionRef": "urn:srcos:shell-session:demo-0001", + "commandClass": "turtleterm-open", + "targetRef": "urn:srcos:workspace-op-state:admitted-syncing-demo-0001", + "payload": { + "profileRef": "urn:srcos:shell-profile:default", + "inlineMaterialIncluded": false + }, + "isDryRun": false, + "policyDecisionRefs": [ + "urn:srcos:policy-decision:op-command-turtleterm-demo-0001" + ] +} diff --git a/examples/workspace-ops/workspace-operation-state.admitted-syncing.example.json b/examples/workspace-ops/workspace-operation-state.admitted-syncing.example.json new file mode 100644 index 0000000..3c0b745 --- /dev/null +++ b/examples/workspace-ops/workspace-operation-state.admitted-syncing.example.json @@ -0,0 +1,50 @@ +{ + "stateId": "urn:srcos:workspace-op-state:admitted-syncing-demo-0001", + "specVersion": "0.1.0", + "projectedAt": "2026-05-07T06:00:00Z", + "workspaceRef": "urn:srcos:workspace:professional-intelligence-demo", + "operationState": "admitted", + "fileAvailabilityState": "syncing", + "deviceIdentity": { + "deviceRef": "urn:srcos:device:workstation-demo-0001", + "trustProfile": "workstation-admitted", + "deviceLabel": "dev-workstation-demo" + }, + "syncStatus": { + "syncRef": "urn:srcos:syncd-session:demo-0001", + "syncState": "syncing", + "lastSyncedAt": "2026-05-07T05:58:00Z", + "pendingChangeCount": 3 + }, + "integrationEntryPoints": { + "turtleTerm": { + "commandRef": "urn:srcos:op-command:turtleterm-open-demo-0001", + "available": true, + "sessionRef": "urn:srcos:turtleterm-session:demo-0001" + }, + "bearBrowser": { + "commandRef": "urn:srcos:op-command:bearbrowser-open-demo-0001", + "available": true, + "tabRef": null + }, + "agentMachine": { + "commandRef": "urn:srcos:op-command:agentmachine-activate-demo-0001", + "available": false, + "agentRegistryRef": null + } + }, + "operationTrayProjection": { + "trayEntryRef": "urn:srcos:tray-entry:demo-0001", + "label": "professional-intelligence-demo", + "badgeState": "active" + }, + "operationInspectorProjection": { + "inspectorRef": "urn:srcos:inspector:demo-0001", + "detailRefs": [ + "urn:srcos:evidence:workspace-op-state-detail-demo-0001" + ] + }, + "policyDecisionRefs": [ + "urn:srcos:policy-decision:workspace-op-state-projection-demo-0001" + ] +} diff --git a/examples/workspace-ops/workspace-operation-state.conflicted.example.json b/examples/workspace-ops/workspace-operation-state.conflicted.example.json new file mode 100644 index 0000000..0ebb867 --- /dev/null +++ b/examples/workspace-ops/workspace-operation-state.conflicted.example.json @@ -0,0 +1,50 @@ +{ + "stateId": "urn:srcos:workspace-op-state:conflicted-demo-0001", + "specVersion": "0.1.0", + "projectedAt": "2026-05-07T06:01:00Z", + "workspaceRef": "urn:srcos:workspace:professional-intelligence-demo", + "operationState": "conflicted", + "fileAvailabilityState": "conflicted", + "deviceIdentity": { + "deviceRef": "urn:srcos:device:workstation-demo-0001", + "trustProfile": "user-verified", + "deviceLabel": "dev-workstation-demo" + }, + "syncStatus": { + "syncRef": "urn:srcos:syncd-session:demo-0002", + "syncState": "conflict", + "lastSyncedAt": "2026-05-07T05:59:00Z", + "pendingChangeCount": 0 + }, + "integrationEntryPoints": { + "turtleTerm": { + "commandRef": "urn:srcos:op-command:turtleterm-open-demo-0002", + "available": true, + "sessionRef": null + }, + "bearBrowser": { + "commandRef": "urn:srcos:op-command:bearbrowser-open-demo-0002", + "available": true, + "tabRef": null + }, + "agentMachine": { + "commandRef": "urn:srcos:op-command:agentmachine-activate-demo-0002", + "available": true, + "agentRegistryRef": "urn:srcos:agent-grant:conflict-resolution-demo" + } + }, + "operationTrayProjection": { + "trayEntryRef": "urn:srcos:tray-entry:demo-0002", + "label": "professional-intelligence-demo", + "badgeState": "conflict" + }, + "operationInspectorProjection": { + "inspectorRef": "urn:srcos:inspector:demo-0002", + "detailRefs": [ + "urn:srcos:evidence:workspace-op-state-conflict-demo-0001" + ] + }, + "policyDecisionRefs": [ + "urn:srcos:policy-decision:workspace-op-state-projection-demo-0002" + ] +} diff --git a/examples/workspace-ops/workspace-operation-state.quarantined.example.json b/examples/workspace-ops/workspace-operation-state.quarantined.example.json new file mode 100644 index 0000000..c7f75a7 --- /dev/null +++ b/examples/workspace-ops/workspace-operation-state.quarantined.example.json @@ -0,0 +1,45 @@ +{ + "stateId": "urn:srcos:workspace-op-state:quarantined-demo-0001", + "specVersion": "0.1.0", + "projectedAt": "2026-05-07T06:02:00Z", + "workspaceRef": "urn:srcos:workspace:quarantine-demo", + "operationState": "quarantined", + "fileAvailabilityState": "quarantined", + "deviceIdentity": { + "deviceRef": "urn:srcos:device:workstation-demo-0001", + "trustProfile": "device-only", + "deviceLabel": null + }, + "syncStatus": { + "syncRef": "urn:srcos:syncd-session:demo-0003", + "syncState": "offline", + "lastSyncedAt": null, + "pendingChangeCount": null + }, + "integrationEntryPoints": { + "turtleTerm": { + "commandRef": "urn:srcos:op-command:turtleterm-open-demo-0003", + "available": false, + "sessionRef": null + }, + "bearBrowser": { + "commandRef": "urn:srcos:op-command:bearbrowser-open-demo-0003", + "available": false, + "tabRef": null + }, + "agentMachine": { + "commandRef": "urn:srcos:op-command:agentmachine-activate-demo-0003", + "available": false, + "agentRegistryRef": null + } + }, + "operationTrayProjection": { + "trayEntryRef": "urn:srcos:tray-entry:demo-0003", + "label": "quarantine-demo", + "badgeState": "quarantined" + }, + "operationInspectorProjection": null, + "policyDecisionRefs": [ + "urn:srcos:policy-decision:workspace-op-state-quarantine-demo-0001" + ] +} diff --git a/schemas/diagnostics-export.schema.json b/schemas/diagnostics-export.schema.json new file mode 100644 index 0000000..eb0aac8 --- /dev/null +++ b/schemas/diagnostics-export.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.ai/sourceos-shell/diagnostics-export.schema.json", + "title": "DiagnosticsExport", + "description": "Redacted diagnostics export record produced by sourceos-shell. Policy Fabric decides what is exported; the shell applies redaction policy before emitting this record.", + "type": "object", + "additionalProperties": false, + "required": [ + "exportId", + "specVersion", + "exportedAt", + "redactionApplied", + "payloadMode", + "contentCaptureEnabled", + "policyDecisionRefs" + ], + "properties": { + "exportId": { + "type": "string", + "pattern": "^urn:srcos:diag-export:" + }, + "specVersion": { "type": "string" }, + "exportedAt": { "type": "string", "format": "date-time" }, + "actorRef": { "type": ["string", "null"] }, + "sessionRef": { "type": ["string", "null"] }, + "workspaceRef": { "type": ["string", "null"] }, + "redactionApplied": { + "type": "boolean", + "description": "Must be true. The shell must always apply redaction policy before export." + }, + "payloadMode": { + "type": "string", + "enum": ["metadata-only", "summary", "ref-only", "redacted"] + }, + "contentCaptureEnabled": { + "type": "boolean", + "description": "Must be false. Diagnostics export must never enable content capture." + }, + "diagnosticClasses": { + "type": "array", + "description": "Classes of diagnostic data included in this export.", + "items": { + "type": "string", + "enum": [ + "operation-state", + "sync-status", + "file-availability", + "device-identity", + "integration-entry-point", + "model-carry-ref", + "workstation-state-ref" + ] + }, + "default": [] + }, + "artifactRefs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + }, + "redactionRefs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + }, + "policyDecisionRefs": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/schemas/operation-command.schema.json b/schemas/operation-command.schema.json new file mode 100644 index 0000000..8eb6544 --- /dev/null +++ b/schemas/operation-command.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.ai/sourceos-shell/operation-command.schema.json", + "title": "OperationCommand", + "description": "Shell-issued OperationCommand routed to the Operation Plane. The shell does not execute commands directly; it emits structured OperationCommand records for the Operation Plane to act on.", + "type": "object", + "additionalProperties": false, + "required": [ + "commandId", + "specVersion", + "issuedAt", + "commandClass", + "targetRef", + "policyDecisionRefs" + ], + "properties": { + "commandId": { + "type": "string", + "pattern": "^urn:srcos:op-command:" + }, + "specVersion": { "type": "string" }, + "issuedAt": { "type": "string", "format": "date-time" }, + "actorRef": { "type": ["string", "null"] }, + "sessionRef": { "type": ["string", "null"] }, + "commandClass": { + "type": "string", + "description": "Class of operation the shell is requesting the Operation Plane to perform.", + "enum": [ + "turtleterm-open", + "turtleterm-close", + "bearbrowser-open", + "bearbrowser-close", + "agentmachine-activate", + "agentmachine-deactivate", + "sync-request", + "sync-cancel", + "diagnostics-export", + "model-carry-initiate", + "workstation-state-query" + ] + }, + "targetRef": { + "type": "string", + "description": "URN reference to the operation, session, or resource the command targets." + }, + "payload": { + "type": ["object", "null"], + "additionalProperties": true, + "description": "Optional command-class-specific metadata. Must not include inline sensitive material." + }, + "isDryRun": { + "type": "boolean", + "description": "When true the shell requests a plan/preview response without side effects.", + "default": false + }, + "policyDecisionRefs": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/schemas/workspace-operation-state.schema.json b/schemas/workspace-operation-state.schema.json new file mode 100644 index 0000000..d3dda27 --- /dev/null +++ b/schemas/workspace-operation-state.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.ai/sourceos-shell/workspace-operation-state.schema.json", + "title": "WorkspaceOperationState", + "description": "Read-only projection of local Workspace Operation state surfaced by sourceos-shell. The shell is a control surface only; it does not own Policy Fabric or Operation Plane runtime logic.", + "type": "object", + "additionalProperties": false, + "required": [ + "stateId", + "specVersion", + "projectedAt", + "operationState", + "fileAvailabilityState", + "deviceIdentity", + "syncStatus", + "integrationEntryPoints", + "policyDecisionRefs" + ], + "properties": { + "stateId": { + "type": "string", + "pattern": "^urn:srcos:workspace-op-state:" + }, + "specVersion": { "type": "string" }, + "projectedAt": { "type": "string", "format": "date-time" }, + "workspaceRef": { "type": ["string", "null"] }, + "operationState": { + "type": "string", + "description": "Current lifecycle state of the workspace operation as reported by the Operation Plane.", + "enum": [ + "stored", + "quarantined", + "admitted", + "activated", + "syncing", + "conflicted", + "failed" + ] + }, + "fileAvailabilityState": { + "type": "string", + "description": "Availability state of the primary file or artefact under this operation.", + "enum": [ + "local", + "remote", + "syncing", + "conflicted", + "quarantined" + ] + }, + "deviceIdentity": { + "type": "object", + "description": "Device identity and trust profile as supplied by the device trust authority. Shell projects this; it does not author it.", + "additionalProperties": false, + "required": ["deviceRef", "trustProfile"], + "properties": { + "deviceRef": { "type": "string" }, + "trustProfile": { + "type": "string", + "enum": ["untrusted", "device-only", "user-verified", "workstation-admitted"] + }, + "deviceLabel": { "type": ["string", "null"] } + } + }, + "syncStatus": { + "type": "object", + "description": "Sync status projection sourced from sourceos-syncd. Shell reads this; it does not produce it.", + "additionalProperties": false, + "required": ["syncRef", "syncState"], + "properties": { + "syncRef": { "type": "string" }, + "syncState": { + "type": "string", + "enum": ["idle", "syncing", "conflict", "error", "offline"] + }, + "lastSyncedAt": { "type": ["string", "null"], "format": "date-time" }, + "pendingChangeCount": { "type": ["integer", "null"], "minimum": 0 } + } + }, + "integrationEntryPoints": { + "type": "object", + "description": "Shell-surface entry points for workspace operation integrations. Each entry point is a ref the shell uses to route an OperationCommand; the shell does not execute the integration directly.", + "additionalProperties": false, + "required": ["turtleTerm", "bearBrowser", "agentMachine"], + "properties": { + "turtleTerm": { + "type": "object", + "description": "TurtleTerm terminal operation integration entry point.", + "additionalProperties": false, + "required": ["commandRef", "available"], + "properties": { + "commandRef": { "type": "string" }, + "available": { "type": "boolean" }, + "sessionRef": { "type": ["string", "null"] } + } + }, + "bearBrowser": { + "type": "object", + "description": "BearBrowser browser operation integration entry point.", + "additionalProperties": false, + "required": ["commandRef", "available"], + "properties": { + "commandRef": { "type": "string" }, + "available": { "type": "boolean" }, + "tabRef": { "type": ["string", "null"] } + } + }, + "agentMachine": { + "type": "object", + "description": "Agent-machine operation integration entry point.", + "additionalProperties": false, + "required": ["commandRef", "available"], + "properties": { + "commandRef": { "type": "string" }, + "available": { "type": "boolean" }, + "agentRegistryRef": { "type": ["string", "null"] } + } + } + } + }, + "operationTrayProjection": { + "type": ["object", "null"], + "description": "Local Operation Tray projection metadata. Shell renders this view; Policy Fabric owns the source.", + "additionalProperties": false, + "properties": { + "trayEntryRef": { "type": "string" }, + "label": { "type": "string" }, + "badgeState": { + "type": "string", + "enum": ["idle", "active", "conflict", "error", "quarantined"] + } + } + }, + "operationInspectorProjection": { + "type": ["object", "null"], + "description": "Local Operation Inspector projection metadata. Shell renders this view; Policy Fabric owns the source.", + "additionalProperties": false, + "properties": { + "inspectorRef": { "type": "string" }, + "detailRefs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + } + } + }, + "policyDecisionRefs": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/scripts/validate_workspace_ops.py b/scripts/validate_workspace_ops.py new file mode 100644 index 0000000..ae4e8c9 --- /dev/null +++ b/scripts/validate_workspace_ops.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +"""Validate workspace-ops example JSON files against their JSON Schemas. + +Mirrors the structure of scripts/validate_ops_history_receipts.py. +""" +from __future__ import annotations + +import json +from pathlib import Path + +import jsonschema + +ROOT = Path(__file__).resolve().parents[1] +EXAMPLE_DIR = ROOT / "examples" / "workspace-ops" + +SCHEMAS = { + "workspace-operation-state": ROOT / "schemas" / "workspace-operation-state.schema.json", + "operation-command": ROOT / "schemas" / "operation-command.schema.json", + "diagnostics-export": ROOT / "schemas" / "diagnostics-export.schema.json", +} + +# Map example filename prefix to schema key +PREFIX_TO_SCHEMA = { + "workspace-operation-state": "workspace-operation-state", + "operation-command": "operation-command", + "diagnostics-export": "diagnostics-export", +} + + +def _schema_for(path: Path) -> dict: + for prefix, key in PREFIX_TO_SCHEMA.items(): + if path.name.startswith(prefix): + schema_path = SCHEMAS[key] + return json.loads(schema_path.read_text(encoding="utf-8")) + raise ValueError(f"No schema mapping found for example file: {path.name}") + + +def _check_workspace_operation_state(data: dict, path: Path) -> None: + """Enforce invariants beyond what JSON Schema can express.""" + if not data.get("stateId", "").startswith("urn:srcos:workspace-op-state:"): + raise ValueError(f"{path}: stateId must start with 'urn:srcos:workspace-op-state:'") + if not data.get("policyDecisionRefs"): + raise ValueError(f"{path}: policyDecisionRefs must be non-empty") + + +def _check_operation_command(data: dict, path: Path) -> None: + if not data.get("commandId", "").startswith("urn:srcos:op-command:"): + raise ValueError(f"{path}: commandId must start with 'urn:srcos:op-command:'") + if not data.get("policyDecisionRefs"): + raise ValueError(f"{path}: policyDecisionRefs must be non-empty") + + +def _check_diagnostics_export(data: dict, path: Path) -> None: + if not data.get("redactionApplied"): + raise ValueError(f"{path}: redactionApplied must be true") + if data.get("contentCaptureEnabled") is not False: + raise ValueError(f"{path}: contentCaptureEnabled must be false") + if data.get("payloadMode") == "redacted" and not data.get("redactionRefs"): + raise ValueError(f"{path}: redacted exports must include redactionRefs") + if not data.get("policyDecisionRefs"): + raise ValueError(f"{path}: policyDecisionRefs must be non-empty") + + +EXTRA_CHECKS = { + "workspace-operation-state": _check_workspace_operation_state, + "operation-command": _check_operation_command, + "diagnostics-export": _check_diagnostics_export, +} + + +def validate_example(path: Path) -> str: + schema = _schema_for(path) + jsonschema.validators.validator_for(schema).check_schema(schema) + data = json.loads(path.read_text(encoding="utf-8")) + jsonschema.validate(data, schema) + for prefix, key in PREFIX_TO_SCHEMA.items(): + if path.name.startswith(prefix): + EXTRA_CHECKS[key](data, path) + break + return path.name + + +def main() -> int: + examples = sorted(EXAMPLE_DIR.glob("*.example.json")) + if not examples: + raise SystemExit("No workspace-ops example files found") + checked = [validate_example(path) for path in examples] + print(json.dumps({"ok": True, "checked": checked}, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())