@sleep2agi/commhub-server 0.9.0-preview.12 → 0.9.0-preview.121
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 +92 -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/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-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 +217 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +77 -23
- package/src/daemon-provider-snapshots.test.ts +256 -0
- package/src/daemon-provider-snapshots.ts +226 -0
- package/src/daemon-token-owner-http.test.ts +231 -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 +903 -35
- 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-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2356
- package/src/lifecycle-guard.ts +1 -1
- package/src/mcp-context-cost-http.test.ts +120 -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 +429 -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 +714 -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 +283 -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 +36 -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/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 +129 -0
- package/src/rest-identity.ts +125 -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/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 +5253 -0
- package/src/session-task-on-dispatch.ts +14 -0
- package/src/shared/codex-inventory.ts +34 -0
- package/src/shared/daemon-provider-snapshot.ts +49 -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 +24 -0
- package/src/shared/reserved-env-drift.test.ts +10 -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 +469 -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 +91 -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 +141 -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-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 +222 -0
- package/src/tool-audience.ts +116 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +3598 -313
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.test.ts +66 -0
- package/src/uploads.ts +138 -5
- 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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Board #668. A dispatch preview is what the hub shows before the node
|
|
2
|
+
// reports. Once the session is already working, that text is the task in
|
|
3
|
+
// flight — a newer message waits in the inbox and must not replace it.
|
|
4
|
+
// Idle and every other status still take the preview.
|
|
5
|
+
|
|
6
|
+
const PLACEHOLDER = /^\?\d+$/;
|
|
7
|
+
|
|
8
|
+
/** SQL expression for `sessions.task` on dispatch. `placeholder` is a `?N` bind. */
|
|
9
|
+
export function sessionTaskOnDispatch(placeholder: string): string {
|
|
10
|
+
if (!PLACEHOLDER.test(placeholder)) {
|
|
11
|
+
throw new Error("sessionTaskOnDispatch placeholder must be a ?N bind");
|
|
12
|
+
}
|
|
13
|
+
return `CASE WHEN status = 'working' THEN task ELSE ${placeholder} END`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// #906 key-free existing Codex inventory. Identical public contract in each package.
|
|
2
|
+
export function validCodexInventory(v: any): boolean {
|
|
3
|
+
const fields = (o: any, allowed: string[]) => o && typeof o === 'object' && !Array.isArray(o)
|
|
4
|
+
&& Object.keys(o).every(k => allowed.includes(k));
|
|
5
|
+
const id = (s: any) => typeof s === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(s);
|
|
6
|
+
const nullable = (s: any) => s === null || id(s);
|
|
7
|
+
if (!fields(v, ['observed_at', 'scope', 'rows', 'installation']) || !Number.isSafeInteger(v.observed_at) || v.observed_at <= 0
|
|
8
|
+
|| v.scope !== 'hub_bound_nodes' || !Array.isArray(v.rows) || v.rows.length > 200
|
|
9
|
+
|| new Set(v.rows.map((r: any) => r?.node_id)).size !== v.rows.length) return false;
|
|
10
|
+
if (v.installation !== undefined && (!fields(v.installation, ['status', 'version'])
|
|
11
|
+
|| !['found', 'missing', 'unknown'].includes(v.installation.status)
|
|
12
|
+
|| !(v.installation.version === null || (typeof v.installation.version === 'string' && /^\d+\.\d+\.\d+(?:[-+.][0-9A-Za-z.-]+)?$/.test(v.installation.version) && v.installation.version.length <= 64)))) return false;
|
|
13
|
+
for (const r of v.rows) {
|
|
14
|
+
if (!r || !id(r.node_id) || typeof r.alias !== 'string' || !/^[\p{L}\p{N}][\p{L}\p{N}_.-]{0,99}$/u.test(r.alias)) return false;
|
|
15
|
+
if (['unavailable', 'not_codex'].includes(r.status)) {
|
|
16
|
+
if (!fields(r, ['node_id', 'alias', 'status'])) return false;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (r.status !== 'observed' || !fields(r, ['node_id', 'alias', 'status', 'runtime', 'home_ref', 'home_source',
|
|
20
|
+
'config_status', 'configured_provider', 'configured_model', 'provider_ids', 'auth_kind', 'credential_status',
|
|
21
|
+
'account_fingerprint', 'verification', 'effective_state', 'node_configured_model', 'config_revision'])) return false;
|
|
22
|
+
if (r.config_revision !== undefined && r.config_revision !== null && (typeof r.config_revision !== 'string' || !/^[a-f0-9]{64}$/.test(r.config_revision))) return false;
|
|
23
|
+
if (!nullable(r.runtime) || !/^[a-f0-9]{16}$/.test(r.home_ref)
|
|
24
|
+
|| !['host_default', 'config.codexHome', 'node-codex-home', 'config.env'].includes(r.home_source)
|
|
25
|
+
|| !['read', 'missing', 'unreadable'].includes(r.config_status) || !nullable(r.configured_provider)
|
|
26
|
+
|| !nullable(r.configured_model) || !nullable(r.node_configured_model)
|
|
27
|
+
|| !Array.isArray(r.provider_ids) || r.provider_ids.length > 101 || !r.provider_ids.every(id)
|
|
28
|
+
|| !['unknown', 'chatgpt', 'api_key'].includes(r.auth_kind)
|
|
29
|
+
|| !['unknown', 'present', 'unreadable'].includes(r.credential_status)
|
|
30
|
+
|| !(r.account_fingerprint === null || /^[a-f0-9]{16}$/.test(r.account_fingerprint))
|
|
31
|
+
|| r.verification !== 'not_checked' || r.effective_state !== 'not_checked') return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// #906 public Provider snapshot contract. Mirrored across separately published
|
|
2
|
+
// server/agent-node packages; drift is tested. No secret-shaped extension fields.
|
|
3
|
+
import { validCodexInventory } from './codex-inventory.js';
|
|
4
|
+
export function validateDaemonProviderSnapshot(value: unknown, networkId: string, daemonId: string): unknown {
|
|
5
|
+
const fail = (): never => { throw new Error("invalid_provider_snapshot"); };
|
|
6
|
+
const obj = (v: any, fields: string[]) => {
|
|
7
|
+
if (!v || typeof v !== "object" || Array.isArray(v) || Object.keys(v).some(k => !fields.includes(k))) fail();
|
|
8
|
+
return v;
|
|
9
|
+
};
|
|
10
|
+
const id = (v: unknown) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(v);
|
|
11
|
+
const unique = (xs: any[], key: string) => { if (new Set(xs.map(x => x[key])).size !== xs.length) fail(); };
|
|
12
|
+
const arr = (v: any, max: number, empty = false): any[] => {
|
|
13
|
+
if (!Array.isArray(v) || (!empty && !v.length) || v.length > max) fail();
|
|
14
|
+
return v;
|
|
15
|
+
};
|
|
16
|
+
const s = obj(value, ["network_id", "daemon_node_id", "revision", "source", "providers", "codex_inventory"]);
|
|
17
|
+
if (s.codex_inventory !== undefined && !validCodexInventory(s.codex_inventory)) fail();
|
|
18
|
+
if (s.network_id !== networkId || s.daemon_node_id !== daemonId || s.source !== "daemon"
|
|
19
|
+
|| !Number.isSafeInteger(s.revision) || s.revision < 0) fail();
|
|
20
|
+
const providers = arr(s.providers, 100, true);
|
|
21
|
+
unique(providers, "id");
|
|
22
|
+
for (const p of providers) {
|
|
23
|
+
obj(p, ["id", "label", "enabled", "runtimes"]);
|
|
24
|
+
if (!id(p.id) || typeof p.label !== "string" || !p.label.trim() || p.label.length > 100
|
|
25
|
+
|| /[\x00-\x1f\x7f]/.test(p.label) || typeof p.enabled !== "boolean") fail();
|
|
26
|
+
const runtimes = arr(p.runtimes, 8); unique(runtimes, "runtime");
|
|
27
|
+
for (const r of runtimes) {
|
|
28
|
+
obj(r, ["runtime", "auth"]); if (r.runtime !== "codex-tui") fail();
|
|
29
|
+
const auths = arr(r.auth, 32); unique(auths, "id");
|
|
30
|
+
for (const a of auths) {
|
|
31
|
+
obj(a, ["id", "kind", "models", "baseUrl", "credential_present", "verification", "application"]);
|
|
32
|
+
if (!id(a.id) || a.verification !== "not_checked" || a.application !== "not_applied") fail();
|
|
33
|
+
const models = arr(a.models, 200);
|
|
34
|
+
if (new Set(models).size !== models.length || models.some(m => typeof m !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(m))) fail();
|
|
35
|
+
if (a.kind === "chatgpt") {
|
|
36
|
+
if (p.id !== "openai" || a.baseUrl !== undefined || a.credential_present !== false) fail();
|
|
37
|
+
} else if (a.kind === "api_key") {
|
|
38
|
+
if (a.credential_present !== true || typeof a.baseUrl !== "string" || a.baseUrl.length > 2048) fail();
|
|
39
|
+
let u: URL; try { u = new URL(a.baseUrl); } catch { return fail(); }
|
|
40
|
+
if (u.protocol !== "https:" || u.username || u.password || u.search || u.hash) fail();
|
|
41
|
+
} else fail();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// JSON roundtrip detaches the validated object from mutable caller references.
|
|
46
|
+
const encoded = JSON.stringify(s);
|
|
47
|
+
if (new TextEncoder().encode(encoded).byteLength > 1024 * 1024) fail();
|
|
48
|
+
return JSON.parse(encoded);
|
|
49
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const FIELD_BOUNDS = [
|
|
2
|
+
[0, 59],
|
|
3
|
+
[0, 23],
|
|
4
|
+
[1, 31],
|
|
5
|
+
[1, 12],
|
|
6
|
+
[0, 7],
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
function integer(text: string, min: number, max: number): number {
|
|
10
|
+
if (!/^\d+$/.test(text)) throw new Error("invalid_cron");
|
|
11
|
+
const value = Number(text);
|
|
12
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error("invalid_cron");
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateAtom(atom: string, min: number, max: number): void {
|
|
17
|
+
const [base, step, extra] = atom.split("/");
|
|
18
|
+
if (extra !== undefined || !base) throw new Error("invalid_cron");
|
|
19
|
+
if (step !== undefined) integer(step, 1, max - min + 1);
|
|
20
|
+
if (base === "*") return;
|
|
21
|
+
const range = base.split("-");
|
|
22
|
+
if (range.length === 1) {
|
|
23
|
+
integer(range[0], min, max);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (range.length !== 2) throw new Error("invalid_cron");
|
|
27
|
+
const start = integer(range[0], min, max);
|
|
28
|
+
const end = integer(range[1], min, max);
|
|
29
|
+
if (start > end) throw new Error("invalid_cron");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Strict five-field cron timing. It deliberately excludes commands, names,
|
|
33
|
+
* @aliases, a sixth seconds field and every control character. */
|
|
34
|
+
export function parseManagedCronExpression(raw: unknown): string {
|
|
35
|
+
if (typeof raw !== "string" || raw.length < 5 || raw.length > 120) throw new Error("invalid_cron");
|
|
36
|
+
if (/[^\x20-\x7e]/.test(raw) || /[\r\n]/.test(raw)) throw new Error("invalid_cron");
|
|
37
|
+
const fields = raw.trim().split(/ +/);
|
|
38
|
+
if (fields.length !== 5) throw new Error("invalid_cron");
|
|
39
|
+
fields.forEach((field, index) => {
|
|
40
|
+
if (!field || !/^[0-9*/,-]+$/.test(field)) throw new Error("invalid_cron");
|
|
41
|
+
for (const atom of field.split(",")) validateAtom(atom, FIELD_BOUNDS[index][0], FIELD_BOUNDS[index][1]);
|
|
42
|
+
});
|
|
43
|
+
return fields.join(" ");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ExternalSchedulePatch = { enabled?: boolean; cron?: string };
|
|
47
|
+
|
|
48
|
+
export function parseExternalSchedulePatch(raw: unknown): ExternalSchedulePatch {
|
|
49
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid_patch");
|
|
50
|
+
const obj = raw as Record<string, unknown>;
|
|
51
|
+
const keys = Object.keys(obj);
|
|
52
|
+
if (keys.length === 0 || keys.some((key) => key !== "enabled" && key !== "cron")) throw new Error("invalid_patch");
|
|
53
|
+
const patch: ExternalSchedulePatch = {};
|
|
54
|
+
if (obj.enabled !== undefined) {
|
|
55
|
+
if (typeof obj.enabled !== "boolean") throw new Error("invalid_enabled");
|
|
56
|
+
patch.enabled = obj.enabled;
|
|
57
|
+
}
|
|
58
|
+
if (obj.cron !== undefined) patch.cron = parseManagedCronExpression(obj.cron);
|
|
59
|
+
return patch;
|
|
60
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
NODE_FOLDER_RE, NODE_NAME_CASES, LEGACY_NODE_NAME_RE,
|
|
6
|
+
checkNodeName, nodeDirNameFor, nodeFolderSlug, fnv1aHex,
|
|
7
|
+
} from "./node-name.js";
|
|
8
|
+
|
|
9
|
+
// Board #652 — one name rule for hub + daemon (+ the app's pinned copy).
|
|
10
|
+
|
|
11
|
+
describe("#652 checkNodeName — shared vectors", () => {
|
|
12
|
+
for (const c of NODE_NAME_CASES) {
|
|
13
|
+
test(`${JSON.stringify(c.input).slice(0, 24)} → ${c.ok ? "ok" : c.error}`, () => {
|
|
14
|
+
const r = checkNodeName(c.input);
|
|
15
|
+
expect(r.ok).toBe(c.ok);
|
|
16
|
+
if (!r.ok) {
|
|
17
|
+
expect(r.error).toBe(c.error!);
|
|
18
|
+
expect(r.message.length).toBeGreaterThan(0);
|
|
19
|
+
}
|
|
20
|
+
if (c.slug !== undefined) expect(nodeFolderSlug(c.input)).toBe(c.slug);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("the vector table really contains Chinese names that pass (old rule would refuse them)", () => {
|
|
25
|
+
const cjkOk = NODE_NAME_CASES.filter(c => c.ok && /\p{Script=Han}/u.test(c.input));
|
|
26
|
+
expect(cjkOk.length).toBeGreaterThan(0);
|
|
27
|
+
for (const c of cjkOk) expect(LEGACY_NODE_NAME_RE.test(c.input)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("#652 folder slug", () => {
|
|
32
|
+
test("every slug — incl. every accepted vector — matches the ASCII folder rule", () => {
|
|
33
|
+
for (const c of NODE_NAME_CASES.filter(x => x.ok)) {
|
|
34
|
+
const s = nodeFolderSlug(c.input);
|
|
35
|
+
expect(NODE_FOLDER_RE.test(s)).toBe(true);
|
|
36
|
+
expect(/^[\x21-\x7e]+$/.test(s)).toBe(true);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
test("non-ASCII → node-<6 hex>, deterministic", () => {
|
|
40
|
+
expect(nodeFolderSlug("测试")).toMatch(/^node-[0-9a-f]{6}$/);
|
|
41
|
+
expect(nodeFolderSlug("测试")).toBe(nodeFolderSlug(" 测试 "));
|
|
42
|
+
expect(nodeFolderSlug("测试")).not.toBe(nodeFolderSlug("测验"));
|
|
43
|
+
});
|
|
44
|
+
test("fnv1a known answers (pins the app reimplementation)", () => {
|
|
45
|
+
expect(fnv1aHex("")).toBe("811c9dc5");
|
|
46
|
+
expect(fnv1aHex("a")).toBe("e40c292c");
|
|
47
|
+
expect(fnv1aHex("foobar")).toBe("bf9cf968");
|
|
48
|
+
});
|
|
49
|
+
test("nodeDirNameFor keeps legacy names as-is (existing nodes never move)", () => {
|
|
50
|
+
for (const n of ["a", "demo-bot", "node_1", "my_agent"]) expect(nodeDirNameFor(n)).toBe(n);
|
|
51
|
+
expect(nodeDirNameFor("测试")).toBe(nodeFolderSlug("测试"));
|
|
52
|
+
expect(nodeDirNameFor("MyBot")).toBe("mybot");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("#652 hub/daemon node-name drift guard", () => {
|
|
57
|
+
test("byte-identical source files", () => {
|
|
58
|
+
const hub = readFileSync(join(import.meta.dir, "node-name.ts"), "utf-8");
|
|
59
|
+
const daemon = readFileSync(join(import.meta.dir, "..", "..", "..", "agent-node", "src", "shared", "node-name.ts"), "utf-8");
|
|
60
|
+
expect(daemon).toBe(hub);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Board #652 — node name (= network alias) rules and the ASCII folder slug derived from it.
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for hub + daemon. This file is byte-identical in
|
|
4
|
+
// server/src/shared/node-name.ts and agent-node/src/shared/node-name.ts
|
|
5
|
+
// (node-name-drift.test.ts fails the build on any difference), and the app
|
|
6
|
+
// (sleep2agi/agent-network-app) keeps a copy pinned to NODE_NAME_CASES below.
|
|
7
|
+
//
|
|
8
|
+
// Before #652 the name had to match /^[a-z][a-z0-9_-]{0,63}$/, so the app
|
|
9
|
+
// wizard refused 「测试」 while the real fleet runs Chinese aliases started by
|
|
10
|
+
// hand. House rule: the alias may be Chinese, the working directory may not.
|
|
11
|
+
// So the name is now any Unicode letters/digits, and every directory derived
|
|
12
|
+
// from it uses nodeFolderSlug() (or the legacy name, for names the old rule
|
|
13
|
+
// already accepted — see nodeDirNameFor).
|
|
14
|
+
|
|
15
|
+
/** The old rule. Every name it accepts keeps its old directory (nodeDirNameFor). */
|
|
16
|
+
export const LEGACY_NODE_NAME_RE = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
17
|
+
|
|
18
|
+
/** Folder / slug rule: what a working directory or `.anet/nodes/<dir>` may be called. */
|
|
19
|
+
export const NODE_FOLDER_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
20
|
+
|
|
21
|
+
export const NODE_NAME_MAX_CHARS = 64;
|
|
22
|
+
|
|
23
|
+
// First char: a letter, a digit or `_` (never `-`: `anet node start -x` reads as a flag;
|
|
24
|
+
// never `.`: hidden files, `..`). Rest: letters, digits, combining marks (Devanagari,
|
|
25
|
+
// Thai… need them), `_`, `-`. Nothing else — no whitespace, `/ \ :`, `.`, quotes,
|
|
26
|
+
// `$`, backticks, control characters: the name ends up in argv, tmux session names
|
|
27
|
+
// (tmux rewrites `.` and `:`), JSON, logs and file names in the trash directory.
|
|
28
|
+
const NODE_NAME_RE = /^[\p{L}\p{N}_][\p{L}\p{M}\p{N}_-]*$/u;
|
|
29
|
+
|
|
30
|
+
export type NodeNameError =
|
|
31
|
+
| "empty"
|
|
32
|
+
| "too_long"
|
|
33
|
+
| "leading_dash"
|
|
34
|
+
| "forbidden_char"
|
|
35
|
+
| "not_nfc";
|
|
36
|
+
|
|
37
|
+
export type NodeNameCheck =
|
|
38
|
+
| { ok: true; name: string }
|
|
39
|
+
| { ok: false; error: NodeNameError; char?: string; message: string };
|
|
40
|
+
|
|
41
|
+
/** Trim + NFC. Callers store the returned `name`, not the raw input. */
|
|
42
|
+
export function normalizeNodeName(raw: string): string {
|
|
43
|
+
return raw.trim().normalize("NFC");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function describeChar(ch: string): string {
|
|
47
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
48
|
+
if (cp < 0x20 || cp === 0x7f) return `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
49
|
+
if (/\s/u.test(ch)) return "空格";
|
|
50
|
+
return ch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check a node name. `raw` is trimmed first; the trimmed, NFC form must be
|
|
55
|
+
* 1..64 characters (code points) of letters / digits / `_` / `-`, not starting with `-`.
|
|
56
|
+
*/
|
|
57
|
+
export function checkNodeName(raw: unknown): NodeNameCheck {
|
|
58
|
+
if (typeof raw !== "string") return { ok: false, error: "empty", message: "名字不能为空" };
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
if (trimmed.length === 0) return { ok: false, error: "empty", message: "名字不能为空" };
|
|
61
|
+
const chars = [...trimmed];
|
|
62
|
+
if (chars.length > NODE_NAME_MAX_CHARS) {
|
|
63
|
+
return { ok: false, error: "too_long", message: `名字最多 ${NODE_NAME_MAX_CHARS} 个字符` };
|
|
64
|
+
}
|
|
65
|
+
if (trimmed !== trimmed.normalize("NFC")) {
|
|
66
|
+
return { ok: false, error: "not_nfc", message: "名字需要是 Unicode NFC 规范形式" };
|
|
67
|
+
}
|
|
68
|
+
if (chars[0] === "-") {
|
|
69
|
+
return { ok: false, error: "leading_dash", char: "-", message: "名字不能以 - 开头" };
|
|
70
|
+
}
|
|
71
|
+
if (!NODE_NAME_RE.test(trimmed)) {
|
|
72
|
+
const bad = chars.find((ch, i) => !(i === 0 ? /^[\p{L}\p{N}_]$/u : /^[\p{L}\p{M}\p{N}_-]$/u).test(ch)) ?? chars[0]!;
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: "forbidden_char",
|
|
76
|
+
char: bad,
|
|
77
|
+
message: `名字里不能有「${describeChar(bad)}」:只能用文字、字母、数字、_ 和 -`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, name: trimmed };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isValidNodeName(raw: unknown): boolean {
|
|
84
|
+
return checkNodeName(raw).ok;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** FNV-1a 32-bit over the UTF-8 bytes, 8 hex digits. Tiny, dependency-free, and the
|
|
88
|
+
* app reimplements it byte for byte (no crypto module needed in the browser / RN). */
|
|
89
|
+
export function fnv1aHex(s: string): string {
|
|
90
|
+
const bytes = new TextEncoder().encode(s);
|
|
91
|
+
let h = 0x811c9dc5;
|
|
92
|
+
for (const b of bytes) {
|
|
93
|
+
h ^= b;
|
|
94
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
95
|
+
}
|
|
96
|
+
return h.toString(16).padStart(8, "0");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ASCII folder name for a node name. Pure-ASCII names are transliterated
|
|
101
|
+
* (lowercase, runs of anything outside [a-z0-9] become one `-`, trimmed, `node-`
|
|
102
|
+
* prefixed when they do not start with a letter); anything else — and anything the
|
|
103
|
+
* transliteration empties — becomes `node-<first 6 hex of fnv1a(name)>`.
|
|
104
|
+
* The result always matches NODE_FOLDER_RE.
|
|
105
|
+
*/
|
|
106
|
+
export function nodeFolderSlug(raw: string): string {
|
|
107
|
+
const name = normalizeNodeName(raw);
|
|
108
|
+
// eslint-disable-next-line no-control-regex
|
|
109
|
+
if (/^[\x00-\x7f]*$/.test(name)) {
|
|
110
|
+
let s = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
111
|
+
if (s && !/^[a-z]/.test(s)) s = `node-${s}`;
|
|
112
|
+
s = s.slice(0, NODE_NAME_MAX_CHARS).replace(/-+$/, "");
|
|
113
|
+
if (NODE_FOLDER_RE.test(s)) return s;
|
|
114
|
+
}
|
|
115
|
+
return `node-${fnv1aHex(name).slice(0, 6)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The directory under `.anet/nodes/` a daemon-created node lives in.
|
|
120
|
+
* Names the old rule accepted keep their old directory (= the name) so nothing that
|
|
121
|
+
* worked before #652 moves; every other name gets nodeFolderSlug(name).
|
|
122
|
+
*/
|
|
123
|
+
export function nodeDirNameFor(name: string): string {
|
|
124
|
+
return LEGACY_NODE_NAME_RE.test(name) ? name : nodeFolderSlug(name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Shared test vectors — the app's copy must produce the same verdicts and slugs. */
|
|
128
|
+
export const NODE_NAME_CASES: ReadonlyArray<{ input: string; ok: boolean; error?: NodeNameError; slug?: string }> = [
|
|
129
|
+
{ input: "测试", ok: true, slug: "node-f78149" },
|
|
130
|
+
{ input: "研发助手A", ok: true, slug: "node-1d348f" },
|
|
131
|
+
{ input: "AB测试牛", ok: true, slug: "node-048250" },
|
|
132
|
+
{ input: "my-bot", ok: true, slug: "my-bot" },
|
|
133
|
+
{ input: "MyBot_2", ok: true, slug: "mybot-2" },
|
|
134
|
+
{ input: " spaced ", ok: true, slug: "spaced" },
|
|
135
|
+
{ input: "123", ok: true, slug: "node-123" },
|
|
136
|
+
{ input: "_x", ok: true, slug: "x" },
|
|
137
|
+
{ input: "Ünïcödé", ok: true, slug: "node-030667" },
|
|
138
|
+
{ input: "a".repeat(64), ok: true, slug: "a".repeat(64) },
|
|
139
|
+
{ input: "测".repeat(64), ok: true, slug: "node-50c3fb" },
|
|
140
|
+
{ input: "", ok: false, error: "empty" },
|
|
141
|
+
{ input: " ", ok: false, error: "empty" },
|
|
142
|
+
{ input: "a".repeat(65), ok: false, error: "too_long" },
|
|
143
|
+
{ input: "测".repeat(65), ok: false, error: "too_long" },
|
|
144
|
+
{ input: "-x", ok: false, error: "leading_dash" },
|
|
145
|
+
{ input: "e\u0301", ok: false, error: "not_nfc" },
|
|
146
|
+
{ input: ".hidden", ok: false, error: "forbidden_char" },
|
|
147
|
+
{ input: "..", ok: false, error: "forbidden_char" },
|
|
148
|
+
{ input: "a/b", ok: false, error: "forbidden_char" },
|
|
149
|
+
{ input: "a\\b", ok: false, error: "forbidden_char" },
|
|
150
|
+
{ input: "a:b", ok: false, error: "forbidden_char" },
|
|
151
|
+
{ input: "a.b", ok: false, error: "forbidden_char" },
|
|
152
|
+
{ input: "a b", ok: false, error: "forbidden_char" },
|
|
153
|
+
{ input: "a\u0000b", ok: false, error: "forbidden_char" },
|
|
154
|
+
{ input: "a\nb", ok: false, error: "forbidden_char" },
|
|
155
|
+
{ input: "a$b", ok: false, error: "forbidden_char" },
|
|
156
|
+
{ input: "a`b", ok: false, error: "forbidden_char" },
|
|
157
|
+
{ input: "a'b", ok: false, error: "forbidden_char" },
|
|
158
|
+
{ input: "a\"b", ok: false, error: "forbidden_char" },
|
|
159
|
+
{ input: "a;b", ok: false, error: "forbidden_char" },
|
|
160
|
+
{ input: "a*b", ok: false, error: "forbidden_char" },
|
|
161
|
+
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// #906 public application protocol. No paths, endpoints or credentials.
|
|
2
|
+
export type ProviderApplicationSelection = {
|
|
3
|
+
node_id: string; node_revision: string; provider_id: string; auth_id: string;
|
|
4
|
+
model: string; confirm_restart: true;
|
|
5
|
+
};
|
|
6
|
+
const fields = (v: any, names: string[]) => v && typeof v === 'object' && !Array.isArray(v)
|
|
7
|
+
&& Object.keys(v).length === names.length && Object.keys(v).every(k => names.includes(k));
|
|
8
|
+
const id = (v: any) => typeof v === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(v);
|
|
9
|
+
const hash = (v: any) => typeof v === 'string' && /^[a-f0-9]{64}$/.test(v);
|
|
10
|
+
export function validProviderApplicationSelection(v: any): v is ProviderApplicationSelection {
|
|
11
|
+
return fields(v, ['node_id', 'node_revision', 'provider_id', 'auth_id', 'model', 'confirm_restart'])
|
|
12
|
+
&& id(v.node_id) && hash(v.node_revision) && id(v.provider_id) && id(v.auth_id) && id(v.model)
|
|
13
|
+
&& v.confirm_restart === true;
|
|
14
|
+
}
|
|
15
|
+
export function validProviderApplicationReceipt(v: any, requestId: string, revision: number,
|
|
16
|
+
selection: ProviderApplicationSelection): boolean {
|
|
17
|
+
return fields(v, ['status', 'request_id', 'node_id', 'provider_id', 'auth_id', 'model', 'runtime_provider',
|
|
18
|
+
'node_revision', 'provider_revision', 'verification', 'applied_at'])
|
|
19
|
+
&& v.status === 'applied' && v.request_id === requestId && v.node_id === selection.node_id
|
|
20
|
+
&& v.provider_id === selection.provider_id && v.auth_id === selection.auth_id && v.model === selection.model
|
|
21
|
+
&& typeof v.runtime_provider === 'string' && /^anet_[a-f0-9]{20}$/.test(v.runtime_provider)
|
|
22
|
+
&& hash(v.node_revision) && v.provider_revision === revision && v.verification === 'runtime_confirmed'
|
|
23
|
+
&& Number.isSafeInteger(v.applied_at) && v.applied_at > 0 && v.applied_at <= Date.now() + 30_000;
|
|
24
|
+
}
|
|
@@ -17,27 +17,37 @@ import {
|
|
|
17
17
|
|
|
18
18
|
const HUB_PATH = join(import.meta.dir, "reserved-env.ts");
|
|
19
19
|
const DAEMON_PATH = join(import.meta.dir, "..", "..", "..", "agent-node", "src", "shared", "reserved-env.ts");
|
|
20
|
+
const CLI_PATH = join(import.meta.dir, "..", "..", "..", "agent-network", "src", "shared", "reserved-env.ts");
|
|
20
21
|
|
|
21
22
|
describe("RFC-026 v4 §4.4.7 G9 — hub/daemon reserved-env drift guard", () => {
|
|
22
23
|
test("byte-identical source files", () => {
|
|
23
24
|
const hub = readFileSync(HUB_PATH, "utf-8");
|
|
24
25
|
const daemon = readFileSync(DAEMON_PATH, "utf-8");
|
|
26
|
+
const cli = readFileSync(CLI_PATH, "utf-8");
|
|
25
27
|
expect(daemon).toBe(hub);
|
|
28
|
+
expect(cli).toBe(hub);
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
test("imported constants are set-equal at runtime", async () => {
|
|
29
32
|
// Use a dynamic file import path so this test breaks if the file
|
|
30
33
|
// moves but still references the daemon copy.
|
|
31
34
|
const daemonMod = await import(DAEMON_PATH);
|
|
35
|
+
const cliMod = await import(CLI_PATH);
|
|
32
36
|
const daemonExact = daemonMod.RESERVED_ENV_KEYS_EXACT as Set<string>;
|
|
33
37
|
const daemonPrefixes = daemonMod.RESERVED_ENV_PREFIXES as ReadonlyArray<string>;
|
|
38
|
+
const cliExact = cliMod.RESERVED_ENV_KEYS_EXACT as Set<string>;
|
|
39
|
+
const cliPrefixes = cliMod.RESERVED_ENV_PREFIXES as ReadonlyArray<string>;
|
|
34
40
|
|
|
35
41
|
// EXACT set equality
|
|
36
42
|
expect(daemonExact.size).toBe(HUB_EXACT.size);
|
|
37
43
|
for (const k of HUB_EXACT) expect(daemonExact.has(k)).toBe(true);
|
|
38
44
|
for (const k of daemonExact) expect(HUB_EXACT.has(k)).toBe(true);
|
|
45
|
+
expect(cliExact.size).toBe(HUB_EXACT.size);
|
|
46
|
+
for (const k of HUB_EXACT) expect(cliExact.has(k)).toBe(true);
|
|
47
|
+
for (const k of cliExact) expect(HUB_EXACT.has(k)).toBe(true);
|
|
39
48
|
|
|
40
49
|
// PREFIX array order-independent equality
|
|
41
50
|
expect(new Set(daemonPrefixes)).toEqual(new Set(HUB_PREFIXES));
|
|
51
|
+
expect(new Set(cliPrefixes)).toEqual(new Set(HUB_PREFIXES));
|
|
42
52
|
});
|
|
43
53
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { SESSION_TASK_PREVIEW_MAX, TASK_CONTENT_MAX, sessionTaskPreview } from "./task-content-limit.js";
|
|
5
|
+
|
|
6
|
+
// Board #668 — the node reports the whole task so the hub can match it
|
|
7
|
+
// against tasks.content. Both sides must agree on the cap.
|
|
8
|
+
const HUB_PATH = join(import.meta.dir, "task-content-limit.ts");
|
|
9
|
+
const NODE_PATH = join(import.meta.dir, "..", "..", "..", "agent-node", "src", "shared", "task-content-limit.ts");
|
|
10
|
+
|
|
11
|
+
describe("#668 — hub/node task-content-limit drift guard", () => {
|
|
12
|
+
test("byte-identical source files", () => {
|
|
13
|
+
expect(readFileSync(NODE_PATH, "utf-8")).toBe(readFileSync(HUB_PATH, "utf-8"));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("constants equal at runtime", async () => {
|
|
17
|
+
const node = await import(NODE_PATH);
|
|
18
|
+
expect(node.TASK_CONTENT_MAX).toBe(TASK_CONTENT_MAX);
|
|
19
|
+
expect(node.SESSION_TASK_PREVIEW_MAX).toBe(SESSION_TASK_PREVIEW_MAX);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("preview is the first 200 chars", () => {
|
|
23
|
+
expect(SESSION_TASK_PREVIEW_MAX).toBe(200);
|
|
24
|
+
const long = "字".repeat(TASK_CONTENT_MAX);
|
|
25
|
+
expect(sessionTaskPreview(long)).toBe(long.slice(0, 200));
|
|
26
|
+
expect(sessionTaskPreview("short")).toBe("short");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Board #668 — task text limits shared by hub and node.
|
|
2
|
+
//
|
|
3
|
+
// This file is byte-identical in server/src/shared/task-content-limit.ts and
|
|
4
|
+
// agent-node/src/shared/task-content-limit.ts (task-content-limit-drift.test.ts
|
|
5
|
+
// fails the build on any difference).
|
|
6
|
+
//
|
|
7
|
+
// TASK_CONTENT_MAX is the cap every dispatch entry (send_task, REST /api/task,
|
|
8
|
+
// scheduled tasks) and report_status.task accept. The node reports the whole
|
|
9
|
+
// task text so the hub can match it against tasks.content and write
|
|
10
|
+
// started_at. If the dispatch cap grew without the node and report_status
|
|
11
|
+
// following, long tasks would silently stop matching again.
|
|
12
|
+
//
|
|
13
|
+
// SESSION_TASK_PREVIEW_MAX is what the hub keeps in sessions.task. That column
|
|
14
|
+
// is readable by get_all_status, the full /api/status and members who may see
|
|
15
|
+
// the agent but not its conversations, so only a short preview is stored —
|
|
16
|
+
// the full text is used for matching, never persisted on the session row.
|
|
17
|
+
|
|
18
|
+
export const TASK_CONTENT_MAX = 10_000;
|
|
19
|
+
|
|
20
|
+
export const SESSION_TASK_PREVIEW_MAX = 200;
|
|
21
|
+
|
|
22
|
+
/** The preview stored in sessions.task — same cut as the dispatch preview. */
|
|
23
|
+
export function sessionTaskPreview(task: string): string {
|
|
24
|
+
return task.slice(0, SESSION_TASK_PREVIEW_MAX);
|
|
25
|
+
}
|