Skip to content
Open
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
138 changes: 122 additions & 16 deletions packages/agentos-toolchain/src/aospkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@

import { readFileSync, writeFileSync } from "node:fs";
import {
type AgentBlock,
type CommandTarget,
decodeMountIndex,
decodePackageManifest,
encodeMountIndex,
encodePackageManifest,
TarEntryKind,
type AgentBlock,
type CommandTarget,
type ManPage,
type PackageManifest,
type ProvidesBlock,
type TarEntry,
TarEntryKind,
} from "./generated-package-format.js";

const AOSPKG_MAGIC = Uint8Array.from([0x89, 0x41, 0x4f, 0x53]); // 0x89 'A' 'O' 'S'
Expand Down Expand Up @@ -53,24 +54,119 @@ export interface AospkgSummary {

export type DecodedAospkgManifest = PackageManifest;

/** Decode the chunk1 manifest from a package container. */
export function decodeAospkgManifest(source: Uint8Array): DecodedAospkgManifest {
const bytes = Buffer.from(source.buffer, source.byteOffset, source.byteLength);
if (bytes.length < 16 || !bytes.subarray(0, 4).equals(Buffer.from(AOSPKG_MAGIC))) {
function aospkgManifestChunk(source: Uint8Array): {
bytes: Buffer;
manifestEnd: number;
manifestPayload: Buffer;
} {
const bytes = Buffer.from(
source.buffer,
source.byteOffset,
source.byteLength,
);
if (
bytes.length < 16 ||
!bytes.subarray(0, 4).equals(Buffer.from(AOSPKG_MAGIC))
) {
throw new Error("invalid .aospkg header");
}
if (bytes.readUInt16LE(4) !== AOSPKG_FORMAT_VERSION) {
throw new Error(`unsupported .aospkg format version: ${bytes.readUInt16LE(4)}`);
throw new Error(
`unsupported .aospkg format version: ${bytes.readUInt16LE(4)}`,
);
}
const manifestLength = bytes.readUInt32LE(8);
const manifestEnd = 16 + manifestLength;
const manifestStart = 16;
const manifestEnd = manifestStart + manifestLength;
if (manifestLength < 2 || manifestEnd > bytes.length) {
throw new Error("invalid .aospkg manifest range");
}
const version = bytes.readUInt16LE(16);
const payload = bytes.subarray(18, manifestEnd);
if (version === PACKAGE_MANIFEST_VERSION) return decodePackageManifest(payload);
throw new Error(`unsupported package manifest version: ${version}`);
if (bytes.readUInt16LE(manifestStart) !== PACKAGE_MANIFEST_VERSION) {
throw new Error(
`unsupported package manifest version: ${bytes.readUInt16LE(manifestStart)}`,
);
}
return {
bytes,
manifestEnd,
manifestPayload: bytes.subarray(manifestStart + 2, manifestEnd),
};
}

function aospkgChunks(source: Uint8Array): {
manifestPayload: Buffer;
indexPayload: Buffer;
} {
const { bytes, manifestEnd, manifestPayload } = aospkgManifestChunk(source);
const indexLength = bytes.readUInt32LE(12);
const indexEnd = manifestEnd + indexLength;
if (indexLength < 2 || indexEnd > bytes.length) {
throw new Error("invalid .aospkg mount index range");
}
if (bytes.readUInt16LE(manifestEnd) !== PACKAGE_MANIFEST_VERSION) {
throw new Error(
`unsupported mount index version: ${bytes.readUInt16LE(manifestEnd)}`,
);
}
return {
manifestPayload,
indexPayload: bytes.subarray(manifestEnd + 2, indexEnd),
};
}

/** Decode the chunk1 manifest from a package container. */
export function decodeAospkgManifest(
source: Uint8Array,
): DecodedAospkgManifest {
return decodePackageManifest(aospkgManifestChunk(source).manifestPayload);
}

/**
* Assert that a packed command package contains its complete declared command
* surface and that every projected target carries an executable mode bit.
*/
export function verifyAospkgCommands(
source: Uint8Array,
expectedCommands: readonly string[],
): DecodedAospkgManifest {
const chunks = aospkgChunks(source);
const manifest = decodePackageManifest(chunks.manifestPayload);
const expected = [...expectedCommands].sort(byteCompare);
const actual = manifest.commands
.map((target) => target.command)
.sort(byteCompare);
if (
expected.length !== actual.length ||
expected.some((command, index) => command !== actual[index])
) {
throw new Error(
`packed command set does not match declarations: expected=${expected.join(",")} ` +
`actual=${actual.join(",")}`,
);
}

const entries = new Map(
decodeMountIndex(chunks.indexPayload).tarEntries.map((entry) => [
entry.path,
entry,
]),
);
for (const target of manifest.commands) {
const path = `/${target.entry.replace(/^\/+/, "")}`;
const entry = entries.get(path);
if (entry === undefined) {
throw new Error(
`packed command ${target.command} targets missing entry ${path}`,
);
}
if (entry.kind === TarEntryKind.Directory || (entry.mode & 0o111) === 0) {
throw new Error(
`packed command ${target.command} targets non-executable entry ${path} ` +
`(mode ${(entry.mode & 0o7777).toString(8)})`,
);
}
}
return manifest;
}

interface SourceManifestJson {
Expand Down Expand Up @@ -106,7 +202,10 @@ interface RawTarMember {

/** Pack `sourceTar` into a `.aospkg` at `dest`. The source
* `agentos-package.json` must carry `name` and `version`. */
export function packAospkgFromTar(sourceTar: string, dest: string): AospkgSummary {
export function packAospkgFromTar(
sourceTar: string,
dest: string,
): AospkgSummary {
const source = readFileSync(sourceTar);
const { bytes, summary } = packAospkgFromTarBytes(source);
writeFileSync(dest, bytes);
Expand Down Expand Up @@ -288,7 +387,11 @@ function indexEntry(member: RawTarMember): TarEntry | undefined {
linkTarget: member.linkTarget,
};
}
if (member.typeflag === "0" || member.typeflag === "\0" || member.typeflag === "7") {
if (
member.typeflag === "0" ||
member.typeflag === "\0" ||
member.typeflag === "7"
) {
return {
...base,
kind: TarEntryKind.File,
Expand Down Expand Up @@ -387,7 +490,10 @@ function manPagesFromIndex(sortedPaths: string[]): ManPage[] {
if (parts.length !== 2) return [];
return [{ section: parts[0], page: parts[1] }];
})
.sort((a, b) => byteCompare(a.section, b.section) || byteCompare(a.page, b.page));
.sort(
(a, b) =>
byteCompare(a.section, b.section) || byteCompare(a.page, b.page),
);
}

function isProjectableCommandName(name: string): boolean {
Expand Down
28 changes: 22 additions & 6 deletions packages/agentos-toolchain/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import {
execFileSync,
} from "node:child_process";
import { execFileSync } from "node:child_process";
import {
chmodSync,
cpSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { join, resolve } from "node:path";
import { packAospkgFromTar } from "./aospkg.js";
import { readManifest, unscopedName } from "./manifest.js";
import { packAospkgFromTar, verifyAospkgCommands } from "./aospkg.js";
import {
declaredCommandNames,
readManifest,
unscopedName,
} from "./manifest.js";

export interface BuildResult {
name: string;
Expand Down Expand Up @@ -106,6 +108,17 @@ export function build(packageDirInput?: string): BuildResult {
.map((entry) => entry.name)
.sort()
: [];
const declaredCommands = declaredCommandNames(srcManifest);
if (
commands.length > 0 &&
(commands.length !== declaredCommands.length ||
commands.some((command, index) => command !== declaredCommands[index]))
) {
throw new Error(
`refusing partial command package ${name}: declared=${declaredCommands.join(",")} ` +
`staged=${commands.join(",")}`,
);
}

const outDir = join(packageDir, "dist", "package");
rmSync(outDir, { recursive: true, force: true });
Expand Down Expand Up @@ -149,6 +162,9 @@ export function build(packageDirInput?: string): BuildResult {
const outAospkg = join(packageDir, "dist", "package.aospkg");
rmSync(outAospkg, { force: true });
packAospkgFromTar(outTar, outAospkg);
if (commands.length > 0) {
verifyAospkgCommands(readFileSync(outAospkg), declaredCommands);
}

process.stdout.write(
commands.length > 0
Expand Down
36 changes: 24 additions & 12 deletions packages/agentos-toolchain/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
export { pack, verifyPackageDir, type PackOptions, type PackResult } from "./pack.js";
export {
type AospkgSummary,
type DecodedAospkgManifest,
decodeAospkgManifest,
packAospkgFromTar,
packAospkgFromTarBytes,
verifyAospkgCommands,
} from "./aospkg.js";
export { type BuildResult, build } from "./build.js";
export {
detectExecutableKind,
type ExecutableKind,
isNativeKind,
parseShebangInterpreter,
type ExecutableKind,
} from "./header.js";
export { stage, type StageOptions, type StageResult } from "./stage.js";
export { build, type BuildResult } from "./build.js";
export {
publish,
resolveTag,
type AgentosPackageManifest,
declaredCommandNames,
readManifest,
} from "./manifest.js";
export {
type PackOptions,
type PackResult,
pack,
verifyPackageDir,
} from "./pack.js";
export {
type PublishOptions,
type PublishResult,
publish,
resolveTag,
} from "./publish.js";
export { readManifest, type AgentosPackageManifest } from "./manifest.js";
export {
packAospkgFromTar,
packAospkgFromTarBytes,
type AospkgSummary,
} from "./aospkg.js";
export { type StageOptions, type StageResult, stage } from "./stage.js";
22 changes: 22 additions & 0 deletions packages/agentos-toolchain/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ export interface AgentosPackageManifest {
stubs?: string[];
}

/** Flat command names a built package must project, in deterministic order. */
export function declaredCommandNames(
manifest: AgentosPackageManifest | undefined,
): string[] {
const names = [
...(manifest?.commands ?? []),
...Object.keys(manifest?.aliases ?? {}),
...(manifest?.stubs ?? []),
];
const seen = new Set<string>();
for (const name of names) {
if (typeof name !== "string" || name.length === 0 || name.includes("/")) {
throw new Error(`invalid declared command name: ${JSON.stringify(name)}`);
}
if (seen.has(name)) {
throw new Error(`command is declared more than once: ${name}`);
}
seen.add(name);
}
return [...seen].sort();
}

export function readManifest(
packageDir: string,
): AgentosPackageManifest | undefined {
Expand Down
28 changes: 25 additions & 3 deletions packages/agentos-toolchain/src/publish.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { verifyAospkgCommands } from "./aospkg.js";
import { declaredCommandNames, readManifest } from "./manifest.js";

export interface PublishOptions {
packageDir: string;
Expand All @@ -24,7 +26,9 @@ export interface PublishResult {
* tag must keep resolving a deliberate release, never whatever was published
* last from a dev machine or CI branch.
*/
export function resolveTag(options: Pick<PublishOptions, "tag" | "latest">): string {
export function resolveTag(
options: Pick<PublishOptions, "tag" | "latest">,
): string {
if (options.latest) {
if (options.tag !== undefined && options.tag !== "latest") {
throw new Error(
Expand All @@ -35,7 +39,7 @@ export function resolveTag(options: Pick<PublishOptions, "tag" | "latest">): str
}
if (options.tag === "latest") {
throw new Error(
'refusing implicit `--tag latest` — pass --latest to move the latest pointer',
"refusing implicit `--tag latest` — pass --latest to move the latest pointer",
);
}
return options.tag ?? "dev";
Expand Down Expand Up @@ -92,6 +96,22 @@ export function publish(options: PublishOptions): PublishResult {
`${pkg.name} is not built (no dist/index.js in ${packageDir}) — build it first`,
);
}
const declaredCommands = declaredCommandNames(readManifest(packageDir));
if (declaredCommands.length > 0) {
const packagePath = join(packageDir, "dist", "package.aospkg");
if (!existsSync(packagePath)) {
throw new Error(
`${pkg.name} declares commands but has no built dist/package.aospkg`,
);
}
try {
verifyAospkgCommands(readFileSync(packagePath), declaredCommands);
} catch (error) {
throw new Error(
`refusing to publish invalid command artifact for ${pkg.name}: ${String(error)}`,
);
}
}

const inPnpmWorkspace =
findUp(packageDir, "pnpm-workspace.yaml") !== undefined;
Expand All @@ -106,7 +126,9 @@ export function publish(options: PublishOptions): PublishResult {
const result = spawnSync(pm, args, { cwd: packageDir, stdio: "inherit" });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${pm} publish failed for ${pkg.name} (exit ${result.status})`);
throw new Error(
`${pm} publish failed for ${pkg.name} (exit ${result.status})`,
);
}
return { name: pkg.name, version: pkg.version, tag };
}
Loading
Loading