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
12 changes: 12 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,22 @@ jobs:
- name: Build
run: npm run build

- name: Resolve exact Daimon provenance fixture revision
id: daimon-provenance
run: |
node --input-type=module <<'NODE' >> "$GITHUB_OUTPUT"
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
const ref = parse(readFileSync('runtimes.yaml', 'utf8')).runtimes.daimon.ref;
if (!/^[a-f0-9]{40}$/.test(ref)) throw Error('Daimon provenance requires a full commit pin');
console.log(`ref=${ref}`);
NODE

- name: Check out Daimon provenance fixture
uses: actions/checkout@v4
with:
repository: noopolis/daimon-harness
ref: ${{ steps.daimon-provenance.outputs.ref }}
path: daimon-fixture

# The shipped runtime refuses any Grok executable but the manifest-pinned
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,82 @@ jobs:
env -u TYPESAFE_API_KEY src/training/dspy/.venv/bin/python -c 'import json, subprocess; result=subprocess.run(["src/training/dspy/.venv/bin/spawnfile-training-dspy"], input="{}\n", capture_output=True, text=True, check=False); terminal=json.loads(result.stdout); assert result.returncode == 1 and terminal["schema"] == "spawnfile.training.dspy.v2" and terminal["error"]["code"] == "invalid_request"'
- name: Test the bridge and enforce per-module Python coverage
run: env -u TYPESAFE_API_KEY npm run test:training-python

# Run the release gate on every PR/main revision: ~6 runner minutes in
# parallel with coverage, with no path filter that can miss an upstream move.
source-provenance:
name: offline source provenance (linux/amd64)
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- name: Check out Spawnfile
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json

- name: Install dependencies
run: env -u TYPESAFE_API_KEY npm ci

# Uses the checked-in native helpers, as the shipped package does.
# The root job separately rebuilds and syscall-tests both architectures.
- name: Build
run: env -u TYPESAFE_API_KEY npm run build

- name: Resolve exact Daimon provenance fixture revision
id: daimon-provenance
run: |
node --input-type=module <<'NODE' >> "$GITHUB_OUTPUT"
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
const ref = parse(readFileSync('runtimes.yaml', 'utf8')).runtimes.daimon.ref;
if (!/^[a-f0-9]{40}$/.test(ref)) throw Error('Daimon provenance requires a full commit pin');
console.log(`ref=${ref}`);
NODE

- name: Check out Daimon provenance fixture
uses: actions/checkout@v4
with:
repository: noopolis/daimon-harness
ref: ${{ steps.daimon-provenance.outputs.ref }}
path: daimon-fixture

# The shipped runtime refuses any Grok executable but the manifest-pinned
# build, so the source build needs the real linux-x86_64 binary. Its URL,
# size, and digest come from the vendored Daimon contract manifest, so a
# re-vendored Daimon bump moves this download with no CI edit.
- name: Fetch the pinned linux-x86_64 Grok CLI
id: grok-cli
shell: bash
run: |
set -euo pipefail

manifest=src/runtime/daimon/contract-manifest.json
read -r url bytes sha256 < <(node -e '
const artifact = require("./" + process.argv[1]).grokEngineBroker?.grokCliArtifacts?.x64;
if (typeof artifact?.url !== "string" || !Number.isInteger(artifact.bytes) || !/^[a-f0-9]{64}$/.test(artifact.sha256 ?? "")) {
throw new Error("vendored Daimon contract manifest has no pinned linux-x86_64 Grok CLI artifact");
}
process.stdout.write(`${artifact.url} ${artifact.bytes} ${artifact.sha256}\n`);
' "${manifest}")

file="${RUNNER_TEMP}/grok"
curl --fail --location --silent --show-error --output "${file}" -- "${url}"
test "$(stat -c %s -- "${file}")" -eq "${bytes}"
echo "${sha256} ${file}" | sha256sum --check --strict -
chmod 0755 -- "${file}"
echo "file=${file}" >> "${GITHUB_OUTPUT}"

- name: Verify Git-free offline linux/amd64 source build
env:
SPAWNFILE_GROK_CLI_FILE: ${{ steps.grok-cli.outputs.file }}
SPAWNFILE_TEST_DAIMON_SOURCE: ${{ github.workspace }}/daimon-fixture
run: env -u TYPESAFE_API_KEY npm run test:source-provenance-docker

- name: Remove Daimon provenance fixture and Grok CLI
if: always()
run: rm -rf -- daimon-fixture "${RUNNER_TEMP}/grok"
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "spawnfile",
"version": "0.1.26",
"version": "0.1.27",
"description": "Canonical source compiler for autonomous agents and teams.",
"license": "MIT",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ in the npm package. Scripts run as native TypeScript on Node 22.19+.
| `verify-package-closure.ts` | `npm run verify:package-closure` | Verify the packed CLI and runtime closure |
| `test-training-python.ts` | `npm run test:training-python`, CI | Run the real DSPy bridge pytest suite and enforce 90% lines and branches for every module and the total; install `src/training/dspy/requirements.lock` and the editable bridge in `src/training/dspy/.venv` first |
| `product-state-volume-integration.test.ts` | `npm run test:product-state-volume`, CI | Test real volume preseed; Docker and host volume access required |
| `source-provenance-bundle.integration.test.ts` | `npm run test:source-provenance-docker` | Test the real offline Daimon archive build |
| `source-provenance-bundle.integration.test.ts` | `npm run test:source-provenance-docker` | PR/main and publish workflows: real offline Daimon archive build and tamper rejection; Docker, Daimon checkout (`SPAWNFILE_TEST_DAIMON_SOURCE`), pinned Linux Grok CLI (`SPAWNFILE_GROK_CLI_FILE`) |
| `moltnet-source-provenance.integration.test.ts` | `npm run test:moltnet-source-provenance-docker` | Test the real offline Moltnet archive build |

`source-provenance-bundle.ts`, `native-helper-artifacts.ts`, and
Expand Down
40 changes: 40 additions & 0 deletions scripts/native-helper-workflows.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { parse } from "yaml";

const gate = "node --experimental-strip-types --test scripts/native-helper-artifacts.test.ts scripts/native-helper-integration.test.ts";

Expand All @@ -22,3 +23,42 @@ test("normal package build copies shipped helpers without invoking the native re
assert.doesNotMatch(packageJson.scripts.build, /native\/build\.ts|docker/u);
assert.equal(packageJson.scripts["build:native"], "node --experimental-strip-types ./src/deployment/native/build.ts");
});

test("PR/main runs the same offline source provenance gate and inputs as release", async () => {
type Step = { name: string; run?: string; env?: Record<string, string>; uses?: string; with?: Record<string, string> };
type Job = { if?: string; "timeout-minutes": number; steps: Step[] };
const ci = parse(await readFile(".github/workflows/test.yml", "utf8")) as {
on: { pull_request: { paths?: string[]; "paths-ignore"?: string[] } | null; push: { branches: string[]; paths?: string[]; "paths-ignore"?: string[] } };
jobs: Record<string, Job>;
};
const release = parse(await readFile(".github/workflows/publish.yml", "utf8")) as { jobs: { publish: Job } };
assert.ok(Object.hasOwn(ci.on, "pull_request"));
assert.ok(ci.on.push.branches.includes("main"));
assert.equal(ci.on.push.paths, undefined);
assert.equal(ci.on.push["paths-ignore"], undefined);
assert.equal(ci.on.pull_request?.paths, undefined);
assert.equal(ci.on.pull_request?.["paths-ignore"], undefined);
const job = ci.jobs["source-provenance"];
assert.ok(job);
assert.equal(job.if, undefined, "the gate must run on every PR/main revision");
assert.ok(job["timeout-minutes"] >= 15 && job["timeout-minutes"] <= 30);
for (const name of ["Resolve exact Daimon provenance fixture revision", "Check out Daimon provenance fixture", "Fetch the pinned linux-x86_64 Grok CLI",
"Verify Git-free offline linux/amd64 source build"]) {
const actual = job.steps.find((step) => step.name === name);
const expected = release.jobs.publish.steps.find((step) => step.name === name);
assert.ok(actual && expected, name);
assert.deepEqual(actual.with, expected.with, name);
assert.deepEqual(actual.env, expected.env, name);
assert.equal(actual.run?.replace("env -u TYPESAFE_API_KEY ", ""), expected.run, name);
}
assert.equal(job.steps.find((step) => step.name === "Check out Daimon provenance fixture")?.with?.ref,
"${{ steps.daimon-provenance.outputs.ref }}");
for (const candidate of [job, release.jobs.publish]) {
const resolve = candidate.steps.findIndex((step) => step.name === "Resolve exact Daimon provenance fixture revision");
const checkout = candidate.steps.findIndex((step) => step.name === "Check out Daimon provenance fixture");
assert.ok(resolve >= 0 && checkout > resolve, "resolve the pin before checkout, or an empty ref follows upstream HEAD");
}
const gate = job.steps.findIndex((step) => step.run?.includes("npm run test:source-provenance-docker"));
const build = job.steps.findIndex((step) => step.run?.includes("npm run build"));
assert.ok(build >= 0 && gate > build, "the gate requires compiled materializer output");
});
51 changes: 43 additions & 8 deletions scripts/source-provenance-bundle.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ const pinnedGrokCli = (): string => {
};
const sha512 = (file: string): string => `sha512:${createHash("sha512").update(readFileSync(file)).digest("hex")}`;

const assertRejected = (result: ReturnType<typeof spawnSync>, label: string, diagnostic: RegExp): void => {
assert.ifError(result.error);
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
assert.ok(result.status !== null && result.status > 0, `${label} must be rejected (status=${result.status})\n${output}`);
assert.match(output, diagnostic, `${label} must fail at its verifier`);
};

// A full pass — offline closure, shipped artifact, wrapper image, literal org
// container, and four tamper rejections — measured 328 s on a GitHub-hosted
// runner, so the budget leaves room for a slower one rather than 32 s.
Expand Down Expand Up @@ -82,7 +89,7 @@ test("actual Daimon lock produces a real offline linux/amd64 shipped artifact an
const port = mapped.slice(mapped.lastIndexOf(":") + 1);
const identityPath = path.join(repository, ".local-daimon-runtime-identity.json"), priorIdentity = existsSync(identityPath) ? readFileSync(identityPath) : null;
try {
execFileSync("npm", ["run", "--silent", "build:local-daimon"], { cwd: repository, env: { ...process.env, AGY_CLI_SHA256: agySha, AGY_CLI_SHA512: sha512(agyTar), AGY_CLI_URL: "https://invalid.example/agy", AGY_CLI_VERSION: "fixture", CODEX_CLI_SHA256: codexSha, GROK_CLI_SHA256: grokSha, GROK_CLI_URL: pinnedManifest.grokEngineBroker.grokCliArtifacts.x64.url, GROK_CLI_VERSION: pinnedManifest.grokEngineBroker.grokCliVersion, SPAWNFILE_AGY_CLI_ARCHIVE: agyTar, SPAWNFILE_DAIMON_DEPENDENCY_BUNDLE: dependencyTar, SPAWNFILE_DAIMON_LOCAL_IMAGE_TAG: `127.0.0.1:${port}/noopolis/spawnfile-runtime-daimon:archive-wrapper`, SPAWNFILE_DAIMON_SOURCE_BUNDLE: sourceTar, SPAWNFILE_GROK_CLI_FILE: grok }, stdio: "ignore" });
execFileSync("npm", ["run", "--silent", "build:local-daimon"], { cwd: repository, env: { ...process.env, SPAWNFILE_DAIMON_TARGET_ARCH: "amd64", AGY_CLI_SHA256: agySha, AGY_CLI_SHA512: sha512(agyTar), AGY_CLI_URL: "https://invalid.example/agy", AGY_CLI_VERSION: "fixture", CODEX_CLI_SHA256: codexSha, GROK_CLI_SHA256: grokSha, GROK_CLI_URL: pinnedManifest.grokEngineBroker.grokCliArtifacts.x64.url, GROK_CLI_VERSION: pinnedManifest.grokEngineBroker.grokCliVersion, SPAWNFILE_AGY_CLI_ARCHIVE: agyTar, SPAWNFILE_DAIMON_DEPENDENCY_BUNDLE: dependencyTar, SPAWNFILE_DAIMON_LOCAL_IMAGE_TAG: `127.0.0.1:${port}/noopolis/spawnfile-runtime-daimon:archive-wrapper`, SPAWNFILE_DAIMON_SOURCE_BUNDLE: sourceTar, SPAWNFILE_GROK_CLI_FILE: grok }, stdio: "inherit" });
const wrapperIdentity = JSON.parse(readFileSync(identityPath, "utf8")); assert.equal(wrapperIdentity.image_architecture, "amd64"); assert.match(wrapperIdentity.image_reference, new RegExp(`^127\\.0\\.0\\.1:${port}/noopolis/spawnfile-runtime-daimon@sha256:[a-f0-9]{64}$`, "u"));
} finally { if (priorIdentity) writeFileSync(identityPath, priorIdentity); else rmSync(identityPath, { force: true }); }
const receipt = { architecture: "amd64", daimon: { package_sha256: packageSha, source_inputs: sourceInputs, source_sha256: digest(path.join(packageContext, "source-inputs.json")) }, engines: { agy: { executable_sha256: agySha }, codex: { executable_sha256: codexSha }, grok: { executable_sha256: grokSha } }, manifest_sha256: manifestSha, provenance: { agy: { archive: { format: "tar.gz", sha512: sha512(agyTar), url: "https://invalid.example/agy", version: "fixture" } }, grok: { executable: { sha256: grokSha, url: pinnedManifest.grokEngineBroker.grokCliArtifacts.x64.url, version: pinnedManifest.grokEngineBroker.grokCliVersion } } }, version: "spawnfile.daimon-runtime-capability-receipt.v1" };
Expand Down Expand Up @@ -138,13 +145,41 @@ test("actual Daimon lock produces a real offline linux/amd64 shipped artifact an
const fakeReceipt = validateSourceBundle(readFileSync(fakeTar));
mkdirSync(fakeContext); mkdirSync(fakeOutput); cpSync(fakeTar, path.join(fakeContext, "dependencies.tar"));
const fakeArgs = args.map((value) => value === `dependency_bundle=${dependencyContext}` ? `dependency_bundle=${fakeContext}` : value === `DEPENDENCY_ARCHIVE_SHA256=${dependencyReceipt.archive_sha256}` ? `DEPENDENCY_ARCHIVE_SHA256=${fakeReceipt.archive_sha256}` : value === `DEPENDENCY_MANIFEST_SHA256=${dependencyReceipt.manifest_sha256}` ? `DEPENDENCY_MANIFEST_SHA256=${fakeReceipt.manifest_sha256}` : value === `type=local,dest=${output}` ? `type=local,dest=${fakeOutput}` : value);
assert.notEqual(spawnSync("docker", fakeArgs, { stdio: "ignore" }).status, 0);
for (const fault of ["missing", "wrong"]) {
const badRoot=path.join(temporary,`bad-native-${fault}`),badTar=path.join(temporary,`bad-native-${fault}.tar`),badContext=path.join(temporary,`bad-native-${fault}-context`),badOutput=path.join(temporary,`bad-native-${fault}-output`);
mkdirSync(badRoot);execFileSync("tar",["-xf",sourceTar,"-C",badRoot]);rmSync(path.join(badRoot,".spawnfile-source-manifest.json"),{force:true});const artifact=path.join(badRoot,"src","runtime","native","artifacts","daimon-engine-broker-x64");if(fault==="missing")rmSync(artifact);else writeFileSync(artifact,"wrong-native-artifact\n",{mode:0o755});
const badReceipt=createSourceBundle(badRoot,badTar);mkdirSync(badContext);mkdirSync(badOutput);cpSync(badTar,path.join(badContext,"source.tar"));const badArgs=args.map((value)=>value===`source_bundle=${sourceContext}`?`source_bundle=${badContext}`:value===`SOURCE_ARCHIVE_SHA256=${sourceReceipt.archive_sha256}`?`SOURCE_ARCHIVE_SHA256=${badReceipt.archive_sha256}`:value===`SOURCE_MANIFEST_SHA256=${sourceReceipt.manifest_sha256}`?`SOURCE_MANIFEST_SHA256=${badReceipt.manifest_sha256}`:value===`type=local,dest=${output}`?`type=local,dest=${badOutput}`:value);assert.notEqual(spawnSync("docker",badArgs,{stdio:"ignore"}).status,0);
assertRejected(spawnSync("docker", fakeArgs, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }), "false Codex lock integrity", /npm error code (?:EINTEGRITY|ENOTCACHED)/u);
for (const fault of ["missing", "wrong"] as const) {
const badRoot = path.join(temporary, `bad-native-${fault}`), badTar = path.join(temporary, `bad-native-${fault}.tar`),
badContext = path.join(temporary, `bad-native-${fault}-context`), badOutput = path.join(temporary, `bad-native-${fault}-output`);
mkdirSync(badRoot);
execFileSync("tar", ["-xf", sourceTar, "-C", badRoot]);
rmSync(path.join(badRoot, ".spawnfile-source-manifest.json"));
// Daimon's engine ownership move relocated the source artifact; the pack
// and installed runtime still ship this exact x64 broker's bytes.
const artifact = path.join(badRoot, "src", "engines", "grok", "broker", "native", "artifacts", "daimon-engine-broker-x64");
assert.deepEqual(readFileSync(artifact), packedBroker, "tamper target must be the shipped broker");
execFileSync("npm", ["run", "verify:native"], { cwd: badRoot, stdio: "pipe" });
if (fault === "missing") rmSync(artifact);
else writeFileSync(artifact, "wrong-native-artifact\n", { mode: 0o755 });
const diagnostic = fault === "missing" ? /ENOENT[^\n]*daimon-engine-broker-x64/u : /native broker provenance mismatch/u;
// Check the verifier directly as well: a missing file breaking a later
// copy/build must not hide a verifier that silently ignores that file.
assertRejected(spawnSync("npm", ["run", "verify:native"], { cwd: badRoot, encoding: "utf8" }), `${fault} native verifier`, diagnostic);
const badReceipt = createSourceBundle(badRoot, badTar);
mkdirSync(badContext); mkdirSync(badOutput); cpSync(badTar, path.join(badContext, "source.tar"));
const badArgs = args.map((value) => value === `source_bundle=${sourceContext}` ? `source_bundle=${badContext}`
: value === `SOURCE_ARCHIVE_SHA256=${sourceReceipt.archive_sha256}` ? `SOURCE_ARCHIVE_SHA256=${badReceipt.archive_sha256}`
: value === `SOURCE_MANIFEST_SHA256=${sourceReceipt.manifest_sha256}` ? `SOURCE_MANIFEST_SHA256=${badReceipt.manifest_sha256}`
: value === `type=local,dest=${output}` ? `type=local,dest=${badOutput}` : value);
assertRejected(spawnSync("docker", badArgs, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }), `${fault} native source build`, diagnostic);
}
const tampered = readFileSync(path.join(dependencyContext, "dependencies.tar")); const tamperOffset = tampered.length - 1025; tampered[tamperOffset] = (tampered[tamperOffset] ?? 0) ^ 1; writeFileSync(path.join(dependencyContext, "dependencies.tar"), tampered);
assert.notEqual(spawnSync("docker", args, { stdio: "ignore" }).status, 0);
// Alter whitespace in the real bundled lock, preserving valid JSON and
// dependency semantics. Only the archive digest should reject this case.
const tampered = readFileSync(path.join(dependencyContext, "dependencies.tar"));
const lockBytes = execFileSync("tar", ["-xOf", dependencyTar, "package-lock.json"]);
const lockOffset = tampered.indexOf(lockBytes), whitespace = lockBytes.indexOf(" ");
assert.ok(lockOffset >= 0 && whitespace >= 0, "archive tamper target must exist");
tampered[lockOffset + whitespace] = 0x09;
writeFileSync(path.join(dependencyContext, "dependencies.tar"), tampered);
assert.notEqual(digest(path.join(dependencyContext, "dependencies.tar")), dependencyReceipt.archive_sha256);
assertRejected(spawnSync("docker", args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }), "changed dependency archive", /did not complete successfully.*exit code: 1/u);
} finally { if (registry) spawnSync("docker", ["rm", "--force", registry], { stdio: "ignore" }); rmSync(temporary, { force: true, recursive: true }); }
});
Loading