mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 22:58:42 -10:00
feat(policy): OPR.0.4.8.3 Seam B complete — materialize-time provenance, restore re-derivation (both paths), successor continuity, expansion/agent-image/fixture surfaces, R6 discrimination pins incl. DB-reopen full_bypass restore proof
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
8cebaec5a4
commit
9e94c274c9
13 files changed
+289
-14
No files matched your search
@@ -36,6 +36,8 @@ export class CodexResumeAdapter {
|
||||
resumeToken: string | null,
|
||||
_cwd: string,
|
||||
codexConfigProfile?: string | null,
|
||||
// OPR.0.4.8.3 Seam B: persisted resolved posture threaded from restore.
|
||||
resolvedPosture?: "floor" | "full_bypass",
|
||||
): Promise<ResumeResult> {
|
||||
if (!this.canResume(resumeType, resumeToken)) {
|
||||
return { ok: false, code: "no_resume", message: "Codex resume not available" };
|
||||
@@ -61,6 +63,8 @@ export class CodexResumeAdapter {
|
||||
resumeToken ?? "",
|
||||
codexConfigProfile,
|
||||
resumeType === "codex_last",
|
||||
undefined,
|
||||
resolvedPosture,
|
||||
);
|
||||
|
||||
const textResult = await this.tmux.sendText(tmuxSessionName, cmd);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type Database from "better-sqlite3";
|
||||
import type { RigRepository } from "./rig-repository.js";
|
||||
import { resolvePermissionPolicyAttachment } from "./permission-policy/policy-ref.js";
|
||||
import type { SessionRegistry } from "./session-registry.js";
|
||||
import type { EventBus } from "./event-bus.js";
|
||||
import type { SnapshotRepository } from "./snapshot-repository.js";
|
||||
@@ -983,7 +984,7 @@ export class RestoreOrchestrator {
|
||||
await this.rollbackToZeroSession(node.id, sessionName, launchResult?.session.id, priorState);
|
||||
return { nodeId: node.id, logicalId: node.logicalId, status: "awaiting-decision", error: `Original session unresumable: resume requested but no token available. No session is running. Re-run with --fresh ${node.logicalId} for a deliberate fresh-primed seat, or restore the original session manually.` };
|
||||
} else {
|
||||
const resumeOutcome = await this.attemptResume(sessionName, resumeType, resumeToken, node.cwd ?? "/", node.codexConfigProfile, node.model);
|
||||
const resumeOutcome = await this.attemptResume(sessionName, resumeType, resumeToken, node.cwd ?? "/", node.codexConfigProfile, node.model, this.resolveRestorePosture(node.id, rigId));
|
||||
if (resumeOutcome.kind === "resumed") {
|
||||
baseStatus = "resumed";
|
||||
} else if (resumeOutcome.kind === "attention_required") {
|
||||
@@ -1104,6 +1105,9 @@ export class RestoreOrchestrator {
|
||||
...launchResult.binding,
|
||||
cwd: node.cwd ?? ".",
|
||||
codexConfigProfile: node.codexConfigProfile ?? undefined,
|
||||
// OPR.0.4.8.3 Seam B: the pod-aware restore path binds the restored posture too
|
||||
// (both restore paths consume persisted provenance — preflight surface 3).
|
||||
launchPosture: this.resolveRestorePosture(node.id, rigId),
|
||||
// OPR.0.4.6.PI1 VM leg finding: the restore binding dropped the
|
||||
// node's model declaration, so a resumed Pi seat relaunched with
|
||||
// no --model — and the runner's provider-key allowlist (keyed off
|
||||
@@ -1235,6 +1239,44 @@ export class RestoreOrchestrator {
|
||||
return row.resume_type === resumeType && row.resume_token === resumeToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* OPR.0.4.8.3 Seam B — a restored seat's launch posture WITHOUT the in-memory RigSpec
|
||||
* (dev-guard restart-provenance ruling):
|
||||
* 1. Persisted node provenance (migration 057) is the primary source. For a CUSTOM
|
||||
* attachment with readable provenance (declaringDir + the node's raw ref) the policy
|
||||
* is REOPENED + re-derived (a custom surface:flag policy restores to full_bypass);
|
||||
* an unreadable file degrades to the PERSISTED posture (still restart-stable).
|
||||
* 2. No provenance (e.g. organic claim/self-attach seats): the persisted RIG-level ref
|
||||
* resolves — builtin refs resolve dirlessly; a custom rig ref without provenance
|
||||
* degrades to the resolver's advisory floor (honest absence of a declaring dir).
|
||||
* 3. Nothing attached → undefined (the env-driven 0.4.8.2 decision, unchanged).
|
||||
*/
|
||||
private resolveRestorePosture(nodeId: string, rigId: string): "floor" | "full_bypass" | undefined {
|
||||
try {
|
||||
const prov = this.rigRepo.getNodePolicyProvenance(nodeId);
|
||||
if (prov) {
|
||||
if (prov.origin === "custom" && prov.declaringDir) {
|
||||
const ref = prov.nodeRef ?? this.rigRepo.getRigPermissionPolicy(rigId);
|
||||
if (ref) {
|
||||
try {
|
||||
return resolvePermissionPolicyAttachment(ref, prov.declaringDir, {
|
||||
readFile: (p) => readFileSync(p, "utf-8"),
|
||||
}).launchPosture;
|
||||
} catch { /* unreadable at restore → persisted posture below */ }
|
||||
}
|
||||
}
|
||||
return prov.launchPosture;
|
||||
}
|
||||
const rigRef = this.rigRepo.getRigPermissionPolicy(rigId);
|
||||
if (rigRef) {
|
||||
return resolvePermissionPolicyAttachment(rigRef, "", {
|
||||
readFile: (p) => readFileSync(p, "utf-8"),
|
||||
}).launchPosture;
|
||||
}
|
||||
} catch { /* posture resolution must never block a restore */ }
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async attemptResume(
|
||||
sessionName: string,
|
||||
resumeType: string,
|
||||
@@ -1242,6 +1284,9 @@ export class RestoreOrchestrator {
|
||||
cwd: string,
|
||||
codexConfigProfile?: string | null,
|
||||
model?: string | null,
|
||||
// OPR.0.4.8.3 Seam B: the seat's restored launch posture (persisted provenance,
|
||||
// custom policies re-validated when readable). Absent = env decision.
|
||||
resolvedPosture?: "floor" | "full_bypass",
|
||||
): Promise<
|
||||
| { kind: "resumed" }
|
||||
| { kind: "retry_fresh" }
|
||||
@@ -1249,7 +1294,7 @@ export class RestoreOrchestrator {
|
||||
| { kind: "attention_required"; message: string; evidence?: string }
|
||||
> {
|
||||
if (this.claudeResume.canResume(resumeType, resumeToken)) {
|
||||
const result = await this.claudeResume.resume(sessionName, resumeType, resumeToken, cwd);
|
||||
const result = await this.claudeResume.resume(sessionName, resumeType, resumeToken, cwd, resolvedPosture);
|
||||
if (result.ok) return { kind: "resumed" };
|
||||
if (result.code === "retry_fresh") return { kind: "retry_fresh" };
|
||||
// L3: surface attention_required from the Claude probe (resume-selection prompt).
|
||||
@@ -1264,7 +1309,7 @@ export class RestoreOrchestrator {
|
||||
}
|
||||
|
||||
if (this.codexResume.canResume(resumeType, resumeToken)) {
|
||||
const result = await this.codexResume.resume(sessionName, resumeType, resumeToken, cwd, codexConfigProfile);
|
||||
const result = await this.codexResume.resume(sessionName, resumeType, resumeToken, cwd, codexConfigProfile, resolvedPosture);
|
||||
if (result.ok) return { kind: "resumed" };
|
||||
if (result.code === "retry_fresh") return { kind: "retry_fresh" };
|
||||
// Codex auth-refusal: stored OAuth token can no longer be refreshed.
|
||||
@@ -1285,7 +1330,7 @@ export class RestoreOrchestrator {
|
||||
// session file returns retry_fresh, which the caller maps to the
|
||||
// awaiting-decision stop-and-ask — never a silent fresh start (BR-6).
|
||||
if (this.piResume?.canResume(resumeType, resumeToken)) {
|
||||
const result = await this.piResume.resume(sessionName, resumeType, resumeToken, cwd, model);
|
||||
const result = await this.piResume.resume(sessionName, resumeType, resumeToken, cwd, model, resolvedPosture);
|
||||
if (result.ok) return { kind: "resumed" };
|
||||
if (result.code === "retry_fresh") return { kind: "retry_fresh" };
|
||||
if (result.code === "attention_required") {
|
||||
|
||||
@@ -142,6 +142,8 @@ export class RigExpansionService {
|
||||
...(member.agentRef ? { agent_ref: member.agentRef } : {}),
|
||||
...(member.profile ? { profile: member.profile } : {}),
|
||||
...(member.codexConfigProfile ? { codex_config_profile: member.codexConfigProfile } : {}),
|
||||
// OPR.0.4.8.3 Seam B: permission_policy rides the fragment→spec map like role.
|
||||
...(member.permissionPolicy ? { permission_policy: member.permissionPolicy } : {}),
|
||||
...(member.cwd ? { cwd: member.cwd } : {}),
|
||||
...(member.model ? { model: member.model } : {}),
|
||||
// OPR.0.4.6.FAC1: role rides the fragment→spec map (a
|
||||
|
||||
@@ -213,11 +213,14 @@ export class RigRepository {
|
||||
resolvedTarget: string | null;
|
||||
declaringDir: string | null;
|
||||
launchPosture: "floor" | "full_bypass";
|
||||
/** The node's own raw ref (member-level; null when the attachment came from the rig). */
|
||||
nodeRef: string | null;
|
||||
} | null {
|
||||
if (!this.hasNodeColumn("policy_launch_posture")) return null;
|
||||
const row = this.db.prepare(
|
||||
"SELECT policy_origin, policy_resolved_target, policy_declaring_dir, policy_launch_posture FROM nodes WHERE id = ?",
|
||||
"SELECT permission_policy, policy_origin, policy_resolved_target, policy_declaring_dir, policy_launch_posture FROM nodes WHERE id = ?",
|
||||
).get(nodeId) as {
|
||||
permission_policy: string | null;
|
||||
policy_origin: string | null;
|
||||
policy_resolved_target: string | null;
|
||||
policy_declaring_dir: string | null;
|
||||
@@ -229,6 +232,7 @@ export class RigRepository {
|
||||
resolvedTarget: row.policy_resolved_target,
|
||||
declaringDir: row.policy_declaring_dir,
|
||||
launchPosture: row.policy_launch_posture as "floor" | "full_bypass",
|
||||
nodeRef: row.permission_policy,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -595,6 +595,10 @@ export class PodRigInstantiator {
|
||||
cwdOverride: opts?.cwdOverride,
|
||||
});
|
||||
logicalIdToNodeId.set(qualifiedId, node.id);
|
||||
// Seam B R2: restart-stable provenance persists AT MATERIALIZE (a
|
||||
// materialized-but-never-launched seat must still restore its posture).
|
||||
const materializeAttachment = this.resolveMemberPolicyAttachment(member.permissionPolicy, rigSpec.permissionPolicy, rigRoot);
|
||||
if (materializeAttachment) this.persistNodePolicyProvenance(node.id, materializeAttachment);
|
||||
nodeResults.push({ logicalId: qualifiedId, status: "materialized" });
|
||||
persistedEvents.push(event);
|
||||
}
|
||||
@@ -908,6 +912,10 @@ export class PodRigInstantiator {
|
||||
});
|
||||
createdNodeId = node.id;
|
||||
createdEvent = event;
|
||||
// Seam B R2: add_member persists provenance at creation too (rig ref from the DB —
|
||||
// no in-memory rigSpec exists on this path).
|
||||
const addMemberAttachment = this.resolveMemberPolicyAttachment(member.permissionPolicy, this.deps.rigRepo.getRigPermissionPolicy(rigId), rigRoot);
|
||||
if (addMemberAttachment) this.persistNodePolicyProvenance(node.id, addMemberAttachment);
|
||||
if (resolvedEdges.length > 0) {
|
||||
const logicalIdToNodeId = new Map(rig.nodes.map((n) => [n.logicalId, n.id]));
|
||||
logicalIdToNodeId.set(qualifiedId, node.id);
|
||||
@@ -1464,11 +1472,11 @@ export class PodRigInstantiator {
|
||||
* materialized rig). Returns undefined when no ref is attached at either level.
|
||||
*/
|
||||
private resolveMemberPolicyAttachment(
|
||||
member: RigSpecPodMember,
|
||||
rigSpec: PodRigSpec,
|
||||
memberRef: string | undefined,
|
||||
rigRef: string | undefined | null,
|
||||
rigRoot: string,
|
||||
): ResolvedPolicyAttachment | undefined {
|
||||
const ref = resolvePermissionPolicyRefValue(member.permissionPolicy, rigSpec.permissionPolicy);
|
||||
const ref = resolvePermissionPolicyRefValue(memberRef, rigRef);
|
||||
if (!ref) return undefined;
|
||||
return resolvePermissionPolicyAttachment(ref, rigRoot, {
|
||||
readFile: (p) => this.deps.fsOps.readFile(p),
|
||||
@@ -1530,7 +1538,7 @@ export class PodRigInstantiator {
|
||||
// OPR.0.4.8.3 Seam B: resolve the seat's permission-policy attachment once for this
|
||||
// launch (member > rig precedence; declaring dir = the rig root, where the declaring
|
||||
// RigSpec + its relative policy files live). Also persists restart-stable provenance.
|
||||
const policyAttachment = this.resolveMemberPolicyAttachment(input.member, input.rigSpec, input.rigRoot);
|
||||
const policyAttachment = this.resolveMemberPolicyAttachment(input.member.permissionPolicy, input.rigSpec.permissionPolicy, input.rigRoot);
|
||||
if (policyAttachment) this.persistNodePolicyProvenance(input.nodeId, policyAttachment);
|
||||
|
||||
const sessionNameErrors = validateSessionComponents(input.pod.id, input.member.id, input.rigSpec.name);
|
||||
@@ -1768,7 +1776,7 @@ export class PodRigInstantiator {
|
||||
cwdOverride?: string;
|
||||
}): Promise<{ status: "launched" | "failed"; error?: string; sessionName?: string; warnings?: string[] }> {
|
||||
const effectiveCwd = resolveLaunchCwd(input.member.cwd, input.rigRoot, input.cwdOverride);
|
||||
const terminalPolicyAttachment = this.resolveMemberPolicyAttachment(input.member, input.rigSpec, input.rigRoot);
|
||||
const terminalPolicyAttachment = this.resolveMemberPolicyAttachment(input.member.permissionPolicy, input.rigSpec.permissionPolicy, input.rigRoot);
|
||||
if (terminalPolicyAttachment) this.persistNodePolicyProvenance(input.nodeId, terminalPolicyAttachment);
|
||||
const sessionNameErrors = validateSessionComponents(input.pod.id, input.member.id, input.rigSpec.name);
|
||||
if (sessionNameErrors.length > 0) {
|
||||
|
||||
@@ -274,8 +274,10 @@ export class SeatHandoverService {
|
||||
// READY agent (§2.1b seam, B1): createSession -> resolve pane -> real
|
||||
// runtime startup (launchHarness + readiness) -> upsertDiscoveredSession.
|
||||
// The successor is a live agent, not a bare shell, before it can commit.
|
||||
const successorPosture = this.rigRepo.getNodePolicyProvenance(node.id)?.launchPosture;
|
||||
const launch = await this.successorLauncher.createSuccessor({
|
||||
node: { id: node.id, runtime: node.runtime, cwd: node.cwd },
|
||||
// Seam B: the successor is the SAME seat continuing — persisted policy posture carries.
|
||||
node: { id: node.id, runtime: node.runtime, cwd: node.cwd, ...(successorPosture ? { launchPosture: successorPosture } : {}) },
|
||||
departingSessionName: latestSession.session_name,
|
||||
});
|
||||
if (!launch.ok) {
|
||||
|
||||
@@ -38,6 +38,10 @@ export interface SuccessorNode {
|
||||
id: string;
|
||||
runtime: string | null;
|
||||
cwd: string | null;
|
||||
/** OPR.0.4.8.3 Seam B: the departing seat's PERSISTED resolved launch posture —
|
||||
* the successor is a CONTINUITY edge of the same seat, so its policy posture
|
||||
* carries (populated by the caller from node provenance; absent = env decision). */
|
||||
launchPosture?: "floor" | "full_bypass";
|
||||
}
|
||||
|
||||
export type SuccessorLaunchResult =
|
||||
@@ -250,6 +254,8 @@ export class SuccessorSessionLauncher {
|
||||
cmuxSurface: null,
|
||||
updatedAt: "",
|
||||
cwd: cwd ?? "",
|
||||
// Seam B: continuity — the successor launches at the departing seat's posture.
|
||||
...(node.launchPosture ? { launchPosture: node.launchPosture } : {}),
|
||||
};
|
||||
|
||||
let launch: Awaited<ReturnType<RuntimeAdapter["launchHarness"]>>;
|
||||
|
||||
@@ -1165,6 +1165,9 @@ export interface ExpansionPodFragment {
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
codexConfigProfile?: string;
|
||||
/** OPR.0.4.8.3 Seam B: per-seat permission_policy REF — threaded through the
|
||||
* expansion ingress exactly like role (never silently dropped). */
|
||||
permissionPolicy?: string;
|
||||
restorePolicy?: string;
|
||||
label?: string;
|
||||
/**
|
||||
|
||||
@@ -68,6 +68,9 @@ interface ForkSourceShape {
|
||||
profile: string | null;
|
||||
cwd: string | null;
|
||||
codexConfigProfile: string | null;
|
||||
/** OPR.0.4.8.3 Seam B: the source seat's raw permission_policy ref — a forked/imaged
|
||||
* successor preserves the source's policy attachment (preflight surface 2). */
|
||||
permissionPolicy: string | null;
|
||||
}
|
||||
|
||||
function resolveForkSourceNode(db: Database.Database, sourceSession: string): ForkSourceShape | null {
|
||||
@@ -86,6 +89,7 @@ function resolveForkSourceNode(db: Database.Database, sourceSession: string): Fo
|
||||
profile: (row["profile"] as string | null) ?? null,
|
||||
cwd: (row["cwd"] as string | null) ?? null,
|
||||
codexConfigProfile: (row["codex_config_profile"] as string | null) ?? null,
|
||||
permissionPolicy: (row["permission_policy"] as string | null) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -254,6 +258,7 @@ export function agentImagesRoutes(deps: AgentImageRoutesDeps): Hono {
|
||||
...(source.profile ? { profile: source.profile } : {}),
|
||||
...(source.cwd ? { cwd: source.cwd } : {}),
|
||||
...(source.codexConfigProfile ? { codex_config_profile: source.codexConfigProfile } : {}),
|
||||
...(source.permissionPolicy ? { permission_policy: source.permissionPolicy } : {}),
|
||||
session_source: sessionSource,
|
||||
};
|
||||
|
||||
|
||||
@@ -127,6 +127,12 @@ function normalizeExpansionPodFragment(raw: Record<string, unknown>): ExpansionP
|
||||
: undefined,
|
||||
cwd: typeof m["cwd"] === "string" ? m["cwd"] : undefined,
|
||||
model: typeof m["model"] === "string" ? m["model"] : undefined,
|
||||
permissionPolicy:
|
||||
typeof m["permissionPolicy"] === "string"
|
||||
? m["permissionPolicy"]
|
||||
: typeof m["permission_policy"] === "string"
|
||||
? m["permission_policy"]
|
||||
: undefined,
|
||||
restorePolicy:
|
||||
typeof m["restorePolicy"] === "string"
|
||||
? m["restorePolicy"]
|
||||
|
||||
@@ -31,6 +31,9 @@ import { contextUsageSchema } from "../../src/db/migrations/018_context_usage.js
|
||||
import { externalCliAttachmentSchema } from "../../src/db/migrations/019_external_cli_attachment.js";
|
||||
import { rigServicesSchema } from "../../src/db/migrations/020_rig_services.js";
|
||||
import { seatHandoverObservabilitySchema } from "../../src/db/migrations/021_seat_handover_observability.js";
|
||||
import { nodePermissionPolicySchema } from "../../src/db/migrations/055_node_permission_policy.js";
|
||||
import { rigPermissionPolicySchema } from "../../src/db/migrations/056_rig_permission_policy.js";
|
||||
import { nodePolicyProvenanceSchema } from "../../src/db/migrations/057_node_policy_provenance.js";
|
||||
import { nodeCodexConfigProfileSchema } from "../../src/db/migrations/022_node_codex_config_profile.js";
|
||||
import { streamItemsSchema } from "../../src/db/migrations/023_stream_items.js";
|
||||
import { queueItemsSchema } from "../../src/db/migrations/024_queue_items.js";
|
||||
@@ -55,7 +58,7 @@ import { sessionsNodeIdIndexSchema } from "../../src/db/migrations/053_sessions_
|
||||
// index-independent).
|
||||
export function createMigratedDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema, packagesSchema, installJournalSchema, journalSeqSchema, bootstrapSchema, discoverySchema, discoveryFkFix, agentspecRebootSchema, startupContextSchema, chatMessagesSchema, podNamespaceSchema, contextUsageSchema, externalCliAttachmentSchema, rigServicesSchema, seatHandoverObservabilitySchema, nodeCodexConfigProfileSchema, streamItemsSchema, queueItemsSchema, queueTransitionsSchema, rigPolicySchema, rigArchiveSchema, resumeProvenanceSchema, resumeVerificationSchema, seatIdentityVerdictsSchema, eventsNodeTypeIndexSchema, sessionsNodeIdIndexSchema]);
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema, packagesSchema, installJournalSchema, journalSeqSchema, bootstrapSchema, discoverySchema, discoveryFkFix, agentspecRebootSchema, startupContextSchema, chatMessagesSchema, podNamespaceSchema, contextUsageSchema, externalCliAttachmentSchema, rigServicesSchema, seatHandoverObservabilitySchema, nodeCodexConfigProfileSchema, nodePermissionPolicySchema, rigPermissionPolicySchema, nodePolicyProvenanceSchema, streamItemsSchema, queueItemsSchema, queueTransitionsSchema, rigPolicySchema, rigArchiveSchema, resumeProvenanceSchema, resumeVerificationSchema, seatIdentityVerdictsSchema, eventsNodeTypeIndexSchema, sessionsNodeIdIndexSchema]);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import { contextUsageSchema } from "../../src/db/migrations/018_context_usage.js
|
||||
import { externalCliAttachmentSchema } from "../../src/db/migrations/019_external_cli_attachment.js";
|
||||
import { rigServicesSchema } from "../../src/db/migrations/020_rig_services.js";
|
||||
import { seatHandoverObservabilitySchema } from "../../src/db/migrations/021_seat_handover_observability.js";
|
||||
import { nodePermissionPolicySchema } from "../../src/db/migrations/055_node_permission_policy.js";
|
||||
import { rigPermissionPolicySchema } from "../../src/db/migrations/056_rig_permission_policy.js";
|
||||
import { nodePolicyProvenanceSchema } from "../../src/db/migrations/057_node_policy_provenance.js";
|
||||
import { nodeCodexConfigProfileSchema } from "../../src/db/migrations/022_node_codex_config_profile.js";
|
||||
// PL-019: GET /api/rigs/:id/graph + GET /api/rigs/:rigId/nodes/:logicalId
|
||||
// now perform a read-side join over queue_items (in-progress qitem
|
||||
@@ -88,9 +91,13 @@ import { AgentActivityStore } from "../../src/domain/agent-activity-store.js";
|
||||
import { createApp } from "../../src/server.js";
|
||||
import fs from "node:fs";
|
||||
|
||||
/** Seam B R6: the canonical full-fixture migration list, exported so file-backed
|
||||
* DB-reopen tests migrate IDENTICALLY to createFullTestDb. */
|
||||
export const migrationsForFullTestDb = [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema, packagesSchema, installJournalSchema, journalSeqSchema, bootstrapSchema, discoverySchema, discoveryFkFix, agentspecRebootSchema, startupContextSchema, chatMessagesSchema, podNamespaceSchema, contextUsageSchema, externalCliAttachmentSchema, rigServicesSchema, seatHandoverObservabilitySchema, nodeCodexConfigProfileSchema, nodePermissionPolicySchema, rigPermissionPolicySchema, nodePolicyProvenanceSchema, streamItemsSchema, queueItemsSchema, queueTransitionsSchema, rigPolicySchema, rigArchiveSchema, resumeProvenanceSchema, resumeVerificationSchema, seatIdentityVerdictsSchema];
|
||||
|
||||
export function createFullTestDb(): Database.Database {
|
||||
const db = createDb();
|
||||
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema, packagesSchema, installJournalSchema, journalSeqSchema, bootstrapSchema, discoverySchema, discoveryFkFix, agentspecRebootSchema, startupContextSchema, chatMessagesSchema, podNamespaceSchema, contextUsageSchema, externalCliAttachmentSchema, rigServicesSchema, seatHandoverObservabilitySchema, nodeCodexConfigProfileSchema, streamItemsSchema, queueItemsSchema, queueTransitionsSchema, rigPolicySchema, rigArchiveSchema, resumeProvenanceSchema, resumeVerificationSchema, seatIdentityVerdictsSchema]);
|
||||
migrate(db, migrationsForFullTestDb);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// OPR.0.4.8.3 Seam B (R6) — full-thread discrimination pins: the permission_policy REF +
|
||||
// resolved posture survive materialize → columns → export → DB-REOPEN, with member-over-rig
|
||||
// precedence and the dev-guard restart ruling (a custom surface:flag policy restores to
|
||||
// full_bypass) proven against a REOPENED database handle. Every pin here discriminates
|
||||
// against the pre-Seam-B tip (none of the columns/threading exist at 80336ff0/4694e86d).
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import type Database from "better-sqlite3";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { createFullTestDb, createTestApp, migrationsForFullTestDb } from "./helpers/test-app.js";
|
||||
import { createDb } from "../src/db/connection.js";
|
||||
import { migrate } from "../src/db/migrate.js";
|
||||
import { RigRepository } from "../src/domain/rig-repository.js";
|
||||
import { resolvePermissionPolicyAttachment } from "../src/domain/permission-policy/policy-ref.js";
|
||||
import { claudePostureFlag, codexPostureArg, piTrust } from "../src/adapters/yolo-mode.js";
|
||||
|
||||
const RIG_ROOT = "/project/rigs/policy-rig";
|
||||
const CUSTOM_POLICY = `---
|
||||
name: operator-full
|
||||
version: "1"
|
||||
description: full-bypass flag policy for the plumb pins
|
||||
surface: flag
|
||||
launch_posture: full_bypass
|
||||
allowed_actions: []
|
||||
ask_actions: []
|
||||
denied_actions: []
|
||||
watch_actions: []
|
||||
---
|
||||
# Operator full
|
||||
`;
|
||||
|
||||
function agentYaml(name: string): string {
|
||||
return `name: ${name}\nversion: "1.0.0"\nresources:\n skills: []\nprofiles:\n default:\n uses:\n skills: []`;
|
||||
}
|
||||
|
||||
function rawMember(id: string, over: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return { id, agent_ref: "local:agents/impl", profile: "default", runtime: "claude-code", cwd: ".", ...over };
|
||||
}
|
||||
|
||||
function rawSpec(members: Record<string, unknown>[], over: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
version: "0.2",
|
||||
name: "policy-rig",
|
||||
pods: [{ id: "dev", label: "Dev", members, edges: [] }],
|
||||
edges: [],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function fsOps() {
|
||||
return {
|
||||
readFile: (p: string) => {
|
||||
if (p.includes("agents/impl")) return agentYaml("impl");
|
||||
if (p.includes("policies/operator-full.md")) return CUSTOM_POLICY;
|
||||
throw new Error(`Not found: ${p}`);
|
||||
},
|
||||
exists: (p: string) => p.includes("agents/impl") || p.includes("policies/operator-full.md"),
|
||||
};
|
||||
}
|
||||
|
||||
describe("Seam B R6 — full-thread plumb (materialize → columns → export)", () => {
|
||||
let db: Database.Database;
|
||||
let setup: ReturnType<typeof createTestApp>;
|
||||
|
||||
beforeEach(() => {
|
||||
db = createFullTestDb();
|
||||
setup = createTestApp(db, { podInstantiatorFsOps: fsOps() });
|
||||
});
|
||||
afterEach(() => { db.close(); });
|
||||
|
||||
it("rig-level + member refs land on their rows; member OVERRIDES rig at resolution (precedence)", async () => {
|
||||
const spec = rawSpec(
|
||||
[rawMember("impl", { permission_policy: "builtin:locked" }), rawMember("helper")],
|
||||
{ permission_policy: "builtin:yolo" },
|
||||
);
|
||||
const outcome = await setup.podInstantiator.materializeStructured(spec, RIG_ROOT);
|
||||
expect(outcome.ok).toBe(true);
|
||||
const rigId = (outcome as { ok: true; result: { rigId: string } }).result.rigId;
|
||||
|
||||
// rig row carries the rig ref
|
||||
expect(setup.rigRepo.getRigPermissionPolicy(rigId)).toBe("builtin:yolo");
|
||||
// member's OWN ref on its node column; ref-less pod-mate NULL (rig ref lives on the rig)
|
||||
const refOf = (lid: string) => (db.prepare("SELECT permission_policy FROM nodes WHERE logical_id = ?").get(lid) as { permission_policy: string | null }).permission_policy;
|
||||
expect(refOf("dev.impl")).toBe("builtin:locked");
|
||||
expect(refOf("dev.helper")).toBeNull();
|
||||
|
||||
// provenance: member ref WINS over rig ref → impl floor (locked), helper full_bypass (rig yolo)
|
||||
const nodeId = (lid: string) => (db.prepare("SELECT id FROM nodes WHERE logical_id = ?").get(lid) as { id: string }).id;
|
||||
expect(setup.rigRepo.getNodePolicyProvenance(nodeId("dev.impl"))).toMatchObject({ origin: "builtin", launchPosture: "floor", resolvedTarget: null });
|
||||
expect(setup.rigRepo.getNodePolicyProvenance(nodeId("dev.helper"))).toMatchObject({ origin: "builtin", launchPosture: "full_bypass", resolvedTarget: null });
|
||||
});
|
||||
|
||||
it("export round-trips BOTH levels: member ref on the pod member, rig ref at the top level", async () => {
|
||||
const spec = rawSpec(
|
||||
[rawMember("impl", { permission_policy: "policies/operator-full.md" })],
|
||||
{ permission_policy: "builtin:standard" },
|
||||
);
|
||||
const outcome = await setup.podInstantiator.materializeStructured(spec, RIG_ROOT);
|
||||
expect(outcome.ok).toBe(true);
|
||||
const rigId = (outcome as { ok: true; result: { rigId: string } }).result.rigId;
|
||||
|
||||
const exported = setup.rigSpecExporter.exportRig(rigId) as import("../src/domain/types.js").RigSpec;
|
||||
expect(exported.permissionPolicy).toBe("builtin:standard");
|
||||
expect(exported.pods[0]!.members[0]!.permissionPolicy).toBe("policies/operator-full.md");
|
||||
});
|
||||
|
||||
it("a CUSTOM surface:flag policy resolves to full_bypass at materialize with restart-stable provenance", async () => {
|
||||
const spec = rawSpec([rawMember("impl", { permission_policy: "policies/operator-full.md" })]);
|
||||
const outcome = await setup.podInstantiator.materializeStructured(spec, RIG_ROOT);
|
||||
expect(outcome.ok).toBe(true);
|
||||
const nodeId = (db.prepare("SELECT id FROM nodes WHERE logical_id = 'dev.impl'").get() as { id: string }).id;
|
||||
const prov = setup.rigRepo.getNodePolicyProvenance(nodeId);
|
||||
expect(prov).toMatchObject({
|
||||
origin: "custom",
|
||||
launchPosture: "full_bypass", // the guard ruling's crux: custom flag CAN be full_bypass
|
||||
declaringDir: RIG_ROOT,
|
||||
resolvedTarget: `${RIG_ROOT}/policies/operator-full.md`,
|
||||
nodeRef: "policies/operator-full.md",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Seam B R6 — DB-REOPEN restart proof (dev-guard ruling)", () => {
|
||||
let dir: string;
|
||||
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), "seam-b-reopen-")); });
|
||||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||||
|
||||
it("a custom surface:flag=full_bypass attachment restores to full_bypass from a REOPENED database", async () => {
|
||||
const dbFile = join(dir, "daemon.sqlite");
|
||||
const db1 = createDb(dbFile);
|
||||
// identical canonical migration set as createFullTestDb, against the FILE-backED db
|
||||
migrate(db1, migrationsForFullTestDb);
|
||||
const setup1 = createTestApp(db1, { podInstantiatorFsOps: fsOps() });
|
||||
const outcome = await setup1.podInstantiator.materializeStructured(
|
||||
rawSpec([rawMember("impl", { permission_policy: "policies/operator-full.md" })]),
|
||||
RIG_ROOT,
|
||||
);
|
||||
expect(outcome.ok).toBe(true);
|
||||
const nodeId = (db1.prepare("SELECT id FROM nodes WHERE logical_id = 'dev.impl'").get() as { id: string }).id;
|
||||
db1.close(); // ── daemon restart boundary ──
|
||||
|
||||
const db2 = createDb(dbFile); // REOPEN: no in-memory RigSpec exists anymore
|
||||
const repo2 = new RigRepository(db2);
|
||||
const prov = repo2.getNodePolicyProvenance(nodeId);
|
||||
expect(prov).not.toBeNull();
|
||||
expect(prov!.launchPosture).toBe("full_bypass"); // persisted posture is restart-stable
|
||||
// AND the ruling's re-derivation: reopen + re-validate the policy from provenance alone
|
||||
const rederived = resolvePermissionPolicyAttachment(prov!.nodeRef!, prov!.declaringDir!, {
|
||||
readFile: (p) => { expect(p).toBe(`${RIG_ROOT}/policies/operator-full.md`); return CUSTOM_POLICY; },
|
||||
});
|
||||
expect(rederived.launchPosture).toBe("full_bypass");
|
||||
expect(rederived.surface).toBe("flag");
|
||||
db2.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Seam B R6 — resolved posture drives the REAL launch helpers on all three harnesses", () => {
|
||||
const yoloOn = { OPENRIG_YOLO: "1" } as NodeJS.ProcessEnv;
|
||||
const yoloOff = {} as NodeJS.ProcessEnv;
|
||||
|
||||
it("full_bypass posture lifts a seat even with YOLO OFF (custom flag policy — the ruling)", () => {
|
||||
expect(claudePostureFlag(yoloOff, "full_bypass")).toBe("--dangerously-skip-permissions");
|
||||
expect(codexPostureArg("", yoloOff, "full_bypass")).toBe(" -s danger-full-access");
|
||||
expect(piTrust(undefined, yoloOff, "full_bypass")).toBe("approve"); // Pi = resource trust, not a permission policy
|
||||
});
|
||||
|
||||
it("floor posture HOLDS a seat at the floor even with global YOLO ON (attached policy is authoritative)", () => {
|
||||
expect(claudePostureFlag(yoloOn, "floor")).toBe("--permission-mode acceptEdits");
|
||||
expect(codexPostureArg(" -p prof", yoloOn, "floor")).toBe(" -p prof");
|
||||
expect(piTrust("no-approve", yoloOn, "floor")).toBe("no-approve");
|
||||
});
|
||||
|
||||
it("ABSENT posture preserves the 0.4.8.2 env behavior byte-for-byte (no policy attached)", () => {
|
||||
expect(claudePostureFlag(yoloOn)).toBe("--dangerously-skip-permissions");
|
||||
expect(claudePostureFlag(yoloOff)).toBe("--permission-mode acceptEdits");
|
||||
expect(codexPostureArg("", yoloOff)).toBe(" -s workspace-write");
|
||||
expect(piTrust(undefined, yoloOff)).toBe("no-approve");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user