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
|
@@ -92,6 +92,10 @@ import { watch } from "node:fs";
|
|
|
92
92
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
93
93
|
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
94
94
|
import { writeQueue } from "../channels/render-queue.js";
|
|
95
|
+
// APRV-403. The classifier is APRV-381's, shared rather than copied: one closed
|
|
96
|
+
// list of transient append codes means the drift scan and the sampling sweep
|
|
97
|
+
// cannot come to disagree about which failures a retry fixes.
|
|
98
|
+
import { isTransientAppendError } from "../core/audit.js";
|
|
95
99
|
import { tick as readClock } from "../core/clock.js";
|
|
96
100
|
import { parseFrontmatter, readTaskFile } from "../core/frontmatter.js";
|
|
97
101
|
import { expire } from "../core/gate.js";
|
|
@@ -105,6 +109,8 @@ import { validate } from "../core/validate.js";
|
|
|
105
109
|
import { publishedState } from "../cli/log-advance.js";
|
|
106
110
|
import { isAdvanceBookkeeping } from "../core/advance-cycle.js";
|
|
107
111
|
import { markDaemonProcess } from "../core/daemon-actor.js";
|
|
112
|
+
import { declareDaemonIdentityFor, refreshDaemonAllowlist, } from "../core/daemon-host.js";
|
|
113
|
+
import { daemonIdentity } from "../core/daemon-identity.js";
|
|
108
114
|
import { repoRoot } from "../cli/git-scope.js";
|
|
109
115
|
import { checkLogAnchor, resolveAnchor } from "../cli/log-anchor.js";
|
|
110
116
|
import { checkLogCheckpoints, checkpointPolicyOf, } from "../core/checkpoint.js";
|
|
@@ -122,6 +128,61 @@ import { driftAlreadyLogged, lapsedRequests, latestRegistration, taskEnvelopeSta
|
|
|
122
128
|
export const DAEMON_ACTOR = "system:daemon";
|
|
123
129
|
/** Backlog.md's conventional task folder, relative to the working directory. */
|
|
124
130
|
export { DEFAULT_TASKS_DIR } from "../core/registration.js";
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// The drift scan's deferral memory (APRV-403)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
/**
|
|
135
|
+
* `envelope.drift` records this PROCESS deferred and has not yet seen appended,
|
|
136
|
+
* keyed by log path, task and drift reason.
|
|
137
|
+
*
|
|
138
|
+
* Exactly the standing `daemon/audit.ts` gives its own deferral memory, and
|
|
139
|
+
* deliberately the same shape: process-lifetime, memory-only, and a
|
|
140
|
+
* de-duplicator for OUTPUT. Nothing here is consulted before an append, and
|
|
141
|
+
* nothing is gated or suppressed by it. The RETRY needs no memory to happen,
|
|
142
|
+
* because the drift scan re-derives the whole question from the verified log on
|
|
143
|
+
* every tick; what this buys is the word "retry" on one line, so an operator can
|
|
144
|
+
* pair the deferral they read with the record that resolved it.
|
|
145
|
+
*
|
|
146
|
+
* It is consulted in exactly one other place, and that one is not cosmetic: the
|
|
147
|
+
* write-back pass skips a file whose drift record this tick could not append, so
|
|
148
|
+
* the file is not repaired off the record (SPEC.md §6.3's fixed order). Losing
|
|
149
|
+
* the memory there costs a repair one tick, never a lost record.
|
|
150
|
+
*
|
|
151
|
+
* Keyed by log path as well as task, because one process can be pointed at more
|
|
152
|
+
* than one gate over its lifetime and a task id is only unique inside one.
|
|
153
|
+
* Bounded, so a long run cannot accumulate entries for a file that drifts, is
|
|
154
|
+
* deferred, and is then deleted.
|
|
155
|
+
*/
|
|
156
|
+
const driftDeferrals = new Set();
|
|
157
|
+
/** How many deferred drift records one process remembers for the retry wording. */
|
|
158
|
+
const DRIFT_DEFERRAL_LIMIT = 256;
|
|
159
|
+
function driftDeferralKey(logPath, task, reason) {
|
|
160
|
+
return JSON.stringify([logPath, task, reason]);
|
|
161
|
+
}
|
|
162
|
+
function rememberDriftDeferral(logPath, task, reason) {
|
|
163
|
+
const key = driftDeferralKey(logPath, task, reason);
|
|
164
|
+
if (driftDeferrals.has(key))
|
|
165
|
+
return;
|
|
166
|
+
// A `Set` iterates in insertion order, so "the oldest" needs no timestamp.
|
|
167
|
+
if (driftDeferrals.size >= DRIFT_DEFERRAL_LIMIT) {
|
|
168
|
+
const oldest = driftDeferrals.values().next();
|
|
169
|
+
if (!oldest.done)
|
|
170
|
+
driftDeferrals.delete(oldest.value);
|
|
171
|
+
}
|
|
172
|
+
driftDeferrals.add(key);
|
|
173
|
+
}
|
|
174
|
+
/** Whether this process deferred this record, consuming the memory of it. */
|
|
175
|
+
function takeDriftDeferral(logPath, task, reason) {
|
|
176
|
+
return driftDeferrals.delete(driftDeferralKey(logPath, task, reason));
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Forget every deferral. Exported for tests, used nowhere else: the memory is
|
|
180
|
+
* output bookkeeping, so a suite that shares a process needs to be able to start
|
|
181
|
+
* from nothing and to exercise what a restarted daemon does.
|
|
182
|
+
*/
|
|
183
|
+
export function resetDriftDeferrals() {
|
|
184
|
+
driftDeferrals.clear();
|
|
185
|
+
}
|
|
125
186
|
/** How often the daemon looks, absent any watcher event. */
|
|
126
187
|
export const DEFAULT_INTERVAL_MS = 30_000;
|
|
127
188
|
/** How long a burst of watcher events is allowed to settle before a tick. */
|
|
@@ -237,6 +298,19 @@ export const DAEMON_WARNING_CODES = [
|
|
|
237
298
|
* repair, and the next tick asks again.
|
|
238
299
|
*/
|
|
239
300
|
"anchor-behind",
|
|
301
|
+
/**
|
|
302
|
+
* The working log changed between this tick's opening read and the anchor
|
|
303
|
+
* check's own read of the file, so the check read it again (APRV-389).
|
|
304
|
+
*
|
|
305
|
+
* A line rather than a stop, and rather than a silence. `approval log sync`
|
|
306
|
+
* and a records pull both rewrite `events.jsonl` under a running daemon
|
|
307
|
+
* (baseline, fast-forward, restore), and this loop reads without the append
|
|
308
|
+
* lock, so a tick can hold a view the file has already moved past. That is a
|
|
309
|
+
* fact about reads, never a fork: the anchor comparison is made again from the
|
|
310
|
+
* file and the verdict beside this line is the second read's. The line is the
|
|
311
|
+
* one an operator acts on when the writer was not a sync they ran.
|
|
312
|
+
*/
|
|
313
|
+
"anchor-reread",
|
|
240
314
|
/**
|
|
241
315
|
* `audit.checkpoint_every` says a human-signed checkpoint is due and the log
|
|
242
316
|
* carries none that recent (APRV-220). A WARNING and never a stop, at every
|
|
@@ -253,6 +327,53 @@ export const DAEMON_WARNING_CODES = [
|
|
|
253
327
|
* month of taps.
|
|
254
328
|
*/
|
|
255
329
|
"draw-unavailable",
|
|
330
|
+
/**
|
|
331
|
+
* This daemon's own id is not one the attested policy's `daemons` list admits,
|
|
332
|
+
* or the id it was launched with is not a usable id at all (APRV-383).
|
|
333
|
+
*
|
|
334
|
+
* A warning rather than a stop, and the distinction matters: the refusal itself
|
|
335
|
+
* happens at the write boundary, one append at a time, so the loop keeps
|
|
336
|
+
* reading, keeps rendering the queue and keeps reporting, and writes nothing.
|
|
337
|
+
* The line exists because an operator should learn that from the first tick
|
|
338
|
+
* rather than from a day of `append-refused` messages. The repair is a line in
|
|
339
|
+
* a policy a human re-attests, or a launch environment fixed, and neither is
|
|
340
|
+
* something this process can do for itself.
|
|
341
|
+
*/
|
|
342
|
+
"daemon-identity-refused",
|
|
343
|
+
/**
|
|
344
|
+
* An `audit.sampled` append met a held lock or a moved head, so the sample was
|
|
345
|
+
* postponed to the next tick (APRV-381). Appended to this union, so no existing
|
|
346
|
+
* entry changed meaning.
|
|
347
|
+
*
|
|
348
|
+
* Distinct from `append-refused` because the operator's next move is different:
|
|
349
|
+
* `append-refused` on a sample is a fact about the record or the file and wants
|
|
350
|
+
* a person, and this is contention between writers that the next sweep resolves
|
|
351
|
+
* by re-deriving the sample from the log. It is still a warning rather than a
|
|
352
|
+
* silence, because a tick that keeps deferring is a log under contention and
|
|
353
|
+
* that is a thing to know, and the `sampled` line that closes it carries
|
|
354
|
+
* `retry`.
|
|
355
|
+
*/
|
|
356
|
+
"sample-deferred",
|
|
357
|
+
/**
|
|
358
|
+
* An `envelope.drift` append met a held lock or a moved head, so the record was
|
|
359
|
+
* postponed to the next tick (APRV-403). Appended to this union, so no existing
|
|
360
|
+
* entry changed meaning.
|
|
361
|
+
*
|
|
362
|
+
* The same split APRV-381 made for `audit.sampled`, for the same reason and
|
|
363
|
+
* from the same closed list of transient codes. Found while landing that task:
|
|
364
|
+
* the end-to-end test that holds the append lockfile for a whole tick showed
|
|
365
|
+
* the drift scan printing `append-refused … (lock-timeout)`, which is true and
|
|
366
|
+
* reads as a lost record, while the next tick re-derived the drift from the
|
|
367
|
+
* verified log and appended it. An operator's next move differs between the
|
|
368
|
+
* two: `append-refused` on a drift record is a fact about the record or the
|
|
369
|
+
* file and wants a person, and this is contention between writers that the
|
|
370
|
+
* scan resolves by itself.
|
|
371
|
+
*
|
|
372
|
+
* Still a warning rather than a silence: a scan that keeps deferring is a log
|
|
373
|
+
* under contention, which is a thing to know. The `drift` line that closes it
|
|
374
|
+
* carries `retry`.
|
|
375
|
+
*/
|
|
376
|
+
"drift-deferred",
|
|
256
377
|
];
|
|
257
378
|
function errorMessage(cause) {
|
|
258
379
|
return cause instanceof Error ? cause.message : String(cause);
|
|
@@ -355,6 +476,16 @@ export class Daemon {
|
|
|
355
476
|
previousSelfWrites = new Set();
|
|
356
477
|
settle = null;
|
|
357
478
|
finished = false;
|
|
479
|
+
/**
|
|
480
|
+
* What this process resolved as its own daemon id (APRV-383), kept so the
|
|
481
|
+
* `started` line can name it and the startup check can say what is wrong with
|
|
482
|
+
* it. The id in FORCE lives in `core/daemon-identity.ts`'s process state, which
|
|
483
|
+
* is what the write boundary reads; this field is the resolution that produced
|
|
484
|
+
* it, and nothing reads it to decide anything.
|
|
485
|
+
*/
|
|
486
|
+
identity;
|
|
487
|
+
/** Has the unlisted-id warning already been said this run? */
|
|
488
|
+
reportedIdentityRefusal = false;
|
|
358
489
|
/** Whether {@link DaemonOptions.draw} actually bound (APRV-208). */
|
|
359
490
|
drawServing = false;
|
|
360
491
|
constructor(options) {
|
|
@@ -367,6 +498,15 @@ export class Daemon {
|
|
|
367
498
|
// `--once` tick and a test driving this object one tick at a time are all
|
|
368
499
|
// the same actor as the loop.
|
|
369
500
|
markDaemonProcess();
|
|
501
|
+
// APRV-383. WHICH daemon this process is, declared in the same breath and at
|
|
502
|
+
// the same moment for the same reason. The resolution is the launch
|
|
503
|
+
// environment's `APPROVAL_DAEMON_ID` or the id derived from this log's
|
|
504
|
+
// instance; an unusable declared id is declared as an identity with NO id,
|
|
505
|
+
// which refuses every append at the write boundary rather than leaving a
|
|
506
|
+
// daemon writing records nothing can attribute. The CLI verbs refuse to start
|
|
507
|
+
// on the same resolution, so this is the belt on that brace, and it is what
|
|
508
|
+
// covers a `Daemon` constructed directly by an embedder or a test.
|
|
509
|
+
this.identity = declareDaemonIdentityFor(this.options.logPath);
|
|
370
510
|
}
|
|
371
511
|
/** Run until stopped (or, with `once`, for exactly one tick). */
|
|
372
512
|
run() {
|
|
@@ -418,7 +558,19 @@ export class Daemon {
|
|
|
418
558
|
// would be a write nobody asked for yet. Seeded into `reportedDangling`
|
|
419
559
|
// so the tick that immediately follows does not repeat it.
|
|
420
560
|
dangling_advances: this.listDanglingAdvancesAtStartup(),
|
|
561
|
+
// APRV-383. Resolved in the constructor, printed here, and `null` for an
|
|
562
|
+
// id this process could not use at all.
|
|
563
|
+
daemon: this.identity.ok ? this.identity.id : null,
|
|
421
564
|
});
|
|
565
|
+
// APRV-383. Said once, before the first tick, when this run cannot write:
|
|
566
|
+
// a bad launch id is knowable without reading anything, and the allowlist
|
|
567
|
+
// is knowable from the attested policy the first tick resolves. An operator
|
|
568
|
+
// who reads it here does not have to infer it from a day of
|
|
569
|
+
// `append-refused` lines.
|
|
570
|
+
if (!this.identity.ok) {
|
|
571
|
+
this.reportedIdentityRefusal = true;
|
|
572
|
+
this.warn("daemon-identity-refused", `${this.identity.message}. Every append this run makes will be refused \`daemon-id-invalid\` until that is fixed; nothing is written and nothing is retried`);
|
|
573
|
+
}
|
|
422
574
|
const outcome = this.tick();
|
|
423
575
|
if (outcome !== null) {
|
|
424
576
|
this.finish(outcome);
|
|
@@ -729,6 +881,14 @@ export class Daemon {
|
|
|
729
881
|
const opening = this.read();
|
|
730
882
|
if (!opening.ok)
|
|
731
883
|
return this.fatal(opening);
|
|
884
|
+
// WHICH daemons this log's ATTESTED policy admits (APRV-383), put in force
|
|
885
|
+
// before anything below appends. Resolved every tick, from this tick's own
|
|
886
|
+
// verified read, so a `daemons` list a human adds and attests takes effect
|
|
887
|
+
// on the next tick rather than at the next restart; a resolution that FAILS
|
|
888
|
+
// (the policy will not load, or is not attested) leaves the previous one
|
|
889
|
+
// standing, so a policy that becomes unreadable is not a way out of the list
|
|
890
|
+
// it carried a moment ago.
|
|
891
|
+
this.refreshIdentity(opening.records);
|
|
732
892
|
// The anchor check (APRV-219), on the full re-proof cadence and on the
|
|
733
893
|
// first tick, which is always a cold walk. Placed immediately after the
|
|
734
894
|
// opening read and before any append this tick could make: a log that
|
|
@@ -740,6 +900,17 @@ export class Daemon {
|
|
|
740
900
|
if (anchor.status === "diverged") {
|
|
741
901
|
return { kind: "anchor-diverged", message: anchor.message };
|
|
742
902
|
}
|
|
903
|
+
// The log was rewritten between this tick's opening read and the
|
|
904
|
+
// check's own read of the file (APRV-389), so the check read it again
|
|
905
|
+
// and this verdict is the second read's. ONE line, and no stop: a
|
|
906
|
+
// `log sync` or a records pull replacing `events.jsonl` under this
|
|
907
|
+
// process is housekeeping on the container, the append lock keeps it
|
|
908
|
+
// from interleaving with any append, and the daemon's every other step
|
|
909
|
+
// re-reads the log for itself anyway. The line is here because a
|
|
910
|
+
// writer nobody expected is a thing to know, and because this is the
|
|
911
|
+
// read that used to be reported as a fork.
|
|
912
|
+
if (anchor.reread !== undefined)
|
|
913
|
+
this.warn("anchor-reread", anchor.reread.detail);
|
|
743
914
|
if (anchor.status === "behind") {
|
|
744
915
|
this.warn("anchor-behind", `${anchor.detail}. Nothing was appended onto the shorter chain by this check; run \`approval log sync\` before this daemon writes anything`);
|
|
745
916
|
}
|
|
@@ -798,14 +969,19 @@ export class Daemon {
|
|
|
798
969
|
...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
|
|
799
970
|
...(this.options.clock === undefined ? {} : { clock: this.options.clock }),
|
|
800
971
|
warn: (message) => this.warn("append-refused", message),
|
|
972
|
+
// APRV-381. Contention with another writer gets its own code, so the
|
|
973
|
+
// window an operator reads distinguishes "this sample is coming" from
|
|
974
|
+
// "an audit record was not written".
|
|
975
|
+
defer: (message) => this.warn("sample-deferred", message),
|
|
801
976
|
// One line per sample appended (APRV-57). The sweep names no event; it
|
|
802
977
|
// hands back what it wrote and the loop says it in the loop's own words.
|
|
803
|
-
sampled: (sample) => this.emit({
|
|
978
|
+
sampled: (sample, retry) => this.emit({
|
|
804
979
|
event: "sampled",
|
|
805
980
|
action_key: sample.candidate.actionKey,
|
|
806
981
|
task: sample.candidate.task,
|
|
807
982
|
seq: sample.record.seq,
|
|
808
983
|
subject_seq: sample.candidate.seq,
|
|
984
|
+
...(retry ? { retry: true } : {}),
|
|
809
985
|
}),
|
|
810
986
|
}));
|
|
811
987
|
// The dark-session sweep (APRV-192), on its own cadence. Placed with the
|
|
@@ -1379,14 +1555,45 @@ export class Daemon {
|
|
|
1379
1555
|
...(this.options.readProof === undefined ? {} : { readProof: this.options.readProof }),
|
|
1380
1556
|
});
|
|
1381
1557
|
}
|
|
1382
|
-
/**
|
|
1383
|
-
|
|
1558
|
+
/** Where this daemon's policy is, in `loadPolicy`'s own vocabulary. */
|
|
1559
|
+
policyWhere() {
|
|
1384
1560
|
const where = this.options.policy.file !== undefined
|
|
1385
1561
|
? { file: this.options.policy.file }
|
|
1386
1562
|
: { dir: this.options.policy.dir ?? this.options.cwd };
|
|
1387
1563
|
if (this.options.schemaDir !== undefined)
|
|
1388
1564
|
where.schemaDir = this.options.schemaDir;
|
|
1389
|
-
|
|
1565
|
+
return where;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Put the attested policy's `daemons` allowlist in force for this tick
|
|
1569
|
+
* (APRV-383).
|
|
1570
|
+
*
|
|
1571
|
+
* Called once per tick, immediately after the opening verified read and before
|
|
1572
|
+
* any sweep that appends. It never widens: a resolution that fails sets nothing,
|
|
1573
|
+
* so whatever was last resolved stays in force, and a run whose policy was never
|
|
1574
|
+
* attested is unrestricted exactly as every run before this key existed was.
|
|
1575
|
+
*
|
|
1576
|
+
* The warning is said ONCE per run. A line every thirty seconds forever stops
|
|
1577
|
+
* being read (the `reportedDangling` set above exists for the same reason), and
|
|
1578
|
+
* the write boundary is where the refusal actually happens: every refused append
|
|
1579
|
+
* carries its own `append-refused` line with the reason in it.
|
|
1580
|
+
*/
|
|
1581
|
+
refreshIdentity(records) {
|
|
1582
|
+
refreshDaemonAllowlist(records, loadPolicy(this.policyWhere()));
|
|
1583
|
+
if (this.reportedIdentityRefusal)
|
|
1584
|
+
return;
|
|
1585
|
+
const state = daemonIdentity();
|
|
1586
|
+
if (state === null || state.id === null)
|
|
1587
|
+
return;
|
|
1588
|
+
const allowed = state.allowed;
|
|
1589
|
+
if (allowed === null || allowed.includes(state.id))
|
|
1590
|
+
return;
|
|
1591
|
+
this.reportedIdentityRefusal = true;
|
|
1592
|
+
this.warn("daemon-identity-refused", `this daemon's id is ${state.id} and the attested policy's \`daemons\` list admits ${allowed.length === 0 ? "no daemon at all" : allowed.join(", ")}: every append this run makes is refused \`daemon-not-allowed\` and nothing is written. Add ${state.id} to \`daemons\` and re-attest the policy, or run the daemon whose id the list already names`);
|
|
1593
|
+
}
|
|
1594
|
+
/** The TTL in force right now, re-read every pass: policy files change. */
|
|
1595
|
+
ttlMs() {
|
|
1596
|
+
const load = loadPolicy(this.policyWhere());
|
|
1390
1597
|
// Fail closed exactly as the gate does: an unloadable policy declares no
|
|
1391
1598
|
// TTL, so nothing lapses and nothing is expired on its behalf.
|
|
1392
1599
|
return load.ok ? load.durations.approvalTtlMs : null;
|
|
@@ -1419,6 +1626,29 @@ export class Daemon {
|
|
|
1419
1626
|
* malformed file is not a *contradiction* of the log, it is a file the runtime
|
|
1420
1627
|
* cannot read a claim out of at all, and inventing a `declared_state` for it
|
|
1421
1628
|
* would put a fact in the log that nobody wrote.
|
|
1629
|
+
*
|
|
1630
|
+
* ## A deferred drift record is not a lost one (APRV-403)
|
|
1631
|
+
*
|
|
1632
|
+
* An append here can fail two ways that call for different things. A held
|
|
1633
|
+
* lockfile or a head that moved under the compare-and-append means the log was
|
|
1634
|
+
* not written and nothing about the file changed; the next tick re-reads the
|
|
1635
|
+
* folder, re-derives the disagreement from the verified log, and appends it.
|
|
1636
|
+
* A validation refusal, a canonicalization failure, a corrupt tail or an I/O
|
|
1637
|
+
* error is a fact about the record or the file that no retry repairs.
|
|
1638
|
+
*
|
|
1639
|
+
* Both used to read as `append-refused … was not appended`, which for the
|
|
1640
|
+
* first is true and reads as a loss. So the transient half now goes out as
|
|
1641
|
+
* {@link deferDrift}'s own `drift-deferred` line, naming the task and saying
|
|
1642
|
+
* that the scan retries, and the `drift` record that closes it carries
|
|
1643
|
+
* `retry`. Everything else keeps the form it had.
|
|
1644
|
+
*
|
|
1645
|
+
* Nothing about the APPEND changed, which is the part that matters for
|
|
1646
|
+
* SPEC.md §11.1 invariant 5: the record is still placed by `appendEvent`
|
|
1647
|
+
* against the `expectedHead` of a read taken immediately before it, and the
|
|
1648
|
+
* whole decision is still remade against that read. A `head-moved` here IS
|
|
1649
|
+
* compare-and-append working — another writer landed a record between this
|
|
1650
|
+
* scan's read and its append — and deferring is the only answer that does not
|
|
1651
|
+
* either write against a head this scan never saw or lose the observation.
|
|
1422
1652
|
*/
|
|
1423
1653
|
scanForDrift() {
|
|
1424
1654
|
// ONE verified read for the whole scan (APRV-211). The scan asks the same
|
|
@@ -1446,6 +1676,24 @@ export class Daemon {
|
|
|
1446
1676
|
}
|
|
1447
1677
|
return { appended, stop: null };
|
|
1448
1678
|
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Say that an `envelope.drift` append did not happen YET, and remember it
|
|
1681
|
+
* (APRV-403).
|
|
1682
|
+
*
|
|
1683
|
+
* Two things this line has to do that `append-refused` did not. It names the
|
|
1684
|
+
* TASK KEY, so an operator can match the deferral to the record that closes
|
|
1685
|
+
* it, and it says in words that the next tick retries — because the sentence
|
|
1686
|
+
* it replaced ended at "was not appended", which is true and reads as a loss.
|
|
1687
|
+
*
|
|
1688
|
+
* The claim is safe to make because the scan carries nothing between ticks:
|
|
1689
|
+
* the next one re-reads the folder, re-derives the state from the verified
|
|
1690
|
+
* log, and finds the same disagreement, so the retry is a property of how the
|
|
1691
|
+
* scan is written rather than a promise this method is keeping.
|
|
1692
|
+
*/
|
|
1693
|
+
deferDrift(task, reason, error) {
|
|
1694
|
+
rememberDriftDeferral(this.options.logPath, task, reason);
|
|
1695
|
+
this.warn("drift-deferred", `envelope.drift${reason === "state-mismatch" ? "" : ` (${reason})`} for ${task} was deferred (${error.code}): ${error.message} The record is NOT lost: the disagreement is still there in the log's own terms, and the scan re-derives it and retries on the next tick.`);
|
|
1696
|
+
}
|
|
1449
1697
|
/** Every `*.md` under the task folder, sorted, non-recursive. */
|
|
1450
1698
|
taskFiles() {
|
|
1451
1699
|
let entries;
|
|
@@ -1566,10 +1814,22 @@ export class Daemon {
|
|
|
1566
1814
|
expectedHead: records.head,
|
|
1567
1815
|
});
|
|
1568
1816
|
if (!result.ok) {
|
|
1569
|
-
|
|
1817
|
+
// APRV-403. A held lock or a moved head says the log was not written and
|
|
1818
|
+
// nothing about this file changed, which is a different fact from a record
|
|
1819
|
+
// the write boundary will never take; the scan re-derives and retries the
|
|
1820
|
+
// first, and only a person repairs the second. `isTransientAppendError`
|
|
1821
|
+
// and its closed list are `core/audit.ts`'s, shared rather than copied, so
|
|
1822
|
+
// the two sweeps cannot disagree about what transient means.
|
|
1823
|
+
if (isTransientAppendError(result.error)) {
|
|
1824
|
+
this.deferDrift(id, "state-mismatch", result.error);
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
this.warn("append-refused", `envelope.drift for ${id} was not appended (${result.error.code}): ${result.error.message}`);
|
|
1828
|
+
}
|
|
1570
1829
|
return { appended: false, stop: null };
|
|
1571
1830
|
}
|
|
1572
1831
|
this.drifts += 1;
|
|
1832
|
+
const retried = takeDriftDeferral(this.options.logPath, id, "state-mismatch");
|
|
1573
1833
|
this.emit({
|
|
1574
1834
|
event: "drift",
|
|
1575
1835
|
task: id,
|
|
@@ -1577,6 +1837,7 @@ export class Daemon {
|
|
|
1577
1837
|
declared_state: declaredState,
|
|
1578
1838
|
derived_state: facts.derivedState,
|
|
1579
1839
|
seq: result.record.seq,
|
|
1840
|
+
...(retried ? { retry: true } : {}),
|
|
1580
1841
|
});
|
|
1581
1842
|
return { appended: true, stop: null };
|
|
1582
1843
|
}
|
|
@@ -1647,10 +1908,20 @@ export class Daemon {
|
|
|
1647
1908
|
expectedHead: records.head,
|
|
1648
1909
|
});
|
|
1649
1910
|
if (!result.ok) {
|
|
1650
|
-
|
|
1911
|
+
// The same split as the mismatch path above (APRV-403). Both sites take
|
|
1912
|
+
// it, because contention does not care which reason the record carries and
|
|
1913
|
+
// an operator reading a deferral for one and a refusal for the other would
|
|
1914
|
+
// be reading two answers to one question.
|
|
1915
|
+
if (isTransientAppendError(result.error)) {
|
|
1916
|
+
this.deferDrift(task, "envelope-missing", result.error);
|
|
1917
|
+
}
|
|
1918
|
+
else {
|
|
1919
|
+
this.warn("append-refused", `envelope.drift (envelope-missing) for ${task} was not appended (${result.error.code}): ${result.error.message}`);
|
|
1920
|
+
}
|
|
1651
1921
|
return { appended: false, stop: null };
|
|
1652
1922
|
}
|
|
1653
1923
|
this.drifts += 1;
|
|
1924
|
+
const retried = takeDriftDeferral(this.options.logPath, task, "envelope-missing");
|
|
1654
1925
|
this.emit({
|
|
1655
1926
|
event: "drift",
|
|
1656
1927
|
task,
|
|
@@ -1659,6 +1930,7 @@ export class Daemon {
|
|
|
1659
1930
|
derived_state: facts.derivedState,
|
|
1660
1931
|
seq: result.record.seq,
|
|
1661
1932
|
reason: "envelope-missing",
|
|
1933
|
+
...(retried ? { retry: true } : {}),
|
|
1662
1934
|
});
|
|
1663
1935
|
return { appended: true, stop: null };
|
|
1664
1936
|
}
|
|
@@ -1773,7 +2045,7 @@ export class Daemon {
|
|
|
1773
2045
|
* log, and produces bytes only through `core/task-file.ts`. A file can no more
|
|
1774
2046
|
* teach the log a state than a screenshot can teach a database a row.
|
|
1775
2047
|
*
|
|
1776
|
-
*
|
|
2048
|
+
* Five rules, each of which is a way of not making things worse:
|
|
1777
2049
|
*
|
|
1778
2050
|
* 1. **Only files that already have an envelope.** `set-state` refuses
|
|
1779
2051
|
* `no-envelope`, and that refusal is honoured silently: a task with no
|
|
@@ -1795,6 +2067,11 @@ export class Daemon {
|
|
|
1795
2067
|
* envelope: each was warned about a few milliseconds ago by
|
|
1796
2068
|
* {@link scanForDrift} over the same folder. Repeating it here would double
|
|
1797
2069
|
* every line an operator reads without adding a fact.
|
|
2070
|
+
* 5. **No repair for a drift record that was deferred** (APRV-403). See the
|
|
2071
|
+
* check inside the loop: a file whose `envelope.drift` this tick could not
|
|
2072
|
+
* append is left exactly as it is, because repairing it would correct the
|
|
2073
|
+
* disagreement off the record and erase what the retry is supposed to
|
|
2074
|
+
* re-derive.
|
|
1798
2075
|
*
|
|
1799
2076
|
* Loop safety comes from the comparison, not from a remembered flag: the next
|
|
1800
2077
|
* tick derives the same state from the same log, finds the file already
|
|
@@ -1844,6 +2121,20 @@ export class Daemon {
|
|
|
1844
2121
|
const derived = taskEnvelopeState(records.records, id, ts, ttlMs).state;
|
|
1845
2122
|
if (declaredState === derived)
|
|
1846
2123
|
continue;
|
|
2124
|
+
// APRV-403, and it is rule 5 of this list. The drift scan earlier in this
|
|
2125
|
+
// tick DEFERRED the record for this task, so the disagreement has not been
|
|
2126
|
+
// written down yet. Repairing the file now would correct it off the record
|
|
2127
|
+
// and, worse, remove the evidence: the next tick would find the file
|
|
2128
|
+
// agreeing with the log, the retry would have nothing to re-derive, and
|
|
2129
|
+
// the deferral an operator was told about would resolve into silence.
|
|
2130
|
+
//
|
|
2131
|
+
// SPEC.md §6.3 fixes the order — the event is appended first and the file
|
|
2132
|
+
// is updated second, never the reverse — and a transient append failure is
|
|
2133
|
+
// exactly the case where "never the reverse" has to be enforced rather
|
|
2134
|
+
// than assumed. So the file waits for the record, and the tick that lands
|
|
2135
|
+
// the record repairs the file after it.
|
|
2136
|
+
if (driftDeferrals.has(driftDeferralKey(this.options.logPath, id, "state-mismatch")))
|
|
2137
|
+
continue;
|
|
1847
2138
|
const rewritten = rewriteTaskFile(text, { kind: "set-state", state: derived }, rewriteOptions);
|
|
1848
2139
|
if (!rewritten.ok) {
|
|
1849
2140
|
if (rewritten.code === "no-envelope")
|