# Round-10 Error-Contract Cross-Layer Propagation Matrix

> Canonical path: `specs/workflow/error-contract-propagation.md`
> Scope: the three round-10 error contracts integrated by #t2687
> (#t2684/#t2665 restored observation, #t2685 canonical failure receipts,
> #t2686 closed Run event/EffectResult schemas).
> Machine-checkable: `guardrails/api/dependency-invariant-round10.sh` proves the
> code and tests named below still exist and are wired, so no behavior or test
> can silently disappear.

This document is the authoritative cross-layer propagation matrix for the
round-10 error contracts. "Propagation" here means: where each contract is
*produced*, where it is *projected*, where it is *consumed*, where it is
*redacted*, and where it is *never exposed*. Redaction/non-enumeration is a
first-class requirement: internal causes and malformed/oversized identity fields
must never surface in public outputs, and unknown/secret-bearing fields must
fail closed rather than being enumerated or truncated into a different identity.

## 1. Contract inventory

| Contract | Dep | Producer | Projection | Consumer(s) | Public surface |
|---|---|---|---|---|---|
| A. Restored structured observation | #t2684/#t2665 | `internal/store` (repository/message-store internal cause) | `internal/api/error_observation.go` (`errorForObservation`, `ObservedErrorWithCause`) | router structured failure log, OpenTelemetry span, Agent Workflow Step handler (`internal/api/agent/api_workflow_steps.go` slog `internal_cause`) | Stable reviewed `ErrorResponse` problem/Code/Stage text only; internal cause never in HTTP/Exec JSON, prompts, protected-selector responses, or `errors.Is/As` |
| B. Canonical failure receipts | #t2685 | `internal/workflow/run` producer (`failure_identity.go`, `failure_diagnostic_budget.go`, `reducer.go`) | `internal/api/workflow/run/failure_projection.go` (`projectOperationalEventEnvelope`, `readFailureReceipt`, prior+fresh merge) | Run history/show/log/event/effect surfaces; Desktop inbound prompt; web Run view | Closed `truncatedFields` vocabulary (`code/stage/path/problem/hint/message/field/key/dependencyPath/index`), `omittedCount`/`droppedCount`/`serializedBytes`/`minimalFallback`; oversized identity fields fail closed (dropped, never re-truncated into a different identity); free-text bounded |
| C. Closed Run event/EffectResult schemas | #t2686 | `internal/api/workflow/run/event_effect_schema.go` (per-kind closed schema registry, producer-derived validators) | `internal/api/workflow/run/failure_projection.go` (`projectOperationalEventEnvelope` routes every envelope through the per-kind allowlist; flat failure events project the exact failure namespace and never inherit the EffectResult `output` exemption), `responses.go` | Run show/log/event/effect, Agent/Desktop consumers, web Run view | Producer-derived ID grammars (kernel logical activation `a`+digits; 32-hex `messageId` + `msg_` legacy; `btn_` notificationId; `agt_`/`mem_`/`usr_` IDs), closed enums (kind/port/decision/status/message-scope incl `approved`/`rejected` authored ports), RFC3339 `retryAt`/`completedAt`, `notificationRevision` int64, validated list members/counts/WorkItem fields via the shared 256-byte `CanonicalWorkItemKey`; unknown fields dropped (no `run_canceled` user-output exemption: `run_canceled` is typed `{reason, actor}` with human/agent actor + authoritative IDs); well-typed secrets and unknown `failure*` fields rejected |

## 2. Cross-layer propagation matrix

The rows are the storage → API → runtime → agent → desktop → web surfaces that
each contract crosses. "Exact" means the public projection is derived directly
from the closed schema / canonical receipt; "Stable" means the internal cause or
raw source is never exposed and only the reviewed public text propagates.

| Layer / surface | A. InternalCause observation | B. Failure receipts | C. Event/Effect schema |
|---|---|---|---|
| Store (`internal/store`, `internal/coredb`) | Source of internal cause (`storeerr.InternalCause`) | Persisted `Failure` + truncation receipt (`FailureTruncation`) | Persisted Run rows with typed failure/effect context |
| Runtime (`internal/workflow/run`, `orchestrator`) | — | Canonical validators produce closed receipts; `completionFailureDigest` includes `OmittedCount`/`DroppedCount`/`SerializedBytes`/`TruncatedFields`/`MinimalFallback` so a replayed receipt with a different cut never matches | Reducer/fan-out/callback/retry/timeout/approval context preserved; non-failure rows also closed |
| API (`internal/api/workflow/run`, `internal/api/error_observation.go`) | `errorForObservation` surfaces internal cause at structured boundary only; `ObservedErrorWithCause` returns public + internal separately | `failure_projection.go` merges prior+fresh receipts, closes field vocabulary, drops oversized identity fields | `event_effect_schema.go` per-kind allowlist; every event envelope routes through it; unknown kinds fail closed to `{}` |
| Agent (`internal/api/agent/api_workflow_steps.go`, `api_workflow_run_follow_test.go`) | Step handler logs `internal_cause` as separate slog attr | Receipt fields reach Agent-visible run history | Canonical event kinds (e.g. `send_message`, `request_approval`) reach Agent follow/live surfaces |
| Desktop (client/Exec/cmd/prompt/E2E) | Stable public problem text in prompts; never internal cause | Prompt/exec output shows canonical bounded failure; never raw oversized/secret | Closed EffectResult flows to Desktop; malformed fields dropped |
| Web (Run view) | Stable `ErrorResponse` problem/Code/Stage | Closed receipt JSON (`droppedCount`/`minimalFallback`/`truncatedFields`) | Closed event/effect JSON; user output preserved only where declared |

## 3. Redaction / non-enumeration guarantees

These are hard invariants, proven by the dependency guardrails and the
dependency-invariant manifest:

1. **Internal causes never propagate publicly.** `storeerr.InternalCause` is
   consumed only at the structured observation boundary. It never appears in
   `errors.Is`/`As` classification, HTTP/Exec JSON bodies, prompt text, or
   protected-selector responses. Guardrail: `guardrails/api/internal-cause-observation.sh`.
2. **Malformed/oversized identity fields fail closed; never truncated into a
   different identity.** `code`/`stage`/`path`/`diagnostic code+path+field`/
   `WorkItem key`+`dependencyPath` are validated against the full raw value by
   the shared canonical producer validators. A malformed or oversized identity
   is *dropped*, not re-truncated into a different valid identity. Only free-text
   fields (`message`/`problem`/`hint`) are bounded.
3. **Receipt field names are a closed vocabulary.** `mergeFailureTruncation`
   true-unions, dedups, and sorts `truncatedFields`; any other name fails closed.
   A fresh `>32KiB` oversized failure always reports `minimalFallback:true` even
   with no prior receipt; prior+fresh merge accumulates `omittedCount`/
   `droppedCount` deterministically and ORs `minimalFallback`.
4. **Event/EffectResult schemas are closed typed.** Every Run event kind and
   flat EffectResult (failure and non-failure) validates producer-derived ID
   grammars (kernel logical activation `a`+digits; 32-hex `messageId` + `msg_`
   legacy; `btn_` notificationId; `agt_`/`mem_`/`usr_` IDs), closed enums
   (kind/port/decision/status/message-scope incl `approved`/`rejected` authored
   ports), RFC3339 times (`retryAt`/`completedAt`), `notificationRevision` int64,
   list members/counts and WorkItem fields (shared 256-byte `CanonicalWorkItemKey`)
   rather than trim/cap-only strings. Unknown fields are dropped; unknown event
   kinds fail closed to an empty object; well-typed `token=SECRET` and wrong
   prefixes/kinds/enums/times are rejected. A flat failure event never inherits
   the EffectResult response's open `output` exemption and never retains unknown
   `failure*` secrets. `run_canceled` is typed `{reason, actor}` (bounded prose
   through the authoritative 2048-byte limit, human/agent actor, authoritative
   user/agent/member IDs) — there is no blanket user-output exemption.
5. **Replay/idempotency is cut-sensitive.** `samePersistedTruncation` compares
   `DroppedCount`+`MinimalFallback`; `completionFailureDigest` includes the full
   receipt; `EffectResult.Normalize` clones all receipt fields. A replayed
   receipt with a different cut never matches the original.

## 4. Focused regression suites

| Contract | Suites (must exist) |
|---|---|
| A | `internal/api/error_observation_test.go`, `internal/api/agent/workflow_observation_test.go` |
| B | `internal/workflow/run/failure_identity_test.go`, `internal/api/workflow/run/canonical_receipt_roundtrip_test.go`, `internal/workflow/orchestrator/persisted_truncation_equality_test.go` |
| C | `internal/api/workflow/run/event_effect_schema_test.go`, `internal/api/workflow/run/event_effect_schema_storage_test.go`, `internal/api/workflow/run/event_kind_invariant_test.go`, `internal/api/workflow/run/producer_shape_roundtrip_test.go`, `internal/api/workflow/run/producer_generator_roundtrip_test.go`, `internal/workflow/orchestrator/producer_event_fields_test.go`, `internal/api/workflow/run/genuine_fullchain_e2e_test.go`, `internal/api/workflow/run/top_level_error_projection_test.go`, `internal/workflow/run/failure_identity_test.go` |

> **#t2587 (cross-package producer->public schema bijection):** the complete
> producer event declarations now live in the neutral shared contract package
> `internal/workflow/eventcontract` (`ProducerEventFieldSets`), imported by BOTH
> the producing boundary (`internal/workflow/orchestrator` re-exports it) and the
> closed public projector (`internal/api/workflow/run`). `eventKindSchema` enforces
> the bijection at projection time (a producer-declared kind must have a public
> schema and matching failure capability; drift fails closed). The bijection
> tests (`event_contract_bijection_test.go`) cover every producer kind (including
> kernel reducer/engine, orchestrator dispatcher/callback/approval/retry/timeout,
> store cancellation, and replay), missing/extra kinds, field drift, failure
> mismatch, and complete kind coverage against the AST-discovered producer kinds.
> The earlier test-only nine-kind claim is replaced by the exhaustive shared
> declaration.

> **#t2709b (producer-identity + actor-redaction correction):** the identity
> grammar is per-generator-family, tied mechanically to each real producer and
> NEVER a universal prefix list. `orchestrator.StableChildID` emits `wfx_`/`wra_` +
> 32 lowercase hex; the deterministic app notification producer emits `btn_` + 32
> lowercase hex; `identifiers.NewEntityID("usr"|"agt")` emits the prefix + exactly
> 14 base36; workspace membership persists the RAW `NewMemberID` (12 base36, no
> `mem_` wrapper). The factually-false `ValidCanonicalEntityID`/`knownEntityIDPrefixes`
> universal source of truth was removed and replaced with exported family
> validators (`ValidCanonicalEntityIDPrefix`, `ValidStableChildID`,
> `ValidMemberIDRaw`, `ValidMessageID`) wired to the correct validator per field
> in `event_effect_schema.go` and `effect_result_projection.go`. `envelopeFieldActor`
> uses exact human/agent NewEntityID id + raw-member grammar, closed keys/types,
> and exact input bytes so `usr_token=SECRET`, `mem_token=SECRET`, punctuation,
> whitespace, invalid UTF-8 and oversized lookalikes are dropped while genuine
> cancel actor context survives. `producer_generator_roundtrip_test.go` drives the
> real generators (StableChildID, NewEntityID, NewMemberID, NewMessageID) through
> the public RunLog/event/effect projections and asserts malformed actor redaction.

> **#t2708 (round-10 final):** the schema registry is validated by a mechanical
> AST/typed bijection (`event_kind_invariant_test.go` `discoverProducerEventKinds`)
> that independently parses the `internal/workflow/run`, `internal/workflow/orchestrator`
> and `internal/store/workflow/run` producer source and proves every producer kind has
> a closed schema, every failure-produced kind is failure-capable (with a small
> reviewed set for hand-built failure EventDrafts such as `message_callback_completed`),
> and no stale schema entry exists. The concrete reviewed field corrections are
> pinned by `TestConcreteReviewedEventFieldCorrections` and exercised end-to-end by
> `producer_shape_roundtrip_test.go` through the RunLog/event/effect HTTP DTO
> surfaces: `parallel_opened.activationId`, `fan_out_opened.nodeId`,
> `fan_out_resolved.port`, `approval_decided` without `notificationRevision`/
> `completedAt`, `loop_broken` NOT failure-capable, and an EffectResult non-failure
> projection matching `orchestrator.EffectResult` exactly (no invented
> `kind`/`status`/`retryAt`/`receiptRecovery`; authoritative Plan port grammar
> `^[a-z][a-z0-9_-]{0,63}$`). Identity fields use the producer-authoritative exact
> grammar (NewEntityID `{prefix}_{14 base36}` for user/agent/effect/notification,
> raw NewMemberID `{12 base36}` for `mem_`, raw 32-hex messageId, kernel logical
> activation `a`+20 digits), rejecting prefixed punctuation/token/oversize/invalid
> UTF-8 while preserving legitimate producer ids. Top-level ErrorCode projection
> (from #t2709 `top_level_error`) uses the shared safe failure-code grammar.

The dependency-invariant manifest (`guardrails/api/dependency-invariant-round10.sh`)
asserts these files and the key production symbols exist and are wired into
`scripts/harness-fast.sh`, so a regression that deletes a behavior or its test
fails CI.
