@open-cr-agent/runtime-opencode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/quota.js ADDED
@@ -0,0 +1,38 @@
1
+ // Longest wait for a rate limit inside a run; per-minute limits ask for less.
2
+ export const MAX_QUOTA_WAIT_MS = 90_000;
3
+ // Waits per model before it counts as out of quota for the rest of the run.
4
+ export const QUOTA_RETRIES = 3;
5
+ const QUOTA_MESSAGE = /exceeded your current quota|quota exceeded|resource[_ ]exhausted|rate limit/i;
6
+ const RETRY_IN = /retry in ([\d.]+)\s*(ms|s)\b/i;
7
+ const RETRY_DELAY = /"retryDelay"\s*:\s*"([\d.]+)s"/i;
8
+ const DAILY = /per ?day|daily/i;
9
+ export function parseQuotaError(message, statusCode) {
10
+ if (statusCode !== 429 && !QUOTA_MESSAGE.test(message))
11
+ return undefined;
12
+ const quota = { daily: DAILY.test(message) };
13
+ const retryIn = RETRY_IN.exec(message);
14
+ const delay = RETRY_DELAY.exec(message);
15
+ if (retryIn) {
16
+ const value = Number(retryIn[1]);
17
+ quota.retryAfterMs = Math.ceil(retryIn[2]?.toLowerCase() === "ms" ? value : value * 1000);
18
+ }
19
+ else if (delay) {
20
+ quota.retryAfterMs = Math.ceil(Number(delay[1]) * 1000);
21
+ }
22
+ return quota;
23
+ }
24
+ // Resolves after `ms`, or at once when the signal aborts.
25
+ export function sleep(ms, signal) {
26
+ if (signal.aborted || ms <= 0)
27
+ return Promise.resolve();
28
+ return new Promise((resolve) => {
29
+ const done = () => {
30
+ clearTimeout(timer);
31
+ signal.removeEventListener("abort", done);
32
+ resolve();
33
+ };
34
+ const timer = setTimeout(done, ms);
35
+ signal.addEventListener("abort", done, { once: true });
36
+ });
37
+ }
38
+ //# sourceMappingURL=quota.js.map
@@ -0,0 +1,21 @@
1
+ import { type ToolDefinition } from "@open-cr-agent/core";
2
+ import { z } from "zod";
3
+ export declare const MAX_READ_LINES = 400;
4
+ export declare const MAX_SEARCH_RESULTS = 50;
5
+ export declare const MAX_LINE_CHARS = 2000;
6
+ export declare const MAX_RESULT_CHARS = 50000;
7
+ export declare const reportFindingInput: z.ZodObject<{
8
+ file: z.ZodString;
9
+ existingCode: z.ZodString;
10
+ severity: z.ZodEnum<{
11
+ critical: "critical";
12
+ suggestion: "suggestion";
13
+ warning: "warning";
14
+ }>;
15
+ title: z.ZodString;
16
+ body: z.ZodString;
17
+ suggestion: z.ZodOptional<z.ZodString>;
18
+ evidence: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
+ }, z.core.$strip>;
20
+ export declare const reviewTools: readonly ToolDefinition[];
21
+ //# sourceMappingURL=review-tools.d.ts.map
@@ -0,0 +1,111 @@
1
+ import { promptData, REVIEW_TOOLS, severitySchema } from "@open-cr-agent/core";
2
+ import { z } from "zod";
3
+ export const MAX_READ_LINES = 400;
4
+ export const MAX_SEARCH_RESULTS = 50;
5
+ // Lines and results are also capped in characters: one line of a minified
6
+ // bundle or a JSON fixture can be megabytes, and every step resends it.
7
+ export const MAX_LINE_CHARS = 2_000;
8
+ export const MAX_RESULT_CHARS = 50_000;
9
+ function clip(line, max = MAX_LINE_CHARS) {
10
+ return line.length > max ? `${line.slice(0, max)}…[${line.length - max} more characters]` : line;
11
+ }
12
+ // The whole lines that fit in the result cap.
13
+ function fitting(lines) {
14
+ const kept = [];
15
+ let size = 0;
16
+ for (const line of lines) {
17
+ size += line.length + 1;
18
+ if (size > MAX_RESULT_CHARS)
19
+ break;
20
+ kept.push(line);
21
+ }
22
+ return kept;
23
+ }
24
+ export const reportFindingInput = z.object({
25
+ file: z.string().min(1).describe("Path of a file in <ocra_review_files>"),
26
+ existingCode: z
27
+ .string()
28
+ .min(1)
29
+ .describe("1-5 lines copied verbatim from the new version of the file"),
30
+ severity: severitySchema,
31
+ title: z.string().min(1),
32
+ body: z.string().min(1),
33
+ suggestion: z.string().optional(),
34
+ evidence: z.array(z.string()).optional(),
35
+ });
36
+ // Tool results carry repository text back to the model, so they are data
37
+ // like every prompt section: they cannot form one of ocra's tags.
38
+ const readFile = {
39
+ name: REVIEW_TOOLS.readFile,
40
+ description: `Read a file at the revision under review, with line numbers. Returns at most ${MAX_READ_LINES} lines; use startLine to page.`,
41
+ inputSchema: z.object({
42
+ path: z.string().min(1),
43
+ startLine: z.number().int().positive().optional(),
44
+ }),
45
+ async execute(args, context) {
46
+ const { path, startLine = 1 } = args;
47
+ const content = await context.readFile(path);
48
+ if (content === undefined)
49
+ return promptData(`File not found: ${path}`);
50
+ const lines = content.split("\n");
51
+ const end = Math.min(lines.length, startLine + MAX_READ_LINES - 1);
52
+ const body = fitting(lines.slice(startLine - 1, end).map((line, i) => `${startLine + i}: ${clip(line)}`));
53
+ const next = startLine + body.length;
54
+ if (next <= lines.length) {
55
+ body.push(`[truncated: ${lines.length - next + 1} more lines; call again with startLine=${next}]`);
56
+ }
57
+ return promptData(body.join("\n"));
58
+ },
59
+ };
60
+ const readDiff = {
61
+ name: REVIEW_TOOLS.readDiff,
62
+ description: "Read the diff of any changed file in this change, including files outside your bundle.",
63
+ inputSchema: z.object({ path: z.string().min(1) }),
64
+ async execute(args, context) {
65
+ const { path } = args;
66
+ const diff = context.readDiff(path);
67
+ if (diff === undefined)
68
+ return promptData(`No changes to ${path} in this change.`);
69
+ const lines = diff.split("\n").map((line) => clip(line));
70
+ const kept = fitting(lines);
71
+ if (kept.length < lines.length)
72
+ kept.push(`[diff truncated after ${kept.length} lines]`);
73
+ return promptData(kept.join("\n"));
74
+ },
75
+ };
76
+ const codeSearch = {
77
+ name: REVIEW_TOOLS.codeSearch,
78
+ description: `Search the revision under review for a literal string (not a regex). Returns up to ${MAX_SEARCH_RESULTS} matches as path:line: text.`,
79
+ inputSchema: z.object({ literal: z.string().min(2) }),
80
+ async execute(args, context) {
81
+ const { literal } = args;
82
+ const matches = await context.searchCode(literal);
83
+ if (matches.length === 0)
84
+ return "No matches.";
85
+ const shown = fitting(matches.slice(0, MAX_SEARCH_RESULTS).map((m) => `${m.path}:${m.line}: ${clip(m.text, 300)}`));
86
+ if (shown.length < matches.length) {
87
+ shown.push(`[${matches.length - shown.length} more matches omitted]`);
88
+ }
89
+ return promptData(shown.join("\n"));
90
+ },
91
+ };
92
+ const reportFinding = {
93
+ name: REVIEW_TOOLS.reportFinding,
94
+ description: "Report one confirmed defect. Call once per issue.",
95
+ inputSchema: reportFindingInput,
96
+ execute: async () => "Recorded.",
97
+ };
98
+ const taskDone = {
99
+ name: REVIEW_TOOLS.taskDone,
100
+ description: "Call once every file in <ocra_review_files> has been reviewed.",
101
+ inputSchema: z.object({}),
102
+ execute: async () => "Done.",
103
+ };
104
+ export const reviewTools = [
105
+ readFile,
106
+ readDiff,
107
+ codeSearch,
108
+ reportFinding,
109
+ taskDone,
110
+ ];
111
+ //# sourceMappingURL=review-tools.js.map
@@ -0,0 +1,71 @@
1
+ import { type AgentEvent, type AgentRuntime, type AgentTaskSpec, type CompletionRequest, type CompletionResult, type RuntimeOptions } from "@open-cr-agent/core";
2
+ import { type ToolServer } from "./tool-server.js";
3
+ export declare const MCP_SERVER = "ocra";
4
+ export declare const MAX_AGENT_STEPS = 30;
5
+ export declare const HELPER_AGENT_STEPS = 2;
6
+ export declare const REVIEW_RESUME: {
7
+ doneTool: string;
8
+ maxSteps: number;
9
+ message: string;
10
+ };
11
+ export declare const OPENCODE_BUILTIN_TOOLS: readonly ["invalid", "question", "bash", "read", "glob", "grep", "edit", "write", "task", "webfetch", "todowrite", "websearch", "skill", "apply_patch"];
12
+ export interface OpenCodeRuntimeOptions extends RuntimeOptions {
13
+ binary?: string;
14
+ }
15
+ export declare class OpenCodeRuntime implements AgentRuntime {
16
+ private readonly options;
17
+ readonly name = "opencode";
18
+ private infra;
19
+ private context;
20
+ private readonly health;
21
+ private readonly helperTools;
22
+ constructor(options: OpenCodeRuntimeOptions);
23
+ runTask(spec: AgentTaskSpec, signal: AbortSignal): AsyncIterable<AgentEvent>;
24
+ complete(request: CompletionRequest, signal: AbortSignal): Promise<CompletionResult>;
25
+ dispose(): Promise<void>;
26
+ private prompt;
27
+ private start;
28
+ private launch;
29
+ }
30
+ export declare function openCodeConfig(tools: Pick<ToolServer, "url" | "headers">, helperTools: Record<string, boolean>): {
31
+ share: string;
32
+ autoupdate: boolean;
33
+ mcp: {
34
+ ocra: {
35
+ type: string;
36
+ url: string;
37
+ headers: Record<string, string>;
38
+ oauth: boolean;
39
+ enabled: boolean;
40
+ };
41
+ };
42
+ agent: {
43
+ "ocra-reviewer": {
44
+ mode: string;
45
+ prompt: string;
46
+ steps: number;
47
+ tools: {
48
+ [k: string]: boolean;
49
+ };
50
+ permission: {
51
+ edit: string;
52
+ bash: string;
53
+ webfetch: string;
54
+ skill: string;
55
+ };
56
+ };
57
+ "ocra-helper": {
58
+ mode: string;
59
+ prompt: string;
60
+ steps: number;
61
+ tools: Record<string, boolean>;
62
+ permission: {
63
+ edit: string;
64
+ bash: string;
65
+ webfetch: string;
66
+ skill: string;
67
+ };
68
+ };
69
+ };
70
+ };
71
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1,246 @@
1
+ import { rmSync } from "node:fs";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { addUsage, CompletionError, emptyUsage, REVIEW_TOOLS, } from "@open-cr-agent/core";
6
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2";
7
+ import { resolveOpencodeBinary } from "./binary.js";
8
+ import { withFailback } from "./failback.js";
9
+ import { ModelHealth, parseModel } from "./models.js";
10
+ import { startOpencodeServer } from "./opencode-server.js";
11
+ import { sleep } from "./quota.js";
12
+ import { reviewTools } from "./review-tools.js";
13
+ import { missingCredentials, serverEnv } from "./server-env.js";
14
+ import { promptSession } from "./session-prompt.js";
15
+ import { startToolServer } from "./tool-server.js";
16
+ import { createUntimedDispatcher, untimedFetch } from "./transport.js";
17
+ export const MCP_SERVER = "ocra";
18
+ const REVIEW_AGENT = "ocra-reviewer";
19
+ const HELPER_AGENT = "ocra-helper";
20
+ // Must not be empty: OpenCode falls back to its full coding prompt otherwise.
21
+ const REVIEW_AGENT_PROMPT = "You are a code review agent run by ocra. Follow the review instructions below.";
22
+ const HELPER_AGENT_PROMPT = "You answer exactly as the instructions below ask, with no tools.";
23
+ // Each step resends the whole conversation, so an unbounded loop is the
24
+ // largest cost risk. At 20 steps a quarter of the review tasks on Vertex
25
+ // ended at the cap and one golden bug was never found; at 30 it was found in
26
+ // both runs, for about a third more cost on average (2026-09-28). Most tasks
27
+ // finish in about 15 steps and never reach it.
28
+ export const MAX_AGENT_STEPS = 30;
29
+ // The helper answers in one step and has no tools. It still needs two:
30
+ // OpenCode appends an assistant message on an agent's last allowed step, and
31
+ // Gemini rejects a request that ends with a model turn (#66).
32
+ export const HELPER_AGENT_STEPS = 2;
33
+ // Sent once to a review agent that stopped before finishing (session-prompt.ts).
34
+ export const REVIEW_RESUME = {
35
+ doneTool: `${MCP_SERVER}_${REVIEW_TOOLS.taskDone}`,
36
+ maxSteps: MAX_AGENT_STEPS,
37
+ message: `You stopped before finishing the review. Continue with the files in <ocra_review_files> you have not reviewed yet, report each confirmed issue with ${REVIEW_TOOLS.reportFinding}, and call ${REVIEW_TOOLS.taskDone} when every file is done.`,
38
+ };
39
+ // Every OpenCode built-in tool of the pinned version; a test fails when an
40
+ // upgrade adds one, so a new write-capable tool can never be enabled silently.
41
+ export const OPENCODE_BUILTIN_TOOLS = [
42
+ "invalid",
43
+ "question",
44
+ "bash",
45
+ "read",
46
+ "glob",
47
+ "grep",
48
+ "edit",
49
+ "write",
50
+ "task",
51
+ "webfetch",
52
+ "todowrite",
53
+ "websearch",
54
+ "skill",
55
+ "apply_patch",
56
+ ];
57
+ const DISABLED_BUILTINS = Object.fromEntries(OPENCODE_BUILTIN_TOOLS.map((t) => [t, false]));
58
+ export class OpenCodeRuntime {
59
+ options;
60
+ name = "opencode";
61
+ infra;
62
+ context;
63
+ health = new ModelHealth();
64
+ helperTools;
65
+ constructor(options) {
66
+ this.options = options;
67
+ const mcpTools = [...reviewTools, ...options.tools].map((t) => [
68
+ `${MCP_SERVER}_${t.name}`,
69
+ false,
70
+ ]);
71
+ this.helperTools = { ...DISABLED_BUILTINS, ...Object.fromEntries(mcpTools) };
72
+ }
73
+ async *runTask(spec, signal) {
74
+ if (this.context && this.context !== spec.context) {
75
+ throw new Error("An OpenCodeRuntime instance serves a single review run");
76
+ }
77
+ this.context = spec.context;
78
+ const chain = this.options.models[spec.modelTier] ?? [];
79
+ if (chain.length === 0) {
80
+ yield {
81
+ type: "error",
82
+ taskId: spec.taskId,
83
+ retryable: false,
84
+ error: noModel(spec.modelTier),
85
+ };
86
+ return;
87
+ }
88
+ const infra = await this.start();
89
+ yield* withFailback({
90
+ taskId: spec.taskId,
91
+ tier: spec.modelTier,
92
+ chain,
93
+ health: this.health,
94
+ signal,
95
+ attempt: (model) => this.prompt(infra, {
96
+ title: `ocra ${spec.taskId}`,
97
+ agent: REVIEW_AGENT,
98
+ model,
99
+ system: spec.systemPrompt,
100
+ user: spec.userPrompt,
101
+ tools: DISABLED_BUILTINS,
102
+ resume: REVIEW_RESUME,
103
+ }, signal),
104
+ });
105
+ }
106
+ async complete(request, signal) {
107
+ const chain = this.options.models[request.tier] ?? [];
108
+ if (chain.length === 0)
109
+ throw new Error(noModel(request.tier));
110
+ const infra = await this.start();
111
+ let usage = emptyUsage();
112
+ let lastError = "";
113
+ for (const model of this.health.order(chain)) {
114
+ for (;;) {
115
+ await sleep(this.health.pausedFor(model), signal);
116
+ if (signal.aborted)
117
+ throw new CompletionError("cancelled", usage);
118
+ const outcome = await this.prompt(infra, {
119
+ title: "ocra helper",
120
+ agent: HELPER_AGENT,
121
+ model,
122
+ system: request.system,
123
+ user: request.user,
124
+ tools: this.helperTools,
125
+ }, signal);
126
+ usage = addUsage(usage, outcome.usage);
127
+ if (!outcome.error) {
128
+ this.health.recordSuccess(model);
129
+ return { text: outcome.text, usage };
130
+ }
131
+ if (!outcome.error.retryable) {
132
+ throw new CompletionError(`${model}: ${outcome.error.message}`, usage);
133
+ }
134
+ if (outcome.error.quota && this.health.recordQuota(model, outcome.error.quota) === "wait") {
135
+ continue;
136
+ }
137
+ if (!outcome.error.quota)
138
+ this.health.recordFailure(model);
139
+ lastError = `${model}: ${outcome.error.message}`;
140
+ break;
141
+ }
142
+ }
143
+ if (!lastError) {
144
+ throw new CompletionError(`every ${request.tier} model is out of quota for this run`, usage);
145
+ }
146
+ throw new CompletionError(`every ${request.tier} model failed (${lastError})`, usage);
147
+ }
148
+ async dispose() {
149
+ const infra = await this.infra?.catch(() => undefined);
150
+ this.infra = undefined;
151
+ if (!infra)
152
+ return;
153
+ process.off("exit", infra.onExit);
154
+ await Promise.allSettled([infra.server.close(), infra.tools.close(), infra.dispatcher.close()]);
155
+ await rm(infra.root, { recursive: true, force: true });
156
+ }
157
+ prompt(infra, input, signal) {
158
+ return promptSession(infra.client.session, input, `${MCP_SERVER}_${REVIEW_TOOLS.reportFinding}`, signal);
159
+ }
160
+ start() {
161
+ this.infra ??= this.launch();
162
+ return this.infra;
163
+ }
164
+ async launch() {
165
+ const missing = missingCredentials(this.options.env, providersOf(this.options.models));
166
+ if (missing.length > 0)
167
+ throw new Error(missing.join("; "));
168
+ const root = await mkdtemp(join(tmpdir(), "ocra-opencode-"));
169
+ const dirs = {
170
+ config: join(root, "config"),
171
+ data: join(root, "data"),
172
+ state: join(root, "state"),
173
+ };
174
+ const workspace = join(root, "workspace");
175
+ await Promise.all([...Object.values(dirs), workspace].map((d) => mkdir(d, { recursive: true })));
176
+ const tools = await startToolServer([...reviewTools, ...this.options.tools], () => this.context);
177
+ let started;
178
+ try {
179
+ const server = await startOpencodeServer({
180
+ binary: this.options.binary ?? resolveOpencodeBinary(this.options.env),
181
+ env: serverEnv(this.options.env, dirs, providersOf(this.options.models)),
182
+ config: openCodeConfig(tools, this.helperTools),
183
+ });
184
+ started = server;
185
+ const dispatcher = createUntimedDispatcher();
186
+ const client = createOpencodeClient({
187
+ baseUrl: server.url,
188
+ directory: workspace,
189
+ headers: { Authorization: server.authorization },
190
+ fetch: untimedFetch(dispatcher),
191
+ });
192
+ // A second Ctrl-C exits at once, before dispose() can run: stop
193
+ // OpenCode and remove its directory synchronously on the way out.
194
+ const onExit = () => {
195
+ server.killNow();
196
+ rmSync(root, { recursive: true, force: true });
197
+ };
198
+ process.on("exit", onExit);
199
+ return { root, tools, server, client, dispatcher, onExit };
200
+ }
201
+ catch (error) {
202
+ await Promise.allSettled([tools.close(), started?.close()]);
203
+ await rm(root, { recursive: true, force: true });
204
+ throw error;
205
+ }
206
+ }
207
+ }
208
+ export function openCodeConfig(tools, helperTools) {
209
+ const permission = { edit: "deny", bash: "deny", webfetch: "deny", skill: "deny" };
210
+ return {
211
+ share: "disabled",
212
+ autoupdate: false,
213
+ mcp: {
214
+ [MCP_SERVER]: {
215
+ type: "remote",
216
+ url: tools.url,
217
+ headers: tools.headers,
218
+ oauth: false,
219
+ enabled: true,
220
+ },
221
+ },
222
+ agent: {
223
+ [REVIEW_AGENT]: {
224
+ mode: "primary",
225
+ prompt: REVIEW_AGENT_PROMPT,
226
+ steps: MAX_AGENT_STEPS,
227
+ tools: DISABLED_BUILTINS,
228
+ permission,
229
+ },
230
+ [HELPER_AGENT]: {
231
+ mode: "primary",
232
+ prompt: HELPER_AGENT_PROMPT,
233
+ steps: HELPER_AGENT_STEPS,
234
+ tools: helperTools,
235
+ permission,
236
+ },
237
+ },
238
+ };
239
+ }
240
+ function providersOf(models) {
241
+ return Object.values(models).flatMap((chain) => (chain ?? []).map((m) => parseModel(m).providerID));
242
+ }
243
+ function noModel(tier) {
244
+ return `No model configured for the "${tier}" tier; set models.${tier} in .ocra/config.json or OCRA_MODEL_${tier.toUpperCase()}`;
245
+ }
246
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,10 @@
1
+ import type { Env } from "@open-cr-agent/core";
2
+ export interface IsolatedDirs {
3
+ config: string;
4
+ data: string;
5
+ state: string;
6
+ }
7
+ export declare function missingCredentials(base: Env, providers: readonly string[]): string[];
8
+ export declare const EXTRA_ENV_VARIABLE = "OCRA_RUNTIME_ENV";
9
+ export declare function serverEnv(base: Env, dirs: IsolatedDirs, providers: readonly string[]): Record<string, string>;
10
+ //# sourceMappingURL=server-env.d.ts.map
@@ -0,0 +1,148 @@
1
+ // OpenCode otherwise reads the user's global config, installed skills and the
2
+ // repository's AGENTS.md into every prompt, which leaks local details to the
3
+ // model provider, duplicates our guidelines and costs tokens.
4
+ const ISOLATION_FLAGS = [
5
+ "OPENCODE_DISABLE_PROJECT_CONFIG",
6
+ "OPENCODE_DISABLE_CLAUDE_CODE",
7
+ "OPENCODE_DISABLE_EXTERNAL_SKILLS",
8
+ "OPENCODE_DISABLE_AUTOUPDATE",
9
+ "OPENCODE_DISABLE_SHARE",
10
+ "OPENCODE_DISABLE_LSP_DOWNLOAD",
11
+ "OPENCODE_DISABLE_EMBEDDED_WEB_UI",
12
+ ];
13
+ // What any process needs to run and reach the network, nothing more.
14
+ const SYSTEM_VARIABLES = [
15
+ "PATH",
16
+ "HOME",
17
+ "USER",
18
+ "LOGNAME",
19
+ "TMPDIR",
20
+ "TEMP",
21
+ "TMP",
22
+ "LANG",
23
+ "TZ",
24
+ "HTTP_PROXY",
25
+ "HTTPS_PROXY",
26
+ "NO_PROXY",
27
+ "ALL_PROXY",
28
+ "http_proxy",
29
+ "https_proxy",
30
+ "no_proxy",
31
+ "all_proxy",
32
+ "NODE_EXTRA_CA_CERTS",
33
+ "SSL_CERT_FILE",
34
+ "SSL_CERT_DIR",
35
+ "SystemRoot",
36
+ "ComSpec",
37
+ "PATHEXT",
38
+ "APPDATA",
39
+ "LOCALAPPDATA",
40
+ "USERPROFILE",
41
+ ];
42
+ // Credentials OpenCode reads per provider. A provider not listed here gets
43
+ // every variable that starts with its uppercased id, such as GROQ_API_KEY.
44
+ const PROVIDER_VARIABLES = {
45
+ google: ["GOOGLE_GENERATIVE_AI_API_KEY"],
46
+ "google-vertex": [
47
+ "GOOGLE_APPLICATION_CREDENTIALS",
48
+ "GOOGLE_CLOUD_PROJECT",
49
+ "GOOGLE_CLOUD_LOCATION",
50
+ "GOOGLE_VERTEX_PROJECT",
51
+ "GOOGLE_VERTEX_LOCATION",
52
+ ],
53
+ "amazon-bedrock": [
54
+ "AWS_ACCESS_KEY_ID",
55
+ "AWS_SECRET_ACCESS_KEY",
56
+ "AWS_SESSION_TOKEN",
57
+ "AWS_REGION",
58
+ "AWS_PROFILE",
59
+ "AWS_BEARER_TOKEN_BEDROCK",
60
+ ],
61
+ azure: ["AZURE_API_KEY", "AZURE_RESOURCE_NAME"],
62
+ };
63
+ // Providers that cannot work without one of these variables. Without it
64
+ // OpenCode silently skips the provider and later reports "model not found".
65
+ const REQUIRED_KEYS = {
66
+ google: ["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
67
+ anthropic: ["ANTHROPIC_API_KEY"],
68
+ openai: ["OPENAI_API_KEY"],
69
+ openrouter: ["OPENROUTER_API_KEY"],
70
+ groq: ["GROQ_API_KEY"],
71
+ mistral: ["MISTRAL_API_KEY"],
72
+ deepseek: ["DEEPSEEK_API_KEY"],
73
+ xai: ["XAI_API_KEY"],
74
+ azure: ["AZURE_API_KEY"],
75
+ };
76
+ export function missingCredentials(base, providers) {
77
+ return [...new Set(providers)].flatMap((provider) => {
78
+ const names = REQUIRED_KEYS[provider];
79
+ if (!names || names.some((name) => base[name]))
80
+ return [];
81
+ return [`No API key for provider "${provider}": set ${names.join(" or ")}`];
82
+ });
83
+ }
84
+ const GOOGLE_KEY = "GOOGLE_GENERATIVE_AI_API_KEY";
85
+ const GOOGLE_KEY_ALIASES = ["GEMINI_API_KEY", "GOOGLE_API_KEY"];
86
+ export const EXTRA_ENV_VARIABLE = "OCRA_RUNTIME_ENV";
87
+ // A model chain naming a provider such as "github" or "aws" must not carry
88
+ // the CI's or the cloud's credentials across by prefix; name them in
89
+ // OCRA_RUNTIME_ENV when they are really meant for a provider.
90
+ const NEVER_BY_PREFIX = [
91
+ "GITHUB_",
92
+ "GH_",
93
+ "ACTIONS_",
94
+ "RUNNER_",
95
+ "AWS_",
96
+ "AZURE_",
97
+ "NPM_",
98
+ "SSH_",
99
+ "OCRA_",
100
+ ];
101
+ // The child sees only what it needs: system basics, the credentials of the
102
+ // providers in the configured model chains, and names listed in
103
+ // OCRA_RUNTIME_ENV. Other secrets in the user's shell never reach it.
104
+ export function serverEnv(base, dirs, providers) {
105
+ const env = {};
106
+ const copy = (name) => {
107
+ const value = base[name];
108
+ if (value !== undefined)
109
+ env[name] = value;
110
+ };
111
+ for (const name of SYSTEM_VARIABLES)
112
+ copy(name);
113
+ for (const name of Object.keys(base))
114
+ if (name.startsWith("LC_"))
115
+ copy(name);
116
+ for (const provider of new Set(providers)) {
117
+ const known = PROVIDER_VARIABLES[provider];
118
+ if (known) {
119
+ for (const name of known)
120
+ copy(name);
121
+ }
122
+ else {
123
+ const prefix = `${provider.toUpperCase().replaceAll("-", "_")}_`;
124
+ for (const name of Object.keys(base)) {
125
+ if (name.startsWith(prefix) && !NEVER_BY_PREFIX.some((p) => name.startsWith(p))) {
126
+ copy(name);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ for (const name of (base[EXTRA_ENV_VARIABLE] ?? "").split(",")) {
132
+ if (name.trim() !== "")
133
+ copy(name.trim());
134
+ }
135
+ if (providers.includes("google") && !env[GOOGLE_KEY]) {
136
+ const alias = GOOGLE_KEY_ALIASES.map((name) => base[name]).find((value) => value);
137
+ if (alias)
138
+ env[GOOGLE_KEY] = alias;
139
+ }
140
+ for (const flag of ISOLATION_FLAGS)
141
+ env[flag] = "1";
142
+ env.OPENCODE_CONFIG_DIR = dirs.config;
143
+ env.XDG_CONFIG_HOME = dirs.config;
144
+ env.XDG_DATA_HOME = dirs.data;
145
+ env.XDG_STATE_HOME = dirs.state;
146
+ return env;
147
+ }
148
+ //# sourceMappingURL=server-env.js.map