@meistrari/agent-core 0.0.0 → 0.1.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 +24 -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 +736 -0
- package/src/agents/claude/claude-provider.ts +191 -0
- package/src/agents/claude/claude-run.ts +464 -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 +708 -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/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +189 -0
- package/src/supervisor/bootstrap-binder.ts +125 -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 +434 -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 +205 -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 +132 -0
- package/src/worker-runtime-client/connection-attempt.ts +340 -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 +137 -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 +300 -0
- package/src/worker-runtime-client/token-crypto.ts +46 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import type { Logger } from '../logger'
|
|
2
|
+
import type { CommandAckEnvelope } from '../supervisor-protocol/envelopes/supervisor-to-control-plane'
|
|
3
|
+
import type {
|
|
4
|
+
BootstrapProvider,
|
|
5
|
+
CommandSource,
|
|
6
|
+
ConnectionAuthorityStore,
|
|
7
|
+
ConnectionClaim,
|
|
8
|
+
ControlAuthoritySigner,
|
|
9
|
+
DurableEventSink,
|
|
10
|
+
EphemeralEventSink,
|
|
11
|
+
RpcDispatcher,
|
|
12
|
+
SandboxConnectionRuntimeTimings,
|
|
13
|
+
SessionFailureSink,
|
|
14
|
+
SupervisorConnection,
|
|
15
|
+
SupervisorConnectionOpener,
|
|
16
|
+
} from './ports'
|
|
17
|
+
import { randomUUID } from 'node:crypto'
|
|
18
|
+
import { wireCommandBodySchema } from '../supervisor-protocol/command-body'
|
|
19
|
+
import { encodeWireFrame } from '../supervisor-protocol/wire-codec'
|
|
20
|
+
import { SessionCommandPump } from './command-pump'
|
|
21
|
+
import { BootstrapRejectedAckError, SupervisorFrameProcessor } from './frame-processor'
|
|
22
|
+
import { decryptToken } from './token-crypto'
|
|
23
|
+
|
|
24
|
+
export class SessionSandboxConnectionAttempt {
|
|
25
|
+
private readonly controller = new AbortController()
|
|
26
|
+
private commandPump: SessionCommandPump | undefined
|
|
27
|
+
private processor: SupervisorFrameProcessor | undefined
|
|
28
|
+
private connection: SupervisorConnection | undefined
|
|
29
|
+
private ready = false
|
|
30
|
+
private readonly parentAbort: () => void
|
|
31
|
+
|
|
32
|
+
constructor(private readonly dependencies: {
|
|
33
|
+
claim: ConnectionClaim
|
|
34
|
+
replicaId: string
|
|
35
|
+
trafficTokenEncryptionKey: string
|
|
36
|
+
signControlAuthority: ControlAuthoritySigner
|
|
37
|
+
openSupervisorConnection: SupervisorConnectionOpener
|
|
38
|
+
authorityStore: ConnectionAuthorityStore
|
|
39
|
+
commands: CommandSource
|
|
40
|
+
durableEvents: DurableEventSink
|
|
41
|
+
ephemeralEvents: EphemeralEventSink
|
|
42
|
+
rpc: RpcDispatcher
|
|
43
|
+
bootstrap: BootstrapProvider
|
|
44
|
+
failures: SessionFailureSink
|
|
45
|
+
timings: SandboxConnectionRuntimeTimings
|
|
46
|
+
logger: Logger
|
|
47
|
+
parentSignal: AbortSignal
|
|
48
|
+
}) {
|
|
49
|
+
this.parentAbort = () => this.stop(dependencies.parentSignal.reason)
|
|
50
|
+
dependencies.parentSignal.addEventListener('abort', this.parentAbort, { once: true })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async run(): Promise<never> {
|
|
54
|
+
const { claim } = this.dependencies
|
|
55
|
+
const renewalFailure = Promise.withResolvers<unknown>()
|
|
56
|
+
void this.renewLease().catch((error) => {
|
|
57
|
+
renewalFailure.resolve(error)
|
|
58
|
+
this.stop(error)
|
|
59
|
+
})
|
|
60
|
+
const runtimeConnectionAttemptId = randomUUID()
|
|
61
|
+
const eventAckFloor = await this.dependencies.durableEvents.getAckFloor({
|
|
62
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
63
|
+
})
|
|
64
|
+
const initialCommandPromise = this.dependencies.commands.initial({ sessionId: claim.sessionId })
|
|
65
|
+
const bootstrapBodyPromise = this.dependencies.bootstrap.loadBootstrapBody({ sessionId: claim.sessionId })
|
|
66
|
+
const secretsPromise = this.dependencies.bootstrap.issueEphemeralSecrets({
|
|
67
|
+
sessionId: claim.sessionId,
|
|
68
|
+
eventAckFloor,
|
|
69
|
+
signal: this.controller.signal,
|
|
70
|
+
})
|
|
71
|
+
const authorityAssertionPromise = this.dependencies.signControlAuthority({
|
|
72
|
+
providerSandboxId: claim.providerSandboxId,
|
|
73
|
+
sessionId: claim.sessionId,
|
|
74
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
75
|
+
connectionGeneration: claim.runtimeConnectionGeneration,
|
|
76
|
+
runtimeConnectionAttemptId,
|
|
77
|
+
eventAckFloor,
|
|
78
|
+
})
|
|
79
|
+
const trafficAccessToken = decryptToken({
|
|
80
|
+
encryptedToken: claim.trafficAccessTokenEncrypted,
|
|
81
|
+
secret: this.dependencies.trafficTokenEncryptionKey,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const queuedFrames: Array<string | ArrayBuffer | Uint8Array> = []
|
|
85
|
+
const closed = Promise.withResolvers<{ code: number, reason: string }>()
|
|
86
|
+
const authorityAssertion = await authorityAssertionPromise
|
|
87
|
+
const connection = await beforeDeadline(this.dependencies.openSupervisorConnection({
|
|
88
|
+
providerSandboxId: claim.providerSandboxId,
|
|
89
|
+
trafficAccessToken,
|
|
90
|
+
sessionId: claim.sessionId,
|
|
91
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
92
|
+
runtimeConnectionAttemptId,
|
|
93
|
+
authorityAssertion,
|
|
94
|
+
connectionGeneration: claim.runtimeConnectionGeneration,
|
|
95
|
+
eventAckFloor,
|
|
96
|
+
signal: this.controller.signal,
|
|
97
|
+
onMessage: frame => this.processor ? this.processor.receive(frame) : queuedFrames.push(frame),
|
|
98
|
+
onClose: close => closed.resolve(close),
|
|
99
|
+
}), performance.now() + this.dependencies.timings.dialTimeoutMs, this.controller.signal, () => new ConnectionClosedError({
|
|
100
|
+
ready: false,
|
|
101
|
+
detail: 'Supervisor connection dial timed out.',
|
|
102
|
+
}))
|
|
103
|
+
this.connection = connection
|
|
104
|
+
let bootstrapAck: CommandAckEnvelope | undefined
|
|
105
|
+
const processor = new SupervisorFrameProcessor({
|
|
106
|
+
ref: {
|
|
107
|
+
sessionId: claim.sessionId,
|
|
108
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
109
|
+
providerSandboxId: claim.providerSandboxId,
|
|
110
|
+
},
|
|
111
|
+
generation: claim.runtimeConnectionGeneration,
|
|
112
|
+
runtimeConnectionId: claim.runtimeConnectionId,
|
|
113
|
+
runtimeConnectionAttemptId,
|
|
114
|
+
connection,
|
|
115
|
+
authorityStore: this.dependencies.authorityStore,
|
|
116
|
+
durableEvents: this.dependencies.durableEvents,
|
|
117
|
+
ephemeralEvents: this.dependencies.ephemeralEvents,
|
|
118
|
+
rpcDispatcher: this.dependencies.rpc,
|
|
119
|
+
failures: this.dependencies.failures,
|
|
120
|
+
logger: this.dependencies.logger,
|
|
121
|
+
onCommandAck: (ack) => {
|
|
122
|
+
if (this.commandPump)
|
|
123
|
+
this.commandPump.acknowledge(ack)
|
|
124
|
+
else
|
|
125
|
+
bootstrapAck = ack
|
|
126
|
+
},
|
|
127
|
+
onFatal: error => this.stop(error),
|
|
128
|
+
})
|
|
129
|
+
this.processor = processor
|
|
130
|
+
for (const frame of queuedFrames)
|
|
131
|
+
processor.receive(frame)
|
|
132
|
+
|
|
133
|
+
const [initialCommand, body, secrets] = await Promise.all([
|
|
134
|
+
initialCommandPromise,
|
|
135
|
+
bootstrapBodyPromise,
|
|
136
|
+
secretsPromise,
|
|
137
|
+
])
|
|
138
|
+
if (!initialCommand || initialCommand.commandSeq !== 1)
|
|
139
|
+
throw new DeterministicEstablishmentError('agent-core.initial-command-invalid', 'Session command 1 is missing or invalid.')
|
|
140
|
+
const parsedInitialBody = wireCommandBodySchema.safeParse(initialCommand.body)
|
|
141
|
+
if (!parsedInitialBody.success) {
|
|
142
|
+
throw new DeterministicEstablishmentError(
|
|
143
|
+
'agent-core.initial-command-invalid',
|
|
144
|
+
'Session command 1 has an invalid body.',
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
const initialBody = parsedInitialBody.data
|
|
148
|
+
await connection.send(encodeWireFrame({
|
|
149
|
+
kind: 'session.bootstrap',
|
|
150
|
+
connectionGeneration: claim.runtimeConnectionGeneration,
|
|
151
|
+
body: {
|
|
152
|
+
...body,
|
|
153
|
+
sessionId: claim.sessionId,
|
|
154
|
+
initialCommand: {
|
|
155
|
+
commandId: initialCommand.commandId,
|
|
156
|
+
commandSeq: initialCommand.commandSeq,
|
|
157
|
+
body: initialBody,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
...secrets,
|
|
161
|
+
}))
|
|
162
|
+
|
|
163
|
+
const readiness = await processor.waitForReadiness({ signal: this.controller.signal }).catch(async (error: unknown) => {
|
|
164
|
+
if (error instanceof BootstrapRejectedAckError) {
|
|
165
|
+
await processor.acknowledgeBootstrapRejection(error.ack)
|
|
166
|
+
throw new DeterministicEstablishmentError(error.ack.errorCode, error.ack.detail, true)
|
|
167
|
+
}
|
|
168
|
+
throw error
|
|
169
|
+
})
|
|
170
|
+
const markedReady = await this.dependencies.authorityStore.markConnectionReady({
|
|
171
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
172
|
+
generation: claim.runtimeConnectionGeneration,
|
|
173
|
+
runtimeConnectionId: claim.runtimeConnectionId,
|
|
174
|
+
lastReceivedCommandSeq: readiness.lastReceivedCommandSeq,
|
|
175
|
+
agentRunSnapshot: readiness.agentRun,
|
|
176
|
+
})
|
|
177
|
+
if (!markedReady)
|
|
178
|
+
throw new ConnectionAuthorityLostError()
|
|
179
|
+
this.ready = true
|
|
180
|
+
|
|
181
|
+
const pump = new SessionCommandPump({
|
|
182
|
+
sessionId: claim.sessionId,
|
|
183
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
184
|
+
runtimeConnectionId: claim.runtimeConnectionId,
|
|
185
|
+
afterSequence: readiness.lastReceivedCommandSeq,
|
|
186
|
+
commands: this.dependencies.commands,
|
|
187
|
+
authorityStore: this.dependencies.authorityStore,
|
|
188
|
+
connection,
|
|
189
|
+
logger: this.dependencies.logger,
|
|
190
|
+
onRejected: async (ack) => {
|
|
191
|
+
await this.dependencies.failures.markSessionFailed({
|
|
192
|
+
sessionId: claim.sessionId,
|
|
193
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
194
|
+
providerSandboxId: claim.providerSandboxId,
|
|
195
|
+
code: ack.errorCode,
|
|
196
|
+
detail: ack.detail,
|
|
197
|
+
})
|
|
198
|
+
connection.close(1008, 'Supervisor command was rejected.')
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
this.commandPump = pump
|
|
202
|
+
if (bootstrapAck)
|
|
203
|
+
pump.acknowledge(bootstrapAck)
|
|
204
|
+
pump.start()
|
|
205
|
+
const close = await Promise.race([
|
|
206
|
+
closed.promise,
|
|
207
|
+
aborted(this.controller.signal),
|
|
208
|
+
renewalFailure.promise.then((error) => { throw error }),
|
|
209
|
+
])
|
|
210
|
+
throw new ConnectionClosedError({ ready: this.ready, detail: 'code' in close ? `${close.code}:${close.reason}` : undefined })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
wakeCommands(): void {
|
|
214
|
+
this.commandPump?.wake()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
stop(reason?: unknown): void {
|
|
218
|
+
this.dependencies.parentSignal.removeEventListener('abort', this.parentAbort)
|
|
219
|
+
if (!this.controller.signal.aborted)
|
|
220
|
+
this.controller.abort(reason ?? new DOMException('Connection attempt stopped.', 'AbortError'))
|
|
221
|
+
this.commandPump?.stop()
|
|
222
|
+
this.processor?.stop(reason)
|
|
223
|
+
this.connection?.close(1001, 'Connection attempt stopped.')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async drain(): Promise<void> {
|
|
227
|
+
await this.processor?.drain()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async renewLease(): Promise<never> {
|
|
231
|
+
const { claim, timings } = this.dependencies
|
|
232
|
+
let leaseSafetyDeadline = monotonicDeadline(claim.leaseExpiresAt)
|
|
233
|
+
while (true) {
|
|
234
|
+
const untilSafetyDeadline = leaseSafetyDeadline - performance.now()
|
|
235
|
+
if (untilSafetyDeadline <= 0)
|
|
236
|
+
throw new ConnectionAuthorityLostError()
|
|
237
|
+
await delay(Math.min(timings.leaseRenewIntervalMs, untilSafetyDeadline), this.controller.signal)
|
|
238
|
+
if (performance.now() >= leaseSafetyDeadline)
|
|
239
|
+
throw new ConnectionAuthorityLostError()
|
|
240
|
+
const result = await beforeDeadline(this.dependencies.authorityStore.renewConnectionLease({
|
|
241
|
+
sessionSandboxId: claim.sessionSandboxId,
|
|
242
|
+
replicaId: this.dependencies.replicaId,
|
|
243
|
+
generation: claim.runtimeConnectionGeneration,
|
|
244
|
+
leaseDurationMs: timings.leaseDurationMs,
|
|
245
|
+
supervisorLivenessObserved: this.processor?.takeSupervisorLivenessObservation() ?? false,
|
|
246
|
+
}), leaseSafetyDeadline, this.controller.signal, () => new ConnectionAuthorityLostError())
|
|
247
|
+
if (!result.renewed)
|
|
248
|
+
throw new ConnectionAuthorityLostError()
|
|
249
|
+
leaseSafetyDeadline = monotonicDeadline(
|
|
250
|
+
result.leaseExpiresAt ?? new Date(Date.now() + timings.leaseDurationMs),
|
|
251
|
+
)
|
|
252
|
+
if (result.hasOutstandingCommand)
|
|
253
|
+
this.commandPump?.wake()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export class DeterministicEstablishmentError extends Error {
|
|
259
|
+
override readonly name = 'DeterministicEstablishmentError'
|
|
260
|
+
readonly code: string
|
|
261
|
+
readonly detail: string | undefined
|
|
262
|
+
readonly reported: boolean
|
|
263
|
+
|
|
264
|
+
constructor(code: string, detail?: string, reported = false) {
|
|
265
|
+
super(detail ?? code)
|
|
266
|
+
this.code = code
|
|
267
|
+
this.detail = detail
|
|
268
|
+
this.reported = reported
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export class ConnectionAuthorityLostError extends Error {
|
|
273
|
+
override readonly name = 'ConnectionAuthorityLostError'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export class ConnectionClosedError extends Error {
|
|
277
|
+
override readonly name = 'ConnectionClosedError'
|
|
278
|
+
readonly ready: boolean
|
|
279
|
+
|
|
280
|
+
constructor(input: { ready: boolean, detail?: string }) {
|
|
281
|
+
super(input.detail ?? 'Supervisor connection closed.')
|
|
282
|
+
this.ready = input.ready
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function aborted(signal: AbortSignal): Promise<never> {
|
|
287
|
+
if (signal.aborted)
|
|
288
|
+
throw signal.reason
|
|
289
|
+
return await new Promise<never>((_resolve, reject) => {
|
|
290
|
+
signal.addEventListener('abort', () => reject(signal.reason), { once: true })
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function delay(milliseconds: number, signal: AbortSignal): Promise<void> {
|
|
295
|
+
if (signal.aborted)
|
|
296
|
+
throw signal.reason
|
|
297
|
+
await new Promise<void>((resolve, reject) => {
|
|
298
|
+
let timeout: ReturnType<typeof setTimeout>
|
|
299
|
+
const abort = () => {
|
|
300
|
+
clearTimeout(timeout)
|
|
301
|
+
signal.removeEventListener('abort', abort)
|
|
302
|
+
reject(signal.reason)
|
|
303
|
+
}
|
|
304
|
+
timeout = setTimeout(() => {
|
|
305
|
+
signal.removeEventListener('abort', abort)
|
|
306
|
+
resolve()
|
|
307
|
+
}, milliseconds)
|
|
308
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function monotonicDeadline(expiresAt: Date): number {
|
|
313
|
+
return performance.now() + Math.max(0, expiresAt.getTime() - Date.now())
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function beforeDeadline<T>(
|
|
317
|
+
task: Promise<T>,
|
|
318
|
+
deadline: number,
|
|
319
|
+
signal: AbortSignal,
|
|
320
|
+
timeoutError: () => Error,
|
|
321
|
+
): Promise<T> {
|
|
322
|
+
signal.throwIfAborted()
|
|
323
|
+
const remaining = deadline - performance.now()
|
|
324
|
+
if (remaining <= 0)
|
|
325
|
+
throw timeoutError()
|
|
326
|
+
let timer: ReturnType<typeof setTimeout>
|
|
327
|
+
let abort: () => void
|
|
328
|
+
const guard = new Promise<never>((_resolve, reject) => {
|
|
329
|
+
abort = () => reject(signal.reason)
|
|
330
|
+
timer = setTimeout(() => reject(timeoutError()), remaining)
|
|
331
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
332
|
+
})
|
|
333
|
+
try {
|
|
334
|
+
return await Promise.race([task, guard])
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
clearTimeout(timer!)
|
|
338
|
+
signal.removeEventListener('abort', abort!)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { SandboxControlAuthorityClaims, SandboxControlAuthorityPublicJwks } from '../supervisor-protocol/control-authority'
|
|
2
|
+
import type { ControlAuthoritySigner } from './ports'
|
|
3
|
+
import { SignJWT } from 'jose/jwt/sign'
|
|
4
|
+
import { importJWK } from 'jose/key/import'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { InfrastructureError } from '../errors'
|
|
7
|
+
import {
|
|
8
|
+
sandboxControlAuthorityAlgorithm,
|
|
9
|
+
sandboxControlAuthorityAssertionLifetimeSeconds,
|
|
10
|
+
sandboxControlAuthorityAudience,
|
|
11
|
+
sandboxControlAuthorityClaimsSchema,
|
|
12
|
+
sandboxControlAuthorityPublicJwksSchema,
|
|
13
|
+
} from '../supervisor-protocol/control-authority'
|
|
14
|
+
|
|
15
|
+
const base64UrlCoordinateSchema = z.string().regex(/^[\w-]+$/u)
|
|
16
|
+
|
|
17
|
+
const privateJwkSchema = z.object({
|
|
18
|
+
kty: z.literal('EC'),
|
|
19
|
+
crv: z.literal('P-256'),
|
|
20
|
+
x: base64UrlCoordinateSchema,
|
|
21
|
+
y: base64UrlCoordinateSchema,
|
|
22
|
+
d: base64UrlCoordinateSchema,
|
|
23
|
+
kid: z.string().regex(/^[\w.-]{1,128}$/u),
|
|
24
|
+
alg: z.literal(sandboxControlAuthorityAlgorithm),
|
|
25
|
+
use: z.literal('sig'),
|
|
26
|
+
key_ops: z.tuple([z.literal('sign')]),
|
|
27
|
+
}).strict()
|
|
28
|
+
|
|
29
|
+
export class ControlAuthorityConfigurationError extends InfrastructureError {
|
|
30
|
+
constructor(input: { message: string, cause?: unknown }) {
|
|
31
|
+
super({
|
|
32
|
+
code: 'agent-core.control-authority-configuration-invalid',
|
|
33
|
+
message: input.message,
|
|
34
|
+
publicMessage: 'Sandbox control authority is not configured.',
|
|
35
|
+
retryable: false,
|
|
36
|
+
cause: input.cause,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createControlAuthoritySigner(input: {
|
|
42
|
+
issuer: string
|
|
43
|
+
activeKeyId: string
|
|
44
|
+
signingJwk: string | Record<string, unknown>
|
|
45
|
+
publicJwks: string | SandboxControlAuthorityPublicJwks
|
|
46
|
+
}): Promise<{ sign: ControlAuthoritySigner, publicJwks: SandboxControlAuthorityPublicJwks }> {
|
|
47
|
+
try {
|
|
48
|
+
const rawPrivateJwk = typeof input.signingJwk === 'string' ? JSON.parse(input.signingJwk) : input.signingJwk
|
|
49
|
+
const rawPublicJwks = typeof input.publicJwks === 'string' ? JSON.parse(input.publicJwks) : input.publicJwks
|
|
50
|
+
const privateJwk = privateJwkSchema.parse(rawPrivateJwk)
|
|
51
|
+
const publicJwks = sandboxControlAuthorityPublicJwksSchema.parse(rawPublicJwks)
|
|
52
|
+
if (privateJwk.kid !== input.activeKeyId)
|
|
53
|
+
configurationFailure('The active key ID does not match the signing key.')
|
|
54
|
+
const activePublicKey = publicJwks.keys.find(key => key.kid === input.activeKeyId)
|
|
55
|
+
if (!activePublicKey)
|
|
56
|
+
configurationFailure('The active signing key is absent from the verification keyset.')
|
|
57
|
+
if (activePublicKey.x !== privateJwk.x || activePublicKey.y !== privateJwk.y)
|
|
58
|
+
configurationFailure('The active signing key does not match its verification key.')
|
|
59
|
+
const signingKey = await importJWK(privateJwk, sandboxControlAuthorityAlgorithm)
|
|
60
|
+
if (!(signingKey instanceof CryptoKey))
|
|
61
|
+
configurationFailure('The control-authority signing key is not asymmetric.')
|
|
62
|
+
|
|
63
|
+
const sign: ControlAuthoritySigner = async (assertionInput) => {
|
|
64
|
+
const now = Math.floor(Date.now() / 1_000)
|
|
65
|
+
const claims = sandboxControlAuthorityClaimsSchema.parse({
|
|
66
|
+
iss: input.issuer,
|
|
67
|
+
aud: sandboxControlAuthorityAudience,
|
|
68
|
+
jti: assertionInput.runtimeConnectionAttemptId,
|
|
69
|
+
iat: now,
|
|
70
|
+
nbf: now,
|
|
71
|
+
exp: now + sandboxControlAuthorityAssertionLifetimeSeconds,
|
|
72
|
+
...assertionInput,
|
|
73
|
+
})
|
|
74
|
+
return await signClaims({ claims, keyId: input.activeKeyId, signingKey })
|
|
75
|
+
}
|
|
76
|
+
return { sign, publicJwks }
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
if (cause instanceof ControlAuthorityConfigurationError)
|
|
80
|
+
throw cause
|
|
81
|
+
throw new ControlAuthorityConfigurationError({
|
|
82
|
+
message: 'Sandbox control-authority signing configuration is invalid.',
|
|
83
|
+
cause,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function configurationFailure(message: string): never {
|
|
89
|
+
throw new ControlAuthorityConfigurationError({ message })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function signClaims(input: {
|
|
93
|
+
claims: SandboxControlAuthorityClaims
|
|
94
|
+
keyId: string
|
|
95
|
+
signingKey: CryptoKey
|
|
96
|
+
}): Promise<string> {
|
|
97
|
+
return await new SignJWT(input.claims)
|
|
98
|
+
.setProtectedHeader({ alg: sandboxControlAuthorityAlgorithm, kid: input.keyId, typ: 'JWT' })
|
|
99
|
+
.sign(input.signingKey)
|
|
100
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { SupervisorConnection, SupervisorConnectionOpener } from './ports'
|
|
2
|
+
import { ProviderError } from '../errors'
|
|
3
|
+
import { sandboxControlAuthorityHeaderName } from '../supervisor-protocol/control-authority'
|
|
4
|
+
|
|
5
|
+
const defaultIngressUrl = 'wss://sandbox.e2b.app/control/ws'
|
|
6
|
+
const defaultSupervisorPort = 8080
|
|
7
|
+
|
|
8
|
+
export function createE2bSupervisorConnectionOpener(input: {
|
|
9
|
+
ingressUrl?: string
|
|
10
|
+
supervisorPort?: number
|
|
11
|
+
dialTimeoutMs?: number
|
|
12
|
+
} = {}): SupervisorConnectionOpener {
|
|
13
|
+
const ingressUrl = input.ingressUrl ?? defaultIngressUrl
|
|
14
|
+
const supervisorPort = input.supervisorPort ?? defaultSupervisorPort
|
|
15
|
+
const dialTimeoutMs = input.dialTimeoutMs ?? 10_000
|
|
16
|
+
|
|
17
|
+
return async (request) => {
|
|
18
|
+
request.signal.throwIfAborted()
|
|
19
|
+
const socket = new WebSocket(ingressUrl, {
|
|
20
|
+
headers: {
|
|
21
|
+
'E2B-Traffic-Access-Token': request.trafficAccessToken,
|
|
22
|
+
'E2b-Sandbox-Id': request.providerSandboxId,
|
|
23
|
+
'E2b-Sandbox-Port': String(supervisorPort),
|
|
24
|
+
'X-Coding-Agent-Session-Id': request.sessionId,
|
|
25
|
+
'X-Coding-Agent-Session-Sandbox-Id': request.sessionSandboxId,
|
|
26
|
+
'X-Coding-Agent-Runtime-Connection-Attempt-Id': request.runtimeConnectionAttemptId,
|
|
27
|
+
[sandboxControlAuthorityHeaderName]: request.authorityAssertion,
|
|
28
|
+
'X-Coding-Agent-Connection-Generation': String(request.connectionGeneration),
|
|
29
|
+
'X-Coding-Agent-Event-Ack': String(request.eventAckFloor),
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
const connection: SupervisorConnection = {
|
|
33
|
+
send(frame) {
|
|
34
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
35
|
+
throw new E2bSupervisorConnectionError({
|
|
36
|
+
message: 'E2B supervisor connection is not open.',
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
socket.send(frame)
|
|
40
|
+
},
|
|
41
|
+
close: (code, reason) => socket.close(code, reason),
|
|
42
|
+
}
|
|
43
|
+
socket.binaryType = 'arraybuffer'
|
|
44
|
+
socket.onmessage = (event) => {
|
|
45
|
+
if (typeof event.data === 'string')
|
|
46
|
+
request.onMessage(event.data)
|
|
47
|
+
else if (event.data instanceof ArrayBuffer)
|
|
48
|
+
request.onMessage(event.data)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await new Promise<void>((resolve, reject) => {
|
|
52
|
+
let timeout: ReturnType<typeof setTimeout>
|
|
53
|
+
const abort = () => {
|
|
54
|
+
clearTimeout(timeout)
|
|
55
|
+
request.signal.removeEventListener('abort', abort)
|
|
56
|
+
socket.close()
|
|
57
|
+
reject(request.signal.reason)
|
|
58
|
+
}
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
clearTimeout(timeout)
|
|
61
|
+
request.signal.removeEventListener('abort', abort)
|
|
62
|
+
}
|
|
63
|
+
timeout = setTimeout(() => {
|
|
64
|
+
cleanup()
|
|
65
|
+
socket.close()
|
|
66
|
+
reject(new E2bSupervisorConnectionError({ message: 'Timed out opening the E2B supervisor connection.' }))
|
|
67
|
+
}, dialTimeoutMs)
|
|
68
|
+
request.signal.addEventListener('abort', abort, { once: true })
|
|
69
|
+
socket.onopen = () => {
|
|
70
|
+
cleanup()
|
|
71
|
+
socket.onclose = event => request.onClose({ code: event.code, reason: event.reason })
|
|
72
|
+
socket.onerror = () => {
|
|
73
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
74
|
+
socket.close()
|
|
75
|
+
}
|
|
76
|
+
resolve()
|
|
77
|
+
}
|
|
78
|
+
socket.onerror = () => undefined
|
|
79
|
+
socket.onclose = (event) => {
|
|
80
|
+
cleanup()
|
|
81
|
+
reject(new E2bSupervisorConnectionError({
|
|
82
|
+
message: `E2B supervisor connection closed during upgrade (${event.code}).`,
|
|
83
|
+
}))
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
return connection
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const openE2bSupervisorConnection = createE2bSupervisorConnectionOpener()
|
|
91
|
+
|
|
92
|
+
class E2bSupervisorConnectionError extends ProviderError {
|
|
93
|
+
constructor(input: { message: string, cause?: unknown }) {
|
|
94
|
+
super({
|
|
95
|
+
code: 'agent-core.e2b-supervisor-connection-failed',
|
|
96
|
+
message: input.message,
|
|
97
|
+
publicMessage: 'The sandbox control connection is unavailable.',
|
|
98
|
+
retryable: true,
|
|
99
|
+
cause: input.cause,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|