@nowcrew/daemon 0.6.98-beta.2 → 0.6.98-beta.4

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 CHANGED
@@ -40,7 +40,40 @@ 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
- ## Browser-Approved Computer Setup
43
+ ## Single-Command Computer Setup
44
+
45
+ After the deployment enables Token setup, an owner/admin opens **Add Computer**, copies the complete
46
+ command, and pastes it into the target computer's terminal. The same steps apply on macOS, Linux/SSH,
47
+ and Windows. Node.js 22+ and npm must already be installed. The command launches the installer with npx;
48
+ no separate global installation, browser confirmation, or verification code is needed during execution.
49
+
50
+ The generated command pins an exact published installer version and includes a `--setup-token`.
51
+ This is an already-approved one-time connection request, valid for ten minutes. The program exchanges
52
+ it for the machine credential only when executed, then reuses native service installation, new-hello
53
+ verification, credential finalization and rollback. Merely generating or copying a command does not
54
+ rotate the existing credential. An expired or consumed command must be regenerated in the Web UI.
55
+
56
+ For a machine credential you already hold, the same setup entry accepts `--api-key` or
57
+ `CREW_MACHINE_TOKEN` without browser pairing:
58
+
59
+ ```bash
60
+ nowwork-computer setup --server-url https://nowwork.nowcoder.com /nowcoder-app \
61
+ --machine <machine-uuid> --api-key <existing-machine-token>
62
+ ```
63
+
64
+ The machine Token must match the specified Workspace and Computer. This path never rotates the
65
+ credential or creates an approval request; it confirms a new control-plane connection with the expected
66
+ profile and paths before reporting success. Token sources cannot be combined. JSON output puts progress
67
+ on stderr and the final result on stdout. On Windows shells that block npm.ps1/npx.ps1, use npx.cmd as
68
+ the package entry; the setup arguments and workflow are unchanged.
69
+
70
+ Deployment requires `CREW_PUBLIC_URL` (or `CREW_SERVER_URL`) and
71
+ `CREW_COMPUTER_TOKEN_SETUP_VERSION` set to an exact, published version known to support the Token flags.
72
+ The server checks its published release catalog and channel before issuing a Token command. Leave the
73
+ version unset until that installer has been released; it returns `computer_token_setup_unavailable`
74
+ instead of generating a command for an unsupported package. Existing clients retain browser setup.
75
+
76
+ ## Browser-Approved Computer Setup (Compatibility)
44
77
 
45
78
  The Web UI generates a token-free command for a specific Workspace and Machine:
46
79
 
@@ -413,6 +446,13 @@ discovery is unchanged: Claude may still load `~/.claude/skills`, and Codex may
413
446
  and `~/.agents/skills`. The DeepSeek Codex variant keeps its Agent-private `.codex-deepseek` Home for
414
447
  provider credentials. Project bindings do not hide or rewrite any of those sources.
415
448
 
449
+ Both Codex launchers disable host automatic memories and inherit the execution's prepared environment,
450
+ overriding host `core`, default exclusions, `exclude` and `include_only` filters. These are process-local
451
+ settings: authentication, providers and Skills retain the configured Home, and no other environment
452
+ scope is merged. The policy version in Codex's session fingerprint cold-starts older native sessions
453
+ once, then subsequent runs can resume normally. Explicit `shell_environment_policy.set` entries, shell
454
+ startup scripts and Agent notes remain separate configuration; this change does not correct them.
455
+
416
456
  Binding changes reconcile immediately when the Computer is online. On reconnect the server sends the
417
457
  complete binding set for every Agent assigned to that Computer, including empty sets that clear stale
418
458
  links left by offline unbinds. Every non-empty execution snapshot is also reconciled immediately before
@@ -39,6 +39,19 @@ const SetupBootstrapSchema = z.object({
39
39
  expiresAt: z.string().datetime({ offset: true }).optional(),
40
40
  machineToken: MachineTokenSchema,
41
41
  }).strict();
42
+ const SetupTokenTargetSchema = SetupBootstrapSchema.omit({ setupId: true, machineToken: true }).extend({
43
+ workspaceSlug: z.string().min(1).max(80),
44
+ releaseTag: z.enum(["latest", "beta"]),
45
+ connection: z.object({
46
+ status: z.enum(["online", "offline"]),
47
+ epoch: z.number().int().nonnegative(),
48
+ profile: z.string().nullable(),
49
+ daemonHome: z.string().nullable(),
50
+ agentsRoot: z.string().nullable(),
51
+ storageId: z.string().optional(),
52
+ credentialMatchesConnection: z.boolean(),
53
+ }).strict(),
54
+ }).strict();
42
55
  const SetupPendingSchema = z.object({
43
56
  status: z.enum(["authorization_pending", "pending"]),
44
57
  intervalSeconds: z.number().int().positive().max(300).optional(),
@@ -237,8 +250,8 @@ export function createComputerSetupClient(options) {
237
250
  setTimeout(resolve, milliseconds);
238
251
  }));
239
252
  const now = options.now ?? Date.now;
240
- let activeMachineId;
241
253
  const request = async (method, path, body, bearer) => {
254
+ const secretInputs = [bearer ?? "", body?.deviceCode ?? ""];
242
255
  const controller = new AbortController();
243
256
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
244
257
  try {
@@ -253,7 +266,7 @@ export function createComputerSetupClient(options) {
253
266
  });
254
267
  const payload = await response.json();
255
268
  if (!response.ok || payload?.success === false) {
256
- const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`);
269
+ const message = redactSetupSecrets(payload?.error?.message ?? `computer setup request failed (HTTP ${response.status})`, secretInputs);
257
270
  throw new ComputerSetupHttpError(message, response.status, payload?.error?.code ?? `HTTP_${response.status}`);
258
271
  }
259
272
  return (payload?.success === true ? payload.data : payload);
@@ -263,15 +276,15 @@ export function createComputerSetupClient(options) {
263
276
  throw new Error("computer setup request timed out");
264
277
  if (error instanceof ComputerSetupHttpError)
265
278
  throw error;
266
- throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error)));
279
+ throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error), secretInputs));
267
280
  }
268
281
  finally {
269
282
  clearTimeout(timeoutId);
270
283
  }
271
284
  };
272
285
  return {
286
+ tokenTarget: async (machineToken) => SetupTokenTargetSchema.parse(await request("GET", "/daemon/computer-setup/target", undefined, machineToken)),
273
287
  start: async (input) => {
274
- activeMachineId = input.machineId;
275
288
  return parseStart(await request("POST", "/api/auth/computer-setup/start", {
276
289
  ...(input.workspace.kind === "id"
277
290
  ? { workspaceId: input.workspace.value }
@@ -282,8 +295,6 @@ export function createComputerSetupClient(options) {
282
295
  }));
283
296
  },
284
297
  poll: async (deviceCode) => {
285
- if (activeMachineId === undefined)
286
- throw new Error("computer setup polling has no active machine");
287
298
  return parsePoll(await request("POST", "/api/auth/computer-setup/poll", { deviceCode }));
288
299
  },
289
300
  waitForHello: async (input) => {
@@ -6,7 +6,7 @@ function safeError(error, machineToken) {
6
6
  const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken === undefined ? [] : [machineToken]);
7
7
  return new Error(message, error instanceof Error ? { cause: error } : undefined);
8
8
  }
9
- function assertBootstrapMatches(bootstrap, input) {
9
+ export function assertBootstrapMatches(bootstrap, input) {
10
10
  if (bootstrap.machineId !== input.args.machineId) {
11
11
  throw new Error("setup bootstrap machine does not match --machine");
12
12
  }
@@ -40,7 +40,7 @@ function secondsUntil(expiresAt, now) {
40
40
  const remaining = Date.parse(expiresAt) - now();
41
41
  return Math.max(0, Math.ceil(remaining / 1_000));
42
42
  }
43
- function localPathsMatch(left, right, platform) {
43
+ export function localPathsMatch(left, right, platform) {
44
44
  const paths = platform === "win32" ? win32 : posix;
45
45
  if (!paths.isAbsolute(left) || !paths.isAbsolute(right))
46
46
  return false;
@@ -51,6 +51,14 @@ function localPathsMatch(left, right, platform) {
51
51
  return normalize(left) === normalize(right);
52
52
  }
53
53
  async function obtainBootstrap(input, deps) {
54
+ if (input.args.setupToken !== undefined) {
55
+ deps.reportPairing("Checking computer connection information...");
56
+ const result = await deps.client.poll(input.args.setupToken);
57
+ if (result.status !== "authorized") {
58
+ throw new Error("Setup command expired or unavailable; regenerate it in NowWork and run the new command");
59
+ }
60
+ return result.bootstrap;
61
+ }
54
62
  const pairing = await deps.client.start({
55
63
  workspace: input.args.workspace,
56
64
  machineId: input.args.machineId,
@@ -116,6 +124,7 @@ export async function runComputerSetupOrchestration(input, deps) {
116
124
  bootstrap = authorized;
117
125
  machineToken = token;
118
126
  assertBootstrapMatches(authorized, input);
127
+ deps.reportPairing("Downloading, installing, and starting the Daemon service...");
119
128
  const installation = await deps.installAutomatically({
120
129
  serverUrl: authorized.serverUrl,
121
130
  machineToken: token,
@@ -128,6 +137,7 @@ export async function runComputerSetupOrchestration(input, deps) {
128
137
  ...(authorized.registryUrl === undefined ? {} : { requestedRegistryUrl: authorized.registryUrl }),
129
138
  beforeRollback: confirmRollback,
130
139
  verifyAfterStart: async (candidate) => {
140
+ deps.reportPairing("Confirming the computer connection...");
131
141
  const hello = await deps.client.waitForHello({
132
142
  machineToken: token,
133
143
  machineId: authorized.machineId,
@@ -174,6 +184,7 @@ export async function runComputerSetupOrchestration(input, deps) {
174
184
  });
175
185
  return {
176
186
  workspaceId: authorized.workspaceId,
187
+ ...(authorized.workspaceSlug === undefined ? {} : { workspaceSlug: authorized.workspaceSlug }),
177
188
  machineId: authorized.machineId,
178
189
  environment: authorized.environment,
179
190
  profile: installation.profile,
@@ -1,11 +1,13 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { z } from "zod";
3
3
  import { DEFAULT_SERVER_URL } from "./config.js";
4
+ import { MachineTokenSchema } from "./computer-profile.js";
4
5
  import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
5
6
  import { createComputerSetupClient, openSetupBrowser, redactSetupSecrets, } from "./computer-setup-client.js";
6
7
  import { runComputerSetupOrchestration, } from "./computer-setup-orchestration.js";
7
8
  import { detectDaemonLang, formatDaemonText } from "./i18n.js";
8
9
  import { systemCommandRunner } from "./computer-service.js";
10
+ import { runMachineTokenSetup } from "./computer-token-setup.js";
9
11
  const WorkspaceSlugSchema = z.string()
10
12
  .min(2)
11
13
  .max(40)
@@ -70,6 +72,8 @@ export function parseSetupArgs(argv) {
70
72
  assertRepeatedOptionValuesAgree(argv, "machine", (value) => MachineIdSchema.parse(value).toLowerCase());
71
73
  assertRepeatedOptionValuesAgree(argv, "machine-id", (value) => MachineIdSchema.parse(value).toLowerCase());
72
74
  assertRepeatedOptionValuesAgree(argv, "server-url", normalizedServerOrigin);
75
+ assertRepeatedOptionValuesAgree(argv, "setup-token", (value) => value);
76
+ assertRepeatedOptionValuesAgree(argv, "api-key", (value) => value);
73
77
  const parsed = parseArgs({
74
78
  args: [...argv],
75
79
  allowPositionals: true,
@@ -82,6 +86,8 @@ export function parseSetupArgs(argv) {
82
86
  "server-url": { type: "string" },
83
87
  json: { type: "boolean", default: false },
84
88
  "no-browser": { type: "boolean", default: false },
89
+ "setup-token": { type: "string" },
90
+ "api-key": { type: "string" },
85
91
  help: { type: "boolean", short: "h", default: false },
86
92
  },
87
93
  });
@@ -124,6 +130,14 @@ export function parseSetupArgs(argv) {
124
130
  }
125
131
  const machineId = parsedMachine;
126
132
  const serverUrl = stringValue(values, "server-url");
133
+ const setupToken = stringValue(values, "setup-token");
134
+ const machineToken = stringValue(values, "api-key");
135
+ if (setupToken !== undefined && machineToken !== undefined)
136
+ throw new Error("Choose one setup credential source");
137
+ if (setupToken !== undefined && !/^[A-Za-z0-9_-]{32,512}$/u.test(setupToken))
138
+ throw new Error("Invalid setup Token");
139
+ if (machineToken !== undefined)
140
+ MachineTokenSchema.parse(machineToken);
127
141
  return Object.freeze({
128
142
  command: "setup",
129
143
  workspace,
@@ -131,6 +145,8 @@ export function parseSetupArgs(argv) {
131
145
  ...(serverUrl === undefined ? {} : { serverUrl: ServerUrlSchema.parse(serverUrl) }),
132
146
  ...(booleanValue(values, "json") ? { json: true } : {}),
133
147
  ...(booleanValue(values, "no-browser") ? { noBrowser: true } : {}),
148
+ ...(setupToken === undefined ? {} : { setupToken }),
149
+ ...(machineToken === undefined ? {} : { machineToken }),
134
150
  });
135
151
  }
136
152
  function resolveSetupServerUrl(args, env) {
@@ -148,7 +164,8 @@ export function setupUsage(td = (message) => message) {
148
164
  " nowwork-computer setup --workspace /workspace --machine <uuid>",
149
165
  " nowwork-computer setup --workspace-id <uuid> --machine-id <uuid>",
150
166
  "",
151
- "Options: --server-url <url> --json --no-browser",
167
+ "Options: --server-url <url> --setup-token <token> --api-key <sk_machine_*> --json --no-browser",
168
+ td("Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported."),
152
169
  ].join("\n") + "\n";
153
170
  }
154
171
  function defaults() {
@@ -167,6 +184,7 @@ function defaults() {
167
184
  }
168
185
  function humanSummary(result, td) {
169
186
  return [
187
+ td("Connected to {{workspace}}.", { workspace: result.workspaceSlug ?? result.workspaceId }),
170
188
  td("Workspace: {{value}}", { value: result.workspaceId }),
171
189
  td("Machine: {{value}}", { value: result.machineId }),
172
190
  td("Environment: {{value}}", { value: result.environment }),
@@ -205,6 +223,7 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
205
223
  return null;
206
224
  const deps = { ...defaults(), ...overrides };
207
225
  const lang = detectDaemonLang(deps.env);
226
+ const secrets = [...optionOccurrences(argv, "setup-token"), ...optionOccurrences(argv, "api-key"), deps.env.CREW_MACHINE_TOKEN ?? ""];
208
227
  try {
209
228
  if (argv.includes("--help") || argv.includes("-h")) {
210
229
  deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
@@ -213,32 +232,43 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
213
232
  let args;
214
233
  try {
215
234
  args = parseSetupArgs(argv);
235
+ const envToken = deps.env.CREW_MACHINE_TOKEN?.trim();
236
+ if (envToken) {
237
+ if (args.setupToken !== undefined || (args.machineToken !== undefined && args.machineToken !== envToken)) {
238
+ throw new Error("Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token");
239
+ }
240
+ args = Object.freeze({ ...args, machineToken: MachineTokenSchema.parse(envToken) });
241
+ }
216
242
  }
217
243
  catch (error) {
218
- deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
244
+ deps.stderr(`nowwork-computer: ${formatDaemonText(lang, redactSetupSecrets(error.message, secrets))}\n`);
219
245
  return 2;
220
246
  }
221
- if (args.json === true) {
247
+ if (args.json === true && args.setupToken === undefined && args.machineToken === undefined) {
222
248
  deps.stderr(`${formatDaemonText(lang, "Starting computer setup pairing...")}\n`);
223
249
  }
224
250
  const serverUrl = resolveSetupServerUrl(args, deps.env);
225
251
  const createClient = overrides.createClient
226
252
  ?? ((value) => createComputerSetupClient({ serverUrl: value, fetch: deps.fetch }));
227
- const result = await runComputerSetupOrchestration({ args, serverUrl }, {
253
+ const orchestrationDeps = {
228
254
  client: createClient(serverUrl),
229
255
  installAutomatically: deps.installAutomatically,
230
256
  openBrowser: args.noBrowser === true ? async () => { } : deps.openBrowser,
231
257
  sleep: deps.sleep,
232
258
  reportPairing: (message) => deps[args.json === true ? "stderr" : "stdout"](`${localizedPairingMessage(lang, message)}\n`),
233
259
  now: deps.now,
234
- });
260
+ platform: deps.platform,
261
+ };
262
+ const result = args.machineToken === undefined
263
+ ? await runComputerSetupOrchestration({ args, serverUrl }, orchestrationDeps)
264
+ : await runMachineTokenSetup({ args, serverUrl }, orchestrationDeps, args.machineToken);
235
265
  deps.stdout(args.json === true
236
266
  ? `${JSON.stringify(result)}\n`
237
267
  : humanSummary(result, (message, values) => formatDaemonText(lang, message, values)));
238
268
  return 0;
239
269
  }
240
270
  catch (error) {
241
- deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
271
+ deps.stderr(`nowwork-computer: ${formatDaemonText(lang, redactSetupSecrets(error.message, secrets))}\n`);
242
272
  return 1;
243
273
  }
244
274
  }
@@ -0,0 +1,46 @@
1
+ import { assertBootstrapMatches, localPathsMatch } from "./computer-setup-orchestration.js";
2
+ import { redactSetupSecrets } from "./computer-setup-client.js";
3
+ export async function runMachineTokenSetup(input, deps, machineToken) {
4
+ try {
5
+ deps.reportPairing("Checking computer connection information...");
6
+ const target = await deps.client.tokenTarget(machineToken);
7
+ assertBootstrapMatches(target, input);
8
+ deps.reportPairing("Downloading, installing, and starting the Daemon service...");
9
+ const installation = await deps.installAutomatically({
10
+ serverUrl: target.serverUrl, machineToken, workspaceId: target.workspaceId, machineId: target.machineId,
11
+ requestedProfile: target.profile,
12
+ ...(target.releaseTag === undefined ? {} : { requestedReleaseTag: target.releaseTag }),
13
+ ...(target.registryUrl === undefined ? {} : { requestedRegistryUrl: target.registryUrl }),
14
+ verifyAfterStart: async (candidate) => {
15
+ deps.reportPairing("Confirming the computer connection...");
16
+ const deadline = deps.now() + 120_000;
17
+ for (;;) {
18
+ const current = await deps.client.tokenTarget(machineToken);
19
+ assertBootstrapMatches(current, input);
20
+ const connection = current.connection;
21
+ const platform = deps.platform ?? process.platform;
22
+ if (connection.status === "online" && connection.epoch > target.minimumConnectionEpoch
23
+ && connection.credentialMatchesConnection && current.environment === target.environment
24
+ && connection.profile === candidate.profile
25
+ && connection.daemonHome !== null && localPathsMatch(connection.daemonHome, candidate.daemonHome, platform)
26
+ && connection.agentsRoot !== null && localPathsMatch(connection.agentsRoot, candidate.agentsRoot, platform)
27
+ && (target.storageId === undefined || connection.storageId === target.storageId))
28
+ return;
29
+ if (deps.now() >= deadline)
30
+ throw new Error("Timed out confirming the new computer connection");
31
+ await deps.sleep(Math.min(2_000, Math.max(0, deadline - deps.now())));
32
+ }
33
+ },
34
+ });
35
+ return {
36
+ workspaceId: target.workspaceId, workspaceSlug: target.workspaceSlug, machineId: target.machineId,
37
+ environment: target.environment, profile: installation.profile, daemonVersion: installation.version,
38
+ serviceId: installation.serviceId, serviceStatus: "running", helloConfirmed: true,
39
+ localNowworkHome: installation.localHome, daemonHome: installation.daemonHome, agentsRoot: installation.agentsRoot,
40
+ ...(target.storageId === undefined ? {} : { storageId: target.storageId }),
41
+ };
42
+ }
43
+ catch (error) {
44
+ throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error), [machineToken]));
45
+ }
46
+ }
package/dist/i18n.js CHANGED
@@ -36,6 +36,16 @@ const zh = {
36
36
  "Code {{code}}": "验证码 {{code}}",
37
37
  "Browser open skipped: {{reason}}": "未能自动打开浏览器:{{reason}}",
38
38
  "Starting computer setup pairing...": "正在发起电脑接入授权…",
39
+ "Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported.": "Token 接入无需浏览器授权,也支持 CREW_MACHINE_TOKEN。",
40
+ "Checking computer connection information...": "正在检查接入信息…",
41
+ "Downloading, installing, and starting the Daemon service...": "正在下载、安装并启动 Daemon 服务…",
42
+ "Confirming the computer connection...": "正在确认电脑连接…",
43
+ "Connected to {{workspace}}.": "已接入 {{workspace}}。",
44
+ "Setup command expired or unavailable; regenerate it in NowWork and run the new command": "接入命令已过期或不可用,请在 NowWork 重新生成并执行新命令。",
45
+ "Timed out confirming the new computer connection": "确认新连接超时,请检查网络和服务状态。",
46
+ "Invalid setup Token": "接入 Token 无效。",
47
+ "Choose one setup credential source": "请只使用一种接入凭据。",
48
+ "Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token": "请只使用一种接入凭据;使用其他 Token 时请清除 CREW_MACHINE_TOKEN。",
39
49
  "Workspace: {{value}}": "工作区:{{value}}",
40
50
  "Machine: {{value}}": "机器: {{value}}",
41
51
  "Environment: {{value}}": "环境:{{value}}",
@@ -11,6 +11,7 @@
11
11
  import { join } from "node:path";
12
12
  import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
13
13
  import { mergeRuntimeEnvironments } from "./runtime-environment-merge.js";
14
+ import { CODEX_EXECUTION_POLICY_VERSION } from "./runtimes/codex-execution-policy.js";
14
15
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
16
  const RESERVED_ENV = new Set([
16
17
  "PATH",
@@ -125,11 +126,15 @@ export function applyDeepSeekHarnessMachineEnv(base, machineEnv, platform = proc
125
126
  }
126
127
  /**
127
128
  * provider 关键配置指纹:Claude Custom 路由和 Codex DeepSeek key 有无都会改变会话边界,
128
- * 供 pickResumeId 判定冷启动。default 恒为 null;不含 key 明文(同 Provider 换 key 可续)。
129
+ * 供 pickResumeId 判定冷启动。Codex 还包含执行策略版本,避免续用带宿主记忆的旧会话。
130
+ * 不含 key 明文(同 Provider 换 key 可续);其他 runtime 的 default 仍为 null。
129
131
  */
130
132
  export function providerFingerprint(runtime, cfg) {
131
- if (runtime === "codex" && cfg.provider === "deepseek") {
132
- return `deepseek|${cfg.providerBaseUrl ?? "default"}|${cfg.providerApiKey ? "key" : "nokey"}`;
133
+ if (runtime === "codex") {
134
+ const provider = cfg.provider === "deepseek"
135
+ ? `deepseek|${cfg.providerBaseUrl ?? "default"}|${cfg.providerApiKey ? "key" : "nokey"}`
136
+ : "default";
137
+ return `${CODEX_EXECUTION_POLICY_VERSION}|${provider}`;
133
138
  }
134
139
  if (runtime !== "claude" || cfg.provider !== "custom")
135
140
  return null;
@@ -10,6 +10,7 @@ import { assertCodexCollaborationVersion, assertCodexNativeWorkspaceVersion, ass
10
10
  import { decideTurnRetry, itemMakesTurnUnsafe } from "./codex-turn-retry.js";
11
11
  import { startFirstProgressWatchdog } from "./progress-watchdog.js";
12
12
  import { prepareThreadDelivery, ThreadDeliveryColdStartRequired } from "../thread-input-delivery.js";
13
+ import { codexExecutionPolicyArgs } from "./codex-execution-policy.js";
13
14
  const RunnerInputSchema = z.object({
14
15
  systemPrompt: z.string().min(1),
15
16
  wakePrompt: z.string().min(1),
@@ -57,6 +58,7 @@ export function codexAppServerArgs(input) {
57
58
  // multi_agent 按开关;multi_agent_v2 无条件关,钉死已验证的 v1 契约(spec 2b)。
58
59
  "-c", `features.multi_agent=${input.collaborationTools === true}`,
59
60
  "-c", "features.multi_agent_v2=false",
61
+ ...codexExecutionPolicyArgs(),
60
62
  "app-server", "--listen", "stdio://",
61
63
  ];
62
64
  }
@@ -0,0 +1,12 @@
1
+ /** Bump when old native transcripts must not cross a changed execution boundary. */
2
+ export const CODEX_EXECUTION_POLICY_VERSION = "nowwork-codex-context-v1";
3
+ /** Process-local overrides: keep host auth/Skills without importing host automatic memory. */
4
+ export function codexExecutionPolicyArgs() {
5
+ return [
6
+ "features.memories=false",
7
+ 'shell_environment_policy.inherit="all"',
8
+ "shell_environment_policy.ignore_default_excludes=true",
9
+ "shell_environment_policy.exclude=[]",
10
+ "shell_environment_policy.include_only=[]",
11
+ ].flatMap(value => ["-c", value]);
12
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  // cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
5
5
  import spawn from "cross-spawn";
6
+ import { codexExecutionPolicyArgs } from "./codex-execution-policy.js";
6
7
  /** Minimum CLI whose real app-server contract supports NowCrew native project workspaces. */
7
8
  export const CODEX_NATIVE_WORKSPACE_MIN_VERSION = "0.148.0";
8
9
  /** Minimum CLI whose `multi_agent` collaboration tool contract was empirically verified (2026-08-28). */
@@ -26,6 +27,7 @@ export function buildCodexArgs(input) {
26
27
  args.push("-c", `model_reasoning_effort=${effort}`);
27
28
  // 纵深防御:exec 启动器没有 initialize 握手、无法断言版本,协作工具恒关(spec 2b)。
28
29
  args.push("-c", "features.multi_agent=false", "-c", "features.multi_agent_v2=false");
30
+ args.push(...codexExecutionPolicyArgs());
29
31
  if (input.projectRootMarkers) {
30
32
  args.push("-c", `project_root_markers=${JSON.stringify(input.projectRootMarkers)}`);
31
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.98-beta.2",
3
+ "version": "0.6.98-beta.4",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",