immune-brain 3.6.9 → 4.0.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/package.json +3 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7587 -5049
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +27 -25
- package/plugins/immune-brain/dist/imm-planner.md +34 -27
- package/plugins/immune-brain/dist/imm-review-retro.md +2 -2
- package/plugins/immune-brain/dist/role-prompts/code-review.md +3 -1
- package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
- package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
- package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
- package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
- package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
- package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
- package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
- package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
- package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
- package/plugins/immune-brain/runtime/plan_core.ts +27 -65
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +3 -1
- package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
- package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
- package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
- package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
- package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
- package/plugins/immune-brain/bin/imm-retired +0 -4
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
- package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
- package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
- package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
closeSync,
|
|
4
|
-
constants,
|
|
5
|
-
existsSync,
|
|
6
|
-
fstatSync,
|
|
7
|
-
fsyncSync,
|
|
8
|
-
lstatSync,
|
|
9
|
-
mkdirSync,
|
|
10
|
-
openSync,
|
|
11
|
-
readFileSync,
|
|
12
|
-
realpathSync,
|
|
13
|
-
rmSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
} from "node:fs";
|
|
16
|
-
import { dirname, resolve, sep } from "node:path";
|
|
17
|
-
import { stableStringify } from "../canonical_json";
|
|
18
|
-
import {
|
|
19
|
-
AUTHORITY_OBSERVATION_GENERATION_V2,
|
|
20
|
-
AUTHORITY_OBSERVER_VERSION_V2,
|
|
21
|
-
} from "../authority_commit_receipts";
|
|
22
|
-
import type { LegacyMapping, V3CommitSourceEvent } from "./types";
|
|
23
|
-
|
|
24
|
-
const JOURNAL_RELATIVE_PATH =
|
|
25
|
-
".imm/state/observations/automatic_observations.jsonl";
|
|
26
|
-
const LOCK_RELATIVE_PATH =
|
|
27
|
-
".imm/state/observations/automatic_observations.lock";
|
|
28
|
-
const MAX_JOURNAL_BYTES = 64 * 1024 * 1024;
|
|
29
|
-
|
|
30
|
-
export interface V3AuthorityObservationV2 {
|
|
31
|
-
contract: "assurance_kernel/v3_authority_observation/v2";
|
|
32
|
-
observation_id: string;
|
|
33
|
-
receipt_record_id: string;
|
|
34
|
-
receipt_attempt_id: string;
|
|
35
|
-
receipt_protocol: "assurance_kernel/authority_commit_receipt/v2";
|
|
36
|
-
receipt_status: "committed" | "recovered_committed";
|
|
37
|
-
source_kind: "state_mutation" | "project_migration";
|
|
38
|
-
source_ref: string;
|
|
39
|
-
state_path_identity: string;
|
|
40
|
-
committed_bytes_sha256: string;
|
|
41
|
-
ledger_revision: string;
|
|
42
|
-
plan_path: string | null;
|
|
43
|
-
plan_signature: string | null;
|
|
44
|
-
source_events: V3CommitSourceEvent[];
|
|
45
|
-
shadow: LegacyMapping;
|
|
46
|
-
divergence: { detected: boolean; fields: string[] };
|
|
47
|
-
observer_generation: typeof AUTHORITY_OBSERVATION_GENERATION_V2;
|
|
48
|
-
observer_version: typeof AUTHORITY_OBSERVER_VERSION_V2;
|
|
49
|
-
committed_at: string;
|
|
50
|
-
observed_at: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export class AutomaticObservationSecurityError extends Error {}
|
|
54
|
-
export class AutomaticObservationConflictError extends Error {}
|
|
55
|
-
|
|
56
|
-
function sha256(value: string): string {
|
|
57
|
-
return createHash("sha256").update(value).digest("hex");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function digest(domain: string, payload: string): string {
|
|
61
|
-
return `sha256:${sha256(`${domain}\0${payload}`)}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function asRecord(value: unknown): Record<string, unknown> {
|
|
65
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
66
|
-
throw new AutomaticObservationSecurityError(
|
|
67
|
-
"automatic observation must be an object",
|
|
68
|
-
);
|
|
69
|
-
return value as Record<string, unknown>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function exactKeys(record: Record<string, unknown>, expected: string[]): void {
|
|
73
|
-
if (
|
|
74
|
-
JSON.stringify(Object.keys(record).sort()) !==
|
|
75
|
-
JSON.stringify([...expected].sort())
|
|
76
|
-
)
|
|
77
|
-
throw new AutomaticObservationSecurityError(
|
|
78
|
-
"automatic observation v2 has unknown or missing fields",
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function nonEmptyString(record: Record<string, unknown>, key: string): string {
|
|
83
|
-
const value = record[key];
|
|
84
|
-
if (typeof value !== "string" || !value)
|
|
85
|
-
throw new AutomaticObservationSecurityError(
|
|
86
|
-
`automatic observation v2 ${key} is invalid`,
|
|
87
|
-
);
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function parseSourceEvents(value: unknown): V3CommitSourceEvent[] {
|
|
92
|
-
if (!Array.isArray(value))
|
|
93
|
-
throw new AutomaticObservationSecurityError(
|
|
94
|
-
"automatic observation v2 source_events is invalid",
|
|
95
|
-
);
|
|
96
|
-
return value.map((entry) => {
|
|
97
|
-
const record = asRecord(entry);
|
|
98
|
-
exactKeys(record, ["action", "at", "id"]);
|
|
99
|
-
const id = nonEmptyString(record, "id");
|
|
100
|
-
const action = nonEmptyString(record, "action");
|
|
101
|
-
if (record.at !== null && typeof record.at !== "string")
|
|
102
|
-
throw new AutomaticObservationSecurityError(
|
|
103
|
-
"automatic observation v2 source event time is invalid",
|
|
104
|
-
);
|
|
105
|
-
return { id, action, at: record.at as string | null };
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function parseLegacyMapping(value: unknown): LegacyMapping {
|
|
110
|
-
const record = asRecord(value);
|
|
111
|
-
exactKeys(record, ["ambiguous", "phase", "reason", "source_states"]);
|
|
112
|
-
if (
|
|
113
|
-
(record.phase !== null &&
|
|
114
|
-
!(["working", "review", "done", "stopped"] as unknown[]).includes(
|
|
115
|
-
record.phase,
|
|
116
|
-
)) ||
|
|
117
|
-
typeof record.reason !== "string" ||
|
|
118
|
-
typeof record.ambiguous !== "boolean" ||
|
|
119
|
-
!Array.isArray(record.source_states) ||
|
|
120
|
-
record.source_states.some((entry) => typeof entry !== "string")
|
|
121
|
-
)
|
|
122
|
-
throw new AutomaticObservationSecurityError(
|
|
123
|
-
"automatic observation v2 shadow is invalid",
|
|
124
|
-
);
|
|
125
|
-
return record as unknown as LegacyMapping;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function parseDivergence(
|
|
129
|
-
value: unknown,
|
|
130
|
-
): { detected: boolean; fields: string[] } {
|
|
131
|
-
const record = asRecord(value);
|
|
132
|
-
exactKeys(record, ["detected", "fields"]);
|
|
133
|
-
if (
|
|
134
|
-
typeof record.detected !== "boolean" ||
|
|
135
|
-
!Array.isArray(record.fields) ||
|
|
136
|
-
record.fields.some((entry) => typeof entry !== "string") ||
|
|
137
|
-
record.detected !== (record.fields.length > 0)
|
|
138
|
-
)
|
|
139
|
-
throw new AutomaticObservationSecurityError(
|
|
140
|
-
"automatic observation v2 divergence is invalid",
|
|
141
|
-
);
|
|
142
|
-
return { detected: record.detected, fields: [...record.fields] as string[] };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export function parseAutomaticObservationV2(
|
|
146
|
-
raw: unknown,
|
|
147
|
-
): V3AuthorityObservationV2 {
|
|
148
|
-
const record = asRecord(raw);
|
|
149
|
-
exactKeys(record, [
|
|
150
|
-
"committed_at",
|
|
151
|
-
"committed_bytes_sha256",
|
|
152
|
-
"contract",
|
|
153
|
-
"divergence",
|
|
154
|
-
"ledger_revision",
|
|
155
|
-
"observation_id",
|
|
156
|
-
"observed_at",
|
|
157
|
-
"observer_generation",
|
|
158
|
-
"observer_version",
|
|
159
|
-
"plan_path",
|
|
160
|
-
"plan_signature",
|
|
161
|
-
"receipt_attempt_id",
|
|
162
|
-
"receipt_protocol",
|
|
163
|
-
"receipt_record_id",
|
|
164
|
-
"receipt_status",
|
|
165
|
-
"shadow",
|
|
166
|
-
"source_events",
|
|
167
|
-
"source_kind",
|
|
168
|
-
"source_ref",
|
|
169
|
-
"state_path_identity",
|
|
170
|
-
]);
|
|
171
|
-
if (
|
|
172
|
-
record.contract !== "assurance_kernel/v3_authority_observation/v2" ||
|
|
173
|
-
record.receipt_protocol !==
|
|
174
|
-
"assurance_kernel/authority_commit_receipt/v2" ||
|
|
175
|
-
(record.receipt_status !== "committed" &&
|
|
176
|
-
record.receipt_status !== "recovered_committed") ||
|
|
177
|
-
record.observer_generation !== AUTHORITY_OBSERVATION_GENERATION_V2 ||
|
|
178
|
-
record.observer_version !== AUTHORITY_OBSERVER_VERSION_V2 ||
|
|
179
|
-
(record.source_kind !== "state_mutation" &&
|
|
180
|
-
record.source_kind !== "project_migration")
|
|
181
|
-
)
|
|
182
|
-
throw new AutomaticObservationSecurityError(
|
|
183
|
-
"automatic observation v2 identity is invalid",
|
|
184
|
-
);
|
|
185
|
-
for (const key of [
|
|
186
|
-
"observation_id",
|
|
187
|
-
"receipt_record_id",
|
|
188
|
-
"receipt_attempt_id",
|
|
189
|
-
"source_ref",
|
|
190
|
-
"state_path_identity",
|
|
191
|
-
"committed_bytes_sha256",
|
|
192
|
-
"ledger_revision",
|
|
193
|
-
"committed_at",
|
|
194
|
-
"observed_at",
|
|
195
|
-
])
|
|
196
|
-
nonEmptyString(record, key);
|
|
197
|
-
if (
|
|
198
|
-
(record.plan_path !== null && typeof record.plan_path !== "string") ||
|
|
199
|
-
(record.plan_signature !== null &&
|
|
200
|
-
typeof record.plan_signature !== "string")
|
|
201
|
-
)
|
|
202
|
-
throw new AutomaticObservationSecurityError(
|
|
203
|
-
"automatic observation v2 Plan identity is invalid",
|
|
204
|
-
);
|
|
205
|
-
const observation: V3AuthorityObservationV2 = {
|
|
206
|
-
...(record as unknown as V3AuthorityObservationV2),
|
|
207
|
-
source_events: parseSourceEvents(record.source_events),
|
|
208
|
-
shadow: parseLegacyMapping(record.shadow),
|
|
209
|
-
divergence: parseDivergence(record.divergence),
|
|
210
|
-
};
|
|
211
|
-
const { observation_id: _observationId, ...core } = observation;
|
|
212
|
-
if (
|
|
213
|
-
observation.observation_id !==
|
|
214
|
-
digest(
|
|
215
|
-
"assurance-kernel-v3-observation/v2",
|
|
216
|
-
stableStringify(core),
|
|
217
|
-
)
|
|
218
|
-
)
|
|
219
|
-
throw new AutomaticObservationSecurityError(
|
|
220
|
-
"automatic observation v2 identity mismatch",
|
|
221
|
-
);
|
|
222
|
-
return observation;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function canonicalRoot(root: string): string {
|
|
226
|
-
const resolved = resolve(root);
|
|
227
|
-
const stat = lstatSync(resolved);
|
|
228
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
229
|
-
throw new AutomaticObservationSecurityError(
|
|
230
|
-
"automatic observation root must be a real directory",
|
|
231
|
-
);
|
|
232
|
-
return realpathSync(resolved);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function insideRoot(root: string, candidate: string): boolean {
|
|
236
|
-
return candidate === root || candidate.startsWith(`${root}${sep}`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function ensureMemoryDirectory(root: string): { root: string; memory: string } {
|
|
240
|
-
// Storage-layout cutover: automatic observations are active runtime state
|
|
241
|
-
// and live under the ignored .imm/state/observations/ directory.
|
|
242
|
-
const canonical = canonicalRoot(root);
|
|
243
|
-
let cursor = canonical;
|
|
244
|
-
for (const segment of [".imm", "state", "observations"]) {
|
|
245
|
-
cursor = resolve(cursor, segment);
|
|
246
|
-
if (!existsSync(cursor)) mkdirSync(cursor, { mode: 0o700 });
|
|
247
|
-
const stat = lstatSync(cursor);
|
|
248
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
249
|
-
throw new AutomaticObservationSecurityError(
|
|
250
|
-
"automatic observation directory is not a real directory",
|
|
251
|
-
);
|
|
252
|
-
const real = realpathSync(cursor);
|
|
253
|
-
if (!insideRoot(canonical, real))
|
|
254
|
-
throw new AutomaticObservationSecurityError(
|
|
255
|
-
"automatic observation directory escapes the project",
|
|
256
|
-
);
|
|
257
|
-
cursor = real;
|
|
258
|
-
}
|
|
259
|
-
return { root: canonical, memory: cursor };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function journalPath(root: string): string {
|
|
263
|
-
return resolve(canonicalRoot(root), JOURNAL_RELATIVE_PATH);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function automaticObservationJournalPath(root: string): string {
|
|
267
|
-
return journalPath(root);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function assertRegularPathOrMissing(path: string): void {
|
|
271
|
-
if (!existsSync(path)) return;
|
|
272
|
-
const stat = lstatSync(path);
|
|
273
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
274
|
-
throw new AutomaticObservationSecurityError(
|
|
275
|
-
"automatic observation path is not a regular file",
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function processIsAlive(pid: number): boolean {
|
|
280
|
-
try {
|
|
281
|
-
process.kill(pid, 0);
|
|
282
|
-
return true;
|
|
283
|
-
} catch (error) {
|
|
284
|
-
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function clearStaleLock(path: string): boolean {
|
|
289
|
-
if (!existsSync(path)) return true;
|
|
290
|
-
const before = lstatSync(path);
|
|
291
|
-
if (before.isSymbolicLink() || !before.isFile())
|
|
292
|
-
throw new AutomaticObservationSecurityError(
|
|
293
|
-
"automatic observation lock is not a regular file",
|
|
294
|
-
);
|
|
295
|
-
let stale = false;
|
|
296
|
-
try {
|
|
297
|
-
const record = JSON.parse(readFileSync(path, "utf8")) as {
|
|
298
|
-
pid?: unknown;
|
|
299
|
-
};
|
|
300
|
-
stale =
|
|
301
|
-
Number.isInteger(record.pid) &&
|
|
302
|
-
Number(record.pid) > 0 &&
|
|
303
|
-
!processIsAlive(Number(record.pid));
|
|
304
|
-
} catch {
|
|
305
|
-
stale = Date.now() - before.mtimeMs > 30_000;
|
|
306
|
-
}
|
|
307
|
-
if (!stale) return false;
|
|
308
|
-
const after = lstatSync(path);
|
|
309
|
-
if (
|
|
310
|
-
after.isSymbolicLink() ||
|
|
311
|
-
after.dev !== before.dev ||
|
|
312
|
-
after.ino !== before.ino
|
|
313
|
-
)
|
|
314
|
-
throw new AutomaticObservationSecurityError(
|
|
315
|
-
"automatic observation lock identity changed",
|
|
316
|
-
);
|
|
317
|
-
rmSync(path);
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function withLock<T>(root: string, operation: () => T): T {
|
|
322
|
-
const { memory } = ensureMemoryDirectory(root);
|
|
323
|
-
const path = resolve(memory, LOCK_RELATIVE_PATH.split("/").at(-1)!);
|
|
324
|
-
let fd: number | null = null;
|
|
325
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
326
|
-
try {
|
|
327
|
-
fd = openSync(
|
|
328
|
-
path,
|
|
329
|
-
constants.O_WRONLY |
|
|
330
|
-
constants.O_CREAT |
|
|
331
|
-
constants.O_EXCL |
|
|
332
|
-
(constants.O_NOFOLLOW ?? 0),
|
|
333
|
-
0o600,
|
|
334
|
-
);
|
|
335
|
-
break;
|
|
336
|
-
} catch (error) {
|
|
337
|
-
if (
|
|
338
|
-
attempt === 0 &&
|
|
339
|
-
(error as NodeJS.ErrnoException).code === "EEXIST" &&
|
|
340
|
-
clearStaleLock(path)
|
|
341
|
-
)
|
|
342
|
-
continue;
|
|
343
|
-
throw new AutomaticObservationConflictError(
|
|
344
|
-
`automatic observation lock is busy: ${error instanceof Error ? error.message : error}`,
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (fd === null)
|
|
349
|
-
throw new AutomaticObservationConflictError(
|
|
350
|
-
"automatic observation lock could not be acquired",
|
|
351
|
-
);
|
|
352
|
-
const identity = fstatSync(fd);
|
|
353
|
-
try {
|
|
354
|
-
writeFileSync(
|
|
355
|
-
fd,
|
|
356
|
-
`${JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() })}\n`,
|
|
357
|
-
"utf8",
|
|
358
|
-
);
|
|
359
|
-
fsyncSync(fd);
|
|
360
|
-
return operation();
|
|
361
|
-
} finally {
|
|
362
|
-
closeSync(fd);
|
|
363
|
-
if (existsSync(path)) {
|
|
364
|
-
const current = lstatSync(path);
|
|
365
|
-
if (
|
|
366
|
-
!current.isSymbolicLink() &&
|
|
367
|
-
current.dev === identity.dev &&
|
|
368
|
-
current.ino === identity.ino
|
|
369
|
-
)
|
|
370
|
-
rmSync(path);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function readAutomaticObservationsV2(
|
|
376
|
-
root: string,
|
|
377
|
-
): V3AuthorityObservationV2[] {
|
|
378
|
-
const path = journalPath(root);
|
|
379
|
-
assertRegularPathOrMissing(path);
|
|
380
|
-
if (!existsSync(path)) return [];
|
|
381
|
-
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
382
|
-
try {
|
|
383
|
-
const before = fstatSync(fd);
|
|
384
|
-
if (before.size > MAX_JOURNAL_BYTES)
|
|
385
|
-
throw new AutomaticObservationSecurityError(
|
|
386
|
-
"automatic observation journal exceeds the read limit",
|
|
387
|
-
);
|
|
388
|
-
const content = readFileSync(fd, "utf8");
|
|
389
|
-
const after = fstatSync(fd);
|
|
390
|
-
if (before.dev !== after.dev || before.ino !== after.ino)
|
|
391
|
-
throw new AutomaticObservationSecurityError(
|
|
392
|
-
"automatic observation journal identity changed",
|
|
393
|
-
);
|
|
394
|
-
return content
|
|
395
|
-
.split("\n")
|
|
396
|
-
.filter((line) => line.trim())
|
|
397
|
-
.map((line, index) => {
|
|
398
|
-
try {
|
|
399
|
-
return parseAutomaticObservationV2(JSON.parse(line));
|
|
400
|
-
} catch (error) {
|
|
401
|
-
throw new AutomaticObservationSecurityError(
|
|
402
|
-
`automatic observation line ${index + 1} is invalid: ${error instanceof Error ? error.message : error}`,
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
} finally {
|
|
407
|
-
closeSync(fd);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
export function appendAutomaticObservationV2(
|
|
412
|
-
root: string,
|
|
413
|
-
input: V3AuthorityObservationV2,
|
|
414
|
-
): "appended" | "duplicate" {
|
|
415
|
-
const observation = parseAutomaticObservationV2(input);
|
|
416
|
-
return withLock(root, () => {
|
|
417
|
-
for (const existing of readAutomaticObservationsV2(root)) {
|
|
418
|
-
if (existing.receipt_record_id !== observation.receipt_record_id)
|
|
419
|
-
continue;
|
|
420
|
-
if (existing.observation_id === observation.observation_id)
|
|
421
|
-
return "duplicate";
|
|
422
|
-
throw new AutomaticObservationConflictError(
|
|
423
|
-
`automatic observation receipt identity conflict: ${observation.receipt_record_id}`,
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
ensureMemoryDirectory(root);
|
|
427
|
-
const path = resolve(root, JOURNAL_RELATIVE_PATH);
|
|
428
|
-
assertRegularPathOrMissing(path);
|
|
429
|
-
const fd = openSync(
|
|
430
|
-
path,
|
|
431
|
-
constants.O_WRONLY |
|
|
432
|
-
constants.O_CREAT |
|
|
433
|
-
constants.O_APPEND |
|
|
434
|
-
(constants.O_NOFOLLOW ?? 0),
|
|
435
|
-
0o600,
|
|
436
|
-
);
|
|
437
|
-
try {
|
|
438
|
-
writeFileSync(fd, `${JSON.stringify(observation)}\n`, "utf8");
|
|
439
|
-
fsyncSync(fd);
|
|
440
|
-
} finally {
|
|
441
|
-
closeSync(fd);
|
|
442
|
-
}
|
|
443
|
-
const directoryFd = openSync(dirname(path), constants.O_RDONLY);
|
|
444
|
-
try {
|
|
445
|
-
fsyncSync(directoryFd);
|
|
446
|
-
} finally {
|
|
447
|
-
closeSync(directoryFd);
|
|
448
|
-
}
|
|
449
|
-
return "appended";
|
|
450
|
-
});
|
|
451
|
-
}
|