immune-brain 3.3.0 → 3.5.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 +2 -2
- package/README.zh-CN.md +3 -3
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +84 -1
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +124 -6
- package/plugins/immune-brain/dist/BASELINE.md +1 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +307 -242
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-loop.md +8 -7
- package/plugins/immune-brain/dist/imm-planner.md +22 -17
- package/plugins/immune-brain/hooks/hooks.json +0 -10
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +7 -1
- package/plugins/immune-brain/runtime/claude/capability.ts +4 -10
- package/plugins/immune-brain/runtime/claude/interaction.ts +55 -21
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +59 -50
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +222 -48
- package/plugins/immune-brain/runtime/claude/review_host.ts +2 -138
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/skills/BASELINE.md +1 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +6 -0
|
@@ -103,7 +103,7 @@ Runtime helpers: `imm_core.buildBrainstormEnsembleRequest`, `imm_core.buildBrain
|
|
|
103
103
|
|
|
104
104
|
A Brainstorm ensemble is optional advisory-only framing input, not a vote and not a child-owned decision. The default roles are clarify scope, divergent options, minimal solution, and risk review.
|
|
105
105
|
|
|
106
|
-
All Brainstorm ensemble children are advisory-only with `tool_policy: no tools`; they do not edit code, write Specs, write Plans, mutate workflow state, or close QA. The parent `imm-brainstorm` owns final framing synthesis, Brainstorm manifest IDs, and decision-delta confirmation. Final Spec and Plan authority stays with `imm-planner`. Routine Managed enrollment uses the
|
|
106
|
+
All Brainstorm ensemble children are advisory-only with `tool_policy: no tools`; they do not edit code, write Specs, write Plans, mutate workflow state, or close QA. The parent `imm-brainstorm` owns final framing synthesis, Brainstorm manifest IDs, and decision-delta confirmation. Final Spec and Plan authority stays with `imm-planner`. Routine Managed enrollment uses the current Host's native confirmation bound to the TaskIntent revision, content hash, and preparation digest as the single authority gate; Enrollment validates descriptor structure without executing acceptance descriptors, deterministic QA executes them after implementation, and the routine task proceeds without a second human stop. A later authority-gate failure stays fail-closed in the current Host and reports exactly one same-Host recovery action; Brainstorm must never recommend another Host, worktree, or unmanaged implementation as a fallback.
|
|
107
107
|
|
|
108
108
|
Pi's adapter may consume `brainstorm_ensemble` dispatch JSON to prepare advisory Pi subagent envelopes, but envelope construction is not child execution and does not transfer framing authority. Pi launches one foreground Agent at a time, consumes its direct result, and re-evaluates the remaining dispatch budget before launching another candidate. Runtime does not call any agent, poll or recover background work, mutate state, or own final Spec/Plan authority. Pi subagent children remain no-tools advisory candidates; the parent `imm-brainstorm` collects outputs before synthesis.
|
|
109
109
|
|
|
@@ -36,7 +36,7 @@ before any workflow mutation.
|
|
|
36
36
|
|
|
37
37
|
## Core Responsibilities
|
|
38
38
|
|
|
39
|
-
- **Main-context completion loop**: Drive the enrolled Kernel task in the current
|
|
39
|
+
- **Main-context completion loop**: Drive the enrolled Kernel task in the current Host conversation until completion or a safe stop.
|
|
40
40
|
- **Context-preserving execution**: Call `imm_loop_action` with `op: route`, then follow the returned `executor` context in the current Parent conversation. Implement only the active Step or pending same-boundary `follow_up`, then record structured execution evidence through the Loop runtime action. A bounded test failure uses the returned internal `test-fixer` dispatch with its explicit delegated test-file list; PR feedback or CI repair uses the returned internal `pr-fix` dispatch inside the current Plan boundary.
|
|
41
41
|
- **Independent authority isolation**: Use the host `Agent` subagent primitive for `awaiting_qa_decision` and for the exact runtime-reported review gate. Standard Plan Steps close from accepted passing evidence before an internal QA boundary exists; Strict Steps and all follow-ups retain isolated QA. The parent records accepted child decisions through the Loop runtime action.
|
|
42
42
|
- **Observable progress**: Update only at major phase changes: Step start, execution evidence recorded, QA/review result, or terminal stop. Always emit a terminal summary.
|
|
@@ -67,6 +67,7 @@ Use Pi native `Agent` subagents. Do not spawn Pi child processes or invoke a sep
|
|
|
67
67
|
- The parent may implement but must not issue its own QA or review pass.
|
|
68
68
|
- QA and reviewer children must not edit files, write Plans, mutate Kernel state, or close decisions directly.
|
|
69
69
|
- Missing `Agent` support, failed or malformed child output, stale child target, runtime write failure, invalid projection, missing credentials, unclear verification, repeated unchanged failure, or user cancellation stops fail-closed with an explicit reason and no decision write.
|
|
70
|
+
- A Managed native authority failure reports its stable reason and exactly one same-Host recovery action. Never recommend another Host, worktree, Direct Path, unmanaged implementation, or automatic retry as a fallback.
|
|
70
71
|
- `replan_needed` stops at `imm-planner`; do not widen scope or rewrite the active Plan. A replacement must use a new sequential Plan path after the current Plan reaches `completed`, or after a literal user explicitly marks it `cancelled` or `superseded`.
|
|
71
72
|
- Plans never suspend, resume, queue, or execute in parallel. Do not insert a repair Plan ahead of the current Plan.
|
|
72
73
|
- Same-boundary review `follow_up` repeats execution, independent QA, and the originating review gate.
|
|
@@ -139,12 +140,12 @@ Default user-facing shape: checkpoint progress lines, then `Conclusion -> Eviden
|
|
|
139
140
|
## Kernel Canary Routing
|
|
140
141
|
|
|
141
142
|
When the Kernel projection reports an active/draining backend claim, keep
|
|
142
|
-
`imm-loop` as the user-facing entry and call the
|
|
143
|
-
that owned task. Enrollment
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
`imm-loop` as the user-facing entry and call the current Host's Kernel integration
|
|
144
|
+
for that owned task. Enrollment and Review authorization use the current Host's
|
|
145
|
+
native gates. When the projection calls for `request_authorization` or
|
|
146
|
+
`approve_breaking_intent_revision`, invoke the exact Tool operation directly
|
|
147
|
+
without asking the user for chat pre-confirmation; the native Host interaction is
|
|
148
|
+
the single authority decision. Invoke
|
|
148
149
|
`repair_authority_state` directly for a proven stale claim; Kernel revalidation
|
|
149
150
|
removes only the redundant claim without user interaction. Do not invoke the
|
|
150
151
|
removed `imm-canary-work` Skill as
|
|
@@ -22,9 +22,12 @@ planning artifacts. A later literal-user request to start Enrollment is a
|
|
|
22
22
|
non-authoritative execution trigger: invoke the native Enrollment gate directly,
|
|
23
23
|
without asking for chat pre-confirmation. For a clear mutation request that
|
|
24
24
|
already includes execution, invoke that gate as soon as the candidate is
|
|
25
|
-
validated and Git-tracked. Literal-user confirmation in the
|
|
26
|
-
the authority boundary.
|
|
27
|
-
|
|
25
|
+
validated and Git-tracked. Literal-user confirmation in the current Host's
|
|
26
|
+
native gate remains the authority boundary. A gate failure preserves candidate
|
|
27
|
+
artifacts and reports its reason plus exactly one same-Host recovery action; it
|
|
28
|
+
must never recommend another Host, worktree, Direct Path, unmanaged
|
|
29
|
+
implementation, or automatic retry. Fast-Track may compress the same phases but
|
|
30
|
+
cannot bypass that boundary, QA, Review, authorization, or completion.
|
|
28
31
|
|
|
29
32
|
## Clarification supplement
|
|
30
33
|
|
|
@@ -73,7 +76,7 @@ Then route deterministically:
|
|
|
73
76
|
- an active or otherwise nonterminal v3 Plan remains on its existing v3 route;
|
|
74
77
|
- no routing policy preserves the legacy v3 Planner behavior;
|
|
75
78
|
- a valid `kernel_task_intent` retirement policy produces one TaskIntent draft
|
|
76
|
-
through
|
|
79
|
+
through the current Host's explicit `imm-planner`;
|
|
77
80
|
- an invalid, unreadable, untracked, or tracked-deleted policy rejects new
|
|
78
81
|
planning authority with `routing_policy_invalid`;
|
|
79
82
|
- no Planner path enrolls a task or falls back to v3 after retirement.
|
|
@@ -90,17 +93,18 @@ The routing projection selects the planning route; explicit
|
|
|
90
93
|
of that Plan artifact. A valid Plan never proves Managed authority. Under an
|
|
91
94
|
active `kernel_task_intent` policy, authority still requires a Git-tracked
|
|
92
95
|
TaskIntent whose `imm-kernel intent validate <path> --json` projection is
|
|
93
|
-
`valid: true` and `enrollment_ready: true`, followed by
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
`valid: true` and `enrollment_ready: true`, followed by current-Host native
|
|
97
|
+
Enrollment.
|
|
98
|
+
|
|
99
|
+
Host identity is implicit and never a planning input. The production boundary
|
|
100
|
+
that turns a Git-tracked TaskIntent draft into managed execution authority is
|
|
101
|
+
the current Host's native Enrollment gate. Invoke that Host integration
|
|
102
|
+
directly when the route is ready; do not ask for chat pre-confirmation. The
|
|
103
|
+
single literal-user decision is bound to the TaskIntent revision, content hash,
|
|
104
|
+
and preparation digest. Enrollment validates the intent, Git ownership, scope,
|
|
105
|
+
workspace claim, and final authority preconditions without executing acceptance
|
|
106
|
+
descriptors. A routine task proceeds from that single confirmation through
|
|
107
|
+
enrollment, execution and QA without a second human stop.
|
|
104
108
|
|
|
105
109
|
Before authoring a TaskIntent, trace each expected behavior from its public or
|
|
106
110
|
runtime entry point through existing imports and callers to the highest focused
|
|
@@ -387,10 +391,11 @@ Iteration plan under `docs/plans/` and spec under `docs/specs/`. Includes: `Summ
|
|
|
387
391
|
- When a project explicitly expects Chinese document prose, `imm-plan <plan-path> --json` includes an `output_language` warning if target Plan or referenced Spec prose appears mostly English.
|
|
388
392
|
- Spec references align with steps: each step’s `Verification` is copy-paste-checkable against repo commands or files.
|
|
389
393
|
- For brainstorm-origin Plans with a manifest, `imm-plan <plan-path> --json` reports `origin_coverage` totals with no `unmapped_items` and no reason-required trace rows without reasons.
|
|
390
|
-
- Managed execution handoff is Git-tracked TaskIntent author/validate plus
|
|
394
|
+
- Managed execution handoff is Git-tracked TaskIntent author/validate plus current-Host native Enrollment. Do not sync a v3 State Ledger or invoke a missing dispatcher.
|
|
391
395
|
|
|
392
396
|
## Next Action
|
|
393
397
|
|
|
394
398
|
- Gate: Reference closure and the clarification supplement are complete; every upstream `BR-*` item is represented; no unresolved user-owned decision remains; any Planner-introduced decision delta is confirmed; the Plan passes `imm-plan --json` validation; and no step has a hypothetical-only verification path.
|
|
395
|
-
- If gates pass: for Kernel-managed work, invoke the
|
|
399
|
+
- If gates pass: for Kernel-managed work, invoke the current Host's native Enrollment Tool directly without chat pre-confirmation. Its single literal-user gate binds the TaskIntent revision, content hash, and preparation digest, validates Enrollment preconditions without executing acceptance descriptors, and enrolls the task to continue through `imm-loop`.
|
|
400
|
+
- If the native gate fails: preserve candidate artifacts and report the stable reason plus exactly one same-Host recovery action. Do not suggest another Host, worktree, Direct Path, unmanaged implementation, or automatic retry.
|
|
396
401
|
- If gates are not met: state which validation failures, unresolved verification paths, or material decision deltas remain; do not name a next skill.
|
|
@@ -559,7 +559,13 @@ export class AssuranceCoordinator {
|
|
|
559
559
|
ensureOperationLive();
|
|
560
560
|
qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, runner, {
|
|
561
561
|
signal: operationController.signal,
|
|
562
|
-
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
562
|
+
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
563
|
+
current: item.index,
|
|
564
|
+
total: item.total,
|
|
565
|
+
acceptance_id: item.acceptance_id,
|
|
566
|
+
acceptance_phase: item.phase,
|
|
567
|
+
elapsed_ms: item.elapsed_ms,
|
|
568
|
+
}),
|
|
563
569
|
});
|
|
564
570
|
ensureOperationLive();
|
|
565
571
|
const invocation = this.openInvocation(taskId);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const MIN_CLAUDE_CODE_VERSION = "2.1.
|
|
1
|
+
export const MIN_CLAUDE_CODE_VERSION = "2.1.236";
|
|
2
2
|
export const HOST_ID = "claude-code" as const;
|
|
3
3
|
export const CORE_CONTRACT = "assurance_kernel/host_adapter/claude-code/v1";
|
|
4
4
|
export const SUPPORTED_PLATFORMS = ["darwin", "linux"] as const;
|
|
@@ -6,12 +6,12 @@ export const SUPPORTED_PLATFORMS = ["darwin", "linux"] as const;
|
|
|
6
6
|
export type PermissionMode = "manual" | "acceptEdits" | "auto" | "bypassPermissions" | "dontAsk";
|
|
7
7
|
|
|
8
8
|
export type HostProbe =
|
|
9
|
-
| { ok: true; version: string;
|
|
9
|
+
| { ok: true; version: string; platform: string }
|
|
10
10
|
| { ok: false; reason: string };
|
|
11
11
|
|
|
12
12
|
function parseSemver(value: string): [number, number, number] | null {
|
|
13
13
|
// Prerelease and build suffixes are rejected: an unstable build such as
|
|
14
|
-
//
|
|
14
|
+
// Prerelease builds never satisfy the stable minimum.
|
|
15
15
|
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(value.trim());
|
|
16
16
|
if (!match) return null;
|
|
17
17
|
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
@@ -57,11 +57,5 @@ export function probeHost(
|
|
|
57
57
|
if (platform !== "darwin" && platform !== "linux") {
|
|
58
58
|
return { ok: false, reason: `unsupported platform ${platform}; native Windows is out of scope` };
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
if (rawPermissionMode !== undefined && rawPermissionMode !== "") {
|
|
62
|
-
const permissionMode = parsePermissionMode(rawPermissionMode);
|
|
63
|
-
if (!permissionMode) return { ok: false, reason: `unsupported permission mode ${rawPermissionMode}` };
|
|
64
|
-
return { ok: true, version, permissionMode, platform };
|
|
65
|
-
}
|
|
66
|
-
return { ok: true, version, permissionMode: "manual", platform };
|
|
60
|
+
return { ok: true, version, platform };
|
|
67
61
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { parsePermissionMode, type PermissionMode } from "./capability";
|
|
3
2
|
|
|
4
3
|
export const PRIVILEGED_OPERATIONS = [
|
|
5
4
|
"enroll",
|
|
@@ -9,49 +8,84 @@ export const PRIVILEGED_OPERATIONS = [
|
|
|
9
8
|
] as const;
|
|
10
9
|
|
|
11
10
|
export type PrivilegedOperation = (typeof PRIVILEGED_OPERATIONS)[number];
|
|
12
|
-
export type NativeDecision = "accept" | "
|
|
11
|
+
export type NativeDecision = "accept" | "decline" | "cancel";
|
|
12
|
+
export type NativeFailureCode =
|
|
13
|
+
| "interaction_not_opened"
|
|
14
|
+
| "user_denied"
|
|
15
|
+
| "user_cancelled"
|
|
16
|
+
| "correlation_missing"
|
|
17
|
+
| "unsupported_host"
|
|
18
|
+
| "workspace_changed";
|
|
19
|
+
|
|
20
|
+
const RECOVERY_ACTIONS: Record<NativeFailureCode, string> = {
|
|
21
|
+
interaction_not_opened: "retry through a fresh native gate in the current Host",
|
|
22
|
+
user_denied: "wait for a fresh literal-user request",
|
|
23
|
+
user_cancelled: "wait for a fresh literal-user request",
|
|
24
|
+
correlation_missing: "retry through a fresh native gate in the current Host",
|
|
25
|
+
unsupported_host: "upgrade to a supported Claude Code version and retry in the current Host",
|
|
26
|
+
workspace_changed: "review the current workspace and retry through a fresh native gate",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class NativeAuthorityError extends Error {
|
|
30
|
+
constructor(
|
|
31
|
+
readonly reasonCode: NativeFailureCode,
|
|
32
|
+
detail: string,
|
|
33
|
+
readonly recoveryAction = RECOVERY_ACTIONS[reasonCode],
|
|
34
|
+
) {
|
|
35
|
+
super(`${reasonCode}: ${detail}; recovery: ${recoveryAction}`);
|
|
36
|
+
this.name = "NativeAuthorityError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
13
39
|
|
|
14
40
|
export function isPrivilegedOperation(operation: string): operation is PrivilegedOperation {
|
|
15
41
|
return (PRIVILEGED_OPERATIONS as readonly string[]).includes(operation);
|
|
16
42
|
}
|
|
17
43
|
|
|
18
44
|
export function privilegedAnnotations(): Record<string, unknown> {
|
|
19
|
-
return {
|
|
20
|
-
destructiveHint: true,
|
|
21
|
-
"anthropic/requiresUserInteraction": true,
|
|
22
|
-
};
|
|
45
|
+
return { destructiveHint: true };
|
|
23
46
|
}
|
|
24
47
|
|
|
25
48
|
export interface NativeGateInput {
|
|
26
49
|
operation: string;
|
|
27
|
-
permissionMode: PermissionMode | string;
|
|
28
|
-
requiresUserInteraction: boolean;
|
|
29
50
|
interactive: boolean;
|
|
30
51
|
decision?: NativeDecision;
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
export type NativeGateResult =
|
|
34
55
|
| { ok: true }
|
|
35
|
-
| { ok: false;
|
|
56
|
+
| { ok: false; error: NativeAuthorityError };
|
|
36
57
|
|
|
37
58
|
export function evaluateNativeGate(input: NativeGateInput): NativeGateResult {
|
|
38
59
|
if (!isPrivilegedOperation(input.operation)) return { ok: true };
|
|
39
|
-
if (!input.interactive) return { ok: false,
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
if (
|
|
43
|
-
if (!input.requiresUserInteraction) {
|
|
44
|
-
return { ok: false, reason: "privileged operation requires anthropic/requiresUserInteraction" };
|
|
45
|
-
}
|
|
46
|
-
if (input.decision === "deny") return { ok: false, reason: "native interaction denied" };
|
|
47
|
-
if (input.decision === "cancel") return { ok: false, reason: "native interaction cancelled" };
|
|
48
|
-
if (input.decision !== "accept") return { ok: false, reason: "native interaction missing" };
|
|
60
|
+
if (!input.interactive) return { ok: false, error: new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable") };
|
|
61
|
+
if (input.decision === "decline") return { ok: false, error: new NativeAuthorityError("user_denied", "native interaction declined") };
|
|
62
|
+
if (input.decision === "cancel") return { ok: false, error: new NativeAuthorityError("user_cancelled", "native interaction cancelled") };
|
|
63
|
+
if (input.decision !== "accept") return { ok: false, error: new NativeAuthorityError("interaction_not_opened", "native interaction returned no decision") };
|
|
49
64
|
return { ok: true };
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
export interface NativeConfirmationInput {
|
|
68
|
+
operation: PrivilegedOperation;
|
|
69
|
+
taskId: string;
|
|
70
|
+
toolCallId: string;
|
|
71
|
+
risk?: string;
|
|
72
|
+
intentRevision?: number;
|
|
73
|
+
intentContentHash?: string;
|
|
74
|
+
bindingDigest?: string;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface NativeConfirmationResult {
|
|
79
|
+
decision: NativeDecision;
|
|
80
|
+
requestId: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type NativeConfirmationPort = (input: NativeConfirmationInput) => Promise<NativeConfirmationResult>;
|
|
84
|
+
|
|
52
85
|
export function confirmationRef(input: {
|
|
53
|
-
|
|
86
|
+
connectionId: string;
|
|
54
87
|
toolCallId: string;
|
|
88
|
+
requestId: string;
|
|
55
89
|
operation: string;
|
|
56
90
|
taskId: string;
|
|
57
91
|
intentRevision?: number;
|
|
@@ -59,7 +93,7 @@ export function confirmationRef(input: {
|
|
|
59
93
|
bindingDigest?: string;
|
|
60
94
|
}): string {
|
|
61
95
|
return `claude-confirm-${createHash("sha256")
|
|
62
|
-
.update(`${input.
|
|
96
|
+
.update(`${input.connectionId}\0${input.toolCallId}\0${input.requestId}\0${input.operation}\0${input.taskId}\0${input.intentRevision ?? ""}\0${input.intentContentHash ?? ""}\0${input.bindingDigest ?? ""}`)
|
|
63
97
|
.digest("hex")
|
|
64
98
|
.slice(0, 16)}`;
|
|
65
99
|
}
|
|
@@ -47,7 +47,14 @@ import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/stora
|
|
|
47
47
|
import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
|
|
48
48
|
import { qaFindingId } from "../assurance/qa_findings";
|
|
49
49
|
import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
confirmationRef,
|
|
52
|
+
enrollmentNonce,
|
|
53
|
+
evaluateNativeGate,
|
|
54
|
+
isPrivilegedOperation,
|
|
55
|
+
NativeAuthorityError,
|
|
56
|
+
type NativeConfirmationPort,
|
|
57
|
+
} from "./interaction";
|
|
51
58
|
import { ClaudeReviewHost, FileHookEventLog, type ClaudeHookEvent } from "./review_host";
|
|
52
59
|
import { probeHost, type PermissionMode } from "./capability";
|
|
53
60
|
|
|
@@ -311,6 +318,10 @@ async function mintCapability(
|
|
|
311
318
|
return registry.issue(binding);
|
|
312
319
|
}
|
|
313
320
|
|
|
321
|
+
function throwIfCancelled(signal?: AbortSignal): void {
|
|
322
|
+
if (signal?.aborted) throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
|
|
323
|
+
}
|
|
324
|
+
|
|
314
325
|
export interface ClaudeRuntimeOptions {
|
|
315
326
|
cwd: string;
|
|
316
327
|
env?: Record<string, string | undefined>;
|
|
@@ -318,7 +329,7 @@ export interface ClaudeRuntimeOptions {
|
|
|
318
329
|
ports?: AssuranceCoordinatorPorts;
|
|
319
330
|
interactive?: boolean;
|
|
320
331
|
permissionMode?: PermissionMode;
|
|
321
|
-
|
|
332
|
+
requestConfirmation?: NativeConfirmationPort;
|
|
322
333
|
}
|
|
323
334
|
|
|
324
335
|
export class ClaudeRuntime {
|
|
@@ -327,8 +338,7 @@ export class ClaudeRuntime {
|
|
|
327
338
|
private readonly cwd: string;
|
|
328
339
|
private readonly env: Record<string, string | undefined>;
|
|
329
340
|
private readonly interactive: boolean;
|
|
330
|
-
private
|
|
331
|
-
private readonly decisions: Map<string, NativeDecision>;
|
|
341
|
+
private requestConfirmation?: NativeConfirmationPort;
|
|
332
342
|
private hostVersion: string | undefined;
|
|
333
343
|
private mutationRegistry: MutationAuthorityRegistry | null = null;
|
|
334
344
|
private enrollmentRegistry = createEnrollmentAuthorityRegistry();
|
|
@@ -338,8 +348,7 @@ export class ClaudeRuntime {
|
|
|
338
348
|
this.cwd = options.cwd;
|
|
339
349
|
this.env = options.env ?? process.env;
|
|
340
350
|
this.interactive = options.interactive ?? true;
|
|
341
|
-
this.
|
|
342
|
-
this.decisions = options.decisions ?? new Map();
|
|
351
|
+
this.requestConfirmation = options.requestConfirmation;
|
|
343
352
|
this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog());
|
|
344
353
|
if (options.ports) {
|
|
345
354
|
this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
|
|
@@ -357,6 +366,10 @@ export class ClaudeRuntime {
|
|
|
357
366
|
this.hostVersion = version || undefined;
|
|
358
367
|
}
|
|
359
368
|
|
|
369
|
+
bindNativeConfirmation(port: NativeConfirmationPort): void {
|
|
370
|
+
this.requestConfirmation = port;
|
|
371
|
+
}
|
|
372
|
+
|
|
360
373
|
async shutdown(): Promise<void> {
|
|
361
374
|
await this.coordinator.onSessionShutdown();
|
|
362
375
|
}
|
|
@@ -393,45 +406,28 @@ export class ClaudeRuntime {
|
|
|
393
406
|
return { registry: this.mutationRegistry, app: this.app };
|
|
394
407
|
}
|
|
395
408
|
|
|
396
|
-
private
|
|
397
|
-
|
|
398
|
-
// any workspace preparation. The live accept elicitation is requested
|
|
399
|
-
// and consumed only after preparation computes the binding digests, so
|
|
400
|
-
// native confirmation is always bound to hashes computed before it.
|
|
401
|
-
const configured = this.decisions.get(operation) ?? meta.decision;
|
|
402
|
-
if (configured === "deny" || configured === "cancel") this.gate(operation, meta);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
private gate(operation: string, meta: ToolMeta, binding: {
|
|
409
|
+
private async gate(operation: string, meta: ToolMeta, binding: {
|
|
410
|
+
risk?: string;
|
|
406
411
|
intentRevision?: number;
|
|
407
412
|
intentContentHash?: string;
|
|
408
413
|
bindingDigest?: string;
|
|
409
|
-
} = {}): { confirmation_ref: string } {
|
|
414
|
+
} = {}): Promise<{ confirmation_ref: string }> {
|
|
415
|
+
throwIfCancelled(meta.signal);
|
|
410
416
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
411
|
-
if (!probe.ok) throw new
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
? this.host.takeConfirmation(meta.sessionId, meta.toolCallId)
|
|
421
|
-
: undefined
|
|
422
|
-
);
|
|
423
|
-
const gate = evaluateNativeGate({
|
|
424
|
-
operation,
|
|
425
|
-
permissionMode,
|
|
426
|
-
requiresUserInteraction,
|
|
427
|
-
interactive: meta.interactive ?? this.interactive,
|
|
428
|
-
decision,
|
|
429
|
-
});
|
|
430
|
-
if (!gate.ok) throw new Error(gate.reason);
|
|
417
|
+
if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
418
|
+
const interactive = meta.interactive ?? this.interactive;
|
|
419
|
+
if (!interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
420
|
+
if (!isPrivilegedOperation(operation)) throw new Error(`unsupported native operation ${operation}`);
|
|
421
|
+
if (!this.requestConfirmation) throw new NativeAuthorityError("interaction_not_opened", "native confirmation port is unavailable");
|
|
422
|
+
const result = await this.requestConfirmation({ operation, taskId: meta.taskId, toolCallId: meta.toolCallId, signal: meta.signal, ...binding });
|
|
423
|
+
throwIfCancelled(meta.signal);
|
|
424
|
+
const gate = evaluateNativeGate({ operation, interactive, decision: result.decision });
|
|
425
|
+
if (!gate.ok) throw gate.error;
|
|
431
426
|
return {
|
|
432
427
|
confirmation_ref: confirmationRef({
|
|
433
|
-
|
|
428
|
+
connectionId: meta.sessionId,
|
|
434
429
|
toolCallId: meta.toolCallId,
|
|
430
|
+
requestId: result.requestId,
|
|
435
431
|
operation,
|
|
436
432
|
taskId: meta.taskId,
|
|
437
433
|
...binding,
|
|
@@ -444,17 +440,19 @@ export class ClaudeRuntime {
|
|
|
444
440
|
}
|
|
445
441
|
|
|
446
442
|
async enroll(taskId: string, meta: ToolMeta) {
|
|
447
|
-
this.rejectBeforePreparation("enroll", { ...meta, taskId });
|
|
448
443
|
const now = new Date().toISOString();
|
|
449
444
|
const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
|
|
450
|
-
const
|
|
445
|
+
const intent = await readTaskIntent(this.cwd, taskId);
|
|
446
|
+
const gate = await this.gate("enroll", { ...meta, taskId }, {
|
|
447
|
+
risk: intent.intent.risk,
|
|
451
448
|
intentRevision: preparation.intent?.revision,
|
|
452
449
|
intentContentHash: preparation.intent?.content_hash,
|
|
453
450
|
bindingDigest: preparation.digest,
|
|
454
451
|
});
|
|
455
452
|
const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
|
|
456
|
-
if (!unchanged) throw new
|
|
453
|
+
if (!unchanged) throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
457
454
|
if (!preparation.intent) throw new Error("enrollment requires a readable TaskIntent");
|
|
455
|
+
throwIfCancelled(meta.signal);
|
|
458
456
|
const nonce = enrollmentNonce();
|
|
459
457
|
const binding: EnrollmentCapabilityBinding = {
|
|
460
458
|
task_id: taskId,
|
|
@@ -501,7 +499,6 @@ export class ClaudeRuntime {
|
|
|
501
499
|
return repairKernelAuthority(this.cwd, taskId, authority.revision);
|
|
502
500
|
}
|
|
503
501
|
if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
|
|
504
|
-
this.rejectBeforePreparation(operation, { ...meta, taskId });
|
|
505
502
|
let op = operation;
|
|
506
503
|
let decisionOp: { finding_id: string; resolution: string } | undefined;
|
|
507
504
|
const projection = await this.status(taskId);
|
|
@@ -562,15 +559,22 @@ export class ClaudeRuntime {
|
|
|
562
559
|
writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
|
|
563
560
|
execFileSync("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
564
561
|
const preparedRecord = await readTaskRecord(this.cwd, taskId);
|
|
565
|
-
if (!preparedRecord.record)
|
|
562
|
+
if (!preparedRecord.record) {
|
|
563
|
+
throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
|
|
564
|
+
}
|
|
566
565
|
preparedDiffHash = diffHashOf(this.cwd, preparedRecord.record);
|
|
567
566
|
}
|
|
568
567
|
const preparedProjection = await this.status(taskId);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
568
|
+
try {
|
|
569
|
+
assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
|
|
570
|
+
if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
|
|
571
|
+
throw new Error("workspace changed while preparing the authority digest");
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
|
|
572
575
|
}
|
|
573
|
-
gate = this.gate(operation, { ...meta, taskId }, {
|
|
576
|
+
gate = await this.gate(operation, { ...meta, taskId }, {
|
|
577
|
+
risk: projection.projection.risk,
|
|
574
578
|
intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
|
|
575
579
|
intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
|
|
576
580
|
bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`,
|
|
@@ -588,11 +592,16 @@ export class ClaudeRuntime {
|
|
|
588
592
|
const confirmation = gate.confirmation_ref;
|
|
589
593
|
try {
|
|
590
594
|
const capabilityProjection = await this.status(taskId);
|
|
591
|
-
|
|
595
|
+
try {
|
|
596
|
+
assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
|
|
597
|
+
} catch (error) {
|
|
598
|
+
throw new NativeAuthorityError("workspace_changed", error instanceof Error ? error.message : String(error));
|
|
599
|
+
}
|
|
592
600
|
const operationDiffHash = capabilityProjection.projection.diff_hash;
|
|
593
601
|
if (nextIntent && operationDiffHash !== preparedDiffHash) {
|
|
594
|
-
throw new
|
|
602
|
+
throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
|
|
595
603
|
}
|
|
604
|
+
throwIfCancelled(meta.signal);
|
|
596
605
|
const capability = await mintCapability(registry, {
|
|
597
606
|
authority_kind: "user",
|
|
598
607
|
task_id: taskId,
|
|
@@ -608,6 +617,7 @@ export class ClaudeRuntime {
|
|
|
608
617
|
...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
|
|
609
618
|
...(op === "stop" ? { reason: extra.reason ?? "user stop" } : {}),
|
|
610
619
|
});
|
|
620
|
+
throwIfCancelled(meta.signal);
|
|
611
621
|
const result = app.execute({
|
|
612
622
|
root: this.cwd,
|
|
613
623
|
task_id: taskId,
|
|
@@ -765,6 +775,5 @@ export interface ToolMeta {
|
|
|
765
775
|
requiresUserInteraction?: boolean;
|
|
766
776
|
permissionMode?: PermissionMode;
|
|
767
777
|
interactive?: boolean;
|
|
768
|
-
decision?: NativeDecision;
|
|
769
778
|
signal?: AbortSignal;
|
|
770
779
|
}
|