@tiangong-ai/cli 0.0.32 → 0.0.34
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 +7 -2
- package/README.md +72 -19
- package/dist/research/commands.js +58 -1
- package/dist/research/commands.js.map +1 -1
- package/dist/research/orchestration.js +388 -31
- 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.js +265 -56
- package/dist/research/workspace/broker.js.map +1 -1
- package/dist/research/workspace/constants.d.ts +1 -0
- package/dist/research/workspace/constants.js +4 -2
- package/dist/research/workspace/constants.js.map +1 -1
- package/dist/research/workspace/context.js +106 -11
- package/dist/research/workspace/context.js.map +1 -1
- 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 +5 -0
- package/dist/research/workspace/preflight.js +37 -17
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/projects.d.ts +3 -1
- package/dist/research/workspace/projects.js +346 -5
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +106 -4
- package/dist/research/workspace/runtime.js +947 -80
- 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 +5 -5
- package/dist/research/workspace/setup-invocation.d.ts +11 -0
- package/dist/research/workspace/setup-invocation.js +34 -0
- package/dist/research/workspace/setup-invocation.js.map +1 -0
- package/dist/research/workspace/setup-wizard.js +10 -4
- package/dist/research/workspace/setup-wizard.js.map +1 -1
- package/dist/research/workspace/setup.d.ts +55 -7
- package/dist/research/workspace/setup.js +603 -32
- package/dist/research/workspace/setup.js.map +1 -1
- package/dist/research/workspace/types.d.ts +52 -3
- package/dist/research/workspace/workspace.js +45 -6
- package/dist/research/workspace/workspace.js.map +1 -1
- package/package.json +4 -2
|
@@ -1,15 +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";
|
|
8
|
+
import { loadBrokerEvidenceCache, loadProjectEvidenceReceipts, persistBrokerEvidence, storeBrokerEvidenceCache, } from "./evidence.js";
|
|
9
|
+
import { registerBrokerCandidates } from "./evidence-ledger.js";
|
|
10
|
+
import { deriveDiscoveryPlan } from "./discovery-planning.js";
|
|
9
11
|
import { appendJournalEvent, readJournal } from "./journal.js";
|
|
10
12
|
import { loadProject } from "./projects.js";
|
|
11
13
|
import { sanitizeResearchRecord, sanitizeResearchText } from "./sanitization.js";
|
|
12
|
-
import { canonicalJson, ensureDirectory, isObject, resolveContained, sha256Text, workspacePaths, } from "./storage.js";
|
|
14
|
+
import { canonicalJson, ensureDirectory, isObject, pathExists, resolveContained, sha256Text, workspacePaths, } from "./storage.js";
|
|
13
15
|
import { loadWorkspaceConfig } from "./workspace.js";
|
|
14
16
|
const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
15
17
|
const MAX_ERROR_RESPONSE_BYTES = 16 * 1024;
|
|
@@ -37,20 +39,8 @@ export async function fetchNativeCandidateSource(input) {
|
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
const config = await loadWorkspaceConfig(input.root);
|
|
40
|
-
const priorCalls = (await readJournal(workspacePaths(input.root).journal)).filter((event) => event.scope === input.projectId && event.type === "capability.fetch.
|
|
41
|
-
|
|
42
|
-
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.rejected", input.projectId, {
|
|
43
|
-
projectId: input.projectId,
|
|
44
|
-
code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
|
|
45
|
-
maxCalls: config.budget.maxBrokerCalls,
|
|
46
|
-
startedCalls: priorCalls,
|
|
47
|
-
});
|
|
48
|
-
throw new CliError("The bounded broker call budget is exhausted.", {
|
|
49
|
-
code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
|
|
50
|
-
exitCode: 3,
|
|
51
|
-
details: { maxCalls: config.budget.maxBrokerCalls, startedCalls: priorCalls },
|
|
52
|
-
});
|
|
53
|
-
}
|
|
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));
|
|
54
44
|
const credentialMap = await loadCapabilityCredentialMap(input.root, declarations.capabilities);
|
|
55
45
|
const receipt = await fetchCandidateSource({
|
|
56
46
|
root: input.root,
|
|
@@ -62,13 +52,19 @@ export async function fetchNativeCandidateSource(input) {
|
|
|
62
52
|
workspaceContextTokens: config.budget.maxBrokerContextTokens,
|
|
63
53
|
workspaceMaxItems: config.budget.maxBrokerItems,
|
|
64
54
|
arguments: input.request,
|
|
55
|
+
onBrokerRequest: () => {
|
|
56
|
+
if (priorCalls >= discoveryPlan.maxCalls)
|
|
57
|
+
throw brokerCallLimitError(discoveryPlan.maxCalls, priorCalls);
|
|
58
|
+
},
|
|
65
59
|
});
|
|
60
|
+
const startedCalls = (await readJournal(workspacePaths(input.root).journal)).filter((event) => event.scope === input.projectId && event.type === "capability.fetch.requested").length;
|
|
66
61
|
return {
|
|
67
62
|
...receipt,
|
|
68
63
|
brokerBudget: {
|
|
69
|
-
maxCalls:
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
maxCalls: discoveryPlan.maxCalls,
|
|
65
|
+
hardCallLimit: discoveryPlan.hardCallLimit,
|
|
66
|
+
startedCalls,
|
|
67
|
+
remainingCalls: Math.max(0, discoveryPlan.maxCalls - startedCalls),
|
|
72
68
|
},
|
|
73
69
|
};
|
|
74
70
|
}
|
|
@@ -87,9 +83,15 @@ export async function startCapabilityBroker(root, projectId, capsuleProject) {
|
|
|
87
83
|
}
|
|
88
84
|
const credentialMap = await loadCapabilityCredentialMap(root, declarations.capabilities);
|
|
89
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;
|
|
90
92
|
const callBudget = {
|
|
91
|
-
maxCalls
|
|
92
|
-
startedCalls:
|
|
93
|
+
maxCalls,
|
|
94
|
+
startedCalls: priorCalls,
|
|
93
95
|
};
|
|
94
96
|
const routeToken = randomUUID().replaceAll("-", "");
|
|
95
97
|
const route = `/mcp/${routeToken}`;
|
|
@@ -196,21 +198,6 @@ async function handleMcpRequest(input) {
|
|
|
196
198
|
sendToolError(input.response, body, "Unsupported tool call.");
|
|
197
199
|
return;
|
|
198
200
|
}
|
|
199
|
-
if (input.callBudget.startedCalls >= input.callBudget.maxCalls) {
|
|
200
|
-
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.rejected", input.projectId, {
|
|
201
|
-
projectId: input.projectId,
|
|
202
|
-
code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
|
|
203
|
-
maxCalls: input.callBudget.maxCalls,
|
|
204
|
-
startedCalls: input.callBudget.startedCalls,
|
|
205
|
-
});
|
|
206
|
-
sendToolError(input.response, body, JSON.stringify({
|
|
207
|
-
code: "RESEARCH_BROKER_CALL_LIMIT_EXCEEDED",
|
|
208
|
-
message: "The bounded broker call budget is exhausted; produce the best supported result from admitted receipts.",
|
|
209
|
-
maxCalls: input.callBudget.maxCalls,
|
|
210
|
-
}));
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
input.callBudget.startedCalls += 1;
|
|
214
201
|
try {
|
|
215
202
|
const receipt = await fetchCandidateSource({
|
|
216
203
|
root: input.root,
|
|
@@ -222,6 +209,12 @@ async function handleMcpRequest(input) {
|
|
|
222
209
|
workspaceContextTokens: input.workspaceContextTokens,
|
|
223
210
|
workspaceMaxItems: input.workspaceMaxItems,
|
|
224
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
|
+
},
|
|
225
218
|
});
|
|
226
219
|
sendRpcResult(input.response, body, {
|
|
227
220
|
content: [
|
|
@@ -240,6 +233,14 @@ async function handleMcpRequest(input) {
|
|
|
240
233
|
});
|
|
241
234
|
}
|
|
242
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
|
+
}
|
|
243
244
|
const detail = error instanceof CliError
|
|
244
245
|
? JSON.stringify({ code: error.code, message: error.message, details: error.details })
|
|
245
246
|
: error instanceof Error
|
|
@@ -344,6 +345,64 @@ async function fetchCandidateSource(input) {
|
|
|
344
345
|
requestBodySha256: encodedRequestBody ? sha256Text(encodedRequestBody) : null,
|
|
345
346
|
accept: capability.http.accept,
|
|
346
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
|
+
}
|
|
347
406
|
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.attempted", input.projectId, {
|
|
348
407
|
attemptId,
|
|
349
408
|
projectId: input.projectId,
|
|
@@ -374,14 +433,23 @@ async function fetchCandidateSource(input) {
|
|
|
374
433
|
contextTotalItems: context.totalItems,
|
|
375
434
|
contextNextOffset: context.nextOffset,
|
|
376
435
|
contextTruncated: context.truncated,
|
|
436
|
+
redactions: cached.redactions,
|
|
377
437
|
retrievedAt: cached.retrievedAt,
|
|
378
438
|
cacheHit: true,
|
|
379
439
|
}, raw, context.bytes);
|
|
380
440
|
if (input.capsuleProject) {
|
|
381
441
|
await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
|
|
382
442
|
}
|
|
383
|
-
|
|
384
|
-
|
|
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");
|
|
385
453
|
}
|
|
386
454
|
}
|
|
387
455
|
const headers = new Headers(capability.http.staticHeaders);
|
|
@@ -471,8 +539,8 @@ async function fetchCandidateSource(input) {
|
|
|
471
539
|
details: { contentType, allowedContentTypes: capability.http.allowedContentTypes },
|
|
472
540
|
});
|
|
473
541
|
}
|
|
474
|
-
|
|
475
|
-
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);
|
|
476
544
|
const receipt = await persistBrokerEvidence(input.root, {
|
|
477
545
|
attemptId,
|
|
478
546
|
projectId: input.projectId,
|
|
@@ -486,35 +554,67 @@ async function fetchCandidateSource(input) {
|
|
|
486
554
|
contextTotalItems: context.totalItems,
|
|
487
555
|
contextNextOffset: context.nextOffset,
|
|
488
556
|
contextTruncated: context.truncated,
|
|
557
|
+
redactions: persistable.redactions,
|
|
489
558
|
retrievedAt: new Date().toISOString(),
|
|
490
559
|
cacheHit: false,
|
|
491
|
-
}, bytes, context.bytes);
|
|
560
|
+
}, persistable.bytes, context.bytes);
|
|
492
561
|
if (input.capsuleProject) {
|
|
493
562
|
await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
|
|
494
563
|
}
|
|
495
564
|
if (!credential)
|
|
496
565
|
await storeBrokerEvidenceCache(input.root, cacheKeySha256, receipt);
|
|
497
|
-
|
|
498
|
-
|
|
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);
|
|
499
576
|
}
|
|
500
577
|
catch (error) {
|
|
501
|
-
const
|
|
502
|
-
|
|
578
|
+
const reportedError = structuredBrokerFailure(error, capabilityId, effectiveCredentialId);
|
|
579
|
+
const safeDetails = reportedError instanceof CliError && isObject(reportedError.details)
|
|
580
|
+
? sanitizeResearchRecord(reportedError.details, [...input.credentialMap.values()])
|
|
503
581
|
: {};
|
|
504
582
|
await appendJournalEvent(workspacePaths(input.root).journal, "capability.fetch.failed", input.projectId, {
|
|
505
583
|
attemptId,
|
|
506
584
|
capabilityId,
|
|
507
585
|
credentialId: effectiveCredentialId,
|
|
508
|
-
error: bounded(sanitizeResearchText(
|
|
509
|
-
|
|
510
|
-
]), 500),
|
|
511
|
-
failureKind: brokerFailureKind(error),
|
|
586
|
+
error: bounded(sanitizeResearchText(reportedError.message, [...input.credentialMap.values()]), 500),
|
|
587
|
+
failureKind: brokerFailureKind(reportedError),
|
|
512
588
|
...safeDetails,
|
|
513
589
|
});
|
|
514
|
-
throw
|
|
590
|
+
throw reportedError;
|
|
515
591
|
}
|
|
516
592
|
}
|
|
517
|
-
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) {
|
|
518
618
|
const inline = contentType.includes("json") || contentType.startsWith("text/");
|
|
519
619
|
return {
|
|
520
620
|
...Object.fromEntries(Object.entries(receipt)),
|
|
@@ -522,8 +622,58 @@ function brokerToolResult(receipt, context, contentType) {
|
|
|
522
622
|
encoding: inline ? "utf8" : "not-inlined",
|
|
523
623
|
text: inline ? context.toString("utf8") : null,
|
|
524
624
|
},
|
|
625
|
+
candidates,
|
|
626
|
+
networkAttempted: reuseScope === null,
|
|
627
|
+
reuseScope,
|
|
525
628
|
};
|
|
526
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
|
+
}
|
|
527
677
|
function assertNoSensitiveResponseMaterial(bytes, contentType) {
|
|
528
678
|
if (!contentType.includes("json") && !contentType.startsWith("text/"))
|
|
529
679
|
return;
|
|
@@ -562,8 +712,10 @@ function containsSensitiveJsonField(value) {
|
|
|
562
712
|
return value.some(containsSensitiveJsonField);
|
|
563
713
|
if (!isObject(value))
|
|
564
714
|
return false;
|
|
565
|
-
|
|
566
|
-
|
|
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);
|
|
567
719
|
}
|
|
568
720
|
async function stageContextObject(capsuleProject, locator, bytes) {
|
|
569
721
|
const destination = resolveContained(capsuleProject, locator);
|
|
@@ -579,7 +731,7 @@ async function stageContextObject(capsuleProject, locator, bytes) {
|
|
|
579
731
|
}
|
|
580
732
|
}
|
|
581
733
|
}
|
|
582
|
-
async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256) {
|
|
734
|
+
async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256, candidateCount, networkAttempted, reuseScope) {
|
|
583
735
|
await appendJournalEvent(workspacePaths(root).journal, "capability.fetch.completed", projectId, {
|
|
584
736
|
attemptId: receipt.attemptId,
|
|
585
737
|
projectId: receipt.projectId,
|
|
@@ -600,12 +752,43 @@ async function appendCompletedReceipt(root, projectId, receipt, cacheKeySha256)
|
|
|
600
752
|
contextTotalItems: receipt.contextTotalItems ?? null,
|
|
601
753
|
contextNextOffset: receipt.contextNextOffset ?? null,
|
|
602
754
|
contextTruncated: receipt.contextTruncated,
|
|
755
|
+
redactions: receipt.redactions,
|
|
756
|
+
candidateCount,
|
|
757
|
+
networkAttempted,
|
|
758
|
+
reuseScope,
|
|
603
759
|
retrievedAt: receipt.retrievedAt,
|
|
604
760
|
servedAt: receipt.servedAt,
|
|
605
761
|
cacheHit: receipt.cacheHit,
|
|
606
762
|
cacheKeySha256,
|
|
607
763
|
});
|
|
608
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
|
+
}
|
|
609
792
|
async function fetchWithRedirectPolicy(initialUrl, headers, capabilityHosts, credentialHosts, method, body, endpoint) {
|
|
610
793
|
let current = initialUrl;
|
|
611
794
|
for (let redirects = 0; redirects <= 5; redirects += 1) {
|
|
@@ -715,6 +898,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
715
898
|
totalItems: null,
|
|
716
899
|
nextOffset: null,
|
|
717
900
|
truncated: boundedBytes.byteLength < bytes.byteLength,
|
|
901
|
+
selectedJsonPointer: null,
|
|
718
902
|
};
|
|
719
903
|
}
|
|
720
904
|
let parsed;
|
|
@@ -724,7 +908,13 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
724
908
|
catch {
|
|
725
909
|
throw new Error("JSON response body is not valid JSON");
|
|
726
910
|
}
|
|
727
|
-
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);
|
|
728
918
|
if (Array.isArray(selected)) {
|
|
729
919
|
const limited = [];
|
|
730
920
|
for (const item of selected.slice(itemOffset, itemOffset + maxItems)) {
|
|
@@ -741,6 +931,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
741
931
|
totalItems: selected.length,
|
|
742
932
|
nextOffset,
|
|
743
933
|
truncated: itemOffset > 0 || nextOffset !== null,
|
|
934
|
+
selectedJsonPointer,
|
|
744
935
|
};
|
|
745
936
|
}
|
|
746
937
|
if (isObject(selected)) {
|
|
@@ -760,6 +951,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
760
951
|
totalItems: entries.length,
|
|
761
952
|
nextOffset,
|
|
762
953
|
truncated: itemOffset > 0 || nextOffset !== null,
|
|
954
|
+
selectedJsonPointer,
|
|
763
955
|
};
|
|
764
956
|
}
|
|
765
957
|
if (itemOffset !== 0)
|
|
@@ -773,6 +965,7 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
773
965
|
totalItems: 1,
|
|
774
966
|
nextOffset: null,
|
|
775
967
|
truncated: false,
|
|
968
|
+
selectedJsonPointer,
|
|
776
969
|
};
|
|
777
970
|
}
|
|
778
971
|
if (typeof selected !== "string") {
|
|
@@ -797,8 +990,24 @@ function buildContextView(bytes, contentType, jsonPointer, itemOffset, maxItems,
|
|
|
797
990
|
totalItems: 1,
|
|
798
991
|
nextOffset: null,
|
|
799
992
|
truncated: true,
|
|
993
|
+
selectedJsonPointer,
|
|
800
994
|
};
|
|
801
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
|
+
}
|
|
802
1011
|
function jsonBytes(value) {
|
|
803
1012
|
return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
|
|
804
1013
|
}
|