@ouro.bot/cli 0.1.0-alpha.363 → 0.1.0-alpha.365

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.
Files changed (44) hide show
  1. package/README.md +16 -7
  2. package/changelog.json +17 -0
  3. package/dist/heart/auth/auth-flow.js +25 -110
  4. package/dist/heart/config.js +69 -55
  5. package/dist/heart/core.js +83 -33
  6. package/dist/heart/daemon/agent-config-check.js +41 -238
  7. package/dist/heart/daemon/agentic-repair.js +1 -1
  8. package/dist/heart/daemon/cli-defaults.js +15 -68
  9. package/dist/heart/daemon/cli-exec.js +334 -102
  10. package/dist/heart/daemon/cli-parse.js +71 -0
  11. package/dist/heart/daemon/daemon-cli.js +1 -2
  12. package/dist/heart/daemon/daemon-entry.js +1 -3
  13. package/dist/heart/daemon/doctor.js +9 -29
  14. package/dist/heart/daemon/provider-discovery.js +32 -59
  15. package/dist/heart/hatch/hatch-flow.js +9 -12
  16. package/dist/heart/hatch/specialist-prompt.js +1 -1
  17. package/dist/heart/hatch/specialist-tools.js +21 -1
  18. package/dist/heart/migrate-config.js +15 -42
  19. package/dist/heart/provider-binding-resolver.js +6 -7
  20. package/dist/heart/provider-credentials.js +379 -0
  21. package/dist/heart/provider-failover.js +3 -11
  22. package/dist/heart/provider-ping.js +13 -3
  23. package/dist/heart/provider-state.js +8 -0
  24. package/dist/heart/provider-visibility.js +3 -6
  25. package/dist/heart/providers/anthropic-token.js +15 -47
  26. package/dist/heart/providers/anthropic.js +4 -9
  27. package/dist/heart/providers/azure.js +3 -3
  28. package/dist/heart/providers/github-copilot.js +2 -2
  29. package/dist/heart/providers/minimax-vlm.js +2 -2
  30. package/dist/heart/providers/minimax.js +1 -1
  31. package/dist/heart/providers/openai-codex.js +4 -9
  32. package/dist/heart/versioning/ouro-path-installer.js +10 -5
  33. package/dist/mind/prompt.js +1 -1
  34. package/dist/repertoire/bitwarden-store.js +63 -17
  35. package/dist/repertoire/bundle-templates.js +2 -2
  36. package/dist/repertoire/credential-access.js +47 -467
  37. package/dist/repertoire/tools-attachments.js +5 -4
  38. package/dist/repertoire/tools-vault.js +10 -80
  39. package/dist/repertoire/vault-unlock.js +359 -0
  40. package/dist/senses/bluebubbles/client.js +39 -4
  41. package/dist/senses/pipeline.js +0 -1
  42. package/package.json +1 -1
  43. package/skills/configure-dev-tools.md +10 -0
  44. package/dist/heart/provider-credential-pool.js +0 -395
package/README.md CHANGED
@@ -11,8 +11,8 @@ Ouroboros is a TypeScript harness for daemon-managed agents that live in externa
11
11
  - `ouro up` starts the daemon from the installed production version, syncs the launcher, installs workflow helpers, and reconciles stale runtime state.
12
12
  - `ouro dev` starts the daemon from a local repo build. It auto-builds from source, disables launchd auto-restart (so the installed daemon doesn't respawn underneath you), persists the repo path in `~/.ouro-cli/dev-config.json` for next time, and force-restarts the daemon. If you run `ouro dev` from inside the repo, it detects the CWD automatically. Run `ouro up` to return to production mode (this also cleans up `dev-config.json`).
13
13
  - Agent bundles live outside the repo at `~/AgentBundles/<agent>.ouro/`.
14
- - Provider credentials live outside the repo at `~/.agentsecrets/providers.json`.
15
- - Sense-specific secrets live outside the repo at `~/.agentsecrets/<agent>/secrets.json`.
14
+ - Provider credentials live in the owning agent's Bitwarden/Vaultwarden vault. Tool/sense credential vault migration is a follow-up.
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
 
18
18
  Current first-class senses:
@@ -91,10 +91,11 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
91
91
 
92
92
  ## Runtime Truths
93
93
 
94
- - `agent.json` is the source of truth for phrase pools, context settings, enabled senses, and the agent's `configPath`. Legacy `humanFacing`/`agentFacing` values are bootstrap inputs, not live machine fallback.
95
- - `configPath` must point to `~/.agentsecrets/<agent>/secrets.json` for sense-specific secrets.
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.
96
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.
97
- - `~/.agentsecrets/providers.json` is the machine credential pool. It stores provider credentials only, records which agent contributed them, and is shared by all agents on that machine.
96
+ - Each agent has one credential vault for provider credentials. There is no machine-wide provider credential pool.
97
+ - Vault unlock material is local machine state. Prefer macOS Keychain, Windows DPAPI, or Linux Secret Service; plaintext fallback is allowed only by explicit human choice.
98
+ - Provider credentials are loaded into daemon memory at startup/auth/unlock/refresh and reused. The remote vault is not queried for every model request.
98
99
  - The daemon discovers bundles dynamically from `~/AgentBundles`.
99
100
  - `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
100
101
  - `bundle-meta.json` tracks the runtime version that last touched a bundle.
@@ -114,7 +115,7 @@ ouro auth --agent <name>
114
115
  ouro auth --agent <name> --provider <provider>
115
116
  ```
116
117
 
117
- `ouro auth` stores credentials only. It does not switch a lane or write provider/model selection.
118
+ `ouro auth` stores credentials in the owning agent's vault. It does not switch a lane or write provider/model selection.
118
119
 
119
120
  When you want this machine to use a provider/model for a lane, use:
120
121
 
@@ -124,6 +125,8 @@ ouro use --agent <name> --lane <outward|inner> --provider <provider> --model <mo
124
125
 
125
126
  The outward lane handles user-facing senses. The inner lane handles the agent's private thinking. `ouro use` performs the provider/model check before committing the lane, so a broken local choice fails fast with a repair path instead of surprising the next turn.
126
127
 
128
+ For the full locked auth/provider contract, including refresh, repair actors, caching, and SerpentGuide hatch bootstrap, see `docs/auth-and-providers.md`.
129
+
127
130
  ## Quickstart
128
131
 
129
132
  ### Use The Published Runtime
@@ -165,8 +168,12 @@ ouro dev --clone # clone repo to ~/Projects/ouroboros, build, st
165
168
  ouro status
166
169
  ouro logs
167
170
  ouro stop
171
+ ouro vault unlock --agent <name>
172
+ ouro vault status --agent <name>
168
173
  ouro auth --agent <name>
169
174
  ouro auth --agent <name> --provider <provider>
175
+ ouro auth verify --agent <name> [--provider <provider>]
176
+ ouro provider refresh --agent <name>
170
177
  ouro use --agent <name> --lane <outward|inner> --provider <provider> --model <model>
171
178
  ouro hatch
172
179
  ouro clone <remote> [--agent <name>] # clone an existing agent from a git remote (see docs/cross-machine-setup.md)
@@ -186,7 +193,7 @@ ouro hook <event> --agent <name> # fire a lifecycle hook (SessionStart,
186
193
 
187
194
  ## Setting Up On Another Machine
188
195
 
189
- 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, run `ouro auth run`, then `ouro up`.
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, and follow the guided prompts (auth, daemon start, dev tool setup are all offered inline).
190
197
 
191
198
  ## The Agent's Inner Life
192
199
 
@@ -233,6 +240,8 @@ See `skills/configure-dev-tools.md` for the full tool inventory and troubleshoot
233
240
  Current daemon, bundle, sense, and update model.
234
241
  - `docs/testing-guide.md`
235
242
  Operator smoke flow for bootstrap, daemon, hatch, chat, and messaging.
243
+ - `docs/auth-and-providers.md`
244
+ Locked credential, provider selection, refresh, repair, and hatch bootstrap contract.
236
245
 
237
246
  ## A Note To Future Maintainers
238
247
 
package/changelog.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.365",
6
+ "changes": [
7
+ "Provider credentials now live in the owning agent's Bitwarden/Vaultwarden vault instead of a machine-wide provider pool; runtime uses daemon-memory credential snapshots and machine-local `state/providers.json` provider/model lanes.",
8
+ "`ouro auth`, `ouro use`, provider status/check/refresh, startup health checks, interactive repair, and failover guidance now share the same agent-vault/local-lane contract with clearer human repair paths.",
9
+ "SerpentGuide bootstrap no longer persists its own provider credentials; hatch flows keep bootstrap credentials in memory and store them only in the hatchling agent vault.",
10
+ "Vault unlock bootstrap supports macOS Keychain, Windows DPAPI, Linux Secret Service, and explicit plaintext fallback, with docs aligned across AGENTS, README, OAuth setup, testing guide, and auth/provider reference."
11
+ ]
12
+ },
13
+ {
14
+ "version": "0.1.0-alpha.364",
15
+ "changes": [
16
+ "Cross-machine polish: bash PATH writes to .bashrc on Linux/WSL instead of .bash_profile (which non-login shells skip on Debian/Ubuntu). Shell hint message matches.",
17
+ "Agent prompt: never guess about harness behavior — consult docs first, investigate in code, fix stale docs via PR.",
18
+ "Agent prompt: harness docs pointer distinguishes dev mode (local read) vs production (fetch from GitHub)."
19
+ ]
20
+ },
4
21
  {
5
22
  "version": "0.1.0-alpha.363",
6
23
  "changes": [
@@ -35,8 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.readAgentConfigForAgent = readAgentConfigForAgent;
37
37
  exports.writeAgentProviderSelection = writeAgentProviderSelection;
38
- exports.loadAgentSecrets = loadAgentSecrets;
39
- exports.writeProviderCredentials = writeProviderCredentials;
38
+ exports.storeProviderCredentials = storeProviderCredentials;
40
39
  exports.writeAgentModel = writeAgentModel;
41
40
  exports.collectRuntimeAuthCredentials = collectRuntimeAuthCredentials;
42
41
  exports.resolveHatchCredentials = resolveHatchCredentials;
@@ -49,71 +48,13 @@ const runtime_1 = require("../../nerves/runtime");
49
48
  const identity_1 = require("../identity");
50
49
  const migrate_config_1 = require("../migrate-config");
51
50
  const provider_models_1 = require("../provider-models");
51
+ const provider_credentials_1 = require("../provider-credentials");
52
52
  const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
53
53
  const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
54
- const DEFAULT_SECRETS_TEMPLATE = {
55
- providers: {
56
- azure: {
57
- apiKey: "",
58
- endpoint: "",
59
- deployment: "",
60
- apiVersion: "2025-04-01-preview",
61
- },
62
- minimax: {
63
- apiKey: "",
64
- },
65
- anthropic: {
66
- setupToken: "",
67
- refreshToken: "",
68
- expiresAt: 0,
69
- },
70
- "openai-codex": {
71
- oauthAccessToken: "",
72
- },
73
- "github-copilot": {
74
- githubToken: "",
75
- baseUrl: "",
76
- },
77
- },
78
- teams: {
79
- clientId: "",
80
- clientSecret: "",
81
- tenantId: "",
82
- },
83
- oauth: {
84
- graphConnectionName: "graph",
85
- adoConnectionName: "ado",
86
- githubConnectionName: "",
87
- },
88
- teamsChannel: {
89
- skipConfirmation: true,
90
- port: 3978,
91
- },
92
- vault: {
93
- masterPassword: "",
94
- },
95
- integrations: {
96
- perplexityApiKey: "",
97
- openaiEmbeddingsApiKey: "",
98
- },
99
- };
100
- function deepMerge(defaults, partial) {
101
- const result = { ...defaults };
102
- for (const key of Object.keys(partial)) {
103
- const left = result[key];
104
- const right = partial[key];
105
- if (right !== null &&
106
- typeof right === "object" &&
107
- !Array.isArray(right) &&
108
- left !== null &&
109
- typeof left === "object" &&
110
- !Array.isArray(left)) {
111
- result[key] = deepMerge(left, right);
112
- continue;
113
- }
114
- result[key] = right;
54
+ function assertPersistentProviderCredentialsAllowed(agentName) {
55
+ if (agentName === "SerpentGuide") {
56
+ throw new Error("SerpentGuide uses provider credentials in memory during hatch bootstrap; persistent SerpentGuide auth is not supported.");
115
57
  }
116
- return result;
117
58
  }
118
59
  function readJsonRecord(filePath, label) {
119
60
  try {
@@ -196,38 +137,18 @@ function writeAgentProviderSelection(agentName, facing, provider, bundlesRoot =
196
137
  });
197
138
  return configPath;
198
139
  }
199
- function resolveAgentSecretsPath(agentName, deps = {}) {
200
- if (deps.secretsRoot)
201
- return path.join(deps.secretsRoot, agentName, "secrets.json");
202
- const homeDir = deps.homeDir ?? os.homedir();
203
- return (0, identity_1.getAgentSecretsPath)(agentName).replace(os.homedir(), homeDir);
204
- }
205
- function loadAgentSecrets(agentName, deps = {}) {
206
- const secretsPath = resolveAgentSecretsPath(agentName, deps);
207
- const secretsDir = path.dirname(secretsPath);
208
- fs.mkdirSync(secretsDir, { recursive: true });
209
- let onDisk = {};
210
- try {
211
- onDisk = readJsonRecord(secretsPath, "secrets config");
212
- }
213
- catch (error) {
214
- const message = error.message;
215
- if (!message.includes("ENOENT"))
216
- throw error;
217
- }
218
- return {
219
- secretsPath,
220
- secrets: deepMerge(DEFAULT_SECRETS_TEMPLATE, onDisk),
221
- };
222
- }
223
- function writeSecrets(secretsPath, secrets) {
224
- fs.writeFileSync(secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, "utf8");
225
- }
226
- function writeProviderCredentials(agentName, provider, credentials, deps = {}) {
227
- const { secretsPath, secrets } = loadAgentSecrets(agentName, deps);
228
- applyCredentials(secrets, provider, credentials);
229
- writeSecrets(secretsPath, secrets);
230
- return { secretsPath, secrets };
140
+ async function storeProviderCredentials(agentName, provider, credentials, deps = {}) {
141
+ assertPersistentProviderCredentialsAllowed(agentName);
142
+ const split = (0, provider_credentials_1.splitProviderCredentialFields)(provider, credentials);
143
+ await (0, provider_credentials_1.upsertProviderCredential)({
144
+ agentName,
145
+ provider,
146
+ credentials: split.credentials,
147
+ config: split.config,
148
+ provenance: { source: "auth-flow" },
149
+ now: deps.now,
150
+ });
151
+ return { credentialPath: (0, provider_credentials_1.providerCredentialItemName)(provider) };
231
152
  }
232
153
  function writeAgentModel(agentName, facing, modelName, deps = {}) {
233
154
  const { configPath, config } = readAgentConfigForAgent(agentName, deps.bundlesRoot);
@@ -427,36 +348,30 @@ async function resolveHatchCredentials(input) {
427
348
  }
428
349
  return credentials;
429
350
  }
430
- function applyCredentials(secrets, provider, credentials) {
431
- const target = secrets.providers[provider];
432
- // Copy all non-empty credential fields to the provider's secrets block
433
- for (const [key, value] of Object.entries(credentials)) {
434
- /* v8 ignore next -- guard: skip null/empty fields from partial credential objects @preserve */
435
- if (value != null && value !== "") {
436
- target[key] = typeof value === "string" ? value.trim() : value;
437
- }
438
- }
439
- }
440
351
  async function runRuntimeAuthFlow(input, deps = {}) {
352
+ assertPersistentProviderCredentialsAllowed(input.agentName);
441
353
  (0, runtime_1.emitNervesEvent)({
442
354
  component: "daemon",
443
355
  event: "daemon.auth_flow_start",
444
356
  message: "starting runtime auth flow",
445
357
  meta: { agentName: input.agentName, provider: input.provider },
446
358
  });
447
- const homeDir = deps.homeDir ?? os.homedir();
359
+ const vault = await (0, provider_credentials_1.refreshProviderCredentialPool)(input.agentName);
360
+ if (!vault.ok && vault.reason === "unavailable") {
361
+ throw new Error(`${vault.error}\nRun \`ouro vault unlock --agent ${input.agentName}\`, then retry auth.`);
362
+ }
448
363
  const credentials = await collectRuntimeAuthCredentials(input, deps);
449
- const { secretsPath } = writeProviderCredentials(input.agentName, input.provider, credentials, { homeDir });
364
+ const { credentialPath } = await storeProviderCredentials(input.agentName, input.provider, credentials);
450
365
  (0, runtime_1.emitNervesEvent)({
451
366
  component: "daemon",
452
367
  event: "daemon.auth_flow_end",
453
368
  message: "completed runtime auth flow",
454
- meta: { agentName: input.agentName, provider: input.provider, secretsPath },
369
+ meta: { agentName: input.agentName, provider: input.provider, credentialPath },
455
370
  });
456
371
  return {
457
372
  agentName: input.agentName,
458
373
  provider: input.provider,
459
- secretsPath,
374
+ credentialPath,
460
375
  message: `authenticated ${input.agentName} with ${input.provider}`,
461
376
  credentials,
462
377
  };
@@ -41,7 +41,6 @@ exports.getMinimaxConfig = getMinimaxConfig;
41
41
  exports.getAnthropicConfig = getAnthropicConfig;
42
42
  exports.getOpenAICodexConfig = getOpenAICodexConfig;
43
43
  exports.getGithubCopilotConfig = getGithubCopilotConfig;
44
- exports.getProviderConfig = getProviderConfig;
45
44
  exports.getTeamsConfig = getTeamsConfig;
46
45
  exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
47
46
  exports.getContextConfig = getContextConfig;
@@ -62,30 +61,9 @@ exports.logPath = logPath;
62
61
  const fs = __importStar(require("fs"));
63
62
  const path = __importStar(require("path"));
64
63
  const identity_1 = require("./identity");
64
+ const provider_credentials_1 = require("./provider-credentials");
65
65
  const runtime_1 = require("../nerves/runtime");
66
- const DEFAULT_SECRETS_TEMPLATE = {
67
- providers: {
68
- azure: {
69
- apiKey: "",
70
- endpoint: "",
71
- deployment: "",
72
- apiVersion: "2025-04-01-preview",
73
- managedIdentityClientId: "",
74
- },
75
- minimax: {
76
- apiKey: "",
77
- },
78
- anthropic: {
79
- setupToken: "",
80
- },
81
- "openai-codex": {
82
- oauthAccessToken: "",
83
- },
84
- "github-copilot": {
85
- githubToken: "",
86
- baseUrl: "",
87
- },
88
- },
66
+ const DEFAULT_LOCAL_RUNTIME_CONFIG = {
89
67
  teams: {
90
68
  clientId: "",
91
69
  clientSecret: "",
@@ -127,26 +105,20 @@ const DEFAULT_SECRETS_TEMPLATE = {
127
105
  };
128
106
  function defaultRuntimeConfig() {
129
107
  return {
130
- providers: {
131
- azure: { ...DEFAULT_SECRETS_TEMPLATE.providers.azure },
132
- minimax: { ...DEFAULT_SECRETS_TEMPLATE.providers.minimax },
133
- anthropic: { ...DEFAULT_SECRETS_TEMPLATE.providers.anthropic },
134
- "openai-codex": { ...DEFAULT_SECRETS_TEMPLATE.providers["openai-codex"] },
135
- "github-copilot": { ...DEFAULT_SECRETS_TEMPLATE.providers["github-copilot"] },
136
- },
137
- teams: { ...DEFAULT_SECRETS_TEMPLATE.teams },
138
- teamsSecondary: { ...DEFAULT_SECRETS_TEMPLATE.teamsSecondary },
139
- oauth: { ...DEFAULT_SECRETS_TEMPLATE.oauth },
108
+ teams: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.teams },
109
+ teamsSecondary: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.teamsSecondary },
110
+ oauth: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.oauth },
140
111
  context: { ...identity_1.DEFAULT_AGENT_CONTEXT },
141
- teamsChannel: { ...DEFAULT_SECRETS_TEMPLATE.teamsChannel },
142
- bluebubbles: { ...DEFAULT_SECRETS_TEMPLATE.bluebubbles },
143
- bluebubblesChannel: { ...DEFAULT_SECRETS_TEMPLATE.bluebubblesChannel },
144
- vault: { ...DEFAULT_SECRETS_TEMPLATE.vault },
145
- integrations: { ...DEFAULT_SECRETS_TEMPLATE.integrations },
112
+ teamsChannel: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.teamsChannel },
113
+ bluebubbles: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.bluebubbles },
114
+ bluebubblesChannel: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.bluebubblesChannel },
115
+ vault: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.vault },
116
+ integrations: { ...DEFAULT_LOCAL_RUNTIME_CONFIG.integrations },
146
117
  };
147
118
  }
148
119
  let _runtimeConfigOverride = null;
149
120
  let _testContextOverride = null;
121
+ let _providerConfigOverride = null;
150
122
  function resolveConfigPath() {
151
123
  return (0, identity_1.getAgentSecretsPath)();
152
124
  }
@@ -185,7 +157,7 @@ function loadConfig() {
185
157
  : undefined;
186
158
  if (errorCode === "ENOENT") {
187
159
  try {
188
- fs.writeFileSync(configPath, JSON.stringify(DEFAULT_SECRETS_TEMPLATE, null, 2) + "\n", "utf-8");
160
+ fs.writeFileSync(configPath, JSON.stringify(DEFAULT_LOCAL_RUNTIME_CONFIG, null, 2) + "\n", "utf-8");
189
161
  }
190
162
  catch (writeError) {
191
163
  (0, runtime_1.emitNervesEvent)({
@@ -214,6 +186,19 @@ function loadConfig() {
214
186
  // ENOENT or parse error -- use defaults
215
187
  }
216
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
+ }
217
202
  if ("context" in sanitizedFileData) {
218
203
  delete sanitizedFileData.context;
219
204
  (0, runtime_1.emitNervesEvent)({
@@ -246,38 +231,67 @@ function loadConfig() {
246
231
  function resetConfigCache() {
247
232
  _runtimeConfigOverride = null;
248
233
  _testContextOverride = null;
234
+ _providerConfigOverride = null;
235
+ (0, provider_credentials_1.resetProviderCredentialCache)();
236
+ }
237
+ function seedProviderCredentialCache(providers) {
238
+ if (!providers)
239
+ return;
240
+ _providerConfigOverride = deepMerge((_providerConfigOverride ?? {}), providers);
241
+ const records = Object.entries(_providerConfigOverride).map(([provider, rawConfig]) => {
242
+ const split = (0, provider_credentials_1.splitProviderCredentialFields)(provider, rawConfig);
243
+ return (0, provider_credentials_1.createProviderCredentialRecord)({
244
+ provider: provider,
245
+ credentials: split.credentials,
246
+ config: split.config,
247
+ provenance: { source: "manual" },
248
+ now: new Date(0),
249
+ });
250
+ });
251
+ (0, provider_credentials_1.cacheProviderCredentialRecords)((0, identity_1.getAgentName)(), records, new Date(0));
249
252
  }
250
253
  function patchRuntimeConfig(partial) {
254
+ const { providers, ...runtimePartial } = partial;
255
+ seedProviderCredentialCache(providers);
251
256
  const contextPatch = partial.context;
252
257
  if (contextPatch) {
253
258
  const base = _testContextOverride ?? identity_1.DEFAULT_AGENT_CONTEXT;
254
259
  _testContextOverride = deepMerge(base, contextPatch);
255
260
  }
256
- _runtimeConfigOverride = deepMerge((_runtimeConfigOverride ?? {}), partial);
261
+ _runtimeConfigOverride = deepMerge((_runtimeConfigOverride ?? {}), runtimePartial);
262
+ }
263
+ function readProviderConfig(provider) {
264
+ const cached = (0, provider_credentials_1.readCachedProviderCredentialRecord)((0, identity_1.getAgentName)(), provider);
265
+ return cached.ok ? { ...cached.record.config, ...cached.record.credentials } : {};
257
266
  }
258
267
  function getAzureConfig() {
259
- const config = loadConfig();
260
- return { ...config.providers.azure };
268
+ const raw = readProviderConfig("azure");
269
+ return {
270
+ apiKey: typeof raw.apiKey === "string" ? raw.apiKey : "",
271
+ endpoint: typeof raw.endpoint === "string" ? raw.endpoint : "",
272
+ deployment: typeof raw.deployment === "string" ? raw.deployment : "",
273
+ apiVersion: typeof raw.apiVersion === "string" ? raw.apiVersion : "2025-04-01-preview",
274
+ managedIdentityClientId: typeof raw.managedIdentityClientId === "string" ? raw.managedIdentityClientId : "",
275
+ };
261
276
  }
262
277
  function getMinimaxConfig() {
263
- const config = loadConfig();
264
- return { ...config.providers.minimax };
278
+ const raw = readProviderConfig("minimax");
279
+ return { apiKey: typeof raw.apiKey === "string" ? raw.apiKey : "" };
265
280
  }
266
281
  function getAnthropicConfig() {
267
- const config = loadConfig();
268
- return { ...config.providers.anthropic };
282
+ const raw = readProviderConfig("anthropic");
283
+ return { setupToken: typeof raw.setupToken === "string" ? raw.setupToken : "" };
269
284
  }
270
285
  function getOpenAICodexConfig() {
271
- const config = loadConfig();
272
- return { ...config.providers["openai-codex"] };
286
+ const raw = readProviderConfig("openai-codex");
287
+ return { oauthAccessToken: typeof raw.oauthAccessToken === "string" ? raw.oauthAccessToken : "" };
273
288
  }
274
289
  function getGithubCopilotConfig() {
275
- const config = loadConfig();
276
- return { ...config.providers["github-copilot"] };
277
- }
278
- function getProviderConfig(provider) {
279
- const config = loadConfig();
280
- return { ...config.providers[provider] };
290
+ const raw = readProviderConfig("github-copilot");
291
+ return {
292
+ githubToken: typeof raw.githubToken === "string" ? raw.githubToken : "",
293
+ baseUrl: typeof raw.baseUrl === "string" ? raw.baseUrl : "",
294
+ };
281
295
  }
282
296
  function getTeamsConfig() {
283
297
  const config = loadConfig();
@@ -32,43 +32,80 @@ const tool_loop_1 = require("./tool-loop");
32
32
  const packets_1 = require("../arc/packets");
33
33
  const tool_friction_1 = require("./tool-friction");
34
34
  const provider_models_1 = require("./provider-models");
35
+ const provider_credentials_1 = require("./provider-credentials");
36
+ const provider_state_1 = require("./provider-state");
35
37
  const provider_attempt_1 = require("./provider-attempt");
36
38
  const _providerRuntimes = {
37
39
  human: null,
38
40
  agent: null,
39
41
  };
40
- function getProviderRuntimeFingerprint(facing) {
42
+ function providerLaneForFacing(facing) {
43
+ return facing === "human" ? "outward" : "inner";
44
+ }
45
+ function resolveRuntimeProviderBinding(facing) {
46
+ const agentName = (0, identity_2.getAgentName)();
47
+ const lane = providerLaneForFacing(facing);
48
+ const stateResult = (0, provider_state_1.readProviderState)((0, identity_2.getAgentRoot)(agentName));
49
+ if (stateResult.ok) {
50
+ const binding = stateResult.state.lanes[lane];
51
+ return { lane, provider: binding.provider, model: binding.model };
52
+ }
53
+ if (stateResult.reason === "invalid") {
54
+ throw new Error(`provider state for ${agentName} is invalid at ${stateResult.statePath}: ${stateResult.error}`);
55
+ }
56
+ // First-run and SerpentGuide bootstrap path. Daemon startup normally
57
+ // bootstraps state/providers.json from agent.json before model calls.
41
58
  const config = (0, identity_1.loadAgentConfig)();
42
59
  const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
43
- const provider = facingConfig.provider;
44
- const model = facingConfig.model;
45
- const providerConfig = (0, config_1.getProviderConfig)(provider);
46
- return JSON.stringify({ provider, model, ...providerConfig });
60
+ return { lane, provider: facingConfig.provider, model: facingConfig.model };
47
61
  }
48
- function createProviderRegistry() {
49
- const factories = {
50
- azure: azure_1.createAzureProviderRuntime,
51
- anthropic: anthropic_1.createAnthropicProviderRuntime,
52
- minimax: minimax_1.createMinimaxProviderRuntime,
53
- "openai-codex": openai_codex_1.createOpenAICodexProviderRuntime,
54
- "github-copilot": github_copilot_1.createGithubCopilotProviderRuntime,
62
+ async function getProviderRuntimeFingerprint(facing) {
63
+ const agentName = (0, identity_2.getAgentName)();
64
+ const binding = resolveRuntimeProviderBinding(facing);
65
+ const credential = await (0, provider_credentials_1.readProviderCredentialRecord)(agentName, binding.provider);
66
+ if (!credential.ok) {
67
+ throw new Error([
68
+ `${binding.lane} provider ${binding.provider} (${binding.model}) has no credentials for ${agentName}.`,
69
+ credential.error,
70
+ `Run \`ouro auth --agent ${agentName} --provider ${binding.provider}\`.`,
71
+ ].join("\n"));
72
+ }
73
+ return {
74
+ binding,
75
+ fingerprint: JSON.stringify({
76
+ lane: binding.lane,
77
+ provider: binding.provider,
78
+ model: binding.model,
79
+ credentialRevision: credential.record.revision,
80
+ }),
81
+ credential: credential.record,
55
82
  };
83
+ }
84
+ function createProviderRegistry() {
56
85
  return {
57
- resolve(provider, model) {
58
- const resolvedProvider = provider ?? (0, identity_1.loadAgentConfig)().humanFacing.provider;
59
- const resolvedModel = model ?? (0, identity_1.loadAgentConfig)().humanFacing.model;
60
- return factories[resolvedProvider](resolvedModel);
86
+ resolve(provider, model, credential) {
87
+ const providerConfig = { ...credential.config, ...credential.credentials };
88
+ switch (provider) {
89
+ case "azure":
90
+ return (0, azure_1.createAzureProviderRuntime)(model, providerConfig);
91
+ case "anthropic":
92
+ return (0, anthropic_1.createAnthropicProviderRuntime)(model, providerConfig);
93
+ case "minimax":
94
+ return (0, minimax_1.createMinimaxProviderRuntime)(model, providerConfig);
95
+ case "openai-codex":
96
+ return (0, openai_codex_1.createOpenAICodexProviderRuntime)(model, providerConfig);
97
+ case "github-copilot":
98
+ return (0, github_copilot_1.createGithubCopilotProviderRuntime)(model, providerConfig);
99
+ }
61
100
  },
62
101
  };
63
102
  }
64
- function getProviderRuntime(facing = "human") {
103
+ async function getProviderRuntime(facing = "human") {
65
104
  try {
66
- const fingerprint = getProviderRuntimeFingerprint(facing);
105
+ const { binding, fingerprint, credential } = await getProviderRuntimeFingerprint(facing);
67
106
  const cached = _providerRuntimes[facing];
68
107
  if (!cached || cached.fingerprint !== fingerprint) {
69
- const config = (0, identity_1.loadAgentConfig)();
70
- const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
71
- const runtime = createProviderRegistry().resolve(facingConfig.provider, facingConfig.model);
108
+ const runtime = createProviderRegistry().resolve(binding.provider, binding.model, credential);
72
109
  _providerRuntimes[facing] = runtime ? { fingerprint, runtime } : null;
73
110
  }
74
111
  }
@@ -109,14 +146,14 @@ function resetProviderRuntime() {
109
146
  _providerRuntimes.agent = null;
110
147
  }
111
148
  function getModel(facing = "human") {
112
- return getProviderRuntime(facing).model;
149
+ return resolveRuntimeProviderBinding(facing).model;
113
150
  }
114
151
  function getProvider(facing = "human") {
115
- return getProviderRuntime(facing).id;
152
+ return resolveRuntimeProviderBinding(facing).provider;
116
153
  }
117
154
  function createSummarize(facing = "human") {
118
155
  return async (transcript, instruction) => {
119
- const runtime = getProviderRuntime(facing);
156
+ const runtime = await getProviderRuntime(facing);
120
157
  const client = runtime.client;
121
158
  const response = await client.chat.completions.create({
122
159
  model: runtime.model,
@@ -130,14 +167,12 @@ function createSummarize(facing = "human") {
130
167
  };
131
168
  }
132
169
  function getProviderDisplayLabel(facing = "human") {
133
- const config = (0, identity_1.loadAgentConfig)();
134
- const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
135
- const provider = facingConfig.provider;
136
- const model = facingConfig.model || "unknown";
170
+ const binding = resolveRuntimeProviderBinding(facing);
171
+ const provider = binding.provider;
172
+ const model = binding.model || "unknown";
137
173
  const providerLabelBuilders = {
138
174
  azure: () => {
139
- const azureCfg = (0, config_1.getAzureConfig)();
140
- return `azure openai (${azureCfg.deployment || "default"}, model: ${model})`;
175
+ return `azure openai (model: ${model})`;
141
176
  },
142
177
  anthropic: () => `anthropic (${model})`,
143
178
  minimax: () => `minimax (${model})`,
@@ -426,7 +461,7 @@ function buildAuthFailureGuidance(provider, model, agentName, detail) {
426
461
  }
427
462
  async function runAgent(messages, callbacks, channel, signal, options) {
428
463
  const facing = (0, channel_1.channelToFacing)(channel);
429
- const providerRuntime = getProviderRuntime(facing);
464
+ let providerRuntime = await getProviderRuntime(facing);
430
465
  const provider = providerRuntime.id;
431
466
  const toolChoiceRequired = options?.toolChoiceRequired ?? true;
432
467
  const traceId = options?.traceId;
@@ -485,7 +520,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
485
520
  await (0, kept_notes_1.injectKeptNotes)(messages, {
486
521
  channel,
487
522
  friend: currentContext?.friend,
488
- judge: async (input) => (0, kept_notes_1.createKeptNotesJudge)(getProviderRuntime("agent"), signal)(input),
523
+ judge: async (input) => (0, kept_notes_1.createKeptNotesJudge)(await getProviderRuntime("agent"), signal)(input),
489
524
  signal,
490
525
  traceId,
491
526
  });
@@ -648,10 +683,25 @@ async function runAgent(messages, callbacks, channel, signal, options) {
648
683
  model: providerRuntime.model,
649
684
  run: callProviderTurnWithOverflowRecovery,
650
685
  classifyError: (error) => providerRuntime.classifyError(error),
651
- onRetry: (record, maxAttempts) => {
686
+ onRetry: async (record, maxAttempts) => {
652
687
  const delayMs = record.delayMs;
653
688
  const seconds = delayMs / 1000;
654
689
  const cause = RETRY_LABELS[record.classification];
690
+ try {
691
+ await (0, provider_credentials_1.refreshProviderCredentialPool)((0, identity_2.getAgentName)(), { preserveCachedOnFailure: true });
692
+ _providerRuntimes[facing] = null;
693
+ providerRuntime = await getProviderRuntime(facing);
694
+ providerRuntime.resetTurnState(messages);
695
+ }
696
+ catch (refreshError) {
697
+ (0, runtime_1.emitNervesEvent)({
698
+ level: "warn",
699
+ component: "engine",
700
+ event: "engine.provider_retry_refresh_failed",
701
+ message: "provider credential refresh failed during retry",
702
+ meta: { provider: record.provider, model: record.model, reason: refreshError instanceof Error ? refreshError.message : String(refreshError) },
703
+ });
704
+ }
655
705
  callbacks.onError(new Error(`${cause}, retrying in ${seconds}s (${record.attempt}/${maxAttempts})...`), "transient");
656
706
  },
657
707
  sleep: async (delayMs) => {