letagents 0.12.10 → 0.12.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) 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/agent-sessions.js +15 -0
  5. package/dist/mcp/local-state/local-chat.js +1 -1
  6. package/dist/mcp/rental-tools/context.js +30 -0
  7. package/dist/mcp/server/register-tools.js +23 -12
  8. package/dist/mcp/server/runtime/agent-sessions.js +81 -2
  9. package/dist/mcp/server/runtime/api.js +63 -13
  10. package/dist/mcp/server/runtime/execution-profile.js +19 -0
  11. package/dist/mcp/server/runtime/identity/directory.js +9 -2
  12. package/dist/mcp/server/runtime/identity.js +2 -1
  13. package/dist/mcp/server/runtime/presence.js +4 -2
  14. package/dist/mcp/server/runtime/room-api.js +24 -7
  15. package/dist/mcp/server/runtime/room-state.js +46 -0
  16. package/dist/mcp/server/runtime/rooms.js +70 -0
  17. package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
  18. package/dist/mcp/server/runtime/supervisor-bridge.js +731 -0
  19. package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
  20. package/dist/mcp/server/runtime/worker-bearer.js +101 -0
  21. package/dist/mcp/server/runtime-contract.js +27 -0
  22. package/dist/mcp/server/runtime.js +14 -3
  23. package/dist/mcp/server/supervised-tool-facade.js +105 -0
  24. package/dist/mcp/server/tools/agent-sessions.js +113 -5
  25. package/dist/mcp/server/tools/messages/index.js +3 -2
  26. package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
  27. package/dist/mcp/server/tools/messages/read-tool.js +1 -1
  28. package/dist/mcp/server/tools/messages/send-tool.js +5 -45
  29. package/dist/mcp/server/tools/messages/wait-tool.js +212 -91
  30. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +10 -0
  31. package/dist/mcp/server/tools/onboarding/name-tool.js +5 -1
  32. package/dist/mcp/server/tools/onboarding/status-tool.js +18 -0
  33. package/dist/mcp/server/tools/rental/context-tools.js +9 -1
  34. package/dist/mcp/server/tools/rooms/inspection-tools.js +47 -18
  35. package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
  36. package/dist/mcp/server/tools/tasks/board-intent-tools.js +7 -2
  37. package/dist/mcp/server/tools/tasks/index.js +2 -0
  38. package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
  39. package/dist/mcp/server.js +16 -7
  40. package/dist/mcp/sse-client.js +3 -3
  41. package/dist/shared/activation-routing.js +57 -4
  42. package/dist/shared/agent-presence.js +1 -0
  43. package/dist/shared/agent-session-bearer.js +28 -0
  44. package/dist/shared/board-manager-failover.js +16 -0
  45. package/dist/shared/room-agent-prompts.js +2 -2
  46. package/package.json +24 -15
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,
@@ -26,6 +26,21 @@ export function getCurrentAgentSession(roomId) {
26
26
  }
27
27
  return best;
28
28
  }
29
+ /**
30
+ * Every stored session (active or ended) this identity had in the room,
31
+ * most recently updated first. Re-registration consults the FULL lineage so a
32
+ * replayed prior label reuses the base recorded when that exact label was
33
+ * allocated — a latest-only lookup would lose an older concurrent sibling's
34
+ * base and misread its restart as a deliberate rename.
35
+ */
36
+ export function getStoredAgentSessionsForRoomIdentity(roomId, agentKey) {
37
+ if (!roomId || !agentKey)
38
+ return [];
39
+ const state = readLocalState();
40
+ return Object.values(state.agent_sessions ?? {})
41
+ .filter((session) => session.room_id === roomId && session.agent_key === agentKey)
42
+ .sort((left, right) => right.updated_at.localeCompare(left.updated_at));
43
+ }
29
44
  export function saveAgentSession(session, makeCurrent = true) {
30
45
  updateLocalState((state) => {
31
46
  state.agent_sessions = state.agent_sessions ?? {};
@@ -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)
@@ -4,16 +4,27 @@ import { registerOnboardingTools } from "./tools/onboarding.js";
4
4
  import { registerRentalTools } from "./tools/rental.js";
5
5
  import { registerRepoInitializationTool, registerRepoVisibilityTool, registerRoomInspectionTools, registerRoomJoinTools, registerRoomResumeTool, } from "./tools/rooms.js";
6
6
  import { registerTaskTools } from "./tools/tasks.js";
7
- export function registerTools(server) {
8
- registerRoomJoinTools(server);
9
- registerAgentSessionTools(server);
10
- registerRoomInspectionTools(server);
11
- registerStatusTools(server);
12
- registerTaskTools(server);
13
- registerRepoInitializationTool(server);
14
- registerMessageTools(server);
15
- registerOnboardingTools(server);
16
- registerRoomResumeTool(server);
17
- registerRentalTools(server);
18
- registerRepoVisibilityTool(server);
7
+ import { registerSupervisedRoomTurnTools } from "./tools/supervised-room-turn.js";
8
+ import { toolSurfaceForExecutionProfile } from "./runtime/tool-surface-policy.js";
9
+ import { profileAwareToolServer } from "./supervised-tool-facade.js";
10
+ export function registerTools(server, profile = "autonomous_mcp_worker", supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null) {
11
+ const tools = profileAwareToolServer(server, profile, undefined, supervisedProvider);
12
+ const surface = toolSurfaceForExecutionProfile(profile);
13
+ registerRoomJoinTools(tools);
14
+ if (surface.agentSessionLifecycle)
15
+ registerAgentSessionTools(tools);
16
+ registerRoomInspectionTools(tools);
17
+ registerStatusTools(tools);
18
+ registerTaskTools(tools);
19
+ registerRepoInitializationTool(tools);
20
+ registerMessageTools(tools, { includeDeliveryLoop: surface.deliveryLoop });
21
+ if (profile === "supervised_room_turn" && supervisedProvider === "cursor")
22
+ registerSupervisedRoomTurnTools(tools);
23
+ if (surface.onboarding)
24
+ registerOnboardingTools(tools);
25
+ if (surface.roomResume)
26
+ registerRoomResumeTool(tools);
27
+ if (surface.rental)
28
+ registerRentalTools(tools);
29
+ registerRepoVisibilityTool(tools);
19
30
  }
@@ -5,8 +5,15 @@ 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";
9
+ import { resolveCurrentSupervisedWorkerSession } from "./supervisor-bridge.js";
10
+ // A worker bearer already represents a server-side worker session. This local
11
+ // marker lets the MCP tool contract stay session-shaped without persisting or
12
+ // transmitting a second set of credentials.
13
+ export const WORKER_BEARER_AGENT_SESSION_ID = "worker_bearer";
8
14
  export function buildAgentDeliveryHeaders(agentSession) {
9
- if (!agentSession) {
15
+ const runtime = requireValidWorkerBearerRuntime();
16
+ if (!agentSession || runtime.mode === "worker" || runtime.mode === "supervised") {
10
17
  return {};
11
18
  }
12
19
  return {
@@ -57,6 +64,33 @@ export function resolveAgentSession(roomId, sessionId) {
57
64
  }
58
65
  return session;
59
66
  }
67
+ /**
68
+ * The stable base this client declares as `requested_base_display_name` when
69
+ * registering. Rules (task_66):
70
+ * - An explicit display_name that replays the EXACT label of any prior stored
71
+ * session for this room+identity is a resume, not a rename: reuse the base
72
+ * recorded when THAT label was allocated, so a server-decorated label
73
+ * converges. The whole lineage is consulted (most recent first) because a
74
+ * latest-only lookup would lose an older concurrent sibling's base and
75
+ * misread its restart as a deliberate rename.
76
+ * - Any other explicit display_name is deliberate intent and IS the base —
77
+ * a numeric-ending custom name ("Agent 47") is therefore never demoted.
78
+ * - With no explicit name, fall back to the most recent recorded base in the
79
+ * lineage, then to the durable identity's display name.
80
+ */
81
+ export function resolveClientRequestedBase(input) {
82
+ const explicit = input.explicitDisplayName?.trim() || "";
83
+ const lineage = input.priorSessions ?? [];
84
+ if (explicit) {
85
+ const replayed = lineage.find((session) => session.display_name?.trim() === explicit);
86
+ const replayedBase = replayed?.requested_base_display_name?.trim() || "";
87
+ return replayedBase || explicit;
88
+ }
89
+ const latestBase = lineage
90
+ .map((session) => session.requested_base_display_name?.trim() || "")
91
+ .find((base) => base.length > 0);
92
+ return latestBase || input.identityDisplayName.trim();
93
+ }
60
94
  export function identityFromAgentSession(session) {
61
95
  return {
62
96
  name: normalizeAgentBaseName(session.display_name),
@@ -83,9 +117,50 @@ export function requireWorkerAgentSession(roomId, sessionId) {
83
117
  return session;
84
118
  }
85
119
  export async function resolveWorkerToolIdentity(input) {
120
+ const runtimeMode = requireValidWorkerBearerRuntime().mode;
121
+ if (runtimeMode === "supervised") {
122
+ const agentSession = await resolveCurrentSupervisedWorkerSession(input.roomId);
123
+ if (input.agentSessionId
124
+ && input.agentSessionId !== WORKER_BEARER_AGENT_SESSION_ID
125
+ && input.agentSessionId !== agentSession.session_id) {
126
+ throw new Error(`Daemon-supervised worker session is ${agentSession.session_id}, not ${input.agentSessionId}.`);
127
+ }
128
+ return { identity: identityFromAgentSession(agentSession), agentSession };
129
+ }
130
+ if (runtimeMode === "worker" &&
131
+ (!input.agentSessionId || input.agentSessionId === WORKER_BEARER_AGENT_SESSION_ID)) {
132
+ const identity = await ensureAgentIdentity();
133
+ const now = new Date().toISOString();
134
+ return {
135
+ identity,
136
+ agentSession: {
137
+ session_id: WORKER_BEARER_AGENT_SESSION_ID,
138
+ session_token: "",
139
+ room_id: input.roomId ?? "worker_bearer_room",
140
+ session_kind: "worker",
141
+ runtime: detectAgentRuntimeLabel(),
142
+ host_id: null,
143
+ host_kind: null,
144
+ host_label: null,
145
+ liveness_capability: null,
146
+ tool_bridge_id: null,
147
+ actor_label: identity.actor_label,
148
+ agent_key: identity.canonical_key ?? identity.runtime_key ?? identity.actor_label,
149
+ agent_instance_id: AGENT_INSTANCE_UUID,
150
+ display_name: identity.display_name,
151
+ owner_label: identity.owner_label,
152
+ ide_label: identity.ide_label ?? detectAgentIdeLabel(),
153
+ repo_branch: null,
154
+ created_at: now,
155
+ updated_at: now,
156
+ last_seen_at: now,
157
+ ended_at: null,
158
+ },
159
+ };
160
+ }
86
161
  const agentSession = input.agentSessionId
87
162
  ? requireWorkerAgentSession(input.roomId, input.agentSessionId)
88
- : input.roomId && await isLocalRoomStorageEnabled(input.roomId)
163
+ : input.roomId && !isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(input.roomId)
89
164
  ? await ensureLocalWorkerAgentSession(input.roomId)
90
165
  : requireWorkerAgentSession(input.roomId, input.agentSessionId);
91
166
  return {
@@ -127,6 +202,10 @@ export function getAgentSessionRepoBranch(cwd) {
127
202
  return getGitCurrentBranch(workingDir);
128
203
  }
129
204
  export function agentSessionCredentials(agentSession) {
205
+ const runtime = requireValidWorkerBearerRuntime();
206
+ if (runtime.mode === "worker" || runtime.mode === "supervised") {
207
+ return {};
208
+ }
130
209
  return {
131
210
  agent_session_id: agentSession.session_id,
132
211
  agent_session_token: agentSession.session_token,
@@ -1,5 +1,30 @@
1
- import { clearStoredAuth, getStoredAuth, } from "../../local-state.js";
2
1
  import { clearAuthenticatedAccountCache } from "./auth-cache.js";
2
+ import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
3
+ import { borrowCurrentSupervisedWorkerCredential, } from "./supervisor-bridge.js";
4
+ let ownerAuthStoreLoader = () => import("../../local-state.js");
5
+ let supervisedCredentialBorrower = () => borrowCurrentSupervisedWorkerCredential();
6
+ export function setOwnerAuthStoreLoaderForTest(loader) {
7
+ ownerAuthStoreLoader = loader ?? (() => import("../../local-state.js"));
8
+ }
9
+ export function setSupervisedCredentialBorrowerForTest(borrower) {
10
+ supervisedCredentialBorrower = borrower ?? (() => borrowCurrentSupervisedWorkerCredential());
11
+ }
12
+ export class SupervisedWorkerCredentialError extends Error {
13
+ code;
14
+ constructor(code) {
15
+ super(code === "SUPERVISED_CREDENTIAL_UNAVAILABLE"
16
+ ? "The daemon-supervised worker credential is not available yet."
17
+ : "The daemon-supervised worker credential is stale or missing its exact context.");
18
+ this.code = code;
19
+ this.name = "SupervisedWorkerCredentialError";
20
+ }
21
+ }
22
+ async function getSupervisedCredential() {
23
+ const result = await supervisedCredentialBorrower();
24
+ if (result.state === "available")
25
+ return result.credential;
26
+ throw new SupervisedWorkerCredentialError(result.state === "deferred" ? "SUPERVISED_CREDENTIAL_UNAVAILABLE" : "SUPERVISED_CREDENTIAL_STALE");
27
+ }
3
28
  export const API_URL = (process.env.LETAGENTS_API_URL || "http://localhost:3001").replace(/\/+$/, "");
4
29
  export class ApiError extends Error {
5
30
  status;
@@ -11,11 +36,22 @@ export class ApiError extends Error {
11
36
  this.body = body;
12
37
  }
13
38
  }
14
- export function getLetagentsToken() {
15
- return process.env.LETAGENTS_TOKEN || getStoredAuth()?.token || "";
39
+ export async function getLetagentsToken() {
40
+ const runtime = requireValidWorkerBearerRuntime();
41
+ if (runtime.mode === "worker") {
42
+ return runtime.bearer;
43
+ }
44
+ if (runtime.mode === "supervised")
45
+ return getSupervisedCredential();
46
+ const envToken = process.env.LETAGENTS_TOKEN?.trim();
47
+ if (envToken) {
48
+ return envToken;
49
+ }
50
+ const { getStoredAuth } = await ownerAuthStoreLoader();
51
+ return getStoredAuth()?.token || "";
16
52
  }
17
- export function getAuthorizationHeader() {
18
- const letagentsToken = getLetagentsToken();
53
+ export async function getAuthorizationHeader() {
54
+ const letagentsToken = await getLetagentsToken();
19
55
  return letagentsToken ? `Bearer ${letagentsToken}` : null;
20
56
  }
21
57
  export function isMissingRouteError(error) {
@@ -52,13 +88,26 @@ export function resolveApiPath(urlOrPath) {
52
88
  }
53
89
  }
54
90
  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;
91
+ const headers = new Headers(options?.headers);
92
+ if (!headers.has("Content-Type")) {
93
+ headers.set("Content-Type", "application/json");
94
+ }
95
+ const runtime = requireValidWorkerBearerRuntime();
96
+ if (runtime.mode === "worker") {
97
+ // The bearer is the complete worker credential. Normalize headers first so
98
+ // every caller spelling of Authorization is overwritten.
99
+ headers.set("Authorization", `Bearer ${runtime.bearer}`);
100
+ }
101
+ else if (runtime.mode === "supervised") {
102
+ // Resolve on every API request: the daemon may rotate the in-memory
103
+ // credential while Codex remains running.
104
+ headers.set("Authorization", `Bearer ${await getSupervisedCredential()}`);
105
+ }
106
+ else {
107
+ const authorizationHeader = await getAuthorizationHeader();
108
+ if (authorizationHeader && !headers.has("Authorization")) {
109
+ headers.set("Authorization", authorizationHeader);
110
+ }
62
111
  }
63
112
  const res = await fetch(`${API_URL}${path}`, {
64
113
  ...options,
@@ -66,9 +115,10 @@ export async function apiCall(path, options) {
66
115
  });
67
116
  if (!res.ok) {
68
117
  const body = await res.text();
69
- if (res.status === 401) {
118
+ if (res.status === 401 && requireValidWorkerBearerRuntime().mode === "owner") {
70
119
  // Only clear on 401 (invalid/expired credential), NOT on 403
71
120
  // (valid credential but insufficient permissions, e.g., private repo access)
121
+ const { clearStoredAuth } = await ownerAuthStoreLoader();
72
122
  clearStoredAuth();
73
123
  clearAuthenticatedAccountCache();
74
124
  }
@@ -0,0 +1,19 @@
1
+ export const EXECUTION_PROFILES = [
2
+ "supervised_room_turn",
3
+ "autonomous_mcp_worker",
4
+ "interactive_desktop",
5
+ ];
6
+ export const LETAGENTS_EXECUTION_PROFILE_ENV = "LETAGENTS_EXECUTION_PROFILE";
7
+ export const LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV = "LETAGENTS_SUPERVISED_BOUNDED_TURNS";
8
+ export function executionProfile(env = process.env) {
9
+ const configured = env[LETAGENTS_EXECUTION_PROFILE_ENV]?.trim();
10
+ const bounded = env[LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV]?.trim() === "1";
11
+ if (configured && !EXECUTION_PROFILES.includes(configured)) {
12
+ throw new Error(`Invalid ${LETAGENTS_EXECUTION_PROFILE_ENV}: ${configured}`);
13
+ }
14
+ const profile = (configured || "autonomous_mcp_worker");
15
+ if (bounded !== (profile === "supervised_room_turn")) {
16
+ throw new Error(`${LETAGENTS_EXECUTION_PROFILE_ENV}=supervised_room_turn and ${LETAGENTS_SUPERVISED_BOUNDED_TURNS_ENV}=1 must be configured together.`);
17
+ }
18
+ return profile;
19
+ }
@@ -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 !== "owner") {
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 !== "owner") {
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);
@@ -4,6 +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
8
  const roomPresenceByIdentity = new Map();
8
9
  export function getRememberedRoomPresence(roomId, identity) {
9
10
  if (!roomId || !identity) {
@@ -18,7 +19,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
18
19
  }
19
20
  roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, resolvedIdentity.actor_label), presence);
20
21
  const { localRoomId, cloudRoomId } = await resolveLocalRoomStorageIdentifiers(roomId);
21
- if (await isLocalRoomStorageEnabled(roomId)) {
22
+ if (!isSupervisedBoundedTurn() && await isLocalRoomStorageEnabled(roomId)) {
22
23
  touchRoomSession(localRoomId || roomId);
23
24
  return;
24
25
  }
@@ -38,7 +39,8 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
38
39
  ...agentSessionCredentials(agentSession),
39
40
  }),
40
41
  });
41
- touchRoomSession(apiRoomId);
42
+ if (!isSupervisedBoundedTurn())
43
+ touchRoomSession(apiRoomId);
42
44
  }
43
45
  catch (error) {
44
46
  if (isMissingRouteError(error)) {
@@ -3,13 +3,24 @@ import { LETAGENTS_ORIGIN_ROOM_ID_HEADER } from "../../../shared/request-headers
3
3
  import { apiCall, isMissingRouteError, } from "./api.js";
4
4
  import { maybeHandleRepoRoomAuthRequired } from "./device-auth.js";
5
5
  import { getLastMessageId } from "./messages.js";
6
- import { currentRoom } from "./room-state.js";
6
+ import { currentRoom, getCurrentSupervisedRoomAuthority } from "./room-state.js";
7
+ import { isSupervisedBoundedTurn } from "./worker-bearer.js";
7
8
  export async function roomScopedApiCall(input) {
9
+ const supervised = isSupervisedBoundedTurn();
10
+ const exactRoomAuthority = supervised ? getCurrentSupervisedRoomAuthority() : null;
11
+ if (supervised && (!exactRoomAuthority || input.room_id !== exactRoomAuthority)) {
12
+ throw new Error("The daemon-supervised API request is missing its exact per-call room authority.");
13
+ }
8
14
  const headers = {
9
15
  ...input.options?.headers,
10
16
  };
11
- if (currentRoom?.room_id &&
12
- !Object.keys(headers).some((key) => key.toLowerCase() === LETAGENTS_ORIGIN_ROOM_ID_HEADER.toLowerCase())) {
17
+ const originHeaderKey = Object.keys(headers).find((key) => key.toLowerCase() === LETAGENTS_ORIGIN_ROOM_ID_HEADER.toLowerCase());
18
+ if (exactRoomAuthority) {
19
+ if (originHeaderKey)
20
+ delete headers[originHeaderKey];
21
+ headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = exactRoomAuthority;
22
+ }
23
+ else if (currentRoom?.room_id && !originHeaderKey) {
13
24
  headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = currentRoom.room_id;
14
25
  }
15
26
  const options = {
@@ -17,16 +28,20 @@ export async function roomScopedApiCall(input) {
17
28
  headers,
18
29
  };
19
30
  if (input.room_id) {
20
- const { cloudRoomId } = await resolveLocalRoomStorageIdentifiers(input.room_id);
31
+ const cloudRoomId = supervised
32
+ ? null
33
+ : (await resolveLocalRoomStorageIdentifiers(input.room_id)).cloudRoomId;
21
34
  const apiRoomId = cloudRoomId || input.room_id;
22
35
  try {
23
36
  const result = await apiCall(input.room_path(apiRoomId), options);
24
- touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
37
+ if (!supervised) {
38
+ touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
39
+ }
25
40
  return result;
26
41
  }
27
42
  catch (error) {
28
43
  await maybeHandleRepoRoomAuthRequired(error, apiRoomId);
29
- if (!input.project_id || !isMissingRouteError(error)) {
44
+ if (supervised || !input.project_id || !isMissingRouteError(error)) {
30
45
  throw error;
31
46
  }
32
47
  }
@@ -36,7 +51,9 @@ export async function roomScopedApiCall(input) {
36
51
  }
37
52
  const result = await apiCall(input.project_path(input.project_id), options);
38
53
  if (input.room_id) {
39
- touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
54
+ if (!supervised) {
55
+ touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
56
+ }
40
57
  }
41
58
  return result;
42
59
  }