negotium 0.19.2 → 0.20.0
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/dist/agent-helpers.js +1146 -212
- package/dist/agent-helpers.js.map +27 -19
- package/dist/background-bash.js.map +1 -1
- package/dist/browser-runtime.js.map +1 -1
- package/dist/{chunk-kkftbxh2.js → chunk-7he9nzfa.js} +1 -1
- package/dist/{chunk-55dd0xw7.js → chunk-ad6xqvqy.js} +2 -2
- package/dist/{chunk-55dd0xw7.js.map → chunk-ad6xqvqy.js.map} +2 -2
- package/dist/{chunk-exqz8bez.js → chunk-hh3f0yr7.js} +3 -3
- package/dist/{chunk-exqz8bez.js.map → chunk-hh3f0yr7.js.map} +2 -2
- package/dist/{chunk-aqqmzmft.js → chunk-nm59q3em.js} +1 -1
- package/dist/{chunk-cv7vdsrn.js → chunk-w3c6fc0p.js} +286 -9
- package/dist/{chunk-cv7vdsrn.js.map → chunk-w3c6fc0p.js.map} +10 -7
- package/dist/hosted-agent.js +290 -12
- package/dist/hosted-agent.js.map +10 -7
- package/dist/main.js +4599 -2724
- package/dist/main.js.map +49 -38
- package/dist/mcp-factories.js +1166 -162
- package/dist/mcp-factories.js.map +27 -19
- package/dist/media.js.map +1 -1
- package/dist/prompts.js.map +1 -1
- package/dist/query-runtime.js.map +3 -3
- package/dist/registry.js +3 -3
- package/dist/rollout.js +2 -2
- package/dist/runtime/src/agents/explicit-agent-switch.ts +101 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +126 -43
- package/dist/runtime/src/agents/public-helpers.ts +5 -0
- package/dist/runtime/src/agents/self-config-core.ts +28 -58
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +34 -8
- package/dist/runtime/src/index.ts +75 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +6 -1
- package/dist/runtime/src/mcp/runtime-spec.ts +145 -10
- package/dist/runtime/src/mcp/session-comm/actor-policy.ts +87 -0
- package/dist/runtime/src/mcp/session-comm/context.ts +42 -1
- package/dist/runtime/src/mcp/session-comm/default-host.ts +178 -2
- package/dist/runtime/src/mcp/session-comm/hub-remote-session.ts +553 -0
- package/dist/runtime/src/mcp/session-comm/limits.ts +8 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +78 -3
- package/dist/runtime/src/mcp/session-comm/server.ts +167 -4
- package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +7 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +32 -10
- package/dist/runtime/src/mcp-runtime-host.ts +2 -1
- package/dist/runtime/src/node-host.ts +26 -0
- package/dist/runtime/src/platform/mcp-config.ts +56 -2
- package/dist/runtime/src/query/active-rooms.ts +6 -0
- package/dist/runtime/src/runtime/actor-topic-reach.ts +85 -0
- package/dist/runtime/src/runtime/actor-topic-scope.ts +175 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +73 -20
- package/dist/runtime/src/runtime/gateway-capabilities.ts +22 -0
- package/dist/runtime/src/runtime/remote-session-grant.ts +221 -0
- package/dist/runtime/src/runtime/remote-session-inbox.ts +476 -0
- package/dist/runtime/src/runtime/remote-session-reply-outbox.ts +156 -0
- package/dist/runtime/src/runtime/turn-runner.ts +157 -33
- package/dist/runtime/src/runtime-gateway.ts +20 -0
- package/dist/runtime/src/storage/api-topics.ts +22 -3
- package/dist/runtime/src/storage/remote-session.ts +526 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +75 -2
- package/dist/runtime/src/topics/derive.ts +3 -0
- package/dist/runtime/src/types/api.ts +6 -0
- package/dist/runtime/src/types.ts +42 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-gateway.js.map +2 -2
- package/dist/runtime-helpers.js.map +1 -1
- package/dist/storage.js +16 -4
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/agents/explicit-agent-switch.d.ts +26 -0
- package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +12 -2
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +1 -0
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +15 -1
- package/dist/types/packages/core/src/mcp/peer-bridge.d.ts +6 -1
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +69 -2
- package/dist/types/packages/core/src/mcp/session-comm/context.d.ts +17 -1
- package/dist/types/packages/core/src/mcp/session-comm/hub-remote-session.d.ts +149 -0
- package/dist/types/packages/core/src/mcp/session-comm/limits.d.ts +8 -0
- package/dist/types/packages/core/src/mcp/session-comm/peer-forward.d.ts +23 -2
- package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +6 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +10 -1
- package/dist/types/packages/core/src/query/active-rooms.d.ts +6 -0
- package/dist/types/packages/core/src/runtime/actor-topic-reach.d.ts +49 -0
- package/dist/types/packages/core/src/runtime/actor-topic-scope.d.ts +91 -0
- package/dist/types/packages/core/src/runtime/ask-callbacks.d.ts +8 -2
- package/dist/types/packages/core/src/runtime/remote-session-grant.d.ts +91 -0
- package/dist/types/packages/core/src/runtime/remote-session-inbox.d.ts +83 -0
- package/dist/types/packages/core/src/runtime/remote-session-reply-outbox.d.ts +37 -0
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +22 -3
- package/dist/types/packages/core/src/runtime-gateway.d.ts +20 -0
- package/dist/types/packages/core/src/storage/api-topics.d.ts +3 -0
- package/dist/types/packages/core/src/storage/remote-session.d.ts +135 -0
- package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +12 -1
- package/dist/types/packages/core/src/topics/derive.d.ts +2 -0
- package/dist/types/packages/core/src/types/api.d.ts +6 -0
- package/dist/types/packages/core/src/types.d.ts +40 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-kkftbxh2.js.map → chunk-7he9nzfa.js.map} +0 -0
- /package/dist/{chunk-aqqmzmft.js.map → chunk-nm59q3em.js.map} +0 -0
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
isTopicBrowserProfileOwner,
|
|
22
22
|
listBrowserProfiles,
|
|
23
23
|
} from "#storage/browser-profiles";
|
|
24
|
+
import { deleteRemoteSessionAsk, recordRemoteSessionAsk } from "#storage/remote-session";
|
|
24
25
|
import {
|
|
25
26
|
clearPendingAsk,
|
|
26
27
|
createPendingAsk,
|
|
@@ -29,6 +30,14 @@ import {
|
|
|
29
30
|
} from "#storage/session-asks";
|
|
30
31
|
import { enqueueSessionInbox } from "#storage/session-inbox";
|
|
31
32
|
import { connectStdio, mcpError, mcpOk } from "../mcp-helpers";
|
|
33
|
+
import { abortTargetRefusal, remoteSessionRoute } from "./actor-policy";
|
|
34
|
+
import {
|
|
35
|
+
hubRemoteAbort,
|
|
36
|
+
hubRemoteAsk,
|
|
37
|
+
hubRemotePeek,
|
|
38
|
+
hubRemoteSessions,
|
|
39
|
+
hubRemoteTell,
|
|
40
|
+
} from "./hub-remote-session";
|
|
32
41
|
import { forwardToPeer, peerSessionsForUser } from "./peer-forward";
|
|
33
42
|
import {
|
|
34
43
|
cronSessionId,
|
|
@@ -40,6 +49,7 @@ import {
|
|
|
40
49
|
MAX_MESSAGE_LENGTH,
|
|
41
50
|
MAX_TELL_DEPTH,
|
|
42
51
|
peerHostQueryId,
|
|
52
|
+
sessionCommContext,
|
|
43
53
|
subagentParentTopicId,
|
|
44
54
|
userId,
|
|
45
55
|
} from "./runtime";
|
|
@@ -51,6 +61,7 @@ import {
|
|
|
51
61
|
setMcpConfig,
|
|
52
62
|
} from "./topic-config";
|
|
53
63
|
import {
|
|
64
|
+
currentSessionSurface,
|
|
54
65
|
getTopicsForUser,
|
|
55
66
|
listSessionTargetsForUser,
|
|
56
67
|
type QueryState,
|
|
@@ -78,6 +89,11 @@ function remotePeerTarget(to: string): PeerTarget | null {
|
|
|
78
89
|
return { node: to.slice(0, slash), topic: to.slice(slash + 1) };
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
/** Which transport serves `node/topic` targets for this turn (see `remoteSessionRoute`). */
|
|
93
|
+
function currentRemoteRoute() {
|
|
94
|
+
return remoteSessionRoute(currentSessionSurface(), sessionCommContext.remoteSession);
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
// --- MCP Server ---
|
|
82
98
|
|
|
83
99
|
const server = new McpServer({
|
|
@@ -166,11 +182,39 @@ server.tool(
|
|
|
166
182
|
|
|
167
183
|
// Multi-node: append each peer node's rooms, addressed as "node/topic".
|
|
168
184
|
// Disabled or unattached nodes answer with an error — skip silently so
|
|
169
|
-
// single-node behavior is untouched.
|
|
185
|
+
// single-node behavior is untouched. On Otium the remote rooms are the
|
|
186
|
+
// hub's answer for this turn's grant, and none without one (see
|
|
187
|
+
// `remoteSessionRoute`).
|
|
170
188
|
const remoteSections: string[] = [];
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
const route = currentSubagentRestricted ? null : currentRemoteRoute();
|
|
190
|
+
if (route?.kind === "hub") {
|
|
191
|
+
const remote = await hubRemoteSessions(route.grant);
|
|
192
|
+
if (!remote.ok) {
|
|
193
|
+
remoteSections.push(`\nRemote nodes: (${remote.error})`);
|
|
194
|
+
} else {
|
|
195
|
+
for (const node of remote.nodes) {
|
|
196
|
+
if (!node.node) continue;
|
|
197
|
+
if (node.error) {
|
|
198
|
+
remoteSections.push(`\nNode ${node.node}: (unreachable: ${node.error})`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const lines = (node.sessions ?? [])
|
|
202
|
+
.filter((session) => Boolean(session.agent))
|
|
203
|
+
.map((s) => {
|
|
204
|
+
const status = s.status === "active" ? "active" : "fresh-start ready";
|
|
205
|
+
const desc = s.description ? ` — ${s.description.slice(0, 60)}` : "";
|
|
206
|
+
return `- ${node.node}/${s.name}: ${status}${desc}`;
|
|
207
|
+
});
|
|
208
|
+
remoteSections.push(
|
|
209
|
+
`\nNode ${node.node}:\n${lines.join("\n") || " (no rooms for this user)"}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const peers =
|
|
215
|
+
route?.kind !== "peer"
|
|
216
|
+
? { ok: true as const, nodes: [] }
|
|
217
|
+
: await peerSessionsForUser(userId, peerHostQueryId || undefined, currentTopicId);
|
|
174
218
|
if (peers.ok && peers.nodes) {
|
|
175
219
|
for (const node of peers.nodes) {
|
|
176
220
|
if (!node.node) continue;
|
|
@@ -409,6 +453,46 @@ server.tool(
|
|
|
409
453
|
),
|
|
410
454
|
);
|
|
411
455
|
}
|
|
456
|
+
// Remote rooms (v1): the hub's active/ready view only — no elapsed time
|
|
457
|
+
// or task text, since the hub does not fan out to every node for a peek.
|
|
458
|
+
const route = currentSubagentRestricted ? null : currentRemoteRoute();
|
|
459
|
+
if (route?.kind === "hub") {
|
|
460
|
+
const remote = await hubRemotePeek(route.grant);
|
|
461
|
+
if (!remote.ok) {
|
|
462
|
+
lines.push(``, `원격 노드: (${remote.error})`);
|
|
463
|
+
} else {
|
|
464
|
+
const remoteRunning: string[] = [];
|
|
465
|
+
const remoteIdle: string[] = [];
|
|
466
|
+
const unreachable: string[] = [];
|
|
467
|
+
for (const node of remote.nodes) {
|
|
468
|
+
if (node.error) {
|
|
469
|
+
unreachable.push(` ${node.node}: (unreachable: ${node.error})`);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
for (const session of node.sessions ?? []) {
|
|
473
|
+
// Same policy as the listing: a room with no AI is not a session.
|
|
474
|
+
if (session.agent === null) continue;
|
|
475
|
+
(session.status === "active" ? remoteRunning : remoteIdle).push(
|
|
476
|
+
`${node.node}/${session.name}`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
lines.push(
|
|
481
|
+
``,
|
|
482
|
+
`원격 실행 중 (${remoteRunning.length}): ${remoteRunning.join(", ") || "없음"}`,
|
|
483
|
+
`원격 유휴 (${remoteIdle.length}): ${remoteIdle.join(", ") || "없음"}`,
|
|
484
|
+
...unreachable,
|
|
485
|
+
);
|
|
486
|
+
if (remote.pendingAsks.length) {
|
|
487
|
+
lines.push(
|
|
488
|
+
`내 원격 ask_session 대기:`,
|
|
489
|
+
...remote.pendingAsks.map(
|
|
490
|
+
(ask) => ` ${ask.to}: ${ask.status} (request_id: ${ask.requestId})`,
|
|
491
|
+
),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
412
496
|
return mcpOk(lines.join("\n"));
|
|
413
497
|
},
|
|
414
498
|
);
|
|
@@ -456,6 +540,8 @@ if (!isReplyOnly) {
|
|
|
456
540
|
// Remote target ("node/topic") — hand to the runtime's peer forwarder.
|
|
457
541
|
const remote = remotePeerTarget(to);
|
|
458
542
|
if (remote) {
|
|
543
|
+
const route = currentRemoteRoute();
|
|
544
|
+
if (route.kind === "refused") return mcpError(route.error);
|
|
459
545
|
const fromRef = currentTopicRef();
|
|
460
546
|
if (!fromRef.topicId) {
|
|
461
547
|
return mcpError(
|
|
@@ -472,6 +558,40 @@ if (!isReplyOnly) {
|
|
|
472
558
|
`"${to}"에 이미 진행 중인 ask_session 요청이 있습니다: ${detail}. 응답이 이 세션에 자동으로 돌아올 때까지 기다리세요.`,
|
|
473
559
|
);
|
|
474
560
|
}
|
|
561
|
+
if (route.kind === "hub") {
|
|
562
|
+
// Durable caller record first: the hub's `ask-reply` delivery is
|
|
563
|
+
// routed back through it, possibly after this process restarted.
|
|
564
|
+
recordRemoteSessionAsk({
|
|
565
|
+
requestId,
|
|
566
|
+
callerTopicId: fromRef.topicId,
|
|
567
|
+
userId,
|
|
568
|
+
fromKey: fromRef.key,
|
|
569
|
+
toKey: to,
|
|
570
|
+
...(sessionCommContext.currentThreadRootId
|
|
571
|
+
? { callerThreadRootId: sessionCommContext.currentThreadRootId }
|
|
572
|
+
: {}),
|
|
573
|
+
});
|
|
574
|
+
const sent = await hubRemoteAsk(route.grant, {
|
|
575
|
+
requestId,
|
|
576
|
+
to: remote,
|
|
577
|
+
message,
|
|
578
|
+
fromDepth: currentDepth,
|
|
579
|
+
fromLabel: { key: fromRef.key, title: fromRef.title },
|
|
580
|
+
});
|
|
581
|
+
if (!sent.ok) {
|
|
582
|
+
if (sent.uncertain) {
|
|
583
|
+
return mcpOk(
|
|
584
|
+
`"${to}" 세션(노드 ${remote.node})에 참조 요청을 보냈지만 hub의 확인을 받지 못했습니다 (${sent.error}).\n\nrequest_id: ${requestId}\n\n응답이 도착하면 '[Reply from ${remote.node}/${remote.topic}]' 형식으로 이 세션에 돌아옵니다. 같은 요청으로 ask_session을 재호출하지 마세요.`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
clearPendingAsk({ userId, from: fromRef.key, to, requestId });
|
|
588
|
+
deleteRemoteSessionAsk(requestId);
|
|
589
|
+
return mcpError(`Error: "${to}" 원격 세션에 전송 실패: ${sent.error}`);
|
|
590
|
+
}
|
|
591
|
+
return mcpOk(
|
|
592
|
+
`"${to}" 세션(노드 ${remote.node})에 참조 요청을 보냈습니다.\n\nrequest_id: ${requestId}\n\n응답은 '[Reply from ${remote.node}/${remote.topic}]' 형식으로 이 세션에 자동으로 돌아옵니다. 응답이 도착할 때까지 같은 요청으로 ask_session을 재호출하지 마세요.`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
475
595
|
const result = await forwardToPeer({
|
|
476
596
|
action: "ask",
|
|
477
597
|
toNode: remote.node,
|
|
@@ -631,6 +751,16 @@ if (!isReplyOnly) {
|
|
|
631
751
|
async ({ to }) => {
|
|
632
752
|
const remote = remotePeerTarget(to);
|
|
633
753
|
if (remote) {
|
|
754
|
+
const route = currentRemoteRoute();
|
|
755
|
+
if (route.kind === "refused") return mcpError(route.error);
|
|
756
|
+
if (route.kind === "hub") {
|
|
757
|
+
const sent = await hubRemoteAbort(route.grant, {
|
|
758
|
+
requestId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
759
|
+
to: remote,
|
|
760
|
+
});
|
|
761
|
+
if (!sent.ok) return mcpError(`Error: "${to}" 원격 abort 실패: ${sent.error}`);
|
|
762
|
+
return mcpOk(`"${to}" 세션(노드 ${remote.node})에 abort 신호를 보냈습니다.`);
|
|
763
|
+
}
|
|
634
764
|
const result = await forwardToPeer({
|
|
635
765
|
action: "abort",
|
|
636
766
|
toNode: remote.node,
|
|
@@ -656,6 +786,15 @@ if (!isReplyOnly) {
|
|
|
656
786
|
if (!targetTopicId) {
|
|
657
787
|
return mcpError(`Error: "${to}" 세션의 토픽 ID를 찾을 수 없습니다.`);
|
|
658
788
|
}
|
|
789
|
+
// Same owner rule as the hosted host, from the same module.
|
|
790
|
+
const refused = abortTargetRefusal({
|
|
791
|
+
surface: currentSessionSurface(),
|
|
792
|
+
currentTopicId: currentTopicId || undefined,
|
|
793
|
+
actorTopicScope: sessionCommContext.actorTopicScope,
|
|
794
|
+
targetTopicId,
|
|
795
|
+
to,
|
|
796
|
+
});
|
|
797
|
+
if (refused) return mcpError(refused);
|
|
659
798
|
// Send query abort signal via inbox
|
|
660
799
|
enqueueSessionInbox({
|
|
661
800
|
userId,
|
|
@@ -705,8 +844,32 @@ if (!isReplyOnly) {
|
|
|
705
844
|
if (currentSubagentRestricted) {
|
|
706
845
|
return mcpError("Error: subagent tell_session cannot target remote topics.");
|
|
707
846
|
}
|
|
847
|
+
const route = currentRemoteRoute();
|
|
848
|
+
if (route.kind === "refused") return mcpError(route.error);
|
|
708
849
|
const fromRef = currentTopicRef();
|
|
709
850
|
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
851
|
+
if (route.kind === "hub") {
|
|
852
|
+
const sent = await hubRemoteTell(route.grant, {
|
|
853
|
+
requestId,
|
|
854
|
+
to: remote,
|
|
855
|
+
message,
|
|
856
|
+
depth: currentDepth + 1,
|
|
857
|
+
fromLabel: { key: fromRef.key, title: fromRef.title },
|
|
858
|
+
});
|
|
859
|
+
if (!sent.ok) {
|
|
860
|
+
// A timed-out response may still have been delivered; say so
|
|
861
|
+
// rather than invite a duplicate.
|
|
862
|
+
if (sent.uncertain) {
|
|
863
|
+
return mcpOk(
|
|
864
|
+
`"${to}" 토픽(노드 ${remote.node})에 메시지를 보냈지만 hub의 확인을 받지 못했습니다 (${sent.error}).\n\nrequest_id: ${requestId}\n\n같은 메시지를 다시 보내지 마세요.`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
return mcpError(`Error: "${to}" 원격 세션에 전송 실패: ${sent.error}`);
|
|
868
|
+
}
|
|
869
|
+
return mcpOk(
|
|
870
|
+
`"${to}" 토픽(노드 ${remote.node})에 메시지를 전달했습니다.\n\nrequest_id: ${requestId}\n\n결과는 해당 노드의 "${remote.topic}" 대화 기록에 남습니다.`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
710
873
|
const result = await forwardToPeer({
|
|
711
874
|
action: "tell",
|
|
712
875
|
toNode: remote.node,
|
|
@@ -43,6 +43,12 @@ export interface SessionTargetCatalogHost<TAgent extends string = string> {
|
|
|
43
43
|
* surface: terminal with terminal, telegram with telegram, otium with otium.
|
|
44
44
|
*/
|
|
45
45
|
readonly currentSurface?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Drop rows with no AI agent entirely instead of listing them as targets
|
|
48
|
+
* that every messaging tool then refuses. The current room and manager rooms
|
|
49
|
+
* are always dropped; this adds AI-off rooms for surfaces that want it.
|
|
50
|
+
*/
|
|
51
|
+
readonly excludeAgentless?: boolean;
|
|
46
52
|
readonly isAgent: (value: string | null) => value is TAgent;
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -64,6 +70,7 @@ export function createSessionTargetCatalog<TAgent extends string = string>(
|
|
|
64
70
|
const eligibleRows = listRows().filter(
|
|
65
71
|
(row) =>
|
|
66
72
|
row.kind !== "manager" &&
|
|
73
|
+
(!host.excludeAgentless || isAgent(row.agent)) &&
|
|
67
74
|
// Rows predating the surface column report null and stay addressable
|
|
68
75
|
// from the local surface rather than vanishing mid-migration.
|
|
69
76
|
(!currentSurface || (row.surface ?? currentSurface) === currentSurface),
|
|
@@ -8,17 +8,20 @@
|
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { sessionInboxPath } from "#query/session-inbox-path";
|
|
11
|
+
import { actorReachableTopicIds } from "#runtime/actor-topic-reach";
|
|
11
12
|
import { sanitizeTopicName } from "#security/sanitize";
|
|
12
13
|
// NOTE: see `runtime.ts` — import these from `@/types` directly to keep
|
|
13
14
|
// `maestro-agent-sdk` (whose `bootstrapHostPath()` prints to stdout) out of
|
|
14
15
|
// this stdio MCP server's import graph.
|
|
15
16
|
import { type AgentKind, isAgentKind, type QueryState } from "#types";
|
|
17
|
+
import { excludesAgentlessTargets } from "./actor-policy";
|
|
16
18
|
import {
|
|
17
19
|
currentTopic,
|
|
18
20
|
currentTopicId,
|
|
19
21
|
PLAYWRIGHT_PORTS_DIR,
|
|
20
22
|
parseJsonField,
|
|
21
23
|
SESSIONS_DB,
|
|
24
|
+
sessionCommContext,
|
|
22
25
|
userId,
|
|
23
26
|
withDb,
|
|
24
27
|
} from "./runtime";
|
|
@@ -171,23 +174,42 @@ function currentSessionPlacement(): CurrentPlacement {
|
|
|
171
174
|
return cachedSessionPlacement;
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
/** Surface of the room this server serves (undefined when it cannot be read). */
|
|
178
|
+
export function currentSessionSurface(): string | undefined {
|
|
179
|
+
return currentSessionPlacement().surface;
|
|
180
|
+
}
|
|
181
|
+
|
|
174
182
|
const sessionTargetCatalog = createSessionTargetCatalog<AgentKind>({
|
|
175
183
|
currentTopicId,
|
|
176
184
|
currentTopicName: currentTopic,
|
|
177
185
|
get currentSurface() {
|
|
178
186
|
return currentSessionPlacement().surface;
|
|
179
187
|
},
|
|
188
|
+
get excludeAgentless() {
|
|
189
|
+
return excludesAgentlessTargets(currentSessionPlacement().surface);
|
|
190
|
+
},
|
|
180
191
|
isAgent: isAgentKind,
|
|
181
|
-
listRows: () =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
listRows: () => {
|
|
193
|
+
// On `otium` the roster join above matches the hub's execution principal,
|
|
194
|
+
// not the person who spoke, so the hub's per-turn assertion decides which
|
|
195
|
+
// of those rooms this actor may actually name. Fail-closed without one.
|
|
196
|
+
const reachable = actorReachableTopicIds({
|
|
197
|
+
surface: currentSessionPlacement().surface,
|
|
198
|
+
currentTopicId: currentTopicId || undefined,
|
|
199
|
+
actorTopicScope: sessionCommContext.actorTopicScope,
|
|
200
|
+
});
|
|
201
|
+
return sessionTargetRows()
|
|
202
|
+
.filter((row) => !reachable || reachable.has(row.id))
|
|
203
|
+
.map((row) => ({
|
|
204
|
+
id: row.id,
|
|
205
|
+
title: row.title,
|
|
206
|
+
kind: row.kind,
|
|
207
|
+
agent: row.agent,
|
|
208
|
+
sessionId: row.session_id,
|
|
209
|
+
description: row.description,
|
|
210
|
+
surface: row.surface,
|
|
211
|
+
}));
|
|
212
|
+
},
|
|
191
213
|
});
|
|
192
214
|
|
|
193
215
|
export function getMcpUserConfig(): McpUserConfig | null {
|
|
@@ -39,6 +39,7 @@ export { OPTIONAL_FORUM_MCP_SERVERS, REQUIRED_FORUM_MCP_SERVERS } from "#platfor
|
|
|
39
39
|
export { isInsideDir } from "#platform/paths";
|
|
40
40
|
export { abortRoom, getRoomQuery } from "#query/active-rooms";
|
|
41
41
|
export { sessionInboxPath } from "#query/session-inbox-path";
|
|
42
|
+
export { actorOwnedTopicIds, actorReachableTopicIds } from "#runtime/actor-topic-reach";
|
|
42
43
|
export { prepareDeliveryAck } from "#runtime/delivery-ack";
|
|
43
44
|
export { storeLocalFileAsUpload } from "#runtime/file-hooks";
|
|
44
45
|
export {
|
|
@@ -70,6 +71,6 @@ export {
|
|
|
70
71
|
TopicCleanupRequiredError,
|
|
71
72
|
} from "#topics/lifecycle";
|
|
72
73
|
export { restartTopicSession } from "#topics/session";
|
|
73
|
-
export type { EffortLevel } from "#types";
|
|
74
|
+
export type { ActorTopicScope, EffortLevel, RemoteSessionGrant } from "#types";
|
|
74
75
|
export { EFFORT_VALUES } from "#types";
|
|
75
76
|
export type { MessageDto, TopicDto, TopicSurface } from "#types/api";
|
|
@@ -30,6 +30,7 @@ export {
|
|
|
30
30
|
asUserId,
|
|
31
31
|
} from "#identifiers";
|
|
32
32
|
export { setRuntimeMcpPort } from "#mcp/runtime-spec";
|
|
33
|
+
export { MAX_PEER_MESSAGE_LENGTH } from "#mcp/session-comm/limits";
|
|
33
34
|
export { killAllBgBash } from "#platform/background-bash/manager";
|
|
34
35
|
export {
|
|
35
36
|
DATA_DIR,
|
|
@@ -65,6 +66,8 @@ export {
|
|
|
65
66
|
reapPlaywrightOrphans as reapOrphanBrowsers,
|
|
66
67
|
} from "#platform/playwright/manager";
|
|
67
68
|
export { abortAllRooms, listRunningTopicQueries } from "#query/active-rooms";
|
|
69
|
+
export { parseActorTopicScope, validateActorTopicScope } from "#runtime/actor-topic-scope";
|
|
70
|
+
export { failInterruptedRemoteAskCallbacks } from "#runtime/ask-callbacks";
|
|
68
71
|
export { listBackgroundSessionsForUser } from "#runtime/background-sessions";
|
|
69
72
|
export type {
|
|
70
73
|
BashrsCompletion,
|
|
@@ -77,7 +80,22 @@ export {
|
|
|
77
80
|
} from "#runtime/bashrs-completions";
|
|
78
81
|
export type { FileHooks, UploadAccess } from "#runtime/file-hooks";
|
|
79
82
|
export { setFileHooks } from "#runtime/file-hooks";
|
|
83
|
+
export {
|
|
84
|
+
listRuntimeGatewayCapabilities,
|
|
85
|
+
registerRuntimeGatewayCapability,
|
|
86
|
+
} from "#runtime/gateway-capabilities";
|
|
80
87
|
export { startSessionInboxWorker } from "#runtime/inbox";
|
|
88
|
+
export { validateRemoteSessionGrant } from "#runtime/remote-session-grant";
|
|
89
|
+
export {
|
|
90
|
+
deliverRemoteSessionInbox,
|
|
91
|
+
parseRemoteSessionInboxDelivery,
|
|
92
|
+
RemoteSessionInboxError,
|
|
93
|
+
recoverRemoteSessionInbox,
|
|
94
|
+
} from "#runtime/remote-session-inbox";
|
|
95
|
+
export {
|
|
96
|
+
runRemoteSessionMaintenance,
|
|
97
|
+
startRemoteSessionReplyOutboxWorker,
|
|
98
|
+
} from "#runtime/remote-session-reply-outbox";
|
|
81
99
|
export { startAiTurn, startDurableTurnRequestWorker } from "#runtime/turn-runner";
|
|
82
100
|
export type { PortableTopicVisual } from "#runtime/visual-store";
|
|
83
101
|
export { getPortableTopicVisual } from "#runtime/visual-store";
|
|
@@ -99,6 +117,14 @@ export {
|
|
|
99
117
|
export { getTopic, upsertTopic } from "#storage/api-topics";
|
|
100
118
|
export { getGlobalAiName, setGlobalAiName } from "#storage/app-settings";
|
|
101
119
|
export { readDecisions, writeDecisionGraphSvg } from "#storage/decisions";
|
|
120
|
+
export {
|
|
121
|
+
claimRemoteSessionInbox,
|
|
122
|
+
purgeRemoteSessionInboxClaims,
|
|
123
|
+
purgeStaleRemoteSessionAsks,
|
|
124
|
+
releaseRemoteSessionInboxClaim,
|
|
125
|
+
remoteSessionPayloadHash,
|
|
126
|
+
takeRemoteSessionAsk,
|
|
127
|
+
} from "#storage/remote-session";
|
|
102
128
|
export type { StoredRuntimeEvent } from "#storage/runtime-events";
|
|
103
129
|
export {
|
|
104
130
|
earliestRuntimeEventSeq,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants as fsConstants } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { explicitAgentSwitchTargets } from "#agents/explicit-agent-switch";
|
|
4
5
|
|
|
5
6
|
import { canonicalMcpBridgeEnv } from "#mcp/canonical-bridge-config";
|
|
6
7
|
import {
|
|
@@ -38,10 +39,18 @@ import {
|
|
|
38
39
|
buildPlaywrightMcpTransport,
|
|
39
40
|
CODEX_BROWSER_CAPABILITY_ENV,
|
|
40
41
|
} from "#platform/playwright/mcp-transport";
|
|
42
|
+
import { encodeActorTopicScopeArg } from "#runtime/actor-topic-scope";
|
|
41
43
|
import { getRegisteredCronSession } from "#runtime/cron-sessions";
|
|
44
|
+
import { encodeRemoteSessionGrantArg } from "#runtime/remote-session-grant";
|
|
42
45
|
import type { HostMcpServerSpec } from "#runtime-gateway";
|
|
43
46
|
import { getTopic } from "#storage/api-topics";
|
|
44
|
-
import type {
|
|
47
|
+
import type {
|
|
48
|
+
ActorTopicScope,
|
|
49
|
+
AgentKind,
|
|
50
|
+
AgentQueryOptions,
|
|
51
|
+
PeerRuntimeBridgeContext,
|
|
52
|
+
RemoteSessionGrant,
|
|
53
|
+
} from "#types";
|
|
45
54
|
|
|
46
55
|
export type { RuntimeMcpScope } from "#platform/mcp-catalog-policy";
|
|
47
56
|
|
|
@@ -92,6 +101,14 @@ function buildBuiltinMcpServer(
|
|
|
92
101
|
const agent = ctx.agent ?? FALLBACK_AGENT;
|
|
93
102
|
return buildHostedMcpSpec(agent, surface, {
|
|
94
103
|
userId: ctx.userId,
|
|
104
|
+
...(ctx.actorUserId ? { actorUserId: ctx.actorUserId } : {}),
|
|
105
|
+
...(ctx.actorTopicScope ? { actorTopicScope: ctx.actorTopicScope } : {}),
|
|
106
|
+
// Only the session-comm surface addresses remote rooms; the other hosted
|
|
107
|
+
// tokens (and the runtime token) never carry the grant, so it costs them
|
|
108
|
+
// no URL budget.
|
|
109
|
+
...(surface === "session-comm" && ctx.remoteSession
|
|
110
|
+
? { remoteSession: ctx.remoteSession }
|
|
111
|
+
: {}),
|
|
95
112
|
topicTitle: ctx.session,
|
|
96
113
|
...(ctx.topicId ? { topicId: ctx.topicId } : {}),
|
|
97
114
|
...(ctx.queryId ? { queryId: ctx.queryId } : {}),
|
|
@@ -221,6 +238,10 @@ export interface RuntimeMcpBuildContext {
|
|
|
221
238
|
userId: string;
|
|
222
239
|
/** Product actor identity when it differs from the execution principal. */
|
|
223
240
|
actorUserId?: string;
|
|
241
|
+
/** Hub-asserted rooms the product actor may reach; forwarded into signed tokens. */
|
|
242
|
+
actorTopicScope?: ActorTopicScope;
|
|
243
|
+
/** Hub-issued per-turn remote session-comm grant; session-comm only. */
|
|
244
|
+
remoteSession?: RemoteSessionGrant;
|
|
224
245
|
/** Vault namespace when credentials belong to a different principal. */
|
|
225
246
|
vaultUserId?: string;
|
|
226
247
|
/** "dm" for DM scope, topic/session name for forum/fork. */
|
|
@@ -375,6 +396,7 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
375
396
|
build({
|
|
376
397
|
userId,
|
|
377
398
|
actorUserId,
|
|
399
|
+
actorTopicScope,
|
|
378
400
|
session,
|
|
379
401
|
topicId,
|
|
380
402
|
queryId,
|
|
@@ -389,6 +411,7 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
389
411
|
peerBridge,
|
|
390
412
|
}) {
|
|
391
413
|
if (!topicId || !agent) return null;
|
|
414
|
+
const switchTargets = explicitAgentSwitchTargets(currentUserPrompt);
|
|
392
415
|
return buildRuntimeMcpSpec(agent, {
|
|
393
416
|
// Runtime tools mutate node-owned state, so authorization stays bound
|
|
394
417
|
// to the canonical execution principal. Keep the product-side human
|
|
@@ -396,13 +419,17 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
396
419
|
// that appears in this node's topic participant roster.
|
|
397
420
|
userId,
|
|
398
421
|
...(actorUserId ? { actorUserId } : {}),
|
|
422
|
+
...(actorTopicScope ? { actorTopicScope } : {}),
|
|
399
423
|
topicId,
|
|
400
424
|
topicTitle: session,
|
|
401
425
|
queryId,
|
|
402
426
|
cwd: cwd ?? resolveTopicWorkspaceDir(topicId),
|
|
403
427
|
agent,
|
|
404
428
|
model,
|
|
405
|
-
|
|
429
|
+
// Derived here, from the full prompt, so the signed token carries the
|
|
430
|
+
// gate's answer (bounded) instead of the prompt (unbounded); see
|
|
431
|
+
// `explicitAgentSwitchTargets`.
|
|
432
|
+
...(switchTargets.length ? { explicitAgentSwitchTargets: switchTargets } : {}),
|
|
406
433
|
autoContinue,
|
|
407
434
|
visualTools,
|
|
408
435
|
fileDeliveryTools,
|
|
@@ -484,6 +511,9 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
484
511
|
build(ctx) {
|
|
485
512
|
const {
|
|
486
513
|
userId,
|
|
514
|
+
actorUserId,
|
|
515
|
+
actorTopicScope,
|
|
516
|
+
remoteSession,
|
|
487
517
|
session,
|
|
488
518
|
topicId,
|
|
489
519
|
subagentParentTopicId,
|
|
@@ -499,6 +529,13 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
499
529
|
: undefined;
|
|
500
530
|
const args = [
|
|
501
531
|
`--user-id=${userId}`,
|
|
532
|
+
...(actorUserId ? [`--actor-user-id=${actorUserId}`] : []),
|
|
533
|
+
...(actorTopicScope
|
|
534
|
+
? [`--actor-topic-scope=${encodeActorTopicScopeArg(actorTopicScope)}`]
|
|
535
|
+
: []),
|
|
536
|
+
...(remoteSession
|
|
537
|
+
? [`--remote-session-grant=${encodeRemoteSessionGrantArg(remoteSession)}`]
|
|
538
|
+
: []),
|
|
502
539
|
`--topic=${session}`,
|
|
503
540
|
...(topicId ? [`--topic-id=${topicId}`] : []),
|
|
504
541
|
...(subagentParentTopicId ? [`--subagent-parent-topic-id=${subagentParentTopicId}`] : []),
|
|
@@ -925,6 +962,9 @@ export function getDmMcpServers(opts: {
|
|
|
925
962
|
*/
|
|
926
963
|
export function getManagerMcpServers(opts: {
|
|
927
964
|
userId: string;
|
|
965
|
+
actorUserId?: string;
|
|
966
|
+
actorTopicScope?: ActorTopicScope;
|
|
967
|
+
remoteSession?: RemoteSessionGrant;
|
|
928
968
|
session?: string;
|
|
929
969
|
topicId?: string;
|
|
930
970
|
queryId?: string;
|
|
@@ -945,6 +985,9 @@ export function getManagerMcpServers(opts: {
|
|
|
945
985
|
const topicId = opts.topicId;
|
|
946
986
|
return buildScope("manager", {
|
|
947
987
|
userId: opts.userId,
|
|
988
|
+
actorUserId: opts.actorUserId,
|
|
989
|
+
actorTopicScope: opts.actorTopicScope,
|
|
990
|
+
remoteSession: opts.remoteSession,
|
|
948
991
|
session: opts.session ?? "General",
|
|
949
992
|
topicId,
|
|
950
993
|
queryId: opts.queryId,
|
|
@@ -970,6 +1013,8 @@ export function getManagerMcpServers(opts: {
|
|
|
970
1013
|
export function getForumMcpServers(opts: {
|
|
971
1014
|
userId: string;
|
|
972
1015
|
actorUserId?: string;
|
|
1016
|
+
actorTopicScope?: ActorTopicScope;
|
|
1017
|
+
remoteSession?: RemoteSessionGrant;
|
|
973
1018
|
vaultUserId?: string;
|
|
974
1019
|
session: string;
|
|
975
1020
|
topicId?: string;
|
|
@@ -996,6 +1041,8 @@ export function getForumMcpServers(opts: {
|
|
|
996
1041
|
const {
|
|
997
1042
|
userId,
|
|
998
1043
|
actorUserId,
|
|
1044
|
+
actorTopicScope,
|
|
1045
|
+
remoteSession,
|
|
999
1046
|
vaultUserId,
|
|
1000
1047
|
session,
|
|
1001
1048
|
topicId,
|
|
@@ -1033,6 +1080,8 @@ export function getForumMcpServers(opts: {
|
|
|
1033
1080
|
{
|
|
1034
1081
|
userId,
|
|
1035
1082
|
actorUserId,
|
|
1083
|
+
actorTopicScope,
|
|
1084
|
+
remoteSession,
|
|
1036
1085
|
vaultUserId,
|
|
1037
1086
|
session,
|
|
1038
1087
|
topicId,
|
|
@@ -1146,6 +1195,9 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
|
|
|
1146
1195
|
mergeHostMcpServers(
|
|
1147
1196
|
getManagerMcpServers({
|
|
1148
1197
|
userId: opts.userId || "local",
|
|
1198
|
+
actorUserId: opts.actorUserId,
|
|
1199
|
+
actorTopicScope: opts.actorTopicScope,
|
|
1200
|
+
remoteSession: opts.remoteSession,
|
|
1149
1201
|
session: opts.session,
|
|
1150
1202
|
topicId: opts.topicId,
|
|
1151
1203
|
queryId: opts.queryId,
|
|
@@ -1169,6 +1221,8 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
|
|
|
1169
1221
|
getForumMcpServers({
|
|
1170
1222
|
userId: opts.userId || "local",
|
|
1171
1223
|
actorUserId: opts.actorUserId,
|
|
1224
|
+
actorTopicScope: opts.actorTopicScope,
|
|
1225
|
+
remoteSession: opts.remoteSession,
|
|
1172
1226
|
vaultUserId: opts.vaultUserId,
|
|
1173
1227
|
session: opts.session || "default",
|
|
1174
1228
|
topicId: opts.topicId,
|
|
@@ -120,6 +120,12 @@ export interface RoomQueryControl {
|
|
|
120
120
|
prompt: string;
|
|
121
121
|
/** Original user submissions represented by prompt, in arrival order. */
|
|
122
122
|
userMessages?: UserTurnEnvelope[];
|
|
123
|
+
/**
|
|
124
|
+
* Product actor a user turn speaks for, when recorded. Decides whether a
|
|
125
|
+
* later human message may steer this turn: only the same actor's follow-up
|
|
126
|
+
* aborts and folds into it; anyone else's waits its turn in the queue.
|
|
127
|
+
*/
|
|
128
|
+
actorUserId?: string;
|
|
123
129
|
/** Durable request rows whose messages are already represented by userMessages. */
|
|
124
130
|
durableRequestIds?: string[];
|
|
125
131
|
/** Attachment ids currently being processed. User-turn preemption carries them forward. */
|