diff --git a/docs/AUTOMATION-ADAPTER.md b/docs/AUTOMATION-ADAPTER.md new file mode 100644 index 0000000..4b85c4d --- /dev/null +++ b/docs/AUTOMATION-ADAPTER.md @@ -0,0 +1,756 @@ +# Coven Automation → Psyche Orchestration Adapter + +**Status:** proposed (spec/design — no runtime behavior changes in this document) +**Issue:** [OpenCoven/psyche#18](https://github.com/OpenCoven/psyche/issues/18) — *P1: Define the Psyche adapter for automation-triggered orchestration without owning schedules* +**Cross-repository program:** [OpenCoven/coven#854](https://github.com/OpenCoven/coven/issues/854) (program), [#855](https://github.com/OpenCoven/coven/issues/855) (core protocol), [#857](https://github.com/OpenCoven/coven/issues/857) (identity/authority), [#858](https://github.com/OpenCoven/coven/issues/858) (certification), [OpenCoven/familiar-contract#17](https://github.com/OpenCoven/familiar-contract/issues/17) (familiar binding), [OpenCoven/coven-threads#29](https://github.com/OpenCoven/coven-threads/issues/29) (authority profile) + +This document defines the canonical adapter by which one Coven automation occurrence +invokes Psyche for multi-step, multi-lane, approval-aware orchestration **without +transferring schedule, occurrence, or run authority into Psyche**. Coven stays the +clock, the occurrence fence, the run/attempt ledger, the dispatch authority, and the +receipt committer. Psyche receives one adopted, identity- and authority-bound +invocation, orchestrates it under its task/lane/lease/approval/recovery semantics, +and returns durable correlated evidence. + +Schemas in this document are normative wire shapes published as JSON Schema drafts +under `docs/schemas/automation-adapter/`; the Rust contract types that will register +them are implementation work, deliberately out of scope here. Every claim about +current behavior cites the owning code path. + +## 1. Artifacts published by this document + +| Artifact | Path | +|---|---| +| Invocation contract | `docs/schemas/automation-adapter/psyche.automation_invocation.v1.schema.json` | +| Event contract | `docs/schemas/automation-adapter/psyche.automation_event.v1.schema.json` | +| Result contract | `docs/schemas/automation-adapter/psyche.automation_result.v1.schema.json` | +| Error contract | `docs/schemas/automation-adapter/psyche.automation_error.v1.schema.json` | +| Golden brief (referenced bytes) | `docs/schemas/automation-adapter/examples/orchestration-brief-v1.json` | +| Invocation golden | `docs/schemas/automation-adapter/examples/automation-invocation-v1-golden.json` | +| Event golden | `docs/schemas/automation-adapter/examples/automation-event-v1-golden.json` | +| Result golden | `docs/schemas/automation-adapter/examples/automation-result-v1-golden.json` | + +The JSON Schemas are the normative wire shape. The Rust contract types that will +carry the same fields are derived from them and must register in the core registry +(decision D1). Until that registration lands, these schemas are the only published +statement of the contract, and no Psyche API may accept or emit automation +invocation, event, result, or error documents in any other shape. + +## 2. Direction of the seam + +Psyche already owns one Coven-facing boundary, in the opposite direction: the +`CovenPort` trait in `crates/psyche-coven/src/port.rs` is where Psyche *dispatches +work into* Coven sessions — `CovenPort::adopt` adopts a digest-attested +`ExecutionRequestInput` (`schema_version` `psyche.execution_request.v1`, const +`EXECUTION_REQUEST_SCHEMA` in that file), `CovenPort::events` reads ordered pages, +`CovenPort::terminate` closes a session, and `persist_then_terminate` in the same +file fixes the persist-before-dispatch ordering. + +Issue #18 is the inverse arrow: Coven is the client, Psyche is the orchestrator. +Everything in this document is written for that direction, and it reuses the +established mechanics — digest-attested envelopes, exact-replay adoption +dispositions, strictly ordered cursor pages, content-addressed evidence, +persist-before-dispatch cancellation — rather than inventing a second set. + +## 3. Canonical boundary + +### 3.1 Coven owns + +- Automation definition and its exact revision. Nothing in Psyche names, versions, + or stores an automation definition; the invocation carries + `automation.definition_revision` and `automation.definition_digest` as opaque + pinned values. +- Trigger evaluation and occurrence planning. Psyche receives the resulting + `occurrence.occurrence_key` and `occurrence.fence_generation` verbatim and never + derives eligibility from them. +- The occurrence fence and claim/lease. Psyche sees only the generation number; a + fenced generation makes later Psyche evidence stale, not authoritative. +- Authenticated principal/familiar/authority/runtime binding: the invocation + references them (`principal.principal_context_ref`, `familiar_binding.*`, + `authority.*`); Psyche consumes the references, it does not resolve or re-derive + them. This mirrors the existing rule that Psyche records never *select* familiar + identity — `ExecutionBinding::familiar_snapshot_id` and `GraphNode`'s + `familiar_snapshot_id` (`crates/psyche-core/src/contracts/execution.rs`, + `crates/psyche-core/src/contracts/graph.rs`) are always references into + Coven-pinned `IdentitySnapshot` records (`ids_`, `crates/psyche-core/src/id.rs`). +- Automation run/attempt lifecycle and retries, cancellation/recovery decisions at + the automation level, and the final automation receipt and delivery status. + Raw automation ledger states stay Coven-side exactly as the raw Coven ledger + statuses (`created`, `running`, `idle`, `completed`, `failed`, `killed`, + `orphaned`) are already forbidden from manufacturing cancellation evidence + (`crates/psyche-test-support/src/suites/coven.rs`, `RAW_LEDGER_STATES`, and + `docs/SCHEMAS.md` § Cancellation and results). + +### 3.2 Psyche owns + +- Orchestration request validation and adoption (§9). +- Task/lane decomposition and dependencies inside its own graph: one adopted + invocation creates one Psyche execution graph, stored under the existing + `psyche.graph.v1` / `psyche.graph_node.v1` contracts with their frozen 11-value + `GraphState` and 22-value `NodeState` vocabularies + (`crates/psyche-core/src/contracts/graph.rs`). The adapter boundary never exposes + the internal graph directly; it exposes events and the terminal result. +- Psyche leases and worker assignments. `Recovery` already carries `lease_id`, + `fence_token`, and `ambiguity` + (`crates/psyche-core/src/contracts/foundation.rs`); lease and worker-attempt + internals stay Psyche-internal and appear at the boundary only as + `lease_ref`/`worker_session_ref` correlation strings. +- Orchestration approvals required by the Psyche protocol (`waiting_approval` + states and the `apr_` approval records, `crates/psyche-core/src/contracts/foundation.rs`). +- Step/artifact/result evidence (`ResultBundle`/`ContentAddressedReference` + precedent, `crates/psyche-coven/src/port.rs`). +- Orchestration recovery and terminal result semantics (`recovery_required` is + already a frozen terminal graph state). +- The versioned `psyche.automation_result.v1` receipt consumed by Coven (§6). + +### 3.3 Neither may + +- Create a second copy of familiar identity or authority policy: familiar identity + has exactly one durable record shape (`IdentitySnapshot`, `ids_`) and the + invocation carries a *reference plus digest*, never an embedded copy. +- Infer authorization from task text: authorization arrives only as + `authority.threads_decision_*`, `authority.approval_evidence[]`, and + `capabilities` fields, each digest-pinned. +- Rewrite the other system's terminal history: Psyche appends evidence; Coven owns + its run/attempt ledger. The store's append-only machinery + (`crates/psyche-store/src/transitions.rs`, strictly increasing `record_version` + with canonical digests; `crates/psyche-store/src/execution_bindings.rs`, + `validate_revision_chain`) is the existing enforcement pattern for both sides. +- Retry ambiguous external side effects blindly: ambiguous effects are disclosed + (`external_effects` in §6) and automation retry is a Coven decision, never + automatic (§11). +- Collapse the seven identifier layers of §4 into one ambiguous object — the + explicit prohibition on reusing a generic `executionBinding` object for + semantically different layers (§4). + +## 4. Semantic separation + +Seven distinct objects, each with exactly one ownership source. No generic +`executionBinding` object is reused across layers; in particular the existing +`ExecutionBinding` (`psyche.execution_binding.v1`, `att_` records — the only +`SchemaKind` that maps onto `RecordKind::Attempt`, +`crates/psyche-core/src/contracts/mod.rs` `SchemaKind::record_kind`) remains +exactly what it is today, the *Psyche→Coven session* binding. The automation +invocation is a different object with a different record identity (decision D3): +an execution binding binds one graph-node attempt to one Coven session; an +automation invocation binds one Coven automation attempt to one Psyche execution. + +| # | Layer | Owned by | Identifier | Durable where | +|---|---|---|---|---| +| 1 | Automation occurrence (why/when work became eligible) | Coven | `occurrence_id`, `occurrence_key`, `fence_generation` (opaque strings + safe integer) | Coven | +| 2 | Automation run — authoritative execution lifecycle | Coven | `coven.run_id` (opaque) | Coven | +| 3 | Automation attempt — one fenced dispatch attempt | Coven | `coven.attempt_id` (opaque) | Coven | +| 4 | Psyche invocation/execution — one adopted orchestration request | Psyche | `invocation_id` (`aiv_` + 26-char ULID, `RecordId` grammar of `crates/psyche-core/src/id.rs`) | Psyche | +| 5 | Psyche task/lane/lease — internal protocol work graph | Psyche | `grf_`/`nod_` records + `lease_ref` (opaque) | Psyche | +| 6 | Worker/runtime session attempt — concrete execution embodiment | Psyche | `worker_session_ref` (opaque) | Psyche | +| 7 | Artifacts/results/receipts — evidence per layer | producer layer | `ars_` result, content-addressed artifacts | producer of that layer | + +Correlation rules: + +- `psyche.automation_invocation.v1` carries the Coven identifiers (`run_id`, + `attempt_id`, `occurrence_id`) as opaque bounded strings; Psyche echoes them + verbatim in every event payload and in the terminal result `correlation` block. + `ResultBundle` already establishes the echo-the-complete-correlation rule at the + reverse boundary (`crates/psyche-coven/src/port.rs`, `ArtifactReference` and + `ResultBundle::validate` reject mismatched correlations; `PortError::CorrelationMismatch`). +- The invocation digest (`invocation_digest`) is the one hash that binds bytes; + every other identifier names a record, never a byte sequence. This is the same + separation `ExecutionBinding` draws between `request_id` and `request_digest` + (`crates/psyche-core/src/contracts/execution.rs`). +- `executionBinding` (or any single object) must not carry semantically different + layers. Lane and worker-attempt correlation live in the result's `orchestration.lanes` + with per-lane `worker_session_ref`/`lease_ref`, never as overloads of the + automation correlation block. + +## 5. Invocation contract — `psyche.automation_invocation.v1` + +Envelope (precedent: `AdoptionRequest { input, request_digest }` in +`crates/psyche-coven/src/port.rs`, digest over the complete typed content): + +```text +{ "invocation": , "invocation_digest": "sha256:" } +``` + +- `invocation_digest` = SHA-256 over the canonical (RFC 8785) JSON bytes of the + `invocation` object alone, computed with `canonical_bytes` semantics + (`crates/psyche-core/src/digest.rs`: RFC 8785 canonicalization, `sha256:` + prefix, 64 lowercase hex, digest recomputed before authority or persistence + accepts it, per `docs/SCHEMAS.md` § Registry and decoding). +- The receiver must recompute the digest with constant-time comparison before any + consequential work (`AdoptionRequest::validate_digest` precedent, constant-time + `constant_time_equal` in `crates/psyche-coven/src/port.rs`). +- Every field required by issue #18 has a schema slot: stable invocation/adoption + ID (`invocation_id`), automation ID/revision/digest, occurrence ID/key/fence + generation, Coven run/attempt IDs, principal context reference, familiar binding + reference/digest, Threads decision and approval evidence references/digests, + requested/granted capabilities and nine explicit scope classes (principal, + familiar, project, repository, paths, network, credentials, publication, + runtime), action/risk class, brief by digest, project/workspace/repository + context, runtime/worker constraints, deadline/cancellation channel, result + expectations, correlation/causation/event cursor, producer identity and pinned + contract versions, privacy/retention class. +- Bounds are the repo's bounds: ≤1 MiB encoded (`MAX_DOCUMENT_BYTES`, + `crates/psyche-core/src/contracts/mod.rs`), safe integers ≤ 2^53−1 + (`MAX_SAFE_INTEGER`), opaque strings ≤255 bytes, capability/token lists ≤64, + strict lowercase media types and canonical absolute paths reusing the exact + validation rules of `validate_media_type`/`validate_absolute_path` + (`crates/psyche-coven/src/port.rs`), RFC 3339 UTC timestamps with `Z` only + (UTC-offset checks in `psyche-core`/`psyche-coven` validators). +- `capabilities.granted` must be a subset of `capabilities.requested`, and every + `bindings_exercised.runtime_capabilities` value in the eventual result must be a + subset of `runtime.required_capabilities` (verified mechanically on the golden + artifacts; enforced by test A-10). +- The brief travels out of band, pinned by `brief.digest`/`size_bytes`/ + `media_type` — the same digest-only pattern as `ExecutionRequestInput`'s + `payload_digest` and `context_manifest_digest`. Psyche must verify brief bytes + against `brief.digest` before planning (`ContentAddressedReference::validate_payload` + precedent in `crates/psyche-coven/src/port.rs`). + +Golden vector: `docs/schemas/automation-adapter/examples/automation-invocation-v1-golden.json` +(canonical bytes, no trailing newline, pinned digests per the golden rules of +`docs/TESTING.md`). Its `brief.digest` binds the shipped +`orchestration-brief-v1.json`; `invocation_digest` recomputes from the envelope's +`invocation` content. + +## 6. Result contract — `psyche.automation_result.v1` + +Envelope `{ result, result_digest }`, digest over the canonical `result` object. +Emitted exactly once per invocation, at terminal state only. Statuses are the +frozen terminal set `completed | failed | cancelled | recovery_required`; there is +no running or partial result — interim observation travels on the event stream +(§8.4). Timeout or lost worker evidence may produce `failed` or `recovery_required`, +never inferred success. + +The result carries exactly what the issue's receipt contract requires: invocation +ID and adopted request digest (echoed, cross-checked by Coven against its own +dispatch record before the evidence is accepted), echoed occurrence/run/attempt +IDs, per-lane terminal states with worker/lease references, the exact +familiar/authority/runtime bindings exercised, approval events with dispositions, +capability narrowing per lane, content-addressed artifacts with expiry +(`ContentAddressedReference` shape), verification evidence per lane, +cancellation and recovery histories using the frozen acknowledgement vocabulary, +external-effect disclosure with an explicit `ambiguous` state, timing and final +event cursor, privacy/retention classification, and an authentication block whose +v1 scheme is `sha256-digest-v1` (the result digest attests the bytes; a signature +scheme is a deliberate future major-version decision, decision D5). + +Receipt composition: Coven embeds or references this result in its final +Automation Receipt. Psyche's evidence proves only the orchestration observations +Psyche owns; the receipt's proof of automation lifecycle, binding, and delivery +remains Coven's. This mirrors the existing division in which +`SessionSnapshot.terminal_state` is an observation that never manufactures +cancellation evidence (`crates/psyche-coven/src/port.rs`, `SessionSnapshot`; +`docs/SCHEMAS.md` § Cancellation and results). + +## 7. Error contract — `psyche.automation_error.v1` + +A persistable, payload-light refusal document (`schema_version`, frozen `code` +vocabulary, `refusal_class` of `permanent | retryable_after_reconciliation | +transport`, UTC `occurred_at`, bounded detail that never embeds rejected bytes). +Rejections reuse the payload-light style of `RejectionReason`/`RejectedDocument` +(`crates/psyche-core/src/contracts/mod.rs`): at most a stable field name is +retained, never the rejected payload. + +The in-process boundary keeps using the payload-free `PortError` vocabulary +(`crates/psyche-coven/src/error.rs`); the JSON document is the transport +projection, and at implementation time each automation code maps into the single +canonical `ErrorCode::ALL` registry (`crates/psyche-core/src/contracts/error.rs`) +so the daemon surface can represent it without a second vocabulary — the registry +already contains the precedent codes `coven_adoption_unknown`, +`coven_cancellation_unknown`, `coven_intent_conflict`, and `delegation_widened` +(decision D4). Mapping: + +| automation code | PortError projection | +|---|---| +| `invocation_invalid` | `InvalidRequest` | +| `invocation_conflict` | `IntentConflict` | +| `invocation_unknown` | `NotFound` | +| `invocation_stale` | `PolicyDenied` | +| `version_unsupported` | `ContractUnsupported` | +| `binding_mismatch`, `principal_mismatch` | `CorrelationMismatch` | +| `authority_missing`, `authority_expired` | `PolicyDenied` | +| `capability_unavailable` | `CapabilityMissing` | +| `runtime_unsatisfiable` | `PolicyDenied` | +| `deadline_exceeded` | `Unavailable` | +| `orchestration_store_unavailable` | `Unavailable` | +| `cancellation_unknown` | `Unavailable` | +| `reconciliation_unresolved` | `Unavailable` | + +`refusal_class = permanent` is terminal for the invocation identity; +`retryable_after_reconciliation` demands lookup by the existing `invocation_id` +(never a new key); `transport` says nothing about durable state. This is the +automation-boundary counterpart of `AdoptionDisposition::{Adopted, +ProvenNotAdopted, Unknown}` (`crates/psyche-coven/src/port.rs`), where `Unknown` +exists precisely so that a timeout after possible adoption is reconciled by +lookup, never retried blind. + +## 8. State machines + +### 8.1 Invocation adoption (Psyche-owned) + +```text + validate + verify digest + check fence/authority + received ────────────────────────────────┬──────────────► adopted + │ │ (idempotent: + ├─ refused │ redelivery of + │ (invalid, stale, the same bytes + │ authority/ │ returns the + │ binding │ existing + │ mismatch) │ adoption) + └─ conflict ─────┘ + (same invocation_id, + different invocation_digest) +``` + +- `received → adopted` happens in one transaction that writes the invocation + record and the adoption fact atomically before any task, lane, or external work + exists. The store's immediate-transaction + canonical-bytes-equality pattern + (`crates/psyche-store/src/execution_bindings.rs`, `insert_in_transaction`: + exact replay returns `InsertStatus::AlreadyPresent`, divergent bytes are + `ExecutionBindingRevisionConflict`) is the durable mechanism to reuse. +- `conflict` is terminal for the identity: the same `invocation_id` with different + bytes is a conflict (`psyche.automation_error.v1`, `invocation_conflict` with + `detail.conflict_digest`), never a second graph. +- `refused` is terminal and persistable as refusal evidence; it is not a retry + target. +- Idempotent redelivery of the same `(invocation_id, invocation_digest)` returns + the existing adoption and correlation. There is no `submitting` state on the + Psyche side: adoption is transactional, so from Coven's side the disposition is + the three-valued `Adopted | ProvenNotAdopted | Unknown` shape that + `AdoptionDisposition` already fixes (`crates/psyche-coven/src/port.rs`). +- Coven does not mark its attempt running or succeeded because Psyche accepted + transport. Coven's attempt state may advance only on durable, digest-attested + adoption evidence — the same rule that forbids raw Coven ledger statuses from + manufacturing cancellation acknowledgement (`crates/psyche-test-support/src/suites/coven.rs`, + `RAW_LEDGER_STATES`; `docs/SCHEMAS.md` § Cancellation and results). +- Psyche never marks the automation run terminal. It emits its own authoritative + result/event evidence; the run's terminal state is Coven's decision (§3.1). + +### 8.2 Orchestration execution (Psyche-owned) + +One adopted invocation creates one Psyche execution graph whose state is the +already-frozen `GraphState` vocabulary (`crates/psyche-core/src/contracts/graph.rs`; +`inspect_typed_enums` in `crates/psyche-core/src/contracts/mod.rs` freezes the +wire spellings). No second lifecycle vocabulary is introduced: + +| GraphState | Automation meaning | Producer of the transition | Typical event kind | +|---|---|---|---| +| `draft` | planning not yet committed | Psyche | — (never leaves the boundary as an event) | +| `admitted` | brief verified, plan admitted | Psyche | `orchestration_started` | +| `running` | lanes executing under leases | Psyche | `lane_state_changed`, `worker_session_bound` | +| `waiting_approval` | a protected step needs a Threads decision | Psyche | `approval_required` | +| `waiting_evidence` | verification evidence outstanding | Psyche | `artifact_produced` | +| `cancelling` | cancellation propagated | Psyche (on Coven request) | `cancellation_requested` | +| `completed` / `failed` / `cancelled` | terminal; result emitted | Psyche | `execution_completed` / `execution_failed` / `cancellation_acknowledged` | +| `recovery_required` | ambiguous; recovery evidence owed | Psyche | `recovery_required`, `ambiguous_effect_disclosed` | + +Lane-level states reuse the frozen 22-value `NodeState` vocabulary, including +`ready → reserved → dispatching → adopted` (with `adoption_unknown` / +`proven_not_adopted`), `waiting_approval`, `awaiting_verification → verified`, +`cancelling → cancelled`, `termination_unknown`, and `recovery_required`. Lane +adoption ambiguity reuses the proven_not_adopted/adoption_unknown discipline +already frozen for nodes. + +### 8.3 Cancellation (frozen vocabulary, reused) + +The automation adapter reuses the core-owned `CancellationState` vocabulary and +its evidence rules unchanged (`crates/psyche-core/src/contracts/execution.rs`): +`not_requested → termination_requested → acknowledged_terminated | +acknowledged_already_terminal | termination_unknown`, with acknowledgement +carried by `CancellationAcknowledgementEvidence` and unresolved outcomes by +`CancellationUnresolvedEvidence` — the exact types +`derive_termination_outcome_revision` already validates and persists before +success (`crates/psyche-coven/src/port.rs`, `derive_termination_outcome_revision`, +`persist_then_terminate`). Raw statuses never manufacture acknowledgement +(`crates/psyche-test-support/src/suites/coven.rs`, `RAW_LEDGER_STATES`). + +In the automation direction the identifiers map as: `session_id` → the +lane/worker session reference, `execution_request_id`/`execution_request_digest` +→ the adopted invocation correlation (`invocation_id`/`invocation_digest`), +`termination_request_id` → a Coven-minted cancellation request ID. All evidence +window rules (acknowledgement inside `[created_at, valid_until]`, refusal of +zero-digest authority evidence, `in_window` checks) carry over unchanged because +they are already contract-level validations in `psyche-core`/`psyche-coven`. + +### 8.4 Event stream rules + +- Identity is `(invocation_id, sequence)`; sequences start at 1 and are strictly + increasing with no gaps (`EventPage::validate_for` already enforces exactly + this ordering discipline for the CovenPort direction, + `crates/psyche-coven/src/port.rs`). +- The payload hash chain (`previous_payload_digest`, rooted at the adopted + `invocation_digest` for sequence 1) makes duplicate, reordered, or tampered + delivery detectable; a consumer that observes a break holds or requests a + snapshot, and never applies the event. +- `snapshot_reset` is the only event whose payload may restate earlier state; it + is emitted when history was compacted or a cursor expired, and it always + restates from a defined baseline so replay after it is self-contained. +- Duplicates are no-ops; out-of-order or chain-breaking events cannot regress + state (state advances only on strictly increasing sequences — the same rule + that makes unknown/out-of-order transitions non-regressing in the store's + append-only transition history, `crates/psyche-store/src/transitions.rs`). +- Unknown event kinds are a strict decode failure and become quarantinable + documents, never dispatchable records (`decode_document` + + `RejectedDocument::from_decode_error`, `crates/psyche-core/src/contracts/mod.rs`; + `Store::ingest` quarantine path in `crates/psyche-store/src/records.rs`). + +## 9. Adoption and idempotency + +1. Coven commits/adopts the automation attempt before invoking Psyche. The + invocation carries `coven.attempt_id`; Coven's attempt record must exist and be + fence-valid before dispatch. (Psyche cannot verify Coven's ledger, which is why + `occurrence.fence_generation` is carried and echoed: a stale generation makes + the invocation stale evidence.) +2. Psyche adopts the invocation key transactionally before creating tasks or + external work. The durable primitive is the existing one: insert inside an + immediate transaction with exact-replay idempotency on canonical bytes and a + hard conflict on divergent bytes (`crates/psyche-store/src/execution_bindings.rs` + `insert_in_transaction`; the same shape as `persist_then_terminate`'s + persist-request-before-dispatch ordering, `crates/psyche-coven/src/port.rs`). +3. Repeated delivery of the same `(invocation_id, invocation_digest)` returns the + existing correlation/result; it does not create a second graph + (`IngestOutcome::AlreadyPresent` semantics, `crates/psyche-store/src/records.rs`). +4. Reuse of an invocation key with different bytes is `invocation_conflict` + (§7), mirroring `PortError::IntentConflict` ("a stable identity was reused for + different intent"). +5. A timeout after possible adoption is reconciled by lookup on `invocation_id`, + never by a new key: the `lookup` + `reconcile` pair of `CovenPort` is the + precedent (`CovenPort::lookup`, `ReconciliationRequest` with + `ReconciliationDisposition::Returned | Fenced | Unresolved`). +6. Coven does not mark the attempt running/succeeded on transport acceptance. +7. Psyche does not mark the automation run terminal; it emits its own authoritative + result evidence (§6), and Coven decides the run's fate. + +Consequence for restart: if Coven restarts after dispatch, it rehydrates by +looking up `invocation_id` and reconciling (§11), never by minting a new +invocation; the mirror state on the Coven side uses the existing three-valued +adoption disposition rather than a second adoption path. + +## 10. Identity and authority + +- Psyche consumes the exact familiar binding selected by Coven + (`familiar_binding.binding_ref` + `binding_digest`); it never selects or + revises familiar identity. This is the same consumption-only posture the + existing contracts enforce: `familiar_snapshot_id` fields always reference a + Coven-resolved `IdentitySnapshot` (`crates/psyche-core/src/contracts/graph.rs`, + `crates/psyche-core/src/contracts/execution.rs`), and identity resolution + details (`familiar_id`, `principal_id`, declaration/soul/role digests) live in + the snapshot record, not in execution records + (`crates/psyche-core/src/contracts/identity.rs`). +- Every lane/worker session inherits or narrows the authorized projection; no + lane may broaden capabilities. The canonical vocabulary already has the denial: + `ErrorCode::DelegationWidened` (`crates/psyche-core/src/contracts/error.rs`), + and `Delegation.scope_digest`/`evidence_scope_digest` + (`crates/psyche-core/src/contracts/foundation.rs`) show the digest-pinned + projection pattern. The result's `capability_narrowing` entries record every + lane decision; an absent entry means inherited-unchanged; there is no + representation for broadening because broadening is a refusal + (`authority_missing`/`capability_unavailable`), never a record. +- Threads decisions and approvals remain operation-specific and are rechecked + where the protocol requires a later protected step: `risk_class: "protected"` + steps recheck at dispatch time (`bindings_exercised.authority.approval_events[].disposition` + records `rechecked`/`granted`/`denied`/`expired`). A later protected step that + the initial grant does not cover requests approval (`waiting_approval`) or + degrades to proposal; it never reinterprets the initial grant. This is the + adapter-level statement of the existing `delegation_widened` refusal. +- All nine scope classes are explicit in the invocation (`capabilities.scopes`) + and are narrowed per lane; the result's `capability_narrowing` entries prove + the narrowing with a reason code. +- Revocation/expiry policy by state, with event trail: `received` → refuse + (`authority_expired`/`binding_mismatch`); queued or `waiting_approval` → + `lane_state_changed` + `approval_denied`, lanes degrade or cancel; + assigned/running → cancellation propagation per §11. Every path emits events + with `emitted_at` ordering; nothing silently continues. + +## 11. Cancellation, timeout, and recovery + +Bidirectional, replay-safe control contract: + +- Coven requests cancellation for the exact `invocation_id` (never "any run on + this occurrence"). Psyche acknowledges and propagates under its own lane/lease + protocol. +- Cancellation remains pending until Psyche reports an authoritative reconciled + state (`acknowledged_terminated`, `acknowledged_already_terminal`, or + `termination_unknown` with unresolved evidence) — the same + acknowledge-or-unresolved discipline as `TerminationDisposition` + (`crates/psyche-coven/src/port.rs`), with the O5 rule intact: raw automation + statuses never manufacture acknowledgement + (`crates/psyche-test-support/src/suites/coven.rs` `RAW_LEDGER_STATES`; + `docs/TESTING.md` §O5). +- A completed-vs-cancelled race resolves by recorded event order and fence + semantics, not client arrival order: the terminal `psyche.automation_result.v1` + is emitted exactly once, and its `cancellation_history` records what happened; + a cancel request arriving after terminal state gets + `acknowledged_already_terminal` semantics, mirroring + `CancellationAcknowledgementKind::AlreadyAuthoritativelyTerminal` + (`crates/psyche-core/src/contracts/execution.rs`). +- Psyche timeout or lost worker evidence → `failed` or `recovery_required`, never + inferred success; `deadline.timeout_at` expiry and lost worker lease evidence + are `recovery_required` with `recovery_history` entries (the `Recovery` + contract's `ambiguity`/`fence_token`/`reconciliation_count` shape, + `crates/psyche-core/src/contracts/foundation.rs`). +- Coven restart: rehydrate from the persisted consumer cursor + `invocation_id`; + `reconcile` returns the durable disposition (`InvocationReconciliationDisposition` + — `Returned`/`Fenced`/`Unresolved`, the `ReconciliationDisposition` shape of + `crates/psyche-coven/src/port.rs`) instead of creating a new invocation. +- Psyche restart: adopted invocation and task/lane state are durable store rows + (SQLite, `crates/psyche-store/src/connection.rs`, migrations v1 in + `crates/psyche-store/src/migrations.rs`); on restart the runtime reopens the + store and continues (`crates/psyche-runtime/src/lib.rs` opens the store during + startup; `LifecycleState` running/draining/stopped with checkpoint-before-stop, + `checkpoint_failure_stops_and_releases_every_shutdown_waiter`). Event sequences + continue; `producer_instance` changes are visible but never reset state. +- Ambiguous external side effects are surfaced as structured + `recovery_required` evidence (`external_effects[].state: "ambiguous"` plus a + `recovery_required` event); automation retry is a Coven decision and is never + automatic. + +## 12. Changefeed and backpressure + +- Events have stable IDs/sequences (`aev_` + ULID; `(invocation_id, sequence)`) + and duplicate-safe replay via the payload chain (§8.4). +- Coven persists its consumer cursor (a `sequence` high-water mark plus + `correlation.event_cursor` reference on resume) and never relies on one live + connection — the `EventCursor`/`EventPage` contract with + `validate_for`-style strictly-increasing, cursor-consistent pages is the + precedent (`crates/psyche-coven/src/port.rs`). +- Unknown/out-of-order events cannot regress state (§8.4). +- Bounded buffers: event pages ≤1024 entries (`MAX_ARTIFACTS` bound precedent), + payloads ≤64 properties, documents ≤1 MiB; a slow consumer must apply + backpressure by pausing consumption, not by growing memory — the pull model + (`expectations.delivery_mode: "event_stream" | "poll" | "receipt_push"`) keeps + the consumer authoritative over pacing. +- Payloads are minimized/redacted per principal authorization before emission + (`payload` is advisory and bounded; the `privacy` class travels in both + invocation and result). +- Snapshot/reset is explicit: `snapshot_reset` events carry the new baseline; + a consumer whose cursor is behind a reset must resynchronize before trusting + further deltas. + +## 13. Adapter interface + +Transport-neutral seam, expressed in repo idiom (async-trait, typed errors, +digest-attested requests — `CovenPort` in `crates/psyche-coven/src/port.rs` is the +style precedent; the issue's conceptual seam is preserved operation-for-operation): + +```rust +#[async_trait::async_trait] +pub trait PsycheAutomationPort: Send + Sync { + /// Negotiates the exact behavior contract, mirroring NegotiateRequest/ + /// CapabilityProfile semantics (crates/psyche-coven/src/port.rs). + async fn negotiate(&self, request: NegotiateRequest) -> Result; + /// Digest-attested adoption; idempotent on (invocation_id, invocation_digest). + async fn adopt(&self, request: InvocationAdoptionRequest) + -> Result; + /// Current durable snapshot for one invocation. + async fn status(&self, invocation_id: &str) -> Result; + /// One ordered, duplicate-safe event page after the given cursor. + async fn events(&self, cursor: InvocationEventCursor) -> Result; + /// Requests cancellation of the exact invocation; pending until an + /// authoritative reconciled state is reported. + async fn request_cancel(&self, request: InvocationCancelRequest) + -> Result; + /// Reconciles a possibly-adopted-but-unknown invocation by lookup. + async fn reconcile(&self, request: InvocationReconciliationRequest) + -> Result; + /// The terminal versioned result, once terminal. + async fn result(&self, invocation_id: &str) -> Result; +} +``` + +Design rules carried from the existing boundary: `adopt` takes a digest-attested +envelope and the receiver recomputes (`AdoptionRequest::validate_digest`); +dispositions are three-valued (`Adopted | ProvenNotAdopted | Unknown`) so +ambiguity is representable; cancellation requires a construction-closed durable +request (`TerminationRequest` pattern: the request is only constructible from a +persisted, validated revision); responses echo complete correlations +(`validate_for` pattern). `status`/`result` take the invocation id, not task +text; nothing at this seam can create or edit a schedule. + +Placement (decision D2): the port belongs beside the existing typed Coven +boundary in `psyche-coven` (module `psyche_coven::automation`), because +`psyche-coven` is the crate that owns "the typed Coven boundary" and already +delegates canonical contract decisions to core (`docs/ARCHITECTURE.md`); the +dependency arrow `psyche-core <- psyche-coven` already supports the contract +types. A separate `psyche-automation` crate is the fallback if the crate grows +beyond two boundary families. `psyche-surfaces` is rejected: that crate owns +bounded chat-surface ports, not cross-program orchestration. + +## 14. Durable records (recommendation, decision D3) + +- New record identities, following the `RecordId` prefix discipline of + `crates/psyche-core/src/id.rs`: automation invocation `aiv_`, automation event + `aev_`, automation result `ars_` — none collide with the fifteen existing + `RecordKind` prefixes. `ExecutionBinding` deliberately stays on `att_` and is + not reused (issue requirement; the one-attempt-record rule is documented in + `crates/psyche-core/src/contracts/mod.rs`). +- New store tables alongside the six foundation tables + (`crates/psyche-store/src/migrations.rs`): `automation_invocations` (one row per + adopted invocation: id, digest, canonical bytes, correlation) and + `automation_events` (append-only, `(invocation_id, sequence)` unique). State + movement rides the existing append-only `transitions` ledger + (`crates/psyche-store/src/transitions.rs`), and terminal results persist as + canonical records, so retention behaves like the existing exclusions + (`crates/psyche-store/src/retention.rs`: unresolved quarantine, binding + revisions, transition history, and audit events are retention-excluded; + automation evidence should join those exclusions while it is referenced). +- Schema registration: `SchemaKind` grows `AutomationInvocation`, `AutomationEvent`, + `AutomationResult`, `AutomationError` with `SUPPORTED_MAJOR` still 1, following + the registry's own rule that a new kind is a deliberate, reviewed change to + `crates/psyche-core/src/contracts/mod.rs` (decision D1 covers the envelope + shape). + +## 15. Golden scenarios + +Numbered exactly as the issue's twenty golden scenarios. Owning locations follow +the repo's testing conventions (`docs/TESTING.md`): scripted deterministic fakes +at adapter-neutral boundaries in `psyche-test-support` (the `ScriptedG2Fixture` +pattern of `crates/psyche-test-support/src/suites/coven.rs`), durable-state tests +in `psyche-store`, digest goldens in `psyche-coven`, proptests with fixed +`PROPTEST_CASES`/`PROPTEST_RNG_SEED` (`docs/TESTING.md`), and crash tests under +`psyche-store`'s `test-fault-injection` feature +(`crates/psyche-store/tests/crash.rs`). + +| # | Scenario (issue numbering) | Owning suite (proposed) | Key observable | +|---|---|---|---| +| A-1 | One occurrence adopts one execution | automation conformance `a_s1_single_adoption` | one `grf_` execution per `aiv_`; second `adopt` returns existing disposition | +| A-2 | Duplicate delivery returns the same execution | `a_s2_duplicate_delivery` | identical correlation, no second graph, `AlreadyPresent`-style replay | +| A-3 | Invocation-key conflict with changed bytes | `a_s3_key_conflict` | `invocation_conflict` with `conflict_digest`, durable refusal | +| A-4 | Multi-lane success with verified artifacts | `a_s4_multi_lane_success` | result `completed`, per-lane `verified` entries (golden result) | +| A-5 | Lane failure → overall failed | `a_s5_lane_failure` | `status: failed`, failed lane listed, no inferred success | +| A-6 | Additional protected step requires approval | `a_s6_protected_step` | `approval_required` event, `waiting_approval` graph state | +| A-7 | Degrade-to-proposal without protected mutation | `a_s7_degrade_to_proposal` | lane `skipped`, no protected mutation, disclosure event | +| A-8 | Principal/familiar/project mismatch refusal | `a_s8_binding_mismatch` | `binding_mismatch`/`principal_mismatch`, permanent refusal | +| A-9 | Stale/revoked familiar binding refusal | `a_s9_stale_binding` | `authority_expired`, refusal evidence durable | +| A-10 | Runtime capability narrowing per lane | `a_s10_capability_narrowing` | `capability_narrowed` events + result entries, no broadening | +| A-11 | Coven restart after adoption | `a_s11_coven_restart` | lookup returns existing adoption; no new invocation | +| A-12 | Psyche restart with active lanes | `a_s12_psyche_restart` | durable adoption/events survive; sequence continues | +| A-13 | Subscriber disconnect/replay duplicates | `a_s13_cursor_replay` | duplicate-safe replay; chain detects gap/regression | +| A-14 | Cancellation before assignment | `a_s14_cancel_before_assignment` | `cancellation_acknowledged`, lanes never dispatched | +| A-15 | Cancellation during worker execution | `a_s15_cancel_running` | propagation to workers under Psyche protocol | +| A-16 | Completion/cancellation race | `a_s16_completion_cancel_race` | resolved by recorded event order, not arrival | +| A-17 | Timeout / lost worker / recovery-required | `a_s17_timeout_lost_worker` | `failed`/`recovery_required`, never inferred success | +| A-18 | Ambiguous external effect, no automatic automation retry | `a_s18_ambiguous_effect` | `ambiguous_effect_disclosed` + result `external_effects` | +| A-19 | Artifact verification or delivery failure | `a_s19_artifact_failure` | verification `failed`/`unavailable`, result reflects it | +| A-20 | Direct Coven run and Psyche run produce compatible receipts | `a_s20_receipt_parity` | both paths compose valid Automation Receipts | + +Negative probes validated locally against the published schemas (canonical-byte, +digest-recompute, unknown-major, unknown-field, bad-ULID, relative-path, +non-terminal-status, malformed-digest mutations) pass; the full suite above is +implementation work (§15). + +## 16. Verification and canaries + +Verification categories map onto existing infrastructure: + +- Schema/canonicalization/golden vectors → `psyche-core` contract/decode test + style (`crates/psyche-core/tests/decode.rs`, strict + `decode_document`), golden files in `crates/psyche-coven/tests/fixtures/` + shape (RFC 3339, canonical bytes, no trailing newline, pinned SHA-256 per + `docs/TESTING.md`). The four published JSON Schemas and goldens in this + document are the wire-shape source for those types. +- Adoption/idempotency/conflict: `psyche-store` immediate-transaction replay + semantics (`crates/psyche-store/src/execution_bindings.rs`); conflict tests + mirror `execution_binding_revision_rejects_same_revision_changed_bytes`. +- State/correlation ownership: state-machine model tests with fixed seed + (`crates/psyche-test-support/tests/state_machine.rs`, CI fixes 2048 cases and + the all-zero seed per `.github/workflows/ci.yml`). +- Duplicate/out-of-order/reconnect: cursor-page validation rules extended from + `EventPage::validate_for`. +- Identity/authority/approval: `psyche-test-support` scripted suites (the + C-S1..C-S12 matrix pattern in `crates/psyche-test-support/tests/conformance.rs`). +- Cancellation/timeout/restart/recovery: `psyche-store/tests/crash.rs` + fault-injection points + `psyche-runtime` lifecycle tests + (`crates/psyche-runtime/tests/lifecycle.rs`). +- Privacy/redaction: payload minimization rules of §12 tested at the event + boundary. +- CI integration: new tests join the existing gates — `cargo fmt`, `cargo clippy + --workspace --all-targets -- -D warnings`, `cargo test --workspace --locked`, + the G2 state-machine/conformance/migrations/crash steps, the + `scripts/g2-test-manifest.json` allowlist checker (which rejects zero-test + targets, missing names, substring filters, and unused entries — new suites must + be registered there), `cargo deny`, gitleaks, and the npm wrapper legs + (`.github/workflows/ci.yml`). + +Cross-repository canaries: evidence pins immutable artifact URLs and SHA-256 +values exactly as `docs/G2-EVIDENCE.md` does (source commit, immutable URL, +SHA-256 table). At certification (OpenCoven/coven#858), the automation-adapter +evidence table must pin the exact Coven plan/specification revisions, +Familiar-Contract profile, Threads authority profile, runtime, SDK, and Cave +artifacts, in the same URL+digest form, with the invocation/result goldens above +as the Psyche-side vectors. + +## 17. Maintainer decisions (recommendation + alternatives) + +- **D1 — Envelope vs. registered record.** The invocation/result contracts are + published as digest-attested envelopes (`{content, digest}`), mirroring + `AdoptionRequest`/`ResultBundle` in `crates/psyche-coven/src/port.rs`. + *Recommended:* keep the envelope now; at implementation, promote to registered + `SchemaKind` variants (`AutomationInvocation`, `AutomationEvent`, + `AutomationResult`, `AutomationError`) with new `RecordKind`s, or keep them as + envelope types. *Alternatives:* (a) register immediately as + `CanonicalDocument` kinds — forces a registry/DB migration in the same PR as + the contract; (b) keep them transport-only forever — rejected, because + adoption evidence must be durably persistable. +- **D2 — Adapter placement.** `psyche_coven::automation` (recommended; crate + already owns typed Coven boundaries, `docs/ARCHITECTURE.md`) vs. new + `psyche-automation` crate vs. `psyche-surfaces` (rejected — wrong boundary + family). +- **D3 — Record identities.** New `aiv_`/`aev_`/`ars_` prefixes and dedicated + tables, versus reusing `att_`/`evd_`. Reuse is rejected: the issue forbids + collapsing layers, and `ExecutionBinding` already occupies `att_` with + append-only revision semantics specific to session binding. +- **D4 — Error vocabulary.** Publish the frozen automation refusal vocabulary in + `psyche.automation_error.v1` and mirror each value into `ErrorCode::ALL` at + implementation (single vocabulary, two projections), rather than maintaining a + second daemon-side vocabulary. +- **D5 — Result authentication scheme.** v1 is `sha256-digest-v1` only. A + signature scheme (signing keys do not exist in this repo today — commits use + DCO trailers, no GPG/SSH artifacts are present) is a deliberate future + major-version decision; the authentication field is versioned so it can be + extended without breaking the envelope. + +## 18. Non-goals (restated from the issue, enforced by this contract) + +- No scheduling in Psyche: nothing in the adapter accepts a schedule, cadence, + or trigger definition; `automation.definition_*` fields are pinned evidence, + not executable policy. No Psyche API creates or edits canonical automation + schedules. +- Simple familiar prompts do not route through this adapter; direct Coven runtime + execution remains valid (scenario A-20 keeps both receipt-compatible). +- Automation and Psyche lifecycle objects stay distinct (§4). +- Lanes inherit-or-narrow; there is no broad ambient authority (§10). +- Ambiguous external side effects are never retried automatically (§11). + +## 19. Acceptance criteria status + +- [x] Versioned automation→Psyche invocation, event, result, and error contracts + are published (this document + the four JSON Schemas and goldens). +- [x] Coven schedule/occurrence/run authority and Psyche task/lane authority are + unambiguous (§3, §4). +- [x] Invocation adoption is specified idempotent and conflict-safe (§9); the + durable mechanism reuses the proven revision-chain pattern + (`crates/psyche-store/src/execution_bindings.rs`). +- [x] Familiar identity and Threads authority are consumed, pinned, and never + broadened by lanes (§10). +- [x] Cancellation, timeout, restart, replay, and ambiguous recovery converge + without duplicate orchestration or false success (§8, §11). +- [x] Psyche result evidence composes into the Coven Automation Receipt (§6). +- [ ] Golden scenarios and immutable cross-repository canaries pass — deferred to + the implementation task that registers the schemas and lands suites A-1 + through A-20 (§15 names each one). +- [x] No Psyche API creates or edits canonical automation schedules (§3.1, §18). + +## 20. Bead packet (for the tracker owner) + +One P1 Psyche Bead mapped to OpenCoven/psyche#18. Dependencies: OpenCoven/coven#855 +(core protocol) and OpenCoven/coven#857 (identity/authority integration), plus the +pinned Familiar Contract (OpenCoven/familiar-contract#17) and Threads authority +(OpenCoven/coven-threads#29) profiles; certification depends on OpenCoven/coven#858. +Required evidence: the normative artifacts in `docs/schemas/automation-adapter/` +(this document, schemas, goldens); adoption/replay/restart scenario results +(A-1..A-3, A-11, A-12); authority narrowing proof (A-10); cancellation/recovery +(A-14..A-18); receipt composition (A-4, A-20); exact canary revisions pinned in +docs/G2-EVIDENCE.md's immutable-URL + SHA-256 table format. Bead state was +deliberately not modified from this PR (task constraints); the packet above is +ready to be registered by the maintainer. diff --git a/docs/schemas/automation-adapter/examples/automation-event-v1-golden.json b/docs/schemas/automation-adapter/examples/automation-event-v1-golden.json new file mode 100644 index 0000000..e312210 --- /dev/null +++ b/docs/schemas/automation-adapter/examples/automation-event-v1-golden.json @@ -0,0 +1 @@ +{"emitted_at":"2026-08-30T04:00:05Z","event_id":"aev_01J0000000000000000000AEV1","invocation_id":"aiv_01J0000000000000000000AV01","kind":"adoption_confirmed","payload":{"execution_ref":"grf_01J00000000000000000000000"},"payload_digest":"sha256:40bc16fee112584d76602d0535035391fe93ef85c531aed75c3317ef3e7988b7","previous_payload_digest":"sha256:bb2991ec76f6c813988a05f2833545833bb96eb9ad5f95158d2b8eec2ec70401","producer_instance":"psyche-daemon-2c9d","schema_version":"psyche.automation_event.v1","sequence":1} \ No newline at end of file diff --git a/docs/schemas/automation-adapter/examples/automation-invocation-v1-golden.json b/docs/schemas/automation-adapter/examples/automation-invocation-v1-golden.json new file mode 100644 index 0000000..b262189 --- /dev/null +++ b/docs/schemas/automation-adapter/examples/automation-invocation-v1-golden.json @@ -0,0 +1 @@ +{"invocation":{"action_class":"scheduled_dependency_upgrade","authority":{"approval_evidence":[{"approval_digest":"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","approval_ref":"threads-approval/val/0007/publication"}],"authority_profile_revision":"threads-profile-2026-08-01","threads_decision_digest":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","threads_decision_ref":"threads-decision/val/0007"},"automation":{"automation_id":"automation/payments-nightly","definition_digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","definition_revision":"rev-41"},"brief":{"digest":"sha256:62b4f7a4b08aa68cac8b53fa7f380f3b9ea54b5eb5dc2d9a4a4c6c8fb56ae955","media_type":"application/json","size_bytes":466},"capabilities":{"granted":["workspace_write","network_fetch","publication_release"],"requested":["workspace_write","network_fetch","publication_release"],"scopes":{"credentials":["registry-token-ref"],"familiar":["payments-assistant"],"network":["crates.io","github.com"],"paths":["src/","Cargo.toml"],"principal":["val"],"project":["payments"],"publication":["release-candidate"],"repository":["github.com/opencoven/payments"],"runtime":["containerized"]}},"context":{"project_id":"project:payments","repository":{"commit":"3f9c2b1a7d5e4c8b0a9f2e6d1c3b5a7f9e0d2c4b","remote":"github.com/opencoven/payments"},"workspace_root":"/workspace/payments"},"correlation":{"causation_id":"sched-eval-2026-08-30T04:00:00Z","correlation_id":"corr-2026-08-30-000123"},"coven":{"attempt_id":"attempt-2026-08-30-000123-1","run_id":"run-2026-08-30-000123"},"deadline":{"cancellation_channel":"automation-cancel/payments-nightly","created_at":"2026-08-30T04:00:00Z","timeout_at":"2026-08-30T04:30:00Z"},"expectations":{"delivery_mode":"event_stream","result_schema":"psyche.automation_result.v1"},"familiar_binding":{"binding_digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","binding_ref":"familiar-binding/val/rev-41"},"invocation_id":"aiv_01J0000000000000000000AV01","occurrence":{"fence_generation":7,"occurrence_id":"occ/payments-nightly/2026-08-30T04:00:00Z","occurrence_key":"occurrence/payments-nightly/2026-08-30T04:00:00Z"},"principal":{"principal_context_ref":"principal:val"},"privacy":{"privacy_class":"operational","retention_class":"standard"},"producer":{"contract_versions":{"coven":"automation.v1","familiar-contract":"familiar-embodiment-binding.v1","threads":"authority-profile-2026-08-01"},"producer_id":"coven","producer_instance":"coven-daemon-7f3a"},"risk_class":"protected","runtime":{"constraints":{"arch":["x86_64"],"os":["linux"]},"required_capabilities":["containerized","node24"]},"schema_version":"psyche.automation_invocation.v1"},"invocation_digest":"sha256:bb2991ec76f6c813988a05f2833545833bb96eb9ad5f95158d2b8eec2ec70401"} \ No newline at end of file diff --git a/docs/schemas/automation-adapter/examples/automation-result-v1-golden.json b/docs/schemas/automation-adapter/examples/automation-result-v1-golden.json new file mode 100644 index 0000000..e73cf84 --- /dev/null +++ b/docs/schemas/automation-adapter/examples/automation-result-v1-golden.json @@ -0,0 +1 @@ +{"result":{"artifacts":[{"artifact_id":"artifact-upgrade-report","digest":"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","expires_at":"2026-09-06T04:30:00Z","lane_id":"lane-upgrade","media_type":"text/plain","provenance":"lane-upgrade:test-output","size_bytes":34},{"artifact_id":"artifact-changelog","digest":"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","expires_at":"2026-09-06T04:30:00Z","lane_id":"lane-release-gate","media_type":"text/markdown","provenance":"lane-release-gate:changelog","size_bytes":512}],"authentication":{"producer_id":"psyche","producer_instance":"psyche-daemon-2c9d","scheme":"sha256-digest-v1"},"bindings_exercised":{"authority":{"approval_events":[{"approval_digest":"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","approval_ref":"threads-approval/val/0007/publication","disposition":"rechecked"}],"threads_decision_digest":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"},"familiar_binding_digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","runtime_capabilities":["containerized","node24"]},"cancellation_history":[],"capability_narrowing":[{"lane_id":"lane-upgrade","narrowed_scopes":{"publication":[]},"reason_code":"lane_class_code_change"}],"correlation":{"attempt_id":"attempt-2026-08-30-000123-1","occurrence_id":"occ/payments-nightly/2026-08-30T04:00:00Z","run_id":"run-2026-08-30-000123"},"external_effects":[{"disclosure_digest":"sha256:3333333333333333333333333333333333333333333333333333333333333333","effect_class":"publication","lane_id":"lane-release-gate","occurred_at":"2026-08-30T04:21:11Z","state":"confirmed"}],"invocation_digest":"sha256:bb2991ec76f6c813988a05f2833545833bb96eb9ad5f95158d2b8eec2ec70401","invocation_id":"aiv_01J0000000000000000000AV01","orchestration":{"execution_ref":"grf_01J00000000000000000000000","lanes":[{"lane_id":"lane-upgrade","lease_ref":"lease-upgrade-1","node_ref":"nod_01J00000000000000000000000","terminal_state":"completed","worker_session_ref":"session-upgrade-1"},{"lane_id":"lane-release-gate","lease_ref":"lease-gate-1","node_ref":"nod_01J00000000000000000000001","terminal_state":"completed","worker_session_ref":"session-gate-1"}]},"privacy":{"privacy_class":"operational","redacted":false,"retention_class":"standard"},"reason_code":"all_lanes_verified","recovery_history":[],"result_id":"ars_01J0000000000000000000ARS1","schema_version":"psyche.automation_result.v1","status":"completed","timing":{"final_event_cursor":12,"finished_at":"2026-08-30T04:21:14Z","started_at":"2026-08-30T04:00:06Z"},"verification":[{"evidence_digest":"sha256:1111111111111111111111111111111111111111111111111111111111111111","lane_id":"lane-upgrade","method":"unit_tests","outcome":"verified"},{"evidence_digest":"sha256:2222222222222222222222222222222222222222222222222222222222222222","lane_id":"lane-release-gate","method":"release_gate","outcome":"verified"}]},"result_digest":"sha256:8abdb845d13916a655a6f4c5dd894ebcb324abfaf9e1cd3ab176bc43d243d472"} \ No newline at end of file diff --git a/docs/schemas/automation-adapter/examples/orchestration-brief-v1.json b/docs/schemas/automation-adapter/examples/orchestration-brief-v1.json new file mode 100644 index 0000000..73e50e3 --- /dev/null +++ b/docs/schemas/automation-adapter/examples/orchestration-brief-v1.json @@ -0,0 +1 @@ +{"lanes":[{"action_class":"code_change","depends_on":[],"lane_id":"lane-upgrade","protected":false,"summary":"Apply the dependency upgrade and run the unit tests"},{"action_class":"publication","depends_on":["lane-upgrade"],"lane_id":"lane-release-gate","protected":true,"summary":"Publish the release candidate and update the changelog"}],"objective":"Upgrade the payments dependency and verify the release gate","schema_version":"opencoven.orchestration_brief.v1"} \ No newline at end of file diff --git a/docs/schemas/automation-adapter/psyche.automation_error.v1.schema.json b/docs/schemas/automation-adapter/psyche.automation_error.v1.schema.json new file mode 100644 index 0000000..9195447 --- /dev/null +++ b/docs/schemas/automation-adapter/psyche.automation_error.v1.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/OpenCoven/psyche/main/docs/schemas/automation-adapter/psyche.automation_error.v1.schema.json", + "title": "psyche.automation_error.v1", + "description": "Structured, persistable refusal or failure document for the automation adapter boundary. It is the automation-boundary projection of the payload-free PortError vocabulary in crates/psyche-coven/src/error.rs: the port enum stays the in-process contract, while this document is what crosses a transport and may be durably retained as refusal evidence. Unlike psyche.error.v1 (crates/psyche-core/src/contracts/error.rs), which exhaustively decodes the daemon-facing ErrorCode::ALL vocabulary and is deliberately not persistable, this contract is persistable by design: a refused invocation must leave durable evidence of what was refused and why, without persisting any rejected payload bytes.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "code", "refusal_class", "occurred_at", "detail"], + "properties": { + "schema_version": { + "const": "psyche.automation_error.v1", + "description": "Exact registry spelling; denied, never coerced." + }, + "code": { + "enum": [ + "invocation_invalid", + "invocation_unknown", + "invocation_conflict", + "invocation_stale", + "version_unsupported", + "binding_mismatch", + "principal_mismatch", + "authority_missing", + "authority_expired", + "capability_unavailable", + "runtime_unsatisfiable", + "deadline_exceeded", + "orchestration_store_unavailable", + "cancellation_unknown", + "reconciliation_unresolved" + ], + "description": "Frozen automation refusal/failure vocabulary. At implementation time each value is mirrored into the canonical ErrorCode::ALL registry so the daemon surface can represent it without a second vocabulary (design decision D4 in docs/AUTOMATION-ADAPTER.md)." + }, + "refusal_class": { + "enum": ["permanent", "retryable_after_reconciliation", "transport"], + "description": "`permanent` refusals are terminal for the invocation identity; `retryable_after_reconciliation` requires lookup/reconcile by the existing invocation_id, never a new key; `transport` failures carry no statement about durable state." + }, + "occurred_at": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$" + }, + "detail": { + "type": "object", + "additionalProperties": false, + "required": ["summary"], + "description": "Bounded, payload-free detail. Rejected input bytes are never embedded; at most a stable field name or category is retained, matching the payload-light rejection style of RejectionReason in crates/psyche-core/src/contracts/mod.rs.", + "properties": { + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "field": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "invocation_id": { + "type": "string", + "pattern": "^aiv_[0-9A-HJKMNP-TV-Z]{26}$", + "description": "Echoed when the invocation identity was parseable, so lookup/reconcile stays possible." + }, + "conflict_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "For invocation_conflict: the digest already adopted under this invocation_id, so the conflicting sender can self-identify the divergence." + } + } + } + } +} diff --git a/docs/schemas/automation-adapter/psyche.automation_event.v1.schema.json b/docs/schemas/automation-adapter/psyche.automation_event.v1.schema.json new file mode 100644 index 0000000..2f42eea --- /dev/null +++ b/docs/schemas/automation-adapter/psyche.automation_event.v1.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/OpenCoven/psyche/main/docs/schemas/automation-adapter/psyche.automation_event.v1.schema.json", + "title": "psyche.automation_event.v1", + "description": "One Psyche orchestration event for one adopted automation invocation. Events have the stable identity (invocation_id, sequence); sequences are strictly increasing per invocation with no gaps, matching the ordered-cursor rules of EventPage::validate_for in crates/psyche-coven/src/port.rs. Every event chains to its predecessor: the first event's previous_payload_digest is the adopted invocation_digest, and every later event's previous_payload_digest is the payload_digest of the event at sequence-1. Duplicate, out-of-order, or chain-breaking delivery can therefore never be mistaken for new state, and Coven persists its own cursor so replay never depends on a live connection.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "event_id", + "invocation_id", + "sequence", + "emitted_at", + "producer_instance", + "kind", + "payload_digest", + "previous_payload_digest", + "payload" + ], + "properties": { + "schema_version": { + "const": "psyche.automation_event.v1", + "description": "Exact registry spelling; denied, never coerced." + }, + "event_id": { + "type": "string", + "pattern": "^aev_[0-9A-HJKMNP-TV-Z]{26}$", + "description": "Durable event identity, unique across all invocations." + }, + "invocation_id": { + "type": "string", + "pattern": "^aiv_[0-9A-HJKMNP-TV-Z]{26}$", + "description": "The adopted invocation this event belongs to." + }, + "sequence": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Strictly increasing per invocation, safe-integer bounded." + }, + "emitted_at": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$", + "description": "RFC 3339 UTC instant at which Psyche durably recorded the event." + }, + "producer_instance": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Stable identity of the Psyche instance that recorded the event. A new instance after restart keeps the adopted history and continues the sequence; it never resets it." + }, + "kind": { + "enum": [ + "adoption_confirmed", + "orchestration_started", + "lane_planned", + "lane_state_changed", + "worker_session_bound", + "worker_session_unbound", + "approval_required", + "approval_granted", + "approval_denied", + "artifact_produced", + "artifact_verification_completed", + "capability_narrowed", + "execution_completed", + "execution_failed", + "cancellation_requested", + "cancellation_acknowledged", + "recovery_required", + "ambiguous_effect_disclosed", + "snapshot_reset" + ], + "description": "Frozen event vocabulary. Unknown kinds are a decode failure and become quarantinable documents (crates/psyche-core/src/contracts/mod.rs inspect_typed_enums)." + }, + "payload_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 over the canonical JSON bytes of the payload object." + }, + "previous_payload_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "Chain link: payload_digest of the event at sequence-1, or the adopted invocation_digest for sequence 1." + }, + "payload": { + "type": "object", + "maxProperties": 64, + "description": "Bounded, minimized event detail, already redacted to the principal authorization Psyche holds. Payloads are advisory observations; only the terminal psyche.automation_result.v1 is authoritative evidence.", + "additionalProperties": true + } + } +} diff --git a/docs/schemas/automation-adapter/psyche.automation_invocation.v1.schema.json b/docs/schemas/automation-adapter/psyche.automation_invocation.v1.schema.json new file mode 100644 index 0000000..a271811 --- /dev/null +++ b/docs/schemas/automation-adapter/psyche.automation_invocation.v1.schema.json @@ -0,0 +1,384 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/OpenCoven/psyche/main/docs/schemas/automation-adapter/psyche.automation_invocation.v1.schema.json", + "title": "psyche.automation_invocation.v1", + "description": "Wire envelope for one adopted automation invocation. Coven (the automation authority) dispatches this to Psyche to request multi-step, multi-lane, approval-aware orchestration. The envelope mirrors the digest-attested AdoptionRequest precedent in crates/psyche-coven/src/port.rs: `invocation` carries the complete typed content, and `invocation_digest` is the SHA-256 over the canonical (RFC 8785) JSON bytes of the `invocation` object alone. Psyche must verify the digest before creating any consequential task, lane, or lease state.", + "type": "object", + "additionalProperties": false, + "required": ["invocation", "invocation_digest"], + "properties": { + "invocation": { "$ref": "#/$defs/invocation_content" }, + "invocation_digest": { "$ref": "#/$defs/sha256_digest" } + }, + "$defs": { + "sha256_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "Lowercase `sha256:` prefix plus exactly 64 lowercase hex characters, matching crates/psyche-core/src/digest.rs Sha256Digest." + }, + "ulid_suffix": { + "type": "string", + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", + "description": "Canonical uppercase Crockford Base32 ULID suffix (no I, L, O, U), matching the RecordId grammar in crates/psyche-core/src/id.rs." + }, + "stable_token": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "maxLength": 64, + "description": "Lowercase snake_case token with the same spelling rules as psyche-coven/src/port.rs stable_token()/reason_code()." + }, + "bounded_string_255": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Opaque bounded string, matching the 255-byte bound used across psyche-core contracts." + }, + "safe_integer": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "description": "Non-negative IEEE-754-interoperable integer, matching MAX_SAFE_INTEGER in crates/psyche-core/src/contracts/mod.rs." + }, + "utc_timestamp": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$", + "description": "RFC 3339 timestamp with an explicit UTC designator. Offsets other than Z are rejected, matching the `offset() == UtcOffset::UTC` checks in psyche-core and psyche-coven." + }, + "absolute_path": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "pattern": "^(/|/[^/]+((/[^/]+)?)*)$", + "description": "Canonical absolute path validated by the same rules as validate_absolute_path() in crates/psyche-coven/src/port.rs: no trailing slash, no empty, `.` or `..` segments, no `//`." + }, + "capability_token": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{1,62}[a-z0-9_]$", + "description": "Stable capability spelling; consumers must accept the exact frozen vocabulary, never a wildcard." + }, + "invocation_content": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "invocation_id", + "automation", + "occurrence", + "coven", + "principal", + "familiar_binding", + "authority", + "capabilities", + "action_class", + "risk_class", + "brief", + "context", + "runtime", + "deadline", + "expectations", + "correlation", + "producer", + "privacy" + ], + "properties": { + "schema_version": { + "const": "psyche.automation_invocation.v1", + "description": "Exact registry spelling. Unknown kinds or majors are denied, never coerced (crates/psyche-core/src/contracts/mod.rs SchemaVersion::parse)." + }, + "invocation_id": { + "type": "string", + "pattern": "^aiv_[0-9A-HJKMNP-TV-Z]{26}$", + "description": "Stable invocation/adoption identity. It is also the idempotency key: reuse with a different invocation_digest is a conflict, never a second graph." + }, + "automation": { + "type": "object", + "additionalProperties": false, + "required": ["automation_id", "definition_revision", "definition_digest"], + "properties": { + "automation_id": { "$ref": "#/$defs/bounded_string_255" }, + "definition_revision": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Exact automation definition revision selected by Coven." + }, + "definition_digest": { "$ref": "#/$defs/sha256_digest" } + } + }, + "occurrence": { + "type": "object", + "additionalProperties": false, + "required": ["occurrence_id", "occurrence_key", "fence_generation"], + "properties": { + "occurrence_id": { "$ref": "#/$defs/bounded_string_255" }, + "occurrence_key": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Canonical occurrence key computed by Coven; Psyche stores it verbatim and never derives eligibility from it." + }, + "fence_generation": { + "$ref": "#/$defs/safe_integer", + "description": "Occurrence fence generation at dispatch time. A later generation fences this invocation: results from a fenced generation are stale evidence, not authority." + } + } + }, + "coven": { + "type": "object", + "additionalProperties": false, + "required": ["run_id", "attempt_id"], + "properties": { + "run_id": { + "$ref": "#/$defs/bounded_string_255", + "description": "Coven's authoritative automation run identity. Opaque to Psyche." + }, + "attempt_id": { + "$ref": "#/$defs/bounded_string_255", + "description": "One fenced Coven dispatch attempt. Opaque to Psyche. Distinct from run_id and from every Psyche identifier." + } + } + }, + "principal": { + "type": "object", + "additionalProperties": false, + "required": ["principal_context_ref"], + "properties": { + "principal_context_ref": { + "$ref": "#/$defs/bounded_string_255", + "description": "Reference to the authenticated principal context bound by Coven. Psyche never embeds or re-derives principal identity." + } + } + }, + "familiar_binding": { + "type": "object", + "additionalProperties": false, + "required": ["binding_ref", "binding_digest"], + "properties": { + "binding_ref": { + "$ref": "#/$defs/bounded_string_255", + "description": "Exact Familiar Embodiment Binding reference selected by Coven. Psyche consumes it; it never selects or revises familiar identity." + }, + "binding_digest": { "$ref": "#/$defs/sha256_digest" } + } + }, + "authority": { + "type": "object", + "additionalProperties": false, + "required": [ + "threads_decision_ref", + "threads_decision_digest", + "approval_evidence", + "authority_profile_revision" + ], + "properties": { + "threads_decision_ref": { "$ref": "#/$defs/bounded_string_255" }, + "threads_decision_digest": { "$ref": "#/$defs/sha256_digest" }, + "approval_evidence": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["approval_ref", "approval_digest"], + "properties": { + "approval_ref": { "$ref": "#/$defs/bounded_string_255" }, + "approval_digest": { "$ref": "#/$defs/sha256_digest" } + } + } + }, + "authority_profile_revision": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "Pinned Threads authority profile revision (OpenCoven/coven-threads#29 shape)." + } + } + }, + "capabilities": { + "type": "object", + "additionalProperties": false, + "required": ["requested", "granted", "scopes"], + "properties": { + "requested": { + "type": "array", + "maxItems": 64, + "items": { "$ref": "#/$defs/capability_token" } + }, + "granted": { + "type": "array", + "maxItems": 64, + "items": { "$ref": "#/$defs/capability_token" }, + "description": "Every granted capability must appear in requested. Lanes may inherit or narrow this set; they may never broaden it." + }, + "scopes": { + "type": "object", + "additionalProperties": false, + "required": ["principal", "familiar", "project", "repository", "paths", "network", "credentials", "publication", "runtime"], + "description": "Explicit projection per scope class. Each class holds the bounded tokens Coven authorized; an empty array authorizes nothing for that class.", + "properties": { + "principal": { "$ref": "#/$defs/token_list" }, + "familiar": { "$ref": "#/$defs/token_list" }, + "project": { "$ref": "#/$defs/token_list" }, + "repository": { "$ref": "#/$defs/token_list" }, + "paths": { "$ref": "#/$defs/token_list" }, + "network": { "$ref": "#/$defs/token_list" }, + "credentials": { "$ref": "#/$defs/token_list" }, + "publication": { "$ref": "#/$defs/token_list" }, + "runtime": { "$ref": "#/$defs/token_list" } + } + } + } + }, + "action_class": { + "$ref": "#/$defs/stable_token", + "description": "Coven's action class for this occurrence, carried verbatim." + }, + "risk_class": { + "enum": ["routine", "elevated", "protected"], + "description": "Frozen risk vocabulary. `protected` steps require a later Threads approval where the protocol demands one; a grant is never reinterpreted." + }, + "brief": { + "type": "object", + "additionalProperties": false, + "required": ["media_type", "size_bytes", "digest"], + "description": "Orchestration brief/action payload referenced by integrity digest, never inlined. Transport carries the bytes out of band; Psyche verifies before planning.", + "properties": { + "media_type": { + "type": "string", + "pattern": "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+$", + "maxLength": 255, + "description": "Strict lowercase media type, matching validate_media_type() in crates/psyche-coven/src/port.rs." + }, + "size_bytes": { "$ref": "#/$defs/safe_integer" }, + "digest": { "$ref": "#/$defs/sha256_digest" } + } + }, + "context": { + "type": "object", + "additionalProperties": false, + "required": ["project_id", "workspace_root"], + "properties": { + "project_id": { "$ref": "#/$defs/bounded_string_255" }, + "workspace_root": { "$ref": "#/$defs/absolute_path" }, + "repository": { + "type": "object", + "additionalProperties": false, + "required": ["remote", "commit"], + "properties": { + "remote": { "$ref": "#/$defs/bounded_string_255" }, + "commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$", + "description": "Exact 40-character lowercase git commit SHA the work is bound to." + } + } + } + } + }, + "runtime": { + "type": "object", + "additionalProperties": false, + "required": ["required_capabilities", "constraints"], + "properties": { + "required_capabilities": { + "type": "array", + "maxItems": 64, + "items": { "$ref": "#/$defs/capability_token" }, + "description": "Runtime/worker capabilities every assigned worker session must satisfy." + }, + "constraints": { + "type": "object", + "maxProperties": 32, + "additionalProperties": { "$ref": "#/$defs/token_list" }, + "description": "Named runtime constraint classes (for example `os`, `arch`, `sandbox`) mapped to bounded token values." + } + } + }, + "deadline": { + "type": "object", + "additionalProperties": false, + "required": ["created_at", "timeout_at", "cancellation_channel"], + "properties": { + "created_at": { "$ref": "#/$defs/utc_timestamp" }, + "timeout_at": { + "$ref": "#/$defs/utc_timestamp", + "description": "Absolute orchestration deadline. Expiry may produce failed or recovery_required, never inferred success." + }, + "cancellation_channel": { + "$ref": "#/$defs/bounded_string_255", + "description": "Stable channel reference over which Coven cancellation requests arrive and Psyche acknowledgements return." + } + } + }, + "expectations": { + "type": "object", + "additionalProperties": false, + "required": ["result_schema", "delivery_mode"], + "properties": { + "result_schema": { + "const": "psyche.automation_result.v1", + "description": "The only result contract v1 invocations may expect." + }, + "delivery_mode": { + "enum": ["event_stream", "poll", "receipt_push"], + "description": "How Coven consumes the terminal result. The cursor remains Coven-persisted in every mode." + } + } + }, + "correlation": { + "type": "object", + "additionalProperties": false, + "required": ["correlation_id"], + "properties": { + "correlation_id": { "$ref": "#/$defs/bounded_string_255" }, + "causation_id": { "$ref": "#/$defs/bounded_string_255" }, + "event_cursor": { + "$ref": "#/$defs/bounded_string_255", + "description": "Coven's last durably consumed event cursor for resumed correlation, when this invocation continues earlier work." + } + } + }, + "producer": { + "type": "object", + "additionalProperties": false, + "required": ["producer_id", "contract_versions"], + "properties": { + "producer_id": { "$ref": "#/$defs/bounded_string_255" }, + "producer_instance": { "$ref": "#/$defs/bounded_string_255" }, + "contract_versions": { + "type": "object", + "maxProperties": 16, + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "description": "Pinned contract/profile versions (coven, familiar-contract, threads, runtime, SDK) keyed by stable program name." + } + } + }, + "privacy": { + "type": "object", + "additionalProperties": false, + "required": ["privacy_class", "retention_class"], + "properties": { + "privacy_class": { "$ref": "#/$defs/stable_token" }, + "retention_class": { "$ref": "#/$defs/stable_token" } + } + } + } + }, + "token_list": { + "type": "array", + "maxItems": 64, + "items": { "$ref": "#/$defs/scope_value" } + }, + "scope_value": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9./:_-]*$", + "description": "One explicit scope selector (repository name, path prefix, host, credential reference). Selectors are case-sensitive and their exact spellings are governed by the pinned authority profile; Psyche matches them literally and never broadens them." + } + } +} diff --git a/docs/schemas/automation-adapter/psyche.automation_result.v1.schema.json b/docs/schemas/automation-adapter/psyche.automation_result.v1.schema.json new file mode 100644 index 0000000..daf2084 --- /dev/null +++ b/docs/schemas/automation-adapter/psyche.automation_result.v1.schema.json @@ -0,0 +1,331 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/OpenCoven/psyche/main/docs/schemas/automation-adapter/psyche.automation_result.v1.schema.json", + "title": "psyche.automation_result.v1", + "description": "Terminal Psyche orchestration evidence for one adopted automation invocation, emitted exactly once per invocation. The envelope mirrors the digest-attested AdoptionRequest precedent in crates/psyche-coven/src/port.rs: `result` carries the complete typed content and `result_digest` is the SHA-256 over the canonical (RFC 8785) JSON bytes of the `result` object alone. Coven embeds or references this evidence in its final Automation Receipt; Psyche evidence proves the orchestration observations Psyche owns, while the Coven receipt proves the automation lifecycle, binding, and delivery Coven owns.", + "type": "object", + "additionalProperties": false, + "required": ["result", "result_digest"], + "properties": { + "result": { "$ref": "#/$defs/result_content" }, + "result_digest": { "$ref": "#/$defs/sha256_digest" } + }, + "$defs": { + "sha256_digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "stable_token": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$", + "maxLength": 64 + }, + "bounded_string_255": { + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "safe_integer": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "utc_timestamp": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]+)?Z$" + }, + "result_content": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "result_id", + "invocation_id", + "invocation_digest", + "status", + "reason_code", + "correlation", + "orchestration", + "bindings_exercised", + "artifacts", + "verification", + "capability_narrowing", + "cancellation_history", + "recovery_history", + "external_effects", + "timing", + "privacy", + "authentication" + ], + "properties": { + "schema_version": { + "const": "psyche.automation_result.v1" + }, + "result_id": { + "type": "string", + "pattern": "^ars_[0-9A-HJKMNP-TV-Z]{26}$", + "description": "Durable result identity, minted once at terminal state." + }, + "invocation_id": { + "type": "string", + "pattern": "^aiv_[0-9A-HJKMNP-TV-Z]{26}$" + }, + "invocation_digest": { + "$ref": "#/$defs/sha256_digest", + "description": "The adopted request digest, echoed exactly. Coven must match it against its own record before treating this result as evidence for the attempt." + }, + "status": { + "enum": ["completed", "failed", "cancelled", "recovery_required"], + "description": "Terminal only. There is no running or partial result: interim observations travel on the event stream. Timeout or lost worker evidence may produce failed or recovery_required, never inferred success." + }, + "reason_code": { + "$ref": "#/$defs/stable_token", + "description": "Stable terminal reason (for example `lanes_failed`, `deadline_exceeded`, `cancelled_by_request`, `worker_evidence_lost`)." + }, + "correlation": { + "type": "object", + "additionalProperties": false, + "required": ["occurrence_id", "run_id", "attempt_id"], + "description": "The exact Coven identifiers echoed from the adopted invocation. A mismatch with Coven's records voids the evidence.", + "properties": { + "occurrence_id": { "$ref": "#/$defs/bounded_string_255" }, + "run_id": { "$ref": "#/$defs/bounded_string_255" }, + "attempt_id": { "$ref": "#/$defs/bounded_string_255" } + } + }, + "orchestration": { + "type": "object", + "additionalProperties": false, + "required": ["execution_ref"], + "description": "References into Psyche-internal orchestration records. `execution_ref` identifies the Psyche execution graph created by adoption; lane entries correlate the internal task graph to worker sessions.", + "properties": { + "execution_ref": { "$ref": "#/$defs/bounded_string_255" }, + "lanes": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["lane_id", "worker_session_ref", "terminal_state"], + "properties": { + "lane_id": { "$ref": "#/$defs/bounded_string_255" }, + "node_ref": { + "$ref": "#/$defs/bounded_string_255", + "description": "Psyche-internal graph node record (nod_ shape) owning this lane's task." + }, + "worker_session_ref": { "$ref": "#/$defs/bounded_string_255" }, + "lease_ref": { "$ref": "#/$defs/bounded_string_255" }, + "terminal_state": { + "enum": ["completed", "failed", "cancelled", "skipped", "recovery_required"], + "description": "Lane terminal state, drawn from the frozen Psyche lane vocabulary." + } + } + } + } + } + }, + "bindings_exercised": { + "type": "object", + "additionalProperties": false, + "required": ["familiar_binding_digest", "authority", "runtime_capabilities"], + "properties": { + "familiar_binding_digest": { "$ref": "#/$defs/sha256_digest" }, + "authority": { + "type": "object", + "additionalProperties": false, + "required": ["threads_decision_digest", "approval_events"], + "properties": { + "threads_decision_digest": { "$ref": "#/$defs/sha256_digest" }, + "approval_events": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["approval_ref", "approval_digest", "disposition"], + "properties": { + "approval_ref": { "$ref": "#/$defs/bounded_string_255" }, + "approval_digest": { "$ref": "#/$defs/sha256_digest" }, + "disposition": { + "enum": ["granted", "denied", "expired", "rechecked"] + } + } + } + } + } + }, + "runtime_capabilities": { + "type": "array", + "maxItems": 64, + "items": { "$ref": "#/$defs/stable_token" }, + "description": "The runtime capability set actually exercised, which must be a subset of the invocation's runtime.required_capabilities." + } + } + }, + "artifacts": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["artifact_id", "digest", "media_type", "size_bytes", "expires_at"], + "description": "Content-addressed artifact references, matching the ContentAddressedReference shape in crates/psyche-coven/src/port.rs.", + "properties": { + "artifact_id": { "$ref": "#/$defs/bounded_string_255" }, + "digest": { "$ref": "#/$defs/sha256_digest" }, + "media_type": { + "type": "string", + "pattern": "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+$", + "maxLength": 255 + }, + "size_bytes": { "$ref": "#/$defs/safe_integer" }, + "expires_at": { "$ref": "#/$defs/utc_timestamp" }, + "provenance": { "$ref": "#/$defs/bounded_string_255" }, + "lane_id": { "$ref": "#/$defs/bounded_string_255" } + } + } + }, + "verification": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["lane_id", "method", "outcome"], + "properties": { + "lane_id": { "$ref": "#/$defs/bounded_string_255" }, + "method": { "$ref": "#/$defs/stable_token" }, + "outcome": { + "enum": ["verified", "failed", "unavailable"] + }, + "evidence_digest": { "$ref": "#/$defs/sha256_digest" } + } + } + }, + "capability_narrowing": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["lane_id", "narrowed_scopes", "reason_code"], + "description": "Every inheritance-or-narrowing decision a lane made. A lane appearing here with an empty narrowed_scopes list inherited the invocation projection unchanged; no entry may ever record a broadening.", + "properties": { + "lane_id": { "$ref": "#/$defs/bounded_string_255" }, + "narrowed_scopes": { + "type": "object", + "maxProperties": 16, + "additionalProperties": { + "type": "array", + "maxItems": 64, + "items": { "$ref": "#/$defs/stable_token" } + } + }, + "reason_code": { "$ref": "#/$defs/stable_token" } + } + } + }, + "cancellation_history": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["requested_at", "acknowledged_state"], + "description": "Replay-safe record of cancellation control traffic, using the frozen CancellationState acknowledgement vocabulary from crates/psyche-core/src/contracts/execution.rs.", + "properties": { + "requested_at": { "$ref": "#/$defs/utc_timestamp" }, + "acknowledged_state": { + "enum": [ + "termination_requested", + "acknowledged_terminated", + "acknowledged_already_terminal", + "termination_unknown" + ] + }, + "evidence_digest": { "$ref": "#/$defs/sha256_digest" } + } + } + }, + "recovery_history": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["recorded_at", "ambiguity", "disposition"], + "description": "Orchestration recovery attempts, matching the ambiguity/fence semantics of Recovery in crates/psyche-core/src/contracts/foundation.rs.", + "properties": { + "recorded_at": { "$ref": "#/$defs/utc_timestamp" }, + "ambiguity": { "$ref": "#/$defs/stable_token" }, + "disposition": { + "enum": ["reconciled", "fenced", "unresolved", "degraded_to_proposal"] + }, + "evidence_digest": { "$ref": "#/$defs/sha256_digest" } + } + } + }, + "external_effects": { + "type": "array", + "maxItems": 1024, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["effect_class", "state", "disclosure_digest"], + "description": "Disclosed external side effects. Ambiguous effects are surfaced here; automation retry is never automatic and stays a Coven decision.", + "properties": { + "effect_class": { "$ref": "#/$defs/stable_token" }, + "state": { + "enum": ["confirmed", "ambiguous", "compensated", "rolled_back"] + }, + "disclosure_digest": { "$ref": "#/$defs/sha256_digest" }, + "occurred_at": { "$ref": "#/$defs/utc_timestamp" }, + "lane_id": { "$ref": "#/$defs/bounded_string_255" } + } + } + }, + "timing": { + "type": "object", + "additionalProperties": false, + "required": ["started_at", "finished_at", "final_event_cursor"], + "properties": { + "started_at": { "$ref": "#/$defs/utc_timestamp" }, + "finished_at": { "$ref": "#/$defs/utc_timestamp" }, + "final_event_cursor": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "description": "Event sequence of the last event preceding this result; a subscriber that has consumed through it holds every observation the result summarizes." + } + } + }, + "privacy": { + "type": "object", + "additionalProperties": false, + "required": ["privacy_class", "retention_class", "redacted"], + "properties": { + "privacy_class": { "$ref": "#/$defs/stable_token" }, + "retention_class": { "$ref": "#/$defs/stable_token" }, + "redacted": { + "type": "boolean", + "description": "True when payload minimization or redaction was applied to this result relative to the unredacted internal evidence." + } + } + }, + "authentication": { + "type": "object", + "additionalProperties": false, + "required": ["scheme", "producer_id"], + "properties": { + "scheme": { + "const": "sha256-digest-v1", + "description": "The only authentication scheme defined for v1: result_digest attests the canonical result bytes and the transport must deliver them unmodified. A signature scheme is a deliberate future major-version decision." + }, + "producer_id": { "$ref": "#/$defs/bounded_string_255" }, + "producer_instance": { "$ref": "#/$defs/bounded_string_255" } + } + } + } + } + } +}