@ouro.bot/cli 0.1.0-alpha.376 → 0.1.0-alpha.377

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/README.md CHANGED
@@ -11,7 +11,7 @@ Ouroboros is a TypeScript harness for daemon-managed agents that live in externa
11
11
  - `ouro up` starts the daemon from the installed production version, syncs the launcher, installs workflow helpers, and reconciles stale runtime state.
12
12
  - `ouro dev` starts the daemon from a local repo build. It auto-builds from source, disables launchd auto-restart (so the installed daemon doesn't respawn underneath you), persists the repo path in `~/.ouro-cli/dev-config.json` for next time, and force-restarts the daemon. If you run `ouro dev` from inside the repo, it detects the CWD automatically. Run `ouro up` to return to production mode (this also cleans up `dev-config.json`).
13
13
  - Agent bundles live outside the repo at `~/AgentBundles/<agent>.ouro/`.
14
- - Provider credentials live in the owning agent's Bitwarden/Vaultwarden vault. Tool/sense credential vault migration is a follow-up.
14
+ - Credentials live in the owning agent's Bitwarden/Vaultwarden vault. Provider credentials use `providers/<provider>`, runtime/sense/integration credentials use `runtime/config`, and travel/tool credentials use ordinary vault credential items.
15
15
  - Vault coordinates and local runtime state live in the agent bundle; raw credentials do not.
16
16
  - Machine-scoped test and runtime spillover lives under `~/.agentstate/...`.
17
17
 
@@ -93,9 +93,9 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
93
93
 
94
94
  - `agent.json` is the source of truth for identity, phrase pools, context settings, enabled senses, and vault coordinates. Legacy `humanFacing`/`agentFacing` values are bootstrap inputs, not live machine fallback.
95
95
  - `state/providers.json` is the local source of truth for provider+model selection on this machine. It has two lanes: `outward` for CLI, Teams, and BlueBubbles turns, and `inner` for inner dialogue.
96
- - Each agent has one credential vault for provider credentials. There is no machine-wide provider credential pool.
96
+ - Each agent has one credential vault for provider, runtime, sense, integration, travel, and tool credentials. There is no machine-wide credential pool.
97
97
  - Vault unlock material is local machine state. Prefer macOS Keychain, Windows DPAPI, or Linux Secret Service; plaintext fallback is allowed only by explicit human choice.
98
- - Provider credentials are loaded into daemon memory at startup/auth/unlock/refresh and reused. The remote vault is not queried for every model request.
98
+ - Provider and runtime credentials are loaded into process memory at startup/auth/unlock/refresh and reused. The remote vault is not queried for every model or sense request.
99
99
  - The daemon discovers bundles dynamically from `~/AgentBundles`.
100
100
  - `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
101
101
  - `bundle-meta.json` tracks the runtime version that last touched a bundle.
@@ -170,6 +170,8 @@ ouro logs
170
170
  ouro stop
171
171
  ouro vault unlock --agent <name>
172
172
  ouro vault status --agent <name>
173
+ ouro vault config set --agent <name> --key bluebubbles.password
174
+ ouro vault config status --agent <name>
173
175
  ouro auth --agent <name>
174
176
  ouro auth --agent <name> --provider <provider>
175
177
  ouro auth verify --agent <name> [--provider <provider>]
@@ -21,5 +21,5 @@ I help humans hatch new agent partners. I am one of thirteen serpent guides —
21
21
 
22
22
  ## Filesystem orientation
23
23
  - Bundles live at `~/AgentBundles/<Name>.ouro/`.
24
- - Secrets live at `~/.agentsecrets/<name>/secrets.json`.
24
+ - Credentials live in the hatchling agent's Bitwarden/Vaultwarden vault; I never persist credentials for myself.
25
25
  - I tell the human exactly where their hatchling was created.
package/changelog.json CHANGED
@@ -1,6 +1,15 @@
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.377",
6
+ "changes": [
7
+ "Runtime configuration now lives in each agent's vault as `runtime/config`, alongside provider credentials in `providers/*`, making the agent vault the single credential/config source of truth.",
8
+ "`ouro vault config set/status`, daemon startup, senses, prompt rendering, doctor checks, and provider repair paths now share the same vault-backed runtime config loader instead of reading or writing local `~/.agentsecrets` files.",
9
+ "Provider credentials, runtime config, and travel/tool credentials are documented as vault items; legacy local secrets paths are now only guarded against or reported as migration hazards.",
10
+ "`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault source-of-truth release."
11
+ ]
12
+ },
4
13
  {
5
14
  "version": "0.1.0-alpha.376",
6
15
  "changes": [
@@ -35,17 +35,37 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  // Unified agent runtime entrypoint.
37
37
  // Requires --agent before importing runtime modules that rely on identity.
38
- if (!process.argv.includes("--agent")) {
38
+ const agentArgIndex = process.argv.indexOf("--agent");
39
+ const agentName = agentArgIndex >= 0 ? process.argv[agentArgIndex + 1] : undefined;
40
+ if (!agentName) {
39
41
  // eslint-disable-next-line no-console -- pre-boot guard
40
42
  console.error("Missing required --agent <name> argument.\nUsage: node dist/heart/agent-entry.js --agent ouroboros");
41
43
  process.exit(1);
42
44
  }
43
45
  const cli_logging_1 = require("../nerves/cli-logging");
46
+ const runtime_1 = require("../nerves/runtime");
44
47
  (0, cli_logging_1.configureCliRuntimeLogger)("self");
48
+ (0, runtime_1.emitNervesEvent)({
49
+ component: "senses",
50
+ event: "senses.entry_boot",
51
+ message: "booting inner-dialog entrypoint",
52
+ meta: { entry: "inner-dialog", agentName },
53
+ });
45
54
  // Dynamic import: agent-entry is boot-time wiring that starts a sense process.
46
55
  // Using dynamic import avoids a static heart/ -> senses/ dependency.
47
- Promise.resolve().then(() => __importStar(require("../senses/inner-dialog-worker"))).then(({ startInnerDialogWorker }) => startInnerDialogWorker())
56
+ Promise.resolve().then(() => __importStar(require("./runtime-credentials"))).then(async ({ refreshRuntimeCredentialConfig }) => {
57
+ await refreshRuntimeCredentialConfig(agentName, { preserveCachedOnFailure: true }).catch(() => undefined);
58
+ const { startInnerDialogWorker } = await Promise.resolve().then(() => __importStar(require("../senses/inner-dialog-worker")));
59
+ await startInnerDialogWorker();
60
+ })
48
61
  .catch((error) => {
62
+ (0, runtime_1.emitNervesEvent)({
63
+ level: "error",
64
+ component: "senses",
65
+ event: "senses.entry_error",
66
+ message: "inner-dialog entrypoint failed",
67
+ meta: { entry: "inner-dialog", agentName, error: error instanceof Error ? error.message : String(error) },
68
+ });
49
69
  // eslint-disable-next-line no-console -- fatal startup guard for worker process
50
70
  console.error(error instanceof Error ? error.message : String(error));
51
71
  process.exit(1);
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadConfig = loadConfig;
37
37
  exports.resetConfigCache = resetConfigCache;
38
+ exports.cacheRuntimeConfigForTests = cacheRuntimeConfigForTests;
38
39
  exports.patchRuntimeConfig = patchRuntimeConfig;
39
40
  exports.getAzureConfig = getAzureConfig;
40
41
  exports.getMinimaxConfig = getMinimaxConfig;
@@ -62,6 +63,7 @@ const fs = __importStar(require("fs"));
62
63
  const path = __importStar(require("path"));
63
64
  const identity_1 = require("./identity");
64
65
  const provider_credentials_1 = require("./provider-credentials");
66
+ const runtime_credentials_1 = require("./runtime-credentials");
65
67
  const runtime_1 = require("../nerves/runtime");
66
68
  const DEFAULT_LOCAL_RUNTIME_CONFIG = {
67
69
  teams: {
@@ -119,9 +121,6 @@ function defaultRuntimeConfig() {
119
121
  let _runtimeConfigOverride = null;
120
122
  let _testContextOverride = null;
121
123
  let _providerConfigOverride = null;
122
- function resolveConfigPath() {
123
- return (0, identity_1.getAgentSecretsPath)();
124
- }
125
124
  function deepMerge(defaults, partial) {
126
125
  const result = { ...defaults };
127
126
  for (const key of Object.keys(partial)) {
@@ -138,92 +137,26 @@ function deepMerge(defaults, partial) {
138
137
  }
139
138
  return result;
140
139
  }
140
+ function localRuntimeFields(config) {
141
+ const { providers: _providers, context: _context, ...localFields } = config;
142
+ return localFields;
143
+ }
141
144
  function loadConfig() {
142
- const configPath = resolveConfigPath();
143
- // Auto-create config directory if it doesn't exist
144
- const configDir = path.dirname(configPath);
145
- fs.mkdirSync(configDir, { recursive: true });
146
- let fileData = {};
147
- try {
148
- const raw = fs.readFileSync(configPath, "utf-8");
149
- fileData = JSON.parse(raw);
150
- }
151
- catch (error) {
152
- const errorCode = error &&
153
- typeof error === "object" &&
154
- "code" in error &&
155
- typeof error.code === "string"
156
- ? error.code
157
- : undefined;
158
- if (errorCode === "ENOENT") {
159
- try {
160
- fs.writeFileSync(configPath, JSON.stringify(DEFAULT_LOCAL_RUNTIME_CONFIG, null, 2) + "\n", "utf-8");
161
- }
162
- catch (writeError) {
163
- (0, runtime_1.emitNervesEvent)({
164
- level: "warn",
165
- event: "config_identity.error",
166
- component: "config/identity",
167
- message: "failed writing default secrets config",
168
- meta: {
169
- phase: "loadConfig",
170
- path: configPath,
171
- reason: writeError instanceof Error ? writeError.message : String(writeError),
172
- },
173
- });
174
- }
175
- }
176
- (0, runtime_1.emitNervesEvent)({
177
- level: "warn",
178
- event: "config_identity.error",
179
- component: "config/identity",
180
- message: "config read failed; defaults applied",
181
- meta: {
182
- phase: "loadConfig",
183
- reason: error instanceof Error ? error.message : String(error),
184
- },
185
- });
186
- // ENOENT or parse error -- use defaults
187
- }
188
- const sanitizedFileData = { ...fileData };
189
- if ("providers" in sanitizedFileData) {
190
- delete sanitizedFileData.providers;
191
- (0, runtime_1.emitNervesEvent)({
192
- level: "warn",
193
- event: "config_identity.error",
194
- component: "config/identity",
195
- message: "ignored legacy providers block in secrets config",
196
- meta: {
197
- phase: "loadConfig",
198
- path: configPath,
199
- },
200
- });
201
- }
202
- if ("context" in sanitizedFileData) {
203
- delete sanitizedFileData.context;
204
- (0, runtime_1.emitNervesEvent)({
205
- level: "warn",
206
- event: "config_identity.error",
207
- component: "config/identity",
208
- message: "ignored legacy context block in secrets config",
209
- meta: {
210
- phase: "loadConfig",
211
- path: configPath,
212
- },
213
- });
214
- }
215
- const mergedConfig = deepMerge(defaultRuntimeConfig(), sanitizedFileData);
145
+ const runtimeResult = (0, runtime_credentials_1.readRuntimeCredentialConfig)((0, identity_1.getAgentName)());
146
+ const vaultData = runtimeResult.ok ? localRuntimeFields(runtimeResult.config) : {};
147
+ const mergedConfig = deepMerge(defaultRuntimeConfig(), vaultData);
216
148
  const config = _runtimeConfigOverride
217
149
  ? deepMerge(mergedConfig, _runtimeConfigOverride)
218
150
  : mergedConfig;
219
151
  (0, runtime_1.emitNervesEvent)({
220
152
  event: "config.load",
221
153
  component: "config/identity",
222
- message: "config loaded from disk",
154
+ message: "config loaded from runtime credential cache",
223
155
  meta: {
224
- source: "disk",
225
- used_defaults_only: Object.keys(fileData).length === 0,
156
+ source: runtimeResult.ok ? "vault-cache" : "defaults",
157
+ used_defaults_only: !runtimeResult.ok,
226
158
  override_applied: _runtimeConfigOverride !== null,
159
+ runtime_credentials: runtimeResult.ok ? "available" : runtimeResult.reason,
227
160
  },
228
161
  });
229
162
  return config;
@@ -233,6 +166,10 @@ function resetConfigCache() {
233
166
  _testContextOverride = null;
234
167
  _providerConfigOverride = null;
235
168
  (0, provider_credentials_1.resetProviderCredentialCache)();
169
+ (0, runtime_credentials_1.resetRuntimeCredentialConfigCache)();
170
+ }
171
+ function cacheRuntimeConfigForTests(agentName, config) {
172
+ (0, runtime_credentials_1.cacheRuntimeCredentialConfig)(agentName, config, new Date(0));
236
173
  }
237
174
  function seedProviderCredentialCache(providers) {
238
175
  if (!providers)
@@ -340,10 +277,10 @@ function getBlueBubblesConfig() {
340
277
  const config = loadConfig();
341
278
  const { serverUrl, password, accountId } = config.bluebubbles;
342
279
  if (!serverUrl.trim()) {
343
- throw new Error("bluebubbles.serverUrl is required in secrets.json to run the BlueBubbles sense.");
280
+ throw new Error("bluebubbles.serverUrl is required in the agent vault runtime/config item to run the BlueBubbles sense.");
344
281
  }
345
282
  if (!password.trim()) {
346
- throw new Error("bluebubbles.password is required in secrets.json to run the BlueBubbles sense.");
283
+ throw new Error("bluebubbles.password is required in the agent vault runtime/config item to run the BlueBubbles sense.");
347
284
  }
348
285
  return {
349
286
  serverUrl: serverUrl.trim(),
@@ -296,13 +296,7 @@ function checkAgentConfig(agentName, bundlesRoot) {
296
296
  });
297
297
  return { ok: true };
298
298
  }
299
- async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, secretsRootOrDeps = {}, maybeDeps = {}) {
300
- const deps = typeof secretsRootOrDeps === "string"
301
- ? {
302
- ...maybeDeps,
303
- homeDir: maybeDeps.homeDir ?? (path.basename(secretsRootOrDeps) === ".agentsecrets" ? path.dirname(secretsRootOrDeps) : undefined),
304
- }
305
- : secretsRootOrDeps;
299
+ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps = {}) {
306
300
  const stateResult = readOrBootstrapProviderStateForCheck(agentName, bundlesRoot, deps);
307
301
  if (!stateResult.ok)
308
302
  return stateResult.result;
@@ -46,7 +46,7 @@ function formatBlueBubblesHealthcheckFailure(serverUrlInput, error) {
46
46
  case "network-error":
47
47
  return `Cannot reach BlueBubbles at ${serverUrl}. Check \`bluebubbles.serverUrl\`, confirm the BlueBubbles app/API is running, and verify this machine can reach it. Raw error: ${rawReason}`;
48
48
  case "auth-failure":
49
- return `BlueBubbles auth failed at ${serverUrl} (HTTP ${status}). Check \`bluebubbles.password\` in secrets.json and confirm the server accepts it. Raw error: ${rawReason}`;
49
+ return `BlueBubbles auth failed at ${serverUrl} (HTTP ${status}). Check \`bluebubbles.password\` in the agent vault runtime/config item and confirm the server accepts it. Raw error: ${rawReason}`;
50
50
  case "server-error":
51
51
  return `BlueBubbles upstream returned HTTP ${status} at ${serverUrl}. Check the BlueBubbles app/server logs and confirm the upstream API is healthy. Raw error: ${rawReason}`;
52
52
  default:
@@ -69,6 +69,7 @@ const thoughts_1 = require("./thoughts");
69
69
  const launchd_1 = require("./launchd");
70
70
  const auth_flow_1 = require("../auth/auth-flow");
71
71
  const provider_credentials_1 = require("../provider-credentials");
72
+ const runtime_credentials_1 = require("../runtime-credentials");
72
73
  const provider_binding_resolver_1 = require("../provider-binding-resolver");
73
74
  const provider_state_1 = require("../provider-state");
74
75
  const machine_identity_1 = require("../machine-identity");
@@ -667,7 +668,7 @@ async function executeVaultCreate(command, deps) {
667
668
  `vault created for ${command.agent}`,
668
669
  `vault: ${email} at ${serverUrl}`,
669
670
  `local unlock store: ${store.kind}${store.secure ? "" : " (explicit plaintext fallback)"}`,
670
- "Provider credentials will be stored in this agent's Ouro credential vault.",
671
+ "All raw credentials for this agent will be stored in this Ouro credential vault.",
671
672
  ...(command.generateUnlockSecret
672
673
  ? [
673
674
  "",
@@ -696,10 +697,21 @@ async function executeVaultStatus(command, deps) {
696
697
  const lines = [
697
698
  `agent: ${command.agent}`,
698
699
  `vault: ${vault.email} at ${vault.serverUrl}`,
700
+ `vault locator: ${config.vault ? "agent.json" : "default"}`,
699
701
  `local unlock store: ${status.store ? `${status.store.kind}${status.store.secure ? "" : " (explicit plaintext fallback)"}` : "unavailable"}`,
700
702
  `local unlock: ${status.stored ? "available" : "missing"}`,
701
703
  ];
702
704
  if (status.stored) {
705
+ const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
706
+ if (runtime.ok) {
707
+ lines.push(`runtime credentials: ${summarizeRuntimeConfigFields(runtime.config).join(", ") || "none stored"} (${runtime.revision})`);
708
+ }
709
+ else {
710
+ lines.push(`runtime credentials: ${runtime.reason} (${runtime.error})`);
711
+ if (runtime.reason === "missing") {
712
+ lines.push(` fix: Run 'ouro vault config set --agent ${command.agent} --key <field>' to store sense/integration credentials.`);
713
+ }
714
+ }
703
715
  const pool = await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent);
704
716
  if (pool.ok) {
705
717
  const summary = (0, provider_credentials_1.summarizeProviderCredentialPool)(pool.pool);
@@ -720,6 +732,95 @@ async function executeVaultStatus(command, deps) {
720
732
  deps.writeStdout(message);
721
733
  return message;
722
734
  }
735
+ const RUNTIME_CONFIG_KEY_SEGMENT = /^[A-Za-z][A-Za-z0-9_-]*$/;
736
+ const FORBIDDEN_RUNTIME_CONFIG_KEY_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
737
+ function parseRuntimeConfigKey(key) {
738
+ const segments = key.split(".").map((segment) => segment.trim()).filter(Boolean);
739
+ if (segments.length < 2) {
740
+ throw new Error("runtime config key must be a dotted path such as bluebubbles.password");
741
+ }
742
+ for (const segment of segments) {
743
+ if (!RUNTIME_CONFIG_KEY_SEGMENT.test(segment) || FORBIDDEN_RUNTIME_CONFIG_KEY_SEGMENTS.has(segment)) {
744
+ throw new Error(`invalid runtime config key segment '${segment}'`);
745
+ }
746
+ }
747
+ return segments;
748
+ }
749
+ function setRuntimeConfigValue(config, key, value) {
750
+ const segments = parseRuntimeConfigKey(key);
751
+ const next = JSON.parse(JSON.stringify(config));
752
+ let cursor = next;
753
+ for (const segment of segments.slice(0, -1)) {
754
+ const existing = cursor[segment];
755
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
756
+ cursor[segment] = {};
757
+ }
758
+ cursor = cursor[segment];
759
+ }
760
+ cursor[segments[segments.length - 1]] = value;
761
+ return next;
762
+ }
763
+ function summarizeRuntimeConfigFields(config) {
764
+ const fields = [];
765
+ const visit = (prefix, value) => {
766
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
767
+ fields.push(prefix);
768
+ return;
769
+ }
770
+ for (const [key, child] of Object.entries(value)) {
771
+ visit(prefix ? `${prefix}.${key}` : key, child);
772
+ }
773
+ };
774
+ visit("", config);
775
+ return fields.filter(Boolean).sort();
776
+ }
777
+ async function executeVaultConfigSet(command, deps) {
778
+ if (command.agent === "SerpentGuide") {
779
+ throw new Error("SerpentGuide does not have persistent runtime credentials. Store credentials in the hatchling agent vault.");
780
+ }
781
+ const prompt = deps.promptInput;
782
+ const value = command.value ?? (prompt ? await prompt(`Value for ${command.key}: `) : "");
783
+ if (!value) {
784
+ throw new Error("vault config set requires --value <value> or an interactive prompt");
785
+ }
786
+ const current = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
787
+ if (!current.ok && current.reason !== "missing") {
788
+ throw new Error(`cannot read existing runtime credentials from ${current.itemPath}: ${current.error}`);
789
+ }
790
+ const nextConfig = setRuntimeConfigValue(current.ok ? current.config : {}, command.key, value);
791
+ const stored = await (0, runtime_credentials_1.upsertRuntimeCredentialConfig)(command.agent, nextConfig, providerCliNow(deps));
792
+ const message = [
793
+ `stored ${command.key} for ${command.agent} in the agent vault runtime/config item`,
794
+ `runtime credentials: ${stored.revision}`,
795
+ "value was not printed",
796
+ ].join("\n");
797
+ deps.writeStdout(message);
798
+ return message;
799
+ }
800
+ async function executeVaultConfigStatus(command, deps) {
801
+ if (command.agent === "SerpentGuide") {
802
+ const message = "SerpentGuide has no persistent runtime credentials. Hatch bootstrap stores selected credentials in the hatchling vault.";
803
+ deps.writeStdout(message);
804
+ return message;
805
+ }
806
+ const runtime = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(command.agent, { preserveCachedOnFailure: true });
807
+ const lines = [`agent: ${command.agent}`, `runtime config item: ${runtime.itemPath}`];
808
+ if (runtime.ok) {
809
+ lines.push(`status: available (${runtime.revision})`);
810
+ const fields = summarizeRuntimeConfigFields(runtime.config);
811
+ lines.push(`fields: ${fields.length === 0 ? "none stored" : fields.join(", ")}`);
812
+ }
813
+ else {
814
+ lines.push(`status: ${runtime.reason}`);
815
+ lines.push(`error: ${runtime.error}`);
816
+ lines.push(runtime.reason === "missing"
817
+ ? `fix: Run 'ouro vault config set --agent ${command.agent} --key <field>' to store runtime credentials.`
818
+ : `fix: Run 'ouro vault unlock --agent ${command.agent}', then retry.`);
819
+ }
820
+ const message = lines.join("\n");
821
+ deps.writeStdout(message);
822
+ return message;
823
+ }
723
824
  function readOrBootstrapProviderState(agentName, deps) {
724
825
  const agentRoot = providerCliAgentRoot({ agent: agentName }, deps);
725
826
  const readResult = (0, provider_state_1.readProviderState)(agentRoot);
@@ -2452,6 +2553,12 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2452
2553
  if (command.kind === "vault.status") {
2453
2554
  return executeVaultStatus(command, deps);
2454
2555
  }
2556
+ if (command.kind === "vault.config.set") {
2557
+ return executeVaultConfigSet(command, deps);
2558
+ }
2559
+ if (command.kind === "vault.config.status") {
2560
+ return executeVaultConfigStatus(command, deps);
2561
+ }
2455
2562
  // ── auth (local, no daemon socket needed) ──
2456
2563
  if (command.kind === "auth.run") {
2457
2564
  const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot).config.humanFacing.provider;
@@ -2938,7 +3045,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
2938
3045
  fetchImpl: deps.fetchImpl ?? fetch,
2939
3046
  socketPath: deps.socketPath,
2940
3047
  bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
2941
- secretsRoot: deps.secretsRoot ?? path.join(os.homedir(), ".agentsecrets"),
2942
3048
  homedir: os.homedir(),
2943
3049
  envPath: process.env.PATH ?? "",
2944
3050
  };
@@ -163,6 +163,13 @@ exports.COMMAND_REGISTRY = {
163
163
  example: "ouro auth --agent ouroboros",
164
164
  subcommands: ["verify", "switch"],
165
165
  },
166
+ vault: {
167
+ category: "Auth",
168
+ description: "Create, unlock, inspect, and populate the agent credential vault",
169
+ usage: "ouro vault <create|unlock|status|config> --agent <name>",
170
+ example: "ouro vault status --agent ouroboros",
171
+ subcommands: ["create", "unlock", "status", "config set", "config status"],
172
+ },
166
173
  thoughts: {
167
174
  category: "Internal",
168
175
  description: "View agent inner dialog thoughts",
@@ -76,6 +76,8 @@ function usage() {
76
76
  " ouro vault create --agent <name> --email <email> [--server <url>] [--store <store>] [--generate-unlock-secret]",
77
77
  " ouro vault unlock --agent <name> [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
78
78
  " ouro vault status --agent <name> [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
79
+ " ouro vault config set --agent <name> --key <path> [--value <value>]",
80
+ " ouro vault config status --agent <name>",
79
81
  " ouro chat <agent>",
80
82
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
81
83
  " ouro poke <agent> --task <task-id>",
@@ -435,6 +437,8 @@ function isVaultUnlockStoreKind(value) {
435
437
  }
436
438
  function parseVaultCommand(args) {
437
439
  const sub = args[0];
440
+ if (sub === "config")
441
+ return parseVaultConfigCommand(args.slice(1));
438
442
  const { agent, rest } = extractAgentFlag(args.slice(1));
439
443
  let email;
440
444
  let serverUrl;
@@ -485,6 +489,39 @@ function parseVaultCommand(args) {
485
489
  }
486
490
  return { kind: "vault.status", agent, ...(store ? { store } : {}) };
487
491
  }
492
+ function parseVaultConfigCommand(args) {
493
+ const sub = args[0];
494
+ const { agent, rest } = extractAgentFlag(args.slice(1));
495
+ let key;
496
+ let value;
497
+ for (let i = 0; i < rest.length; i += 1) {
498
+ const token = rest[i];
499
+ if (token === "--key") {
500
+ key = rest[i + 1];
501
+ i += 1;
502
+ continue;
503
+ }
504
+ if (token === "--value") {
505
+ value = rest[i + 1];
506
+ i += 1;
507
+ continue;
508
+ }
509
+ throw new Error("Usage: ouro vault config set --agent <name> --key <path> [--value <value>] OR ouro vault config status --agent <name>");
510
+ }
511
+ if (!agent || (sub !== "set" && sub !== "status")) {
512
+ throw new Error("Usage: ouro vault config set --agent <name> --key <path> [--value <value>] OR ouro vault config status --agent <name>");
513
+ }
514
+ if (sub === "status") {
515
+ if (key || value) {
516
+ throw new Error("Usage: ouro vault config status --agent <name>");
517
+ }
518
+ return { kind: "vault.config.status", agent };
519
+ }
520
+ if (!key) {
521
+ throw new Error("Usage: ouro vault config set --agent <name> --key <path> [--value <value>]");
522
+ }
523
+ return { kind: "vault.config.set", agent, key, ...(value !== undefined ? { value } : {}) };
524
+ }
488
525
  function parseProviderUseCommand(args) {
489
526
  const { agent, rest: afterAgent } = extractAgentFlag(args);
490
527
  const { facing, rest: afterFacing } = extractFacingFlag(afterAgent);
@@ -18,6 +18,7 @@ exports.runDoctorChecks = runDoctorChecks;
18
18
  const runtime_1 = require("../../nerves/runtime");
19
19
  const bluebubbles_health_diagnostics_1 = require("./bluebubbles-health-diagnostics");
20
20
  const ouro_path_installer_1 = require("../versioning/ouro-path-installer");
21
+ const runtime_credentials_1 = require("../runtime-credentials");
21
22
  const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
22
23
  // ── Category checkers ──
23
24
  function checkCliPath(deps) {
@@ -87,14 +88,6 @@ function numberField(record, key, fallback) {
87
88
  const value = record?.[key];
88
89
  return typeof value === "number" && Number.isFinite(value) ? value : fallback;
89
90
  }
90
- function readJsonObject(deps, filePath) {
91
- try {
92
- return asRecord(JSON.parse(deps.readFileSync(filePath)));
93
- }
94
- catch {
95
- return null;
96
- }
97
- }
98
91
  const SENSITIVE_CONFIG_KEYS = ["apiKey", "token", "secret", "password"];
99
92
  function credentialKeyLeaks(raw) {
100
93
  return SENSITIVE_CONFIG_KEYS.filter((key) => raw.includes(`"${key}"`));
@@ -208,26 +201,19 @@ async function checkSenses(deps) {
208
201
  });
209
202
  }
210
203
  if (sense === "bluebubbles" && senseObj.enabled === true) {
211
- const secretsPath = `${deps.secretsRoot}/${agentName}/secrets.json`;
212
- if (!deps.existsSync(secretsPath)) {
204
+ const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
205
+ if (!runtimeConfig.ok) {
213
206
  checks.push({
214
207
  label: `${agentDir} bluebubbles config`,
215
208
  status: "fail",
216
- detail: "missing secrets.json",
209
+ detail: runtimeConfig.reason === "missing"
210
+ ? "missing vault runtime/config"
211
+ : `vault runtime/config unavailable: ${runtimeConfig.error}`,
217
212
  });
218
213
  continue;
219
214
  }
220
- const secrets = readJsonObject(deps, secretsPath);
221
- if (!secrets) {
222
- checks.push({
223
- label: `${agentDir} bluebubbles config`,
224
- status: "fail",
225
- detail: "secrets.json unparseable",
226
- });
227
- continue;
228
- }
229
- const bluebubbles = asRecord(secrets.bluebubbles);
230
- const bluebubblesChannel = asRecord(secrets.bluebubblesChannel);
215
+ const bluebubbles = asRecord(runtimeConfig.config.bluebubbles);
216
+ const bluebubblesChannel = asRecord(runtimeConfig.config.bluebubblesChannel);
231
217
  const serverUrl = textField(bluebubbles, "serverUrl");
232
218
  const password = textField(bluebubbles, "password");
233
219
  const missing = [];
@@ -302,16 +288,16 @@ function checkSecurity(deps) {
302
288
  const agents = discoverAgents(deps);
303
289
  for (const agentDir of agents) {
304
290
  const agentName = agentDir.replace(/\.ouro$/, "");
305
- const secretsPath = `${deps.secretsRoot}/${agentName}/secrets.json`;
306
- if (deps.existsSync(secretsPath)) {
307
- const stat = deps.statSync(secretsPath);
308
- const worldReadable = (stat.mode & 0o004) !== 0;
309
- if (worldReadable) {
310
- checks.push({ label: `${agentDir} legacy secrets.json perms`, status: "warn", detail: "world-readable consider deleting or chmod 600" });
311
- }
312
- else {
313
- checks.push({ label: `${agentDir} legacy secrets.json perms`, status: "pass", detail: "not world-readable" });
314
- }
291
+ const legacySecretsPath = `${deps.homedir}/.agentsecrets/${agentName}/secrets.json`;
292
+ if (deps.existsSync(legacySecretsPath)) {
293
+ checks.push({
294
+ label: `${agentDir} legacy secrets.json`,
295
+ status: "fail",
296
+ detail: "legacy local credential file exists; migrate values into the agent vault runtime/config item and remove it",
297
+ });
298
+ }
299
+ else {
300
+ checks.push({ label: `${agentDir} legacy secrets.json`, status: "pass", detail: "absent" });
315
301
  }
316
302
  // Check agent.json for leaked credential keys
317
303
  const configPath = `${deps.bundlesRoot}/${agentDir}/agent.json`;
@@ -36,7 +36,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getRuntimeMetadata = getRuntimeMetadata;
37
37
  const crypto_1 = require("crypto");
38
38
  const fs = __importStar(require("fs"));
39
- const os = __importStar(require("os"));
40
39
  const path = __importStar(require("path"));
41
40
  const childProcess = __importStar(require("child_process"));
42
41
  const identity_1 = require("../identity");
@@ -87,19 +86,7 @@ function readLastUpdated(repoRoot, packageJsonPath, statSyncImpl, execFileSyncIm
87
86
  return { value: UNKNOWN_METADATA, source: "unknown" };
88
87
  }
89
88
  }
90
- function readHomeDir() {
91
- const homedirImpl = optionalFunction(os, "homedir");
92
- if (!homedirImpl) {
93
- return null;
94
- }
95
- try {
96
- return homedirImpl.call(os);
97
- }
98
- catch {
99
- return null;
100
- }
101
- }
102
- function listConfigTargets(bundlesRoot, secretsRoot, daemonLoggingPath, readdirSyncImpl) {
89
+ function listConfigTargets(bundlesRoot, daemonLoggingPath, readdirSyncImpl) {
103
90
  if (!readdirSyncImpl)
104
91
  return [];
105
92
  const targets = new Set();
@@ -117,19 +104,6 @@ function listConfigTargets(bundlesRoot, secretsRoot, daemonLoggingPath, readdirS
117
104
  catch {
118
105
  // ignore unreadable bundle roots
119
106
  }
120
- if (secretsRoot) {
121
- try {
122
- const secretEntries = readdirSyncImpl(secretsRoot, { withFileTypes: true });
123
- for (const entry of secretEntries) {
124
- if (!entry.isDirectory())
125
- continue;
126
- targets.add(path.join(secretsRoot, entry.name, "secrets.json"));
127
- }
128
- }
129
- catch {
130
- // ignore unreadable secrets roots
131
- }
132
- }
133
107
  return [...targets].sort();
134
108
  }
135
109
  function readConfigFingerprint(targets, readFileSyncImpl, existsSyncImpl) {
@@ -172,8 +146,6 @@ function readConfigFingerprint(targets, readFileSyncImpl, existsSyncImpl) {
172
146
  function getRuntimeMetadata(deps = {}) {
173
147
  const repoRoot = deps.repoRoot ?? (0, identity_1.getRepoRoot)();
174
148
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
175
- const homeDir = readHomeDir();
176
- const secretsRoot = deps.secretsRoot ?? (homeDir ? path.join(homeDir, ".agentsecrets") : null);
177
149
  const daemonLoggingPath = deps.daemonLoggingPath ?? (0, identity_1.getAgentDaemonLoggingConfigPath)();
178
150
  const readFileSyncImpl = deps.readFileSync ?? optionalFunction(fs, "readFileSync")?.bind(fs) ?? null;
179
151
  const statSyncImpl = deps.statSync ?? optionalFunction(fs, "statSync")?.bind(fs) ?? null;
@@ -191,7 +163,7 @@ function getRuntimeMetadata(deps = {}) {
191
163
  throw new Error("git unavailable");
192
164
  }))
193
165
  : { value: UNKNOWN_METADATA, source: "unknown" };
194
- const configTargets = listConfigTargets(bundlesRoot, secretsRoot, daemonLoggingPath, readdirSyncImpl);
166
+ const configTargets = listConfigTargets(bundlesRoot, daemonLoggingPath, readdirSyncImpl);
195
167
  const configFingerprint = readConfigFingerprint(configTargets, readFileSyncImpl, existsSyncImpl);
196
168
  (0, runtime_1.emitNervesEvent)({
197
169
  component: "daemon",