letagents 0.12.10 → 0.12.12
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 -0
- package/dist/api/board-intent-payloads.js +25 -0
- package/dist/mcp/codex-session/runtime-bridge.js +42 -25
- package/dist/mcp/local-state/agent-sessions.js +15 -0
- package/dist/mcp/local-state/local-chat.js +1 -1
- package/dist/mcp/rental-tools/context.js +30 -0
- package/dist/mcp/server/register-tools.js +23 -12
- package/dist/mcp/server/runtime/agent-sessions.js +81 -2
- package/dist/mcp/server/runtime/api.js +63 -13
- package/dist/mcp/server/runtime/execution-profile.js +19 -0
- package/dist/mcp/server/runtime/identity/directory.js +9 -2
- package/dist/mcp/server/runtime/identity.js +2 -1
- package/dist/mcp/server/runtime/presence.js +4 -2
- package/dist/mcp/server/runtime/room-api.js +24 -7
- package/dist/mcp/server/runtime/room-state.js +46 -0
- package/dist/mcp/server/runtime/rooms.js +70 -0
- package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +731 -0
- package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
- package/dist/mcp/server/runtime/worker-bearer.js +101 -0
- package/dist/mcp/server/runtime-contract.js +27 -0
- package/dist/mcp/server/runtime.js +14 -3
- package/dist/mcp/server/supervised-tool-facade.js +105 -0
- package/dist/mcp/server/tools/agent-sessions.js +113 -5
- package/dist/mcp/server/tools/messages/index.js +3 -2
- package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
- package/dist/mcp/server/tools/messages/read-tool.js +1 -1
- package/dist/mcp/server/tools/messages/send-tool.js +5 -45
- package/dist/mcp/server/tools/messages/wait-tool.js +212 -91
- package/dist/mcp/server/tools/onboarding/device-auth-tools.js +10 -0
- package/dist/mcp/server/tools/onboarding/name-tool.js +5 -1
- package/dist/mcp/server/tools/onboarding/status-tool.js +18 -0
- package/dist/mcp/server/tools/rental/context-tools.js +9 -1
- package/dist/mcp/server/tools/rooms/inspection-tools.js +47 -18
- package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
- package/dist/mcp/server/tools/tasks/board-intent-tools.js +7 -2
- package/dist/mcp/server/tools/tasks/index.js +2 -0
- package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
- package/dist/mcp/server.js +16 -7
- package/dist/mcp/sse-client.js +3 -3
- package/dist/shared/activation-routing.js +57 -4
- package/dist/shared/agent-presence.js +1 -0
- package/dist/shared/agent-session-bearer.js +28 -0
- package/dist/shared/board-manager-failover.js +16 -0
- package/dist/shared/room-agent-prompts.js +2 -2
- package/package.json +24 -15
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const TOOL_SURFACE_BY_PROFILE = {
|
|
2
|
+
supervised_room_turn: {
|
|
3
|
+
agentSessionLifecycle: false,
|
|
4
|
+
deliveryLoop: false,
|
|
5
|
+
onboarding: false,
|
|
6
|
+
rental: false,
|
|
7
|
+
roomResume: false,
|
|
8
|
+
},
|
|
9
|
+
autonomous_mcp_worker: {
|
|
10
|
+
agentSessionLifecycle: true,
|
|
11
|
+
deliveryLoop: true,
|
|
12
|
+
onboarding: true,
|
|
13
|
+
rental: true,
|
|
14
|
+
roomResume: true,
|
|
15
|
+
},
|
|
16
|
+
interactive_desktop: {
|
|
17
|
+
agentSessionLifecycle: true,
|
|
18
|
+
deliveryLoop: true,
|
|
19
|
+
onboarding: true,
|
|
20
|
+
rental: true,
|
|
21
|
+
roomResume: true,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export function toolSurfaceForExecutionProfile(profile) {
|
|
25
|
+
return TOOL_SURFACE_BY_PROFILE[profile];
|
|
26
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export const LETAGENTS_AGENT_SESSION_BEARER_ENV = "LETAGENTS_AGENT_SESSION_BEARER";
|
|
2
|
+
export const LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV = "LETAGENTS_SUPERVISED_BOUNDED_TURNS";
|
|
3
|
+
export class WorkerBearerRuntimeConfigurationError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "WorkerBearerRuntimeConfigurationError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function getWorkerBearerRuntime() {
|
|
10
|
+
const bearer = process.env.LETAGENTS_AGENT_SESSION_BEARER?.trim();
|
|
11
|
+
const supervised = process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() === "1";
|
|
12
|
+
const profile = process.env.LETAGENTS_EXECUTION_PROFILE?.trim();
|
|
13
|
+
if (supervised !== (profile === "supervised_room_turn")) {
|
|
14
|
+
return {
|
|
15
|
+
mode: "invalid",
|
|
16
|
+
error: "LETAGENTS_EXECUTION_PROFILE=supervised_room_turn and LETAGENTS_SUPERVISED_BOUNDED_TURNS=1 must be configured together.",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (!bearer && !supervised)
|
|
20
|
+
return { mode: "owner" };
|
|
21
|
+
if (bearer && supervised) {
|
|
22
|
+
return {
|
|
23
|
+
mode: "invalid",
|
|
24
|
+
error: "Daemon-supervised bounded turns refuse LETAGENTS_AGENT_SESSION_BEARER; credentials must be borrowed from the exact supervisor generation.",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const apiUrl = process.env.LETAGENTS_API_URL?.trim();
|
|
28
|
+
if (!apiUrl) {
|
|
29
|
+
return {
|
|
30
|
+
mode: "invalid",
|
|
31
|
+
error: "Worker bearer mode requires an explicit LETAGENTS_API_URL.",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(apiUrl);
|
|
36
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
37
|
+
throw new Error("unsupported protocol");
|
|
38
|
+
}
|
|
39
|
+
const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
40
|
+
if (parsed.protocol === "http:" && !loopbackHosts.has(parsed.hostname.toLowerCase())) {
|
|
41
|
+
return {
|
|
42
|
+
mode: "invalid",
|
|
43
|
+
error: "Worker bearer mode requires HTTPS unless LETAGENTS_API_URL uses an exact loopback host.",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return {
|
|
49
|
+
mode: "invalid",
|
|
50
|
+
error: "Worker bearer mode requires LETAGENTS_API_URL to be a valid HTTP(S) URL.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (bearer && process.env.LETAGENTS_TOKEN?.trim()) {
|
|
54
|
+
return {
|
|
55
|
+
mode: "invalid",
|
|
56
|
+
error: "Worker bearer mode refuses LETAGENTS_TOKEN. Remove the owner token from this process before starting the worker.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return bearer ? { mode: "worker", bearer } : { mode: "supervised" };
|
|
60
|
+
}
|
|
61
|
+
export function requireValidWorkerBearerRuntime() {
|
|
62
|
+
const runtime = getWorkerBearerRuntime();
|
|
63
|
+
if (runtime.mode === "invalid") {
|
|
64
|
+
throw new WorkerBearerRuntimeConfigurationError(runtime.error);
|
|
65
|
+
}
|
|
66
|
+
return runtime;
|
|
67
|
+
}
|
|
68
|
+
export function isSupervisedBoundedTurn() {
|
|
69
|
+
return requireValidWorkerBearerRuntime().mode === "supervised";
|
|
70
|
+
}
|
|
71
|
+
export function workerModeDisabledToolResult(toolDescription = "This owner-auth onboarding tool") {
|
|
72
|
+
const runtime = getWorkerBearerRuntime();
|
|
73
|
+
if (runtime.mode === "invalid") {
|
|
74
|
+
return { success: false, error: "worker_bearer_configuration_invalid", message: runtime.error };
|
|
75
|
+
}
|
|
76
|
+
if (runtime.mode === "worker" || runtime.mode === "supervised") {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
error: runtime.mode === "worker" ? "worker_bearer_mode" : "supervised_bounded_mode",
|
|
80
|
+
message: runtime.mode === "worker"
|
|
81
|
+
? `${toolDescription} is disabled while LETAGENTS_AGENT_SESSION_BEARER is configured.`
|
|
82
|
+
: `${toolDescription} is disabled during a daemon-supervised bounded turn.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Supervised room delivery belongs to the desktop daemon. A bounded provider
|
|
89
|
+
* turn must never recreate the permanent MCP polling loop, even if its prompt
|
|
90
|
+
* asks it to do so.
|
|
91
|
+
*/
|
|
92
|
+
export function supervisedBoundedDeliveryDisabledToolResult(toolName = "wait_for_messages") {
|
|
93
|
+
if (process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
error: "supervised_bounded_delivery",
|
|
99
|
+
message: `${toolName} is disabled because supervised room delivery is owned by the desktop daemon.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { registerTools } from "./register-tools.js";
|
|
2
|
+
export const LETAGENTS_RUNTIME_CONTRACT_ARG = "--letagents-runtime-contract";
|
|
3
|
+
/**
|
|
4
|
+
* Discover through the production registration path rather than maintaining a
|
|
5
|
+
* second capability list that can drift from the MCP server.
|
|
6
|
+
*/
|
|
7
|
+
export function registeredToolNames(profile, supervisedProvider = null) {
|
|
8
|
+
const names = new Set();
|
|
9
|
+
const recorder = {
|
|
10
|
+
tool(name) {
|
|
11
|
+
names.add(name);
|
|
12
|
+
return {};
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
registerTools(recorder, profile, supervisedProvider);
|
|
16
|
+
return [...names].sort();
|
|
17
|
+
}
|
|
18
|
+
export function letAgentsRuntimeContract() {
|
|
19
|
+
return {
|
|
20
|
+
format: 1,
|
|
21
|
+
profiles: {
|
|
22
|
+
cursor_supervised_room_turn: {
|
|
23
|
+
tools: registeredToolNames("supervised_room_turn", "cursor"),
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
// Compatibility facade for MCP server runtime helpers. The implementation lives
|
|
2
2
|
// in src/mcp/server/runtime/* so tool modules can import focused responsibilities
|
|
3
3
|
// without turning this file back into the runtime god module.
|
|
4
|
+
import { isLocalRoomStorageEnabled as isStoredLocalRoomStorageEnabled, touchRoomSession as touchStoredRoomSession, } from "../local-state.js";
|
|
5
|
+
import { isSupervisedBoundedTurn } from "./runtime/worker-bearer.js";
|
|
6
|
+
/** A daemon-supervised turn must always use its exact cloud worker route. */
|
|
7
|
+
export async function isLocalRoomStorageEnabled(roomId) {
|
|
8
|
+
return !isSupervisedBoundedTurn() && isStoredLocalRoomStorageEnabled(roomId);
|
|
9
|
+
}
|
|
10
|
+
export function touchRoomSession(roomId, lastMessageId) {
|
|
11
|
+
if (!isSupervisedBoundedTurn())
|
|
12
|
+
touchStoredRoomSession(roomId, lastMessageId);
|
|
13
|
+
}
|
|
4
14
|
export { API_URL, ApiError, apiCall, getAuthorizationHeader, getLetagentsToken, isMissingRouteError, parseApiErrorPayload, resolveApiPath, } from "./runtime/api.js";
|
|
5
15
|
export { clearAuthenticatedAccountCache, getAuthenticatedAccountCache, setAuthenticatedAccountCache, } from "./runtime/auth-cache.js";
|
|
6
16
|
export { RepoRoomAuthRequiredError, maybeHandleRepoRoomAuthRequired, startPendingDeviceAuth, toRepoRoomAuthRequiredResult, } from "./runtime/device-auth.js";
|
|
7
17
|
export { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, getConversationIdentity, getSessionLivenessRegistration, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, withAgentIdentity, } from "./runtime/identity.js";
|
|
8
|
-
export { agentSessionCredentials, buildAgentDeliveryHeaders, ensureLocalWorkerAgentSession, getAgentSessionRepoBranch, identityFromAgentSession, requireWorkerAgentSession, resolveAgentSession, resolveWorkerToolIdentity, toPublicAgentSession, } from "./runtime/agent-sessions.js";
|
|
18
|
+
export { agentSessionCredentials, buildAgentDeliveryHeaders, ensureLocalWorkerAgentSession, getAgentSessionRepoBranch, identityFromAgentSession, requireWorkerAgentSession, resolveAgentSession, resolveClientRequestedBase, resolveWorkerToolIdentity, toPublicAgentSession, WORKER_BEARER_AGENT_SESSION_ID, } from "./runtime/agent-sessions.js";
|
|
9
19
|
export { appendIncludePromptOnly, getLastMessageId, normalizeOptionalToolString, toAgentReadableMessages, withJoinRoomAgentPrompt, } from "./runtime/messages.js";
|
|
10
|
-
export { currentRoom, attachMcpServer, getFallbackProjectId, getTargetRoomId, rememberRoom, shutdownRuntime, toPublicRoomResponse, toPublicRoomState, toPublicStoredRoomSession, toRoomState, touchCurrentRoom, withCanonicalRoomLink, } from "./runtime/room-state.js";
|
|
20
|
+
export { currentRoom, attachMcpServer, getCurrentSupervisedRoomAuthority, getFallbackProjectId, getTargetRoomId, rememberRoom, runWithCurrentSupervisedRoom, shutdownRuntime, toPublicRoomResponse, toPublicCurrentRoomState, toPublicRoomState, toPublicStoredRoomSession, toRoomState, touchCurrentRoom, withCanonicalRoomLink, } from "./runtime/room-state.js";
|
|
11
21
|
export { getRememberedRoomPresence, heartbeatRoomPresence, syncRoomPresence, } from "./runtime/presence.js";
|
|
12
22
|
export { roomScopedApiCall } from "./runtime/room-api.js";
|
|
23
|
+
export { borrowSupervisedWorkerCredential, borrowCurrentSupervisedWorkerCredential, bindSupervisedWorkerSession, checkpointSupervisedWorkerCursor, isRetryableSupervisorBridgeError, scheduleSupervisedWorkerCursorCheckpoint, resolveCurrentSupervisedWorkerSession, } from "./runtime/supervisor-bridge.js";
|
|
13
24
|
export { autoJoinFromContext, buildJoinResponse, createInviteRoom, getCurrentLiveSessionPayload, joinInviteCode, joinNamedRoom, joinRoomIdentifier, joinRoomIdentifierWithoutImplicitGitRefCreate, normalizeJoinSessionMode, } from "./runtime/rooms.js";
|
|
14
|
-
export { clearPendingDeviceAuth, clearStoredAuth, clearStoredAuth as clearStoredAuthorization, endStoredAgentSession, getCurrentAgentSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAgentSession, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveAgentSession, setPendingDeviceAuth, setStoredAuth, setStoredAgentIdentity,
|
|
25
|
+
export { clearPendingDeviceAuth, clearStoredAuth, clearStoredAuth as clearStoredAuthorization, endStoredAgentSession, getCurrentAgentSession, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAgentSession, getStoredAgentSessionsForRoomIdentity, getStoredAuth, getStoredCurrentRoom, getStoredRoomSession, listStoredCodexLiveSessions, saveAgentSession, setPendingDeviceAuth, setStoredAuth, setStoredAgentIdentity, addLocalChatMessage, addLocalTask, claimLocalTaskReviewLease, getLatestLocalChatMessages, getLocalChatMessages, getLocalTask, listLocalTasks, isLocalChatStorageEnabled, resolveLocalRoomStorageIdentifiers, releaseLocalTaskReviewLease, updateLocalTask, waitForLocalChatMessages, } from "../local-state.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { runWithCurrentSupervisedRoom } from "./runtime/room-state.js";
|
|
2
|
+
import { completeCurrentSupervisedEffect, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
|
|
3
|
+
const READ_TOOLS = new Set([
|
|
4
|
+
"get_current_room",
|
|
5
|
+
"check_repo",
|
|
6
|
+
"check_repo_visibility",
|
|
7
|
+
"read_messages",
|
|
8
|
+
"wait_for_messages",
|
|
9
|
+
"get_board",
|
|
10
|
+
"get_board_settings",
|
|
11
|
+
"get_room_artifacts",
|
|
12
|
+
"get_room_events",
|
|
13
|
+
"list_board_intents",
|
|
14
|
+
"get_onboarding_status",
|
|
15
|
+
"status_local_codex_session",
|
|
16
|
+
"rental_list_requests",
|
|
17
|
+
]);
|
|
18
|
+
function instruction(text, data = {}) {
|
|
19
|
+
const payload = { ...data, instruction: text };
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
22
|
+
structuredContent: payload,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const productionDependencies = {
|
|
26
|
+
prepareEffect: prepareCurrentSupervisedEffect,
|
|
27
|
+
completeEffect: completeCurrentSupervisedEffect,
|
|
28
|
+
withRoom: runWithCurrentSupervisedRoom,
|
|
29
|
+
};
|
|
30
|
+
export function profileAwareToolServer(server, profile, dependencies = productionDependencies, supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null) {
|
|
31
|
+
if (profile !== "supervised_room_turn")
|
|
32
|
+
return server;
|
|
33
|
+
return new Proxy(server, {
|
|
34
|
+
get(target, property, receiver) {
|
|
35
|
+
if (property !== "tool") {
|
|
36
|
+
const value = Reflect.get(target, property, receiver);
|
|
37
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
38
|
+
}
|
|
39
|
+
return (name, ...registration) => {
|
|
40
|
+
const callback = registration.at(-1);
|
|
41
|
+
if (typeof callback !== "function")
|
|
42
|
+
throw new Error(`Tool ${name} has no callback.`);
|
|
43
|
+
const wrapped = async (...call) => {
|
|
44
|
+
const extra = (call.at(-1) ?? {});
|
|
45
|
+
const input = call.length > 1 ? call[0] : {};
|
|
46
|
+
if (extra.requestId === undefined || extra.requestId === null || String(extra.requestId).trim() === "") {
|
|
47
|
+
throw new Error(`Supervised tool ${name} is missing its MCP request id; refusing an effect that cannot be deduplicated safely.`);
|
|
48
|
+
}
|
|
49
|
+
const prepared = await dependencies.prepareEffect({
|
|
50
|
+
toolName: name,
|
|
51
|
+
input,
|
|
52
|
+
mcpRequestId: String(extra.requestId),
|
|
53
|
+
mutation: !READ_TOOLS.has(name),
|
|
54
|
+
});
|
|
55
|
+
return dependencies.withRoom(prepared.roomId, async () => {
|
|
56
|
+
if (prepared.state === "completed")
|
|
57
|
+
return prepared.result;
|
|
58
|
+
if (prepared.state === "uncertain") {
|
|
59
|
+
return instruction("This mutating tool may already have completed, but its result was not durably checkpointed. Verify the external state before issuing a new request; this exact request will not be repeated automatically.", {
|
|
60
|
+
code: "SUPERVISED_EFFECT_OUTCOME_UNCERTAIN",
|
|
61
|
+
effect_id: prepared.effectId,
|
|
62
|
+
detail: prepared.error,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (prepared.action === "use_final_answer") {
|
|
66
|
+
return instruction(supervisedProvider === "cursor"
|
|
67
|
+
? "Do not send the activating room reply with a message tool. Keep working, then record the one public answer with complete_room_turn; Cursor's aggregate final text is live evidence only."
|
|
68
|
+
: "Do not send the activating room reply with a message tool. Return it as your final answer; the daemon will publish it exactly once.", {
|
|
69
|
+
code: "USE_FINAL_ANSWER",
|
|
70
|
+
source_message_id: prepared.sourceMessageId,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (prepared.action === "room_move_prepared") {
|
|
74
|
+
return instruction(supervisedProvider === "cursor"
|
|
75
|
+
? "The room move is prepared. Finish the work, then call complete_room_turn with the public response; the daemon will publish that proposal and then move the agent."
|
|
76
|
+
: "The room move is prepared. Finish this turn normally; the daemon will publish the activating response and then move the agent.", {
|
|
77
|
+
code: "ROOM_MOVE_PREPARED",
|
|
78
|
+
destination_room: prepared.destinationRoom,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = await callback(...call);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
try {
|
|
87
|
+
await dependencies.completeEffect({ effectId: prepared.effectId, error: error instanceof Error ? error.message : String(error) });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Preserve the callback error. An unacknowledged journal entry
|
|
91
|
+
// remains executing, which is safer than repeating the effect.
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
// Completion transport is deliberately outside the callback catch.
|
|
96
|
+
// A reporting failure must never relabel a successful action failed.
|
|
97
|
+
await dependencies.completeEffect({ effectId: prepared.effectId, result });
|
|
98
|
+
return result;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
return target.tool.call(target, name, ...registration.slice(0, -1), wrapped);
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -2,7 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { scheduleCodexRuntimeStreamBridgeBind, } from "../../codex-session.js";
|
|
3
3
|
import { getManagedAgentProvider, toManagedAgentStartResponse, } from "../../managed-agent-providers.js";
|
|
4
4
|
import { encodeRoomIdPath, looksLikeInviteCode, normalizeInviteCode } from "../../room-id.js";
|
|
5
|
-
import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, } from "../runtime.js";
|
|
5
|
+
import { AGENT_INSTANCE_UUID, RepoRoomAuthRequiredError, apiCall, agentSessionCredentials, currentRoom, detectAgentIdeLabel, detectAgentRuntimeLabel, endStoredAgentSession, ensureAgentIdentity, getSessionLivenessRegistration, getAgentSessionRepoBranch, getStoredAgentSession, getStoredAgentSessionsForRoomIdentity, getTargetRoomId, ensureLocalWorkerAgentSession, isLocalRoomStorageEnabled, joinRoomIdentifier, resolveLocalRoomStorageIdentifiers, resolveClientRequestedBase, saveAgentSession, toPublicAgentSession, toPublicRoomState, toRepoRoomAuthRequiredResult, withAgentIdentity, resolveWorkerToolIdentity, } from "../runtime.js";
|
|
6
|
+
import { requireValidWorkerBearerRuntime, workerModeDisabledToolResult, } from "../runtime/worker-bearer.js";
|
|
7
|
+
import { bindSupervisedWorkerSessionWithContext } from "../runtime/supervisor-bridge.js";
|
|
6
8
|
export function registerAgentSessionTools(server) {
|
|
7
9
|
// -- register_agent_session -------------------------------------------------
|
|
8
10
|
server.tool("register_agent_session", "Register this MCP client as an explicit room agent session. Unregistered MCP traffic is treated as controller traffic and stays out of the connected-agent roster.", {
|
|
@@ -25,8 +27,29 @@ export function registerAgentSessionTools(server) {
|
|
|
25
27
|
cwd: z
|
|
26
28
|
.string()
|
|
27
29
|
.optional()
|
|
28
|
-
.describe("
|
|
30
|
+
.describe("Worker working directory used for branch detection and exact supervised Codex binding. Defaults to the MCP server's working directory."),
|
|
29
31
|
}, async ({ room_id, session_kind, runtime, display_name, cwd }) => {
|
|
32
|
+
const workerRuntime = requireValidWorkerBearerRuntime();
|
|
33
|
+
if (workerRuntime.mode === "supervised") {
|
|
34
|
+
// Resolve before currentRoom, config, branch, or local storage. The
|
|
35
|
+
// daemon context is the only authority for a bounded worker's room.
|
|
36
|
+
const { agentSession } = await resolveWorkerToolIdentity({ roomId: room_id?.trim() || null });
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
success: true,
|
|
43
|
+
worker_bearer_mode: false,
|
|
44
|
+
supervised_bounded_mode: true,
|
|
45
|
+
agent_session: toPublicAgentSession(agentSession),
|
|
46
|
+
agent_session_id: agentSession.session_id,
|
|
47
|
+
use_agent_session_id: "This exact daemon-supervised agent_session_id may be passed to room tools. The credential remains daemon-private.",
|
|
48
|
+
}, null, 2),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
30
53
|
const targetRoomId = getTargetRoomId(room_id);
|
|
31
54
|
if (!targetRoomId) {
|
|
32
55
|
return {
|
|
@@ -66,6 +89,29 @@ export function registerAgentSessionTools(server) {
|
|
|
66
89
|
],
|
|
67
90
|
};
|
|
68
91
|
}
|
|
92
|
+
const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(targetRoomId);
|
|
93
|
+
const apiRoomId = cloudRoomId || targetRoomId;
|
|
94
|
+
if (workerRuntime.mode === "worker") {
|
|
95
|
+
// The supplied bearer is issued for an existing server-side agent
|
|
96
|
+
// session. Do not call the owner-only registration endpoint or write
|
|
97
|
+
// any session credential to local storage.
|
|
98
|
+
const { agentSession } = await resolveWorkerToolIdentity({ roomId: apiRoomId });
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: JSON.stringify({
|
|
104
|
+
success: true,
|
|
105
|
+
worker_bearer_mode: workerRuntime.mode === "worker",
|
|
106
|
+
supervised_bounded_mode: false,
|
|
107
|
+
agent_session: toPublicAgentSession(agentSession),
|
|
108
|
+
agent_session_id: agentSession.session_id,
|
|
109
|
+
use_agent_session_id: "This local worker-bearer session marker may be passed to room tools. The supplied bearer remains the only server credential.",
|
|
110
|
+
}, null, 2),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
69
115
|
const identity = await ensureAgentIdentity();
|
|
70
116
|
if (!identity.canonical_key) {
|
|
71
117
|
return {
|
|
@@ -80,8 +126,22 @@ export function registerAgentSessionTools(server) {
|
|
|
80
126
|
],
|
|
81
127
|
};
|
|
82
128
|
}
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
// Stable-base signal (task_66): declare the intent behind display_name
|
|
130
|
+
// so the server can converge a replayed decorated label without ever
|
|
131
|
+
// guessing from numeric shape.
|
|
132
|
+
const priorRoomSessions = getStoredAgentSessionsForRoomIdentity(apiRoomId, identity.canonical_key);
|
|
133
|
+
const requestedSessionKind = session_kind ?? "worker";
|
|
134
|
+
const requestedBaseDisplayName = resolveClientRequestedBase({
|
|
135
|
+
explicitDisplayName: display_name,
|
|
136
|
+
identityDisplayName: identity.display_name,
|
|
137
|
+
priorSessions: priorRoomSessions,
|
|
138
|
+
});
|
|
139
|
+
const replacementSession = requestedSessionKind === "worker"
|
|
140
|
+
? priorRoomSessions.find((session) => !session.ended_at
|
|
141
|
+
&& session.session_kind === "worker"
|
|
142
|
+
&& session.agent_instance_id === AGENT_INSTANCE_UUID
|
|
143
|
+
&& Boolean(session.session_token)) ?? null
|
|
144
|
+
: null;
|
|
85
145
|
const created = await apiCall(`/rooms/${encodeRoomIdPath(apiRoomId)}/agent-sessions`, {
|
|
86
146
|
method: "POST",
|
|
87
147
|
body: JSON.stringify({
|
|
@@ -90,10 +150,13 @@ export function registerAgentSessionTools(server) {
|
|
|
90
150
|
ide_label: identity.ide_label ?? detectAgentIdeLabel(),
|
|
91
151
|
agent_instance_id: AGENT_INSTANCE_UUID,
|
|
92
152
|
display_name: display_name?.trim() || identity.display_name,
|
|
153
|
+
requested_base_display_name: requestedBaseDisplayName,
|
|
93
154
|
session_kind: session_kind ?? "worker",
|
|
94
155
|
runtime: requestedRuntime,
|
|
95
156
|
repo_branch: repoBranch,
|
|
96
157
|
registration_liveness: getSessionLivenessRegistration(requestedRuntime),
|
|
158
|
+
replace_agent_session_id: replacementSession?.session_id ?? null,
|
|
159
|
+
replace_agent_session_token: replacementSession?.session_token ?? null,
|
|
97
160
|
}),
|
|
98
161
|
});
|
|
99
162
|
const sessionId = typeof created.session_id === "string" ? created.session_id : "";
|
|
@@ -101,7 +164,10 @@ export function registerAgentSessionTools(server) {
|
|
|
101
164
|
if (!sessionId || !sessionToken) {
|
|
102
165
|
throw new Error("Agent session registration response was missing session credentials.");
|
|
103
166
|
}
|
|
104
|
-
|
|
167
|
+
if (replacementSession) {
|
|
168
|
+
endStoredAgentSession(replacementSession.session_id, typeof created.created_at === "string" ? created.created_at : new Date().toISOString());
|
|
169
|
+
}
|
|
170
|
+
let session = saveAgentSession({
|
|
105
171
|
session_id: sessionId,
|
|
106
172
|
session_token: sessionToken,
|
|
107
173
|
room_id: typeof created.room_id === "string" ? created.room_id : apiRoomId,
|
|
@@ -116,6 +182,11 @@ export function registerAgentSessionTools(server) {
|
|
|
116
182
|
agent_key: typeof created.agent_key === "string" ? created.agent_key : identity.canonical_key,
|
|
117
183
|
agent_instance_id: typeof created.agent_instance_id === "string" ? created.agent_instance_id : AGENT_INSTANCE_UUID,
|
|
118
184
|
display_name: typeof created.display_name === "string" ? created.display_name : identity.display_name,
|
|
185
|
+
// Prefer the server-recorded allocation base; fall back to the base we
|
|
186
|
+
// declared so a later resume still replays a stable signal.
|
|
187
|
+
requested_base_display_name: (typeof created.assigned_base_display_name === "string" && created.assigned_base_display_name.trim())
|
|
188
|
+
|| requestedBaseDisplayName
|
|
189
|
+
|| null,
|
|
119
190
|
owner_label: typeof created.owner_label === "string" ? created.owner_label : identity.owner_label,
|
|
120
191
|
ide_label: typeof created.ide_label === "string" ? created.ide_label : identity.ide_label ?? detectAgentIdeLabel(),
|
|
121
192
|
repo_branch: typeof created.repo_branch === "string" ? created.repo_branch : repoBranch,
|
|
@@ -124,6 +195,13 @@ export function registerAgentSessionTools(server) {
|
|
|
124
195
|
last_seen_at: typeof created.last_seen_at === "string" ? created.last_seen_at : new Date().toISOString(),
|
|
125
196
|
ended_at: typeof created.ended_at === "string" ? created.ended_at : null,
|
|
126
197
|
});
|
|
198
|
+
const supervisorBinding = await bindSupervisedWorkerSessionWithContext(session, process.env, { cwd: cwd?.trim() || process.cwd() });
|
|
199
|
+
if (supervisorBinding.supervisorContextCwd) {
|
|
200
|
+
session = saveAgentSession({
|
|
201
|
+
...session,
|
|
202
|
+
supervisor_context_cwd: supervisorBinding.supervisorContextCwd,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
127
205
|
scheduleCodexRuntimeStreamBridgeBind(session);
|
|
128
206
|
return {
|
|
129
207
|
content: [
|
|
@@ -144,6 +222,18 @@ export function registerAgentSessionTools(server) {
|
|
|
144
222
|
room_id: z.string().optional().describe("Canonical room ID. Defaults to the current room or the stored session room."),
|
|
145
223
|
agent_session_id: z.string().optional().describe("Registered agent session to disconnect."),
|
|
146
224
|
}, async ({ room_id, agent_session_id }) => {
|
|
225
|
+
if (requireValidWorkerBearerRuntime().mode === "supervised") {
|
|
226
|
+
return {
|
|
227
|
+
content: [{
|
|
228
|
+
type: "text",
|
|
229
|
+
text: JSON.stringify({
|
|
230
|
+
success: false,
|
|
231
|
+
error: "supervised_bounded_mode",
|
|
232
|
+
message: "Agent-session disconnection is owned by the desktop supervisor during a bounded turn.",
|
|
233
|
+
}, null, 2),
|
|
234
|
+
}],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
147
237
|
let targetRoomId = getTargetRoomId(room_id);
|
|
148
238
|
const localSession = agent_session_id
|
|
149
239
|
? getStoredAgentSession(agent_session_id)
|
|
@@ -258,6 +348,12 @@ export function registerAgentSessionTools(server) {
|
|
|
258
348
|
.optional()
|
|
259
349
|
.describe("Optional hard stop in minutes. Defaults to 0, which means run until stopped."),
|
|
260
350
|
}, async ({ room, cwd, stop_phrase, max_minutes }) => {
|
|
351
|
+
const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
|
|
352
|
+
if (disabled) {
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
261
357
|
const joinedVia = looksLikeInviteCode(room) ? "join_code" : "join_room";
|
|
262
358
|
try {
|
|
263
359
|
const joined = await joinRoomIdentifier(room, joinedVia);
|
|
@@ -305,6 +401,12 @@ export function registerAgentSessionTools(server) {
|
|
|
305
401
|
.optional()
|
|
306
402
|
.describe("Optional session id. Defaults to the current local Codex live session."),
|
|
307
403
|
}, async ({ session_id }) => {
|
|
404
|
+
const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
|
|
405
|
+
if (disabled) {
|
|
406
|
+
return {
|
|
407
|
+
content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
308
410
|
const provider = getManagedAgentProvider("codex");
|
|
309
411
|
const status = await provider.inspectLocalSession(session_id, currentRoom?.room_id);
|
|
310
412
|
if (!status) {
|
|
@@ -343,6 +445,12 @@ export function registerAgentSessionTools(server) {
|
|
|
343
445
|
.optional()
|
|
344
446
|
.describe("If true, also terminate the spawned codex app-server process when possible."),
|
|
345
447
|
}, async ({ session_id, shutdown_server }) => {
|
|
448
|
+
const disabled = workerModeDisabledToolResult("Local Codex session orchestration");
|
|
449
|
+
if (disabled) {
|
|
450
|
+
return {
|
|
451
|
+
content: [{ type: "text", text: JSON.stringify(disabled, null, 2) }],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
346
454
|
const provider = getManagedAgentProvider("codex");
|
|
347
455
|
const stopped = await provider.stopLocalSession({
|
|
348
456
|
session_id,
|
|
@@ -7,8 +7,9 @@ export function registerStatusTools(server) {
|
|
|
7
7
|
registerPostStatusTool(server);
|
|
8
8
|
registerPostReasoningTool(server);
|
|
9
9
|
}
|
|
10
|
-
export function registerMessageTools(server) {
|
|
10
|
+
export function registerMessageTools(server, options = {}) {
|
|
11
11
|
registerSendMessageTool(server);
|
|
12
12
|
registerReadMessagesTool(server);
|
|
13
|
-
|
|
13
|
+
if (options.includeDeliveryLoop !== false)
|
|
14
|
+
registerWaitForMessagesTool(server);
|
|
14
15
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { encodeRoomIdPath } from "../../../room-id.js";
|
|
2
|
+
import { ApiError, appendIncludePromptOnly, getLocalChatMessages, isMissingRouteError, roomScopedApiCall, } from "../../runtime.js";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return Boolean(value && typeof value === "object");
|
|
5
|
+
}
|
|
6
|
+
export async function findLocalMessageById(roomId, messageId) {
|
|
7
|
+
let afterCursor;
|
|
8
|
+
for (;;) {
|
|
9
|
+
const result = await getLocalChatMessages(roomId, {
|
|
10
|
+
after: afterCursor,
|
|
11
|
+
include_prompt_only: true,
|
|
12
|
+
});
|
|
13
|
+
const messages = (result.messages ?? []).filter(isRecord);
|
|
14
|
+
const match = messages.find((message) => message.id === messageId);
|
|
15
|
+
if (match)
|
|
16
|
+
return match;
|
|
17
|
+
if (!result.has_more || messages.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
const lastMessage = messages[messages.length - 1];
|
|
20
|
+
afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
21
|
+
if (!afterCursor)
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function findRemoteMessageById(input) {
|
|
26
|
+
if (input.roomId) {
|
|
27
|
+
try {
|
|
28
|
+
const result = await roomScopedApiCall({
|
|
29
|
+
room_id: input.roomId,
|
|
30
|
+
room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages/${encodeURIComponent(input.messageId)}`),
|
|
31
|
+
project_path: () => "",
|
|
32
|
+
// Context lookups must not advance the room session cursor.
|
|
33
|
+
preserve_session_cursor: true,
|
|
34
|
+
});
|
|
35
|
+
return isRecord(result.message) ? result.message : null;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (isMissingRouteError(error)) {
|
|
39
|
+
// Older API without the by-id route: fall back to scanning history.
|
|
40
|
+
}
|
|
41
|
+
else if (error instanceof ApiError && error.status === 404) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return scanRemoteMessageHistoryById(input);
|
|
50
|
+
}
|
|
51
|
+
async function scanRemoteMessageHistoryById(input) {
|
|
52
|
+
let afterCursor;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const query = new URLSearchParams();
|
|
55
|
+
if (afterCursor)
|
|
56
|
+
query.set("after", afterCursor);
|
|
57
|
+
const qs = query.toString();
|
|
58
|
+
const result = await roomScopedApiCall({
|
|
59
|
+
room_id: input.roomId,
|
|
60
|
+
project_id: input.projectId,
|
|
61
|
+
room_path: (roomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(roomId)}/messages${qs ? `?${qs}` : ""}`),
|
|
62
|
+
project_path: (projectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(projectId)}/messages${qs ? `?${qs}` : ""}`),
|
|
63
|
+
});
|
|
64
|
+
const messages = (result.messages ?? []).filter(isRecord);
|
|
65
|
+
const match = messages.find((message) => message.id === input.messageId);
|
|
66
|
+
if (match)
|
|
67
|
+
return match;
|
|
68
|
+
if (!result.has_more || messages.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const lastMessage = messages[messages.length - 1];
|
|
71
|
+
afterCursor = typeof lastMessage?.id === "string" ? lastMessage.id : undefined;
|
|
72
|
+
if (!afterCursor)
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -24,7 +24,7 @@ function withRecencyTelemetry(output, selection) {
|
|
|
24
24
|
}
|
|
25
25
|
// Fetch the most recent messages by paging BACKWARDS from the tail
|
|
26
26
|
// (before=latest), so a limited read never walks the full room history.
|
|
27
|
-
async function fetchRecentRemoteMessages(input) {
|
|
27
|
+
export async function fetchRecentRemoteMessages(input) {
|
|
28
28
|
const collected = [];
|
|
29
29
|
let beforeCursor = "latest";
|
|
30
30
|
let truncated = false;
|