@nowcrew/daemon 0.6.88-beta.3 → 0.6.88-beta.31
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 +89 -14
- package/dist/agent-ability/controller.js +11 -1
- package/dist/agent-ability/git-errors.js +22 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-ability/resolver.js +12 -4
- 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/agent-memory/policy.js +3 -1
- package/dist/automatic-daemon-installer.js +51 -2
- package/dist/automatic-install-target.js +8 -0
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +2 -1
- package/dist/execution-runner.js +16 -10
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +13 -11
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +88 -92
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +20 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +24 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- 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 +70 -25
- 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/main.js +4 -69
- 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 +246 -0
- package/dist/native-thread-context.js +59 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/prompt.js +15 -15
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +31 -19
- 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 +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/claude-session-start-hook.js +24 -0
- package/dist/runtimes/claude-thread-runner.js +266 -0
- package/dist/runtimes/claude.js +23 -4
- package/dist/runtimes/codex-app-server-runner.js +43 -4
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +84 -15
- package/dist/runtimes/opencode-runner.js +7 -1
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +95 -184
- package/dist/server-url.js +25 -0
- package/dist/session.js +4 -0
- package/dist/slog.js +2 -1
- package/dist/storage-identity.js +0 -3
- package/dist/storage-runtime.js +2 -2
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +28 -18
- package/dist/thread-input-delivery.js +217 -0
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +5 -4
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
package/README.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
The daemon is NowWork's local execution adapter. It holds a machine credential, keeps one control-plane
|
|
4
4
|
WebSocket open, and launches coding runtimes on that machine. Product policy remains on the server.
|
|
5
5
|
|
|
6
|
+
## 已完成搬迁后的存储运行
|
|
7
|
+
|
|
8
|
+
NAS / `NOWWORK_HOME` 搬迁和旧 daemon 布局标准化入口已退役。daemon 不再提供内部
|
|
9
|
+
`storage-migration-worker` / `storage-migration-recover` 命令,不处理 `daemon:migrate`、
|
|
10
|
+
`daemon:storage-doctor`、`daemon:storage-migrate` 或迁移确认消息,也不再声明
|
|
11
|
+
`storage_migration_v1` / `storage_migration_status_v1` / `daemon_layout_migration_v1` 能力。
|
|
12
|
+
|
|
13
|
+
现有 `installations.json` 即使仍有历史 `storageMigrationId`,也按当前 installation 解析
|
|
14
|
+
Agent、仓库和报告目录;启动、hello 和身份绑定不再读取迁移 receipt、outbox 或备份。
|
|
15
|
+
hello 继续报告布局、环境、Workspace、路径指纹与 ownership,但不再发送迁移 ID。
|
|
16
|
+
每次连接仍须完成 Server 身份绑定后才能执行任务;Workspace 或 Machine 身份不匹配时拒绝执行。
|
|
17
|
+
|
|
18
|
+
旧布局读取兼容、日常安装/升级、NAS 所有权校验、执行 journal 与 ACK/replay 继续保留。
|
|
19
|
+
退役代码不会删除历史安装字段、迁移记录、receipt、备份或旧数据目录;这些数据的保留与清理另行处理。
|
|
20
|
+
|
|
6
21
|
## Run
|
|
7
22
|
|
|
8
23
|
For a temporary foreground connection, run:
|
|
@@ -25,6 +40,50 @@ This compatibility form stays resident in the current terminal and reconnects wi
|
|
|
25
40
|
`pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
|
|
26
41
|
manual one-shot entry.
|
|
27
42
|
|
|
43
|
+
## Browser-Approved Computer Setup
|
|
44
|
+
|
|
45
|
+
The Web UI generates a token-free command for a specific Workspace and Machine:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx -y --registry https://registry.npmjs.org --package @nowcrew/daemon@latest nowwork-computer setup /nowcoder-app --machine <machine-uuid> --server-url https://nowwork.nowcoder.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`nowwork-computer setup` opens a one-time authorization page, prints the user code, polls until an owner or
|
|
52
|
+
admin approves it, then installs the selected daemon through the existing isolated profile and native service
|
|
53
|
+
installer. The command contains no long-lived `sk_machine_*` credential. The issued credential is held
|
|
54
|
+
only in memory until the private profile is written, and becomes permanent after successful finalization.
|
|
55
|
+
Each command run needs a new approval. Browser approval expires after ten minutes; credential exchange starts
|
|
56
|
+
a separate thirty-minute installation window. Failed installations restore the prior credential when still owned
|
|
57
|
+
by that setup. Interrupted setups are reconciled at server startup and every minute.
|
|
58
|
+
Only one unfinished credential exchange can own a Machine at a time; a concurrent setup receives a conflict.
|
|
59
|
+
Use `--no-browser` on hosts without a desktop browser and
|
|
60
|
+
`--json` for machine-readable status output. `--server-url` must be explicit for remote servers; it may also
|
|
61
|
+
come from `CREW_SERVER_URL`.
|
|
62
|
+
|
|
63
|
+
Alternatively, install both command entries first, then run setup:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install --global --registry https://registry.npmjs.org @nowcrew/daemon@latest
|
|
67
|
+
nowwork-computer setup --server-url https://nowwork.nowcoder.com /nowcoder-app --machine <machine-uuid>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
With `CREW_SERVER_URL=https://nowwork.nowcoder.com` already set, the second command can be shortened to
|
|
71
|
+
`nowwork-computer setup /nowcoder-app --machine <machine-uuid>`. Replace the placeholder with the
|
|
72
|
+
Machine UUID from the Web command. New production setup links use `nowwork.nowcoder.com`;
|
|
73
|
+
the old `nowwork-in.nowcoder.com` hostname remains a production alias for existing profiles and pairing.
|
|
74
|
+
|
|
75
|
+
The setup flow validates the exact Workspace and Machine UUID and waits for the machine control-plane hello
|
|
76
|
+
to confirm Workspace, Machine, environment, profile, and storage identity. It does not migrate NAS or copy
|
|
77
|
+
existing `.crew` data. Existing `crew-daemon install --server-url ... --api-key ...` and profile lifecycle
|
|
78
|
+
commands remain available as compatibility and recovery entry points.
|
|
79
|
+
|
|
80
|
+
## Machine Environment
|
|
81
|
+
|
|
82
|
+
The daemon loads exported variables from its OS account’s bash/zsh configuration on macOS/Linux,
|
|
83
|
+
and native system/user variables on Windows. Agent directory layers override ordinary machine values;
|
|
84
|
+
running tasks keep immutable snapshots. See [machine environment](../docs/machine-environment.md) for
|
|
85
|
+
source rules, refresh behavior, local controls and the Windows native acceptance status.
|
|
86
|
+
|
|
28
87
|
## Computer Profiles And Service Lifecycle
|
|
29
88
|
|
|
30
89
|
Persist a named connection without putting its machine token in shell history or service arguments:
|
|
@@ -50,7 +109,8 @@ npx -y --registry https://registry.npmjs.org @nowcrew/daemon@beta install \
|
|
|
50
109
|
--release-tag beta --registry https://registry.npmjs.org
|
|
51
110
|
```
|
|
52
111
|
|
|
53
|
-
The installer selects the fixed
|
|
112
|
+
The installer selects the fixed production layout for both `nowwork.nowcoder.com` (canonical) and
|
|
113
|
+
`nowwork-in.nowcoder.com` (legacy compatibility), and the fixed `dev` layout for
|
|
54
114
|
`nowwork-dev.nowcoder.com`. It compares the parsed hostname exactly; lookalike suffixes are not accepted.
|
|
55
115
|
An unknown hostname is rejected before any write and reports how to retry with an explicit custom profile:
|
|
56
116
|
|
|
@@ -242,26 +302,26 @@ The stable adapters keep prompts out of provider argv wherever the provider prot
|
|
|
242
302
|
|
|
243
303
|
| Runtime | Transport | System/wake input | Native resume |
|
|
244
304
|
| --- | --- | --- | --- |
|
|
245
|
-
| Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
|
|
246
|
-
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume
|
|
247
|
-
| Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load`
|
|
248
|
-
| Hermes | ACP over stdio | `session/prompt` | `session/resume` (`session/load`
|
|
249
|
-
| OpenCode | `run --format json` | stdin | `--session
|
|
305
|
+
| Claude | stream-json CLI | daemon-owned system prompt file + user input | CLI session id; thread-bound: matching native SessionStart proof + SDK initialization |
|
|
306
|
+
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume`; thread-bound: returned id must match |
|
|
307
|
+
| Kimi | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
308
|
+
| Hermes | ACP over stdio | `session/prompt` | unbound: `session/resume` (`session/load` fallback); thread-bound: cold |
|
|
309
|
+
| OpenCode | `run --format json` | stdin | unbound: `--session`; thread-bound: cold |
|
|
250
310
|
| DeepSeek Harness | ACP over stdio | `session/prompt` | none (fresh session per execution) |
|
|
251
311
|
|
|
252
312
|
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
253
313
|
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
254
|
-
CLI
|
|
314
|
+
CLI; only unbound executions can carry a saved native session id. Unrelated ACP failures stay failures. The CLI fallback is
|
|
255
315
|
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
256
316
|
durable Job Object ownership is available.
|
|
257
317
|
|
|
258
318
|
Hermes uses only ACP capabilities advertised by the installed CLI. Model and effort choices come from
|
|
259
319
|
a short-lived discovery session; credentials, provider profiles, plugins, and `HERMES_HOME` remain
|
|
260
|
-
machine-local.
|
|
320
|
+
machine-local. For unbound executions, a missing resumed ACP session is retried once as a fresh session.
|
|
261
321
|
|
|
262
322
|
OpenCode receives the prompt through stdin and runs with both cwd and `PWD` anchored to the Agent
|
|
263
323
|
workspace. Models and variants come from `opencode models --verbose` (with the plain catalog as a
|
|
264
|
-
compatibility fallback).
|
|
324
|
+
compatibility fallback). For unbound executions, a missing resumed session is retried once without `--session`; malformed,
|
|
265
325
|
empty, incomplete, or explicitly failed JSON streams remain failures. NowWork never writes
|
|
266
326
|
`opencode.json` or replaces `OPENCODE_CONFIG_CONTENT`. The daemon advertises OpenCode as executable
|
|
267
327
|
only when `opencode run --help` exposes `--format`, `--dangerously-skip-permissions`, `--dir`,
|
|
@@ -291,8 +351,12 @@ uses fresh sessions and committed text; it does not claim resume, images, MCP, r
|
|
|
291
351
|
or transcript support. Agent environment variables cannot override `DEEPSEEK_*`, `DSH_*`, or
|
|
292
352
|
`NOWCREW_DEEPSEEK_HARNESS_CONFIG`; those values always come from the daemon service environment.
|
|
293
353
|
|
|
294
|
-
Codex, Kimi, Hermes, and OpenCode sessions
|
|
295
|
-
|
|
354
|
+
Unbound Codex, Kimi, Hermes, and OpenCode sessions retain the same configured warm/budget policy and keyed
|
|
355
|
+
lease as Claude through protocol v1. Thread-bound Claude/Codex sessions keep their native session across
|
|
356
|
+
warm-cache expiry, token estimates, and turn counts; verified restoration receives only accepted-cursor
|
|
357
|
+
deltas. New sessions and runtimes without verified restoration receive complete current-thread history
|
|
358
|
+
before the business turn. See the [thread context contract](../docs/thread-scoped-native-context.md).
|
|
359
|
+
A first-progress watchdog covers a provider that accepts a turn but remains
|
|
296
360
|
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
297
361
|
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
298
362
|
DeepSeek Harness is execution-v1-only and intentionally skips native session persistence.
|
|
@@ -343,9 +407,11 @@ changing the task cwd. Claude receives
|
|
|
343
407
|
so a machine policy that disables project settings also disables this source even when the projection is
|
|
344
408
|
healthy.
|
|
345
409
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
410
|
+
Ordinary Codex always uses the shared user Home: the inherited `CODEX_HOME`, or `~/.codex` when
|
|
411
|
+
unset. There is no Computer-level Home mode or private-Home migration command. Existing user-level
|
|
412
|
+
discovery is unchanged: Claude may still load `~/.claude/skills`, and Codex may load `$CODEX_HOME/skills`
|
|
413
|
+
and `~/.agents/skills`. The DeepSeek Codex variant keeps its Agent-private `.codex-deepseek` Home for
|
|
414
|
+
provider credentials. Project bindings do not hide or rewrite any of those sources.
|
|
349
415
|
|
|
350
416
|
Binding changes reconcile immediately when the Computer is online. On reconnect the server sends the
|
|
351
417
|
complete binding set for every Agent assigned to that Computer, including empty sets that clear stale
|
|
@@ -354,6 +420,15 @@ Runtime launch, which recovers from a missed notification or daemon restart. Ski
|
|
|
354
420
|
are visible through the symlink immediately; adding, deleting, renaming, or changing frontmatter requires
|
|
355
421
|
a rescan. Web descriptions remain at the last inventory value until that rescan.
|
|
356
422
|
|
|
423
|
+
## 目录投影测试的平台范围
|
|
424
|
+
|
|
425
|
+
`test/directory-projection.test.ts` 在所有平台运行,覆盖路径检查、复制发布、只读权限请求及失败恢复。
|
|
426
|
+
其受控文件系统夹具记录目录 `chmod` 请求,但保留宿主目录的写权限,避免把 macOS 的目录重命名规则当作 Windows 行为。
|
|
427
|
+
生产代码的只读保护不受此夹具影响。
|
|
428
|
+
|
|
429
|
+
`test/directory-projection.windows.test.ts` 仅在 Windows 上运行,使用真实文件系统验证只读目录发布与文件保护。
|
|
430
|
+
非 Windows 的全量测试会明确跳过该文件,不能据此宣称 Windows 原生行为已验证。
|
|
431
|
+
|
|
357
432
|
## Reliability
|
|
358
433
|
|
|
359
434
|
Protocol-v1 lifecycle is `accepted → started → completed`, followed by a server
|
|
@@ -3,7 +3,17 @@ import { resolveAbilityRelease } from "./resolver.js";
|
|
|
3
3
|
export const normalizeAbilityCommandError = (error, fallback) => {
|
|
4
4
|
const message = error instanceof Error ? error.message : fallback;
|
|
5
5
|
const structured = message.match(/^(ability_[a-z0-9_]+)(?::|$)/u)?.[1];
|
|
6
|
-
|
|
6
|
+
if (structured)
|
|
7
|
+
return structured;
|
|
8
|
+
if (error instanceof Error && "code" in error) {
|
|
9
|
+
if (error.code === "ENOSPC")
|
|
10
|
+
return "ability_disk_full";
|
|
11
|
+
if (error.code === "EACCES" || error.code === "EPERM")
|
|
12
|
+
return "ability_filesystem_permission_denied";
|
|
13
|
+
if (error.code === "ENOENT")
|
|
14
|
+
return "ability_local_file_missing";
|
|
15
|
+
}
|
|
16
|
+
return fallback;
|
|
7
17
|
};
|
|
8
18
|
export function createAgentAbilityController(deps) {
|
|
9
19
|
const resolver = deps.resolve ?? resolveAbilityRelease;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Classify Git failures without returning command arguments or raw stderr. */
|
|
2
|
+
export function abilityGitErrorCode(error) {
|
|
3
|
+
if (!(error instanceof Error))
|
|
4
|
+
return "ability_git_failed";
|
|
5
|
+
if ("code" in error && error.code === "ENOENT")
|
|
6
|
+
return "ability_git_unavailable";
|
|
7
|
+
if ("killed" in error && error.killed === true)
|
|
8
|
+
return "ability_git_timeout";
|
|
9
|
+
const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
|
|
10
|
+
if (/no space left on device/i.test(stderr))
|
|
11
|
+
return "ability_disk_full";
|
|
12
|
+
if (/host key verification failed|remote host identification has changed/i.test(stderr)) {
|
|
13
|
+
return "ability_git_host_verification_failed";
|
|
14
|
+
}
|
|
15
|
+
if (/permission denied \(publickey|authentication failed|could not read username|repository not found|access denied/i.test(stderr)) {
|
|
16
|
+
return "ability_git_access_denied";
|
|
17
|
+
}
|
|
18
|
+
if (/could not resolve host|could not resolve hostname|connection timed out|connection refused|network is unreachable|could not connect to server/i.test(stderr)) {
|
|
19
|
+
return "ability_git_network_failed";
|
|
20
|
+
}
|
|
21
|
+
return "ability_git_failed";
|
|
22
|
+
}
|
|
@@ -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);
|
|
@@ -7,6 +7,7 @@ import { parse } from "yaml";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { abilityWorkspaceProjectionPath, DaemonAbilityLockSchema, DaemonAbilityManifestSchema, matchesAbilityBranch, } from "./types.js";
|
|
9
9
|
import { PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "../project-dependency-manifest.js";
|
|
10
|
+
import { abilityGitErrorCode } from "./git-errors.js";
|
|
10
11
|
import { normalizeRepositoryIdentity } from "../repository-identity.js";
|
|
11
12
|
const runFile = promisify(execFile);
|
|
12
13
|
const MAX_FILES = 5_000;
|
|
@@ -42,6 +43,7 @@ const gitEnv = () => ({
|
|
|
42
43
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
43
44
|
GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
|
|
44
45
|
GIT_TERMINAL_PROMPT: "0",
|
|
46
|
+
LC_ALL: "C",
|
|
45
47
|
});
|
|
46
48
|
export const abilitySha256 = (content) => `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
47
49
|
export const safeAbilityName = (value) => value.replace(/[^A-Za-z0-9._-]/gu, "_").slice(0, 128);
|
|
@@ -73,10 +75,16 @@ const comparePosixPaths = (left, right) => {
|
|
|
73
75
|
return leftParts.length - rightParts.length;
|
|
74
76
|
};
|
|
75
77
|
const git = async (args, cwd) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
try {
|
|
79
|
+
const result = await runFile("git", [...GIT_ARGS, ...args], {
|
|
80
|
+
...(cwd ? { cwd } : {}), env: gitEnv(), timeout: 60_000, maxBuffer: 16 * 1024 * 1024,
|
|
81
|
+
});
|
|
82
|
+
return result.stdout.trim();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
// 仅回传固定错误分类,不把 Git stderr 中的凭据、仓库地址或本地路径发到界面。
|
|
86
|
+
throw new Error(abilityGitErrorCode(error));
|
|
87
|
+
}
|
|
80
88
|
};
|
|
81
89
|
export const parseAbilityRefLine = (line) => {
|
|
82
90
|
const [branch, commit, rawTime, ...extra] = line.split("\t");
|
|
@@ -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
|
+
}
|