mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
feat(slice-05): /install audit-write integration (Item 4 close-out) (OPR.0.3.2.5)
Closes Item 4. /install handler now writes a BundleAuditRecord at each apply-mode completion path (completed, partial, failed, caught error). Plan mode does NOT write (planning doesn't change state). The history endpoint (GET /api/bundles/history) now surfaces real records in production usage. Audit record composition: - installedAt: new Date().toISOString() at write time - bundlePath: from request body - outcome: "success" / "failed" / "partial" mapped from bootstrap status - targetRigId: from bootstrap result.rigId when present - targetRigName: from installMeta.rigName (extracted from bundle's rig.yaml) - daemonVersion: from getDaemonVersion() at call time - cliVersion: from clientCliVersion (request body field) - sourceHost: from installMeta.bundleManifest provenance, if present - archiveHash: not included this commit (operator correlates via bundlePath; can be added in a follow-on if needed) Files touched: - packages/daemon/src/routes/bundles.ts: extend BundleAuditReader import to also import BundleAuditWriter + BundleAuditRecord; new writeInstallAudit helper that composes a record + appends via BundleAuditWriter. Best-effort: helper has internal try/catch so audit-write failures never fail the install response (the install has already succeeded or failed; the audit is a side-channel record). 4 call sites added at each apply-mode completion branch - packages/daemon/test/bundle-routes.test.ts: 2 new tests — plan mode does NOT write an audit record (history total stays 0); apply mode writes a record visible via /history with required shape (installedAt, bundlePath, outcome in success/failed/partial, daemonVersion). Test isolation via OPENRIG_HOME env override to per-test tmpDir Diff: 2 files, +134 / -1 lines. Verified: - 42/42 daemon (was 40; +2 new audit-write integration tests) - 32/32 bundle-routes; 10/10 bundle-audit - CLI unchanged (no CLI files touched this commit) - Discriminator confirmed: temporarily renaming the writeInstallAudit function definition to writeInstallAudit_BROKEN causes 2/32 bundle-routes tests to fail (the two new audit-write tests); restoring returns 32/32. (Earlier weak-discriminator attempt only flipped 2 of 4 call sites — the catch branch still wrote a "failed" outcome which the test accepted; the function-rename discriminator is stronger because it removes the symbol entirely.) - scope-and-versioning sections 1+3 coherence grep clean - audit-every-layer: zero new module-level constants; writeInstallAudit + auditFsOps + bundleAuditPath all call-time reads Pre-commit discipline applied (banked): git diff --cached --stat verified +134/-1 across 2 intended files BEFORE commit. PRD bounce conditions verified: - No schema bump - No new SQLite migrations (JSONL only — matches PRD Item 4) - No event types (audit failures stay side-channel; eventBus surface is a future enhancement explicitly out of this commit) - No core-routing or bootstrap-orchestrator mutation (the bootstrap delegation is unchanged; audit-write happens AFTER bootstrap returns) - No sidebar entries touched Item 4 closes here. Item 4 sub-chain on this branch: -0af652dc(Item 4 part 1: audit module, CLEAR) -b0ae54ad(Item 4 part 2: history route + CLI, CLEAR) - THIS COMMIT — Item 4 part 3: /install audit-write integration Slice-05 progress: Items 1+2+3+4 all complete. Remaining: Items 5 (inspector UI + Files browser composition) + 6 (cross-primitive bundling + gstack HYBRID proof case per orch-ratified decision doc). Then adversarial QA + orch heavy-verify + LOCAL --no-ff merge. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
b0ae54ad57
commit
63ee7dd793
2 files changed
+134
-1
No files matched your search
@@ -19,7 +19,7 @@ import type { PodBundleManifest, BundleProvenance, BundleCompatibility } from ".
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { detectBundleConflicts, type BundleConflict } from "../domain/bundle-conflict-detector.js";
|
||||
import type { RigRepository } from "../domain/rig-repository.js";
|
||||
import { BundleAuditReader, type BundleAuditFsOps } from "../domain/bundle-audit.js";
|
||||
import { BundleAuditReader, BundleAuditWriter, type BundleAuditFsOps, type BundleAuditRecord } from "../domain/bundle-audit.js";
|
||||
import { getDefaultOpenRigPath } from "../openrig-compat.js";
|
||||
|
||||
/**
|
||||
@@ -295,6 +295,44 @@ function bundleAuditPath(): string {
|
||||
return getDefaultOpenRigPath("bundle-audit.jsonl");
|
||||
}
|
||||
|
||||
/**
|
||||
* Item 4 / slice-05 Checkpoint 5.3: append a bundle install audit record.
|
||||
* Best-effort — audit failures are logged via the eventBus shape but never
|
||||
* fail the install response (the install already happened or failed; the
|
||||
* audit is a side-channel record). bundleManifest is optional; when present,
|
||||
* provenance.source_host is mirrored into the record.
|
||||
*/
|
||||
function writeInstallAudit(opts: {
|
||||
bundlePath: string;
|
||||
outcome: "success" | "failed" | "partial";
|
||||
targetRigId?: string;
|
||||
targetRigName?: string;
|
||||
cliVersion?: string;
|
||||
bundleManifest?: Record<string, unknown>;
|
||||
}): void {
|
||||
try {
|
||||
const writer = new BundleAuditWriter({
|
||||
opts: { auditPath: bundleAuditPath() },
|
||||
fsOps: auditFsOps(),
|
||||
});
|
||||
const provenance = normalizeProvenanceBlock(opts.bundleManifest?.["provenance"]);
|
||||
const record: BundleAuditRecord = {
|
||||
installedAt: new Date().toISOString(),
|
||||
bundlePath: opts.bundlePath,
|
||||
outcome: opts.outcome,
|
||||
};
|
||||
if (opts.targetRigId) record.targetRigId = opts.targetRigId;
|
||||
if (opts.targetRigName) record.targetRigName = opts.targetRigName;
|
||||
if (opts.cliVersion) record.cliVersion = opts.cliVersion;
|
||||
record.daemonVersion = getDaemonVersion();
|
||||
if (provenance?.sourceHost) record.sourceHost = provenance.sourceHost;
|
||||
writer.append(record);
|
||||
} catch {
|
||||
// Audit-write failure is side-channel; never fail the install response.
|
||||
// Future enhancement: surface via eventBus (out of scope this commit).
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/bundles/create
|
||||
bundleRoutes.post("/create", async (c) => {
|
||||
const { eventBus } = getDeps(c);
|
||||
@@ -657,20 +695,40 @@ bundleRoutes.post("/install", async (c) => {
|
||||
|
||||
if (result.status === "completed") {
|
||||
eventBus.emit({ type: "bootstrap.completed", runId: result.runId, rigId: result.rigId!, sourceRef: bundlePath });
|
||||
writeInstallAudit({
|
||||
bundlePath, outcome: "success", targetRigId: result.rigId,
|
||||
targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
|
||||
bundleManifest: installMeta?.bundleManifest,
|
||||
});
|
||||
return c.json(result, 201);
|
||||
}
|
||||
if (result.status === "partial") {
|
||||
const ok = result.stages.filter((s: { status: string }) => s.status === "ok").length;
|
||||
const fail = result.stages.filter((s: { status: string }) => s.status === "failed" || s.status === "blocked").length;
|
||||
eventBus.emit({ type: "bootstrap.partial", runId: result.runId, sourceRef: bundlePath, rigId: result.rigId, completed: ok, failed: fail });
|
||||
writeInstallAudit({
|
||||
bundlePath, outcome: "partial", targetRigId: result.rigId,
|
||||
targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
|
||||
bundleManifest: installMeta?.bundleManifest,
|
||||
});
|
||||
return c.json(result, 200);
|
||||
}
|
||||
eventBus.emit({ type: "bootstrap.failed", runId: result.runId, sourceRef: bundlePath, error: result.errors[0] ?? "failed" });
|
||||
writeInstallAudit({
|
||||
bundlePath, outcome: "failed",
|
||||
targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
|
||||
bundleManifest: installMeta?.bundleManifest,
|
||||
});
|
||||
const hasBlocked = result.stages.some((s: { status: string }) => s.status === "blocked");
|
||||
return c.json(result, hasBlocked ? 409 : 500);
|
||||
} catch (err) {
|
||||
bootstrapRepo.updateRunStatus(run.id, "failed");
|
||||
eventBus.emit({ type: "bootstrap.failed", runId: run.id, sourceRef: bundlePath, error: (err as Error).message });
|
||||
writeInstallAudit({
|
||||
bundlePath, outcome: "failed",
|
||||
targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
|
||||
bundleManifest: installMeta?.bundleManifest,
|
||||
});
|
||||
return c.json({ runId: run.id, status: "failed", error: (err as Error).message }, 500);
|
||||
}
|
||||
} finally { bootstrapOrchestrator.release(bundlePath); }
|
||||
|
||||
@@ -1036,6 +1036,81 @@ describe("Bundle API routes", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Item 4 / slice-05 Checkpoint 5.3: /install writes audit record on apply
|
||||
// completion paths; plan mode does NOT write (planning doesn't change
|
||||
// state). End-to-end: install -> GET /api/bundles/history reflects the
|
||||
// record. Test isolation via OPENRIG_HOME env override to per-test tmpDir.
|
||||
it("POST /api/bundles/install plan mode does NOT write an audit record (planning doesn't change state)", async () => {
|
||||
const origHome = process.env.OPENRIG_HOME;
|
||||
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-audit-plan-"));
|
||||
process.env.OPENRIG_HOME = auditHome;
|
||||
try {
|
||||
const { specPath } = seedPackage();
|
||||
const bundlePath = path.join(tmpDir, "plan-mode.rigbundle");
|
||||
await app.request("/api/bundles/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ specPath, bundleName: "plan-mode", bundleVersion: "0.1.0", outputPath: bundlePath }),
|
||||
});
|
||||
|
||||
await app.request("/api/bundles/install", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ bundlePath, plan: true }),
|
||||
});
|
||||
|
||||
const history = await app.request("/api/bundles/history");
|
||||
const body = await history.json();
|
||||
expect(body.total).toBe(0);
|
||||
} finally {
|
||||
if (origHome === undefined) delete process.env.OPENRIG_HOME;
|
||||
else process.env.OPENRIG_HOME = origHome;
|
||||
fs.rmSync(auditHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("POST /api/bundles/install apply mode writes an audit record visible via /history (any outcome)", async () => {
|
||||
const origHome = process.env.OPENRIG_HOME;
|
||||
const auditHome = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-audit-apply-"));
|
||||
process.env.OPENRIG_HOME = auditHome;
|
||||
try {
|
||||
const { specPath } = seedPackage();
|
||||
const bundlePath = path.join(tmpDir, "apply-mode.rigbundle");
|
||||
const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "apply-target-"));
|
||||
try {
|
||||
await app.request("/api/bundles/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ specPath, bundleName: "apply-mode", bundleVersion: "0.1.0", outputPath: bundlePath }),
|
||||
});
|
||||
|
||||
// Apply mode triggers the audit-write at one of the completion branches
|
||||
// (completed / partial / failed / caught-error). Outcome may vary in
|
||||
// the test bootstrap, but the record MUST be written.
|
||||
await app.request("/api/bundles/install", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ bundlePath, targetRoot, autoApprove: true }),
|
||||
});
|
||||
|
||||
const history = await app.request("/api/bundles/history");
|
||||
const body = await history.json();
|
||||
expect(body.total).toBe(1);
|
||||
const rec = body.records[0];
|
||||
expect(typeof rec.installedAt).toBe("string");
|
||||
expect(rec.bundlePath).toBe(bundlePath);
|
||||
expect(["success", "failed", "partial"]).toContain(rec.outcome);
|
||||
expect(typeof rec.daemonVersion).toBe("string");
|
||||
} finally {
|
||||
fs.rmSync(targetRoot, { recursive: true, force: true });
|
||||
}
|
||||
} finally {
|
||||
if (origHome === undefined) delete process.env.OPENRIG_HOME;
|
||||
else process.env.OPENRIG_HOME = origHome;
|
||||
fs.rmSync(auditHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// T11: Install concurrency lock
|
||||
it("concurrent bundle install returns 409", async () => {
|
||||
// Acquire lock manually
|
||||
|
||||
Reference in new issue
Block a user