approval-md 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/NOTICE +5 -0
- package/README.md +909 -4
- package/SPEC.md +445 -32
- package/cli.js +29 -3
- package/dist/src/adapters/agentmail.js +1200 -0
- package/dist/src/adapters/agentmail.js.map +1 -0
- package/dist/src/adapters/conformance.js +461 -0
- package/dist/src/adapters/conformance.js.map +1 -0
- package/dist/src/adapters/contract.js +941 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/email.js +749 -0
- package/dist/src/adapters/email.js.map +1 -0
- package/dist/src/adapters/env-passphrase.js +132 -0
- package/dist/src/adapters/env-passphrase.js.map +1 -0
- package/dist/src/adapters/registry.js +76 -0
- package/dist/src/adapters/registry.js.map +1 -0
- package/dist/src/adapters/smtp.js +499 -0
- package/dist/src/adapters/smtp.js.map +1 -0
- package/dist/src/adapters/vault-provider.js +161 -0
- package/dist/src/adapters/vault-provider.js.map +1 -0
- package/dist/src/channels/batch.js +121 -0
- package/dist/src/channels/batch.js.map +1 -0
- package/dist/src/channels/cli.js +468 -0
- package/dist/src/channels/cli.js.map +1 -0
- package/dist/src/channels/conformance.js +445 -0
- package/dist/src/channels/conformance.js.map +1 -0
- package/dist/src/channels/contract.js +494 -0
- package/dist/src/channels/contract.js.map +1 -0
- package/dist/src/channels/payload-view.js +43 -0
- package/dist/src/channels/payload-view.js.map +1 -0
- package/dist/src/channels/render-queue.js +564 -0
- package/dist/src/channels/render-queue.js.map +1 -0
- package/dist/src/channels/tagging.js +723 -0
- package/dist/src/channels/tagging.js.map +1 -0
- package/dist/src/channels/telegram.js +3190 -0
- package/dist/src/channels/telegram.js.map +1 -0
- package/dist/src/channels/web.js +903 -0
- package/dist/src/channels/web.js.map +1 -0
- package/dist/src/cli/adapter.js +278 -0
- package/dist/src/cli/adapter.js.map +1 -0
- package/dist/src/cli/amend.js +2171 -0
- package/dist/src/cli/amend.js.map +1 -0
- package/dist/src/cli/args.js +86 -0
- package/dist/src/cli/args.js.map +1 -0
- package/dist/src/cli/attest.js +307 -0
- package/dist/src/cli/attest.js.map +1 -0
- package/dist/src/cli/audit-card.js +201 -0
- package/dist/src/cli/audit-card.js.map +1 -0
- package/dist/src/cli/audit.js +460 -0
- package/dist/src/cli/audit.js.map +1 -0
- package/dist/src/cli/channel-telegram.js +2063 -0
- package/dist/src/cli/channel-telegram.js.map +1 -0
- package/dist/src/cli/channel-web.js +357 -0
- package/dist/src/cli/channel-web.js.map +1 -0
- package/dist/src/cli/channel.js +438 -0
- package/dist/src/cli/channel.js.map +1 -0
- package/dist/src/cli/checkpoint-tap.js +238 -0
- package/dist/src/cli/checkpoint-tap.js.map +1 -0
- package/dist/src/cli/coverage.js +343 -0
- package/dist/src/cli/coverage.js.map +1 -0
- package/dist/src/cli/daemon.js +631 -0
- package/dist/src/cli/daemon.js.map +1 -0
- package/dist/src/cli/doctor.js +2648 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/env.js +302 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/execute.js +1682 -0
- package/dist/src/cli/execute.js.map +1 -0
- package/dist/src/cli/exit-codes.js +82 -0
- package/dist/src/cli/exit-codes.js.map +1 -0
- package/dist/src/cli/feedback.js +205 -0
- package/dist/src/cli/feedback.js.map +1 -0
- package/dist/src/cli/gate-window.js +294 -0
- package/dist/src/cli/gate-window.js.map +1 -0
- package/dist/src/cli/gate.js +557 -0
- package/dist/src/cli/gate.js.map +1 -0
- package/dist/src/cli/git-scope.js +295 -0
- package/dist/src/cli/git-scope.js.map +1 -0
- package/dist/src/cli/gloss-attach.js +107 -0
- package/dist/src/cli/gloss-attach.js.map +1 -0
- package/dist/src/cli/gloss-codex-child.js +149 -0
- package/dist/src/cli/gloss-codex-child.js.map +1 -0
- package/dist/src/cli/gloss-codex.js +255 -0
- package/dist/src/cli/gloss-codex.js.map +1 -0
- package/dist/src/cli/gloss-options.js +79 -0
- package/dist/src/cli/gloss-options.js.map +1 -0
- package/dist/src/cli/gloss.js +362 -0
- package/dist/src/cli/gloss.js.map +1 -0
- package/dist/src/cli/help.js +2217 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/hook.js +2743 -0
- package/dist/src/cli/hook.js.map +1 -0
- package/dist/src/cli/import.js +175 -0
- package/dist/src/cli/import.js.map +1 -0
- package/dist/src/cli/init.js +336 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/instructions.js +262 -0
- package/dist/src/cli/instructions.js.map +1 -0
- package/dist/src/cli/journal.js +238 -0
- package/dist/src/cli/journal.js.map +1 -0
- package/dist/src/cli/log-advance.js +749 -0
- package/dist/src/cli/log-advance.js.map +1 -0
- package/dist/src/cli/log-anchor.js +387 -0
- package/dist/src/cli/log-anchor.js.map +1 -0
- package/dist/src/cli/log-checkpoint.js +128 -0
- package/dist/src/cli/log-checkpoint.js.map +1 -0
- package/dist/src/cli/log-sync.js +849 -0
- package/dist/src/cli/log-sync.js.map +1 -0
- package/dist/src/cli/log-verbs.js +354 -0
- package/dist/src/cli/log-verbs.js.map +1 -0
- package/dist/src/cli/long-help.js +148 -0
- package/dist/src/cli/long-help.js.map +1 -0
- package/dist/src/cli/main.js +1056 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/mcp.js +306 -0
- package/dist/src/cli/mcp.js.map +1 -0
- package/dist/src/cli/paths.js +79 -0
- package/dist/src/cli/paths.js.map +1 -0
- package/dist/src/cli/payload.js +253 -0
- package/dist/src/cli/payload.js.map +1 -0
- package/dist/src/cli/policy.js +229 -0
- package/dist/src/cli/policy.js.map +1 -0
- package/dist/src/cli/preflight.js +888 -0
- package/dist/src/cli/preflight.js.map +1 -0
- package/dist/src/cli/progress.js +112 -0
- package/dist/src/cli/progress.js.map +1 -0
- package/dist/src/cli/prompt.js +312 -0
- package/dist/src/cli/prompt.js.map +1 -0
- package/dist/src/cli/records.js +66 -0
- package/dist/src/cli/records.js.map +1 -0
- package/dist/src/cli/render.js +132 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/sandbox.js +150 -0
- package/dist/src/cli/sandbox.js.map +1 -0
- package/dist/src/cli/scaffold.js +137 -0
- package/dist/src/cli/scaffold.js.map +1 -0
- package/dist/src/cli/setup-adapter.js +475 -0
- package/dist/src/cli/setup-adapter.js.map +1 -0
- package/dist/src/cli/setup-channel.js +635 -0
- package/dist/src/cli/setup-channel.js.map +1 -0
- package/dist/src/cli/setup-checkpoint.js +196 -0
- package/dist/src/cli/setup-checkpoint.js.map +1 -0
- package/dist/src/cli/setup-common.js +376 -0
- package/dist/src/cli/setup-common.js.map +1 -0
- package/dist/src/cli/setup-flow.js +476 -0
- package/dist/src/cli/setup-flow.js.map +1 -0
- package/dist/src/cli/setup-service.js +308 -0
- package/dist/src/cli/setup-service.js.map +1 -0
- package/dist/src/cli/setup.js +473 -0
- package/dist/src/cli/setup.js.map +1 -0
- package/dist/src/cli/style.js +469 -0
- package/dist/src/cli/style.js.map +1 -0
- package/dist/src/cli/token.js +274 -0
- package/dist/src/cli/token.js.map +1 -0
- package/dist/src/cli/up.js +847 -0
- package/dist/src/cli/up.js.map +1 -0
- package/dist/src/cli/usage.js +91 -0
- package/dist/src/cli/usage.js.map +1 -0
- package/dist/src/cli/values.js +189 -0
- package/dist/src/cli/values.js.map +1 -0
- package/dist/src/cli/vault.js +362 -0
- package/dist/src/cli/vault.js.map +1 -0
- package/dist/src/cli/verb-registry.js +2173 -0
- package/dist/src/cli/verb-registry.js.map +1 -0
- package/dist/src/cli/wordmark.js +52 -0
- package/dist/src/cli/wordmark.js.map +1 -0
- package/dist/src/core/advance-cycle.js +200 -0
- package/dist/src/core/advance-cycle.js.map +1 -0
- package/dist/src/core/agents-md.js +747 -0
- package/dist/src/core/agents-md.js.map +1 -0
- package/dist/src/core/attest.js +577 -0
- package/dist/src/core/attest.js.map +1 -0
- package/dist/src/core/audit.js +882 -0
- package/dist/src/core/audit.js.map +1 -0
- package/dist/src/core/budgets.js +449 -0
- package/dist/src/core/budgets.js.map +1 -0
- package/dist/src/core/checkpoint.js +738 -0
- package/dist/src/core/checkpoint.js.map +1 -0
- package/dist/src/core/child-env.js +86 -0
- package/dist/src/core/child-env.js.map +1 -0
- package/dist/src/core/clock.js +43 -0
- package/dist/src/core/clock.js.map +1 -0
- package/dist/src/core/command-class.js +2321 -0
- package/dist/src/core/command-class.js.map +1 -0
- package/dist/src/core/coverage-sources/adapter.js +71 -0
- package/dist/src/core/coverage-sources/adapter.js.map +1 -0
- package/dist/src/core/coverage-sources/gh.js +136 -0
- package/dist/src/core/coverage-sources/gh.js.map +1 -0
- package/dist/src/core/coverage-sources/git.js +269 -0
- package/dist/src/core/coverage-sources/git.js.map +1 -0
- package/dist/src/core/coverage.js +337 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/credential-spec.js +23 -0
- package/dist/src/core/credential-spec.js.map +1 -0
- package/dist/src/core/dark-session.js +714 -0
- package/dist/src/core/dark-session.js.map +1 -0
- package/dist/src/core/decision-refusal.js +265 -0
- package/dist/src/core/decision-refusal.js.map +1 -0
- package/dist/src/core/env-file.js +837 -0
- package/dist/src/core/env-file.js.map +1 -0
- package/dist/src/core/execute.js +1233 -0
- package/dist/src/core/execute.js.map +1 -0
- package/dist/src/core/frontmatter.js +100 -0
- package/dist/src/core/frontmatter.js.map +1 -0
- package/dist/src/core/gate-window.js +506 -0
- package/dist/src/core/gate-window.js.map +1 -0
- package/dist/src/core/gate.js +2947 -0
- package/dist/src/core/gate.js.map +1 -0
- package/dist/src/core/git-run.js +93 -0
- package/dist/src/core/git-run.js.map +1 -0
- package/dist/src/core/harness-version.js +210 -0
- package/dist/src/core/harness-version.js.map +1 -0
- package/dist/src/core/harness-wait.js +58 -0
- package/dist/src/core/harness-wait.js.map +1 -0
- package/dist/src/core/head-retry.js +121 -0
- package/dist/src/core/head-retry.js.map +1 -0
- package/dist/src/core/instance.js +319 -0
- package/dist/src/core/instance.js.map +1 -0
- package/dist/src/core/intake-limits.js +350 -0
- package/dist/src/core/intake-limits.js.map +1 -0
- package/dist/src/core/jcs.js +132 -0
- package/dist/src/core/jcs.js.map +1 -0
- package/dist/src/core/journal.js +200 -0
- package/dist/src/core/journal.js.map +1 -0
- package/dist/src/core/live-draw.js +703 -0
- package/dist/src/core/live-draw.js.map +1 -0
- package/dist/src/core/log-reconcile.js +136 -0
- package/dist/src/core/log-reconcile.js.map +1 -0
- package/dist/src/core/log.js +546 -0
- package/dist/src/core/log.js.map +1 -0
- package/dist/src/core/loop.js +476 -0
- package/dist/src/core/loop.js.map +1 -0
- package/dist/src/core/md-fence.js +74 -0
- package/dist/src/core/md-fence.js.map +1 -0
- package/dist/src/core/money.js +195 -0
- package/dist/src/core/money.js.map +1 -0
- package/dist/src/core/payload-census.js +146 -0
- package/dist/src/core/payload-census.js.map +1 -0
- package/dist/src/core/payload-store.js +340 -0
- package/dist/src/core/payload-store.js.map +1 -0
- package/dist/src/core/payload.js +80 -0
- package/dist/src/core/payload.js.map +1 -0
- package/dist/src/core/policy-diff.js +565 -0
- package/dist/src/core/policy-diff.js.map +1 -0
- package/dist/src/core/policy-expectations.js +394 -0
- package/dist/src/core/policy-expectations.js.map +1 -0
- package/dist/src/core/policy-explain.js +230 -0
- package/dist/src/core/policy-explain.js.map +1 -0
- package/dist/src/core/policy-load.js +524 -0
- package/dist/src/core/policy-load.js.map +1 -0
- package/dist/src/core/policy-match.js +467 -0
- package/dist/src/core/policy-match.js.map +1 -0
- package/dist/src/core/policy-proposal.js +458 -0
- package/dist/src/core/policy-proposal.js.map +1 -0
- package/dist/src/core/prompt-layout.js +422 -0
- package/dist/src/core/prompt-layout.js.map +1 -0
- package/dist/src/core/protected-path-guard.js +1087 -0
- package/dist/src/core/protected-path-guard.js.map +1 -0
- package/dist/src/core/registration.js +39 -0
- package/dist/src/core/registration.js.map +1 -0
- package/dist/src/core/reindex.js +336 -0
- package/dist/src/core/reindex.js.map +1 -0
- package/dist/src/core/sampler.js +388 -0
- package/dist/src/core/sampler.js.map +1 -0
- package/dist/src/core/sandbox.js +424 -0
- package/dist/src/core/sandbox.js.map +1 -0
- package/dist/src/core/seal.js +290 -0
- package/dist/src/core/seal.js.map +1 -0
- package/dist/src/core/state.js +1009 -0
- package/dist/src/core/state.js.map +1 -0
- package/dist/src/core/task-file.js +464 -0
- package/dist/src/core/task-file.js.map +1 -0
- package/dist/src/core/telegram-config.js +114 -0
- package/dist/src/core/telegram-config.js.map +1 -0
- package/dist/src/core/token.js +578 -0
- package/dist/src/core/token.js.map +1 -0
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -0
- package/dist/src/core/values.js +153 -0
- package/dist/src/core/values.js.map +1 -0
- package/dist/src/core/vault.js +612 -0
- package/dist/src/core/vault.js.map +1 -0
- package/dist/src/core/verified-snapshot.js +506 -0
- package/dist/src/core/verified-snapshot.js.map +1 -0
- package/dist/src/core/verify.js +549 -0
- package/dist/src/core/verify.js.map +1 -0
- package/dist/src/core/version.js +9 -0
- package/dist/src/core/version.js.map +1 -0
- package/dist/src/core/wysiwys.js +728 -0
- package/dist/src/core/wysiwys.js.map +1 -0
- package/dist/src/daemon/advance-child.js +78 -0
- package/dist/src/daemon/advance-child.js.map +1 -0
- package/dist/src/daemon/advance.js +849 -0
- package/dist/src/daemon/advance.js.map +1 -0
- package/dist/src/daemon/audit.js +90 -0
- package/dist/src/daemon/audit.js.map +1 -0
- package/dist/src/daemon/daemon.js +1988 -0
- package/dist/src/daemon/daemon.js.map +1 -0
- package/dist/src/daemon/dark-session.js +119 -0
- package/dist/src/daemon/dark-session.js.map +1 -0
- package/dist/src/daemon/draw-child.js +132 -0
- package/dist/src/daemon/draw-child.js.map +1 -0
- package/dist/src/daemon/draw.js +458 -0
- package/dist/src/daemon/draw.js.map +1 -0
- package/dist/src/daemon/git-evidence.js +345 -0
- package/dist/src/daemon/git-evidence.js.map +1 -0
- package/dist/src/daemon/projection.js +233 -0
- package/dist/src/daemon/projection.js.map +1 -0
- package/dist/src/daemon/prune.js +376 -0
- package/dist/src/daemon/prune.js.map +1 -0
- package/dist/src/mcp/http.js +343 -0
- package/dist/src/mcp/http.js.map +1 -0
- package/dist/src/mcp/server.js +594 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/docs/cli-reference.md +5363 -0
- package/package.json +43 -4
- package/schema/.gitkeep +0 -0
- package/schema/LICENSE +117 -0
- package/schema/envelope.schema.json +137 -0
- package/schema/event.schema.json +1810 -0
- package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
- package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
- package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
- package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
- package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
- package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
- package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
- package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
- package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
- package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
- package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
- package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
- package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
- package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
- package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
- package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
- package/schema/fixtures/envelope/valid/canonical.json +25 -0
- package/schema/fixtures/envelope/valid/minimal.json +7 -0
- package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
- package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
- package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
- package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
- package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
- package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
- package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
- package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
- package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
- package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
- package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
- package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
- package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
- package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
- package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
- package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
- package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
- package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
- package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
- package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
- package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
- package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
- package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
- package/schema/fixtures/event/invalid/missing-alg.json +14 -0
- package/schema/fixtures/event/invalid/missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
- package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
- package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
- package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
- package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
- package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
- package/schema/fixtures/event/invalid/short-hash.json +15 -0
- package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
- package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
- package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
- package/schema/fixtures/event/valid/approval-expired.json +15 -0
- package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
- package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
- package/schema/fixtures/event/valid/approval-granted.json +15 -0
- package/schema/fixtures/event/valid/approval-rejected.json +15 -0
- package/schema/fixtures/event/valid/approval-requested.json +19 -0
- package/schema/fixtures/event/valid/approval-revoked.json +15 -0
- package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
- package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
- package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
- package/schema/fixtures/event/valid/audit-sampled.json +14 -0
- package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
- package/schema/fixtures/event/valid/envelope-drift.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
- package/schema/fixtures/event/valid/execution-completed.json +15 -0
- package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
- package/schema/fixtures/event/valid/execution-failed.json +15 -0
- package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
- package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
- package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
- package/schema/fixtures/event/valid/execution-started.json +14 -0
- package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
- package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
- package/schema/fixtures/event/valid/gate-closed.json +14 -0
- package/schema/fixtures/event/valid/gate-opened.json +16 -0
- package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
- package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
- package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
- package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
- package/schema/fixtures/event/valid/payload-pruned.json +17 -0
- package/schema/fixtures/event/valid/policy-declined.json +16 -0
- package/schema/fixtures/event/valid/policy-proposed.json +35 -0
- package/schema/fixtures/event/valid/policy-updated.json +14 -0
- package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
- package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
- package/schema/fixtures/event/valid/route-accepted.json +15 -0
- package/schema/fixtures/event/valid/route-proposed.json +16 -0
- package/schema/fixtures/event/valid/spec-example.json +15 -0
- package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
- package/schema/fixtures/event/valid/task-registered.json +14 -0
- package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
- package/schema/fixtures/hash/known-answer.json +74 -0
- package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
- package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
- package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
- package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
- package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
- package/schema/fixtures/policy/invalid/missing-version.json +8 -0
- package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
- package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
- package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
- package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
- package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
- package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
- package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
- package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
- package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
- package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
- package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
- package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
- package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
- package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
- package/schema/fixtures/policy/valid/canonical.json +47 -0
- package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
- package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
- package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
- package/schema/fixtures/policy/valid/global-budgets.json +19 -0
- package/schema/fixtures/policy/valid/human-only.json +9 -0
- package/schema/fixtures/policy/valid/minimal.json +6 -0
- package/schema/fixtures/policy/valid/protected-paths.json +10 -0
- package/schema/fixtures/policy/valid/record-namespace.json +13 -0
- package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
- package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
- package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
- package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
- package/schema/fixtures/policy/valid/wildcards.json +15 -0
- package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
- package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
- package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
- package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
- package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
- package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
- package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
- package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
- package/schema/fixtures/policy-md/valid/canonical.md +50 -0
- package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
- package/schema/fixtures/policy-md/valid/minimal.md +3 -0
- package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
- package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
- package/schema/fixtures/policy-md/valid/with-values.md +79 -0
- package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
- package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
- package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
- package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
- package/schema/fixtures/sample-record/valid/minimal.json +4 -0
- package/schema/fixtures/sample-record/valid/with-note.json +5 -0
- package/schema/fixtures/values/invalid/class-shaped.json +9 -0
- package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
- package/schema/fixtures/values/invalid/non-string-item.json +4 -0
- package/schema/fixtures/values/invalid/over-cap.json +26 -0
- package/schema/fixtures/values/invalid/unknown-key.json +5 -0
- package/schema/fixtures/values/invalid/version-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +7 -0
- package/schema/fixtures/values/valid/full.json +20 -0
- package/schema/fixtures/values/valid/minimal.json +1 -0
- package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
- package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
- package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
- package/schema/fixtures/values-md/valid/absent.md +50 -0
- package/schema/fixtures/values-md/valid/with-values.md +79 -0
- package/schema/policy.schema.json +481 -0
- package/schema/sample-record.schema.json +26 -0
- package/schema/values.schema.json +55 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audit lifecycle (SPEC.md §5.2, §9.1, §12): `audit.sampled` →
|
|
3
|
+
* `audit.reviewed` → (on a denial) `reconciliation.required` →
|
|
4
|
+
* `reconciliation.satisfied`.
|
|
5
|
+
*
|
|
6
|
+
* ## What a retrospective denial can and cannot do (amended SPEC.md §5.2, APRV-127)
|
|
7
|
+
*
|
|
8
|
+
* The action already happened. The runtime cannot undo it, and any design that
|
|
9
|
+
* pretended otherwise would be lying to the person who denied it. What it can do
|
|
10
|
+
* is **oblige and record**: a denial appends an obligation naming the action, its
|
|
11
|
+
* class, and the review that denied, and the obligation stays open, and loud,
|
|
12
|
+
* until a HUMAN says it is discharged. Which obligation depends on whether the
|
|
13
|
+
* action was declared reversible — see {@link obligationFor}, including why a
|
|
14
|
+
* self-reported `reversible` may choose the shape and can never remove one.
|
|
15
|
+
*
|
|
16
|
+
* This is also why the autonomy split has a floor. `supervised-retro` refuses an
|
|
17
|
+
* action declaring `reversible: false` (`core/policy-match.ts`'s irreversibility
|
|
18
|
+
* floor pushes it to `manual`), because retrospective review of something that
|
|
19
|
+
* cannot be undone is regret with a paper trail. The floor is a floor and not a
|
|
20
|
+
* proof: it acts on a claim the acting party makes about itself, so it catches
|
|
21
|
+
* the honest declaration and never the lie. The obligation machinery here is what
|
|
22
|
+
* remains when the floor did not apply and the review said no anyway.
|
|
23
|
+
*
|
|
24
|
+
* Two halves, deliberately asymmetric:
|
|
25
|
+
*
|
|
26
|
+
* - **Sampling is the runtime's.** It is mechanical, it is retrospective, and it
|
|
27
|
+
* escalates a supervised action that already executed into a human's review
|
|
28
|
+
* queue. {@link sampleSupervised} appends `audit.sampled` with a `system:`
|
|
29
|
+
* actor. Selection is `core/sampler.ts`'s keyed HMAC, so no agent can predict
|
|
30
|
+
* or steer it.
|
|
31
|
+
* - **Review is a human's.** {@link reviewSample} appends `audit.reviewed` and
|
|
32
|
+
* refuses any actor that is not `human:<id>`, by the same rule that governs
|
|
33
|
+
* `grant`, `reject`, `revoke`, `policy attest`, and `execution resolve`. A
|
|
34
|
+
* runtime that could mark its own samples reviewed would be a supervision
|
|
35
|
+
* backlog that empties itself, which is precisely the failure the sampled
|
|
36
|
+
* backlog exists to make visible.
|
|
37
|
+
*
|
|
38
|
+
* ## Eligibility is derived, never self-reported (global invariant 4)
|
|
39
|
+
*
|
|
40
|
+
* A candidate is an `execution.started` record whose action's class, as recorded
|
|
41
|
+
* in the `task.registered` declaration the log already holds, resolves to
|
|
42
|
+
* `supervised` under the live policy. Two things follow, both load-bearing:
|
|
43
|
+
*
|
|
44
|
+
* - the class comes from the registration record rather than from the
|
|
45
|
+
* `execution.started` payload, and the autonomy comes from re-running
|
|
46
|
+
* `core/policy-match.ts` rather than from any field claiming an autonomy. No
|
|
47
|
+
* payload key an authoring party writes can move an action out of the
|
|
48
|
+
* candidate set;
|
|
49
|
+
* - eligibility is recomputed from the log every sweep, so it does not depend on
|
|
50
|
+
* any remembered flag, and a candidate cannot exclude itself by writing
|
|
51
|
+
* anything into its own event.
|
|
52
|
+
*
|
|
53
|
+
* The manual path is excluded because it never resolves `supervised`: a manual
|
|
54
|
+
* action's start is authorized by a token and its class resolves `manual`, so it
|
|
55
|
+
* is not a candidate and is not double-counted.
|
|
56
|
+
*
|
|
57
|
+
* ## Exactly once, without remembering anything
|
|
58
|
+
*
|
|
59
|
+
* Every sweep re-derives the whole candidate set and subtracts the subjects the
|
|
60
|
+
* log already carries an `audit.sampled` for, keyed on the subject record's
|
|
61
|
+
* `hash` (unique per record by construction, and stable across re-reads). A
|
|
62
|
+
* daemon restart, a second daemon, and a manual sweep all converge on the same
|
|
63
|
+
* set, and none of them can double-sample. Every append passes `expectedHead`,
|
|
64
|
+
* so a check made against one log cannot land on another (SPEC.md §11.1
|
|
65
|
+
* invariant 5).
|
|
66
|
+
*
|
|
67
|
+
* ## Time
|
|
68
|
+
*
|
|
69
|
+
* `audit.*` is gate-typed (SPEC.md §8), so no public function here takes a `ts`:
|
|
70
|
+
* the timestamp is read from the injected clock at the write boundary, and the
|
|
71
|
+
* party being audited does not author the clock it is judged by.
|
|
72
|
+
*/
|
|
73
|
+
import { tick } from "./clock.js";
|
|
74
|
+
import { findDeclaration, indexDeclarations } from "./execute.js";
|
|
75
|
+
import { appendEvent, } from "./log.js";
|
|
76
|
+
import { loadPolicy } from "./policy-load.js";
|
|
77
|
+
import { resolve } from "./policy-match.js";
|
|
78
|
+
import { resolveSampler } from "./sampler.js";
|
|
79
|
+
import { payloadOf, readVerifiedRecords } from "./state.js";
|
|
80
|
+
/**
|
|
81
|
+
* SPEC.md §8: the sampler is the runtime, so its actor is `system:`. Distinct
|
|
82
|
+
* from `system:gate` (expiries) and `system:daemon` (envelope drift) so a reader
|
|
83
|
+
* can tell which part of the runtime spoke without reading the payload.
|
|
84
|
+
*/
|
|
85
|
+
export const AUDIT_ACTOR = "system:audit";
|
|
86
|
+
/** `human:<id>`, the only actor a review may carry. */
|
|
87
|
+
const HUMAN_ACTOR = /^human:.+/u;
|
|
88
|
+
/**
|
|
89
|
+
* The closed set of audit refusal codes. Frozen public API in the same sense the
|
|
90
|
+
* gate's and the executor's are: a supervisor branches on these strings, so
|
|
91
|
+
* adding one is a spec change and renaming one is a breaking change.
|
|
92
|
+
*/
|
|
93
|
+
export const AUDIT_REFUSAL_CODES = [
|
|
94
|
+
/** Review was attempted by an actor that is not `human:<id>`. */
|
|
95
|
+
"actor-not-human",
|
|
96
|
+
/** No `audit.sampled` record matches the subject named. */
|
|
97
|
+
"not-sampled",
|
|
98
|
+
/** That sample already has a later `audit.reviewed`. */
|
|
99
|
+
"already-reviewed",
|
|
100
|
+
/** An action key with more than one unreviewed sample; name the seq instead. */
|
|
101
|
+
"ambiguous-subject",
|
|
102
|
+
/** No `reconciliation.required` record at the seq named (APRV-127). */
|
|
103
|
+
"not-obliged",
|
|
104
|
+
/** That obligation already has a `reconciliation.satisfied` (APRV-127). */
|
|
105
|
+
"already-satisfied",
|
|
106
|
+
/**
|
|
107
|
+
* A verb that requires a reason was given none. Two shapes, one code: a
|
|
108
|
+
* reconciliation satisfied with a blank note (APRV-127), and a review whose
|
|
109
|
+
* `reaction` is `loved` or `disliked` with a blank note (APRV-239). Both are
|
|
110
|
+
* an assertion nobody can check, and both are evaluated after the actor check
|
|
111
|
+
* and before the log is read.
|
|
112
|
+
*/
|
|
113
|
+
"note-required",
|
|
114
|
+
/**
|
|
115
|
+
* A review that says the action should not have happened and that the human
|
|
116
|
+
* liked or loved it (APRV-239). Evaluated beside `note-required`, before the
|
|
117
|
+
* log is read; nothing is appended.
|
|
118
|
+
*
|
|
119
|
+
* The two fields point opposite ways and only one of them is enforcement, so
|
|
120
|
+
* the safe reading is not "believe the verdict and drop the grade": a record
|
|
121
|
+
* carrying both would be read by a person later as evidence of whichever half
|
|
122
|
+
* suited them, and by an agent as a signal that a denial is survivable if the
|
|
123
|
+
* operator is pleased. The reviewer is asked to say which they meant.
|
|
124
|
+
*/
|
|
125
|
+
"reaction-conflicts-verdict",
|
|
126
|
+
/**
|
|
127
|
+
* A `gated-revert` obligation whose satisfaction names no completed revert
|
|
128
|
+
* (APRV-127). The obligation is to undo the action THROUGH THE GATE, and the
|
|
129
|
+
* evidence of that is an `execution.completed` in this same log.
|
|
130
|
+
*/
|
|
131
|
+
"revert-required",
|
|
132
|
+
/**
|
|
133
|
+
* The denial was recorded and its obligation was not (APRV-127). The log is
|
|
134
|
+
* NOT inconsistent — `audit.reviewed` stands and says `denied` — but the
|
|
135
|
+
* obligation it should have created is missing and must be created by
|
|
136
|
+
* reviewing again once the head settles.
|
|
137
|
+
*/
|
|
138
|
+
"obligation-not-appended",
|
|
139
|
+
/** The log could not be read, or holds a line that is not a record. */
|
|
140
|
+
"log-unreadable",
|
|
141
|
+
/** The log's final line is unterminated (a crashed write). */
|
|
142
|
+
"log-torn-tail",
|
|
143
|
+
/** The chain does not verify; nothing is derived from an untrustworthy log. */
|
|
144
|
+
"log-corrupt",
|
|
145
|
+
/** The append itself failed; `append` carries the underlying error. */
|
|
146
|
+
"append-failed",
|
|
147
|
+
];
|
|
148
|
+
function refuse(code, message, extra = {}) {
|
|
149
|
+
return { ok: false, code, message, ...extra };
|
|
150
|
+
}
|
|
151
|
+
function policyFor(options, cwd) {
|
|
152
|
+
const where = options.policy?.file !== undefined
|
|
153
|
+
? { file: options.policy.file }
|
|
154
|
+
: { dir: options.policy?.dir ?? cwd };
|
|
155
|
+
if (options.schemaDir !== undefined)
|
|
156
|
+
where.schemaDir = options.schemaDir;
|
|
157
|
+
return loadPolicy(where);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Every `execution.started` whose action resolves `supervised` under `load`, in
|
|
161
|
+
* log order.
|
|
162
|
+
*
|
|
163
|
+
* Pure: no I/O, no clock, no environment. Records with no action key, and keys
|
|
164
|
+
* no `task.registered` record declares, are skipped rather than guessed at — an
|
|
165
|
+
* undeclared key has no class, and inventing one would put a fact in the sample
|
|
166
|
+
* that nobody wrote.
|
|
167
|
+
*/
|
|
168
|
+
export function supervisedExecutions(records, load) {
|
|
169
|
+
const all = records;
|
|
170
|
+
const autonomyByClass = new Map();
|
|
171
|
+
const candidates = [];
|
|
172
|
+
// One forward pass over the log answers, for every key at once, the three
|
|
173
|
+
// questions this loop used to ask per candidate with a full scan each
|
|
174
|
+
// (APRV-211): which tasks declare the key (declaringTasks), what the last
|
|
175
|
+
// registration declared (findDeclaration), and whether a human was ever asked
|
|
176
|
+
// (hasApprovalCycle). Same records, same answers, same order — the index is a
|
|
177
|
+
// per-call derivation of this call's own verified records and nothing is
|
|
178
|
+
// remembered between sweeps. `tests/audit-index.test.ts` pins it against the
|
|
179
|
+
// per-key helpers, key by key, and against this function's previous algorithm.
|
|
180
|
+
const index = indexDeclarations(all);
|
|
181
|
+
for (const record of all) {
|
|
182
|
+
if (record.event !== "execution.started")
|
|
183
|
+
continue;
|
|
184
|
+
const actionKey = record.action_key;
|
|
185
|
+
if (typeof actionKey !== "string" || actionKey.length === 0)
|
|
186
|
+
continue;
|
|
187
|
+
// A key declared by more than one task is a refused collision (APRV-138);
|
|
188
|
+
// do not sample from an ambiguous declaration.
|
|
189
|
+
if ((index.declaringTasks.get(actionKey)?.length ?? 0) > 1)
|
|
190
|
+
continue;
|
|
191
|
+
// APRV-127. An action a human was already asked about is not a candidate for
|
|
192
|
+
// review of an unreviewed decision — there was a decision. The case is a
|
|
193
|
+
// `supervised-live` action the live draw selected: it executed on a grant,
|
|
194
|
+
// through the manual path, and its class still resolves `supervised`, so
|
|
195
|
+
// without this line it would be drawn a second time into a backlog asking a
|
|
196
|
+
// person to review the answer they themselves gave. Costs nothing for every
|
|
197
|
+
// other supervised action, which never carries an approval cycle.
|
|
198
|
+
if (index.requested.has(actionKey))
|
|
199
|
+
continue;
|
|
200
|
+
// `index.declarations` holds, per key, exactly what findDeclaration returns
|
|
201
|
+
// for it: the class comes from the `task.registered` record the log carries
|
|
202
|
+
// and from nowhere else.
|
|
203
|
+
const declared = index.declarations.get(actionKey) ?? null;
|
|
204
|
+
if (declared === null)
|
|
205
|
+
continue;
|
|
206
|
+
let autonomy = autonomyByClass.get(declared.class);
|
|
207
|
+
if (autonomy === undefined) {
|
|
208
|
+
autonomy = resolve(load, declared.class).autonomy;
|
|
209
|
+
autonomyByClass.set(declared.class, autonomy);
|
|
210
|
+
}
|
|
211
|
+
if (autonomy !== "supervised")
|
|
212
|
+
continue;
|
|
213
|
+
candidates.push({
|
|
214
|
+
seq: record.seq,
|
|
215
|
+
hash: record.hash,
|
|
216
|
+
ts: record.ts,
|
|
217
|
+
actionKey,
|
|
218
|
+
task: typeof record.task === "string" ? record.task : declared.task,
|
|
219
|
+
class: declared.class,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return candidates;
|
|
223
|
+
}
|
|
224
|
+
function stringOrNull(value) {
|
|
225
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Every `audit.sampled` in the log, each tagged with the `audit.reviewed` that
|
|
229
|
+
* closes it.
|
|
230
|
+
*
|
|
231
|
+
* A review closes a sample only when it comes **after** it in the chain and
|
|
232
|
+
* names the same action key. An earlier review is a review of an earlier sample;
|
|
233
|
+
* treating it as covering this one would silently empty the backlog, which is
|
|
234
|
+
* exactly the failure a sampled-audit backlog exists to prevent. This mirrors
|
|
235
|
+
* `channels/render-queue.ts`'s matching rule, so the CLI and the queue
|
|
236
|
+
* projection never disagree about what is outstanding.
|
|
237
|
+
*/
|
|
238
|
+
export function sampledSubjects(records) {
|
|
239
|
+
const subjects = [];
|
|
240
|
+
for (const record of records) {
|
|
241
|
+
if (record.event !== "audit.sampled")
|
|
242
|
+
continue;
|
|
243
|
+
const payload = payloadOf(record);
|
|
244
|
+
const actionKey = stringOrNull(record.action_key) ?? stringOrNull(payload["action_key"]);
|
|
245
|
+
const subjectSeq = payload["subject_seq"];
|
|
246
|
+
subjects.push({
|
|
247
|
+
seq: record.seq,
|
|
248
|
+
ts: record.ts,
|
|
249
|
+
actionKey,
|
|
250
|
+
task: stringOrNull(record.task) ?? stringOrNull(payload["task"]),
|
|
251
|
+
subjectHash: stringOrNull(payload["subject_hash"]),
|
|
252
|
+
subjectSeq: typeof subjectSeq === "number" && Number.isInteger(subjectSeq) ? subjectSeq : null,
|
|
253
|
+
reviewedSeq: null,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
for (const subject of subjects) {
|
|
257
|
+
for (const record of records) {
|
|
258
|
+
if (record.event !== "audit.reviewed" || record.seq <= subject.seq)
|
|
259
|
+
continue;
|
|
260
|
+
const payload = payloadOf(record);
|
|
261
|
+
const key = stringOrNull(record.action_key) ?? stringOrNull(payload["action_key"]);
|
|
262
|
+
const reviewedSeq = payload["subject_seq"];
|
|
263
|
+
const namesThisSample = typeof reviewedSeq === "number" && reviewedSeq === subject.seq
|
|
264
|
+
? true
|
|
265
|
+
: subject.actionKey !== null && key !== null && key === subject.actionKey;
|
|
266
|
+
if (!namesThisSample)
|
|
267
|
+
continue;
|
|
268
|
+
subject.reviewedSeq = record.seq;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return subjects;
|
|
273
|
+
}
|
|
274
|
+
/** Samples with no later review, oldest first. The human's audit backlog. */
|
|
275
|
+
export function openSamples(records) {
|
|
276
|
+
return sampledSubjects(records).filter((subject) => subject.reviewedSeq === null);
|
|
277
|
+
}
|
|
278
|
+
/** The candidates a sweep would sample now: eligible, selected, not yet sampled. */
|
|
279
|
+
export function pendingSamples(records, load, sampler) {
|
|
280
|
+
if (!sampler.enabled)
|
|
281
|
+
return [];
|
|
282
|
+
const alreadySampled = new Set();
|
|
283
|
+
for (const subject of sampledSubjects(records)) {
|
|
284
|
+
if (subject.subjectHash !== null)
|
|
285
|
+
alreadySampled.add(subject.subjectHash);
|
|
286
|
+
}
|
|
287
|
+
// APRV-183. The draw is per class: a class declaring its own `retro_rate` is
|
|
288
|
+
// compared against that rate, and one that declares none against
|
|
289
|
+
// `audit.supervised_sample_rate`. Same secret, same HMAC, same input; only the
|
|
290
|
+
// threshold moves, so there is still exactly one selection mechanism.
|
|
291
|
+
return supervisedExecutions(records, load).filter((candidate) => !alreadySampled.has(candidate.hash) &&
|
|
292
|
+
sampler.selectsFor(candidate.class, candidate.hash));
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Sample every supervised execution the log does not yet carry an
|
|
296
|
+
* `audit.sampled` for, and append one event per selection.
|
|
297
|
+
*
|
|
298
|
+
* Re-reads the verified log before every append so the head each
|
|
299
|
+
* compare-and-append is made against is the head the decision was made from. A
|
|
300
|
+
* `head-moved` refusal is collected and reported rather than retried: only the
|
|
301
|
+
* next sweep, which re-derives the whole question from the log as it now is,
|
|
302
|
+
* knows whether the candidate is still a candidate.
|
|
303
|
+
*
|
|
304
|
+
* Returns `ok` with an empty `appended` list when sampling is disabled; the
|
|
305
|
+
* reason travels on `sampler`. A disabled sampler is not a refusal, because
|
|
306
|
+
* nothing was asked for and nothing failed. See `core/sampler.ts` on why a
|
|
307
|
+
* missing secret disables sampling rather than escalating everything.
|
|
308
|
+
*/
|
|
309
|
+
export function sampleSupervised(logPath, cwd, options = {}) {
|
|
310
|
+
const load = policyFor(options, cwd);
|
|
311
|
+
const sampler = resolveSampler(load, options.env ?? process.env);
|
|
312
|
+
if (!sampler.enabled)
|
|
313
|
+
return { ok: true, sampler, appended: [], refusals: [] };
|
|
314
|
+
const appended = [];
|
|
315
|
+
const refusals = [];
|
|
316
|
+
const validate = options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir };
|
|
317
|
+
for (;;) {
|
|
318
|
+
const read = readVerifiedRecords(logPath, validate);
|
|
319
|
+
if (!read.ok) {
|
|
320
|
+
// A log that cannot be read is reported whole: partial progress is already
|
|
321
|
+
// in the log (each append is its own record) and nothing is rolled back.
|
|
322
|
+
return appended.length === 0
|
|
323
|
+
? refuse(read.code, read.message)
|
|
324
|
+
: { ok: true, sampler, appended, refusals: [...refusals, refuse(read.code, read.message)] };
|
|
325
|
+
}
|
|
326
|
+
const pending = pendingSamples(read.records, load, sampler);
|
|
327
|
+
const next = pending.find((candidate) => !appended.some((done) => done.candidate.hash === candidate.hash));
|
|
328
|
+
if (next === undefined)
|
|
329
|
+
break;
|
|
330
|
+
const result = appendSample(logPath, next, sampler, read.head, options);
|
|
331
|
+
if (!result.ok) {
|
|
332
|
+
refusals.push(result);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
appended.push({ record: result.record, candidate: next });
|
|
336
|
+
}
|
|
337
|
+
return { ok: true, sampler, appended, refusals };
|
|
338
|
+
}
|
|
339
|
+
function appendSample(logPath, candidate, sampler, head, options) {
|
|
340
|
+
const payload = {
|
|
341
|
+
// What was sampled, named by the chain's own identifiers so a reviewer (and
|
|
342
|
+
// a reproducing operator) can find the subject without a projection.
|
|
343
|
+
subject_seq: candidate.seq,
|
|
344
|
+
subject_hash: candidate.hash,
|
|
345
|
+
subject_event: "execution.started",
|
|
346
|
+
subject_ts: candidate.ts,
|
|
347
|
+
class: candidate.class,
|
|
348
|
+
// Why it was sampled. The selection VALUE is deliberately absent: it is
|
|
349
|
+
// derived from the operator's secret, an operator holding the secret can
|
|
350
|
+
// recompute it from subject_hash at will, and a value in the log is an
|
|
351
|
+
// oracle nobody needs.
|
|
352
|
+
reason: "supervised-sample",
|
|
353
|
+
selection: "hmac-sha256/event-hash",
|
|
354
|
+
// APRV-183: the rate this candidate was actually drawn at — the class's own
|
|
355
|
+
// `retro_rate` when it declared one, the global fallback otherwise. The
|
|
356
|
+
// record states the number the verdict was compared against, so a
|
|
357
|
+
// reproducing operator needs no second lookup and no guess about which key
|
|
358
|
+
// was in force.
|
|
359
|
+
rate: sampler.rateFor(candidate.class).rate ?? sampler.rate,
|
|
360
|
+
autonomy: "supervised",
|
|
361
|
+
};
|
|
362
|
+
const result = appendEvent(logPath, {
|
|
363
|
+
ts: tick(options),
|
|
364
|
+
event: "audit.sampled",
|
|
365
|
+
actor: AUDIT_ACTOR,
|
|
366
|
+
...(candidate.task === null ? {} : { task: candidate.task }),
|
|
367
|
+
action_key: candidate.actionKey,
|
|
368
|
+
payload,
|
|
369
|
+
}, {
|
|
370
|
+
...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
|
|
371
|
+
expectedHead: head,
|
|
372
|
+
});
|
|
373
|
+
if (!result.ok) {
|
|
374
|
+
return refuse("append-failed", `audit.sampled for ${candidate.actionKey} was not appended (${result.error.code}): ${result.error.message}`, { append: result.error });
|
|
375
|
+
}
|
|
376
|
+
return { ok: true, record: result.record };
|
|
377
|
+
}
|
|
378
|
+
/** Parse the CLI's one positional: a bare integer is a seq, anything else a key. */
|
|
379
|
+
export function parseSubjectRef(text) {
|
|
380
|
+
return /^[1-9][0-9]*$/u.test(text)
|
|
381
|
+
? { kind: "seq", seq: Number(text) }
|
|
382
|
+
: { kind: "action-key", actionKey: text };
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The graded reaction a review or a grant MAY carry (amended SPEC.md §5.2,
|
|
386
|
+
* APRV-237/APRV-239), in the order the schema's enum lists them: worst to best.
|
|
387
|
+
*
|
|
388
|
+
* **This is not enforcement, and it lives here rather than in an enforcement
|
|
389
|
+
* module for that reason.** `verdict` is the field the runtime acts on;
|
|
390
|
+
* `reaction` is what the human thought, travelling human-to-agent, and SPEC.md
|
|
391
|
+
* §11.1 invariant 10 says no routing, class matching, sampling, budget, token,
|
|
392
|
+
* gate-window or execution decision may read it. `tests/values-inert.test.ts`
|
|
393
|
+
* enforces that as a static guard over the enforcement modules, which is why the
|
|
394
|
+
* tuple is exported from `core/audit.ts` (the projection's home) and imported by
|
|
395
|
+
* the surfaces that show it, and by nothing that decides.
|
|
396
|
+
*
|
|
397
|
+
* The vocabulary is closed on purpose. Four words are a grade a person can give
|
|
398
|
+
* in one tap and an agent can read back without interpretation; an open field
|
|
399
|
+
* would accumulate synonyms across surfaces until "meh" and "indifferent" were
|
|
400
|
+
* two different signals. The absence of the field is absence: it is never read
|
|
401
|
+
* as `indifferent`, which is a thing a person had to actually say.
|
|
402
|
+
*/
|
|
403
|
+
export const REACTIONS = ["disliked", "indifferent", "liked", "loved"];
|
|
404
|
+
/** Whether a string is one of the four graded reactions. Used at the CLI boundary. */
|
|
405
|
+
export function isReaction(value) {
|
|
406
|
+
return REACTIONS.includes(value);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The two grades that require the human's own words (amended SPEC.md §5.2).
|
|
410
|
+
*
|
|
411
|
+
* `loved` and `disliked` are the reactions an agent is most likely to act on and
|
|
412
|
+
* least able to interpret alone: "disliked" with no words says something went
|
|
413
|
+
* wrong and nothing about what, which is the shape of a signal that gets guessed
|
|
414
|
+
* at. `liked` and `indifferent` are the ordinary readings and demand nothing,
|
|
415
|
+
* because a one-tap signal that opens a form is a signal that gets switched off.
|
|
416
|
+
*/
|
|
417
|
+
const REACTIONS_REQUIRING_NOTE = new Set(["disliked", "loved"]);
|
|
418
|
+
/** A note that is present and is not only whitespace. Blank is not a note. */
|
|
419
|
+
function hasNote(note) {
|
|
420
|
+
return note !== null && note !== undefined && note.trim().length > 0;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The obligation a denial creates, chosen by the action's DECLARED
|
|
424
|
+
* reversibility (amended SPEC.md §5.2/§7, APRV-127).
|
|
425
|
+
*
|
|
426
|
+
* - `reversible: true` → `gated-revert`. The action can be undone, so the
|
|
427
|
+
* obligation is to undo it *through the gate*: the revert is itself a
|
|
428
|
+
* side-effecting action, and routing it through the gate is what closes the
|
|
429
|
+
* loop inside the log rather than inside a promise.
|
|
430
|
+
* - `reversible: false` → `policy-finding`. There is nothing to revert. What is
|
|
431
|
+
* left is the finding that the class should not have permitted this without a
|
|
432
|
+
* human, and the sanctioned response is tightening the class; the obligation
|
|
433
|
+
* is the review of that tightening.
|
|
434
|
+
* - **declared nothing** → `policy-finding`, the same as `false`. This is the
|
|
435
|
+
* fail-closed direction, and the reason is worth stating: obliging a revert of
|
|
436
|
+
* an action nobody said could be reverted would record an obligation that may
|
|
437
|
+
* be impossible to discharge, and an impossible obligation is one that gets
|
|
438
|
+
* closed dishonestly. A policy finding is always dischargeable, and it is the
|
|
439
|
+
* heavier of the two: it puts the CLASS on the table rather than one action.
|
|
440
|
+
*
|
|
441
|
+
* ## Self-reported, and only ever in the safe direction
|
|
442
|
+
*
|
|
443
|
+
* `reversible` is written by the party whose action is under review, so global
|
|
444
|
+
* invariant 4 applies: it may never reduce scrutiny. Here it does not. It
|
|
445
|
+
* selects the SHAPE of an obligation that exists either way; it cannot remove
|
|
446
|
+
* one, delay one, or decide whether the denial happened. The one thing a false
|
|
447
|
+
* `reversible: true` buys is a revert obligation instead of a policy finding —
|
|
448
|
+
* and the revert obligation is the one whose satisfaction this runtime checks
|
|
449
|
+
* against the log (`revert-required`), so the lie makes the claimant's own exit
|
|
450
|
+
* harder rather than easier.
|
|
451
|
+
*/
|
|
452
|
+
export function obligationFor(reversible) {
|
|
453
|
+
return reversible === true ? "gated-revert" : "policy-finding";
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Append `audit.reviewed` for one open sample.
|
|
457
|
+
*
|
|
458
|
+
* HUMAN-ONLY, by the same rule as `grant`/`reject`/`revoke`: the whole content
|
|
459
|
+
* of the event is that a person looked. An agent- or system-authored review
|
|
460
|
+
* would be the party under oversight closing its own audit item, and a backlog
|
|
461
|
+
* that can be emptied by the thing it supervises measures nothing.
|
|
462
|
+
*
|
|
463
|
+
* No attestation is required, for the reason `execution resolve` states: review
|
|
464
|
+
* records an observation and exercises no policy authority. It authorizes
|
|
465
|
+
* nothing, spends no budget, and mints no token.
|
|
466
|
+
*
|
|
467
|
+
* `--note` is optional and recorded verbatim when present. It is not mandatory
|
|
468
|
+
* the way `execution resolve`'s is, because that verb writes an *outcome* the
|
|
469
|
+
* runtime does not know while this one writes only "seen".
|
|
470
|
+
*/
|
|
471
|
+
export function reviewSample(logPath, ref, actor, note, options = {}) {
|
|
472
|
+
const verdict = options.verdict ?? "ok";
|
|
473
|
+
if (!HUMAN_ACTOR.test(actor)) {
|
|
474
|
+
return refuse("actor-not-human", `audit review is human-only: the event's entire content is that a person looked at a sampled action, and a runtime that could mark its own samples reviewed would be a supervision backlog that empties itself. The actor must match human:<id>, got ${JSON.stringify(actor)}.`);
|
|
475
|
+
}
|
|
476
|
+
// APRV-239, and deliberately here: after the actor check and BEFORE the log is
|
|
477
|
+
// read. Both rules are properties of the two arguments in front of this
|
|
478
|
+
// function, so neither needs a log to decide, and a refusal that had already
|
|
479
|
+
// read (and verified) a log would report a log failure for an invocation that
|
|
480
|
+
// was malformed before it ever touched one. Nothing is appended on either
|
|
481
|
+
// path; the reviewer fixes the invocation and reviews again.
|
|
482
|
+
const reaction = options.reaction;
|
|
483
|
+
if (reaction !== undefined) {
|
|
484
|
+
if (verdict === "denied" && (reaction === "liked" || reaction === "loved")) {
|
|
485
|
+
return refuse("reaction-conflicts-verdict", `a denied review cannot also be ${reaction}: \`verdict\` says the action should not have happened and \`reaction\` says the human was pleased by it, and only the first of those is enforcement. A record carrying both reads afterwards as evidence of whichever half suits the reader, and reads to an agent as a denial being survivable when the operator is pleased. Nothing was appended. Say which one you meant: drop --deny, or use --reaction disliked or indifferent and put the nuance in --note.`);
|
|
486
|
+
}
|
|
487
|
+
if (REACTIONS_REQUIRING_NOTE.has(reaction) && !hasNote(note)) {
|
|
488
|
+
return refuse("note-required", `--reaction ${reaction} requires --note "<text>": it is the grade an agent is most likely to act on and least able to interpret alone, and "${reaction}" with no words says something happened and nothing about what. Blank is not a note. \`liked\` and \`indifferent\` need none. Nothing was appended.`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const read = readVerifiedRecords(logPath, options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir });
|
|
492
|
+
if (!read.ok)
|
|
493
|
+
return refuse(read.code, read.message);
|
|
494
|
+
const subjects = sampledSubjects(read.records);
|
|
495
|
+
const located = locate(subjects, ref);
|
|
496
|
+
if (!located.ok)
|
|
497
|
+
return located;
|
|
498
|
+
const subject = located.subject;
|
|
499
|
+
const payload = {
|
|
500
|
+
subject_seq: subject.seq,
|
|
501
|
+
subject_event: "audit.sampled",
|
|
502
|
+
reviewed: true,
|
|
503
|
+
verdict,
|
|
504
|
+
};
|
|
505
|
+
if (subject.subjectHash !== null)
|
|
506
|
+
payload["sampled_subject_hash"] = subject.subjectHash;
|
|
507
|
+
if (note !== null && note.trim().length > 0)
|
|
508
|
+
payload["note"] = note;
|
|
509
|
+
// Written only when it was given. An omitted reaction leaves no key, which is
|
|
510
|
+
// the difference between "the human said nothing" and "the human said
|
|
511
|
+
// indifferent" — a distinction the read surfaces depend on.
|
|
512
|
+
if (reaction !== undefined)
|
|
513
|
+
payload["reaction"] = reaction;
|
|
514
|
+
const result = appendEvent(logPath, {
|
|
515
|
+
ts: tick(options),
|
|
516
|
+
event: "audit.reviewed",
|
|
517
|
+
actor,
|
|
518
|
+
...(subject.task === null ? {} : { task: subject.task }),
|
|
519
|
+
...(subject.actionKey === null ? {} : { action_key: subject.actionKey }),
|
|
520
|
+
payload,
|
|
521
|
+
}, {
|
|
522
|
+
...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
|
|
523
|
+
expectedHead: read.head,
|
|
524
|
+
});
|
|
525
|
+
if (!result.ok) {
|
|
526
|
+
return refuse("append-failed", `audit.reviewed for the sample at seq ${String(subject.seq)} was not appended (${result.error.code}): ${result.error.message}`, { append: result.error });
|
|
527
|
+
}
|
|
528
|
+
if (verdict !== "denied") {
|
|
529
|
+
return { ok: true, record: result.record, subject, obligation: null };
|
|
530
|
+
}
|
|
531
|
+
// APRV-127. The denial is recorded; now record what it obliges. Two events,
|
|
532
|
+
// not one, because they are two facts with two authors: a human concluded the
|
|
533
|
+
// action should not have happened, and the runtime derived — mechanically,
|
|
534
|
+
// from the declaration the log already holds — what must now be done about it.
|
|
535
|
+
// Collapsing them would let the reviewer's own words decide the obligation.
|
|
536
|
+
//
|
|
537
|
+
// Compare-and-append against the review itself: the obligation must land
|
|
538
|
+
// directly on the record it names, so nothing can slip between a denial and
|
|
539
|
+
// the obligation it creates.
|
|
540
|
+
const obliged = appendObligation(logPath, read.records, subject, result.record, options);
|
|
541
|
+
if (!obliged.ok)
|
|
542
|
+
return obliged;
|
|
543
|
+
return { ok: true, record: result.record, subject, obligation: obliged.record };
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Append the `reconciliation.required` that a denial creates.
|
|
547
|
+
*
|
|
548
|
+
* Actor `system:audit`, not the reviewer. The obligation is a derivation, not an
|
|
549
|
+
* opinion: it follows from the denial and the action's declared reversibility by
|
|
550
|
+
* the rule {@link obligationFor} states, and the event schema refuses any actor
|
|
551
|
+
* that is not `system:`. A human-authored obligation would be one a human could
|
|
552
|
+
* word into something easier to discharge, and the party under oversight would
|
|
553
|
+
* then be describing its own homework.
|
|
554
|
+
*/
|
|
555
|
+
function appendObligation(logPath, records, subject, review, options) {
|
|
556
|
+
const actionKey = subject.actionKey;
|
|
557
|
+
if (actionKey === null) {
|
|
558
|
+
return refuse("not-sampled", `the sample at seq ${String(subject.seq)} names no action key, so a denial of it can oblige nothing: a reconciliation names the action it concerns, and there is none to name. The denial itself was recorded at seq ${String(review.seq)}.`, { seq: subject.seq });
|
|
559
|
+
}
|
|
560
|
+
// The class and the reversibility come from the REGISTRATION, never from the
|
|
561
|
+
// review and never from the execution's own payload (global invariant 4). The
|
|
562
|
+
// party whose action was denied does not get to describe the action.
|
|
563
|
+
const declared = findDeclaration(records, actionKey);
|
|
564
|
+
const cls = declared?.class ?? null;
|
|
565
|
+
if (cls === null || cls.length === 0) {
|
|
566
|
+
return refuse("not-obliged", `action ${actionKey} has no task.registered declaration carrying a class, so the obligation its denial creates cannot name one. A policy finding tightens a CLASS; an obligation that names none is one nobody can act on. The denial itself was recorded at seq ${String(review.seq)}.`, { seq: review.seq });
|
|
567
|
+
}
|
|
568
|
+
const reversible = declared?.reversible ?? null;
|
|
569
|
+
const result = appendEvent(logPath, {
|
|
570
|
+
ts: tick(options),
|
|
571
|
+
event: "reconciliation.required",
|
|
572
|
+
actor: AUDIT_ACTOR,
|
|
573
|
+
...(subject.task === null ? {} : { task: subject.task }),
|
|
574
|
+
action_key: actionKey,
|
|
575
|
+
payload: {
|
|
576
|
+
action_key: actionKey,
|
|
577
|
+
class: cls,
|
|
578
|
+
review_seq: review.seq,
|
|
579
|
+
obligation: obligationFor(reversible),
|
|
580
|
+
reversible,
|
|
581
|
+
// Restated so a reader of this record alone knows what the runtime could
|
|
582
|
+
// and could not do about it. The gate cannot undo anything; it obliges.
|
|
583
|
+
reason: "retrospective-denial",
|
|
584
|
+
},
|
|
585
|
+
}, {
|
|
586
|
+
...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
|
|
587
|
+
expectedHead: { seq: review.seq, hash: review.hash },
|
|
588
|
+
});
|
|
589
|
+
if (!result.ok) {
|
|
590
|
+
return refuse("obligation-not-appended", `the denial of ${actionKey} was recorded at seq ${String(review.seq)} and its reconciliation obligation was NOT appended (${result.error.code}): ${result.error.message}. The log is consistent — the review stands and says denied — but nothing yet records what the denial requires. Review the sample again once the head settles, or open the obligation by hand through a human-authored process; an unreconciled denial is exactly what \`approval status\` and \`approval doctor\` are meant to shout about.`, { seq: review.seq, append: result.error });
|
|
591
|
+
}
|
|
592
|
+
return { ok: true, record: result.record };
|
|
593
|
+
}
|
|
594
|
+
function obligationOf(record) {
|
|
595
|
+
const payload = payloadOf(record);
|
|
596
|
+
const actionKey = stringOrNull(record.action_key) ?? stringOrNull(payload["action_key"]);
|
|
597
|
+
const cls = stringOrNull(payload["class"]);
|
|
598
|
+
const reviewSeq = payload["review_seq"];
|
|
599
|
+
const shape = payload["obligation"];
|
|
600
|
+
if (actionKey === null || cls === null)
|
|
601
|
+
return null;
|
|
602
|
+
if (typeof reviewSeq !== "number" || !Number.isInteger(reviewSeq))
|
|
603
|
+
return null;
|
|
604
|
+
if (shape !== "gated-revert" && shape !== "policy-finding")
|
|
605
|
+
return null;
|
|
606
|
+
const reversible = payload["reversible"];
|
|
607
|
+
return {
|
|
608
|
+
seq: record.seq,
|
|
609
|
+
ts: record.ts,
|
|
610
|
+
actionKey,
|
|
611
|
+
task: stringOrNull(record.task) ?? stringOrNull(payload["task"]),
|
|
612
|
+
class: cls,
|
|
613
|
+
reviewSeq,
|
|
614
|
+
obligation: shape,
|
|
615
|
+
reversible: typeof reversible === "boolean" ? reversible : null,
|
|
616
|
+
satisfiedSeq: null,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Every reconciliation obligation the log carries, each tagged with the
|
|
621
|
+
* satisfaction that closes it.
|
|
622
|
+
*
|
|
623
|
+
* A satisfaction closes an obligation only when it comes **after** it in the
|
|
624
|
+
* chain and names its seq — the same "later, and names it" rule
|
|
625
|
+
* {@link sampledSubjects} applies to reviews, and for the same reason: a
|
|
626
|
+
* backlog that an earlier record could close is a backlog that empties itself.
|
|
627
|
+
*
|
|
628
|
+
* A malformed `reconciliation.required` (no action key, no class, no usable
|
|
629
|
+
* obligation shape) is SKIPPED rather than guessed at. Such a record cannot
|
|
630
|
+
* reach the log through this runtime — the event schema requires all three — so
|
|
631
|
+
* one that is there arrived some other way, and inventing the missing field
|
|
632
|
+
* would put a fact in the backlog that nobody wrote.
|
|
633
|
+
*/
|
|
634
|
+
export function reconciliationObligations(records) {
|
|
635
|
+
const found = [];
|
|
636
|
+
for (const record of records) {
|
|
637
|
+
if (record.event !== "reconciliation.required")
|
|
638
|
+
continue;
|
|
639
|
+
const parsed = obligationOf(record);
|
|
640
|
+
if (parsed !== null)
|
|
641
|
+
found.push(parsed);
|
|
642
|
+
}
|
|
643
|
+
for (const item of found) {
|
|
644
|
+
for (const record of records) {
|
|
645
|
+
if (record.event !== "reconciliation.satisfied" || record.seq <= item.seq)
|
|
646
|
+
continue;
|
|
647
|
+
if (payloadOf(record)["obligation_seq"] !== item.seq)
|
|
648
|
+
continue;
|
|
649
|
+
item.satisfiedSeq = record.seq;
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return found;
|
|
654
|
+
}
|
|
655
|
+
/** Obligations with no later satisfaction, oldest first. The loud backlog. */
|
|
656
|
+
export function openObligations(records) {
|
|
657
|
+
return reconciliationObligations(records).filter((item) => item.satisfiedSeq === null);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Close one reconciliation obligation.
|
|
661
|
+
*
|
|
662
|
+
* **HUMAN-ONLY**, by the same rule that governs `grant`, `reject`, `revoke` and
|
|
663
|
+
* `audit.reviewed`, and enforced twice: here in code and again by the event
|
|
664
|
+
* schema. The entire content of the record is that a person judged the
|
|
665
|
+
* obligation discharged. A runtime that could satisfy its own obligations would
|
|
666
|
+
* be a reconciliation backlog that empties itself, which is precisely the
|
|
667
|
+
* silence an unreconciled denial exists to break.
|
|
668
|
+
*
|
|
669
|
+
* Two checks beyond the actor, and both are about evidence rather than trust:
|
|
670
|
+
*
|
|
671
|
+
* - **A note is required.** `audit.reviewed` may record only "seen"; this record
|
|
672
|
+
* asserts that something was DONE, and an assertion nobody described is one no
|
|
673
|
+
* auditor can check.
|
|
674
|
+
* - **A `gated-revert` obligation requires a completed revert IN THIS LOG.** The
|
|
675
|
+
* obligation was "undo it through the gate", so the discharge is a gated
|
|
676
|
+
* action that ran, and the runtime looks for its `execution.completed` rather
|
|
677
|
+
* than accepting a sentence saying it happened. That is what closes the loop
|
|
678
|
+
* in the chain. A `policy-finding` obligation has no such artifact — the
|
|
679
|
+
* sanctioned response is a policy amendment, which is a separate human
|
|
680
|
+
* ceremony with its own `policy.updated` record — so the note is the discharge
|
|
681
|
+
* there, and the note is required.
|
|
682
|
+
*
|
|
683
|
+
* No attestation is required, for the reason `audit review` and `execution
|
|
684
|
+
* resolve` state: this record exercises no policy authority, authorizes nothing,
|
|
685
|
+
* spends no budget, and mints no token.
|
|
686
|
+
*/
|
|
687
|
+
export function satisfyObligation(logPath, obligationSeq, actor, input, options = {}) {
|
|
688
|
+
if (!HUMAN_ACTOR.test(actor)) {
|
|
689
|
+
return refuse("actor-not-human", `satisfying a reconciliation obligation is human-only: the event's entire content is that a PERSON judged the obligation discharged, and a runtime that could close its own obligations would be a reconciliation backlog that empties itself. The actor must match human:<id>, got ${JSON.stringify(actor)}.`);
|
|
690
|
+
}
|
|
691
|
+
const note = input.note.trim();
|
|
692
|
+
if (note.length === 0) {
|
|
693
|
+
return refuse("note-required", `a reconciliation.satisfied must say what was done. Unlike \`audit review\`, whose whole content may be "a person looked", this record asserts that an obligation was DISCHARGED, and a discharge nobody described is one no auditor can check. Pass --note "<what you did>".`);
|
|
694
|
+
}
|
|
695
|
+
const read = readVerifiedRecords(logPath, options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir });
|
|
696
|
+
if (!read.ok)
|
|
697
|
+
return refuse(read.code, read.message);
|
|
698
|
+
const obligation = reconciliationObligations(read.records).find((item) => item.seq === obligationSeq);
|
|
699
|
+
if (obligation === undefined) {
|
|
700
|
+
return refuse("not-obliged", `no reconciliation.required record at seq ${String(obligationSeq)}. \`approval audit reconcile\` names the OBLIGATION, not the action it concerns and not the review that created it. Run \`approval audit obligations\` for the open ones.`, { seq: obligationSeq });
|
|
701
|
+
}
|
|
702
|
+
if (obligation.satisfiedSeq !== null) {
|
|
703
|
+
return refuse("already-satisfied", `the obligation at seq ${String(obligation.seq)} was already satisfied at seq ${String(obligation.satisfiedSeq)}; a second satisfaction would record a second discharge of one obligation, which the log cannot tell apart from the first`, { seq: obligation.satisfiedSeq });
|
|
704
|
+
}
|
|
705
|
+
const revertKey = input.revertActionKey?.trim() ?? "";
|
|
706
|
+
if (obligation.obligation === "gated-revert") {
|
|
707
|
+
if (revertKey.length === 0) {
|
|
708
|
+
return refuse("revert-required", `the obligation at seq ${String(obligation.seq)} is a gated-revert: ${obligation.actionKey} was declared reversible, so the sanctioned response to its denial is to UNDO it through the gate. Name the revert with --revert <action-key>. The revert is itself a side-effecting action, and routing it through the gate is what closes this loop inside the log rather than inside a sentence.`, { seq: obligation.seq });
|
|
709
|
+
}
|
|
710
|
+
const completed = read.records.some((record) => record.event === "execution.completed" && record.action_key === revertKey);
|
|
711
|
+
if (!completed) {
|
|
712
|
+
return refuse("revert-required", `this log carries no execution.completed for ${JSON.stringify(revertKey)}, so the revert this obligation requires has not been shown to have run. Request the revert, have it granted, run it through \`approval run\`, and then satisfy the obligation naming it. The runtime checks the chain rather than the claim, because a discharge that could be asserted is a backlog that empties itself.`, { seq: obligation.seq });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const payload = {
|
|
716
|
+
obligation_seq: obligation.seq,
|
|
717
|
+
note,
|
|
718
|
+
action_key: obligation.actionKey,
|
|
719
|
+
class: obligation.class,
|
|
720
|
+
obligation: obligation.obligation,
|
|
721
|
+
};
|
|
722
|
+
if (obligation.obligation === "gated-revert")
|
|
723
|
+
payload["revert_action_key"] = revertKey;
|
|
724
|
+
const result = appendEvent(logPath, {
|
|
725
|
+
ts: tick(options),
|
|
726
|
+
event: "reconciliation.satisfied",
|
|
727
|
+
actor,
|
|
728
|
+
...(obligation.task === null ? {} : { task: obligation.task }),
|
|
729
|
+
action_key: obligation.actionKey,
|
|
730
|
+
payload,
|
|
731
|
+
}, {
|
|
732
|
+
...(options.schemaDir === undefined ? {} : { schemaDir: options.schemaDir }),
|
|
733
|
+
expectedHead: read.head,
|
|
734
|
+
});
|
|
735
|
+
if (!result.ok) {
|
|
736
|
+
return refuse("append-failed", `reconciliation.satisfied for the obligation at seq ${String(obligation.seq)} was not appended (${result.error.code}): ${result.error.message}`, { append: result.error });
|
|
737
|
+
}
|
|
738
|
+
return { ok: true, record: result.record, obligation };
|
|
739
|
+
}
|
|
740
|
+
function locate(subjects, ref) {
|
|
741
|
+
if (ref.kind === "seq") {
|
|
742
|
+
const subject = subjects.find((entry) => entry.seq === ref.seq);
|
|
743
|
+
if (subject === undefined) {
|
|
744
|
+
return refuse("not-sampled", `no audit.sampled record at seq ${String(ref.seq)}; \`approval audit review\` names the SAMPLE, not the execution it sampled. Run \`approval audit list\` (or read .approval/QUEUE.md's sampled-audit backlog) for the open samples.`, { seq: ref.seq });
|
|
745
|
+
}
|
|
746
|
+
if (subject.reviewedSeq !== null) {
|
|
747
|
+
return refuse("already-reviewed", `the sample at seq ${String(subject.seq)} was already reviewed at seq ${String(subject.reviewedSeq)}; a second review would record a second human observation of the same item, which the log cannot tell apart from the first`, { seq: subject.reviewedSeq });
|
|
748
|
+
}
|
|
749
|
+
return { ok: true, subject };
|
|
750
|
+
}
|
|
751
|
+
const matching = subjects.filter((entry) => entry.actionKey === ref.actionKey);
|
|
752
|
+
if (matching.length === 0) {
|
|
753
|
+
return refuse("not-sampled", `no audit.sampled record names action ${JSON.stringify(ref.actionKey)}; only a SAMPLED action can be reviewed, and sampling is the runtime's decision, never a caller's`);
|
|
754
|
+
}
|
|
755
|
+
const open = matching.filter((entry) => entry.reviewedSeq === null);
|
|
756
|
+
if (open.length === 0) {
|
|
757
|
+
const last = matching[matching.length - 1];
|
|
758
|
+
return refuse("already-reviewed", `every audit.sampled for ${ref.actionKey} is reviewed (the latest, seq ${String(last?.seq ?? 0)}, at seq ${String(last?.reviewedSeq ?? 0)})`, last?.reviewedSeq === undefined || last.reviewedSeq === null ? {} : { seq: last.reviewedSeq });
|
|
759
|
+
}
|
|
760
|
+
if (open.length > 1) {
|
|
761
|
+
return refuse("ambiguous-subject", `action ${ref.actionKey} has ${String(open.length)} unreviewed samples (seq ${open
|
|
762
|
+
.map((entry) => String(entry.seq))
|
|
763
|
+
.join(", ")}); name the one you reviewed by its seq, because a review that could mean either would close the wrong item`);
|
|
764
|
+
}
|
|
765
|
+
return { ok: true, subject: open[0] };
|
|
766
|
+
}
|
|
767
|
+
/** The actor that appended a record, when it is a non-empty string. */
|
|
768
|
+
function actorOf(record) {
|
|
769
|
+
return typeof record.actor === "string" && record.actor.length > 0 ? record.actor : null;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Per action key, the actor of the `task.registered` that declared it, and
|
|
773
|
+
* failing that the actor of the `execution.started` that ran it.
|
|
774
|
+
*
|
|
775
|
+
* Registration first because it is the earliest and most specific statement of
|
|
776
|
+
* whose work this is: the party that put the action in a task envelope. The
|
|
777
|
+
* execution actor is the fallback for a log whose registration is missing (an
|
|
778
|
+
* imported or truncated log), and a key with neither is reported as `null`
|
|
779
|
+
* rather than guessed.
|
|
780
|
+
*/
|
|
781
|
+
function agentActorsByKey(records) {
|
|
782
|
+
const registered = new Map();
|
|
783
|
+
const executed = new Map();
|
|
784
|
+
for (const record of records) {
|
|
785
|
+
if (record.event === "task.registered") {
|
|
786
|
+
const actor = actorOf(record);
|
|
787
|
+
if (actor === null)
|
|
788
|
+
continue;
|
|
789
|
+
const actions = payloadOf(record)["actions"];
|
|
790
|
+
if (!Array.isArray(actions))
|
|
791
|
+
continue;
|
|
792
|
+
for (const entry of actions) {
|
|
793
|
+
if (typeof entry !== "object" || entry === null)
|
|
794
|
+
continue;
|
|
795
|
+
const key = entry["idempotency_key"];
|
|
796
|
+
if (typeof key === "string" && key.length > 0)
|
|
797
|
+
registered.set(key, actor);
|
|
798
|
+
}
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (record.event !== "execution.started")
|
|
802
|
+
continue;
|
|
803
|
+
const key = record.action_key;
|
|
804
|
+
const actor = actorOf(record);
|
|
805
|
+
if (typeof key === "string" && key.length > 0 && actor !== null && !executed.has(key)) {
|
|
806
|
+
executed.set(key, actor);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
for (const [key, actor] of executed) {
|
|
810
|
+
if (!registered.has(key))
|
|
811
|
+
registered.set(key, actor);
|
|
812
|
+
}
|
|
813
|
+
return registered;
|
|
814
|
+
}
|
|
815
|
+
function reactionOf(payload) {
|
|
816
|
+
const value = payload["reaction"];
|
|
817
|
+
return typeof value === "string" && isReaction(value) ? value : null;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Every reaction and every note a human wrote about an action, oldest first.
|
|
821
|
+
*
|
|
822
|
+
* The HUMAN-TO-AGENT direction of the log (amended SPEC.md §5.2). Two sources,
|
|
823
|
+
* because a human says what they thought in two places: at the gate, answering a
|
|
824
|
+
* request (`approval.granted`), and afterwards, reviewing a sampled action
|
|
825
|
+
* (`audit.reviewed`). Rejections and revocations carry no reaction at all, so
|
|
826
|
+
* they are not a source: their reason IS their note, and the record already says
|
|
827
|
+
* what happened.
|
|
828
|
+
*
|
|
829
|
+
* **An entry with neither a reaction nor a note is omitted.** A grant with no
|
|
830
|
+
* words is the ordinary case, most grants are, and listing thousands of them as
|
|
831
|
+
* blank rows would bury the handful where somebody actually said something.
|
|
832
|
+
* Absence of feedback is not feedback.
|
|
833
|
+
*
|
|
834
|
+
* Reads only the records it is given, and callers pass VERIFIED records: this is
|
|
835
|
+
* a projection in the sense the rest of this module uses the word, it writes
|
|
836
|
+
* nothing, decides nothing, and no enforcement path reads it (SPEC.md §11.1
|
|
837
|
+
* invariant 10).
|
|
838
|
+
*/
|
|
839
|
+
export function humanFeedback(records) {
|
|
840
|
+
const index = indexDeclarations(records);
|
|
841
|
+
const agents = agentActorsByKey(records);
|
|
842
|
+
const subjectBySeq = new Map();
|
|
843
|
+
for (const subject of sampledSubjects(records)) {
|
|
844
|
+
if (subject.reviewedSeq !== null)
|
|
845
|
+
subjectBySeq.set(subject.reviewedSeq, subject);
|
|
846
|
+
}
|
|
847
|
+
const entries = [];
|
|
848
|
+
for (const record of records) {
|
|
849
|
+
const isReview = record.event === "audit.reviewed";
|
|
850
|
+
if (!isReview && record.event !== "approval.granted")
|
|
851
|
+
continue;
|
|
852
|
+
const payload = payloadOf(record);
|
|
853
|
+
const reaction = reactionOf(payload);
|
|
854
|
+
const note = stringOrNull(payload["note"]);
|
|
855
|
+
if (reaction === null && note === null)
|
|
856
|
+
continue;
|
|
857
|
+
const subject = isReview ? (subjectBySeq.get(record.seq) ?? null) : null;
|
|
858
|
+
const actionKey = stringOrNull(record.action_key) ??
|
|
859
|
+
stringOrNull(payload["action_key"]) ??
|
|
860
|
+
subject?.actionKey ??
|
|
861
|
+
null;
|
|
862
|
+
const declared = actionKey === null ? undefined : index.declarations.get(actionKey);
|
|
863
|
+
const rawVerdict = payload["verdict"];
|
|
864
|
+
entries.push({
|
|
865
|
+
seq: record.seq,
|
|
866
|
+
ts: record.ts,
|
|
867
|
+
source: isReview ? "review" : "decision",
|
|
868
|
+
event: record.event,
|
|
869
|
+
actor: actorOf(record) ?? "-",
|
|
870
|
+
reaction,
|
|
871
|
+
note,
|
|
872
|
+
verdict: isReview && (rawVerdict === "ok" || rawVerdict === "denied") ? rawVerdict : null,
|
|
873
|
+
actionKey,
|
|
874
|
+
task: stringOrNull(record.task) ?? subject?.task ?? declared?.task ?? null,
|
|
875
|
+
class: declared?.class ?? null,
|
|
876
|
+
agentActor: actionKey === null ? null : (agents.get(actionKey) ?? null),
|
|
877
|
+
sampleSeq: subject?.seq ?? null,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return entries;
|
|
881
|
+
}
|
|
882
|
+
//# sourceMappingURL=audit.js.map
|