@ouro.bot/cli 0.1.0-alpha.382 → 0.1.0-alpha.384
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.384",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Added `ouro vault recover --agent <agent> --from <json>` for existing alpha agents whose vault coordinates exist but whose unlock secret was never saved.",
|
|
8
|
+
"`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.",
|
|
9
|
+
"Auth/provider docs now distinguish normal vault unlock from lost-secret replacement-vault recovery, including the old-auth checklist path.",
|
|
10
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault recovery release."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.1.0-alpha.383",
|
|
15
|
+
"changes": [
|
|
16
|
+
"`ouro provider refresh` now stops after a failed vault credential refresh instead of also reporting that the daemon restarted to reload credentials.",
|
|
17
|
+
"Provider refresh CLI tests now cover the locked-vault failure path separately from successful daemon restart and restart-skip paths.",
|
|
18
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the provider refresh failure-output release."
|
|
19
|
+
]
|
|
20
|
+
},
|
|
4
21
|
{
|
|
5
22
|
"version": "0.1.0-alpha.382",
|
|
6
23
|
"changes": [
|
|
@@ -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.";
|
|
@@ -1083,6 +1257,9 @@ async function executeProviderRefresh(command, deps) {
|
|
|
1083
1257
|
else {
|
|
1084
1258
|
lines.push(`provider credential refresh failed for ${command.agent}: ${pool.error}`);
|
|
1085
1259
|
lines.push(`Run \`ouro vault unlock --agent ${command.agent}\`, then retry.`);
|
|
1260
|
+
const message = lines.join("\n");
|
|
1261
|
+
deps.writeStdout(message);
|
|
1262
|
+
return message;
|
|
1086
1263
|
}
|
|
1087
1264
|
try {
|
|
1088
1265
|
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
@@ -2550,6 +2727,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
2550
2727
|
if (command.kind === "vault.create") {
|
|
2551
2728
|
return executeVaultCreate(command, deps);
|
|
2552
2729
|
}
|
|
2730
|
+
if (command.kind === "vault.recover") {
|
|
2731
|
+
return executeVaultRecover(command, deps);
|
|
2732
|
+
}
|
|
2553
2733
|
if (command.kind === "vault.status") {
|
|
2554
2734
|
return executeVaultStatus(command, deps);
|
|
2555
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
|
-
|
|
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) {
|