@ouro.bot/cli 0.1.0-alpha.201 → 0.1.0-alpha.202
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/changelog.json +10 -0
- package/dist/heart/config.js +5 -0
- package/dist/heart/core.js +2 -25
- package/dist/heart/daemon/agent-config-check.js +3 -3
- package/dist/heart/daemon/auth-flow.js +42 -75
- package/dist/heart/daemon/daemon-cli.js +37 -67
- package/dist/heart/daemon/hatch-flow.js +1 -10
- package/dist/heart/daemon/process-manager.js +30 -0
- package/dist/heart/identity.js +9 -12
- package/dist/heart/provider-ping.js +3 -19
- package/dist/senses/cli/ouro-tui.js +11 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
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.202",
|
|
6
|
+
"changes": [
|
|
7
|
+
"refactor: unify hatch + runtime auth into single credential path (PROVIDER_CREDENTIALS table)",
|
|
8
|
+
"fix: github-copilot credential discovery from stored secrets and env vars",
|
|
9
|
+
"feat: ping-verify credentials before entering serpent guide session",
|
|
10
|
+
"fix: clean up React tooltip timeouts on unmount to prevent state update warning",
|
|
11
|
+
"fix: guard against undefined child from spawn to prevent unhandled rejections"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
4
14
|
{
|
|
5
15
|
"version": "0.1.0-alpha.201",
|
|
6
16
|
"changes": [
|
package/dist/heart/config.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.getMinimaxConfig = getMinimaxConfig;
|
|
|
41
41
|
exports.getAnthropicConfig = getAnthropicConfig;
|
|
42
42
|
exports.getOpenAICodexConfig = getOpenAICodexConfig;
|
|
43
43
|
exports.getGithubCopilotConfig = getGithubCopilotConfig;
|
|
44
|
+
exports.getProviderConfig = getProviderConfig;
|
|
44
45
|
exports.getTeamsConfig = getTeamsConfig;
|
|
45
46
|
exports.getTeamsSecondaryConfig = getTeamsSecondaryConfig;
|
|
46
47
|
exports.getContextConfig = getContextConfig;
|
|
@@ -270,6 +271,10 @@ function getGithubCopilotConfig() {
|
|
|
270
271
|
const config = loadConfig();
|
|
271
272
|
return { ...config.providers["github-copilot"] };
|
|
272
273
|
}
|
|
274
|
+
function getProviderConfig(provider) {
|
|
275
|
+
const config = loadConfig();
|
|
276
|
+
return { ...config.providers[provider] };
|
|
277
|
+
}
|
|
273
278
|
function getTeamsConfig() {
|
|
274
279
|
const config = loadConfig();
|
|
275
280
|
return { ...config.teams };
|
package/dist/heart/core.js
CHANGED
|
@@ -42,31 +42,8 @@ function getProviderRuntimeFingerprint(facing) {
|
|
|
42
42
|
const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
|
|
43
43
|
const provider = facingConfig.provider;
|
|
44
44
|
const model = facingConfig.model;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
case "azure": {
|
|
48
|
-
const { apiKey, endpoint, deployment, apiVersion, managedIdentityClientId } = (0, config_1.getAzureConfig)();
|
|
49
|
-
return JSON.stringify({ provider, model, apiKey, endpoint, deployment, apiVersion, managedIdentityClientId });
|
|
50
|
-
}
|
|
51
|
-
case "anthropic": {
|
|
52
|
-
const { setupToken } = (0, config_1.getAnthropicConfig)();
|
|
53
|
-
return JSON.stringify({ provider, model, setupToken });
|
|
54
|
-
}
|
|
55
|
-
case "minimax": {
|
|
56
|
-
const { apiKey } = (0, config_1.getMinimaxConfig)();
|
|
57
|
-
return JSON.stringify({ provider, model, apiKey });
|
|
58
|
-
}
|
|
59
|
-
case "openai-codex": {
|
|
60
|
-
const { oauthAccessToken } = (0, config_1.getOpenAICodexConfig)();
|
|
61
|
-
return JSON.stringify({ provider, model, oauthAccessToken });
|
|
62
|
-
}
|
|
63
|
-
/* v8 ignore start -- fingerprint: tested via provider init tests @preserve */
|
|
64
|
-
case "github-copilot": {
|
|
65
|
-
const { githubToken, baseUrl } = (0, config_1.getGithubCopilotConfig)();
|
|
66
|
-
return JSON.stringify({ provider, model, githubToken, baseUrl });
|
|
67
|
-
}
|
|
68
|
-
/* v8 ignore stop */
|
|
69
|
-
}
|
|
45
|
+
const providerConfig = (0, config_1.getProviderConfig)(provider);
|
|
46
|
+
return JSON.stringify({ provider, model, ...providerConfig });
|
|
70
47
|
}
|
|
71
48
|
function createProviderRegistry() {
|
|
72
49
|
const factories = {
|
|
@@ -87,8 +87,8 @@ function checkAgentConfig(agentName, bundlesRoot, secretsRoot) {
|
|
|
87
87
|
fix: `Add humanFacing.provider to ${agentJsonPath}. Valid providers: ${Object.keys(identity_1.PROVIDER_CREDENTIALS).join(", ")}`,
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
90
|
+
const desc = identity_1.PROVIDER_CREDENTIALS[provider];
|
|
91
|
+
if (!desc) {
|
|
92
92
|
return {
|
|
93
93
|
ok: false,
|
|
94
94
|
error: `Unknown provider '${provider}' in agent.json for '${agentName}'`,
|
|
@@ -128,7 +128,7 @@ function checkAgentConfig(agentName, bundlesRoot, secretsRoot) {
|
|
|
128
128
|
return { ok: true };
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
const missing =
|
|
131
|
+
const missing = desc.required.filter((field) => {
|
|
132
132
|
const val = providerSecrets[field];
|
|
133
133
|
return typeof val !== "string" || val.length === 0;
|
|
134
134
|
});
|
|
@@ -263,7 +263,7 @@ async function collectRuntimeAuthCredentials(input, deps) {
|
|
|
263
263
|
const spawnSync = deps.spawnSync ?? child_process_1.spawnSync;
|
|
264
264
|
const homeDir = deps.homeDir ?? os.homedir();
|
|
265
265
|
if (input.provider === "github-copilot") {
|
|
266
|
-
let token = process.env.GH_TOKEN?.trim() || "";
|
|
266
|
+
let token = process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || "";
|
|
267
267
|
if (!token) {
|
|
268
268
|
const result = spawnSync("gh", ["auth", "token"], { encoding: "utf8" });
|
|
269
269
|
token = (result.status === 0 && result.stdout ? result.stdout.trim() : "");
|
|
@@ -361,95 +361,62 @@ async function collectRuntimeAuthCredentials(input, deps) {
|
|
|
361
361
|
/* v8 ignore stop */
|
|
362
362
|
return { setupToken };
|
|
363
363
|
}
|
|
364
|
-
|
|
365
|
-
const prompt = ensurePromptInput(input.promptInput, input.provider);
|
|
366
|
-
const apiKey = (await prompt("MiniMax API key: ")).trim();
|
|
367
|
-
if (!apiKey)
|
|
368
|
-
throw new Error("MiniMax API key is required.");
|
|
369
|
-
return { apiKey };
|
|
370
|
-
}
|
|
364
|
+
// Generic prompt-for-fields fallback (minimax, azure, any future simple providers)
|
|
371
365
|
const prompt = ensurePromptInput(input.promptInput, input.provider);
|
|
372
|
-
const
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
366
|
+
const desc = identity_1.PROVIDER_CREDENTIALS[input.provider];
|
|
367
|
+
const creds = {};
|
|
368
|
+
for (const field of desc.required) {
|
|
369
|
+
/* v8 ignore next -- fallback: all current providers define promptLabels for required fields @preserve */
|
|
370
|
+
const label = desc.promptLabels[field] ?? field;
|
|
371
|
+
const value = (await prompt(`${label}: `)).trim();
|
|
372
|
+
if (!value)
|
|
373
|
+
throw new Error(`${label} is required.`);
|
|
374
|
+
creds[field] = value;
|
|
377
375
|
}
|
|
378
|
-
return
|
|
376
|
+
return creds;
|
|
379
377
|
}
|
|
380
378
|
async function resolveHatchCredentials(input) {
|
|
381
|
-
const prompt = input.promptInput;
|
|
382
379
|
const credentials = { ...(input.credentials ?? {}) };
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
if (input.provider === "anthropic" && !credentials.setupToken && input.runAuthFlow) {
|
|
380
|
+
// If all required fields are already provided, return as-is
|
|
381
|
+
const cred = credentials;
|
|
382
|
+
const missing = identity_1.PROVIDER_CREDENTIALS[input.provider].required.some((key) => !cred[key]);
|
|
383
|
+
if (!missing)
|
|
384
|
+
return credentials;
|
|
385
|
+
// Try the full auth flow (wraps collectRuntimeAuthCredentials + writes secrets)
|
|
386
|
+
if (input.runAuthFlow) {
|
|
392
387
|
const result = await input.runAuthFlow({
|
|
393
388
|
agentName: input.agentName,
|
|
394
|
-
provider:
|
|
395
|
-
promptInput:
|
|
389
|
+
provider: input.provider,
|
|
390
|
+
promptInput: input.promptInput,
|
|
396
391
|
});
|
|
397
392
|
Object.assign(credentials, result.credentials);
|
|
393
|
+
/* v8 ignore next 3 -- branch: auth flow always fills all required fields in production @preserve */
|
|
394
|
+
if (!identity_1.PROVIDER_CREDENTIALS[input.provider].required.some((key) => !credentials[key])) {
|
|
395
|
+
return credentials;
|
|
396
|
+
}
|
|
398
397
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
if (input.provider === "openai-codex" && !credentials.oauthAccessToken && prompt) {
|
|
411
|
-
credentials.oauthAccessToken = await prompt("OpenAI Codex OAuth token: ");
|
|
412
|
-
}
|
|
413
|
-
if (input.provider === "minimax" && !credentials.apiKey && prompt) {
|
|
414
|
-
credentials.apiKey = await prompt("MiniMax API key: ");
|
|
415
|
-
}
|
|
416
|
-
if (input.provider === "azure") {
|
|
417
|
-
if (!credentials.apiKey && prompt)
|
|
418
|
-
credentials.apiKey = await prompt("Azure API key: ");
|
|
419
|
-
if (!credentials.endpoint && prompt)
|
|
420
|
-
credentials.endpoint = await prompt("Azure endpoint: ");
|
|
421
|
-
if (!credentials.deployment && prompt)
|
|
422
|
-
credentials.deployment = await prompt("Azure deployment: ");
|
|
398
|
+
// Prompt for any still-missing required fields
|
|
399
|
+
/* v8 ignore next -- guard: no promptInput means we can't collect remaining fields @preserve */
|
|
400
|
+
if (input.promptInput) {
|
|
401
|
+
const desc = identity_1.PROVIDER_CREDENTIALS[input.provider];
|
|
402
|
+
for (const field of desc.required) {
|
|
403
|
+
if (!cred[field]) {
|
|
404
|
+
const label = desc.promptLabels[field] ?? field;
|
|
405
|
+
cred[field] = await input.promptInput(`${label}: `);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
423
408
|
}
|
|
424
409
|
return credentials;
|
|
425
410
|
}
|
|
426
411
|
function applyCredentials(secrets, provider, credentials) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
/* v8 ignore stop */
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (provider === "github-copilot") {
|
|
438
|
-
secrets.providers["github-copilot"].githubToken = credentials.githubToken.trim();
|
|
439
|
-
secrets.providers["github-copilot"].baseUrl = credentials.baseUrl.trim();
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
if (provider === "openai-codex") {
|
|
443
|
-
secrets.providers["openai-codex"].oauthAccessToken = credentials.oauthAccessToken.trim();
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
if (provider === "minimax") {
|
|
447
|
-
secrets.providers.minimax.apiKey = credentials.apiKey.trim();
|
|
448
|
-
return;
|
|
412
|
+
const target = secrets.providers[provider];
|
|
413
|
+
// Copy all non-empty credential fields to the provider's secrets block
|
|
414
|
+
for (const [key, value] of Object.entries(credentials)) {
|
|
415
|
+
/* v8 ignore next -- guard: skip null/empty fields from partial credential objects @preserve */
|
|
416
|
+
if (value != null && value !== "") {
|
|
417
|
+
target[key] = typeof value === "string" ? value.trim() : value;
|
|
418
|
+
}
|
|
449
419
|
}
|
|
450
|
-
secrets.providers.azure.apiKey = credentials.apiKey.trim();
|
|
451
|
-
secrets.providers.azure.endpoint = credentials.endpoint.trim();
|
|
452
|
-
secrets.providers.azure.deployment = credentials.deployment.trim();
|
|
453
420
|
}
|
|
454
421
|
async function runRuntimeAuthFlow(input, deps = {}) {
|
|
455
422
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -561,20 +561,10 @@ function parseLinkCommand(args, kind = "friend.link") {
|
|
|
561
561
|
function isAgentProvider(value) {
|
|
562
562
|
return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
|
|
563
563
|
}
|
|
564
|
-
/* v8 ignore
|
|
564
|
+
/* v8 ignore next 3 -- only called from auth.switch inside integration block @preserve */
|
|
565
565
|
function hasStoredCredentials(provider, providerSecrets) {
|
|
566
|
-
|
|
567
|
-
return !!providerSecrets.setupToken;
|
|
568
|
-
if (provider === "openai-codex")
|
|
569
|
-
return !!providerSecrets.oauthAccessToken;
|
|
570
|
-
if (provider === "github-copilot")
|
|
571
|
-
return !!providerSecrets.githubToken;
|
|
572
|
-
if (provider === "minimax")
|
|
573
|
-
return !!providerSecrets.apiKey;
|
|
574
|
-
// azure
|
|
575
|
-
return !!providerSecrets.endpoint && !!providerSecrets.apiKey;
|
|
566
|
+
return identity_1.PROVIDER_CREDENTIALS[provider].required.every((key) => !!providerSecrets[key]);
|
|
576
567
|
}
|
|
577
|
-
/* v8 ignore stop */
|
|
578
568
|
/* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
|
|
579
569
|
async function verifyProviderCredentials(provider, providers) {
|
|
580
570
|
const config = providers[provider];
|
|
@@ -1379,17 +1369,16 @@ function discoverExistingCredentials(secretsRoot) {
|
|
|
1379
1369
|
if (!parsed.providers)
|
|
1380
1370
|
continue;
|
|
1381
1371
|
for (const [provName, provConfig] of Object.entries(parsed.providers)) {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
|
|
1372
|
+
const desc = identity_1.PROVIDER_CREDENTIALS[provName];
|
|
1373
|
+
/* v8 ignore next -- guard: unknown provider names in stored secrets @preserve */
|
|
1374
|
+
if (!desc)
|
|
1375
|
+
continue;
|
|
1376
|
+
const hasRequired = desc.required.every((key) => !!provConfig[key]);
|
|
1377
|
+
if (hasRequired) {
|
|
1378
|
+
const credentials = {};
|
|
1379
|
+
for (const key of Object.keys(provConfig))
|
|
1380
|
+
credentials[key] = provConfig[key];
|
|
1381
|
+
found.push({ agentName: entry.name, provider: provName, credentials, providerConfig: { ...provConfig } });
|
|
1393
1382
|
}
|
|
1394
1383
|
}
|
|
1395
1384
|
}
|
|
@@ -1433,33 +1422,27 @@ async function defaultRunSerpentGuide() {
|
|
|
1433
1422
|
"github-copilot": "claude-sonnet-4.6",
|
|
1434
1423
|
azure: "",
|
|
1435
1424
|
};
|
|
1436
|
-
// Scan environment variables for API keys
|
|
1437
|
-
const envKeys = [
|
|
1438
|
-
{ provider: "anthropic", envVar: "ANTHROPIC_API_KEY", credKey: "setupToken" },
|
|
1439
|
-
{ provider: "openai-codex", envVar: "OPENAI_API_KEY", credKey: "oauthAccessToken" },
|
|
1440
|
-
{ provider: "azure", envVar: "AZURE_OPENAI_API_KEY", credKey: "apiKey" },
|
|
1441
|
-
{ provider: "azure", envVar: "AZURE_OPENAI_KEY", credKey: "apiKey" },
|
|
1442
|
-
{ provider: "minimax", envVar: "MINIMAX_API_KEY", credKey: "apiKey" },
|
|
1443
|
-
{ provider: "github-copilot", envVar: "GITHUB_TOKEN", credKey: "token" },
|
|
1444
|
-
];
|
|
1425
|
+
// Scan environment variables for API keys using the canonical descriptor
|
|
1445
1426
|
const envDiscovered = [];
|
|
1446
|
-
for (const
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
if (
|
|
1455
|
-
|
|
1456
|
-
if (deployment)
|
|
1457
|
-
envCred.deployment = deployment;
|
|
1427
|
+
for (const [provider, desc] of Object.entries(identity_1.PROVIDER_CREDENTIALS)) {
|
|
1428
|
+
const envCred = {};
|
|
1429
|
+
let firstEnvVar;
|
|
1430
|
+
for (const [envVar, credKey] of Object.entries(desc.envVars)) {
|
|
1431
|
+
const value = process.env[envVar];
|
|
1432
|
+
if (value) {
|
|
1433
|
+
;
|
|
1434
|
+
envCred[credKey] = value;
|
|
1435
|
+
if (!firstEnvVar)
|
|
1436
|
+
firstEnvVar = envVar;
|
|
1458
1437
|
}
|
|
1438
|
+
}
|
|
1439
|
+
// Only register if at least one required field was found
|
|
1440
|
+
const hasRequired = desc.required.some((key) => !!envCred[key]);
|
|
1441
|
+
if (hasRequired && firstEnvVar) {
|
|
1459
1442
|
const provCfg = { model: defaultModels[provider] };
|
|
1460
1443
|
if (provider === "azure" && envCred.deployment)
|
|
1461
1444
|
provCfg.deployment = envCred.deployment;
|
|
1462
|
-
envDiscovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg, envVar });
|
|
1445
|
+
envDiscovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg, envVar: firstEnvVar });
|
|
1463
1446
|
discovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg });
|
|
1464
1447
|
}
|
|
1465
1448
|
}
|
|
@@ -1491,17 +1474,7 @@ async function defaultRunSerpentGuide() {
|
|
|
1491
1474
|
}
|
|
1492
1475
|
providerRaw = pRaw;
|
|
1493
1476
|
providerConfig = { model: defaultModels[providerRaw] };
|
|
1494
|
-
|
|
1495
|
-
credentials.setupToken = await coldPrompt("API key: ");
|
|
1496
|
-
if (providerRaw === "openai-codex")
|
|
1497
|
-
credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
|
|
1498
|
-
if (providerRaw === "minimax")
|
|
1499
|
-
credentials.apiKey = await coldPrompt("API key: ");
|
|
1500
|
-
if (providerRaw === "azure") {
|
|
1501
|
-
credentials.apiKey = await coldPrompt("API key: ");
|
|
1502
|
-
credentials.endpoint = await coldPrompt("endpoint: ");
|
|
1503
|
-
credentials.deployment = await coldPrompt("deployment: ");
|
|
1504
|
-
}
|
|
1477
|
+
credentials = await (0, auth_flow_1.collectRuntimeAuthCredentials)({ agentName: "SerpentGuide", provider: providerRaw, promptInput: coldPrompt }, {});
|
|
1505
1478
|
}
|
|
1506
1479
|
}
|
|
1507
1480
|
else {
|
|
@@ -1515,17 +1488,7 @@ async function defaultRunSerpentGuide() {
|
|
|
1515
1488
|
}
|
|
1516
1489
|
providerRaw = pRaw;
|
|
1517
1490
|
providerConfig = { model: defaultModels[providerRaw] };
|
|
1518
|
-
|
|
1519
|
-
credentials.setupToken = await coldPrompt("API key: ");
|
|
1520
|
-
if (providerRaw === "openai-codex")
|
|
1521
|
-
credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
|
|
1522
|
-
if (providerRaw === "minimax")
|
|
1523
|
-
credentials.apiKey = await coldPrompt("API key: ");
|
|
1524
|
-
if (providerRaw === "azure") {
|
|
1525
|
-
credentials.apiKey = await coldPrompt("API key: ");
|
|
1526
|
-
credentials.endpoint = await coldPrompt("endpoint: ");
|
|
1527
|
-
credentials.deployment = await coldPrompt("deployment: ");
|
|
1528
|
-
}
|
|
1491
|
+
credentials = await (0, auth_flow_1.collectRuntimeAuthCredentials)({ agentName: "SerpentGuide", provider: providerRaw, promptInput: coldPrompt }, {});
|
|
1529
1492
|
}
|
|
1530
1493
|
coldRl.close();
|
|
1531
1494
|
process.stdout.write("\n");
|
|
@@ -1561,6 +1524,13 @@ async function defaultRunSerpentGuide() {
|
|
|
1561
1524
|
[providerRaw]: { ...providerConfig, ...credentials },
|
|
1562
1525
|
},
|
|
1563
1526
|
});
|
|
1527
|
+
// Ping-verify credentials before entering the serpent guide session
|
|
1528
|
+
const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../provider-ping")));
|
|
1529
|
+
const pingResult = await pingProvider(providerRaw, { ...providerConfig, ...credentials });
|
|
1530
|
+
if (!pingResult.ok) {
|
|
1531
|
+
process.stdout.write(`credentials didn't work (${pingResult.message}). run 'ouro hatch' to try again.\n`);
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1564
1534
|
const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
|
|
1565
1535
|
const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
|
|
1566
1536
|
tempDir,
|
|
@@ -45,16 +45,7 @@ const auth_flow_1 = require("./auth-flow");
|
|
|
45
45
|
const habit_parser_1 = require("./habit-parser");
|
|
46
46
|
const hatch_specialist_1 = require("./hatch-specialist");
|
|
47
47
|
function requiredCredentialKeys(provider) {
|
|
48
|
-
|
|
49
|
-
return ["setupToken"];
|
|
50
|
-
if (provider === "openai-codex")
|
|
51
|
-
return ["oauthAccessToken"];
|
|
52
|
-
/* v8 ignore next -- branch tested via requiredCredentialKeys unit test @preserve */
|
|
53
|
-
if (provider === "github-copilot")
|
|
54
|
-
return ["githubToken"];
|
|
55
|
-
if (provider === "minimax")
|
|
56
|
-
return ["apiKey"];
|
|
57
|
-
return ["apiKey", "endpoint", "deployment"];
|
|
48
|
+
return identity_1.PROVIDER_CREDENTIALS[provider].required;
|
|
58
49
|
}
|
|
59
50
|
function validateCredentials(provider, credentials) {
|
|
60
51
|
const missing = requiredCredentialKeys(provider).filter((key) => {
|
|
@@ -77,6 +77,7 @@ class DaemonProcessManager {
|
|
|
77
77
|
stopRequested: false,
|
|
78
78
|
cooldownTimer: null,
|
|
79
79
|
cooldownRetryCount: 0,
|
|
80
|
+
fastCrashCount: 0,
|
|
80
81
|
snapshot: {
|
|
81
82
|
name: agent.name,
|
|
82
83
|
channel: agent.channel,
|
|
@@ -141,6 +142,12 @@ class DaemonProcessManager {
|
|
|
141
142
|
env: state.config.env ? { ...process.env, ...state.config.env } : process.env,
|
|
142
143
|
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
143
144
|
});
|
|
145
|
+
/* v8 ignore next 7 -- defensive: spawn should always return a ChildProcess @preserve */
|
|
146
|
+
if (!child) {
|
|
147
|
+
state.snapshot.status = "crashed";
|
|
148
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "daemon", event: "daemon.agent_spawn_failed", message: "spawn returned null", meta: { agent } });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
144
151
|
state.process = child;
|
|
145
152
|
state.snapshot.status = "running";
|
|
146
153
|
state.snapshot.pid = child.pid ?? null;
|
|
@@ -251,6 +258,29 @@ class DaemonProcessManager {
|
|
|
251
258
|
return;
|
|
252
259
|
}
|
|
253
260
|
state.snapshot.lastCrashAt = new Date(now).toISOString();
|
|
261
|
+
// Fast-crash detection: if the agent dies within 5 seconds of starting, it's likely
|
|
262
|
+
// a configuration issue (missing credentials, bad provider, etc.) not a transient failure.
|
|
263
|
+
// After 3 consecutive fast crashes, stop retrying and mark as config-failed.
|
|
264
|
+
const FAST_CRASH_THRESHOLD_MS = 5000;
|
|
265
|
+
const FAST_CRASH_MAX = 3;
|
|
266
|
+
if (runDuration < FAST_CRASH_THRESHOLD_MS) {
|
|
267
|
+
state.fastCrashCount = state.fastCrashCount + 1;
|
|
268
|
+
if (state.fastCrashCount >= FAST_CRASH_MAX) {
|
|
269
|
+
state.snapshot.status = "crashed";
|
|
270
|
+
(0, runtime_1.emitNervesEvent)({
|
|
271
|
+
level: "error",
|
|
272
|
+
component: "daemon",
|
|
273
|
+
event: "daemon.agent_config_failure",
|
|
274
|
+
message: `agent crashed ${FAST_CRASH_MAX} times within ${FAST_CRASH_THRESHOLD_MS}ms of startup — likely a configuration issue (missing credentials, bad provider). Not retrying. Fix the config and run \`ouro up\` to restart.`,
|
|
275
|
+
meta: { agent: state.config.name, fastCrashCount: state.fastCrashCount, avgRunDurationMs: runDuration },
|
|
276
|
+
});
|
|
277
|
+
return; // Don't schedule cooldown recovery — this needs human/agent intervention
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
// Reset fast-crash counter on a stable run
|
|
282
|
+
state.fastCrashCount = 0;
|
|
283
|
+
}
|
|
254
284
|
state.crashTimestamps = state.crashTimestamps.filter((crashTs) => crashTs >= startOfHour(now));
|
|
255
285
|
state.crashTimestamps.push(now);
|
|
256
286
|
if (state.crashTimestamps.length > this.maxRestartsPerHour) {
|
package/dist/heart/identity.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.HARNESS_CANONICAL_REPO_URL = exports.
|
|
36
|
+
exports.HARNESS_CANONICAL_REPO_URL = exports.DEFAULT_AGENT_SENSES = exports.DEFAULT_AGENT_PHRASES = exports.DEFAULT_AGENT_CONTEXT = exports.PROVIDER_CREDENTIALS = void 0;
|
|
37
37
|
exports.buildDefaultAgentTemplate = buildDefaultAgentTemplate;
|
|
38
38
|
exports.getAgentName = getAgentName;
|
|
39
39
|
exports.getRepoRoot = getRepoRoot;
|
|
@@ -57,6 +57,14 @@ const os = __importStar(require("os"));
|
|
|
57
57
|
const path = __importStar(require("path"));
|
|
58
58
|
const runtime_1 = require("../nerves/runtime");
|
|
59
59
|
const migrate_config_1 = require("./migrate-config");
|
|
60
|
+
/** Single source of truth for per-provider credential field names, env var mappings, and prompt labels. */
|
|
61
|
+
exports.PROVIDER_CREDENTIALS = {
|
|
62
|
+
anthropic: { required: ["setupToken"], envVars: { ANTHROPIC_API_KEY: "setupToken" }, promptLabels: { setupToken: "Anthropic setup-token" } },
|
|
63
|
+
"openai-codex": { required: ["oauthAccessToken"], envVars: { OPENAI_API_KEY: "oauthAccessToken" }, promptLabels: { oauthAccessToken: "OpenAI Codex OAuth token" } },
|
|
64
|
+
azure: { required: ["apiKey", "endpoint", "deployment"], envVars: { AZURE_OPENAI_API_KEY: "apiKey", AZURE_OPENAI_KEY: "apiKey", AZURE_OPENAI_ENDPOINT: "endpoint", AZURE_OPENAI_DEPLOYMENT: "deployment" }, promptLabels: { apiKey: "Azure API key", endpoint: "Azure endpoint", deployment: "Azure deployment" } },
|
|
65
|
+
minimax: { required: ["apiKey"], envVars: { MINIMAX_API_KEY: "apiKey" }, promptLabels: { apiKey: "MiniMax API key" } },
|
|
66
|
+
"github-copilot": { required: ["githubToken", "baseUrl"], envVars: { GH_TOKEN: "githubToken", GITHUB_TOKEN: "githubToken" }, promptLabels: { githubToken: "GitHub token" } },
|
|
67
|
+
};
|
|
60
68
|
exports.DEFAULT_AGENT_CONTEXT = {
|
|
61
69
|
maxTokens: 80000,
|
|
62
70
|
contextMargin: 20,
|
|
@@ -208,17 +216,6 @@ function resolveOptionalAgentName(agentName) {
|
|
|
208
216
|
function getAgentStateRoot(agentName) {
|
|
209
217
|
return path.join(getAgentRoot(resolveOptionalAgentName(agentName)), "state");
|
|
210
218
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Required credential fields per provider.
|
|
213
|
-
* Used by daemon config-check to validate secrets before spawning an agent.
|
|
214
|
-
*/
|
|
215
|
-
exports.PROVIDER_CREDENTIALS = {
|
|
216
|
-
azure: ["apiKey", "endpoint", "deployment"],
|
|
217
|
-
minimax: ["apiKey"],
|
|
218
|
-
anthropic: ["setupToken"],
|
|
219
|
-
"openai-codex": ["oauthAccessToken"],
|
|
220
|
-
"github-copilot": ["githubToken", "baseUrl"],
|
|
221
|
-
};
|
|
222
219
|
exports.HARNESS_CANONICAL_REPO_URL = "https://github.com/ouroborosbot/ouroboros.git";
|
|
223
220
|
function getAgentRepoWorkspacesRoot(agentName) {
|
|
224
221
|
return path.join(getAgentStateRoot(resolveOptionalAgentName(agentName)), "workspaces");
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.sanitizeErrorMessage = sanitizeErrorMessage;
|
|
4
4
|
exports.pingProvider = pingProvider;
|
|
5
5
|
exports.runHealthInventory = runHealthInventory;
|
|
6
|
+
const identity_1 = require("./identity");
|
|
6
7
|
const anthropic_1 = require("./providers/anthropic");
|
|
7
8
|
const azure_1 = require("./providers/azure");
|
|
8
9
|
const minimax_1 = require("./providers/minimax");
|
|
@@ -42,25 +43,8 @@ function sanitizeErrorMessage(message) {
|
|
|
42
43
|
return message;
|
|
43
44
|
}
|
|
44
45
|
function hasEmptyCredentials(provider, config) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return !config.setupToken;
|
|
48
|
-
case "openai-codex":
|
|
49
|
-
return !config.oauthAccessToken;
|
|
50
|
-
case "minimax":
|
|
51
|
-
return !config.apiKey;
|
|
52
|
-
case "azure": {
|
|
53
|
-
const azure = config;
|
|
54
|
-
return !(azure.apiKey && azure.endpoint && azure.deployment);
|
|
55
|
-
}
|
|
56
|
-
case "github-copilot": {
|
|
57
|
-
const copilot = config;
|
|
58
|
-
return !(copilot.githubToken && copilot.baseUrl);
|
|
59
|
-
}
|
|
60
|
-
/* v8 ignore next 2 -- exhaustive: all providers handled above @preserve */
|
|
61
|
-
default:
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
46
|
+
const record = config;
|
|
47
|
+
return identity_1.PROVIDER_CREDENTIALS[provider].required.some((key) => !record[key]);
|
|
64
48
|
}
|
|
65
49
|
function createRuntimeForPing(provider, _config) {
|
|
66
50
|
// The provider constructors read credentials from the active config via getXxxConfig().
|
|
@@ -151,6 +151,11 @@ function InputArea({ onSubmit, onCtrlC, history, agentName, model }) {
|
|
|
151
151
|
const cursorRef = (0, react_1.useRef)(0);
|
|
152
152
|
const historyIdx = (0, react_1.useRef)(-1);
|
|
153
153
|
const savedInput = (0, react_1.useRef)("");
|
|
154
|
+
const tooltipTimerRef = (0, react_1.useRef)(null);
|
|
155
|
+
(0, react_1.useEffect)(() => () => {
|
|
156
|
+
if (tooltipTimerRef.current)
|
|
157
|
+
clearTimeout(tooltipTimerRef.current);
|
|
158
|
+
}, []);
|
|
154
159
|
// Helper: update input and cursor together
|
|
155
160
|
const updateInput = (text, pos) => {
|
|
156
161
|
inputRef.current = text;
|
|
@@ -174,7 +179,9 @@ function InputArea({ onSubmit, onCtrlC, history, agentName, model }) {
|
|
|
174
179
|
else if (action === "warn") {
|
|
175
180
|
// tooltip handled below
|
|
176
181
|
setTooltip("Ctrl-C again to exit");
|
|
177
|
-
|
|
182
|
+
if (tooltipTimerRef.current)
|
|
183
|
+
clearTimeout(tooltipTimerRef.current);
|
|
184
|
+
tooltipTimerRef.current = setTimeout(() => setTooltip(""), 3000);
|
|
178
185
|
}
|
|
179
186
|
// "abort" and "exit" are handled by the parent (cli.ts)
|
|
180
187
|
}, [onCtrlC]);
|
|
@@ -191,7 +198,9 @@ function InputArea({ onSubmit, onCtrlC, history, agentName, model }) {
|
|
|
191
198
|
updateInput("");
|
|
192
199
|
historyIdx.current = -1;
|
|
193
200
|
setTooltip("Esc again to clear");
|
|
194
|
-
|
|
201
|
+
if (tooltipTimerRef.current)
|
|
202
|
+
clearTimeout(tooltipTimerRef.current);
|
|
203
|
+
tooltipTimerRef.current = setTimeout(() => setTooltip(""), 2000);
|
|
195
204
|
}
|
|
196
205
|
else {
|
|
197
206
|
setTooltip("");
|