@scalequality/cli 0.4.9-rc.2 → 0.4.9
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/connect.build.json +3 -3
- package/dist/connect.cjs +2335 -1578
- package/package.json +1 -1
package/dist/connect.cjs
CHANGED
|
@@ -33,123 +33,11 @@ var import_os4 = require("os");
|
|
|
33
33
|
var import_path16 = require("path");
|
|
34
34
|
var import_url = require("url");
|
|
35
35
|
|
|
36
|
-
// src/application/services/workspaceSandbox/
|
|
37
|
-
var
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
function parseReasoningCapability(raw) {
|
|
42
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
43
|
-
const r = raw;
|
|
44
|
-
if (!Array.isArray(r.levels)) return null;
|
|
45
|
-
const listed = [...r.levels, ...r.off === true ? ["off"] : []];
|
|
46
|
-
const levels = REASONING_LEVELS.filter((l) => listed.includes(l));
|
|
47
|
-
if (!levels.length) return null;
|
|
48
|
-
const def = isReasoningLevel(r.default) && levels.includes(r.default) ? r.default : levels[0];
|
|
49
|
-
return { levels, default: def };
|
|
50
|
-
}
|
|
51
|
-
function effectiveReasoning(requested, capability) {
|
|
52
|
-
if (!capability) return null;
|
|
53
|
-
if (requested && capability.levels.includes(requested)) return requested;
|
|
54
|
-
return capability.default;
|
|
55
|
-
}
|
|
56
|
-
function isMaxMode(level, capability) {
|
|
57
|
-
if (!level || level === "off" || !capability) return false;
|
|
58
|
-
return capability.levels[capability.levels.length - 1] === level;
|
|
59
|
-
}
|
|
60
|
-
function turnReasoning(level, capability) {
|
|
61
|
-
if (!capability || !level) return { options: {}, forceNoThinking: true, outputCeiling: false };
|
|
62
|
-
if (level === "off") return { options: { thinking: { type: "disabled" } }, forceNoThinking: true, outputCeiling: false };
|
|
63
|
-
return { options: { effort: level, thinking: { type: "adaptive" } }, forceNoThinking: false, outputCeiling: isMaxMode(level, capability) };
|
|
64
|
-
}
|
|
65
|
-
function engineModelCapabilities(aliases) {
|
|
66
|
-
const entries = ["-mid_conv_system"];
|
|
67
|
-
const seen = /* @__PURE__ */ new Set();
|
|
68
|
-
for (const { alias, reasoning } of aliases) {
|
|
69
|
-
const name = alias.trim();
|
|
70
|
-
if (!name || seen.has(name) || /[;=,]/.test(name)) continue;
|
|
71
|
-
seen.add(name);
|
|
72
|
-
entries.push(reasoning ? `${name}=effort,${reasoning.levels.includes("max") ? "" : "-"}max_effort,${reasoning.levels.includes("xhigh") ? "" : "-"}xhigh_effort` : `${name}=-effort,-max_effort,-xhigh_effort`);
|
|
73
|
-
}
|
|
74
|
-
return entries.join(";");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/application/services/workspaceSandbox/autoRouting.ts
|
|
78
|
-
var LONG_INSTRUCTION_CHARS = 3e3;
|
|
79
|
-
var LONG_CODE_LINES = 60;
|
|
80
|
-
var MANY_FILES = 5;
|
|
81
|
-
var HARD_WORK = [
|
|
82
|
-
[/\b(refactor|refator|refactoriz)\w*\b[\s\S]{0,80}\b(across|between|all|every|entre|todos|todas|varios|varias|modul|servic|packages?|pacotes?|layers?|camadas?)/, "refactor across modules"],
|
|
83
|
-
[/\b(architect|arquitet|arquitect|redesign|re-architect)\w*/, "architecture"],
|
|
84
|
-
[/\b(root cause|causa raiz|causa-raiz|debug|depur|investigat|investig|stack ?trace|flaky|race condition|condicao de corrida|memory leak|vazamento de memoria|deadlock|intermittent|intermitente)\w*/, "debugging, root cause"],
|
|
85
|
-
[/\b(migrat|migra(c|t)a?o|migrar|migre|upgrade\b[\s\S]{0,40}\b(from|to|de|para)\b|port (this|the|it)\b[\s\S]{0,40}\bto\b)/, "migration"],
|
|
86
|
-
[/\b(vulnerab|cve-\d|security (fix|issue|flaw|hole|bug)|injection|injecao|xss|csrf|ssrf|rce\b|seguranca|seguridad|exploit)\w*/, "security fix"],
|
|
87
|
-
[/\b(every|all|each|todos os|todas as|cada|todos los|todas las) (the )?(files?|modules?|services?|endpoints?|packages?|arquivos?|modulos?|servicos?|archivos?)\b|\b(across|throughout) (the )?(whole )?(codebase|repo|repository|project|modules|services)\b|\b(codebase|base de codigo)[- ](wide|inteira|toda)\b/, "many files"],
|
|
88
|
-
[/\b(refactor|refator|refactoriz)\w*/, "refactor"],
|
|
89
|
-
[/\b(step[- ]by[- ]step|passo a passo|paso a paso|multi[- ]step|end[- ]to[- ]end|threat model|performance (issue|regression|problem)|regressao de performance|rewrite|reescrev|reescrib)\w*/, "multi-step work"]
|
|
90
|
-
];
|
|
91
|
-
var normalize = (text3) => text3.toLowerCase().normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
92
|
-
function namedFiles(text3) {
|
|
93
|
-
const found = /* @__PURE__ */ new Set();
|
|
94
|
-
for (const m of text3.matchAll(/(?:^|[\s`'"(\[])((?:\.{0,2}\/)?[\w.-]+(?:\/[\w.-]+)+\/?|[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|cpp|c|h|swift|scala|sql|yml|yaml|json|tf|vue|svelte))(?=$|[\s`'"),.:;\]])/g)) {
|
|
95
|
-
found.add(m[1].replace(/^\.\//, ""));
|
|
96
|
-
}
|
|
97
|
-
return found.size;
|
|
98
|
-
}
|
|
99
|
-
function classifyCodeTurn(s) {
|
|
100
|
-
if (s.maxMode) return { hard: true, reason: "Max Mode" };
|
|
101
|
-
if (s.previousTrouble) return { hard: true, reason: s.previousTrouble };
|
|
102
|
-
const text3 = s.content;
|
|
103
|
-
const codeLines = [...text3.matchAll(/```[\s\S]*?```/g)].reduce((n, block) => n + block[0].split("\n").length, 0);
|
|
104
|
-
if (codeLines > LONG_CODE_LINES) return { hard: true, reason: `${codeLines} lines of code in the request` };
|
|
105
|
-
if (text3.length > LONG_INSTRUCTION_CHARS) return { hard: true, reason: `long instruction (${text3.length} characters)` };
|
|
106
|
-
const plain = normalize(text3);
|
|
107
|
-
for (const [re, reason] of HARD_WORK) if (re.test(plain)) return { hard: true, reason };
|
|
108
|
-
const files = namedFiles(text3);
|
|
109
|
-
if (files >= MANY_FILES) return { hard: true, reason: `${files} files named` };
|
|
110
|
-
return { hard: false, reason: text3.length > 600 || codeLines > 0 ? "bounded task" : "short, direct request" };
|
|
111
|
-
}
|
|
112
|
-
var STREAM_REASONS = ["ORG_OUTPUT_TEXT_RULES", "SEMANTIC_INSPECTOR", "APPLICATION_OUTPUT_TEXT_RULES", "DESTINATION_NO_STREAM"];
|
|
113
|
-
function routeAutoTurn(boot, signals) {
|
|
114
|
-
if (boot.model !== "sq-auto") return null;
|
|
115
|
-
const aliases = boot.runtime.aliases ?? [];
|
|
116
|
-
if (!aliases.some((a) => a.tier)) return null;
|
|
117
|
-
const primary = boot.runtime.primaryModel || aliases.find((a) => a.tier === "MEDIUM")?.alias || null;
|
|
118
|
-
if (!primary) return null;
|
|
119
|
-
const decision = classifyCodeTurn(signals);
|
|
120
|
-
const max = decision.hard ? aliases.find((a) => a.tier === "COMPLEX") ?? aliases.find((a) => a.alias === "sq-auto-max") : void 0;
|
|
121
|
-
const chosen = max ?? aliases.find((a) => a.alias === primary) ?? { alias: primary, reasoning: null, tier: "MEDIUM" };
|
|
122
|
-
const fallback = decision.hard && !max;
|
|
123
|
-
const kind = decision.hard ? "hard task" : "everyday task";
|
|
124
|
-
return {
|
|
125
|
-
alias: chosen.alias,
|
|
126
|
-
tier: chosen.tier ?? null,
|
|
127
|
-
hard: decision.hard,
|
|
128
|
-
reason: decision.reason,
|
|
129
|
-
label: `SQ Auto \xB7 ${kind} \xB7 ${decision.reason}${fallback ? " (no hard-task model available, main model used)" : ""}`,
|
|
130
|
-
capability: chosen.reasoning ?? null,
|
|
131
|
-
maxOutputTokens: typeof chosen.maxOutputTokens === "number" && chosen.maxOutputTokens > 0 ? chosen.maxOutputTokens : null,
|
|
132
|
-
fallback
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function credentialAlias(raw) {
|
|
136
|
-
const tier = raw.tier === "LIGHT" || raw.tier === "MEDIUM" || raw.tier === "COMPLEX" ? raw.tier : null;
|
|
137
|
-
const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.trunc(v) : null;
|
|
138
|
-
const max = positive(raw.maxOutputTokens), organization = positive(raw.organizationMaxOutputTokens);
|
|
139
|
-
const source = raw.outputLimitSource === "ORGANIZATION" || raw.outputLimitSource === "MODEL" || raw.outputLimitSource === "PLATFORM" ? raw.outputLimitSource : null;
|
|
140
|
-
const s = raw.stream && typeof raw.stream === "object" ? raw.stream : null;
|
|
141
|
-
const mode = s && (s.mode === "LIVE" || s.mode === "BUFFERED" || s.mode === "BUFFERED_POLICY") ? s.mode : null;
|
|
142
|
-
const reason = s && STREAM_REASONS.includes(s.reason) ? s.reason : null;
|
|
143
|
-
return {
|
|
144
|
-
alias: String(raw.alias),
|
|
145
|
-
reasoning: parseReasoningCapability(raw.reasoning),
|
|
146
|
-
...tier ? { tier } : {},
|
|
147
|
-
...max ? { maxOutputTokens: max } : {},
|
|
148
|
-
...source ? { outputLimitSource: source } : {},
|
|
149
|
-
...organization ? { organizationMaxOutputTokens: organization } : {},
|
|
150
|
-
...mode ? { stream: { mode, ...reason ? { reason } : {} } } : {}
|
|
151
|
-
};
|
|
152
|
-
}
|
|
36
|
+
// src/application/services/workspaceSandbox/importers.ts
|
|
37
|
+
var import_fs = require("fs");
|
|
38
|
+
var import_promises = require("fs/promises");
|
|
39
|
+
var import_path = require("path");
|
|
40
|
+
var import_readline = require("readline");
|
|
153
41
|
|
|
154
42
|
// src/application/services/workspaceSandbox/machineShared.ts
|
|
155
43
|
var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
@@ -229,838 +117,615 @@ function localRepoId(rel) {
|
|
|
229
117
|
return `local:${rel}`;
|
|
230
118
|
}
|
|
231
119
|
|
|
232
|
-
// src/application/services/workspaceSandbox/
|
|
233
|
-
var
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
120
|
+
// src/application/services/workspaceSandbox/secretScrubber.ts
|
|
121
|
+
var R = (kind) => `[REDACTED:${kind}]`;
|
|
122
|
+
function looksLikeSecretValue(v) {
|
|
123
|
+
if (v.length < 8) return false;
|
|
124
|
+
if (/^\[REDACTED:/.test(v)) return false;
|
|
125
|
+
if (/^(true|false|null|undefined|none|nil|string|number|boolean|required|optional|redacted|changeme|example|placeholder|xxx+|\*+)$/i.test(v)) return false;
|
|
126
|
+
if (/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)+(\(\))?$/.test(v) || /^[A-Za-z_$][\w$]*\(\)?$/.test(v)) return false;
|
|
127
|
+
if (/^(\$\{?[A-Za-z_][\w]*\}?|\{\{.*\}\}|<[^>]*>|%[A-Za-z_]+%)$/.test(v)) return false;
|
|
128
|
+
if (/^[A-Za-z_]+$/.test(v) && v.length < 24) return false;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
var RULES = [
|
|
132
|
+
{ kind: "PRIVATE_KEY", re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/g },
|
|
133
|
+
{ kind: "CONNECTION_STRING", re: /\b([a-z][a-z0-9+.-]{1,30}:\/\/)([^\s:@/'"]{1,200}):([^\s@/'"]{1,300})@/gi, replace: (_m, scheme, user) => `${scheme}${user}:${R("PASSWORD")}@` },
|
|
134
|
+
{ kind: "AWS_ACCESS_KEY", re: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA|ANVA|AIPA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
|
|
135
|
+
{ kind: "AWS_SECRET_KEY", re: /\b(aws_?secret_?access_?key|aws_?secret|secretAccessKey)(["']?\s*[:=]\s*["']?)([A-Za-z0-9/+=]{40})\b/gi, replace: (_m, k, sep11) => `${k}${sep11}${R("AWS_SECRET_KEY")}` },
|
|
136
|
+
{ kind: "ANTHROPIC_KEY", re: /\bsk-ant-[a-z]{2,10}\d{0,3}-[A-Za-z0-9_-]{20,}/g },
|
|
137
|
+
{ kind: "OPENAI_KEY", re: /\bsk-(?:proj-|svcacct-|admin-|None-)?[A-Za-z0-9_-]{20,}/g },
|
|
138
|
+
{ kind: "GITHUB_TOKEN", re: /\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{22,255})\b/g },
|
|
139
|
+
{ kind: "GITLAB_TOKEN", re: /\bgl(?:pat|dt|rt|ptt|ft|cbt|imt|oas|soat|agent)-[A-Za-z0-9_-]{20,}/g },
|
|
140
|
+
{ kind: "SLACK_TOKEN", re: /\bxox[abposre]-[A-Za-z0-9-]{10,}/g },
|
|
141
|
+
{ kind: "SLACK_WEBHOOK", re: /https:\/\/hooks\.slack\.com\/(?:services|workflows|triggers)\/[A-Za-z0-9_/-]{20,}/g },
|
|
142
|
+
{ kind: "STRIPE_KEY", re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
|
|
143
|
+
{ kind: "STRIPE_WEBHOOK_SECRET", re: /\bwhsec_[A-Za-z0-9]{24,}\b/g },
|
|
144
|
+
{ kind: "GOOGLE_API_KEY", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
145
|
+
{ kind: "JWT", re: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g },
|
|
146
|
+
{ kind: "BEARER_TOKEN", re: /\b(Bearer\s+)([A-Za-z0-9._~+/=-]{20,})/g, replace: (_m, p) => `${p}${R("BEARER_TOKEN")}` },
|
|
147
|
+
{
|
|
148
|
+
kind: "ASSIGNED_SECRET",
|
|
149
|
+
// password=..., "api_key": "...", SECRET_TOKEN: ..., --token ... (key names that say "secret").
|
|
150
|
+
re: /\b([A-Za-z0-9_.-]*(?:passw(?:or)?d|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|credential|auth[_-]?key)[A-Za-z0-9_-]*)(["']?\s*(?:=|:|\s)\s*)(["']?)([^\s"'`,;)}\]]{8,500})\3/gi,
|
|
151
|
+
replace: (_m, key, sep11, quote, value) => looksLikeSecretValue(value) ? `${key}${sep11}${quote}${R("SECRET")}${quote}` : null
|
|
238
152
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
153
|
+
];
|
|
154
|
+
function scrubSecrets(input) {
|
|
155
|
+
if (!input) return { text: input, count: 0 };
|
|
156
|
+
let text3 = input;
|
|
157
|
+
let count = 0;
|
|
158
|
+
for (const rule of RULES) {
|
|
159
|
+
text3 = text3.replace(rule.re, (...args) => {
|
|
160
|
+
const match = args[0];
|
|
161
|
+
const groups = args.slice(1, -2).map((g) => typeof g === "string" ? g : "");
|
|
162
|
+
if (!rule.replace) {
|
|
163
|
+
count++;
|
|
164
|
+
return R(rule.kind);
|
|
165
|
+
}
|
|
166
|
+
const out2 = rule.replace(match, ...groups);
|
|
167
|
+
if (out2 === null) return match;
|
|
168
|
+
count++;
|
|
169
|
+
return out2;
|
|
170
|
+
});
|
|
247
171
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
172
|
+
return { text: text3, count };
|
|
173
|
+
}
|
|
174
|
+
function countSecrets(input) {
|
|
175
|
+
return scrubSecrets(input).count;
|
|
176
|
+
}
|
|
253
177
|
|
|
254
|
-
// src/application/services/workspaceSandbox/
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const wait = Math.max(0, Math.min(25, Math.floor(waitSeconds)));
|
|
274
|
-
const body = await this.get(`/commands?wait=${wait}`, (wait + 15) * 1e3, signal);
|
|
275
|
-
return Array.isArray(body?.commands) ? body.commands : [];
|
|
276
|
-
}
|
|
277
|
-
async postEvents(events) {
|
|
278
|
-
if (events.length === 0) return;
|
|
279
|
-
await this.post("/events", { events }, this.opts.requestTimeoutMs ?? 3e4);
|
|
280
|
-
}
|
|
281
|
-
callTool(name, args, approvalId) {
|
|
282
|
-
if (!/^[a-z][a-z0-9_]{0,63}$/.test(name)) throw new TransportError("invalid tool name", null, false);
|
|
283
|
-
return this.post(`/tools/${name}`, approvalId ? { args, approvalId } : { args }, this.opts.toolTimeoutMs ?? 12e4);
|
|
178
|
+
// src/application/services/workspaceSandbox/importers.ts
|
|
179
|
+
function defaultImportSources(home, env = process.env) {
|
|
180
|
+
return { claudeDir: env.CLAUDE_CONFIG_DIR || (0, import_path.join)(home, ".claude"), codexDir: env.CODEX_HOME || (0, import_path.join)(home, ".codex") };
|
|
181
|
+
}
|
|
182
|
+
var MAX_LINE = 8 * 1024 * 1024;
|
|
183
|
+
var MAX_FILES = IMPORT_LIMITS.maxScanItems;
|
|
184
|
+
var READ_WINDOW_BYTES = 4 * IMPORT_LIMITS.maxBytes;
|
|
185
|
+
var WRAPPER = /^\s*<(command-[a-z-]+|local-command-[a-z-]+|bash-(?:input|stdout|stderr)|task-notification|system-reminder|user-prompt-submit-hook|environment_context|user_instructions|permissions instructions|recommended_plugins|app-context|skills_instructions|collaboration_mode|multi_agent_[a-z_]+|turn_aborted|user_shell_command)[^>]*>/i;
|
|
186
|
+
var oneLine = (s, max) => {
|
|
187
|
+
const t = s.replace(/\s+/g, " ").trim();
|
|
188
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
189
|
+
};
|
|
190
|
+
function summarizeToolInput(name, input) {
|
|
191
|
+
if (typeof input === "string") {
|
|
192
|
+
try {
|
|
193
|
+
return summarizeToolInput(name, JSON.parse(input));
|
|
194
|
+
} catch {
|
|
195
|
+
return oneLine(input, IMPORT_LIMITS.maxToolSummary);
|
|
196
|
+
}
|
|
284
197
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
198
|
+
if (!input || typeof input !== "object") return "";
|
|
199
|
+
const o = input;
|
|
200
|
+
for (const key of ["command", "cmd", "file_path", "path", "notebook_path", "pattern", "query", "url", "description", "prompt"]) {
|
|
201
|
+
const v = o[key];
|
|
202
|
+
if (typeof v === "string" && v.trim()) return oneLine(v, IMPORT_LIMITS.maxToolSummary);
|
|
203
|
+
if (Array.isArray(v) && v.every((x) => typeof x === "string")) return oneLine(v.join(" "), IMPORT_LIMITS.maxToolSummary);
|
|
288
204
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
token: text3(raw?.token),
|
|
306
|
-
provider: text3(raw?.provider),
|
|
307
|
-
repoFullName: text3(raw?.repoFullName, repoFullName),
|
|
308
|
-
defaultBranch,
|
|
309
|
-
branch: typeof raw?.branch === "string" ? raw.branch : defaultBranch
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
async importedHistory() {
|
|
313
|
-
const raw = await this.get("/imported", this.opts.requestTimeoutMs ?? 6e4);
|
|
314
|
-
const messages = Array.isArray(raw?.messages) ? raw.messages : [];
|
|
315
|
-
return {
|
|
316
|
-
messages: messages.filter((m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "assistant")).map((m) => ({
|
|
317
|
-
role: m.role,
|
|
318
|
-
text: typeof m.text === "string" ? m.text : "",
|
|
319
|
-
at: typeof m.at === "string" ? m.at : null,
|
|
320
|
-
...Array.isArray(m.tools) ? { tools: m.tools.filter((t) => !!t && typeof t.name === "string" && typeof t.summary === "string") } : {}
|
|
321
|
-
}))
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
async saveTranscript(t) {
|
|
325
|
-
await this.post("/transcript", t, this.opts.requestTimeoutMs ?? 6e4);
|
|
205
|
+
const first = Object.values(o).find((v) => typeof v === "string" && v.trim());
|
|
206
|
+
return first ? oneLine(first, IMPORT_LIMITS.maxToolSummary) : "";
|
|
207
|
+
}
|
|
208
|
+
async function* lines(file) {
|
|
209
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs.createReadStream)(file, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
210
|
+
try {
|
|
211
|
+
for await (const line of rl) {
|
|
212
|
+
if (!line || line.length > MAX_LINE || line[0] !== "{") continue;
|
|
213
|
+
try {
|
|
214
|
+
const rec = JSON.parse(line);
|
|
215
|
+
if (rec && typeof rec === "object") yield rec;
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
rl.close();
|
|
326
221
|
}
|
|
327
|
-
|
|
328
|
-
|
|
222
|
+
}
|
|
223
|
+
var MessageWindow = class {
|
|
224
|
+
messages = [];
|
|
225
|
+
bytes = 0;
|
|
226
|
+
total = 0;
|
|
227
|
+
push(m) {
|
|
228
|
+
this.messages.push(m);
|
|
229
|
+
this.total++;
|
|
230
|
+
this.bytes += importBytes([m]);
|
|
231
|
+
while (this.bytes > READ_WINDOW_BYTES && this.messages.length > 1) this.bytes -= importBytes([this.messages.shift()]);
|
|
329
232
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
await this.post(`/replies/${commandId}`, reply, this.opts.requestTimeoutMs ?? 3e4);
|
|
233
|
+
get last() {
|
|
234
|
+
return this.messages[this.messages.length - 1];
|
|
333
235
|
}
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
236
|
+
grow(m, text3, tools) {
|
|
237
|
+
const before = importBytes([m]);
|
|
238
|
+
if (text3) m.text = m.text ? `${m.text}
|
|
239
|
+
|
|
240
|
+
${text3}` : text3;
|
|
241
|
+
if (tools.length) m.tools = [...m.tools ?? [], ...tools].slice(0, IMPORT_LIMITS.maxToolsPerMessage);
|
|
242
|
+
this.bytes += importBytes([m]) - before;
|
|
338
243
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
244
|
+
};
|
|
245
|
+
function claudeText(content, role) {
|
|
246
|
+
if (typeof content === "string") return { text: role === "user" && WRAPPER.test(content) ? "" : content, tools: [] };
|
|
247
|
+
if (!Array.isArray(content)) return { text: "", tools: [] };
|
|
248
|
+
const texts = [];
|
|
249
|
+
const tools = [];
|
|
250
|
+
for (const b of content) {
|
|
251
|
+
if (b?.type === "text" && typeof b.text === "string" && !(role === "user" && WRAPPER.test(b.text))) texts.push(b.text);
|
|
252
|
+
else if (b?.type === "tool_use" && typeof b.name === "string") tools.push({ name: b.name.slice(0, 200), summary: summarizeToolInput(b.name, b.input) });
|
|
253
|
+
else if (b?.type === "image") texts.push("[image]");
|
|
346
254
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
255
|
+
return { text: texts.join("\n\n").trim(), tools };
|
|
256
|
+
}
|
|
257
|
+
async function parseClaudeCodeFile(file, opts = {}) {
|
|
258
|
+
const externalId = (0, import_path.basename)(file, ".jsonl");
|
|
259
|
+
if (!isExternalId(externalId)) return null;
|
|
260
|
+
const win = new MessageWindow();
|
|
261
|
+
let folder = null;
|
|
262
|
+
let customTitle = null;
|
|
263
|
+
let aiTitle = null;
|
|
264
|
+
let summary = null;
|
|
265
|
+
let firstUser = null;
|
|
266
|
+
let current = null;
|
|
267
|
+
for await (const r of lines(file)) {
|
|
268
|
+
if (opts.probe && win.total) break;
|
|
269
|
+
if (!folder && typeof r.cwd === "string") folder = r.cwd;
|
|
270
|
+
if (r.type === "custom-title" && typeof r.customTitle === "string") customTitle = r.customTitle;
|
|
271
|
+
else if (r.type === "ai-title" && typeof r.aiTitle === "string") aiTitle = r.aiTitle;
|
|
272
|
+
else if (r.type === "summary" && typeof r.summary === "string") summary = r.summary;
|
|
273
|
+
if (r.type !== "user" && r.type !== "assistant" || r.isSidechain === true || r.isMeta === true) continue;
|
|
274
|
+
const role = r.type;
|
|
275
|
+
const { text: text3, tools } = claudeText(r.message?.content, role);
|
|
276
|
+
const at = typeof r.timestamp === "string" ? r.timestamp : null;
|
|
277
|
+
if (role === "assistant") {
|
|
278
|
+
const id = typeof r.message?.id === "string" ? r.message.id : null;
|
|
279
|
+
if (!current || !id || current.id !== id) current = { id, msg: null, at };
|
|
280
|
+
if (!text3 && !tools.length) continue;
|
|
281
|
+
if (current.msg && win.last === current.msg) win.grow(current.msg, text3, tools);
|
|
282
|
+
else {
|
|
283
|
+
current.msg = { role, text: text3, at: current.at ?? at, ...tools.length ? { tools } : {} };
|
|
284
|
+
win.push(current.msg);
|
|
360
285
|
}
|
|
286
|
+
continue;
|
|
361
287
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
288
|
+
const onlyToolResults = Array.isArray(r.message?.content) && r.message.content.every((b) => b?.type === "tool_result");
|
|
289
|
+
if (!onlyToolResults) current = null;
|
|
290
|
+
if (!text3) continue;
|
|
291
|
+
const shown = r.isCompactSummary === true ? `[Summary of the earlier conversation]
|
|
292
|
+
${text3}` : text3;
|
|
293
|
+
if (!firstUser && r.isCompactSummary !== true) firstUser = text3;
|
|
294
|
+
win.push({ role, text: shown, at });
|
|
366
295
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
throw new TransportError(`${method} ${routeLabel(path)} failed: ${e.name}`, null, true);
|
|
380
|
-
}
|
|
381
|
-
if (this.gone.has(res.status)) {
|
|
382
|
-
await res.body?.cancel().catch(() => void 0);
|
|
383
|
-
throw new SessionGoneError(res.status);
|
|
384
|
-
}
|
|
385
|
-
if (!res.ok) {
|
|
386
|
-
const code = await res.text().then((t) => JSON.parse(t)?.code, () => void 0).catch(() => void 0);
|
|
387
|
-
const retryable = res.status === 429 || res.status >= 500;
|
|
388
|
-
const error = new TransportError(`${method} ${routeLabel(path)} answered ${res.status}`, res.status, retryable);
|
|
389
|
-
if (typeof code === "string" && /^[A-Z_]{3,80}$/.test(code)) error.code = code;
|
|
390
|
-
throw error;
|
|
391
|
-
}
|
|
392
|
-
if (res.status === 204) return void 0;
|
|
393
|
-
const text3 = await res.text();
|
|
394
|
-
if (!text3) return void 0;
|
|
395
|
-
try {
|
|
396
|
-
return JSON.parse(text3);
|
|
397
|
-
} catch {
|
|
398
|
-
throw new TransportError(`${method} ${routeLabel(path)} returned invalid JSON`, res.status, false);
|
|
399
|
-
}
|
|
296
|
+
if (!win.total) return null;
|
|
297
|
+
const title = oneLine(customTitle || aiTitle || summary || firstUser || "Untitled conversation", IMPORT_LIMITS.maxTitle);
|
|
298
|
+
const st = await (0, import_promises.lstat)(file).catch(() => null);
|
|
299
|
+
return { source: "CLAUDE_CODE", externalId, title, folder, messages: win.messages, updatedAt: st ? st.mtime.toISOString() : null };
|
|
300
|
+
}
|
|
301
|
+
async function regularFiles(dir, match) {
|
|
302
|
+
const out2 = [];
|
|
303
|
+
const names = await (0, import_promises.readdir)(dir).catch(() => []);
|
|
304
|
+
for (const name of names) {
|
|
305
|
+
if (!match(name)) continue;
|
|
306
|
+
const st = await (0, import_promises.lstat)((0, import_path.join)(dir, name)).catch(() => null);
|
|
307
|
+
if (st?.isFile()) out2.push({ path: (0, import_path.join)(dir, name), mtime: st.mtimeMs });
|
|
400
308
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
ac.abort(s.reason);
|
|
409
|
-
return ac.signal;
|
|
410
|
-
}
|
|
411
|
-
s.addEventListener("abort", () => ac.abort(s.reason), { once: true });
|
|
309
|
+
return out2;
|
|
310
|
+
}
|
|
311
|
+
async function subdirs(dir) {
|
|
312
|
+
const out2 = [];
|
|
313
|
+
for (const name of await (0, import_promises.readdir)(dir).catch(() => [])) {
|
|
314
|
+
const st = await (0, import_promises.lstat)((0, import_path.join)(dir, name)).catch(() => null);
|
|
315
|
+
if (st?.isDirectory()) out2.push((0, import_path.join)(dir, name));
|
|
412
316
|
}
|
|
413
|
-
return
|
|
317
|
+
return out2;
|
|
414
318
|
}
|
|
415
|
-
function
|
|
416
|
-
|
|
319
|
+
async function claudeCodeFiles(claudeDir) {
|
|
320
|
+
const files = [];
|
|
321
|
+
for (const project of await subdirs((0, import_path.join)(claudeDir, "projects"))) files.push(...await regularFiles(project, (n) => n.endsWith(".jsonl")));
|
|
322
|
+
return files;
|
|
417
323
|
}
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
resolve8();
|
|
424
|
-
}, { once: true });
|
|
425
|
-
});
|
|
324
|
+
function codexText(content, role) {
|
|
325
|
+
if (typeof content === "string") return content;
|
|
326
|
+
if (!Array.isArray(content)) return "";
|
|
327
|
+
const want = role === "user" ? "input_text" : "output_text";
|
|
328
|
+
return content.filter((b) => b?.type === want && typeof b.text === "string" && !(role === "user" && WRAPPER.test(b.text))).map((b) => b.text).join("\n\n").trim();
|
|
426
329
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
imported: importedInfo(session.imported),
|
|
471
|
-
checkpointPatch: typeof raw?.checkpointPatch === "string" ? raw.checkpointPatch : null,
|
|
472
|
-
sdkSessionId: typeof session.sdkSessionId === "string" ? session.sdkSessionId : null,
|
|
473
|
-
workspaceKind: session.workspaceKind === "LOCAL" ? "LOCAL" : "CLOUD",
|
|
474
|
-
projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null,
|
|
475
|
-
folderLink: typeof session.folderLink?.projectId === "string" && session.folderLink.projectId ? { projectId: session.folderLink.projectId } : null,
|
|
476
|
-
transcript: transcriptOf(raw?.transcript),
|
|
477
|
-
baseMeasurements: raw?.baseMeasurements && typeof raw.baseMeasurements === "object" && !Array.isArray(raw.baseMeasurements) ? raw.baseMeasurements : {},
|
|
478
|
-
pullRequests: pullRequestsOf(raw?.pullRequests),
|
|
479
|
-
skills: skillsOf(raw?.skills),
|
|
480
|
-
plugins: pluginsOf(raw?.plugins),
|
|
481
|
-
environments: environmentsOf(raw?.environments)
|
|
330
|
+
async function parseCodexFile(file, opts = {}) {
|
|
331
|
+
const fromName = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(file)?.[1] ?? null;
|
|
332
|
+
let externalId = null;
|
|
333
|
+
let folder = null;
|
|
334
|
+
let firstUser = null;
|
|
335
|
+
const win = new MessageWindow();
|
|
336
|
+
for await (const r of lines(file)) {
|
|
337
|
+
if (opts.probe && win.total && externalId) break;
|
|
338
|
+
const p = r.payload;
|
|
339
|
+
if (!p || typeof p !== "object") continue;
|
|
340
|
+
if (r.type === "session_meta") {
|
|
341
|
+
if (p.source && typeof p.source === "object" && p.source.subagent) return null;
|
|
342
|
+
if (!externalId && typeof p.id === "string") externalId = p.id;
|
|
343
|
+
if (!folder && typeof p.cwd === "string") folder = p.cwd;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (r.type !== "response_item") continue;
|
|
347
|
+
const at = typeof r.timestamp === "string" ? r.timestamp : null;
|
|
348
|
+
if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
|
|
349
|
+
const text3 = codexText(p.content, p.role);
|
|
350
|
+
if (!text3) continue;
|
|
351
|
+
if (p.role === "user" && !firstUser) firstUser = text3;
|
|
352
|
+
const last = win.last;
|
|
353
|
+
if (p.role === "assistant" && last?.role === "assistant" && !last.text) win.grow(last, text3, []);
|
|
354
|
+
else win.push({ role: p.role, text: text3, at });
|
|
355
|
+
} else if ((p.type === "function_call" || p.type === "custom_tool_call" || p.type === "local_shell_call") && (typeof p.name === "string" || p.type === "local_shell_call")) {
|
|
356
|
+
const name = typeof p.name === "string" ? p.name : "shell";
|
|
357
|
+
const tool = { name: name.slice(0, 200), summary: summarizeToolInput(name, p.arguments ?? p.input ?? p.action) };
|
|
358
|
+
const last = win.last;
|
|
359
|
+
if (last?.role === "assistant") win.grow(last, "", [tool]);
|
|
360
|
+
else win.push({ role: "assistant", text: "", at, tools: [tool] });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const id = externalId ?? fromName;
|
|
364
|
+
if (!id || !isExternalId(id) || !win.total) return null;
|
|
365
|
+
const st = await (0, import_promises.lstat)(file).catch(() => null);
|
|
366
|
+
return {
|
|
367
|
+
source: "CODEX",
|
|
368
|
+
externalId: id,
|
|
369
|
+
title: oneLine(firstUser || "Untitled conversation", IMPORT_LIMITS.maxTitle),
|
|
370
|
+
folder,
|
|
371
|
+
messages: win.messages,
|
|
372
|
+
updatedAt: st ? st.mtime.toISOString() : null
|
|
482
373
|
};
|
|
483
374
|
}
|
|
484
|
-
function
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
375
|
+
async function codexFiles(codexDir) {
|
|
376
|
+
const files = [];
|
|
377
|
+
const walk = async (dir, depth) => {
|
|
378
|
+
files.push(...await regularFiles(dir, (n) => n.startsWith("rollout-") && n.endsWith(".jsonl")));
|
|
379
|
+
if (depth < 4) for (const sub of await subdirs(dir)) await walk(sub, depth + 1);
|
|
380
|
+
};
|
|
381
|
+
await walk((0, import_path.join)(codexDir, "sessions"), 0);
|
|
382
|
+
return files;
|
|
488
383
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
384
|
+
function secretsIn(c) {
|
|
385
|
+
let n = 0;
|
|
386
|
+
for (const m of c.messages) {
|
|
387
|
+
n += countSecrets(m.text);
|
|
388
|
+
for (const t of m.tools ?? []) n += countSecrets(t.summary);
|
|
389
|
+
}
|
|
390
|
+
return n;
|
|
493
391
|
}
|
|
494
|
-
function
|
|
495
|
-
|
|
496
|
-
|
|
392
|
+
async function importFiles(sources) {
|
|
393
|
+
return [
|
|
394
|
+
...(await claudeCodeFiles(sources.claudeDir)).map((f) => ({ ...f, source: "CLAUDE_CODE" })),
|
|
395
|
+
...(await codexFiles(sources.codexDir)).map((f) => ({ ...f, source: "CODEX" }))
|
|
396
|
+
].sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES);
|
|
497
397
|
}
|
|
498
|
-
function
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
398
|
+
async function scanImports(sources) {
|
|
399
|
+
const files = await importFiles(sources);
|
|
400
|
+
const items = [];
|
|
401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
402
|
+
for (const f of files) {
|
|
403
|
+
const c = await (f.source === "CLAUDE_CODE" ? parseClaudeCodeFile(f.path) : parseCodexFile(f.path)).catch(() => null);
|
|
404
|
+
if (!c || seen.has(`${c.source}:${c.externalId}`)) continue;
|
|
405
|
+
seen.add(`${c.source}:${c.externalId}`);
|
|
406
|
+
items.push({
|
|
407
|
+
source: c.source,
|
|
408
|
+
externalId: c.externalId,
|
|
409
|
+
title: oneLine(scrubSecrets(c.title).text, IMPORT_LIMITS.maxTitle),
|
|
410
|
+
folder: c.folder,
|
|
411
|
+
messageCount: Math.min(c.messages.length, IMPORT_LIMITS.maxMessages),
|
|
412
|
+
updatedAt: c.updatedAt,
|
|
413
|
+
secretsFound: secretsIn(c)
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return items;
|
|
506
417
|
}
|
|
507
|
-
function
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
for (const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
files: Array.isArray(v.files) ? v.files.filter((f) => typeof f === "string") : []
|
|
517
|
-
};
|
|
418
|
+
async function countImports(sources) {
|
|
419
|
+
const counts = { claudeCode: 0, codex: 0 };
|
|
420
|
+
const seen = /* @__PURE__ */ new Set();
|
|
421
|
+
for (const f of await importFiles(sources)) {
|
|
422
|
+
const c = await (f.source === "CLAUDE_CODE" ? parseClaudeCodeFile(f.path, { probe: true }) : parseCodexFile(f.path, { probe: true })).catch(() => null);
|
|
423
|
+
if (!c || seen.has(`${c.source}:${c.externalId}`)) continue;
|
|
424
|
+
seen.add(`${c.source}:${c.externalId}`);
|
|
425
|
+
if (c.source === "CLAUDE_CODE") counts.claudeCode++;
|
|
426
|
+
else counts.codex++;
|
|
518
427
|
}
|
|
519
|
-
return
|
|
428
|
+
return counts;
|
|
520
429
|
}
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
source:
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
430
|
+
async function conversationFolders(sources, deadline) {
|
|
431
|
+
const claude = [];
|
|
432
|
+
for (const project of await subdirs((0, import_path.join)(sources.claudeDir, "projects"))) {
|
|
433
|
+
if (Date.now() > deadline) break;
|
|
434
|
+
const newest = (await regularFiles(project, (n) => n.endsWith(".jsonl"))).sort((a, b) => b.mtime - a.mtime)[0];
|
|
435
|
+
if (newest) claude.push({ ...newest, source: "CLAUDE_CODE" });
|
|
436
|
+
}
|
|
437
|
+
const codex = Date.now() > deadline ? [] : (await codexFiles(sources.codexDir)).map((f) => ({ ...f, source: "CODEX" }));
|
|
438
|
+
const files = [...claude, ...codex].sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES);
|
|
439
|
+
const out2 = [];
|
|
440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
441
|
+
for (const f of files) {
|
|
442
|
+
if (Date.now() > deadline) break;
|
|
443
|
+
let n = 0;
|
|
444
|
+
try {
|
|
445
|
+
for await (const r of lines(f.path)) {
|
|
446
|
+
if (++n > 50) break;
|
|
447
|
+
const cwd = f.source === "CLAUDE_CODE" ? r.cwd : r.type === "session_meta" && r.payload && typeof r.payload === "object" && !(r.payload.source && typeof r.payload.source === "object" && r.payload.source.subagent) ? r.payload.cwd : void 0;
|
|
448
|
+
if (typeof cwd === "string" && cwd) {
|
|
449
|
+
if (!seen.has(cwd)) {
|
|
450
|
+
seen.add(cwd);
|
|
451
|
+
out2.push(cwd);
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return out2;
|
|
533
460
|
}
|
|
534
|
-
function
|
|
535
|
-
if (!
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
461
|
+
async function findConversation(sources, source, externalId) {
|
|
462
|
+
if (!isExternalId(externalId)) return null;
|
|
463
|
+
if (source === "CLAUDE_CODE") {
|
|
464
|
+
const file = (await claudeCodeFiles(sources.claudeDir)).find((f) => (0, import_path.basename)(f.path, ".jsonl") === externalId);
|
|
465
|
+
return file ? parseClaudeCodeFile(file.path) : null;
|
|
466
|
+
}
|
|
467
|
+
const files = (await codexFiles(sources.codexDir)).filter((f) => f.path.includes(externalId)).sort((a, b) => b.mtime - a.mtime);
|
|
468
|
+
for (const f of files) {
|
|
469
|
+
const c = await parseCodexFile(f.path).catch(() => null);
|
|
470
|
+
if (c?.externalId === externalId) return c;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
542
473
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
474
|
+
function prepareImport(c) {
|
|
475
|
+
let removed = 0;
|
|
476
|
+
const scrub = (s) => {
|
|
477
|
+
const r = scrubSecrets(s);
|
|
478
|
+
removed += r.count;
|
|
479
|
+
return r.text;
|
|
480
|
+
};
|
|
481
|
+
const cap = (s, max) => Buffer.byteLength(s) > max ? `${Buffer.from(s).subarray(0, max - 32).toString("utf8").replace(/�+$/, "")}
|
|
482
|
+
[... cut ...]` : s;
|
|
483
|
+
const messages = c.messages.map((m) => ({
|
|
484
|
+
role: m.role,
|
|
485
|
+
text: cap(scrub(m.text), IMPORT_LIMITS.maxMessageBytes),
|
|
486
|
+
at: m.at,
|
|
487
|
+
...m.tools?.length ? { tools: m.tools.slice(0, IMPORT_LIMITS.maxToolsPerMessage).map((t) => ({ name: t.name, summary: oneLine(scrub(t.summary), IMPORT_LIMITS.maxToolSummary) })) } : {}
|
|
555
488
|
}));
|
|
556
|
-
const
|
|
557
|
-
|
|
489
|
+
const title = oneLine(scrubSecrets(c.title).text, IMPORT_LIMITS.maxTitle) || "Untitled conversation";
|
|
490
|
+
let start = Math.max(0, messages.length - IMPORT_LIMITS.maxMessages);
|
|
491
|
+
let bytes = importBytes(messages.slice(start));
|
|
492
|
+
while (bytes > IMPORT_LIMITS.maxBytes && start < messages.length - 1) bytes -= importBytes([messages[start++]]);
|
|
493
|
+
return { source: c.source, externalId: c.externalId, folder: c.folder, title, secretsRemoved: removed, messages: messages.slice(start), omitted: start };
|
|
558
494
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
{ kind: "SLACK_TOKEN", re: /\bxox[abposre]-[A-Za-z0-9-]{10,}/g },
|
|
587
|
-
{ kind: "SLACK_WEBHOOK", re: /https:\/\/hooks\.slack\.com\/(?:services|workflows|triggers)\/[A-Za-z0-9_/-]{20,}/g },
|
|
588
|
-
{ kind: "STRIPE_KEY", re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
|
|
589
|
-
{ kind: "STRIPE_WEBHOOK_SECRET", re: /\bwhsec_[A-Za-z0-9]{24,}\b/g },
|
|
590
|
-
{ kind: "GOOGLE_API_KEY", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
591
|
-
{ kind: "JWT", re: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g },
|
|
592
|
-
{ kind: "BEARER_TOKEN", re: /\b(Bearer\s+)([A-Za-z0-9._~+/=-]{20,})/g, replace: (_m, p) => `${p}${R("BEARER_TOKEN")}` },
|
|
593
|
-
{
|
|
594
|
-
kind: "ASSIGNED_SECRET",
|
|
595
|
-
// password=..., "api_key": "...", SECRET_TOKEN: ..., --token ... (key names that say "secret").
|
|
596
|
-
re: /\b([A-Za-z0-9_.-]*(?:passw(?:or)?d|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|credential|auth[_-]?key)[A-Za-z0-9_-]*)(["']?\s*(?:=|:|\s)\s*)(["']?)([^\s"'`,;)}\]]{8,500})\3/gi,
|
|
597
|
-
replace: (_m, key, sep11, quote, value) => looksLikeSecretValue(value) ? `${key}${sep11}${quote}${R("SECRET")}${quote}` : null
|
|
598
|
-
}
|
|
599
|
-
];
|
|
600
|
-
function scrubSecrets(input) {
|
|
601
|
-
if (!input) return { text: input, count: 0 };
|
|
602
|
-
let text3 = input;
|
|
603
|
-
let count = 0;
|
|
604
|
-
for (const rule of RULES) {
|
|
605
|
-
text3 = text3.replace(rule.re, (...args) => {
|
|
606
|
-
const match = args[0];
|
|
607
|
-
const groups = args.slice(1, -2).map((g) => typeof g === "string" ? g : "");
|
|
608
|
-
if (!rule.replace) {
|
|
609
|
-
count++;
|
|
610
|
-
return R(rule.kind);
|
|
495
|
+
function scrubRecord(record) {
|
|
496
|
+
let removed = 0;
|
|
497
|
+
const walk = (value, key) => {
|
|
498
|
+
if (typeof value === "string") {
|
|
499
|
+
if (key === "signature" || key === "data") return value;
|
|
500
|
+
const r = scrubSecrets(value);
|
|
501
|
+
removed += r.count;
|
|
502
|
+
return r.text;
|
|
503
|
+
}
|
|
504
|
+
if (Array.isArray(value)) {
|
|
505
|
+
const out2 = [];
|
|
506
|
+
for (const item of value) {
|
|
507
|
+
const block = item;
|
|
508
|
+
if (block && typeof block === "object" && block.type === "thinking" && typeof block.thinking === "string") {
|
|
509
|
+
const r = scrubSecrets(block.thinking);
|
|
510
|
+
if (r.count) {
|
|
511
|
+
removed += r.count;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
out2.push(item);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (block && typeof block === "object" && block.type === "redacted_thinking") {
|
|
518
|
+
out2.push(item);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
out2.push(walk(item));
|
|
611
522
|
}
|
|
612
|
-
const out2 = rule.replace(match, ...groups);
|
|
613
|
-
if (out2 === null) return match;
|
|
614
|
-
count++;
|
|
615
523
|
return out2;
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
return { text: text3, count };
|
|
619
|
-
}
|
|
620
|
-
function countSecrets(input) {
|
|
621
|
-
return scrubSecrets(input).count;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// src/application/services/workspaceSandbox/importers.ts
|
|
625
|
-
function defaultImportSources(home, env = process.env) {
|
|
626
|
-
return { claudeDir: env.CLAUDE_CONFIG_DIR || (0, import_path.join)(home, ".claude"), codexDir: env.CODEX_HOME || (0, import_path.join)(home, ".codex") };
|
|
627
|
-
}
|
|
628
|
-
var MAX_LINE = 8 * 1024 * 1024;
|
|
629
|
-
var MAX_FILES = IMPORT_LIMITS.maxScanItems;
|
|
630
|
-
var READ_WINDOW_BYTES = 4 * IMPORT_LIMITS.maxBytes;
|
|
631
|
-
var WRAPPER = /^\s*<(command-[a-z-]+|local-command-[a-z-]+|bash-(?:input|stdout|stderr)|task-notification|system-reminder|user-prompt-submit-hook|environment_context|user_instructions|permissions instructions|recommended_plugins|app-context|skills_instructions|collaboration_mode|multi_agent_[a-z_]+|turn_aborted|user_shell_command)[^>]*>/i;
|
|
632
|
-
var oneLine = (s, max) => {
|
|
633
|
-
const t = s.replace(/\s+/g, " ").trim();
|
|
634
|
-
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
635
|
-
};
|
|
636
|
-
function summarizeToolInput(name, input) {
|
|
637
|
-
if (typeof input === "string") {
|
|
638
|
-
try {
|
|
639
|
-
return summarizeToolInput(name, JSON.parse(input));
|
|
640
|
-
} catch {
|
|
641
|
-
return oneLine(input, IMPORT_LIMITS.maxToolSummary);
|
|
642
524
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
return first ? oneLine(first, IMPORT_LIMITS.maxToolSummary) : "";
|
|
525
|
+
if (value && typeof value === "object") {
|
|
526
|
+
const o = value;
|
|
527
|
+
for (const k of Object.keys(o)) o[k] = walk(o[k], k);
|
|
528
|
+
return o;
|
|
529
|
+
}
|
|
530
|
+
return value;
|
|
531
|
+
};
|
|
532
|
+
walk(record);
|
|
533
|
+
return removed;
|
|
653
534
|
}
|
|
654
|
-
async function
|
|
655
|
-
const
|
|
535
|
+
async function writeScrubbedTranscript(source, target) {
|
|
536
|
+
const out2 = (0, import_fs.createWriteStream)(target, { mode: 384, flags: "wx" });
|
|
537
|
+
let removed = 0;
|
|
538
|
+
const done = new Promise((resolve8, reject) => {
|
|
539
|
+
out2.on("finish", resolve8);
|
|
540
|
+
out2.on("error", reject);
|
|
541
|
+
});
|
|
542
|
+
const rl = (0, import_readline.createInterface)({ input: (0, import_fs.createReadStream)(source, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
656
543
|
try {
|
|
657
544
|
for await (const line of rl) {
|
|
658
|
-
if (!line
|
|
545
|
+
if (!line) continue;
|
|
546
|
+
let record;
|
|
659
547
|
try {
|
|
660
|
-
|
|
661
|
-
if (rec && typeof rec === "object") yield rec;
|
|
548
|
+
record = JSON.parse(line);
|
|
662
549
|
} catch {
|
|
550
|
+
continue;
|
|
663
551
|
}
|
|
552
|
+
removed += scrubRecord(record);
|
|
553
|
+
if (!out2.write(`${JSON.stringify(record)}
|
|
554
|
+
`)) await new Promise((r) => out2.once("drain", () => r()));
|
|
664
555
|
}
|
|
665
556
|
} finally {
|
|
666
557
|
rl.close();
|
|
558
|
+
out2.end();
|
|
667
559
|
}
|
|
560
|
+
await done;
|
|
561
|
+
return removed;
|
|
668
562
|
}
|
|
669
|
-
var MessageWindow = class {
|
|
670
|
-
messages = [];
|
|
671
|
-
bytes = 0;
|
|
672
|
-
total = 0;
|
|
673
|
-
push(m) {
|
|
674
|
-
this.messages.push(m);
|
|
675
|
-
this.total++;
|
|
676
|
-
this.bytes += importBytes([m]);
|
|
677
|
-
while (this.bytes > READ_WINDOW_BYTES && this.messages.length > 1) this.bytes -= importBytes([this.messages.shift()]);
|
|
678
|
-
}
|
|
679
|
-
get last() {
|
|
680
|
-
return this.messages[this.messages.length - 1];
|
|
681
|
-
}
|
|
682
|
-
grow(m, text3, tools) {
|
|
683
|
-
const before = importBytes([m]);
|
|
684
|
-
if (text3) m.text = m.text ? `${m.text}
|
|
685
563
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
for (const b of content) {
|
|
697
|
-
if (b?.type === "text" && typeof b.text === "string" && !(role === "user" && WRAPPER.test(b.text))) texts.push(b.text);
|
|
698
|
-
else if (b?.type === "tool_use" && typeof b.name === "string") tools.push({ name: b.name.slice(0, 200), summary: summarizeToolInput(b.name, b.input) });
|
|
699
|
-
else if (b?.type === "image") texts.push("[image]");
|
|
700
|
-
}
|
|
701
|
-
return { text: texts.join("\n\n").trim(), tools };
|
|
564
|
+
// src/application/services/workspaceSandbox/backgroundService.ts
|
|
565
|
+
var import_child_process2 = require("child_process");
|
|
566
|
+
var import_crypto3 = require("crypto");
|
|
567
|
+
var import_fs5 = require("fs");
|
|
568
|
+
var import_path6 = require("path");
|
|
569
|
+
|
|
570
|
+
// src/application/services/workspaceSandbox/reasoning.ts
|
|
571
|
+
var REASONING_LEVELS = ["off", "low", "medium", "high", "xhigh", "max"];
|
|
572
|
+
function isReasoningLevel(value) {
|
|
573
|
+
return typeof value === "string" && REASONING_LEVELS.includes(value);
|
|
702
574
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
let current = null;
|
|
713
|
-
for await (const r of lines(file)) {
|
|
714
|
-
if (opts.probe && win.total) break;
|
|
715
|
-
if (!folder && typeof r.cwd === "string") folder = r.cwd;
|
|
716
|
-
if (r.type === "custom-title" && typeof r.customTitle === "string") customTitle = r.customTitle;
|
|
717
|
-
else if (r.type === "ai-title" && typeof r.aiTitle === "string") aiTitle = r.aiTitle;
|
|
718
|
-
else if (r.type === "summary" && typeof r.summary === "string") summary = r.summary;
|
|
719
|
-
if (r.type !== "user" && r.type !== "assistant" || r.isSidechain === true || r.isMeta === true) continue;
|
|
720
|
-
const role = r.type;
|
|
721
|
-
const { text: text3, tools } = claudeText(r.message?.content, role);
|
|
722
|
-
const at = typeof r.timestamp === "string" ? r.timestamp : null;
|
|
723
|
-
if (role === "assistant") {
|
|
724
|
-
const id = typeof r.message?.id === "string" ? r.message.id : null;
|
|
725
|
-
if (!current || !id || current.id !== id) current = { id, msg: null, at };
|
|
726
|
-
if (!text3 && !tools.length) continue;
|
|
727
|
-
if (current.msg && win.last === current.msg) win.grow(current.msg, text3, tools);
|
|
728
|
-
else {
|
|
729
|
-
current.msg = { role, text: text3, at: current.at ?? at, ...tools.length ? { tools } : {} };
|
|
730
|
-
win.push(current.msg);
|
|
731
|
-
}
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
const onlyToolResults = Array.isArray(r.message?.content) && r.message.content.every((b) => b?.type === "tool_result");
|
|
735
|
-
if (!onlyToolResults) current = null;
|
|
736
|
-
if (!text3) continue;
|
|
737
|
-
const shown = r.isCompactSummary === true ? `[Summary of the earlier conversation]
|
|
738
|
-
${text3}` : text3;
|
|
739
|
-
if (!firstUser && r.isCompactSummary !== true) firstUser = text3;
|
|
740
|
-
win.push({ role, text: shown, at });
|
|
741
|
-
}
|
|
742
|
-
if (!win.total) return null;
|
|
743
|
-
const title = oneLine(customTitle || aiTitle || summary || firstUser || "Untitled conversation", IMPORT_LIMITS.maxTitle);
|
|
744
|
-
const st = await (0, import_promises.lstat)(file).catch(() => null);
|
|
745
|
-
return { source: "CLAUDE_CODE", externalId, title, folder, messages: win.messages, updatedAt: st ? st.mtime.toISOString() : null };
|
|
575
|
+
function parseReasoningCapability(raw) {
|
|
576
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
577
|
+
const r = raw;
|
|
578
|
+
if (!Array.isArray(r.levels)) return null;
|
|
579
|
+
const listed = [...r.levels, ...r.off === true ? ["off"] : []];
|
|
580
|
+
const levels = REASONING_LEVELS.filter((l) => listed.includes(l));
|
|
581
|
+
if (!levels.length) return null;
|
|
582
|
+
const def = isReasoningLevel(r.default) && levels.includes(r.default) ? r.default : levels[0];
|
|
583
|
+
return { levels, default: def };
|
|
746
584
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (!match(name)) continue;
|
|
752
|
-
const st = await (0, import_promises.lstat)((0, import_path.join)(dir, name)).catch(() => null);
|
|
753
|
-
if (st?.isFile()) out2.push({ path: (0, import_path.join)(dir, name), mtime: st.mtimeMs });
|
|
754
|
-
}
|
|
755
|
-
return out2;
|
|
585
|
+
function effectiveReasoning(requested, capability) {
|
|
586
|
+
if (!capability) return null;
|
|
587
|
+
if (requested && capability.levels.includes(requested)) return requested;
|
|
588
|
+
return capability.default;
|
|
756
589
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const st = await (0, import_promises.lstat)((0, import_path.join)(dir, name)).catch(() => null);
|
|
761
|
-
if (st?.isDirectory()) out2.push((0, import_path.join)(dir, name));
|
|
762
|
-
}
|
|
763
|
-
return out2;
|
|
764
|
-
}
|
|
765
|
-
async function claudeCodeFiles(claudeDir) {
|
|
766
|
-
const files = [];
|
|
767
|
-
for (const project of await subdirs((0, import_path.join)(claudeDir, "projects"))) files.push(...await regularFiles(project, (n) => n.endsWith(".jsonl")));
|
|
768
|
-
return files;
|
|
590
|
+
function isMaxMode(level, capability) {
|
|
591
|
+
if (!level || level === "off" || !capability) return false;
|
|
592
|
+
return capability.levels[capability.levels.length - 1] === level;
|
|
769
593
|
}
|
|
770
|
-
function
|
|
771
|
-
if (
|
|
772
|
-
if (
|
|
773
|
-
|
|
774
|
-
return content.filter((b) => b?.type === want && typeof b.text === "string" && !(role === "user" && WRAPPER.test(b.text))).map((b) => b.text).join("\n\n").trim();
|
|
594
|
+
function turnReasoning(level, capability) {
|
|
595
|
+
if (!capability || !level) return { options: {}, forceNoThinking: true, outputCeiling: false };
|
|
596
|
+
if (level === "off") return { options: { thinking: { type: "disabled" } }, forceNoThinking: true, outputCeiling: false };
|
|
597
|
+
return { options: { effort: level, thinking: { type: "adaptive" } }, forceNoThinking: false, outputCeiling: isMaxMode(level, capability) };
|
|
775
598
|
}
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
const p = r.payload;
|
|
785
|
-
if (!p || typeof p !== "object") continue;
|
|
786
|
-
if (r.type === "session_meta") {
|
|
787
|
-
if (p.source && typeof p.source === "object" && p.source.subagent) return null;
|
|
788
|
-
if (!externalId && typeof p.id === "string") externalId = p.id;
|
|
789
|
-
if (!folder && typeof p.cwd === "string") folder = p.cwd;
|
|
790
|
-
continue;
|
|
791
|
-
}
|
|
792
|
-
if (r.type !== "response_item") continue;
|
|
793
|
-
const at = typeof r.timestamp === "string" ? r.timestamp : null;
|
|
794
|
-
if (p.type === "message" && (p.role === "user" || p.role === "assistant")) {
|
|
795
|
-
const text3 = codexText(p.content, p.role);
|
|
796
|
-
if (!text3) continue;
|
|
797
|
-
if (p.role === "user" && !firstUser) firstUser = text3;
|
|
798
|
-
const last = win.last;
|
|
799
|
-
if (p.role === "assistant" && last?.role === "assistant" && !last.text) win.grow(last, text3, []);
|
|
800
|
-
else win.push({ role: p.role, text: text3, at });
|
|
801
|
-
} else if ((p.type === "function_call" || p.type === "custom_tool_call" || p.type === "local_shell_call") && (typeof p.name === "string" || p.type === "local_shell_call")) {
|
|
802
|
-
const name = typeof p.name === "string" ? p.name : "shell";
|
|
803
|
-
const tool = { name: name.slice(0, 200), summary: summarizeToolInput(name, p.arguments ?? p.input ?? p.action) };
|
|
804
|
-
const last = win.last;
|
|
805
|
-
if (last?.role === "assistant") win.grow(last, "", [tool]);
|
|
806
|
-
else win.push({ role: "assistant", text: "", at, tools: [tool] });
|
|
807
|
-
}
|
|
599
|
+
function engineModelCapabilities(aliases) {
|
|
600
|
+
const entries = ["-mid_conv_system"];
|
|
601
|
+
const seen = /* @__PURE__ */ new Set();
|
|
602
|
+
for (const { alias, reasoning } of aliases) {
|
|
603
|
+
const name = alias.trim();
|
|
604
|
+
if (!name || seen.has(name) || /[;=,]/.test(name)) continue;
|
|
605
|
+
seen.add(name);
|
|
606
|
+
entries.push(reasoning ? `${name}=effort,${reasoning.levels.includes("max") ? "" : "-"}max_effort,${reasoning.levels.includes("xhigh") ? "" : "-"}xhigh_effort` : `${name}=-effort,-max_effort,-xhigh_effort`);
|
|
808
607
|
}
|
|
809
|
-
|
|
810
|
-
if (!id || !isExternalId(id) || !win.total) return null;
|
|
811
|
-
const st = await (0, import_promises.lstat)(file).catch(() => null);
|
|
812
|
-
return {
|
|
813
|
-
source: "CODEX",
|
|
814
|
-
externalId: id,
|
|
815
|
-
title: oneLine(firstUser || "Untitled conversation", IMPORT_LIMITS.maxTitle),
|
|
816
|
-
folder,
|
|
817
|
-
messages: win.messages,
|
|
818
|
-
updatedAt: st ? st.mtime.toISOString() : null
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
async function codexFiles(codexDir) {
|
|
822
|
-
const files = [];
|
|
823
|
-
const walk = async (dir, depth) => {
|
|
824
|
-
files.push(...await regularFiles(dir, (n) => n.startsWith("rollout-") && n.endsWith(".jsonl")));
|
|
825
|
-
if (depth < 4) for (const sub of await subdirs(dir)) await walk(sub, depth + 1);
|
|
826
|
-
};
|
|
827
|
-
await walk((0, import_path.join)(codexDir, "sessions"), 0);
|
|
828
|
-
return files;
|
|
608
|
+
return entries.join(";");
|
|
829
609
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
610
|
+
|
|
611
|
+
// src/application/services/workspaceSandbox/autoRouting.ts
|
|
612
|
+
var LONG_INSTRUCTION_CHARS = 3e3;
|
|
613
|
+
var LONG_CODE_LINES = 60;
|
|
614
|
+
var MANY_FILES = 5;
|
|
615
|
+
var HARD_WORK = [
|
|
616
|
+
[/\b(refactor|refator|refactoriz)\w*\b[\s\S]{0,80}\b(across|between|all|every|entre|todos|todas|varios|varias|modul|servic|packages?|pacotes?|layers?|camadas?)/, "refactor across modules"],
|
|
617
|
+
[/\b(architect|arquitet|arquitect|redesign|re-architect)\w*/, "architecture"],
|
|
618
|
+
[/\b(root cause|causa raiz|causa-raiz|debug|depur|investigat|investig|stack ?trace|flaky|race condition|condicao de corrida|memory leak|vazamento de memoria|deadlock|intermittent|intermitente)\w*/, "debugging, root cause"],
|
|
619
|
+
[/\b(migrat|migra(c|t)a?o|migrar|migre|upgrade\b[\s\S]{0,40}\b(from|to|de|para)\b|port (this|the|it)\b[\s\S]{0,40}\bto\b)/, "migration"],
|
|
620
|
+
[/\b(vulnerab|cve-\d|security (fix|issue|flaw|hole|bug)|injection|injecao|xss|csrf|ssrf|rce\b|seguranca|seguridad|exploit)\w*/, "security fix"],
|
|
621
|
+
[/\b(every|all|each|todos os|todas as|cada|todos los|todas las) (the )?(files?|modules?|services?|endpoints?|packages?|arquivos?|modulos?|servicos?|archivos?)\b|\b(across|throughout) (the )?(whole )?(codebase|repo|repository|project|modules|services)\b|\b(codebase|base de codigo)[- ](wide|inteira|toda)\b/, "many files"],
|
|
622
|
+
[/\b(refactor|refator|refactoriz)\w*/, "refactor"],
|
|
623
|
+
[/\b(step[- ]by[- ]step|passo a passo|paso a paso|multi[- ]step|end[- ]to[- ]end|threat model|performance (issue|regression|problem)|regressao de performance|rewrite|reescrev|reescrib)\w*/, "multi-step work"]
|
|
624
|
+
];
|
|
625
|
+
var normalize = (text3) => text3.toLowerCase().normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
|
|
626
|
+
function namedFiles(text3) {
|
|
627
|
+
const found = /* @__PURE__ */ new Set();
|
|
628
|
+
for (const m of text3.matchAll(/(?:^|[\s`'"(\[])((?:\.{0,2}\/)?[\w.-]+(?:\/[\w.-]+)+\/?|[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|cpp|c|h|swift|scala|sql|yml|yaml|json|tf|vue|svelte))(?=$|[\s`'"),.:;\]])/g)) {
|
|
629
|
+
found.add(m[1].replace(/^\.\//, ""));
|
|
835
630
|
}
|
|
836
|
-
return
|
|
631
|
+
return found.size;
|
|
837
632
|
}
|
|
838
|
-
|
|
839
|
-
return
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
].
|
|
633
|
+
function classifyCodeTurn(s) {
|
|
634
|
+
if (s.maxMode) return { hard: true, reason: "Max Mode" };
|
|
635
|
+
if (s.previousTrouble) return { hard: true, reason: s.previousTrouble };
|
|
636
|
+
const text3 = s.content;
|
|
637
|
+
const codeLines = [...text3.matchAll(/```[\s\S]*?```/g)].reduce((n, block) => n + block[0].split("\n").length, 0);
|
|
638
|
+
if (codeLines > LONG_CODE_LINES) return { hard: true, reason: `${codeLines} lines of code in the request` };
|
|
639
|
+
if (text3.length > LONG_INSTRUCTION_CHARS) return { hard: true, reason: `long instruction (${text3.length} characters)` };
|
|
640
|
+
const plain = normalize(text3);
|
|
641
|
+
for (const [re, reason] of HARD_WORK) if (re.test(plain)) return { hard: true, reason };
|
|
642
|
+
const files = namedFiles(text3);
|
|
643
|
+
if (files >= MANY_FILES) return { hard: true, reason: `${files} files named` };
|
|
644
|
+
return { hard: false, reason: text3.length > 600 || codeLines > 0 ? "bounded task" : "short, direct request" };
|
|
843
645
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
646
|
+
var STREAM_REASONS = ["ORG_OUTPUT_TEXT_RULES", "SEMANTIC_INSPECTOR", "APPLICATION_OUTPUT_TEXT_RULES", "DESTINATION_NO_STREAM"];
|
|
647
|
+
function routeAutoTurn(boot, signals) {
|
|
648
|
+
if (boot.model !== "sq-auto") return null;
|
|
649
|
+
const aliases = boot.runtime.aliases ?? [];
|
|
650
|
+
if (!aliases.some((a) => a.tier)) return null;
|
|
651
|
+
const primary = boot.runtime.primaryModel || aliases.find((a) => a.tier === "MEDIUM")?.alias || null;
|
|
652
|
+
if (!primary) return null;
|
|
653
|
+
const decision = classifyCodeTurn(signals);
|
|
654
|
+
const max = decision.hard ? aliases.find((a) => a.tier === "COMPLEX") ?? aliases.find((a) => a.alias === "sq-auto-max") : void 0;
|
|
655
|
+
const chosen = max ?? aliases.find((a) => a.alias === primary) ?? { alias: primary, reasoning: null, tier: "MEDIUM" };
|
|
656
|
+
const fallback = decision.hard && !max;
|
|
657
|
+
const kind = decision.hard ? "hard task" : "everyday task";
|
|
658
|
+
return {
|
|
659
|
+
alias: chosen.alias,
|
|
660
|
+
tier: chosen.tier ?? null,
|
|
661
|
+
hard: decision.hard,
|
|
662
|
+
reason: decision.reason,
|
|
663
|
+
label: `SQ Auto \xB7 ${kind} \xB7 ${decision.reason}${fallback ? " (no hard-task model available, main model used)" : ""}`,
|
|
664
|
+
capability: chosen.reasoning ?? null,
|
|
665
|
+
maxOutputTokens: typeof chosen.maxOutputTokens === "number" && chosen.maxOutputTokens > 0 ? chosen.maxOutputTokens : null,
|
|
666
|
+
fallback
|
|
667
|
+
};
|
|
863
668
|
}
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
669
|
+
function credentialAlias(raw) {
|
|
670
|
+
const tier = raw.tier === "LIGHT" || raw.tier === "MEDIUM" || raw.tier === "COMPLEX" ? raw.tier : null;
|
|
671
|
+
const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.trunc(v) : null;
|
|
672
|
+
const max = positive(raw.maxOutputTokens), organization = positive(raw.organizationMaxOutputTokens);
|
|
673
|
+
const source = raw.outputLimitSource === "ORGANIZATION" || raw.outputLimitSource === "MODEL" || raw.outputLimitSource === "PLATFORM" ? raw.outputLimitSource : null;
|
|
674
|
+
const s = raw.stream && typeof raw.stream === "object" ? raw.stream : null;
|
|
675
|
+
const mode = s && (s.mode === "LIVE" || s.mode === "BUFFERED" || s.mode === "BUFFERED_POLICY") ? s.mode : null;
|
|
676
|
+
const reason = s && STREAM_REASONS.includes(s.reason) ? s.reason : null;
|
|
677
|
+
return {
|
|
678
|
+
alias: String(raw.alias),
|
|
679
|
+
reasoning: parseReasoningCapability(raw.reasoning),
|
|
680
|
+
...tier ? { tier } : {},
|
|
681
|
+
...max ? { maxOutputTokens: max } : {},
|
|
682
|
+
...source ? { outputLimitSource: source } : {},
|
|
683
|
+
...organization ? { organizationMaxOutputTokens: organization } : {},
|
|
684
|
+
...mode ? { stream: { mode, ...reason ? { reason } : {} } } : {}
|
|
685
|
+
};
|
|
875
686
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
687
|
+
|
|
688
|
+
// src/application/services/workspaceSandbox/toolPolicy.ts
|
|
689
|
+
var import_fs3 = require("fs");
|
|
690
|
+
var import_promises4 = require("fs/promises");
|
|
691
|
+
var import_path3 = require("path");
|
|
692
|
+
|
|
693
|
+
// src/application/services/workspaceSandbox/workspaceGit.ts
|
|
694
|
+
var import_child_process = require("child_process");
|
|
695
|
+
var import_util = require("util");
|
|
696
|
+
var import_crypto = require("crypto");
|
|
697
|
+
var import_promises2 = require("fs/promises");
|
|
698
|
+
var import_fs2 = require("fs");
|
|
699
|
+
var import_os = require("os");
|
|
700
|
+
var import_path2 = require("path");
|
|
701
|
+
|
|
702
|
+
// src/application/services/workspaceSandbox/checkpointMap.ts
|
|
703
|
+
var CHECKPOINT_MAP_VERSION = 2;
|
|
704
|
+
var LOCAL_FOLDER_KEY = ".";
|
|
705
|
+
function parseCheckpoints(raw, legacyRepo) {
|
|
706
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
707
|
+
if (!raw) return out2;
|
|
708
|
+
const trimmed = raw.trimStart();
|
|
709
|
+
if (trimmed.startsWith("{")) {
|
|
890
710
|
try {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
if (!seen.has(cwd)) {
|
|
896
|
-
seen.add(cwd);
|
|
897
|
-
out2.push(cwd);
|
|
898
|
-
}
|
|
899
|
-
break;
|
|
711
|
+
const parsed = JSON.parse(trimmed);
|
|
712
|
+
if (parsed && parsed.version === CHECKPOINT_MAP_VERSION && parsed.repos && typeof parsed.repos === "object" && !Array.isArray(parsed.repos)) {
|
|
713
|
+
for (const [repo2, patch] of Object.entries(parsed.repos)) {
|
|
714
|
+
if (repo2 && typeof patch === "string" && patch) out2.set(repo2, patch);
|
|
900
715
|
}
|
|
901
716
|
}
|
|
902
717
|
} catch {
|
|
903
718
|
}
|
|
719
|
+
return out2;
|
|
904
720
|
}
|
|
721
|
+
out2.set(legacyRepo ?? LOCAL_FOLDER_KEY, raw);
|
|
905
722
|
return out2;
|
|
906
723
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
}
|
|
913
|
-
const files = (await codexFiles(sources.codexDir)).filter((f) => f.path.includes(externalId)).sort((a, b) => b.mtime - a.mtime);
|
|
914
|
-
for (const f of files) {
|
|
915
|
-
const c = await parseCodexFile(f.path).catch(() => null);
|
|
916
|
-
if (c?.externalId === externalId) return c;
|
|
917
|
-
}
|
|
918
|
-
return null;
|
|
919
|
-
}
|
|
920
|
-
function prepareImport(c) {
|
|
921
|
-
let removed = 0;
|
|
922
|
-
const scrub = (s) => {
|
|
923
|
-
const r = scrubSecrets(s);
|
|
924
|
-
removed += r.count;
|
|
925
|
-
return r.text;
|
|
926
|
-
};
|
|
927
|
-
const cap = (s, max) => Buffer.byteLength(s) > max ? `${Buffer.from(s).subarray(0, max - 32).toString("utf8").replace(/�+$/, "")}
|
|
928
|
-
[... cut ...]` : s;
|
|
929
|
-
const messages = c.messages.map((m) => ({
|
|
930
|
-
role: m.role,
|
|
931
|
-
text: cap(scrub(m.text), IMPORT_LIMITS.maxMessageBytes),
|
|
932
|
-
at: m.at,
|
|
933
|
-
...m.tools?.length ? { tools: m.tools.slice(0, IMPORT_LIMITS.maxToolsPerMessage).map((t) => ({ name: t.name, summary: oneLine(scrub(t.summary), IMPORT_LIMITS.maxToolSummary) })) } : {}
|
|
934
|
-
}));
|
|
935
|
-
const title = oneLine(scrubSecrets(c.title).text, IMPORT_LIMITS.maxTitle) || "Untitled conversation";
|
|
936
|
-
let start = Math.max(0, messages.length - IMPORT_LIMITS.maxMessages);
|
|
937
|
-
let bytes = importBytes(messages.slice(start));
|
|
938
|
-
while (bytes > IMPORT_LIMITS.maxBytes && start < messages.length - 1) bytes -= importBytes([messages[start++]]);
|
|
939
|
-
return { source: c.source, externalId: c.externalId, folder: c.folder, title, secretsRemoved: removed, messages: messages.slice(start), omitted: start };
|
|
940
|
-
}
|
|
941
|
-
function scrubRecord(record) {
|
|
942
|
-
let removed = 0;
|
|
943
|
-
const walk = (value, key) => {
|
|
944
|
-
if (typeof value === "string") {
|
|
945
|
-
if (key === "signature" || key === "data") return value;
|
|
946
|
-
const r = scrubSecrets(value);
|
|
947
|
-
removed += r.count;
|
|
948
|
-
return r.text;
|
|
949
|
-
}
|
|
950
|
-
if (Array.isArray(value)) {
|
|
951
|
-
const out2 = [];
|
|
952
|
-
for (const item of value) {
|
|
953
|
-
const block = item;
|
|
954
|
-
if (block && typeof block === "object" && block.type === "thinking" && typeof block.thinking === "string") {
|
|
955
|
-
const r = scrubSecrets(block.thinking);
|
|
956
|
-
if (r.count) {
|
|
957
|
-
removed += r.count;
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
out2.push(item);
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
if (block && typeof block === "object" && block.type === "redacted_thinking") {
|
|
964
|
-
out2.push(item);
|
|
965
|
-
continue;
|
|
966
|
-
}
|
|
967
|
-
out2.push(walk(item));
|
|
968
|
-
}
|
|
969
|
-
return out2;
|
|
970
|
-
}
|
|
971
|
-
if (value && typeof value === "object") {
|
|
972
|
-
const o = value;
|
|
973
|
-
for (const k of Object.keys(o)) o[k] = walk(o[k], k);
|
|
974
|
-
return o;
|
|
975
|
-
}
|
|
976
|
-
return value;
|
|
977
|
-
};
|
|
978
|
-
walk(record);
|
|
979
|
-
return removed;
|
|
980
|
-
}
|
|
981
|
-
async function writeScrubbedTranscript(source, target) {
|
|
982
|
-
const out2 = (0, import_fs.createWriteStream)(target, { mode: 384, flags: "wx" });
|
|
983
|
-
let removed = 0;
|
|
984
|
-
const done = new Promise((resolve8, reject) => {
|
|
985
|
-
out2.on("finish", resolve8);
|
|
986
|
-
out2.on("error", reject);
|
|
987
|
-
});
|
|
988
|
-
const rl = (0, import_readline.createInterface)({ input: (0, import_fs.createReadStream)(source, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
989
|
-
try {
|
|
990
|
-
for await (const line of rl) {
|
|
991
|
-
if (!line) continue;
|
|
992
|
-
let record;
|
|
993
|
-
try {
|
|
994
|
-
record = JSON.parse(line);
|
|
995
|
-
} catch {
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
removed += scrubRecord(record);
|
|
999
|
-
if (!out2.write(`${JSON.stringify(record)}
|
|
1000
|
-
`)) await new Promise((r) => out2.once("drain", () => r()));
|
|
1001
|
-
}
|
|
1002
|
-
} finally {
|
|
1003
|
-
rl.close();
|
|
1004
|
-
out2.end();
|
|
1005
|
-
}
|
|
1006
|
-
await done;
|
|
1007
|
-
return removed;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// src/application/services/workspaceSandbox/backgroundService.ts
|
|
1011
|
-
var import_child_process2 = require("child_process");
|
|
1012
|
-
var import_crypto3 = require("crypto");
|
|
1013
|
-
var import_fs5 = require("fs");
|
|
1014
|
-
var import_path6 = require("path");
|
|
1015
|
-
|
|
1016
|
-
// src/application/services/workspaceSandbox/localWorkspace.ts
|
|
1017
|
-
var import_promises4 = require("fs/promises");
|
|
1018
|
-
var import_os3 = require("os");
|
|
1019
|
-
var import_path4 = require("path");
|
|
1020
|
-
|
|
1021
|
-
// src/application/services/workspaceSandbox/localTracking.ts
|
|
1022
|
-
var import_crypto2 = require("crypto");
|
|
1023
|
-
var import_fs3 = require("fs");
|
|
1024
|
-
var import_promises3 = require("fs/promises");
|
|
1025
|
-
var import_os2 = require("os");
|
|
1026
|
-
var import_path3 = require("path");
|
|
1027
|
-
|
|
1028
|
-
// src/application/services/workspaceSandbox/workspaceGit.ts
|
|
1029
|
-
var import_child_process = require("child_process");
|
|
1030
|
-
var import_util = require("util");
|
|
1031
|
-
var import_crypto = require("crypto");
|
|
1032
|
-
var import_promises2 = require("fs/promises");
|
|
1033
|
-
var import_fs2 = require("fs");
|
|
1034
|
-
var import_os = require("os");
|
|
1035
|
-
var import_path2 = require("path");
|
|
1036
|
-
|
|
1037
|
-
// src/application/services/workspaceSandbox/checkpointMap.ts
|
|
1038
|
-
var CHECKPOINT_MAP_VERSION = 2;
|
|
1039
|
-
var LOCAL_FOLDER_KEY = ".";
|
|
1040
|
-
function parseCheckpoints(raw, legacyRepo) {
|
|
1041
|
-
const out2 = /* @__PURE__ */ new Map();
|
|
1042
|
-
if (!raw) return out2;
|
|
1043
|
-
const trimmed = raw.trimStart();
|
|
1044
|
-
if (trimmed.startsWith("{")) {
|
|
1045
|
-
try {
|
|
1046
|
-
const parsed = JSON.parse(trimmed);
|
|
1047
|
-
if (parsed && parsed.version === CHECKPOINT_MAP_VERSION && parsed.repos && typeof parsed.repos === "object" && !Array.isArray(parsed.repos)) {
|
|
1048
|
-
for (const [repo2, patch] of Object.entries(parsed.repos)) {
|
|
1049
|
-
if (repo2 && typeof patch === "string" && patch) out2.set(repo2, patch);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
} catch {
|
|
1053
|
-
}
|
|
1054
|
-
return out2;
|
|
1055
|
-
}
|
|
1056
|
-
out2.set(legacyRepo ?? LOCAL_FOLDER_KEY, raw);
|
|
1057
|
-
return out2;
|
|
1058
|
-
}
|
|
1059
|
-
function serializeCheckpoints(map) {
|
|
1060
|
-
const repos = {};
|
|
1061
|
-
for (const key of [...map.keys()].sort()) {
|
|
1062
|
-
const patch = map.get(key);
|
|
1063
|
-
if (patch) repos[key] = patch;
|
|
724
|
+
function serializeCheckpoints(map) {
|
|
725
|
+
const repos = {};
|
|
726
|
+
for (const key of [...map.keys()].sort()) {
|
|
727
|
+
const patch = map.get(key);
|
|
728
|
+
if (patch) repos[key] = patch;
|
|
1064
729
|
}
|
|
1065
730
|
return Object.keys(repos).length ? JSON.stringify({ version: CHECKPOINT_MAP_VERSION, repos }) : "";
|
|
1066
731
|
}
|
|
@@ -1221,148 +886,1369 @@ async function filesForPullRequest(root, base, published = [], scope) {
|
|
|
1221
886
|
out2.skipped.push({ path: c.path, reason: "binary" });
|
|
1222
887
|
continue;
|
|
1223
888
|
}
|
|
1224
|
-
const r = await prText((0, import_path2.join)(root, c.path));
|
|
1225
|
-
if ("content" in r) out2.files.push({ path: c.path, content: r.content });
|
|
1226
|
-
else if (r.reason === "missing") out2.files.push({ path: c.path, content: "", deleted: true });
|
|
1227
|
-
else out2.skipped.push({ path: c.path, reason: r.reason });
|
|
889
|
+
const r = await prText((0, import_path2.join)(root, c.path));
|
|
890
|
+
if ("content" in r) out2.files.push({ path: c.path, content: r.content });
|
|
891
|
+
else if (r.reason === "missing") out2.files.push({ path: c.path, content: "", deleted: true });
|
|
892
|
+
else out2.skipped.push({ path: c.path, reason: r.reason });
|
|
893
|
+
}
|
|
894
|
+
const changed = new Set(changes.map((c) => c.path));
|
|
895
|
+
for (const path of published) {
|
|
896
|
+
if (changed.has(path) || !path || path.startsWith("/") || path.split("/").includes("..")) continue;
|
|
897
|
+
if (scope?.prefix && !path.startsWith(`${scope.prefix}/`)) continue;
|
|
898
|
+
const buf = await gitBuffer(["show", `${base}:${path}`], { cwd: root, maxBuffer: 8 * 1024 * 1024, env: scope?.env }).catch(() => null);
|
|
899
|
+
if (buf === null) {
|
|
900
|
+
out2.files.push({ path, content: "", deleted: true });
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const content = buf.toString("utf8");
|
|
904
|
+
if (buf.includes(0) || Buffer.byteLength(content, "utf8") !== buf.length) {
|
|
905
|
+
out2.skipped.push({ path, reason: "binary" });
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
out2.files.push({ path, content });
|
|
909
|
+
}
|
|
910
|
+
return out2;
|
|
911
|
+
}
|
|
912
|
+
async function resolveInside(root, p) {
|
|
913
|
+
if (!p || p.includes("\0")) return null;
|
|
914
|
+
const abs = (0, import_path2.resolve)(root, p);
|
|
915
|
+
const realRoot = await (0, import_promises2.realpath)(root).catch(() => (0, import_path2.resolve)(root));
|
|
916
|
+
let probe = abs;
|
|
917
|
+
while (!(0, import_fs2.existsSync)(probe) && (0, import_path2.dirname)(probe) !== probe) probe = (0, import_path2.dirname)(probe);
|
|
918
|
+
const realProbe = await (0, import_promises2.realpath)(probe).catch(() => probe);
|
|
919
|
+
const rest = (0, import_path2.relative)(probe, abs);
|
|
920
|
+
const finalPath = rest ? (0, import_path2.join)(realProbe, rest) : realProbe;
|
|
921
|
+
if (finalPath !== realRoot && !finalPath.startsWith(realRoot + import_path2.sep)) return null;
|
|
922
|
+
return finalPath;
|
|
923
|
+
}
|
|
924
|
+
async function discardPath(root, base, relPath) {
|
|
925
|
+
if ((0, import_path2.isAbsolute)(relPath)) relPath = (0, import_path2.relative)(root, relPath);
|
|
926
|
+
const abs = await resolveInside(root, relPath);
|
|
927
|
+
if (!abs) throw new Error("PATH_OUTSIDE_WORKSPACE");
|
|
928
|
+
const rel = (0, import_path2.relative)(await (0, import_promises2.realpath)(root).catch(() => root), abs).split(import_path2.sep).join("/");
|
|
929
|
+
if (rel === "" || rel === ".git" || rel.startsWith(".git/")) throw new Error("PATH_NOT_DISCARDABLE");
|
|
930
|
+
const existed = await git(["cat-file", "-e", `${base}:${rel}`], { cwd: root }).then(() => true, () => false);
|
|
931
|
+
if (existed) {
|
|
932
|
+
await git(["checkout", base, "--", rel], { cwd: root });
|
|
933
|
+
await git(["reset", "-q", "--", rel], { cwd: root }).catch(() => void 0);
|
|
934
|
+
return "restored";
|
|
935
|
+
}
|
|
936
|
+
await (0, import_promises2.rm)(abs, { recursive: true, force: true });
|
|
937
|
+
await git(["rm", "-q", "--cached", "--ignore-unmatch", "--", rel], { cwd: root }).catch(() => void 0);
|
|
938
|
+
return "removed";
|
|
939
|
+
}
|
|
940
|
+
function truncateUtf8(s, maxBytes) {
|
|
941
|
+
if (maxBytes <= 0) return "";
|
|
942
|
+
const buf = Buffer.from(s, "utf8");
|
|
943
|
+
if (buf.length <= maxBytes) return s;
|
|
944
|
+
let end = maxBytes;
|
|
945
|
+
while (end > 0 && (buf[end] & 192) === 128) end--;
|
|
946
|
+
return buf.subarray(0, end).toString("utf8");
|
|
947
|
+
}
|
|
948
|
+
function tailUtf8(s, maxBytes) {
|
|
949
|
+
const buf = Buffer.from(s, "utf8");
|
|
950
|
+
if (buf.length <= maxBytes) return s;
|
|
951
|
+
let start = buf.length - maxBytes;
|
|
952
|
+
while (start < buf.length && (buf[start] & 192) === 128) start++;
|
|
953
|
+
return buf.subarray(start).toString("utf8");
|
|
954
|
+
}
|
|
955
|
+
function parseHunks(path, patch) {
|
|
956
|
+
const out2 = [];
|
|
957
|
+
let current = null;
|
|
958
|
+
for (const line of patch.split("\n")) {
|
|
959
|
+
const m = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
960
|
+
if (m) {
|
|
961
|
+
current = {
|
|
962
|
+
hash: "",
|
|
963
|
+
header: line,
|
|
964
|
+
lines: [],
|
|
965
|
+
oldStart: Number(m[1]),
|
|
966
|
+
oldLines: m[2] === void 0 ? 1 : Number(m[2]),
|
|
967
|
+
newStart: Number(m[3]),
|
|
968
|
+
newLines: m[4] === void 0 ? 1 : Number(m[4])
|
|
969
|
+
};
|
|
970
|
+
out2.push(current);
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
if (!current) continue;
|
|
974
|
+
if (line.startsWith(" ") || line.startsWith("+") || line.startsWith("-") || line.startsWith("\\")) current.lines.push(line);
|
|
975
|
+
}
|
|
976
|
+
const seen = /* @__PURE__ */ new Map();
|
|
977
|
+
for (const h of out2) {
|
|
978
|
+
const base = (0, import_crypto.createHash)("sha256").update(`${path}
|
|
979
|
+
${h.lines.join("\n")}`).digest("hex").slice(0, 16);
|
|
980
|
+
const n = (seen.get(base) ?? 0) + 1;
|
|
981
|
+
seen.set(base, n);
|
|
982
|
+
h.hash = n === 1 ? base : `${base}-${n}`;
|
|
983
|
+
}
|
|
984
|
+
return out2;
|
|
985
|
+
}
|
|
986
|
+
async function discardHunk(root, base, relPath, hash, scope) {
|
|
987
|
+
const abs = await resolveInside(root, relPath);
|
|
988
|
+
if (!abs) throw new Error("PATH_OUTSIDE_WORKSPACE");
|
|
989
|
+
const rel = (0, import_path2.relative)(await (0, import_promises2.realpath)(root).catch(() => root), abs).split(import_path2.sep).join("/");
|
|
990
|
+
if (rel === "" || rel === ".git" || rel.startsWith(".git/")) throw new Error("PATH_NOT_DISCARDABLE");
|
|
991
|
+
return withWorktreeIndex(root, async (env) => {
|
|
992
|
+
const patch = await git(["diff", "--cached", "--no-renames", "--no-color", base, "--", rel], { cwd: root, env });
|
|
993
|
+
if (!patch) return "not_found";
|
|
994
|
+
const hunk = parseHunks(rel, patch).find((h) => h.hash === hash);
|
|
995
|
+
if (!hunk) return "not_found";
|
|
996
|
+
const headerEnd = patch.indexOf("\n@@");
|
|
997
|
+
const fileHeader = patch.slice(0, headerEnd + 1);
|
|
998
|
+
const one = `${fileHeader}${hunk.header}
|
|
999
|
+
${hunk.lines.join("\n")}
|
|
1000
|
+
`;
|
|
1001
|
+
const file = (0, import_path2.join)((0, import_os.tmpdir)(), `sq-hunk-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.patch`);
|
|
1002
|
+
await (0, import_promises2.writeFile)(file, one, "utf8");
|
|
1003
|
+
try {
|
|
1004
|
+
await git(["apply", "-R", "--whitespace=nowarn", file], { cwd: root, env: scope?.env });
|
|
1005
|
+
} finally {
|
|
1006
|
+
await (0, import_promises2.rm)(file, { force: true }).catch(() => void 0);
|
|
1007
|
+
}
|
|
1008
|
+
return "discarded";
|
|
1009
|
+
}, scope);
|
|
1010
|
+
}
|
|
1011
|
+
async function restoreTree(root, treeId, scope) {
|
|
1012
|
+
if (!/^[0-9a-f]{40}([0-9a-f]{24})?$/.test(treeId)) throw new Error("INVALID_TREE");
|
|
1013
|
+
await withWorktreeIndex(root, async (env) => {
|
|
1014
|
+
const now = (await git(["write-tree"], { cwd: root, env })).trim();
|
|
1015
|
+
const added = await git(["diff", "--no-renames", "--name-only", "-z", "--diff-filter=A", treeId, now, ...scopeSpec(scope)], { cwd: root, env });
|
|
1016
|
+
for (const rel of added.split("\0").filter(Boolean)) {
|
|
1017
|
+
const abs = await resolveInside(root, rel);
|
|
1018
|
+
if (abs) await (0, import_promises2.rm)(abs, { force: true });
|
|
1019
|
+
}
|
|
1020
|
+
if (scope?.prefix) {
|
|
1021
|
+
await git(["checkout", treeId, "--", scope.prefix], { cwd: root, env }).catch(() => void 0);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
await git(["read-tree", treeId], { cwd: root, env });
|
|
1025
|
+
await git(["checkout-index", "-a", "-f"], { cwd: root, env });
|
|
1026
|
+
}, scope);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/application/services/workspaceSandbox/webFetchGuard.ts
|
|
1030
|
+
var import_promises3 = require("dns/promises");
|
|
1031
|
+
var import_net = require("net");
|
|
1032
|
+
var defaultLookup = (host) => (0, import_promises3.lookup)(host, { all: true, verbatim: true });
|
|
1033
|
+
var ALLOWED_PORTS = /* @__PURE__ */ new Set(["", "80", "443", "8080", "8443"]);
|
|
1034
|
+
function v4Private(ip) {
|
|
1035
|
+
const [a, b] = ip.split(".").map(Number);
|
|
1036
|
+
return a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 198 && (b === 18 || b === 19);
|
|
1037
|
+
}
|
|
1038
|
+
function v6Private(ip) {
|
|
1039
|
+
const s = ip.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1040
|
+
if (s === "::" || s === "::1") return true;
|
|
1041
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(s);
|
|
1042
|
+
if (mapped) return v4Private(mapped[1]);
|
|
1043
|
+
const head = parseInt(s.split(":")[0] || "0", 16);
|
|
1044
|
+
return (head & 65024) === 64512 || (head & 65472) === 65152 || (head & 65280) === 65280 || s.startsWith("fd00:ec2::");
|
|
1045
|
+
}
|
|
1046
|
+
function privateAddress(ip) {
|
|
1047
|
+
const kind = (0, import_net.isIP)(ip.replace(/^\[|\]$/g, ""));
|
|
1048
|
+
if (kind === 4) return v4Private(ip);
|
|
1049
|
+
if (kind === 6) return v6Private(ip);
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
async function webFetchDenial(raw, resolve8 = defaultLookup) {
|
|
1053
|
+
if (typeof raw !== "string" || !raw.trim()) return "WebFetch needs a URL.";
|
|
1054
|
+
let url;
|
|
1055
|
+
try {
|
|
1056
|
+
url = new URL(raw.trim());
|
|
1057
|
+
} catch {
|
|
1058
|
+
return "WebFetch needs a valid http or https URL.";
|
|
1059
|
+
}
|
|
1060
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return "WebFetch reads only http and https pages.";
|
|
1061
|
+
if (url.username || url.password) return "URLs with credentials cannot be fetched from the workspace.";
|
|
1062
|
+
if (!ALLOWED_PORTS.has(url.port)) return "WebFetch reads public pages on the standard web ports only.";
|
|
1063
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1064
|
+
if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
1065
|
+
return "Addresses of this machine or its private network cannot be fetched from the workspace.";
|
|
1066
|
+
}
|
|
1067
|
+
if ((0, import_net.isIP)(host)) return privateAddress(host) ? "Addresses of this machine or its private network cannot be fetched from the workspace." : null;
|
|
1068
|
+
let addresses;
|
|
1069
|
+
try {
|
|
1070
|
+
addresses = await resolve8(host);
|
|
1071
|
+
} catch {
|
|
1072
|
+
return `The host ${host} could not be resolved.`;
|
|
1073
|
+
}
|
|
1074
|
+
if (!addresses.length || addresses.some((a) => privateAddress(a.address))) {
|
|
1075
|
+
return "Addresses of this machine or its private network cannot be fetched from the workspace.";
|
|
1076
|
+
}
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/application/services/workspaceSandbox/toolPolicy.ts
|
|
1081
|
+
var SQ_MCP_SERVER = "scalequality";
|
|
1082
|
+
var SQ_MCP_PREFIX = `mcp__${SQ_MCP_SERVER}__`;
|
|
1083
|
+
var DENIED_TOOLS = ["WebSearch", "RemoteTrigger", "CronCreate", "CronDelete", "CronList", "ScheduleWakeup", "PushNotification", "EnterWorktree", "ExitWorktree", "Artifact", "Workflow", "SendFeedback", "ClaudeDesign", "Projects"];
|
|
1084
|
+
var READ_TOOLS = { Read: "file_path", Glob: "path", Grep: "path", LS: "path" };
|
|
1085
|
+
var WRITE_TOOLS = { Write: "file_path", Edit: "file_path", MultiEdit: "file_path", NotebookEdit: "notebook_path" };
|
|
1086
|
+
var WRITE_TOOL_PATHS = WRITE_TOOLS;
|
|
1087
|
+
var HARMLESS = /* @__PURE__ */ new Set(["TodoWrite", "BashOutput", "KillShell", "TaskStop", "TaskOutput", "Skill"]);
|
|
1088
|
+
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
1089
|
+
var SQ_READ_ONLY = /* @__PURE__ */ new Set([
|
|
1090
|
+
"get_project_measurement",
|
|
1091
|
+
"get_measurement_findings",
|
|
1092
|
+
"get_agent_options",
|
|
1093
|
+
"get_agent_quote",
|
|
1094
|
+
"get_pr_followup_status",
|
|
1095
|
+
"get_agent_request",
|
|
1096
|
+
"get_measurement_request",
|
|
1097
|
+
"get_measurement_setup",
|
|
1098
|
+
"explain_change_request",
|
|
1099
|
+
"read_scalequality_guide",
|
|
1100
|
+
"list_repositories",
|
|
1101
|
+
"open_repository",
|
|
1102
|
+
"measure_change"
|
|
1103
|
+
]);
|
|
1104
|
+
var BASH_MAX_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1105
|
+
var CLOUD_BASH_DENY = [
|
|
1106
|
+
{ re: /\bgit\b[^\n;&|]*\s(push|send-pack|send-email|request-pull)\b/, why: "Publishing from the workspace is not allowed. Use the open_pull_request tool; it asks the user for approval." },
|
|
1107
|
+
{ re: /\bgit\b[^\n;&|]*\sremote\b/, why: "Git remotes are managed by ScaleQuality and cannot be read or changed from the workspace." }
|
|
1108
|
+
];
|
|
1109
|
+
var BASH_DENY = [
|
|
1110
|
+
{ re: /\bgit\b[^\n;&|]*\scredential\b/, why: "Git credentials are not available in the workspace." },
|
|
1111
|
+
{ re: /\bgit\b[^\n;&|]*\sconfig\b[^\n;&|]*(credential|insteadof|pushinsteadof|extraheader|\bremote\.|\burl\.)/, why: "Git credential and remote settings cannot be changed from the workspace." },
|
|
1112
|
+
{ re: /credential[._-]?helper/, why: "Git credential helpers cannot be configured in the workspace." },
|
|
1113
|
+
{ re: /\/proc\/[^\s'"]*\/(environ|mem)\b/, why: "Process environments are not readable from the workspace." },
|
|
1114
|
+
{ re: /\b(INTERNAL_API_SECRET|WORKSPACE_SESSION_SECRET|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL)\b/, why: "Workspace credentials are not available to commands." },
|
|
1115
|
+
{ re: /(^|[;&|(`]|\$\()\s*(env|printenv|export\s+-p|declare\s+-x|set)\s*($|[;&|)>`])/, why: "Dumping the whole environment is not allowed; read the specific variable your task needs." }
|
|
1116
|
+
];
|
|
1117
|
+
var HOME_PREFIX = String.raw`(~|\$\{?home\}?|\$\{?userprofile\}?|/users/[^/\s]+|/home/[^/\s]+|/root)/`;
|
|
1118
|
+
var LOCAL_BASH_DENY = [
|
|
1119
|
+
{ re: /(^|[\s=:/~])\.(ssh|aws|gnupg)(\/|\s|$)/i, why: "Credential directories on this machine are not readable from the workspace." },
|
|
1120
|
+
{ re: /(^|[\s=:/~])\.(netrc|git-credentials|pgpass)\b/i, why: "Credential files on this machine are not readable from the workspace." },
|
|
1121
|
+
// Projects have their own .npmrc or .docker folder; only the ones in the home directory hold credentials.
|
|
1122
|
+
{ re: new RegExp(`${HOME_PREFIX}\\.(npmrc|yarnrc\\.yml|pypirc|docker|kube|azure|config/(gh|hub|gcloud|op))\\b`, "i"), why: "Credential files on this machine are not readable from the workspace." },
|
|
1123
|
+
{ re: /(^|[;&|(`]|\$\()\s*(security\s+(find|dump|export)-|gh\s+auth\s+(token|status\s+-t\b|status\s+--show-token)|op\s+(read|item\s+get)|pass\s+show|secret-tool\s+lookup|aws\s+configure\s+(get|export-credentials)|gcloud\s+auth\s+print-)/i, why: "Credential stores on this machine are not readable from the workspace." }
|
|
1124
|
+
];
|
|
1125
|
+
function normalizeCommand(command) {
|
|
1126
|
+
return command.replace(/\\\n/g, " ").replace(/['"\\]/g, "").replace(/[ \t]+/g, " ").toLowerCase();
|
|
1127
|
+
}
|
|
1128
|
+
function bashDenial(command, opts = {}) {
|
|
1129
|
+
const n = normalizeCommand(command);
|
|
1130
|
+
for (const { re, why } of BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1131
|
+
if (!opts.local) {
|
|
1132
|
+
for (const { re, why } of CLOUD_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1133
|
+
}
|
|
1134
|
+
if (opts.local) {
|
|
1135
|
+
for (const { re, why } of LOCAL_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1136
|
+
}
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
async function realpathLoose(p) {
|
|
1140
|
+
let probe = (0, import_path3.resolve)(p);
|
|
1141
|
+
while (!(0, import_fs3.existsSync)(probe) && (0, import_path3.dirname)(probe) !== probe) probe = (0, import_path3.dirname)(probe);
|
|
1142
|
+
const real = await (0, import_promises4.realpath)(probe).catch(() => probe);
|
|
1143
|
+
const rest = (0, import_path3.relative)(probe, (0, import_path3.resolve)(p));
|
|
1144
|
+
return rest ? (0, import_path3.join)(real, rest) : real;
|
|
1145
|
+
}
|
|
1146
|
+
function inside(root, abs) {
|
|
1147
|
+
return abs === root || abs.startsWith(root + import_path3.sep);
|
|
1148
|
+
}
|
|
1149
|
+
var PLAN_ONLY_READS = "Plan mode is read-only: read and search the code, write the plan to the plan file, then call ExitPlanMode. Changes start after the user approves the plan.";
|
|
1150
|
+
var READ_ONLY_PROGRAMS = /* @__PURE__ */ new Set([
|
|
1151
|
+
"ls",
|
|
1152
|
+
"cat",
|
|
1153
|
+
"head",
|
|
1154
|
+
"tail",
|
|
1155
|
+
"wc",
|
|
1156
|
+
"grep",
|
|
1157
|
+
"rg",
|
|
1158
|
+
"ag",
|
|
1159
|
+
"tree",
|
|
1160
|
+
"pwd",
|
|
1161
|
+
"file",
|
|
1162
|
+
"stat",
|
|
1163
|
+
"du",
|
|
1164
|
+
"df",
|
|
1165
|
+
"sort",
|
|
1166
|
+
"uniq",
|
|
1167
|
+
"cut",
|
|
1168
|
+
"tr",
|
|
1169
|
+
"which",
|
|
1170
|
+
"echo",
|
|
1171
|
+
"basename",
|
|
1172
|
+
"dirname",
|
|
1173
|
+
"realpath",
|
|
1174
|
+
"diff",
|
|
1175
|
+
"cloc",
|
|
1176
|
+
"jq",
|
|
1177
|
+
"less",
|
|
1178
|
+
"nl",
|
|
1179
|
+
"column",
|
|
1180
|
+
"date",
|
|
1181
|
+
"uname",
|
|
1182
|
+
"true"
|
|
1183
|
+
]);
|
|
1184
|
+
var READ_ONLY_GIT = /* @__PURE__ */ new Set(["status", "log", "show", "diff", "blame", "branch", "ls-files", "ls-tree", "grep", "rev-parse", "describe", "shortlog", "tag", "cat-file", "reflog"]);
|
|
1185
|
+
function readOnlyCommand(command) {
|
|
1186
|
+
if (/[<>`]|\$\(|\$\{/.test(command)) return false;
|
|
1187
|
+
const segments2 = command.split(/\|\||&&|\||;/).map((s) => s.trim()).filter(Boolean);
|
|
1188
|
+
if (!segments2.length) return false;
|
|
1189
|
+
return segments2.every((seg) => {
|
|
1190
|
+
const words = seg.split(/\s+/);
|
|
1191
|
+
let i = 0;
|
|
1192
|
+
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i])) i++;
|
|
1193
|
+
const program = words[i] ?? "";
|
|
1194
|
+
const rest = words.slice(i + 1);
|
|
1195
|
+
if (program === "git") {
|
|
1196
|
+
const sub = rest.find((w) => !w.startsWith("-"));
|
|
1197
|
+
if (!sub || !READ_ONLY_GIT.has(sub)) return false;
|
|
1198
|
+
if (sub === "branch" || sub === "tag") return rest.slice(rest.indexOf(sub) + 1).every((w) => /^(-a|-r|-v|-vv|-l|--list|--all|--show-current|--sort=\S+|--contains|--merged|--no-merged)$/.test(w));
|
|
1199
|
+
return !rest.some((w) => /^(--output|--open-files-in-pager|-O|--ext-diff)(=|$)/.test(w) || /^-O\S/.test(w));
|
|
1200
|
+
}
|
|
1201
|
+
if (program === "find") return !rest.some((w) => /^-(delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/.test(w));
|
|
1202
|
+
if (program === "rg") return !rest.some((w) => /^--pre(=|$)/.test(w));
|
|
1203
|
+
if (program === "sort" || program === "tree") return !rest.some((w) => /^(-o|--output)/.test(w));
|
|
1204
|
+
if (program === "uniq") return rest.filter((w) => !w.startsWith("-")).length <= 1;
|
|
1205
|
+
return READ_ONLY_PROGRAMS.has(program);
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
async function decideToolUse(toolName, input, ctx) {
|
|
1209
|
+
if (toolName.startsWith("mcp__") && ctx.orgMcpTools?.all.has(toolName)) {
|
|
1210
|
+
if (ctx.plan && !ctx.orgMcpTools.readOnly.has(toolName)) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1211
|
+
return { behavior: "allow", updatedInput: input };
|
|
1212
|
+
}
|
|
1213
|
+
if (toolName.startsWith("mcp__")) {
|
|
1214
|
+
if (!toolName.startsWith(SQ_MCP_PREFIX)) return { behavior: "deny", message: "Only ScaleQuality tools and the MCP servers your organization approved are available in this workspace." };
|
|
1215
|
+
if (ctx.plan && !SQ_READ_ONLY.has(toolName.slice(SQ_MCP_PREFIX.length))) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1216
|
+
return { behavior: "allow", updatedInput: input };
|
|
1217
|
+
}
|
|
1218
|
+
if (HARMLESS.has(toolName)) return { behavior: "allow", updatedInput: input };
|
|
1219
|
+
if (SUBAGENT_TOOLS.has(toolName)) {
|
|
1220
|
+
if (typeof input.prompt !== "string" || !input.prompt.trim()) return { behavior: "deny", message: "A subagent needs a task (prompt)." };
|
|
1221
|
+
const updated = { ...input, run_in_background: false };
|
|
1222
|
+
for (const k of ["isolation", "model", "name", "team_name", "mode"]) delete updated[k];
|
|
1223
|
+
return { behavior: "allow", updatedInput: updated };
|
|
1224
|
+
}
|
|
1225
|
+
if (toolName === "WebFetch") {
|
|
1226
|
+
const why = await webFetchDenial(input.url, ctx.lookup);
|
|
1227
|
+
return why ? { behavior: "deny", message: why } : { behavior: "allow", updatedInput: input };
|
|
1228
|
+
}
|
|
1229
|
+
if (ctx.protections?.secretFiles && (toolName in READ_TOOLS || toolName in WRITE_TOOLS)) {
|
|
1230
|
+
const why = secretFileToolDenial(toolName, input);
|
|
1231
|
+
if (why) return { behavior: "deny", message: why, reason: "PROTECTION_SECRET_FILE" };
|
|
1232
|
+
}
|
|
1233
|
+
if (toolName in WRITE_TOOLS && ctx.planDir) {
|
|
1234
|
+
const raw = input[WRITE_TOOLS[toolName]];
|
|
1235
|
+
if (typeof raw === "string" && raw.endsWith(".md")) {
|
|
1236
|
+
const plans = await realpathLoose(ctx.planDir);
|
|
1237
|
+
const abs = await realpathLoose((0, import_path3.resolve)(ctx.planDir, raw));
|
|
1238
|
+
if (inside(plans, abs) && abs !== plans) return { behavior: "allow", updatedInput: input };
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (ctx.plan && toolName in WRITE_TOOLS) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1242
|
+
if (toolName in READ_TOOLS || toolName in WRITE_TOOLS) {
|
|
1243
|
+
const field = READ_TOOLS[toolName] ?? WRITE_TOOLS[toolName];
|
|
1244
|
+
const raw = input[field];
|
|
1245
|
+
if ((raw === void 0 || raw === null || raw === "") && toolName in READ_TOOLS && toolName !== "Read") {
|
|
1246
|
+
return { behavior: "allow", updatedInput: input };
|
|
1247
|
+
}
|
|
1248
|
+
if (typeof raw !== "string" || raw.length === 0) return { behavior: "deny", message: `${toolName} needs a path inside the repository.` };
|
|
1249
|
+
const abs = await resolveInside(ctx.root, raw);
|
|
1250
|
+
let denied = false;
|
|
1251
|
+
for (const d of ctx.deniedRoots ?? []) {
|
|
1252
|
+
const realDenied = await (0, import_promises4.realpath)(d).catch(() => (0, import_path3.resolve)(d));
|
|
1253
|
+
if (abs && inside(realDenied, abs)) denied = true;
|
|
1254
|
+
}
|
|
1255
|
+
if (abs && !denied) {
|
|
1256
|
+
if (toolName in WRITE_TOOLS) {
|
|
1257
|
+
const realRoot = await (0, import_promises4.realpath)(ctx.root).catch(() => (0, import_path3.resolve)(ctx.root));
|
|
1258
|
+
const rel = (0, import_path3.relative)(realRoot, abs).split(import_path3.sep);
|
|
1259
|
+
if (rel.includes(".git")) return { behavior: "deny", message: "Files under .git cannot be written from the workspace." };
|
|
1260
|
+
}
|
|
1261
|
+
return { behavior: "allow", updatedInput: input };
|
|
1262
|
+
}
|
|
1263
|
+
if (toolName in READ_TOOLS) {
|
|
1264
|
+
for (const extra of ctx.extraReadRoots ?? []) {
|
|
1265
|
+
const e = await resolveInside(extra, raw);
|
|
1266
|
+
const realExtra = await (0, import_promises4.realpath)(extra).catch(() => (0, import_path3.resolve)(extra));
|
|
1267
|
+
if (e && inside(realExtra, e)) return { behavior: "allow", updatedInput: input };
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return { behavior: "deny", message: `${toolName} is limited to files inside the repository (${ctx.root}).` };
|
|
1271
|
+
}
|
|
1272
|
+
if (toolName === "Bash") {
|
|
1273
|
+
const command = typeof input.command === "string" ? input.command : "";
|
|
1274
|
+
if (!command.trim()) return { behavior: "deny", message: "Empty command." };
|
|
1275
|
+
const why = bashDenial(command, { local: ctx.local });
|
|
1276
|
+
if (why) return { behavior: "deny", message: why };
|
|
1277
|
+
const secret = ctx.protections?.secretFiles ? secretFileCommandDenial(command) : null;
|
|
1278
|
+
if (secret) return { behavior: "deny", message: secret, reason: "PROTECTION_SECRET_FILE" };
|
|
1279
|
+
if (ctx.plan && !readOnlyCommand(command)) return { behavior: "deny", message: PLAN_ONLY_READS };
|
|
1280
|
+
const updated = { ...input };
|
|
1281
|
+
delete updated.dangerouslyDisableSandbox;
|
|
1282
|
+
const t = typeof input.timeout === "number" && Number.isFinite(input.timeout) ? input.timeout : void 0;
|
|
1283
|
+
if (t !== void 0) updated.timeout = Math.max(1e3, Math.min(BASH_MAX_TIMEOUT_MS, t));
|
|
1284
|
+
return { behavior: "allow", updatedInput: updated };
|
|
1285
|
+
}
|
|
1286
|
+
if (toolName === "WebSearch") {
|
|
1287
|
+
return { behavior: "deny", message: "Web search is not available in the ScaleQuality workspace. Fetch a specific page with WebFetch." };
|
|
1288
|
+
}
|
|
1289
|
+
return { behavior: "deny", message: `${toolName} is not available in the ScaleQuality workspace.` };
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/application/services/workspaceSandbox/protections.ts
|
|
1293
|
+
var NO_PROTECTIONS = Object.freeze({ secretFiles: false, protectedPush: false, deployApproval: false, destructiveCommands: false, unknownPackage: null });
|
|
1294
|
+
var KNOWN_RULES = {
|
|
1295
|
+
"secret-files": ["block"],
|
|
1296
|
+
"protected-push": ["require"],
|
|
1297
|
+
"deploy-approval": ["require"],
|
|
1298
|
+
"destructive-commands": ["require"],
|
|
1299
|
+
"unknown-package": ["block", "warn"]
|
|
1300
|
+
};
|
|
1301
|
+
function protectionsWireOf(raw) {
|
|
1302
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
1303
|
+
const r = raw;
|
|
1304
|
+
if (!r.rules || typeof r.rules !== "object" || Array.isArray(r.rules)) return null;
|
|
1305
|
+
const rules = {};
|
|
1306
|
+
for (const [key, value] of Object.entries(r.rules)) {
|
|
1307
|
+
if (typeof value === "string" && KNOWN_RULES[key]?.includes(value)) rules[key] = value;
|
|
1308
|
+
}
|
|
1309
|
+
const version = typeof r.version === "number" && Number.isFinite(r.version) ? r.version : 0;
|
|
1310
|
+
return Object.keys(rules).length ? { version, rules } : null;
|
|
1311
|
+
}
|
|
1312
|
+
function parseProtections(raw) {
|
|
1313
|
+
const wire = protectionsWireOf(raw);
|
|
1314
|
+
if (!wire) return NO_PROTECTIONS;
|
|
1315
|
+
const r = wire.rules;
|
|
1316
|
+
return {
|
|
1317
|
+
secretFiles: r["secret-files"] === "block",
|
|
1318
|
+
protectedPush: r["protected-push"] === "require",
|
|
1319
|
+
deployApproval: r["deploy-approval"] === "require",
|
|
1320
|
+
destructiveCommands: r["destructive-commands"] === "require",
|
|
1321
|
+
unknownPackage: r["unknown-package"] === "block" || r["unknown-package"] === "warn" ? r["unknown-package"] : null
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
var SECRET_FILE_MESSAGE = "Blocked by your organization's protection: secret files are never sent to the model. Do not try to read this file another way; ask the user for the specific value your task needs, or use the example file (such as .env.example).";
|
|
1325
|
+
var ENV_TEMPLATES = /* @__PURE__ */ new Set(["example", "sample", "template", "dist"]);
|
|
1326
|
+
var SECRET_EXTENSIONS = /\.(pem|key|p12|pfx|jks|keystore)$/;
|
|
1327
|
+
var SECRET_NAMES = /* @__PURE__ */ new Set([".npmrc", ".pypirc", ".netrc", ".git-credentials", "credentials.json", "secrets.yml", "secrets.yaml", "terraform.tfvars"]);
|
|
1328
|
+
function isSecretPath(path) {
|
|
1329
|
+
const clean = path.trim().replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
1330
|
+
if (!clean) return false;
|
|
1331
|
+
const parts = clean.split("/");
|
|
1332
|
+
const name = parts[parts.length - 1];
|
|
1333
|
+
if (name === ".env") return true;
|
|
1334
|
+
if (name.startsWith(".env.")) return !name.slice(5).split(".").some((part) => ENV_TEMPLATES.has(part));
|
|
1335
|
+
if (SECRET_EXTENSIONS.test(name)) return true;
|
|
1336
|
+
if (/^id_(rsa|dsa|ecdsa|ed25519)/.test(name) && !name.endsWith(".pub")) return true;
|
|
1337
|
+
if (SECRET_NAMES.has(name) || name.endsWith(".auto.tfvars")) return true;
|
|
1338
|
+
if (/^service-account.*\.json$/.test(name)) return true;
|
|
1339
|
+
return name === "credentials" && parts[parts.length - 2] === ".aws";
|
|
1340
|
+
}
|
|
1341
|
+
var SECRET_SAMPLES = [
|
|
1342
|
+
".env",
|
|
1343
|
+
".env.local",
|
|
1344
|
+
".env.production",
|
|
1345
|
+
"server.pem",
|
|
1346
|
+
"tls.key",
|
|
1347
|
+
"cert.p12",
|
|
1348
|
+
"cert.pfx",
|
|
1349
|
+
"release.jks",
|
|
1350
|
+
"app.keystore",
|
|
1351
|
+
"id_rsa",
|
|
1352
|
+
"id_ed25519",
|
|
1353
|
+
".npmrc",
|
|
1354
|
+
".pypirc",
|
|
1355
|
+
".netrc",
|
|
1356
|
+
".git-credentials",
|
|
1357
|
+
"credentials.json",
|
|
1358
|
+
"service-account.json",
|
|
1359
|
+
"service-account-prod.json",
|
|
1360
|
+
"prod.auto.tfvars",
|
|
1361
|
+
"terraform.tfvars",
|
|
1362
|
+
".aws/credentials",
|
|
1363
|
+
"secrets.yml",
|
|
1364
|
+
"secrets.yaml"
|
|
1365
|
+
];
|
|
1366
|
+
var ORDINARY_SAMPLES = ["package.json", "index.ts", "main.py", "README.md", "app.yml", "app.yaml", "main.tf", "Main.java", "config"];
|
|
1367
|
+
function globRegex(glob) {
|
|
1368
|
+
let re = "";
|
|
1369
|
+
for (let i = 0; i < glob.length; i++) {
|
|
1370
|
+
const c = glob[i];
|
|
1371
|
+
if (c === "*") {
|
|
1372
|
+
if (glob[i + 1] === "*") {
|
|
1373
|
+
re += ".*";
|
|
1374
|
+
i++;
|
|
1375
|
+
if (glob[i + 1] === "/") i++;
|
|
1376
|
+
} else re += "[^/]*";
|
|
1377
|
+
} else if (c === "?") re += "[^/]";
|
|
1378
|
+
else if (c === "{") re += "(";
|
|
1379
|
+
else if (c === "}") re += ")";
|
|
1380
|
+
else if (c === ",") re += "|";
|
|
1381
|
+
else re += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
|
|
1382
|
+
}
|
|
1383
|
+
return new RegExp(`^${re}$`, "i");
|
|
1384
|
+
}
|
|
1385
|
+
function globTargetsSecrets(glob) {
|
|
1386
|
+
const patterns = glob.split(/\s+/).filter(Boolean).filter((g) => !g.startsWith("!"));
|
|
1387
|
+
return patterns.some((g) => {
|
|
1388
|
+
let re;
|
|
1389
|
+
try {
|
|
1390
|
+
re = globRegex(g.replace(/^\.\//, ""));
|
|
1391
|
+
} catch {
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
const test = (name) => re.test(name) || !g.includes("/") && re.test(name.split("/").pop());
|
|
1395
|
+
return SECRET_SAMPLES.some(test) && !ORDINARY_SAMPLES.some(test);
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
var SECRET_READ_DENY_RULES = [
|
|
1399
|
+
...[
|
|
1400
|
+
".env",
|
|
1401
|
+
".env.local",
|
|
1402
|
+
".env.*.local",
|
|
1403
|
+
".env.development",
|
|
1404
|
+
".env.dev",
|
|
1405
|
+
".env.production",
|
|
1406
|
+
".env.prod",
|
|
1407
|
+
".env.staging",
|
|
1408
|
+
".env.stage",
|
|
1409
|
+
".env.test",
|
|
1410
|
+
".env.testing",
|
|
1411
|
+
".env.qa",
|
|
1412
|
+
".env.uat",
|
|
1413
|
+
".env.preview",
|
|
1414
|
+
".env.ci",
|
|
1415
|
+
".env.docker",
|
|
1416
|
+
".env.vault",
|
|
1417
|
+
".env.keys",
|
|
1418
|
+
".env.backup",
|
|
1419
|
+
".env.bak",
|
|
1420
|
+
".env.old",
|
|
1421
|
+
".env.secret",
|
|
1422
|
+
".env.secrets",
|
|
1423
|
+
".env.override",
|
|
1424
|
+
"*.pem",
|
|
1425
|
+
"*.key",
|
|
1426
|
+
"*.p12",
|
|
1427
|
+
"*.pfx",
|
|
1428
|
+
"*.jks",
|
|
1429
|
+
"*.keystore",
|
|
1430
|
+
"id_rsa",
|
|
1431
|
+
"id_rsa_*",
|
|
1432
|
+
"id_dsa",
|
|
1433
|
+
"id_dsa_*",
|
|
1434
|
+
"id_ecdsa",
|
|
1435
|
+
"id_ecdsa_*",
|
|
1436
|
+
"id_ed25519",
|
|
1437
|
+
"id_ed25519_*",
|
|
1438
|
+
".npmrc",
|
|
1439
|
+
".pypirc",
|
|
1440
|
+
".netrc",
|
|
1441
|
+
".git-credentials",
|
|
1442
|
+
"credentials.json",
|
|
1443
|
+
"service-account*.json",
|
|
1444
|
+
"*.auto.tfvars",
|
|
1445
|
+
"terraform.tfvars",
|
|
1446
|
+
".aws/credentials",
|
|
1447
|
+
"secrets.yml",
|
|
1448
|
+
"secrets.yaml"
|
|
1449
|
+
].map((p) => `Read(**/${p})`)
|
|
1450
|
+
];
|
|
1451
|
+
var FILE_TOOL_PATHS = {
|
|
1452
|
+
Read: ["file_path"],
|
|
1453
|
+
Write: ["file_path"],
|
|
1454
|
+
Edit: ["file_path"],
|
|
1455
|
+
MultiEdit: ["file_path"],
|
|
1456
|
+
NotebookEdit: ["notebook_path"],
|
|
1457
|
+
Grep: ["path"],
|
|
1458
|
+
Glob: ["path"],
|
|
1459
|
+
LS: ["path"]
|
|
1460
|
+
};
|
|
1461
|
+
function secretFileToolDenial(toolName, input) {
|
|
1462
|
+
for (const field of FILE_TOOL_PATHS[toolName] ?? []) {
|
|
1463
|
+
const v = input[field];
|
|
1464
|
+
if (typeof v === "string" && isSecretPath(v)) return SECRET_FILE_MESSAGE;
|
|
1465
|
+
}
|
|
1466
|
+
if (toolName === "Grep" && typeof input.glob === "string" && globTargetsSecrets(input.glob)) return SECRET_FILE_MESSAGE;
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
var HARMLESS_WITH_SECRET = /* @__PURE__ */ new Set(["ls", "stat", "test", "[", "[[", "file", "wc", "rm", "touch", "echo", "printf", "chmod", "mkdir", "realpath", "basename", "dirname", "du", "true", "cd", "pwd"]);
|
|
1470
|
+
var HARMLESS_GIT_WITH_SECRET = /* @__PURE__ */ new Set(["status", "add", "rm", "check-ignore", "ls-files", "update-index"]);
|
|
1471
|
+
var RECURSIVE_SEARCH = /(^|\s)(grep|egrep|fgrep)\s(.*\s)?(-[a-z]*r[a-z]*|--recursive|--dereference-recursive)(\s|$)|(^|\s)(rg|ag|ack|ack-grep)(\s|$)|(^|\s)git\s+(-c\s+\S+\s+)*grep(\s|$)/;
|
|
1472
|
+
function commandSegments(command) {
|
|
1473
|
+
return command.replace(/\\\n/g, " ").split(/\|\||&&|[|;&\n\r`]|\$\(|<\(|>\(|[()]/).map((s) => s.trim()).filter(Boolean);
|
|
1474
|
+
}
|
|
1475
|
+
function programWords(segment) {
|
|
1476
|
+
const words = segment.split(/\s+/).filter(Boolean);
|
|
1477
|
+
let i = 0;
|
|
1478
|
+
for (; ; ) {
|
|
1479
|
+
const w = words[i];
|
|
1480
|
+
if (w === void 0) break;
|
|
1481
|
+
if (/^[a-z_][a-z0-9_]*=/i.test(w)) {
|
|
1482
|
+
i++;
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (["sudo", "doas", "command", "builtin", "exec", "nohup", "time", "nice", "env", "xargs"].includes(w.replace(/^.*\//, ""))) {
|
|
1486
|
+
i++;
|
|
1487
|
+
while (words[i]?.startsWith("-")) i++;
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
return words.slice(i);
|
|
1493
|
+
}
|
|
1494
|
+
function pathPieces(word) {
|
|
1495
|
+
return word.replace(/^[<>]+/, "").split(/[=:@,]/).map((p) => p.replace(/^[<>]+/, "")).filter(Boolean);
|
|
1496
|
+
}
|
|
1497
|
+
function secretFileCommandDenial(command) {
|
|
1498
|
+
const n = normalizeCommand(command);
|
|
1499
|
+
const segments2 = commandSegments(n);
|
|
1500
|
+
const words = n.split(/[\s|;&()`]+/).filter(Boolean);
|
|
1501
|
+
const namesSecret = words.some((w) => pathPieces(w).some(isSecretPath)) || /(^|\s)(source|\.)\s+\S*\.env(\s|$)/.test(n);
|
|
1502
|
+
if (namesSecret) {
|
|
1503
|
+
const onlyHarmless = segments2.every((seg) => {
|
|
1504
|
+
const w = programWords(seg);
|
|
1505
|
+
const p = (w[0] ?? "").replace(/^.*\//, "");
|
|
1506
|
+
if (p === "git") {
|
|
1507
|
+
const sub = w.slice(1).find((x) => !x.startsWith("-"));
|
|
1508
|
+
return !!sub && HARMLESS_GIT_WITH_SECRET.has(sub);
|
|
1509
|
+
}
|
|
1510
|
+
if (p === "cp" || p === "mv" || p === "install") {
|
|
1511
|
+
const args = w.slice(1).filter((x) => !x.startsWith("-"));
|
|
1512
|
+
return args.length >= 2 && !args.slice(0, -1).some((a) => pathPieces(a).some(isSecretPath));
|
|
1513
|
+
}
|
|
1514
|
+
return HARMLESS_WITH_SECRET.has(p);
|
|
1515
|
+
});
|
|
1516
|
+
if (!onlyHarmless || /<\s*[^<\s]/.test(n.replace(/<<-?\s*\S+/g, ""))) return SECRET_FILE_MESSAGE;
|
|
1517
|
+
}
|
|
1518
|
+
const pipelineStarts = n.replace(/\\\n/g, " ").split(/&&|\|\||[;&\n\r`]|\$\(|<\(|>\(|[()]/).map((c) => c.split("|")[0].trim()).filter(Boolean);
|
|
1519
|
+
if (pipelineStarts.some((seg) => RECURSIVE_SEARCH.test(programWords(seg).join(" ")))) {
|
|
1520
|
+
return "Blocked by your organization's protection: secret files are never sent to the model, and a recursive search from a command would read them. Use the Grep tool for this search (it skips secret files), or search specific files by name.";
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
var RECURSIVE_FLAG = /(^|\s)(-[a-z]*r[a-z]*|--recursive)(\s|$)/;
|
|
1525
|
+
var DESTRUCTIVE_SEGMENT = [
|
|
1526
|
+
(w) => w[0] === "rm" && RECURSIVE_FLAG.test(w.slice(1).join(" ")),
|
|
1527
|
+
(w, seg) => w[0] === "find" && /\s-delete(\s|$)/.test(seg),
|
|
1528
|
+
(w, seg) => w[0] === "find" && /\s-exec(dir)?\s+(\S*\/)?rm\s/.test(seg),
|
|
1529
|
+
(w, seg) => w[0] === "git" && /(^|\s)reset\s+(.*\s)?--hard(\s|$)/.test(seg),
|
|
1530
|
+
(w, seg) => w[0] === "git" && /(^|\s)clean\s+(.*\s)?(-[a-z]*f[a-z]*|--force)(\s|$)/.test(seg),
|
|
1531
|
+
(w, seg) => w[0] === "git" && /(^|\s)checkout\s+(.*\s)?(--\s+)?(\.|:\/|\*)(\s|$)/.test(seg),
|
|
1532
|
+
(w, seg) => w[0] === "git" && /(^|\s)restore\s+(.*\s)?(\.|:\/|\*)(\s|$)/.test(seg),
|
|
1533
|
+
(w) => w[0] === "dd",
|
|
1534
|
+
(w) => !!w[0]?.startsWith("mkfs"),
|
|
1535
|
+
(w) => w[0] === "shred",
|
|
1536
|
+
(w, seg) => w[0] === "chmod" && RECURSIVE_FLAG.test(seg) && /(^|\s)(0?777|a\+rwx|ugo\+rwx)(\s|$)/.test(seg),
|
|
1537
|
+
(w, seg) => w[0] === "chown" && RECURSIVE_FLAG.test(seg),
|
|
1538
|
+
(w) => w[0] === "dropdb"
|
|
1539
|
+
];
|
|
1540
|
+
var DOWNLOAD_TO_SHELL = [
|
|
1541
|
+
/\b(curl|wget|fetch)\b[^|;\n]*\|\s*(sudo\s+(-\S+\s+)*)?(env\s+)?(\S*\/)?(sh|bash|zsh|dash|ksh|fish|python3?|perl|ruby|node)(\s|$)/,
|
|
1542
|
+
/\b(sh|bash|zsh|dash|ksh|fish|source|\.)\s+<\(\s*(curl|wget)\b/,
|
|
1543
|
+
/\b(sh|bash|zsh|dash|ksh|fish)\s+-c\s+\$\(\s*(curl|wget)\b/,
|
|
1544
|
+
/\beval\s+\$\(\s*(curl|wget)\b/
|
|
1545
|
+
];
|
|
1546
|
+
function isDestructiveCommand(command) {
|
|
1547
|
+
const n = normalizeCommand(command);
|
|
1548
|
+
if (DOWNLOAD_TO_SHELL.some((re) => re.test(n))) return true;
|
|
1549
|
+
if (/\bdrop\s+(database|table|schema)\b/.test(n)) return true;
|
|
1550
|
+
if (/(^|[\s;&|(])(psql|mysql|mariadb|sqlite3|clickhouse-client|cockroach)\b.*\btruncate\b/.test(n)) return true;
|
|
1551
|
+
return commandSegments(n).some((seg) => {
|
|
1552
|
+
const words = programWords(seg).map((w, i) => i === 0 ? w.replace(/^.*\//, "") : w);
|
|
1553
|
+
return DESTRUCTIVE_SEGMENT.some((test) => test(words, words.join(" ")));
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
var DEPLOY = [
|
|
1557
|
+
/\b(npm|pnpm|yarn|bun)\s+(.*\s)?publish(\s|$)/,
|
|
1558
|
+
/\bcargo\s+publish\b/,
|
|
1559
|
+
/\bgem\s+push\b/,
|
|
1560
|
+
/\bpoetry\s+publish\b/,
|
|
1561
|
+
/\btwine\s+upload\b/,
|
|
1562
|
+
/\b(docker|podman)\s+(image\s+)?push\b/,
|
|
1563
|
+
/\b(terraform|tofu)\s+(.*\s)?(apply|destroy)\b/,
|
|
1564
|
+
/\bpulumi\s+(.*\s)?(up|destroy|update)\b/,
|
|
1565
|
+
/\bcdk\s+(.*\s)?(deploy|destroy)\b/,
|
|
1566
|
+
/\b(serverless|sls)\s+(.*\s)?deploy\b/,
|
|
1567
|
+
/\bkubectl\s+(.*\s)?(apply|delete|rollout)\b/,
|
|
1568
|
+
/\bhelm\s+(.*\s)?(install|upgrade|uninstall)\b/,
|
|
1569
|
+
/(^|\s)vercel(\s+.*)?\s(--prod|deploy)\b/,
|
|
1570
|
+
/\bnetlify\s+(.*\s)?deploy\b/,
|
|
1571
|
+
/\b(fly|flyctl)\s+(.*\s)?deploy\b/,
|
|
1572
|
+
/\bwrangler\s+(.*\s)?(deploy|publish)\b/,
|
|
1573
|
+
/\bgcloud\s+(.*\s)?deploy\b/,
|
|
1574
|
+
/\baz\s+(.*\s)?(deploy|deployment|webapp\s+up)\b/,
|
|
1575
|
+
/\baws\s+(.*\s)?cloudformation\s+(.*\s)?deploy\b/,
|
|
1576
|
+
/\baws\s+(.*\s)?ecs\s+(.*\s)?update-service\b/,
|
|
1577
|
+
/\baws\s+(.*\s)?lambda\s+(.*\s)?update-function-code\b/,
|
|
1578
|
+
/\baws\s+(.*\s)?s3\s+sync\s+(.*\s)?\S+\s+s3:\/\/\S+(\s+-.*)?$/,
|
|
1579
|
+
/\bgh\s+release\s+create\b/,
|
|
1580
|
+
/\beas\s+submit\b/,
|
|
1581
|
+
/\bmvn\b.*\bdeploy\b/,
|
|
1582
|
+
/\bgradlew?\b.*\bpublish/,
|
|
1583
|
+
/\bdotnet\s+nuget\s+push\b/,
|
|
1584
|
+
/\bfirebase\s+(.*\s)?deploy\b/,
|
|
1585
|
+
/\beb\s+deploy\b/
|
|
1586
|
+
];
|
|
1587
|
+
function isDeployCommand(command) {
|
|
1588
|
+
return commandSegments(normalizeCommand(command)).some((seg) => {
|
|
1589
|
+
const words = programWords(seg);
|
|
1590
|
+
const text3 = words.join(" ");
|
|
1591
|
+
return DEPLOY.some((re) => re.test(text3));
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
var PROTECTED_BRANCH = /^(main|master|production|prod|develop|development|release\/.+)$/;
|
|
1595
|
+
function isProtectedBranch(branch2) {
|
|
1596
|
+
return PROTECTED_BRANCH.test(branch2.replace(/^refs\/heads\//, "").toLowerCase());
|
|
1597
|
+
}
|
|
1598
|
+
var PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec", "--signed"]);
|
|
1599
|
+
function gitPushes(command) {
|
|
1600
|
+
const out2 = [];
|
|
1601
|
+
for (const seg of commandSegments(normalizeCommand(command))) {
|
|
1602
|
+
const words = programWords(seg);
|
|
1603
|
+
if ((words[0] ?? "").replace(/^.*\//, "") !== "git") continue;
|
|
1604
|
+
let i = 1;
|
|
1605
|
+
while (i < words.length && words[i].startsWith("-")) i += words[i] === "-C" || words[i] === "-c" ? 2 : 1;
|
|
1606
|
+
if (words[i] !== "push") continue;
|
|
1607
|
+
const t = { branches: [], current: false, force: false, all: false };
|
|
1608
|
+
let tagsOnly = false;
|
|
1609
|
+
const positional = [];
|
|
1610
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
1611
|
+
const w = words[j];
|
|
1612
|
+
if (w === "--") {
|
|
1613
|
+
positional.push(...words.slice(j + 1));
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
if (PUSH_VALUE_FLAGS.has(w)) {
|
|
1617
|
+
j++;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (/^--force(-with-lease|-if-includes)?(=|$)/.test(w) || w === "--mirror") t.force = true;
|
|
1621
|
+
if (w === "--all" || w === "--mirror" || w === "--branches") t.all = true;
|
|
1622
|
+
if (w === "--tags") tagsOnly = true;
|
|
1623
|
+
if (/^-[a-z]+$/.test(w) && !w.startsWith("--") && w.includes("f")) t.force = true;
|
|
1624
|
+
if (w.startsWith("-")) continue;
|
|
1625
|
+
positional.push(w);
|
|
1626
|
+
}
|
|
1627
|
+
const refspecs = positional.slice(1);
|
|
1628
|
+
if (!refspecs.length && !tagsOnly) t.current = true;
|
|
1629
|
+
for (const spec of refspecs) {
|
|
1630
|
+
if (spec.startsWith("+")) t.force = true;
|
|
1631
|
+
const s = spec.replace(/^\+/, "");
|
|
1632
|
+
const dst = s.includes(":") ? s.slice(s.indexOf(":") + 1) : s;
|
|
1633
|
+
const name = dst.replace(/^refs\/heads\//, "");
|
|
1634
|
+
if (!name) continue;
|
|
1635
|
+
if (name === "head" || name === "@") t.current = true;
|
|
1636
|
+
else t.branches.push(name);
|
|
1637
|
+
}
|
|
1638
|
+
out2.push(t);
|
|
1639
|
+
}
|
|
1640
|
+
return out2;
|
|
1641
|
+
}
|
|
1642
|
+
async function isProtectedPush(command, currentBranch) {
|
|
1643
|
+
const pushes = gitPushes(command);
|
|
1644
|
+
if (!pushes.length) return false;
|
|
1645
|
+
let current;
|
|
1646
|
+
for (const p of pushes) {
|
|
1647
|
+
if (p.force || p.all || p.branches.some(isProtectedBranch)) return true;
|
|
1648
|
+
if (p.current) {
|
|
1649
|
+
if (current === void 0) current = await currentBranch().catch(() => null);
|
|
1650
|
+
if (!current || current === "head" || current === "HEAD" || isProtectedBranch(current)) return true;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
var NPM_NAME = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
1656
|
+
var PYPI_NAME = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
|
|
1657
|
+
var CRATE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1658
|
+
var NPM_VALUE_FLAGS = /* @__PURE__ */ new Set(["--registry", "--prefix", "-w", "--workspace", "--tag", "--omit", "--include", "--cache", "--userconfig", "-c", "--filter", "-F", "--dir", "-C", "--cwd", "-p", "--package"]);
|
|
1659
|
+
var PIP_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
1660
|
+
"-r",
|
|
1661
|
+
"--requirement",
|
|
1662
|
+
"-c",
|
|
1663
|
+
"--constraint",
|
|
1664
|
+
"-e",
|
|
1665
|
+
"--editable",
|
|
1666
|
+
"-i",
|
|
1667
|
+
"--index-url",
|
|
1668
|
+
"--extra-index-url",
|
|
1669
|
+
"-f",
|
|
1670
|
+
"--find-links",
|
|
1671
|
+
"-t",
|
|
1672
|
+
"--target",
|
|
1673
|
+
"--prefix",
|
|
1674
|
+
"--root",
|
|
1675
|
+
"--python",
|
|
1676
|
+
"-p",
|
|
1677
|
+
"--group",
|
|
1678
|
+
"--optional",
|
|
1679
|
+
"-G",
|
|
1680
|
+
"--source",
|
|
1681
|
+
"--platform",
|
|
1682
|
+
"--python-version",
|
|
1683
|
+
"--implementation",
|
|
1684
|
+
"--abi",
|
|
1685
|
+
"--trusted-host",
|
|
1686
|
+
"--cert",
|
|
1687
|
+
"--client-cert",
|
|
1688
|
+
"--cache-dir",
|
|
1689
|
+
"--src",
|
|
1690
|
+
"--upgrade-strategy",
|
|
1691
|
+
"--progress-bar",
|
|
1692
|
+
"--log",
|
|
1693
|
+
"--timeout",
|
|
1694
|
+
"--retries",
|
|
1695
|
+
"--proxy",
|
|
1696
|
+
"-E",
|
|
1697
|
+
"--extras",
|
|
1698
|
+
"--with",
|
|
1699
|
+
"--dev-group"
|
|
1700
|
+
]);
|
|
1701
|
+
var CARGO_VALUE_FLAGS = /* @__PURE__ */ new Set(["-F", "--features", "--rename", "-p", "--package", "--registry", "--manifest-path", "--target", "--branch", "--tag", "--rev"]);
|
|
1702
|
+
function notRegistryTarget(t) {
|
|
1703
|
+
return /^(\.|\/|~|file:|link:|workspace:|portal:|patch:|git[+:]|github:|gitlab:|bitbucket:|https?:|ssh:)/.test(t) || /\.(tgz|tar\.gz|tar|zip|whl)$/.test(t) || t.includes("://");
|
|
1704
|
+
}
|
|
1705
|
+
function npmName(target) {
|
|
1706
|
+
if (notRegistryTarget(target)) return null;
|
|
1707
|
+
const alias = /^[^@]+@npm:(.+)$/.exec(target) ?? /^@[^/]+\/[^@]+@npm:(.+)$/.exec(target);
|
|
1708
|
+
const t = alias ? alias[1] : target;
|
|
1709
|
+
const scoped = t.startsWith("@");
|
|
1710
|
+
const at = t.indexOf("@", scoped ? 1 : 0);
|
|
1711
|
+
const name = at > 0 ? t.slice(0, at) : t;
|
|
1712
|
+
if (!scoped && name.includes("/")) return null;
|
|
1713
|
+
return NPM_NAME.test(name) ? name : null;
|
|
1714
|
+
}
|
|
1715
|
+
function pypiName(target) {
|
|
1716
|
+
if (notRegistryTarget(target) || target.includes(" @ ")) return null;
|
|
1717
|
+
const name = target.split(/[\[<>=!~;@\s]/)[0];
|
|
1718
|
+
return PYPI_NAME.test(name) ? name : null;
|
|
1719
|
+
}
|
|
1720
|
+
function crateName(target) {
|
|
1721
|
+
const name = target.split("@")[0];
|
|
1722
|
+
return CRATE_NAME.test(name) ? name : null;
|
|
1723
|
+
}
|
|
1724
|
+
function targets(words, valueFlags) {
|
|
1725
|
+
const out2 = [];
|
|
1726
|
+
for (let i = 0; i < words.length; i++) {
|
|
1727
|
+
const w = words[i];
|
|
1728
|
+
if (valueFlags.has(w)) {
|
|
1729
|
+
i++;
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
if (w.startsWith("-")) continue;
|
|
1733
|
+
out2.push(w);
|
|
1734
|
+
}
|
|
1735
|
+
return out2;
|
|
1736
|
+
}
|
|
1737
|
+
function packageInstalls(command, opts = {}) {
|
|
1738
|
+
const out2 = [];
|
|
1739
|
+
const add = (ecosystem, name) => {
|
|
1740
|
+
if (name && !out2.some((p) => p.ecosystem === ecosystem && p.name === name)) out2.push({ ecosystem, name });
|
|
1741
|
+
};
|
|
1742
|
+
for (const seg of commandSegments(normalizeCommand(command))) {
|
|
1743
|
+
let w = programWords(seg);
|
|
1744
|
+
const p = (w[0] ?? "").replace(/^.*\//, "");
|
|
1745
|
+
const rest = w.slice(1);
|
|
1746
|
+
const otherRegistry = rest.some((x) => /^(--registry|--index-url|-i)(=|$)/.test(x));
|
|
1747
|
+
if (otherRegistry) continue;
|
|
1748
|
+
if (["npm", "pnpm", "yarn", "bun"].includes(p)) {
|
|
1749
|
+
const args = targets(rest, NPM_VALUE_FLAGS);
|
|
1750
|
+
let k = 0;
|
|
1751
|
+
if (args[0] === "global") k = 1;
|
|
1752
|
+
const sub = args[k];
|
|
1753
|
+
const installs = sub === "add" || (sub === "install" || sub === "i" || sub === "in") && p !== "yarn";
|
|
1754
|
+
if (installs) for (const t of args.slice(k + 1)) add("npm", npmName(t));
|
|
1755
|
+
if ((p === "pnpm" || p === "yarn") && sub === "dlx" && args[k + 1]) add("npm", npmName(args[k + 1]));
|
|
1756
|
+
if (p === "npm" && (sub === "exec" || sub === "x") && args[k + 1]) {
|
|
1757
|
+
const name = npmName(args[k + 1]);
|
|
1758
|
+
if (name && !opts.installedLocally?.(name)) add("npm", name);
|
|
1759
|
+
}
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (p === "npx" || p === "bunx" || p === "pnpx") {
|
|
1763
|
+
const pkgFlag = rest.findIndex((x) => x === "-p" || x === "--package");
|
|
1764
|
+
if (pkgFlag >= 0 && rest[pkgFlag + 1]) {
|
|
1765
|
+
add("npm", npmName(rest[pkgFlag + 1]));
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
const first = targets(rest, /* @__PURE__ */ new Set(["--registry", "--cache", "--prefix"]))[0];
|
|
1769
|
+
const name = first ? npmName(first) : null;
|
|
1770
|
+
if (name && !opts.installedLocally?.(name)) add("npm", name);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
if (/^python[0-9.]*$/.test(p) && rest[0] === "-m" && /^pip3?$/.test(rest[1] ?? "")) w = [rest[1], ...rest.slice(2)];
|
|
1774
|
+
else if (p === "uv" && rest[0] === "pip") w = ["pip", ...rest.slice(1)];
|
|
1775
|
+
const q = (w[0] ?? "").replace(/^.*\//, "");
|
|
1776
|
+
const r = w.slice(1);
|
|
1777
|
+
if (/^pip3?(\.\d+)?$/.test(q) && r[0] === "install") {
|
|
1778
|
+
for (const t of targets(r.slice(1), PIP_VALUE_FLAGS)) add("pypi", pypiName(t));
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if ((q === "uv" || q === "poetry") && r[0] === "add") {
|
|
1782
|
+
for (const t of targets(r.slice(1), PIP_VALUE_FLAGS)) add("pypi", pypiName(t));
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (q === "cargo" && r[0] === "add") {
|
|
1786
|
+
if (r.some((x) => /^--(git|path)(=|$)/.test(x))) continue;
|
|
1787
|
+
for (const t of targets(r.slice(1), CARGO_VALUE_FLAGS)) add("crates", crateName(t));
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return out2;
|
|
1791
|
+
}
|
|
1792
|
+
var NEW_PACKAGE_DAYS = 30;
|
|
1793
|
+
var REGISTRY_TIMEOUT_MS = 3e3;
|
|
1794
|
+
function registryUrl(p) {
|
|
1795
|
+
if (p.ecosystem === "npm") return `https://registry.npmjs.org/${p.name.replace("/", "%2F")}`;
|
|
1796
|
+
if (p.ecosystem === "pypi") return `https://pypi.org/pypi/${encodeURIComponent(p.name)}/json`;
|
|
1797
|
+
return `https://crates.io/api/v1/crates/${encodeURIComponent(p.name)}`;
|
|
1798
|
+
}
|
|
1799
|
+
function firstPublished(p, body) {
|
|
1800
|
+
let iso3 = null;
|
|
1801
|
+
if (p.ecosystem === "npm") iso3 = body?.time?.created;
|
|
1802
|
+
else if (p.ecosystem === "crates") iso3 = body?.crate?.created_at;
|
|
1803
|
+
else {
|
|
1804
|
+
let min = null;
|
|
1805
|
+
const releases = body?.releases && typeof body.releases === "object" ? Object.values(body.releases) : [];
|
|
1806
|
+
for (const files of releases) {
|
|
1807
|
+
if (!Array.isArray(files)) continue;
|
|
1808
|
+
for (const f of files) {
|
|
1809
|
+
const t2 = Date.parse(String(f?.upload_time_iso_8601 ?? f?.upload_time ?? ""));
|
|
1810
|
+
if (Number.isFinite(t2) && (min === null || t2 < min)) min = t2;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return min === null ? null : new Date(min);
|
|
1814
|
+
}
|
|
1815
|
+
const t = typeof iso3 === "string" ? Date.parse(iso3) : NaN;
|
|
1816
|
+
return Number.isFinite(t) ? new Date(t) : null;
|
|
1817
|
+
}
|
|
1818
|
+
var RegistryChecker = class {
|
|
1819
|
+
constructor(o = {}) {
|
|
1820
|
+
this.o = o;
|
|
1821
|
+
}
|
|
1822
|
+
o;
|
|
1823
|
+
cache = /* @__PURE__ */ new Map();
|
|
1824
|
+
async check(refs) {
|
|
1825
|
+
const out2 = /* @__PURE__ */ new Map();
|
|
1826
|
+
const signal = AbortSignal.timeout(this.o.timeoutMs ?? REGISTRY_TIMEOUT_MS);
|
|
1827
|
+
await Promise.all(refs.map(async (p) => {
|
|
1828
|
+
const key = `${p.ecosystem}:${p.name}`;
|
|
1829
|
+
const cached = this.cache.get(key);
|
|
1830
|
+
if (cached) {
|
|
1831
|
+
out2.set(key, cached);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const status = await this.lookup(p, signal);
|
|
1835
|
+
if (status !== "UNVERIFIED") this.cache.set(key, status);
|
|
1836
|
+
out2.set(key, status);
|
|
1837
|
+
}));
|
|
1838
|
+
return out2;
|
|
1839
|
+
}
|
|
1840
|
+
async lookup(p, signal) {
|
|
1841
|
+
try {
|
|
1842
|
+
const res = await (this.o.fetchImpl ?? fetch)(registryUrl(p), {
|
|
1843
|
+
headers: { accept: "application/json", "user-agent": this.o.userAgent ?? "scalequality-code (+https://scalequality.io)" },
|
|
1844
|
+
signal,
|
|
1845
|
+
redirect: "follow"
|
|
1846
|
+
});
|
|
1847
|
+
if (res.status === 404) return "NOT_FOUND";
|
|
1848
|
+
if (!res.ok) return "UNVERIFIED";
|
|
1849
|
+
const body = await res.json();
|
|
1850
|
+
const first = firstPublished(p, body);
|
|
1851
|
+
if (!first) return "UNVERIFIED";
|
|
1852
|
+
const ageDays = ((this.o.now?.() ?? /* @__PURE__ */ new Date()).getTime() - first.getTime()) / 864e5;
|
|
1853
|
+
return ageDays < NEW_PACKAGE_DAYS ? "TOO_NEW" : "OK";
|
|
1854
|
+
} catch {
|
|
1855
|
+
return "UNVERIFIED";
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
function mayNeedProtection(command, p) {
|
|
1860
|
+
if (p.destructiveCommands && isDestructiveCommand(command)) return true;
|
|
1861
|
+
if (p.deployApproval && isDeployCommand(command)) return true;
|
|
1862
|
+
if (p.protectedPush && gitPushes(command).length > 0) return true;
|
|
1863
|
+
return p.unknownPackage !== null && packageInstalls(command).length > 0;
|
|
1864
|
+
}
|
|
1865
|
+
function packageMessage(name, ecosystem, reason) {
|
|
1866
|
+
const registry = ecosystem === "npm" ? "npm" : ecosystem === "pypi" ? "PyPI" : "crates.io";
|
|
1867
|
+
return reason === "NOT_FOUND" ? `Blocked by your organization's protection: the package "${name}" does not exist in the public ${registry} registry. It was not installed. Check the name (it may be a typo or an invented package) and ask the user before trying another one.` : `Blocked by your organization's protection: the package "${name}" was first published on ${registry} less than ${NEW_PACKAGE_DAYS} days ago. It was not installed. Use an established package or ask the user.`;
|
|
1868
|
+
}
|
|
1869
|
+
async function protectionVerdict(command, p, deps) {
|
|
1870
|
+
const reasons = [];
|
|
1871
|
+
let pkg = null;
|
|
1872
|
+
if (p.unknownPackage) {
|
|
1873
|
+
const refs = packageInstalls(command, { installedLocally: deps.installedLocally });
|
|
1874
|
+
if (refs.length) {
|
|
1875
|
+
const results = await deps.registry.check(refs);
|
|
1876
|
+
const flagged = refs.map((r) => ({ r, status: results.get(`${r.ecosystem}:${r.name}`) ?? "UNVERIFIED" })).filter((x) => x.status !== "OK");
|
|
1877
|
+
const definite = flagged.find((x) => x.status === "NOT_FOUND" || x.status === "TOO_NEW");
|
|
1878
|
+
if (definite && p.unknownPackage === "block") {
|
|
1879
|
+
const reason = definite.status;
|
|
1880
|
+
return { kind: "deny", message: packageMessage(definite.r.name, definite.r.ecosystem, reason), reason: "PROTECTION_UNKNOWN_PACKAGE", package: definite.r.name, packageReason: reason };
|
|
1881
|
+
}
|
|
1882
|
+
const ask = definite ?? (p.unknownPackage === "block" ? flagged[0] : void 0);
|
|
1883
|
+
if (ask) {
|
|
1884
|
+
reasons.push("PROTECTION_UNKNOWN_PACKAGE");
|
|
1885
|
+
pkg = { name: ask.r.name, reason: ask.status };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
if (p.destructiveCommands && isDestructiveCommand(command)) reasons.push("PROTECTION_DESTRUCTIVE_COMMAND");
|
|
1890
|
+
if (p.deployApproval && isDeployCommand(command)) reasons.push("PROTECTION_DEPLOY");
|
|
1891
|
+
if (p.protectedPush && await isProtectedPush(command, deps.currentBranch)) reasons.push("PROTECTION_PROTECTED_PUSH");
|
|
1892
|
+
if (!reasons.length) return { kind: "allow" };
|
|
1893
|
+
return { kind: "ask", reasons, ...pkg ? { package: pkg.name, packageReason: pkg.reason } : {} };
|
|
1894
|
+
}
|
|
1895
|
+
var PROTECTION_MESSAGES = {
|
|
1896
|
+
rejected: "Your organization's protection requires a person to approve this command, and the user denied it. It did not run. Do not run it again or another way unless the user asks.",
|
|
1897
|
+
timedOut: (minutes) => `Your organization's protection requires a person to approve this command, and nobody answered within ${minutes} minutes. It did not run. Ask the user before trying it again.`,
|
|
1898
|
+
stopped: "The request was stopped before the command ran.",
|
|
1899
|
+
unavailable: "Your organization's protection requires a person to approve this command, and the approval could not be requested. It did not run. Tell the user; do not try to run it another way."
|
|
1900
|
+
};
|
|
1901
|
+
|
|
1902
|
+
// src/application/services/workspaceSandbox/SessionTransport.ts
|
|
1903
|
+
var SessionGoneError = class extends Error {
|
|
1904
|
+
constructor(status) {
|
|
1905
|
+
super(`session unavailable (${status})`);
|
|
1906
|
+
this.status = status;
|
|
1907
|
+
this.name = "SessionGoneError";
|
|
1908
|
+
}
|
|
1909
|
+
status;
|
|
1910
|
+
};
|
|
1911
|
+
var TransportError = class extends Error {
|
|
1912
|
+
constructor(message, status, retryable) {
|
|
1913
|
+
super(message);
|
|
1914
|
+
this.status = status;
|
|
1915
|
+
this.retryable = retryable;
|
|
1916
|
+
this.name = "TransportError";
|
|
1917
|
+
}
|
|
1918
|
+
status;
|
|
1919
|
+
retryable;
|
|
1920
|
+
/** The API's error code (e.g. REPOSITORY_NOT_IN_SCOPE), when it sent one. */
|
|
1921
|
+
code;
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
// src/application/services/workspaceSandbox/HttpSessionTransport.ts
|
|
1925
|
+
var GONE = [401, 403, 404, 410];
|
|
1926
|
+
var HttpSessionTransport = class {
|
|
1927
|
+
constructor(opts) {
|
|
1928
|
+
this.opts = opts;
|
|
1929
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
1930
|
+
this.gone = new Set(opts.goneStatuses ?? GONE);
|
|
1931
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
1932
|
+
this.root = `${base}/api/ai-governance/internal/workspace-sessions/${encodeURIComponent(opts.sessionId)}`;
|
|
1933
|
+
}
|
|
1934
|
+
opts;
|
|
1935
|
+
fetchImpl;
|
|
1936
|
+
root;
|
|
1937
|
+
gone;
|
|
1938
|
+
async bootstrap() {
|
|
1939
|
+
const raw = await this.get(`/bootstrap?features=${ENGINE_FEATURES.join(",")}`, this.opts.requestTimeoutMs ?? 3e4);
|
|
1940
|
+
return toSessionBootstrap(raw);
|
|
1941
|
+
}
|
|
1942
|
+
async pollCommands(waitSeconds, signal) {
|
|
1943
|
+
const wait = Math.max(0, Math.min(25, Math.floor(waitSeconds)));
|
|
1944
|
+
const body = await this.get(`/commands?wait=${wait}`, (wait + 15) * 1e3, signal);
|
|
1945
|
+
return Array.isArray(body?.commands) ? body.commands : [];
|
|
1946
|
+
}
|
|
1947
|
+
async postEvents(events) {
|
|
1948
|
+
if (events.length === 0) return;
|
|
1949
|
+
await this.post("/events", { events }, this.opts.requestTimeoutMs ?? 3e4);
|
|
1950
|
+
}
|
|
1951
|
+
callTool(name, args, approvalId) {
|
|
1952
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(name)) throw new TransportError("invalid tool name", null, false);
|
|
1953
|
+
return this.post(`/tools/${name}`, approvalId ? { args, approvalId } : { args }, this.opts.toolTimeoutMs ?? 12e4);
|
|
1954
|
+
}
|
|
1955
|
+
async mcpTools() {
|
|
1956
|
+
const raw = await this.get("/mcp/tools", this.opts.requestTimeoutMs ?? 6e4);
|
|
1957
|
+
return orgMcpCatalogOf(raw);
|
|
1958
|
+
}
|
|
1959
|
+
mcpCall(req) {
|
|
1960
|
+
return this.post("/mcp/call", req, this.opts.toolTimeoutMs ?? 18e4);
|
|
1961
|
+
}
|
|
1962
|
+
openPullRequest(req) {
|
|
1963
|
+
return this.post("/pull-request", req, this.opts.toolTimeoutMs ?? 18e4);
|
|
1964
|
+
}
|
|
1965
|
+
async checkpoint(req) {
|
|
1966
|
+
await this.post("/checkpoint", req, this.opts.requestTimeoutMs ?? 6e4);
|
|
1967
|
+
}
|
|
1968
|
+
async openRepository(repoFullName) {
|
|
1969
|
+
const raw = await this.post("/repositories/open", { repoFullName }, this.opts.requestTimeoutMs ?? 3e4);
|
|
1970
|
+
const text3 = (v, fallback = "") => typeof v === "string" ? v : fallback;
|
|
1971
|
+
const defaultBranch = text3(raw?.defaultBranch, "main");
|
|
1972
|
+
return {
|
|
1973
|
+
cloneUrl: text3(raw?.cloneUrl),
|
|
1974
|
+
scheme: text3(raw?.scheme),
|
|
1975
|
+
token: text3(raw?.token),
|
|
1976
|
+
provider: text3(raw?.provider),
|
|
1977
|
+
repoFullName: text3(raw?.repoFullName, repoFullName),
|
|
1978
|
+
defaultBranch,
|
|
1979
|
+
branch: typeof raw?.branch === "string" ? raw.branch : defaultBranch
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
async importedHistory() {
|
|
1983
|
+
const raw = await this.get("/imported", this.opts.requestTimeoutMs ?? 6e4);
|
|
1984
|
+
const messages = Array.isArray(raw?.messages) ? raw.messages : [];
|
|
1985
|
+
return {
|
|
1986
|
+
messages: messages.filter((m) => !!m && typeof m === "object" && (m.role === "user" || m.role === "assistant")).map((m) => ({
|
|
1987
|
+
role: m.role,
|
|
1988
|
+
text: typeof m.text === "string" ? m.text : "",
|
|
1989
|
+
at: typeof m.at === "string" ? m.at : null,
|
|
1990
|
+
...Array.isArray(m.tools) ? { tools: m.tools.filter((t) => !!t && typeof t.name === "string" && typeof t.summary === "string") } : {}
|
|
1991
|
+
}))
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
async saveTranscript(t) {
|
|
1995
|
+
await this.post("/transcript", t, this.opts.requestTimeoutMs ?? 6e4);
|
|
1996
|
+
}
|
|
1997
|
+
async saveBaseMeasurement(r) {
|
|
1998
|
+
await this.post("/measurements/base", r, this.opts.requestTimeoutMs ?? 3e4);
|
|
1999
|
+
}
|
|
2000
|
+
async reply(commandId, reply) {
|
|
2001
|
+
if (!/^[A-Za-z0-9-]{1,80}$/.test(commandId)) throw new TransportError("invalid command id", null, false);
|
|
2002
|
+
await this.post(`/replies/${commandId}`, reply, this.opts.requestTimeoutMs ?? 3e4);
|
|
2003
|
+
}
|
|
2004
|
+
async requestCommandApproval(body) {
|
|
2005
|
+
const raw = await this.post("/command-approvals", body, this.opts.requestTimeoutMs ?? 3e4);
|
|
2006
|
+
const approvalId = typeof raw?.approvalId === "string" ? raw.approvalId : "";
|
|
2007
|
+
if (!approvalId) throw new TransportError("invalid command approval response", 502, false);
|
|
2008
|
+
return { approvalId };
|
|
2009
|
+
}
|
|
2010
|
+
async cancelCommandApproval(approvalId, reason) {
|
|
2011
|
+
await this.post(`/command-approvals/${encodeURIComponent(approvalId)}/cancel`, { reason }, this.opts.requestTimeoutMs ?? 3e4);
|
|
2012
|
+
}
|
|
2013
|
+
async refreshRuntime() {
|
|
2014
|
+
const raw = await this.post("/runtime", {}, this.opts.requestTimeoutMs ?? 3e4);
|
|
2015
|
+
const boot = toSessionBootstrap({ session: { model: raw?.model, reasoning: raw?.reasoning }, runtime: raw?.runtime, repository: null });
|
|
2016
|
+
return { model: boot.model, runtime: boot.runtime, reasoning: boot.reasoning ?? null };
|
|
2017
|
+
}
|
|
2018
|
+
headers(json) {
|
|
2019
|
+
return {
|
|
2020
|
+
"x-workspace-session-secret": this.opts.secret,
|
|
2021
|
+
accept: "application/json",
|
|
2022
|
+
...this.opts.userAgent ? { "user-agent": this.opts.userAgent } : {},
|
|
2023
|
+
...json ? { "content-type": "application/json" } : {}
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
async get(path, timeoutMs, outer) {
|
|
2027
|
+
const attempts = Math.max(1, this.opts.getAttempts ?? 4);
|
|
2028
|
+
const delay = this.opts.retryDelayMs ?? 1e3;
|
|
2029
|
+
let last;
|
|
2030
|
+
for (let i = 0; i < attempts; i++) {
|
|
2031
|
+
if (outer?.aborted) throw new TransportError("aborted", null, false);
|
|
2032
|
+
try {
|
|
2033
|
+
return await this.request("GET", path, void 0, timeoutMs, outer);
|
|
2034
|
+
} catch (e) {
|
|
2035
|
+
last = e;
|
|
2036
|
+
if (e instanceof SessionGoneError) throw e;
|
|
2037
|
+
if (e instanceof TransportError && !e.retryable) throw e;
|
|
2038
|
+
if (i < attempts - 1) await sleep(delay * 2 ** i, outer);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
throw last;
|
|
2042
|
+
}
|
|
2043
|
+
post(path, body, timeoutMs) {
|
|
2044
|
+
return this.request("POST", path, body, timeoutMs);
|
|
2045
|
+
}
|
|
2046
|
+
async request(method, path, body, timeoutMs, outer) {
|
|
2047
|
+
const signals = [AbortSignal.timeout(timeoutMs), ...outer ? [outer] : []];
|
|
2048
|
+
const signal = signals.length > 1 ? anySignal(signals) : signals[0];
|
|
2049
|
+
let res;
|
|
2050
|
+
try {
|
|
2051
|
+
res = await this.fetchImpl(this.root + path, {
|
|
2052
|
+
method,
|
|
2053
|
+
headers: this.headers(body !== void 0),
|
|
2054
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
2055
|
+
signal
|
|
2056
|
+
});
|
|
2057
|
+
} catch (e) {
|
|
2058
|
+
throw new TransportError(`${method} ${routeLabel(path)} failed: ${e.name}`, null, true);
|
|
2059
|
+
}
|
|
2060
|
+
if (this.gone.has(res.status)) {
|
|
2061
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2062
|
+
throw new SessionGoneError(res.status);
|
|
2063
|
+
}
|
|
2064
|
+
if (!res.ok) {
|
|
2065
|
+
const code = await res.text().then((t) => JSON.parse(t)?.code, () => void 0).catch(() => void 0);
|
|
2066
|
+
const retryable = res.status === 429 || res.status >= 500;
|
|
2067
|
+
const error = new TransportError(`${method} ${routeLabel(path)} answered ${res.status}`, res.status, retryable);
|
|
2068
|
+
if (typeof code === "string" && /^[A-Z_]{3,80}$/.test(code)) error.code = code;
|
|
2069
|
+
throw error;
|
|
2070
|
+
}
|
|
2071
|
+
if (res.status === 204) return void 0;
|
|
2072
|
+
const text3 = await res.text();
|
|
2073
|
+
if (!text3) return void 0;
|
|
2074
|
+
try {
|
|
2075
|
+
return JSON.parse(text3);
|
|
2076
|
+
} catch {
|
|
2077
|
+
throw new TransportError(`${method} ${routeLabel(path)} returned invalid JSON`, res.status, false);
|
|
2078
|
+
}
|
|
1228
2079
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
const content = buf.toString("utf8");
|
|
1239
|
-
if (buf.includes(0) || Buffer.byteLength(content, "utf8") !== buf.length) {
|
|
1240
|
-
out2.skipped.push({ path, reason: "binary" });
|
|
1241
|
-
continue;
|
|
2080
|
+
};
|
|
2081
|
+
function anySignal(signals) {
|
|
2082
|
+
const native = AbortSignal.any;
|
|
2083
|
+
if (typeof native === "function") return native.call(AbortSignal, signals);
|
|
2084
|
+
const ac = new AbortController();
|
|
2085
|
+
for (const s of signals) {
|
|
2086
|
+
if (s.aborted) {
|
|
2087
|
+
ac.abort(s.reason);
|
|
2088
|
+
return ac.signal;
|
|
1242
2089
|
}
|
|
1243
|
-
|
|
2090
|
+
s.addEventListener("abort", () => ac.abort(s.reason), { once: true });
|
|
1244
2091
|
}
|
|
1245
|
-
return
|
|
2092
|
+
return ac.signal;
|
|
1246
2093
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const abs = (0, import_path2.resolve)(root, p);
|
|
1250
|
-
const realRoot = await (0, import_promises2.realpath)(root).catch(() => (0, import_path2.resolve)(root));
|
|
1251
|
-
let probe = abs;
|
|
1252
|
-
while (!(0, import_fs2.existsSync)(probe) && (0, import_path2.dirname)(probe) !== probe) probe = (0, import_path2.dirname)(probe);
|
|
1253
|
-
const realProbe = await (0, import_promises2.realpath)(probe).catch(() => probe);
|
|
1254
|
-
const rest = (0, import_path2.relative)(probe, abs);
|
|
1255
|
-
const finalPath = rest ? (0, import_path2.join)(realProbe, rest) : realProbe;
|
|
1256
|
-
if (finalPath !== realRoot && !finalPath.startsWith(realRoot + import_path2.sep)) return null;
|
|
1257
|
-
return finalPath;
|
|
2094
|
+
function routeLabel(path) {
|
|
2095
|
+
return path.split("?")[0];
|
|
1258
2096
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
await git(["checkout", base, "--", rel], { cwd: root });
|
|
1268
|
-
await git(["reset", "-q", "--", rel], { cwd: root }).catch(() => void 0);
|
|
1269
|
-
return "restored";
|
|
1270
|
-
}
|
|
1271
|
-
await (0, import_promises2.rm)(abs, { recursive: true, force: true });
|
|
1272
|
-
await git(["rm", "-q", "--cached", "--ignore-unmatch", "--", rel], { cwd: root }).catch(() => void 0);
|
|
1273
|
-
return "removed";
|
|
2097
|
+
function sleep(ms, signal) {
|
|
2098
|
+
return new Promise((resolve8) => {
|
|
2099
|
+
const t = setTimeout(resolve8, ms);
|
|
2100
|
+
signal?.addEventListener("abort", () => {
|
|
2101
|
+
clearTimeout(t);
|
|
2102
|
+
resolve8();
|
|
2103
|
+
}, { once: true });
|
|
2104
|
+
});
|
|
1274
2105
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
2106
|
+
var ENGINE_FEATURES = ["message_queue_v1"];
|
|
2107
|
+
function toSessionBootstrap(raw) {
|
|
2108
|
+
if (raw && raw.repo && !raw.repository) return raw;
|
|
2109
|
+
const session = raw?.session ?? {};
|
|
2110
|
+
const repository = raw?.repository && typeof raw.repository === "object" ? raw.repository : null;
|
|
2111
|
+
const runtime = raw?.runtime ?? {};
|
|
2112
|
+
const text3 = (v, fallback = "") => typeof v === "string" ? v : fallback;
|
|
2113
|
+
const defaultBranch = text3(repository?.defaultBranch, "main");
|
|
2114
|
+
const repo2 = repository ? {
|
|
2115
|
+
cloneUrl: text3(repository.cloneUrl),
|
|
2116
|
+
scheme: text3(repository.scheme),
|
|
2117
|
+
token: text3(repository.token),
|
|
2118
|
+
provider: text3(repository.provider, text3(session.provider)),
|
|
2119
|
+
repoFullName: text3(repository.repoFullName, text3(session.repoFullName)),
|
|
2120
|
+
defaultBranch
|
|
2121
|
+
} : null;
|
|
2122
|
+
const repositories = scopeRepos(raw?.repositories) ?? scopeRepos(session.scope?.repos) ?? (repo2 ? [{ repoFullName: repo2.repoFullName, provider: repo2.provider, projectId: text3(session.projectId), defaultBranch: repo2.defaultBranch }] : []);
|
|
2123
|
+
const rawScope = session.scope && typeof session.scope === "object" ? session.scope : null;
|
|
2124
|
+
const kind = rawScope?.kind === "ALL" || rawScope?.kind === "TEAM" || rawScope?.kind === "BUSINESS_AREA" ? rawScope.kind : "PROJECTS";
|
|
2125
|
+
return {
|
|
2126
|
+
repo: repo2,
|
|
2127
|
+
repositories,
|
|
2128
|
+
scope: {
|
|
2129
|
+
kind,
|
|
2130
|
+
teamId: typeof rawScope?.teamId === "string" ? rawScope.teamId : null,
|
|
2131
|
+
projectIds: Array.isArray(rawScope?.projectIds) ? rawScope.projectIds.filter((p) => typeof p === "string") : typeof session.projectId === "string" && session.projectId ? [session.projectId] : [],
|
|
2132
|
+
repos: repositories
|
|
2133
|
+
},
|
|
2134
|
+
branch: text3(session.branch, repo2 ? defaultBranch : ""),
|
|
2135
|
+
projectId: text3(session.projectId),
|
|
2136
|
+
model: text3(session.model, "sq-auto"),
|
|
2137
|
+
runtime: {
|
|
2138
|
+
baseUrl: text3(runtime.baseUrl),
|
|
2139
|
+
token: text3(runtime.token),
|
|
2140
|
+
models: runtime.models,
|
|
2141
|
+
primaryModel: typeof runtime.primaryModel === "string" ? runtime.primaryModel : null,
|
|
2142
|
+
fastModel: typeof runtime.fastModel === "string" ? runtime.fastModel : null,
|
|
2143
|
+
maxOutputTokens: typeof runtime.maxOutputTokens === "number" ? runtime.maxOutputTokens : null,
|
|
2144
|
+
maxOutputTokensCeiling: typeof runtime.maxOutputTokensCeiling === "number" ? runtime.maxOutputTokensCeiling : null,
|
|
2145
|
+
reasoning: parseReasoningCapability(runtime.reasoning),
|
|
2146
|
+
aliases: Array.isArray(runtime.aliases) ? runtime.aliases.filter((a) => typeof a?.alias === "string").map(credentialAlias) : void 0,
|
|
2147
|
+
protections: protectionsWireOf(runtime.protections)
|
|
2148
|
+
},
|
|
2149
|
+
reasoning: isReasoningLevel(session.reasoning) ? session.reasoning : null,
|
|
2150
|
+
imported: importedInfo(session.imported),
|
|
2151
|
+
checkpointPatch: typeof raw?.checkpointPatch === "string" ? raw.checkpointPatch : null,
|
|
2152
|
+
sdkSessionId: typeof session.sdkSessionId === "string" ? session.sdkSessionId : null,
|
|
2153
|
+
workspaceKind: session.workspaceKind === "LOCAL" ? "LOCAL" : "CLOUD",
|
|
2154
|
+
projectName: typeof session.projectName === "string" ? session.projectName : typeof raw?.project?.name === "string" ? raw.project.name : null,
|
|
2155
|
+
folderLink: typeof session.folderLink?.projectId === "string" && session.folderLink.projectId ? { projectId: session.folderLink.projectId } : null,
|
|
2156
|
+
transcript: transcriptOf(raw?.transcript),
|
|
2157
|
+
baseMeasurements: raw?.baseMeasurements && typeof raw.baseMeasurements === "object" && !Array.isArray(raw.baseMeasurements) ? raw.baseMeasurements : {},
|
|
2158
|
+
pullRequests: pullRequestsOf(raw?.pullRequests),
|
|
2159
|
+
skills: skillsOf(raw?.skills),
|
|
2160
|
+
plugins: pluginsOf(raw?.plugins),
|
|
2161
|
+
environments: environmentsOf(raw?.environments)
|
|
2162
|
+
};
|
|
1282
2163
|
}
|
|
1283
|
-
function
|
|
1284
|
-
const
|
|
1285
|
-
if (
|
|
1286
|
-
|
|
1287
|
-
while (start < buf.length && (buf[start] & 192) === 128) start++;
|
|
1288
|
-
return buf.subarray(start).toString("utf8");
|
|
2164
|
+
function transcriptOf(raw) {
|
|
2165
|
+
const t = raw;
|
|
2166
|
+
if (!t || typeof t !== "object" || typeof t.sdkSessionId !== "string" || t.encoding !== "gzip-base64" || typeof t.data !== "string") return null;
|
|
2167
|
+
return { sdkSessionId: t.sdkSessionId, encoding: "gzip-base64", data: t.data, bytes: typeof t.bytes === "number" ? t.bytes : 0, sha256: typeof t.sha256 === "string" ? t.sha256 : "" };
|
|
1289
2168
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
2169
|
+
var strings = (v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => typeof x === "string")) : {};
|
|
2170
|
+
function environmentsOf(raw) {
|
|
2171
|
+
if (!Array.isArray(raw)) return [];
|
|
2172
|
+
return raw.filter((e) => !!e && typeof e === "object" && typeof e.projectId === "string").slice(0, 50).map((e) => ({ projectId: e.projectId, setupCommand: typeof e.setupCommand === "string" && e.setupCommand.trim() ? e.setupCommand : null, env: strings(e.env), secrets: strings(e.secrets) }));
|
|
2173
|
+
}
|
|
2174
|
+
function pluginsOf(raw) {
|
|
2175
|
+
if (!Array.isArray(raw)) return [];
|
|
2176
|
+
return raw.filter((p) => !!p && typeof p === "object" && typeof p.name === "string" && Array.isArray(p.files)).slice(0, 20).map((p) => ({ name: p.name, files: p.files.filter((f) => !!f && typeof f.path === "string" && typeof f.content === "string").slice(0, 300) }));
|
|
2177
|
+
}
|
|
2178
|
+
function skillsOf(raw) {
|
|
2179
|
+
if (!Array.isArray(raw)) return [];
|
|
2180
|
+
return raw.filter((s) => !!s && typeof s === "object").filter((s) => typeof s.name === "string" && typeof s.body === "string").slice(0, 200).map((s) => ({
|
|
2181
|
+
name: s.name,
|
|
2182
|
+
description: typeof s.description === "string" ? s.description : "",
|
|
2183
|
+
body: s.body,
|
|
2184
|
+
argumentHint: typeof s.argumentHint === "string" ? s.argumentHint : null
|
|
2185
|
+
}));
|
|
2186
|
+
}
|
|
2187
|
+
function pullRequestsOf(raw) {
|
|
2188
|
+
const out2 = {};
|
|
2189
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return out2;
|
|
2190
|
+
for (const [repo2, v] of Object.entries(raw)) {
|
|
2191
|
+
if (!v || typeof v.branch !== "string") continue;
|
|
2192
|
+
out2[repo2] = {
|
|
2193
|
+
branch: v.branch,
|
|
2194
|
+
number: typeof v.number === "number" ? v.number : null,
|
|
2195
|
+
url: typeof v.url === "string" ? v.url : null,
|
|
2196
|
+
files: Array.isArray(v.files) ? v.files.filter((f) => typeof f === "string") : []
|
|
2197
|
+
};
|
|
1318
2198
|
}
|
|
1319
2199
|
return out2;
|
|
1320
2200
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const one = `${fileHeader}${hunk.header}
|
|
1334
|
-
${hunk.lines.join("\n")}
|
|
1335
|
-
`;
|
|
1336
|
-
const file = (0, import_path2.join)((0, import_os.tmpdir)(), `sq-hunk-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.patch`);
|
|
1337
|
-
await (0, import_promises2.writeFile)(file, one, "utf8");
|
|
1338
|
-
try {
|
|
1339
|
-
await git(["apply", "-R", "--whitespace=nowarn", file], { cwd: root, env: scope?.env });
|
|
1340
|
-
} finally {
|
|
1341
|
-
await (0, import_promises2.rm)(file, { force: true }).catch(() => void 0);
|
|
1342
|
-
}
|
|
1343
|
-
return "discarded";
|
|
1344
|
-
}, scope);
|
|
2201
|
+
function importedInfo(raw) {
|
|
2202
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2203
|
+
const r = raw;
|
|
2204
|
+
if (r.source !== "CLAUDE_CODE" && r.source !== "CODEX" || !isExternalId(r.externalId)) return null;
|
|
2205
|
+
return {
|
|
2206
|
+
source: r.source,
|
|
2207
|
+
externalId: r.externalId,
|
|
2208
|
+
title: typeof r.title === "string" ? r.title : "",
|
|
2209
|
+
folder: typeof r.folder === "string" ? r.folder : null,
|
|
2210
|
+
messageCount: typeof r.messageCount === "number" ? r.messageCount : 0,
|
|
2211
|
+
nativeResume: r.nativeResume === true
|
|
2212
|
+
};
|
|
1345
2213
|
}
|
|
1346
|
-
|
|
1347
|
-
if (
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
2214
|
+
function scopeRepos(raw) {
|
|
2215
|
+
if (!Array.isArray(raw)) return null;
|
|
2216
|
+
return raw.filter((r) => r && typeof r === "object" && typeof r.repoFullName === "string").map((r) => ({
|
|
2217
|
+
repoFullName: r.repoFullName,
|
|
2218
|
+
provider: typeof r.provider === "string" ? r.provider : "",
|
|
2219
|
+
projectId: typeof r.projectId === "string" ? r.projectId : "",
|
|
2220
|
+
defaultBranch: typeof r.defaultBranch === "string" ? r.defaultBranch : null
|
|
2221
|
+
}));
|
|
2222
|
+
}
|
|
2223
|
+
function orgMcpCatalogOf(raw) {
|
|
2224
|
+
const r = raw ?? {};
|
|
2225
|
+
const servers = (Array.isArray(r.servers) ? r.servers : []).filter((s) => !!s && typeof s === "object" && typeof s.server === "string" && /^[a-z0-9][a-z0-9-]{0,31}$/.test(s.server)).slice(0, 20).map((s) => ({
|
|
2226
|
+
server: s.server,
|
|
2227
|
+
description: typeof s.description === "string" ? s.description : null,
|
|
2228
|
+
approval: typeof s.approval === "string" ? s.approval : "WRITE",
|
|
2229
|
+
tools: (Array.isArray(s.tools) ? s.tools : []).filter((t) => !!t && typeof t === "object" && typeof t.name === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(t.name)).slice(0, 100).map((t) => ({
|
|
2230
|
+
name: t.name,
|
|
2231
|
+
description: typeof t.description === "string" ? t.description : "",
|
|
2232
|
+
inputSchema: t.inputSchema && typeof t.inputSchema === "object" && !Array.isArray(t.inputSchema) ? t.inputSchema : { type: "object" },
|
|
2233
|
+
readOnly: t.readOnly === true
|
|
2234
|
+
}))
|
|
2235
|
+
}));
|
|
2236
|
+
const unavailable = (Array.isArray(r.unavailable) ? r.unavailable : []).filter((u) => !!u && typeof u.server === "string").map((u) => ({ server: String(u.server).slice(0, 32), error: typeof u.error === "string" ? u.error.slice(0, 60) : "MCP_FAILED" }));
|
|
2237
|
+
return { servers, unavailable };
|
|
1362
2238
|
}
|
|
1363
2239
|
|
|
2240
|
+
// src/application/services/workspaceSandbox/localWorkspace.ts
|
|
2241
|
+
var import_promises6 = require("fs/promises");
|
|
2242
|
+
var import_os3 = require("os");
|
|
2243
|
+
var import_path5 = require("path");
|
|
2244
|
+
|
|
1364
2245
|
// src/application/services/workspaceSandbox/localTracking.ts
|
|
1365
|
-
var
|
|
2246
|
+
var import_crypto2 = require("crypto");
|
|
2247
|
+
var import_fs4 = require("fs");
|
|
2248
|
+
var import_promises5 = require("fs/promises");
|
|
2249
|
+
var import_os2 = require("os");
|
|
2250
|
+
var import_path4 = require("path");
|
|
2251
|
+
var posix = (p) => p.split(import_path4.sep).join("/");
|
|
1366
2252
|
function underPrefix(path, prefix) {
|
|
1367
2253
|
if (!prefix) return true;
|
|
1368
2254
|
const clean = path.replace(/^\/+/, "");
|
|
@@ -1428,14 +2314,14 @@ async function discoverRepositories(root, limits = {}) {
|
|
|
1428
2314
|
const children = [];
|
|
1429
2315
|
let isRepo = false;
|
|
1430
2316
|
try {
|
|
1431
|
-
const d = await (0,
|
|
2317
|
+
const d = await (0, import_promises5.opendir)(dir);
|
|
1432
2318
|
for await (const e of d) {
|
|
1433
2319
|
if (e.name === ".git") {
|
|
1434
2320
|
isRepo = true;
|
|
1435
2321
|
continue;
|
|
1436
2322
|
}
|
|
1437
2323
|
if (!e.isDirectory() || e.name.startsWith(".") || DISCOVERY_SKIP.has(e.name)) continue;
|
|
1438
|
-
children.push((0,
|
|
2324
|
+
children.push((0, import_path4.join)(dir, e.name));
|
|
1439
2325
|
}
|
|
1440
2326
|
} catch {
|
|
1441
2327
|
continue;
|
|
@@ -1476,7 +2362,7 @@ async function repoFingerprint(root) {
|
|
|
1476
2362
|
const r = recs[i];
|
|
1477
2363
|
if (!r || r.length < 4) continue;
|
|
1478
2364
|
n++;
|
|
1479
|
-
const st = await (0,
|
|
2365
|
+
const st = await (0, import_promises5.stat)((0, import_path4.join)(root, r.slice(3))).catch(() => null);
|
|
1480
2366
|
h.update(`\0${st ? `${st.size}:${st.mtimeMs}` : "-"}`);
|
|
1481
2367
|
if (r[0] === "R" || r[0] === "C") i++;
|
|
1482
2368
|
}
|
|
@@ -1497,13 +2383,13 @@ var LooseFileTracker = class {
|
|
|
1497
2383
|
return this.o.root;
|
|
1498
2384
|
}
|
|
1499
2385
|
get gitDir() {
|
|
1500
|
-
return (0,
|
|
2386
|
+
return (0, import_path4.join)(this.o.stateDir, "shadow.git");
|
|
1501
2387
|
}
|
|
1502
2388
|
get baseIndex() {
|
|
1503
|
-
return (0,
|
|
2389
|
+
return (0, import_path4.join)(this.o.stateDir, "base.index");
|
|
1504
2390
|
}
|
|
1505
2391
|
get manifest() {
|
|
1506
|
-
return (0,
|
|
2392
|
+
return (0, import_path4.join)(this.o.stateDir, "manifest.json");
|
|
1507
2393
|
}
|
|
1508
2394
|
get env() {
|
|
1509
2395
|
return { GIT_DIR: this.gitDir, GIT_WORK_TREE: this.o.root };
|
|
@@ -1513,8 +2399,8 @@ var LooseFileTracker = class {
|
|
|
1513
2399
|
return this.entries.map((e) => e.path);
|
|
1514
2400
|
}
|
|
1515
2401
|
async init() {
|
|
1516
|
-
await (0,
|
|
1517
|
-
if (!(0,
|
|
2402
|
+
await (0, import_promises5.mkdir)(this.o.stateDir, { recursive: true, mode: 448 });
|
|
2403
|
+
if (!(0, import_fs4.existsSync)((0, import_path4.join)(this.gitDir, "HEAD"))) {
|
|
1518
2404
|
await git(["init", "-q", "--bare", this.gitDir], { cwd: this.o.stateDir });
|
|
1519
2405
|
for (const [k, v] of [
|
|
1520
2406
|
["core.bare", "false"],
|
|
@@ -1527,7 +2413,7 @@ var LooseFileTracker = class {
|
|
|
1527
2413
|
await git(["config", k, v], { cwd: this.o.stateDir, env: { GIT_DIR: this.gitDir } });
|
|
1528
2414
|
}
|
|
1529
2415
|
}
|
|
1530
|
-
const saved = await (0,
|
|
2416
|
+
const saved = await (0, import_promises5.readFile)(this.manifest, "utf8").then((t) => JSON.parse(t), () => null);
|
|
1531
2417
|
if (saved?.version === 1 && saved.root === this.o.root && Array.isArray(saved.entries)) {
|
|
1532
2418
|
this.entries = saved.entries.filter((e) => e && typeof e.path === "string" && typeof e.existed === "boolean");
|
|
1533
2419
|
}
|
|
@@ -1539,12 +2425,12 @@ var LooseFileTracker = class {
|
|
|
1539
2425
|
return next;
|
|
1540
2426
|
}
|
|
1541
2427
|
async track(abs) {
|
|
1542
|
-
const rel = (0,
|
|
1543
|
-
if (!rel || rel.startsWith("..") || (0,
|
|
2428
|
+
const rel = (0, import_path4.relative)(this.o.root, abs);
|
|
2429
|
+
if (!rel || rel.startsWith("..") || (0, import_path4.isAbsolute)(rel)) return;
|
|
1544
2430
|
const path = posix(rel);
|
|
1545
2431
|
if (path.split("/").includes(".git") || this.entries.some((e) => e.path === path) || this.skipped.has(path)) return;
|
|
1546
|
-
if (this.o.excluded().some((r) => abs === r || abs.startsWith(r +
|
|
1547
|
-
const st = await (0,
|
|
2432
|
+
if (this.o.excluded().some((r) => abs === r || abs.startsWith(r + import_path4.sep))) return;
|
|
2433
|
+
const st = await (0, import_promises5.lstat)(abs).catch(() => null);
|
|
1548
2434
|
if (st && !st.isFile()) return;
|
|
1549
2435
|
if (st && st.size > (this.o.maxBytes ?? LOOSE_FILE_MAX_BYTES)) {
|
|
1550
2436
|
this.skipped.add(path);
|
|
@@ -1562,8 +2448,8 @@ var LooseFileTracker = class {
|
|
|
1562
2448
|
async persist() {
|
|
1563
2449
|
const tmp = `${this.manifest}.${process.pid}.tmp`;
|
|
1564
2450
|
const data = { version: 1, root: this.o.root, entries: this.entries };
|
|
1565
|
-
await (0,
|
|
1566
|
-
await (0,
|
|
2451
|
+
await (0, import_promises5.writeFile)(tmp, JSON.stringify(data), { mode: 384 });
|
|
2452
|
+
await (0, import_promises5.rename)(tmp, this.manifest);
|
|
1567
2453
|
}
|
|
1568
2454
|
/** Waits for the copies in progress. */
|
|
1569
2455
|
settled() {
|
|
@@ -1577,8 +2463,8 @@ var LooseFileTracker = class {
|
|
|
1577
2463
|
scope() {
|
|
1578
2464
|
return {
|
|
1579
2465
|
env: this.env,
|
|
1580
|
-
index: (0,
|
|
1581
|
-
paths: this.entries.filter((e) => e.existed || (0,
|
|
2466
|
+
index: (0, import_fs4.existsSync)(this.baseIndex) ? this.baseIndex : null,
|
|
2467
|
+
paths: this.entries.filter((e) => e.existed || (0, import_fs4.existsSync)((0, import_path4.join)(this.o.root, e.path))).map((e) => e.path)
|
|
1582
2468
|
};
|
|
1583
2469
|
}
|
|
1584
2470
|
entry(path) {
|
|
@@ -1600,7 +2486,7 @@ var LooseFileTracker = class {
|
|
|
1600
2486
|
if (e.existed) {
|
|
1601
2487
|
await git(["checkout-index", "-f", "--", e.path], { cwd: this.o.root, env: { ...this.env, GIT_INDEX_FILE: this.baseIndex, GIT_LITERAL_PATHSPECS: "1" } });
|
|
1602
2488
|
} else {
|
|
1603
|
-
await (0,
|
|
2489
|
+
await (0, import_promises5.rm)(abs, { force: true });
|
|
1604
2490
|
}
|
|
1605
2491
|
}
|
|
1606
2492
|
async discardHunk(path, hash) {
|
|
@@ -1623,523 +2509,261 @@ var LooseFileTracker = class {
|
|
|
1623
2509
|
await this.chain;
|
|
1624
2510
|
const snap = JSON.parse(snapshot);
|
|
1625
2511
|
if (!this.entries.length) return;
|
|
1626
|
-
const idx = (0,
|
|
2512
|
+
const idx = (0, import_path4.join)((0, import_os2.tmpdir)(), `sq-loose-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
|
|
1627
2513
|
const env = { ...this.env, GIT_INDEX_FILE: idx };
|
|
1628
2514
|
let target;
|
|
1629
2515
|
try {
|
|
1630
2516
|
if (snap.t) await git(["read-tree", snap.t], { cwd: this.o.root, env });
|
|
1631
|
-
else await git(["read-tree", "--empty"], { cwd: this.o.root, env });
|
|
1632
|
-
for (const e of this.entries.slice(snap.n)) {
|
|
1633
|
-
if (!e.existed) continue;
|
|
1634
|
-
const line = (await git(["ls-files", "-s", "--", e.path], { cwd: this.o.root, env: { ...this.env, GIT_INDEX_FILE: this.baseIndex, GIT_LITERAL_PATHSPECS: "1" } })).trim();
|
|
1635
|
-
const m = /^(\d+) ([0-9a-f]+) \d\t/.exec(line);
|
|
1636
|
-
if (m) await git(["update-index", "--add", "--cacheinfo", `${m[1]},${m[2]},${e.path}`], { cwd: this.o.root, env });
|
|
1637
|
-
}
|
|
1638
|
-
target = (await git(["write-tree"], { cwd: this.o.root, env })).trim();
|
|
1639
|
-
} finally {
|
|
1640
|
-
await (0,
|
|
1641
|
-
}
|
|
1642
|
-
await restoreTree(this.o.root, target, this.scope());
|
|
1643
|
-
}
|
|
1644
|
-
async checkpoint() {
|
|
1645
|
-
await this.chain;
|
|
1646
|
-
if (!this.entries.length) return "";
|
|
1647
|
-
return checkpointPatch(this.o.root, await this.baseTree(), this.scope());
|
|
1648
|
-
}
|
|
1649
|
-
async prFiles(published) {
|
|
1650
|
-
await this.chain;
|
|
1651
|
-
if (!this.entries.length && !published.length) return { files: [], skipped: [] };
|
|
1652
|
-
return filesForPullRequest(this.o.root, await this.baseTree(), published, this.scope());
|
|
1653
|
-
}
|
|
1654
|
-
async treeId() {
|
|
1655
|
-
await this.chain;
|
|
1656
|
-
return this.entries.length ? worktreeTreeId(this.o.root, this.scope()) : null;
|
|
1657
|
-
}
|
|
1658
|
-
};
|
|
1659
|
-
var SESSION_STATE_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
|
|
1660
|
-
async function pruneSessionStates(parent, keep, now = Date.now(), maxAgeMs = SESSION_STATE_MAX_AGE_MS) {
|
|
1661
|
-
const removed = [];
|
|
1662
|
-
for (const name of await (0, import_promises3.readdir)(parent).catch(() => [])) {
|
|
1663
|
-
const dir = (0, import_path3.join)(parent, name);
|
|
1664
|
-
if (dir === keep) continue;
|
|
1665
|
-
const st = await (0, import_promises3.stat)((0, import_path3.join)(dir, "manifest.json")).catch(() => null) ?? await (0, import_promises3.stat)(dir).catch(() => null);
|
|
1666
|
-
if (!st || now - st.mtimeMs < maxAgeMs) continue;
|
|
1667
|
-
await (0, import_promises3.rm)(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
1668
|
-
removed.push(name);
|
|
1669
|
-
}
|
|
1670
|
-
return removed;
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
// src/application/services/workspaceSandbox/localWorkspace.ts
|
|
1674
|
-
var EMPTY_TREE = {
|
|
1675
|
-
sha1: "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
|
|
1676
|
-
sha256: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"
|
|
1677
|
-
};
|
|
1678
|
-
var LocalWorkspaceError = class extends Error {
|
|
1679
|
-
constructor(code, publicMessage) {
|
|
1680
|
-
super(publicMessage);
|
|
1681
|
-
this.code = code;
|
|
1682
|
-
this.publicMessage = publicMessage;
|
|
1683
|
-
this.name = "LocalWorkspaceError";
|
|
1684
|
-
}
|
|
1685
|
-
code;
|
|
1686
|
-
publicMessage;
|
|
1687
|
-
};
|
|
1688
|
-
async function localFolderRoot(dir) {
|
|
1689
|
-
const abs = (0, import_path4.resolve)(dir);
|
|
1690
|
-
const st = await (0, import_promises4.stat)(abs).catch(() => null);
|
|
1691
|
-
if (!st || !st.isDirectory()) throw new LocalWorkspaceError("FOLDER_NOT_FOUND", `The folder ${abs} does not exist.`);
|
|
1692
|
-
return (0, import_promises4.realpath)(abs);
|
|
1693
|
-
}
|
|
1694
|
-
var posix2 = (p) => p.split(import_path4.sep).join("/");
|
|
1695
|
-
async function enclosingRepository(root) {
|
|
1696
|
-
let missing = false;
|
|
1697
|
-
const top = await git(["rev-parse", "--show-toplevel"], { cwd: root }).then((o) => o.trim(), (e) => {
|
|
1698
|
-
if (e?.code === "ENOENT") missing = true;
|
|
1699
|
-
return "";
|
|
1700
|
-
});
|
|
1701
|
-
if (missing) return "gitMissing";
|
|
1702
|
-
if (!top) return null;
|
|
1703
|
-
const realTop = await (0, import_promises4.realpath)(top).catch(() => top);
|
|
1704
|
-
if (realTop === root) return { top: realTop, prefix: "" };
|
|
1705
|
-
const prefix = posix2((0, import_path4.relative)(realTop, root));
|
|
1706
|
-
if (!prefix || prefix.startsWith("..")) return null;
|
|
1707
|
-
const env = { GIT_LITERAL_PATHSPECS: "1" };
|
|
1708
|
-
const inHead = (await git(["ls-tree", "-d", "--name-only", "HEAD", "--", prefix], { cwd: realTop, env }).catch(() => "")).trim().length > 0;
|
|
1709
|
-
if (inHead) return { top: realTop, prefix };
|
|
1710
|
-
const home = await (0, import_promises4.realpath)((0, import_os3.homedir)()).catch(() => (0, import_os3.homedir)());
|
|
1711
|
-
if (realTop === home || (0, import_path4.relative)(realTop, home).split(import_path4.sep)[0] !== "..") return null;
|
|
1712
|
-
const ignored = await git(["check-ignore", "-q", "--", prefix], { cwd: realTop, env }).then(() => true, () => false);
|
|
1713
|
-
return ignored ? null : { top: realTop, prefix };
|
|
1714
|
-
}
|
|
1715
|
-
async function repoHead(top) {
|
|
1716
|
-
const head = (await git(["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: top }).catch(() => "")).trim();
|
|
1717
|
-
let baseRevision = head;
|
|
1718
|
-
let baseKind = "commit";
|
|
1719
|
-
if (!head) {
|
|
1720
|
-
const format = (await git(["rev-parse", "--show-object-format"], { cwd: top }).catch(() => "sha1")).trim();
|
|
1721
|
-
baseRevision = EMPTY_TREE[format] ?? EMPTY_TREE.sha1;
|
|
1722
|
-
baseKind = "empty-tree";
|
|
1723
|
-
}
|
|
1724
|
-
const branch2 = (await git(["symbolic-ref", "--short", "-q", "HEAD"], { cwd: top }).catch(() => "")).trim() || null;
|
|
1725
|
-
const originUrl = (await git(["config", "--get", "remote.origin.url"], { cwd: top }).catch(() => "")).trim() || null;
|
|
1726
|
-
return { baseRevision, baseKind, branch: branch2, originUrl: originUrl ? stripUserinfo(originUrl) : null };
|
|
1727
|
-
}
|
|
1728
|
-
async function nestedRepo(folder, dir) {
|
|
1729
|
-
const real = await (0, import_promises4.realpath)(dir).catch(() => null);
|
|
1730
|
-
if (!real) return null;
|
|
1731
|
-
const top = (await git(["rev-parse", "--show-toplevel"], { cwd: real }).catch(() => "")).trim();
|
|
1732
|
-
if (!top || await (0, import_promises4.realpath)(top).catch(() => top) !== real) return null;
|
|
1733
|
-
return { root: real, rel: posix2((0, import_path4.relative)(folder, real)), ...await repoHead(real) };
|
|
1734
|
-
}
|
|
1735
|
-
async function inspectLocalFolder(dir, opts = {}) {
|
|
1736
|
-
const root = await localFolderRoot(dir);
|
|
1737
|
-
const owner = await enclosingRepository(root);
|
|
1738
|
-
if (owner === null || owner === "gitMissing") {
|
|
1739
|
-
const gitAvailable = owner !== "gitMissing";
|
|
1740
|
-
const found = gitAvailable ? await discoverRepositories(root, opts.discovery) : { repos: [], truncated: false, scannedDirs: 0 };
|
|
1741
|
-
const repos = (await mapLimit(found.repos, 8, (d) => nestedRepo(root, d).catch(() => null))).filter((r) => !!r);
|
|
1742
|
-
return {
|
|
1743
|
-
kind: "folder",
|
|
1744
|
-
root,
|
|
1745
|
-
repoRoot: null,
|
|
1746
|
-
prefix: "",
|
|
1747
|
-
baseRevision: "",
|
|
1748
|
-
baseKind: "commit",
|
|
1749
|
-
branch: null,
|
|
1750
|
-
changedAtStart: 0,
|
|
1751
|
-
originUrl: null,
|
|
1752
|
-
headOnRemote: null,
|
|
1753
|
-
repos,
|
|
1754
|
-
reposTruncated: found.truncated,
|
|
1755
|
-
gitAvailable
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
const { top, prefix } = owner;
|
|
1759
|
-
const head = await repoHead(top);
|
|
1760
|
-
const status = await git(
|
|
1761
|
-
["--no-optional-locks", "status", "--porcelain", "-z", "--untracked-files=all", ...prefix ? ["--", prefix] : []],
|
|
1762
|
-
{ cwd: top, timeoutMs: 5 * 6e4, env: { GIT_LITERAL_PATHSPECS: "1" } }
|
|
1763
|
-
).catch(() => "");
|
|
1764
|
-
const changedAtStart = countPorcelainZ(status);
|
|
1765
|
-
let headOnRemote = null;
|
|
1766
|
-
const anyRemoteBranch = (await git(["branch", "-r"], { cwd: top }).catch(() => "")).trim().length > 0;
|
|
1767
|
-
if (head.baseKind === "commit" && anyRemoteBranch) {
|
|
1768
|
-
const remotes = await git(["branch", "-r", "--contains", head.baseRevision], { cwd: top }).catch(() => null);
|
|
1769
|
-
headOnRemote = remotes === null ? null : remotes.trim().length > 0;
|
|
1770
|
-
}
|
|
1771
|
-
return { kind: prefix ? "subfolder" : "repo", root, repoRoot: top, prefix, ...head, changedAtStart, headOnRemote, repos: [], reposTruncated: false, gitAvailable: true };
|
|
1772
|
-
}
|
|
1773
|
-
function countPorcelainZ(out2) {
|
|
1774
|
-
const recs = out2.split("\0");
|
|
1775
|
-
let n = 0;
|
|
1776
|
-
for (let i = 0; i < recs.length; i++) {
|
|
1777
|
-
const r = recs[i];
|
|
1778
|
-
if (!r || r.length < 4) continue;
|
|
1779
|
-
n++;
|
|
1780
|
-
if (r[0] === "R" || r[0] === "C") i++;
|
|
1781
|
-
}
|
|
1782
|
-
return n;
|
|
1783
|
-
}
|
|
1784
|
-
function remotePathSegments(url) {
|
|
1785
|
-
const u = url.trim();
|
|
1786
|
-
const scp = /^(?:[^@\s/]+@)?([^:\s/]+):(?!\/\/)(.+)$/.exec(u);
|
|
1787
|
-
let host = "";
|
|
1788
|
-
let path = u;
|
|
1789
|
-
if (scp && !/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
1790
|
-
host = scp[1];
|
|
1791
|
-
path = scp[2];
|
|
1792
|
-
} else {
|
|
1793
|
-
try {
|
|
1794
|
-
const parsed = new URL(u);
|
|
1795
|
-
host = parsed.hostname;
|
|
1796
|
-
path = parsed.pathname;
|
|
1797
|
-
} catch {
|
|
1798
|
-
path = u;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
const segments2 = path.split("/").map((s) => {
|
|
1802
|
-
try {
|
|
1803
|
-
return decodeURIComponent(s);
|
|
1804
|
-
} catch {
|
|
1805
|
-
return s;
|
|
1806
|
-
}
|
|
1807
|
-
}).map((s) => s.toLowerCase().replace(/\.git$/, "")).filter((s) => s && s !== "_git" && s !== "v3");
|
|
1808
|
-
return { host: host.toLowerCase(), segments: segments2 };
|
|
1809
|
-
}
|
|
1810
|
-
var PROVIDER_HOSTS = [[/github/, "GITHUB"], [/gitlab/, "GITLAB"], [/bitbucket/, "BITBUCKET"], [/(dev\.azure|visualstudio)/, "AZURE"]];
|
|
1811
|
-
function matchRemoteToScope(originUrl, repos) {
|
|
1812
|
-
if (!originUrl) return null;
|
|
1813
|
-
const { host, segments: segments2 } = remotePathSegments(originUrl);
|
|
1814
|
-
if (!segments2.length) return null;
|
|
1815
|
-
const provider = PROVIDER_HOSTS.find(([re]) => re.test(host))?.[1] ?? null;
|
|
1816
|
-
let best = [];
|
|
1817
|
-
let bestLen = 0;
|
|
1818
|
-
for (const r of repos) {
|
|
1819
|
-
const rs = r.repoFullName.toLowerCase().split("/").map((s) => s.replace(/\.git$/, "")).filter(Boolean);
|
|
1820
|
-
if (!rs.length || rs.length > segments2.length) continue;
|
|
1821
|
-
const offset = segments2.length - rs.length;
|
|
1822
|
-
if (!rs.every((s, i) => s === segments2[offset + i])) continue;
|
|
1823
|
-
if (rs.length > bestLen) {
|
|
1824
|
-
best = [r];
|
|
1825
|
-
bestLen = rs.length;
|
|
1826
|
-
} else if (rs.length === bestLen) best.push(r);
|
|
1827
|
-
}
|
|
1828
|
-
if (best.length > 1 && provider) best = best.filter((r) => r.provider.toUpperCase().startsWith(provider));
|
|
1829
|
-
return best.length === 1 ? best[0] : null;
|
|
1830
|
-
}
|
|
1831
|
-
function localFolderRepo(originUrl, repos, linkedProjectId) {
|
|
1832
|
-
const linked = linkedProjectId ? repos.filter((r) => r.projectId === linkedProjectId) : [];
|
|
1833
|
-
if (linked.length) return { repo: matchRemoteToScope(originUrl, linked) ?? (linked.length === 1 ? linked[0] : null), linked };
|
|
1834
|
-
return { repo: matchRemoteToScope(originUrl, repos), linked: [] };
|
|
1835
|
-
}
|
|
1836
|
-
function bootScopeRepos(boot) {
|
|
1837
|
-
if (boot.scope?.repos) return boot.scope.repos;
|
|
1838
|
-
if (boot.repositories) return boot.repositories;
|
|
1839
|
-
return boot.repo ? [{ repoFullName: boot.repo.repoFullName, provider: boot.repo.provider, projectId: boot.projectId, defaultBranch: boot.repo.defaultBranch }] : [];
|
|
1840
|
-
}
|
|
1841
|
-
function stripUserinfo(url) {
|
|
1842
|
-
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^@/]+@/i, "$1");
|
|
1843
|
-
}
|
|
1844
|
-
async function prepareLocalWorkspace(dir, _boot, onStep) {
|
|
1845
|
-
const t0 = Date.now();
|
|
1846
|
-
onStep?.("Reading the local folder");
|
|
1847
|
-
const local = await inspectLocalFolder(dir);
|
|
1848
|
-
return {
|
|
1849
|
-
baseRevision: local.baseRevision,
|
|
1850
|
-
baseKind: local.baseKind,
|
|
1851
|
-
branch: local.branch ?? "HEAD",
|
|
1852
|
-
restore: "none",
|
|
1853
|
-
originUrl: local.originUrl,
|
|
1854
|
-
timings: { local_folder: Date.now() - t0 },
|
|
1855
|
-
local
|
|
1856
|
-
};
|
|
1857
|
-
}
|
|
1858
|
-
function localWarnings(local, boot) {
|
|
1859
|
-
const out2 = [];
|
|
1860
|
-
if (local.kind === "folder") {
|
|
1861
|
-
if (local.reposTruncated) out2.push(`The folder is large: the repositories inside it were looked for up to ${DISCOVERY_LIMITS.maxDepth} levels down, for a few seconds. Others are still available; their changes are tracked when you edit them here.`);
|
|
1862
|
-
return out2;
|
|
1863
|
-
}
|
|
1864
|
-
const { repo: match, linked } = localFolderRepo(local.originUrl, bootScopeRepos(boot), boot.folderLink?.projectId);
|
|
1865
|
-
const sessionBranch = match ? boot.repo?.repoFullName === match.repoFullName ? boot.branch || boot.repo.defaultBranch : match.defaultBranch : null;
|
|
1866
|
-
if (sessionBranch && local.branch && local.branch !== sessionBranch) {
|
|
1867
|
-
out2.push(`This folder is on branch "${local.branch}", and the session targets "${sessionBranch}". A pull request is opened against "${sessionBranch}" and only when it is at the same commit as this folder.`);
|
|
1868
|
-
}
|
|
1869
|
-
if (!local.branch) out2.push("This folder is on a detached HEAD.");
|
|
1870
|
-
if (local.headOnRemote === false) out2.push("HEAD has commits that are not on any remote branch this folder knows about. Push them first if you plan to open a pull request from this session.");
|
|
1871
|
-
if (local.changedAtStart > 0) out2.push(`${local.changedAtStart} file(s) already differ from HEAD. They are part of this session's change.`);
|
|
1872
|
-
if (linked.length) {
|
|
1873
|
-
if (!match) out2.push(`This folder is linked to a project with several repositories (${linked.map((r) => r.repoFullName).join(", ")}). A pull request names the one it goes to.`);
|
|
1874
|
-
return out2;
|
|
1875
|
-
}
|
|
1876
|
-
if (local.originUrl && !match) {
|
|
1877
|
-
out2.push(`The origin remote (${local.originUrl}) is not a repository in this session's scope. You can work on the code here; a pull request cannot be opened from this folder.`);
|
|
2517
|
+
else await git(["read-tree", "--empty"], { cwd: this.o.root, env });
|
|
2518
|
+
for (const e of this.entries.slice(snap.n)) {
|
|
2519
|
+
if (!e.existed) continue;
|
|
2520
|
+
const line = (await git(["ls-files", "-s", "--", e.path], { cwd: this.o.root, env: { ...this.env, GIT_INDEX_FILE: this.baseIndex, GIT_LITERAL_PATHSPECS: "1" } })).trim();
|
|
2521
|
+
const m = /^(\d+) ([0-9a-f]+) \d\t/.exec(line);
|
|
2522
|
+
if (m) await git(["update-index", "--add", "--cacheinfo", `${m[1]},${m[2]},${e.path}`], { cwd: this.o.root, env });
|
|
2523
|
+
}
|
|
2524
|
+
target = (await git(["write-tree"], { cwd: this.o.root, env })).trim();
|
|
2525
|
+
} finally {
|
|
2526
|
+
await (0, import_promises5.rm)(idx, { force: true }).catch(() => void 0);
|
|
2527
|
+
}
|
|
2528
|
+
await restoreTree(this.o.root, target, this.scope());
|
|
1878
2529
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
// src/application/services/workspaceSandbox/toolPolicy.ts
|
|
1884
|
-
var import_fs4 = require("fs");
|
|
1885
|
-
var import_promises6 = require("fs/promises");
|
|
1886
|
-
var import_path5 = require("path");
|
|
1887
|
-
|
|
1888
|
-
// src/application/services/workspaceSandbox/webFetchGuard.ts
|
|
1889
|
-
var import_promises5 = require("dns/promises");
|
|
1890
|
-
var import_net = require("net");
|
|
1891
|
-
var defaultLookup = (host) => (0, import_promises5.lookup)(host, { all: true, verbatim: true });
|
|
1892
|
-
var ALLOWED_PORTS = /* @__PURE__ */ new Set(["", "80", "443", "8080", "8443"]);
|
|
1893
|
-
function v4Private(ip) {
|
|
1894
|
-
const [a, b] = ip.split(".").map(Number);
|
|
1895
|
-
return a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 192 && b === 0 || a === 198 && (b === 18 || b === 19);
|
|
1896
|
-
}
|
|
1897
|
-
function v6Private(ip) {
|
|
1898
|
-
const s = ip.toLowerCase().replace(/^\[|\]$/g, "");
|
|
1899
|
-
if (s === "::" || s === "::1") return true;
|
|
1900
|
-
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(s);
|
|
1901
|
-
if (mapped) return v4Private(mapped[1]);
|
|
1902
|
-
const head = parseInt(s.split(":")[0] || "0", 16);
|
|
1903
|
-
return (head & 65024) === 64512 || (head & 65472) === 65152 || (head & 65280) === 65280 || s.startsWith("fd00:ec2::");
|
|
1904
|
-
}
|
|
1905
|
-
function privateAddress(ip) {
|
|
1906
|
-
const kind = (0, import_net.isIP)(ip.replace(/^\[|\]$/g, ""));
|
|
1907
|
-
if (kind === 4) return v4Private(ip);
|
|
1908
|
-
if (kind === 6) return v6Private(ip);
|
|
1909
|
-
return true;
|
|
1910
|
-
}
|
|
1911
|
-
async function webFetchDenial(raw, resolve8 = defaultLookup) {
|
|
1912
|
-
if (typeof raw !== "string" || !raw.trim()) return "WebFetch needs a URL.";
|
|
1913
|
-
let url;
|
|
1914
|
-
try {
|
|
1915
|
-
url = new URL(raw.trim());
|
|
1916
|
-
} catch {
|
|
1917
|
-
return "WebFetch needs a valid http or https URL.";
|
|
2530
|
+
async checkpoint() {
|
|
2531
|
+
await this.chain;
|
|
2532
|
+
if (!this.entries.length) return "";
|
|
2533
|
+
return checkpointPatch(this.o.root, await this.baseTree(), this.scope());
|
|
1918
2534
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
if (!host || host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
1924
|
-
return "Addresses of this machine or its private network cannot be fetched from the workspace.";
|
|
2535
|
+
async prFiles(published) {
|
|
2536
|
+
await this.chain;
|
|
2537
|
+
if (!this.entries.length && !published.length) return { files: [], skipped: [] };
|
|
2538
|
+
return filesForPullRequest(this.o.root, await this.baseTree(), published, this.scope());
|
|
1925
2539
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
addresses = await resolve8(host);
|
|
1930
|
-
} catch {
|
|
1931
|
-
return `The host ${host} could not be resolved.`;
|
|
2540
|
+
async treeId() {
|
|
2541
|
+
await this.chain;
|
|
2542
|
+
return this.entries.length ? worktreeTreeId(this.o.root, this.scope()) : null;
|
|
1932
2543
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2544
|
+
};
|
|
2545
|
+
var SESSION_STATE_MAX_AGE_MS = 30 * 24 * 60 * 6e4;
|
|
2546
|
+
async function pruneSessionStates(parent, keep, now = Date.now(), maxAgeMs = SESSION_STATE_MAX_AGE_MS) {
|
|
2547
|
+
const removed = [];
|
|
2548
|
+
for (const name of await (0, import_promises5.readdir)(parent).catch(() => [])) {
|
|
2549
|
+
const dir = (0, import_path4.join)(parent, name);
|
|
2550
|
+
if (dir === keep) continue;
|
|
2551
|
+
const st = await (0, import_promises5.stat)((0, import_path4.join)(dir, "manifest.json")).catch(() => null) ?? await (0, import_promises5.stat)(dir).catch(() => null);
|
|
2552
|
+
if (!st || now - st.mtimeMs < maxAgeMs) continue;
|
|
2553
|
+
await (0, import_promises5.rm)(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
2554
|
+
removed.push(name);
|
|
1935
2555
|
}
|
|
1936
|
-
return
|
|
2556
|
+
return removed;
|
|
1937
2557
|
}
|
|
1938
2558
|
|
|
1939
|
-
// src/application/services/workspaceSandbox/
|
|
1940
|
-
var
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
var
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
"get_measurement_findings",
|
|
1951
|
-
"get_agent_options",
|
|
1952
|
-
"get_agent_quote",
|
|
1953
|
-
"get_pr_followup_status",
|
|
1954
|
-
"get_agent_request",
|
|
1955
|
-
"get_measurement_request",
|
|
1956
|
-
"get_measurement_setup",
|
|
1957
|
-
"explain_change_request",
|
|
1958
|
-
"read_scalequality_guide",
|
|
1959
|
-
"list_repositories",
|
|
1960
|
-
"open_repository",
|
|
1961
|
-
"measure_change"
|
|
1962
|
-
]);
|
|
1963
|
-
var BASH_MAX_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1964
|
-
var CLOUD_BASH_DENY = [
|
|
1965
|
-
{ re: /\bgit\b[^\n;&|]*\s(push|send-pack|send-email|request-pull)\b/, why: "Publishing from the workspace is not allowed. Use the open_pull_request tool; it asks the user for approval." },
|
|
1966
|
-
{ re: /\bgit\b[^\n;&|]*\sremote\b/, why: "Git remotes are managed by ScaleQuality and cannot be read or changed from the workspace." }
|
|
1967
|
-
];
|
|
1968
|
-
var BASH_DENY = [
|
|
1969
|
-
{ re: /\bgit\b[^\n;&|]*\scredential\b/, why: "Git credentials are not available in the workspace." },
|
|
1970
|
-
{ re: /\bgit\b[^\n;&|]*\sconfig\b[^\n;&|]*(credential|insteadof|pushinsteadof|extraheader|\bremote\.|\burl\.)/, why: "Git credential and remote settings cannot be changed from the workspace." },
|
|
1971
|
-
{ re: /credential[._-]?helper/, why: "Git credential helpers cannot be configured in the workspace." },
|
|
1972
|
-
{ re: /\/proc\/[^\s'"]*\/(environ|mem)\b/, why: "Process environments are not readable from the workspace." },
|
|
1973
|
-
{ re: /\b(INTERNAL_API_SECRET|WORKSPACE_SESSION_SECRET|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_BASE_URL)\b/, why: "Workspace credentials are not available to commands." },
|
|
1974
|
-
{ re: /(^|[;&|(`]|\$\()\s*(env|printenv|export\s+-p|declare\s+-x|set)\s*($|[;&|)>`])/, why: "Dumping the whole environment is not allowed; read the specific variable your task needs." }
|
|
1975
|
-
];
|
|
1976
|
-
var HOME_PREFIX = String.raw`(~|\$\{?home\}?|\$\{?userprofile\}?|/users/[^/\s]+|/home/[^/\s]+|/root)/`;
|
|
1977
|
-
var LOCAL_BASH_DENY = [
|
|
1978
|
-
{ re: /(^|[\s=:/~])\.(ssh|aws|gnupg)(\/|\s|$)/i, why: "Credential directories on this machine are not readable from the workspace." },
|
|
1979
|
-
{ re: /(^|[\s=:/~])\.(netrc|git-credentials|pgpass)\b/i, why: "Credential files on this machine are not readable from the workspace." },
|
|
1980
|
-
// Projects have their own .npmrc or .docker folder; only the ones in the home directory hold credentials.
|
|
1981
|
-
{ re: new RegExp(`${HOME_PREFIX}\\.(npmrc|yarnrc\\.yml|pypirc|docker|kube|azure|config/(gh|hub|gcloud|op))\\b`, "i"), why: "Credential files on this machine are not readable from the workspace." },
|
|
1982
|
-
{ re: /(^|[;&|(`]|\$\()\s*(security\s+(find|dump|export)-|gh\s+auth\s+(token|status\s+-t\b|status\s+--show-token)|op\s+(read|item\s+get)|pass\s+show|secret-tool\s+lookup|aws\s+configure\s+(get|export-credentials)|gcloud\s+auth\s+print-)/i, why: "Credential stores on this machine are not readable from the workspace." }
|
|
1983
|
-
];
|
|
1984
|
-
function normalizeCommand(command) {
|
|
1985
|
-
return command.replace(/\\\n/g, " ").replace(/['"\\]/g, "").replace(/[ \t]+/g, " ").toLowerCase();
|
|
1986
|
-
}
|
|
1987
|
-
function bashDenial(command, opts = {}) {
|
|
1988
|
-
const n = normalizeCommand(command);
|
|
1989
|
-
for (const { re, why } of BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1990
|
-
if (!opts.local) {
|
|
1991
|
-
for (const { re, why } of CLOUD_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
1992
|
-
}
|
|
1993
|
-
if (opts.local) {
|
|
1994
|
-
for (const { re, why } of LOCAL_BASH_DENY) if (re.test(n) || re.test(command)) return why;
|
|
2559
|
+
// src/application/services/workspaceSandbox/localWorkspace.ts
|
|
2560
|
+
var EMPTY_TREE = {
|
|
2561
|
+
sha1: "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
|
|
2562
|
+
sha256: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321"
|
|
2563
|
+
};
|
|
2564
|
+
var LocalWorkspaceError = class extends Error {
|
|
2565
|
+
constructor(code, publicMessage) {
|
|
2566
|
+
super(publicMessage);
|
|
2567
|
+
this.code = code;
|
|
2568
|
+
this.publicMessage = publicMessage;
|
|
2569
|
+
this.name = "LocalWorkspaceError";
|
|
1995
2570
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
return
|
|
2004
|
-
}
|
|
2005
|
-
function inside(root, abs) {
|
|
2006
|
-
return abs === root || abs.startsWith(root + import_path5.sep);
|
|
2571
|
+
code;
|
|
2572
|
+
publicMessage;
|
|
2573
|
+
};
|
|
2574
|
+
async function localFolderRoot(dir) {
|
|
2575
|
+
const abs = (0, import_path5.resolve)(dir);
|
|
2576
|
+
const st = await (0, import_promises6.stat)(abs).catch(() => null);
|
|
2577
|
+
if (!st || !st.isDirectory()) throw new LocalWorkspaceError("FOLDER_NOT_FOUND", `The folder ${abs} does not exist.`);
|
|
2578
|
+
return (0, import_promises6.realpath)(abs);
|
|
2007
2579
|
}
|
|
2008
|
-
var
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
"
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
"wc",
|
|
2015
|
-
"grep",
|
|
2016
|
-
"rg",
|
|
2017
|
-
"ag",
|
|
2018
|
-
"tree",
|
|
2019
|
-
"pwd",
|
|
2020
|
-
"file",
|
|
2021
|
-
"stat",
|
|
2022
|
-
"du",
|
|
2023
|
-
"df",
|
|
2024
|
-
"sort",
|
|
2025
|
-
"uniq",
|
|
2026
|
-
"cut",
|
|
2027
|
-
"tr",
|
|
2028
|
-
"which",
|
|
2029
|
-
"echo",
|
|
2030
|
-
"basename",
|
|
2031
|
-
"dirname",
|
|
2032
|
-
"realpath",
|
|
2033
|
-
"diff",
|
|
2034
|
-
"cloc",
|
|
2035
|
-
"jq",
|
|
2036
|
-
"less",
|
|
2037
|
-
"nl",
|
|
2038
|
-
"column",
|
|
2039
|
-
"date",
|
|
2040
|
-
"uname",
|
|
2041
|
-
"true"
|
|
2042
|
-
]);
|
|
2043
|
-
var READ_ONLY_GIT = /* @__PURE__ */ new Set(["status", "log", "show", "diff", "blame", "branch", "ls-files", "ls-tree", "grep", "rev-parse", "describe", "shortlog", "tag", "cat-file", "reflog"]);
|
|
2044
|
-
function readOnlyCommand(command) {
|
|
2045
|
-
if (/[<>`]|\$\(|\$\{/.test(command)) return false;
|
|
2046
|
-
const segments2 = command.split(/\|\||&&|\||;/).map((s) => s.trim()).filter(Boolean);
|
|
2047
|
-
if (!segments2.length) return false;
|
|
2048
|
-
return segments2.every((seg) => {
|
|
2049
|
-
const words = seg.split(/\s+/);
|
|
2050
|
-
let i = 0;
|
|
2051
|
-
while (i < words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i])) i++;
|
|
2052
|
-
const program = words[i] ?? "";
|
|
2053
|
-
const rest = words.slice(i + 1);
|
|
2054
|
-
if (program === "git") {
|
|
2055
|
-
const sub = rest.find((w) => !w.startsWith("-"));
|
|
2056
|
-
if (!sub || !READ_ONLY_GIT.has(sub)) return false;
|
|
2057
|
-
if (sub === "branch" || sub === "tag") return rest.slice(rest.indexOf(sub) + 1).every((w) => /^(-a|-r|-v|-vv|-l|--list|--all|--show-current|--sort=\S+|--contains|--merged|--no-merged)$/.test(w));
|
|
2058
|
-
return !rest.some((w) => /^(--output|--open-files-in-pager|-O|--ext-diff)(=|$)/.test(w) || /^-O\S/.test(w));
|
|
2059
|
-
}
|
|
2060
|
-
if (program === "find") return !rest.some((w) => /^-(delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/.test(w));
|
|
2061
|
-
if (program === "rg") return !rest.some((w) => /^--pre(=|$)/.test(w));
|
|
2062
|
-
if (program === "sort" || program === "tree") return !rest.some((w) => /^(-o|--output)/.test(w));
|
|
2063
|
-
if (program === "uniq") return rest.filter((w) => !w.startsWith("-")).length <= 1;
|
|
2064
|
-
return READ_ONLY_PROGRAMS.has(program);
|
|
2580
|
+
var posix2 = (p) => p.split(import_path5.sep).join("/");
|
|
2581
|
+
async function enclosingRepository(root) {
|
|
2582
|
+
let missing = false;
|
|
2583
|
+
const top = await git(["rev-parse", "--show-toplevel"], { cwd: root }).then((o) => o.trim(), (e) => {
|
|
2584
|
+
if (e?.code === "ENOENT") missing = true;
|
|
2585
|
+
return "";
|
|
2065
2586
|
});
|
|
2587
|
+
if (missing) return "gitMissing";
|
|
2588
|
+
if (!top) return null;
|
|
2589
|
+
const realTop = await (0, import_promises6.realpath)(top).catch(() => top);
|
|
2590
|
+
if (realTop === root) return { top: realTop, prefix: "" };
|
|
2591
|
+
const prefix = posix2((0, import_path5.relative)(realTop, root));
|
|
2592
|
+
if (!prefix || prefix.startsWith("..")) return null;
|
|
2593
|
+
const env = { GIT_LITERAL_PATHSPECS: "1" };
|
|
2594
|
+
const inHead = (await git(["ls-tree", "-d", "--name-only", "HEAD", "--", prefix], { cwd: realTop, env }).catch(() => "")).trim().length > 0;
|
|
2595
|
+
if (inHead) return { top: realTop, prefix };
|
|
2596
|
+
const home = await (0, import_promises6.realpath)((0, import_os3.homedir)()).catch(() => (0, import_os3.homedir)());
|
|
2597
|
+
if (realTop === home || (0, import_path5.relative)(realTop, home).split(import_path5.sep)[0] !== "..") return null;
|
|
2598
|
+
const ignored = await git(["check-ignore", "-q", "--", prefix], { cwd: realTop, env }).then(() => true, () => false);
|
|
2599
|
+
return ignored ? null : { top: realTop, prefix };
|
|
2066
2600
|
}
|
|
2067
|
-
async function
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2601
|
+
async function repoHead(top) {
|
|
2602
|
+
const head = (await git(["rev-parse", "--verify", "-q", "HEAD^{commit}"], { cwd: top }).catch(() => "")).trim();
|
|
2603
|
+
let baseRevision = head;
|
|
2604
|
+
let baseKind = "commit";
|
|
2605
|
+
if (!head) {
|
|
2606
|
+
const format = (await git(["rev-parse", "--show-object-format"], { cwd: top }).catch(() => "sha1")).trim();
|
|
2607
|
+
baseRevision = EMPTY_TREE[format] ?? EMPTY_TREE.sha1;
|
|
2608
|
+
baseKind = "empty-tree";
|
|
2071
2609
|
}
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2610
|
+
const branch2 = (await git(["symbolic-ref", "--short", "-q", "HEAD"], { cwd: top }).catch(() => "")).trim() || null;
|
|
2611
|
+
const originUrl = (await git(["config", "--get", "remote.origin.url"], { cwd: top }).catch(() => "")).trim() || null;
|
|
2612
|
+
return { baseRevision, baseKind, branch: branch2, originUrl: originUrl ? stripUserinfo(originUrl) : null };
|
|
2613
|
+
}
|
|
2614
|
+
async function nestedRepo(folder, dir) {
|
|
2615
|
+
const real = await (0, import_promises6.realpath)(dir).catch(() => null);
|
|
2616
|
+
if (!real) return null;
|
|
2617
|
+
const top = (await git(["rev-parse", "--show-toplevel"], { cwd: real }).catch(() => "")).trim();
|
|
2618
|
+
if (!top || await (0, import_promises6.realpath)(top).catch(() => top) !== real) return null;
|
|
2619
|
+
return { root: real, rel: posix2((0, import_path5.relative)(folder, real)), ...await repoHead(real) };
|
|
2620
|
+
}
|
|
2621
|
+
async function inspectLocalFolder(dir, opts = {}) {
|
|
2622
|
+
const root = await localFolderRoot(dir);
|
|
2623
|
+
const owner = await enclosingRepository(root);
|
|
2624
|
+
if (owner === null || owner === "gitMissing") {
|
|
2625
|
+
const gitAvailable = owner !== "gitMissing";
|
|
2626
|
+
const found = gitAvailable ? await discoverRepositories(root, opts.discovery) : { repos: [], truncated: false, scannedDirs: 0 };
|
|
2627
|
+
const repos = (await mapLimit(found.repos, 8, (d) => nestedRepo(root, d).catch(() => null))).filter((r) => !!r);
|
|
2628
|
+
return {
|
|
2629
|
+
kind: "folder",
|
|
2630
|
+
root,
|
|
2631
|
+
repoRoot: null,
|
|
2632
|
+
prefix: "",
|
|
2633
|
+
baseRevision: "",
|
|
2634
|
+
baseKind: "commit",
|
|
2635
|
+
branch: null,
|
|
2636
|
+
changedAtStart: 0,
|
|
2637
|
+
originUrl: null,
|
|
2638
|
+
headOnRemote: null,
|
|
2639
|
+
repos,
|
|
2640
|
+
reposTruncated: found.truncated,
|
|
2641
|
+
gitAvailable
|
|
2642
|
+
};
|
|
2076
2643
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2644
|
+
const { top, prefix } = owner;
|
|
2645
|
+
const head = await repoHead(top);
|
|
2646
|
+
const status = await git(
|
|
2647
|
+
["--no-optional-locks", "status", "--porcelain", "-z", "--untracked-files=all", ...prefix ? ["--", prefix] : []],
|
|
2648
|
+
{ cwd: top, timeoutMs: 5 * 6e4, env: { GIT_LITERAL_PATHSPECS: "1" } }
|
|
2649
|
+
).catch(() => "");
|
|
2650
|
+
const changedAtStart = countPorcelainZ(status);
|
|
2651
|
+
let headOnRemote = null;
|
|
2652
|
+
const anyRemoteBranch = (await git(["branch", "-r"], { cwd: top }).catch(() => "")).trim().length > 0;
|
|
2653
|
+
if (head.baseKind === "commit" && anyRemoteBranch) {
|
|
2654
|
+
const remotes = await git(["branch", "-r", "--contains", head.baseRevision], { cwd: top }).catch(() => null);
|
|
2655
|
+
headOnRemote = remotes === null ? null : remotes.trim().length > 0;
|
|
2083
2656
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2657
|
+
return { kind: prefix ? "subfolder" : "repo", root, repoRoot: top, prefix, ...head, changedAtStart, headOnRemote, repos: [], reposTruncated: false, gitAvailable: true };
|
|
2658
|
+
}
|
|
2659
|
+
function countPorcelainZ(out2) {
|
|
2660
|
+
const recs = out2.split("\0");
|
|
2661
|
+
let n = 0;
|
|
2662
|
+
for (let i = 0; i < recs.length; i++) {
|
|
2663
|
+
const r = recs[i];
|
|
2664
|
+
if (!r || r.length < 4) continue;
|
|
2665
|
+
n++;
|
|
2666
|
+
if (r[0] === "R" || r[0] === "C") i++;
|
|
2087
2667
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2668
|
+
return n;
|
|
2669
|
+
}
|
|
2670
|
+
function remotePathSegments(url) {
|
|
2671
|
+
const u = url.trim();
|
|
2672
|
+
const scp = /^(?:[^@\s/]+@)?([^:\s/]+):(?!\/\/)(.+)$/.exec(u);
|
|
2673
|
+
let host = "";
|
|
2674
|
+
let path = u;
|
|
2675
|
+
if (scp && !/^[a-z][a-z0-9+.-]*:\/\//i.test(u)) {
|
|
2676
|
+
host = scp[1];
|
|
2677
|
+
path = scp[2];
|
|
2678
|
+
} else {
|
|
2679
|
+
try {
|
|
2680
|
+
const parsed = new URL(u);
|
|
2681
|
+
host = parsed.hostname;
|
|
2682
|
+
path = parsed.pathname;
|
|
2683
|
+
} catch {
|
|
2684
|
+
path = u;
|
|
2094
2685
|
}
|
|
2095
2686
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
return { behavior: "allow", updatedInput: input };
|
|
2102
|
-
}
|
|
2103
|
-
if (typeof raw !== "string" || raw.length === 0) return { behavior: "deny", message: `${toolName} needs a path inside the repository.` };
|
|
2104
|
-
const abs = await resolveInside(ctx.root, raw);
|
|
2105
|
-
let denied = false;
|
|
2106
|
-
for (const d of ctx.deniedRoots ?? []) {
|
|
2107
|
-
const realDenied = await (0, import_promises6.realpath)(d).catch(() => (0, import_path5.resolve)(d));
|
|
2108
|
-
if (abs && inside(realDenied, abs)) denied = true;
|
|
2109
|
-
}
|
|
2110
|
-
if (abs && !denied) {
|
|
2111
|
-
if (toolName in WRITE_TOOLS) {
|
|
2112
|
-
const realRoot = await (0, import_promises6.realpath)(ctx.root).catch(() => (0, import_path5.resolve)(ctx.root));
|
|
2113
|
-
const rel = (0, import_path5.relative)(realRoot, abs).split(import_path5.sep);
|
|
2114
|
-
if (rel.includes(".git")) return { behavior: "deny", message: "Files under .git cannot be written from the workspace." };
|
|
2115
|
-
}
|
|
2116
|
-
return { behavior: "allow", updatedInput: input };
|
|
2117
|
-
}
|
|
2118
|
-
if (toolName in READ_TOOLS) {
|
|
2119
|
-
for (const extra of ctx.extraReadRoots ?? []) {
|
|
2120
|
-
const e = await resolveInside(extra, raw);
|
|
2121
|
-
const realExtra = await (0, import_promises6.realpath)(extra).catch(() => (0, import_path5.resolve)(extra));
|
|
2122
|
-
if (e && inside(realExtra, e)) return { behavior: "allow", updatedInput: input };
|
|
2123
|
-
}
|
|
2687
|
+
const segments2 = path.split("/").map((s) => {
|
|
2688
|
+
try {
|
|
2689
|
+
return decodeURIComponent(s);
|
|
2690
|
+
} catch {
|
|
2691
|
+
return s;
|
|
2124
2692
|
}
|
|
2125
|
-
|
|
2693
|
+
}).map((s) => s.toLowerCase().replace(/\.git$/, "")).filter((s) => s && s !== "_git" && s !== "v3");
|
|
2694
|
+
return { host: host.toLowerCase(), segments: segments2 };
|
|
2695
|
+
}
|
|
2696
|
+
var PROVIDER_HOSTS = [[/github/, "GITHUB"], [/gitlab/, "GITLAB"], [/bitbucket/, "BITBUCKET"], [/(dev\.azure|visualstudio)/, "AZURE"]];
|
|
2697
|
+
function matchRemoteToScope(originUrl, repos) {
|
|
2698
|
+
if (!originUrl) return null;
|
|
2699
|
+
const { host, segments: segments2 } = remotePathSegments(originUrl);
|
|
2700
|
+
if (!segments2.length) return null;
|
|
2701
|
+
const provider = PROVIDER_HOSTS.find(([re]) => re.test(host))?.[1] ?? null;
|
|
2702
|
+
let best = [];
|
|
2703
|
+
let bestLen = 0;
|
|
2704
|
+
for (const r of repos) {
|
|
2705
|
+
const rs = r.repoFullName.toLowerCase().split("/").map((s) => s.replace(/\.git$/, "")).filter(Boolean);
|
|
2706
|
+
if (!rs.length || rs.length > segments2.length) continue;
|
|
2707
|
+
const offset = segments2.length - rs.length;
|
|
2708
|
+
if (!rs.every((s, i) => s === segments2[offset + i])) continue;
|
|
2709
|
+
if (rs.length > bestLen) {
|
|
2710
|
+
best = [r];
|
|
2711
|
+
bestLen = rs.length;
|
|
2712
|
+
} else if (rs.length === bestLen) best.push(r);
|
|
2126
2713
|
}
|
|
2127
|
-
if (
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2714
|
+
if (best.length > 1 && provider) best = best.filter((r) => r.provider.toUpperCase().startsWith(provider));
|
|
2715
|
+
return best.length === 1 ? best[0] : null;
|
|
2716
|
+
}
|
|
2717
|
+
function localFolderRepo(originUrl, repos, linkedProjectId) {
|
|
2718
|
+
const linked = linkedProjectId ? repos.filter((r) => r.projectId === linkedProjectId) : [];
|
|
2719
|
+
if (linked.length) return { repo: matchRemoteToScope(originUrl, linked) ?? (linked.length === 1 ? linked[0] : null), linked };
|
|
2720
|
+
return { repo: matchRemoteToScope(originUrl, repos), linked: [] };
|
|
2721
|
+
}
|
|
2722
|
+
function bootScopeRepos(boot) {
|
|
2723
|
+
if (boot.scope?.repos) return boot.scope.repos;
|
|
2724
|
+
if (boot.repositories) return boot.repositories;
|
|
2725
|
+
return boot.repo ? [{ repoFullName: boot.repo.repoFullName, provider: boot.repo.provider, projectId: boot.projectId, defaultBranch: boot.repo.defaultBranch }] : [];
|
|
2726
|
+
}
|
|
2727
|
+
function stripUserinfo(url) {
|
|
2728
|
+
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^@/]+@/i, "$1");
|
|
2729
|
+
}
|
|
2730
|
+
async function prepareLocalWorkspace(dir, _boot, onStep) {
|
|
2731
|
+
const t0 = Date.now();
|
|
2732
|
+
onStep?.("Reading the local folder");
|
|
2733
|
+
const local = await inspectLocalFolder(dir);
|
|
2734
|
+
return {
|
|
2735
|
+
baseRevision: local.baseRevision,
|
|
2736
|
+
baseKind: local.baseKind,
|
|
2737
|
+
branch: local.branch ?? "HEAD",
|
|
2738
|
+
restore: "none",
|
|
2739
|
+
originUrl: local.originUrl,
|
|
2740
|
+
timings: { local_folder: Date.now() - t0 },
|
|
2741
|
+
local
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
function localWarnings(local, boot) {
|
|
2745
|
+
const out2 = [];
|
|
2746
|
+
if (local.kind === "folder") {
|
|
2747
|
+
if (local.reposTruncated) out2.push(`The folder is large: the repositories inside it were looked for up to ${DISCOVERY_LIMITS.maxDepth} levels down, for a few seconds. Others are still available; their changes are tracked when you edit them here.`);
|
|
2748
|
+
return out2;
|
|
2138
2749
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2750
|
+
const { repo: match, linked } = localFolderRepo(local.originUrl, bootScopeRepos(boot), boot.folderLink?.projectId);
|
|
2751
|
+
const sessionBranch = match ? boot.repo?.repoFullName === match.repoFullName ? boot.branch || boot.repo.defaultBranch : match.defaultBranch : null;
|
|
2752
|
+
if (sessionBranch && local.branch && local.branch !== sessionBranch) {
|
|
2753
|
+
out2.push(`This folder is on branch "${local.branch}", and the session targets "${sessionBranch}". A pull request is opened against "${sessionBranch}" and only when it is at the same commit as this folder.`);
|
|
2141
2754
|
}
|
|
2142
|
-
|
|
2755
|
+
if (!local.branch) out2.push("This folder is on a detached HEAD.");
|
|
2756
|
+
if (local.headOnRemote === false) out2.push("HEAD has commits that are not on any remote branch this folder knows about. Push them first if you plan to open a pull request from this session.");
|
|
2757
|
+
if (local.changedAtStart > 0) out2.push(`${local.changedAtStart} file(s) already differ from HEAD. They are part of this session's change.`);
|
|
2758
|
+
if (linked.length) {
|
|
2759
|
+
if (!match) out2.push(`This folder is linked to a project with several repositories (${linked.map((r) => r.repoFullName).join(", ")}). A pull request names the one it goes to.`);
|
|
2760
|
+
return out2;
|
|
2761
|
+
}
|
|
2762
|
+
if (local.originUrl && !match) {
|
|
2763
|
+
out2.push(`The origin remote (${local.originUrl}) is not a repository in this session's scope. You can work on the code here; a pull request cannot be opened from this folder.`);
|
|
2764
|
+
}
|
|
2765
|
+
if (!local.originUrl) out2.push("This folder has no origin remote, so it is not matched to a repository in this session's scope. You can work on the code here; a pull request cannot be opened from this folder.");
|
|
2766
|
+
return out2;
|
|
2143
2767
|
}
|
|
2144
2768
|
|
|
2145
2769
|
// src/application/services/workspaceSandbox/commandRules.ts
|
|
@@ -2395,13 +3019,19 @@ var BrowserCommandGate = class {
|
|
|
2395
3019
|
this.chain = next.catch(() => void 0);
|
|
2396
3020
|
return next;
|
|
2397
3021
|
}
|
|
3022
|
+
checkProtected(command, opts) {
|
|
3023
|
+
const next = this.chain.then(() => this.ask(command, { description: opts.description, signal: opts.signal, protection: opts }));
|
|
3024
|
+
this.chain = next.catch(() => void 0);
|
|
3025
|
+
return next;
|
|
3026
|
+
}
|
|
2398
3027
|
checkEdit(path, opts = {}) {
|
|
2399
3028
|
const next = this.chain.then(() => this.ask(`edit ${path}`, { signal: opts.signal, edit: { path, ...opts.preview ? { preview: opts.preview } : {} } }));
|
|
2400
3029
|
this.chain = next.catch(() => void 0);
|
|
2401
3030
|
return next;
|
|
2402
3031
|
}
|
|
2403
3032
|
async ask(command, opts) {
|
|
2404
|
-
|
|
3033
|
+
const protection = opts.protection;
|
|
3034
|
+
if (!opts.edit && !protection && matchingRule(command, this.allRules())) return { allow: true };
|
|
2405
3035
|
if (opts.signal?.aborted) return { allow: false, message: STOPPED };
|
|
2406
3036
|
let approvalId;
|
|
2407
3037
|
let approved = false;
|
|
@@ -2410,19 +3040,21 @@ var BrowserCommandGate = class {
|
|
|
2410
3040
|
command,
|
|
2411
3041
|
cwd: this.o.root,
|
|
2412
3042
|
...opts.description ? { description: opts.description.slice(0, 1e3) } : {},
|
|
2413
|
-
reasons: opts.edit ? [] : approvalReasons(command),
|
|
2414
|
-
suggestedRule: opts.edit ? null : suggestRule(command),
|
|
2415
|
-
...opts.edit ? { edit: { path: opts.edit.path, ...opts.edit.preview ? { preview: opts.edit.preview.slice(0, 4e3) } : {} } } : {}
|
|
3043
|
+
reasons: opts.edit ? [] : protection ? [...protection.reasons, ...approvalReasons(command)] : approvalReasons(command),
|
|
3044
|
+
suggestedRule: opts.edit || protection ? null : suggestRule(command),
|
|
3045
|
+
...opts.edit ? { edit: { path: opts.edit.path, ...opts.edit.preview ? { preview: opts.edit.preview.slice(0, 4e3) } : {} } } : {},
|
|
3046
|
+
...protection?.package ? { package: protection.package, ...protection.packageReason ? { packageReason: protection.packageReason } : {} } : {}
|
|
2416
3047
|
}));
|
|
2417
3048
|
} catch (e) {
|
|
2418
3049
|
this.o.log?.(`command approval could not be requested: ${e.message}`);
|
|
2419
3050
|
const code = e instanceof CommandApprovalError ? e.code : null;
|
|
3051
|
+
if (protection && code !== "COMMAND_NOT_ALLOWED") return { allow: false, message: PROTECTION_MESSAGES.unavailable };
|
|
2420
3052
|
return {
|
|
2421
3053
|
allow: false,
|
|
2422
3054
|
message: code === "COMMAND_NOT_ALLOWED" ? "This command is not allowed in the ScaleQuality workspace. It did not run; do not try to run it another way." : "The command could not be sent to the user for approval, so it did not run. Tell the user; do not try to run it another way."
|
|
2423
3055
|
};
|
|
2424
3056
|
}
|
|
2425
|
-
if (approved) return { allow: true };
|
|
3057
|
+
if (approved && !protection) return { allow: true };
|
|
2426
3058
|
this.o.onWaiting?.(command);
|
|
2427
3059
|
const timeoutMs = this.o.timeoutMs ?? COMMAND_APPROVAL_TIMEOUT_MS;
|
|
2428
3060
|
const timer = new AbortController();
|
|
@@ -2433,12 +3065,14 @@ var BrowserCommandGate = class {
|
|
|
2433
3065
|
if (!decision) {
|
|
2434
3066
|
const reason = opts.signal?.aborted ? "STOPPED" : "TIMED_OUT";
|
|
2435
3067
|
void this.o.api.cancel(approvalId, reason).catch(() => void 0);
|
|
3068
|
+
if (protection) return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
|
|
2436
3069
|
return reason === "STOPPED" ? { allow: false, message: STOPPED } : { allow: false, message: `The user did not answer the approval for this command within ${Math.round(timeoutMs / 6e4)} minutes, so it did not run. Ask the user before trying it again.` };
|
|
2437
3070
|
}
|
|
2438
3071
|
if (decision.decision === "REJECT") {
|
|
3072
|
+
if (protection) return { allow: false, message: PROTECTION_MESSAGES.rejected };
|
|
2439
3073
|
return opts.edit ? { allow: false, message: `The user declined this change to ${opts.edit.path} in the browser. The file was not changed. Ask the user what to do differently.` } : { allow: false, message: "The user denied this command in the browser. It did not run. Do not run the same command again unless the user asks; continue another way or ask the user." };
|
|
2440
3074
|
}
|
|
2441
|
-
if (decision.remember) {
|
|
3075
|
+
if (decision.remember && !protection) {
|
|
2442
3076
|
if (this.o.onRemember) this.o.onRemember(decision.remember);
|
|
2443
3077
|
else this.remembered.push(decision.remember);
|
|
2444
3078
|
}
|
|
@@ -2532,6 +3166,37 @@ function parseConnectCode(code) {
|
|
|
2532
3166
|
if (!/^[A-Za-z0-9_\-.~+/=]{32,128}$/.test(secret)) return null;
|
|
2533
3167
|
return { sessionId, secret };
|
|
2534
3168
|
}
|
|
3169
|
+
var NotLocalSessionError = class extends Error {
|
|
3170
|
+
};
|
|
3171
|
+
var LocalSessionTransport = class extends HttpSessionTransport {
|
|
3172
|
+
constructor(local) {
|
|
3173
|
+
super({ goneStatuses: [401, 403, 404, 409, 410], ...local });
|
|
3174
|
+
this.local = local;
|
|
3175
|
+
}
|
|
3176
|
+
local;
|
|
3177
|
+
refused = false;
|
|
3178
|
+
async bootstrap() {
|
|
3179
|
+
try {
|
|
3180
|
+
const boot = await super.bootstrap();
|
|
3181
|
+
if (boot.workspaceKind !== "LOCAL") {
|
|
3182
|
+
this.refused = true;
|
|
3183
|
+
if (boot.repo) boot.repo.token = "";
|
|
3184
|
+
throw new NotLocalSessionError("not a local session");
|
|
3185
|
+
}
|
|
3186
|
+
return boot;
|
|
3187
|
+
} catch (e) {
|
|
3188
|
+
this.local.onBootstrapError?.(e);
|
|
3189
|
+
throw e;
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
async pollCommands(waitSeconds, signal) {
|
|
3193
|
+
return (await super.pollCommands(waitSeconds, signal)).filter((c) => !(c.kind === "approval" && this.local.decisions.offer(c.payload)));
|
|
3194
|
+
}
|
|
3195
|
+
async postEvents(events) {
|
|
3196
|
+
if (this.refused) return;
|
|
3197
|
+
await super.postEvents(events);
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
2535
3200
|
function makeStyle(color) {
|
|
2536
3201
|
const wrap = (open2, close) => (s) => color ? `\x1B[${open2}m${s}\x1B[${close}m` : s;
|
|
2537
3202
|
return { bold: wrap(1, 22), dim: wrap(2, 22), red: wrap(31, 39), green: wrap(32, 39), yellow: wrap(33, 39) };
|
|
@@ -4445,7 +5110,7 @@ function limitInEngineText(text3) {
|
|
|
4445
5110
|
// src/application/services/workspaceSandbox/scalequalityTools.ts
|
|
4446
5111
|
var import_crypto4 = require("crypto");
|
|
4447
5112
|
|
|
4448
|
-
//
|
|
5113
|
+
// node_modules/zod/v3/external.js
|
|
4449
5114
|
var external_exports = {};
|
|
4450
5115
|
__export(external_exports, {
|
|
4451
5116
|
BRAND: () => BRAND,
|
|
@@ -4557,7 +5222,7 @@ __export(external_exports, {
|
|
|
4557
5222
|
void: () => voidType
|
|
4558
5223
|
});
|
|
4559
5224
|
|
|
4560
|
-
//
|
|
5225
|
+
// node_modules/zod/v3/helpers/util.js
|
|
4561
5226
|
var util;
|
|
4562
5227
|
(function(util2) {
|
|
4563
5228
|
util2.assertEqual = (_) => {
|
|
@@ -4691,7 +5356,7 @@ var getParsedType = (data) => {
|
|
|
4691
5356
|
}
|
|
4692
5357
|
};
|
|
4693
5358
|
|
|
4694
|
-
//
|
|
5359
|
+
// node_modules/zod/v3/ZodError.js
|
|
4695
5360
|
var ZodIssueCode = util.arrayToEnum([
|
|
4696
5361
|
"invalid_type",
|
|
4697
5362
|
"invalid_literal",
|
|
@@ -4809,7 +5474,7 @@ ZodError.create = (issues) => {
|
|
|
4809
5474
|
return error;
|
|
4810
5475
|
};
|
|
4811
5476
|
|
|
4812
|
-
//
|
|
5477
|
+
// node_modules/zod/v3/locales/en.js
|
|
4813
5478
|
var errorMap = (issue, _ctx) => {
|
|
4814
5479
|
let message;
|
|
4815
5480
|
switch (issue.code) {
|
|
@@ -4912,7 +5577,7 @@ var errorMap = (issue, _ctx) => {
|
|
|
4912
5577
|
};
|
|
4913
5578
|
var en_default = errorMap;
|
|
4914
5579
|
|
|
4915
|
-
//
|
|
5580
|
+
// node_modules/zod/v3/errors.js
|
|
4916
5581
|
var overrideErrorMap = en_default;
|
|
4917
5582
|
function setErrorMap(map) {
|
|
4918
5583
|
overrideErrorMap = map;
|
|
@@ -4921,7 +5586,7 @@ function getErrorMap() {
|
|
|
4921
5586
|
return overrideErrorMap;
|
|
4922
5587
|
}
|
|
4923
5588
|
|
|
4924
|
-
//
|
|
5589
|
+
// node_modules/zod/v3/helpers/parseUtil.js
|
|
4925
5590
|
var makeIssue = (params) => {
|
|
4926
5591
|
const { data, path, errorMaps, issueData } = params;
|
|
4927
5592
|
const fullPath = [...path, ...issueData.path || []];
|
|
@@ -5031,14 +5696,14 @@ var isDirty = (x) => x.status === "dirty";
|
|
|
5031
5696
|
var isValid = (x) => x.status === "valid";
|
|
5032
5697
|
var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
|
|
5033
5698
|
|
|
5034
|
-
//
|
|
5699
|
+
// node_modules/zod/v3/helpers/errorUtil.js
|
|
5035
5700
|
var errorUtil;
|
|
5036
5701
|
(function(errorUtil2) {
|
|
5037
5702
|
errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
|
|
5038
5703
|
errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
|
|
5039
5704
|
})(errorUtil || (errorUtil = {}));
|
|
5040
5705
|
|
|
5041
|
-
//
|
|
5706
|
+
// node_modules/zod/v3/types.js
|
|
5042
5707
|
var ParseInputLazyPath = class {
|
|
5043
5708
|
constructor(parent, value, path, key) {
|
|
5044
5709
|
this._cachedPath = [];
|
|
@@ -9266,6 +9931,51 @@ var TurnTiming = class {
|
|
|
9266
9931
|
}
|
|
9267
9932
|
};
|
|
9268
9933
|
|
|
9934
|
+
// src/application/services/workspaceSandbox/protectionApproval.ts
|
|
9935
|
+
function gateProtectionApprover(gate) {
|
|
9936
|
+
if (!gate.checkProtected) return null;
|
|
9937
|
+
return { ask: (command, check) => gate.checkProtected(command, check) };
|
|
9938
|
+
}
|
|
9939
|
+
function cloudProtectionApprover(o) {
|
|
9940
|
+
let chain = Promise.resolve();
|
|
9941
|
+
const ask = async (command, check) => {
|
|
9942
|
+
if (check.signal?.aborted) return { allow: false, message: PROTECTION_MESSAGES.stopped };
|
|
9943
|
+
let approvalId;
|
|
9944
|
+
try {
|
|
9945
|
+
({ approvalId } = await o.calls.requestCommandApproval({
|
|
9946
|
+
command,
|
|
9947
|
+
cwd: o.cwd,
|
|
9948
|
+
...check.description ? { description: check.description.slice(0, 1e3) } : {},
|
|
9949
|
+
reasons: [...check.reasons],
|
|
9950
|
+
suggestedRule: null,
|
|
9951
|
+
...check.package ? { package: check.package, ...check.packageReason ? { packageReason: check.packageReason } : {} } : {}
|
|
9952
|
+
}));
|
|
9953
|
+
} catch (e) {
|
|
9954
|
+
o.log?.(`protection approval could not be requested: ${e.message}`);
|
|
9955
|
+
return { allow: false, message: PROTECTION_MESSAGES.unavailable };
|
|
9956
|
+
}
|
|
9957
|
+
const timeoutMs = o.timeoutMs ?? COMMAND_APPROVAL_TIMEOUT_MS;
|
|
9958
|
+
const timer = new AbortController();
|
|
9959
|
+
const t = setTimeout(() => timer.abort(), timeoutMs);
|
|
9960
|
+
t.unref?.();
|
|
9961
|
+
const decision = await o.broker.wait(approvalId, check.signal ? anySignal([check.signal, timer.signal]) : timer.signal);
|
|
9962
|
+
clearTimeout(t);
|
|
9963
|
+
if (!decision) {
|
|
9964
|
+
const reason = check.signal?.aborted ? "STOPPED" : "TIMED_OUT";
|
|
9965
|
+
void o.calls.cancelCommandApproval(approvalId, reason).catch(() => void 0);
|
|
9966
|
+
return { allow: false, message: reason === "STOPPED" ? PROTECTION_MESSAGES.stopped : PROTECTION_MESSAGES.timedOut(Math.round(timeoutMs / 6e4)) };
|
|
9967
|
+
}
|
|
9968
|
+
return decision.decision === "APPROVE" ? { allow: true } : { allow: false, message: PROTECTION_MESSAGES.rejected };
|
|
9969
|
+
};
|
|
9970
|
+
return {
|
|
9971
|
+
ask(command, check) {
|
|
9972
|
+
const next = chain.then(() => ask(command, check));
|
|
9973
|
+
chain = next.catch(() => void 0);
|
|
9974
|
+
return next;
|
|
9975
|
+
}
|
|
9976
|
+
};
|
|
9977
|
+
}
|
|
9978
|
+
|
|
9269
9979
|
// src/application/services/workspaceSandbox/systemPrompt.ts
|
|
9270
9980
|
var FOLDER_LISTED = 60;
|
|
9271
9981
|
function layoutLines(root, l) {
|
|
@@ -10010,6 +10720,9 @@ var WorkspaceEngine = class {
|
|
|
10010
10720
|
redactor = new Redactor();
|
|
10011
10721
|
sink;
|
|
10012
10722
|
broker = new ApprovalBroker();
|
|
10723
|
+
/** Registry answers of unknown-package, kept for the session. */
|
|
10724
|
+
registry = new RegistryChecker();
|
|
10725
|
+
approver;
|
|
10013
10726
|
boot = null;
|
|
10014
10727
|
scope = { kind: "PROJECTS", teamId: null, projectIds: [], repos: [] };
|
|
10015
10728
|
/** Open repositories by folder. */
|
|
@@ -10876,6 +11589,42 @@ ${String(item.content)}` : String(item.content);
|
|
|
10876
11589
|
this.error("MODEL_SWITCH_FAILED", { model: boot.runtime.primaryModel || boot.model });
|
|
10877
11590
|
}
|
|
10878
11591
|
}
|
|
11592
|
+
/**
|
|
11593
|
+
* The organization's protection rules in force: the ones the latest credential brought (bootstrap, model switch,
|
|
11594
|
+
* renewal). None when the Runtime sent none.
|
|
11595
|
+
*/
|
|
11596
|
+
protections() {
|
|
11597
|
+
const raw = this.boot?.runtime?.protections;
|
|
11598
|
+
return raw ? parseProtections(raw) : NO_PROTECTIONS;
|
|
11599
|
+
}
|
|
11600
|
+
/** What the protections read from the workspace: the registries, the checked out branch, the installed packages. */
|
|
11601
|
+
protectionDeps() {
|
|
11602
|
+
return {
|
|
11603
|
+
registry: this.registry,
|
|
11604
|
+
currentBranch: async () => (await git(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: this.deps.root, timeoutMs: 3e3 })).trim() || null,
|
|
11605
|
+
installedLocally: (name) => [this.deps.root, ...[...this.repos.values()].map((r) => r.root)].some((dir) => (0, import_fs9.existsSync)((0, import_path15.join)(dir, "node_modules", name, "package.json")) || (0, import_fs9.existsSync)((0, import_path15.join)(dir, "node_modules", ".bin", name)))
|
|
11606
|
+
};
|
|
11607
|
+
}
|
|
11608
|
+
/**
|
|
11609
|
+
* Who approves a command a protection holds: on a connected computer the browser gate (its decisions come through
|
|
11610
|
+
* the CLI), in the cloud workspace the same card through the internal route, decided on this engine's poll.
|
|
11611
|
+
*/
|
|
11612
|
+
protectionApprover() {
|
|
11613
|
+
if (this.approver !== void 0) return this.approver;
|
|
11614
|
+
const gate = this.deps.commandGate;
|
|
11615
|
+
const transport = this.deps.transport;
|
|
11616
|
+
if (gate) {
|
|
11617
|
+
this.approver = gateProtectionApprover(timedGate(gate, (ms) => this.live?.timing.permission(ms)));
|
|
11618
|
+
} else if (transport.requestCommandApproval && transport.cancelCommandApproval) {
|
|
11619
|
+
this.approver = cloudProtectionApprover({
|
|
11620
|
+
calls: { requestCommandApproval: (b) => transport.requestCommandApproval(b), cancelCommandApproval: (id, r) => transport.cancelCommandApproval(id, r) },
|
|
11621
|
+
broker: this.broker,
|
|
11622
|
+
cwd: this.deps.root,
|
|
11623
|
+
log: (msg) => this.deps.log.warn(this.redactor.text(msg))
|
|
11624
|
+
});
|
|
11625
|
+
} else this.approver = null;
|
|
11626
|
+
return this.approver;
|
|
11627
|
+
}
|
|
10879
11628
|
/**
|
|
10880
11629
|
* A fresh gateway credential for the same model (silent): the limits and protections of now. A failure keeps
|
|
10881
11630
|
* the current one; the turn runs as before.
|
|
@@ -11719,7 +12468,8 @@ ${text3}`;
|
|
|
11719
12468
|
skills: { dir: this.skillsDir(), enabled: enabledSkillNames(t.off.skillEntries), pluginDirs: t.off.pluginDirs },
|
|
11720
12469
|
skillsKey: this.skillsSent,
|
|
11721
12470
|
orgMcpServers: org ? t.off.mcpServers : {},
|
|
11722
|
-
folderWrites: this.localFolder?.kind === "folder"
|
|
12471
|
+
folderWrites: this.localFolder?.kind === "folder",
|
|
12472
|
+
secretFiles: this.protections().secretFiles
|
|
11723
12473
|
};
|
|
11724
12474
|
}
|
|
11725
12475
|
/**
|
|
@@ -11749,6 +12499,9 @@ ${text3}`;
|
|
|
11749
12499
|
...Object.keys(i.orgMcpServers).length ? { orgMcpServers: i.orgMcpServers } : {},
|
|
11750
12500
|
pathToClaudeCodeExecutable: this.deps.pathToClaudeCodeExecutable,
|
|
11751
12501
|
commandGate: gate ? timedGate(gate, (ms) => this.live?.timing.permission(ms)) : void 0,
|
|
12502
|
+
secretFiles: i.secretFiles,
|
|
12503
|
+
protections: () => this.protections(),
|
|
12504
|
+
protection: { deps: this.protectionDeps(), approver: this.protectionApprover() },
|
|
11752
12505
|
beforeTool: async () => {
|
|
11753
12506
|
const t0 = Date.now();
|
|
11754
12507
|
await this.toolGate;
|
|
@@ -11916,6 +12669,8 @@ ${patch}`;
|
|
|
11916
12669
|
/** The engine conversation, without secrets, kept by the API (only when it changed). */
|
|
11917
12670
|
async saveTranscript() {
|
|
11918
12671
|
if (!this.deps.persistTranscript || !this.sdkSessionId || !this.deps.transport.saveTranscript) return;
|
|
12672
|
+
const imported = this.boot?.imported;
|
|
12673
|
+
if (imported?.source === "CLAUDE_CODE" && (this.local && imported.nativeResume || imported.externalId === this.sdkSessionId)) return;
|
|
11919
12674
|
const pack = await packTranscript(this.deps.configDir, this.sdkSessionId, this.redactor);
|
|
11920
12675
|
if (!pack) return;
|
|
11921
12676
|
if ("tooLarge" in pack) {
|
|
@@ -12479,11 +13234,13 @@ function buildQueryOptions(o) {
|
|
|
12479
13234
|
if (name.startsWith("mcp__") && provenance?.source && provenance.source !== "sdk") {
|
|
12480
13235
|
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "Only ScaleQuality tools are available in this workspace." } };
|
|
12481
13236
|
}
|
|
12482
|
-
const
|
|
12483
|
-
const
|
|
13237
|
+
const protections = o.protections?.() ?? NO_PROTECTIONS;
|
|
13238
|
+
const d = await decideToolUse(name, input.tool_input ?? {}, { ...o.policy, protections });
|
|
13239
|
+
const guarded = d.behavior === "allow" && name === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections);
|
|
13240
|
+
const gated = guarded || d.behavior === "allow" && !!gate && (name === "Bash" || !!gate.checkEdit && !o.plan && name in WRITE_TOOL_PATHS);
|
|
12484
13241
|
if (d.behavior === "allow" && !gated) await beforeWrite(name, d.updatedInput);
|
|
12485
13242
|
if (gated) {
|
|
12486
|
-
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: "Commands and changes on this machine follow the person's permission mode." } };
|
|
13243
|
+
return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: guarded ? "This command follows the organization's protections." : "Commands and changes on this machine follow the person's permission mode." } };
|
|
12487
13244
|
}
|
|
12488
13245
|
return d.behavior === "allow" ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: d.updatedInput } } : { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: d.message } };
|
|
12489
13246
|
};
|
|
@@ -12499,8 +13256,28 @@ function buildQueryOptions(o) {
|
|
|
12499
13256
|
o.onPlan?.(text3);
|
|
12500
13257
|
return { behavior: "deny", message: "The plan is now with the user for review in ScaleQuality. Do not continue and do not call more tools: end your turn with one short sentence saying the plan is ready for review." };
|
|
12501
13258
|
}
|
|
12502
|
-
const
|
|
13259
|
+
const protections = o.protections?.() ?? NO_PROTECTIONS;
|
|
13260
|
+
const d = await decideToolUse(toolName, input, { ...o.policy, protections });
|
|
12503
13261
|
if (d.behavior !== "allow") return { behavior: "deny", message: d.message };
|
|
13262
|
+
let protectionApproved = false;
|
|
13263
|
+
if (toolName === "Bash" && mayNeedProtection(String(d.updatedInput.command ?? ""), protections)) {
|
|
13264
|
+
const command = String(d.updatedInput.command ?? "");
|
|
13265
|
+
const signal = opts?.signal ?? o.abortController.signal;
|
|
13266
|
+
if (!o.protection) return { behavior: "deny", message: PROTECTION_MESSAGES.unavailable };
|
|
13267
|
+
const v = await protectionVerdict(command, protections, o.protection.deps);
|
|
13268
|
+
if (v.kind === "deny") return { behavior: "deny", message: v.message };
|
|
13269
|
+
if (v.kind === "ask") {
|
|
13270
|
+
const approver = o.protection.approver;
|
|
13271
|
+
const r = approver ? await approver.ask(command, {
|
|
13272
|
+
reasons: v.reasons,
|
|
13273
|
+
...v.package ? { package: v.package, packageReason: v.packageReason } : {},
|
|
13274
|
+
description: typeof input.description === "string" ? input.description : void 0,
|
|
13275
|
+
signal
|
|
13276
|
+
}) : { allow: false, message: PROTECTION_MESSAGES.unavailable };
|
|
13277
|
+
if (!r.allow) return { behavior: "deny", message: r.message };
|
|
13278
|
+
protectionApproved = true;
|
|
13279
|
+
}
|
|
13280
|
+
}
|
|
12504
13281
|
if (gate?.checkEdit && !o.plan && toolName in WRITE_TOOL_PATHS) {
|
|
12505
13282
|
const field = WRITE_TOOL_PATHS[toolName];
|
|
12506
13283
|
const path = typeof d.updatedInput[field] === "string" ? (0, import_path15.relative)(o.root, d.updatedInput[field]) || String(d.updatedInput[field]) : "";
|
|
@@ -12508,7 +13285,7 @@ function buildQueryOptions(o) {
|
|
|
12508
13285
|
if (!r.allow) return { behavior: "deny", message: r.message };
|
|
12509
13286
|
}
|
|
12510
13287
|
await beforeWrite(toolName, d.updatedInput);
|
|
12511
|
-
if (gate && toolName === "Bash") {
|
|
13288
|
+
if (gate && toolName === "Bash" && !protectionApproved) {
|
|
12512
13289
|
const r = await gate.check(String(d.updatedInput.command ?? ""), {
|
|
12513
13290
|
description: typeof input.description === "string" ? input.description : void 0,
|
|
12514
13291
|
signal: opts?.signal ?? o.abortController.signal
|
|
@@ -12537,7 +13314,8 @@ function buildQueryOptions(o) {
|
|
|
12537
13314
|
tools: o.plan ? PLAN_TOOLS : MODEL_TOOLS,
|
|
12538
13315
|
// No settings file is read (settingSources above); these are ours: WebFetch asks no outside service
|
|
12539
13316
|
// before fetching (the policy already decided), and plan files stay in the engine's own folder.
|
|
12540
|
-
|
|
13317
|
+
// secret-files: Read deny rules, which the engine also applies to its searches (Grep and Glob skip those files).
|
|
13318
|
+
settings: { skipWebFetchPreflight: true, ...o.secretFiles ? { permissions: { deny: [...SECRET_READ_DENY_RULES] } } : {} },
|
|
12541
13319
|
// Skills: the built-in ones that fit the workspace and the plugin the engine built (never the repository's .claude settings).
|
|
12542
13320
|
...o.skills ? { plugins: [o.skills.dir, ...o.skills.pluginDirs ?? []].map((path) => ({ type: "local", path, skipMcpDiscovery: true })), skills: o.skills.enabled } : { skills: [] },
|
|
12543
13321
|
disallowedTools: DENIED_TOOLS,
|
|
@@ -12565,7 +13343,8 @@ function timedGate(gate, took) {
|
|
|
12565
13343
|
};
|
|
12566
13344
|
return {
|
|
12567
13345
|
check: (command, opts) => timed(() => gate.check(command, opts)),
|
|
12568
|
-
...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {}
|
|
13346
|
+
...gate.checkEdit ? { checkEdit: (path, opts) => timed(() => gate.checkEdit(path, opts)) } : {},
|
|
13347
|
+
...gate.checkProtected ? { checkProtected: (command, opts) => timed(() => gate.checkProtected(command, opts)) } : {}
|
|
12569
13348
|
};
|
|
12570
13349
|
}
|
|
12571
13350
|
async function hasLocalTranscript(configDir, sessionId) {
|
|
@@ -12593,8 +13372,6 @@ var HOME = (0, import_os4.homedir)();
|
|
|
12593
13372
|
var SQ_HOME = (0, import_path16.join)(HOME, ".scalequality");
|
|
12594
13373
|
var ENGINE_HOME = (0, import_path16.join)(SQ_HOME, "workspace");
|
|
12595
13374
|
var credentials = new CredentialStore((0, import_path16.join)(SQ_HOME, "credentials.json"));
|
|
12596
|
-
var NotLocalSessionError = class extends Error {
|
|
12597
|
-
};
|
|
12598
13375
|
function startFailure(e, api) {
|
|
12599
13376
|
if (e instanceof NotLocalSessionError) return 'This code belongs to a cloud workspace session. In the AI Workspace, choose "Use a folder on your computer" to get a code for this command.';
|
|
12600
13377
|
if (e instanceof SessionGoneError) {
|
|
@@ -12630,14 +13407,6 @@ function createLocalEngine(o) {
|
|
|
12630
13407
|
if (o.verbose) say(style.dim(`${o.prefix ?? ""}[warn] ${msg} ${ctx ? JSON.stringify(ctx) : ""}`));
|
|
12631
13408
|
}
|
|
12632
13409
|
};
|
|
12633
|
-
const http = new HttpSessionTransport({
|
|
12634
|
-
baseUrl: o.api,
|
|
12635
|
-
sessionId: o.sessionId,
|
|
12636
|
-
secret: o.secret,
|
|
12637
|
-
// 409 is SESSION_CLOSED on the session routes: end instead of retrying.
|
|
12638
|
-
goneStatuses: [401, 403, 404, 409, 410],
|
|
12639
|
-
userAgent: userAgent(o.mode)
|
|
12640
|
-
});
|
|
12641
13410
|
const decisions = new CommandDecisions();
|
|
12642
13411
|
const gate = new BrowserCommandGate({
|
|
12643
13412
|
root: o.root,
|
|
@@ -12649,31 +13418,16 @@ function createLocalEngine(o) {
|
|
|
12649
13418
|
onWaiting: (command) => say(`${o.prefix ?? ""}${style.yellow("Waiting for your approval in the browser:")} $ ${visibleText(command.replace(/\s+/g, " ")).slice(0, 160)}`)
|
|
12650
13419
|
});
|
|
12651
13420
|
let startError = null;
|
|
12652
|
-
|
|
12653
|
-
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
return boot;
|
|
12663
|
-
} catch (e) {
|
|
12664
|
-
startError = startFailure(e, o.api);
|
|
12665
|
-
throw e;
|
|
12666
|
-
}
|
|
12667
|
-
},
|
|
12668
|
-
pollCommands: async (w, s) => (await http.pollCommands(w, s)).filter((c) => !(c.kind === "approval" && decisions.offer(c.payload))),
|
|
12669
|
-
postEvents: (ev) => refused ? Promise.resolve() : http.postEvents(ev),
|
|
12670
|
-
callTool: (n, a, id) => http.callTool(n, a, id),
|
|
12671
|
-
openPullRequest: (r) => http.openPullRequest(r),
|
|
12672
|
-
checkpoint: (r) => http.checkpoint(r),
|
|
12673
|
-
// Never called in local mode (the folder is never cloned); the API refuses it anyway.
|
|
12674
|
-
openRepository: (r) => http.openRepository(r),
|
|
12675
|
-
importedHistory: () => http.importedHistory()
|
|
12676
|
-
};
|
|
13421
|
+
const transport = new LocalSessionTransport({
|
|
13422
|
+
baseUrl: o.api,
|
|
13423
|
+
sessionId: o.sessionId,
|
|
13424
|
+
secret: o.secret,
|
|
13425
|
+
userAgent: userAgent(o.mode),
|
|
13426
|
+
decisions,
|
|
13427
|
+
onBootstrapError: (e) => {
|
|
13428
|
+
startError = startFailure(e, o.api);
|
|
13429
|
+
}
|
|
13430
|
+
});
|
|
12677
13431
|
const sources = defaultImportSources(HOME);
|
|
12678
13432
|
return new WorkspaceEngine({
|
|
12679
13433
|
transport,
|
|
@@ -12689,6 +13443,9 @@ function createLocalEngine(o) {
|
|
|
12689
13443
|
return prepared;
|
|
12690
13444
|
},
|
|
12691
13445
|
createMeasurer: null,
|
|
13446
|
+
// The conversation is kept (without secrets) with each checkpoint, as in the cloud: moved to the cloud or
|
|
13447
|
+
// resumed on another computer, the session continues it. An imported Claude Code conversation never leaves.
|
|
13448
|
+
persistTranscript: true,
|
|
12692
13449
|
// A new conversation is named from its first message, through the gateway with the session's credential.
|
|
12693
13450
|
nameConversation: (r) => gatewayTitle(r),
|
|
12694
13451
|
// The copies of files changed outside any repository of the folder, per session, outside the folder.
|