@ouro.bot/cli 0.1.0-alpha.383 → 0.1.0-alpha.385

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/changelog.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.385",
6
+ "changes": [
7
+ "`ouro up` provider-check repair hints for locked agent vaults now mention both normal unlock and lost-secret `ouro vault recover --agent <agent> --from <json>` recovery.",
8
+ "Provider health checks now share the locked-vault unlock-or-recover wording so compact degraded summaries do not send operators back to a secret they never saved.",
9
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the locked-vault repair-hint release."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.384",
14
+ "changes": [
15
+ "Added `ouro vault recover --agent <agent> --from <json>` for existing alpha agents whose vault coordinates exist but whose unlock secret was never saved.",
16
+ "`ouro vault recover` creates a replacement agent vault, imports provider credentials into `providers/*`, imports runtime/sense/integration credentials into `runtime/config`, and prints only redacted field/provider summaries.",
17
+ "Auth/provider docs now distinguish normal vault unlock from lost-secret replacement-vault recovery, including the old-auth checklist path.",
18
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault recovery release."
19
+ ]
20
+ },
4
21
  {
5
22
  "version": "0.1.0-alpha.383",
6
23
  "changes": [
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.vaultUnlockOrRecoverFix = vaultUnlockOrRecoverFix;
36
37
  exports.checkAgentConfig = checkAgentConfig;
37
38
  exports.checkAgentConfigWithProviderHealth = checkAgentConfigWithProviderHealth;
38
39
  const fs = __importStar(require("fs"));
@@ -238,7 +239,7 @@ function invalidPoolResult(agentName, lane, provider, model, pool) {
238
239
  return {
239
240
  ok: false,
240
241
  error: `${lane} provider ${provider} model ${model} cannot read provider credentials because ${agentName}'s credential vault is locked on this machine.`,
241
- fix: `Run 'ouro vault unlock --agent ${agentName}', then run 'ouro up' again.`,
242
+ fix: vaultUnlockOrRecoverFix(agentName),
242
243
  };
243
244
  }
244
245
  if (pool.reason === "invalid") {
@@ -251,13 +252,20 @@ function invalidPoolResult(agentName, lane, provider, model, pool) {
251
252
  return {
252
253
  ok: false,
253
254
  error: `${lane} provider ${provider} model ${model} cannot read provider credentials from ${agentName}'s vault at ${pool.poolPath}: ${pool.error}`,
254
- fix: `Run 'ouro vault unlock --agent ${agentName}', then run 'ouro up' again. If the credential is missing or stale after unlock, run 'ouro auth --agent ${agentName} --provider ${provider}'.`,
255
+ fix: vaultUnlockOrRecoverFix(agentName, `Then run 'ouro up' again. If the credential is missing or stale after unlock or recovery, run 'ouro auth --agent ${agentName} --provider ${provider}'.`),
255
256
  };
256
257
  }
257
258
  function isVaultLockedError(error) {
258
259
  const normalized = error.toLowerCase();
259
260
  return /(?:ouro )?credential vault is locked|vault(?: is)? locked/.test(normalized);
260
261
  }
262
+ function vaultUnlockOrRecoverFix(agentName, nextStep = "Then run 'ouro up' again.") {
263
+ return [
264
+ `Run 'ouro vault unlock --agent ${agentName}' if you have the saved vault unlock secret.`,
265
+ `If nobody saved it, run 'ouro vault recover --agent ${agentName} --from <json>' with a local credential export.`,
266
+ nextStep,
267
+ ].join(" ");
268
+ }
261
269
  function failedPingResult(agentName, lane, provider, model, result) {
262
270
  return {
263
271
  ok: false,
@@ -599,6 +599,101 @@ function writeAgentVaultConfig(agentName, configPath, config, vault) {
599
599
  meta: { agentName, configPath, email: vault.email, serverUrl: vault.serverUrl },
600
600
  });
601
601
  }
602
+ function isJsonRecord(value) {
603
+ return !!value && typeof value === "object" && !Array.isArray(value);
604
+ }
605
+ function cloneJsonRecord(value) {
606
+ return JSON.parse(JSON.stringify(value));
607
+ }
608
+ function importableCredentialFields(value) {
609
+ if (!isJsonRecord(value))
610
+ return {};
611
+ const result = {};
612
+ for (const [key, fieldValue] of Object.entries(value)) {
613
+ if (typeof fieldValue === "string" && fieldValue.trim()) {
614
+ result[key] = fieldValue;
615
+ }
616
+ else if (typeof fieldValue === "number" && Number.isFinite(fieldValue)) {
617
+ result[key] = fieldValue;
618
+ }
619
+ }
620
+ return result;
621
+ }
622
+ function providerImportFromRaw(provider, raw) {
623
+ if (!isJsonRecord(raw))
624
+ return null;
625
+ const hasStructuredFields = isJsonRecord(raw.credentials) || isJsonRecord(raw.config);
626
+ const fields = hasStructuredFields
627
+ ? {
628
+ credentials: importableCredentialFields(raw.credentials),
629
+ config: importableCredentialFields(raw.config),
630
+ }
631
+ : (0, provider_credentials_1.splitProviderCredentialFields)(provider, raw);
632
+ if (Object.keys(fields.credentials).length === 0 && Object.keys(fields.config).length === 0)
633
+ return null;
634
+ return { provider, credentials: fields.credentials, config: fields.config };
635
+ }
636
+ function recoverProviderImports(raw) {
637
+ const providers = isJsonRecord(raw.providers) ? raw.providers : raw;
638
+ const imports = [];
639
+ for (const [providerName, providerRaw] of Object.entries(providers)) {
640
+ if (!(0, cli_parse_2.isAgentProvider)(providerName))
641
+ continue;
642
+ const imported = providerImportFromRaw(providerName, providerRaw);
643
+ if (imported)
644
+ imports.push(imported);
645
+ }
646
+ return imports;
647
+ }
648
+ const RECOVER_RUNTIME_EXCLUDED_TOP_LEVEL = new Set(["providers", "vault", "context", "schemaVersion", "updatedAt"]);
649
+ function recoverRuntimeConfig(raw) {
650
+ const config = {};
651
+ for (const [key, value] of Object.entries(raw)) {
652
+ if (RECOVER_RUNTIME_EXCLUDED_TOP_LEVEL.has(key) || (0, cli_parse_2.isAgentProvider)(key))
653
+ continue;
654
+ config[key] = isJsonRecord(value) ? cloneJsonRecord(value) : value;
655
+ }
656
+ return config;
657
+ }
658
+ function mergeRuntimeConfig(a, b) {
659
+ const merged = { ...a };
660
+ for (const [key, value] of Object.entries(b)) {
661
+ if (isJsonRecord(merged[key]) && isJsonRecord(value)) {
662
+ merged[key] = mergeRuntimeConfig(merged[key], value);
663
+ }
664
+ else {
665
+ merged[key] = isJsonRecord(value) ? cloneJsonRecord(value) : value;
666
+ }
667
+ }
668
+ return merged;
669
+ }
670
+ function readVaultRecoverSource(sourcePath) {
671
+ const resolved = path.resolve(sourcePath);
672
+ let parsed;
673
+ try {
674
+ parsed = JSON.parse(fs.readFileSync(resolved, "utf8"));
675
+ }
676
+ catch (error) {
677
+ const reason = String(error);
678
+ throw new Error(`cannot read vault recover source ${resolved}: ${reason}`);
679
+ }
680
+ if (!isJsonRecord(parsed)) {
681
+ throw new Error(`vault recover source ${resolved} must be a JSON object`);
682
+ }
683
+ return {
684
+ sourcePath: resolved,
685
+ providers: recoverProviderImports(parsed),
686
+ runtimeConfig: recoverRuntimeConfig(parsed),
687
+ };
688
+ }
689
+ function defaultRecoveredVaultEmail(agentName, now) {
690
+ const local = agentName
691
+ .toLowerCase()
692
+ .replace(/[^a-z0-9._-]+/g, "-")
693
+ .replace(/^-+|-+$/g, "") || "agent";
694
+ const stamp = now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
695
+ return `${local}+recovered-${stamp}@ouro.bot`;
696
+ }
602
697
  async function executeVaultUnlock(command, deps) {
603
698
  if (command.agent === "SerpentGuide") {
604
699
  throw new Error("SerpentGuide does not have a persistent credential vault. Hatch bootstrap uses selected provider credentials in memory only.");
@@ -681,6 +776,85 @@ async function executeVaultCreate(command, deps) {
681
776
  deps.writeStdout(message);
682
777
  return message;
683
778
  }
779
+ async function executeVaultRecover(command, deps) {
780
+ if (command.agent === "SerpentGuide") {
781
+ throw new Error("SerpentGuide does not have a persistent credential vault. Recover the hatchling agent vault, not SerpentGuide.");
782
+ }
783
+ const prompt = deps.promptInput;
784
+ const now = providerCliNow(deps);
785
+ const { configPath, config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
786
+ const configuredVault = (0, identity_1.resolveVaultConfig)(command.agent, config.vault);
787
+ const email = command.email ?? defaultRecoveredVaultEmail(command.agent, now);
788
+ const serverUrl = command.serverUrl ?? config.vault?.serverUrl ?? configuredVault.serverUrl;
789
+ const requestedUnlockSecret = command.generateUnlockSecret
790
+ ? ""
791
+ : prompt
792
+ ? (await prompt(`Choose replacement Ouro vault unlock secret for ${email}: `)).trim()
793
+ : "";
794
+ if (!requestedUnlockSecret && !command.generateUnlockSecret) {
795
+ throw new Error("vault recover requires a replacement unlock secret. Re-run with an interactive terminal or pass --generate-unlock-secret.");
796
+ }
797
+ const unlockSecret = requestedUnlockSecret || (0, crypto_1.randomBytes)(32).toString("base64");
798
+ const sourceImports = command.sources.map(readVaultRecoverSource);
799
+ const result = await (0, vault_setup_1.createVaultAccount)("Ouro credential vault", serverUrl, email, unlockSecret);
800
+ if (!result.success) {
801
+ const message = [
802
+ `vault recover failed for ${command.agent}: ${result.error}`,
803
+ "",
804
+ "Recovery creates a replacement vault. If that vault account already exists, retry with a fresh --email value.",
805
+ ].join("\n");
806
+ deps.writeStdout(message);
807
+ return message;
808
+ }
809
+ writeAgentVaultConfig(command.agent, configPath, config, { email, serverUrl });
810
+ const store = (0, vault_unlock_1.storeVaultUnlockSecret)({
811
+ agentName: command.agent,
812
+ email,
813
+ serverUrl,
814
+ }, unlockSecret, { homeDir: deps.homeDir, store: command.store });
815
+ (0, credential_access_1.resetCredentialStore)();
816
+ await (0, credential_access_1.getCredentialStore)(command.agent).get("__ouro_vault_probe__");
817
+ const importedProviders = new Set();
818
+ let mergedRuntimeConfig = {};
819
+ for (const source of sourceImports) {
820
+ for (const provider of source.providers) {
821
+ await (0, provider_credentials_1.upsertProviderCredential)({
822
+ agentName: command.agent,
823
+ provider: provider.provider,
824
+ credentials: provider.credentials,
825
+ config: provider.config,
826
+ provenance: { source: "manual" },
827
+ now,
828
+ });
829
+ importedProviders.add(provider.provider);
830
+ }
831
+ mergedRuntimeConfig = mergeRuntimeConfig(mergedRuntimeConfig, source.runtimeConfig);
832
+ }
833
+ const runtimeFields = summarizeRuntimeConfigFields(mergedRuntimeConfig);
834
+ if (runtimeFields.length > 0) {
835
+ await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(command.agent, mergedRuntimeConfig, now);
836
+ }
837
+ const providerList = [...importedProviders].sort();
838
+ const message = [
839
+ `vault recovered for ${command.agent}`,
840
+ `vault: ${email} at ${serverUrl}`,
841
+ `local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`,
842
+ `sources imported: ${sourceImports.length}`,
843
+ `provider credentials imported: ${providerList.length === 0 ? "none" : providerList.join(", ")}`,
844
+ `runtime credentials imported: ${runtimeFields.length === 0 ? "none" : runtimeFields.join(", ")}`,
845
+ "credential values were not printed",
846
+ ...(command.generateUnlockSecret
847
+ ? [
848
+ "",
849
+ `vault unlock secret: ${unlockSecret}`,
850
+ "",
851
+ "Keep this saved outside Ouro now. Another machine cannot unlock the vault without it.",
852
+ ]
853
+ : ["Keep the replacement vault unlock secret saved outside Ouro. Another machine will need it once."]),
854
+ ].join("\n");
855
+ deps.writeStdout(message);
856
+ return message;
857
+ }
684
858
  async function executeVaultStatus(command, deps) {
685
859
  if (command.agent === "SerpentGuide") {
686
860
  const message = "SerpentGuide has no persistent credential vault. Hatch bootstrap uses selected provider credentials in memory only.";
@@ -2553,6 +2727,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2553
2727
  if (command.kind === "vault.create") {
2554
2728
  return executeVaultCreate(command, deps);
2555
2729
  }
2730
+ if (command.kind === "vault.recover") {
2731
+ return executeVaultRecover(command, deps);
2732
+ }
2556
2733
  if (command.kind === "vault.status") {
2557
2734
  return executeVaultStatus(command, deps);
2558
2735
  }
@@ -165,10 +165,10 @@ exports.COMMAND_REGISTRY = {
165
165
  },
166
166
  vault: {
167
167
  category: "Auth",
168
- description: "Create, unlock, inspect, and populate the agent credential vault",
169
- usage: "ouro vault <create|unlock|status|config> --agent <name>",
168
+ description: "Create, recover, unlock, inspect, and populate the agent credential vault",
169
+ usage: "ouro vault <create|recover|unlock|status|config> --agent <name>",
170
170
  example: "ouro vault status --agent ouroboros",
171
- subcommands: ["create", "unlock", "status", "config set", "config status"],
171
+ subcommands: ["create", "recover", "unlock", "status", "config set", "config status"],
172
172
  },
173
173
  thoughts: {
174
174
  category: "Internal",
@@ -74,6 +74,7 @@ function usage() {
74
74
  " ouro auth verify --agent <name> [--provider <provider>]",
75
75
  " ouro auth switch --agent <name> --provider <provider>",
76
76
  " ouro vault create --agent <name> --email <email> [--server <url>] [--store <store>] [--generate-unlock-secret]",
77
+ " ouro vault recover --agent <name> --from <json> [--from <json>] [--email <email>] [--server <url>] [--store <store>] [--generate-unlock-secret]",
77
78
  " ouro vault unlock --agent <name> [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
78
79
  " ouro vault status --agent <name> [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
79
80
  " ouro vault config set --agent <name> --key <path> [--value <value>]",
@@ -444,6 +445,7 @@ function parseVaultCommand(args) {
444
445
  let serverUrl;
445
446
  let store;
446
447
  let generateUnlockSecret = false;
448
+ const sources = [];
447
449
  for (let i = 0; i < rest.length; i += 1) {
448
450
  const token = rest[i];
449
451
  if (token === "--email") {
@@ -465,14 +467,22 @@ function parseVaultCommand(args) {
465
467
  i += 1;
466
468
  continue;
467
469
  }
470
+ if (token === "--from") {
471
+ const value = rest[i + 1];
472
+ if (!value)
473
+ throw new Error("Usage: ouro vault recover --agent <name> --from <json> [--from <json>]");
474
+ sources.push(value);
475
+ i += 1;
476
+ continue;
477
+ }
468
478
  if (token === "--generate-unlock-secret") {
469
479
  generateUnlockSecret = true;
470
480
  continue;
471
481
  }
472
- throw new Error("Usage: ouro vault create|unlock|status --agent <name>");
482
+ throw new Error("Usage: ouro vault create|recover|unlock|status --agent <name>");
473
483
  }
474
- if (!agent || (sub !== "create" && sub !== "unlock" && sub !== "status")) {
475
- throw new Error("Usage: ouro vault create|unlock|status --agent <name>");
484
+ if (!agent || (sub !== "create" && sub !== "recover" && sub !== "unlock" && sub !== "status")) {
485
+ throw new Error("Usage: ouro vault create|recover|unlock|status --agent <name>");
476
486
  }
477
487
  if (sub === "create") {
478
488
  return {
@@ -484,6 +494,20 @@ function parseVaultCommand(args) {
484
494
  ...(generateUnlockSecret ? { generateUnlockSecret: true } : {}),
485
495
  };
486
496
  }
497
+ if (sub === "recover") {
498
+ if (sources.length === 0) {
499
+ throw new Error("Usage: ouro vault recover --agent <name> --from <json> [--from <json>]");
500
+ }
501
+ return {
502
+ kind: "vault.recover",
503
+ agent,
504
+ sources,
505
+ ...(email ? { email } : {}),
506
+ ...(serverUrl ? { serverUrl } : {}),
507
+ ...(store ? { store } : {}),
508
+ ...(generateUnlockSecret ? { generateUnlockSecret: true } : {}),
509
+ };
510
+ }
487
511
  if (sub === "unlock") {
488
512
  return { kind: "vault.unlock", agent, ...(store ? { store } : {}) };
489
513
  }
@@ -111,7 +111,9 @@ function lockedMessage(config, store) {
111
111
  "This can happen on a new computer, after a local profile or hostname migration, or if the local unlock entry was removed.",
112
112
  "",
113
113
  `Run \`${command}\` and enter the saved agent vault unlock secret from the human/operator who controls that vault.`,
114
- "If nobody saved that unlock secret, Ouro cannot recover it; create or rotate the agent vault and re-enter credentials.",
114
+ config.agentName
115
+ ? `If nobody saved that unlock secret, run \`ouro vault recover --agent ${config.agentName} --from <json>\` with a local credential export, or create a replacement vault and re-enter credentials.`
116
+ : "If nobody saved that unlock secret, run `ouro vault recover --agent <agent> --from <json>` with a local credential export, or create a replacement vault and re-enter credentials.",
115
117
  ].join("\n");
116
118
  }
117
119
  function validateStoreKind(store) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.383",
3
+ "version": "0.1.0-alpha.385",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",