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/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"
+]