letagents 0.12.10 → 0.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/api/board-intent-payloads.js +25 -0
- package/dist/mcp/codex-session/runtime-bridge.js +42 -25
- package/dist/mcp/local-state/agent-sessions.js +15 -0
- package/dist/mcp/local-state/local-chat.js +1 -1
- package/dist/mcp/rental-tools/context.js +30 -0
- package/dist/mcp/server/register-tools.js +23 -12
- package/dist/mcp/server/runtime/agent-sessions.js +81 -2
- package/dist/mcp/server/runtime/api.js +63 -13
- package/dist/mcp/server/runtime/execution-profile.js +19 -0
- package/dist/mcp/server/runtime/identity/directory.js +9 -2
- package/dist/mcp/server/runtime/identity.js +2 -1
- package/dist/mcp/server/runtime/presence.js +4 -2
- package/dist/mcp/server/runtime/room-api.js +24 -7
- package/dist/mcp/server/runtime/room-state.js +46 -0
- package/dist/mcp/server/runtime/rooms.js +70 -0
- package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +731 -0
- package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
- package/dist/mcp/server/runtime/worker-bearer.js +101 -0
- package/dist/mcp/server/runtime-contract.js +27 -0
- package/dist/mcp/server/runtime.js +14 -3
- package/dist/mcp/server/supervised-tool-facade.js +105 -0
- package/dist/mcp/server/tools/agent-sessions.js +113 -5
- package/dist/mcp/server/tools/messages/index.js +3 -2
- package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
- package/dist/mcp/server/tools/messages/read-tool.js +1 -1
- package/dist/mcp/server/tools/messages/send-tool.js +5 -45
- package/dist/mcp/server/tools/messages/wait-tool.js +212 -91
- package/dist/mcp/server/tools/onboarding/device-auth-tools.js +10 -0
- package/dist/mcp/server/tools/onboarding/name-tool.js +5 -1
- package/dist/mcp/server/tools/onboarding/status-tool.js +18 -0
- package/dist/mcp/server/tools/rental/context-tools.js +9 -1
- package/dist/mcp/server/tools/rooms/inspection-tools.js +47 -18
- package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
- package/dist/mcp/server/tools/tasks/board-intent-tools.js +7 -2
- package/dist/mcp/server/tools/tasks/index.js +2 -0
- package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
- package/dist/mcp/server.js +16 -7
- package/dist/mcp/sse-client.js +3 -3
- package/dist/shared/activation-routing.js +57 -4
- package/dist/shared/agent-presence.js +1 -0
- package/dist/shared/agent-session-bearer.js +28 -0
- package/dist/shared/board-manager-failover.js +16 -0
- package/dist/shared/room-agent-prompts.js +2 -2
- package/package.json +24 -15
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,
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
: "
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
if (!supervised) {
|
|
55
|
+
touchRoomSession(input.room_id, input.preserve_session_cursor ? undefined : getLastMessageId(result));
|
|
56
|
+
}
|
|
40
57
|
}
|
|
41
58
|
return result;
|
|
42
59
|
}
|