mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 22:58:42 -10:00
fix(policy): Seam B — bootstrap node creation + required provenance is one ATOMIC unit (savepoint rollback on setter fault; nodeIdMap populated only after success; sibling partial-success preserved; no half-created node can resume wider) + two-member selective-fault pin (Guard multi-seat probe at 16e853a7)
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
16e853a75d
commit
4ac243c3c0
2 files changed
+87
-10
No files matched your search
@@ -1154,8 +1154,7 @@ export class PodRigInstantiator {
|
||||
}
|
||||
|
||||
// Create node
|
||||
let nodeId: string;
|
||||
try {
|
||||
const createUnit = this.db.transaction(() => {
|
||||
const node = this.deps.rigRepo.addNode(rigId, qualifiedId, {
|
||||
// OPR.0.4.6.FAC1 (VM-caught): the pod-aware bootstrap
|
||||
// instantiate-from-YAML path (`rig up <spec>`) creates agent
|
||||
@@ -1184,15 +1183,21 @@ export class PodRigInstantiator {
|
||||
resolvedSpecVersion: configResult.config.resolvedSpecVersion,
|
||||
resolvedSpecHash: configResult.config.resolvedSpecHash,
|
||||
});
|
||||
nodeId = node.id;
|
||||
nodeIdMap[qualifiedId] = nodeId;
|
||||
// R2 terminal (4c49c758): the bootstrap path's provenance write is the FIRST
|
||||
// write for this node (not a launch-time refresh) — it is LOAD-BEARING restart
|
||||
// state and must be STRICT: a real setter fault fails THIS seat visibly (failed
|
||||
// node result, no silent success), so restore can never fall through a missing
|
||||
// member record to the rig attachment (member precedence + restart truth).
|
||||
// Guard multi-seat correction (16e853a7): node creation + required provenance
|
||||
// are ONE ATOMIC UNIT (SQLite savepoint). A setter fault rolls the INSERT back
|
||||
// too — under partial-success semantics the sibling may proceed, but a failed
|
||||
// member can never survive as a half-created node whose restore posture would
|
||||
// widen to the rig attachment. (Previously the row survived with provenance
|
||||
// null, and resolveRestorePosture fell through to rig full_bypass.)
|
||||
const bootstrapAttachment = this.resolveMemberPolicyAttachment(member.permissionPolicy, rigSpec.permissionPolicy, rigRoot);
|
||||
if (bootstrapAttachment) this.persistNodePolicyProvenanceStrict(nodeId, bootstrapAttachment);
|
||||
if (bootstrapAttachment) this.persistNodePolicyProvenanceStrict(node.id, bootstrapAttachment);
|
||||
return node.id;
|
||||
});
|
||||
let nodeId: string;
|
||||
try {
|
||||
nodeId = createUnit();
|
||||
// nodeIdMap only AFTER the atomic unit succeeds (Guard contract item 2).
|
||||
nodeIdMap[qualifiedId] = nodeId;
|
||||
} catch (err) {
|
||||
nodeResults.push({ logicalId: qualifiedId, status: "failed", error: (err as Error).message });
|
||||
continue;
|
||||
|
||||
@@ -35,7 +35,79 @@ pods:
|
||||
edges: []
|
||||
`;
|
||||
|
||||
const TWO_MEMBER_RIG_YAML = `version: "0.2"
|
||||
name: bootstrap-policy-probe-2
|
||||
permission_policy: builtin:yolo
|
||||
pods:
|
||||
- id: dev
|
||||
label: Dev
|
||||
members:
|
||||
- id: impl
|
||||
runtime: claude-code
|
||||
agent_ref: local:agents/impl
|
||||
profile: default
|
||||
permission_policy: builtin:locked
|
||||
cwd: .
|
||||
- id: qa
|
||||
runtime: claude-code
|
||||
agent_ref: local:agents/impl
|
||||
profile: default
|
||||
permission_policy: builtin:locked
|
||||
cwd: .
|
||||
edges: []
|
||||
edges: []
|
||||
`;
|
||||
|
||||
describe("R2 bootstrap provenance fault probe", () => {
|
||||
it("SELECTIVE first-setter fault in a TWO-member bootstrap: the sibling may launch (partial-success semantics), but the failed member must NOT survive as a resumable node whose restore posture widens to the rig full_bypass (Guard multi-seat probe at 16e853a7)", async () => {
|
||||
const bindings: NodeBinding[] = [];
|
||||
const adapter: RuntimeAdapter = {
|
||||
runtime: "claude-code",
|
||||
listInstalled: async () => [],
|
||||
project: async () => ({ projected: [], skipped: [], failed: [] }),
|
||||
deliverStartup: async () => ({ delivered: 0, failed: [] }),
|
||||
launchHarness: async (binding) => { bindings.push(binding); return { ok: true }; },
|
||||
checkReady: async () => ({ ready: true }),
|
||||
} as unknown as RuntimeAdapter;
|
||||
const db = createFullTestDb();
|
||||
try {
|
||||
const setup = createTestApp(db, {
|
||||
adapters: { "claude-code": adapter },
|
||||
podInstantiatorFsOps: {
|
||||
exists: (path: string) => path.includes("agents/impl"),
|
||||
readFile: (path: string) => {
|
||||
if (path.includes("agents/impl")) return AGENT_YAML;
|
||||
throw new Error(`not found: ${path}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
// selective fault: FIRST provenance write throws, later ones delegate normally
|
||||
const original = setup.rigRepo.setNodePolicyProvenance.bind(setup.rigRepo);
|
||||
let calls = 0;
|
||||
setup.rigRepo.setNodePolicyProvenance = (nodeId, prov) => {
|
||||
calls += 1;
|
||||
if (calls === 1) throw new Error("injected selective provenance fault");
|
||||
return original(nodeId, prov);
|
||||
};
|
||||
|
||||
const outcome = await setup.podInstantiator.instantiate(TWO_MEMBER_RIG_YAML, "/rig");
|
||||
|
||||
// partial-success semantics: the UNAFFECTED sibling may launch and the rig may report ok
|
||||
expect(bindings.length, "exactly the unaffected sibling launches").toBe(1);
|
||||
void outcome; // ok may be true under partial-success — that is the preserved contract
|
||||
|
||||
// the load-bearing invariant: NO resumable node lacking required provenance survives.
|
||||
const rows = db.prepare("SELECT logical_id FROM nodes WHERE logical_id LIKE 'dev.%'").all() as { logical_id: string }[];
|
||||
const survivors = rows.map((r) => r.logical_id).sort();
|
||||
expect(survivors, "the failed member must not survive as a half-created node").toHaveLength(1);
|
||||
// the surviving sibling carries its member provenance (locked/floor), never the rig widening
|
||||
const survivor = db.prepare("SELECT id, rig_id FROM nodes WHERE logical_id = ?").get(survivors[0]!) as { id: string; rig_id: string };
|
||||
expect(setup.rigRepo.getNodePolicyProvenance(survivor.id)).toMatchObject({ origin: "builtin", launchPosture: "floor" });
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("happy path: bootstrap persists the MEMBER attachment (locked/floor) with the rig attachment (yolo) alongside — precedence + restart truth agree", async () => {
|
||||
const bindings: NodeBinding[] = [];
|
||||
const adapter: RuntimeAdapter = {
|
||||
|
||||
Reference in new issue
Block a user