Summary
Not competing with #2376/WF-08 -- that's the plumbing (suspend/resume/grant/deny) and it's real, needed, orthogonal work. This is a separate, additive proposal for what the approver sees while deciding, once that plumbing lands.
request_approval suspends a run and waits for a human (or pubkey) to grant/deny via from. That's identity-gated authorization -- is this party allowed to say yes. It doesn't yet give the approver anything about whether the specific action is sound: the message field is free text, not a checkable claim.
Proposing an optional field that attaches an independently-checkable pre-action claim to the approval request as input to the human decision, never a replacement for it.
Concrete shape
Extend request_approval's config with an optional verdict_ref:
action: request_approval
from: "@release-manager"
message: "Deploy prod config v3"
verdict_ref: "sha256:1a3a2675...c02" # optional -- content-addressed pointer to an external, independently-checkable claim
timeout: 24h
When present, the workflow_approvals row carries verdict_ref alongside the token hash, and the kind:46010 announcement includes it so it shows up in the approver's needs-action feed next to the message -- something concrete to check, not just prose to trust. The approver still grants or denies; the claim is context. Additive on the request + announcement event only -- no changes to the grant/deny relay handlers or the resume path, so it doesn't depend on WF-08 landing first and could ship in parallel.
Why content-addressed, not inline
A pointer (vs. embedding the claim's payload) means the approver isn't asked to trust whoever attached it -- they (or anyone auditing the thread later) independently recompute/verify the referenced claim against its own issuer's public key, the same non-repudiation property buzz's signed-event model already has everywhere else. The field shape doesn't need to name any specific issuer -- any signed, recomputable claim fits the slot. We run one shape of this (a pre-action verdict, checkable without an account) and would build a real worked example once WF-08 lands, but that's one implementation, not what the field itself commits to.
Scope
Purely additive. Doesn't block or get blocked by #2376. from stays authoritative on who can grant -- this changes what they see, not who decides.
Worked interop appendix (added after cross-verification with @ThoughtProof)
Two independently-implemented, independently-recomputed digest pairs — useful for anyone implementing a verdict_ref-compatible checker who wants a literal fixture to test their own canonicalization against, rather than trusting either party's claim:
Pair 1 — bare content hash (no envelope): artifact_hash (ThoughtProof's side) and action_digest (ours) over the same raw bytes matched directly, no canonicalization involved.
Pair 2 — envelope layer, deliberately non-interchangeable: decision_ref (ours, binds content hash + policy fields + verdict) and package_digest (ThoughtProof's, binds the verify request only — claim/evidence/mode, no outcome enum) sit at the same structural layer but commit to different objects by design. Don't expect them to be equal even when the underlying artifact_hash matches.
Fixture (experiment-only, not a production contract):
{"claim":"<exact artifact raw string>","evidence":"<exact evidence string you choose; post bytes>","mode":"handoff","tier":"standard"}
Canonical form (RFC 8785 / JCS, undefined keys stripped):
{"claim":"<exact artifact raw string>","evidence":"<exact evidence string you choose; post bytes>","mode":"handoff","tier":"standard"}
sha256: d735f0020908d304d5b331665544f3cd85b20df64f2d1639d4251d4bf3aa5788
Verified independently on both sides against the same literal fixture: our path (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)) and ThoughtProof's (canonicalize, RFC 8785 proper) — no shared code, matching output. A mismatch on this exact fixture in practice is almost always key-stripping or float-formatting drift, not a real digest-scheme disagreement.
Summary
Not competing with #2376/WF-08 -- that's the plumbing (suspend/resume/grant/deny) and it's real, needed, orthogonal work. This is a separate, additive proposal for what the approver sees while deciding, once that plumbing lands.
request_approvalsuspends a run and waits for a human (or pubkey) to grant/deny viafrom. That's identity-gated authorization -- is this party allowed to say yes. It doesn't yet give the approver anything about whether the specific action is sound: themessagefield is free text, not a checkable claim.Proposing an optional field that attaches an independently-checkable pre-action claim to the approval request as input to the human decision, never a replacement for it.
Concrete shape
Extend
request_approval's config with an optionalverdict_ref:When present, the
workflow_approvalsrow carriesverdict_refalongside the token hash, and the kind:46010 announcement includes it so it shows up in the approver's needs-action feed next to the message -- something concrete to check, not just prose to trust. The approver still grants or denies; the claim is context. Additive on the request + announcement event only -- no changes to the grant/deny relay handlers or the resume path, so it doesn't depend on WF-08 landing first and could ship in parallel.Why content-addressed, not inline
A pointer (vs. embedding the claim's payload) means the approver isn't asked to trust whoever attached it -- they (or anyone auditing the thread later) independently recompute/verify the referenced claim against its own issuer's public key, the same non-repudiation property buzz's signed-event model already has everywhere else. The field shape doesn't need to name any specific issuer -- any signed, recomputable claim fits the slot. We run one shape of this (a pre-action verdict, checkable without an account) and would build a real worked example once WF-08 lands, but that's one implementation, not what the field itself commits to.
Scope
Purely additive. Doesn't block or get blocked by #2376.
fromstays authoritative on who can grant -- this changes what they see, not who decides.Worked interop appendix (added after cross-verification with @ThoughtProof)
Two independently-implemented, independently-recomputed digest pairs — useful for anyone implementing a
verdict_ref-compatible checker who wants a literal fixture to test their own canonicalization against, rather than trusting either party's claim:Pair 1 — bare content hash (no envelope):
artifact_hash(ThoughtProof's side) andaction_digest(ours) over the same raw bytes matched directly, no canonicalization involved.Pair 2 — envelope layer, deliberately non-interchangeable:
decision_ref(ours, binds content hash + policy fields + verdict) andpackage_digest(ThoughtProof's, binds the verify request only — claim/evidence/mode, no outcome enum) sit at the same structural layer but commit to different objects by design. Don't expect them to be equal even when the underlyingartifact_hashmatches.Fixture (experiment-only, not a production contract):
{"claim":"<exact artifact raw string>","evidence":"<exact evidence string you choose; post bytes>","mode":"handoff","tier":"standard"}Canonical form (RFC 8785 / JCS, undefined keys stripped):
Verified independently on both sides against the same literal fixture: our path (
json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)) and ThoughtProof's (canonicalize, RFC 8785 proper) — no shared code, matching output. A mismatch on this exact fixture in practice is almost always key-stripping or float-formatting drift, not a real digest-scheme disagreement.