@opengeni/sdk 0.2.0 → 0.4.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/index.d.ts +731 -5
- package/dist/index.js +305 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/client.ts +241 -0
- package/src/desktop.ts +152 -0
- package/src/index.ts +104 -0
- package/src/terminal.ts +91 -0
- package/src/types.ts +432 -1
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Zero-dependency interactive-terminal (ttyd PTY-over-websocket) transport
|
|
2
|
+
// contract. Symmetric with `desktop.ts`: the SDK ships only the pure transport
|
|
3
|
+
// CONTRACT (a URL assembler + the ttyd wire-protocol frame codec), with no DOM
|
|
4
|
+
// and no deps. The actual WebSocket open + xterm attach lives in `@opengeni/react`
|
|
5
|
+
// (`use-terminal-stream.ts`).
|
|
6
|
+
//
|
|
7
|
+
// The interactive terminal is a REAL PTY streamed over the SAME Modal raw-TLS
|
|
8
|
+
// tunnel as the desktop noVNC, with the SAME scoped stream-token mechanism. The
|
|
9
|
+
// box bakes `ttyd` on `TERMINAL_STREAM_PORT` (7681); `session.resolveExposedPort`
|
|
10
|
+
// mints the tunnel URL. The Terminal capability cell carries transport "pty-ws" +
|
|
11
|
+
// that url + the scoped token when the box is warm and a viewer is attached.
|
|
12
|
+
|
|
13
|
+
import type { TerminalCapability } from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Translate the negotiated `pty-ws` Terminal capability into the WebSocket URL
|
|
17
|
+
* the ttyd client dials. The scoped provider token is ALREADY embedded in the
|
|
18
|
+
* minted `url` (the Modal tunnel host) by `session.resolveExposedPort(7681)` —
|
|
19
|
+
* we do NOT append `cap.token` (identical posture to the desktop: the gate is the
|
|
20
|
+
* unguessable short-TTL tunnel URL + the server-recorded scoped stream token; ttyd
|
|
21
|
+
* runs `--writable` with no `-c` credential in v1). We only normalize the scheme
|
|
22
|
+
* to `ws`/`wss`; a bare host is already the ttyd websocket endpoint.
|
|
23
|
+
*/
|
|
24
|
+
export function terminalSocketUrl(cap: Pick<TerminalCapability, "url">): string {
|
|
25
|
+
if (!cap.url) {
|
|
26
|
+
throw new Error("terminal capability has no url (transport is not pty-ws)");
|
|
27
|
+
}
|
|
28
|
+
const u = new URL(cap.url);
|
|
29
|
+
// https → wss, http → ws (ttyd is dialed as a WebSocket, not an HTTP page).
|
|
30
|
+
if (u.protocol === "https:") {
|
|
31
|
+
u.protocol = "wss:";
|
|
32
|
+
} else if (u.protocol === "http:") {
|
|
33
|
+
u.protocol = "ws:";
|
|
34
|
+
}
|
|
35
|
+
return u.toString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── ttyd wire protocol ───────────────────────────────────────────────────────
|
|
39
|
+
// ttyd frames are a single ASCII command char + payload. These mirror ttyd's
|
|
40
|
+
// `protocol.h` Command enum. Client→server and server→client share the byte 0/1/2
|
|
41
|
+
// values but mean different things per direction (see below).
|
|
42
|
+
|
|
43
|
+
/** ttyd subprotocol — REQUIRED on the WebSocket handshake or ttyd refuses it. */
|
|
44
|
+
export const TTYD_SUBPROTOCOL = "tty";
|
|
45
|
+
|
|
46
|
+
/** Client→server command bytes (the first char of each outbound text frame). */
|
|
47
|
+
export const TtydClientCommand = {
|
|
48
|
+
/** stdin: "0" + raw input bytes. */
|
|
49
|
+
INPUT: "0",
|
|
50
|
+
/** window resize: "1" + JSON.stringify({ columns, rows }). */
|
|
51
|
+
RESIZE: "1",
|
|
52
|
+
/** flow-control pause (back-pressure): "2". */
|
|
53
|
+
PAUSE: "2",
|
|
54
|
+
/** flow-control resume: "3". */
|
|
55
|
+
RESUME: "3",
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
/** Server→client command bytes (the first char of each inbound frame). */
|
|
59
|
+
export const TtydServerCommand = {
|
|
60
|
+
/** stdout/stderr: "0" + raw output bytes (write the rest into xterm). */
|
|
61
|
+
OUTPUT: "0",
|
|
62
|
+
/** set the window title: "1" + title string. */
|
|
63
|
+
SET_WINDOW_TITLE: "1",
|
|
64
|
+
/** ttyd client preferences JSON: "2" + json (ignored by us). */
|
|
65
|
+
SET_PREFERENCES: "2",
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The ttyd handshake's first frame: an auth message. ttyd expects
|
|
70
|
+
* `JSON.stringify({ AuthToken })` as the FIRST text frame on the socket. We send
|
|
71
|
+
* an empty token — our gate is the tunnel URL + scoped stream token, NOT a ttyd
|
|
72
|
+
* `-c` basic-auth credential (which the box does not set in v1). Optional ttyd
|
|
73
|
+
* `columns`/`rows` can ride this frame to seed the PTY size before the first
|
|
74
|
+
* resize. Pure (string-building only) so it stays unit-testable in the SDK.
|
|
75
|
+
*/
|
|
76
|
+
export function ttydAuthFrame(opts?: { columns?: number; rows?: number }): string {
|
|
77
|
+
const frame: { AuthToken: string; columns?: number; rows?: number } = { AuthToken: "" };
|
|
78
|
+
if (opts?.columns && opts.columns > 0) frame.columns = opts.columns;
|
|
79
|
+
if (opts?.rows && opts.rows > 0) frame.rows = opts.rows;
|
|
80
|
+
return JSON.stringify(frame);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Build a client→server INPUT (stdin) frame: "0" + data. */
|
|
84
|
+
export function ttydInputFrame(data: string): string {
|
|
85
|
+
return TtydClientCommand.INPUT + data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Build a client→server RESIZE frame: "1" + JSON.stringify({ columns, rows }). */
|
|
89
|
+
export function ttydResizeFrame(columns: number, rows: number): string {
|
|
90
|
+
return TtydClientCommand.RESIZE + JSON.stringify({ columns, rows });
|
|
91
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -11,7 +11,193 @@ export type SessionStatus =
|
|
|
11
11
|
| "failed"
|
|
12
12
|
| "cancelled";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Mirror of `@opengeni/contracts` SandboxBackend (10 values; existing four keep
|
|
15
|
+
// position). 3-way enum parity is pinned by `test/contract-parity.test.ts`.
|
|
16
|
+
export type SandboxBackend =
|
|
17
|
+
| "docker"
|
|
18
|
+
| "modal"
|
|
19
|
+
| "local"
|
|
20
|
+
| "none"
|
|
21
|
+
| "daytona"
|
|
22
|
+
| "runloop"
|
|
23
|
+
| "e2b"
|
|
24
|
+
| "blaxel"
|
|
25
|
+
| "cloudflare"
|
|
26
|
+
| "vercel";
|
|
27
|
+
|
|
28
|
+
// Mirror of `@opengeni/contracts` SandboxOs. Only "linux" is reachable in v1.
|
|
29
|
+
export type SandboxOs = "linux" | "macos" | "windows";
|
|
30
|
+
|
|
31
|
+
// Mirror of `@opengeni/contracts` SandboxCapabilityName.
|
|
32
|
+
export type SandboxCapabilityName =
|
|
33
|
+
| "FileSystem"
|
|
34
|
+
| "Terminal"
|
|
35
|
+
| "Git"
|
|
36
|
+
| "DesktopStream"
|
|
37
|
+
| "Recording";
|
|
38
|
+
|
|
39
|
+
// Mirror of `@opengeni/contracts` CapabilityUnavailableReason.
|
|
40
|
+
export type CapabilityUnavailableReason =
|
|
41
|
+
| "backend_unsupported"
|
|
42
|
+
| "os_unsupported"
|
|
43
|
+
| "not_provisioned"
|
|
44
|
+
| "disabled_by_policy"
|
|
45
|
+
| "lease_cold"
|
|
46
|
+
| "tier_headless";
|
|
47
|
+
|
|
48
|
+
// Mirror of `@opengeni/contracts` SessionCapabilities (the negotiated handshake
|
|
49
|
+
// document). The descriptor table itself is NOT mirrored — it lives in
|
|
50
|
+
// contracts (P0.1) and is consumed by the SDK config in a later PR.
|
|
51
|
+
export type SessionCapabilities = {
|
|
52
|
+
sessionId: string;
|
|
53
|
+
backend: SandboxBackend;
|
|
54
|
+
os: SandboxOs;
|
|
55
|
+
liveness: "cold" | "warming" | "warm" | "draining";
|
|
56
|
+
leaseEpoch: number;
|
|
57
|
+
viewerHeartbeatIntervalMs: number;
|
|
58
|
+
FileSystem: {
|
|
59
|
+
available: boolean;
|
|
60
|
+
readOnly: boolean;
|
|
61
|
+
root: string;
|
|
62
|
+
pathSep: "/" | "\\";
|
|
63
|
+
treeMode: "lazy" | "snapshot";
|
|
64
|
+
reason: CapabilityUnavailableReason | null;
|
|
65
|
+
};
|
|
66
|
+
Terminal: {
|
|
67
|
+
transport: "sse-events" | "pty-ws" | null;
|
|
68
|
+
ptyCapable: boolean;
|
|
69
|
+
shell: string;
|
|
70
|
+
url: string | null;
|
|
71
|
+
token: string | null;
|
|
72
|
+
reason: CapabilityUnavailableReason | null;
|
|
73
|
+
};
|
|
74
|
+
Git: {
|
|
75
|
+
available: boolean;
|
|
76
|
+
repos: string[];
|
|
77
|
+
reason: CapabilityUnavailableReason | null;
|
|
78
|
+
};
|
|
79
|
+
DesktopStream: {
|
|
80
|
+
transport: "vnc-ws" | "rdp-ws" | "webrtc" | null;
|
|
81
|
+
client: "novnc" | "web-rdp" | null;
|
|
82
|
+
mode: "read-only" | "interactive";
|
|
83
|
+
url: string | null;
|
|
84
|
+
token: string | null;
|
|
85
|
+
expiresAt: string | null;
|
|
86
|
+
resolution: [number, number];
|
|
87
|
+
unredacted: boolean;
|
|
88
|
+
requiresAcknowledgment: boolean;
|
|
89
|
+
acknowledged: boolean;
|
|
90
|
+
// Shared-exposure disclosure (addendum E.1): `shared` when the group has >1
|
|
91
|
+
// session; `sharedSessionIds` lists the OTHER sessions' ids ONLY (never their
|
|
92
|
+
// conversation/metadata).
|
|
93
|
+
shared: boolean;
|
|
94
|
+
sharedSessionIds: string[];
|
|
95
|
+
reason: CapabilityUnavailableReason | null;
|
|
96
|
+
};
|
|
97
|
+
Recording: {
|
|
98
|
+
available: boolean;
|
|
99
|
+
modes: ("manual" | "on-turn" | "on-verify")[];
|
|
100
|
+
codecs: ("h264-mp4" | "vp9-webm")[];
|
|
101
|
+
reason: CapabilityUnavailableReason | null;
|
|
102
|
+
};
|
|
103
|
+
ComputerUse: {
|
|
104
|
+
available: boolean;
|
|
105
|
+
readOnly: boolean;
|
|
106
|
+
reason: CapabilityUnavailableReason | null;
|
|
107
|
+
};
|
|
108
|
+
negotiatedAt: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Convenience aliases for the per-surface cells of `SessionCapabilities`, so the
|
|
112
|
+
// client hooks/components can take a single cell without restating the inline
|
|
113
|
+
// shape. These are exact structural views of the cells above.
|
|
114
|
+
export type FileSystemCapability = SessionCapabilities["FileSystem"];
|
|
115
|
+
export type TerminalCapability = SessionCapabilities["Terminal"];
|
|
116
|
+
export type GitCapability = SessionCapabilities["Git"];
|
|
117
|
+
export type DesktopStreamCapability = SessionCapabilities["DesktopStream"];
|
|
118
|
+
export type RecordingCapability = SessionCapabilities["Recording"];
|
|
119
|
+
export type ComputerUseCapability = SessionCapabilities["ComputerUse"];
|
|
120
|
+
|
|
121
|
+
// ── Stream-surfacing client surface (Phase 5) ───────────────────────────────
|
|
122
|
+
// Mirrors of the contracts viewer-attach / acknowledge / heartbeat shapes that
|
|
123
|
+
// the capability-gated client (`@opengeni/react`) drives. The desktop pixel
|
|
124
|
+
// plane rides Channel B (direct-to-provider noVNC); the structured terminal/
|
|
125
|
+
// files/git surfaces ride Channel A (the existing event spine + the synchronous
|
|
126
|
+
// fs/git/terminal point queries above). These are TYPES only (the SDK keeps zero
|
|
127
|
+
// runtime deps); the contract-parity test pins them.
|
|
128
|
+
|
|
129
|
+
// Mirror of `@opengeni/contracts` StreamUrlRotatedPayload — the Channel-A event
|
|
130
|
+
// the client folds in to hot-swap its noVNC socket on a box rollover, fenced on
|
|
131
|
+
// leaseEpoch.
|
|
132
|
+
export type StreamUrlRotatedPayload = {
|
|
133
|
+
url: string;
|
|
134
|
+
token: string | null;
|
|
135
|
+
expiresAt: string | null;
|
|
136
|
+
leaseEpoch: number;
|
|
137
|
+
transport: "vnc-ws";
|
|
138
|
+
viewerId: string | null;
|
|
139
|
+
};
|
|
140
|
+
export type StreamOpenedPayload = { viewerId: string; shared: boolean; viewerCount: number };
|
|
141
|
+
export type StreamClosedPayload = {
|
|
142
|
+
viewerId: string;
|
|
143
|
+
reason: "client-disconnect" | "reaped" | "revoked" | "box-rollover";
|
|
144
|
+
viewerCount: number;
|
|
145
|
+
};
|
|
146
|
+
export type StreamRevokedPayload = {
|
|
147
|
+
viewerId: string | null;
|
|
148
|
+
reason: "grant-revoked" | "session-failed" | "admin";
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Mirror of `@opengeni/contracts` AttachViewerRequest. Omitting `viewerId` mints
|
|
152
|
+
// a fresh holder id (returned on the response, carried through heartbeat/detach).
|
|
153
|
+
// `desktop:true` opts into the un-redacted pixel plane (the consent-gated noVNC
|
|
154
|
+
// stream); a terminal/files-only warm attach omits it (defaults false) so it
|
|
155
|
+
// warms the box + mints the pty-ws terminal cell WITHOUT tripping the consent 409.
|
|
156
|
+
export type AttachViewerRequest = { viewerId?: string | undefined; desktop?: boolean | undefined };
|
|
157
|
+
|
|
158
|
+
// Mirror of `@opengeni/contracts` ViewerHolder + the P4.2 desktop-stream fields
|
|
159
|
+
// the POST /viewers handler folds in when the pixel plane is minted in-process.
|
|
160
|
+
export type ViewerHolder = {
|
|
161
|
+
viewerId: string;
|
|
162
|
+
sandboxGroupId: string;
|
|
163
|
+
liveness: "cold" | "warming" | "warm" | "draining";
|
|
164
|
+
leaseEpoch: number;
|
|
165
|
+
viewerHeartbeatIntervalMs: number;
|
|
166
|
+
dataPlaneUrl: string | null;
|
|
167
|
+
};
|
|
168
|
+
export type AttachViewerResponse = ViewerHolder & {
|
|
169
|
+
// The scoped desktop-stream address minted for THIS holder (P4.2). Null when
|
|
170
|
+
// the deployment is headless / desktop is disabled / the mint degraded —
|
|
171
|
+
// the client then falls back to the Channel-A surfaces only.
|
|
172
|
+
streamToken: string | null;
|
|
173
|
+
streamExpiresAt: string | null;
|
|
174
|
+
resolution: [number, number] | null;
|
|
175
|
+
transport: "vnc-ws" | null;
|
|
176
|
+
client: "novnc" | null;
|
|
177
|
+
// The scoped ttyd PTY-over-websocket address minted for THIS holder — the REAL
|
|
178
|
+
// interactive terminal, symmetric with the desktop pixel plane (same Modal
|
|
179
|
+
// tunnel, same scoped stream token). Populated on a warm box; null when the
|
|
180
|
+
// terminal mint degraded (headless / no secret / tunnel failure), in which case
|
|
181
|
+
// the client falls back to the Channel-A read-only command-output firehose.
|
|
182
|
+
// `terminalTransport` is "pty-ws" iff a live `terminalUrl` was minted.
|
|
183
|
+
terminalUrl: string | null;
|
|
184
|
+
terminalToken: string | null;
|
|
185
|
+
terminalTransport: "pty-ws" | null;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Mirror of `@opengeni/contracts` AcknowledgeStreamRequest/Response — the
|
|
189
|
+
// un-redacted-pixel + shared-exposure consent gate (P3.2).
|
|
190
|
+
export type AcknowledgeStreamRequest = {
|
|
191
|
+
acknowledgeUnredacted?: boolean | undefined;
|
|
192
|
+
acknowledgeShared?: boolean | undefined;
|
|
193
|
+
};
|
|
194
|
+
export type AcknowledgeStreamResponse = { acknowledged: boolean; acknowledgedShared: boolean };
|
|
195
|
+
|
|
196
|
+
// Mirror of `@opengeni/contracts` ViewerHeartbeatRequest/Response — the
|
|
197
|
+
// Channel-A viewer-liveness ping, epoch-fenced (a stale-epoch beat → alive:false
|
|
198
|
+
// → the client re-attaches).
|
|
199
|
+
export type ViewerHeartbeatRequest = { leaseEpoch: number };
|
|
200
|
+
export type ViewerHeartbeatResponse = { alive: boolean };
|
|
15
201
|
|
|
16
202
|
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
17
203
|
|
|
@@ -130,6 +316,23 @@ export const SESSION_EVENT_TYPES = [
|
|
|
130
316
|
"goal.paused",
|
|
131
317
|
"goal.resumed",
|
|
132
318
|
"goal.continuation",
|
|
319
|
+
// Channel-B desktop pixel-plane signals (mirror of contracts SessionEventType;
|
|
320
|
+
// the contract-parity test asserts sorted equality).
|
|
321
|
+
"stream.url.rotated",
|
|
322
|
+
"stream.opened",
|
|
323
|
+
"stream.closed",
|
|
324
|
+
"stream.revoked",
|
|
325
|
+
// Channel-B recording signals (P4.3 — "agent films itself proving the fix").
|
|
326
|
+
"recording.started",
|
|
327
|
+
"recording.available",
|
|
328
|
+
"recording.failed",
|
|
329
|
+
// Channel-A structured-service notifications (P4.4; mirror of contracts
|
|
330
|
+
// SessionEventType — the contract-parity test asserts sorted equality).
|
|
331
|
+
"fs.changed",
|
|
332
|
+
"git.changed",
|
|
333
|
+
"terminal.pty.started",
|
|
334
|
+
"terminal.pty.output.delta",
|
|
335
|
+
"terminal.pty.exited",
|
|
133
336
|
] as const;
|
|
134
337
|
|
|
135
338
|
export type KnownSessionEventType = (typeof SESSION_EVENT_TYPES)[number];
|
|
@@ -166,6 +369,144 @@ export type AgentToolCallCreatedPayload = {
|
|
|
166
369
|
export type AgentToolCallOutputPayload = { id: string | null; output: unknown };
|
|
167
370
|
export type SessionStatusChangedPayload = { status: SessionStatus };
|
|
168
371
|
|
|
372
|
+
// Recording payloads (P4.3 — plain TS mirror of the contracts Zod schemas; the
|
|
373
|
+
// SDK is zero-runtime-dep so these are TYPES, not Zod, F15). The contract-parity
|
|
374
|
+
// test asserts the event-type literals; these shapes document the wire payloads.
|
|
375
|
+
export type RecordingMode = "manual" | "on-turn" | "on-verify";
|
|
376
|
+
export type RecordingCodec = "h264-mp4" | "vp9-webm";
|
|
377
|
+
export type RecordingContentType = "video/mp4" | "video/webm";
|
|
378
|
+
export type RecordingFailedReason =
|
|
379
|
+
| "ffmpeg-error"
|
|
380
|
+
| "box-death"
|
|
381
|
+
| "box-rollover"
|
|
382
|
+
| "upload-failed"
|
|
383
|
+
| "max-bytes-exceeded"
|
|
384
|
+
| "display-unavailable";
|
|
385
|
+
|
|
386
|
+
export type RecordingStartedPayload = {
|
|
387
|
+
recordingId: string;
|
|
388
|
+
turnId: string | null;
|
|
389
|
+
mode: RecordingMode;
|
|
390
|
+
codec: RecordingCodec;
|
|
391
|
+
dimensions: [number, number];
|
|
392
|
+
framerate: number;
|
|
393
|
+
startedAt: string;
|
|
394
|
+
reason?: string | null | undefined;
|
|
395
|
+
};
|
|
396
|
+
export type RecordingAvailablePayload = {
|
|
397
|
+
recordingId: string;
|
|
398
|
+
turnId: string | null;
|
|
399
|
+
codec: RecordingCodec;
|
|
400
|
+
contentType: RecordingContentType;
|
|
401
|
+
storageKey: string;
|
|
402
|
+
durationSeconds: number | null;
|
|
403
|
+
sizeBytes: number;
|
|
404
|
+
dimensions: [number, number];
|
|
405
|
+
};
|
|
406
|
+
export type RecordingFailedPayload = {
|
|
407
|
+
recordingId: string;
|
|
408
|
+
turnId: string | null;
|
|
409
|
+
reason: RecordingFailedReason;
|
|
410
|
+
detail?: string | null | undefined;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// ── Channel-A structured services (P4.4) — hand-written wire mirrors ─────────
|
|
414
|
+
|
|
415
|
+
// A1 notification payloads.
|
|
416
|
+
export type SandboxCommandOutputDeltaPayload = {
|
|
417
|
+
stream: "stdout" | "stderr";
|
|
418
|
+
chunk: string;
|
|
419
|
+
commandId?: string | undefined;
|
|
420
|
+
seq?: number | undefined;
|
|
421
|
+
};
|
|
422
|
+
export type FsChangeKind = "created" | "modified" | "deleted" | "renamed";
|
|
423
|
+
export type FsChangedPayload = {
|
|
424
|
+
changes: { path: string; kind: FsChangeKind; isDir: boolean; sizeBytes: number | null; oldPath?: string | undefined }[];
|
|
425
|
+
source: "write" | "watch" | "agent";
|
|
426
|
+
revision: number;
|
|
427
|
+
leaseEpoch: number;
|
|
428
|
+
};
|
|
429
|
+
export type GitChangedPayload = {
|
|
430
|
+
head: string | null;
|
|
431
|
+
dirty: boolean;
|
|
432
|
+
ahead: number;
|
|
433
|
+
behind: number;
|
|
434
|
+
changedFileCount: number;
|
|
435
|
+
reason: "commit" | "checkout" | "stage" | "worktree" | "fetch" | "unknown";
|
|
436
|
+
revision: number;
|
|
437
|
+
leaseEpoch: number;
|
|
438
|
+
};
|
|
439
|
+
export type TerminalPtyStartedPayload = { ptyId: string; cols: number; rows: number; shell: string; cwd: string };
|
|
440
|
+
export type TerminalPtyOutputDeltaPayload = { ptyId: string; stream: "stdout" | "stderr"; chunk: string; seq: number };
|
|
441
|
+
export type TerminalPtyExitedPayload = { ptyId: string; exitCode: number | null; reason: "exit" | "killed" | "owner_gone" | "timeout" };
|
|
442
|
+
|
|
443
|
+
// A2 FileSystem request/response.
|
|
444
|
+
export type FsNodeType = "file" | "dir" | "symlink" | "other";
|
|
445
|
+
export type FsTreeNode = {
|
|
446
|
+
name: string;
|
|
447
|
+
path: string;
|
|
448
|
+
type: FsNodeType;
|
|
449
|
+
sizeBytes: number | null;
|
|
450
|
+
mtimeMs: number | null;
|
|
451
|
+
mode: number | null;
|
|
452
|
+
children?: FsTreeNode[] | undefined;
|
|
453
|
+
truncated: boolean;
|
|
454
|
+
};
|
|
455
|
+
export type FsEncoding = "utf8" | "base64";
|
|
456
|
+
export type FsListRequest = { path?: string; depth?: number; maxEntries?: number; includeHidden?: boolean };
|
|
457
|
+
export type FsListResponse = { root: FsTreeNode; revision: number; truncated: boolean };
|
|
458
|
+
export type FsReadRequest = { path: string; encoding?: FsEncoding; maxBytes?: number };
|
|
459
|
+
export type FsReadResponse = { path: string; encoding: FsEncoding; content: string; sizeBytes: number; truncated: boolean; isBinary: boolean; revision: number };
|
|
460
|
+
export type FsWriteRequest = { path: string; encoding?: FsEncoding; content: string; overwrite?: boolean; createParents?: boolean };
|
|
461
|
+
export type FsWriteResponse = { path: string; sizeBytes: number; revision: number };
|
|
462
|
+
export type FsDeleteRequest = { path: string; recursive?: boolean };
|
|
463
|
+
export type FsDeleteResponse = { revision: number };
|
|
464
|
+
export type FsMoveRequest = { path: string; newPath: string; overwrite?: boolean; createParents?: boolean };
|
|
465
|
+
export type FsMoveResponse = { path: string; newPath: string; revision: number };
|
|
466
|
+
export type FsMkdirRequest = { path: string; recursive?: boolean };
|
|
467
|
+
export type FsMkdirResponse = { path: string; revision: number };
|
|
468
|
+
|
|
469
|
+
// A2 Git request/response (the Pierre-diff feed).
|
|
470
|
+
export type GitFileStatusCode = "added" | "modified" | "deleted" | "renamed" | "copied" | "untracked" | "ignored" | "conflicted" | "typechange";
|
|
471
|
+
export type GitFileStatus = { path: string; oldPath: string | null; index: GitFileStatusCode | null; worktree: GitFileStatusCode | null; isConflicted: boolean };
|
|
472
|
+
export type GitStatusRequest = { path?: string };
|
|
473
|
+
export type GitStatusResponse = { isRepo: boolean; head: string | null; detached: boolean; upstream: string | null; ahead: number; behind: number; files: GitFileStatus[]; revision: number };
|
|
474
|
+
export type GitDiffLineType = "context" | "add" | "del" | "meta";
|
|
475
|
+
export type GitDiffLine = { type: GitDiffLineType; oldNo: number | null; newNo: number | null; text: string };
|
|
476
|
+
export type GitDiffHunk = { oldStart: number; oldLines: number; newStart: number; newLines: number; header: string; lines: GitDiffLine[] };
|
|
477
|
+
export type GitFileDiff = { path: string; oldPath: string | null; status: GitFileStatusCode; isBinary: boolean; isImage: boolean; additions: number; deletions: number; hunks: GitDiffHunk[]; truncated: boolean };
|
|
478
|
+
export type GitDiffRequest = { path?: string; staged?: boolean; fromRef?: string; toRef?: string; pathspec?: string[]; contextLines?: number; maxBytesPerFile?: number };
|
|
479
|
+
export type GitDiffResponse = { files: GitFileDiff[]; revision: number };
|
|
480
|
+
export type GitLogRequest = { path?: string; ref?: string; maxCount?: number; skip?: number; pathspec?: string[] };
|
|
481
|
+
export type GitCommit = {
|
|
482
|
+
sha: string;
|
|
483
|
+
shortSha: string;
|
|
484
|
+
parents: string[];
|
|
485
|
+
author: { name: string; email: string; timestamp: number };
|
|
486
|
+
committer: { name: string; email: string; timestamp: number };
|
|
487
|
+
subject: string;
|
|
488
|
+
body: string;
|
|
489
|
+
refs: string[];
|
|
490
|
+
};
|
|
491
|
+
export type GitLogResponse = { commits: GitCommit[]; hasMore: boolean };
|
|
492
|
+
export type GitShowRequest = { path?: string; ref: string; filePath?: string; encoding?: FsEncoding; maxBytesPerFile?: number };
|
|
493
|
+
export type GitShowResponse = { commit: GitCommit | null; files: GitFileDiff[]; blob: { content: string; encoding: FsEncoding; sizeBytes: number; truncated: boolean } | null; revision: number };
|
|
494
|
+
|
|
495
|
+
// A2 Terminal exec + PTY.
|
|
496
|
+
export type TerminalExecRequest = { command: string; cwd?: string; timeoutMs?: number; emitStream?: boolean };
|
|
497
|
+
export type TerminalExecResponse = { stdout: string; stderr: string; exitCode: number | null; running: boolean; wallTimeSeconds: number };
|
|
498
|
+
export type PtyOpenRequest = { cols?: number; rows?: number; cwd?: string; shell?: string };
|
|
499
|
+
export type PtyOpenResponse = { ptyId: string; streamVia: "sse-events"; supportsInput: boolean };
|
|
500
|
+
export type PtyWriteRequest = { ptyId: string; data: string };
|
|
501
|
+
export type PtyResizeRequest = { ptyId: string; cols: number; rows: number };
|
|
502
|
+
export type PtyCloseRequest = { ptyId: string };
|
|
503
|
+
|
|
504
|
+
export type SessionStructuredCapabilities = {
|
|
505
|
+
FileSystem: { available: boolean; readOnly: boolean; root: string };
|
|
506
|
+
Terminal: { events: boolean; exec: boolean; pty: { available: boolean } };
|
|
507
|
+
Git: { available: boolean; repos: string[] };
|
|
508
|
+
};
|
|
509
|
+
|
|
169
510
|
export type ScheduledTaskStatus = "active" | "paused";
|
|
170
511
|
|
|
171
512
|
export type ScheduledTaskRunMode = "new_session_per_run" | "reusable_session";
|
|
@@ -258,8 +599,17 @@ export const KNOWN_PERMISSIONS = [
|
|
|
258
599
|
"sessions:create",
|
|
259
600
|
"sessions:read",
|
|
260
601
|
"sessions:control",
|
|
602
|
+
// Sandbox-surfacing (mirror of @opengeni/contracts Permission). stream:view is
|
|
603
|
+
// strictly broader than sessions:read (un-redacted pixels); stream:control is
|
|
604
|
+
// the never-granted-v1 raw-input plane; stream:acknowledge is the secret-leak
|
|
605
|
+
// consent gate.
|
|
606
|
+
"stream:view",
|
|
607
|
+
"stream:control",
|
|
608
|
+
"stream:acknowledge",
|
|
261
609
|
"files:upload",
|
|
262
610
|
"files:read",
|
|
611
|
+
"files:write",
|
|
612
|
+
"terminal:attach",
|
|
263
613
|
"documents:manage",
|
|
264
614
|
"documents:search",
|
|
265
615
|
"scheduled_tasks:manage",
|
|
@@ -282,6 +632,58 @@ export type Permission = KnownPermission | (string & {});
|
|
|
282
632
|
|
|
283
633
|
export type ProductAccessMode = "local" | "configured" | "managed";
|
|
284
634
|
|
|
635
|
+
/**
|
|
636
|
+
* One model a client may select at send time, plus the provider that serves it.
|
|
637
|
+
* The wire API (`responses` | `chat`) lets a client reason about provider
|
|
638
|
+
* capabilities; the provider id/label drive a picker's grouping. Mirrors the
|
|
639
|
+
* `ClientModel` shape projected into `ClientConfig` by the server.
|
|
640
|
+
*/
|
|
641
|
+
export type ClientModel = {
|
|
642
|
+
id: string;
|
|
643
|
+
label: string;
|
|
644
|
+
/** Provider id (e.g. `openai`, `azure`, or a registry provider id). */
|
|
645
|
+
provider: string;
|
|
646
|
+
providerLabel: string;
|
|
647
|
+
api: "responses" | "chat";
|
|
648
|
+
contextWindowTokens?: number | undefined;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* How a deployment expects clients to authenticate to it, surfaced so a UI can
|
|
653
|
+
* wire up the right header/cookie without prior knowledge of the host setup.
|
|
654
|
+
* Discriminated on `mode`; `none` is the back-compat default.
|
|
655
|
+
*/
|
|
656
|
+
export type ClientAuthConfig =
|
|
657
|
+
| { mode: "none" }
|
|
658
|
+
| { mode: "deploymentKey"; headerName: "x-opengeni-access-key" }
|
|
659
|
+
| { mode: "configuredToken"; headerName: "authorization"; scheme: "bearer" }
|
|
660
|
+
| { mode: "managedSession"; session: "cookie" };
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Public, unauthenticated-by-default client bootstrap config returned by
|
|
664
|
+
* `GET /v1/config/client`: which models + reasoning efforts are exposed, the
|
|
665
|
+
* MCP servers and file-upload limits a composer should offer, and how the
|
|
666
|
+
* deployment expects the client to authenticate. `allowedModels` is kept for
|
|
667
|
+
* back-compat; `models` carries the richer provider-grouped list for a picker.
|
|
668
|
+
*/
|
|
669
|
+
export type ClientConfig = {
|
|
670
|
+
deploymentRevision: string;
|
|
671
|
+
defaultModel: string;
|
|
672
|
+
allowedModels: string[];
|
|
673
|
+
models: ClientModel[];
|
|
674
|
+
defaultReasoningEffort: ReasoningEffort;
|
|
675
|
+
allowedReasoningEfforts: ReasoningEffort[];
|
|
676
|
+
mcpServers: { id: string; name: string }[];
|
|
677
|
+
fileUploads: { enabled: boolean; maxSizeBytes: number };
|
|
678
|
+
productAccessMode: ProductAccessMode;
|
|
679
|
+
auth: ClientAuthConfig;
|
|
680
|
+
// Server-wide hint: does this deployment support Channel-A structured services
|
|
681
|
+
// at all (P4.4). Per-session availability is negotiated on /stream-capabilities;
|
|
682
|
+
// this is the coarse on/off the client uses to decide whether to even attempt
|
|
683
|
+
// the fs/git/terminal panels.
|
|
684
|
+
structuredServices: { fileSystem: boolean; git: boolean; terminalEvents: boolean };
|
|
685
|
+
};
|
|
686
|
+
|
|
285
687
|
export type AccountRole = "owner" | "admin" | "member";
|
|
286
688
|
|
|
287
689
|
export type AccountGrant = {
|
|
@@ -369,6 +771,32 @@ export type ListApiKeysResponse = {
|
|
|
369
771
|
apiKeys: ApiKey[];
|
|
370
772
|
};
|
|
371
773
|
|
|
774
|
+
// A person (or API key) with access to a workspace. `subjectId` is
|
|
775
|
+
// `user:<betterAuthUserId>` or `api_key:<id>`; the People surface lists the
|
|
776
|
+
// `user:` subjects (api_key subjects belong to the API keys section).
|
|
777
|
+
export type WorkspaceMember = {
|
|
778
|
+
subjectId: string;
|
|
779
|
+
subjectLabel: string | null;
|
|
780
|
+
role: string;
|
|
781
|
+
permissions: Permission[];
|
|
782
|
+
createdAt: string;
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
export type ListWorkspaceMembersResponse = {
|
|
786
|
+
members: WorkspaceMember[];
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
export type AddWorkspaceMemberRequest = {
|
|
790
|
+
email: string;
|
|
791
|
+
role?: string | undefined;
|
|
792
|
+
permissions: Permission[];
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
export type UpdateWorkspaceMemberRequest = {
|
|
796
|
+
role?: string | undefined;
|
|
797
|
+
permissions: Permission[];
|
|
798
|
+
};
|
|
799
|
+
|
|
372
800
|
// --- Goals -------------------------------------------------------------------
|
|
373
801
|
|
|
374
802
|
export type SessionGoalStatus = "active" | "paused" | "completed";
|
|
@@ -945,6 +1373,9 @@ export const KNOWN_USAGE_EVENT_TYPES = [
|
|
|
945
1373
|
"document.indexed",
|
|
946
1374
|
"scheduled_task.fired",
|
|
947
1375
|
"api_key.request",
|
|
1376
|
+
// sandbox warm-time metering (P2.1) — mirrors contracts UsageEventType.
|
|
1377
|
+
"sandbox.warm_seconds",
|
|
1378
|
+
"sandbox.warm_cost",
|
|
948
1379
|
] as const;
|
|
949
1380
|
|
|
950
1381
|
export type KnownUsageEventType = (typeof KNOWN_USAGE_EVENT_TYPES)[number];
|