fix(adapters): keep user-scoped env bindings on the agent Test action

The agent "Test" action dropped a user-scoped environment variable that a
real run keeps. In create mode the form builds the adapter config with the
adapter build-config parser. That parser recognized only `plain` and
`secret_ref` bindings and dropped every `user_secret_ref` binding. So the
resolved config sent to the test route had no binding to resolve, and the
probe ran without the variable.

Add one shared, binding-type-agnostic parser in `@paperclipai/adapter-utils`
(`buildAdapterEnvConfig`, `parseEnvBindings`, `parseEnvVars`). It keeps
`plain`, `secret_ref`, and `user_secret_ref` bindings so the server resolver
receives each one, the same way a real run does. The parser never resolves a
secret and never reads a secret value; it only preserves the binding shape.

Replace the eight byte-identical copies of the parser across the adapter
build-config files with the shared helper. This removes the duplication and
fixes the same drop in every adapter at once. Edit mode already passed the env
map through unchanged, so it was not affected.

Add unit tests: the shared parser keeps each binding type and the create-mode
build-config keeps a `user_secret_ref` binding.

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Priya Raman 2026-08-05 23:03:58 +00:00
parent 72b509c895
commit c09d2509e3
No known key found for this signature in database
GPG Key ID: 4861541D36B2037E
12 changed files with 232 additions and 408 deletions

View File

@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import { buildAdapterEnvConfig, parseEnvBindings, parseEnvVars } from "./env-bindings.js";
describe("parseEnvBindings", () => {
it("keeps plain, company secret, and user-scoped bindings", () => {
const env = parseEnvBindings({
PLAIN: { type: "plain", value: "on" },
LEGACY_STRING: "raw",
COMPANY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" },
USER: { type: "user_secret_ref", key: "github_token", version: "latest", required: true },
});
expect(env).toEqual({
PLAIN: { type: "plain", value: "on" },
LEGACY_STRING: { type: "plain", value: "raw" },
COMPANY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" },
USER: { type: "user_secret_ref", key: "github_token", version: "latest", required: true },
});
});
it("keeps a user-scoped binding without an optional version or required flag", () => {
const env = parseEnvBindings({
USER: { type: "user_secret_ref", key: "github_token" },
});
expect(env).toEqual({ USER: { type: "user_secret_ref", key: "github_token" } });
});
it("preserves allowMissingOverride when present", () => {
const env = parseEnvBindings({
USER: { type: "user_secret_ref", key: "k", allowMissingOverride: true },
});
expect(env.USER).toEqual({ type: "user_secret_ref", key: "k", allowMissingOverride: true });
});
it("drops invalid keys, unknown shapes, and incomplete refs", () => {
const env = parseEnvBindings({
"1BAD": { type: "plain", value: "x" },
MISSING_KEY: { type: "user_secret_ref" },
MISSING_ID: { type: "secret_ref" },
UNKNOWN: { type: "mystery", value: "y" },
});
expect(env).toEqual({});
});
it("returns an empty map for non-object input", () => {
expect(parseEnvBindings(null)).toEqual({});
expect(parseEnvBindings([])).toEqual({});
expect(parseEnvBindings("nope")).toEqual({});
});
});
describe("parseEnvVars", () => {
it("reads KEY=value lines and skips blanks, comments, and invalid keys", () => {
// The key is trimmed; the value keeps every character after the first "=".
const env = parseEnvVars("A=1\n# comment\n\nB = two = three\n1BAD=x\nNOEQ");
expect(env).toEqual({ A: "1", B: " two = three" });
});
});
describe("buildAdapterEnvConfig", () => {
it("lets a structured binding win over a legacy plain-text entry with the same key", () => {
const env = buildAdapterEnvConfig(
{ SHARED: { type: "user_secret_ref", key: "token" } },
"SHARED=legacy\nONLY_LEGACY=x",
);
expect(env).toEqual({
SHARED: { type: "user_secret_ref", key: "token" },
ONLY_LEGACY: { type: "plain", value: "x" },
});
});
it("tolerates missing legacy text", () => {
expect(buildAdapterEnvConfig({ A: { type: "plain", value: "1" } }, undefined)).toEqual({
A: { type: "plain", value: "1" },
});
});
});

View File

@ -0,0 +1,99 @@
// Shared parser for the agent configuration form environment fields.
//
// The form supplies two sources for adapter `env`:
// - `envBindings`: structured bindings keyed by variable name. A binding is a
// plain value, a company `secret_ref`, or a user-scoped `user_secret_ref`.
// - `envVars`: a legacy plain-text block of `KEY=value` lines.
//
// The server resolves every binding to a string, both at run time and at test
// time. This parser must keep each binding shape intact so the resolver gets
// it. A dropped binding makes the agent "Test" action miss a variable that a
// real run receives. So this parser keeps all three binding types. It never
// resolves a secret and never reads a secret value; it only preserves the
// binding shape for the server resolver.
const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
function isVersionSelector(value: unknown): value is number | "latest" {
return typeof value === "number" || value === "latest";
}
/**
* Convert the structured `envBindings` map into an adapter `env` map. Keep
* `plain`, `secret_ref`, and `user_secret_ref` bindings. Drop entries with an
* invalid variable name or an unknown binding shape.
*/
export function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!ENV_KEY_RE.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(isVersionSelector(rec.version) ? { version: rec.version } : {}),
};
continue;
}
if (rec.type === "user_secret_ref" && typeof rec.key === "string") {
env[key] = {
type: "user_secret_ref",
key: rec.key,
...(isVersionSelector(rec.version) ? { version: rec.version } : {}),
...(typeof rec.required === "boolean" ? { required: rec.required } : {}),
...(typeof rec.allowMissingOverride === "boolean"
? { allowMissingOverride: rec.allowMissingOverride }
: {}),
};
}
}
return env;
}
/**
* Parse the legacy plain-text `KEY=value` block. Skip blank lines, comment
* lines, and lines with an invalid variable name.
*/
export function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!ENV_KEY_RE.test(key)) continue;
env[key] = value;
}
return env;
}
/**
* Build the adapter `env` map from both form sources. A structured binding wins
* over a legacy plain-text entry with the same key.
*/
export function buildAdapterEnvConfig(
envBindings: unknown,
envVars: string | undefined | null,
): Record<string, unknown> {
const env = parseEnvBindings(envBindings);
const legacy = parseEnvVars(envVars ?? "");
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
return env;
}

View File

@ -65,6 +65,11 @@ export {
redactCommandText,
} from "./command-redaction.js";
export { buildSandboxNpmInstallCommand } from "./sandbox-install-command.js";
export {
buildAdapterEnvConfig,
parseEnvBindings,
parseEnvVars,
} from "./env-bindings.js";
export { createRuntimeProgressReporter } from "./runtime-progress.js";
export type {
RuntimeProgressSink,

View File

@ -47,4 +47,34 @@ describe("buildClaudeLocalConfig", () => {
expect(buildClaudeLocalConfig(makeValues({ claudeEngine: "cli" }))).toMatchObject({ engine: "cli" });
expect(buildClaudeLocalConfig(makeValues({ claudeEngine: "acp" }))).toMatchObject({ engine: "acp" });
});
it("keeps user-scoped env bindings so the server resolves them at test time", () => {
const config = buildClaudeLocalConfig(
makeValues({
envBindings: {
GH_TOKEN: { type: "user_secret_ref", key: "github_token", version: "latest", required: true },
},
}),
);
expect(config.env).toEqual({
GH_TOKEN: { type: "user_secret_ref", key: "github_token", version: "latest", required: true },
});
});
it("keeps company secret and plain env bindings", () => {
const config = buildClaudeLocalConfig(
makeValues({
envBindings: {
API_KEY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" },
FLAG: { type: "plain", value: "on" },
},
}),
);
expect(config.env).toEqual({
API_KEY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" },
FLAG: { type: "plain", value: "on" },
});
});
});

View File

@ -1,4 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
function parseCommaArgs(value: string): string[] {
return value
@ -7,49 +7,6 @@ function parseCommaArgs(value: string): string[] {
.filter(Boolean);
}
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
function parseJsonObject(text: string): Record<string, unknown> | null {
const trimmed = text.trim();
if (!trimmed) return null;
@ -84,13 +41,7 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, un
if (v.chrome) ac.chrome = true;
ac.timeoutSec = 0;
ac.graceSec = 15;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(v.envBindings, v.envVars);
if (Object.keys(env).length > 0) ac.env = env;
ac.maxTurnsPerRun = v.maxTurnsPerRun;
ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions;

View File

@ -1,4 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
import { DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX } from "../index.js";
function parseCommaArgs(value: string): string[] {
@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] {
.filter(Boolean);
}
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
function parseJsonObject(text: string): Record<string, unknown> | null {
const trimmed = text.trim();
if (!trimmed) return null;
@ -79,13 +36,7 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record<string, unk
}
ac.timeoutSec = 0;
ac.graceSec = 15;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(v.envBindings, v.envVars);
if (Object.keys(env).length > 0) ac.env = env;
ac.search = v.search;
ac.fastMode = v.fastMode;

View File

@ -1,47 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
export function buildCursorCloudConfig(values: CreateConfigValues): Record<string, unknown> {
const config: Record<string, unknown> = {
@ -52,13 +9,7 @@ export function buildCursorCloudConfig(values: CreateConfigValues): Record<strin
if (values.bootstrapPrompt) config.bootstrapPromptTemplate = values.bootstrapPrompt;
if (values.model?.trim()) config.model = values.model.trim();
const env = parseEnvBindings(values.envBindings);
const legacy = parseEnvVars(values.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(values.envBindings, values.envVars);
if (Object.keys(env).length > 0) {
config.env = env;
}

View File

@ -1,4 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
import { DEFAULT_CURSOR_LOCAL_MODEL } from "../index.js";
function parseCommaArgs(value: string): string[] {
@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] {
.filter(Boolean);
}
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
function normalizeMode(value: string): "plan" | "ask" | null {
const mode = value.trim().toLowerCase();
if (mode === "plan" || mode === "ask") return mode;
@ -66,13 +23,7 @@ export function buildCursorLocalConfig(v: CreateConfigValues): Record<string, un
if (mode) ac.mode = mode;
ac.timeoutSec = 0;
ac.graceSec = 15;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(v.envBindings, v.envVars);
if (Object.keys(env).length > 0) ac.env = env;
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);

View File

@ -1,4 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js";
function parseCommaArgs(value: string): string[] {
@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] {
.filter(Boolean);
}
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
export function buildGeminiLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
@ -66,13 +23,7 @@ export function buildGeminiLocalConfig(v: CreateConfigValues): Record<string, un
ac.model = v.model || DEFAULT_GEMINI_LOCAL_MODEL;
ac.timeoutSec = 0;
ac.graceSec = 15;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(v.envBindings, v.envVars);
if (Object.keys(env).length > 0) ac.env = env;
ac.sandbox = !v.dangerouslyBypassSandbox;

View File

@ -1,4 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
import { DEFAULT_GROK_LOCAL_MODEL } from "../index.js";
function parseCommaArgs(value: string): string[] {
@ -8,49 +8,6 @@ function parseCommaArgs(value: string): string[] {
.filter(Boolean);
}
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
export function buildGrokLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
@ -59,13 +16,7 @@ export function buildGrokLocalConfig(v: CreateConfigValues): Record<string, unkn
ac.timeoutSec = 0;
ac.graceSec = 20;
if (v.thinkingEffort) ac.reasoningEffort = v.thinkingEffort;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(v.envBindings, v.envVars);
if (Object.keys(env).length > 0) ac.env = env;
if (v.command) ac.command = v.command;

View File

@ -1,4 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
function parseCommaArgs(value: string): string[] {
return value
@ -7,49 +7,6 @@ function parseCommaArgs(value: string): string[] {
.filter(Boolean);
}
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
export function buildOpenCodeLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
if (v.cwd) ac.cwd = v.cwd;
@ -61,13 +18,7 @@ export function buildOpenCodeLocalConfig(v: CreateConfigValues): Record<string,
// and rely on graceSec for termination handling when a timeout is configured elsewhere.
ac.timeoutSec = 0;
ac.graceSec = 20;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(v.envBindings, v.envVars);
if (Object.keys(env).length > 0) ac.env = env;
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs);

View File

@ -1,47 +1,4 @@
import type { CreateConfigValues } from "@paperclipai/adapter-utils";
function parseEnvVars(text: string): Record<string, string> {
const env: Record<string, string> = {};
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const eq = trimmed.indexOf("=");
if (eq <= 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1);
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
env[key] = value;
}
return env;
}
function parseEnvBindings(bindings: unknown): Record<string, unknown> {
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return {};
const env: Record<string, unknown> = {};
for (const [key, raw] of Object.entries(bindings)) {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
if (typeof raw === "string") {
env[key] = { type: "plain", value: raw };
continue;
}
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
const rec = raw as Record<string, unknown>;
if (rec.type === "plain" && typeof rec.value === "string") {
env[key] = { type: "plain", value: rec.value };
continue;
}
if (rec.type === "secret_ref" && typeof rec.secretId === "string") {
env[key] = {
type: "secret_ref",
secretId: rec.secretId,
...(typeof rec.version === "number" || rec.version === "latest"
? { version: rec.version }
: {}),
};
}
}
return env;
}
import { buildAdapterEnvConfig, type CreateConfigValues } from "@paperclipai/adapter-utils";
export function buildPiLocalConfig(v: CreateConfigValues): Record<string, unknown> {
const ac: Record<string, unknown> = {};
@ -54,13 +11,7 @@ export function buildPiLocalConfig(v: CreateConfigValues): Record<string, unknow
ac.timeoutSec = 0;
ac.graceSec = 20;
const env = parseEnvBindings(v.envBindings);
const legacy = parseEnvVars(v.envVars);
for (const [key, value] of Object.entries(legacy)) {
if (!Object.prototype.hasOwnProperty.call(env, key)) {
env[key] = { type: "plain", value };
}
}
const env = buildAdapterEnvConfig(v.envBindings, v.envVars);
if (Object.keys(env).length > 0) ac.env = env;
if (v.command) ac.command = v.command;
if (v.extraArgs) ac.extraArgs = v.extraArgs;