@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.js
CHANGED
|
@@ -169,6 +169,10 @@ function createAgentJobManager(options) {
|
|
|
169
169
|
job.updatedAt = dependencies.now();
|
|
170
170
|
emitSnapshot(job);
|
|
171
171
|
emitPhase(job, phaseMessage);
|
|
172
|
+
if (!isActive(status)) {
|
|
173
|
+
job.lease?.release();
|
|
174
|
+
job.lease = void 0;
|
|
175
|
+
}
|
|
172
176
|
};
|
|
173
177
|
const probeResolved = async (selection, signal) => {
|
|
174
178
|
const key = capabilityCacheKey(selection.provider, selection.model);
|
|
@@ -358,6 +362,7 @@ function createAgentJobManager(options) {
|
|
|
358
362
|
[...jobs.values()].map((job) => job.runPromise).filter((promise) => promise !== void 0)
|
|
359
363
|
);
|
|
360
364
|
capabilityCache.clear();
|
|
365
|
+
for (const job of jobs.values()) job.lease?.release();
|
|
361
366
|
jobs.clear();
|
|
362
367
|
},
|
|
363
368
|
create(request) {
|
|
@@ -382,8 +387,13 @@ function createAgentJobManager(options) {
|
|
|
382
387
|
request.providerProfileId,
|
|
383
388
|
request.modelProfileId
|
|
384
389
|
);
|
|
390
|
+
const lease = options.coordinator?.acquire("change");
|
|
391
|
+
if (options.coordinator !== void 0 && lease === void 0) {
|
|
392
|
+
throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
|
|
393
|
+
}
|
|
385
394
|
const id = dependencies.createJobId();
|
|
386
395
|
if (!JOB_ID_PATTERN.test(id) || jobs.has(id)) {
|
|
396
|
+
lease?.release();
|
|
387
397
|
throw new SpotPatchError(ERROR_CODES.INTERNAL_ERROR);
|
|
388
398
|
}
|
|
389
399
|
const timestamp = dependencies.now();
|
|
@@ -397,6 +407,7 @@ function createAgentJobManager(options) {
|
|
|
397
407
|
events: [],
|
|
398
408
|
id,
|
|
399
409
|
listeners: /* @__PURE__ */ new Set(),
|
|
410
|
+
lease,
|
|
400
411
|
model: selection.model,
|
|
401
412
|
phaseMessage: "Agent job queued.",
|
|
402
413
|
preparedChange: void 0,
|
|
@@ -439,6 +450,11 @@ function createAgentJobManager(options) {
|
|
|
439
450
|
hasActiveJob(job.id) ? ERROR_CODES.AGENT_BUSY : ERROR_CODES.APPLY_CONFLICT
|
|
440
451
|
);
|
|
441
452
|
}
|
|
453
|
+
const lease = options.coordinator?.acquire("change");
|
|
454
|
+
if (options.coordinator !== void 0 && lease === void 0) {
|
|
455
|
+
throw new SpotPatchError(ERROR_CODES.AGENT_BUSY);
|
|
456
|
+
}
|
|
457
|
+
job.lease = lease;
|
|
442
458
|
transition(job, "reverting", "Reverting the applied Agent change.");
|
|
443
459
|
try {
|
|
444
460
|
await dependencies.revertChange(job.preparedChange);
|
|
@@ -1249,12 +1265,12 @@ function createExternalHandoffStore(options) {
|
|
|
1249
1265
|
}
|
|
1250
1266
|
return current.snapshot;
|
|
1251
1267
|
};
|
|
1252
|
-
const findReplay = (requestId,
|
|
1268
|
+
const findReplay = (requestId, fingerprint2) => {
|
|
1253
1269
|
requireOpen();
|
|
1254
1270
|
sweep();
|
|
1255
1271
|
const record = idempotency.get(requestId);
|
|
1256
1272
|
if (record === void 0) return void 0;
|
|
1257
|
-
if (record.fingerprint !==
|
|
1273
|
+
if (record.fingerprint !== fingerprint2) {
|
|
1258
1274
|
throw new SpotPatchError5(ERROR_CODES5.HANDOFF_VALIDATION_FAILED);
|
|
1259
1275
|
}
|
|
1260
1276
|
return replayResult(record);
|
|
@@ -1499,12 +1515,12 @@ function createExternalHandoffService(options) {
|
|
|
1499
1515
|
},
|
|
1500
1516
|
async publish(request, authorize) {
|
|
1501
1517
|
requireReady();
|
|
1502
|
-
const
|
|
1503
|
-
const replayed = store.replay(request.requestId,
|
|
1518
|
+
const fingerprint2 = fingerprintExternalHandoffAnnotation(request.annotation);
|
|
1519
|
+
const replayed = store.replay(request.requestId, fingerprint2);
|
|
1504
1520
|
if (replayed !== void 0) return replayed;
|
|
1505
1521
|
const pending = inFlight.get(request.requestId);
|
|
1506
1522
|
if (pending !== void 0) {
|
|
1507
|
-
if (pending.fingerprint !==
|
|
1523
|
+
if (pending.fingerprint !== fingerprint2) {
|
|
1508
1524
|
throw new SpotPatchError6(ERROR_CODES6.HANDOFF_VALIDATION_FAILED);
|
|
1509
1525
|
}
|
|
1510
1526
|
return asReplay(await pending.promise);
|
|
@@ -1514,12 +1530,12 @@ function createExternalHandoffService(options) {
|
|
|
1514
1530
|
const annotation = await authorize(request.annotation);
|
|
1515
1531
|
return store.publish({
|
|
1516
1532
|
annotation,
|
|
1517
|
-
fingerprint,
|
|
1533
|
+
fingerprint: fingerprint2,
|
|
1518
1534
|
requestId: request.requestId,
|
|
1519
1535
|
reserve: activeRegistry.reserve
|
|
1520
1536
|
});
|
|
1521
1537
|
})();
|
|
1522
|
-
const activePublish = Object.freeze({ fingerprint, promise });
|
|
1538
|
+
const activePublish = Object.freeze({ fingerprint: fingerprint2, promise });
|
|
1523
1539
|
inFlight.set(request.requestId, activePublish);
|
|
1524
1540
|
try {
|
|
1525
1541
|
return await promise;
|
|
@@ -1840,6 +1856,8 @@ import {
|
|
|
1840
1856
|
MAX_ANNOTATION_TARGETS,
|
|
1841
1857
|
SPOTPATCH_EDITOR_PREFERENCES,
|
|
1842
1858
|
SPOTPATCH_LOCALE_PREFERENCES,
|
|
1859
|
+
contextualAskOptionsSchema,
|
|
1860
|
+
CONTEXTUAL_ASK_LIMITS,
|
|
1843
1861
|
DEFAULT_DATA_FLOW_LIMITS
|
|
1844
1862
|
} from "@spotpatch/shared";
|
|
1845
1863
|
import { z } from "zod";
|
|
@@ -1878,7 +1896,8 @@ var DEFAULT_OPTIONS = Object.freeze({
|
|
|
1878
1896
|
runtime: "dispatch",
|
|
1879
1897
|
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
1880
1898
|
}),
|
|
1881
|
-
externalAgent: Object.freeze({ enabled: false })
|
|
1899
|
+
externalAgent: Object.freeze({ enabled: false }),
|
|
1900
|
+
contextualAsk: Object.freeze({ enabled: false })
|
|
1882
1901
|
});
|
|
1883
1902
|
var PROFILE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
1884
1903
|
var ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{1,127}$/;
|
|
@@ -2188,6 +2207,35 @@ function resolveDataFlowOptions(options) {
|
|
|
2188
2207
|
limits: DEFAULT_DATA_FLOW_LIMITS
|
|
2189
2208
|
});
|
|
2190
2209
|
}
|
|
2210
|
+
function resolveContextualAskOptions(value, ai) {
|
|
2211
|
+
if (value === void 0 || value === false) return DEFAULT_OPTIONS.contextualAsk;
|
|
2212
|
+
const parsed = contextualAskOptionsSchema.safeParse(value);
|
|
2213
|
+
if (!parsed.success) {
|
|
2214
|
+
throw new RangeError("SpotPatch contextualAsk configuration is invalid.");
|
|
2215
|
+
}
|
|
2216
|
+
const configured = typeof parsed.data === "boolean" ? {} : parsed.data;
|
|
2217
|
+
const preference = configured.defaultExecutor;
|
|
2218
|
+
if (preference?.kind === "configured-key") {
|
|
2219
|
+
const provider = ai === false ? void 0 : ai.providers[preference.providerProfileId];
|
|
2220
|
+
if (provider?.models[preference.modelProfileId] === void 0) {
|
|
2221
|
+
throw new RangeError(
|
|
2222
|
+
"SpotPatch contextualAsk default Key executor must reference a configured AI model."
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (ai !== false && Object.values(ai.providers).reduce(
|
|
2227
|
+
(total, provider) => total + Object.keys(provider.models).length,
|
|
2228
|
+
0
|
|
2229
|
+
) >= CONTEXTUAL_ASK_LIMITS.maximumExecutors) {
|
|
2230
|
+
throw new RangeError(
|
|
2231
|
+
"SpotPatch contextualAsk configuration exceeds the executor limit."
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
return Object.freeze({
|
|
2235
|
+
enabled: true,
|
|
2236
|
+
...preference === void 0 ? {} : { defaultExecutor: Object.freeze({ ...preference }) }
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2191
2239
|
function createRuntimeDataFlowConfig(options) {
|
|
2192
2240
|
return Object.freeze({
|
|
2193
2241
|
enabled: options.enabled,
|
|
@@ -2226,6 +2274,7 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
2226
2274
|
`SpotPatch maxTargets must be an integer between 1 and ${String(MAX_ANNOTATION_TARGETS)}.`
|
|
2227
2275
|
);
|
|
2228
2276
|
}
|
|
2277
|
+
const ai = resolveAiOptions(options.ai ?? environmentAi);
|
|
2229
2278
|
const resolved = {
|
|
2230
2279
|
enabled: options.enabled ?? DEFAULT_OPTIONS.enabled,
|
|
2231
2280
|
include: Object.freeze([...options.include ?? DEFAULT_OPTIONS.include]),
|
|
@@ -2238,11 +2287,12 @@ function resolveOptions(options = {}, environmentAi) {
|
|
|
2238
2287
|
debug: options.debug ?? DEFAULT_OPTIONS.debug,
|
|
2239
2288
|
locale,
|
|
2240
2289
|
maxTargets,
|
|
2241
|
-
ai
|
|
2290
|
+
ai,
|
|
2242
2291
|
dataFlow: resolveDataFlowOptions(options.dataFlow),
|
|
2243
2292
|
externalAgent: Object.freeze({
|
|
2244
2293
|
enabled: options.externalAgent ?? DEFAULT_OPTIONS.externalAgent.enabled
|
|
2245
|
-
})
|
|
2294
|
+
}),
|
|
2295
|
+
contextualAsk: resolveContextualAskOptions(options.contextualAsk, ai)
|
|
2246
2296
|
};
|
|
2247
2297
|
if (resolved.shortcut.trim().length === 0 || resolved.shortcut.length > 128 || resolved.shortcut.includes("\0")) {
|
|
2248
2298
|
throw new RangeError("SpotPatch shortcut is invalid.");
|
|
@@ -2483,6 +2533,9 @@ function createSourceRegistry(options = {}) {
|
|
|
2483
2533
|
resolve(fileId) {
|
|
2484
2534
|
return idToPath.get(fileId);
|
|
2485
2535
|
},
|
|
2536
|
+
findRegisteredId(absolutePath) {
|
|
2537
|
+
return pathToId.get(normalizeAbsolutePath(absolutePath));
|
|
2538
|
+
},
|
|
2486
2539
|
resolveDataFlowComponent(componentSourceId) {
|
|
2487
2540
|
return componentAnchors.get(componentSourceId);
|
|
2488
2541
|
},
|
|
@@ -2495,21 +2548,1342 @@ function createSourceRegistry(options = {}) {
|
|
|
2495
2548
|
});
|
|
2496
2549
|
}
|
|
2497
2550
|
|
|
2551
|
+
// src/contextual-ask/manager.ts
|
|
2552
|
+
import { createHash as createHash4, randomBytes as randomBytes9 } from "crypto";
|
|
2553
|
+
import {
|
|
2554
|
+
askAnswerDraftSchema,
|
|
2555
|
+
askAnswerResultSchema,
|
|
2556
|
+
askJobCreateRequestSchema,
|
|
2557
|
+
askJobEventSchema,
|
|
2558
|
+
askJobResultResponseSchema,
|
|
2559
|
+
askJobSnapshotSchema,
|
|
2560
|
+
contextualAskCapabilitySchema,
|
|
2561
|
+
contextualAskExecutorCapabilitySchema,
|
|
2562
|
+
CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS3,
|
|
2563
|
+
CONTEXTUAL_ASK_SCHEMA_VERSION
|
|
2564
|
+
} from "@spotpatch/shared";
|
|
2565
|
+
|
|
2566
|
+
// src/contextual-ask/error.ts
|
|
2567
|
+
import { ContextualAskExecutorError } from "@spotpatch/agent";
|
|
2568
|
+
var ContextualAskError = class extends Error {
|
|
2569
|
+
code;
|
|
2570
|
+
constructor(code, options = {}) {
|
|
2571
|
+
super(code, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
2572
|
+
this.name = "ContextualAskError";
|
|
2573
|
+
this.code = code;
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
function asContextualAskError(error) {
|
|
2577
|
+
if (error instanceof ContextualAskError) return error;
|
|
2578
|
+
if (error instanceof ContextualAskExecutorError) {
|
|
2579
|
+
return new ContextualAskError(error.code, { cause: error });
|
|
2580
|
+
}
|
|
2581
|
+
return new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/contextual-ask/read-snapshot.ts
|
|
2585
|
+
import { createHash as createHash3, randomBytes as randomBytes8 } from "crypto";
|
|
2586
|
+
import { constants } from "fs";
|
|
2587
|
+
import { lstat as lstat4, open as open2, realpath as realpath3 } from "fs/promises";
|
|
2588
|
+
import path5 from "path";
|
|
2589
|
+
import {
|
|
2590
|
+
CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS2
|
|
2591
|
+
} from "@spotpatch/shared";
|
|
2592
|
+
import { parseSync, Visitor } from "oxc-parser";
|
|
2593
|
+
var SOURCE_EXTENSIONS = Object.freeze([
|
|
2594
|
+
".ts",
|
|
2595
|
+
".tsx",
|
|
2596
|
+
".js",
|
|
2597
|
+
".jsx",
|
|
2598
|
+
".mjs",
|
|
2599
|
+
".cjs",
|
|
2600
|
+
".css",
|
|
2601
|
+
".scss",
|
|
2602
|
+
".sass",
|
|
2603
|
+
".less"
|
|
2604
|
+
]);
|
|
2605
|
+
var PROTECTED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
2606
|
+
".git",
|
|
2607
|
+
".ssh",
|
|
2608
|
+
".spotpatch",
|
|
2609
|
+
"coverage",
|
|
2610
|
+
"dist",
|
|
2611
|
+
"node_modules"
|
|
2612
|
+
]);
|
|
2613
|
+
var PROTECTED_FILES = /* @__PURE__ */ new Set([
|
|
2614
|
+
".gitmodules",
|
|
2615
|
+
".git-credentials",
|
|
2616
|
+
".netrc",
|
|
2617
|
+
".npmrc",
|
|
2618
|
+
".pnpmrc",
|
|
2619
|
+
".pypirc",
|
|
2620
|
+
".yarnrc",
|
|
2621
|
+
"bun.lock",
|
|
2622
|
+
"bun.lockb",
|
|
2623
|
+
"npm-shrinkwrap.json",
|
|
2624
|
+
"package-lock.json",
|
|
2625
|
+
"pnpm-lock.yaml",
|
|
2626
|
+
"yarn.lock"
|
|
2627
|
+
]);
|
|
2628
|
+
var decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2629
|
+
function deny(code) {
|
|
2630
|
+
throw new ContextualAskError(code);
|
|
2631
|
+
}
|
|
2632
|
+
function toPosixRelative(realRoot, absolutePath) {
|
|
2633
|
+
const relative = path5.relative(realRoot, absolutePath);
|
|
2634
|
+
if (relative.length === 0 || relative === ".." || relative.startsWith(`..${path5.sep}`) || path5.isAbsolute(relative)) {
|
|
2635
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2636
|
+
}
|
|
2637
|
+
const normalized = relative.split(path5.sep).join("/");
|
|
2638
|
+
const segments = normalized.toLowerCase().split("/");
|
|
2639
|
+
const fileName = segments.at(-1) ?? "";
|
|
2640
|
+
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")) {
|
|
2641
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2642
|
+
}
|
|
2643
|
+
return normalized;
|
|
2644
|
+
}
|
|
2645
|
+
async function assertNoSymlinkPath(realRoot, absolutePath) {
|
|
2646
|
+
const relative = toPosixRelative(realRoot, absolutePath);
|
|
2647
|
+
let current = realRoot;
|
|
2648
|
+
for (const segment of relative.split("/")) {
|
|
2649
|
+
current = path5.join(current, segment);
|
|
2650
|
+
const metadata = await lstat4(current).catch(() => void 0);
|
|
2651
|
+
if (metadata === void 0 || metadata.isSymbolicLink()) {
|
|
2652
|
+
deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
async function readSecureSource(realRoot, absolutePath) {
|
|
2657
|
+
const normalizedAbsolute = path5.normalize(path5.resolve(absolutePath));
|
|
2658
|
+
const relativePath = toPosixRelative(realRoot, normalizedAbsolute);
|
|
2659
|
+
await assertNoSymlinkPath(realRoot, normalizedAbsolute);
|
|
2660
|
+
const canonical = await realpath3(normalizedAbsolute).catch(() => void 0);
|
|
2661
|
+
if (canonical === void 0 || canonical !== normalizedAbsolute) {
|
|
2662
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2663
|
+
}
|
|
2664
|
+
let handle;
|
|
2665
|
+
try {
|
|
2666
|
+
handle = await open2(normalizedAbsolute, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
2667
|
+
const before = await handle.stat();
|
|
2668
|
+
if (!before.isFile() || before.nlink !== 1) {
|
|
2669
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2670
|
+
}
|
|
2671
|
+
if (before.size > CONTEXTUAL_ASK_LIMITS2.maximumReadBytesPerFile) {
|
|
2672
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
2673
|
+
}
|
|
2674
|
+
const bytes = await handle.readFile();
|
|
2675
|
+
const after = await handle.stat();
|
|
2676
|
+
if (bytes.byteLength !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || bytes.includes(0)) {
|
|
2677
|
+
return deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2678
|
+
}
|
|
2679
|
+
let content;
|
|
2680
|
+
try {
|
|
2681
|
+
content = decoder.decode(bytes);
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED", { cause: error });
|
|
2684
|
+
}
|
|
2685
|
+
return Object.freeze({
|
|
2686
|
+
content,
|
|
2687
|
+
contentHash: createHash3("sha256").update(bytes).digest("hex"),
|
|
2688
|
+
relativePath,
|
|
2689
|
+
size: bytes.byteLength
|
|
2690
|
+
});
|
|
2691
|
+
} finally {
|
|
2692
|
+
await handle?.close().catch(() => void 0);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
function confidence(value) {
|
|
2696
|
+
return value === "exact" || value === "probable" ? value : "approximate";
|
|
2697
|
+
}
|
|
2698
|
+
async function authorizeSeeds(selection, registry, root, realRoot) {
|
|
2699
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2700
|
+
for (const target of selection.targets) {
|
|
2701
|
+
let fileId = target.react.source?.fileId ?? target.source.fileId;
|
|
2702
|
+
let sourceVersion;
|
|
2703
|
+
if (target.react.componentSourceId !== void 0) {
|
|
2704
|
+
const anchor = registry.resolveDataFlowComponent(target.react.componentSourceId);
|
|
2705
|
+
if (anchor === void 0 || target.react.sourceVersion !== anchor.sourceVersion) {
|
|
2706
|
+
deny("ASK_SELECTION_STALE");
|
|
2707
|
+
}
|
|
2708
|
+
fileId = anchor.fileId;
|
|
2709
|
+
sourceVersion = anchor.sourceVersion;
|
|
2710
|
+
}
|
|
2711
|
+
if (fileId === void 0) continue;
|
|
2712
|
+
const registered = registry.resolve(fileId);
|
|
2713
|
+
if (registered === void 0) deny("ASK_SELECTION_STALE");
|
|
2714
|
+
const canonical = await realpath3(registered).catch(() => void 0);
|
|
2715
|
+
if (canonical === void 0) deny("ASK_SELECTION_STALE");
|
|
2716
|
+
const absolutePath = path5.normalize(canonical);
|
|
2717
|
+
const registeredRelative = toPosixRelative(realRoot, absolutePath);
|
|
2718
|
+
const claimedRelative = target.react.source?.relativePath ?? target.source.relativePath;
|
|
2719
|
+
if (claimedRelative !== void 0 && claimedRelative.split("\\").join("/") !== registeredRelative) {
|
|
2720
|
+
deny("ASK_SELECTION_STALE");
|
|
2721
|
+
}
|
|
2722
|
+
const existing = byPath.get(absolutePath);
|
|
2723
|
+
const targetIds = Object.freeze([
|
|
2724
|
+
...existing?.targetIds ?? [],
|
|
2725
|
+
...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
|
|
2726
|
+
]);
|
|
2727
|
+
byPath.set(
|
|
2728
|
+
absolutePath,
|
|
2729
|
+
Object.freeze({
|
|
2730
|
+
absolutePath,
|
|
2731
|
+
fileId,
|
|
2732
|
+
confidence: confidence(
|
|
2733
|
+
target.react.source?.confidence ?? target.source.confidence
|
|
2734
|
+
),
|
|
2735
|
+
targetIds,
|
|
2736
|
+
...sourceVersion === void 0 ? {} : { sourceVersion }
|
|
2737
|
+
})
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
for (const target of selection.targets) {
|
|
2741
|
+
for (const rule of target.styles.matchedRules) {
|
|
2742
|
+
const claimed = rule.source;
|
|
2743
|
+
if (claimed === void 0 || claimed.startsWith("/") || claimed.includes("\\") || claimed.includes("\0") || claimed.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
const registeredPath = path5.resolve(root, ...claimed.split("/"));
|
|
2747
|
+
const fileId = registry.findRegisteredId(registeredPath);
|
|
2748
|
+
if (fileId === void 0) continue;
|
|
2749
|
+
const canonical = await realpath3(registeredPath).catch(() => void 0);
|
|
2750
|
+
if (canonical === void 0) continue;
|
|
2751
|
+
const absolutePath = path5.normalize(canonical);
|
|
2752
|
+
toPosixRelative(realRoot, absolutePath);
|
|
2753
|
+
const existing = byPath.get(absolutePath);
|
|
2754
|
+
const targetIds = Object.freeze([
|
|
2755
|
+
...existing?.targetIds ?? [],
|
|
2756
|
+
...existing?.targetIds.includes(target.targetId) === true ? [] : [target.targetId]
|
|
2757
|
+
]);
|
|
2758
|
+
byPath.set(
|
|
2759
|
+
absolutePath,
|
|
2760
|
+
Object.freeze({
|
|
2761
|
+
absolutePath,
|
|
2762
|
+
fileId,
|
|
2763
|
+
confidence: existing?.confidence ?? "probable",
|
|
2764
|
+
targetIds,
|
|
2765
|
+
...existing?.sourceVersion === void 0 ? {} : { sourceVersion: existing.sourceVersion }
|
|
2766
|
+
})
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
if (byPath.size === 0) deny("ASK_SELECTION_REQUIRED");
|
|
2771
|
+
return [...byPath.values()];
|
|
2772
|
+
}
|
|
2773
|
+
function localSpecifiers(filePath, content) {
|
|
2774
|
+
const found = /* @__PURE__ */ new Set();
|
|
2775
|
+
if (/\.(?:css|scss|sass|less)$/iu.test(filePath)) {
|
|
2776
|
+
const pattern = /@import\s+(?:url\(\s*)?["']([^"']+)["']/gu;
|
|
2777
|
+
for (const match of content.matchAll(pattern)) {
|
|
2778
|
+
if (match[1]?.startsWith(".")) found.add(match[1]);
|
|
2779
|
+
}
|
|
2780
|
+
return [...found].sort();
|
|
2781
|
+
}
|
|
2782
|
+
try {
|
|
2783
|
+
const parsed = parseSync(filePath, content, { sourceType: "module" });
|
|
2784
|
+
if (parsed.errors.length > 0) return [];
|
|
2785
|
+
const add = (value) => {
|
|
2786
|
+
if (typeof value === "string" && value.startsWith(".")) found.add(value);
|
|
2787
|
+
};
|
|
2788
|
+
const visitor = new Visitor({
|
|
2789
|
+
ImportDeclaration(node) {
|
|
2790
|
+
add(node.source.value);
|
|
2791
|
+
},
|
|
2792
|
+
ExportNamedDeclaration(node) {
|
|
2793
|
+
add(node.source?.value);
|
|
2794
|
+
},
|
|
2795
|
+
ExportAllDeclaration(node) {
|
|
2796
|
+
add(node.source.value);
|
|
2797
|
+
},
|
|
2798
|
+
ImportExpression(node) {
|
|
2799
|
+
if ("value" in node.source) add(node.source.value);
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
visitor.visit(parsed.program);
|
|
2803
|
+
} catch {
|
|
2804
|
+
return [];
|
|
2805
|
+
}
|
|
2806
|
+
return [...found].sort();
|
|
2807
|
+
}
|
|
2808
|
+
function importCandidates(importer, specifier) {
|
|
2809
|
+
const base = path5.resolve(path5.dirname(importer), specifier);
|
|
2810
|
+
if (path5.extname(base) !== "") return Object.freeze([base]);
|
|
2811
|
+
return Object.freeze([
|
|
2812
|
+
...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`),
|
|
2813
|
+
...SOURCE_EXTENSIONS.map((extension) => path5.join(base, `index${extension}`))
|
|
2814
|
+
]);
|
|
2815
|
+
}
|
|
2816
|
+
function countLines(content) {
|
|
2817
|
+
return content.length === 0 ? 1 : content.split("\n").length;
|
|
2818
|
+
}
|
|
2819
|
+
function publicSourceEntry(source) {
|
|
2820
|
+
return Object.freeze({
|
|
2821
|
+
handleId: source.handleId,
|
|
2822
|
+
fileId: source.fileId,
|
|
2823
|
+
relativePath: source.relativePath,
|
|
2824
|
+
label: source.label,
|
|
2825
|
+
lineCount: source.lineCount,
|
|
2826
|
+
size: source.size,
|
|
2827
|
+
contentHash: source.contentHash,
|
|
2828
|
+
confidence: source.confidence,
|
|
2829
|
+
targetIds: source.targetIds,
|
|
2830
|
+
...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion }
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
function assertSelectionEvidenceCurrent(selection, captured) {
|
|
2834
|
+
for (const target of selection.targets) {
|
|
2835
|
+
if (target.code === void 0) continue;
|
|
2836
|
+
const source = captured.find(
|
|
2837
|
+
(candidate) => candidate.relativePath === target.code?.relativePath
|
|
2838
|
+
);
|
|
2839
|
+
if (source === void 0) deny("ASK_SELECTION_STALE");
|
|
2840
|
+
const lines = source.content.split(/\r?\n/u);
|
|
2841
|
+
if (target.code.endLine > lines.length) deny("ASK_SELECTION_STALE");
|
|
2842
|
+
const currentExcerpt = lines.slice(target.code.startLine - 1, target.code.endLine).join("\n");
|
|
2843
|
+
if (currentExcerpt !== target.code.excerpt) deny("ASK_SELECTION_STALE");
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
async function captureAskReadSnapshot(options) {
|
|
2847
|
+
options.signal?.throwIfAborted();
|
|
2848
|
+
const realRoot = await realpath3(options.root).catch(() => void 0);
|
|
2849
|
+
if (realRoot === void 0) deny("ASK_SOURCE_SCOPE_DENIED");
|
|
2850
|
+
const seeds = await authorizeSeeds(
|
|
2851
|
+
options.selection,
|
|
2852
|
+
options.registry,
|
|
2853
|
+
options.root,
|
|
2854
|
+
realRoot
|
|
2855
|
+
);
|
|
2856
|
+
const createHandleId = options.createHandleId ?? (() => randomBytes8(16).toString("base64url"));
|
|
2857
|
+
const captured = [];
|
|
2858
|
+
const capturedPaths = /* @__PURE__ */ new Set();
|
|
2859
|
+
const queue = [...seeds];
|
|
2860
|
+
let totalBytes = 0;
|
|
2861
|
+
let truncated = false;
|
|
2862
|
+
while (queue.length > 0) {
|
|
2863
|
+
options.signal?.throwIfAborted();
|
|
2864
|
+
const candidate = queue.shift();
|
|
2865
|
+
if (candidate === void 0 || capturedPaths.has(candidate.absolutePath)) continue;
|
|
2866
|
+
if (captured.length >= CONTEXTUAL_ASK_LIMITS2.maximumReadFiles) {
|
|
2867
|
+
truncated = true;
|
|
2868
|
+
break;
|
|
2869
|
+
}
|
|
2870
|
+
let read;
|
|
2871
|
+
try {
|
|
2872
|
+
read = await readSecureSource(realRoot, candidate.absolutePath);
|
|
2873
|
+
options.signal?.throwIfAborted();
|
|
2874
|
+
} catch (error) {
|
|
2875
|
+
if (captured.length < seeds.length) throw error;
|
|
2876
|
+
truncated = true;
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
if (totalBytes + read.size > CONTEXTUAL_ASK_LIMITS2.maximumReadBytesTotal) {
|
|
2880
|
+
if (captured.length < seeds.length)
|
|
2881
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
2882
|
+
truncated = true;
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
capturedPaths.add(candidate.absolutePath);
|
|
2886
|
+
totalBytes += read.size;
|
|
2887
|
+
const entry = Object.freeze({
|
|
2888
|
+
handleId: createHandleId(),
|
|
2889
|
+
fileId: candidate.fileId,
|
|
2890
|
+
relativePath: read.relativePath,
|
|
2891
|
+
label: path5.basename(read.relativePath),
|
|
2892
|
+
lineCount: countLines(read.content),
|
|
2893
|
+
size: read.size,
|
|
2894
|
+
contentHash: read.contentHash,
|
|
2895
|
+
confidence: candidate.confidence,
|
|
2896
|
+
targetIds: Object.freeze([...candidate.targetIds]),
|
|
2897
|
+
...candidate.sourceVersion === void 0 ? {} : { sourceVersion: candidate.sourceVersion },
|
|
2898
|
+
absolutePath: candidate.absolutePath,
|
|
2899
|
+
content: read.content
|
|
2900
|
+
});
|
|
2901
|
+
captured.push(entry);
|
|
2902
|
+
for (const specifier of localSpecifiers(candidate.absolutePath, read.content)) {
|
|
2903
|
+
options.signal?.throwIfAborted();
|
|
2904
|
+
for (const importPath of importCandidates(candidate.absolutePath, specifier)) {
|
|
2905
|
+
if (capturedPaths.has(importPath)) continue;
|
|
2906
|
+
const metadata = await lstat4(importPath).catch(() => void 0);
|
|
2907
|
+
if (metadata?.isFile() !== true || metadata.isSymbolicLink()) continue;
|
|
2908
|
+
const fileId = options.registry.findRegisteredId(importPath) ?? options.registry.register(importPath);
|
|
2909
|
+
queue.push(
|
|
2910
|
+
Object.freeze({
|
|
2911
|
+
absolutePath: path5.normalize(importPath),
|
|
2912
|
+
fileId,
|
|
2913
|
+
confidence: "probable",
|
|
2914
|
+
targetIds: candidate.targetIds
|
|
2915
|
+
})
|
|
2916
|
+
);
|
|
2917
|
+
break;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
assertSelectionEvidenceCurrent(options.selection, captured);
|
|
2922
|
+
const sourceByHandle = new Map(captured.map((source) => [source.handleId, source]));
|
|
2923
|
+
const publicEntries = Object.freeze(
|
|
2924
|
+
captured.map((source) => publicSourceEntry(source))
|
|
2925
|
+
);
|
|
2926
|
+
const contextHash = createHash3("sha256").update(
|
|
2927
|
+
JSON.stringify({
|
|
2928
|
+
selectionId: options.selection.selectionId,
|
|
2929
|
+
sources: publicEntries.map((source) => ({
|
|
2930
|
+
relativePath: source.relativePath,
|
|
2931
|
+
contentHash: source.contentHash,
|
|
2932
|
+
targetIds: source.targetIds,
|
|
2933
|
+
sourceVersion: source.sourceVersion ?? null
|
|
2934
|
+
}))
|
|
2935
|
+
})
|
|
2936
|
+
).digest("hex");
|
|
2937
|
+
let disposed = false;
|
|
2938
|
+
const requireSource = (handleId) => {
|
|
2939
|
+
if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
2940
|
+
const source = sourceByHandle.get(handleId);
|
|
2941
|
+
if (source === void 0) throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2942
|
+
return source;
|
|
2943
|
+
};
|
|
2944
|
+
const snapshot2 = Object.freeze({
|
|
2945
|
+
manifest: () => {
|
|
2946
|
+
if (disposed) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
2947
|
+
return publicEntries;
|
|
2948
|
+
},
|
|
2949
|
+
read(handleId, range = {}) {
|
|
2950
|
+
const source = requireSource(handleId);
|
|
2951
|
+
const startLine = range.startLine ?? 1;
|
|
2952
|
+
const endLine = range.endLine ?? source.lineCount;
|
|
2953
|
+
if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
|
|
2954
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2955
|
+
}
|
|
2956
|
+
return Object.freeze({
|
|
2957
|
+
handleId,
|
|
2958
|
+
startLine,
|
|
2959
|
+
endLine,
|
|
2960
|
+
content: source.content.split("\n").slice(startLine - 1, endLine).join("\n")
|
|
2961
|
+
});
|
|
2962
|
+
},
|
|
2963
|
+
search(query) {
|
|
2964
|
+
if (query.length === 0 || query.length > CONTEXTUAL_ASK_LIMITS2.maximumSearchQueryCharacters || query.includes("\0")) {
|
|
2965
|
+
throw new ContextualAskError("ASK_SOURCE_SCOPE_DENIED");
|
|
2966
|
+
}
|
|
2967
|
+
const normalized = query.toLocaleLowerCase("en-US");
|
|
2968
|
+
const matches = [];
|
|
2969
|
+
for (const source of captured) {
|
|
2970
|
+
for (const [index, line] of source.content.split("\n").entries()) {
|
|
2971
|
+
if (line.toLocaleLowerCase("en-US").includes(normalized)) {
|
|
2972
|
+
matches.push(
|
|
2973
|
+
Object.freeze({
|
|
2974
|
+
handleId: source.handleId,
|
|
2975
|
+
line: index + 1,
|
|
2976
|
+
preview: line.slice(
|
|
2977
|
+
0,
|
|
2978
|
+
CONTEXTUAL_ASK_LIMITS2.maximumSearchPreviewCharacters
|
|
2979
|
+
)
|
|
2980
|
+
})
|
|
2981
|
+
);
|
|
2982
|
+
if (matches.length >= CONTEXTUAL_ASK_LIMITS2.maximumSearchResults) {
|
|
2983
|
+
return Object.freeze(matches);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return Object.freeze(matches);
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
return Object.freeze({
|
|
2992
|
+
grant: Object.freeze({ contextHash, truncated, sources: publicEntries }),
|
|
2993
|
+
snapshot: snapshot2,
|
|
2994
|
+
dispose() {
|
|
2995
|
+
disposed = true;
|
|
2996
|
+
sourceByHandle.clear();
|
|
2997
|
+
captured.splice(0, captured.length);
|
|
2998
|
+
},
|
|
2999
|
+
async isStale() {
|
|
3000
|
+
for (const source of captured) {
|
|
3001
|
+
try {
|
|
3002
|
+
const current = await readSecureSource(realRoot, source.absolutePath);
|
|
3003
|
+
if (current.contentHash !== source.contentHash) return true;
|
|
3004
|
+
} catch {
|
|
3005
|
+
return true;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
return false;
|
|
3009
|
+
},
|
|
3010
|
+
projectCitation(handleId, startLine, endLine) {
|
|
3011
|
+
const source = requireSource(handleId);
|
|
3012
|
+
if (!Number.isSafeInteger(startLine) || !Number.isSafeInteger(endLine) || startLine < 1 || endLine < startLine || endLine > source.lineCount) {
|
|
3013
|
+
throw new ContextualAskError("ASK_ANSWER_INVALID");
|
|
3014
|
+
}
|
|
3015
|
+
return Object.freeze({ ...publicSourceEntry(source), startLine, endLine });
|
|
3016
|
+
}
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
// src/contextual-ask/manager.ts
|
|
3021
|
+
var ACTIVE_STATUSES = /* @__PURE__ */ new Set([
|
|
3022
|
+
"queued",
|
|
3023
|
+
"authorizing",
|
|
3024
|
+
"running",
|
|
3025
|
+
"cancelling"
|
|
3026
|
+
]);
|
|
3027
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["answered", "cancelled", "failed"]);
|
|
3028
|
+
var DEFAULT_DEPENDENCIES2 = Object.freeze({
|
|
3029
|
+
captureSnapshot: captureAskReadSnapshot,
|
|
3030
|
+
createId: () => randomBytes9(16).toString("base64url"),
|
|
3031
|
+
now: () => /* @__PURE__ */ new Date()
|
|
3032
|
+
});
|
|
3033
|
+
function fingerprint(request) {
|
|
3034
|
+
return createHash4("sha256").update(
|
|
3035
|
+
JSON.stringify({
|
|
3036
|
+
envelope: request.envelope,
|
|
3037
|
+
executorId: request.executorId,
|
|
3038
|
+
providerDataConsent: request.providerDataConsent
|
|
3039
|
+
})
|
|
3040
|
+
).digest("hex");
|
|
3041
|
+
}
|
|
3042
|
+
function fileCountBucket(count) {
|
|
3043
|
+
if (count <= 1) return "one";
|
|
3044
|
+
if (count <= 5) return "few";
|
|
3045
|
+
return "many";
|
|
3046
|
+
}
|
|
3047
|
+
function raceWithAbort(operation, signal) {
|
|
3048
|
+
const reason = () => signal.reason instanceof Error ? signal.reason : new ContextualAskError("ASK_CANCELLED");
|
|
3049
|
+
if (signal.aborted) return Promise.reject(reason());
|
|
3050
|
+
return new Promise((resolve, reject) => {
|
|
3051
|
+
const onAbort = () => {
|
|
3052
|
+
reject(reason());
|
|
3053
|
+
};
|
|
3054
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3055
|
+
operation.then(
|
|
3056
|
+
(value) => {
|
|
3057
|
+
signal.removeEventListener("abort", onAbort);
|
|
3058
|
+
resolve(value);
|
|
3059
|
+
},
|
|
3060
|
+
(error) => {
|
|
3061
|
+
signal.removeEventListener("abort", onAbort);
|
|
3062
|
+
reject(
|
|
3063
|
+
error instanceof Error ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error })
|
|
3064
|
+
);
|
|
3065
|
+
}
|
|
3066
|
+
);
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
function createContextualAskManager(options) {
|
|
3070
|
+
const dependencies = Object.freeze({
|
|
3071
|
+
...DEFAULT_DEPENDENCIES2,
|
|
3072
|
+
...options.dependencies
|
|
3073
|
+
});
|
|
3074
|
+
const executors = /* @__PURE__ */ new Map();
|
|
3075
|
+
for (const executor of options.executors ?? []) {
|
|
3076
|
+
if (executors.has(executor.executorId)) {
|
|
3077
|
+
throw new TypeError("Contextual Ask executor IDs must be unique.");
|
|
3078
|
+
}
|
|
3079
|
+
executors.set(executor.executorId, executor);
|
|
3080
|
+
}
|
|
3081
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
3082
|
+
const requests = /* @__PURE__ */ new Map();
|
|
3083
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
3084
|
+
let closed = false;
|
|
3085
|
+
const isClosed = () => closed;
|
|
3086
|
+
const cleanupExpired = () => {
|
|
3087
|
+
const now = dependencies.now().getTime();
|
|
3088
|
+
for (const [jobId, job] of jobs) {
|
|
3089
|
+
if (job.expiresAt !== void 0 && now >= job.expiresAt) {
|
|
3090
|
+
job.snapshot?.dispose();
|
|
3091
|
+
jobs.delete(jobId);
|
|
3092
|
+
const request = requests.get(job.request.requestId);
|
|
3093
|
+
if (request?.jobId === jobId) requests.delete(job.request.requestId);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
const requireJob = (jobId) => {
|
|
3098
|
+
cleanupExpired();
|
|
3099
|
+
const job = jobs.get(jobId);
|
|
3100
|
+
if (job === void 0) throw new ContextualAskError("ASK_RESULT_EXPIRED");
|
|
3101
|
+
return job;
|
|
3102
|
+
};
|
|
3103
|
+
const publicSnapshot = (job) => askJobSnapshotSchema.parse({
|
|
3104
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3105
|
+
jobId: job.id,
|
|
3106
|
+
selectionId: job.request.envelope.selection.selectionId,
|
|
3107
|
+
status: job.status,
|
|
3108
|
+
executor: {
|
|
3109
|
+
executorId: job.executorCapability.executorId,
|
|
3110
|
+
kind: job.executorCapability.kind,
|
|
3111
|
+
label: job.executorCapability.label,
|
|
3112
|
+
modelLabel: job.executorCapability.effectiveModelLabel
|
|
3113
|
+
},
|
|
3114
|
+
...job.phaseMessage === void 0 ? {} : { phaseMessage: job.phaseMessage },
|
|
3115
|
+
createdAt: job.createdAt,
|
|
3116
|
+
updatedAt: job.updatedAt,
|
|
3117
|
+
canCancel: ["queued", "authorizing", "running"].includes(job.status),
|
|
3118
|
+
...job.errorCode === void 0 ? {} : { errorCode: job.errorCode }
|
|
3119
|
+
});
|
|
3120
|
+
const appendEvent = (job, event) => {
|
|
3121
|
+
job.events.push(askJobEventSchema.parse(event));
|
|
3122
|
+
if (job.events.length > CONTEXTUAL_ASK_LIMITS3.maximumRetainedEvents) {
|
|
3123
|
+
job.events.splice(
|
|
3124
|
+
0,
|
|
3125
|
+
job.events.length - CONTEXTUAL_ASK_LIMITS3.maximumRetainedEvents
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
for (const listener of job.listeners) listener(event);
|
|
3129
|
+
};
|
|
3130
|
+
const eventBase = (job) => {
|
|
3131
|
+
job.sequence += 1;
|
|
3132
|
+
return {
|
|
3133
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3134
|
+
sequence: job.sequence,
|
|
3135
|
+
jobId: job.id,
|
|
3136
|
+
status: job.status,
|
|
3137
|
+
timestamp: dependencies.now().toISOString()
|
|
3138
|
+
};
|
|
3139
|
+
};
|
|
3140
|
+
const emitSnapshot = (job) => {
|
|
3141
|
+
appendEvent(job, {
|
|
3142
|
+
...eventBase(job),
|
|
3143
|
+
type: "snapshot",
|
|
3144
|
+
snapshot: publicSnapshot(job)
|
|
3145
|
+
});
|
|
3146
|
+
};
|
|
3147
|
+
const emitPhase = (job, message) => {
|
|
3148
|
+
appendEvent(job, { ...eventBase(job), type: "phase", message });
|
|
3149
|
+
};
|
|
3150
|
+
const emitReadActivity = (job, entry, state) => {
|
|
3151
|
+
appendEvent(job, {
|
|
3152
|
+
...eventBase(job),
|
|
3153
|
+
type: "read-activity",
|
|
3154
|
+
activity: entry === void 0 ? {
|
|
3155
|
+
kind: "file-count",
|
|
3156
|
+
bucket: fileCountBucket(job.snapshot?.grant.sources.length ?? 0)
|
|
3157
|
+
} : {
|
|
3158
|
+
kind: "source",
|
|
3159
|
+
sourceId: entry.fileId,
|
|
3160
|
+
relativePath: entry.relativePath
|
|
3161
|
+
},
|
|
3162
|
+
state
|
|
3163
|
+
});
|
|
3164
|
+
};
|
|
3165
|
+
const transition = (job, status, phaseMessage, errorCode) => {
|
|
3166
|
+
job.status = status;
|
|
3167
|
+
job.phaseMessage = phaseMessage;
|
|
3168
|
+
job.errorCode = errorCode;
|
|
3169
|
+
job.updatedAt = dependencies.now().toISOString();
|
|
3170
|
+
emitSnapshot(job);
|
|
3171
|
+
if (!TERMINAL_STATUSES.has(status)) emitPhase(job, phaseMessage);
|
|
3172
|
+
};
|
|
3173
|
+
const finish = (job) => {
|
|
3174
|
+
job.expiresAt = dependencies.now().getTime() + CONTEXTUAL_ASK_LIMITS3.resultTtlMs;
|
|
3175
|
+
job.lease.release();
|
|
3176
|
+
job.snapshot?.dispose();
|
|
3177
|
+
job.snapshot = void 0;
|
|
3178
|
+
};
|
|
3179
|
+
const fail = (job, error) => {
|
|
3180
|
+
if (TERMINAL_STATUSES.has(job.status)) return;
|
|
3181
|
+
const normalized = asContextualAskError(error);
|
|
3182
|
+
const abortReason = job.controller.signal.reason;
|
|
3183
|
+
const cancelled = normalized.code === "ASK_CANCELLED" || abortReason instanceof ContextualAskError && abortReason.code === "ASK_CANCELLED";
|
|
3184
|
+
transition(
|
|
3185
|
+
job,
|
|
3186
|
+
cancelled ? "cancelled" : "failed",
|
|
3187
|
+
cancelled ? "Question cancelled." : "Question failed.",
|
|
3188
|
+
cancelled ? "ASK_CANCELLED" : normalized.code
|
|
3189
|
+
);
|
|
3190
|
+
appendEvent(job, {
|
|
3191
|
+
...eventBase(job),
|
|
3192
|
+
type: "error",
|
|
3193
|
+
errorCode: cancelled ? "ASK_CANCELLED" : normalized.code
|
|
3194
|
+
});
|
|
3195
|
+
finish(job);
|
|
3196
|
+
};
|
|
3197
|
+
const instrumentSnapshot = (job, snapshot2, onLimitExceeded) => {
|
|
3198
|
+
const byHandle = new Map(
|
|
3199
|
+
snapshot2.manifest().map((entry) => [entry.handleId, entry])
|
|
3200
|
+
);
|
|
3201
|
+
let readOperations = 0;
|
|
3202
|
+
const beginOperation = () => {
|
|
3203
|
+
readOperations += 1;
|
|
3204
|
+
if (readOperations > CONTEXTUAL_ASK_LIMITS3.maximumToolCalls) {
|
|
3205
|
+
onLimitExceeded();
|
|
3206
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3207
|
+
}
|
|
3208
|
+
};
|
|
3209
|
+
return Object.freeze({
|
|
3210
|
+
manifest: () => snapshot2.manifest(),
|
|
3211
|
+
read(handleId, range) {
|
|
3212
|
+
beginOperation();
|
|
3213
|
+
const entry = byHandle.get(handleId);
|
|
3214
|
+
if (job.status === "running") emitReadActivity(job, entry, "started");
|
|
3215
|
+
try {
|
|
3216
|
+
const result = snapshot2.read(handleId, range);
|
|
3217
|
+
if (job.status === "running") emitReadActivity(job, entry, "succeeded");
|
|
3218
|
+
return result;
|
|
3219
|
+
} catch (error) {
|
|
3220
|
+
if (job.status === "running") emitReadActivity(job, entry, "failed");
|
|
3221
|
+
throw error;
|
|
3222
|
+
}
|
|
3223
|
+
},
|
|
3224
|
+
search(query) {
|
|
3225
|
+
beginOperation();
|
|
3226
|
+
if (job.status === "running") emitReadActivity(job, void 0, "started");
|
|
3227
|
+
try {
|
|
3228
|
+
const result = snapshot2.search(query);
|
|
3229
|
+
if (job.status === "running") {
|
|
3230
|
+
emitReadActivity(job, void 0, "succeeded");
|
|
3231
|
+
}
|
|
3232
|
+
return result;
|
|
3233
|
+
} catch (error) {
|
|
3234
|
+
if (job.status === "running") emitReadActivity(job, void 0, "failed");
|
|
3235
|
+
throw error;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
};
|
|
3240
|
+
const projectAnswer = async (job, draft, captured) => {
|
|
3241
|
+
const sourceIds = /* @__PURE__ */ new Map();
|
|
3242
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3243
|
+
const projectCitations = (citations) => citations.map((citation) => {
|
|
3244
|
+
const key = `${citation.handleId}:${String(citation.startLine)}:${String(citation.endLine)}`;
|
|
3245
|
+
let sourceId = sourceIds.get(key);
|
|
3246
|
+
if (sourceId === void 0) {
|
|
3247
|
+
sourceId = dependencies.createId();
|
|
3248
|
+
sourceIds.set(key, sourceId);
|
|
3249
|
+
sources.set(
|
|
3250
|
+
sourceId,
|
|
3251
|
+
captured.projectCitation(
|
|
3252
|
+
citation.handleId,
|
|
3253
|
+
citation.startLine,
|
|
3254
|
+
citation.endLine
|
|
3255
|
+
)
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
return sourceId;
|
|
3259
|
+
});
|
|
3260
|
+
const blocks = draft.blocks.map((block) => {
|
|
3261
|
+
if (block.kind === "list") {
|
|
3262
|
+
return {
|
|
3263
|
+
kind: "list",
|
|
3264
|
+
items: block.items.map((item) => ({
|
|
3265
|
+
text: item.text,
|
|
3266
|
+
sourceIds: projectCitations(item.citations)
|
|
3267
|
+
}))
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
return Object.freeze({
|
|
3271
|
+
kind: block.kind,
|
|
3272
|
+
...block.kind === "paragraph" ? { text: block.text } : { code: block.code },
|
|
3273
|
+
...block.kind === "code" && block.language !== void 0 ? { language: block.language } : {},
|
|
3274
|
+
sourceIds: projectCitations(block.citations)
|
|
3275
|
+
});
|
|
3276
|
+
});
|
|
3277
|
+
const warnings = [...draft.warnings];
|
|
3278
|
+
if (captured.grant.truncated && !warnings.some((warning) => warning.code === "source-truncated")) {
|
|
3279
|
+
warnings.push(Object.freeze({ code: "source-truncated" }));
|
|
3280
|
+
}
|
|
3281
|
+
if (await captured.isStale() && !warnings.some((warning) => warning.code === "source-stale")) {
|
|
3282
|
+
warnings.push(Object.freeze({ code: "source-stale" }));
|
|
3283
|
+
}
|
|
3284
|
+
const created = dependencies.now();
|
|
3285
|
+
return askAnswerResultSchema.parse({
|
|
3286
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3287
|
+
jobId: job.id,
|
|
3288
|
+
selectionId: job.request.envelope.selection.selectionId,
|
|
3289
|
+
contextHash: captured.grant.contextHash,
|
|
3290
|
+
executor: {
|
|
3291
|
+
executorId: job.executorCapability.executorId,
|
|
3292
|
+
kind: job.executorCapability.kind,
|
|
3293
|
+
label: job.executorCapability.label,
|
|
3294
|
+
modelLabel: job.executorCapability.effectiveModelLabel
|
|
3295
|
+
},
|
|
3296
|
+
blocks,
|
|
3297
|
+
sources: [...sources].map(([sourceId, source]) => ({
|
|
3298
|
+
sourceId,
|
|
3299
|
+
label: source.label,
|
|
3300
|
+
relativePath: source.relativePath,
|
|
3301
|
+
fileId: source.fileId,
|
|
3302
|
+
startLine: source.startLine,
|
|
3303
|
+
endLine: source.endLine,
|
|
3304
|
+
confidence: source.confidence,
|
|
3305
|
+
targetIds: source.targetIds,
|
|
3306
|
+
...source.sourceVersion === void 0 ? {} : { sourceVersion: source.sourceVersion },
|
|
3307
|
+
contentHash: source.contentHash
|
|
3308
|
+
})),
|
|
3309
|
+
warnings,
|
|
3310
|
+
createdAt: created.toISOString(),
|
|
3311
|
+
expiresAt: new Date(
|
|
3312
|
+
created.getTime() + CONTEXTUAL_ASK_LIMITS3.resultTtlMs
|
|
3313
|
+
).toISOString()
|
|
3314
|
+
});
|
|
3315
|
+
};
|
|
3316
|
+
const run = async (job) => {
|
|
3317
|
+
const timeout = setTimeout(() => {
|
|
3318
|
+
job.controller.abort(new ContextualAskError("ASK_TIMEOUT"));
|
|
3319
|
+
}, CONTEXTUAL_ASK_LIMITS3.jobTimeoutMs);
|
|
3320
|
+
timeout.unref();
|
|
3321
|
+
try {
|
|
3322
|
+
transition(job, "authorizing", "Authorizing selected sources.");
|
|
3323
|
+
const captured = await dependencies.captureSnapshot({
|
|
3324
|
+
root: options.root,
|
|
3325
|
+
registry: options.registry,
|
|
3326
|
+
selection: job.request.envelope.selection,
|
|
3327
|
+
signal: job.controller.signal,
|
|
3328
|
+
createHandleId: dependencies.createId
|
|
3329
|
+
});
|
|
3330
|
+
if (job.controller.signal.aborted) {
|
|
3331
|
+
captured.dispose();
|
|
3332
|
+
}
|
|
3333
|
+
job.controller.signal.throwIfAborted();
|
|
3334
|
+
job.snapshot = captured;
|
|
3335
|
+
transition(job, "running", "Analyzing the authorized snapshot.");
|
|
3336
|
+
emitReadActivity(job, void 0, "succeeded");
|
|
3337
|
+
const readLimit = { exceeded: false };
|
|
3338
|
+
const execution = await raceWithAbort(
|
|
3339
|
+
job.executor.execute(
|
|
3340
|
+
{
|
|
3341
|
+
jobId: job.id,
|
|
3342
|
+
envelope: job.request.envelope,
|
|
3343
|
+
grant: captured.grant,
|
|
3344
|
+
snapshot: instrumentSnapshot(job, captured.snapshot, () => {
|
|
3345
|
+
readLimit.exceeded = true;
|
|
3346
|
+
})
|
|
3347
|
+
},
|
|
3348
|
+
job.controller.signal
|
|
3349
|
+
),
|
|
3350
|
+
job.controller.signal
|
|
3351
|
+
);
|
|
3352
|
+
const draft = askAnswerDraftSchema.parse(execution);
|
|
3353
|
+
const effectiveModelLabel = job.executor.effectiveModelLabel?.();
|
|
3354
|
+
if (effectiveModelLabel !== void 0) {
|
|
3355
|
+
job.executorCapability = contextualAskExecutorCapabilitySchema.parse({
|
|
3356
|
+
...job.executorCapability,
|
|
3357
|
+
effectiveModelLabel
|
|
3358
|
+
});
|
|
3359
|
+
}
|
|
3360
|
+
if (readLimit.exceeded) throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3361
|
+
job.controller.signal.throwIfAborted();
|
|
3362
|
+
const result = await projectAnswer(job, draft, captured);
|
|
3363
|
+
job.controller.signal.throwIfAborted();
|
|
3364
|
+
job.result = result;
|
|
3365
|
+
transition(job, "answered", "Answer ready.");
|
|
3366
|
+
appendEvent(job, { ...eventBase(job), type: "answer-ready" });
|
|
3367
|
+
finish(job);
|
|
3368
|
+
} catch (error) {
|
|
3369
|
+
fail(
|
|
3370
|
+
job,
|
|
3371
|
+
error instanceof Error && error.name === "ZodError" ? new ContextualAskError("ASK_ANSWER_INVALID", { cause: error }) : asContextualAskError(error)
|
|
3372
|
+
);
|
|
3373
|
+
} finally {
|
|
3374
|
+
clearTimeout(timeout);
|
|
3375
|
+
}
|
|
3376
|
+
};
|
|
3377
|
+
return Object.freeze({
|
|
3378
|
+
async capability(signal) {
|
|
3379
|
+
if (!options.enabled || closed) {
|
|
3380
|
+
return contextualAskCapabilitySchema.parse({
|
|
3381
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3382
|
+
enabled: false,
|
|
3383
|
+
executors: [],
|
|
3384
|
+
safety: {
|
|
3385
|
+
selectionRequired: true,
|
|
3386
|
+
singleTurn: true,
|
|
3387
|
+
writesAllowed: false,
|
|
3388
|
+
historyStored: false
|
|
3389
|
+
},
|
|
3390
|
+
checkedAt: dependencies.now().toISOString()
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
const capabilities = (await Promise.all(
|
|
3394
|
+
[...executors.values()].map(async (executor) => {
|
|
3395
|
+
try {
|
|
3396
|
+
return contextualAskExecutorCapabilitySchema.parse(
|
|
3397
|
+
await raceWithAbort(executor.capability(signal), signal)
|
|
3398
|
+
);
|
|
3399
|
+
} catch {
|
|
3400
|
+
return void 0;
|
|
3401
|
+
}
|
|
3402
|
+
})
|
|
3403
|
+
)).filter((capability) => capability !== void 0);
|
|
3404
|
+
return contextualAskCapabilitySchema.parse({
|
|
3405
|
+
schemaVersion: CONTEXTUAL_ASK_SCHEMA_VERSION,
|
|
3406
|
+
enabled: true,
|
|
3407
|
+
executors: capabilities,
|
|
3408
|
+
safety: {
|
|
3409
|
+
selectionRequired: true,
|
|
3410
|
+
singleTurn: true,
|
|
3411
|
+
writesAllowed: false,
|
|
3412
|
+
historyStored: false
|
|
3413
|
+
},
|
|
3414
|
+
checkedAt: dependencies.now().toISOString()
|
|
3415
|
+
});
|
|
3416
|
+
},
|
|
3417
|
+
async create(rawRequest) {
|
|
3418
|
+
cleanupExpired();
|
|
3419
|
+
if (!options.enabled || closed) throw new ContextualAskError("ASK_DISABLED");
|
|
3420
|
+
const parsed = askJobCreateRequestSchema.safeParse(rawRequest);
|
|
3421
|
+
if (!parsed.success) throw new ContextualAskError("ASK_QUESTION_INVALID");
|
|
3422
|
+
const request = parsed.data;
|
|
3423
|
+
const requestFingerprint = fingerprint(request);
|
|
3424
|
+
const previous = requests.get(request.requestId);
|
|
3425
|
+
if (previous !== void 0) {
|
|
3426
|
+
if (previous.fingerprint !== requestFingerprint) {
|
|
3427
|
+
throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
|
|
3428
|
+
}
|
|
3429
|
+
return publicSnapshot(requireJob(previous.jobId));
|
|
3430
|
+
}
|
|
3431
|
+
const pending = pendingRequests.get(request.requestId);
|
|
3432
|
+
if (pending !== void 0) {
|
|
3433
|
+
if (pending.fingerprint !== requestFingerprint) {
|
|
3434
|
+
throw new ContextualAskError("ASK_IDEMPOTENCY_CONFLICT");
|
|
3435
|
+
}
|
|
3436
|
+
return await pending.promise;
|
|
3437
|
+
}
|
|
3438
|
+
if ([...jobs.values()].some((job) => ACTIVE_STATUSES.has(job.status))) {
|
|
3439
|
+
throw new ContextualAskError("ASK_BUSY");
|
|
3440
|
+
}
|
|
3441
|
+
const executor = executors.get(request.executorId);
|
|
3442
|
+
if (executor === void 0)
|
|
3443
|
+
throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
|
|
3444
|
+
const lease = options.coordinator.acquire("ask");
|
|
3445
|
+
if (lease === void 0) throw new ContextualAskError("ASK_BUSY");
|
|
3446
|
+
const controller = new AbortController();
|
|
3447
|
+
let leaseTransferred = false;
|
|
3448
|
+
const creation = (async () => {
|
|
3449
|
+
try {
|
|
3450
|
+
const capabilitySignal = AbortSignal.any([
|
|
3451
|
+
controller.signal,
|
|
3452
|
+
AbortSignal.timeout(CONTEXTUAL_ASK_LIMITS3.capabilityTimeoutMs)
|
|
3453
|
+
]);
|
|
3454
|
+
const capability = contextualAskExecutorCapabilitySchema.parse(
|
|
3455
|
+
await raceWithAbort(
|
|
3456
|
+
executor.capability(capabilitySignal),
|
|
3457
|
+
capabilitySignal
|
|
3458
|
+
)
|
|
3459
|
+
);
|
|
3460
|
+
if (isClosed()) throw new ContextualAskError("ASK_DISABLED");
|
|
3461
|
+
if (capability.executorId !== executor.executorId || capability.state !== "ready" || !capability.readOnlyProven) {
|
|
3462
|
+
throw new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE");
|
|
3463
|
+
}
|
|
3464
|
+
for (const [jobId, existing] of jobs) {
|
|
3465
|
+
if (TERMINAL_STATUSES.has(existing.status)) {
|
|
3466
|
+
existing.snapshot?.dispose();
|
|
3467
|
+
jobs.delete(jobId);
|
|
3468
|
+
requests.delete(existing.request.requestId);
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
const createdAt = dependencies.now().toISOString();
|
|
3472
|
+
const job = {
|
|
3473
|
+
controller: new AbortController(),
|
|
3474
|
+
createdAt,
|
|
3475
|
+
events: [],
|
|
3476
|
+
executor,
|
|
3477
|
+
executorCapability: capability,
|
|
3478
|
+
fingerprint: requestFingerprint,
|
|
3479
|
+
id: dependencies.createId(),
|
|
3480
|
+
lease,
|
|
3481
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
3482
|
+
request,
|
|
3483
|
+
errorCode: void 0,
|
|
3484
|
+
expiresAt: void 0,
|
|
3485
|
+
phaseMessage: "Question queued.",
|
|
3486
|
+
result: void 0,
|
|
3487
|
+
runPromise: void 0,
|
|
3488
|
+
sequence: 0,
|
|
3489
|
+
snapshot: void 0,
|
|
3490
|
+
status: "queued",
|
|
3491
|
+
updatedAt: createdAt
|
|
3492
|
+
};
|
|
3493
|
+
jobs.set(job.id, job);
|
|
3494
|
+
requests.set(request.requestId, {
|
|
3495
|
+
fingerprint: requestFingerprint,
|
|
3496
|
+
jobId: job.id
|
|
3497
|
+
});
|
|
3498
|
+
leaseTransferred = true;
|
|
3499
|
+
emitSnapshot(job);
|
|
3500
|
+
emitPhase(job, "Question queued.");
|
|
3501
|
+
job.runPromise = run(job);
|
|
3502
|
+
return publicSnapshot(job);
|
|
3503
|
+
} catch (error) {
|
|
3504
|
+
throw error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", {
|
|
3505
|
+
cause: error
|
|
3506
|
+
});
|
|
3507
|
+
} finally {
|
|
3508
|
+
if (!leaseTransferred) lease.release();
|
|
3509
|
+
pendingRequests.delete(request.requestId);
|
|
3510
|
+
}
|
|
3511
|
+
})();
|
|
3512
|
+
pendingRequests.set(request.requestId, {
|
|
3513
|
+
controller,
|
|
3514
|
+
fingerprint: requestFingerprint,
|
|
3515
|
+
promise: creation
|
|
3516
|
+
});
|
|
3517
|
+
return await creation;
|
|
3518
|
+
},
|
|
3519
|
+
cancel(jobId) {
|
|
3520
|
+
const job = requireJob(jobId);
|
|
3521
|
+
if (TERMINAL_STATUSES.has(job.status)) return publicSnapshot(job);
|
|
3522
|
+
if (job.status !== "cancelling") {
|
|
3523
|
+
transition(job, "cancelling", "Cancelling question.");
|
|
3524
|
+
job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3525
|
+
job.snapshot?.dispose();
|
|
3526
|
+
job.snapshot = void 0;
|
|
3527
|
+
}
|
|
3528
|
+
return publicSnapshot(job);
|
|
3529
|
+
},
|
|
3530
|
+
events(jobId, afterSequence = 0) {
|
|
3531
|
+
if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
|
|
3532
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3533
|
+
}
|
|
3534
|
+
return Object.freeze(
|
|
3535
|
+
requireJob(jobId).events.filter((event) => event.sequence > afterSequence)
|
|
3536
|
+
);
|
|
3537
|
+
},
|
|
3538
|
+
async result(jobId) {
|
|
3539
|
+
const job = requireJob(jobId);
|
|
3540
|
+
await job.runPromise;
|
|
3541
|
+
const current = requireJob(jobId);
|
|
3542
|
+
return askJobResultResponseSchema.parse({
|
|
3543
|
+
snapshot: publicSnapshot(current),
|
|
3544
|
+
...current.result === void 0 ? {} : { result: current.result }
|
|
3545
|
+
});
|
|
3546
|
+
},
|
|
3547
|
+
snapshot(jobId) {
|
|
3548
|
+
return publicSnapshot(requireJob(jobId));
|
|
3549
|
+
},
|
|
3550
|
+
subscribe(jobId, listener) {
|
|
3551
|
+
const job = requireJob(jobId);
|
|
3552
|
+
if (job.listeners.size >= CONTEXTUAL_ASK_LIMITS3.maximumEventSubscribers) {
|
|
3553
|
+
throw new ContextualAskError("ASK_LIMIT_EXCEEDED");
|
|
3554
|
+
}
|
|
3555
|
+
job.listeners.add(listener);
|
|
3556
|
+
return () => job.listeners.delete(listener);
|
|
3557
|
+
},
|
|
3558
|
+
async close() {
|
|
3559
|
+
if (closed) return;
|
|
3560
|
+
closed = true;
|
|
3561
|
+
for (const pending of pendingRequests.values()) {
|
|
3562
|
+
pending.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3563
|
+
}
|
|
3564
|
+
await Promise.allSettled(
|
|
3565
|
+
[...pendingRequests.values()].map((pending) => pending.promise)
|
|
3566
|
+
);
|
|
3567
|
+
for (const job of jobs.values()) {
|
|
3568
|
+
if (ACTIVE_STATUSES.has(job.status)) {
|
|
3569
|
+
job.controller.abort(new ContextualAskError("ASK_CANCELLED"));
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
await Promise.allSettled(
|
|
3573
|
+
[...jobs.values()].map((job) => job.runPromise ?? Promise.resolve())
|
|
3574
|
+
);
|
|
3575
|
+
for (const job of jobs.values()) {
|
|
3576
|
+
job.snapshot?.dispose();
|
|
3577
|
+
job.listeners.clear();
|
|
3578
|
+
job.lease.release();
|
|
3579
|
+
}
|
|
3580
|
+
jobs.clear();
|
|
3581
|
+
requests.clear();
|
|
3582
|
+
pendingRequests.clear();
|
|
3583
|
+
await Promise.allSettled(
|
|
3584
|
+
[...executors.values()].map(async (executor) => executor.dispose?.())
|
|
3585
|
+
);
|
|
3586
|
+
}
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
// src/contextual-ask/configured-key-executors.ts
|
|
3591
|
+
import {
|
|
3592
|
+
createConfiguredKeyAskExecutor,
|
|
3593
|
+
resolveProviderCredential as resolveProviderCredential2
|
|
3594
|
+
} from "@spotpatch/agent";
|
|
3595
|
+
function createConfiguredKeyAskExecutors(options) {
|
|
3596
|
+
const preferred = options.defaultExecutor?.kind === "configured-key" ? options.defaultExecutor : Object.freeze({
|
|
3597
|
+
kind: "configured-key",
|
|
3598
|
+
providerProfileId: options.ai.defaultProvider,
|
|
3599
|
+
modelProfileId: options.ai.providers[options.ai.defaultProvider]?.defaultModel ?? ""
|
|
3600
|
+
});
|
|
3601
|
+
const providers = Object.values(options.ai.providers).sort(
|
|
3602
|
+
(left, right) => left.id === preferred.providerProfileId ? -1 : right.id === preferred.providerProfileId ? 1 : 0
|
|
3603
|
+
);
|
|
3604
|
+
return Object.freeze(
|
|
3605
|
+
providers.flatMap((provider) => {
|
|
3606
|
+
const credential = resolveProviderCredential2(
|
|
3607
|
+
provider.apiKeyEnv,
|
|
3608
|
+
options.environment
|
|
3609
|
+
);
|
|
3610
|
+
const models = Object.values(provider.models).sort(
|
|
3611
|
+
(left, right) => provider.id !== preferred.providerProfileId ? 0 : left.id === preferred.modelProfileId ? -1 : right.id === preferred.modelProfileId ? 1 : 0
|
|
3612
|
+
);
|
|
3613
|
+
return models.map(
|
|
3614
|
+
(model) => createConfiguredKeyAskExecutor({
|
|
3615
|
+
provider,
|
|
3616
|
+
model,
|
|
3617
|
+
credential,
|
|
3618
|
+
limits: options.ai.execution.limits,
|
|
3619
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
3620
|
+
})
|
|
3621
|
+
);
|
|
3622
|
+
})
|
|
3623
|
+
);
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/contextual-ask/executors.ts
|
|
3627
|
+
function composeContextualAskExecutors(options) {
|
|
3628
|
+
return Object.freeze(
|
|
3629
|
+
options.defaultExecutor?.kind === "managed-codex" ? [options.managedCodex, ...options.configuredKey] : [...options.configuredKey, options.managedCodex]
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
// src/workspace/activity-coordinator.ts
|
|
3634
|
+
function createWorkspaceActivityCoordinator() {
|
|
3635
|
+
let active;
|
|
3636
|
+
return Object.freeze({
|
|
3637
|
+
acquire(kind) {
|
|
3638
|
+
if (active !== void 0) return void 0;
|
|
3639
|
+
active = kind;
|
|
3640
|
+
let released = false;
|
|
3641
|
+
return Object.freeze({
|
|
3642
|
+
release() {
|
|
3643
|
+
if (released) return;
|
|
3644
|
+
released = true;
|
|
3645
|
+
if (active === kind) active = void 0;
|
|
3646
|
+
}
|
|
3647
|
+
});
|
|
3648
|
+
},
|
|
3649
|
+
current() {
|
|
3650
|
+
return active;
|
|
3651
|
+
}
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
|
|
2498
3655
|
// src/server/middleware.ts
|
|
2499
3656
|
import {
|
|
2500
3657
|
ERROR_CODES as ERROR_CODES16,
|
|
2501
3658
|
SPOTPATCH_API_BASE,
|
|
2502
|
-
SPOTPATCH_ENDPOINTS as
|
|
3659
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS5,
|
|
2503
3660
|
SpotPatchError as SpotPatchError16,
|
|
2504
3661
|
openEditorRequestSchema,
|
|
2505
3662
|
sourceContextRequestSchema
|
|
2506
3663
|
} from "@spotpatch/shared";
|
|
2507
3664
|
|
|
3665
|
+
// src/contextual-ask/http.ts
|
|
3666
|
+
import {
|
|
3667
|
+
askJobActionRequestSchema,
|
|
3668
|
+
askJobCreateRequestSchema as askJobCreateRequestSchema2,
|
|
3669
|
+
askJobEventsRequestSchema,
|
|
3670
|
+
CONTEXTUAL_ASK_LIMITS as CONTEXTUAL_ASK_LIMITS4,
|
|
3671
|
+
SPOTPATCH_ENDPOINTS
|
|
3672
|
+
} from "@spotpatch/shared";
|
|
3673
|
+
var STATUS_BY_ERROR = Object.freeze({
|
|
3674
|
+
ASK_DISABLED: 404,
|
|
3675
|
+
ASK_SELECTION_REQUIRED: 422,
|
|
3676
|
+
ASK_SELECTION_STALE: 409,
|
|
3677
|
+
ASK_QUESTION_INVALID: 422,
|
|
3678
|
+
ASK_EXECUTOR_UNAVAILABLE: 503,
|
|
3679
|
+
ASK_TIMEOUT: 504,
|
|
3680
|
+
ASK_CONSENT_REQUIRED: 403,
|
|
3681
|
+
ASK_BUSY: 409,
|
|
3682
|
+
ASK_IDEMPOTENCY_CONFLICT: 409,
|
|
3683
|
+
ASK_SOURCE_SCOPE_DENIED: 403,
|
|
3684
|
+
ASK_LIMIT_EXCEEDED: 413,
|
|
3685
|
+
ASK_ANSWER_INVALID: 502,
|
|
3686
|
+
ASK_WRITE_ATTEMPTED: 403,
|
|
3687
|
+
ASK_CANCELLED: 409,
|
|
3688
|
+
ASK_RESULT_EXPIRED: 410,
|
|
3689
|
+
ASK_PROTOCOL_INCOMPATIBLE: 400
|
|
3690
|
+
});
|
|
3691
|
+
var PUBLIC_MESSAGES = Object.freeze({
|
|
3692
|
+
ASK_DISABLED: "Contextual Ask is not enabled.",
|
|
3693
|
+
ASK_SELECTION_REQUIRED: "Select at least one source-backed element.",
|
|
3694
|
+
ASK_SELECTION_STALE: "The selected source is stale.",
|
|
3695
|
+
ASK_QUESTION_INVALID: "The question is invalid.",
|
|
3696
|
+
ASK_EXECUTOR_UNAVAILABLE: "The selected Ask executor is unavailable.",
|
|
3697
|
+
ASK_TIMEOUT: "The Ask executor did not answer before the deadline.",
|
|
3698
|
+
ASK_CONSENT_REQUIRED: "Provider data consent is required.",
|
|
3699
|
+
ASK_BUSY: "Another workspace task is active.",
|
|
3700
|
+
ASK_IDEMPOTENCY_CONFLICT: "The request ID was reused with different content.",
|
|
3701
|
+
ASK_SOURCE_SCOPE_DENIED: "The requested source scope was denied.",
|
|
3702
|
+
ASK_LIMIT_EXCEEDED: "The Ask request exceeded a safety limit.",
|
|
3703
|
+
ASK_ANSWER_INVALID: "The executor returned an invalid answer.",
|
|
3704
|
+
ASK_WRITE_ATTEMPTED: "Ask executors cannot write project files.",
|
|
3705
|
+
ASK_CANCELLED: "The Ask job was cancelled.",
|
|
3706
|
+
ASK_RESULT_EXPIRED: "The Ask result has expired.",
|
|
3707
|
+
ASK_PROTOCOL_INCOMPATIBLE: "The Ask protocol request is incompatible."
|
|
3708
|
+
});
|
|
3709
|
+
function writeJson2(response, status, data) {
|
|
3710
|
+
response.statusCode = status;
|
|
3711
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3712
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3713
|
+
response.end(JSON.stringify({ ok: true, data }));
|
|
3714
|
+
}
|
|
3715
|
+
function writeError(response, error, logger) {
|
|
3716
|
+
const normalized = error instanceof ContextualAskError ? error : new ContextualAskError("ASK_EXECUTOR_UNAVAILABLE", { cause: error });
|
|
3717
|
+
if (normalized.code === "ASK_EXECUTOR_UNAVAILABLE" && error !== normalized) {
|
|
3718
|
+
logger?.warn("[spotpatch:ask] Internal contextual Ask request failure.");
|
|
3719
|
+
}
|
|
3720
|
+
response.statusCode = STATUS_BY_ERROR[normalized.code];
|
|
3721
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3722
|
+
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
3723
|
+
response.end(
|
|
3724
|
+
JSON.stringify({
|
|
3725
|
+
ok: false,
|
|
3726
|
+
error: {
|
|
3727
|
+
code: normalized.code,
|
|
3728
|
+
message: PUBLIC_MESSAGES[normalized.code]
|
|
3729
|
+
}
|
|
3730
|
+
})
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
function matchContextualAskPath(pathname) {
|
|
3734
|
+
if (pathname === SPOTPATCH_ENDPOINTS.askCapability) {
|
|
3735
|
+
return Object.freeze({ kind: "capability" });
|
|
3736
|
+
}
|
|
3737
|
+
if (pathname === SPOTPATCH_ENDPOINTS.askJobs) {
|
|
3738
|
+
return Object.freeze({ kind: "create" });
|
|
3739
|
+
}
|
|
3740
|
+
const prefix = `${SPOTPATCH_ENDPOINTS.askJobs}/`;
|
|
3741
|
+
if (!pathname.startsWith(prefix)) return void 0;
|
|
3742
|
+
const segments = pathname.slice(prefix.length).split("/");
|
|
3743
|
+
if (segments.length !== 2) return void 0;
|
|
3744
|
+
let jobId;
|
|
3745
|
+
try {
|
|
3746
|
+
jobId = decodeURIComponent(segments[0] ?? "");
|
|
3747
|
+
} catch {
|
|
3748
|
+
return void 0;
|
|
3749
|
+
}
|
|
3750
|
+
const action = segments[1];
|
|
3751
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/u.test(jobId) || action !== "events" && action !== "result" && action !== "cancel") {
|
|
3752
|
+
return void 0;
|
|
3753
|
+
}
|
|
3754
|
+
return Object.freeze({ kind: "job", action, jobId });
|
|
3755
|
+
}
|
|
3756
|
+
function parseAfterSequence(request) {
|
|
3757
|
+
let url;
|
|
3758
|
+
try {
|
|
3759
|
+
url = new URL(request.url ?? "/", "http://spotpatch.invalid");
|
|
3760
|
+
} catch {
|
|
3761
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3762
|
+
}
|
|
3763
|
+
if ([...url.searchParams.keys()].some((key) => key !== "afterSequence")) {
|
|
3764
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3765
|
+
}
|
|
3766
|
+
const raw = url.searchParams.get("afterSequence");
|
|
3767
|
+
if (raw === null) return 0;
|
|
3768
|
+
if (!/^\d+$/u.test(raw)) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3769
|
+
const parsed = askJobEventsRequestSchema.safeParse({ afterSequence: Number(raw) });
|
|
3770
|
+
if (!parsed.success || !Number.isSafeInteger(parsed.data.afterSequence)) {
|
|
3771
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3772
|
+
}
|
|
3773
|
+
return parsed.data.afterSequence ?? 0;
|
|
3774
|
+
}
|
|
3775
|
+
function streamEvents(request, response, manager, jobId) {
|
|
3776
|
+
const afterSequence = parseAfterSequence(request);
|
|
3777
|
+
response.statusCode = 200;
|
|
3778
|
+
response.setHeader("Cache-Control", "no-store");
|
|
3779
|
+
response.setHeader("Content-Type", "application/x-ndjson; charset=utf-8");
|
|
3780
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
3781
|
+
let unsubscribe = () => void 0;
|
|
3782
|
+
const heartbeat = setInterval(
|
|
3783
|
+
() => response.write("\n"),
|
|
3784
|
+
CONTEXTUAL_ASK_LIMITS4.eventHeartbeatMs
|
|
3785
|
+
);
|
|
3786
|
+
heartbeat.unref();
|
|
3787
|
+
const cleanup = () => {
|
|
3788
|
+
clearInterval(heartbeat);
|
|
3789
|
+
unsubscribe();
|
|
3790
|
+
};
|
|
3791
|
+
unsubscribe = manager.subscribe(jobId, (event) => {
|
|
3792
|
+
response.write(`${JSON.stringify(event)}
|
|
3793
|
+
`);
|
|
3794
|
+
if (["answered", "cancelled", "failed"].includes(event.status)) {
|
|
3795
|
+
cleanup();
|
|
3796
|
+
response.end();
|
|
3797
|
+
}
|
|
3798
|
+
});
|
|
3799
|
+
const replay = manager.events(jobId, afterSequence);
|
|
3800
|
+
for (const event of replay) response.write(`${JSON.stringify(event)}
|
|
3801
|
+
`);
|
|
3802
|
+
const latest = replay.at(-1);
|
|
3803
|
+
const currentStatus = latest?.status ?? manager.snapshot(jobId).status;
|
|
3804
|
+
if (["answered", "cancelled", "failed"].includes(currentStatus)) {
|
|
3805
|
+
cleanup();
|
|
3806
|
+
response.end();
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
request.once("close", cleanup);
|
|
3810
|
+
response.once("close", cleanup);
|
|
3811
|
+
}
|
|
3812
|
+
function hasEmptySelection(value) {
|
|
3813
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3814
|
+
const envelope = value.envelope;
|
|
3815
|
+
if (typeof envelope !== "object" || envelope === null) return false;
|
|
3816
|
+
const selection = envelope.selection;
|
|
3817
|
+
if (typeof selection !== "object" || selection === null) return false;
|
|
3818
|
+
return Array.isArray(selection.targets) && selection.targets.length === 0;
|
|
3819
|
+
}
|
|
3820
|
+
function lacksProviderDataConsent(value) {
|
|
3821
|
+
return typeof value === "object" && value !== null && value.providerDataConsent !== true;
|
|
3822
|
+
}
|
|
3823
|
+
async function handleContextualAskRequest(request, response, route, manager, logger) {
|
|
3824
|
+
try {
|
|
3825
|
+
if (manager === void 0) throw new ContextualAskError("ASK_DISABLED");
|
|
3826
|
+
if (route.kind === "capability") {
|
|
3827
|
+
if (request.method !== "GET")
|
|
3828
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3829
|
+
writeJson2(
|
|
3830
|
+
response,
|
|
3831
|
+
200,
|
|
3832
|
+
await manager.capability(
|
|
3833
|
+
AbortSignal.timeout(CONTEXTUAL_ASK_LIMITS4.capabilityTimeoutMs)
|
|
3834
|
+
)
|
|
3835
|
+
);
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
if (route.kind === "create") {
|
|
3839
|
+
if (request.method !== "POST")
|
|
3840
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3841
|
+
const raw = await readJsonRequestBody(
|
|
3842
|
+
request,
|
|
3843
|
+
CONTEXTUAL_ASK_LIMITS4.maximumRequestBodyBytes
|
|
3844
|
+
).catch((error) => {
|
|
3845
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
|
|
3846
|
+
});
|
|
3847
|
+
const parsed = askJobCreateRequestSchema2.safeParse(raw);
|
|
3848
|
+
if (!parsed.success) {
|
|
3849
|
+
throw new ContextualAskError(
|
|
3850
|
+
hasEmptySelection(raw) ? "ASK_SELECTION_REQUIRED" : lacksProviderDataConsent(raw) ? "ASK_CONSENT_REQUIRED" : "ASK_PROTOCOL_INCOMPATIBLE"
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
writeJson2(response, 202, await manager.create(parsed.data));
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
if (route.action === "events") {
|
|
3857
|
+
if (request.method !== "GET")
|
|
3858
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3859
|
+
streamEvents(request, response, manager, route.jobId);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
if (route.action === "result") {
|
|
3863
|
+
if (request.method !== "GET")
|
|
3864
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3865
|
+
writeJson2(response, 200, await manager.result(route.jobId));
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (request.method !== "POST")
|
|
3869
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3870
|
+
const action = askJobActionRequestSchema.safeParse(
|
|
3871
|
+
await readJsonRequestBody(request).catch((error) => {
|
|
3872
|
+
throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE", { cause: error });
|
|
3873
|
+
})
|
|
3874
|
+
);
|
|
3875
|
+
if (!action.success) throw new ContextualAskError("ASK_PROTOCOL_INCOMPATIBLE");
|
|
3876
|
+
writeJson2(response, 200, manager.cancel(route.jobId));
|
|
3877
|
+
} catch (error) {
|
|
3878
|
+
writeError(response, error, logger);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
|
|
2508
3882
|
// src/external-handoff/browser-http.ts
|
|
2509
3883
|
import {
|
|
2510
3884
|
ERROR_CODES as ERROR_CODES10,
|
|
2511
3885
|
EXTERNAL_HANDOFF_LIMITS as EXTERNAL_HANDOFF_LIMITS5,
|
|
2512
|
-
SPOTPATCH_ENDPOINTS,
|
|
3886
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS2,
|
|
2513
3887
|
SpotPatchError as SpotPatchError10,
|
|
2514
3888
|
externalHandoffCapabilityRequestSchema,
|
|
2515
3889
|
externalHandoffPublishRequestSchema,
|
|
@@ -2518,8 +3892,8 @@ import {
|
|
|
2518
3892
|
} from "@spotpatch/shared";
|
|
2519
3893
|
|
|
2520
3894
|
// src/server/annotation-authorizer.ts
|
|
2521
|
-
import { realpath as
|
|
2522
|
-
import
|
|
3895
|
+
import { realpath as realpath6 } from "fs/promises";
|
|
3896
|
+
import path8 from "path";
|
|
2523
3897
|
import {
|
|
2524
3898
|
ERROR_CODES as ERROR_CODES9,
|
|
2525
3899
|
SpotPatchError as SpotPatchError9,
|
|
@@ -2528,8 +3902,8 @@ import {
|
|
|
2528
3902
|
} from "@spotpatch/shared";
|
|
2529
3903
|
|
|
2530
3904
|
// src/server/source-context.ts
|
|
2531
|
-
import { readFile as readFile3, realpath as
|
|
2532
|
-
import
|
|
3905
|
+
import { readFile as readFile3, realpath as realpath5 } from "fs/promises";
|
|
3906
|
+
import path7 from "path";
|
|
2533
3907
|
import {
|
|
2534
3908
|
ERROR_CODES as ERROR_CODES8,
|
|
2535
3909
|
SpotPatchError as SpotPatchError8
|
|
@@ -2537,8 +3911,8 @@ import {
|
|
|
2537
3911
|
|
|
2538
3912
|
// src/server/extract-code-context.ts
|
|
2539
3913
|
import {
|
|
2540
|
-
parseSync,
|
|
2541
|
-
Visitor
|
|
3914
|
+
parseSync as parseSync2,
|
|
3915
|
+
Visitor as Visitor2
|
|
2542
3916
|
} from "oxc-parser";
|
|
2543
3917
|
function isComponentName(name) {
|
|
2544
3918
|
return /^[A-Z]/u.test(name);
|
|
@@ -2622,7 +3996,7 @@ function findComponentSpan(options) {
|
|
|
2622
3996
|
}
|
|
2623
3997
|
let parseResult;
|
|
2624
3998
|
try {
|
|
2625
|
-
parseResult =
|
|
3999
|
+
parseResult = parseSync2(options.sourcePath, options.source, {
|
|
2626
4000
|
sourceType: "module"
|
|
2627
4001
|
});
|
|
2628
4002
|
} catch {
|
|
@@ -2633,7 +4007,7 @@ function findComponentSpan(options) {
|
|
|
2633
4007
|
}
|
|
2634
4008
|
const jsxNodes = [];
|
|
2635
4009
|
const components = [];
|
|
2636
|
-
const visitor = new
|
|
4010
|
+
const visitor = new Visitor2({
|
|
2637
4011
|
JSXElement(node) {
|
|
2638
4012
|
jsxNodes.push(node);
|
|
2639
4013
|
},
|
|
@@ -2765,8 +4139,8 @@ function extractCodeContext(options) {
|
|
|
2765
4139
|
}
|
|
2766
4140
|
|
|
2767
4141
|
// src/server/source-file.ts
|
|
2768
|
-
import { realpath as
|
|
2769
|
-
import
|
|
4142
|
+
import { realpath as realpath4, stat as stat2 } from "fs/promises";
|
|
4143
|
+
import path6 from "path";
|
|
2770
4144
|
import { ERROR_CODES as ERROR_CODES7, SpotPatchError as SpotPatchError7 } from "@spotpatch/shared";
|
|
2771
4145
|
var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".jsx", ".tsx"]);
|
|
2772
4146
|
function isMissingFileError(error) {
|
|
@@ -2777,8 +4151,8 @@ async function assertInsideRoot(root, candidate) {
|
|
|
2777
4151
|
let realCandidate;
|
|
2778
4152
|
try {
|
|
2779
4153
|
[realRoot, realCandidate] = await Promise.all([
|
|
2780
|
-
|
|
2781
|
-
|
|
4154
|
+
realpath4(root),
|
|
4155
|
+
realpath4(candidate)
|
|
2782
4156
|
]);
|
|
2783
4157
|
} catch (error) {
|
|
2784
4158
|
if (isMissingFileError(error)) {
|
|
@@ -2788,8 +4162,8 @@ async function assertInsideRoot(root, candidate) {
|
|
|
2788
4162
|
}
|
|
2789
4163
|
throw error;
|
|
2790
4164
|
}
|
|
2791
|
-
const relative =
|
|
2792
|
-
const outside = relative.startsWith(`..${
|
|
4165
|
+
const relative = path6.relative(realRoot, realCandidate);
|
|
4166
|
+
const outside = relative.startsWith(`..${path6.sep}`) || relative === ".." || path6.isAbsolute(relative);
|
|
2793
4167
|
if (outside) {
|
|
2794
4168
|
throw new SpotPatchError7(ERROR_CODES7.SOURCE_OUTSIDE_ROOT);
|
|
2795
4169
|
}
|
|
@@ -2801,7 +4175,7 @@ async function resolveSourceFile(options) {
|
|
|
2801
4175
|
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
2802
4176
|
}
|
|
2803
4177
|
const sourcePath = await assertInsideRoot(options.root, registeredPath);
|
|
2804
|
-
if (!ALLOWED_EXTENSIONS.has(
|
|
4178
|
+
if (!ALLOWED_EXTENSIONS.has(path6.extname(sourcePath).toLowerCase())) {
|
|
2805
4179
|
throw new SpotPatchError7(ERROR_CODES7.SOURCE_NOT_FOUND);
|
|
2806
4180
|
}
|
|
2807
4181
|
let sourceStat;
|
|
@@ -2826,7 +4200,7 @@ async function resolveSourceFile(options) {
|
|
|
2826
4200
|
|
|
2827
4201
|
// src/server/source-context.ts
|
|
2828
4202
|
function toDisplayPath(root, sourcePath) {
|
|
2829
|
-
return
|
|
4203
|
+
return path7.relative(root, sourcePath).split(path7.sep).join("/");
|
|
2830
4204
|
}
|
|
2831
4205
|
async function readSourceContext(options) {
|
|
2832
4206
|
const sourcePath = await resolveSourceFile({
|
|
@@ -2849,11 +4223,11 @@ async function readSourceContext(options) {
|
|
|
2849
4223
|
if (options.request.line > lines.length) {
|
|
2850
4224
|
throw new SpotPatchError8(ERROR_CODES8.INVALID_REQUEST);
|
|
2851
4225
|
}
|
|
2852
|
-
const extension =
|
|
4226
|
+
const extension = path7.extname(sourcePath).toLowerCase();
|
|
2853
4227
|
return extractCodeContext({
|
|
2854
4228
|
source,
|
|
2855
4229
|
sourcePath,
|
|
2856
|
-
relativePath: toDisplayPath(await
|
|
4230
|
+
relativePath: toDisplayPath(await realpath5(options.root), sourcePath),
|
|
2857
4231
|
language: extension === ".tsx" ? "tsx" : "jsx",
|
|
2858
4232
|
line: options.request.line,
|
|
2859
4233
|
column: options.request.column,
|
|
@@ -2895,7 +4269,7 @@ async function authorizeSourceRef(source, registry, root) {
|
|
|
2895
4269
|
return compactSourceRef(source);
|
|
2896
4270
|
}
|
|
2897
4271
|
const sourcePath = await resolveSourceFile({ fileId: source.fileId, registry, root });
|
|
2898
|
-
const relativePath =
|
|
4272
|
+
const relativePath = path8.relative(await realpath6(root), sourcePath).split(path8.sep).join("/");
|
|
2899
4273
|
if (source.relativePath !== void 0 && source.relativePath !== relativePath) {
|
|
2900
4274
|
throw new SpotPatchError9(ERROR_CODES9.INVALID_REQUEST);
|
|
2901
4275
|
}
|
|
@@ -2998,11 +4372,11 @@ async function authorizeAnnotation(input) {
|
|
|
2998
4372
|
}
|
|
2999
4373
|
|
|
3000
4374
|
// src/external-handoff/browser-http.ts
|
|
3001
|
-
function matchExternalHandoffBrowserPath(
|
|
3002
|
-
if (
|
|
3003
|
-
if (
|
|
3004
|
-
if (
|
|
3005
|
-
if (
|
|
4375
|
+
function matchExternalHandoffBrowserPath(path10) {
|
|
4376
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffCapability) return "capability";
|
|
4377
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffPublish) return "publish";
|
|
4378
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffStatus) return "status";
|
|
4379
|
+
if (path10 === SPOTPATCH_ENDPOINTS2.externalHandoffResolveDelivery) {
|
|
3006
4380
|
return "resolve-delivery";
|
|
3007
4381
|
}
|
|
3008
4382
|
return void 0;
|
|
@@ -3082,7 +4456,7 @@ async function handleExternalHandoffBrowserRequest(request, response, route, opt
|
|
|
3082
4456
|
import {
|
|
3083
4457
|
ERROR_CODES as ERROR_CODES11,
|
|
3084
4458
|
EXTERNAL_AGENT_CONTROL_LIMITS,
|
|
3085
|
-
SPOTPATCH_ENDPOINTS as
|
|
4459
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS3,
|
|
3086
4460
|
SpotPatchError as SpotPatchError11,
|
|
3087
4461
|
externalAgentControlCancelRequestSchema,
|
|
3088
4462
|
externalAgentControlConnectRequestSchema,
|
|
@@ -3094,15 +4468,15 @@ import {
|
|
|
3094
4468
|
externalAgentManagedResultSchema,
|
|
3095
4469
|
externalAgentResultRequestSchema
|
|
3096
4470
|
} from "@spotpatch/shared";
|
|
3097
|
-
function matchExternalAgentBrowserPath(
|
|
3098
|
-
if (
|
|
3099
|
-
if (
|
|
3100
|
-
if (
|
|
4471
|
+
function matchExternalAgentBrowserPath(path10) {
|
|
4472
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlStatus) return "status";
|
|
4473
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlConnect) return "connect";
|
|
4474
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlDisconnect) {
|
|
3101
4475
|
return "disconnect";
|
|
3102
4476
|
}
|
|
3103
|
-
if (
|
|
3104
|
-
if (
|
|
3105
|
-
if (
|
|
4477
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentControlCancel) return "cancel";
|
|
4478
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentEvents) return "events";
|
|
4479
|
+
if (path10 === SPOTPATCH_ENDPOINTS3.externalAgentResult) return "result";
|
|
3106
4480
|
return void 0;
|
|
3107
4481
|
}
|
|
3108
4482
|
function writeEvent(response, event) {
|
|
@@ -3241,7 +4615,7 @@ function createExternalAgentBrowserController(port) {
|
|
|
3241
4615
|
// src/server/agent-http.ts
|
|
3242
4616
|
import {
|
|
3243
4617
|
ERROR_CODES as ERROR_CODES12,
|
|
3244
|
-
SPOTPATCH_ENDPOINTS as
|
|
4618
|
+
SPOTPATCH_ENDPOINTS as SPOTPATCH_ENDPOINTS4,
|
|
3245
4619
|
SpotPatchError as SpotPatchError12,
|
|
3246
4620
|
agentCapabilityRequestSchema,
|
|
3247
4621
|
agentJobActionRequestSchema,
|
|
@@ -3286,21 +4660,21 @@ var EVENT_STREAM_END_STATUSES = /* @__PURE__ */ new Set([
|
|
|
3286
4660
|
"reverted",
|
|
3287
4661
|
"failed"
|
|
3288
4662
|
]);
|
|
3289
|
-
function matchAgentRequestPath(
|
|
3290
|
-
if (
|
|
4663
|
+
function matchAgentRequestPath(path10) {
|
|
4664
|
+
if (path10 === SPOTPATCH_ENDPOINTS4.agentCapability) {
|
|
3291
4665
|
return Object.freeze({ kind: "capability" });
|
|
3292
4666
|
}
|
|
3293
|
-
if (
|
|
4667
|
+
if (path10 === SPOTPATCH_ENDPOINTS4.agentWorkspaceHealth) {
|
|
3294
4668
|
return Object.freeze({ kind: "workspace-health" });
|
|
3295
4669
|
}
|
|
3296
|
-
if (
|
|
4670
|
+
if (path10 === SPOTPATCH_ENDPOINTS4.agentJobs) {
|
|
3297
4671
|
return Object.freeze({ kind: "create-job" });
|
|
3298
4672
|
}
|
|
3299
|
-
const prefix = `${
|
|
3300
|
-
if (!
|
|
4673
|
+
const prefix = `${SPOTPATCH_ENDPOINTS4.agentJobs}/`;
|
|
4674
|
+
if (!path10.startsWith(prefix)) {
|
|
3301
4675
|
return void 0;
|
|
3302
4676
|
}
|
|
3303
|
-
const segments =
|
|
4677
|
+
const segments = path10.slice(prefix.length).split("/");
|
|
3304
4678
|
const jobId = segments[0];
|
|
3305
4679
|
const action = segments[1];
|
|
3306
4680
|
if (segments.length !== 2 || jobId === void 0 || !AGENT_JOB_ID_PATTERN.test(jobId) || action === void 0 || !AGENT_JOB_ACTIONS.has(action)) {
|
|
@@ -3494,13 +4868,13 @@ function detectIntegratedEditor(environment) {
|
|
|
3494
4868
|
function editorCommand(editor) {
|
|
3495
4869
|
return editor === "cursor" ? "cursor" : "code";
|
|
3496
4870
|
}
|
|
3497
|
-
var
|
|
4871
|
+
var DEFAULT_DEPENDENCIES3 = Object.freeze({
|
|
3498
4872
|
environment: process.env,
|
|
3499
4873
|
fallbackLauncher: launchEditor,
|
|
3500
4874
|
processSpawner: (command, arguments_, options) => spawn(command, [...arguments_], options),
|
|
3501
4875
|
startupGraceMs: EDITOR_STARTUP_GRACE_MS
|
|
3502
4876
|
});
|
|
3503
|
-
function createEditorLauncher(dependencies =
|
|
4877
|
+
function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES3) {
|
|
3504
4878
|
return (target, configuredEditor) => {
|
|
3505
4879
|
const integratedEditor = detectIntegratedEditor(dependencies.environment);
|
|
3506
4880
|
const resolvedEditor = configuredEditor === "auto" ? integratedEditor : configuredEditor;
|
|
@@ -3555,7 +4929,7 @@ function createEditorLauncher(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
3555
4929
|
var launchConfiguredEditor = createEditorLauncher();
|
|
3556
4930
|
|
|
3557
4931
|
// src/server/data-flow-http.ts
|
|
3558
|
-
import { createHash as
|
|
4932
|
+
import { createHash as createHash5 } from "crypto";
|
|
3559
4933
|
import {
|
|
3560
4934
|
createStaticDataFlowAnalyzer
|
|
3561
4935
|
} from "@spotpatch/analyzer";
|
|
@@ -3684,7 +5058,7 @@ async function handlePageDataFlowReport(request, analyzer, options) {
|
|
|
3684
5058
|
const analyzedVersions = new Set(
|
|
3685
5059
|
componentReports.flatMap((report2) => report2.baseline.analyzedSourceVersions)
|
|
3686
5060
|
);
|
|
3687
|
-
const reportId = `page_${
|
|
5061
|
+
const reportId = `page_${createHash5("sha256").update(componentReports.map((report2) => report2.reportId).join("\0")).digest("base64url").slice(0, 22)}`;
|
|
3688
5062
|
const complete = componentReports.every((report2) => report2.completeness.complete);
|
|
3689
5063
|
const report = Object.freeze({
|
|
3690
5064
|
schemaVersion: DATA_FLOW_SCHEMA_VERSION,
|
|
@@ -3777,6 +5151,9 @@ function parseOrigin(value) {
|
|
|
3777
5151
|
return void 0;
|
|
3778
5152
|
}
|
|
3779
5153
|
}
|
|
5154
|
+
function isSameOriginBrowserGet(request) {
|
|
5155
|
+
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";
|
|
5156
|
+
}
|
|
3780
5157
|
function assertRequestAuthorized(request, options) {
|
|
3781
5158
|
const actualToken = getSingleHeader(request, SPOTPATCH_TOKEN_HEADER);
|
|
3782
5159
|
if (!tokensMatch2(actualToken, options.sessionToken)) {
|
|
@@ -3786,10 +5163,16 @@ function assertRequestAuthorized(request, options) {
|
|
|
3786
5163
|
const originHeader = getSingleHeader(request, "origin");
|
|
3787
5164
|
const host = hostHeader === void 0 ? void 0 : parseHost(hostHeader);
|
|
3788
5165
|
const origin = originHeader === void 0 ? void 0 : parseOrigin(originHeader);
|
|
3789
|
-
if (host === void 0
|
|
5166
|
+
if (host === void 0) {
|
|
3790
5167
|
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
3791
5168
|
}
|
|
3792
5169
|
const hostIsLoopback = isLoopbackHostname(host.hostname);
|
|
5170
|
+
if (origin === void 0) {
|
|
5171
|
+
if (!isSameOriginBrowserGet(request) || !options.allowLan && !hostIsLoopback) {
|
|
5172
|
+
throw new SpotPatchError14(ERROR_CODES14.ORIGIN_NOT_ALLOWED);
|
|
5173
|
+
}
|
|
5174
|
+
return;
|
|
5175
|
+
}
|
|
3793
5176
|
const originIsLoopback = isLoopbackHostname(origin.hostname);
|
|
3794
5177
|
if (!options.allowLan) {
|
|
3795
5178
|
if (!hostIsLoopback || !originIsLoopback) {
|
|
@@ -3862,7 +5245,7 @@ async function readRuntimeBootstrap(request, options) {
|
|
|
3862
5245
|
}
|
|
3863
5246
|
|
|
3864
5247
|
// src/server/middleware.ts
|
|
3865
|
-
var
|
|
5248
|
+
var STATUS_BY_ERROR2 = Object.freeze({
|
|
3866
5249
|
[ERROR_CODES16.INVALID_REQUEST]: 400,
|
|
3867
5250
|
[ERROR_CODES16.INVALID_TOKEN]: 401,
|
|
3868
5251
|
[ERROR_CODES16.ORIGIN_NOT_ALLOWED]: 403,
|
|
@@ -3918,7 +5301,7 @@ var STATUS_BY_ERROR = Object.freeze({
|
|
|
3918
5301
|
[ERROR_CODES16.APPLY_CONFLICT]: 409,
|
|
3919
5302
|
[ERROR_CODES16.INTERNAL_ERROR]: 500
|
|
3920
5303
|
});
|
|
3921
|
-
var
|
|
5304
|
+
var PUBLIC_MESSAGES2 = Object.freeze({
|
|
3922
5305
|
[ERROR_CODES16.INVALID_REQUEST]: "The request is invalid.",
|
|
3923
5306
|
[ERROR_CODES16.INVALID_TOKEN]: "The session token is invalid.",
|
|
3924
5307
|
[ERROR_CODES16.ORIGIN_NOT_ALLOWED]: "The request origin is not allowed.",
|
|
@@ -3974,7 +5357,7 @@ var PUBLIC_MESSAGES = Object.freeze({
|
|
|
3974
5357
|
[ERROR_CODES16.APPLY_CONFLICT]: "The change conflicts with the current worktree.",
|
|
3975
5358
|
[ERROR_CODES16.INTERNAL_ERROR]: "The request could not be completed."
|
|
3976
5359
|
});
|
|
3977
|
-
function
|
|
5360
|
+
function writeJson3(response, status, payload) {
|
|
3978
5361
|
response.statusCode = status;
|
|
3979
5362
|
response.setHeader("Cache-Control", "no-store");
|
|
3980
5363
|
response.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
@@ -3983,16 +5366,16 @@ function writeJson2(response, status, payload) {
|
|
|
3983
5366
|
function asSpotPatchError(error) {
|
|
3984
5367
|
return error instanceof SpotPatchError16 ? error : new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR, void 0, { cause: error });
|
|
3985
5368
|
}
|
|
3986
|
-
function
|
|
5369
|
+
function writeError2(response, error, logger) {
|
|
3987
5370
|
const normalized = asSpotPatchError(error);
|
|
3988
5371
|
if (normalized.code === ERROR_CODES16.INTERNAL_ERROR) {
|
|
3989
5372
|
logger?.warn("[spotpatch:server] Internal request failure.");
|
|
3990
5373
|
}
|
|
3991
|
-
|
|
5374
|
+
writeJson3(response, STATUS_BY_ERROR2[normalized.code], {
|
|
3992
5375
|
ok: false,
|
|
3993
5376
|
error: {
|
|
3994
5377
|
code: normalized.code,
|
|
3995
|
-
message:
|
|
5378
|
+
message: PUBLIC_MESSAGES2[normalized.code]
|
|
3996
5379
|
}
|
|
3997
5380
|
});
|
|
3998
5381
|
}
|
|
@@ -4048,44 +5431,55 @@ function createSpotPatchMiddleware(options) {
|
|
|
4048
5431
|
const dataFlowAnalyzer = createDataFlowAnalyzer(options);
|
|
4049
5432
|
const externalAgentController = options.externalAgentControl === void 0 ? void 0 : createExternalAgentBrowserController(options.externalAgentControl);
|
|
4050
5433
|
const middleware = (request, response, next) => {
|
|
4051
|
-
const
|
|
4052
|
-
const agentRoute = matchAgentRequestPath(
|
|
4053
|
-
const
|
|
4054
|
-
const
|
|
4055
|
-
|
|
5434
|
+
const path10 = requestPath(request);
|
|
5435
|
+
const agentRoute = matchAgentRequestPath(path10);
|
|
5436
|
+
const askRoute = matchContextualAskPath(path10);
|
|
5437
|
+
const externalAgentRoute = matchExternalAgentBrowserPath(path10);
|
|
5438
|
+
const externalHandoffRoute = matchExternalHandoffBrowserPath(path10);
|
|
5439
|
+
if (path10 !== SPOTPATCH_ENDPOINTS5.sourceContext && path10 !== SPOTPATCH_ENDPOINTS5.openEditor && path10 !== SPOTPATCH_ENDPOINTS5.dataFlowComponentReport && path10 !== SPOTPATCH_ENDPOINTS5.dataFlowPageReport && agentRoute === void 0 && askRoute === void 0 && externalAgentRoute === void 0 && externalHandoffRoute === void 0 && !path10.startsWith(`${SPOTPATCH_API_BASE}/`)) {
|
|
4056
5440
|
next();
|
|
4057
5441
|
return;
|
|
4058
5442
|
}
|
|
4059
5443
|
const handle = async () => {
|
|
4060
|
-
if (
|
|
5444
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.bootstrap && bootstrap !== void 0) {
|
|
4061
5445
|
const data = await readRuntimeBootstrap(
|
|
4062
5446
|
request,
|
|
4063
5447
|
bootstrap
|
|
4064
5448
|
);
|
|
4065
|
-
|
|
5449
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4066
5450
|
return;
|
|
4067
5451
|
}
|
|
4068
5452
|
assertRequestAuthorized(request, {
|
|
4069
5453
|
allowLan: options.options.allowLan,
|
|
4070
5454
|
sessionToken: options.session.token
|
|
4071
5455
|
});
|
|
4072
|
-
if (
|
|
5456
|
+
if (askRoute !== void 0) {
|
|
5457
|
+
await handleContextualAskRequest(
|
|
5458
|
+
request,
|
|
5459
|
+
response,
|
|
5460
|
+
askRoute,
|
|
5461
|
+
options.contextualAskManager,
|
|
5462
|
+
options.logger
|
|
5463
|
+
);
|
|
5464
|
+
return;
|
|
5465
|
+
}
|
|
5466
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.sourceContext) {
|
|
4073
5467
|
if (request.method !== "POST") {
|
|
4074
5468
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4075
5469
|
}
|
|
4076
5470
|
const data = await handleSourceContext(request, options);
|
|
4077
|
-
|
|
5471
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4078
5472
|
return;
|
|
4079
5473
|
}
|
|
4080
|
-
if (
|
|
5474
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.openEditor) {
|
|
4081
5475
|
if (request.method !== "POST") {
|
|
4082
5476
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4083
5477
|
}
|
|
4084
5478
|
const data = await handleOpenEditor(request, options);
|
|
4085
|
-
|
|
5479
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4086
5480
|
return;
|
|
4087
5481
|
}
|
|
4088
|
-
if (
|
|
5482
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowComponentReport) {
|
|
4089
5483
|
if (request.method !== "POST") {
|
|
4090
5484
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4091
5485
|
}
|
|
@@ -4094,15 +5488,15 @@ function createSpotPatchMiddleware(options) {
|
|
|
4094
5488
|
dataFlowAnalyzer,
|
|
4095
5489
|
options
|
|
4096
5490
|
);
|
|
4097
|
-
|
|
5491
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4098
5492
|
return;
|
|
4099
5493
|
}
|
|
4100
|
-
if (
|
|
5494
|
+
if (path10 === SPOTPATCH_ENDPOINTS5.dataFlowPageReport) {
|
|
4101
5495
|
if (request.method !== "POST") {
|
|
4102
5496
|
throw new SpotPatchError16(ERROR_CODES16.INVALID_REQUEST);
|
|
4103
5497
|
}
|
|
4104
5498
|
const data = await handlePageDataFlowReport(request, dataFlowAnalyzer, options);
|
|
4105
|
-
|
|
5499
|
+
writeJson3(response, 200, { ok: true, data });
|
|
4106
5500
|
return;
|
|
4107
5501
|
}
|
|
4108
5502
|
if (externalAgentRoute !== void 0) {
|
|
@@ -4114,7 +5508,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
4114
5508
|
response,
|
|
4115
5509
|
externalAgentRoute,
|
|
4116
5510
|
(target, status, data) => {
|
|
4117
|
-
|
|
5511
|
+
writeJson3(target, status, { ok: true, data });
|
|
4118
5512
|
}
|
|
4119
5513
|
);
|
|
4120
5514
|
return;
|
|
@@ -4131,7 +5525,7 @@ function createSpotPatchMiddleware(options) {
|
|
|
4131
5525
|
...options.externalHandoffService === void 0 ? {} : { service: options.externalHandoffService }
|
|
4132
5526
|
},
|
|
4133
5527
|
(target, status, data) => {
|
|
4134
|
-
|
|
5528
|
+
writeJson3(target, status, { ok: true, data });
|
|
4135
5529
|
}
|
|
4136
5530
|
);
|
|
4137
5531
|
return;
|
|
@@ -4145,12 +5539,12 @@ function createSpotPatchMiddleware(options) {
|
|
|
4145
5539
|
options,
|
|
4146
5540
|
agentRoute,
|
|
4147
5541
|
(target, status, data) => {
|
|
4148
|
-
|
|
5542
|
+
writeJson3(target, status, { ok: true, data });
|
|
4149
5543
|
}
|
|
4150
5544
|
);
|
|
4151
5545
|
};
|
|
4152
5546
|
void handle().catch((error) => {
|
|
4153
|
-
|
|
5547
|
+
writeError2(response, error, options.logger);
|
|
4154
5548
|
});
|
|
4155
5549
|
};
|
|
4156
5550
|
return Object.assign(middleware, {
|
|
@@ -4162,8 +5556,8 @@ function createSpotPatchMiddleware(options) {
|
|
|
4162
5556
|
|
|
4163
5557
|
// src/server/source-registration.ts
|
|
4164
5558
|
import { timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
4165
|
-
import { lstat as
|
|
4166
|
-
import
|
|
5559
|
+
import { lstat as lstat5, readFile as readFile4, realpath as realpath7 } from "fs/promises";
|
|
5560
|
+
import path9 from "path";
|
|
4167
5561
|
import {
|
|
4168
5562
|
createDataFlowSourceFilter,
|
|
4169
5563
|
createDataFlowSourceVersion,
|
|
@@ -4203,13 +5597,13 @@ function identitiesMatch(actual, expected) {
|
|
|
4203
5597
|
return actualBytes.byteLength === expectedBytes.byteLength && timingSafeEqual3(actualBytes, expectedBytes);
|
|
4204
5598
|
}
|
|
4205
5599
|
function isWithinRoot(root, candidate) {
|
|
4206
|
-
const relative =
|
|
4207
|
-
return relative === "" || !relative.startsWith(`..${
|
|
5600
|
+
const relative = path9.relative(root, candidate);
|
|
5601
|
+
return relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
|
|
4208
5602
|
}
|
|
4209
5603
|
function hasForbiddenSegment(root, candidate) {
|
|
4210
|
-
return
|
|
5604
|
+
return path9.relative(root, candidate).split(path9.sep).some((segment) => FORBIDDEN_SOURCE_SEGMENTS.has(segment));
|
|
4211
5605
|
}
|
|
4212
|
-
function
|
|
5606
|
+
function writeJson4(response, statusCode, payload) {
|
|
4213
5607
|
const body = JSON.stringify(payload);
|
|
4214
5608
|
response.statusCode = statusCode;
|
|
4215
5609
|
response.setHeader("Cache-Control", "no-store");
|
|
@@ -4229,15 +5623,15 @@ function requestComesFromLoopbackWorker(request) {
|
|
|
4229
5623
|
}
|
|
4230
5624
|
}
|
|
4231
5625
|
async function resolveAuthorizedSource(root, requestedPath, shouldTransform) {
|
|
4232
|
-
if (!
|
|
5626
|
+
if (!path9.isAbsolute(requestedPath) || requestedPath.includes("\0")) {
|
|
4233
5627
|
return void 0;
|
|
4234
5628
|
}
|
|
4235
5629
|
try {
|
|
4236
|
-
const sourceStat = await
|
|
5630
|
+
const sourceStat = await lstat5(requestedPath);
|
|
4237
5631
|
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
|
|
4238
5632
|
return void 0;
|
|
4239
5633
|
}
|
|
4240
|
-
const resolvedPath = await
|
|
5634
|
+
const resolvedPath = await realpath7(requestedPath);
|
|
4241
5635
|
if (!isWithinRoot(root, resolvedPath) || hasForbiddenSegment(root, resolvedPath) || !shouldTransform(resolvedPath)) {
|
|
4242
5636
|
return void 0;
|
|
4243
5637
|
}
|
|
@@ -4250,7 +5644,7 @@ async function createSourceRegistrationService(input) {
|
|
|
4250
5644
|
if (!REGISTRATION_IDENTITY_PATTERN.test(input.internalSecret) || !REGISTRATION_IDENTITY_PATTERN.test(input.registryEpoch)) {
|
|
4251
5645
|
throw new TypeError("The source registration identity is invalid.");
|
|
4252
5646
|
}
|
|
4253
|
-
const root = await
|
|
5647
|
+
const root = await realpath7(input.root);
|
|
4254
5648
|
const sourceFilter = createSourceFilter(root, input.options);
|
|
4255
5649
|
const dataFlowFilter = createDataFlowSourceFilter(root, input.options);
|
|
4256
5650
|
const registrationQueues = /* @__PURE__ */ new Map();
|
|
@@ -4286,14 +5680,14 @@ async function createSourceRegistrationService(input) {
|
|
|
4286
5680
|
getSingleHeader3(request, INTERNAL_SECRET_HEADER),
|
|
4287
5681
|
input.internalSecret
|
|
4288
5682
|
)) {
|
|
4289
|
-
|
|
5683
|
+
writeJson4(response, 403, { ok: false });
|
|
4290
5684
|
return;
|
|
4291
5685
|
}
|
|
4292
5686
|
const parsed = registrationRequestSchema.safeParse(
|
|
4293
5687
|
await readJsonRequestBody(request, REGISTRATION_BODY_LIMIT_BYTES)
|
|
4294
5688
|
);
|
|
4295
5689
|
if (!parsed.success || parsed.data.epoch !== input.registryEpoch) {
|
|
4296
|
-
|
|
5690
|
+
writeJson4(response, 400, { ok: false });
|
|
4297
5691
|
return;
|
|
4298
5692
|
}
|
|
4299
5693
|
const sourcePath = await resolveAuthorizedSource(
|
|
@@ -4302,19 +5696,19 @@ async function createSourceRegistrationService(input) {
|
|
|
4302
5696
|
(absolutePath) => parsed.data.dataFlow === void 0 ? sourceFilter.shouldTransform(absolutePath, "<") : dataFlowFilter.shouldTransform(absolutePath, "")
|
|
4303
5697
|
);
|
|
4304
5698
|
if (sourcePath === void 0) {
|
|
4305
|
-
|
|
5699
|
+
writeJson4(response, 403, { ok: false });
|
|
4306
5700
|
return;
|
|
4307
5701
|
}
|
|
4308
5702
|
const fileId = await registerCurrentSource(sourcePath, parsed.data.dataFlow);
|
|
4309
5703
|
if (fileId === void 0) {
|
|
4310
|
-
|
|
5704
|
+
writeJson4(response, 409, { ok: false });
|
|
4311
5705
|
return;
|
|
4312
5706
|
}
|
|
4313
|
-
|
|
5707
|
+
writeJson4(response, 200, { epoch: input.registryEpoch, fileId });
|
|
4314
5708
|
};
|
|
4315
5709
|
void handle().catch(() => {
|
|
4316
5710
|
if (!response.headersSent) {
|
|
4317
|
-
|
|
5711
|
+
writeJson4(response, 400, { ok: false });
|
|
4318
5712
|
} else {
|
|
4319
5713
|
response.destroy();
|
|
4320
5714
|
}
|
|
@@ -4324,11 +5718,11 @@ async function createSourceRegistrationService(input) {
|
|
|
4324
5718
|
}
|
|
4325
5719
|
|
|
4326
5720
|
// src/session/session.ts
|
|
4327
|
-
import { randomBytes as
|
|
5721
|
+
import { randomBytes as randomBytes10 } from "crypto";
|
|
4328
5722
|
function createSession() {
|
|
4329
5723
|
return Object.freeze({
|
|
4330
|
-
id:
|
|
4331
|
-
token:
|
|
5724
|
+
id: randomBytes10(16).toString("base64url"),
|
|
5725
|
+
token: randomBytes10(16).toString("base64url")
|
|
4332
5726
|
});
|
|
4333
5727
|
}
|
|
4334
5728
|
|
|
@@ -4339,6 +5733,7 @@ var OPTION_KEYS = Object.freeze([
|
|
|
4339
5733
|
"budget",
|
|
4340
5734
|
"debug",
|
|
4341
5735
|
"dataFlow",
|
|
5736
|
+
"contextualAsk",
|
|
4342
5737
|
"editor",
|
|
4343
5738
|
"enabled",
|
|
4344
5739
|
"externalAgent",
|
|
@@ -4430,6 +5825,9 @@ function serializeResolvedSpotPatchOptions(options) {
|
|
|
4430
5825
|
dataFlow: options.dataFlow.enabled ? Object.freeze({
|
|
4431
5826
|
runtime: options.dataFlow.runtime
|
|
4432
5827
|
}) : false,
|
|
5828
|
+
contextualAsk: options.contextualAsk.enabled ? Object.freeze({
|
|
5829
|
+
...options.contextualAsk.defaultExecutor === void 0 ? {} : { defaultExecutor: options.contextualAsk.defaultExecutor }
|
|
5830
|
+
}) : false,
|
|
4433
5831
|
editor: options.editor,
|
|
4434
5832
|
enabled: options.enabled,
|
|
4435
5833
|
externalAgent: options.externalAgent.enabled,
|
|
@@ -4482,6 +5880,33 @@ function parseDataFlow(value) {
|
|
|
4482
5880
|
runtime: value.runtime
|
|
4483
5881
|
});
|
|
4484
5882
|
}
|
|
5883
|
+
function parseContextualAsk(value) {
|
|
5884
|
+
if (value === false || value === true) return value;
|
|
5885
|
+
if (!isRecord2(value)) {
|
|
5886
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5887
|
+
}
|
|
5888
|
+
const keys = Object.keys(value);
|
|
5889
|
+
if (keys.length === 0) return Object.freeze({});
|
|
5890
|
+
if (!hasExactKeys(value, ["defaultExecutor"]) || !isRecord2(value.defaultExecutor)) {
|
|
5891
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5892
|
+
}
|
|
5893
|
+
const preference = value.defaultExecutor;
|
|
5894
|
+
if (preference.kind === "managed-codex" && hasExactKeys(preference, ["kind"])) {
|
|
5895
|
+
return Object.freeze({
|
|
5896
|
+
defaultExecutor: Object.freeze({ kind: "managed-codex" })
|
|
5897
|
+
});
|
|
5898
|
+
}
|
|
5899
|
+
if (preference.kind === "configured-key" && hasExactKeys(preference, ["kind", "modelProfileId", "providerProfileId"]) && typeof preference.providerProfileId === "string" && typeof preference.modelProfileId === "string") {
|
|
5900
|
+
return Object.freeze({
|
|
5901
|
+
defaultExecutor: Object.freeze({
|
|
5902
|
+
kind: "configured-key",
|
|
5903
|
+
providerProfileId: preference.providerProfileId,
|
|
5904
|
+
modelProfileId: preference.modelProfileId
|
|
5905
|
+
})
|
|
5906
|
+
});
|
|
5907
|
+
}
|
|
5908
|
+
throw new TypeError("The SpotPatch contextual Ask transport is invalid.");
|
|
5909
|
+
}
|
|
4485
5910
|
function parseSerializedSpotPatchOptions(value) {
|
|
4486
5911
|
if (!isRecord2(value) || !hasExactKeys(value, OPTION_KEYS)) {
|
|
4487
5912
|
throw new TypeError("The SpotPatch options transport is invalid.");
|
|
@@ -4496,6 +5921,7 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
4496
5921
|
budget: parseBudget(value.budget),
|
|
4497
5922
|
debug: value.debug,
|
|
4498
5923
|
dataFlow: parseDataFlow(value.dataFlow),
|
|
5924
|
+
contextualAsk: parseContextualAsk(value.contextualAsk),
|
|
4499
5925
|
editor: value.editor,
|
|
4500
5926
|
enabled: value.enabled,
|
|
4501
5927
|
externalAgent: value.externalAgent,
|
|
@@ -4513,10 +5939,15 @@ function parseSerializedSpotPatchOptions(value) {
|
|
|
4513
5939
|
}
|
|
4514
5940
|
}
|
|
4515
5941
|
export {
|
|
5942
|
+
ContextualAskError,
|
|
4516
5943
|
DEFAULT_EXCLUDE,
|
|
4517
5944
|
DEFAULT_OPTIONS,
|
|
4518
5945
|
applyIntegrationPlan,
|
|
5946
|
+
captureAskReadSnapshot,
|
|
5947
|
+
composeContextualAskExecutors,
|
|
4519
5948
|
createAgentJobManager,
|
|
5949
|
+
createConfiguredKeyAskExecutors,
|
|
5950
|
+
createContextualAskManager,
|
|
4520
5951
|
createExternalHandoffService,
|
|
4521
5952
|
createIntegrationFileChange,
|
|
4522
5953
|
createRuntimeAiConfig,
|
|
@@ -4525,9 +5956,12 @@ export {
|
|
|
4525
5956
|
createSourceRegistrationService,
|
|
4526
5957
|
createSourceRegistry,
|
|
4527
5958
|
createSpotPatchMiddleware,
|
|
5959
|
+
createWorkspaceActivityCoordinator,
|
|
4528
5960
|
discoverProjectValidationCheck,
|
|
5961
|
+
handleContextualAskRequest,
|
|
4529
5962
|
integrationPathExists,
|
|
4530
5963
|
isLoopbackHostname,
|
|
5964
|
+
matchContextualAskPath,
|
|
4531
5965
|
parseSerializedSpotPatchOptions,
|
|
4532
5966
|
readIntegrationFile,
|
|
4533
5967
|
readJsonRequestBody,
|