gsd-pi 2.76.0-dev.b072ebb73 → 2.76.0-dev.fe143342a
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/dist/mcp-server.d.ts +7 -0
- package/dist/mcp-server.js +35 -1
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +2 -8
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
- package/dist/resources/extensions/gsd/auto/phases.js +4 -1
- package/dist/resources/extensions/gsd/auto/session.js +4 -0
- package/dist/resources/extensions/gsd/auto-model-selection.js +39 -13
- package/dist/resources/extensions/gsd/auto-start.js +39 -21
- package/dist/resources/extensions/gsd/auto.js +15 -12
- package/dist/resources/extensions/gsd/blocked-models.js +68 -0
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +76 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
- package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
- package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
- package/dist/resources/extensions/gsd/error-classifier.js +31 -3
- package/dist/resources/extensions/gsd/exec-history.js +120 -0
- package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
- package/dist/resources/extensions/gsd/gsd-db.js +62 -4
- package/dist/resources/extensions/gsd/init-wizard.js +15 -1
- package/dist/resources/extensions/gsd/key-manager.js +6 -0
- package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
- package/dist/resources/extensions/gsd/preferences-types.js +9 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
- package/dist/resources/extensions/gsd/preferences.js +17 -17
- package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
- package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
- package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
- package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
- package/dist/resources/extensions/search-the-web/command-search-provider.js +5 -4
- package/dist/resources/extensions/search-the-web/native-search.js +45 -13
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +64 -25
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
- package/packages/mcp-server/src/workflow-tools.ts +84 -43
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
- package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
- package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
- package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
- package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
- package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
- package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
- package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
- package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
- package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
- package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/scripts/link-workspace-packages.cjs +1 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/phases.ts +4 -0
- package/src/resources/extensions/gsd/auto/session.ts +7 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +50 -12
- package/src/resources/extensions/gsd/auto-start.ts +40 -22
- package/src/resources/extensions/gsd/auto.ts +15 -12
- package/src/resources/extensions/gsd/blocked-models.ts +98 -0
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +97 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
- package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
- package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
- package/src/resources/extensions/gsd/error-classifier.ts +36 -3
- package/src/resources/extensions/gsd/exec-history.ts +153 -0
- package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
- package/src/resources/extensions/gsd/gsd-db.ts +68 -4
- package/src/resources/extensions/gsd/init-wizard.ts +15 -1
- package/src/resources/extensions/gsd/key-manager.ts +6 -0
- package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
- package/src/resources/extensions/gsd/preferences-types.ts +38 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
- package/src/resources/extensions/gsd/preferences.ts +17 -17
- package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
- package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
- package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +91 -0
- package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
- package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
- package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
- package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
- package/src/resources/extensions/search-the-web/command-search-provider.ts +5 -4
- package/src/resources/extensions/search-the-web/native-search.ts +48 -12
- /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// GSD Exec Sandbox — tool-output sandboxing for sub-sessions.
|
|
2
|
+
//
|
|
3
|
+
// Runs a script in a subprocess and persists stdout/stderr to
|
|
4
|
+
// `.gsd/exec/<id>.{stdout,stderr,meta.json}`. Only a short digest is
|
|
5
|
+
// returned to the calling agent's context, keeping large outputs
|
|
6
|
+
// (e.g. Playwright snapshots, issue dumps) out of the window.
|
|
7
|
+
//
|
|
8
|
+
// Inspired by mksglu/context-mode (Elastic License 2.0). Independent
|
|
9
|
+
// implementation — no upstream code incorporated.
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
export interface ExecSandboxRequest {
|
|
17
|
+
/** Interpreter to use. */
|
|
18
|
+
runtime: "bash" | "node" | "python";
|
|
19
|
+
/** Script body. Executed via the runtime's -c equivalent. */
|
|
20
|
+
script: string;
|
|
21
|
+
/** Optional purpose/label recorded in meta.json. */
|
|
22
|
+
purpose?: string;
|
|
23
|
+
/** Per-invocation timeout in ms. Clamped to `clamp_timeout_ms`. */
|
|
24
|
+
timeout_ms?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ExecSandboxOptions {
|
|
28
|
+
/** Project root. stdout/stderr persist under `<baseDir>/.gsd/exec/`. */
|
|
29
|
+
baseDir: string;
|
|
30
|
+
/** Absolute upper bound for the timeout. */
|
|
31
|
+
clamp_timeout_ms: number;
|
|
32
|
+
/** Default timeout if request omits one. */
|
|
33
|
+
default_timeout_ms: number;
|
|
34
|
+
/** Cap on persisted stdout bytes. Further output is truncated with a marker. */
|
|
35
|
+
stdout_cap_bytes: number;
|
|
36
|
+
/** Cap on persisted stderr bytes. */
|
|
37
|
+
stderr_cap_bytes: number;
|
|
38
|
+
/** Number of trailing stdout chars returned as the digest. */
|
|
39
|
+
digest_chars: number;
|
|
40
|
+
/** Env var allowlist (case-sensitive). PATH/HOME always forwarded. */
|
|
41
|
+
env_allowlist: readonly string[];
|
|
42
|
+
/** Optional override of process.env for tests. */
|
|
43
|
+
env?: NodeJS.ProcessEnv;
|
|
44
|
+
/** Optional override for the current time (tests). */
|
|
45
|
+
now?: () => Date;
|
|
46
|
+
/** Optional override for id generation (tests). */
|
|
47
|
+
generateId?: () => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ExecSandboxResult {
|
|
51
|
+
id: string;
|
|
52
|
+
runtime: ExecSandboxRequest["runtime"];
|
|
53
|
+
exit_code: number | null;
|
|
54
|
+
signal: NodeJS.Signals | null;
|
|
55
|
+
timed_out: boolean;
|
|
56
|
+
duration_ms: number;
|
|
57
|
+
stdout_bytes: number;
|
|
58
|
+
stderr_bytes: number;
|
|
59
|
+
stdout_truncated: boolean;
|
|
60
|
+
stderr_truncated: boolean;
|
|
61
|
+
stdout_path: string;
|
|
62
|
+
stderr_path: string;
|
|
63
|
+
meta_path: string;
|
|
64
|
+
digest: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ALWAYS_FORWARD_ENV = ["PATH", "HOME"] as const;
|
|
68
|
+
|
|
69
|
+
export const EXEC_DEFAULTS = {
|
|
70
|
+
clampTimeoutMs: 600_000,
|
|
71
|
+
defaultTimeoutMs: 30_000,
|
|
72
|
+
stdoutCapBytes: 1_048_576,
|
|
73
|
+
stderrCapBytes: 262_144,
|
|
74
|
+
digestChars: 300,
|
|
75
|
+
envAllowlist: [
|
|
76
|
+
"LANG",
|
|
77
|
+
"LC_ALL",
|
|
78
|
+
"TERM",
|
|
79
|
+
"TZ",
|
|
80
|
+
"SHELL",
|
|
81
|
+
"USER",
|
|
82
|
+
"LOGNAME",
|
|
83
|
+
"TMPDIR",
|
|
84
|
+
"NODE_OPTIONS",
|
|
85
|
+
"PYTHONPATH",
|
|
86
|
+
"PYTHONIOENCODING",
|
|
87
|
+
] as const,
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
function buildChildEnv(opts: ExecSandboxOptions): NodeJS.ProcessEnv {
|
|
91
|
+
const source = opts.env ?? process.env;
|
|
92
|
+
const out: NodeJS.ProcessEnv = {};
|
|
93
|
+
const allowed = new Set<string>([...ALWAYS_FORWARD_ENV, ...opts.env_allowlist]);
|
|
94
|
+
for (const key of allowed) {
|
|
95
|
+
const value = source[key];
|
|
96
|
+
if (typeof value === "string") out[key] = value;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clampTimeout(request: ExecSandboxRequest, opts: ExecSandboxOptions): number {
|
|
102
|
+
const requested = typeof request.timeout_ms === "number" && Number.isFinite(request.timeout_ms)
|
|
103
|
+
? Math.floor(request.timeout_ms)
|
|
104
|
+
: opts.default_timeout_ms;
|
|
105
|
+
if (requested < 1) return 1;
|
|
106
|
+
if (requested > opts.clamp_timeout_ms) return opts.clamp_timeout_ms;
|
|
107
|
+
return requested;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveCommand(runtime: ExecSandboxRequest["runtime"]): { cmd: string; args: string[] } {
|
|
111
|
+
switch (runtime) {
|
|
112
|
+
case "bash":
|
|
113
|
+
return { cmd: "bash", args: ["-c"] };
|
|
114
|
+
case "node":
|
|
115
|
+
return { cmd: process.execPath, args: ["-e"] };
|
|
116
|
+
case "python":
|
|
117
|
+
return { cmd: "python3", args: ["-c"] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function tail(buf: Buffer, chars: number): string {
|
|
122
|
+
if (chars <= 0) return "";
|
|
123
|
+
const text = buf.toString("utf-8");
|
|
124
|
+
return text.length <= chars ? text : text.slice(text.length - chars);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run a script in a subprocess, capture stdout/stderr to files under
|
|
129
|
+
* `.gsd/exec/<id>.{stdout,stderr,meta.json}`, and return an `ExecSandboxResult`
|
|
130
|
+
* containing the digest plus metadata.
|
|
131
|
+
*
|
|
132
|
+
* Errors from spawn failures resolve (not reject) with `exit_code=null`.
|
|
133
|
+
* The function is pure with respect to its inputs — no global state beyond
|
|
134
|
+
* filesystem writes under `baseDir`.
|
|
135
|
+
*/
|
|
136
|
+
export function runExecSandbox(
|
|
137
|
+
request: ExecSandboxRequest,
|
|
138
|
+
opts: ExecSandboxOptions,
|
|
139
|
+
): Promise<ExecSandboxResult> {
|
|
140
|
+
return new Promise((resolveP) => {
|
|
141
|
+
const id = (opts.generateId ?? defaultGenerateId)();
|
|
142
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
143
|
+
const execDir = resolve(opts.baseDir, ".gsd", "exec");
|
|
144
|
+
if (!existsSync(execDir)) mkdirSync(execDir, { recursive: true });
|
|
145
|
+
const stdoutPath = resolve(execDir, `${id}.stdout`);
|
|
146
|
+
const stderrPath = resolve(execDir, `${id}.stderr`);
|
|
147
|
+
const metaPath = resolve(execDir, `${id}.meta.json`);
|
|
148
|
+
|
|
149
|
+
const timeoutMs = clampTimeout(request, opts);
|
|
150
|
+
const { cmd, args } = resolveCommand(request.runtime);
|
|
151
|
+
const env = buildChildEnv(opts);
|
|
152
|
+
const useProcessGroup = process.platform !== "win32";
|
|
153
|
+
|
|
154
|
+
const started = Date.now();
|
|
155
|
+
let child;
|
|
156
|
+
try {
|
|
157
|
+
child = spawn(cmd, [...args, request.script], {
|
|
158
|
+
cwd: opts.baseDir,
|
|
159
|
+
env,
|
|
160
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
161
|
+
...(useProcessGroup ? { detached: true } : {}),
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const duration = Date.now() - started;
|
|
165
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
166
|
+
writeFileSync(stdoutPath, "");
|
|
167
|
+
writeFileSync(stderrPath, `spawn error: ${message}\n`);
|
|
168
|
+
const result: ExecSandboxResult = {
|
|
169
|
+
id,
|
|
170
|
+
runtime: request.runtime,
|
|
171
|
+
exit_code: null,
|
|
172
|
+
signal: null,
|
|
173
|
+
timed_out: false,
|
|
174
|
+
duration_ms: duration,
|
|
175
|
+
stdout_bytes: 0,
|
|
176
|
+
stderr_bytes: Buffer.byteLength(`spawn error: ${message}\n`),
|
|
177
|
+
stdout_truncated: false,
|
|
178
|
+
stderr_truncated: false,
|
|
179
|
+
stdout_path: stdoutPath,
|
|
180
|
+
stderr_path: stderrPath,
|
|
181
|
+
meta_path: metaPath,
|
|
182
|
+
digest: `[spawn error: ${message}]`,
|
|
183
|
+
};
|
|
184
|
+
writeMeta(metaPath, result, request, now);
|
|
185
|
+
resolveP(result);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const stdoutChunks: Buffer[] = [];
|
|
190
|
+
const stderrChunks: Buffer[] = [];
|
|
191
|
+
let stdoutBytes = 0;
|
|
192
|
+
let stderrBytes = 0;
|
|
193
|
+
let stdoutTruncated = false;
|
|
194
|
+
let stderrTruncated = false;
|
|
195
|
+
|
|
196
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
197
|
+
const remaining = opts.stdout_cap_bytes - stdoutBytes;
|
|
198
|
+
if (remaining <= 0) {
|
|
199
|
+
stdoutTruncated = true;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (chunk.length <= remaining) {
|
|
203
|
+
stdoutChunks.push(chunk);
|
|
204
|
+
stdoutBytes += chunk.length;
|
|
205
|
+
} else {
|
|
206
|
+
stdoutChunks.push(chunk.subarray(0, remaining));
|
|
207
|
+
stdoutBytes += remaining;
|
|
208
|
+
stdoutTruncated = true;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
212
|
+
const remaining = opts.stderr_cap_bytes - stderrBytes;
|
|
213
|
+
if (remaining <= 0) {
|
|
214
|
+
stderrTruncated = true;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (chunk.length <= remaining) {
|
|
218
|
+
stderrChunks.push(chunk);
|
|
219
|
+
stderrBytes += chunk.length;
|
|
220
|
+
} else {
|
|
221
|
+
stderrChunks.push(chunk.subarray(0, remaining));
|
|
222
|
+
stderrBytes += remaining;
|
|
223
|
+
stderrTruncated = true;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
let timedOut = false;
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
timedOut = true;
|
|
230
|
+
if (useProcessGroup && child.pid != null) {
|
|
231
|
+
try {
|
|
232
|
+
process.kill(-child.pid, "SIGKILL");
|
|
233
|
+
} catch {
|
|
234
|
+
child.kill("SIGKILL");
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
child.kill("SIGKILL");
|
|
238
|
+
}
|
|
239
|
+
}, timeoutMs);
|
|
240
|
+
timer.unref?.();
|
|
241
|
+
|
|
242
|
+
const finalize = (exitCode: number | null, signal: NodeJS.Signals | null) => {
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
const duration = Date.now() - started;
|
|
245
|
+
const stdoutBuf = Buffer.concat(stdoutChunks);
|
|
246
|
+
const stderrBuf = Buffer.concat(stderrChunks);
|
|
247
|
+
const stdoutSuffix = stdoutTruncated ? "\n[truncated: stdout cap reached]\n" : "";
|
|
248
|
+
const stderrSuffix = stderrTruncated ? "\n[truncated: stderr cap reached]\n" : "";
|
|
249
|
+
writeFileSync(stdoutPath, Buffer.concat([stdoutBuf, Buffer.from(stdoutSuffix, "utf-8")]));
|
|
250
|
+
writeFileSync(stderrPath, Buffer.concat([stderrBuf, Buffer.from(stderrSuffix, "utf-8")]));
|
|
251
|
+
|
|
252
|
+
const digestBody = tail(stdoutBuf, opts.digest_chars);
|
|
253
|
+
const digest =
|
|
254
|
+
digestBody.length > 0
|
|
255
|
+
? digestBody
|
|
256
|
+
: timedOut
|
|
257
|
+
? "[no stdout — timed out]"
|
|
258
|
+
: stderrBuf.length > 0
|
|
259
|
+
? `[no stdout — tail of stderr]\n${tail(stderrBuf, opts.digest_chars)}`
|
|
260
|
+
: "[no output]";
|
|
261
|
+
|
|
262
|
+
const result: ExecSandboxResult = {
|
|
263
|
+
id,
|
|
264
|
+
runtime: request.runtime,
|
|
265
|
+
exit_code: exitCode,
|
|
266
|
+
signal,
|
|
267
|
+
timed_out: timedOut,
|
|
268
|
+
duration_ms: duration,
|
|
269
|
+
stdout_bytes: stdoutBytes,
|
|
270
|
+
stderr_bytes: stderrBytes,
|
|
271
|
+
stdout_truncated: stdoutTruncated,
|
|
272
|
+
stderr_truncated: stderrTruncated,
|
|
273
|
+
stdout_path: stdoutPath,
|
|
274
|
+
stderr_path: stderrPath,
|
|
275
|
+
meta_path: metaPath,
|
|
276
|
+
digest,
|
|
277
|
+
};
|
|
278
|
+
writeMeta(metaPath, result, request, now);
|
|
279
|
+
resolveP(result);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
child.on("error", (err) => {
|
|
283
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
284
|
+
const line = `child error: ${message}\n`;
|
|
285
|
+
const remaining = opts.stderr_cap_bytes - stderrBytes;
|
|
286
|
+
if (remaining > 0) {
|
|
287
|
+
const chunk = Buffer.from(line, "utf-8").subarray(0, remaining);
|
|
288
|
+
stderrChunks.push(chunk);
|
|
289
|
+
stderrBytes += chunk.length;
|
|
290
|
+
if (chunk.length < Buffer.byteLength(line, "utf-8")) stderrTruncated = true;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
child.on("close", (code, signal) => finalize(code, signal));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function defaultGenerateId(): string {
|
|
298
|
+
return randomUUID();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function writeMeta(
|
|
302
|
+
path: string,
|
|
303
|
+
result: ExecSandboxResult,
|
|
304
|
+
request: ExecSandboxRequest,
|
|
305
|
+
now: Date,
|
|
306
|
+
): void {
|
|
307
|
+
const meta = {
|
|
308
|
+
id: result.id,
|
|
309
|
+
runtime: result.runtime,
|
|
310
|
+
purpose: request.purpose ?? null,
|
|
311
|
+
script_chars: request.script.length,
|
|
312
|
+
started_at: now.toISOString(),
|
|
313
|
+
finished_at: new Date(now.getTime() + result.duration_ms).toISOString(),
|
|
314
|
+
exit_code: result.exit_code,
|
|
315
|
+
signal: result.signal,
|
|
316
|
+
timed_out: result.timed_out,
|
|
317
|
+
duration_ms: result.duration_ms,
|
|
318
|
+
stdout_bytes: result.stdout_bytes,
|
|
319
|
+
stderr_bytes: result.stderr_bytes,
|
|
320
|
+
stdout_truncated: result.stdout_truncated,
|
|
321
|
+
stderr_truncated: result.stderr_truncated,
|
|
322
|
+
stdout_path: result.stdout_path,
|
|
323
|
+
stderr_path: result.stderr_path,
|
|
324
|
+
};
|
|
325
|
+
writeFileSync(path, `${JSON.stringify(meta, null, 2)}\n`);
|
|
326
|
+
}
|
|
@@ -564,7 +564,9 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
|
564
564
|
// indexes so old DBs can open far enough for the normal migration chain.
|
|
565
565
|
ensureBootstrapIndexColumns(db);
|
|
566
566
|
|
|
567
|
-
db
|
|
567
|
+
if (columnExists(db, "memories", "scope")) {
|
|
568
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope)");
|
|
569
|
+
}
|
|
568
570
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_kind ON memory_sources(kind)");
|
|
569
571
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_sources_scope ON memory_sources(scope)");
|
|
570
572
|
db.exec("CREATE INDEX IF NOT EXISTS idx_memory_relations_from ON memory_relations(from_id)");
|
|
@@ -1199,6 +1201,8 @@ let currentPath: string | null = null;
|
|
|
1199
1201
|
let currentPid: number = 0;
|
|
1200
1202
|
let _exitHandlerRegistered = false;
|
|
1201
1203
|
let _dbOpenAttempted = false;
|
|
1204
|
+
let _lastDbError: Error | null = null;
|
|
1205
|
+
let _lastDbPhase: "open" | "initSchema" | "vacuum-recovery" | null = null;
|
|
1202
1206
|
|
|
1203
1207
|
export function getDbProvider(): ProviderName | null {
|
|
1204
1208
|
loadProvider();
|
|
@@ -1219,12 +1223,58 @@ export function wasDbOpenAttempted(): boolean {
|
|
|
1219
1223
|
return _dbOpenAttempted;
|
|
1220
1224
|
}
|
|
1221
1225
|
|
|
1226
|
+
export function getDbStatus(): {
|
|
1227
|
+
available: boolean;
|
|
1228
|
+
provider: ProviderName | null;
|
|
1229
|
+
attempted: boolean;
|
|
1230
|
+
lastError: Error | null;
|
|
1231
|
+
lastPhase: "open" | "initSchema" | "vacuum-recovery" | null;
|
|
1232
|
+
} {
|
|
1233
|
+
loadProvider();
|
|
1234
|
+
return {
|
|
1235
|
+
available: currentDb !== null,
|
|
1236
|
+
provider: providerName,
|
|
1237
|
+
attempted: _dbOpenAttempted,
|
|
1238
|
+
lastError: _lastDbError,
|
|
1239
|
+
lastPhase: _lastDbPhase,
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1222
1243
|
export function openDatabase(path: string): boolean {
|
|
1223
1244
|
_dbOpenAttempted = true;
|
|
1224
1245
|
if (currentDb && currentPath !== path) closeDatabase();
|
|
1225
1246
|
if (currentDb && currentPath === path) return true;
|
|
1226
1247
|
|
|
1227
|
-
|
|
1248
|
+
// Reset error state only when a new open attempt is actually going to run.
|
|
1249
|
+
_lastDbError = null;
|
|
1250
|
+
_lastDbPhase = null;
|
|
1251
|
+
|
|
1252
|
+
let rawDb: unknown;
|
|
1253
|
+
let fallbackProvider: ProviderName | null = null;
|
|
1254
|
+
let fallbackModule: unknown = null;
|
|
1255
|
+
try {
|
|
1256
|
+
rawDb = openRawDb(path);
|
|
1257
|
+
} catch (primaryErr) {
|
|
1258
|
+
_lastDbPhase = "open";
|
|
1259
|
+
_lastDbError = primaryErr instanceof Error ? primaryErr : new Error(String(primaryErr));
|
|
1260
|
+
// node:sqlite loaded but failed to open this file — try better-sqlite3 as fallback.
|
|
1261
|
+
if (providerName === "node:sqlite") {
|
|
1262
|
+
try {
|
|
1263
|
+
const mod = _require("better-sqlite3");
|
|
1264
|
+
const Db = (mod && mod.default) ? mod.default : mod;
|
|
1265
|
+
if (typeof Db === "function") {
|
|
1266
|
+
rawDb = new Db(path);
|
|
1267
|
+
fallbackProvider = "better-sqlite3";
|
|
1268
|
+
fallbackModule = Db;
|
|
1269
|
+
_lastDbError = null;
|
|
1270
|
+
_lastDbPhase = null;
|
|
1271
|
+
}
|
|
1272
|
+
} catch {
|
|
1273
|
+
// fallback unavailable; surface original error
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (!rawDb) throw primaryErr;
|
|
1277
|
+
}
|
|
1228
1278
|
if (!rawDb) return false;
|
|
1229
1279
|
|
|
1230
1280
|
const adapter = createAdapter(rawDb);
|
|
@@ -1240,15 +1290,25 @@ export function openDatabase(path: string): boolean {
|
|
|
1240
1290
|
initSchema(adapter, fileBacked);
|
|
1241
1291
|
process.stderr.write("gsd-db: recovered corrupt database via VACUUM\n");
|
|
1242
1292
|
} catch (retryErr) {
|
|
1293
|
+
_lastDbPhase = "vacuum-recovery";
|
|
1294
|
+
_lastDbError = retryErr instanceof Error ? retryErr : new Error(String(retryErr));
|
|
1243
1295
|
try { adapter.close(); } catch (e) { logWarning("db", `close after VACUUM failed: ${(e as Error).message}`); }
|
|
1244
1296
|
throw retryErr;
|
|
1245
1297
|
}
|
|
1246
1298
|
} else {
|
|
1247
|
-
|
|
1299
|
+
_lastDbPhase = "initSchema";
|
|
1300
|
+
_lastDbError = err instanceof Error ? err : new Error(String(err));
|
|
1301
|
+
try { adapter.close(); } catch (e) { logWarning("db", `close after initSchema failed: ${(e as Error).message}`); }
|
|
1248
1302
|
throw err;
|
|
1249
1303
|
}
|
|
1250
1304
|
}
|
|
1251
1305
|
|
|
1306
|
+
// Commit fallback provider switch only after open + schema both succeeded.
|
|
1307
|
+
if (fallbackProvider) {
|
|
1308
|
+
providerName = fallbackProvider;
|
|
1309
|
+
providerModule = fallbackModule;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1252
1312
|
currentDb = adapter;
|
|
1253
1313
|
currentPath = path;
|
|
1254
1314
|
currentPid = process.pid;
|
|
@@ -1276,8 +1336,12 @@ export function closeDatabase(): void {
|
|
|
1276
1336
|
currentDb = null;
|
|
1277
1337
|
currentPath = null;
|
|
1278
1338
|
currentPid = 0;
|
|
1279
|
-
_dbOpenAttempted = false;
|
|
1280
1339
|
}
|
|
1340
|
+
// Reset session-scoped state unconditionally so stale error info from a
|
|
1341
|
+
// failed open doesn't persist into the next open attempt or status check.
|
|
1342
|
+
_dbOpenAttempted = false;
|
|
1343
|
+
_lastDbError = null;
|
|
1344
|
+
_lastDbPhase = null;
|
|
1281
1345
|
}
|
|
1282
1346
|
|
|
1283
1347
|
/** Run a full VACUUM — call sparingly (e.g. after milestone completion). */
|
|
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
|
|
|
10
10
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { showNextAction } from "../shared/tui.js";
|
|
13
|
-
import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
|
|
13
|
+
import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
|
|
14
14
|
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
15
15
|
import { gsdRoot } from "./paths.js";
|
|
16
16
|
import { assertSafeDirectory } from "./validate-directory.js";
|
|
@@ -74,6 +74,7 @@ export async function showProjectInit(
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// ── Step 2: Git setup ──────────────────────────────────────────────────────
|
|
77
|
+
let didInitGit = false;
|
|
77
78
|
if (!signals.isGitRepo) {
|
|
78
79
|
const gitChoice = await showNextAction(ctx, {
|
|
79
80
|
title: "GSD — Project Setup",
|
|
@@ -89,6 +90,7 @@ export async function showProjectInit(
|
|
|
89
90
|
|
|
90
91
|
if (gitChoice === "init_git") {
|
|
91
92
|
nativeInit(basePath, prefs.mainBranch);
|
|
93
|
+
didInitGit = true;
|
|
92
94
|
}
|
|
93
95
|
} else {
|
|
94
96
|
// Auto-detect main branch from existing repo
|
|
@@ -295,6 +297,18 @@ export async function showProjectInit(
|
|
|
295
297
|
ensureGitignore(basePath);
|
|
296
298
|
untrackRuntimeFiles(basePath);
|
|
297
299
|
|
|
300
|
+
// Create initial commit so git log and git worktree work immediately (#4530).
|
|
301
|
+
// Without this, the branch is "unborn" (zero commits) and downstream operations
|
|
302
|
+
// like `git log` and `git worktree add` fail.
|
|
303
|
+
if (didInitGit) {
|
|
304
|
+
try {
|
|
305
|
+
nativeAddAll(basePath);
|
|
306
|
+
nativeCommit(basePath, "chore: init project");
|
|
307
|
+
} catch {
|
|
308
|
+
// Non-fatal — user can commit manually; don't block project init
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
298
312
|
// Auto-generate codebase map for instant agent orientation
|
|
299
313
|
try {
|
|
300
314
|
const result = generateCodebaseMap(basePath);
|
|
@@ -35,6 +35,12 @@ export interface ProviderInfo {
|
|
|
35
35
|
export const PROVIDER_REGISTRY: ProviderInfo[] = [
|
|
36
36
|
// LLM Providers
|
|
37
37
|
{ id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
|
|
38
|
+
// Claude Code CLI: routes through the local `claude` binary — no API key,
|
|
39
|
+
// authentication is handled by the CLI's own OAuth flow.
|
|
40
|
+
// Referenced by doctor-providers.ts, auto-model-selection.ts, and others;
|
|
41
|
+
// must be in the canonical registry so all consumers see the same catalog.
|
|
42
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4541
|
|
43
|
+
{ id: "claude-code", label: "Claude Code CLI", category: "llm", hasOAuth: true },
|
|
38
44
|
{ id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
|
|
39
45
|
{ id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", hasOAuth: true },
|
|
40
46
|
{ id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)",category: "llm", hasOAuth: true },
|
|
@@ -91,8 +91,13 @@ export function extractPackageReferences(description: string): string[] {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// require('pkg') or import from 'pkg' in code blocks
|
|
95
|
-
|
|
94
|
+
// require('pkg') or `import ... from 'pkg'` in code blocks.
|
|
95
|
+
// The `from\s+['"]` branch MUST be preceded by an `import` keyword so that
|
|
96
|
+
// natural-language prose like `from "What's Next"` or `from 'master'` does
|
|
97
|
+
// not produce false package-existence failures. Requiring the leading import
|
|
98
|
+
// keyword anchors the match to JavaScript/TypeScript syntax.
|
|
99
|
+
// See: https://github.com/gsd-build/gsd-2/issues/4388
|
|
100
|
+
const importPattern = /(?:require\s*\(\s*['"]|import\b[\s\S]*?\bfrom\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
|
|
96
101
|
let importMatch: RegExpExecArray | null;
|
|
97
102
|
while ((importMatch = importPattern.exec(description)) !== null) {
|
|
98
103
|
// Skip relative imports and node builtins
|
|
@@ -325,7 +330,12 @@ function extractPathFromAnnotation(raw: string): string {
|
|
|
325
330
|
|
|
326
331
|
const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
|
|
327
332
|
if (annotatedMatch) {
|
|
328
|
-
|
|
333
|
+
const prefix = annotatedMatch[1].trim();
|
|
334
|
+
const prefixBacktickMatch = prefix.match(/`([^`]+)`/);
|
|
335
|
+
if (prefixBacktickMatch && looksLikePathOrUrl(prefixBacktickMatch[1].trim())) {
|
|
336
|
+
return prefixBacktickMatch[1].trim();
|
|
337
|
+
}
|
|
338
|
+
return prefix.replace(/`/g, "").trim();
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
// Fallback: scan all backticked tokens and return the first one that looks
|
|
@@ -28,6 +28,37 @@ export interface ContextManagementConfig {
|
|
|
28
28
|
compaction_threshold_percent?: number; // default: 0.70, range: 0.5-0.95
|
|
29
29
|
tool_result_max_chars?: number; // default: 800, range: 200-10000
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Opt-in tool-output sandboxing for sub-sessions. When enabled, the gsd_exec
|
|
34
|
+
* MCP tool runs scripts in an isolated subprocess and returns only a short
|
|
35
|
+
* digest to the calling agent's context window; full stdout/stderr persist
|
|
36
|
+
* in the project memory store and can be retrieved by id later.
|
|
37
|
+
*
|
|
38
|
+
* Inspired by mksglu/context-mode (Elastic License 2.0). This is an
|
|
39
|
+
* independent implementation — no upstream code is incorporated.
|
|
40
|
+
*/
|
|
41
|
+
export interface ContextModeConfig {
|
|
42
|
+
/** Master switch. Default: true (opt-out via `enabled: false`). */
|
|
43
|
+
enabled?: boolean;
|
|
44
|
+
/** Per-invocation timeout in milliseconds. Default: 30_000. Range: 1_000–600_000. */
|
|
45
|
+
exec_timeout_ms?: number;
|
|
46
|
+
/** Cap on persisted stdout bytes per invocation. Default: 1_048_576 (1 MiB). Range: 4_096–16_777_216. */
|
|
47
|
+
exec_stdout_cap_bytes?: number;
|
|
48
|
+
/** Number of trailing stdout characters returned in the digest. Default: 300. Range: 0–4_000. */
|
|
49
|
+
exec_digest_chars?: number;
|
|
50
|
+
/** Environment variables forwarded to sandboxed processes (case-sensitive names). PATH and HOME are always forwarded. */
|
|
51
|
+
exec_env_allowlist?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve whether context-mode features (gsd_exec sandbox + compaction
|
|
56
|
+
* snapshot) should be active. Default is ON: missing config or missing
|
|
57
|
+
* `enabled` is treated as true. Only `enabled: false` disables.
|
|
58
|
+
*/
|
|
59
|
+
export function isContextModeEnabled(prefs: { context_mode?: ContextModeConfig } | null | undefined): boolean {
|
|
60
|
+
return prefs?.context_mode?.enabled !== false;
|
|
61
|
+
}
|
|
31
62
|
import type { GitHubSyncConfig } from "../github-sync/types.js";
|
|
32
63
|
|
|
33
64
|
// ─── Workflow Modes ──────────────────────────────────────────────────────────
|
|
@@ -117,6 +148,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|
|
117
148
|
"flat_rate_providers",
|
|
118
149
|
"language",
|
|
119
150
|
"context_window_override",
|
|
151
|
+
"context_mode",
|
|
120
152
|
]);
|
|
121
153
|
|
|
122
154
|
/** Canonical list of all dispatch unit types. */
|
|
@@ -300,6 +332,12 @@ export interface GSDPreferences {
|
|
|
300
332
|
*/
|
|
301
333
|
context_window_override?: number;
|
|
302
334
|
context_management?: ContextManagementConfig;
|
|
335
|
+
/**
|
|
336
|
+
* Tool-output sandboxing via gsd_exec. Keeps sub-session context windows
|
|
337
|
+
* clean by running scripts in a subprocess and only surfacing a short
|
|
338
|
+
* digest. See `ContextModeConfig`. Default: disabled.
|
|
339
|
+
*/
|
|
340
|
+
context_mode?: ContextModeConfig;
|
|
303
341
|
token_profile?: TokenProfile;
|
|
304
342
|
phases?: PhaseSkipPreferences;
|
|
305
343
|
auto_visualize?: boolean;
|
|
@@ -644,6 +644,50 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
644
644
|
}
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
+
// ─── Context Mode (gsd_exec sandbox) ────────────────────────────────────
|
|
648
|
+
if (preferences.context_mode !== undefined) {
|
|
649
|
+
if (typeof preferences.context_mode === "object" && preferences.context_mode !== null) {
|
|
650
|
+
const cmode = preferences.context_mode as unknown as Record<string, unknown>;
|
|
651
|
+
const validCmode: Record<string, unknown> = {};
|
|
652
|
+
|
|
653
|
+
if (cmode.enabled !== undefined) {
|
|
654
|
+
if (typeof cmode.enabled === "boolean") validCmode.enabled = cmode.enabled;
|
|
655
|
+
else errors.push("context_mode.enabled must be a boolean");
|
|
656
|
+
}
|
|
657
|
+
if (cmode.exec_timeout_ms !== undefined) {
|
|
658
|
+
const t = cmode.exec_timeout_ms;
|
|
659
|
+
if (typeof t === "number" && t >= 1000 && t <= 600_000) validCmode.exec_timeout_ms = Math.floor(t);
|
|
660
|
+
else errors.push("context_mode.exec_timeout_ms must be a number between 1000 and 600000");
|
|
661
|
+
}
|
|
662
|
+
if (cmode.exec_stdout_cap_bytes !== undefined) {
|
|
663
|
+
const b = cmode.exec_stdout_cap_bytes;
|
|
664
|
+
if (typeof b === "number" && b >= 4096 && b <= 16_777_216) validCmode.exec_stdout_cap_bytes = Math.floor(b);
|
|
665
|
+
else errors.push("context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216");
|
|
666
|
+
}
|
|
667
|
+
if (cmode.exec_digest_chars !== undefined) {
|
|
668
|
+
const c = cmode.exec_digest_chars;
|
|
669
|
+
if (typeof c === "number" && c >= 0 && c <= 4000) validCmode.exec_digest_chars = Math.floor(c);
|
|
670
|
+
else errors.push("context_mode.exec_digest_chars must be a number between 0 and 4000");
|
|
671
|
+
}
|
|
672
|
+
if (cmode.exec_env_allowlist !== undefined) {
|
|
673
|
+
if (
|
|
674
|
+
Array.isArray(cmode.exec_env_allowlist) &&
|
|
675
|
+
cmode.exec_env_allowlist.every((v) => typeof v === "string" && /^[A-Z_][A-Z0-9_]*$/i.test(v))
|
|
676
|
+
) {
|
|
677
|
+
validCmode.exec_env_allowlist = cmode.exec_env_allowlist;
|
|
678
|
+
} else {
|
|
679
|
+
errors.push("context_mode.exec_env_allowlist must be an array of valid env var names");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (Object.keys(validCmode).length > 0) {
|
|
684
|
+
validated.context_mode = validCmode as any;
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
errors.push("context_mode must be an object");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
647
691
|
// ─── Parallel Config ────────────────────────────────────────────────────
|
|
648
692
|
if (preferences.parallel && typeof preferences.parallel === "object") {
|
|
649
693
|
const p = preferences.parallel as unknown as Record<string, unknown>;
|
|
@@ -697,6 +741,41 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|
|
697
741
|
}
|
|
698
742
|
}
|
|
699
743
|
|
|
744
|
+
// ─── Slice Parallel Config ───────────────────────────────────────────────
|
|
745
|
+
if (preferences.slice_parallel !== undefined) {
|
|
746
|
+
if (typeof preferences.slice_parallel === "object" && preferences.slice_parallel !== null) {
|
|
747
|
+
const sp = preferences.slice_parallel as Record<string, unknown>;
|
|
748
|
+
const validSp: NonNullable<GSDPreferences["slice_parallel"]> = {};
|
|
749
|
+
|
|
750
|
+
if (sp.enabled !== undefined) {
|
|
751
|
+
if (typeof sp.enabled === "boolean") validSp.enabled = sp.enabled;
|
|
752
|
+
else errors.push("slice_parallel.enabled must be a boolean");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (sp.max_workers !== undefined) {
|
|
756
|
+
const maxWorkers = typeof sp.max_workers === "number" ? sp.max_workers : Number(sp.max_workers);
|
|
757
|
+
if (Number.isFinite(maxWorkers) && maxWorkers >= 1 && maxWorkers <= 8) {
|
|
758
|
+
validSp.max_workers = Math.floor(maxWorkers);
|
|
759
|
+
} else {
|
|
760
|
+
errors.push("slice_parallel.max_workers must be a number between 1 and 8");
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const knownSliceParallelKeys = new Set(["enabled", "max_workers"]);
|
|
765
|
+
for (const key of Object.keys(sp)) {
|
|
766
|
+
if (!knownSliceParallelKeys.has(key)) {
|
|
767
|
+
warnings.push(`unknown slice_parallel key "${key}" — ignored`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (Object.keys(validSp).length > 0) {
|
|
772
|
+
validated.slice_parallel = validSp;
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
errors.push("slice_parallel must be an object");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
700
779
|
// ─── Reactive Execution ─────────────────────────────────────────────────
|
|
701
780
|
if (preferences.reactive_execution !== undefined) {
|
|
702
781
|
if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {
|