From 420be1b33fdb73f106fe54dda21ae4403714354f Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Wed, 15 Jul 2026 13:52:17 -0600 Subject: [PATCH 1/3] feat(agent-interface): add content-store artifact locator --- .changeset/add-content-store-locator.md | 5 + .../agent-candidate-artifact-schema.test.ts | 130 ++++++++++++++++-- .../src/agent-candidate-artifact-schema.ts | 38 ++++- .../agent-interface/src/agent-candidate.ts | 8 +- 4 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 .changeset/add-content-store-locator.md diff --git a/.changeset/add-content-store-locator.md b/.changeset/add-content-store-locator.md new file mode 100644 index 0000000..91fbfaf --- /dev/null +++ b/.changeset/add-content-store-locator.md @@ -0,0 +1,5 @@ +--- +"@tangle-network/agent-interface": minor +--- + +Add a strict generic content-store locator for externally stored candidate artifacts. diff --git a/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts b/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts index 11bf421..6c03aaf 100644 --- a/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts +++ b/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts @@ -11,6 +11,14 @@ import { agentCandidateWorkspaceSnapshotEvidenceSchema, } from "./agent-candidate-artifact-schema.js"; import { candidateGit, candidateSha } from "./agent-candidate.test-fixture.js"; +import type { + AgentCandidateArtifactLocator, + AgentCandidateContentStoreLocator, +} from "./index.js"; +import { + agentCandidateArtifactLocatorSchema, + agentCandidateContentStoreLocatorSchema, +} from "./index.js"; const failClosedResources: AgentCandidateResources = { failOnError: true }; // @ts-expect-error Candidate resources may never silently degrade. @@ -19,20 +27,47 @@ void failClosedResources; void missingFailClosedPolicy; describe("candidate artifact schemas", () => { - it("accepts only closed, non-URL artifact locators", () => { - expect(() => - agentCandidateArtifactRefSchema.parse({ - locator: { - kind: "s3", - bucket: "agent-candidate-artifacts", - key: "runs/r360/trace.json", - region: "us-east-1", - }, - sha256: candidateSha("1"), - byteLength: 42, + it("accepts S3, IPFS, and content-store artifact locators", () => { + const contentStoreLocator: AgentCandidateContentStoreLocator = { + kind: "content-store", + store: "candidate-artifacts-2", + }; + const locators: AgentCandidateArtifactLocator[] = [ + { + kind: "s3", + bucket: "agent-candidate-artifacts", + key: "runs/r360/trace.json", + region: "us-east-1", + }, + { + kind: "ipfs", + cid: `Qm${"1".repeat(44)}`, + path: "runs/r360/trace.json", + }, + contentStoreLocator, + ]; + + for (const locator of locators) { + expect(agentCandidateArtifactLocatorSchema.parse(locator)).toEqual( + locator, + ); + expect( + agentCandidateArtifactRefSchema.parse({ + locator, + sha256: candidateSha("1"), + byteLength: 42, + }), + ).toEqual({ locator, sha256: candidateSha("1"), byteLength: 42 }); + } + expect( + agentCandidateContentStoreLocatorSchema.parse({ + kind: "content-store", + store: "a".repeat(63), }), - ).not.toThrow(); + ).toEqual({ kind: "content-store", store: "a".repeat(63) }); + }); + it("rejects arbitrary URL locator kinds", () => { for (const uri of [ "file:///etc/passwd", "http://169.254.169.254/latest/meta-data", @@ -50,6 +85,77 @@ describe("candidate artifact schemas", () => { } }); + it("rejects unsafe content-store names and unknown locator fields", () => { + for (const store of [ + "", + "-candidate-artifacts", + "candidate-artifacts-", + "Candidate-Artifacts", + "candidate_artifacts", + "candidate.artifacts", + "candidate/artifacts", + "candidate\\artifacts", + "../candidate-artifacts", + "https://artifacts.example.com", + "s3://candidate-artifacts", + "sk-live-abcdefghijklmnop", + "ghp-abcdefghijklmnop", + ["xoxb", "123456789012", "123456789012", "abcdefghijklmnopqrstuvwx"].join( + "-", + ), + "xapp-1-abcdefghijklmnopqrstuvwx", + "xwfp-1-abcdefghijklmnopqrstuvwx", + "glpat-abcdefghijklmnopqrst", + "gloas-abcdefghijklmnopqrst", + "glrtr-abcdefghijklmnopqrst", + "glft-abcdefghijklmnopqrst", + "glwt-abcdefghijklmnopqrst", + "token-abcdefghijkl", + "tokens-prod", + "api-key-abcdefghijkl", + "api-keys-prod", + "access-key-abcdefghijkl", + "access-keys-prod", + "private-key-abcdefghijkl", + "private-keys-prod", + "secret-abcdefghijkl", + "secrets-prod", + "password-abcdefghijkl", + "passwords-prod", + "credentials-abcdefghijkl", + "authorization-abcdefghijkl", + "bearer-abcdefghijkl", + "cookie-abcdefghijkl", + "cookies-prod", + "database-url-abcdefghijkl", + "dsn-abcdefghijkl", + "pat-abcdefghijkl", + "pats-prod", + "a".repeat(64), + ]) { + expect(() => + agentCandidateContentStoreLocatorSchema.parse({ + kind: "content-store", + store, + }), + ).toThrow(); + } + + for (const extra of [ + { url: "https://artifacts.example.com" }, + { path: "runs/r360/trace.json" }, + { token: "sk-live-abcdefghijklmnop" }, + ]) { + expect(() => + agentCandidateContentStoreLocatorSchema.parse({ + kind: "content-store", + store: "candidate-artifacts", + ...extra, + }), + ).toThrow(); + } + }); + it("rejects traversal-shaped artifact keys and reserved roots", () => { for (const key of [ "../secret", diff --git a/packages/agent-interface/src/agent-candidate-artifact-schema.ts b/packages/agent-interface/src/agent-candidate-artifact-schema.ts index bad5fe7..7b0ebd1 100644 --- a/packages/agent-interface/src/agent-candidate-artifact-schema.ts +++ b/packages/agent-interface/src/agent-candidate-artifact-schema.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import type { AgentCandidateArtifactRef, AgentCandidateCapturedArtifact, + AgentCandidateContentStoreLocator, AgentCandidateEmbeddedArtifact, AgentCandidateFileMount, AgentCandidateGitHubResource, @@ -27,6 +28,21 @@ const base64Alphabet = const s3BucketPattern = /^(?!\d+\.\d+\.\d+\.\d+$)[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/; const awsRegionPattern = /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/; const ipfsCidPattern = /^(?:Qm[1-9A-HJ-NP-Za-km-z]{44}|b[a-z2-7]{20,})$/; +const contentStoreSlugPattern = + /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; +const credentialBearingSlugNamePattern = + /(?:^|-)(?:api-?keys?|access-?keys?|private-?keys?|tokens?|secrets?|passwords?|credentials?|authorizations?|bearers?|cookies?|database-?urls?|dsns?|pats?)(?:-|$)/; +// These token formats remain valid lowercase slugs after delimiter restrictions. +const slugShapedCredentialPattern = + /^(?:xox[abeoprs]|xapp|xwfp|glpat|gloas|gldt|glrt|glrtr|glptt|glcbt|glimt|glagent|glsoat|glffct|glft|glwt|grt|sq0atp|sq0csp)-[a-z0-9-]{8,}$/; + +function looksLikeContentStoreCredential(value: string): boolean { + return ( + looksLikeCredential(value) || + credentialBearingSlugNamePattern.test(value) || + slugShapedCredentialPattern.test(value) + ); +} function decodedBase64ByteLength(value: string): number { if (value.length === 0) return 0; @@ -101,9 +117,29 @@ export const agentCandidateIpfsLocatorSchema = z }) .strict() satisfies z.ZodType; +export const agentCandidateContentStoreLocatorSchema = z + .object({ + kind: z.literal("content-store"), + store: z + .string() + .regex( + contentStoreSlugPattern, + "content store must be a 1-63 character lowercase slug", + ) + .refine( + (value) => !looksLikeContentStoreCredential(value), + "content store must not contain credentials", + ), + }) + .strict() satisfies z.ZodType; + export const agentCandidateArtifactLocatorSchema = z.discriminatedUnion( "kind", - [agentCandidateS3LocatorSchema, agentCandidateIpfsLocatorSchema], + [ + agentCandidateS3LocatorSchema, + agentCandidateIpfsLocatorSchema, + agentCandidateContentStoreLocatorSchema, + ], ); export const agentCandidateArtifactRefSchema = z diff --git a/packages/agent-interface/src/agent-candidate.ts b/packages/agent-interface/src/agent-candidate.ts index 133097e..a7f5548 100644 --- a/packages/agent-interface/src/agent-candidate.ts +++ b/packages/agent-interface/src/agent-candidate.ts @@ -40,10 +40,16 @@ export interface AgentCandidateIpfsLocator { path?: string; } +export interface AgentCandidateContentStoreLocator { + kind: "content-store"; + store: string; +} + /** Closed locator set whose resolvers cannot choose arbitrary URL schemes. */ export type AgentCandidateArtifactLocator = | AgentCandidateS3Locator - | AgentCandidateIpfsLocator; + | AgentCandidateIpfsLocator + | AgentCandidateContentStoreLocator; /** Content-addressed artifact stored outside the candidate manifest. */ export interface AgentCandidateArtifactRef { From 742f597f4ed94a1d2ff8d58541e266017aeed357 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Wed, 15 Jul 2026 13:52:23 -0600 Subject: [PATCH 2/3] fix(release): make installed packages repackable --- .changeset/fix-package-repack-lifecycle.md | 12 + .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 2 + package.json | 3 +- packages/agent-core/package.json | 2 +- packages/agent-interface/package.json | 2 +- .../agent-provider-cli-bridge/package.json | 2 +- .../agent-provider-computesdk/package.json | 2 +- packages/agent-provider-daytona/package.json | 2 +- packages/agent-provider-e2b/package.json | 2 +- packages/agent-provider-tangle/package.json | 2 +- packages/agent-provider-testkit/package.json | 2 +- scripts/check-package-artifacts.mjs | 378 ++++++++++++++++++ 13 files changed, 403 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-package-repack-lifecycle.md create mode 100644 scripts/check-package-artifacts.mjs diff --git a/.changeset/fix-package-repack-lifecycle.md b/.changeset/fix-package-repack-lifecycle.md new file mode 100644 index 0000000..bf42b37 --- /dev/null +++ b/.changeset/fix-package-repack-lifecycle.md @@ -0,0 +1,12 @@ +--- +"@tangle-network/agent-core": patch +"@tangle-network/agent-interface": patch +"@tangle-network/agent-provider-cli-bridge": patch +"@tangle-network/agent-provider-computesdk": patch +"@tangle-network/agent-provider-daytona": patch +"@tangle-network/agent-provider-e2b": patch +"@tangle-network/agent-provider-tangle": patch +"@tangle-network/agent-provider-testkit": patch +--- + +Build only before publishing so installed package artifacts can be repacked with lifecycle scripts enabled. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e65210e..1123fe2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,3 +19,4 @@ jobs: - run: pnpm -r build - run: pnpm -r check-types - run: pnpm -r test + - run: pnpm check:package-artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8b3202..5d18b76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,8 @@ jobs: - run: | npm install -g npm@11.18.0 test "$(npm --version)" = "11.18.0" + - run: pnpm -r build + - run: pnpm check:package-artifacts - uses: changesets/action@v1 with: version: pnpm changeset:version diff --git a/package.json b/package.json index 978b3d2..a98bde2 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,12 @@ "packageManager": "pnpm@10.23.0", "scripts": { "build": "pnpm -r build", + "check:package-artifacts": "node scripts/check-package-artifacts.mjs", "check-types": "pnpm -r check-types", "test": "pnpm -r test", "changeset": "changeset", "changeset:version": "changeset version", - "changeset:publish": "pnpm -r build && changeset publish" + "changeset:publish": "changeset publish" }, "devDependencies": { "@changesets/cli": "^2.31.0" diff --git a/packages/agent-core/package.json b/packages/agent-core/package.json index 05b1c13..f000415 100644 --- a/packages/agent-core/package.json +++ b/packages/agent-core/package.json @@ -165,7 +165,7 @@ "scripts": { "build": "tsdown", "check-types": "tsc --noEmit", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run", "test:watch": "vitest" }, diff --git a/packages/agent-interface/package.json b/packages/agent-interface/package.json index 77d95b7..3f962f6 100644 --- a/packages/agent-interface/package.json +++ b/packages/agent-interface/package.json @@ -50,7 +50,7 @@ "clean": "rm -rf dist tsconfig.tsbuildinfo", "test": "vitest run", "test:watch": "vitest", - "prepare": "pnpm run build" + "prepublishOnly": "pnpm run build" }, "dependencies": { "@noble/hashes": "1.8.0", diff --git a/packages/agent-provider-cli-bridge/package.json b/packages/agent-provider-cli-bridge/package.json index c727fb8..2f7d957 100644 --- a/packages/agent-provider-cli-bridge/package.json +++ b/packages/agent-provider-cli-bridge/package.json @@ -40,7 +40,7 @@ "build": "tsc -p tsconfig.json", "check-types": "tsc --noEmit", "clean": "rm -rf dist", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run" }, "dependencies": { diff --git a/packages/agent-provider-computesdk/package.json b/packages/agent-provider-computesdk/package.json index 41dd847..9a158b9 100644 --- a/packages/agent-provider-computesdk/package.json +++ b/packages/agent-provider-computesdk/package.json @@ -40,7 +40,7 @@ "build": "tsc -p tsconfig.json", "check-types": "tsc --noEmit", "clean": "rm -rf dist", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run" }, "dependencies": { diff --git a/packages/agent-provider-daytona/package.json b/packages/agent-provider-daytona/package.json index 2aa1693..575aecc 100644 --- a/packages/agent-provider-daytona/package.json +++ b/packages/agent-provider-daytona/package.json @@ -40,7 +40,7 @@ "build": "tsc -p tsconfig.json", "check-types": "tsc --noEmit", "clean": "rm -rf dist", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run" }, "dependencies": { diff --git a/packages/agent-provider-e2b/package.json b/packages/agent-provider-e2b/package.json index 7807ecd..5d7a4e3 100644 --- a/packages/agent-provider-e2b/package.json +++ b/packages/agent-provider-e2b/package.json @@ -40,7 +40,7 @@ "build": "tsc -p tsconfig.json", "check-types": "tsc --noEmit", "clean": "rm -rf dist", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run" }, "dependencies": { diff --git a/packages/agent-provider-tangle/package.json b/packages/agent-provider-tangle/package.json index a2b67f5..8330d88 100644 --- a/packages/agent-provider-tangle/package.json +++ b/packages/agent-provider-tangle/package.json @@ -40,7 +40,7 @@ "build": "tsc -p tsconfig.json", "check-types": "tsc --noEmit", "clean": "rm -rf dist", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run" }, "dependencies": { diff --git a/packages/agent-provider-testkit/package.json b/packages/agent-provider-testkit/package.json index b9bdd38..d66248d 100644 --- a/packages/agent-provider-testkit/package.json +++ b/packages/agent-provider-testkit/package.json @@ -40,7 +40,7 @@ "build": "tsc -p tsconfig.json", "check-types": "tsc --noEmit", "clean": "rm -rf dist", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run" }, "dependencies": { diff --git a/scripts/check-package-artifacts.mjs b/scripts/check-package-artifacts.mjs new file mode 100644 index 0000000..41f3b1e --- /dev/null +++ b/scripts/check-package-artifacts.mjs @@ -0,0 +1,378 @@ +import { spawnSync } from "node:child_process"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const packagesRoot = join(root, "packages"); +const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; +const npm = process.platform === "win32" ? "npm.cmd" : "npm"; +const node = process.execPath; + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd ?? root, + encoding: "utf8", + env: { ...process.env, ...options.env }, + maxBuffer: 50 * 1024 * 1024, + }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error( + [ + `${command} ${args.join(" ")} exited ${result.status}`, + result.stdout.trim(), + result.stderr.trim(), + ] + .filter(Boolean) + .join("\n"), + ); + } + return result.stdout.trim(); +} + +function runJson(command, args, options) { + const output = run(command, args, options); + try { + return JSON.parse(output); + } catch (error) { + throw new Error( + `${command} did not return JSON:\n${output}`, + { cause: error }, + ); + } +} + +function publishablePackages() { + return readdirSync(packagesRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => { + const directory = join(packagesRoot, entry.name); + const manifestPath = join(directory, "package.json"); + if (!existsSync(manifestPath)) return undefined; + return { directory, manifestPath, manifest: readJson(manifestPath) }; + }) + .filter((entry) => entry !== undefined && entry.manifest.private !== true) + .sort((left, right) => left.manifest.name.localeCompare(right.manifest.name)); +} + +function exportEntries(exportsField) { + if (exportsField === undefined) return []; + if ( + typeof exportsField !== "object" || + exportsField === null || + Array.isArray(exportsField) || + !Object.keys(exportsField).some((key) => key.startsWith(".")) + ) { + return [[".", exportsField]]; + } + return Object.entries(exportsField); +} + +function exportTargets(value, targets = new Set()) { + if (typeof value === "string") { + targets.add(value); + } else if (Array.isArray(value)) { + for (const item of value) exportTargets(item, targets); + } else if (value && typeof value === "object") { + for (const item of Object.values(value)) exportTargets(item, targets); + } + return targets; +} + +function targetPath(packageDirectory, target) { + if (!target.startsWith("./") || target.includes("*")) { + throw new Error(`unsupported export target ${target}`); + } + const path = resolve(packageDirectory, target); + if (path !== packageDirectory && !path.startsWith(`${packageDirectory}${sep}`)) { + throw new Error(`export target escapes package: ${target}`); + } + return path; +} + +function assertInstalledExports(packageDirectory, manifest) { + const entries = exportEntries(manifest.exports); + if (entries.length === 0) { + throw new Error(`${manifest.name} declares no exports`); + } + + let targetCount = 0; + const specifiers = []; + for (const [subpath, definition] of entries) { + if (subpath !== "." && !subpath.startsWith("./")) { + throw new Error(`${manifest.name} has invalid export ${subpath}`); + } + specifiers.push( + subpath === "." ? manifest.name : `${manifest.name}/${subpath.slice(2)}`, + ); + const targets = exportTargets(definition); + if (targets.size === 0) { + throw new Error(`${manifest.name} export ${subpath} has no targets`); + } + for (const target of targets) { + targetCount++; + if (!existsSync(targetPath(packageDirectory, target))) { + throw new Error(`${manifest.name} export target is missing: ${target}`); + } + } + } + + for (const field of ["main", "types"]) { + const target = manifest[field]; + if (target && !existsSync(targetPath(packageDirectory, target))) { + throw new Error(`${manifest.name} ${field} target is missing: ${target}`); + } + } + return { specifiers, targetCount }; +} + +function assertRepackedExports(manifest, files) { + const paths = new Set(files.map((file) => file.path)); + for (const [, definition] of exportEntries(manifest.exports)) { + for (const target of exportTargets(definition)) { + const packedPath = target.replace(/^\.\//, ""); + if (!paths.has(packedPath)) { + throw new Error( + `${manifest.name} repack omitted export target: ${target}`, + ); + } + } + } +} + +function installedDirectory(consumerDirectory, packageName) { + return join(consumerDirectory, "node_modules", ...packageName.split("/")); +} + +const runtimeDependencyFields = [ + "dependencies", + "optionalDependencies", + "peerDependencies", +]; + +function internalDependencies(manifest, packagesByName) { + return runtimeDependencyFields.flatMap((field) => + Object.keys(manifest[field] ?? {}) + .filter((name) => packagesByName.has(name)) + .map((name) => ({ field, name })), + ); +} + +function internalDependencyClosure(entry, packagesByName) { + const closure = new Set(); + const pending = internalDependencies(entry.manifest, packagesByName).map( + ({ name }) => name, + ); + while (pending.length > 0) { + const name = pending.pop(); + if (name === entry.manifest.name || closure.has(name)) continue; + closure.add(name); + const dependency = packagesByName.get(name); + if (!dependency) throw new Error(`missing workspace package ${name}`); + pending.push( + ...internalDependencies(dependency.manifest, packagesByName).map( + ({ name: dependencyName }) => dependencyName, + ), + ); + } + return [...closure].sort(); +} + +function expectedPackedDependencySpecifier(sourceSpecifier, dependency) { + if (!sourceSpecifier.startsWith("workspace:")) return sourceSpecifier; + const selector = sourceSpecifier.slice("workspace:".length); + if (selector === "*") return dependency.manifest.version; + if (selector === "^" || selector === "~") { + return `${selector}${dependency.manifest.version}`; + } + if (selector.length === 0) { + throw new Error(`empty workspace dependency for ${dependency.manifest.name}`); + } + return selector; +} + +function assertInternalDependencyMetadata(source, packed, packagesByName) { + for (const field of runtimeDependencyFields) { + const expectedNames = internalDependencies(source, packagesByName) + .filter((dependency) => dependency.field === field) + .map((dependency) => dependency.name) + .sort(); + const actualNames = internalDependencies(packed, packagesByName) + .filter((dependency) => dependency.field === field) + .map((dependency) => dependency.name) + .sort(); + if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) { + throw new Error( + `${source.name} packed ${field} do not match source: expected ${expectedNames.join(", ") || "none"}; received ${actualNames.join(", ") || "none"}`, + ); + } + for (const name of expectedNames) { + const dependency = packagesByName.get(name); + if (!dependency) throw new Error(`missing workspace package ${name}`); + const expectedSpecifier = expectedPackedDependencySpecifier( + source[field][name], + dependency, + ); + const actualSpecifier = packed[field][name]; + if (actualSpecifier !== expectedSpecifier) { + throw new Error( + `${source.name} packed ${name} as ${actualSpecifier}; expected ${expectedSpecifier}`, + ); + } + } + } +} + +const packages = publishablePackages(); +if (packages.length === 0) throw new Error("no publishable packages found"); +const packagesByName = new Map( + packages.map((entry) => [entry.manifest.name, entry]), +); + +for (const entry of packages) { + const { name, scripts } = entry.manifest; + if (!name) throw new Error(`${entry.manifestPath} has no package name`); + if (scripts?.prepare !== undefined) { + throw new Error(`${name} must not define the install/repack prepare lifecycle`); + } + if (scripts?.prepublishOnly !== "pnpm run build") { + throw new Error(`${name} must build through prepublishOnly`); + } +} + +const temporaryRoot = mkdtempSync(join(tmpdir(), "agent-sdk-artifacts-")); +try { + const sourceTarballs = join(temporaryRoot, "source"); + const consumersRoot = join(temporaryRoot, "consumers"); + const repackedTarballs = join(temporaryRoot, "repacked"); + const npmCache = join(temporaryRoot, "npm-cache"); + for (const directory of [sourceTarballs, consumersRoot, repackedTarballs]) { + mkdirSync(directory, { recursive: true }); + } + + const tarballs = new Map(); + for (const entry of packages) { + const packed = runJson( + pnpm, + ["pack", "--json", "--pack-destination", sourceTarballs], + { cwd: entry.directory }, + ); + if (packed.name !== entry.manifest.name || !existsSync(packed.filename)) { + throw new Error(`failed to pack ${entry.manifest.name}`); + } + tarballs.set(entry.manifest.name, packed.filename); + } + + const npmEnvironment = { + npm_config_audit: "false", + npm_config_cache: npmCache, + npm_config_fund: "false", + npm_config_ignore_scripts: "false", + npm_config_update_notifier: "false", + }; + let exportCount = 0; + let exportTargetCount = 0; + for (const entry of packages) { + const consumerDirectory = join( + consumersRoot, + entry.manifest.name.replace(/^@/, "").replaceAll("/", "-"), + ); + mkdirSync(consumerDirectory, { recursive: true }); + const dependencyNames = [ + entry.manifest.name, + ...internalDependencyClosure(entry, packagesByName), + ]; + const dependencies = Object.fromEntries( + dependencyNames.map((name) => { + const tarball = tarballs.get(name); + if (!tarball) throw new Error(`missing tarball for ${name}`); + return [ + name, + `file:${relative(consumerDirectory, tarball).split(sep).join("/")}`, + ]; + }), + ); + writeFileSync( + join(consumerDirectory, "package.json"), + `${JSON.stringify( + { + name: "agent-sdk-package-artifact-check", + private: true, + type: "module", + dependencies, + }, + null, + 2, + )}\n`, + ); + run( + npm, + ["install", "--package-lock=false", "--ignore-scripts=false"], + { cwd: consumerDirectory, env: npmEnvironment }, + ); + + const directory = installedDirectory(consumerDirectory, entry.manifest.name); + const manifest = readJson(join(directory, "package.json")); + if (manifest.name !== entry.manifest.name) { + throw new Error(`installed the wrong package for ${entry.manifest.name}`); + } + if (manifest.scripts?.prepare !== undefined) { + throw new Error(`${manifest.name} tarball still contains prepare`); + } + assertInternalDependencyMetadata( + entry.manifest, + manifest, + packagesByName, + ); + const exportsCheck = assertInstalledExports(directory, manifest); + exportCount += exportsCheck.specifiers.length; + exportTargetCount += exportsCheck.targetCount; + run( + node, + [ + "--input-type=module", + "--eval", + `for (const specifier of ${JSON.stringify(exportsCheck.specifiers)}) { await import(specifier) }`, + ], + { cwd: consumerDirectory }, + ); + + const packed = runJson( + npm, + [ + "pack", + directory, + "--json", + "--pack-destination", + repackedTarballs, + "--ignore-scripts=false", + ], + { cwd: consumerDirectory, env: npmEnvironment }, + ); + const result = Array.isArray(packed) ? packed[0] : packed; + if (!result || !existsSync(join(repackedTarballs, result.filename))) { + throw new Error(`failed to repack ${entry.manifest.name}`); + } + assertRepackedExports(manifest, result.files ?? []); + } + + console.log( + `Checked ${packages.length} isolated packages, ${exportCount} exports, and ${exportTargetCount} export targets.`, + ); +} finally { + rmSync(temporaryRoot, { recursive: true, force: true }); +} From d47de50baba9c071bea6277a6a7d1ffbbaf8ea52 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Wed, 15 Jul 2026 14:01:24 -0600 Subject: [PATCH 3/3] revert(agent-interface): remove content-store artifact locator --- .changeset/add-content-store-locator.md | 5 - .../agent-candidate-artifact-schema.test.ts | 130 ++---------------- .../src/agent-candidate-artifact-schema.ts | 38 +---- .../agent-interface/src/agent-candidate.ts | 8 +- 4 files changed, 14 insertions(+), 167 deletions(-) delete mode 100644 .changeset/add-content-store-locator.md diff --git a/.changeset/add-content-store-locator.md b/.changeset/add-content-store-locator.md deleted file mode 100644 index 91fbfaf..0000000 --- a/.changeset/add-content-store-locator.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@tangle-network/agent-interface": minor ---- - -Add a strict generic content-store locator for externally stored candidate artifacts. diff --git a/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts b/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts index 6c03aaf..11bf421 100644 --- a/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts +++ b/packages/agent-interface/src/agent-candidate-artifact-schema.test.ts @@ -11,14 +11,6 @@ import { agentCandidateWorkspaceSnapshotEvidenceSchema, } from "./agent-candidate-artifact-schema.js"; import { candidateGit, candidateSha } from "./agent-candidate.test-fixture.js"; -import type { - AgentCandidateArtifactLocator, - AgentCandidateContentStoreLocator, -} from "./index.js"; -import { - agentCandidateArtifactLocatorSchema, - agentCandidateContentStoreLocatorSchema, -} from "./index.js"; const failClosedResources: AgentCandidateResources = { failOnError: true }; // @ts-expect-error Candidate resources may never silently degrade. @@ -27,47 +19,20 @@ void failClosedResources; void missingFailClosedPolicy; describe("candidate artifact schemas", () => { - it("accepts S3, IPFS, and content-store artifact locators", () => { - const contentStoreLocator: AgentCandidateContentStoreLocator = { - kind: "content-store", - store: "candidate-artifacts-2", - }; - const locators: AgentCandidateArtifactLocator[] = [ - { - kind: "s3", - bucket: "agent-candidate-artifacts", - key: "runs/r360/trace.json", - region: "us-east-1", - }, - { - kind: "ipfs", - cid: `Qm${"1".repeat(44)}`, - path: "runs/r360/trace.json", - }, - contentStoreLocator, - ]; - - for (const locator of locators) { - expect(agentCandidateArtifactLocatorSchema.parse(locator)).toEqual( - locator, - ); - expect( - agentCandidateArtifactRefSchema.parse({ - locator, - sha256: candidateSha("1"), - byteLength: 42, - }), - ).toEqual({ locator, sha256: candidateSha("1"), byteLength: 42 }); - } - expect( - agentCandidateContentStoreLocatorSchema.parse({ - kind: "content-store", - store: "a".repeat(63), + it("accepts only closed, non-URL artifact locators", () => { + expect(() => + agentCandidateArtifactRefSchema.parse({ + locator: { + kind: "s3", + bucket: "agent-candidate-artifacts", + key: "runs/r360/trace.json", + region: "us-east-1", + }, + sha256: candidateSha("1"), + byteLength: 42, }), - ).toEqual({ kind: "content-store", store: "a".repeat(63) }); - }); + ).not.toThrow(); - it("rejects arbitrary URL locator kinds", () => { for (const uri of [ "file:///etc/passwd", "http://169.254.169.254/latest/meta-data", @@ -85,77 +50,6 @@ describe("candidate artifact schemas", () => { } }); - it("rejects unsafe content-store names and unknown locator fields", () => { - for (const store of [ - "", - "-candidate-artifacts", - "candidate-artifacts-", - "Candidate-Artifacts", - "candidate_artifacts", - "candidate.artifacts", - "candidate/artifacts", - "candidate\\artifacts", - "../candidate-artifacts", - "https://artifacts.example.com", - "s3://candidate-artifacts", - "sk-live-abcdefghijklmnop", - "ghp-abcdefghijklmnop", - ["xoxb", "123456789012", "123456789012", "abcdefghijklmnopqrstuvwx"].join( - "-", - ), - "xapp-1-abcdefghijklmnopqrstuvwx", - "xwfp-1-abcdefghijklmnopqrstuvwx", - "glpat-abcdefghijklmnopqrst", - "gloas-abcdefghijklmnopqrst", - "glrtr-abcdefghijklmnopqrst", - "glft-abcdefghijklmnopqrst", - "glwt-abcdefghijklmnopqrst", - "token-abcdefghijkl", - "tokens-prod", - "api-key-abcdefghijkl", - "api-keys-prod", - "access-key-abcdefghijkl", - "access-keys-prod", - "private-key-abcdefghijkl", - "private-keys-prod", - "secret-abcdefghijkl", - "secrets-prod", - "password-abcdefghijkl", - "passwords-prod", - "credentials-abcdefghijkl", - "authorization-abcdefghijkl", - "bearer-abcdefghijkl", - "cookie-abcdefghijkl", - "cookies-prod", - "database-url-abcdefghijkl", - "dsn-abcdefghijkl", - "pat-abcdefghijkl", - "pats-prod", - "a".repeat(64), - ]) { - expect(() => - agentCandidateContentStoreLocatorSchema.parse({ - kind: "content-store", - store, - }), - ).toThrow(); - } - - for (const extra of [ - { url: "https://artifacts.example.com" }, - { path: "runs/r360/trace.json" }, - { token: "sk-live-abcdefghijklmnop" }, - ]) { - expect(() => - agentCandidateContentStoreLocatorSchema.parse({ - kind: "content-store", - store: "candidate-artifacts", - ...extra, - }), - ).toThrow(); - } - }); - it("rejects traversal-shaped artifact keys and reserved roots", () => { for (const key of [ "../secret", diff --git a/packages/agent-interface/src/agent-candidate-artifact-schema.ts b/packages/agent-interface/src/agent-candidate-artifact-schema.ts index 7b0ebd1..bad5fe7 100644 --- a/packages/agent-interface/src/agent-candidate-artifact-schema.ts +++ b/packages/agent-interface/src/agent-candidate-artifact-schema.ts @@ -2,7 +2,6 @@ import { z } from "zod"; import type { AgentCandidateArtifactRef, AgentCandidateCapturedArtifact, - AgentCandidateContentStoreLocator, AgentCandidateEmbeddedArtifact, AgentCandidateFileMount, AgentCandidateGitHubResource, @@ -28,21 +27,6 @@ const base64Alphabet = const s3BucketPattern = /^(?!\d+\.\d+\.\d+\.\d+$)[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/; const awsRegionPattern = /^[a-z]{2}(?:-gov)?-[a-z]+-\d$/; const ipfsCidPattern = /^(?:Qm[1-9A-HJ-NP-Za-km-z]{44}|b[a-z2-7]{20,})$/; -const contentStoreSlugPattern = - /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; -const credentialBearingSlugNamePattern = - /(?:^|-)(?:api-?keys?|access-?keys?|private-?keys?|tokens?|secrets?|passwords?|credentials?|authorizations?|bearers?|cookies?|database-?urls?|dsns?|pats?)(?:-|$)/; -// These token formats remain valid lowercase slugs after delimiter restrictions. -const slugShapedCredentialPattern = - /^(?:xox[abeoprs]|xapp|xwfp|glpat|gloas|gldt|glrt|glrtr|glptt|glcbt|glimt|glagent|glsoat|glffct|glft|glwt|grt|sq0atp|sq0csp)-[a-z0-9-]{8,}$/; - -function looksLikeContentStoreCredential(value: string): boolean { - return ( - looksLikeCredential(value) || - credentialBearingSlugNamePattern.test(value) || - slugShapedCredentialPattern.test(value) - ); -} function decodedBase64ByteLength(value: string): number { if (value.length === 0) return 0; @@ -117,29 +101,9 @@ export const agentCandidateIpfsLocatorSchema = z }) .strict() satisfies z.ZodType; -export const agentCandidateContentStoreLocatorSchema = z - .object({ - kind: z.literal("content-store"), - store: z - .string() - .regex( - contentStoreSlugPattern, - "content store must be a 1-63 character lowercase slug", - ) - .refine( - (value) => !looksLikeContentStoreCredential(value), - "content store must not contain credentials", - ), - }) - .strict() satisfies z.ZodType; - export const agentCandidateArtifactLocatorSchema = z.discriminatedUnion( "kind", - [ - agentCandidateS3LocatorSchema, - agentCandidateIpfsLocatorSchema, - agentCandidateContentStoreLocatorSchema, - ], + [agentCandidateS3LocatorSchema, agentCandidateIpfsLocatorSchema], ); export const agentCandidateArtifactRefSchema = z diff --git a/packages/agent-interface/src/agent-candidate.ts b/packages/agent-interface/src/agent-candidate.ts index a7f5548..133097e 100644 --- a/packages/agent-interface/src/agent-candidate.ts +++ b/packages/agent-interface/src/agent-candidate.ts @@ -40,16 +40,10 @@ export interface AgentCandidateIpfsLocator { path?: string; } -export interface AgentCandidateContentStoreLocator { - kind: "content-store"; - store: string; -} - /** Closed locator set whose resolvers cannot choose arbitrary URL schemes. */ export type AgentCandidateArtifactLocator = | AgentCandidateS3Locator - | AgentCandidateIpfsLocator - | AgentCandidateContentStoreLocator; + | AgentCandidateIpfsLocator; /** Content-addressed artifact stored outside the candidate manifest. */ export interface AgentCandidateArtifactRef {