approval-md 0.3.0 → 0.4.1
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 +83 -914
- package/SPEC.md +101 -13
- package/cli.js +134 -7
- package/dist/src/adapters/smtp.d.ts +18 -0
- package/dist/src/adapters/smtp.js +24 -3
- package/dist/src/adapters/smtp.js.map +1 -1
- package/dist/src/channels/contract.d.ts +72 -0
- package/dist/src/channels/contract.js +161 -1
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/relay-server.d.ts +90 -0
- package/dist/src/channels/relay-server.js +295 -0
- package/dist/src/channels/relay-server.js.map +1 -0
- package/dist/src/channels/relay.d.ts +273 -0
- package/dist/src/channels/relay.js +564 -0
- package/dist/src/channels/relay.js.map +1 -0
- package/dist/src/channels/tagging.d.ts +25 -0
- package/dist/src/channels/tagging.js +63 -6
- package/dist/src/channels/tagging.js.map +1 -1
- package/dist/src/channels/telegram-minimal.d.ts +251 -0
- package/dist/src/channels/telegram-minimal.js +618 -0
- package/dist/src/channels/telegram-minimal.js.map +1 -0
- package/dist/src/channels/telegram-webhook.d.ts +273 -0
- package/dist/src/channels/telegram-webhook.js +536 -0
- package/dist/src/channels/telegram-webhook.js.map +1 -0
- package/dist/src/channels/telegram.d.ts +725 -20
- package/dist/src/channels/telegram.js +1332 -123
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/cli/amend.js +358 -5
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.js +43 -6
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/audit-card.d.ts +1 -1
- package/dist/src/cli/audit-card.js +40 -1
- package/dist/src/cli/audit-card.js.map +1 -1
- package/dist/src/cli/audit.d.ts +7 -5
- package/dist/src/cli/audit.js +22 -10
- package/dist/src/cli/audit.js.map +1 -1
- package/dist/src/cli/channel-relay.d.ts +108 -0
- package/dist/src/cli/channel-relay.js +334 -0
- package/dist/src/cli/channel-relay.js.map +1 -0
- package/dist/src/cli/channel-telegram-webhook.d.ts +275 -0
- package/dist/src/cli/channel-telegram-webhook.js +716 -0
- package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
- package/dist/src/cli/channel-telegram.d.ts +347 -28
- package/dist/src/cli/channel-telegram.js +648 -56
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +1 -1
- package/dist/src/cli/channel.js +5 -1
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/daemon.js +37 -5
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.d.ts +63 -0
- package/dist/src/cli/doctor.js +856 -111
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.d.ts +29 -1
- package/dist/src/cli/execute.js +387 -21
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/exit-codes.d.ts +12 -0
- package/dist/src/cli/exit-codes.js +13 -0
- package/dist/src/cli/exit-codes.js.map +1 -1
- package/dist/src/cli/gate.d.ts +2 -0
- package/dist/src/cli/gate.js +233 -2
- package/dist/src/cli/gate.js.map +1 -1
- package/dist/src/cli/help.d.ts +10 -1
- package/dist/src/cli/help.js +283 -54
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook.d.ts +485 -6
- package/dist/src/cli/hook.js +2370 -70
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/init.js +16 -7
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.js +24 -3
- package/dist/src/cli/instructions.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +55 -0
- package/dist/src/cli/log-anchor.js +199 -60
- package/dist/src/cli/log-anchor.js.map +1 -1
- package/dist/src/cli/log-unlock.d.ts +23 -0
- package/dist/src/cli/log-unlock.js +109 -0
- package/dist/src/cli/log-unlock.js.map +1 -0
- package/dist/src/cli/main.d.ts +0 -12
- package/dist/src/cli/main.js +72 -4
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/muse.d.ts +2 -0
- package/dist/src/cli/muse.js +62 -0
- package/dist/src/cli/muse.js.map +1 -0
- package/dist/src/cli/payload.js +79 -1
- package/dist/src/cli/payload.js.map +1 -1
- package/dist/src/cli/scaffold.js +6 -0
- package/dist/src/cli/scaffold.js.map +1 -1
- package/dist/src/cli/serve.d.ts +53 -0
- package/dist/src/cli/serve.js +270 -0
- package/dist/src/cli/serve.js.map +1 -0
- package/dist/src/cli/up.js +26 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +2 -0
- package/dist/src/cli/verb-registry.js +338 -39
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/core/attest-bootstrap.d.ts +95 -0
- package/dist/src/core/attest-bootstrap.js +135 -0
- package/dist/src/core/attest-bootstrap.js.map +1 -0
- package/dist/src/core/attest.d.ts +20 -3
- package/dist/src/core/attest.js +25 -0
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +153 -12
- package/dist/src/core/audit.js +415 -20
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-lease.d.ts +322 -0
- package/dist/src/core/channel-lease.js +663 -0
- package/dist/src/core/channel-lease.js.map +1 -0
- package/dist/src/core/command-class.d.ts +51 -0
- package/dist/src/core/command-class.js +766 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/daemon-host.d.ts +178 -0
- package/dist/src/core/daemon-host.js +228 -0
- package/dist/src/core/daemon-host.js.map +1 -0
- package/dist/src/core/daemon-identity.d.ts +238 -0
- package/dist/src/core/daemon-identity.js +252 -0
- package/dist/src/core/daemon-identity.js.map +1 -0
- package/dist/src/core/gate.d.ts +364 -6
- package/dist/src/core/gate.js +802 -27
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/harness-version.d.ts +128 -4
- package/dist/src/core/harness-version.js +179 -12
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +82 -3
- package/dist/src/core/harness-wait.js +89 -3
- package/dist/src/core/harness-wait.js.map +1 -1
- package/dist/src/core/log-lock.d.ts +435 -0
- package/dist/src/core/log-lock.js +1093 -0
- package/dist/src/core/log-lock.js.map +1 -0
- package/dist/src/core/log-subscribe.d.ts +21 -0
- package/dist/src/core/log-subscribe.js +16 -6
- package/dist/src/core/log-subscribe.js.map +1 -1
- package/dist/src/core/log-write-layer.d.ts +55 -0
- package/dist/src/core/log-write-layer.js +91 -0
- package/dist/src/core/log-write-layer.js.map +1 -0
- package/dist/src/core/log.d.ts +130 -22
- package/dist/src/core/log.js +473 -50
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/payload-census.d.ts +51 -0
- package/dist/src/core/payload-census.js +125 -2
- package/dist/src/core/payload-census.js.map +1 -1
- package/dist/src/core/payload-store.d.ts +31 -0
- package/dist/src/core/payload-store.js +113 -8
- package/dist/src/core/payload-store.js.map +1 -1
- package/dist/src/core/payload.d.ts +18 -3
- package/dist/src/core/payload.js +20 -4
- package/dist/src/core/payload.js.map +1 -1
- package/dist/src/core/policy-diff.d.ts +10 -0
- package/dist/src/core/policy-diff.js +10 -2
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +7 -1
- package/dist/src/core/policy-explain.js +16 -1
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +24 -0
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +38 -0
- package/dist/src/core/policy-match.js +35 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +40 -0
- package/dist/src/core/policy-proposal.js +37 -16
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/prompt-layout.d.ts +186 -1
- package/dist/src/core/prompt-layout.js +345 -3
- package/dist/src/core/prompt-layout.js.map +1 -1
- package/dist/src/core/read-scope.d.ts +10 -0
- package/dist/src/core/read-scope.js +4 -0
- package/dist/src/core/read-scope.js.map +1 -1
- package/dist/src/core/run-payload.d.ts +160 -0
- package/dist/src/core/run-payload.js +304 -0
- package/dist/src/core/run-payload.js.map +1 -0
- package/dist/src/core/sender-identity.d.ts +59 -11
- package/dist/src/core/sender-identity.js +79 -11
- package/dist/src/core/sender-identity.js.map +1 -1
- package/dist/src/core/state.d.ts +52 -1
- package/dist/src/core/state.js +52 -6
- package/dist/src/core/state.js.map +1 -1
- package/dist/src/core/telegram-config.d.ts +79 -0
- package/dist/src/core/telegram-config.js +108 -0
- package/dist/src/core/telegram-config.js.map +1 -1
- package/dist/src/core/validate.d.ts +20 -6
- package/dist/src/core/validate.js +0 -0
- package/dist/src/core/validate.js.map +1 -1
- package/dist/src/core/verify.d.ts +23 -1
- package/dist/src/core/verify.js +20 -2
- package/dist/src/core/verify.js.map +1 -1
- package/dist/src/daemon/audit.d.ts +80 -5
- package/dist/src/daemon/audit.js +126 -6
- package/dist/src/daemon/audit.js.map +1 -1
- package/dist/src/daemon/daemon.d.ts +174 -2
- package/dist/src/daemon/daemon.js +298 -7
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +13 -2
- package/dist/src/daemon/projection.js +39 -6
- package/dist/src/daemon/projection.js.map +1 -1
- package/dist/src/mcp/server.d.ts +39 -3
- package/dist/src/mcp/server.js +22 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/muse/server.d.ts +16 -0
- package/dist/src/muse/server.js +238 -0
- package/dist/src/muse/server.js.map +1 -0
- package/dist/src/serve/archive.d.ts +166 -0
- package/dist/src/serve/archive.js +363 -0
- package/dist/src/serve/archive.js.map +1 -0
- package/dist/src/serve/arguments.d.ts +93 -0
- package/dist/src/serve/arguments.js +228 -0
- package/dist/src/serve/arguments.js.map +1 -0
- package/dist/src/serve/credentials.d.ts +91 -0
- package/dist/src/serve/credentials.js +143 -0
- package/dist/src/serve/credentials.js.map +1 -0
- package/dist/src/serve/follow.d.ts +86 -0
- package/dist/src/serve/follow.js +101 -0
- package/dist/src/serve/follow.js.map +1 -0
- package/dist/src/serve/hook-thread.d.ts +134 -0
- package/dist/src/serve/hook-thread.js +333 -0
- package/dist/src/serve/hook-thread.js.map +1 -0
- package/dist/src/serve/hook-worker.d.ts +134 -0
- package/dist/src/serve/hook-worker.js +173 -0
- package/dist/src/serve/hook-worker.js.map +1 -0
- package/dist/src/serve/server.d.ts +412 -0
- package/dist/src/serve/server.js +1251 -0
- package/dist/src/serve/server.js.map +1 -0
- package/docs/README-extended.md +768 -0
- package/docs/cli-reference.md +2139 -67
- package/package.json +4 -2
- package/schema/event.schema.json +413 -30
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
- package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
- package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
- package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
- package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
- package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
- package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
- package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
- package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
- package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
- package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
- package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
- package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
- package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
- package/schema/fixtures/event/valid/audit-sampled.json +2 -1
- package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
- package/schema/fixtures/event/valid/daemon-identity.json +16 -0
- package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
- package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
- package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
- package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
- package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
- package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
- package/schema/policy.schema.json +102 -6
|
@@ -54,7 +54,14 @@
|
|
|
54
54
|
import { spawnSync } from "node:child_process";
|
|
55
55
|
import { childEnvironment } from "./child-env.js";
|
|
56
56
|
/** The harnesses this runtime speaks a hook protocol for. */
|
|
57
|
-
export const HARNESS_KINDS = [
|
|
57
|
+
export const HARNESS_KINDS = [
|
|
58
|
+
"claude-code",
|
|
59
|
+
"cursor",
|
|
60
|
+
"codex",
|
|
61
|
+
"grok",
|
|
62
|
+
"muse",
|
|
63
|
+
"hermes",
|
|
64
|
+
];
|
|
58
65
|
/** Is `value` one of the harnesses this runtime knows? */
|
|
59
66
|
export function isHarnessKind(value) {
|
|
60
67
|
return typeof value === "string" && HARNESS_KINDS.includes(value);
|
|
@@ -71,6 +78,7 @@ export const HARNESS_BINARY = {
|
|
|
71
78
|
codex: "codex",
|
|
72
79
|
grok: "grok",
|
|
73
80
|
muse: "muse",
|
|
81
|
+
hermes: "hermes",
|
|
74
82
|
};
|
|
75
83
|
/**
|
|
76
84
|
* How long a version string may be, and the reason there is a cap at all.
|
|
@@ -121,7 +129,40 @@ export function normalizeHarnessVersion(raw) {
|
|
|
121
129
|
*/
|
|
122
130
|
export const HARNESS_PROBE_TIMEOUT_MS = 10_000;
|
|
123
131
|
/**
|
|
124
|
-
*
|
|
132
|
+
* How long the RAW first line of `<binary> --version` may be.
|
|
133
|
+
*
|
|
134
|
+
* Wider than {@link HARNESS_VERSION_LIMIT} because it holds a different kind of
|
|
135
|
+
* value for a different reader: nothing capped by this number is ever appended to
|
|
136
|
+
* the log (only {@link normalizeHarnessVersion}'s output is), and what needs the
|
|
137
|
+
* raw line is a diagnostic that must be able to read a build stamp a harness
|
|
138
|
+
* prints beside its semver. Hermes prints
|
|
139
|
+
* `Hermes Agent v0.21.3 (2026.9.14) · upstream 913d4098`, which the log's own
|
|
140
|
+
* rule rejects on the non-ASCII separator alone. Capped and stripped of control
|
|
141
|
+
* characters all the same: this is third-party output on its way to a terminal.
|
|
142
|
+
*/
|
|
143
|
+
export const HARNESS_RAW_VERSION_LIMIT = 200;
|
|
144
|
+
/**
|
|
145
|
+
* The first line of a `--version` output, as a line a diagnostic may print.
|
|
146
|
+
*
|
|
147
|
+
* One line, trimmed, control characters removed, capped. `null` for empty output
|
|
148
|
+
* or a line past the cap. It keeps non-ASCII bytes, which is the whole difference
|
|
149
|
+
* from {@link normalizeHarnessVersion}: a build stamp behind a typographic
|
|
150
|
+
* separator is exactly the fact a version floor has to read, and it is exactly
|
|
151
|
+
* the fact the write boundary will not accept.
|
|
152
|
+
*/
|
|
153
|
+
export function rawHarnessVersionLine(raw) {
|
|
154
|
+
if (typeof raw !== "string")
|
|
155
|
+
return null;
|
|
156
|
+
const first = raw.split("\n", 1)[0] ?? "";
|
|
157
|
+
// `\p{Cc}` rather than an escaped range: the class is the Unicode CONTROL
|
|
158
|
+
// category, which is the thing being removed, and it reads as that.
|
|
159
|
+
const text = first.replace(/\p{Cc}/gu, "").trim();
|
|
160
|
+
if (text.length === 0 || text.length > HARNESS_RAW_VERSION_LIMIT)
|
|
161
|
+
return null;
|
|
162
|
+
return text;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Run `<binary> --version` once, uncached, and return its raw first line.
|
|
125
166
|
*
|
|
126
167
|
* Every failure is a value rather than an exception, in the manner of
|
|
127
168
|
* {@link spawnGloss}: a missing binary, a non-zero exit and a timeout kill are
|
|
@@ -132,7 +173,7 @@ export const HARNESS_PROBE_TIMEOUT_MS = 10_000;
|
|
|
132
173
|
* vault passphrase, and it has no use for either. Nothing is declared, because
|
|
133
174
|
* reading a version is not a granted action.
|
|
134
175
|
*/
|
|
135
|
-
export function
|
|
176
|
+
export function probeHarnessVersionRaw(kind) {
|
|
136
177
|
let result;
|
|
137
178
|
try {
|
|
138
179
|
result = spawnSync(HARNESS_BINARY[kind], ["--version"], {
|
|
@@ -150,32 +191,158 @@ export function probeHarnessVersion(kind) {
|
|
|
150
191
|
}
|
|
151
192
|
if (result.error !== undefined || result.status !== 0)
|
|
152
193
|
return null;
|
|
153
|
-
return
|
|
194
|
+
return rawHarnessVersionLine(result.stdout);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Run `<binary> --version` once, uncached, and normalize what came back.
|
|
198
|
+
*
|
|
199
|
+
* The recordable form: one line, printable ASCII, capped. Exported for the test
|
|
200
|
+
* that proves the memo below is a memo.
|
|
201
|
+
*/
|
|
202
|
+
export function probeHarnessVersion(kind) {
|
|
203
|
+
return normalizeHarnessVersion(probeHarnessVersionRaw(kind));
|
|
154
204
|
}
|
|
155
205
|
/**
|
|
156
206
|
* The memo. Holds the FAILURES too: a harness that is not on PATH is not on
|
|
157
207
|
* PATH, and asking a second time in the same process would pay a second spawn
|
|
158
208
|
* for the same `null`.
|
|
159
|
-
*/
|
|
160
|
-
const probed = new Map();
|
|
161
|
-
/**
|
|
162
|
-
* The installed version of `kind`, read at most once per process.
|
|
163
209
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
210
|
+
* It holds the RAW line rather than the recordable one, so the two readers — the
|
|
211
|
+
* provenance field and the version floor — still cost ONE spawn per process
|
|
212
|
+
* between them. A harness whose raw line the write boundary rejects has a
|
|
213
|
+
* `null` recordable version and a readable raw one, which is precisely Hermes's
|
|
214
|
+
* case and precisely why the floor is not read off a record.
|
|
166
215
|
*/
|
|
167
|
-
|
|
216
|
+
const probed = new Map();
|
|
217
|
+
/** The raw first line of `<binary> --version`, read at most once per process. */
|
|
218
|
+
export function installedHarnessVersionRaw(kind) {
|
|
168
219
|
const memo = probed.get(kind);
|
|
169
220
|
if (memo !== undefined)
|
|
170
221
|
return memo;
|
|
171
|
-
const value =
|
|
222
|
+
const value = probeHarnessVersionRaw(kind);
|
|
172
223
|
probed.set(kind, value);
|
|
173
224
|
return value;
|
|
174
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* The installed version of `kind`, read at most once per process.
|
|
228
|
+
*
|
|
229
|
+
* This is the only entry point callers should use for a RECORDABLE version.
|
|
230
|
+
* `probeHarnessVersion` is exported for the test that proves the memo is a memo.
|
|
231
|
+
*/
|
|
232
|
+
export function installedHarnessVersion(kind) {
|
|
233
|
+
return normalizeHarnessVersion(installedHarnessVersionRaw(kind));
|
|
234
|
+
}
|
|
175
235
|
/** Drop the memo. TEST ONLY: a process reads a version once, by design. */
|
|
176
236
|
export function resetHarnessVersionCache() {
|
|
177
237
|
probed.clear();
|
|
178
238
|
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// The Hermes fail-closed version floor (APRV-415)
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* The build at which Hermes Agent starts honouring `fail_closed` (APRV-415).
|
|
244
|
+
*
|
|
245
|
+
* ## Why a floor exists at all, and why it is a DATE rather than a version
|
|
246
|
+
*
|
|
247
|
+
* `approval hook hermes` is enforcement rather than a backstop only because of a
|
|
248
|
+
* per-entry `fail_closed: true` that makes a hook crash, a hook timeout and
|
|
249
|
+
* unparseable hook output BLOCK. The live probe measured both halves of that on
|
|
250
|
+
* two builds of the same harness, and the answer differs between them:
|
|
251
|
+
*
|
|
252
|
+
* - `main` at `118984d7`, built 2026-09-20: crash, garbage and hang were all
|
|
253
|
+
* REFUSED (the hang at the 300s per-entry cap). The key works;
|
|
254
|
+
* - `v0.21.3`, built 2026.9.14: all three PROCEEDED. That build does not know the
|
|
255
|
+
* key and ignores it SILENTLY. `hermes hooks list` renders no `fail_closed`
|
|
256
|
+
* flag on either build, so the listing cannot be used to tell them apart.
|
|
257
|
+
*
|
|
258
|
+
* The two builds report the SAME semver. `hermes --version` on the older one
|
|
259
|
+
* prints `Hermes Agent v0.21.3 (2026.9.14) · upstream 913d4098`, and the update
|
|
260
|
+
* that fixed the behaviour moved 670 commits without moving `0.21.3`. So a floor
|
|
261
|
+
* expressed as a semantic version would compare the one field that did not
|
|
262
|
+
* change, and would pass a build that fails open. The build DATE and the upstream
|
|
263
|
+
* commit are the two fields that did move, and they are what this compares.
|
|
264
|
+
*
|
|
265
|
+
* ## What it can and cannot do
|
|
266
|
+
*
|
|
267
|
+
* It can only ADD a red line to `approval doctor` (SPEC.md §11.1 invariant 4: a
|
|
268
|
+
* self-reported field never reduces scrutiny). A build claiming a later date buys
|
|
269
|
+
* nothing — it defeats a row that would have asked a human to look — and nothing
|
|
270
|
+
* in the runtime reads this to widen a verdict, skip a wait or lower a class. The
|
|
271
|
+
* refusals the adapter emits are the same on every build; what changes below the
|
|
272
|
+
* floor is whether a BROKEN hook stops the call, which no code in this repository
|
|
273
|
+
* can observe from inside the hook it is broken in.
|
|
274
|
+
*/
|
|
275
|
+
export const HERMES_FAIL_CLOSED_FLOOR = {
|
|
276
|
+
/** The first build observed to honour the key, by upstream commit. */
|
|
277
|
+
upstream: "118984d7",
|
|
278
|
+
/** That build's date, in the `YYYY.M.D` stamp `hermes --version` prints. */
|
|
279
|
+
date: { year: 2026, month: 9, day: 20 },
|
|
280
|
+
/** The one sentence every surface quotes, so they cannot drift apart. */
|
|
281
|
+
statement: "a build at or after main 118984d7 of 2026-09-20; v0.21.3 (2026.9.14) fails open silently",
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Read the fields out of `hermes --version`'s first line, or `null`.
|
|
285
|
+
*
|
|
286
|
+
* Shape-only and forgiving in one direction: a field this cannot find is `null`
|
|
287
|
+
* rather than a guess, and a line naming none of the three is not a Hermes
|
|
288
|
+
* version line at all. The observed line is
|
|
289
|
+
* `Hermes Agent v0.21.3 (2026.9.14) · upstream 913d4098`; the separator is read
|
|
290
|
+
* past rather than required, because a release that changes its punctuation must
|
|
291
|
+
* not silently turn the floor check into an unknown.
|
|
292
|
+
*/
|
|
293
|
+
export function parseHermesVersion(raw) {
|
|
294
|
+
const line = rawHarnessVersionLine(raw);
|
|
295
|
+
if (line === null)
|
|
296
|
+
return null;
|
|
297
|
+
const semver = /\bv(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/u.exec(line);
|
|
298
|
+
const stamp = /\((\d{4})\.(\d{1,2})\.(\d{1,2})\)/u.exec(line);
|
|
299
|
+
const upstream = /\bupstream\s+([0-9a-f]{7,40})\b/iu.exec(line);
|
|
300
|
+
if (semver === null && stamp === null && upstream === null)
|
|
301
|
+
return null;
|
|
302
|
+
return {
|
|
303
|
+
semver: semver?.[1] ?? null,
|
|
304
|
+
date: stamp === null
|
|
305
|
+
? null
|
|
306
|
+
: {
|
|
307
|
+
year: Number(stamp[1]),
|
|
308
|
+
month: Number(stamp[2]),
|
|
309
|
+
day: Number(stamp[3]),
|
|
310
|
+
},
|
|
311
|
+
upstream: upstream?.[1]?.toLowerCase() ?? null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Does this Hermes build honour `fail_closed`?
|
|
316
|
+
*
|
|
317
|
+
* - `"honours"` — its upstream commit IS the floor's, or its build date is on or
|
|
318
|
+
* after the floor's date;
|
|
319
|
+
* - `"ignores"` — its build date is BEFORE the floor's. This is the one answer
|
|
320
|
+
* that names a fault, and it is the state a live probe measured rather than
|
|
321
|
+
* inferred;
|
|
322
|
+
* - `"unknown"` — no version could be read, or the line carries neither a build
|
|
323
|
+
* date nor the floor's commit. Two commit hashes cannot be ORDERED without a
|
|
324
|
+
* repository, so a build stamped with an unfamiliar commit and no date is
|
|
325
|
+
* honestly unknown rather than quietly either.
|
|
326
|
+
*
|
|
327
|
+
* `"unknown"` is not a synonym for `"ignores"`, deliberately. This value feeds a
|
|
328
|
+
* health check, and a health check that cried fault on every unreadable banner
|
|
329
|
+
* would train an operator to stop reading it, which costs more than the case it
|
|
330
|
+
* would catch. What guards the unknown case instead is the doc, the help block
|
|
331
|
+
* and the probe's own report, all of which state {@link
|
|
332
|
+
* HERMES_FAIL_CLOSED_FLOOR.statement} verbatim.
|
|
333
|
+
*/
|
|
334
|
+
export function hermesFailClosedSupport(raw) {
|
|
335
|
+
const fields = parseHermesVersion(raw);
|
|
336
|
+
if (fields === null)
|
|
337
|
+
return "unknown";
|
|
338
|
+
const floor = HERMES_FAIL_CLOSED_FLOOR;
|
|
339
|
+
if (fields.upstream !== null && fields.upstream.startsWith(floor.upstream))
|
|
340
|
+
return "honours";
|
|
341
|
+
if (fields.date === null)
|
|
342
|
+
return "unknown";
|
|
343
|
+
const asNumber = (date) => date.year * 10_000 + date.month * 100 + date.day;
|
|
344
|
+
return asNumber(fields.date) >= asNumber(floor.date) ? "honours" : "ignores";
|
|
345
|
+
}
|
|
179
346
|
/**
|
|
180
347
|
* Build the pair for `kind`, preferring the version the hook event supplied.
|
|
181
348
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-version.js","sourceRoot":"","sources":["../../../src/core/harness-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,aAAa,GAAG,
|
|
1
|
+
{"version":3,"file":"harness-version.js","sourceRoot":"","sources":["../../../src/core/harness-version.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* How long a harness hook waits, and how long the question it opened outlives
|
|
3
3
|
* that wait (APRV-287).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* rather than beside
|
|
7
|
-
* request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
|
|
5
|
+
* Three numbers with two readers each, which is the whole reason they live
|
|
6
|
+
* here rather than beside any one of them. `cli/hook.ts` uses them to decide
|
|
7
|
+
* when a request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
|
|
8
8
|
* to decide which pending requests on a listener's first cycle are stale enough
|
|
9
9
|
* to collapse into one message. Two copies of the same duration would be two
|
|
10
10
|
* answers to "is anybody still waiting on this", and the channel would collapse
|
|
11
11
|
* questions the hook was still holding open, or leave a dead queue on a phone.
|
|
12
|
+
* The third, APRV-423's margin, is read by the hook (which refuses a harness
|
|
13
|
+
* ceiling too short to hold a human's answer) and by `core/state.ts` (which
|
|
14
|
+
* judges every request against the window that ceiling leaves).
|
|
12
15
|
*
|
|
13
16
|
* Nothing here reads a clock, a policy or the log: these are durations, and
|
|
14
17
|
* every judgment made with them is made by a caller that already holds the
|
|
@@ -53,3 +56,79 @@ export declare const HOOK_RETRY_GRACE_MS: number;
|
|
|
53
56
|
* caller states about when it started waiting.
|
|
54
57
|
*/
|
|
55
58
|
export declare function abandonedAfterMs(waitMs: number, graceMs: number): number;
|
|
59
|
+
/**
|
|
60
|
+
* How far below a harness's own timeout a hook-originated request must lapse
|
|
61
|
+
* (APRV-423).
|
|
62
|
+
*
|
|
63
|
+
* A harness bounds the hook process: Hermes caps a `pre_tool_call` entry at
|
|
64
|
+
* 300 s, Claude Code kills the hook at the `timeout` in its settings file, and
|
|
65
|
+
* every one of them treats the killed process as a NON-BLOCKING error and runs
|
|
66
|
+
* the tool call. So when the harness gives up first the hook is already gone,
|
|
67
|
+
* the request is still pending on somebody's phone, and the tap that arrives
|
|
68
|
+
* afterwards is recorded as a grant on a call nobody holds (APRV-410, observed
|
|
69
|
+
* 2026-09-20). The two records then disagree about the same moment: one says
|
|
70
|
+
* denied, the other says approved.
|
|
71
|
+
*
|
|
72
|
+
* The fix is to make the question end BEFORE the process that asked it does,
|
|
73
|
+
* and this is the distance between the two. It is 60 s because the daemon's
|
|
74
|
+
* TTL sweep runs on a 30 s interval (`daemon/daemon.ts`'s
|
|
75
|
+
* `DEFAULT_INTERVAL_MS`): a request that lapses one millisecond after a sweep
|
|
76
|
+
* waits a whole interval for the next one, so a margin of two intervals leaves
|
|
77
|
+
* the `approval.expired` append a full tick of room in the common case, and it
|
|
78
|
+
* lands inside the cap. Anything smaller would make the ordering this task is
|
|
79
|
+
* about depend on where in the sweep cycle the lapse fell.
|
|
80
|
+
*
|
|
81
|
+
* The common case, and not a guarantee. `Daemon.tick` skips a sweep when the
|
|
82
|
+
* previous tick is still running, and the request record's `ts` is stamped at
|
|
83
|
+
* the write boundary, some intake latency after the harness spawned the hook,
|
|
84
|
+
* so the cap runs from an instant slightly before the one the margin counts
|
|
85
|
+
* from. An overrunning tick plus a slow intake can put the `approval.expired`
|
|
86
|
+
* record after the kill. What keeps a late tap safe regardless is not this
|
|
87
|
+
* margin but the lazy refusal: `core/state.ts` judges the lapse by arithmetic
|
|
88
|
+
* whether or not the record exists, so `decide` refuses the tap `expired` and
|
|
89
|
+
* materialises the record then. The margin buys the ORDERING in the common
|
|
90
|
+
* case, so the two records agree without a human ever seeing the disagreement;
|
|
91
|
+
* the refusal is what makes the grant impossible in every case.
|
|
92
|
+
*
|
|
93
|
+
* It is a floor on the cap as well as a subtraction from it. A cap at or below
|
|
94
|
+
* the margin leaves no window in which a human could answer at all: `cli/hook.ts`
|
|
95
|
+
* refuses such a configuration rather than opening a question that is already
|
|
96
|
+
* dead, and `schema/event.schema.json` refuses a record carrying one at the
|
|
97
|
+
* write boundary (its `minimum` is this constant plus one, pinned equal by
|
|
98
|
+
* `tests/harness-cap-ttl.test.ts`).
|
|
99
|
+
*/
|
|
100
|
+
export declare const HARNESS_CAP_MARGIN_MS = 60000;
|
|
101
|
+
/**
|
|
102
|
+
* The deadline a hook-originated request is judged by: the SHORTER of the
|
|
103
|
+
* policy's TTL and the harness's cap minus {@link HARNESS_CAP_MARGIN_MS}
|
|
104
|
+
* (APRV-423).
|
|
105
|
+
*
|
|
106
|
+
* `null` means no deadline, exactly as `defaults.approval_ttl`'s absence means
|
|
107
|
+
* it (SPEC.md §6): nothing lapses, and no duration is invented for a policy
|
|
108
|
+
* that declared none.
|
|
109
|
+
*
|
|
110
|
+
* **The cap may only shorten.** It is a self-reported field (the hook states
|
|
111
|
+
* the ceiling its own harness runs it under), and SPEC.md §11.1 invariant 4
|
|
112
|
+
* says such a field never reduces scrutiny. Recording it raw and taking the
|
|
113
|
+
* minimum HERE is what makes that structural: a cap larger than the policy TTL
|
|
114
|
+
* changes nothing, a cap of an hour under a one-minute TTL changes nothing, and
|
|
115
|
+
* the only direction a requester can move its own deadline is earlier. The same
|
|
116
|
+
* shape as `core/gate-window.ts`'s `expires_at`, which clamps a claimed window
|
|
117
|
+
* against the derived one for the same reason.
|
|
118
|
+
*
|
|
119
|
+
* A cap that does not clear the margin yields `0`, which reads as lapsed from
|
|
120
|
+
* the instant the request is written. That is the fail-closed answer for a
|
|
121
|
+
* harness whose ceiling leaves no room for a human, and it is unreachable
|
|
122
|
+
* through the write path: the hook refuses the configuration before it
|
|
123
|
+
* appends, and the event schema refuses the record for any caller that did
|
|
124
|
+
* not come through the hook.
|
|
125
|
+
*/
|
|
126
|
+
export declare function harnessCappedTtlMs(policyTtlMs: number | null, capMs: number | null): number | null;
|
|
127
|
+
/**
|
|
128
|
+
* Does `capMs` leave a window a human could answer in?
|
|
129
|
+
*
|
|
130
|
+
* The one question `cli/hook.ts` asks before it opens a request under a cap.
|
|
131
|
+
* Strictly greater: a cap EQUAL to the margin yields a zero-length window,
|
|
132
|
+
* which is a question that expires as it is asked.
|
|
133
|
+
*/
|
|
134
|
+
export declare function harnessCapFitsMargin(capMs: number): boolean;
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
* How long a harness hook waits, and how long the question it opened outlives
|
|
3
3
|
* that wait (APRV-287).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* rather than beside
|
|
7
|
-
* request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
|
|
5
|
+
* Three numbers with two readers each, which is the whole reason they live
|
|
6
|
+
* here rather than beside any one of them. `cli/hook.ts` uses them to decide
|
|
7
|
+
* when a request it opened has been abandoned; `cli/channel-telegram.ts` uses the wait
|
|
8
8
|
* to decide which pending requests on a listener's first cycle are stale enough
|
|
9
9
|
* to collapse into one message. Two copies of the same duration would be two
|
|
10
10
|
* answers to "is anybody still waiting on this", and the channel would collapse
|
|
11
11
|
* questions the hook was still holding open, or leave a dead queue on a phone.
|
|
12
|
+
* The third, APRV-423's margin, is read by the hook (which refuses a harness
|
|
13
|
+
* ceiling too short to hold a human's answer) and by `core/state.ts` (which
|
|
14
|
+
* judges every request against the window that ceiling leaves).
|
|
12
15
|
*
|
|
13
16
|
* Nothing here reads a clock, a policy or the log: these are durations, and
|
|
14
17
|
* every judgment made with them is made by a caller that already holds the
|
|
@@ -55,4 +58,87 @@ export const HOOK_RETRY_GRACE_MS = 5 * 60_000;
|
|
|
55
58
|
export function abandonedAfterMs(waitMs, graceMs) {
|
|
56
59
|
return Math.max(0, waitMs) + Math.max(0, graceMs);
|
|
57
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* How far below a harness's own timeout a hook-originated request must lapse
|
|
63
|
+
* (APRV-423).
|
|
64
|
+
*
|
|
65
|
+
* A harness bounds the hook process: Hermes caps a `pre_tool_call` entry at
|
|
66
|
+
* 300 s, Claude Code kills the hook at the `timeout` in its settings file, and
|
|
67
|
+
* every one of them treats the killed process as a NON-BLOCKING error and runs
|
|
68
|
+
* the tool call. So when the harness gives up first the hook is already gone,
|
|
69
|
+
* the request is still pending on somebody's phone, and the tap that arrives
|
|
70
|
+
* afterwards is recorded as a grant on a call nobody holds (APRV-410, observed
|
|
71
|
+
* 2026-09-20). The two records then disagree about the same moment: one says
|
|
72
|
+
* denied, the other says approved.
|
|
73
|
+
*
|
|
74
|
+
* The fix is to make the question end BEFORE the process that asked it does,
|
|
75
|
+
* and this is the distance between the two. It is 60 s because the daemon's
|
|
76
|
+
* TTL sweep runs on a 30 s interval (`daemon/daemon.ts`'s
|
|
77
|
+
* `DEFAULT_INTERVAL_MS`): a request that lapses one millisecond after a sweep
|
|
78
|
+
* waits a whole interval for the next one, so a margin of two intervals leaves
|
|
79
|
+
* the `approval.expired` append a full tick of room in the common case, and it
|
|
80
|
+
* lands inside the cap. Anything smaller would make the ordering this task is
|
|
81
|
+
* about depend on where in the sweep cycle the lapse fell.
|
|
82
|
+
*
|
|
83
|
+
* The common case, and not a guarantee. `Daemon.tick` skips a sweep when the
|
|
84
|
+
* previous tick is still running, and the request record's `ts` is stamped at
|
|
85
|
+
* the write boundary, some intake latency after the harness spawned the hook,
|
|
86
|
+
* so the cap runs from an instant slightly before the one the margin counts
|
|
87
|
+
* from. An overrunning tick plus a slow intake can put the `approval.expired`
|
|
88
|
+
* record after the kill. What keeps a late tap safe regardless is not this
|
|
89
|
+
* margin but the lazy refusal: `core/state.ts` judges the lapse by arithmetic
|
|
90
|
+
* whether or not the record exists, so `decide` refuses the tap `expired` and
|
|
91
|
+
* materialises the record then. The margin buys the ORDERING in the common
|
|
92
|
+
* case, so the two records agree without a human ever seeing the disagreement;
|
|
93
|
+
* the refusal is what makes the grant impossible in every case.
|
|
94
|
+
*
|
|
95
|
+
* It is a floor on the cap as well as a subtraction from it. A cap at or below
|
|
96
|
+
* the margin leaves no window in which a human could answer at all: `cli/hook.ts`
|
|
97
|
+
* refuses such a configuration rather than opening a question that is already
|
|
98
|
+
* dead, and `schema/event.schema.json` refuses a record carrying one at the
|
|
99
|
+
* write boundary (its `minimum` is this constant plus one, pinned equal by
|
|
100
|
+
* `tests/harness-cap-ttl.test.ts`).
|
|
101
|
+
*/
|
|
102
|
+
export const HARNESS_CAP_MARGIN_MS = 60_000;
|
|
103
|
+
/**
|
|
104
|
+
* The deadline a hook-originated request is judged by: the SHORTER of the
|
|
105
|
+
* policy's TTL and the harness's cap minus {@link HARNESS_CAP_MARGIN_MS}
|
|
106
|
+
* (APRV-423).
|
|
107
|
+
*
|
|
108
|
+
* `null` means no deadline, exactly as `defaults.approval_ttl`'s absence means
|
|
109
|
+
* it (SPEC.md §6): nothing lapses, and no duration is invented for a policy
|
|
110
|
+
* that declared none.
|
|
111
|
+
*
|
|
112
|
+
* **The cap may only shorten.** It is a self-reported field (the hook states
|
|
113
|
+
* the ceiling its own harness runs it under), and SPEC.md §11.1 invariant 4
|
|
114
|
+
* says such a field never reduces scrutiny. Recording it raw and taking the
|
|
115
|
+
* minimum HERE is what makes that structural: a cap larger than the policy TTL
|
|
116
|
+
* changes nothing, a cap of an hour under a one-minute TTL changes nothing, and
|
|
117
|
+
* the only direction a requester can move its own deadline is earlier. The same
|
|
118
|
+
* shape as `core/gate-window.ts`'s `expires_at`, which clamps a claimed window
|
|
119
|
+
* against the derived one for the same reason.
|
|
120
|
+
*
|
|
121
|
+
* A cap that does not clear the margin yields `0`, which reads as lapsed from
|
|
122
|
+
* the instant the request is written. That is the fail-closed answer for a
|
|
123
|
+
* harness whose ceiling leaves no room for a human, and it is unreachable
|
|
124
|
+
* through the write path: the hook refuses the configuration before it
|
|
125
|
+
* appends, and the event schema refuses the record for any caller that did
|
|
126
|
+
* not come through the hook.
|
|
127
|
+
*/
|
|
128
|
+
export function harnessCappedTtlMs(policyTtlMs, capMs) {
|
|
129
|
+
if (capMs === null)
|
|
130
|
+
return policyTtlMs;
|
|
131
|
+
const fromCap = Math.max(0, capMs - HARNESS_CAP_MARGIN_MS);
|
|
132
|
+
return policyTtlMs === null ? fromCap : Math.min(policyTtlMs, fromCap);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Does `capMs` leave a window a human could answer in?
|
|
136
|
+
*
|
|
137
|
+
* The one question `cli/hook.ts` asks before it opens a request under a cap.
|
|
138
|
+
* Strictly greater: a cap EQUAL to the margin yields a zero-length window,
|
|
139
|
+
* which is a question that expires as it is asked.
|
|
140
|
+
*/
|
|
141
|
+
export function harnessCapFitsMargin(capMs) {
|
|
142
|
+
return capMs > HARNESS_CAP_MARGIN_MS;
|
|
143
|
+
}
|
|
58
144
|
//# sourceMappingURL=harness-wait.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness-wait.js","sourceRoot":"","sources":["../../../src/core/harness-wait.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"harness-wait.js","sourceRoot":"","sources":["../../../src/core/harness-wait.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEvC,iDAAiD;AACjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,GAAG,MAAM,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc,EAAE,OAAe;IAC9D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,kBAAkB,CAChC,WAA0B,EAC1B,KAAoB;IAEpB,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,qBAAqB,CAAC,CAAC;IAC3D,OAAO,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAO,KAAK,GAAG,qBAAqB,CAAC;AACvC,CAAC"}
|