negotium 0.1.7 → 0.1.8
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 +2 -2
- package/dist/chunk-0ay6cjdy.js +1331 -0
- package/dist/chunk-0ay6cjdy.js.map +16 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-5ahkew9c.js +806 -0
- package/dist/chunk-5ahkew9c.js.map +17 -0
- package/dist/chunk-6cpn7ht2.js +287 -0
- package/dist/chunk-6cpn7ht2.js.map +13 -0
- package/dist/chunk-6kctn20e.js +3529 -0
- package/dist/chunk-6kctn20e.js.map +20 -0
- package/dist/chunk-7xqj7aex.js +77 -0
- package/dist/chunk-7xqj7aex.js.map +11 -0
- package/dist/chunk-9p0ww3tk.js +41 -0
- package/dist/chunk-9p0ww3tk.js.map +10 -0
- package/dist/chunk-a92a7nrk.js +252 -0
- package/dist/chunk-a92a7nrk.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-cds1qxsf.js +1644 -0
- package/dist/chunk-cds1qxsf.js.map +17 -0
- package/dist/chunk-e4407g8x.js +274 -0
- package/dist/chunk-e4407g8x.js.map +10 -0
- package/dist/chunk-e5pbggsh.js +363 -0
- package/dist/chunk-e5pbggsh.js.map +11 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-exd0v74a.js +3012 -0
- package/dist/chunk-exd0v74a.js.map +32 -0
- package/dist/chunk-fgrme9tr.js +4398 -0
- package/dist/chunk-fgrme9tr.js.map +47 -0
- package/dist/chunk-fka9eqe1.js +581 -0
- package/dist/chunk-fka9eqe1.js.map +11 -0
- package/dist/chunk-g82jygez.js +108 -0
- package/dist/chunk-g82jygez.js.map +10 -0
- package/dist/chunk-ghs2j1fr.js +312 -0
- package/dist/chunk-ghs2j1fr.js.map +10 -0
- package/dist/chunk-gmy11efb.js +125 -0
- package/dist/chunk-gmy11efb.js.map +10 -0
- package/dist/chunk-gxma4c70.js +110 -0
- package/dist/chunk-gxma4c70.js.map +10 -0
- package/dist/chunk-hkdqhm34.js +61 -0
- package/dist/chunk-hkdqhm34.js.map +10 -0
- package/dist/chunk-hpa0cpzh.js +134 -0
- package/dist/chunk-hpa0cpzh.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j2behpy7.js +214 -0
- package/dist/chunk-j2behpy7.js.map +10 -0
- package/dist/chunk-jc0at0tc.js +102 -0
- package/dist/chunk-jc0at0tc.js.map +10 -0
- package/dist/chunk-jkqq5ef6.js +243 -0
- package/dist/chunk-jkqq5ef6.js.map +11 -0
- package/dist/chunk-jzkw2f01.js +51 -0
- package/dist/chunk-jzkw2f01.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-n17yxpa8.js +23 -0
- package/dist/chunk-n17yxpa8.js.map +10 -0
- package/dist/chunk-naa09ags.js +2143 -0
- package/dist/chunk-naa09ags.js.map +15 -0
- package/dist/chunk-ns3ke88n.js +142 -0
- package/dist/chunk-ns3ke88n.js.map +10 -0
- package/dist/chunk-p3cr2ejg.js +1626 -0
- package/dist/chunk-p3cr2ejg.js.map +15 -0
- package/dist/chunk-q5bcpf23.js +2925 -0
- package/dist/chunk-q5bcpf23.js.map +20 -0
- package/dist/chunk-rafc55bs.js +424 -0
- package/dist/chunk-rafc55bs.js.map +11 -0
- package/dist/chunk-rbrxjvbj.js +2830 -0
- package/dist/chunk-rbrxjvbj.js.map +23 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sm6fh5c5.js +569 -0
- package/dist/chunk-sm6fh5c5.js.map +14 -0
- package/dist/chunk-t9zcg8f1.js +499 -0
- package/dist/chunk-t9zcg8f1.js.map +13 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-w13jjf4a.js +46 -0
- package/dist/chunk-w13jjf4a.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-xfq68rch.js +583 -0
- package/dist/chunk-xfq68rch.js.map +11 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +48 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +47 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +865 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +357 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +532 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +255 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +16 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { canonicalMcpBridgeEnv } from "#mcp/canonical-bridge-config";
|
|
3
|
+
import { buildRuntimeMcpSpec, RUNTIME_MCP_KEY } from "#mcp/runtime-spec";
|
|
4
|
+
import { peerSessionBridgeIpcEnv } from "#mcp/session-comm/bridge-ipc-config";
|
|
5
|
+
import {
|
|
6
|
+
AGENT_HEALTH_SERVER,
|
|
7
|
+
CANONICAL_MCP_PROXY_SERVER,
|
|
8
|
+
FALLBACK_AGENT,
|
|
9
|
+
PADDLEOCR_SERVER,
|
|
10
|
+
resolveTopicWorkspaceDir,
|
|
11
|
+
SESSION_COMM_SERVER,
|
|
12
|
+
SYSTEM_HEALTH_SERVER,
|
|
13
|
+
TASK_SERVER,
|
|
14
|
+
TOKEN_STATS_SERVER,
|
|
15
|
+
TSCONFIG_PATH,
|
|
16
|
+
TSX_BIN,
|
|
17
|
+
VAULT_SERVER,
|
|
18
|
+
WIKI_SERVER,
|
|
19
|
+
} from "#platform/config";
|
|
20
|
+
import { logger } from "#platform/logger";
|
|
21
|
+
import type { AgentKind, AgentQueryOptions, PeerRuntimeBridgeContext } from "#types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the stdio transport spec for a Otium MCP server entry.
|
|
25
|
+
*
|
|
26
|
+
* claude/maestro launch the .ts server directly with `bun run` (fast, native
|
|
27
|
+
* TypeScript + bun:sqlite). codex CANNOT — codex 0.135's rmcp stdio client
|
|
28
|
+
* fails to handshake with bun-spawned servers (initialize is dropped/raced, so
|
|
29
|
+
* no tools reach the model), while pure-node servers connect reliably. So for
|
|
30
|
+
* the `codex` agent we launch the SAME server file via node + tsx, passing the
|
|
31
|
+
* tsconfig explicitly (servers run with cwd ≠ PROJECT_ROOT, so tsx can't find
|
|
32
|
+
* it on its own) so `@/*` path aliases resolve. Per-turn agent-aware config
|
|
33
|
+
* means switching agents mid-topic just works: each turn re-spawns with the
|
|
34
|
+
* right runtime.
|
|
35
|
+
*/
|
|
36
|
+
export function buildStdioMcpServer(
|
|
37
|
+
agent: AgentKind | undefined,
|
|
38
|
+
serverFile: string,
|
|
39
|
+
serverArgs: string[],
|
|
40
|
+
env?: Record<string, string>,
|
|
41
|
+
): Record<string, unknown> {
|
|
42
|
+
if (agent === "codex") {
|
|
43
|
+
return {
|
|
44
|
+
command: "node",
|
|
45
|
+
args: [TSX_BIN, serverFile, ...serverArgs],
|
|
46
|
+
// Merged onto codex's inherited env (not a replacement) — verified.
|
|
47
|
+
env: { TSX_TSCONFIG_PATH: TSCONFIG_PATH, ...env },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
command: "bun",
|
|
52
|
+
args: ["run", serverFile, ...serverArgs],
|
|
53
|
+
...(env ? { env } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Playwright unavailable notifier + per-turn state ---
|
|
58
|
+
//
|
|
59
|
+
// When the playwright manager fails to allocate a long-lived MCP port,
|
|
60
|
+
// the catalog used to fall back to a per-turn stdio chromium that ran as
|
|
61
|
+
// a child of the agent CLI subprocess — which meant Chromium was bound
|
|
62
|
+
// to the agent's process tree and died on cascade-kill. We now skip the
|
|
63
|
+
// browser entry entirely (the playwright entry is omitted from the
|
|
64
|
+
// catalog for that turn) AND surface the gap two ways:
|
|
65
|
+
//
|
|
66
|
+
// 1. `notifyPlaywrightUnavailable` — fire-and-forget callback for the host
|
|
67
|
+
// (telegram bot) to push an alert to the current topic. Throttled
|
|
68
|
+
// per (userId, topic) with a 5-minute cooldown so a flaky
|
|
69
|
+
// manager doesn't spam the chat. Per-process state — restart clears.
|
|
70
|
+
//
|
|
71
|
+
// 2. `consumePlaywrightUnavailable` — the system-prompt builder reads
|
|
72
|
+
// this flag to add an explicit "Playwright unavailable this turn"
|
|
73
|
+
// reminder to the agent's user message, so the model knows to
|
|
74
|
+
// route around browser tools instead of hallucinating a call to a
|
|
75
|
+
// tool that was silently removed from the schema list.
|
|
76
|
+
|
|
77
|
+
type PlaywrightUnavailableContext = {
|
|
78
|
+
userId: string;
|
|
79
|
+
topic: string | undefined;
|
|
80
|
+
agent: AgentKind | undefined;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type PlaywrightUnavailableNotifier = (ctx: PlaywrightUnavailableContext) => void;
|
|
84
|
+
|
|
85
|
+
let _playwrightUnavailableNotifier: PlaywrightUnavailableNotifier | undefined;
|
|
86
|
+
const _playwrightUnavailableLastNotifiedAt = new Map<string, number>();
|
|
87
|
+
const _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS = 5 * 60_000;
|
|
88
|
+
|
|
89
|
+
const _playwrightUnavailableThisTurn = new Set<string>();
|
|
90
|
+
|
|
91
|
+
function _playwrightUnavailableKey(userId: string, topic: string | undefined): string {
|
|
92
|
+
return `${userId}:${topic ?? ""}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Register the host's "playwright unavailable" alert sink. Called once at bot
|
|
97
|
+
* startup. Idempotent — last writer wins.
|
|
98
|
+
*/
|
|
99
|
+
export function setPlaywrightUnavailableNotifier(cb: PlaywrightUnavailableNotifier): void {
|
|
100
|
+
_playwrightUnavailableNotifier = cb;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Mark the current turn as missing long-lived Playwright and notify the host.
|
|
105
|
+
* The playwright catalog entry calls this when it is built without a port, but
|
|
106
|
+
* route-level callers can call it earlier after ensurePlaywright fails so the
|
|
107
|
+
* system prompt for the same turn can include the unavailable reminder.
|
|
108
|
+
*/
|
|
109
|
+
export function markPlaywrightUnavailable(ctx: PlaywrightUnavailableContext): void {
|
|
110
|
+
_markPlaywrightUnavailable(ctx);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _markPlaywrightUnavailable(ctx: PlaywrightUnavailableContext): void {
|
|
114
|
+
// Mark unavailable for the next system-prompt build, regardless of
|
|
115
|
+
// notifier presence (the model-side reminder must always fire so the
|
|
116
|
+
// agent doesn't try to call a missing tool).
|
|
117
|
+
_playwrightUnavailableThisTurn.add(_playwrightUnavailableKey(ctx.userId, ctx.topic));
|
|
118
|
+
|
|
119
|
+
if (!_playwrightUnavailableNotifier) return;
|
|
120
|
+
const key = _playwrightUnavailableKey(ctx.userId, ctx.topic);
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const last = _playwrightUnavailableLastNotifiedAt.get(key) ?? 0;
|
|
123
|
+
if (now - last < _PLAYWRIGHT_UNAVAILABLE_COOLDOWN_MS) return;
|
|
124
|
+
_playwrightUnavailableLastNotifiedAt.set(key, now);
|
|
125
|
+
try {
|
|
126
|
+
_playwrightUnavailableNotifier(ctx);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
logger.warn({ err }, "playwright unavailable notifier threw");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read + clear the "playwright unavailable this turn" flag for a
|
|
134
|
+
* (user, group, topic). Used by the system-prompt builder right before
|
|
135
|
+
* the agent query starts. Single consumer pattern — calling more than
|
|
136
|
+
* once per turn returns `false` for the second caller (intended; the
|
|
137
|
+
* reminder only needs to ride one prompt).
|
|
138
|
+
*/
|
|
139
|
+
export function consumePlaywrightUnavailable(userId: string, topic: string | undefined): boolean {
|
|
140
|
+
const key = _playwrightUnavailableKey(userId, topic);
|
|
141
|
+
if (!_playwrightUnavailableThisTurn.has(key)) return false;
|
|
142
|
+
_playwrightUnavailableThisTurn.delete(key);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Single source of truth for MCP server registration.
|
|
148
|
+
*
|
|
149
|
+
* Each entry declares which scopes it participates in (`dm`, `forum`, `fork`, `cron`)
|
|
150
|
+
* and how to build its spawn config given a context. Public helpers
|
|
151
|
+
* (`getDmMcpServers`, `getForumMcpServers`, `getForkMcpServers`) and the
|
|
152
|
+
* derived `ALL_FORUM_MCP_SERVER_NAMES` / `REQUIRED_FORUM_MCP_SERVERS`
|
|
153
|
+
* constants all read from this catalog, so adding a new MCP server is a
|
|
154
|
+
* one-line append here instead of touching three places.
|
|
155
|
+
*
|
|
156
|
+
* Conventions:
|
|
157
|
+
* - `scopes` lists every scope the server should appear in by default.
|
|
158
|
+
* - `forumRequired: true` keeps the server even when a forum topic
|
|
159
|
+
* restricts the enabled list via `configure_mcp` (forum scope only).
|
|
160
|
+
* - `build(ctx)` returns the raw transport config (Claude-SDK shape).
|
|
161
|
+
* Codex callers funnel through `toCodexMcpServers` separately.
|
|
162
|
+
*
|
|
163
|
+
* Insertion order is preserved by `Object.entries`, which determines the
|
|
164
|
+
* order of server names in user-facing displays.
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
export type RuntimeMcpScope = "dm" | "forum" | "fork" | "manager" | "cron";
|
|
168
|
+
|
|
169
|
+
export interface RuntimeMcpBuildContext {
|
|
170
|
+
userId: string;
|
|
171
|
+
/** "dm" for DM scope, topic/session name for forum/fork. */
|
|
172
|
+
session: string;
|
|
173
|
+
/** REST/WS topic id when known. Prefer for authorization/scope checks. */
|
|
174
|
+
topicId?: string;
|
|
175
|
+
/** REST/WS query id when known. */
|
|
176
|
+
queryId?: string;
|
|
177
|
+
/** Topic id whose wiki memory should be read/written. */
|
|
178
|
+
wikiTopicId?: string;
|
|
179
|
+
agent?: AgentKind;
|
|
180
|
+
cwd?: string;
|
|
181
|
+
model?: string;
|
|
182
|
+
currentUserPrompt?: string;
|
|
183
|
+
depth?: number;
|
|
184
|
+
playwrightPort?: number;
|
|
185
|
+
playwrightCapability?: string;
|
|
186
|
+
bgBashPort?: number;
|
|
187
|
+
autoContinue?: boolean;
|
|
188
|
+
silent?: boolean;
|
|
189
|
+
peerBridge?: PeerRuntimeBridgeContext;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface RuntimeMcpCatalogEntry {
|
|
193
|
+
scopes: readonly RuntimeMcpScope[];
|
|
194
|
+
/** Forum scope only: cannot be removed via the enabled whitelist. */
|
|
195
|
+
forumRequired?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Returns the raw transport spec (Claude-SDK shape), or `null` to
|
|
198
|
+
* signal "this server is unavailable for this turn — omit it from the
|
|
199
|
+
* catalog entirely." Used by the playwright entry when the manager
|
|
200
|
+
* cannot provide a long-lived port.
|
|
201
|
+
*/
|
|
202
|
+
build(ctx: RuntimeMcpBuildContext): Record<string, unknown> | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Playwright transport builders ---
|
|
206
|
+
|
|
207
|
+
export const CODEX_BROWSER_CAPABILITY_ENV = "NEGOTIUM_BROWSER_CAPABILITY";
|
|
208
|
+
|
|
209
|
+
export function browserOwnerCapability(capability: string, owner: string): string {
|
|
210
|
+
return createHmac("sha256", capability).update(owner).digest("hex");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function browserOwnerForContext(ctx: {
|
|
214
|
+
userId?: string;
|
|
215
|
+
session?: string;
|
|
216
|
+
topicId?: string;
|
|
217
|
+
}): string | undefined {
|
|
218
|
+
if (ctx.topicId) return `topic:${ctx.topicId}`;
|
|
219
|
+
if (ctx.userId && ctx.session) return `user:${ctx.userId}:${ctx.session}`;
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function playwrightTransport(port: number, owner: string, capability: string, agent?: AgentKind) {
|
|
224
|
+
const ownerCapability = browserOwnerCapability(capability, owner);
|
|
225
|
+
if (agent === "codex") {
|
|
226
|
+
return {
|
|
227
|
+
url: `http://127.0.0.1:${port}/mcp`,
|
|
228
|
+
http_headers: { "X-Browser-Owner": owner },
|
|
229
|
+
env_http_headers: { "X-Browser-Capability": CODEX_BROWSER_CAPABILITY_ENV },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (agent === "maestro") {
|
|
233
|
+
const query = new URLSearchParams({ owner, capability: ownerCapability });
|
|
234
|
+
return {
|
|
235
|
+
type: "sse" as const,
|
|
236
|
+
url: `http://127.0.0.1:${port}/sse?${query}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
type: "sse" as const,
|
|
241
|
+
url: `http://127.0.0.1:${port}/sse`,
|
|
242
|
+
headers: { "X-Browser-Owner": owner, "X-Browser-Capability": ownerCapability },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function longLivedHttpMcp(agent: AgentKind | undefined, port: number) {
|
|
247
|
+
return agent === "codex"
|
|
248
|
+
? { url: `http://127.0.0.1:${port}/mcp` }
|
|
249
|
+
: { type: "sse" as const, url: `http://127.0.0.1:${port}/sse` };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- Catalog ---
|
|
253
|
+
|
|
254
|
+
const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
255
|
+
playwright: {
|
|
256
|
+
scopes: ["dm", "forum", "fork", "cron"],
|
|
257
|
+
build({ userId, session, topicId, playwrightPort, playwrightCapability, agent }) {
|
|
258
|
+
// Codex uses streamable HTTP while Claude and Maestro use SSE. Both
|
|
259
|
+
// transports terminate at the same long-lived browser/profile server.
|
|
260
|
+
if (playwrightPort && playwrightCapability) {
|
|
261
|
+
const owner = browserOwnerForContext({ userId, session, topicId });
|
|
262
|
+
if (!owner) return null;
|
|
263
|
+
return playwrightTransport(playwrightPort, owner, playwrightCapability, agent);
|
|
264
|
+
}
|
|
265
|
+
// No port available — the playwright manager could not allocate one
|
|
266
|
+
// for this turn. The previous behavior was to fall back to a per-turn
|
|
267
|
+
// stdio chromium, but that made Chromium a child of the agent CLI
|
|
268
|
+
// process tree, so cascade-kill on agent exit took the browser down
|
|
269
|
+
// with it (visible in pm2 as "Chromium died with claude"). We now
|
|
270
|
+
// omit the playwright entry from this turn's catalog AND fire two
|
|
271
|
+
// side-effects:
|
|
272
|
+
// 1. notify the host so it can push an alert into the current
|
|
273
|
+
// topic (rate-limited 5min per scope key);
|
|
274
|
+
// 2. mark the (user, topic) as "playwright unavailable
|
|
275
|
+
// this turn" so the system-prompt builder can tell the model
|
|
276
|
+
// explicitly, instead of letting the model hallucinate a call
|
|
277
|
+
// to a tool whose schema silently vanished.
|
|
278
|
+
_markPlaywrightUnavailable({
|
|
279
|
+
userId,
|
|
280
|
+
topic: session,
|
|
281
|
+
agent,
|
|
282
|
+
});
|
|
283
|
+
return null;
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
[RUNTIME_MCP_KEY]: {
|
|
287
|
+
scopes: ["forum", "manager", "fork", "cron"],
|
|
288
|
+
forumRequired: true,
|
|
289
|
+
build({
|
|
290
|
+
userId,
|
|
291
|
+
session,
|
|
292
|
+
topicId,
|
|
293
|
+
queryId,
|
|
294
|
+
agent,
|
|
295
|
+
cwd,
|
|
296
|
+
model,
|
|
297
|
+
currentUserPrompt,
|
|
298
|
+
autoContinue,
|
|
299
|
+
peerBridge,
|
|
300
|
+
}) {
|
|
301
|
+
if (!topicId || !agent) return null;
|
|
302
|
+
return buildRuntimeMcpSpec(agent, {
|
|
303
|
+
userId,
|
|
304
|
+
topicId,
|
|
305
|
+
topicTitle: session,
|
|
306
|
+
queryId,
|
|
307
|
+
cwd: cwd ?? resolveTopicWorkspaceDir(topicId),
|
|
308
|
+
agent,
|
|
309
|
+
model,
|
|
310
|
+
currentUserPrompt,
|
|
311
|
+
autoContinue,
|
|
312
|
+
peerBridge,
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
paddleocr: {
|
|
317
|
+
scopes: ["dm", "forum", "fork", "cron"],
|
|
318
|
+
build({ agent }) {
|
|
319
|
+
return buildStdioMcpServer(agent, PADDLEOCR_SERVER, []);
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
"token-stats": {
|
|
323
|
+
scopes: ["dm", "forum", "manager", "cron"],
|
|
324
|
+
forumRequired: true,
|
|
325
|
+
build({ userId, agent }) {
|
|
326
|
+
return buildStdioMcpServer(agent, TOKEN_STATS_SERVER, [`--user-id=${userId}`]);
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
// Otium-owned shared task system. This is the only authoritative task/todo
|
|
330
|
+
// surface across claude/codex/maestro; provider-native task stores are
|
|
331
|
+
// blocked or ignored because they do not survive agent switches.
|
|
332
|
+
task: {
|
|
333
|
+
scopes: ["dm", "forum", "manager", "cron"],
|
|
334
|
+
forumRequired: true,
|
|
335
|
+
build({ userId, session, topicId, queryId, agent, peerBridge }) {
|
|
336
|
+
if (peerBridge) {
|
|
337
|
+
if (!topicId || !queryId) return null;
|
|
338
|
+
const env = canonicalMcpBridgeEnv({
|
|
339
|
+
surface: "task",
|
|
340
|
+
userId,
|
|
341
|
+
topicId,
|
|
342
|
+
queryId,
|
|
343
|
+
peerBridge,
|
|
344
|
+
});
|
|
345
|
+
return env
|
|
346
|
+
? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=task"], env)
|
|
347
|
+
: null;
|
|
348
|
+
}
|
|
349
|
+
const args = [`--user-id=${userId}`, `--topic=${session}`];
|
|
350
|
+
if (topicId) args.push(`--topic-id=${topicId}`);
|
|
351
|
+
return buildStdioMcpServer(agent, TASK_SERVER, args);
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
"session-comm": {
|
|
355
|
+
// Exposed in `manager` scope as well so the General-topic manager agent
|
|
356
|
+
// can wake fresh-created topics via tell_session/ask_session right after
|
|
357
|
+
// create_topic. Without this, a newly-created topic would stay in the
|
|
358
|
+
// "fresh-start ready" state until the user manually visits it.
|
|
359
|
+
scopes: ["forum", "fork", "manager"],
|
|
360
|
+
forumRequired: true,
|
|
361
|
+
build({ userId, session, topicId, agent, depth = 0, silent, peerBridge }) {
|
|
362
|
+
const effectiveAgent = agent ?? FALLBACK_AGENT;
|
|
363
|
+
const args = [
|
|
364
|
+
`--user-id=${userId}`,
|
|
365
|
+
`--topic=${session}`,
|
|
366
|
+
...(topicId ? [`--topic-id=${topicId}`] : []),
|
|
367
|
+
`--depth=${depth}`,
|
|
368
|
+
`--agent=${effectiveAgent}`,
|
|
369
|
+
...(silent ? ["--reply-only=true"] : []),
|
|
370
|
+
...(peerBridge ? [`--peer-host-query-id=${peerBridge.hostQueryId}`] : []),
|
|
371
|
+
];
|
|
372
|
+
return buildStdioMcpServer(
|
|
373
|
+
effectiveAgent,
|
|
374
|
+
SESSION_COMM_SERVER,
|
|
375
|
+
args,
|
|
376
|
+
peerBridge ? peerSessionBridgeIpcEnv() : undefined,
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
wiki: {
|
|
381
|
+
scopes: ["dm", "forum", "manager", "cron"],
|
|
382
|
+
forumRequired: true,
|
|
383
|
+
build({ userId, session, topicId, queryId, wikiTopicId, agent, peerBridge }) {
|
|
384
|
+
if (peerBridge) {
|
|
385
|
+
if (!topicId || !queryId) return null;
|
|
386
|
+
const env = canonicalMcpBridgeEnv({
|
|
387
|
+
surface: "wiki",
|
|
388
|
+
userId,
|
|
389
|
+
topicId,
|
|
390
|
+
queryId,
|
|
391
|
+
peerBridge,
|
|
392
|
+
});
|
|
393
|
+
return env
|
|
394
|
+
? buildStdioMcpServer(agent, CANONICAL_MCP_PROXY_SERVER, ["--surface=wiki"], env)
|
|
395
|
+
: null;
|
|
396
|
+
}
|
|
397
|
+
const args = [`--user-id=${userId}`];
|
|
398
|
+
// Prefer the REST topic id so save_wiki_entry writes filenames and SQLite
|
|
399
|
+
// briefs keyed the same way the topic wiki API reads them.
|
|
400
|
+
const resolvedWikiTopicId =
|
|
401
|
+
wikiTopicId ?? topicId ?? (session !== "dm" ? session : undefined);
|
|
402
|
+
if (resolvedWikiTopicId) args.push(`--topic-id=${resolvedWikiTopicId}`);
|
|
403
|
+
args.push("--surface=wiki");
|
|
404
|
+
return buildStdioMcpServer(agent, WIKI_SERVER, args);
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
skills: {
|
|
408
|
+
scopes: ["dm", "forum", "manager", "cron"],
|
|
409
|
+
forumRequired: true,
|
|
410
|
+
build({ userId, topicId, agent }) {
|
|
411
|
+
const args = [`--user-id=${userId}`, "--surface=skills"];
|
|
412
|
+
if (topicId) args.push(`--topic-id=${topicId}`);
|
|
413
|
+
return buildStdioMcpServer(agent, WIKI_SERVER, args);
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
"system-health": {
|
|
417
|
+
scopes: ["dm", "forum", "manager", "cron"],
|
|
418
|
+
forumRequired: true,
|
|
419
|
+
build({ agent }) {
|
|
420
|
+
return buildStdioMcpServer(agent, SYSTEM_HEALTH_SERVER, []);
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
"background-bash": {
|
|
424
|
+
scopes: ["forum"],
|
|
425
|
+
build({ agent, bgBashPort }) {
|
|
426
|
+
if (bgBashPort === undefined) return null;
|
|
427
|
+
return longLivedHttpMcp(agent, bgBashPort);
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
"agent-health": {
|
|
431
|
+
scopes: ["forum", "manager", "cron"],
|
|
432
|
+
forumRequired: true,
|
|
433
|
+
build({ userId, agent }) {
|
|
434
|
+
const args = [`--user-id=${userId}`];
|
|
435
|
+
return buildStdioMcpServer(agent, AGENT_HEALTH_SERVER, args);
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
vault: {
|
|
439
|
+
scopes: ["dm", "forum", "manager", "cron"],
|
|
440
|
+
// Credential references must never dead-end because a topic whitelist
|
|
441
|
+
// omitted the broker while provider hooks correctly block raw expansion.
|
|
442
|
+
forumRequired: true,
|
|
443
|
+
build({ userId, agent }) {
|
|
444
|
+
const args = [`--user-id=${userId}`];
|
|
445
|
+
// Codex has no host-side PostToolUse redaction. Keep its Vault surface
|
|
446
|
+
// broker-only and non-persistent: HTTPS responses are scrubbed before the
|
|
447
|
+
// MCP result reaches Codex, while arbitrary shell could write a secret to
|
|
448
|
+
// disk and reveal it through a later Read.
|
|
449
|
+
if (agent === "codex") args.push("--http-only=true");
|
|
450
|
+
return buildStdioMcpServer(agent, VAULT_SERVER, args);
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// --- Derived catalog views ---
|
|
456
|
+
|
|
457
|
+
function namesInScope(scope: RuntimeMcpScope): string[] {
|
|
458
|
+
return Object.entries(MCP_CATALOG)
|
|
459
|
+
.filter(([, e]) => e.scopes.includes(scope))
|
|
460
|
+
.map(([name]) => name);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** All forum-eligible MCP server names, in display order. */
|
|
464
|
+
const allForumMcpServerNames: string[] = [];
|
|
465
|
+
export const ALL_FORUM_MCP_SERVER_NAMES: readonly string[] = allForumMcpServerNames;
|
|
466
|
+
|
|
467
|
+
/** Forum servers that cannot be removed via the enabled whitelist. */
|
|
468
|
+
const requiredForumMcpServers: string[] = [];
|
|
469
|
+
export const REQUIRED_FORUM_MCP_SERVERS: readonly string[] = requiredForumMcpServers;
|
|
470
|
+
|
|
471
|
+
/** Forum servers that can be toggled via the enabled whitelist (not required). */
|
|
472
|
+
const optionalForumMcpServers: string[] = [];
|
|
473
|
+
export const OPTIONAL_FORUM_MCP_SERVERS: readonly string[] = optionalForumMcpServers;
|
|
474
|
+
|
|
475
|
+
function refreshForumCatalogViews(): void {
|
|
476
|
+
const all = namesInScope("forum");
|
|
477
|
+
const required = Object.entries(MCP_CATALOG)
|
|
478
|
+
.filter(([, entry]) => entry.scopes.includes("forum") && entry.forumRequired)
|
|
479
|
+
.map(([name]) => name);
|
|
480
|
+
allForumMcpServerNames.splice(0, allForumMcpServerNames.length, ...all);
|
|
481
|
+
requiredForumMcpServers.splice(0, requiredForumMcpServers.length, ...required);
|
|
482
|
+
optionalForumMcpServers.splice(
|
|
483
|
+
0,
|
|
484
|
+
optionalForumMcpServers.length,
|
|
485
|
+
...all.filter((name) => !required.includes(name)),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
refreshForumCatalogViews();
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Mount one MCP capability supplied by an optional node module.
|
|
493
|
+
*
|
|
494
|
+
* Registration happens once at node startup, never in the per-event hot path.
|
|
495
|
+
* The returned cleanup closure only removes the exact entry installed by this
|
|
496
|
+
* call, making module stop/restart safe.
|
|
497
|
+
*/
|
|
498
|
+
export function registerRuntimeMcpServer(name: string, entry: RuntimeMcpCatalogEntry): () => void {
|
|
499
|
+
const key = name.trim();
|
|
500
|
+
if (!key || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(key)) {
|
|
501
|
+
throw new Error(`invalid runtime MCP server name: ${name}`);
|
|
502
|
+
}
|
|
503
|
+
if (MCP_CATALOG[key]) throw new Error(`runtime MCP server already registered: ${key}`);
|
|
504
|
+
MCP_CATALOG[key] = entry;
|
|
505
|
+
refreshForumCatalogViews();
|
|
506
|
+
return () => {
|
|
507
|
+
if (MCP_CATALOG[key] !== entry) return;
|
|
508
|
+
delete MCP_CATALOG[key];
|
|
509
|
+
refreshForumCatalogViews();
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Format the active MCP config for display (used by get_mcp_config /
|
|
515
|
+
* get_topic_mcp / configure_mcp response).
|
|
516
|
+
*
|
|
517
|
+
* In whitelist mode the DB stores only the user-chosen optional servers;
|
|
518
|
+
* required servers are always active but never written to the whitelist.
|
|
519
|
+
* This helper surfaces that split so the model never confuses "not in
|
|
520
|
+
* whitelist" with "not active".
|
|
521
|
+
*/
|
|
522
|
+
export function formatMcpStatus(config: {
|
|
523
|
+
enabled: string[] | null;
|
|
524
|
+
extra?: Record<string, unknown>;
|
|
525
|
+
}): string[] {
|
|
526
|
+
const extraNames = Object.keys(config.extra ?? {});
|
|
527
|
+
|
|
528
|
+
if (config.enabled === null) {
|
|
529
|
+
const total = ALL_FORUM_MCP_SERVER_NAMES.length + extraNames.length;
|
|
530
|
+
return [
|
|
531
|
+
`설정 방식: 기본값 (전체 활성)`,
|
|
532
|
+
`활성 서버 (${total}개): ${[...ALL_FORUM_MCP_SERVER_NAMES, ...extraNames].join(", ")}`,
|
|
533
|
+
`추가 서버: ${extraNames.length > 0 ? extraNames.join(", ") : "없음"}`,
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// whitelist mode: stored list may or may not include required servers
|
|
538
|
+
// (old entries might have them; new entries won't). Either way, split
|
|
539
|
+
// by required vs optional for display.
|
|
540
|
+
const optionalActive = config.enabled.filter((n) => !REQUIRED_FORUM_MCP_SERVERS.includes(n));
|
|
541
|
+
const total = REQUIRED_FORUM_MCP_SERVERS.length + optionalActive.length + extraNames.length;
|
|
542
|
+
return [
|
|
543
|
+
`설정 방식: whitelist`,
|
|
544
|
+
`필수 서버 (항상 활성, ${REQUIRED_FORUM_MCP_SERVERS.length}개): ${REQUIRED_FORUM_MCP_SERVERS.join(", ")}`,
|
|
545
|
+
`선택 서버 (whitelist, ${optionalActive.length}개): ${optionalActive.length > 0 ? optionalActive.join(", ") : "없음"}`,
|
|
546
|
+
`실제 활성 합계: ${total}개`,
|
|
547
|
+
`추가 서버: ${extraNames.length > 0 ? extraNames.join(", ") : "없음"}`,
|
|
548
|
+
``,
|
|
549
|
+
`선택 가능 서버 전체 (${OPTIONAL_FORUM_MCP_SERVERS.length}개): ${OPTIONAL_FORUM_MCP_SERVERS.join(", ")}`,
|
|
550
|
+
];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// --- Builders for each scope ---
|
|
554
|
+
|
|
555
|
+
// ── Node-assigned MCP servers (mcp-host manifest) ───────────────────
|
|
556
|
+
//
|
|
557
|
+
// The per-node MCP manifest (`negotium mcp add`) lives in @negotium/mcp-host,
|
|
558
|
+
// which depends on core — so core cannot read it directly. Instead the host
|
|
559
|
+
// process resolves the manifest at node start (ensuring long-lived http
|
|
560
|
+
// servers are running and have ports) and installs the result here. Entries
|
|
561
|
+
// then ride every eligible scope's catalog, subject to the same per-topic
|
|
562
|
+
// `enabled` whitelist as optional built-ins.
|
|
563
|
+
|
|
564
|
+
export type NodeMcpEntry =
|
|
565
|
+
| { key: string; kind: "http"; port: number }
|
|
566
|
+
| { key: string; kind: "stdio"; command: string; args?: string[]; env?: Record<string, string> };
|
|
567
|
+
|
|
568
|
+
let nodeMcpEntries: NodeMcpEntry[] = [];
|
|
569
|
+
|
|
570
|
+
/** Install the node's assigned MCP servers. Last writer wins (host restart-safe). */
|
|
571
|
+
export function setNodeMcpServers(entries: NodeMcpEntry[]): void {
|
|
572
|
+
nodeMcpEntries = entries.filter((entry) => !(entry.key in MCP_CATALOG));
|
|
573
|
+
const dropped = entries.length - nodeMcpEntries.length;
|
|
574
|
+
if (dropped > 0) {
|
|
575
|
+
logger.warn({ dropped }, "setNodeMcpServers: entries shadowing built-in catalog keys ignored");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function getNodeMcpServers(): readonly NodeMcpEntry[] {
|
|
580
|
+
return nodeMcpEntries;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function buildNodeMcpSpecs(
|
|
584
|
+
agent: AgentKind | undefined,
|
|
585
|
+
filter: (name: string) => boolean,
|
|
586
|
+
): Record<string, unknown> {
|
|
587
|
+
const out: Record<string, unknown> = {};
|
|
588
|
+
for (const entry of nodeMcpEntries) {
|
|
589
|
+
if (!filter(entry.key)) continue;
|
|
590
|
+
out[entry.key] =
|
|
591
|
+
entry.kind === "http"
|
|
592
|
+
? longLivedHttpMcp(agent, entry.port)
|
|
593
|
+
: {
|
|
594
|
+
command: entry.command,
|
|
595
|
+
args: entry.args ?? [],
|
|
596
|
+
...(entry.env ? { env: entry.env } : {}),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function buildScope(
|
|
603
|
+
scope: RuntimeMcpScope,
|
|
604
|
+
ctx: RuntimeMcpBuildContext,
|
|
605
|
+
filter: (name: string) => boolean = () => true,
|
|
606
|
+
): Record<string, unknown> {
|
|
607
|
+
const out: Record<string, unknown> = {};
|
|
608
|
+
for (const [name, entry] of Object.entries(MCP_CATALOG)) {
|
|
609
|
+
if (!entry.scopes.includes(scope)) continue;
|
|
610
|
+
if (!filter(name)) continue;
|
|
611
|
+
// `null` from build() means "skip this server for this turn" —
|
|
612
|
+
// currently used by the playwright entry when no long-lived port
|
|
613
|
+
// is available.
|
|
614
|
+
const spec = entry.build(ctx);
|
|
615
|
+
if (spec === null) continue;
|
|
616
|
+
out[name] = spec;
|
|
617
|
+
}
|
|
618
|
+
// Node-assigned MCPs join every scope except cron (scheduled runs keep the
|
|
619
|
+
// deliberately narrow built-in set).
|
|
620
|
+
if (scope !== "cron") {
|
|
621
|
+
Object.assign(out, buildNodeMcpSpecs(ctx.agent, filter));
|
|
622
|
+
}
|
|
623
|
+
return out;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** DM session: catalog entries with `dm` scope, no whitelist. */
|
|
627
|
+
export function getDmMcpServers(opts: {
|
|
628
|
+
userId: string;
|
|
629
|
+
agent?: AgentKind;
|
|
630
|
+
playwrightPort?: number;
|
|
631
|
+
playwrightCapability?: string;
|
|
632
|
+
}) {
|
|
633
|
+
return buildScope("dm", {
|
|
634
|
+
userId: opts.userId,
|
|
635
|
+
session: "dm",
|
|
636
|
+
agent: opts.agent,
|
|
637
|
+
playwrightPort: opts.playwrightPort,
|
|
638
|
+
playwrightCapability: opts.playwrightCapability,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Manager session: lives in the caller's private General API topic. Owns topic CRUD,
|
|
644
|
+
* invite/user admin, and cross-topic MCP toggling through `topic-admin`,
|
|
645
|
+
* plus general utilities. Browser/OCR work belongs in dedicated topics, so
|
|
646
|
+
* heavyweight Playwright/PaddleOCR tools are intentionally excluded here.
|
|
647
|
+
* No whitelist — manager-scope catalog entries are loaded as-is.
|
|
648
|
+
*/
|
|
649
|
+
export function getManagerMcpServers(opts: {
|
|
650
|
+
userId: string;
|
|
651
|
+
session?: string;
|
|
652
|
+
topicId?: string;
|
|
653
|
+
queryId?: string;
|
|
654
|
+
wikiTopicId?: string;
|
|
655
|
+
agent?: AgentKind;
|
|
656
|
+
cwd?: string;
|
|
657
|
+
model?: string;
|
|
658
|
+
currentUserPrompt?: string;
|
|
659
|
+
playwrightPort?: number;
|
|
660
|
+
playwrightCapability?: string;
|
|
661
|
+
autoContinue?: boolean;
|
|
662
|
+
}) {
|
|
663
|
+
if (!opts.topicId) {
|
|
664
|
+
throw new Error("getManagerMcpServers: private General topicId is required");
|
|
665
|
+
}
|
|
666
|
+
const topicId = opts.topicId;
|
|
667
|
+
return buildScope("manager", {
|
|
668
|
+
userId: opts.userId,
|
|
669
|
+
session: opts.session ?? "General",
|
|
670
|
+
topicId,
|
|
671
|
+
queryId: opts.queryId,
|
|
672
|
+
wikiTopicId: opts.wikiTopicId ?? topicId,
|
|
673
|
+
agent: opts.agent,
|
|
674
|
+
cwd: opts.cwd,
|
|
675
|
+
model: opts.model,
|
|
676
|
+
currentUserPrompt: opts.currentUserPrompt,
|
|
677
|
+
playwrightPort: opts.playwrightPort,
|
|
678
|
+
playwrightCapability: opts.playwrightCapability,
|
|
679
|
+
autoContinue: opts.autoContinue,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Forum session: catalog entries with `forum` scope.
|
|
685
|
+
* - `enabled === null` → all forum servers.
|
|
686
|
+
* - `enabled` is an array → whitelist; required servers are always included.
|
|
687
|
+
* - `extra` is merged on top, allowing one-off injections per topic.
|
|
688
|
+
*/
|
|
689
|
+
export function getForumMcpServers(opts: {
|
|
690
|
+
userId: string;
|
|
691
|
+
session: string;
|
|
692
|
+
topicId?: string;
|
|
693
|
+
queryId?: string;
|
|
694
|
+
wikiTopicId?: string;
|
|
695
|
+
agent: AgentKind;
|
|
696
|
+
cwd?: string;
|
|
697
|
+
model?: string;
|
|
698
|
+
currentUserPrompt?: string;
|
|
699
|
+
playwrightPort?: number;
|
|
700
|
+
playwrightCapability?: string;
|
|
701
|
+
bgBashPort?: number;
|
|
702
|
+
autoContinue?: boolean;
|
|
703
|
+
depth?: number;
|
|
704
|
+
enabled?: string[] | null;
|
|
705
|
+
extra?: Record<string, unknown>;
|
|
706
|
+
silent?: boolean;
|
|
707
|
+
peerBridge?: PeerRuntimeBridgeContext;
|
|
708
|
+
}) {
|
|
709
|
+
const {
|
|
710
|
+
userId,
|
|
711
|
+
session,
|
|
712
|
+
topicId,
|
|
713
|
+
queryId,
|
|
714
|
+
wikiTopicId,
|
|
715
|
+
agent,
|
|
716
|
+
cwd,
|
|
717
|
+
model,
|
|
718
|
+
currentUserPrompt,
|
|
719
|
+
playwrightPort,
|
|
720
|
+
playwrightCapability,
|
|
721
|
+
depth = 0,
|
|
722
|
+
enabled = null,
|
|
723
|
+
extra = {},
|
|
724
|
+
silent = false,
|
|
725
|
+
bgBashPort,
|
|
726
|
+
autoContinue,
|
|
727
|
+
peerBridge,
|
|
728
|
+
} = opts;
|
|
729
|
+
|
|
730
|
+
const filter = (name: string) => {
|
|
731
|
+
if (silent && name === "task") return false;
|
|
732
|
+
if (enabled === null) return true;
|
|
733
|
+
return (
|
|
734
|
+
enabled.includes(name) || (REQUIRED_FORUM_MCP_SERVERS as readonly string[]).includes(name)
|
|
735
|
+
);
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const base = buildScope(
|
|
739
|
+
"forum",
|
|
740
|
+
{
|
|
741
|
+
userId,
|
|
742
|
+
session,
|
|
743
|
+
topicId,
|
|
744
|
+
queryId,
|
|
745
|
+
wikiTopicId,
|
|
746
|
+
agent,
|
|
747
|
+
cwd,
|
|
748
|
+
model,
|
|
749
|
+
currentUserPrompt,
|
|
750
|
+
depth,
|
|
751
|
+
playwrightPort,
|
|
752
|
+
playwrightCapability,
|
|
753
|
+
bgBashPort,
|
|
754
|
+
autoContinue,
|
|
755
|
+
silent,
|
|
756
|
+
peerBridge,
|
|
757
|
+
},
|
|
758
|
+
filter,
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
return { ...base, ...extra };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Cron session: scheduled background run for a concrete API topic. Keep the
|
|
766
|
+
* catalog narrower than forum scope: runtime delivery, topic memory/status,
|
|
767
|
+
* OCR/vault utilities, and a caller-provided disposable Playwright port.
|
|
768
|
+
*/
|
|
769
|
+
export function getCronMcpServers(opts: {
|
|
770
|
+
userId: string;
|
|
771
|
+
session: string;
|
|
772
|
+
topicId: string;
|
|
773
|
+
queryId?: string;
|
|
774
|
+
wikiTopicId?: string;
|
|
775
|
+
agent: AgentKind;
|
|
776
|
+
cwd?: string;
|
|
777
|
+
model?: string;
|
|
778
|
+
currentUserPrompt?: string;
|
|
779
|
+
playwrightPort?: number;
|
|
780
|
+
playwrightCapability?: string;
|
|
781
|
+
}) {
|
|
782
|
+
return buildScope("cron", {
|
|
783
|
+
userId: opts.userId,
|
|
784
|
+
session: opts.session,
|
|
785
|
+
topicId: opts.topicId,
|
|
786
|
+
queryId: opts.queryId,
|
|
787
|
+
wikiTopicId: opts.wikiTopicId ?? opts.topicId,
|
|
788
|
+
agent: opts.agent,
|
|
789
|
+
cwd: opts.cwd,
|
|
790
|
+
model: opts.model,
|
|
791
|
+
currentUserPrompt: opts.currentUserPrompt,
|
|
792
|
+
playwrightPort: opts.playwrightPort,
|
|
793
|
+
playwrightCapability: opts.playwrightCapability,
|
|
794
|
+
autoContinue: false,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Pick the right MCP server bundle for an agent query. DM/ephemeral sessions
|
|
800
|
+
* get the DM bundle; forum sessions get the forum bundle with full options
|
|
801
|
+
* forwarded. Returned shape is the Claude-SDK shape; Codex callers funnel it
|
|
802
|
+
* through their own translator (`toCodexMcpServers`).
|
|
803
|
+
*/
|
|
804
|
+
export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, unknown> {
|
|
805
|
+
if (opts.sessionType === "cron") {
|
|
806
|
+
if (!opts.topicId) throw new Error("getMcpServersForQuery: cron sessionType requires topicId");
|
|
807
|
+
return getCronMcpServers({
|
|
808
|
+
userId: opts.userId || "default",
|
|
809
|
+
session: opts.session || "cron",
|
|
810
|
+
topicId: opts.topicId,
|
|
811
|
+
queryId: opts.queryId,
|
|
812
|
+
wikiTopicId: opts.wikiTopicId,
|
|
813
|
+
agent: opts.agent,
|
|
814
|
+
cwd: opts.cwd,
|
|
815
|
+
model: opts.model,
|
|
816
|
+
currentUserPrompt: opts.prompt,
|
|
817
|
+
playwrightPort: opts.playwrightPort,
|
|
818
|
+
playwrightCapability: opts.playwrightCapability,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
if (opts.sessionType === "dm" || opts.sessionType === "ephemeral") {
|
|
822
|
+
return getDmMcpServers({
|
|
823
|
+
userId: opts.userId || "default",
|
|
824
|
+
agent: opts.agent,
|
|
825
|
+
playwrightPort: opts.playwrightPort,
|
|
826
|
+
playwrightCapability: opts.playwrightCapability,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
if (opts.sessionType === "manager") {
|
|
830
|
+
return getManagerMcpServers({
|
|
831
|
+
userId: opts.userId || "default",
|
|
832
|
+
session: opts.session,
|
|
833
|
+
topicId: opts.topicId,
|
|
834
|
+
queryId: opts.queryId,
|
|
835
|
+
wikiTopicId: opts.wikiTopicId,
|
|
836
|
+
agent: opts.agent,
|
|
837
|
+
cwd: opts.cwd,
|
|
838
|
+
model: opts.model,
|
|
839
|
+
currentUserPrompt: opts.prompt,
|
|
840
|
+
playwrightPort: opts.playwrightPort,
|
|
841
|
+
playwrightCapability: opts.playwrightCapability,
|
|
842
|
+
autoContinue: opts.autoContinue,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return getForumMcpServers({
|
|
846
|
+
userId: opts.userId || "default",
|
|
847
|
+
session: opts.session || "default",
|
|
848
|
+
topicId: opts.topicId,
|
|
849
|
+
queryId: opts.queryId,
|
|
850
|
+
wikiTopicId: opts.wikiTopicId,
|
|
851
|
+
agent: opts.agent,
|
|
852
|
+
cwd: opts.cwd,
|
|
853
|
+
model: opts.model,
|
|
854
|
+
currentUserPrompt: opts.prompt,
|
|
855
|
+
playwrightPort: opts.playwrightPort,
|
|
856
|
+
playwrightCapability: opts.playwrightCapability,
|
|
857
|
+
bgBashPort: opts.bgBashPort,
|
|
858
|
+
autoContinue: opts.autoContinue,
|
|
859
|
+
depth: opts.depth,
|
|
860
|
+
enabled: opts.mcpEnabled,
|
|
861
|
+
extra: opts.mcpExtra,
|
|
862
|
+
silent: opts.silent,
|
|
863
|
+
peerBridge: opts.peerBridge,
|
|
864
|
+
});
|
|
865
|
+
}
|