@ouro.bot/cli 0.1.0-alpha.376 → 0.1.0-alpha.378
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 +7 -4
- package/SerpentGuide.ouro/psyche/SOUL.md +1 -1
- package/changelog.json +18 -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 +112 -6
- 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 +2 -1
- 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,8 +11,9 @@ 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
|
+
- The only Ouro-owned durable credential locations are the bundle and the agent vault. Local unlock material is a machine-local cache, not a credential source of truth.
|
|
16
17
|
- Machine-scoped test and runtime spillover lives under `~/.agentstate/...`.
|
|
17
18
|
|
|
18
19
|
Current first-class senses:
|
|
@@ -93,9 +94,9 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
|
|
|
93
94
|
|
|
94
95
|
- `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
96
|
- `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
|
|
97
|
+
- Each agent has one credential vault for provider, runtime, sense, integration, travel, and tool credentials. There is no machine-wide credential pool.
|
|
97
98
|
- 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
|
|
99
|
+
- 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
100
|
- The daemon discovers bundles dynamically from `~/AgentBundles`.
|
|
100
101
|
- `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
|
|
101
102
|
- `bundle-meta.json` tracks the runtime version that last touched a bundle.
|
|
@@ -170,6 +171,8 @@ ouro logs
|
|
|
170
171
|
ouro stop
|
|
171
172
|
ouro vault unlock --agent <name>
|
|
172
173
|
ouro vault status --agent <name>
|
|
174
|
+
ouro vault config set --agent <name> --key bluebubbles.password
|
|
175
|
+
ouro vault config status --agent <name>
|
|
173
176
|
ouro auth --agent <name>
|
|
174
177
|
ouro auth --agent <name> --provider <provider>
|
|
175
178
|
ouro auth verify --agent <name> [--provider <provider>]
|
|
@@ -193,7 +196,7 @@ ouro hook <event> --agent <name> # fire a lifecycle hook (SessionStart,
|
|
|
193
196
|
|
|
194
197
|
## Setting Up On Another Machine
|
|
195
198
|
|
|
196
|
-
To clone an existing agent onto a new machine (macOS, Linux, or Windows via WSL2), see **[docs/cross-machine-setup.md](docs/cross-machine-setup.md)**. The short version: `npx ouro.bot`, pick "clone", enter the bundle's git remote URL,
|
|
199
|
+
To clone an existing agent onto a new machine (macOS, Linux, or Windows via WSL2), see **[docs/cross-machine-setup.md](docs/cross-machine-setup.md)**. The short version is bundle plus vault: `npx ouro.bot`, pick "clone", enter the bundle's git remote URL, unlock the agent vault, refresh/verify credentials, and start with `ouro up`.
|
|
197
200
|
|
|
198
201
|
## The Agent's Inner Life
|
|
199
202
|
|
|
@@ -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,24 @@
|
|
|
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.378",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Existing-agent continuation docs now spell out the bundle plus vault flow: clone the bundle, unlock the agent vault, refresh and verify provider/runtime credentials, then run `ouro up`.",
|
|
8
|
+
"Locked-vault and vault-create guidance no longer names an operator password manager as an Ouro credential location; local unlock material is documented as a machine-local cache, not a source of truth.",
|
|
9
|
+
"`ouro clone` non-interactive next steps now lead with `ouro vault unlock`, `ouro provider refresh`, and `ouro auth verify` instead of stale `ouro auth run` guidance.",
|
|
10
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the existing-bundle vault continuation release."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"version": "0.1.0-alpha.377",
|
|
15
|
+
"changes": [
|
|
16
|
+
"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.",
|
|
17
|
+
"`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.",
|
|
18
|
+
"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.",
|
|
19
|
+
"`@ouro.bot/cli` and the `ouro.bot` wrapper are version-synced for the vault source-of-truth release."
|
|
20
|
+
]
|
|
21
|
+
},
|
|
4
22
|
{
|
|
5
23
|
"version": "0.1.0-alpha.376",
|
|
6
24
|
"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,15 +668,15 @@ 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
|
"",
|
|
674
675
|
`vault unlock secret: ${unlockSecret}`,
|
|
675
676
|
"",
|
|
676
|
-
"
|
|
677
|
+
"Keep this saved outside Ouro now. Another machine cannot unlock the vault without it.",
|
|
677
678
|
]
|
|
678
|
-
: ["
|
|
679
|
+
: ["Keep the vault unlock secret saved outside Ouro. Another machine will need it once."]),
|
|
679
680
|
].join("\n");
|
|
680
681
|
deps.writeStdout(message);
|
|
681
682
|
return message;
|
|
@@ -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
|
};
|
|
@@ -3034,7 +3140,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
3034
3140
|
/* v8 ignore start -- chained command failures: tested via interactive clone test, catch branches are defensive @preserve */
|
|
3035
3141
|
}
|
|
3036
3142
|
catch (e) {
|
|
3037
|
-
deps.writeStdout(`auth setup failed: ${e instanceof Error ? e.message : String(e)}\nYou can retry later with: ouro auth
|
|
3143
|
+
deps.writeStdout(`auth setup failed: ${e instanceof Error ? e.message : String(e)}\nYou can retry later with: ouro auth --agent ${agentName}`);
|
|
3038
3144
|
}
|
|
3039
3145
|
/* v8 ignore stop */
|
|
3040
3146
|
}
|
|
@@ -3066,7 +3172,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
|
|
|
3066
3172
|
}
|
|
3067
3173
|
}
|
|
3068
3174
|
else {
|
|
3069
|
-
deps.writeStdout(`\nnext steps:\n ouro auth
|
|
3175
|
+
deps.writeStdout(`\nnext steps:\n ouro vault unlock --agent ${agentName}\n ouro provider refresh --agent ${agentName}\n ouro auth verify --agent ${agentName}\n ouro up\n ouro setup --tool claude-code --agent ${agentName}`);
|
|
3070
3176
|
}
|
|
3071
3177
|
/* v8 ignore start -- PATH hint: only fires inside npx, not testable in vitest @preserve */
|
|
3072
3178
|
if (process.env.npm_execpath) {
|
|
@@ -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`;
|