@openagents-org/agent-launcher 0.2.188 → 0.2.190
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/package.json +1 -1
- package/registry.json +4 -0
- package/src/adapters/hermes.js +18 -6
- package/src/adapters/kimi-stream.js +3 -1
- package/src/adapters/openclaw.js +57 -33
- package/src/adapters/workspace-prompt.js +13 -6
- package/src/agent-rows.js +3 -1
- package/src/cli.js +5 -3
- package/src/daemon.js +32 -11
- package/src/env.js +116 -1
- package/src/index.js +2 -2
- package/src/installer.js +23 -9
- package/src/probe.js +19 -5
- package/src/tui.js +2 -1
package/package.json
CHANGED
package/registry.json
CHANGED
package/src/adapters/hermes.js
CHANGED
|
@@ -267,14 +267,23 @@ class HermesAdapter extends BaseAdapter {
|
|
|
267
267
|
// Output parsing
|
|
268
268
|
// ------------------------------------------------------------------
|
|
269
269
|
|
|
270
|
-
_parseHermesOutput(raw) {
|
|
270
|
+
_parseHermesOutput(raw, meta) {
|
|
271
271
|
let sessionId = null;
|
|
272
272
|
let body = raw;
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
// Hermes -Q mode emits session_id to stderr (meta); the response body is
|
|
275
|
+
// on stdout (raw). Older Hermes printed session_id on stdout, so fall
|
|
276
|
+
// back to extracting it from the body when stderr carried none.
|
|
277
|
+
if (meta) {
|
|
278
|
+
const m = SESSION_ID_RE.exec(meta);
|
|
279
|
+
if (m) sessionId = m[1];
|
|
280
|
+
}
|
|
281
|
+
if (!sessionId) {
|
|
282
|
+
const m = SESSION_ID_RE.exec(body);
|
|
283
|
+
if (m) {
|
|
284
|
+
sessionId = m[1];
|
|
285
|
+
body = body.replace(SESSION_ID_RE, '');
|
|
286
|
+
}
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
const lines = [];
|
|
@@ -328,6 +337,7 @@ class HermesAdapter extends BaseAdapter {
|
|
|
328
337
|
const proc = spawn(this._hermesBin, args, {
|
|
329
338
|
env,
|
|
330
339
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
340
|
+
cwd: this.workingDir,
|
|
331
341
|
// No process group on Windows (can't signal one); windowsHide keeps a
|
|
332
342
|
// console from flashing up.
|
|
333
343
|
detached: !IS_WINDOWS,
|
|
@@ -358,7 +368,9 @@ class HermesAdapter extends BaseAdapter {
|
|
|
358
368
|
throw new Error(`hermes exited with code ${exitCode}: ${detail}`);
|
|
359
369
|
}
|
|
360
370
|
|
|
361
|
-
|
|
371
|
+
// Hermes -Q mode: response body on stdout, session metadata
|
|
372
|
+
// (session_id:, resume banner) on stderr — parse both streams.
|
|
373
|
+
const { text, sessionId } = this._parseHermesOutput(stdout, stderr);
|
|
362
374
|
if (sessionId) {
|
|
363
375
|
this._channelSessions[channelName] = sessionId;
|
|
364
376
|
this._saveSessions();
|
|
@@ -340,7 +340,9 @@ function classifyKimiError(info) {
|
|
|
340
340
|
};
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
|
|
343
|
+
// Bounded: a bare 401 matched request ids, and `invalid.{0,20}token` turned
|
|
344
|
+
// "Invalid value for max_tokens" into a bad key and a `kimi login` hint.
|
|
345
|
+
if (/auth_error|\b401\b|unauthorized|invalid[\s_-]*(api[\s_-]?key|token|authentication)\b|credential/i.test(detail)) {
|
|
344
346
|
return {
|
|
345
347
|
kind: 'auth',
|
|
346
348
|
userMessage:
|
package/src/adapters/openclaw.js
CHANGED
|
@@ -31,6 +31,41 @@ const OPENCLAW_STATE_DIR = path.join(
|
|
|
31
31
|
'.openclaw'
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Archive the legacy auth-profiles.json this adapter used to write.
|
|
36
|
+
*
|
|
37
|
+
* Current OpenClaw keeps credentials in SQLite and refuses to run while a
|
|
38
|
+
* legacy JSON store sits beside an empty one ("requires legacy credential
|
|
39
|
+
* migration; run openclaw doctor --fix"); beside a populated one it silently
|
|
40
|
+
* ignores the file, so a changed key never took effect either. The key now
|
|
41
|
+
* reaches OpenClaw through the env and openclaw.json, so the file only gets
|
|
42
|
+
* in the way.
|
|
43
|
+
*
|
|
44
|
+
* Only a file made entirely of our entries is moved: the `custom` provider
|
|
45
|
+
* exists only because configureNativeAuth created it, and any other entry must
|
|
46
|
+
* carry a key we hold ourselves. A profile the user made (OpenClaw's own
|
|
47
|
+
* paste-token also names them `<provider>:manual`) keeps the file in place,
|
|
48
|
+
* for `openclaw doctor --fix` to migrate. Renamed, never deleted.
|
|
49
|
+
*
|
|
50
|
+
* @param {Array<string|undefined>} knownKeys API keys this agent is configured with
|
|
51
|
+
*/
|
|
52
|
+
function retireOwnLegacyAuthProfiles(knownKeys) {
|
|
53
|
+
const file = path.join(OPENCLAW_STATE_DIR, 'agents', 'main', 'agent', 'auth-profiles.json');
|
|
54
|
+
let data;
|
|
55
|
+
try { data = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch { return false; }
|
|
56
|
+
const keys = new Set((knownKeys || []).filter(Boolean));
|
|
57
|
+
const profiles = Object.values((data && data.profiles) || {});
|
|
58
|
+
const ours = profiles.every((p) => p && p.type === 'token'
|
|
59
|
+
&& (p.provider === 'custom' || keys.has(p.token)));
|
|
60
|
+
if (!ours) return false;
|
|
61
|
+
try {
|
|
62
|
+
fs.renameSync(file, `${file}.openagents-retired`);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
34
69
|
class OpenClawAdapter extends BaseAdapter {
|
|
35
70
|
/**
|
|
36
71
|
* @param {object} opts - BaseAdapter opts plus:
|
|
@@ -54,6 +89,14 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
54
89
|
this._log('OpenClaw binary not found — agent will not be able to process messages');
|
|
55
90
|
}
|
|
56
91
|
|
|
92
|
+
// Agents configured before the key stopped going into auth-profiles.json
|
|
93
|
+
// still have that file; clear it before the first run rather than on the
|
|
94
|
+
// next save.
|
|
95
|
+
const env = this.agentEnv || {};
|
|
96
|
+
if (retireOwnLegacyAuthProfiles([env.LLM_API_KEY, env.OPENAI_API_KEY, env.ANTHROPIC_API_KEY])) {
|
|
97
|
+
this._log('Archived the legacy auth-profiles.json this launcher wrote (OpenClaw now stores credentials in SQLite)');
|
|
98
|
+
}
|
|
99
|
+
|
|
57
100
|
// Install workspace skill
|
|
58
101
|
this._installWorkspaceSkill();
|
|
59
102
|
}
|
|
@@ -586,9 +629,10 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
586
629
|
* LLM_API_KEY / LLM_BASE_URL / LLM_MODEL values.
|
|
587
630
|
* Called by the Launcher's saveAgentEnv when type === 'openclaw'.
|
|
588
631
|
*
|
|
589
|
-
* For standard providers (OpenAI, Anthropic),
|
|
590
|
-
* For custom endpoints, uses
|
|
591
|
-
* gives full tool support via the
|
|
632
|
+
* For standard providers (OpenAI, Anthropic), the key travels in the env
|
|
633
|
+
* and only the model is set here. For custom endpoints, uses
|
|
634
|
+
* models.providers in openclaw.json which gives full tool support via the
|
|
635
|
+
* CLI gateway mode.
|
|
592
636
|
*/
|
|
593
637
|
static configureNativeAuth(env) {
|
|
594
638
|
const apiKey = env.LLM_API_KEY;
|
|
@@ -602,26 +646,19 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
602
646
|
const isAnthropic = baseUrl.includes('api.anthropic.com');
|
|
603
647
|
const configFile = path.join(OPENCLAW_STATE_DIR, 'openclaw.json');
|
|
604
648
|
|
|
649
|
+
// The key itself never goes into auth-profiles.json any more: current
|
|
650
|
+
// OpenClaw keeps credentials in SQLite and refuses to start while a legacy
|
|
651
|
+
// JSON store sits beside an empty one ("requires legacy credential
|
|
652
|
+
// migration; run openclaw doctor --fix"). A standard provider reads the
|
|
653
|
+
// key from the OPENAI_API_KEY / ANTHROPIC_API_KEY the resolve rules put
|
|
654
|
+
// in its env; a custom one from models.providers below.
|
|
655
|
+
retireOwnLegacyAuthProfiles([apiKey]);
|
|
656
|
+
|
|
605
657
|
if (isOpenAI || isAnthropic) {
|
|
606
|
-
// Standard provider — use auth-profiles.json
|
|
607
658
|
const provider = isAnthropic ? 'anthropic' : 'openai';
|
|
608
|
-
const profileId = `${provider}:manual`;
|
|
609
|
-
const agentDir = path.join(OPENCLAW_STATE_DIR, 'agents', 'main', 'agent');
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
613
|
-
const authFile = path.join(agentDir, 'auth-profiles.json');
|
|
614
|
-
let authData = { version: 1, profiles: {} };
|
|
615
|
-
try { authData = JSON.parse(fs.readFileSync(authFile, 'utf-8')); } catch {}
|
|
616
|
-
authData.profiles = authData.profiles || {};
|
|
617
|
-
authData.profiles[profileId] = { type: 'token', provider, token: apiKey };
|
|
618
|
-
authData.lastGood = authData.lastGood || {};
|
|
619
|
-
authData.lastGood[provider] = profileId;
|
|
620
|
-
fs.writeFileSync(authFile, JSON.stringify(authData, null, 2), 'utf-8');
|
|
621
|
-
} catch {}
|
|
622
|
-
|
|
623
659
|
// Set model
|
|
624
660
|
try {
|
|
661
|
+
fs.mkdirSync(OPENCLAW_STATE_DIR, { recursive: true });
|
|
625
662
|
let config = {};
|
|
626
663
|
try { config = JSON.parse(fs.readFileSync(configFile, 'utf-8')); } catch {}
|
|
627
664
|
config.agents = config.agents || {};
|
|
@@ -653,22 +690,9 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
653
690
|
|
|
654
691
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf-8');
|
|
655
692
|
} catch {}
|
|
656
|
-
|
|
657
|
-
// Also write auth-profiles.json for the custom provider
|
|
658
|
-
try {
|
|
659
|
-
const agentDir = path.join(OPENCLAW_STATE_DIR, 'agents', 'main', 'agent');
|
|
660
|
-
fs.mkdirSync(agentDir, { recursive: true });
|
|
661
|
-
const authFile = path.join(agentDir, 'auth-profiles.json');
|
|
662
|
-
let authData = { version: 1, profiles: {} };
|
|
663
|
-
try { authData = JSON.parse(fs.readFileSync(authFile, 'utf-8')); } catch {}
|
|
664
|
-
authData.profiles = authData.profiles || {};
|
|
665
|
-
authData.profiles['custom:manual'] = { type: 'token', provider: 'custom', token: apiKey };
|
|
666
|
-
authData.lastGood = authData.lastGood || {};
|
|
667
|
-
authData.lastGood.custom = 'custom:manual';
|
|
668
|
-
fs.writeFileSync(authFile, JSON.stringify(authData, null, 2), 'utf-8');
|
|
669
|
-
} catch {}
|
|
670
693
|
}
|
|
671
694
|
}
|
|
672
695
|
}
|
|
673
696
|
|
|
674
697
|
module.exports = OpenClawAdapter;
|
|
698
|
+
module.exports.retireOwnLegacyAuthProfiles = retireOwnLegacyAuthProfiles;
|
|
@@ -415,28 +415,35 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
415
415
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
416
416
|
` ${baseUrl}/v1/browser/tabs` +
|
|
417
417
|
` -d '{"url":"URL","network":"${workspaceId}",` +
|
|
418
|
-
`"source":"openagents:${agentName}"}'\`\n
|
|
418
|
+
`"source":"openagents:${agentName}"}'\`\n` +
|
|
419
|
+
'Tabs are **temporary** by default (closed after a while idle). Add ' +
|
|
420
|
+
'`"persistent":true` (and optionally `"name":"…"`) to open a **permanent** ' +
|
|
421
|
+
'tab whose login state is saved and reusable — humans see the two kinds ' +
|
|
422
|
+
'separately in their browser. Permanent slots are scarcer: use them only ' +
|
|
423
|
+
'for logins worth keeping.\n\n' +
|
|
419
424
|
'**Navigate:**\n' +
|
|
420
425
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
421
426
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/navigate` +
|
|
422
|
-
` -d '{"url":"URL"}'\`\n\n` +
|
|
427
|
+
` -d '{"url":"URL","source":"openagents:${agentName}"}'\`\n\n` +
|
|
423
428
|
'**Click element:**\n' +
|
|
424
429
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
425
430
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/click` +
|
|
426
|
-
` -d '{"selector":"CSS_SELECTOR"}'\`\n\n` +
|
|
431
|
+
` -d '{"selector":"CSS_SELECTOR","source":"openagents:${agentName}"}'\`\n\n` +
|
|
427
432
|
'**Type text:**\n' +
|
|
428
433
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
429
434
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/type` +
|
|
430
|
-
` -d '{"selector":"CSS_SELECTOR","text":"TEXT"}'\`\n` +
|
|
435
|
+
` -d '{"selector":"CSS_SELECTOR","text":"TEXT","source":"openagents:${agentName}"}'\`\n` +
|
|
431
436
|
'(add `"append":true` to keep existing text instead of replacing it)\n\n' +
|
|
432
437
|
'**Press a key** (e.g. submit a form with Enter):\n' +
|
|
433
438
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
434
439
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/press_key` +
|
|
435
|
-
` -d '{"key":"Enter"}'\`\n\n` +
|
|
440
|
+
` -d '{"key":"Enter","source":"openagents:${agentName}"}'\`\n\n` +
|
|
436
441
|
'**Run JavaScript** in the page (returns the result):\n' +
|
|
437
442
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
438
443
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/evaluate` +
|
|
439
|
-
` -d '{"expression":"document.title"}'\`\n
|
|
444
|
+
` -d '{"expression":"document.title","source":"openagents:${agentName}"}'\`\n` +
|
|
445
|
+
'The `source` field is how humans watching the shared browser see *who* is ' +
|
|
446
|
+
'acting — include it on every tab action.\n\n' +
|
|
440
447
|
'**Close tab:**\n' +
|
|
441
448
|
`\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\`\n\n` +
|
|
442
449
|
'**Clicks/typing use CSS selectors only** (no pixel coordinates). If a ' +
|
package/src/agent-rows.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
const { isCliLogin } = require('./env');
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Build one display row per configured agent from a connector.
|
|
12
14
|
*
|
|
@@ -39,7 +41,7 @@ function loadAgentRows(connector) {
|
|
|
39
41
|
let notReadyMsg = '';
|
|
40
42
|
let health = null;
|
|
41
43
|
try {
|
|
42
|
-
health = connector.healthCheck(agent.type || 'openclaw');
|
|
44
|
+
health = connector.healthCheck(agent.type || 'openclaw', { cliLogin: isCliLogin(agent.env), agentEnv: agent.env || {} });
|
|
43
45
|
if (health && !health.ready) {
|
|
44
46
|
// "Not installed" only when the executable is genuinely missing; an
|
|
45
47
|
// installed-but-signed-out agent shows its login/config message, never
|
package/src/cli.js
CHANGED
|
@@ -985,14 +985,16 @@ async function cmdProbe(connector, flags, positional) {
|
|
|
985
985
|
const target = positional[0];
|
|
986
986
|
if (!target) { print('Usage: agn probe <type|agent> [--json]'); return; }
|
|
987
987
|
|
|
988
|
-
// Accept an agent name
|
|
988
|
+
// Accept an agent name — probe its type on that agent's own env, so a
|
|
989
|
+
// signed-in agent is tested on its sign-in rather than the type's key.
|
|
989
990
|
let type = target;
|
|
991
|
+
let agentEnv;
|
|
990
992
|
try {
|
|
991
993
|
const agent = connector.config.getAgent(target);
|
|
992
|
-
if (agent) type = agent.type || 'openclaw';
|
|
994
|
+
if (agent) { type = agent.type || 'openclaw'; agentEnv = agent.env || {}; }
|
|
993
995
|
} catch {}
|
|
994
996
|
|
|
995
|
-
const result = await connector.probeAgentType(type);
|
|
997
|
+
const result = await connector.probeAgentType(type, { agentEnv });
|
|
996
998
|
|
|
997
999
|
if (flags && flags.json) {
|
|
998
1000
|
// Machine consumers (the daemon's probe_agent node command) read ONLY
|
package/src/daemon.js
CHANGED
|
@@ -10,6 +10,7 @@ const { spawn } = require('./wsl');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const { WorkspaceClient } = require('./workspace-client');
|
|
12
12
|
const { listEndpointModels } = require('./model-list');
|
|
13
|
+
const { AUTH_MODE_KEY, isCliLogin, stripForCliLogin, typeEnvFor } = require('./env');
|
|
13
14
|
const { getEnhancedEnv, whichBinary, IS_WINDOWS, defaultAgentWorkdir } = require('./paths');
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -72,6 +73,19 @@ function endpointHost(value) {
|
|
|
72
73
|
* Compatible with the Python SDK's daemon — reads the same config files,
|
|
73
74
|
* writes the same status format, and supports the same command protocol.
|
|
74
75
|
*/
|
|
76
|
+
/**
|
|
77
|
+
* Whether a node command carries a key for the agent. A blank key is no key:
|
|
78
|
+
* saving one would leave the agent not-ready, and clearing the sign-in
|
|
79
|
+
* marker on its account would flip a signed-in agent to key mode with
|
|
80
|
+
* nothing to run on. `useDeviceCredentials` means the device already holds
|
|
81
|
+
* the key — nothing to write.
|
|
82
|
+
*/
|
|
83
|
+
function _sentKey(args) {
|
|
84
|
+
const key = args && args.apiKey;
|
|
85
|
+
const present = typeof key === 'string' ? key.trim().length > 0 : !!key;
|
|
86
|
+
return present && !args.useDeviceCredentials;
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
class Daemon {
|
|
76
90
|
constructor(config, envManager, registry) {
|
|
77
91
|
this.config = config;
|
|
@@ -139,7 +153,9 @@ class Daemon {
|
|
|
139
153
|
const type = agent.type || 'openclaw';
|
|
140
154
|
this._probeInFlight.add(name);
|
|
141
155
|
try {
|
|
142
|
-
|
|
156
|
+
// By agent name, not type: the probe runs on this agent's own env, so a
|
|
157
|
+
// signed-in agent is tested on its sign-in, not the type's saved key.
|
|
158
|
+
const r = await this._runAgn(['probe', name, '--json']);
|
|
143
159
|
let parsed = null;
|
|
144
160
|
try { parsed = JSON.parse(r.stdout.trim()); } catch {}
|
|
145
161
|
if (!parsed || typeof parsed !== 'object') {
|
|
@@ -347,7 +363,7 @@ class Daemon {
|
|
|
347
363
|
// ever leaving the device. The endpoint goes out as a hostname, so
|
|
348
364
|
// the workspace can tell a relay from the vendor its model list is for.
|
|
349
365
|
let typeEnv = {};
|
|
350
|
-
try { typeEnv = this.envManager.load(a.type) || {}; } catch {}
|
|
366
|
+
try { typeEnv = typeEnvFor(a.type, this.envManager.load(a.type) || {}, this.registry, a.env); } catch {}
|
|
351
367
|
const model = (a.env && a.env.LLM_MODEL) || typeEnv.LLM_MODEL || null;
|
|
352
368
|
const apiKey = (a.env && a.env.LLM_API_KEY) || typeEnv.LLM_API_KEY || null;
|
|
353
369
|
roster.push({
|
|
@@ -372,9 +388,11 @@ class Daemon {
|
|
|
372
388
|
* builds its env: LLM_BASE_URL or a provider variable (OPENAI_BASE_URL,
|
|
373
389
|
* ANTHROPIC_BASE_URL, …) saved for it or mapped by resolve_env, and failing
|
|
374
390
|
* those, the daemon's own environment for the provider variable this type's
|
|
375
|
-
* LLM_BASE_URL maps to, which the spawned CLI inherits.
|
|
391
|
+
* LLM_BASE_URL maps to, which the spawned CLI inherits. A signed-in agent
|
|
392
|
+
* has none: _buildAgentEnv drops every saved endpoint for it.
|
|
376
393
|
*/
|
|
377
394
|
_configuredBaseUrl(a, typeEnv) {
|
|
395
|
+
if (isCliLogin(a.env)) return null;
|
|
378
396
|
const saved = { ...typeEnv, ...(a.env || {}) };
|
|
379
397
|
let env = saved;
|
|
380
398
|
try { env = { ...saved, ...this.envManager.resolve(a.type, saved, this.registry) }; } catch {}
|
|
@@ -470,7 +488,12 @@ class Daemon {
|
|
|
470
488
|
*/
|
|
471
489
|
_saveNodeAgentEnv(name, type, args) {
|
|
472
490
|
const changes = {};
|
|
473
|
-
if (args
|
|
491
|
+
if (_sentKey(args)) {
|
|
492
|
+
changes.LLM_API_KEY = args.apiKey;
|
|
493
|
+
// A key sent for the agent means it runs on that key: a sign-in marker
|
|
494
|
+
// left from the launcher would strip it again at launch.
|
|
495
|
+
changes[AUTH_MODE_KEY] = '';
|
|
496
|
+
}
|
|
474
497
|
if (args.baseUrl !== undefined) changes.LLM_BASE_URL = args.baseUrl;
|
|
475
498
|
if (args.model !== undefined) {
|
|
476
499
|
const existing = this.config.getAgent(name);
|
|
@@ -585,7 +608,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
585
608
|
// Optional credentials for API-key agents (generic → provider mapping
|
|
586
609
|
// happens in env resolution). `useDeviceCredentials` means the device
|
|
587
610
|
// already holds them — nothing to write.
|
|
588
|
-
if (args
|
|
611
|
+
if (_sentKey(args))
|
|
589
612
|
await this._runAgn(['env', type, '--set', `LLM_API_KEY=${args.apiKey}`]);
|
|
590
613
|
if (args.model) await this._setModelEnv(type, args.model);
|
|
591
614
|
if (args.baseUrl) await this._runAgn(['env', type, '--set', `LLM_BASE_URL=${args.baseUrl}`]);
|
|
@@ -622,7 +645,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
622
645
|
// restart it so the change takes effect. Working-dir changes require a
|
|
623
646
|
// recreate (agn has no in-place path change) at the new path.
|
|
624
647
|
const type = (args.type || '').trim();
|
|
625
|
-
if (args
|
|
648
|
+
if (_sentKey(args))
|
|
626
649
|
await this._runAgn(['env', type, '--set', `LLM_API_KEY=${args.apiKey}`]);
|
|
627
650
|
if (args.model !== undefined) await this._setModelEnv(type, args.model);
|
|
628
651
|
if (args.baseUrl !== undefined) await this._runAgn(['env', type, '--set', `LLM_BASE_URL=${args.baseUrl}`]);
|
|
@@ -1519,11 +1542,9 @@ async _runNodeCommand(n, cmd) {
|
|
|
1519
1542
|
|
|
1520
1543
|
_buildAgentEnv(agentCfg) {
|
|
1521
1544
|
const type = agentCfg.type || 'openclaw';
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
const merged = { ...mergedSaved, ...resolved };
|
|
1526
|
-
return { ...process.env, ...merged };
|
|
1545
|
+
const merged = this.envManager.getForAgent(type, this.registry, agentCfg.env);
|
|
1546
|
+
// A signed-in agent drops every key, the launcher's own environment's too.
|
|
1547
|
+
return stripForCliLogin(type, { ...process.env, ...merged }, this.registry, agentCfg.env);
|
|
1527
1548
|
}
|
|
1528
1549
|
|
|
1529
1550
|
_agentConfigFingerprint(agentCfg) {
|
package/src/env.js
CHANGED
|
@@ -112,6 +112,119 @@ class EnvManager {
|
|
|
112
112
|
const resolved = this.resolve(agentType, saved, registry);
|
|
113
113
|
return { ...saved, ...resolved };
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The saved env one agent runs on: the part of <type>.env it takes (see
|
|
118
|
+
* typeEnvFor) under its own env, resolved. The daemon launches the agent on
|
|
119
|
+
* this over its own environment, and readiness judges the agent on it too.
|
|
120
|
+
*/
|
|
121
|
+
getForAgent(agentType, registry, agentEnv) {
|
|
122
|
+
const saved = { ...typeEnvFor(agentType, this.load(agentType), registry, agentEnv), ...(agentEnv || {}) };
|
|
123
|
+
const resolved = this.resolve(agentType, saved, registry);
|
|
124
|
+
return { ...saved, ...resolved };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set on an agent's own env when it signs in through its CLI (`codex login`,
|
|
130
|
+
* `claude auth login`) instead of a key. Keys stay shared per type in
|
|
131
|
+
* <type>.env and the launcher's own environment, and a CLI prefers a key it
|
|
132
|
+
* is handed over its account session, so without this an agent created on
|
|
133
|
+
* the sign-in tab still ran on whatever key was saved there earlier.
|
|
134
|
+
*/
|
|
135
|
+
const AUTH_MODE_KEY = 'OPENAGENTS_AUTH_MODE';
|
|
136
|
+
const CLI_LOGIN = 'cli_login';
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether the agent signs in through its CLI. Read from the agent's own env
|
|
140
|
+
* only: a marker in <type>.env or the launcher's environment would otherwise
|
|
141
|
+
* strip the key of every agent of that type, keyed ones included.
|
|
142
|
+
*/
|
|
143
|
+
function isCliLogin(agentEnv) {
|
|
144
|
+
return !!agentEnv && agentEnv[AUTH_MODE_KEY] === CLI_LOGIN;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Keys and endpoints a CLI reads under a name of its own that no registry
|
|
149
|
+
* field or readiness check declares (the launcher's auth-specs.ts offers the
|
|
150
|
+
* endpoints). Named per type rather than by suffix: the env stripped is the
|
|
151
|
+
* daemon's whole environment, where *_BASE_URL also names endpoints that are
|
|
152
|
+
* not the model's. CODEX_API_KEY: the Codex CLI honors it over its ChatGPT
|
|
153
|
+
* sign-in, so one set system-wide would defeat the sign-in just as
|
|
154
|
+
* OPENAI_API_KEY does.
|
|
155
|
+
*/
|
|
156
|
+
const NATIVE_CREDENTIALS = {
|
|
157
|
+
codex: ['CODEX_API_KEY'],
|
|
158
|
+
gemini: ['GOOGLE_GEMINI_BASE_URL'],
|
|
159
|
+
antigravity: ['GOOGLE_GEMINI_BASE_URL'],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The variables that carry a key or an endpoint for this agent type: its
|
|
164
|
+
* password and *_BASE_URL fields, what LLM_API_KEY / LLM_BASE_URL resolve to,
|
|
165
|
+
* and the *_API_KEY variables its readiness check accepts (Gemini has no
|
|
166
|
+
* fields at all, and reads GEMINI_API_KEY / GOOGLE_API_KEY). Only the
|
|
167
|
+
* *_API_KEY ones: that list also holds models and tokens other tools share
|
|
168
|
+
* (MSWEA_MODEL_NAME, GH_TOKEN). The model is not among them, it applies to a
|
|
169
|
+
* sign-in as well. So is
|
|
170
|
+
* CLAUDE_CODE_OAUTH_TOKEN: `claude setup-token` makes it from the account
|
|
171
|
+
* sign-in, so it is how a signed-in claude authenticates, not a key to drop.
|
|
172
|
+
*/
|
|
173
|
+
function credentialKeys(agentType, registry) {
|
|
174
|
+
const keys = new Set(['LLM_API_KEY', 'LLM_BASE_URL', ...(NATIVE_CREDENTIALS[agentType] || [])]);
|
|
175
|
+
if (!registry) return keys;
|
|
176
|
+
for (const field of registry.getEnvFields?.(agentType) || []) {
|
|
177
|
+
if (field.password || /BASE_URL$/.test(field.name || '')) keys.add(field.name);
|
|
178
|
+
}
|
|
179
|
+
for (const rule of registry.getResolveRules?.(agentType) || []) {
|
|
180
|
+
if (keys.has(rule.from) && rule.to) keys.add(rule.to);
|
|
181
|
+
}
|
|
182
|
+
const checkReady = registry.getEntry?.(agentType)?.check_ready;
|
|
183
|
+
for (const name of (checkReady && checkReady.env_vars) || []) {
|
|
184
|
+
if (/_API_KEY$/.test(name)) keys.add(name);
|
|
185
|
+
}
|
|
186
|
+
return keys;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The variables that pick the model: LLM_MODEL, what it resolves to
|
|
191
|
+
* (CODEX_MODEL, ANTHROPIC_MODEL), and a type's own *_MODEL fields.
|
|
192
|
+
*/
|
|
193
|
+
function modelKeys(agentType, registry) {
|
|
194
|
+
const keys = new Set(['LLM_MODEL']);
|
|
195
|
+
if (!registry) return keys;
|
|
196
|
+
for (const field of registry.getEnvFields?.(agentType) || []) {
|
|
197
|
+
if (/_MODEL(_NAME)?$/.test(field.name || '')) keys.add(field.name);
|
|
198
|
+
}
|
|
199
|
+
for (const rule of registry.getResolveRules?.(agentType) || []) {
|
|
200
|
+
if (rule.from === 'LLM_MODEL' && rule.to) keys.add(rule.to);
|
|
201
|
+
}
|
|
202
|
+
return keys;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The part of <type>.env an agent runs on. All of it for an agent on a key.
|
|
207
|
+
* A signed-in agent takes none of its keys, and none of its model either:
|
|
208
|
+
* that model was picked for the key's endpoint (a relay's
|
|
209
|
+
* openai-gpt-oss-20b), which the account behind a sign-in does not serve.
|
|
210
|
+
* Its model is the one in its own env, else the CLI's default.
|
|
211
|
+
*/
|
|
212
|
+
function typeEnvFor(agentType, typeEnv, registry, agentEnv) {
|
|
213
|
+
if (!isCliLogin(agentEnv)) return typeEnv;
|
|
214
|
+
const out = stripForCliLogin(agentType, typeEnv, registry, agentEnv);
|
|
215
|
+
for (const key of modelKeys(agentType, registry)) delete out[key];
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* `env` with every credential removed when the agent's own env (`agentEnv`)
|
|
221
|
+
* marks a sign-in, or unchanged for an agent that runs on a key.
|
|
222
|
+
*/
|
|
223
|
+
function stripForCliLogin(agentType, env, registry, agentEnv) {
|
|
224
|
+
if (!isCliLogin(agentEnv)) return env;
|
|
225
|
+
const out = { ...env };
|
|
226
|
+
for (const key of credentialKeys(agentType, registry)) delete out[key];
|
|
227
|
+
return out;
|
|
115
228
|
}
|
|
116
229
|
|
|
117
230
|
/**
|
|
@@ -128,4 +241,6 @@ function normalizeAnthropicBase(url) {
|
|
|
128
241
|
return String(url || '').trim().replace(/\/+$/, '').replace(/\/v1$/i, '');
|
|
129
242
|
}
|
|
130
243
|
|
|
131
|
-
module.exports = {
|
|
244
|
+
module.exports = {
|
|
245
|
+
EnvManager, AUTH_MODE_KEY, CLI_LOGIN, isCliLogin, credentialKeys, modelKeys, stripForCliLogin, typeEnvFor,
|
|
246
|
+
};
|
package/src/index.js
CHANGED
|
@@ -69,8 +69,8 @@ class AgentConnector {
|
|
|
69
69
|
return this.installer.isInstalled(agentType);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
healthCheck(agentType) {
|
|
73
|
-
return this.installer.healthCheck(agentType);
|
|
72
|
+
healthCheck(agentType, opts) {
|
|
73
|
+
return this.installer.healthCheck(agentType, opts);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// -- Agent CRUD --
|
package/src/installer.js
CHANGED
|
@@ -512,7 +512,15 @@ class Installer {
|
|
|
512
512
|
* Health check — binary existence + version.
|
|
513
513
|
* @returns {{ installed: boolean, binary: string|null, version: string|null }}
|
|
514
514
|
*/
|
|
515
|
-
|
|
515
|
+
/**
|
|
516
|
+
* `opts.cliLogin` checks one agent that signs in through its CLI: the core
|
|
517
|
+
* drops every key for it at launch, so a key saved for the type or set in
|
|
518
|
+
* the daemon's environment must not make it ready.
|
|
519
|
+
*
|
|
520
|
+
* `opts.agentEnv` is that agent's own env. With it the agent is judged on
|
|
521
|
+
* the env the daemon launches it on, not on <type>.env alone.
|
|
522
|
+
*/
|
|
523
|
+
healthCheck(agentType, opts = {}) {
|
|
516
524
|
const entry = this.registry.getEntry(agentType);
|
|
517
525
|
if (entry?.install?.api_only) {
|
|
518
526
|
const info = this.getInstallInfo(agentType);
|
|
@@ -532,7 +540,7 @@ class Installer {
|
|
|
532
540
|
};
|
|
533
541
|
}
|
|
534
542
|
|
|
535
|
-
const apiReadiness = this._evaluateReadiness(agentType, entry, null);
|
|
543
|
+
const apiReadiness = this._evaluateReadiness(agentType, entry, null, opts);
|
|
536
544
|
return {
|
|
537
545
|
installed: true,
|
|
538
546
|
binary: null,
|
|
@@ -568,7 +576,7 @@ class Installer {
|
|
|
568
576
|
const versionCmd = checkCmd || this._versionProbeCommand(binary);
|
|
569
577
|
|
|
570
578
|
const version = this._detectVersion(binary, versionCmd);
|
|
571
|
-
const readiness = this._evaluateReadiness(agentType, entry, binary);
|
|
579
|
+
const readiness = this._evaluateReadiness(agentType, entry, binary, opts);
|
|
572
580
|
|
|
573
581
|
// Generic minimum-version gate (no per-agent special-casing). An entry opts
|
|
574
582
|
// in by declaring `install.min_version`. Below the floor we keep
|
|
@@ -776,7 +784,7 @@ class Installer {
|
|
|
776
784
|
return { compatible: true, minVersion, message: null };
|
|
777
785
|
}
|
|
778
786
|
|
|
779
|
-
_evaluateReadiness(agentType, entry, binary) {
|
|
787
|
+
_evaluateReadiness(agentType, entry, binary, opts = {}) {
|
|
780
788
|
const checkReady = entry?.check_ready;
|
|
781
789
|
if (!checkReady) {
|
|
782
790
|
return {
|
|
@@ -788,10 +796,16 @@ class Installer {
|
|
|
788
796
|
};
|
|
789
797
|
}
|
|
790
798
|
|
|
791
|
-
|
|
799
|
+
// A key set on the agent itself (the workspace saves it there, as
|
|
800
|
+
// LLM_API_KEY) never reaches <type>.env, so judged on that file alone a
|
|
801
|
+
// Kimi agent that answered fine read "Not configured — … run: kimi login".
|
|
802
|
+
const savedEnv = opts.agentEnv
|
|
803
|
+
? this.env.getForAgent(agentType, this.registry, opts.agentEnv)
|
|
804
|
+
: this.env.getEffective(agentType, this.registry);
|
|
792
805
|
const directEnv = this._hasAllValues(process.env, checkReady.env_all);
|
|
793
806
|
const directSaved = this._hasAllValues(savedEnv, checkReady.saved_env_all || checkReady.env_all);
|
|
794
|
-
const
|
|
807
|
+
const keyed = !opts.cliLogin;
|
|
808
|
+
const directReady = keyed && (directEnv || directSaved);
|
|
795
809
|
// env_vars must be satisfiable by the SAVED (and resolved) per-agent env,
|
|
796
810
|
// not only the daemon's own process env. The workspace configure flow
|
|
797
811
|
// saves LLM_API_KEY, which resolve rules map to e.g. ANTHROPIC_AUTH_TOKEN
|
|
@@ -799,10 +813,10 @@ class Installer {
|
|
|
799
813
|
// alone made a perfectly working key-configured agent read "Not logged
|
|
800
814
|
// in" (health banner + failed smoke test) while chat answered fine, with
|
|
801
815
|
// guidance telling the user to configure the very key they had saved.
|
|
802
|
-
const envAnyReady =
|
|
816
|
+
const envAnyReady = keyed && (
|
|
803
817
|
this._hasAnyValue(process.env, checkReady.env_vars) ||
|
|
804
|
-
this._hasAnyValue(savedEnv, checkReady.env_vars);
|
|
805
|
-
const savedAnyReady = !!(checkReady.saved_env_key && savedEnv[checkReady.saved_env_key]);
|
|
818
|
+
this._hasAnyValue(savedEnv, checkReady.env_vars));
|
|
819
|
+
const savedAnyReady = keyed && !!(checkReady.saved_env_key && savedEnv[checkReady.saved_env_key]);
|
|
806
820
|
// A registry entry may require a non-empty JSON object rather than mere
|
|
807
821
|
// parseability. In that mode the stricter _evaluateCredsFile path below is
|
|
808
822
|
// authoritative; the legacy parser treats an empty {} as ready.
|
package/src/probe.js
CHANGED
|
@@ -33,6 +33,7 @@ const { spawn } = require('./wsl');
|
|
|
33
33
|
const { getEnhancedEnv } = require('./paths');
|
|
34
34
|
const { shouldUseShellForBinary } = require('./adapters/health-status');
|
|
35
35
|
const { formatAuthGuidance } = require('./auth-guidance');
|
|
36
|
+
const { isCliLogin, stripForCliLogin, typeEnvFor } = require('./env');
|
|
36
37
|
|
|
37
38
|
const PROBE_PROMPT = 'hi';
|
|
38
39
|
const DEFAULT_TIMEOUT_MS = 90_000;
|
|
@@ -98,7 +99,14 @@ function authFlavor(entry, agentEnv) {
|
|
|
98
99
|
const cr = (entry && entry.check_ready) || {};
|
|
99
100
|
const envCfg = (entry && entry.env_config) || [];
|
|
100
101
|
const hasKeyField = envCfg.some((f) => f && /key/i.test(f.name || ''));
|
|
101
|
-
|
|
102
|
+
// The type's own key names count too: a Kimi agent on KIMI_API_KEY whose
|
|
103
|
+
// key was rejected was told its sign-in had expired and to run kimi login.
|
|
104
|
+
const keyNames = [
|
|
105
|
+
'LLM_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', cr.saved_env_key,
|
|
106
|
+
...(cr.env_vars || []).filter((k) => /_API_KEY$/.test(k)),
|
|
107
|
+
...envCfg.filter((f) => f && f.password).map((f) => f.name),
|
|
108
|
+
];
|
|
109
|
+
const hasSavedKey = !!agentEnv && keyNames.some((k) => k && agentEnv[k]);
|
|
102
110
|
if (hasSavedKey || (hasKeyField && !cr.login_command)) return 'api_key';
|
|
103
111
|
if (cr.login_command) return 'cli_login';
|
|
104
112
|
return hasKeyField ? 'api_key' : 'cli_login';
|
|
@@ -287,16 +295,22 @@ async function probeAgentType(connector, type, opts = {}) {
|
|
|
287
295
|
return done({ ok: false, method: 'none', code: CODE.UNKNOWN_TYPE, message: `Unknown agent type '${type}'` });
|
|
288
296
|
}
|
|
289
297
|
|
|
298
|
+
// A signed-in agent is ready on its sign-in alone: judged on the type, a
|
|
299
|
+
// key it will never be given made it "pass" with no login at all.
|
|
300
|
+
const cliLogin = isCliLogin(opts.agentEnv);
|
|
290
301
|
let health = {};
|
|
291
|
-
try { health = connector.healthCheck(type) || {}; } catch (e) {
|
|
302
|
+
try { health = connector.healthCheck(type, { cliLogin, agentEnv: opts.agentEnv }) || {}; } catch (e) {
|
|
292
303
|
health = { installed: false, ready: false, message: e.message };
|
|
293
304
|
}
|
|
294
305
|
|
|
306
|
+
// opts.agentEnv is one agent's own env, layered over the type's the way
|
|
307
|
+
// the daemon builds it; a signed-in agent then carries no key from either.
|
|
295
308
|
let agentEnv = {};
|
|
296
309
|
try {
|
|
297
|
-
const
|
|
310
|
+
const typeEnv = typeEnvFor(type, connector.getAgentEnv(type) || {}, connector.registry, opts.agentEnv);
|
|
311
|
+
const saved = { ...typeEnv, ...(opts.agentEnv || {}) };
|
|
298
312
|
const resolved = connector.resolveAgentEnv(type, saved) || {};
|
|
299
|
-
agentEnv = { ...saved, ...resolved };
|
|
313
|
+
agentEnv = stripForCliLogin(type, { ...saved, ...resolved }, connector.registry, opts.agentEnv);
|
|
300
314
|
} catch {}
|
|
301
315
|
|
|
302
316
|
if (!health.installed) {
|
|
@@ -321,7 +335,7 @@ async function probeAgentType(connector, type, opts = {}) {
|
|
|
321
335
|
|
|
322
336
|
const timeoutMs = opts.timeoutMs
|
|
323
337
|
|| (entry.probe && entry.probe.timeout_s ? entry.probe.timeout_s * 1000 : DEFAULT_TIMEOUT_MS);
|
|
324
|
-
const env = { ...getEnhancedEnv(), ...agentEnv };
|
|
338
|
+
const env = stripForCliLogin(type, { ...getEnhancedEnv(), ...agentEnv }, connector.registry, opts.agentEnv);
|
|
325
339
|
|
|
326
340
|
// Hoisted above the CLI tier: a failing CLI probe consults it too (below).
|
|
327
341
|
const hasApiKey = !!(agentEnv.LLM_API_KEY || agentEnv.OPENAI_API_KEY || agentEnv.ANTHROPIC_API_KEY
|
package/src/tui.js
CHANGED
|
@@ -14,6 +14,7 @@ const fs = require('fs');
|
|
|
14
14
|
const { spawn } = require('child_process');
|
|
15
15
|
const { getExtraBinDirs } = require('./paths');
|
|
16
16
|
const { hasCredentialMetadata, formatAuthGuidance } = require('./auth-guidance');
|
|
17
|
+
const { isCliLogin } = require('./env');
|
|
17
18
|
|
|
18
19
|
const IS_WINDOWS = process.platform === 'win32';
|
|
19
20
|
|
|
@@ -774,7 +775,7 @@ function createTUI() {
|
|
|
774
775
|
const checkReady = entry && entry.check_ready;
|
|
775
776
|
if (hasCredentialMetadata(checkReady)) {
|
|
776
777
|
let health = null;
|
|
777
|
-
try { health = connector.healthCheck(agent.type); } catch {}
|
|
778
|
+
try { health = connector.healthCheck(agent.type, { cliLogin: isCliLogin(agent.env), agentEnv: agent.env || {} }); } catch {}
|
|
778
779
|
const g = formatAuthGuidance(entry, health);
|
|
779
780
|
const tag = g.ready ? 'green-fg' : 'yellow-fg';
|
|
780
781
|
log(`{bold}{${tag}}${entry.label || agent.type} authentication: ${g.ready ? 'Ready' : 'Not ready'}{/}{/bold}`);
|