approval-md 0.1.0 → 0.2.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/README.md +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- package/templates/codex/README.md +9 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WYSIWYS: the canonical rendering of a payload (APRV-119), and the structural
|
|
3
|
+
* views it is built from (APRV-100, APRV-124, APRV-126, APRV-144).
|
|
4
|
+
*
|
|
5
|
+
* ## What you see is what you sign
|
|
6
|
+
*
|
|
7
|
+
* The prompt a human approves is a deterministic function of the payload bytes
|
|
8
|
+
* and the action class, and of nothing else. {@link canonicalRender} is that
|
|
9
|
+
* function. Two channels, two runtimes, or two versions of this one cannot show
|
|
10
|
+
* two humans two different readings of the same payload without the difference
|
|
11
|
+
* being detectable: the rendering carries its own `display_hash`, the gate
|
|
12
|
+
* records that hash on `approval.requested`, and a rendering that disagrees is a
|
|
13
|
+
* rendering whose hash disagrees.
|
|
14
|
+
*
|
|
15
|
+
* The threat this closes is signoff social engineering. An approval surface that
|
|
16
|
+
* renders benign text while the hashed payload is malicious leaves the human
|
|
17
|
+
* signing blind. `payload_hash` binds the bytes; `display_hash` binds the
|
|
18
|
+
* reading of them.
|
|
19
|
+
*
|
|
20
|
+
* Four properties, and they are the whole module:
|
|
21
|
+
*
|
|
22
|
+
* 1. **Pure.** No clock, no locale, no environment, no randomness, no IO. The
|
|
23
|
+
* only inputs are the payload value and the class string, and
|
|
24
|
+
* `tests/wysiwys.test.ts` reads this file's own source and fails on a
|
|
25
|
+
* reference to any of them.
|
|
26
|
+
* 2. **Closed field set per kind.** A payload is recognised as one of four kinds
|
|
27
|
+
* (`command`, `file-change`, `email`, `opaque`) by its STRUCTURE, and each
|
|
28
|
+
* kind renders a fixed list of fields. A shape carrying one key its kind does
|
|
29
|
+
* not render is not that kind: it falls through to `opaque`, where the
|
|
30
|
+
* canonical JSON is shown whole. Nothing is hidden by being unrecognised.
|
|
31
|
+
* 3. **Absent renders explicitly.** A field the payload does not carry is
|
|
32
|
+
* printed as {@link ABSENT}, never omitted. An omitted line and a line whose
|
|
33
|
+
* value happens to be empty are different facts, and a reader who cannot tell
|
|
34
|
+
* them apart is reading a rendering that lost information.
|
|
35
|
+
* 4. **Claimed material stays outside.** Everything inside the canonical block
|
|
36
|
+
* is derived from the bound bytes. Summaries, cost estimates, rationale,
|
|
37
|
+
* confidence, and model-written glosses are rendered OUTSIDE it, under the
|
|
38
|
+
* channel's own claimed heading (SPEC.md §9).
|
|
39
|
+
*
|
|
40
|
+
* ## Why this lives in `src/core/`, not `src/channels/`
|
|
41
|
+
*
|
|
42
|
+
* It is deterministic core in the sense CLAUDE.md means: pure, exhaustively
|
|
43
|
+
* tested, and consulted by the gate. `core/gate.ts` computes `display_hash` at
|
|
44
|
+
* the write boundary from the same function every channel renders with, so the
|
|
45
|
+
* log states what rendering the approver was shown. A renderer under
|
|
46
|
+
* `src/channels/` would have to be imported BY core to do that, inverting the
|
|
47
|
+
* direction the codebase is built on. `channels/payload-view.ts` is the
|
|
48
|
+
* channel-side facade over this module and holds the one function that needs a
|
|
49
|
+
* channel type.
|
|
50
|
+
*
|
|
51
|
+
* ## The reading aids this absorbed (APRV-100, APRV-124, APRV-126)
|
|
52
|
+
*
|
|
53
|
+
* SPEC.md §10.4 requires a channel to present, for a `manual` action, "the full
|
|
54
|
+
* payload or a faithful rendering of it". Until now every channel used the one
|
|
55
|
+
* rendering `channels/tagging.ts` builds: pretty-printed JSON. That is faithful
|
|
56
|
+
* and it is exact, and for an email it is close to unreadable — the observed
|
|
57
|
+
* failure (2026-08-18, examples/email-demo.md) is a body arriving on a phone as
|
|
58
|
+
* a single line carrying literal `\n` sequences, which is precisely the text a
|
|
59
|
+
* human is being asked to take responsibility for.
|
|
60
|
+
*
|
|
61
|
+
* So this module adds a second rendering *on top of* the first, never instead
|
|
62
|
+
* of it:
|
|
63
|
+
*
|
|
64
|
+
* - A payload whose **structure** matches the email adapter's payload shape
|
|
65
|
+
* ({@link ../adapters/email.js}) is rendered field by field, with the body as
|
|
66
|
+
* the human will read it: real line breaks, no JSON escapes.
|
|
67
|
+
* - A payload whose structure is a **shell command** (APRV-126: the shape
|
|
68
|
+
* `cli/hook.ts` builds for a `Bash` tool call) is rendered as the command a
|
|
69
|
+
* shell would receive, over as many lines as it really has, with `cwd` on its
|
|
70
|
+
* own line beneath it. Same reason again: `"command": "gh pr create --body
|
|
71
|
+
* 'a\nb'"` on a phone is one paragraph of escaped JSON, and the observed
|
|
72
|
+
* failure (2026-08-20) is a human being asked to take responsibility for it.
|
|
73
|
+
* - A payload whose structure is a **file change** (APRV-124: the shape
|
|
74
|
+
* `cli/hook.ts` builds for an `Edit` or a `Write` tool call) is rendered as a
|
|
75
|
+
* diff, so the approver reads the change rather than the fact that a file was
|
|
76
|
+
* touched. Same reason as the email case: `"before": "a\nb"` on a phone is
|
|
77
|
+
* bytes nobody can check.
|
|
78
|
+
* - Every other shape falls to `opaque`, whose view IS the canonical JSON: the
|
|
79
|
+
* bytes whole, pretty-printed, exactly the rendering every payload had before
|
|
80
|
+
* the structural views existed.
|
|
81
|
+
*
|
|
82
|
+
* Three properties this file is careful about:
|
|
83
|
+
*
|
|
84
|
+
* 1. **Detection is structural.** Nothing here reads a self-declared "kind" or
|
|
85
|
+
* "type" field, because that field would be authored by the party under
|
|
86
|
+
* oversight, and a channel that lets an agent choose its own presentation
|
|
87
|
+
* has handed the agent the display. A payload is email-shaped when its keys
|
|
88
|
+
* and its value types say so, and for no other reason.
|
|
89
|
+
* 2. **Nothing is hidden.** The shape is accepted only when *every* key is one
|
|
90
|
+
* this module renders, so the field-by-field view never omits a byte of the
|
|
91
|
+
* payload — a `bcc` or a `content_type` that the reader could not see would
|
|
92
|
+
* be the same failure as a truncated payload, wearing a friendlier face.
|
|
93
|
+
* 3. **This is claimed content.** The payload is authored by the requesting
|
|
94
|
+
* agent. The block says so in its first line, and the computed binding (the
|
|
95
|
+
* `sha256` label each channel already prints around this region) stays where
|
|
96
|
+
* it is. Making the payload *legible* must not make it look *verified*.
|
|
97
|
+
* A `tool` or a `rule` value inside a file-change payload is rendered for
|
|
98
|
+
* the reader and is never what selects the rendering: the shape is, exactly
|
|
99
|
+
* as for an email.
|
|
100
|
+
* 4. **Two different byte strings never look the same.** A reading aid that
|
|
101
|
+
* interprets escape sequences has to answer the question it creates: if a
|
|
102
|
+
* real line break becomes a line break, what does the two-byte sequence
|
|
103
|
+
* backslash-`n` become? Rendering both as a line break would let an agent
|
|
104
|
+
* write one payload and have the approver read another. So the rendering is
|
|
105
|
+
* INJECTIVE by construction ({@link markEscapes}), and the property is
|
|
106
|
+
* tested by generating pairs of distinct byte strings.
|
|
107
|
+
* 5. **The view is the whole reading (APRV-162, `approval.md/wysiwys/2`).** A
|
|
108
|
+
* structured kind's view is the canonical rendering entire; no canonical-JSON
|
|
109
|
+
* appendix follows it. The completeness argument is property 2 above: kind
|
|
110
|
+
* detection is a closed field set, one unrecognised key sends the payload to
|
|
111
|
+
* `opaque` whose view is the whole JSON, so a structural view that renders at
|
|
112
|
+
* all renders every byte. The views therefore do not fold. A fold was
|
|
113
|
+
* survivable only while the appendix restated the hidden lines underneath it;
|
|
114
|
+
* with the appendix gone it would hide bytes from the only reading a human
|
|
115
|
+
* gets, which is the failure this module exists to remove.
|
|
116
|
+
*
|
|
117
|
+
* The output is plain text with real newlines. Escaping belongs to the channel:
|
|
118
|
+
* `telegram.ts` and `web.ts` each pass this through their own `escapeHtml` and
|
|
119
|
+
* their own `<pre>`, so the injection surface is exactly what it was before.
|
|
120
|
+
*/
|
|
121
|
+
import { type ProtectedPathEntry } from "./command-class.js";
|
|
122
|
+
/**
|
|
123
|
+
* How a field the payload does not carry is rendered.
|
|
124
|
+
*
|
|
125
|
+
* Never an omission. A closed field set that silently drops its absent members
|
|
126
|
+
* is not a closed field set: the reader cannot tell "no `cc`" from "a `cc` this
|
|
127
|
+
* renderer does not know how to show", and those are the two cases the whole
|
|
128
|
+
* design exists to keep apart.
|
|
129
|
+
*/
|
|
130
|
+
export declare const ABSENT = "(absent)";
|
|
131
|
+
/** The heading and delimiters. Exported because the tests pin them. */
|
|
132
|
+
export declare const EMAIL_VIEW_HEADING = "email \u2014 rendered field by field; every value below is CLAIMED, authored by the requesting party";
|
|
133
|
+
export declare const BODY_BEGIN = "--- body begins ---";
|
|
134
|
+
export declare const BODY_END = "--- body ends ---";
|
|
135
|
+
export declare const CANONICAL_JSON_HEADING = "--- the same bytes, canonical JSON ---";
|
|
136
|
+
/** One labelled line of the field-by-field view. */
|
|
137
|
+
export interface EmailViewField {
|
|
138
|
+
label: string;
|
|
139
|
+
/** The value as text. For `body`, this may contain newlines. */
|
|
140
|
+
text: string;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Recognise an email-shaped payload, structurally.
|
|
144
|
+
*
|
|
145
|
+
* Returns the fields in display order, or `null` when the value is any other
|
|
146
|
+
* shape — including an email-ish object carrying one key this module does not
|
|
147
|
+
* know how to show, which falls back to JSON rather than hiding it.
|
|
148
|
+
*/
|
|
149
|
+
export declare function emailPayloadFields(value: unknown): EmailViewField[] | null;
|
|
150
|
+
/** The heading and delimiters of the diff view. Exported because tests pin them. */
|
|
151
|
+
export declare const EDIT_VIEW_HEADING = "file change \u2014 the change itself, not the touch; every value below is CLAIMED, authored by the requesting party";
|
|
152
|
+
export declare const DIFF_BEGIN = "--- change begins ---";
|
|
153
|
+
export declare const DIFF_END = "--- change ends ---";
|
|
154
|
+
/** The qualifier a proposal-tier touch renders (APRV-124). */
|
|
155
|
+
export declare const PROPOSAL_QUALIFIER = "this edit targets a file inside an AGENT WORKTREE: it is a branch PROPOSAL, not the live file. Merging it to the live checkout is a separate gated action.";
|
|
156
|
+
export declare const LIVE_QUALIFIER = "this edit targets the LIVE checkout, not a branch proposal.";
|
|
157
|
+
/** The qualifier a protected-name touch outside the gated checkout renders (APRV-161). */
|
|
158
|
+
export declare const ELSEWHERE_QUALIFIER = "this edit targets a file NAMED like a policy file, OUTSIDE the gated checkout: it is not the live policy. It gates because the name is protected wherever it sits.";
|
|
159
|
+
/** A file change, recognised structurally. */
|
|
160
|
+
export interface ChangeView {
|
|
161
|
+
/** Labelled single-line fields, in display order. */
|
|
162
|
+
labels: EmailViewField[];
|
|
163
|
+
/** The removed side, or `null` for a whole-file write. */
|
|
164
|
+
before: string | null;
|
|
165
|
+
/** The added side: the new text, or the whole new content. */
|
|
166
|
+
after: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Recognise a file-change payload, structurally.
|
|
170
|
+
*
|
|
171
|
+
* Accepted when the payload names a `file` and carries either both sides of an
|
|
172
|
+
* edit (`before` and `after`) or a whole-file `content`, and every other key is
|
|
173
|
+
* one this module renders. Anything else — including a payload that carries
|
|
174
|
+
* only `before`, where the reader would be shown half a change — is `null` and
|
|
175
|
+
* falls back to JSON.
|
|
176
|
+
*/
|
|
177
|
+
export declare function changePayloadView(value: unknown): ChangeView | null;
|
|
178
|
+
/** The heading and delimiters of the command view. Exported because tests pin them. */
|
|
179
|
+
export declare const COMMAND_VIEW_HEADING = "command \u2014 rendered; the hash binds the RAW BYTES, not this view. Every value below is CLAIMED, authored by the requesting party";
|
|
180
|
+
export declare const COMMAND_BEGIN = "--- command begins ---";
|
|
181
|
+
export declare const COMMAND_END = "--- command ends ---";
|
|
182
|
+
/**
|
|
183
|
+
* The delimiters around a marked escape sequence.
|
|
184
|
+
*
|
|
185
|
+
* Guillemets rather than brackets: `[` and `]` are ordinary shell and regex
|
|
186
|
+
* characters, so a marker built from them would be indistinguishable from the
|
|
187
|
+
* command's own text at a glance, which is the failure this marker exists to
|
|
188
|
+
* prevent.
|
|
189
|
+
*/
|
|
190
|
+
export declare const ESCAPE_OPEN = "\u00AB";
|
|
191
|
+
export declare const ESCAPE_CLOSE = "\u00BB";
|
|
192
|
+
/** The legend printed above every command block, so the marker needs no lore. */
|
|
193
|
+
export declare const ESCAPE_LEGEND = "escapes: \u00AB\\n\u00BB is the two LITERAL bytes backslash-n; a real line break is a line break";
|
|
194
|
+
/**
|
|
195
|
+
* One line of a command, with literal escape sequences marked.
|
|
196
|
+
*
|
|
197
|
+
* INJECTIVE, and the proof is short enough to keep here. The rendering is a
|
|
198
|
+
* left-to-right tokenizer over two tokens: a backslash followed by a letter in
|
|
199
|
+
* {@link MARKED_ESCAPES} becomes `«\c»`, and every other character is itself.
|
|
200
|
+
* A `«\c»` in the OUTPUT can therefore only have come from that first token,
|
|
201
|
+
* because a backslash followed by such a letter in the input is never emitted
|
|
202
|
+
* bare — so reading the output back left to right recovers the input exactly,
|
|
203
|
+
* and a left inverse is all injectivity needs.
|
|
204
|
+
*
|
|
205
|
+
* Real newlines are handled by the caller, which splits on them before calling
|
|
206
|
+
* this: a line break in the output comes from a line break in the input, and
|
|
207
|
+
* from nothing else.
|
|
208
|
+
*/
|
|
209
|
+
export declare function markEscapes(line: string): string;
|
|
210
|
+
/** A shell command, recognised structurally. */
|
|
211
|
+
export interface CommandView {
|
|
212
|
+
/** The command, exactly as the payload carries it. */
|
|
213
|
+
command: string;
|
|
214
|
+
/** The working directory, or `null` when the payload names none. */
|
|
215
|
+
cwd: string | null;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Recognise a command payload, structurally.
|
|
219
|
+
*
|
|
220
|
+
* Accepted when the payload carries a string `command` and nothing this module
|
|
221
|
+
* cannot show. `cwd` is optional here even though `cli/hook.ts` always sets it:
|
|
222
|
+
* the question this answers is "will a human read this better as a command?",
|
|
223
|
+
* and a payload missing its directory reads better either way.
|
|
224
|
+
*/
|
|
225
|
+
export declare function commandPayloadView(value: unknown): CommandView | null;
|
|
226
|
+
/**
|
|
227
|
+
* Where the exact bytes live, and how to get them back (APRV-126, APRV-162).
|
|
228
|
+
*
|
|
229
|
+
* Carried by every structural view, not the command view alone: with no
|
|
230
|
+
* canonical-JSON appendix underneath, this line is the reader's only route from
|
|
231
|
+
* a rendering back to the bytes it was derived from.
|
|
232
|
+
*
|
|
233
|
+
* The store is content-addressed by this very hash and re-verified on every
|
|
234
|
+
* read (`core/payload-store.ts`), so the line is an instruction, never a claim:
|
|
235
|
+
* following it produces the bytes or produces a refusal, and never something
|
|
236
|
+
* else wearing the same name.
|
|
237
|
+
*/
|
|
238
|
+
export declare function rawBytesLine(hash: string): string;
|
|
239
|
+
/** What separates two segments of the breakdown. Exported: the tests pin it. */
|
|
240
|
+
export declare const BREAKDOWN_SEPARATOR = " \u00B7 ";
|
|
241
|
+
/** Characters one segment of the breakdown may take before it folds. */
|
|
242
|
+
export declare const BREAKDOWN_SEGMENT_BUDGET = 40;
|
|
243
|
+
/** Segments the breakdown shows before it says how many it did not. */
|
|
244
|
+
export declare const BREAKDOWN_MAX_SEGMENTS = 8;
|
|
245
|
+
/**
|
|
246
|
+
* What a compound command does, segment by segment (APRV-144).
|
|
247
|
+
*
|
|
248
|
+
* `git add … · git commit · git push origin main:records-… · gh pr create`.
|
|
249
|
+
*
|
|
250
|
+
* The observed complaint (Carter, 2026-08-25) is that the claimed summary of a
|
|
251
|
+
* shell action is `truncate(command, 160)`, which for a chained command is the
|
|
252
|
+
* first clause and a path prefix: the approver reads where the command starts
|
|
253
|
+
* and never what it ends by doing. This is the deterministic half of the
|
|
254
|
+
* answer. It is derived from {@link commandSegmentWords} — the classifier's own
|
|
255
|
+
* tokenizer, never a second one — so a channel showing it cannot describe a
|
|
256
|
+
* command differently from the module that chose its class.
|
|
257
|
+
*
|
|
258
|
+
* `null` for a string the tokenizer refuses (the same input the classifier
|
|
259
|
+
* answers `unparseable` for) and for one with no segment carrying a binary: an
|
|
260
|
+
* aid that cannot be derived is absent, never guessed.
|
|
261
|
+
*/
|
|
262
|
+
export declare function commandBreakdown(command: string): string | null;
|
|
263
|
+
/** The path that made an action `policy.edit`, and the rule that matched it. */
|
|
264
|
+
export interface ProtectedPathView {
|
|
265
|
+
path: string;
|
|
266
|
+
rule: string;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Which protected path selected this payload's class, when one did (APRV-143).
|
|
270
|
+
*
|
|
271
|
+
* A prompt that says `class: policy.edit` and stops there tells the approver
|
|
272
|
+
* that *some* rule fired and leaves them to find the file. Both gated shapes
|
|
273
|
+
* can say which:
|
|
274
|
+
*
|
|
275
|
+
* - a shell payload is re-classified here, by the same
|
|
276
|
+
* {@link classifyCommand} the hook decided with, and the segment that took
|
|
277
|
+
* `policy.edit` carries the word it matched (`ClassifiedSegment.path`);
|
|
278
|
+
* - a file-tool payload names its target in `file`, and
|
|
279
|
+
* {@link isProtectedPath} is re-run over it rather than trusted: the answer
|
|
280
|
+
* is recomputed from the bound bytes, so this stays a computed field. The
|
|
281
|
+
* payload's own `rule` is used as the label only when it is one of the three
|
|
282
|
+
* the hook writes, which is what keeps the worktree-proposal and
|
|
283
|
+
* protected-name-elsewhere tiers legible (APRV-124, APRV-161).
|
|
284
|
+
*
|
|
285
|
+
* `extra` is `policy.protected_paths`, passed exactly as every enforcement path
|
|
286
|
+
* passes it; omitting it narrows the answer and never widens it.
|
|
287
|
+
*/
|
|
288
|
+
export declare function protectedPathView(value: unknown, extra?: readonly ProtectedPathEntry[]): ProtectedPathView | null;
|
|
289
|
+
/**
|
|
290
|
+
* The renderer's identity, printed inside every canonical block.
|
|
291
|
+
*
|
|
292
|
+
* Inside the text, and therefore inside {@link CanonicalRendering.display_hash}:
|
|
293
|
+
* a version that rode alongside the hash rather than inside it would let two
|
|
294
|
+
* renderer versions produce the same digest for two different readings, which is
|
|
295
|
+
* the one thing the digest exists to make impossible. Any change to the bytes
|
|
296
|
+
* this module emits — a new field, a reworded heading, a line that used to be
|
|
297
|
+
* folded away — is a new version, and a reader comparing a stored
|
|
298
|
+
* `display_hash` against a re-render can see which renderer wrote it. A record
|
|
299
|
+
* written under an earlier version re-derives under the renderer its own hashed
|
|
300
|
+
* text names, never under this one.
|
|
301
|
+
*
|
|
302
|
+
* `/2` (APRV-162): the structural views render whole and carry no canonical-JSON
|
|
303
|
+
* appendix; `opaque` is unchanged, its view being that JSON.
|
|
304
|
+
*/
|
|
305
|
+
export declare const CANONICAL_RENDERER_VERSION = "approval.md/wysiwys/2";
|
|
306
|
+
/**
|
|
307
|
+
* The `approval.requested` payload field carrying {@link
|
|
308
|
+
* CanonicalRendering.display_hash} (APRV-119).
|
|
309
|
+
*
|
|
310
|
+
* Written by the gate at the write boundary, exactly as `ts` and `policy_sha256`
|
|
311
|
+
* are, and for the same reason: the requesting party must not be able to name
|
|
312
|
+
* the rendering it claims a human was shown. {@link RequestInput} carries no
|
|
313
|
+
* field for it.
|
|
314
|
+
*/
|
|
315
|
+
export declare const DISPLAY_HASH_FIELD = "display_hash";
|
|
316
|
+
/** The delimiters of the canonical block. Exported because the tests pin them. */
|
|
317
|
+
export declare const CANONICAL_BEGIN = "--- canonical rendering begins ---";
|
|
318
|
+
export declare const CANONICAL_END = "--- canonical rendering ends ---";
|
|
319
|
+
/** The heading of the `opaque` kind: no structural view, the bytes whole. */
|
|
320
|
+
export declare const OPAQUE_VIEW_HEADING = "payload \u2014 no structural view applies to this shape; every byte of it is in the canonical JSON below, and every value is CLAIMED, authored by the requesting party";
|
|
321
|
+
/**
|
|
322
|
+
* The kinds a payload can be rendered as.
|
|
323
|
+
*
|
|
324
|
+
* Closed, and decided by structure alone. `opaque` is not a failure: it is the
|
|
325
|
+
* kind whose closed field set is "the whole canonical JSON", which is the
|
|
326
|
+
* rendering every payload had before the structural views existed.
|
|
327
|
+
*/
|
|
328
|
+
export declare const CANONICAL_KINDS: readonly ["command", "file-change", "email", "opaque"];
|
|
329
|
+
export type CanonicalKind = (typeof CANONICAL_KINDS)[number];
|
|
330
|
+
/** One canonical rendering: what the human reads, and the digest of it. */
|
|
331
|
+
export interface CanonicalRendering {
|
|
332
|
+
/** {@link CANONICAL_RENDERER_VERSION}, for a caller that wants it separately. */
|
|
333
|
+
version: string;
|
|
334
|
+
/** Which structural view was applied. */
|
|
335
|
+
kind: CanonicalKind;
|
|
336
|
+
/** The text, with real newlines. Escaping belongs to the channel. */
|
|
337
|
+
text: string;
|
|
338
|
+
/** SHA-256 (lowercase hex) over `text` as UTF-8. */
|
|
339
|
+
display_hash: string;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Render a payload the way every channel MUST present it (APRV-119).
|
|
343
|
+
*
|
|
344
|
+
* A pure function of `(payload, actionClass)`. Same arguments, byte-identical
|
|
345
|
+
* `text` and `display_hash`, in this process or another, today or next year
|
|
346
|
+
* under the same {@link CANONICAL_RENDERER_VERSION}.
|
|
347
|
+
*
|
|
348
|
+
* The block states its own renderer, class, kind and payload digest before it
|
|
349
|
+
* shows anything, so a reader who is handed the text alone can tell what
|
|
350
|
+
* produced it and what it binds to. Then the view for the kind, which is the
|
|
351
|
+
* whole reading: it renders every byte of the payload or the payload is
|
|
352
|
+
* `opaque` and the view is its JSON (APRV-162).
|
|
353
|
+
*
|
|
354
|
+
* Throws `JcsError` for a payload RFC 8785 cannot serialize (a cycle, a NaN).
|
|
355
|
+
* That is {@link payloadHash}'s contract and it is the right one here too: a
|
|
356
|
+
* payload that cannot be bound to must not acquire a plausible-looking rendering
|
|
357
|
+
* of itself. Every caller in this repository renders material that has already
|
|
358
|
+
* been hash-checked against the log's binding, so the throw is unreachable on
|
|
359
|
+
* the paths a human ever sees.
|
|
360
|
+
*/
|
|
361
|
+
export declare function canonicalRender(payload: unknown, actionClass: string): CanonicalRendering;
|
|
362
|
+
/**
|
|
363
|
+
* The `display_hash` of a payload, or `null` when there is none to compute.
|
|
364
|
+
*
|
|
365
|
+
* The gate's entry point (`core/gate.ts`), where a payload that cannot be
|
|
366
|
+
* canonicalized must not abort a request that has already passed every check
|
|
367
|
+
* that matters. A missing `display_hash` costs a reader one cross-check; a
|
|
368
|
+
* throw here would cost them the request.
|
|
369
|
+
*/
|
|
370
|
+
export declare function displayHashOf(payload: unknown, actionClass: string): string | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon's advance, run in a child process (APRV-211).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this file exists
|
|
5
|
+
*
|
|
6
|
+
* `approval up` runs the daemon loop and the Telegram listener in one process,
|
|
7
|
+
* and `cli/log-advance.ts` is `spawnSync` from end to end. So an advance run on
|
|
8
|
+
* the daemon's own stack blocks the loop for as long as `git fetch`, the
|
|
9
|
+
* scratch-index commit, `git push` and `gh pr create` take, and every callback
|
|
10
|
+
* that arrived meanwhile was answered past Telegram's window: the
|
|
11
|
+
* `answerCallbackQuery: HTTP 400`s Carter saw on 2026-09-02. Nothing that runs
|
|
12
|
+
* on that loop can fix it, because synchronous work does not yield. Another
|
|
13
|
+
* process can.
|
|
14
|
+
*
|
|
15
|
+
* ## What it is allowed to do, and what it is not
|
|
16
|
+
*
|
|
17
|
+
* It runs the verb. That is the entire remit.
|
|
18
|
+
*
|
|
19
|
+
* It does NOT touch the gate, and it could not if it tried: `core/child-env.ts`
|
|
20
|
+
* strips `APPROVAL_*` from a child's environment (APRV-205), which is where the
|
|
21
|
+
* `supervised-live` draw's secret lives, so a child that asked the gate would
|
|
22
|
+
* fail closed on every tick. The register/request/start half happens in the
|
|
23
|
+
* daemon before this is spawned and the `execution.completed`/`failed` is
|
|
24
|
+
* appended by the daemon after it exits. This process appends nothing, decides
|
|
25
|
+
* nothing, and holds no authority: if it were replaced wholesale by something
|
|
26
|
+
* hostile, the worst it could do is refuse to advance the log or report a
|
|
27
|
+
* failure that did not happen — it cannot authorise anything, because by the
|
|
28
|
+
* time it runs the authorisation is already in the log and already spent.
|
|
29
|
+
*
|
|
30
|
+
* ## The protocol
|
|
31
|
+
*
|
|
32
|
+
* One argument: the JSON `LogAdvanceOptions` subset the daemon chose. One line
|
|
33
|
+
* on stdout: the `LogAdvanceResult` verbatim, `{ok:true,report}` or
|
|
34
|
+
* `{ok:false,code,message}`. The parent VALIDATES that line rather than
|
|
35
|
+
* trusting it, and treats anything else as a failed advance with a
|
|
36
|
+
* machine-readable reason. Nothing is written to stdout but that line, which is
|
|
37
|
+
* why the verb is given no progress reporter.
|
|
38
|
+
*/
|
|
39
|
+
export {};
|