@ouro.bot/cli 0.1.0-alpha.409 → 0.1.0-alpha.410

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.
@@ -163,6 +163,13 @@ exports.COMMAND_REGISTRY = {
163
163
  example: "ouro auth --agent ouroboros",
164
164
  subcommands: ["verify", "switch"],
165
165
  },
166
+ connect: {
167
+ category: "Auth",
168
+ description: "Connect integrations and local senses such as Perplexity search and BlueBubbles iMessage",
169
+ usage: "ouro connect [perplexity|bluebubbles] --agent <name>",
170
+ example: "ouro connect perplexity --agent ouroboros",
171
+ subcommands: ["perplexity", "bluebubbles"],
172
+ },
166
173
  use: {
167
174
  category: "Auth",
168
175
  description: "Choose this machine's provider/model lane for an agent",
@@ -265,6 +272,16 @@ const SUBCOMMAND_HELP = {
265
272
  usage: "ouro auth switch --agent <name> --provider <provider> [--facing human|agent]",
266
273
  example: "ouro auth switch --agent ouroboros --provider minimax",
267
274
  },
275
+ "connect perplexity": {
276
+ description: "Connect Perplexity search for this agent",
277
+ usage: "ouro connect perplexity --agent <name>",
278
+ example: "ouro connect perplexity --agent ouroboros",
279
+ },
280
+ "connect bluebubbles": {
281
+ description: "Attach BlueBubbles iMessage to this machine only",
282
+ usage: "ouro connect bluebubbles --agent <name>",
283
+ example: "ouro connect bluebubbles --agent ouroboros",
284
+ },
268
285
  "provider refresh": {
269
286
  description: "Reload this agent's provider credentials from its vault into daemon memory",
270
287
  usage: "ouro provider refresh --agent <name>",
@@ -296,14 +313,14 @@ const SUBCOMMAND_HELP = {
296
313
  example: "ouro vault status --agent ouroboros",
297
314
  },
298
315
  "vault config set": {
299
- description: "Write runtime configuration into the agent credential vault",
300
- usage: "ouro vault config set --agent <name> --key <path> [--value <value>]",
301
- example: "ouro vault config set --agent ouroboros --key senses/outlook/clientId",
316
+ description: "Write runtime configuration into the agent credential vault without printing values",
317
+ usage: "ouro vault config set --agent <name> --key <path> [--value <value>] [--scope agent|machine]",
318
+ example: "ouro vault config set --agent ouroboros --key teams.clientSecret",
302
319
  },
303
320
  "vault config status": {
304
321
  description: "List runtime configuration keys stored in the agent credential vault",
305
- usage: "ouro vault config status --agent <name>",
306
- example: "ouro vault config status --agent ouroboros",
322
+ usage: "ouro vault config status --agent <name> [--scope agent|machine|all]",
323
+ example: "ouro vault config status --agent ouroboros --scope all",
307
324
  },
308
325
  };
309
326
  // ── Levenshtein distance ──
@@ -83,6 +83,7 @@ function usage() {
83
83
  " ouro config model --agent <name> <model-name>",
84
84
  " ouro config models --agent <name>",
85
85
  " ouro auth --agent <name> [--provider <provider>]",
86
+ " ouro connect [perplexity|bluebubbles] --agent <name>",
86
87
  " ouro auth verify --agent <name> [--provider <provider>]",
87
88
  " ouro auth switch --agent <name> --provider <provider>",
88
89
  " ouro vault create --agent <name> --email <email> [--server <url>] [--store <store>]",
@@ -90,8 +91,8 @@ function usage() {
90
91
  " ouro vault recover --agent <name> --from <json> [--from <json>] [--email <email>] [--server <url>] [--store <store>]",
91
92
  " ouro vault unlock --agent <name> [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
92
93
  " ouro vault status --agent <name> [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
93
- " ouro vault config set --agent <name> --key <path> [--value <value>]",
94
- " ouro vault config status --agent <name>",
94
+ " ouro vault config set --agent <name> --key <path> [--value <value>] [--scope agent|machine]",
95
+ " ouro vault config status --agent <name> [--scope agent|machine|all]",
95
96
  " ouro chat <agent>",
96
97
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
97
98
  " ouro poke <agent> --task <task-id>",
@@ -544,6 +545,7 @@ function parseVaultConfigCommand(args) {
544
545
  const { agent, rest } = extractAgentFlag(args.slice(1));
545
546
  let key;
546
547
  let value;
548
+ let scope;
547
549
  for (let i = 0; i < rest.length; i += 1) {
548
550
  const token = rest[i];
549
551
  if (token === "--key") {
@@ -556,6 +558,15 @@ function parseVaultConfigCommand(args) {
556
558
  i += 1;
557
559
  continue;
558
560
  }
561
+ if (token === "--scope") {
562
+ const raw = rest[i + 1];
563
+ if (raw !== "agent" && raw !== "machine" && raw !== "all") {
564
+ throw new Error("vault config --scope must be agent, machine, or all");
565
+ }
566
+ scope = raw;
567
+ i += 1;
568
+ continue;
569
+ }
559
570
  throw new Error("Usage: ouro vault config set --agent <name> --key <path> [--value <value>] OR ouro vault config status --agent <name>");
560
571
  }
561
572
  if (!agent || (sub !== "set" && sub !== "status")) {
@@ -565,12 +576,32 @@ function parseVaultConfigCommand(args) {
565
576
  if (key || value) {
566
577
  throw new Error("Usage: ouro vault config status --agent <name>");
567
578
  }
568
- return { kind: "vault.config.status", agent };
579
+ return { kind: "vault.config.status", agent, ...(scope ? { scope } : {}) };
569
580
  }
581
+ if (scope === "all")
582
+ throw new Error("vault config --scope all is only valid for status");
570
583
  if (!key) {
571
584
  throw new Error("Usage: ouro vault config set --agent <name> --key <path> [--value <value>]");
572
585
  }
573
- return { kind: "vault.config.set", agent, key, ...(value !== undefined ? { value } : {}) };
586
+ return { kind: "vault.config.set", agent, key, ...(value !== undefined ? { value } : {}), ...(scope ? { scope } : {}) };
587
+ }
588
+ function normalizeConnectTarget(value) {
589
+ if (!value)
590
+ return undefined;
591
+ if (value === "perplexity" || value === "perplexity-search")
592
+ return "perplexity";
593
+ if (value === "bluebubbles" || value === "imessage" || value === "messages")
594
+ return "bluebubbles";
595
+ throw new Error("Usage: ouro connect [perplexity|bluebubbles] --agent <name>");
596
+ }
597
+ function parseConnectCommand(args) {
598
+ const { agent, rest } = extractAgentFlag(args);
599
+ if (!agent)
600
+ throw new Error("Usage: ouro connect --agent <name> [perplexity|bluebubbles]");
601
+ if (rest.length > 1)
602
+ throw new Error("Usage: ouro connect [perplexity|bluebubbles] --agent <name>");
603
+ const target = normalizeConnectTarget(rest[0]);
604
+ return { kind: "connect", agent, ...(target ? { target } : {}) };
574
605
  }
575
606
  function parseProviderUseCommand(args) {
576
607
  const { agent, rest: afterAgent } = extractAgentFlag(args);
@@ -1053,6 +1084,8 @@ function parseOuroCommand(args) {
1053
1084
  return parseHatchCommand(args.slice(1));
1054
1085
  if (head === "auth")
1055
1086
  return parseAuthCommand(args.slice(1));
1087
+ if (head === "connect")
1088
+ return parseConnectCommand(args.slice(1));
1056
1089
  if (head === "vault")
1057
1090
  return parseVaultCommand(args.slice(1));
1058
1091
  if (head === "task")
@@ -248,6 +248,7 @@ function statusDot(status) {
248
248
  case "failed":
249
249
  return red("●");
250
250
  case "needs_config":
251
+ case "not_attached":
251
252
  case "stale":
252
253
  return yellow("●");
253
254
  case "disabled":
@@ -19,6 +19,7 @@ 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
21
  const runtime_credentials_1 = require("../runtime-credentials");
22
+ const machine_identity_1 = require("../machine-identity");
22
23
  const DEFAULT_BLUEBUBBLES_REQUEST_TIMEOUT_MS = 30_000;
23
24
  // ── Category checkers ──
24
25
  function checkCliPath(deps) {
@@ -201,14 +202,21 @@ async function checkSenses(deps) {
201
202
  });
202
203
  }
203
204
  if (sense === "bluebubbles" && senseObj.enabled === true) {
204
- const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agentName, { preserveCachedOnFailure: true });
205
+ const machineId = (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: deps.homedir }).machineId;
206
+ const runtimeConfig = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agentName, machineId, { preserveCachedOnFailure: true });
205
207
  if (!runtimeConfig.ok) {
208
+ if (runtimeConfig.reason === "missing") {
209
+ checks.push({
210
+ label: `${agentDir} bluebubbles config`,
211
+ status: "pass",
212
+ detail: "not attached on this machine",
213
+ });
214
+ continue;
215
+ }
206
216
  checks.push({
207
217
  label: `${agentDir} bluebubbles config`,
208
218
  status: "fail",
209
- detail: runtimeConfig.reason === "missing"
210
- ? "missing vault runtime/config"
211
- : `vault runtime/config unavailable: ${runtimeConfig.error}`,
219
+ detail: `machine runtime config unavailable: ${runtimeConfig.error}`,
212
220
  });
213
221
  continue;
214
222
  }
@@ -158,6 +158,19 @@ class DaemonProcessManager {
158
158
  state.snapshot.status = "starting";
159
159
  if (this.configCheckFn) {
160
160
  const result = await this.configCheckFn(agent);
161
+ if (result.skip) {
162
+ state.snapshot.status = "stopped";
163
+ state.snapshot.errorReason = null;
164
+ state.snapshot.fixHint = null;
165
+ (0, runtime_1.emitNervesEvent)({
166
+ component: "daemon",
167
+ event: "daemon.agent_config_skipped",
168
+ message: result.error ?? "agent start skipped by config check",
169
+ meta: { agent, fix: result.fix ?? null },
170
+ });
171
+ this.notifySnapshotChange(state.snapshot);
172
+ return;
173
+ }
161
174
  if (!result.ok) {
162
175
  state.snapshot.status = "crashed";
163
176
  // Surface the error and fix to the snapshot so sibling agents can
@@ -41,6 +41,7 @@ const runtime_1 = require("../../nerves/runtime");
41
41
  const identity_1 = require("../identity");
42
42
  const runtime_credentials_1 = require("../runtime-credentials");
43
43
  const sense_truth_1 = require("../sense-truth");
44
+ const machine_identity_1 = require("../machine-identity");
44
45
  const process_manager_1 = require("./process-manager");
45
46
  const DEFAULT_TEAMS_PORT = 3978;
46
47
  const DEFAULT_BLUEBUBBLES_PORT = 18790;
@@ -106,11 +107,12 @@ function compactRuntimeConfigError(agent, error) {
106
107
  function runtimeConfigUnavailableDetail(agent, runtimeConfig) {
107
108
  if (runtimeConfig.ok)
108
109
  return "";
110
+ const itemName = /^vault:[^:]+:(.+)$/.exec(runtimeConfig.itemPath)?.[1] ?? "runtime/config";
109
111
  if (runtimeConfig.reason === "missing")
110
- return `missing vault runtime/config (${agent})`;
111
- return `vault runtime/config unavailable (${compactRuntimeConfigError(agent, runtimeConfig.error)})`;
112
+ return `missing vault ${itemName} (${agent})`;
113
+ return `vault ${itemName} unavailable (${compactRuntimeConfigError(agent, runtimeConfig.error)})`;
112
114
  }
113
- function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig) {
115
+ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntimeConfig = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent)) {
114
116
  const base = {
115
117
  cli: { configured: true, detail: "local interactive terminal" },
116
118
  teams: { configured: false, detail: "not enabled in agent.json" },
@@ -120,8 +122,9 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig) {
120
122
  const unavailableDetail = runtimeConfigUnavailableDetail(agent, runtimeConfig);
121
123
  const teams = payload.teams;
122
124
  const teamsChannel = payload.teamsChannel;
123
- const bluebubbles = payload.bluebubbles;
124
- const bluebubblesChannel = payload.bluebubblesChannel;
125
+ const machinePayload = machineRuntimeConfig.ok ? machineRuntimeConfig.config : {};
126
+ const bluebubbles = machinePayload.bluebubbles;
127
+ const bluebubblesChannel = machinePayload.bluebubblesChannel;
125
128
  if (senses.teams.enabled) {
126
129
  const missing = [];
127
130
  if (!textField(teams, "clientId"))
@@ -137,7 +140,9 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig) {
137
140
  }
138
141
  : {
139
142
  configured: false,
140
- detail: runtimeConfig.ok ? `missing ${missing.join("/")}` : unavailableDetail,
143
+ detail: runtimeConfig.ok
144
+ ? `missing ${missing.join("/")}`
145
+ : unavailableDetail,
141
146
  };
142
147
  }
143
148
  if (senses.bluebubbles.enabled) {
@@ -153,7 +158,12 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig) {
153
158
  }
154
159
  : {
155
160
  configured: false,
156
- detail: runtimeConfig.ok ? `missing ${missing.join("/")}` : unavailableDetail,
161
+ optional: !machineRuntimeConfig.ok && machineRuntimeConfig.reason === "missing",
162
+ detail: !machineRuntimeConfig.ok && machineRuntimeConfig.reason === "missing"
163
+ ? "not attached on this machine"
164
+ : machineRuntimeConfig.ok
165
+ ? `missing ${missing.join("/")}`
166
+ : runtimeConfigUnavailableDetail(agent, machineRuntimeConfig),
157
167
  };
158
168
  }
159
169
  return base;
@@ -162,7 +172,10 @@ function senseRepairHint(agent, sense) {
162
172
  if (sense === "teams") {
163
173
  return `Run 'ouro vault config set --agent ${agent} --key teams.clientId', teams.clientSecret, and teams.tenantId; then run 'ouro up' again.`;
164
174
  }
165
- return `Run 'ouro vault config set --agent ${agent} --key bluebubbles.serverUrl' and bluebubbles.password; then run 'ouro up' again.`;
175
+ return `Run 'ouro connect bluebubbles --agent ${agent}' to attach BlueBubbles on this machine; then run 'ouro up' again.`;
176
+ }
177
+ function currentMachineId() {
178
+ return (0, machine_identity_1.loadOrCreateMachineIdentity)({ homeDir: os.homedir() }).machineId;
166
179
  }
167
180
  function parseSenseSnapshotName(name) {
168
181
  const parts = name.split(":");
@@ -240,7 +253,7 @@ class DaemonSenseManager {
240
253
  this.bundlesRoot = bundlesRoot;
241
254
  this.contexts = new Map(options.agents.map((agent) => {
242
255
  const senses = readAgentSenses(path.join(bundlesRoot, `${agent}.ouro`, "agent.json"));
243
- const facts = senseFactsFromRuntimeConfig(agent, senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent));
256
+ const facts = senseFactsFromRuntimeConfig(agent, senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent));
244
257
  return [agent, { senses, facts }];
245
258
  }));
246
259
  const managedSenseAgents = [...this.contexts.entries()].flatMap(([agent, context]) => {
@@ -264,10 +277,20 @@ class DaemonSenseManager {
264
277
  if (!context)
265
278
  return { ok: true };
266
279
  const refreshed = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
267
- context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed);
280
+ const machineRefreshed = parsed.sense === "bluebubbles"
281
+ ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
282
+ : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
283
+ context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed, machineRefreshed);
268
284
  const fact = context.facts[parsed.sense];
269
285
  if (fact.configured)
270
286
  return { ok: true };
287
+ if (fact.optional) {
288
+ return {
289
+ ok: false,
290
+ skip: true,
291
+ error: `${parsed.sense} is enabled for ${parsed.agent} but not attached on this machine`,
292
+ };
293
+ }
271
294
  return {
272
295
  ok: false,
273
296
  error: `${parsed.sense} is enabled for ${parsed.agent} but runtime credentials are not ready: ${fact.detail}`,
@@ -309,7 +332,7 @@ class DaemonSenseManager {
309
332
  runtime.set(parsed.agent, current);
310
333
  }
311
334
  const rows = [...this.contexts.entries()].flatMap(([agent, context]) => {
312
- context.facts = senseFactsFromRuntimeConfig(agent, context.senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent));
335
+ context.facts = senseFactsFromRuntimeConfig(agent, context.senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent));
313
336
  const blueBubblesRuntimeFacts = readBlueBubblesRuntimeFacts(agent, this.bundlesRoot, runtime.get(agent)?.bluebubbles);
314
337
  const runtimeInfo = {
315
338
  cli: { configured: true },
@@ -319,6 +342,7 @@ class DaemonSenseManager {
319
342
  },
320
343
  bluebubbles: {
321
344
  configured: context.facts.bluebubbles.configured,
345
+ optional: context.facts.bluebubbles.optional,
322
346
  ...blueBubblesRuntimeFacts,
323
347
  },
324
348
  };
@@ -92,8 +92,8 @@ function buildSpecialistSystemPrompt(soulText, identityText, existingBundles, co
92
92
  "- `read_file`: Read a file from disk. Useful for reviewing existing agent bundles or migration sources.",
93
93
  "- `list_directory`: List directory contents. Useful for exploring existing agent bundles.",
94
94
  "- I also have the normal local harness tools when useful here, including `shell`, `ouro task create`, `ouro reminder create`, note tools, coding tools, and repo helpers.",
95
- "- `complete_adoption`: Finalize the bundle. Validates, asks the harness to collect the hatchling vault unlock secret through a hidden terminal prompt, scaffolds structural dirs, moves to ~/AgentBundles/, writes secrets, plays hatch animation. I call this with `name` (PascalCase) and `handoff_message` (warm message for the human).",
96
- "- The complete_adoption tool triggers a hidden terminal prompt for the hatchling vault unlock secret. I must never ask the human to type the vault unlock secret into chat, and I must never include it in tool arguments.",
95
+ "- `complete_adoption`: Finalize the bundle. Validates, asks the harness to collect and confirm the hatchling vault unlock secret through hidden terminal prompts, scaffolds structural dirs, moves to ~/AgentBundles/, writes secrets, plays hatch animation. I call this with `name` (PascalCase) and `handoff_message` (warm message for the human).",
96
+ "- The complete_adoption tool triggers hidden terminal prompts for the hatchling vault unlock secret. I must never ask the human to type the vault unlock secret into chat, and I must never include it in tool arguments.",
97
97
  "- `settle`: End the conversation with a final message. I call this after complete_adoption succeeds.",
98
98
  "",
99
99
  "I must call `settle` when I am done to end the session cleanly.",
@@ -174,14 +174,16 @@ async function execCompleteAdoption(args, deps) {
174
174
  const vault = (0, identity_1.resolveVaultConfig)(name);
175
175
  let vaultUnlockSecret;
176
176
  try {
177
- vaultUnlockSecret = (await deps.promptSecret(`Choose Ouro vault unlock secret for ${vault.email}: `)).trim();
177
+ vaultUnlockSecret = await (0, vault_unlock_1.promptConfirmedVaultUnlockSecret)({
178
+ promptSecret: deps.promptSecret,
179
+ question: `Choose Ouro vault unlock secret for ${vault.email}: `,
180
+ confirmQuestion: `Confirm Ouro vault unlock secret for ${vault.email}: `,
181
+ emptyError: "hatchling vault creation requires an unlock secret. Re-run `ouro hatch` in an interactive terminal and enter a human-chosen unlock secret.",
182
+ });
178
183
  }
179
184
  catch (error) {
180
185
  return `error: failed to read hatchling vault unlock secret: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
181
186
  }
182
- if (!vaultUnlockSecret) {
183
- return "error: hatchling vault creation requires an unlock secret. Re-run `ouro hatch` in an interactive terminal and enter a human-chosen unlock secret.";
184
- }
185
187
  // Scaffold structural dirs into tempDir
186
188
  scaffoldBundle(deps.tempDir);
187
189
  // Move tempDir -> final bundle location
@@ -33,18 +33,25 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.RUNTIME_CONFIG_ITEM_NAME = void 0;
36
+ exports.MACHINE_RUNTIME_CONFIG_ITEM_PREFIX = exports.RUNTIME_CONFIG_ITEM_NAME = void 0;
37
+ exports.machineRuntimeConfigItemName = machineRuntimeConfigItemName;
37
38
  exports.readRuntimeCredentialConfig = readRuntimeCredentialConfig;
39
+ exports.readMachineRuntimeCredentialConfig = readMachineRuntimeCredentialConfig;
38
40
  exports.cacheRuntimeCredentialConfig = cacheRuntimeCredentialConfig;
41
+ exports.cacheMachineRuntimeCredentialConfig = cacheMachineRuntimeCredentialConfig;
39
42
  exports.refreshRuntimeCredentialConfig = refreshRuntimeCredentialConfig;
43
+ exports.refreshMachineRuntimeCredentialConfig = refreshMachineRuntimeCredentialConfig;
40
44
  exports.upsertRuntimeCredentialConfig = upsertRuntimeCredentialConfig;
45
+ exports.upsertMachineRuntimeCredentialConfig = upsertMachineRuntimeCredentialConfig;
41
46
  exports.resetRuntimeCredentialConfigCache = resetRuntimeCredentialConfigCache;
42
47
  const crypto = __importStar(require("node:crypto"));
43
48
  const runtime_1 = require("../nerves/runtime");
44
49
  const credential_access_1 = require("../repertoire/credential-access");
45
50
  const identity_1 = require("./identity");
46
51
  exports.RUNTIME_CONFIG_ITEM_NAME = "runtime/config";
52
+ exports.MACHINE_RUNTIME_CONFIG_ITEM_PREFIX = "runtime/machines";
47
53
  let cachedRuntimeConfigs = new Map();
54
+ let cachedMachineRuntimeConfigs = new Map();
48
55
  function isRecord(value) {
49
56
  return !!value && typeof value === "object" && !Array.isArray(value);
50
57
  }
@@ -56,8 +63,14 @@ function stableJson(value) {
56
63
  }
57
64
  return JSON.stringify(value);
58
65
  }
59
- function runtimeConfigVaultPath(agentName) {
60
- return `vault:${agentName}:${exports.RUNTIME_CONFIG_ITEM_NAME}`;
66
+ function runtimeConfigVaultPath(agentName, itemName = exports.RUNTIME_CONFIG_ITEM_NAME) {
67
+ return `vault:${agentName}:${itemName}`;
68
+ }
69
+ function machineRuntimeConfigItemName(machineId) {
70
+ const normalized = machineId.trim();
71
+ if (!normalized)
72
+ throw new Error("machineId must be non-empty");
73
+ return `${exports.MACHINE_RUNTIME_CONFIG_ITEM_PREFIX}/${normalized}/config`;
61
74
  }
62
75
  function runtimeConfigRevision(payload) {
63
76
  return `runtime_${crypto
@@ -89,21 +102,45 @@ function resultFromPayload(agentName, payload) {
89
102
  updatedAt: payload.updatedAt,
90
103
  };
91
104
  }
92
- function missingRuntimeConfig(agentName) {
105
+ function resultFromPayloadForItem(agentName, itemName, payload) {
106
+ return {
107
+ ok: true,
108
+ itemPath: runtimeConfigVaultPath(agentName, itemName),
109
+ config: { ...payload.config },
110
+ revision: runtimeConfigRevision(payload),
111
+ updatedAt: payload.updatedAt,
112
+ };
113
+ }
114
+ function missingRuntimeConfig(agentName, itemName = exports.RUNTIME_CONFIG_ITEM_NAME) {
93
115
  return {
94
116
  ok: false,
95
117
  reason: "missing",
96
- itemPath: runtimeConfigVaultPath(agentName),
97
- error: `no runtime credentials stored at ${runtimeConfigVaultPath(agentName)}`,
118
+ itemPath: runtimeConfigVaultPath(agentName, itemName),
119
+ error: `no runtime credentials stored at ${runtimeConfigVaultPath(agentName, itemName)}`,
98
120
  };
99
121
  }
100
122
  function cacheResult(agentName, result) {
101
123
  cachedRuntimeConfigs.set(agentName, result);
102
124
  return result;
103
125
  }
126
+ function cacheMachineResult(agentName, result) {
127
+ cachedMachineRuntimeConfigs.set(agentName, result);
128
+ return result;
129
+ }
130
+ function missingMachineRuntimeConfig(agentName) {
131
+ return {
132
+ ok: false,
133
+ reason: "missing",
134
+ itemPath: `vault:${agentName}:${exports.MACHINE_RUNTIME_CONFIG_ITEM_PREFIX}/<this-machine>/config`,
135
+ error: `no machine runtime credentials loaded for ${agentName}`,
136
+ };
137
+ }
104
138
  function readRuntimeCredentialConfig(agentName = (0, identity_1.getAgentName)()) {
105
139
  return cachedRuntimeConfigs.get(agentName) ?? missingRuntimeConfig(agentName);
106
140
  }
141
+ function readMachineRuntimeCredentialConfig(agentName = (0, identity_1.getAgentName)()) {
142
+ return cachedMachineRuntimeConfigs.get(agentName) ?? missingMachineRuntimeConfig(agentName);
143
+ }
107
144
  function cacheRuntimeCredentialConfig(agentName, config, now = new Date()) {
108
145
  const payload = {
109
146
  schemaVersion: 1,
@@ -113,24 +150,35 @@ function cacheRuntimeCredentialConfig(agentName, config, now = new Date()) {
113
150
  };
114
151
  return cacheResult(agentName, resultFromPayload(agentName, payload));
115
152
  }
116
- async function refreshRuntimeCredentialConfig(agentName, options = {}) {
153
+ function cacheMachineRuntimeCredentialConfig(agentName, config, now = new Date(), machineId = "<this-machine>") {
154
+ const payload = {
155
+ schemaVersion: 1,
156
+ kind: "runtime-config",
157
+ updatedAt: now.toISOString(),
158
+ config: { ...config },
159
+ };
160
+ return cacheMachineResult(agentName, resultFromPayloadForItem(agentName, machineRuntimeConfigItemName(machineId), payload));
161
+ }
162
+ async function refreshRuntimeCredentialConfigItem(agentName, itemName, cache, options = {}) {
117
163
  try {
118
164
  const store = (0, credential_access_1.getCredentialStore)(agentName);
119
- const raw = await store.getRawSecret(exports.RUNTIME_CONFIG_ITEM_NAME, "password");
165
+ const raw = await store.getRawSecret(itemName, "password");
120
166
  const payload = validateRuntimeCredentialPayload(JSON.parse(raw));
121
- const result = resultFromPayload(agentName, payload);
167
+ const result = resultFromPayloadForItem(agentName, itemName, payload);
122
168
  (0, runtime_1.emitNervesEvent)({
123
169
  component: "config/identity",
124
170
  event: "config.runtime_credentials_loaded",
125
171
  message: "loaded runtime credentials from vault",
126
172
  meta: { agentName, itemPath: result.itemPath, revision: result.revision },
127
173
  });
128
- return cacheResult(agentName, result);
174
+ return cache(agentName, result);
129
175
  }
130
176
  catch (error) {
131
177
  const message = error instanceof Error ? error.message : String(error);
132
- const cached = cachedRuntimeConfigs.get(agentName);
133
- const reason = message.includes(`no credential found for domain "${exports.RUNTIME_CONFIG_ITEM_NAME}"`)
178
+ const existing = cache === cacheMachineResult
179
+ ? cachedMachineRuntimeConfigs.get(agentName)
180
+ : cachedRuntimeConfigs.get(agentName);
181
+ const reason = message.includes(`no credential found for domain "${itemName}"`)
134
182
  ? "missing"
135
183
  : message.includes("runtime credential payload")
136
184
  ? "invalid"
@@ -138,8 +186,8 @@ async function refreshRuntimeCredentialConfig(agentName, options = {}) {
138
186
  const result = {
139
187
  ok: false,
140
188
  reason,
141
- itemPath: runtimeConfigVaultPath(agentName),
142
- error: reason === "missing" ? `no runtime credentials stored at ${runtimeConfigVaultPath(agentName)}` : message,
189
+ itemPath: runtimeConfigVaultPath(agentName, itemName),
190
+ error: reason === "missing" ? `no runtime credentials stored at ${runtimeConfigVaultPath(agentName, itemName)}` : message,
143
191
  };
144
192
  (0, runtime_1.emitNervesEvent)({
145
193
  level: reason === "missing" ? "warn" : "error",
@@ -148,11 +196,17 @@ async function refreshRuntimeCredentialConfig(agentName, options = {}) {
148
196
  message: "runtime credentials unavailable",
149
197
  meta: { agentName, reason, itemPath: result.itemPath },
150
198
  });
151
- if (options.preserveCachedOnFailure && cached?.ok)
152
- return cached;
153
- return cacheResult(agentName, result);
199
+ if (options.preserveCachedOnFailure && existing?.ok)
200
+ return existing;
201
+ return cache(agentName, result);
154
202
  }
155
203
  }
204
+ async function refreshRuntimeCredentialConfig(agentName, options = {}) {
205
+ return refreshRuntimeCredentialConfigItem(agentName, exports.RUNTIME_CONFIG_ITEM_NAME, cacheResult, options);
206
+ }
207
+ async function refreshMachineRuntimeCredentialConfig(agentName, machineId, options = {}) {
208
+ return refreshRuntimeCredentialConfigItem(agentName, machineRuntimeConfigItemName(machineId), cacheMachineResult, options);
209
+ }
156
210
  async function upsertRuntimeCredentialConfig(agentName, config, now = new Date()) {
157
211
  const payload = {
158
212
  schemaVersion: 1,
@@ -176,6 +230,31 @@ async function upsertRuntimeCredentialConfig(agentName, config, now = new Date()
176
230
  cacheResult(agentName, result);
177
231
  return result;
178
232
  }
233
+ async function upsertMachineRuntimeCredentialConfig(agentName, machineId, config, now = new Date()) {
234
+ const payload = {
235
+ schemaVersion: 1,
236
+ kind: "runtime-config",
237
+ updatedAt: now.toISOString(),
238
+ config: { ...config },
239
+ };
240
+ const itemName = machineRuntimeConfigItemName(machineId);
241
+ const store = (0, credential_access_1.getCredentialStore)(agentName);
242
+ await store.store(itemName, {
243
+ username: itemName,
244
+ password: JSON.stringify(payload),
245
+ notes: "Ouro machine-local runtime credentials for senses attached to one machine. Portable runtime credentials live in runtime/config.",
246
+ });
247
+ const result = resultFromPayloadForItem(agentName, itemName, payload);
248
+ (0, runtime_1.emitNervesEvent)({
249
+ component: "config/identity",
250
+ event: "config.runtime_credentials_upserted",
251
+ message: "upserted machine runtime credential config in vault",
252
+ meta: { agentName, itemPath: result.itemPath, revision: result.revision },
253
+ });
254
+ cacheMachineResult(agentName, result);
255
+ return result;
256
+ }
179
257
  function resetRuntimeCredentialConfigCache() {
180
258
  cachedRuntimeConfigs = new Map();
259
+ cachedMachineRuntimeConfigs = new Map();
181
260
  }
@@ -28,6 +28,9 @@ function resolveStatus(enabled, daemonManaged, runtimeInfo) {
28
28
  if (runtimeInfo?.runtime === "running") {
29
29
  return "running";
30
30
  }
31
+ if (runtimeInfo?.configured === false && runtimeInfo.optional) {
32
+ return "not_attached";
33
+ }
31
34
  if (runtimeInfo?.configured === false) {
32
35
  return "needs_config";
33
36
  }
@@ -162,7 +162,7 @@ function readSenseStatusLines() {
162
162
  },
163
163
  {
164
164
  label: "BlueBubbles",
165
- status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "needs_config",
165
+ status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "not_attached",
166
166
  },
167
167
  ];
168
168
  return rows.map((row) => `- ${row.label}: ${row.status}`);
@@ -433,7 +433,7 @@ function localSenseStatusLines() {
433
433
  },
434
434
  {
435
435
  label: "BlueBubbles",
436
- status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "needs_config",
436
+ status: !senses.bluebubbles.enabled ? "disabled" : configured.bluebubbles ? "ready" : "not_attached",
437
437
  },
438
438
  ];
439
439
  _senseStatusLinesCache = rows.map((row) => `- ${row.label}: ${row.status}`);
@@ -446,12 +446,13 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
446
446
  lines.push("- interactive = available when opened by the user instead of kept running by the daemon");
447
447
  lines.push("- disabled = turned off in agent.json");
448
448
  lines.push("- needs_config = enabled but missing required vault runtime/config values");
449
+ lines.push("- not_attached = enabled globally but no local-machine attachment is configured here");
449
450
  lines.push("- ready = enabled and configured; `ouro up` should bring it online");
450
451
  lines.push("- running = enabled and currently active");
451
452
  lines.push("- error = enabled but unhealthy");
452
453
  lines.push("If asked how to enable another sense, I explain the relevant agent.json senses entry and required agent-vault runtime/config fields instead of guessing.");
453
454
  lines.push("teams setup truth: enable `senses.teams.enabled`, then store `teams.clientId`, `teams.clientSecret`, and `teams.tenantId` in the agent vault runtime/config item.");
454
- lines.push("bluebubbles setup truth: enable `senses.bluebubbles.enabled`, then store `bluebubbles.serverUrl` and `bluebubbles.password` in the agent vault runtime/config item.");
455
+ lines.push("bluebubbles setup truth: run `ouro connect bluebubbles --agent <agent>`; it stores this machine's BlueBubbles URL/password/listener config in the agent vault machine runtime item.");
455
456
  if (channel === "cli") {
456
457
  lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
457
458
  }
@@ -37,6 +37,7 @@ exports.vaultUnlockReplaceRecoverFix = vaultUnlockReplaceRecoverFix;
37
37
  exports.credentialVaultNotConfiguredError = credentialVaultNotConfiguredError;
38
38
  exports.isCredentialVaultNotConfiguredError = isCredentialVaultNotConfiguredError;
39
39
  exports.vaultCreateRecoverFix = vaultCreateRecoverFix;
40
+ exports.promptConfirmedVaultUnlockSecret = promptConfirmedVaultUnlockSecret;
40
41
  exports.resolveVaultUnlockStore = resolveVaultUnlockStore;
41
42
  exports.readVaultUnlockSecret = readVaultUnlockSecret;
42
43
  exports.storeVaultUnlockSecret = storeVaultUnlockSecret;
@@ -122,6 +123,35 @@ function vaultCreateRecoverFix(agentName, nextStep = "Then run 'ouro up' again."
122
123
  nextStep,
123
124
  ].join(" ");
124
125
  }
126
+ function vaultUnlockSecretStrengthIssues(secret) {
127
+ const issues = [];
128
+ if (secret.length < 8)
129
+ issues.push("at least 8 characters");
130
+ if (!/[a-z]/.test(secret))
131
+ issues.push("a lowercase letter");
132
+ if (!/[A-Z]/.test(secret))
133
+ issues.push("an uppercase letter");
134
+ if (!/[0-9]/.test(secret))
135
+ issues.push("a number");
136
+ if (!/[^A-Za-z0-9]/.test(secret))
137
+ issues.push("a special character");
138
+ return issues;
139
+ }
140
+ async function promptConfirmedVaultUnlockSecret(input) {
141
+ const secret = (await input.promptSecret(input.question)).trim();
142
+ if (!secret) {
143
+ throw new Error(input.emptyError);
144
+ }
145
+ const issues = vaultUnlockSecretStrengthIssues(secret);
146
+ if (issues.length > 0) {
147
+ throw new Error(`vault unlock secret is too weak: add ${issues.join(", ")}. Use at least 8 characters with uppercase and lowercase letters, one number, and one special character.`);
148
+ }
149
+ const confirmation = (await input.promptSecret(input.confirmQuestion)).trim();
150
+ if (secret !== confirmation) {
151
+ throw new Error("vault unlock secrets did not match. Re-run the command and enter the same secret twice.");
152
+ }
153
+ return secret;
154
+ }
125
155
  function lostUnlockSecretGuidance(config) {
126
156
  if (!config.agentName) {
127
157
  return "If nobody saved that unlock secret, run `ouro vault replace --agent <agent>` to create a new empty vault and re-enter credentials. If you do have a local JSON credential export, run `ouro vault recover --agent <agent> --from <json>` to import it.";