diff --git a/.github/workflows/agentic-shell.yml b/.github/workflows/agentic-shell.yml new file mode 100644 index 0000000..edaa1e6 --- /dev/null +++ b/.github/workflows/agentic-shell.yml @@ -0,0 +1,53 @@ +name: agentic-shell + +on: + pull_request: + paths: + - 'schemas/agentic-task.schema.json' + - 'schemas/agentic-workset.schema.json' + - 'schemas/agentic-evidence-packet.schema.json' + - 'schemas/agentic-recommended-action.schema.json' + - 'examples/agentic-shell/**' + - 'scripts/validate_agentic_shell.py' + - '.github/workflows/agentic-shell.yml' + push: + branches: + - main + paths: + - 'schemas/agentic-task.schema.json' + - 'schemas/agentic-workset.schema.json' + - 'schemas/agentic-evidence-packet.schema.json' + - 'schemas/agentic-recommended-action.schema.json' + - 'examples/agentic-shell/**' + - 'scripts/validate_agentic_shell.py' + - '.github/workflows/agentic-shell.yml' + workflow_dispatch: + +permissions: + contents: read + +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 + - name: Validate positive examples + run: python scripts/validate_agentic_shell.py + - name: Assert high-risk-without-approval fixture fails (approval model bites) + run: | + if python scripts/validate_agentic_shell.py \ + examples/agentic-shell/negative/agentic-recommended-action.high-risk-no-approval.fail.json; then + echo 'negative fixture unexpectedly passed: high-risk action without approval' + exit 1 + fi + - name: Assert waiting-for-approval-without-requirements fixture fails + run: | + if python scripts/validate_agentic_shell.py \ + examples/agentic-shell/negative/agentic-task.waiting-approval-empty.fail.json; then + echo 'negative fixture unexpectedly passed: waiting_for_approval without approvalRequirements' + exit 1 + fi diff --git a/docs/agentic-gitlab-style-shell.md b/docs/agentic-gitlab-style-shell.md new file mode 100644 index 0000000..4a20864 --- /dev/null +++ b/docs/agentic-gitlab-style-shell.md @@ -0,0 +1,72 @@ +# Agentic GitLab-Style Shell — object model (v0.1) + +This document records the first buildable slice of the *Agentic GitLab-Style Shell* +spec (v0.1): freezing the **task object schema** and defining the **approval / +policy contract** as machine-readable schemas in `sourceos-shell`. + +The spec redesigns the GitLab group/project shell from a navigation-first +administrative interface into a supervisory-control workspace for cross-repository +agentic operations. It makes the *task object* first-class so the shell can +organize around supervision (what is the goal, what is the agent doing, why, on +what evidence, what needs approval) rather than navigation. + +Per the spec's *Immediate next design steps*, step 1 ("Freeze the task object +schema") and step 2 ("Define the approval and policy engine contract") are +implemented here. The remaining steps (desktop shell anatomy, layout, keyboard +model, clickable prototype, telemetry) are UI/runtime work tracked separately — +see the linked issue. + +## What is frozen here + +| Object | Schema | Spec section | +|---|---|---| +| Task | `schemas/agentic-task.schema.json` | Object model → Task object; State model | +| Workset | `schemas/agentic-workset.schema.json` | Object model → Workset | +| Evidence packet | `schemas/agentic-evidence-packet.schema.json` | Object model → Evidence packet | +| Recommended action | `schemas/agentic-recommended-action.schema.json` | State model; Approval model | + +### State model + +* **Task states**: `drafting`, `ready`, `running`, `waiting_for_evidence`, + `waiting_for_approval`, `blocked`, `completed`, `aborted`, `rolled_back`. +* **Action states**: `proposed`, `staged`, `approved`, `executing`, `succeeded`, + `failed`, `reverted`. + +### Approval model (risk-based, explicit) + +Risk tiers `low` / `medium` / `high` drive approval. The schemas and validator +enforce the spec's rule that risk-based approval must be explicit: + +* every recommended action must state **why** it fell into its risk tier + (`riskRationale`, non-empty); +* `medium`/`high`-risk actions may not reach `approved` / `executing` / + `succeeded` without a non-empty `approvalRefs`; +* `high`-risk actions additionally require richer evidence (non-empty + `evidenceRefs`); +* a task in `waiting_for_approval` must carry non-empty `approvalRequirements`. + +These invariants live in `scripts/validate_agentic_shell.py` (beyond JSON Schema) +and are proven to bite by negative fixtures under +`examples/agentic-shell/negative/` that CI asserts must fail. + +## FIPS + +Any content digest (`auditLog[].contentDigest`, evidence `contentDigest`) is +SHA-256 — `sha256:<64 hex>` — enforced by both schema pattern and validator. + +## Validate locally + +```bash +python -m pip install jsonschema +python scripts/validate_agentic_shell.py +``` + +## Not yet buildable (tracked) + +The desktop shell anatomy (five persistent regions: global objective bar, task +status strip, left rail, center work canvas, right inspector, bottom timeline), +layout spec, keyboard/accessibility model, primary workflows (group health audit, +dependency update campaign, ownership remediation), preview/simulation and undo +surfaces, and telemetry require a shell frontend + orchestration runtime that +does not yet exist in this repo. These are filed as a follow-up issue with +acceptance criteria. diff --git a/examples/agentic-shell/agentic-evidence-packet.ci-failure.example.json b/examples/agentic-shell/agentic-evidence-packet.ci-failure.example.json new file mode 100644 index 0000000..6290338 --- /dev/null +++ b/examples/agentic-shell/agentic-evidence-packet.ci-failure.example.json @@ -0,0 +1,18 @@ +{ + "evidenceId": "urn:srcos:agentic-evidence:service-alpha-ci-fail-demo-0001", + "specVersion": "0.1.0", + "resourceId": "urn:srcos:repo:developers/service-alpha-demo", + "sourceType": "ci_run", + "sourcePointer": "urn:srcos:ci-run:developers/service-alpha-demo/pipeline-4821", + "contentDigest": "sha256:9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6b5a493827160f5e4d3c2b1a0", + "extractedClaim": "CI pipeline #4821 failed on step 'lint' at 2026-08-03T08:40:00Z.", + "confidence": 0.97, + "freshness": { + "observedAt": "2026-08-03T08:45:00Z", + "ageSeconds": 900, + "stale": false + }, + "policyRelevance": [ + "urn:srcos:policy:open-issues-only-in-active-repos" + ] +} diff --git a/examples/agentic-shell/agentic-recommended-action.high-risk-approved.example.json b/examples/agentic-shell/agentic-recommended-action.high-risk-approved.example.json new file mode 100644 index 0000000..54a90b2 --- /dev/null +++ b/examples/agentic-shell/agentic-recommended-action.high-risk-approved.example.json @@ -0,0 +1,27 @@ +{ + "actionId": "urn:srcos:agentic-action:security-remediation-gateway-demo-0002", + "specVersion": "0.1.0", + "taskId": "urn:srcos:agentic-task:dep-update-campaign-demo-0002", + "worksetId": null, + "actionClass": "security-remediation", + "riskTier": "high", + "riskRationale": "Security remediation with service impact on a production gateway; requires explicit approval and richer evidence.", + "state": "approved", + "targetRef": "urn:srcos:service:services/gateway-demo", + "evidenceRefs": [ + "urn:srcos:agentic-evidence:service-alpha-ci-fail-demo-0001" + ], + "preview": { + "diffRef": "urn:srcos:diff:gateway-demo-openssl-bump", + "draftPreviewRef": "urn:srcos:mr-draft:services/gateway-demo-bump-openssl", + "expectedBlastRadius": "gateway service + 3 downstream consumers", + "policyCompliant": true, + "rollbackPath": "revert MR + redeploy prior image" + }, + "approvalRefs": [ + "urn:srcos:approval:operator-demo-gateway-security-remediation-0002" + ], + "policyDecisionRefs": [ + "urn:srcos:policy-decision:agentic-action-security-remediation-gateway-demo-0002" + ] +} diff --git a/examples/agentic-shell/agentic-recommended-action.low-risk-staged.example.json b/examples/agentic-shell/agentic-recommended-action.low-risk-staged.example.json new file mode 100644 index 0000000..3c3f5a1 --- /dev/null +++ b/examples/agentic-shell/agentic-recommended-action.low-risk-staged.example.json @@ -0,0 +1,25 @@ +{ + "actionId": "urn:srcos:agentic-action:create-issue-draft-alpha-demo-0001", + "specVersion": "0.1.0", + "taskId": "urn:srcos:agentic-task:group-health-audit-demo-0001", + "worksetId": "urn:srcos:agentic-workset:unowned-repos-demo-0001", + "actionClass": "create-issue-draft", + "riskTier": "low", + "riskRationale": "Creates an issue draft only; does not open it and touches no production surface.", + "state": "staged", + "targetRef": "urn:srcos:repo:developers/service-alpha-demo", + "evidenceRefs": [ + "urn:srcos:agentic-evidence:service-alpha-ci-fail-demo-0001" + ], + "preview": { + "diffRef": null, + "draftPreviewRef": "urn:srcos:issue-draft:service-alpha-demo-ci-fail", + "expectedBlastRadius": "single repo, draft only", + "policyCompliant": true, + "rollbackPath": "discard draft" + }, + "approvalRefs": [], + "policyDecisionRefs": [ + "urn:srcos:policy-decision:agentic-action-create-issue-draft-alpha-demo-0001" + ] +} diff --git a/examples/agentic-shell/agentic-task.running-audit.example.json b/examples/agentic-shell/agentic-task.running-audit.example.json new file mode 100644 index 0000000..81ba360 --- /dev/null +++ b/examples/agentic-shell/agentic-task.running-audit.example.json @@ -0,0 +1,50 @@ +{ + "taskId": "urn:srcos:agentic-task:group-health-audit-demo-0001", + "specVersion": "0.1.0", + "goal": "Audit all repos in Developers for failing CI, stale dependencies, and missing owners. Draft issues but do not open them.", + "scope": { + "scopeRef": "urn:srcos:group:developers-demo", + "selectionQuery": "group:Developers", + "resourceRefs": [ + "urn:srcos:repo:developers/service-alpha-demo", + "urn:srcos:repo:developers/service-beta-demo" + ] + }, + "policies": [ + "urn:srcos:policy:no-prod-writes-without-approval", + "urn:srcos:policy:open-issues-only-in-active-repos" + ], + "autonomyLevel": "stage_low_risk", + "budget": { + "timeCapSeconds": 3600, + "actionCap": 50, + "costCapUnits": null + }, + "owner": "urn:srcos:subject:operator-demo", + "state": "running", + "createdAt": "2026-08-03T09:00:00Z", + "updatedAt": "2026-08-03T09:04:00Z", + "outputs": [], + "auditLog": [ + { + "at": "2026-08-03T09:00:00Z", + "event": "task.materialized_scope", + "actorRef": "urn:srcos:subject:operator-demo", + "contentDigest": "sha256:3b1f2c4d5e6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f7089a1b2c3d4e5f" + }, + { + "at": "2026-08-03T09:03:00Z", + "event": "task.health_matrix_populated", + "actorRef": null, + "contentDigest": null + } + ], + "confidenceSummary": { + "score": 0.82, + "uncertaintyNote": "Ownership signal ambiguous for 2 of 14 repos." + }, + "approvalRequirements": [], + "policyDecisionRefs": [ + "urn:srcos:policy-decision:agentic-task-group-health-audit-demo-0001" + ] +} diff --git a/examples/agentic-shell/agentic-task.waiting-for-approval.example.json b/examples/agentic-shell/agentic-task.waiting-for-approval.example.json new file mode 100644 index 0000000..d63caad --- /dev/null +++ b/examples/agentic-shell/agentic-task.waiting-for-approval.example.json @@ -0,0 +1,46 @@ +{ + "taskId": "urn:srcos:agentic-task:dep-update-campaign-demo-0002", + "specVersion": "0.1.0", + "goal": "Find all services with outdated critical dependencies. Prepare patch branches and draft MRs only for services with passing tests.", + "scope": { + "scopeRef": "urn:srcos:group:services-demo", + "selectionQuery": "dependency.severity:critical AND tests:passing", + "resourceRefs": [ + "urn:srcos:service:services/gateway-demo" + ] + }, + "policies": [ + "urn:srcos:policy:no-prod-writes-without-approval" + ], + "autonomyLevel": "suggest", + "budget": null, + "owner": "urn:srcos:subject:operator-demo", + "state": "waiting_for_approval", + "createdAt": "2026-08-03T10:00:00Z", + "updatedAt": "2026-08-03T10:12:00Z", + "outputs": [ + "urn:srcos:mr-draft:services/gateway-demo-bump-openssl" + ], + "auditLog": [ + { + "at": "2026-08-03T10:11:00Z", + "event": "task.escalated_high_risk", + "actorRef": null, + "contentDigest": null + } + ], + "confidenceSummary": { + "score": 0.9, + "uncertaintyNote": null + }, + "approvalRequirements": [ + { + "requirementRef": "urn:srcos:approval-req:gateway-demo-dependency-touches-build-config", + "riskTier": "medium", + "reason": "Dependency update touches build config." + } + ], + "policyDecisionRefs": [ + "urn:srcos:policy-decision:agentic-task-dep-update-campaign-demo-0002" + ] +} diff --git a/examples/agentic-shell/agentic-workset.unowned-repos.example.json b/examples/agentic-shell/agentic-workset.unowned-repos.example.json new file mode 100644 index 0000000..2db7cbe --- /dev/null +++ b/examples/agentic-shell/agentic-workset.unowned-repos.example.json @@ -0,0 +1,17 @@ +{ + "worksetId": "urn:srcos:agentic-workset:unowned-repos-demo-0001", + "specVersion": "0.1.0", + "taskId": "urn:srcos:agentic-task:group-health-audit-demo-0001", + "selectionQuery": "show only unowned repos with critical vulns and no release in 90 days", + "resources": [ + "urn:srcos:repo:developers/service-alpha-demo", + "urn:srcos:repo:developers/service-gamma-demo" + ], + "aggregateRisk": "high", + "recommendedActionRefs": [ + "urn:srcos:agentic-action:create-issue-draft-alpha-demo-0001" + ], + "blockedReasons": [ + "No CODEOWNERS present; owner cannot be auto-assigned." + ] +} diff --git a/examples/agentic-shell/negative/agentic-recommended-action.high-risk-no-approval.fail.json b/examples/agentic-shell/negative/agentic-recommended-action.high-risk-no-approval.fail.json new file mode 100644 index 0000000..8260c1c --- /dev/null +++ b/examples/agentic-shell/negative/agentic-recommended-action.high-risk-no-approval.fail.json @@ -0,0 +1,14 @@ +{ + "actionId": "urn:srcos:agentic-action:prod-config-change-demo-9001", + "specVersion": "0.1.0", + "taskId": "urn:srcos:agentic-task:dep-update-campaign-demo-0002", + "worksetId": null, + "actionClass": "prod-config-change", + "riskTier": "high", + "riskRationale": "Production config change.", + "state": "approved", + "targetRef": "urn:srcos:service:services/gateway-demo", + "evidenceRefs": [], + "approvalRefs": [], + "policyDecisionRefs": [] +} diff --git a/examples/agentic-shell/negative/agentic-task.waiting-approval-empty.fail.json b/examples/agentic-shell/negative/agentic-task.waiting-approval-empty.fail.json new file mode 100644 index 0000000..08d109d --- /dev/null +++ b/examples/agentic-shell/negative/agentic-task.waiting-approval-empty.fail.json @@ -0,0 +1,17 @@ +{ + "taskId": "urn:srcos:agentic-task:bad-waiting-approval-demo-9002", + "specVersion": "0.1.0", + "goal": "Apply mass permission changes across all repositories.", + "scope": { + "scopeRef": "urn:srcos:group:developers-demo" + }, + "autonomyLevel": "suggest", + "owner": "urn:srcos:subject:operator-demo", + "state": "waiting_for_approval", + "createdAt": "2026-08-03T11:00:00Z", + "updatedAt": "2026-08-03T11:05:00Z", + "approvalRequirements": [], + "policyDecisionRefs": [ + "urn:srcos:policy-decision:agentic-task-bad-waiting-approval-demo-9002" + ] +} diff --git a/schemas/agentic-evidence-packet.schema.json b/schemas/agentic-evidence-packet.schema.json new file mode 100644 index 0000000..78440a1 --- /dev/null +++ b/schemas/agentic-evidence-packet.schema.json @@ -0,0 +1,80 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.ai/sourceos-shell/agentic-evidence-packet.schema.json", + "title": "AgenticEvidencePacket", + "description": "Evidence packet backing a finding or recommended action in the Agentic GitLab-Style Shell. The right inspector is the trust surface: an agentic shell without an evidence surface becomes an opaque automation surface.", + "type": "object", + "additionalProperties": false, + "required": [ + "evidenceId", + "specVersion", + "resourceId", + "sourceType", + "sourcePointer", + "extractedClaim", + "confidence" + ], + "properties": { + "evidenceId": { + "type": "string", + "pattern": "^urn:srcos:agentic-evidence:" + }, + "specVersion": { "type": "string" }, + "resourceId": { + "type": "string", + "description": "URN reference to the repository, service, or subgroup the evidence concerns." + }, + "sourceType": { + "type": "string", + "description": "Kind of source the claim was extracted from.", + "enum": [ + "ci_run", + "dependency_manifest", + "codeowners", + "issue", + "merge_request", + "commit_activity", + "security_scan", + "documentation", + "release", + "other" + ] + }, + "sourcePointer": { + "type": "string", + "minLength": 1, + "description": "Verifiable pointer to the source (URL, ref, or urn). May carry an optional SHA-256 content digest via contentDigest." + }, + "contentDigest": { + "type": ["string", "null"], + "description": "Optional SHA-256 digest pinning the exact source content the claim was extracted from. FIPS: SHA-256 is authoritative.", + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "extractedClaim": { + "type": "string", + "minLength": 1, + "description": "The specific claim the agent extracted, e.g. 'CI pipeline #4821 failed on step lint'." + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "freshness": { + "type": ["object", "null"], + "description": "How recent the evidence is.", + "additionalProperties": false, + "properties": { + "observedAt": { "type": ["string", "null"], "format": "date-time" }, + "ageSeconds": { "type": ["integer", "null"], "minimum": 0 }, + "stale": { "type": ["boolean", "null"] } + } + }, + "policyRelevance": { + "type": "array", + "description": "Policy refs this evidence is relevant to (why approval is or is not required).", + "items": { "type": "string" }, + "default": [] + } + } +} diff --git a/schemas/agentic-recommended-action.schema.json b/schemas/agentic-recommended-action.schema.json new file mode 100644 index 0000000..12eb5c1 --- /dev/null +++ b/schemas/agentic-recommended-action.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.ai/sourceos-shell/agentic-recommended-action.schema.json", + "title": "AgenticRecommendedAction", + "description": "A single agent-proposed action within the Agentic GitLab-Style Shell. Risk-based approval must be explicit: the shell must always state why an action fell into a given risk tier, and high-risk actions require explicit approval and richer evidence before they may execute.", + "type": "object", + "additionalProperties": false, + "required": [ + "actionId", + "specVersion", + "taskId", + "actionClass", + "riskTier", + "riskRationale", + "state" + ], + "properties": { + "actionId": { + "type": "string", + "pattern": "^urn:srcos:agentic-action:" + }, + "specVersion": { "type": "string" }, + "taskId": { + "type": "string", + "pattern": "^urn:srcos:agentic-task:" + }, + "worksetId": { + "type": ["string", "null"], + "pattern": "^urn:srcos:agentic-workset:" + }, + "actionClass": { + "type": "string", + "description": "What the action does, e.g. 'create-issue-draft', 'apply-label', 'open-mr-draft', 'archive-candidate', 'dependency-update', 'prod-config-change', 'security-remediation', 'permission-change', 'mass-action'." + }, + "riskTier": { + "type": "string", + "description": "Risk tier that drives the approval model. low: auto-stage + batch approval. medium: contextual review. high: explicit approval + richer evidence.", + "enum": ["low", "medium", "high"] + }, + "riskRationale": { + "type": "string", + "minLength": 1, + "description": "Why the action fell into its risk tier. Mandatory for every action per spec." + }, + "state": { + "type": "string", + "description": "Action lifecycle state. Must be visually distinct and queryable.", + "enum": [ + "proposed", + "staged", + "approved", + "executing", + "succeeded", + "failed", + "reverted" + ] + }, + "targetRef": { + "type": ["string", "null"], + "description": "URN reference to the resource the action targets." + }, + "evidenceRefs": { + "type": "array", + "items": { "type": "string", "pattern": "^urn:srcos:agentic-evidence:" }, + "default": [] + }, + "preview": { + "type": ["object", "null"], + "description": "Pre-apply simulation surface: diff, draft preview, blast radius, policy compliance, rollback path.", + "additionalProperties": false, + "properties": { + "diffRef": { "type": ["string", "null"] }, + "draftPreviewRef": { "type": ["string", "null"] }, + "expectedBlastRadius": { "type": ["string", "null"] }, + "policyCompliant": { "type": ["boolean", "null"] }, + "rollbackPath": { "type": ["string", "null"] } + } + }, + "approvalRefs": { + "type": "array", + "description": "Approval decision refs. Required to be non-empty before a medium/high-risk action may reach approved/executing/succeeded.", + "items": { "type": "string" }, + "default": [] + }, + "policyDecisionRefs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + } + } +} diff --git a/schemas/agentic-task.schema.json b/schemas/agentic-task.schema.json new file mode 100644 index 0000000..e57add4 --- /dev/null +++ b/schemas/agentic-task.schema.json @@ -0,0 +1,151 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.ai/sourceos-shell/agentic-task.schema.json", + "title": "AgenticTask", + "description": "First-class Task object for the Agentic GitLab-Style Shell. The shell is a supervisory-control surface: it makes the task object explicit (goal, scope, constraints, autonomy, state, evidence, outputs) so the surface can organize around supervision rather than navigation. The shell does not own Policy Fabric or Operation Plane runtime logic; it projects and routes.", + "type": "object", + "additionalProperties": false, + "required": [ + "taskId", + "specVersion", + "goal", + "scope", + "autonomyLevel", + "owner", + "state", + "createdAt", + "updatedAt", + "policyDecisionRefs" + ], + "properties": { + "taskId": { + "type": "string", + "pattern": "^urn:srcos:agentic-task:" + }, + "specVersion": { "type": "string" }, + "goal": { + "type": "string", + "minLength": 1, + "description": "Operator goal in natural language or mixed structured tokens, e.g. 'Audit all repos in Developers for failing CI, stale dependencies, and missing owners. Draft issues but do not open them.'" + }, + "scope": { + "type": "object", + "description": "Explicit scope the task is authorized to operate over.", + "additionalProperties": false, + "required": ["scopeRef"], + "properties": { + "scopeRef": { + "type": "string", + "description": "URN reference to the group / subgroup / repository set the task is bound to." + }, + "selectionQuery": { "type": ["string", "null"] }, + "resourceRefs": { + "type": "array", + "items": { "type": "string" }, + "default": [] + } + } + }, + "policies": { + "type": "array", + "description": "Policy envelope refs applied to this task (e.g. 'no prod writes without approval', 'open issues only in active repos'). Authored by Policy Fabric; the shell references them.", + "items": { "type": "string" }, + "default": [] + }, + "autonomyLevel": { + "type": "string", + "description": "How far the task may act without human approval. Higher levels never bypass the risk-based approval model for medium/high-risk actions.", + "enum": [ + "read_only", + "suggest", + "stage_low_risk", + "execute_approved" + ] + }, + "budget": { + "type": ["object", "null"], + "description": "Optional budget / time cap for the task.", + "additionalProperties": false, + "properties": { + "timeCapSeconds": { "type": ["integer", "null"], "minimum": 0 }, + "actionCap": { "type": ["integer", "null"], "minimum": 0 }, + "costCapUnits": { "type": ["number", "null"], "minimum": 0 } + } + }, + "owner": { + "type": "string", + "description": "URN reference to the human operator accountable for the task." + }, + "state": { + "type": "string", + "description": "Task lifecycle state. States must be visually distinct and queryable in the shell.", + "enum": [ + "drafting", + "ready", + "running", + "waiting_for_evidence", + "waiting_for_approval", + "blocked", + "completed", + "aborted", + "rolled_back" + ] + }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, + "outputs": { + "type": "array", + "description": "Refs to artefacts the task produced (issue drafts, MR drafts, documentation diffs, reports).", + "items": { "type": "string" }, + "default": [] + }, + "auditLog": { + "type": "array", + "description": "Append-only audit trail of task events. Each entry is content-addressable; when a digest is present it MUST be SHA-256 (FIPS).", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["at", "event"], + "properties": { + "at": { "type": "string", "format": "date-time" }, + "event": { "type": "string" }, + "actorRef": { "type": ["string", "null"] }, + "contentDigest": { + "type": ["string", "null"], + "description": "SHA-256 digest of the referenced event payload. FIPS: SHA-256 is authoritative.", + "pattern": "^sha256:[a-f0-9]{64}$" + } + } + }, + "default": [] + }, + "confidenceSummary": { + "type": ["object", "null"], + "description": "Aggregate confidence / uncertainty summary surfaced in the status strip.", + "additionalProperties": false, + "properties": { + "score": { "type": ["number", "null"], "minimum": 0, "maximum": 1 }, + "uncertaintyNote": { "type": ["string", "null"] } + } + }, + "approvalRequirements": { + "type": "array", + "description": "Explicit approval requirements that must be satisfied before the task may leave waiting_for_approval. Non-empty whenever state is waiting_for_approval.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["requirementRef", "riskTier"], + "properties": { + "requirementRef": { "type": "string" }, + "riskTier": { "type": "string", "enum": ["low", "medium", "high"] }, + "reason": { "type": ["string", "null"] } + } + }, + "default": [] + }, + "policyDecisionRefs": { + "type": "array", + "items": { "type": "string" } + } + } +} diff --git a/schemas/agentic-workset.schema.json b/schemas/agentic-workset.schema.json new file mode 100644 index 0000000..9c33998 --- /dev/null +++ b/schemas/agentic-workset.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.sourceos.ai/sourceos-shell/agentic-workset.schema.json", + "title": "AgenticWorkset", + "description": "A task-scoped collection of entities selected for comparison or batch action in the Agentic GitLab-Style Shell. Multi-select in the center work canvas materializes a workset and updates aggregate risk and batch controls.", + "type": "object", + "additionalProperties": false, + "required": [ + "worksetId", + "specVersion", + "taskId", + "resources" + ], + "properties": { + "worksetId": { + "type": "string", + "pattern": "^urn:srcos:agentic-workset:" + }, + "specVersion": { "type": "string" }, + "taskId": { + "type": "string", + "pattern": "^urn:srcos:agentic-task:" + }, + "selectionQuery": { + "type": ["string", "null"], + "description": "Structured or natural-language selection that produced the workset, e.g. 'show only unowned repos with critical vulns and no release in 90 days'." + }, + "resources": { + "type": "array", + "description": "Resource refs in the workset.", + "items": { "type": "string" }, + "minItems": 1 + }, + "aggregateRisk": { + "type": ["string", "null"], + "enum": ["low", "medium", "high", null], + "description": "Aggregate risk across the workset, used to gate batch controls." + }, + "recommendedActionRefs": { + "type": "array", + "items": { "type": "string", "pattern": "^urn:srcos:agentic-action:" }, + "default": [] + }, + "blockedReasons": { + "type": "array", + "items": { "type": "string" }, + "default": [] + } + } +} diff --git a/scripts/validate_agentic_shell.py b/scripts/validate_agentic_shell.py new file mode 100644 index 0000000..b9cc7c7 --- /dev/null +++ b/scripts/validate_agentic_shell.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Validate Agentic GitLab-Style Shell example JSON files against their JSON Schemas. + +Mirrors the structure of scripts/validate_workspace_ops.py, and adds invariant +"teeth" beyond what JSON Schema can express so the approval model actually bites: + + * high/medium-risk actions may not reach approved/executing/succeeded without + a non-empty approvalRefs; + * high-risk actions require richer evidence (non-empty evidenceRefs); + * a task in waiting_for_approval must carry non-empty approvalRequirements; + * every recommended action must state why it fell into its risk tier; + * any content digest is SHA-256 (FIPS). + +Usage: + validate_agentic_shell.py # validate all positive examples (must pass) + validate_agentic_shell.py FILE [FILE ...] # validate specific files (nonzero exit on failure) +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import jsonschema + +ROOT = Path(__file__).resolve().parents[1] +EXAMPLE_DIR = ROOT / "examples" / "agentic-shell" +SCHEMA_DIR = ROOT / "schemas" + +SCHEMAS = { + "agentic-task": SCHEMA_DIR / "agentic-task.schema.json", + "agentic-workset": SCHEMA_DIR / "agentic-workset.schema.json", + "agentic-evidence-packet": SCHEMA_DIR / "agentic-evidence-packet.schema.json", + "agentic-recommended-action": SCHEMA_DIR / "agentic-recommended-action.schema.json", +} + +# Longest prefix first so 'agentic-recommended-action' wins over 'agentic'. +PREFIXES = sorted(SCHEMAS.keys(), key=len, reverse=True) + +_SHA256 = "sha256:" +_ACTION_ACTED_STATES = {"approved", "executing", "succeeded"} + + +def _key_for(path: Path) -> str: + for prefix in PREFIXES: + if path.name.startswith(prefix): + return prefix + raise ValueError(f"No schema mapping found for example file: {path.name}") + + +def _require_sha256(value, path: Path, field: str) -> None: + if value is None: + return + if not (isinstance(value, str) and value.startswith(_SHA256) and len(value) == len(_SHA256) + 64): + raise ValueError(f"{path}: {field} must be a SHA-256 digest 'sha256:<64 hex>' (FIPS)") + + +def _check_task(data: dict, path: Path) -> None: + if not data.get("taskId", "").startswith("urn:srcos:agentic-task:"): + raise ValueError(f"{path}: taskId must start with 'urn:srcos:agentic-task:'") + if not data.get("policyDecisionRefs"): + raise ValueError(f"{path}: policyDecisionRefs must be non-empty") + if data.get("state") == "waiting_for_approval" and not data.get("approvalRequirements"): + raise ValueError( + f"{path}: state 'waiting_for_approval' requires non-empty approvalRequirements" + ) + for entry in data.get("auditLog", []) or []: + _require_sha256(entry.get("contentDigest"), path, "auditLog[].contentDigest") + + +def _check_action(data: dict, path: Path) -> None: + if not data.get("actionId", "").startswith("urn:srcos:agentic-action:"): + raise ValueError(f"{path}: actionId must start with 'urn:srcos:agentic-action:'") + if not (data.get("riskRationale") or "").strip(): + raise ValueError(f"{path}: riskRationale must state why the action fell into its risk tier") + risk = data.get("riskTier") + state = data.get("state") + if risk in {"medium", "high"} and state in _ACTION_ACTED_STATES and not data.get("approvalRefs"): + raise ValueError( + f"{path}: {risk}-risk action in state '{state}' requires non-empty approvalRefs" + ) + if risk == "high" and state in _ACTION_ACTED_STATES and not data.get("evidenceRefs"): + raise ValueError( + f"{path}: high-risk action in state '{state}' requires richer evidence (evidenceRefs)" + ) + + +def _check_workset(data: dict, path: Path) -> None: + if not data.get("worksetId", "").startswith("urn:srcos:agentic-workset:"): + raise ValueError(f"{path}: worksetId must start with 'urn:srcos:agentic-workset:'") + if not data.get("resources"): + raise ValueError(f"{path}: resources must be non-empty") + + +def _check_evidence(data: dict, path: Path) -> None: + if not data.get("evidenceId", "").startswith("urn:srcos:agentic-evidence:"): + raise ValueError(f"{path}: evidenceId must start with 'urn:srcos:agentic-evidence:'") + _require_sha256(data.get("contentDigest"), path, "contentDigest") + + +EXTRA_CHECKS = { + "agentic-task": _check_task, + "agentic-workset": _check_workset, + "agentic-evidence-packet": _check_evidence, + "agentic-recommended-action": _check_action, +} + + +def validate_example(path: Path) -> str: + key = _key_for(path) + schema = json.loads(SCHEMAS[key].read_text(encoding="utf-8")) + jsonschema.validators.validator_for(schema).check_schema(schema) + data = json.loads(path.read_text(encoding="utf-8")) + jsonschema.validate(data, schema) + EXTRA_CHECKS[key](data, path) + return path.name + + +def main(argv: list[str]) -> int: + if argv: + paths = [Path(a) for a in argv] + else: + paths = sorted(EXAMPLE_DIR.glob("*.example.json")) + if not paths: + raise SystemExit("No agentic-shell example files found") + checked = [validate_example(path) for path in paths] + print(json.dumps({"ok": True, "checked": checked}, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))