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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull-based subscription to the verified event log (APRV-322).
|
|
3
|
+
*
|
|
4
|
+
* A filesystem notification is only a hint that another read may be useful.
|
|
5
|
+
* Every emitted batch comes from one complete, genesis-to-head verification;
|
|
6
|
+
* a notification, a parsed line, or an intact prefix is never authority by
|
|
7
|
+
* itself. The iterator queues no events: while a consumer is slow it retains
|
|
8
|
+
* only the current verified snapshot and coalesces every wakeup into one bit.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, watch } from "node:fs";
|
|
11
|
+
import { dirname } from "node:path";
|
|
12
|
+
import { verifyText } from "./verify.js";
|
|
13
|
+
/** A terminal subscription failure, mapped by the CLI to the existing exits. */
|
|
14
|
+
export class LogSubscriptionError extends Error {
|
|
15
|
+
kind;
|
|
16
|
+
reason;
|
|
17
|
+
constructor(kind, message, reason = null) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "LogSubscriptionError";
|
|
20
|
+
this.kind = kind;
|
|
21
|
+
this.reason = reason;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
25
|
+
function readSnapshot(logPath) {
|
|
26
|
+
try {
|
|
27
|
+
return readFileSync(logPath, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
if (cause.code === "ENOENT")
|
|
31
|
+
return "";
|
|
32
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
33
|
+
throw new LogSubscriptionError("io", `log ${logPath} could not be read: ${detail}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function checkedSnapshot(logPath) {
|
|
37
|
+
const verified = verifyText(logPath, readSnapshot(logPath));
|
|
38
|
+
if (verified.result.status === "torn-tail") {
|
|
39
|
+
throw new LogSubscriptionError("torn-tail", verified.result.message);
|
|
40
|
+
}
|
|
41
|
+
if (verified.result.status === "corrupt") {
|
|
42
|
+
throw new LogSubscriptionError("integrity", verified.result.message, verified.result.reason);
|
|
43
|
+
}
|
|
44
|
+
return verified.records;
|
|
45
|
+
}
|
|
46
|
+
function validateOptions(options) {
|
|
47
|
+
const from = options.from ?? 0;
|
|
48
|
+
if (!Number.isSafeInteger(from) || from < 0) {
|
|
49
|
+
throw new TypeError(`from must be a non-negative safe integer, got ${String(from)}`);
|
|
50
|
+
}
|
|
51
|
+
if (options.expectedHash !== undefined) {
|
|
52
|
+
if (from === 0)
|
|
53
|
+
throw new TypeError("expectedHash requires from to be greater than zero");
|
|
54
|
+
if (!/^[a-f0-9]{64}$/u.test(options.expectedHash)) {
|
|
55
|
+
throw new TypeError("expectedHash must be a lowercase 64-character SHA-256 digest");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
59
|
+
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 1) {
|
|
60
|
+
throw new TypeError(`pollIntervalMs must be a positive safe integer, got ${String(pollIntervalMs)}`);
|
|
61
|
+
}
|
|
62
|
+
return { from, expectedHash: options.expectedHash, pollIntervalMs };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Yield verified records after an exclusive cursor, then wait for appends.
|
|
66
|
+
*
|
|
67
|
+
* The optional expected hash binds the first read to the caller's stored
|
|
68
|
+
* cursor. After every yield, the iterator carries that same binding forward,
|
|
69
|
+
* so truncation or replacement during one process lifetime is also refused.
|
|
70
|
+
*/
|
|
71
|
+
export async function* subscribeVerifiedLog(logPath, options = {}) {
|
|
72
|
+
const parsed = validateOptions(options);
|
|
73
|
+
let cursorSeq = parsed.from;
|
|
74
|
+
let cursorHash = parsed.expectedHash;
|
|
75
|
+
let wakeVersion = 0;
|
|
76
|
+
let wake = null;
|
|
77
|
+
let watcher = null;
|
|
78
|
+
let timer = null;
|
|
79
|
+
const hint = () => {
|
|
80
|
+
wakeVersion += 1;
|
|
81
|
+
const pending = wake;
|
|
82
|
+
wake = null;
|
|
83
|
+
pending?.();
|
|
84
|
+
};
|
|
85
|
+
const abort = () => hint();
|
|
86
|
+
options.signal?.addEventListener("abort", abort, { once: false });
|
|
87
|
+
// Watch the directory so creation of an initially absent log is visible.
|
|
88
|
+
// Failure to establish a watch is harmless: bounded polling remains active.
|
|
89
|
+
try {
|
|
90
|
+
watcher = watch(dirname(logPath), { persistent: false }, hint);
|
|
91
|
+
watcher.on("error", hint);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
watcher = null;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
while (!options.signal?.aborted) {
|
|
98
|
+
const versionBeforeRead = wakeVersion;
|
|
99
|
+
const records = checkedSnapshot(logPath);
|
|
100
|
+
const headSeq = records.at(-1)?.seq ?? 0;
|
|
101
|
+
if (cursorSeq > headSeq) {
|
|
102
|
+
throw new LogSubscriptionError("integrity", `log ${logPath} ends at seq ${String(headSeq)}, before subscription cursor seq ${String(cursorSeq)}: records have been removed or this cursor belongs to another log`, "cursor-mismatch");
|
|
103
|
+
}
|
|
104
|
+
if (cursorSeq > 0) {
|
|
105
|
+
const actual = records[cursorSeq - 1];
|
|
106
|
+
if (actual === undefined || (cursorHash !== undefined && actual.hash !== cursorHash)) {
|
|
107
|
+
throw new LogSubscriptionError("integrity", `log ${logPath} does not match subscription cursor seq ${String(cursorSeq)}${cursorHash === undefined ? "" : ` hash ${cursorHash}`}: the retained prefix was truncated, replaced, or belongs to another log`, "cursor-mismatch");
|
|
108
|
+
}
|
|
109
|
+
// A sequence-only bootstrap is weaker on its first read, by design,
|
|
110
|
+
// but once this process has verified that prefix it can retain the
|
|
111
|
+
// actual digest and detect any later replacement even while caught up.
|
|
112
|
+
cursorHash ??= actual.hash;
|
|
113
|
+
}
|
|
114
|
+
// No slice: it would duplicate an arbitrarily large suffix. The verified
|
|
115
|
+
// snapshot is the only event-bearing allocation retained by this batch.
|
|
116
|
+
for (let index = cursorSeq; index < records.length; index += 1) {
|
|
117
|
+
if (options.signal?.aborted)
|
|
118
|
+
return;
|
|
119
|
+
const record = records[index];
|
|
120
|
+
if (record === undefined)
|
|
121
|
+
break;
|
|
122
|
+
// The yielded object is ordinary mutable JavaScript. Capture the
|
|
123
|
+
// verified cursor before handing it to untrusted consumer code, so a
|
|
124
|
+
// mutation cannot change where this iterator resumes or what prefix it
|
|
125
|
+
// binds on the next verification.
|
|
126
|
+
const verifiedSeq = record.seq;
|
|
127
|
+
const verifiedHash = record.hash;
|
|
128
|
+
yield record;
|
|
129
|
+
cursorSeq = verifiedSeq;
|
|
130
|
+
cursorHash = verifiedHash;
|
|
131
|
+
}
|
|
132
|
+
if (options.signal?.aborted)
|
|
133
|
+
return;
|
|
134
|
+
// An append between watcher setup/read/drain and this point changes the
|
|
135
|
+
// version and causes an immediate verification instead of a lost wakeup.
|
|
136
|
+
if (wakeVersion !== versionBeforeRead)
|
|
137
|
+
continue;
|
|
138
|
+
const versionBeforeWait = wakeVersion;
|
|
139
|
+
await new Promise((resolve) => {
|
|
140
|
+
wake = resolve;
|
|
141
|
+
timer = setTimeout(hint, parsed.pollIntervalMs);
|
|
142
|
+
// Close the last race: an abort or append may land after the check
|
|
143
|
+
// above and before `wake` is installed. Its version change must turn
|
|
144
|
+
// into an immediate retry, not a wait for the polling fallback.
|
|
145
|
+
if (options.signal?.aborted || wakeVersion !== versionBeforeWait)
|
|
146
|
+
hint();
|
|
147
|
+
});
|
|
148
|
+
if (timer !== null)
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
timer = null;
|
|
151
|
+
wake = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (timer !== null)
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
wake = null;
|
|
158
|
+
watcher?.close();
|
|
159
|
+
options.signal?.removeEventListener("abort", abort);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=log-subscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-subscribe.js","sourceRoot":"","sources":["../../../src/core/log-subscribe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only event log writer (SPEC.md §8, `.approval/log/events.jsonl`).
|
|
3
|
+
*
|
|
4
|
+
* The log is the truth. This module is the *only* sanctioned way to put a line
|
|
5
|
+
* into it, and its public API deliberately exposes no mutation, reorder,
|
|
6
|
+
* rewrite, or truncate operation — there is nothing here to call that could
|
|
7
|
+
* disturb an existing byte. Reading, verifying, and projecting live elsewhere.
|
|
8
|
+
*
|
|
9
|
+
* Guarantees, in the order they are enforced by {@link appendEvent}:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Exclusive access.** A dependency-free advisory lockfile (`<log>.lock`,
|
|
12
|
+
* created `wx`) serializes the read-tail → compute → write sequence, so two
|
|
13
|
+
* concurrent appenders cannot both read seq N and both write seq N+1.
|
|
14
|
+
* 1b. **Compare-and-append (APRV-20 finding B1).** The lock serializes *writes*,
|
|
15
|
+
* but every caller that checks the log before appending — "no live request
|
|
16
|
+
* exists", "this token is unspent", "the budget has room" — made that check
|
|
17
|
+
* outside the lock, against a log that could have moved on before its own
|
|
18
|
+
* append took the lock. {@link AppendOptions.expectedHead} closes that
|
|
19
|
+
* window: the caller states the `(seq, hash)` it read, this module compares
|
|
20
|
+
* it against the actual tail **under the lock**, and refuses `head-moved`
|
|
21
|
+
* when they differ. Nothing is written. The refusal is deliberately *not*
|
|
22
|
+
* retried here: only the caller knows whether re-deriving its decision
|
|
23
|
+
* against the newer log is safe, so the core reports and stops.
|
|
24
|
+
* 2. **Refuse to build on a corrupt tail.** If the file's last line is
|
|
25
|
+
* truncated (no terminating newline) or unparseable, the append is
|
|
26
|
+
* rejected. Chaining onto a half-written record would bake the corruption
|
|
27
|
+
* into every subsequent hash. Since APRV-206 that tail is read from the END
|
|
28
|
+
* of the file rather than by reading the whole log: an append needs the last
|
|
29
|
+
* line and nothing else, and paying one full read of an ever-growing file per
|
|
30
|
+
* append put log length into the latency of every grant. Every refusal above
|
|
31
|
+
* is unchanged, byte for byte.
|
|
32
|
+
* 3. **The runtime stamps the chain fields.** Callers supply content only;
|
|
33
|
+
* `seq`, `prev`, `alg`, and `hash` are computed here. `alg` is always
|
|
34
|
+
* `sha256/jcs` (SPEC.md §8 defines exactly one value at v0.1).
|
|
35
|
+
* 4. **Validate at the write boundary.** The complete record — chain fields
|
|
36
|
+
* included — must pass the `event` JSON Schema before any byte is written.
|
|
37
|
+
* On failure the file is left byte-identical and a structured error is
|
|
38
|
+
* returned. Fail closed: nothing here throws a validation problem past the
|
|
39
|
+
* caller as a partially-written line.
|
|
40
|
+
* 5. **One line, one write.** The stored line is the JCS canonicalization of
|
|
41
|
+
* the complete record (`hash` included); the digest input is that same
|
|
42
|
+
* canonicalization minus the `hash` field. One scheme, two inputs — a
|
|
43
|
+
* verifier strips `hash` and re-derives. Appended with a single `write(2)`
|
|
44
|
+
* on a handle opened `O_APPEND`.
|
|
45
|
+
*
|
|
46
|
+
* Determinism: `ts` is supplied by the caller. This module never reads the
|
|
47
|
+
* clock, because a hash-relevant field sourced from ambient state would make
|
|
48
|
+
* the log irreproducible.
|
|
49
|
+
*/
|
|
50
|
+
import { type ValidateOptions, type ValidationError } from "./validate.js";
|
|
51
|
+
/** SPEC.md §8: the hash scheme identifier stamped on every v0.1 record. */
|
|
52
|
+
export declare const ALG = "sha256/jcs";
|
|
53
|
+
/**
|
|
54
|
+
* SPEC.md §8: `prev` of the first record in a log. `null`, not a zero digest —
|
|
55
|
+
* the schema and the `genesis-null-prev` fixture both encode this choice.
|
|
56
|
+
*/
|
|
57
|
+
export declare const GENESIS_PREV: null;
|
|
58
|
+
/**
|
|
59
|
+
* The closed set of event types (SPEC.md §8, mirrored by the schema enum).
|
|
60
|
+
*
|
|
61
|
+
* `payload.pruned` (APRV-38) is the first addition after the v0.1 draft set of
|
|
62
|
+
* sixteen: the daemon appends one per payload file it removes under
|
|
63
|
+
* `payload_retention`, so a log states what its payload store no longer holds.
|
|
64
|
+
* `approval.withdrawn` (APRV-106) is the second: the requester's retraction of
|
|
65
|
+
* a request nobody has answered yet (amended SPEC.md §6.3).
|
|
66
|
+
* `execution.indeterminate` and `execution.reconciled` (APRV-120) are the third
|
|
67
|
+
* and fourth: a side effect that was attempted and whose outcome is unknown, and
|
|
68
|
+
* the human resolution that later says which it was (amended SPEC.md §6.3,
|
|
69
|
+
* §10.4).
|
|
70
|
+
* `reconciliation.required` / `reconciliation.satisfied` (APRV-127) are the
|
|
71
|
+
* fifth and sixth: the obligation a retrospective DENIAL creates, and the
|
|
72
|
+
* human act that discharges it (amended SPEC.md §5.2).
|
|
73
|
+
* `policy.proposed` / `policy.declined` (APRV-109) are the seventh and eighth:
|
|
74
|
+
* an agent asking a human to attest prepared policy bytes, and the human's
|
|
75
|
+
* refusal (amended SPEC.md §10.1, §10.3). The acceptance is `policy.updated`,
|
|
76
|
+
* which already existed and is still the only event an attestation is.
|
|
77
|
+
* `audit.dark_session` (APRV-192) is the ninth: the daemon recording git
|
|
78
|
+
* activity it can see for which the log carries no corresponding record
|
|
79
|
+
* (amended SPEC.md §10.2). It is an observation the RUNTIME makes about a
|
|
80
|
+
* session, never a record written on a session's behalf, so it carries a
|
|
81
|
+
* `system:` actor and the schema refuses any other.
|
|
82
|
+
* `gate.opened`, `gate.closed` and `gate.bypassed` (APRV-214) are the tenth,
|
|
83
|
+
* eleventh and twelfth: a human time-boxing a full harness bypass so the gate
|
|
84
|
+
* itself can be debugged, that human closing it early, and one gated tool call
|
|
85
|
+
* the hook allowed while it stood (amended SPEC.md §5.2). The window's whole
|
|
86
|
+
* state is these records, deliberately: a file the runtime read on its own
|
|
87
|
+
* would let anything able to write it act as the human. The first two carry a
|
|
88
|
+
* `human:` actor and the schema refuses any other.
|
|
89
|
+
* `log.checkpoint` (APRV-220) is the thirteenth: a human's signature over the
|
|
90
|
+
* chain head at a moment, made with a key no agent process holds. The chain is
|
|
91
|
+
* unkeyed, so a party with write access to this file can truncate it and
|
|
92
|
+
* recompute a self-consistent forgery; a checkpoint is a witness that survives
|
|
93
|
+
* them, because a rewritten chain cannot reproduce a signature over the hashes
|
|
94
|
+
* it replaced. Human actor, for the reason `gate.opened` carries one.
|
|
95
|
+
* `audit.decision_refused` (APRV-235) is the fourteenth: a human decided
|
|
96
|
+
* through a channel or the CLI and the gate refused to record the decision, so
|
|
97
|
+
* the log states that the answer was given and could not be taken (amended
|
|
98
|
+
* SPEC.md §5.2). Audit tier — it grants nothing, and no verdict, budget, streak
|
|
99
|
+
* or sampling path reads it. `system:` actor, like `audit.dark_session`: it is
|
|
100
|
+
* the runtime's statement about its own refusal, and the human whose decision
|
|
101
|
+
* it was is named in the payload. Refusals handed to AGENTS are not recorded;
|
|
102
|
+
* the asymmetry is deliberate, and `core/decision-refusal.ts` states why.
|
|
103
|
+
* `gate.organ.attested` (APRV-272) is the fifteenth: a human's sign-off on the
|
|
104
|
+
* exact bytes of one of the gate's ORGANS, the harness files that install the
|
|
105
|
+
* hook (amended SPEC.md §5.2, §8). Those files are `policy.core`, which is
|
|
106
|
+
* human-only, so the gate mints no record of any kind for them and no
|
|
107
|
+
* grant-shaped evidence for a hand edit to one can exist; content attestation
|
|
108
|
+
* is the only evidence there is, and this is it. `human:` actor and the schema
|
|
109
|
+
* refuses any other, for the reason `gate.opened` carries the same rule.
|
|
110
|
+
*
|
|
111
|
+
* It is a type of its own rather than a `policy.updated` variant, and that is
|
|
112
|
+
* the load-bearing choice: every reader of the POLICY attestation selects on
|
|
113
|
+
* `event === "policy.updated"` (`core/attest.ts`, `core/policy-proposal.ts`,
|
|
114
|
+
* `cli/amend.ts`, `cli/channel-telegram.ts`, `core/protected-path-guard.ts`),
|
|
115
|
+
* so a distinct type is ignored by all of them by construction rather than by a
|
|
116
|
+
* filter each one would have to remember. `checkAttestationOfBytes` returns at
|
|
117
|
+
* the FIRST record carrying a `sha256` as it scans backwards, so an organ
|
|
118
|
+
* attestation written as a `policy.updated` would have reported a correctly
|
|
119
|
+
* attested policy as `hash-mismatch` and refused every gate operation until
|
|
120
|
+
* somebody re-attested it. The `gate.` prefix already carries the write-boundary
|
|
121
|
+
* clock in `core/verify.ts`, which is what an attestation's `ts` has to be.
|
|
122
|
+
*/
|
|
123
|
+
export type EventType = "task.registered" | "route.proposed" | "route.accepted" | "approval.requested" | "approval.granted" | "approval.rejected" | "approval.expired" | "approval.revoked" | "approval.withdrawn" | "execution.started" | "execution.completed" | "execution.failed" | "execution.indeterminate" | "execution.reconciled" | "budget.exceeded" | "policy.updated" | "policy.proposed" | "policy.declined" | "envelope.drift" | "audit.sampled" | "audit.reviewed" | "audit.dark_session" | "audit.decision_refused" | "reconciliation.required" | "reconciliation.satisfied" | "payload.pruned" | "gate.opened" | "gate.closed" | "gate.bypassed" | "gate.organ.attested" | "log.checkpoint";
|
|
124
|
+
/** Caller-supplied content of an event. Chain fields are not accepted. */
|
|
125
|
+
export interface EventInput {
|
|
126
|
+
/** RFC 3339 timestamp. Supplied by the caller; never read from the clock. */
|
|
127
|
+
ts: string;
|
|
128
|
+
event: EventType;
|
|
129
|
+
/** `human:`, `agent:`, or `system:` prefixed identity (SPEC.md §8). */
|
|
130
|
+
actor: string;
|
|
131
|
+
task?: string;
|
|
132
|
+
action_key?: string;
|
|
133
|
+
channel?: string;
|
|
134
|
+
payload?: Record<string, unknown>;
|
|
135
|
+
}
|
|
136
|
+
/** A complete log record: caller content plus runtime-stamped chain fields. */
|
|
137
|
+
export interface EventRecord extends EventInput {
|
|
138
|
+
seq: number;
|
|
139
|
+
alg: typeof ALG;
|
|
140
|
+
hash: string;
|
|
141
|
+
prev: string | null;
|
|
142
|
+
}
|
|
143
|
+
/** The hash input: a record with every field except `hash`. */
|
|
144
|
+
export type UnhashedRecord = Omit<EventRecord, "hash">;
|
|
145
|
+
/**
|
|
146
|
+
* Why an append was refused. Every failure is one of these, never a throw.
|
|
147
|
+
*
|
|
148
|
+
* Frozen public API in the same sense the gate's refusal codes are: callers
|
|
149
|
+
* branch on these strings, so adding one is a spec change and renaming one is a
|
|
150
|
+
* breaking change. `head-moved` is the APRV-20 addition (finding B1), sanctioned
|
|
151
|
+
* by the human decision of 2026-08-07.
|
|
152
|
+
*/
|
|
153
|
+
export declare const APPEND_ERROR_CODES: readonly [
|
|
154
|
+
/** The lockfile was held by another writer for longer than the timeout. */
|
|
155
|
+
"lock-timeout",
|
|
156
|
+
/** The file's last line is truncated or unparseable; nothing may chain onto it. */
|
|
157
|
+
"corrupt-tail",
|
|
158
|
+
/** The complete record failed the `event` schema at the write boundary. */
|
|
159
|
+
"validation",
|
|
160
|
+
/** The record could not be canonicalized (RFC 8785). */
|
|
161
|
+
"canonicalization",
|
|
162
|
+
/** The log could not be created, opened, or written. */
|
|
163
|
+
"io",
|
|
164
|
+
/**
|
|
165
|
+
* The caller supplied {@link AppendOptions.expectedHead} and the log's actual
|
|
166
|
+
* tail, read under the lock, is a different `(seq, hash)`. Someone appended
|
|
167
|
+
* between the caller's read and this append, so every read-dependent check the
|
|
168
|
+
* caller made is stale. Nothing was written.
|
|
169
|
+
*/
|
|
170
|
+
"head-moved"];
|
|
171
|
+
export type AppendErrorCode = (typeof APPEND_ERROR_CODES)[number];
|
|
172
|
+
export interface AppendError {
|
|
173
|
+
code: AppendErrorCode;
|
|
174
|
+
message: string;
|
|
175
|
+
/** Schema errors, present when `code` is "validation". */
|
|
176
|
+
errors?: ValidationError[];
|
|
177
|
+
}
|
|
178
|
+
export type AppendResult = {
|
|
179
|
+
ok: true;
|
|
180
|
+
record: EventRecord;
|
|
181
|
+
line: string;
|
|
182
|
+
} | {
|
|
183
|
+
ok: false;
|
|
184
|
+
error: AppendError;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* A chain head: the last record's position and digest.
|
|
188
|
+
*
|
|
189
|
+
* Defined here rather than in `core/verify.ts` because the writer needs it for
|
|
190
|
+
* {@link AppendOptions.expectedHead} and the writer cannot import the verifier
|
|
191
|
+
* (the verifier imports the writer). `core/verify.ts` re-exports this exact
|
|
192
|
+
* type, so there is one definition and one meaning.
|
|
193
|
+
*/
|
|
194
|
+
export interface LogHead {
|
|
195
|
+
seq: number;
|
|
196
|
+
hash: string;
|
|
197
|
+
}
|
|
198
|
+
/** Options for {@link appendEvent}. */
|
|
199
|
+
export interface AppendOptions extends ValidateOptions {
|
|
200
|
+
/** Milliseconds to keep retrying the lockfile before giving up. */
|
|
201
|
+
lockTimeoutMs?: number;
|
|
202
|
+
/** Milliseconds between lock acquisition attempts. */
|
|
203
|
+
lockRetryMs?: number;
|
|
204
|
+
/**
|
|
205
|
+
* Compare-and-append precondition (guarantee 1b in the module header).
|
|
206
|
+
*
|
|
207
|
+
* - a `LogHead` — the append proceeds only if the log's tail is exactly that
|
|
208
|
+
* `(seq, hash)`;
|
|
209
|
+
* - `null` — the append proceeds only if the log is empty or absent;
|
|
210
|
+
* - absent/`undefined` — no precondition, the pre-APRV-20 behavior.
|
|
211
|
+
*
|
|
212
|
+
* Evaluated **under the lock**, after the tail is read and before anything is
|
|
213
|
+
* computed or written. A mismatch is `head-moved` and writes nothing. Callers
|
|
214
|
+
* that made a decision from the log MUST pass the head they read; callers with
|
|
215
|
+
* no read-dependent decision (an unconditional append) legitimately omit it.
|
|
216
|
+
*/
|
|
217
|
+
expectedHead?: LogHead | null;
|
|
218
|
+
}
|
|
219
|
+
/** How long a whole-operation lock holder waits before reporting `lock-timeout`. */
|
|
220
|
+
export interface LockOptions {
|
|
221
|
+
lockTimeoutMs?: number;
|
|
222
|
+
lockRetryMs?: number;
|
|
223
|
+
}
|
|
224
|
+
/** The outcome of {@link withAppendLock}: the callback's value, or a refusal. */
|
|
225
|
+
export type LockedResult<T> = {
|
|
226
|
+
ok: true;
|
|
227
|
+
value: T;
|
|
228
|
+
} | {
|
|
229
|
+
ok: false;
|
|
230
|
+
error: AppendError;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* The record's digest under `alg: "sha256/jcs"`.
|
|
234
|
+
*
|
|
235
|
+
* Hash input = **the full record minus its `hash` property**, with `prev`
|
|
236
|
+
* included, serialized per RFC 8785 (JCS) and digested with SHA-256. Output is
|
|
237
|
+
* lowercase hex. Passing an already-hashed record is fine: the `hash` field is
|
|
238
|
+
* removed before canonicalization, so `computeRecordHash(r)` is stable whether
|
|
239
|
+
* or not `r` carries a digest.
|
|
240
|
+
*/
|
|
241
|
+
export declare function computeRecordHash(record: UnhashedRecord | EventRecord): string;
|
|
242
|
+
/**
|
|
243
|
+
* Inverse of {@link computeRecordHash}: recompute and compare. Lives beside
|
|
244
|
+
* the writer so the two can never diverge; the M1 chain verifier consumes it.
|
|
245
|
+
*/
|
|
246
|
+
export declare function verifyRecordHash(record: EventRecord): boolean;
|
|
247
|
+
/** The stored line for a record: its canonical serialization (no newline). */
|
|
248
|
+
export declare function serializeRecord(record: EventRecord): string;
|
|
249
|
+
/**
|
|
250
|
+
* Append exactly one event to `logPath`, stamping `seq`, `prev`, `alg`, and
|
|
251
|
+
* `hash`.
|
|
252
|
+
*
|
|
253
|
+
* Returns a structured result rather than throwing: an append that cannot be
|
|
254
|
+
* made safely leaves the file byte-identical and says why.
|
|
255
|
+
*
|
|
256
|
+
* Pass {@link AppendOptions.expectedHead} whenever the append is authorized by
|
|
257
|
+
* something read from the log: the head is compared under the lock and a moved
|
|
258
|
+
* head refuses `head-moved` without writing.
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* Hold `<logPath>.lock` for the whole of `run`, then release it.
|
|
262
|
+
*
|
|
263
|
+
* The lockfile exists so that a read-tail → compute → write sequence cannot
|
|
264
|
+
* interleave with another writer's. Some operations need that exclusion over a
|
|
265
|
+
* span much longer than one append: `approval log sync` (APRV-125) reads the
|
|
266
|
+
* chain, moves the file aside, lets git advance the committed baseline, and
|
|
267
|
+
* puts the chain back, and an append landing anywhere inside that window is
|
|
268
|
+
* exactly the interleaving that forked this repository's own log on 2026-08-20.
|
|
269
|
+
*
|
|
270
|
+
* The callback is handed no lock handle and no write primitive: this module
|
|
271
|
+
* still exposes nothing that mutates an existing byte, and a caller holding the
|
|
272
|
+
* lock has the same append-only API everyone else has. What it gains is the
|
|
273
|
+
* guarantee that nobody else is appending while it works.
|
|
274
|
+
*/
|
|
275
|
+
export declare function withAppendLock<T>(logPath: string, run: () => T, options?: LockOptions): LockedResult<T>;
|
|
276
|
+
/** Subscribe to successful appends. Process-wide, memory-only, additive. */
|
|
277
|
+
export declare function onLogAppended(listener: (logPath: string) => void): void;
|
|
278
|
+
export declare function appendEvent(logPath: string, input: EventInput, options?: AppendOptions): AppendResult;
|