@spotpatch/dev-server 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1876 -458
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -3
- package/dist/index.d.ts +107 -3
- package/dist/index.js +1535 -101
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ContextualAskError: () => ContextualAskError,
|
|
33
34
|
DEFAULT_EXCLUDE: () => DEFAULT_EXCLUDE,
|
|
34
35
|
DEFAULT_OPTIONS: () => DEFAULT_OPTIONS,
|
|
35
36
|
applyIntegrationPlan: () => applyIntegrationPlan,
|
|
37
|
+
captureAskReadSnapshot: () => captureAskReadSnapshot,
|
|
38
|
+
composeContextualAskExecutors: () => composeContextualAskExecutors,
|
|
36
39
|
createAgentJobManager: () => createAgentJobManager,
|
|
40
|
+
createConfiguredKeyAskExecutors: () => createConfiguredKeyAskExecutors,
|
|
41
|
+
createContextualAskManager: () => createContextualAskManager,
|
|
37
42
|
createExternalHandoffService: () => createExternalHandoffService,
|
|
38
43
|
createIntegrationFileChange: () => createIntegrationFileChange,
|
|
39
44
|
createRuntimeAiConfig: () => createRuntimeAiConfig,
|
|
@@ -42,9 +47,12 @@ __export(index_exports, {
|
|
|
42
47
|
createSourceRegistrationService: () => createSourceRegistrationService,
|
|
43
48
|
createSourceRegistry: () => createSourceRegistry,
|
|
44
49
|
createSpotPatchMiddleware: () => createSpotPatchMiddleware,
|
|
50
|
+
createWorkspaceActivityCoordinator: () => createWorkspaceActivityCoordinator,
|
|
45
51
|
discoverProjectValidationCheck: () => discoverProjectValidationCheck,
|
|
52
|
+
handleContextualAskRequest: () => handleContextualAskRequest,
|
|
46
53
|
integrationPathExists: () => integrationPathExists,
|
|
47
54
|
isLoopbackHostname: () => isLoopbackHostname,
|
|
55
|
+
matchContextualAskPath: () => matchContextualAskPath,
|
|
48
56
|
parseSerializedSpotPatchOptions: () => parseSerializedSpotPatchOptions,
|
|
49
57
|
readIntegrationFile: () => readIntegrationFile,
|
|
50
58
|
readJsonRequestBody: () => readJsonRequestBody,
|
|
@@ -221,6 +229,10 @@ function createAgentJobManager(options) {
|
|
|
221
229
|
job.updatedAt = dependencies.now();
|
|
222
230
|
emitSnapshot(job);
|
|
223
231
|
emitPhase(job, phaseMessage);
|
|
232
|
+
if (!isActive(status)) {
|
|
233
|
+
job.lease?.release();
|
|
234
|
+
job.lease = void 0;
|
|
235
|
+
}
|
|
224
236
|
};
|
|
225
237
|
const probeResolved = async (selection, signal) => {
|
|
226
238
|
const key = capabilityCacheKey(selection.provider, selection.model);
|
|
@@ -410,6 +422,7 @@ function createAgentJobManager(options) {
|
|
|
410
422
|
[...jobs.values()].map((job) => job.runPromise).filter((promise) => promise !== void 0)
|
|
411
423
|
);
|
|
412
424
|
capabilityCache.clear();
|
|
425
|
+
for (const job of jobs.values()) job.lease?.release();
|
|
413
426
|
jobs.clear();
|
|
414
427
|
},
|
|
415
428
|
create(request) {
|
|
@@ -434,8 +447,13 @@ function createAgentJobManager(options) {
|
|
|
434
447
|
request.providerProfileId,
|
|
435
448
|
request.modelProfileId
|
|
436
449
|
);
|
|
450
|
+
const lease = options.coordinator?.acquire("change");
|
|
451
|
+
if (options.coordinator !== void 0 && lease === void 0) {
|
|
452
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AGENT_BUSY);
|
|
453
|
+
}
|
|
437
454
|
const id = dependencies.createJobId();
|
|
438
455
|
if (!JOB_ID_PATTERN.test(id) || jobs.has(id)) {
|
|
456
|
+
lease?.release();
|
|
439
457
|
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INTERNAL_ERROR);
|
|
440
458
|
}
|
|
441
459
|
const timestamp = dependencies.now();
|
|
@@ -449,6 +467,7 @@ function createAgentJobManager(options) {
|
|
|
449
467
|
events: [],
|
|
450
468
|
id,
|
|
451
469
|
listeners: /* @__PURE__ */ new Set(),
|
|
470
|
+
lease,
|
|
452
471
|
model: selection.model,
|
|
453
472
|
phaseMessage: "Agent job queued.",
|
|
454
473
|
preparedChange: void 0,
|
|
@@ -491,6 +510,11 @@ function createAgentJobManager(options) {
|
|
|
491
510
|
hasActiveJob(job.id) ? import_shared.ERROR_CODES.AGENT_BUSY : import_shared.ERROR_CODES.APPLY_CONFLICT
|
|
492
511
|
);
|
|
493
512
|
}
|
|
513
|
+
const lease = options.coordinator?.acquire("change");
|
|
514
|
+
if (options.coordinator !== void 0 && lease === void 0) {
|
|
515
|
+
throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.AGENT_BUSY);
|
|
516
|
+
}
|
|
517
|
+
job.lease = lease;
|
|
494
518
|
transition(job, "reverting", "Reverting the applied Agent change.");
|
|
495
519
|
try {
|
|
496
520
|
await dependencies.revertChange(job.preparedChange);
|
|
@@ -1262,12 +1286,12 @@ function createExternalHandoffStore(options) {
|
|
|
1262
1286
|
}
|
|
1263
1287
|
return current.snapshot;
|
|
1264
1288
|
};
|
|
1265
|
-
const findReplay = (requestId,
|
|
1289
|
+
const findReplay = (requestId, fingerprint2) => {
|
|
1266
1290
|
requireOpen();
|
|
1267
1291
|
sweep();
|
|
1268
1292
|
const record = idempotency.get(requestId);
|
|
1269
1293
|
if (record === void 0) return void 0;
|
|
1270
|
-
if (record.fingerprint !==
|
|
1294
|
+
if (record.fingerprint !== fingerprint2) {
|
|
1271
1295
|
throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
1272
1296
|
}
|
|
1273
1297
|
return replayResult(record);
|
|
@@ -1512,12 +1536,12 @@ function createExternalHandoffService(options) {
|
|
|
1512
1536
|
},
|
|
1513
1537
|
async publish(request, authorize) {
|
|
1514
1538
|
requireReady();
|
|
1515
|
-
const
|
|
1516
|
-
const replayed = store.replay(request.requestId,
|
|
1539
|
+
const fingerprint2 = fingerprintExternalHandoffAnnotation(request.annotation);
|
|
1540
|
+
const replayed = store.replay(request.requestId, fingerprint2);
|
|
1517
1541
|
if (replayed !== void 0) return replayed;
|
|
1518
1542
|
const pending = inFlight.get(request.requestId);
|
|
1519
1543
|
if (pending !== void 0) {
|
|
1520
|
-
if (pending.fingerprint !==
|
|
1544
|
+
if (pending.fingerprint !== fingerprint2) {
|
|
1521
1545
|
throw new import_shared7.SpotPatchError(import_shared7.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
1522
1546
|
}
|
|
1523
1547
|
return asReplay(await pending.promise);
|
|
@@ -1527,12 +1551,12 @@ function createExternalHandoffService(options) {
|
|
|
1527
1551
|
const annotation = await authorize(request.annotation);
|
|
1528
1552
|
return store.publish({
|
|
1529
1553
|
annotation,
|
|
1530
|
-
fingerprint,
|
|
1554
|
+
fingerprint: fingerprint2,
|
|
1531
1555
|
requestId: request.requestId,
|
|
1532
1556
|
reserve: activeRegistry.reserve
|
|
1533
1557
|
});
|
|
1534
1558
|
})();
|
|
1535
|
-
const activePublish = Object.freeze({ fingerprint, promise });
|
|
1559
|
+
const activePublish = Object.freeze({ fingerprint: fingerprint2, promise });
|
|
1536
1560
|
inFlight.set(request.requestId, activePublish);
|
|
1537
1561
|
try {
|
|
1538
1562
|
return await promise;
|
|
@@ -1874,7 +1898,8 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
1874
1898
|
runtime: "dispatch",
|
|
1875
1899
|
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
1876
1900
|
}),
|
|
1877
|
-
externalAgent: Object.freeze({ enabled: false })
|
|
1901
|
+
externalAgent: Object.freeze({ enabled: false }),
|
|
1902
|
+
contextualAsk: Object.freeze({ enabled: false })
|
|
1878
1903
|
});
|
|
1879
1904
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
1880
1905
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -2184,6 +2209,35 @@ function resolveDataFlowOptions(options) {
|
|
|
2184
2209
|
limits: import_shared8.DEFAULT_DATA_FLOW_LIMITS
|
|
2185
2210
|
});
|
|
2186
2211
|
}
|
|
2212
|
+
function resolveContextualAskOptions(value, ai) {
|
|
2213
|
+
if (value === void 0 || value === false) return DEFAULT_OPTIONS.contextualAsk;
|
|
2214
|
+
const parsed = import_shared8.contextualAskOptionsSchema.safeParse(value);
|
|
2215
|
+
if (!parsed.success) {
|
|
2216
|
+
throw new RangeError("SpotPatch contextualAsk configuration is invalid.");
|
|
2217
|
+
}
|
|
2218
|
+
const configured = typeof parsed.data === "boolean" ? {} : parsed.data;
|
|
2219
|
+
const preference = configured.defaultExecutor;
|
|
2220
|
+
if (preference?.kind === "configured-key") {
|
|
2221
|
+
const provider = ai === false ? void 0 : ai.providers[preference.providerProfileId];
|
|
2222
|
+
if (provider?.models[preference.modelProfileId] === void 0) {
|
|
2223
|
+
throw new RangeError(
|
|
2224
|
+
"SpotPatch contextualAsk default Key executor must reference a configured AI model."
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (ai !== false && Object.values(ai.providers).reduce(
|
|
2229
|
+
(total, provider) => total + Object.keys(provider.models).length,
|
|
2230
|
+
0
|
|
2231
|
+
) >= import_shared8.CONTEXTUAL_ASK_LIMITS.maximumExecutors) {
|
|
2232
|
+
throw new RangeError(
|
|
2233
|
+
"SpotPatch contextualAsk configuration exceeds the executor limit."
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
return Object.freeze({
|
|
2237
|
+
enabled: true,
|
|
2238
|
+
...preference === void 0 ? {} : { defaultExecutor: Object.freeze({ ...preference }) }
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2187
2241
|
function createRuntimeDataFlowConfig(options) {
|
|
2188
2242
|
return Object.freeze({
|
|
2189
2243
|
enabled: options.enabled,
|
|
@@ -2222,6 +2276,7 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
2222
2276
|
`SpotPatch maxTargets must be an integer between 1 and ${String(import_shared8.MAX_ANNOTATION_TARGETS)}.`
|
|
2223
2277
|
);
|
|
2224
2278
|
}
|
|
2279
|
+
const ai = resolveAiOptions(options.ai ?? environmentAi);
|
|
2225
2280
|
const resolved = {
|
|
2226
2281
|
enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
|
|
2227
2282
|
include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
|
|
@@ -2234,11 +2289,12 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
2234
2289
|
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
2235
2290
|
locale,
|
|
2236
2291
|
maxTargets,
|
|
2237
|
-
ai
|
|
2292
|
+
ai,
|
|
2238
2293
|
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2239
2294
|
externalAgent: Object.freeze({
|
|
2240
2295
|
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2241
|
-
})
|
|
2296
|
+
}),
|
|
2297
|
+
contextualAsk: resolveContextualAskOptions(options.contextualAsk, ai)
|
|
2242
2298
|
};
|
|
2243
2299
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
2244
2300
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -2477,6 +2533,9 @@ function createSourceRegistry(options = {}) {
|
|
|
2477
2533
|
resolve(fileId) {
|
|
2478
2534
|
return idToPath.get(fileId);
|
|
2479
2535
|
},
|
|
2536
|
+
findRegisteredId(absolutePath) {
|
|
2537
|
+
return pathToId.get(normalizeAbsolutePath(absolutePath));
|
|
2538
|
+
},
|
|
2480
2539
|
resolveDataFlowComponent(componentSourceId) {
|
|
2481
2540
|
return componentAnchors.get(componentSourceId);
|
|
2482
2541
|
},
|
|
@@ -2489,179 +2548,1478 @@ function createSourceRegistry(options = {}) {
|
|
|
2489
2548
|
});
|
|
2490
2549
|
}
|
|
2491
2550
|
|
|
2492
|
-
// src/
|
|
2493
|
-
var
|
|
2494
|
-
|
|
2495
|
-
// src/external-handoff/browser-http.ts
|
|
2496
|
-
var import_shared13 = require("@spotpatch/shared");
|
|
2497
|
-
|
|
2498
|
-
// src/server/annotation-authorizer.ts
|
|
2499
|
-
var import_promises6 = require("fs/promises");
|
|
2500
|
-
var import_node_path7 = __toESM(require("path"), 1);
|
|
2501
|
-
var import_shared12 = require("@spotpatch/shared");
|
|
2502
|
-
|
|
2503
|
-
// src/server/source-context.ts
|
|
2504
|
-
var import_promises5 = require("fs/promises");
|
|
2505
|
-
var import_node_path6 = __toESM(require("path"), 1);
|
|
2551
|
+
// src/contextual-ask/manager.ts
|
|
2552
|
+
var import_node_crypto10 = require("crypto");
|
|
2506
2553
|
var import_shared11 = require("@spotpatch/shared");
|
|
2507
2554
|
|
|
2508
|
-
// src/
|
|
2509
|
-
var
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
case "TSSatisfiesExpression":
|
|
2517
|
-
case "TSTypeAssertion":
|
|
2518
|
-
case "TSNonNullExpression":
|
|
2519
|
-
case "TSInstantiationExpression":
|
|
2520
|
-
return unwrapTypeExpression(expression.expression);
|
|
2521
|
-
default:
|
|
2522
|
-
return expression;
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
function calleeName(expression) {
|
|
2526
|
-
const unwrapped = unwrapTypeExpression(expression);
|
|
2527
|
-
if (unwrapped.type === "Identifier") {
|
|
2528
|
-
return unwrapped.name;
|
|
2555
|
+
// src/contextual-ask/error.ts
|
|
2556
|
+
var import_agent2 = require("@spotpatch/agent");
|
|
2557
|
+
var ContextualAskError = class extends Error {
|
|
2558
|
+
code;
|
|
2559
|
+
constructor(code, options = {}) {
|
|
2560
|
+
super(code, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
2561
|
+
this.name = "ContextualAskError";
|
|
2562
|
+
this.code = code;
|
|
2529
2563
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2564
|
+
};
|
|
2565
|
+
function asContextualAskError(error) {
|
|
2566
|
+
if (error instanceof ContextualAskError) return error;
|
|
2567
|
+
if (error instanceof import_agent2.ContextualAskExecutorError) {
|
|
2568
|
+
return new ContextualAskError(error.code, { cause: error });
|
|
2532
2569
|
}
|
|
2533
|
-
return
|
|
2570
|
+
return new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
|
|
2534
2571
|
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2572
|
+
|
|
2573
|
+
// src/contextual-ask/read-snapshot.ts
|
|
2574
|
+
var import_node_crypto9 = require("crypto");
|
|
2575
|
+
var import_node_fs = require("fs");
|
|
2576
|
+
var import_promises4 = require("fs/promises");
|
|
2577
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
2578
|
+
var import_shared10 = require("@spotpatch/shared");
|
|
2579
|
+
var import_oxc_parser = require("oxc-parser");
|
|
2580
|
+
var SOURCE_EXTENSIONS = Object.freeze([
|
|
2581
|
+
".ts",
|
|
2582
|
+
".tsx",
|
|
2583
|
+
".js",
|
|
2584
|
+
".jsx",
|
|
2585
|
+
".mjs",
|
|
2586
|
+
".cjs",
|
|
2587
|
+
".css",
|
|
2588
|
+
".scss",
|
|
2589
|
+
".sass",
|
|
2590
|
+
".less"
|
|
2591
|
+
]);
|
|
2592
|
+
var PROTECTED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
2593
|
+
".git",
|
|
2594
|
+
".ssh",
|
|
2595
|
+
".spotpatch",
|
|
2596
|
+
"coverage",
|
|
2597
|
+
"dist",
|
|
2598
|
+
"node_modules"
|
|
2599
|
+
]);
|
|
2600
|
+
var PROTECTED_FILES = /* @__PURE__ */ new Set([
|
|
2601
|
+
".gitmodules",
|
|
2602
|
+
".git-credentials",
|
|
2603
|
+
".netrc",
|
|
2604
|
+
".npmrc",
|
|
2605
|
+
".pnpmrc",
|
|
2606
|
+
".pypirc",
|
|
2607
|
+
".yarnrc",
|
|
2608
|
+
"bun.lock",
|
|
2609
|
+
"bun.lockb",
|
|
2610
|
+
"npm-shrinkwrap.json",
|
|
2611
|
+
"package-lock.json",
|
|
2612
|
+
"pnpm-lock.yaml",
|
|
2613
|
+
"yarn.lock"
|
|
2614
|
+
]);
|
|
2615
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2616
|
+
function deny(code) {
|
|
2617
|
+
throw new ContextualAskError(code);
|
|
2538
2618
|
}
|
|
2539
|
-
function
|
|
2540
|
-
const
|
|
2541
|
-
if (
|
|
2542
|
-
return
|
|
2543
|
-
}
|
|
2544
|
-
if (unwrapped.type !== "CallExpression") {
|
|
2545
|
-
return false;
|
|
2619
|
+
function toPosixRelative(realRoot, absolutePath) {
|
|
2620
|
+
const relative = import_node_path5.default.relative(realRoot, absolutePath);
|
|
2621
|
+
if (relative.length === 0 || relative === ".." || relative.startsWith(`..${import_node_path5.default.sep}`) || import_node_path5.default.isAbsolute(relative)) {
|
|
2622
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2546
2623
|
}
|
|
2547
|
-
const
|
|
2548
|
-
|
|
2549
|
-
|
|
2624
|
+
const normalized = relative.split(import_node_path5.default.sep).join("/");
|
|
2625
|
+
const segments = normalized.toLowerCase().split("/");
|
|
2626
|
+
const fileName = segments.at(-1) ?? "";
|
|
2627
|
+
if (segments.some((segment) => PROTECTED_DIRECTORIES.has(segment)) || PROTECTED_FILES.has(fileName) || fileName === ".env" || fileName === ".envrc" || fileName === ".dev.vars" || fileName.startsWith(".env.") || fileName.endsWith(".pem") || fileName.endsWith(".key") || fileName.endsWith(".p12") || fileName.endsWith(".pfx") || fileName.startsWith("id_rsa")) {
|
|
2628
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2550
2629
|
}
|
|
2551
|
-
|
|
2552
|
-
return firstArgument !== void 0 && firstArgument.type !== "SpreadElement" && (isFunctionExpression(firstArgument) || isSupportedComponentInitializer(firstArgument));
|
|
2630
|
+
return normalized;
|
|
2553
2631
|
}
|
|
2554
|
-
function
|
|
2555
|
-
|
|
2556
|
-
|
|
2632
|
+
async function assertNoSymlinkPath(realRoot, absolutePath) {
|
|
2633
|
+
const relative = toPosixRelative(realRoot, absolutePath);
|
|
2634
|
+
let current = realRoot;
|
|
2635
|
+
for (const segment of relative.split("/")) {
|
|
2636
|
+
current = import_node_path5.default.join(current, segment);
|
|
2637
|
+
const metadata = await (0, import_promises4.lstat)(current).catch(() => void 0);
|
|
2638
|
+
if (metadata === void 0 || metadata.isSymbolicLink()) {
|
|
2639
|
+
deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2640
|
+
}
|
|
2557
2641
|
}
|
|
2558
|
-
return Object.freeze({ start: node.start, end: node.end, name: node.id.name });
|
|
2559
|
-
}
|
|
2560
|
-
function functionComponent(node) {
|
|
2561
|
-
return node.id !== null && isComponentName(node.id.name) && node.body !== null ? Object.freeze({ start: node.start, end: node.end, name: node.id.name }) : void 0;
|
|
2562
2642
|
}
|
|
2563
|
-
function
|
|
2564
|
-
|
|
2565
|
-
|
|
2643
|
+
async function readSecureSource(realRoot, absolutePath) {
|
|
2644
|
+
const normalizedAbsolute = import_node_path5.default.normalize(import_node_path5.default.resolve(absolutePath));
|
|
2645
|
+
const relativePath = toPosixRelative(realRoot, normalizedAbsolute);
|
|
2646
|
+
await assertNoSymlinkPath(realRoot, normalizedAbsolute);
|
|
2647
|
+
const canonical = await (0, import_promises4.realpath)(normalizedAbsolute).catch(() => void 0);
|
|
2648
|
+
if (canonical === void 0 || canonical !== normalizedAbsolute) {
|
|
2649
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2566
2650
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2651
|
+
let handle;
|
|
2652
|
+
try {
|
|
2653
|
+
handle = await (0, import_promises4.open)(normalizedAbsolute, import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_NOFOLLOW);
|
|
2654
|
+
const before = await handle.stat();
|
|
2655
|
+
if (!before.isFile() || before.nlink !== 1) {
|
|
2656
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2657
|
+
}
|
|
2658
|
+
if (before.size > import_shared10.CONTEXTUAL_ASK_LIMITS.maximumReadBytesPerFile) {
|
|
2659
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
2660
|
+
}
|
|
2661
|
+
const bytes = await handle.readFile();
|
|
2662
|
+
const after = await handle.stat();
|
|
2663
|
+
if (bytes.byteLength !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.includes(0)) {
|
|
2664
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2665
|
+
}
|
|
2666
|
+
let content;
|
|
2667
|
+
try {
|
|
2668
|
+
content = decoder.decode(bytes);
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED", { cause: error });
|
|
2671
|
+
}
|
|
2672
|
+
return Object.freeze({
|
|
2673
|
+
content,
|
|
2674
|
+
contentHash: (0, import_node_crypto9.createHash)("sha256").update(bytes).digest("hex"),
|
|
2675
|
+
relativePath,
|
|
2676
|
+
size: bytes.byteLength
|
|
2677
|
+
});
|
|
2678
|
+
} finally {
|
|
2679
|
+
await handle?.close().catch(() => void 0);
|
|
2570
2680
|
}
|
|
2571
|
-
return unwrapped.type === "MemberExpression" && !unwrapped.computed && unwrapped.object.type === "Identifier" && unwrapped.object.name === "React" && (unwrapped.property.name === "Component" || unwrapped.property.name === "PureComponent");
|
|
2572
2681
|
}
|
|
2573
|
-
function
|
|
2574
|
-
return
|
|
2682
|
+
function confidence(value) {
|
|
2683
|
+
return value === "exact" || value === "probable" ? value : "approximate";
|
|
2575
2684
|
}
|
|
2576
|
-
function
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
|
|
2685
|
+
async function authorizeSeeds(selection, registry, root, realRoot) {
|
|
2686
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2687
|
+
for (const target of selection.targets) {
|
|
2688
|
+
let fileId = target.react.source?.fileId ?? target.source.fileId;
|
|
2689
|
+
let sourceVersion;
|
|
2690
|
+
if (target.react.componentSourceId !== void 0) {
|
|
2691
|
+
const anchor = registry.resolveDataFlowComponent(target.react.componentSourceId);
|
|
2692
|
+
if (anchor === void 0 || target.react.sourceVersion !== anchor.sourceVersion) {
|
|
2693
|
+
deny("ASK_SELECTION_STALE");
|
|
2694
|
+
}
|
|
2695
|
+
fileId = anchor.fileId;
|
|
2696
|
+
sourceVersion = anchor.sourceVersion;
|
|
2697
|
+
}
|
|
2698
|
+
if (fileId === void 0) continue;
|
|
2699
|
+
const registered = registry.resolve(fileId);
|
|
2700
|
+
if (registered === void 0) deny("ASK_SELECTION_STALE");
|
|
2701
|
+
const canonical = await (0, import_promises4.realpath)(registered).catch(() => void 0);
|
|
2702
|
+
if (canonical === void 0) deny("ASK_SELECTION_STALE");
|
|
2703
|
+
const absolutePath = import_node_path5.default.normalize(canonical);
|
|
2704
|
+
const registeredRelative = toPosixRelative(realRoot, absolutePath);
|
|
2705
|
+
const claimedRelative = target.react.source?.relativePath ?? target.source.relativePath;
|
|
2706
|
+
if (claimedRelative !== void 0 && claimedRelative.split("\\").join("/") !== registeredRelative) {
|
|
2707
|
+
deny("ASK_SELECTION_STALE");
|
|
2708
|
+
}
|
|
2709
|
+
const existing = byPath.get(absolutePath);
|
|
2710
|
+
const targetIds = Object.freeze([
|
|
2711
|
+
...existing?.targetIds ?? [],
|
|
2712
|
+
...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
|
|
2713
|
+
]);
|
|
2714
|
+
byPath.set(
|
|
2715
|
+
absolutePath,
|
|
2716
|
+
Object.freeze({
|
|
2717
|
+
absolutePath,
|
|
2718
|
+
fileId,
|
|
2719
|
+
confidence: confidence(
|
|
2720
|
+
target.react.source?.confidence ?? target.source.confidence
|
|
2721
|
+
),
|
|
2722
|
+
targetIds,
|
|
2723
|
+
...sourceVersion === void 0 ? {} : { sourceVersion }
|
|
2724
|
+
})
|
|
2725
|
+
);
|
|
2580
2726
|
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2727
|
+
for (const target of selection.targets) {
|
|
2728
|
+
for (const rule of target.styles.matchedRules) {
|
|
2729
|
+
const claimed = rule.source;
|
|
2730
|
+
if (claimed === void 0 || claimed.startsWith("/") || claimed.includes("\\") || claimed.includes("\0") || claimed.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
const registeredPath = import_node_path5.default.resolve(root, ...claimed.split("/"));
|
|
2734
|
+
const fileId = registry.findRegisteredId(registeredPath);
|
|
2735
|
+
if (fileId === void 0) continue;
|
|
2736
|
+
const canonical = await (0, import_promises4.realpath)(registeredPath).catch(() => void 0);
|
|
2737
|
+
if (canonical === void 0) continue;
|
|
2738
|
+
const absolutePath = import_node_path5.default.normalize(canonical);
|
|
2739
|
+
toPosixRelative(realRoot, absolutePath);
|
|
2740
|
+
const existing = byPath.get(absolutePath);
|
|
2741
|
+
const targetIds = Object.freeze([
|
|
2742
|
+
...existing?.targetIds ?? [],
|
|
2743
|
+
...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
|
|
2744
|
+
]);
|
|
2745
|
+
byPath.set(
|
|
2746
|
+
absolutePath,
|
|
2747
|
+
Object.freeze({
|
|
2748
|
+
absolutePath,
|
|
2749
|
+
fileId,
|
|
2750
|
+
confidence: existing?.confidence ?? "probable",
|
|
2751
|
+
targetIds,
|
|
2752
|
+
...existing?.sourceVersion === void 0 ? {} : { sourceVersion: existing.sourceVersion }
|
|
2753
|
+
})
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
if (byPath.size === 0) deny("ASK_SELECTION_REQUIRED");
|
|
2758
|
+
return [...byPath.values()];
|
|
2584
2759
|
}
|
|
2585
|
-
function
|
|
2586
|
-
const
|
|
2587
|
-
if (
|
|
2588
|
-
|
|
2760
|
+
function localSpecifiers(filePath, content) {
|
|
2761
|
+
const found = /* @__PURE__ */ new Set();
|
|
2762
|
+
if (/\.(?:css|scss|sass|less)$/iu.test(filePath)) {
|
|
2763
|
+
const pattern = /@import\s+(?:url\(\s*)?["']([^"']+)["']/gu;
|
|
2764
|
+
for (const match of content.matchAll(pattern)) {
|
|
2765
|
+
if (match[1]?.startsWith(".")) found.add(match[1]);
|
|
2766
|
+
}
|
|
2767
|
+
return [...found].sort();
|
|
2589
2768
|
}
|
|
2590
|
-
let parseResult;
|
|
2591
2769
|
try {
|
|
2592
|
-
|
|
2593
|
-
|
|
2770
|
+
const parsed = (0, import_oxc_parser.parseSync)(filePath, content, { sourceType: "module" });
|
|
2771
|
+
if (parsed.errors.length > 0) return [];
|
|
2772
|
+
const add = (value) => {
|
|
2773
|
+
if (typeof value === "string" && value.startsWith(".")) found.add(value);
|
|
2774
|
+
};
|
|
2775
|
+
const visitor = new import_oxc_parser.Visitor({
|
|
2776
|
+
ImportDeclaration(node) {
|
|
2777
|
+
add(node.source.value);
|
|
2778
|
+
},
|
|
2779
|
+
ExportNamedDeclaration(node) {
|
|
2780
|
+
add(node.source?.value);
|
|
2781
|
+
},
|
|
2782
|
+
ExportAllDeclaration(node) {
|
|
2783
|
+
add(node.source.value);
|
|
2784
|
+
},
|
|
2785
|
+
ImportExpression(node) {
|
|
2786
|
+
if ("value" in node.source) add(node.source.value);
|
|
2787
|
+
}
|
|
2594
2788
|
});
|
|
2789
|
+
visitor.visit(parsed.program);
|
|
2595
2790
|
} catch {
|
|
2596
|
-
return
|
|
2791
|
+
return [];
|
|
2597
2792
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2793
|
+
return [...found].sort();
|
|
2794
|
+
}
|
|
2795
|
+
function importCandidates(importer, specifier) {
|
|
2796
|
+
const base = import_node_path5.default.resolve(import_node_path5.default.dirname(importer), specifier);
|
|
2797
|
+
if (import_node_path5.default.extname(base) !== "") return Object.freeze([base]);
|
|
2798
|
+
return Object.freeze([
|
|
2799
|
+
...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
2800
|
+
...SOURCE_EXTENSIONS.map((extension) => import_node_path5.default.join(base, `index${extension}`))
|
|
2801
|
+
]);
|
|
2802
|
+
}
|
|
2803
|
+
function countLines(content) {
|
|
2804
|
+
return content.length === 0 ? 1 : content.split("\n").length;
|
|
2805
|
+
}
|
|
2806
|
+
function publicSourceEntry(source) {
|
|
2807
|
+
return Object.freeze({
|
|
2808
|
+
handleId: source.handleId,
|
|
2809
|
+
fileId: source.fileId,
|
|
2810
|
+
relativePath: source.relativePath,
|
|
2811
|
+
label: source.label,
|
|
2812
|
+
lineCount: source.lineCount,
|
|
2813
|
+
size: source.size,
|
|
2814
|
+
contentHash: source.contentHash,
|
|
2815
|
+
confidence: source.confidence,
|
|
2816
|
+
targetIds: source.targetIds,
|
|
2817
|
+
...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion }
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
function assertSelectionEvidenceCurrent(selection, captured) {
|
|
2821
|
+
for (const target of selection.targets) {
|
|
2822
|
+
if (target.code === void 0) continue;
|
|
2823
|
+
const source = captured.find(
|
|
2824
|
+
(candidate) => candidate.relativePath === target.code?.relativePath
|
|
2825
|
+
);
|
|
2826
|
+
if (source === void 0) deny("ASK_SELECTION_STALE");
|
|
2827
|
+
const lines = source.content.split(/\r?\n/u);
|
|
2828
|
+
if (target.code.endLine > lines.length) deny("ASK_SELECTION_STALE");
|
|
2829
|
+
const currentExcerpt = lines.slice(target.code.startLine - 1, target.code.endLine).join("\n");
|
|
2830
|
+
if (currentExcerpt !== target.code.excerpt) deny("ASK_SELECTION_STALE");
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
async function captureAskReadSnapshot(options) {
|
|
2834
|
+
options.signal?.throwIfAborted();
|
|
2835
|
+
const realRoot = await (0, import_promises4.realpath)(options.root).catch(() => void 0);
|
|
2836
|
+
if (realRoot === void 0) deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2837
|
+
const seeds = await authorizeSeeds(
|
|
2838
|
+
options.selection,
|
|
2839
|
+
options.registry,
|
|
2840
|
+
options.root,
|
|
2841
|
+
realRoot
|
|
2842
|
+
);
|
|
2843
|
+
const createHandleId = options.createHandleId ?? (() => (0, import_node_crypto9.randomBytes)(16).toString("base64url"));
|
|
2844
|
+
const captured = [];
|
|
2845
|
+
const capturedPaths = /* @__PURE__ */ new Set();
|
|
2846
|
+
const queue = [...seeds];
|
|
2847
|
+
let totalBytes = 0;
|
|
2848
|
+
let truncated = false;
|
|
2849
|
+
while (queue.length > 0) {
|
|
2850
|
+
options.signal?.throwIfAborted();
|
|
2851
|
+
const candidate = queue.shift();
|
|
2852
|
+
if (candidate === void 0 || capturedPaths.has(candidate.absolutePath)) continue;
|
|
2853
|
+
if (captured.length >= import_shared10.CONTEXTUAL_ASK_LIMITS.maximumReadFiles) {
|
|
2854
|
+
truncated = true;
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
let read;
|
|
2858
|
+
try {
|
|
2859
|
+
read = await readSecureSource(realRoot, candidate.absolutePath);
|
|
2860
|
+
options.signal?.throwIfAborted();
|
|
2861
|
+
} catch (error) {
|
|
2862
|
+
if (captured.length < seeds.length) throw error;
|
|
2863
|
+
truncated = true;
|
|
2864
|
+
continue;
|
|
2865
|
+
}
|
|
2866
|
+
if (totalBytes + read.size > import_shared10.CONTEXTUAL_ASK_LIMITS.maximumReadBytesTotal) {
|
|
2867
|
+
if (captured.length < seeds.length)
|
|
2868
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
2869
|
+
truncated = true;
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
capturedPaths.add(candidate.absolutePath);
|
|
2873
|
+
totalBytes += read.size;
|
|
2874
|
+
const entry = Object.freeze({
|
|
2875
|
+
handleId: createHandleId(),
|
|
2876
|
+
fileId: candidate.fileId,
|
|
2877
|
+
relativePath: read.relativePath,
|
|
2878
|
+
label: import_node_path5.default.basename(read.relativePath),
|
|
2879
|
+
lineCount: countLines(read.content),
|
|
2880
|
+
size: read.size,
|
|
2881
|
+
contentHash: read.contentHash,
|
|
2882
|
+
confidence: candidate.confidence,
|
|
2883
|
+
targetIds: Object.freeze([...candidate.targetIds]),
|
|
2884
|
+
...candidate.sourceVersion === void 0 ? {} : { sourceVersion: candidate.sourceVersion },
|
|
2885
|
+
absolutePath: candidate.absolutePath,
|
|
2886
|
+
content: read.content
|
|
2887
|
+
});
|
|
2888
|
+
captured.push(entry);
|
|
2889
|
+
for (const specifier of localSpecifiers(candidate.absolutePath, read.content)) {
|
|
2890
|
+
options.signal?.throwIfAborted();
|
|
2891
|
+
for (const importPath of importCandidates(candidate.absolutePath, specifier)) {
|
|
2892
|
+
if (capturedPaths.has(importPath)) continue;
|
|
2893
|
+
const metadata = await (0, import_promises4.lstat)(importPath).catch(() => void 0);
|
|
2894
|
+
if (metadata?.isFile() !== true || metadata.isSymbolicLink()) continue;
|
|
2895
|
+
const fileId = options.registry.findRegisteredId(importPath) ?? options.registry.register(importPath);
|
|
2896
|
+
queue.push(
|
|
2897
|
+
Object.freeze({
|
|
2898
|
+
absolutePath: import_node_path5.default.normalize(importPath),
|
|
2899
|
+
fileId,
|
|
2900
|
+
confidence: "probable",
|
|
2901
|
+
targetIds: candidate.targetIds
|
|
2902
|
+
})
|
|
2903
|
+
);
|
|
2904
|
+
break;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2600
2907
|
}
|
|
2601
|
-
|
|
2602
|
-
const
|
|
2603
|
-
const
|
|
2604
|
-
|
|
2605
|
-
|
|
2908
|
+
assertSelectionEvidenceCurrent(options.selection, captured);
|
|
2909
|
+
const sourceByHandle = new Map(captured.map((source) => [source.handleId, source]));
|
|
2910
|
+
const publicEntries = Object.freeze(
|
|
2911
|
+
captured.map((source) => publicSourceEntry(source))
|
|
2912
|
+
);
|
|
2913
|
+
const contextHash = (0, import_node_crypto9.createHash)("sha256").update(
|
|
2914
|
+
JSON.stringify({
|
|
2915
|
+
selectionId: options.selection.selectionId,
|
|
2916
|
+
sources: publicEntries.map((source) => ({
|
|
2917
|
+
relativePath: source.relativePath,
|
|
2918
|
+
contentHash: source.contentHash,
|
|
2919
|
+
targetIds: source.targetIds,
|
|
2920
|
+
sourceVersion: source.sourceVersion ?? null
|
|
2921
|
+
}))
|
|
2922
|
+
})
|
|
2923
|
+
).digest("hex");
|
|
2924
|
+
let disposed = false;
|
|
2925
|
+
const requireSource = (handleId) => {
|
|
2926
|
+
if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
2927
|
+
const source = sourceByHandle.get(handleId);
|
|
2928
|
+
if (source === void 0) throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2929
|
+
return source;
|
|
2930
|
+
};
|
|
2931
|
+
const snapshot2 = Object.freeze({
|
|
2932
|
+
manifest: () => {
|
|
2933
|
+
if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
2934
|
+
return publicEntries;
|
|
2606
2935
|
},
|
|
2607
|
-
|
|
2608
|
-
|
|
2936
|
+
read(handleId, range = {}) {
|
|
2937
|
+
const source = requireSource(handleId);
|
|
2938
|
+
const startLine = range.startLine ?? 1;
|
|
2939
|
+
const endLine = range.endLine ?? source.lineCount;
|
|
2940
|
+
if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
|
|
2941
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2942
|
+
}
|
|
2943
|
+
return Object.freeze({
|
|
2944
|
+
handleId,
|
|
2945
|
+
startLine,
|
|
2946
|
+
endLine,
|
|
2947
|
+
content: source.content.split("\n").slice(startLine - 1, endLine).join("\n")
|
|
2948
|
+
});
|
|
2609
2949
|
},
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2950
|
+
search(query) {
|
|
2951
|
+
if (query.length === 0 || query.length > import_shared10.CONTEXTUAL_ASK_LIMITS.maximumSearchQueryCharacters || query.includes("\0")) {
|
|
2952
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2953
|
+
}
|
|
2954
|
+
const normalized = query.toLocaleLowerCase("en-US");
|
|
2955
|
+
const matches = [];
|
|
2956
|
+
for (const source of captured) {
|
|
2957
|
+
for (const [index, line] of source.content.split("\n").entries()) {
|
|
2958
|
+
if (line.toLocaleLowerCase("en-US").includes(normalized)) {
|
|
2959
|
+
matches.push(
|
|
2960
|
+
Object.freeze({
|
|
2961
|
+
handleId: source.handleId,
|
|
2962
|
+
line: index + 1,
|
|
2963
|
+
preview: line.slice(
|
|
2964
|
+
0,
|
|
2965
|
+
import_shared10.CONTEXTUAL_ASK_LIMITS.maximumSearchPreviewCharacters
|
|
2966
|
+
)
|
|
2967
|
+
})
|
|
2968
|
+
);
|
|
2969
|
+
if (matches.length >= import_shared10.CONTEXTUAL_ASK_LIMITS.maximumSearchResults) {
|
|
2970
|
+
return Object.freeze(matches);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2614
2974
|
}
|
|
2975
|
+
return Object.freeze(matches);
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
return Object.freeze({
|
|
2979
|
+
grant: Object.freeze({ contextHash, truncated, sources: publicEntries }),
|
|
2980
|
+
snapshot: snapshot2,
|
|
2981
|
+
dispose() {
|
|
2982
|
+
disposed = true;
|
|
2983
|
+
sourceByHandle.clear();
|
|
2984
|
+
captured.splice(0, captured.length);
|
|
2615
2985
|
},
|
|
2616
|
-
|
|
2617
|
-
const
|
|
2618
|
-
|
|
2619
|
-
|
|
2986
|
+
async isStale() {
|
|
2987
|
+
for (const source of captured) {
|
|
2988
|
+
try {
|
|
2989
|
+
const current = await readSecureSource(realRoot, source.absolutePath);
|
|
2990
|
+
if (current.contentHash !== source.contentHash) return true;
|
|
2991
|
+
} catch {
|
|
2992
|
+
return true;
|
|
2993
|
+
}
|
|
2620
2994
|
}
|
|
2995
|
+
return false;
|
|
2621
2996
|
},
|
|
2622
|
-
|
|
2623
|
-
const
|
|
2624
|
-
if (
|
|
2625
|
-
|
|
2997
|
+
projectCitation(handleId, startLine, endLine) {
|
|
2998
|
+
const source = requireSource(handleId);
|
|
2999
|
+
if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
|
|
3000
|
+
throw new ContextualAskError("ASK_ANSWER_INVALID");
|
|
2626
3001
|
}
|
|
3002
|
+
return Object.freeze({ ...publicSourceEntry(source), startLine, endLine });
|
|
2627
3003
|
}
|
|
2628
3004
|
});
|
|
2629
|
-
visitor.visit(parseResult.program);
|
|
2630
|
-
const selectedJsx = jsxNodes.filter((node) => node.start <= offset && node.end >= offset).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
2631
|
-
if (selectedJsx === void 0) {
|
|
2632
|
-
return void 0;
|
|
2633
|
-
}
|
|
2634
|
-
return components.filter(
|
|
2635
|
-
(component) => component.start <= selectedJsx.start && component.end >= selectedJsx.end
|
|
2636
|
-
).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
2637
|
-
}
|
|
2638
|
-
function lineAtOffset(source, offset) {
|
|
2639
|
-
let line = 1;
|
|
2640
|
-
for (let index = 0; index < offset; index += 1) {
|
|
2641
|
-
if (source[index] === "\n") {
|
|
2642
|
-
line += 1;
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
return line;
|
|
2646
3005
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
3006
|
+
|
|
3007
|
+
// src/contextual-ask/manager.ts
|
|
3008
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
|
|
3009
|
+
"queued",
|
|
3010
|
+
"authorizing",
|
|
3011
|
+
"running",
|
|
3012
|
+
"cancelling"
|
|
3013
|
+
]);
|
|
3014
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["answered", "cancelled", "failed"]);
|
|
3015
|
+
var DEFAULT_DEPENDENCIES2 = Object.freeze({
|
|
3016
|
+
captureSnapshot: captureAskReadSnapshot,
|
|
3017
|
+
createId: () => (0, import_node_crypto10.randomBytes)(16).toString("base64url"),
|
|
3018
|
+
now: () => /* @__PURE__ */ new Date()
|
|
3019
|
+
});
|
|
3020
|
+
function fingerprint(request) {
|
|
3021
|
+
return (0, import_node_crypto10.createHash)("sha256").update(
|
|
3022
|
+
JSON.stringify({
|
|
3023
|
+
envelope: request.envelope,
|
|
3024
|
+
executorId: request.executorId,
|
|
3025
|
+
providerDataConsent: request.providerDataConsent
|
|
3026
|
+
})
|
|
3027
|
+
).digest("hex");
|
|
3028
|
+
}
|
|
3029
|
+
function fileCountBucket(count) {
|
|
3030
|
+
if (count <= 1) return "one";
|
|
3031
|
+
if (count <= 5) return "few";
|
|
3032
|
+
return "many";
|
|
3033
|
+
}
|
|
3034
|
+
function raceWithAbort(operation, signal) {
|
|
3035
|
+
const reason = () => signal.reason instanceof Error ? signal.reason : new ContextualAskError("ASK_CANCELLED");
|
|
3036
|
+
if (signal.aborted) return Promise.reject(reason());
|
|
3037
|
+
return new Promise((resolve, reject) => {
|
|
3038
|
+
const onAbort = () => {
|
|
3039
|
+
reject(reason());
|
|
3040
|
+
};
|
|
3041
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3042
|
+
operation.then(
|
|
3043
|
+
(value) => {
|
|
3044
|
+
signal.removeEventListener("abort", onAbort);
|
|
3045
|
+
resolve(value);
|
|
3046
|
+
},
|
|
3047
|
+
(error) => {
|
|
3048
|
+
signal.removeEventListener("abort", onAbort);
|
|
3049
|
+
reject(
|
|
3050
|
+
error instanceof Error ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error })
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
);
|
|
2651
3054
|
});
|
|
2652
3055
|
}
|
|
2653
|
-
function
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
3056
|
+
function createContextualAskManager(options) {
|
|
3057
|
+
const dependencies = Object.freeze({
|
|
3058
|
+
...DEFAULT_DEPENDENCIES2,
|
|
3059
|
+
...options.dependencies
|
|
3060
|
+
});
|
|
3061
|
+
const executors = /* @__PURE__ */ new Map();
|
|
3062
|
+
for (const executor of options.executors ?? []) {
|
|
3063
|
+
if (executors.has(executor.executorId)) {
|
|
3064
|
+
throw new TypeError("Contextual Ask executor IDs must be unique.");
|
|
3065
|
+
}
|
|
3066
|
+
executors.set(executor.executorId, executor);
|
|
2659
3067
|
}
|
|
2660
|
-
const
|
|
2661
|
-
const
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
3068
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
3069
|
+
const requests = /* @__PURE__ */ new Map();
|
|
3070
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
3071
|
+
let closed = false;
|
|
3072
|
+
const isClosed = () => closed;
|
|
3073
|
+
const cleanupExpired = () => {
|
|
3074
|
+
const now = dependencies.now().getTime();
|
|
3075
|
+
for (const [jobId, job] of jobs) {
|
|
3076
|
+
if (job.expiresAt !== void 0 && now >= job.expiresAt) {
|
|
3077
|
+
job.snapshot?.dispose();
|
|
3078
|
+
jobs.delete(jobId);
|
|
3079
|
+
const request = requests.get(job.request.requestId);
|
|
3080
|
+
if (request?.jobId === jobId) requests.delete(job.request.requestId);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
const requireJob = (jobId) => {
|
|
3085
|
+
cleanupExpired();
|
|
3086
|
+
const job = jobs.get(jobId);
|
|
3087
|
+
if (job === void 0) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
3088
|
+
return job;
|
|
3089
|
+
};
|
|
3090
|
+
const publicSnapshot = (job) => import_shared11.askJobSnapshotSchema.parse({
|
|
3091
|
+
schemaVersion: import_shared11.CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3092
|
+
jobId: job.id,
|
|
3093
|
+
selectionId: job.request.envelope.selection.selectionId,
|
|
3094
|
+
status: job.status,
|
|
3095
|
+
executor: {
|
|
3096
|
+
executorId: job.executorCapability.executorId,
|
|
3097
|
+
kind: job.executorCapability.kind,
|
|
3098
|
+
label: job.executorCapability.label,
|
|
3099
|
+
modelLabel: job.executorCapability.effectiveModelLabel
|
|
3100
|
+
},
|
|
3101
|
+
...job.phaseMessage === void 0 ? {} : { phaseMessage: job.phaseMessage },
|
|
3102
|
+
createdAt: job.createdAt,
|
|
3103
|
+
updatedAt: job.updatedAt,
|
|
3104
|
+
canCancel: ["queued", "authorizing", "running"].includes(job.status),
|
|
3105
|
+
...job.errorCode === void 0 ? {} : { errorCode: job.errorCode }
|
|
3106
|
+
});
|
|
3107
|
+
const appendEvent = (job, event) => {
|
|
3108
|
+
job.events.push(import_shared11.askJobEventSchema.parse(event));
|
|
3109
|
+
if (job.events.length > import_shared11.CONTEXTUAL_ASK_LIMITS.maximumRetainedEvents) {
|
|
3110
|
+
job.events.splice(
|
|
3111
|
+
0,
|
|
3112
|
+
job.events.length - import_shared11.CONTEXTUAL_ASK_LIMITS.maximumRetainedEvents
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
for (const listener of job.listeners) listener(event);
|
|
3116
|
+
};
|
|
3117
|
+
const eventBase = (job) => {
|
|
3118
|
+
job.sequence += 1;
|
|
3119
|
+
return {
|
|
3120
|
+
schemaVersion: import_shared11.CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3121
|
+
sequence: job.sequence,
|
|
3122
|
+
jobId: job.id,
|
|
3123
|
+
status: job.status,
|
|
3124
|
+
timestamp: dependencies.now().toISOString()
|
|
3125
|
+
};
|
|
3126
|
+
};
|
|
3127
|
+
const emitSnapshot = (job) => {
|
|
3128
|
+
appendEvent(job, {
|
|
3129
|
+
...eventBase(job),
|
|
3130
|
+
type: "snapshot",
|
|
3131
|
+
snapshot: publicSnapshot(job)
|
|
3132
|
+
});
|
|
3133
|
+
};
|
|
3134
|
+
const emitPhase = (job, message) => {
|
|
3135
|
+
appendEvent(job, { ...eventBase(job), type: "phase", message });
|
|
3136
|
+
};
|
|
3137
|
+
const emitReadActivity = (job, entry, state) => {
|
|
3138
|
+
appendEvent(job, {
|
|
3139
|
+
...eventBase(job),
|
|
3140
|
+
type: "read-activity",
|
|
3141
|
+
activity: entry === void 0 ? {
|
|
3142
|
+
kind: "file-count",
|
|
3143
|
+
bucket: fileCountBucket(job.snapshot?.grant.sources.length ?? 0)
|
|
3144
|
+
} : {
|
|
3145
|
+
kind: "source",
|
|
3146
|
+
sourceId: entry.fileId,
|
|
3147
|
+
relativePath: entry.relativePath
|
|
3148
|
+
},
|
|
3149
|
+
state
|
|
3150
|
+
});
|
|
3151
|
+
};
|
|
3152
|
+
const transition = (job, status, phaseMessage, errorCode) => {
|
|
3153
|
+
job.status = status;
|
|
3154
|
+
job.phaseMessage = phaseMessage;
|
|
3155
|
+
job.errorCode = errorCode;
|
|
3156
|
+
job.updatedAt = dependencies.now().toISOString();
|
|
3157
|
+
emitSnapshot(job);
|
|
3158
|
+
if (!TERMINAL_STATUSES.has(status)) emitPhase(job, phaseMessage);
|
|
3159
|
+
};
|
|
3160
|
+
const finish = (job) => {
|
|
3161
|
+
job.expiresAt = dependencies.now().getTime() + import_shared11.CONTEXTUAL_ASK_LIMITS.resultTtlMs;
|
|
3162
|
+
job.lease.release();
|
|
3163
|
+
job.snapshot?.dispose();
|
|
3164
|
+
job.snapshot = void 0;
|
|
3165
|
+
};
|
|
3166
|
+
const fail = (job, error) => {
|
|
3167
|
+
if (TERMINAL_STATUSES.has(job.status)) return;
|
|
3168
|
+
const normalized = asContextualAskError(error);
|
|
3169
|
+
const abortReason = job.controller.signal.reason;
|
|
3170
|
+
const cancelled = normalized.code === "ASK_CANCELLED" || abortReason instanceof ContextualAskError && abortReason.code === "ASK_CANCELLED";
|
|
3171
|
+
transition(
|
|
3172
|
+
job,
|
|
3173
|
+
cancelled ? "cancelled" : "failed",
|
|
3174
|
+
cancelled ? "Question cancelled." : "Question failed.",
|
|
3175
|
+
cancelled ? "ASK_CANCELLED" : normalized.code
|
|
3176
|
+
);
|
|
3177
|
+
appendEvent(job, {
|
|
3178
|
+
...eventBase(job),
|
|
3179
|
+
type: "error",
|
|
3180
|
+
errorCode: cancelled ? "ASK_CANCELLED" : normalized.code
|
|
3181
|
+
});
|
|
3182
|
+
finish(job);
|
|
3183
|
+
};
|
|
3184
|
+
const instrumentSnapshot = (job, snapshot2, onLimitExceeded) => {
|
|
3185
|
+
const byHandle = new Map(
|
|
3186
|
+
snapshot2.manifest().map((entry) => [entry.handleId, entry])
|
|
3187
|
+
);
|
|
3188
|
+
let readOperations = 0;
|
|
3189
|
+
const beginOperation = () => {
|
|
3190
|
+
readOperations += 1;
|
|
3191
|
+
if (readOperations > import_shared11.CONTEXTUAL_ASK_LIMITS.maximumToolCalls) {
|
|
3192
|
+
onLimitExceeded();
|
|
3193
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
return Object.freeze({
|
|
3197
|
+
manifest: () => snapshot2.manifest(),
|
|
3198
|
+
read(handleId, range) {
|
|
3199
|
+
beginOperation();
|
|
3200
|
+
const entry = byHandle.get(handleId);
|
|
3201
|
+
if (job.status === "running") emitReadActivity(job, entry, "started");
|
|
3202
|
+
try {
|
|
3203
|
+
const result = snapshot2.read(handleId, range);
|
|
3204
|
+
if (job.status === "running") emitReadActivity(job, entry, "succeeded");
|
|
3205
|
+
return result;
|
|
3206
|
+
} catch (error) {
|
|
3207
|
+
if (job.status === "running") emitReadActivity(job, entry, "failed");
|
|
3208
|
+
throw error;
|
|
3209
|
+
}
|
|
3210
|
+
},
|
|
3211
|
+
search(query) {
|
|
3212
|
+
beginOperation();
|
|
3213
|
+
if (job.status === "running") emitReadActivity(job, void 0, "started");
|
|
3214
|
+
try {
|
|
3215
|
+
const result = snapshot2.search(query);
|
|
3216
|
+
if (job.status === "running") {
|
|
3217
|
+
emitReadActivity(job, void 0, "succeeded");
|
|
3218
|
+
}
|
|
3219
|
+
return result;
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
if (job.status === "running") emitReadActivity(job, void 0, "failed");
|
|
3222
|
+
throw error;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
};
|
|
3227
|
+
const projectAnswer = async (job, draft, captured) => {
|
|
3228
|
+
const sourceIds = /* @__PURE__ */ new Map();
|
|
3229
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3230
|
+
const projectCitations = (citations) => citations.map((citation) => {
|
|
3231
|
+
const key = `${citation.handleId}:${String(citation.startLine)}:${String(citation.endLine)}`;
|
|
3232
|
+
let sourceId = sourceIds.get(key);
|
|
3233
|
+
if (sourceId === void 0) {
|
|
3234
|
+
sourceId = dependencies.createId();
|
|
3235
|
+
sourceIds.set(key, sourceId);
|
|
3236
|
+
sources.set(
|
|
3237
|
+
sourceId,
|
|
3238
|
+
captured.projectCitation(
|
|
3239
|
+
citation.handleId,
|
|
3240
|
+
citation.startLine,
|
|
3241
|
+
citation.endLine
|
|
3242
|
+
)
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
return sourceId;
|
|
3246
|
+
});
|
|
3247
|
+
const blocks = draft.blocks.map((block) => {
|
|
3248
|
+
if (block.kind === "list") {
|
|
3249
|
+
return {
|
|
3250
|
+
kind: "list",
|
|
3251
|
+
items: block.items.map((item) => ({
|
|
3252
|
+
text: item.text,
|
|
3253
|
+
sourceIds: projectCitations(item.citations)
|
|
3254
|
+
}))
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
return Object.freeze({
|
|
3258
|
+
kind: block.kind,
|
|
3259
|
+
...block.kind === "paragraph" ? { text: block.text } : { code: block.code },
|
|
3260
|
+
...block.kind === "code" && block.language !== void 0 ? { language: block.language } : {},
|
|
3261
|
+
sourceIds: projectCitations(block.citations)
|
|
3262
|
+
});
|
|
3263
|
+
});
|
|
3264
|
+
const warnings = [...draft.warnings];
|
|
3265
|
+
if (captured.grant.truncated && !warnings.some((warning) => warning.code === "source-truncated")) {
|
|
3266
|
+
warnings.push(Object.freeze({ code: "source-truncated" }));
|
|
3267
|
+
}
|
|
3268
|
+
if (await captured.isStale() && !warnings.some((warning) => warning.code === "source-stale")) {
|
|
3269
|
+
warnings.push(Object.freeze({ code: "source-stale" }));
|
|
3270
|
+
}
|
|
3271
|
+
const created = dependencies.now();
|
|
3272
|
+
return import_shared11.askAnswerResultSchema.parse({
|
|
3273
|
+
schemaVersion: import_shared11.CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3274
|
+
jobId: job.id,
|
|
3275
|
+
selectionId: job.request.envelope.selection.selectionId,
|
|
3276
|
+
contextHash: captured.grant.contextHash,
|
|
3277
|
+
executor: {
|
|
3278
|
+
executorId: job.executorCapability.executorId,
|
|
3279
|
+
kind: job.executorCapability.kind,
|
|
3280
|
+
label: job.executorCapability.label,
|
|
3281
|
+
modelLabel: job.executorCapability.effectiveModelLabel
|
|
3282
|
+
},
|
|
3283
|
+
blocks,
|
|
3284
|
+
sources: [...sources].map(([sourceId, source]) => ({
|
|
3285
|
+
sourceId,
|
|
3286
|
+
label: source.label,
|
|
3287
|
+
relativePath: source.relativePath,
|
|
3288
|
+
fileId: source.fileId,
|
|
3289
|
+
startLine: source.startLine,
|
|
3290
|
+
endLine: source.endLine,
|
|
3291
|
+
confidence: source.confidence,
|
|
3292
|
+
targetIds: source.targetIds,
|
|
3293
|
+
...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion },
|
|
3294
|
+
contentHash: source.contentHash
|
|
3295
|
+
})),
|
|
3296
|
+
warnings,
|
|
3297
|
+
createdAt: created.toISOString(),
|
|
3298
|
+
expiresAt: new Date(
|
|
3299
|
+
created.getTime() + import_shared11.CONTEXTUAL_ASK_LIMITS.resultTtlMs
|
|
3300
|
+
).toISOString()
|
|
3301
|
+
});
|
|
3302
|
+
};
|
|
3303
|
+
const run = async (job) => {
|
|
3304
|
+
const timeout = setTimeout(() => {
|
|
3305
|
+
job.controller.abort(new ContextualAskError("ASK_TIMEOUT"));
|
|
3306
|
+
}, import_shared11.CONTEXTUAL_ASK_LIMITS.jobTimeoutMs);
|
|
3307
|
+
timeout.unref();
|
|
3308
|
+
try {
|
|
3309
|
+
transition(job, "authorizing", "Authorizing selected sources.");
|
|
3310
|
+
const captured = await dependencies.captureSnapshot({
|
|
3311
|
+
root: options.root,
|
|
3312
|
+
registry: options.registry,
|
|
3313
|
+
selection: job.request.envelope.selection,
|
|
3314
|
+
signal: job.controller.signal,
|
|
3315
|
+
createHandleId: dependencies.createId
|
|
3316
|
+
});
|
|
3317
|
+
if (job.controller.signal.aborted) {
|
|
3318
|
+
captured.dispose();
|
|
3319
|
+
}
|
|
3320
|
+
job.controller.signal.throwIfAborted();
|
|
3321
|
+
job.snapshot = captured;
|
|
3322
|
+
transition(job, "running", "Analyzing the authorized snapshot.");
|
|
3323
|
+
emitReadActivity(job, void 0, "succeeded");
|
|
3324
|
+
const readLimit = { exceeded: false };
|
|
3325
|
+
const execution = await raceWithAbort(
|
|
3326
|
+
job.executor.execute(
|
|
3327
|
+
{
|
|
3328
|
+
jobId: job.id,
|
|
3329
|
+
envelope: job.request.envelope,
|
|
3330
|
+
grant: captured.grant,
|
|
3331
|
+
snapshot: instrumentSnapshot(job, captured.snapshot, () => {
|
|
3332
|
+
readLimit.exceeded = true;
|
|
3333
|
+
})
|
|
3334
|
+
},
|
|
3335
|
+
job.controller.signal
|
|
3336
|
+
),
|
|
3337
|
+
job.controller.signal
|
|
3338
|
+
);
|
|
3339
|
+
const draft = import_shared11.askAnswerDraftSchema.parse(execution);
|
|
3340
|
+
const effectiveModelLabel = job.executor.effectiveModelLabel?.();
|
|
3341
|
+
if (effectiveModelLabel !== void 0) {
|
|
3342
|
+
job.executorCapability = import_shared11.contextualAskExecutorCapabilitySchema.parse({
|
|
3343
|
+
...job.executorCapability,
|
|
3344
|
+
effectiveModelLabel
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
if (readLimit.exceeded) throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3348
|
+
job.controller.signal.throwIfAborted();
|
|
3349
|
+
const result = await projectAnswer(job, draft, captured);
|
|
3350
|
+
job.controller.signal.throwIfAborted();
|
|
3351
|
+
job.result = result;
|
|
3352
|
+
transition(job, "answered", "Answer ready.");
|
|
3353
|
+
appendEvent(job, { ...eventBase(job), type: "answer-ready" });
|
|
3354
|
+
finish(job);
|
|
3355
|
+
} catch (error) {
|
|
3356
|
+
fail(
|
|
3357
|
+
job,
|
|
3358
|
+
error instanceof Error && error.name === "ZodError" ? new ContextualAskError("ASK_ANSWER_INVALID", { cause: error }) : asContextualAskError(error)
|
|
3359
|
+
);
|
|
3360
|
+
} finally {
|
|
3361
|
+
clearTimeout(timeout);
|
|
3362
|
+
}
|
|
3363
|
+
};
|
|
3364
|
+
return Object.freeze({
|
|
3365
|
+
async capability(signal) {
|
|
3366
|
+
if (!options.enabled || closed) {
|
|
3367
|
+
return import_shared11.contextualAskCapabilitySchema.parse({
|
|
3368
|
+
schemaVersion: import_shared11.CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3369
|
+
enabled: false,
|
|
3370
|
+
executors: [],
|
|
3371
|
+
safety: {
|
|
3372
|
+
selectionRequired: true,
|
|
3373
|
+
singleTurn: true,
|
|
3374
|
+
writesAllowed: false,
|
|
3375
|
+
historyStored: false
|
|
3376
|
+
},
|
|
3377
|
+
checkedAt: dependencies.now().toISOString()
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
const capabilities = (await Promise.all(
|
|
3381
|
+
[...executors.values()].map(async (executor) => {
|
|
3382
|
+
try {
|
|
3383
|
+
return import_shared11.contextualAskExecutorCapabilitySchema.parse(
|
|
3384
|
+
await raceWithAbort(executor.capability(signal), signal)
|
|
3385
|
+
);
|
|
3386
|
+
} catch {
|
|
3387
|
+
return void 0;
|
|
3388
|
+
}
|
|
3389
|
+
})
|
|
3390
|
+
)).filter((capability) => capability !== void 0);
|
|
3391
|
+
return import_shared11.contextualAskCapabilitySchema.parse({
|
|
3392
|
+
schemaVersion: import_shared11.CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3393
|
+
enabled: true,
|
|
3394
|
+
executors: capabilities,
|
|
3395
|
+
safety: {
|
|
3396
|
+
selectionRequired: true,
|
|
3397
|
+
singleTurn: true,
|
|
3398
|
+
writesAllowed: false,
|
|
3399
|
+
historyStored: false
|
|
3400
|
+
},
|
|
3401
|
+
checkedAt: dependencies.now().toISOString()
|
|
3402
|
+
});
|
|
3403
|
+
},
|
|
3404
|
+
async create(rawRequest) {
|
|
3405
|
+
cleanupExpired();
|
|
3406
|
+
if (!options.enabled || closed) throw new ContextualAskError("ASK_DISABLED");
|
|
3407
|
+
const parsed = import_shared11.askJobCreateRequestSchema.safeParse(rawRequest);
|
|
3408
|
+
if (!parsed.success) throw new ContextualAskError("ASK_QUESTION_INVALID");
|
|
3409
|
+
const request = parsed.data;
|
|
3410
|
+
const requestFingerprint = fingerprint(request);
|
|
3411
|
+
const previous = requests.get(request.requestId);
|
|
3412
|
+
if (previous !== void 0) {
|
|
3413
|
+
if (previous.fingerprint !== requestFingerprint) {
|
|
3414
|
+
throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
|
|
3415
|
+
}
|
|
3416
|
+
return publicSnapshot(requireJob(previous.jobId));
|
|
3417
|
+
}
|
|
3418
|
+
const pending = pendingRequests.get(request.requestId);
|
|
3419
|
+
if (pending !== void 0) {
|
|
3420
|
+
if (pending.fingerprint !== requestFingerprint) {
|
|
3421
|
+
throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
|
|
3422
|
+
}
|
|
3423
|
+
return await pending.promise;
|
|
3424
|
+
}
|
|
3425
|
+
if ([...jobs.values()].some((job) => ACTIVE_STATUSES.has(job.status))) {
|
|
3426
|
+
throw new ContextualAskError("ASK_BUSY");
|
|
3427
|
+
}
|
|
3428
|
+
const executor = executors.get(request.executorId);
|
|
3429
|
+
if (executor === void 0)
|
|
3430
|
+
throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
|
|
3431
|
+
const lease = options.coordinator.acquire("ask");
|
|
3432
|
+
if (lease === void 0) throw new ContextualAskError("ASK_BUSY");
|
|
3433
|
+
const controller = new AbortController();
|
|
3434
|
+
let leaseTransferred = false;
|
|
3435
|
+
const creation = (async () => {
|
|
3436
|
+
try {
|
|
3437
|
+
const capabilitySignal = AbortSignal.any([
|
|
3438
|
+
controller.signal,
|
|
3439
|
+
AbortSignal.timeout(import_shared11.CONTEXTUAL_ASK_LIMITS.capabilityTimeoutMs)
|
|
3440
|
+
]);
|
|
3441
|
+
const capability = import_shared11.contextualAskExecutorCapabilitySchema.parse(
|
|
3442
|
+
await raceWithAbort(
|
|
3443
|
+
executor.capability(capabilitySignal),
|
|
3444
|
+
capabilitySignal
|
|
3445
|
+
)
|
|
3446
|
+
);
|
|
3447
|
+
if (isClosed()) throw new ContextualAskError("ASK_DISABLED");
|
|
3448
|
+
if (capability.executorId !== executor.executorId || capability.state !== "ready" || !capability.readOnlyProven) {
|
|
3449
|
+
throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
|
|
3450
|
+
}
|
|
3451
|
+
for (const [jobId, existing] of jobs) {
|
|
3452
|
+
if (TERMINAL_STATUSES.has(existing.status)) {
|
|
3453
|
+
existing.snapshot?.dispose();
|
|
3454
|
+
jobs.delete(jobId);
|
|
3455
|
+
requests.delete(existing.request.requestId);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
const createdAt = dependencies.now().toISOString();
|
|
3459
|
+
const job = {
|
|
3460
|
+
controller: new AbortController(),
|
|
3461
|
+
createdAt,
|
|
3462
|
+
events: [],
|
|
3463
|
+
executor,
|
|
3464
|
+
executorCapability: capability,
|
|
3465
|
+
fingerprint: requestFingerprint,
|
|
3466
|
+
id: dependencies.createId(),
|
|
3467
|
+
lease,
|
|
3468
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
3469
|
+
request,
|
|
3470
|
+
errorCode: void 0,
|
|
3471
|
+
expiresAt: void 0,
|
|
3472
|
+
phaseMessage: "Question queued.",
|
|
3473
|
+
result: void 0,
|
|
3474
|
+
runPromise: void 0,
|
|
3475
|
+
sequence: 0,
|
|
3476
|
+
snapshot: void 0,
|
|
3477
|
+
status: "queued",
|
|
3478
|
+
updatedAt: createdAt
|
|
3479
|
+
};
|
|
3480
|
+
jobs.set(job.id, job);
|
|
3481
|
+
requests.set(request.requestId, {
|
|
3482
|
+
fingerprint: requestFingerprint,
|
|
3483
|
+
jobId: job.id
|
|
3484
|
+
});
|
|
3485
|
+
leaseTransferred = true;
|
|
3486
|
+
emitSnapshot(job);
|
|
3487
|
+
emitPhase(job, "Question queued.");
|
|
3488
|
+
job.runPromise = run(job);
|
|
3489
|
+
return publicSnapshot(job);
|
|
3490
|
+
} catch (error) {
|
|
3491
|
+
throw error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", {
|
|
3492
|
+
cause: error
|
|
3493
|
+
});
|
|
3494
|
+
} finally {
|
|
3495
|
+
if (!leaseTransferred) lease.release();
|
|
3496
|
+
pendingRequests.delete(request.requestId);
|
|
3497
|
+
}
|
|
3498
|
+
})();
|
|
3499
|
+
pendingRequests.set(request.requestId, {
|
|
3500
|
+
controller,
|
|
3501
|
+
fingerprint: requestFingerprint,
|
|
3502
|
+
promise: creation
|
|
3503
|
+
});
|
|
3504
|
+
return await creation;
|
|
3505
|
+
},
|
|
3506
|
+
cancel(jobId) {
|
|
3507
|
+
const job = requireJob(jobId);
|
|
3508
|
+
if (TERMINAL_STATUSES.has(job.status)) return publicSnapshot(job);
|
|
3509
|
+
if (job.status !== "cancelling") {
|
|
3510
|
+
transition(job, "cancelling", "Cancelling question.");
|
|
3511
|
+
job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3512
|
+
job.snapshot?.dispose();
|
|
3513
|
+
job.snapshot = void 0;
|
|
3514
|
+
}
|
|
3515
|
+
return publicSnapshot(job);
|
|
3516
|
+
},
|
|
3517
|
+
events(jobId, afterSequence = 0) {
|
|
3518
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
|
|
3519
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3520
|
+
}
|
|
3521
|
+
return Object.freeze(
|
|
3522
|
+
requireJob(jobId).events.filter((event) => event.sequence > afterSequence)
|
|
3523
|
+
);
|
|
3524
|
+
},
|
|
3525
|
+
async result(jobId) {
|
|
3526
|
+
const job = requireJob(jobId);
|
|
3527
|
+
await job.runPromise;
|
|
3528
|
+
const current = requireJob(jobId);
|
|
3529
|
+
return import_shared11.askJobResultResponseSchema.parse({
|
|
3530
|
+
snapshot: publicSnapshot(current),
|
|
3531
|
+
...current.result === void 0 ? {} : { result: current.result }
|
|
3532
|
+
});
|
|
3533
|
+
},
|
|
3534
|
+
snapshot(jobId) {
|
|
3535
|
+
return publicSnapshot(requireJob(jobId));
|
|
3536
|
+
},
|
|
3537
|
+
subscribe(jobId, listener) {
|
|
3538
|
+
const job = requireJob(jobId);
|
|
3539
|
+
if (job.listeners.size >= import_shared11.CONTEXTUAL_ASK_LIMITS.maximumEventSubscribers) {
|
|
3540
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3541
|
+
}
|
|
3542
|
+
job.listeners.add(listener);
|
|
3543
|
+
return () => job.listeners.delete(listener);
|
|
3544
|
+
},
|
|
3545
|
+
async close() {
|
|
3546
|
+
if (closed) return;
|
|
3547
|
+
closed = true;
|
|
3548
|
+
for (const pending of pendingRequests.values()) {
|
|
3549
|
+
pending.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3550
|
+
}
|
|
3551
|
+
await Promise.allSettled(
|
|
3552
|
+
[...pendingRequests.values()].map((pending) => pending.promise)
|
|
3553
|
+
);
|
|
3554
|
+
for (const job of jobs.values()) {
|
|
3555
|
+
if (ACTIVE_STATUSES.has(job.status)) {
|
|
3556
|
+
job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
await Promise.allSettled(
|
|
3560
|
+
[...jobs.values()].map((job) => job.runPromise ?? Promise.resolve())
|
|
3561
|
+
);
|
|
3562
|
+
for (const job of jobs.values()) {
|
|
3563
|
+
job.snapshot?.dispose();
|
|
3564
|
+
job.listeners.clear();
|
|
3565
|
+
job.lease.release();
|
|
3566
|
+
}
|
|
3567
|
+
jobs.clear();
|
|
3568
|
+
requests.clear();
|
|
3569
|
+
pendingRequests.clear();
|
|
3570
|
+
await Promise.allSettled(
|
|
3571
|
+
[...executors.values()].map(async (executor) => executor.dispose?.())
|
|
3572
|
+
);
|
|
3573
|
+
}
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
// src/contextual-ask/configured-key-executors.ts
|
|
3578
|
+
var import_agent3 = require("@spotpatch/agent");
|
|
3579
|
+
function createConfiguredKeyAskExecutors(options) {
|
|
3580
|
+
const preferred = options.defaultExecutor?.kind === "configured-key" ? options.defaultExecutor : Object.freeze({
|
|
3581
|
+
kind: "configured-key",
|
|
3582
|
+
providerProfileId: options.ai.defaultProvider,
|
|
3583
|
+
modelProfileId: options.ai.providers[options.ai.defaultProvider]?.defaultModel ?? ""
|
|
3584
|
+
});
|
|
3585
|
+
const providers = Object.values(options.ai.providers).sort(
|
|
3586
|
+
(left, right) => left.id === preferred.providerProfileId ? -1 : right.id === preferred.providerProfileId ? 1 : 0
|
|
3587
|
+
);
|
|
3588
|
+
return Object.freeze(
|
|
3589
|
+
providers.flatMap((provider) => {
|
|
3590
|
+
const credential = (0, import_agent3.resolveProviderCredential)(
|
|
3591
|
+
provider.apiKeyEnv,
|
|
3592
|
+
options.environment
|
|
3593
|
+
);
|
|
3594
|
+
const models = Object.values(provider.models).sort(
|
|
3595
|
+
(left, right) => provider.id !== preferred.providerProfileId ? 0 : left.id === preferred.modelProfileId ? -1 : right.id === preferred.modelProfileId ? 1 : 0
|
|
3596
|
+
);
|
|
3597
|
+
return models.map(
|
|
3598
|
+
(model) => (0, import_agent3.createConfiguredKeyAskExecutor)({
|
|
3599
|
+
provider,
|
|
3600
|
+
model,
|
|
3601
|
+
credential,
|
|
3602
|
+
limits: options.ai.execution.limits,
|
|
3603
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
3604
|
+
})
|
|
3605
|
+
);
|
|
3606
|
+
})
|
|
3607
|
+
);
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// src/contextual-ask/executors.ts
|
|
3611
|
+
function composeContextualAskExecutors(options) {
|
|
3612
|
+
return Object.freeze(
|
|
3613
|
+
options.defaultExecutor?.kind === "managed-codex" ? [options.managedCodex, ...options.configuredKey] : [...options.configuredKey, options.managedCodex]
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
// src/workspace/activity-coordinator.ts
|
|
3618
|
+
function createWorkspaceActivityCoordinator() {
|
|
3619
|
+
let active;
|
|
3620
|
+
return Object.freeze({
|
|
3621
|
+
acquire(kind) {
|
|
3622
|
+
if (active !== void 0) return void 0;
|
|
3623
|
+
active = kind;
|
|
3624
|
+
let released = false;
|
|
3625
|
+
return Object.freeze({
|
|
3626
|
+
release() {
|
|
3627
|
+
if (released) return;
|
|
3628
|
+
released = true;
|
|
3629
|
+
if (active === kind) active = void 0;
|
|
3630
|
+
}
|
|
3631
|
+
});
|
|
3632
|
+
},
|
|
3633
|
+
current() {
|
|
3634
|
+
return active;
|
|
3635
|
+
}
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
// src/server/middleware.ts
|
|
3640
|
+
var import_shared23 = require("@spotpatch/shared");
|
|
3641
|
+
|
|
3642
|
+
// src/contextual-ask/http.ts
|
|
3643
|
+
var import_shared12 = require("@spotpatch/shared");
|
|
3644
|
+
var STATUS_BY_ERROR = Object.freeze({
|
|
3645
|
+
ASK_DISABLED: 404,
|
|
3646
|
+
ASK_SELECTION_REQUIRED: 422,
|
|
3647
|
+
ASK_SELECTION_STALE: 409,
|
|
3648
|
+
ASK_QUESTION_INVALID: 422,
|
|
3649
|
+
ASK_EXECUTOR_UNAVAILABLE: 503,
|
|
3650
|
+
ASK_TIMEOUT: 504,
|
|
3651
|
+
ASK_CONSENT_REQUIRED: 403,
|
|
3652
|
+
ASK_BUSY: 409,
|
|
3653
|
+
ASK_IDEMPOTENCY_CONFLICT: 409,
|
|
3654
|
+
ASK_SOURCE_SCOPE_DENIED: 403,
|
|
3655
|
+
ASK_LIMIT_EXCEEDED: 413,
|
|
3656
|
+
ASK_ANSWER_INVALID: 502,
|
|
3657
|
+
ASK_WRITE_ATTEMPTED: 403,
|
|
3658
|
+
ASK_CANCELLED: 409,
|
|
3659
|
+
ASK_RESULT_EXPIRED: 410,
|
|
3660
|
+
ASK_PROTOCOL_INCOMPATIBLE: 400
|
|
3661
|
+
});
|
|
3662
|
+
var PUBLIC_MESSAGES = Object.freeze({
|
|
3663
|
+
ASK_DISABLED: "Contextual Ask is not enabled.",
|
|
3664
|
+
ASK_SELECTION_REQUIRED: "Select at least one source-backed element.",
|
|
3665
|
+
ASK_SELECTION_STALE: "The selected source is stale.",
|
|
3666
|
+
ASK_QUESTION_INVALID: "The question is invalid.",
|
|
3667
|
+
ASK_EXECUTOR_UNAVAILABLE: "The selected Ask executor is unavailable.",
|
|
3668
|
+
ASK_TIMEOUT: "The Ask executor did not answer before the deadline.",
|
|
3669
|
+
ASK_CONSENT_REQUIRED: "Provider data consent is required.",
|
|
3670
|
+
ASK_BUSY: "Another workspace task is active.",
|
|
3671
|
+
ASK_IDEMPOTENCY_CONFLICT: "The request ID was reused with different content.",
|
|
3672
|
+
ASK_SOURCE_SCOPE_DENIED: "The requested source scope was denied.",
|
|
3673
|
+
ASK_LIMIT_EXCEEDED: "The Ask request exceeded a safety limit.",
|
|
3674
|
+
ASK_ANSWER_INVALID: "The executor returned an invalid answer.",
|
|
3675
|
+
ASK_WRITE_ATTEMPTED: "Ask executors cannot write project files.",
|
|
3676
|
+
ASK_CANCELLED: "The Ask job was cancelled.",
|
|
3677
|
+
ASK_RESULT_EXPIRED: "The Ask result has expired.",
|
|
3678
|
+
ASK_PROTOCOL_INCOMPATIBLE: "The Ask protocol request is incompatible."
|
|
3679
|
+
});
|
|
3680
|
+
function writeJson2(response, status, data) {
|
|
3681
|
+
response.statusCode = status;
|
|
3682
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3683
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3684
|
+
response.end(JSON.stringify({ ok: true, data }));
|
|
3685
|
+
}
|
|
3686
|
+
function writeError(response, error, logger) {
|
|
3687
|
+
const normalized = error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
|
|
3688
|
+
if (normalized.code === "ASK_EXECUTOR_UNAVAILABLE" && error !== normalized) {
|
|
3689
|
+
logger?.warn("[spotpatch:ask] Internal contextual Ask request failure.");
|
|
3690
|
+
}
|
|
3691
|
+
response.statusCode = STATUS_BY_ERROR[normalized.code];
|
|
3692
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3693
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3694
|
+
response.end(
|
|
3695
|
+
JSON.stringify({
|
|
3696
|
+
ok: false,
|
|
3697
|
+
error: {
|
|
3698
|
+
code: normalized.code,
|
|
3699
|
+
message: PUBLIC_MESSAGES[normalized.code]
|
|
3700
|
+
}
|
|
3701
|
+
})
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
function matchContextualAskPath(pathname) {
|
|
3705
|
+
if (pathname === import_shared12.SPOTPATCH_ENDPOINTS.askCapability) {
|
|
3706
|
+
return Object.freeze({ kind: "capability" });
|
|
3707
|
+
}
|
|
3708
|
+
if (pathname === import_shared12.SPOTPATCH_ENDPOINTS.askJobs) {
|
|
3709
|
+
return Object.freeze({ kind: "create" });
|
|
3710
|
+
}
|
|
3711
|
+
const prefix = `${import_shared12.SPOTPATCH_ENDPOINTS.askJobs}/`;
|
|
3712
|
+
if (!pathname.startsWith(prefix)) return void 0;
|
|
3713
|
+
const segments = pathname.slice(prefix.length).split("/");
|
|
3714
|
+
if (segments.length !== 2) return void 0;
|
|
3715
|
+
let jobId;
|
|
3716
|
+
try {
|
|
3717
|
+
jobId = decodeURIComponent(segments[0] ?? "");
|
|
3718
|
+
} catch {
|
|
3719
|
+
return void 0;
|
|
3720
|
+
}
|
|
3721
|
+
const action = segments[1];
|
|
3722
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/u.test(jobId) || action !== "events" && action !== "result" && action !== "cancel") {
|
|
3723
|
+
return void 0;
|
|
3724
|
+
}
|
|
3725
|
+
return Object.freeze({ kind: "job", action, jobId });
|
|
3726
|
+
}
|
|
3727
|
+
function parseAfterSequence(request) {
|
|
3728
|
+
let url;
|
|
3729
|
+
try {
|
|
3730
|
+
url = new URL(request.url ?? "/", "http://spotpatch.invalid");
|
|
3731
|
+
} catch {
|
|
3732
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3733
|
+
}
|
|
3734
|
+
if ([...url.searchParams.keys()].some((key) => key !== "afterSequence")) {
|
|
3735
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3736
|
+
}
|
|
3737
|
+
const raw = url.searchParams.get("afterSequence");
|
|
3738
|
+
if (raw === null) return 0;
|
|
3739
|
+
if (!/^\d+$/u.test(raw)) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3740
|
+
const parsed = import_shared12.askJobEventsRequestSchema.safeParse({ afterSequence: Number(raw) });
|
|
3741
|
+
if (!parsed.success || !Number.isSafeInteger(parsed.data.afterSequence)) {
|
|
3742
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3743
|
+
}
|
|
3744
|
+
return parsed.data.afterSequence ?? 0;
|
|
3745
|
+
}
|
|
3746
|
+
function streamEvents(request, response, manager, jobId) {
|
|
3747
|
+
const afterSequence = parseAfterSequence(request);
|
|
3748
|
+
response.statusCode = 200;
|
|
3749
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3750
|
+
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
3751
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
3752
|
+
let unsubscribe = () => void 0;
|
|
3753
|
+
const heartbeat = setInterval(
|
|
3754
|
+
() => response.write("\n"),
|
|
3755
|
+
import_shared12.CONTEXTUAL_ASK_LIMITS.eventHeartbeatMs
|
|
3756
|
+
);
|
|
3757
|
+
heartbeat.unref();
|
|
3758
|
+
const cleanup = () => {
|
|
3759
|
+
clearInterval(heartbeat);
|
|
3760
|
+
unsubscribe();
|
|
3761
|
+
};
|
|
3762
|
+
unsubscribe = manager.subscribe(jobId, (event) => {
|
|
3763
|
+
response.write(`${JSON.stringify(event)}
|
|
3764
|
+
`);
|
|
3765
|
+
if (["answered", "cancelled", "failed"].includes(event.status)) {
|
|
3766
|
+
cleanup();
|
|
3767
|
+
response.end();
|
|
3768
|
+
}
|
|
3769
|
+
});
|
|
3770
|
+
const replay = manager.events(jobId, afterSequence);
|
|
3771
|
+
for (const event of replay) response.write(`${JSON.stringify(event)}
|
|
3772
|
+
`);
|
|
3773
|
+
const latest = replay.at(-1);
|
|
3774
|
+
const currentStatus = latest?.status ?? manager.snapshot(jobId).status;
|
|
3775
|
+
if (["answered", "cancelled", "failed"].includes(currentStatus)) {
|
|
3776
|
+
cleanup();
|
|
3777
|
+
response.end();
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
request.once("close", cleanup);
|
|
3781
|
+
response.once("close", cleanup);
|
|
3782
|
+
}
|
|
3783
|
+
function hasEmptySelection(value) {
|
|
3784
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3785
|
+
const envelope = value.envelope;
|
|
3786
|
+
if (typeof envelope !== "object" || envelope === null) return false;
|
|
3787
|
+
const selection = envelope.selection;
|
|
3788
|
+
if (typeof selection !== "object" || selection === null) return false;
|
|
3789
|
+
return Array.isArray(selection.targets) && selection.targets.length === 0;
|
|
3790
|
+
}
|
|
3791
|
+
function lacksProviderDataConsent(value) {
|
|
3792
|
+
return typeof value === "object" && value !== null && value.providerDataConsent !== true;
|
|
3793
|
+
}
|
|
3794
|
+
async function handleContextualAskRequest(request, response, route, manager, logger) {
|
|
3795
|
+
try {
|
|
3796
|
+
if (manager === void 0) throw new ContextualAskError("ASK_DISABLED");
|
|
3797
|
+
if (route.kind === "capability") {
|
|
3798
|
+
if (request.method !== "GET")
|
|
3799
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3800
|
+
writeJson2(
|
|
3801
|
+
response,
|
|
3802
|
+
200,
|
|
3803
|
+
await manager.capability(
|
|
3804
|
+
AbortSignal.timeout(import_shared12.CONTEXTUAL_ASK_LIMITS.capabilityTimeoutMs)
|
|
3805
|
+
)
|
|
3806
|
+
);
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
if (route.kind === "create") {
|
|
3810
|
+
if (request.method !== "POST")
|
|
3811
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3812
|
+
const raw = await readJsonRequestBody(
|
|
3813
|
+
request,
|
|
3814
|
+
import_shared12.CONTEXTUAL_ASK_LIMITS.maximumRequestBodyBytes
|
|
3815
|
+
).catch((error) => {
|
|
3816
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
|
|
3817
|
+
});
|
|
3818
|
+
const parsed = import_shared12.askJobCreateRequestSchema.safeParse(raw);
|
|
3819
|
+
if (!parsed.success) {
|
|
3820
|
+
throw new ContextualAskError(
|
|
3821
|
+
hasEmptySelection(raw) ? "ASK_SELECTION_REQUIRED" : lacksProviderDataConsent(raw) ? "ASK_CONSENT_REQUIRED" : "ASK_PROTOCOL_INCOMPATIBLE"
|
|
3822
|
+
);
|
|
3823
|
+
}
|
|
3824
|
+
writeJson2(response, 202, await manager.create(parsed.data));
|
|
3825
|
+
return;
|
|
3826
|
+
}
|
|
3827
|
+
if (route.action === "events") {
|
|
3828
|
+
if (request.method !== "GET")
|
|
3829
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3830
|
+
streamEvents(request, response, manager, route.jobId);
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
if (route.action === "result") {
|
|
3834
|
+
if (request.method !== "GET")
|
|
3835
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3836
|
+
writeJson2(response, 200, await manager.result(route.jobId));
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
if (request.method !== "POST")
|
|
3840
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3841
|
+
const action = import_shared12.askJobActionRequestSchema.safeParse(
|
|
3842
|
+
await readJsonRequestBody(request).catch((error) => {
|
|
3843
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
|
|
3844
|
+
})
|
|
3845
|
+
);
|
|
3846
|
+
if (!action.success) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3847
|
+
writeJson2(response, 200, manager.cancel(route.jobId));
|
|
3848
|
+
} catch (error) {
|
|
3849
|
+
writeError(response, error, logger);
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// src/external-handoff/browser-http.ts
|
|
3854
|
+
var import_shared16 = require("@spotpatch/shared");
|
|
3855
|
+
|
|
3856
|
+
// src/server/annotation-authorizer.ts
|
|
3857
|
+
var import_promises7 = require("fs/promises");
|
|
3858
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
3859
|
+
var import_shared15 = require("@spotpatch/shared");
|
|
3860
|
+
|
|
3861
|
+
// src/server/source-context.ts
|
|
3862
|
+
var import_promises6 = require("fs/promises");
|
|
3863
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
3864
|
+
var import_shared14 = require("@spotpatch/shared");
|
|
3865
|
+
|
|
3866
|
+
// src/server/extract-code-context.ts
|
|
3867
|
+
var import_oxc_parser2 = require("oxc-parser");
|
|
3868
|
+
function isComponentName(name) {
|
|
3869
|
+
return /^[A-Z]/u.test(name);
|
|
3870
|
+
}
|
|
3871
|
+
function unwrapTypeExpression(expression) {
|
|
3872
|
+
switch (expression.type) {
|
|
3873
|
+
case "TSAsExpression":
|
|
3874
|
+
case "TSSatisfiesExpression":
|
|
3875
|
+
case "TSTypeAssertion":
|
|
3876
|
+
case "TSNonNullExpression":
|
|
3877
|
+
case "TSInstantiationExpression":
|
|
3878
|
+
return unwrapTypeExpression(expression.expression);
|
|
3879
|
+
default:
|
|
3880
|
+
return expression;
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
function calleeName(expression) {
|
|
3884
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
3885
|
+
if (unwrapped.type === "Identifier") {
|
|
3886
|
+
return unwrapped.name;
|
|
3887
|
+
}
|
|
3888
|
+
if (unwrapped.type === "MemberExpression" && !unwrapped.computed) {
|
|
3889
|
+
return unwrapped.property.type === "Identifier" ? unwrapped.property.name : void 0;
|
|
3890
|
+
}
|
|
3891
|
+
return void 0;
|
|
3892
|
+
}
|
|
3893
|
+
function isFunctionExpression(expression) {
|
|
3894
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
3895
|
+
return unwrapped.type === "ArrowFunctionExpression" || unwrapped.type === "FunctionExpression";
|
|
3896
|
+
}
|
|
3897
|
+
function isSupportedComponentInitializer(expression) {
|
|
3898
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
3899
|
+
if (isFunctionExpression(unwrapped)) {
|
|
3900
|
+
return true;
|
|
3901
|
+
}
|
|
3902
|
+
if (unwrapped.type !== "CallExpression") {
|
|
3903
|
+
return false;
|
|
3904
|
+
}
|
|
3905
|
+
const name = calleeName(unwrapped.callee);
|
|
3906
|
+
if (name !== "memo" && name !== "forwardRef") {
|
|
3907
|
+
return false;
|
|
3908
|
+
}
|
|
3909
|
+
const firstArgument = unwrapped.arguments[0];
|
|
3910
|
+
return firstArgument !== void 0 && firstArgument.type !== "SpreadElement" && (isFunctionExpression(firstArgument) || isSupportedComponentInitializer(firstArgument));
|
|
3911
|
+
}
|
|
3912
|
+
function variableComponent(node) {
|
|
3913
|
+
if (node.id.type !== "Identifier" || !isComponentName(node.id.name) || node.init === null || !isSupportedComponentInitializer(node.init)) {
|
|
3914
|
+
return void 0;
|
|
3915
|
+
}
|
|
3916
|
+
return Object.freeze({ start: node.start, end: node.end, name: node.id.name });
|
|
3917
|
+
}
|
|
3918
|
+
function functionComponent(node) {
|
|
3919
|
+
return node.id !== null && isComponentName(node.id.name) && node.body !== null ? Object.freeze({ start: node.start, end: node.end, name: node.id.name }) : void 0;
|
|
3920
|
+
}
|
|
3921
|
+
function isReactComponentSuperclass(expression) {
|
|
3922
|
+
if (expression === null) {
|
|
3923
|
+
return false;
|
|
3924
|
+
}
|
|
3925
|
+
const unwrapped = unwrapTypeExpression(expression);
|
|
3926
|
+
if (unwrapped.type === "Identifier") {
|
|
3927
|
+
return unwrapped.name === "Component" || unwrapped.name === "PureComponent";
|
|
3928
|
+
}
|
|
3929
|
+
return unwrapped.type === "MemberExpression" && !unwrapped.computed && unwrapped.object.type === "Identifier" && unwrapped.object.name === "React" && (unwrapped.property.name === "Component" || unwrapped.property.name === "PureComponent");
|
|
3930
|
+
}
|
|
3931
|
+
function classComponent(node) {
|
|
3932
|
+
return node.id !== null && isComponentName(node.id.name) && isReactComponentSuperclass(node.superClass) ? Object.freeze({ start: node.start, end: node.end, name: node.id.name }) : void 0;
|
|
3933
|
+
}
|
|
3934
|
+
function selectedOffset(source, line, column) {
|
|
3935
|
+
const lines = source.split(/\r?\n/u);
|
|
3936
|
+
if (line < 1 || line > lines.length) {
|
|
3937
|
+
return void 0;
|
|
3938
|
+
}
|
|
3939
|
+
const lineStart = lines.slice(0, line - 1).reduce((total, value) => total + value.length + 1, 0);
|
|
3940
|
+
const lineLength = lines[line - 1]?.length ?? 0;
|
|
3941
|
+
return lineStart + Math.min(Math.max(0, column - 1), lineLength);
|
|
3942
|
+
}
|
|
3943
|
+
function findComponentSpan(options) {
|
|
3944
|
+
const offset = selectedOffset(options.source, options.line, options.column);
|
|
3945
|
+
if (offset === void 0) {
|
|
3946
|
+
return void 0;
|
|
3947
|
+
}
|
|
3948
|
+
let parseResult;
|
|
3949
|
+
try {
|
|
3950
|
+
parseResult = (0, import_oxc_parser2.parseSync)(options.sourcePath, options.source, {
|
|
3951
|
+
sourceType: "module"
|
|
3952
|
+
});
|
|
3953
|
+
} catch {
|
|
3954
|
+
return void 0;
|
|
3955
|
+
}
|
|
3956
|
+
if (parseResult.errors.length > 0) {
|
|
3957
|
+
return void 0;
|
|
3958
|
+
}
|
|
3959
|
+
const jsxNodes = [];
|
|
3960
|
+
const components = [];
|
|
3961
|
+
const visitor = new import_oxc_parser2.Visitor({
|
|
3962
|
+
JSXElement(node) {
|
|
3963
|
+
jsxNodes.push(node);
|
|
3964
|
+
},
|
|
3965
|
+
JSXFragment(node) {
|
|
3966
|
+
jsxNodes.push(node);
|
|
3967
|
+
},
|
|
3968
|
+
FunctionDeclaration(node) {
|
|
3969
|
+
const candidate = functionComponent(node);
|
|
3970
|
+
if (candidate !== void 0) {
|
|
3971
|
+
components.push(candidate);
|
|
3972
|
+
}
|
|
3973
|
+
},
|
|
3974
|
+
VariableDeclarator(node) {
|
|
3975
|
+
const candidate = variableComponent(node);
|
|
3976
|
+
if (candidate !== void 0) {
|
|
3977
|
+
components.push(candidate);
|
|
3978
|
+
}
|
|
3979
|
+
},
|
|
3980
|
+
ClassDeclaration(node) {
|
|
3981
|
+
const candidate = classComponent(node);
|
|
3982
|
+
if (candidate !== void 0) {
|
|
3983
|
+
components.push(candidate);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
});
|
|
3987
|
+
visitor.visit(parseResult.program);
|
|
3988
|
+
const selectedJsx = jsxNodes.filter((node) => node.start <= offset && node.end >= offset).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
3989
|
+
if (selectedJsx === void 0) {
|
|
3990
|
+
return void 0;
|
|
3991
|
+
}
|
|
3992
|
+
return components.filter(
|
|
3993
|
+
(component) => component.start <= selectedJsx.start && component.end >= selectedJsx.end
|
|
3994
|
+
).sort((left, right) => left.end - left.start - (right.end - right.start))[0];
|
|
3995
|
+
}
|
|
3996
|
+
function lineAtOffset(source, offset) {
|
|
3997
|
+
let line = 1;
|
|
3998
|
+
for (let index = 0; index < offset; index += 1) {
|
|
3999
|
+
if (source[index] === "\n") {
|
|
4000
|
+
line += 1;
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return line;
|
|
4004
|
+
}
|
|
4005
|
+
function componentRange(source, component) {
|
|
4006
|
+
return Object.freeze({
|
|
4007
|
+
startLine: lineAtOffset(source, component.start),
|
|
4008
|
+
endLine: lineAtOffset(source, Math.max(component.start, component.end - 1))
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
function truncateSelectedLine(line, column, maxCharacters) {
|
|
4012
|
+
if (line.length <= maxCharacters) {
|
|
4013
|
+
return line;
|
|
4014
|
+
}
|
|
4015
|
+
if (maxCharacters === 1) {
|
|
4016
|
+
return "\u2026";
|
|
4017
|
+
}
|
|
4018
|
+
const contentCharacters = maxCharacters - 2;
|
|
4019
|
+
const desiredStart = Math.max(0, column - 1 - Math.floor(contentCharacters / 2));
|
|
4020
|
+
const start = Math.min(desiredStart, line.length - contentCharacters);
|
|
4021
|
+
const end = start + contentCharacters;
|
|
4022
|
+
return `${start > 0 ? "\u2026" : ""}${line.slice(start, end)}${end < line.length ? "\u2026" : ""}`.slice(
|
|
2665
4023
|
0,
|
|
2666
4024
|
maxCharacters
|
|
2667
4025
|
);
|
|
@@ -2732,9 +4090,9 @@ function extractCodeContext(options) {
|
|
|
2732
4090
|
}
|
|
2733
4091
|
|
|
2734
4092
|
// src/server/source-file.ts
|
|
2735
|
-
var
|
|
2736
|
-
var
|
|
2737
|
-
var
|
|
4093
|
+
var import_promises5 = require("fs/promises");
|
|
4094
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
4095
|
+
var import_shared13 = require("@spotpatch/shared");
|
|
2738
4096
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
2739
4097
|
function isMissingFileError(error) {
|
|
2740
4098
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
@@ -2744,56 +4102,56 @@ async function assertInsideRoot(root, candidate) {
|
|
|
2744
4102
|
let realCandidate;
|
|
2745
4103
|
try {
|
|
2746
4104
|
[realRoot, realCandidate] = await Promise.all([
|
|
2747
|
-
(0,
|
|
2748
|
-
(0,
|
|
4105
|
+
(0, import_promises5.realpath)(root),
|
|
4106
|
+
(0, import_promises5.realpath)(candidate)
|
|
2749
4107
|
]);
|
|
2750
4108
|
} catch (error) {
|
|
2751
4109
|
if (isMissingFileError(error)) {
|
|
2752
|
-
throw new
|
|
4110
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
2753
4111
|
cause: error
|
|
2754
4112
|
});
|
|
2755
4113
|
}
|
|
2756
4114
|
throw error;
|
|
2757
4115
|
}
|
|
2758
|
-
const relative =
|
|
2759
|
-
const outside = relative.startsWith(`..${
|
|
4116
|
+
const relative = import_node_path6.default.relative(realRoot, realCandidate);
|
|
4117
|
+
const outside = relative.startsWith(`..${import_node_path6.default.sep}`) || relative === ".." || import_node_path6.default.isAbsolute(relative);
|
|
2760
4118
|
if (outside) {
|
|
2761
|
-
throw new
|
|
4119
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.SOURCE_OUTSIDE_ROOT);
|
|
2762
4120
|
}
|
|
2763
4121
|
return realCandidate;
|
|
2764
4122
|
}
|
|
2765
4123
|
async function resolveSourceFile(options) {
|
|
2766
4124
|
const registeredPath = options.registry.resolve(options.fileId);
|
|
2767
4125
|
if (registeredPath === void 0) {
|
|
2768
|
-
throw new
|
|
4126
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
2769
4127
|
}
|
|
2770
4128
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
2771
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
2772
|
-
throw new
|
|
4129
|
+
if (!ALLOWED_EXTENSIONS.has(import_node_path6.default.extname(sourcePath).toLowerCase())) {
|
|
4130
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
2773
4131
|
}
|
|
2774
4132
|
let sourceStat;
|
|
2775
4133
|
try {
|
|
2776
|
-
sourceStat = await (0,
|
|
4134
|
+
sourceStat = await (0, import_promises5.stat)(sourcePath);
|
|
2777
4135
|
} catch (error) {
|
|
2778
4136
|
if (isMissingFileError(error)) {
|
|
2779
|
-
throw new
|
|
4137
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
2780
4138
|
cause: error
|
|
2781
4139
|
});
|
|
2782
4140
|
}
|
|
2783
4141
|
throw error;
|
|
2784
4142
|
}
|
|
2785
4143
|
if (!sourceStat.isFile()) {
|
|
2786
|
-
throw new
|
|
4144
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.SOURCE_NOT_FOUND);
|
|
2787
4145
|
}
|
|
2788
4146
|
if (sourceStat.size > MAX_SOURCE_FILE_BYTES) {
|
|
2789
|
-
throw new
|
|
4147
|
+
throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.SOURCE_TOO_LARGE);
|
|
2790
4148
|
}
|
|
2791
4149
|
return sourcePath;
|
|
2792
4150
|
}
|
|
2793
4151
|
|
|
2794
4152
|
// src/server/source-context.ts
|
|
2795
4153
|
function toDisplayPath(root, sourcePath) {
|
|
2796
|
-
return
|
|
4154
|
+
return import_node_path7.default.relative(root, sourcePath).split(import_node_path7.default.sep).join("/");
|
|
2797
4155
|
}
|
|
2798
4156
|
async function readSourceContext(options) {
|
|
2799
4157
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2803,10 +4161,10 @@ async function readSourceContext(options) {
|
|
|
2803
4161
|
});
|
|
2804
4162
|
let source;
|
|
2805
4163
|
try {
|
|
2806
|
-
source = await (0,
|
|
4164
|
+
source = await (0, import_promises6.readFile)(sourcePath, "utf8");
|
|
2807
4165
|
} catch (error) {
|
|
2808
4166
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
2809
|
-
throw new
|
|
4167
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.SOURCE_NOT_FOUND, void 0, {
|
|
2810
4168
|
cause: error
|
|
2811
4169
|
});
|
|
2812
4170
|
}
|
|
@@ -2814,13 +4172,13 @@ async function readSourceContext(options) {
|
|
|
2814
4172
|
}
|
|
2815
4173
|
const lines = source.split(/\r?\n/);
|
|
2816
4174
|
if (options.request.line > lines.length) {
|
|
2817
|
-
throw new
|
|
4175
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.INVALID_REQUEST);
|
|
2818
4176
|
}
|
|
2819
|
-
const extension =
|
|
4177
|
+
const extension = import_node_path7.default.extname(sourcePath).toLowerCase();
|
|
2820
4178
|
return extractCodeContext({
|
|
2821
4179
|
source,
|
|
2822
4180
|
sourcePath,
|
|
2823
|
-
relativePath: toDisplayPath(await (0,
|
|
4181
|
+
relativePath: toDisplayPath(await (0, import_promises6.realpath)(options.root), sourcePath),
|
|
2824
4182
|
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
2825
4183
|
line: options.request.line,
|
|
2826
4184
|
column: options.request.column,
|
|
@@ -2832,9 +4190,9 @@ async function readSourceContext(options) {
|
|
|
2832
4190
|
// src/server/annotation-authorizer.ts
|
|
2833
4191
|
function sanitizePageContext(page) {
|
|
2834
4192
|
return Object.freeze({
|
|
2835
|
-
url: (0,
|
|
2836
|
-
pathname: (0,
|
|
2837
|
-
title: (0,
|
|
4193
|
+
url: (0, import_shared15.sanitizeUrl)(page.url, page.url),
|
|
4194
|
+
pathname: (0, import_shared15.redactSensitiveText)(page.pathname),
|
|
4195
|
+
title: (0, import_shared15.redactSensitiveText)(page.title),
|
|
2838
4196
|
viewportWidth: page.viewportWidth,
|
|
2839
4197
|
viewportHeight: page.viewportHeight,
|
|
2840
4198
|
devicePixelRatio: page.devicePixelRatio
|
|
@@ -2853,18 +4211,18 @@ function compactSourceRef(source) {
|
|
|
2853
4211
|
async function authorizeSourceRef(source, registry, root) {
|
|
2854
4212
|
const markerOrigin = source.origin === "jsx-host" || source.origin === "dom-ancestor";
|
|
2855
4213
|
if (markerOrigin && (source.fileId === void 0 || source.line === void 0 || source.column === void 0)) {
|
|
2856
|
-
throw new
|
|
4214
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2857
4215
|
}
|
|
2858
4216
|
if (source.fileId === void 0) {
|
|
2859
4217
|
if (source.origin === "none" && source.relativePath !== void 0) {
|
|
2860
|
-
throw new
|
|
4218
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2861
4219
|
}
|
|
2862
4220
|
return compactSourceRef(source);
|
|
2863
4221
|
}
|
|
2864
4222
|
const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
|
|
2865
|
-
const relativePath =
|
|
4223
|
+
const relativePath = import_node_path8.default.relative(await (0, import_promises7.realpath)(root), sourcePath).split(import_node_path8.default.sep).join("/");
|
|
2866
4224
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
2867
|
-
throw new
|
|
4225
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2868
4226
|
}
|
|
2869
4227
|
return Object.freeze({ ...compactSourceRef(source), relativePath });
|
|
2870
4228
|
}
|
|
@@ -2900,7 +4258,7 @@ async function authorizeTarget(target, input) {
|
|
|
2900
4258
|
maxLines: input.options.budget.maxCodeLines
|
|
2901
4259
|
});
|
|
2902
4260
|
if (marker === void 0 && target.code !== void 0) {
|
|
2903
|
-
throw new
|
|
4261
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2904
4262
|
}
|
|
2905
4263
|
const code = marker === void 0 ? void 0 : await readSourceContext({
|
|
2906
4264
|
request: marker,
|
|
@@ -2910,7 +4268,7 @@ async function authorizeTarget(target, input) {
|
|
|
2910
4268
|
maxLines: input.options.budget.maxCodeLines
|
|
2911
4269
|
});
|
|
2912
4270
|
if (target.code !== void 0 && target.code.relativePath !== code?.relativePath) {
|
|
2913
|
-
throw new
|
|
4271
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2914
4272
|
}
|
|
2915
4273
|
return Object.freeze({
|
|
2916
4274
|
instruction: target.instruction,
|
|
@@ -2945,11 +4303,11 @@ async function authorizeTarget(target, input) {
|
|
|
2945
4303
|
async function authorizeAnnotation(input) {
|
|
2946
4304
|
const requestedTargets = input.annotation.targets;
|
|
2947
4305
|
if (requestedTargets.length > input.options.maxTargets) {
|
|
2948
|
-
throw new
|
|
4306
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2949
4307
|
}
|
|
2950
4308
|
const identities = requestedTargets.map(targetIdentity);
|
|
2951
4309
|
if (new Set(identities).size !== identities.length) {
|
|
2952
|
-
throw new
|
|
4310
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INVALID_REQUEST);
|
|
2953
4311
|
}
|
|
2954
4312
|
const targets = Object.freeze(
|
|
2955
4313
|
await Promise.all(requestedTargets.map((target) => authorizeTarget(target, input)))
|
|
@@ -2965,30 +4323,30 @@ async function authorizeAnnotation(input) {
|
|
|
2965
4323
|
}
|
|
2966
4324
|
|
|
2967
4325
|
// src/external-handoff/browser-http.ts
|
|
2968
|
-
function matchExternalHandoffBrowserPath(
|
|
2969
|
-
if (
|
|
2970
|
-
if (
|
|
2971
|
-
if (
|
|
2972
|
-
if (
|
|
4326
|
+
function matchExternalHandoffBrowserPath(path10) {
|
|
4327
|
+
if (path10 === import_shared16.SPOTPATCH_ENDPOINTS.externalHandoffCapability) return "capability";
|
|
4328
|
+
if (path10 === import_shared16.SPOTPATCH_ENDPOINTS.externalHandoffPublish) return "publish";
|
|
4329
|
+
if (path10 === import_shared16.SPOTPATCH_ENDPOINTS.externalHandoffStatus) return "status";
|
|
4330
|
+
if (path10 === import_shared16.SPOTPATCH_ENDPOINTS.externalHandoffResolveDelivery) {
|
|
2973
4331
|
return "resolve-delivery";
|
|
2974
4332
|
}
|
|
2975
4333
|
return void 0;
|
|
2976
4334
|
}
|
|
2977
4335
|
function requireService(options) {
|
|
2978
4336
|
if (!options.options.externalAgent.enabled || options.service === void 0) {
|
|
2979
|
-
throw new
|
|
4337
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED);
|
|
2980
4338
|
}
|
|
2981
4339
|
return options.service;
|
|
2982
4340
|
}
|
|
2983
4341
|
function remapAuthorizationError(error) {
|
|
2984
|
-
if (error instanceof
|
|
2985
|
-
if (error.code ===
|
|
2986
|
-
throw new
|
|
4342
|
+
if (error instanceof import_shared16.SpotPatchError) {
|
|
4343
|
+
if (error.code === import_shared16.ERROR_CODES.SOURCE_NOT_FOUND || error.code === import_shared16.ERROR_CODES.SOURCE_OUTSIDE_ROOT || error.code === import_shared16.ERROR_CODES.SOURCE_TOO_LARGE) {
|
|
4344
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.HANDOFF_SOURCE_STALE, void 0, {
|
|
2987
4345
|
cause: error
|
|
2988
4346
|
});
|
|
2989
4347
|
}
|
|
2990
|
-
if (error.code ===
|
|
2991
|
-
throw new
|
|
4348
|
+
if (error.code === import_shared16.ERROR_CODES.INVALID_REQUEST) {
|
|
4349
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.HANDOFF_VALIDATION_FAILED, void 0, {
|
|
2992
4350
|
cause: error
|
|
2993
4351
|
});
|
|
2994
4352
|
}
|
|
@@ -2997,38 +4355,38 @@ function remapAuthorizationError(error) {
|
|
|
2997
4355
|
}
|
|
2998
4356
|
async function handleExternalHandoffBrowserRequest(request, response, route, options, writeSuccess) {
|
|
2999
4357
|
if (request.method !== "POST") {
|
|
3000
|
-
throw new
|
|
4358
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
3001
4359
|
}
|
|
3002
4360
|
const service = requireService(options);
|
|
3003
4361
|
if (route === "capability") {
|
|
3004
|
-
const parsed2 =
|
|
4362
|
+
const parsed2 = import_shared16.externalHandoffCapabilityRequestSchema.safeParse(
|
|
3005
4363
|
await readJsonRequestBody(request)
|
|
3006
4364
|
);
|
|
3007
|
-
if (!parsed2.success) throw new
|
|
4365
|
+
if (!parsed2.success) throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
3008
4366
|
writeSuccess(response, 200, service.capability());
|
|
3009
4367
|
return;
|
|
3010
4368
|
}
|
|
3011
4369
|
if (route === "status") {
|
|
3012
|
-
const parsed2 =
|
|
4370
|
+
const parsed2 = import_shared16.externalHandoffStatusRequestSchema.safeParse(
|
|
3013
4371
|
await readJsonRequestBody(request)
|
|
3014
4372
|
);
|
|
3015
|
-
if (!parsed2.success) throw new
|
|
4373
|
+
if (!parsed2.success) throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
3016
4374
|
writeSuccess(response, 200, service.status(parsed2.data.cursor));
|
|
3017
4375
|
return;
|
|
3018
4376
|
}
|
|
3019
4377
|
if (route === "resolve-delivery") {
|
|
3020
|
-
const parsed2 =
|
|
4378
|
+
const parsed2 = import_shared16.externalHandoffResolveDeliveryRequestSchema.safeParse(
|
|
3021
4379
|
await readJsonRequestBody(request)
|
|
3022
4380
|
);
|
|
3023
|
-
if (!parsed2.success) throw new
|
|
4381
|
+
if (!parsed2.success) throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INVALID_REQUEST);
|
|
3024
4382
|
writeSuccess(response, 200, service.resolveDelivery(parsed2.data.cursor));
|
|
3025
4383
|
return;
|
|
3026
4384
|
}
|
|
3027
|
-
const parsed =
|
|
3028
|
-
await readJsonRequestBody(request,
|
|
4385
|
+
const parsed = import_shared16.externalHandoffPublishRequestSchema.safeParse(
|
|
4386
|
+
await readJsonRequestBody(request, import_shared16.EXTERNAL_HANDOFF_LIMITS.maximumPublishBodyBytes)
|
|
3029
4387
|
);
|
|
3030
4388
|
if (!parsed.success) {
|
|
3031
|
-
throw new
|
|
4389
|
+
throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.HANDOFF_VALIDATION_FAILED);
|
|
3032
4390
|
}
|
|
3033
4391
|
const result = await service.publish(parsed.data, async (annotation) => {
|
|
3034
4392
|
try {
|
|
@@ -3046,41 +4404,41 @@ async function handleExternalHandoffBrowserRequest(request, response, route, opt
|
|
|
3046
4404
|
}
|
|
3047
4405
|
|
|
3048
4406
|
// src/external-agent/browser-http.ts
|
|
3049
|
-
var
|
|
3050
|
-
function matchExternalAgentBrowserPath(
|
|
3051
|
-
if (
|
|
3052
|
-
if (
|
|
3053
|
-
if (
|
|
4407
|
+
var import_shared17 = require("@spotpatch/shared");
|
|
4408
|
+
function matchExternalAgentBrowserPath(path10) {
|
|
4409
|
+
if (path10 === import_shared17.SPOTPATCH_ENDPOINTS.externalAgentControlStatus) return "status";
|
|
4410
|
+
if (path10 === import_shared17.SPOTPATCH_ENDPOINTS.externalAgentControlConnect) return "connect";
|
|
4411
|
+
if (path10 === import_shared17.SPOTPATCH_ENDPOINTS.externalAgentControlDisconnect) {
|
|
3054
4412
|
return "disconnect";
|
|
3055
4413
|
}
|
|
3056
|
-
if (
|
|
3057
|
-
if (
|
|
3058
|
-
if (
|
|
4414
|
+
if (path10 === import_shared17.SPOTPATCH_ENDPOINTS.externalAgentControlCancel) return "cancel";
|
|
4415
|
+
if (path10 === import_shared17.SPOTPATCH_ENDPOINTS.externalAgentEvents) return "events";
|
|
4416
|
+
if (path10 === import_shared17.SPOTPATCH_ENDPOINTS.externalAgentResult) return "result";
|
|
3059
4417
|
return void 0;
|
|
3060
4418
|
}
|
|
3061
4419
|
function writeEvent(response, event) {
|
|
3062
|
-
response.write(`${JSON.stringify(
|
|
4420
|
+
response.write(`${JSON.stringify(import_shared17.externalAgentEventSchema.parse(event))}
|
|
3063
4421
|
`);
|
|
3064
4422
|
}
|
|
3065
4423
|
function createExternalAgentBrowserController(port) {
|
|
3066
4424
|
const streams = /* @__PURE__ */ new Set();
|
|
3067
4425
|
let disposed = false;
|
|
3068
4426
|
const handleEvents = async (request, response) => {
|
|
3069
|
-
const parsed =
|
|
4427
|
+
const parsed = import_shared17.externalAgentEventsRequestSchema.safeParse(
|
|
3070
4428
|
await readJsonRequestBody(request)
|
|
3071
4429
|
);
|
|
3072
4430
|
if (!parsed.success) {
|
|
3073
|
-
throw new
|
|
4431
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
3074
4432
|
}
|
|
3075
|
-
if (disposed || streams.size >=
|
|
3076
|
-
throw new
|
|
4433
|
+
if (disposed || streams.size >= import_shared17.EXTERNAL_AGENT_CONTROL_LIMITS.maximumEventSubscribers) {
|
|
4434
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.BRIDGE_BUSY);
|
|
3077
4435
|
}
|
|
3078
4436
|
response.statusCode = 200;
|
|
3079
4437
|
response.setHeader("Cache-Control", "no-store");
|
|
3080
4438
|
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
3081
4439
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
3082
4440
|
streams.add(response);
|
|
3083
|
-
const initial =
|
|
4441
|
+
const initial = import_shared17.externalAgentControlStatusSchema.parse(port.getStatus());
|
|
3084
4442
|
writeEvent(response, { type: "status", data: initial });
|
|
3085
4443
|
const unsubscribe = port.subscribe((status) => {
|
|
3086
4444
|
if (!response.destroyed && status.sequence > (parsed.data.afterSequence ?? -1)) {
|
|
@@ -3096,7 +4454,7 @@ function createExternalAgentBrowserController(port) {
|
|
|
3096
4454
|
emittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3097
4455
|
});
|
|
3098
4456
|
}
|
|
3099
|
-
},
|
|
4457
|
+
}, import_shared17.EXTERNAL_AGENT_CONTROL_LIMITS.eventHeartbeatMs);
|
|
3100
4458
|
heartbeat.unref();
|
|
3101
4459
|
await new Promise((resolve) => {
|
|
3102
4460
|
const close = () => {
|
|
@@ -3112,29 +4470,29 @@ function createExternalAgentBrowserController(port) {
|
|
|
3112
4470
|
return Object.freeze({
|
|
3113
4471
|
async handle(request, response, route, writeSuccess) {
|
|
3114
4472
|
if (request.method !== "POST" || disposed) {
|
|
3115
|
-
throw new
|
|
4473
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
3116
4474
|
}
|
|
3117
4475
|
if (route === "events") {
|
|
3118
4476
|
await handleEvents(request, response);
|
|
3119
4477
|
return;
|
|
3120
4478
|
}
|
|
3121
4479
|
if (route === "status") {
|
|
3122
|
-
const parsed2 =
|
|
4480
|
+
const parsed2 = import_shared17.externalAgentControlStatusRequestSchema.safeParse(
|
|
3123
4481
|
await readJsonRequestBody(request)
|
|
3124
4482
|
);
|
|
3125
|
-
if (!parsed2.success) throw new
|
|
4483
|
+
if (!parsed2.success) throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
3126
4484
|
writeSuccess(
|
|
3127
4485
|
response,
|
|
3128
4486
|
200,
|
|
3129
|
-
|
|
4487
|
+
import_shared17.externalAgentControlStatusSchema.parse(port.getStatus())
|
|
3130
4488
|
);
|
|
3131
4489
|
return;
|
|
3132
4490
|
}
|
|
3133
4491
|
if (route === "connect") {
|
|
3134
|
-
const parsed2 =
|
|
4492
|
+
const parsed2 = import_shared17.externalAgentControlConnectRequestSchema.safeParse(
|
|
3135
4493
|
await readJsonRequestBody(request)
|
|
3136
4494
|
);
|
|
3137
|
-
if (!parsed2.success) throw new
|
|
4495
|
+
if (!parsed2.success) throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
3138
4496
|
const controller = new AbortController();
|
|
3139
4497
|
response.once("close", () => {
|
|
3140
4498
|
if (!response.writableEnded) controller.abort("browser-disconnected");
|
|
@@ -3142,45 +4500,45 @@ function createExternalAgentBrowserController(port) {
|
|
|
3142
4500
|
writeSuccess(
|
|
3143
4501
|
response,
|
|
3144
4502
|
200,
|
|
3145
|
-
|
|
4503
|
+
import_shared17.externalAgentControlStatusSchema.parse(
|
|
3146
4504
|
await port.connect(parsed2.data, controller.signal)
|
|
3147
4505
|
)
|
|
3148
4506
|
);
|
|
3149
4507
|
return;
|
|
3150
4508
|
}
|
|
3151
4509
|
if (route === "disconnect") {
|
|
3152
|
-
const parsed2 =
|
|
4510
|
+
const parsed2 = import_shared17.externalAgentControlDisconnectRequestSchema.safeParse(
|
|
3153
4511
|
await readJsonRequestBody(request)
|
|
3154
4512
|
);
|
|
3155
|
-
if (!parsed2.success) throw new
|
|
4513
|
+
if (!parsed2.success) throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
3156
4514
|
writeSuccess(
|
|
3157
4515
|
response,
|
|
3158
4516
|
200,
|
|
3159
|
-
|
|
4517
|
+
import_shared17.externalAgentControlStatusSchema.parse(await port.disconnect(parsed2.data))
|
|
3160
4518
|
);
|
|
3161
4519
|
return;
|
|
3162
4520
|
}
|
|
3163
4521
|
if (route === "cancel") {
|
|
3164
|
-
const parsed2 =
|
|
4522
|
+
const parsed2 = import_shared17.externalAgentControlCancelRequestSchema.safeParse(
|
|
3165
4523
|
await readJsonRequestBody(request)
|
|
3166
4524
|
);
|
|
3167
|
-
if (!parsed2.success) throw new
|
|
4525
|
+
if (!parsed2.success) throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
3168
4526
|
writeSuccess(
|
|
3169
4527
|
response,
|
|
3170
4528
|
200,
|
|
3171
|
-
|
|
4529
|
+
import_shared17.externalAgentControlStatusSchema.parse(await port.cancel(parsed2.data))
|
|
3172
4530
|
);
|
|
3173
4531
|
return;
|
|
3174
4532
|
}
|
|
3175
|
-
const parsed =
|
|
4533
|
+
const parsed = import_shared17.externalAgentResultRequestSchema.safeParse(
|
|
3176
4534
|
await readJsonRequestBody(request)
|
|
3177
4535
|
);
|
|
3178
|
-
if (!parsed.success) throw new
|
|
4536
|
+
if (!parsed.success) throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INVALID_REQUEST);
|
|
3179
4537
|
const result = port.getResult(parsed.data.revision);
|
|
3180
4538
|
if (result === void 0) {
|
|
3181
|
-
throw new
|
|
4539
|
+
throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.HANDOFF_NOT_FOUND);
|
|
3182
4540
|
}
|
|
3183
|
-
writeSuccess(response, 200,
|
|
4541
|
+
writeSuccess(response, 200, import_shared17.externalAgentManagedResultSchema.parse(result));
|
|
3184
4542
|
},
|
|
3185
4543
|
dispose() {
|
|
3186
4544
|
if (disposed) return;
|
|
@@ -3192,10 +4550,10 @@ function createExternalAgentBrowserController(port) {
|
|
|
3192
4550
|
}
|
|
3193
4551
|
|
|
3194
4552
|
// src/server/agent-http.ts
|
|
3195
|
-
var
|
|
4553
|
+
var import_shared19 = require("@spotpatch/shared");
|
|
3196
4554
|
|
|
3197
4555
|
// src/server/agent-request.ts
|
|
3198
|
-
var
|
|
4556
|
+
var import_shared18 = require("@spotpatch/shared");
|
|
3199
4557
|
async function authorizeAgentJobRequest(input) {
|
|
3200
4558
|
const annotation = await authorizeAnnotation({
|
|
3201
4559
|
annotation: input.request.annotation,
|
|
@@ -3231,21 +4589,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
3231
4589
|
"reverted",
|
|
3232
4590
|
"failed"
|
|
3233
4591
|
]);
|
|
3234
|
-
function matchAgentRequestPath(
|
|
3235
|
-
if (
|
|
4592
|
+
function matchAgentRequestPath(path10) {
|
|
4593
|
+
if (path10 === import_shared19.SPOTPATCH_ENDPOINTS.agentCapability) {
|
|
3236
4594
|
return Object.freeze({ kind: "capability" });
|
|
3237
4595
|
}
|
|
3238
|
-
if (
|
|
4596
|
+
if (path10 === import_shared19.SPOTPATCH_ENDPOINTS.agentWorkspaceHealth) {
|
|
3239
4597
|
return Object.freeze({ kind: "workspace-health" });
|
|
3240
4598
|
}
|
|
3241
|
-
if (
|
|
4599
|
+
if (path10 === import_shared19.SPOTPATCH_ENDPOINTS.agentJobs) {
|
|
3242
4600
|
return Object.freeze({ kind: "create-job" });
|
|
3243
4601
|
}
|
|
3244
|
-
const prefix = `${
|
|
3245
|
-
if (!
|
|
4602
|
+
const prefix = `${import_shared19.SPOTPATCH_ENDPOINTS.agentJobs}/`;
|
|
4603
|
+
if (!path10.startsWith(prefix)) {
|
|
3246
4604
|
return void 0;
|
|
3247
4605
|
}
|
|
3248
|
-
const segments =
|
|
4606
|
+
const segments = path10.slice(prefix.length).split("/");
|
|
3249
4607
|
const jobId = segments[0];
|
|
3250
4608
|
const action = segments[1];
|
|
3251
4609
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -3259,7 +4617,7 @@ function matchAgentRequestPath(path9) {
|
|
|
3259
4617
|
}
|
|
3260
4618
|
function requireAgentManager(options) {
|
|
3261
4619
|
if (options.agentManager === void 0 || options.options.ai === false) {
|
|
3262
|
-
throw new
|
|
4620
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AI_DISABLED);
|
|
3263
4621
|
}
|
|
3264
4622
|
return options.agentManager;
|
|
3265
4623
|
}
|
|
@@ -3312,13 +4670,13 @@ function streamAgentJobEvents(response, manager, jobId) {
|
|
|
3312
4670
|
}
|
|
3313
4671
|
async function handleCapability(request, response, options, writeSuccess) {
|
|
3314
4672
|
if (request.method !== "POST") {
|
|
3315
|
-
throw new
|
|
4673
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3316
4674
|
}
|
|
3317
|
-
const parsed =
|
|
4675
|
+
const parsed = import_shared19.agentCapabilityRequestSchema.safeParse(
|
|
3318
4676
|
await readJsonRequestBody(request)
|
|
3319
4677
|
);
|
|
3320
4678
|
if (!parsed.success) {
|
|
3321
|
-
throw new
|
|
4679
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3322
4680
|
}
|
|
3323
4681
|
const controller = new AbortController();
|
|
3324
4682
|
const abort = () => {
|
|
@@ -3337,13 +4695,13 @@ async function handleCapability(request, response, options, writeSuccess) {
|
|
|
3337
4695
|
}
|
|
3338
4696
|
async function handleCreateJob(request, response, options, writeSuccess) {
|
|
3339
4697
|
if (request.method !== "POST") {
|
|
3340
|
-
throw new
|
|
4698
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3341
4699
|
}
|
|
3342
|
-
const parsed =
|
|
4700
|
+
const parsed = import_shared19.agentJobCreateRequestSchema.safeParse(
|
|
3343
4701
|
await readJsonRequestBody(request, MAX_AGENT_REQUEST_BODY_BYTES)
|
|
3344
4702
|
);
|
|
3345
4703
|
if (!parsed.success) {
|
|
3346
|
-
throw new
|
|
4704
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3347
4705
|
}
|
|
3348
4706
|
const authorizedRequest = await authorizeAgentJobRequest({
|
|
3349
4707
|
request: parsed.data,
|
|
@@ -3356,13 +4714,13 @@ async function handleCreateJob(request, response, options, writeSuccess) {
|
|
|
3356
4714
|
}
|
|
3357
4715
|
async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
3358
4716
|
if (request.method !== "POST") {
|
|
3359
|
-
throw new
|
|
4717
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3360
4718
|
}
|
|
3361
|
-
const parsed =
|
|
4719
|
+
const parsed = import_shared19.agentWorkspaceHealthRequestSchema.safeParse(
|
|
3362
4720
|
await readJsonRequestBody(request)
|
|
3363
4721
|
);
|
|
3364
4722
|
if (!parsed.success) {
|
|
3365
|
-
throw new
|
|
4723
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3366
4724
|
}
|
|
3367
4725
|
const controller = new AbortController();
|
|
3368
4726
|
const abort = () => {
|
|
@@ -3379,13 +4737,13 @@ async function handleWorkspaceHealth(request, response, options, writeSuccess) {
|
|
|
3379
4737
|
async function handleJobAction(request, response, options, route, writeSuccess) {
|
|
3380
4738
|
const manager = requireAgentManager(options);
|
|
3381
4739
|
if (request.method !== "POST") {
|
|
3382
|
-
throw new
|
|
4740
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3383
4741
|
}
|
|
3384
|
-
const parsed =
|
|
4742
|
+
const parsed = import_shared19.agentJobActionRequestSchema.safeParse(
|
|
3385
4743
|
await readJsonRequestBody(request)
|
|
3386
4744
|
);
|
|
3387
4745
|
if (!parsed.success) {
|
|
3388
|
-
throw new
|
|
4746
|
+
throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INVALID_REQUEST);
|
|
3389
4747
|
}
|
|
3390
4748
|
if (route.action === "events") {
|
|
3391
4749
|
streamAgentJobEvents(response, manager, route.jobId);
|
|
@@ -3439,13 +4797,13 @@ function detectIntegratedEditor(environment) {
|
|
|
3439
4797
|
function editorCommand(editor) {
|
|
3440
4798
|
return editor === "cursor" ? "cursor" : "code";
|
|
3441
4799
|
}
|
|
3442
|
-
var
|
|
4800
|
+
var DEFAULT_DEPENDENCIES3 = Object.freeze({
|
|
3443
4801
|
environment: process.env,
|
|
3444
4802
|
fallbackLauncher: import_launch_editor.default,
|
|
3445
4803
|
processSpawner: (command, arguments_, options) => (0, import_node_child_process2.spawn)(command, [...arguments_], options),
|
|
3446
4804
|
startupGraceMs: EDITOR_STARTUP_GRACE_MS
|
|
3447
4805
|
});
|
|
3448
|
-
function createEditorLauncher(dependencies =
|
|
4806
|
+
function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES3) {
|
|
3449
4807
|
return (target, configuredEditor) => {
|
|
3450
4808
|
const integratedEditor = detectIntegratedEditor(dependencies.environment);
|
|
3451
4809
|
const resolvedEditor = configuredEditor === "auto" ? integratedEditor : configuredEditor;
|
|
@@ -3500,27 +4858,27 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
3500
4858
|
var launchConfiguredEditor = createEditorLauncher();
|
|
3501
4859
|
|
|
3502
4860
|
// src/server/data-flow-http.ts
|
|
3503
|
-
var
|
|
4861
|
+
var import_node_crypto11 = require("crypto");
|
|
3504
4862
|
var import_analyzer = require("@spotpatch/analyzer");
|
|
3505
|
-
var
|
|
4863
|
+
var import_shared20 = require("@spotpatch/shared");
|
|
3506
4864
|
function envelopeBytes(report) {
|
|
3507
4865
|
return Buffer.byteLength(JSON.stringify({ ok: true, data: report }), "utf8");
|
|
3508
4866
|
}
|
|
3509
4867
|
function limitDataFlowReportToBytes(report, maximumBytes) {
|
|
3510
|
-
const structurallyLimited = (0,
|
|
4868
|
+
const structurallyLimited = (0, import_shared20.limitDataFlowReportCollections)(report);
|
|
3511
4869
|
if (envelopeBytes(structurallyLimited) <= maximumBytes) {
|
|
3512
4870
|
return structurallyLimited;
|
|
3513
4871
|
}
|
|
3514
|
-
let limited = (0,
|
|
4872
|
+
let limited = (0, import_shared20.limitDataFlowReportCollections)(structurallyLimited, {
|
|
3515
4873
|
forceTruncation: true,
|
|
3516
4874
|
maximumDependencies: 0,
|
|
3517
4875
|
truncatedBy: "bytes"
|
|
3518
4876
|
});
|
|
3519
4877
|
if (envelopeBytes(limited) > maximumBytes) {
|
|
3520
|
-
throw new
|
|
4878
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INTERNAL_ERROR);
|
|
3521
4879
|
}
|
|
3522
4880
|
for (let maximumDependencies = 1; maximumDependencies <= structurallyLimited.dependencies.length; maximumDependencies += 1) {
|
|
3523
|
-
const candidate = (0,
|
|
4881
|
+
const candidate = (0, import_shared20.limitDataFlowReportCollections)(structurallyLimited, {
|
|
3524
4882
|
forceTruncation: true,
|
|
3525
4883
|
maximumDependencies,
|
|
3526
4884
|
truncatedBy: "bytes"
|
|
@@ -3546,7 +4904,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
3546
4904
|
request.componentSourceId
|
|
3547
4905
|
);
|
|
3548
4906
|
if (anchor?.sourceVersion !== request.sourceVersion) {
|
|
3549
|
-
throw new
|
|
4907
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
3550
4908
|
}
|
|
3551
4909
|
return anchor;
|
|
3552
4910
|
}
|
|
@@ -3563,7 +4921,7 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
3563
4921
|
column: resolvedRequest.column
|
|
3564
4922
|
});
|
|
3565
4923
|
if (resolvedRequest.sourceVersion !== void 0 && resolvedRequest.sourceVersion !== report.component.source.sourceVersion) {
|
|
3566
|
-
throw new
|
|
4924
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.DATA_FLOW_SOURCE_STALE);
|
|
3567
4925
|
}
|
|
3568
4926
|
return limitDataFlowReportToBytes(
|
|
3569
4927
|
report,
|
|
@@ -3572,31 +4930,31 @@ async function analyzeTarget(request, analyzer, options) {
|
|
|
3572
4930
|
}
|
|
3573
4931
|
function requireAnalyzer(analyzer) {
|
|
3574
4932
|
if (analyzer === void 0) {
|
|
3575
|
-
throw new
|
|
4933
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.DATA_FLOW_DISABLED);
|
|
3576
4934
|
}
|
|
3577
4935
|
return analyzer;
|
|
3578
4936
|
}
|
|
3579
4937
|
async function handleComponentDataFlowReport(request, analyzer, options) {
|
|
3580
|
-
const parsed =
|
|
4938
|
+
const parsed = import_shared20.dataFlowComponentReportRequestSchema.safeParse(
|
|
3581
4939
|
await readJsonRequestBody(
|
|
3582
4940
|
request,
|
|
3583
4941
|
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
3584
4942
|
)
|
|
3585
4943
|
);
|
|
3586
4944
|
if (!parsed.success) {
|
|
3587
|
-
throw new
|
|
4945
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
3588
4946
|
}
|
|
3589
4947
|
return analyzeTarget(parsed.data, requireAnalyzer(analyzer), options);
|
|
3590
4948
|
}
|
|
3591
4949
|
async function handlePageDataFlowReport(request, analyzer, options) {
|
|
3592
|
-
const parsed =
|
|
4950
|
+
const parsed = import_shared20.dataFlowPageReportRequestSchema.safeParse(
|
|
3593
4951
|
await readJsonRequestBody(
|
|
3594
4952
|
request,
|
|
3595
4953
|
options.options.dataFlow.limits.protocolRequestMaxBytes
|
|
3596
4954
|
)
|
|
3597
4955
|
);
|
|
3598
4956
|
if (!parsed.success) {
|
|
3599
|
-
throw new
|
|
4957
|
+
throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INVALID_REQUEST);
|
|
3600
4958
|
}
|
|
3601
4959
|
const activeAnalyzer = requireAnalyzer(analyzer);
|
|
3602
4960
|
const componentReports = await Promise.all(
|
|
@@ -3620,10 +4978,10 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
3620
4978
|
const analyzedVersions = new Set(
|
|
3621
4979
|
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
3622
4980
|
);
|
|
3623
|
-
const reportId = `page_${(0,
|
|
4981
|
+
const reportId = `page_${(0, import_node_crypto11.createHash)("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
3624
4982
|
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
3625
4983
|
const report = Object.freeze({
|
|
3626
|
-
schemaVersion:
|
|
4984
|
+
schemaVersion: import_shared20.DATA_FLOW_SCHEMA_VERSION,
|
|
3627
4985
|
reportId,
|
|
3628
4986
|
baseline: Object.freeze({
|
|
3629
4987
|
registryEpoch: options.session.id,
|
|
@@ -3667,9 +5025,9 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
3667
5025
|
}
|
|
3668
5026
|
|
|
3669
5027
|
// src/server/request-security.ts
|
|
3670
|
-
var
|
|
5028
|
+
var import_node_crypto12 = require("crypto");
|
|
3671
5029
|
var import_node_net = require("net");
|
|
3672
|
-
var
|
|
5030
|
+
var import_shared21 = require("@spotpatch/shared");
|
|
3673
5031
|
function getSingleHeader(request, name) {
|
|
3674
5032
|
const value = request.headers[name.toLowerCase()];
|
|
3675
5033
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -3680,7 +5038,7 @@ function tokensMatch2(actual, expected) {
|
|
|
3680
5038
|
}
|
|
3681
5039
|
const actualBytes = Buffer.from(actual);
|
|
3682
5040
|
const expectedBytes = Buffer.from(expected);
|
|
3683
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
5041
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto12.timingSafeEqual)(actualBytes, expectedBytes);
|
|
3684
5042
|
}
|
|
3685
5043
|
function isLoopbackHostname(hostname) {
|
|
3686
5044
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
@@ -3713,33 +5071,42 @@ function parseOrigin(value) {
|
|
|
3713
5071
|
return void 0;
|
|
3714
5072
|
}
|
|
3715
5073
|
}
|
|
5074
|
+
function isSameOriginBrowserGet(request) {
|
|
5075
|
+
return request.method === "GET" && getSingleHeader(request, "sec-fetch-site")?.toLowerCase() === "same-origin" && getSingleHeader(request, "sec-fetch-mode")?.toLowerCase() === "cors" && getSingleHeader(request, "sec-fetch-dest")?.toLowerCase() === "empty";
|
|
5076
|
+
}
|
|
3716
5077
|
function assertRequestAuthorized(request, options) {
|
|
3717
|
-
const actualToken = getSingleHeader(request,
|
|
5078
|
+
const actualToken = getSingleHeader(request, import_shared21.SPOTPATCH_TOKEN_HEADER);
|
|
3718
5079
|
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
3719
|
-
throw new
|
|
5080
|
+
throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.INVALID_TOKEN);
|
|
3720
5081
|
}
|
|
3721
5082
|
const hostHeader = getSingleHeader(request, "host");
|
|
3722
5083
|
const originHeader = getSingleHeader(request, "origin");
|
|
3723
5084
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
3724
5085
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
3725
|
-
if (host === void 0
|
|
3726
|
-
throw new
|
|
5086
|
+
if (host === void 0) {
|
|
5087
|
+
throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
3727
5088
|
}
|
|
3728
5089
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
5090
|
+
if (origin === void 0) {
|
|
5091
|
+
if (!isSameOriginBrowserGet(request) || !options.allowLan && !hostIsLoopback) {
|
|
5092
|
+
throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
5093
|
+
}
|
|
5094
|
+
return;
|
|
5095
|
+
}
|
|
3729
5096
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
3730
5097
|
if (!options.allowLan) {
|
|
3731
5098
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
3732
|
-
throw new
|
|
5099
|
+
throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
3733
5100
|
}
|
|
3734
5101
|
return;
|
|
3735
5102
|
}
|
|
3736
5103
|
if (!originIsLoopback && origin.host.toLowerCase() !== host.host.toLowerCase()) {
|
|
3737
|
-
throw new
|
|
5104
|
+
throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
3738
5105
|
}
|
|
3739
5106
|
}
|
|
3740
5107
|
|
|
3741
5108
|
// src/server/runtime-bootstrap.ts
|
|
3742
|
-
var
|
|
5109
|
+
var import_shared22 = require("@spotpatch/shared");
|
|
3743
5110
|
function getSingleHeader2(request, name) {
|
|
3744
5111
|
const value = request.headers[name.toLowerCase()];
|
|
3745
5112
|
return Array.isArray(value) ? value[0] : value;
|
|
@@ -3754,7 +5121,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
3754
5121
|
if (expectedOrigin.origin !== options.expectedOrigin || expectedOrigin.protocol !== "http:" || !isLoopbackHostname(expectedOrigin.hostname)) {
|
|
3755
5122
|
throw new TypeError("The SpotPatch bootstrap origin must be a loopback origin.");
|
|
3756
5123
|
}
|
|
3757
|
-
const parsedConfig =
|
|
5124
|
+
const parsedConfig = import_shared22.runtimeConfigSchema.safeParse(options.runtimeConfig);
|
|
3758
5125
|
if (!parsedConfig.success) {
|
|
3759
5126
|
throw new TypeError("The SpotPatch Runtime configuration is invalid.");
|
|
3760
5127
|
}
|
|
@@ -3766,7 +5133,7 @@ function resolveRuntimeBootstrapOptions(options) {
|
|
|
3766
5133
|
function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
3767
5134
|
const contentType = getSingleHeader2(request, "content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
3768
5135
|
if (request.method !== "POST" || contentType !== "application/json") {
|
|
3769
|
-
throw new
|
|
5136
|
+
throw new import_shared22.SpotPatchError(import_shared22.ERROR_CODES.INVALID_REQUEST);
|
|
3770
5137
|
}
|
|
3771
5138
|
const host = getSingleHeader2(request, "host");
|
|
3772
5139
|
let hostIsLoopback = false;
|
|
@@ -3778,152 +5145,152 @@ function assertRuntimeBootstrapRequest(request, expectedOrigin) {
|
|
|
3778
5145
|
}
|
|
3779
5146
|
}
|
|
3780
5147
|
if (!hostIsLoopback || getSingleHeader2(request, "origin") !== expectedOrigin || getSingleHeader2(request, "sec-fetch-site") !== "same-origin") {
|
|
3781
|
-
throw new
|
|
5148
|
+
throw new import_shared22.SpotPatchError(import_shared22.ERROR_CODES.ORIGIN_NOT_ALLOWED);
|
|
3782
5149
|
}
|
|
3783
5150
|
}
|
|
3784
5151
|
async function readRuntimeBootstrap(request, options) {
|
|
3785
5152
|
assertRuntimeBootstrapRequest(request, options.expectedOrigin);
|
|
3786
|
-
const parsedBody =
|
|
5153
|
+
const parsedBody = import_shared22.runtimeBootstrapRequestSchema.safeParse(
|
|
3787
5154
|
await readJsonRequestBody(request)
|
|
3788
5155
|
);
|
|
3789
5156
|
if (!parsedBody.success) {
|
|
3790
|
-
throw new
|
|
5157
|
+
throw new import_shared22.SpotPatchError(import_shared22.ERROR_CODES.INVALID_REQUEST);
|
|
3791
5158
|
}
|
|
3792
5159
|
return options.runtimeConfig;
|
|
3793
5160
|
}
|
|
3794
5161
|
|
|
3795
5162
|
// src/server/middleware.ts
|
|
3796
|
-
var
|
|
3797
|
-
[
|
|
3798
|
-
[
|
|
3799
|
-
[
|
|
3800
|
-
[
|
|
3801
|
-
[
|
|
3802
|
-
[
|
|
3803
|
-
[
|
|
3804
|
-
[
|
|
3805
|
-
[
|
|
3806
|
-
[
|
|
3807
|
-
[
|
|
3808
|
-
[
|
|
3809
|
-
[
|
|
3810
|
-
[
|
|
3811
|
-
[
|
|
3812
|
-
[
|
|
3813
|
-
[
|
|
3814
|
-
[
|
|
3815
|
-
[
|
|
3816
|
-
[
|
|
3817
|
-
[
|
|
3818
|
-
[
|
|
3819
|
-
[
|
|
3820
|
-
[
|
|
3821
|
-
[
|
|
3822
|
-
[
|
|
3823
|
-
[
|
|
3824
|
-
[
|
|
3825
|
-
[
|
|
3826
|
-
[
|
|
3827
|
-
[
|
|
3828
|
-
[
|
|
3829
|
-
[
|
|
3830
|
-
[
|
|
3831
|
-
[
|
|
3832
|
-
[
|
|
3833
|
-
[
|
|
3834
|
-
[
|
|
3835
|
-
[
|
|
3836
|
-
[
|
|
3837
|
-
[
|
|
3838
|
-
[
|
|
3839
|
-
[
|
|
3840
|
-
[
|
|
3841
|
-
[
|
|
3842
|
-
[
|
|
3843
|
-
[
|
|
3844
|
-
[
|
|
3845
|
-
[
|
|
3846
|
-
[
|
|
3847
|
-
[
|
|
3848
|
-
[
|
|
3849
|
-
[
|
|
3850
|
-
[
|
|
5163
|
+
var STATUS_BY_ERROR2 = Object.freeze({
|
|
5164
|
+
[import_shared23.ERROR_CODES.INVALID_REQUEST]: 400,
|
|
5165
|
+
[import_shared23.ERROR_CODES.INVALID_TOKEN]: 401,
|
|
5166
|
+
[import_shared23.ERROR_CODES.ORIGIN_NOT_ALLOWED]: 403,
|
|
5167
|
+
[import_shared23.ERROR_CODES.SOURCE_NOT_FOUND]: 404,
|
|
5168
|
+
[import_shared23.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: 403,
|
|
5169
|
+
[import_shared23.ERROR_CODES.SOURCE_TOO_LARGE]: 413,
|
|
5170
|
+
[import_shared23.ERROR_CODES.EDITOR_OPEN_FAILED]: 500,
|
|
5171
|
+
[import_shared23.ERROR_CODES.DATA_FLOW_DISABLED]: 404,
|
|
5172
|
+
[import_shared23.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: 409,
|
|
5173
|
+
[import_shared23.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: 409,
|
|
5174
|
+
[import_shared23.ERROR_CODES.AI_DISABLED]: 404,
|
|
5175
|
+
[import_shared23.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: 503,
|
|
5176
|
+
[import_shared23.ERROR_CODES.PROVIDER_AUTH_FAILED]: 502,
|
|
5177
|
+
[import_shared23.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: 502,
|
|
5178
|
+
[import_shared23.ERROR_CODES.MODEL_NOT_ALLOWED]: 400,
|
|
5179
|
+
[import_shared23.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: 422,
|
|
5180
|
+
[import_shared23.ERROR_CODES.PROVIDER_RATE_LIMITED]: 429,
|
|
5181
|
+
[import_shared23.ERROR_CODES.AGENT_BUSY]: 409,
|
|
5182
|
+
[import_shared23.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: 413,
|
|
5183
|
+
[import_shared23.ERROR_CODES.AGENT_CANCELLED]: 409,
|
|
5184
|
+
[import_shared23.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: 404,
|
|
5185
|
+
[import_shared23.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: 503,
|
|
5186
|
+
[import_shared23.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: 422,
|
|
5187
|
+
[import_shared23.ERROR_CODES.HANDOFF_SOURCE_STALE]: 409,
|
|
5188
|
+
[import_shared23.ERROR_CODES.HANDOFF_NOT_FOUND]: 404,
|
|
5189
|
+
[import_shared23.ERROR_CODES.HANDOFF_EXPIRED]: 410,
|
|
5190
|
+
[import_shared23.ERROR_CODES.HANDOFF_CURSOR_INVALID]: 409,
|
|
5191
|
+
[import_shared23.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: 413,
|
|
5192
|
+
[import_shared23.ERROR_CODES.BRIDGE_UNAUTHORIZED]: 401,
|
|
5193
|
+
[import_shared23.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: 409,
|
|
5194
|
+
[import_shared23.ERROR_CODES.BRIDGE_BUSY]: 429,
|
|
5195
|
+
[import_shared23.ERROR_CODES.EXTERNAL_AGENT_BUSY]: 409,
|
|
5196
|
+
[import_shared23.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: 409,
|
|
5197
|
+
[import_shared23.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: 409,
|
|
5198
|
+
[import_shared23.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: 409,
|
|
5199
|
+
[import_shared23.ERROR_CODES.SESSION_NOT_FOUND]: 404,
|
|
5200
|
+
[import_shared23.ERROR_CODES.SESSION_AMBIGUOUS]: 409,
|
|
5201
|
+
[import_shared23.ERROR_CODES.SESSION_CLOSED]: 410,
|
|
5202
|
+
[import_shared23.ERROR_CODES.WORKTREE_DIRTY]: 409,
|
|
5203
|
+
[import_shared23.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: 409,
|
|
5204
|
+
[import_shared23.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: 409,
|
|
5205
|
+
[import_shared23.ERROR_CODES.WORKTREE_CONFLICTED]: 409,
|
|
5206
|
+
[import_shared23.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: 413,
|
|
5207
|
+
[import_shared23.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: 409,
|
|
5208
|
+
[import_shared23.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: 409,
|
|
5209
|
+
[import_shared23.ERROR_CODES.TOOL_DENIED]: 403,
|
|
5210
|
+
[import_shared23.ERROR_CODES.TOOL_INPUT_INVALID]: 422,
|
|
5211
|
+
[import_shared23.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: 422,
|
|
5212
|
+
[import_shared23.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: 422,
|
|
5213
|
+
[import_shared23.ERROR_CODES.TOOL_PATH_DENIED]: 403,
|
|
5214
|
+
[import_shared23.ERROR_CODES.PATCH_REJECTED]: 422,
|
|
5215
|
+
[import_shared23.ERROR_CODES.VALIDATION_FAILED]: 422,
|
|
5216
|
+
[import_shared23.ERROR_CODES.APPLY_CONFLICT]: 409,
|
|
5217
|
+
[import_shared23.ERROR_CODES.INTERNAL_ERROR]: 500
|
|
3851
5218
|
});
|
|
3852
|
-
var
|
|
3853
|
-
[
|
|
3854
|
-
[
|
|
3855
|
-
[
|
|
3856
|
-
[
|
|
3857
|
-
[
|
|
3858
|
-
[
|
|
3859
|
-
[
|
|
3860
|
-
[
|
|
3861
|
-
[
|
|
3862
|
-
[
|
|
3863
|
-
[
|
|
3864
|
-
[
|
|
3865
|
-
[
|
|
3866
|
-
[
|
|
3867
|
-
[
|
|
3868
|
-
[
|
|
3869
|
-
[
|
|
3870
|
-
[
|
|
3871
|
-
[
|
|
3872
|
-
[
|
|
3873
|
-
[
|
|
3874
|
-
[
|
|
3875
|
-
[
|
|
3876
|
-
[
|
|
3877
|
-
[
|
|
3878
|
-
[
|
|
3879
|
-
[
|
|
3880
|
-
[
|
|
3881
|
-
[
|
|
3882
|
-
[
|
|
3883
|
-
[
|
|
3884
|
-
[
|
|
3885
|
-
[
|
|
3886
|
-
[
|
|
3887
|
-
[
|
|
3888
|
-
[
|
|
3889
|
-
[
|
|
3890
|
-
[
|
|
3891
|
-
[
|
|
3892
|
-
[
|
|
3893
|
-
[
|
|
3894
|
-
[
|
|
3895
|
-
[
|
|
3896
|
-
[
|
|
3897
|
-
[
|
|
3898
|
-
[
|
|
3899
|
-
[
|
|
3900
|
-
[
|
|
3901
|
-
[
|
|
3902
|
-
[
|
|
3903
|
-
[
|
|
3904
|
-
[
|
|
3905
|
-
[
|
|
3906
|
-
[
|
|
5219
|
+
var PUBLIC_MESSAGES2 = Object.freeze({
|
|
5220
|
+
[import_shared23.ERROR_CODES.INVALID_REQUEST]: "The request is invalid.",
|
|
5221
|
+
[import_shared23.ERROR_CODES.INVALID_TOKEN]: "The session token is invalid.",
|
|
5222
|
+
[import_shared23.ERROR_CODES.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
5223
|
+
[import_shared23.ERROR_CODES.SOURCE_NOT_FOUND]: "The source file is unavailable.",
|
|
5224
|
+
[import_shared23.ERROR_CODES.SOURCE_OUTSIDE_ROOT]: "The source file is outside the project root.",
|
|
5225
|
+
[import_shared23.ERROR_CODES.SOURCE_TOO_LARGE]: "The source file exceeds the size limit.",
|
|
5226
|
+
[import_shared23.ERROR_CODES.EDITOR_OPEN_FAILED]: "The editor request could not be started.",
|
|
5227
|
+
[import_shared23.ERROR_CODES.DATA_FLOW_DISABLED]: "Component data-flow analysis is not enabled.",
|
|
5228
|
+
[import_shared23.ERROR_CODES.DATA_FLOW_SOURCE_STALE]: "The selected source version is stale.",
|
|
5229
|
+
[import_shared23.ERROR_CODES.DATA_FLOW_ANALYSIS_CANCELLED]: "The data-flow analysis was cancelled.",
|
|
5230
|
+
[import_shared23.ERROR_CODES.AI_DISABLED]: "AI execution is not enabled.",
|
|
5231
|
+
[import_shared23.ERROR_CODES.PROVIDER_NOT_CONFIGURED]: "The AI provider is unavailable.",
|
|
5232
|
+
[import_shared23.ERROR_CODES.PROVIDER_AUTH_FAILED]: "The AI provider rejected authentication.",
|
|
5233
|
+
[import_shared23.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED]: "The AI provider protocol is unsupported.",
|
|
5234
|
+
[import_shared23.ERROR_CODES.MODEL_NOT_ALLOWED]: "The selected model is not allowed.",
|
|
5235
|
+
[import_shared23.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED]: "The selected model cannot run SpotPatch tools.",
|
|
5236
|
+
[import_shared23.ERROR_CODES.PROVIDER_RATE_LIMITED]: "The AI provider is rate limited.",
|
|
5237
|
+
[import_shared23.ERROR_CODES.AGENT_BUSY]: "Another Agent job is already running.",
|
|
5238
|
+
[import_shared23.ERROR_CODES.AGENT_LIMIT_EXCEEDED]: "The Agent job exceeded a safety limit.",
|
|
5239
|
+
[import_shared23.ERROR_CODES.AGENT_CANCELLED]: "The Agent job was cancelled.",
|
|
5240
|
+
[import_shared23.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED]: "External Agent handoff is not enabled.",
|
|
5241
|
+
[import_shared23.ERROR_CODES.EXTERNAL_HANDOFF_UNAVAILABLE]: "External Agent handoff is temporarily unavailable.",
|
|
5242
|
+
[import_shared23.ERROR_CODES.HANDOFF_VALIDATION_FAILED]: "The handoff content is invalid.",
|
|
5243
|
+
[import_shared23.ERROR_CODES.HANDOFF_SOURCE_STALE]: "The selected source is stale.",
|
|
5244
|
+
[import_shared23.ERROR_CODES.HANDOFF_NOT_FOUND]: "No current handoff is available.",
|
|
5245
|
+
[import_shared23.ERROR_CODES.HANDOFF_EXPIRED]: "The handoff has expired.",
|
|
5246
|
+
[import_shared23.ERROR_CODES.HANDOFF_CURSOR_INVALID]: "The handoff cursor is invalid.",
|
|
5247
|
+
[import_shared23.ERROR_CODES.HANDOFF_RESPONSE_TOO_LARGE]: "The handoff exceeds the size limit.",
|
|
5248
|
+
[import_shared23.ERROR_CODES.BRIDGE_UNAUTHORIZED]: "The local bridge request is unauthorized.",
|
|
5249
|
+
[import_shared23.ERROR_CODES.BRIDGE_PROTOCOL_MISMATCH]: "The local bridge protocol is incompatible.",
|
|
5250
|
+
[import_shared23.ERROR_CODES.BRIDGE_BUSY]: "The local bridge is busy.",
|
|
5251
|
+
[import_shared23.ERROR_CODES.EXTERNAL_AGENT_BUSY]: "The connected external Agent is busy.",
|
|
5252
|
+
[import_shared23.ERROR_CODES.ACTIVE_ADAPTER_CONFLICT]: "Another active Agent adapter is connected.",
|
|
5253
|
+
[import_shared23.ERROR_CODES.ACTIVE_ADAPTER_LEASE_INVALID]: "The active Agent adapter lease is invalid.",
|
|
5254
|
+
[import_shared23.ERROR_CODES.ACTIVE_DISPATCH_INVALID]: "The active Agent dispatch transition is invalid.",
|
|
5255
|
+
[import_shared23.ERROR_CODES.SESSION_NOT_FOUND]: "No active SpotPatch session was found.",
|
|
5256
|
+
[import_shared23.ERROR_CODES.SESSION_AMBIGUOUS]: "More than one SpotPatch session matches.",
|
|
5257
|
+
[import_shared23.ERROR_CODES.SESSION_CLOSED]: "The SpotPatch session has closed.",
|
|
5258
|
+
[import_shared23.ERROR_CODES.WORKTREE_DIRTY]: "Local changes require explicit inclusion consent.",
|
|
5259
|
+
[import_shared23.ERROR_CODES.WORKTREE_NOT_REPOSITORY]: "The project root is not a Git repository.",
|
|
5260
|
+
[import_shared23.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS]: "A Git operation is currently in progress.",
|
|
5261
|
+
[import_shared23.ERROR_CODES.WORKTREE_CONFLICTED]: "The local workspace contains unresolved merge conflicts.",
|
|
5262
|
+
[import_shared23.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE]: "The local workspace exceeds the safe isolation size limit.",
|
|
5263
|
+
[import_shared23.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED]: "An untracked path cannot be isolated safely.",
|
|
5264
|
+
[import_shared23.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED]: "The local workspace state cannot be isolated safely.",
|
|
5265
|
+
[import_shared23.ERROR_CODES.TOOL_DENIED]: "The Agent tool request was denied.",
|
|
5266
|
+
[import_shared23.ERROR_CODES.TOOL_INPUT_INVALID]: "The Agent tool input was invalid.",
|
|
5267
|
+
[import_shared23.ERROR_CODES.TOOL_ARGUMENTS_INVALID]: "The Agent tool arguments are invalid.",
|
|
5268
|
+
[import_shared23.ERROR_CODES.TOOL_CALL_ID_CONFLICT]: "A tool call ID conflicts within one Agent turn.",
|
|
5269
|
+
[import_shared23.ERROR_CODES.TOOL_PATH_DENIED]: "The Agent tool path was denied.",
|
|
5270
|
+
[import_shared23.ERROR_CODES.PATCH_REJECTED]: "The proposed patch was rejected.",
|
|
5271
|
+
[import_shared23.ERROR_CODES.VALIDATION_FAILED]: "The proposed change failed validation.",
|
|
5272
|
+
[import_shared23.ERROR_CODES.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
5273
|
+
[import_shared23.ERROR_CODES.INTERNAL_ERROR]: "The request could not be completed."
|
|
3907
5274
|
});
|
|
3908
|
-
function
|
|
5275
|
+
function writeJson3(response, status, payload) {
|
|
3909
5276
|
response.statusCode = status;
|
|
3910
5277
|
response.setHeader("Cache-Control", "no-store");
|
|
3911
5278
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3912
5279
|
response.end(JSON.stringify(payload));
|
|
3913
5280
|
}
|
|
3914
5281
|
function asSpotPatchError(error) {
|
|
3915
|
-
return error instanceof
|
|
5282
|
+
return error instanceof import_shared23.SpotPatchError ? error : new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INTERNAL_ERROR, void 0, { cause: error });
|
|
3916
5283
|
}
|
|
3917
|
-
function
|
|
5284
|
+
function writeError2(response, error, logger) {
|
|
3918
5285
|
const normalized = asSpotPatchError(error);
|
|
3919
|
-
if (normalized.code ===
|
|
5286
|
+
if (normalized.code === import_shared23.ERROR_CODES.INTERNAL_ERROR) {
|
|
3920
5287
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
3921
5288
|
}
|
|
3922
|
-
|
|
5289
|
+
writeJson3(response, STATUS_BY_ERROR2[normalized.code], {
|
|
3923
5290
|
ok: false,
|
|
3924
5291
|
error: {
|
|
3925
5292
|
code: normalized.code,
|
|
3926
|
-
message:
|
|
5293
|
+
message: PUBLIC_MESSAGES2[normalized.code]
|
|
3927
5294
|
}
|
|
3928
5295
|
});
|
|
3929
5296
|
}
|
|
@@ -3935,11 +5302,11 @@ function requestPath(request) {
|
|
|
3935
5302
|
}
|
|
3936
5303
|
}
|
|
3937
5304
|
async function handleSourceContext(request, options) {
|
|
3938
|
-
const parsed =
|
|
5305
|
+
const parsed = import_shared23.sourceContextRequestSchema.safeParse(
|
|
3939
5306
|
await readJsonRequestBody(request)
|
|
3940
5307
|
);
|
|
3941
5308
|
if (!parsed.success) {
|
|
3942
|
-
throw new
|
|
5309
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INVALID_REQUEST);
|
|
3943
5310
|
}
|
|
3944
5311
|
return readSourceContext({
|
|
3945
5312
|
request: parsed.data,
|
|
@@ -3950,9 +5317,9 @@ async function handleSourceContext(request, options) {
|
|
|
3950
5317
|
});
|
|
3951
5318
|
}
|
|
3952
5319
|
async function handleOpenEditor(request, options) {
|
|
3953
|
-
const parsed =
|
|
5320
|
+
const parsed = import_shared23.openEditorRequestSchema.safeParse(await readJsonRequestBody(request));
|
|
3954
5321
|
if (!parsed.success) {
|
|
3955
|
-
throw new
|
|
5322
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INVALID_REQUEST);
|
|
3956
5323
|
}
|
|
3957
5324
|
const body = parsed.data;
|
|
3958
5325
|
const sourcePath = await resolveSourceFile({
|
|
@@ -3969,7 +5336,7 @@ async function handleOpenEditor(request, options) {
|
|
|
3969
5336
|
options.logger?.warn(
|
|
3970
5337
|
`[spotpatch:server] ${options.options.editor === "auto" ? "The detected editor" : options.options.editor} rejected an editor request.`
|
|
3971
5338
|
);
|
|
3972
|
-
throw new
|
|
5339
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.EDITOR_OPEN_FAILED, void 0, {
|
|
3973
5340
|
cause: error
|
|
3974
5341
|
});
|
|
3975
5342
|
}
|
|
@@ -3979,73 +5346,84 @@ function createSpotPatchMiddleware(options) {
|
|
|
3979
5346
|
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
3980
5347
|
const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
|
|
3981
5348
|
const middleware = (request, response, next) => {
|
|
3982
|
-
const
|
|
3983
|
-
const agentRoute = matchAgentRequestPath(
|
|
3984
|
-
const
|
|
3985
|
-
const
|
|
3986
|
-
|
|
5349
|
+
const path10 = requestPath(request);
|
|
5350
|
+
const agentRoute = matchAgentRequestPath(path10);
|
|
5351
|
+
const askRoute = matchContextualAskPath(path10);
|
|
5352
|
+
const externalAgentRoute = matchExternalAgentBrowserPath(path10);
|
|
5353
|
+
const externalHandoffRoute = matchExternalHandoffBrowserPath(path10);
|
|
5354
|
+
if (path10 !== import_shared23.SPOTPATCH_ENDPOINTS.sourceContext && path10 !== import_shared23.SPOTPATCH_ENDPOINTS.openEditor && path10 !== import_shared23.SPOTPATCH_ENDPOINTS.dataFlowComponentReport && path10 !== import_shared23.SPOTPATCH_ENDPOINTS.dataFlowPageReport && agentRoute === void 0 && askRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path10.startsWith(`${import_shared23.SPOTPATCH_API_BASE}/`)) {
|
|
3987
5355
|
next();
|
|
3988
5356
|
return;
|
|
3989
5357
|
}
|
|
3990
5358
|
const handle = async () => {
|
|
3991
|
-
if (
|
|
5359
|
+
if (path10 === import_shared23.SPOTPATCH_ENDPOINTS.bootstrap && bootstrap !== void 0) {
|
|
3992
5360
|
const data = await readRuntimeBootstrap(
|
|
3993
5361
|
request,
|
|
3994
5362
|
bootstrap
|
|
3995
5363
|
);
|
|
3996
|
-
|
|
5364
|
+
writeJson3(response, 200, { ok: true, data });
|
|
3997
5365
|
return;
|
|
3998
5366
|
}
|
|
3999
5367
|
assertRequestAuthorized(request, {
|
|
4000
5368
|
allowLan: options.options.allowLan,
|
|
4001
5369
|
sessionToken: options.session.token
|
|
4002
5370
|
});
|
|
4003
|
-
if (
|
|
5371
|
+
if (askRoute !== void 0) {
|
|
5372
|
+
await handleContextualAskRequest(
|
|
5373
|
+
request,
|
|
5374
|
+
response,
|
|
5375
|
+
askRoute,
|
|
5376
|
+
options.contextualAskManager,
|
|
5377
|
+
options.logger
|
|
5378
|
+
);
|
|
5379
|
+
return;
|
|
5380
|
+
}
|
|
5381
|
+
if (path10 === import_shared23.SPOTPATCH_ENDPOINTS.sourceContext) {
|
|
4004
5382
|
if (request.method !== "POST") {
|
|
4005
|
-
throw new
|
|
5383
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INVALID_REQUEST);
|
|
4006
5384
|
}
|
|
4007
5385
|
const data = await handleSourceContext(request, options);
|
|
4008
|
-
|
|
5386
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4009
5387
|
return;
|
|
4010
5388
|
}
|
|
4011
|
-
if (
|
|
5389
|
+
if (path10 === import_shared23.SPOTPATCH_ENDPOINTS.openEditor) {
|
|
4012
5390
|
if (request.method !== "POST") {
|
|
4013
|
-
throw new
|
|
5391
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INVALID_REQUEST);
|
|
4014
5392
|
}
|
|
4015
5393
|
const data = await handleOpenEditor(request, options);
|
|
4016
|
-
|
|
5394
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4017
5395
|
return;
|
|
4018
5396
|
}
|
|
4019
|
-
if (
|
|
5397
|
+
if (path10 === import_shared23.SPOTPATCH_ENDPOINTS.dataFlowComponentReport) {
|
|
4020
5398
|
if (request.method !== "POST") {
|
|
4021
|
-
throw new
|
|
5399
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INVALID_REQUEST);
|
|
4022
5400
|
}
|
|
4023
5401
|
const data = await handleComponentDataFlowReport(
|
|
4024
5402
|
request,
|
|
4025
5403
|
dataFlowAnalyzer,
|
|
4026
5404
|
options
|
|
4027
5405
|
);
|
|
4028
|
-
|
|
5406
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4029
5407
|
return;
|
|
4030
5408
|
}
|
|
4031
|
-
if (
|
|
5409
|
+
if (path10 === import_shared23.SPOTPATCH_ENDPOINTS.dataFlowPageReport) {
|
|
4032
5410
|
if (request.method !== "POST") {
|
|
4033
|
-
throw new
|
|
5411
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INVALID_REQUEST);
|
|
4034
5412
|
}
|
|
4035
5413
|
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
4036
|
-
|
|
5414
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4037
5415
|
return;
|
|
4038
5416
|
}
|
|
4039
5417
|
if (externalAgentRoute !== void 0) {
|
|
4040
5418
|
if (!options.options.externalAgent.enabled || externalAgentController === void 0) {
|
|
4041
|
-
throw new
|
|
5419
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.EXTERNAL_HANDOFF_DISABLED);
|
|
4042
5420
|
}
|
|
4043
5421
|
await externalAgentController.handle(
|
|
4044
5422
|
request,
|
|
4045
5423
|
response,
|
|
4046
5424
|
externalAgentRoute,
|
|
4047
5425
|
(target, status, data) => {
|
|
4048
|
-
|
|
5426
|
+
writeJson3(target, status, { ok: true, data });
|
|
4049
5427
|
}
|
|
4050
5428
|
);
|
|
4051
5429
|
return;
|
|
@@ -4062,13 +5440,13 @@ function createSpotPatchMiddleware(options) {
|
|
|
4062
5440
|
...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
|
|
4063
5441
|
},
|
|
4064
5442
|
(target, status, data) => {
|
|
4065
|
-
|
|
5443
|
+
writeJson3(target, status, { ok: true, data });
|
|
4066
5444
|
}
|
|
4067
5445
|
);
|
|
4068
5446
|
return;
|
|
4069
5447
|
}
|
|
4070
5448
|
if (agentRoute === void 0) {
|
|
4071
|
-
throw new
|
|
5449
|
+
throw new import_shared23.SpotPatchError(import_shared23.ERROR_CODES.INVALID_REQUEST);
|
|
4072
5450
|
}
|
|
4073
5451
|
await handleAgentRequest(
|
|
4074
5452
|
request,
|
|
@@ -4076,12 +5454,12 @@ function createSpotPatchMiddleware(options) {
|
|
|
4076
5454
|
options,
|
|
4077
5455
|
agentRoute,
|
|
4078
5456
|
(target, status, data) => {
|
|
4079
|
-
|
|
5457
|
+
writeJson3(target, status, { ok: true, data });
|
|
4080
5458
|
}
|
|
4081
5459
|
);
|
|
4082
5460
|
};
|
|
4083
5461
|
void handle().catch((error) => {
|
|
4084
|
-
|
|
5462
|
+
writeError2(response, error, options.logger);
|
|
4085
5463
|
});
|
|
4086
5464
|
};
|
|
4087
5465
|
return Object.assign(middleware, {
|
|
@@ -4092,13 +5470,13 @@ function createSpotPatchMiddleware(options) {
|
|
|
4092
5470
|
}
|
|
4093
5471
|
|
|
4094
5472
|
// src/server/source-registration.ts
|
|
4095
|
-
var
|
|
4096
|
-
var
|
|
4097
|
-
var
|
|
5473
|
+
var import_node_crypto13 = require("crypto");
|
|
5474
|
+
var import_promises8 = require("fs/promises");
|
|
5475
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
4098
5476
|
var import_compiler = require("@spotpatch/compiler");
|
|
4099
|
-
var
|
|
5477
|
+
var import_shared24 = require("@spotpatch/shared");
|
|
4100
5478
|
var import_zod2 = require("zod");
|
|
4101
|
-
var REGISTRATION_BODY_LIMIT_BYTES =
|
|
5479
|
+
var REGISTRATION_BODY_LIMIT_BYTES = import_shared24.DEFAULT_DATA_FLOW_LIMITS.protocolRequestMaxBytes;
|
|
4102
5480
|
var REGISTRATION_IDENTITY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/u;
|
|
4103
5481
|
var DATA_FLOW_IDENTITY_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
4104
5482
|
var INTERNAL_SECRET_HEADER = "x-spotpatch-internal";
|
|
@@ -4114,7 +5492,7 @@ var registrationRequestSchema = import_zod2.z.strictObject({
|
|
|
4114
5492
|
line: import_zod2.z.number().int().positive().max(1e7),
|
|
4115
5493
|
column: import_zod2.z.number().int().positive().max(1e7)
|
|
4116
5494
|
})
|
|
4117
|
-
).max(
|
|
5495
|
+
).max(import_shared24.DEFAULT_DATA_FLOW_LIMITS.sourceMaxComponents)
|
|
4118
5496
|
}).optional()
|
|
4119
5497
|
});
|
|
4120
5498
|
function getSingleHeader3(request, name) {
|
|
@@ -4127,16 +5505,16 @@ function identitiesMatch(actual, expected) {
|
|
|
4127
5505
|
}
|
|
4128
5506
|
const actualBytes = Buffer.from(actual);
|
|
4129
5507
|
const expectedBytes = Buffer.from(expected);
|
|
4130
|
-
return actualBytes.byteLength === expectedBytes.byteLength && (0,
|
|
5508
|
+
return actualBytes.byteLength === expectedBytes.byteLength && (0, import_node_crypto13.timingSafeEqual)(actualBytes, expectedBytes);
|
|
4131
5509
|
}
|
|
4132
5510
|
function isWithinRoot(root, candidate) {
|
|
4133
|
-
const relative =
|
|
4134
|
-
return relative === "" || !relative.startsWith(`..${
|
|
5511
|
+
const relative = import_node_path9.default.relative(root, candidate);
|
|
5512
|
+
return relative === "" || !relative.startsWith(`..${import_node_path9.default.sep}`) && relative !== ".." && !import_node_path9.default.isAbsolute(relative);
|
|
4135
5513
|
}
|
|
4136
5514
|
function hasForbiddenSegment(root, candidate) {
|
|
4137
|
-
return
|
|
5515
|
+
return import_node_path9.default.relative(root, candidate).split(import_node_path9.default.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
4138
5516
|
}
|
|
4139
|
-
function
|
|
5517
|
+
function writeJson4(response, statusCode, payload) {
|
|
4140
5518
|
const body = JSON.stringify(payload);
|
|
4141
5519
|
response.statusCode = statusCode;
|
|
4142
5520
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -4156,15 +5534,15 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
4156
5534
|
}
|
|
4157
5535
|
}
|
|
4158
5536
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
4159
|
-
if (!
|
|
5537
|
+
if (!import_node_path9.default.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
4160
5538
|
return void 0;
|
|
4161
5539
|
}
|
|
4162
5540
|
try {
|
|
4163
|
-
const sourceStat = await (0,
|
|
5541
|
+
const sourceStat = await (0, import_promises8.lstat)(requestedPath);
|
|
4164
5542
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
4165
5543
|
return void 0;
|
|
4166
5544
|
}
|
|
4167
|
-
const resolvedPath = await (0,
|
|
5545
|
+
const resolvedPath = await (0, import_promises8.realpath)(requestedPath);
|
|
4168
5546
|
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
4169
5547
|
return void 0;
|
|
4170
5548
|
}
|
|
@@ -4177,7 +5555,7 @@ async function createSourceRegistrationService(input) {
|
|
|
4177
5555
|
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
4178
5556
|
throw new TypeError("The source registration identity is invalid.");
|
|
4179
5557
|
}
|
|
4180
|
-
const root = await (0,
|
|
5558
|
+
const root = await (0, import_promises8.realpath)(input.root);
|
|
4181
5559
|
const sourceFilter = (0, import_compiler.createSourceFilter)(root, input.options);
|
|
4182
5560
|
const dataFlowFilter = (0, import_compiler.createDataFlowSourceFilter)(root, input.options);
|
|
4183
5561
|
const registrationQueues = /* @__PURE__ */ new Map();
|
|
@@ -4188,7 +5566,7 @@ async function createSourceRegistrationService(input) {
|
|
|
4188
5566
|
let fileId;
|
|
4189
5567
|
const previous = registrationQueues.get(sourcePath) ?? Promise.resolve();
|
|
4190
5568
|
const current = previous.catch(() => void 0).then(async () => {
|
|
4191
|
-
const currentSource = await (0,
|
|
5569
|
+
const currentSource = await (0, import_promises8.readFile)(sourcePath, "utf8");
|
|
4192
5570
|
if ((0, import_compiler.createDataFlowSourceVersion)(currentSource) !== dataFlow.sourceVersion) {
|
|
4193
5571
|
return;
|
|
4194
5572
|
}
|
|
@@ -4213,14 +5591,14 @@ async function createSourceRegistrationService(input) {
|
|
|
4213
5591
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
4214
5592
|
input.internalSecret
|
|
4215
5593
|
)) {
|
|
4216
|
-
|
|
5594
|
+
writeJson4(response, 403, { ok: false });
|
|
4217
5595
|
return;
|
|
4218
5596
|
}
|
|
4219
5597
|
const parsed = registrationRequestSchema.safeParse(
|
|
4220
5598
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
4221
5599
|
);
|
|
4222
5600
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
4223
|
-
|
|
5601
|
+
writeJson4(response, 400, { ok: false });
|
|
4224
5602
|
return;
|
|
4225
5603
|
}
|
|
4226
5604
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -4229,19 +5607,19 @@ async function createSourceRegistrationService(input) {
|
|
|
4229
5607
|
(absolutePath) => parsed.data.dataFlow === void 0 ? sourceFilter.shouldTransform(absolutePath, "<") : dataFlowFilter.shouldTransform(absolutePath, "")
|
|
4230
5608
|
);
|
|
4231
5609
|
if (sourcePath === void 0) {
|
|
4232
|
-
|
|
5610
|
+
writeJson4(response, 403, { ok: false });
|
|
4233
5611
|
return;
|
|
4234
5612
|
}
|
|
4235
5613
|
const fileId = await registerCurrentSource(sourcePath, parsed.data.dataFlow);
|
|
4236
5614
|
if (fileId === void 0) {
|
|
4237
|
-
|
|
5615
|
+
writeJson4(response, 409, { ok: false });
|
|
4238
5616
|
return;
|
|
4239
5617
|
}
|
|
4240
|
-
|
|
5618
|
+
writeJson4(response, 200, { epoch: input.registryEpoch, fileId });
|
|
4241
5619
|
};
|
|
4242
5620
|
void handle().catch(() => {
|
|
4243
5621
|
if (!response.headersSent) {
|
|
4244
|
-
|
|
5622
|
+
writeJson4(response, 400, { ok: false });
|
|
4245
5623
|
} else {
|
|
4246
5624
|
response.destroy();
|
|
4247
5625
|
}
|
|
@@ -4251,11 +5629,11 @@ async function createSourceRegistrationService(input) {
|
|
|
4251
5629
|
}
|
|
4252
5630
|
|
|
4253
5631
|
// src/session/session.ts
|
|
4254
|
-
var
|
|
5632
|
+
var import_node_crypto14 = require("crypto");
|
|
4255
5633
|
function createSession() {
|
|
4256
5634
|
return Object.freeze({
|
|
4257
|
-
id: (0,
|
|
4258
|
-
token: (0,
|
|
5635
|
+
id: (0, import_node_crypto14.randomBytes)(16).toString("base64url"),
|
|
5636
|
+
token: (0, import_node_crypto14.randomBytes)(16).toString("base64url")
|
|
4259
5637
|
});
|
|
4260
5638
|
}
|
|
4261
5639
|
|
|
@@ -4266,6 +5644,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
4266
5644
|
"budget",
|
|
4267
5645
|
"debug",
|
|
4268
5646
|
"dataFlow",
|
|
5647
|
+
"contextualAsk",
|
|
4269
5648
|
"editor",
|
|
4270
5649
|
"enabled",
|
|
4271
5650
|
"externalAgent",
|
|
@@ -4357,6 +5736,9 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
4357
5736
|
dataFlow: options.dataFlow.enabled ? Object.freeze({
|
|
4358
5737
|
runtime: options.dataFlow.runtime
|
|
4359
5738
|
}) : false,
|
|
5739
|
+
contextualAsk: options.contextualAsk.enabled ? Object.freeze({
|
|
5740
|
+
...options.contextualAsk.defaultExecutor === void 0 ? {} : { defaultExecutor: options.contextualAsk.defaultExecutor }
|
|
5741
|
+
}) : false,
|
|
4360
5742
|
editor: options.editor,
|
|
4361
5743
|
enabled: options.enabled,
|
|
4362
5744
|
externalAgent: options.externalAgent.enabled,
|
|
@@ -4409,6 +5791,33 @@ function parseDataFlow(value) {
|
|
|
4409
5791
|
runtime: value.runtime
|
|
4410
5792
|
});
|
|
4411
5793
|
}
|
|
5794
|
+
function parseContextualAsk(value) {
|
|
5795
|
+
if (value === false || value === true) return value;
|
|
5796
|
+
if (!isRecord2(value)) {
|
|
5797
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5798
|
+
}
|
|
5799
|
+
const keys = Object.keys(value);
|
|
5800
|
+
if (keys.length === 0) return Object.freeze({});
|
|
5801
|
+
if (!hasExactKeys(value, ["defaultExecutor"]) || !isRecord2(value.defaultExecutor)) {
|
|
5802
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5803
|
+
}
|
|
5804
|
+
const preference = value.defaultExecutor;
|
|
5805
|
+
if (preference.kind === "managed-codex" && hasExactKeys(preference, ["kind"])) {
|
|
5806
|
+
return Object.freeze({
|
|
5807
|
+
defaultExecutor: Object.freeze({ kind: "managed-codex" })
|
|
5808
|
+
});
|
|
5809
|
+
}
|
|
5810
|
+
if (preference.kind === "configured-key" && hasExactKeys(preference, ["kind", "modelProfileId", "providerProfileId"]) && typeof preference.providerProfileId === "string" && typeof preference.modelProfileId === "string") {
|
|
5811
|
+
return Object.freeze({
|
|
5812
|
+
defaultExecutor: Object.freeze({
|
|
5813
|
+
kind: "configured-key",
|
|
5814
|
+
providerProfileId: preference.providerProfileId,
|
|
5815
|
+
modelProfileId: preference.modelProfileId
|
|
5816
|
+
})
|
|
5817
|
+
});
|
|
5818
|
+
}
|
|
5819
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5820
|
+
}
|
|
4412
5821
|
function parseSerializedSpotPatchOptions(value) {
|
|
4413
5822
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
4414
5823
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
@@ -4423,6 +5832,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
4423
5832
|
budget: parseBudget(value.budget),
|
|
4424
5833
|
debug: value.debug,
|
|
4425
5834
|
dataFlow: parseDataFlow(value.dataFlow),
|
|
5835
|
+
contextualAsk: parseContextualAsk(value.contextualAsk),
|
|
4426
5836
|
editor: value.editor,
|
|
4427
5837
|
enabled: value.enabled,
|
|
4428
5838
|
externalAgent: value.externalAgent,
|
|
@@ -4441,10 +5851,15 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
4441
5851
|
}
|
|
4442
5852
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4443
5853
|
0 && (module.exports = {
|
|
5854
|
+
ContextualAskError,
|
|
4444
5855
|
DEFAULT_EXCLUDE,
|
|
4445
5856
|
DEFAULT_OPTIONS,
|
|
4446
5857
|
applyIntegrationPlan,
|
|
5858
|
+
captureAskReadSnapshot,
|
|
5859
|
+
composeContextualAskExecutors,
|
|
4447
5860
|
createAgentJobManager,
|
|
5861
|
+
createConfiguredKeyAskExecutors,
|
|
5862
|
+
createContextualAskManager,
|
|
4448
5863
|
createExternalHandoffService,
|
|
4449
5864
|
createIntegrationFileChange,
|
|
4450
5865
|
createRuntimeAiConfig,
|
|
@@ -4453,9 +5868,12 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
4453
5868
|
createSourceRegistrationService,
|
|
4454
5869
|
createSourceRegistry,
|
|
4455
5870
|
createSpotPatchMiddleware,
|
|
5871
|
+
createWorkspaceActivityCoordinator,
|
|
4456
5872
|
discoverProjectValidationCheck,
|
|
5873
|
+
handleContextualAskRequest,
|
|
4457
5874
|
integrationPathExists,
|
|
4458
5875
|
isLoopbackHostname,
|
|
5876
|
+
matchContextualAskPath,
|
|
4459
5877
|
parseSerializedSpotPatchOptions,
|
|
4460
5878
|
readIntegrationFile,
|
|
4461
5879
|
readJsonRequestBody,
|