diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 5628848c34..339501aa80 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -20,7 +20,7 @@ jobs: node-version: 24 cache: pnpm cache-dependency-path: pnpm-lock.yaml - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.1 with: targets: wasm32-wasip1 - uses: dtolnay/rust-toolchain@nightly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3793323bd..38878dee35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: node-version: 24 cache: pnpm cache-dependency-path: pnpm-lock.yaml - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.1 with: components: rustfmt - run: | @@ -134,7 +134,7 @@ jobs: node-version: 24 cache: pnpm cache-dependency-path: pnpm-lock.yaml - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.1 with: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 00fb3e5178..e89b0d76b5 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -421,7 +421,7 @@ jobs: node-version: 24 cache: pnpm cache-dependency-path: pnpm-lock.yaml - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.1 - uses: Swatinem/rust-cache@v2 with: workspaces: . -> target diff --git a/packages/core/scripts/stage-default-software.mjs b/packages/core/scripts/stage-default-software.mjs index 80e2f111b7..6c3f65a78d 100644 --- a/packages/core/scripts/stage-default-software.mjs +++ b/packages/core/scripts/stage-default-software.mjs @@ -1,26 +1,17 @@ -import { cpSync, mkdirSync, rmSync, statSync } from "node:fs"; +import { cpSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -const DEFAULT_SOFTWARE = [ - "coreutils", - "sed", - "grep", - "gawk", - "findutils", - "diffutils", - "tar", - "gzip", -]; - const packageRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = join(packageRoot, "..", ".."); +const listPath = join(repoRoot, "software", "default-software.json"); const outputDir = join(packageRoot, "dist", "default-software"); +const defaultSoftware = JSON.parse(readFileSync(listPath, "utf8")); rmSync(outputDir, { recursive: true, force: true }); mkdirSync(outputDir, { recursive: true }); -for (const name of DEFAULT_SOFTWARE) { +for (const name of defaultSoftware) { const source = join(repoRoot, "software", name, "dist", "package.aospkg"); const size = statSync(source).size; if (size === 0) { @@ -28,7 +19,10 @@ for (const name of DEFAULT_SOFTWARE) { } cpSync(source, join(outputDir, `${name}.aospkg`)); } +// The runtime reads the list from the staged directory, so the published +// package carries it next to the artifacts. +cpSync(listPath, join(outputDir, "default-software.json")); process.stdout.write( - `staged ${DEFAULT_SOFTWARE.length} default software artifacts -> ${outputDir}\n`, + `staged ${defaultSoftware.length} default software artifacts -> ${outputDir}\n`, ); diff --git a/packages/core/src/default-software.ts b/packages/core/src/default-software.ts index 2bc0551168..5c222e3d13 100644 --- a/packages/core/src/default-software.ts +++ b/packages/core/src/default-software.ts @@ -1,21 +1,12 @@ +import { readFileSync } from "node:fs"; import type { SoftwarePackageRef } from "./agentos-package.js"; -const DEFAULT_SOFTWARE = [ - "coreutils", - "sed", - "grep", - "gawk", - "findutils", - "diffutils", - "tar", - "gzip", -] as const; - /** - * Default software for a bare `AgentOs.create()`. These immutable `.aospkg` - * files are vendored into the Core package at build time; runtime resolution - * never consults npm or scans node_modules. Opt out with - * `defaultSoftware: false`; add more trusted paths via `software`. + * Default software for a bare `AgentOs.create()`. The build stages the list + * from `software/default-software.json` and its immutable `.aospkg` files into + * the Core package; runtime resolution never consults npm or scans + * node_modules. Opt out with `defaultSoftware: false`; add more trusted paths + * via `software`. */ export function resolveDefaultSoftware(): SoftwarePackageRef[] { // Published consumers execute this module from dist/, while Vitest executes @@ -25,8 +16,11 @@ export function resolveDefaultSoftware(): SoftwarePackageRef[] { const artifactDirectory = moduleDirectory.pathname.endsWith("/src/") ? new URL("../dist/default-software/", moduleDirectory) : new URL("./default-software/", moduleDirectory); + const names: string[] = JSON.parse( + readFileSync(new URL("default-software.json", artifactDirectory), "utf8"), + ); - return DEFAULT_SOFTWARE.map((name) => ({ + return names.map((name) => ({ packagePath: new URL(`${name}.aospkg`, artifactDirectory).pathname, })); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000..b0cd3b63c5 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.98.1" +components = ["clippy", "rustfmt"] +profile = "minimal" diff --git a/scripts/publish/src/ci/bin.ts b/scripts/publish/src/ci/bin.ts index 3c4655d5b2..bcd5aa1caf 100644 --- a/scripts/publish/src/ci/bin.ts +++ b/scripts/publish/src/ci/bin.ts @@ -253,7 +253,7 @@ program // --------------------------------------------------------------------------- program .command("stage-software") - .description("Stage immutable .aospkg files and their manifest") + .description("Stage immutable .aospkg files, their manifest, and the default software list") .requiredOption("--output ", "Local artifact directory to replace") .action((opts) => { const repoRoot = findRepoRoot(); diff --git a/scripts/publish/src/lib/software-artifacts.test.ts b/scripts/publish/src/lib/software-artifacts.test.ts index 2949ff312e..90cd81747f 100644 --- a/scripts/publish/src/lib/software-artifacts.test.ts +++ b/scripts/publish/src/lib/software-artifacts.test.ts @@ -34,6 +34,10 @@ test("stages deterministic digest-addressed .aospkg artifacts", () => { ); const bytes = Buffer.from("aospkg fixture"); writeFileSync(join(packageDir, "dist", "package.aospkg"), bytes); + writeFileSync( + join(root, "software", "default-software.json"), + JSON.stringify(["example"]), + ); const result = stageSoftwareArtifacts( root, @@ -60,6 +64,10 @@ test("stages deterministic digest-addressed .aospkg artifacts", () => { JSON.parse(readFileSync(result.manifestPath, "utf8")), result.manifest, ); + assert.deepEqual( + JSON.parse(readFileSync(result.defaultSoftwarePath, "utf8")), + result.manifest, + ); } finally { rmSync(root, { recursive: true, force: true }); } @@ -119,3 +127,34 @@ test("refuses output outside the repository or with an ambiguous name", () => { rmSync(root, { recursive: true, force: true }); } }); + +test("fails when a default package is not in the software catalog", () => { + const root = mkdtempSync(join(tmpdir(), "agentos-software-artifacts-")); + try { + const packageDir = join(root, "software", "example"); + mkdirSync(join(packageDir, "dist"), { recursive: true }); + writeFileSync( + join(packageDir, "package.json"), + JSON.stringify({ name: "@agentos-software/example", version: "0.0.1" }), + ); + writeFileSync( + join(packageDir, "agentos-package.json"), + JSON.stringify({ commands: ["example"] }), + ); + writeFileSync(join(packageDir, "dist", "package.aospkg"), "aospkg fixture"); + writeFileSync( + join(root, "software", "default-software.json"), + JSON.stringify(["coreutils"]), + ); + assert.throws( + () => + stageSoftwareArtifacts( + root, + join(root, "target", "software-artifacts"), + ), + /default software coreutils is not in the software catalog/, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/scripts/publish/src/lib/software-artifacts.ts b/scripts/publish/src/lib/software-artifacts.ts index b660f7610e..4f7deb25ad 100644 --- a/scripts/publish/src/lib/software-artifacts.ts +++ b/scripts/publish/src/lib/software-artifacts.ts @@ -27,6 +27,9 @@ export interface SoftwareArtifactManifest { export interface StageSoftwareArtifactsResult { manifest: SoftwareArtifactManifest; manifestPath: string; + /** Manifest entries of the default software, in install order. */ + defaultSoftware: SoftwareArtifactManifest; + defaultSoftwarePath: string; outputDir: string; } @@ -129,5 +132,24 @@ export function stageSoftwareArtifacts( const manifest: SoftwareArtifactManifest = { schemaVersion: 1, artifacts }; const manifestPath = join(outputDir, "manifest.json"); writeFileSync(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`); - return { manifest, manifestPath, outputDir }; + + const defaultSoftwareNames: string[] = JSON.parse( + readFileSync(join(softwareRoot, "default-software.json"), "utf8"), + ); + const defaultSoftware: SoftwareArtifactManifest = { + schemaVersion: 1, + artifacts: defaultSoftwareNames.map((name) => { + const artifact = artifacts.find((candidate) => candidate.name === name); + if (!artifact) { + throw new Error(`default software ${name} is not in the software catalog`); + } + return artifact; + }), + }; + const defaultSoftwarePath = join(outputDir, "default-software.json"); + writeFileSync( + defaultSoftwarePath, + `${JSON.stringify(defaultSoftware, null, "\t")}\n`, + ); + return { manifest, manifestPath, defaultSoftware, defaultSoftwarePath, outputDir }; } diff --git a/software/default-software.json b/software/default-software.json new file mode 100644 index 0000000000..c90319860b --- /dev/null +++ b/software/default-software.json @@ -0,0 +1,10 @@ +[ + "coreutils", + "sed", + "grep", + "gawk", + "findutils", + "diffutils", + "tar", + "gzip" +]