From e0f3b9448b495f5f74b476075c6905dfe82476d1 Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Wed, 12 Aug 2026 09:40:09 +0800 Subject: [PATCH 1/2] feat: support Qoder environment variables Change-Id: I062cc8f29ab291bf8f43bb6b6f0d82525ea589d7 --- docs/guides/deploy-to-qoder.md | 17 +++++++++++++++++ docs/reference/configuration.md | 2 ++ .../sdk/src/internal/core/session-runtime.ts | 2 ++ .../sdk/src/internal/core/validate-config.ts | 7 +++++++ packages/sdk/src/internal/parser/schema.ts | 1 + .../src/internal/providers/qoder/adapter.ts | 3 +++ .../sdk/src/internal/providers/qoder/mapper.ts | 6 ++++++ .../src/internal/session/session-manager.ts | 8 ++++++++ packages/sdk/src/internal/types/config.ts | 2 ++ packages/sdk/src/internal/types/session.ts | 2 ++ packages/sdk/tests/unit/map-session.test.ts | 8 ++++++++ .../tests/unit/qoder-forward-template.test.ts | 5 +++++ .../sdk/tests/unit/session-manager.test.ts | 2 ++ .../sdk/tests/unit/validate-config.test.ts | 18 ++++++++++++++++++ 14 files changed, 83 insertions(+) diff --git a/docs/guides/deploy-to-qoder.md b/docs/guides/deploy-to-qoder.md index dd039b3..b47bdf6 100644 --- a/docs/guides/deploy-to-qoder.md +++ b/docs/guides/deploy-to-qoder.md @@ -27,6 +27,23 @@ providers: A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side. +## Runtime environment variables + +Declare `environment_variables` on an Agent to inject variables into its Qoder runtime: + +```yaml +agents: + assistant: + model: { qoder: auto } + instructions: Help the user. + environment: dev + environment_variables: + FEATURE_FLAG: "on" + LOG_LEVEL: debug +``` + +OpenAgentPack maps this to each Qoder API's native shape: a top-level object on Forward Templates, `config.environment_variables` on Forward Sessions, and the required `KEY=VALUE;...` string on managed Sessions. Other providers reject this Qoder-specific field during validation. + ## Tool naming Qoder uses PascalCase tool names natively (`Read`, `Glob`, `Grep`, `WebFetch`, `WebSearch`, `Write`, `Edit`, `Bash`). Write tools **lowercase** in config and OpenAgentPack converts them automatically when applying to Qoder — this keeps the same config portable to Bailian, Claude, and Volcengine Ark. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index f06101d..33a0812 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -246,6 +246,7 @@ agents: skills: [ | { type, skill_id, version? } ] vault: memory_stores: [ ] + environment_variables: { : } # Qoder only resources: [ SessionResource ] multiagent: { type: "coordinator", agents: [...] } metadata: { : } @@ -266,6 +267,7 @@ agents: | `skills[]` | string \| AgentSkillRef | no | Skill name or `{ type: "official"\|"custom", skill_id, version? }`. | | `vault` | string | no | Vault name. | | `memory_stores` | string[] | no | Bound memory stores. | +| `environment_variables` | map | no | Qoder runtime variables. Managed Sessions use Qoder's `KEY=VALUE;...` wire format; Forward Templates store the map as defaults and Forward Sessions send it under `config.environment_variables`. | | `resources` | SessionResource[] | no | Resources attached to every managed Session created for the Agent. | | `multiagent.type` | `"coordinator"` | no | Declare a coordinator agent. | | `multiagent.agents` | string[] | yes (with multiagent) | Agents it orchestrates. | diff --git a/packages/sdk/src/internal/core/session-runtime.ts b/packages/sdk/src/internal/core/session-runtime.ts index b108553..c0fb281 100644 --- a/packages/sdk/src/internal/core/session-runtime.ts +++ b/packages/sdk/src/internal/core/session-runtime.ts @@ -154,6 +154,7 @@ export async function createSessionForAgent( resources: options.resources, title: options.title, metadata: options.metadata, + environmentVariables: options.environmentVariables, }); const session = await adapter.createSession(bindings); return { agentName, provider, session }; @@ -178,6 +179,7 @@ export async function startSessionRun( resources: options.resources, title: options.title, metadata: options.metadata, + environmentVariables: options.environmentVariables, }); const session = await adapter.createSession(bindings); return { diff --git a/packages/sdk/src/internal/core/validate-config.ts b/packages/sdk/src/internal/core/validate-config.ts index 2bf9490..fb5dfeb 100644 --- a/packages/sdk/src/internal/core/validate-config.ts +++ b/packages/sdk/src/internal/core/validate-config.ts @@ -344,6 +344,13 @@ export function collectProviderCapabilities( } } for (const [name, agent] of Object.entries(config.agents ?? {})) { + if (agent.environment_variables && (!agent.provider || agent.provider === providerName)) { + diagnostics.error( + `${providerName}.agent.environment_variables.unsupported`, + `agent.${name}: environment_variables is supported only by Qoder; remove it or pin this agent to qoder.`, + { type: "agent", name, provider: providerName }, + ); + } if (agent.tunnel && (!agent.provider || agent.provider === providerName)) { diagnostics.error( `${providerName}.agent.tunnel.unsupported`, diff --git a/packages/sdk/src/internal/parser/schema.ts b/packages/sdk/src/internal/parser/schema.ts index f25a735..46ed591 100644 --- a/packages/sdk/src/internal/parser/schema.ts +++ b/packages/sdk/src/internal/parser/schema.ts @@ -255,6 +255,7 @@ const agentSchema = z.object({ resources: z.array(sessionGithubRepoResourceSchema).optional(), multiagent: multiagentSchema.optional(), metadata: z.record(z.string(), z.string()).optional(), + environment_variables: z.record(z.string().min(1), z.string()).optional(), delivery: z.record(z.string(), agentDeliverySchema).optional(), }); diff --git a/packages/sdk/src/internal/providers/qoder/adapter.ts b/packages/sdk/src/internal/providers/qoder/adapter.ts index 3ff31b2..c22bf65 100644 --- a/packages/sdk/src/internal/providers/qoder/adapter.ts +++ b/packages/sdk/src/internal/providers/qoder/adapter.ts @@ -797,6 +797,9 @@ export class QoderAdapter implements ProviderAdapter { }; if (bindings.title) body.title = bindings.title; if (bindings.metadata) body.metadata = bindings.metadata; + if (bindings.environment_variables) { + body.config = { environment_variables: bindings.environment_variables }; + } if (bindings.files?.length) { body.resources = bindings.files.map((file) => ({ type: "file", diff --git a/packages/sdk/src/internal/providers/qoder/mapper.ts b/packages/sdk/src/internal/providers/qoder/mapper.ts index a29a8be..0474691 100644 --- a/packages/sdk/src/internal/providers/qoder/mapper.ts +++ b/packages/sdk/src/internal/providers/qoder/mapper.ts @@ -479,6 +479,7 @@ export function mapForwardTemplate( if (refs.tunnel_id) body.tunnel_id = refs.tunnel_id; if (projectName) body.metadata = injectMetadata(decl.metadata, projectName, name); else body.metadata = decl.metadata ?? {}; + if (decl.environment_variables) body.environment_variables = decl.environment_variables; if (decl.tools) { body.tools = [ @@ -650,6 +651,11 @@ export function mapSession(bindings: ManagedSessionBindings): unknown { if (bindings.tunnel_id) body.tunnel_id = bindings.tunnel_id; if (bindings.title) body.title = bindings.title; if (bindings.metadata) body.metadata = bindings.metadata; + if (bindings.environment_variables) { + body.environment_variables = Object.entries(bindings.environment_variables) + .map(([key, value]) => `${key}=${value}`) + .join(";"); + } if (bindings.vault_ids.length) body.vault_ids = bindings.vault_ids; // Memory stores and user-uploaded files share the `resources` array (vaults are separate // via `vault_ids`). Every entry needs a non-empty `type`; file shape mirrors qoder's diff --git a/packages/sdk/src/internal/session/session-manager.ts b/packages/sdk/src/internal/session/session-manager.ts index a6f7af0..b066417 100644 --- a/packages/sdk/src/internal/session/session-manager.ts +++ b/packages/sdk/src/internal/session/session-manager.ts @@ -37,6 +37,8 @@ export interface SessionCreateOptions { title?: string; provider?: string; metadata?: Record; + /** Session-level environment variables. Currently supported by Qoder. */ + environmentVariables?: Record; } export function resolveSessionProvider(agentName: string, config: ProjectConfig, overrideProvider?: string): string { @@ -71,6 +73,10 @@ export function buildSessionBindings( throw new UserError(`Agent '${agentName}' not found in config. Available agents: ${available || "(none)"}`); } const sessionResources = options.resources ?? agent.resources; + const environmentVariables = options.environmentVariables ?? agent.environment_variables; + if (environmentVariables && provider !== "qoder") { + throw new UserError("Session environment variables are supported only by Qoder."); + } const providerFeatures = getProvider(provider)?.features; for (const resource of sessionResources ?? []) { if (!providerFeatures?.session_resources.includes(resource.type)) { @@ -102,6 +108,7 @@ export function buildSessionBindings( files: (options.files ?? []).map((file) => ({ file_id: file.fileId, mount_path: file.mountPath })), title: options.title, metadata: options.metadata, + environment_variables: environmentVariables, }; } @@ -157,6 +164,7 @@ export function buildSessionBindings( resources: sessionResources, title: options.title, metadata: options.metadata, + environment_variables: environmentVariables, }; } diff --git a/packages/sdk/src/internal/types/config.ts b/packages/sdk/src/internal/types/config.ts index 038db52..39b0ae1 100644 --- a/packages/sdk/src/internal/types/config.ts +++ b/packages/sdk/src/internal/types/config.ts @@ -173,6 +173,8 @@ export interface AgentDecl { resources?: SessionResourceDecl[]; multiagent?: MultiagentDecl; metadata?: Record; + /** Qoder runtime environment variables. Forward delivery stores these as Template defaults. */ + environment_variables?: Record; /** Provider-specific remote materialization. Omitted means the existing managed Agent resource. */ delivery?: Record; } diff --git a/packages/sdk/src/internal/types/session.ts b/packages/sdk/src/internal/types/session.ts index 083da7f..b0c59e4 100644 --- a/packages/sdk/src/internal/types/session.ts +++ b/packages/sdk/src/internal/types/session.ts @@ -22,6 +22,8 @@ interface CommonSessionBindings { resources?: SessionResource[]; title?: string; metadata?: Record; + /** Provider-neutral representation; Qoder maps it to each Session API's wire shape. */ + environment_variables?: Record; } export interface ManagedSessionBindings extends CommonSessionBindings { diff --git a/packages/sdk/tests/unit/map-session.test.ts b/packages/sdk/tests/unit/map-session.test.ts index 8b3d2ed..f255d4d 100644 --- a/packages/sdk/tests/unit/map-session.test.ts +++ b/packages/sdk/tests/unit/map-session.test.ts @@ -100,6 +100,14 @@ describe("Qoder mapSession", () => { expect(body.metadata).toEqual({ team: "eng" }); }); + test("serializes environment variables using Qoder's managed Session string format", () => { + const body = mapQoderSession({ + ...minimalBindings(), + environment_variables: { FEATURE_FLAG: "on", LOG_LEVEL: "debug" }, + }) as Record; + expect(body.environment_variables).toBe("FEATURE_FLAG=on;LOG_LEVEL=debug"); + }); + test("tunnel_id is omitted when not provided", () => { const bindings = minimalBindings(); const body = mapQoderSession(bindings) as Record; diff --git a/packages/sdk/tests/unit/qoder-forward-template.test.ts b/packages/sdk/tests/unit/qoder-forward-template.test.ts index 8ddd964..905a33c 100644 --- a/packages/sdk/tests/unit/qoder-forward-template.test.ts +++ b/packages/sdk/tests/unit/qoder-forward-template.test.ts @@ -51,6 +51,7 @@ function forwardConfig(): ProjectConfig { vault: "mcp", tools: { builtin: ["Bash", "Read"], permissions: { bash: "ask" } }, mcp_servers: [{ name: "coop", type: "http", url: "https://mcp.example.test/mcp" }], + environment_variables: { BASE_MODE: "support" }, delivery: { qoder: { type: "forward" } }, }, }, @@ -163,6 +164,7 @@ describe("Qoder Forward Template declaration", () => { describe("Qoder Forward Template mapping and lifecycle", () => { test("maps BYOC bindings and tool permissions", () => { const decl = forwardConfig().agents!.assistant!; + decl.environment_variables = { BASE_MODE: "support" }; const body = mapForwardTemplate("assistant", decl, { environment_id: "env_byoc", tunnel_id: "tnl_internal", @@ -176,6 +178,7 @@ describe("Qoder Forward Template mapping and lifecycle", () => { tunnel_id: "tnl_internal", vault_ids: ["vault_mcp"], mcp_servers: [{ name: "coop", type: "http", url: "https://mcp.example.test/mcp" }], + environment_variables: { BASE_MODE: "support" }, }); expect(body.tools[0].configs).toEqual([ { @@ -305,6 +308,7 @@ describe("Qoder Forward Template mapping and lifecycle", () => { template_id: "tmpl_1", identity_id: "idn_zhang", title: "Forward test", + environment_variables: { API_KEY: "secret", REGION: "cn-hangzhou" }, }); const eventId = await adapter.sendSessionMessage(created.id, "hello"); const listed = await adapter.listSessionEvents(created.id, { limit: 100 }); @@ -316,6 +320,7 @@ describe("Qoder Forward Template mapping and lifecycle", () => { expect(calls.find((call) => call.path === "/sessions")?.body).toMatchObject({ identity_id: "idn_zhang", template_id: "tmpl_1", + config: { environment_variables: { API_KEY: "secret", REGION: "cn-hangzhou" } }, }); expect(eventId).toBe("evt_user"); expect(listed.events[0]).toMatchObject({ type: "tool_use", tool_name: "search" }); diff --git a/packages/sdk/tests/unit/session-manager.test.ts b/packages/sdk/tests/unit/session-manager.test.ts index 8d461a3..e7f7c4b 100644 --- a/packages/sdk/tests/unit/session-manager.test.ts +++ b/packages/sdk/tests/unit/session-manager.test.ts @@ -77,6 +77,7 @@ function makeState(): StateManager { describe("buildSessionBindings", () => { test("inherits environment, vault, memory_stores from agent declaration", () => { const config = makeConfig(); + config.agents!.researcher!.environment_variables = { FEATURE_FLAG: "on" }; const state = makeState(); const bindings = buildSessionBindings("researcher", config, "qoder", state); @@ -85,6 +86,7 @@ describe("buildSessionBindings", () => { expect(bindings.environment_id).toBe("env_dev"); expect(bindings.vault_ids).toEqual(["vault_s1"]); expect(bindings.memory_store_ids).toEqual(["ms_docs"]); + expect(bindings.environment_variables).toEqual({ FEATURE_FLAG: "on" }); }); test("inherits provider-neutral session resources from the agent declaration", () => { diff --git a/packages/sdk/tests/unit/validate-config.test.ts b/packages/sdk/tests/unit/validate-config.test.ts index a52b9a0..707c0e3 100644 --- a/packages/sdk/tests/unit/validate-config.test.ts +++ b/packages/sdk/tests/unit/validate-config.test.ts @@ -58,6 +58,24 @@ test("validates tunnel references and limits tunnels to Qoder", () => { expect(diagnostics.some((d) => d.code === "claude.agent.tunnel.unsupported")).toBe(true); }); +test("limits Agent environment variables to Qoder", () => { + const config: ProjectConfig = { + version: "1", + providers: { claude: {} }, + defaults: { provider: "claude" }, + agents: { + assistant: { + model: "claude", + instructions: "test", + environment_variables: { FEATURE_FLAG: "on" }, + }, + }, + }; + + const diagnostics = validateProjectConfig(config); + expect(diagnostics.some((d) => d.code === "claude.agent.environment_variables.unsupported")).toBe(true); +}); + test("rejects tool approval and GitHub Session resources on unsupported providers", () => { const config: ProjectConfig = { version: "1", From 9e85cd70cfd3a934dd10887bcb4d244205ec354a Mon Sep 17 00:00:00 2001 From: heimanba <371510756@qq.com> Date: Wed, 12 Aug 2026 11:19:06 +0800 Subject: [PATCH 2/2] fix: update vulnerable dependencies Change-Id: I0d3309e56596e337bafa1a25745c7a91927a518f --- bun.lock | 15 ++++++++------- package.json | 7 ++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bun.lock b/bun.lock index aecda17..10aaa7a 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@changesets/cli": "^2.31.0", "@types/bun": "^1.3.14", "dependency-cruiser": "^17.4.3", - "js-yaml": "4.3.0", + "js-yaml": "4.3.1", "tsup": "^8.5.1", "typescript": "^6.0.3", }, @@ -127,10 +127,11 @@ "@ast-grep/cli", ], "overrides": { - "brace-expansion": "5.0.8", + "brace-expansion": "5.0.9", "esbuild": "0.28.1", "fast-equals": "5.3.3", - "js-yaml": "4.3.0", + "js-yaml": "4.3.1", + "nanoid": "3.3.17", "postcss": "8.5.23", }, "packages": { @@ -486,7 +487,7 @@ "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], - "brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -612,7 +613,7 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - "hono": ["hono@4.12.29", "", {}, "sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw=="], + "hono": ["hono@4.13.1", "", {}, "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -672,7 +673,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -830,7 +831,7 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + "nanoid": ["nanoid@3.3.17", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], diff --git a/package.json b/package.json index 0942d42..ae70704 100644 --- a/package.json +++ b/package.json @@ -67,15 +67,16 @@ "@changesets/cli": "^2.31.0", "@types/bun": "^1.3.14", "dependency-cruiser": "^17.4.3", - "js-yaml": "4.3.0", + "js-yaml": "4.3.1", "tsup": "^8.5.1", "typescript": "^6.0.3" }, "overrides": { - "brace-expansion": "5.0.8", + "brace-expansion": "5.0.9", "esbuild": "0.28.1", "fast-equals": "5.3.3", - "js-yaml": "4.3.0", + "js-yaml": "4.3.1", + "nanoid": "3.3.17", "postcss": "8.5.23" }, "trustedDependencies": [