Skip to content
Merged
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
11 changes: 11 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ opencomputer secrets list --environment development
opencomputer secrets remove GITHUB_TOKEN --environment development
```

For values that agent code and commands must read directly from the process
environment, use encrypted agent runtime variables. They require no source
declaration and apply to newly started runtimes:

```bash
opencomputer env set DATABASE_URL
opencomputer env set DATABASE_URL --agent current --environment production
opencomputer env list --environment development
opencomputer env remove DATABASE_URL --environment development
```

Agent code declares secret-backed destinations with `defineConnection()` and
`useSecret()`. Requests use the managed gateway, which
injects a secret only for the declared origin, path, method, agent, and
Expand Down
4 changes: 2 additions & 2 deletions cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencomputer/cli",
"version": "0.5.4",
"version": "0.5.5",
"description": "Build, test, deploy, and share OpenComputer agents as code.",
"type": "module",
"bin": {
Expand Down
60 changes: 60 additions & 0 deletions cli/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ export interface ManagedSecretMetadata {
updatedAt: string;
}

export interface AgentRuntimeVariableMetadata {
name: string;
projectId: string;
environment: "development" | "production";
agentId?: string;
createdAt: string;
updatedAt: string;
}

export interface ManagedAgentLog {
id: string;
cursor: string;
Expand Down Expand Up @@ -262,6 +271,57 @@ export class OpenComputerClient {
);
}

async runtimeVariables(input: {
projectId: string;
environment?: "development" | "production";
agentId?: string;
}): Promise<AgentRuntimeVariableMetadata[]> {
const query = new URLSearchParams();
if (input.environment) query.set("environment", input.environment);
if (input.agentId) query.set("agentId", input.agentId);
const suffix = query.size ? `?${query.toString()}` : "";
const result = await this.request<{
variables: AgentRuntimeVariableMetadata[];
}>(
`/api/managed-agents/projects/${encodeURIComponent(input.projectId)}/runtime-variables${suffix}`,
);
return result.variables;
}

putRuntimeVariable(input: {
projectId: string;
name: string;
value: string;
environment: "development" | "production";
agentId?: string;
}) {
return this.request<AgentRuntimeVariableMetadata>(
`/api/managed-agents/projects/${encodeURIComponent(input.projectId)}/runtime-variables/${encodeURIComponent(input.name)}`,
{
method: "PUT",
body: JSON.stringify({
value: input.value,
environment: input.environment,
...(input.agentId ? { agentId: input.agentId } : {}),
}),
},
);
}

deleteRuntimeVariable(input: {
projectId: string;
name: string;
environment: "development" | "production";
agentId?: string;
}) {
const query = new URLSearchParams({ environment: input.environment });
if (input.agentId) query.set("agentId", input.agentId);
return this.request<void>(
`/api/managed-agents/projects/${encodeURIComponent(input.projectId)}/runtime-variables/${encodeURIComponent(input.name)}?${query.toString()}`,
{ method: "DELETE" },
);
}

logs(input: {
agentId?: string;
sessionId?: string;
Expand Down
82 changes: 78 additions & 4 deletions cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ async function readSecretValue(): Promise<string> {
for await (const chunk of process.stdin) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
const value = Buffer.concat(chunks)
.toString("utf8")
.replace(/\r?\n$/, "");
if (!value) throw new Error("Secret value was empty");
return value;
}
Expand Down Expand Up @@ -663,9 +665,7 @@ export async function runCommand(
}
const name = args.shift();
if (!name) {
throw new Error(
"Use `opencomputer secrets set|list|remove <name>`.",
);
throw new Error("Use `opencomputer secrets set|list|remove <name>`.");
}
if (action === "set") {
const explicitOrigins = options(args, "--allow-origin");
Expand Down Expand Up @@ -724,6 +724,80 @@ export async function runCommand(
throw new Error("Use `opencomputer secrets set`, `list`, or `remove`.");
}

if (command === "env") {
const action = args.shift();
const projectReference = option(args, "--project");
const agentOption = option(args, "--agent");
const environment = environmentOption(option(args, "--environment"));
const project = await selectedProject(
client,
config,
projectReference,
!globals.json,
);
const agentId = agentOption
? agentOption === "current"
? project.agentId
: agentOption
: undefined;
if (action === "list") {
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
const variables = await client.runtimeVariables({
projectId: project.projectId,
environment,
...(agentId ? { agentId } : {}),
});
if (globals.json) printJSON(variables);
else if (!variables.length)
process.stdout.write("No runtime variables.\n");
else {
for (const variable of variables) {
process.stdout.write(
`${variable.name.padEnd(28)} ${variable.environment.padEnd(12)} ` +
`${variable.agentId ?? "project"}\n`,
);
}
}
return;
}
const name = args.shift()?.trim().toUpperCase();
if (!name) {
throw new Error("Use `opencomputer env set|list|remove <name>`.");
}
if (action === "set") {
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
const variable = await client.putRuntimeVariable({
projectId: project.projectId,
name,
value: await readSecretValue(),
environment,
...(agentId ? { agentId } : {}),
});
if (globals.json) printJSON(variable);
else {
process.stdout.write(
`Set ${variable.name} for ${variable.agentId ?? "project"} ` +
`(${variable.environment}). Restart the agent runtime to apply it.\n`,
);
}
return;
}
if (action === "remove" || action === "delete") {
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
await client.deleteRuntimeVariable({
projectId: project.projectId,
name,
environment,
...(agentId ? { agentId } : {}),
});
if (globals.json)
printJSON({ removed: true, name, environment, agentId });
else process.stdout.write(`Removed runtime variable ${name}.\n`);
return;
}
throw new Error("Use `opencomputer env set|list|remove <name>`.");
}

if (command === "logs") {
const follow = flag(args, "--follow");
let agentId = option(args, "--agent");
Expand Down
3 changes: 3 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ Usage:
opencomputer secrets set <name> [--environment development|production] [--agent <agent>|current]
opencomputer secrets list [--environment development|production] [--agent <agent>|current]
opencomputer secrets remove <name> [--environment development|production] [--agent <agent>|current]
opencomputer env set <name> [--environment development|production] [--agent <agent>|current]
opencomputer env list [--environment development|production] [--agent <agent>|current]
opencomputer env remove <name> [--environment development|production] [--agent <agent>|current]
opencomputer logs [--agent <agent>] [--session <session-id>] [--environment development|production] [--follow]
opencomputer deploy [--alias <alias>]
opencomputer run <agent> <prompt> [--keep]
Expand Down
49 changes: 46 additions & 3 deletions cloudflare-workers/api-edge/src/managed_agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,8 +443,7 @@ describe("managed agents proxy", () => {

expect(fetchSpy).toHaveBeenCalledWith(
expect.objectContaining({
href:
"https://managedagents.test/v1/outboxes?agentId=reviewer-agent&environment=development",
href: "https://managedagents.test/v1/outboxes?agentId=reviewer-agent&environment=development",
}),
expect.anything(),
);
Expand Down Expand Up @@ -918,6 +917,48 @@ describe("managed agents proxy", () => {
});
});

it("forwards runtime variable metadata without returning its value", async () => {
const fetchSpy = vi.fn(async () =>
Response.json({
name: "DATABASE_URL",
value: "postgres://must-not-leak",
projectId: "prj_1",
environment: "production",
createdAt: "2026-08-18T00:00:00.000Z",
updatedAt: "2026-08-18T00:00:00.000Z",
}),
);
vi.stubGlobal("fetch", fetchSpy);

const response = await proxyManagedAgents(
new Request(
"https://app.opencomputer.dev/api/managed-agents/projects/prj_1/runtime-variables/DATABASE_URL",
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({
value: "postgres://must-not-leak",
environment: "production",
}),
},
),
{
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(200);
const body = await response.json();
expect(body).toMatchObject({
name: "DATABASE_URL",
environment: "production",
});
expect(JSON.stringify(body)).not.toContain("postgres://must-not-leak");
});

it("forwards redacted reactive render snapshots for the debug playground", async () => {
vi.stubGlobal(
"fetch",
Expand Down Expand Up @@ -967,7 +1008,9 @@ describe("managed agents proxy", () => {
],
});
expect(JSON.stringify(body)).not.toContain("never-return-this");
expect(JSON.stringify(body)).not.toContain("You are an OpenComputer agent.");
expect(JSON.stringify(body)).not.toContain(
"You are an OpenComputer agent.",
);
expect(JSON.stringify(body)).not.toContain("platformInstructions");
});

Expand Down
28 changes: 28 additions & 0 deletions cloudflare-workers/api-edge/src/managed_agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,20 @@ function stripPrivateValues(value: unknown): unknown {
);
}

function publicRuntimeVariable(value: unknown): Record<string, unknown> {
const variable = record(value) ?? {};
return {
name: variable.name,
projectId: variable.projectId,
environment: variable.environment,
...(typeof variable.agentId === "string"
? { agentId: variable.agentId }
: {}),
createdAt: variable.createdAt,
updatedAt: variable.updatedAt,
};
}

const PRIVATE_EVENT_KEYS = new Set([
"accountId",
"account_id",
Expand Down Expand Up @@ -400,6 +414,14 @@ function publicSuccessBody(
) {
return stripPrivateValues(body);
}
if (
(method === "GET" || method === "PUT") &&
/^\/projects\/[^/]+\/runtime-variables(?:\/[^/]+)?$/.test(suffix)
) {
return Array.isArray(body.variables)
? { variables: body.variables.map(publicRuntimeVariable) }
: publicRuntimeVariable(body);
}
if (method === "GET" && suffix === "/logs") {
return stripPrivateValues(body);
}
Expand Down Expand Up @@ -714,6 +736,12 @@ function isAllowedManagedAgentsRoute(method: string, suffix: string): boolean {
) {
return true;
}
if (
(method === "GET" || method === "PUT" || method === "DELETE") &&
/^\/projects\/[^/]+\/runtime-variables(?:\/[^/]+)?$/.test(suffix)
) {
return true;
}
if (method === "GET" && suffix === "/logs") return true;
if (method === "POST" && suffix === "/deployments") return true;
if (method === "POST" && suffix === "/benchmarks/warm-pool") return true;
Expand Down
Loading
Loading