@sleep2agi/commhub-server 0.9.0-preview.13 → 0.9.0-preview.131
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 +130 -10
- package/bin/commhub.ts +99 -11
- package/package.json +7 -7
- package/src/ack-create-request-transport.test.ts +182 -0
- package/src/ack-create-request.test.ts +50 -7
- package/src/admin-delete-network-http.test.ts +228 -0
- package/src/admin-networks-http.test.ts +88 -0
- package/src/adoption-candidates-http.test.ts +137 -0
- package/src/adoption-candidates.ts +158 -0
- package/src/agent-access-batch.test.ts +210 -0
- package/src/agent-access.ts +487 -0
- package/src/agent-acl-groups-http.test.ts +247 -0
- package/src/agent-acl-http.test.ts +659 -0
- package/src/agent-acl-slice3-http.test.ts +267 -0
- package/src/agent-groups-perf.test.ts +223 -0
- package/src/agent-notice.ts +48 -0
- package/src/agent-team-whoami-http.test.ts +98 -0
- package/src/agent-teams-http.test.ts +323 -0
- package/src/agent-teams.ts +244 -0
- package/src/alias-filter.test.ts +72 -0
- package/src/alias-filter.ts +42 -0
- package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
- package/src/api-host-supervisors-fallback.test.ts +271 -0
- package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
- package/src/api-nodes-shape.test.ts +26 -10
- package/src/auth-password-change-cli-tokens-http.test.ts +294 -0
- package/src/auth-sessions-http.test.ts +250 -0
- package/src/auth-validate.test.ts +5 -1
- package/src/auth-weak-password-login.test.ts +176 -0
- package/src/auth.ts +598 -49
- package/src/avatar-validate.test.ts +163 -0
- package/src/avatar-validate.ts +103 -0
- package/src/binding-generation-http.test.ts +121 -0
- package/src/blocked-exit-on-reply.test.ts +122 -0
- package/src/boot-side-effect.test.ts +124 -0
- package/src/clone-node.test.ts +248 -0
- package/src/clone-node.ts +289 -0
- package/src/codex-config-flags.ts +77 -0
- package/src/codex-fork-contract.ts +23 -0
- package/src/codex-fork-migration.test.ts +38 -0
- package/src/commhub-bin-args.test.ts +85 -0
- package/src/config-apply-sec1.test.ts +107 -0
- package/src/config-apply-validate.test.ts +134 -1
- package/src/config-apply-validate.ts +111 -5
- package/src/create-node-blocked-gate.test.ts +356 -0
- package/src/create-node-network-id.test.ts +79 -0
- package/src/create-node-tool-schema.test.ts +48 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +193 -7
- package/src/create-node-validate.ts +227 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +77 -23
- package/src/daemon-provider-snapshots.test.ts +421 -0
- package/src/daemon-provider-snapshots.ts +327 -0
- package/src/daemon-token-owner-http.test.ts +236 -0
- package/src/dashboard-slash-routing-http.test.ts +107 -0
- package/src/db-adapter-guard.test.ts +148 -0
- package/src/db-adapter-translate.test.ts +128 -0
- package/src/db-adapter.ts +323 -87
- package/src/db-timestamp.test.ts +38 -0
- package/src/db-timestamp.ts +15 -0
- package/src/db.ts +933 -36
- package/src/delivered-without-push.test.ts +140 -0
- package/src/department-groups-http.test.ts +209 -0
- package/src/department-groups-sync-http.test.ts +328 -0
- package/src/department-groups.ts +434 -0
- package/src/department-heads-http.test.ts +330 -0
- package/src/department-heads.ts +226 -0
- package/src/departments-http.test.ts +185 -0
- package/src/departments.ts +260 -0
- package/src/desktop-message-seam.test.ts +150 -0
- package/src/desktop-message.test.ts +368 -0
- package/src/dm-file-scope-http.test.ts +178 -0
- package/src/ext-token-shared.test.ts +60 -0
- package/src/ext-validation-regression.test.ts +131 -0
- package/src/external-schedule-edits-http.test.ts +321 -0
- package/src/external-schedule-edits-timestamps.test.ts +64 -0
- package/src/external-schedule-edits.ts +327 -0
- package/src/file-download-authz-dev-open.test.ts +135 -0
- package/src/file-download-authz.test.ts +404 -0
- package/src/file-download-date-bucket.test.ts +436 -0
- package/src/file-network-scope-dev-open.test.ts +97 -0
- package/src/file-network-scope.test.ts +732 -0
- package/src/group-membership-events-http.test.ts +264 -0
- package/src/group-messages-http.test.ts +482 -0
- package/src/group-messages.ts +297 -0
- package/src/health-redaction.test.ts +642 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-provider-config.test.ts +139 -0
- package/src/hub-provider-config.ts +105 -0
- package/src/hub-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/inbox-caller.ts +49 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2362
- package/src/lifecycle-guard.ts +1 -1
- package/src/mcp-context-cost-http.test.ts +123 -0
- package/src/mcp-projects-events-http.test.ts +162 -0
- package/src/mcp-route-label-http.test.ts +120 -0
- package/src/mcp-write-network-resolution.test.ts +432 -0
- package/src/member-audit-network-id-http.test.ts +93 -0
- package/src/member-presence-http.test.ts +210 -0
- package/src/messages-ack-by-agent-http.test.ts +142 -0
- package/src/migrate-sqlite-to-pg.test.ts +47 -0
- package/src/migrate-sqlite-to-pg.ts +263 -0
- package/src/model-auth-notify-http.test.ts +181 -0
- package/src/model-auth-notify.ts +116 -0
- package/src/network-name-http.test.ts +75 -0
- package/src/network-scope.ts +276 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/node-alias-control.ts +12 -0
- package/src/node-attrs-validate.ts +88 -0
- package/src/node-attrs.test.ts +217 -0
- package/src/node-caller-identity-http.test.ts +781 -0
- package/src/node-create-request-status-http.test.ts +105 -0
- package/src/node-daemon-bindings.test.ts +132 -0
- package/src/node-daemon-bindings.ts +130 -0
- package/src/node-files-transport.test.ts +344 -0
- package/src/node-health-codex-login.test.ts +120 -0
- package/src/node-health-dispatch-http.test.ts +279 -0
- package/src/node-health-guard.ts +105 -0
- package/src/node-health-passthrough.test.ts +115 -0
- package/src/node-health-store.ts +101 -0
- package/src/node-id-alias.test.ts +190 -0
- package/src/node-identity-conflict-http.test.ts +269 -0
- package/src/node-identity-conflict.ts +142 -0
- package/src/node-identity-mismatch.test.ts +74 -0
- package/src/node-lifecycle-controllable.test.ts +64 -0
- package/src/node-lifecycle-controllable.ts +53 -0
- package/src/node-lifecycle-read-http.test.ts +245 -0
- package/src/node-lifecycle-read.ts +121 -0
- package/src/node-logs-transport.test.ts +263 -0
- package/src/node-permissions-http.test.ts +276 -0
- package/src/node-permissions.ts +285 -0
- package/src/node-request-retention.test.ts +281 -0
- package/src/node-request-retention.ts +120 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-ownership.ts +51 -0
- package/src/node-token-privilege-http.test.ts +158 -0
- package/src/observer-avatar-http.test.ts +431 -0
- package/src/observer-push.test.ts +252 -0
- package/src/opencode-v2-catalog.test.ts +268 -0
- package/src/opencode-v2-catalog.ts +352 -0
- package/src/package-version-consistency.test.ts +25 -0
- package/src/peer-reply-atomic.test.ts +455 -0
- package/src/peer-reply-capability.test.ts +70 -0
- package/src/pg-worker.ts +87 -0
- package/src/project-dir-warn-clear-http.test.ts +132 -0
- package/src/project-viewer-can-http.test.ts +114 -0
- package/src/push.ts +429 -10
- package/src/redact-tokens.test.ts +65 -0
- package/src/redact-tokens.ts +40 -0
- package/src/rename.ts +22 -2
- package/src/reply-target-derivation.test.ts +96 -0
- package/src/report-status-handover-capabilities-http.test.ts +124 -0
- package/src/report-status-host-schema.test.ts +235 -0
- package/src/report-status-resume-id-handover.test.ts +150 -0
- package/src/require-explicit-test-db.ts +9 -0
- package/src/requirement-due-reminders-http.test.ts +620 -0
- package/src/requirement-due-reminders.ts +416 -0
- package/src/requirement-events-http.test.ts +268 -0
- package/src/requirement-events.test.ts +45 -0
- package/src/requirement-events.ts +144 -0
- package/src/requirement-notify.ts +165 -0
- package/src/requirement-participant-notify-http.test.ts +309 -0
- package/src/requirement-tag-ops-http.test.ts +266 -0
- package/src/requirement-tags.ts +89 -0
- package/src/requirements-abandoned-http.test.ts +236 -0
- package/src/requirements-comment-http.test.ts +167 -0
- package/src/requirements-completed-at-migration.test.ts +70 -0
- package/src/requirements-errors-mcp-http.test.ts +164 -0
- package/src/requirements-errors.ts +143 -0
- package/src/requirements-etag-http.test.ts +91 -0
- package/src/requirements-http.test.ts +709 -0
- package/src/requirements-last-event-http.test.ts +349 -0
- package/src/requirements-list-mcp-slim-http.test.ts +155 -0
- package/src/requirements-list-slim-http.test.ts +319 -0
- package/src/requirements-mcp.test.ts +163 -0
- package/src/requirements-migrate.ts +259 -0
- package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
- package/src/requirements-people-http.test.ts +114 -0
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +127 -0
- package/src/requirements-priority-lowest.test.ts +114 -0
- package/src/requirements-search-http.test.ts +182 -0
- package/src/requirements-search.test.ts +32 -0
- package/src/requirements-search.ts +145 -0
- package/src/requirements-seq-http.test.ts +193 -0
- package/src/requirements-seq-migration.test.ts +63 -0
- package/src/requirements-start-date.test.ts +125 -0
- package/src/requirements-stats-http.test.ts +288 -0
- package/src/requirements-stats.ts +167 -0
- package/src/requirements-status-participants-http.test.ts +222 -0
- package/src/requirements-upgrade-e2e.test.ts +126 -0
- package/src/requirements-upgrade-migration.test.ts +98 -0
- package/src/requirements.ts +1806 -0
- package/src/resolve-port.test.ts +57 -0
- package/src/resolve-port.ts +44 -0
- package/src/response-charset.test.ts +130 -0
- package/src/rest-explicit-columns-http.test.ts +222 -0
- package/src/rest-identity.test.ts +130 -0
- package/src/rest-identity.ts +139 -0
- package/src/rest-projections.ts +68 -0
- package/src/rest-write-network-resolution.test.ts +133 -0
- package/src/restricted-files.ts +76 -0
- package/src/retention.ts +15 -3
- package/src/route-callers-http.test.ts +120 -0
- package/src/route-timing.test.ts +211 -0
- package/src/route-timing.ts +258 -0
- package/src/rules-file-duplicate-alias.test.ts +234 -0
- package/src/rules-file-session-target.test.ts +207 -0
- package/src/rules-file-transport.test.ts +237 -0
- package/src/runtime-label.test.ts +22 -0
- package/src/runtime-label.ts +21 -0
- package/src/runtime-readiness.ts +90 -0
- package/src/schedule-agent-mcp-http.test.ts +381 -0
- package/src/schedule-agent.ts +192 -0
- package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
- package/src/scheduled-failures-http.test.ts +233 -0
- package/src/scheduled-failures.ts +179 -0
- package/src/scheduled-offline.test.ts +131 -0
- package/src/scheduled-offline.ts +66 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-run-terminal.test.ts +268 -0
- package/src/scheduled-skip-blocking-task.test.ts +120 -0
- package/src/scheduled-stuck-http.test.ts +238 -0
- package/src/scheduled-stuck.ts +153 -0
- package/src/scheduled-tasks-http.test.ts +638 -0
- package/src/scheduled-tasks.ts +766 -0
- package/src/scoped-secrets.ts +124 -0
- package/src/send-dedup-request-id.test.ts +229 -0
- package/src/send-reply-agent-warning.test.ts +202 -0
- package/src/send-reply-alias-optional.test.ts +131 -0
- package/src/send-reply-attachments.test.ts +458 -0
- package/src/send_dedup.test.ts +54 -0
- package/src/send_dedup.ts +52 -6
- package/src/serve-error.test.ts +38 -0
- package/src/serve-error.ts +27 -0
- package/src/server.ts +5257 -0
- package/src/session-task-on-dispatch.ts +14 -0
- package/src/session-token-ownership-http.test.ts +122 -0
- package/src/shared/codex-inventory.ts +41 -0
- package/src/shared/daemon-provider-login.ts +31 -0
- package/src/shared/daemon-provider-snapshot.ts +52 -0
- package/src/shared/external-schedule-contract.ts +60 -0
- package/src/shared/node-name.test.ts +62 -0
- package/src/shared/node-name.ts +161 -0
- package/src/shared/provider-application.ts +29 -0
- package/src/shared/provider-collection.ts +14 -0
- package/src/shared/provider-initialization.ts +8 -0
- package/src/shared/reserved-env-drift.test.ts +10 -0
- package/src/shared/scoped-secret-contract.ts +5 -0
- package/src/shared/task-content-limit-drift.test.ts +28 -0
- package/src/shared/task-content-limit.ts +25 -0
- package/src/side-thread-command-transport.test.ts +312 -0
- package/src/side-thread-command-transport.ts +318 -0
- package/src/side-thread-http-integration.test.ts +233 -0
- package/src/side-thread-http.ts +504 -0
- package/src/side-thread-production.test.ts +72 -0
- package/src/side-thread-production.ts +118 -0
- package/src/side-thread.test.ts +1067 -0
- package/src/side-thread.ts +2499 -0
- package/src/skillhub-http.test.ts +108 -0
- package/src/sqlite-close-checkpoint.test.ts +78 -0
- package/src/start-node.test.ts +264 -0
- package/src/status-alias-filter-http.test.ts +106 -0
- package/src/status-alias-resolver-http.test.ts +163 -0
- package/src/status-alias-resolver.ts +19 -0
- package/src/status-node-id-filter-http.test.ts +161 -0
- package/src/status-read-cache-http.test.ts +229 -0
- package/src/status-read-cache.ts +122 -0
- package/src/stop-delete-node.test.ts +509 -1
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-auth-origin-http.test.ts +93 -0
- package/src/task-auth-origin.test.ts +24 -0
- package/src/task-auth-origin.ts +7 -0
- package/src/task-consumption.test.ts +482 -0
- package/src/task-diagnostic-http.test.ts +149 -0
- package/src/task-diagnostic.test.ts +115 -0
- package/src/task-diagnostic.ts +118 -0
- package/src/task-expiry-notice-http.test.ts +287 -0
- package/src/task-expiry-notice.ts +315 -0
- package/src/task-idempotency-mcp.test.ts +142 -0
- package/src/task-idempotency.test.ts +60 -0
- package/src/task-idempotency.ts +75 -0
- package/src/task-late-replies.ts +54 -0
- package/src/task-late-reply-http.test.ts +152 -0
- package/src/task-lifecycle-watcher.test.ts +204 -0
- package/src/task-lifecycle-watcher.ts +49 -0
- package/src/task-network-resolution-http.test.ts +140 -0
- package/src/task-orphan-http.test.ts +287 -0
- package/src/task-orphan.ts +119 -0
- package/src/task-queue-ahead-http.test.ts +309 -0
- package/src/task-queue-ahead-scale.test.ts +236 -0
- package/src/task-queue-ahead.ts +186 -0
- package/src/task-rejection-log-http.test.ts +160 -0
- package/src/task-rejection-log.ts +49 -0
- package/src/task-sender-http.test.ts +143 -0
- package/src/task-stale-open-http.test.ts +290 -0
- package/src/task-stale-open.ts +103 -0
- package/src/tasks-pagination-http.test.ts +141 -0
- package/src/tool-audience-http.test.ts +220 -0
- package/src/tool-audience.ts +118 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +3940 -325
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.ts +103 -6
- package/src/user-avatar-http.test.ts +74 -0
- package/src/user-avatar-migration.test.ts +25 -0
- package/src/user-inbox-by-agent-http.test.ts +161 -0
- package/src/user-inbox-netid-notnull.test.ts +122 -0
- package/src/user-inbox-read-http.test.ts +223 -0
- package/src/user-inbox-schema.test.ts +54 -0
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
ValidationError,
|
|
4
|
-
validateName, validateRuntime, validateModel, validateFlagValue,
|
|
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, validateOpenCodeV2ProviderModel, 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;
|
|
10
10
|
const allow = (k: string) => new Set<string>([k, "ANTHROPIC_API_KEY", "OPENAI_API_KEY"]);
|
|
11
11
|
|
|
12
|
-
describe("validateName (§4.2.2)", () => {
|
|
13
|
-
test("good names", () => {
|
|
14
|
-
for (const n of ["a", "demo-bot", "node_1", "z2", "abc-def-ghi"]) {
|
|
12
|
+
describe("validateName (§4.2.2, #652 Unicode names)", () => {
|
|
13
|
+
test("good names — incl. Chinese, upper case, digits first", () => {
|
|
14
|
+
for (const n of ["a", "demo-bot", "node_1", "z2", "abc-def-ghi", "测试", "研发助手A", "Demo", "1demo", "测".repeat(64)]) {
|
|
15
15
|
expect(() => validateName(n)).not.toThrow();
|
|
16
16
|
}
|
|
17
17
|
});
|
|
18
|
-
test("
|
|
19
|
-
|
|
18
|
+
test("returns the trimmed name", () => {
|
|
19
|
+
expect(validateName(" 测试 ")).toBe("测试");
|
|
20
|
+
});
|
|
21
|
+
test("rejects shell-injection-like and path-like names with a reason", () => {
|
|
22
|
+
for (const n of [";rm -rf /", "demo bot", "-demo", "demo;rm", "demo$INJECT", "x".repeat(65), "测".repeat(65),
|
|
23
|
+
"a/b", "a\\b", "a:b", ".hidden", "..", "a\u0000b", "a`id`", "", " "]) {
|
|
20
24
|
expect(() => validateName(n)).toThrow(ValidationError);
|
|
21
25
|
}
|
|
22
26
|
});
|
|
27
|
+
test("error carries reason + offending char + a readable message", () => {
|
|
28
|
+
try { validateName("a/b"); throw new Error("no throw"); }
|
|
29
|
+
catch (e: any) {
|
|
30
|
+
expect(e).toBeInstanceOf(ValidationError);
|
|
31
|
+
expect(e.code).toBe("node_name_invalid");
|
|
32
|
+
expect(e.detail.reason).toBe("forbidden_char");
|
|
33
|
+
expect(e.detail.char).toBe("/");
|
|
34
|
+
expect(String(e.detail.message)).toContain("/");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
23
37
|
});
|
|
24
38
|
|
|
25
39
|
describe("validateRuntime / validateModel (§4.2.2)", () => {
|
|
@@ -35,6 +49,15 @@ describe("validateRuntime / validateModel (§4.2.2)", () => {
|
|
|
35
49
|
expect(() => validateModel("gpt-4o")).not.toThrow();
|
|
36
50
|
expect(() => validateModel("bad model")).toThrow(ValidationError);
|
|
37
51
|
expect(() => validateModel("x;rm")).toThrow(ValidationError);
|
|
52
|
+
// provider/model(OpenCode 共存;桌面向导 0.2.61 起发这种形状,Vincent 2026-09-08 撞到 model_invalid)
|
|
53
|
+
expect(() => validateModel("opencode/mimo-v2.5-free")).not.toThrow();
|
|
54
|
+
expect(() => validateModel("anthropic/claude-sonnet-4")).not.toThrow();
|
|
55
|
+
expect(() => validateModel("a/b/c")).toThrow(ValidationError); // 只许一个斜杠
|
|
56
|
+
expect(() => validateModel("/model")).toThrow(ValidationError); // 不能以斜杠开头
|
|
57
|
+
expect(() => validateModel("provider/")).toThrow(ValidationError); // 不能以斜杠结尾
|
|
58
|
+
expect(() => validateModel("../x")).toThrow(ValidationError); // 纯点段(路径穿越形状)
|
|
59
|
+
expect(() => validateModel("x/.")).toThrow(ValidationError);
|
|
60
|
+
expect(() => validateModel("open code/m")).toThrow(ValidationError);
|
|
38
61
|
expect(() => validateModel("")).toThrow(ValidationError);
|
|
39
62
|
});
|
|
40
63
|
});
|
|
@@ -60,6 +83,30 @@ describe("validateFlagValue (§4.2.2)", () => {
|
|
|
60
83
|
expect(() => validateFlagValue("permissionMode", "plan")).not.toThrow();
|
|
61
84
|
expect(() => validateFlagValue("permissionMode", "anything-else")).toThrow(ValidationError);
|
|
62
85
|
});
|
|
86
|
+
// #605 —— agent-node 按毫秒读 flags.timeout(默认 300000)。这里曾收 1..86400(秒的形状):
|
|
87
|
+
// 600 原样到子节点 = 0.6 秒超时;600000(10 分钟)反被拒。现在与 update_node_config 同一把尺子。
|
|
88
|
+
test("timeout is milliseconds: 0 or 1000..3_600_000 (#605)", () => {
|
|
89
|
+
for (const v of [0, 1000, 300_000, 600_000, 3_600_000]) {
|
|
90
|
+
expect(() => validateFlagValue("timeout", v)).not.toThrow();
|
|
91
|
+
}
|
|
92
|
+
for (const v of [1, 600, 999, 86_400 * 1000 + 1, 3_600_001, -1, 1500.5, "600000", null]) {
|
|
93
|
+
expect(() => validateFlagValue("timeout", v)).toThrow(ValidationError);
|
|
94
|
+
}
|
|
95
|
+
try { validateFlagValue("timeout", 600); throw new Error("unreachable"); }
|
|
96
|
+
catch (e) {
|
|
97
|
+
expect(e).toBeInstanceOf(ValidationError);
|
|
98
|
+
expect((e as ValidationError).code).toBe("flag_value_invalid");
|
|
99
|
+
expect(String((e as ValidationError).detail?.reason)).toContain("milliseconds");
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
test("timeout range matches update_node_config's (#605)", async () => {
|
|
103
|
+
const { validatePatch } = await import("./config-apply-validate.js");
|
|
104
|
+
for (const v of [0, 1, 600, 999, 1000, 600_000, 3_600_000, 3_600_001, -1, 1.5]) {
|
|
105
|
+
let createOk = true;
|
|
106
|
+
try { validateFlagValue("timeout", v); } catch { createOk = false; }
|
|
107
|
+
expect({ v, ok: createOk }).toEqual({ v, ok: validatePatch(undefined, { timeout: v }) === null });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
63
110
|
});
|
|
64
111
|
|
|
65
112
|
describe("validateEnvRefs (§4.4.7 + B1 G7/G8 sub-cases)", () => {
|
|
@@ -169,6 +216,41 @@ describe("buildAnetArgs (§4.2.2 F2 — fully validated argv)", () => {
|
|
|
169
216
|
expect(args).toContain("--permission-mode");
|
|
170
217
|
expect(args[args.indexOf("--permission-mode") + 1]).toBe("plan");
|
|
171
218
|
});
|
|
219
|
+
test("OpenCode V2 requires provider/model only after unsafe opt-in", () => {
|
|
220
|
+
let unsafe = "";
|
|
221
|
+
try {
|
|
222
|
+
buildAnetArgs({ name: "v2", runtime: "opencode-cli", flags: { opencodeGeneration: "v2" } });
|
|
223
|
+
} catch (e) { unsafe = (e as ValidationError).code; }
|
|
224
|
+
expect(unsafe).toBe("opencode_v2_requires_unsafe_opt_in");
|
|
225
|
+
let missing = "";
|
|
226
|
+
try {
|
|
227
|
+
buildAnetArgs({ name: "v2", runtime: "opencode-cli", flags: { opencodeGeneration: "v2", opencodeUnsafeTools: true } });
|
|
228
|
+
} catch (e) { missing = (e as ValidationError).code; }
|
|
229
|
+
expect(missing).toBe("opencode_v2_requires_provider_model");
|
|
230
|
+
let bare = "";
|
|
231
|
+
try {
|
|
232
|
+
buildAnetArgs({ name: "v2", runtime: "opencode-cli", model: "opencode", flags: { opencodeGeneration: "v2", opencodeUnsafeTools: true } });
|
|
233
|
+
} catch (e) { bare = (e as ValidationError).code; }
|
|
234
|
+
expect(bare).toBe("opencode_v2_requires_provider_model");
|
|
235
|
+
const args = buildAnetArgs({
|
|
236
|
+
name: "v2", runtime: "opencode-cli", model: "stub/model",
|
|
237
|
+
flags: { opencodeGeneration: "v2", opencodeUnsafeTools: true },
|
|
238
|
+
});
|
|
239
|
+
expect(args).toContain("stub/model");
|
|
240
|
+
expect(() => validateOpenCodeV2ProviderModel("opencode-cli", undefined, { opencodeGeneration: "v2" })).not.toThrow();
|
|
241
|
+
expect(() => validateOpenCodeV2ProviderModel("claude-agent-sdk", undefined, { opencodeGeneration: "v2", opencodeUnsafeTools: true })).not.toThrow();
|
|
242
|
+
expect(buildAnetArgs({ name: "x", runtime: "opencode-cli" })).toEqual(["node", "create", "x", "--runtime", "opencode-cli"]);
|
|
243
|
+
});
|
|
244
|
+
test("omitted model is allowed and does not emit --model", () => {
|
|
245
|
+
const args = buildAnetArgs({
|
|
246
|
+
name: "x", runtime: "codex-sdk",
|
|
247
|
+
flags: { permissionMode: "plan" },
|
|
248
|
+
});
|
|
249
|
+
expect(args).toEqual(["node", "create", "x", "--runtime", "codex-sdk", "--permission-mode", "plan"]);
|
|
250
|
+
});
|
|
251
|
+
test("empty model is still rejected", () => {
|
|
252
|
+
expect(() => buildAnetArgs({ name: "x", runtime: "codex-sdk", model: "" })).toThrow(ValidationError);
|
|
253
|
+
});
|
|
172
254
|
test("rejects bad name with shell metachar", () => {
|
|
173
255
|
expect(() => buildAnetArgs({ name: ";rm -rf /", runtime: "claude-agent-sdk", model: "x" })).toThrow(ValidationError);
|
|
174
256
|
});
|
|
@@ -185,3 +267,107 @@ describe("buildAnetArgs (§4.2.2 F2 — fully validated argv)", () => {
|
|
|
185
267
|
} as any)).toThrow(ValidationError);
|
|
186
268
|
});
|
|
187
269
|
});
|
|
270
|
+
|
|
271
|
+
// ─── runtime_invalid 要说清「允许哪些」和「还有什么路」 ────────────────
|
|
272
|
+
//
|
|
273
|
+
// 2026-08-28 实测:从 Dashboard 建一个 codex-app-server 节点,用户拿到的是
|
|
274
|
+
// {"ok":false,"error":"runtime_invalid","value":"codex-app-server"}
|
|
275
|
+
// 而目标机器的 daemon 日志里一行都没有 —— 请求被 hub 拦在最前面,
|
|
276
|
+
// 用户无从知道①哪些允许②是不是打错了③有没有别的办法。
|
|
277
|
+
// 🔴 2026-08-28:这一节原本拿 `codex-app-server` / `opencode-cli` 当"非法 runtime"的例子。
|
|
278
|
+
// #1298 把 hub 的 RUNTIMES 从 3 个放开到 7 个之后,那两个变成了合法值,这两条测试因此红了 ——
|
|
279
|
+
// **测试正确地抓到了行为变更**。修法是换例子,不是削弱断言:
|
|
280
|
+
// 意图仍然是「非法 runtime 要给可操作的报错」,只是例子换成永远不会合法的字符串。
|
|
281
|
+
describe("validateRuntime — 报错要可操作", () => {
|
|
282
|
+
function thrown(v: unknown): any {
|
|
283
|
+
try { validateRuntime(v); } catch (e: any) { return e; }
|
|
284
|
+
throw new Error("expected validateRuntime to throw");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
test("非法 runtime 的报错带上允许集合", () => {
|
|
288
|
+
const e = thrown("definitely-not-a-runtime");
|
|
289
|
+
expect(e.code).toBe("runtime_invalid");
|
|
290
|
+
expect(Array.isArray(e.detail?.allowed)).toBe(true);
|
|
291
|
+
// 🔴 断言它等于 RUNTIMES 本身,而不是等于一份手抄的清单 ——
|
|
292
|
+
// 手抄的那份会漂,漂了之后报错会理直气壮地告诉用户一组错的名字。
|
|
293
|
+
expect(e.detail.allowed).toEqual([...RUNTIMES]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("报错带上一条出路提示", () => {
|
|
297
|
+
const e = thrown("another-bogus-runtime");
|
|
298
|
+
expect(typeof e.detail?.hint).toBe("string");
|
|
299
|
+
expect(e.detail.hint.length).toBeGreaterThan(10);
|
|
300
|
+
// 提示必须点名那条真实存在的路,否则它只是安慰话
|
|
301
|
+
expect(e.detail.hint).toContain("anet node create");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("仍然回报用户传进来的那个值(便于识别是不是打错了)", () => {
|
|
305
|
+
const e = thrown("codex-app-servr"); // 故意少一个 e
|
|
306
|
+
expect(e.detail.value).toBe("codex-app-servr");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("🔴 合法 runtime 一个都不能被这次改动误伤", () => {
|
|
310
|
+
for (const r of RUNTIMES) {
|
|
311
|
+
expect(() => validateRuntime(r)).not.toThrow();
|
|
312
|
+
}
|
|
313
|
+
// 正控:非字符串必须仍然被拒 —— 否则上面那条循环全过也说明不了什么
|
|
314
|
+
expect(() => validateRuntime(123 as any)).toThrow();
|
|
315
|
+
expect(() => validateRuntime(undefined as any)).toThrow();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// #1298 —— daemon 远程创建放开到 7 个 runtime(Vincent 2026-08-28 定,
|
|
320
|
+
// 理由是兜底排错:节点不动了,人要能进去跟 Codex / Claude Code 对话)。
|
|
321
|
+
describe("#1298 RUNTIMES —— 七个都放行,且与 CLI 侧一致", () => {
|
|
322
|
+
test("三个共存 runtime 不再被拒", () => {
|
|
323
|
+
for (const r of ["codex-app-server", "grok-build-cli", "opencode-cli"]) {
|
|
324
|
+
expect(() => validateRuntime(r)).not.toThrow();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
test("原有四个仍然放行(放开不该顺手改坏别的)", () => {
|
|
328
|
+
for (const r of ["claude-agent-sdk", "claude-code-cli", "codex-sdk", "grok-build-acp"]) {
|
|
329
|
+
expect(() => validateRuntime(r)).not.toThrow();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
test("🔴 恰好七个 —— 多一个少一个都要有人解释", () => {
|
|
333
|
+
expect(RUNTIMES.length).toBe(7);
|
|
334
|
+
// 正控:不在名单里的仍然被拒,证明上面不是"什么都放行"
|
|
335
|
+
expect(() => validateRuntime("definitely-not-a-runtime")).toThrow();
|
|
336
|
+
expect(() => validateRuntime("")).toThrow();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// #584 —— app「Codex(TUI 共存)」经 daemon 建出来的是无头节点:create_node 没有任何字段能表达「要共存」。
|
|
341
|
+
describe("#584 flags.copresence —— 建节点时的共存开关", () => {
|
|
342
|
+
test("is a known flag key and must be boolean", () => {
|
|
343
|
+
expect(() => validateFlagValue("copresence", true)).not.toThrow();
|
|
344
|
+
expect(() => validateFlagValue("copresence", false)).not.toThrow();
|
|
345
|
+
for (const bad of ["true", 1, null, {}]) {
|
|
346
|
+
try { validateFlagValue("copresence", bad); throw new Error("did not throw"); }
|
|
347
|
+
catch (e) { expect((e as ValidationError).code).toBe("flag_value_invalid"); }
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
test("only codex-app-server accepts it — elsewhere it would be swallowed silently", () => {
|
|
351
|
+
expect(() => validateFlagsForRuntime("codex-app-server", { copresence: true })).not.toThrow();
|
|
352
|
+
expect(() => validateFlagsForRuntime("claude-agent-sdk", { permissionMode: "default" })).not.toThrow();
|
|
353
|
+
expect(() => validateFlagsForRuntime("claude-agent-sdk", undefined)).not.toThrow();
|
|
354
|
+
for (const rt of ["claude-agent-sdk", "claude-code-cli", "codex-sdk", "grok-build-acp", "grok-build-cli", "opencode-cli"]) {
|
|
355
|
+
try { validateFlagsForRuntime(rt, { copresence: true }); throw new Error(`did not throw for ${rt}`); }
|
|
356
|
+
catch (e) {
|
|
357
|
+
expect((e as ValidationError).code).toBe("flag_not_applicable_to_runtime");
|
|
358
|
+
expect((e as ValidationError).detail).toMatchObject({ field: "copresence", runtime: rt, applicable: [...COPRESENCE_FLAG_RUNTIMES] });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
test("buildAnetArgs emits the CLI's boolean `--copresence` (no value), and nothing for false", () => {
|
|
363
|
+
expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { copresence: true } }))
|
|
364
|
+
.toEqual(["node", "create", "cx", "--runtime", "codex-app-server", "--copresence"]);
|
|
365
|
+
expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { copresence: false } }))
|
|
366
|
+
.toEqual(["node", "create", "cx", "--runtime", "codex-app-server"]);
|
|
367
|
+
expect(() => buildAnetArgs({ name: "cx", runtime: "codex-sdk", flags: { copresence: true } })).toThrow(ValidationError);
|
|
368
|
+
});
|
|
369
|
+
test("the old request shape (no flag) is unchanged", () => {
|
|
370
|
+
expect(buildAnetArgs({ name: "cx", runtime: "codex-app-server", flags: { permissionMode: "default" } }))
|
|
371
|
+
.toEqual(["node", "create", "cx", "--runtime", "codex-app-server", "--permission-mode", "default"]);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
// payload (RFC §4.6).
|
|
9
9
|
|
|
10
10
|
import { isReservedEnvKey } from "./shared/reserved-env.js";
|
|
11
|
+
import { checkNodeName } from "./shared/node-name.js";
|
|
12
|
+
import { CODEX_CONFIG_ONLY_FLAG_KEYS, validateCodexConfigFlag } from "./codex-config-flags.js";
|
|
13
|
+
import { validProviderInitialization } from './shared/provider-initialization.js';
|
|
11
14
|
|
|
12
15
|
export class ValidationError extends Error {
|
|
13
16
|
constructor(public code: string, public detail?: Record<string, unknown>) {
|
|
@@ -17,10 +20,50 @@ export class ValidationError extends Error {
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
// §4.2.2 — structural enums.
|
|
20
|
-
|
|
23
|
+
// #1298 —— daemon 远程创建允许的 runtime。2026-08-28 从 3 个放开到 7 个。
|
|
24
|
+
//
|
|
25
|
+
// 放开之前挡着的问题是「daemon 建出来的是**无人值守**的后台进程,而三个共存 runtime
|
|
26
|
+
// 的本质是人和 agent 共用一个 TUI 会话 —— 建出来给谁用?」
|
|
27
|
+
//
|
|
28
|
+
// Vincent 2026-08-28 定,理由是**兜底排错**:
|
|
29
|
+
// 「某个节点突然不动了,人可以进去看。在 daemon 还没那么靠谱的情况下,
|
|
30
|
+
// 这非常重要 —— 至少可以跟 Codex 和 Claude Code 对话。」
|
|
31
|
+
// 🔴 关键在于 **attach 是「建好之后」的动作,不是「建的时候」的前提**。
|
|
32
|
+
//
|
|
33
|
+
// 支撑读数(2026-08-28 实测舰队):`通信牛` 跑 codex-app-server(共存 runtime),
|
|
34
|
+
// **764 次成功完成任务**,当天仍在跑 —— 比两个普通 runtime 的节点(61 次 / 3 次)
|
|
35
|
+
// 都多一个量级。⇒ 任务链路不需要人坐在屏幕前。
|
|
36
|
+
//
|
|
37
|
+
// 🔴 这七个名字与 `agent-network/src/normalize-runtime.ts` 的 `RuntimeName` 一致。
|
|
38
|
+
// 那边是 CLI 侧的全集;两边分叉过一次(daemon 侧的 VALID_RUNTIMES 早就是 7 个,
|
|
39
|
+
// 只有这里还是 3 个,而记录此事的 #1298 正文写的是「三道闸都关着」——
|
|
40
|
+
// **一份快照式的盘点,只在写下的那一刻是真的**)。
|
|
41
|
+
export const RUNTIMES = [
|
|
42
|
+
"claude-agent-sdk",
|
|
43
|
+
"claude-code-cli",
|
|
44
|
+
"codex-sdk",
|
|
45
|
+
"codex-app-server",
|
|
46
|
+
"grok-build-acp",
|
|
47
|
+
"grok-build-cli",
|
|
48
|
+
"opencode-cli",
|
|
49
|
+
] as const;
|
|
21
50
|
export type Runtime = typeof RUNTIMES[number];
|
|
22
51
|
|
|
23
|
-
|
|
52
|
+
// #584 —— `copresence` 是**建节点时**的开关,不是 agent 的运行参数:为 true 时 daemon 把它写成子节点
|
|
53
|
+
// config 的 `codexCopresence: true`,之后一条 `anet node start` 就起共享 TUI(app 向导的「Codex(TUI 共存)」)。
|
|
54
|
+
// 🔴 故意放在 flags 里而不是 node_spec 顶层:老 hub 的 zod 会静默丢弃未知顶层字段、老 daemon 也静默忽略
|
|
55
|
+
// —— 那正是 #584 的形状(用户选了共存,拿到无头节点,没有任何报错)。flags 的未知键在老 hub(这份
|
|
56
|
+
// FLAG_KEYS)和老 daemon(buildAnetArgsDaemon)都**拒绝**,新 app 撞到老组件得到的是一条报错。
|
|
57
|
+
export const FLAG_KEYS = [
|
|
58
|
+
"permissionMode", "dangerouslySkipPermissions", "maxTurns", "budget", "timeout",
|
|
59
|
+
"copresence", "opencodeGeneration", "opencodeUnsafeTools",
|
|
60
|
+
"approvalPolicy", "sandboxMode", "skipGitRepoCheck", "copresenceFullAccess", "modelReasoningEffort",
|
|
61
|
+
"daemonProvider",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
/** #584 —— 只有这些 runtime 认 `flags.copresence`。目前只有 codex-app-server 的共存靠 config 字段
|
|
65
|
+
* (`codexCopresence`)决定;grok-build-cli / opencode-cli 各有自己的字段,接进来之前一律拒,不静默吞。 */
|
|
66
|
+
export const COPRESENCE_FLAG_RUNTIMES = ["codex-app-server"] as const;
|
|
24
67
|
export type FlagKey = typeof FLAG_KEYS[number];
|
|
25
68
|
|
|
26
69
|
export const PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"] as const;
|
|
@@ -31,29 +74,85 @@ export const MAX_ENV_VALUE_BYTES = 16 * 1024;
|
|
|
31
74
|
const ENV_KEY_RE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
32
75
|
|
|
33
76
|
// §4.2.2 name + model.
|
|
34
|
-
|
|
35
|
-
|
|
77
|
+
// Board #652 — the name rule lives in ./shared/node-name.ts (byte-identical copy in
|
|
78
|
+
// agent-node/src/shared/, drift-tested). It used to be /^[a-z][a-z0-9_-]{0,63}$/ here.
|
|
79
|
+
// OpenCode 共存的模型带 provider 前缀(`opencode/mimo-v2.5-free`,agent-node parseModelRef 按第一个 `/` 切),
|
|
80
|
+
// 桌面向导 0.2.61 起就发这种形状 —— 允许**恰好一个**斜杠分隔的两段;每段仍是原字符集,且不能是纯点(`..`/`.`)。
|
|
81
|
+
const MODEL_SEGMENT = "[a-zA-Z0-9._:\\-]+";
|
|
82
|
+
const MODEL_RE = new RegExp(`^${MODEL_SEGMENT}(?:/${MODEL_SEGMENT})?$`);
|
|
83
|
+
const MODEL_DOT_ONLY_SEGMENT = /(^|\/)\.+(\/|$)/;
|
|
36
84
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
85
|
+
/** Board #652 — returns the normalized (trimmed) name the caller must store and forward. */
|
|
86
|
+
export function validateName(s: unknown): string {
|
|
87
|
+
const r = checkNodeName(s);
|
|
88
|
+
if (!r.ok) {
|
|
89
|
+
throw new ValidationError("node_name_invalid", {
|
|
90
|
+
value: typeof s === "string" ? s.slice(0, 80) : typeof s,
|
|
91
|
+
reason: r.error,
|
|
92
|
+
...(r.char !== undefined ? { char: r.char } : {}),
|
|
93
|
+
message: r.message,
|
|
94
|
+
});
|
|
40
95
|
}
|
|
96
|
+
return r.name;
|
|
41
97
|
}
|
|
42
98
|
|
|
43
99
|
export function validateRuntime(s: unknown): asserts s is Runtime {
|
|
44
100
|
if (typeof s !== "string" || !(RUNTIMES as readonly string[]).includes(s)) {
|
|
45
|
-
|
|
101
|
+
// 🔴 原来只回一个 `runtime_invalid` + 用户传进来的那个值 —— **不说允许哪些,
|
|
102
|
+
// 也不说还有没有别的路**。2026-08-28 实测:从 Dashboard 建一个
|
|
103
|
+
// `codex-app-server` 节点,用户拿到的就是
|
|
104
|
+
// {"ok":false,"error":"runtime_invalid","value":"codex-app-server"}
|
|
105
|
+
// 而目标机器的 daemon 日志里一行都没有(请求根本没到)。
|
|
106
|
+
// 用户无从知道:①哪些是允许的 ②这个名字是不是打错了 ③有没有别的办法。
|
|
107
|
+
//
|
|
108
|
+
// `allowed` 直接由 RUNTIMES 展开,不是手写的第二份清单 ——
|
|
109
|
+
// 🔴 手写一份会漂移,而漂移之后报错会**理直气壮地告诉用户一组错的名字**。
|
|
110
|
+
throw new ValidationError("runtime_invalid", {
|
|
111
|
+
value: typeof s === "string" ? s.slice(0, 80) : typeof s,
|
|
112
|
+
allowed: [...RUNTIMES],
|
|
113
|
+
// 这句提示的前提是查过的:`agent-network/src/normalize-runtime.ts` 的
|
|
114
|
+
// `RuntimeName` / `SUPPORTED_RUNTIME_NAMES` 是 7 个,包含此处不放行的那几个,
|
|
115
|
+
// 所以「在目标机器上直接建」确实是一条真实存在的路,不是安慰话。
|
|
116
|
+
hint: "daemon 远程创建目前只放行以上 runtime;其余 runtime 可以在目标机器上直接 `anet node create --runtime <name>` 创建。",
|
|
117
|
+
});
|
|
46
118
|
}
|
|
47
119
|
}
|
|
48
120
|
|
|
49
|
-
export function validateModel(s: unknown): asserts s is string {
|
|
50
|
-
if (
|
|
121
|
+
export function validateModel(s: unknown): asserts s is string | undefined | null {
|
|
122
|
+
if (s === undefined || s === null) return;
|
|
123
|
+
if (typeof s !== "string" || s.length === 0 || s.length > 100 || !MODEL_RE.test(s) || MODEL_DOT_ONLY_SEGMENT.test(s)) {
|
|
51
124
|
throw new ValidationError("model_invalid", { value: typeof s === "string" ? s.slice(0, 80) : typeof s });
|
|
52
125
|
}
|
|
53
126
|
}
|
|
54
127
|
|
|
128
|
+
/**
|
|
129
|
+
* #605 —— `flags.timeout` 的单位是**毫秒**:agent-node 就是这么读的(cli.ts currentClaudeTimeoutMs /
|
|
130
|
+
* currentCodexTimeoutMs → util/timeout.ts resolveTimeoutMs、runtime/opencode-timeout.ts,默认 300000)。
|
|
131
|
+
* 这里曾按「秒」收 1..86400:填 600 想要 10 分钟的人拿到 0.6 秒、每个任务都超时;真正的毫秒值被拒。
|
|
132
|
+
*
|
|
133
|
+
* 范围与 update_node_config(config-apply-validate.ts)/ agent-node config-apply 同一把尺子:
|
|
134
|
+
* 0(不设上限)或 TIMEOUT_MS_MIN..TIMEOUT_MS_MAX。1..999 拒绝 —— 旧客户端按秒发的小值分辨不出意图,
|
|
135
|
+
* 不偷偷乘 1000,但也不再让任何人悄悄拿到一个亚秒超时。
|
|
136
|
+
*/
|
|
137
|
+
export const TIMEOUT_MS_MIN = 1000;
|
|
138
|
+
export const TIMEOUT_MS_MAX = 3_600_000;
|
|
139
|
+
export const TIMEOUT_MS_REASON =
|
|
140
|
+
`timeout is in milliseconds: 0 (no limit) or an integer ${TIMEOUT_MS_MIN}..${TIMEOUT_MS_MAX} (e.g. 600000 = 10 min)`;
|
|
141
|
+
export function isValidTimeoutMs(v: unknown): v is number {
|
|
142
|
+
return typeof v === "number" && Number.isInteger(v) && (v === 0 || (v >= TIMEOUT_MS_MIN && v <= TIMEOUT_MS_MAX));
|
|
143
|
+
}
|
|
144
|
+
|
|
55
145
|
export function validateFlagValue(k: string, v: unknown): void {
|
|
56
146
|
switch (k) {
|
|
147
|
+
case 'daemonProvider':
|
|
148
|
+
if (!validProviderInitialization(v)) throw new ValidationError('flag_value_invalid', { field: k });
|
|
149
|
+
return;
|
|
150
|
+
case "opencodeGeneration":
|
|
151
|
+
if (v !== "v1" && v !== "v2") throw new ValidationError("flag_value_invalid", { field: k, reason: "must be v1 or v2" });
|
|
152
|
+
return;
|
|
153
|
+
case "opencodeUnsafeTools":
|
|
154
|
+
if (typeof v !== "boolean") throw new ValidationError("flag_value_invalid", { field: k, reason: "must be boolean" });
|
|
155
|
+
return;
|
|
57
156
|
case "permissionMode":
|
|
58
157
|
if (typeof v !== "string" || !(PERMISSION_MODES as readonly string[]).includes(v)) {
|
|
59
158
|
throw new ValidationError("flag_value_invalid", { field: k, reason: "must be one of default/acceptEdits/plan/bypassPermissions" });
|
|
@@ -73,15 +172,82 @@ export function validateFlagValue(k: string, v: unknown): void {
|
|
|
73
172
|
}
|
|
74
173
|
return;
|
|
75
174
|
case "timeout":
|
|
76
|
-
if (!
|
|
77
|
-
throw new ValidationError("flag_value_invalid", { field: k, reason:
|
|
175
|
+
if (!isValidTimeoutMs(v)) {
|
|
176
|
+
throw new ValidationError("flag_value_invalid", { field: k, reason: TIMEOUT_MS_REASON });
|
|
78
177
|
}
|
|
79
178
|
return;
|
|
179
|
+
case "copresence":
|
|
180
|
+
if (typeof v !== "boolean") throw new ValidationError("flag_value_invalid", { field: k, reason: "must be boolean" });
|
|
181
|
+
return;
|
|
80
182
|
default:
|
|
183
|
+
if (CODEX_CONFIG_ONLY_FLAG_KEYS.has(k)) {
|
|
184
|
+
const fail = validateCodexConfigFlag(k, v, null);
|
|
185
|
+
if (fail) throw new ValidationError("flag_value_invalid", { field: k, reason: fail.reason });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
81
188
|
throw new ValidationError("flag_key_unknown", { field: k });
|
|
82
189
|
}
|
|
83
190
|
}
|
|
84
191
|
|
|
192
|
+
/** #584 —— 逐键校验之外的跨字段一条:`copresence` 只对 COPRESENCE_FLAG_RUNTIMES 有意义。
|
|
193
|
+
* 在别的 runtime 上收下它而什么都不做,就是又一个「选了共存、拿到无头」。 */
|
|
194
|
+
export function validateFlagsForRuntime(runtime: string, flags: Record<string, unknown> | undefined | null): void {
|
|
195
|
+
if (!flags) return;
|
|
196
|
+
if (flags.daemonProvider !== undefined && (runtime !== 'codex-app-server' || flags.copresence !== true)) {
|
|
197
|
+
throw new ValidationError('flag_not_applicable_to_runtime', { field: 'daemonProvider', runtime, applicable: ['codex-app-server copresence'] });
|
|
198
|
+
}
|
|
199
|
+
// Strict flags envelope: old Hub/daemon reject these keys rather than dropping
|
|
200
|
+
// unknown node_spec fields. Opt-in must be explicit on this create request.
|
|
201
|
+
for (const field of ["opencodeGeneration", "opencodeUnsafeTools"]) {
|
|
202
|
+
if (!Object.prototype.hasOwnProperty.call(flags, field)) continue;
|
|
203
|
+
validateFlagValue(field, flags[field]);
|
|
204
|
+
if (runtime !== "opencode-cli") throw new ValidationError("flag_not_applicable_to_runtime", { field, runtime, applicable: ["opencode-cli"] });
|
|
205
|
+
}
|
|
206
|
+
if (Object.prototype.hasOwnProperty.call(flags, "opencodeUnsafeTools") && flags.opencodeGeneration !== "v2") {
|
|
207
|
+
throw new ValidationError("opencode_unsafe_requires_v2", { reason: "opencodeUnsafeTools requires explicit opencodeGeneration=v2" });
|
|
208
|
+
}
|
|
209
|
+
if (flags.opencodeGeneration === "v2" && flags.opencodeUnsafeTools !== true) {
|
|
210
|
+
throw new ValidationError("opencode_v2_requires_unsafe_opt_in", { reason: "V2 preview enables every local tool; trusted tasks only. Explicit opencodeUnsafeTools=true required." });
|
|
211
|
+
}
|
|
212
|
+
if (Object.prototype.hasOwnProperty.call(flags, "copresence") && !(COPRESENCE_FLAG_RUNTIMES as readonly string[]).includes(runtime)) {
|
|
213
|
+
throw new ValidationError("flag_not_applicable_to_runtime", {
|
|
214
|
+
field: "copresence", runtime, applicable: [...COPRESENCE_FLAG_RUNTIMES],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
218
|
+
if (!CODEX_CONFIG_ONLY_FLAG_KEYS.has(k)) continue;
|
|
219
|
+
const fail = validateCodexConfigFlag(k, v, runtime);
|
|
220
|
+
if (fail) throw new ValidationError("flag_value_invalid", { field: k, reason: fail.reason });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* V2 co-presence start requires OpenCode's own `provider/model`.
|
|
226
|
+
* Call only after the unsafe-opt-in check: a missing opt-in stays
|
|
227
|
+
* `opencode_v2_requires_unsafe_opt_in`. This API does not take an anet
|
|
228
|
+
* provider preset or a vault key (#2565 owns presets).
|
|
229
|
+
*/
|
|
230
|
+
export function validateOpenCodeV2ProviderModel(
|
|
231
|
+
runtime: string,
|
|
232
|
+
model: unknown,
|
|
233
|
+
flags: Record<string, unknown> | undefined | null,
|
|
234
|
+
): void {
|
|
235
|
+
if (runtime !== "opencode-cli" || !flags || typeof flags !== "object") return;
|
|
236
|
+
if (flags.opencodeGeneration !== "v2" || flags.opencodeUnsafeTools !== true) return;
|
|
237
|
+
const native = typeof model === "string"
|
|
238
|
+
&& model.length > 0
|
|
239
|
+
&& model.length <= 100
|
|
240
|
+
&& MODEL_RE.test(model)
|
|
241
|
+
&& !MODEL_DOT_ONLY_SEGMENT.test(model)
|
|
242
|
+
&& model.includes("/")
|
|
243
|
+
&& model.indexOf("/") === model.lastIndexOf("/");
|
|
244
|
+
if (!native) {
|
|
245
|
+
throw new ValidationError("opencode_v2_requires_provider_model", {
|
|
246
|
+
reason: "OpenCode V2 uses the installed OpenCode provider/model (exactly one slash, for example provider/model). This API does not take an anet provider preset.",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
85
251
|
// §4.4.7 — env_refs 7-step gate. networkSecretsGet is injected so
|
|
86
252
|
// callers can supply either a real DB lookup (hub) or a fixture (tests).
|
|
87
253
|
// The 6th step also reports the resolved value size; we return the
|
|
@@ -144,7 +310,7 @@ export function validateEnvRefs(
|
|
|
144
310
|
return out;
|
|
145
311
|
}
|
|
146
312
|
|
|
147
|
-
// §4.4.7 — safe serializer for .env.
|
|
313
|
+
// §4.4.7 — safe serializer for the node secrets.env file. Dotenv-quoted with escape
|
|
148
314
|
// for backslash / double-quote / newline / carriage-return so that a
|
|
149
315
|
// value containing `\n"evil=KEY2"` cannot pollute the next line.
|
|
150
316
|
export function serializeEnvLocal(env: Record<string, string>): string {
|
|
@@ -158,6 +324,40 @@ export function serializeEnvLocal(env: Record<string, string>): string {
|
|
|
158
324
|
}).join("\n") + "\n";
|
|
159
325
|
}
|
|
160
326
|
|
|
327
|
+
// app「新建节点」工作目录 —— hub 只判**形状**(daemon 才知道那台机器的 $HOME、系统目录、
|
|
328
|
+
// 目录里住没住别的节点,那几条在 agent-node/src/runtime/child-workdir.ts)。
|
|
329
|
+
// 这里挡的是「根本不像一条路径」的输入,让它在派发前就失败,而不是占一个请求行再被 daemon 拒。
|
|
330
|
+
export const MAX_WORKDIR_LEN = 1024;
|
|
331
|
+
export function validateWorkdir(s: unknown): asserts s is string | undefined | null {
|
|
332
|
+
if (s === undefined || s === null) return;
|
|
333
|
+
if (typeof s !== "string") throw new ValidationError("workdir_invalid", { reason: "must be string" });
|
|
334
|
+
const v = s.trim();
|
|
335
|
+
if (v.length === 0 || v.length > MAX_WORKDIR_LEN) {
|
|
336
|
+
throw new ValidationError("workdir_invalid", { reason: `length 1..${MAX_WORKDIR_LEN}` });
|
|
337
|
+
}
|
|
338
|
+
if (/[\u0000-\u001f\u007f]/.test(v)) throw new ValidationError("workdir_invalid", { reason: "control character" });
|
|
339
|
+
const shapeOk = v.startsWith("/") || v === "~" || v.startsWith("~/") || /^[A-Za-z]:[\\/]/.test(v);
|
|
340
|
+
if (!shapeOk) {
|
|
341
|
+
throw new ValidationError("workdir_invalid", { reason: "must be absolute (/…, ~/…, or C:\\…)", value: v.slice(0, 80) });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** daemon 自报的默认工作目录根(`daemon_capabilities.default_workdir_root`),消毒后返回;
|
|
346
|
+
* 没报 / 形状不对 = null。**非 null 即表示该 daemon 认 `node_spec.workdir`** ——
|
|
347
|
+
* create_node 的「老 daemon 不收 workdir」门和 /api/host-supervisors 的透出共用这一个判据。 */
|
|
348
|
+
export function daemonDefaultWorkdirRoot(configSnapshot: unknown): string | null {
|
|
349
|
+
try {
|
|
350
|
+
const snap = typeof configSnapshot === "string" ? JSON.parse(configSnapshot) : configSnapshot;
|
|
351
|
+
const v = (snap as any)?.daemon_capabilities?.default_workdir_root;
|
|
352
|
+
if (typeof v !== "string" || v.length === 0 || v.length > MAX_WORKDIR_LEN) return null;
|
|
353
|
+
if (/[\u0000-\u001f\u007f]/.test(v)) return null;
|
|
354
|
+
if (!(v.startsWith("/") || /^[A-Za-z]:[\\/]/.test(v))) return null;
|
|
355
|
+
return v;
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
161
361
|
// §4.2.5 — channels fail-closed in P1.
|
|
162
362
|
export function validateChannelsP1(channels: unknown): void {
|
|
163
363
|
if (channels === undefined || channels === null) return;
|
|
@@ -173,7 +373,7 @@ export function validateChannelsP1(channels: unknown): void {
|
|
|
173
373
|
export interface NodeSpec {
|
|
174
374
|
name: string;
|
|
175
375
|
runtime: Runtime;
|
|
176
|
-
model
|
|
376
|
+
model?: string | null;
|
|
177
377
|
flags?: Record<string, unknown>;
|
|
178
378
|
channels?: unknown;
|
|
179
379
|
}
|
|
@@ -187,20 +387,32 @@ function kebab(k: string): string {
|
|
|
187
387
|
// shell, no string concat). Each value has already passed type/enum
|
|
188
388
|
// validation, so String() coercion is safe.
|
|
189
389
|
export function buildAnetArgs(spec: NodeSpec): string[] {
|
|
390
|
+
for (const field of ["opencodeGeneration", "opencodeUnsafeTools"]) {
|
|
391
|
+
if (Object.prototype.hasOwnProperty.call(spec, field)) throw new ValidationError("flag_location_invalid", { field, reason: "use node_spec.flags" });
|
|
392
|
+
}
|
|
190
393
|
validateName(spec.name);
|
|
191
394
|
validateRuntime(spec.runtime);
|
|
192
395
|
validateModel(spec.model);
|
|
193
396
|
validateChannelsP1(spec.channels);
|
|
397
|
+
if (spec.flags?.daemonProvider !== undefined && (!validProviderInitialization(spec.flags.daemonProvider)
|
|
398
|
+
|| spec.model !== spec.flags.daemonProvider.model)) throw new ValidationError('provider_create_model_mismatch');
|
|
194
399
|
|
|
195
|
-
const args: string[] = ["node", "create", spec.name, "--runtime", spec.runtime
|
|
400
|
+
const args: string[] = ["node", "create", spec.name, "--runtime", spec.runtime];
|
|
401
|
+
if (spec.model) args.push("--model", spec.model);
|
|
196
402
|
if (spec.flags && typeof spec.flags === "object") {
|
|
197
403
|
for (const [k, v] of Object.entries(spec.flags)) {
|
|
198
404
|
if (!(FLAG_KEYS as readonly string[]).includes(k)) {
|
|
199
405
|
throw new ValidationError("flag_key_unknown", { field: k });
|
|
200
406
|
}
|
|
201
407
|
validateFlagValue(k, v);
|
|
408
|
+
// `anet node create --copresence` 是布尔开关,不吃值。
|
|
409
|
+
if (k === "copresence") { if (v === true) args.push("--copresence"); continue; }
|
|
410
|
+
if (k === "opencodeUnsafeTools") { if (v === true) args.push("--opencode-unsafe-tools"); continue; }
|
|
411
|
+
if (CODEX_CONFIG_ONLY_FLAG_KEYS.has(k) || k === 'daemonProvider') continue;
|
|
202
412
|
args.push(`--${kebab(k)}`, String(v));
|
|
203
413
|
}
|
|
414
|
+
validateFlagsForRuntime(spec.runtime, spec.flags);
|
|
204
415
|
}
|
|
416
|
+
validateOpenCodeV2ProviderModel(spec.runtime, spec.model, spec.flags);
|
|
205
417
|
return args;
|
|
206
418
|
}
|