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.
|
|
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
|
-
|
|
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
|
+
}
|