immune-brain 4.5.0 → 5.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/README.md +157 -39
- package/README.zh-CN.md +160 -40
- 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 +42 -45
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +189 -476
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +77 -102
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +7 -2
- package/plugins/immune-brain/.pi-extension/tsconfig.json +2 -2
- package/plugins/immune-brain/agents/immune-brain-advisory-reviewer.md +23 -0
- package/plugins/immune-brain/agents/immune-brain-qa.md +42 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +90 -1
- package/plugins/immune-brain/agents/immune-brain-ui-review.md +21 -0
- package/plugins/immune-brain/dist/BASELINE.md +4 -4
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +9924 -9654
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
- package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
- package/plugins/immune-brain/dist/imm-planner.md +60 -20
- package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
- package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
- package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +44 -11
- package/plugins/immune-brain/dist/registry.yaml +9 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +138 -69
- package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +135 -396
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +5 -4
- package/plugins/immune-brain/runtime/claude/review_host.ts +63 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +83 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
- package/plugins/immune-brain/runtime/kernel/application.ts +7 -7
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
- 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 +6 -5
- 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 +170 -17
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
- 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/loop_contract.ts +22 -22
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +27 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +66 -28
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +132 -45
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +40 -36
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +98 -209
- 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 +78 -8
- package/plugins/immune-brain/skills/BASELINE.md +4 -4
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
- package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/registry.yaml +9 -9
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -584
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type MutationAuthorityRegistry,
|
|
10
10
|
} from "./authority_port";
|
|
11
11
|
import { asTaskDiffSnapshot } from "./completion";
|
|
12
|
+
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
12
13
|
import { canonicalIntentHash, readTaskIntent } from "./intent";
|
|
13
14
|
import {
|
|
14
15
|
inspectIntentTokenPair,
|
|
@@ -51,9 +52,9 @@ export interface ApplyTaskActionInput {
|
|
|
51
52
|
/** Paired mutation authority registry that must recognize every supplied capability. */
|
|
52
53
|
registry: MutationAuthorityRegistry;
|
|
53
54
|
capability?: MutationAuthorityCapabilityV2;
|
|
54
|
-
/** Trusted injected diff provider; the action's diff_hash is only an expectation.
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
/** Trusted injected diff provider; the action's diff_hash is only an expectation.
|
|
56
|
+
* Defaults to the shared delivery identity selector when omitted. */
|
|
57
|
+
diffProvider?: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
|
|
57
58
|
/**
|
|
58
59
|
* Terminal commit mode: record + workspace + active-claim removal + task
|
|
59
60
|
* tombstone converge through one recoverable terminal transaction marker
|
|
@@ -72,8 +73,8 @@ export interface ApplyTaskActionInput {
|
|
|
72
73
|
export function applyTaskAction(
|
|
73
74
|
input: ApplyTaskActionInput,
|
|
74
75
|
): StoredTaskMutationV3 {
|
|
75
|
-
const { root, task_id, prior_intent_token, registry, capability
|
|
76
|
-
|
|
76
|
+
const { root, task_id, prior_intent_token, registry, capability } = input;
|
|
77
|
+
const diffProvider = input.diffProvider ?? taskDeliveryIdentity;
|
|
77
78
|
return withKernelStoreLock(root, () => {
|
|
78
79
|
const current = readTaskRecordRaw(root, task_id);
|
|
79
80
|
if (!current.record)
|
|
@@ -192,7 +193,7 @@ export function applyTaskAction(
|
|
|
192
193
|
// replay identity, and conflicting reuse, so any throw is a zero-write
|
|
193
194
|
// failure (including stale record/workspace CAS).
|
|
194
195
|
const inspectedAudit = expectedAuthority
|
|
195
|
-
? registry.inspect(capability, expectedAuthority
|
|
196
|
+
? registry.inspect(capability, expectedAuthority)
|
|
196
197
|
: null;
|
|
197
198
|
const authorityRunId = inspectedAudit?.run_id;
|
|
198
199
|
const mutation = reduceTask(
|
|
@@ -237,7 +238,6 @@ export function applyTaskAction(
|
|
|
237
238
|
registry.consume(
|
|
238
239
|
capability as MutationAuthorityCapabilityV2,
|
|
239
240
|
expectedAuthority,
|
|
240
|
-
now,
|
|
241
241
|
);
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// projection that binds a task's backend claim, TaskRecord v3, workspace,
|
|
3
3
|
// attestations, findings, and completion facts into one closed result.
|
|
4
4
|
//
|
|
5
|
-
// This module is deliberately INTERNAL to the Kernel runtime:
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// This module is deliberately INTERNAL to the Kernel runtime: both host
|
|
6
|
+
// adapters import it statically and it is never re-exported from ./index.ts.
|
|
7
|
+
// Hosts consume the projection without
|
|
8
8
|
// reconstructing evidence/approval freshness or authorization readiness.
|
|
9
9
|
//
|
|
10
10
|
// It does not know about Pi session state, pending native Review verdicts,
|
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
import { readBackendClaim, readTaskTombstone } from "./backend_claim";
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
readTaskRecordRaw,
|
|
17
17
|
readAuditTaskPair,
|
|
18
|
+
localRunId,
|
|
18
19
|
readCommittedRecord,
|
|
19
20
|
readWorkspaceStateRaw,
|
|
20
21
|
reconcileKernelAuthority,
|
|
21
22
|
} from "./storage";
|
|
22
23
|
import { projectTask, resolveProjectedRisk } from "./completion";
|
|
24
|
+
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
23
25
|
import type { AssuranceObligation, TaskIntentV1, TaskRecord, TaskRecordV3 } from "./types";
|
|
24
26
|
import type { TaskRecordV2 } from "./legacy_task_record";
|
|
25
27
|
|
|
@@ -244,7 +246,9 @@ function projectFromRecord(
|
|
|
244
246
|
/**
|
|
245
247
|
* One closed read-only projection for a task. The injected diffProvider keeps
|
|
246
248
|
* this module free of git/worktree concerns; hosts supply it from their own
|
|
247
|
-
* task-scope diff implementation.
|
|
249
|
+
* task-scope diff implementation. When no provider is supplied the shared
|
|
250
|
+
* delivery identity selector (workspace_scope.taskDeliveryIdentity) computes
|
|
251
|
+
* it; a supplied provider still overrides it (I5).
|
|
248
252
|
*
|
|
249
253
|
* Error semantics:
|
|
250
254
|
* - no authority facts -> error null, claim null, empty projection
|
|
@@ -257,7 +261,7 @@ function projectFromRecord(
|
|
|
257
261
|
export async function projectAssurance(
|
|
258
262
|
root: string,
|
|
259
263
|
taskId: string,
|
|
260
|
-
diffProvider: (root: string, record: TaskRecord) => TaskDiffSnapshot,
|
|
264
|
+
diffProvider: (root: string, record: TaskRecord) => TaskDiffSnapshot = taskDeliveryIdentity,
|
|
261
265
|
): Promise<AssuranceProjectionResult> {
|
|
262
266
|
const fail = (
|
|
263
267
|
error: string,
|
|
@@ -303,7 +307,7 @@ export async function projectAssurance(
|
|
|
303
307
|
}
|
|
304
308
|
let read;
|
|
305
309
|
try {
|
|
306
|
-
read = await
|
|
310
|
+
read = await readTaskRecordRaw(root, taskId);
|
|
307
311
|
} catch (error) {
|
|
308
312
|
if (!terminalOwner || !(error instanceof Error) || !error.message.startsWith("TaskRecord v2")) throw error;
|
|
309
313
|
return fail(error instanceof Error ? error.message : String(error));
|
|
@@ -332,7 +336,10 @@ export async function projectAssurance(
|
|
|
332
336
|
},
|
|
333
337
|
};
|
|
334
338
|
}
|
|
335
|
-
|
|
339
|
+
// Run-aware: with a local run row the store already answered above;
|
|
340
|
+
// without one (a fresh clone or pre-run-layout evidence) only the
|
|
341
|
+
// resolved pair may serve, never another run's directory by drift.
|
|
342
|
+
const auditPair = await readAuditTaskPair(root, taskId, localRunId(root, taskId) ?? undefined);
|
|
336
343
|
if (!auditPair) return fail(`task ${taskId} has no terminal audit pair`, claim);
|
|
337
344
|
const workspace = await readWorkspaceStateRaw(root);
|
|
338
345
|
if (auditPair.record.contract === "assurance_kernel/task_record/v2")
|
|
@@ -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
|
};
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./authority_port";
|
|
15
15
|
import { applyTaskAction } from "./application";
|
|
16
16
|
import { asTaskDiffSnapshot } from "./completion";
|
|
17
|
+
import { taskDeliveryIdentity } from "../workspace_scope";
|
|
17
18
|
import { readTaskIntent } from "./intent";
|
|
18
19
|
import {
|
|
19
20
|
inspectIntentTokenPair,
|
|
@@ -67,8 +68,9 @@ export interface CanaryExecuteInput {
|
|
|
67
68
|
task_id: string;
|
|
68
69
|
operation: CanaryOperation;
|
|
69
70
|
prior_intent_token: TaskIntentIdentityToken;
|
|
70
|
-
/** Trusted injected diff provider; the application derives the diff identity.
|
|
71
|
-
|
|
71
|
+
/** Trusted injected diff provider; the application derives the diff identity.
|
|
72
|
+
* Defaults to the shared delivery identity selector when omitted. */
|
|
73
|
+
diffProvider?: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
|
|
72
74
|
now?: string;
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -313,7 +315,7 @@ export function createCanaryApplication(
|
|
|
313
315
|
record: current.record,
|
|
314
316
|
};
|
|
315
317
|
});
|
|
316
|
-
const diffHash = asTaskDiffSnapshot(input.diffProvider(input.root, snapshot.record)).diff_hash;
|
|
318
|
+
const diffHash = asTaskDiffSnapshot((input.diffProvider ?? taskDeliveryIdentity)(input.root, snapshot.record)).diff_hash;
|
|
317
319
|
if (operation.op === "stop" && !("capability" in operation))
|
|
318
320
|
throw new KernelInvariantError(["stop requires user authority capability"]);
|
|
319
321
|
const hasBoundSpec = boundSpecPath(snapshot.intent_snapshot) !== undefined;
|
|
@@ -431,7 +433,6 @@ export function createCanaryApplication(
|
|
|
431
433
|
registry,
|
|
432
434
|
capability: capability as never,
|
|
433
435
|
diffProvider: input.diffProvider,
|
|
434
|
-
now: Date.parse(now),
|
|
435
436
|
...(operation.op === "complete" || operation.op === "stop"
|
|
436
437
|
? { terminal: { terminalized_at: now } }
|
|
437
438
|
: {}),
|
|
@@ -492,7 +493,7 @@ export function createCanaryApplication(
|
|
|
492
493
|
intent_revision: current.record.intent_snapshot.revision,
|
|
493
494
|
intent_content_hash: current.record.intent_ref.content_hash,
|
|
494
495
|
diff_hash: "sha256:" + "0".repeat(64),
|
|
495
|
-
}
|
|
496
|
+
});
|
|
496
497
|
const nextClaim: BackendClaim = {
|
|
497
498
|
...claim,
|
|
498
499
|
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
|
},
|