pi-ast-sgrep 1.3.2 → 2.0.0

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.
@@ -0,0 +1,242 @@
1
+ import { Worker } from "node:worker_threads";
2
+ const DEFAULT_TIMEOUT_MS = 30_000;
3
+ const MAX_CODE_CHARS = 32_000;
4
+ const MAX_BRIDGE_CALLS = 256;
5
+ const MAX_BRIDGE_REQUEST_CHARS = 64_000;
6
+ const MAX_BRIDGE_RESPONSE_CHARS = 4 * 1024 * 1024;
7
+ const MAX_ERROR_CHARS = 8_192;
8
+ const MAX_LOG_LINES = 100;
9
+ const MAX_LOG_CHARS = 64_000;
10
+ const MAX_LOG_LINE_CHARS = 4_096;
11
+ const MAX_RESULT_JSON_CHARS = 1_000_000;
12
+ const RESULT_SERIALIZE_TIMEOUT_MS = 1_000;
13
+ const MAX_TIMER_MS = 2_147_483_647;
14
+ /** Strip markdown fences and normalize to an async IIFE expression. */
15
+ export function normalizeCode(raw) {
16
+ let code = raw.trim();
17
+ if (code.startsWith("```")) {
18
+ code = code.replace(/^```(?:javascript|js|typescript|ts)?\s*/i, "").replace(/\s*```$/, "").trim();
19
+ }
20
+ if (/^async\s*\(/.test(code) || /^async\s+function\b/.test(code)) {
21
+ return `(${code.endsWith(";") ? code.slice(0, -1) : code})()`;
22
+ }
23
+ return `(async () => {\n${code}\n})()`;
24
+ }
25
+ /**
26
+ * Run model-generated JavaScript against the typed `asgrep` connector.
27
+ *
28
+ * Model-generated code is not trusted with the extension host's ambient Node
29
+ * authority. A dedicated worker contains CPU/microtask denial of service; its
30
+ * VM hides `process`, module loading, and host constructors, with a JSON bridge
31
+ * as the only exposed capability. This is not an OS sandbox, so deployments
32
+ * requiring adversarial-code isolation should still restrict the Pi process.
33
+ */
34
+ export async function runCodemode(rawCode, asgrep, options = {}) {
35
+ const requestedTimeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
36
+ const timeoutMs = Number.isFinite(requestedTimeout)
37
+ ? Math.min(MAX_TIMER_MS, Math.max(1, Math.trunc(requestedTimeout)))
38
+ : DEFAULT_TIMEOUT_MS;
39
+ const wall0 = Date.now();
40
+ if (rawCode.length > MAX_CODE_CHARS) {
41
+ return resultErr(`code exceeds ${MAX_CODE_CHARS} characters`, [], rawCode.slice(0, 200), wall0, options.stats);
42
+ }
43
+ if (options.signal?.aborted) {
44
+ return resultErr("codemode aborted", [], rawCode.slice(0, 200), wall0, options.stats);
45
+ }
46
+ const code = normalizeCode(rawCode);
47
+ const runController = new AbortController();
48
+ const hostMethods = {
49
+ search: asgrep.search.bind(asgrep),
50
+ semantic: asgrep.semantic.bind(asgrep),
51
+ chain: asgrep.chain.bind(asgrep),
52
+ defs: asgrep.defs.bind(asgrep),
53
+ callers: asgrep.callers.bind(asgrep),
54
+ imports: asgrep.imports.bind(asgrep),
55
+ indexStatus: asgrep.indexStatus.bind(asgrep),
56
+ indexRepo: asgrep.indexRepo.bind(asgrep),
57
+ catalogSearch: asgrep.catalogSearch.bind(asgrep),
58
+ catalogDescribe: asgrep.catalogDescribe.bind(asgrep),
59
+ };
60
+ const workerUrl = new URL(import.meta.url.endsWith(".ts") ? "./sandbox-worker.ts" : "./sandbox-worker.js", import.meta.url);
61
+ let worker;
62
+ try {
63
+ worker = new Worker(workerUrl, {
64
+ workerData: {
65
+ code,
66
+ timeoutMs,
67
+ limits: {
68
+ bridgeCalls: MAX_BRIDGE_CALLS,
69
+ bridgeRequestChars: MAX_BRIDGE_REQUEST_CHARS,
70
+ errorChars: MAX_ERROR_CHARS,
71
+ logLines: MAX_LOG_LINES,
72
+ logChars: MAX_LOG_CHARS,
73
+ logLineChars: MAX_LOG_LINE_CHARS,
74
+ resultJsonChars: MAX_RESULT_JSON_CHARS,
75
+ serializeTimeoutMs: RESULT_SERIALIZE_TIMEOUT_MS,
76
+ },
77
+ },
78
+ resourceLimits: {
79
+ maxOldGenerationSizeMb: 64,
80
+ maxYoungGenerationSizeMb: 16,
81
+ stackSizeMb: 4,
82
+ },
83
+ });
84
+ }
85
+ catch (cause) {
86
+ return resultErr(cause instanceof Error ? cause.message : String(cause), [], code, wall0, options.stats);
87
+ }
88
+ return new Promise((resolve) => {
89
+ let active = true;
90
+ const receivedCallIds = new Set();
91
+ const finish = (outcome) => {
92
+ if (!active)
93
+ return;
94
+ active = false;
95
+ clearTimeout(timer);
96
+ options.signal?.removeEventListener("abort", onAbort);
97
+ // Cancel host work that the disposable worker was awaiting or abandoned.
98
+ runController.abort();
99
+ void worker.terminate().catch(() => undefined).then(() => {
100
+ outcome.wallMs = Date.now() - wall0;
101
+ resolve(outcome);
102
+ });
103
+ };
104
+ const fail = (error, logs = []) => {
105
+ finish(resultErr(error, logs, code, wall0, options.stats));
106
+ };
107
+ const onAbort = () => fail("codemode aborted");
108
+ const timer = setTimeout(() => fail(`codemode timeout after ${timeoutMs}ms`), timeoutMs);
109
+ worker.on("message", (message) => {
110
+ if (!active)
111
+ return;
112
+ if (!isSandboxMessage(message)) {
113
+ fail("codemode worker sent an invalid message");
114
+ return;
115
+ }
116
+ if (message.type === "done") {
117
+ if (message.ok) {
118
+ finish(resultOk(message.result, message.logs, code, wall0, options.stats));
119
+ }
120
+ else {
121
+ fail(message.error ?? "codemode worker failed", message.logs);
122
+ }
123
+ return;
124
+ }
125
+ for (const call of message.calls) {
126
+ if (call.id >= MAX_BRIDGE_CALLS || receivedCallIds.has(call.id)) {
127
+ fail("codemode worker exceeded its bridge call allowance");
128
+ return;
129
+ }
130
+ receivedCallIds.add(call.id);
131
+ }
132
+ for (const call of message.calls)
133
+ void handleSandboxCall(call);
134
+ });
135
+ worker.once("error", (error) => fail(error.message));
136
+ worker.once("exit", (code) => {
137
+ if (active)
138
+ fail(`codemode worker exited ${code}`);
139
+ });
140
+ const handleSandboxCall = async (call) => {
141
+ if (!active)
142
+ return;
143
+ let payload;
144
+ try {
145
+ if (call.payload.length > MAX_BRIDGE_REQUEST_CHARS) {
146
+ throw new Error(`codemode call arguments exceed ${MAX_BRIDGE_REQUEST_CHARS} characters`);
147
+ }
148
+ if (!Object.hasOwn(hostMethods, call.method)) {
149
+ throw new Error(`unknown asgrep method: ${call.method}`);
150
+ }
151
+ const input = JSON.parse(call.payload);
152
+ const methodCall = hostMethods[call.method];
153
+ const value = await methodCall(input, { signal: runController.signal });
154
+ payload = stringifyBounded({ ok: true, value }, MAX_BRIDGE_RESPONSE_CHARS, "codemode call result");
155
+ }
156
+ catch (cause) {
157
+ payload = JSON.stringify({
158
+ ok: false,
159
+ error: safeErrorMessage(cause).slice(0, MAX_ERROR_CHARS),
160
+ });
161
+ }
162
+ if (active)
163
+ worker.postMessage({ type: "callResult", id: call.id, payload });
164
+ };
165
+ options.signal?.addEventListener("abort", onAbort, { once: true });
166
+ if (options.signal?.aborted)
167
+ onAbort();
168
+ });
169
+ }
170
+ function isSandboxMessage(message) {
171
+ if (typeof message !== "object" || message === null || !("type" in message))
172
+ return false;
173
+ if (message.type === "calls") {
174
+ return "calls" in message
175
+ && Array.isArray(message.calls)
176
+ && message.calls.length > 0
177
+ && message.calls.length <= MAX_BRIDGE_CALLS
178
+ && message.calls.every((call) => isSandboxCall(call));
179
+ }
180
+ if (message.type !== "done"
181
+ || !("ok" in message)
182
+ || typeof message.ok !== "boolean"
183
+ || !("logs" in message)
184
+ || !Array.isArray(message.logs)
185
+ || message.logs.length > MAX_LOG_LINES
186
+ || !message.logs.every((line) => typeof line === "string" && line.length <= MAX_LOG_LINE_CHARS)
187
+ || message.logs.reduce((total, line) => total + line.length, 0) > MAX_LOG_CHARS) {
188
+ return false;
189
+ }
190
+ return !("error" in message)
191
+ || message.error === undefined
192
+ || (typeof message.error === "string" && message.error.length <= MAX_ERROR_CHARS);
193
+ }
194
+ function isSandboxCall(call) {
195
+ return typeof call === "object"
196
+ && call !== null
197
+ && "id" in call
198
+ && typeof call.id === "number"
199
+ && Number.isSafeInteger(call.id)
200
+ && call.id >= 0
201
+ && "method" in call
202
+ && typeof call.method === "string"
203
+ && "payload" in call
204
+ && typeof call.payload === "string";
205
+ }
206
+ function safeErrorMessage(cause) {
207
+ try {
208
+ return String(cause instanceof Error ? cause.message : cause);
209
+ }
210
+ catch {
211
+ return "codemode call failed";
212
+ }
213
+ }
214
+ function stringifyBounded(value, maxBytes, label) {
215
+ let remaining = maxBytes;
216
+ const payload = JSON.stringify(value, (key, item) => {
217
+ remaining -= Buffer.byteLength(key) + 8;
218
+ if (typeof item === "string")
219
+ remaining -= Buffer.byteLength(item);
220
+ if (remaining < 0)
221
+ throw new Error(`${label} exceeds ${maxBytes} bytes`);
222
+ return item;
223
+ });
224
+ if (payload === undefined || Buffer.byteLength(payload) > maxBytes) {
225
+ throw new Error(`${label} exceeds ${maxBytes} bytes`);
226
+ }
227
+ return payload;
228
+ }
229
+ function resultOk(result, logs, code, wall0, statsFn) {
230
+ const out = { ok: true, result, logs, code, wallMs: Date.now() - wall0 };
231
+ const stats = statsFn?.();
232
+ if (stats)
233
+ out.stats = stats;
234
+ return out;
235
+ }
236
+ function resultErr(error, logs, code, wall0, statsFn) {
237
+ const out = { ok: false, result: null, logs, error, code, wallMs: Date.now() - wall0 };
238
+ const stats = statsFn?.();
239
+ if (stats)
240
+ out.stats = stats;
241
+ return out;
242
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,204 @@
1
+ import vm from "node:vm";
2
+ import { parentPort, workerData } from "node:worker_threads";
3
+ const port = (() => {
4
+ if (!parentPort)
5
+ throw new Error("codemode sandbox requires a parent port");
6
+ return parentPort;
7
+ })();
8
+ const data = workerData;
9
+ const pending = new Map();
10
+ const outgoing = [];
11
+ let nextCallId = 0;
12
+ let flushScheduled = false;
13
+ port.on("message", (message) => {
14
+ if (message.type !== "callResult")
15
+ return;
16
+ const resolve = pending.get(message.id);
17
+ if (!resolve)
18
+ return;
19
+ pending.delete(message.id);
20
+ resolve(message.payload);
21
+ });
22
+ const bridge = (method, payload) => new Promise((resolve) => {
23
+ if (nextCallId >= data.limits.bridgeCalls) {
24
+ resolve(JSON.stringify({
25
+ ok: false,
26
+ error: `codemode exceeds ${data.limits.bridgeCalls} host calls`,
27
+ }));
28
+ return;
29
+ }
30
+ const id = nextCallId++;
31
+ pending.set(id, resolve);
32
+ outgoing.push({ id, method, payload });
33
+ if (!flushScheduled) {
34
+ flushScheduled = true;
35
+ queueMicrotask(() => {
36
+ flushScheduled = false;
37
+ const calls = outgoing.splice(0);
38
+ if (calls.length > 0)
39
+ port.postMessage({ type: "calls", calls });
40
+ });
41
+ }
42
+ });
43
+ void run();
44
+ async function run() {
45
+ const logs = [];
46
+ let logChars = 0;
47
+ const logBridge = (line) => {
48
+ if (logs.length >= data.limits.logLines || logChars >= data.limits.logChars)
49
+ return;
50
+ const remaining = data.limits.logChars - logChars;
51
+ const bounded = line.length <= remaining
52
+ ? line
53
+ : `${line.slice(0, Math.max(0, remaining - 1))}…`;
54
+ logs.push(bounded);
55
+ logChars += bounded.length;
56
+ };
57
+ Object.setPrototypeOf(bridge, null);
58
+ Object.setPrototypeOf(logBridge, null);
59
+ Object.freeze(bridge);
60
+ Object.freeze(logBridge);
61
+ try {
62
+ const globals = Object.create(null);
63
+ globals.__asgrepBridge = bridge;
64
+ globals.__asgrepLog = logBridge;
65
+ const context = vm.createContext(globals, {
66
+ codeGeneration: { strings: false, wasm: false },
67
+ });
68
+ new vm.Script(bootstrap(data.limits), {
69
+ filename: "asgrep-codemode-bootstrap.js",
70
+ }).runInContext(context, { timeout: Math.min(data.timeoutMs, 1_000) });
71
+ const script = new vm.Script(data.code, { filename: "asgrep-codemode.js" });
72
+ const value = await Promise.resolve(script.runInContext(context, {
73
+ displayErrors: true,
74
+ timeout: data.timeoutMs,
75
+ }));
76
+ const setResult = context.__asgrepSetResult;
77
+ if (typeof setResult !== "function") {
78
+ throw new Error("codemode result bridge is unavailable");
79
+ }
80
+ setResult(value);
81
+ const serialized = new vm.Script("globalThis.__asgrepSerializeResult()", {
82
+ filename: "asgrep-codemode-result.js",
83
+ }).runInContext(context, {
84
+ displayErrors: true,
85
+ timeout: Math.min(data.timeoutMs, data.limits.serializeTimeoutMs),
86
+ });
87
+ const result = serialized === undefined ? undefined : JSON.parse(serialized);
88
+ finish({ type: "done", ok: true, result, logs });
89
+ }
90
+ catch (cause) {
91
+ finish({
92
+ type: "done",
93
+ ok: false,
94
+ error: safeErrorMessage(cause).slice(0, data.limits.errorChars),
95
+ logs,
96
+ });
97
+ }
98
+ }
99
+ function safeErrorMessage(cause) {
100
+ try {
101
+ return String(cause instanceof Error ? cause.message : cause);
102
+ }
103
+ catch {
104
+ return "codemode worker failed";
105
+ }
106
+ }
107
+ function finish(message) {
108
+ port.postMessage(message);
109
+ port.close();
110
+ }
111
+ function bootstrap(limits) {
112
+ return `
113
+ {
114
+ const hostCall = globalThis.__asgrepBridge;
115
+ const hostLog = globalThis.__asgrepLog;
116
+ delete globalThis.__asgrepBridge;
117
+ delete globalThis.__asgrepLog;
118
+
119
+ let resultValue;
120
+ const setResult = (value) => { resultValue = value; };
121
+ const stringify = JSON.stringify;
122
+ const stringifyBounded = (value, maxChars, label) => {
123
+ let remaining = maxChars;
124
+ const serialized = stringify(value, (key, item) => {
125
+ remaining -= key.length + 8;
126
+ if (typeof item === "string") remaining -= item.length;
127
+ if (remaining < 0) throw new Error(\`codemode \${label} exceeds \${maxChars} characters\`);
128
+ return item;
129
+ });
130
+ if (serialized !== undefined && serialized.length > maxChars) {
131
+ throw new Error(\`codemode \${label} exceeds \${maxChars} characters\`);
132
+ }
133
+ return serialized;
134
+ };
135
+ const serializeResult = () => stringifyBounded(resultValue, ${limits.resultJsonChars}, "result");
136
+ Object.freeze(setResult);
137
+ Object.freeze(serializeResult);
138
+ Object.defineProperty(globalThis, "__asgrepSetResult", {
139
+ value: setResult, configurable: false, writable: false,
140
+ });
141
+ Object.defineProperty(globalThis, "__asgrepSerializeResult", {
142
+ value: serializeResult, configurable: false, writable: false,
143
+ });
144
+
145
+ // Worker heap limits do not reliably account for backing stores. Code Mode
146
+ // exchanges JSON, so raw-memory and WebAssembly APIs add risk without utility.
147
+ for (const name of [
148
+ "ArrayBuffer", "SharedArrayBuffer", "DataView", "Atomics", "WebAssembly",
149
+ "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array",
150
+ "Int32Array", "Uint32Array", "Float32Array", "Float64Array",
151
+ "BigInt64Array", "BigUint64Array",
152
+ ]) {
153
+ Object.defineProperty(globalThis, name, {
154
+ value: undefined, configurable: false, writable: false,
155
+ });
156
+ }
157
+
158
+ const invoke = async (method, args = {}) => {
159
+ const payload = stringifyBounded(args, ${limits.bridgeRequestChars}, "call arguments");
160
+ const response = JSON.parse(await hostCall(method, payload));
161
+ if (!response.ok) throw new Error(response.error || \`asgrep.\${method} failed\`);
162
+ return response.value;
163
+ };
164
+ const api = Object.create(null);
165
+ for (const method of [
166
+ "search", "semantic", "chain", "defs", "callers", "imports",
167
+ "indexStatus", "indexRepo", "catalogSearch", "catalogDescribe",
168
+ ]) {
169
+ Object.defineProperty(api, method, {
170
+ enumerable: true,
171
+ value: (args = {}) => invoke(method, args),
172
+ });
173
+ }
174
+ Object.freeze(api);
175
+
176
+ const formatLog = (value) => {
177
+ if (typeof value === "string") return value.slice(0, ${limits.logLineChars});
178
+ try { return stringifyBounded(value, ${limits.logLineChars}, "log line"); }
179
+ catch { return "[unserializable or oversized log value]"; }
180
+ };
181
+ const consoleApi = Object.create(null);
182
+ for (const level of ["log", "info", "warn", "error", "debug"]) {
183
+ Object.defineProperty(consoleApi, level, {
184
+ enumerable: true,
185
+ value: (...args) => {
186
+ let line = "";
187
+ for (const arg of args) {
188
+ const part = formatLog(arg);
189
+ const prefix = line.length === 0 ? "" : " ";
190
+ const remaining = ${limits.logLineChars} - line.length;
191
+ if (remaining <= 0) break;
192
+ line += (prefix + part).slice(0, remaining);
193
+ }
194
+ hostLog(line);
195
+ },
196
+ });
197
+ }
198
+ Object.freeze(consoleApi);
199
+
200
+ Object.defineProperty(globalThis, "asgrep", { value: api, configurable: false, writable: false });
201
+ Object.defineProperty(globalThis, "console", { value: consoleApi, configurable: false, writable: false });
202
+ }
203
+ `;
204
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Session-scoped native Code Mode sessions.
3
+ *
4
+ * Primary path: in-process NAPI (`CodeModeSession` inside Node) — same model as
5
+ * MCP linking core. Zero CLI spawn.
6
+ *
7
+ * Fallback: sticky `codemode-serve` child only when the `.node` addon is missing
8
+ * (unsupported host / incomplete install). Doctor reports that as degraded.
9
+ */
10
+ import type { MachineEnvelope } from "../runtime.js";
11
+ import { type StickyWorker } from "./dispatch.js";
12
+ import { type StickyWorkerOptions } from "./worker.js";
13
+ export type SessionPoolOptions = {
14
+ /** Required only for CLI sticky fallback. */
15
+ binary?: string;
16
+ env?: NodeJS.ProcessEnv;
17
+ timeoutMs?: number;
18
+ maxOutputBytes?: number;
19
+ root?: string;
20
+ indexPath?: string;
21
+ useEmbed?: boolean;
22
+ limit?: number;
23
+ };
24
+ export type StickyStarter = (options: StickyWorkerOptions) => Promise<StickyWorker>;
25
+ export declare class NativeSessionPool {
26
+ #private;
27
+ constructor(startFn?: StickyStarter);
28
+ configure(options: SessionPoolOptions): void;
29
+ configured(): boolean;
30
+ /** Active backend after first successful acquire. */
31
+ backend(): "napi" | "cli" | "none";
32
+ acquire(root: string): Promise<StickyWorker | null>;
33
+ call(root: string, tool: string, args?: Record<string, unknown>, options?: {
34
+ signal?: AbortSignal;
35
+ }): Promise<MachineEnvelope>;
36
+ invalidate(root: string): Promise<void>;
37
+ shutdown(): Promise<void>;
38
+ }
39
+ /** Singleton for advanced hosts; tools registration uses a local pool. */
40
+ export declare const sharedNativePool: NativeSessionPool;