@opengeni/runtime 0.2.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/dist/chunk-2PO56VAL.js +3478 -0
- package/dist/chunk-2PO56VAL.js.map +1 -0
- package/dist/index.d.ts +912 -0
- package/dist/index.js +3663 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/index.d.ts +1738 -0
- package/dist/sandbox/index.js +187 -0
- package/dist/sandbox/index.js.map +1 -0
- package/package.json +49 -0
- package/src/bundled_hashicorp_terraform_skills/LICENSE +373 -0
- package/src/bundled_hashicorp_terraform_skills/README.md +18 -0
- package/src/bundled_hashicorp_terraform_skills/UPSTREAM_GIT_SHA +1 -0
- package/src/bundled_hashicorp_terraform_skills/azure-verified-modules/SKILL.md +613 -0
- package/src/bundled_hashicorp_terraform_skills/checkov/SKILL.md +43 -0
- package/src/bundled_hashicorp_terraform_skills/refactor-module/SKILL.md +538 -0
- package/src/bundled_hashicorp_terraform_skills/social-media-marketing/SKILL.md +35 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/SKILL.md +372 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/references/MANUAL-IMPORT.md +113 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-search-import/scripts/list_resources.sh +38 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/SKILL.md +480 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/api-monitoring.md +543 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/component-blocks.md +476 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/deployment-blocks.md +391 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/examples.md +1529 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/linked-stacks.md +187 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-stacks/references/troubleshooting.md +671 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-style-guide/SKILL.md +353 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/SKILL.md +451 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/CI_CD.md +80 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/EXAMPLES.md +314 -0
- package/src/bundled_hashicorp_terraform_skills/terraform-test/references/MOCK_PROVIDERS.md +171 -0
- package/src/codex-tool-search.ts +267 -0
- package/src/context-compaction.ts +538 -0
- package/src/history-sanitizer.ts +719 -0
- package/src/index.ts +3299 -0
- package/src/sandbox/capabilities.ts +69 -0
- package/src/sandbox/channel-a.ts +1031 -0
- package/src/sandbox/display-stack.ts +231 -0
- package/src/sandbox/errors.ts +34 -0
- package/src/sandbox/index.ts +832 -0
- package/src/sandbox/providers/blaxel.ts +35 -0
- package/src/sandbox/providers/cloudflare.ts +24 -0
- package/src/sandbox/providers/daytona.ts +34 -0
- package/src/sandbox/providers/docker.ts +17 -0
- package/src/sandbox/providers/e2b.ts +36 -0
- package/src/sandbox/providers/index.ts +107 -0
- package/src/sandbox/providers/local.ts +13 -0
- package/src/sandbox/providers/modal.ts +55 -0
- package/src/sandbox/providers/none.ts +13 -0
- package/src/sandbox/providers/runloop.ts +32 -0
- package/src/sandbox/providers/selfhosted.ts +96 -0
- package/src/sandbox/providers/types.ts +38 -0
- package/src/sandbox/providers/vercel.ts +29 -0
- package/src/sandbox/recording.ts +286 -0
- package/src/sandbox/routing/backend-resolver.ts +189 -0
- package/src/sandbox/routing/routing-session.ts +455 -0
- package/src/sandbox/select.ts +371 -0
- package/src/sandbox/selfhosted/capabilities.ts +255 -0
- package/src/sandbox/selfhosted/control-rpc.ts +351 -0
- package/src/sandbox/selfhosted/session.ts +930 -0
- package/src/sandbox/selfhosted/testing.ts +230 -0
- package/src/sandbox/stream-port.ts +185 -0
- package/src/sandbox/stream-token.ts +90 -0
- package/src/sandbox/terminal-server.ts +203 -0
- package/src/sandbox-computer.ts +835 -0
|
@@ -0,0 +1,1738 @@
|
|
|
1
|
+
import { Settings } from '@opengeni/config';
|
|
2
|
+
export { collectSandboxEnvironment, parseExposedPorts } from '@opengeni/config';
|
|
3
|
+
import { SandboxBackend, CapabilityDescriptor, SandboxOs, SessionCapabilities, StreamTokenPayload, SessionEventType, SessionStructuredCapabilities, FsListRequest, FsListResponse, FsReadRequest, FsReadResponse, FsWriteRequest, FsWriteResponse, FsDeleteRequest, FsDeleteResponse, FsMoveRequest, FsMoveResponse, FsMkdirRequest, FsMkdirResponse, GitStatusRequest, GitStatusResponse, GitDiffRequest, GitDiffResponse, GitLogRequest, GitLogResponse, GitShowRequest, GitShowResponse, TerminalExecRequest, TerminalExecResponse, PtyOpenRequest, PtyOpenResponse, PtyWriteRequest, PtyResizeRequest, PtyCloseRequest, GitChangedPayload, GitDiffHunk, GitFileStatusCode, CapabilityUnavailableReason } from '@opengeni/contracts';
|
|
4
|
+
export { CAPABILITY_DESCRIPTORS, CapabilityDescriptor, DESKTOP_STREAM_PORT, StreamTokenPayload, StreamTokenPayload as StreamTokenPayloadType, TERMINAL_STREAM_PORT } from '@opengeni/contracts';
|
|
5
|
+
import { Manifest, SandboxClient, SandboxSessionState } from '@openai/agents/sandbox';
|
|
6
|
+
import { ControlRequest, ControlResponse, ErrorCode, AgentError, DesktopInputRequest, ExecRequest, ExecResponse } from '@opengeni/agent-proto';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Descriptor-table invariants, asserted once at registry build (and from a unit
|
|
10
|
+
* test). This is the guardrail that keeps the static matrix internally coherent.
|
|
11
|
+
* It validates the descriptor data only; the descriptor.backendId === SDK
|
|
12
|
+
* client.backendId assertion (the deferred-from-P0.1 check) lives in
|
|
13
|
+
* providers/index.ts because it must construct the real SDK clients.
|
|
14
|
+
*/
|
|
15
|
+
declare function assertDescriptorRegistryInvariants(): void;
|
|
16
|
+
|
|
17
|
+
declare class SandboxConfigError extends Error {
|
|
18
|
+
readonly backend: SandboxBackend | string;
|
|
19
|
+
constructor(backend: SandboxBackend | string, message: string);
|
|
20
|
+
}
|
|
21
|
+
declare class SandboxProviderUnavailableError extends Error {
|
|
22
|
+
readonly backend: SandboxBackend | string;
|
|
23
|
+
constructor(backend: SandboxBackend | string);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ProviderConstructionContext {
|
|
27
|
+
settings: Settings;
|
|
28
|
+
/** The env map for the box (collectSandboxEnvironment / per-run environment). */
|
|
29
|
+
environment: Record<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Parsed exposed ports (config string -> number[]); already includes the
|
|
32
|
+
* desktop stream port (6080) when this is a desktop tier with desktop enabled
|
|
33
|
+
* and the provider cannot expose ports on demand (the merge happens in
|
|
34
|
+
* createSandboxClient before build()).
|
|
35
|
+
*/
|
|
36
|
+
exposedPorts: number[];
|
|
37
|
+
}
|
|
38
|
+
interface ProviderRegistration {
|
|
39
|
+
backend: SandboxBackend;
|
|
40
|
+
descriptor: CapabilityDescriptor;
|
|
41
|
+
/**
|
|
42
|
+
* Validate that the settings carry the credentials/config this provider
|
|
43
|
+
* REQUIRES. Throw SandboxConfigError on any missing/contradictory field.
|
|
44
|
+
* Pure — no network. Called by both the factory and a deploy-time preflight.
|
|
45
|
+
* The factory calls this before build(), so build() may assume valid settings.
|
|
46
|
+
*/
|
|
47
|
+
validateCredentials(settings: Settings): void;
|
|
48
|
+
/**
|
|
49
|
+
* Build the raw SDK SandboxClient. Returns undefined ONLY for "none".
|
|
50
|
+
* The factory calls validateCredentials() first, so build() can assume valid.
|
|
51
|
+
*/
|
|
52
|
+
build(ctx: ProviderConstructionContext): unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
declare const PROVIDER_REGISTRY: Record<SandboxBackend, ProviderRegistration>;
|
|
56
|
+
/**
|
|
57
|
+
* Assert the descriptor table AND that each registered provider's SDK client
|
|
58
|
+
* reports the backendId its descriptor claims. The latter is the
|
|
59
|
+
* deferred-from-P0.1 invariant — it can only run here because it constructs the
|
|
60
|
+
* real clients. Called once at registry build (and from a unit test).
|
|
61
|
+
*/
|
|
62
|
+
declare function assertProviderRegistryInvariants(): void;
|
|
63
|
+
|
|
64
|
+
interface NegotiationContext {
|
|
65
|
+
sessionId: string;
|
|
66
|
+
backend: SandboxBackend;
|
|
67
|
+
os: SandboxOs;
|
|
68
|
+
/** Current lease liveness; cold means nothing is provisioned yet. */
|
|
69
|
+
liveness: "cold" | "warming" | "warm" | "draining";
|
|
70
|
+
/** The lease epoch echoed on viewer heartbeats (the split-brain fence). */
|
|
71
|
+
leaseEpoch: number;
|
|
72
|
+
/** The deployment desktop toggle (settings.sandboxDesktopEnabled). */
|
|
73
|
+
desktopEnabled: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The HUMAN take-control toggle (settings.sandboxDesktopInteractive). When true
|
|
76
|
+
* (default) and the desktop cell is available, the negotiated DesktopStream.mode
|
|
77
|
+
* is "interactive" — the noVNC viewer can drive mouse+keyboard into :0 (the box's
|
|
78
|
+
* x11vnc runs without -viewonly). When false the cell reports mode "read-only"
|
|
79
|
+
* and the client disables the "Take control" affordance (a genuinely read-only
|
|
80
|
+
* deployment). Independent of `computerUseReadOnly`, which gates the AGENT
|
|
81
|
+
* driver, not the human viewer plane. Defaults to true so a caller that never
|
|
82
|
+
* threads it (e.g. headless tests) still gets the interactive plane when the
|
|
83
|
+
* desktop is available.
|
|
84
|
+
*/
|
|
85
|
+
desktopInteractive?: boolean;
|
|
86
|
+
/** The deployment computer-use toggle (settings.computerUseEnabled). The agent
|
|
87
|
+
* drives :0 via xdotool/scrot; availability tracks desktop. Defaults to true. */
|
|
88
|
+
computerUseEnabled?: boolean;
|
|
89
|
+
/** Whether the agent computer-use driver is gated to no-op input
|
|
90
|
+
* (settings.computerUseReadOnly). v1 default false (the agent clicks/types). */
|
|
91
|
+
computerUseReadOnly?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Whether a scoped-stream-token secret is resolvable (I8/OD-8). When desktop
|
|
94
|
+
* is enabled but this is false (no streamTokenSecret AND no delegationSecret),
|
|
95
|
+
* the desktop plane GRACEFULLY DEGRADES to transport:null — the deployment
|
|
96
|
+
* boots, but the pixel plane cannot mint scoped tokens. Defaults to true so a
|
|
97
|
+
* caller that never threads it (e.g. headless tests) is unaffected.
|
|
98
|
+
*/
|
|
99
|
+
streamTokenSecretAvailable?: boolean;
|
|
100
|
+
/** Whether the calling principal has acknowledged the un-redacted desktop
|
|
101
|
+
* pixels (and, for a shared box, the shared-exposure disclosure). When the
|
|
102
|
+
* box is shared this must be the SHARED acknowledgment; a bare un-redacted ack
|
|
103
|
+
* does not satisfy a shared box. */
|
|
104
|
+
desktopAcknowledged?: boolean;
|
|
105
|
+
/** True when the box's group has >1 session: watching this desktop also shows
|
|
106
|
+
* the sibling sessions' agents on the one :0 framebuffer (addendum E.1). */
|
|
107
|
+
shared?: boolean;
|
|
108
|
+
/** The OTHER sessions whose agents may appear on the shared desktop — IDS
|
|
109
|
+
* ONLY, never their conversation/metadata (stress g). Empty for a solo box. */
|
|
110
|
+
sharedSessionIds?: string[];
|
|
111
|
+
/**
|
|
112
|
+
* The minted pixel-plane endpoint (P4.2): the direct-to-provider WS URL + the
|
|
113
|
+
* scoped stream token + its expiry + the framebuffer geometry. Threaded by the
|
|
114
|
+
* API-direct handshake AFTER it has resumed the box, ensured the display stack,
|
|
115
|
+
* and resolved the provider tunnel. When ABSENT (the negotiation-only read, a
|
|
116
|
+
* cold lease, or a degraded desktop) the DesktopStream cell reports url/token/
|
|
117
|
+
* expiresAt as null — the capability is advertised, the live address is not yet
|
|
118
|
+
* minted (the caller POSTs to /viewers to mint it). Presence does NOT override
|
|
119
|
+
* the gates: a degraded/cold/unacked desktop still reports transport:null and
|
|
120
|
+
* the minted endpoint is dropped.
|
|
121
|
+
*/
|
|
122
|
+
desktopStream?: {
|
|
123
|
+
url: string;
|
|
124
|
+
token: string;
|
|
125
|
+
expiresAt: string;
|
|
126
|
+
resolution: [number, number];
|
|
127
|
+
};
|
|
128
|
+
/** The deployment terminal toggle (settings.sandboxTerminalEnabled). The REAL
|
|
129
|
+
* PTY (ttyd pty-ws) is gated on this + a real-PTY backend; when off the
|
|
130
|
+
* Terminal cell still advertises the read-only sse-events firehose. Defaults to
|
|
131
|
+
* true so a caller that never threads it is unaffected. */
|
|
132
|
+
terminalEnabled?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* The minted terminal-plane endpoint (P5.t): the direct-to-provider ttyd
|
|
135
|
+
* PTY-over-websocket URL + the scoped stream token + its expiry. Threaded by the
|
|
136
|
+
* API-direct handshake AFTER it has resumed the box, ensured the terminal
|
|
137
|
+
* server, and resolved the provider tunnel (mintTerminalStream) — SYMMETRIC with
|
|
138
|
+
* `desktopStream`. When ABSENT (the negotiation-only read, a cold lease, or a
|
|
139
|
+
* degraded terminal) the Terminal cell reports url/token/expiresAt as null and
|
|
140
|
+
* falls back to transport "sse-events" (the read-only firehose) — the caller
|
|
141
|
+
* POSTs to /viewers to mint the live pty-ws address.
|
|
142
|
+
*/
|
|
143
|
+
terminalStream?: {
|
|
144
|
+
url: string;
|
|
145
|
+
token: string;
|
|
146
|
+
expiresAt: string;
|
|
147
|
+
};
|
|
148
|
+
/** Override the negotiation clock (tests). */
|
|
149
|
+
now?: Date;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Resolve the descriptor for a backend. Throws on an unknown backend rather than
|
|
153
|
+
* returning a half-formed default (the registry is the single source of truth).
|
|
154
|
+
*/
|
|
155
|
+
declare function selectBackend(backend: SandboxBackend): CapabilityDescriptor;
|
|
156
|
+
/** True iff the descriptor lists the requested OS as supported. */
|
|
157
|
+
declare function backendSupportsOs(descriptor: CapabilityDescriptor, os: SandboxOs): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* True iff the backend can serve the Channel-B desktop pixel plane at all — i.e.
|
|
160
|
+
* its static descriptor advertises DesktopStream as available. The gate the
|
|
161
|
+
* worker / API use before launching the display stack (so a headless-only
|
|
162
|
+
* backend like cloudflare/vercel/none never tries). This is the STATIC
|
|
163
|
+
* feasibility only; the runtime `sandboxDesktopEnabled` policy toggle and the
|
|
164
|
+
* stream-token-secret gate are layered on by the caller / negotiateCapabilities.
|
|
165
|
+
*
|
|
166
|
+
* Accepts EITHER the SandboxBackend enum value (e.g. "local") OR the SDK
|
|
167
|
+
* client backendId (e.g. "unix_local") — they diverge for the local backend —
|
|
168
|
+
* so a caller holding only `established.backendId` resolves correctly.
|
|
169
|
+
*/
|
|
170
|
+
declare function desktopCapableBackend(backend: SandboxBackend | string): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Negotiate a coherent SessionCapabilities document for (backend, os). Every
|
|
173
|
+
* capability is reported with availability + a reason-when-unavailable; nothing
|
|
174
|
+
* is ever absent. The reason precedence is: os_unsupported (the OS axis can't be
|
|
175
|
+
* served at all) > the per-capability static feasibility > policy/liveness gates.
|
|
176
|
+
*/
|
|
177
|
+
declare function negotiateCapabilities(ctx: NegotiationContext): SessionCapabilities;
|
|
178
|
+
|
|
179
|
+
declare const STREAM_TOKEN_DEFAULT_TTL_SECONDS = 120;
|
|
180
|
+
type MintStreamTokenInput = {
|
|
181
|
+
workspaceId: string;
|
|
182
|
+
sessionId: string;
|
|
183
|
+
/** The sandbox_lease_holders viewer row id. */
|
|
184
|
+
viewerId: string;
|
|
185
|
+
/** The epoch the token is fenced to. For a Modal box this is the live LEASE
|
|
186
|
+
* epoch (re-minted on box rollover). For a SELFHOSTED relay stream (M8b) this is
|
|
187
|
+
* the session's swap `active_epoch`: the relay tracks the highest epoch any
|
|
188
|
+
* viewer presented per channel and REJECTS a token with a lower epoch, so a
|
|
189
|
+
* viewer whose token predates a swap-away cannot reach the machine the session
|
|
190
|
+
* swapped off of. One field, two fences — the relay/in-box edge reads it as the
|
|
191
|
+
* stale-viewer floor either way. */
|
|
192
|
+
leaseEpoch: number;
|
|
193
|
+
/** v1 is always "view"; "control" is the never-granted raw-input plane. */
|
|
194
|
+
mode?: "view" | "control";
|
|
195
|
+
/** The exposed stream port (noVNC); defaults to 6080. */
|
|
196
|
+
port?: number;
|
|
197
|
+
/** TTL in seconds; defaults to STREAM_TOKEN_DEFAULT_TTL_SECONDS. */
|
|
198
|
+
ttlSeconds?: number;
|
|
199
|
+
/** Override the issue clock (tests). Seconds since the epoch. */
|
|
200
|
+
nowSeconds?: number;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Mint a scoped stream token for one viewer holder. Builds the hard-narrow
|
|
204
|
+
* StreamTokenPayload (the claim set the in-box edge / control plane validates)
|
|
205
|
+
* and signs it with the resolved stream-token secret via the contracts HMAC
|
|
206
|
+
* envelope (`ogs_` prefix). The token is RECORDED against the holder row by the
|
|
207
|
+
* caller and is NEVER appended to the data-plane URL as a query param.
|
|
208
|
+
*/
|
|
209
|
+
declare function mintStreamToken(secret: string, input: MintStreamTokenInput): Promise<string>;
|
|
210
|
+
/**
|
|
211
|
+
* Verify a scoped stream token. Returns the parsed claims on success, or null on
|
|
212
|
+
* a bad prefix / malformed envelope / bad HMAC signature / schema-invalid claims
|
|
213
|
+
* / expiry. Re-exports the contracts verify; the leaf is the agent-loop-free
|
|
214
|
+
* import surface the API uses.
|
|
215
|
+
*
|
|
216
|
+
* The epoch fence (claim.leaseEpoch vs the LIVE lease epoch) and the
|
|
217
|
+
* workspace+session scope are enforced at USE by the caller against the live
|
|
218
|
+
* lease + route params — verify proves authenticity + freshness only.
|
|
219
|
+
*/
|
|
220
|
+
declare function verifyStreamToken(secret: string, token: string, nowSeconds?: number): Promise<StreamTokenPayload | null>;
|
|
221
|
+
|
|
222
|
+
declare const STREAM_PORT = 6080;
|
|
223
|
+
declare const DISPLAY_STACK_TIMEOUT_MS = 60000;
|
|
224
|
+
/** Desktop geometry for the framebuffer. v1 has no live RANDR: a resolution
|
|
225
|
+
* change is a full down -> up restart (a separate op). */
|
|
226
|
+
type DesktopGeometry = {
|
|
227
|
+
width: number;
|
|
228
|
+
height: number;
|
|
229
|
+
dpi: number;
|
|
230
|
+
};
|
|
231
|
+
declare const DEFAULT_DESKTOP_GEOMETRY: DesktopGeometry;
|
|
232
|
+
/** Thrown when a stage of the launch script failed. exitCode 11/12/13 map to
|
|
233
|
+
* Xvfb / x11vnc / websockify respectively (the stage that died). Degradation is
|
|
234
|
+
* surfaced as a value to viewers by the caller; this error is for diagnostics. */
|
|
235
|
+
declare class DisplayStackError extends Error {
|
|
236
|
+
readonly exitCode: number;
|
|
237
|
+
readonly stage: "xvfb" | "x11vnc" | "websockify" | "unknown";
|
|
238
|
+
constructor(exitCode: number, output: string);
|
|
239
|
+
}
|
|
240
|
+
/** Thrown when the provider session cannot run commands (a headless-only
|
|
241
|
+
* backend with neither `exec` nor `execCommand`). The desktop tier degrades to
|
|
242
|
+
* Channel-A-only — the caller maps this to `DesktopStream.transport: null`. */
|
|
243
|
+
declare class DisplayStackUnsupportedError extends Error {
|
|
244
|
+
constructor(message: string);
|
|
245
|
+
}
|
|
246
|
+
type EnsureDisplayStackOptions = {
|
|
247
|
+
geometry?: DesktopGeometry;
|
|
248
|
+
/** The exposed stream port; defaults to 6080. */
|
|
249
|
+
port?: number;
|
|
250
|
+
/** Per-exec timeout; defaults to DISPLAY_STACK_TIMEOUT_MS. */
|
|
251
|
+
timeoutMs?: number;
|
|
252
|
+
};
|
|
253
|
+
type EnsureDisplayStackResult = {
|
|
254
|
+
/** The exposed port the stack listens on (websockify/noVNC). */
|
|
255
|
+
port: number;
|
|
256
|
+
geometry: DesktopGeometry;
|
|
257
|
+
/** The raw `OPENGENI_DESKTOP_UP …` marker line, for diagnostics. Never
|
|
258
|
+
* surfaced to viewers. */
|
|
259
|
+
marker: string;
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Build the shell command that runs the idempotent up-script under an in-box
|
|
263
|
+
* `flock`. The script is shipped in the image at /usr/local/bin/opengeni-desktop-up
|
|
264
|
+
* (the canonical desktop image); we set the geometry/port env and wrap the call
|
|
265
|
+
* in `flock` so two concurrent ensureDisplayStack callers (the API viewer op +
|
|
266
|
+
* the agent turn, both racing after a rollover) serialize without a double
|
|
267
|
+
* launch. The up-script's own per-stage PID guards make the second call a no-op.
|
|
268
|
+
*
|
|
269
|
+
* Exported (pure, side-effect-free) so the ensureDisplayStack unit test can
|
|
270
|
+
* assert the exact command sequence without a live box.
|
|
271
|
+
*/
|
|
272
|
+
declare function buildDisplayStackScript(options?: EnsureDisplayStackOptions): string;
|
|
273
|
+
/**
|
|
274
|
+
* Idempotently bring up the desktop display stack on the live box. Safe to call
|
|
275
|
+
* N times (the in-box flock + the up-script's PID guards make a second call a
|
|
276
|
+
* no-op). Resolves with the exposed port + geometry on success; throws
|
|
277
|
+
* `DisplayStackError` on a stage failure and `DisplayStackUnsupportedError` when
|
|
278
|
+
* the session cannot run commands.
|
|
279
|
+
*
|
|
280
|
+
* `session` is the externally-owned provider session (the `established.session`
|
|
281
|
+
* from establishSandboxSessionFromEnvelope, or any SandboxSessionLike). We
|
|
282
|
+
* prefer `session.exec` (structured `{exitCode}`) and fall back to
|
|
283
|
+
* `session.execCommand` (bare string), inferring success from the up-script's
|
|
284
|
+
* marker line in the fallback case.
|
|
285
|
+
*/
|
|
286
|
+
declare function ensureDisplayStack(session: unknown, options?: EnsureDisplayStackOptions): Promise<EnsureDisplayStackResult>;
|
|
287
|
+
/** Tear the stack down (down-script). Best-effort; never throws on a missing
|
|
288
|
+
* process. Used by the geometry-change restart and cold/drain. */
|
|
289
|
+
declare function tearDownDisplayStack(session: unknown): Promise<void>;
|
|
290
|
+
|
|
291
|
+
declare const TERMINAL_SERVER_TIMEOUT_MS = 60000;
|
|
292
|
+
/** Thrown when the ttyd launch failed inside the box. exitCode 14 maps to the
|
|
293
|
+
* up-script's "ttyd failed to come up" stage; any other non-zero is unknown.
|
|
294
|
+
* Degradation is surfaced as a value to clients by the caller (Terminal
|
|
295
|
+
* transport falls back to sse-events / null); this error is for diagnostics. */
|
|
296
|
+
declare class TerminalServerError extends Error {
|
|
297
|
+
readonly exitCode: number;
|
|
298
|
+
readonly stage: "ttyd" | "unknown";
|
|
299
|
+
constructor(exitCode: number, output: string);
|
|
300
|
+
}
|
|
301
|
+
/** Thrown when the provider session cannot run commands (a headless-only backend
|
|
302
|
+
* with neither `exec` nor `execCommand`). The terminal tier degrades to the
|
|
303
|
+
* Channel-A sse-events firehose — the caller maps this to a `transport:null`
|
|
304
|
+
* pty-ws (the read-only firehose still works). */
|
|
305
|
+
declare class TerminalServerUnsupportedError extends Error {
|
|
306
|
+
constructor(message: string);
|
|
307
|
+
}
|
|
308
|
+
type EnsureTerminalServerOptions = {
|
|
309
|
+
/** The exposed terminal port; defaults to 7681 (ttyd default). */
|
|
310
|
+
port?: number;
|
|
311
|
+
/** Per-exec timeout; defaults to TERMINAL_SERVER_TIMEOUT_MS. */
|
|
312
|
+
timeoutMs?: number;
|
|
313
|
+
};
|
|
314
|
+
type EnsureTerminalServerResult = {
|
|
315
|
+
/** The exposed port ttyd listens on (PTY-over-websocket). */
|
|
316
|
+
port: number;
|
|
317
|
+
/** The raw `OPENGENI_TERMINAL_UP …` marker line, for diagnostics. Never
|
|
318
|
+
* surfaced to clients. */
|
|
319
|
+
marker: string;
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Build the shell command that runs the idempotent up-script under an in-box
|
|
323
|
+
* `flock`. The script is shipped in the image at /usr/local/bin/opengeni-terminal-up
|
|
324
|
+
* (the canonical desktop image, alongside opengeni-desktop-up); we set the port
|
|
325
|
+
* env and wrap the call in `flock` so two concurrent ensureTerminalServer callers
|
|
326
|
+
* (the API viewer op + the agent turn, both racing after a rollover) serialize
|
|
327
|
+
* without a double launch. The up-script's own curl readiness probe makes the
|
|
328
|
+
* second call a no-op.
|
|
329
|
+
*
|
|
330
|
+
* Exported (pure, side-effect-free) so the ensureTerminalServer unit test can
|
|
331
|
+
* assert the exact command sequence without a live box. Mirrors
|
|
332
|
+
* buildDisplayStackScript.
|
|
333
|
+
*/
|
|
334
|
+
declare function buildTerminalServerScript(options?: EnsureTerminalServerOptions): string;
|
|
335
|
+
/**
|
|
336
|
+
* Idempotently bring up the ttyd PTY-over-websocket server on the live box. Safe
|
|
337
|
+
* to call N times (the in-box flock + the up-script's curl readiness probe make a
|
|
338
|
+
* second call a no-op). Resolves with the exposed port on success; throws
|
|
339
|
+
* `TerminalServerError` on a launch failure and `TerminalServerUnsupportedError`
|
|
340
|
+
* when the session cannot run commands.
|
|
341
|
+
*
|
|
342
|
+
* `session` is the externally-owned provider session (the `established.session`
|
|
343
|
+
* from establishSandboxSessionFromEnvelope, or any SandboxSessionLike). We prefer
|
|
344
|
+
* `session.exec` (structured `{exitCode}`) and fall back to `session.execCommand`
|
|
345
|
+
* (bare string), inferring success from the up-script's marker line in the
|
|
346
|
+
* fallback case. Mirrors ensureDisplayStack exactly.
|
|
347
|
+
*/
|
|
348
|
+
declare function ensureTerminalServer(session: unknown, options?: EnsureTerminalServerOptions): Promise<EnsureTerminalServerResult>;
|
|
349
|
+
/** Tear the terminal server down (down-script). Best-effort; never throws on a
|
|
350
|
+
* missing process. Mirrors tearDownDisplayStack. */
|
|
351
|
+
declare function tearDownTerminalServer(session: unknown): Promise<void>;
|
|
352
|
+
|
|
353
|
+
/** The provider-resolved endpoint for an exposed port. Mirrors the SDK's
|
|
354
|
+
* `ExposedPortEndpoint` (host/port/tls/query/...) WITHOUT importing the
|
|
355
|
+
* agent-loop barrel — the leaf stays agent-loop-free. */
|
|
356
|
+
type ExposedPortEndpoint = {
|
|
357
|
+
host: string;
|
|
358
|
+
port: number;
|
|
359
|
+
tls?: boolean;
|
|
360
|
+
query?: string;
|
|
361
|
+
protocol?: string;
|
|
362
|
+
url?: string;
|
|
363
|
+
/** The URL path the socket connects on. Modal/Daytona/Blaxel serve the edge at
|
|
364
|
+
* the root (`/`, the default); the selfhosted relay serves it at `/stream`
|
|
365
|
+
* (M8b). When set, buildStreamUrl uses it instead of the root. */
|
|
366
|
+
path?: string;
|
|
367
|
+
[key: string]: unknown;
|
|
368
|
+
};
|
|
369
|
+
/** Thrown when the provider cannot expose the stream port (no resolveExposedPort,
|
|
370
|
+
* or the provider tunnel lookup failed). The caller degrades the desktop cell to
|
|
371
|
+
* `transport:null` (a value, never a crash) — a headless-only provider or a
|
|
372
|
+
* transient tunnel failure must not fail the whole handshake. */
|
|
373
|
+
declare class StreamPortUnavailableError extends Error {
|
|
374
|
+
readonly cause?: unknown | undefined;
|
|
375
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
376
|
+
}
|
|
377
|
+
type ExposeStreamPortInput = {
|
|
378
|
+
workspaceId: string;
|
|
379
|
+
sessionId: string;
|
|
380
|
+
/** The sandbox_lease_holders viewer row id the token is scoped to. */
|
|
381
|
+
viewerId: string;
|
|
382
|
+
/** The live lease epoch — the fence the token is pinned to. */
|
|
383
|
+
leaseEpoch: number;
|
|
384
|
+
/** The HMAC secret for the scoped stream token (resolveStreamTokenSecret). */
|
|
385
|
+
streamTokenSecret: string;
|
|
386
|
+
/** The exposed stream port; defaults to 6080. */
|
|
387
|
+
port?: number;
|
|
388
|
+
/** Token TTL in seconds; defaults to STREAM_TOKEN_DEFAULT_TTL_SECONDS. */
|
|
389
|
+
ttlSeconds?: number;
|
|
390
|
+
/** The framebuffer geometry to echo back to the client. */
|
|
391
|
+
resolution?: [number, number];
|
|
392
|
+
/** Override the issue clock (tests). Seconds since the epoch. */
|
|
393
|
+
nowSeconds?: number;
|
|
394
|
+
};
|
|
395
|
+
type ExposeStreamPortResult = {
|
|
396
|
+
/** The direct-to-provider WS URL the viewer connects to (provider-scoped; the
|
|
397
|
+
* OpenGeni token is NOT appended). */
|
|
398
|
+
url: string;
|
|
399
|
+
/** The scoped OpenGeni stream token — recorded against the holder, NEVER a URL
|
|
400
|
+
* query param. */
|
|
401
|
+
token: string;
|
|
402
|
+
/** ISO absolute expiry of the token (the rotation hot-swap window backstop). */
|
|
403
|
+
expiresAt: string;
|
|
404
|
+
/** The pixel transport the client speaks. */
|
|
405
|
+
transport: "vnc-ws";
|
|
406
|
+
/** The reference noVNC client the SDK helper mounts. */
|
|
407
|
+
client: "novnc";
|
|
408
|
+
resolution: [number, number];
|
|
409
|
+
leaseEpoch: number;
|
|
410
|
+
};
|
|
411
|
+
/**
|
|
412
|
+
* Assemble the direct-to-provider WS URL from a resolved endpoint. The SDK's
|
|
413
|
+
* `urlForExposedPort(endpoint,'ws')` is the canonical tls-aware, IPv6-bracketing,
|
|
414
|
+
* provider-query-preserving assembler — we reimplement its exact logic here so
|
|
415
|
+
* the leaf stays agent-loop-free (the helper lives behind the bare
|
|
416
|
+
* `@openai/agents-core` root, which the import-discipline test forbids). The
|
|
417
|
+
* provider's own `endpoint.query` (Blaxel `bl_preview_token`, Daytona signed
|
|
418
|
+
* token) is preserved; the OpenGeni token is NOT appended (it is recorded against
|
|
419
|
+
* the holder + validated at the in-box websockify edge).
|
|
420
|
+
*/
|
|
421
|
+
declare function buildStreamUrl(endpoint: ExposedPortEndpoint): string;
|
|
422
|
+
/**
|
|
423
|
+
* Resolve the provider's scoped tunnel for the stream port and mint the scoped
|
|
424
|
+
* OpenGeni stream token. Returns a coherent `{url, token, expiresAt, transport,
|
|
425
|
+
* client, resolution}` cell the caller records on the lease (data_plane_url) and
|
|
426
|
+
* returns in the DesktopStream handshake.
|
|
427
|
+
*
|
|
428
|
+
* Throws `StreamPortUnavailableError` when the provider session cannot resolve
|
|
429
|
+
* the port (no `resolveExposedPort`, or the tunnel lookup failed) — the caller
|
|
430
|
+
* maps this to a `transport:null` degradation (a value, never a crash).
|
|
431
|
+
*/
|
|
432
|
+
declare function exposeStreamPort(session: unknown, input: ExposeStreamPortInput): Promise<ExposeStreamPortResult>;
|
|
433
|
+
|
|
434
|
+
type RecordingCodec = "h264-mp4" | "vp9-webm";
|
|
435
|
+
type RecordingContentType = "video/mp4" | "video/webm";
|
|
436
|
+
declare function contentTypeForCodec(codec: RecordingCodec): RecordingContentType;
|
|
437
|
+
declare function extForCodec(codec: RecordingCodec): string;
|
|
438
|
+
/** No exec/execCommand on the session — the box cannot run ffmpeg. */
|
|
439
|
+
declare class RecordingUnavailableError extends Error {
|
|
440
|
+
constructor(message: string);
|
|
441
|
+
}
|
|
442
|
+
/** ffmpeg failed, the file is missing, or the byte read failed. */
|
|
443
|
+
declare class RecordingError extends Error {
|
|
444
|
+
readonly reason: "ffmpeg-error" | "box-death" | "max-bytes-exceeded" | "display-unavailable";
|
|
445
|
+
constructor(message: string, reason: "ffmpeg-error" | "box-death" | "max-bytes-exceeded" | "display-unavailable");
|
|
446
|
+
}
|
|
447
|
+
type StartRecordingInput = {
|
|
448
|
+
recordingId: string;
|
|
449
|
+
codec?: RecordingCodec;
|
|
450
|
+
framerate?: number;
|
|
451
|
+
maxSeconds?: number;
|
|
452
|
+
dimensions?: [number, number];
|
|
453
|
+
display?: string;
|
|
454
|
+
runAs?: string;
|
|
455
|
+
tmpDir?: string;
|
|
456
|
+
};
|
|
457
|
+
type RecordingProcess = {
|
|
458
|
+
recordingId: string;
|
|
459
|
+
codec: RecordingCodec;
|
|
460
|
+
boxPath: string;
|
|
461
|
+
pidFile: string;
|
|
462
|
+
dimensions: [number, number];
|
|
463
|
+
framerate: number;
|
|
464
|
+
/** epoch-ms when ffmpeg was launched (for duration computation, F14). */
|
|
465
|
+
startedAt: number;
|
|
466
|
+
display: string;
|
|
467
|
+
runAs?: string;
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* Launch ffmpeg x11grab on :0 → an mp4/webm file on the box. Backgrounded with
|
|
471
|
+
* `nohup … & echo $!` so the launch returns immediately (F12 — the exec does not
|
|
472
|
+
* block on the recording). A hard `-t <maxSeconds>` ceiling bounds a runaway file
|
|
473
|
+
* across a multi-day turn. Returns the handle the caller carries to stop+finalize.
|
|
474
|
+
*/
|
|
475
|
+
declare function startRecording(session: unknown, input: StartRecordingInput): Promise<RecordingProcess>;
|
|
476
|
+
/**
|
|
477
|
+
* SIGINT ffmpeg (so it writes a clean moov atom / webm trailer) and wait for the
|
|
478
|
+
* pid to exit. Bounded well under the yield window (F3). Idempotent: a missing
|
|
479
|
+
* pid file is a no-op.
|
|
480
|
+
*/
|
|
481
|
+
declare function stopRecording(session: unknown, proc: RecordingProcess): Promise<void>;
|
|
482
|
+
type FinalizeRecordingResult = {
|
|
483
|
+
bytes: Uint8Array;
|
|
484
|
+
contentType: RecordingContentType;
|
|
485
|
+
sizeBytes: number;
|
|
486
|
+
durationSeconds: number;
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* Read the finalized recording bytes off the box.
|
|
490
|
+
*
|
|
491
|
+
* TRANSPORT: the bytes are read via a DIRECT exec (`base64 <path>` over stdout),
|
|
492
|
+
* NOT via session.readFile(). The recording artifact lives at an absolute /tmp
|
|
493
|
+
* path on purpose — recordings must never be written inside the user's workspace
|
|
494
|
+
* /git tree — but session.readFile() resolves every path against the manifest
|
|
495
|
+
* workspace root and rejects anything outside it ("Sandbox path … escapes the
|
|
496
|
+
* workspace root"), which fataled finalize. Raw exec runs unrestricted shell, so
|
|
497
|
+
* `base64` reads the /tmp file directly; we decode the base64 back to bytes here.
|
|
498
|
+
* The byte-read exec passes `maxOutputTokens: null` so the provider never
|
|
499
|
+
* truncates a large recording's base64.
|
|
500
|
+
*
|
|
501
|
+
* F8: we DO NOT assume any over-limit behavior. First `stat` the file size on the
|
|
502
|
+
* box; if it exceeds maxBytes, fail `max-bytes-exceeded` (never upload a truncated
|
|
503
|
+
* video). Otherwise read the raw bytes.
|
|
504
|
+
*
|
|
505
|
+
* F9: this does NOT delete the box file. The caller deletes it (deleteRecordingArtifacts)
|
|
506
|
+
* ONLY after the storage PUT + `available` commit — so a failed upload leaves the
|
|
507
|
+
* bytes recoverable on the box for a retry.
|
|
508
|
+
*
|
|
509
|
+
* F14: duration is wall-clock (now − startedAt), a close approximation of the
|
|
510
|
+
* SIGINT-flushed video length.
|
|
511
|
+
*/
|
|
512
|
+
declare function readRecordingBytes(session: unknown, proc: RecordingProcess, maxBytes?: number): Promise<FinalizeRecordingResult>;
|
|
513
|
+
/**
|
|
514
|
+
* Delete the box artifacts. F9: call this ONLY after the storage PUT confirmed
|
|
515
|
+
* and the `available` row committed — never before. Best-effort; never throws.
|
|
516
|
+
*/
|
|
517
|
+
declare function deleteRecordingArtifacts(session: unknown, proc: RecordingProcess): Promise<void>;
|
|
518
|
+
/** The storage object key for a recording artifact (parallels the file-asset layout). */
|
|
519
|
+
declare function recordingStorageKey(workspaceId: string, sessionId: string, recordingId: string, codec: RecordingCodec): string;
|
|
520
|
+
|
|
521
|
+
type ChannelAExecResult = {
|
|
522
|
+
output?: string;
|
|
523
|
+
stdout?: string;
|
|
524
|
+
stderr?: string;
|
|
525
|
+
exitCode?: number | null;
|
|
526
|
+
sessionId?: number;
|
|
527
|
+
wallTimeSeconds?: number;
|
|
528
|
+
};
|
|
529
|
+
type ChannelAExecArgs = {
|
|
530
|
+
cmd: string;
|
|
531
|
+
workdir?: string | undefined;
|
|
532
|
+
shell?: string | undefined;
|
|
533
|
+
login?: boolean | undefined;
|
|
534
|
+
tty?: boolean | undefined;
|
|
535
|
+
yieldTimeMs?: number | undefined;
|
|
536
|
+
maxOutputTokens?: number | undefined;
|
|
537
|
+
runAs?: string | undefined;
|
|
538
|
+
};
|
|
539
|
+
type ChannelAEditor = {
|
|
540
|
+
createFile?(op: unknown): Promise<unknown>;
|
|
541
|
+
updateFile?(op: unknown): Promise<unknown>;
|
|
542
|
+
deleteFile?(op: unknown): Promise<unknown>;
|
|
543
|
+
};
|
|
544
|
+
type ChannelASession = {
|
|
545
|
+
exec?(args: ChannelAExecArgs): Promise<ChannelAExecResult>;
|
|
546
|
+
execCommand?(args: ChannelAExecArgs): Promise<string>;
|
|
547
|
+
readFile?(args: {
|
|
548
|
+
path: string;
|
|
549
|
+
runAs?: string;
|
|
550
|
+
maxBytes?: number;
|
|
551
|
+
}): Promise<string | Uint8Array>;
|
|
552
|
+
writeStdin?(args: {
|
|
553
|
+
sessionId: number;
|
|
554
|
+
chars?: string;
|
|
555
|
+
yieldTimeMs?: number;
|
|
556
|
+
maxOutputTokens?: number;
|
|
557
|
+
}): Promise<string>;
|
|
558
|
+
createEditor?(runAs?: string): ChannelAEditor;
|
|
559
|
+
supportsPty?(): boolean;
|
|
560
|
+
};
|
|
561
|
+
declare class ChannelAValidationError extends Error {
|
|
562
|
+
constructor(message: string);
|
|
563
|
+
}
|
|
564
|
+
declare class ChannelAConflictError extends Error {
|
|
565
|
+
constructor(message: string);
|
|
566
|
+
}
|
|
567
|
+
declare class ChannelANotFoundError extends Error {
|
|
568
|
+
constructor(message: string);
|
|
569
|
+
}
|
|
570
|
+
declare class ChannelAUnsupportedError extends Error {
|
|
571
|
+
constructor(message: string);
|
|
572
|
+
}
|
|
573
|
+
type ChannelAEmitter = (events: {
|
|
574
|
+
type: SessionEventType;
|
|
575
|
+
payload: unknown;
|
|
576
|
+
}[]) => Promise<void>;
|
|
577
|
+
type SandboxChannelAServiceOptions = {
|
|
578
|
+
session: ChannelASession;
|
|
579
|
+
workspaceRoot?: string;
|
|
580
|
+
leaseEpoch?: number;
|
|
581
|
+
revision?: number;
|
|
582
|
+
emit?: ChannelAEmitter;
|
|
583
|
+
runAs?: string;
|
|
584
|
+
};
|
|
585
|
+
declare class SandboxChannelAService {
|
|
586
|
+
private readonly session;
|
|
587
|
+
private readonly workspaceRoot;
|
|
588
|
+
private readonly leaseEpoch;
|
|
589
|
+
private revision;
|
|
590
|
+
private readonly emit?;
|
|
591
|
+
private readonly runAs?;
|
|
592
|
+
constructor(opts: SandboxChannelAServiceOptions);
|
|
593
|
+
/** Capability probe — the compact Channel-A projection. */
|
|
594
|
+
capabilities(repos?: string[]): SessionStructuredCapabilities;
|
|
595
|
+
private run;
|
|
596
|
+
fsList(req: FsListRequest): Promise<FsListResponse>;
|
|
597
|
+
fsRead(req: FsReadRequest): Promise<FsReadResponse>;
|
|
598
|
+
/** Read a file by base64-ing it through exec. Binary-safe and — crucially —
|
|
599
|
+
* NOT subject to the provider's native-readFile workspace-escape validation,
|
|
600
|
+
* so it can render a symlink whose target lives outside /workspace (the link
|
|
601
|
+
* node itself is in-workspace). `base64 <path>` follows the symlink. */
|
|
602
|
+
private fsReadViaExec;
|
|
603
|
+
private shapeRead;
|
|
604
|
+
fsWrite(req: FsWriteRequest): Promise<FsWriteResponse>;
|
|
605
|
+
private tryEditorWrite;
|
|
606
|
+
fsDelete(req: FsDeleteRequest): Promise<FsDeleteResponse>;
|
|
607
|
+
fsMove(req: FsMoveRequest): Promise<FsMoveResponse>;
|
|
608
|
+
fsMkdir(req: FsMkdirRequest): Promise<FsMkdirResponse>;
|
|
609
|
+
gitStatus(req: GitStatusRequest): Promise<GitStatusResponse>;
|
|
610
|
+
gitDiff(req: GitDiffRequest): Promise<GitDiffResponse>;
|
|
611
|
+
gitLog(req: GitLogRequest): Promise<GitLogResponse>;
|
|
612
|
+
gitShow(req: GitShowRequest): Promise<GitShowResponse>;
|
|
613
|
+
/** Detect repo roots within the workspace (for the Git.repos capability). */
|
|
614
|
+
detectRepos(): Promise<string[]>;
|
|
615
|
+
/** Run a bounded command, return buffered stdout/stderr + exit code inline. The
|
|
616
|
+
* long-running tail (when the process hasn't exited within timeoutMs) keeps
|
|
617
|
+
* running in-box; if emitStream is set the buffered output is also published as
|
|
618
|
+
* the agent firehose so other viewers see it. */
|
|
619
|
+
terminalExec(req: TerminalExecRequest): Promise<TerminalExecResponse>;
|
|
620
|
+
/** Open an interactive PTY: exec the shell with tty:true, yielding the numeric
|
|
621
|
+
* exec-session id the caller persists (ptyId<->execSessionId) so subsequent
|
|
622
|
+
* writeStdin can drive it. Returns the supportsInput gate (false when the
|
|
623
|
+
* backend has no writeStdin). The caller emits terminal.pty.started after it
|
|
624
|
+
* persists the row. */
|
|
625
|
+
ptyOpen(req: PtyOpenRequest, ptyId: string): Promise<{
|
|
626
|
+
response: PtyOpenResponse;
|
|
627
|
+
execSessionId: number | null;
|
|
628
|
+
shell: string;
|
|
629
|
+
initialOutput: string;
|
|
630
|
+
}>;
|
|
631
|
+
/** Drive an open PTY's stdin. Returns the drained output (the caller publishes
|
|
632
|
+
* it as terminal.pty.output.delta). Throws ChannelAUnsupportedError when the
|
|
633
|
+
* backend has no writeStdin. */
|
|
634
|
+
ptyWrite(_req: PtyWriteRequest, execSessionId: number, data: string): Promise<string>;
|
|
635
|
+
/** Resize an open PTY (SIGWINCH via stty against the exec-session). The SDK has
|
|
636
|
+
* no resize method; stty in the same tty session updates the geometry. */
|
|
637
|
+
ptyResize(req: PtyResizeRequest, execSessionId: number): Promise<void>;
|
|
638
|
+
/** Close an open PTY: write exit/EOF. The caller marks the row closed + emits
|
|
639
|
+
* terminal.pty.exited. */
|
|
640
|
+
ptyClose(_req: PtyCloseRequest, execSessionId: number | null): Promise<void>;
|
|
641
|
+
/** The current FS revision (for the caller to persist/seed). */
|
|
642
|
+
currentRevision(): number;
|
|
643
|
+
private joinRoot;
|
|
644
|
+
private repoWorkdir;
|
|
645
|
+
private emitEvents;
|
|
646
|
+
private emitFsChanged;
|
|
647
|
+
/** Re-probe git after a mutation and emit git.changed (best-effort, used by the
|
|
648
|
+
* worker agent-turn side after FS-mutating tools). */
|
|
649
|
+
emitGitChanged(repoPath: string, reason: GitChangedPayload["reason"]): Promise<void>;
|
|
650
|
+
}
|
|
651
|
+
declare function stripExecBanner(raw: string): string;
|
|
652
|
+
declare function isWorkspaceEscapeError(error: unknown): boolean;
|
|
653
|
+
declare function isExecSessionLostBanner(out: string, execSessionId: number): boolean;
|
|
654
|
+
declare function parseExecBannerSessionId(raw: string): number | null;
|
|
655
|
+
declare function assertSafeRelPath(p: string): string;
|
|
656
|
+
declare function parsePorcelainV2(z: string): Omit<GitStatusResponse, "revision">;
|
|
657
|
+
type NumstatEntry = {
|
|
658
|
+
additions: number;
|
|
659
|
+
deletions: number;
|
|
660
|
+
binary: boolean;
|
|
661
|
+
oldPath: string | null;
|
|
662
|
+
newPath: string;
|
|
663
|
+
};
|
|
664
|
+
declare function parseNumstatZ(z: string): NumstatEntry[];
|
|
665
|
+
declare function parseUnifiedPatch(patch: string): {
|
|
666
|
+
hunks: GitDiffHunk[];
|
|
667
|
+
status: GitFileStatusCode;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
type SelfhostedUnavailableReason = Extract<CapabilityUnavailableReason, "agent_offline" | "agent_reconnecting" | "consent_required" | "display_unavailable">;
|
|
671
|
+
/**
|
|
672
|
+
* The selfhosted control-plane transport seam. ONE method: `request` — send a
|
|
673
|
+
* `ControlRequest` to the agent addressed by subject and await its
|
|
674
|
+
* `ControlResponse`. The subject is `subjectFor(workspaceId, agentId)`.
|
|
675
|
+
*
|
|
676
|
+
* The CONTRACT every implementor MUST honour (the M3 ruling): a
|
|
677
|
+
* no-responder / request-timeout is NOT an exception that means "not found" — it
|
|
678
|
+
* is surfaced as a `ControlResponse` carrying an `AgentError` with code
|
|
679
|
+
* `AGENT_OFFLINE` (no responder at all) or, when the caller can distinguish a
|
|
680
|
+
* transient blip, `TIMEOUT` (→ `agent_reconnecting`). The session maps these to
|
|
681
|
+
* the runtime error taxonomy; it NEVER lets agent-offline look like a provider
|
|
682
|
+
* NotFound (which would cold-create a rival box for a user's real machine).
|
|
683
|
+
*/
|
|
684
|
+
interface ControlRpc {
|
|
685
|
+
request(subject: string, req: ControlRequest, opts: {
|
|
686
|
+
timeoutMs: number;
|
|
687
|
+
}): Promise<ControlResponse>;
|
|
688
|
+
}
|
|
689
|
+
/** The control-plane RPC subject for an enrolled agent — its subscription IS the
|
|
690
|
+
* registry (the binding two-plane decision). */
|
|
691
|
+
declare function subjectFor(workspaceId: string, agentId: string): string;
|
|
692
|
+
/**
|
|
693
|
+
* The runtime-level error a `SelfhostedSession` op throws when the agent returns
|
|
694
|
+
* an `AgentError` (or no responder / timeout maps to one). It carries:
|
|
695
|
+
* - `code` — the wire `ErrorCode` (single-source-of-truth);
|
|
696
|
+
* - `reason` — the negotiated `CapabilityUnavailableReason` the capability /
|
|
697
|
+
* liveness surface uses (`agent_offline` / `agent_reconnecting`
|
|
698
|
+
* / `consent_required`), or null for op-level errors
|
|
699
|
+
* (OS/NOT_FOUND/UNSUPPORTED/STREAM/PROTOCOL) that are not a
|
|
700
|
+
* machine-liveness condition;
|
|
701
|
+
* - `retryable`— whether the caller should re-resolve + retry (DRAINING /
|
|
702
|
+
* FENCED / a reconnecting blip);
|
|
703
|
+
* - `notFound` — ALWAYS the provider-NotFound discriminator value: for
|
|
704
|
+
* selfhosted this is true ONLY for an OS-level NOT_FOUND of a
|
|
705
|
+
* path/ref (a real "the file does not exist"), and is FALSE for
|
|
706
|
+
* AGENT_OFFLINE (the machine isn't recreatable — never let the
|
|
707
|
+
* lease cold-create a rival). `isProviderSandboxNotFoundError`
|
|
708
|
+
* reads this.
|
|
709
|
+
*/
|
|
710
|
+
declare class SelfhostedControlError extends Error {
|
|
711
|
+
readonly name = "SelfhostedControlError";
|
|
712
|
+
readonly code: ErrorCode;
|
|
713
|
+
readonly reason: SelfhostedUnavailableReason | null;
|
|
714
|
+
readonly retryable: boolean;
|
|
715
|
+
readonly fenced: boolean;
|
|
716
|
+
readonly draining: boolean;
|
|
717
|
+
readonly agentOffline: boolean;
|
|
718
|
+
readonly osNotFound: boolean;
|
|
719
|
+
readonly detail: Record<string, string>;
|
|
720
|
+
constructor(input: {
|
|
721
|
+
message: string;
|
|
722
|
+
code: ErrorCode;
|
|
723
|
+
reason: SelfhostedUnavailableReason | null;
|
|
724
|
+
retryable: boolean;
|
|
725
|
+
fenced?: boolean;
|
|
726
|
+
draining?: boolean;
|
|
727
|
+
agentOffline?: boolean;
|
|
728
|
+
osNotFound?: boolean;
|
|
729
|
+
detail?: Record<string, string>;
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Map an `AgentError` (from a `ControlResponse`) to the runtime
|
|
734
|
+
* `SelfhostedControlError`. THE load-bearing mapping (the M3 ruling):
|
|
735
|
+
* - AGENT_OFFLINE → reason `agent_offline`, agentOffline=true,
|
|
736
|
+
* osNotFound=FALSE (NEVER a provider NotFound).
|
|
737
|
+
* - TIMEOUT (a transient missed-window / no-responder blip the caller marked
|
|
738
|
+
* retryable) → reason `agent_reconnecting`.
|
|
739
|
+
* - CONSENT_REQUIRED → reason `consent_required`.
|
|
740
|
+
* - DRAINING → no capability reason; retryable (turn pauses + retries).
|
|
741
|
+
* - FENCED → no capability reason; retryable (the existing
|
|
742
|
+
* epoch-fence retry; the caller re-resolves + retries).
|
|
743
|
+
* - NOT_FOUND → an OS-level path/ref NotFound — osNotFound=true (a
|
|
744
|
+
* real "file does not exist"), no machine-liveness
|
|
745
|
+
* reason. (This is the ONLY NotFound; it is NOT the
|
|
746
|
+
* box-gone NotFound that licenses a cold restore.)
|
|
747
|
+
* - OS / UNSUPPORTED / STREAM / PROTOCOL / UNSPECIFIED → op-level error, no
|
|
748
|
+
* reason, non-retryable.
|
|
749
|
+
*/
|
|
750
|
+
declare function agentErrorToControlError(err: AgentError): SelfhostedControlError;
|
|
751
|
+
/** Build a synthesized AGENT_OFFLINE `AgentError` — the control plane uses this
|
|
752
|
+
* when no agent responds on the subject at all. */
|
|
753
|
+
declare function offlineAgentError(message?: string): AgentError;
|
|
754
|
+
/** Build a synthesized TIMEOUT `AgentError` — the control plane uses this when a
|
|
755
|
+
* responder existed but the request timed out (a transient blip → reconnecting). */
|
|
756
|
+
declare function timeoutAgentError(message?: string): AgentError;
|
|
757
|
+
/**
|
|
758
|
+
* The minimal NATS request/reply surface `NatsControlRpc` needs. It mirrors the
|
|
759
|
+
* `nats` `NatsConnection.request` signature WITHOUT importing `nats` into the
|
|
760
|
+
* agent-loop-free runtime leaf: the API/worker injects the live connection (the
|
|
761
|
+
* SAME `@opengeni/events` bus connection). A factory may return `null` when NATS
|
|
762
|
+
* is not configured (boot must not require a live NATS) — `NatsControlRpc` then
|
|
763
|
+
* surfaces `agent_offline` for every request rather than throwing.
|
|
764
|
+
*/
|
|
765
|
+
interface NatsRequestConnection {
|
|
766
|
+
request(subject: string, payload: Uint8Array, opts: {
|
|
767
|
+
timeout: number;
|
|
768
|
+
}): Promise<{
|
|
769
|
+
data: Uint8Array;
|
|
770
|
+
}>;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* A thin `ControlRpc` over a NATS request/reply connection. Constructed with a
|
|
774
|
+
* LAZY factory: the connection is resolved on first `request` (so boot never
|
|
775
|
+
* requires a live NATS). A null factory result, a no-responder error, or a
|
|
776
|
+
* request timeout each yield a `ControlResponse` carrying a synthesized
|
|
777
|
+
* `AgentError` (AGENT_OFFLINE / TIMEOUT) — NEVER a thrown transport error and
|
|
778
|
+
* NEVER a NotFound.
|
|
779
|
+
*
|
|
780
|
+
* The factory is async and memoized; it may itself dial the bus. M4 replaces the
|
|
781
|
+
* factory's body with the Accounts-scoped, hardened connection — this class's
|
|
782
|
+
* shape does not change.
|
|
783
|
+
*/
|
|
784
|
+
declare class NatsControlRpc implements ControlRpc {
|
|
785
|
+
private readonly connect;
|
|
786
|
+
private connection;
|
|
787
|
+
constructor(connect: () => Promise<NatsRequestConnection | null>);
|
|
788
|
+
private resolveConnection;
|
|
789
|
+
request(subject: string, req: ControlRequest, opts: {
|
|
790
|
+
timeoutMs: number;
|
|
791
|
+
}): Promise<ControlResponse>;
|
|
792
|
+
}
|
|
793
|
+
/** A `ControlResponse` carrying a synthesized AGENT_OFFLINE error. */
|
|
794
|
+
declare function offlineControlResponse(requestId: string): ControlResponse;
|
|
795
|
+
/** A `ControlResponse` carrying a synthesized TIMEOUT error (→ reconnecting). */
|
|
796
|
+
declare function timeoutControlResponse(requestId: string): ControlResponse;
|
|
797
|
+
|
|
798
|
+
/** The V4A-diff applier the SDK's apply_patch editor uses. The leaf cannot import
|
|
799
|
+
* `@openai/agents`'s `applyDiff` (the agent-loop root the leaf forbids), so the
|
|
800
|
+
* runtime barrel (`packages/runtime/src/index.ts`, which DOES import that root)
|
|
801
|
+
* injects it via `setSelfhostedApplyDiff` at module load. Until injected,
|
|
802
|
+
* `createEditor()` surfaces a clear error rather than a silent wrong-edit. */
|
|
803
|
+
type SelfhostedApplyDiff = (input: string, diff: string, mode?: "default" | "create") => string;
|
|
804
|
+
/** Register the SDK's `applyDiff` so `SelfhostedSession.createEditor()` can apply
|
|
805
|
+
* V4A diffs over the NATS fs ops. Called once by the runtime barrel. */
|
|
806
|
+
declare function setSelfhostedApplyDiff(fn: SelfhostedApplyDiff): void;
|
|
807
|
+
/** The structural Editor surface the SDK's filesystem capability consumes (the
|
|
808
|
+
* three apply_patch operations). Mirrors `@openai/agents-core`'s `Editor`. */
|
|
809
|
+
interface SelfhostedEditor {
|
|
810
|
+
createFile(operation: {
|
|
811
|
+
path: string;
|
|
812
|
+
diff: string;
|
|
813
|
+
}, context?: unknown): Promise<{
|
|
814
|
+
output?: string;
|
|
815
|
+
} | void>;
|
|
816
|
+
updateFile(operation: {
|
|
817
|
+
path: string;
|
|
818
|
+
diff: string;
|
|
819
|
+
moveTo?: string;
|
|
820
|
+
}, context?: unknown): Promise<{
|
|
821
|
+
output?: string;
|
|
822
|
+
} | void>;
|
|
823
|
+
deleteFile(operation: {
|
|
824
|
+
path: string;
|
|
825
|
+
}, context?: unknown): Promise<{
|
|
826
|
+
output?: string;
|
|
827
|
+
} | void>;
|
|
828
|
+
}
|
|
829
|
+
/** The image tool-output shape the SDK's view_image tool expects (mirror of
|
|
830
|
+
* `ToolOutputImage` — not re-exported by `@openai/agents/sandbox`, so structural). */
|
|
831
|
+
interface SelfhostedImageOutput {
|
|
832
|
+
type: "image";
|
|
833
|
+
image: {
|
|
834
|
+
data: Uint8Array;
|
|
835
|
+
mediaType: string;
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
/** Default control-op timeout. A transient miss surfaces as `agent_reconnecting`
|
|
839
|
+
* (the turn pauses + retries); it is NOT a hard failure. */
|
|
840
|
+
declare const SELFHOSTED_DEFAULT_TIMEOUT_MS = 30000;
|
|
841
|
+
/** The relay-URL shape config the session needs to build a stream endpoint. M8b
|
|
842
|
+
* wires the real relay deployment behind THIS seam so `buildStreamUrl` works
|
|
843
|
+
* unchanged behind `resolveExposedPort`. */
|
|
844
|
+
interface SelfhostedRelayConfig {
|
|
845
|
+
/** The relay edge host (no scheme), e.g. "relay.opengeni.ai". */
|
|
846
|
+
host: string;
|
|
847
|
+
/** The relay port. Defaults to 443 (the relay terminates TLS). */
|
|
848
|
+
port?: number;
|
|
849
|
+
/** Whether the relay endpoint is TLS (wss/https). Defaults true. */
|
|
850
|
+
tls?: boolean;
|
|
851
|
+
/** The relay's stream-dial path (the `opengeni-relay` wss route). Defaults to
|
|
852
|
+
* "/stream" — the route the relay listens on (M8b). */
|
|
853
|
+
path?: string;
|
|
854
|
+
}
|
|
855
|
+
/** The relay's default wss dial path (the `opengeni-relay` server route). */
|
|
856
|
+
declare const SELFHOSTED_RELAY_STREAM_PATH = "/stream";
|
|
857
|
+
interface SelfhostedSessionDeps {
|
|
858
|
+
workspaceId: string;
|
|
859
|
+
agentId: string;
|
|
860
|
+
controlRpc: ControlRpc;
|
|
861
|
+
relay: SelfhostedRelayConfig;
|
|
862
|
+
/** The lease/active epoch this session is fenced under (echoed on every
|
|
863
|
+
* ControlRequest so the agent can reject a stale op with ERROR_CODE_FENCED).
|
|
864
|
+
* Defaults to 0 (no fence) for the negotiation-only / test path. */
|
|
865
|
+
epoch?: number;
|
|
866
|
+
/** Override the control-op timeout (tests). */
|
|
867
|
+
timeoutMs?: number;
|
|
868
|
+
/**
|
|
869
|
+
* The run's declared sandbox environment — the SAME `Record<string,string>` the
|
|
870
|
+
* worker turn passes to `runtime.buildAgent`'s `sandboxEnvironment` (and that the
|
|
871
|
+
* agent's TARGET manifest, `buildManifest`, carries). The SDK injects this
|
|
872
|
+
* selfhosted session NON-OWNED and applies the agent's manifest as a provided-
|
|
873
|
+
* session delta; `validateNoEnvironmentDelta` throws "Live sandbox sessions cannot
|
|
874
|
+
* change manifest environment variables" on ANY env mismatch. So `state.manifest`'s
|
|
875
|
+
* `environment` MUST EQUAL the turn's environment for the delta to be empty. The
|
|
876
|
+
* selfhosted exec routes over NATS and does NOT consume the env, but the manifest
|
|
877
|
+
* must carry it for parity. Omitted → `{}` (the negotiation-only / test path,
|
|
878
|
+
* which never applies a turn manifest, so there is no delta to validate).
|
|
879
|
+
*/
|
|
880
|
+
environment?: Record<string, string>;
|
|
881
|
+
/**
|
|
882
|
+
* The session's working directory — the BASE every path/cwd is rooted under (see
|
|
883
|
+
* `toMachinePath` / SELFHOSTED_VIRTUAL_ROOT). A launch-workspace_root-relative
|
|
884
|
+
* subdir (resolved under workspace_root by the agent's `resolve_cwd`) or an
|
|
885
|
+
* absolute machine path. Omitted/empty (the default) ⇒ "" ⇒ today's behavior
|
|
886
|
+
* exactly (an empty cwd lets the agent substitute its workspace_root).
|
|
887
|
+
*/
|
|
888
|
+
workingDir?: string;
|
|
889
|
+
}
|
|
890
|
+
/** The Channel-A `exec` result shape (a structural superset of the SDK's). */
|
|
891
|
+
interface SelfhostedExecResult {
|
|
892
|
+
output: string;
|
|
893
|
+
stdout: string;
|
|
894
|
+
stderr: string;
|
|
895
|
+
exitCode: number | null;
|
|
896
|
+
}
|
|
897
|
+
/** The `exec` args the structural surface accepts (mirrors ChannelAExecArgs). */
|
|
898
|
+
interface SelfhostedExecArgs {
|
|
899
|
+
cmd: string;
|
|
900
|
+
workdir?: string | undefined;
|
|
901
|
+
shell?: string | undefined;
|
|
902
|
+
login?: boolean | undefined;
|
|
903
|
+
tty?: boolean | undefined;
|
|
904
|
+
runAs?: string | undefined;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* The persistable session state. For selfhosted this is `{agentId}` ONLY — there
|
|
908
|
+
* is NO provider box id, no snapshot, no manifest. Resume re-addresses the live
|
|
909
|
+
* subject; the machine itself is the persistence (`persistable:false`).
|
|
910
|
+
*/
|
|
911
|
+
interface SelfhostedSessionState {
|
|
912
|
+
agentId: string;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* A live selfhosted session — the structural `SandboxSessionLike` surface over a
|
|
916
|
+
* `ControlRpc`. Mirrors Modal's session shape so Channel-A/viewer/computer-use
|
|
917
|
+
* consume it unchanged.
|
|
918
|
+
*/
|
|
919
|
+
declare class SelfhostedSession {
|
|
920
|
+
readonly backendId: "selfhosted";
|
|
921
|
+
readonly workspaceId: string;
|
|
922
|
+
readonly agentId: string;
|
|
923
|
+
private readonly controlRpc;
|
|
924
|
+
private readonly relay;
|
|
925
|
+
private readonly epoch;
|
|
926
|
+
private readonly timeoutMs;
|
|
927
|
+
private readonly subject;
|
|
928
|
+
/** The session working directory — the path/cwd base every op is rooted under
|
|
929
|
+
* (see `toMachinePath`). "" by default ⇒ today's workspace_root behavior. */
|
|
930
|
+
private readonly workingDir;
|
|
931
|
+
/**
|
|
932
|
+
* The structural `state` slice consumers read. `agentId`/`instanceId` serve the
|
|
933
|
+
* channel-a `readInstanceId` + docker-network decoration (the agentId IS the
|
|
934
|
+
* identity). `manifest` is the slice the @openai/agents SDK reads AND writes per
|
|
935
|
+
* turn (serializeManifestEnvironment / validateProvidedSessionManifestUpdate read
|
|
936
|
+
* `manifest.root` + iterate `manifest.environment`; providedSessionManifest WRITES
|
|
937
|
+
* `state.manifest = next`). It must be a real, MUTABLE Manifest field — when the
|
|
938
|
+
* RoutingSandboxSession proxy resolves THIS as the active backend it returns
|
|
939
|
+
* `session.state` BY REFERENCE, so the SDK's read and write must both land on a
|
|
940
|
+
* well-formed Manifest here (defined `root`, object `environment`). Without it the
|
|
941
|
+
* SDK crashes with `undefined is not an object (evaluating 'current.root')`.
|
|
942
|
+
*
|
|
943
|
+
* `manifest` is intentionally a plain mutable field (not `readonly`) so the SDK's
|
|
944
|
+
* `state.manifest = next` write succeeds. It is NOT part of the persistable state
|
|
945
|
+
* (`serializeSessionState` round-trips `{agentId}` only).
|
|
946
|
+
*
|
|
947
|
+
* `environment` is the SDK `SandboxSessionState.environment` (a `Record<string,
|
|
948
|
+
* string>`). It MUST be present because the GROUP box's client serializes THIS
|
|
949
|
+
* (the active backend's) state at end-of-turn — the non-owned injected session is
|
|
950
|
+
* serialized via the CONFIGURED client (modal in prod), NOT the selfhosted client.
|
|
951
|
+
* Modal's `serializeRemoteSandboxSessionState` does `Object.entries(state.environment)`;
|
|
952
|
+
* an absent field crashes the post-turn RunState serialize with "Object.entries
|
|
953
|
+
* requires that input parameter not be null or undefined". It carries the run's
|
|
954
|
+
* threaded environment (or `{}`). The resulting modal-tagged envelope is inert for
|
|
955
|
+
* selfhosted (resume re-addresses the machine by agentId via the lease pointer,
|
|
956
|
+
* never from this SDK envelope), so its only job is to not crash the serialize.
|
|
957
|
+
*/
|
|
958
|
+
readonly state: {
|
|
959
|
+
agentId: string;
|
|
960
|
+
instanceId: string;
|
|
961
|
+
manifest: Manifest;
|
|
962
|
+
environment: Record<string, string>;
|
|
963
|
+
};
|
|
964
|
+
constructor(deps: SelfhostedSessionDeps);
|
|
965
|
+
/** Issue a control op, decoding the agent's reply or throwing the mapped
|
|
966
|
+
* `SelfhostedControlError` on an AgentError (incl. a synthesized offline /
|
|
967
|
+
* timeout error from the transport). */
|
|
968
|
+
private call;
|
|
969
|
+
/** Channel-A `exec`: run a command on the machine and return its output. */
|
|
970
|
+
exec(args: SelfhostedExecArgs): Promise<SelfhostedExecResult>;
|
|
971
|
+
/** SDK shell capability `execCommand`: run a command and return its stdout (the
|
|
972
|
+
* `exec_command` tool). Selfhosted exec is non-interactive (no PTY) — `tty` is
|
|
973
|
+
* ignored; `supportsPty()` is false so the SDK never offers a stdin session. */
|
|
974
|
+
execCommand(args: {
|
|
975
|
+
cmd: string;
|
|
976
|
+
workdir?: string;
|
|
977
|
+
runAs?: string;
|
|
978
|
+
}): Promise<string>;
|
|
979
|
+
/** SDK shell capability never calls this (gated on `supportsPty()` which is
|
|
980
|
+
* false), but the surface advertises it. Selfhosted exec has no interactive PTY
|
|
981
|
+
* session over the structured RPC, so a stdin write is unsupported. */
|
|
982
|
+
supportsPty(): boolean;
|
|
983
|
+
/** SDK filesystem capability `view_image`: read the image bytes off the machine
|
|
984
|
+
* and wrap them in the tool-output image shape (magic-byte sniff + path fallback,
|
|
985
|
+
* mirroring the SDK's `imageOutputFromBytes`). */
|
|
986
|
+
viewImage(args: {
|
|
987
|
+
path: string;
|
|
988
|
+
runAs?: string;
|
|
989
|
+
}): Promise<SelfhostedImageOutput>;
|
|
990
|
+
/** SDK skills/filesystem `pathExists`: whether a path exists on the machine. */
|
|
991
|
+
pathExists(path: string, _runAs?: string): Promise<boolean>;
|
|
992
|
+
/** SDK skills `listDir`: list a directory as `{name, path, type}[]`. */
|
|
993
|
+
listDir(args: {
|
|
994
|
+
path: string;
|
|
995
|
+
runAs?: string;
|
|
996
|
+
}): Promise<Array<{
|
|
997
|
+
name: string;
|
|
998
|
+
path: string;
|
|
999
|
+
type: "file" | "dir" | "other";
|
|
1000
|
+
}>>;
|
|
1001
|
+
/** SDK manifest-delta `materializeEntry`: a NO-OP for selfhosted. Source
|
|
1002
|
+
* materialization (cloning repos / staging files into the box) is how cloud
|
|
1003
|
+
* providers prepare a fresh box; a bring-your-own machine already owns its
|
|
1004
|
+
* filesystem and is prepared by the agent itself, so there is nothing to stage.
|
|
1005
|
+
* Present (not absent) so the SDK's provided-session manifest apply path — which
|
|
1006
|
+
* requires `applyManifest()` OR `materializeEntry()` when the agent declares
|
|
1007
|
+
* entries — is satisfied without error. The selfhosted manifest declares no
|
|
1008
|
+
* entries, so in practice this is never invoked with a real entry. */
|
|
1009
|
+
materializeEntry(_args: {
|
|
1010
|
+
path: string;
|
|
1011
|
+
entry: unknown;
|
|
1012
|
+
runAs?: string;
|
|
1013
|
+
}): Promise<void>;
|
|
1014
|
+
/** SDK filesystem capability `createEditor`: the apply_patch host. Applies V4A
|
|
1015
|
+
* diffs over the NATS fs ops (read → applyDiff → write). `applyDiff` is the SDK's
|
|
1016
|
+
* own parser, injected by the runtime barrel (the leaf cannot import it). */
|
|
1017
|
+
createEditor(runAs?: string): SelfhostedEditor;
|
|
1018
|
+
/** Channel-A `readFile`: read a file off the machine (binary-safe). */
|
|
1019
|
+
readFile(args: {
|
|
1020
|
+
path: string;
|
|
1021
|
+
runAs?: string;
|
|
1022
|
+
maxBytes?: number;
|
|
1023
|
+
}): Promise<Uint8Array>;
|
|
1024
|
+
/** Write a file onto the machine (the fs surface the descriptor advertises). */
|
|
1025
|
+
writeFile(args: {
|
|
1026
|
+
path: string;
|
|
1027
|
+
content: string | Uint8Array;
|
|
1028
|
+
createParents?: boolean;
|
|
1029
|
+
append?: boolean;
|
|
1030
|
+
}): Promise<number>;
|
|
1031
|
+
/** List a directory on the machine. */
|
|
1032
|
+
listFiles(args: {
|
|
1033
|
+
path: string;
|
|
1034
|
+
recursive?: boolean;
|
|
1035
|
+
}): Promise<NonNullable<ControlResponse["result"]> & {
|
|
1036
|
+
$case: "fsList";
|
|
1037
|
+
}>;
|
|
1038
|
+
/** Stat a path on the machine. */
|
|
1039
|
+
statFile(args: {
|
|
1040
|
+
path: string;
|
|
1041
|
+
}): Promise<{
|
|
1042
|
+
exists: boolean;
|
|
1043
|
+
}>;
|
|
1044
|
+
/** Computer-use WRITE op: inject one synthetic desktop input event (pointer/key/
|
|
1045
|
+
* scroll) on the machine's OWN display. The agent injects via CGEvent (macOS) /
|
|
1046
|
+
* XTEST (Linux) and CONSENT-GATES it — an unconsented call never touches the OS
|
|
1047
|
+
* and surfaces the mapped control error (ERROR_CODE_CONSENT_REQUIRED) via `call()`. */
|
|
1048
|
+
desktopInput(event: DesktopInputRequest["event"]): Promise<void>;
|
|
1049
|
+
/** Computer-use VIEW op: capture a single PNG screenshot of the machine's desktop
|
|
1050
|
+
* plus its geometry (via ScreenCaptureKit / x11). NOT consent-gated (a view op —
|
|
1051
|
+
* the view/control decoupling), so it works with a display but no screen-control
|
|
1052
|
+
* consent. Returns the raw encoded bytes + width/height. */
|
|
1053
|
+
screenshot(): Promise<{
|
|
1054
|
+
png: Uint8Array;
|
|
1055
|
+
width: number;
|
|
1056
|
+
height: number;
|
|
1057
|
+
}>;
|
|
1058
|
+
/** A cheap liveness probe — request a Ping on the subject; returns true iff a
|
|
1059
|
+
* responder answered (no AgentError). Used by `negotiateSelfhostedCapabilities`.
|
|
1060
|
+
* The wire `nonce` is a uint64 (a numeric string), so the default is a random
|
|
1061
|
+
* numeric value — NOT a UUID (which would fail proto uint64 encoding). */
|
|
1062
|
+
ping(nonce?: string): Promise<boolean>;
|
|
1063
|
+
/**
|
|
1064
|
+
* Resolve an exposed port to a relay stream endpoint (the viewer/pty plane).
|
|
1065
|
+
* Returns the relay URL SHAPE — `{host:relay, port, tls, query:channel-key}` —
|
|
1066
|
+
* after asking the agent to ensure a stream channel for the port. M8b wires the
|
|
1067
|
+
* real relay tier (the byte pump) behind THIS seam.
|
|
1068
|
+
*
|
|
1069
|
+
* THE CHANNEL-KEY QUERY (the M8b relay-dial contract, dossier §10.5): the relay
|
|
1070
|
+
* routes by `{workspaceId, agentId, port}` — the EXACT `ChannelKey::query` the
|
|
1071
|
+
* agent's relay client (`opengeni-agent-stream`) appends when it registers the
|
|
1072
|
+
* producer side: `ws=<workspaceId>&agent=<agentId>&port=<port>`. We append the
|
|
1073
|
+
* agent-registered `channel=<channelId>` as a correlation hint. So the viewer
|
|
1074
|
+
* dials `wss://<relay>/stream?ws=&agent=&port=&channel=` and presents the minted
|
|
1075
|
+
* `ogs_` token in-band (NEVER as a URL param) — the relay pairs it with the
|
|
1076
|
+
* producer by the routing key.
|
|
1077
|
+
*/
|
|
1078
|
+
resolveExposedPort(port: number): Promise<ExposedPortEndpoint>;
|
|
1079
|
+
/** Round-trip the persistable state — `{agentId}` ONLY (resume = re-address). */
|
|
1080
|
+
serializeSessionState(): Promise<SelfhostedSessionState>;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* The selfhosted SDK-client surface the registry builds. `backendId:"selfhosted"`
|
|
1084
|
+
* (the resume-fence field asserted against the descriptor). `create()`/`resume()`
|
|
1085
|
+
* return a `SelfhostedSession` bound to `{workspaceId, agentId, controlRpc}`.
|
|
1086
|
+
*
|
|
1087
|
+
* `create()` and `resume()` are IDENTICAL for selfhosted — there is no box to
|
|
1088
|
+
* provision (the machine already exists); both just bind a session to the live
|
|
1089
|
+
* subject. `serializeSessionState`/`deserializeSessionState` round-trip
|
|
1090
|
+
* `{agentId}` only.
|
|
1091
|
+
*
|
|
1092
|
+
* The `controlRpc` is constructed LAZILY via an injected factory (defaulting to
|
|
1093
|
+
* `NatsControlRpc`); a session built before NATS is configured surfaces
|
|
1094
|
+
* `agent_offline` on its first op rather than failing at construction.
|
|
1095
|
+
*/
|
|
1096
|
+
declare class SelfhostedSandboxClient {
|
|
1097
|
+
readonly backendId: "selfhosted";
|
|
1098
|
+
readonly supportsDefaultOptions = false;
|
|
1099
|
+
private readonly workspaceId;
|
|
1100
|
+
private readonly relay;
|
|
1101
|
+
private readonly controlRpcFactory;
|
|
1102
|
+
private readonly defaultAgentId;
|
|
1103
|
+
private readonly epoch;
|
|
1104
|
+
private readonly timeoutMs;
|
|
1105
|
+
private readonly environment;
|
|
1106
|
+
private readonly workingDir;
|
|
1107
|
+
private controlRpcMemo;
|
|
1108
|
+
constructor(opts: {
|
|
1109
|
+
workspaceId: string;
|
|
1110
|
+
relay: SelfhostedRelayConfig;
|
|
1111
|
+
/** Lazily build the ControlRpc (defaults to NatsControlRpc in the provider). */
|
|
1112
|
+
controlRpcFactory: () => ControlRpc;
|
|
1113
|
+
/** The agentId a bare create()/resume() (no state) binds to. Optional: the
|
|
1114
|
+
* resume path supplies it via deserializeSessionState. */
|
|
1115
|
+
agentId?: string;
|
|
1116
|
+
epoch?: number;
|
|
1117
|
+
timeoutMs?: number;
|
|
1118
|
+
/** The run's declared sandbox environment, threaded into every bound session's
|
|
1119
|
+
* `state.manifest.environment` so the SDK's per-turn manifest-env delta is
|
|
1120
|
+
* empty (validateNoEnvironmentDelta). See SelfhostedSessionDeps.environment.
|
|
1121
|
+
* Omitted → `{}` (the negotiation-only path; no turn manifest is applied). */
|
|
1122
|
+
environment?: Record<string, string>;
|
|
1123
|
+
/** The session working directory threaded into every bound session (the path/
|
|
1124
|
+
* cwd base; see SelfhostedSessionDeps.workingDir). Omitted/empty ⇒ the default
|
|
1125
|
+
* workspace_root behavior. */
|
|
1126
|
+
workingDir?: string;
|
|
1127
|
+
});
|
|
1128
|
+
private controlRpc;
|
|
1129
|
+
private bind;
|
|
1130
|
+
/** Bind a session to the live agent subject. There is no box to provision. */
|
|
1131
|
+
create(_manifest?: unknown, _options?: unknown): Promise<SelfhostedSession>;
|
|
1132
|
+
/** Resume = re-address the subject. Identical to create — no provider state. */
|
|
1133
|
+
resume(state: SelfhostedSessionState | Record<string, unknown>, _options?: unknown): Promise<SelfhostedSession>;
|
|
1134
|
+
/** Serialize a live session's state → `{agentId}` ONLY. */
|
|
1135
|
+
serializeSessionState(state: SelfhostedSessionState | {
|
|
1136
|
+
agentId?: string;
|
|
1137
|
+
} | unknown): Promise<SelfhostedSessionState>;
|
|
1138
|
+
/** Deserialize `{agentId}` from the persisted envelope. */
|
|
1139
|
+
deserializeSessionState(state: Record<string, unknown>): Promise<SelfhostedSessionState>;
|
|
1140
|
+
/** selfhosted is NOT persistable — there is no owned session state to preserve
|
|
1141
|
+
* (the machine is the persistence). The lease never snapshots it. */
|
|
1142
|
+
canPersistOwnedSessionState(): Promise<boolean>;
|
|
1143
|
+
private requireAgentId;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* The dependency shape `buildSelfhostedBackendSession` needs to bind a live
|
|
1147
|
+
* selfhosted session to a target machine. A structural superset of the fields the
|
|
1148
|
+
* routing resolver (backend-resolver.ts) reads off its deps + pointer, and the
|
|
1149
|
+
* fields the WORKER turn's machine-primary establish branch threads in — so a
|
|
1150
|
+
* SINGLE build shape is shared by both (never two divergent constructions of the
|
|
1151
|
+
* same SelfhostedSandboxClient/resume pair).
|
|
1152
|
+
*/
|
|
1153
|
+
interface SelfhostedSessionBuild {
|
|
1154
|
+
/** The workspace the machine's control-plane subject is scoped to. */
|
|
1155
|
+
workspaceId: string;
|
|
1156
|
+
/** The enrollment id == the agent id `agent.<ws>.<id>.rpc` addresses. */
|
|
1157
|
+
agentId: string;
|
|
1158
|
+
/** The relay-URL shape for stream endpoints. */
|
|
1159
|
+
relay: SelfhostedRelayConfig;
|
|
1160
|
+
/** Lazily build the live ControlRpc (the request-scoped NATS connection). */
|
|
1161
|
+
controlRpcFactory: () => ControlRpc;
|
|
1162
|
+
/** The lease/active epoch the session is fenced under (echoed on every op). */
|
|
1163
|
+
epoch: number;
|
|
1164
|
+
/** The run's declared sandbox environment → the session manifest.environment
|
|
1165
|
+
* (env-parity; see SelfhostedSessionDeps.environment). */
|
|
1166
|
+
environment?: Record<string, string>;
|
|
1167
|
+
/** The session working directory (the path/cwd base). Null/absent ⇒ workspace_root. */
|
|
1168
|
+
workingDir?: string | null;
|
|
1169
|
+
/** Override the control-op timeout (tests). */
|
|
1170
|
+
timeoutMs?: number;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Build a live selfhosted session bound to a target machine: construct a request-
|
|
1174
|
+
* scoped `SelfhostedSandboxClient` (fenced under `epoch`, carrying the run's env +
|
|
1175
|
+
* working dir) and `resume()` it (= re-address the live subject — no provider box
|
|
1176
|
+
* is created). Returns BOTH the client (the OWNED-sandbox client the turn injects,
|
|
1177
|
+
* whose `serializeSessionState` round-trips `{agentId}`) and the live session.
|
|
1178
|
+
*
|
|
1179
|
+
* Shared by:
|
|
1180
|
+
* - the routing resolver (backend-resolver.ts) — a swap target, where only the
|
|
1181
|
+
* session is needed; and
|
|
1182
|
+
* - the worker turn's machine-primary establish branch — where the client is the
|
|
1183
|
+
* owned-sandbox client AND the session is the pinned routing default.
|
|
1184
|
+
* Factoring it here keeps the two builds identical (no divergence in the fence
|
|
1185
|
+
* epoch, env threading, or working-dir base).
|
|
1186
|
+
*/
|
|
1187
|
+
declare function buildSelfhostedBackendSession(deps: SelfhostedSessionBuild): Promise<{
|
|
1188
|
+
client: SelfhostedSandboxClient;
|
|
1189
|
+
session: SelfhostedSession;
|
|
1190
|
+
}>;
|
|
1191
|
+
/**
|
|
1192
|
+
* The selfhosted NotFound discriminator — THE load-bearing safety property
|
|
1193
|
+
* (dossier §10.2/§19): for selfhosted, `agent-offline` (no responder) is NEVER a
|
|
1194
|
+
* provider NotFound. A user's real machine is not recreatable; if the lease saw
|
|
1195
|
+
* agent-offline as NotFound it would cold-create a RIVAL box (a Modal box) for
|
|
1196
|
+
* the user's machine. So this ALWAYS returns FALSE for selfhosted — there is no
|
|
1197
|
+
* "box gone, recreate it" condition. An OS-level file NotFound is an op-level
|
|
1198
|
+
* error the fs layer 404s; it is likewise NOT a session-recreate condition.
|
|
1199
|
+
*
|
|
1200
|
+
* `establishSandboxSessionFromEnvelope` cold-restores ONLY when the per-backend
|
|
1201
|
+
* NotFound discriminator returns true; returning false here guarantees the
|
|
1202
|
+
* selfhosted path never cold-creates a rival — the op surfaces agent_offline and
|
|
1203
|
+
* the caller backs off / retries.
|
|
1204
|
+
*/
|
|
1205
|
+
declare function isSelfhostedProviderNotFoundError(_error: unknown): false;
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* The structural slice of the M2 `@opengeni/db` `EnrollmentRecord` the selfhosted
|
|
1209
|
+
* negotiation reads. Defined STRUCTURALLY (not imported from `@opengeni/db`) so
|
|
1210
|
+
* the agent-loop-free sandbox leaf does not couple to the DB package's graph —
|
|
1211
|
+
* the API/worker pass an `EnrollmentRecord`, which satisfies this shape. The
|
|
1212
|
+
* fields: `status` (active gates reachability), `exposure` +
|
|
1213
|
+
* `allowScreenControl` (whole-machine + screen-control consent),`hasDisplay`
|
|
1214
|
+
* (the display plane), `lastSeenAt` (the reconnecting-window disambiguator).
|
|
1215
|
+
*/
|
|
1216
|
+
interface SelfhostedEnrollment {
|
|
1217
|
+
status: string;
|
|
1218
|
+
exposure: string;
|
|
1219
|
+
allowScreenControl: boolean;
|
|
1220
|
+
hasDisplay: boolean;
|
|
1221
|
+
lastSeenAt: string | null;
|
|
1222
|
+
}
|
|
1223
|
+
/** The derived liveness state of a selfhosted machine (the online/offline/
|
|
1224
|
+
* reconnecting/consent/display matrix). */
|
|
1225
|
+
interface SelfhostedLivenessState {
|
|
1226
|
+
/** The dominant machine state. */
|
|
1227
|
+
state: "online" | "reconnecting" | "offline";
|
|
1228
|
+
/** Whole-machine + screen-control consent acknowledged (gates desktop input). */
|
|
1229
|
+
consented: boolean;
|
|
1230
|
+
/** A display (real or Xvfb) is present (gates the desktop pixel plane). */
|
|
1231
|
+
hasDisplay: boolean;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* The window after `lastSeenAt` within which a missed liveness probe is read as a
|
|
1235
|
+
* transient BLIP (`reconnecting`) rather than a hard `offline`. Mirrors the
|
|
1236
|
+
* resiliency model (§10.6: reconnecting after 1 missed window, offline after
|
|
1237
|
+
* ~30s). A probe miss with a lastSeenAt inside this window → reconnecting.
|
|
1238
|
+
*/
|
|
1239
|
+
declare const SELFHOSTED_RECONNECT_WINDOW_MS = 30000;
|
|
1240
|
+
/**
|
|
1241
|
+
* Derive the selfhosted liveness state from the enrollment row + a liveness probe
|
|
1242
|
+
* outcome. The probe is the authoritative "is the agent answering NOW" signal;
|
|
1243
|
+
* `lastSeenAt` disambiguates a probe-miss into reconnecting (recent) vs offline
|
|
1244
|
+
* (stale / never seen).
|
|
1245
|
+
*
|
|
1246
|
+
* - no enrollment / revoked → offline (the machine isn't enrolled).
|
|
1247
|
+
* - probe responded → online.
|
|
1248
|
+
* - probe missed, lastSeenAt recent → reconnecting (a transient blip).
|
|
1249
|
+
* - probe missed, lastSeenAt stale → offline.
|
|
1250
|
+
*/
|
|
1251
|
+
declare function selfhostedLiveness(input: {
|
|
1252
|
+
enrollment: SelfhostedEnrollment | null;
|
|
1253
|
+
/** The ControlRpc Ping outcome: true iff a responder answered. */
|
|
1254
|
+
probeResponded: boolean;
|
|
1255
|
+
/** Override the clock (tests). */
|
|
1256
|
+
now?: Date;
|
|
1257
|
+
}): SelfhostedLivenessState;
|
|
1258
|
+
interface SelfhostedNegotiationInput {
|
|
1259
|
+
sessionId: string;
|
|
1260
|
+
os?: SandboxOs;
|
|
1261
|
+
leaseEpoch: number;
|
|
1262
|
+
/** The M2 enrollment row for the machine (null → never enrolled → offline). */
|
|
1263
|
+
enrollment: SelfhostedEnrollment | null;
|
|
1264
|
+
/** A live liveness probe — typically `session.ping()`. When a session is
|
|
1265
|
+
* provided this is called; otherwise pass `probeResponded` explicitly. */
|
|
1266
|
+
session?: Pick<SelfhostedSession, "ping">;
|
|
1267
|
+
/** Explicit probe outcome (when no session is given, e.g. a pure read). */
|
|
1268
|
+
probeResponded?: boolean;
|
|
1269
|
+
/** The deployment desktop/terminal/computer-use policy toggles (threaded
|
|
1270
|
+
* through to the base negotiation). */
|
|
1271
|
+
desktopEnabled?: boolean;
|
|
1272
|
+
terminalEnabled?: boolean;
|
|
1273
|
+
computerUseEnabled?: boolean;
|
|
1274
|
+
/** Whether the calling principal acknowledged the un-redacted desktop. */
|
|
1275
|
+
desktopAcknowledged?: boolean;
|
|
1276
|
+
shared?: boolean;
|
|
1277
|
+
sharedSessionIds?: string[];
|
|
1278
|
+
/** Override the clock (tests). */
|
|
1279
|
+
now?: Date;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Negotiate the full `SessionCapabilities` document for a selfhosted machine,
|
|
1283
|
+
* with the online/offline/reconnecting/consent_required/display_unavailable cells
|
|
1284
|
+
* correctly decided. Async because it issues the liveness probe.
|
|
1285
|
+
*/
|
|
1286
|
+
declare function negotiateSelfhostedCapabilities(input: SelfhostedNegotiationInput): Promise<SessionCapabilities>;
|
|
1287
|
+
|
|
1288
|
+
/** A pluggable exec handler — given an ExecRequest, return an ExecResponse (or
|
|
1289
|
+
* throw to surface a synthesized error). Defaults to a trivial echo. */
|
|
1290
|
+
type MockExecHandler = (req: ExecRequest) => ExecResponse | Promise<ExecResponse>;
|
|
1291
|
+
interface MockAgentResponderOptions {
|
|
1292
|
+
/** Whether a responder exists at all. When false EVERY request yields an
|
|
1293
|
+
* AGENT_OFFLINE error (the "machine is offline" condition) — used to drive the
|
|
1294
|
+
* agent_offline capability + the isProviderSandboxNotFoundError test. */
|
|
1295
|
+
online?: boolean;
|
|
1296
|
+
/** Whether the agent has acknowledged whole-machine / screen-control consent.
|
|
1297
|
+
* When false, an op gated on consent yields CONSENT_REQUIRED. Defaults true. */
|
|
1298
|
+
consented?: boolean;
|
|
1299
|
+
/** Force the agent into a draining posture (every op → DRAINING). */
|
|
1300
|
+
draining?: boolean;
|
|
1301
|
+
/** Seed files (path → string|Uint8Array) into the virtual filesystem. */
|
|
1302
|
+
files?: Record<string, string | Uint8Array>;
|
|
1303
|
+
/** A custom exec handler; defaults to an echo of argv. */
|
|
1304
|
+
exec?: MockExecHandler;
|
|
1305
|
+
/** The hostname the mock reports (so PTY/exec `$HOSTNAME`-style asserts work). */
|
|
1306
|
+
hostname?: string;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* An in-process `ControlRpc` answering the agent op table against an in-memory
|
|
1310
|
+
* virtual filesystem. Drive a `SelfhostedSession` with this to test exec /
|
|
1311
|
+
* readFile / writeFile / list / stat round-trips without any NATS.
|
|
1312
|
+
*/
|
|
1313
|
+
declare class MockAgentResponder implements ControlRpc {
|
|
1314
|
+
private online;
|
|
1315
|
+
private readonly consented;
|
|
1316
|
+
private readonly draining;
|
|
1317
|
+
private readonly files;
|
|
1318
|
+
private readonly execHandler;
|
|
1319
|
+
readonly hostname: string;
|
|
1320
|
+
/** Every request seen, for assertion (subject + decoded ControlRequest). */
|
|
1321
|
+
readonly requests: Array<{
|
|
1322
|
+
subject: string;
|
|
1323
|
+
req: ControlRequest;
|
|
1324
|
+
}>;
|
|
1325
|
+
constructor(opts?: MockAgentResponderOptions);
|
|
1326
|
+
/** Flip the responder offline mid-test (a deliberate stop / blip). */
|
|
1327
|
+
setOnline(online: boolean): void;
|
|
1328
|
+
/** Read a file the session wrote (test assertion helper). */
|
|
1329
|
+
fileText(path: string): string | undefined;
|
|
1330
|
+
request(subject: string, req: ControlRequest, _opts: {
|
|
1331
|
+
timeoutMs: number;
|
|
1332
|
+
}): Promise<ControlResponse>;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/** The per-session active-sandbox pointer the proxy re-reads on every op. Mirror
|
|
1336
|
+
* of `@opengeni/db`'s `ActiveSandboxPointer` (structural, so the leaf does not
|
|
1337
|
+
* import the DB package). `activeSandboxId === null` == "use the session's own
|
|
1338
|
+
* group sandbox" (the default/backward-compat target). */
|
|
1339
|
+
interface ActivePointer {
|
|
1340
|
+
activeSandboxId: string | null;
|
|
1341
|
+
activeEpoch: number;
|
|
1342
|
+
/** The session's working directory — the path/cwd base for a selfhosted backend
|
|
1343
|
+
* (threaded into the SelfhostedSession via the resolver). `null`/absent ⇒ the
|
|
1344
|
+
* default workspace_root behavior. Optional so the default-pointer fallback
|
|
1345
|
+
* (`{ activeSandboxId: null, activeEpoch: 0 }`) the readPointer wiring synthesizes
|
|
1346
|
+
* when no row exists needs no extra field. Only the selfhosted branch reads it;
|
|
1347
|
+
* the modal/default branches ignore it. */
|
|
1348
|
+
workingDir?: string | null;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* The structural slice of a backend session the routing proxy forwards to. It is
|
|
1352
|
+
* a superset-by-optionality of every backend's surface (Modal's `SandboxSession`
|
|
1353
|
+
* AND the `SelfhostedSession`): each method is optional because a heterogeneous
|
|
1354
|
+
* target may or may not implement it, and the proxy reflects that at call-time.
|
|
1355
|
+
*/
|
|
1356
|
+
interface RoutableBackendSession {
|
|
1357
|
+
state?: unknown;
|
|
1358
|
+
exec?(args: unknown): Promise<unknown>;
|
|
1359
|
+
execCommand?(args: unknown): Promise<string>;
|
|
1360
|
+
writeStdin?(args: unknown): Promise<string>;
|
|
1361
|
+
readFile?(args: unknown): Promise<string | Uint8Array>;
|
|
1362
|
+
writeFile?(args: unknown): Promise<unknown>;
|
|
1363
|
+
createEditor?(runAs?: string): unknown;
|
|
1364
|
+
listDir?(args: unknown): Promise<unknown>;
|
|
1365
|
+
pathExists?(path: string, runAs?: string): Promise<boolean>;
|
|
1366
|
+
viewImage?(args: unknown): Promise<unknown>;
|
|
1367
|
+
materializeEntry?(args: unknown): Promise<void>;
|
|
1368
|
+
supportsPty?(): boolean;
|
|
1369
|
+
resolveExposedPort?(port: number): Promise<ExposedPortEndpoint>;
|
|
1370
|
+
serializeSessionState?(): Promise<unknown>;
|
|
1371
|
+
desktopInput?(event: unknown): Promise<void>;
|
|
1372
|
+
screenshot?(): Promise<{
|
|
1373
|
+
png: Uint8Array;
|
|
1374
|
+
width: number;
|
|
1375
|
+
height: number;
|
|
1376
|
+
}>;
|
|
1377
|
+
}
|
|
1378
|
+
/** The resolved active backend for an epoch: the live session + the sandbox id it
|
|
1379
|
+
* belongs to (`null` == the group sandbox) so a fence-retry can detect a move. */
|
|
1380
|
+
interface ResolvedActiveBackend {
|
|
1381
|
+
session: RoutableBackendSession;
|
|
1382
|
+
/** The sandbox id this backend serves (`null` == the session's group sandbox). */
|
|
1383
|
+
sandboxId: string | null;
|
|
1384
|
+
/** A label for diagnostics ("modal" | "selfhosted" | the sandbox name). */
|
|
1385
|
+
kind: string;
|
|
1386
|
+
}
|
|
1387
|
+
interface RoutingSandboxSessionDeps {
|
|
1388
|
+
/**
|
|
1389
|
+
* The DEFAULT backend resolved at construction time (the same shape `resolve()`
|
|
1390
|
+
* caches as `lastResolved`). This seeds `session.state` BEFORE the first op so a
|
|
1391
|
+
* consumer that reads `session.state.manifest` at turn START — the @openai/agents
|
|
1392
|
+
* SDK does, before any tool runs — sees the real default backend's state object
|
|
1393
|
+
* (and writes to `session.state.manifest = …` land on it by reference), instead
|
|
1394
|
+
* of an empty `{}` that crashes serializeManifestEnvironment /
|
|
1395
|
+
* validateProvidedSessionManifestUpdate. The default-pointer case
|
|
1396
|
+
* (`activeSandboxId === null`) resolves synchronously to this same backend, so
|
|
1397
|
+
* seeding it here is byte-identical to what the first `resolve()` would produce.
|
|
1398
|
+
*/
|
|
1399
|
+
defaultResolved?: ResolvedActiveBackend;
|
|
1400
|
+
/** Re-read the per-session active pointer. Called on EVERY op (the per-call
|
|
1401
|
+
* re-resolve that makes a mid-turn swap visible to the next tool call). */
|
|
1402
|
+
readPointer(): Promise<ActivePointer>;
|
|
1403
|
+
/**
|
|
1404
|
+
* Resolve the active backend session for a pointer. The proxy memoizes the
|
|
1405
|
+
* result by `activeEpoch`, so this is called at most once per epoch (per op the
|
|
1406
|
+
* pointer is re-read, but the heavy resolve only re-runs when the epoch moved).
|
|
1407
|
+
* For `pointer.activeSandboxId === null` this returns the default/group backend
|
|
1408
|
+
* (typically the already-established turn box); for a non-null target it builds
|
|
1409
|
+
* the target backend (a sibling Modal box or a selfhosted machine session).
|
|
1410
|
+
*/
|
|
1411
|
+
resolveActiveBackend(pointer: ActivePointer): Promise<ResolvedActiveBackend>;
|
|
1412
|
+
/** Max fence/stale retries within a single op before surfacing the error.
|
|
1413
|
+
* Defaults to 3 — enough to absorb a couple of concurrent swaps, bounded so a
|
|
1414
|
+
* swap-storm cannot loop forever. */
|
|
1415
|
+
maxFenceRetries?: number;
|
|
1416
|
+
/** Optional structured-log sink for swap/fence transitions (diagnostics). */
|
|
1417
|
+
onTransition?: (event: RoutingTransitionEvent) => void;
|
|
1418
|
+
}
|
|
1419
|
+
interface RoutingTransitionEvent {
|
|
1420
|
+
type: "resolved" | "fenced-retry" | "epoch-changed";
|
|
1421
|
+
fromEpoch: number;
|
|
1422
|
+
toEpoch: number;
|
|
1423
|
+
sandboxId: string | null;
|
|
1424
|
+
kind: string;
|
|
1425
|
+
}
|
|
1426
|
+
/** Thrown when the active backend does not implement the requested op (a
|
|
1427
|
+
* heterogeneous target whose surface lacks the method the caller reached for). */
|
|
1428
|
+
declare class RoutingUnsupportedError extends Error {
|
|
1429
|
+
readonly name = "RoutingUnsupportedError";
|
|
1430
|
+
constructor(op: string, kind: string);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* ONE stable session-shaped object the SDK binds to. Every method re-reads the
|
|
1434
|
+
* pointer, resolves the active backend (cached by epoch), and dispatches. A
|
|
1435
|
+
* stale-epoch fence (the pointer moved mid-op) re-resolves and retries.
|
|
1436
|
+
*
|
|
1437
|
+
* The proxy implements ALL of the consumed surface so the SDK (which binds method
|
|
1438
|
+
* presence ONCE) always sees `exec`/`readFile`/`resolveExposedPort`/… present. If
|
|
1439
|
+
* the CURRENTLY-active backend lacks a method, the proxy applies the natural
|
|
1440
|
+
* fallback (`exec`→`execCommand`) or throws `RoutingUnsupportedError` — degrade is
|
|
1441
|
+
* a value, not a crash.
|
|
1442
|
+
*
|
|
1443
|
+
* `state` is a STABLE getter so a consumer reading `session.state` (channel-a's
|
|
1444
|
+
* `readInstanceId`, the docker-network decoration) gets a coherent snapshot of the
|
|
1445
|
+
* currently-active backend without a method call.
|
|
1446
|
+
*/
|
|
1447
|
+
declare class RoutingSandboxSession implements RoutableBackendSession {
|
|
1448
|
+
private readonly deps;
|
|
1449
|
+
private readonly maxFenceRetries;
|
|
1450
|
+
private cachedEpoch;
|
|
1451
|
+
private cached;
|
|
1452
|
+
private lastResolved;
|
|
1453
|
+
desktopInput?: (event: unknown) => Promise<void>;
|
|
1454
|
+
screenshot?: () => Promise<{
|
|
1455
|
+
png: Uint8Array;
|
|
1456
|
+
width: number;
|
|
1457
|
+
height: number;
|
|
1458
|
+
}>;
|
|
1459
|
+
constructor(deps: RoutingSandboxSessionDeps);
|
|
1460
|
+
/**
|
|
1461
|
+
* A method-free read of the active backend's `state` (best-effort: the last
|
|
1462
|
+
* resolved backend, falling back to the default backend resolved at construction
|
|
1463
|
+
* so this is non-empty BEFORE the first op). Consumers that read `session.state`
|
|
1464
|
+
* (instanceId/decoration) get the active backend's state.
|
|
1465
|
+
*
|
|
1466
|
+
* CRITICAL: this returns the underlying backend's `state` OBJECT BY REFERENCE
|
|
1467
|
+
* (never a fresh `{}` when a backend exists). The @openai/agents SDK both READS
|
|
1468
|
+
* `session.state.manifest` and WRITES `session.state.manifest = nextManifest`
|
|
1469
|
+
* (providedSessionManifest); returning the live object by reference means those
|
|
1470
|
+
* property writes land on the real backend state and persist. Only when NO
|
|
1471
|
+
* backend has been resolved yet (no default seeded, no op dispatched) do we
|
|
1472
|
+
* return an empty object — and that path no longer occurs in the turn wiring,
|
|
1473
|
+
* which always seeds `defaultResolved`.
|
|
1474
|
+
*/
|
|
1475
|
+
get state(): unknown;
|
|
1476
|
+
/**
|
|
1477
|
+
* Re-read the pointer and resolve the active backend, using the per-epoch cache.
|
|
1478
|
+
* The cache is keyed by `activeEpoch`: if the epoch is unchanged we return the
|
|
1479
|
+
* cached backend; if it moved (a swap) we re-resolve and update the cache. This
|
|
1480
|
+
* is THE per-call re-read that makes a mid-turn swap land on the next op.
|
|
1481
|
+
*/
|
|
1482
|
+
private resolve;
|
|
1483
|
+
/**
|
|
1484
|
+
* Dispatch an op to the currently-active backend, retrying on a stale-epoch
|
|
1485
|
+
* fence. The sequence per attempt:
|
|
1486
|
+
* 1. re-read the pointer + resolve the active backend (cached by epoch),
|
|
1487
|
+
* 2. run `fn(activeSession)`,
|
|
1488
|
+
* 3. on a FENCE error (the pointer moved under us / the backend rejected a
|
|
1489
|
+
* stale epoch), INVALIDATE the cache and retry against the re-resolved
|
|
1490
|
+
* active sandbox — up to `maxFenceRetries`.
|
|
1491
|
+
* A non-fence error propagates immediately (it is a real op failure, not a swap
|
|
1492
|
+
* race).
|
|
1493
|
+
*/
|
|
1494
|
+
private dispatch;
|
|
1495
|
+
exec(args: unknown): Promise<unknown>;
|
|
1496
|
+
execCommand(args: unknown): Promise<string>;
|
|
1497
|
+
writeStdin(args: unknown): Promise<string>;
|
|
1498
|
+
readFile(args: unknown): Promise<string | Uint8Array>;
|
|
1499
|
+
writeFile(args: unknown): Promise<unknown>;
|
|
1500
|
+
listDir(args: unknown): Promise<unknown>;
|
|
1501
|
+
pathExists(path: string, runAs?: string): Promise<boolean>;
|
|
1502
|
+
viewImage(args: unknown): Promise<unknown>;
|
|
1503
|
+
materializeEntry(args: unknown): Promise<void>;
|
|
1504
|
+
/** PTY support reflects the LAST-resolved backend (a synchronous probe; the SDK
|
|
1505
|
+
* reads it to decide if the terminal is interactive). It cannot re-read the
|
|
1506
|
+
* pointer (synchronous), so it answers from the last resolve — coherent with
|
|
1507
|
+
* the resolve the surrounding op already performed. Defaults false before the
|
|
1508
|
+
* first resolve. */
|
|
1509
|
+
supportsPty(): boolean;
|
|
1510
|
+
/** createEditor is a synchronous factory in the SDK surface; it binds to the
|
|
1511
|
+
* last-resolved backend's editor (or the default backend before the first op).
|
|
1512
|
+
* Returns undefined when the active backend has no editor (channel-a falls back
|
|
1513
|
+
* to its exec-based write path). */
|
|
1514
|
+
createEditor(runAs?: string): unknown;
|
|
1515
|
+
resolveExposedPort(port: number): Promise<ExposedPortEndpoint>;
|
|
1516
|
+
/** Serialize the active backend's session state. Used by the resume-by-id seam
|
|
1517
|
+
* to fold the live box onto the lease. Dispatches to the active backend. */
|
|
1518
|
+
serializeSessionState(): Promise<unknown>;
|
|
1519
|
+
/** Force a resolve (priming the proxy before the first op so `state`/`supportsPty`
|
|
1520
|
+
* read a real backend). Optional — every op resolves lazily anyway. */
|
|
1521
|
+
prime(): Promise<ResolvedActiveBackend>;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/** The structural slice of a first-class sandbox the resolver reads (mirror of
|
|
1525
|
+
* `@opengeni/db`'s `SandboxRecord`; structural so the leaf does not import DB). */
|
|
1526
|
+
interface RoutableSandbox {
|
|
1527
|
+
id: string;
|
|
1528
|
+
kind: "modal" | "selfhosted" | string;
|
|
1529
|
+
name: string;
|
|
1530
|
+
/** For a selfhosted sandbox this is its enrollment id (== the agent id the
|
|
1531
|
+
* control-plane subject `agent.<ws>.<id>.rpc` addresses). Null for modal. */
|
|
1532
|
+
enrollmentId: string | null;
|
|
1533
|
+
}
|
|
1534
|
+
interface ActiveBackendResolverDeps {
|
|
1535
|
+
/** The workspace the session belongs to (the control-plane subject scope). */
|
|
1536
|
+
workspaceId: string;
|
|
1537
|
+
/** The session's own group sandbox session — the DEFAULT target
|
|
1538
|
+
* (`activeSandboxId === null`). Already established (lease-owned); the proxy
|
|
1539
|
+
* never re-establishes it. */
|
|
1540
|
+
defaultBackend: RoutableBackendSession;
|
|
1541
|
+
/** A label for the default backend (its backend id: "modal"/"selfhosted"/…). */
|
|
1542
|
+
defaultKind: string;
|
|
1543
|
+
/** Look up a first-class sandbox by id (the swap target). Returns null when the
|
|
1544
|
+
* id is unknown or not in this workspace (the caller 409s the swap). */
|
|
1545
|
+
getSandbox(sandboxId: string): Promise<RoutableSandbox | null>;
|
|
1546
|
+
/** Build a live `ControlRpc` for the selfhosted control plane (the request-
|
|
1547
|
+
* scoped NATS connection). Returns a ControlRpc whose offline/timeout maps to
|
|
1548
|
+
* agent_offline/agent_reconnecting (never a NotFound). */
|
|
1549
|
+
controlRpcFactory(): ControlRpc;
|
|
1550
|
+
/** The relay-URL shape config for selfhosted stream endpoints. */
|
|
1551
|
+
relay: SelfhostedRelayConfig;
|
|
1552
|
+
/** Establish (resume-by-id) a NON-DEFAULT modal target's box session for a swap.
|
|
1553
|
+
* Supplied by the API/worker (a closure over the sibling sandbox's lease). When
|
|
1554
|
+
* absent, a modal swap target surfaces as unsupported (the caller validated
|
|
1555
|
+
* liveness, so this is the "modal swap not wired in this context" guard). */
|
|
1556
|
+
establishModalTarget?: (sandbox: RoutableSandbox) => Promise<RoutableBackendSession>;
|
|
1557
|
+
/** Override the selfhosted control-op timeout (tests). */
|
|
1558
|
+
selfhostedTimeoutMs?: number;
|
|
1559
|
+
/**
|
|
1560
|
+
* The run's declared sandbox environment — the SAME `Record<string,string>` the
|
|
1561
|
+
* worker turn threads into the agent's TARGET manifest (and into the group box at
|
|
1562
|
+
* create). Threaded into a selfhosted swap target's session so its
|
|
1563
|
+
* `state.manifest.environment` EQUALS the turn's, making the SDK's per-turn
|
|
1564
|
+
* provided-session manifest-env delta empty (validateNoEnvironmentDelta).
|
|
1565
|
+
* WITHOUT this a pin-to-vm turn throws "Live sandbox sessions cannot change
|
|
1566
|
+
* manifest environment variables". Omitted → `{}` (the test/negotiation path).
|
|
1567
|
+
*/
|
|
1568
|
+
environment?: Record<string, string>;
|
|
1569
|
+
/**
|
|
1570
|
+
* A pre-established selfhosted session to PIN for the STEADY-STATE machine
|
|
1571
|
+
* pointer (the worker turn's machine-primary path, Stage D). When the pointer
|
|
1572
|
+
* targets THIS sandbox at THIS epoch, the resolver returns this SAME instance
|
|
1573
|
+
* instead of building a fresh `SelfhostedSession`. This is the instance-identity
|
|
1574
|
+
* pin: the SDK reads/writes `state.manifest` at turn START via the proxy's `state`
|
|
1575
|
+
* getter (which reads the default/last-resolved backend's state) and then reads it
|
|
1576
|
+
* per op via this resolver — those MUST land on ONE SelfhostedSession/manifest, or
|
|
1577
|
+
* a turn-start manifest write is invisible to the per-op reads (two-instance
|
|
1578
|
+
* divergence). A swap AWAY (a different sandbox id, or the same id at a moved epoch)
|
|
1579
|
+
* falls through to a fresh build under the new epoch. Omitted for the API/live-swap
|
|
1580
|
+
* path (which always builds fresh — it has no pre-established turn session).
|
|
1581
|
+
*/
|
|
1582
|
+
pinnedSelfhosted?: {
|
|
1583
|
+
sandboxId: string;
|
|
1584
|
+
epoch: number;
|
|
1585
|
+
session: RoutableBackendSession;
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
/** Thrown when a swap target cannot be resolved (unknown sandbox, or a modal
|
|
1589
|
+
* target with no establisher in this context). The caller maps it to a 409. */
|
|
1590
|
+
declare class ActiveBackendUnresolvableError extends Error {
|
|
1591
|
+
readonly name = "ActiveBackendUnresolvableError";
|
|
1592
|
+
constructor(message: string);
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Build the `resolveActiveBackend(pointer)` closure for a `RoutingSandboxSession`.
|
|
1596
|
+
* The returned closure is re-invoked by the proxy whenever the active_epoch moves
|
|
1597
|
+
* (the per-epoch cache miss), so it must be cheap-and-correct for the steady-state
|
|
1598
|
+
* (default pointer → the already-established group box) and build a fresh backend
|
|
1599
|
+
* for a swap target.
|
|
1600
|
+
*
|
|
1601
|
+
* - `activeSandboxId === null` → the default group backend (no re-establish).
|
|
1602
|
+
* - a selfhosted target → a `SelfhostedSession` bound to the enrollment agentId,
|
|
1603
|
+
* fenced under `pointer.activeEpoch` (echoed on every ControlRequest so the
|
|
1604
|
+
* agent can reject a stale op with ERROR_CODE_FENCED — the swap-race fence).
|
|
1605
|
+
* - a modal target → `establishModalTarget` (the resume-by-id closure), else
|
|
1606
|
+
* unresolvable.
|
|
1607
|
+
*/
|
|
1608
|
+
declare function makeActiveBackendResolver(deps: ActiveBackendResolverDeps): (pointer: ActivePointer) => Promise<ResolvedActiveBackend>;
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Construct the raw provider SandboxClient for the configured backend. Registry-
|
|
1612
|
+
* driven (the old flat if/else is gone): the backend's ProviderRegistration owns
|
|
1613
|
+
* validateCredentials + build, with per-provider units/field-names. Returns
|
|
1614
|
+
* undefined for "none".
|
|
1615
|
+
*
|
|
1616
|
+
* The desktop stream port (6080) is merged into exposedPorts for every desktop-
|
|
1617
|
+
* capable (backend, os) when desktop is enabled AND the provider cannot expose
|
|
1618
|
+
* ports on demand (modal/runloop/e2b pre-declare; blaxel resolves on demand).
|
|
1619
|
+
* Existing modal/docker/local construction is behavior-preserved.
|
|
1620
|
+
*/
|
|
1621
|
+
declare function createSandboxClient(settings: Settings, environment?: Record<string, string>): unknown;
|
|
1622
|
+
/**
|
|
1623
|
+
* Construct the raw provider SandboxClient for an EXPLICIT backend, independent
|
|
1624
|
+
* of settings.sandboxBackend. This is the resume-by-id builder the per-turn
|
|
1625
|
+
* resume path (and the API-direct control plane) call: a lease's box was created
|
|
1626
|
+
* on a specific backend (the envelope's backendId / the lease's
|
|
1627
|
+
* resume_backend_id), and the client that reattaches to it must be built for
|
|
1628
|
+
* THAT backend, not the process's currently-configured default. When the backend
|
|
1629
|
+
* equals settings.sandboxBackend this is identical to createSandboxClient
|
|
1630
|
+
* (behavior-preserved). Returns undefined for "none".
|
|
1631
|
+
*/
|
|
1632
|
+
declare function createSandboxClientForBackend(backend: SandboxBackend, settings: Settings, environment?: Record<string, string>): unknown;
|
|
1633
|
+
/**
|
|
1634
|
+
* Extract the sandbox recovery entry from a run state as a plain JSON record,
|
|
1635
|
+
* for storage decoupled from the RunState blob (issue #35). Encapsulates the
|
|
1636
|
+
* underscore-internal `_sandbox` read in exactly one place.
|
|
1637
|
+
*/
|
|
1638
|
+
declare function sandboxStateEntryFromRunState(state: unknown): Record<string, unknown> | null;
|
|
1639
|
+
/**
|
|
1640
|
+
* Items-mode counterpart of restoredSandboxSessionState: rebuild the live
|
|
1641
|
+
* sandbox session state from a stored entry (as produced by
|
|
1642
|
+
* sandboxStateEntryFromRunState) instead of from a RunState blob.
|
|
1643
|
+
*/
|
|
1644
|
+
declare function restoredSandboxSessionStateFromEntry(entry: Record<string, unknown>, client: unknown): Promise<SandboxSessionState | undefined>;
|
|
1645
|
+
/**
|
|
1646
|
+
* Read the persisted /workspace snapshot archive off a lease envelope's
|
|
1647
|
+
* `sessionState` (sandbox-file-persistence). The reaper (persistDrainSnapshot)
|
|
1648
|
+
* folds the base64 archive — a Modal native snapshot-ref or a tar archive, the
|
|
1649
|
+
* exact bytes `session.persistWorkspace()` returned — at
|
|
1650
|
+
* `sessionState.workspaceArchive`. Cold-restore decodes it and replays it via
|
|
1651
|
+
* `session.hydrateWorkspace(archive)` on the freshly-created box so /workspace is
|
|
1652
|
+
* restored. Returns undefined when the envelope carries no archive (a box that
|
|
1653
|
+
* was never drain-persisted, or a non-persistence config that stored none).
|
|
1654
|
+
*
|
|
1655
|
+
* It is deliberately read SEPARATELY from deserializeSandboxSessionStateEnvelope:
|
|
1656
|
+
* the archive does NOT ride serializeSessionState (it originates at reaper time),
|
|
1657
|
+
* and the SDK's deserializeSessionState must NOT receive it (it is an opaque
|
|
1658
|
+
* runtime-level field, not provider state).
|
|
1659
|
+
*/
|
|
1660
|
+
declare function readWorkspaceArchiveFromEnvelopeSessionState(sessionState: unknown): Uint8Array | undefined;
|
|
1661
|
+
/** Decode the Modal snapshot id out of a persisted base64 archive ref, or
|
|
1662
|
+
* undefined when the archive is a tar payload (no provider snapshot to GC) or
|
|
1663
|
+
* is unparseable. Used only for keep-latest-per-lease snapshot GC. */
|
|
1664
|
+
declare function decodeModalSnapshotId(archive: Uint8Array): string | undefined;
|
|
1665
|
+
/**
|
|
1666
|
+
* Best-effort GC of a SUPERSEDED Modal filesystem/directory snapshot
|
|
1667
|
+
* (sandbox-file-persistence). restoreSnapshotFilesystem terminates the previous
|
|
1668
|
+
* SANDBOX but never deletes the prior SNAPSHOT image, so snapshots accumulate
|
|
1669
|
+
* unbounded across warm/cold cycles. The reaper keeps only the latest per lease:
|
|
1670
|
+
* when it writes a NEW archive it passes the PRIOR archive here to delete its
|
|
1671
|
+
* image via the live session's Modal client (`session.modal.images.delete(id)` —
|
|
1672
|
+
* the same API the SDK uses for directory images). Never throws (GC is a
|
|
1673
|
+
* best-effort backstop; a leaked snapshot is a cost issue, not a correctness one).
|
|
1674
|
+
* A tar archive (no snapshot id) is a no-op. Returns the deleted snapshot id (or
|
|
1675
|
+
* undefined when nothing was deleted) for observability.
|
|
1676
|
+
*/
|
|
1677
|
+
declare function deletePriorPersistedSnapshot(session: unknown, priorArchiveBase64: string | null | undefined): Promise<string | undefined>;
|
|
1678
|
+
declare function deserializeSandboxSessionStateEnvelope(client: SandboxClient, envelope: unknown): Promise<SandboxSessionState | undefined>;
|
|
1679
|
+
/** A live, externally-owned sandbox session re-established from the group lease
|
|
1680
|
+
* envelope. The caller injects `{client, session, sessionState}` NON-OWNED into
|
|
1681
|
+
* the run (or drives session.exec/readFile/resolveExposedPort directly) and
|
|
1682
|
+
* drops the handle when done — the lease, not this handle, owns the box. */
|
|
1683
|
+
type EstablishedSandboxSession = {
|
|
1684
|
+
client: unknown;
|
|
1685
|
+
session: unknown;
|
|
1686
|
+
sessionState: unknown;
|
|
1687
|
+
instanceId: string;
|
|
1688
|
+
backendId: string;
|
|
1689
|
+
};
|
|
1690
|
+
/**
|
|
1691
|
+
* Per-provider NotFound discriminator. The @openai/agents-extensions
|
|
1692
|
+
* `isProviderSandboxNotFoundError` / `assertResumeRecreateAllowed` helpers live
|
|
1693
|
+
* under `@openai/agents-extensions/sandbox/shared`, which is NOT an exported
|
|
1694
|
+
* subpath (the package `exports` map only exposes `./sandbox/<provider>`), so we
|
|
1695
|
+
* re-implement the discrimination here by inspecting the thrown error shape.
|
|
1696
|
+
*
|
|
1697
|
+
* "Box no longer running" (the box was reaped / idled out / 24h-ceiling) is the
|
|
1698
|
+
* ONLY error that licenses a cold-restore via create(). Every other resume
|
|
1699
|
+
* failure (transient provider error, auth, network) must propagate so the caller
|
|
1700
|
+
* backs off — never spawns a rival box. We err on the side of NOT recreating:
|
|
1701
|
+
* an unrecognized error is treated as "not NotFound" (propagate), because a
|
|
1702
|
+
* false-positive recreate is the dangerous direction (double-spawn).
|
|
1703
|
+
*/
|
|
1704
|
+
declare function isProviderSandboxNotFoundError(backendId: string, error: unknown): boolean;
|
|
1705
|
+
/**
|
|
1706
|
+
* Resume the one box by id from its recovery envelope, or cold-restore from the
|
|
1707
|
+
* snapshot when the provider reports it gone. The envelope is the lease's
|
|
1708
|
+
* box-identity descriptor (the same per-turn `_sandbox` envelope upserted by the
|
|
1709
|
+
* turn activity). A null envelope means a cold session that was never warmed →
|
|
1710
|
+
* create() directly.
|
|
1711
|
+
*
|
|
1712
|
+
* - `opts.backendOverride ?? envelope.backendId ?? settings.sandboxBackend`
|
|
1713
|
+
* selects the backend; the client is built for THAT backend (resume-by-id is
|
|
1714
|
+
* fenced to the original provider).
|
|
1715
|
+
* - warm reattach: deserialize the envelope sessionState → client.resume(state)
|
|
1716
|
+
* (no lock; R4-safe). On a provider NotFound, cold-restore via create().
|
|
1717
|
+
* - cold restore / cold session: client.create() — the ONLY create() site.
|
|
1718
|
+
*/
|
|
1719
|
+
declare function establishSandboxSessionFromEnvelope(settings: Settings, envelope: Record<string, unknown> | null, opts: {
|
|
1720
|
+
sessionId: string;
|
|
1721
|
+
backendOverride?: SandboxBackend;
|
|
1722
|
+
environment?: Record<string, string>;
|
|
1723
|
+
}): Promise<EstablishedSandboxSession>;
|
|
1724
|
+
/**
|
|
1725
|
+
* Fold a freshly-established (or resumed) sandbox session into the persistable
|
|
1726
|
+
* `resume_state` envelope the lease stores — the SAME `{ backendId, sessionState }`
|
|
1727
|
+
* shape `establishSandboxSessionFromEnvelope` consumes to RESUME BY ID. The
|
|
1728
|
+
* API-direct control plane (viewer attach / Channel-A) MUST persist this onto the
|
|
1729
|
+
* lease at warm-commit time, or a later op (which reads the lease's resume_state)
|
|
1730
|
+
* has nothing to resume from and COLD-CREATES A RIVAL BOX — the box-churn the
|
|
1731
|
+
* prove-it surfaced (fs.write then fs.read 404'd on a different box; N Channel-A
|
|
1732
|
+
* ops leaked N boxes). Returns null when the client cannot serialize (the caller
|
|
1733
|
+
* stores null and the box rides the provider idle-timeout — no rival spawn, just
|
|
1734
|
+
* no warm-reattach).
|
|
1735
|
+
*/
|
|
1736
|
+
declare function serializeEstablishedSandboxEnvelope(established: EstablishedSandboxSession): Promise<Record<string, unknown> | null>;
|
|
1737
|
+
|
|
1738
|
+
export { type ActiveBackendResolverDeps, ActiveBackendUnresolvableError, type ActivePointer, ChannelAConflictError, type ChannelAEmitter, type ChannelAExecArgs, type ChannelAExecResult, ChannelANotFoundError, type ChannelASession, ChannelAUnsupportedError, ChannelAValidationError, type ControlRpc, DEFAULT_DESKTOP_GEOMETRY, DISPLAY_STACK_TIMEOUT_MS, type DesktopGeometry, DisplayStackError, DisplayStackUnsupportedError, type EnsureDisplayStackOptions, type EnsureDisplayStackResult, type EnsureTerminalServerOptions, type EnsureTerminalServerResult, type EstablishedSandboxSession, type ExposeStreamPortInput, type ExposeStreamPortResult, type ExposedPortEndpoint, type FinalizeRecordingResult, type MintStreamTokenInput, MockAgentResponder, type MockAgentResponderOptions, type MockExecHandler, NatsControlRpc, type NatsRequestConnection, type NegotiationContext, type NumstatEntry, PROVIDER_REGISTRY, type ProviderConstructionContext, type ProviderRegistration, type RecordingCodec, type RecordingContentType, RecordingError, type RecordingProcess, RecordingUnavailableError, type ResolvedActiveBackend, type RoutableBackendSession, type RoutableSandbox, RoutingSandboxSession, type RoutingSandboxSessionDeps, type RoutingTransitionEvent, RoutingUnsupportedError, SELFHOSTED_DEFAULT_TIMEOUT_MS, SELFHOSTED_RECONNECT_WINDOW_MS, SELFHOSTED_RELAY_STREAM_PATH, STREAM_PORT, STREAM_TOKEN_DEFAULT_TTL_SECONDS, SandboxChannelAService, type SandboxChannelAServiceOptions, SandboxConfigError, SandboxProviderUnavailableError, type SelfhostedApplyDiff, SelfhostedControlError, type SelfhostedEditor, type SelfhostedEnrollment, type SelfhostedExecArgs, type SelfhostedExecResult, type SelfhostedImageOutput, type SelfhostedLivenessState, type SelfhostedNegotiationInput, type SelfhostedRelayConfig, SelfhostedSandboxClient, SelfhostedSession, type SelfhostedSessionBuild, type SelfhostedSessionDeps, type SelfhostedSessionState, type SelfhostedUnavailableReason, type StartRecordingInput, StreamPortUnavailableError, TERMINAL_SERVER_TIMEOUT_MS, TerminalServerError, TerminalServerUnsupportedError, agentErrorToControlError, assertDescriptorRegistryInvariants, assertProviderRegistryInvariants, assertSafeRelPath, backendSupportsOs, buildDisplayStackScript, buildSelfhostedBackendSession, buildStreamUrl, buildTerminalServerScript, contentTypeForCodec, createSandboxClient, createSandboxClientForBackend, decodeModalSnapshotId, deletePriorPersistedSnapshot, deleteRecordingArtifacts, deserializeSandboxSessionStateEnvelope, desktopCapableBackend, ensureDisplayStack, ensureTerminalServer, establishSandboxSessionFromEnvelope, exposeStreamPort, extForCodec, isExecSessionLostBanner, isProviderSandboxNotFoundError, isSelfhostedProviderNotFoundError, isWorkspaceEscapeError, makeActiveBackendResolver, mintStreamToken, negotiateCapabilities, negotiateSelfhostedCapabilities, offlineAgentError, offlineControlResponse, parseExecBannerSessionId, parseNumstatZ, parsePorcelainV2, parseUnifiedPatch, readRecordingBytes, readWorkspaceArchiveFromEnvelopeSessionState, recordingStorageKey, restoredSandboxSessionStateFromEntry, sandboxStateEntryFromRunState, selectBackend, selfhostedLiveness, serializeEstablishedSandboxEnvelope, setSelfhostedApplyDiff, startRecording, stopRecording, stripExecBanner, subjectFor, tearDownDisplayStack, tearDownTerminalServer, timeoutAgentError, timeoutControlResponse, verifyStreamToken };
|