@ai-workbench/network 0.4.1 → 0.4.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/lib/network.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { control } from './control.ts';
2
2
  import type { PluginSettings, NetworkChannels, NetworkState } from './api.ts';
3
+ import type { EmployeeRuntime } from './employee-endpoint.ts';
3
4
  export interface Executable {
4
5
  path: string;
5
6
  sha256: string;
6
7
  platform: string;
7
8
  arch: string;
8
9
  }
9
- export declare function createNetwork(settings: PluginSettings, executable: Executable, emit?: (state: NetworkState) => void, controlIO?: typeof control): NetworkChannels & {
10
+ export declare function createNetwork(settings: PluginSettings, executable: Executable, emit?: (state: NetworkState) => void, controlIO?: typeof control): NetworkChannels & EmployeeRuntime & {
10
11
  start(): Promise<void>;
11
12
  };
package/lib/network.js CHANGED
@@ -5,6 +5,9 @@ import { privateFile, verifyExecutable } from "./private.js";
5
5
  import { control, jsonResponse, firstFrame } from "./control.js";
6
6
  import { fail, ok } from "./api.js";
7
7
  import { downloadThroughProxy } from "./download.js";
8
+ import { effectiveConfig, helperPolicyArgs, validHelperPolicy } from "./settings.js";
9
+ import { ObservationExpired, observationNeed } from "./observation.js";
10
+ import { readEmployeeProcess, employeeReady } from "./employee-endpoint.js";
8
11
  const seconds = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0.001 && v * 1000 <= 2147483647;
9
12
  const integer = (v) => Number.isSafeInteger(v) && Number(v) > 0;
10
13
  const quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
@@ -39,10 +42,11 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
39
42
  let state = { phase: 'starting' }, cfg, child;
40
43
  let starting, disposing, disposed = false, online = false;
41
44
  let poll;
42
- let processFailed = false, startupReceipt, terminalNeed, stoppingChild;
45
+ let processFailed = false, startupReceipt, terminalNeed, stoppingChild, shutdownNeed;
43
46
  const childGone = new AbortController();
44
47
  const lifetime = new AbortController(), listeners = new Set(), updates = new Set();
45
48
  const leases = new Set();
49
+ let employeeParent, employeeUmask;
46
50
  const snapshot = () => structuredClone(state);
47
51
  const publish = (next) => {
48
52
  state = next;
@@ -72,9 +76,10 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
72
76
  if (!owned?.pid || owned.exitCode !== null || owned.signalCode !== null)
73
77
  return false;
74
78
  const exit = new Promise(resolve => owned.once('exit', () => resolve()));
79
+ const started = performance.now();
75
80
  owned.kill('SIGTERM');
76
81
  let forced = false, timer;
77
- await Promise.race([exit, new Promise(resolve => { timer = setTimeout(() => { forced = true; owned.kill('SIGKILL'); resolve(); }, settings.shutdownObservationSeconds * 1000); })]);
82
+ await Promise.race([exit, new Promise(resolve => { timer = setTimeout(() => { forced = true; shutdownNeed = observationNeed(new ObservationExpired('HELPER_SHUTDOWN_FORCED', 'shutdownObservationSeconds', settings.shutdownObservationSeconds, started, '自有网络组件正常退出', '检查组件退出日志;本次仅强制结束自有进程,不终止未知进程')); owned.kill('SIGKILL'); resolve(); }, settings.shutdownObservationSeconds * 1000); })]);
78
83
  if (timer)
79
84
  clearTimeout(timer);
80
85
  if (forced)
@@ -94,9 +99,14 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
94
99
  if (!disposed && alive())
95
100
  publish({ phase: s.enrollment === 'online' && !s.error ? 'online' : s.error ? 'blocked' : 'starting', status: s, ...s.error ? { error: s.error } : {} });
96
101
  }
97
- catch {
98
- if (!disposed && alive())
99
- block('CONTROL_UNAVAILABLE', '连接中断,正在等待网络组件恢复;根因未查明。', '等待恢复后自动重新连接;持续无响应时联系管理员');
102
+ catch (error) {
103
+ if (!disposed && alive()) {
104
+ const observed = observationNeed(error);
105
+ if (observed)
106
+ publish({ phase: 'blocked', error: observed });
107
+ else
108
+ block('CONTROL_UNAVAILABLE', '连接中断,正在等待网络组件恢复;根因未查明。', '检查组件私有日志、控制socket和当前网络;状态刷新不是修复,不重启或重交材料');
109
+ }
100
110
  }
101
111
  if (!disposed && alive())
102
112
  poll = setTimeout(() => void observe(), cfg.statusRefreshIntervalSeconds * 1000);
@@ -119,6 +129,9 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
119
129
  finally {
120
130
  await file.close();
121
131
  }
132
+ if (!validHelperPolicy(settings))
133
+ throw new Error('CONFIG_INVALID');
134
+ cfg = effectiveConfig(cfg, settings);
122
135
  if (normalize(settings.helperLogFile) !== settings.helperLogFile || normalize(settings.configFile) !== settings.configFile)
123
136
  throw new Error('CONFIG_INVALID');
124
137
  const canonical = async (p) => { try {
@@ -174,8 +187,20 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
174
187
  await log.close();
175
188
  return;
176
189
  }
190
+ const parent = readEmployeeProcess();
191
+ employeeParent = parent.ok ? parent.value : undefined;
192
+ const environment = { ...process.env };
193
+ delete environment.AW_NET_PARENT_PID;
194
+ delete environment.AW_NET_PARENT_SHELL;
195
+ delete environment.AW_NET_PARENT_UMASK;
196
+ if (employeeParent) {
197
+ employeeUmask = process.umask();
198
+ environment.AW_NET_PARENT_PID = String(process.pid);
199
+ environment.AW_NET_PARENT_SHELL = employeeParent.account.shell;
200
+ environment.AW_NET_PARENT_UMASK = String(employeeUmask);
201
+ }
177
202
  try {
178
- child = spawn(executable.path, ['serve', '--config', settings.configFile], { stdio: ['ignore', 'pipe', log.fd], env: { ...process.env } });
203
+ child = spawn(executable.path, ['serve', '--config', settings.configFile, ...helperPolicyArgs(settings), '--controlResponseMaxBytes', String(settings.controlResponseMaxBytes)], { stdio: ['ignore', 'pipe', log.fd], env: environment });
179
204
  }
180
205
  catch (error) {
181
206
  await log.close();
@@ -196,7 +221,8 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
196
221
  processChild.once('exit', exited);
197
222
  const ready = new Promise((resolve, reject) => {
198
223
  let data = Buffer.alloc(0), settled = false;
199
- const timer = setTimeout(() => finish(new Error('HELPER_STARTUP_EXPIRED')), settings.startupObservationSeconds * 1000);
224
+ const started = performance.now();
225
+ const timer = setTimeout(() => finish(new ObservationExpired('HELPER_STARTUP_EXPIRED', 'startupObservationSeconds', settings.startupObservationSeconds, started, '自有网络组件ready首帧', '检查组件私有日志、完整性与启动状态;保留身份,不重交开通材料')), settings.startupObservationSeconds * 1000);
200
226
  const aborted = () => finish(new Error('DISPOSED'));
201
227
  const earlyExit = () => finish(new Error('HELPER_EXITED'));
202
228
  const errored = () => finish(new Error('HELPER_START_FAILED'));
@@ -258,10 +284,12 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
258
284
  if (disposed)
259
285
  return;
260
286
  const code = error instanceof Error && /^(LOCAL_PERMISSION_DENIED|LOCAL_PATH_INVALID|HELPER_[A-Z_]+|CONTROL_[A-Z_]+|CONFIG_INVALID)$/.test(error.message) ? error.message : 'CONFIG_INVALID';
261
- terminal(startupNeed ?? needs(code, '网络组件未能启动,根因未查明', '管理员检查安装配置、helper完整性和私有日志'));
287
+ terminal(startupNeed ?? observationNeed(error) ?? needs(code, '网络组件未能启动,根因未查明', '管理员检查安装配置、helper完整性和私有日志'));
262
288
  const forced = await stopOwned();
263
- if (forced && terminalNeed)
264
- terminalNeed.cause = '自有helper未在shutdownObservationSeconds内退出,已SIGKILL清理';
289
+ if (forced && terminalNeed) {
290
+ terminalNeed.cause = [terminalNeed.cause, shutdownNeed?.message, shutdownNeed?.needs].filter(Boolean).join(';');
291
+ publish({ ...state, error: terminalNeed });
292
+ }
265
293
  }
266
294
  })();
267
295
  const api = {
@@ -325,16 +353,57 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
325
353
  return fail('DISPOSED', '登录代理已取消', '重新发起登录');
326
354
  return ok(current);
327
355
  }
328
- catch {
356
+ catch (error) {
329
357
  owner.abort();
330
358
  signal.removeEventListener('abort', cancel);
331
359
  lifetime.signal.removeEventListener('abort', cancel);
332
360
  childGone.signal.removeEventListener('abort', cancel);
361
+ diagnostic ??= observationNeed(error);
333
362
  return diagnostic ? { ok: false, error: diagnostic } : fail('PROXY_UNAVAILABLE', '登录代理未能建立,根因未查明', '查看主机网络与信任材料');
334
363
  }
335
364
  },
336
365
  async download(source, signal) { const lease = await api.openLoginProxy(signal); if (!lease.ok)
337
366
  return lease; return downloadThroughProxy(source, lease.value, signal, settings.downloadMaxRedirects, cfg.dialTimeoutSeconds, cfg.httpMaxResponseHeaderBytes); },
367
+ async prepareEmployeeEndpoint(settingsPath, signal) {
368
+ if (disposed)
369
+ return fail('DISPOSED', '网络服务已停止。', '重新加载插件');
370
+ if (signal.aborted)
371
+ return fail('EMPLOYEE_ENDPOINT_PREPARE_CANCELLED', '员工端点准备等待已取消。', '派发前重新核验来源');
372
+ if (!cfg || state.phase !== 'online' || !alive())
373
+ return fail('NETWORK_NOT_READY', '远程主机尚未连接。', '等待原网络连接');
374
+ const actual = readEmployeeProcess();
375
+ if (!actual.ok)
376
+ return actual;
377
+ if (!employeeParent || employeeUmask !== process.umask() || JSON.stringify(employeeParent) !== JSON.stringify(actual.value))
378
+ return fail('EMPLOYEE_ENVIRONMENT_UNAVAILABLE', '客户端环境在组件启动后已变化。', '正常重载插件后重新核验,不替换账户或环境');
379
+ try {
380
+ const response = await controlIO(socket(), 'POST', '/v1/employee-endpoint', AbortSignal.any([signal, childGone.signal, lifetime.signal]), cfg.dialTimeoutSeconds, cfg.httpMaxHeaderBytes, { version: 1, settingsPath });
381
+ const value = await jsonResponse(response, settings.controlResponseMaxBytes);
382
+ if (response.statusCode !== 200) {
383
+ const error = value?.error;
384
+ if (value?.apiVersion !== 1 || !error || !employeeNeedCodes.has(error.code) || !['message', 'needs', 'actor'].every(k => typeof error[k] === 'string') || (error.cause !== null && error.cause !== undefined && typeof error.cause !== 'string'))
385
+ return fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID', '员工端点控制响应无效。', '核对网络组件版本与合同');
386
+ return { ok: false, error: { code: error.code, message: error.message, needs: error.needs, actor: 'network', cause: error.cause ?? null } };
387
+ }
388
+ if (value?.apiVersion !== 1 || Object.keys(value).sort().join() !== 'apiVersion,bridge,endpoint')
389
+ return fail('EMPLOYEE_BRIDGE_PROTOCOL_INVALID', '员工端点控制帧无效。', '核对网络组件版本');
390
+ return employeeReady({ endpoint: value.endpoint, bridge: value.bridge }, actual.value, settingsPath);
391
+ }
392
+ catch (error) {
393
+ if (signal.aborted)
394
+ return fail('EMPLOYEE_ENDPOINT_PREPARE_CANCELLED', '员工端点准备等待已取消。', '重新核验后派发,不重放旧操作');
395
+ const observed = observationNeed(error);
396
+ return observed ? { ok: false, error: observed } : fail('EMPLOYEE_ENDPOINT_UNAVAILABLE', '员工端点控制连接失败,根因未查明。', '检查组件私有日志;普通网络状态不代表端点就绪');
397
+ }
398
+ },
399
+ async closeEmployeeEndpoint() {
400
+ if (!cfg || !alive())
401
+ return;
402
+ const response = await request('DELETE', '/v1/employee-endpoint', lifetime.signal);
403
+ const value = await jsonResponse(response, settings.controlResponseMaxBytes);
404
+ if (response.statusCode !== 200 || value?.apiVersion !== 1 || value?.closed !== true)
405
+ throw Error('EMPLOYEE_ENDPOINT_UNAVAILABLE');
406
+ },
338
407
  dispose() {
339
408
  return disposing ??= (async () => {
340
409
  disposed = true;
@@ -346,7 +415,7 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
346
415
  await starting;
347
416
  await Promise.all([...leases].map(x => x.release()));
348
417
  const forced = await stopOwned();
349
- publish(forced ? { phase: 'blocked', error: needs('HELPER_SHUTDOWN_FORCED', '网络组件未在配置期限内退出,已终止自有进程', '管理员检查组件退出日志') } : { phase: 'stopped' });
418
+ publish(forced ? { phase: 'blocked', error: shutdownNeed } : { phase: 'stopped' });
350
419
  listeners.clear();
351
420
  updates.clear();
352
421
  })();
@@ -354,3 +423,4 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
354
423
  };
355
424
  return api;
356
425
  }
426
+ const employeeNeedCodes = new Set(['EMPLOYEE_CONTEXT_UNAVAILABLE', 'EMPLOYEE_CONTEXT_INVALID', 'EMPLOYEE_CONTEXT_CHANGED', 'EMPLOYEE_ACCOUNT_UNAVAILABLE', 'EMPLOYEE_ENVIRONMENT_UNAVAILABLE', 'EMPLOYEE_IDENTITY_STATE_INVALID', 'EMPLOYEE_IDENTITY_ACCOUNT_MISMATCH', 'EMPLOYEE_IDENTITY_BUSY_OR_UNSAFE', 'EMPLOYEE_IDENTITY_CREATE_FAILED', 'EMPLOYEE_IDENTITY_STORAGE_FAILED', 'EMPLOYEE_BRIDGE_UNAVAILABLE', 'EMPLOYEE_BRIDGE_PROTOCOL_INVALID', 'EMPLOYEE_BRIDGE_FAILED', 'EMPLOYEE_ENDPOINT_UNAVAILABLE', 'EMPLOYEE_ENDPOINT_PREPARE_CANCELLED', 'HELPER_PLATFORM_UNSUPPORTED', 'HOST_TRUST_REQUIRED', 'HOST_KEY_MISMATCH', 'ENROLLMENT_MISMATCH', 'DIAL_OBSERVATION_EXPIRED', 'LOCAL_PERMISSION_DENIED', 'NETWORK_INTERRUPTED', 'SSH_FAILED', 'NETWORK_NOT_READY', 'DISPOSED']);
@@ -0,0 +1,6 @@
1
+ import type { Need } from './api.ts';
2
+ export declare class ObservationExpired extends Error {
3
+ readonly need: Need;
4
+ constructor(code: string, setting: string, seconds: number, started: number, waitingFor: string, nextStep: string);
5
+ }
6
+ export declare function observationNeed(error: unknown): Need | undefined;
@@ -0,0 +1,14 @@
1
+ // Only fixed stage descriptions and numeric timings enter public diagnostics.
2
+ export class ObservationExpired extends Error {
3
+ need;
4
+ constructor(code, setting, seconds, started, waitingFor, nextStep) {
5
+ const elapsed = Math.max(0, (performance.now() - started) / 1000);
6
+ const message = `等待${waitingFor},已等${elapsed.toFixed(3)}秒(${setting}=${seconds}秒),观察期限已到,根因未查明。`;
7
+ super(message + ' 下一步:' + nextStep);
8
+ this.name = 'ObservationExpired';
9
+ this.need = { code, message, needs: nextStep, actor: 'network', cause: JSON.stringify({ setting, configuredSeconds: seconds, elapsedSeconds: elapsed, waitingFor }) };
10
+ }
11
+ }
12
+ export function observationNeed(error) {
13
+ return error instanceof ObservationExpired ? structuredClone(error.need) : undefined;
14
+ }
@@ -1,25 +1,29 @@
1
- import { createRequire } from 'node:module';
2
- import { readFileSync } from 'node:fs';
1
+ import { readFileSync, lstatSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
3
  import { fail, ok } from "./api.js";
4
4
  export function resolvePackagedHelper(moduleURL, platform = process.platform, arch = process.arch) {
5
+ if (platform !== 'darwin' || !['arm64', 'x64'].includes(arch))
6
+ return fail('HELPER_PLATFORM_UNSUPPORTED', `我需要支持 ${platform}-${arch} 的联网助手。`, '当前提供 macOS arm64 和 x64,请管理员核对设备平台');
5
7
  const name = `@ai-workbench/net-helper-${platform}-${arch}`;
6
8
  let expected;
7
9
  try {
8
10
  const root = JSON.parse(readFileSync(new URL('../package.json', moduleURL), 'utf8'));
9
- expected = root.optionalDependencies?.[name];
10
- if (expected === undefined)
11
- return fail('HELPER_PLATFORM_UNSUPPORTED', `我需要支持 ${platform}-${arch} 的联网助手包。`, '当前提供 macOS arm64 和 x64 平台包,请管理员核对设备平台');
12
- if (expected !== root.version)
13
- throw new Error('平台助手依赖必须与主包版本精确一致');
14
- const require = createRequire(moduleURL), metadata = JSON.parse(readFileSync(require.resolve(name + '/package.json'), 'utf8'));
15
- if (metadata.name !== name || metadata.version !== expected || JSON.stringify(metadata.os) !== JSON.stringify([platform]) || JSON.stringify(metadata.cpu) !== JSON.stringify([arch]))
16
- throw new Error('平台助手包的名称、版本或平台不匹配');
17
- const manifest = JSON.parse(readFileSync(require.resolve(name + '/helper-manifest.json'), 'utf8'));
11
+ expected = root.version;
12
+ if (root.name !== '@ai-workbench/network' || typeof expected !== 'string' || !expected)
13
+ throw new Error('network主包的名称或版本无效');
14
+ const dir = new URL(`../helpers/${platform}-${arch}/`, moduleURL);
15
+ const metadata = JSON.parse(readFileSync(new URL('package.json', dir), 'utf8'));
16
+ if (metadata.name !== name || metadata.version !== expected || metadata.private !== true || JSON.stringify(metadata.os) !== JSON.stringify([platform]) || JSON.stringify(metadata.cpu) !== JSON.stringify([arch]))
17
+ throw new Error('内嵌助手的名称、版本或平台不匹配');
18
+ const manifest = JSON.parse(readFileSync(new URL('helper-manifest.json', dir), 'utf8'));
18
19
  if (manifest.platform !== platform || manifest.arch !== arch || typeof manifest.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(manifest.sha256))
19
- throw new Error('平台助手 manifest 的架构或摘要无效');
20
- return ok({ path: require.resolve(name + '/bin/aw-net'), sha256: manifest.sha256, platform, arch });
20
+ throw new Error('内嵌助手 manifest 的架构或摘要无效');
21
+ const path = fileURLToPath(new URL('bin/aw-net', dir)), st = lstatSync(path);
22
+ if (!st.isFile() || st.isSymbolicLink())
23
+ throw new Error('内嵌助手不是普通二进制文件');
24
+ return ok({ path, sha256: manifest.sha256, platform, arch });
21
25
  }
22
26
  catch (cause) {
23
- return { ok: false, error: { code: 'HELPER_NOT_PACKAGED', message: '我需要完整的当前平台联网助手包。', needs: `请管理员安装 ${name}${expected ? '@' + expected : '(与主包同版本)'};不要切换架构或改用旧助手`, actor: 'network', cause: cause instanceof Error ? cause.message : String(cause) } };
27
+ return { ok: false, error: { code: 'HELPER_NOT_PACKAGED', message: '我需要完整的当前版本远程主机网络插件。', needs: `请管理员重新安装 @ai-workbench/network${expected ? '@' + expected : ''} 的完整包;不要切换架构或改用旧助手`, actor: 'network', cause: cause instanceof Error ? cause.message : String(cause) } };
24
28
  }
25
29
  }
@@ -0,0 +1,10 @@
1
+ import type { PluginSettings, HelperConfig } from './api.ts';
2
+ export declare const helperPolicyFields: readonly ["statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes", "httpMaxResponseHeaderBytes"];
3
+ export declare const pluginPolicyFields: readonly ["startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "downloadMaxRedirects"];
4
+ export declare const settingsFields: readonly ["configFile", "helperLogFile", "startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "downloadMaxRedirects", "statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes", "httpMaxResponseHeaderBytes"];
5
+ export declare const timerSeconds: (v: unknown) => v is number;
6
+ export declare const positiveInteger: (v: unknown) => v is number;
7
+ export declare function validHelperPolicy(settings: PluginSettings): boolean;
8
+ export declare function mergedSettings(initial: PluginSettings, provided: PluginSettings): PluginSettings;
9
+ export declare function effectiveConfig(config: HelperConfig, settings: PluginSettings): HelperConfig;
10
+ export declare function helperPolicyArgs(settings: PluginSettings): string[];
@@ -0,0 +1,34 @@
1
+ export const helperPolicyFields = ['statusRefreshIntervalSeconds', 'dialTimeoutSeconds', 'headerTimeoutSeconds', 'enrollmentObservationSeconds', 'httpMaxHeaderBytes', 'httpMaxResponseHeaderBytes'];
2
+ export const pluginPolicyFields = ['startupObservationSeconds', 'shutdownObservationSeconds', 'controlResponseMaxBytes', 'downloadMaxRedirects'];
3
+ export const settingsFields = ['configFile', 'helperLogFile', ...pluginPolicyFields, ...helperPolicyFields];
4
+ export const timerSeconds = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0.001 && v * 1000 <= 2147483647;
5
+ export const positiveInteger = (v) => Number.isSafeInteger(v) && Number(v) > 0;
6
+ export function validHelperPolicy(settings) {
7
+ return helperPolicyFields.every(key => {
8
+ const v = settings[key];
9
+ if (v === undefined)
10
+ return true;
11
+ if (key === 'httpMaxHeaderBytes' || key === 'httpMaxResponseHeaderBytes')
12
+ return positiveInteger(v);
13
+ if (key === 'dialTimeoutSeconds' || key === 'statusRefreshIntervalSeconds')
14
+ return timerSeconds(v);
15
+ return typeof v === 'number' && Number.isFinite(v) && v >= 1e-9 && v < 9223372036.854775;
16
+ });
17
+ }
18
+ export function mergedSettings(initial, provided) {
19
+ const merged = { ...provided };
20
+ for (const key of [...pluginPolicyFields, ...helperPolicyFields])
21
+ if (initial[key] !== undefined)
22
+ merged[key] = initial[key];
23
+ return merged;
24
+ }
25
+ export function effectiveConfig(config, settings) {
26
+ const result = { ...config };
27
+ for (const key of helperPolicyFields)
28
+ if (settings[key] !== undefined)
29
+ result[key] = settings[key];
30
+ return result;
31
+ }
32
+ export function helperPolicyArgs(settings) {
33
+ return helperPolicyFields.flatMap(key => settings[key] === undefined ? [] : ['--' + key, String(settings[key])]);
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-workbench/network",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24.13.1"
@@ -25,7 +25,8 @@
25
25
  "lib",
26
26
  "NOTICE.md",
27
27
  "cordis.patch.yml",
28
- "locale"
28
+ "locale",
29
+ "helpers"
29
30
  ],
30
31
  "dsh": {
31
32
  "bundle": {
@@ -55,10 +56,6 @@
55
56
  "dsh-plugin",
56
57
  "deepseek-harness"
57
58
  ],
58
- "optionalDependencies": {
59
- "@ai-workbench/net-helper-darwin-arm64": "0.4.1",
60
- "@ai-workbench/net-helper-darwin-x64": "0.4.1"
61
- },
62
59
  "repository": {
63
60
  "type": "git",
64
61
  "url": "git+https://github.com/yzsnstotz/ai-workbench-network.git"