@tiangong-ai/cli 0.0.31 → 0.0.33
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/AGENTS.md +2 -2
- package/README.md +140 -98
- package/dist/research/orchestration.js +519 -21
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition.d.ts +71 -0
- package/dist/research/workspace/acquisition.js +593 -0
- package/dist/research/workspace/acquisition.js.map +1 -0
- package/dist/research/workspace/artifacts.d.ts +43 -0
- package/dist/research/workspace/artifacts.js +544 -0
- package/dist/research/workspace/artifacts.js.map +1 -0
- package/dist/research/workspace/broker.d.ts +5 -0
- package/dist/research/workspace/broker.js +318 -43
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/companion-readiness.d.ts +11 -0
- package/dist/research/workspace/companion-readiness.js +75 -0
- package/dist/research/workspace/companion-readiness.js.map +1 -0
- package/dist/research/workspace/discovery-planning.d.ts +25 -0
- package/dist/research/workspace/discovery-planning.js +106 -0
- package/dist/research/workspace/discovery-planning.js.map +1 -0
- package/dist/research/workspace/discovery-status.d.ts +46 -0
- package/dist/research/workspace/discovery-status.js +183 -0
- package/dist/research/workspace/discovery-status.js.map +1 -0
- package/dist/research/workspace/discovery.d.ts +25 -0
- package/dist/research/workspace/discovery.js +268 -0
- package/dist/research/workspace/discovery.js.map +1 -0
- package/dist/research/workspace/downloads.d.ts +76 -0
- package/dist/research/workspace/downloads.js +274 -0
- package/dist/research/workspace/downloads.js.map +1 -0
- package/dist/research/workspace/evidence-ledger.d.ts +52 -0
- package/dist/research/workspace/evidence-ledger.js +487 -0
- package/dist/research/workspace/evidence-ledger.js.map +1 -0
- package/dist/research/workspace/evidence.d.ts +1 -0
- package/dist/research/workspace/evidence.js +2 -0
- package/dist/research/workspace/evidence.js.map +1 -1
- package/dist/research/workspace/input-plan.js +4 -0
- package/dist/research/workspace/input-plan.js.map +1 -1
- package/dist/research/workspace/native-activity.d.ts +65 -0
- package/dist/research/workspace/native-activity.js +153 -0
- package/dist/research/workspace/native-activity.js.map +1 -0
- package/dist/research/workspace/preflight.d.ts +18 -0
- package/dist/research/workspace/preflight.js +47 -20
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +3 -1
- package/dist/research/workspace/projects.js +357 -5
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +171 -3
- package/dist/research/workspace/runtime.js +1582 -77
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.js +28 -6
- package/dist/research/workspace/sanitization.js.map +1 -1
- package/dist/research/workspace/schemas.d.ts +3 -0
- package/dist/research/workspace/schemas.js +206 -33
- package/dist/research/workspace/schemas.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +2 -2
- package/dist/research/workspace/setup-wizard.js +45 -17
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +27 -5
- package/dist/research/workspace/setup.js +400 -85
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +38 -3
- package/dist/research/workspace/workspace.d.ts +2 -0
- package/dist/research/workspace/workspace.js +128 -26
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname } from "node:path";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import { CliError } from "../../errors.js";
|
|
6
6
|
import { loadCapabilityDeclarations, verifyCapabilities } from "./capabilities.js";
|
|
7
7
|
import { loadCapabilityCredentialMap } from "./credentials.js";
|
|
8
|
-
import { loadBrokerEvidenceCache, persistBrokerEvidence, storeBrokerEvidenceCache, } from "./evidence.js";
|
|
9
|
-
import {
|
|
8
|
+
import { loadBrokerEvidenceCache, loadProjectEvidenceReceipts, persistBrokerEvidence, storeBrokerEvidenceCache, } from "./evidence.js";
|
|
9
|
+
import { registerBrokerCandidates } from "./evidence-ledger.js";
|
|
10
|
+
import { deriveDiscoveryPlan } from "./discovery-planning.js";
|
|
11
|
+
import { appendJournalEvent, readJournal } from "./journal.js";
|
|
12
|
+
import { loadProject } from "./projects.js";
|
|
10
13
|
import { sanitizeResearchRecord, sanitizeResearchText } from "./sanitization.js";
|
|
11
|
-
import { canonicalJson, ensureDirectory, isObject, resolveContained, sha256Text, workspacePaths, } from "./storage.js";
|
|
14
|
+
import { canonicalJson, ensureDirectory, isObject, pathExists, resolveContained, sha256Text, workspacePaths, } from "./storage.js";
|
|
12
15
|
import { loadWorkspaceConfig } from "./workspace.js";
|
|
13
16
|
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
14
17
|
const MAX_ERROR_RESPONSE_BYTES = 16 * 1024;
|
|
@@ -16,6 +19,55 @@ const BROKER_CONTEXT_BYTES_PER_TOKEN = 3;
|
|
|
16
19
|
const MAX_RATE_LIMIT_RETRIES = 1;
|
|
17
20
|
const MAX_INLINE_RETRY_DELAY_SECONDS = 5;
|
|
18
21
|
const DEFAULT_RATE_LIMIT_RETRY_SECONDS = 1;
|
|
22
|
+
export async function fetchNativeCandidateSource(input) {
|
|
23
|
+
const project = await loadProject(input.root, input.projectId);
|
|
24
|
+
const discover = project.packages.find((workPackage) => workPackage.stage === "discover");
|
|
25
|
+
if (discover?.status !== "running" || discover.executor !== "producer") {
|
|
26
|
+
throw new CliError("Native evidence fetch is allowed only during an active discover stage.", {
|
|
27
|
+
code: "RESEARCH_NATIVE_STAGE_REQUIRED",
|
|
28
|
+
exitCode: 3,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const declarations = await loadCapabilityDeclarations(input.root);
|
|
32
|
+
const capabilities = declarations.capabilities.filter((capability) => capability.permissions.includes("brokered-network"));
|
|
33
|
+
const verification = await verifyCapabilities(input.root);
|
|
34
|
+
if (verification.status !== "verified") {
|
|
35
|
+
throw new CliError("Native evidence fetch requires verified capability locks.", {
|
|
36
|
+
code: "RESEARCH_CAPABILITY_DRIFT",
|
|
37
|
+
exitCode: 3,
|
|
38
|
+
details: verification,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const config = await loadWorkspaceConfig(input.root);
|
|
42
|
+
const priorCalls = (await readJournal(workspacePaths(input.root).journal)).filter((event) => event.scope === input.projectId && event.type === "capability.fetch.requested").length;
|
|
43
|
+
const discoveryPlan = deriveDiscoveryPlan(project.evidenceRequirements, config, capabilities.map((capability) => capability.id));
|
|
44
|
+
const credentialMap = await loadCapabilityCredentialMap(input.root, declarations.capabilities);
|
|
45
|
+
const receipt = await fetchCandidateSource({
|
|
46
|
+
root: input.root,
|
|
47
|
+
projectId: input.projectId,
|
|
48
|
+
capsuleProject: null,
|
|
49
|
+
capabilities,
|
|
50
|
+
credentialMap,
|
|
51
|
+
workspaceResponseBytes: config.budget.maxBrokerResponseBytes,
|
|
52
|
+
workspaceContextTokens: config.budget.maxBrokerContextTokens,
|
|
53
|
+
workspaceMaxItems: config.budget.maxBrokerItems,
|
|
54
|
+
arguments: input.request,
|
|
55
|
+
onBrokerRequest: () => {
|
|
56
|
+
if (priorCalls >= discoveryPlan.maxCalls)
|
|
57
|
+
throw brokerCallLimitError(discoveryPlan.maxCalls, priorCalls);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const startedCalls = (await readJournal(workspacePaths(input.root).journal)).filter((event) => event.scope === input.projectId && event.type === "capability.fetch.requested").length;
|
|
61
|
+
return {
|
|
62
|
+
...receipt,
|
|
63
|
+
brokerBudget: {
|
|
64
|
+
maxCalls: discoveryPlan.maxCalls,
|
|
65
|
+
hardCallLimit: discoveryPlan.hardCallLimit,
|
|
66
|
+
startedCalls,
|
|
67
|
+
remainingCalls: Math.max(0, discoveryPlan.maxCalls - startedCalls),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
19
71
|
export async function startCapabilityBroker(root, projectId, capsuleProject) {
|
|
20
72
|
const declarations = await loadCapabilityDeclarations(root);
|
|
21
73
|
const networkCapabilities = declarations.capabilities.filter((capability) => capability.permissions.includes("brokered-network"));
|
|
@@ -31,9 +83,15 @@ export async function startCapabilityBroker(root, projectId, capsuleProject) {
|
|
|
31
83
|
}
|
|
32
84
|
const credentialMap = await loadCapabilityCredentialMap(root, declarations.capabilities);
|
|
33
85
|
const config = await loadWorkspaceConfig(root);
|
|
86
|
+
const projectPath = join(workspacePaths(root).projects, projectId, "project.json");
|
|
87
|
+
const project = (await pathExists(projectPath)) ? await loadProject(root, projectId) : null;
|
|
88
|
+
const maxCalls = project
|
|
89
|
+
? deriveDiscoveryPlan(project.evidenceRequirements, config, networkCapabilities.map((capability) => capability.id)).maxCalls
|
|
90
|
+
: config.budget.maxBrokerCalls;
|
|
91
|
+
const priorCalls = (await readJournal(workspacePaths(root).journal)).filter((event) => event.scope === projectId && event.type === "capability.fetch.requested").length;
|
|
34
92
|
const callBudget = {
|
|
35
|
-
maxCalls
|
|
36
|
-
startedCalls:
|
|
93
|
+
maxCalls,
|
|
94
|
+
startedCalls: priorCalls,
|
|
37
95
|
};
|
|
38
96
|
const routeToken = randomUUID().replaceAll("-", "");
|
|
39
97
|
const route = `/mcp/${routeToken}`;
|
|
@@ -140,21 +198,6 @@ async function handleMcpRequest(input) {
|
|
|
140
198
|
sendToolError(input.response, body, "Unsupported tool call.");
|
|
141
199
|
return;
|
|
142
200
|
}
|
|
143
|
-
if (input.callBudget.startedCalls >= input.callBudget.maxCalls) {
|
|
144
|
-
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.rejected", input.projectId, {
|
|
145
|
-
projectId: input.projectId,
|
|
146
|
-
code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
|
|
147
|
-
maxCalls: input.callBudget.maxCalls,
|
|
148
|
-
startedCalls: input.callBudget.startedCalls,
|
|
149
|
-
});
|
|
150
|
-
sendToolError(input.response, body, JSON.stringify({
|
|
151
|
-
code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
|
|
152
|
-
message: "The bounded broker call budget is exhausted; produce the best supported result from admitted receipts.",
|
|
153
|
-
maxCalls: input.callBudget.maxCalls,
|
|
154
|
-
}));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
input.callBudget.startedCalls += 1;
|
|
158
201
|
try {
|
|
159
202
|
const receipt = await fetchCandidateSource({
|
|
160
203
|
root: input.root,
|
|
@@ -166,6 +209,12 @@ async function handleMcpRequest(input) {
|
|
|
166
209
|
workspaceContextTokens: input.workspaceContextTokens,
|
|
167
210
|
workspaceMaxItems: input.workspaceMaxItems,
|
|
168
211
|
arguments: params.arguments,
|
|
212
|
+
onBrokerRequest: () => {
|
|
213
|
+
if (input.callBudget.startedCalls >= input.callBudget.maxCalls) {
|
|
214
|
+
throw brokerCallLimitError(input.callBudget.maxCalls, input.callBudget.startedCalls);
|
|
215
|
+
}
|
|
216
|
+
input.callBudget.startedCalls += 1;
|
|
217
|
+
},
|
|
169
218
|
});
|
|
170
219
|
sendRpcResult(input.response, body, {
|
|
171
220
|
content: [
|
|
@@ -184,6 +233,14 @@ async function handleMcpRequest(input) {
|
|
|
184
233
|
});
|
|
185
234
|
}
|
|
186
235
|
catch (error) {
|
|
236
|
+
if (error instanceof CliError && error.code === "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED") {
|
|
237
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.rejected", input.projectId, {
|
|
238
|
+
projectId: input.projectId,
|
|
239
|
+
code: error.code,
|
|
240
|
+
maxCalls: input.callBudget.maxCalls,
|
|
241
|
+
startedCalls: input.callBudget.startedCalls,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
187
244
|
const detail = error instanceof CliError
|
|
188
245
|
? JSON.stringify({ code: error.code, message: error.message, details: error.details })
|
|
189
246
|
: error instanceof Error
|
|
@@ -288,6 +345,64 @@ async function fetchCandidateSource(input) {
|
|
|
288
345
|
requestBodySha256: encodedRequestBody ? sha256Text(encodedRequestBody) : null,
|
|
289
346
|
accept: capability.http.accept,
|
|
290
347
|
}));
|
|
348
|
+
input.onBrokerRequest?.();
|
|
349
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.requested", input.projectId, {
|
|
350
|
+
attemptId,
|
|
351
|
+
projectId: input.projectId,
|
|
352
|
+
capabilityId,
|
|
353
|
+
credentialId: effectiveCredentialId,
|
|
354
|
+
targetSha256: sha256Text(target.toString()),
|
|
355
|
+
method: capability.http.method,
|
|
356
|
+
requestBodySha256: encodedRequestBody ? sha256Text(encodedRequestBody) : null,
|
|
357
|
+
cacheMode,
|
|
358
|
+
cacheKeySha256,
|
|
359
|
+
});
|
|
360
|
+
const projectReuse = await findProjectRequestReuse(input.root, input.projectId, cacheKeySha256, effectiveCredentialId);
|
|
361
|
+
if (projectReuse) {
|
|
362
|
+
const raw = await readFile(resolveContained(workspacePaths(input.root).control, projectReuse.locator));
|
|
363
|
+
const context = buildContextView(raw, projectReuse.contentType, jsonPointer, requestedItemOffset, maxItems, input.workspaceContextTokens * BROKER_CONTEXT_BYTES_PER_TOKEN);
|
|
364
|
+
const receipt = await persistBrokerEvidence(input.root, {
|
|
365
|
+
attemptId,
|
|
366
|
+
projectId: input.projectId,
|
|
367
|
+
capabilityId,
|
|
368
|
+
credentialId: effectiveCredentialId,
|
|
369
|
+
status: projectReuse.status,
|
|
370
|
+
contentType: projectReuse.contentType,
|
|
371
|
+
sourceSha256: projectReuse.sourceSha256,
|
|
372
|
+
contextItems: context.items,
|
|
373
|
+
contextOffset: context.offset,
|
|
374
|
+
contextTotalItems: context.totalItems,
|
|
375
|
+
contextNextOffset: context.nextOffset,
|
|
376
|
+
contextTruncated: context.truncated,
|
|
377
|
+
redactions: projectReuse.redactions,
|
|
378
|
+
retrievedAt: projectReuse.retrievedAt,
|
|
379
|
+
cacheHit: true,
|
|
380
|
+
}, raw, context.bytes);
|
|
381
|
+
if (input.capsuleProject) {
|
|
382
|
+
await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
|
|
383
|
+
}
|
|
384
|
+
const candidates = await registerBrokerCandidates({
|
|
385
|
+
root: input.root,
|
|
386
|
+
projectId: input.projectId,
|
|
387
|
+
receipt,
|
|
388
|
+
contextBytes: context.bytes,
|
|
389
|
+
selectedJsonPointer: context.selectedJsonPointer,
|
|
390
|
+
itemOffset: context.offset,
|
|
391
|
+
});
|
|
392
|
+
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.reused", input.projectId, {
|
|
393
|
+
attemptId,
|
|
394
|
+
reusedAttemptId: projectReuse.attemptId,
|
|
395
|
+
projectId: input.projectId,
|
|
396
|
+
capabilityId,
|
|
397
|
+
credentialId: effectiveCredentialId,
|
|
398
|
+
cacheKeySha256,
|
|
399
|
+
contextOffset: context.offset,
|
|
400
|
+
contextItems: context.items,
|
|
401
|
+
candidateCount: candidates.length,
|
|
402
|
+
});
|
|
403
|
+
await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256, candidates.length, false, "project");
|
|
404
|
+
return brokerToolResult(receipt, context.bytes, projectReuse.contentType, candidates, "project");
|
|
405
|
+
}
|
|
291
406
|
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.attempted", input.projectId, {
|
|
292
407
|
attemptId,
|
|
293
408
|
projectId: input.projectId,
|
|
@@ -318,12 +433,23 @@ async function fetchCandidateSource(input) {
|
|
|
318
433
|
contextTotalItems: context.totalItems,
|
|
319
434
|
contextNextOffset: context.nextOffset,
|
|
320
435
|
contextTruncated: context.truncated,
|
|
436
|
+
redactions: cached.redactions,
|
|
321
437
|
retrievedAt: cached.retrievedAt,
|
|
322
438
|
cacheHit: true,
|
|
323
439
|
}, raw, context.bytes);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
440
|
+
if (input.capsuleProject) {
|
|
441
|
+
await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
|
|
442
|
+
}
|
|
443
|
+
const candidates = await registerBrokerCandidates({
|
|
444
|
+
root: input.root,
|
|
445
|
+
projectId: input.projectId,
|
|
446
|
+
receipt,
|
|
447
|
+
contextBytes: context.bytes,
|
|
448
|
+
selectedJsonPointer: context.selectedJsonPointer,
|
|
449
|
+
itemOffset: context.offset,
|
|
450
|
+
});
|
|
451
|
+
await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256, candidates.length, false, "workspace");
|
|
452
|
+
return brokerToolResult(receipt, context.bytes, cached.contentType, candidates, "workspace");
|
|
327
453
|
}
|
|
328
454
|
}
|
|
329
455
|
const headers = new Headers(capability.http.staticHeaders);
|
|
@@ -413,8 +539,8 @@ async function fetchCandidateSource(input) {
|
|
|
413
539
|
details: { contentType, allowedContentTypes: capability.http.allowedContentTypes },
|
|
414
540
|
});
|
|
415
541
|
}
|
|
416
|
-
|
|
417
|
-
const context = buildContextView(bytes, contentType, jsonPointer, requestedItemOffset, maxItems, input.workspaceContextTokens * BROKER_CONTEXT_BYTES_PER_TOKEN);
|
|
542
|
+
const persistable = preparePersistableResponse(bytes, contentType);
|
|
543
|
+
const context = buildContextView(persistable.bytes, contentType, jsonPointer, requestedItemOffset, maxItems, input.workspaceContextTokens * BROKER_CONTEXT_BYTES_PER_TOKEN);
|
|
418
544
|
const receipt = await persistBrokerEvidence(input.root, {
|
|
419
545
|
attemptId,
|
|
420
546
|
projectId: input.projectId,
|
|
@@ -428,33 +554,67 @@ async function fetchCandidateSource(input) {
|
|
|
428
554
|
contextTotalItems: context.totalItems,
|
|
429
555
|
contextNextOffset: context.nextOffset,
|
|
430
556
|
contextTruncated: context.truncated,
|
|
557
|
+
redactions: persistable.redactions,
|
|
431
558
|
retrievedAt: new Date().toISOString(),
|
|
432
559
|
cacheHit: false,
|
|
433
|
-
}, bytes, context.bytes);
|
|
434
|
-
|
|
560
|
+
}, persistable.bytes, context.bytes);
|
|
561
|
+
if (input.capsuleProject) {
|
|
562
|
+
await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
|
|
563
|
+
}
|
|
435
564
|
if (!credential)
|
|
436
565
|
await storeBrokerEvidenceCache(input.root, cacheKeySha256, receipt);
|
|
437
|
-
|
|
438
|
-
|
|
566
|
+
const candidates = await registerBrokerCandidates({
|
|
567
|
+
root: input.root,
|
|
568
|
+
projectId: input.projectId,
|
|
569
|
+
receipt,
|
|
570
|
+
contextBytes: context.bytes,
|
|
571
|
+
selectedJsonPointer: context.selectedJsonPointer,
|
|
572
|
+
itemOffset: context.offset,
|
|
573
|
+
});
|
|
574
|
+
await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256, candidates.length, true, null);
|
|
575
|
+
return brokerToolResult(receipt, context.bytes, contentType, candidates, null);
|
|
439
576
|
}
|
|
440
577
|
catch (error) {
|
|
441
|
-
const
|
|
442
|
-
|
|
578
|
+
const reportedError = structuredBrokerFailure(error, capabilityId, effectiveCredentialId);
|
|
579
|
+
const safeDetails = reportedError instanceof CliError && isObject(reportedError.details)
|
|
580
|
+
? sanitizeResearchRecord(reportedError.details, [...input.credentialMap.values()])
|
|
443
581
|
: {};
|
|
444
582
|
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.failed", input.projectId, {
|
|
445
583
|
attemptId,
|
|
446
584
|
capabilityId,
|
|
447
585
|
credentialId: effectiveCredentialId,
|
|
448
|
-
error: bounded(sanitizeResearchText(
|
|
449
|
-
|
|
450
|
-
]), 500),
|
|
451
|
-
failureKind: brokerFailureKind(error),
|
|
586
|
+
error: bounded(sanitizeResearchText(reportedError.message, [...input.credentialMap.values()]), 500),
|
|
587
|
+
failureKind: brokerFailureKind(reportedError),
|
|
452
588
|
...safeDetails,
|
|
453
589
|
});
|
|
454
|
-
throw
|
|
590
|
+
throw reportedError;
|
|
455
591
|
}
|
|
456
592
|
}
|
|
457
|
-
function
|
|
593
|
+
function structuredBrokerFailure(error, capabilityId, credentialId) {
|
|
594
|
+
if (error instanceof CliError)
|
|
595
|
+
return error;
|
|
596
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
597
|
+
const disclosureRejected = /credential-like material|credential disclosure screening/i.test(message);
|
|
598
|
+
return new CliError(disclosureRejected
|
|
599
|
+
? "The provider response was rejected by disclosure screening and was not persisted."
|
|
600
|
+
: bounded(sanitizeResearchText(message), 500), {
|
|
601
|
+
code: disclosureRejected
|
|
602
|
+
? "RESEARCH_BROKER_RESPONSE_REJECTED"
|
|
603
|
+
: "RESEARCH_BROKER_FETCH_FAILED",
|
|
604
|
+
exitCode: 3,
|
|
605
|
+
details: {
|
|
606
|
+
executionMode: "broker",
|
|
607
|
+
credentialScope: "broker",
|
|
608
|
+
networkAttempted: true,
|
|
609
|
+
minimumAction: disclosureRejected
|
|
610
|
+
? "Refine the request or capability response projection so credential-like provider fields are excluded before retrying."
|
|
611
|
+
: "Inspect the sanitized broker failure, then retry only after correcting the deterministic request/provider condition or a transient network failure.",
|
|
612
|
+
capabilityId,
|
|
613
|
+
credentialId,
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function brokerToolResult(receipt, context, contentType, candidates, reuseScope) {
|
|
458
618
|
const inline = contentType.includes("json") || contentType.startsWith("text/");
|
|
459
619
|
return {
|
|
460
620
|
...Object.fromEntries(Object.entries(receipt)),
|
|
@@ -462,8 +622,58 @@ function brokerToolResult(receipt, context, contentType) {
|
|
|
462
622
|
encoding: inline ? "utf8" : "not-inlined",
|
|
463
623
|
text: inline ? context.toString("utf8") : null,
|
|
464
624
|
},
|
|
625
|
+
candidates,
|
|
626
|
+
networkAttempted: reuseScope === null,
|
|
627
|
+
reuseScope,
|
|
465
628
|
};
|
|
466
629
|
}
|
|
630
|
+
function preparePersistableResponse(bytes, contentType) {
|
|
631
|
+
if (!contentType.includes("json")) {
|
|
632
|
+
assertNoSensitiveResponseMaterial(bytes, contentType);
|
|
633
|
+
return { bytes, redactions: 0 };
|
|
634
|
+
}
|
|
635
|
+
let value;
|
|
636
|
+
try {
|
|
637
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
throw new Error("JSON response body is not valid JSON");
|
|
641
|
+
}
|
|
642
|
+
const result = redactSensitiveJson(value);
|
|
643
|
+
const persistable = Buffer.from(JSON.stringify(result.value), "utf8");
|
|
644
|
+
assertNoSensitiveResponseMaterial(persistable, contentType);
|
|
645
|
+
return { bytes: persistable, redactions: result.redactions };
|
|
646
|
+
}
|
|
647
|
+
function redactSensitiveJson(value) {
|
|
648
|
+
if (Array.isArray(value)) {
|
|
649
|
+
const items = [];
|
|
650
|
+
let redactions = 0;
|
|
651
|
+
for (const item of value) {
|
|
652
|
+
const result = redactSensitiveJson(item);
|
|
653
|
+
items.push(result.value);
|
|
654
|
+
redactions += result.redactions;
|
|
655
|
+
}
|
|
656
|
+
return { value: items, redactions };
|
|
657
|
+
}
|
|
658
|
+
if (!isObject(value)) {
|
|
659
|
+
if (typeof value !== "string")
|
|
660
|
+
return { value, redactions: 0 };
|
|
661
|
+
const sanitized = sanitizeResearchText(value);
|
|
662
|
+
return { value: sanitized, redactions: sanitized === value ? 0 : 1 };
|
|
663
|
+
}
|
|
664
|
+
const result = {};
|
|
665
|
+
let redactions = 0;
|
|
666
|
+
for (const [key, item] of Object.entries(value)) {
|
|
667
|
+
if (sensitiveJsonKey(key)) {
|
|
668
|
+
redactions += 1;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const child = redactSensitiveJson(item);
|
|
672
|
+
result[key] = child.value;
|
|
673
|
+
redactions += child.redactions;
|
|
674
|
+
}
|
|
675
|
+
return { value: result, redactions };
|
|
676
|
+
}
|
|
467
677
|
function assertNoSensitiveResponseMaterial(bytes, contentType) {
|
|
468
678
|
if (!contentType.includes("json") && !contentType.startsWith("text/"))
|
|
469
679
|
return;
|
|
@@ -502,8 +712,10 @@ function containsSensitiveJsonField(value) {
|
|
|
502
712
|
return value.some(containsSensitiveJsonField);
|
|
503
713
|
if (!isObject(value))
|
|
504
714
|
return false;
|
|
505
|
-
|
|
506
|
-
|
|
715
|
+
return Object.entries(value).some(([key, item]) => (sensitiveJsonKey(key) && item !== null && item !== "") || containsSensitiveJsonField(item));
|
|
716
|
+
}
|
|
717
|
+
function sensitiveJsonKey(key) {
|
|
718
|
+
return /^(access_token|api[_-]?key|apikey|authorization|cookie|password|secret|session|token)$/i.test(key);
|
|
507
719
|
}
|
|
508
720
|
async function stageContextObject(capsuleProject, locator, bytes) {
|
|
509
721
|
const destination = resolveContained(capsuleProject, locator);
|
|
@@ -519,7 +731,7 @@ async function stageContextObject(capsuleProject, locator, bytes) {
|
|
|
519
731
|
}
|
|
520
732
|
}
|
|
521
733
|
}
|
|
522
|
-
async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256) {
|
|
734
|
+
async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256, candidateCount, networkAttempted, reuseScope) {
|
|
523
735
|
await appendJournalEvent(workspacePaths(root).journal, "capability.fetch.completed", projectId, {
|
|
524
736
|
attemptId: receipt.attemptId,
|
|
525
737
|
projectId: receipt.projectId,
|
|
@@ -540,12 +752,43 @@ async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256)
|
|
|
540
752
|
contextTotalItems: receipt.contextTotalItems ?? null,
|
|
541
753
|
contextNextOffset: receipt.contextNextOffset ?? null,
|
|
542
754
|
contextTruncated: receipt.contextTruncated,
|
|
755
|
+
redactions: receipt.redactions,
|
|
756
|
+
candidateCount,
|
|
757
|
+
networkAttempted,
|
|
758
|
+
reuseScope,
|
|
543
759
|
retrievedAt: receipt.retrievedAt,
|
|
544
760
|
servedAt: receipt.servedAt,
|
|
545
761
|
cacheHit: receipt.cacheHit,
|
|
546
762
|
cacheKeySha256,
|
|
547
763
|
});
|
|
548
764
|
}
|
|
765
|
+
async function findProjectRequestReuse(root, projectId, cacheKeySha256, credentialId) {
|
|
766
|
+
const events = await readJournal(workspacePaths(root).journal);
|
|
767
|
+
const completed = [...events]
|
|
768
|
+
.reverse()
|
|
769
|
+
.find((event) => event.scope === projectId &&
|
|
770
|
+
event.type === "capability.fetch.completed" &&
|
|
771
|
+
event.payload.cacheKeySha256 === cacheKeySha256 &&
|
|
772
|
+
event.payload.credentialId === credentialId &&
|
|
773
|
+
typeof event.payload.attemptId === "string");
|
|
774
|
+
if (!completed)
|
|
775
|
+
return null;
|
|
776
|
+
const receipts = await loadProjectEvidenceReceipts(root, projectId);
|
|
777
|
+
return receipts.find((receipt) => receipt.attemptId === completed.payload.attemptId) ?? null;
|
|
778
|
+
}
|
|
779
|
+
function brokerCallLimitError(maxCalls, startedCalls) {
|
|
780
|
+
return new CliError("The derived broker call budget is exhausted.", {
|
|
781
|
+
code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
|
|
782
|
+
exitCode: 3,
|
|
783
|
+
details: {
|
|
784
|
+
executionMode: "broker",
|
|
785
|
+
networkAttempted: false,
|
|
786
|
+
maxCalls,
|
|
787
|
+
startedCalls,
|
|
788
|
+
minimumAction: "Assess the registered candidates and coverage gaps; increase the reviewed hard ceiling only if a justified gap-fill batch remains.",
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
}
|
|
549
792
|
async function fetchWithRedirectPolicy(initialUrl, headers, capabilityHosts, credentialHosts, method, body, endpoint) {
|
|
550
793
|
let current = initialUrl;
|
|
551
794
|
for (let redirects = 0; redirects <= 5; redirects += 1) {
|
|
@@ -655,6 +898,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
655
898
|
totalItems: null,
|
|
656
899
|
nextOffset: null,
|
|
657
900
|
truncated: boundedBytes.byteLength < bytes.byteLength,
|
|
901
|
+
selectedJsonPointer: null,
|
|
658
902
|
};
|
|
659
903
|
}
|
|
660
904
|
let parsed;
|
|
@@ -664,7 +908,13 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
664
908
|
catch {
|
|
665
909
|
throw new Error("JSON response body is not valid JSON");
|
|
666
910
|
}
|
|
667
|
-
const
|
|
911
|
+
const automaticSelection = jsonPointer === undefined ? defaultCandidateCollection(parsed) : null;
|
|
912
|
+
const selectedJsonPointer = jsonPointer ?? automaticSelection?.jsonPointer ?? null;
|
|
913
|
+
const selected = automaticSelection
|
|
914
|
+
? automaticSelection.value
|
|
915
|
+
: jsonPointer === undefined
|
|
916
|
+
? parsed
|
|
917
|
+
: resolveJsonPointer(parsed, jsonPointer);
|
|
668
918
|
if (Array.isArray(selected)) {
|
|
669
919
|
const limited = [];
|
|
670
920
|
for (const item of selected.slice(itemOffset, itemOffset + maxItems)) {
|
|
@@ -681,6 +931,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
681
931
|
totalItems: selected.length,
|
|
682
932
|
nextOffset,
|
|
683
933
|
truncated: itemOffset > 0 || nextOffset !== null,
|
|
934
|
+
selectedJsonPointer,
|
|
684
935
|
};
|
|
685
936
|
}
|
|
686
937
|
if (isObject(selected)) {
|
|
@@ -700,6 +951,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
700
951
|
totalItems: entries.length,
|
|
701
952
|
nextOffset,
|
|
702
953
|
truncated: itemOffset > 0 || nextOffset !== null,
|
|
954
|
+
selectedJsonPointer,
|
|
703
955
|
};
|
|
704
956
|
}
|
|
705
957
|
if (itemOffset !== 0)
|
|
@@ -713,6 +965,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
713
965
|
totalItems: 1,
|
|
714
966
|
nextOffset: null,
|
|
715
967
|
truncated: false,
|
|
968
|
+
selectedJsonPointer,
|
|
716
969
|
};
|
|
717
970
|
}
|
|
718
971
|
if (typeof selected !== "string") {
|
|
@@ -737,8 +990,24 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
737
990
|
totalItems: 1,
|
|
738
991
|
nextOffset: null,
|
|
739
992
|
truncated: true,
|
|
993
|
+
selectedJsonPointer,
|
|
740
994
|
};
|
|
741
995
|
}
|
|
996
|
+
function defaultCandidateCollection(value) {
|
|
997
|
+
if (!isObject(value))
|
|
998
|
+
return null;
|
|
999
|
+
for (const [jsonPointer, candidate] of [
|
|
1000
|
+
["/web/results", isObject(value.web) ? value.web.results : undefined],
|
|
1001
|
+
["/results", value.results],
|
|
1002
|
+
["/records", value.records],
|
|
1003
|
+
["/items", value.items],
|
|
1004
|
+
["/data", value.data],
|
|
1005
|
+
]) {
|
|
1006
|
+
if (Array.isArray(candidate))
|
|
1007
|
+
return { value: candidate, jsonPointer };
|
|
1008
|
+
}
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
742
1011
|
function jsonBytes(value) {
|
|
743
1012
|
return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
|
|
744
1013
|
}
|
|
@@ -844,7 +1113,13 @@ function brokerFailureKind(error) {
|
|
|
844
1113
|
}
|
|
845
1114
|
function validateHttpsUrl(value) {
|
|
846
1115
|
const url = new URL(value);
|
|
847
|
-
|
|
1116
|
+
const sensitive = /^(access_token|api[_-]?key|apikey|auth|authorization|cookie|key|password|secret|session|sig|signature|token)$/i;
|
|
1117
|
+
if (url.protocol !== "https:" ||
|
|
1118
|
+
url.username ||
|
|
1119
|
+
url.password ||
|
|
1120
|
+
url.hash ||
|
|
1121
|
+
!url.hostname ||
|
|
1122
|
+
[...url.searchParams.keys()].some((key) => sensitive.test(key))) {
|
|
848
1123
|
throw new Error("broker URL must be credential-free HTTPS");
|
|
849
1124
|
}
|
|
850
1125
|
return url;
|