Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0a452df
feat: add full-task-scoped runtime gateway
lightcloud00 Aug 21, 2026
2b4d163
fix: isolate packaged acceptance from live services
lightcloud00 Aug 21, 2026
5fc5fef
build: isolate packaged acceptance identity
lightcloud00 Aug 21, 2026
cde7e7b
build: clean dev acceptance output
lightcloud00 Aug 21, 2026
165a1fb
fix: keep telemetry sinks in Node mode
lightcloud00 Aug 21, 2026
7e7d301
fix: keep new provider auth behind the gateway
lightcloud00 Aug 21, 2026
35dcafb
Harden telemetry turn identity and retrieval snapshots
lightcloud00 Aug 21, 2026
1ded2bd
fix: isolate gateway credential injection per turn
lightcloud00 Aug 21, 2026
a6d19d8
fix: harden full access denial routing
lightcloud00 Aug 21, 2026
7fab0ea
test: isolate packaged server smoke from host CLIs
lightcloud00 Aug 21, 2026
a11b9a4
fix: make full-task gateway cross-platform
lightcloud00 Aug 21, 2026
cb8ff22
fix: start CUA for hard-death package smoke
lightcloud00 Aug 21, 2026
cc3580e
fix: retry Windows process registry identity
lightcloud00 Aug 21, 2026
3c8f04c
Merge upstream main and harden full-task-scoped runtime
lightcloud00 Aug 21, 2026
d18b80c
fix: close final Windows and review gates
lightcloud00 Aug 22, 2026
d5aab09
Merge upstream main into full-task-scoped runtime
lightcloud00 Aug 22, 2026
f27c9ec
feat(mcp): register OpenMaus fleet bridge
lightcloud00 Aug 22, 2026
c94a5d9
Harden OpenMaus observer bridge
lightcloud00 Aug 22, 2026
3552bfb
feat: add governed improvement agent graphs
lightcloud00 Aug 22, 2026
d1412b7
Merge remote-tracking branch 'origin/main' into codex/openmaus-govern…
lightcloud00 Aug 22, 2026
e14833b
feat: add fleet catalog roles and shared goals
lightcloud00 Aug 22, 2026
35e6fba
chore: allow known synthetic redaction fixture
lightcloud00 Aug 22, 2026
6e7a6c2
fix: launch Windows Node credential proxy directly
lightcloud00 Aug 22, 2026
a886d5f
fix: close fleet role review gates
lightcloud00 Aug 22, 2026
ff7bab8
feat(models): project fleet catalog into OpenMausBot
lightcloud00 Aug 22, 2026
f6816d0
feat: add task-scoped RAG profiles
lightcloud00 Aug 22, 2026
0461635
feat: harden task-scoped retrieval rollout
lightcloud00 Aug 22, 2026
113218d
fix: accept current-verified degraded index evidence
lightcloud00 Aug 22, 2026
bb499c6
fix: make RAG acceptance portable on Windows
lightcloud00 Aug 22, 2026
6b4f476
fix: confine task-scoped retrieval evidence
lightcloud00 Aug 22, 2026
f862079
fix: reject redirected transcript evidence
lightcloud00 Aug 22, 2026
26fa15f
feat: add trusted Chief status capsule
lightcloud00 Aug 22, 2026
2b8fece
fix: rebind agent graph evidence paths
lightcloud00 Aug 22, 2026
96a7592
test: pin delegated unattended provider
lightcloud00 Aug 22, 2026
0426a4e
fix: preserve graph filesystem checks on Windows
lightcloud00 Aug 22, 2026
602d996
fix: close governed graph review gates
lightcloud00 Aug 22, 2026
656762b
fix: accept canonical Windows graph parents
lightcloud00 Aug 22, 2026
7e64928
fix: close governed graph review findings
lightcloud00 Aug 22, 2026
20294e1
fix: close anchored worker stdin on hook failure
lightcloud00 Aug 22, 2026
dd3c030
fix: await anchored worker close on stdin failure
lightcloud00 Aug 22, 2026
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 .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.bat text eol=crlf
*.cmd text eol=crlf
140 changes: 140 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ on:
default: false

permissions:
actions: read
contents: read

jobs:
Expand All @@ -53,6 +54,145 @@ jobs:
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
- name: Gate the exact commit on completed all-platform CI
env:
GH_TOKEN: ${{ github.token }}
RELEASE_SHA: ${{ steps.pin.outputs.sha }}
REPOSITORY: ${{ github.repository }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
node --input-type=module <<'EOF'
const required = new Map([
["typecheck + test (macos-latest)", [
"Run pnpm typecheck",
"Run pnpm test",
"Run pnpm check:electron",
]],
["typecheck + test (ubuntu-latest)", [
"Run pnpm typecheck",
"Run pnpm test",
"Run pnpm check:electron",
"production UI build",
]],
["typecheck + test (windows-latest)", [
"Run pnpm typecheck",
"Run pnpm test",
"Run pnpm check:electron",
]],
["package + smoke (Ubuntu 24.04 x64)", [
"Package from the verified offline CUA stage",
"Run node scripts/verify-linux-package.mjs",
"Launch packaged app and verify lifecycle",
]],
]);

const token = process.env.GH_TOKEN;
const repository = process.env.REPOSITORY;
const sha = process.env.RELEASE_SHA;
const defaultBranch = process.env.DEFAULT_BRANCH;
const apiBase = process.env.GITHUB_API_URL ?? "https://api.github.com";
if (!token || !repository || !defaultBranch || !/^[0-9a-f]{40}$/.test(sha ?? "")) {
throw new Error("release CI gate is missing a token, repository, default branch, or exact 40-character SHA");
}

const { readFileSync } = await import("node:fs");
const ciWorkflow = readFileSync(".github/workflows/ci.yml", "utf8");
const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
const requiredCiSource = [
"os: [macos-latest, ubuntu-latest, windows-latest]",
"- run: pnpm typecheck",
"- run: pnpm test",
"- run: pnpm check:electron",
"name: package + smoke (Ubuntu 24.04 x64)",
"run: pnpm package:linux:offline",
"run: node scripts/verify-linux-package.mjs",
"run: pnpm smoke:linux-package",
];
for (const fragment of requiredCiSource) {
if (!ciWorkflow.includes(fragment)) {
throw new Error(`exact release SHA no longer contains required CI/package contract: ${fragment}`);
}
}
if (!(packageJson.scripts?.test ?? "").includes("pnpm test:packaged-server")) {
throw new Error("pnpm test no longer includes the packaged-server gate");
}
if (!(packageJson.scripts?.["test:packaged-server"] ?? "").includes("scripts/smoke-packaged-server.mjs")) {
throw new Error("test:packaged-server no longer starts the isolated packaged-server smoke");
}

const headers = {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"x-github-api-version": "2022-11-28",
};
const getJson = async (path) => {
const response = await fetch(new URL(path, `${apiBase}/`), { headers });
if (!response.ok) {
throw new Error(`GitHub Actions proof unavailable for ${path}: HTTP ${response.status}`);
}
return response.json();
};

const runsUrl = new URL(
`repos/${repository}/actions/workflows/ci.yml/runs`,
`${apiBase}/`,
);
runsUrl.searchParams.set("head_sha", sha);
runsUrl.searchParams.set("status", "success");
runsUrl.searchParams.set("per_page", "100");
const runs = await getJson(`${runsUrl.pathname}${runsUrl.search}`);
const candidates = (runs.workflow_runs ?? [])
.filter((run) => (
run.head_sha === sha
&& run.path === ".github/workflows/ci.yml"
&& run.status === "completed"
&& run.conclusion === "success"
// pull_request jobs checkout a synthetic merge ref, not the
// requested release SHA. A successful default-branch push is
// the exact-byte proof used by this release gate.
&& run.event === "push"
&& run.head_branch === defaultBranch
))
.sort((left, right) => (
(right.run_attempt ?? 0) - (left.run_attempt ?? 0)
|| right.id - left.id
));
if (candidates.length === 0) {
throw new Error(
`no successful completed CI push run on ${defaultBranch} proves exact release SHA ${sha}`,
);
}

const run = candidates[0];
const jobs = await getJson(
`repos/${repository}/actions/runs/${run.id}/jobs?filter=latest&per_page=100`,
);
if ((jobs.total_count ?? 0) > (jobs.jobs ?? []).length) {
throw new Error(`CI run ${run.id} returned incomplete paginated job proof`);
}

for (const [jobName, requiredSteps] of required) {
const matches = (jobs.jobs ?? []).filter((job) => job.name === jobName);
if (matches.length !== 1) {
throw new Error(`CI run ${run.id} must contain exactly one ${jobName} job; found ${matches.length}`);
}
const job = matches[0];
if (job.head_sha !== sha || job.status !== "completed" || job.conclusion !== "success") {
throw new Error(`CI job ${jobName} is not a completed success for exact SHA ${sha}`);
}
for (const stepName of requiredSteps) {
const steps = (job.steps ?? []).filter((step) => step.name === stepName);
if (steps.length !== 1 || steps[0].status !== "completed" || steps[0].conclusion !== "success") {
throw new Error(`CI job ${jobName} lacks successful required step: ${stepName}`);
}
}
}

console.log(
`exact-SHA CI gate passed: ${repository}@${sha} via run ${run.id}; ${required.size} required jobs verified`,
);
EOF
- name: Refuse to overwrite a published release
env:
GH_TOKEN: ${{ secrets.RELEASES_PAT }}
Expand Down
2 changes: 2 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Intentional synthetic authorization fixture used to verify secret redaction.
0a452dfb0ffaaaa180da86d0c85cd93474335011:server/redact.test.ts:curl-auth-header:139
15 changes: 15 additions & 0 deletions electron-builder.dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Packaged acceptance must not register itself as the production application.
# macOS LaunchServices routes later opens by bundle identity, so using the
# release identity for a development build can redirect unrelated callers to
# an unsigned candidate without its isolated environment. A distinct product
# name also gives environmentless relaunches separate userData and logs paths.
extends: ./electron-builder.yml
appId: com.openmausbot.app.full-task-dev
productName: OpenMausBot Full Task Dev
artifactName: OpenMausBot-Full-Task-Dev-${version}-${arch}.${ext}

directories:
output: release-dev

mac:
artifactName: OpenMausBot-Full-Task-Dev-${version}-${arch}.${ext}
175 changes: 175 additions & 0 deletions electron/agent-graph-approval.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use strict";

const { createHash } = require("node:crypto");

const HASH = /^sha256:[0-9a-f]{64}$/;
const ID = /^[\w-]+$/;
const BIDI = /[\u202a-\u202e\u2066-\u2069]/i;
const REDACTED = /(?:\b(?:redacted|omitted|withheld)\b|\*{3,}|\[(?:secret|private)\])/i;

function text(value, max = 500) {
return typeof value === "string"
? value.replace(/[\u202a-\u202e\u2066-\u2069]/gi, "").replace(/[\r\n\t]+/g, " ").trim().slice(0, max)
: "";
}

function canonical(value) {
const visit = (item) => {
if (Array.isArray(item)) return item.map(visit);
if (!item || typeof item !== "object") return item;
return Object.fromEntries(
Object.entries(item)
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
.map(([key, nested]) => [key, visit(nested)]),
);
};
return JSON.stringify(visit(value));
}

function canonicalHash(value) {
return `sha256:${createHash("sha256").update(canonical(value)).digest("hex")}`;
}

/** Validate the server-owned immutable draft and render its semantic scope. */
function graphApprovalDetail(payload, expectedId, expectedHash) {
if (!ID.test(expectedId) || !HASH.test(expectedHash)) throw new Error("Invalid agent graph approval target");
const graph = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.graph : null;
if (!graph || typeof graph !== "object" || Array.isArray(graph)) throw new Error("Agent graph draft is unavailable");
if (graph.id !== expectedId || graph.graphHash !== expectedHash || graph.status !== "draft") {
throw new Error("Agent graph changed before approval; preview it again");
}
if (!Array.isArray(graph.nodes) || graph.nodes.length < 1 || graph.nodes.length > 32) {
throw new Error("Agent graph node manifest is invalid");
}

const lines = [
`Graph: ${expectedId}`,
`Exact hash: ${expectedHash}`,
`Objective: ${text(graph.objective, 800) || "(missing)"}`,
`Proposal feed: ${HASH.test(graph.feedHash) ? graph.feedHash : "none"}`,
];
const proposals = Array.isArray(graph.proposalSnapshots) ? graph.proposalSnapshots.slice(0, 20) : [];
if (proposals.length) {
lines.push("Proposals:");
for (const proposal of proposals) {
const evidence = Array.isArray(proposal?.evidenceHashes)
? proposal.evidenceHashes.filter((value) => HASH.test(value)).slice(0, 8).join(", ")
: "";
lines.push(`- ${text(proposal?.proposalId, 100)} content=${HASH.test(proposal?.contentHash) ? proposal.contentHash : "invalid"}${evidence ? ` evidence=${evidence}` : ""}`);
lines.push(` change=${text(proposal?.proposedChange, 1_500) || "(none)"}`);
lines.push(` risk=${text(proposal?.risk, 700) || "(none)"}`);
const tests = Array.isArray(proposal?.tests) ? proposal.tests.slice(0, 5).map((value) => text(value, 400)).filter(Boolean) : [];
lines.push(` tests=${tests.join(" | ") || "(none)"}`);
lines.push(` rollback=${text(proposal?.rollback, 700) || "(none)"}`);
}
}
lines.push("Nodes:");
for (const [index, node] of graph.nodes.entries()) {
const routes = Array.isArray(node?.routes) ? node.routes.slice(0, 8) : [];
if (!routes.length) throw new Error("Agent graph route manifest is invalid");
lines.push(`${index + 1}. ${text(node.id, 100)} — ${text(node.title, 300)}`);
lines.push(` role=${text(node.role, 100)} permission=${text(node.permissionClass, 40)}`);
for (const route of routes) {
lines.push(` route=${text(route?.botId, 100)} / ${text(route?.engine, 80)} / ${text(route?.model, 160)}`);
lines.push(` workspace=${text(route?.workspaceRoot, 700)}`);
lines.push(` workspace identity=${HASH.test(route?.workspaceIdentity) ? route.workspaceIdentity : "invalid"}`);
lines.push(` authority digest=${HASH.test(route?.authorityDigest) ? route.authorityDigest : "invalid"}`);
}
}
const detail = lines.join("\n");
if (detail.length > 16_000) throw new Error("Agent graph approval manifest is too large");
return detail;
}

/** Render and independently bind the exact completed run the host is asked
* to promote. The server still performs the authoritative admission refresh;
* this native manifest ensures the visible approval covers every requirement,
* route identity, task/turn identity, and proof reference in the receipt. */
function graphVerificationDetail(payload, expectedId, expectedGraphHash, expectedReceiptHash) {
if (!ID.test(expectedId) || !HASH.test(expectedGraphHash) || !HASH.test(expectedReceiptHash)) {
throw new Error("Invalid agent graph verification target");
}
const graph = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.graph : null;
const receipt = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.receipt : null;
const preview = payload && typeof payload === "object" && !Array.isArray(payload) ? payload.verificationPreview : null;
if (!graph || typeof graph !== "object" || Array.isArray(graph) ||
!receipt || typeof receipt !== "object" || Array.isArray(receipt) ||
!preview || typeof preview !== "object" || Array.isArray(preview)) {
throw new Error("Agent graph verification evidence is unavailable");
}
if (
payload.receiptHash !== expectedReceiptHash || canonicalHash(receipt) !== expectedReceiptHash ||
graph.id !== expectedId || graph.graphHash !== expectedGraphHash || graph.status !== "completed" ||
receipt.graph_id !== expectedId || receipt.graph_hash !== expectedGraphHash || receipt.status !== "completed" ||
receipt.verification_status !== "unverified" ||
receipt.completion_claim !== "provider_turns_completed_with_task_receipts_unverified" ||
receipt.automatic_mutation !== false || receipt.model_weights_changed !== false ||
receipt.instruction_authority !== false || typeof receipt.finished_at !== "string" ||
receipt.verified_at !== null || receipt.evidence_manifest_hash !== null ||
preview.graph_id !== expectedId || preview.graph_hash !== expectedGraphHash ||
preview.receipt_hash !== expectedReceiptHash || !HASH.test(preview.evidence_manifest_hash) ||
!Array.isArray(preview.evidence) || canonicalHash(preview.evidence) !== preview.evidence_manifest_hash
) throw new Error("Agent graph run changed before verification; read it again");
if (
!Array.isArray(graph.nodes) || !Array.isArray(receipt.nodes) || !graph.nodes.length ||
graph.nodes.length !== receipt.nodes.length || graph.nodes.length > 40
) throw new Error("Agent graph verification node manifest is invalid");

const lines = [
`Graph: ${expectedId}`,
`Exact graph hash: ${expectedGraphHash}`,
`Exact run receipt hash: ${expectedReceiptHash}`,
`Exact evidence manifest hash: ${preview.evidence_manifest_hash}`,
`Finished: ${text(receipt.finished_at, 100)}`,
"Completed nodes and host evidence:",
];
for (const [index, graphNode] of graph.nodes.entries()) {
const evidence = receipt.nodes[index];
const route = graphNode?.selectedRoute;
const requirements = Array.isArray(graphNode?.proofRequirements) ? graphNode.proofRequirements : [];
const references = Array.isArray(evidence?.proof_refs) ? evidence.proof_refs : [];
const hostEvidence = preview.evidence.filter((item) => item?.node_id === graphNode?.id);
if (
!graphNode || !evidence || evidence.id !== graphNode.id || graphNode.status !== "completed" ||
evidence.status !== "completed" || evidence.evidence_status !== "task-receipt-only" || evidence.error !== null ||
!Array.isArray(evidence.verified_evidence) || evidence.verified_evidence.length !== 0 ||
!route || typeof route !== "object" || !requirements.length || requirements.length > 10 ||
!hostEvidence.length || hostEvidence.length > 8 ||
!references.length || references.length > 40 || new Set(references).size !== references.length ||
!references.includes(`thread:${evidence.thread_id}`) ||
![evidence.task_id, evidence.thread_id, evidence.turn_id, evidence.bot_id, evidence.instance_id,
evidence.engine, evidence.model, evidence.workspace_root, evidence.workspace_identity].every((value) =>
typeof value === "string" && value.length > 0) ||
evidence.bot_id !== route.botId || evidence.instance_id !== route.instanceId ||
evidence.engine !== route.engine || evidence.model !== route.model ||
evidence.workspace_root !== route.workspaceRoot || evidence.workspace_identity !== route.workspaceIdentity ||
!HASH.test(route.workspaceIdentity) || !HASH.test(route.authorityDigest)
) throw new Error(`Agent graph node ${text(graphNode?.id, 100) || index + 1} has partial verification evidence`);
if (references.some((reference) => typeof reference !== "string" || BIDI.test(reference) || REDACTED.test(reference))) {
throw new Error(`Agent graph node ${text(graphNode.id, 100)} contains redacted verification evidence`);
}
if (hostEvidence.some((item) =>
!item || typeof item.relative_path !== "string" || !item.relative_path || item.relative_path.length > 700 ||
/^(?:[\\/]|[A-Za-z]:[\\/])/.test(item.relative_path) ||
item.relative_path.split(/[\\/]/).includes("..") || BIDI.test(item.relative_path) || REDACTED.test(item.relative_path) ||
item.workspace_identity !== route.workspaceIdentity || !HASH.test(item.sha256) ||
!Number.isSafeInteger(item.bytes) || item.bytes < 0 || item.bytes > 1024 * 1024
)) throw new Error(`Agent graph node ${text(graphNode.id, 100)} contains invalid host file evidence`);
lines.push(`${index + 1}. ${text(graphNode.id, 100)} — ${text(graphNode.title, 300)}`);
lines.push(` task=${text(evidence.task_id, 200)} thread=${text(evidence.thread_id, 200)} turn=${text(evidence.turn_id, 200)}`);
lines.push(` route=${text(route.botId, 100)} / ${text(route.engine, 80)} / ${text(route.model, 160)}`);
lines.push(` workspace=${text(route.workspaceRoot, 700)}`);
lines.push(` workspace identity=${route.workspaceIdentity}`);
lines.push(` authority digest=${route.authorityDigest}`);
for (const requirement of requirements) lines.push(` requirement=${text(requirement, 500)}`);
for (const reference of references) lines.push(` proof=${text(reference, 500)}`);
for (const item of hostEvidence) {
lines.push(` file=${text(item.relative_path, 700)} sha256=${item.sha256} bytes=${item.bytes}`);
}
}
const detail = lines.join("\n");
if (detail.length > 24_000) throw new Error("Agent graph verification manifest is too large");
return detail;
}

module.exports = { graphApprovalDetail, graphVerificationDetail };
Loading
Loading