@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.
Files changed (85) hide show
  1. package/README.md +36 -3
  2. package/bin/supervisor.ts +116 -0
  3. package/package.json +42 -3
  4. package/scripts/build-supervisor-executable.ts +32 -0
  5. package/src/agents/agent-error-serializer.ts +23 -0
  6. package/src/agents/agent-event-stream.ts +81 -0
  7. package/src/agents/agent-id.ts +17 -0
  8. package/src/agents/agent-operation.ts +11 -0
  9. package/src/agents/agent-provider.ts +37 -0
  10. package/src/agents/agent-run.ts +32 -0
  11. package/src/agents/agent-runtime-error.ts +161 -0
  12. package/src/agents/agent-session-events.ts +32 -0
  13. package/src/agents/agent-tool-runner.ts +83 -0
  14. package/src/agents/agent-tool.ts +111 -0
  15. package/src/agents/author-context.ts +36 -0
  16. package/src/agents/claude/claude-command-mapper.ts +234 -0
  17. package/src/agents/claude/claude-event-mapper.ts +748 -0
  18. package/src/agents/claude/claude-provider.ts +191 -0
  19. package/src/agents/claude/claude-run.ts +468 -0
  20. package/src/agents/claude/claude-tool-mapper.ts +186 -0
  21. package/src/agents/claude/index.ts +1 -0
  22. package/src/agents/codex/codex-auth.ts +34 -0
  23. package/src/agents/codex/codex-command-mapper.ts +78 -0
  24. package/src/agents/codex/codex-event-mapper.ts +721 -0
  25. package/src/agents/codex/codex-json-rpc-client.ts +326 -0
  26. package/src/agents/codex/codex-protocol.ts +36 -0
  27. package/src/agents/codex/codex-provider.ts +1050 -0
  28. package/src/agents/codex/codex-run.ts +404 -0
  29. package/src/agents/codex/codex-skill-catalog.ts +158 -0
  30. package/src/agents/codex/codex-skill-roots.ts +19 -0
  31. package/src/agents/codex/codex-tool-mapper.ts +55 -0
  32. package/src/agents/codex/codex.errors.ts +68 -0
  33. package/src/agents/codex/generated/meta.gen.ts +606 -0
  34. package/src/agents/codex/generated/namespaces.gen.ts +311 -0
  35. package/src/agents/codex/generated/schema.gen.ts +34883 -0
  36. package/src/agents/codex/index.ts +2 -0
  37. package/src/agents/index.ts +14 -0
  38. package/src/agents/input-attachment-preparation.ts +86 -0
  39. package/src/agents/input-attachment.errors.ts +16 -0
  40. package/src/agents/instructions.ts +8 -0
  41. package/src/agents/materialized-input-attachment.ts +26 -0
  42. package/src/agents/message-id.ts +23 -0
  43. package/src/agents/normalize.ts +8 -0
  44. package/src/agents/sandbox-environment.ts +1 -0
  45. package/src/agents/tools/ping.tool.ts +13 -0
  46. package/src/agents/user-input-request.ts +470 -0
  47. package/src/protocol/agent-event.ts +2 -1
  48. package/src/protocol/agent-usage.ts +14 -0
  49. package/src/provenance.gen.ts +3 -3
  50. package/src/supervisor/agent-provider-factory.ts +189 -0
  51. package/src/supervisor/bootstrap-binder.ts +133 -0
  52. package/src/supervisor/config.ts +49 -0
  53. package/src/supervisor/control-authority-verifier.ts +135 -0
  54. package/src/supervisor/create-supervisor-runtime.ts +25 -0
  55. package/src/supervisor/errors.ts +24 -0
  56. package/src/supervisor/index.ts +34 -0
  57. package/src/supervisor/persistence/json.ts +21 -0
  58. package/src/supervisor/persistence/state-discovery.ts +56 -0
  59. package/src/supervisor/persistence/supervisor-store.ts +364 -0
  60. package/src/supervisor/ports/index.ts +109 -0
  61. package/src/supervisor/provider-factory.ts +37 -0
  62. package/src/supervisor/resident.ts +143 -0
  63. package/src/supervisor/rpc-client.ts +120 -0
  64. package/src/supervisor/runtime-handler.ts +309 -0
  65. package/src/supervisor/websocket-server.ts +435 -0
  66. package/src/supervisor-protocol/bootstrap.ts +1 -1
  67. package/src/template-onboarding.ts +47 -0
  68. package/src/testing/es256-test-keys.ts +73 -0
  69. package/src/testing/in-memory-runtime-control-plane.ts +206 -0
  70. package/src/testing/index.ts +6 -0
  71. package/src/testing/loopback-supervisor-connection.ts +71 -0
  72. package/src/testing/scripted-provider.ts +64 -0
  73. package/src/worker-runtime-client/command-pump.ts +127 -0
  74. package/src/worker-runtime-client/connection-attempt.ts +349 -0
  75. package/src/worker-runtime-client/control-authority-signer.ts +100 -0
  76. package/src/worker-runtime-client/e2b-supervisor-connection.ts +102 -0
  77. package/src/worker-runtime-client/frame-processor.ts +178 -0
  78. package/src/worker-runtime-client/index.ts +27 -0
  79. package/src/worker-runtime-client/lease-reconciler.ts +14 -0
  80. package/src/worker-runtime-client/ports.ts +139 -0
  81. package/src/worker-runtime-client/postgres-notification-listener.ts +91 -0
  82. package/src/worker-runtime-client/rpc-dispatcher.ts +27 -0
  83. package/src/worker-runtime-client/rpc-request-manager.ts +141 -0
  84. package/src/worker-runtime-client/sandbox-connection-runtime.ts +301 -0
  85. package/src/worker-runtime-client/token-crypto.ts +46 -0
@@ -0,0 +1,301 @@
1
+ import type { Logger } from '../logger'
2
+ import type {
3
+ BootstrapProvider,
4
+ CommandSource,
5
+ ConnectionAuthorityStore,
6
+ ControlAuthoritySigner,
7
+ DurableEventSink,
8
+ EphemeralEventSink,
9
+ RpcDispatcher,
10
+ RuntimeNotifications,
11
+ SandboxConnectionRuntimeTimings,
12
+ SessionFailureSink,
13
+ SupervisorConnectionOpener,
14
+ } from './ports'
15
+ import {
16
+ ConnectionAuthorityLostError,
17
+ ConnectionClosedError,
18
+ DeterministicEstablishmentError,
19
+ SessionSandboxConnectionAttempt,
20
+ } from './connection-attempt'
21
+ import { TokenDecryptionError } from './token-crypto'
22
+
23
+ const defaultTimings: SandboxConnectionRuntimeTimings = {
24
+ leaseDurationMs: 30_000,
25
+ leaseRenewIntervalMs: 10_000,
26
+ establishmentBudgetMs: 30_000,
27
+ dialTimeoutMs: 10_000,
28
+ retryInitialDelayMs: 250,
29
+ retryMaxDelayMs: 5_000,
30
+ candidateLimit: 100,
31
+ }
32
+
33
+ export function createSandboxConnectionRuntime(input: {
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
+ notifications: RuntimeNotifications
46
+ timings?: Partial<SandboxConnectionRuntimeTimings>
47
+ logger: Logger
48
+ }): {
49
+ start: () => Promise<void>
50
+ stop: () => Promise<void>
51
+ wake: (sessionSandboxId: string) => void
52
+ wakeCommands: (sessionId: string) => void
53
+ } {
54
+ const timings = validatedTimings({ ...defaultTimings, ...input.timings })
55
+ const owned = new Map<string, OwnedConnection>()
56
+ const sessionIndex = new Map<string, string>()
57
+ let started = false
58
+ let stopping = false
59
+
60
+ const wake = (sessionSandboxId: string) => {
61
+ if (!started || stopping || owned.has(sessionSandboxId))
62
+ return
63
+ const connection = new OwnedConnection({ ...input, timings, sessionSandboxId })
64
+ owned.set(sessionSandboxId, connection)
65
+ void connection.run().finally(() => {
66
+ if (owned.get(sessionSandboxId) === connection)
67
+ owned.delete(sessionSandboxId)
68
+ const sessionId = connection.sessionId()
69
+ if (sessionId && sessionIndex.get(sessionId) === sessionSandboxId)
70
+ sessionIndex.delete(sessionId)
71
+ })
72
+ connection.onClaimed((sessionId) => {
73
+ sessionIndex.set(sessionId, sessionSandboxId)
74
+ })
75
+ }
76
+
77
+ return {
78
+ async start() {
79
+ if (started)
80
+ return
81
+ started = true
82
+ try {
83
+ await input.notifications.start({
84
+ onConnectionDemand: wake,
85
+ onCommandsReady: sessionId => owned.get(sessionIndex.get(sessionId) ?? '')?.wakeCommands(),
86
+ onOwnerChanged: change => owned.get(change.sessionSandboxId)?.ownerChanged(change.generation),
87
+ })
88
+ const candidates = await input.authorityStore.listConnectionCandidates({ limit: timings.candidateLimit })
89
+ for (const candidate of candidates)
90
+ wake(candidate.sessionSandboxId)
91
+ }
92
+ catch (error) {
93
+ started = false
94
+ await input.notifications.stop().catch(() => undefined)
95
+ throw error
96
+ }
97
+ },
98
+ async stop() {
99
+ if (stopping)
100
+ return
101
+ stopping = true
102
+ await input.notifications.stop()
103
+ const connections = [...owned.values()]
104
+ for (const connection of connections)
105
+ connection.stop()
106
+ await Promise.allSettled(connections.map(connection => connection.finished()))
107
+ owned.clear()
108
+ sessionIndex.clear()
109
+ },
110
+ wake,
111
+ wakeCommands(sessionId) {
112
+ owned.get(sessionIndex.get(sessionId) ?? '')?.wakeCommands()
113
+ },
114
+ }
115
+ }
116
+
117
+ class OwnedConnection {
118
+ private readonly controller = new AbortController()
119
+ private readonly completion = Promise.withResolvers<void>()
120
+ private attempt: SessionSandboxConnectionAttempt | undefined
121
+ private claimSessionId: string | undefined
122
+ private claimGeneration: number | undefined
123
+ private claimed: ((sessionId: string) => void) | undefined
124
+
125
+ constructor(private readonly dependencies: Parameters<typeof createSandboxConnectionRuntime>[0] & {
126
+ sessionSandboxId: string
127
+ timings: SandboxConnectionRuntimeTimings
128
+ }) {}
129
+
130
+ onClaimed(callback: (sessionId: string) => void): void {
131
+ this.claimed = callback
132
+ if (this.claimSessionId)
133
+ callback(this.claimSessionId)
134
+ }
135
+
136
+ sessionId(): string | undefined {
137
+ return this.claimSessionId
138
+ }
139
+
140
+ async run(): Promise<void> {
141
+ try {
142
+ const claim = await this.dependencies.authorityStore.claimConnection({
143
+ sessionSandboxId: this.dependencies.sessionSandboxId,
144
+ replicaId: this.dependencies.replicaId,
145
+ leaseDurationMs: this.dependencies.timings.leaseDurationMs,
146
+ })
147
+ if (!claim)
148
+ return
149
+ this.claimSessionId = claim.sessionId
150
+ this.claimGeneration = claim.runtimeConnectionGeneration
151
+ this.claimed?.(claim.sessionId)
152
+ let establishmentDeadline = performance.now() + this.dependencies.timings.establishmentBudgetMs
153
+ let backoff = this.dependencies.timings.retryInitialDelayMs
154
+ while (!this.controller.signal.aborted) {
155
+ const attempt = new SessionSandboxConnectionAttempt({
156
+ claim,
157
+ establishmentDeadline,
158
+ replicaId: this.dependencies.replicaId,
159
+ trafficTokenEncryptionKey: this.dependencies.trafficTokenEncryptionKey,
160
+ signControlAuthority: this.dependencies.signControlAuthority,
161
+ openSupervisorConnection: this.dependencies.openSupervisorConnection,
162
+ authorityStore: this.dependencies.authorityStore,
163
+ commands: this.dependencies.commands,
164
+ durableEvents: this.dependencies.durableEvents,
165
+ ephemeralEvents: this.dependencies.ephemeralEvents,
166
+ rpc: this.dependencies.rpc,
167
+ bootstrap: this.dependencies.bootstrap,
168
+ failures: this.dependencies.failures,
169
+ timings: this.dependencies.timings,
170
+ logger: this.dependencies.logger,
171
+ parentSignal: this.controller.signal,
172
+ })
173
+ this.attempt = attempt
174
+ try {
175
+ await attempt.run()
176
+ }
177
+ catch (error) {
178
+ attempt.stop(error)
179
+ await attempt.drain()
180
+ if (this.controller.signal.aborted)
181
+ return
182
+ if (error instanceof DeterministicEstablishmentError) {
183
+ if (!error.reported) {
184
+ await this.dependencies.failures.failDeterministicEstablishment({
185
+ sessionId: claim.sessionId,
186
+ sessionSandboxId: claim.sessionSandboxId,
187
+ providerSandboxId: claim.providerSandboxId,
188
+ generation: claim.runtimeConnectionGeneration,
189
+ code: error.code,
190
+ detail: error.detail,
191
+ })
192
+ }
193
+ return
194
+ }
195
+ if (error instanceof ConnectionAuthorityLostError)
196
+ return
197
+ if (error instanceof TokenDecryptionError) {
198
+ await this.dependencies.failures.markSessionFailed({
199
+ sessionId: claim.sessionId,
200
+ sessionSandboxId: claim.sessionSandboxId,
201
+ providerSandboxId: claim.providerSandboxId,
202
+ code: 'agent-core.traffic-token-decryption-failed',
203
+ })
204
+ return
205
+ }
206
+ if (error instanceof ConnectionClosedError && error.ready) {
207
+ establishmentDeadline = performance.now() + this.dependencies.timings.establishmentBudgetMs
208
+ backoff = this.dependencies.timings.retryInitialDelayMs
209
+ }
210
+ await this.dependencies.authorityStore.markConnectionReconnecting({
211
+ sessionSandboxId: claim.sessionSandboxId,
212
+ generation: claim.runtimeConnectionGeneration,
213
+ })
214
+ if (performance.now() + backoff >= establishmentDeadline)
215
+ break
216
+ await abortableDelay(backoff, this.controller.signal)
217
+ backoff = Math.min(backoff * 2, this.dependencies.timings.retryMaxDelayMs)
218
+ }
219
+ }
220
+ if (!this.controller.signal.aborted) {
221
+ const exhausted = await this.dependencies.authorityStore.markConnectionRetryExhausted({
222
+ sessionSandboxId: claim.sessionSandboxId,
223
+ generation: claim.runtimeConnectionGeneration,
224
+ replicaId: this.dependencies.replicaId,
225
+ })
226
+ if (exhausted) {
227
+ await this.dependencies.failures.markEstablishmentExhausted({
228
+ sessionId: claim.sessionId,
229
+ sessionSandboxId: claim.sessionSandboxId,
230
+ providerSandboxId: claim.providerSandboxId,
231
+ generation: claim.runtimeConnectionGeneration,
232
+ })
233
+ }
234
+ }
235
+ }
236
+ catch (error) {
237
+ this.dependencies.logger.error({ err: error, sessionSandboxId: this.dependencies.sessionSandboxId }, 'sandbox connection owner failed')
238
+ }
239
+ finally {
240
+ this.attempt?.stop()
241
+ if (this.claimGeneration !== undefined) {
242
+ await this.dependencies.authorityStore.releaseConnection({
243
+ sessionSandboxId: this.dependencies.sessionSandboxId,
244
+ generation: this.claimGeneration,
245
+ reason: this.controller.signal.aborted ? 'runtime_stopped' : 'connection_owner_finished',
246
+ }).catch(error => this.dependencies.logger.warn({ err: error }, 'sandbox connection release failed'))
247
+ }
248
+ this.completion.resolve()
249
+ }
250
+ }
251
+
252
+ wakeCommands(): void {
253
+ this.attempt?.wakeCommands()
254
+ }
255
+
256
+ ownerChanged(generation: number): void {
257
+ if (!this.attempt || generation !== this.claimGeneration)
258
+ return
259
+ this.stop(new ConnectionAuthorityLostError())
260
+ }
261
+
262
+ stop(reason?: unknown): void {
263
+ if (!this.controller.signal.aborted)
264
+ this.controller.abort(reason ?? new DOMException('Sandbox connection runtime stopped.', 'AbortError'))
265
+ this.attempt?.stop(reason)
266
+ }
267
+
268
+ async finished(): Promise<void> {
269
+ await this.completion.promise
270
+ }
271
+ }
272
+
273
+ async function abortableDelay(milliseconds: number, signal: AbortSignal): Promise<void> {
274
+ if (signal.aborted)
275
+ throw signal.reason
276
+ await new Promise<void>((resolve, reject) => {
277
+ let timeout: ReturnType<typeof setTimeout>
278
+ const abort = () => {
279
+ clearTimeout(timeout)
280
+ signal.removeEventListener('abort', abort)
281
+ reject(signal.reason)
282
+ }
283
+ timeout = setTimeout(() => {
284
+ signal.removeEventListener('abort', abort)
285
+ resolve()
286
+ }, milliseconds)
287
+ signal.addEventListener('abort', abort, { once: true })
288
+ })
289
+ }
290
+
291
+ function validatedTimings(timings: SandboxConnectionRuntimeTimings): SandboxConnectionRuntimeTimings {
292
+ for (const [name, value] of Object.entries(timings)) {
293
+ if (!Number.isSafeInteger(value) || value <= 0)
294
+ throw new RangeError(`${name} must be a positive safe integer.`)
295
+ }
296
+ if (timings.leaseRenewIntervalMs >= timings.leaseDurationMs)
297
+ throw new RangeError('leaseRenewIntervalMs must be lower than leaseDurationMs.')
298
+ if (timings.retryInitialDelayMs > timings.retryMaxDelayMs)
299
+ throw new RangeError('retryInitialDelayMs must not exceed retryMaxDelayMs.')
300
+ return timings
301
+ }
@@ -0,0 +1,46 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'
3
+
4
+ const algorithm = 'aes-256-gcm'
5
+ const ivBytes = 12
6
+
7
+ function encryptionKey(secret: string): Buffer {
8
+ if (!secret)
9
+ throw new TypeError('Token encryption key must not be empty.')
10
+ return createHash('sha256').update(secret, 'utf8').digest()
11
+ }
12
+
13
+ export function encryptToken(input: { token: string, secret: string }): string {
14
+ const iv = randomBytes(ivBytes)
15
+ const cipher = createCipheriv(algorithm, encryptionKey(input.secret), iv)
16
+ const ciphertext = Buffer.concat([cipher.update(input.token, 'utf8'), cipher.final()])
17
+ return [iv, cipher.getAuthTag(), ciphertext].map(value => value.toString('base64url')).join('.')
18
+ }
19
+
20
+ export function decryptToken(input: { encryptedToken: string, secret: string }): string {
21
+ try {
22
+ const parts = input.encryptedToken.split('.')
23
+ if (parts.length !== 3)
24
+ throw new Error('Encrypted token has an invalid shape.')
25
+ const [encodedIv, encodedTag, encodedCiphertext] = parts
26
+ if (!encodedIv || !encodedTag || !encodedCiphertext)
27
+ throw new Error('Encrypted token contains an empty segment.')
28
+ const decipher = createDecipheriv(algorithm, encryptionKey(input.secret), Buffer.from(encodedIv, 'base64url'))
29
+ decipher.setAuthTag(Buffer.from(encodedTag, 'base64url'))
30
+ return Buffer.concat([
31
+ decipher.update(Buffer.from(encodedCiphertext, 'base64url')),
32
+ decipher.final(),
33
+ ]).toString('utf8')
34
+ }
35
+ catch (cause) {
36
+ throw new TokenDecryptionError({ cause })
37
+ }
38
+ }
39
+
40
+ export class TokenDecryptionError extends Error {
41
+ override readonly name = 'TokenDecryptionError'
42
+
43
+ constructor(options: { cause: unknown }) {
44
+ super('Stored sandbox traffic credential could not be decrypted.', options)
45
+ }
46
+ }