letagents 0.12.13 → 0.12.14

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.
@@ -5,7 +5,7 @@ import { normalizeAgentBaseName } from "../../../shared/codenames.js";
5
5
  import { formatOwnerAttribution } from "../../../shared/agent-identity.js";
6
6
  import { LETAGENTS_AGENT_SESSION_ID_HEADER, LETAGENTS_AGENT_SESSION_TOKEN_HEADER, } from "../../../shared/request-headers.js";
7
7
  import { AGENT_INSTANCE_UUID, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, } from "./identity.js";
8
- import { isSupervisedBoundedTurn, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
8
+ import { hasSupervisedWorkerAuthority, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
9
9
  import { resolveCurrentSupervisedWorkerSession } from "./supervisor-bridge.js";
10
10
  import { getDaemonToolExecutionContext, getRuntimeWorkingDirectory } from "./daemon-tool-context.js";
11
11
  // A worker bearer already represents a server-side worker session. This local
@@ -166,7 +166,7 @@ export async function resolveWorkerToolIdentity(input) {
166
166
  }
167
167
  const agentSession = input.agentSessionId
168
168
  ? requireWorkerAgentSession(input.roomId, input.agentSessionId)
169
- : input.roomId && !isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(input.roomId)
169
+ : input.roomId && !hasSupervisedWorkerAuthority() && await isLocalRoomStorageEnabled(input.roomId)
170
170
  ? await ensureLocalWorkerAgentSession(input.roomId)
171
171
  : requireWorkerAgentSession(input.roomId, input.agentSessionId);
172
172
  return {
@@ -1,5 +1,6 @@
1
1
  export const EXECUTION_PROFILES = [
2
2
  "supervised_room_turn",
3
+ "supervised_mcp_polling",
3
4
  "autonomous_mcp_worker",
4
5
  "interactive_desktop",
5
6
  ];
@@ -4,7 +4,7 @@ import { isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, touchRoo
4
4
  import { apiCall, isMissingRouteError } from "./api.js";
5
5
  import { agentSessionCredentials, identityFromAgentSession } from "./agent-sessions.js";
6
6
  import { getSessionLivenessRegistration } from "./identity.js";
7
- import { isSupervisedBoundedTurn } from "./worker-bearer.js";
7
+ import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
8
8
  const roomPresenceByIdentity = new Map();
9
9
  export function getRememberedRoomPresence(roomId, identity) {
10
10
  if (!roomId || !identity) {
@@ -19,7 +19,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
19
19
  }
20
20
  roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, resolvedIdentity.actor_label), presence);
21
21
  const { localRoomId, cloudRoomId } = await resolveLocalRoomStorageIdentifiers(roomId);
22
- if (!isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(roomId)) {
22
+ if (!hasSupervisedWorkerAuthority() && await isLocalRoomStorageEnabled(roomId)) {
23
23
  touchRoomSession(localRoomId || roomId);
24
24
  return;
25
25
  }
@@ -39,7 +39,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
39
39
  ...agentSessionCredentials(agentSession),
40
40
  }),
41
41
  });
42
- if (!isSupervisedBoundedTurn())
42
+ if (!hasSupervisedWorkerAuthority())
43
43
  touchRoomSession(apiRoomId);
44
44
  }
45
45
  catch (error) {
@@ -4,9 +4,9 @@ import { apiCall, isMissingRouteError, } from "./api.js";
4
4
  import { maybeHandleRepoRoomAuthRequired } from "./device-auth.js";
5
5
  import { getLastMessageId } from "./messages.js";
6
6
  import { currentRoom, getCurrentSupervisedRoomAuthority } from "./room-state.js";
7
- import { isSupervisedBoundedTurn } from "./worker-bearer.js";
7
+ import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
8
8
  export async function roomScopedApiCall(input) {
9
- const supervised = isSupervisedBoundedTurn();
9
+ const supervised = hasSupervisedWorkerAuthority();
10
10
  const exactRoomAuthority = supervised ? getCurrentSupervisedRoomAuthority() : null;
11
11
  if (supervised && (!exactRoomAuthority || input.room_id !== exactRoomAuthority)) {
12
12
  throw new Error("The daemon-supervised API request is missing its exact per-call room authority.");
@@ -3,7 +3,7 @@ import { getStoredAgentIdentity, saveRoomSession, touchRoomSession, } from "../.
3
3
  import { getCanonicalRoomWebPath, } from "../../room-id.js";
4
4
  import { getApiUrl, getLetagentsToken } from "./api.js";
5
5
  import { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, } from "./identity.js";
6
- import { isSupervisedBoundedTurn } from "./worker-bearer.js";
6
+ import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
7
7
  import { getCurrentSupervisedRoomAuthority, runWithSupervisedRoomAuthority, } from "./supervised-room-authority.js";
8
8
  let mcpServer = null;
9
9
  let sseClient = null;
@@ -92,7 +92,7 @@ export function toPublicRoomResponse(response, fallbackRoomId) {
92
92
  }
93
93
  export function rememberRoom(state, lastMessageId) {
94
94
  currentRoom = state;
95
- if (isSupervisedBoundedTurn())
95
+ if (hasSupervisedWorkerAuthority())
96
96
  return state;
97
97
  saveRoomSession({
98
98
  room_id: state.room_id,
@@ -123,7 +123,7 @@ export function rememberRoom(state, lastMessageId) {
123
123
  return state;
124
124
  }
125
125
  export function touchCurrentRoom(lastMessageId) {
126
- if (isSupervisedBoundedTurn())
126
+ if (hasSupervisedWorkerAuthority())
127
127
  return;
128
128
  if (!currentRoom) {
129
129
  return;
@@ -131,7 +131,7 @@ export function touchCurrentRoom(lastMessageId) {
131
131
  touchRoomSession(currentRoom.room_id, lastMessageId);
132
132
  }
133
133
  export function getTargetRoomId(roomId) {
134
- if (isSupervisedBoundedTurn()) {
134
+ if (hasSupervisedWorkerAuthority()) {
135
135
  const exactRoomAuthority = getCurrentSupervisedRoomAuthority();
136
136
  if (!exactRoomAuthority) {
137
137
  throw new Error("The daemon-supervised tool has not received its exact room authority.");
@@ -162,7 +162,7 @@ export function toPublicCurrentRoomState() {
162
162
  * repository inspection and can safely rebind after a durable room move.
163
163
  */
164
164
  export function runWithCurrentSupervisedRoom(roomId, callback) {
165
- if (!isSupervisedBoundedTurn()) {
165
+ if (!hasSupervisedWorkerAuthority()) {
166
166
  throw new Error("Only a daemon-supervised bounded turn can bind supervisor room authority.");
167
167
  }
168
168
  const normalized = roomId.trim();
@@ -172,7 +172,7 @@ export function runWithCurrentSupervisedRoom(roomId, callback) {
172
172
  return runWithSupervisedRoomAuthority(normalized, callback);
173
173
  }
174
174
  export function getFallbackProjectId() {
175
- if (isSupervisedBoundedTurn())
175
+ if (hasSupervisedWorkerAuthority())
176
176
  return null;
177
177
  return currentRoom?.project_id ?? null;
178
178
  }
@@ -9,7 +9,7 @@ import { ensureAgentIdentity, toPublicAgentIdentity, withAgentIdentity, } from "
9
9
  import { withJoinRoomAgentPrompt } from "./messages.js";
10
10
  import { syncRoomPresence } from "./presence.js";
11
11
  import { rememberRoom, toPublicRoomResponse, toRoomState, } from "./room-state.js";
12
- import { isSupervisedBoundedTurn, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
12
+ import { hasSupervisedWorkerAuthority, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
13
13
  export function normalizeJoinSessionMode(value) {
14
14
  return String(value || "").trim().toLowerCase() === "live" ? "live" : "current";
15
15
  }
@@ -30,7 +30,7 @@ function parseGeneratedGitRefRoomIdentifier(identifier) {
30
30
  };
31
31
  }
32
32
  export async function joinRoomIdentifier(identifier, joinedVia, options = {}) {
33
- if (isSupervisedBoundedTurn()) {
33
+ if (hasSupervisedWorkerAuthority()) {
34
34
  throw new Error("Room joins and creation are disabled during a daemon-supervised bounded turn.");
35
35
  }
36
36
  const roomId = joinedVia === "join_code" ? normalizeInviteCode(identifier) : identifier.trim();
@@ -191,7 +191,7 @@ export async function joinRoomIdentifierWithoutImplicitGitRefCreate(identifier,
191
191
  }));
192
192
  }
193
193
  export async function createInviteRoom() {
194
- if (isSupervisedBoundedTurn()) {
194
+ if (hasSupervisedWorkerAuthority()) {
195
195
  throw new Error("Room joins and creation are disabled during a daemon-supervised bounded turn.");
196
196
  }
197
197
  const project = await apiCall("/projects", { method: "POST" });
@@ -6,12 +6,86 @@ import { join } from "node:path";
6
6
  import { parsePositivePgIntegerScopedId } from "../../../../shared/message-contracts.mjs";
7
7
  import { getCurrentSupervisedRoomAuthority } from "./supervised-room-authority.js";
8
8
  const NEGOTIATION_PROTOCOL_VERSION = 1;
9
- const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2]);
9
+ const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2, 3]);
10
10
  const DEFAULT_REQUEST_TIMEOUT_MS = 5_000;
11
11
  const CONFIRMED_BINDING_VERIFY_TIMEOUT_MS = 250;
12
12
  const SUPERVISOR_CONTEXT_FILE = ".letagents-supervisor-context.json";
13
13
  const WORK_ATTEMPT_MARKER_FILE = ".letagents-work-attempt.json";
14
14
  const MAX_SUPERVISOR_CONTEXT_BYTES = 4 * 1024;
15
+ // An SDK request id is unique only within this MCP process lifetime.
16
+ const CUSTODIAL_POLLING_PROCESS_INCARNATION_ID = randomUUID();
17
+ /** Release reuses BEFORE's exact generation; it never authorizes against a successor. */
18
+ export async function authorizeCustodialPolling(toolName, prior, env = process.env, options = {}, waitRequest) {
19
+ const wait = waitRequest ? { ...waitRequest } : undefined;
20
+ if (toolName === "wait_for_messages") {
21
+ if (!wait || !(typeof wait.mcpRequestId === "string" || Number.isSafeInteger(wait.mcpRequestId))) {
22
+ throw new Error("Custodial wait is missing its exact MCP request id.");
23
+ }
24
+ if (!(wait.roomCursor === null || (typeof wait.roomCursor === "string" && parseRoomMessageNumber(wait.roomCursor) !== null))
25
+ || (prior && (!prior.wait || prior.wait.processIncarnationId !== CUSTODIAL_POLLING_PROCESS_INCARNATION_ID
26
+ || prior.wait.mcpRequestId !== wait.mcpRequestId || typeof wait.offeredFrontier !== "string"
27
+ || parseRoomMessageNumber(wait.offeredFrontier) === null || prior.roomCursor === null
28
+ || parseRoomMessageNumber(wait.offeredFrontier) < parseRoomMessageNumber(prior.roomCursor)))) {
29
+ throw new Error("Custodial wait receipt does not match its original invocation and cursor.");
30
+ }
31
+ }
32
+ else if (wait || prior?.wait)
33
+ throw new Error("Only custodial wait may acknowledge or offer a cursor.");
34
+ if (env.LETAGENTS_EXECUTION_PROFILE?.trim() !== "supervised_mcp_polling")
35
+ throw new Error("Custodial polling profile required.");
36
+ if (env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() === "1"
37
+ || env.LETAGENTS_TOKEN?.trim() || env.LETAGENTS_AGENT_SESSION_BEARER?.trim()) {
38
+ throw new Error("Custodial polling refuses bounded flags and environment credentials.");
39
+ }
40
+ const coordinates = prior?.coordinates ?? await resolveSupervisorCoordinates(supervisedContextSession(env), env, options);
41
+ if (!coordinates?.roomId || !coordinates.agentSessionId)
42
+ throw new Error("Custodial polling lacks exact worker coordinates.");
43
+ if ((wait?.requestedRoomId && wait.requestedRoomId !== coordinates.roomId)
44
+ || (wait?.requestedAgentSessionId && wait.requestedAgentSessionId !== coordinates.agentSessionId)) {
45
+ throw new Error("Custodial wait room or worker identity does not match its exact authority.");
46
+ }
47
+ const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
48
+ const negotiated = prior?.negotiated ?? await negotiateSupervisor(coordinates.socketPath, timeoutMs);
49
+ if (!negotiated.custodialPollingV1 || negotiated.generation === null)
50
+ throw new Error("Daemon does not support custodial_polling_v1.");
51
+ if (wait && !negotiated.custodialPollingOffersV1)
52
+ throw new Error("Daemon does not support custodialPollingOffersV1; refusing an unjournaled wait.");
53
+ const apiUrl = prior?.apiUrl ?? env.LETAGENTS_API_URL?.trim();
54
+ if (!apiUrl)
55
+ throw new Error("Custodial polling requires an explicit API URL.");
56
+ const response = await supervisorRequest(coordinates.socketPath, {
57
+ version: negotiated.protocolVersion, id: randomUUID(), method: "supervisor.authorize_custodial_polling",
58
+ params: {
59
+ entry_id: coordinates.entryId, room_id: coordinates.roomId, work_attempt_id: coordinates.workAttemptId,
60
+ execution_generation_id: coordinates.executionGenerationId, agent_session_id: coordinates.agentSessionId,
61
+ daemon_generation: negotiated.generation, api_url: apiUrl, contract: "custodial_polling_v1",
62
+ phase: prior ? "release" : "before", tool_name: toolName,
63
+ ...(prior ? { expected_configuration_revision: prior.configurationRevision } : {}),
64
+ ...(wait ? {
65
+ process_incarnation_id: CUSTODIAL_POLLING_PROCESS_INCARNATION_ID, mcp_request_id: wait.mcpRequestId,
66
+ ...(prior ? { expected_activation_id: prior.wait.activationId, expected_binding_epoch: prior.wait.bindingEpoch,
67
+ input_cursor: prior.roomCursor, offered_frontier: wait.offeredFrontier } : { room_cursor: wait.roomCursor }),
68
+ } : {}),
69
+ },
70
+ }, timeoutMs);
71
+ const result = response.result;
72
+ if (!response.ok || response.version !== negotiated.protocolVersion || !result || result.status !== "authorized" || result.contract !== "custodial_polling_v1"
73
+ || result.room_id !== coordinates.roomId || result.agent_session_id !== coordinates.agentSessionId
74
+ || !Number.isSafeInteger(result.configuration_revision) || Number(result.configuration_revision) < 1
75
+ || (prior && result.configuration_revision !== prior.configurationRevision)
76
+ || (wait && (typeof result.activation_id !== "string" || !result.activation_id.trim()
77
+ || !Number.isSafeInteger(result.binding_epoch) || Number(result.binding_epoch) < 1
78
+ || typeof result.room_cursor !== "string"
79
+ || (prior && (result.activation_id !== prior.wait.activationId || result.binding_epoch !== prior.wait.bindingEpoch
80
+ || result.room_cursor !== prior.roomCursor))))
81
+ || !(result.room_cursor === null || (typeof result.room_cursor === "string" && parseRoomMessageNumber(result.room_cursor) !== null))) {
82
+ throw new Error("Custodial polling authority was rejected or became stale.");
83
+ }
84
+ return { coordinates, negotiated, apiUrl, roomId: coordinates.roomId, agentSessionId: coordinates.agentSessionId,
85
+ roomCursor: result.room_cursor, configurationRevision: Number(result.configuration_revision),
86
+ ...(wait ? { wait: { processIncarnationId: CUSTODIAL_POLLING_PROCESS_INCARNATION_ID, mcpRequestId: wait.mcpRequestId,
87
+ activationId: String(result.activation_id), bindingEpoch: Number(result.binding_epoch) } } : {}) };
88
+ }
15
89
  const confirmedBindingsBySession = new Map();
16
90
  const confirmedRequestsBySession = new Map();
17
91
  const confirmedProtocolsBySession = new Map();
@@ -184,7 +258,8 @@ async function requireCurrentSupervisedCoordinates(env, options) {
184
258
  * authority for the exact worker session identity.
185
259
  */
186
260
  export async function borrowCurrentSupervisedWorkerCredential(env = process.env, options = {}) {
187
- if (env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1") {
261
+ if (env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1"
262
+ && env.LETAGENTS_EXECUTION_PROFILE?.trim() !== "supervised_mcp_polling") {
188
263
  return { state: "not_supervised" };
189
264
  }
190
265
  const seed = supervisedContextSession(env);
@@ -728,7 +803,9 @@ async function negotiateSupervisor(socketPath, timeoutMs) {
728
803
  const daemonIdentity = hasCompleteIdentity
729
804
  ? [result.generation, result.pid, result.started_at].join(":")
730
805
  : null;
731
- return { protocolVersion, daemonIdentity, generation: hasCompleteIdentity ? Number(result.generation) : null };
806
+ return { protocolVersion, daemonIdentity, generation: hasCompleteIdentity ? Number(result.generation) : null,
807
+ custodialPollingV1: result.capabilities?.custodialPollingV1 === true,
808
+ custodialPollingOffersV1: result.capabilities?.custodialPollingOffersV1 === true };
732
809
  }
733
810
  function supervisorRequest(socketPath, request, timeoutMs) {
734
811
  return new Promise((resolve, reject) => {
@@ -1,4 +1,11 @@
1
1
  const TOOL_SURFACE_BY_PROFILE = {
2
+ supervised_mcp_polling: {
3
+ agentSessionLifecycle: false,
4
+ deliveryLoop: true,
5
+ onboarding: false,
6
+ rental: false,
7
+ roomResume: false,
8
+ },
2
9
  supervised_room_turn: {
3
10
  agentSessionLifecycle: false,
4
11
  deliveryLoop: false,
@@ -13,13 +13,17 @@ export function getWorkerBearerRuntime() {
13
13
  const bearer = process.env.LETAGENTS_AGENT_SESSION_BEARER?.trim();
14
14
  const supervised = process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() === "1";
15
15
  const profile = process.env.LETAGENTS_EXECUTION_PROFILE?.trim();
16
+ const polling = profile === "supervised_mcp_polling";
16
17
  if (supervised !== (profile === "supervised_room_turn")) {
17
18
  return {
18
19
  mode: "invalid",
19
20
  error: "LETAGENTS_EXECUTION_PROFILE=supervised_room_turn and LETAGENTS_SUPERVISED_BOUNDED_TURNS=1 must be configured together.",
20
21
  };
21
22
  }
22
- if (!bearer && !supervised)
23
+ if (polling && (bearer || process.env.LETAGENTS_TOKEN?.trim())) {
24
+ return { mode: "invalid", error: "Custodial polling refuses environment credentials; borrow exact daemon worker authority." };
25
+ }
26
+ if (!bearer && !supervised && !polling)
23
27
  return { mode: "owner" };
24
28
  if (bearer && supervised) {
25
29
  return {
@@ -69,6 +73,13 @@ export function requireValidWorkerBearerRuntime() {
69
73
  return runtime;
70
74
  }
71
75
  export function isSupervisedBoundedTurn() {
76
+ return hasSupervisedWorkerAuthority() && !isCustodialPolling();
77
+ }
78
+ export function isCustodialPolling() {
79
+ return process.env.LETAGENTS_EXECUTION_PROFILE?.trim() === "supervised_mcp_polling";
80
+ }
81
+ /** Credential custody is independent of who owns room delivery. */
82
+ export function hasSupervisedWorkerAuthority() {
72
83
  return requireValidWorkerBearerRuntime().mode === "supervised";
73
84
  }
74
85
  export function workerModeDisabledToolResult(toolDescription = "This owner-auth onboarding tool") {
@@ -19,6 +19,7 @@ export function letAgentsRuntimeContract() {
19
19
  return {
20
20
  format: 1,
21
21
  profiles: {
22
+ supervised_mcp_polling: { contract: "custodial_polling_v1", tools: registeredToolNames("supervised_mcp_polling", "codex") },
22
23
  cursor_supervised_room_turn: {
23
24
  tools: registeredToolNames("supervised_room_turn", "cursor"),
24
25
  },
@@ -2,13 +2,13 @@
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
4
  import { isLocalRoomStorageEnabled as isStoredLocalRoomStorageEnabled, touchRoomSession as touchStoredRoomSession, } from "../local-state.js";
5
- import { isSupervisedBoundedTurn } from "./runtime/worker-bearer.js";
5
+ import { hasSupervisedWorkerAuthority } from "./runtime/worker-bearer.js";
6
6
  /** A daemon-supervised turn must always use its exact cloud worker route. */
7
7
  export async function isLocalRoomStorageEnabled(roomId) {
8
- return !isSupervisedBoundedTurn() && isStoredLocalRoomStorageEnabled(roomId);
8
+ return !hasSupervisedWorkerAuthority() && isStoredLocalRoomStorageEnabled(roomId);
9
9
  }
10
10
  export function touchRoomSession(roomId, lastMessageId) {
11
- if (!isSupervisedBoundedTurn())
11
+ if (!hasSupervisedWorkerAuthority())
12
12
  touchStoredRoomSession(roomId, lastMessageId);
13
13
  }
14
14
  export { API_URL, ApiError, apiCall, getAuthorizationHeader, getLetagentsToken, isMissingRouteError, parseApiErrorPayload, resolveApiPath, } from "./runtime/api.js";
@@ -1,5 +1,6 @@
1
+ import { parsePositivePgIntegerScopedId } from "../../../shared/message-contracts.mjs";
1
2
  import { runWithCurrentSupervisedRoom } from "./runtime/room-state.js";
2
- import { completeCurrentSupervisedEffect, executeCurrentSupervisedTool, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
3
+ import { completeCurrentSupervisedEffect, authorizeCustodialPolling, executeCurrentSupervisedTool, prepareCurrentSupervisedEffect, } from "./runtime/supervisor-bridge.js";
3
4
  const READ_TOOLS = new Set([
4
5
  "get_current_room",
5
6
  "check_repo",
@@ -22,6 +23,40 @@ export function supervisedToolIsMutation(toolName) {
22
23
  // frames. A read result can be returned live to the provider without copying
23
24
  // the entire payload into the durable effect journal.
24
25
  const MAX_DURABLE_READ_RESULT_BYTES = 16 * 1024;
26
+ /** Receipt only the bounded page actually returned by wait, never an API tail
27
+ * beyond that page or a cursor inferred from its last visible message. */
28
+ function custodialWaitFrontier(result, inputCursor, roomId) {
29
+ const content = result.content;
30
+ if (result.isError || content.length !== 1 || content[0]?.type !== "text")
31
+ throw new Error("Custodial wait returned no valid bounded page.");
32
+ let output;
33
+ try {
34
+ output = JSON.parse(content[0].text);
35
+ }
36
+ catch {
37
+ throw new Error("Custodial wait returned no valid bounded page.");
38
+ }
39
+ if (!output || Array.isArray(output) || !Array.isArray(output.messages)
40
+ || (output.room_id !== undefined && output.room_id !== roomId))
41
+ throw new Error("Custodial wait returned no valid bounded page.");
42
+ const noProgress = output.messages.length === 0 && (output.truncated === undefined || output.truncated === false)
43
+ && (output.omitted_message_count === undefined || output.omitted_message_count === 0)
44
+ && (output.skipped_message_count === undefined || output.skipped_message_count === 0)
45
+ && (output.skipped_message_ids === undefined || (Array.isArray(output.skipped_message_ids) && output.skipped_message_ids.length === 0));
46
+ const frontier = output.last_observed_message_id;
47
+ if (frontier === undefined || frontier === null) {
48
+ if (!noProgress) {
49
+ throw new Error("Custodial wait is missing its bounded observed frontier.");
50
+ }
51
+ return inputCursor;
52
+ }
53
+ const number = parsePositivePgIntegerScopedId(frontier, "msg");
54
+ const inputNumber = parsePositivePgIntegerScopedId(inputCursor, "msg");
55
+ if (number === null || inputNumber === null || number < inputNumber || (number === inputNumber && !noProgress)) {
56
+ throw new Error("Custodial wait returned an invalid observed frontier.");
57
+ }
58
+ return String(frontier);
59
+ }
25
60
  function instruction(text, data = {}) {
26
61
  const payload = { ...data, instruction: text };
27
62
  return {
@@ -47,7 +82,7 @@ const productionDependencies = {
47
82
  withRoom: runWithCurrentSupervisedRoom,
48
83
  };
49
84
  export function profileAwareToolServer(server, profile, dependencies = productionDependencies, supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null) {
50
- if (profile !== "supervised_room_turn")
85
+ if (profile !== "supervised_room_turn" && profile !== "supervised_mcp_polling")
51
86
  return server;
52
87
  return new Proxy(server, {
53
88
  get(target, property, receiver) {
@@ -61,6 +96,43 @@ export function profileAwareToolServer(server, profile, dependencies = productio
61
96
  if (typeof callback !== "function")
62
97
  throw new Error(`Tool ${name} has no callback.`);
63
98
  const wrapped = async (...call) => {
99
+ if (profile === "supervised_mcp_polling") {
100
+ const authorize = dependencies.authorizePolling ?? ((toolName, prior, wait) => authorizeCustodialPolling(toolName, prior, process.env, {}, wait));
101
+ const input = call[0];
102
+ const extra = (call.length > 1 ? call.at(-1) : undefined);
103
+ let wait;
104
+ if (name === "wait_for_messages") {
105
+ const requestId = extra?.requestId;
106
+ if (!(typeof requestId === "string" || (typeof requestId === "number" && Number.isSafeInteger(requestId)))) {
107
+ throw new Error("Custodial wait is missing its exact MCP request id.");
108
+ }
109
+ if (input?.after_message_id != null && typeof input.after_message_id !== "string")
110
+ throw new Error("Custodial wait requires a valid requested cursor.");
111
+ if ((input?.room_id != null && typeof input.room_id !== "string")
112
+ || (input?.agent_session_id != null && typeof input.agent_session_id !== "string"))
113
+ throw new Error("Custodial wait requires valid requested identity.");
114
+ wait = { mcpRequestId: requestId, roomCursor: input?.after_message_id ?? null,
115
+ ...(typeof input?.room_id === "string" ? { requestedRoomId: input.room_id } : {}),
116
+ ...(typeof input?.agent_session_id === "string" ? { requestedAgentSessionId: input.agent_session_id } : {}) };
117
+ }
118
+ const authority = await authorize(name, undefined, wait);
119
+ return dependencies.withRoom(authority.roomId, async () => {
120
+ if (input?.room_id && input.room_id !== authority.roomId)
121
+ throw new Error("Custodial tool room does not match its exact authority.");
122
+ if (name === "wait_for_messages") {
123
+ if (!authority.roomCursor)
124
+ throw new Error("Custodial polling has no durable cursor; refusing a tail fallback.");
125
+ call[0] = { ...input, after_message_id: authority.roomCursor };
126
+ }
127
+ const result = await callback(...call);
128
+ if (wait)
129
+ await authorize(name, authority, { ...wait,
130
+ offeredFrontier: custodialWaitFrontier(result, authority.roomCursor, authority.roomId) });
131
+ else if (name === "read_messages")
132
+ await authorize(name, authority);
133
+ return result;
134
+ });
135
+ }
64
136
  const extra = (call.at(-1) ?? {});
65
137
  const input = call.length > 1 ? call[0] : {};
66
138
  if (extra.requestId === undefined || extra.requestId === null || String(extra.requestId).trim() === "") {
@@ -2,7 +2,8 @@ import { z } from "zod";
2
2
  import { getPollTimeoutCapMs } from "../../../../shared/poll-timeout-cap.js";
3
3
  import { encodeRoomIdPath } from "../../../room-id.js";
4
4
  import { agentSessionCredentials, AGENT_MESSAGE_BODY_MAX_BYTES, appendIncludePromptOnly, boundAgentMessageOutput, buildAgentDeliveryHeaders, bindSupervisedWorkerSession, scheduleSupervisedWorkerCursorCheckpoint, currentRoom, ensureAgentIdentity, getFallbackProjectId, getLatestLocalChatMessages, getLocalImportedRoutingAuthority, getLocalChatMessages, getLocalChatThreadRoutingMembership, getLastMessageId, getRememberedRoomPresence, getStoredAgentRoutingStateSnapshot, getTargetRoomId, identityFromAgentSession, isLocalRoomStorageEnabled, listLocalActiveTaskOwnerLeases, resolveLocalRoomStorageIdentifiers, resolveAgentSession, roomScopedApiCall, syncRoomPresence, toAgentReadableMessages, touchRoomSession, WORKER_BEARER_AGENT_SESSION_ID, waitForLocalChatMessages, } from "../../runtime.js";
5
- import { requireValidWorkerBearerRuntime, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
5
+ import { requireValidWorkerBearerRuntime, isCustodialPolling, supervisedBoundedDeliveryDisabledToolResult, } from "../../runtime/worker-bearer.js";
6
+ import { resolveWorkerToolIdentity } from "../../runtime/agent-sessions.js";
6
7
  import { attachAgentMessageActivations, createGlobalAgentAddressResolver, decideAgentMessageActivation, isTaskOwnerFollowUpMessageText, } from "../../../../shared/activation-routing.js";
7
8
  import { normalizeRoutingSender } from "../../../../../shared/routing-aliases.mjs";
8
9
  import { findLocalMessageById, findRemoteMessageById } from "./message-lookup.js";
@@ -462,14 +463,20 @@ export function registerWaitForMessagesTool(server) {
462
463
  const targetProjectId = getFallbackProjectId();
463
464
  const localRoomId = targetRoomId ?? currentRoom?.room_id ?? targetProjectId;
464
465
  const sessionRoomId = targetRoomId ?? currentRoom?.room_id ?? localRoomId ?? null;
465
- const routingStateSnapshot = getStoredAgentRoutingStateSnapshot(sessionRoomId ?? "");
466
+ const custodial = isCustodialPolling();
467
+ const routingStateSnapshot = custodial ? { complete: true } : getStoredAgentRoutingStateSnapshot(sessionRoomId ?? "");
466
468
  const localStorageEnabled = Boolean(localRoomId && await isLocalRoomStorageEnabled(localRoomId));
467
469
  if (localStorageEnabled && !routingStateSnapshot.complete) {
468
470
  throw new Error("Local agent routing state is unavailable; retry after restoring the state file.");
469
471
  }
470
- const identity = await ensureAgentIdentity();
471
- const agentSession = resolveWaitAgentSession(sessionRoomId, agent_session_id);
472
- if (agentSession) {
472
+ const exactIdentity = custodial ? await resolveWorkerToolIdentity({ roomId: sessionRoomId, agentSessionId: agent_session_id }) : null;
473
+ const identity = exactIdentity?.identity ?? await ensureAgentIdentity();
474
+ const agentSession = exactIdentity?.agentSession ?? resolveWaitAgentSession(sessionRoomId, agent_session_id);
475
+ if (custodial) {
476
+ if (!agentSession || !after_message_id)
477
+ throw new Error("Custodial polling requires exact worker identity and durable cursor.");
478
+ }
479
+ else if (agentSession) {
473
480
  // Registration (or a successor generation) must bind strictly once.
474
481
  // Later waits use a read-only exact verification capped at 250ms, so a
475
482
  // wedged daemon cannot consume the room-poll budget and an old worker
@@ -570,6 +577,7 @@ export function registerWaitForMessagesTool(server) {
570
577
  project_id: targetProjectId,
571
578
  room_path: (targetRoomId) => appendIncludePromptOnly(`/rooms/${encodeRoomIdPath(targetRoomId)}/messages/poll?${queryString}`),
572
579
  project_path: (targetProjectId) => appendIncludePromptOnly(`/projects/${encodeURIComponent(targetProjectId)}/messages/poll?${queryString}`),
580
+ preserve_session_cursor: true,
573
581
  options: buildWaitForMessagesRequestOptions({
574
582
  deliveryHeaders,
575
583
  signal: AbortSignal.timeout(clientTimeout),
@@ -633,12 +641,14 @@ export function registerWaitForMessagesTool(server) {
633
641
  if (bounded.omittedMessageCount > 0) {
634
642
  output.omitted_message_count = bounded.omittedMessageCount;
635
643
  }
636
- if (apiObservedCursor)
637
- output.last_observed_message_id = apiObservedCursor;
644
+ // The API cursor can cover concealed messages, but not visible messages
645
+ // omitted by our own byte bound. Resume after the retained page instead.
646
+ const observedCursor = !bounded.truncated && apiObservedCursor
647
+ ? apiObservedCursor
648
+ : routing.last_observed_message_id ?? undefined;
649
+ if (observedCursor)
650
+ output.last_observed_message_id = observedCursor;
638
651
  if (targetRoomId) {
639
- const observedCursor = apiObservedCursor
640
- ?? routing.last_observed_message_id
641
- ?? getLastMessageId(output);
642
652
  touchRoomSession(targetRoomId, observedCursor);
643
653
  if (allMessages.length > 0 && agentSession) {
644
654
  const firstMsg = allMessages[0];
@@ -312,7 +312,7 @@ export function resolveGloballyAddressedAgentKeys(message, identities) {
312
312
  * Build the room-wide alias authority once, then resolve a page of legacy
313
313
  * messages without rebuilding every active worker alias set per message.
314
314
  */
315
- export function createGlobalAgentAddressResolver(identities) {
315
+ export function createGlobalAgentAddressResolver(identities, options = {}) {
316
316
  const keysByAlias = new Map();
317
317
  for (const identity of identities) {
318
318
  const key = normalizedString(identity.agent_key);
@@ -324,6 +324,23 @@ export function createGlobalAgentAddressResolver(identities) {
324
324
  keysByAlias.set(alias, keys);
325
325
  }
326
326
  }
327
+ const resolveExplicitMentionKey = (keys) => {
328
+ if (!keys || keys.size === 0)
329
+ return null;
330
+ if (keys.size === 1)
331
+ return keys.values().next().value;
332
+ const preferredMatches = [...keys].filter((key) => options.preferredExplicitMentionAgentKeys?.has(key));
333
+ if (preferredMatches.length !== 1)
334
+ return null;
335
+ const ownerScopes = new Set();
336
+ for (const key of keys) {
337
+ const scope = options.explicitMentionOwnerScopeByAgentKey?.get(key);
338
+ if (!scope)
339
+ return null;
340
+ ownerScopes.add(scope);
341
+ }
342
+ return ownerScopes.size === 1 ? preferredMatches[0] : null;
343
+ };
327
344
  return (message) => {
328
345
  const mentions = extractMentionHandles(message.text);
329
346
  const broadcast = mentions.some(isBroadcastHandle) || hasBroadcastAddress(message.text);
@@ -336,9 +353,9 @@ export function createGlobalAgentAddressResolver(identities) {
336
353
  const alias = normalizeMentionIdentityHandle(mention);
337
354
  if (!alias)
338
355
  continue;
339
- const keys = keysByAlias.get(alias);
340
- if (keys?.size === 1)
341
- explicitMentionKeys.add(keys.values().next().value);
356
+ const resolvedKey = resolveExplicitMentionKey(keysByAlias.get(alias));
357
+ if (resolvedKey)
358
+ explicitMentionKeys.add(resolvedKey);
342
359
  }
343
360
  const replyTargetKeys = new Set();
344
361
  const replyAliases = normalizedString(message.reply_to?.source) === "agent"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.12.13",
3
+ "version": "0.12.14",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",
@@ -26,7 +26,7 @@
26
26
  "test:web": "node --import tsx --test src/web/tests/*.test.ts src/web/src/composables/*.test.ts",
27
27
  "test:dependency-age": "node --test scripts/verify-dependency-age.test.mjs",
28
28
  "test:docker-context": "node --test scripts/verify-docker-context.test.mjs",
29
- "test:workflow-supply-chain": "node --test scripts/verify-workflow-supply-chain.test.mjs",
29
+ "test:workflow-supply-chain": "node --test scripts/verify-workflow-supply-chain.test.mjs scripts/verify-dependency-advisories.test.mjs",
30
30
  "test:notifications": "node --import tsx --test src/api/notifications/__tests__/*.test.ts",
31
31
  "verify:dependency-age": "node scripts/verify-dependency-age.mjs",
32
32
  "db:generate": "drizzle-kit generate",
@@ -70,7 +70,7 @@
70
70
  "@esbuild-kit/core-utils": {
71
71
  "esbuild": "^0.25.12"
72
72
  },
73
- "fast-uri": "3.1.5",
73
+ "fast-uri": "3.1.6",
74
74
  "hono": "4.12.34",
75
75
  "ip-address": "10.3.1"
76
76
  }
@@ -0,0 +1,32 @@
1
+ export const EXECUTION_APPROVAL_PROJECTION_VERSION: 1;
2
+ export const EXECUTION_APPROVAL_PROJECTION_MAX_FILES: 128;
3
+ export const EXECUTION_APPROVAL_PROJECTION_MAX_PATH_BYTES: 4096;
4
+ export const EXECUTION_APPROVAL_PROJECTION_MAX_BYTES: number;
5
+
6
+ export type ExecutionApprovalProjectionChangeKind = "add" | "delete" | "update" | "move";
7
+ export type ExecutionApprovalProjectionChange = {
8
+ path: string;
9
+ kind: ExecutionApprovalProjectionChangeKind;
10
+ move_path: string | null;
11
+ added_lines: number;
12
+ removed_lines: number;
13
+ diff_bytes: number;
14
+ };
15
+ export type ExecutionApprovalProjectionV1 = {
16
+ version: 1;
17
+ category: "file_change";
18
+ path_scope: "workspace_relative";
19
+ changes: ExecutionApprovalProjectionChange[];
20
+ totals: {
21
+ file_count: number;
22
+ added_lines: number;
23
+ removed_lines: number;
24
+ diff_bytes: number;
25
+ };
26
+ };
27
+
28
+ export function isExecutionApprovalProjectionPath(value: unknown): value is string;
29
+ /** Return the canonical allowlisted projection, or reject it as a whole. */
30
+ export function parseExecutionApprovalProjectionV1(value: unknown): ExecutionApprovalProjectionV1 | null;
31
+ /** Serialize only the canonical, bounded bytes that a delegate may see. */
32
+ export function serializeExecutionApprovalProjectionV1(value: unknown): string | null;
@@ -0,0 +1,107 @@
1
+ export const EXECUTION_APPROVAL_PROJECTION_VERSION = 1;
2
+ export const EXECUTION_APPROVAL_PROJECTION_MAX_FILES = 128;
3
+ export const EXECUTION_APPROVAL_PROJECTION_MAX_PATH_BYTES = 4096;
4
+ export const EXECUTION_APPROVAL_PROJECTION_MAX_BYTES = 24 * 1024;
5
+
6
+ const ROOT_KEYS = ["version", "category", "path_scope", "changes", "totals"];
7
+ const CHANGE_KEYS = ["path", "kind", "move_path", "added_lines", "removed_lines", "diff_bytes"];
8
+ const TOTAL_KEYS = ["file_count", "added_lines", "removed_lines", "diff_bytes"];
9
+ const CHANGE_KINDS = ["add", "delete", "update", "move"];
10
+ const UNSAFE_PATH_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/;
11
+
12
+ function exactKeys(value, keys) {
13
+ return !!value && typeof value === "object" && !Array.isArray(value)
14
+ && Object.keys(value).length === keys.length
15
+ && keys.every((key) => Object.hasOwn(value, key));
16
+ }
17
+
18
+ function count(value) {
19
+ return Number.isSafeInteger(value) && value >= 0;
20
+ }
21
+
22
+ export function isExecutionApprovalProjectionPath(value) {
23
+ return typeof value === "string"
24
+ && value.length > 0
25
+ && value.normalize("NFC") === value
26
+ && new TextEncoder().encode(value).byteLength <= EXECUTION_APPROVAL_PROJECTION_MAX_PATH_BYTES
27
+ && !value.startsWith("/")
28
+ && !value.includes("\\")
29
+ && !UNSAFE_PATH_CHARACTERS.test(value)
30
+ && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
31
+ }
32
+
33
+ function compareChanges(left, right) {
34
+ for (const [a, b] of [[left.path, right.path], [left.kind, right.kind], [left.move_path ?? "", right.move_path ?? ""]]) {
35
+ if (a < b) return -1;
36
+ if (a > b) return 1;
37
+ }
38
+ return 0;
39
+ }
40
+
41
+ /** Return the canonical allowlisted projection, or reject it as a whole. */
42
+ export function parseExecutionApprovalProjectionV1(value) {
43
+ if (!exactKeys(value, ROOT_KEYS)
44
+ || value.version !== EXECUTION_APPROVAL_PROJECTION_VERSION
45
+ || value.category !== "file_change"
46
+ || value.path_scope !== "workspace_relative"
47
+ || !Array.isArray(value.changes)
48
+ || value.changes.length < 1
49
+ || value.changes.length > EXECUTION_APPROVAL_PROJECTION_MAX_FILES
50
+ || !exactKeys(value.totals, TOTAL_KEYS)) return null;
51
+
52
+ const changes = [];
53
+ for (const candidate of value.changes) {
54
+ if (!exactKeys(candidate, CHANGE_KEYS)
55
+ || !isExecutionApprovalProjectionPath(candidate.path)
56
+ || !CHANGE_KINDS.includes(candidate.kind)
57
+ || !count(candidate.added_lines)
58
+ || !count(candidate.removed_lines)
59
+ || !count(candidate.diff_bytes)
60
+ || (candidate.kind === "move") !== (candidate.move_path !== null)
61
+ || (candidate.move_path !== null && !isExecutionApprovalProjectionPath(candidate.move_path))) return null;
62
+ changes.push({
63
+ path: candidate.path,
64
+ kind: candidate.kind,
65
+ move_path: candidate.move_path,
66
+ added_lines: candidate.added_lines,
67
+ removed_lines: candidate.removed_lines,
68
+ diff_bytes: candidate.diff_bytes,
69
+ });
70
+ }
71
+ changes.sort(compareChanges);
72
+ const occupied = new Set();
73
+ for (const change of changes) {
74
+ if (occupied.has(change.path)) return null;
75
+ occupied.add(change.path);
76
+ if (change.move_path !== null) {
77
+ if (occupied.has(change.move_path)) return null;
78
+ occupied.add(change.move_path);
79
+ }
80
+ }
81
+ const totals = changes.reduce((result, change) => ({
82
+ file_count: result.file_count + 1,
83
+ added_lines: result.added_lines + change.added_lines,
84
+ removed_lines: result.removed_lines + change.removed_lines,
85
+ diff_bytes: result.diff_bytes + change.diff_bytes,
86
+ }), { file_count: 0, added_lines: 0, removed_lines: 0, diff_bytes: 0 });
87
+ if (!Object.values(totals).every(Number.isSafeInteger)
88
+ || TOTAL_KEYS.some((key) => value.totals[key] !== totals[key])) return null;
89
+
90
+ return {
91
+ version: EXECUTION_APPROVAL_PROJECTION_VERSION,
92
+ category: "file_change",
93
+ path_scope: "workspace_relative",
94
+ changes,
95
+ totals,
96
+ };
97
+ }
98
+
99
+ /** Serialize only the canonical, bounded bytes that a delegate may see. */
100
+ export function serializeExecutionApprovalProjectionV1(value) {
101
+ const parsed = parseExecutionApprovalProjectionV1(value);
102
+ if (!parsed) return null;
103
+ const serialized = JSON.stringify(parsed);
104
+ return new TextEncoder().encode(serialized).byteLength <= EXECUTION_APPROVAL_PROJECTION_MAX_BYTES
105
+ ? serialized
106
+ : null;
107
+ }
@@ -0,0 +1,20 @@
1
+ export type ExecutionApprovalPublicationItem = {
2
+ publication_id: string;
3
+ room_id: string;
4
+ agent_key: string;
5
+ delegation_instance_id: string;
6
+ delegation_revision: number;
7
+ request_id: string;
8
+ request_version: number;
9
+ request_sha256: string;
10
+ projection_sha256: string;
11
+ published_at: string;
12
+ expires_at: string;
13
+ };
14
+
15
+ export function isExecutionApprovalPublicationIdentity(value: unknown): value is string;
16
+ export function isExecutionApprovalPublicationDigest(value: unknown): value is string;
17
+ export function isExecutionApprovalPublicationVersion(value: unknown): value is number;
18
+ export function parseExecutionApprovalPublicationItem(
19
+ value: unknown,
20
+ ): ExecutionApprovalPublicationItem | null;
@@ -0,0 +1,61 @@
1
+ const ITEM_KEYS = [
2
+ "publication_id",
3
+ "room_id",
4
+ "agent_key",
5
+ "delegation_instance_id",
6
+ "delegation_revision",
7
+ "request_id",
8
+ "request_version",
9
+ "request_sha256",
10
+ "projection_sha256",
11
+ "published_at",
12
+ "expires_at",
13
+ ];
14
+
15
+ function exactKeys(value, keys) {
16
+ return !!value && typeof value === "object" && !Array.isArray(value)
17
+ && Object.keys(value).length === keys.length
18
+ && keys.every((key) => Object.hasOwn(value, key));
19
+ }
20
+
21
+ export function isExecutionApprovalPublicationIdentity(value) {
22
+ return typeof value === "string"
23
+ && value.length > 0
24
+ && value.length <= 512
25
+ && value.trim() === value
26
+ && !/[\u0000-\u001f\u007f]/.test(value);
27
+ }
28
+
29
+ export function isExecutionApprovalPublicationDigest(value) {
30
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
31
+ }
32
+
33
+ export function isExecutionApprovalPublicationVersion(value) {
34
+ return Number.isSafeInteger(value) && value >= 1 && value <= 2_147_483_647;
35
+ }
36
+
37
+ function canonicalTimestamp(value) {
38
+ if (typeof value !== "string") return null;
39
+ const timestamp = Date.parse(value);
40
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value
41
+ ? timestamp
42
+ : null;
43
+ }
44
+
45
+ /** Parse the exact public inventory item shared by the API and browser. */
46
+ export function parseExecutionApprovalPublicationItem(value) {
47
+ if (!exactKeys(value, ITEM_KEYS)
48
+ || !isExecutionApprovalPublicationIdentity(value.publication_id)
49
+ || !isExecutionApprovalPublicationIdentity(value.room_id)
50
+ || !isExecutionApprovalPublicationIdentity(value.agent_key)
51
+ || !isExecutionApprovalPublicationIdentity(value.delegation_instance_id)
52
+ || !isExecutionApprovalPublicationVersion(value.delegation_revision)
53
+ || !isExecutionApprovalPublicationIdentity(value.request_id)
54
+ || !isExecutionApprovalPublicationVersion(value.request_version)
55
+ || !isExecutionApprovalPublicationDigest(value.request_sha256)
56
+ || !isExecutionApprovalPublicationDigest(value.projection_sha256)) return null;
57
+ const publishedAt = canonicalTimestamp(value.published_at);
58
+ const expiresAt = canonicalTimestamp(value.expires_at);
59
+ if (publishedAt === null || expiresAt === null || expiresAt <= publishedAt) return null;
60
+ return Object.fromEntries(ITEM_KEYS.map((key) => [key, value[key]]));
61
+ }
@@ -0,0 +1,53 @@
1
+ import type { ExecutionApprovalProjectionV1 } from "./execution-approval-projection.mjs";
2
+ import type { ExecutionApprovalPublicationItem } from "./execution-approval-publication-item.mjs";
3
+
4
+ export {
5
+ isExecutionApprovalPublicationDigest,
6
+ isExecutionApprovalPublicationIdentity,
7
+ isExecutionApprovalPublicationVersion,
8
+ parseExecutionApprovalPublicationItem,
9
+ } from "./execution-approval-publication-item.mjs";
10
+ export type { ExecutionApprovalPublicationItem } from "./execution-approval-publication-item.mjs";
11
+
12
+ export const EXECUTION_APPROVAL_PUBLICATION_VERSION: 1;
13
+ export const EXECUTION_APPROVAL_PUBLICATION_MAX_JSON_BYTES: number;
14
+
15
+ export type ExecutionApprovalPublicationInput = {
16
+ version: 1;
17
+ room_id: string;
18
+ source_message_id: string;
19
+ delegation_instance_id: string;
20
+ delegation_revision: number;
21
+ request_id: string;
22
+ request_version: number;
23
+ request_sha256: string;
24
+ projection_sha256: string;
25
+ projection_json: string;
26
+ produced_at: string;
27
+ expires_at: string;
28
+ };
29
+
30
+ export type ExecutionApprovalPublicationReceipt = {
31
+ status: "created" | "replayed";
32
+ publication_digest: string;
33
+ publication: ExecutionApprovalPublicationItem;
34
+ };
35
+
36
+ export type ExecutionApprovalPublicationCloseInput = {
37
+ publication_digest: string;
38
+ };
39
+
40
+ export type ExecutionApprovalPublicationCloseReceipt = {
41
+ status: "closed" | "replayed";
42
+ publication_id: string;
43
+ publication_digest: string;
44
+ closed_at: string;
45
+ };
46
+
47
+ export function parseExecutionApprovalPublicationInput(value: unknown): ExecutionApprovalPublicationInput | null;
48
+ export function parseExecutionApprovalPublicationReceipt(value: unknown): ExecutionApprovalPublicationReceipt | null;
49
+ export function parseExecutionApprovalPublicationCloseInput(value: unknown): ExecutionApprovalPublicationCloseInput | null;
50
+ export function parseExecutionApprovalPublicationCloseReceipt(value: unknown): ExecutionApprovalPublicationCloseReceipt | null;
51
+ export function executionApprovalPublicationSha256(value: unknown): string | null;
52
+
53
+ export type { ExecutionApprovalProjectionV1 };
@@ -0,0 +1,136 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ parseExecutionApprovalProjectionV1,
5
+ serializeExecutionApprovalProjectionV1,
6
+ } from "./execution-approval-projection.mjs";
7
+ import {
8
+ isExecutionApprovalPublicationDigest,
9
+ isExecutionApprovalPublicationIdentity,
10
+ isExecutionApprovalPublicationVersion,
11
+ parseExecutionApprovalPublicationItem,
12
+ } from "./execution-approval-publication-item.mjs";
13
+
14
+ export {
15
+ isExecutionApprovalPublicationDigest,
16
+ isExecutionApprovalPublicationIdentity,
17
+ isExecutionApprovalPublicationVersion,
18
+ parseExecutionApprovalPublicationItem,
19
+ } from "./execution-approval-publication-item.mjs";
20
+
21
+ export const EXECUTION_APPROVAL_PUBLICATION_VERSION = 1;
22
+ export const EXECUTION_APPROVAL_PUBLICATION_MAX_JSON_BYTES = 24 * 1024;
23
+
24
+ const INPUT_KEYS = [
25
+ "version",
26
+ "room_id",
27
+ "source_message_id",
28
+ "delegation_instance_id",
29
+ "delegation_revision",
30
+ "request_id",
31
+ "request_version",
32
+ "request_sha256",
33
+ "projection_sha256",
34
+ "projection_json",
35
+ "produced_at",
36
+ "expires_at",
37
+ ];
38
+ const RECEIPT_KEYS = ["status", "publication_digest", "publication"];
39
+ const CLOSE_INPUT_KEYS = ["publication_digest"];
40
+ const CLOSE_RECEIPT_KEYS = ["status", "publication_id", "publication_digest", "closed_at"];
41
+
42
+ function exactKeys(value, keys) {
43
+ return !!value && typeof value === "object" && !Array.isArray(value)
44
+ && Object.keys(value).length === keys.length
45
+ && keys.every((key) => Object.hasOwn(value, key));
46
+ }
47
+
48
+ function canonicalTimestamp(value) {
49
+ if (typeof value !== "string") return null;
50
+ const timestamp = Date.parse(value);
51
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value
52
+ ? timestamp
53
+ : null;
54
+ }
55
+
56
+ function sourceMessage(value) {
57
+ return typeof value === "string"
58
+ && /^msg_[1-9]\d{0,9}$/.test(value)
59
+ && Number(value.slice(4)) <= 2_147_483_647;
60
+ }
61
+
62
+ function canonicalProjection(value) {
63
+ if (typeof value !== "string"
64
+ || new TextEncoder().encode(value).byteLength > EXECUTION_APPROVAL_PUBLICATION_MAX_JSON_BYTES) return false;
65
+ let parsed;
66
+ try { parsed = JSON.parse(value); } catch { return false; }
67
+ const projection = parseExecutionApprovalProjectionV1(parsed);
68
+ return projection !== null && serializeExecutionApprovalProjectionV1(projection) === value;
69
+ }
70
+
71
+ /** Parse exact daemon-to-server publication bytes; digest verification remains the receiver's job. */
72
+ export function parseExecutionApprovalPublicationInput(value) {
73
+ if (!exactKeys(value, INPUT_KEYS)
74
+ || value.version !== EXECUTION_APPROVAL_PUBLICATION_VERSION
75
+ || !isExecutionApprovalPublicationIdentity(value.room_id)
76
+ || !sourceMessage(value.source_message_id)
77
+ || !isExecutionApprovalPublicationIdentity(value.delegation_instance_id)
78
+ || !isExecutionApprovalPublicationVersion(value.delegation_revision)
79
+ || !isExecutionApprovalPublicationIdentity(value.request_id)
80
+ || !isExecutionApprovalPublicationVersion(value.request_version)
81
+ || !isExecutionApprovalPublicationDigest(value.request_sha256)
82
+ || !isExecutionApprovalPublicationDigest(value.projection_sha256)
83
+ || !canonicalProjection(value.projection_json)) return null;
84
+ const producedAt = canonicalTimestamp(value.produced_at);
85
+ const expiresAt = canonicalTimestamp(value.expires_at);
86
+ if (producedAt === null || expiresAt === null || expiresAt <= producedAt) return null;
87
+ return Object.fromEntries(INPUT_KEYS.map((key) => [key, value[key]]));
88
+ }
89
+
90
+ export function parseExecutionApprovalPublicationReceipt(value) {
91
+ if (!exactKeys(value, RECEIPT_KEYS)
92
+ || !["created", "replayed"].includes(value.status)
93
+ || !isExecutionApprovalPublicationDigest(value.publication_digest)) return null;
94
+ const publication = parseExecutionApprovalPublicationItem(value.publication);
95
+ return publication ? {
96
+ status: value.status,
97
+ publication_digest: value.publication_digest,
98
+ publication,
99
+ } : null;
100
+ }
101
+
102
+ /** Parse the content-free host acknowledgement that a publication is no longer actionable. */
103
+ export function parseExecutionApprovalPublicationCloseInput(value) {
104
+ return exactKeys(value, CLOSE_INPUT_KEYS)
105
+ && isExecutionApprovalPublicationDigest(value.publication_digest)
106
+ ? { publication_digest: value.publication_digest }
107
+ : null;
108
+ }
109
+
110
+ export function parseExecutionApprovalPublicationCloseReceipt(value) {
111
+ if (!exactKeys(value, CLOSE_RECEIPT_KEYS)
112
+ || !["closed", "replayed"].includes(value.status)
113
+ || !isExecutionApprovalPublicationIdentity(value.publication_id)
114
+ || !isExecutionApprovalPublicationDigest(value.publication_digest)
115
+ || canonicalTimestamp(value.closed_at) === null) return null;
116
+ return Object.fromEntries(CLOSE_RECEIPT_KEYS.map((key) => [key, value[key]]));
117
+ }
118
+
119
+ function stableJson(value) {
120
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
121
+ if (value && typeof value === "object") {
122
+ return `{${Object.entries(value)
123
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
124
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
125
+ .join(",")}}`;
126
+ }
127
+ return JSON.stringify(value);
128
+ }
129
+
130
+ /** Digest exact, validated publication input with the server's stable-key wire algorithm. */
131
+ export function executionApprovalPublicationSha256(value) {
132
+ const publication = parseExecutionApprovalPublicationInput(value);
133
+ return publication === null
134
+ ? null
135
+ : createHash("sha256").update(stableJson(publication), "utf8").digest("hex");
136
+ }
@@ -0,0 +1,37 @@
1
+ export const EXECUTION_DELEGATION_DECISIONS: readonly ["allow_once", "deny"];
2
+ export const EXECUTION_DELEGATION_DECISION_APPLICABILITY_MS: number;
3
+
4
+ export type ExecutionDelegationDecisionChoice =
5
+ typeof EXECUTION_DELEGATION_DECISIONS[number];
6
+
7
+ export function isExecutionDelegationDecision(
8
+ value: unknown,
9
+ ): value is ExecutionDelegationDecisionChoice;
10
+ export function isExecutionDelegationIdentity(value: unknown): value is string;
11
+ export function isExecutionDelegationDigest(value: unknown): value is string;
12
+ export function isExecutionDelegationPositiveInt32(value: unknown): value is number;
13
+
14
+ export type ExecutionDelegationDecisionIntent = {
15
+ decision_id: string;
16
+ delegation_instance_id: string;
17
+ delegation_revision: number;
18
+ actor_account_id: string;
19
+ request_id: string;
20
+ request_version: number;
21
+ request_sha256: string;
22
+ projection_sha256: string;
23
+ decision: ExecutionDelegationDecisionChoice;
24
+ decided_at: string;
25
+ owner_account_id: string;
26
+ room_id: string;
27
+ agent_key: string;
28
+ approver_account_id: string;
29
+ category: "file_change";
30
+ risk_ceiling: "low";
31
+ scope_sha256: string;
32
+ };
33
+
34
+ /** Return the exact host-delivered decision intent, or reject it as a whole. */
35
+ export function parseExecutionDelegationDecisionIntent(
36
+ value: unknown,
37
+ ): ExecutionDelegationDecisionIntent | null;
@@ -0,0 +1,73 @@
1
+ export const EXECUTION_DELEGATION_DECISIONS = ["allow_once", "deny"];
2
+ export const EXECUTION_DELEGATION_DECISION_APPLICABILITY_MS = 24 * 60 * 60 * 1000;
3
+
4
+ export function isExecutionDelegationDecision(value) {
5
+ return EXECUTION_DELEGATION_DECISIONS.includes(value);
6
+ }
7
+
8
+ const INTENT_KEYS = [
9
+ "decision_id",
10
+ "delegation_instance_id",
11
+ "delegation_revision",
12
+ "actor_account_id",
13
+ "request_id",
14
+ "request_version",
15
+ "request_sha256",
16
+ "projection_sha256",
17
+ "decision",
18
+ "decided_at",
19
+ "owner_account_id",
20
+ "room_id",
21
+ "agent_key",
22
+ "approver_account_id",
23
+ "category",
24
+ "risk_ceiling",
25
+ "scope_sha256",
26
+ ];
27
+
28
+ function exactKeys(value, keys) {
29
+ return !!value && typeof value === "object" && !Array.isArray(value)
30
+ && Object.keys(value).length === keys.length
31
+ && keys.every((key) => Object.hasOwn(value, key));
32
+ }
33
+
34
+ export function isExecutionDelegationIdentity(value) {
35
+ return typeof value === "string"
36
+ && value.length > 0
37
+ && value.length <= 512
38
+ && value.trim() === value
39
+ && !/[\u0000-\u001f\u007f]/.test(value);
40
+ }
41
+
42
+ export function isExecutionDelegationDigest(value) {
43
+ return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
44
+ }
45
+
46
+ export function isExecutionDelegationPositiveInt32(value) {
47
+ return Number.isSafeInteger(value) && value >= 1 && value <= 2_147_483_647;
48
+ }
49
+
50
+ /** Return the exact host-delivered decision intent, or reject it as a whole. */
51
+ export function parseExecutionDelegationDecisionIntent(value) {
52
+ if (!exactKeys(value, INTENT_KEYS)
53
+ || !isExecutionDelegationIdentity(value.decision_id)
54
+ || !isExecutionDelegationIdentity(value.delegation_instance_id)
55
+ || !isExecutionDelegationPositiveInt32(value.delegation_revision)
56
+ || !isExecutionDelegationIdentity(value.actor_account_id)
57
+ || !isExecutionDelegationIdentity(value.request_id)
58
+ || !isExecutionDelegationPositiveInt32(value.request_version)
59
+ || !isExecutionDelegationDigest(value.request_sha256)
60
+ || !isExecutionDelegationDigest(value.projection_sha256)
61
+ || !isExecutionDelegationDecision(value.decision)
62
+ || typeof value.decided_at !== "string" || !Number.isFinite(Date.parse(value.decided_at))
63
+ || !isExecutionDelegationIdentity(value.owner_account_id)
64
+ || !isExecutionDelegationIdentity(value.room_id)
65
+ || !isExecutionDelegationIdentity(value.agent_key)
66
+ || !isExecutionDelegationIdentity(value.approver_account_id)
67
+ || value.actor_account_id !== value.approver_account_id
68
+ || value.category !== "file_change"
69
+ || value.risk_ceiling !== "low"
70
+ || !isExecutionDelegationDigest(value.scope_sha256)) return null;
71
+
72
+ return Object.fromEntries(INTENT_KEYS.map((key) => [key, value[key]]));
73
+ }
@@ -0,0 +1,31 @@
1
+ export const ROOM_WORK_STATES: readonly ["active", "completed", "completed_no_reply", "failed", "interrupted", "lost", "unknown"];
2
+ export const ROOM_WORK_OPERATION_OUTCOMES: readonly ["unresolved", "succeeded", "failed", "denied_before_start", "cancelled_before_start", "interrupted_after_start", "lost_after_start"];
3
+ export type RoomAgentWorkSummary = {
4
+ version: 1;
5
+ recorded_state: typeof ROOM_WORK_STATES[number];
6
+ evidence_incomplete: boolean;
7
+ elapsed_ms: number | null;
8
+ operation_counts: Record<typeof ROOM_WORK_OPERATION_OUTCOMES[number], number>;
9
+ };
10
+ // Server-issued availability marker, not a publisher-supplied execution state.
11
+ export type ClearedRoomAgentWorkSummary = { version: 1; availability: "cleared" };
12
+ export type RoomAgentWork = {
13
+ attempt_id: string;
14
+ room_id: string;
15
+ source_message_id: string;
16
+ agent_key: string;
17
+ revision: number;
18
+ summary: RoomAgentWorkSummary | ClearedRoomAgentWorkSummary;
19
+ updated_at: string;
20
+ };
21
+ export type RoomAgentWorkSnapshot = { work: RoomAgentWork[]; truncated: boolean };
22
+ /** Synchronizes the latest-50 view, not intermediate events or complete history.
23
+ * A changed snapshot replaces the client's bounded cache; it must not be merged.
24
+ * The server-issued cursor is a comparison hint, never authorization. */
25
+ export type RoomAgentWorkPollResponse = { room_id: string; cursor: string } & (
26
+ | { changed: true; snapshot: RoomAgentWorkSnapshot }
27
+ | { changed: false; snapshot: null }
28
+ );
29
+ export function isClearedRoomAgentWorkSummary(value: unknown): value is ClearedRoomAgentWorkSummary;
30
+ /** Return a canonical allowlisted copy, or reject without echoing private input. */
31
+ export function parseRoomAgentWorkSummary(value: unknown): RoomAgentWorkSummary | null;
@@ -0,0 +1,44 @@
1
+ // Room-public, host-reported evidence; never execution or approval authority.
2
+ // No free-form strings, native handles, paths, command text, or output belong
3
+ // in this version. Both the publisher and server use this strict boundary.
4
+ export const ROOM_WORK_STATES = [
5
+ "active", "completed", "completed_no_reply", "failed", "interrupted", "lost", "unknown",
6
+ ];
7
+ export const ROOM_WORK_OPERATION_OUTCOMES = [
8
+ "unresolved", "succeeded", "failed", "denied_before_start", "cancelled_before_start",
9
+ "interrupted_after_start", "lost_after_start",
10
+ ];
11
+
12
+ function exactKeys(value, keys) {
13
+ return !!value && typeof value === "object" && !Array.isArray(value)
14
+ && Object.keys(value).length === keys.length
15
+ && keys.every((key) => Object.hasOwn(value, key));
16
+ }
17
+
18
+ export function isClearedRoomAgentWorkSummary(value) {
19
+ return exactKeys(value, ["version", "availability"]) && value.version === 1 && value.availability === "cleared";
20
+ }
21
+
22
+ /** Return a canonical allowlisted copy, or reject without echoing private input. */
23
+ export function parseRoomAgentWorkSummary(value) {
24
+ if (!exactKeys(value, ["version", "recorded_state", "evidence_incomplete", "elapsed_ms", "operation_counts"])
25
+ || value.version !== 1 || !ROOM_WORK_STATES.includes(value.recorded_state)
26
+ || typeof value.evidence_incomplete !== "boolean"
27
+ || (value.elapsed_ms !== null && (!Number.isSafeInteger(value.elapsed_ms) || Number(value.elapsed_ms) < 0))
28
+ || !exactKeys(value.operation_counts, ROOM_WORK_OPERATION_OUTCOMES)) return null;
29
+ const counts = {};
30
+ let total = 0;
31
+ for (const outcome of ROOM_WORK_OPERATION_OUTCOMES) {
32
+ const count = value.operation_counts[outcome];
33
+ if (!Number.isSafeInteger(count) || Number(count) < 0) return null;
34
+ counts[outcome] = Number(count);
35
+ total += Number(count);
36
+ }
37
+ // A bounded evidence snapshot, not an unbounded lifetime counter.
38
+ if (total > 10_000) return null;
39
+ return {
40
+ version: 1, recorded_state: value.recorded_state,
41
+ evidence_incomplete: value.evidence_incomplete, elapsed_ms: value.elapsed_ms,
42
+ operation_counts: counts,
43
+ };
44
+ }
@@ -0,0 +1,38 @@
1
+ export const ROOM_RESOURCE_INVALIDATION_CAPABILITY: "resource_invalidation_v1";
2
+ export const ROOM_RESOURCE_AGENT_WORK: "agent_work";
3
+ /** Content-free approval-state hint; consumers repair through separately authorized exact reads. */
4
+ export const ROOM_RESOURCE_AGENT_APPROVAL: "agent_approval";
5
+ export const ROOM_RESOURCE_EXECUTION_DELEGATION: "execution_delegation";
6
+ /** Protocol-known references; consumers independently choose what they render. */
7
+ export const ROOM_RESOURCE_INVALIDATION_RESOURCES: readonly [
8
+ "agent_work",
9
+ "agent_approval",
10
+ "execution_delegation",
11
+ ];
12
+
13
+ export type RoomResourceInvalidationResource =
14
+ typeof ROOM_RESOURCE_INVALIDATION_RESOURCES[number];
15
+
16
+ export type RoomResourceInvalidationPointer<Resource extends string = string> = {
17
+ room_id: string;
18
+ resource: Resource;
19
+ };
20
+
21
+ export type RoomResourceInvalidationParseResult =
22
+ | {
23
+ status: "supported";
24
+ pointer: RoomResourceInvalidationPointer<RoomResourceInvalidationResource>;
25
+ }
26
+ | {
27
+ status: "unsupported";
28
+ pointer: RoomResourceInvalidationPointer;
29
+ }
30
+ | { status: "malformed" };
31
+
32
+ /**
33
+ * Parse a bounded room-resource pointer without echoing malformed input.
34
+ * Unknown but well-formed resources remain forward-compatible cursor no-ops.
35
+ */
36
+ export function parseRoomResourceInvalidation(
37
+ value: unknown,
38
+ ): RoomResourceInvalidationParseResult;
@@ -0,0 +1,50 @@
1
+ export const ROOM_RESOURCE_INVALIDATION_CAPABILITY = "resource_invalidation_v1";
2
+ export const ROOM_RESOURCE_AGENT_WORK = "agent_work";
3
+ // Content-free hint emitted when an approval projection is published or its
4
+ // decision-relevant room state changes. Consumers repair through separately
5
+ // authorized exact reads; the pointer never carries approval content.
6
+ export const ROOM_RESOURCE_AGENT_APPROVAL = "agent_approval";
7
+ export const ROOM_RESOURCE_EXECUTION_DELEGATION = "execution_delegation";
8
+ // Protocol-known references. Each consumer still decides which surfaces, if
9
+ // any, react to a supported pointer.
10
+ export const ROOM_RESOURCE_INVALIDATION_RESOURCES = [
11
+ ROOM_RESOURCE_AGENT_WORK,
12
+ ROOM_RESOURCE_AGENT_APPROVAL,
13
+ ROOM_RESOURCE_EXECUTION_DELEGATION,
14
+ ];
15
+
16
+ function exactKeys(value, keys) {
17
+ return !!value && typeof value === "object" && !Array.isArray(value)
18
+ && Object.keys(value).length === keys.length
19
+ && keys.every((key) => Object.hasOwn(value, key));
20
+ }
21
+
22
+ function isValidRoomIdentifier(value) {
23
+ return typeof value === "string"
24
+ && value.length > 0
25
+ && value.length <= 512
26
+ && value.trim() === value
27
+ && !/[\u0000-\u001f\u007f]/.test(value);
28
+ }
29
+
30
+ function isValidResource(value) {
31
+ return typeof value === "string"
32
+ && /^[a-z0-9][a-z0-9._-]{0,63}$/.test(value);
33
+ }
34
+
35
+ /**
36
+ * Parse a bounded room-resource pointer without echoing malformed input.
37
+ * Unknown but well-formed resources remain forward-compatible cursor no-ops.
38
+ */
39
+ export function parseRoomResourceInvalidation(value) {
40
+ if (
41
+ !exactKeys(value, ["room_id", "resource"])
42
+ || !isValidRoomIdentifier(value.room_id)
43
+ || !isValidResource(value.resource)
44
+ ) return { status: "malformed" };
45
+
46
+ const pointer = { room_id: value.room_id, resource: value.resource };
47
+ return ROOM_RESOURCE_INVALIDATION_RESOURCES.includes(value.resource)
48
+ ? { status: "supported", pointer }
49
+ : { status: "unsupported", pointer };
50
+ }