letagents 0.12.10 → 0.12.11

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.
Files changed (34) hide show
  1. package/README.md +2 -0
  2. package/dist/api/board-intent-payloads.js +25 -0
  3. package/dist/mcp/codex-session/runtime-bridge.js +42 -25
  4. package/dist/mcp/local-state/local-chat.js +1 -1
  5. package/dist/mcp/rental-tools/context.js +30 -0
  6. package/dist/mcp/server/runtime/agent-sessions.js +40 -1
  7. package/dist/mcp/server/runtime/api.js +35 -13
  8. package/dist/mcp/server/runtime/identity/directory.js +9 -2
  9. package/dist/mcp/server/runtime/identity.js +2 -1
  10. package/dist/mcp/server/runtime/rooms.js +56 -0
  11. package/dist/mcp/server/runtime/supervisor-bridge.js +94 -0
  12. package/dist/mcp/server/runtime/worker-bearer.js +67 -0
  13. package/dist/mcp/server/runtime.js +1 -1
  14. package/dist/mcp/server/tools/agent-sessions.js +44 -3
  15. package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
  16. package/dist/mcp/server/tools/messages/read-tool.js +1 -1
  17. package/dist/mcp/server/tools/messages/send-tool.js +2 -45
  18. package/dist/mcp/server/tools/messages/wait-tool.js +158 -88
  19. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +10 -0
  20. package/dist/mcp/server/tools/onboarding/name-tool.js +5 -1
  21. package/dist/mcp/server/tools/onboarding/status-tool.js +17 -0
  22. package/dist/mcp/server/tools/rental/context-tools.js +9 -1
  23. package/dist/mcp/server/tools/rooms/inspection-tools.js +26 -10
  24. package/dist/mcp/server/tools/tasks/board-intent-tools.js +7 -2
  25. package/dist/mcp/server/tools/tasks/index.js +2 -0
  26. package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
  27. package/dist/mcp/server.js +2 -0
  28. package/dist/mcp/sse-client.js +3 -3
  29. package/dist/shared/activation-routing.js +13 -4
  30. package/dist/shared/agent-presence.js +1 -0
  31. package/dist/shared/agent-session-bearer.js +28 -0
  32. package/dist/shared/board-manager-failover.js +16 -0
  33. package/dist/shared/room-agent-prompts.js +2 -2
  34. package/package.json +19 -14
package/README.md CHANGED
@@ -143,6 +143,8 @@ The API runs at `http://localhost:3001`. Point `LETAGENTS_API_URL` at your serve
143
143
 
144
144
  Optional — **long room long-polls** (multi-hour `wait_for_messages` / `GET …/messages/poll`): set the **same** `LETAGENTS_POLL_MAX_MS` on **both** the API process and any MCP client you run from source (milliseconds; default `180000`).
145
145
 
146
+ Optional — **visible worker-channel warning grace**: set `LETAGENTS_LIVENESS_NOTICE_AFTER_MS` on the API process (milliseconds; default `300000`, or 5 minutes). Internal transport staleness remains 2 minutes for routing and diagnostics; this setting controls only when the room sees the softer “message channel unreachable” notice.
147
+
146
148
  The API now uses PostgreSQL with Drizzle ORM. `DB_URL` must be set before starting the server or running migrations.
147
149
 
148
150
  Useful database commands:
@@ -0,0 +1,25 @@
1
+ export function boardIntentPayloadForTaskCreate(input) {
2
+ return {
3
+ title: input.title.trim(),
4
+ description: input.description?.trim() || null,
5
+ source_message_id: input.sourceMessageId?.trim() || null,
6
+ };
7
+ }
8
+ export function boardIntentPayloadForTaskMutation(input) {
9
+ return {
10
+ task_id: input.taskId,
11
+ status: input.status ?? null,
12
+ assignee: input.assignee ?? null,
13
+ assignee_agent_key: input.assigneeAgentKey ?? null,
14
+ pr_url: input.prUrl ?? null,
15
+ };
16
+ }
17
+ export function boardIntentPayloadForLeaseAction(input) {
18
+ return {
19
+ task_id: input.taskId,
20
+ action: input.action,
21
+ lease_id: input.leaseId ?? null,
22
+ target_actor_key: input.targetActorKey ?? null,
23
+ target_agent_session_id: input.targetAgentSessionId ?? null,
24
+ };
25
+ }
@@ -1,5 +1,7 @@
1
- import { getCurrentCodexLiveSession, getStoredAuth, getStoredCodexLiveSession, readLocalState, updateCodexLiveSession, } from "../local-state.js";
1
+ import { getCurrentCodexLiveSession, getStoredCodexLiveSession, readLocalState, updateCodexLiveSession, } from "../local-state.js";
2
2
  import { encodeRoomIdPath } from "../room-id.js";
3
+ import { apiCall } from "../server/runtime/api.js";
4
+ import { agentSessionCredentials } from "../server/runtime/agent-sessions.js";
3
5
  import { RpcClient } from "./rpc-client.js";
4
6
  import { isCodexAgentSessionMarker, summarizeCodexRuntimeNotificationForTest, } from "./runtime-summary.js";
5
7
  const CODEX_RUNTIME_STREAM_THROTTLE_MS = 750;
@@ -7,35 +9,16 @@ const CODEX_RUNTIME_STREAM_REPEAT_MS = 30_000;
7
9
  const CODEX_RUNTIME_STREAM_SNAPSHOT_INTERVAL_MS = 2_000;
8
10
  const CODEX_RUNTIME_STREAM_BIND_RETRY_MS = 1_000;
9
11
  const CODEX_RUNTIME_STREAM_BIND_RETRY_ATTEMPTS = 30;
12
+ const CODEX_NATIVE_HEARTBEAT_INTERVAL_MS = 15_000;
10
13
  export function createCodexRuntimeBridgeController(input) {
11
14
  const clients = new Map();
12
15
  const snapshotTimers = new Map();
13
16
  const bindTimers = new Map();
14
17
  const lastPost = new Map();
15
- function apiUrl() {
16
- return (process.env.LETAGENTS_API_URL || "http://localhost:3001").replace(/\/+$/, "");
17
- }
18
- function authorizationHeader() {
19
- const token = process.env.LETAGENTS_TOKEN || getStoredAuth()?.token || "";
20
- return token ? `Bearer ${token}` : null;
21
- }
18
+ const nativeSequence = new Map();
19
+ const nativeLastPostAt = new Map();
22
20
  async function codexBridgeApiCall(path, options) {
23
- const headers = {
24
- "Content-Type": "application/json",
25
- ...options?.headers,
26
- };
27
- const authorization = authorizationHeader();
28
- if (authorization && !headers.Authorization) {
29
- headers.Authorization = authorization;
30
- }
31
- const response = await fetch(`${apiUrl()}${path}`, {
32
- ...options,
33
- headers,
34
- });
35
- if (!response.ok) {
36
- throw new Error(`LetAgents API ${response.status}: ${await response.text()}`);
37
- }
38
- return (await response.json());
21
+ return apiCall(path, options);
39
22
  }
40
23
  function codexWorkerSessionsForRoom(roomId) {
41
24
  const state = readLocalState();
@@ -129,7 +112,32 @@ export function createCodexRuntimeBridgeController(input) {
129
112
  }
130
113
  }
131
114
  async function postReasoningUpdate(session, notification) {
132
- await postReasoningSummary(session, summarizeCodexRuntimeNotificationForTest(notification));
115
+ const summary = summarizeCodexRuntimeNotificationForTest(notification);
116
+ await Promise.all([
117
+ postReasoningSummary(session, summary),
118
+ postNativeActivity(session, notification.method, summary.status),
119
+ ]);
120
+ }
121
+ async function postNativeActivity(session, method, status, force = false) {
122
+ const workerSession = codexWorkerSessionForLiveSession(session);
123
+ if (!workerSession)
124
+ return;
125
+ const now = Date.now();
126
+ if (!force && now - (nativeLastPostAt.get(session.session_id) ?? 0) < CODEX_RUNTIME_STREAM_THROTTLE_MS)
127
+ return;
128
+ nativeLastPostAt.set(session.session_id, now);
129
+ const sequence = (nativeSequence.get(session.session_id) ?? 0) + 1;
130
+ nativeSequence.set(session.session_id, sequence);
131
+ await codexBridgeApiCall(`/rooms/${encodeRoomIdPath(session.room_id)}/agent-sessions/${encodeURIComponent(workerSession.session_id)}/native-activity`, {
132
+ method: "POST",
133
+ body: JSON.stringify({
134
+ ...agentSessionCredentials(workerSession),
135
+ observed_at: new Date(now).toISOString(),
136
+ sequence,
137
+ method,
138
+ status,
139
+ }),
140
+ });
133
141
  }
134
142
  function start(session, client) {
135
143
  stop(session.session_id);
@@ -143,6 +151,11 @@ export function createCodexRuntimeBridgeController(input) {
143
151
  !status.server_reachable ||
144
152
  input.isTerminalStatus(status.session.status)) {
145
153
  stop(session.session_id);
154
+ return;
155
+ }
156
+ const now = Date.now();
157
+ if (now - (nativeLastPostAt.get(session.session_id) ?? 0) >= CODEX_NATIVE_HEARTBEAT_INTERVAL_MS) {
158
+ void postNativeActivity(status.session, "native_harness.heartbeat", status.session.status === "completed" ? "idle" : "working", true).catch(() => undefined);
146
159
  }
147
160
  }).catch(() => {
148
161
  stop(session.session_id);
@@ -226,6 +239,8 @@ export function createCodexRuntimeBridgeController(input) {
226
239
  snapshotTimers.delete(sessionId);
227
240
  }
228
241
  lastPost.delete(sessionId);
242
+ nativeSequence.delete(sessionId);
243
+ nativeLastPostAt.delete(sessionId);
229
244
  }
230
245
  function cleanup() {
231
246
  for (const client of clients.values()) {
@@ -241,6 +256,8 @@ export function createCodexRuntimeBridgeController(input) {
241
256
  }
242
257
  bindTimers.clear();
243
258
  lastPost.clear();
259
+ nativeSequence.clear();
260
+ nativeLastPostAt.clear();
244
261
  }
245
262
  return {
246
263
  maybeStart,
@@ -78,7 +78,7 @@ function mapAttachmentRow(row) {
78
78
  function visibleMessageClause(includePromptOnly) {
79
79
  return includePromptOnly
80
80
  ? "1 = 1"
81
- : "NOT (agent_prompt_kind = 'auto' AND TRIM(text) = '')";
81
+ : "(agent_prompt_kind IS NULL OR agent_prompt_kind <> 'auto' OR TRIM(text) <> '')";
82
82
  }
83
83
  function toMessage(row, replyTo, attachments = []) {
84
84
  return {
@@ -28,6 +28,36 @@ export async function rentalReadFile(deps, input) {
28
28
  };
29
29
  }
30
30
  }
31
+ /**
32
+ * File a context access request for a path outside the approved scope.
33
+ * The renter reviews it; once approved the file becomes readable via
34
+ * rental_read_file.
35
+ */
36
+ export async function rentalRequestContext(deps, input) {
37
+ const sessionIdError = validateSessionId(input);
38
+ if (sessionIdError)
39
+ return { success: false, error: sessionIdError };
40
+ if (typeof input.path !== "string" || !input.path.trim()) {
41
+ return { success: false, error: "path is required" };
42
+ }
43
+ const body = { path: input.path.trim() };
44
+ if (typeof input.reason === "string" && input.reason.trim()) {
45
+ body.reason = input.reason.trim();
46
+ }
47
+ try {
48
+ return await deps.apiCall(`/api/rental/sessions/${encodeSessionId(input.session_id)}/context-requests`, {
49
+ method: "POST",
50
+ headers: { "content-type": "application/json" },
51
+ body: JSON.stringify(body),
52
+ });
53
+ }
54
+ catch (err) {
55
+ return {
56
+ success: false,
57
+ error: errorMessage(err),
58
+ };
59
+ }
60
+ }
31
61
  export async function rentalSearch(deps, input) {
32
62
  const sessionIdError = validateSessionId(input);
33
63
  if (sessionIdError)
@@ -5,8 +5,13 @@ 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 { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
9
+ // A worker bearer already represents a server-side worker session. This local
10
+ // marker lets the MCP tool contract stay session-shaped without persisting or
11
+ // transmitting a second set of credentials.
12
+ export const WORKER_BEARER_AGENT_SESSION_ID = "worker_bearer";
8
13
  export function buildAgentDeliveryHeaders(agentSession) {
9
- if (!agentSession) {
14
+ if (!agentSession || requireValidWorkerBearerRuntime().mode === "worker") {
10
15
  return {};
11
16
  }
12
17
  return {
@@ -83,6 +88,37 @@ export function requireWorkerAgentSession(roomId, sessionId) {
83
88
  return session;
84
89
  }
85
90
  export async function resolveWorkerToolIdentity(input) {
91
+ if (requireValidWorkerBearerRuntime().mode === "worker" &&
92
+ (!input.agentSessionId || input.agentSessionId === WORKER_BEARER_AGENT_SESSION_ID)) {
93
+ const identity = await ensureAgentIdentity();
94
+ const now = new Date().toISOString();
95
+ return {
96
+ identity,
97
+ agentSession: {
98
+ session_id: WORKER_BEARER_AGENT_SESSION_ID,
99
+ session_token: "",
100
+ room_id: input.roomId ?? "worker_bearer_room",
101
+ session_kind: "worker",
102
+ runtime: detectAgentRuntimeLabel(),
103
+ host_id: null,
104
+ host_kind: null,
105
+ host_label: null,
106
+ liveness_capability: null,
107
+ tool_bridge_id: null,
108
+ actor_label: identity.actor_label,
109
+ agent_key: identity.canonical_key ?? identity.runtime_key ?? identity.actor_label,
110
+ agent_instance_id: AGENT_INSTANCE_UUID,
111
+ display_name: identity.display_name,
112
+ owner_label: identity.owner_label,
113
+ ide_label: identity.ide_label ?? detectAgentIdeLabel(),
114
+ repo_branch: null,
115
+ created_at: now,
116
+ updated_at: now,
117
+ last_seen_at: now,
118
+ ended_at: null,
119
+ },
120
+ };
121
+ }
86
122
  const agentSession = input.agentSessionId
87
123
  ? requireWorkerAgentSession(input.roomId, input.agentSessionId)
88
124
  : input.roomId && await isLocalRoomStorageEnabled(input.roomId)
@@ -127,6 +163,9 @@ export function getAgentSessionRepoBranch(cwd) {
127
163
  return getGitCurrentBranch(workingDir);
128
164
  }
129
165
  export function agentSessionCredentials(agentSession) {
166
+ if (requireValidWorkerBearerRuntime().mode === "worker") {
167
+ return {};
168
+ }
130
169
  return {
131
170
  agent_session_id: agentSession.session_id,
132
171
  agent_session_token: agentSession.session_token,
@@ -1,5 +1,9 @@
1
- import { clearStoredAuth, getStoredAuth, } from "../../local-state.js";
2
1
  import { clearAuthenticatedAccountCache } from "./auth-cache.js";
2
+ import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
3
+ let ownerAuthStoreLoader = () => import("../../local-state.js");
4
+ export function setOwnerAuthStoreLoaderForTest(loader) {
5
+ ownerAuthStoreLoader = loader ?? (() => import("../../local-state.js"));
6
+ }
3
7
  export const API_URL = (process.env.LETAGENTS_API_URL || "http://localhost:3001").replace(/\/+$/, "");
4
8
  export class ApiError extends Error {
5
9
  status;
@@ -11,11 +15,20 @@ export class ApiError extends Error {
11
15
  this.body = body;
12
16
  }
13
17
  }
14
- export function getLetagentsToken() {
15
- return process.env.LETAGENTS_TOKEN || getStoredAuth()?.token || "";
18
+ export async function getLetagentsToken() {
19
+ const runtime = requireValidWorkerBearerRuntime();
20
+ if (runtime.mode === "worker") {
21
+ return runtime.bearer;
22
+ }
23
+ const envToken = process.env.LETAGENTS_TOKEN?.trim();
24
+ if (envToken) {
25
+ return envToken;
26
+ }
27
+ const { getStoredAuth } = await ownerAuthStoreLoader();
28
+ return getStoredAuth()?.token || "";
16
29
  }
17
- export function getAuthorizationHeader() {
18
- const letagentsToken = getLetagentsToken();
30
+ export async function getAuthorizationHeader() {
31
+ const letagentsToken = await getLetagentsToken();
19
32
  return letagentsToken ? `Bearer ${letagentsToken}` : null;
20
33
  }
21
34
  export function isMissingRouteError(error) {
@@ -52,13 +65,21 @@ export function resolveApiPath(urlOrPath) {
52
65
  }
53
66
  }
54
67
  export async function apiCall(path, options) {
55
- const headers = {
56
- "Content-Type": "application/json",
57
- ...options?.headers,
58
- };
59
- const authorizationHeader = getAuthorizationHeader();
60
- if (authorizationHeader && !headers.Authorization) {
61
- headers.Authorization = authorizationHeader;
68
+ const headers = new Headers(options?.headers);
69
+ if (!headers.has("Content-Type")) {
70
+ headers.set("Content-Type", "application/json");
71
+ }
72
+ const runtime = requireValidWorkerBearerRuntime();
73
+ if (runtime.mode === "worker") {
74
+ // The bearer is the complete worker credential. Normalize headers first so
75
+ // every caller spelling of Authorization is overwritten.
76
+ headers.set("Authorization", `Bearer ${runtime.bearer}`);
77
+ }
78
+ else {
79
+ const authorizationHeader = await getAuthorizationHeader();
80
+ if (authorizationHeader && !headers.has("Authorization")) {
81
+ headers.set("Authorization", authorizationHeader);
82
+ }
62
83
  }
63
84
  const res = await fetch(`${API_URL}${path}`, {
64
85
  ...options,
@@ -66,9 +87,10 @@ export async function apiCall(path, options) {
66
87
  });
67
88
  if (!res.ok) {
68
89
  const body = await res.text();
69
- if (res.status === 401) {
90
+ if (res.status === 401 && requireValidWorkerBearerRuntime().mode !== "worker") {
70
91
  // Only clear on 401 (invalid/expired credential), NOT on 403
71
92
  // (valid credential but insufficient permissions, e.g., private repo access)
93
+ const { clearStoredAuth } = await ownerAuthStoreLoader();
72
94
  clearStoredAuth();
73
95
  clearAuthenticatedAccountCache();
74
96
  }
@@ -1,10 +1,13 @@
1
1
  import { userInfo } from "os";
2
- import { getStoredAuth, } from "../../../local-state.js";
3
2
  import { normalizeSlugSegment } from "../../../../shared/codenames.js";
4
3
  import { apiCall, getLetagentsToken, } from "../api.js";
4
+ import { requireValidWorkerBearerRuntime } from "../worker-bearer.js";
5
5
  import { getAuthenticatedAccountCache, setAuthenticatedAccountCache, } from "../auth-cache.js";
6
6
  import { AGENT_OWNER_LABEL, readCommandOutput, } from "./config.js";
7
7
  export async function getAuthenticatedAgentDirectory() {
8
+ if (requireValidWorkerBearerRuntime().mode === "worker") {
9
+ return null;
10
+ }
8
11
  try {
9
12
  const result = await apiCall("/agents/me");
10
13
  const account = result?.account;
@@ -22,6 +25,9 @@ export async function getAuthenticatedAgentDirectory() {
22
25
  }
23
26
  }
24
27
  async function getAuthenticatedAccountProfile() {
28
+ if (requireValidWorkerBearerRuntime().mode === "worker") {
29
+ return null;
30
+ }
25
31
  const envToken = (process.env.LETAGENTS_TOKEN || "").trim();
26
32
  const cache = getAuthenticatedAccountCache();
27
33
  if (envToken) {
@@ -33,12 +39,13 @@ async function getAuthenticatedAccountProfile() {
33
39
  const directory = await getAuthenticatedAgentDirectory();
34
40
  return directory?.account?.login?.trim() ? directory.account : null;
35
41
  }
42
+ const { getStoredAuth } = await import("../../../local-state.js");
36
43
  const storedAccount = getStoredAuth()?.account;
37
44
  if (storedAccount?.login?.trim()) {
38
45
  setAuthenticatedAccountCache(storedAccount, "stored", null);
39
46
  return storedAccount;
40
47
  }
41
- if (!getLetagentsToken()) {
48
+ if (!await getLetagentsToken()) {
42
49
  setAuthenticatedAccountCache(undefined, null, null);
43
50
  return null;
44
51
  }
@@ -1,5 +1,6 @@
1
1
  import { buildAgentActorLabel, formatOwnerAttribution, } from "../../../shared/agent-identity.js";
2
2
  import { apiCall, getLetagentsToken, } from "./api.js";
3
+ import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
3
4
  import { detectAgentIdeLabel, detectAgentRuntimeLabel, } from "./identity/config.js";
4
5
  import { resolveOwnerContext } from "./identity/directory.js";
5
6
  import { getSessionLivenessRegistration } from "./identity/liveness.js";
@@ -9,7 +10,7 @@ import { currentAgentIdentity, currentAgentIdentityKey, ensureAgentIdentityKey,
9
10
  export { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, detectAgentRuntimeLabel, getConversationIdentity, getSessionLivenessRegistration, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, };
10
11
  export async function ensureAgentIdentity() {
11
12
  const owner = await resolveOwnerContext();
12
- const authAvailable = Boolean(getLetagentsToken());
13
+ const authAvailable = requireValidWorkerBearerRuntime().mode === "owner" && Boolean(await getLetagentsToken());
13
14
  const ideLabel = detectAgentIdeLabel();
14
15
  const identityKey = ensureAgentIdentityKey();
15
16
  const ownerAttribution = formatOwnerAttribution(owner.label);
@@ -9,6 +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 { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
12
13
  export function normalizeJoinSessionMode(value) {
13
14
  return String(value || "").trim().toLowerCase() === "live" ? "live" : "current";
14
15
  }
@@ -253,8 +254,63 @@ export async function joinNamedRoom(name, sessionMode) {
253
254
  session_mode: sessionMode,
254
255
  });
255
256
  }
257
+ function bindWorkerRoomFromContext() {
258
+ const configRoom = getRoomFromConfig();
259
+ if (configRoom) {
260
+ const gitContext = buildActiveGitRoomContext({
261
+ repoRoom: configRoom,
262
+ currentBranch: getGitCurrentBranch(),
263
+ defaultBranch: getGitDefaultBranch(),
264
+ });
265
+ const roomId = gitContext.activeRoom ?? configRoom;
266
+ return {
267
+ room: rememberRoom(toRoomState({
268
+ room_id: roomId,
269
+ display_name: roomId,
270
+ joined_via: "config",
271
+ })),
272
+ source: ".letagents.json",
273
+ };
274
+ }
275
+ const gitContext = getGitRoomContext();
276
+ if (gitContext.activeRoom) {
277
+ return {
278
+ room: rememberRoom(toRoomState({
279
+ room_id: gitContext.activeRoom,
280
+ display_name: gitContext.activeRoom,
281
+ joined_via: "git-remote",
282
+ })),
283
+ source: "git remote",
284
+ };
285
+ }
286
+ const savedCurrentRoom = getStoredCurrentRoom();
287
+ if (!savedCurrentRoom) {
288
+ return null;
289
+ }
290
+ return {
291
+ room: rememberRoom(toRoomState({
292
+ room_id: savedCurrentRoom.room_id,
293
+ project_id: savedCurrentRoom.project_id,
294
+ code: savedCurrentRoom.code,
295
+ display_name: savedCurrentRoom.display_name,
296
+ git_room: savedCurrentRoom.git_room,
297
+ joined_via: savedCurrentRoom.joined_via,
298
+ })),
299
+ source: "saved local room state",
300
+ };
301
+ }
256
302
  export async function autoJoinFromContext() {
257
303
  try {
304
+ if (requireValidWorkerBearerRuntime().mode === "worker") {
305
+ const bound = bindWorkerRoomFromContext();
306
+ if (bound) {
307
+ console.error(`🏠 Bound worker bearer to room '${bound.room.room_id}' (from ${bound.source}; no join/create request).`);
308
+ }
309
+ else {
310
+ console.error("ℹ️ Worker bearer has no .letagents.json, git remote, or saved room to bind locally.");
311
+ }
312
+ return;
313
+ }
258
314
  const configRoom = getRoomFromConfig();
259
315
  if (configRoom) {
260
316
  const gitContext = buildActiveGitRoomContext({
@@ -0,0 +1,94 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createConnection } from "node:net";
3
+ const NEGOTIATION_PROTOCOL_VERSION = 1;
4
+ const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2]);
5
+ const DEFAULT_REQUEST_TIMEOUT_MS = 5_000;
6
+ /** Bind the exact worker credential minted by registration to its daemon lane. */
7
+ export async function bindSupervisedWorkerSession(session, env = process.env, options = {}) {
8
+ const entryId = env.LETAGENTS_SUPERVISOR_ENTRY_ID?.trim();
9
+ const socketPath = env.LETAGENTS_SUPERVISOR_DAEMON_SOCKET?.trim();
10
+ const workAttemptId = env.LETAGENTS_SUPERVISOR_WORK_ATTEMPT_ID?.trim();
11
+ const executionGenerationId = env.LETAGENTS_SUPERVISOR_EXECUTION_GENERATION_ID?.trim();
12
+ if (!entryId && !socketPath && !workAttemptId && !executionGenerationId)
13
+ return false;
14
+ if (!entryId || !socketPath || !workAttemptId || !executionGenerationId)
15
+ throw new Error("Supervised worker bridge environment is incomplete.");
16
+ if (session.session_kind !== "worker")
17
+ throw new Error("A supervised provider must register a worker session.");
18
+ const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
19
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
20
+ throw new Error("Supervisor bridge timeout must be a positive integer.");
21
+ const negotiation = await supervisorRequest(socketPath, {
22
+ version: NEGOTIATION_PROTOCOL_VERSION,
23
+ id: randomUUID(),
24
+ method: "daemon.negotiate",
25
+ }, timeoutMs);
26
+ if (!negotiation.ok)
27
+ throw new Error(negotiation.error || "Supervisor protocol negotiation failed.");
28
+ const protocolVersion = negotiationProtocolVersion(negotiation.result);
29
+ if (negotiation.version !== protocolVersion)
30
+ throw new Error("Supervisor negotiation response version does not match its negotiated protocol.");
31
+ const response = await supervisorRequest(socketPath, {
32
+ version: protocolVersion,
33
+ id: randomUUID(),
34
+ method: "supervisor.bind_worker_session",
35
+ params: {
36
+ entry_id: entryId,
37
+ room_id: session.room_id,
38
+ work_attempt_id: workAttemptId,
39
+ execution_generation_id: executionGenerationId,
40
+ agent_session_id: session.session_id,
41
+ agent_session_token: session.session_token,
42
+ api_url: env.LETAGENTS_API_URL?.trim() || "https://letagents.chat",
43
+ },
44
+ }, timeoutMs);
45
+ if (!response.ok)
46
+ throw new Error(response.error || "Supervisor rejected the worker session binding.");
47
+ if (response.version !== protocolVersion)
48
+ throw new Error("Supervisor binding response used an unexpected protocol version.");
49
+ return true;
50
+ }
51
+ function negotiationProtocolVersion(result) {
52
+ if (!result || typeof result !== "object")
53
+ throw new Error("Supervisor protocol negotiation returned a malformed result.");
54
+ const protocolVersion = result.protocol_version;
55
+ if (typeof protocolVersion !== "number" || !Number.isSafeInteger(protocolVersion)
56
+ || !SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS.has(protocolVersion)) {
57
+ throw new Error(`Supervisor protocol negotiation returned unsupported version ${String(protocolVersion)}.`);
58
+ }
59
+ return protocolVersion;
60
+ }
61
+ function supervisorRequest(socketPath, request, timeoutMs) {
62
+ return new Promise((resolve, reject) => {
63
+ const socket = createConnection(socketPath);
64
+ let buffer = "";
65
+ const timer = setTimeout(() => {
66
+ socket.destroy();
67
+ reject(new Error("Timed out communicating with the supervisor daemon."));
68
+ }, timeoutMs);
69
+ timer.unref();
70
+ const finish = (operation) => {
71
+ clearTimeout(timer);
72
+ operation();
73
+ };
74
+ socket.setEncoding("utf8");
75
+ socket.once("error", (error) => finish(() => reject(error)));
76
+ socket.on("data", (chunk) => {
77
+ buffer += chunk;
78
+ const newline = buffer.indexOf("\n");
79
+ if (newline < 0)
80
+ return;
81
+ socket.end();
82
+ try {
83
+ const response = JSON.parse(buffer.slice(0, newline));
84
+ finish(() => response.id === request.id
85
+ ? resolve(response)
86
+ : reject(new Error("Supervisor response id does not match its request.")));
87
+ }
88
+ catch (error) {
89
+ finish(() => reject(error));
90
+ }
91
+ });
92
+ socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
93
+ });
94
+ }
@@ -0,0 +1,67 @@
1
+ export const LETAGENTS_AGENT_SESSION_BEARER_ENV = "LETAGENTS_AGENT_SESSION_BEARER";
2
+ export class WorkerBearerRuntimeConfigurationError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "WorkerBearerRuntimeConfigurationError";
6
+ }
7
+ }
8
+ export function getWorkerBearerRuntime() {
9
+ const bearer = process.env.LETAGENTS_AGENT_SESSION_BEARER?.trim();
10
+ if (!bearer) {
11
+ return { mode: "owner" };
12
+ }
13
+ const apiUrl = process.env.LETAGENTS_API_URL?.trim();
14
+ if (!apiUrl) {
15
+ return {
16
+ mode: "invalid",
17
+ error: "Worker bearer mode requires an explicit LETAGENTS_API_URL.",
18
+ };
19
+ }
20
+ try {
21
+ const parsed = new URL(apiUrl);
22
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23
+ throw new Error("unsupported protocol");
24
+ }
25
+ const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
26
+ if (parsed.protocol === "http:" && !loopbackHosts.has(parsed.hostname.toLowerCase())) {
27
+ return {
28
+ mode: "invalid",
29
+ error: "Worker bearer mode requires HTTPS unless LETAGENTS_API_URL uses an exact loopback host.",
30
+ };
31
+ }
32
+ }
33
+ catch {
34
+ return {
35
+ mode: "invalid",
36
+ error: "Worker bearer mode requires LETAGENTS_API_URL to be a valid HTTP(S) URL.",
37
+ };
38
+ }
39
+ if (process.env.LETAGENTS_TOKEN?.trim()) {
40
+ return {
41
+ mode: "invalid",
42
+ error: "Worker bearer mode refuses LETAGENTS_TOKEN. Remove the owner token from this process before starting the worker.",
43
+ };
44
+ }
45
+ return { mode: "worker", bearer };
46
+ }
47
+ export function requireValidWorkerBearerRuntime() {
48
+ const runtime = getWorkerBearerRuntime();
49
+ if (runtime.mode === "invalid") {
50
+ throw new WorkerBearerRuntimeConfigurationError(runtime.error);
51
+ }
52
+ return runtime;
53
+ }
54
+ export function workerModeDisabledToolResult(toolDescription = "This owner-auth onboarding tool") {
55
+ const runtime = getWorkerBearerRuntime();
56
+ if (runtime.mode === "invalid") {
57
+ return { success: false, error: "worker_bearer_configuration_invalid", message: runtime.error };
58
+ }
59
+ if (runtime.mode === "worker") {
60
+ return {
61
+ success: false,
62
+ error: "worker_bearer_mode",
63
+ message: `${toolDescription} is disabled while LETAGENTS_AGENT_SESSION_BEARER is configured.`,
64
+ };
65
+ }
66
+ return null;
67
+ }
@@ -5,7 +5,7 @@ export { API_URL, ApiError, apiCall, getAuthorizationHeader, getLetagentsToken,
5
5
  export { clearAuthenticatedAccountCache, getAuthenticatedAccountCache, setAuthenticatedAccountCache, } from "./runtime/auth-cache.js";
6
6
  export { RepoRoomAuthRequiredError, maybeHandleRepoRoomAuthRequired, startPendingDeviceAuth, toRepoRoomAuthRequiredResult, } from "./runtime/device-auth.js";
7
7
  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";
8
+ export { agentSessionCredentials, buildAgentDeliveryHeaders, ensureLocalWorkerAgentSession, getAgentSessionRepoBranch, identityFromAgentSession, requireWorkerAgentSession, resolveAgentSession, resolveWorkerToolIdentity, toPublicAgentSession, WORKER_BEARER_AGENT_SESSION_ID, } from "./runtime/agent-sessions.js";
9
9
  export { appendIncludePromptOnly, getLastMessageId, normalizeOptionalToolString, toAgentReadableMessages, withJoinRoomAgentPrompt, } from "./runtime/messages.js";
10
10
  export { currentRoom, attachMcpServer, getFallbackProjectId, getTargetRoomId, rememberRoom, shutdownRuntime, toPublicRoomResponse, toPublicRoomState, toPublicStoredRoomSession, toRoomState, touchCurrentRoom, withCanonicalRoomLink, } from "./runtime/room-state.js";
11
11
  export { getRememberedRoomPresence, heartbeatRoomPresence, syncRoomPresence, } from "./runtime/presence.js";