@meistrari/agent-core 0.0.0 → 0.1.1
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 +36 -3
- package/bin/supervisor.ts +116 -0
- package/package.json +42 -3
- package/scripts/build-supervisor-executable.ts +32 -0
- package/src/agents/agent-error-serializer.ts +23 -0
- package/src/agents/agent-event-stream.ts +81 -0
- package/src/agents/agent-id.ts +17 -0
- package/src/agents/agent-operation.ts +11 -0
- package/src/agents/agent-provider.ts +37 -0
- package/src/agents/agent-run.ts +32 -0
- package/src/agents/agent-runtime-error.ts +161 -0
- package/src/agents/agent-session-events.ts +32 -0
- package/src/agents/agent-tool-runner.ts +83 -0
- package/src/agents/agent-tool.ts +111 -0
- package/src/agents/author-context.ts +36 -0
- package/src/agents/claude/claude-command-mapper.ts +234 -0
- package/src/agents/claude/claude-event-mapper.ts +748 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +468 -0
- package/src/agents/claude/claude-tool-mapper.ts +186 -0
- package/src/agents/claude/index.ts +1 -0
- package/src/agents/codex/codex-auth.ts +34 -0
- package/src/agents/codex/codex-command-mapper.ts +78 -0
- package/src/agents/codex/codex-event-mapper.ts +721 -0
- package/src/agents/codex/codex-json-rpc-client.ts +326 -0
- package/src/agents/codex/codex-protocol.ts +36 -0
- package/src/agents/codex/codex-provider.ts +1050 -0
- package/src/agents/codex/codex-run.ts +404 -0
- package/src/agents/codex/codex-skill-catalog.ts +158 -0
- package/src/agents/codex/codex-skill-roots.ts +19 -0
- package/src/agents/codex/codex-tool-mapper.ts +55 -0
- package/src/agents/codex/codex.errors.ts +68 -0
- package/src/agents/codex/generated/meta.gen.ts +606 -0
- package/src/agents/codex/generated/namespaces.gen.ts +311 -0
- package/src/agents/codex/generated/schema.gen.ts +34883 -0
- package/src/agents/codex/index.ts +2 -0
- package/src/agents/index.ts +14 -0
- package/src/agents/input-attachment-preparation.ts +86 -0
- package/src/agents/input-attachment.errors.ts +16 -0
- package/src/agents/instructions.ts +8 -0
- package/src/agents/materialized-input-attachment.ts +26 -0
- package/src/agents/message-id.ts +23 -0
- package/src/agents/normalize.ts +8 -0
- package/src/agents/sandbox-environment.ts +1 -0
- package/src/agents/tools/ping.tool.ts +13 -0
- package/src/agents/user-input-request.ts +470 -0
- package/src/protocol/agent-event.ts +2 -1
- package/src/protocol/agent-usage.ts +14 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +133 -0
- package/src/supervisor/config.ts +49 -0
- package/src/supervisor/control-authority-verifier.ts +135 -0
- package/src/supervisor/create-supervisor-runtime.ts +25 -0
- package/src/supervisor/errors.ts +24 -0
- package/src/supervisor/index.ts +34 -0
- package/src/supervisor/persistence/json.ts +21 -0
- package/src/supervisor/persistence/state-discovery.ts +56 -0
- package/src/supervisor/persistence/supervisor-store.ts +364 -0
- package/src/supervisor/ports/index.ts +109 -0
- package/src/supervisor/provider-factory.ts +37 -0
- package/src/supervisor/resident.ts +143 -0
- package/src/supervisor/rpc-client.ts +120 -0
- package/src/supervisor/runtime-handler.ts +309 -0
- package/src/supervisor/websocket-server.ts +435 -0
- package/src/supervisor-protocol/bootstrap.ts +1 -1
- package/src/template-onboarding.ts +47 -0
- package/src/testing/es256-test-keys.ts +73 -0
- package/src/testing/in-memory-runtime-control-plane.ts +206 -0
- package/src/testing/index.ts +6 -0
- package/src/testing/loopback-supervisor-connection.ts +71 -0
- package/src/testing/scripted-provider.ts +64 -0
- package/src/worker-runtime-client/command-pump.ts +127 -0
- package/src/worker-runtime-client/connection-attempt.ts +349 -0
- package/src/worker-runtime-client/control-authority-signer.ts +100 -0
- package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
- package/src/worker-runtime-client/frame-processor.ts +178 -0
- package/src/worker-runtime-client/index.ts +27 -0
- package/src/worker-runtime-client/lease-reconciler.ts +14 -0
- package/src/worker-runtime-client/ports.ts +139 -0
- package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
- package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
- package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +301 -0
- package/src/worker-runtime-client/token-crypto.ts +46 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import type { Server, ServerWebSocket } from 'bun'
|
|
2
|
+
import type { Buffer } from 'node:buffer'
|
|
3
|
+
import type { Logger } from '../logger'
|
|
4
|
+
import type { BootstrapBinder } from './bootstrap-binder'
|
|
5
|
+
import type { ResidentSupervisorConfig } from './config'
|
|
6
|
+
import type { VerifySupervisorControlAuthority } from './control-authority-verifier'
|
|
7
|
+
import type { RuntimeConnectionHandler } from './runtime-handler'
|
|
8
|
+
import {
|
|
9
|
+
supervisorBootstrapRejectionReceiptCloseCode,
|
|
10
|
+
supervisorBootstrapRejectionReceiptCloseReason,
|
|
11
|
+
} from '../supervisor-protocol/bootstrap-rejection-receipt'
|
|
12
|
+
import { sandboxControlAuthorityHeaderName } from '../supervisor-protocol/control-authority'
|
|
13
|
+
import { decodeControlPlaneFrame, encodeWireFrame } from '../supervisor-protocol/wire-codec'
|
|
14
|
+
|
|
15
|
+
const sessionHeader = 'x-coding-agent-session-id'
|
|
16
|
+
const sessionSandboxHeader = 'x-coding-agent-session-sandbox-id'
|
|
17
|
+
const runtimeConnectionAttemptHeader = 'x-coding-agent-runtime-connection-attempt-id'
|
|
18
|
+
const controlAuthorityHeader = sandboxControlAuthorityHeaderName.toLowerCase()
|
|
19
|
+
const generationHeader = 'x-coding-agent-connection-generation'
|
|
20
|
+
const eventAckHeader = 'x-coding-agent-event-ack'
|
|
21
|
+
const e2bSandboxHeader = 'e2b-sandbox-id'
|
|
22
|
+
const nativePingIntervalMs = 15_000
|
|
23
|
+
const idleTimeoutSeconds = 60
|
|
24
|
+
const maximumControlFrameBytes = 128 * 1024
|
|
25
|
+
const protocolErrorCloseCode = 1002
|
|
26
|
+
const policyViolationCloseCode = 1008
|
|
27
|
+
const internalErrorCloseCode = 1011
|
|
28
|
+
const terminalRejectionReceiptBudgetMs = 5_000
|
|
29
|
+
const listenerStopBudgetMs = 500
|
|
30
|
+
|
|
31
|
+
interface SocketData {
|
|
32
|
+
sessionId: string
|
|
33
|
+
runtimeConnectionAttemptId: string
|
|
34
|
+
generation: number
|
|
35
|
+
eventAckFloor: number
|
|
36
|
+
handler?: RuntimeConnectionHandler
|
|
37
|
+
runtimeAttached: boolean
|
|
38
|
+
messageChain: Promise<void>
|
|
39
|
+
terminalRejection?: {
|
|
40
|
+
terminate: () => void
|
|
41
|
+
timeout: ReturnType<typeof setTimeout>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SupervisorBootstrapStartupStageFact {
|
|
46
|
+
event: 'sandbox.startup-stage'
|
|
47
|
+
stage: 'bootstrap-persisted' | 'bootstrap-readiness-sent'
|
|
48
|
+
outcome: 'success' | 'failure'
|
|
49
|
+
duration: number
|
|
50
|
+
sessionId: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Dependencies {
|
|
54
|
+
config: ResidentSupervisorConfig
|
|
55
|
+
logger: Logger
|
|
56
|
+
health: () => unknown
|
|
57
|
+
getBoundSessionId: () => string | undefined
|
|
58
|
+
getLocalEventMax: () => number
|
|
59
|
+
verifyControlAuthority: VerifySupervisorControlAuthority
|
|
60
|
+
acceptBoundConnectionGeneration: (input: { generation: number, runtimeConnectionAttemptId: string }) => void
|
|
61
|
+
bindBootstrap: BootstrapBinder
|
|
62
|
+
onStartupStage: (fact: SupervisorBootstrapStartupStageFact) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ResidentSupervisorWebSocketServer {
|
|
66
|
+
private readonly sockets = new Set<ServerWebSocket<SocketData>>()
|
|
67
|
+
private server: Server<SocketData> | undefined
|
|
68
|
+
private pingTimer: ReturnType<typeof setInterval> | undefined
|
|
69
|
+
private localSandboxId: string | undefined
|
|
70
|
+
private localSandboxIdRead: Promise<string> | undefined
|
|
71
|
+
private authoritativeSocket: ServerWebSocket<SocketData> | undefined
|
|
72
|
+
|
|
73
|
+
constructor(private readonly dependencies: Dependencies) {}
|
|
74
|
+
|
|
75
|
+
start(): void {
|
|
76
|
+
if (this.server)
|
|
77
|
+
return
|
|
78
|
+
const { config } = this.dependencies
|
|
79
|
+
const websocket: Bun.WebSocketHandler<SocketData> = {
|
|
80
|
+
perMessageDeflate: false,
|
|
81
|
+
maxPayloadLength: maximumControlFrameBytes,
|
|
82
|
+
idleTimeout: idleTimeoutSeconds,
|
|
83
|
+
open: socket => this.onOpen(socket),
|
|
84
|
+
message: (socket, message) => this.onMessage(socket, message),
|
|
85
|
+
close: (socket, code, reason) => this.onClose(socket, code, reason),
|
|
86
|
+
drain: socket => this.onDrain(socket),
|
|
87
|
+
}
|
|
88
|
+
if (config.unixSocketPath) {
|
|
89
|
+
this.server = Bun.serve<SocketData>({
|
|
90
|
+
unix: config.unixSocketPath,
|
|
91
|
+
fetch: async (request, server) => await this.handleRequest(request, server),
|
|
92
|
+
websocket,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this.server = Bun.serve<SocketData>({
|
|
97
|
+
hostname: config.host,
|
|
98
|
+
port: config.port,
|
|
99
|
+
idleTimeout: idleTimeoutSeconds,
|
|
100
|
+
fetch: async (request, server) => await this.handleRequest(request, server),
|
|
101
|
+
websocket,
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
this.pingTimer = setInterval(() => {
|
|
105
|
+
for (const socket of this.sockets)
|
|
106
|
+
socket.ping()
|
|
107
|
+
}, nativePingIntervalMs)
|
|
108
|
+
this.pingTimer.unref?.()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
port(): number | undefined {
|
|
112
|
+
return this.server?.port
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async stop(): Promise<void> {
|
|
116
|
+
if (this.pingTimer)
|
|
117
|
+
clearInterval(this.pingTimer)
|
|
118
|
+
this.pingTimer = undefined
|
|
119
|
+
for (const socket of this.sockets) {
|
|
120
|
+
this.cancelPendingTerminalRejection(socket)
|
|
121
|
+
socket.close(1001, 'Supervisor listener stopping.')
|
|
122
|
+
socket.terminate()
|
|
123
|
+
}
|
|
124
|
+
this.sockets.clear()
|
|
125
|
+
this.authoritativeSocket = undefined
|
|
126
|
+
const server = this.server
|
|
127
|
+
this.server = undefined
|
|
128
|
+
if (server) {
|
|
129
|
+
const stopping = server.stop(true)
|
|
130
|
+
const completed = await Promise.race([
|
|
131
|
+
stopping.then(() => true),
|
|
132
|
+
Bun.sleep(listenerStopBudgetMs).then(() => false),
|
|
133
|
+
])
|
|
134
|
+
if (!completed) {
|
|
135
|
+
this.dependencies.logger.warn({}, 'supervisor listener stop exceeded its drain budget')
|
|
136
|
+
void stopping.catch(error => this.dependencies.logger.warn({ err: error }, 'supervisor listener stopped with an error'))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async handleRequest(request: Request, server: Server<SocketData>): Promise<Response | undefined> {
|
|
142
|
+
const url = new URL(request.url)
|
|
143
|
+
if (request.method === 'GET' && url.pathname === this.dependencies.config.healthPath) {
|
|
144
|
+
return Response.json(this.dependencies.health())
|
|
145
|
+
}
|
|
146
|
+
if (request.method !== 'GET' || url.pathname !== this.dependencies.config.controlPath)
|
|
147
|
+
return new Response('Not found', { status: 404 })
|
|
148
|
+
|
|
149
|
+
const sessionId = request.headers.get(sessionHeader)
|
|
150
|
+
const sessionSandboxId = request.headers.get(sessionSandboxHeader)
|
|
151
|
+
const runtimeConnectionAttemptId = request.headers.get(runtimeConnectionAttemptHeader)
|
|
152
|
+
const authorityAssertion = request.headers.get(controlAuthorityHeader)
|
|
153
|
+
const sandboxId = request.headers.get(e2bSandboxHeader)
|
|
154
|
+
const generation = parseBoundedInteger(request.headers.get(generationHeader), 1)
|
|
155
|
+
const eventAckFloor = parseBoundedInteger(request.headers.get(eventAckHeader), 0)
|
|
156
|
+
const boundSessionId = this.dependencies.getBoundSessionId()
|
|
157
|
+
if (!sandboxId || !sessionId || !sessionSandboxId || !runtimeConnectionAttemptId
|
|
158
|
+
|| generation === undefined || eventAckFloor === undefined) {
|
|
159
|
+
return new Response('Invalid supervisor routing headers.', { status: 400 })
|
|
160
|
+
}
|
|
161
|
+
let localSandboxId: string
|
|
162
|
+
try {
|
|
163
|
+
localSandboxId = await this.getLocalSandboxId()
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.dependencies.logger.error({ err: error }, 'failed to read local E2B sandbox identity')
|
|
167
|
+
return new Response('Local sandbox identity is unavailable.', { status: 503 })
|
|
168
|
+
}
|
|
169
|
+
if (sandboxId !== localSandboxId)
|
|
170
|
+
return new Response('E2B sandbox identity mismatch.', { status: 409 })
|
|
171
|
+
try {
|
|
172
|
+
await this.dependencies.verifyControlAuthority({
|
|
173
|
+
assertion: authorityAssertion,
|
|
174
|
+
providerSandboxId: sandboxId,
|
|
175
|
+
sessionId,
|
|
176
|
+
sessionSandboxId,
|
|
177
|
+
connectionGeneration: generation,
|
|
178
|
+
runtimeConnectionAttemptId,
|
|
179
|
+
eventAckFloor,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return new Response('Supervisor control authority is invalid.', { status: 401 })
|
|
184
|
+
}
|
|
185
|
+
if (boundSessionId && sessionId !== boundSessionId)
|
|
186
|
+
return new Response('Supervisor session binding mismatch.', { status: 409 })
|
|
187
|
+
if (eventAckFloor > this.dependencies.getLocalEventMax())
|
|
188
|
+
return new Response('Supervisor event ACK floor is ahead of local state.', { status: 409 })
|
|
189
|
+
if (boundSessionId) {
|
|
190
|
+
try {
|
|
191
|
+
this.dependencies.acceptBoundConnectionGeneration({ generation, runtimeConnectionAttemptId })
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
this.dependencies.logger.warn({ err: error }, 'rejecting supervisor connection generation')
|
|
195
|
+
return new Response('Supervisor connection generation is stale.', { status: 409 })
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const upgraded = server.upgrade(request, {
|
|
200
|
+
data: {
|
|
201
|
+
sessionId,
|
|
202
|
+
runtimeConnectionAttemptId,
|
|
203
|
+
generation,
|
|
204
|
+
eventAckFloor,
|
|
205
|
+
runtimeAttached: false,
|
|
206
|
+
messageChain: Promise.resolve(),
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
return upgraded ? undefined : new Response('WebSocket upgrade failed.', { status: 400 })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async getLocalSandboxId(): Promise<string> {
|
|
213
|
+
if (this.localSandboxId)
|
|
214
|
+
return this.localSandboxId
|
|
215
|
+
|
|
216
|
+
const read = this.localSandboxIdRead ?? readLocalSandboxId(this.dependencies.config.sandboxIdPath)
|
|
217
|
+
this.localSandboxIdRead = read
|
|
218
|
+
try {
|
|
219
|
+
const sandboxId = await read
|
|
220
|
+
this.localSandboxId = sandboxId
|
|
221
|
+
return sandboxId
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
if (this.localSandboxIdRead === read)
|
|
225
|
+
this.localSandboxIdRead = undefined
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private onOpen(socket: ServerWebSocket<SocketData>): void {
|
|
230
|
+
this.sockets.add(socket)
|
|
231
|
+
const previous = this.authoritativeSocket
|
|
232
|
+
this.authoritativeSocket = socket
|
|
233
|
+
if (previous && previous !== socket) {
|
|
234
|
+
this.cancelPendingTerminalRejection(previous)
|
|
235
|
+
this.sockets.delete(previous)
|
|
236
|
+
if (previous.data.runtimeAttached) {
|
|
237
|
+
previous.data.runtimeAttached = false
|
|
238
|
+
previous.data.handler?.detach(previous)
|
|
239
|
+
}
|
|
240
|
+
previous.close(policyViolationCloseCode, 'Connection attempt was superseded.')
|
|
241
|
+
previous.terminate()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private onMessage(socket: ServerWebSocket<SocketData>, message: string | Buffer): void {
|
|
246
|
+
socket.data.messageChain = socket.data.messageChain.then(async () => {
|
|
247
|
+
const bootstrapHandlingStartedAt = performance.now()
|
|
248
|
+
if (!this.isAuthoritativeLive(socket) || socket.data.terminalRejection)
|
|
249
|
+
return
|
|
250
|
+
const decoded = decodeControlPlaneFrame(typeof message === 'string' ? message : message.toString('utf8'))
|
|
251
|
+
const handler = socket.data.handler
|
|
252
|
+
if (handler && (decoded.kind !== 'ok' || decoded.frame.kind !== 'session.bootstrap')) {
|
|
253
|
+
if (socket.data.runtimeAttached)
|
|
254
|
+
handler.receive(socket, message)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (decoded.kind !== 'ok' || decoded.frame.kind !== 'session.bootstrap') {
|
|
258
|
+
socket.close(protocolErrorCloseCode, 'Expected session bootstrap as first frame.')
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
if (decoded.frame.body.sessionId !== socket.data.sessionId
|
|
262
|
+
|| decoded.frame.connectionGeneration !== socket.data.generation) {
|
|
263
|
+
socket.close(policyViolationCloseCode, 'Bootstrap does not match upgrade headers.')
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let binding: Awaited<ReturnType<BootstrapBinder>>
|
|
268
|
+
try {
|
|
269
|
+
binding = await this.dependencies.bindBootstrap({
|
|
270
|
+
envelope: decoded.frame,
|
|
271
|
+
runtimeConnectionAttemptId: socket.data.runtimeConnectionAttemptId,
|
|
272
|
+
})
|
|
273
|
+
this.observeBootstrapStage({
|
|
274
|
+
stage: 'bootstrap-persisted',
|
|
275
|
+
outcome: binding.outcome === 'accepted' ? 'success' : 'failure',
|
|
276
|
+
startedAt: bootstrapHandlingStartedAt,
|
|
277
|
+
sessionId: socket.data.sessionId,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
this.observeBootstrapStage({
|
|
282
|
+
stage: 'bootstrap-persisted',
|
|
283
|
+
outcome: 'failure',
|
|
284
|
+
startedAt: bootstrapHandlingStartedAt,
|
|
285
|
+
sessionId: socket.data.sessionId,
|
|
286
|
+
})
|
|
287
|
+
throw error
|
|
288
|
+
}
|
|
289
|
+
if (!this.isAuthoritativeLive(socket)) {
|
|
290
|
+
this.observeBootstrapStage({
|
|
291
|
+
stage: 'bootstrap-readiness-sent',
|
|
292
|
+
outcome: 'failure',
|
|
293
|
+
startedAt: bootstrapHandlingStartedAt,
|
|
294
|
+
sessionId: socket.data.sessionId,
|
|
295
|
+
})
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
const readiness = binding.outcome === 'accepted' ? binding.runtimeState : binding.ack
|
|
299
|
+
let sendResult: number
|
|
300
|
+
try {
|
|
301
|
+
sendResult = socket.send(encodeWireFrame(readiness))
|
|
302
|
+
this.observeBootstrapStage({
|
|
303
|
+
stage: 'bootstrap-readiness-sent',
|
|
304
|
+
outcome: binding.outcome === 'accepted' && sendResult >= 0 ? 'success' : 'failure',
|
|
305
|
+
startedAt: bootstrapHandlingStartedAt,
|
|
306
|
+
sessionId: socket.data.sessionId,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
this.observeBootstrapStage({
|
|
311
|
+
stage: 'bootstrap-readiness-sent',
|
|
312
|
+
outcome: 'failure',
|
|
313
|
+
startedAt: bootstrapHandlingStartedAt,
|
|
314
|
+
sessionId: socket.data.sessionId,
|
|
315
|
+
})
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
if (sendResult < 0) {
|
|
319
|
+
socket.close(internalErrorCloseCode, binding.outcome === 'accepted'
|
|
320
|
+
? 'Supervisor bootstrap readiness delivery failed.'
|
|
321
|
+
: 'Supervisor bootstrap ACK delivery failed.')
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
if (binding.outcome === 'rejected') {
|
|
325
|
+
this.awaitTerminalRejectionReceipt({ socket, terminate: binding.terminate })
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
const activated = await binding.activate()
|
|
329
|
+
this.attachIfLive({ socket, handler: activated })
|
|
330
|
+
}).catch((error: unknown) => {
|
|
331
|
+
this.dependencies.logger.error({ err: error }, 'resident supervisor connection failed')
|
|
332
|
+
socket.close(internalErrorCloseCode, 'Supervisor bootstrap failed.')
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private observeBootstrapStage(input: {
|
|
337
|
+
stage: SupervisorBootstrapStartupStageFact['stage']
|
|
338
|
+
outcome: SupervisorBootstrapStartupStageFact['outcome']
|
|
339
|
+
startedAt: number
|
|
340
|
+
sessionId: string
|
|
341
|
+
}): void {
|
|
342
|
+
try {
|
|
343
|
+
this.dependencies.onStartupStage({
|
|
344
|
+
event: 'sandbox.startup-stage',
|
|
345
|
+
stage: input.stage,
|
|
346
|
+
outcome: input.outcome,
|
|
347
|
+
duration: Math.max(0, Math.round(performance.now() - input.startedAt)),
|
|
348
|
+
sessionId: input.sessionId,
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Startup telemetry is best effort and cannot alter bootstrap behavior.
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private onClose(socket: ServerWebSocket<SocketData>, code: number, reason: string): void {
|
|
357
|
+
const terminalRejection = socket.data.terminalRejection
|
|
358
|
+
const receiptConfirmed = terminalRejection !== undefined
|
|
359
|
+
&& this.authoritativeSocket === socket
|
|
360
|
+
&& code === supervisorBootstrapRejectionReceiptCloseCode
|
|
361
|
+
&& reason === supervisorBootstrapRejectionReceiptCloseReason
|
|
362
|
+
if (terminalRejection) {
|
|
363
|
+
clearTimeout(terminalRejection.timeout)
|
|
364
|
+
socket.data.terminalRejection = undefined
|
|
365
|
+
}
|
|
366
|
+
this.sockets.delete(socket)
|
|
367
|
+
if (this.authoritativeSocket === socket)
|
|
368
|
+
this.authoritativeSocket = undefined
|
|
369
|
+
if (socket.data.runtimeAttached)
|
|
370
|
+
socket.data.handler?.detach(socket)
|
|
371
|
+
if (receiptConfirmed)
|
|
372
|
+
terminalRejection?.terminate()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private awaitTerminalRejectionReceipt(input: {
|
|
376
|
+
socket: ServerWebSocket<SocketData>
|
|
377
|
+
terminate: () => void
|
|
378
|
+
}): void {
|
|
379
|
+
const timeout = setTimeout(() => {
|
|
380
|
+
if (input.socket.data.terminalRejection?.terminate !== input.terminate)
|
|
381
|
+
return
|
|
382
|
+
input.socket.data.terminalRejection = undefined
|
|
383
|
+
input.socket.close(internalErrorCloseCode, 'Supervisor rejection receipt was not observed.')
|
|
384
|
+
input.terminate()
|
|
385
|
+
}, terminalRejectionReceiptBudgetMs)
|
|
386
|
+
timeout.unref?.()
|
|
387
|
+
input.socket.data.terminalRejection = { terminate: input.terminate, timeout }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private cancelPendingTerminalRejection(socket: ServerWebSocket<SocketData>): void {
|
|
391
|
+
const pending = socket.data.terminalRejection
|
|
392
|
+
if (!pending)
|
|
393
|
+
return
|
|
394
|
+
clearTimeout(pending.timeout)
|
|
395
|
+
socket.data.terminalRejection = undefined
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private onDrain(socket: ServerWebSocket<SocketData>): void {
|
|
399
|
+
if (socket.data.runtimeAttached && this.isAuthoritativeLive(socket))
|
|
400
|
+
socket.data.handler?.drain(socket)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private attachIfLive(input: {
|
|
404
|
+
socket: ServerWebSocket<SocketData>
|
|
405
|
+
handler: RuntimeConnectionHandler
|
|
406
|
+
}): void {
|
|
407
|
+
if (!this.isAuthoritativeLive(input.socket))
|
|
408
|
+
return
|
|
409
|
+
input.socket.data.handler = input.handler
|
|
410
|
+
input.handler.attach({
|
|
411
|
+
socket: input.socket,
|
|
412
|
+
generation: input.socket.data.generation,
|
|
413
|
+
eventAckFloor: input.socket.data.eventAckFloor,
|
|
414
|
+
})
|
|
415
|
+
input.socket.data.runtimeAttached = true
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private isAuthoritativeLive(socket: ServerWebSocket<SocketData>): boolean {
|
|
419
|
+
return this.sockets.has(socket) && this.authoritativeSocket === socket
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseBoundedInteger(value: string | null, minimum: number): number | undefined {
|
|
424
|
+
if (value === null || !/^\d+$/u.test(value))
|
|
425
|
+
return undefined
|
|
426
|
+
const parsed = Number(value)
|
|
427
|
+
return Number.isSafeInteger(parsed) && parsed >= minimum ? parsed : undefined
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function readLocalSandboxId(path: string): Promise<string> {
|
|
431
|
+
const sandboxId = (await Bun.file(path).text()).trim()
|
|
432
|
+
if (!sandboxId)
|
|
433
|
+
throw new Error('E2B sandbox identity file is empty.')
|
|
434
|
+
return sandboxId
|
|
435
|
+
}
|
|
@@ -67,7 +67,7 @@ export const sessionBootstrapBodySchema = z.object({
|
|
|
67
67
|
}).strict().superRefine((body, context) => {
|
|
68
68
|
const workspacePrefix = `${body.agent.cwd}/`
|
|
69
69
|
for (const [index, repository] of body.repositories.entries()) {
|
|
70
|
-
if (!repository.root.startsWith(workspacePrefix)) {
|
|
70
|
+
if (repository.root !== body.agent.cwd && !repository.root.startsWith(workspacePrefix)) {
|
|
71
71
|
context.addIssue({
|
|
72
72
|
code: 'custom',
|
|
73
73
|
path: ['repositories', index, 'root'],
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import z from 'zod'
|
|
2
|
+
import { sessionProfileNameSchema } from './supervisor-protocol/profile'
|
|
3
|
+
|
|
4
|
+
const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/u)
|
|
5
|
+
const componentNameSchema = z.string().max(64).regex(/^[a-z][a-z0-9.-]*$/u)
|
|
6
|
+
const componentVersionSchema = z.string().min(1).max(256)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Immutable provenance supplied when a service onboards one of its E2B
|
|
10
|
+
* templates. The owning service persists this record; agent-core deliberately
|
|
11
|
+
* has no global template registry or preferred template ID.
|
|
12
|
+
*/
|
|
13
|
+
export const sandboxTemplateOnboardingSchema = z.object({
|
|
14
|
+
owner: z.object({
|
|
15
|
+
service: sessionProfileNameSchema,
|
|
16
|
+
profileVersion: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
17
|
+
}).strict(),
|
|
18
|
+
provider: z.literal('e2b'),
|
|
19
|
+
templateId: z.string().min(1).max(512),
|
|
20
|
+
agentCore: z.object({
|
|
21
|
+
version: z.string().min(1).max(256),
|
|
22
|
+
sha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
23
|
+
}).strict(),
|
|
24
|
+
supervisorEntrySha256: sha256Schema,
|
|
25
|
+
runtimeHash: sha256Schema,
|
|
26
|
+
components: z.record(componentNameSchema, componentVersionSchema).refine(
|
|
27
|
+
components => Object.keys(components).length <= 64,
|
|
28
|
+
{ message: 'A template may declare at most 64 versioned components.' },
|
|
29
|
+
),
|
|
30
|
+
onboardedAt: z.iso.datetime(),
|
|
31
|
+
}).strict()
|
|
32
|
+
|
|
33
|
+
export type SandboxTemplateOnboarding = z.infer<typeof sandboxTemplateOnboardingSchema>
|
|
34
|
+
|
|
35
|
+
export interface SandboxTemplateOnboardingStore {
|
|
36
|
+
upsert: (registration: SandboxTemplateOnboarding) => Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Validates locally, then delegates persistence to the service-owned store. */
|
|
40
|
+
export async function onboardSandboxTemplate(input: {
|
|
41
|
+
registration: unknown
|
|
42
|
+
store: SandboxTemplateOnboardingStore
|
|
43
|
+
}): Promise<SandboxTemplateOnboarding> {
|
|
44
|
+
const registration = sandboxTemplateOnboardingSchema.parse(input.registration)
|
|
45
|
+
await input.store.upsert(registration)
|
|
46
|
+
return registration
|
|
47
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { SandboxControlAuthorityPublicJwks } from '../supervisor-protocol/control-authority'
|
|
2
|
+
import { sandboxControlAuthorityPublicJwksSchema } from '../supervisor-protocol/control-authority'
|
|
3
|
+
import { createControlAuthoritySigner } from '../worker-runtime-client/control-authority-signer'
|
|
4
|
+
|
|
5
|
+
export interface TestControlAuthority {
|
|
6
|
+
issuer: string
|
|
7
|
+
keyId: string
|
|
8
|
+
signingJwk: Record<string, unknown>
|
|
9
|
+
publicJwks: SandboxControlAuthorityPublicJwks
|
|
10
|
+
sign: Awaited<ReturnType<typeof createControlAuthoritySigner>>['sign']
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function createTestControlAuthority(input: {
|
|
14
|
+
issuer?: string
|
|
15
|
+
keyId?: string
|
|
16
|
+
} = {}): Promise<TestControlAuthority> {
|
|
17
|
+
const issuer = input.issuer ?? 'agent-core-loopback'
|
|
18
|
+
const keyId = input.keyId ?? 'loopback-key'
|
|
19
|
+
const pair = await crypto.subtle.generateKey(
|
|
20
|
+
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
21
|
+
true,
|
|
22
|
+
['sign', 'verify'],
|
|
23
|
+
)
|
|
24
|
+
const [privateMaterial, publicMaterial] = await Promise.all([
|
|
25
|
+
crypto.subtle.exportKey('jwk', pair.privateKey),
|
|
26
|
+
crypto.subtle.exportKey('jwk', pair.publicKey),
|
|
27
|
+
])
|
|
28
|
+
const { x, y, d } = requiredEcPrivateMaterial(privateMaterial)
|
|
29
|
+
const signingJwk: Record<string, unknown> = {
|
|
30
|
+
kty: 'EC',
|
|
31
|
+
crv: 'P-256',
|
|
32
|
+
x,
|
|
33
|
+
y,
|
|
34
|
+
d,
|
|
35
|
+
kid: keyId,
|
|
36
|
+
alg: 'ES256',
|
|
37
|
+
use: 'sig',
|
|
38
|
+
key_ops: ['sign'],
|
|
39
|
+
}
|
|
40
|
+
const publicJwks = sandboxControlAuthorityPublicJwksSchema.parse({
|
|
41
|
+
keys: [{
|
|
42
|
+
kty: 'EC',
|
|
43
|
+
crv: 'P-256',
|
|
44
|
+
x: requiredCoordinate(publicMaterial.x, 'x'),
|
|
45
|
+
y: requiredCoordinate(publicMaterial.y, 'y'),
|
|
46
|
+
kid: keyId,
|
|
47
|
+
alg: 'ES256' as const,
|
|
48
|
+
use: 'sig' as const,
|
|
49
|
+
key_ops: ['verify'],
|
|
50
|
+
}],
|
|
51
|
+
})
|
|
52
|
+
const signer = await createControlAuthoritySigner({
|
|
53
|
+
issuer,
|
|
54
|
+
activeKeyId: keyId,
|
|
55
|
+
signingJwk,
|
|
56
|
+
publicJwks,
|
|
57
|
+
})
|
|
58
|
+
return { issuer, keyId, signingJwk, publicJwks: signer.publicJwks, sign: signer.sign }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function requiredEcPrivateMaterial(jwk: { x?: string, y?: string, d?: string }): { x: string, y: string, d: string } {
|
|
62
|
+
return {
|
|
63
|
+
x: requiredCoordinate(jwk.x, 'x'),
|
|
64
|
+
y: requiredCoordinate(jwk.y, 'y'),
|
|
65
|
+
d: requiredCoordinate(jwk.d, 'd'),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requiredCoordinate(value: string | undefined, name: string): string {
|
|
70
|
+
if (!value)
|
|
71
|
+
throw new Error(`Generated EC test key is missing ${name}.`)
|
|
72
|
+
return value
|
|
73
|
+
}
|