handmux 0.23.0 → 0.25.0
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 +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open, realpath } from 'node:fs/promises';
|
|
3
|
+
import { basename, isAbsolute, relative, sep } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1000;
|
|
6
|
+
const DEFAULT_MAX_RESOURCE_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
const DEFAULT_MAX_BYTE_STORAGE = 64 * 1024 * 1024;
|
|
8
|
+
const DEFAULT_MAX_RESOURCES = 1024;
|
|
9
|
+
const DEFAULT_MAX_SESSION_RESOURCES = 128;
|
|
10
|
+
const RESOURCE_ID_RE = /^[a-zA-Z0-9_-]{16,256}$/;
|
|
11
|
+
const MEDIA_TYPE_RE = /^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}\/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$/;
|
|
12
|
+
export class AgentResourceError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = 'AgentResourceError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function validText(value, max) {
|
|
21
|
+
return typeof value === 'string' && value.length > 0 && Buffer.byteLength(value) <= max;
|
|
22
|
+
}
|
|
23
|
+
function validSession(value) {
|
|
24
|
+
return value !== null && typeof value === 'object'
|
|
25
|
+
&& validText(value.agentId, 64)
|
|
26
|
+
&& validText(value.sessionId, 1024);
|
|
27
|
+
}
|
|
28
|
+
function validName(value) {
|
|
29
|
+
return value === undefined || (validText(value, 1024) && !/[\\/\0-\x1f\x7f]/.test(value));
|
|
30
|
+
}
|
|
31
|
+
function validMediaType(value) {
|
|
32
|
+
return typeof value === 'string' && MEDIA_TYPE_RE.test(value);
|
|
33
|
+
}
|
|
34
|
+
function within(root, target) {
|
|
35
|
+
const child = relative(root, target);
|
|
36
|
+
return child === '' || (!child.startsWith(`..${sep}`) && child !== '..' && !isAbsolute(child));
|
|
37
|
+
}
|
|
38
|
+
function positiveLimit(value, fallback) {
|
|
39
|
+
return value === undefined ? fallback : Math.max(1, Math.floor(value));
|
|
40
|
+
}
|
|
41
|
+
export class AgentResourceService {
|
|
42
|
+
#allowedFileRoots;
|
|
43
|
+
#now;
|
|
44
|
+
#newResourceId;
|
|
45
|
+
#ttlMs;
|
|
46
|
+
#maxResourceBytes;
|
|
47
|
+
#maxByteStorage;
|
|
48
|
+
#maxResources;
|
|
49
|
+
#maxSessionResources;
|
|
50
|
+
#records = new Map();
|
|
51
|
+
#byteStorage = 0;
|
|
52
|
+
constructor({ allowedFileRoots = {}, now = Date.now, newResourceId = randomUUID, ttlMs = DEFAULT_TTL_MS, maxResourceBytes = DEFAULT_MAX_RESOURCE_BYTES, maxByteStorage = DEFAULT_MAX_BYTE_STORAGE, maxResources = DEFAULT_MAX_RESOURCES, maxSessionResources = DEFAULT_MAX_SESSION_RESOURCES, } = {}) {
|
|
53
|
+
this.#allowedFileRoots = allowedFileRoots;
|
|
54
|
+
this.#now = now;
|
|
55
|
+
this.#newResourceId = newResourceId;
|
|
56
|
+
this.#ttlMs = positiveLimit(ttlMs, DEFAULT_TTL_MS);
|
|
57
|
+
this.#maxResourceBytes = positiveLimit(maxResourceBytes, DEFAULT_MAX_RESOURCE_BYTES);
|
|
58
|
+
this.#maxByteStorage = positiveLimit(maxByteStorage, DEFAULT_MAX_BYTE_STORAGE);
|
|
59
|
+
this.#maxResources = positiveLimit(maxResources, DEFAULT_MAX_RESOURCES);
|
|
60
|
+
this.#maxSessionResources = positiveLimit(maxSessionResources, DEFAULT_MAX_SESSION_RESOURCES);
|
|
61
|
+
}
|
|
62
|
+
forAdapter(agentId) {
|
|
63
|
+
if (!validText(agentId, 64))
|
|
64
|
+
throw new TypeError('Agent resource registry requires an adapter id');
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
register: (session, source) => (this.#register(agentId, session, source)),
|
|
67
|
+
revoke: (resourceId) => this.#revoke(agentId, resourceId),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async read(session, resourceId) {
|
|
71
|
+
if (!validSession(session) || !RESOURCE_ID_RE.test(resourceId))
|
|
72
|
+
return null;
|
|
73
|
+
this.clearExpired();
|
|
74
|
+
const record = this.#records.get(resourceId);
|
|
75
|
+
if (!record || record.agentId !== session.agentId || record.sessionId !== session.sessionId)
|
|
76
|
+
return null;
|
|
77
|
+
let data;
|
|
78
|
+
if (record.kind === 'bytes') {
|
|
79
|
+
data = new Uint8Array(record.data);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
try {
|
|
83
|
+
const resolved = await realpath(record.path);
|
|
84
|
+
if (resolved !== record.path || !record.roots.some((root) => within(root, resolved))) {
|
|
85
|
+
this.#delete(record);
|
|
86
|
+
throw new AgentResourceError('resource-changed', 'Agent resource path changed after registration');
|
|
87
|
+
}
|
|
88
|
+
const handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
89
|
+
try {
|
|
90
|
+
const stat = await handle.stat();
|
|
91
|
+
if (!stat.isFile() || stat.size !== record.size || stat.size > this.#maxResourceBytes
|
|
92
|
+
|| stat.dev !== record.dev || stat.ino !== record.ino || stat.mtimeMs !== record.mtimeMs) {
|
|
93
|
+
this.#delete(record);
|
|
94
|
+
throw new AgentResourceError('resource-changed', 'Agent resource changed after registration');
|
|
95
|
+
}
|
|
96
|
+
data = new Uint8Array(await handle.readFile());
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await handle.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof AgentResourceError)
|
|
104
|
+
throw error;
|
|
105
|
+
this.#delete(record);
|
|
106
|
+
throw new AgentResourceError('resource-changed', 'Agent resource is no longer readable');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
resourceId,
|
|
111
|
+
session: { agentId: record.agentId, sessionId: record.sessionId },
|
|
112
|
+
data,
|
|
113
|
+
size: record.size,
|
|
114
|
+
...(record.name === undefined ? {} : { name: record.name }),
|
|
115
|
+
mediaType: record.mediaType,
|
|
116
|
+
expiresAt: record.expiresAt,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
clearExpired() {
|
|
120
|
+
const now = this.#now();
|
|
121
|
+
let removed = 0;
|
|
122
|
+
for (const record of this.#records.values()) {
|
|
123
|
+
if (record.expiresAt > now)
|
|
124
|
+
continue;
|
|
125
|
+
this.#delete(record);
|
|
126
|
+
removed += 1;
|
|
127
|
+
}
|
|
128
|
+
return removed;
|
|
129
|
+
}
|
|
130
|
+
revokeAdapter(agentId) {
|
|
131
|
+
for (const record of this.#records.values()) {
|
|
132
|
+
if (record.agentId === agentId)
|
|
133
|
+
this.#delete(record);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async #register(agentId, session, source) {
|
|
137
|
+
if (!validSession(session) || session.agentId !== agentId || !source || typeof source !== 'object'
|
|
138
|
+
|| !validName(source.name)) {
|
|
139
|
+
throw new AgentResourceError('invalid-source', 'Invalid Agent resource owner or metadata');
|
|
140
|
+
}
|
|
141
|
+
this.clearExpired();
|
|
142
|
+
const sessionCount = [...this.#records.values()].filter((record) => (record.agentId === agentId && record.sessionId === session.sessionId)).length;
|
|
143
|
+
if (this.#records.size >= this.#maxResources || sessionCount >= this.#maxSessionResources) {
|
|
144
|
+
throw new AgentResourceError('capacity-exceeded', 'Agent resource capacity is exhausted');
|
|
145
|
+
}
|
|
146
|
+
const common = {
|
|
147
|
+
resourceId: this.#allocateId(),
|
|
148
|
+
agentId,
|
|
149
|
+
sessionId: session.sessionId,
|
|
150
|
+
expiresAt: this.#now() + this.#ttlMs,
|
|
151
|
+
};
|
|
152
|
+
let record;
|
|
153
|
+
if (source.kind === 'bytes') {
|
|
154
|
+
if (!(source.data instanceof Uint8Array) || !validMediaType(source.mediaType)) {
|
|
155
|
+
throw new AgentResourceError('invalid-source', 'Invalid Agent byte resource');
|
|
156
|
+
}
|
|
157
|
+
const data = new Uint8Array(source.data);
|
|
158
|
+
if (data.byteLength > this.#maxResourceBytes) {
|
|
159
|
+
throw new AgentResourceError('resource-too-large', 'Agent resource exceeds the size limit');
|
|
160
|
+
}
|
|
161
|
+
if (this.#byteStorage + data.byteLength > this.#maxByteStorage) {
|
|
162
|
+
throw new AgentResourceError('capacity-exceeded', 'Agent byte resource storage is exhausted');
|
|
163
|
+
}
|
|
164
|
+
record = {
|
|
165
|
+
...common,
|
|
166
|
+
kind: 'bytes',
|
|
167
|
+
data,
|
|
168
|
+
size: data.byteLength,
|
|
169
|
+
...(source.name === undefined ? {} : { name: source.name }),
|
|
170
|
+
mediaType: source.mediaType,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
else if (source.kind === 'file') {
|
|
174
|
+
if (!validText(source.path, 16 * 1024) || !isAbsolute(source.path)
|
|
175
|
+
|| (source.mediaType !== undefined && !validMediaType(source.mediaType))) {
|
|
176
|
+
throw new AgentResourceError('invalid-source', 'Invalid Agent file resource');
|
|
177
|
+
}
|
|
178
|
+
const roots = await Promise.all((this.#allowedFileRoots[agentId] ?? []).map((root) => realpath(root)));
|
|
179
|
+
let resolved;
|
|
180
|
+
try {
|
|
181
|
+
resolved = await realpath(source.path);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
throw new AgentResourceError('path-denied', 'Agent resource file is unavailable');
|
|
185
|
+
}
|
|
186
|
+
if (!roots.some((root) => within(root, resolved))) {
|
|
187
|
+
throw new AgentResourceError('path-denied', 'Agent resource file is outside allowed roots');
|
|
188
|
+
}
|
|
189
|
+
const handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
190
|
+
let stat;
|
|
191
|
+
try {
|
|
192
|
+
stat = await handle.stat();
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
await handle.close();
|
|
196
|
+
}
|
|
197
|
+
if (!stat.isFile())
|
|
198
|
+
throw new AgentResourceError('invalid-source', 'Agent resource must be a regular file');
|
|
199
|
+
if (stat.size > this.#maxResourceBytes) {
|
|
200
|
+
throw new AgentResourceError('resource-too-large', 'Agent resource exceeds the size limit');
|
|
201
|
+
}
|
|
202
|
+
const basenameValue = basename(resolved).replace(/[\0-\x1f\x7f]/g, '_');
|
|
203
|
+
record = {
|
|
204
|
+
...common,
|
|
205
|
+
kind: 'file',
|
|
206
|
+
path: resolved,
|
|
207
|
+
roots,
|
|
208
|
+
dev: stat.dev,
|
|
209
|
+
ino: stat.ino,
|
|
210
|
+
mtimeMs: stat.mtimeMs,
|
|
211
|
+
size: stat.size,
|
|
212
|
+
name: source.name ?? basenameValue,
|
|
213
|
+
mediaType: source.mediaType ?? 'application/octet-stream',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
throw new AgentResourceError('invalid-source', 'Unknown Agent resource source');
|
|
218
|
+
}
|
|
219
|
+
this.#records.set(record.resourceId, record);
|
|
220
|
+
if (record.kind === 'bytes')
|
|
221
|
+
this.#byteStorage += record.size;
|
|
222
|
+
return { resourceId: record.resourceId, expiresAt: record.expiresAt };
|
|
223
|
+
}
|
|
224
|
+
async #revoke(agentId, resourceId) {
|
|
225
|
+
if (!RESOURCE_ID_RE.test(resourceId))
|
|
226
|
+
return;
|
|
227
|
+
const record = this.#records.get(resourceId);
|
|
228
|
+
if (record?.agentId === agentId)
|
|
229
|
+
this.#delete(record);
|
|
230
|
+
}
|
|
231
|
+
#allocateId() {
|
|
232
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
233
|
+
const candidate = this.#newResourceId();
|
|
234
|
+
if (RESOURCE_ID_RE.test(candidate) && !this.#records.has(candidate))
|
|
235
|
+
return candidate;
|
|
236
|
+
}
|
|
237
|
+
throw new AgentResourceError('capacity-exceeded', 'Unable to allocate an Agent resource id');
|
|
238
|
+
}
|
|
239
|
+
#delete(record) {
|
|
240
|
+
if (!this.#records.delete(record.resourceId))
|
|
241
|
+
return;
|
|
242
|
+
if (record.kind === 'bytes')
|
|
243
|
+
this.#byteStorage -= record.size;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export class AgentRunError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'AgentRunError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function attachmentKey(agentId, attachmentId) {
|
|
11
|
+
return `${agentId}\0${attachmentId}`;
|
|
12
|
+
}
|
|
13
|
+
function validText(value, max) {
|
|
14
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
15
|
+
}
|
|
16
|
+
function validateCandidate(candidate) {
|
|
17
|
+
if (!candidate || !validText(candidate.paneId, 256) || !validText(candidate.attachmentId, 512)) {
|
|
18
|
+
throw new AgentRunError('invalid-candidate', 'Attachment requires a valid paneId and attachmentId');
|
|
19
|
+
}
|
|
20
|
+
if (candidate.sessionId !== undefined && !validText(candidate.sessionId, 1024)) {
|
|
21
|
+
throw new AgentRunError('invalid-candidate', 'Attachment sessionId must be a non-empty bounded string');
|
|
22
|
+
}
|
|
23
|
+
if (candidate.implementationVersion !== undefined
|
|
24
|
+
&& (!Number.isSafeInteger(candidate.implementationVersion)
|
|
25
|
+
|| candidate.implementationVersion <= 0)) {
|
|
26
|
+
throw new AgentRunError('invalid-candidate', 'Attachment implementationVersion must be a positive integer');
|
|
27
|
+
}
|
|
28
|
+
if (!candidate.process || !Number.isSafeInteger(candidate.process.pid) || candidate.process.pid <= 0) {
|
|
29
|
+
throw new AgentRunError('invalid-candidate', 'Attachment process requires a positive integer pid');
|
|
30
|
+
}
|
|
31
|
+
if (candidate.process.startedAt !== undefined
|
|
32
|
+
&& (!Number.isFinite(candidate.process.startedAt) || candidate.process.startedAt < 0)) {
|
|
33
|
+
throw new AgentRunError('invalid-candidate', 'Attachment process startedAt must be a finite timestamp');
|
|
34
|
+
}
|
|
35
|
+
if (candidate.process.tty !== undefined && !validText(candidate.process.tty, 1024)) {
|
|
36
|
+
throw new AgentRunError('invalid-candidate', 'Attachment process tty must be a non-empty bounded string');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function sameProcess(first, second) {
|
|
40
|
+
return first.pid === second.pid
|
|
41
|
+
&& first.startedAt === second.startedAt
|
|
42
|
+
&& first.tty === second.tty;
|
|
43
|
+
}
|
|
44
|
+
function refFor(agentId, paneId, runId, sessionId, implementationVersion) {
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
agentId,
|
|
47
|
+
paneId,
|
|
48
|
+
runId,
|
|
49
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
50
|
+
...(implementationVersion === undefined ? {} : { implementationVersion }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Owns logical run generations only. Bridge connection nonces and transport reconnects never enter here.
|
|
54
|
+
export class AgentRunRuntime {
|
|
55
|
+
#byRunId = new Map();
|
|
56
|
+
#knownRunRefs = new Map();
|
|
57
|
+
#byPane = new Map();
|
|
58
|
+
#byAttachment = new Map();
|
|
59
|
+
#records = new WeakMap();
|
|
60
|
+
#paneQueues = new Map();
|
|
61
|
+
#disabledAdapters = new Set();
|
|
62
|
+
#newRunId;
|
|
63
|
+
#verifyTimeoutMs;
|
|
64
|
+
#shuttingDown = false;
|
|
65
|
+
constructor({ newRunId = randomUUID, verifyTimeoutMs = 1000, } = {}) {
|
|
66
|
+
this.#newRunId = newRunId;
|
|
67
|
+
this.#verifyTimeoutMs = Math.max(1, verifyTimeoutMs);
|
|
68
|
+
}
|
|
69
|
+
resolve(ref) {
|
|
70
|
+
if (!ref || typeof ref.runId !== 'string')
|
|
71
|
+
return null;
|
|
72
|
+
const record = this.#byRunId.get(ref.runId);
|
|
73
|
+
if (!record || record.abort.signal.aborted)
|
|
74
|
+
return null;
|
|
75
|
+
if (record.ref.agentId !== ref.agentId || record.ref.paneId !== ref.paneId)
|
|
76
|
+
return null;
|
|
77
|
+
if (ref.sessionId !== undefined && record.ref.sessionId !== ref.sessionId)
|
|
78
|
+
return null;
|
|
79
|
+
return record.lease;
|
|
80
|
+
}
|
|
81
|
+
status(ref) {
|
|
82
|
+
if (this.resolve(ref))
|
|
83
|
+
return 'current';
|
|
84
|
+
const known = ref && typeof ref.runId === 'string' ? this.#knownRunRefs.get(ref.runId) : undefined;
|
|
85
|
+
if (!known || known.agentId !== ref.agentId || known.paneId !== ref.paneId)
|
|
86
|
+
return 'unknown';
|
|
87
|
+
if (ref.sessionId !== undefined && known.sessionId !== ref.sessionId)
|
|
88
|
+
return 'unknown';
|
|
89
|
+
return 'revoked';
|
|
90
|
+
}
|
|
91
|
+
isLive(lease) {
|
|
92
|
+
const record = this.#records.get(lease);
|
|
93
|
+
return record !== undefined && this.#byRunId.get(record.ref.runId) === record
|
|
94
|
+
&& !record.abort.signal.aborted;
|
|
95
|
+
}
|
|
96
|
+
requireLive(lease) {
|
|
97
|
+
if (!this.isLive(lease))
|
|
98
|
+
throw new AgentRunError('stale-lease', 'Agent run lease is no longer live');
|
|
99
|
+
return lease.ref;
|
|
100
|
+
}
|
|
101
|
+
currentForPane(paneId) {
|
|
102
|
+
const record = this.#byPane.get(paneId);
|
|
103
|
+
return record && !record.abort.signal.aborted ? record.lease : null;
|
|
104
|
+
}
|
|
105
|
+
controller(agentId, verify) {
|
|
106
|
+
if (!validText(agentId, 64) || typeof verify !== 'function') {
|
|
107
|
+
throw new TypeError('Scoped Agent run controller requires an agentId and attachment verifier');
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
attach: (candidate) => this.#attach(agentId, verify, candidate),
|
|
111
|
+
associateSession: (lease, sessionId) => (this.#associateSession(agentId, lease, sessionId)),
|
|
112
|
+
replace: (current, candidate, reason) => this.#replace(agentId, verify, current, candidate, reason),
|
|
113
|
+
revoke: (lease, reason) => this.#revoke(agentId, lease, reason),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async revokeAdapter(agentId, reason = 'adapter_stopped') {
|
|
117
|
+
this.#disabledAdapters.add(agentId);
|
|
118
|
+
const records = [...this.#byRunId.values()].filter((record) => record.agentId === agentId);
|
|
119
|
+
await Promise.all(records.map((record) => this.#withPane(record.paneId, async () => {
|
|
120
|
+
if (this.#byRunId.get(record.ref.runId) === record)
|
|
121
|
+
this.#remove(record, reason);
|
|
122
|
+
})));
|
|
123
|
+
}
|
|
124
|
+
// Root Runtime uses this after process identity changes. Scoped adapter controllers deliberately do not
|
|
125
|
+
// receive it, so one adapter cannot evict another adapter's run by naming its pane.
|
|
126
|
+
async revokePane(paneId, reason) {
|
|
127
|
+
await this.#withPane(paneId, async () => {
|
|
128
|
+
const record = this.#byPane.get(paneId);
|
|
129
|
+
if (record)
|
|
130
|
+
this.#remove(record, reason);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async shutdown() {
|
|
134
|
+
if (this.#shuttingDown)
|
|
135
|
+
return;
|
|
136
|
+
this.#shuttingDown = true;
|
|
137
|
+
const records = [...this.#byRunId.values()];
|
|
138
|
+
await Promise.all(records.map((record) => this.#withPane(record.paneId, async () => {
|
|
139
|
+
if (this.#byRunId.get(record.ref.runId) === record)
|
|
140
|
+
this.#remove(record, 'runtime_shutdown');
|
|
141
|
+
})));
|
|
142
|
+
}
|
|
143
|
+
async #withPane(paneId, operation) {
|
|
144
|
+
const previous = this.#paneQueues.get(paneId) ?? Promise.resolve();
|
|
145
|
+
let release;
|
|
146
|
+
const current = new Promise((resolve) => { release = resolve; });
|
|
147
|
+
const queued = previous.catch(() => { }).then(() => current);
|
|
148
|
+
this.#paneQueues.set(paneId, queued);
|
|
149
|
+
await previous.catch(() => { });
|
|
150
|
+
try {
|
|
151
|
+
return await operation();
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
release();
|
|
155
|
+
if (this.#paneQueues.get(paneId) === queued)
|
|
156
|
+
this.#paneQueues.delete(paneId);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async #verify(verify, candidate) {
|
|
160
|
+
let accepted = false;
|
|
161
|
+
let timer;
|
|
162
|
+
try {
|
|
163
|
+
accepted = await Promise.race([
|
|
164
|
+
Promise.resolve(verify(candidate)).then(Boolean, () => false),
|
|
165
|
+
new Promise((resolve) => {
|
|
166
|
+
timer = setTimeout(() => resolve(false), this.#verifyTimeoutMs);
|
|
167
|
+
}),
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
catch { /* adapter probe failure is a rejection */ }
|
|
171
|
+
finally {
|
|
172
|
+
if (timer)
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
if (!accepted) {
|
|
176
|
+
throw new AgentRunError('attachment-unverified', 'Agent attachment no longer matches the live process');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
#recordFor(agentId, lease) {
|
|
180
|
+
const record = this.#records.get(lease);
|
|
181
|
+
if (!record || record.agentId !== agentId) {
|
|
182
|
+
throw new AgentRunError('foreign-lease', 'Agent run lease does not belong to this scoped controller');
|
|
183
|
+
}
|
|
184
|
+
return record;
|
|
185
|
+
}
|
|
186
|
+
#create(agentId, candidate) {
|
|
187
|
+
const abort = new AbortController();
|
|
188
|
+
const record = {};
|
|
189
|
+
record.agentId = agentId;
|
|
190
|
+
record.paneId = candidate.paneId;
|
|
191
|
+
record.attachmentId = candidate.attachmentId;
|
|
192
|
+
record.process = Object.freeze({ ...candidate.process });
|
|
193
|
+
let runId = '';
|
|
194
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
195
|
+
const proposed = this.#newRunId();
|
|
196
|
+
if (validText(proposed, 256) && !this.#byRunId.has(proposed)) {
|
|
197
|
+
runId = proposed;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!runId)
|
|
202
|
+
throw new Error('Unable to allocate a unique Agent runId');
|
|
203
|
+
record.ref = refFor(agentId, candidate.paneId, runId, candidate.sessionId, candidate.implementationVersion);
|
|
204
|
+
this.#knownRunRefs.set(runId, record.ref);
|
|
205
|
+
record.abort = abort;
|
|
206
|
+
record.lease = Object.freeze({
|
|
207
|
+
get ref() { return record.ref; },
|
|
208
|
+
signal: abort.signal,
|
|
209
|
+
});
|
|
210
|
+
this.#records.set(record.lease, record);
|
|
211
|
+
return record;
|
|
212
|
+
}
|
|
213
|
+
#install(record) {
|
|
214
|
+
this.#byRunId.set(record.ref.runId, record);
|
|
215
|
+
this.#byPane.set(record.paneId, record);
|
|
216
|
+
this.#byAttachment.set(attachmentKey(record.agentId, record.attachmentId), record);
|
|
217
|
+
}
|
|
218
|
+
#remove(record, reason) {
|
|
219
|
+
if (this.#byRunId.get(record.ref.runId) !== record)
|
|
220
|
+
return;
|
|
221
|
+
this.#byRunId.delete(record.ref.runId);
|
|
222
|
+
if (this.#byPane.get(record.paneId) === record)
|
|
223
|
+
this.#byPane.delete(record.paneId);
|
|
224
|
+
const key = attachmentKey(record.agentId, record.attachmentId);
|
|
225
|
+
if (this.#byAttachment.get(key) === record)
|
|
226
|
+
this.#byAttachment.delete(key);
|
|
227
|
+
record.abort.abort(reason);
|
|
228
|
+
}
|
|
229
|
+
#replaceInstalled(current, next, reason) {
|
|
230
|
+
this.#byRunId.delete(current.ref.runId);
|
|
231
|
+
if (this.#byPane.get(current.paneId) === current)
|
|
232
|
+
this.#byPane.delete(current.paneId);
|
|
233
|
+
const currentKey = attachmentKey(current.agentId, current.attachmentId);
|
|
234
|
+
if (this.#byAttachment.get(currentKey) === current)
|
|
235
|
+
this.#byAttachment.delete(currentKey);
|
|
236
|
+
this.#install(next);
|
|
237
|
+
current.abort.abort(reason);
|
|
238
|
+
return next.lease;
|
|
239
|
+
}
|
|
240
|
+
async #attach(agentId, verify, candidate) {
|
|
241
|
+
validateCandidate(candidate);
|
|
242
|
+
return this.#withPane(candidate.paneId, async () => {
|
|
243
|
+
if (this.#shuttingDown || this.#disabledAdapters.has(agentId)) {
|
|
244
|
+
throw new AgentRunError('runtime-unavailable', 'Agent runtime or adapter is stopped');
|
|
245
|
+
}
|
|
246
|
+
await this.#verify(verify, candidate);
|
|
247
|
+
const paneOwner = this.#byPane.get(candidate.paneId);
|
|
248
|
+
if (paneOwner && paneOwner.agentId !== agentId) {
|
|
249
|
+
throw new AgentRunError('pane-owned-by-another-adapter', `Pane ${candidate.paneId} is owned by another Agent adapter`);
|
|
250
|
+
}
|
|
251
|
+
const attached = this.#byAttachment.get(attachmentKey(agentId, candidate.attachmentId));
|
|
252
|
+
if (attached && attached.paneId !== candidate.paneId) {
|
|
253
|
+
throw new AgentRunError('attachment-already-attached', 'Agent attachment is already live on another pane');
|
|
254
|
+
}
|
|
255
|
+
if (!paneOwner) {
|
|
256
|
+
const created = this.#create(agentId, candidate);
|
|
257
|
+
this.#install(created);
|
|
258
|
+
return created.lease;
|
|
259
|
+
}
|
|
260
|
+
if (paneOwner.attachmentId === candidate.attachmentId
|
|
261
|
+
&& sameProcess(paneOwner.process, candidate.process)) {
|
|
262
|
+
const currentSession = paneOwner.ref.sessionId;
|
|
263
|
+
if (currentSession !== undefined && candidate.sessionId !== undefined
|
|
264
|
+
&& currentSession !== candidate.sessionId) {
|
|
265
|
+
throw new AgentRunError('session-replacement-required', 'A different session requires replace(), even when process and pane are unchanged');
|
|
266
|
+
}
|
|
267
|
+
if (currentSession === undefined && candidate.sessionId !== undefined) {
|
|
268
|
+
paneOwner.ref = refFor(agentId, candidate.paneId, paneOwner.ref.runId, candidate.sessionId);
|
|
269
|
+
this.#knownRunRefs.set(paneOwner.ref.runId, paneOwner.ref);
|
|
270
|
+
}
|
|
271
|
+
return paneOwner.lease;
|
|
272
|
+
}
|
|
273
|
+
const created = this.#create(agentId, candidate);
|
|
274
|
+
return this.#replaceInstalled(paneOwner, created, 'process_exit');
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async #associateSession(agentId, lease, sessionId) {
|
|
278
|
+
if (!validText(sessionId, 1024)) {
|
|
279
|
+
throw new AgentRunError('invalid-candidate', 'sessionId must be a non-empty bounded string');
|
|
280
|
+
}
|
|
281
|
+
const known = this.#recordFor(agentId, lease);
|
|
282
|
+
return this.#withPane(known.paneId, async () => {
|
|
283
|
+
const record = this.#recordFor(agentId, lease);
|
|
284
|
+
if (this.#byRunId.get(record.ref.runId) !== record || record.abort.signal.aborted) {
|
|
285
|
+
throw new AgentRunError('stale-lease', 'Cannot associate a session with a stale Agent run');
|
|
286
|
+
}
|
|
287
|
+
if (record.ref.sessionId === sessionId)
|
|
288
|
+
return record.ref;
|
|
289
|
+
if (record.ref.sessionId !== undefined) {
|
|
290
|
+
throw new AgentRunError('session-replacement-required', 'An associated session is immutable; use replace() for a new session');
|
|
291
|
+
}
|
|
292
|
+
record.ref = refFor(agentId, record.paneId, record.ref.runId, sessionId);
|
|
293
|
+
this.#knownRunRefs.set(record.ref.runId, record.ref);
|
|
294
|
+
return record.ref;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
async #replace(agentId, verify, current, candidate, reason) {
|
|
298
|
+
validateCandidate(candidate);
|
|
299
|
+
const known = this.#recordFor(agentId, current);
|
|
300
|
+
if (candidate.paneId !== known.paneId) {
|
|
301
|
+
throw new AgentRunError('pane-change-requires-attach', 'replace() cannot move an Agent attachment to another pane');
|
|
302
|
+
}
|
|
303
|
+
return this.#withPane(known.paneId, async () => {
|
|
304
|
+
const record = this.#recordFor(agentId, current);
|
|
305
|
+
if (this.#byRunId.get(record.ref.runId) !== record || record.abort.signal.aborted) {
|
|
306
|
+
throw new AgentRunError('stale-lease', 'Cannot replace a stale Agent run');
|
|
307
|
+
}
|
|
308
|
+
if (this.#shuttingDown || this.#disabledAdapters.has(agentId)) {
|
|
309
|
+
throw new AgentRunError('runtime-unavailable', 'Agent runtime or adapter is stopped');
|
|
310
|
+
}
|
|
311
|
+
await this.#verify(verify, candidate);
|
|
312
|
+
const attached = this.#byAttachment.get(attachmentKey(agentId, candidate.attachmentId));
|
|
313
|
+
if (attached && attached !== record) {
|
|
314
|
+
throw new AgentRunError('attachment-already-attached', 'Replacement attachment is already live');
|
|
315
|
+
}
|
|
316
|
+
const created = this.#create(agentId, candidate);
|
|
317
|
+
return this.#replaceInstalled(record, created, reason);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async #revoke(agentId, lease, reason) {
|
|
321
|
+
const known = this.#recordFor(agentId, lease);
|
|
322
|
+
await this.#withPane(known.paneId, async () => {
|
|
323
|
+
const record = this.#recordFor(agentId, lease);
|
|
324
|
+
if (this.#byRunId.get(record.ref.runId) === record)
|
|
325
|
+
this.#remove(record, reason);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|