@openagents-org/agent-launcher 0.2.143 → 0.2.145

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/src/agent-rows.js CHANGED
@@ -40,7 +40,16 @@ function loadAgentRows(connector) {
40
40
  let health = null;
41
41
  try {
42
42
  health = connector.healthCheck(agent.type || 'openclaw');
43
- if (health && !health.ready) notReadyMsg = health.message || 'Not configured';
43
+ if (health && !health.ready) {
44
+ // "Not installed" only when the executable is genuinely missing; an
45
+ // installed-but-signed-out agent shows its login/config message, never
46
+ // "Not installed". Mirrors the launcher's formatHealthLabel rule.
47
+ const notInstalled =
48
+ health.reason === 'not_installed' || health.installed === false;
49
+ notReadyMsg = notInstalled
50
+ ? 'Not installed'
51
+ : health.message || 'Not configured';
52
+ }
44
53
  } catch {}
45
54
 
46
55
  return {
@@ -55,6 +64,9 @@ function loadAgentRows(connector) {
55
64
  path: agent.path || '',
56
65
  network: agent.network || '',
57
66
  lastError: info.last_error || '',
67
+ // Structured daemon failure reason (spawn_failed / workspace_join_failed /
68
+ // heartbeat_failed / runtime_missing / …) paired with lastError.
69
+ errorReason: info.error_reason || '',
58
70
  notReadyMsg,
59
71
  health,
60
72
  configured: true,
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pure, content-free helpers that turn an agent's registry `check_ready` plus a
5
+ * health-check result into human-readable authentication guidance.
6
+ *
7
+ * Used by the connector TUI (`showConfigureScreen`) and CLI (`agn env`) so an
8
+ * agent whose sign-in is an OAuth/credential FILE (e.g. Gemini's
9
+ * ~/.gemini/oauth_creds.json) is guided to authenticate instead of being
10
+ * mislabeled "No configuration required for this agent type".
11
+ *
12
+ * These helpers NEVER read, parse, copy, or print credential CONTENTS. They use
13
+ * only the agent's DECLARED metadata — the login command and env-var NAMES from
14
+ * `check_ready`, and the already-computed (non-sensitive) `message` / status
15
+ * fields the installer's health check returns. No token, key, email, or file
16
+ * path is ever emitted.
17
+ */
18
+
19
+ /**
20
+ * True when an agent declares any credential / sign-in capability in
21
+ * `check_ready` — an OAuth/credential file, API-key env vars, a service-account
22
+ * path env, or a login command. This is the OPT-IN gate: agents WITHOUT any of
23
+ * these are left exactly as before (the caller keeps showing "No configuration
24
+ * required"), so other agents are never polluted by Gemini's guidance.
25
+ *
26
+ * @param {object|null|undefined} checkReady
27
+ * @returns {boolean}
28
+ */
29
+ function hasCredentialMetadata(checkReady) {
30
+ if (!checkReady || typeof checkReady !== 'object') return false;
31
+ return !!(
32
+ checkReady.creds_file ||
33
+ (Array.isArray(checkReady.env_vars) && checkReady.env_vars.length) ||
34
+ (Array.isArray(checkReady.creds_path_env) && checkReady.creds_path_env.length) ||
35
+ checkReady.login_command
36
+ );
37
+ }
38
+
39
+ /**
40
+ * A short, NON-sensitive label for a ready auth_mode. Registry-overridable via
41
+ * `check_ready.auth_detected_labels` (e.g. Gemini maps cli_login → "Google
42
+ * account sign-in detected"). Falls back to a generic, agent-agnostic phrase so
43
+ * any future credential-file agent reads sensibly without extra config.
44
+ *
45
+ * @param {object} checkReady
46
+ * @param {string|null|undefined} authMode 'cli_login' | 'api_key' | other
47
+ * @returns {string}
48
+ */
49
+ function readyLabel(checkReady, authMode) {
50
+ const labels = (checkReady && checkReady.auth_detected_labels) || {};
51
+ if (authMode === 'cli_login') return labels.cli_login || 'CLI sign-in detected';
52
+ if (authMode === 'api_key') return labels.api_key || 'API key detected';
53
+ return labels[authMode] || 'authenticated';
54
+ }
55
+
56
+ /**
57
+ * Build display lines (plain text, no markup, no secrets) describing an agent's
58
+ * authentication state and the concrete next step. The caller is responsible
59
+ * for any terminal styling.
60
+ *
61
+ * @param {object} entry Registry catalog entry (has .label/.name/.check_ready)
62
+ * @param {object|null} health Result of installer.healthCheck(type)
63
+ * @returns {{ ready: boolean, status: string, lines: string[] }}
64
+ */
65
+ function formatAuthGuidance(entry, health) {
66
+ const checkReady = (entry && entry.check_ready) || {};
67
+ const label = (entry && (entry.label || entry.name)) || 'This agent';
68
+ const h = health || {};
69
+ const lines = [];
70
+
71
+ if (h.installed === false) {
72
+ lines.push(`${label} is not installed yet.`);
73
+ return { ready: false, status: 'not_installed', lines };
74
+ }
75
+
76
+ if (h.ready) {
77
+ lines.push(`Ready — ${readyLabel(checkReady, h.auth_mode)}.`);
78
+ return { ready: true, status: h.auth_status || 'ready', lines };
79
+ }
80
+
81
+ // Not ready: lead with the registry's own (non-sensitive) reason, then the
82
+ // concrete next steps derived from the DECLARED login command + env-var names.
83
+ if (h.message) lines.push(h.message);
84
+ const loginCmd = checkReady.login_command;
85
+ if (loginCmd) {
86
+ lines.push(
87
+ `Run \`${loginCmd}\` in your terminal and complete sign-in, then return here and refresh the agent status.`,
88
+ );
89
+ }
90
+ const envVars = Array.isArray(checkReady.env_vars) ? checkReady.env_vars : [];
91
+ if (envVars.length) {
92
+ lines.push(`Alternatively, configure ${envVars.join(' or ')}.`);
93
+ }
94
+ if (!lines.length) lines.push(`${label} requires authentication before use.`);
95
+ return { ready: false, status: h.auth_status || 'no_credentials', lines };
96
+ }
97
+
98
+ module.exports = { hasCredentialMetadata, readyLabel, formatAuthGuidance };
package/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { AgentConnector, Daemon } = require('./index');
4
+ const { hasCredentialMetadata, formatAuthGuidance } = require('./auth-guidance');
4
5
 
5
6
  // ---------------------------------------------------------------------------
6
7
  // Arg parsing
@@ -483,14 +484,30 @@ async function cmdEnv(connector, flags, positional) {
483
484
  const display = field.password && val ? '***' : (val || '(not set)');
484
485
  print(` ${field.name}: ${display} ${field.required ? '(required)' : ''}`);
485
486
  }
487
+ return;
488
+ }
489
+
490
+ // No env_config, but the agent may still require a sign-in/credential (e.g.
491
+ // Gemini's OAuth login). Surface readiness + login guidance from the registry
492
+ // check_ready rather than a misleading "No env vars configured". Gated on
493
+ // credential metadata so plain no-config agents keep their existing output.
494
+ const entry = connector.registry.getEntry(type);
495
+ if (hasCredentialMetadata(entry && entry.check_ready)) {
496
+ let health = null;
497
+ try { health = connector.healthCheck(type); } catch {}
498
+ const g = formatAuthGuidance(entry, health);
499
+ print(`${entry.label || type} authentication status: ${g.ready ? 'Ready' : 'Not ready'}`);
500
+ print('');
501
+ for (const line of g.lines) print(line);
502
+ return;
503
+ }
504
+
505
+ const entries = Object.entries(env);
506
+ if (entries.length === 0) {
507
+ print(`No env vars configured for ${type}`);
486
508
  } else {
487
- const entries = Object.entries(env);
488
- if (entries.length === 0) {
489
- print(`No env vars configured for ${type}`);
490
- } else {
491
- for (const [k, v] of entries) {
492
- print(` ${k}: ${v}`);
493
- }
509
+ for (const [k, v] of entries) {
510
+ print(` ${k}: ${v}`);
494
511
  }
495
512
  }
496
513
  }
package/src/config.js CHANGED
@@ -426,8 +426,14 @@ function parseYamlValue(val) {
426
426
  if (val === 'true') return true;
427
427
  if (val === 'false') return false;
428
428
  if (/^\d+$/.test(val)) return parseInt(val, 10);
429
- if ((val.startsWith("'") && val.endsWith("'")) ||
430
- (val.startsWith('"') && val.endsWith('"'))) {
429
+ if (val.startsWith('"') && val.endsWith('"')) {
430
+ // Reverse serializeYamlValue's escaping (\\ → \, \" → "). Without this a
431
+ // double-quoted Windows path like "D:\重要资料" (quoted because of the drive
432
+ // colon) round-trips to D:\\重要资料 and accumulates an extra backslash on
433
+ // every save/load cycle, corrupting the agent's working dir.
434
+ return val.slice(1, -1).replace(/\\(["\\])/g, '$1');
435
+ }
436
+ if (val.startsWith("'") && val.endsWith("'")) {
431
437
  return val.slice(1, -1);
432
438
  }
433
439
  if (val.startsWith('{') || val.startsWith('[')) {
package/src/daemon.js CHANGED
@@ -210,6 +210,7 @@ class Daemon {
210
210
  restarts: info.restarts,
211
211
  started_at: info.startedAt || null,
212
212
  last_error: info.lastError || null,
213
+ error_reason: info.errorReason || null,
213
214
  };
214
215
  }
215
216
  return result;
@@ -363,6 +364,9 @@ class Daemon {
363
364
  restarts: 0,
364
365
  startedAt: null,
365
366
  lastError: null,
367
+ // Structured failure reason (health-status REASON) paired with lastError,
368
+ // so the UI/TUI can classify "why" without parsing the message.
369
+ errorReason: null,
366
370
  proc: null,
367
371
  _backoff: 2,
368
372
  };
@@ -472,9 +476,33 @@ class Daemon {
472
476
  // Internal — adapter loop (workspace-connected agents)
473
477
  // ---------------------------------------------------------------------------
474
478
 
479
+ /**
480
+ * Apply a live status update reported by a running adapter (join/heartbeat
481
+ * health). A genuine failure reason → state 'error' + redacted last_error so
482
+ * the Agents list / TUI show the real cause; a null reason → recovered, clear
483
+ * the error. Ignored once the agent is stopping (a user stop must win).
484
+ */
485
+ _applyAdapterStatus(name, info, update) {
486
+ if (!update || this._shuttingDown || this._stoppedAgents.has(name)) return;
487
+ const { isErrorReason, redactDiagnostic } = require('./adapters/health-status');
488
+ const reason = update.reason || null;
489
+ if (reason && isErrorReason(reason)) {
490
+ info.state = 'error';
491
+ info.errorReason = reason;
492
+ info.lastError = redactDiagnostic(update.message || reason);
493
+ } else {
494
+ // Recovered / healthy again — clear any prior connectivity error.
495
+ if (info.state === 'error') info.state = 'running';
496
+ info.errorReason = null;
497
+ info.lastError = null;
498
+ }
499
+ this._writeStatus();
500
+ }
501
+
475
502
  async _adapterLoop(name, agentCfg, info, network) {
476
503
  const { createAdapter } = require('./adapters');
477
504
  const { skillsToDisabledModules } = require('./skill-catalog');
505
+ const { redactDiagnostic } = require('./adapters/health-status');
478
506
  const agentType = agentCfg.type || 'openclaw';
479
507
  const endpoint = network.endpoint || 'https://workspace-endpoint.openagents.org';
480
508
 
@@ -501,20 +529,46 @@ class Daemon {
501
529
  // .claude/skills there failed with EPERM. Root it under ~/.openagents.
502
530
  workingDir: agentCfg.path || defaultAgentWorkdir(name),
503
531
  toolMode: agentCfg.tool_mode || 'skills',
532
+ // Live runtime/connectivity status → daemon.status.json (Agents list/TUI).
533
+ onStatus: (update) => this._applyAdapterStatus(name, info, update),
504
534
  });
505
535
  } catch (e) {
506
- this._log(`${name} failed to create ${agentType} adapter: ${e.message}`);
536
+ // A construction failure (e.g. unknown agent type in this core) is a hard
537
+ // error with a real cause — surface it (redacted), do not pretend stopped.
507
538
  info.state = 'error';
508
- info.lastError = e.message;
539
+ info.errorReason = 'adapter_crashed';
540
+ info.lastError = redactDiagnostic(e.message || String(e));
541
+ this._log(`${name} failed to create ${agentType} adapter: ${info.lastError}`);
509
542
  this._writeStatus();
510
543
  return;
511
544
  }
512
545
 
546
+ // Preflight: when the agent's runtime binary is genuinely missing, surface a
547
+ // precise reason ('runtime_missing') and DO NOT join the workspace — there is
548
+ // no point spinning a join/poll loop that can never run the CLI. Adapters
549
+ // that don't override preflight() always pass.
550
+ try {
551
+ const pf =
552
+ typeof adapter.preflight === 'function' ? adapter.preflight() : null;
553
+ if (pf && pf.ok === false) {
554
+ info.state = 'error';
555
+ info.errorReason = pf.reason || 'runtime_missing';
556
+ info.lastError = redactDiagnostic(pf.message || 'runtime not available');
557
+ this._writeStatus();
558
+ this._log(`${name} preflight failed (${info.errorReason}): ${info.lastError}`);
559
+ return;
560
+ }
561
+ } catch (e) {
562
+ this._log(`${name} preflight error (ignored, continuing): ${e.message}`);
563
+ }
564
+
513
565
  // Store adapter reference for stop and duplicate detection
514
566
  this._adapters[name] = adapter;
515
567
 
516
568
  info.state = 'running';
517
569
  info.startedAt = new Date().toISOString();
570
+ info.lastError = null;
571
+ info.errorReason = null;
518
572
  this._writeStatus();
519
573
  this._log(`${name} adapter online → ${network.slug} (type: ${agentType})`);
520
574
 
@@ -531,14 +585,41 @@ class Daemon {
531
585
  await adapter.run();
532
586
  clearInterval(checkStop);
533
587
  } catch (e) {
534
- info.lastError = (e.message || String(e)).slice(0, 200);
588
+ info.errorReason = info.errorReason || 'adapter_crashed';
589
+ info.lastError = redactDiagnostic((e.message || String(e)).slice(0, 200));
535
590
  this._log(`${name} adapter error: ${info.lastError}`);
536
591
  }
537
592
 
538
593
  delete this._adapters[name];
539
- info.state = 'stopped';
594
+
595
+ // Final state: a clean stop (user stop / daemon shutdown) is NEVER an error.
596
+ // A terminal failure recorded by the adapter (join/heartbeat/session-revoked)
597
+ // or a crash surfaces as 'error' with its classified, redacted reason.
598
+ const userStopped =
599
+ this._shuttingDown ||
600
+ this._stoppedAgents.has(name) ||
601
+ (typeof adapter.wasStopRequested === 'function' && adapter.wasStopRequested());
602
+ if (userStopped) {
603
+ info.state = 'stopped';
604
+ info.lastError = null;
605
+ info.errorReason = null;
606
+ } else {
607
+ const exit =
608
+ (typeof adapter.getExitInfo === 'function' && adapter.getExitInfo()) ||
609
+ null;
610
+ if (exit && exit.reason) {
611
+ info.state = 'error';
612
+ info.errorReason = exit.reason;
613
+ info.lastError = redactDiagnostic(exit.message || exit.reason);
614
+ } else if (info.errorReason) {
615
+ // A crash, or a live error report that never recovered — keep it visible.
616
+ info.state = 'error';
617
+ } else {
618
+ info.state = 'stopped';
619
+ }
620
+ }
540
621
  this._writeStatus();
541
- this._log(`${name} adapter stopped`);
622
+ this._log(`${name} adapter stopped (state: ${info.state})`);
542
623
  }
543
624
 
544
625
  // NOTE: Adapter-specific message handling (openclaw, claude, codex)