@love-moon/conductor-cli 0.9.0 → 0.10.0
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.md +27 -0
- package/bin/conductor-config.js +10 -0
- package/bin/conductor-diagnose.js +3 -1
- package/bin/conductor-fire.js +11 -0
- package/bin/conductor-update.js +13 -0
- package/package.json +5 -5
- package/src/daemon.js +73 -0
- package/src/runtime-backends.js +63 -2
- package/src/version-check.js +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @love-moon/conductor-cli
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 43c4f87: Runtime health preflight and agent schedule access control. The daemon now
|
|
8
|
+
advertises positive backend runtime health (`x-conductor-runtime-health`) so the
|
|
9
|
+
backend can reject task creation with `503 runtime_unavailable` before any
|
|
10
|
+
timeline activity, and adds `disable_built_in_cli_list` to opt out of built-in
|
|
11
|
+
SDK backends. The SDK attributes agent-originated scheduled-message calls with
|
|
12
|
+
`X-Conductor-Actor: agent` so per-task `agent_schedule_access`
|
|
13
|
+
(full/read_only/blocked) can govern `conductor task schedule` from agents while
|
|
14
|
+
human/UI calls stay unrestricted.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 43c4f87: Fix `conductor fire` and `conductor diagnose` with an explicit `--config-file`
|
|
19
|
+
resolving `agent_token`/`backend_url` from daemon-injected `CONDUCTOR_*` env
|
|
20
|
+
instead of the file. Fire could dial the file's websocket with the inherited
|
|
21
|
+
token and loop on `4002 invalid-token`; diagnose queried the wrong backend and
|
|
22
|
+
404'd. An explicit config file now wins over inherited env, matching the
|
|
23
|
+
daemon's behavior.
|
|
24
|
+
- 43c4f87: Fix `conductor update` installing into the wrong npm prefix.
|
|
25
|
+
- Updated dependencies [43c4f87]
|
|
26
|
+
- Updated dependencies [43c4f87]
|
|
27
|
+
- @love-moon/ai-sdk@0.10.0
|
|
28
|
+
- @love-moon/conductor-sdk@0.10.0
|
|
29
|
+
|
|
3
30
|
## 0.9.0
|
|
4
31
|
|
|
5
32
|
### Minor Changes
|
package/bin/conductor-config.js
CHANGED
|
@@ -319,6 +319,16 @@ async function main() {
|
|
|
319
319
|
});
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
lines.push(
|
|
323
|
+
"",
|
|
324
|
+
"# Uncomment to disable built-in SDK backends that are otherwise advertised",
|
|
325
|
+
"# automatically (copilot, dsh). Listing a name here hides it from the",
|
|
326
|
+
"# daemon's supported backends without removing the shipped dependency.",
|
|
327
|
+
"# disable_built_in_cli_list:",
|
|
328
|
+
"# - dsh",
|
|
329
|
+
"# - copilot"
|
|
330
|
+
);
|
|
331
|
+
|
|
322
332
|
lines.push(
|
|
323
333
|
"",
|
|
324
334
|
"# Uncomment to use custom envs, such as proxy.",
|
|
@@ -6,6 +6,8 @@ import { hideBin } from "yargs/helpers";
|
|
|
6
6
|
|
|
7
7
|
import { loadConfig } from "@love-moon/conductor-sdk";
|
|
8
8
|
|
|
9
|
+
import { envForExplicitConfigFile } from "../src/config-env.js";
|
|
10
|
+
|
|
9
11
|
const CLI_NAME = process.env.CONDUCTOR_CLI_NAME || "conductor diagnose";
|
|
10
12
|
const DEFAULT_TIMEOUT_MS = 8000;
|
|
11
13
|
|
|
@@ -45,7 +47,7 @@ main().catch((error) => {
|
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
async function main() {
|
|
48
|
-
const config = loadConfig(args.configFile);
|
|
50
|
+
const config = loadConfig(args.configFile, { env: envForExplicitConfigFile(args.configFile) });
|
|
49
51
|
const timeoutMs = normalizePositiveInt(args.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
50
52
|
const baseUrl = String(config.backendUrl || "").replace(/\/+$/, "");
|
|
51
53
|
const endpoint = `${baseUrl}/api/diagnostics/tasks/${encodeURIComponent(taskId)}`;
|
package/bin/conductor-fire.js
CHANGED
|
@@ -583,6 +583,17 @@ export function createPendingRemoteInterruptQueue() {
|
|
|
583
583
|
async function main() {
|
|
584
584
|
syncPwdEnvWithProcessCwdForDaemonLaunch();
|
|
585
585
|
const cliArgs = await parseCliArgs();
|
|
586
|
+
if (cliArgs.configFile) {
|
|
587
|
+
// An explicit --config-file must win over CONDUCTOR_* backend/token env
|
|
588
|
+
// injected by an owning daemon or session (same convention as
|
|
589
|
+
// envForExplicitConfigFile); otherwise fire dials the file's websocket
|
|
590
|
+
// with the inherited token and loops on 4002 invalid-token.
|
|
591
|
+
delete process.env.CONDUCTOR_AGENT_TOKEN;
|
|
592
|
+
delete process.env.CONDUCTOR_BACKEND_URL;
|
|
593
|
+
delete process.env.CONDUCTOR_WS_URL;
|
|
594
|
+
delete process.env.CONDUCTOR_BACKEND_WS_URL;
|
|
595
|
+
process.env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
596
|
+
}
|
|
586
597
|
let runtimeProjectPath = process.cwd();
|
|
587
598
|
let backendSession = null;
|
|
588
599
|
|
package/bin/conductor-update.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
fetchLatestVersion,
|
|
18
18
|
isNewerVersion,
|
|
19
19
|
detectPackageManager,
|
|
20
|
+
resolveGlobalInstallPrefix,
|
|
20
21
|
} from "../src/version-check.js";
|
|
21
22
|
import {
|
|
22
23
|
buildPnpmAllowBuildArgs,
|
|
@@ -163,6 +164,18 @@ async function performUpdate() {
|
|
|
163
164
|
console.log(` Using package manager: ${colorize(packageManager, "cyan")}`);
|
|
164
165
|
console.log("");
|
|
165
166
|
|
|
167
|
+
// `npm install -g` targets whichever npm wins the PATH lookup, which is not necessarily the
|
|
168
|
+
// one that installed us -- a Conductor-managed Node, an nvm shim and a system npm all resolve
|
|
169
|
+
// to different prefixes. When they disagree the update lands in a tree nobody runs, so
|
|
170
|
+
// `conductor --version` keeps reporting the old build and the next update repeats the cycle.
|
|
171
|
+
// Pin every child command to the prefix the running package actually lives in.
|
|
172
|
+
const installPrefix = packageManager === "npm" ? resolveGlobalInstallPrefix(PKG_ROOT) : null;
|
|
173
|
+
if (installPrefix) {
|
|
174
|
+
process.env.npm_config_prefix = installPrefix;
|
|
175
|
+
console.log(` Install prefix: ${colorize(installPrefix, "cyan")}`);
|
|
176
|
+
console.log("");
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
if (packageManager === "pnpm") {
|
|
167
180
|
console.log(" Preparing pnpm native dependency allowlist...");
|
|
168
181
|
await ensurePnpmOnlyBuiltDependencies({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"gitCommitId": "bc7b3a5",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test": "node --test test/*.test.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@love-moon/ai-sdk": "0.
|
|
28
|
-
"@love-moon/conductor-sdk": "0.
|
|
27
|
+
"@love-moon/ai-sdk": "0.10.0",
|
|
28
|
+
"@love-moon/conductor-sdk": "0.10.0",
|
|
29
29
|
"@github/copilot-sdk": "^0.3.0",
|
|
30
30
|
"chrome-launcher": "^1.2.1",
|
|
31
31
|
"chrome-remote-interface": "^0.33.0",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
40
|
"@roamhq/wrtc": "^0.10.0",
|
|
41
|
-
"@love-moon/chat-web": "0.
|
|
41
|
+
"@love-moon/chat-web": "0.10.0"
|
|
42
42
|
},
|
|
43
43
|
"pnpm": {
|
|
44
44
|
"onlyBuiltDependencies": [
|
package/src/daemon.js
CHANGED
|
@@ -353,6 +353,57 @@ function serializeRuntimeBackendMap(runtimeBackendMap) {
|
|
|
353
353
|
.join(",");
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
// Backends whose install/auth health can be probed via the AI manager. Others
|
|
357
|
+
// (external providers) are left out of the advertised health map so the web
|
|
358
|
+
// runtime preflight fails open for them.
|
|
359
|
+
const RUNTIME_HEALTH_TOOLS = new Set(["codex", "claude", "copilot", "kimi", "dsh"]);
|
|
360
|
+
|
|
361
|
+
function runtimeHealthToolForBackend(backend, runtimeBackendMap) {
|
|
362
|
+
const normalized = String(backend || "").trim().toLowerCase();
|
|
363
|
+
if (RUNTIME_HEALTH_TOOLS.has(normalized)) {
|
|
364
|
+
return normalized;
|
|
365
|
+
}
|
|
366
|
+
const runtime = String(runtimeBackendMap?.[normalized] || "").trim().toLowerCase();
|
|
367
|
+
return RUNTIME_HEALTH_TOOLS.has(runtime) ? runtime : null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function serializeRuntimeHealth(runtimeHealth) {
|
|
371
|
+
if (!runtimeHealth || typeof runtimeHealth !== "object") {
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
return Object.entries(runtimeHealth)
|
|
375
|
+
.filter(([backend, state]) => backend && state)
|
|
376
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
377
|
+
.map(([backend, state]) => `${backend}=${state}`)
|
|
378
|
+
.join(",");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Best-effort per-backend runtime health for the web runtime preflight. Only a
|
|
382
|
+
// cheap install probe (which/--version) runs, once at startup. We advertise
|
|
383
|
+
// only a POSITIVE `ready` confirmation: a supported backend whose default-name
|
|
384
|
+
// `which` probe fails is very likely resolved via a custom or absolute path
|
|
385
|
+
// (allow_cli_list, or a systemd PATH that differs from the login shell) and is
|
|
386
|
+
// still runnable, so reporting it as "missing" would be an unreliable false
|
|
387
|
+
// negative that could wrongly block task creation. Omitting it lets the web
|
|
388
|
+
// preflight fail open for that backend.
|
|
389
|
+
async function computeAdvertisedRuntimeHealth(manager, supportedBackends, runtimeBackendMap) {
|
|
390
|
+
if (!manager || typeof manager.checkInstallAll !== "function") {
|
|
391
|
+
return {};
|
|
392
|
+
}
|
|
393
|
+
const install = await manager.checkInstallAll();
|
|
394
|
+
const runtimeHealth = {};
|
|
395
|
+
for (const backend of supportedBackends || []) {
|
|
396
|
+
const tool = runtimeHealthToolForBackend(backend, runtimeBackendMap);
|
|
397
|
+
if (!tool) {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (install?.[tool]?.installed) {
|
|
401
|
+
runtimeHealth[String(backend).trim().toLowerCase()] = "ready";
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return runtimeHealth;
|
|
405
|
+
}
|
|
406
|
+
|
|
356
407
|
async function defaultCreatePty(command, args, options) {
|
|
357
408
|
if (!nodePtySpawnPromise) {
|
|
358
409
|
const spawnHelperInfo = ensureNodePtySpawnHelperExecutable();
|
|
@@ -3085,6 +3136,28 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3085
3136
|
if (typeof client?.setExtraHeaders === "function") {
|
|
3086
3137
|
client.setExtraHeaders(extraHeaders);
|
|
3087
3138
|
}
|
|
3139
|
+
|
|
3140
|
+
// Advertise best-effort runtime health so the backend can reject a task
|
|
3141
|
+
// whose backend is configured but cannot start (CLI missing/not signed in)
|
|
3142
|
+
// before creating any timeline activity. Fully additive and guarded: any
|
|
3143
|
+
// failure just omits the header and the preflight fails open.
|
|
3144
|
+
try {
|
|
3145
|
+
const runtimeHealth = await computeAdvertisedRuntimeHealth(
|
|
3146
|
+
aiManagerHandlers?.manager,
|
|
3147
|
+
SUPPORTED_BACKENDS,
|
|
3148
|
+
SUPPORTED_BACKEND_RUNTIME_MAP,
|
|
3149
|
+
);
|
|
3150
|
+
const serializedRuntimeHealth = serializeRuntimeHealth(runtimeHealth);
|
|
3151
|
+
if (serializedRuntimeHealth) {
|
|
3152
|
+
extraHeaders["x-conductor-runtime-health"] = serializedRuntimeHealth;
|
|
3153
|
+
if (typeof client?.setExtraHeaders === "function") {
|
|
3154
|
+
client.setExtraHeaders(extraHeaders);
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
logError(`Failed to probe runtime health: ${error?.message || error}`);
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3088
3161
|
if (daemonShuttingDown) {
|
|
3089
3162
|
return;
|
|
3090
3163
|
}
|
package/src/runtime-backends.js
CHANGED
|
@@ -366,6 +366,46 @@ function readConfigEnvValue(configFilePath, key) {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
// Read the top-level `disable_built_in_cli_list` config key: the mirror of
|
|
370
|
+
// `allow_cli_list` for the command-optional built-in backends (copilot, dsh)
|
|
371
|
+
// that are advertised WITHOUT any allow_cli_list entry. Listing a name here
|
|
372
|
+
// suppresses that automatic advertising so an operator can opt out of a
|
|
373
|
+
// built-in they don't want. Scoped to command-optional built-ins on purpose:
|
|
374
|
+
// a stray external or PATH-CLI name (e.g. "codex") is a documented no-op,
|
|
375
|
+
// since those never auto-advertise in the first place.
|
|
376
|
+
function readDisabledBuiltInBackends(configFilePath) {
|
|
377
|
+
const targetPath = resolveConductorConfigPath(configFilePath);
|
|
378
|
+
try {
|
|
379
|
+
if (!targetPath || !fs.existsSync(targetPath)) {
|
|
380
|
+
return new Set();
|
|
381
|
+
}
|
|
382
|
+
const parsed = yaml.load(fs.readFileSync(targetPath, "utf8"));
|
|
383
|
+
const raw = parsed?.disable_built_in_cli_list;
|
|
384
|
+
if (!Array.isArray(raw)) {
|
|
385
|
+
return new Set();
|
|
386
|
+
}
|
|
387
|
+
return new Set(
|
|
388
|
+
raw
|
|
389
|
+
.map((value) => normalizeRuntimeBackendName(value))
|
|
390
|
+
.filter((value) => value && isCommandOptionalBuiltInRuntimeBackend(value)),
|
|
391
|
+
);
|
|
392
|
+
} catch {
|
|
393
|
+
return new Set();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Reuse a caller-provided disabled set when present (so a hot loop reads the
|
|
398
|
+
// config file once), else read it now. Keeps every call site one line.
|
|
399
|
+
function resolveDisabledBuiltIns(options = {}) {
|
|
400
|
+
return options.disabledBuiltIns instanceof Set
|
|
401
|
+
? options.disabledBuiltIns
|
|
402
|
+
: readDisabledBuiltInBackends(options.configFilePath);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function isDisabledBuiltInRuntimeBackend(backend, options = {}) {
|
|
406
|
+
return resolveDisabledBuiltIns(options).has(normalizeRuntimeBackendName(backend));
|
|
407
|
+
}
|
|
408
|
+
|
|
369
409
|
function resolveProviderModulePaths(options = {}) {
|
|
370
410
|
return [
|
|
371
411
|
...listProviderModulePaths(process.env.AISDK_PROVIDER_PATH),
|
|
@@ -577,6 +617,21 @@ export async function filterRuntimeSupportedAllowCliList(allowCliList, options =
|
|
|
577
617
|
}
|
|
578
618
|
|
|
579
619
|
export async function resolveConfiguredRuntimeBackend(backend, allowCliList, options = {}) {
|
|
620
|
+
const resolved = await resolveConfiguredRuntimeBackendInner(backend, allowCliList, options);
|
|
621
|
+
if (!resolved?.runtimeBackend) {
|
|
622
|
+
return resolved;
|
|
623
|
+
}
|
|
624
|
+
// A disabled command-optional built-in (copilot/dsh via
|
|
625
|
+
// `disable_built_in_cli_list`) is never resolvable, whether it reached here
|
|
626
|
+
// through an explicit allow_cli_list entry or the command-optional fallback.
|
|
627
|
+
const disabled = resolveDisabledBuiltIns(options);
|
|
628
|
+
if (disabled.has(resolved.runtimeBackend) || disabled.has(resolved.requestedBackend)) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
return resolved;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function resolveConfiguredRuntimeBackendInner(backend, allowCliList, options = {}) {
|
|
580
635
|
const normalizedBackend = normalizeRuntimeBackendName(backend);
|
|
581
636
|
if (!normalizedBackend || LEGACY_RUNTIME_BACKEND_ALIASES.has(normalizedBackend)) {
|
|
582
637
|
return null;
|
|
@@ -634,6 +689,9 @@ export async function listAdvertisedBackends(allowCliList, options = {}) {
|
|
|
634
689
|
)
|
|
635
690
|
: {};
|
|
636
691
|
const configuredBackends = Object.keys(filteredAllowCliList);
|
|
692
|
+
// Read the disable list once and thread it through every per-backend
|
|
693
|
+
// resolve below, so this whole pass touches the config file a single time.
|
|
694
|
+
const resolveOptions = { ...options, disabledBuiltIns: readDisabledBuiltInBackends(options.configFilePath) };
|
|
637
695
|
let runtimeBackends = [...BUILT_IN_RUNTIME_BACKENDS];
|
|
638
696
|
let discoveryError = null;
|
|
639
697
|
try {
|
|
@@ -647,7 +705,7 @@ export async function listAdvertisedBackends(allowCliList, options = {}) {
|
|
|
647
705
|
|
|
648
706
|
for (const backend of configuredBackends) {
|
|
649
707
|
try {
|
|
650
|
-
const configuredBackend = await resolveConfiguredRuntimeBackend(backend, filteredAllowCliList,
|
|
708
|
+
const configuredBackend = await resolveConfiguredRuntimeBackend(backend, filteredAllowCliList, resolveOptions);
|
|
651
709
|
if (!configuredBackend?.runtimeBackend) {
|
|
652
710
|
continue;
|
|
653
711
|
}
|
|
@@ -683,7 +741,10 @@ export async function listAdvertisedBackends(allowCliList, options = {}) {
|
|
|
683
741
|
}
|
|
684
742
|
|
|
685
743
|
const commandOptionalBuiltIns = BUILT_IN_RUNTIME_BACKENDS.filter(
|
|
686
|
-
(backend) =>
|
|
744
|
+
(backend) =>
|
|
745
|
+
isCommandOptionalBuiltInRuntimeBackend(backend) &&
|
|
746
|
+
!runtimeBackendMap[backend] &&
|
|
747
|
+
!resolveOptions.disabledBuiltIns.has(backend),
|
|
687
748
|
);
|
|
688
749
|
for (const backend of commandOptionalBuiltIns) {
|
|
689
750
|
runtimeBackendMap[backend] = backend;
|
package/src/version-check.js
CHANGED
|
@@ -13,6 +13,7 @@ export const DEFAULT_HOMEBREW_FORMULA = "lovemoon-ai/tap/conductor";
|
|
|
13
13
|
const DEFAULT_UPDATE_WINDOW = { startMinutes: 120, endMinutes: 240 };
|
|
14
14
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
15
15
|
const INSTALL_METHOD_FILENAME = ".install-method";
|
|
16
|
+
const GLOBAL_PACKAGE_MARKER = `${path.sep}lib${path.sep}node_modules${path.sep}`;
|
|
16
17
|
|
|
17
18
|
function resolveTimeoutMs(value) {
|
|
18
19
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
@@ -139,6 +140,28 @@ export function resolveInstallMethod(options = {}) {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Derive the npm global prefix that the currently running package was installed into.
|
|
145
|
+
*
|
|
146
|
+
* A global install always lands at `<prefix>/lib/node_modules/<package>`, so the prefix is
|
|
147
|
+
* whatever precedes that marker. Returns `null` for layouts that are not a global install
|
|
148
|
+
* (git checkout, project-local `node_modules`), where the caller should leave npm's own
|
|
149
|
+
* prefix resolution alone.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveGlobalInstallPrefix(packageRoot) {
|
|
152
|
+
if (typeof packageRoot !== "string" || !packageRoot.trim()) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const normalized = path.resolve(packageRoot);
|
|
157
|
+
const markerIndex = normalized.lastIndexOf(GLOBAL_PACKAGE_MARKER);
|
|
158
|
+
if (markerIndex <= 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return normalized.slice(0, markerIndex);
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
export function buildUpgradeCommand(options = {}) {
|
|
143
166
|
const installMethod = resolveInstallMethod(options);
|
|
144
167
|
if (installMethod === "homebrew") {
|