diff --git a/cli/package-lock.json b/cli/package-lock.json index cc6ae0196..eb95dc371 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@opencomputer/cli", - "version": "0.6.5", + "version": "0.6.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@opencomputer/cli", - "version": "0.6.5", + "version": "0.6.6", "dependencies": { "@opencode-ai/sdk": "1.18.4", "ai": "^7.0.45", diff --git a/cli/package.json b/cli/package.json index dd38bb5ad..7fd057e5a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@opencomputer/cli", - "version": "0.6.5", + "version": "0.6.6", "description": "Build, test, deploy, and share OpenComputer agents as code.", "type": "module", "bin": { diff --git a/cli/src/api.ts b/cli/src/api.ts index cf8007261..dd826119e 100644 --- a/cli/src/api.ts +++ b/cli/src/api.ts @@ -38,6 +38,9 @@ export interface ManagedAgentDeployment { projectDeploymentId?: string; localAgentId?: string; createdAt: string; + status?: "pending" | "ready" | "failed"; + stage?: "validating" | "uploading" | "building" | "snapshotting" | "verifying" | "ready"; + environmentDigest?: string; } export interface ManagedAgentEvent { @@ -561,6 +564,14 @@ export class OpenComputerClient { contentType: string; body: string; }; + environmentSource?: { + digest: string; + size: number; + contentType: string; + body: string; + baseImage: string; + architecture: "linux/arm64"; + }; }) { return this.request( "/api/managed-agents/deployments", diff --git a/cli/src/dev.ts b/cli/src/dev.ts index e3683bf1a..b2b9538ec 100644 --- a/cli/src/dev.ts +++ b/cli/src/dev.ts @@ -257,6 +257,18 @@ async function registerBuiltDeployment( contentType: "application/vnd.opencomputer.agent+json", body: built.body.toString("utf8"), }, + ...(built.environment + ? { + environmentSource: { + digest: built.environment.digest, + size: built.environment.size, + contentType: built.environment.contentType, + body: built.environment.body.toString("utf8"), + baseImage: built.environment.baseImage, + architecture: built.environment.architecture, + }, + } + : {}), }); return { built, deployment }; } @@ -307,6 +319,7 @@ export async function publishProjectDeployment( agents: builtAgents.map(({ source, built }) => ({ id: source.localId, artifact: built.digest, + environment: built.environment?.digest, })), }), ) diff --git a/cli/src/environment.test.ts b/cli/src/environment.test.ts new file mode 100644 index 000000000..140aad752 --- /dev/null +++ b/cli/src/environment.test.ts @@ -0,0 +1,96 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { resolve } from "node:path"; +import test from "node:test"; +import { + buildAgentEnvironment, + validateAgentDockerfile, +} from "./environment.js"; + +const runtime = "registry.opencomputer.dev/serverless-agent:0.6.5"; + +test("accepts a versioned OpenComputer runtime as the final stage", () => { + assert.deepEqual(validateAgentDockerfile(`FROM ${runtime}\nRUN npm i`), { + baseImage: runtime, + }); +}); + +test("accepts a final stage descended from an approved named stage", () => { + assert.deepEqual( + validateAgentDockerfile(` +FROM node:22 AS build +RUN echo compiling +FROM ${runtime} AS platform +COPY --from=build /tmp/result /tmp/result +FROM platform AS final +RUN echo ready +`), + { baseImage: runtime }, + ); +}); + +test("rejects an arbitrary or mutable final runtime", () => { + assert.throws( + () => validateAgentDockerfile("FROM ubuntu:24.04"), + /final Dockerfile stage must inherit/, + ); + assert.throws( + () => + validateAgentDockerfile( + "FROM registry.opencomputer.dev/serverless-agent:latest", + ), + /final Dockerfile stage must inherit/, + ); +}); + +test("rejects build arguments in FROM and custom frontends", () => { + assert.throws( + () => validateAgentDockerfile("ARG BASE\nFROM ${BASE}"), + /cannot use build arguments/, + ); + assert.throws( + () => validateAgentDockerfile(`# syntax=example/custom:1\nFROM ${runtime}`), + /custom Dockerfile frontend/, + ); +}); + +test("packages a deterministic narrow environment context", async () => { + const root = await mkdtemp(resolve(tmpdir(), "opencomputer-environment-")); + try { + await mkdir(resolve(root, "sandbox")); + await writeFile(resolve(root, "Dockerfile"), `FROM ${runtime}\n`); + await writeFile(resolve(root, "sandbox", "requirements.txt"), "ruff==1.0\n"); + await writeFile(resolve(root, "ignored.txt"), "not in context\n"); + const first = await buildAgentEnvironment(root); + const second = await buildAgentEnvironment(root); + assert.ok(first); + assert.equal(first.digest, second?.digest); + assert.equal(first.baseImage, runtime); + const body = JSON.parse(first.body.toString("utf8")) as { + files: Array<{ path: string }>; + }; + assert.deepEqual( + body.files.map((file) => file.path), + ["Dockerfile", "sandbox/requirements.txt"], + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("rejects symlinks and secret-looking context files", async () => { + const root = await mkdtemp(resolve(tmpdir(), "opencomputer-environment-")); + try { + await mkdir(resolve(root, "sandbox")); + await writeFile(resolve(root, "Dockerfile"), `FROM ${runtime}\n`); + await writeFile(resolve(root, "outside"), "secret"); + await symlink(resolve(root, "outside"), resolve(root, "sandbox", "link")); + await assert.rejects(buildAgentEnvironment(root), /cannot include symlink/); + await rm(resolve(root, "sandbox", "link")); + await writeFile(resolve(root, "sandbox", ".env.local"), "TOKEN=nope\n"); + await assert.rejects(buildAgentEnvironment(root), /cannot include sandbox\/\.env\.local/); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/cli/src/environment.ts b/cli/src/environment.ts new file mode 100644 index 000000000..b592d793a --- /dev/null +++ b/cli/src/environment.ts @@ -0,0 +1,162 @@ +import { createHash } from "node:crypto"; +import { lstat, readFile, readdir } from "node:fs/promises"; +import { relative, resolve } from "node:path"; + +const APPROVED_RUNTIME = + /^registry\.opencomputer\.dev\/serverless-agent:(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/; +const MAX_FILE_BYTES = 5 * 1024 * 1024; +const MAX_CONTEXT_BYTES = 20 * 1024 * 1024; + +export interface AgentEnvironmentSource { + body: Buffer; + digest: string; + size: number; + contentType: "application/vnd.opencomputer.agent-environment+json"; + baseImage: string; + architecture: "linux/arm64"; +} + +interface DockerfileStage { + approvedBase?: string; +} + +function dockerfileLines(source: string): string[] { + const lines: string[] = []; + let current = ""; + for (const physical of source.replaceAll("\r\n", "\n").split("\n")) { + const continued = /\\\s*$/.test(physical); + current += `${current ? " " : ""}${physical.replace(/\\\s*$/, "").trim()}`; + if (!continued) { + if (current) lines.push(current); + current = ""; + } + } + if (current) lines.push(current); + return lines; +} + +export function validateAgentDockerfile(source: string): { baseImage: string } { + const stages = new Map(); + let finalStage: DockerfileStage | undefined; + for (const line of dockerfileLines(source)) { + if (/^#\s*syntax\s*=/i.test(line)) { + throw new Error("Agent Dockerfiles cannot select a custom Dockerfile frontend"); + } + if (line.startsWith("#")) continue; + const [instruction = "", ...rest] = line.split(/\s+/); + if (instruction.toUpperCase() !== "FROM") continue; + const tokens = rest.filter((token) => !token.startsWith("--platform=")); + const image = tokens[0]; + if (!image) throw new Error("Every FROM instruction requires an image"); + if (image.includes("$") || image.includes("${")) { + throw new Error("Agent Dockerfile FROM images cannot use build arguments"); + } + const inherited = stages.get(image.toLowerCase()); + const match = image.match(APPROVED_RUNTIME); + finalStage = { + approvedBase: match ? image : inherited?.approvedBase, + }; + if (tokens[1]?.toUpperCase() === "AS") { + const alias = tokens[2]; + if (!alias || !/^[a-zA-Z0-9_.-]+$/.test(alias)) { + throw new Error("Dockerfile stage aliases must be static names"); + } + stages.set(alias.toLowerCase(), finalStage); + } + } + if (!finalStage) throw new Error("Agent Dockerfile must contain a FROM instruction"); + if (!finalStage.approvedBase) { + throw new Error( + "The final Dockerfile stage must inherit from registry.opencomputer.dev/serverless-agent:", + ); + } + return { baseImage: finalStage.approvedBase }; +} + +async function collectSandboxFiles( + agentRoot: string, + directory: string, +): Promise> { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; + throw error; + } + const files: Array<{ path: string; content: string }> = []; + for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { + const path = resolve(directory, entry.name); + const normalized = relative(agentRoot, path).split("\\").join("/"); + if ( + entry.name === ".git" || + entry.name === ".opencomputer" || + entry.name === "node_modules" || + entry.name === ".env" || + entry.name.startsWith(".env.") + ) { + throw new Error(`Agent environment context cannot include ${normalized}`); + } + const metadata = await lstat(path); + if (metadata.isSymbolicLink()) { + throw new Error(`Agent environment context cannot include symlink ${normalized}`); + } + if (metadata.isDirectory()) { + files.push(...(await collectSandboxFiles(agentRoot, path))); + continue; + } + if (!metadata.isFile()) { + throw new Error(`Agent environment context only supports regular files: ${normalized}`); + } + if (metadata.size > MAX_FILE_BYTES) { + throw new Error(`Agent environment file exceeds 5 MiB: ${normalized}`); + } + files.push({ path: normalized, content: (await readFile(path)).toString("base64") }); + } + return files; +} + +export async function buildAgentEnvironment( + agentRoot: string, +): Promise { + const dockerfilePath = resolve(agentRoot, "Dockerfile"); + let dockerfile: Buffer; + try { + const metadata = await lstat(dockerfilePath); + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error("Agent Dockerfile must be a regular file"); + } + dockerfile = await readFile(dockerfilePath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; + throw error; + } + const { baseImage } = validateAgentDockerfile(dockerfile.toString("utf8")); + const files = [ + { path: "Dockerfile", content: dockerfile.toString("base64") }, + ...(await collectSandboxFiles(agentRoot, resolve(agentRoot, "sandbox"))), + ]; + const decodedSize = files.reduce( + (total, file) => total + Buffer.byteLength(file.content, "base64"), + 0, + ); + if (decodedSize > MAX_CONTEXT_BYTES) { + throw new Error("Agent environment context exceeds 20 MiB"); + } + const body = Buffer.from( + JSON.stringify({ + version: 1, + architecture: "linux/arm64", + baseImage, + files, + }), + ); + return { + body, + digest: createHash("sha256").update(body).digest("hex"), + size: body.byteLength, + contentType: "application/vnd.opencomputer.agent-environment+json", + baseImage, + architecture: "linux/arm64", + }; +} diff --git a/cli/src/project.ts b/cli/src/project.ts index 894c9a6e7..83dc1efd0 100644 --- a/cli/src/project.ts +++ b/cli/src/project.ts @@ -12,6 +12,10 @@ import { basename, dirname, relative, resolve } from "node:path"; import { Cron } from "croner"; import { build as bundle } from "esbuild"; import ts from "typescript"; +import { + buildAgentEnvironment, + type AgentEnvironmentSource, +} from "./environment.js"; export interface AgentManifest { schema: 1; @@ -28,6 +32,7 @@ export interface BuiltAgentArtifact { body: Buffer; digest: string; elapsedMs: number; + environment?: AgentEnvironmentSource; } export interface HttpConnectionManifest { @@ -2063,6 +2068,7 @@ export async function buildAgentArtifact( ) as { connections?: string[]; httpConnections?: HttpConnectionManifest[] }; const connections = [...new Set(reactive.connections ?? [])].sort(); const httpConnections = reactive.httpConnections ?? []; + const environment = await buildAgentEnvironment(root); const body = Buffer.from( JSON.stringify({ version: 1, @@ -2079,5 +2085,6 @@ export async function buildAgentArtifact( body, digest: createHash("sha256").update(body).digest("hex"), elapsedMs: Math.round(performance.now() - startedAt), + ...(environment ? { environment } : {}), }; } diff --git a/cloudflare-workers/api-edge/src/managed_agents.test.ts b/cloudflare-workers/api-edge/src/managed_agents.test.ts index ad63acb75..c2276a38b 100644 --- a/cloudflare-workers/api-edge/src/managed_agents.test.ts +++ b/cloudflare-workers/api-edge/src/managed_agents.test.ts @@ -1077,6 +1077,9 @@ describe("managed agents proxy", () => { }, ], createdAt: "2026-07-30T00:00:00.000Z", + status: "pending", + stage: "building", + environmentDigest: "e".repeat(64), artifact: { bucket: "private-bucket" }, imageArn: "arn:aws:private", }, @@ -1149,11 +1152,79 @@ describe("managed agents proxy", () => { }), ], }); - expect(JSON.stringify(await response.json())).not.toMatch( + const deployment = await response.json(); + expect(deployment).toMatchObject({ + status: "pending", + stage: "building", + environmentDigest: "e".repeat(64), + }); + expect(JSON.stringify(deployment)).not.toMatch( /bucket|imageArn|arn:aws|uploads\.test/i, ); }); + it("rejects an environment that does not inherit from the approved runtime", async () => { + const source = JSON.stringify({ version: 1, files: [] }); + const environment = JSON.stringify({ + version: 1, + architecture: "linux/arm64", + baseImage: "registry.opencomputer.dev/serverless-agent:0.6.5", + files: [ + { + path: "Dockerfile", + content: btoa("FROM ubuntu:24.04\n"), + }, + ], + }); + const digest = async (value: string) => + Array.from( + new Uint8Array( + await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)), + ), + ) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); + const fetchSpy = vi.fn(); + vi.stubGlobal("fetch", fetchSpy); + const response = await proxyManagedAgents( + new Request("https://app.opencomputer.dev/api/managed-agents/deployments", { + method: "POST", + body: JSON.stringify({ + agentId: "custom-agent", + alias: "development", + source: { + digest: await digest(source), + size: source.length, + contentType: "application/vnd.opencomputer.agent+json", + body: source, + }, + environmentSource: { + digest: await digest(environment), + size: environment.length, + contentType: "application/vnd.opencomputer.agent-environment+json", + body: environment, + baseImage: "registry.opencomputer.dev/serverless-agent:0.6.5", + architecture: "linux/arm64", + }, + }), + }), + { + OC_MANAGED_AGENTS_SECRET: "test-secret", + MANAGED_AGENTS_API_URL: "https://managedagents.test", + }, + { orgID: "org_test", userID: "user_test" }, + "/api/managed-agents", + ); + expect(response.status).toBe(400); + expect(await response.json()).toEqual({ + error: { + code: "invalid_deployment", + message: "The agent environment failed runtime validation.", + }, + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + it("forwards managed secret metadata and aggregate logs through the public contract", async () => { const fetchSpy = vi .fn() diff --git a/cloudflare-workers/api-edge/src/managed_agents.ts b/cloudflare-workers/api-edge/src/managed_agents.ts index d8ad482c5..1f1aa94d6 100644 --- a/cloudflare-workers/api-edge/src/managed_agents.ts +++ b/cloudflare-workers/api-edge/src/managed_agents.ts @@ -16,6 +16,9 @@ export interface ManagedAgentsCaller { const DEFAULT_MANAGED_AGENTS_API_URL = "https://managedagents.opencomputer.dev"; const MAX_AGENT_SOURCE_BYTES = 10 * 1024 * 1024; +const MAX_AGENT_ENVIRONMENT_BYTES = 28 * 1024 * 1024; +const APPROVED_AGENT_RUNTIME = + /^registry\.opencomputer\.dev\/serverless-agent:\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/; export async function hasBYOKPlanAccess( env: ManagedAgentsEnv, @@ -203,6 +206,21 @@ function strings(value: unknown): string[] { function publicDeployment(value: unknown): Record { const deployment = record(value) ?? {}; + const status = + deployment.status === "pending" || + deployment.status === "ready" || + deployment.status === "failed" + ? deployment.status + : undefined; + const stage = + deployment.stage === "validating" || + deployment.stage === "uploading" || + deployment.stage === "building" || + deployment.stage === "snapshotting" || + deployment.stage === "verifying" || + deployment.stage === "ready" + ? deployment.stage + : undefined; return { id: deployment.id, agentId: deployment.agentId, @@ -210,6 +228,11 @@ function publicDeployment(value: unknown): Record { channels: strings(deployment.channels), connections: strings(deployment.connections), createdAt: deployment.createdAt, + ...(status ? { status } : {}), + ...(stage ? { stage } : {}), + ...(typeof deployment.environmentDigest === "string" + ? { environmentDigest: deployment.environmentDigest } + : {}), ...(deployment.projectDeployment ? { projectDeployment: stripPrivateValues(deployment.projectDeployment) } : {}), @@ -849,6 +872,97 @@ async function sha256Hex(value: Uint8Array): Promise { .join(""); } +function validateAgentEnvironment(bytes: Uint8Array): string | undefined { + let value: unknown; + try { + value = JSON.parse(new TextDecoder().decode(bytes)); + } catch { + return undefined; + } + const environment = record(value); + if ( + environment?.version !== 1 || + environment.architecture !== "linux/arm64" || + typeof environment.baseImage !== "string" || + !APPROVED_AGENT_RUNTIME.test(environment.baseImage) || + !Array.isArray(environment.files) + ) return undefined; + const dockerfileEntry = environment.files.find((entry) => { + const file = record(entry); + return file?.path === "Dockerfile" && typeof file.content === "string"; + }); + const encoded = record(dockerfileEntry)?.content; + if (typeof encoded !== "string") return undefined; + let dockerfile: string; + try { + dockerfile = atob(encoded); + } catch { + return undefined; + } + if (/^\s*#\s*syntax\s*=/im.test(dockerfile)) return undefined; + const stages = new Map(); + let approvedBase: string | undefined; + const logical = dockerfile + .replaceAll("\r\n", "\n") + .replace(/\\\s*\n/g, " ") + .split("\n"); + for (const raw of logical) { + const line = raw.trim(); + if (!line || line.startsWith("#")) continue; + const [instruction = "", ...rest] = line.split(/\s+/); + if (instruction.toUpperCase() !== "FROM") continue; + const tokens = rest.filter((token) => !token.startsWith("--platform=")); + const image = tokens[0]; + if (!image || image.includes("$")) return undefined; + approvedBase = APPROVED_AGENT_RUNTIME.test(image) + ? image + : stages.get(image.toLowerCase()); + if (tokens[1]?.toUpperCase() === "AS" && tokens[2]) { + stages.set(tokens[2].toLowerCase(), approvedBase); + } + } + return approvedBase === environment.baseImage ? approvedBase : undefined; +} + +async function uploadDeploymentSource( + base: string, + upstreamHeaders: Headers, + agentId: string, + source: Record, + bytes: Uint8Array, + kind: "agent" | "environment", +): Promise | Response> { + const uploadResponse = await fetch(`${base}/v1/deployment-uploads`, { + method: "POST", + headers: upstreamHeaders, + body: JSON.stringify({ + agentId, + digest: source.digest, + size: source.size, + contentType: source.contentType, + kind, + }), + redirect: "manual", + }); + if (!uploadResponse.ok) return publicErrorResponse(uploadResponse); + const upload = record(await uploadResponse.json().catch(() => null)); + const artifact = record(upload?.artifact); + const uploadURL = typeof upload?.uploadUrl === "string" ? new URL(upload.uploadUrl) : null; + if (!uploadURL || uploadURL.protocol !== "https:" || upload?.method !== "PUT" || !artifact) { + return Response.json({ error: "managed agents service is unavailable" }, { status: 502 }); + } + const stored = await fetch(uploadURL, { + method: "PUT", + headers: { "content-type": String(source.contentType) }, + body: bytes, + redirect: "manual", + }); + if (!stored.ok) { + return Response.json({ error: "managed agents service is unavailable" }, { status: 502 }); + } + return artifact; +} + async function deploySourceAgent( request: Request, base: string, @@ -856,6 +970,7 @@ async function deploySourceAgent( ): Promise { const body = record(await request.json().catch(() => null)); const source = record(body?.source); + const environmentSource = record(body?.environmentSource); if ( !body || typeof body.agentId !== "string" || @@ -878,47 +993,38 @@ async function deploySourceAgent( "The agent source size or digest did not match.", ); } + let environmentBytes: Uint8Array | undefined; + if (environmentSource) { + if ( + typeof environmentSource.digest !== "string" || + typeof environmentSource.size !== "number" || + typeof environmentSource.contentType !== "string" || + typeof environmentSource.body !== "string" + ) return invalidDeploymentResponse("The agent environment was invalid."); + environmentBytes = new TextEncoder().encode(environmentSource.body); + const baseImage = validateAgentEnvironment(environmentBytes); + if ( + environmentBytes.byteLength !== environmentSource.size || + environmentBytes.byteLength > MAX_AGENT_ENVIRONMENT_BYTES || + (await sha256Hex(environmentBytes)) !== environmentSource.digest || + baseImage !== environmentSource.baseImage || + environmentSource.architecture !== "linux/arm64" + ) return invalidDeploymentResponse("The agent environment failed runtime validation."); + } const uploadHeaders = new Headers(upstreamHeaders); uploadHeaders.set("content-type", "application/json"); - const uploadResponse = await fetch(`${base}/v1/deployment-uploads`, { - method: "POST", - headers: uploadHeaders, - body: JSON.stringify({ - agentId: body.agentId, - digest: source.digest, - size: source.size, - contentType: source.contentType, - }), - redirect: "manual", - }); - if (!uploadResponse.ok) return publicErrorResponse(uploadResponse); - const upload = record(await uploadResponse.json().catch(() => null)); - const artifact = record(upload?.artifact); - const uploadURL = - typeof upload?.uploadUrl === "string" ? new URL(upload.uploadUrl) : null; - if ( - !uploadURL || - uploadURL.protocol !== "https:" || - upload?.method !== "PUT" || - !artifact - ) { - return Response.json( - { error: "managed agents service is unavailable" }, - { status: 502 }, - ); - } - const stored = await fetch(uploadURL, { - method: "PUT", - headers: { "content-type": source.contentType }, - body: bytes, - redirect: "manual", - }); - if (!stored.ok) { - return Response.json( - { error: "managed agents service is unavailable" }, - { status: 502 }, + const artifact = await uploadDeploymentSource( + base, uploadHeaders, body.agentId, source, bytes, "agent", + ); + if (artifact instanceof Response) return artifact; + let environmentArtifact: Record | undefined; + if (environmentSource && environmentBytes) { + const uploaded = await uploadDeploymentSource( + base, uploadHeaders, body.agentId, environmentSource, environmentBytes, "environment", ); + if (uploaded instanceof Response) return uploaded; + environmentArtifact = uploaded; } const deploymentResponse = await fetch(`${base}/v1/deployments`, { @@ -940,6 +1046,16 @@ async function deploySourceAgent( ? { projectDeployment: body.projectDeployment } : {}), artifact, + ...(environmentArtifact + ? { + environment: { + artifact: environmentArtifact, + digest: environmentSource!.digest, + baseImage: environmentSource!.baseImage, + architecture: "linux/arm64", + }, + } + : {}), }), redirect: "manual", }); diff --git a/create-start/package.json b/create-start/package.json index d846d71e2..de0f4541e 100644 --- a/create-start/package.json +++ b/create-start/package.json @@ -1,6 +1,6 @@ { "name": "@opencomputer/create-start", - "version": "0.6.5", + "version": "0.6.6", "description": "Create a hello-world OpenComputer agent application.", "type": "module", "bin": { @@ -18,7 +18,7 @@ "node": ">=22.0.0" }, "dependencies": { - "@opencomputer/cli": "0.6.5" + "@opencomputer/cli": "0.6.6" }, "publishConfig": { "access": "public"