feat(policy): surface lifecycle discovery

This commit is contained in:
v-openrig-build committed 2026-08-05 02:58:11 +00:00
1 parent 01cfe05f9c
commit e9b258914f
4 files changed
+243 -30

No files matched your search

+1 -22
View File
@@ -11,8 +11,7 @@
"workspaces": [
"packages/daemon",
"packages/ui",
"packages/cli",
"packages/tui"
"packages/cli"
],
"engines": {
"node": "^20 || ^22 || ^24"
@@ -1167,10 +1166,6 @@
"resolved": "packages/daemon",
"link": true
},
"node_modules/@openrig/tui": {
"resolved": "packages/tui",
"link": true
},
"node_modules/@openrig/ui": {
"resolved": "packages/ui",
"link": true
@@ -7473,7 +7468,6 @@
"zod": "^4.3.6"
},
"bin": {
"openrig-tui": "tui/dist/main.js",
"rig": "dist/bin-wrapper.js"
},
"devDependencies": {
@@ -7510,21 +7504,6 @@
"node": "^20 || ^22 || ^24"
}
},
"packages/tui": {
"name": "@openrig/tui",
"version": "0.1.0",
"dependencies": {
"yaml": "^2.8.3"
},
"bin": {
"openrig-tui": "dist/main.js"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
}
},
"packages/ui": {
"name": "@openrig/ui",
"version": "0.4.7",
@@ -287,7 +287,7 @@ export class RigInstantiator {
import { RigSpecCodec as PodRigSpecCodec } from "./rigspec-codec.js";
import { RigSpecSchema as PodRigSpecSchema, VALID_EDGE_KINDS } from "./rigspec-schema.js";
import { rigPreflight, preflightValidatedSpec } from "./rigspec-preflight.js";
import { permissionPolicyDiscoveryWarnings, rigPreflight, preflightValidatedSpec } from "./rigspec-preflight.js";
import { resolveAgentRef, type AgentResolverFsOps } from "./agent-resolver.js";
import { resolveNodeConfig } from "./profile-resolver.js";
import { resolveStartup } from "./startup-resolver.js";
@@ -343,7 +343,8 @@ export interface MaterializeResult {
specName: string;
specVersion: string;
nodes: Array<{ logicalId: string; status: "materialized" }>;
/** NONFATAL READY-path preflight warnings (e.g. managed activity-hook delivery gaps). */
/** NONFATAL READY-path warnings: managed activity-hook delivery gaps (main's floor) FOLLOWED BY
* permission-policy lifecycle-discovery warnings (§6 reconciliation — activity-hook-first). */
warnings?: string[];
}
@@ -685,7 +686,12 @@ export class PodRigInstantiator {
specName: rigSpec.name,
specVersion: rigSpec.version,
nodes: nodeResults,
warnings: carryWarnings && carryWarnings.length > 0 ? carryWarnings : undefined,
// §6 reconciliation (activity-hook-first, policy-appended): both proven warning sources ride;
// undefined-when-empty preserved from main's floor.
warnings: ((): string[] | undefined => {
const merged = [...(carryWarnings ?? []), ...permissionPolicyDiscoveryWarnings(rigSpec, { rigRoot, fsOps: this.deps.fsOps })];
return merged.length > 0 ? merged : undefined;
})(),
},
};
} catch (err) {
@@ -1084,10 +1090,10 @@ export class PodRigInstantiator {
const nodeResults: { logicalId: string; status: "launched" | "failed" | "attention_required"; error?: string; evidence?: string; sessionName?: string }[] = [];
const nodeIdMap: Record<string, string> = {}; // "pod.member" -> node DB id
const launchedSessionNames: string[] = []; // Track for orphan cleanup on total failure
// Seed with the READY-path preflight warnings (nonfatal — e.g. managed activity-hook
// delivery gaps) so they ride along in the successful instantiate result alongside
// launch/materialization warnings; rig up stays rc0.
const podInstantiateWarnings: string[] = [...preflight.warnings];
// §6 reconciliation (activity-hook-first, policy-appended): seed with the READY-path preflight
// warnings (nonfatal — managed activity-hook delivery gaps, main's floor) FIRST, then append the
// permission-policy lifecycle-discovery warnings; both proven sources ride, rig up stays rc0.
const podInstantiateWarnings: string[] = [...preflight.warnings, ...permissionPolicyDiscoveryWarnings(rigSpec, { rigRoot, fsOps: this.deps.fsOps })];
// Store per-member context for deferred launch
const memberContext = new Map<string, { pod: typeof rigSpec.pods[0]; member: typeof rigSpec.pods[0]["members"][0]; podId: string; nodeId: string; resolveResult: any; configResult: any }>();
@@ -124,6 +124,10 @@ import { resolveNodeConfig, type ResolutionContext } from "./profile-resolver.js
import { getOpenRigInstallCwdError, resolveLaunchCwd } from "./cwd-resolution.js";
import nodePath from "node:path";
import { validateClaudeActivityHookDelivery, CLAUDE_ACTIVITY_HOOKS_RESOURCE_TYPE } from "./claude-activity-hooks.js";
import {
resolvePermissionPolicyAttachment,
resolvePermissionPolicyRefValue,
} from "./permission-policy/policy-ref.js";
const SUPPORTED_RUNTIMES = new Set(["claude-code", "codex", "pi", "terminal"]);
@@ -162,6 +166,33 @@ export interface PreflightSpecContext {
claudeActivityAssets?: { relayPath?: string; manifestPath?: string };
}
/**
* Seam C: describe each seat's effective permission-policy attachment without
* changing readiness or state. The existing Seam-B resolver remains the one
* source of truth for member-over-rig precedence, origin, and launch posture.
*/
export function permissionPolicyDiscoveryWarnings(
rigSpec: PodRigSpec,
preflightCtx: Pick<PreflightSpecContext, "rigRoot" | "fsOps">,
): string[] {
const warnings: string[] = [];
for (const pod of rigSpec.pods) {
for (const member of pod.members) {
const logicalId = `${pod.id}.${member.id}`;
const ref = resolvePermissionPolicyRefValue(member.permissionPolicy, rigSpec.permissionPolicy);
if (!ref) {
warnings.push(`${logicalId}: permission_policy absent; launch_posture=floor`);
continue;
}
const attachment = resolvePermissionPolicyAttachment(ref, preflightCtx.rigRoot, {
readFile: (path) => preflightCtx.fsOps.readFile(path),
});
warnings.push(`${logicalId}: permission_policy ref="${attachment.ref}" origin=${attachment.origin} launch_posture=${attachment.launchPosture}`);
}
}
return warnings;
}
/**
* Rebooted rig preflight: validates rig spec, resolves all agent refs + profiles,
* checks runtimes and cwd. Pure domain, no side effects beyond filesystem reads.
@@ -205,7 +236,7 @@ export async function rigPreflight(input: RigPreflightInput & { exec?: (cmd: str
*/
export async function preflightValidatedSpec(rigSpec: PodRigSpec, preflightCtx: PreflightSpecContext): Promise<PreflightResult> {
const errors: string[] = [];
const warnings: string[] = [];
const warnings = permissionPolicyDiscoveryWarnings(rigSpec, preflightCtx);
// 2. Validate session name components for all pod members
const effectiveRigName = preflightCtx.rigNameOverride ?? rigSpec.name;
@@ -0,0 +1,197 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { rigPreflight } from "../src/domain/rigspec-preflight.js";
import type { AgentResolverFsOps } from "../src/domain/agent-resolver.js";
import type { NodeBinding, RuntimeAdapter } from "../src/domain/runtime-adapter.js";
import { createFullTestDb, createTestApp } from "./helpers/test-app.js";
import { SystemPreflight } from "../../cli/src/system-preflight.js";
const RIG_ROOT = "/project/rigs/policy-discovery";
const AGENT_YAML = `name: impl
version: "1.0.0"
resources:
skills: []
profiles:
default:
uses:
skills: []
`;
const CUSTOM_POLICY = `---
policy_schema_version: 1
name: operator-full
source: custom
description: full-bypass flag policy
surface: flag
launch_posture: full_bypass
---
# Operator full
`;
function fsOps(): AgentResolverFsOps {
return {
exists: (path) => path.includes("agents/impl") || path.endsWith("policies/operator-full.md"),
readFile: (path) => {
if (path.includes("agents/impl")) return AGENT_YAML;
if (path.endsWith("policies/operator-full.md")) return CUSTOM_POLICY;
throw new Error(`Not found: ${path}`);
},
};
}
function rigYaml(opts: { name?: string; rigPolicy?: string; memberPolicy?: string } = {}): string {
const rigPolicy = opts.rigPolicy ? `permission_policy: ${opts.rigPolicy}\n` : "";
const memberPolicy = opts.memberPolicy ? ` permission_policy: ${opts.memberPolicy}\n` : "";
return `version: "0.2"
name: ${opts.name ?? "policy-discovery"}
${rigPolicy}pods:
- id: dev
label: Dev
members:
- id: impl
runtime: claude-code
agent_ref: local:agents/impl
profile: default
${memberPolicy} cwd: .
edges: []
edges: []
`;
}
const policyLines = (warnings: string[]) => warnings.filter((warning) => warning.includes("permission_policy"));
describe("Seam C permission-policy discovery", () => {
const tempDirs: string[] = [];
afterEach(() => {
vi.restoreAllMocks();
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
it("surfaces an attached builtin ref with honest origin and bound posture", async () => {
const result = await rigPreflight({
rigSpecYaml: rigYaml({ rigPolicy: "builtin:yolo" }),
rigRoot: RIG_ROOT,
fsOps: fsOps(),
});
expect(result.ready).toBe(true);
expect(result.errors).toEqual([]);
expect(policyLines(result.warnings)).toEqual([
'dev.impl: permission_policy ref="builtin:yolo" origin=builtin launch_posture=full_bypass',
]);
});
it("surfaces the per-member custom override instead of the rig-level policy", async () => {
const result = await rigPreflight({
rigSpecYaml: rigYaml({ rigPolicy: "builtin:standard", memberPolicy: "policies/operator-full.md" }),
rigRoot: RIG_ROOT,
fsOps: fsOps(),
});
expect(result.ready).toBe(true);
expect(policyLines(result.warnings)).toEqual([
'dev.impl: permission_policy ref="policies/operator-full.md" origin=custom launch_posture=full_bypass',
]);
expect(result.warnings.join("\n")).not.toContain("builtin:standard");
});
it("surfaces honest absence as the floor without errors or blocking", async () => {
const result = await rigPreflight({ rigSpecYaml: rigYaml(), rigRoot: RIG_ROOT, fsOps: fsOps() });
expect(result.ready).toBe(true);
expect(result.errors).toEqual([]);
expect(policyLines(result.warnings)).toEqual([
"dev.impl: permission_policy absent; launch_posture=floor",
]);
});
it("keeps launch outcomes identical while carrying discovery on the rig-up result", 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 RuntimeAdapter;
const db = createFullTestDb();
try {
const setup = createTestApp(db, {
adapters: { "claude-code": adapter },
podInstantiatorFsOps: fsOps(),
});
const absent = await setup.podInstantiator.instantiate(rigYaml({ name: "policy-absent" }), RIG_ROOT);
const attached = await setup.podInstantiator.instantiate(
rigYaml({ name: "policy-attached", rigPolicy: "builtin:locked" }),
RIG_ROOT,
);
expect(absent.ok).toBe(true);
expect(attached.ok).toBe(true);
if (!absent.ok || !attached.ok) return;
expect(absent.result.nodes.map(({ logicalId, status }) => ({ logicalId, status })))
.toEqual(attached.result.nodes.map(({ logicalId, status }) => ({ logicalId, status })));
expect(bindings).toHaveLength(2);
expect(policyLines(absent.result.warnings ?? [])).toEqual([
"dev.impl: permission_policy absent; launch_posture=floor",
]);
expect(policyLines(attached.result.warnings ?? [])).toEqual([
'dev.impl: permission_policy ref="builtin:locked" origin=builtin launch_posture=floor',
]);
} finally {
db.close();
}
});
it("keeps SYSTEM setup preflight policy-blind while rig lifecycle preflight discovers policy", async () => {
const home = mkdtempSync(join(tmpdir(), "seam-c-system-preflight-"));
tempDirs.push(home);
const system = new SystemPreflight({
exec: async (cmd) => cmd === "tmux -V" ? "tmux 3.6" : "",
configStore: {
resolve: () => ({
daemon: { host: "127.0.0.1", port: 0 },
db: { path: join(home, "openrig.db") },
transcripts: { path: join(home, "transcripts") },
}),
} as never,
getDaemonStatus: async () => ({ state: "stopped" }),
openrigHome: home,
});
const systemResult = await system.run({ port: 0 });
expect(JSON.stringify(systemResult)).not.toContain("permission_policy");
const lifecycleResult = await rigPreflight({
rigSpecYaml: rigYaml({ rigPolicy: "builtin:standard" }),
rigRoot: RIG_ROOT,
fsOps: fsOps(),
});
expect(policyLines(lifecycleResult.warnings)).toHaveLength(1);
});
it("rejects an invalid ref upstream and never misreports it as floor discovery", async () => {
const invalid = await rigPreflight({
rigSpecYaml: rigYaml({ rigPolicy: "builtin:missing" }),
rigRoot: RIG_ROOT,
fsOps: fsOps(),
});
expect(invalid.ready).toBe(false);
expect(invalid.errors.join("\n")).toContain("unknown built-in policy 'missing'");
expect(policyLines(invalid.warnings)).toEqual([]);
const valid = await rigPreflight({
rigSpecYaml: rigYaml({ rigPolicy: "builtin:standard" }),
rigRoot: RIG_ROOT,
fsOps: fsOps(),
});
expect(valid.ready).toBe(true);
expect(policyLines(valid.warnings)).toEqual([
'dev.impl: permission_policy ref="builtin:standard" origin=builtin launch_posture=floor',
]);
});
});