@nowcrew/daemon 0.6.79-beta.2 → 0.6.79

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.
@@ -10,8 +10,8 @@ import { PROJECT_MANIFEST_SOURCE_PATH, ProjectDependencyManifestSchema, } from "
10
10
  import { normalizeRepositoryIdentity } from "../repository-identity.js";
11
11
  const runFile = promisify(execFile);
12
12
  const MAX_FILES = 5_000;
13
- const MAX_TOTAL_BYTES = 25 * 1024 * 1024;
14
- const MAX_FILE_BYTES = 2 * 1024 * 1024;
13
+ export const AGENT_ABILITY_MAX_TOTAL_BYTES = 200 * 1024 * 1024;
14
+ export const AGENT_ABILITY_MAX_FILE_BYTES = 100 * 1024 * 1024;
15
15
  const MAX_INSTRUCTIONS_BYTES = 96 * 1024;
16
16
  const MAX_MEMORY_BYTES = 64 * 1024;
17
17
  const AUTHORING_ONLY_ROOTS = new Set([".claude"]);
@@ -125,10 +125,11 @@ export const collectAbilityFiles = async (root) => {
125
125
  if (info.isDirectory())
126
126
  await visit(absolute);
127
127
  else if (info.isFile()) {
128
- if (info.size > MAX_FILE_BYTES)
128
+ if (info.size > AGENT_ABILITY_MAX_FILE_BYTES) {
129
129
  throw new Error(`ability_file_size_exceeded:${relative(root, absolute)}`);
130
+ }
130
131
  totalBytes += info.size;
131
- if (totalBytes > MAX_TOTAL_BYTES)
132
+ if (totalBytes > AGENT_ABILITY_MAX_TOTAL_BYTES)
132
133
  throw new Error("ability_total_size_exceeded");
133
134
  if (files.length >= MAX_FILES)
134
135
  throw new Error("ability_file_count_exceeded");
@@ -262,7 +263,7 @@ export const validateAbilityDependencyDeclarations = (manifest, lock) => {
262
263
  throw new Error("ability_lock_has_undeclared_dependency");
263
264
  }
264
265
  };
265
- const scan = (files, manifest) => {
266
+ export const scanAbilityFiles = (files, manifest) => {
266
267
  const findings = [];
267
268
  const secrets = [
268
269
  /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/u,
@@ -270,7 +271,9 @@ const scan = (files, manifest) => {
270
271
  /\bAK(?:ID|IA)[A-Z0-9]{12,}\b/u,
271
272
  ];
272
273
  const executable = new Set([".exe", ".dll", ".dylib", ".so", ".bin", ".class", ".jar"]);
273
- const allowedBinaryDocuments = new Set([".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".xlsx", ".xls", ".docx", ".pptx"]);
274
+ const allowedBinaryDocuments = new Set([
275
+ ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".xlsx", ".xls", ".docx", ".pptx", ".ttf", ".otf",
276
+ ]);
274
277
  for (const file of files) {
275
278
  let text;
276
279
  try {
@@ -396,7 +399,7 @@ export async function resolveAbilityRelease(agentsRoot, command) {
396
399
  }
397
400
  const externalPaths = await verifyExternalDependencies(cacheRoot, resolved.root, lock);
398
401
  files = await collectAbilityFiles(resolved.root);
399
- const security = scan(files, manifest);
402
+ const security = scanAbilityFiles(files, manifest);
400
403
  const kinds = new Set(lock.assets.map((asset) => asset.kind));
401
404
  const memory = [];
402
405
  for (const [layer, mounts] of [["release-policy", manifest.spec.memory.policy], ["release-seed", manifest.spec.memory.seeds]]) {
@@ -200,6 +200,7 @@ export const ExecutionStartSchema = z.object({
200
200
  language: z.enum(["zh", "en"]),
201
201
  systemPrompt: z.string(),
202
202
  wakePrompt: z.string(),
203
+ maintenancePurpose: z.literal("self_training").optional(),
203
204
  }).strict(),
204
205
  runtime: z.object({
205
206
  name: RuntimeSchema,
@@ -567,8 +567,9 @@ export async function runExecution(config, input, dependencies) {
567
567
  projectContext = await cancellable(resolveProjectContext(projectSnapshot, projectWorkspaceRegistry), dependencies.cancellation);
568
568
  sessionContextFingerprint = projectSessionContextFingerprint(spec.runtime.name, projectSnapshot);
569
569
  }
570
+ const selfTraining = spec.instructions.maintenancePurpose === "self_training";
570
571
  let recalledMemory = "";
571
- if (spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
572
+ if (!selfTraining && spec.agent.memoryEnabled === true && dependencies.agentMemory !== undefined) {
572
573
  try {
573
574
  recalledMemory = await cancellable(dependencies.agentMemory.recall(spec.agent.handle, spec.instructions.wakePrompt, spec.executionId, spec.agent.memoryAgentId ?? null), dependencies.cancellation);
574
575
  }
@@ -827,7 +828,10 @@ export async function runExecution(config, input, dependencies) {
827
828
  const baseSystemPromptBudget = spec.agent.abilityRelease === undefined
828
829
  ? systemPromptBudget
829
830
  : Math.max(0, Math.min(systemPromptBudget, Math.max(32 * 1024, systemPromptBudget - 168 * 1024)));
830
- const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, baseSystemPromptBudget, { collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex" });
831
+ const systemPromptWithLocalFacts = withLocalExecutionFacts(spec.instructions.systemPrompt, baseSystemPromptBudget, {
832
+ ...(selfTraining ? { isolateLocalMemory: true } : {}),
833
+ collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
834
+ });
831
835
  const boundedSystemPrompt = (context) => appendAgentMemoryContext(typeof systemPromptWithLocalFacts === "string"
832
836
  ? systemPromptWithLocalFacts
833
837
  : systemPromptWithLocalFacts(context), recalledMemory, baseSystemPromptBudget);
@@ -876,12 +880,13 @@ export async function runExecution(config, input, dependencies) {
876
880
  } : {}),
877
881
  },
878
882
  session: {
879
- enabled: config.resume,
883
+ enabled: selfTraining ? false : config.resume,
880
884
  warmMs: config.resumeWarmMs,
881
885
  budgetTokens: config.sessionBudgetTokens,
882
886
  softTokens: config.sessionSoftTokens,
883
887
  maxTurns: config.sessionMaxTurns,
884
888
  },
889
+ ...(selfTraining ? { maintenancePurpose: "self_training" } : {}),
885
890
  localMemoryV2Enabled: config.localMemoryV2Handles?.has(spec.agent.handle) === true,
886
891
  };
887
892
  const result = await Promise.race([
@@ -1048,6 +1053,7 @@ export async function runExecution(config, input, dependencies) {
1048
1053
  }
1049
1054
  await dependencies.journal.complete(spec.executionId, completion);
1050
1055
  if (completion.outcome === "succeeded"
1056
+ && spec.instructions.maintenancePurpose !== "self_training"
1051
1057
  && spec.agent.memoryEnabled === true
1052
1058
  && memoryCaptureFinalText !== null
1053
1059
  && dependencies.agentMemory !== undefined) {
@@ -47,12 +47,13 @@ function takeCompletePromptSections(sections, maxBytes) {
47
47
  /** v1 server instructions opt in to daemon-owned paths and bounded local workspace context. */
48
48
  export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
49
49
  return ({ workspace, resuming, projectContext, trainingProjectContext }) => {
50
- const memory = capMemoryForInject(workspace.memory);
50
+ const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory);
51
51
  const workLog = resuming ? "" : capWorkLogForInject(workspace.workLog);
52
52
  const collaborationPrompt = options.collaborationTools === true ? buildCollaborationToolsPrompt() : "";
53
53
  const mandatoryWorkspacePrompt = `${buildRuntimeWorkspacePrompt(projectContext)}${buildTrainingGitProjectPrompt(trainingProjectContext)}`;
54
+ const crewHome = options.isolateLocalMemory ? workspace.runDir : workspace.dir;
54
55
  const localFactSections = [`\n\n## Local execution facts
55
- - $CREW_HOME: ${workspace.dir}
56
+ - $CREW_HOME: ${crewHome}
56
57
  - $CREW_TASK_LOG: ${workspace.workLogPath}`,
57
58
  ...(memory ? [`\n\n## Injected MEMORY.md (bounded local context)\n${memory}`] : []),
58
59
  ...(workLog ? [`\n\n## Injected work-log (bounded local context)\n${workLog}`] : []),
@@ -1,6 +1,7 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { delimiter, join } from "node:path";
4
+ import { cleanupSelfTrainingRunDir } from "./self-training-workspace.js";
4
5
  import { prepareWorkspace, rotateAgentSession, safeKey, } from "./workspace.js";
5
6
  import { CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV, isClaudeAdditionalDirectoryInstructionsSupported, probeClaudeVersion, spawnClaude, } from "./runtimes/claude.js";
6
7
  import { spawnCodex } from "./runtimes/codex.js";
@@ -8,7 +9,7 @@ import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepS
8
9
  import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
9
10
  import { materializePrivateCodexHome } from "./codex-home-launch.js";
10
11
  import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
11
- import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
12
+ import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, sanitizeEnvVars, } from "./provider-env.js";
12
13
  import { augmentedPath } from "./runtime-path.js";
13
14
  import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
14
15
  import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
@@ -68,14 +69,6 @@ function withoutClaudeInstructionFlag(env) {
68
69
  const warnRuntimeLocally = (code) => {
69
70
  process.stderr.write(`[runtime-warning] ${code}\n`);
70
71
  };
71
- export function sanitizeEnvVars(raw) {
72
- if (!raw)
73
- return {};
74
- return Object.fromEntries(Object.entries(raw).filter(([key, value]) => typeof value === "string"
75
- && ENV_KEY_RE.test(key)
76
- && !key.toUpperCase().startsWith("CREW_")
77
- && !RESERVED_ENV.has(key.toUpperCase())));
78
- }
79
72
  export function awaitExit(child) {
80
73
  return new Promise((resolve) => {
81
74
  let settled = false;
@@ -126,6 +119,7 @@ async function launchLegacyRuntime(request) {
126
119
  effectivePermission: request.effectivePermission,
127
120
  ...(request.model === undefined ? {} : { model: request.model }),
128
121
  ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
122
+ ...(request.networkAccess === undefined ? {} : { networkAccess: request.networkAccess }),
129
123
  };
130
124
  if (request.runtime === "claude") {
131
125
  const additionalDirectories = request.claudeAdditionalDirectories
@@ -215,6 +209,7 @@ export async function executeLocal(input, callbacks = {}, dependencies = {}) {
215
209
  async function executeLocalUnlocked(input, callbacks, dependencies) {
216
210
  const providerConfig = input.launch.providerConfig ?? {};
217
211
  const { runtime } = input;
212
+ const isolatedMaintenance = input.maintenancePurpose === "self_training";
218
213
  const isDeepSeekCodex = runtime.name === "codex" && providerConfig.provider === "deepseek";
219
214
  const currentModel = isDeepSeekCodex ? DEEPSEEK_CODEX_MODEL : runtime.model ?? null;
220
215
  const launchModel = isDeepSeekCodex ? DEEPSEEK_CODEX_MODEL : runtime.model;
@@ -239,6 +234,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
239
234
  ? {}
240
235
  : { sessionContextFingerprint: input.sessionContextFingerprint }),
241
236
  ...(input.launch.description ? { description: input.launch.description } : {}),
237
+ ...(isolatedMaintenance ? { skipLocalMemory: true } : {}),
242
238
  }), dependencies.cancellation);
243
239
  }
244
240
  catch (error) {
@@ -269,12 +265,23 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
269
265
  }
270
266
  const sourceCodexHome = inheritedEnv.CODEX_HOME
271
267
  ?? (inheritedEnv.HOME === undefined ? undefined : join(inheritedEnv.HOME, ".codex"));
268
+ let materializedDeepSeekHome;
272
269
  try {
273
270
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
274
271
  throw new Error("DeepSeek API key is not configured for this Agent");
275
272
  }
276
273
  if (isDeepSeekCodex) {
277
- await awaitWithCancellation((dependencies.materializeDeepSeekCodexHome ?? materializeDeepSeekCodexHome)(workspace.homeDir), dependencies.cancellation);
274
+ const providerBaseUrl = providerConfig.providerBaseUrl
275
+ ? new URL(providerConfig.providerBaseUrl, input.launch.serverUrl).toString()
276
+ : undefined;
277
+ const materialize = dependencies.materializeDeepSeekCodexHome ?? materializeDeepSeekCodexHome;
278
+ const materializeOptions = {
279
+ ...(providerBaseUrl === undefined ? {} : { baseUrl: providerBaseUrl }),
280
+ ...(isolatedMaintenance ? { selfTraining: true } : {}),
281
+ };
282
+ materializedDeepSeekHome = await awaitWithCancellation(providerBaseUrl === undefined && !isolatedMaintenance
283
+ ? materialize(workspace.homeDir)
284
+ : materialize(workspace.homeDir, materializeOptions), dependencies.cancellation);
278
285
  }
279
286
  const supportsNativeResume = runtime.name === "claude"
280
287
  || (dependencies.launchRuntime !== undefined && runtimeCapability(runtime.name).nativeResume);
@@ -402,29 +409,30 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
402
409
  const systemPrompt = localMemoryResultPath === null
403
410
  ? baseSystemPrompt
404
411
  : `${baseSystemPrompt}${structuredMemoryInstructions ?? ""}`;
405
- try {
406
- localMemoryTelemetry = createLocalMemoryTelemetry({
407
- executionId: input.executionId,
408
- agentHandle: input.handle,
409
- ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
410
- runtime: runtime.name,
411
- stableProtocolRuntime: dependencies.launchRuntime !== undefined,
412
- agentDir: executionWorkspace.dir,
413
- runDir: executionWorkspace.runDir,
414
- isMemoryPrune: memoryPruneTraceId !== null,
415
- diagnosticsTimeoutMs: dependencies.localMemoryDiagnosticsTimeoutMs
416
- ?? LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS,
417
- });
418
- await localMemoryTelemetry.captureBefore();
419
- }
420
- catch (error) {
421
- localMemoryTelemetry = null;
422
- logLocalMemoryDiagnosticsFailure({
423
- executionId: input.executionId,
424
- agentHandle: input.handle,
425
- ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
426
- }, "initialize", error);
427
- }
412
+ if (!isolatedMaintenance)
413
+ try {
414
+ localMemoryTelemetry = createLocalMemoryTelemetry({
415
+ executionId: input.executionId,
416
+ agentHandle: input.handle,
417
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
418
+ runtime: runtime.name,
419
+ stableProtocolRuntime: dependencies.launchRuntime !== undefined,
420
+ agentDir: executionWorkspace.dir,
421
+ runDir: executionWorkspace.runDir,
422
+ isMemoryPrune: memoryPruneTraceId !== null,
423
+ diagnosticsTimeoutMs: dependencies.localMemoryDiagnosticsTimeoutMs
424
+ ?? LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS,
425
+ });
426
+ await localMemoryTelemetry.captureBefore();
427
+ }
428
+ catch (error) {
429
+ localMemoryTelemetry = null;
430
+ logLocalMemoryDiagnosticsFailure({
431
+ executionId: input.executionId,
432
+ agentHandle: input.handle,
433
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
434
+ }, "initialize", error);
435
+ }
428
436
  if (memoryPruneTraceId !== null) {
429
437
  memoryPruneSharedWriteKey = JSON.stringify([input.launch.agentsRoot, input.handle]);
430
438
  const activePruneCount = (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 0) + 1;
@@ -458,6 +466,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
458
466
  }
459
467
  }
460
468
  memoryPruneFailurePhase = "runtime_prepare";
469
+ const crewHome = isolatedMaintenance ? executionWorkspace.runDir : workspace.dir;
461
470
  const baseEnv = {
462
471
  ...inheritedEnv,
463
472
  ...sanitizeEnvVars(providerConfig.envVars),
@@ -466,7 +475,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
466
475
  CREW_SERVER_URL: input.launch.serverUrl,
467
476
  CREW_TOKEN: input.launch.token,
468
477
  CREW_CHANNEL: input.channelId,
469
- CREW_HOME: workspace.dir,
478
+ CREW_HOME: crewHome,
470
479
  // Keep the daemon-selected storage root authoritative for runtimes.
471
480
  // In profile-backed NowWork sessions the daemon config may differ from
472
481
  // the daemon process environment, so inheriting CREW_AGENTS_ROOT is not
@@ -476,7 +485,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
476
485
  ...(input.launch.qaReportRoot === undefined ? {} : { QA_REPORT_HUB: input.launch.qaReportRoot }),
477
486
  // Expose only the training root; ability-specific workspace layout stays
478
487
  // owned by the training package rather than the NowWork daemon.
479
- QA_RUNTIME_ROOT: join(workspace.dir, "training"),
488
+ QA_RUNTIME_ROOT: join(crewHome, "training"),
480
489
  CREW_TASK_DIR: executionWorkspace.runDir,
481
490
  CREW_TASK_LOG: executionWorkspace.workLogPath,
482
491
  ...(input.wakeMessageId ? { CREW_WAKE_MESSAGE_ID: input.wakeMessageId } : {}),
@@ -492,7 +501,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
492
501
  };
493
502
  const providerEnv = applyProviderEnv(baseEnv, runtime.name, input.launch.codexHomeMode === undefined
494
503
  ? providerConfig
495
- : { ...providerConfig, codexHomeMode: input.launch.codexHomeMode }, workspace.homeDir);
504
+ : { ...providerConfig, codexHomeMode: input.launch.codexHomeMode }, workspace.homeDir, materializedDeepSeekHome);
496
505
  const childEnv = runtime.name === "deepseek-harness"
497
506
  ? applyDeepSeekHarnessMachineEnv(providerEnv, inheritedEnv)
498
507
  : providerEnv;
@@ -503,7 +512,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
503
512
  qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
504
513
  qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
505
514
  inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
506
- training_root: join(workspace.dir, "training"),
515
+ training_root: join(crewHome, "training"),
507
516
  project_context_present: input.projectContext !== undefined,
508
517
  project_ids: JSON.stringify(input.projectContext === undefined
509
518
  ? []
@@ -635,6 +644,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
635
644
  ...(input.projectContext === undefined ? {} : {
636
645
  projectContext: input.projectContext,
637
646
  }),
647
+ ...(isolatedMaintenance && runtime.name === "codex" ? { networkAccess: true } : {}),
638
648
  ...(input.projectContext === undefined && workspaceRoots.length === 0
639
649
  ? {}
640
650
  : { workspaceRoots: Object.freeze(workspaceRoots) }),
@@ -1118,5 +1128,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
1118
1128
  const detail = error instanceof Error ? error.message : String(error);
1119
1129
  process.stderr.write(`[execution] failed to remove prompt ${workspace.systemPromptPath}: ${detail}\n`);
1120
1130
  }
1131
+ if (isolatedMaintenance) {
1132
+ await cleanupSelfTrainingRunDir(join(workspace.dir, "tasks"), executionWorkspace.runDir);
1133
+ }
1121
1134
  }
1122
1135
  }
@@ -25,7 +25,9 @@ export { executableRuntimes } from "./runtime-capabilities.js";
25
25
  const execFileP = promisify(execFile);
26
26
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
27
27
  export const TRAINING_PROJECT_DEPENDENCIES_CAPABILITY = "training_project_dependencies_v1";
28
+ export const SELF_TRAINING_EXECUTION_CAPABILITY = "self_training_execution_v1";
28
29
  export const EXECUTION_LARGE_ATTACHMENTS_CAPABILITY = "execution_attachments_300m_v1";
30
+ export const PROJECT_SKILL_PROJECTION_CAPABILITY = "project_skill_projection_v2";
29
31
  export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
30
32
  resolverReady: true,
31
33
  nativeRuntimeAdaptersReady: true,
@@ -42,6 +44,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
42
44
  "execution_machine_queue_v1",
43
45
  "execution_agent_memory_policy_v1",
44
46
  "execution_agent_memory_mapping_v1",
47
+ SELF_TRAINING_EXECUTION_CAPABILITY,
45
48
  "project_skills_v1",
46
49
  "project_skill_projection_v2",
47
50
  PROJECT_WORKSPACES_CAPABILITY,
@@ -16,12 +16,18 @@ export async function initializeAfterProjectSkillLeaseProtection(input) {
16
16
  /**
17
17
  * Runs only after execution-journal restart reconciliation has settled. A lease without a terminal
18
18
  * journal fact remains durable and prevents the caller from marking Project Skill v2 ready.
19
+ *
20
+ * 重连时本进程仍在跑的 execution 合法持有 lease(`liveExecutionIds`):既不释放,也不算 unresolved。
21
+ * 否则一次网络抖动就会让 daemon 在整条连接上摘掉 Project Skills 能力(incident: 2026-09-02,
22
+ * 绑定项目的 Agent 全部 execution_spec_incompatible)。
19
23
  */
20
24
  export async function recoverProjectSkillRuntimeRoots(input) {
21
25
  for (const agentRoot of input.agentRoots) {
22
26
  await input.roots.recover(agentRoot);
23
27
  let unresolvedLease = false;
24
28
  await input.leases.recover(agentRoot, async (executionId) => {
29
+ if (input.liveExecutionIds?.has(executionId))
30
+ return false;
25
31
  const entry = await input.journal.get(executionId);
26
32
  const releasable = entry?.state === "completed" || entry?.state === "interrupted";
27
33
  if (!releasable)
@@ -22,13 +22,14 @@ export function createProjectSkillRuntimeRootStartup() {
22
22
  leases,
23
23
  initialize,
24
24
  }),
25
- recover: async (agentsRoot, journal, protectedExecutionIds) => {
25
+ recover: async (agentsRoot, journal, protectedExecutionIds, liveExecutionIds) => {
26
26
  await recoverProjectSkillRuntimeRoots({
27
27
  agentRoots: await agentRoots(agentsRoot),
28
28
  journal,
29
29
  roots,
30
30
  leases,
31
31
  gc,
32
+ ...(liveExecutionIds === undefined ? {} : { liveExecutionIds }),
32
33
  currentRootId: async (agentRoot) => (await readAppliedProjectSkillManifest(agentRoot))?.rootId ?? null,
33
34
  afterLeaseRecovery: async () => {
34
35
  const remaining = await initializeAfterProjectSkillLeaseProtection({
@@ -1,6 +1,29 @@
1
1
  import { createExecutionJournal } from "../execution-journal.js";
2
2
  import { reconcileExecutionJournal } from "../execution-recovery.js";
3
3
  import { dslog, flushSlog } from "../slog.js";
4
+ /**
5
+ * 每次连接 preflight 的 Runtime root / lease 恢复。重连时本进程仍在跑的 execution 合法持有 lease,
6
+ * 必须当作存活而非孤儿;失败也必须留痕,否则整条连接静默摘掉 Project Skills 能力
7
+ * (incident: 2026-09-02,绑定项目的 Agent 全部 execution_spec_incompatible)。
8
+ */
9
+ export function createProjectSkillReconnectRecovery(startup, agentsRoot, journal, protectedExecutionIds, liveExecutionIds) {
10
+ return async () => {
11
+ const live = liveExecutionIds();
12
+ try {
13
+ await startup.recover(agentsRoot, journal(), protectedExecutionIds, live);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ dslog("project_skills.preflight_recovery_failed", "Project Skill runtime root 恢复失败,本连接不宣告 Project Skills 能力", {
18
+ level: "ERROR",
19
+ error_code: error.code ?? "unknown",
20
+ error_message: error.message,
21
+ live_execution_count: live.size,
22
+ });
23
+ return false;
24
+ }
25
+ };
26
+ }
4
27
  export async function initializeProjectSkillProtectedExecutionJournal(input) {
5
28
  return input.startup.initializeAfterLeaseProtection(input.config.agentsRoot, async (snapshot) => {
6
29
  input.protectedExecutionIds.clear();
@@ -11,6 +11,24 @@
11
11
  import { join } from "node:path";
12
12
  import { DEEPSEEK_CODEX_KEY_ENV, deepSeekCodexHome, } from "./runtimes/codex-deepseek-config.js";
13
13
  import { DEEPSEEK_HARNESS_CONFIG_ENV } from "./runtimes/deepseek-harness.js";
14
+ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
+ const RESERVED_ENV = new Set([
16
+ "PATH",
17
+ "XDG_CONFIG_HOME",
18
+ "XDG_DATA_HOME",
19
+ "XDG_CACHE_HOME",
20
+ "GH_CONFIG_DIR",
21
+ "CLOUDSDK_CONFIG",
22
+ ]);
23
+ /** Keep user-provided variables away from daemon-owned runtime and credential fields. */
24
+ export function sanitizeEnvVars(raw) {
25
+ if (!raw)
26
+ return {};
27
+ return Object.fromEntries(Object.entries(raw).filter(([key, value]) => typeof value === "string"
28
+ && ENV_KEY_RE.test(key)
29
+ && !key.toUpperCase().startsWith("CREW_")
30
+ && !RESERVED_ENV.has(key.toUpperCase())));
31
+ }
14
32
  /** custom 模式下必须从继承 env 中剔除的全局路由/凭证变量(避免与注入值叠加或抢优先级)。 */
15
33
  const CONFLICTING_ENV = [
16
34
  "ANTHROPIC_BASE_URL",
@@ -39,12 +57,12 @@ const CONFLICTING_CODEX_ENV = [
39
57
  DEEPSEEK_CODEX_KEY_ENV,
40
58
  ];
41
59
  /** 返回叠加了 provider 隔离的新 env(不改入参);不支持的 runtime/provider 组合原样返回。 */
42
- export function applyProviderEnv(base, runtime, cfg, homeDir) {
60
+ export function applyProviderEnv(base, runtime, cfg, homeDir, deepSeekHomeOverride) {
43
61
  if (runtime === "codex" && cfg.provider === "deepseek") {
44
62
  const env = { ...base };
45
63
  for (const key of CONFLICTING_CODEX_ENV)
46
64
  delete env[key];
47
- env.CODEX_HOME = deepSeekCodexHome(homeDir);
65
+ env.CODEX_HOME = deepSeekHomeOverride ?? deepSeekCodexHome(homeDir);
48
66
  if (cfg.providerApiKey)
49
67
  env[DEEPSEEK_CODEX_KEY_ENV] = cfg.providerApiKey;
50
68
  return env;
@@ -100,7 +118,7 @@ export function applyDeepSeekHarnessMachineEnv(base, machineEnv) {
100
118
  */
101
119
  export function providerFingerprint(runtime, cfg) {
102
120
  if (runtime === "codex" && cfg.provider === "deepseek") {
103
- return `deepseek|${cfg.providerApiKey ? "key" : "nokey"}`;
121
+ return `deepseek|${cfg.providerBaseUrl ?? "default"}|${cfg.providerApiKey ? "key" : "nokey"}`;
104
122
  }
105
123
  if (runtime !== "claude" || cfg.provider !== "custom")
106
124
  return null;
package/dist/runner.js CHANGED
@@ -11,7 +11,8 @@ import { readOriginDecisionFile, resetOriginDecisionFile } from "./origin-decisi
11
11
  import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-decision.js";
12
12
  import { launchSupervisedRuntime } from "./supervised-runtime.js";
13
13
  import { awaitWithCancellation, } from "./runtime-cancellation.js";
14
- export { awaitExit, exitActivity, sanitizeEnvVars } from "./local-executor.js";
14
+ export { awaitExit, exitActivity } from "./local-executor.js";
15
+ export { sanitizeEnvVars } from "./provider-env.js";
15
16
  const ICON = {
16
17
  init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
17
18
  claiming: "📌", crew: "⚙️", tool: "🛠️", tool_result: "↩️", done: "✅", error: "❌",
@@ -21,9 +21,14 @@ const RunnerInputSchema = z.object({
21
21
  cwd: z.string().min(1).optional(),
22
22
  workspaceRoots: z.array(z.string().min(1)).optional(),
23
23
  skillRoots: z.array(z.string().min(1)).optional(),
24
+ networkAccess: z.boolean().optional(),
24
25
  codexCollaborationTools: z.boolean().optional(),
25
26
  resume: z.boolean(),
26
- }).strict();
27
+ }).strict().superRefine((input, ctx) => {
28
+ if (input.networkAccess === true && input.effectivePermission !== "workspace_write") {
29
+ ctx.addIssue({ code: "custom", message: "network_access_requires_workspace_write" });
30
+ }
31
+ });
27
32
  const RPC_TIMEOUT_MS = 30_000;
28
33
  const INITIALIZE_RPC_TIMEOUT_MS = 60_000;
29
34
  const ERROR_MESSAGE_CAP = 2_000;
@@ -613,6 +618,9 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
613
618
  ...(input.reasoning === undefined || input.reasoning === "default"
614
619
  ? {}
615
620
  : { effort: input.reasoning }),
621
+ ...(input.networkAccess === true ? {
622
+ sandboxPolicy: { type: "workspaceWrite", networkAccess: true },
623
+ } : {}),
616
624
  });
617
625
  logStage(activeStage, "ok", attempt, stageStartedAt);
618
626
  turnId = started.turn.id;
@@ -7,10 +7,20 @@ export const DEEPSEEK_CODEX_BASE_URL = "https://api.deepseek.com/";
7
7
  export const DEEPSEEK_CODEX_KEY_ENV = "NOWCREW_DEEPSEEK_API_KEY";
8
8
  export const DEEPSEEK_CODEX_REASONING_LEVELS = ["low", "high", "max"];
9
9
  const PRIVATE_CODEX_HOME = ".codex-deepseek";
10
- export function deepSeekCodexHome(homeDir) {
11
- return resolve(homeDir, PRIVATE_CODEX_HOME);
10
+ const SELF_TRAINING_PRIVATE_CODEX_HOME = ".codex-deepseek-self-training";
11
+ export function deepSeekCodexHome(homeDir, selfTraining = false) {
12
+ return resolve(homeDir, selfTraining ? SELF_TRAINING_PRIVATE_CODEX_HOME : PRIVATE_CODEX_HOME);
12
13
  }
13
- export function renderDeepSeekConfig(codexHome) {
14
+ function normalizedBaseUrl(value) {
15
+ const url = new URL(value);
16
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
17
+ throw new Error("DeepSeek provider base URL must use HTTP(S)");
18
+ }
19
+ if (!url.pathname.endsWith("/"))
20
+ url.pathname = `${url.pathname}/`;
21
+ return url.toString();
22
+ }
23
+ export function renderDeepSeekConfig(codexHome, baseUrl = DEEPSEEK_CODEX_BASE_URL) {
14
24
  const catalogPath = join(codexHome, "models.json");
15
25
  return [
16
26
  `model = ${JSON.stringify(DEEPSEEK_CODEX_MODEL)}`,
@@ -20,7 +30,7 @@ export function renderDeepSeekConfig(codexHome) {
20
30
  "",
21
31
  "[model_providers.deepseek]",
22
32
  'name = "deepseek"',
23
- `base_url = ${JSON.stringify(DEEPSEEK_CODEX_BASE_URL)}`,
33
+ `base_url = ${JSON.stringify(normalizedBaseUrl(baseUrl))}`,
24
34
  'wire_api = "responses"',
25
35
  `env_key = ${JSON.stringify(DEEPSEEK_CODEX_KEY_ENV)}`,
26
36
  "",
@@ -40,11 +50,11 @@ async function writePrivateAtomic(path, content) {
40
50
  await rm(temporary, { force: true });
41
51
  }
42
52
  }
43
- export async function materializeDeepSeekCodexHome(homeDir) {
44
- const codexHome = deepSeekCodexHome(homeDir);
53
+ export async function materializeDeepSeekCodexHome(homeDir, options = {}) {
54
+ const codexHome = deepSeekCodexHome(homeDir, options.selfTraining === true);
45
55
  await mkdir(codexHome, { recursive: true, mode: 0o700 });
46
56
  await chmod(codexHome, 0o700);
47
57
  await writePrivateAtomic(join(codexHome, "models.json"), DEEPSEEK_CODEX_CATALOG);
48
- await writePrivateAtomic(join(codexHome, "config.toml"), renderDeepSeekConfig(codexHome));
58
+ await writePrivateAtomic(join(codexHome, "config.toml"), renderDeepSeekConfig(codexHome, options.baseUrl));
49
59
  return codexHome;
50
60
  }
@@ -36,6 +36,9 @@ export function buildCodexArgs(input) {
36
36
  else if (input.effectivePermission === "full_access" || (input.effectivePermission === undefined && input.dangerous)) {
37
37
  args.push("--dangerously-bypass-approvals-and-sandbox");
38
38
  }
39
+ if (input.effectivePermission === "workspace_write" && input.networkAccess === true) {
40
+ args.push("-c", "sandbox_workspace_write.network_access=true");
41
+ }
39
42
  for (const imagePath of input.imagePaths ?? [])
40
43
  args.push("--image", imagePath);
41
44
  // `-` instructs codex exec to read the prompt from stdin. Keeping the complete prompt out of argv
@@ -0,0 +1,15 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { isAbsolute, relative, sep } from "node:path";
3
+ /** Delete only a concrete child of this Agent's tasks root; never follow an ambiguous target. */
4
+ export async function cleanupSelfTrainingRunDir(tasksRoot, runDir) {
5
+ const child = relative(tasksRoot, runDir);
6
+ if (child === "" || isAbsolute(child) || child === ".." || child.startsWith(`..${sep}`))
7
+ return;
8
+ try {
9
+ await rm(runDir, { recursive: true, force: true });
10
+ }
11
+ catch (error) {
12
+ const detail = error instanceof Error ? error.message : String(error);
13
+ process.stderr.write(`[execution] failed to remove self-training run dir ${runDir}: ${detail}\n`);
14
+ }
15
+ }
@@ -46,6 +46,7 @@ export function supervisorLaunch(request) {
46
46
  effectivePermission: request.effectivePermission,
47
47
  ...(request.model === undefined ? {} : { model: request.model }),
48
48
  ...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
49
+ ...(request.networkAccess === undefined ? {} : { networkAccess: request.networkAccess }),
49
50
  ...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
50
51
  ...(request.imagePaths === undefined ? {} : { imagePaths: request.imagePaths }),
51
52
  ...(request.agentRoot === undefined ? {} : {
package/dist/workspace.js CHANGED
@@ -59,12 +59,13 @@ export async function prepareWorkspace(input) {
59
59
  const crewDir = join(dir, ".crew");
60
60
  await mkdir(crewDir, { recursive: true });
61
61
  // 预先创建明细目录,让收尾流程可以按功能创建 notes/<topic>.md。
62
- await mkdir(join(dir, "notes"), { recursive: true });
62
+ if (!input.skipLocalMemory)
63
+ await mkdir(join(dir, "notes"), { recursive: true });
63
64
  // MEMORY.md:首次创建"持久身份 + 知识索引"骨架,之后由 agent 自己维护
64
65
  const memoryPath = join(dir, "MEMORY.md");
65
- const memorySeedCreated = !(await exists(memoryPath));
66
+ const memorySeedCreated = !input.skipLocalMemory && !(await exists(memoryPath));
66
67
  const localMemoryV2Initialized = await hasLocalMemoryV2Marker(dir);
67
- if (memorySeedCreated) {
68
+ if (!input.skipLocalMemory && memorySeedCreated) {
68
69
  try {
69
70
  await writeFile(memoryPath, memorySeed(input.handle, input.description), "utf8");
70
71
  }
@@ -73,13 +74,15 @@ export async function prepareWorkspace(input) {
73
74
  throw error;
74
75
  }
75
76
  }
76
- let memory;
77
- try {
78
- memory = await readFile(memoryPath, "utf8");
79
- }
80
- catch (error) {
81
- logMemoryPrepareFailure(input, "memory_read", error);
82
- throw error;
77
+ let memory = "";
78
+ if (!input.skipLocalMemory) {
79
+ try {
80
+ memory = await readFile(memoryPath, "utf8");
81
+ }
82
+ catch (error) {
83
+ logMemoryPrepareFailure(input, "memory_read", error);
84
+ throw error;
85
+ }
83
86
  }
84
87
  // 系统提示词按 execution 隔离;省略 identity 的兼容调用每次使用随机路径。
85
88
  const promptDir = join(crewDir, "prompts");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.79-beta.2",
3
+ "version": "0.6.79",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",