immune-brain 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +0 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +47 -2
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +23 -18
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -3
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +679 -762
- package/plugins/immune-brain/dist/imm-loop.md +11 -4
- package/plugins/immune-brain/dist/imm-planner.md +37 -12
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +55 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
- package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
- package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
- package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +9 -1
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +120 -31
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +6 -7
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +72 -119
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
- package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +44 -8
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
|
@@ -53,7 +53,6 @@ export interface ApplyTaskActionInput {
|
|
|
53
53
|
capability?: MutationAuthorityCapabilityV2;
|
|
54
54
|
/** Trusted injected diff provider; the action's diff_hash is only an expectation. */
|
|
55
55
|
diffProvider: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
|
|
56
|
-
now?: number;
|
|
57
56
|
/**
|
|
58
57
|
* Terminal commit mode: record + workspace + active-claim removal + task
|
|
59
58
|
* tombstone converge through one recoverable terminal transaction marker
|
|
@@ -72,8 +71,7 @@ export interface ApplyTaskActionInput {
|
|
|
72
71
|
export function applyTaskAction(
|
|
73
72
|
input: ApplyTaskActionInput,
|
|
74
73
|
): StoredTaskMutationV3 {
|
|
75
|
-
const { root, task_id, prior_intent_token, registry, capability, diffProvider
|
|
76
|
-
input;
|
|
74
|
+
const { root, task_id, prior_intent_token, registry, capability, diffProvider } = input;
|
|
77
75
|
return withKernelStoreLock(root, () => {
|
|
78
76
|
const current = readTaskRecordRaw(root, task_id);
|
|
79
77
|
if (!current.record)
|
|
@@ -192,7 +190,7 @@ export function applyTaskAction(
|
|
|
192
190
|
// replay identity, and conflicting reuse, so any throw is a zero-write
|
|
193
191
|
// failure (including stale record/workspace CAS).
|
|
194
192
|
const inspectedAudit = expectedAuthority
|
|
195
|
-
? registry.inspect(capability, expectedAuthority
|
|
193
|
+
? registry.inspect(capability, expectedAuthority)
|
|
196
194
|
: null;
|
|
197
195
|
const authorityRunId = inspectedAudit?.run_id;
|
|
198
196
|
const mutation = reduceTask(
|
|
@@ -237,7 +235,6 @@ export function applyTaskAction(
|
|
|
237
235
|
registry.consume(
|
|
238
236
|
capability as MutationAuthorityCapabilityV2,
|
|
239
237
|
expectedAuthority,
|
|
240
|
-
now,
|
|
241
238
|
);
|
|
242
239
|
}
|
|
243
240
|
|
|
@@ -62,17 +62,15 @@ export interface MutationAuthorityRegistry {
|
|
|
62
62
|
readonly brand: symbol;
|
|
63
63
|
/** Issue one capability bound to an exact binding. Library primitive; no production caller in P2B2. */
|
|
64
64
|
issue(binding: CapabilityBindingV2, issuedAt?: string): MutationAuthorityCapabilityV2;
|
|
65
|
-
/** Inspect without consuming. Fails on missing,
|
|
65
|
+
/** Inspect without consuming. Fails on missing, mismatched, or reused capability. */
|
|
66
66
|
inspect(
|
|
67
67
|
capability: MutationAuthorityCapabilityV2 | undefined,
|
|
68
68
|
expected: MutationAuthorityInspection,
|
|
69
|
-
now?: number,
|
|
70
69
|
): ValidatedAuthorityV2;
|
|
71
70
|
/** Consume irreversibly. Returns the validated authority. */
|
|
72
71
|
consume(
|
|
73
72
|
capability: MutationAuthorityCapabilityV2,
|
|
74
73
|
expected: MutationAuthorityInspection,
|
|
75
|
-
now?: number,
|
|
76
74
|
): ValidatedAuthorityV2;
|
|
77
75
|
isConsumed(capability: MutationAuthorityCapabilityV2): boolean;
|
|
78
76
|
}
|
|
@@ -88,7 +86,7 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
|
|
|
88
86
|
const inner = createCapabilityRegistry<CapabilityBindingV2, MutationAuthorityInspection, ValidatedAuthorityV2, MutationAuthorityCapabilityV2>(
|
|
89
87
|
MUTATION_AUTHORITY_CAPABILITY_BRAND,
|
|
90
88
|
{
|
|
91
|
-
validateBinding(binding
|
|
89
|
+
validateBinding(binding) {
|
|
92
90
|
const missing: string[] = [];
|
|
93
91
|
if (binding.run_id !== undefined && binding.run_id.length === 0)
|
|
94
92
|
throw new Error("authority capability run_id must not be empty");
|
|
@@ -100,12 +98,8 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
|
|
|
100
98
|
throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
|
|
101
99
|
if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
|
|
102
100
|
throw new Error("authority capability findings_digest must be a canonical sha256 hash");
|
|
103
|
-
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
104
|
-
throw new Error("authority capability must have a future expiry");
|
|
105
101
|
},
|
|
106
|
-
validateAndProject(state, expected
|
|
107
|
-
if (Date.parse(state.expires_at) <= now)
|
|
108
|
-
throw new Error("authority capability has expired");
|
|
102
|
+
validateAndProject(state, expected) {
|
|
109
103
|
const actionDigest = digestOfAction(expected.action);
|
|
110
104
|
if (state.action_digest !== actionDigest)
|
|
111
105
|
throw new Error("authority capability action digest mismatch");
|
|
@@ -141,7 +135,6 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
|
|
|
141
135
|
actor_id: canonicalActorId(state.actor_id),
|
|
142
136
|
confirmation_ref: state.confirmation_ref,
|
|
143
137
|
issued_at: state.issued_at,
|
|
144
|
-
expires_at: state.expires_at,
|
|
145
138
|
},
|
|
146
139
|
action_digest: actionDigest,
|
|
147
140
|
...(state.run_id !== undefined ? { run_id: state.run_id } : {}),
|
|
@@ -162,12 +155,11 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
|
|
|
162
155
|
inspect(
|
|
163
156
|
capability: MutationAuthorityCapabilityV2 | undefined,
|
|
164
157
|
expected: MutationAuthorityInspection,
|
|
165
|
-
now = Date.now(),
|
|
166
158
|
): ValidatedAuthorityV2 {
|
|
167
159
|
if (!capability)
|
|
168
160
|
throw new Error("privileged action requires an opaque authority capability");
|
|
169
161
|
try {
|
|
170
|
-
return inner.inspect(capability, expected
|
|
162
|
+
return inner.inspect(capability, expected);
|
|
171
163
|
} catch (err) {
|
|
172
164
|
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
173
165
|
throw new Error("privileged action requires an opaque authority capability");
|
|
@@ -177,10 +169,9 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
|
|
|
177
169
|
consume(
|
|
178
170
|
capability: MutationAuthorityCapabilityV2,
|
|
179
171
|
expected: MutationAuthorityInspection,
|
|
180
|
-
now = Date.now(),
|
|
181
172
|
): ValidatedAuthorityV2 {
|
|
182
173
|
try {
|
|
183
|
-
return inner.consume(capability, expected
|
|
174
|
+
return inner.consume(capability, expected);
|
|
184
175
|
} catch (err) {
|
|
185
176
|
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
186
177
|
throw new Error("privileged action requires an opaque authority capability");
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// plus an advancing HEAD lineage instead.
|
|
9
9
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
11
12
|
import { createCapabilityRegistry } from "./capability_registry";
|
|
12
13
|
import { isLiteralUserActor } from "./actor_identity";
|
|
13
14
|
import type { BaseCapabilityBinding, EnrollmentCapabilityBinding } from "./enrollment_authority";
|
|
@@ -19,15 +20,6 @@ export const BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for(
|
|
|
19
20
|
|
|
20
21
|
const GIT_COMMIT_ID = /^[a-f0-9]{40}$/;
|
|
21
22
|
|
|
22
|
-
/** review-7(3rd rework): typed expiry marker thrown at the Kernel enrollment
|
|
23
|
-
* boundary so the driver can classify a real enrollment-time expiry as an
|
|
24
|
-
* intentional budget stop without free-form message matching. */
|
|
25
|
-
export class BatchAuthorizationExpiryError extends Error {
|
|
26
|
-
constructor(message: string) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = "BatchAuthorizationExpiryError";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
23
|
|
|
32
24
|
export interface BatchPlanChild {
|
|
33
25
|
task_id: string;
|
|
@@ -39,7 +31,6 @@ export interface BatchPlanChild {
|
|
|
39
31
|
|
|
40
32
|
export interface BatchBudget {
|
|
41
33
|
max_children: number;
|
|
42
|
-
deadline_at: string;
|
|
43
34
|
qa_failure_limit: number;
|
|
44
35
|
}
|
|
45
36
|
|
|
@@ -63,7 +54,6 @@ export interface ValidatedBatchAuthorization {
|
|
|
63
54
|
actor_id: string;
|
|
64
55
|
confirmation_ref: string;
|
|
65
56
|
issued_at: string;
|
|
66
|
-
expires_at: string;
|
|
67
57
|
nonce: string;
|
|
68
58
|
}
|
|
69
59
|
|
|
@@ -81,7 +71,6 @@ export interface BatchAuthorityRegistry {
|
|
|
81
71
|
inspect(
|
|
82
72
|
capability: object,
|
|
83
73
|
expected: BatchAuthorizationBinding,
|
|
84
|
-
now?: number,
|
|
85
74
|
): ValidatedBatchAuthorization;
|
|
86
75
|
children(capability: object): BatchPlanChild[];
|
|
87
76
|
consumedChildren(capability: object): string[];
|
|
@@ -91,7 +80,6 @@ export interface BatchAuthorityRegistry {
|
|
|
91
80
|
capability: object,
|
|
92
81
|
expected: BatchAuthorizationBinding,
|
|
93
82
|
taskId: string,
|
|
94
|
-
now?: number,
|
|
95
83
|
): ValidatedBatchAuthorization;
|
|
96
84
|
/** Undo one slot consumption when the bound write did not commit. */
|
|
97
85
|
releaseChild(capability: object, taskId: string): void;
|
|
@@ -138,7 +126,6 @@ function requireNonEmpty(binding: BatchAuthorizationBinding): void {
|
|
|
138
126
|
"base_head",
|
|
139
127
|
"actor_id",
|
|
140
128
|
"confirmation_ref",
|
|
141
|
-
"expires_at",
|
|
142
129
|
"nonce",
|
|
143
130
|
] as const) {
|
|
144
131
|
const value = binding[key];
|
|
@@ -149,14 +136,11 @@ function requireNonEmpty(binding: BatchAuthorizationBinding): void {
|
|
|
149
136
|
throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
|
|
150
137
|
}
|
|
151
138
|
|
|
152
|
-
function validateBudget(budget: BatchBudget
|
|
139
|
+
function validateBudget(budget: BatchBudget): void {
|
|
153
140
|
if (!Number.isInteger(budget.max_children) || budget.max_children <= 0)
|
|
154
141
|
throw new Error("batch budget max_children must be a positive integer");
|
|
155
142
|
if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
|
|
156
143
|
throw new Error("batch budget qa_failure_limit must be a positive integer");
|
|
157
|
-
const deadline = Date.parse(budget.deadline_at);
|
|
158
|
-
if (Number.isNaN(deadline) || deadline <= Date.parse(issuedAt))
|
|
159
|
-
throw new Error("batch budget must have a future deadline_at");
|
|
160
144
|
}
|
|
161
145
|
|
|
162
146
|
function validateChildren(children: BatchPlanChild[], planDigest: string): void {
|
|
@@ -210,7 +194,7 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
|
|
|
210
194
|
>(
|
|
211
195
|
BATCH_AUTHORITY_CAPABILITY_BRAND,
|
|
212
196
|
{
|
|
213
|
-
validateBinding(binding
|
|
197
|
+
validateBinding(binding) {
|
|
214
198
|
requireNonEmpty(binding);
|
|
215
199
|
// Both the canonical literal-user spelling and the historical `user`
|
|
216
200
|
// spelling are read, so a batch authorized before the convergence keeps
|
|
@@ -219,29 +203,15 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
|
|
|
219
203
|
throw new Error("batch authorization requires a literal-user actor_id");
|
|
220
204
|
if (!GIT_COMMIT_ID.test(binding.base_head))
|
|
221
205
|
throw new Error("batch authorization base_head must be a committed 40-hex commit id");
|
|
222
|
-
|
|
223
|
-
if (Number.isNaN(expires) || expires <= Date.parse(issuedAt))
|
|
224
|
-
throw new Error("batch authorization must have a future expiry");
|
|
225
|
-
validateBudget(binding.budget, issuedAt);
|
|
206
|
+
validateBudget(binding.budget);
|
|
226
207
|
},
|
|
227
|
-
validateAndProject(state, expected
|
|
228
|
-
// Fail closed on an unusable clock. `now` reaches here as
|
|
229
|
-
// Date.parse(...) from callers, and NaN makes every `<=` compare
|
|
230
|
-
// false, which would silently accept an expired authorization.
|
|
231
|
-
if (!Number.isFinite(now))
|
|
232
|
-
throw new Error("batch authorization requires a valid clock");
|
|
233
|
-
const expires = Date.parse(state.expires_at);
|
|
234
|
-
if (Number.isNaN(expires) || expires <= now)
|
|
235
|
-
throw new BatchAuthorizationExpiryError("batch authorization has expired");
|
|
236
|
-
if (Date.parse(state.budget.deadline_at) <= now)
|
|
237
|
-
throw new BatchAuthorizationExpiryError("batch authorization deadline has expired");
|
|
208
|
+
validateAndProject(state, expected) {
|
|
238
209
|
for (const key of Object.keys(expected) as Array<keyof BatchAuthorizationBinding>) {
|
|
239
210
|
if (key === "budget") {
|
|
240
211
|
const a = state.budget ?? ({} as BatchBudget);
|
|
241
212
|
const b = expected.budget ?? ({} as BatchBudget);
|
|
242
213
|
if (
|
|
243
214
|
a.max_children !== b.max_children ||
|
|
244
|
-
a.deadline_at !== b.deadline_at ||
|
|
245
215
|
a.qa_failure_limit !== b.qa_failure_limit
|
|
246
216
|
)
|
|
247
217
|
throw new Error("batch authorization budget mismatch");
|
|
@@ -263,7 +233,6 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
|
|
|
263
233
|
actor_id: state.actor_id,
|
|
264
234
|
confirmation_ref: state.confirmation_ref,
|
|
265
235
|
issued_at: state.issued_at,
|
|
266
|
-
expires_at: state.expires_at,
|
|
267
236
|
nonce: state.nonce,
|
|
268
237
|
};
|
|
269
238
|
},
|
|
@@ -299,9 +268,9 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
|
|
|
299
268
|
consumed.set(capability, new Set<string>());
|
|
300
269
|
return capability;
|
|
301
270
|
},
|
|
302
|
-
inspect(capability, expected
|
|
271
|
+
inspect(capability, expected) {
|
|
303
272
|
planOf(capability);
|
|
304
|
-
return inner.inspect(capability, expected
|
|
273
|
+
return inner.inspect(capability, expected);
|
|
305
274
|
},
|
|
306
275
|
children(capability) {
|
|
307
276
|
return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
|
|
@@ -312,8 +281,8 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
|
|
|
312
281
|
isChildConsumed(capability, taskId) {
|
|
313
282
|
return slotsOf(capability).has(taskId);
|
|
314
283
|
},
|
|
315
|
-
consumeChild(capability, expected, taskId
|
|
316
|
-
const validated = this.inspect(capability, expected
|
|
284
|
+
consumeChild(capability, expected, taskId) {
|
|
285
|
+
const validated = this.inspect(capability, expected);
|
|
317
286
|
const plan = planOf(capability);
|
|
318
287
|
if (!plan.some((child) => child.task_id === taskId))
|
|
319
288
|
throw new Error(`batch_child_not_in_plan: ${taskId}`);
|
|
@@ -331,6 +300,24 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
|
|
|
331
300
|
};
|
|
332
301
|
}
|
|
333
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Before a batch has consumed a slot it has created no commit, so its lineage
|
|
305
|
+
* can only stand on the confirmed base_head or on a fast-forward of it: the
|
|
306
|
+
* user's own commit on the batch branch, which the runner adopts. A head that
|
|
307
|
+
* does not descend from base_head is not a lineage.
|
|
308
|
+
*/
|
|
309
|
+
export function assertBatchLineageOrigin(root: string, baseHead: string, expectedHead: string): void {
|
|
310
|
+
if (expectedHead === baseHead) return;
|
|
311
|
+
const descends =
|
|
312
|
+
spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", baseHead, expectedHead], {
|
|
313
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
314
|
+
}).status === 0;
|
|
315
|
+
if (!descends)
|
|
316
|
+
throw new Error(
|
|
317
|
+
`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${baseHead} or a fast-forward of it, not ${expectedHead}`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
334
321
|
export interface DeriveChildEnrollmentInput {
|
|
335
322
|
capability: object;
|
|
336
323
|
binding: BatchAuthorizationBinding;
|
|
@@ -355,7 +342,7 @@ export function deriveChildEnrollment(
|
|
|
355
342
|
registry: BatchAuthorityRegistry,
|
|
356
343
|
input: DeriveChildEnrollmentInput,
|
|
357
344
|
): DerivedChildEnrollment {
|
|
358
|
-
const validated = registry.inspect(input.capability, input.binding
|
|
345
|
+
const validated = registry.inspect(input.capability, input.binding);
|
|
359
346
|
const child = registry
|
|
360
347
|
.children(input.capability)
|
|
361
348
|
.find((entry) => entry.task_id === input.task_id);
|
|
@@ -385,13 +372,8 @@ export function deriveChildEnrollment(
|
|
|
385
372
|
// settled a child there is no commit it could have created, so a
|
|
386
373
|
// caller-supplied expected_head other than base_head is not a lineage.
|
|
387
374
|
// Checked last so the intent-divergence reason keeps its precedence.
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
input.expected_head !== validated.base_head
|
|
391
|
-
)
|
|
392
|
-
throw new Error(
|
|
393
|
-
`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${validated.base_head}, not ${input.expected_head}`,
|
|
394
|
-
);
|
|
375
|
+
if (registry.consumedChildren(input.capability).length === 0)
|
|
376
|
+
assertBatchLineageOrigin(root, validated.base_head, input.expected_head);
|
|
395
377
|
|
|
396
378
|
return {
|
|
397
379
|
child,
|
|
@@ -404,7 +386,6 @@ export function deriveChildEnrollment(
|
|
|
404
386
|
preparation_digest: preparation.digest,
|
|
405
387
|
actor_id: validated.actor_id,
|
|
406
388
|
confirmation_ref: validated.confirmation_ref,
|
|
407
|
-
expires_at: validated.expires_at,
|
|
408
389
|
nonce: `${validated.nonce}:${child.task_id}`,
|
|
409
390
|
},
|
|
410
391
|
};
|
|
@@ -431,7 +431,6 @@ export function createCanaryApplication(
|
|
|
431
431
|
registry,
|
|
432
432
|
capability: capability as never,
|
|
433
433
|
diffProvider: input.diffProvider,
|
|
434
|
-
now: Date.parse(now),
|
|
435
434
|
...(operation.op === "complete" || operation.op === "stop"
|
|
436
435
|
? { terminal: { terminalized_at: now } }
|
|
437
436
|
: {}),
|
|
@@ -492,7 +491,7 @@ export function createCanaryApplication(
|
|
|
492
491
|
intent_revision: current.record.intent_snapshot.revision,
|
|
493
492
|
intent_content_hash: current.record.intent_ref.content_hash,
|
|
494
493
|
diff_hash: "sha256:" + "0".repeat(64),
|
|
495
|
-
}
|
|
494
|
+
});
|
|
496
495
|
const nextClaim: BackendClaim = {
|
|
497
496
|
...claim,
|
|
498
497
|
lifecycle_status: "draining",
|
|
@@ -4,20 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
export interface CapabilityRegistryHooks<TBinding, TExpected, TValidated> {
|
|
6
6
|
/** Validate the binding at issue time. Throw on invalid input. */
|
|
7
|
-
validateBinding(binding: TBinding
|
|
7
|
+
validateBinding(binding: TBinding): void;
|
|
8
8
|
/** Validate stored state against expected input at inspect time. Return the domain-specific validated result. */
|
|
9
9
|
validateAndProject(
|
|
10
10
|
state: TBinding & { issued_at: string },
|
|
11
11
|
expected: TExpected,
|
|
12
|
-
now: number,
|
|
13
12
|
): TValidated;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export interface CapabilityRegistry<TBinding, TExpected, TValidated, TCapability> {
|
|
17
16
|
readonly brand: symbol;
|
|
18
17
|
issue(binding: TBinding, issuedAt?: string): TCapability;
|
|
19
|
-
inspect(capability: TCapability, expected: TExpected
|
|
20
|
-
consume(capability: TCapability, expected: TExpected
|
|
18
|
+
inspect(capability: TCapability, expected: TExpected): TValidated;
|
|
19
|
+
consume(capability: TCapability, expected: TExpected): TValidated;
|
|
21
20
|
isConsumed(capability: TCapability): boolean;
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -53,7 +52,7 @@ export function createCapabilityRegistry<TBinding, TExpected, TValidated, TCapab
|
|
|
53
52
|
return {
|
|
54
53
|
brand,
|
|
55
54
|
issue(binding: TBinding, issuedAt = new Date().toISOString()): TCapability {
|
|
56
|
-
hooks.validateBinding(binding
|
|
55
|
+
hooks.validateBinding(binding);
|
|
57
56
|
const capability = Object.freeze(
|
|
58
57
|
Object.defineProperties(
|
|
59
58
|
{},
|
|
@@ -66,7 +65,7 @@ export function createCapabilityRegistry<TBinding, TExpected, TValidated, TCapab
|
|
|
66
65
|
states.set(capability as object, { binding: { ...binding }, issued_at: issuedAt, consumed: false });
|
|
67
66
|
return capability;
|
|
68
67
|
},
|
|
69
|
-
inspect(capability: TCapability, expected: TExpected
|
|
68
|
+
inspect(capability: TCapability, expected: TExpected): TValidated {
|
|
70
69
|
if (!isCapability(capability))
|
|
71
70
|
throw new Error(`${domainLabel} capability is not recognized by this registry`);
|
|
72
71
|
const state = stateOf(capability);
|
|
@@ -74,11 +73,10 @@ export function createCapabilityRegistry<TBinding, TExpected, TValidated, TCapab
|
|
|
74
73
|
return hooks.validateAndProject(
|
|
75
74
|
{ ...state.binding, issued_at: state.issued_at },
|
|
76
75
|
expected,
|
|
77
|
-
now,
|
|
78
76
|
);
|
|
79
77
|
},
|
|
80
|
-
consume(capability: TCapability, expected: TExpected
|
|
81
|
-
const validated = this.inspect(capability, expected
|
|
78
|
+
consume(capability: TCapability, expected: TExpected): TValidated {
|
|
79
|
+
const validated = this.inspect(capability, expected);
|
|
82
80
|
stateOf(capability).consumed = true;
|
|
83
81
|
return validated;
|
|
84
82
|
},
|
|
@@ -15,8 +15,9 @@ import type {
|
|
|
15
15
|
BatchAuthorizationBinding,
|
|
16
16
|
} from "./batch_authority";
|
|
17
17
|
import type { BackendClaim } from "./backend_claim";
|
|
18
|
+
import { assertBatchLineageOrigin } from "./batch_authority";
|
|
18
19
|
import { preparePiCanary, readGitHead } from "./pi_canary_prepare";
|
|
19
|
-
import { writeEnrollmentBaseline } from "../workspace_scope";
|
|
20
|
+
import { dirtyScopePaths, writeEnrollmentBaseline } from "../workspace_scope";
|
|
20
21
|
import { enrollmentRequestDigest } from "./run_identity";
|
|
21
22
|
import {
|
|
22
23
|
commitEnrollmentLocked,
|
|
@@ -163,6 +164,20 @@ function runEnrollmentPreconditionChecks<T>(
|
|
|
163
164
|
} catch (error) {
|
|
164
165
|
fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
165
166
|
}
|
|
167
|
+
// In-scope dirt present now would be recorded in the Enrollment baseline as
|
|
168
|
+
// user dirt and withheld from every later task snapshot, so the task could
|
|
169
|
+
// never deliver it. Refuse before the capability is consumed.
|
|
170
|
+
if (intent && gitBaseHead) {
|
|
171
|
+
try {
|
|
172
|
+
const dirty = dirtyScopePaths(root, intent.intent.scope_hint, input.task_id);
|
|
173
|
+
if (dirty.length > 0) {
|
|
174
|
+
const message = `task scope is already dirty before Enrollment: ${dirty.join(", ")}; commit, stash or revert these paths first`;
|
|
175
|
+
fail(message, new Error(message));
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
fail(`scope: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
166
181
|
|
|
167
182
|
const state: EnrollmentPreconditionState = {
|
|
168
183
|
validated,
|
|
@@ -332,24 +347,19 @@ export function enrollCanaryTask(
|
|
|
332
347
|
|
|
333
348
|
// A batch-derived enrollment must still stand on the lineage the
|
|
334
349
|
// literal user confirmed: child N's base is the commit child N-1
|
|
335
|
-
// produced on the batch branch, and child 1's base is base_head
|
|
350
|
+
// produced on the batch branch, and child 1's base is base_head or a
|
|
351
|
+
// fast-forward of it the runner adopted.
|
|
336
352
|
// expected_head is caller-supplied, so anchor its origin here rather
|
|
337
353
|
// than trusting the caller's own assertion: before this batch has
|
|
338
354
|
// consumed any slot it has created no commit, so the only lineage
|
|
339
|
-
// value it can hold
|
|
355
|
+
// value it can hold descends from the confirmed base_head.
|
|
340
356
|
if (input.batch) {
|
|
341
357
|
const batch = input.batch.registry.inspect(
|
|
342
358
|
input.batch.capability,
|
|
343
359
|
input.batch.binding,
|
|
344
|
-
Date.parse(input.now),
|
|
345
360
|
);
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
input.batch.expected_head !== batch.base_head
|
|
349
|
-
)
|
|
350
|
-
throw new Error(
|
|
351
|
-
`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${batch.base_head}, not ${input.batch.expected_head}`,
|
|
352
|
-
);
|
|
361
|
+
if (input.batch.registry.consumedChildren(input.batch.capability).length === 0)
|
|
362
|
+
assertBatchLineageOrigin(root, batch.base_head, input.batch.expected_head);
|
|
353
363
|
if (checks.gitBaseHead !== input.batch.expected_head)
|
|
354
364
|
throw new Error(
|
|
355
365
|
`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`,
|
|
@@ -364,7 +374,6 @@ export function enrollCanaryTask(
|
|
|
364
374
|
input.batch.capability,
|
|
365
375
|
input.batch.binding,
|
|
366
376
|
input.task_id,
|
|
367
|
-
Date.parse(input.now),
|
|
368
377
|
);
|
|
369
378
|
|
|
370
379
|
// Set by beforeLock above, which throws when the repository has no
|
|
@@ -7,7 +7,7 @@ import { createCapabilityRegistry } from "./capability_registry";
|
|
|
7
7
|
export const ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* The
|
|
10
|
+
* The two fields every capability binding shares, whichever authority issues
|
|
11
11
|
* it. `nonce` is deliberately not part of this base: the enrollment and batch
|
|
12
12
|
* bindings carry it for their own replay digest, while `CapabilityBindingV2`
|
|
13
13
|
* uses `action_digest` instead and has no `nonce` field at all.
|
|
@@ -15,7 +15,6 @@ export const ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollme
|
|
|
15
15
|
export interface BaseCapabilityBinding {
|
|
16
16
|
actor_id: string;
|
|
17
17
|
confirmation_ref: string;
|
|
18
|
-
expires_at: string;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export interface EnrollmentCapabilityBinding extends BaseCapabilityBinding {
|
|
@@ -36,7 +35,6 @@ export interface ValidatedEnrollment {
|
|
|
36
35
|
actor_id: string;
|
|
37
36
|
confirmation_ref: string;
|
|
38
37
|
issued_at: string;
|
|
39
|
-
expires_at: string;
|
|
40
38
|
nonce: string;
|
|
41
39
|
}
|
|
42
40
|
|
|
@@ -44,8 +42,8 @@ export interface EnrollmentAuthorityRegistry {
|
|
|
44
42
|
readonly brand: symbol;
|
|
45
43
|
/** Issue one capability bound to an exact binding. Library primitive. */
|
|
46
44
|
issue(binding: EnrollmentCapabilityBinding, issuedAt?: string): object;
|
|
47
|
-
inspect(capability: object, expected: EnrollmentCapabilityBinding
|
|
48
|
-
consume(capability: object, expected: EnrollmentCapabilityBinding
|
|
45
|
+
inspect(capability: object, expected: EnrollmentCapabilityBinding): ValidatedEnrollment;
|
|
46
|
+
consume(capability: object, expected: EnrollmentCapabilityBinding): ValidatedEnrollment;
|
|
49
47
|
isConsumed(capability: object): boolean;
|
|
50
48
|
}
|
|
51
49
|
|
|
@@ -53,18 +51,15 @@ export function createEnrollmentAuthorityRegistry(): EnrollmentAuthorityRegistry
|
|
|
53
51
|
return createCapabilityRegistry<EnrollmentCapabilityBinding, EnrollmentCapabilityBinding, ValidatedEnrollment>(
|
|
54
52
|
ENROLLMENT_CAPABILITY_BRAND,
|
|
55
53
|
{
|
|
56
|
-
validateBinding(binding
|
|
54
|
+
validateBinding(binding) {
|
|
57
55
|
const missing: string[] = [];
|
|
58
56
|
for (const [key, value] of Object.entries(binding)) {
|
|
59
57
|
if (value === undefined || value === null || value === "") missing.push(key);
|
|
60
58
|
}
|
|
61
59
|
if (missing.length > 0)
|
|
62
60
|
throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
|
|
63
|
-
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
64
|
-
throw new Error("enrollment capability must have a future expiry");
|
|
65
61
|
},
|
|
66
|
-
validateAndProject(state, expected
|
|
67
|
-
if (Date.parse(state.expires_at) <= now) throw new Error("enrollment capability has expired");
|
|
62
|
+
validateAndProject(state, expected) {
|
|
68
63
|
for (const key of Object.keys(expected) as Array<keyof EnrollmentCapabilityBinding>) {
|
|
69
64
|
if (state[key] !== expected[key])
|
|
70
65
|
throw new Error(`enrollment capability ${key} mismatch`);
|
|
@@ -78,7 +73,6 @@ export function createEnrollmentAuthorityRegistry(): EnrollmentAuthorityRegistry
|
|
|
78
73
|
actor_id: state.actor_id,
|
|
79
74
|
confirmation_ref: state.confirmation_ref,
|
|
80
75
|
issued_at: state.issued_at,
|
|
81
|
-
expires_at: state.expires_at,
|
|
82
76
|
nonce: state.nonce,
|
|
83
77
|
};
|
|
84
78
|
},
|
|
@@ -265,7 +265,8 @@ export interface AuthorityAuditDescriptor {
|
|
|
265
265
|
actor_id: string;
|
|
266
266
|
confirmation_ref: string;
|
|
267
267
|
issued_at: string;
|
|
268
|
-
|
|
268
|
+
/** Read-only: history written before authority lost its clock still carries it. Never written. */
|
|
269
|
+
expires_at?: string;
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
export interface TaskActionBase {
|
|
@@ -287,7 +287,9 @@ function parseHistoryV3(
|
|
|
287
287
|
actor_id: stringAt(auth.actor_id, `${path}.authority.actor_id`, violations),
|
|
288
288
|
confirmation_ref: stringAt(auth.confirmation_ref, `${path}.authority.confirmation_ref`, violations),
|
|
289
289
|
issued_at: stringAt(auth.issued_at, `${path}.authority.issued_at`, violations),
|
|
290
|
-
|
|
290
|
+
...(auth.expires_at !== undefined
|
|
291
|
+
? { expires_at: stringAt(auth.expires_at, `${path}.authority.expires_at`, violations) }
|
|
292
|
+
: {}),
|
|
291
293
|
};
|
|
292
294
|
}
|
|
293
295
|
return {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "4.
|
|
2
|
+
export const PLUGIN_VERSION = "4.6.0" as const;
|
|
@@ -20,6 +20,7 @@ import { dirname, join } from "node:path";
|
|
|
20
20
|
import { localRunId, readAuditTaskPair, readSecureProjectFile } from "../kernel/storage";
|
|
21
21
|
import { captureGitTaskRevisionSnapshot, pathMatchesScope } from "../workspace_scope";
|
|
22
22
|
import { expectedBatchHead, findExistingActiveBatch, findSettledBatchRecord } from "./batch_preflight";
|
|
23
|
+
export { classifyBatchLineage, type BatchLineage } from "./batch_preflight";
|
|
23
24
|
|
|
24
25
|
export type BatchGitPreflightRejectReason =
|
|
25
26
|
| "batch_branch_exists"
|
|
@@ -744,7 +745,14 @@ export async function lookupBatchCommit(input: {
|
|
|
744
745
|
stdio: ["ignore", "pipe", "pipe"],
|
|
745
746
|
});
|
|
746
747
|
const currentHead = currentHeadResult.stdout.trim();
|
|
747
|
-
|
|
748
|
+
// HEAD is the own commit, or a fast-forward the user added on top of it
|
|
749
|
+
// before the runner persisted it.
|
|
750
|
+
if (
|
|
751
|
+
currentHead !== commit &&
|
|
752
|
+
spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", commit, currentHead], {
|
|
753
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
754
|
+
}).status !== 0
|
|
755
|
+
) {
|
|
748
756
|
throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
|
|
749
757
|
}
|
|
750
758
|
const parentsResult = spawnSync("git", ["-C", root, "rev-parse", `${commit}^@`], {
|
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
} from "./types";
|
|
20
20
|
|
|
21
21
|
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
22
|
-
export const DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1_000;
|
|
23
22
|
const DEFAULT_QA_FAILURE_LIMIT = 2;
|
|
24
23
|
|
|
25
24
|
/**
|
|
@@ -123,7 +122,7 @@ function dependencyClosures(order: GithubInitiativeObservation["tasks"]): Map<st
|
|
|
123
122
|
return closures;
|
|
124
123
|
}
|
|
125
124
|
|
|
126
|
-
function budget(input: ProjectBatchPlanInput, enrollableCount: number
|
|
125
|
+
function budget(input: ProjectBatchPlanInput, enrollableCount: number): BatchPlanBudget {
|
|
127
126
|
const maxChildren = input.budget?.max_children === undefined
|
|
128
127
|
? positiveSafeInteger(enrollableCount, "budget.max_children")
|
|
129
128
|
: positiveSafeInteger(input.budget.max_children, "budget.max_children");
|
|
@@ -132,12 +131,7 @@ function budget(input: ProjectBatchPlanInput, enrollableCount: number, confirmat
|
|
|
132
131
|
const qaFailureLimit = input.budget?.qa_failure_limit === undefined
|
|
133
132
|
? DEFAULT_QA_FAILURE_LIMIT
|
|
134
133
|
: positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
|
|
135
|
-
|
|
136
|
-
? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString()
|
|
137
|
-
: timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
|
|
138
|
-
if (Date.parse(deadline) <= confirmationTime.milliseconds)
|
|
139
|
-
throw new Error("budget.deadline_at must be later than confirmation_time");
|
|
140
|
-
return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
|
|
134
|
+
return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
|
|
141
135
|
}
|
|
142
136
|
|
|
143
137
|
export async function projectBatchPlan(
|
|
@@ -238,6 +232,6 @@ export async function projectBatchPlan(
|
|
|
238
232
|
children,
|
|
239
233
|
enrollable,
|
|
240
234
|
plan_digest: planDigest,
|
|
241
|
-
budget: budget(input, enrollable.length
|
|
235
|
+
budget: budget(input, enrollable.length),
|
|
242
236
|
};
|
|
243
237
|
}
|