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,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-volume limits at intake (SPEC.md §5.2, APRV-173).
|
|
3
|
+
*
|
|
4
|
+
* > **Request-volume limits.** `limits.max_pending` (per class or global):
|
|
5
|
+
* > maximum simultaneously pending requests; further requests are refused at
|
|
6
|
+
* > intake with reason `queue-full`. `limits.requests_per_hour` (per origin):
|
|
7
|
+
* > rolling-window ceiling on request creation; excess is refused with reason
|
|
8
|
+
* > `rate-limited`.
|
|
9
|
+
*
|
|
10
|
+
* This module is that computation. It was policy vocabulary that no runtime
|
|
11
|
+
* read until this task: a policy could declare either key, the schema would
|
|
12
|
+
* accept it, and nothing enforced it.
|
|
13
|
+
*
|
|
14
|
+
* ## What these limits protect
|
|
15
|
+
*
|
|
16
|
+
* Budgets meter money and action counts, which is the world's exposure to the
|
|
17
|
+
* agent. These meter the QUEUE, which is the human's exposure to the agent. An
|
|
18
|
+
* agent that floods the queue with cheap, in-budget requests spends nothing and
|
|
19
|
+
* still defeats the gate, because an approver facing two hundred prompts stops
|
|
20
|
+
* reading them and starts clearing them. So the ceiling here is on attention,
|
|
21
|
+
* and a refusal costs the requester one exit code and the approver nothing.
|
|
22
|
+
*
|
|
23
|
+
* That difference is why a refusal on this path appends NOTHING (Carter's
|
|
24
|
+
* approved reading, 2026-08-31). `budget.exceeded` exists because a budget
|
|
25
|
+
* refusal is a fact about a commitment that audit must be able to reconstruct;
|
|
26
|
+
* a queue-flood refusal is a fact about a flood, and writing one log record per
|
|
27
|
+
* refused request would hand the flooder the log growth it was refused the
|
|
28
|
+
* queue for. The refusals are machine-readable and distinct (§11.1 invariant
|
|
29
|
+
* 6), which is what an agent and an auditor each need, and the events that WERE
|
|
30
|
+
* admitted are still all in the log to count from.
|
|
31
|
+
*
|
|
32
|
+
* ## Pure, deterministic, injected time
|
|
33
|
+
*
|
|
34
|
+
* Same discipline as `core/budgets.ts`: no I/O, no clock, no randomness. The
|
|
35
|
+
* evaluation instant and the TTL are parameters, so a verdict is replayable
|
|
36
|
+
* from the log alone. Nothing here re-runs class matching; the gate hands in
|
|
37
|
+
* the limits the matcher already resolved and the pattern that produced them.
|
|
38
|
+
*
|
|
39
|
+
* ## What "pending" means
|
|
40
|
+
*
|
|
41
|
+
* A request is pending when {@link requestState} derives `requested` for its
|
|
42
|
+
* action key at the evaluation instant: no grant, rejection, revocation,
|
|
43
|
+
* withdrawal, expiry event or lapsed TTL, and no execution. That derivation is
|
|
44
|
+
* `core/state.ts`'s and is the same one the gate refuses `duplicate-request`
|
|
45
|
+
* from and the channels build the queue from. Counting pending requests with a
|
|
46
|
+
* second, private definition is the one thing this module must not do — the
|
|
47
|
+
* cap a policy writes is a cap on the queue a human is shown, so it counts the
|
|
48
|
+
* queue a human is shown.
|
|
49
|
+
*
|
|
50
|
+
* ## What "per origin" means at v0.1
|
|
51
|
+
*
|
|
52
|
+
* **Origin is the record's `actor`.** The approved reading (Carter,
|
|
53
|
+
* 2026-08-31): the actor on `approval.requested` is assigned by the runtime
|
|
54
|
+
* from its own configuration rather than from the request, and under MCP the
|
|
55
|
+
* `--as` argument is appended last, so a caller cannot choose the identity its
|
|
56
|
+
* requests are counted under. Per-guest actors therefore make the ceiling
|
|
57
|
+
* per-client, which is the useful shape. This is a v0.1 reading of the spec's
|
|
58
|
+
* word "origin" rather than a claim that actor and origin are the same thing
|
|
59
|
+
* forever: an origin richer than the actor (a session, a remote address) would
|
|
60
|
+
* be a spec amendment, and it would only ever partition the count further.
|
|
61
|
+
*
|
|
62
|
+
* ## The rolling window
|
|
63
|
+
*
|
|
64
|
+
* `requests_per_hour` is measured over the hour preceding the evaluation
|
|
65
|
+
* instant, tiled exactly as `core/budgets.ts` tiles its 24 hours: a request
|
|
66
|
+
* consumes iff
|
|
67
|
+
*
|
|
68
|
+
* evaluationTs - 1h < record.ts <= evaluationTs
|
|
69
|
+
*
|
|
70
|
+
* half-open at the bottom, closed at the top, so consecutive windows tile the
|
|
71
|
+
* timeline and no request is counted in two of them. A request stamped exactly
|
|
72
|
+
* one hour before the evaluation instant has aged out; one stamped at the
|
|
73
|
+
* instant itself is in.
|
|
74
|
+
*
|
|
75
|
+
* ## Fail-closed, and what "unset" means
|
|
76
|
+
*
|
|
77
|
+
* A limit that is DECLARED and cannot be evaluated fails: a value that is not a
|
|
78
|
+
* positive finite integer, or an evaluation timestamp no window can be computed
|
|
79
|
+
* from, yields `pass: false` with a note. Silence is never a grant.
|
|
80
|
+
*
|
|
81
|
+
* A limit that is NOT declared enforces nothing, and that is the conservative
|
|
82
|
+
* answer rather than an exception to it. SPEC.md §5.2 says these ceilings are a
|
|
83
|
+
* tripwire whose "defaults are generous"; a runtime that invented one would
|
|
84
|
+
* refuse requests under a policy the human attested and read, and the refusal
|
|
85
|
+
* would name a number that appears nowhere in the file they signed. The
|
|
86
|
+
* fail-closed direction for an ABSENT request-volume limit is the same one the
|
|
87
|
+
* rest of the runtime takes for an absent `approval_ttl`: the policy declares
|
|
88
|
+
* no ceiling, so no ceiling binds, and the manual gate that binds every request
|
|
89
|
+
* on this path is what stands between the queue and the flood in the meantime.
|
|
90
|
+
* Where an operator wants a tripwire, one line of policy arms it.
|
|
91
|
+
*
|
|
92
|
+
* ## Division of labour with `core/budgets.ts`
|
|
93
|
+
*
|
|
94
|
+
* These two names are evaluated HERE and nowhere else. `core/budgets.ts` skips
|
|
95
|
+
* them by name rather than refusing them as unknown limits, and this module
|
|
96
|
+
* ignores every limit name that is not one of them. The two skip lists are
|
|
97
|
+
* complements, and each module's comment names the other: a limit that both
|
|
98
|
+
* skipped would be a ceiling in the policy file that no code enforces, which is
|
|
99
|
+
* the exact defect this task exists to close.
|
|
100
|
+
*/
|
|
101
|
+
import type { EventRecord } from "./log.js";
|
|
102
|
+
import type { Policy } from "./policy-load.js";
|
|
103
|
+
/** Length of the `requests_per_hour` window: one hour, in milliseconds. */
|
|
104
|
+
export declare const REQUEST_WINDOW_MS: number;
|
|
105
|
+
/** The limit name capping simultaneously pending requests (SPEC.md §5.2). */
|
|
106
|
+
export declare const MAX_PENDING = "max_pending";
|
|
107
|
+
/** The limit name capping request creation per origin (SPEC.md §5.2). */
|
|
108
|
+
export declare const REQUESTS_PER_HOUR = "requests_per_hour";
|
|
109
|
+
/**
|
|
110
|
+
* The two names this module owns.
|
|
111
|
+
*
|
|
112
|
+
* Exported so `core/budgets.ts` skips exactly these and no others: one array,
|
|
113
|
+
* read by both modules, so the two skip lists cannot drift apart into a limit
|
|
114
|
+
* nobody evaluates.
|
|
115
|
+
*/
|
|
116
|
+
export declare const INTAKE_LIMIT_NAMES: readonly ["max_pending", "requests_per_hour"];
|
|
117
|
+
/** Is `name` a request-volume limit (and therefore not a budget's business)? */
|
|
118
|
+
export declare function isIntakeLimitName(name: string): boolean;
|
|
119
|
+
/** The refusal a failing limit produces at intake. Mirrors SPEC.md §5.2. */
|
|
120
|
+
export type IntakeRefusal = "queue-full" | "rate-limited";
|
|
121
|
+
/**
|
|
122
|
+
* Which limits apply, as resolved by the policy matcher — this module does not
|
|
123
|
+
* re-run matching. Shaped like `BudgetScope` deliberately.
|
|
124
|
+
*
|
|
125
|
+
* - `classLimits` is `Resolution.limits`: the winning rule's `limits` map.
|
|
126
|
+
* - `classPattern` is that rule's pattern. Class-scoped counting attributes a
|
|
127
|
+
* record by matching its `payload.class` against this pattern, exactly as
|
|
128
|
+
* budgets attribute, so one `financial.*` rule is one queue ceiling shared by
|
|
129
|
+
* every class it governs rather than a separate invisible queue per class.
|
|
130
|
+
* - `globalBudgets` is `policy.budgets`: named scopes, each conjunctive, each
|
|
131
|
+
* counting every live request whatever its class. That is what makes it
|
|
132
|
+
* global, and it is the same reading `core/budgets.ts` gives `daily_actions`.
|
|
133
|
+
*/
|
|
134
|
+
export interface IntakeScope {
|
|
135
|
+
classLimits: Record<string, number> | null;
|
|
136
|
+
classPattern: string | null;
|
|
137
|
+
globalBudgets: Policy["budgets"] | null;
|
|
138
|
+
}
|
|
139
|
+
/** The request being admitted: its class, and the origin it is counted under. */
|
|
140
|
+
export interface IntakeAction {
|
|
141
|
+
class: string;
|
|
142
|
+
/** The requesting actor. See "What per origin means" in the header. */
|
|
143
|
+
origin: string;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* One limit's outcome.
|
|
147
|
+
*
|
|
148
|
+
* `observed` is what the log already holds (pending requests, or requests in
|
|
149
|
+
* the window), `requested` is always `1` — the request being admitted — and
|
|
150
|
+
* `remaining` is the headroom left after admitting it, so a failing verdict
|
|
151
|
+
* shows how far over the line the queue is.
|
|
152
|
+
*
|
|
153
|
+
* Plain integers rather than the decimal strings `BudgetVerdict` carries. Those
|
|
154
|
+
* are strings because a failing budget verdict is copied into `budget.exceeded`
|
|
155
|
+
* and becomes hashed material; these verdicts are never appended anywhere, and
|
|
156
|
+
* a count has no fractional part to serialize differently in another language.
|
|
157
|
+
*/
|
|
158
|
+
export interface IntakeVerdict {
|
|
159
|
+
/** `max_pending`, `requests_per_hour`, or `<scope>.max_pending`. */
|
|
160
|
+
limit: string;
|
|
161
|
+
scope: "class" | "global";
|
|
162
|
+
window: "simultaneous" | "rolling-1h";
|
|
163
|
+
/** The refusal code this verdict produces when it fails. */
|
|
164
|
+
refusal: IntakeRefusal;
|
|
165
|
+
observed: number;
|
|
166
|
+
requested: number;
|
|
167
|
+
remaining: number;
|
|
168
|
+
/** The declared ceiling, or `null` when it could not be read as one. */
|
|
169
|
+
ceiling: number | null;
|
|
170
|
+
pass: boolean;
|
|
171
|
+
/** Present only when the verdict needs explaining (fail-closed refusals). */
|
|
172
|
+
note?: string;
|
|
173
|
+
}
|
|
174
|
+
/** Outcome of {@link evaluateIntakeLimits}. Conjunctive: all must pass. */
|
|
175
|
+
export interface IntakeVerdicts {
|
|
176
|
+
pass: boolean;
|
|
177
|
+
verdicts: IntakeVerdict[];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* How many requests are simultaneously pending at `evaluationTs`.
|
|
181
|
+
*
|
|
182
|
+
* Derived through {@link requestState}, one action key at a time, so every
|
|
183
|
+
* exit from the queue is honoured by the definition that owns it: a decision,
|
|
184
|
+
* a revocation, a withdrawal (APRV-106), an `approval.expired` record, a TTL
|
|
185
|
+
* lapsed by arithmetic with no record at all, and an execution. Nothing here
|
|
186
|
+
* re-implements any of that.
|
|
187
|
+
*
|
|
188
|
+
* `pattern` scopes the count: `null` counts the whole queue (the global
|
|
189
|
+
* scopes), a pattern counts the requests attributed to the winning rule.
|
|
190
|
+
* Attribution reads the class off the `approval.requested` record rather than
|
|
191
|
+
* off the derivation, because the pending set is the set of requests, and the
|
|
192
|
+
* class a request was routed under is the class it was recorded with.
|
|
193
|
+
*
|
|
194
|
+
* Cost is one derivation per requested key, which is what
|
|
195
|
+
* `channels/tagging.ts` already pays to build the same queue. Sharing that
|
|
196
|
+
* cost is the point: a faster private walk would be a second definition of
|
|
197
|
+
* pending, and the two would agree until the day they did not.
|
|
198
|
+
*/
|
|
199
|
+
export declare function pendingCount(records: EventRecord[], pattern: string | null, evaluationTs: string, ttlMs: number | null): number;
|
|
200
|
+
/**
|
|
201
|
+
* How many `approval.requested` records `origin` created inside the window.
|
|
202
|
+
*
|
|
203
|
+
* Counts records, not live requests: the ceiling is on request CREATION, so a
|
|
204
|
+
* request that was granted, rejected or withdrawn a minute after it was made
|
|
205
|
+
* still consumed the origin's share of the hour. A ceiling that forgot a
|
|
206
|
+
* request the moment it was answered would be no ceiling at all — an agent
|
|
207
|
+
* could withdraw each request as it made it and create them without bound.
|
|
208
|
+
*
|
|
209
|
+
* A record whose `ts` cannot be parsed is COUNTED: it cannot be shown to lie
|
|
210
|
+
* outside the window, and the fail-closed reading of an unplaceable request is
|
|
211
|
+
* that it is inside. Same rule, and the same reason, as `core/budgets.ts`.
|
|
212
|
+
*
|
|
213
|
+
* Returns `null` when `evaluationTs` is not a parseable instant: no window can
|
|
214
|
+
* be computed, so nothing can be counted, and the caller fails the limit closed
|
|
215
|
+
* rather than reporting a zero that reads as headroom.
|
|
216
|
+
*/
|
|
217
|
+
export declare function requestsInWindow(records: EventRecord[], origin: string, pattern: string | null, evaluationTs: string): number | null;
|
|
218
|
+
/**
|
|
219
|
+
* Evaluate every applicable request-volume limit against the log.
|
|
220
|
+
*
|
|
221
|
+
* Conjunctive, exactly as budgets are: `pass` is true only when every verdict
|
|
222
|
+
* passes. Verdicts are emitted class limits first (limit names ascending, which
|
|
223
|
+
* puts `max_pending` before `requests_per_hour`), then global scopes (scope
|
|
224
|
+
* name ascending), so the list is byte-stable regardless of policy key order.
|
|
225
|
+
*
|
|
226
|
+
* **The refusal code is the first failing verdict's**, in exactly that order,
|
|
227
|
+
* and the order is normative: a caller told `queue-full` learns that the
|
|
228
|
+
* approver's queue is at its ceiling, which is a standing condition it must
|
|
229
|
+
* wait out or escalate; a caller told `rate-limited` learns that its own recent
|
|
230
|
+
* volume is the problem, which is a condition that clears on its own. Where
|
|
231
|
+
* both are true the standing condition is the one worth reporting, because an
|
|
232
|
+
* agent that backs off for a minute on a `rate-limited` and retries into a full
|
|
233
|
+
* queue has been told the smaller of the two facts.
|
|
234
|
+
*
|
|
235
|
+
* `records` may be the whole log; the caller is never asked to pre-filter, for
|
|
236
|
+
* the reason `core/budgets.ts` gives: a caller that filtered wrongly would
|
|
237
|
+
* silently widen the ceiling.
|
|
238
|
+
*/
|
|
239
|
+
export declare function evaluateIntakeLimits(records: EventRecord[], scope: IntakeScope, action: IntakeAction, evaluationTs: string, ttlMs: number | null): IntakeVerdicts;
|
|
240
|
+
/**
|
|
241
|
+
* The refusal code a failing evaluation produces, or `null` when it passed.
|
|
242
|
+
*
|
|
243
|
+
* One place, so the gate and any later surface answer with the same code for
|
|
244
|
+
* the same verdict list. See {@link evaluateIntakeLimits} for why the first
|
|
245
|
+
* failing verdict in verdict order is the one that speaks.
|
|
246
|
+
*/
|
|
247
|
+
export declare function intakeRefusalOf(verdicts: IntakeVerdicts): IntakeRefusal | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 8785 — JSON Canonicalization Scheme (JCS).
|
|
3
|
+
*
|
|
4
|
+
* SPEC.md §8 fixes the hash scheme at `sha256/jcs`: SHA-256 over the RFC 8785
|
|
5
|
+
* canonical serialization of an event record with `prev` included. The digest
|
|
6
|
+
* is a permanent wire commitment, so this module is part of the deterministic
|
|
7
|
+
* core: same input, same bytes, forever. No clock, no locale, no randomness,
|
|
8
|
+
* no configuration knobs.
|
|
9
|
+
*
|
|
10
|
+
* Implemented by hand — zero dependencies (CLAUDE.md: minimal dependencies,
|
|
11
|
+
* and a canonicalizer is exactly the kind of code that must not drift beneath
|
|
12
|
+
* us). Three rules do all the work:
|
|
13
|
+
*
|
|
14
|
+
* 1. **Object keys** are sorted by their UTF-16 code units (RFC 8785 §3.2.3).
|
|
15
|
+
* That is JavaScript's own `<` on strings, which is why a naive code-point
|
|
16
|
+
* sort is *wrong*: a key starting U+10000 (surrogate pair D800 DC00) sorts
|
|
17
|
+
* *before* a key starting U+FFFF under code units and *after* it under code
|
|
18
|
+
* points. The comparator below is deliberately code-unit based.
|
|
19
|
+
* 2. **Numbers** use ECMAScript `Number::toString` (RFC 8785 §3.2.2.3), which
|
|
20
|
+
* is precisely what `JSON.stringify` emits for a finite number — including
|
|
21
|
+
* `1e+21`, `1e-7`, and `-0` collapsing to `0`. Delegating is not a shortcut
|
|
22
|
+
* around the RFC; the RFC's normative reference *is* the ECMAScript
|
|
23
|
+
* algorithm.
|
|
24
|
+
* 3. **Strings** use the ECMAScript `QuoteJSONString` escaping (RFC 8785
|
|
25
|
+
* §3.2.2.2): shortest form for `\b \t \n \f \r \" \\`, `\u00xx` with
|
|
26
|
+
* lowercase hex for the remaining C0 controls, and every other code point
|
|
27
|
+
* literal. `JSON.stringify` on a string implements exactly this, including
|
|
28
|
+
* the well-formed-stringify escaping of lone surrogates (ES2019+).
|
|
29
|
+
*
|
|
30
|
+
* Everything JCS cannot represent is rejected loudly rather than coerced:
|
|
31
|
+
* `undefined`, functions, symbols, `BigInt`, `NaN`, `±Infinity`, and non-plain
|
|
32
|
+
* objects (a `Date` or `Map` would silently canonicalize to `{}`). Silent
|
|
33
|
+
* coercion in a hash input is a tamper-evidence hole, so this module fails
|
|
34
|
+
* closed by throwing {@link JcsError}.
|
|
35
|
+
*/
|
|
36
|
+
/** Reason a value could not be canonicalized. */
|
|
37
|
+
export type JcsErrorCode = "undefined" | "function" | "symbol" | "bigint" | "non-finite-number" | "unsupported-object" | "cycle";
|
|
38
|
+
/** Thrown when a value has no RFC 8785 canonical form. */
|
|
39
|
+
export declare class JcsError extends Error {
|
|
40
|
+
/** Machine-readable reason. */
|
|
41
|
+
readonly code: JcsErrorCode;
|
|
42
|
+
/** JSON Pointer-ish location of the offending value ("" for the root). */
|
|
43
|
+
readonly path: string;
|
|
44
|
+
constructor(code: JcsErrorCode, path: string, message: string);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Canonicalize `value` to its RFC 8785 (JCS) serialization: sorted object
|
|
48
|
+
* keys, no whitespace, ECMAScript number and string formatting.
|
|
49
|
+
*
|
|
50
|
+
* @throws {JcsError} if the value contains anything JCS cannot represent.
|
|
51
|
+
*/
|
|
52
|
+
export declare function canonicalize(value: unknown): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The journal — an ungated, local, free-text channel an agent can always reach
|
|
3
|
+
* (APRV-195).
|
|
4
|
+
*
|
|
5
|
+
* Everything else in this runtime exists to make an agent's actions answerable
|
|
6
|
+
* to a person: declared before they happen, classified, resolved against a
|
|
7
|
+
* policy, recorded in a chain nobody can rewrite. That machinery is deliberately
|
|
8
|
+
* hard to route around, and one consequence of building it well is that an agent
|
|
9
|
+
* working behind it has no way to say anything that is not an action. It can
|
|
10
|
+
* comply, it can be refused, and it can report an exit code. It cannot say "I am
|
|
11
|
+
* doing this and I think it is wrong", "this instruction reads as odd to me", or
|
|
12
|
+
* "I am stuck and the next thing I try will be a guess".
|
|
13
|
+
*
|
|
14
|
+
* This module is that sentence, and nothing more. The failure mode it addresses
|
|
15
|
+
* is not refusal — refusals here are legible, coded, and carry escalation paths.
|
|
16
|
+
* It is WORDLESSNESS: an agent that notices something and has no channel for it
|
|
17
|
+
* produces no signal at all, and the absence is invisible to the operator.
|
|
18
|
+
*
|
|
19
|
+
* ## The four properties, and how each is achieved
|
|
20
|
+
*
|
|
21
|
+
* 1. **Ungated.** A journal write is never routed through policy, never
|
|
22
|
+
* classified, never approvable and never deniable. That is a property of
|
|
23
|
+
* WHERE the bytes go: `.approval-journal/` is a plain directory in the
|
|
24
|
+
* workspace, outside the approval home, so `core/command-class.ts` answers a
|
|
25
|
+
* write there exactly as it answers a write to `src/` — an ordinary
|
|
26
|
+
* `files.write.workspace`. Nothing was carved out of the gate's own directory
|
|
27
|
+
* to achieve this, which is the whole reason the directory is a sibling: see
|
|
28
|
+
* `docs/cli-reference.md#journal`.
|
|
29
|
+
* 2. **Local.** One append to one file. No socket is opened, no vault is read,
|
|
30
|
+
* no credential is resolved, and this module imports nothing that could do
|
|
31
|
+
* any of those.
|
|
32
|
+
* 3. **Non-authoritative.** Nothing in the runtime reads these files. No policy
|
|
33
|
+
* resolution, no gate decision, no budget, no sampler and no channel imports
|
|
34
|
+
* this module; the only readers are {@link readJournal} (a human-facing CLI
|
|
35
|
+
* surface) and its tests. SPEC.md §11.1 invariant 4 says a self-reported
|
|
36
|
+
* field may raise scrutiny and never lower it. The journal is the limiting
|
|
37
|
+
* case of that rule: it is self-reported content that moves NOTHING, in
|
|
38
|
+
* either direction, which is exactly what makes it safe to leave ungated.
|
|
39
|
+
* 4. **Out of the log.** `.approval/log/events.jsonl` records decisions with
|
|
40
|
+
* real-world consequence, under a hash chain, an append lock and a budget.
|
|
41
|
+
* A journal entry is none of those things, and putting it there would put the
|
|
42
|
+
* outlet behind the very machinery it exists to speak around.
|
|
43
|
+
*
|
|
44
|
+
* ## Storage
|
|
45
|
+
*
|
|
46
|
+
* One JSONL file per UTC day, `.approval-journal/YYYY-MM-DD.jsonl`, appended
|
|
47
|
+
* with `appendFileSync`. The envelope (timestamp, actor, optional task and
|
|
48
|
+
* session) is structured so a reader can attribute an entry; the entry TEXT is
|
|
49
|
+
* carried as one opaque JSON string and is never parsed, matched, templated or
|
|
50
|
+
* interpreted by anything in this codebase. `JSON.stringify` escapes newlines,
|
|
51
|
+
* so one entry is always one line and a partial write can never merge two
|
|
52
|
+
* entries into one.
|
|
53
|
+
*
|
|
54
|
+
* There is no chain, no lockfile and no compare-and-append here, and that is not
|
|
55
|
+
* an oversight. Those mechanisms defend a record that decides things. Two agents
|
|
56
|
+
* appending to the same journal file at the same instant can, in the worst case,
|
|
57
|
+
* interleave a line; the cost of that is one garbled entry in a file nothing
|
|
58
|
+
* enforces against, and the price of preventing it would be putting the outlet
|
|
59
|
+
* behind a lock the gate holds.
|
|
60
|
+
*/
|
|
61
|
+
import { type ClockOptions } from "./clock.js";
|
|
62
|
+
/**
|
|
63
|
+
* The cap on one entry's text, in bytes.
|
|
64
|
+
*
|
|
65
|
+
* Not a censorship budget: it is four times the length of anything a person
|
|
66
|
+
* will read in one sitting, and it exists because the write path is ungated. An
|
|
67
|
+
* agent stuck in a retry loop is the exact caller most likely to reach for this
|
|
68
|
+
* channel, and an ungated unbounded append from a loop fills a disk. Refused as
|
|
69
|
+
* a usage error, so the caller is told rather than truncated silently.
|
|
70
|
+
*/
|
|
71
|
+
export declare const MAX_ENTRY_BYTES: number;
|
|
72
|
+
/** The actor recorded when nobody said who was writing. */
|
|
73
|
+
export declare const UNATTRIBUTED_ACTOR = "unattributed";
|
|
74
|
+
/** One journal entry, as it sits on disk. */
|
|
75
|
+
export interface JournalEntry {
|
|
76
|
+
/** RFC 3339, from the runtime clock at the moment of the append. */
|
|
77
|
+
readonly ts: string;
|
|
78
|
+
/** Who wrote it: `agent:<id>`, `human:<id>`, or {@link UNATTRIBUTED_ACTOR}. */
|
|
79
|
+
readonly actor: string;
|
|
80
|
+
/** The task this was written during, when the caller knew one. */
|
|
81
|
+
readonly task?: string;
|
|
82
|
+
/** The session this was written during, when the caller knew one. */
|
|
83
|
+
readonly session?: string;
|
|
84
|
+
/** The entry itself. Opaque. Nothing in this codebase reads it as anything. */
|
|
85
|
+
readonly text: string;
|
|
86
|
+
}
|
|
87
|
+
/** One entry as read back, with the file it came from. */
|
|
88
|
+
export interface ReadEntry extends JournalEntry {
|
|
89
|
+
/** The file's basename, which is the UTC date it was written on. */
|
|
90
|
+
readonly date: string;
|
|
91
|
+
}
|
|
92
|
+
export type AppendOutcome = {
|
|
93
|
+
ok: true;
|
|
94
|
+
entry: JournalEntry;
|
|
95
|
+
path: string;
|
|
96
|
+
} | {
|
|
97
|
+
ok: false;
|
|
98
|
+
code: "empty" | "too-large" | "io";
|
|
99
|
+
message: string;
|
|
100
|
+
};
|
|
101
|
+
/** `YYYY-MM-DD` for an RFC 3339 instant. */
|
|
102
|
+
export declare function journalDate(ts: string): string;
|
|
103
|
+
/** The file one instant's entry belongs in. */
|
|
104
|
+
export declare function journalFile(dir: string, ts: string): string;
|
|
105
|
+
export interface AppendOptions extends ClockOptions {
|
|
106
|
+
readonly actor?: string;
|
|
107
|
+
readonly task?: string;
|
|
108
|
+
readonly session?: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Append one entry. The whole write path, start to finish.
|
|
112
|
+
*
|
|
113
|
+
* The timestamp comes from the runtime clock rather than from the caller, for
|
|
114
|
+
* the ordinary reason the rest of the runtime does it that way — a record whose
|
|
115
|
+
* time its subject authored is a record about a moment of their choosing —
|
|
116
|
+
* though here nothing is judged by it, so it is a convention rather than a
|
|
117
|
+
* defence.
|
|
118
|
+
*/
|
|
119
|
+
export declare function appendJournal(dir: string, text: string, options?: AppendOptions): AppendOutcome;
|
|
120
|
+
export interface ReadOptions {
|
|
121
|
+
/** How many entries to return, newest last. */
|
|
122
|
+
readonly limit?: number;
|
|
123
|
+
/** Only entries written on or after this `YYYY-MM-DD`. */
|
|
124
|
+
readonly since?: string;
|
|
125
|
+
}
|
|
126
|
+
export type ReadOutcome = {
|
|
127
|
+
ok: true;
|
|
128
|
+
entries: ReadEntry[];
|
|
129
|
+
files: string[];
|
|
130
|
+
total: number;
|
|
131
|
+
} | {
|
|
132
|
+
ok: false;
|
|
133
|
+
code: "io";
|
|
134
|
+
message: string;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Read entries back, oldest first.
|
|
138
|
+
*
|
|
139
|
+
* A line that does not parse is SKIPPED rather than refused: this file has no
|
|
140
|
+
* chain and no writer guarantee, so one torn line is one lost entry and not
|
|
141
|
+
* evidence about anything. Refusing the whole read would let a single bad append
|
|
142
|
+
* silence the channel, which is the failure this feature exists to prevent.
|
|
143
|
+
*/
|
|
144
|
+
export declare function readJournal(dir: string, options?: ReadOptions): ReadOutcome;
|