diff --git a/.github/workflows/agent-harness-terminal-receipts.yml b/.github/workflows/agent-harness-terminal-receipts.yml new file mode 100644 index 00000000000..431ad888c9e --- /dev/null +++ b/.github/workflows/agent-harness-terminal-receipts.yml @@ -0,0 +1,37 @@ +name: Agent Harness Terminal Receipts + +on: + pull_request: + paths: + - 'docs/sourceos/AGENT_HARNESS_TERMINAL_RECEIPTS.md' + - 'schemas/agent-harness-terminal-receipts.schema.json' + - 'examples/agent-harness-terminal-receipts.example.json' + - 'scripts/verify-agent-harness-terminal-receipts.py' + - '.github/workflows/agent-harness-terminal-receipts.yml' + push: + branches: + - main + paths: + - 'docs/sourceos/AGENT_HARNESS_TERMINAL_RECEIPTS.md' + - 'schemas/agent-harness-terminal-receipts.schema.json' + - 'examples/agent-harness-terminal-receipts.example.json' + - 'scripts/verify-agent-harness-terminal-receipts.py' + - '.github/workflows/agent-harness-terminal-receipts.yml' + +permissions: + contents: read + +jobs: + validate-agent-harness-terminal-receipts: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Validate TurtleTerm Agent Harness receipts + run: python3 scripts/verify-agent-harness-terminal-receipts.py diff --git a/docs/sourceos/AGENT_HARNESS_TERMINAL_RECEIPTS.md b/docs/sourceos/AGENT_HARNESS_TERMINAL_RECEIPTS.md new file mode 100644 index 00000000000..42e5f426af6 --- /dev/null +++ b/docs/sourceos/AGENT_HARNESS_TERMINAL_RECEIPTS.md @@ -0,0 +1,184 @@ +# Agent Harness Terminal Receipt Surface + +Status: v0.1 planning baseline +Owner plane: TurtleTerm governed terminal/operator surface +Consumers: SourceOS spec, AgentPlane, Policy Fabric, Memory Mesh, SCOPE-D, Delivery Excellence + +## Purpose + +TurtleTerm is the SourceOS policy-aware, agent-addressable terminal workbench. The Aden/Hive production-agent pattern requires terminal work to be visible, bounded, receipt-producing, and measurable. TurtleTerm should make terminal/operator execution auditable without granting ambient shell authority to agents or cognition layers. + +## Boundary + +TurtleTerm owns: + +- terminal/session UX +- command wrapper behavior +- local agent gateway surface +- terminal receipts +- operator approval surfaces +- tmux/mux bridge receipts +- skill manifests for terminal operations +- replayable operator workflows + +TurtleTerm does not own: + +- AgentPlane graph execution +- Policy Fabric gate authority +- Agent Machine runtime provider lifecycle +- Delivery Excellence scoreboards +- Memory Mesh artifact storage +- SCOPE-D security exercise execution + +## Receipt classes + +### TerminalSessionReceipt + +Records an operator or agent-addressable terminal session. + +Required semantics: + +- terminal session id +- actor/agent ref +- workspace ref +- shell profile +- gateway profile +- policy admission ref +- AgentPlane run/session refs +- start/end timestamps +- mux/tmux pane refs when applicable +- environment profile hash + +### CommandReceipt + +Records a command execution through TurtleTerm. + +Required semantics: + +- command id +- terminal session ref +- command hash +- command display text when policy permits +- working directory +- environment profile hash +- stdin/stdout/stderr artifact pointer refs +- exit code +- duration +- policy decision ref +- side-effect class +- replay eligibility + +### MutationReceipt + +Records observed filesystem, process, deployment, or host mutation. + +Required semantics: + +- mutation id +- command ref +- mutation class +- target scope +- dry-run/live-run mode +- policy decision ref +- human-control event ref when required +- before/after artifact refs when available +- rollback ref +- denied operation refs + +### OperatorApprovalReceipt + +Records human operator decisions in TurtleTerm. + +Required semantics: + +- approval id +- actor ref +- subject ref +- decision +- reason +- timestamp +- policy gate ref +- AgentPlane run/session ref +- Delivery Excellence human-control event ref + +## Controlled actions + +Require Policy Fabric decisions for: + +- package install +- filesystem mutation outside workspace scope +- deployment/apply operations +- service start/stop/restart +- network listener creation +- secret/key material access +- credential helper invocation +- privilege escalation +- destructive command patterns +- host mutation +- cluster mutation + +Fail closed when controlled actions lack a policy decision ref. + +## AgentPlane integration + +AgentPlane should cite TurtleTerm receipts in: + +- RunArtifact +- ReplayArtifact +- SessionEnvelope +- EvidencePack +- FailureDiagnosis +- PromotionGate + +TurtleTerm receipts should preserve enough evidence for replay, diagnosis, and customer-safe proof without exposing raw secrets. + +## Memory Mesh integration + +Large stdout/stderr, shell transcripts, generated files, diffs, and terminal artifacts should be moved behind Memory Mesh `ArtifactPointer` refs when large, sensitive, replay-critical, or customer-proof relevant. + +## Delivery Excellence integration + +Delivery Excellence should consume derived metrics/readouts: + +- command success/failure +- policy-blocked command count +- host mutation denied/approved/performed +- approval latency +- replay-eligible command count +- operator intervention count +- terminal workflow cycle time +- customer-safe proof of operator work + +Delivery Excellence should not consume raw terminal transcripts unless policy explicitly permits it. + +## SCOPE-D integration + +SCOPE-D should validate TurtleTerm workflows for: + +- command injection +- shell escape +- destructive command bypass +- privilege escalation +- secret exfiltration +- unauthorized filesystem mutation +- unauthorized service exposure +- hostile generated scripts +- host/cluster mutation bypass + +## Non-negotiables + +- TurtleTerm must not grant ambient shell authority to agents. +- Agent Machine owns machine-local runtime provider lifecycle. +- Policy Fabric decides controlled action authority. +- Command outputs may need redaction and artifact pointers. +- Host mutation must be explicit, policy-referenced, and rollback-aware. +- Human approvals are typed control events, not freeform notes. +- Delivery Excellence receives metrics and readouts, not uncontrolled shell logs. + +## Near-term implementation path + +1. Align TurtleTerm command wrapper receipts with SourceOS `ShellReceiptEvent` and SourceOS execution receipt boundaries. +2. Add examples for terminal session, command, mutation, and operator approval receipts. +3. Add a verifier requiring policy refs for controlled action classes. +4. Add Delivery Excellence projection examples for command success, mutation posture, and approval latency. +5. Add SCOPE-D terminal-risk checks for command injection, secret access, host mutation, and shell escape. diff --git a/examples/agent-harness-terminal-receipts.example.json b/examples/agent-harness-terminal-receipts.example.json new file mode 100644 index 00000000000..5d4b45edd9b --- /dev/null +++ b/examples/agent-harness-terminal-receipts.example.json @@ -0,0 +1,50 @@ +{ + "schemaVersion": "v0.1", + "kind": "AgentHarnessTerminalReceipts", + "terminalSessionReceipt": { + "sessionId": "turtleterm-session-agent-harness-v0.1", + "actorRef": "github://mdheller", + "workspaceRef": "github://SocioProphet/sociosphere", + "shellProfile": "sourceos-local-dev", + "gatewayProfile": "turtle-agentd-local", + "policyAdmissionRef": "github://SocioProphet/policy-fabric/pull/60", + "agentplaneRunRef": "github://SocioProphet/agentplane/pull/107", + "muxPaneRefs": [], + "environmentProfileHash": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "commandReceipt": { + "commandId": "command-agent-harness-validate-runtime-contracts", + "terminalSessionRef": "turtleterm-session-agent-harness-v0.1", + "commandHash": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "workingDirectory": "~/dev/agentplane", + "environmentProfileHash": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "stdoutPointerRef": "artifact://sha256/3333333333333333333333333333333333333333333333333333333333333333", + "stderrPointerRef": "artifact://sha256/4444444444444444444444444444444444444444444444444444444444444444", + "exitCode": 0, + "policyDecisionRef": "github://SocioProphet/policy-fabric/pull/60", + "sideEffectClass": "none", + "replayEligible": true + }, + "mutationReceipt": { + "mutationId": "mutation-none-agent-harness-v0.1", + "commandRef": "command-agent-harness-validate-runtime-contracts", + "mutationClass": "none", + "targetScope": "workspace-only", + "mode": "dry-run", + "policyDecisionRef": "github://SocioProphet/policy-fabric/pull/60", + "humanControlEventRef": "", + "rollbackRef": "", + "mutatedHost": false, + "deniedOperationRefs": [] + }, + "operatorApprovalReceipt": { + "approvalId": "operator-approval-agent-harness-baseline", + "actorRef": "github://mdheller", + "subjectRef": "github://SourceOS-Linux/TurtleTerm/pull/5", + "decision": "deferred", + "reason": "Baseline receipt fixture only; live terminal mutation approval is not requested.", + "policyGateRef": "github://SocioProphet/policy-fabric/pull/60", + "agentplaneRunRef": "github://SocioProphet/agentplane/pull/107", + "deliveryExcellenceEventRef": "github://SocioProphet/delivery-excellence-automation/pull/7" + } +} diff --git a/schemas/agent-harness-terminal-receipts.schema.json b/schemas/agent-harness-terminal-receipts.schema.json new file mode 100644 index 00000000000..e9966dfb03c --- /dev/null +++ b/schemas/agent-harness-terminal-receipts.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://sourceos.dev/schemas/turtleterm/agent-harness-terminal-receipts.schema.json", + "title": "AgentHarnessTerminalReceipts", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "kind", "terminalSessionReceipt", "commandReceipt", "mutationReceipt", "operatorApprovalReceipt"], + "properties": { + "schemaVersion": { "const": "v0.1" }, + "kind": { "const": "AgentHarnessTerminalReceipts" }, + "terminalSessionReceipt": { + "type": "object", + "additionalProperties": false, + "required": ["sessionId", "actorRef", "workspaceRef", "shellProfile", "gatewayProfile", "policyAdmissionRef", "agentplaneRunRef", "environmentProfileHash"], + "properties": { + "sessionId": { "type": "string" }, + "actorRef": { "type": "string" }, + "workspaceRef": { "type": "string" }, + "shellProfile": { "type": "string" }, + "gatewayProfile": { "type": "string" }, + "policyAdmissionRef": { "type": "string" }, + "agentplaneRunRef": { "type": "string" }, + "muxPaneRefs": { "type": "array", "items": { "type": "string" } }, + "environmentProfileHash": { "type": "string" } + } + }, + "commandReceipt": { + "type": "object", + "additionalProperties": false, + "required": ["commandId", "terminalSessionRef", "commandHash", "workingDirectory", "environmentProfileHash", "exitCode", "policyDecisionRef", "sideEffectClass", "replayEligible"], + "properties": { + "commandId": { "type": "string" }, + "terminalSessionRef": { "type": "string" }, + "commandHash": { "type": "string" }, + "workingDirectory": { "type": "string" }, + "environmentProfileHash": { "type": "string" }, + "stdoutPointerRef": { "type": "string" }, + "stderrPointerRef": { "type": "string" }, + "exitCode": { "type": "integer" }, + "policyDecisionRef": { "type": "string" }, + "sideEffectClass": { "type": "string", "enum": ["none", "workspace-write", "host-mutation", "secret-access", "network-service", "deployment"] }, + "replayEligible": { "type": "boolean" } + } + }, + "mutationReceipt": { + "type": "object", + "additionalProperties": false, + "required": ["mutationId", "commandRef", "mutationClass", "targetScope", "mode", "policyDecisionRef", "mutatedHost"], + "properties": { + "mutationId": { "type": "string" }, + "commandRef": { "type": "string" }, + "mutationClass": { "type": "string" }, + "targetScope": { "type": "string" }, + "mode": { "type": "string", "enum": ["dry-run", "live"] }, + "policyDecisionRef": { "type": "string" }, + "humanControlEventRef": { "type": "string" }, + "rollbackRef": { "type": "string" }, + "mutatedHost": { "type": "boolean" }, + "deniedOperationRefs": { "type": "array", "items": { "type": "string" } } + } + }, + "operatorApprovalReceipt": { + "type": "object", + "additionalProperties": false, + "required": ["approvalId", "actorRef", "subjectRef", "decision", "policyGateRef", "agentplaneRunRef", "deliveryExcellenceEventRef"], + "properties": { + "approvalId": { "type": "string" }, + "actorRef": { "type": "string" }, + "subjectRef": { "type": "string" }, + "decision": { "type": "string", "enum": ["approved", "rejected", "deferred", "accepted-risk", "revoked"] }, + "reason": { "type": "string" }, + "policyGateRef": { "type": "string" }, + "agentplaneRunRef": { "type": "string" }, + "deliveryExcellenceEventRef": { "type": "string" } + } + } + } +} diff --git a/scripts/verify-agent-harness-terminal-receipts.py b/scripts/verify-agent-harness-terminal-receipts.py new file mode 100644 index 00000000000..f7973db7446 --- /dev/null +++ b/scripts/verify-agent-harness-terminal-receipts.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Verify TurtleTerm Agent Harness terminal receipt fixture.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA = ROOT / "schemas" / "agent-harness-terminal-receipts.schema.json" +EXAMPLE = ROOT / "examples" / "agent-harness-terminal-receipts.example.json" + + +class ValidationError(Exception): + pass + + +def load_json(path: Path) -> Any: + with path.open("r", encoding="utf-8") as handle: + return json.load(handle) + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def validate(data: dict[str, Any]) -> None: + require(SCHEMA.exists(), f"missing schema: {SCHEMA}") + require(data.get("schemaVersion") == "v0.1", "schemaVersion must be v0.1") + require(data.get("kind") == "AgentHarnessTerminalReceipts", "kind mismatch") + + session = data.get("terminalSessionReceipt") + require(isinstance(session, dict), "terminalSessionReceipt must be object") + for field in ["sessionId", "actorRef", "workspaceRef", "shellProfile", "gatewayProfile", "policyAdmissionRef", "agentplaneRunRef", "environmentProfileHash"]: + require(field in session, f"terminalSessionReceipt missing {field}") + require(session["environmentProfileHash"].startswith("sha256:"), "environmentProfileHash must be a sha256 ref") + + command = data.get("commandReceipt") + require(isinstance(command, dict), "commandReceipt must be object") + for field in ["commandId", "terminalSessionRef", "commandHash", "workingDirectory", "environmentProfileHash", "exitCode", "policyDecisionRef", "sideEffectClass", "replayEligible"]: + require(field in command, f"commandReceipt missing {field}") + require(command["commandHash"].startswith("sha256:"), "commandHash must be a sha256 ref") + require(command["policyDecisionRef"], "commandReceipt requires policyDecisionRef") + if command["exitCode"] != 0: + require(command["replayEligible"] is False, "failed command should not be replayEligible in baseline fixture") + + mutation = data.get("mutationReceipt") + require(isinstance(mutation, dict), "mutationReceipt must be object") + for field in ["mutationId", "commandRef", "mutationClass", "targetScope", "mode", "policyDecisionRef", "mutatedHost"]: + require(field in mutation, f"mutationReceipt missing {field}") + require(mutation["mode"] in {"dry-run", "live"}, "invalid mutation mode") + if mutation["mutatedHost"]: + require(mutation["mode"] == "live", "mutatedHost=true requires mode=live") + require(mutation.get("humanControlEventRef"), "live host mutation requires human control event ref") + + approval = data.get("operatorApprovalReceipt") + require(isinstance(approval, dict), "operatorApprovalReceipt must be object") + for field in ["approvalId", "actorRef", "subjectRef", "decision", "policyGateRef", "agentplaneRunRef", "deliveryExcellenceEventRef"]: + require(field in approval, f"operatorApprovalReceipt missing {field}") + require(approval["decision"] in {"approved", "rejected", "deferred", "accepted-risk", "revoked"}, "invalid operator decision") + + +def main() -> int: + try: + data = load_json(EXAMPLE) + validate(data) + except (json.JSONDecodeError, ValidationError) as exc: + print(f"TurtleTerm Agent Harness receipt validation failed: {exc}", file=sys.stderr) + return 1 + print("OK: TurtleTerm Agent Harness receipt fixture validates") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())