diff --git a/README.md b/README.md index 4585753..3cc2218 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,41 @@ This repo defines **contracts** and **conformance** for workstation/CI lanes. It If you need to *run* a lane, you’re looking for the workspace controller / runner repo, not this one. +## Agent Machine and Office Plane conformance + +This repo now includes conformance fixtures for SourceOS Agent Machine scoped mounts and Prophet Workspace Office Plane dry-run behavior. + +Good fixture: + +```text +conformance/good/agent-machine-office-dry-run.json +``` + +Bad fixtures: + +```text +conformance/bad/agent-machine-whole-home-mount.json +conformance/bad/agent-machine-unscoped-downloads.json +conformance/bad/office-raw-apple-app-db.json +``` + +The semantic validator rejects: + +- `$HOME` / `~` as a whole-home Agent Machine mount root; +- unscoped `~/Downloads` browser download mounts; +- raw Apple app database/library access for Notes, Photos, Reminders, or Voice Memos; +- Agent Machine lanes that do not emit `agent-machine.mount.evidence`; +- Office Plane lanes that do not emit `office.artifact.evidence`. + +These checks preserve the intended boundary: + +```text +sourceosctl agent-machine mounts plan -> scoped mount evidence +sourceosctl office ... -> OfficeArtifact-compatible dry-run/evidence +agent-term office ... -> governance-preserving operator event +AgentPlane -> AgentMachineMountEvidence / OfficeArtifactEvidence +``` + ## IPC v0 reference harness This repo now includes a small IPC v0 reference harness under: @@ -68,6 +103,8 @@ The production runner/orchestrator remains out of scope for this repo. The refer - Implementing the production runner/orchestrator (execution belongs elsewhere) - Hosting container images (this repo only **pins** digests once published) - Being a monorepo for all workstation tooling (we stay small and auditable) +- Executing Agent Machine mounts or Office generation/conversion directly +- Mounting raw host app databases such as Apple Notes, Photos, Reminders, or Voice Memos ## How this plugs into the platform diff --git a/conformance/bad/agent-machine-unscoped-downloads.json b/conformance/bad/agent-machine-unscoped-downloads.json new file mode 100644 index 0000000..974311c --- /dev/null +++ b/conformance/bad/agent-machine-unscoped-downloads.json @@ -0,0 +1,35 @@ +{ + "api_version": "workstation-contracts.socios.io/v0.1", + "kind": "WorkstationContract", + "metadata": { + "id": "agent-machine-unscoped-downloads", + "name": "Invalid Agent Machine unscoped downloads mount", + "description": "This fixture must fail because browser downloads must use ~/Downloads/SourceOS/agent-downloads, not the whole host Downloads directory.", + "labels": { + "sourceos.agent_machine": "true" + } + }, + "spec": { + "lanes": [ + { + "name": "invalid-unscoped-downloads", + "backend": { + "type": "container", + "container": { + "image": "ghcr.io/sourceos-linux/workstation-contracts-truth-lane@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "workdir": "/workspace" + } + }, + "steps": [ + { + "name": "bad-downloads-mount", + "run": "sourceosctl agent-machine mounts plan --dev-root ~/dev --docs-root ~/Documents/SourceOS/agent-output --downloads-root ~/Downloads" + } + ], + "evidence": { + "emit": ["agent-machine.mount.plan"] + } + } + ] + } +} diff --git a/conformance/bad/agent-machine-whole-home-mount.json b/conformance/bad/agent-machine-whole-home-mount.json new file mode 100644 index 0000000..bd81f14 --- /dev/null +++ b/conformance/bad/agent-machine-whole-home-mount.json @@ -0,0 +1,35 @@ +{ + "api_version": "workstation-contracts.socios.io/v0.1", + "kind": "WorkstationContract", + "metadata": { + "id": "agent-machine-whole-home-mount", + "name": "Invalid Agent Machine whole home mount", + "description": "This fixture must fail because Agent Machine must never mount $HOME wholesale.", + "labels": { + "sourceos.agent_machine": "true" + } + }, + "spec": { + "lanes": [ + { + "name": "invalid-whole-home-mount", + "backend": { + "type": "container", + "container": { + "image": "ghcr.io/sourceos-linux/workstation-contracts-truth-lane@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "workdir": "/workspace" + } + }, + "steps": [ + { + "name": "bad-whole-home-mount", + "run": "sourceosctl agent-machine mounts plan --dev-root $HOME --docs-root ~/Documents/SourceOS/agent-output --downloads-root ~/Downloads/SourceOS/agent-downloads" + } + ], + "evidence": { + "emit": ["agent-machine.mount.plan"] + } + } + ] + } +} diff --git a/conformance/bad/office-raw-apple-app-db.json b/conformance/bad/office-raw-apple-app-db.json new file mode 100644 index 0000000..e5594a8 --- /dev/null +++ b/conformance/bad/office-raw-apple-app-db.json @@ -0,0 +1,35 @@ +{ + "api_version": "workstation-contracts.socios.io/v0.1", + "kind": "WorkstationContract", + "metadata": { + "id": "office-raw-apple-app-db", + "name": "Invalid raw Apple app database access", + "description": "This fixture must fail because Notes, Photos, Reminders, and Voice Memos must be App Doors, not default raw database mounts.", + "labels": { + "sourceos.office_plane": "true" + } + }, + "spec": { + "lanes": [ + { + "name": "invalid-raw-apple-app-db", + "backend": { + "type": "container", + "container": { + "image": "ghcr.io/sourceos-linux/workstation-contracts-truth-lane@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "workdir": "/workspace" + } + }, + "steps": [ + { + "name": "bad-raw-notes-db-access", + "run": "sourceosctl office inspect ~/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite" + } + ], + "evidence": { + "emit": ["office.artifact.evidence"] + } + } + ] + } +} diff --git a/conformance/good/agent-machine-office-dry-run.json b/conformance/good/agent-machine-office-dry-run.json new file mode 100644 index 0000000..0292df5 --- /dev/null +++ b/conformance/good/agent-machine-office-dry-run.json @@ -0,0 +1,60 @@ +{ + "api_version": "workstation-contracts.socios.io/v0.1", + "kind": "WorkstationContract", + "metadata": { + "id": "agent-machine-office-dry-run", + "name": "Agent Machine and Office Plane dry-run conformance lane", + "description": "Conformance fixture for SourceOS Agent Machine scoped mounts and Prophet Workspace Office Plane dry-run behavior.", + "labels": { + "sourceos.capability": "agent-machine-office", + "sourceos.agent_machine": "true", + "sourceos.office_plane": "true" + } + }, + "spec": { + "lanes": [ + { + "name": "agent-machine-office-dry-run", + "backend": { + "type": "container", + "container": { + "image": "ghcr.io/sourceos-linux/workstation-contracts-truth-lane@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "workdir": "/workspace" + } + }, + "steps": [ + { + "name": "plan-scoped-agent-machine-mounts", + "run": "sourceosctl agent-machine mounts plan --dev-root ~/dev --docs-root ~/Documents/SourceOS/agent-output --downloads-root ~/Downloads/SourceOS/agent-downloads" + }, + { + "name": "inspect-office-backends", + "run": "sourceosctl office doctor" + }, + { + "name": "plan-office-deck-generation", + "run": "sourceosctl office generate --dry-run --artifact-type slide-deck --format pptx --title 'Demo Briefing Deck' --workroom-id workroom-demo-0001" + }, + { + "name": "plan-office-pdf-conversion", + "run": "sourceosctl office convert /workspace/output/demo.docx --to pdf --dry-run --workroom-id workroom-demo-0001" + }, + { + "name": "record-agentterm-office-event", + "run": "agent-term office create-deck '!prophet-workspace' --workroom workroom-demo-0001 --title 'Demo Briefing Deck'" + } + ], + "evidence": { + "emit": [ + "env.fingerprint", + "agent-machine.mount.plan", + "agent-machine.mount.evidence", + "office.artifact.plan", + "office.artifact.evidence", + "agentterm.office.event" + ] + } + } + ] + } +} diff --git a/tools/validate_contract.py b/tools/validate_contract.py index 849c28c..fd4ba09 100755 --- a/tools/validate_contract.py +++ b/tools/validate_contract.py @@ -11,9 +11,38 @@ DIGEST_RE = re.compile(r"@sha256:[0-9a-f]{64}$") SENTINEL = "@sha256:REPLACE_WITH_DIGEST" +FORBIDDEN_RUN_PATTERNS = [ + ( + re.compile(r"sourceosctl\s+agent-machine\s+mounts\s+plan[^\n]*--dev-root\s+(?:\$HOME|~)(?:\s|$)"), + "Agent Machine must not use $HOME or ~ as the dev/code root; use ~/dev or an explicit repo allowlist root.", + ), + ( + re.compile(r"sourceosctl\s+agent-machine\s+mounts\s+plan[^\n]*--docs-root\s+(?:\$HOME|~)(?:\s|$)"), + "Agent Machine must not use $HOME or ~ as the document root; use ~/Documents/SourceOS/agent-output.", + ), + ( + re.compile(r"sourceosctl\s+agent-machine\s+mounts\s+plan[^\n]*--downloads-root\s+~/Downloads(?:\s|$)"), + "Browser downloads must use scoped ~/Downloads/SourceOS/agent-downloads, not the whole host Downloads directory.", + ), + ( + re.compile(r"sourceosctl\s+office\s+inspect\s+[^\n]*(?:NoteStore\.sqlite|group\.com\.apple\.notes|Photos\.photoslibrary|Voice\s*Memos|VoiceMemos|Reminders)", re.IGNORECASE), + "Office/App integrations must use future App Doors; raw Apple app databases/libraries must not be inspected or mounted by default.", + ), +] + +REQUIRED_EVIDENCE_BY_LABEL = { + "sourceos.agent_machine": "agent-machine.mount.evidence", + "sourceos.office_plane": "office.artifact.evidence", +} + def load_json(p: Path): return json.loads(p.read_text(encoding="utf-8")) +def iter_steps(doc): + for lane in doc["spec"]["lanes"]: + for step in lane.get("steps", []): + yield lane, step + def main(): if len(sys.argv) < 2: print("Usage: tools/validate_contract.py [...]", file=sys.stderr) @@ -33,6 +62,8 @@ def main(): print(f"FAIL schema: {p}: {e.message}", file=sys.stderr) continue + labels = doc.get("metadata", {}).get("labels", {}) + for lane in doc["spec"]["lanes"]: b = lane["backend"] if b["type"] == "container": @@ -47,6 +78,25 @@ def main(): ok = False print(f"FAIL semantic: {p}: lane '{lane['name']}' container.image must be digest-pinned (@sha256:<64hex>)", file=sys.stderr) + evidence_emit = set(lane.get("evidence", {}).get("emit", [])) + for label, required_evidence in REQUIRED_EVIDENCE_BY_LABEL.items(): + if labels.get(label) == "true" and required_evidence not in evidence_emit: + ok = False + print( + f"FAIL semantic: {p}: lane '{lane['name']}' must emit {required_evidence} when {label}=true", + file=sys.stderr, + ) + + for lane, step in iter_steps(doc): + run = step.get("run", "") + for pattern, message in FORBIDDEN_RUN_PATTERNS: + if pattern.search(run): + ok = False + print( + f"FAIL semantic: {p}: step '{step.get('name', '')}' in lane '{lane['name']}': {message}", + file=sys.stderr, + ) + if ok: print("OK: all contracts valid") return 0