@nowcrew/daemon 0.6.77-beta.1 → 0.6.77-beta.3

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.
@@ -0,0 +1,4 @@
1
+ export const ATTACHMENT_MAX_FILE_BYTES = 300 * 1024 * 1024;
2
+ export const ATTACHMENT_MAX_TOTAL_BYTES = ATTACHMENT_MAX_FILE_BYTES;
3
+ export const ATTACHMENT_DOWNLOAD_TIMEOUT_MS = 10 * 60_000;
4
+ export const ATTACHMENT_NATIVE_IMAGE_MAX_BYTES = 25 * 1024 * 1024;
@@ -1,7 +1,6 @@
1
- import { mkdir, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, open, rm } from "node:fs/promises";
2
2
  import { basename, extname, resolve, sep } from "node:path";
3
- export const ATTACHMENT_MAX_FILE_BYTES = 25 * 1024 * 1024;
4
- export const ATTACHMENT_MAX_TOTAL_BYTES = 50 * 1024 * 1024;
3
+ import { ATTACHMENT_DOWNLOAD_TIMEOUT_MS, ATTACHMENT_MAX_FILE_BYTES, ATTACHMENT_MAX_TOTAL_BYTES, } from "./attachment-limits.js";
5
4
  const throwIfAborted = (signal) => {
6
5
  if (signal?.aborted)
7
6
  throw signal.reason ?? new Error("Attachment materialization aborted");
@@ -46,7 +45,7 @@ function checkedRedirect(value, base) {
46
45
  }
47
46
  return url;
48
47
  }
49
- async function readBounded(response, maxBytes) {
48
+ async function writeBounded(response, path, maxBytes, signal) {
50
49
  const declared = Number(response.headers.get("content-length") ?? "");
51
50
  if (Number.isFinite(declared) && declared > maxBytes) {
52
51
  throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
@@ -54,21 +53,33 @@ async function readBounded(response, maxBytes) {
54
53
  if (!response.body)
55
54
  throw new Error("Attachment response body is empty");
56
55
  const reader = response.body.getReader();
57
- const chunks = [];
58
56
  let size = 0;
59
- while (true) {
60
- const part = await reader.read();
61
- if (part.done)
62
- break;
63
- const chunk = Buffer.from(part.value);
64
- size += chunk.length;
65
- if (size > maxBytes) {
66
- await reader.cancel();
67
- throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
57
+ try {
58
+ const file = await open(path, "wx", 0o600);
59
+ try {
60
+ while (true) {
61
+ throwIfAborted(signal);
62
+ const part = await reader.read();
63
+ if (part.done)
64
+ break;
65
+ const chunk = Buffer.from(part.value);
66
+ size += chunk.length;
67
+ if (size > maxBytes) {
68
+ throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
69
+ }
70
+ await file.writeFile(chunk);
71
+ }
72
+ throwIfAborted(signal);
73
+ return size;
74
+ }
75
+ finally {
76
+ await file.close();
68
77
  }
69
- chunks.push(chunk);
70
78
  }
71
- return Buffer.concat(chunks, size);
79
+ catch (error) {
80
+ await reader.cancel().catch(() => { });
81
+ throw error;
82
+ }
72
83
  }
73
84
  async function downloadAttachment(input) {
74
85
  const initial = new URL(`/agent/attachments/${encodeURIComponent(input.attachment.id)}/download`, input.serverUrl);
@@ -104,12 +115,11 @@ async function downloadAttachment(input) {
104
115
  }
105
116
  if (!response.ok)
106
117
  throw new Error(`Attachment download failed with status ${response.status}`);
107
- const data = await readBounded(response, input.maxFileBytes);
118
+ const size = await writeBounded(response, input.path, input.maxFileBytes, input.signal);
108
119
  throwIfAborted(input.signal);
109
- if (data.length !== input.attachment.sizeBytes) {
110
- throw new Error(`Attachment size mismatch: expected ${input.attachment.sizeBytes}, received ${data.length}`);
120
+ if (size !== input.attachment.sizeBytes) {
121
+ throw new Error(`Attachment size mismatch: expected ${input.attachment.sizeBytes}, received ${size}`);
111
122
  }
112
- return data;
113
123
  }
114
124
  catch (error) {
115
125
  if (input.signal?.aborted) {
@@ -161,22 +171,18 @@ export async function materializeAttachments(input) {
161
171
  }
162
172
  used.add(filename.toLowerCase());
163
173
  const path = containedPath(directory, filename);
164
- const data = await downloadAttachment({
174
+ await downloadAttachment({
165
175
  serverUrl: input.serverUrl,
166
176
  token: input.token,
167
177
  attachment,
168
178
  fetchImpl: input.fetchImpl ?? fetch,
169
179
  maxFileBytes,
170
180
  maxRedirects: input.maxRedirects ?? 3,
171
- timeoutMs: input.timeoutMs ?? 10_000,
181
+ timeoutMs: input.timeoutMs ?? ATTACHMENT_DOWNLOAD_TIMEOUT_MS,
182
+ path,
172
183
  ...(input.signal === undefined ? {} : { signal: input.signal }),
173
184
  });
174
185
  throwIfAborted(input.signal);
175
- await writeFile(path, data, {
176
- flag: "wx",
177
- mode: 0o600,
178
- ...(input.signal === undefined ? {} : { signal: input.signal }),
179
- });
180
186
  materialized.push({ ...attachment, filename, path });
181
187
  }
182
188
  return { directory, attachments: materialized };
@@ -1,20 +1,31 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { resolve } from "node:path";
2
+ import { posix, resolve, win32 } from "node:path";
3
3
  import { systemCommandRunner } from "./computer-service.js";
4
4
  import { daemonGlobalInstallation } from "./daemon-installation.js";
5
5
  import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-version.js";
6
6
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
7
- function configuredRegistryArgs(allowLoopbackHttp) {
7
+ const PUBLIC_NPM_REGISTRY = normalizeDaemonNpmRegistry();
8
+ function configuredRegistries(betaUpdate) {
8
9
  try {
9
- return process.env.CREW_NPM_REGISTRY === undefined ? [] : [
10
- "--registry",
11
- normalizeDaemonNpmRegistry(process.env.CREW_NPM_REGISTRY, { allowLoopbackHttp }),
12
- ];
10
+ const configured = process.env.CREW_NPM_REGISTRY === undefined
11
+ ? PUBLIC_NPM_REGISTRY
12
+ : normalizeDaemonNpmRegistry(process.env.CREW_NPM_REGISTRY, { allowLoopbackHttp: betaUpdate });
13
+ return betaUpdate ? [...new Set([configured, PUBLIC_NPM_REGISTRY])] : [configured];
13
14
  }
14
15
  catch {
15
16
  return null;
16
17
  }
17
18
  }
19
+ function npmCommand(platform, nodePath) {
20
+ const paths = platform === "win32" ? win32 : posix;
21
+ return paths.join(paths.dirname(nodePath), platform === "win32" ? "npm.cmd" : "npm");
22
+ }
23
+ function registryArgs(registry) {
24
+ return ["--registry", registry];
25
+ }
26
+ function isRegistryAuthFailure(stderr) {
27
+ return /(?:\bE401\b|\b401\b.*\bUnauthorized\b)/iu.test(stderr);
28
+ }
18
29
  export async function prepareDaemonRestart(input) {
19
30
  const local = input.localSlots.tryAcquireExclusive();
20
31
  if (local === null)
@@ -40,10 +51,10 @@ async function readPackageVersion(packageRoot) {
40
51
  const body = JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
41
52
  return typeof body.version === "string" ? body.version : "";
42
53
  }
43
- async function restoreInstalledVersion(input, runner, version, registryArgs) {
54
+ async function restoreInstalledVersion(input, runner, command, version, registryArgs) {
44
55
  if (!isDaemonReleaseVersion(version))
45
56
  return false;
46
- const result = await runner((input.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm", [
57
+ const result = await runner(command, [
47
58
  "install",
48
59
  "--global",
49
60
  "--prefix",
@@ -98,6 +109,8 @@ export async function installExactDaemonUpdate(input) {
98
109
  return { ok: false, errorCode: "ineligible" };
99
110
  }
100
111
  const runner = input.runner ?? systemCommandRunner;
112
+ const platform = input.platform ?? process.platform;
113
+ const command = npmCommand(platform, input.nodePath ?? process.execPath);
101
114
  let previousVersion = "";
102
115
  try {
103
116
  previousVersion = await (input.readInstalledVersion ?? readPackageVersion)(input.packageRoot);
@@ -111,9 +124,9 @@ export async function installExactDaemonUpdate(input) {
111
124
  await release();
112
125
  return { ok: false, errorCode: "install_failed" };
113
126
  }
114
- const registryArgs = configuredRegistryArgs(daemonReleaseChannel(previousVersion) === "beta"
127
+ const registries = configuredRegistries(daemonReleaseChannel(previousVersion) === "beta"
115
128
  || daemonReleaseChannel(input.targetVersion) === "beta");
116
- if (registryArgs === null) {
129
+ if (registries === null) {
117
130
  await release();
118
131
  return { ok: false, errorCode: "install_failed" };
119
132
  }
@@ -124,20 +137,33 @@ export async function installExactDaemonUpdate(input) {
124
137
  await release();
125
138
  return { ok: false, errorCode: "install_failed" };
126
139
  }
127
- const result = await runner((input.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm", [
128
- "install",
129
- "--global",
130
- "--prefix",
131
- input.npmPrefix,
132
- "--ignore-scripts",
133
- "--no-audit",
134
- "--no-fund",
135
- ...registryArgs,
136
- `@nowcrew/daemon@${input.targetVersion}`,
137
- ]).catch(() => null);
138
- if (result === null || result.exitCode !== 0) {
140
+ let selectedRegistryArgs = [];
141
+ let installed = false;
142
+ let authFailed = false;
143
+ for (const registry of registries) {
144
+ const args = registryArgs(registry);
145
+ const result = await runner(command, [
146
+ "install",
147
+ "--global",
148
+ "--prefix",
149
+ input.npmPrefix,
150
+ "--ignore-scripts",
151
+ "--no-audit",
152
+ "--no-fund",
153
+ ...args,
154
+ `@nowcrew/daemon@${input.targetVersion}`,
155
+ ]).catch(() => null);
156
+ if (result?.exitCode === 0) {
157
+ selectedRegistryArgs = args;
158
+ installed = true;
159
+ break;
160
+ }
161
+ if (result !== null && isRegistryAuthFailure(result.stderr))
162
+ authFailed = true;
163
+ }
164
+ if (!installed) {
139
165
  await release();
140
- return { ok: false, errorCode: "install_failed" };
166
+ return { ok: false, errorCode: authFailed ? "registry_auth_failed" : "install_failed" };
141
167
  }
142
168
  let installedVersion = "";
143
169
  try {
@@ -147,7 +173,7 @@ export async function installExactDaemonUpdate(input) {
147
173
  // Version verification is authoritative; unreadable package metadata is a mismatch.
148
174
  }
149
175
  if (installedVersion !== input.targetVersion) {
150
- const restored = await restoreInstalledVersion(input, runner, previousVersion, registryArgs);
176
+ const restored = await restoreInstalledVersion(input, runner, command, previousVersion, selectedRegistryArgs);
151
177
  if (!restored) {
152
178
  console.error(`[daemon-update] version verification failed; restore of ${previousVersion} was unsuccessful`);
153
179
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ATTACHMENT_MAX_FILE_BYTES, ATTACHMENT_MAX_TOTAL_BYTES, } from "./attachment-limits.js";
2
3
  import { AbilityRepositoryUrlSchema, SafeAbilityPathSchema, WorkspaceAbilityTargetSchema, } from "./agent-ability/types.js";
3
4
  import { compareProjectSkillRefs, MAX_AGENT_PROJECT_SKILL_BINDINGS, MAX_PROJECT_ID_LENGTH, MAX_PROJECT_SKILL_NAME_LENGTH, } from "./project-skills/types.js";
4
5
  const ExecutionIdSchema = z.string().uuid();
@@ -160,8 +161,10 @@ export const ExecutionAttachmentSchema = z.object({
160
161
  id: z.string().min(1).max(200),
161
162
  filename: z.string().min(1).max(255),
162
163
  mime: z.string().min(1).max(200),
163
- sizeBytes: z.number().int().nonnegative().max(25 * 1024 * 1024),
164
+ sizeBytes: z.number().int().nonnegative().max(ATTACHMENT_MAX_FILE_BYTES),
164
165
  }).strict();
166
+ const ExecutionAttachmentsSchema = z.array(ExecutionAttachmentSchema).max(20).refine((attachments) => attachments.reduce((total, attachment) => total + attachment.sizeBytes, 0)
167
+ <= ATTACHMENT_MAX_TOTAL_BYTES, "attachment bytes exceed the execution limit");
165
168
  const ExecutionAgentSchema = z.object({
166
169
  id: z.string().min(1),
167
170
  handle: AgentHandleSchema,
@@ -214,7 +217,7 @@ export const ExecutionStartSchema = z.object({
214
217
  scheduledRunId: ExecutionIdSchema.optional(),
215
218
  externalResponseSessionId: ExecutionIdSchema.optional(),
216
219
  answerStream: z.boolean().optional(),
217
- attachments: z.array(ExecutionAttachmentSchema).max(20).optional(),
220
+ attachments: ExecutionAttachmentsSchema.optional(),
218
221
  }).strict(),
219
222
  reporting: z.object({
220
223
  captureFinal: z.boolean(),
@@ -25,6 +25,7 @@ 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 EXECUTION_LARGE_ATTACHMENTS_CAPABILITY = "execution_attachments_300m_v1";
28
29
  export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
29
30
  resolverReady: true,
30
31
  nativeRuntimeAdaptersReady: true,
@@ -36,6 +37,7 @@ export const DAEMON_CAPABILITIES = Object.freeze([
36
37
  "execution_telemetry_ack_v1",
37
38
  "execution_external_output_v1",
38
39
  "execution_attachments_v1",
40
+ EXECUTION_LARGE_ATTACHMENTS_CAPABILITY,
39
41
  "execution_answer_stream_v1",
40
42
  "execution_machine_queue_v1",
41
43
  "execution_agent_memory_policy_v1",
@@ -1,3 +1,4 @@
1
+ import { ATTACHMENT_NATIVE_IMAGE_MAX_BYTES } from "./attachment-limits.js";
1
2
  export const LOCAL_EXECUTION_RUNTIMES = [
2
3
  "claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness",
3
4
  ];
@@ -46,7 +47,8 @@ export function routeRuntimeAttachments(runtime, attachments) {
46
47
  const lines = attachments.map((attachment) => `- ${JSON.stringify(attachment.path)} (${attachment.mime}, ${attachment.sizeBytes} bytes)`);
47
48
  return {
48
49
  nativeImagePaths: runtime === "codex"
49
- ? attachments.filter((attachment) => attachment.mime.startsWith("image/"))
50
+ ? attachments.filter((attachment) => attachment.mime.startsWith("image/")
51
+ && attachment.sizeBytes <= ATTACHMENT_NATIVE_IMAGE_MAX_BYTES)
50
52
  .map((attachment) => attachment.path)
51
53
  : [],
52
54
  promptSuffix: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.77-beta.1",
3
+ "version": "0.6.77-beta.3",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",