Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/db/migrations/0012_deliverable_git.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "deliverables" ADD COLUMN "commit_sha" text DEFAULT '' NOT NULL;--> statement-breakpoint
ALTER TABLE "deliverables" ADD COLUMN "git_ref" text DEFAULT '' NOT NULL;
7 changes: 7 additions & 0 deletions packages/db/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@
"when": 1747500005000,
"tag": "0011_kpi_snapshots",
"breakpoints": false
},
{
"idx": 9,
"version": "7",
"when": 1747500006000,
"tag": "0012_deliverable_git",
"breakpoints": false
}
]
}
6 changes: 6 additions & 0 deletions packages/db/src/schema/deliverables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export const deliverables = pgTable(
sizeBytes: bigint("size_bytes", { mode: "number" }).notNull().default(0),
contentHash: text("content_hash").notNull().default(""),

// Git-first verifiability — the artifact is committed to a per-company
// deliverables repo so `git -C <dir> show <commit_sha>` is the proof.
// commitSha is the canonical content-address; gitRef is the branch/ref.
commitSha: text("commit_sha").notNull().default(""),
gitRef: text("git_ref").notNull().default(""),

// What it is
title: text("title").notNull(),
description: text("description").notNull().default(""),
Expand Down
13 changes: 12 additions & 1 deletion packages/shared/src/types/mission-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ export interface Deliverable {
sizeBytes: number;
contentHash: string;

// Git-first verifiability — the artifact is committed to a per-company
// deliverables repo; commitSha is the proof, gitRef the branch/ref.
commitSha?: string;
gitRef?: string;

// What it is
title: string;
description: string;
Expand Down Expand Up @@ -204,7 +209,12 @@ export type DeliverableStatus =
| "in_review"
| "approved"
| "rejected"
| "published";
| "published"
// Git-artifact integrity outcomes (matches the cloud deliverable_ledger
// vocabulary): `verified` = commit resolves + content hash matches;
// `failed` = verification could not confirm the artifact.
| "verified"
| "failed";

export type DeliverableKind =
| "document"
Expand Down Expand Up @@ -349,6 +359,7 @@ export type ActivityEventKind =
| "deliverable_revised"
| "deliverable_approved"
| "deliverable_published"
| "deliverable_verified"
// Node lifecycle
| "node_paused"
| "node_resumed"
Expand Down
161 changes: 160 additions & 1 deletion packages/wavex-os-server/src/mission-control/deliverables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
* knowing the layout; `writeDeliverable()` computes the path. */

import { randomUUID, createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, writeFile, readFile, stat } from "node:fs/promises";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { dirname, join } from "node:path";
import { and, desc, eq } from "drizzle-orm";

const execFileAsync = promisify(execFile);
import {
type Db,
deliverables,
Expand Down Expand Up @@ -89,6 +93,53 @@ function deliverableActionForKind(kind: DeliverableKind): string {
}

const PRODUCED_EVENT: ActivityEventKind = "deliverable_produced";
const VERIFIED_EVENT: ActivityEventKind = "deliverable_verified";

// Inline committer identity so we never depend on the operator's global
// git config (which may be unset on a fresh box). Local-only repo.
const GIT_IDENT = [
"-c", "user.email=deliverables@wavex-os.local",
"-c", "user.name=WaveX Deliverables",
];

async function gitDir(dir: string): Promise<boolean> {
try {
await stat(join(dir, ".git"));
return true;
} catch {
return false;
}
}

/** Commit one artifact file into the per-company deliverables git repo so
* it is verifiable via `git -C <dir> show <sha>`. Lazily `git init`s the
* repo on first write. Best-effort: returns null on any git failure so
* the caller still records the DB row (the row is the source of truth;
* the commit is the proof layer). */
async function commitArtifact(
dir: string,
relPath: string,
message: string,
): Promise<{ commitSha: string; gitRef: string } | null> {
try {
if (!(await gitDir(dir))) {
await execFileAsync("git", ["init", "-q"], { cwd: dir });
// Pin a deterministic default branch — older git defaults to master.
await execFileAsync("git", ["symbolic-ref", "HEAD", "refs/heads/main"], { cwd: dir }).catch(() => {});
}
await execFileAsync("git", ["add", "--", relPath], { cwd: dir });
await execFileAsync(
"git",
[...GIT_IDENT, "commit", "-q", "-m", message, "--", relPath],
{ cwd: dir },
);
const { stdout: sha } = await execFileAsync("git", ["rev-parse", "HEAD"], { cwd: dir });
const { stdout: ref } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir });
return { commitSha: sha.trim(), gitRef: ref.trim() };
} catch {
return null;
}
}

/** Write a Deliverable row + JSON envelope to disk + emit MC event.
* Returns the canonical Deliverable shape. Best-effort disk write — a
Expand Down Expand Up @@ -126,9 +177,22 @@ export async function writeDeliverable(
const contentHash = createHash("sha256").update(envelope).digest("hex");
const sizeBytes = Buffer.byteLength(envelope, "utf8");

let commitSha = "";
let gitRef = "";
try {
await mkdir(dir, { recursive: true });
await writeFile(diskPath, envelope, "utf8");
// Git-first: commit the artifact so it is verifiable. Best-effort —
// a git failure leaves commitSha empty and the row is still written.
const committed = await commitArtifact(
dir,
filename,
`deliverable(${input.kind}): ${input.title} [${input.taskRefType}:${input.taskRefId}]`,
);
if (committed) {
commitSha = committed.commitSha;
gitRef = committed.gitRef;
}
} catch {
// Disk write is best-effort — the DB row is the canonical record.
}
Expand All @@ -145,6 +209,8 @@ export async function writeDeliverable(
relPath: filename,
sizeBytes,
contentHash,
commitSha,
gitRef,
title: input.title,
description,
previewText: input.previewText ?? null,
Expand Down Expand Up @@ -242,6 +308,8 @@ function rowToDeliverable(row: typeof deliverables.$inferSelect): DeliverableSha
relPath: row.relPath,
sizeBytes: row.sizeBytes,
contentHash: row.contentHash,
commitSha: row.commitSha || undefined,
gitRef: row.gitRef || undefined,
title: row.title,
description: row.description,
previewText: row.previewText ?? undefined,
Expand Down Expand Up @@ -273,3 +341,94 @@ export async function deliverableFolder(
if (!d || !d.diskPath) return null;
return { folder: dirname(d.diskPath), diskPath: d.diskPath };
}

export interface VerifyResult {
ok: boolean;
/** Final status written to the row: "verified" on success, "failed" otherwise. */
status: DeliverableStatus;
/** Human-readable reason when verification fails. */
reason?: string;
deliverable: DeliverableShape;
}

/** Verify a deliverable's git artifact: (1) the recorded commit resolves
* in the company's deliverables repo, and (2) the on-disk file still
* hashes to the recorded contentHash. Sets status `verified` / `failed`,
* stamps reviewedAt, and emits a `deliverable_verified` event. Returns
* null if the deliverable id is unknown. */
export async function verifyDeliverable(
id: string,
reviewerNodeId: string | undefined,
db?: Db,
): Promise<VerifyResult | null> {
const resolved = db ?? (await getDb());
const rows = await resolved
.select()
.from(deliverables)
.where(eq(deliverables.id, id))
.limit(1);
const row = rows[0];
if (!row) return null;

const dir = dirname(row.diskPath);
const failures: string[] = [];

// (1) Commit must resolve in the deliverables repo.
if (row.commitSha) {
try {
await execFileAsync("git", ["cat-file", "-e", `${row.commitSha}^{commit}`], { cwd: dir });
} catch {
failures.push(`commit ${row.commitSha.slice(0, 10)} not found in deliverables repo`);
}
} else {
failures.push("no commit_sha recorded (artifact was not git-committed)");
}

// (2) On-disk content must still match the recorded hash.
try {
const buf = await readFile(row.diskPath);
const actual = createHash("sha256").update(buf).digest("hex");
if (actual !== row.contentHash) {
failures.push("content hash mismatch — artifact changed since it was recorded");
}
} catch {
failures.push("artifact file missing on disk");
}

const status: DeliverableStatus = failures.length === 0 ? "verified" : "failed";
const reason = failures.length ? failures.join("; ") : undefined;
const reviewedAt = new Date();

const updated = await resolved
.update(deliverables)
.set({
status,
reviewedByNodeId: reviewerNodeId ?? null,
reviewedAt,
reviewNotes: reason ?? null,
})
.where(eq(deliverables.id, id))
.returning();
const stored = updated[0] ?? row;
const deliverable = rowToDeliverable(stored);

const modeContext: PaperclipMode = row.diskPath.includes("/avatars/")
? "avatar"
: "solo_founder";
await logMissionControlActivity({
companyId: row.companyId,
instanceId: row.instanceId,
kind: VERIFIED_EVENT,
modeContext,
actorNodeId: reviewerNodeId ?? row.producedByNodeId,
action: `deliverable.${row.kind}.${status}`,
subjectRef: { kind: "deliverable", id, title: row.title },
deliverableRef: { id, title: row.title, kind: row.kind as DeliverableKind },
plainLanguageSentence:
status === "verified"
? `Verified deliverable "${row.title}" — commit resolves and content is intact.`
: `Could not verify deliverable "${row.title}": ${reason}.`,
});

return { ok: status === "verified", status, reason, deliverable };
}
94 changes: 94 additions & 0 deletions packages/wavex-os-server/src/routes/mission-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import {
deliverableFolder,
getDeliverable,
queryDeliverables,
writeDeliverable,
verifyDeliverable,
type QueryDeliverablesInput,
type WriteDeliverableInput,
} from "../mission-control/deliverables.js";
import {
announceDueImpacts,
Expand Down Expand Up @@ -542,6 +545,97 @@ export function registerMissionControlRoutes(app: FastifyInstance): void {
}
});

// ── POST /api/mission-control/deliverable ─────────────────────────────
// Producer write-path: an agent run records its output as a git-committed,
// verifiable artifact. Body must carry the task linkage + the artifact
// (payload). writeDeliverable() commits it to the company's deliverables
// repo and returns the row + commit_sha.
app.post("/api/mission-control/deliverable", async (req, reply) => {
const ar = authReq(req);
try {
assertBoard(ar);
} catch (e) {
if (e instanceof AuthError)
return reply.status(e.statusCode).send({ error: e.message });
throw e;
}
const body = (req.body ?? {}) as Partial<WriteDeliverableInput>;
const missing = (["instanceId", "taskRefId", "producedByNodeId", "kind", "title"] as const).filter(
(k) => !body[k],
);
if (missing.length) {
return reply.status(400).send({ ok: false, error: `missing required fields: ${missing.join(", ")}` });
}
assertCompanyAccess(ar, body.instanceId as string);
await ensureBootstrap();
try {
const deliverable = await writeDeliverable({
companyId: body.companyId ?? (body.instanceId as string),
instanceId: body.instanceId as string,
modeContext: body.modeContext ?? "solo_founder",
taskRefType: body.taskRefType ?? "issue",
taskRefId: body.taskRefId as string,
producedByNodeId: body.producedByNodeId as string,
kind: body.kind as WriteDeliverableInput["kind"],
title: body.title as string,
description: body.description,
previewText: body.previewText,
mimeType: body.mimeType,
payload: body.payload,
templateUsed: body.templateUsed,
promptUsedRef: body.promptUsedRef,
expectedKpiImpactId: body.expectedKpiImpactId,
filename: body.filename,
taskRef: body.taskRef,
plainLanguageSentence: body.plainLanguageSentence,
});
return { ok: true, deliverable, commit_sha: deliverable.commitSha ?? null };
} catch (e) {
if (e instanceof AuthError)
return reply.status(e.statusCode).send({ error: e.message });
return reply.status(503).send({ ok: false, error: e instanceof Error ? e.message : String(e) });
}
});

// ── POST /api/mission-control/deliverable/:id/verify ──────────────────
// Confirms the recorded commit resolves in the deliverables repo AND the
// on-disk artifact still hashes to contentHash. Flips status verified /
// failed. This is what turns "the fleet ran" into "the output is real".
app.post("/api/mission-control/deliverable/:id/verify", async (req, reply) => {
const ar = authReq(req);
try {
assertBoard(ar);
} catch (e) {
if (e instanceof AuthError)
return reply.status(e.statusCode).send({ error: e.message });
throw e;
}
const { id } = req.params as { id: string };
const body = (req.body ?? {}) as { reviewerNodeId?: string };
await ensureBootstrap();
try {
const existing = await getDeliverable(id);
if (!existing) {
return reply.status(404).send({ ok: false, error: "not found" });
}
assertCompanyAccess(ar, existing.instanceId);
const result = await verifyDeliverable(id, body.reviewerNodeId);
if (!result) {
return reply.status(404).send({ ok: false, error: "not found" });
}
return {
ok: result.ok,
status: result.status,
reason: result.reason ?? null,
deliverable: result.deliverable,
};
} catch (e) {
if (e instanceof AuthError)
return reply.status(e.statusCode).send({ error: e.message });
return reply.status(503).send({ ok: false, error: e instanceof Error ? e.message : String(e) });
}
});

// ── KPI impacts ───────────────────────────────────────────────────────
app.post(
"/api/mission-control/:companyId/kpi-impacts",
Expand Down
Loading
Loading