immune-brain 2.8.3 → 3.0.2
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/.claude-plugin/marketplace.json +16 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +9 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
- package/plugins/immune-brain/.mcp.json +8 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
- package/plugins/immune-brain/dist/imm-loop.md +6 -7
- package/plugins/immune-brain/dist/imm-planner.md +1 -1
- package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
- package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +55 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
- package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
- package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
- package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
- package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
- package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +2 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
- package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
- package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
- package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
- package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
- package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
- package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
KernelStoreSecurityError,
|
|
16
16
|
appendJournalEntry,
|
|
17
17
|
readSecureProjectFile,
|
|
18
|
+
readTaskRecordRaw,
|
|
19
|
+
readWorkspaceStateRaw,
|
|
18
20
|
type JournalEntry,
|
|
19
21
|
type JournalReasonCode,
|
|
20
22
|
type TaskPhase,
|
|
@@ -23,7 +25,12 @@ import {
|
|
|
23
25
|
INTENT_MAX_BYTES,
|
|
24
26
|
canonicalIntentHash,
|
|
25
27
|
parseTaskIntentV1,
|
|
28
|
+
RISK_FLOOR_SCOPE_PREFIXES,
|
|
26
29
|
} from "../kernel/intent";
|
|
30
|
+
import { projectTask, resolveProjectedRisk } from "../kernel/completion";
|
|
31
|
+
import { deriveAssuranceAuthorization } from "../kernel/assurance_projection";
|
|
32
|
+
import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
|
|
33
|
+
import type { TaskIntentV1, TaskRisk } from "../kernel/types";
|
|
27
34
|
import {
|
|
28
35
|
canonicalDescriptorBytes,
|
|
29
36
|
parseVerificationDescriptor,
|
|
@@ -152,6 +159,202 @@ function runStatus(root: string): KernelExecution {
|
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
161
|
|
|
162
|
+
const UNOBSERVABLE = {
|
|
163
|
+
capability: "unobservable",
|
|
164
|
+
rehearsal: "unobservable",
|
|
165
|
+
cas_holder: "unobservable",
|
|
166
|
+
} as const;
|
|
167
|
+
|
|
168
|
+
const RISK_ORDER: Record<TaskRisk, number> = {
|
|
169
|
+
routine: 0,
|
|
170
|
+
material: 1,
|
|
171
|
+
critical: 2,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function matchingFloorEntries(intent: TaskIntentV1): string[] {
|
|
175
|
+
return intent.scope_hint.filter((entry) => {
|
|
176
|
+
try {
|
|
177
|
+
const trial = parseTaskIntentV1({
|
|
178
|
+
contract: intent.contract,
|
|
179
|
+
task_id: intent.task_id,
|
|
180
|
+
goal: intent.goal,
|
|
181
|
+
acceptance: intent.acceptance,
|
|
182
|
+
scope_hint: [entry],
|
|
183
|
+
risk: "routine",
|
|
184
|
+
revision: intent.revision,
|
|
185
|
+
owner: "user",
|
|
186
|
+
});
|
|
187
|
+
return trial.risk !== "routine";
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readWorkspaceOwner(root: string): string | null {
|
|
195
|
+
try {
|
|
196
|
+
const raw = JSON.parse(readSecureProjectFile(root, ".imm/state/workspace.json")) as {
|
|
197
|
+
current_working?: unknown;
|
|
198
|
+
};
|
|
199
|
+
return typeof raw.current_working === "string" ? raw.current_working : null;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* `inspect --json` is a read-only Inspect Projection. It never journals,
|
|
207
|
+
* never takes a Kernel store lock, and never becomes authority.
|
|
208
|
+
*/
|
|
209
|
+
function runInspect(root: string): KernelExecution {
|
|
210
|
+
try {
|
|
211
|
+
const layout = inspectStorageLayout(root);
|
|
212
|
+
const claim = readBackendClaim(root);
|
|
213
|
+
const workspace = readWorkspaceOwner(root);
|
|
214
|
+
const kernel = {
|
|
215
|
+
claim: claim
|
|
216
|
+
? {
|
|
217
|
+
task_id: claim.task_id,
|
|
218
|
+
lifecycle_status: claim.lifecycle_status,
|
|
219
|
+
}
|
|
220
|
+
: null,
|
|
221
|
+
workspace: { current_working: workspace },
|
|
222
|
+
};
|
|
223
|
+
const idleRisk = {
|
|
224
|
+
declared: null as TaskRisk | null,
|
|
225
|
+
resolved: null as TaskRisk | null,
|
|
226
|
+
floor_prefixes: [...RISK_FLOOR_SCOPE_PREFIXES],
|
|
227
|
+
floor_applied: false,
|
|
228
|
+
matching_scope_entries: [] as string[],
|
|
229
|
+
};
|
|
230
|
+
const ok = layout.layout === "ready" || layout.layout === "migration_uncommitted";
|
|
231
|
+
if (!claim || !ok || layout.layout !== "ready") {
|
|
232
|
+
return {
|
|
233
|
+
result: jsonResult({
|
|
234
|
+
contract: "assurance_kernel/inspect/v1",
|
|
235
|
+
layout,
|
|
236
|
+
kernel,
|
|
237
|
+
assurance: null,
|
|
238
|
+
risk: idleRisk,
|
|
239
|
+
unobservable: UNOBSERVABLE,
|
|
240
|
+
}),
|
|
241
|
+
journal: journalFor(
|
|
242
|
+
"inspect",
|
|
243
|
+
null,
|
|
244
|
+
ok ? "ok" : "escalated",
|
|
245
|
+
layout.layout === "ready" ? "command_ok" : "source_invalid",
|
|
246
|
+
null,
|
|
247
|
+
layout.reason,
|
|
248
|
+
),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
let declared: TaskRisk;
|
|
252
|
+
let intent: TaskIntentV1;
|
|
253
|
+
try {
|
|
254
|
+
const raw = JSON.parse(
|
|
255
|
+
readSecureProjectFile(root, `docs/plans/${claim.task_id}.intent.json`),
|
|
256
|
+
) as { risk?: unknown };
|
|
257
|
+
if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
|
|
258
|
+
throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
|
|
259
|
+
declared = raw.risk;
|
|
260
|
+
intent = parseTaskIntentV1(raw);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return sourceFailure("inspect", error);
|
|
263
|
+
}
|
|
264
|
+
let recordRead: ReturnType<typeof readTaskRecordRaw>;
|
|
265
|
+
try {
|
|
266
|
+
recordRead = readTaskRecordRaw(root, claim.task_id);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return sourceFailure("inspect", error);
|
|
269
|
+
}
|
|
270
|
+
if (!recordRead.record) {
|
|
271
|
+
return {
|
|
272
|
+
result: errorResult(
|
|
273
|
+
"source_read_failed",
|
|
274
|
+
`task ${claim.task_id} has no TaskRecord`,
|
|
275
|
+
1,
|
|
276
|
+
),
|
|
277
|
+
journal: journalFor(
|
|
278
|
+
"inspect",
|
|
279
|
+
null,
|
|
280
|
+
"rejected",
|
|
281
|
+
"source_read_failed",
|
|
282
|
+
claim.task_id,
|
|
283
|
+
"Resolve the claimed TaskRecord before retrying inspect.",
|
|
284
|
+
),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const record = recordRead.record;
|
|
288
|
+
const workspaceState = readWorkspaceStateRaw(root);
|
|
289
|
+
let identity;
|
|
290
|
+
try {
|
|
291
|
+
identity =
|
|
292
|
+
record.contract === "assurance_kernel/task_record/v4"
|
|
293
|
+
? taskRevisionIdentity(root, intent.scope_hint, record.git_base_head)
|
|
294
|
+
: taskDiffIdentity(root, intent.scope_hint);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
return sourceFailure("inspect", error);
|
|
297
|
+
}
|
|
298
|
+
const diffHash = identity.diff_hash;
|
|
299
|
+
const changedPaths = identity.changed_paths;
|
|
300
|
+
const decision = projectTask(intent, record, diffHash, record.intent_ref.content_hash, changedPaths);
|
|
301
|
+
const openUserDecisionCount = record.findings.filter(
|
|
302
|
+
(finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
|
|
303
|
+
).length;
|
|
304
|
+
const resolved = resolveProjectedRisk(intent, changedPaths);
|
|
305
|
+
return {
|
|
306
|
+
result: jsonResult({
|
|
307
|
+
contract: "assurance_kernel/inspect/v1",
|
|
308
|
+
layout,
|
|
309
|
+
kernel,
|
|
310
|
+
assurance: {
|
|
311
|
+
contract: "assurance_kernel/assurance_projection/v1",
|
|
312
|
+
task_id: claim.task_id,
|
|
313
|
+
error: null,
|
|
314
|
+
claim: kernel.claim,
|
|
315
|
+
projection: {
|
|
316
|
+
record_revision: recordRead.revision,
|
|
317
|
+
workspace_revision: workspaceState.revision,
|
|
318
|
+
intent_revision: intent.revision,
|
|
319
|
+
intent_content_hash: record.intent_ref.content_hash,
|
|
320
|
+
diff_hash: diffHash,
|
|
321
|
+
lifecycle: record.lifecycle,
|
|
322
|
+
artifact_state: record.artifact_state,
|
|
323
|
+
risk: resolveProjectedRisk(intent, changedPaths),
|
|
324
|
+
next_obligation: decision.next_obligation,
|
|
325
|
+
fresh_acceptance_ids: decision.fresh_acceptance_ids,
|
|
326
|
+
missing_acceptance_ids: decision.missing_acceptance_ids,
|
|
327
|
+
stale_attestation_ids: decision.stale_attestation_ids,
|
|
328
|
+
fresh_approval_kinds: [],
|
|
329
|
+
missing_approval_kinds: decision.missing_approval_kinds,
|
|
330
|
+
blocking_finding_ids: decision.blocking_finding_ids,
|
|
331
|
+
unresolved_user_decision_ids: decision.unresolved_user_decision_ids,
|
|
332
|
+
replan_required_ids: decision.replan_required_ids,
|
|
333
|
+
independence_violations: decision.independence_violations,
|
|
334
|
+
open_user_decision_count: openUserDecisionCount,
|
|
335
|
+
completion_ready: decision.complete,
|
|
336
|
+
authorization: deriveAssuranceAuthorization({
|
|
337
|
+
next_obligation: decision.next_obligation,
|
|
338
|
+
open_user_decision_count: openUserDecisionCount,
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
risk: {
|
|
343
|
+
declared,
|
|
344
|
+
resolved,
|
|
345
|
+
floor_prefixes: [...RISK_FLOOR_SCOPE_PREFIXES],
|
|
346
|
+
floor_applied: RISK_ORDER[resolved] > RISK_ORDER[declared],
|
|
347
|
+
matching_scope_entries: matchingFloorEntries(intent),
|
|
348
|
+
},
|
|
349
|
+
unobservable: UNOBSERVABLE,
|
|
350
|
+
}),
|
|
351
|
+
journal: journalFor("inspect", null, "ok", "command_ok", claim.task_id, null),
|
|
352
|
+
};
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return sourceFailure("inspect", error);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
155
358
|
const INTENT_SIDECAR_PREFIX = "docs/plans/";
|
|
156
359
|
const TASK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
157
360
|
|
|
@@ -1024,6 +1227,21 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
|
|
|
1024
1227
|
};
|
|
1025
1228
|
return runStatus(root);
|
|
1026
1229
|
}
|
|
1230
|
+
if (command === "inspect") {
|
|
1231
|
+
if (flags.length !== 1 || flags[0] !== "--json")
|
|
1232
|
+
return {
|
|
1233
|
+
result: errorResult("invalid_command", "inspect accepts only --json", 2),
|
|
1234
|
+
journal: journalFor(
|
|
1235
|
+
command,
|
|
1236
|
+
null,
|
|
1237
|
+
"rejected",
|
|
1238
|
+
"invalid_command",
|
|
1239
|
+
null,
|
|
1240
|
+
"Run imm-kernel inspect --json.",
|
|
1241
|
+
),
|
|
1242
|
+
};
|
|
1243
|
+
return runInspect(root);
|
|
1244
|
+
}
|
|
1027
1245
|
if (command === "audit") {
|
|
1028
1246
|
if (args.length !== 2 || args[1] !== "--legacy")
|
|
1029
1247
|
return {
|
|
@@ -1097,7 +1315,7 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
|
|
|
1097
1315
|
return {
|
|
1098
1316
|
result: {
|
|
1099
1317
|
stdout:
|
|
1100
|
-
"usage: imm-kernel status --json\n imm-kernel audit --legacy\n imm-kernel intent author <path> --stdin --json\n imm-kernel intent validate <path> --json\n",
|
|
1318
|
+
"usage: imm-kernel status --json\n imm-kernel inspect --json\n imm-kernel audit --legacy\n imm-kernel intent author <path> --stdin --json\n imm-kernel intent validate <path> --json\n",
|
|
1101
1319
|
stderr: "",
|
|
1102
1320
|
returncode: 0,
|
|
1103
1321
|
},
|
|
@@ -1106,7 +1324,7 @@ function executeKernelCommand(args: string[], root: string): KernelExecution {
|
|
|
1106
1324
|
return {
|
|
1107
1325
|
result: errorResult(
|
|
1108
1326
|
"invalid_command",
|
|
1109
|
-
"usage: imm-kernel status --json | audit --legacy | intent author <path> --stdin --json | intent validate <path> --json",
|
|
1327
|
+
"usage: imm-kernel status --json | inspect --json | audit --legacy | intent author <path> --stdin --json | intent validate <path> --json",
|
|
1110
1328
|
|
|
1111
1329
|
2,
|
|
1112
1330
|
),
|
|
@@ -1130,7 +1348,7 @@ export function runKernelCommand(
|
|
|
1130
1348
|
// subcommands (migrate/readiness/journal) are no-journal: they never
|
|
1131
1349
|
// append the friction journal. Only the fallback unknown-command path
|
|
1132
1350
|
// keeps journaling.
|
|
1133
|
-
if (["intent", "status", "audit", "migrate", "readiness", "journal"].includes(args[0] ?? ""))
|
|
1351
|
+
if (["intent", "status", "inspect", "audit", "migrate", "readiness", "journal"].includes(args[0] ?? ""))
|
|
1134
1352
|
return execution.result;
|
|
1135
1353
|
let warning = "";
|
|
1136
1354
|
try {
|
|
@@ -313,7 +313,7 @@ function parseIssues(raw: string): GithubIssue[] {
|
|
|
313
313
|
|| !Number.isSafeInteger(item.number)
|
|
314
314
|
|| typeof item.html_url !== "string"
|
|
315
315
|
|| typeof item.title !== "string"
|
|
316
|
-
|| typeof item.body !== "string"
|
|
316
|
+
|| (typeof item.body !== "string" && item.body !== null)
|
|
317
317
|
|| (item.state !== "open" && item.state !== "closed")
|
|
318
318
|
) throw new Error("gh returned a malformed Issue");
|
|
319
319
|
return {
|
|
@@ -321,7 +321,7 @@ function parseIssues(raw: string): GithubIssue[] {
|
|
|
321
321
|
number: item.number as number,
|
|
322
322
|
url: item.html_url,
|
|
323
323
|
title: item.title,
|
|
324
|
-
body: item.body,
|
|
324
|
+
body: typeof item.body === "string" ? item.body : "",
|
|
325
325
|
state: item.state,
|
|
326
326
|
state_reason: typeof item.state_reason === "string" ? item.state_reason.toLowerCase() : null,
|
|
327
327
|
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type MutationAuthorityRegistry,
|
|
9
9
|
type ValidatedAuthorityV2,
|
|
10
10
|
} from "./authority_port";
|
|
11
|
+
import { asTaskDiffSnapshot } from "./completion";
|
|
11
12
|
import { canonicalIntentHash, readTaskIntent } from "./intent";
|
|
12
13
|
import {
|
|
13
14
|
inspectIntentTokenPair,
|
|
@@ -38,6 +39,7 @@ import type {
|
|
|
38
39
|
StoredTaskMutationV3,
|
|
39
40
|
TaskAction,
|
|
40
41
|
TaskIntentV1,
|
|
42
|
+
TaskRecord,
|
|
41
43
|
} from "./types";
|
|
42
44
|
import { TASK_TOMBSTONE_CONTRACT, type TaskTombstone } from "./backend_claim";
|
|
43
45
|
import { KernelInvariantError, parseTaskAction } from "./validation";
|
|
@@ -51,7 +53,7 @@ export interface ApplyTaskActionInput {
|
|
|
51
53
|
registry: MutationAuthorityRegistry;
|
|
52
54
|
capability?: MutationAuthorityCapabilityV2;
|
|
53
55
|
/** Trusted injected diff provider; the action's diff_hash is only an expectation. */
|
|
54
|
-
diffProvider: (root: string,
|
|
56
|
+
diffProvider: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
|
|
55
57
|
now?: number;
|
|
56
58
|
/**
|
|
57
59
|
* Terminal commit mode: record + workspace + active-claim removal + task
|
|
@@ -89,10 +91,10 @@ export function applyTaskAction(
|
|
|
89
91
|
// closed before any token or capability is consumed.
|
|
90
92
|
|
|
91
93
|
// Trusted diff provider is the only diff authority.
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
+
const trustedDiff = asTaskDiffSnapshot(diffProvider(root, current.record));
|
|
95
|
+
if (trustedDiff.diff_hash !== action.diff_hash)
|
|
94
96
|
throw new KernelInvariantError([
|
|
95
|
-
`action diff_hash ${action.diff_hash} does not match the trusted diff ${
|
|
97
|
+
`action diff_hash ${action.diff_hash} does not match the trusted diff ${trustedDiff.diff_hash}`,
|
|
96
98
|
]);
|
|
97
99
|
|
|
98
100
|
// Fresh secure reread of the sidecar inside the same lock.
|
|
@@ -173,7 +175,7 @@ export function applyTaskAction(
|
|
|
173
175
|
intent_content_hash: isRevisionAction
|
|
174
176
|
? freshRead.content_hash
|
|
175
177
|
: current.record.intent_ref.content_hash,
|
|
176
|
-
diff_hash:
|
|
178
|
+
diff_hash: trustedDiff.diff_hash,
|
|
177
179
|
...(action.type === "request_rework"
|
|
178
180
|
? { findings_digest: findingsDigestV2(action.findings) }
|
|
179
181
|
: {}),
|
|
@@ -191,6 +193,7 @@ export function applyTaskAction(
|
|
|
191
193
|
current.record,
|
|
192
194
|
action,
|
|
193
195
|
inspectedAudit ? (inspectedAudit.audit as AuthorityAuditDescriptor) : null,
|
|
196
|
+
trustedDiff.changed_paths,
|
|
194
197
|
);
|
|
195
198
|
if (!isReducedMutation(mutation))
|
|
196
199
|
throw new KernelInvariantError(["reducer returned an invalid mutation"]);
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import { readBackendClaim, readTaskTombstone } from "./backend_claim";
|
|
15
15
|
import { readTaskRecord, readAuditTaskPair, readWorkspaceStateRaw, reconcileKernelAuthority } from "./storage";
|
|
16
|
-
import { projectTask } from "./completion";
|
|
17
|
-
import type { AssuranceObligation, TaskIntentV1,
|
|
16
|
+
import { projectTask, resolveProjectedRisk } from "./completion";
|
|
17
|
+
import type { AssuranceObligation, TaskIntentV1, TaskRecord, TaskRecordV2 } from "./types";
|
|
18
|
+
|
|
19
|
+
export interface TaskDiffSnapshot {
|
|
20
|
+
diff_hash: string;
|
|
21
|
+
changed_paths: readonly string[];
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
export interface AssuranceAuthorizationReadiness {
|
|
20
25
|
/**
|
|
@@ -127,7 +132,7 @@ function projectHistoricalTerminal(
|
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
function freshApprovalKinds(
|
|
130
|
-
record:
|
|
135
|
+
record: TaskRecord,
|
|
131
136
|
currentIntentContentHash: string,
|
|
132
137
|
diffHash: string,
|
|
133
138
|
): string[] {
|
|
@@ -148,14 +153,20 @@ function freshApprovalKinds(
|
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
function projectFromRecord(
|
|
151
|
-
record:
|
|
156
|
+
record: TaskRecord,
|
|
152
157
|
recordRevision: string,
|
|
153
158
|
workspaceRevision: string,
|
|
154
|
-
|
|
159
|
+
snapshot: TaskDiffSnapshot,
|
|
155
160
|
): AssuranceProjection {
|
|
156
161
|
const intent: TaskIntentV1 = record.intent_snapshot;
|
|
157
|
-
const decision = projectTask(
|
|
158
|
-
|
|
162
|
+
const decision = projectTask(
|
|
163
|
+
intent,
|
|
164
|
+
record,
|
|
165
|
+
snapshot.diff_hash,
|
|
166
|
+
record.intent_ref.content_hash,
|
|
167
|
+
snapshot.changed_paths,
|
|
168
|
+
);
|
|
169
|
+
const approvalKinds = freshApprovalKinds(record, record.intent_ref.content_hash, snapshot.diff_hash);
|
|
159
170
|
const openUserDecisionCount = record.findings.filter(
|
|
160
171
|
(finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
|
|
161
172
|
).length;
|
|
@@ -164,10 +175,10 @@ function projectFromRecord(
|
|
|
164
175
|
workspace_revision: workspaceRevision,
|
|
165
176
|
intent_revision: record.intent_snapshot.revision,
|
|
166
177
|
intent_content_hash: record.intent_ref.content_hash,
|
|
167
|
-
diff_hash:
|
|
178
|
+
diff_hash: snapshot.diff_hash,
|
|
168
179
|
lifecycle: record.lifecycle,
|
|
169
180
|
artifact_state: record.artifact_state,
|
|
170
|
-
risk: intent.
|
|
181
|
+
risk: resolveProjectedRisk(intent, snapshot.changed_paths),
|
|
171
182
|
next_obligation: decision.next_obligation,
|
|
172
183
|
fresh_acceptance_ids: decision.fresh_acceptance_ids,
|
|
173
184
|
missing_acceptance_ids: decision.missing_acceptance_ids,
|
|
@@ -203,7 +214,7 @@ function projectFromRecord(
|
|
|
203
214
|
export async function projectAssurance(
|
|
204
215
|
root: string,
|
|
205
216
|
taskId: string,
|
|
206
|
-
diffProvider: (root: string,
|
|
217
|
+
diffProvider: (root: string, record: TaskRecord) => TaskDiffSnapshot,
|
|
207
218
|
): Promise<AssuranceProjectionResult> {
|
|
208
219
|
const fail = (
|
|
209
220
|
error: string,
|
|
@@ -262,7 +273,7 @@ export async function projectAssurance(
|
|
|
262
273
|
task_id: taskId,
|
|
263
274
|
error: null,
|
|
264
275
|
claim: null,
|
|
265
|
-
projection: projectFromRecord(auditPair.record, auditPair.recordRevision, workspace.revision, diffProvider(root, auditPair.record
|
|
276
|
+
projection: projectFromRecord(auditPair.record, auditPair.recordRevision, workspace.revision, diffProvider(root, auditPair.record)),
|
|
266
277
|
};
|
|
267
278
|
}
|
|
268
279
|
if (read.record.task_id !== taskId)
|
|
@@ -270,13 +281,13 @@ export async function projectAssurance(
|
|
|
270
281
|
if (claim && read.record.lifecycle !== "active")
|
|
271
282
|
return fail(`terminal task ${taskId} has no matching tombstone proof`, claim);
|
|
272
283
|
const workspace = await readWorkspaceStateRaw(root);
|
|
273
|
-
const
|
|
284
|
+
const snapshot = diffProvider(root, read.record);
|
|
274
285
|
return {
|
|
275
286
|
contract: "assurance_kernel/assurance_projection/v1",
|
|
276
287
|
task_id: taskId,
|
|
277
288
|
error: null,
|
|
278
289
|
claim,
|
|
279
|
-
projection: projectFromRecord(read.record, read.revision, workspace.revision,
|
|
290
|
+
projection: projectFromRecord(read.record, read.revision, workspace.revision, snapshot),
|
|
280
291
|
};
|
|
281
292
|
} catch (error) {
|
|
282
293
|
return fail(error instanceof Error ? error.message : String(error));
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// P2B2 mutation authority consumption port. NOT exported from kernel/index.ts.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// exist here; the production registry/application pair is created inside the
|
|
6
|
-
// Pi lifecycle extension activation closure, and tests issue capabilities
|
|
7
|
-
// through tests/fixtures/mutation-authority-test-seam.ts.
|
|
2
|
+
// Uses the shared capability registry factory; mutation-specific binding
|
|
3
|
+
// validation, action digest, findings digest enforcement, and projection
|
|
4
|
+
// remain here.
|
|
8
5
|
|
|
9
6
|
import { createHash } from "node:crypto";
|
|
10
7
|
|
|
@@ -15,6 +12,7 @@ import {
|
|
|
15
12
|
type MutationAuthorityKind,
|
|
16
13
|
type TaskAction,
|
|
17
14
|
} from "./types";
|
|
15
|
+
import { createCapabilityRegistry } from "./capability_registry";
|
|
18
16
|
|
|
19
17
|
export interface CapabilityBindingV2 {
|
|
20
18
|
authority_kind: MutationAuthorityKind;
|
|
@@ -31,11 +29,6 @@ export interface CapabilityBindingV2 {
|
|
|
31
29
|
findings_digest: string | null;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
interface CapabilityState extends CapabilityBindingV2 {
|
|
35
|
-
issued_at: string;
|
|
36
|
-
consumed: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
32
|
export interface ValidatedAuthorityV2 {
|
|
40
33
|
audit: AuthorityAuditDescriptor;
|
|
41
34
|
action_digest: string;
|
|
@@ -84,125 +77,95 @@ export function digestOfAction(action: TaskAction): string {
|
|
|
84
77
|
}
|
|
85
78
|
|
|
86
79
|
export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
[brand]: {
|
|
134
|
-
value: true,
|
|
135
|
-
enumerable: false,
|
|
136
|
-
writable: false,
|
|
137
|
-
configurable: false,
|
|
138
|
-
},
|
|
80
|
+
const inner = createCapabilityRegistry<CapabilityBindingV2, MutationAuthorityInspection, ValidatedAuthorityV2, MutationAuthorityCapabilityV2>(
|
|
81
|
+
MUTATION_AUTHORITY_CAPABILITY_BRAND,
|
|
82
|
+
{
|
|
83
|
+
validateBinding(binding, issuedAt) {
|
|
84
|
+
const missing: string[] = [];
|
|
85
|
+
for (const [key, value] of Object.entries(binding)) {
|
|
86
|
+
if (key === "findings_digest") continue;
|
|
87
|
+
if (value === undefined || value === null || value === "") missing.push(key);
|
|
88
|
+
}
|
|
89
|
+
if (missing.length > 0)
|
|
90
|
+
throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
|
|
91
|
+
if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
|
|
92
|
+
throw new Error("authority capability findings_digest must be a canonical sha256 hash");
|
|
93
|
+
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
94
|
+
throw new Error("authority capability must have a future expiry");
|
|
95
|
+
},
|
|
96
|
+
validateAndProject(state, expected, now) {
|
|
97
|
+
if (Date.parse(state.expires_at) <= now)
|
|
98
|
+
throw new Error("authority capability has expired");
|
|
99
|
+
const actionDigest = digestOfAction(expected.action);
|
|
100
|
+
if (state.action_digest !== actionDigest)
|
|
101
|
+
throw new Error("authority capability action digest mismatch");
|
|
102
|
+
if (state.task_id !== expected.task_id)
|
|
103
|
+
throw new Error("authority capability task mismatch");
|
|
104
|
+
if (state.expected_record_hash !== expected.expected_record_hash)
|
|
105
|
+
throw new Error("authority capability record hash mismatch");
|
|
106
|
+
if (state.intent_revision !== expected.intent_revision)
|
|
107
|
+
throw new Error("authority capability intent revision mismatch");
|
|
108
|
+
if (state.intent_content_hash !== expected.intent_content_hash)
|
|
109
|
+
throw new Error("authority capability intent hash mismatch");
|
|
110
|
+
if (state.diff_hash !== expected.diff_hash)
|
|
111
|
+
throw new Error("authority capability diff hash mismatch");
|
|
112
|
+
if (expected.findings_digest !== undefined) {
|
|
113
|
+
if (state.findings_digest === null)
|
|
114
|
+
throw new Error("authority capability is not bound to findings");
|
|
115
|
+
if (state.findings_digest !== expected.findings_digest)
|
|
116
|
+
throw new Error("authority capability findings digest mismatch");
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
audit: {
|
|
120
|
+
authority_kind: state.authority_kind,
|
|
121
|
+
actor_id: state.actor_id,
|
|
122
|
+
confirmation_ref: state.confirmation_ref,
|
|
123
|
+
issued_at: state.issued_at,
|
|
124
|
+
expires_at: state.expires_at,
|
|
139
125
|
},
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
126
|
+
action_digest: actionDigest,
|
|
127
|
+
expected_record_hash: state.expected_record_hash,
|
|
128
|
+
intent_revision: state.intent_revision,
|
|
129
|
+
intent_content_hash: state.intent_content_hash,
|
|
130
|
+
diff_hash: state.diff_hash,
|
|
131
|
+
task_id: state.task_id,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
148
134
|
},
|
|
135
|
+
"authority",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
brand: inner.brand,
|
|
140
|
+
issue: inner.issue.bind(inner),
|
|
149
141
|
inspect(
|
|
150
142
|
capability: MutationAuthorityCapabilityV2 | undefined,
|
|
151
143
|
expected: MutationAuthorityInspection,
|
|
152
144
|
now = Date.now(),
|
|
153
145
|
): ValidatedAuthorityV2 {
|
|
154
|
-
if (!capability
|
|
146
|
+
if (!capability)
|
|
155
147
|
throw new Error("privileged action requires an opaque authority capability");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
throw new Error("authority capability action digest mismatch");
|
|
163
|
-
if (state.task_id !== expected.task_id)
|
|
164
|
-
throw new Error("authority capability task mismatch");
|
|
165
|
-
if (state.expected_record_hash !== expected.expected_record_hash)
|
|
166
|
-
throw new Error("authority capability record hash mismatch");
|
|
167
|
-
if (state.intent_revision !== expected.intent_revision)
|
|
168
|
-
throw new Error("authority capability intent revision mismatch");
|
|
169
|
-
if (state.intent_content_hash !== expected.intent_content_hash)
|
|
170
|
-
throw new Error("authority capability intent hash mismatch");
|
|
171
|
-
if (state.diff_hash !== expected.diff_hash)
|
|
172
|
-
throw new Error("authority capability diff hash mismatch");
|
|
173
|
-
if (expected.findings_digest !== undefined) {
|
|
174
|
-
if (state.findings_digest === null)
|
|
175
|
-
throw new Error("authority capability is not bound to findings");
|
|
176
|
-
if (state.findings_digest !== expected.findings_digest)
|
|
177
|
-
throw new Error("authority capability findings digest mismatch");
|
|
148
|
+
try {
|
|
149
|
+
return inner.inspect(capability, expected, now);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
152
|
+
throw new Error("privileged action requires an opaque authority capability");
|
|
153
|
+
throw err;
|
|
178
154
|
}
|
|
179
|
-
return {
|
|
180
|
-
audit: {
|
|
181
|
-
authority_kind: state.authority_kind,
|
|
182
|
-
actor_id: state.actor_id,
|
|
183
|
-
confirmation_ref: state.confirmation_ref,
|
|
184
|
-
issued_at: state.issued_at,
|
|
185
|
-
expires_at: state.expires_at,
|
|
186
|
-
},
|
|
187
|
-
action_digest: actionDigest,
|
|
188
|
-
expected_record_hash: state.expected_record_hash,
|
|
189
|
-
intent_revision: state.intent_revision,
|
|
190
|
-
intent_content_hash: state.intent_content_hash,
|
|
191
|
-
diff_hash: state.diff_hash,
|
|
192
|
-
task_id: state.task_id,
|
|
193
|
-
};
|
|
194
155
|
},
|
|
195
156
|
consume(
|
|
196
157
|
capability: MutationAuthorityCapabilityV2,
|
|
197
158
|
expected: MutationAuthorityInspection,
|
|
198
159
|
now = Date.now(),
|
|
199
160
|
): ValidatedAuthorityV2 {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
161
|
+
try {
|
|
162
|
+
return inner.consume(capability, expected, now);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
165
|
+
throw new Error("privileged action requires an opaque authority capability");
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
206
168
|
},
|
|
169
|
+
isConsumed: inner.isConsumed.bind(inner),
|
|
207
170
|
};
|
|
208
171
|
}
|