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
4 changes: 2 additions & 2 deletions runtime-images/daimon/publish-inputs.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"version": "0.162.1"
},
"daimon": {
"commit": "e3eb84209cb6fd6bd8d6e9862697796df88887c4",
"package_version": "0.2.8",
"commit": "5e8bffc10779838e3ff0b6687de06db6b6fde02a",
"package_version": "0.2.9",
"source": "https://github.com/noopolis/daimon-harness.git"
},
"image": "noopolis/spawnfile-runtime-daimon",
Expand Down
12 changes: 6 additions & 6 deletions runtimes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
runtimes:
daimon:
remote: git@github.com:noopolis/daimon-harness.git
ref: e3eb84209cb6fd6bd8d6e9862697796df88887c4
ref: 5e8bffc10779838e3ff0b6687de06db6b6fde02a
default_branch: main
install:
kind: container_image
image: noopolis/spawnfile-runtime-daimon
tag: 0.2.8-e3eb842
digest: sha256:2a98082125bd14dc907fc9b126069b1735e66315f78439d1bd7b27dc258c7ee4
tag: 0.2.9-5e8bffc
digest: sha256:a8f385f57d06f81b0f64e61b2a455e9753216898eff333174da799c5d0aa7184
capability_receipts:
amd64: sha256:8730f5e2f03bf66ea89906da0b91b9efbcf23b6abb550a8b82c00b0226d53072
arm64: sha256:17bbc1fe023423342423f009871fa017f31a306941bac96a358b2fb0a5e09558
contract_manifest_sha256: sha256:e39551277c6727c8807b1247e759f57addcee3f17f1405ebc927179647d64de6
amd64: sha256:c66fa10b3d186830bba369b96f8636ec8fe3c28afa9724beadf5ed983ebb4b09
arm64: sha256:7deca0034e8f81fc7ec0cb822f1251a98c98a4ccfbad36b62ffc53fea66ae8fe
contract_manifest_sha256: sha256:9f91388983cc5308137ba0259302b9faec75a9f0e032b2a031b6a774ff3f409f
status: active

openclaw:
Expand Down
9 changes: 5 additions & 4 deletions scripts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ tools live in `../archive/legacy-worktree-tools/` and are not active helpers.
- `publish-daimon-runtime.ts` is the CI publication path. It reads only the
checked-in `runtime-images/daimon/publish-inputs.json` (full Daimon commit,
per-architecture AGY pins, Codex pin; Grok comes from the vendored manifest),
refuses a packed manifest that differs from the vendored contract, and never
refuses a packed manifest that differs from the explicit publication
`contract_manifest_sha256` (or vendored contract for legacy inputs), and never
moves an existing tag. Its receipts carry `mode: "ci-published"` provenance.
Re-vendoring the contract manifest must bump `daimon.commit` to the vendored
Daimon commit in the same change; the `runtime-images` PR dry run fails
otherwise.
A publication may precede the compiler/runtime re-pin by setting the explicit
manifest digest alongside `daimon.commit`; the Dockerfile still validates
contract semantics and every engine artifact pin.
The published identity is pinned into `runtimes.yaml` by a reviewed PR, never
written by the script.
- Clean Git remains the default source mode. Explicit Daimon archive mode
Expand Down
6 changes: 4 additions & 2 deletions scripts/build-local-daimon-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,10 @@ test("Dockerfile contract validation accepts the vendored manifest and labels ea
try {
const run = (file: string): number | null => spawnSync(process.execPath, ["-e", code, file]).status;
assert.equal(run(fileURLToPath(manifest)), 0, "Dockerfile rejects the pinned contract");
for (const [before, after] of [['"version":"noopolis.daimon.runtime-contract-manifest.v5"', '"version":"noopolis.daimon.runtime-contract-manifest.v3"'], ['"terminalReceiptHorizon":2048', '"terminalReceiptHorizon":0']]) {
writeFileSync(path.join(root, "invalid.json"), readFileSync(manifest, "utf8").replace(before!, after!));
for (const [before, after] of [['"version":"noopolis.daimon.runtime-contract-manifest.v6"', '"version":"noopolis.daimon.runtime-contract-manifest.v3"'], ['"terminalReceiptHorizon":2048', '"terminalReceiptHorizon":0']]) {
const source = readFileSync(manifest, "utf8");
assert.ok(source.includes(before!), `mutation target missing: ${before}`);
writeFileSync(path.join(root, "invalid.json"), source.replace(before!, after!));
assert.equal(run(path.join(root, "invalid.json")), 1, `invalid contract must fail: ${after}`);
}
const lines = dockerfile.slice(dockerfile.indexOf("FROM daimon_${DAIMON_DEPENDENCY_MODE} AS build")).split("\n");
Expand Down
12 changes: 9 additions & 3 deletions scripts/daimon-image-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ type Material = {
directoryMode: number; fileMode: number;
};
const readJson = (file: string): unknown => JSON.parse(readFileSync(new URL(file, import.meta.url), "utf8"));
const v5 = readJson("../src/runtime/daimon/contract-manifest.json") as Record<string, unknown>;
const vendored = readJson("../src/runtime/daimon/contract-manifest.json") as Record<string, unknown>;
const { engines: _engines, ...legacy } = vendored;
const v5 = { ...legacy, version: "noopolis.daimon.runtime-contract-manifest.v5" };
const mirror = readJson("../src/runtime/daimon/engine-descriptors.json") as {
engines: Record<string, { id: string; auth: { staging: string }; credentialMaterial: Material | null }>;
};
Expand All @@ -24,7 +26,7 @@ const v6 = (portableCount: 1 | 2) => {
sourceRelativePath: ".daimon-inbound/another-auth", sourceSlot: "another-auth"
} };
}
return { ...structuredClone(v5), version: "noopolis.daimon.runtime-contract-manifest.v6",
return { ...structuredClone(vendored), version: "noopolis.daimon.runtime-contract-manifest.v6",
engines, supportedEngineKinds: Object.keys(engines).sort(), engineCredentialMaterial: Object.fromEntries(
Object.values(engines).filter(e => e.auth.staging === "portable-file").map(e => [e.id, structuredClone(e.credentialMaterial!)])) };
};
Expand All @@ -48,7 +50,11 @@ const checkImageContract = (value: unknown): number | null => {
} finally { rmSync(root, { force: true, recursive: true }); }
};

test("the image accepts the pinned v5 contract", () => {
test("the image accepts the pinned v6 contract", () => {
assert.equal(checkImageContract(vendored), 0);
});

test("the image retains v5 compatibility", () => {
assert.equal(checkImageContract(v5), 0);
});

Expand Down
17 changes: 16 additions & 1 deletion scripts/daimon-publish-inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
parseDaimonPublishInputs,
renderRuntimesYamlPin,
resolvePublishedRepository,
resolvePublishedTag
resolvePublishedTag,
resolvePublishManifestSha256
} from "./daimon-publish-inputs.ts";
import { createDaimonImageBuildArgs, tagExists } from "./publish-daimon-runtime.ts";

Expand All @@ -29,6 +30,20 @@ test("checked-in publish inputs pin a full Daimon commit, both architectures, an
assert.notEqual(resolvePublishedTag(inputs), "latest");
});

test("publication uses its explicit manifest pin before the runtime re-pin", () => {
const raw = checkedIn();
const pinned = `sha256:${digest("9")}`;
const vendored = `sha256:${digest("e")}`;
const inputs = parseDaimonPublishInputs({ ...raw, contract_manifest_sha256: pinned });
assert.equal(inputs.contract_manifest_sha256, pinned);
assert.equal(resolvePublishManifestSha256(inputs, vendored), pinned);
delete raw.contract_manifest_sha256;
assert.equal(resolvePublishManifestSha256(parseDaimonPublishInputs(raw), vendored), vendored);
for (const invalid of [null, undefined, "latest", "sha256:bad"]) {
assert.throws(() => parseDaimonPublishInputs({ ...raw, contract_manifest_sha256: invalid }), /contract_manifest_sha256/u);
}
});

test("Daimon Dockerfile manifest literals accept the vendored contract", () => {
const dockerfile = readFileSync(new URL("../runtime-images/daimon/Dockerfile", import.meta.url), "utf8");
const validators = [...dockerfile.matchAll(/node -e '([^'\n]+)' \$\{RUNTIME_ROOT\}\/contract-manifest\.json/gu)];
Expand Down
9 changes: 8 additions & 1 deletion scripts/daimon-publish-inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type AgyPlatformPin = {
export type DaimonPublishInputs = {
agy: { platforms: Partial<Record<DaimonPublishArchitecture, AgyPlatformPin>>; version: string };
codex: { executable_sha256: string; version: string };
contract_manifest_sha256?: string;
daimon: { commit: string; package_version: string; source: string };
image: string;
platforms: DaimonPublishArchitecture[];
Expand Down Expand Up @@ -77,7 +78,8 @@ const httpsUrl = (value: unknown, label: string): string => {

/** Validates the checked-in publish inputs; every pin is explicit, nothing floats. */
export const parseDaimonPublishInputs = (raw: unknown): DaimonPublishInputs => {
const root = exactKeys(raw, ["agy", "codex", "daimon", "image", "platforms", "registry", "version"], "publish inputs");
const manifestKeys = isRecord(raw) && "contract_manifest_sha256" in raw ? ["contract_manifest_sha256"] : [];
const root = exactKeys(raw, ["agy", "codex", "daimon", "image", "platforms", "registry", "version", ...manifestKeys], "publish inputs");
if (root.version !== "spawnfile.daimon-runtime-publish-inputs.v1") throw new Error("publish inputs version is unsupported");
const platforms = root.platforms;
if (!Array.isArray(platforms) || platforms.length === 0 || new Set(platforms).size !== platforms.length ||
Expand All @@ -103,6 +105,7 @@ export const parseDaimonPublishInputs = (raw: unknown): DaimonPublishInputs => {
executable_sha256: matching(codex.executable_sha256, SHA256, "codex.executable_sha256"),
version: matching(codex.version, VERSION, "codex.version")
},
...(manifestKeys.length ? { contract_manifest_sha256: matching(root.contract_manifest_sha256, SHA256, "contract_manifest_sha256") } : {}),
daimon: {
commit: matching(daimon.commit, COMMIT, "daimon.commit"),
package_version: matching(daimon.package_version, VERSION, "daimon.package_version"),
Expand All @@ -115,6 +118,10 @@ export const parseDaimonPublishInputs = (raw: unknown): DaimonPublishInputs => {
};
};

/** Publication may precede the compiler/runtime re-pin; legacy inputs use the vendored digest. */
export const resolvePublishManifestSha256 = (inputs: DaimonPublishInputs, vendoredDigest: string): string =>
inputs.contract_manifest_sha256 ?? vendoredDigest;

/** One tag per pinned Daimon commit: `<package version>-<short commit>`, never `latest`. */
export const resolvePublishedTag = (inputs: DaimonPublishInputs): string =>
`${inputs.daimon.package_version}-${inputs.daimon.commit.slice(0, 7)}`;
Expand Down
7 changes: 4 additions & 3 deletions scripts/publish-daimon-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
parseDaimonPublishInputs,
renderRuntimesYamlPin,
resolvePublishedRepository,
resolvePublishedTag
resolvePublishedTag,
resolvePublishManifestSha256
} from "./daimon-publish-inputs.ts";
import type { DaimonPublishArchitecture, DaimonPublishInputs, PublishedPlatform } from "./daimon-publish-inputs.ts";

Expand Down Expand Up @@ -100,7 +101,7 @@ const buildPlatform = (options: {
const packagePath = packDaimon(options.checkout, architecture, stage);
const manifestSha256 = sha256(execFileSync("tar", ["-xOf", packagePath, "package/dist/runtime/contract-manifest.json"]));
if (manifestSha256 !== options.manifestSha256) {
throw new Error(`Daimon ${architecture} package manifest ${manifestSha256} is not the vendored contract ${options.manifestSha256}`);
throw new Error(`Daimon ${architecture} package manifest ${manifestSha256} is not the publication contract ${options.manifestSha256}`);
}
const grok = readPinnedGrokCli(architecture);
const agy = inputs.agy.platforms[architecture];
Expand Down Expand Up @@ -187,7 +188,7 @@ const main = (): void => {
process.stdout.write(`${repository}:${tag} already exists; refusing to move a published tag (pass --force to republish)\n`);
return;
}
const manifestSha256 = readFileSync(vendoredManifestDigestPath, "utf8").trim();
const manifestSha256 = resolvePublishManifestSha256(inputs, readFileSync(vendoredManifestDigestPath, "utf8").trim());
const workDirectory = mkdtempSync(path.join(os.tmpdir(), "spawnfile-daimon-publish-"));
try {
const checkout = prepareDaimonCheckout(inputs, workDirectory);
Expand Down
8 changes: 4 additions & 4 deletions scripts/vendor-daimon-grok-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type DaimonModules = {

/** `COMPONENT` is not exported, so its pattern is read from the source it is defined in; a missing definition fails vendoring. */
const skillComponentPattern = (daimonDirectory: string): string => {
const match = /^const COMPONENT = \/(.+)\/u;$/mu.exec(readFileSync(path.join(daimonDirectory, "src/runtime/grokWorkerSkills.ts"), "utf8"));
const match = /^const COMPONENT = \/(.+)\/u;$/mu.exec(readFileSync(path.join(daimonDirectory, "src/engines/grok/broker/grokWorkerSkills.ts"), "utf8"));
if (!match) throw new Error("Daimon grokWorkerSkills.ts no longer defines its COMPONENT rule as a /.../u literal");
return match[1]!;
};
Expand All @@ -55,9 +55,9 @@ const loadDaimon = async (daimonDirectory: string): Promise<DaimonModules> => {
const load = (relative: string): Promise<Record<string, unknown>> => import(pathToFileURL(path.join(daimonDirectory, relative)).href);
const [emitter, config, profile, skills] = await Promise.all([
load("scripts/emitRuntimeContractManifest.ts"),
load("src/runtime/grokBrokerWorkerConfig.ts"),
load("src/runtime/grokWorkerSandboxProfile.ts"),
load("src/runtime/grokWorkerSkills.ts")
load("src/engines/grok/broker/grokBrokerWorkerConfig.ts"),
load("src/engines/grok/broker/grokWorkerSandboxProfile.ts"),
load("src/engines/grok/broker/grokWorkerSkills.ts")
]);
return {
contractManifestArtifacts: emitter.contractManifestArtifacts as DaimonModules["contractManifestArtifacts"],
Expand Down
8 changes: 4 additions & 4 deletions src/compiler/publicDaimonHost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ describe("public Daimon host fixture", () => {
expect(agent.containerPaths.home).toBe(config.agents[0].runtimeHomePath);
expect(agent.containerPaths.skillRoots).toEqual([".agents", ".codex"].map(root => `${config.agents[0].workspacePath}/${root}/skills`));
const dockerfile = await readUtf8File(path.join(outputDirectory, "Dockerfile"));
expect(dockerfile).toContain("COPY --from=noopolis/spawnfile-runtime-daimon@sha256:2a98082125bd14dc907fc9b126069b1735e66315f78439d1bd7b27dc258c7ee4 ");
expect(dockerfile).toContain('amd64) expected="sha256:8730f5e2f03bf66ea89906da0b91b9efbcf23b6abb550a8b82c00b0226d53072"');
expect(dockerfile).toContain('arm64) expected="sha256:17bbc1fe023423342423f009871fa017f31a306941bac96a358b2fb0a5e09558"');
expect(dockerfile).toContain("COPY --from=noopolis/spawnfile-runtime-daimon@sha256:a8f385f57d06f81b0f64e61b2a455e9753216898eff333174da799c5d0aa7184 ");
expect(dockerfile).toContain('amd64) expected="sha256:c66fa10b3d186830bba369b96f8636ec8fe3c28afa9724beadf5ed983ebb4b09"');
expect(dockerfile).toContain('arm64) expected="sha256:7deca0034e8f81fc7ec0cb822f1251a98c98a4ccfbad36b62ffc53fea66ae8fe"');
});

it("compiles against an explicit local identity without changing production registry pins", async () => {
Expand Down Expand Up @@ -66,7 +66,7 @@ describe("public Daimon host fixture", () => {
`COPY --from=${LOCAL_DAIMON_IMAGE_REPOSITORY}@${digest("c")} `
);
expect(dockerfile).toContain(digest("a"));
expect(dockerfile).not.toContain("sha256:2a98082125bd14dc907fc9b126069b1735e66315f78439d1bd7b27dc258c7ee4");
expect(dockerfile).not.toContain("sha256:a8f385f57d06f81b0f64e61b2a455e9753216898eff333174da799c5d0aa7184");
await expect((await import("node:fs/promises")).readFile(path.join(outputDirectory, "spawnfile-report.json"), "utf8").then(JSON.parse)).resolves.toMatchObject({
container: { local_daimon_runtime: { registry_authority: "127.0.0.1:54321", image_reference: `${LOCAL_DAIMON_IMAGE_REPOSITORY}@${digest("c")}` } }
});
Expand Down
10 changes: 5 additions & 5 deletions src/compiler/training/broker/projection.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import { DAIMON_GROK_WORKER_SKILLS_SAMPLES } from "../../../runtime/daimon/grokW
import { renderDaimonGrokServiceConfig } from "../../containerDaimonGrokWorkerRender.js";

// Source checkout integration is explicit: no runtime dependency on Daimon and no provider calls.
const source = process.env.SPAWNFILE_DAIMON_SOURCE_DIR;
const source = process.env.DAIMON_SOURCE_ROOT ?? process.env.SPAWNFILE_DAIMON_SOURCE_DIR;
const load = (file: string) => import(/* @vite-ignore */ pathToFileURL(path.join(source!, file)).href);

describe.skipIf(!source)("clocked training slots against Daimon's actual contract", () => {
it("attests every model/effort and slot, and verifies the clocked projection receipt", async () => {
const [projectionApi, slotApi, configApi, receiptApi] = await Promise.all([
load("src/runtime/grokBrokerProjection.ts"), load("src/runtime/grokBrokerSlot.ts"),
load("src/runtime/grokBrokerWorkerConfig.ts"), load("src/runtime/grokSlotPreflightReceipt.ts")
load("src/engines/grok/broker/grokBrokerProjection.ts"), load("src/engines/grok/broker/grokBrokerSlot.ts"),
load("src/engines/grok/broker/grokBrokerWorkerConfig.ts"), load("src/engines/grok/broker/grokSlotPreflightReceipt.ts")
]);
const daimon = { ...projectionApi, GROK_ENGINE_BROKER: DAIMON_GROK_ENGINE_BROKER } as DaimonProjectionModule;
for (const count of [1, 4, 8]) for (const p of trainingSlotDescriptors(count)) {
Expand Down Expand Up @@ -49,8 +49,8 @@ describe.skipIf(!source)("clocked training slots against Daimon's actual contrac

it("binds declared skills exactly as Daimon's projection, service parser and pre-turn attestation do", async () => {
const [projectionApi, serviceApi, homeApi, skillsApi] = await Promise.all([
load("src/runtime/grokBrokerProjection.ts"), load("src/runtime/engineBrokerServiceConfig.ts"),
load("src/runtime/grokWorkerHomeAttestation.ts"), load("src/runtime/grokWorkerSkills.ts")
load("src/engines/grok/broker/grokBrokerProjection.ts"), load("src/engines/grok/broker/engineBrokerServiceConfig.ts"),
load("src/engines/grok/broker/grokWorkerHomeAttestation.ts"), load("src/engines/grok/broker/grokWorkerSkills.ts")
]);
const daimon = { ...projectionApi, GROK_ENGINE_BROKER: DAIMON_GROK_ENGINE_BROKER } as DaimonProjectionModule;
const skills = DAIMON_GROK_WORKER_SKILLS_SAMPLES[2]!.skills.map((skill) => ({ name: skill.name, files: skill.files.map((file) => ({ ...file })) }));
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ describe("runtime container install recipes", () => {
it("copies the CI-published Daimon index and checks the per-architecture receipt by default", async () => {
const recipe = await createRuntimeInstallRecipe("daimon");
expect(recipe.copyCommands).toEqual([
`COPY --from=noopolis/spawnfile-runtime-daimon@sha256:2a98082125bd14dc907fc9b126069b1735e66315f78439d1bd7b27dc258c7ee4 ${RUNTIME_INSTALL_ROOT}/daimon ${RUNTIME_INSTALL_ROOT}/daimon`
`COPY --from=noopolis/spawnfile-runtime-daimon@sha256:a8f385f57d06f81b0f64e61b2a455e9753216898eff333174da799c5d0aa7184 ${RUNTIME_INSTALL_ROOT}/daimon ${RUNTIME_INSTALL_ROOT}/daimon`
]);
expect(recipe.commands[0]).toContain('case "$arch" in amd64) expected="sha256:8730f5e2f03bf66ea89906da0b91b9efbcf23b6abb550a8b82c00b0226d53072" ;; arm64) expected="sha256:17bbc1fe023423342423f009871fa017f31a306941bac96a358b2fb0a5e09558" ;;');
expect(recipe.commands[0]).toContain('case "$arch" in amd64) expected="sha256:c66fa10b3d186830bba369b96f8636ec8fe3c28afa9724beadf5ed983ebb4b09" ;; arm64) expected="sha256:7deca0034e8f81fc7ec0cb822f1251a98c98a4ccfbad36b62ffc53fea66ae8fe" ;;');
});

it("installs overridden runtime packages from the vendored tarball path in the Pi recipe", async () => {
Expand Down Expand Up @@ -211,7 +211,7 @@ describe("runtime container install recipes", () => {
process.env.SPAWNFILE_DAIMON_RUNTIME_BASE_IMAGE = "noopolis/spawnfile-runtime-daimon:legacy";
const recipe = await createRuntimeInstallRecipe("daimon");
expect(recipe.baseImage).toBeUndefined();
expect(recipe.copyCommands[0]).toContain("noopolis/spawnfile-runtime-daimon@sha256:2a98082125bd14dc907fc9b126069b1735e66315f78439d1bd7b27dc258c7ee4");
expect(recipe.copyCommands[0]).toContain("noopolis/spawnfile-runtime-daimon@sha256:a8f385f57d06f81b0f64e61b2a455e9753216898eff333174da799c5d0aa7184");
});

it("omits NOOPOLIS_RUN_ID from every recipe's env when unset", async () => {
Expand Down
Loading
Loading