shraga 0.1.27 → 0.1.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -14,6 +14,7 @@ import type { WsEvent, AskQuestion, QuestionAnswers, QuestionHandler } from '../
14
14
  import type { AgentEngine, EngineStreamOpts, EngineModel } from './types.ts';
15
15
  import { getPromptSuffix } from '../prompt-suffix.ts';
16
16
  import { APP_ROOT } from '../paths.ts';
17
+ import { writeMcpConfigFile } from './mcp-config-file.ts';
17
18
  const IMMUTABLE_SYSTEM_PROMPT = readFileSync(path.resolve(import.meta.dirname, '../../../defaults/system-prompt.md'), 'utf-8');
18
19
  const DEFAULT_USER_PROMPT = `You are a helpful assistant with access to MCP tools.`;
19
20
  const DEFAULT_ALLOWED_TOOLS = ['Read', 'Edit', 'Bash', 'WebSearch', 'Glob', 'LS', 'ToolSearch'];
@@ -271,7 +272,14 @@ export class ClaudeCodeEngine implements AgentEngine {
271
272
  const addonSuffix = getPromptSuffix(opts.turnHints);
272
273
  options['systemPrompt'] = `${IMMUTABLE_SYSTEM_PROMPT}\n\n${userPrompt}${addonSuffix ? `\n\n${addonSuffix}` : ''}`;
273
274
  if (opts.abortController) options['abortController'] = opts.abortController;
274
- if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) options['mcpServers'] = opts.mcpServers;
275
+ // Passed as a FILE, never as `options.mcpServers` the SDK would put the whole config (every MCP
276
+ // server's credentials) on the CLI's argv, where `ps` / `/proc` / journald expose it. See
277
+ // writeMcpConfigFile. Setting both would re-add the argv copy, so it's one or the other.
278
+ let mcpConfigFile: { path: string; cleanup: () => void } | undefined;
279
+ if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
280
+ mcpConfigFile = writeMcpConfigFile(opts.mcpServers);
281
+ options['extraArgs'] = { ...(options['extraArgs'] as Record<string, string> | undefined), 'mcp-config': mcpConfigFile.path };
282
+ }
275
283
 
276
284
  const mcpNames = opts.mcpServers ? Object.keys(opts.mcpServers) : [];
277
285
  const activeModel = (options['model'] as string) || 'default';
@@ -517,6 +525,9 @@ export class ClaudeCodeEngine implements AgentEngine {
517
525
  return;
518
526
  } finally {
519
527
  clearBgTimer();
528
+ // The CLI has read the file by now (it loads MCP config at startup); holding it any longer just
529
+ // widens the window in which the credentials sit on disk.
530
+ mcpConfigFile?.cleanup();
520
531
  }
521
532
 
522
533
  const inferredReason = turnCount >= maxTurns ? 'max_turns_reached' : 'end_turn';
@@ -0,0 +1,35 @@
1
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import type { McpConfig } from '../mcp.ts';
5
+
6
+ /**
7
+ * Hands the MCP config to the Claude Code CLI through a private FILE instead of its argv.
8
+ *
9
+ * WHY: the SDK serialises `options.mcpServers` straight onto the command line
10
+ * (`--mcp-config '{"mcpServers":{…}}'`), and an MCP server's `env` block is where every vendor
11
+ * credential lives. On the Circles box that meant the Stripe live key, a GitHub PAT with destructive
12
+ * writes, two Firebase service-account private keys, the prod Postgres password + bastion SSH key and
13
+ * the app-store signing keys were all readable by ANY local process via `ps` / `/proc/<pid>/cmdline`,
14
+ * and were echoed verbatim into journald (so into anything that ships logs). Argv is not a secret
15
+ * channel — a file we own with 0600 is.
16
+ *
17
+ * HOW: the CLI's `--mcp-config` takes either inline JSON or a path, so we write the same JSON to a
18
+ * 0600 file inside a 0700 temp dir and pass the path via the SDK's `extraArgs` escape hatch. The
19
+ * caller must NOT also set `options.mcpServers`, or the SDK appends a second `--mcp-config` with the
20
+ * secrets back in argv. Safe here because shraga only ships `stdio` + `http` servers; an in-process
21
+ * `sdk` server would have to stay on `options.mcpServers` (it holds no secrets — it's a live object).
22
+ */
23
+ export function writeMcpConfigFile(mcpServers: McpConfig): { path: string; cleanup: () => void } {
24
+ // mkdtemp gives us a 0700 dir with an unguessable name, so the file is unreachable even in the
25
+ // window before the mode is applied, and two concurrent sessions can never collide.
26
+ const dir = mkdtempSync(path.join(tmpdir(), 'shraga-mcp-'));
27
+ const file = path.join(dir, 'mcp-config.json');
28
+ writeFileSync(file, JSON.stringify({ mcpServers }), { mode: 0o600 });
29
+ return {
30
+ path: file,
31
+ // Best-effort: a leaked temp dir is a much smaller problem than a crash on teardown, and the
32
+ // 0700/0600 modes mean a leftover file is still unreadable by other users.
33
+ cleanup: () => { try { rmSync(dir, { recursive: true, force: true }); } catch { /* nothing to do */ } },
34
+ };
35
+ }