@sleep2agi/commhub-server 0.9.0-preview.14 → 0.9.0-preview.141
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-department-http.test.ts +148 -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 +423 -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 +604 -51
- 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/chain-attempt-dedup.test.ts +447 -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 +49 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +199 -7
- package/src/create-node-validate.ts +232 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +88 -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 +109 -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 +1119 -42
- 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 +276 -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/env-transport-gate-http.test.ts +146 -0
- package/src/env-transport-gate.ts +74 -0
- package/src/env-transport-policy.ts +21 -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/host-supervisor-identity-http.test.ts +152 -0
- package/src/host-supervisor-identity.ts +92 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-catalog-read-http.test.ts +187 -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/human-inbox.ts +42 -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/list-host-supervisors.test.ts +5 -4
- 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 +1083 -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 +135 -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-secret-rest-http.test.ts +190 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-identity.ts +35 -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 +442 -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/password-network-tokens.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 +38 -10
- 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 +184 -0
- package/src/report-status-secret-keys.test.ts +140 -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 +131 -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 +357 -0
- package/src/requirements-stats.ts +195 -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 +141 -0
- package/src/rest-projections.ts +70 -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-backoff-http.test.ts +193 -0
- package/src/scheduled-backoff.ts +64 -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-outcome-guidance-http.test.ts +139 -0
- package/src/scheduled-outcome-guidance.ts +9 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-requirement-wake-http.test.ts +264 -0
- package/src/scheduled-requirement-wake.ts +125 -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 +312 -0
- package/src/scheduled-stuck.ts +186 -0
- package/src/scheduled-tasks-http.test.ts +679 -0
- package/src/scheduled-tasks.ts +885 -0
- package/src/scheduled-work-outcome-http.test.ts +154 -0
- package/src/scheduled-work-outcome.ts +50 -0
- package/src/scoped-secrets.ts +139 -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 +5398 -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 -2
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-activity.ts +60 -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 +4008 -355
- 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
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Agent 团队(看板 #764,父卡 #750):Agent(节点)自己的组织树,和人的部门树(network_departments)完全独立。
|
|
2
|
+
//
|
|
3
|
+
// 只增:两张新表。network_agent_teams = 团队(多层,同一上级下不重名);network_agent_team_members = 节点归哪个团队
|
|
4
|
+
// (一个节点最多一个团队;行存在 = 在这个团队)。nodes / 部门表一概不动,已有接口的响应不变。
|
|
5
|
+
//
|
|
6
|
+
// 权限(每次请求按 owner_user_id 现算,不存展开结果):
|
|
7
|
+
// 网络 owner / admin、Hub 管理员:全部;
|
|
8
|
+
// 团队的 owner_user_id:管这个团队的子树 —— 改名 / 排序 / 设 lead、在子树里建子团队、在子树里挪动、把节点放进 / 移出子树;
|
|
9
|
+
// 但不能改自己那个团队的 owner,也不能把它挪出去(这两样要求「上级也在我的范围里」);
|
|
10
|
+
// 其他成员、节点令牌:只读。viewer 当 owner 也不获得写权限。
|
|
11
|
+
// 读的时候 JOIN nodes:节点删了,它的归属 / lead 自然消失;删节点的两条路径另外调 clearNodeFromAgentTeams 真删行。
|
|
12
|
+
|
|
13
|
+
import { db } from "./db.js";
|
|
14
|
+
|
|
15
|
+
db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS network_agent_teams (
|
|
17
|
+
network_id TEXT NOT NULL,
|
|
18
|
+
team_id TEXT NOT NULL,
|
|
19
|
+
name TEXT NOT NULL,
|
|
20
|
+
parent_id TEXT,
|
|
21
|
+
lead_node_id TEXT,
|
|
22
|
+
owner_user_id TEXT,
|
|
23
|
+
sort INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
created_by TEXT,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
27
|
+
PRIMARY KEY (network_id, team_id)
|
|
28
|
+
);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_network_agent_teams_parent ON network_agent_teams(network_id, parent_id);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS network_agent_team_members (
|
|
31
|
+
network_id TEXT NOT NULL,
|
|
32
|
+
node_id TEXT NOT NULL,
|
|
33
|
+
team_id TEXT NOT NULL,
|
|
34
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
PRIMARY KEY (network_id, node_id)
|
|
36
|
+
);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_network_agent_team_members_team ON network_agent_team_members(network_id, team_id);
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
export const AGENT_TEAM_SCOPE_DENIED = "agent_team_scope_denied";
|
|
41
|
+
const MAX_DEPTH = 10;
|
|
42
|
+
const MAX_TEAMS = 500;
|
|
43
|
+
const NAME_MAX = 40;
|
|
44
|
+
|
|
45
|
+
type Row = { team_id: string; name: string; parent_id: string | null; lead_node_id: string | null; owner_user_id: string | null; sort: number; created_at: string; updated_at: string };
|
|
46
|
+
type Fail = { ok: false; status: number; error: string };
|
|
47
|
+
const fail = (status: number, error: string): Fail => ({ ok: false, status, error });
|
|
48
|
+
const nil = (v: unknown) => v === undefined || v === null || v === "";
|
|
49
|
+
|
|
50
|
+
function rows(networkId: string): Row[] {
|
|
51
|
+
return db.all<Row>(
|
|
52
|
+
"SELECT team_id, name, parent_id, lead_node_id, owner_user_id, sort, created_at, updated_at FROM network_agent_teams WHERE network_id = ?1 ORDER BY sort, created_at, team_id",
|
|
53
|
+
networkId,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function one(networkId: string, id: unknown): Row | null {
|
|
57
|
+
if (typeof id !== "string") return null;
|
|
58
|
+
return rows(networkId).find((r) => r.team_id === id) ?? null;
|
|
59
|
+
}
|
|
60
|
+
function parentOf(networkId: string): Map<string, string | null> {
|
|
61
|
+
return new Map(rows(networkId).map((r) => [r.team_id, r.parent_id]));
|
|
62
|
+
}
|
|
63
|
+
/** id 和它的全部上级(从自己往上)。 */
|
|
64
|
+
function chain(parent: Map<string, string | null>, id: string | null): string[] {
|
|
65
|
+
const out: string[] = [];
|
|
66
|
+
let cur = id;
|
|
67
|
+
while (cur && parent.has(cur) && !out.includes(cur)) { out.push(cur); cur = parent.get(cur) ?? null; }
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function height(parent: Map<string, string | null>, id: string): number {
|
|
71
|
+
let h = 1;
|
|
72
|
+
for (const [c, p] of parent) if (p === id && c !== id) h = Math.max(h, 1 + height(parent, c));
|
|
73
|
+
return h;
|
|
74
|
+
}
|
|
75
|
+
const isMember = (networkId: string, userId: string) =>
|
|
76
|
+
!!db.get("SELECT user_id FROM network_members WHERE network_id = ?1 AND user_id = ?2", networkId, userId);
|
|
77
|
+
type Hidden = (n: NodeRef) => boolean;
|
|
78
|
+
/** 写路径用同一个可见性判据:不在本网络、或调用者看不见(只看授权 Agent)→ 一律当不存在(同一个 404,不能拿来探测)。 */
|
|
79
|
+
function visibleNode(networkId: string, nodeId: unknown, hidden: Hidden): boolean {
|
|
80
|
+
if (typeof nodeId !== "string") return false;
|
|
81
|
+
const n = db.get<NodeRef>("SELECT node_id, alias, display_name FROM nodes WHERE network_id = ?1 AND node_id = ?2", networkId, nodeId);
|
|
82
|
+
return !!n && !hidden(n);
|
|
83
|
+
}
|
|
84
|
+
const publicTeam = (networkId: string, id: string, hidden: Hidden) => listAgentTeams(networkId, hidden).find((t) => t.id === id)!;
|
|
85
|
+
const cleanName = (raw: unknown): string | null => {
|
|
86
|
+
if (typeof raw !== "string") return null;
|
|
87
|
+
const n = raw.trim();
|
|
88
|
+
return n && [...n].length <= NAME_MAX ? n : null;
|
|
89
|
+
};
|
|
90
|
+
function nameTaken(networkId: string, parentId: string | null, name: string, except?: string): boolean {
|
|
91
|
+
return rows(networkId).some((r) => r.parent_id === parentId && r.name === name && r.team_id !== except);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** userId 能管的团队:他当 owner 的团队及全部下级(owner 不在本网络 / 是 viewer → 空)。 */
|
|
95
|
+
export function ownedScope(networkId: string, userId: string): Set<string> {
|
|
96
|
+
const role = db.get<{ role: string | null }>("SELECT role FROM network_members WHERE network_id = ?1 AND user_id = ?2", networkId, userId)?.role;
|
|
97
|
+
const out = new Set<string>();
|
|
98
|
+
if (!role || role === "viewer") return out;
|
|
99
|
+
const all = rows(networkId);
|
|
100
|
+
const parent = new Map(all.map((r) => [r.team_id, r.parent_id]));
|
|
101
|
+
const owned = new Set(all.filter((r) => r.owner_user_id === userId).map((r) => r.team_id));
|
|
102
|
+
for (const r of all) if (chain(parent, r.team_id).some((id) => owned.has(id))) out.add(r.team_id);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type NodeRef = { node_id: string; alias: string | null; display_name: string | null };
|
|
107
|
+
/** 整棵树 + 每个团队的成员 / lead / owner。hidden(node) = true 的节点不出现(只看授权 Agent 的成员)。 */
|
|
108
|
+
export function listAgentTeams(networkId: string, hidden: Hidden) {
|
|
109
|
+
const nodes = new Map(db.all<NodeRef>("SELECT node_id, alias, display_name FROM nodes WHERE network_id = ?1", networkId).map((n) => [n.node_id, n]));
|
|
110
|
+
const people = new Map(db.all<{ user_id: string; username: string; display_name: string | null }>(
|
|
111
|
+
"SELECT u.user_id, u.username, u.display_name FROM network_members m JOIN users u ON u.user_id = m.user_id WHERE m.network_id = ?1", networkId,
|
|
112
|
+
).map((u) => [u.user_id, !u.display_name || u.display_name === u.username ? "" : u.display_name])); // 同 requirements-people 的显示名规则
|
|
113
|
+
const seen = (id: string | null) => (id && nodes.has(id) && !hidden(nodes.get(id)!) ? nodes.get(id)! : null);
|
|
114
|
+
const members = new Map<string, NodeRef[]>();
|
|
115
|
+
for (const m of db.all<{ node_id: string; team_id: string }>("SELECT node_id, team_id FROM network_agent_team_members WHERE network_id = ?1 ORDER BY node_id", networkId)) {
|
|
116
|
+
const n = seen(m.node_id);
|
|
117
|
+
if (n) members.set(m.team_id, [...(members.get(m.team_id) ?? []), n]);
|
|
118
|
+
}
|
|
119
|
+
return rows(networkId).map((r) => ({
|
|
120
|
+
id: r.team_id, name: r.name, parent_id: r.parent_id, sort: r.sort,
|
|
121
|
+
lead: seen(r.lead_node_id),
|
|
122
|
+
owner: r.owner_user_id && people.has(r.owner_user_id) ? { user_id: r.owner_user_id, display_name: people.get(r.owner_user_id)! } : null,
|
|
123
|
+
members: members.get(r.team_id) ?? [],
|
|
124
|
+
created_at: r.created_at, updated_at: r.updated_at,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** #765: node-token identity only; no fallback to a human's department.
|
|
129
|
+
* Reuse the REST projection's network joins and display-name privacy rules.
|
|
130
|
+
*/
|
|
131
|
+
export function agentTeamWhoami(networkId: string, nodeId: string | null) {
|
|
132
|
+
if (!nodeId) return { ok: false, error: "node_identity_unbound" };
|
|
133
|
+
const teams = listAgentTeams(networkId, () => false);
|
|
134
|
+
const self = teams.find(t => t.members.some(n => n.node_id === nodeId));
|
|
135
|
+
if (!self) return { ok: true, source: "none", node_id: nodeId, team: null, ancestors: [], lead: null, owner: null, agents: [], truncated: false };
|
|
136
|
+
const byId = new Map(teams.map(t => [t.id, t]));
|
|
137
|
+
const seen = new Set([self.id]);
|
|
138
|
+
const ancestors: { id: string; name: string }[] = [];
|
|
139
|
+
for (let id = self.parent_id; id && !seen.has(id);) {
|
|
140
|
+
const parent = byId.get(id);
|
|
141
|
+
if (!parent) break;
|
|
142
|
+
seen.add(id);
|
|
143
|
+
ancestors.push({ id, name: parent.name });
|
|
144
|
+
id = parent.parent_id;
|
|
145
|
+
}
|
|
146
|
+
return { ok: true, source: "node", node_id: nodeId, team: { id: self.id, name: self.name }, ancestors,
|
|
147
|
+
lead: self.lead, owner: self.owner, agents: self.members.slice(0, 50), truncated: self.members.length > 50 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** scope = null 表示全管;否则是调用者能管的团队集合。 */
|
|
151
|
+
export function createAgentTeam(networkId: string, actor: string, scope: Set<string> | null, hidden: Hidden, body: Record<string, unknown>) {
|
|
152
|
+
const name = cleanName(body.name);
|
|
153
|
+
if (!name) return fail(400, "invalid_team_name");
|
|
154
|
+
const parentId = nil(body.parent_id) ? null : body.parent_id;
|
|
155
|
+
if (parentId !== null && !one(networkId, parentId)) return fail(400, "parent_not_found");
|
|
156
|
+
if (scope && (parentId === null || !scope.has(parentId as string))) return fail(403, AGENT_TEAM_SCOPE_DENIED);
|
|
157
|
+
if (chain(parentOf(networkId), parentId as string | null).length + 1 > MAX_DEPTH) return fail(400, "team_too_deep");
|
|
158
|
+
if (nameTaken(networkId, parentId as string | null, name)) return fail(409, "team_name_taken");
|
|
159
|
+
if (rows(networkId).length >= MAX_TEAMS) return fail(400, "too_many_teams");
|
|
160
|
+
const id = `team_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
161
|
+
const sort = rows(networkId).filter((r) => r.parent_id === parentId).length;
|
|
162
|
+
db.run("INSERT INTO network_agent_teams (network_id, team_id, name, parent_id, sort, created_by) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", [networkId, id, name, parentId, sort, actor]);
|
|
163
|
+
return { ok: true as const, team: publicTeam(networkId, id, hidden) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function updateAgentTeam(networkId: string, id: string, scope: Set<string> | null, hidden: Hidden, body: Record<string, unknown>) {
|
|
167
|
+
const cur = one(networkId, id);
|
|
168
|
+
if (!cur) return fail(404, "team_not_found");
|
|
169
|
+
// 子树 owner:团队本身要在范围里;改 owner / 挪动还要求它的上级也在范围里(不能改自己那个团队的 owner、不能挪出去)。
|
|
170
|
+
if (scope && !scope.has(id)) return fail(403, AGENT_TEAM_SCOPE_DENIED);
|
|
171
|
+
const parentInScope = !scope || (cur.parent_id !== null && scope.has(cur.parent_id));
|
|
172
|
+
const next = { ...cur };
|
|
173
|
+
let touched = false;
|
|
174
|
+
if (body.name !== undefined) {
|
|
175
|
+
const n = cleanName(body.name);
|
|
176
|
+
if (!n) return fail(400, "invalid_team_name");
|
|
177
|
+
next.name = n; touched = true;
|
|
178
|
+
}
|
|
179
|
+
if (body.parent_id !== undefined) {
|
|
180
|
+
const p = nil(body.parent_id) ? null : body.parent_id;
|
|
181
|
+
if (p !== null && !one(networkId, p)) return fail(400, "parent_not_found");
|
|
182
|
+
if (scope && (!parentInScope || p === null || !scope.has(p as string))) return fail(403, AGENT_TEAM_SCOPE_DENIED);
|
|
183
|
+
const parent = parentOf(networkId);
|
|
184
|
+
if (p !== null && chain(parent, p as string).includes(id)) return fail(400, "team_cycle");
|
|
185
|
+
if (chain(parent, p as string | null).length + height(parent, id) > MAX_DEPTH) return fail(400, "team_too_deep");
|
|
186
|
+
next.parent_id = p as string | null; touched = true;
|
|
187
|
+
}
|
|
188
|
+
if (body.sort !== undefined) {
|
|
189
|
+
if (!Number.isInteger(body.sort)) return fail(400, "invalid_sort");
|
|
190
|
+
next.sort = body.sort as number; touched = true;
|
|
191
|
+
}
|
|
192
|
+
if (body.lead_node_id !== undefined) {
|
|
193
|
+
const l = nil(body.lead_node_id) ? null : body.lead_node_id;
|
|
194
|
+
if (l !== null && !visibleNode(networkId, l, hidden)) return fail(404, "node_not_found");
|
|
195
|
+
next.lead_node_id = l as string | null; touched = true;
|
|
196
|
+
}
|
|
197
|
+
if (body.owner_user_id !== undefined) {
|
|
198
|
+
const o = nil(body.owner_user_id) ? null : body.owner_user_id;
|
|
199
|
+
if (!parentInScope) return fail(403, AGENT_TEAM_SCOPE_DENIED); // 先判权限,再判成员:没权限的人不能借此探测谁是成员
|
|
200
|
+
if (o !== null && (typeof o !== "string" || !isMember(networkId, o))) return fail(400, "owner_not_member");
|
|
201
|
+
next.owner_user_id = o as string | null; touched = true;
|
|
202
|
+
}
|
|
203
|
+
if (!touched) return fail(400, "empty_patch");
|
|
204
|
+
if ((next.name !== cur.name || next.parent_id !== cur.parent_id) && nameTaken(networkId, next.parent_id, next.name, id)) return fail(409, "team_name_taken");
|
|
205
|
+
db.run(
|
|
206
|
+
"UPDATE network_agent_teams SET name = ?3, parent_id = ?4, sort = ?5, lead_node_id = ?6, owner_user_id = ?7, updated_at = datetime('now') WHERE network_id = ?1 AND team_id = ?2",
|
|
207
|
+
[networkId, id, next.name, next.parent_id, next.sort, next.lead_node_id, next.owner_user_id],
|
|
208
|
+
);
|
|
209
|
+
return { ok: true as const, team: publicTeam(networkId, id, hidden) };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 只删没有子团队的团队;成员归属同一事务清掉(节点变回未分配)。 */
|
|
213
|
+
export function deleteAgentTeam(networkId: string, id: string, scope: Set<string> | null) {
|
|
214
|
+
const cur = one(networkId, id);
|
|
215
|
+
if (!cur) return fail(404, "team_not_found");
|
|
216
|
+
if (scope && (cur.parent_id === null || !scope.has(cur.parent_id))) return fail(403, AGENT_TEAM_SCOPE_DENIED);
|
|
217
|
+
const children = rows(networkId).filter((r) => r.parent_id === id).length;
|
|
218
|
+
if (children) return { ...fail(409, "team_has_children"), children };
|
|
219
|
+
db.transaction(() => {
|
|
220
|
+
db.run("DELETE FROM network_agent_teams WHERE network_id = ?1 AND team_id = ?2", [networkId, id]);
|
|
221
|
+
db.run("DELETE FROM network_agent_team_members WHERE network_id = ?1 AND team_id = ?2", [networkId, id]);
|
|
222
|
+
});
|
|
223
|
+
return { ok: true as const, deleted: id };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 节点归一个团队(null = 未分配)。子树 owner:目标团队、节点当前的团队(有的话)都要在范围里。 */
|
|
227
|
+
export function setNodeAgentTeam(networkId: string, nodeId: string, scope: Set<string> | null, hidden: Hidden, teamId: unknown) {
|
|
228
|
+
if (!visibleNode(networkId, nodeId, hidden)) return fail(404, "node_not_found");
|
|
229
|
+
const target = nil(teamId) ? null : teamId;
|
|
230
|
+
if (target !== null && !one(networkId, target)) return fail(400, "team_not_found");
|
|
231
|
+
const current = db.get<{ team_id: string }>("SELECT team_id FROM network_agent_team_members WHERE network_id = ?1 AND node_id = ?2", networkId, nodeId)?.team_id ?? null;
|
|
232
|
+
if (scope && ((target === null && current === null) || (target !== null && !scope.has(target as string)) || (current !== null && !scope.has(current)))) return fail(403, AGENT_TEAM_SCOPE_DENIED);
|
|
233
|
+
db.transaction(() => {
|
|
234
|
+
db.run("DELETE FROM network_agent_team_members WHERE network_id = ?1 AND node_id = ?2", [networkId, nodeId]);
|
|
235
|
+
if (target !== null) db.run("INSERT INTO network_agent_team_members (network_id, node_id, team_id) VALUES (?1, ?2, ?3)", [networkId, nodeId, target]);
|
|
236
|
+
});
|
|
237
|
+
return { ok: true as const, node_id: nodeId, team_id: target as string | null };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 节点删除时调用:清掉它的团队归属,指向它的 lead 置空。 */
|
|
241
|
+
export function clearNodeFromAgentTeams(nodeId: string): void {
|
|
242
|
+
db.run("DELETE FROM network_agent_team_members WHERE node_id = ?1", [nodeId]);
|
|
243
|
+
db.run("UPDATE network_agent_teams SET lead_node_id = NULL WHERE lead_node_id = ?1", [nodeId]);
|
|
244
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { parseAliasFilter } from "./alias-filter";
|
|
3
|
+
|
|
4
|
+
test("no filter given means no filtering, not an empty match", () => {
|
|
5
|
+
for (const raw of [undefined, null, "", " ", ",", " , , "]) {
|
|
6
|
+
const f = parseAliasFilter(raw as any);
|
|
7
|
+
expect(f.aliases).toEqual([]);
|
|
8
|
+
expect(f.sql).toBe("");
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("a single alias produces one placeholder", () => {
|
|
13
|
+
const f = parseAliasFilter("节点甲");
|
|
14
|
+
expect(f.aliases).toEqual(["节点甲"]);
|
|
15
|
+
expect(f.sql).toBe(" AND alias IN (?)");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("several aliases keep their order and count", () => {
|
|
19
|
+
const f = parseAliasFilter("A站内容,A站内容牛,hub");
|
|
20
|
+
expect(f.aliases).toEqual(["A站内容", "A站内容牛", "hub"]);
|
|
21
|
+
expect(f.sql).toBe(" AND alias IN (?,?,?)");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("surrounding whitespace is trimmed", () => {
|
|
25
|
+
expect(parseAliasFilter(" a , b ").aliases).toEqual(["a", "b"]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// The point of the module. A trailing comma must not become `alias = ''`,
|
|
29
|
+
// which matches nothing and reads exactly like "those nodes do not exist".
|
|
30
|
+
test("blank entries are dropped, never turned into a match-nothing term", () => {
|
|
31
|
+
const f = parseAliasFilter("a,,b,");
|
|
32
|
+
expect(f.aliases).toEqual(["a", "b"]);
|
|
33
|
+
expect(f.sql).toBe(" AND alias IN (?,?)");
|
|
34
|
+
expect(f.aliases).not.toContain("");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a filter of only commas is the same as no filter — it must not silently match zero rows", () => {
|
|
38
|
+
const f = parseAliasFilter(",,,");
|
|
39
|
+
expect(f.sql).toBe("");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("placeholder count always equals alias count, so params can never misalign", () => {
|
|
43
|
+
for (const raw of ["a", "a,b", "a,,b", " a , b , c ", ",x,"]) {
|
|
44
|
+
const f = parseAliasFilter(raw);
|
|
45
|
+
expect((f.sql.match(/\?/g) ?? []).length).toBe(f.aliases.length);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("aliases are passed through verbatim — no globbing, no case folding", () => {
|
|
50
|
+
const f = parseAliasFilter("A站内容,a站内容");
|
|
51
|
+
expect(f.aliases).toEqual(["A站内容", "a站内容"]);
|
|
52
|
+
expect(f.sql).not.toContain("LIKE");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Wiring: the tool must actually use this module, and must say what its
|
|
56
|
+
// `summary` counted — a caller who asked about three aliases and gets back
|
|
57
|
+
// "idle: 96" can easily read the 96 as being about their three.
|
|
58
|
+
import { readFileSync } from "fs";
|
|
59
|
+
import { join } from "path";
|
|
60
|
+
|
|
61
|
+
test("get_all_status uses parseAliasFilter and declares what summary counted", () => {
|
|
62
|
+
const source = readFileSync(join(import.meta.dir, "tools.ts"), "utf8");
|
|
63
|
+
const a = source.indexOf('"get_all_status"');
|
|
64
|
+
expect(a).toBeGreaterThan(-1);
|
|
65
|
+
const body = source.slice(a, source.indexOf('server.tool(', a + 10));
|
|
66
|
+
expect(body).toContain("filter_alias");
|
|
67
|
+
expect(body).toContain("parseAliasFilter(filter_alias)");
|
|
68
|
+
expect(body).toContain("summary_scope");
|
|
69
|
+
expect(body).toContain("sessions_returned");
|
|
70
|
+
// The alias list must go through parameters, never be interpolated.
|
|
71
|
+
expect(body).not.toMatch(/alias IN \(\$\{/);
|
|
72
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Parse the `filter_alias` argument of `get_all_status` into an exact-match
|
|
2
|
+
// IN list.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists at all: on a 222-session hub the unfiltered response is about
|
|
5
|
+
// 259 KB — past what an MCP client accepts in one result. A caller who wanted
|
|
6
|
+
// the status of three specific nodes could not get it from the tool, and had to
|
|
7
|
+
// go around it to the REST API. The filter is the fix; this module is the part
|
|
8
|
+
// of it worth pinning, because the interesting behaviour is what happens to the
|
|
9
|
+
// inputs that are not a clean alias.
|
|
10
|
+
//
|
|
11
|
+
// Blank entries are DROPPED rather than matched. A trailing comma would
|
|
12
|
+
// otherwise produce `alias = ''`, which matches no row — and "no rows" reads
|
|
13
|
+
// exactly like "those nodes do not exist". The failure and the true answer
|
|
14
|
+
// would be indistinguishable to the caller, which is the thing to avoid.
|
|
15
|
+
|
|
16
|
+
export interface AliasFilter {
|
|
17
|
+
/** Exact aliases to match. Empty means "no alias filtering". */
|
|
18
|
+
aliases: string[];
|
|
19
|
+
/** SQL fragment to append, or "" when there is nothing to filter on. */
|
|
20
|
+
sql: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseAliasFilter(raw: string | undefined | null): AliasFilter {
|
|
24
|
+
const aliases = (raw ?? "")
|
|
25
|
+
.split(",")
|
|
26
|
+
.map(a => a.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
return {
|
|
29
|
+
aliases,
|
|
30
|
+
sql: aliases.length > 0 ? ` AND alias IN (${aliases.map(() => "?").join(",")})` : "",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The same `alias IN (…)` clause with numbered placeholders, starting after `paramsSoFar` already-bound
|
|
36
|
+
* parameters. Use it when the query already carries numbered `?N` placeholders: the PG adapter numbers a bare
|
|
37
|
+
* `?` from $1 again, so mixing the two binds the aliases to the wrong slots (SQLite happens to number them right).
|
|
38
|
+
*/
|
|
39
|
+
export function numberedAliasInClause(aliases: string[], paramsSoFar: number): string {
|
|
40
|
+
if (aliases.length === 0) return "";
|
|
41
|
+
return ` AND alias IN (${aliases.map((_, i) => `?${paramsSoFar + i + 1}`).join(",")})`;
|
|
42
|
+
}
|