@sleep2agi/commhub-server 0.9.0-preview.105 → 0.9.0-preview.106

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.105",
3
+ "version": "0.9.0-preview.106",
4
4
  "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -3,7 +3,7 @@ import {
3
3
  ValidationError,
4
4
  validateName, validateRuntime, validateModel, validateFlagValue, RUNTIMES,
5
5
  validateEnvRefs, validateChannelsP1, serializeEnvLocal, buildAnetArgs,
6
- MAX_ENV_KEYS_PER_NODE,
6
+ MAX_ENV_KEYS_PER_NODE, validateFlagsForRuntime, COPRESENCE_FLAG_RUNTIMES,
7
7
  } from "./create-node-validate.js";
8
8
 
9
9
  const okSecret = (k: string, _net: string, key: string) => k === key ? `value-of-${k}` : undefined;
@@ -273,3 +273,38 @@ describe("#1298 RUNTIMES —— 七个都放行,且与 CLI 侧一致", () => {
273
273
  expect(() => validateRuntime("")).toThrow();
274
274
  });
275
275
  });
276
+
277
+ // #584 —— app「Codex(TUI 共存)」经 daemon 建出来的是无头节点:create_node 没有任何字段能表达「要共存」。
278
+ describe("#584 flags.copresence —— 建节点时的共存开关", () => {
279
+ test("is a known flag key and must be boolean", () => {
280
+ expect(() => validateFlagValue("copresence", true)).not.toThrow();
281
+ expect(() => validateFlagValue("copresence", false)).not.toThrow();
282
+ for (const bad of ["true", 1, null, {}]) {
283
+ try { validateFlagValue("copresence", bad); throw new Error("did not throw"); }
284
+ catch (e) { expect((e as ValidationError).code).toBe("flag_value_invalid"); }
285
+ }
286
+ });
287
+ test("only codex-app-server accepts it — elsewhere it would be swallowed silently", () => {
288
+ expect(() => validateFlagsForRuntime("codex-app-server", { copresence: true })).not.toThrow();
289
+ expect(() => validateFlagsForRuntime("claude-agent-sdk", { permissionMode: "default" })).not.toThrow();
290
+ expect(() => validateFlagsForRuntime("claude-agent-sdk", undefined)).not.toThrow();
291
+ for (const rt of ["claude-agent-sdk", "claude-code-cli", "codex-sdk", "grok-build-acp", "grok-build-cli", "opencode-cli"]) {
292
+ try { validateFlagsForRuntime(rt, { copresence: true }); throw new Error(`did not throw for ${rt}`); }
293
+ catch (e) {
294
+ expect((e as ValidationError).code).toBe("flag_not_applicable_to_runtime");
295
+ expect((e as ValidationError).detail).toMatchObject({ field: "copresence", runtime: rt, applicable: [...COPRESENCE_FLAG_RUNTIMES] });
296
+ }
297
+ }
298
+ });
299
+ test("buildAnetArgs emits the CLI's boolean `--copresence` (no value), and nothing for false", () => {
300
+ expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { copresence: true } }))
301
+ .toEqual(["node", "create", "cx", "--runtime", "codex-app-server", "--copresence"]);
302
+ expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { copresence: false } }))
303
+ .toEqual(["node", "create", "cx", "--runtime", "codex-app-server"]);
304
+ expect(() => buildAnetArgs({ name: "cx", runtime: "codex-sdk", flags: { copresence: true } })).toThrow(ValidationError);
305
+ });
306
+ test("the old request shape (no flag) is unchanged", () => {
307
+ expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { permissionMode: "default" } }))
308
+ .toEqual(["node", "create", "cx", "--runtime", "codex-app-server", "--permission-mode", "default"]);
309
+ });
310
+ });
@@ -46,7 +46,16 @@ export const RUNTIMES = [
46
46
  ] as const;
47
47
  export type Runtime = typeof RUNTIMES[number];
48
48
 
49
- export const FLAG_KEYS = ["permissionMode", "dangerouslySkipPermissions", "maxTurns", "budget", "timeout"] as const;
49
+ // #584 —— `copresence` 是**建节点时**的开关,不是 agent 的运行参数:为 true 时 daemon 把它写成子节点
50
+ // config 的 `codexCopresence: true`,之后一条 `anet node start` 就起共享 TUI(app 向导的「Codex(TUI 共存)」)。
51
+ // 🔴 故意放在 flags 里而不是 node_spec 顶层:老 hub 的 zod 会静默丢弃未知顶层字段、老 daemon 也静默忽略
52
+ // —— 那正是 #584 的形状(用户选了共存,拿到无头节点,没有任何报错)。flags 的未知键在老 hub(这份
53
+ // FLAG_KEYS)和老 daemon(buildAnetArgsDaemon)都**拒绝**,新 app 撞到老组件得到的是一条报错。
54
+ export const FLAG_KEYS = ["permissionMode", "dangerouslySkipPermissions", "maxTurns", "budget", "timeout", "copresence"] as const;
55
+
56
+ /** #584 —— 只有这些 runtime 认 `flags.copresence`。目前只有 codex-app-server 的共存靠 config 字段
57
+ * (`codexCopresence`)决定;grok-build-cli / opencode-cli 各有自己的字段,接进来之前一律拒,不静默吞。 */
58
+ export const COPRESENCE_FLAG_RUNTIMES = ["codex-app-server"] as const;
50
59
  export type FlagKey = typeof FLAG_KEYS[number];
51
60
 
52
61
  export const PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"] as const;
@@ -124,11 +133,25 @@ export function validateFlagValue(k: string, v: unknown): void {
124
133
  throw new ValidationError("flag_value_invalid", { field: k, reason: "must be integer 1..86400" });
125
134
  }
126
135
  return;
136
+ case "copresence":
137
+ if (typeof v !== "boolean") throw new ValidationError("flag_value_invalid", { field: k, reason: "must be boolean" });
138
+ return;
127
139
  default:
128
140
  throw new ValidationError("flag_key_unknown", { field: k });
129
141
  }
130
142
  }
131
143
 
144
+ /** #584 —— 逐键校验之外的跨字段一条:`copresence` 只对 COPRESENCE_FLAG_RUNTIMES 有意义。
145
+ * 在别的 runtime 上收下它而什么都不做,就是又一个「选了共存、拿到无头」。 */
146
+ export function validateFlagsForRuntime(runtime: string, flags: Record<string, unknown> | undefined | null): void {
147
+ if (!flags || !Object.prototype.hasOwnProperty.call(flags, "copresence")) return;
148
+ if (!(COPRESENCE_FLAG_RUNTIMES as readonly string[]).includes(runtime)) {
149
+ throw new ValidationError("flag_not_applicable_to_runtime", {
150
+ field: "copresence", runtime, applicable: [...COPRESENCE_FLAG_RUNTIMES],
151
+ });
152
+ }
153
+ }
154
+
132
155
  // §4.4.7 — env_refs 7-step gate. networkSecretsGet is injected so
133
156
  // callers can supply either a real DB lookup (hub) or a fixture (tests).
134
157
  // The 6th step also reports the resolved value size; we return the
@@ -281,8 +304,11 @@ export function buildAnetArgs(spec: NodeSpec): string[] {
281
304
  throw new ValidationError("flag_key_unknown", { field: k });
282
305
  }
283
306
  validateFlagValue(k, v);
307
+ // `anet node create --copresence` 是布尔开关,不吃值。
308
+ if (k === "copresence") { if (v === true) args.push("--copresence"); continue; }
284
309
  args.push(`--${kebab(k)}`, String(v));
285
310
  }
311
+ validateFlagsForRuntime(spec.runtime, spec.flags);
286
312
  }
287
313
  return args;
288
314
  }
package/src/tools.ts CHANGED
@@ -30,6 +30,7 @@ import {
30
30
  validateEnvRefs,
31
31
  FLAG_KEYS,
32
32
  validateFlagValue,
33
+ validateFlagsForRuntime,
33
34
  ValidationError,
34
35
  } from "./create-node-validate.js";
35
36
  import {
@@ -4252,6 +4253,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
4252
4253
  if (!(FLAG_KEYS as readonly string[]).includes(k)) throw new ValidationError("flag_key_unknown", { field: k });
4253
4254
  validateFlagValue(k, v);
4254
4255
  }
4256
+ validateFlagsForRuntime(node_spec.runtime, node_spec.flags);
4255
4257
  } catch (e) {
4256
4258
  return validationFailReply(e);
4257
4259
  }