@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.
Files changed (64) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +140 -98
  3. package/dist/research/orchestration.js +519 -21
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/workspace/acquisition.d.ts +71 -0
  6. package/dist/research/workspace/acquisition.js +593 -0
  7. package/dist/research/workspace/acquisition.js.map +1 -0
  8. package/dist/research/workspace/artifacts.d.ts +43 -0
  9. package/dist/research/workspace/artifacts.js +544 -0
  10. package/dist/research/workspace/artifacts.js.map +1 -0
  11. package/dist/research/workspace/broker.d.ts +5 -0
  12. package/dist/research/workspace/broker.js +318 -43
  13. package/dist/research/workspace/broker.js.map +1 -1
  14. package/dist/research/workspace/companion-readiness.d.ts +11 -0
  15. package/dist/research/workspace/companion-readiness.js +75 -0
  16. package/dist/research/workspace/companion-readiness.js.map +1 -0
  17. package/dist/research/workspace/discovery-planning.d.ts +25 -0
  18. package/dist/research/workspace/discovery-planning.js +106 -0
  19. package/dist/research/workspace/discovery-planning.js.map +1 -0
  20. package/dist/research/workspace/discovery-status.d.ts +46 -0
  21. package/dist/research/workspace/discovery-status.js +183 -0
  22. package/dist/research/workspace/discovery-status.js.map +1 -0
  23. package/dist/research/workspace/discovery.d.ts +25 -0
  24. package/dist/research/workspace/discovery.js +268 -0
  25. package/dist/research/workspace/discovery.js.map +1 -0
  26. package/dist/research/workspace/downloads.d.ts +76 -0
  27. package/dist/research/workspace/downloads.js +274 -0
  28. package/dist/research/workspace/downloads.js.map +1 -0
  29. package/dist/research/workspace/evidence-ledger.d.ts +52 -0
  30. package/dist/research/workspace/evidence-ledger.js +487 -0
  31. package/dist/research/workspace/evidence-ledger.js.map +1 -0
  32. package/dist/research/workspace/evidence.d.ts +1 -0
  33. package/dist/research/workspace/evidence.js +2 -0
  34. package/dist/research/workspace/evidence.js.map +1 -1
  35. package/dist/research/workspace/input-plan.js +4 -0
  36. package/dist/research/workspace/input-plan.js.map +1 -1
  37. package/dist/research/workspace/native-activity.d.ts +65 -0
  38. package/dist/research/workspace/native-activity.js +153 -0
  39. package/dist/research/workspace/native-activity.js.map +1 -0
  40. package/dist/research/workspace/preflight.d.ts +18 -0
  41. package/dist/research/workspace/preflight.js +47 -20
  42. package/dist/research/workspace/preflight.js.map +1 -1
  43. package/dist/research/workspace/projects.d.ts +3 -1
  44. package/dist/research/workspace/projects.js +357 -5
  45. package/dist/research/workspace/projects.js.map +1 -1
  46. package/dist/research/workspace/runtime.d.ts +171 -3
  47. package/dist/research/workspace/runtime.js +1582 -77
  48. package/dist/research/workspace/runtime.js.map +1 -1
  49. package/dist/research/workspace/sanitization.js +28 -6
  50. package/dist/research/workspace/sanitization.js.map +1 -1
  51. package/dist/research/workspace/schemas.d.ts +3 -0
  52. package/dist/research/workspace/schemas.js +206 -33
  53. package/dist/research/workspace/schemas.js.map +1 -1
  54. package/dist/research/workspace/setup-catalog.js +2 -2
  55. package/dist/research/workspace/setup-wizard.js +45 -17
  56. package/dist/research/workspace/setup-wizard.js.map +1 -1
  57. package/dist/research/workspace/setup.d.ts +27 -5
  58. package/dist/research/workspace/setup.js +400 -85
  59. package/dist/research/workspace/setup.js.map +1 -1
  60. package/dist/research/workspace/types.d.ts +38 -3
  61. package/dist/research/workspace/workspace.d.ts +2 -0
  62. package/dist/research/workspace/workspace.js +128 -26
  63. package/dist/research/workspace/workspace.js.map +1 -1
  64. 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 { appendJournalEvent } from "./journal.js";
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: config.budget.maxBrokerCalls,
36
- startedCalls: 0,
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
- await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
325
- await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256);
326
- return brokerToolResult(receipt, context.bytes, cached.contentType);
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
- assertNoSensitiveResponseMaterial(bytes, contentType);
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
- await stageContextObject(input.capsuleProject, receipt.contextLocator, context.bytes);
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
- await appendCompletedReceipt(input.root, input.projectId, receipt, cacheKeySha256);
438
- return brokerToolResult(receipt, context.bytes, contentType);
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 safeDetails = error instanceof CliError && isObject(error.details)
442
- ? sanitizeResearchRecord(error.details, [...input.credentialMap.values()])
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(error instanceof Error ? error.message : String(error), [
449
- ...input.credentialMap.values(),
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 error;
590
+ throw reportedError;
455
591
  }
456
592
  }
457
- function brokerToolResult(receipt, context, contentType) {
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
- const sensitive = /^(access_token|api[_-]?key|apikey|authorization|cookie|password|secret|session|token)$/i;
506
- return Object.entries(value).some(([key, item]) => (sensitive.test(key) && item !== null && item !== "") || containsSensitiveJsonField(item));
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 selected = jsonPointer === undefined ? parsed : resolveJsonPointer(parsed, jsonPointer);
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
- if (url.protocol !== "https:" || url.username || url.password || !url.hostname) {
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;