immune-brain 2.8.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/README.md +239 -0
- package/README.zh-CN.md +239 -0
- package/package.json +84 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
- package/plugins/immune-brain/.pi-extension/package.json +11 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
- package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
- package/plugins/immune-brain/bin/imm-kernel +4 -0
- package/plugins/immune-brain/bin/imm-plan +4 -0
- package/plugins/immune-brain/bin/imm-pr-diag +230 -0
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
- package/plugins/immune-brain/bin/imm-retired +4 -0
- package/plugins/immune-brain/bin/imm-tracker +4 -0
- package/plugins/immune-brain/dist/BASELINE.md +138 -0
- package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
- package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
- package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
- package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
- package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
- package/plugins/immune-brain/dist/imm-loop.md +158 -0
- package/plugins/immune-brain/dist/imm-planner.md +387 -0
- package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
- package/plugins/immune-brain/dist/registry.yaml +49 -0
- package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
- package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
- package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
- package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
- package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
- package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
- package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
- package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
- package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
- package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
- package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
- package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
- package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
- package/plugins/immune-brain/skills/.ignore +1 -0
- package/plugins/immune-brain/skills/BASELINE.md +138 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
- package/plugins/immune-brain/skills/registry.yaml +49 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-owned managed-task routing policy (v1).
|
|
3
|
+
*
|
|
4
|
+
* The optional project policy at `docs/plans/managed-task-routing-policy.json`
|
|
5
|
+
* is a strict, canonical, Git-owned routing contract evaluated at the
|
|
6
|
+
* `imm-plan --sync` central boundary. This module owns the policy reader and
|
|
7
|
+
* read-only status projection. The runtime never stages or commits policy
|
|
8
|
+
* bytes; activating a policy is an explicit `git add` integration action.
|
|
9
|
+
*
|
|
10
|
+
* Wire contract (v1, canonical bytes only):
|
|
11
|
+
*
|
|
12
|
+
* ```json
|
|
13
|
+
* {
|
|
14
|
+
* "contract": "immune_brain/managed_task_routing_policy/v1",
|
|
15
|
+
* "revision": 1,
|
|
16
|
+
* "new_task_route": "kernel_task_intent",
|
|
17
|
+
* "v3_new_plan_sync": "retired",
|
|
18
|
+
* "legacy_v3_mode": "drain_read_only",
|
|
19
|
+
* "terminal_import": "disabled"
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Two-space JSON indentation, the field order shown above, and one trailing
|
|
24
|
+
* newline. Revision 1 accepts no formatting-equivalent alternative.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import {
|
|
30
|
+
closeSync,
|
|
31
|
+
constants as fsConstants,
|
|
32
|
+
fstatSync,
|
|
33
|
+
lstatSync,
|
|
34
|
+
openSync,
|
|
35
|
+
readFileSync,
|
|
36
|
+
realpathSync,
|
|
37
|
+
statSync,
|
|
38
|
+
} from "node:fs";
|
|
39
|
+
import { join, resolve, sep } from "node:path";
|
|
40
|
+
|
|
41
|
+
export const MANAGED_TASK_ROUTING_POLICY_RELATIVE_PATH =
|
|
42
|
+
"docs/plans/managed-task-routing-policy.json";
|
|
43
|
+
|
|
44
|
+
export const MANAGED_TASK_ROUTING_POLICY_V1_SHA256 =
|
|
45
|
+
"43949f0ef456efb9ca7dccbe1c8bc2355d6acce66486213fb2750a87388ec71e";
|
|
46
|
+
|
|
47
|
+
/** Bounded policy size; the canonical v1 wire form is a few hundred bytes. */
|
|
48
|
+
const POLICY_MAX_BYTES = 4096;
|
|
49
|
+
|
|
50
|
+
const POLICY_CONTRACT = "immune_brain/managed_task_routing_policy/v1";
|
|
51
|
+
const POLICY_REVISION = 1;
|
|
52
|
+
const POLICY_NEW_TASK_ROUTE = "kernel_task_intent";
|
|
53
|
+
const POLICY_V3_NEW_PLAN_SYNC = "retired";
|
|
54
|
+
const POLICY_LEGACY_V3_MODE = "drain_read_only";
|
|
55
|
+
const POLICY_TERMINAL_IMPORT = "disabled";
|
|
56
|
+
|
|
57
|
+
const POLICY_V1_FIELDS = [
|
|
58
|
+
"contract",
|
|
59
|
+
"revision",
|
|
60
|
+
"new_task_route",
|
|
61
|
+
"v3_new_plan_sync",
|
|
62
|
+
"legacy_v3_mode",
|
|
63
|
+
"terminal_import",
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
export type RoutingPolicyStatus = "legacy_v3" | "active" | "invalid";
|
|
67
|
+
|
|
68
|
+
export type RoutingPolicyOwnership =
|
|
69
|
+
| "absent"
|
|
70
|
+
| "untracked"
|
|
71
|
+
| "tracked_clean"
|
|
72
|
+
| "tracked_deleted"
|
|
73
|
+
| "worktree_index_drift"
|
|
74
|
+
| "unavailable";
|
|
75
|
+
|
|
76
|
+
export interface RoutingPolicyProjection {
|
|
77
|
+
policy_status: RoutingPolicyStatus;
|
|
78
|
+
route: "kernel_task_intent" | null;
|
|
79
|
+
v3_new_plan_sync: "retired" | "allowed";
|
|
80
|
+
legacy_v3_mode: "drain_read_only" | null;
|
|
81
|
+
terminal_import: "disabled" | null;
|
|
82
|
+
worktree_sha256: string | null;
|
|
83
|
+
index_sha256: string | null;
|
|
84
|
+
ownership: RoutingPolicyOwnership;
|
|
85
|
+
reason_code: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface RoutingPolicyGateResult {
|
|
89
|
+
blocked: { stdout: string; stderr: string; returncode: number } | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sha256(bytes: string | Buffer): string {
|
|
93
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveCanonicalRoot(root: string): string {
|
|
97
|
+
const resolved = resolve(root);
|
|
98
|
+
const rootStat = lstatSync(resolved);
|
|
99
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
|
|
100
|
+
throw new Error("project root must be a real directory, not a symlink");
|
|
101
|
+
return realpathSync(resolved);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function collectPathIdentities(
|
|
105
|
+
canonicalRoot: string,
|
|
106
|
+
relativePath: string,
|
|
107
|
+
): { dev: number; ino: number }[] {
|
|
108
|
+
const identities: { dev: number; ino: number }[] = [];
|
|
109
|
+
let current = canonicalRoot;
|
|
110
|
+
for (const part of relativePath.split("/")) {
|
|
111
|
+
current = join(current, part);
|
|
112
|
+
const stat = lstatSync(current);
|
|
113
|
+
if (stat.isSymbolicLink())
|
|
114
|
+
throw new Error("routing policy path contains a symlink");
|
|
115
|
+
identities.push({ dev: stat.dev, ino: stat.ino });
|
|
116
|
+
}
|
|
117
|
+
return identities;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertIdentitiesUnchanged(
|
|
121
|
+
expected: { dev: number; ino: number }[],
|
|
122
|
+
canonicalRoot: string,
|
|
123
|
+
relativePath: string,
|
|
124
|
+
): void {
|
|
125
|
+
let current = canonicalRoot;
|
|
126
|
+
const parts = relativePath.split("/");
|
|
127
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
128
|
+
current = join(current, parts[index]);
|
|
129
|
+
const stat = lstatSync(current);
|
|
130
|
+
if (stat.dev !== expected[index].dev || stat.ino !== expected[index].ino)
|
|
131
|
+
throw new Error(
|
|
132
|
+
`path component changed while being read: ${parts.slice(0, index + 1).join("/")}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Test-only fault seam; production always reads through the secure path. */
|
|
138
|
+
let routingPolicyReaderTestHook: {
|
|
139
|
+
onBeforeDescriptorRead?: () => void;
|
|
140
|
+
} | null = null;
|
|
141
|
+
|
|
142
|
+
export function setRoutingPolicyReaderTestHook(
|
|
143
|
+
hook: { onBeforeDescriptorRead?: () => void } | null,
|
|
144
|
+
): void {
|
|
145
|
+
routingPolicyReaderTestHook = hook;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function statIdentity(stat: ReturnType<typeof statSync>): {
|
|
149
|
+
dev: number;
|
|
150
|
+
ino: number;
|
|
151
|
+
size: number;
|
|
152
|
+
mtimeMs: number;
|
|
153
|
+
} {
|
|
154
|
+
return {
|
|
155
|
+
dev: Number(stat.dev),
|
|
156
|
+
ino: Number(stat.ino),
|
|
157
|
+
size: Number(stat.size),
|
|
158
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function assertSameIdentity(
|
|
163
|
+
before: { dev: number; ino: number; size: number; mtimeMs: number },
|
|
164
|
+
after: ReturnType<typeof fstatSync>,
|
|
165
|
+
what: string,
|
|
166
|
+
): void {
|
|
167
|
+
const current = {
|
|
168
|
+
dev: Number(after.dev),
|
|
169
|
+
ino: Number(after.ino),
|
|
170
|
+
size: Number(after.size),
|
|
171
|
+
mtimeMs: Number(after.mtimeMs),
|
|
172
|
+
};
|
|
173
|
+
if (
|
|
174
|
+
current.dev !== before.dev ||
|
|
175
|
+
current.ino !== before.ino ||
|
|
176
|
+
current.size !== before.size ||
|
|
177
|
+
current.mtimeMs !== before.mtimeMs
|
|
178
|
+
)
|
|
179
|
+
throw new Error(`${what} changed while being read`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Strict canonical v1 parse. Returns a stable reason code on failure. */
|
|
183
|
+
function parseCanonicalPolicy(
|
|
184
|
+
raw: string,
|
|
185
|
+
): { parsed: Record<string, unknown> } | string {
|
|
186
|
+
// Duplicate-key detection must happen before JSON.parse: the reviver runs
|
|
187
|
+
// only on the final object, where an earlier duplicate was already
|
|
188
|
+
// overwritten. The canonical v1 wire form has no ':' inside string values.
|
|
189
|
+
const scanned = [...raw.matchAll(/"([A-Za-z0-9_]+)"\s*:/g)].map(
|
|
190
|
+
(match) => match[1],
|
|
191
|
+
);
|
|
192
|
+
const seenKeys = new Set<string>();
|
|
193
|
+
for (const key of scanned) {
|
|
194
|
+
if (seenKeys.has(key)) return "policy_duplicate_key";
|
|
195
|
+
seenKeys.add(key);
|
|
196
|
+
}
|
|
197
|
+
let parsed: unknown;
|
|
198
|
+
try {
|
|
199
|
+
parsed = JSON.parse(raw);
|
|
200
|
+
} catch {
|
|
201
|
+
return "policy_invalid_json";
|
|
202
|
+
}
|
|
203
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
204
|
+
return "policy_invalid_json";
|
|
205
|
+
}
|
|
206
|
+
const record = parsed as Record<string, unknown>;
|
|
207
|
+
const keys = Object.keys(record);
|
|
208
|
+
const expected = new Set<string>(POLICY_V1_FIELDS);
|
|
209
|
+
if (keys.length !== POLICY_V1_FIELDS.length) {
|
|
210
|
+
const missing = POLICY_V1_FIELDS.some((field) => !(field in record));
|
|
211
|
+
return missing ? "policy_missing_field" : "policy_unknown_field";
|
|
212
|
+
}
|
|
213
|
+
for (const key of keys) {
|
|
214
|
+
if (!expected.has(key)) return "policy_unknown_field";
|
|
215
|
+
}
|
|
216
|
+
// Field order is part of the wire contract; JSON.parse preserves file order,
|
|
217
|
+
// so a reordered object needs an explicit sequence check.
|
|
218
|
+
for (let index = 0; index < POLICY_V1_FIELDS.length; index += 1) {
|
|
219
|
+
if (keys[index] !== POLICY_V1_FIELDS[index]) {
|
|
220
|
+
return "policy_non_canonical_bytes";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (record.contract !== POLICY_CONTRACT) return "policy_unsupported_value";
|
|
224
|
+
if (record.revision !== POLICY_REVISION) return "policy_unsupported_value";
|
|
225
|
+
if (record.new_task_route !== POLICY_NEW_TASK_ROUTE)
|
|
226
|
+
return "policy_unsupported_value";
|
|
227
|
+
if (record.v3_new_plan_sync !== POLICY_V3_NEW_PLAN_SYNC)
|
|
228
|
+
return "policy_unsupported_value";
|
|
229
|
+
if (record.legacy_v3_mode !== POLICY_LEGACY_V3_MODE)
|
|
230
|
+
return "policy_unsupported_value";
|
|
231
|
+
if (record.terminal_import !== POLICY_TERMINAL_IMPORT)
|
|
232
|
+
return "policy_unsupported_value";
|
|
233
|
+
// Indentation and trailing newline are part of the wire contract too.
|
|
234
|
+
if (JSON.stringify(record, null, 2) + "\n" !== raw) {
|
|
235
|
+
return "policy_non_canonical_bytes";
|
|
236
|
+
}
|
|
237
|
+
return { parsed: record };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function invalidProjection(
|
|
241
|
+
reasonCode: string,
|
|
242
|
+
ownership: RoutingPolicyOwnership,
|
|
243
|
+
): RoutingPolicyProjection {
|
|
244
|
+
return {
|
|
245
|
+
policy_status: "invalid",
|
|
246
|
+
route: null,
|
|
247
|
+
v3_new_plan_sync: "allowed",
|
|
248
|
+
legacy_v3_mode: null,
|
|
249
|
+
terminal_import: null,
|
|
250
|
+
worktree_sha256: null,
|
|
251
|
+
index_sha256: null,
|
|
252
|
+
ownership,
|
|
253
|
+
reason_code: reasonCode,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Read-only routing-policy projection. Never writes project state and never
|
|
259
|
+
* mutates the Git index. Fails closed: any present policy that cannot prove
|
|
260
|
+
* exact canonical worktree/index bytes is `invalid`.
|
|
261
|
+
*/
|
|
262
|
+
export function inspectRoutingPolicy(root: string): RoutingPolicyProjection {
|
|
263
|
+
const canonicalRoot = resolveCanonicalRoot(root);
|
|
264
|
+
const relativePath = MANAGED_TASK_ROUTING_POLICY_RELATIVE_PATH;
|
|
265
|
+
const target = join(canonicalRoot, relativePath);
|
|
266
|
+
if (!target.startsWith(canonicalRoot + sep)) {
|
|
267
|
+
return invalidProjection("policy_path_outside_root", "unavailable");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Presence uses lstat, so a symlink at the policy path counts as present.
|
|
271
|
+
let worktreeEntry: ReturnType<typeof lstatSync> | null = null;
|
|
272
|
+
try {
|
|
273
|
+
worktreeEntry = lstatSync(target);
|
|
274
|
+
} catch {
|
|
275
|
+
worktreeEntry = null;
|
|
276
|
+
}
|
|
277
|
+
const inWorktree = worktreeEntry !== null;
|
|
278
|
+
|
|
279
|
+
// Git ownership check: the policy must be present in the index.
|
|
280
|
+
let tracked = false;
|
|
281
|
+
try {
|
|
282
|
+
execFileSync("git", ["ls-files", "--error-unmatch", "--", relativePath], {
|
|
283
|
+
cwd: canonicalRoot,
|
|
284
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
285
|
+
});
|
|
286
|
+
tracked = true;
|
|
287
|
+
} catch {
|
|
288
|
+
tracked = false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!tracked && !inWorktree) {
|
|
292
|
+
// Absent from both worktree and index: legacy compatibility state.
|
|
293
|
+
return {
|
|
294
|
+
policy_status: "legacy_v3",
|
|
295
|
+
route: null,
|
|
296
|
+
v3_new_plan_sync: "allowed",
|
|
297
|
+
legacy_v3_mode: null,
|
|
298
|
+
terminal_import: null,
|
|
299
|
+
worktree_sha256: null,
|
|
300
|
+
index_sha256: null,
|
|
301
|
+
ownership: "absent",
|
|
302
|
+
reason_code: "policy_absent",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (tracked && !inWorktree) {
|
|
306
|
+
// Indexed but deleted from the worktree: drift, never treated as missing.
|
|
307
|
+
const indexBytes = indexPolicyBytes(canonicalRoot, relativePath);
|
|
308
|
+
return {
|
|
309
|
+
policy_status: "invalid",
|
|
310
|
+
route: null,
|
|
311
|
+
v3_new_plan_sync: "allowed",
|
|
312
|
+
legacy_v3_mode: null,
|
|
313
|
+
terminal_import: null,
|
|
314
|
+
worktree_sha256: null,
|
|
315
|
+
index_sha256: indexBytes === null ? null : sha256(indexBytes),
|
|
316
|
+
ownership: "tracked_deleted",
|
|
317
|
+
reason_code: "policy_tracked_deleted",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (!tracked) {
|
|
321
|
+
// A symlinked policy is invalid before ownership: it cannot be a trusted
|
|
322
|
+
// regular file regardless of index state.
|
|
323
|
+
try {
|
|
324
|
+
collectPathIdentities(canonicalRoot, relativePath);
|
|
325
|
+
} catch {
|
|
326
|
+
return invalidProjection("policy_symlink", "untracked");
|
|
327
|
+
}
|
|
328
|
+
return invalidProjection("policy_untracked", "untracked");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Secure regular-file read with symlink rejection and identity re-check.
|
|
332
|
+
const pathIdentities = collectPathIdentities(canonicalRoot, relativePath);
|
|
333
|
+
let before: ReturnType<typeof lstatSync>;
|
|
334
|
+
try {
|
|
335
|
+
before = lstatSync(target);
|
|
336
|
+
} catch {
|
|
337
|
+
return invalidProjection("policy_read_drift", "unavailable");
|
|
338
|
+
}
|
|
339
|
+
if (!before.isFile()) {
|
|
340
|
+
return invalidProjection("policy_not_regular_file", "tracked_clean");
|
|
341
|
+
}
|
|
342
|
+
if (before.size > POLICY_MAX_BYTES) {
|
|
343
|
+
return invalidProjection("policy_oversize", "tracked_clean");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Worktree/index byte identity is checked before canonical parsing: an
|
|
347
|
+
// unstaged rewrite is drift even when the worktree bytes are malformed.
|
|
348
|
+
let indexBytes: string | null = null;
|
|
349
|
+
try {
|
|
350
|
+
indexBytes = indexPolicyBytes(canonicalRoot, relativePath);
|
|
351
|
+
} catch {
|
|
352
|
+
indexBytes = null;
|
|
353
|
+
}
|
|
354
|
+
if (indexBytes === null) {
|
|
355
|
+
return invalidProjection("policy_index_unavailable", "unavailable");
|
|
356
|
+
}
|
|
357
|
+
let bytes: Buffer;
|
|
358
|
+
const fd = openSync(
|
|
359
|
+
target,
|
|
360
|
+
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW,
|
|
361
|
+
);
|
|
362
|
+
try {
|
|
363
|
+
const fdStat = fstatSync(fd);
|
|
364
|
+
assertSameIdentity(
|
|
365
|
+
statIdentity(before),
|
|
366
|
+
fdStat,
|
|
367
|
+
"routing policy descriptor",
|
|
368
|
+
);
|
|
369
|
+
routingPolicyReaderTestHook?.onBeforeDescriptorRead?.();
|
|
370
|
+
bytes = readFileSync(fd);
|
|
371
|
+
} catch {
|
|
372
|
+
closeSync(fd);
|
|
373
|
+
return invalidProjection("policy_read_drift", "unavailable");
|
|
374
|
+
}
|
|
375
|
+
closeSync(fd);
|
|
376
|
+
try {
|
|
377
|
+
assertIdentitiesUnchanged(pathIdentities, canonicalRoot, relativePath);
|
|
378
|
+
} catch {
|
|
379
|
+
return invalidProjection("policy_read_drift", "unavailable");
|
|
380
|
+
}
|
|
381
|
+
// Re-stat the target itself: a same-inode rewrite after the descriptor read
|
|
382
|
+
// is still drift (size/mtime differ from the pre-read snapshot).
|
|
383
|
+
let afterRead: ReturnType<typeof lstatSync>;
|
|
384
|
+
try {
|
|
385
|
+
afterRead = lstatSync(target);
|
|
386
|
+
} catch {
|
|
387
|
+
return invalidProjection("policy_read_drift", "unavailable");
|
|
388
|
+
}
|
|
389
|
+
const beforeIdentity = statIdentity(before);
|
|
390
|
+
if (
|
|
391
|
+
afterRead.dev !== beforeIdentity.dev ||
|
|
392
|
+
afterRead.ino !== beforeIdentity.ino ||
|
|
393
|
+
afterRead.size !== beforeIdentity.size ||
|
|
394
|
+
afterRead.mtimeMs !== beforeIdentity.mtimeMs
|
|
395
|
+
) {
|
|
396
|
+
return invalidProjection("policy_read_drift", "unavailable");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const raw = bytes.toString("utf8");
|
|
400
|
+
const worktreeHash = sha256(raw);
|
|
401
|
+
if (worktreeHash !== sha256(indexBytes)) {
|
|
402
|
+
return {
|
|
403
|
+
policy_status: "invalid",
|
|
404
|
+
route: null,
|
|
405
|
+
v3_new_plan_sync: "allowed",
|
|
406
|
+
legacy_v3_mode: null,
|
|
407
|
+
terminal_import: null,
|
|
408
|
+
worktree_sha256: worktreeHash,
|
|
409
|
+
index_sha256: sha256(indexBytes),
|
|
410
|
+
ownership: "worktree_index_drift",
|
|
411
|
+
reason_code: "policy_worktree_index_drift",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const parsed = parseCanonicalPolicy(raw);
|
|
415
|
+
if (typeof parsed === "string") {
|
|
416
|
+
return invalidProjection(parsed, "tracked_clean");
|
|
417
|
+
}
|
|
418
|
+
if (worktreeHash !== MANAGED_TASK_ROUTING_POLICY_V1_SHA256) {
|
|
419
|
+
return invalidProjection("policy_hash_mismatch", "tracked_clean");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
policy_status: "active",
|
|
424
|
+
route: "kernel_task_intent",
|
|
425
|
+
v3_new_plan_sync: "retired",
|
|
426
|
+
legacy_v3_mode: "drain_read_only",
|
|
427
|
+
terminal_import: "disabled",
|
|
428
|
+
worktree_sha256: worktreeHash,
|
|
429
|
+
index_sha256: sha256(indexBytes),
|
|
430
|
+
ownership: "tracked_clean",
|
|
431
|
+
reason_code: "policy_active",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function indexPolicyBytes(
|
|
436
|
+
canonicalRoot: string,
|
|
437
|
+
relativePath: string,
|
|
438
|
+
): string | null {
|
|
439
|
+
try {
|
|
440
|
+
return execFileSync("git", ["show", `:${relativePath}`], {
|
|
441
|
+
cwd: canonicalRoot,
|
|
442
|
+
encoding: "utf8",
|
|
443
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
444
|
+
});
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Canonical v1 policy bytes for fixtures and documentation. */
|
|
451
|
+
export function policyV1CanonicalBytes(): string {
|
|
452
|
+
return (
|
|
453
|
+
'{\n' +
|
|
454
|
+
' "contract": "immune_brain/managed_task_routing_policy/v1",\n' +
|
|
455
|
+
' "revision": 1,\n' +
|
|
456
|
+
' "new_task_route": "kernel_task_intent",\n' +
|
|
457
|
+
' "v3_new_plan_sync": "retired",\n' +
|
|
458
|
+
' "legacy_v3_mode": "drain_read_only",\n' +
|
|
459
|
+
' "terminal_import": "disabled"\n' +
|
|
460
|
+
'}\n'
|
|
461
|
+
);
|
|
462
|
+
}
|