@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 +5 -3
- package/SerpentGuide.ouro/psyche/SOUL.md +1 -1
- package/changelog.json +9 -0
- package/dist/heart/agent-entry.js +22 -2
- package/dist/heart/config.js +19 -82
- package/dist/heart/daemon/agent-config-check.js +1 -7
- package/dist/heart/daemon/bluebubbles-health-diagnostics.js +1 -1
- package/dist/heart/daemon/cli-exec.js +108 -2
- package/dist/heart/daemon/cli-help.js +7 -0
- package/dist/heart/daemon/cli-parse.js +37 -0
- package/dist/heart/daemon/doctor.js +18 -32
- package/dist/heart/daemon/runtime-metadata.js +2 -30
- package/dist/heart/daemon/sense-manager.js +37 -27
- package/dist/heart/identity.js +0 -7
- package/dist/heart/runtime-credentials.js +181 -0
- package/dist/heart/turn-context.js +1 -12
- package/dist/mind/prompt.js +6 -15
- package/dist/repertoire/tools-notes.js +1 -1
- package/dist/repertoire/vault-unlock.js +1 -0
- package/dist/senses/bluebubbles/entry.js +60 -3
- package/dist/senses/cli-entry.js +60 -8
- package/dist/senses/teams-entry.js +60 -8
- package/package.json +1 -1
- package/skills/travel-planning.md +2 -6
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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("
|
|
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);
|
package/dist/heart/config.js
CHANGED
|
@@ -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
|
|
143
|
-
|
|
144
|
-
const
|
|
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
|
|
154
|
+
message: "config loaded from runtime credential cache",
|
|
223
155
|
meta: {
|
|
224
|
-
source: "
|
|
225
|
-
used_defaults_only:
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
"
|
|
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
|
|
212
|
-
if (!
|
|
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
|
|
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
|
|
221
|
-
|
|
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
|
|
306
|
-
if (deps.existsSync(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
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,
|
|
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",
|