@nowcrew/daemon 0.6.88-beta.27 → 0.6.88-beta.29
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/README.md +7 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/config.js +7 -0
- package/dist/execution-runner.js +2 -0
- package/dist/list-models.js +12 -12
- package/dist/local-executor.js +20 -48
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +24 -16
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/marketplace-controller.js +75 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-runner.js +234 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/message-file-preview.js +239 -0
- package/dist/provider-env.js +26 -12
- package/dist/runner.js +14 -0
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +2 -2
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness.js +17 -12
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +7 -6
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +58 -105
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,6 +40,13 @@ This compatibility form stays resident in the current terminal and reconnects wi
|
|
|
40
40
|
`pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
|
|
41
41
|
manual one-shot entry.
|
|
42
42
|
|
|
43
|
+
## Machine Environment
|
|
44
|
+
|
|
45
|
+
The daemon loads exported variables from its OS account’s bash/zsh configuration on macOS/Linux,
|
|
46
|
+
and native system/user variables on Windows. Agent directory layers override ordinary machine values;
|
|
47
|
+
running tasks keep immutable snapshots. See [machine environment](../docs/machine-environment.md) for
|
|
48
|
+
source rules, refresh behavior, local controls and the Windows native acceptance status.
|
|
49
|
+
|
|
43
50
|
## Computer Profiles And Service Lifecycle
|
|
44
51
|
|
|
45
52
|
Persist a named connection without putting its machine token in shell history or service arguments:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureTrainingEnvironmentFiles, isEnvironmentOnlyTrainingDirectory, restoreTrainingEnvironmentFiles, withEnvironmentFilesLock } from "../agent-environments.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { chmod, cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
@@ -315,7 +316,7 @@ const assertManagedTrainingTreeIfPresent = async (root, errorCode) => {
|
|
|
315
316
|
if (!await isRealDirectory(root))
|
|
316
317
|
throw new Error(errorCode);
|
|
317
318
|
const catalog = await readManagedCatalog(root).catch(() => null);
|
|
318
|
-
if (catalog?.managedBy !== "nowcrew-agent-training")
|
|
319
|
+
if (catalog?.managedBy !== "nowcrew-agent-training" && !await isEnvironmentOnlyTrainingDirectory(root))
|
|
319
320
|
throw new Error(errorCode);
|
|
320
321
|
return true;
|
|
321
322
|
};
|
|
@@ -404,7 +405,7 @@ const switchTrainingDirectory = async (agentRoot, staging) => {
|
|
|
404
405
|
throw error;
|
|
405
406
|
}
|
|
406
407
|
};
|
|
407
|
-
const restoreTrainingRollback = async (agentRoot, ability) => {
|
|
408
|
+
const restoreTrainingRollback = async (agentRoot, ability, environmentFiles) => {
|
|
408
409
|
await recoverTrainingSwitch(agentRoot);
|
|
409
410
|
const currentCatalog = await readManagedCatalog(join(agentRoot, "training")).catch(() => null);
|
|
410
411
|
const rollbackRoot = trainingRollbackRoot(agentRoot);
|
|
@@ -419,6 +420,8 @@ const restoreTrainingRollback = async (agentRoot, ability) => {
|
|
|
419
420
|
const discardRoot = trainingDiscardRoot(agentRoot);
|
|
420
421
|
await assertManagedTrainingTreeIfPresent(discardRoot, "ability_training_discard_invalid");
|
|
421
422
|
await removeManagedTree(discardRoot);
|
|
423
|
+
// 回滚目录先携带当前环境集合,再发布;旧来源文件仅按该目录自己的哈希证明删除。
|
|
424
|
+
await restoreTrainingEnvironmentFiles(rollbackRoot, environmentFiles);
|
|
422
425
|
await writeTrainingSwitchJournal(agentRoot, { operation: "restore", phase: "prepared" });
|
|
423
426
|
await finishTrainingRestore(agentRoot);
|
|
424
427
|
dslog("ability.workspace.rollback_restored", "Previous Agent training directory restored locally", {
|
|
@@ -443,7 +446,7 @@ const activeTrainingRoot = async (agentsRoot, handle, ability) => {
|
|
|
443
446
|
? trainingRoot
|
|
444
447
|
: null;
|
|
445
448
|
};
|
|
446
|
-
const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
449
|
+
const materializeWorkspaceTraining = async (agentsRoot, handle, ability, environmentFiles) => {
|
|
447
450
|
let verified;
|
|
448
451
|
try {
|
|
449
452
|
verified = await verifyCache(agentsRoot, ability);
|
|
@@ -477,10 +480,10 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
477
480
|
if (!await isRealDirectory(trainingRoot))
|
|
478
481
|
throw new Error("ability_workspace_training_conflict");
|
|
479
482
|
previousCatalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
480
|
-
if (previousCatalog?.managedBy !== "nowcrew-agent-training") {
|
|
483
|
+
if (previousCatalog?.managedBy !== "nowcrew-agent-training" && !await isEnvironmentOnlyTrainingDirectory(trainingRoot)) {
|
|
481
484
|
throw new Error("ability_workspace_training_conflict");
|
|
482
485
|
}
|
|
483
|
-
if (previousCatalog
|
|
486
|
+
if (previousCatalog?.releaseId === ability.releaseId
|
|
484
487
|
&& previousCatalog.artifactDigest === ability.artifactDigest
|
|
485
488
|
&& previousCatalog.projectionDigest === projectionDigest
|
|
486
489
|
&& await projectedSkillsComplete(trainingRoot, ability.skills))
|
|
@@ -578,6 +581,8 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
578
581
|
await copyMissingEntries(previous, target);
|
|
579
582
|
}
|
|
580
583
|
}
|
|
584
|
+
// staging 同时准备训练资产和环境文件;rename 后即是完整可用的目录。
|
|
585
|
+
await restoreTrainingEnvironmentFiles(staging, environmentFiles);
|
|
581
586
|
await switchTrainingDirectory(agentRoot, staging);
|
|
582
587
|
}
|
|
583
588
|
catch (error) {
|
|
@@ -588,11 +593,17 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
588
593
|
};
|
|
589
594
|
const ensureWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
590
595
|
const agentRoot = join(agentsRoot, handle);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
+
return withEnvironmentFilesLock(agentRoot, async () => {
|
|
597
|
+
await recoverTrainingSwitch(agentRoot);
|
|
598
|
+
const environmentFiles = await captureTrainingEnvironmentFiles(agentRoot);
|
|
599
|
+
const trainingRoot = await activeTrainingRoot(agentsRoot, handle, ability)
|
|
600
|
+
?? await restoreTrainingRollback(agentRoot, ability, environmentFiles)
|
|
601
|
+
?? await materializeWorkspaceTraining(agentsRoot, handle, ability, environmentFiles);
|
|
602
|
+
const rollback = trainingRollbackRoot(agentRoot);
|
|
603
|
+
if (await exists(rollback) && await isEnvironmentOnlyTrainingDirectory(rollback))
|
|
604
|
+
await removeManagedTree(rollback);
|
|
605
|
+
return trainingRoot;
|
|
606
|
+
});
|
|
596
607
|
};
|
|
597
608
|
const projectWorkspaceTraining = async (agentsRoot, handle, ability, trainingRoot) => {
|
|
598
609
|
const agentRoot = join(agentsRoot, handle);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { parseEnv } from "node:util";
|
|
3
|
+
export const AGENT_ENVIRONMENTS_CAPABILITY = "agent_environments_v1";
|
|
4
|
+
const identifier = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
5
|
+
const environmentId = /^[a-z][a-z0-9_-]{0,47}$/u;
|
|
6
|
+
export const environmentRelativePath = (value) => value === "" || value === "training"
|
|
7
|
+
|| /^repos\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
|
|
8
|
+
const reserved = new Set(["PATH", "HOME", "USERPROFILE", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "GH_CONFIG_DIR", "CLOUDSDK_CONFIG", "NPM_CONFIG_USERCONFIG", "YARN_REGISTRY", "NPM_CONFIG_ALWAYS_AUTH", "QA_REPORT_HUB", "QA_RUNTIME_ROOT", "QA_REPORT_DELIVERY_CONTEXT", "KIMI_MODEL_THINKING_EFFORT"]);
|
|
9
|
+
export const validEnvironmentKey = (key) => /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(key)
|
|
10
|
+
&& !/^(CREW_|NOWWORK_)/iu.test(key) && !reserved.has(key.toUpperCase());
|
|
11
|
+
const ScopeSchema = z.object({
|
|
12
|
+
id: z.string().min(1).max(160), path: z.string().refine(environmentRelativePath),
|
|
13
|
+
activeEnvironmentId: z.string().regex(environmentId).nullable(),
|
|
14
|
+
files: z.array(z.object({
|
|
15
|
+
name: z.string().regex(/^\.env(?:\.[a-z][a-z0-9_-]{0,47})?$/u),
|
|
16
|
+
content: z.string().max(256 * 1024).refine(value => !value.includes("\0")),
|
|
17
|
+
}).strict()).max(21),
|
|
18
|
+
effective: z.record(z.string().max(4096).refine(value => !value.includes("\0"))),
|
|
19
|
+
}).strict();
|
|
20
|
+
export const AgentEnvironmentPlanSchema = z.object({
|
|
21
|
+
revision: z.number().int().nonnegative(), scopes: z.array(ScopeSchema).min(1).max(102),
|
|
22
|
+
}).strict().superRefine((plan, context) => {
|
|
23
|
+
const paths = new Set(), ids = new Set();
|
|
24
|
+
for (const scope of plan.scopes) {
|
|
25
|
+
const expectedId = scope.path === "" ? "root" : scope.path === "training" ? "training" : `repo:${scope.path.slice(6)}`;
|
|
26
|
+
if (scope.id !== expectedId || paths.has(scope.path) || ids.has(scope.id))
|
|
27
|
+
context.addIssue({ code: "custom", message: "environment_scope_invalid" });
|
|
28
|
+
paths.add(scope.path);
|
|
29
|
+
ids.add(scope.id);
|
|
30
|
+
const names = new Set(scope.files.map(file => file.name));
|
|
31
|
+
if (names.size !== scope.files.length || (scope.activeEnvironmentId !== null && !names.has(`.env.${scope.activeEnvironmentId}`))) {
|
|
32
|
+
context.addIssue({ code: "custom", message: "environment_files_invalid" });
|
|
33
|
+
}
|
|
34
|
+
if (Object.keys(scope.effective).length > 200 || Object.keys(scope.effective).some(key => !validEnvironmentKey(key))) {
|
|
35
|
+
context.addIssue({ code: "custom", message: "environment_key_invalid" });
|
|
36
|
+
}
|
|
37
|
+
for (const file of scope.files) {
|
|
38
|
+
const parsed = parseEnv(file.content);
|
|
39
|
+
if (Object.keys(parsed).some(key => !validEnvironmentKey(key)))
|
|
40
|
+
context.addIssue({ code: "custom", message: "environment_key_invalid" });
|
|
41
|
+
if (file.name === ".env" && (Object.keys(parsed).length !== Object.keys(scope.effective).length
|
|
42
|
+
|| Object.entries(scope.effective).some(([key, value]) => parsed[key] !== value))) {
|
|
43
|
+
context.addIssue({ code: "custom", message: "environment_effective_file_mismatch" });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!names.has(".env") && Object.keys(scope.effective).length > 0)
|
|
47
|
+
context.addIssue({ code: "custom", message: "environment_effective_file_missing" });
|
|
48
|
+
}
|
|
49
|
+
if (!paths.has(""))
|
|
50
|
+
context.addIssue({ code: "custom", message: "environment_root_missing" });
|
|
51
|
+
});
|
|
52
|
+
export const AgentEnvironmentsApplySchema = z.object({
|
|
53
|
+
type: z.literal("agent:environments:apply"), reqId: z.string().min(1).max(200),
|
|
54
|
+
handle: z.string().regex(identifier), plan: AgentEnvironmentPlanSchema,
|
|
55
|
+
}).strict();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { AgentEnvironmentsApplySchema } from "./agent-environment-contract.js";
|
|
3
|
+
import { applyAgentEnvironmentPlan } from "./agent-environments.js";
|
|
4
|
+
export async function handleAgentEnvironmentFrame(decoded, agentsRoot, send) {
|
|
5
|
+
if (typeof decoded !== "object" || decoded === null || !("type" in decoded)
|
|
6
|
+
|| decoded.type !== "agent:environments:apply")
|
|
7
|
+
return false;
|
|
8
|
+
const parsed = AgentEnvironmentsApplySchema.safeParse(decoded);
|
|
9
|
+
const reqId = "reqId" in decoded && typeof decoded.reqId === "string" ? decoded.reqId.slice(0, 200) : "invalid";
|
|
10
|
+
if (!parsed.success) {
|
|
11
|
+
send({ type: "fs:result", reqId, ok: false, error: "environment_plan_invalid" });
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const data = await applyAgentEnvironmentPlan(join(agentsRoot, parsed.data.handle), parsed.data.plan);
|
|
16
|
+
send({ type: "fs:result", reqId, ok: true, data });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error && /^environment_[a-z_]+(?::[A-Za-z0-9_./-]+)?$/u.test(error.message)
|
|
20
|
+
? error.message : "environment_apply_failed";
|
|
21
|
+
send({ type: "fs:result", reqId, ok: false, error: message });
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { providerOwnedEnvironmentKeys } from "./provider-env.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { AgentEnvironmentPlanSchema } from "./agent-environment-contract.js";
|
|
6
|
+
import { applyAgentEnvironmentPlan, environmentForExecution } from "./agent-environments.js";
|
|
7
|
+
import { mergeRuntimeEnvironments, runtimeEnvironmentValue } from "./runtime-environment-merge.js";
|
|
8
|
+
export function environmentDirectoryContains(root, cwd, paths) {
|
|
9
|
+
const path = paths.relative(root, cwd);
|
|
10
|
+
return path === "" || (path !== ".." && !path.startsWith(`..${paths.sep}`) && !paths.isAbsolute(path));
|
|
11
|
+
}
|
|
12
|
+
/** 每次执行的不可变变量快照,供 Agent 显式在不同工作目录运行工具时加载。 */
|
|
13
|
+
const RUNNER = `import { readFileSync } from 'node:fs';
|
|
14
|
+
import { dirname, resolve, relative, sep, isAbsolute } from 'node:path';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
const snapshot = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), 'snapshot.json'), 'utf8'));
|
|
17
|
+
const { default: spawn } = await import(pathToFileURL(snapshot.spawnModule).href);
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
if (args[0] !== '--cwd' || !args[1] || args[2] !== '--' || !args[3]) {
|
|
20
|
+
process.stderr.write('Usage: node run.mjs --cwd <directory> -- <command> [arguments...]\\n'); process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
const cwd = resolve(args[1]);
|
|
23
|
+
const directoryContains = ${environmentDirectoryContains.toString()};
|
|
24
|
+
const matches = snapshot.scopes.filter(s => directoryContains(s.root,cwd,{relative,isAbsolute,sep}));
|
|
25
|
+
const scope = matches.sort((a,b)=>b.root.length-a.root.length)[0] || snapshot.scopes.find(s=>s.id==='root');
|
|
26
|
+
const merge = ${mergeRuntimeEnvironments.toString()};
|
|
27
|
+
let env = {...(snapshot.executionBaseEnvironment ?? process.env)};
|
|
28
|
+
for (const [key,value] of Object.entries(snapshot.ambient)) {
|
|
29
|
+
for(const existing of Object.keys(env))if(snapshot.platform==='win32'?existing.toUpperCase()===key.toUpperCase():existing===key)delete env[existing];
|
|
30
|
+
if(value!==null)env[key]=value;
|
|
31
|
+
}
|
|
32
|
+
env = merge([env,scope?.effective||{}],snapshot.platform);
|
|
33
|
+
for (const [key,value] of Object.entries(snapshot.runtimeOverrides)) {for(const existing of Object.keys(env))if(snapshot.platform==='win32'?existing.toUpperCase()===key.toUpperCase():existing===key)delete env[existing];if(value!==null)env[key]=value;}
|
|
34
|
+
const child=spawn(args[3],args.slice(4),{cwd,env,stdio:'inherit',shell:false});
|
|
35
|
+
child.on('error',()=>{process.stderr.write('environment_command_failed\\n');process.exitCode=1;});
|
|
36
|
+
for(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>child.kill(signal));
|
|
37
|
+
child.on('exit',(code,signal)=>{process.exitCode=code??(signal==='SIGINT'?130:143);});
|
|
38
|
+
`;
|
|
39
|
+
export async function prepareEnvironmentExecutionSnapshot(input) {
|
|
40
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/u.test(input.executionId))
|
|
41
|
+
throw new Error("environment_execution_id_invalid");
|
|
42
|
+
const parsed = AgentEnvironmentPlanSchema.safeParse(input.plan);
|
|
43
|
+
if (!parsed.success)
|
|
44
|
+
throw new Error("environment_plan_invalid");
|
|
45
|
+
// 文件更新失败须终止本次准备,不能使用旧文件却声称新环境已就绪。
|
|
46
|
+
try {
|
|
47
|
+
await applyAgentEnvironmentPlan(input.agentRoot, parsed.data);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
// 令牌签发后可能已有较新配置落盘。本轮保留签发时快照,不回滚共享文件或丢掉任务。
|
|
51
|
+
if (!(error instanceof Error) || error.message !== "environment_revision_stale")
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const directory = join(input.runDir, `.environment-${input.executionId}`);
|
|
55
|
+
await mkdir(directory, { mode: 0o700 });
|
|
56
|
+
try {
|
|
57
|
+
const scopes = parsed.data.scopes.map(scope => ({
|
|
58
|
+
id: scope.id,
|
|
59
|
+
root: join(input.agentRoot, scope.path),
|
|
60
|
+
effective: scope.effective,
|
|
61
|
+
}));
|
|
62
|
+
for (const project of input.projectRoots ?? []) {
|
|
63
|
+
scopes.push({ id: `repo:${project.projectId}`, root: project.root,
|
|
64
|
+
effective: environmentForExecution(parsed.data, input.agentRoot, project.root, project.projectId) });
|
|
65
|
+
}
|
|
66
|
+
const keys = [...new Set(parsed.data.scopes.flatMap(scope => Object.keys(scope.effective)))];
|
|
67
|
+
const platform = input.platform ?? process.platform;
|
|
68
|
+
const identity = (key) => platform === "win32" ? key.toUpperCase() : key;
|
|
69
|
+
const providerKeys = providerOwnedEnvironmentKeys(input.runtime ?? "", input.providerConfig ?? {}, keys, platform);
|
|
70
|
+
const owned = new Map([...providerKeys, ...input.authoritativeKeys ?? []].map(key => [identity(key), key]));
|
|
71
|
+
const value = (env, key) => runtimeEnvironmentValue(env, key, platform);
|
|
72
|
+
const snapshot = {
|
|
73
|
+
revision: parsed.data.revision, scopes, spawnModule: createRequire(import.meta.url).resolve("cross-spawn"),
|
|
74
|
+
platform,
|
|
75
|
+
...(input.executionBaseEnvironment === undefined ? {} : { executionBaseEnvironment: mergeRuntimeEnvironments([input.executionBaseEnvironment], platform) }),
|
|
76
|
+
...(input.machineEnvironmentVersion === undefined ? {} : { machineEnvironmentVersion: input.machineEnvironmentVersion }),
|
|
77
|
+
runtimeOverrides: Object.fromEntries(keys.filter(key => input.runtimeEnvironment !== undefined
|
|
78
|
+
&& (owned.has(identity(key))
|
|
79
|
+
|| value(input.runtimeEnvironment, key) !== (value(input.selectedEnvironment, key) ?? value(input.ambient, key))))
|
|
80
|
+
.map(key => { const canonical = owned.get(identity(key)) ?? key; return [canonical, value(input.runtimeEnvironment, canonical) ?? null]; })),
|
|
81
|
+
ambient: Object.fromEntries(keys.map(key => [key, value(input.ambient, key) ?? null])),
|
|
82
|
+
};
|
|
83
|
+
await writeFile(join(directory, "snapshot.json"), JSON.stringify(snapshot), { mode: 0o600, flag: "wx" });
|
|
84
|
+
await writeFile(join(directory, "run.mjs"), RUNNER, { mode: 0o600, flag: "wx" });
|
|
85
|
+
return {
|
|
86
|
+
directory,
|
|
87
|
+
prompt: `## Per-directory environment configuration\nThis execution uses environment revision ${parsed.data.revision}. `
|
|
88
|
+
+ `Directory layers are independent: Agent root is inherited by training and each repo; sibling repos do not inherit each other. `
|
|
89
|
+
+ `When running a command in a training or repository directory, use the immutable environment harness: `
|
|
90
|
+
+ `\`node ${JSON.stringify(join(directory, "run.mjs"))} --cwd <absolute-directory> -- <command> [arguments...]\`. `
|
|
91
|
+
+ `Changing directory alone does not reload process variables. Do not source mutable .env files or print environment values.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
await rm(directory, { recursive: true, force: true });
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/** Server 决策生效计划;本模块只做受控文件物化与当前执行目录选择。 */
|
|
2
|
+
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, unlink } from "node:fs/promises";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { acquireProfileSaveLock } from "./computer-profile-lock.js";
|
|
7
|
+
import { AgentEnvironmentPlanSchema } from "./agent-environment-contract.js";
|
|
8
|
+
const STATE_DIRECTORY = ".nowwork-environments";
|
|
9
|
+
const FileHashes = z.record(z.string().regex(/^[a-f0-9]{64}$/u));
|
|
10
|
+
const StateSchema = z.object({
|
|
11
|
+
revision: z.number().int().nonnegative(), digest: z.string(), files: FileHashes,
|
|
12
|
+
pending: z.object({ revision: z.number().int().nonnegative(), digest: z.string(), files: FileHashes }).optional(),
|
|
13
|
+
}).strict();
|
|
14
|
+
const digest = (content) => createHash("sha256").update(content).digest("hex");
|
|
15
|
+
const codeOf = (error) => error.code;
|
|
16
|
+
async function safeDirectory(root, path) {
|
|
17
|
+
let current = root;
|
|
18
|
+
for (const segment of path.split("/").filter(Boolean)) {
|
|
19
|
+
if (segment === "." || segment === ".." || segment.includes("\\"))
|
|
20
|
+
throw new Error("environment_path_invalid");
|
|
21
|
+
current = join(current, segment);
|
|
22
|
+
await mkdir(current, { mode: 0o700 }).catch(error => { if (codeOf(error) !== "EEXIST")
|
|
23
|
+
throw error; });
|
|
24
|
+
const info = await lstat(current);
|
|
25
|
+
if (info.isSymbolicLink())
|
|
26
|
+
throw new Error("environment_path_symlink");
|
|
27
|
+
if (!info.isDirectory())
|
|
28
|
+
throw new Error("environment_path_invalid");
|
|
29
|
+
}
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
async function regularFileContent(path) {
|
|
33
|
+
const info = await lstat(path).catch(error => { if (codeOf(error) === "ENOENT")
|
|
34
|
+
return null; throw error; });
|
|
35
|
+
if (!info)
|
|
36
|
+
return null;
|
|
37
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink > 1)
|
|
38
|
+
throw new Error("environment_file_unsafe");
|
|
39
|
+
if (info.size > 8 * 1024 * 1024)
|
|
40
|
+
throw new Error("environment_file_too_large");
|
|
41
|
+
return readFile(path, "utf8");
|
|
42
|
+
}
|
|
43
|
+
async function atomicWrite(path, content, beforeCommit) {
|
|
44
|
+
const temporary = join(dirname(path), `.env-write-${randomUUID()}`);
|
|
45
|
+
try {
|
|
46
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
47
|
+
try {
|
|
48
|
+
await handle.writeFile(content, "utf8");
|
|
49
|
+
await handle.sync();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
// stage 的慢 IO 完成后重验归属;最终校验 + rename 不是针对外部编辑器的系统级 CAS。
|
|
55
|
+
await beforeCommit?.();
|
|
56
|
+
await rename(temporary, path);
|
|
57
|
+
await chmod(path, 0o600);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await unlink(temporary).catch(error => { if (codeOf(error) !== "ENOENT")
|
|
61
|
+
throw error; });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function requireUnchangedFile(path, expectedHash, relativePath) {
|
|
65
|
+
const current = await regularFileContent(path);
|
|
66
|
+
if ((current === null ? null : digest(current)) !== expectedHash)
|
|
67
|
+
throw new Error(`environment_file_conflict:${relativePath}`);
|
|
68
|
+
}
|
|
69
|
+
const relativeFileValid = (path) => /^(?:(?:training|repos\/[A-Za-z0-9][A-Za-z0-9._-]{0,127})\/)?\.env(?:\.[a-z][a-z0-9_-]{0,47})?$/u.test(path);
|
|
70
|
+
export async function withEnvironmentFilesLock(agentRoot, operation) {
|
|
71
|
+
await mkdir(agentRoot, { recursive: true, mode: 0o700 });
|
|
72
|
+
const rootInfo = await lstat(agentRoot);
|
|
73
|
+
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
|
|
74
|
+
throw new Error("environment_path_symlink");
|
|
75
|
+
const stateRoot = await safeDirectory(await realpath(agentRoot), STATE_DIRECTORY);
|
|
76
|
+
const release = await acquireProfileSaveLock(stateRoot);
|
|
77
|
+
try {
|
|
78
|
+
return await operation();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
await release();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const TRAINING_FILE_MARKER = ".nowwork-environment-files.json";
|
|
85
|
+
const trainingFileName = (name) => /^\.env(?:\.[a-z][a-z0-9_-]{0,47})?$/u.test(name);
|
|
86
|
+
const TrainingFileOwnershipSchema = z.object({ files: FileHashes, pending: FileHashes.optional() }).strict();
|
|
87
|
+
const trainingFileHashes = (files) => Object.fromEntries(Object.entries(files).map(([name, content]) => [name, digest(content)]));
|
|
88
|
+
async function trainingFileOwnership(directory) {
|
|
89
|
+
const text = await regularFileContent(join(directory, TRAINING_FILE_MARKER));
|
|
90
|
+
if (text === null)
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
const ownership = TrainingFileOwnershipSchema.parse(JSON.parse(text));
|
|
94
|
+
if (Object.keys({ ...ownership.files, ...ownership.pending }).some(name => !trainingFileName(name)))
|
|
95
|
+
throw new Error();
|
|
96
|
+
return ownership;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error("environment_training_state_invalid");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** 训练包安装前,仅接受由环境管理器创建、没有其他用户文件的占位目录。 */
|
|
103
|
+
export async function isEnvironmentOnlyTrainingDirectory(directory) {
|
|
104
|
+
const ownership = await trainingFileOwnership(directory);
|
|
105
|
+
const stateText = ownership === null ? await regularFileContent(join(dirname(directory), STATE_DIRECTORY, "state.json")) : null;
|
|
106
|
+
const state = stateText === null ? null : StateSchema.parse(JSON.parse(stateText));
|
|
107
|
+
if (ownership === null && state === null)
|
|
108
|
+
return false;
|
|
109
|
+
const names = await readdir(directory);
|
|
110
|
+
for (const name of names) {
|
|
111
|
+
if (name === TRAINING_FILE_MARKER && ownership !== null)
|
|
112
|
+
continue;
|
|
113
|
+
if (!trainingFileName(name))
|
|
114
|
+
return false;
|
|
115
|
+
const content = await regularFileContent(join(directory, name));
|
|
116
|
+
const hash = content === null ? null : digest(content);
|
|
117
|
+
const owned = ownership === null
|
|
118
|
+
? hash !== null && (hash === state?.files[`training/${name}`] || hash === state?.pending?.files[`training/${name}`])
|
|
119
|
+
: hash !== null && (hash === ownership.files[name] || hash === ownership.pending?.[name]);
|
|
120
|
+
if (!owned)
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/** 保存当前全局状态已确认的文件,并给即将移入 rollback 的目录留下独立归属证明。 */
|
|
126
|
+
export async function captureTrainingEnvironmentFiles(agentRoot) {
|
|
127
|
+
const stateText = await regularFileContent(join(agentRoot, STATE_DIRECTORY, "state.json"));
|
|
128
|
+
if (!stateText)
|
|
129
|
+
return {};
|
|
130
|
+
const state = StateSchema.parse(JSON.parse(stateText));
|
|
131
|
+
const directory = join(agentRoot, "training");
|
|
132
|
+
const info = await lstat(directory).catch(error => { if (codeOf(error) === "ENOENT")
|
|
133
|
+
return null; throw error; });
|
|
134
|
+
if (info === null)
|
|
135
|
+
return {};
|
|
136
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
137
|
+
throw new Error("environment_path_symlink");
|
|
138
|
+
await trainingFileOwnership(directory); // 损坏的归属记录不能被静默覆盖。
|
|
139
|
+
const result = {};
|
|
140
|
+
for (const name of await readdir(directory)) {
|
|
141
|
+
if (!trainingFileName(name))
|
|
142
|
+
continue;
|
|
143
|
+
const path = `training/${name}`;
|
|
144
|
+
const content = await regularFileContent(join(directory, name));
|
|
145
|
+
if (content === null)
|
|
146
|
+
continue;
|
|
147
|
+
if (digest(content) !== state.files[path] && digest(content) !== state.pending?.files[path])
|
|
148
|
+
throw new Error(`environment_file_conflict:${path}`);
|
|
149
|
+
result[name] = content;
|
|
150
|
+
}
|
|
151
|
+
if ((info.mode & 0o200) === 0)
|
|
152
|
+
await chmod(directory, info.mode | 0o200);
|
|
153
|
+
await atomicWrite(join(directory, TRAINING_FILE_MARKER), JSON.stringify({ files: trainingFileHashes(result) }));
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
/** 发布前收敛完整文件集合;只移除目录自身归属记录确认的旧文件,支持中途失败后重试。 */
|
|
157
|
+
export async function restoreTrainingEnvironmentFiles(directory, files) {
|
|
158
|
+
const info = await lstat(directory);
|
|
159
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
160
|
+
throw new Error("environment_path_symlink");
|
|
161
|
+
const ownership = await trainingFileOwnership(directory);
|
|
162
|
+
const names = [...new Set([
|
|
163
|
+
...(await readdir(directory)).filter(trainingFileName),
|
|
164
|
+
...Object.keys(ownership?.files ?? {}), ...Object.keys(ownership?.pending ?? {}), ...Object.keys(files),
|
|
165
|
+
])].sort();
|
|
166
|
+
const assertOwned = async (name) => {
|
|
167
|
+
if (!trainingFileName(name))
|
|
168
|
+
throw new Error("environment_state_path_invalid");
|
|
169
|
+
const current = await regularFileContent(join(directory, name));
|
|
170
|
+
if (current !== null && digest(current) !== ownership?.files[name] && digest(current) !== ownership?.pending?.[name]) {
|
|
171
|
+
throw new Error(`environment_file_conflict:training/${name}`);
|
|
172
|
+
}
|
|
173
|
+
return current;
|
|
174
|
+
};
|
|
175
|
+
const currentHashes = {};
|
|
176
|
+
for (const name of names) {
|
|
177
|
+
const current = await assertOwned(name);
|
|
178
|
+
if (current !== null)
|
|
179
|
+
currentHashes[name] = digest(current);
|
|
180
|
+
}
|
|
181
|
+
const nextHashes = trainingFileHashes(files);
|
|
182
|
+
if ((info.mode & 0o200) === 0)
|
|
183
|
+
await chmod(directory, info.mode | 0o200);
|
|
184
|
+
await atomicWrite(join(directory, TRAINING_FILE_MARKER), JSON.stringify({ files: currentHashes, pending: nextHashes }));
|
|
185
|
+
for (const name of names) {
|
|
186
|
+
const target = join(directory, name);
|
|
187
|
+
const current = await regularFileContent(target);
|
|
188
|
+
if (current !== null && digest(current) !== currentHashes[name])
|
|
189
|
+
throw new Error(`environment_file_conflict:training/${name}`);
|
|
190
|
+
const content = files[name];
|
|
191
|
+
if (content === undefined)
|
|
192
|
+
await unlink(target).catch(error => { if (codeOf(error) !== "ENOENT")
|
|
193
|
+
throw error; });
|
|
194
|
+
else {
|
|
195
|
+
const expectedHash = current === null ? null : digest(current);
|
|
196
|
+
await atomicWrite(target, content, () => requireUnchangedFile(target, expectedHash, `training/${name}`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await atomicWrite(join(directory, TRAINING_FILE_MARKER), JSON.stringify({ files: nextHashes }));
|
|
200
|
+
}
|
|
201
|
+
export async function applyAgentEnvironmentPlan(agentRoot, input) {
|
|
202
|
+
const parsed = AgentEnvironmentPlanSchema.safeParse(input);
|
|
203
|
+
// 不让 Zod 的输入值或上游凭据出现在 RPC / console 错误中。
|
|
204
|
+
if (!parsed.success)
|
|
205
|
+
throw new Error("environment_plan_invalid");
|
|
206
|
+
const plan = parsed.data;
|
|
207
|
+
await mkdir(agentRoot, { recursive: true, mode: 0o700 });
|
|
208
|
+
const rootInfo = await lstat(agentRoot);
|
|
209
|
+
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink())
|
|
210
|
+
throw new Error("environment_path_symlink");
|
|
211
|
+
const root = await realpath(agentRoot);
|
|
212
|
+
const stateRoot = await safeDirectory(root, STATE_DIRECTORY);
|
|
213
|
+
const release = await acquireProfileSaveLock(stateRoot);
|
|
214
|
+
try {
|
|
215
|
+
const statePath = join(stateRoot, "state.json");
|
|
216
|
+
const stateText = await regularFileContent(statePath);
|
|
217
|
+
const previous = stateText === null ? null : StateSchema.parse(JSON.parse(stateText));
|
|
218
|
+
const hash = digest(JSON.stringify(plan));
|
|
219
|
+
const latest = previous?.pending ?? previous;
|
|
220
|
+
if (latest && plan.revision < latest.revision)
|
|
221
|
+
throw new Error("environment_revision_stale");
|
|
222
|
+
if (latest && plan.revision === latest.revision && hash !== latest.digest)
|
|
223
|
+
throw new Error("environment_revision_conflict");
|
|
224
|
+
const nextContents = Object.fromEntries(plan.scopes.flatMap(scope => scope.files.map(file => [
|
|
225
|
+
scope.path ? `${scope.path}/${file.name}` : file.name, file.content,
|
|
226
|
+
])));
|
|
227
|
+
const nextHashes = Object.fromEntries(Object.entries(nextContents).map(([path, text]) => [path, digest(text)]));
|
|
228
|
+
const paths = [...new Set([...Object.keys(previous?.files ?? {}), ...Object.keys(previous?.pending?.files ?? {}), ...Object.keys(nextContents)])].sort();
|
|
229
|
+
// 所有候选先预检,再开始写入,避免可预见的冲突造成半套配置。
|
|
230
|
+
const currentHashes = {};
|
|
231
|
+
for (const path of paths) {
|
|
232
|
+
if (!relativeFileValid(path))
|
|
233
|
+
throw new Error("environment_state_path_invalid");
|
|
234
|
+
await safeDirectory(root, path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "");
|
|
235
|
+
const current = await regularFileContent(join(root, path));
|
|
236
|
+
if (current !== null && digest(current) !== previous?.files[path] && digest(current) !== previous?.pending?.files[path]) {
|
|
237
|
+
throw new Error(`environment_file_conflict:${path}`);
|
|
238
|
+
}
|
|
239
|
+
if (current !== null)
|
|
240
|
+
currentHashes[path] = digest(current);
|
|
241
|
+
}
|
|
242
|
+
const pending = { revision: plan.revision, digest: hash, files: nextHashes };
|
|
243
|
+
await atomicWrite(statePath, JSON.stringify({ revision: previous?.revision ?? 0, digest: previous?.digest ?? "", files: currentHashes, pending }));
|
|
244
|
+
for (const path of paths) {
|
|
245
|
+
// 重检目录和文件,确保写入前仍处于托管边界。
|
|
246
|
+
await safeDirectory(root, path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "");
|
|
247
|
+
const target = join(root, path);
|
|
248
|
+
const current = await regularFileContent(target);
|
|
249
|
+
if (current !== null && digest(current) !== currentHashes[path]) {
|
|
250
|
+
throw new Error(`environment_file_conflict:${path}`);
|
|
251
|
+
}
|
|
252
|
+
const content = nextContents[path];
|
|
253
|
+
if (content === undefined)
|
|
254
|
+
await unlink(target).catch(error => { if (codeOf(error) !== "ENOENT")
|
|
255
|
+
throw error; });
|
|
256
|
+
else {
|
|
257
|
+
const expectedHash = current === null ? null : digest(current);
|
|
258
|
+
await atomicWrite(target, content, () => requireUnchangedFile(target, expectedHash, path));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
await atomicWrite(statePath, JSON.stringify(pending));
|
|
262
|
+
return { revision: plan.revision, files: Object.keys(nextContents).sort() };
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await release();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** 按工作目录/绑定项目选择唯一分支;绝不把多个兄弟 Repo 的变量混成进程全局值。 */
|
|
269
|
+
export function environmentForExecution(plan, agentRoot, cwd, projectId) {
|
|
270
|
+
const selected = projectId === undefined ? undefined : plan.scopes.find(scope => scope.id === `repo:${projectId}`);
|
|
271
|
+
const path = relative(resolve(agentRoot), resolve(cwd)).split(sep).join("/");
|
|
272
|
+
const nearest = selected ?? [...plan.scopes].sort((a, b) => b.path.length - a.path.length)
|
|
273
|
+
.find(scope => scope.path !== "" && (path === scope.path || path.startsWith(`${scope.path}/`)));
|
|
274
|
+
return { ...(nearest ?? plan.scopes.find(scope => scope.path === ""))?.effective };
|
|
275
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -131,6 +131,12 @@ function localMemoryV2Handles(env, serverUrl) {
|
|
|
131
131
|
return new Set(handles);
|
|
132
132
|
}
|
|
133
133
|
export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
|
|
134
|
+
function machineEnvironmentModeEnv(env) {
|
|
135
|
+
const value = env.NOWWORK_MACHINE_ENV_MODE ?? "auto";
|
|
136
|
+
if (value === "auto" || value === "zsh" || value === "bash" || value === "windows" || value === "off")
|
|
137
|
+
return value;
|
|
138
|
+
throw new ConfigError("NOWWORK_MACHINE_ENV_MODE must be auto, zsh, bash, windows, or off");
|
|
139
|
+
}
|
|
134
140
|
function codexCollaborationToolsEnv(env) {
|
|
135
141
|
const value = (env.CREW_CODEX_COLLABORATION_TOOLS ?? "").trim().toLowerCase();
|
|
136
142
|
if (value === "" || value === "0" || value === "false" || value === "off")
|
|
@@ -180,6 +186,7 @@ export function loadConfig(env = process.env) {
|
|
|
180
186
|
: defaultQaReportRoot,
|
|
181
187
|
cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
|
|
182
188
|
runtimeBin: env.CREW_RUNTIME ?? "claude",
|
|
189
|
+
machineEnvironmentMode: machineEnvironmentModeEnv(env),
|
|
183
190
|
codexCollaborationTools: codexCollaborationToolsEnv(env),
|
|
184
191
|
dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
|
|
185
192
|
resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
|
package/dist/execution-runner.js
CHANGED
|
@@ -210,6 +210,7 @@ function launchProviderConfig(config) {
|
|
|
210
210
|
? {}
|
|
211
211
|
: { providerSmallFastModel: config.providerSmallFastModel }),
|
|
212
212
|
...(config.envVars === undefined ? {} : { envVars: config.envVars }),
|
|
213
|
+
...(config.environmentPlan === undefined ? {} : { environmentPlan: config.environmentPlan }),
|
|
213
214
|
...(config.description === undefined ? {} : { description: config.description }),
|
|
214
215
|
};
|
|
215
216
|
}
|
|
@@ -746,6 +747,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
746
747
|
},
|
|
747
748
|
});
|
|
748
749
|
const localDependencies = {
|
|
750
|
+
...(dependencies.machineEnvironment === undefined ? {} : { machineEnvironment: dependencies.machineEnvironment }),
|
|
749
751
|
...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }),
|
|
750
752
|
...(dependencies.startupGate === undefined ? {} : { startupGate: dependencies.startupGate }),
|
|
751
753
|
...(dependencies.startupTimeoutMs === undefined
|
package/dist/list-models.js
CHANGED
|
@@ -8,8 +8,8 @@ const execFileRaw = promisify(execFile);
|
|
|
8
8
|
const MODEL_PROBE_TIMEOUT_MS = 8_000;
|
|
9
9
|
const MODEL_PROBE_MAX_BUFFER_BYTES = 1024 * 1024;
|
|
10
10
|
// win32 上 npm CLI 是 .cmd shim,execFile 需 shell 才能执行;参数全是固定字面量,无注入面。
|
|
11
|
-
const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
12
|
-
env
|
|
11
|
+
const execFileP = (bin, args, env = { ...process.env, PATH: augmentedPath() }) => execFileRaw(bin, args, {
|
|
12
|
+
env,
|
|
13
13
|
shell: isWin(),
|
|
14
14
|
timeout: MODEL_PROBE_TIMEOUT_MS,
|
|
15
15
|
killSignal: "SIGKILL",
|
|
@@ -18,17 +18,17 @@ const execFileP = (bin, args) => execFileRaw(bin, args, {
|
|
|
18
18
|
export async function listRuntimeModels(runtime, options = {}) {
|
|
19
19
|
switch (runtime) {
|
|
20
20
|
case "claude":
|
|
21
|
-
return listClaudeModels();
|
|
21
|
+
return listClaudeModels(undefined, options.env, options.environmentGeneration);
|
|
22
22
|
case "codex":
|
|
23
|
-
return parseCodexModels((await execFileP("codex", ["debug", "models"])).stdout);
|
|
23
|
+
return parseCodexModels((await execFileP("codex", ["debug", "models"], options.env)).stdout);
|
|
24
24
|
case "cursor":
|
|
25
|
-
return parseCursorModels((await execFileP("cursor-agent", ["--list-models"])).stdout);
|
|
25
|
+
return parseCursorModels((await execFileP("cursor-agent", ["--list-models"], options.env)).stdout);
|
|
26
26
|
case "hermes":
|
|
27
27
|
return listHermesModels(options);
|
|
28
28
|
case "opencode":
|
|
29
|
-
return listOpencodeModels();
|
|
29
|
+
return listOpencodeModels(options.env);
|
|
30
30
|
case "pi":
|
|
31
|
-
return parsePiModels((await execFileP("pi", ["--list-models"])).stdout);
|
|
31
|
+
return parsePiModels((await execFileP("pi", ["--list-models"], options.env)).stdout);
|
|
32
32
|
default:
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
@@ -84,9 +84,9 @@ function parseCursorModels(stdout) {
|
|
|
84
84
|
return { id: id.trim(), label: (label || id).trim(), ...(index === 0 ? { default: true } : {}) };
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
async function outputEvenOnFailure(bin, args) {
|
|
87
|
+
async function outputEvenOnFailure(bin, args, env) {
|
|
88
88
|
try {
|
|
89
|
-
return (await execFileP(bin, args)).stdout;
|
|
89
|
+
return (await execFileP(bin, args, env)).stdout;
|
|
90
90
|
}
|
|
91
91
|
catch (error) {
|
|
92
92
|
const stdout = error.stdout;
|
|
@@ -97,11 +97,11 @@ async function outputEvenOnFailure(bin, args) {
|
|
|
97
97
|
return "";
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
async function listOpencodeModels() {
|
|
101
|
-
const verbose = parseOpencodeModels(await outputEvenOnFailure("opencode", ["models", "--verbose"]));
|
|
100
|
+
async function listOpencodeModels(env) {
|
|
101
|
+
const verbose = parseOpencodeModels(await outputEvenOnFailure("opencode", ["models", "--verbose"], env));
|
|
102
102
|
if (verbose.length > 0)
|
|
103
103
|
return verbose;
|
|
104
|
-
return parseOpencodeModels(await outputEvenOnFailure("opencode", ["models"]));
|
|
104
|
+
return parseOpencodeModels(await outputEvenOnFailure("opencode", ["models"], env));
|
|
105
105
|
}
|
|
106
106
|
const VARIANT_ORDER = new Map([
|
|
107
107
|
["none", 0], ["minimal", 1], ["low", 2], ["medium", 3],
|