diff --git a/package.json b/package.json index f166188..dc8d42e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@askalf/agent", - "version": "3.1.6", + "version": "3.2.0", "description": "Connect any device to an AI workforce that thinks, heals, remembers, and evolves. Nervous system signals, immune system alerts, auto-reconnect, capabilities scan. One command to install as a persistent service.", "type": "module", "main": "dist/index.js", diff --git a/src/bridge.ts b/src/bridge.ts index 270bcbf..aac0092 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -36,6 +36,11 @@ interface TaskPayload { maxBudget?: number; credentials?: string; mode?: ExecutionMode; + // When set, passed to Claude CLI as --model. Without this, claude falls back + // to the device's local default — which can be any tier the device's OAuth + // account happens to be on. The dispatcher should send the forge agent's + // model_id so cost stays controllable from the platform side. + model?: string; } export function scanCapabilities(): Record { @@ -328,6 +333,7 @@ export class AgentBridge { private async handleTask(task: TaskPayload): Promise { console.log(` [${new Date().toISOString()}] Task received: ${task.agentName} (${task.executionId})`); console.log(` Input: ${task.input.substring(0, 100)}${task.input.length > 100 ? '...' : ''}`); + if (task.model) console.log(` Model: ${task.model}`); // Acknowledge receipt this.send('execution:accepted', { executionId: task.executionId }); @@ -382,6 +388,18 @@ export class AgentBridge { if (task.maxBudget) { args.push('--max-budget-usd', String(task.maxBudget)); } + if (task.model) { + // Validate against an allowlist of model prefixes to avoid passing + // attacker-controlled strings into the CLI. Claude CLI itself rejects + // unknown models, but a malformed value could still spawn the process + // with surprising args. + const safe = /^[a-z0-9][a-z0-9.\-]{0,60}$/.test(task.model); + if (safe) { + args.push('--model', task.model); + } else { + console.warn(` Ignoring malformed model "${task.model}" — falling back to local default`); + } + } // Pass prompt as a positional arg, not via stdin. Claude CLI's stdin code // path skips API-key auth and falls back to OAuth-only — so a host running