castle-web-cli 0.4.58 → 0.4.59

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.
Files changed (2) hide show
  1. package/dist/agent.js +52 -1
  2. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -16,6 +16,7 @@
16
16
  // --force. Claude support can slot in later behind runAgentCli.
17
17
  import { execFileSync, spawn } from 'child_process';
18
18
  import * as fs from 'fs';
19
+ import * as os from 'os';
19
20
  import * as path from 'path';
20
21
  import { nanoid } from 'nanoid';
21
22
  import { WebSocketServer } from 'ws';
@@ -187,6 +188,56 @@ function toolActivityLabel(ev) {
187
188
  return 'running a command';
188
189
  return 'working';
189
190
  }
191
+ // Castle's agent CLI keys, delivered to the sandbox as a file
192
+ // (~/.castle/keys.json) rather than sandbox-wide env -- so an ambient key can't
193
+ // override a user's own subscription login. Falls back to process.env for
194
+ // older sandboxes that still inject the keys as env.
195
+ const CASTLE_KEYS_PATH = path.join(os.homedir(), '.castle', 'keys.json');
196
+ function castleKeys() {
197
+ try {
198
+ return JSON.parse(fs.readFileSync(CASTLE_KEYS_PATH, 'utf8'));
199
+ }
200
+ catch {
201
+ return {};
202
+ }
203
+ }
204
+ const BACKEND_KEY_ENV = {
205
+ claude: 'ANTHROPIC_API_KEY',
206
+ cursor: 'CURSOR_API_KEY',
207
+ };
208
+ // True when the user has their OWN saved auth for this backend -- a /login, or
209
+ // (for cursor, which reuses one auth.json) any saved creds. When so we do NOT
210
+ // inject Castle's key, so their auth is used and billed to them.
211
+ function backendHasSavedAuth(backend) {
212
+ const home = os.homedir();
213
+ if (backend === 'claude') {
214
+ return fs.existsSync(path.join(home, '.claude', '.credentials.json'));
215
+ }
216
+ if (backend === 'cursor') {
217
+ return fs.existsSync(path.join(home, '.config', 'cursor', 'auth.json'));
218
+ }
219
+ return false;
220
+ }
221
+ // Env for an agent spawn: inject Castle's key ONLY when the backend has no saved
222
+ // auth of the user's own. This is what lets internal testers run on their own
223
+ // subscription (log in once in the terminal) instead of Castle's key.
224
+ function envForAgentSpawn(backend) {
225
+ const env = { ...process.env };
226
+ const keyName = BACKEND_KEY_ENV[backend];
227
+ if (!keyName)
228
+ return env;
229
+ if (backendHasSavedAuth(backend)) {
230
+ delete env[keyName];
231
+ }
232
+ else {
233
+ const val = castleKeys()[keyName] ?? process.env[keyName];
234
+ if (val)
235
+ env[keyName] = val;
236
+ else
237
+ delete env[keyName];
238
+ }
239
+ return env;
240
+ }
190
241
  // One headless agent CLI run (cursor or claude), normalized to the same
191
242
  // delta/activity/result hooks. Cursor: assistant events carrying timestamp_ms
192
243
  // are text deltas; the trailing assistant event without one repeats the whole
@@ -196,7 +247,7 @@ function runAgentCli(opts) {
196
247
  return new Promise((resolve) => {
197
248
  const child = spawn(opts.command, opts.args, {
198
249
  cwd: opts.cwd,
199
- env: process.env,
250
+ env: envForAgentSpawn(opts.parser),
200
251
  stdio: ['ignore', 'pipe', 'pipe'],
201
252
  });
202
253
  opts.children.add(child);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-cli",
3
- "version": "0.4.58",
3
+ "version": "0.4.59",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castle-web": "./dist/index.js"