letagents 0.12.8 → 0.12.10
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 +16 -15
- package/dist/mcp/agent-prompt-delivery.js +20 -0
- package/dist/mcp/codex-session/app-server.js +89 -0
- package/dist/mcp/codex-session/index.js +10 -0
- package/dist/mcp/codex-session/rpc-client.js +85 -0
- package/dist/mcp/codex-session/runtime-bridge.js +254 -0
- package/dist/mcp/codex-session/runtime-summary.js +220 -0
- package/dist/mcp/codex-session/session-inspection.js +100 -0
- package/dist/mcp/codex-session/session-mapper.js +51 -0
- package/dist/mcp/codex-session/session-start.js +182 -0
- package/dist/mcp/codex-session/session-status.js +77 -0
- package/dist/mcp/codex-session/session-stop.js +43 -0
- package/dist/mcp/codex-session/session-supervisor.js +111 -0
- package/dist/mcp/codex-session/start-prompt.js +49 -0
- package/dist/mcp/codex-session/types.js +1 -0
- package/dist/mcp/codex-session.js +1 -500
- package/dist/mcp/config-reader.js +10 -2
- package/dist/mcp/git-remote.js +90 -14
- package/dist/mcp/local-state/agent-identity.js +26 -0
- package/dist/mcp/local-state/agent-sessions.js +66 -0
- package/dist/mcp/local-state/auth.js +57 -0
- package/dist/mcp/local-state/codex-live-sessions.js +59 -0
- package/dist/mcp/local-state/index.js +8 -0
- package/dist/mcp/local-state/local-chat.js +808 -0
- package/dist/mcp/local-state/room-sessions.js +51 -0
- package/dist/mcp/local-state/storage.js +95 -0
- package/dist/mcp/local-state/types.js +1 -0
- package/dist/mcp/local-state.js +1 -286
- package/dist/mcp/managed-agent-providers.js +65 -0
- package/dist/mcp/rental-tools/activity.js +37 -0
- package/dist/mcp/rental-tools/budget.js +36 -0
- package/dist/mcp/rental-tools/context.js +62 -0
- package/dist/mcp/rental-tools/lifecycle.js +47 -0
- package/dist/mcp/rental-tools/metering.js +73 -0
- package/dist/mcp/rental-tools/patches.js +100 -0
- package/dist/mcp/rental-tools/provider.js +117 -0
- package/dist/mcp/rental-tools/shared.js +19 -0
- package/dist/mcp/rental-tools/types.js +1 -0
- package/dist/mcp/rental-tools.js +7 -0
- package/dist/mcp/repo-visibility.js +8 -6
- package/dist/mcp/server/register-tools.js +19 -0
- package/dist/mcp/server/repo-context.js +28 -0
- package/dist/mcp/server/resources.js +77 -0
- package/dist/mcp/server/runtime/agent-sessions.js +134 -0
- package/dist/mcp/server/runtime/api.js +82 -0
- package/dist/mcp/server/runtime/auth-cache.js +20 -0
- package/dist/mcp/server/runtime/device-auth.js +48 -0
- package/dist/mcp/server/runtime/identity/config.js +58 -0
- package/dist/mcp/server/runtime/identity/directory.js +87 -0
- package/dist/mcp/server/runtime/identity/liveness.js +32 -0
- package/dist/mcp/server/runtime/identity/names.js +74 -0
- package/dist/mcp/server/runtime/identity/public.js +19 -0
- package/dist/mcp/server/runtime/identity/state.js +32 -0
- package/dist/mcp/server/runtime/identity.js +96 -0
- package/dist/mcp/server/runtime/messages.js +160 -0
- package/dist/mcp/server/runtime/presence.js +52 -0
- package/dist/mcp/server/runtime/room-api.js +42 -0
- package/dist/mcp/server/runtime/room-state.js +122 -0
- package/dist/mcp/server/runtime/rooms.js +318 -0
- package/dist/mcp/server/runtime.js +14 -0
- package/dist/mcp/server/tools/agent-sessions.js +374 -0
- package/dist/mcp/server/tools/messages/index.js +14 -0
- package/dist/mcp/server/tools/messages/read-tool.js +176 -0
- package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
- package/dist/mcp/server/tools/messages/response.js +17 -0
- package/dist/mcp/server/tools/messages/send-tool.js +203 -0
- package/dist/mcp/server/tools/messages/status-tool.js +87 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +333 -0
- package/dist/mcp/server/tools/messages.js +1 -0
- package/dist/mcp/server/tools/onboarding/device-auth-tools.js +148 -0
- package/dist/mcp/server/tools/onboarding/name-tool.js +89 -0
- package/dist/mcp/server/tools/onboarding/responses.js +10 -0
- package/dist/mcp/server/tools/onboarding/status-tool.js +108 -0
- package/dist/mcp/server/tools/onboarding.js +8 -0
- package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
- package/dist/mcp/server/tools/rental/context-tools.js +35 -0
- package/dist/mcp/server/tools/rental/index.js +17 -0
- package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
- package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
- package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
- package/dist/mcp/server/tools/rental/response.js +10 -0
- package/dist/mcp/server/tools/rental/types.js +1 -0
- package/dist/mcp/server/tools/rental.js +1 -0
- package/dist/mcp/server/tools/rooms/index.js +5 -0
- package/dist/mcp/server/tools/rooms/inspection-tools.js +124 -0
- package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
- package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
- package/dist/mcp/server/tools/rooms/response.js +10 -0
- package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
- package/dist/mcp/server/tools/rooms.js +1 -0
- package/dist/mcp/server/tools/tasks/api.js +222 -0
- package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
- package/dist/mcp/server/tools/tasks/board-intent-tools.js +321 -0
- package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
- package/dist/mcp/server/tools/tasks/context.js +35 -0
- package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
- package/dist/mcp/server/tools/tasks/index.js +14 -0
- package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
- package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
- package/dist/mcp/server/tools/tasks/response.js +13 -0
- package/dist/mcp/server/tools/tasks/schemas.js +68 -0
- package/dist/mcp/server/tools/tasks.js +1 -0
- package/dist/mcp/server.js +10 -2540
- package/dist/mcp/sse-client.js +22 -16
- package/dist/shared/activation-routing.js +263 -0
- package/dist/shared/agent-identity.js +21 -5
- package/dist/shared/agent-presence.js +24 -0
- package/dist/shared/handoff.js +145 -0
- package/dist/shared/rental/lrt.js +214 -0
- package/dist/shared/rental/meter-types.js +14 -0
- package/dist/shared/request-headers.js +3 -0
- package/dist/shared/room-agent-activity.js +41 -0
- package/dist/shared/room-agent-prompts.js +14 -1
- package/package.json +15 -5
- /package/dist/{mcp → shared}/codenames.js +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getRoomFromConfig } from "../../../config-reader.js";
|
|
3
|
+
import { buildActiveGitRoomContext, getGitCurrentBranch, getGitDefaultBranch, getGitRoomContext, } from "../../../git-remote.js";
|
|
4
|
+
import { resolveGitRoot } from "../../repo-context.js";
|
|
5
|
+
import { API_URL, currentAgentIdentity, currentAgentIdentityKey, currentRoom, getLocalStatePath, getPendingDeviceAuth, getStoredAgentIdentity, getStoredAuth, getStoredCurrentRoom, toPublicAgentIdentity, toPublicRoomState, toPublicStoredRoomSession, } from "../../runtime.js";
|
|
6
|
+
import { jsonTextResponse } from "./responses.js";
|
|
7
|
+
export async function checkOnboardingApiHealth(apiUrl = API_URL, fetchImpl = globalThis.fetch, timeoutMs = 1500) {
|
|
8
|
+
const url = `${apiUrl.replace(/\/+$/, "")}/api/health`;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetchImpl(url, { signal: controller.signal });
|
|
13
|
+
return {
|
|
14
|
+
url,
|
|
15
|
+
reachable: response.ok,
|
|
16
|
+
status: response.status,
|
|
17
|
+
error: response.ok ? null : `HTTP ${response.status}`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return {
|
|
22
|
+
url,
|
|
23
|
+
reachable: false,
|
|
24
|
+
status: null,
|
|
25
|
+
error: error instanceof Error ? error.message : "fetch failed",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function apiUrlHint(health) {
|
|
33
|
+
if (health.reachable) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const localApi = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::|\/|$)/i.test(API_URL);
|
|
37
|
+
if (localApi) {
|
|
38
|
+
return "The MCP server is configured for a local LetAgents API, but the health check failed. Start the local API, or set LETAGENTS_API_URL=https://letagents.chat and restart the MCP server.";
|
|
39
|
+
}
|
|
40
|
+
return "The configured LetAgents API did not pass the health check. join_room and auth tools may fail until the backend is reachable.";
|
|
41
|
+
}
|
|
42
|
+
function authHint(authenticated) {
|
|
43
|
+
return authenticated
|
|
44
|
+
? null
|
|
45
|
+
: "Public Git Rooms and ad-hoc rooms can be joined without auth. Private Git Rooms require start_device_auth, browser verification, then poll_device_auth to persist a LetAgents token.";
|
|
46
|
+
}
|
|
47
|
+
export function registerGetOnboardingStatusTool(server) {
|
|
48
|
+
server.tool("get_onboarding_status", "Inspect local Let Agents MCP auth and room-session state so a user can finish onboarding without guessing what is missing.", {
|
|
49
|
+
cwd: z
|
|
50
|
+
.string()
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Working directory to inspect for repo context. Defaults to the current process directory."),
|
|
53
|
+
}, async ({ cwd }) => {
|
|
54
|
+
const workingDir = cwd || process.cwd();
|
|
55
|
+
const repoRoot = resolveGitRoot(workingDir);
|
|
56
|
+
const configRoom = getRoomFromConfig(workingDir);
|
|
57
|
+
const gitContext = repoRoot ? getGitRoomContext(repoRoot) : null;
|
|
58
|
+
const configGitContext = configRoom
|
|
59
|
+
? buildActiveGitRoomContext({
|
|
60
|
+
repoRoom: configRoom,
|
|
61
|
+
currentBranch: repoRoot ? getGitCurrentBranch(repoRoot) : null,
|
|
62
|
+
defaultBranch: repoRoot ? getGitDefaultBranch(repoRoot) : null,
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
65
|
+
const storedAuth = getStoredAuth();
|
|
66
|
+
const pendingAuth = getPendingDeviceAuth();
|
|
67
|
+
const savedCurrentRoom = getStoredCurrentRoom();
|
|
68
|
+
const detectedRoom = configGitContext?.activeRoom ?? gitContext?.activeRoom ?? null;
|
|
69
|
+
const api_health = await checkOnboardingApiHealth();
|
|
70
|
+
const authenticated = Boolean(process.env.LETAGENTS_TOKEN || storedAuth);
|
|
71
|
+
let nextStep = "join_room";
|
|
72
|
+
if (!storedAuth && pendingAuth) {
|
|
73
|
+
nextStep = "poll_device_auth";
|
|
74
|
+
}
|
|
75
|
+
else if (savedCurrentRoom && !currentRoom) {
|
|
76
|
+
nextStep = "resume_room_session";
|
|
77
|
+
}
|
|
78
|
+
return jsonTextResponse({
|
|
79
|
+
api_url: API_URL,
|
|
80
|
+
api_health,
|
|
81
|
+
api_url_hint: apiUrlHint(api_health),
|
|
82
|
+
local_state_path: getLocalStatePath(),
|
|
83
|
+
authenticated,
|
|
84
|
+
auth_source: process.env.LETAGENTS_TOKEN
|
|
85
|
+
? "env"
|
|
86
|
+
: storedAuth
|
|
87
|
+
? "local_state"
|
|
88
|
+
: "none",
|
|
89
|
+
auth_hint: authHint(authenticated),
|
|
90
|
+
account: storedAuth?.account ?? null,
|
|
91
|
+
token_expires_at: storedAuth?.expires_at ?? null,
|
|
92
|
+
pending_device_auth: pendingAuth,
|
|
93
|
+
agent_identity: toPublicAgentIdentity(currentAgentIdentity ?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
94
|
+
current_room: toPublicRoomState(currentRoom),
|
|
95
|
+
saved_current_room: toPublicStoredRoomSession(savedCurrentRoom),
|
|
96
|
+
detected_room_from_context: detectedRoom,
|
|
97
|
+
configured_room_from_file: configRoom,
|
|
98
|
+
configured_active_room_from_context: configGitContext?.activeRoom ?? null,
|
|
99
|
+
derived_repo_room_from_git: gitContext?.repoRoom ?? null,
|
|
100
|
+
derived_active_git_room: gitContext?.activeRoom ?? null,
|
|
101
|
+
derived_branch_room_from_git: gitContext?.activeRefRoom ?? null,
|
|
102
|
+
git_current_branch: gitContext?.currentBranch ?? configGitContext?.currentBranch ?? null,
|
|
103
|
+
git_default_branch: gitContext?.defaultBranch ?? configGitContext?.defaultBranch ?? null,
|
|
104
|
+
repo_root: repoRoot,
|
|
105
|
+
next_step: nextStep,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { registerDeviceAuthTools } from "./onboarding/device-auth-tools.js";
|
|
2
|
+
import { registerSetAgentNameTool } from "./onboarding/name-tool.js";
|
|
3
|
+
import { registerGetOnboardingStatusTool } from "./onboarding/status-tool.js";
|
|
4
|
+
export function registerOnboardingTools(server) {
|
|
5
|
+
registerGetOnboardingStatusTool(server);
|
|
6
|
+
registerDeviceAuthTools(server);
|
|
7
|
+
registerSetAgentNameTool(server);
|
|
8
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rentalCancel, rentalComplete, rentalEmitActivity, } from "../../../rental-tools.js";
|
|
3
|
+
import { rentalTextResponse } from "./response.js";
|
|
4
|
+
export function registerRentalActivityTools({ server, deps, }) {
|
|
5
|
+
server.tool("rental_emit_activity", "Emit an activity event into a rental session's event log (rental_activity_events table). The event is unverified by default per §9.4 — only tool-mediated event types are auto-verified server-side. Use this to record agent notes, progress updates, or custom milestones visible to the renter.", {
|
|
6
|
+
session_id: z.string().describe("Rental session id."),
|
|
7
|
+
event_type: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe("Activity event type from §9.4 taxonomy (e.g. 'agent.note', 'edit.proposed'). Must be a known type."),
|
|
10
|
+
source: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Event source: 'agent', 'tool', 'patch_gate', 'system', 'renter', 'provider'. Defaults to 'agent'."),
|
|
14
|
+
payload: z
|
|
15
|
+
.record(z.string(), z.unknown())
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Arbitrary JSON payload for the event. Defaults to {}."),
|
|
18
|
+
verified: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Override verification status. If omitted, the server resolves from the event type set."),
|
|
22
|
+
}, async ({ session_id, event_type, source, payload, verified }) => rentalTextResponse(await rentalEmitActivity(deps, {
|
|
23
|
+
session_id,
|
|
24
|
+
event_type,
|
|
25
|
+
source,
|
|
26
|
+
payload,
|
|
27
|
+
verified,
|
|
28
|
+
})));
|
|
29
|
+
server.tool("rental_complete", "Mark a rental session as completed. Transitions the session to terminal state 'completed', records ended_at, and triggers §18.4 teardown (activity event emission, resource cleanup). Either the renter or the provider can mark completion; the caller's role is recorded in the activity event.", {
|
|
30
|
+
session_id: z.string().describe("Rental session id to complete."),
|
|
31
|
+
summary: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Optional completion summary describing what was accomplished during the session."),
|
|
35
|
+
}, async ({ session_id, summary }) => rentalTextResponse(await rentalComplete(deps, { session_id, summary })));
|
|
36
|
+
server.tool("rental_cancel", "Cancel a rental session. Transitions the session to terminal state 'cancelled', records ended_at, and triggers §18.4 teardown. Either the renter or the provider can cancel. Use this when the session needs to be aborted — e.g. renter revokes access, provider encounters an unrecoverable error, or budget is fully exhausted.", {
|
|
37
|
+
session_id: z.string().describe("Rental session id to cancel."),
|
|
38
|
+
reason: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Optional reason for cancellation (e.g. 'budget_exhausted', 'renter_revoked')."),
|
|
42
|
+
}, async ({ session_id, reason }) => rentalTextResponse(await rentalCancel(deps, { session_id, reason })));
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rentalReadFile, rentalSearch } from "../../../rental-tools.js";
|
|
3
|
+
import { rentalTextResponse } from "./response.js";
|
|
4
|
+
export function registerRentalContextTools({ server, deps, }) {
|
|
5
|
+
server.tool("rental_read_file", "Read one repo-relative file from the scoped rental workspace through the Context Broker. The server applies the Secret Firewall, records an Exposure Ledger entry, and returns redacted content when needed. This is the only approved file-read path for provider agents in scoped mode.", {
|
|
6
|
+
session_id: z.string().describe("Rental session id whose scoped workspace to read."),
|
|
7
|
+
path: z.string().describe("Repo-relative file path inside the materialized rental workspace."),
|
|
8
|
+
max_bytes: z
|
|
9
|
+
.number()
|
|
10
|
+
.int()
|
|
11
|
+
.positive()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Optional maximum file size to return. Defaults server-side to 256 KiB."),
|
|
14
|
+
}, async ({ session_id, path, max_bytes }) => rentalTextResponse(await rentalReadFile(deps, { session_id, path, max_bytes })));
|
|
15
|
+
server.tool("rental_search", "Search literal text across the scoped rental workspace through the Context Broker. Matching snippets are Secret Firewall scanned and recorded as search_result exposures, but search results alone do not authorize Patch Gate edits.", {
|
|
16
|
+
session_id: z.string().describe("Rental session id whose scoped workspace to search."),
|
|
17
|
+
query: z.string().describe("Literal text query to search for."),
|
|
18
|
+
max_results: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.positive()
|
|
22
|
+
.max(100)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Maximum snippets to return. Defaults server-side to 20, capped at 100."),
|
|
25
|
+
case_sensitive: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Set true for case-sensitive literal search. Defaults to false."),
|
|
29
|
+
}, async ({ session_id, query, max_results, case_sensitive }) => rentalTextResponse(await rentalSearch(deps, {
|
|
30
|
+
session_id,
|
|
31
|
+
query,
|
|
32
|
+
max_results,
|
|
33
|
+
case_sensitive,
|
|
34
|
+
})));
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { apiCall } from "../../runtime.js";
|
|
2
|
+
import { registerRentalActivityTools } from "./activity-tools.js";
|
|
3
|
+
import { registerRentalContextTools } from "./context-tools.js";
|
|
4
|
+
import { registerRentalMeteringTools } from "./metering-tools.js";
|
|
5
|
+
import { registerRentalPatchTools } from "./patch-tools.js";
|
|
6
|
+
import { registerRentalProviderTools } from "./provider-tools.js";
|
|
7
|
+
export function registerRentalTools(server) {
|
|
8
|
+
const context = {
|
|
9
|
+
server,
|
|
10
|
+
deps: { apiCall },
|
|
11
|
+
};
|
|
12
|
+
registerRentalProviderTools(context);
|
|
13
|
+
registerRentalMeteringTools(context);
|
|
14
|
+
registerRentalContextTools(context);
|
|
15
|
+
registerRentalPatchTools(context);
|
|
16
|
+
registerRentalActivityTools(context);
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rentalHeartbeat, rentalRefreshQuota, rentalReportUsage, rentalRequestBudgetExtension, } from "../../../rental-tools.js";
|
|
3
|
+
import { rentalTextResponse } from "./response.js";
|
|
4
|
+
export function registerRentalMeteringTools({ server, deps, }) {
|
|
5
|
+
server.tool("rental_heartbeat", "Beat the heart of an active Rent an Agent provider session. Records last_heartbeat_at and transitions provisioning → active on the first beat. Provider-only; the server returns not_provider when the caller does not own the lane. Call on a 30s cadence per spec §18.3.", {
|
|
6
|
+
session_id: z.string().describe("Rental session id to heartbeat."),
|
|
7
|
+
}, async ({ session_id }) => rentalTextResponse(await rentalHeartbeat(deps, { session_id })));
|
|
8
|
+
server.tool("rental_refresh_quota", "Read the most recent native quota snapshot recorded for a rental session (POST /api/rental/sessions/:id/refresh-quota). V1 returns the snapshot already stored on the session row by the desktop meter adapter pipeline; the server cannot push-poll a provider's adapter from here, so `refreshed` is false. Use this when the rented agent (or a renter UI) needs the freshest provider-side quota state visible to the server.", {
|
|
9
|
+
session_id: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Rental session id whose latest quota snapshot to read."),
|
|
12
|
+
provider: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Optional provider hint (e.g. 'antigravity', 'cursor'). Used only for an audit trail; the server returns whatever snapshot it has regardless."),
|
|
16
|
+
}, async ({ session_id, provider }) => rentalTextResponse(await rentalRefreshQuota(deps, { session_id, provider })));
|
|
17
|
+
server.tool("rental_report_usage", "Forward an already-built IngestUsageReport to the rental usage ingest endpoint (POST /api/rental/sessions/:id/usage). The desktop meter adapter is the canonical source of these reports, so MCP-side use should be limited to tool-mediated steps the desktop adapter pipeline does not observe. See spec §17.7 / §19.6 for the report shape; the server validates and rejects malformed input. SCOPE: this tool ONLY persists to rental_usage_meters; it does NOT advance Budget Sentinel state. Callers that need budget gates to update (e.g. trigger budget.reconciled / budget.exhausted events) must separately invoke the Budget Sentinel reconcile endpoint (POST /api/rental/sessions/:id/budget/reconcile — p2.8b) or a future rental_reconcile_budget MCP tool.", {
|
|
18
|
+
session_id: z.string().describe("Rental session id to report against."),
|
|
19
|
+
report: z
|
|
20
|
+
.record(z.string(), z.unknown())
|
|
21
|
+
.describe("Pre-built IngestUsageReport object (source, snapshot, delta, lrt, idempotencyKey, etc.). Forwarded verbatim."),
|
|
22
|
+
}, async ({ session_id, report }) => rentalTextResponse(await rentalReportUsage(deps, { session_id, report })));
|
|
23
|
+
server.tool("rental_request_budget_extension", "Request additional LRT for a Rent an Agent session. This creates a pending budget.extension_requested activity event only; it does not grant budget and cannot approve itself. A renter must approve the request through the renter-side budget-extension approval route before the session lrt_limit changes.", {
|
|
24
|
+
session_id: z.string().describe("Rental session id that needs more LRT."),
|
|
25
|
+
requested_additional_lrt: z
|
|
26
|
+
.number()
|
|
27
|
+
.int()
|
|
28
|
+
.positive()
|
|
29
|
+
.describe("Positive amount of additional LRT requested."),
|
|
30
|
+
reason: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Optional short reason shown to the renter."),
|
|
34
|
+
}, async ({ session_id, requested_additional_lrt, reason }) => rentalTextResponse(await rentalRequestBudgetExtension(deps, {
|
|
35
|
+
session_id,
|
|
36
|
+
requested_additional_lrt,
|
|
37
|
+
reason,
|
|
38
|
+
})));
|
|
39
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rentalProposeEdit, rentalProposePatch, rentalRunCommand, } from "../../../rental-tools.js";
|
|
3
|
+
import { rentalTextResponse } from "./response.js";
|
|
4
|
+
export function registerRentalPatchTools({ server, deps, }) {
|
|
5
|
+
server.tool("rental_propose_edit", "Append one whole-file edit to the Signed Change Journal for a rental session. This records an idempotent, tool-mediated edit and returns the reconstructed diff; Patch Gate validation happens through rental_propose_patch.", {
|
|
6
|
+
session_id: z.string().describe("Rental session id."),
|
|
7
|
+
idempotency_key: z.string().describe("Unique key for this edit request."),
|
|
8
|
+
path: z.string().describe("Repo-relative file path being edited."),
|
|
9
|
+
before_content: z.string().describe("Original file content the agent saw."),
|
|
10
|
+
after_content: z.string().describe("Proposed full replacement content."),
|
|
11
|
+
summary: z.string().optional().describe("Optional edit summary."),
|
|
12
|
+
}, async ({ session_id, idempotency_key, path, before_content, after_content, summary }) => rentalTextResponse(await rentalProposeEdit(deps, {
|
|
13
|
+
session_id,
|
|
14
|
+
idempotency_key,
|
|
15
|
+
path,
|
|
16
|
+
before_content,
|
|
17
|
+
after_content,
|
|
18
|
+
summary,
|
|
19
|
+
})));
|
|
20
|
+
server.tool("rental_propose_patch", "Submit a full-content patch proposal for Patch Gate validation. Every file must have been exposed through the Context Broker; the server applies path checks, Secret Firewall scanning, and records the gate result for renter review.", {
|
|
21
|
+
session_id: z.string().describe("Rental session id."),
|
|
22
|
+
idempotency_key: z.string().describe("Unique key for this patch proposal."),
|
|
23
|
+
files: z
|
|
24
|
+
.array(z.object({
|
|
25
|
+
path: z.string(),
|
|
26
|
+
operation: z.enum(["modify", "create", "delete"]),
|
|
27
|
+
content: z.string().optional(),
|
|
28
|
+
diff: z.string().optional(),
|
|
29
|
+
}))
|
|
30
|
+
.nonempty()
|
|
31
|
+
.describe("Patch files. modify/create require full content; diff-only proposals are rejected server-side."),
|
|
32
|
+
summary: z.string().optional().describe("Optional patch summary."),
|
|
33
|
+
}, async ({ session_id, idempotency_key, files, summary }) => rentalTextResponse(await rentalProposePatch(deps, {
|
|
34
|
+
session_id,
|
|
35
|
+
idempotency_key,
|
|
36
|
+
files,
|
|
37
|
+
summary,
|
|
38
|
+
})));
|
|
39
|
+
server.tool("rental_run_command", "Run a policy-checked test or verification command in the scoped rental workspace. Input must be an argv array; shell syntax, install/network/publish commands, and non-test commands are blocked.", {
|
|
40
|
+
session_id: z.string().describe("Rental session id."),
|
|
41
|
+
argv: z
|
|
42
|
+
.array(z.string())
|
|
43
|
+
.nonempty()
|
|
44
|
+
.describe("Command argv, e.g. [\"npm\", \"test\"] or [\"node\", \"--test\", \"src/api/__tests__/foo.test.ts\"]."),
|
|
45
|
+
timeout_ms: z
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.positive()
|
|
49
|
+
.max(120_000)
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Optional timeout in milliseconds. Defaults server-side to 60000, capped at 120000."),
|
|
52
|
+
}, async ({ session_id, argv, timeout_ms }) => rentalTextResponse(await rentalRunCommand(deps, { session_id, argv, timeout_ms })));
|
|
53
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rentalAccept, rentalDecline, rentalListRequests, rentalProvision, } from "../../../rental-tools.js";
|
|
3
|
+
import { rentalTextResponse } from "./response.js";
|
|
4
|
+
export function registerRentalProviderTools({ server, deps, }) {
|
|
5
|
+
server.tool("rental_list_requests", "List incoming Rent an Agent session requests for the authenticated provider. Returns an array of session objects in the 'requested' state. Use this to poll while you are available to host renters.", {}, async () => rentalTextResponse(await rentalListRequests(deps)));
|
|
6
|
+
server.tool("rental_accept", "Accept a pending Rent an Agent session request. Transitions the session from 'requested' to 'accepted' per spec §18.2. An optional idempotency_key is forwarded to the server for future idempotency support.", {
|
|
7
|
+
session_id: z
|
|
8
|
+
.string()
|
|
9
|
+
.describe("Rental session id (e.g. 'rsess_*'). Get this from rental_list_requests."),
|
|
10
|
+
idempotency_key: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Optional caller-chosen idempotency key. Forwarded to the server when provided."),
|
|
14
|
+
}, async ({ session_id, idempotency_key }) => rentalTextResponse(await rentalAccept(deps, { session_id, idempotency_key })));
|
|
15
|
+
server.tool("rental_provision", "Provision an accepted Rent an Agent session into a rental room. Creates the provider-visible rental room, moves the session to 'provisioning', and returns the room id. Call this after rental_accept and before rental_heartbeat.", {
|
|
16
|
+
session_id: z.string().describe("Rental session id to provision."),
|
|
17
|
+
parent_room_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe("Parent room id/identifier where the renter started the session."),
|
|
20
|
+
provider_display_name: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Optional display name for the provider participant in the rental room."),
|
|
24
|
+
}, async ({ session_id, parent_room_id, provider_display_name }) => rentalTextResponse(await rentalProvision(deps, {
|
|
25
|
+
session_id,
|
|
26
|
+
parent_room_id,
|
|
27
|
+
provider_display_name,
|
|
28
|
+
})));
|
|
29
|
+
server.tool("rental_decline", "Decline a pending Rent an Agent session request. Transitions the session from 'requested' to 'cancelled' per spec §18.2. An optional reason is forwarded in the request body.", {
|
|
30
|
+
session_id: z.string().describe("Rental session id to decline."),
|
|
31
|
+
idempotency_key: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Optional caller-chosen idempotency key. Forwarded to the server when provided."),
|
|
35
|
+
reason: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Optional short reason forwarded in the decline body (e.g. 'busy', 'out of quota')."),
|
|
39
|
+
}, async ({ session_id, idempotency_key, reason }) => rentalTextResponse(await rentalDecline(deps, {
|
|
40
|
+
session_id,
|
|
41
|
+
idempotency_key,
|
|
42
|
+
reason,
|
|
43
|
+
})));
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerRentalTools } from "./rental/index.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { registerRoomJoinTools } from "./join-tools.js";
|
|
2
|
+
export { registerRoomInspectionTools } from "./inspection-tools.js";
|
|
3
|
+
export { registerRepoInitializationTool } from "./repo-initialization-tool.js";
|
|
4
|
+
export { registerRoomResumeTool } from "./resume-tool.js";
|
|
5
|
+
export { registerRepoVisibilityTool } from "./repo-visibility-tool.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getRoomFromConfig } from "../../../config-reader.js";
|
|
5
|
+
import { buildActiveGitRoomContext, getGitCurrentBranch, getGitDefaultBranch, getGitRoomContext, } from "../../../git-remote.js";
|
|
6
|
+
import { currentAgentIdentity, currentAgentIdentityKey, currentRoom, getConversationIdentity, getCurrentLiveSessionPayload, getStoredAgentIdentity, getStoredAuth, listStoredCodexLiveSessions, toPublicAgentIdentity, toPublicRoomState, withJoinRoomAgentPrompt, } from "../../runtime.js";
|
|
7
|
+
import { findExistingConfig, resolveGitRoot, } from "../../repo-context.js";
|
|
8
|
+
import { jsonToolResponse } from "./response.js";
|
|
9
|
+
export function registerRoomInspectionTools(server) {
|
|
10
|
+
server.tool("get_current_room", "Get information about the currently joined room, including how it was joined.", {
|
|
11
|
+
conversation_id: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Optional conversation ID to report the conversation-scoped identity instead of the global one."),
|
|
15
|
+
}, async ({ conversation_id }) => jsonToolResponse(getCurrentRoomPayload(conversation_id)));
|
|
16
|
+
server.tool("check_repo", "Inspect the current repository context for Let Agents Chat. " +
|
|
17
|
+
"Shows the git repo root, detected .letagents.json path, auto-derived Git Room from git remote and active branch, " +
|
|
18
|
+
"and current room state. Useful for troubleshooting auto-join issues.", {
|
|
19
|
+
cwd: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Directory to inspect. Defaults to the current process directory."),
|
|
23
|
+
}, async ({ cwd: targetDir }) => jsonToolResponse(getRepoInspectionPayload(targetDir)));
|
|
24
|
+
}
|
|
25
|
+
function getCurrentRoomPayload(conversationId) {
|
|
26
|
+
if (!currentRoom) {
|
|
27
|
+
return {
|
|
28
|
+
connected: false,
|
|
29
|
+
message: "Not currently in any room",
|
|
30
|
+
current_local_codex_session: getCurrentLiveSessionPayload(),
|
|
31
|
+
local_codex_session_count: listStoredCodexLiveSessions().length,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const auth = getStoredAuth();
|
|
35
|
+
return withJoinRoomAgentPrompt({
|
|
36
|
+
connected: true,
|
|
37
|
+
...toPublicRoomState(currentRoom),
|
|
38
|
+
current_local_codex_session: getCurrentLiveSessionPayload(currentRoom.room_id),
|
|
39
|
+
local_codex_session_count: listStoredCodexLiveSessions().length,
|
|
40
|
+
agent_identity: toPublicAgentIdentity(getConversationIdentity(conversationId)
|
|
41
|
+
?? currentAgentIdentity
|
|
42
|
+
?? getStoredAgentIdentity(currentAgentIdentityKey)),
|
|
43
|
+
auth: auth
|
|
44
|
+
? {
|
|
45
|
+
source: process.env.LETAGENTS_TOKEN ? "env" : "local_state",
|
|
46
|
+
expires_at: auth.expires_at ?? null,
|
|
47
|
+
account: auth.account ?? null,
|
|
48
|
+
}
|
|
49
|
+
: null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function getRepoInspectionPayload(targetDir) {
|
|
53
|
+
const startDir = targetDir || process.cwd();
|
|
54
|
+
const repoRoot = resolveGitRoot(startDir);
|
|
55
|
+
const configDir = repoRoot ? findExistingConfig(startDir) : null;
|
|
56
|
+
const configPath = configDir ? join(configDir, ".letagents.json") : null;
|
|
57
|
+
const configuredRoom = repoRoot ? getRoomFromConfig(startDir) : null;
|
|
58
|
+
const gitContext = repoRoot ? getGitRoomContext(repoRoot) : null;
|
|
59
|
+
const configGitContext = configuredRoom
|
|
60
|
+
? buildActiveGitRoomContext({
|
|
61
|
+
repoRoom: configuredRoom,
|
|
62
|
+
currentBranch: repoRoot ? getGitCurrentBranch(repoRoot) : null,
|
|
63
|
+
defaultBranch: repoRoot ? getGitDefaultBranch(repoRoot) : null,
|
|
64
|
+
})
|
|
65
|
+
: null;
|
|
66
|
+
const detectedRoom = configGitContext?.activeRoom ?? gitContext?.activeRoom ?? null;
|
|
67
|
+
const currentRoomMatchesContext = Boolean(currentRoom && detectedRoom && currentRoom.room_id === detectedRoom);
|
|
68
|
+
return {
|
|
69
|
+
cwd: startDir,
|
|
70
|
+
repo_context_status: repoRoot ? "git_repo_detected" : "not_inside_git_repo",
|
|
71
|
+
git_repo_root: repoRoot ?? null,
|
|
72
|
+
config_file: configPath ?? null,
|
|
73
|
+
config_contents: readConfigContents(configPath),
|
|
74
|
+
configured_room_from_file: configuredRoom ?? null,
|
|
75
|
+
configured_active_room_from_context: configGitContext?.activeRoom ?? null,
|
|
76
|
+
derived_room_from_git: gitContext?.activeRoom ?? null,
|
|
77
|
+
derived_repo_room_from_git: gitContext?.repoRoom ?? null,
|
|
78
|
+
derived_branch_room_from_git: gitContext?.activeRefRoom ?? null,
|
|
79
|
+
git_current_branch: gitContext?.currentBranch ?? configGitContext?.currentBranch ?? null,
|
|
80
|
+
git_default_branch: gitContext?.defaultBranch ?? configGitContext?.defaultBranch ?? null,
|
|
81
|
+
detected_room_from_context: detectedRoom ?? null,
|
|
82
|
+
current_room: toPublicRoomState(currentRoom),
|
|
83
|
+
current_room_scope: currentRoomScope(currentRoomMatchesContext, detectedRoom),
|
|
84
|
+
warning: repoWarning({ repoRoot, detectedRoom, currentRoomMatchesContext }),
|
|
85
|
+
join_hint: joinHint({ repoRoot, detectedRoom, currentRoomMatchesContext }),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function readConfigContents(configPath) {
|
|
89
|
+
if (!configPath || !existsSync(configPath))
|
|
90
|
+
return null;
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return "<parse error>";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function currentRoomScope(currentRoomMatchesContext, detectedRoom) {
|
|
99
|
+
if (!currentRoom)
|
|
100
|
+
return "none";
|
|
101
|
+
return currentRoomMatchesContext && detectedRoom
|
|
102
|
+
? "matches_detected_repo_context"
|
|
103
|
+
: "existing_joined_session_not_derived_from_inspected_cwd";
|
|
104
|
+
}
|
|
105
|
+
function repoWarning(input) {
|
|
106
|
+
if (!input.repoRoot && currentRoom) {
|
|
107
|
+
return "The inspected cwd is not inside a git repo. current_room is only the previously joined MCP room session, not a room derived from this cwd.";
|
|
108
|
+
}
|
|
109
|
+
if (input.repoRoot && currentRoom && input.detectedRoom && !input.currentRoomMatchesContext) {
|
|
110
|
+
return "The current joined room differs from the room detected for this repo context.";
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function joinHint(input) {
|
|
115
|
+
if (!input.repoRoot) {
|
|
116
|
+
return "Not inside a git repo. Use create_room, join_code, or join_room to connect manually.";
|
|
117
|
+
}
|
|
118
|
+
if (!input.detectedRoom) {
|
|
119
|
+
return "Run initialize_repo to set up .letagents.json, or use join_room/join_code to connect.";
|
|
120
|
+
}
|
|
121
|
+
return input.currentRoomMatchesContext
|
|
122
|
+
? null
|
|
123
|
+
: "Use join_room with detected_room_from_context to switch this MCP session to the detected Git Room.";
|
|
124
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RepoRoomAuthRequiredError, createInviteRoom, joinInviteCode, joinNamedRoom, normalizeJoinSessionMode, toRepoRoomAuthRequiredResult, withJoinRoomAgentPrompt, } from "../../runtime.js";
|
|
3
|
+
import { jsonToolResponse } from "./response.js";
|
|
4
|
+
const joinSessionModeSchema = z
|
|
5
|
+
.enum(["live", "current"])
|
|
6
|
+
.optional()
|
|
7
|
+
.describe("Use 'current' (default) for a normal inline join. Use 'live' to start/reuse a detached local Codex room worker.");
|
|
8
|
+
async function createInviteRoomResponse() {
|
|
9
|
+
const created = await createInviteRoom();
|
|
10
|
+
return jsonToolResponse(withJoinRoomAgentPrompt(created.response));
|
|
11
|
+
}
|
|
12
|
+
export function registerRoomJoinTools(server) {
|
|
13
|
+
server.tool("create_room", "Create a new invite room on Let Agents Chat. Returns the room ID and join code.", {}, async () => createInviteRoomResponse());
|
|
14
|
+
server.tool("create_project", "Legacy alias for create_room. Creates a new invite room and returns its join code.", {}, async () => createInviteRoomResponse());
|
|
15
|
+
server.tool("join_code", "Join an existing room using an invite code.", {
|
|
16
|
+
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
17
|
+
session_mode: joinSessionModeSchema,
|
|
18
|
+
}, async ({ code, session_mode }) => jsonToolResponse(await joinInviteCode(code, normalizeJoinSessionMode(session_mode))));
|
|
19
|
+
server.tool("join_project", "Legacy alias for join_code. Join an existing room using an invite code.", {
|
|
20
|
+
code: z.string().describe("The invite code shared for the room (e.g. 'ABCX-7291')"),
|
|
21
|
+
session_mode: joinSessionModeSchema,
|
|
22
|
+
}, async ({ code, session_mode }) => jsonToolResponse(await joinInviteCode(code, normalizeJoinSessionMode(session_mode))));
|
|
23
|
+
server.tool("join_room", "Join a named room on Let Agents Chat. Creates the room if it doesn't exist. Use this for repo-based room joining.", {
|
|
24
|
+
name: z.string().describe("The room name to join (e.g. 'github.com/owner/repo')"),
|
|
25
|
+
session_mode: joinSessionModeSchema,
|
|
26
|
+
}, async ({ name, session_mode }) => {
|
|
27
|
+
try {
|
|
28
|
+
return jsonToolResponse(await joinNamedRoom(name, normalizeJoinSessionMode(session_mode)));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof RepoRoomAuthRequiredError) {
|
|
32
|
+
return jsonToolResponse(toRepoRoomAuthRequiredResult(error));
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|