@tiangong-ai/cli 0.0.27 → 0.0.29

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 (41) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +114 -48
  3. package/dist/research/orchestration.js +38 -0
  4. package/dist/research/orchestration.js.map +1 -1
  5. package/dist/research/setup-command.js +45 -17
  6. package/dist/research/setup-command.js.map +1 -1
  7. package/dist/research/workspace/broker.d.ts +5 -0
  8. package/dist/research/workspace/broker.js +98 -9
  9. package/dist/research/workspace/broker.js.map +1 -1
  10. package/dist/research/workspace/capabilities.js +32 -11
  11. package/dist/research/workspace/capabilities.js.map +1 -1
  12. package/dist/research/workspace/credentials.d.ts +21 -1
  13. package/dist/research/workspace/credentials.js +45 -10
  14. package/dist/research/workspace/credentials.js.map +1 -1
  15. package/dist/research/workspace/executor.js +43 -3
  16. package/dist/research/workspace/executor.js.map +1 -1
  17. package/dist/research/workspace/external-skills.d.ts +31 -0
  18. package/dist/research/workspace/external-skills.js +161 -13
  19. package/dist/research/workspace/external-skills.js.map +1 -1
  20. package/dist/research/workspace/preflight.d.ts +34 -2
  21. package/dist/research/workspace/preflight.js +76 -21
  22. package/dist/research/workspace/preflight.js.map +1 -1
  23. package/dist/research/workspace/runtime.js +135 -50
  24. package/dist/research/workspace/runtime.js.map +1 -1
  25. package/dist/research/workspace/schemas.d.ts +4 -0
  26. package/dist/research/workspace/schemas.js +29 -3
  27. package/dist/research/workspace/schemas.js.map +1 -1
  28. package/dist/research/workspace/setup-catalog.d.ts +3 -2
  29. package/dist/research/workspace/setup-catalog.js +22 -1
  30. package/dist/research/workspace/setup-catalog.js.map +1 -1
  31. package/dist/research/workspace/setup-wizard.d.ts +6 -0
  32. package/dist/research/workspace/setup-wizard.js +379 -132
  33. package/dist/research/workspace/setup-wizard.js.map +1 -1
  34. package/dist/research/workspace/setup.d.ts +17 -0
  35. package/dist/research/workspace/setup.js +190 -95
  36. package/dist/research/workspace/setup.js.map +1 -1
  37. package/dist/research/workspace/types.d.ts +2 -0
  38. package/dist/research/workspace/workspace.d.ts +2 -1
  39. package/dist/research/workspace/workspace.js +62 -13
  40. package/dist/research/workspace/workspace.js.map +1 -1
  41. package/package.json +1 -1
@@ -7,8 +7,8 @@ import { setTimeout as sleep } from "node:timers/promises";
7
7
  import { CliError } from "../../errors.js";
8
8
  import { loadCapabilityDeclarations } from "./capabilities.js";
9
9
  import { inspectResearchContext } from "./context.js";
10
- import { inspectCapabilityCredentialEnvironment, setCapabilityCredentialFromEnvironment, } from "./credentials.js";
11
- import { configureExternalSkillProfile, configureTiangongSciCapability, doctorExternalCapabilities, EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, } from "./external-skills.js";
10
+ import { inspectCapabilityCredentialEnvironment, loadCapabilityCredentialMapForIds, reconcileCapabilityCredentialEnvironment, setCapabilityCredentialValue, } from "./credentials.js";
11
+ import { configureExternalSkillProfile, configureTiangongSciCapability, doctorExternalCapabilities, EXTERNAL_SKILL_CONTEXT_PROFILE, EXTERNAL_SKILL_MEDIA_PROFILE, EXTERNAL_SKILL_PROFILE, reconcileSetupManagedCapabilities, } from "./external-skills.js";
12
12
  import { appendJournalEvent } from "./journal.js";
13
13
  import { configuredResearchSecrets, isSensitiveEnvironmentName, sanitizeResearchRecord, sanitizeResearchText, } from "./sanitization.js";
14
14
  import { inspectResearchSetupCatalog, RESEARCH_SETUP_CREDENTIALS, RESEARCH_SETUP_INSTALLER, RESEARCH_SETUP_SETTINGS, RESEARCH_SETUP_SKILLS, resolveSetupSkills, setupSkill, setupSource, setupTargetRoot, } from "./setup-catalog.js";
@@ -162,7 +162,7 @@ export async function createResearchSetupPlan(input) {
162
162
  globalMutation: scope === "global",
163
163
  agentSmokeCost: input.agentSmoke === true,
164
164
  },
165
- mutations: setupMutations(root, targets, selected, credentialSources),
165
+ mutations: setupMutations(root, targets, selected),
166
166
  };
167
167
  const plan = {
168
168
  ...unsigned,
@@ -312,6 +312,12 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
312
312
  state = await startSetupStep(root, state, "credential-preflight");
313
313
  await assertRequiredCredentialPreflight(plan, environment);
314
314
  state = await completeSetupStep(root, state, "credential-preflight");
315
+ // Persist an explicitly supplied credential before any installer or source
316
+ // download. A later installation failure can then resume from the immutable
317
+ // plan without asking the operator to expose the value again.
318
+ state = await startSetupStep(root, state, "credentials");
319
+ await configurePlanCredentials(plan, environment);
320
+ state = await completeSetupStep(root, state, "credentials");
315
321
  state = await startSetupStep(root, state, "installation-preflight");
316
322
  const selected = plan.selection.skillIds.map(setupSkill);
317
323
  const installInspection = await inspectSelectedInstallations(plan, selected, environment);
@@ -381,6 +387,7 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
381
387
  }
382
388
  state = await startSetupStep(root, state, "capability-configuration");
383
389
  await configureSelectedCapabilities(plan, environment);
390
+ await reconcilePlanCredentialStores(plan);
384
391
  state = await completeSetupStep(root, state, "capability-configuration");
385
392
  state = await startSetupStep(root, state, "settings");
386
393
  await writeJsonAtomic(paths.setupConfig, {
@@ -391,9 +398,6 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
391
398
  updatedAt: new Date().toISOString(),
392
399
  });
393
400
  state = await completeSetupStep(root, state, "settings");
394
- state = await startSetupStep(root, state, "credentials");
395
- await configurePlanCredentials(plan, environment);
396
- state = await completeSetupStep(root, state, "credentials");
397
401
  await appendJournalEvent(paths.journal, "research.setup.applied", "workspace", {
398
402
  planSha256: plan.planSha256,
399
403
  selectedSkillIds: plan.selection.skillIds,
@@ -425,6 +429,7 @@ export async function applyResearchSetupPlan(planPath, options = {}) {
425
429
  runner,
426
430
  ...(options.fetcher === undefined ? {} : { fetcher: options.fetcher }),
427
431
  ...(options.sleeper === undefined ? {} : { sleeper: options.sleeper }),
432
+ ...(options.executor === undefined ? {} : { executor: options.executor }),
428
433
  });
429
434
  state = await updateSetupState(root, {
430
435
  ...state,
@@ -498,19 +503,7 @@ export async function inspectResearchSetupStatus(workspace, environment = proces
498
503
  }
499
504
  export async function setResearchSetupCredentialFromEnvironment(input) {
500
505
  const root = requireAbsoluteWorkspace(input.workspace);
501
- const plan = await loadAndVerifyResearchSetupPlan(workspacePaths(root).setupPlan);
502
- const selected = selectedCredentialDefinitions(plan);
503
- const credential = selected.find((candidate) => candidate.id === input.credentialId);
504
- if (!credential) {
505
- throw setupError({
506
- code: "RESEARCH_SETUP_CREDENTIAL_INVALID",
507
- step: "credentials",
508
- reason: `Credential is not declared by the selected setup plan: ${input.credentialId}.`,
509
- minimumAction: "Inspect the setup catalog and selected plan credential IDs.",
510
- retryCommand: `tiangong-ai research setup status --workspace ${root} --json`,
511
- exitCode: 2,
512
- });
513
- }
506
+ const { credential } = await selectedSetupCredential(root, input.credentialId);
514
507
  assertEnvironmentName(input.environmentName);
515
508
  const value = input.environment[input.environmentName];
516
509
  if (typeof value !== "string" || Buffer.byteLength(value, "utf8") < credential.minimumUtf8Bytes) {
@@ -523,27 +516,85 @@ export async function setResearchSetupCredentialFromEnvironment(input) {
523
516
  exitCode: 3,
524
517
  });
525
518
  }
519
+ return persistResearchSetupCredential({
520
+ root,
521
+ credentialId: input.credentialId,
522
+ value,
523
+ inputMethod: "environment",
524
+ sourceEnvironmentName: input.environmentName,
525
+ });
526
+ }
527
+ export async function setResearchSetupCredentialValue(input) {
528
+ const root = requireAbsoluteWorkspace(input.workspace);
529
+ const { credential } = await selectedSetupCredential(root, input.credentialId);
530
+ if (Buffer.byteLength(input.value, "utf8") < credential.minimumUtf8Bytes) {
531
+ throw setupError({
532
+ code: "RESEARCH_SETUP_CREDENTIAL_INVALID",
533
+ step: "credentials",
534
+ reason: "Credential value is missing or does not meet the selected provider minimum.",
535
+ minimumAction: "Retry with secure input, stdin, or a configured owner environment variable.",
536
+ retryCommand: `tiangong-ai research setup credential set --id ${credential.id} --prompt --workspace ${root} --json`,
537
+ exitCode: 3,
538
+ });
539
+ }
540
+ return persistResearchSetupCredential({
541
+ root,
542
+ credentialId: input.credentialId,
543
+ value: input.value,
544
+ inputMethod: input.inputMethod,
545
+ });
546
+ }
547
+ async function selectedSetupCredential(root, credentialId) {
548
+ const plan = await loadAndVerifyResearchSetupPlan(workspacePaths(root).setupPlan);
549
+ const selected = selectedCredentialDefinitions(plan);
550
+ const credential = selected.find((candidate) => candidate.id === credentialId);
551
+ if (!credential) {
552
+ throw setupError({
553
+ code: "RESEARCH_SETUP_CREDENTIAL_INVALID",
554
+ step: "credentials",
555
+ reason: `Credential is not declared by the selected setup plan: ${credentialId}.`,
556
+ minimumAction: "Inspect the setup catalog and selected plan credential IDs.",
557
+ retryCommand: `tiangong-ai research setup status --workspace ${root} --json`,
558
+ exitCode: 2,
559
+ });
560
+ }
561
+ return { plan, selected, credential };
562
+ }
563
+ async function persistResearchSetupCredential(input) {
564
+ const { selected, credential } = await selectedSetupCredential(input.root, input.credentialId);
526
565
  if (credential.storage === "broker") {
527
- const declarations = await loadCapabilityDeclarations(root);
528
- await setCapabilityCredentialFromEnvironment({
529
- root,
530
- capabilities: declarations.capabilities,
566
+ const currentDeclarations = (await pathExists(workspacePaths(input.root).capabilityDeclarations))
567
+ ? await loadCapabilityDeclarations(input.root)
568
+ : { capabilities: [] };
569
+ await setCapabilityCredentialValue({
570
+ root: input.root,
571
+ declaredCredentialIds: [
572
+ ...new Set([
573
+ ...currentDeclarations.capabilities.flatMap((capability) => capability.credentials.map((candidate) => candidate.id)),
574
+ ...selected
575
+ .filter((candidate) => candidate.storage === "broker")
576
+ .map((candidate) => candidate.id),
577
+ ]),
578
+ ],
531
579
  credentialId: credential.id,
532
- environmentName: input.environmentName,
533
- environment: input.environment,
580
+ value: input.value,
581
+ minimumUtf8Bytes: credential.minimumUtf8Bytes,
534
582
  });
535
583
  }
536
584
  else {
537
- await setAdapterCredential(root, selected, credential.id, value);
585
+ await setAdapterCredential(input.root, selected.filter((candidate) => candidate.storage === "adapter"), credential.id, input.value);
538
586
  }
539
- await appendJournalEvent(workspacePaths(root).journal, "research.setup.credential.configured", "workspace", {
587
+ await appendJournalEvent(workspacePaths(input.root).journal, "research.setup.credential.configured", "workspace", {
540
588
  credentialId: credential.id,
541
- sourceEnvironmentNameSha256: sha256Text(input.environmentName),
589
+ inputMethod: input.inputMethod,
590
+ ...(input.sourceEnvironmentName === undefined
591
+ ? {}
592
+ : { sourceEnvironmentNameSha256: sha256Text(input.sourceEnvironmentName) }),
542
593
  storage: credential.storage,
543
594
  });
544
595
  return {
545
596
  schemaVersion: 1,
546
- workspace: root,
597
+ workspace: input.root,
547
598
  credentialId: credential.id,
548
599
  configured: true,
549
600
  storage: credential.storage,
@@ -693,11 +744,13 @@ export async function doctorResearchSetup(workspace, options = {}) {
693
744
  checks.push({
694
745
  id: `setting.${setting.id}`,
695
746
  category: "configuration",
696
- status: configured ? "pass" : setting.required ? "fail" : "warn",
747
+ status: configured || !setting.required ? "pass" : "fail",
697
748
  detail: configured
698
749
  ? "Declared non-secret setting is configured."
699
- : "Setting is not configured.",
700
- minimumAction: configured
750
+ : setting.required
751
+ ? "Required setting is not configured."
752
+ : "Optional setting was explicitly omitted.",
753
+ minimumAction: configured || !setting.required
701
754
  ? null
702
755
  : `Create a reviewed replacement plan with the ${setting.id} setting.`,
703
756
  });
@@ -736,13 +789,15 @@ export async function doctorResearchSetup(workspace, options = {}) {
736
789
  checks.push({
737
790
  id: `credential.${credential.id}`,
738
791
  category: "credential",
739
- status: configured ? "pass" : credential.required ? "fail" : "warn",
792
+ status: configured || !credential.required ? "pass" : "fail",
740
793
  detail: configured
741
794
  ? "Credential is present in an owner-only store; its value was not emitted."
742
- : "Credential is not configured.",
743
- minimumAction: configured
795
+ : credential.required
796
+ ? "Required credential is not configured."
797
+ : "Optional credential was explicitly omitted.",
798
+ minimumAction: configured || !credential.required
744
799
  ? null
745
- : `Run research setup credential set --id ${credential.id} --from-env <OWNER_ENV_NAME> --workspace ${root}.`,
800
+ : `Run research setup credential set --id ${credential.id} --prompt --workspace ${root}.`,
746
801
  });
747
802
  }
748
803
  await appendDependencyChecks(checks, selected, runner, root, environment);
@@ -784,15 +839,6 @@ export async function doctorResearchSetup(workspace, options = {}) {
784
839
  allowSyntheticUnstructureUpload: options.allowSyntheticUnstructureUpload === true,
785
840
  });
786
841
  }
787
- else {
788
- checks.push({
789
- id: "live-provider-checks",
790
- category: "live-check",
791
- status: "warn",
792
- detail: "Live provider checks were not requested.",
793
- minimumAction: `Run tiangong-ai research setup doctor --live --workspace ${root} --json after reviewing quota impact.`,
794
- });
795
- }
796
842
  let workspaceDoctor = null;
797
843
  try {
798
844
  workspaceDoctor = await doctorResearchWorkspace(root, {
@@ -800,15 +846,23 @@ export async function doctorResearchSetup(workspace, options = {}) {
800
846
  capabilitySmoke: options.live === true,
801
847
  environment,
802
848
  capabilityFetcher: fetcher,
849
+ ...(options.executor === undefined ? {} : { executor: options.executor }),
803
850
  });
851
+ const requiredRuntimeChecks = options.agentSmoke === true || options.live === true;
852
+ const runtimeBlocked = workspaceDoctor.status !== "ready" && requiredRuntimeChecks;
853
+ const failedWorkspaceChecks = workspaceDoctor.checks
854
+ .filter((check) => check.status === "fail")
855
+ .map((check) => check.id);
804
856
  checks.push({
805
857
  id: "production-runtime",
806
858
  category: "research-runtime",
807
- status: workspaceDoctor.status === "ready" ? "pass" : "warn",
859
+ status: workspaceDoctor.status === "ready" ? "pass" : runtimeBlocked ? "fail" : "warn",
808
860
  detail: `Workspace doctor reported ${workspaceDoctor.status}.`,
809
861
  minimumAction: workspaceDoctor.status === "ready"
810
862
  ? null
811
- : "Configure explicit production models/pricing and run the separately confirmed agent/capability smoke checks.",
863
+ : runtimeBlocked
864
+ ? `Resolve the failed workspace doctor checks (${failedWorkspaceChecks.join(", ") || "unknown"}); an explicitly requested smoke failure blocks readiness.`
865
+ : "Configure explicit production models/pricing and run the separately confirmed agent/capability smoke checks.",
812
866
  });
813
867
  }
814
868
  catch (error) {
@@ -820,6 +874,20 @@ export async function doctorResearchSetup(workspace, options = {}) {
820
874
  minimumAction: "Repair workspace runtime state, then rerun setup doctor.",
821
875
  });
822
876
  }
877
+ if (!options.live) {
878
+ const attestedCapabilitySmoke = workspaceDoctor?.checks.some((check) => check.id === "capability-live-smoke" && check.status === "pass");
879
+ checks.push({
880
+ id: "live-provider-checks",
881
+ category: "live-check",
882
+ status: attestedCapabilitySmoke ? "pass" : "warn",
883
+ detail: attestedCapabilitySmoke
884
+ ? "Reused the unexpired, runtime-bound capability smoke attestation."
885
+ : "Live provider checks were not requested and no reusable attestation is available.",
886
+ minimumAction: attestedCapabilitySmoke
887
+ ? null
888
+ : `Run tiangong-ai research setup doctor --live --workspace ${root} --json after reviewing quota impact.`,
889
+ });
890
+ }
823
891
  const readiness = checks.some((check) => check.status === "fail")
824
892
  ? "BLOCKED"
825
893
  : checks.some((check) => check.status === "warn")
@@ -1012,6 +1080,7 @@ function parseResearchSetupPlan(value) {
1012
1080
  typeof skill.expectedTreeSha256 !== "string" ||
1013
1081
  !/^[0-9a-f]{64}$/.test(skill.expectedTreeSha256) ||
1014
1082
  ![
1083
+ "orchestrator",
1015
1084
  "evidence-capability",
1016
1085
  "input-preprocessor",
1017
1086
  "acquisition-adapter",
@@ -1129,7 +1198,7 @@ function assertPlanMatchesCatalog(plan) {
1129
1198
  if (plan.checks.agentSmoke !== plan.confirmations.agentSmokeCost) {
1130
1199
  throw planCatalogDrift("agent smoke confirmation");
1131
1200
  }
1132
- const expectedMutations = setupMutations(plan.workspace.path, plan.install.targets, selected, plan.credentialSources);
1201
+ const expectedMutations = setupMutations(plan.workspace.path, plan.install.targets, selected);
1133
1202
  if (canonicalJson(plan.mutations) !== canonicalJson(expectedMutations)) {
1134
1203
  throw planCatalogDrift("declared mutations");
1135
1204
  }
@@ -1668,11 +1737,9 @@ async function installSetupSkills(input) {
1668
1737
  }
1669
1738
  }
1670
1739
  async function configureSelectedCapabilities(plan, _environment) {
1671
- if (plan.selection.evidenceProfile === "none" &&
1672
- !plan.selection.skillIds.includes("tiangong.kb-sci-search")) {
1673
- return;
1674
- }
1675
- const codexRoot = plannedTargetRoot(plan, "codex");
1740
+ const hasBraveProfile = plan.selection.evidenceProfile !== "none";
1741
+ const hasTiangongSci = plan.selection.skillIds.includes("tiangong.kb-sci-search");
1742
+ const codexRoot = hasBraveProfile || hasTiangongSci ? plannedTargetRoot(plan, "codex") : null;
1676
1743
  if (plan.selection.evidenceProfile !== "none") {
1677
1744
  await configureExternalSkillProfile({
1678
1745
  workspace: plan.workspace.path,
@@ -1680,7 +1747,7 @@ async function configureSelectedCapabilities(plan, _environment) {
1680
1747
  skillRoot: codexRoot,
1681
1748
  });
1682
1749
  }
1683
- if (plan.selection.skillIds.includes("tiangong.kb-sci-search")) {
1750
+ if (hasTiangongSci) {
1684
1751
  const skill = setupSkill("tiangong.kb-sci-search");
1685
1752
  const source = setupSource(skill.sourceId);
1686
1753
  await configureTiangongSciCapability({
@@ -1700,6 +1767,47 @@ async function configureSelectedCapabilities(plan, _environment) {
1700
1767
  : { region: plan.settings["tiangong.sci.region"] }),
1701
1768
  });
1702
1769
  }
1770
+ await reconcileSetupManagedCapabilities({
1771
+ workspace: plan.workspace.path,
1772
+ selectedCapabilityIds: [
1773
+ ...BRAVE_PROFILE_SKILLS[plan.selection.evidenceProfile].map(setupManagedCapabilityId),
1774
+ ...(hasTiangongSci ? ["database.tiangong.sci-search"] : []),
1775
+ ],
1776
+ });
1777
+ }
1778
+ function setupManagedCapabilityId(skillId) {
1779
+ const mapping = {
1780
+ "brave.web-search": "method.brave.web-search",
1781
+ "brave.news-search": "method.brave.news-search",
1782
+ "brave.llm-context": "method.brave.llm-context",
1783
+ "brave.images-search": "method.brave.images-search",
1784
+ "brave.videos-search": "method.brave.videos-search",
1785
+ };
1786
+ const capabilityId = mapping[skillId] ?? null;
1787
+ if (!capabilityId) {
1788
+ throw setupError({
1789
+ code: "RESEARCH_SETUP_CATALOG_DRIFT",
1790
+ step: "capability-configuration",
1791
+ reason: `Setup-managed evidence Skill has no capability mapping: ${skillId}.`,
1792
+ minimumAction: "Use the exact CLI/catalog release and regenerate the setup plan.",
1793
+ retryCommand: "tiangong-ai research setup catalog --json",
1794
+ exitCode: 3,
1795
+ });
1796
+ }
1797
+ return capabilityId;
1798
+ }
1799
+ async function reconcilePlanCredentialStores(plan) {
1800
+ const declarations = await loadCapabilityDeclarations(plan.workspace.path);
1801
+ await reconcileCapabilityCredentialEnvironment(plan.workspace.path, declarations.capabilities);
1802
+ const adapterPath = workspacePaths(plan.workspace.path).setupAdapterEnv;
1803
+ if (!(await pathExists(adapterPath)))
1804
+ return;
1805
+ const definitions = selectedCredentialDefinitions(plan).filter((credential) => credential.storage === "adapter");
1806
+ const configured = await loadAdapterCredentials(plan.workspace.path, definitions, {
1807
+ ignoreUndeclared: true,
1808
+ });
1809
+ const serialized = Object.fromEntries([...configured.entries()].sort(([left], [right]) => left.localeCompare(right)));
1810
+ await writeTextAtomic(adapterPath, `${ADAPTER_ENV_KEY}=${JSON.stringify(serialized)}\n`, 0o600);
1703
1811
  }
1704
1812
  async function configurePlanCredentials(plan, environment) {
1705
1813
  for (const credential of plan.credentialSources) {
@@ -1723,22 +1831,16 @@ async function assertRequiredCredentialPreflight(plan, environment) {
1723
1831
  const definitions = selectedCredentialDefinitions(plan);
1724
1832
  let adapterCredentials = new Map();
1725
1833
  try {
1726
- adapterCredentials = await loadAdapterCredentials(plan.workspace.path, definitions);
1834
+ adapterCredentials = await loadAdapterCredentials(plan.workspace.path, definitions, {
1835
+ ignoreUndeclared: true,
1836
+ });
1727
1837
  }
1728
1838
  catch (error) {
1729
1839
  if (error instanceof CliError)
1730
1840
  throw error;
1731
1841
  }
1732
- let configuredBrokerIds = new Set();
1733
- try {
1734
- const declarations = await loadCapabilityDeclarations(plan.workspace.path);
1735
- const status = await inspectCapabilityCredentialEnvironment(plan.workspace.path, declarations.capabilities);
1736
- configuredBrokerIds = new Set(status.configuredIds);
1737
- }
1738
- catch {
1739
- // A first apply has not configured capability declarations yet. The
1740
- // reviewed plan's environment mapping remains the only accepted source.
1741
- }
1842
+ const brokerDefinitions = definitions.filter((definition) => definition.storage === "broker");
1843
+ const configuredBrokerIds = new Set((await loadCapabilityCredentialMapForIds(plan.workspace.path, brokerDefinitions.map((definition) => definition.id), { ignoreUndeclared: true })).keys());
1742
1844
  const failures = [];
1743
1845
  for (const definition of definitions) {
1744
1846
  const planned = plan.credentialSources.find((candidate) => candidate.id === definition.id);
@@ -1749,10 +1851,7 @@ async function assertRequiredCredentialPreflight(plan, environment) {
1749
1851
  Buffer.byteLength(environment[planned.fromEnvironment] ?? "", "utf8") >=
1750
1852
  definition.minimumUtf8Bytes;
1751
1853
  if ((!planned && definition.required && !stored) || (planned && !supplied && !stored)) {
1752
- failures.push({
1753
- id: definition.id,
1754
- environmentName: planned?.fromEnvironment ?? null,
1755
- });
1854
+ failures.push({ id: definition.id });
1756
1855
  }
1757
1856
  }
1758
1857
  if (failures.length) {
@@ -1762,8 +1861,8 @@ async function assertRequiredCredentialPreflight(plan, environment) {
1762
1861
  reason: `Required or explicitly selected credentials are unavailable: ${failures
1763
1862
  .map((failure) => failure.id)
1764
1863
  .join(", ")}.`,
1765
- minimumAction: `Set the reviewed owner environment variables before any download (${failures
1766
- .map((failure) => `${failure.id}=${failure.environmentName ?? "<mapping-required>"}`)
1864
+ minimumAction: `Configure each unavailable logical credential with research setup credential set --prompt, --from-stdin, or --from-env before any download (${failures
1865
+ .map((failure) => failure.id)
1767
1866
  .join(", ")}), then retry this exact step.`,
1768
1867
  retryCommand: `tiangong-ai research setup retry --step credential-preflight --workspace ${plan.workspace.path} --json`,
1769
1868
  exitCode: 3,
@@ -2211,7 +2310,7 @@ function companionArtifactError(root, reason) {
2211
2310
  exitCode: 3,
2212
2311
  });
2213
2312
  }
2214
- async function loadAdapterCredentials(root, definitions) {
2313
+ async function loadAdapterCredentials(root, definitions, options = {}) {
2215
2314
  const path = workspacePaths(root).setupAdapterEnv;
2216
2315
  if (!(await pathExists(path)))
2217
2316
  return new Map();
@@ -2282,6 +2381,8 @@ async function loadAdapterCredentials(root, definitions) {
2282
2381
  const result = new Map();
2283
2382
  for (const [id, credentialValue] of Object.entries(value)) {
2284
2383
  const definition = allowed.get(id);
2384
+ if (!definition && options.ignoreUndeclared)
2385
+ continue;
2285
2386
  if (!definition ||
2286
2387
  typeof credentialValue !== "string" ||
2287
2388
  Buffer.byteLength(credentialValue, "utf8") < definition.minimumUtf8Bytes) {
@@ -2299,7 +2400,7 @@ async function loadAdapterCredentials(root, definitions) {
2299
2400
  return result;
2300
2401
  }
2301
2402
  async function setAdapterCredential(root, definitions, credentialId, value) {
2302
- const configured = await loadAdapterCredentials(root, definitions);
2403
+ const configured = await loadAdapterCredentials(root, definitions, { ignoreUndeclared: true });
2303
2404
  configured.set(credentialId, value);
2304
2405
  const serialized = Object.fromEntries([...configured.entries()].sort(([left], [right]) => left.localeCompare(right)));
2305
2406
  await writeTextAtomic(workspacePaths(root).setupAdapterEnv, `${ADAPTER_ENV_KEY}=${JSON.stringify(serialized)}\n`, 0o600);
@@ -2498,7 +2599,7 @@ function validateSetupSetting(id, validation, value) {
2498
2599
  });
2499
2600
  }
2500
2601
  }
2501
- function setupMutations(root, targets, selected, credentialSources) {
2602
+ function setupMutations(root, targets, selected) {
2502
2603
  const mutations = [
2503
2604
  {
2504
2605
  step: "workspace",
@@ -2515,27 +2616,21 @@ function setupMutations(root, targets, selected, credentialSources) {
2515
2616
  });
2516
2617
  }
2517
2618
  }
2518
- if (selected.some((skill) => skill.role === "evidence-capability")) {
2519
- mutations.push({
2520
- step: "capability-configuration",
2521
- target: workspacePaths(root).capabilityDeclarations,
2522
- reason: "Declare and lock the explicitly selected evidence capabilities.",
2523
- });
2524
- }
2525
- if (credentialSources.some((credential) => credential.storage === "broker")) {
2526
- mutations.push({
2527
- step: "credentials",
2528
- target: workspacePaths(root).env,
2529
- reason: "Store selected broker credentials in the owner-only workspace environment file.",
2530
- });
2531
- }
2532
- if (credentialSources.some((credential) => credential.storage === "adapter")) {
2533
- mutations.push({
2534
- step: "credentials",
2535
- target: workspacePaths(root).setupAdapterEnv,
2536
- reason: "Store selected companion-adapter credentials in an owner-only file.",
2537
- });
2538
- }
2619
+ mutations.push({
2620
+ step: "capability-configuration",
2621
+ target: workspacePaths(root).capabilityDeclarations,
2622
+ reason: "Reconcile and lock the explicitly selected setup-managed evidence capabilities while preserving custom declarations.",
2623
+ });
2624
+ mutations.push({
2625
+ step: "credentials",
2626
+ target: workspacePaths(root).env,
2627
+ reason: "Reconcile selected broker credentials in the owner-only workspace environment file.",
2628
+ });
2629
+ mutations.push({
2630
+ step: "credentials",
2631
+ target: workspacePaths(root).setupAdapterEnv,
2632
+ reason: "Reconcile selected companion-adapter credentials in an owner-only file.",
2633
+ });
2539
2634
  return mutations.sort((left, right) => `${left.step}\0${left.target}`.localeCompare(`${right.step}\0${right.target}`));
2540
2635
  }
2541
2636
  function setupLockPayload(planSha256) {