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,160 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApprovalKind,
|
|
3
|
+
CompletionDecision,
|
|
4
|
+
TaskAttestationV3,
|
|
5
|
+
TaskIntentV1,
|
|
6
|
+
TaskProjectionV3,
|
|
7
|
+
TaskRecordV3,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import { assertKernelInvariantsV3 } from "./validation";
|
|
10
|
+
|
|
11
|
+
const REQUIRED_ATTESTATIONS: Record<TaskIntentV1["risk"], ApprovalKind[]> = {
|
|
12
|
+
routine: ["qa"],
|
|
13
|
+
material: ["qa", "review"],
|
|
14
|
+
critical: ["qa", "review", "user"],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function hasDistinctAuthorityAssignment(
|
|
18
|
+
required: ApprovalKind[],
|
|
19
|
+
candidates: TaskAttestationV3[],
|
|
20
|
+
index = 0,
|
|
21
|
+
usedActors = new Set<string>(),
|
|
22
|
+
): boolean {
|
|
23
|
+
if (index >= required.length) return true;
|
|
24
|
+
for (const attestation of candidates.filter((item) => item.kind === required[index])) {
|
|
25
|
+
if (usedActors.has(attestation.actor_id)) continue;
|
|
26
|
+
const nextActors = new Set(usedActors);
|
|
27
|
+
nextActors.add(attestation.actor_id);
|
|
28
|
+
if (hasDistinctAuthorityAssignment(required, candidates, index + 1, nextActors)) return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function completionDecision(
|
|
34
|
+
intent: TaskIntentV1,
|
|
35
|
+
record: TaskRecordV3,
|
|
36
|
+
currentDiffHash: string,
|
|
37
|
+
currentIntentContentHash: string,
|
|
38
|
+
): CompletionDecision {
|
|
39
|
+
assertKernelInvariantsV3(intent, record);
|
|
40
|
+
const freshAttestations = record.attestations.filter(
|
|
41
|
+
(item) =>
|
|
42
|
+
item.task_revision === intent.revision &&
|
|
43
|
+
item.intent_content_hash === currentIntentContentHash &&
|
|
44
|
+
item.diff_hash === currentDiffHash,
|
|
45
|
+
);
|
|
46
|
+
const freshQaResults = freshAttestations
|
|
47
|
+
.filter((item) => item.kind === "qa")
|
|
48
|
+
.flatMap((item) => item.acceptance_results)
|
|
49
|
+
.filter((item) => item.status === "passed");
|
|
50
|
+
const freshAcceptanceIds = intent.acceptance
|
|
51
|
+
.map((item) => item.id)
|
|
52
|
+
.filter((id) => freshQaResults.some((result) => result.acceptance_id === id));
|
|
53
|
+
const freshAcceptanceSet = new Set(freshAcceptanceIds);
|
|
54
|
+
const missingAcceptanceIds = intent.acceptance
|
|
55
|
+
.map((item) => item.id)
|
|
56
|
+
.filter((id) => !freshAcceptanceSet.has(id));
|
|
57
|
+
const staleAttestationIds = record.attestations
|
|
58
|
+
.filter(
|
|
59
|
+
(item) =>
|
|
60
|
+
item.task_revision !== intent.revision ||
|
|
61
|
+
item.intent_content_hash !== currentIntentContentHash ||
|
|
62
|
+
item.diff_hash !== currentDiffHash,
|
|
63
|
+
)
|
|
64
|
+
.map((item) => item.id);
|
|
65
|
+
|
|
66
|
+
const requiredKinds = REQUIRED_ATTESTATIONS[intent.risk];
|
|
67
|
+
const candidates = freshAttestations.filter((item) => requiredKinds.includes(item.kind));
|
|
68
|
+
const missingApprovalKinds = requiredKinds.filter(
|
|
69
|
+
(kind) => !candidates.some((attestation) => attestation.kind === kind),
|
|
70
|
+
);
|
|
71
|
+
const separationFailure =
|
|
72
|
+
missingApprovalKinds.length === 0 &&
|
|
73
|
+
!hasDistinctAuthorityAssignment(requiredKinds, candidates);
|
|
74
|
+
const repeatedActors = new Set<string>();
|
|
75
|
+
if (separationFailure) {
|
|
76
|
+
for (const attestation of candidates) {
|
|
77
|
+
const kinds = new Set(
|
|
78
|
+
candidates
|
|
79
|
+
.filter((candidate) => candidate.actor_id === attestation.actor_id)
|
|
80
|
+
.map((candidate) => candidate.kind),
|
|
81
|
+
);
|
|
82
|
+
if (kinds.size > 1) repeatedActors.add(attestation.actor_id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const independenceViolations = candidates
|
|
86
|
+
.filter((item) => repeatedActors.has(item.actor_id))
|
|
87
|
+
.map((item) => item.id);
|
|
88
|
+
|
|
89
|
+
const blockingFindingIds = record.findings
|
|
90
|
+
.filter((item) => item.status === "open" && item.kind === "blocking")
|
|
91
|
+
.map((item) => item.id);
|
|
92
|
+
const unresolvedUserDecisionIds = record.findings
|
|
93
|
+
.filter((item) => item.status === "open" && item.kind === "unresolved_user_decision")
|
|
94
|
+
.map((item) => item.id);
|
|
95
|
+
const replanRequiredIds = record.findings
|
|
96
|
+
.filter((item) => item.status === "open" && item.kind === "replan_required")
|
|
97
|
+
.map((item) => item.id);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
complete:
|
|
101
|
+
missingAcceptanceIds.length === 0 &&
|
|
102
|
+
missingApprovalKinds.length === 0 &&
|
|
103
|
+
blockingFindingIds.length === 0 &&
|
|
104
|
+
unresolvedUserDecisionIds.length === 0 &&
|
|
105
|
+
replanRequiredIds.length === 0 &&
|
|
106
|
+
independenceViolations.length === 0,
|
|
107
|
+
fresh_acceptance_ids: freshAcceptanceIds,
|
|
108
|
+
missing_acceptance_ids: missingAcceptanceIds,
|
|
109
|
+
stale_attestation_ids: staleAttestationIds,
|
|
110
|
+
missing_approval_kinds: missingApprovalKinds,
|
|
111
|
+
blocking_finding_ids: blockingFindingIds,
|
|
112
|
+
unresolved_user_decision_ids: unresolvedUserDecisionIds,
|
|
113
|
+
replan_required_ids: replanRequiredIds,
|
|
114
|
+
independence_violations: independenceViolations,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function projectTask(
|
|
119
|
+
intent: TaskIntentV1,
|
|
120
|
+
record: TaskRecordV3,
|
|
121
|
+
currentDiffHash: string,
|
|
122
|
+
currentIntentContentHash: string,
|
|
123
|
+
): TaskProjectionV3 {
|
|
124
|
+
const decision = completionDecision(intent, record, currentDiffHash, currentIntentContentHash);
|
|
125
|
+
const blocked =
|
|
126
|
+
decision.blocking_finding_ids.length > 0 ||
|
|
127
|
+
decision.unresolved_user_decision_ids.length > 0 ||
|
|
128
|
+
decision.replan_required_ids.length > 0 ||
|
|
129
|
+
decision.independence_violations.length > 0;
|
|
130
|
+
let nextObligation: TaskProjectionV3["next_obligation"] = "none";
|
|
131
|
+
if (record.lifecycle === "active") {
|
|
132
|
+
if (decision.unresolved_user_decision_ids.length > 0) {
|
|
133
|
+
nextObligation = "resolve_user_decision";
|
|
134
|
+
} else if (decision.replan_required_ids.length > 0) {
|
|
135
|
+
nextObligation = "revise_intent";
|
|
136
|
+
} else if (decision.blocking_finding_ids.length > 0 || decision.independence_violations.length > 0) {
|
|
137
|
+
nextObligation = "resolve_findings";
|
|
138
|
+
} else if (record.artifact_state === "active") {
|
|
139
|
+
nextObligation = "submit_assurance";
|
|
140
|
+
} else if (decision.missing_acceptance_ids.length > 0 || decision.missing_approval_kinds.includes("qa")) {
|
|
141
|
+
nextObligation = "run_qa";
|
|
142
|
+
} else if (decision.missing_approval_kinds.includes("review")) {
|
|
143
|
+
nextObligation = "run_review";
|
|
144
|
+
} else if (decision.missing_approval_kinds.includes("user")) {
|
|
145
|
+
nextObligation = "authorize_user";
|
|
146
|
+
} else if (decision.complete) {
|
|
147
|
+
nextObligation = "complete";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
contract: "assurance_kernel/projection/v3",
|
|
152
|
+
task_id: record.task_id,
|
|
153
|
+
intent_revision: record.intent_snapshot.revision,
|
|
154
|
+
lifecycle: record.lifecycle,
|
|
155
|
+
artifact_state: record.artifact_state,
|
|
156
|
+
blocked,
|
|
157
|
+
next_obligation: nextObligation,
|
|
158
|
+
...decision,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// P2B0 canary enrollment core. NOT exported from kernel/index.ts.
|
|
2
|
+
// Atomically creates TaskRecord v3 + workspace working claim + backend claim
|
|
3
|
+
// for one confirmed canary task. Requires a valid EnrollmentCapability.
|
|
4
|
+
// No CLI, runtime route, or production issuer exists in P2B0.
|
|
5
|
+
|
|
6
|
+
import { canonicalIntentHash, readTaskIntent } from "./intent";
|
|
7
|
+
import {
|
|
8
|
+
type EnrollmentAuthorityRegistry,
|
|
9
|
+
type EnrollmentCapabilityBinding,
|
|
10
|
+
} from "./enrollment_authority";
|
|
11
|
+
import { readTaskTombstone, type BackendClaim } from "./backend_claim";
|
|
12
|
+
import { preparePiCanary } from "./pi_canary_prepare";
|
|
13
|
+
import {
|
|
14
|
+
commitEnrollmentLocked,
|
|
15
|
+
readTaskRecordRaw,
|
|
16
|
+
readWorkspaceStateRaw,
|
|
17
|
+
withKernelStoreLock,
|
|
18
|
+
} from "./storage";
|
|
19
|
+
import type { TaskRecordV3, WorkspaceStateLike } from "./types";
|
|
20
|
+
|
|
21
|
+
export interface EnrollCanaryInput {
|
|
22
|
+
task_id: string;
|
|
23
|
+
intent_path: string;
|
|
24
|
+
intent_revision: number;
|
|
25
|
+
preparation_digest: string;
|
|
26
|
+
capability: object;
|
|
27
|
+
capability_binding: EnrollmentCapabilityBinding;
|
|
28
|
+
now: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EnrollCanaryResult {
|
|
32
|
+
record: TaskRecordV3;
|
|
33
|
+
backend_claim: BackendClaim;
|
|
34
|
+
workspace: { revision: string; state: WorkspaceStateLike };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildTaskRecordV3(
|
|
38
|
+
input: EnrollCanaryInput,
|
|
39
|
+
intent: Awaited<ReturnType<typeof readTaskIntent>>,
|
|
40
|
+
): TaskRecordV3 {
|
|
41
|
+
return {
|
|
42
|
+
contract: "assurance_kernel/task_record/v3",
|
|
43
|
+
task_id: input.task_id,
|
|
44
|
+
intent_snapshot: intent.intent,
|
|
45
|
+
intent_ref: {
|
|
46
|
+
path: input.intent_path,
|
|
47
|
+
content_hash: intent.content_hash,
|
|
48
|
+
},
|
|
49
|
+
lifecycle: "active",
|
|
50
|
+
artifact_state: "active",
|
|
51
|
+
baseline: intent.content_hash,
|
|
52
|
+
attestations: [],
|
|
53
|
+
findings: [],
|
|
54
|
+
history: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Rehearsal: validates every precondition and returns evidence, but writes
|
|
60
|
+
* nothing and never consumes the capability.
|
|
61
|
+
*/
|
|
62
|
+
export function runEnrollmentRehearsal(
|
|
63
|
+
root: string,
|
|
64
|
+
input: EnrollCanaryInput,
|
|
65
|
+
capability: object,
|
|
66
|
+
registry: EnrollmentAuthorityRegistry,
|
|
67
|
+
): {
|
|
68
|
+
rehearsed: boolean;
|
|
69
|
+
writes_performed: boolean;
|
|
70
|
+
evidence: {
|
|
71
|
+
contract: "assurance_kernel/enrollment_rehearsal/v1";
|
|
72
|
+
task_id: string;
|
|
73
|
+
outcome: "ready" | "not_ready";
|
|
74
|
+
blockers: string[];
|
|
75
|
+
generated_at: string;
|
|
76
|
+
};
|
|
77
|
+
} {
|
|
78
|
+
const blockers: string[] = [];
|
|
79
|
+
const result = {
|
|
80
|
+
rehearsed: true,
|
|
81
|
+
writes_performed: false,
|
|
82
|
+
evidence: {
|
|
83
|
+
contract: "assurance_kernel/enrollment_rehearsal/v1" as const,
|
|
84
|
+
task_id: input.task_id,
|
|
85
|
+
outcome: "ready" as "ready" | "not_ready",
|
|
86
|
+
blockers,
|
|
87
|
+
generated_at: input.now,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
// capability inspect (does not consume)
|
|
92
|
+
registry.inspect(capability, input.capability_binding);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
blockers.push(`capability: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
+
}
|
|
96
|
+
withKernelStoreLock(root, () => {
|
|
97
|
+
const tombstone = readTaskTombstone(root, input.task_id);
|
|
98
|
+
if (tombstone) {
|
|
99
|
+
blockers.push("task tombstone exists; same-task re-enrollment is forbidden");
|
|
100
|
+
} else {
|
|
101
|
+
const current = readTaskRecordRaw(root, input.task_id);
|
|
102
|
+
if (current.record) blockers.push("task record already exists");
|
|
103
|
+
}
|
|
104
|
+
const workspace = readWorkspaceStateRaw(root);
|
|
105
|
+
if (workspace.state.current_working !== null)
|
|
106
|
+
blockers.push(`workspace already owned by ${workspace.state.current_working}`);
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
readTaskIntent(root, input.task_id);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
blockers.push(`intent: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
result.evidence.outcome = blockers.length === 0 ? "ready" : "not_ready";
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Atomic canary enrollment. Runs inside the same store lock as v1/v2
|
|
119
|
+
* transactions; consumes the capability only after every precondition
|
|
120
|
+
* passes, immediately before writing the enrollment marker.
|
|
121
|
+
*/
|
|
122
|
+
export function enrollCanaryTask(
|
|
123
|
+
root: string,
|
|
124
|
+
input: EnrollCanaryInput,
|
|
125
|
+
registry: EnrollmentAuthorityRegistry,
|
|
126
|
+
): EnrollCanaryResult {
|
|
127
|
+
const validated = registry.inspect(input.capability, input.capability_binding);
|
|
128
|
+
if (validated.task_id !== input.task_id)
|
|
129
|
+
throw new Error("enrollment capability task mismatch");
|
|
130
|
+
|
|
131
|
+
// v4 storage retirement: the capability is bound to the preparation
|
|
132
|
+
// digest computed from Kernel owners only. Recompute it inside the store
|
|
133
|
+
// lock and reject if the owner set changed since the confirmation.
|
|
134
|
+
const recomputed = preparePiCanary(root, { task_id: input.task_id, now: input.now });
|
|
135
|
+
if (recomputed.digest !== input.preparation_digest)
|
|
136
|
+
throw new Error("enrollment preparation digest mismatch");
|
|
137
|
+
|
|
138
|
+
return withKernelStoreLock(root, () => {
|
|
139
|
+
const tombstone = readTaskTombstone(root, input.task_id);
|
|
140
|
+
if (tombstone)
|
|
141
|
+
throw new Error(
|
|
142
|
+
`task ${input.task_id} is terminal; same-task re-enrollment is forbidden`,
|
|
143
|
+
);
|
|
144
|
+
const current = readTaskRecordRaw(root, input.task_id);
|
|
145
|
+
if (current.record)
|
|
146
|
+
throw new Error(`task ${input.task_id} already has a TaskRecord`);
|
|
147
|
+
const workspace = readWorkspaceStateRaw(root);
|
|
148
|
+
if (workspace.state.current_working !== null)
|
|
149
|
+
throw new Error(`workspace is already owned by ${workspace.state.current_working}`);
|
|
150
|
+
|
|
151
|
+
// fresh secure intent reread inside the lock
|
|
152
|
+
const intent = readTaskIntent(root, input.task_id);
|
|
153
|
+
if (intent.intent.revision !== input.intent_revision)
|
|
154
|
+
throw new Error("intent revision mismatch");
|
|
155
|
+
|
|
156
|
+
// consume immediately before the marker write
|
|
157
|
+
registry.consume(input.capability, input.capability_binding);
|
|
158
|
+
|
|
159
|
+
const record = buildTaskRecordV3(input, intent);
|
|
160
|
+
const nextWorkspace: WorkspaceStateLike = {
|
|
161
|
+
...workspace.state,
|
|
162
|
+
current_working: input.task_id,
|
|
163
|
+
};
|
|
164
|
+
const claim: BackendClaim = {
|
|
165
|
+
contract: "assurance_kernel/backend_claim/v2",
|
|
166
|
+
backend: "kernel",
|
|
167
|
+
task_id: input.task_id,
|
|
168
|
+
intent_revision: input.intent_revision,
|
|
169
|
+
intent_content_hash: intent.content_hash,
|
|
170
|
+
enrollment_event_id: `enroll-${input.task_id}-${input.now}`,
|
|
171
|
+
lifecycle_status: "active",
|
|
172
|
+
created_at: input.now,
|
|
173
|
+
updated_at: input.now,
|
|
174
|
+
};
|
|
175
|
+
const mutation = commitEnrollmentLocked(
|
|
176
|
+
root,
|
|
177
|
+
input.task_id,
|
|
178
|
+
{
|
|
179
|
+
contract: "assurance_kernel/workspace_transaction/v2",
|
|
180
|
+
task_id: input.task_id,
|
|
181
|
+
expected_record_hash: current.revision,
|
|
182
|
+
next_record_content: `${JSON.stringify(record, null, 2)}\n`,
|
|
183
|
+
expected_workspace_hash: workspace.revision,
|
|
184
|
+
next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}\n`,
|
|
185
|
+
},
|
|
186
|
+
claim as unknown as Record<string, unknown>,
|
|
187
|
+
);
|
|
188
|
+
return {
|
|
189
|
+
record: mutation.record,
|
|
190
|
+
backend_claim: claim,
|
|
191
|
+
workspace: { revision: "", state: mutation.workspace },
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// P2B1 enrollment authority. NOT exported from kernel/index.ts.
|
|
2
|
+
// Explicit registry factory: each registry owns its WeakMap; capabilities are
|
|
3
|
+
// only recognized by the registry that issued them. No module-level singleton.
|
|
4
|
+
// The production registry is created only inside the Pi extension activation
|
|
5
|
+
// closure; tests create isolated registries via the fixture seam.
|
|
6
|
+
|
|
7
|
+
import { MUTATION_AUTHORITY_CAPABILITY_BRAND } from "./types";
|
|
8
|
+
|
|
9
|
+
export const ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
|
|
10
|
+
|
|
11
|
+
export interface EnrollmentCapabilityBinding {
|
|
12
|
+
task_id: string;
|
|
13
|
+
intent_path: string;
|
|
14
|
+
intent_revision: number;
|
|
15
|
+
intent_content_hash: string;
|
|
16
|
+
preparation_digest: string;
|
|
17
|
+
actor_id: string;
|
|
18
|
+
confirmation_ref: string;
|
|
19
|
+
expires_at: string;
|
|
20
|
+
nonce: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface EnrollmentCapabilityState extends EnrollmentCapabilityBinding {
|
|
24
|
+
issued_at: string;
|
|
25
|
+
consumed: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ValidatedEnrollment {
|
|
29
|
+
task_id: string;
|
|
30
|
+
intent_path: string;
|
|
31
|
+
intent_revision: number;
|
|
32
|
+
intent_content_hash: string;
|
|
33
|
+
preparation_digest: string;
|
|
34
|
+
actor_id: string;
|
|
35
|
+
confirmation_ref: string;
|
|
36
|
+
issued_at: string;
|
|
37
|
+
expires_at: string;
|
|
38
|
+
nonce: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EnrollmentAuthorityRegistry {
|
|
42
|
+
readonly brand: symbol;
|
|
43
|
+
/** Issue one capability bound to an exact binding. Library primitive. */
|
|
44
|
+
issue(binding: EnrollmentCapabilityBinding, issuedAt?: string): object;
|
|
45
|
+
inspect(capability: object, expected: EnrollmentCapabilityBinding, now?: number): ValidatedEnrollment;
|
|
46
|
+
consume(capability: object, expected: EnrollmentCapabilityBinding, now?: number): ValidatedEnrollment;
|
|
47
|
+
isConsumed(capability: object): boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createEnrollmentAuthorityRegistry(): EnrollmentAuthorityRegistry {
|
|
51
|
+
const states = new WeakMap<object, EnrollmentCapabilityState>();
|
|
52
|
+
const brand = Symbol.for("assurance-kernel.enrollment-capability-registry");
|
|
53
|
+
|
|
54
|
+
function isEnrollmentCapability(value: unknown): value is object {
|
|
55
|
+
return (
|
|
56
|
+
!!value &&
|
|
57
|
+
typeof value === "object" &&
|
|
58
|
+
(value as Record<symbol, unknown>)[ENROLLMENT_CAPABILITY_BRAND] === true &&
|
|
59
|
+
(value as Record<symbol, unknown>)[brand] === true
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stateOf(capability: object): EnrollmentCapabilityState {
|
|
64
|
+
const state = states.get(capability);
|
|
65
|
+
if (!state) throw new Error("enrollment capability is not recognized by this registry");
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
brand,
|
|
71
|
+
issue(binding: EnrollmentCapabilityBinding, issuedAt = new Date().toISOString()): object {
|
|
72
|
+
const missing: string[] = [];
|
|
73
|
+
for (const [key, value] of Object.entries(binding)) {
|
|
74
|
+
if (value === undefined || value === null || value === "") missing.push(key);
|
|
75
|
+
}
|
|
76
|
+
if (missing.length > 0)
|
|
77
|
+
throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
|
|
78
|
+
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
79
|
+
throw new Error("enrollment capability must have a future expiry");
|
|
80
|
+
const capability = Object.freeze(
|
|
81
|
+
Object.defineProperties(
|
|
82
|
+
{},
|
|
83
|
+
{
|
|
84
|
+
[ENROLLMENT_CAPABILITY_BRAND]: { value: true, enumerable: false, writable: false, configurable: false },
|
|
85
|
+
[brand]: { value: true, enumerable: false, writable: false, configurable: false },
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
states.set(capability, { ...binding, issued_at: issuedAt, consumed: false });
|
|
90
|
+
return capability;
|
|
91
|
+
},
|
|
92
|
+
inspect(capability: object, expected: EnrollmentCapabilityBinding, now = Date.now()): ValidatedEnrollment {
|
|
93
|
+
if (!isEnrollmentCapability(capability)) throw new Error("enrollment capability required");
|
|
94
|
+
const state = stateOf(capability);
|
|
95
|
+
if (state.consumed) throw new Error("enrollment capability already consumed");
|
|
96
|
+
if (Date.parse(state.expires_at) <= now) throw new Error("enrollment capability has expired");
|
|
97
|
+
for (const key of Object.keys(expected) as Array<keyof EnrollmentCapabilityBinding>) {
|
|
98
|
+
if (state[key] !== expected[key])
|
|
99
|
+
throw new Error(`enrollment capability ${key} mismatch`);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
task_id: state.task_id,
|
|
103
|
+
intent_path: state.intent_path,
|
|
104
|
+
intent_revision: state.intent_revision,
|
|
105
|
+
intent_content_hash: state.intent_content_hash,
|
|
106
|
+
preparation_digest: state.preparation_digest,
|
|
107
|
+
actor_id: state.actor_id,
|
|
108
|
+
confirmation_ref: state.confirmation_ref,
|
|
109
|
+
issued_at: state.issued_at,
|
|
110
|
+
expires_at: state.expires_at,
|
|
111
|
+
nonce: state.nonce,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
consume(capability: object, expected: EnrollmentCapabilityBinding, now = Date.now()): ValidatedEnrollment {
|
|
115
|
+
const validated = this.inspect(capability, expected, now);
|
|
116
|
+
stateOf(capability).consumed = true;
|
|
117
|
+
return validated;
|
|
118
|
+
},
|
|
119
|
+
isConsumed(capability: object): boolean {
|
|
120
|
+
return stateOf(capability).consumed;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./intent";
|
|
3
|
+
export * from "./validation";
|
|
4
|
+
export * from "./completion";
|
|
5
|
+
export * from "./legacy";
|
|
6
|
+
// v4 storage retirement: the v1 TaskRecord storage entry points are no
|
|
7
|
+
// longer part of the production kernel surface. Only the v2 store read/commit
|
|
8
|
+
// primitives (used by enrollment/rehearsal/audit) remain exported from
|
|
9
|
+
// ./storage; the v1 writer/reader names are absent.
|
|
10
|
+
export {
|
|
11
|
+
readWorkspaceStateRaw,
|
|
12
|
+
readTaskRecordRaw,
|
|
13
|
+
readTaskRecord,
|
|
14
|
+
readSecureProjectFile,
|
|
15
|
+
commitTaskRecordLocked,
|
|
16
|
+
withKernelStoreLock,
|
|
17
|
+
serializeWorkspace,
|
|
18
|
+
revisionForContent,
|
|
19
|
+
appendJournalEntry,
|
|
20
|
+
KernelStoreSecurityError,
|
|
21
|
+
setAfterTaskTransactionWriteForTest,
|
|
22
|
+
} from "./storage";
|
|
23
|
+
export {
|
|
24
|
+
reduceTask,
|
|
25
|
+
canonicalRecordHash,
|
|
26
|
+
actionFingerprint,
|
|
27
|
+
recordedActionFingerprint,
|
|
28
|
+
isReducedMutation,
|
|
29
|
+
} from "./reducer";
|