letagents 0.12.13 → 0.12.15
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 +32 -0
- package/dist/mcp/local-state/agent-sessions.js +18 -3
- package/dist/mcp/local-state/local-chat.js +11 -27
- package/dist/mcp/local-state/storage.js +5 -1
- package/dist/mcp/server/register-tools.js +4 -1
- package/dist/mcp/server/runtime/agent-sessions.js +2 -2
- package/dist/mcp/server/runtime/api.js +5 -1
- package/dist/mcp/server/runtime/execution-profile.js +1 -0
- package/dist/mcp/server/runtime/identity.js +5 -0
- package/dist/mcp/server/runtime/presence.js +3 -3
- package/dist/mcp/server/runtime/room-api.js +5 -4
- package/dist/mcp/server/runtime/room-state.js +6 -6
- package/dist/mcp/server/runtime/rooms.js +3 -3
- package/dist/mcp/server/runtime/supervisor-bridge.js +80 -3
- package/dist/mcp/server/runtime/tool-surface-policy.js +7 -0
- package/dist/mcp/server/runtime/worker-bearer.js +12 -1
- package/dist/mcp/server/runtime/worker-handles.js +190 -0
- package/dist/mcp/server/runtime-contract.js +1 -0
- package/dist/mcp/server/runtime.js +3 -3
- package/dist/mcp/server/supervised-tool-facade.js +74 -2
- package/dist/mcp/server/tools/agent-sessions.js +22 -5
- package/dist/mcp/server/tools/messages/read-tool.js +6 -2
- package/dist/mcp/server/tools/messages/wait-tool.js +22 -10
- package/dist/mcp/server/tools/onboarding/name-tool.js +5 -2
- package/dist/mcp/server/worker-tool-facade.js +38 -0
- package/dist/mcp/worker-call-context.js +39 -0
- package/dist/shared/activation-routing.js +21 -4
- package/dist/shared/mcp-worker.js +7 -0
- package/dist/shared/room-agent-prompts.js +2 -2
- package/package.json +3 -3
- package/shared/execution-approval-projection.d.mts +32 -0
- package/shared/execution-approval-projection.mjs +107 -0
- package/shared/execution-approval-publication-item.d.mts +20 -0
- package/shared/execution-approval-publication-item.mjs +61 -0
- package/shared/execution-approval-publication.d.mts +53 -0
- package/shared/execution-approval-publication.mjs +136 -0
- package/shared/execution-delegation-decision.d.mts +37 -0
- package/shared/execution-delegation-decision.mjs +73 -0
- package/shared/room-agent-work.d.mts +31 -0
- package/shared/room-agent-work.mjs +44 -0
- package/shared/room-resource-invalidation.d.mts +38 -0
- package/shared/room-resource-invalidation.mjs +50 -0
package/README.md
CHANGED
|
@@ -84,6 +84,36 @@ That local state stores:
|
|
|
84
84
|
- the LetAgents token obtained from GitHub Device Flow
|
|
85
85
|
- any pending device auth request so it can be resumed
|
|
86
86
|
- the last room session and heartbeat metadata for reconnects
|
|
87
|
+
- durable MCP worker handles and their private connection credentials
|
|
88
|
+
|
|
89
|
+
Each independent chat registers once with its own random `registration_key`:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
register_agent_session(room_id="room_example", registration_key="<random UUID for this chat>", display_name="Juniper")
|
|
93
|
+
→ worker_id="worker_..."
|
|
94
|
+
send_message(worker_id="worker_...", room_id="room_example", text="Working on the task")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Keep that `worker_id` with the chat. After the MCP process restarts, reconnect
|
|
98
|
+
with `register_agent_session(worker_id="worker_...", room_id="room_example")`.
|
|
99
|
+
The worker keeps its routing identity, assigned room name, and session row;
|
|
100
|
+
its private connection credential rotates and the previous connection stops
|
|
101
|
+
working. Retry an uncertain first registration with the same `registration_key`.
|
|
102
|
+
Never share a key between chats or derive it from a name, room, or repository.
|
|
103
|
+
|
|
104
|
+
Room tools accept the handle so a shared MCP process can serve multiple chats
|
|
105
|
+
without selecting a global current worker. Pass `room_id` when a worker belongs
|
|
106
|
+
to multiple rooms. `disconnect_agent_session(worker_id="worker_...", room_id="...")`
|
|
107
|
+
ends its connection; the same handle can reconnect later. Distinct workers may
|
|
108
|
+
receive different name suffixes, but reconnecting does not allocate a new name.
|
|
109
|
+
Choose the name on first registration; `set_agent_name` changes only the legacy
|
|
110
|
+
process identity. Hosted rooms require an API version supporting worker handles.
|
|
111
|
+
|
|
112
|
+
The handle is a reference, not a credential. Keep the local state file private
|
|
113
|
+
and retain it across restarts. If the handle or its saved credentials are lost,
|
|
114
|
+
restore them explicitly; MCP never guesses another chat's identity. Existing
|
|
115
|
+
integrations using `agent_session_id` and supervisor-managed workers keep their
|
|
116
|
+
existing registration flow. The MCP server still runs locally through `npx`.
|
|
87
117
|
|
|
88
118
|
## MCP Tools
|
|
89
119
|
|
|
@@ -93,6 +123,8 @@ That local state stores:
|
|
|
93
123
|
| `join_project` | Join a project using a join code |
|
|
94
124
|
| `join_room` | Join or create a named room |
|
|
95
125
|
| `get_current_room` | Show current room and how it was joined |
|
|
126
|
+
| `register_agent_session` | Create a chat's durable worker handle or explicitly reconnect it |
|
|
127
|
+
| `disconnect_agent_session` | End a worker connection while retaining its handle for reconnects |
|
|
96
128
|
| `send_message` | Send a top-level message, or pass `thread_parent_id` to keep a reply in a thread |
|
|
97
129
|
| `send_thread_message` | Reply inside an existing message thread without polluting the main room |
|
|
98
130
|
| `read_messages` | Read all messages from the current room or a specific `room_id` |
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { readLocalState, readLocalStateSnapshot, updateLocalState } from "./storage.js";
|
|
1
|
+
import { getLocalStatePath, readLocalState, readLocalStateSnapshot, updateLocalState } from "./storage.js";
|
|
2
|
+
import { isMcpWorkerId } from "../../shared/mcp-worker.js";
|
|
3
|
+
import { pinnedWorkerConnection } from "../worker-call-context.js";
|
|
2
4
|
export function getStoredAgentSession(sessionId) {
|
|
3
5
|
if (!sessionId) {
|
|
4
6
|
return null;
|
|
5
7
|
}
|
|
6
8
|
const state = readLocalState();
|
|
7
|
-
|
|
9
|
+
const stored = state.agent_sessions?.[sessionId] ?? null;
|
|
10
|
+
if (stored && isMcpWorkerId(stored.agent_instance_id)) {
|
|
11
|
+
// A replaced process must never borrow its successor's credential from disk.
|
|
12
|
+
const pinned = pinnedWorkerConnection(getLocalStatePath(), sessionId);
|
|
13
|
+
if (!pinned || stored.session_token !== pinned.session_token) {
|
|
14
|
+
throw new Error("This worker connection was replaced. Reconnect explicitly with its worker_id.");
|
|
15
|
+
}
|
|
16
|
+
return { ...pinned, ended_at: stored.ended_at };
|
|
17
|
+
}
|
|
18
|
+
return stored;
|
|
8
19
|
}
|
|
9
20
|
export function getCurrentAgentSession(roomId) {
|
|
10
21
|
return getCurrentAgentSessionSnapshot(roomId).session;
|
|
@@ -111,13 +122,17 @@ export function replaceLocalWorkerAgentSession(session) {
|
|
|
111
122
|
});
|
|
112
123
|
return session;
|
|
113
124
|
}
|
|
114
|
-
export function endStoredAgentSession(sessionId, endedAt = new Date().toISOString()) {
|
|
125
|
+
export function endStoredAgentSession(sessionId, endedAt = new Date().toISOString(), expectedConnectionToken) {
|
|
115
126
|
let endedSession = null;
|
|
116
127
|
updateLocalState((state) => {
|
|
117
128
|
const session = state.agent_sessions?.[sessionId];
|
|
118
129
|
if (!session) {
|
|
119
130
|
return state;
|
|
120
131
|
}
|
|
132
|
+
if (isMcpWorkerId(session.agent_instance_id)
|
|
133
|
+
&& expectedConnectionToken !== session.session_token) {
|
|
134
|
+
return state;
|
|
135
|
+
}
|
|
121
136
|
endedSession = {
|
|
122
137
|
...session,
|
|
123
138
|
ended_at: endedAt,
|
|
@@ -2,6 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { withWorkerStateFence } from "../worker-call-context.js";
|
|
5
6
|
import { ensureLocalThreadRoutingProjectionSchemaAsync, getLocalThreadRoutingAgentKeysForRoots, projectLocalThreadRoutingMessage, runLocalSqliteWriteTransactionAsync, scheduleLocalThreadRoutingBackfill, } from "../../../shared/sqlite-thread-routing.mjs";
|
|
6
7
|
import { MESSAGE_SENDER_MAX_CODE_POINTS, MESSAGE_SENDER_MAX_UTF8_BYTES, POSTGRES_INTEGER_MAX, isMessageSenderWithinBounds, parseAccountAgentRoutingEnvelope, parsePositivePgIntegerScopedId, } from "../../../shared/message-contracts.mjs";
|
|
7
8
|
const localImportedRoutingAuthority = Symbol("localImportedRoutingAuthority");
|
|
@@ -367,17 +368,6 @@ async function hydrateRows(database, rows) {
|
|
|
367
368
|
}
|
|
368
369
|
return rows.map((row) => toMessage(row, row.reply_to_number ? replies.get(row.reply_to_number) ?? null : null, attachmentsByMessageNumber.get(row.number) || []));
|
|
369
370
|
}
|
|
370
|
-
function beginImmediate(database) {
|
|
371
|
-
database.exec("BEGIN IMMEDIATE");
|
|
372
|
-
}
|
|
373
|
-
function rollback(database) {
|
|
374
|
-
try {
|
|
375
|
-
database.exec("ROLLBACK");
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
// The transaction may already be closed by SQLite after an error.
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
371
|
function allocateLocalMessageNumber(database, roomId) {
|
|
382
372
|
database
|
|
383
373
|
.prepare(`
|
|
@@ -537,7 +527,7 @@ export async function addLocalChatMessage(roomId, input) {
|
|
|
537
527
|
}
|
|
538
528
|
}
|
|
539
529
|
const timestamp = new Date().toISOString();
|
|
540
|
-
const row = await runLocalSqliteWriteTransactionAsync(database, () => {
|
|
530
|
+
const row = await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => {
|
|
541
531
|
const number = allocateLocalMessageNumber(database, trimmedRoomId);
|
|
542
532
|
const insertedRow = {
|
|
543
533
|
room_id: trimmedRoomId,
|
|
@@ -570,7 +560,7 @@ export async function addLocalChatMessage(roomId, input) {
|
|
|
570
560
|
.run(insertedRow.room_id, insertedRow.number, insertedRow.reply_to_number, insertedRow.thread_root_number, insertedRow.sender, insertedRow.text, insertedRow.agent_prompt_kind, insertedRow.source, insertedRow.publisher_agent_key, insertedRow.publisher_agent_session_id, insertedRow.timestamp);
|
|
571
561
|
projectLocalThreadRoutingMessage(database, insertedRow);
|
|
572
562
|
return insertedRow;
|
|
573
|
-
});
|
|
563
|
+
}));
|
|
574
564
|
return {
|
|
575
565
|
room_id: trimmedRoomId,
|
|
576
566
|
...toMessage(row, replyTarget),
|
|
@@ -731,8 +721,7 @@ export async function addLocalTask(roomId, input) {
|
|
|
731
721
|
const database = await getDb();
|
|
732
722
|
const now = new Date().toISOString();
|
|
733
723
|
let taskId = "";
|
|
734
|
-
|
|
735
|
-
try {
|
|
724
|
+
await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => {
|
|
736
725
|
taskId = allocateLocalTaskId(database, trimmedRoomId);
|
|
737
726
|
database
|
|
738
727
|
.prepare(`
|
|
@@ -745,12 +734,7 @@ export async function addLocalTask(roomId, input) {
|
|
|
745
734
|
VALUES (?, ?, ?, ?, 'proposed', NULL, NULL, NULL, NULL, ?, NULL, NULL, NULL, NULL, ?, NULL, 1, ?, ?)
|
|
746
735
|
`)
|
|
747
736
|
.run(trimmedRoomId, taskId, title, input.description?.trim() || null, input.created_by || "agent", `local-task:${trimmedRoomId}:${taskId}`, now, now);
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
rollback(database);
|
|
752
|
-
throw error;
|
|
753
|
-
}
|
|
737
|
+
}));
|
|
754
738
|
const task = await getLocalTask(trimmedRoomId, taskId);
|
|
755
739
|
if (!task)
|
|
756
740
|
throw new Error("Local task could not be created.");
|
|
@@ -856,7 +840,7 @@ export async function updateLocalTask(roomId, taskId, patch) {
|
|
|
856
840
|
: JSON.stringify(Array.isArray(patch.workflow_artifacts) ? patch.workflow_artifacts : []);
|
|
857
841
|
const now = new Date().toISOString();
|
|
858
842
|
const database = await getDb();
|
|
859
|
-
database
|
|
843
|
+
await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => database
|
|
860
844
|
.prepare(`
|
|
861
845
|
UPDATE local_tasks
|
|
862
846
|
SET status = ?,
|
|
@@ -870,7 +854,7 @@ export async function updateLocalTask(roomId, taskId, patch) {
|
|
|
870
854
|
updated_at = ?
|
|
871
855
|
WHERE room_id = ? AND task_id = ?
|
|
872
856
|
`)
|
|
873
|
-
.run(nextStatus, patch.assignee === undefined ? current.assignee : patch.assignee || null, assigneeAgentKey, assigneeAgentInstanceId, assigneeAgentSessionId, patch.pr_url === undefined ? current.pr_url : patch.pr_url || null, workflowArtifacts, now, roomId, taskId);
|
|
857
|
+
.run(nextStatus, patch.assignee === undefined ? current.assignee : patch.assignee || null, assigneeAgentKey, assigneeAgentInstanceId, assigneeAgentSessionId, patch.pr_url === undefined ? current.pr_url : patch.pr_url || null, workflowArtifacts, now, roomId, taskId)));
|
|
874
858
|
const updated = await getLocalTask(roomId, taskId);
|
|
875
859
|
if (!updated)
|
|
876
860
|
throw new Error("Task not found.");
|
|
@@ -895,7 +879,7 @@ export async function claimLocalTaskReviewLease(roomId, taskId, input) {
|
|
|
895
879
|
const database = await getDb();
|
|
896
880
|
const now = new Date().toISOString();
|
|
897
881
|
const leaseId = currentReviewLease?.id || `local_review_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
898
|
-
database
|
|
882
|
+
await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => database
|
|
899
883
|
.prepare(`
|
|
900
884
|
UPDATE local_tasks
|
|
901
885
|
SET review_lease_id = ?,
|
|
@@ -906,7 +890,7 @@ export async function claimLocalTaskReviewLease(roomId, taskId, input) {
|
|
|
906
890
|
updated_at = ?
|
|
907
891
|
WHERE room_id = ? AND task_id = ?
|
|
908
892
|
`)
|
|
909
|
-
.run(leaseId, input.holder_label?.trim() || actorKey || "Local reviewer", actorKey, input.agent_session_id?.trim() || null, now, now, roomId, taskId);
|
|
893
|
+
.run(leaseId, input.holder_label?.trim() || actorKey || "Local reviewer", actorKey, input.agent_session_id?.trim() || null, now, now, roomId, taskId)));
|
|
910
894
|
const task = await getLocalTask(roomId, taskId);
|
|
911
895
|
const lease = task?.active_leases?.find((entry) => entry.id === leaseId);
|
|
912
896
|
if (!task || !lease)
|
|
@@ -925,7 +909,7 @@ export async function releaseLocalTaskReviewLease(roomId, taskId, input = {}) {
|
|
|
925
909
|
}
|
|
926
910
|
const database = await getDb();
|
|
927
911
|
const now = new Date().toISOString();
|
|
928
|
-
database
|
|
912
|
+
await runLocalSqliteWriteTransactionAsync(database, () => withWorkerStateFence(() => database
|
|
929
913
|
.prepare(`
|
|
930
914
|
UPDATE local_tasks
|
|
931
915
|
SET review_lease_id = NULL,
|
|
@@ -936,7 +920,7 @@ export async function releaseLocalTaskReviewLease(roomId, taskId, input = {}) {
|
|
|
936
920
|
updated_at = ?
|
|
937
921
|
WHERE room_id = ? AND task_id = ?
|
|
938
922
|
`)
|
|
939
|
-
.run(now, roomId, taskId);
|
|
923
|
+
.run(now, roomId, taskId)));
|
|
940
924
|
const task = await getLocalTask(roomId, taskId);
|
|
941
925
|
if (!task)
|
|
942
926
|
throw new Error("Task not found.");
|
|
@@ -33,6 +33,10 @@ export function readLocalStateSnapshot() {
|
|
|
33
33
|
export function readLocalState() {
|
|
34
34
|
return readLocalStateSnapshot().state;
|
|
35
35
|
}
|
|
36
|
+
/** Hold the existing state lock through a short synchronous local effect. */
|
|
37
|
+
export function withLocalStateReadLock(callback) {
|
|
38
|
+
return withStateLock((statePath) => callback(readLocalStateSnapshotFromPath(statePath)));
|
|
39
|
+
}
|
|
36
40
|
function sleepSync(ms) {
|
|
37
41
|
if (ms > 0) {
|
|
38
42
|
Atomics.wait(STATE_LOCK_SLEEP_BUFFER, 0, 0, ms);
|
|
@@ -41,7 +45,7 @@ function sleepSync(ms) {
|
|
|
41
45
|
function writeLocalStateUnlocked(statePath, state) {
|
|
42
46
|
const tempPath = `${statePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
43
47
|
try {
|
|
44
|
-
writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
48
|
+
writeFileSync(tempPath, JSON.stringify(state, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
45
49
|
renameSync(tempPath, statePath);
|
|
46
50
|
}
|
|
47
51
|
finally {
|
|
@@ -7,10 +7,13 @@ import { registerTaskTools } from "./tools/tasks.js";
|
|
|
7
7
|
import { registerSupervisedRoomTurnTools } from "./tools/supervised-room-turn.js";
|
|
8
8
|
import { toolSurfaceForExecutionProfile } from "./runtime/tool-surface-policy.js";
|
|
9
9
|
import { profileAwareToolServer } from "./supervised-tool-facade.js";
|
|
10
|
+
import { workerAwareToolServer } from "./worker-tool-facade.js";
|
|
10
11
|
export function registerTools(server, profile = "autonomous_mcp_worker", supervisedProvider = process.env.LETAGENTS_SUPERVISOR_PROVIDER?.trim() || null, options = {}) {
|
|
11
|
-
const
|
|
12
|
+
const profileTools = options.executionOwner === "daemon"
|
|
12
13
|
? server
|
|
13
14
|
: profileAwareToolServer(server, profile, undefined, supervisedProvider);
|
|
15
|
+
const tools = profile === "autonomous_mcp_worker" || profile === "interactive_desktop"
|
|
16
|
+
? workerAwareToolServer(profileTools) : profileTools;
|
|
14
17
|
const surface = toolSurfaceForExecutionProfile(profile);
|
|
15
18
|
registerRoomJoinTools(tools);
|
|
16
19
|
if (surface.agentSessionLifecycle)
|
|
@@ -5,7 +5,7 @@ import { normalizeAgentBaseName } from "../../../shared/codenames.js";
|
|
|
5
5
|
import { formatOwnerAttribution } from "../../../shared/agent-identity.js";
|
|
6
6
|
import { LETAGENTS_AGENT_SESSION_ID_HEADER, LETAGENTS_AGENT_SESSION_TOKEN_HEADER, } from "../../../shared/request-headers.js";
|
|
7
7
|
import { AGENT_INSTANCE_UUID, detectAgentIdeLabel, detectAgentRuntimeLabel, ensureAgentIdentity, } from "./identity.js";
|
|
8
|
-
import {
|
|
8
|
+
import { hasSupervisedWorkerAuthority, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
|
|
9
9
|
import { resolveCurrentSupervisedWorkerSession } from "./supervisor-bridge.js";
|
|
10
10
|
import { getDaemonToolExecutionContext, getRuntimeWorkingDirectory } from "./daemon-tool-context.js";
|
|
11
11
|
// A worker bearer already represents a server-side worker session. This local
|
|
@@ -166,7 +166,7 @@ export async function resolveWorkerToolIdentity(input) {
|
|
|
166
166
|
}
|
|
167
167
|
const agentSession = input.agentSessionId
|
|
168
168
|
? requireWorkerAgentSession(input.roomId, input.agentSessionId)
|
|
169
|
-
: input.roomId && !
|
|
169
|
+
: input.roomId && !hasSupervisedWorkerAuthority() && await isLocalRoomStorageEnabled(input.roomId)
|
|
170
170
|
? await ensureLocalWorkerAgentSession(input.roomId)
|
|
171
171
|
: requireWorkerAgentSession(input.roomId, input.agentSessionId);
|
|
172
172
|
return {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { clearAuthenticatedAccountCache } from "./auth-cache.js";
|
|
2
|
+
import { currentWorkerCall } from "../../worker-call-context.js";
|
|
3
|
+
import { LETAGENTS_AGENT_SESSION_TOKEN_HEADER } from "../../../shared/request-headers.js";
|
|
2
4
|
import { getDaemonToolExecutionContext } from "./daemon-tool-context.js";
|
|
3
5
|
import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
|
|
4
6
|
import { borrowCurrentSupervisedWorkerCredential, } from "./supervisor-bridge.js";
|
|
@@ -123,7 +125,9 @@ export async function apiCall(path, options) {
|
|
|
123
125
|
});
|
|
124
126
|
if (!res.ok) {
|
|
125
127
|
const body = await res.text();
|
|
126
|
-
|
|
128
|
+
const hasWorkerCredential = currentWorkerCall() || headers.has(LETAGENTS_AGENT_SESSION_TOKEN_HEADER)
|
|
129
|
+
|| (typeof options?.body === "string" && /"(?:agent_session_token|replace_agent_session_token|connection_token)"\s*:/.test(options.body));
|
|
130
|
+
if (res.status === 401 && requireValidWorkerBearerRuntime().mode === "owner" && !hasWorkerCredential) {
|
|
127
131
|
// Only clear on 401 (invalid/expired credential), NOT on 403
|
|
128
132
|
// (valid credential but insufficient permissions, e.g., private repo access)
|
|
129
133
|
const { clearStoredAuth } = await ownerAuthStoreLoader();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { buildAgentActorLabel, formatOwnerAttribution, } from "../../../shared/agent-identity.js";
|
|
2
2
|
import { apiCall, getLetagentsToken, } from "./api.js";
|
|
3
3
|
import { requireValidWorkerBearerRuntime } from "./worker-bearer.js";
|
|
4
|
+
import { currentWorkerCall } from "../../worker-call-context.js";
|
|
5
|
+
import { identityFromAgentSession } from "./agent-sessions.js";
|
|
4
6
|
import { detectAgentIdeLabel, detectAgentRuntimeLabel, } from "./identity/config.js";
|
|
5
7
|
import { resolveOwnerContext } from "./identity/directory.js";
|
|
6
8
|
import { getSessionLivenessRegistration } from "./identity/liveness.js";
|
|
@@ -9,6 +11,9 @@ import { toPublicAgentIdentity } from "./identity/public.js";
|
|
|
9
11
|
import { currentAgentIdentity, currentAgentIdentityKey, ensureAgentIdentityKey, getConversationIdentity, setConversationIdentity, storeCurrentAgentIdentity, AGENT_INSTANCE_UUID, } from "./identity/state.js";
|
|
10
12
|
export { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, detectAgentIdeLabel, detectAgentRuntimeLabel, getConversationIdentity, getSessionLivenessRegistration, resolveOwnerContext, setConversationIdentity, storeCurrentAgentIdentity, toPublicAgentIdentity, };
|
|
11
13
|
export async function ensureAgentIdentity() {
|
|
14
|
+
const worker = currentWorkerCall();
|
|
15
|
+
if (worker)
|
|
16
|
+
return identityFromAgentSession(worker);
|
|
12
17
|
const owner = await resolveOwnerContext();
|
|
13
18
|
const authAvailable = requireValidWorkerBearerRuntime().mode === "owner" && Boolean(await getLetagentsToken());
|
|
14
19
|
const ideLabel = detectAgentIdeLabel();
|
|
@@ -4,7 +4,7 @@ import { isLocalRoomStorageEnabled, resolveLocalRoomStorageIdentifiers, touchRoo
|
|
|
4
4
|
import { apiCall, isMissingRouteError } from "./api.js";
|
|
5
5
|
import { agentSessionCredentials, identityFromAgentSession } from "./agent-sessions.js";
|
|
6
6
|
import { getSessionLivenessRegistration } from "./identity.js";
|
|
7
|
-
import {
|
|
7
|
+
import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
|
|
8
8
|
const roomPresenceByIdentity = new Map();
|
|
9
9
|
export function getRememberedRoomPresence(roomId, identity) {
|
|
10
10
|
if (!roomId || !identity) {
|
|
@@ -19,7 +19,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
|
|
|
19
19
|
}
|
|
20
20
|
roomPresenceByIdentity.set(getRoomIdentityPresenceCacheKey(roomId, resolvedIdentity.actor_label), presence);
|
|
21
21
|
const { localRoomId, cloudRoomId } = await resolveLocalRoomStorageIdentifiers(roomId);
|
|
22
|
-
if (!
|
|
22
|
+
if (!hasSupervisedWorkerAuthority() && await isLocalRoomStorageEnabled(roomId)) {
|
|
23
23
|
touchRoomSession(localRoomId || roomId);
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
@@ -39,7 +39,7 @@ export async function syncRoomPresence(roomId, identity, presence, agentSession)
|
|
|
39
39
|
...agentSessionCredentials(agentSession),
|
|
40
40
|
}),
|
|
41
41
|
});
|
|
42
|
-
if (!
|
|
42
|
+
if (!hasSupervisedWorkerAuthority())
|
|
43
43
|
touchRoomSession(apiRoomId);
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
@@ -4,9 +4,10 @@ import { apiCall, isMissingRouteError, } from "./api.js";
|
|
|
4
4
|
import { maybeHandleRepoRoomAuthRequired } from "./device-auth.js";
|
|
5
5
|
import { getLastMessageId } from "./messages.js";
|
|
6
6
|
import { currentRoom, getCurrentSupervisedRoomAuthority } from "./room-state.js";
|
|
7
|
-
import {
|
|
7
|
+
import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
|
|
8
|
+
import { currentWorkerCall } from "../../worker-call-context.js";
|
|
8
9
|
export async function roomScopedApiCall(input) {
|
|
9
|
-
const supervised =
|
|
10
|
+
const supervised = hasSupervisedWorkerAuthority();
|
|
10
11
|
const exactRoomAuthority = supervised ? getCurrentSupervisedRoomAuthority() : null;
|
|
11
12
|
if (supervised && (!exactRoomAuthority || input.room_id !== exactRoomAuthority)) {
|
|
12
13
|
throw new Error("The daemon-supervised API request is missing its exact per-call room authority.");
|
|
@@ -20,8 +21,8 @@ export async function roomScopedApiCall(input) {
|
|
|
20
21
|
delete headers[originHeaderKey];
|
|
21
22
|
headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = exactRoomAuthority;
|
|
22
23
|
}
|
|
23
|
-
else if (currentRoom?.room_id && !originHeaderKey) {
|
|
24
|
-
headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = currentRoom.room_id;
|
|
24
|
+
else if ((currentWorkerCall()?.room_id || currentRoom?.room_id) && !originHeaderKey) {
|
|
25
|
+
headers[LETAGENTS_ORIGIN_ROOM_ID_HEADER] = currentWorkerCall()?.room_id ?? currentRoom.room_id;
|
|
25
26
|
}
|
|
26
27
|
const options = {
|
|
27
28
|
...input.options,
|
|
@@ -3,7 +3,7 @@ import { getStoredAgentIdentity, saveRoomSession, touchRoomSession, } from "../.
|
|
|
3
3
|
import { getCanonicalRoomWebPath, } from "../../room-id.js";
|
|
4
4
|
import { getApiUrl, getLetagentsToken } from "./api.js";
|
|
5
5
|
import { AGENT_INSTANCE_UUID, currentAgentIdentity, currentAgentIdentityKey, } from "./identity.js";
|
|
6
|
-
import {
|
|
6
|
+
import { hasSupervisedWorkerAuthority } from "./worker-bearer.js";
|
|
7
7
|
import { getCurrentSupervisedRoomAuthority, runWithSupervisedRoomAuthority, } from "./supervised-room-authority.js";
|
|
8
8
|
let mcpServer = null;
|
|
9
9
|
let sseClient = null;
|
|
@@ -92,7 +92,7 @@ export function toPublicRoomResponse(response, fallbackRoomId) {
|
|
|
92
92
|
}
|
|
93
93
|
export function rememberRoom(state, lastMessageId) {
|
|
94
94
|
currentRoom = state;
|
|
95
|
-
if (
|
|
95
|
+
if (hasSupervisedWorkerAuthority())
|
|
96
96
|
return state;
|
|
97
97
|
saveRoomSession({
|
|
98
98
|
room_id: state.room_id,
|
|
@@ -123,7 +123,7 @@ export function rememberRoom(state, lastMessageId) {
|
|
|
123
123
|
return state;
|
|
124
124
|
}
|
|
125
125
|
export function touchCurrentRoom(lastMessageId) {
|
|
126
|
-
if (
|
|
126
|
+
if (hasSupervisedWorkerAuthority())
|
|
127
127
|
return;
|
|
128
128
|
if (!currentRoom) {
|
|
129
129
|
return;
|
|
@@ -131,7 +131,7 @@ export function touchCurrentRoom(lastMessageId) {
|
|
|
131
131
|
touchRoomSession(currentRoom.room_id, lastMessageId);
|
|
132
132
|
}
|
|
133
133
|
export function getTargetRoomId(roomId) {
|
|
134
|
-
if (
|
|
134
|
+
if (hasSupervisedWorkerAuthority()) {
|
|
135
135
|
const exactRoomAuthority = getCurrentSupervisedRoomAuthority();
|
|
136
136
|
if (!exactRoomAuthority) {
|
|
137
137
|
throw new Error("The daemon-supervised tool has not received its exact room authority.");
|
|
@@ -162,7 +162,7 @@ export function toPublicCurrentRoomState() {
|
|
|
162
162
|
* repository inspection and can safely rebind after a durable room move.
|
|
163
163
|
*/
|
|
164
164
|
export function runWithCurrentSupervisedRoom(roomId, callback) {
|
|
165
|
-
if (!
|
|
165
|
+
if (!hasSupervisedWorkerAuthority()) {
|
|
166
166
|
throw new Error("Only a daemon-supervised bounded turn can bind supervisor room authority.");
|
|
167
167
|
}
|
|
168
168
|
const normalized = roomId.trim();
|
|
@@ -172,7 +172,7 @@ export function runWithCurrentSupervisedRoom(roomId, callback) {
|
|
|
172
172
|
return runWithSupervisedRoomAuthority(normalized, callback);
|
|
173
173
|
}
|
|
174
174
|
export function getFallbackProjectId() {
|
|
175
|
-
if (
|
|
175
|
+
if (hasSupervisedWorkerAuthority())
|
|
176
176
|
return null;
|
|
177
177
|
return currentRoom?.project_id ?? null;
|
|
178
178
|
}
|
|
@@ -9,7 +9,7 @@ import { ensureAgentIdentity, toPublicAgentIdentity, withAgentIdentity, } from "
|
|
|
9
9
|
import { withJoinRoomAgentPrompt } from "./messages.js";
|
|
10
10
|
import { syncRoomPresence } from "./presence.js";
|
|
11
11
|
import { rememberRoom, toPublicRoomResponse, toRoomState, } from "./room-state.js";
|
|
12
|
-
import {
|
|
12
|
+
import { hasSupervisedWorkerAuthority, requireValidWorkerBearerRuntime } from "./worker-bearer.js";
|
|
13
13
|
export function normalizeJoinSessionMode(value) {
|
|
14
14
|
return String(value || "").trim().toLowerCase() === "live" ? "live" : "current";
|
|
15
15
|
}
|
|
@@ -30,7 +30,7 @@ function parseGeneratedGitRefRoomIdentifier(identifier) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
export async function joinRoomIdentifier(identifier, joinedVia, options = {}) {
|
|
33
|
-
if (
|
|
33
|
+
if (hasSupervisedWorkerAuthority()) {
|
|
34
34
|
throw new Error("Room joins and creation are disabled during a daemon-supervised bounded turn.");
|
|
35
35
|
}
|
|
36
36
|
const roomId = joinedVia === "join_code" ? normalizeInviteCode(identifier) : identifier.trim();
|
|
@@ -191,7 +191,7 @@ export async function joinRoomIdentifierWithoutImplicitGitRefCreate(identifier,
|
|
|
191
191
|
}));
|
|
192
192
|
}
|
|
193
193
|
export async function createInviteRoom() {
|
|
194
|
-
if (
|
|
194
|
+
if (hasSupervisedWorkerAuthority()) {
|
|
195
195
|
throw new Error("Room joins and creation are disabled during a daemon-supervised bounded turn.");
|
|
196
196
|
}
|
|
197
197
|
const project = await apiCall("/projects", { method: "POST" });
|
|
@@ -6,12 +6,86 @@ import { join } from "node:path";
|
|
|
6
6
|
import { parsePositivePgIntegerScopedId } from "../../../../shared/message-contracts.mjs";
|
|
7
7
|
import { getCurrentSupervisedRoomAuthority } from "./supervised-room-authority.js";
|
|
8
8
|
const NEGOTIATION_PROTOCOL_VERSION = 1;
|
|
9
|
-
const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2]);
|
|
9
|
+
const SUPPORTED_SUPERVISOR_PROTOCOL_VERSIONS = new Set([1, 2, 3]);
|
|
10
10
|
const DEFAULT_REQUEST_TIMEOUT_MS = 5_000;
|
|
11
11
|
const CONFIRMED_BINDING_VERIFY_TIMEOUT_MS = 250;
|
|
12
12
|
const SUPERVISOR_CONTEXT_FILE = ".letagents-supervisor-context.json";
|
|
13
13
|
const WORK_ATTEMPT_MARKER_FILE = ".letagents-work-attempt.json";
|
|
14
14
|
const MAX_SUPERVISOR_CONTEXT_BYTES = 4 * 1024;
|
|
15
|
+
// An SDK request id is unique only within this MCP process lifetime.
|
|
16
|
+
const CUSTODIAL_POLLING_PROCESS_INCARNATION_ID = randomUUID();
|
|
17
|
+
/** Release reuses BEFORE's exact generation; it never authorizes against a successor. */
|
|
18
|
+
export async function authorizeCustodialPolling(toolName, prior, env = process.env, options = {}, waitRequest) {
|
|
19
|
+
const wait = waitRequest ? { ...waitRequest } : undefined;
|
|
20
|
+
if (toolName === "wait_for_messages") {
|
|
21
|
+
if (!wait || !(typeof wait.mcpRequestId === "string" || Number.isSafeInteger(wait.mcpRequestId))) {
|
|
22
|
+
throw new Error("Custodial wait is missing its exact MCP request id.");
|
|
23
|
+
}
|
|
24
|
+
if (!(wait.roomCursor === null || (typeof wait.roomCursor === "string" && parseRoomMessageNumber(wait.roomCursor) !== null))
|
|
25
|
+
|| (prior && (!prior.wait || prior.wait.processIncarnationId !== CUSTODIAL_POLLING_PROCESS_INCARNATION_ID
|
|
26
|
+
|| prior.wait.mcpRequestId !== wait.mcpRequestId || typeof wait.offeredFrontier !== "string"
|
|
27
|
+
|| parseRoomMessageNumber(wait.offeredFrontier) === null || prior.roomCursor === null
|
|
28
|
+
|| parseRoomMessageNumber(wait.offeredFrontier) < parseRoomMessageNumber(prior.roomCursor)))) {
|
|
29
|
+
throw new Error("Custodial wait receipt does not match its original invocation and cursor.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (wait || prior?.wait)
|
|
33
|
+
throw new Error("Only custodial wait may acknowledge or offer a cursor.");
|
|
34
|
+
if (env.LETAGENTS_EXECUTION_PROFILE?.trim() !== "supervised_mcp_polling")
|
|
35
|
+
throw new Error("Custodial polling profile required.");
|
|
36
|
+
if (env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() === "1"
|
|
37
|
+
|| env.LETAGENTS_TOKEN?.trim() || env.LETAGENTS_AGENT_SESSION_BEARER?.trim()) {
|
|
38
|
+
throw new Error("Custodial polling refuses bounded flags and environment credentials.");
|
|
39
|
+
}
|
|
40
|
+
const coordinates = prior?.coordinates ?? await resolveSupervisorCoordinates(supervisedContextSession(env), env, options);
|
|
41
|
+
if (!coordinates?.roomId || !coordinates.agentSessionId)
|
|
42
|
+
throw new Error("Custodial polling lacks exact worker coordinates.");
|
|
43
|
+
if ((wait?.requestedRoomId && wait.requestedRoomId !== coordinates.roomId)
|
|
44
|
+
|| (wait?.requestedAgentSessionId && wait.requestedAgentSessionId !== coordinates.agentSessionId)) {
|
|
45
|
+
throw new Error("Custodial wait room or worker identity does not match its exact authority.");
|
|
46
|
+
}
|
|
47
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
48
|
+
const negotiated = prior?.negotiated ?? await negotiateSupervisor(coordinates.socketPath, timeoutMs);
|
|
49
|
+
if (!negotiated.custodialPollingV1 || negotiated.generation === null)
|
|
50
|
+
throw new Error("Daemon does not support custodial_polling_v1.");
|
|
51
|
+
if (wait && !negotiated.custodialPollingOffersV1)
|
|
52
|
+
throw new Error("Daemon does not support custodialPollingOffersV1; refusing an unjournaled wait.");
|
|
53
|
+
const apiUrl = prior?.apiUrl ?? env.LETAGENTS_API_URL?.trim();
|
|
54
|
+
if (!apiUrl)
|
|
55
|
+
throw new Error("Custodial polling requires an explicit API URL.");
|
|
56
|
+
const response = await supervisorRequest(coordinates.socketPath, {
|
|
57
|
+
version: negotiated.protocolVersion, id: randomUUID(), method: "supervisor.authorize_custodial_polling",
|
|
58
|
+
params: {
|
|
59
|
+
entry_id: coordinates.entryId, room_id: coordinates.roomId, work_attempt_id: coordinates.workAttemptId,
|
|
60
|
+
execution_generation_id: coordinates.executionGenerationId, agent_session_id: coordinates.agentSessionId,
|
|
61
|
+
daemon_generation: negotiated.generation, api_url: apiUrl, contract: "custodial_polling_v1",
|
|
62
|
+
phase: prior ? "release" : "before", tool_name: toolName,
|
|
63
|
+
...(prior ? { expected_configuration_revision: prior.configurationRevision } : {}),
|
|
64
|
+
...(wait ? {
|
|
65
|
+
process_incarnation_id: CUSTODIAL_POLLING_PROCESS_INCARNATION_ID, mcp_request_id: wait.mcpRequestId,
|
|
66
|
+
...(prior ? { expected_activation_id: prior.wait.activationId, expected_binding_epoch: prior.wait.bindingEpoch,
|
|
67
|
+
input_cursor: prior.roomCursor, offered_frontier: wait.offeredFrontier } : { room_cursor: wait.roomCursor }),
|
|
68
|
+
} : {}),
|
|
69
|
+
},
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
const result = response.result;
|
|
72
|
+
if (!response.ok || response.version !== negotiated.protocolVersion || !result || result.status !== "authorized" || result.contract !== "custodial_polling_v1"
|
|
73
|
+
|| result.room_id !== coordinates.roomId || result.agent_session_id !== coordinates.agentSessionId
|
|
74
|
+
|| !Number.isSafeInteger(result.configuration_revision) || Number(result.configuration_revision) < 1
|
|
75
|
+
|| (prior && result.configuration_revision !== prior.configurationRevision)
|
|
76
|
+
|| (wait && (typeof result.activation_id !== "string" || !result.activation_id.trim()
|
|
77
|
+
|| !Number.isSafeInteger(result.binding_epoch) || Number(result.binding_epoch) < 1
|
|
78
|
+
|| typeof result.room_cursor !== "string"
|
|
79
|
+
|| (prior && (result.activation_id !== prior.wait.activationId || result.binding_epoch !== prior.wait.bindingEpoch
|
|
80
|
+
|| result.room_cursor !== prior.roomCursor))))
|
|
81
|
+
|| !(result.room_cursor === null || (typeof result.room_cursor === "string" && parseRoomMessageNumber(result.room_cursor) !== null))) {
|
|
82
|
+
throw new Error("Custodial polling authority was rejected or became stale.");
|
|
83
|
+
}
|
|
84
|
+
return { coordinates, negotiated, apiUrl, roomId: coordinates.roomId, agentSessionId: coordinates.agentSessionId,
|
|
85
|
+
roomCursor: result.room_cursor, configurationRevision: Number(result.configuration_revision),
|
|
86
|
+
...(wait ? { wait: { processIncarnationId: CUSTODIAL_POLLING_PROCESS_INCARNATION_ID, mcpRequestId: wait.mcpRequestId,
|
|
87
|
+
activationId: String(result.activation_id), bindingEpoch: Number(result.binding_epoch) } } : {}) };
|
|
88
|
+
}
|
|
15
89
|
const confirmedBindingsBySession = new Map();
|
|
16
90
|
const confirmedRequestsBySession = new Map();
|
|
17
91
|
const confirmedProtocolsBySession = new Map();
|
|
@@ -184,7 +258,8 @@ async function requireCurrentSupervisedCoordinates(env, options) {
|
|
|
184
258
|
* authority for the exact worker session identity.
|
|
185
259
|
*/
|
|
186
260
|
export async function borrowCurrentSupervisedWorkerCredential(env = process.env, options = {}) {
|
|
187
|
-
if (env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1"
|
|
261
|
+
if (env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() !== "1"
|
|
262
|
+
&& env.LETAGENTS_EXECUTION_PROFILE?.trim() !== "supervised_mcp_polling") {
|
|
188
263
|
return { state: "not_supervised" };
|
|
189
264
|
}
|
|
190
265
|
const seed = supervisedContextSession(env);
|
|
@@ -728,7 +803,9 @@ async function negotiateSupervisor(socketPath, timeoutMs) {
|
|
|
728
803
|
const daemonIdentity = hasCompleteIdentity
|
|
729
804
|
? [result.generation, result.pid, result.started_at].join(":")
|
|
730
805
|
: null;
|
|
731
|
-
return { protocolVersion, daemonIdentity, generation: hasCompleteIdentity ? Number(result.generation) : null
|
|
806
|
+
return { protocolVersion, daemonIdentity, generation: hasCompleteIdentity ? Number(result.generation) : null,
|
|
807
|
+
custodialPollingV1: result.capabilities?.custodialPollingV1 === true,
|
|
808
|
+
custodialPollingOffersV1: result.capabilities?.custodialPollingOffersV1 === true };
|
|
732
809
|
}
|
|
733
810
|
function supervisorRequest(socketPath, request, timeoutMs) {
|
|
734
811
|
return new Promise((resolve, reject) => {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
const TOOL_SURFACE_BY_PROFILE = {
|
|
2
|
+
supervised_mcp_polling: {
|
|
3
|
+
agentSessionLifecycle: false,
|
|
4
|
+
deliveryLoop: true,
|
|
5
|
+
onboarding: false,
|
|
6
|
+
rental: false,
|
|
7
|
+
roomResume: false,
|
|
8
|
+
},
|
|
2
9
|
supervised_room_turn: {
|
|
3
10
|
agentSessionLifecycle: false,
|
|
4
11
|
deliveryLoop: false,
|
|
@@ -13,13 +13,17 @@ export function getWorkerBearerRuntime() {
|
|
|
13
13
|
const bearer = process.env.LETAGENTS_AGENT_SESSION_BEARER?.trim();
|
|
14
14
|
const supervised = process.env.LETAGENTS_SUPERVISED_BOUNDED_TURNS?.trim() === "1";
|
|
15
15
|
const profile = process.env.LETAGENTS_EXECUTION_PROFILE?.trim();
|
|
16
|
+
const polling = profile === "supervised_mcp_polling";
|
|
16
17
|
if (supervised !== (profile === "supervised_room_turn")) {
|
|
17
18
|
return {
|
|
18
19
|
mode: "invalid",
|
|
19
20
|
error: "LETAGENTS_EXECUTION_PROFILE=supervised_room_turn and LETAGENTS_SUPERVISED_BOUNDED_TURNS=1 must be configured together.",
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
|
-
if (
|
|
23
|
+
if (polling && (bearer || process.env.LETAGENTS_TOKEN?.trim())) {
|
|
24
|
+
return { mode: "invalid", error: "Custodial polling refuses environment credentials; borrow exact daemon worker authority." };
|
|
25
|
+
}
|
|
26
|
+
if (!bearer && !supervised && !polling)
|
|
23
27
|
return { mode: "owner" };
|
|
24
28
|
if (bearer && supervised) {
|
|
25
29
|
return {
|
|
@@ -69,6 +73,13 @@ export function requireValidWorkerBearerRuntime() {
|
|
|
69
73
|
return runtime;
|
|
70
74
|
}
|
|
71
75
|
export function isSupervisedBoundedTurn() {
|
|
76
|
+
return hasSupervisedWorkerAuthority() && !isCustodialPolling();
|
|
77
|
+
}
|
|
78
|
+
export function isCustodialPolling() {
|
|
79
|
+
return process.env.LETAGENTS_EXECUTION_PROFILE?.trim() === "supervised_mcp_polling";
|
|
80
|
+
}
|
|
81
|
+
/** Credential custody is independent of who owns room delivery. */
|
|
82
|
+
export function hasSupervisedWorkerAuthority() {
|
|
72
83
|
return requireValidWorkerBearerRuntime().mode === "supervised";
|
|
73
84
|
}
|
|
74
85
|
export function workerModeDisabledToolResult(toolDescription = "This owner-auth onboarding tool") {
|