@opengeni/sdk 0.11.0 → 0.15.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/src/client.ts CHANGED
@@ -1,5 +1,13 @@
1
- import { OpenGeniApiError } from "./errors";
2
- import { streamSessionEvents, type SessionEventStreamTransport, type StreamSessionEventsOptions } from "./stream";
1
+ import { OpenGeniApiContractMismatchError, OpenGeniApiError } from "./errors";
2
+ import {
3
+ streamSessionEvents,
4
+ type SessionEventStreamTransport,
5
+ type StreamSessionEventsOptions,
6
+ } from "./stream";
7
+ import {
8
+ streamWorkspaceControlEvents,
9
+ type WorkspaceControlStreamTransport,
10
+ } from "./workspace-control-stream";
3
11
  import type {
4
12
  AccessContext,
5
13
  AddWorkspaceMemberRequest,
@@ -23,11 +31,13 @@ import type {
23
31
  ClientSessionEventInput,
24
32
  CompactSessionContextResult,
25
33
  CompleteFileUploadResponse,
34
+ ConnectionMetadata,
26
35
  CreateApiKeyRequest,
27
36
  CreateApiKeyResponse,
28
37
  CreateCapabilityCatalogItemRequest,
29
38
  CreateCheckoutRequest,
30
39
  CreateCheckoutResponse,
40
+ CreateConnectionRequest,
31
41
  CreateDocumentBaseRequest,
32
42
  CreateFileUploadRequest,
33
43
  CreateFileUploadResponse,
@@ -36,7 +46,8 @@ import type {
36
46
  CreateKnowledgeMemoryRequest,
37
47
  CreateScheduledTaskRequest,
38
48
  CreateSessionRequest,
39
- CreateWorkspaceEnvironmentRequest,
49
+ CreateVariableSetRequest,
50
+ CreateRigRequest,
40
51
  CreateWorkspaceRequest,
41
52
  // Enrollment UX (design 11): the click-Grant approve-page lookup/deny + headless
42
53
  // enroll-token mint.
@@ -62,7 +73,6 @@ import type {
62
73
  ListPacksResponse,
63
74
  // Bring-your-own-compute: the Machines dashboard + per-machine metrics (M10).
64
75
  MachinesResponse,
65
- MachineView,
66
76
  MetricSample,
67
77
  MachineMetricsSeriesResponse,
68
78
  // Bring-your-own-compute: the user-authenticated active-sandbox swap (M7).
@@ -76,8 +86,23 @@ import type {
76
86
  ScheduledTask,
77
87
  ScheduledTaskRun,
78
88
  Session,
89
+ SessionListResponse,
90
+ UpdateSessionPinRequest,
79
91
  SessionEvent,
80
92
  SessionGoal,
93
+ SessionLineageResponse,
94
+ SessionMcpCredentialUpdateInput,
95
+ SessionQueueSnapshot,
96
+ SessionQueueMutationResponse,
97
+ ComposerDraft,
98
+ DeleteSessionQueueItemRequest,
99
+ EditSessionQueueItemRequest,
100
+ MoveSessionQueueItemRequest,
101
+ SaveComposerDraftRequest,
102
+ SteerSessionQueueItemRequest,
103
+ SessionControlResponse,
104
+ WorkspaceInferenceControlResponse,
105
+ WorkspaceControlEvent,
81
106
  SessionTurn,
82
107
  // Stream surfacing (Phase 5): capability negotiation + viewer lifecycle + config.
83
108
  SessionCapabilities,
@@ -108,6 +133,9 @@ import type {
108
133
  GitLogResponse,
109
134
  GitShowRequest,
110
135
  GitShowResponse,
136
+ // Workbench v2 turn-end capture reads (M2, dossier §10.3).
137
+ GetWorkspaceCaptureResponse,
138
+ GetWorkspaceCaptureFileResponse,
111
139
  TerminalExecRequest,
112
140
  TerminalExecResponse,
113
141
  PtyOpenRequest,
@@ -116,21 +144,35 @@ import type {
116
144
  PtyResizeRequest,
117
145
  PtyCloseRequest,
118
146
  ToolRef,
147
+ UpdateConnectionRequest,
119
148
  UpdateKnowledgeMemoryRequest,
120
149
  UpdateScheduledTaskRequest,
121
150
  UpdateSessionGoalRequest,
122
151
  UpdateSessionRequest,
123
- UpdateSessionTurnRequest,
124
- UpdateWorkspaceEnvironmentRequest,
152
+ UpdateVariableSetRequest,
153
+ UpdateRigRequest,
125
154
  UpdateWorkspaceMemberRequest,
126
155
  UpdateWorkspaceRequest,
156
+ UpdateWorkspaceSettingsRequest,
157
+ SetWorkspaceDefaultRigRequest,
127
158
  UploadFileInput,
128
- WorkspaceEnvironment,
129
- WorkspaceEnvironmentVariableMetadata,
159
+ VariableSet,
160
+ VariableSetVariableMetadata,
161
+ Rig,
162
+ RigVersion,
163
+ RigChange,
164
+ ProposeRigChangeRequest,
130
165
  WorkspaceMember,
166
+ WorkspaceMemorySearchRequest,
167
+ WorkspaceMemorySearchResponse,
131
168
  WorkspaceRegisteredPack,
132
169
  Workspace,
170
+ ListConnectionsResponse,
171
+ ConnectionResponse,
172
+ OAuthStartRequest,
173
+ OAuthStartResponse,
133
174
  } from "./types";
175
+ import { OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION } from "./types";
134
176
 
135
177
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
136
178
 
@@ -152,19 +194,16 @@ export type SendMessageInput = {
152
194
  model?: string;
153
195
  reasoningEffort?: ReasoningEffort;
154
196
  clientEventId?: string;
197
+ controlEtag?: string;
198
+ expectedDraftRevision?: number;
199
+ mcpCredentialUpdates?: SessionMcpCredentialUpdateInput[];
155
200
  };
156
201
 
157
202
  export type SteerMessageResult = {
158
203
  /** The accepted `user.message` event. */
159
204
  accepted: SessionEvent;
160
- /**
161
- * The turn created for the message, when it could be located — usually
162
- * still queued, but already claimed (running/requires_action or even
163
- * finished) when the worker picked it up mid-call.
164
- */
165
- turn: SessionTurn | null;
166
- /** True when the running turn was interrupted to make way for the message. */
167
- interrupted: boolean;
205
+ /** The exact turn created for this message in the same server transaction. */
206
+ turn: SessionTurn;
168
207
  };
169
208
 
170
209
  /**
@@ -180,34 +219,128 @@ export class OpenGeniClient {
180
219
  constructor(options: OpenGeniClientOptions) {
181
220
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
182
221
  this.options = options;
183
- // Bind lazily so environments that polyfill fetch after module load work.
222
+ // Bind lazily so variable sets that polyfill fetch after module load work.
184
223
  this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
185
224
  }
186
225
 
187
226
  // --- Session lifecycle ---------------------------------------------------
188
227
 
189
228
  async createSession(workspaceId: string, request: CreateSessionRequest): Promise<Session> {
190
- return await this.requestJson<Session>("POST", `/v1/workspaces/${workspaceId}/sessions`, request);
229
+ return await this.requestJson<Session>(
230
+ "POST",
231
+ `/v1/workspaces/${workspaceId}/sessions`,
232
+ request,
233
+ );
191
234
  }
192
235
 
193
236
  async getSession(workspaceId: string, sessionId: string): Promise<Session> {
194
- return await this.requestJson<Session>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}`);
237
+ return await this.requestJson<Session>(
238
+ "GET",
239
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}`,
240
+ );
195
241
  }
196
242
 
197
- async updateSession(workspaceId: string, sessionId: string, request: UpdateSessionRequest): Promise<Session> {
198
- return await this.requestJson<Session>("PATCH", `/v1/workspaces/${workspaceId}/sessions/${sessionId}`, request);
243
+ async updateSession(
244
+ workspaceId: string,
245
+ sessionId: string,
246
+ request: UpdateSessionRequest,
247
+ ): Promise<Session> {
248
+ return await this.requestJson<Session>(
249
+ "PATCH",
250
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}`,
251
+ request,
252
+ );
199
253
  }
200
254
 
201
- async listSessions(workspaceId: string, options: { limit?: number } = {}): Promise<Session[]> {
202
- return await this.requestJson<Session[]>("GET", `/v1/workspaces/${workspaceId}/sessions`, undefined, {
203
- ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
204
- });
255
+ async listSessions(
256
+ workspaceId: string,
257
+ options: {
258
+ limit?: number;
259
+ parentSessionId?: string | null;
260
+ search?: string;
261
+ } = {},
262
+ ): Promise<Session[]> {
263
+ return await this.requestJson<Session[]>(
264
+ "GET",
265
+ `/v1/workspaces/${workspaceId}/sessions`,
266
+ undefined,
267
+ {
268
+ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
269
+ ...(options.search?.trim() ? { search: options.search.trim() } : {}),
270
+ ...(Object.prototype.hasOwnProperty.call(options, "parentSessionId") &&
271
+ options.parentSessionId !== undefined
272
+ ? {
273
+ parentSessionId:
274
+ options.parentSessionId === null ? "null" : String(options.parentSessionId),
275
+ }
276
+ : {}),
277
+ },
278
+ );
205
279
  }
206
280
 
207
- async listTurns(workspaceId: string, sessionId: string, options: { limit?: number } = {}): Promise<SessionTurn[]> {
208
- return await this.requestJson<SessionTurn[]>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns`, undefined, {
209
- ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
210
- });
281
+ /** Pin-aware ordinary-session page with a stable keyset cursor. */
282
+ async listSessionPage(
283
+ workspaceId: string,
284
+ options: {
285
+ limit?: number;
286
+ parentSessionId?: string | null;
287
+ cursor?: string;
288
+ search?: string;
289
+ } = {},
290
+ ): Promise<SessionListResponse> {
291
+ return await this.requestJson<SessionListResponse>(
292
+ "GET",
293
+ `/v1/workspaces/${workspaceId}/sessions`,
294
+ undefined,
295
+ {
296
+ view: "page",
297
+ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
298
+ ...(options.cursor !== undefined ? { cursor: options.cursor } : {}),
299
+ ...(options.search?.trim() ? { search: options.search.trim() } : {}),
300
+ ...(Object.prototype.hasOwnProperty.call(options, "parentSessionId") &&
301
+ options.parentSessionId !== undefined
302
+ ? {
303
+ parentSessionId:
304
+ options.parentSessionId === null ? "null" : String(options.parentSessionId),
305
+ }
306
+ : {}),
307
+ },
308
+ );
309
+ }
310
+
311
+ /** Set this authenticated member's personal workspace pin for a session. */
312
+ async updateSessionPin(
313
+ workspaceId: string,
314
+ sessionId: string,
315
+ request: UpdateSessionPinRequest,
316
+ ): Promise<Session> {
317
+ return await this.requestJson<Session>(
318
+ "PUT",
319
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/pin`,
320
+ request,
321
+ );
322
+ }
323
+
324
+ async getSessionLineage(workspaceId: string, sessionId: string): Promise<SessionLineageResponse> {
325
+ return await this.requestJson<SessionLineageResponse>(
326
+ "GET",
327
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/lineage`,
328
+ );
329
+ }
330
+
331
+ async listTurns(
332
+ workspaceId: string,
333
+ sessionId: string,
334
+ options: { limit?: number } = {},
335
+ ): Promise<SessionTurn[]> {
336
+ return await this.requestJson<SessionTurn[]>(
337
+ "GET",
338
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns`,
339
+ undefined,
340
+ {
341
+ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
342
+ },
343
+ );
211
344
  }
212
345
 
213
346
  // --- Bring-your-own-compute: Machines dashboard + metrics (M10) ------------
@@ -218,10 +351,18 @@ export class OpenGeniClient {
218
351
  * sharedSessionCount. Pass `sessionId` for an in-session view, which adds the
219
352
  * session's synthetic Modal group box + the active-sandbox pointer.
220
353
  */
221
- async listMachines(workspaceId: string, options: { sessionId?: string } = {}): Promise<MachinesResponse> {
222
- return await this.requestJson<MachinesResponse>("GET", `/v1/workspaces/${workspaceId}/machines`, undefined, {
223
- ...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
224
- });
354
+ async listMachines(
355
+ workspaceId: string,
356
+ options: { sessionId?: string } = {},
357
+ ): Promise<MachinesResponse> {
358
+ return await this.requestJson<MachinesResponse>(
359
+ "GET",
360
+ `/v1/workspaces/${workspaceId}/machines`,
361
+ undefined,
362
+ {
363
+ ...(options.sessionId !== undefined ? { sessionId: options.sessionId } : {}),
364
+ },
365
+ );
225
366
  }
226
367
 
227
368
  /**
@@ -254,7 +395,11 @@ export class OpenGeniClient {
254
395
  * the request.
255
396
  */
256
397
  async lookupDeviceEnrollment(userCode: string): Promise<DeviceEnrollmentLookupResponse> {
257
- return await this.requestJson<DeviceEnrollmentLookupResponse>("POST", "/v1/enrollments/device/lookup", { userCode });
398
+ return await this.requestJson<DeviceEnrollmentLookupResponse>(
399
+ "POST",
400
+ "/v1/enrollments/device/lookup",
401
+ { userCode },
402
+ );
258
403
  }
259
404
 
260
405
  /**
@@ -324,14 +469,25 @@ export class OpenGeniClient {
324
469
 
325
470
  // --- Scheduled tasks -------------------------------------------------------
326
471
 
327
- async listScheduledTasks(workspaceId: string, options: { limit?: number } = {}): Promise<ScheduledTask[]> {
328
- return await this.requestJson<ScheduledTask[]>("GET", `/v1/workspaces/${workspaceId}/scheduled-tasks`, undefined, {
329
- ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
330
- });
472
+ async listScheduledTasks(
473
+ workspaceId: string,
474
+ options: { limit?: number } = {},
475
+ ): Promise<ScheduledTask[]> {
476
+ return await this.requestJson<ScheduledTask[]>(
477
+ "GET",
478
+ `/v1/workspaces/${workspaceId}/scheduled-tasks`,
479
+ undefined,
480
+ {
481
+ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
482
+ },
483
+ );
331
484
  }
332
485
 
333
486
  async getScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
334
- return await this.requestJson<ScheduledTask>("GET", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`);
487
+ return await this.requestJson<ScheduledTask>(
488
+ "GET",
489
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`,
490
+ );
335
491
  }
336
492
 
337
493
  // --- Events: replay, send, stream ----------------------------------------
@@ -347,20 +503,37 @@ export class OpenGeniClient {
347
503
  sessionId: string,
348
504
  options: { after?: number; before?: number; limit?: number; compact?: boolean } = {},
349
505
  ): Promise<SessionEvent[]> {
350
- return await this.requestJson<SessionEvent[]>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, undefined, {
351
- ...(options.after !== undefined ? { after: String(options.after) } : {}),
352
- ...(options.before !== undefined ? { before: String(options.before) } : {}),
353
- ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
354
- ...(options.compact ? { compact: "1" } : {}),
355
- });
506
+ return await this.requestJson<SessionEvent[]>(
507
+ "GET",
508
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`,
509
+ undefined,
510
+ {
511
+ ...(options.after !== undefined ? { after: String(options.after) } : {}),
512
+ ...(options.before !== undefined ? { before: String(options.before) } : {}),
513
+ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
514
+ ...(options.compact ? { compact: "1" } : {}),
515
+ },
516
+ );
356
517
  }
357
518
 
358
519
  /** POST a user/control event to the session. Returns the accepted event. */
359
- async sendEvent(workspaceId: string, sessionId: string, event: ClientSessionEventInput): Promise<SessionEvent> {
360
- return await this.requestJson<SessionEvent>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`, event);
520
+ async sendEvent(
521
+ workspaceId: string,
522
+ sessionId: string,
523
+ event: ClientSessionEventInput,
524
+ ): Promise<SessionEvent> {
525
+ return await this.requestJson<SessionEvent>(
526
+ "POST",
527
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/events`,
528
+ event,
529
+ );
361
530
  }
362
531
 
363
- async sendMessage(workspaceId: string, sessionId: string, message: string | SendMessageInput): Promise<SessionEvent> {
532
+ async sendMessage(
533
+ workspaceId: string,
534
+ sessionId: string,
535
+ message: string | SendMessageInput,
536
+ ): Promise<SessionEvent> {
364
537
  const input = typeof message === "string" ? { text: message } : message;
365
538
  const { clientEventId, ...payload } = input;
366
539
  return await this.sendEvent(workspaceId, sessionId, {
@@ -370,22 +543,28 @@ export class OpenGeniClient {
370
543
  });
371
544
  }
372
545
 
373
- async interrupt(
546
+ async pauseSession(
374
547
  workspaceId: string,
375
548
  sessionId: string,
376
- options: { reason?: string; clientEventId?: string } = {},
377
- ): Promise<SessionEvent> {
378
- return await this.sendEvent(workspaceId, sessionId, {
379
- type: "user.interrupt",
380
- ...(options.clientEventId !== undefined ? { clientEventId: options.clientEventId } : {}),
381
- payload: options.reason !== undefined ? { reason: options.reason } : {},
549
+ options: { reason?: string; clientEventId?: string; expectedControlEtag?: string } = {},
550
+ ): Promise<SessionControlResponse> {
551
+ return await this.controlSession(workspaceId, sessionId, {
552
+ action: "pause",
553
+ clientEventId: options.clientEventId ?? crypto.randomUUID(),
554
+ ...(options.reason ? { reason: options.reason } : {}),
555
+ ...(options.expectedControlEtag ? { expectedControlEtag: options.expectedControlEtag } : {}),
382
556
  });
383
557
  }
384
558
 
385
559
  async sendApprovalDecision(
386
560
  workspaceId: string,
387
561
  sessionId: string,
388
- decision: { approvalId: string; decision: "approve" | "reject"; message?: string; clientEventId?: string },
562
+ decision: {
563
+ approvalId: string;
564
+ decision: "approve" | "reject";
565
+ message?: string;
566
+ clientEventId?: string;
567
+ },
389
568
  ): Promise<SessionEvent> {
390
569
  const { clientEventId, ...payload } = decision;
391
570
  return await this.sendEvent(workspaceId, sessionId, {
@@ -411,8 +590,13 @@ export class OpenGeniClient {
411
590
  /** The transport `streamEvents` runs on; useful for custom streaming layers. */
412
591
  eventStreamTransport(workspaceId: string, sessionId: string): SessionEventStreamTransport {
413
592
  return {
414
- openStream: async (after, signal) => await this.openEventStream(workspaceId, sessionId, { after, ...(signal ? { signal } : {}) }),
415
- listEvents: async (after, limit) => await this.listEvents(workspaceId, sessionId, { after, limit }),
593
+ openStream: async (after, signal) =>
594
+ await this.openEventStream(workspaceId, sessionId, {
595
+ after,
596
+ ...(signal ? { signal } : {}),
597
+ }),
598
+ listEvents: async (after, limit) =>
599
+ await this.listEvents(workspaceId, sessionId, { after, limit }),
416
600
  };
417
601
  }
418
602
 
@@ -430,6 +614,7 @@ export class OpenGeniClient {
430
614
  headers: { ...this.headers(), Accept: "text/event-stream" },
431
615
  ...(options.signal ? { signal: options.signal } : {}),
432
616
  });
617
+ assertApiContractResponse(response);
433
618
  if (!response.ok) {
434
619
  throw new OpenGeniApiError(response.status, await safeText(response));
435
620
  }
@@ -441,122 +626,233 @@ export class OpenGeniClient {
441
626
 
442
627
  // --- Turn queue ------------------------------------------------------------
443
628
 
444
- /** Edit a still-queued turn (prompt, model, resources, tools, ...). */
445
- async updateQueuedTurn(
629
+ async getQueue(workspaceId: string, sessionId: string): Promise<SessionQueueSnapshot> {
630
+ return await this.requestJson<SessionQueueSnapshot>(
631
+ "GET",
632
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue`,
633
+ );
634
+ }
635
+
636
+ async moveQueueItem(
446
637
  workspaceId: string,
447
638
  sessionId: string,
448
639
  turnId: string,
449
- update: UpdateSessionTurnRequest,
450
- ): Promise<SessionTurn> {
451
- return await this.requestJson<SessionTurn>(
452
- "PATCH",
453
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/${turnId}`,
454
- update,
640
+ request: MoveSessionQueueItemRequest,
641
+ ): Promise<SessionQueueMutationResponse> {
642
+ return await this.requestJson<SessionQueueMutationResponse>(
643
+ "POST",
644
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/move`,
645
+ request,
455
646
  );
456
647
  }
457
648
 
458
- /**
459
- * Reorder the queued turns. `turnIds` must all reference queued turns; the
460
- * server assigns positions in the given order and returns the queue.
461
- */
462
- async reorderQueuedTurns(workspaceId: string, sessionId: string, turnIds: string[]): Promise<SessionTurn[]> {
463
- return await this.requestJson<SessionTurn[]>(
649
+ async editQueueItem(
650
+ workspaceId: string,
651
+ sessionId: string,
652
+ turnId: string,
653
+ request: EditSessionQueueItemRequest,
654
+ ): Promise<SessionQueueMutationResponse> {
655
+ return await this.requestJson<SessionQueueMutationResponse>(
464
656
  "POST",
465
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/reorder`,
466
- { turnIds },
657
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/edit`,
658
+ request,
467
659
  );
468
660
  }
469
661
 
470
- /** Cancel a queued turn before it is claimed. Returns the cancelled turn. */
471
- async deleteQueuedTurn(workspaceId: string, sessionId: string, turnId: string): Promise<SessionTurn> {
472
- return await this.requestJson<SessionTurn>(
473
- "DELETE",
474
- `/v1/workspaces/${workspaceId}/sessions/${sessionId}/turns/${turnId}`,
662
+ async steerQueueItem(
663
+ workspaceId: string,
664
+ sessionId: string,
665
+ turnId: string,
666
+ request: SteerSessionQueueItemRequest,
667
+ ): Promise<SessionQueueMutationResponse> {
668
+ return await this.requestJson<SessionQueueMutationResponse>(
669
+ "POST",
670
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/steer`,
671
+ request,
672
+ );
673
+ }
674
+
675
+ async deleteQueueItem(
676
+ workspaceId: string,
677
+ sessionId: string,
678
+ turnId: string,
679
+ request: DeleteSessionQueueItemRequest,
680
+ ): Promise<SessionQueueMutationResponse> {
681
+ return await this.requestJson<SessionQueueMutationResponse>(
682
+ "POST",
683
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/queue/${turnId}/delete`,
684
+ request,
685
+ );
686
+ }
687
+
688
+ async getComposerDraft(workspaceId: string, sessionId: string): Promise<ComposerDraft> {
689
+ return await this.requestJson<ComposerDraft>(
690
+ "GET",
691
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/composer-draft`,
692
+ );
693
+ }
694
+
695
+ async saveComposerDraft(
696
+ workspaceId: string,
697
+ sessionId: string,
698
+ request: SaveComposerDraftRequest,
699
+ ): Promise<ComposerDraft> {
700
+ return await this.requestJson<ComposerDraft>(
701
+ "PUT",
702
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/composer-draft`,
703
+ request,
704
+ );
705
+ }
706
+
707
+ async controlSession(
708
+ workspaceId: string,
709
+ sessionId: string,
710
+ request: {
711
+ action: "pause" | "resume";
712
+ reason?: string;
713
+ clientEventId: string;
714
+ expectedControlEtag?: string;
715
+ },
716
+ ): Promise<SessionControlResponse> {
717
+ return await this.requestJson<SessionControlResponse>(
718
+ "POST",
719
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/control`,
720
+ request,
721
+ );
722
+ }
723
+
724
+ async resumeSession(
725
+ workspaceId: string,
726
+ sessionId: string,
727
+ options: { reason?: string; clientEventId?: string; expectedControlEtag?: string } = {},
728
+ ): Promise<SessionControlResponse> {
729
+ return await this.controlSession(workspaceId, sessionId, {
730
+ action: "resume",
731
+ clientEventId: options.clientEventId ?? crypto.randomUUID(),
732
+ ...(options.reason ? { reason: options.reason } : {}),
733
+ ...(options.expectedControlEtag ? { expectedControlEtag: options.expectedControlEtag } : {}),
734
+ });
735
+ }
736
+
737
+ async setWorkspaceInferenceState(
738
+ workspaceId: string,
739
+ request: {
740
+ action: "pause" | "resume";
741
+ reason?: string;
742
+ clientEventId: string;
743
+ expectedRevision?: number;
744
+ },
745
+ ): Promise<WorkspaceInferenceControlResponse> {
746
+ return await this.requestJson<WorkspaceInferenceControlResponse>(
747
+ "POST",
748
+ `/v1/workspaces/${workspaceId}/inference-control`,
749
+ request,
475
750
  );
476
751
  }
477
752
 
753
+ async listWorkspaceControlEvents(
754
+ workspaceId: string,
755
+ options: { after?: number; limit?: number } = {},
756
+ ): Promise<WorkspaceControlEvent[]> {
757
+ return await this.requestJson<WorkspaceControlEvent[]>(
758
+ "GET",
759
+ `/v1/workspaces/${workspaceId}/control-events`,
760
+ undefined,
761
+ {
762
+ ...(options.after !== undefined ? { after: String(options.after) } : {}),
763
+ ...(options.limit !== undefined ? { limit: String(options.limit) } : {}),
764
+ },
765
+ );
766
+ }
767
+
768
+ streamWorkspaceControlEvents(
769
+ workspaceId: string,
770
+ options: StreamSessionEventsOptions = {},
771
+ ): AsyncGenerator<WorkspaceControlEvent, void, void> {
772
+ return streamWorkspaceControlEvents(this.workspaceControlStreamTransport(workspaceId), options);
773
+ }
774
+
775
+ workspaceControlStreamTransport(workspaceId: string): WorkspaceControlStreamTransport {
776
+ return {
777
+ openStream: async (after, signal) =>
778
+ await this.openWorkspaceControlEventStream(workspaceId, {
779
+ after,
780
+ ...(signal ? { signal } : {}),
781
+ }),
782
+ };
783
+ }
784
+
785
+ async openWorkspaceControlEventStream(
786
+ workspaceId: string,
787
+ options: { after?: number; signal?: AbortSignal } = {},
788
+ ): Promise<ReadableStream<Uint8Array>> {
789
+ const response = await this.fetchImpl(
790
+ this.url(`/v1/workspaces/${workspaceId}/control-events/stream`, {
791
+ after: String(options.after ?? 0),
792
+ }),
793
+ {
794
+ method: "GET",
795
+ headers: { ...this.headers(), Accept: "text/event-stream" },
796
+ ...(options.signal ? { signal: options.signal } : {}),
797
+ },
798
+ );
799
+ assertApiContractResponse(response);
800
+ if (!response.ok) throw new OpenGeniApiError(response.status, await safeText(response));
801
+ if (!response.body) {
802
+ throw new OpenGeniApiError(response.status, "SSE response did not include a readable body");
803
+ }
804
+ return response.body;
805
+ }
806
+
478
807
  /**
479
- * Steer: deliver a message *now* instead of behind the queue. Sends the
480
- * message, promotes its queued turn to the front, and interrupts the
481
- * running turn so the session picks the steer turn up next. On a session
482
- * that is not running this degrades gracefully to a plain queued message.
483
- *
484
- * The steer turn is located by `triggerEventId` across ALL turns (retried
485
- * briefly in case the server is still materializing it) — not just the
486
- * queued ones, because the worker can claim the steer turn before it is
487
- * ever observed queued, and a claimed steer turn means the message is
488
- * already being delivered: interrupting then would cancel the very message
489
- * being steered. If the turn cannot be found while other turns are queued,
490
- * the interrupt is also skipped — stopping the running turn would otherwise
491
- * promote someone else's queued work over this message — and the call
492
- * degrades to a plain queued send (`interrupted: false`).
808
+ * Steer: atomically put this prompt at the head and supersede the current
809
+ * inference. The client performs one request and renders server order.
493
810
  */
494
811
  async steerMessage(
495
812
  workspaceId: string,
496
813
  sessionId: string,
497
814
  message: string | SendMessageInput,
498
815
  ): Promise<SteerMessageResult> {
499
- const accepted = await this.sendMessage(workspaceId, sessionId, message);
500
- let steerTurn: SessionTurn | null = null;
501
- let queued: SessionTurn[] = [];
502
- for (let attempt = 0; attempt < 4; attempt += 1) {
503
- if (attempt > 0) {
504
- await delay(150 * attempt);
505
- }
506
- const turns = await this.listTurns(workspaceId, sessionId);
507
- queued = turns
508
- .filter((turn) => turn.status === "queued")
509
- .sort((a, b) => a.position - b.position || a.createdAt.localeCompare(b.createdAt));
510
- // Match against every turn, whatever its status: a steer turn that is
511
- // already running/requires_action (or even finished) was claimed before
512
- // this listing — that is delivery, not grounds for an interrupt.
513
- steerTurn = turns.find((turn) => turn.triggerEventId === accepted.id) ?? null;
514
- if (steerTurn) {
515
- break;
516
- }
517
- }
518
- const steerTurnQueued = steerTurn?.status === "queued";
519
- if (steerTurn && steerTurnQueued && queued.length > 1) {
520
- const front = steerTurn;
521
- await this.reorderQueuedTurns(workspaceId, sessionId, [
522
- front.id,
523
- ...queued.filter((turn) => turn.id !== front.id).map((turn) => turn.id),
524
- ]);
525
- }
526
- // Interrupting is only safe when the next claim is provably this message:
527
- // either the steer turn sits queued (now at the front), or no turn
528
- // materialized yet AND nothing else is queued. A steer turn observed in
529
- // any non-queued state was already claimed — skip the interrupt.
530
- const canDeliverNext = steerTurnQueued || (steerTurn === null && queued.length === 0);
531
- const session = await this.getSession(workspaceId, sessionId);
532
- // If the previously running turn already finished and the session claimed
533
- // the steer turn itself, interrupting now would cancel the very message
534
- // being steered. `activeTurnId` is the claim check; the residual window
535
- // between this read and the interrupt landing is accepted (an interrupt
536
- // can never be atomic with a status read over HTTP).
537
- const steerTurnAlreadyActive = steerTurn !== null && session.activeTurnId === steerTurn.id;
538
- const interrupted = canDeliverNext
539
- && !steerTurnAlreadyActive
540
- && (session.status === "running" || session.status === "requires_action");
541
- if (interrupted) {
542
- await this.interrupt(workspaceId, sessionId, { reason: "steer" });
543
- }
544
- return { accepted, turn: steerTurn, interrupted };
816
+ const input = typeof message === "string" ? { text: message } : message;
817
+ return await this.requestJson<SteerMessageResult>(
818
+ "POST",
819
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/steer`,
820
+ input,
821
+ );
545
822
  }
546
823
 
547
824
  // --- Goals -------------------------------------------------------------------
548
825
 
549
826
  /** The session's goal. 404s when the session never had one. */
550
827
  async getGoal(workspaceId: string, sessionId: string): Promise<SessionGoal> {
551
- return await this.requestJson<SessionGoal>("GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`);
828
+ return await this.requestJson<SessionGoal>(
829
+ "GET",
830
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`,
831
+ );
832
+ }
833
+
834
+ async updateGoal(
835
+ workspaceId: string,
836
+ sessionId: string,
837
+ request: UpdateSessionGoalRequest,
838
+ ): Promise<SessionGoal> {
839
+ return await this.requestJson<SessionGoal>(
840
+ "PATCH",
841
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`,
842
+ request,
843
+ );
552
844
  }
553
845
 
554
- async updateGoal(workspaceId: string, sessionId: string, request: UpdateSessionGoalRequest): Promise<SessionGoal> {
555
- return await this.requestJson<SessionGoal>("PATCH", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`, request);
846
+ async deleteGoal(workspaceId: string, sessionId: string): Promise<void> {
847
+ await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/goal`);
556
848
  }
557
849
 
558
850
  /** Pause the goal loop: the session stops self-continuing until resumed. */
559
- async pauseGoal(workspaceId: string, sessionId: string, options: { rationale?: string } = {}): Promise<SessionGoal> {
851
+ async pauseGoal(
852
+ workspaceId: string,
853
+ sessionId: string,
854
+ options: { rationale?: string } = {},
855
+ ): Promise<SessionGoal> {
560
856
  return await this.updateGoal(workspaceId, sessionId, {
561
857
  status: "paused",
562
858
  ...(options.rationale !== undefined ? { rationale: options.rationale } : {}),
@@ -578,17 +874,23 @@ export class OpenGeniClient {
578
874
  * context — the destructive intent is explicit on the wire.
579
875
  */
580
876
  async clearSessionContext(workspaceId: string, sessionId: string): Promise<void> {
581
- await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/clear`, { confirm: true });
877
+ await this.requestVoid(
878
+ "POST",
879
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/clear`,
880
+ { confirm: true },
881
+ );
582
882
  }
583
883
 
584
- /**
585
- * Trigger conversation compaction now. On the client-managed (Azure) path this
586
- * queues a forced compaction the worker honors before the next turn
587
- * (`status:"queued"`); on a server-managed provider or when compaction is off
588
- * it is a no-op (`status:"noop"`) with an explanatory message.
589
- */
590
- async compactSessionContext(workspaceId: string, sessionId: string): Promise<CompactSessionContextResult> {
591
- return await this.requestJson<CompactSessionContextResult>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/compact`, {});
884
+ /** Request one durable portable compaction at the next safe model boundary. */
885
+ async compactSessionContext(
886
+ workspaceId: string,
887
+ sessionId: string,
888
+ ): Promise<CompactSessionContextResult> {
889
+ return await this.requestJson<CompactSessionContextResult>(
890
+ "POST",
891
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/context/compact`,
892
+ {},
893
+ );
592
894
  }
593
895
 
594
896
  // --- Channel-A structured services (P4.4) ------------------------------------
@@ -597,79 +899,232 @@ export class OpenGeniClient {
597
899
  // notifications + the PTY output stream arrive on the existing event SSE.
598
900
 
599
901
  /** FileSystem: list a directory tree (feeds the Pierre file tree). */
600
- async fsList(workspaceId: string, sessionId: string, request: FsListRequest = {}): Promise<FsListResponse> {
601
- return await this.requestJson<FsListResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/list`, request);
902
+ async fsList(
903
+ workspaceId: string,
904
+ sessionId: string,
905
+ request: FsListRequest = {},
906
+ ): Promise<FsListResponse> {
907
+ return await this.requestJson<FsListResponse>(
908
+ "POST",
909
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/list`,
910
+ request,
911
+ );
602
912
  }
603
913
 
604
914
  /** FileSystem: read a file (text or base64; binary-safe, size-capped). */
605
- async fsRead(workspaceId: string, sessionId: string, request: FsReadRequest): Promise<FsReadResponse> {
606
- return await this.requestJson<FsReadResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/read`, request);
915
+ async fsRead(
916
+ workspaceId: string,
917
+ sessionId: string,
918
+ request: FsReadRequest,
919
+ ): Promise<FsReadResponse> {
920
+ return await this.requestJson<FsReadResponse>(
921
+ "POST",
922
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/read`,
923
+ request,
924
+ );
607
925
  }
608
926
 
609
927
  /** FileSystem: write a file (last-writer-wins; emits fs.changed). */
610
- async fsWrite(workspaceId: string, sessionId: string, request: FsWriteRequest): Promise<FsWriteResponse> {
611
- return await this.requestJson<FsWriteResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/write`, request);
928
+ async fsWrite(
929
+ workspaceId: string,
930
+ sessionId: string,
931
+ request: FsWriteRequest,
932
+ ): Promise<FsWriteResponse> {
933
+ return await this.requestJson<FsWriteResponse>(
934
+ "POST",
935
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/write`,
936
+ request,
937
+ );
612
938
  }
613
939
 
614
940
  /** FileSystem: delete a path (emits fs.changed). */
615
- async fsDelete(workspaceId: string, sessionId: string, request: FsDeleteRequest): Promise<FsDeleteResponse> {
616
- return await this.requestJson<FsDeleteResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/delete`, request);
941
+ async fsDelete(
942
+ workspaceId: string,
943
+ sessionId: string,
944
+ request: FsDeleteRequest,
945
+ ): Promise<FsDeleteResponse> {
946
+ return await this.requestJson<FsDeleteResponse>(
947
+ "POST",
948
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/delete`,
949
+ request,
950
+ );
617
951
  }
618
952
 
619
953
  /** FileSystem: move/rename a path (emits fs.changed; 409 if destination exists and overwrite is false). */
620
- async fsMove(workspaceId: string, sessionId: string, request: FsMoveRequest): Promise<FsMoveResponse> {
621
- return await this.requestJson<FsMoveResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/move`, request);
954
+ async fsMove(
955
+ workspaceId: string,
956
+ sessionId: string,
957
+ request: FsMoveRequest,
958
+ ): Promise<FsMoveResponse> {
959
+ return await this.requestJson<FsMoveResponse>(
960
+ "POST",
961
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/move`,
962
+ request,
963
+ );
622
964
  }
623
965
 
624
966
  /** FileSystem: create a directory (emits fs.changed; recursive defaults to true). */
625
- async fsMkdir(workspaceId: string, sessionId: string, request: FsMkdirRequest): Promise<FsMkdirResponse> {
626
- return await this.requestJson<FsMkdirResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/mkdir`, request);
967
+ async fsMkdir(
968
+ workspaceId: string,
969
+ sessionId: string,
970
+ request: FsMkdirRequest,
971
+ ): Promise<FsMkdirResponse> {
972
+ return await this.requestJson<FsMkdirResponse>(
973
+ "POST",
974
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/fs/mkdir`,
975
+ request,
976
+ );
627
977
  }
628
978
 
629
979
  /** Git: working-tree/index status (the Pierre file-status feed). */
630
- async gitStatus(workspaceId: string, sessionId: string, request: GitStatusRequest = {}): Promise<GitStatusResponse> {
631
- return await this.requestJson<GitStatusResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/status`, request);
980
+ async gitStatus(
981
+ workspaceId: string,
982
+ sessionId: string,
983
+ request: GitStatusRequest = {},
984
+ ): Promise<GitStatusResponse> {
985
+ return await this.requestJson<GitStatusResponse>(
986
+ "POST",
987
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/status`,
988
+ request,
989
+ );
632
990
  }
633
991
 
634
992
  /** Git: structured diff hunks (the Pierre diff feed). */
635
- async gitDiff(workspaceId: string, sessionId: string, request: GitDiffRequest = {}): Promise<GitDiffResponse> {
636
- return await this.requestJson<GitDiffResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/diff`, request);
993
+ async gitDiff(
994
+ workspaceId: string,
995
+ sessionId: string,
996
+ request: GitDiffRequest = {},
997
+ ): Promise<GitDiffResponse> {
998
+ return await this.requestJson<GitDiffResponse>(
999
+ "POST",
1000
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/diff`,
1001
+ request,
1002
+ );
637
1003
  }
638
1004
 
639
1005
  /** Git: commit log. */
640
- async gitLog(workspaceId: string, sessionId: string, request: GitLogRequest = {}): Promise<GitLogResponse> {
641
- return await this.requestJson<GitLogResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/log`, request);
1006
+ async gitLog(
1007
+ workspaceId: string,
1008
+ sessionId: string,
1009
+ request: GitLogRequest = {},
1010
+ ): Promise<GitLogResponse> {
1011
+ return await this.requestJson<GitLogResponse>(
1012
+ "POST",
1013
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/log`,
1014
+ request,
1015
+ );
642
1016
  }
643
1017
 
644
1018
  /** Git: show a commit (diff vs first parent) or fetch a raw blob at a ref. */
645
- async gitShow(workspaceId: string, sessionId: string, request: GitShowRequest): Promise<GitShowResponse> {
646
- return await this.requestJson<GitShowResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/show`, request);
1019
+ async gitShow(
1020
+ workspaceId: string,
1021
+ sessionId: string,
1022
+ request: GitShowRequest,
1023
+ ): Promise<GitShowResponse> {
1024
+ return await this.requestJson<GitShowResponse>(
1025
+ "POST",
1026
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/git/show`,
1027
+ request,
1028
+ );
1029
+ }
1030
+
1031
+ /** Workspace capture: the latest turn-end snapshot of the session's workspace
1032
+ * (tree + per-repo diff + file after-image refs), served from durable storage
1033
+ * WITHOUT warming a machine — the workbench cold-paint source. Returns
1034
+ * `{available:false}` when no capture exists yet (fall back to the live path). */
1035
+ async getWorkspaceCapture(
1036
+ workspaceId: string,
1037
+ sessionId: string,
1038
+ ): Promise<GetWorkspaceCaptureResponse> {
1039
+ return await this.requestJson<GetWorkspaceCaptureResponse>(
1040
+ "GET",
1041
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/workspace/capture`,
1042
+ );
1043
+ }
1044
+
1045
+ /** Workspace capture: a single file's after-image from the capture (revision
1046
+ * pins a specific one; omitted → latest). Content is inline for small files,
1047
+ * else a short-TTL signed URL; a tooLarge file returns metadata only. */
1048
+ async getWorkspaceCaptureFile(
1049
+ workspaceId: string,
1050
+ sessionId: string,
1051
+ path: string,
1052
+ revision?: number,
1053
+ ): Promise<GetWorkspaceCaptureFileResponse> {
1054
+ const query: Record<string, string> = { path };
1055
+ if (revision !== undefined) query.revision = String(revision);
1056
+ return await this.requestJson<GetWorkspaceCaptureFileResponse>(
1057
+ "GET",
1058
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/workspace/capture/file`,
1059
+ undefined,
1060
+ query,
1061
+ );
647
1062
  }
648
1063
 
649
1064
  /** Terminal: run a bounded command, returning buffered stdout/stderr inline. */
650
- async terminalExec(workspaceId: string, sessionId: string, request: TerminalExecRequest): Promise<TerminalExecResponse> {
651
- return await this.requestJson<TerminalExecResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/exec`, request);
1065
+ async terminalExec(
1066
+ workspaceId: string,
1067
+ sessionId: string,
1068
+ request: TerminalExecRequest,
1069
+ ): Promise<TerminalExecResponse> {
1070
+ return await this.requestJson<TerminalExecResponse>(
1071
+ "POST",
1072
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/exec`,
1073
+ request,
1074
+ );
652
1075
  }
653
1076
 
654
1077
  /** Terminal: open an interactive PTY. Output streams on the event SSE as
655
1078
  * terminal.pty.output.delta; drive it with terminalPtyWrite. */
656
- async terminalPtyOpen(workspaceId: string, sessionId: string, request: PtyOpenRequest = {}): Promise<PtyOpenResponse> {
657
- return await this.requestJson<PtyOpenResponse>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty`, request);
1079
+ async terminalPtyOpen(
1080
+ workspaceId: string,
1081
+ sessionId: string,
1082
+ request: PtyOpenRequest = {},
1083
+ ): Promise<PtyOpenResponse> {
1084
+ return await this.requestJson<PtyOpenResponse>(
1085
+ "POST",
1086
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty`,
1087
+ request,
1088
+ );
658
1089
  }
659
1090
 
660
1091
  /** Terminal: send stdin to an open PTY (output rides A1). */
661
- async terminalPtyWrite(workspaceId: string, sessionId: string, request: PtyWriteRequest): Promise<void> {
662
- await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/write`, request);
1092
+ async terminalPtyWrite(
1093
+ workspaceId: string,
1094
+ sessionId: string,
1095
+ request: PtyWriteRequest,
1096
+ ): Promise<void> {
1097
+ await this.requestVoid(
1098
+ "POST",
1099
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/write`,
1100
+ request,
1101
+ );
663
1102
  }
664
1103
 
665
1104
  /** Terminal: resize an open PTY. */
666
- async terminalPtyResize(workspaceId: string, sessionId: string, request: PtyResizeRequest): Promise<void> {
667
- await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/resize`, request);
1105
+ async terminalPtyResize(
1106
+ workspaceId: string,
1107
+ sessionId: string,
1108
+ request: PtyResizeRequest,
1109
+ ): Promise<void> {
1110
+ await this.requestVoid(
1111
+ "POST",
1112
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/resize`,
1113
+ request,
1114
+ );
668
1115
  }
669
1116
 
670
1117
  /** Terminal: close an open PTY (idempotent). */
671
- async terminalPtyClose(workspaceId: string, sessionId: string, request: PtyCloseRequest): Promise<void> {
672
- await this.requestVoid("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/close`, request);
1118
+ async terminalPtyClose(
1119
+ workspaceId: string,
1120
+ sessionId: string,
1121
+ request: PtyCloseRequest,
1122
+ ): Promise<void> {
1123
+ await this.requestVoid(
1124
+ "POST",
1125
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/terminal/pty/close`,
1126
+ request,
1127
+ );
673
1128
  }
674
1129
 
675
1130
  // --- Stream surfacing: capability negotiation + viewer lifecycle (Phase 5) ---
@@ -684,19 +1139,29 @@ export class OpenGeniClient {
684
1139
  * liveness the client polls on while `cold`/`warming`. The desktop URL/token
685
1140
  * are minted in-process only when the box is warm AND the principal has
686
1141
  * acknowledged the un-redacted plane. */
687
- async getStreamCapabilities(workspaceId: string, sessionId: string): Promise<SessionCapabilities> {
1142
+ async getStreamCapabilities(
1143
+ workspaceId: string,
1144
+ sessionId: string,
1145
+ ): Promise<SessionCapabilities> {
688
1146
  return await this.requestJson<SessionCapabilities>(
689
- "GET", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/stream-capabilities`);
1147
+ "GET",
1148
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/stream-capabilities`,
1149
+ );
690
1150
  }
691
1151
 
692
1152
  /** Record the calling principal's acknowledgment of the un-redacted desktop
693
1153
  * pixel plane (and, when the box is shared, the shared-exposure disclosure).
694
1154
  * The desktop viewer-attach path returns 409 until this is recorded. */
695
1155
  async acknowledgeStream(
696
- workspaceId: string, sessionId: string, request: AcknowledgeStreamRequest = {},
1156
+ workspaceId: string,
1157
+ sessionId: string,
1158
+ request: AcknowledgeStreamRequest = {},
697
1159
  ): Promise<AcknowledgeStreamResponse> {
698
1160
  return await this.requestJson<AcknowledgeStreamResponse>(
699
- "POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/stream-capabilities/acknowledge`, request);
1161
+ "POST",
1162
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/stream-capabilities/acknowledge`,
1163
+ request,
1164
+ );
700
1165
  }
701
1166
 
702
1167
  /** Attach a viewer holder (refcounted liveness — keeps the box warm while
@@ -708,25 +1173,39 @@ export class OpenGeniClient {
708
1173
  * (`desktop` omitted/false) warms the box + mints the pty-ws terminal cell with
709
1174
  * NO consent gate. An omitted `viewerId` mints a fresh one. */
710
1175
  async attachViewer(
711
- workspaceId: string, sessionId: string, request: AttachViewerRequest = {},
1176
+ workspaceId: string,
1177
+ sessionId: string,
1178
+ request: AttachViewerRequest = {},
712
1179
  ): Promise<AttachViewerResponse> {
713
1180
  return await this.requestJson<AttachViewerResponse>(
714
- "POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers`, request);
1181
+ "POST",
1182
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers`,
1183
+ request,
1184
+ );
715
1185
  }
716
1186
 
717
1187
  /** Heartbeat a viewer holder (Channel-A app-level liveness). A closed laptop
718
1188
  * stops sending these → the reaper drops the holder within ~90s. Echoes
719
1189
  * `leaseEpoch` so a superseded epoch is rejected (`alive:false` → re-attach). */
720
1190
  async heartbeatViewer(
721
- workspaceId: string, sessionId: string, viewerId: string, request: ViewerHeartbeatRequest,
1191
+ workspaceId: string,
1192
+ sessionId: string,
1193
+ viewerId: string,
1194
+ request: ViewerHeartbeatRequest,
722
1195
  ): Promise<ViewerHeartbeatResponse> {
723
1196
  return await this.requestJson<ViewerHeartbeatResponse>(
724
- "POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers/${viewerId}/heartbeat`, request);
1197
+ "POST",
1198
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers/${viewerId}/heartbeat`,
1199
+ request,
1200
+ );
725
1201
  }
726
1202
 
727
1203
  /** Detach a viewer (delete this holder; idempotent delete-my-row). */
728
1204
  async detachViewer(workspaceId: string, sessionId: string, viewerId: string): Promise<void> {
729
- await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers/${viewerId}`);
1205
+ await this.requestVoid(
1206
+ "DELETE",
1207
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/viewers/${viewerId}`,
1208
+ );
730
1209
  }
731
1210
 
732
1211
  // --- Access + workspaces -----------------------------------------------------
@@ -739,7 +1218,14 @@ export class OpenGeniClient {
739
1218
  * knowledge of the host setup; safe to call before any auth is established.
740
1219
  */
741
1220
  async getClientConfig(): Promise<ClientConfig> {
742
- return await this.requestJson<ClientConfig>("GET", "/v1/config/client");
1221
+ const config = await this.requestJson<ClientConfig>("GET", "/v1/config/client");
1222
+ if (config.apiContractRevision !== OPENGENI_API_CONTRACT_REVISION) {
1223
+ throw new OpenGeniApiContractMismatchError(
1224
+ OPENGENI_API_CONTRACT_REVISION,
1225
+ String(config.apiContractRevision || "(missing)"),
1226
+ );
1227
+ }
1228
+ return config;
743
1229
  }
744
1230
 
745
1231
  /** The caller's access context: subject, account + workspace grants, defaults. */
@@ -755,159 +1241,396 @@ export class OpenGeniClient {
755
1241
  return await this.requestJson<Workspace>("POST", "/v1/workspaces", request);
756
1242
  }
757
1243
 
758
- async getWorkspace(workspaceId: string): Promise<Workspace> {
759
- return await this.requestJson<Workspace>("GET", `/v1/workspaces/${workspaceId}`);
1244
+ async getWorkspace(workspaceId: string): Promise<Workspace> {
1245
+ return await this.requestJson<Workspace>("GET", `/v1/workspaces/${workspaceId}`);
1246
+ }
1247
+
1248
+ async updateWorkspace(workspaceId: string, request: UpdateWorkspaceRequest): Promise<Workspace> {
1249
+ return await this.requestJson<Workspace>("PATCH", `/v1/workspaces/${workspaceId}`, request);
1250
+ }
1251
+
1252
+ /**
1253
+ * Delete a workspace and everything in it. Refused (409) for the account's
1254
+ * only workspace and while it still has a running session. Irreversible.
1255
+ */
1256
+ async deleteWorkspace(workspaceId: string): Promise<void> {
1257
+ await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}`);
1258
+ }
1259
+
1260
+ // --- Members ("People with access") -------------------------------------------
1261
+
1262
+ /** The workspace's members (user + api_key subjects). */
1263
+ async listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]> {
1264
+ const response = await this.requestJson<ListWorkspaceMembersResponse>(
1265
+ "GET",
1266
+ `/v1/workspaces/${workspaceId}/members`,
1267
+ );
1268
+ return response.members;
1269
+ }
1270
+
1271
+ /**
1272
+ * Add an already-registered user by email. 404s when no user with that email
1273
+ * exists (email invites for unknown users are deferred).
1274
+ */
1275
+ async addWorkspaceMember(
1276
+ workspaceId: string,
1277
+ request: AddWorkspaceMemberRequest,
1278
+ ): Promise<WorkspaceMember> {
1279
+ return await this.requestJson<WorkspaceMember>(
1280
+ "POST",
1281
+ `/v1/workspaces/${workspaceId}/members`,
1282
+ request,
1283
+ );
1284
+ }
1285
+
1286
+ async updateWorkspaceMember(
1287
+ workspaceId: string,
1288
+ subjectId: string,
1289
+ request: UpdateWorkspaceMemberRequest,
1290
+ ): Promise<WorkspaceMember> {
1291
+ return await this.requestJson<WorkspaceMember>(
1292
+ "PATCH",
1293
+ `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(subjectId)}`,
1294
+ request,
1295
+ );
1296
+ }
1297
+
1298
+ /**
1299
+ * Remove a member. Refused (409) for your own membership and for the last
1300
+ * member who can still manage the workspace.
1301
+ */
1302
+ async removeWorkspaceMember(workspaceId: string, subjectId: string): Promise<void> {
1303
+ await this.requestVoid(
1304
+ "DELETE",
1305
+ `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(subjectId)}`,
1306
+ );
1307
+ }
1308
+
1309
+ // --- Scheduled tasks (write + runs) -------------------------------------------
1310
+
1311
+ async createScheduledTask(
1312
+ workspaceId: string,
1313
+ request: CreateScheduledTaskRequest,
1314
+ ): Promise<ScheduledTask> {
1315
+ return await this.requestJson<ScheduledTask>(
1316
+ "POST",
1317
+ `/v1/workspaces/${workspaceId}/scheduled-tasks`,
1318
+ request,
1319
+ );
1320
+ }
1321
+
1322
+ async updateScheduledTask(
1323
+ workspaceId: string,
1324
+ taskId: string,
1325
+ request: UpdateScheduledTaskRequest,
1326
+ ): Promise<ScheduledTask> {
1327
+ return await this.requestJson<ScheduledTask>(
1328
+ "PATCH",
1329
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`,
1330
+ request,
1331
+ );
1332
+ }
1333
+
1334
+ async pauseScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
1335
+ return await this.requestJson<ScheduledTask>(
1336
+ "POST",
1337
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/pause`,
1338
+ );
1339
+ }
1340
+
1341
+ async resumeScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
1342
+ return await this.requestJson<ScheduledTask>(
1343
+ "POST",
1344
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/resume`,
1345
+ );
1346
+ }
1347
+
1348
+ /**
1349
+ * Fire the task immediately (manual trigger), independent of its schedule.
1350
+ * Pass a stable `triggerId` to make a retried trigger idempotent — the same
1351
+ * token charges once and starts one run. Omit it and each call is distinct.
1352
+ */
1353
+ async triggerScheduledTask(
1354
+ workspaceId: string,
1355
+ taskId: string,
1356
+ options: { triggerId?: string } = {},
1357
+ ): Promise<ScheduledTask> {
1358
+ return await this.requestJson<ScheduledTask>(
1359
+ "POST",
1360
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/trigger`,
1361
+ options.triggerId ? { triggerId: options.triggerId } : undefined,
1362
+ );
1363
+ }
1364
+
1365
+ async deleteScheduledTask(workspaceId: string, taskId: string): Promise<void> {
1366
+ await this.requestJson<unknown>(
1367
+ "DELETE",
1368
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`,
1369
+ );
1370
+ }
1371
+
1372
+ async listScheduledTaskRuns(
1373
+ workspaceId: string,
1374
+ taskId: string,
1375
+ options: { limit?: number } = {},
1376
+ ): Promise<ScheduledTaskRun[]> {
1377
+ return await this.requestJson<ScheduledTaskRun[]>(
1378
+ "GET",
1379
+ `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/runs`,
1380
+ undefined,
1381
+ { ...(options.limit !== undefined ? { limit: String(options.limit) } : {}) },
1382
+ );
1383
+ }
1384
+
1385
+ // --- VariableSets --------------------------------------------------------------
1386
+ // Variable values are write-only: reads return name/version metadata only.
1387
+
1388
+ async listVariableSets(workspaceId: string): Promise<VariableSet[]> {
1389
+ return await this.requestJson<VariableSet[]>(
1390
+ "GET",
1391
+ `/v1/workspaces/${workspaceId}/variable-sets`,
1392
+ );
1393
+ }
1394
+
1395
+ async createVariableSet(
1396
+ workspaceId: string,
1397
+ request: CreateVariableSetRequest,
1398
+ ): Promise<VariableSet> {
1399
+ return await this.requestJson<VariableSet>(
1400
+ "POST",
1401
+ `/v1/workspaces/${workspaceId}/variable-sets`,
1402
+ request,
1403
+ );
1404
+ }
1405
+
1406
+ async getVariableSet(workspaceId: string, variableSetId: string): Promise<VariableSet> {
1407
+ return await this.requestJson<VariableSet>(
1408
+ "GET",
1409
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}`,
1410
+ );
1411
+ }
1412
+
1413
+ async updateVariableSet(
1414
+ workspaceId: string,
1415
+ variableSetId: string,
1416
+ request: UpdateVariableSetRequest,
1417
+ ): Promise<VariableSet> {
1418
+ return await this.requestJson<VariableSet>(
1419
+ "PATCH",
1420
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}`,
1421
+ request,
1422
+ );
1423
+ }
1424
+
1425
+ async deleteVariableSet(workspaceId: string, variableSetId: string): Promise<void> {
1426
+ await this.requestJson<unknown>(
1427
+ "DELETE",
1428
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}`,
1429
+ );
1430
+ }
1431
+
1432
+ /** Create or rotate a variable. The value never comes back on any read. */
1433
+ async setVariableSetVariable(
1434
+ workspaceId: string,
1435
+ variableSetId: string,
1436
+ name: string,
1437
+ value: string,
1438
+ ): Promise<VariableSetVariableMetadata> {
1439
+ return await this.requestJson<VariableSetVariableMetadata>(
1440
+ "PUT",
1441
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}/variables/${encodeURIComponent(name)}`,
1442
+ { value },
1443
+ );
1444
+ }
1445
+
1446
+ async deleteVariableSetVariable(
1447
+ workspaceId: string,
1448
+ variableSetId: string,
1449
+ name: string,
1450
+ ): Promise<void> {
1451
+ await this.requestJson<unknown>(
1452
+ "DELETE",
1453
+ `/v1/workspaces/${workspaceId}/variable-sets/${variableSetId}/variables/${encodeURIComponent(name)}`,
1454
+ );
760
1455
  }
761
1456
 
762
- async updateWorkspace(workspaceId: string, request: UpdateWorkspaceRequest): Promise<Workspace> {
763
- return await this.requestJson<Workspace>("PATCH", `/v1/workspaces/${workspaceId}`, request);
764
- }
1457
+ // --- Rigs ------------------------------------------------------------------
1458
+ // Workspace-scoped, versioned sandbox machine definitions. rigs:use gates read
1459
+ // + proposeRigChange; rigs:manage gates create / update / delete / activate.
765
1460
 
766
- /**
767
- * Delete a workspace and everything in it. Refused (409) for the account's
768
- * only workspace and while it still has a running session. Irreversible.
769
- */
770
- async deleteWorkspace(workspaceId: string): Promise<void> {
771
- await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}`);
1461
+ async listRigs(workspaceId: string): Promise<Rig[]> {
1462
+ return await this.requestJson<Rig[]>("GET", `/v1/workspaces/${workspaceId}/rigs`);
772
1463
  }
773
1464
 
774
- // --- Members ("People with access") -------------------------------------------
775
-
776
- /** The workspace's members (user + api_key subjects). */
777
- async listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]> {
778
- const response = await this.requestJson<ListWorkspaceMembersResponse>("GET", `/v1/workspaces/${workspaceId}/members`);
779
- return response.members;
1465
+ async createRig(workspaceId: string, request: CreateRigRequest): Promise<Rig> {
1466
+ return await this.requestJson<Rig>("POST", `/v1/workspaces/${workspaceId}/rigs`, request);
780
1467
  }
781
1468
 
782
- /**
783
- * Add an already-registered user by email. 404s when no user with that email
784
- * exists (email invites for unknown users are deferred).
785
- */
786
- async addWorkspaceMember(workspaceId: string, request: AddWorkspaceMemberRequest): Promise<WorkspaceMember> {
787
- return await this.requestJson<WorkspaceMember>("POST", `/v1/workspaces/${workspaceId}/members`, request);
1469
+ async getRig(workspaceId: string, rigId: string): Promise<Rig> {
1470
+ return await this.requestJson<Rig>("GET", `/v1/workspaces/${workspaceId}/rigs/${rigId}`);
788
1471
  }
789
1472
 
790
- async updateWorkspaceMember(workspaceId: string, subjectId: string, request: UpdateWorkspaceMemberRequest): Promise<WorkspaceMember> {
791
- return await this.requestJson<WorkspaceMember>(
1473
+ async updateRig(workspaceId: string, rigId: string, request: UpdateRigRequest): Promise<Rig> {
1474
+ return await this.requestJson<Rig>(
792
1475
  "PATCH",
793
- `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(subjectId)}`,
1476
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}`,
794
1477
  request,
795
1478
  );
796
1479
  }
797
1480
 
798
- /**
799
- * Remove a member. Refused (409) for your own membership and for the last
800
- * member who can still manage the workspace.
801
- */
802
- async removeWorkspaceMember(workspaceId: string, subjectId: string): Promise<void> {
803
- await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(subjectId)}`);
1481
+ async deleteRig(workspaceId: string, rigId: string): Promise<void> {
1482
+ await this.requestJson<unknown>("DELETE", `/v1/workspaces/${workspaceId}/rigs/${rigId}`);
804
1483
  }
805
1484
 
806
- // --- Scheduled tasks (write + runs) -------------------------------------------
1485
+ async listRigVersions(workspaceId: string, rigId: string): Promise<RigVersion[]> {
1486
+ return await this.requestJson<RigVersion[]>(
1487
+ "GET",
1488
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/versions`,
1489
+ );
1490
+ }
807
1491
 
808
- async createScheduledTask(workspaceId: string, request: CreateScheduledTaskRequest): Promise<ScheduledTask> {
809
- return await this.requestJson<ScheduledTask>("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks`, request);
1492
+ /** Roll the active version to an existing one (rollback / promote-activate). */
1493
+ async activateRigVersion(
1494
+ workspaceId: string,
1495
+ rigId: string,
1496
+ versionId: string,
1497
+ ): Promise<RigVersion> {
1498
+ return await this.requestJson<RigVersion>(
1499
+ "POST",
1500
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/versions/${versionId}/activate`,
1501
+ );
810
1502
  }
811
1503
 
812
- async updateScheduledTask(workspaceId: string, taskId: string, request: UpdateScheduledTaskRequest): Promise<ScheduledTask> {
813
- return await this.requestJson<ScheduledTask>("PATCH", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`, request);
1504
+ async listRigChanges(workspaceId: string, rigId: string): Promise<RigChange[]> {
1505
+ return await this.requestJson<RigChange[]>(
1506
+ "GET",
1507
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/changes`,
1508
+ );
814
1509
  }
815
1510
 
816
- async pauseScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
817
- return await this.requestJson<ScheduledTask>("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/pause`);
1511
+ /** Propose a change against the rig's active version (rigs:use). */
1512
+ async proposeRigChange(
1513
+ workspaceId: string,
1514
+ rigId: string,
1515
+ request: ProposeRigChangeRequest,
1516
+ ): Promise<RigChange> {
1517
+ return await this.requestJson<RigChange>(
1518
+ "POST",
1519
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/changes`,
1520
+ request,
1521
+ );
818
1522
  }
819
1523
 
820
- async resumeScheduledTask(workspaceId: string, taskId: string): Promise<ScheduledTask> {
821
- return await this.requestJson<ScheduledTask>("POST", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/resume`);
1524
+ async getRigChange(workspaceId: string, rigId: string, changeId: string): Promise<RigChange> {
1525
+ return await this.requestJson<RigChange>(
1526
+ "GET",
1527
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/changes/${changeId}`,
1528
+ );
822
1529
  }
823
1530
 
824
1531
  /**
825
- * Fire the task immediately (manual trigger), independent of its schedule.
826
- * Pass a stable `triggerId` to make a retried trigger idempotent — the same
827
- * token charges once and starts one run. Omit it and each call is distinct.
1532
+ * Re-run verification for a change (rigs:use). Verification is asynchronous:
1533
+ * this returns the change immediately with status `verifying`; poll
1534
+ * `getRigChange`/`listRigChanges` for the terminal outcome + logs.
828
1535
  */
829
- async triggerScheduledTask(workspaceId: string, taskId: string, options: { triggerId?: string } = {}): Promise<ScheduledTask> {
830
- return await this.requestJson<ScheduledTask>(
1536
+ async verifyRigChange(workspaceId: string, rigId: string, changeId: string): Promise<RigChange> {
1537
+ return await this.requestJson<RigChange>(
831
1538
  "POST",
832
- `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/trigger`,
833
- options.triggerId ? { triggerId: options.triggerId } : undefined,
1539
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/changes/${changeId}/verify`,
834
1540
  );
835
1541
  }
836
1542
 
837
- async deleteScheduledTask(workspaceId: string, taskId: string): Promise<void> {
838
- await this.requestJson<unknown>("DELETE", `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}`);
839
- }
840
-
841
- async listScheduledTaskRuns(
1543
+ /**
1544
+ * Promote a verified `definition_edit` change into a new active rig version
1545
+ * (rigs:manage). Only valid once the change's verification passed; returns the
1546
+ * newly minted version.
1547
+ */
1548
+ async promoteRigChange(
842
1549
  workspaceId: string,
843
- taskId: string,
844
- options: { limit?: number } = {},
845
- ): Promise<ScheduledTaskRun[]> {
846
- return await this.requestJson<ScheduledTaskRun[]>(
847
- "GET",
848
- `/v1/workspaces/${workspaceId}/scheduled-tasks/${taskId}/runs`,
849
- undefined,
850
- { ...(options.limit !== undefined ? { limit: String(options.limit) } : {}) },
1550
+ rigId: string,
1551
+ changeId: string,
1552
+ ): Promise<RigVersion> {
1553
+ return await this.requestJson<RigVersion>(
1554
+ "POST",
1555
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/changes/${changeId}/promote`,
851
1556
  );
852
1557
  }
853
1558
 
854
- // --- Environments --------------------------------------------------------------
855
- // Variable values are write-only: reads return name/version metadata only.
1559
+ /**
1560
+ * Re-run the active version's checks in a clean throwaway sandbox (rigs:use).
1561
+ * Asynchronous — returns the version id being verified; the outcome lands on
1562
+ * the version's audit trail.
1563
+ */
1564
+ async verifyRig(workspaceId: string, rigId: string): Promise<{ ok: boolean; versionId: string }> {
1565
+ return await this.requestJson<{ ok: boolean; versionId: string }>(
1566
+ "POST",
1567
+ `/v1/workspaces/${workspaceId}/rigs/${rigId}/verify`,
1568
+ );
1569
+ }
856
1570
 
857
- async listEnvironments(workspaceId: string): Promise<WorkspaceEnvironment[]> {
858
- return await this.requestJson<WorkspaceEnvironment[]>("GET", `/v1/workspaces/${workspaceId}/environments`);
1571
+ /** @deprecated use listVariableSets */
1572
+ async listEnvironments(workspaceId: string): Promise<VariableSet[]> {
1573
+ return await this.listVariableSets(workspaceId);
859
1574
  }
860
1575
 
861
- async createEnvironment(workspaceId: string, request: CreateWorkspaceEnvironmentRequest): Promise<WorkspaceEnvironment> {
862
- return await this.requestJson<WorkspaceEnvironment>("POST", `/v1/workspaces/${workspaceId}/environments`, request);
1576
+ /** @deprecated use createVariableSet */
1577
+ async createEnvironment(
1578
+ workspaceId: string,
1579
+ request: CreateVariableSetRequest,
1580
+ ): Promise<VariableSet> {
1581
+ return await this.createVariableSet(workspaceId, request);
863
1582
  }
864
1583
 
865
- async getEnvironment(workspaceId: string, environmentId: string): Promise<WorkspaceEnvironment> {
866
- return await this.requestJson<WorkspaceEnvironment>("GET", `/v1/workspaces/${workspaceId}/environments/${environmentId}`);
1584
+ /** @deprecated use getVariableSet */
1585
+ async getEnvironment(workspaceId: string, environmentId: string): Promise<VariableSet> {
1586
+ return await this.getVariableSet(workspaceId, environmentId);
867
1587
  }
868
1588
 
1589
+ /** @deprecated use updateVariableSet */
869
1590
  async updateEnvironment(
870
1591
  workspaceId: string,
871
1592
  environmentId: string,
872
- request: UpdateWorkspaceEnvironmentRequest,
873
- ): Promise<WorkspaceEnvironment> {
874
- return await this.requestJson<WorkspaceEnvironment>(
875
- "PATCH",
876
- `/v1/workspaces/${workspaceId}/environments/${environmentId}`,
877
- request,
878
- );
1593
+ request: UpdateVariableSetRequest,
1594
+ ): Promise<VariableSet> {
1595
+ return await this.updateVariableSet(workspaceId, environmentId, request);
879
1596
  }
880
1597
 
1598
+ /** @deprecated use deleteVariableSet */
881
1599
  async deleteEnvironment(workspaceId: string, environmentId: string): Promise<void> {
882
- await this.requestJson<unknown>("DELETE", `/v1/workspaces/${workspaceId}/environments/${environmentId}`);
1600
+ await this.deleteVariableSet(workspaceId, environmentId);
883
1601
  }
884
1602
 
885
- /** Create or rotate a variable. The value never comes back on any read. */
1603
+ /** @deprecated use setVariableSetVariable */
886
1604
  async setEnvironmentVariable(
887
1605
  workspaceId: string,
888
1606
  environmentId: string,
889
1607
  name: string,
890
1608
  value: string,
891
- ): Promise<WorkspaceEnvironmentVariableMetadata> {
892
- return await this.requestJson<WorkspaceEnvironmentVariableMetadata>(
893
- "PUT",
894
- `/v1/workspaces/${workspaceId}/environments/${environmentId}/variables/${encodeURIComponent(name)}`,
895
- { value },
896
- );
1609
+ ): Promise<VariableSetVariableMetadata> {
1610
+ return await this.setVariableSetVariable(workspaceId, environmentId, name, value);
897
1611
  }
898
1612
 
899
- async deleteEnvironmentVariable(workspaceId: string, environmentId: string, name: string): Promise<void> {
900
- await this.requestJson<unknown>(
901
- "DELETE",
902
- `/v1/workspaces/${workspaceId}/environments/${environmentId}/variables/${encodeURIComponent(name)}`,
903
- );
1613
+ /** @deprecated use deleteVariableSetVariable */
1614
+ async deleteEnvironmentVariable(
1615
+ workspaceId: string,
1616
+ environmentId: string,
1617
+ name: string,
1618
+ ): Promise<void> {
1619
+ await this.deleteVariableSetVariable(workspaceId, environmentId, name);
904
1620
  }
905
1621
 
906
1622
  // --- Files -----------------------------------------------------------------------
907
1623
 
908
1624
  /** Step 1 of the upload flow: returns the pre-signed PUT target. */
909
- async beginFileUpload(workspaceId: string, request: CreateFileUploadRequest): Promise<CreateFileUploadResponse> {
910
- return await this.requestJson<CreateFileUploadResponse>("POST", `/v1/workspaces/${workspaceId}/files/uploads`, request);
1625
+ async beginFileUpload(
1626
+ workspaceId: string,
1627
+ request: CreateFileUploadRequest,
1628
+ ): Promise<CreateFileUploadResponse> {
1629
+ return await this.requestJson<CreateFileUploadResponse>(
1630
+ "POST",
1631
+ `/v1/workspaces/${workspaceId}/files/uploads`,
1632
+ request,
1633
+ );
911
1634
  }
912
1635
 
913
1636
  /** Step 3 of the upload flow: server verifies the object and marks it ready. */
@@ -927,12 +1650,14 @@ export class OpenGeniClient {
927
1650
  async uploadFile(workspaceId: string, input: UploadFileInput): Promise<FileAsset> {
928
1651
  // Copy Uint8Array views into a Blob so byte offsets/shared buffers can't
929
1652
  // leak surrounding bytes into the PUT body.
930
- const body: Blob | ArrayBuffer | string = input.data instanceof Uint8Array
931
- ? new Blob([input.data.slice()])
932
- : input.data;
933
- const sizeBytes = typeof body === "string"
934
- ? new TextEncoder().encode(body).byteLength
935
- : body instanceof Blob ? body.size : body.byteLength;
1653
+ const body: Blob | ArrayBuffer | string =
1654
+ input.data instanceof Uint8Array ? new Blob([input.data.slice()]) : input.data;
1655
+ const sizeBytes =
1656
+ typeof body === "string"
1657
+ ? new TextEncoder().encode(body).byteLength
1658
+ : body instanceof Blob
1659
+ ? body.size
1660
+ : body.byteLength;
936
1661
  const upload = await this.beginFileUpload(workspaceId, {
937
1662
  filename: input.filename,
938
1663
  contentType: input.contentType,
@@ -957,39 +1682,76 @@ export class OpenGeniClient {
957
1682
  }
958
1683
 
959
1684
  async getFile(workspaceId: string, fileId: string): Promise<FileAsset> {
960
- return await this.requestJson<FileAsset>("GET", `/v1/workspaces/${workspaceId}/files/${fileId}`);
1685
+ return await this.requestJson<FileAsset>(
1686
+ "GET",
1687
+ `/v1/workspaces/${workspaceId}/files/${fileId}`,
1688
+ );
961
1689
  }
962
1690
 
963
1691
  /** Mint a short-lived signed download URL for a ready file. */
964
- async createFileDownloadUrl(workspaceId: string, fileId: string): Promise<FileDownloadUrlResponse> {
965
- return await this.requestJson<FileDownloadUrlResponse>("POST", `/v1/workspaces/${workspaceId}/files/${fileId}/download-url`);
1692
+ async createFileDownloadUrl(
1693
+ workspaceId: string,
1694
+ fileId: string,
1695
+ ): Promise<FileDownloadUrlResponse> {
1696
+ return await this.requestJson<FileDownloadUrlResponse>(
1697
+ "POST",
1698
+ `/v1/workspaces/${workspaceId}/files/${fileId}/download-url`,
1699
+ );
966
1700
  }
967
1701
 
968
1702
  // --- Documents ----------------------------------------------------------------------
969
1703
 
970
- async createDocumentBase(workspaceId: string, request: CreateDocumentBaseRequest): Promise<DocumentBase> {
971
- return await this.requestJson<DocumentBase>("POST", `/v1/workspaces/${workspaceId}/document-bases`, request);
1704
+ async createDocumentBase(
1705
+ workspaceId: string,
1706
+ request: CreateDocumentBaseRequest,
1707
+ ): Promise<DocumentBase> {
1708
+ return await this.requestJson<DocumentBase>(
1709
+ "POST",
1710
+ `/v1/workspaces/${workspaceId}/document-bases`,
1711
+ request,
1712
+ );
972
1713
  }
973
1714
 
974
1715
  async listDocumentBases(workspaceId: string): Promise<DocumentBase[]> {
975
- return await this.requestJson<DocumentBase[]>("GET", `/v1/workspaces/${workspaceId}/document-bases`);
1716
+ return await this.requestJson<DocumentBase[]>(
1717
+ "GET",
1718
+ `/v1/workspaces/${workspaceId}/document-bases`,
1719
+ );
976
1720
  }
977
1721
 
978
1722
  async getDocumentBase(workspaceId: string, baseId: string): Promise<DocumentBase> {
979
- return await this.requestJson<DocumentBase>("GET", `/v1/workspaces/${workspaceId}/document-bases/${baseId}`);
1723
+ return await this.requestJson<DocumentBase>(
1724
+ "GET",
1725
+ `/v1/workspaces/${workspaceId}/document-bases/${baseId}`,
1726
+ );
980
1727
  }
981
1728
 
982
1729
  /** Index an uploaded file into the base. The file must be `ready`. */
983
- async addDocument(workspaceId: string, baseId: string, request: AddDocumentRequest): Promise<Document> {
984
- return await this.requestJson<Document>("POST", `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`, request);
1730
+ async addDocument(
1731
+ workspaceId: string,
1732
+ baseId: string,
1733
+ request: AddDocumentRequest,
1734
+ ): Promise<Document> {
1735
+ return await this.requestJson<Document>(
1736
+ "POST",
1737
+ `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`,
1738
+ request,
1739
+ );
985
1740
  }
986
1741
 
987
1742
  async listDocuments(workspaceId: string, baseId: string): Promise<Document[]> {
988
- return await this.requestJson<Document[]>("GET", `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`);
1743
+ return await this.requestJson<Document[]>(
1744
+ "GET",
1745
+ `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents`,
1746
+ );
989
1747
  }
990
1748
 
991
1749
  /** Retry indexing for a failed document. */
992
- async reindexDocument(workspaceId: string, baseId: string, documentId: string): Promise<Document> {
1750
+ async reindexDocument(
1751
+ workspaceId: string,
1752
+ baseId: string,
1753
+ documentId: string,
1754
+ ): Promise<Document> {
993
1755
  return await this.requestJson<Document>(
994
1756
  "POST",
995
1757
  `/v1/workspaces/${workspaceId}/document-bases/${baseId}/documents/${documentId}/reindex`,
@@ -1023,10 +1785,17 @@ export class OpenGeniClient {
1023
1785
  workspaceId: string,
1024
1786
  request: DocumentSearchRequest,
1025
1787
  ): Promise<DocumentSearchResponse> {
1026
- return await this.requestJson<DocumentSearchResponse>("POST", `/v1/workspaces/${workspaceId}/knowledge/search`, request);
1788
+ return await this.requestJson<DocumentSearchResponse>(
1789
+ "POST",
1790
+ `/v1/workspaces/${workspaceId}/knowledge/search`,
1791
+ request,
1792
+ );
1027
1793
  }
1028
1794
 
1029
- async listKnowledgeMemories(workspaceId: string, request: KnowledgeMemorySearchRequest = {}): Promise<KnowledgeMemory[]> {
1795
+ async listKnowledgeMemories(
1796
+ workspaceId: string,
1797
+ request: KnowledgeMemorySearchRequest = {},
1798
+ ): Promise<KnowledgeMemory[]> {
1030
1799
  const params = new URLSearchParams();
1031
1800
  if (request.query) params.set("query", request.query);
1032
1801
  if (request.status) params.set("status", request.status);
@@ -1034,19 +1803,75 @@ export class OpenGeniClient {
1034
1803
  if (request.scope) params.set("scope", request.scope);
1035
1804
  if (request.limit) params.set("limit", String(request.limit));
1036
1805
  const query = params.toString();
1037
- return await this.requestJson<KnowledgeMemory[]>("GET", `/v1/workspaces/${workspaceId}/knowledge/memories${query ? `?${query}` : ""}`);
1806
+ return await this.requestJson<KnowledgeMemory[]>(
1807
+ "GET",
1808
+ `/v1/workspaces/${workspaceId}/knowledge/memories${query ? `?${query}` : ""}`,
1809
+ );
1038
1810
  }
1039
1811
 
1040
1812
  async getKnowledgeMemory(workspaceId: string, memoryId: string): Promise<KnowledgeMemory> {
1041
- return await this.requestJson<KnowledgeMemory>("GET", `/v1/workspaces/${workspaceId}/knowledge/memories/${memoryId}`);
1813
+ return await this.requestJson<KnowledgeMemory>(
1814
+ "GET",
1815
+ `/v1/workspaces/${workspaceId}/knowledge/memories/${memoryId}`,
1816
+ );
1042
1817
  }
1043
1818
 
1044
- async createKnowledgeMemory(workspaceId: string, request: CreateKnowledgeMemoryRequest): Promise<KnowledgeMemory> {
1045
- return await this.requestJson<KnowledgeMemory>("POST", `/v1/workspaces/${workspaceId}/knowledge/memories`, request);
1819
+ async createKnowledgeMemory(
1820
+ workspaceId: string,
1821
+ request: CreateKnowledgeMemoryRequest,
1822
+ ): Promise<KnowledgeMemory> {
1823
+ return await this.requestJson<KnowledgeMemory>(
1824
+ "POST",
1825
+ `/v1/workspaces/${workspaceId}/knowledge/memories`,
1826
+ request,
1827
+ );
1828
+ }
1829
+
1830
+ async updateKnowledgeMemory(
1831
+ workspaceId: string,
1832
+ memoryId: string,
1833
+ request: UpdateKnowledgeMemoryRequest,
1834
+ ): Promise<KnowledgeMemory> {
1835
+ return await this.requestJson<KnowledgeMemory>(
1836
+ "PATCH",
1837
+ `/v1/workspaces/${workspaceId}/knowledge/memories/${memoryId}`,
1838
+ request,
1839
+ );
1840
+ }
1841
+
1842
+ /** Hybrid (semantic + keyword) search over the workspace's agent-visible memory. */
1843
+ async searchWorkspaceMemories(
1844
+ workspaceId: string,
1845
+ request: WorkspaceMemorySearchRequest,
1846
+ ): Promise<WorkspaceMemorySearchResponse> {
1847
+ return await this.requestJson<WorkspaceMemorySearchResponse>(
1848
+ "POST",
1849
+ `/v1/workspaces/${workspaceId}/knowledge/memories/search`,
1850
+ request,
1851
+ );
1046
1852
  }
1047
1853
 
1048
- async updateKnowledgeMemory(workspaceId: string, memoryId: string, request: UpdateKnowledgeMemoryRequest): Promise<KnowledgeMemory> {
1049
- return await this.requestJson<KnowledgeMemory>("PATCH", `/v1/workspaces/${workspaceId}/knowledge/memories/${memoryId}`, request);
1854
+ /** Deep-merge a settings patch into the workspace (preserves unknown keys). */
1855
+ async updateWorkspaceSettings(
1856
+ workspaceId: string,
1857
+ request: UpdateWorkspaceSettingsRequest,
1858
+ ): Promise<Workspace> {
1859
+ return await this.requestJson<Workspace>(
1860
+ "PATCH",
1861
+ `/v1/workspaces/${workspaceId}/settings`,
1862
+ request,
1863
+ );
1864
+ }
1865
+
1866
+ async setWorkspaceDefaultRig(
1867
+ workspaceId: string,
1868
+ request: SetWorkspaceDefaultRigRequest,
1869
+ ): Promise<Workspace> {
1870
+ return await this.requestJson<Workspace>(
1871
+ "PUT",
1872
+ `/v1/workspaces/${workspaceId}/default-rig`,
1873
+ request,
1874
+ );
1050
1875
  }
1051
1876
 
1052
1877
  // --- Capability packs ------------------------------------------------------------------
@@ -1057,15 +1882,29 @@ export class OpenGeniClient {
1057
1882
  }
1058
1883
 
1059
1884
  /** Register (or replace) a workspace-scoped pack from a manifest. */
1060
- async registerPack(workspaceId: string, manifest: RegisterCapabilityPackRequest): Promise<WorkspaceRegisteredPack> {
1061
- return await this.requestJson<WorkspaceRegisteredPack>("POST", `/v1/workspaces/${workspaceId}/packs`, manifest);
1885
+ async registerPack(
1886
+ workspaceId: string,
1887
+ manifest: RegisterCapabilityPackRequest,
1888
+ ): Promise<WorkspaceRegisteredPack> {
1889
+ return await this.requestJson<WorkspaceRegisteredPack>(
1890
+ "POST",
1891
+ `/v1/workspaces/${workspaceId}/packs`,
1892
+ manifest,
1893
+ );
1062
1894
  }
1063
1895
 
1064
1896
  async getPack(workspaceId: string, packId: string): Promise<GetPackResponse> {
1065
- return await this.requestJson<GetPackResponse>("GET", `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`);
1897
+ return await this.requestJson<GetPackResponse>(
1898
+ "GET",
1899
+ `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`,
1900
+ );
1066
1901
  }
1067
1902
 
1068
- async enablePack(workspaceId: string, packId: string, request: EnablePackRequest = {}): Promise<PackInstallation> {
1903
+ async enablePack(
1904
+ workspaceId: string,
1905
+ packId: string,
1906
+ request: EnablePackRequest = {},
1907
+ ): Promise<PackInstallation> {
1069
1908
  return await this.requestJson<PackInstallation>(
1070
1909
  "POST",
1071
1910
  `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}/enable`,
@@ -1075,22 +1914,38 @@ export class OpenGeniClient {
1075
1914
 
1076
1915
  /** Unregister a workspace-scoped pack (built-in packs cannot be deleted). */
1077
1916
  async deletePack(workspaceId: string, packId: string): Promise<void> {
1078
- await this.requestVoid("DELETE", `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`);
1917
+ await this.requestVoid(
1918
+ "DELETE",
1919
+ `/v1/workspaces/${workspaceId}/packs/${encodeURIComponent(packId)}`,
1920
+ );
1079
1921
  }
1080
1922
 
1081
1923
  async listPackInstallations(workspaceId: string): Promise<PackInstallation[]> {
1082
- return await this.requestJson<PackInstallation[]>("GET", `/v1/workspaces/${workspaceId}/packs/installations`);
1924
+ return await this.requestJson<PackInstallation[]>(
1925
+ "GET",
1926
+ `/v1/workspaces/${workspaceId}/packs/installations`,
1927
+ );
1083
1928
  }
1084
1929
 
1085
1930
  // --- Capabilities -------------------------------------------------------------------------
1086
1931
 
1087
1932
  async listCapabilities(workspaceId: string): Promise<CapabilityCatalogResponse> {
1088
- return await this.requestJson<CapabilityCatalogResponse>("GET", `/v1/workspaces/${workspaceId}/capabilities`);
1933
+ return await this.requestJson<CapabilityCatalogResponse>(
1934
+ "GET",
1935
+ `/v1/workspaces/${workspaceId}/capabilities`,
1936
+ );
1089
1937
  }
1090
1938
 
1091
1939
  /** Add a manual capability catalog item (e.g. a remote MCP server). */
1092
- async createCapability(workspaceId: string, request: CreateCapabilityCatalogItemRequest): Promise<CapabilityCatalogItem> {
1093
- return await this.requestJson<CapabilityCatalogItem>("POST", `/v1/workspaces/${workspaceId}/capabilities`, request);
1940
+ async createCapability(
1941
+ workspaceId: string,
1942
+ request: CreateCapabilityCatalogItemRequest,
1943
+ ): Promise<CapabilityCatalogItem> {
1944
+ return await this.requestJson<CapabilityCatalogItem>(
1945
+ "POST",
1946
+ `/v1/workspaces/${workspaceId}/capabilities`,
1947
+ request,
1948
+ );
1094
1949
  }
1095
1950
 
1096
1951
  async enableCapability(
@@ -1105,7 +1960,10 @@ export class OpenGeniClient {
1105
1960
  );
1106
1961
  }
1107
1962
 
1108
- async disableCapability(workspaceId: string, capabilityId: string): Promise<CapabilityInstallation> {
1963
+ async disableCapability(
1964
+ workspaceId: string,
1965
+ capabilityId: string,
1966
+ ): Promise<CapabilityInstallation> {
1109
1967
  return await this.requestJson<CapabilityInstallation>(
1110
1968
  "POST",
1111
1969
  `/v1/workspaces/${workspaceId}/capabilities/${encodeURIComponent(capabilityId)}/disable`,
@@ -1128,6 +1986,66 @@ export class OpenGeniClient {
1128
1986
  );
1129
1987
  }
1130
1988
 
1989
+ // --- Connections -------------------------------------------------------------------------------
1990
+
1991
+ async listConnections(workspaceId: string): Promise<ConnectionMetadata[]> {
1992
+ const response = await this.requestJson<ListConnectionsResponse>(
1993
+ "GET",
1994
+ `/v1/workspaces/${workspaceId}/connections`,
1995
+ );
1996
+ return response.connections;
1997
+ }
1998
+
1999
+ async createConnection(
2000
+ workspaceId: string,
2001
+ request: CreateConnectionRequest,
2002
+ ): Promise<ConnectionMetadata> {
2003
+ const response = await this.requestJson<ConnectionResponse>(
2004
+ "POST",
2005
+ `/v1/workspaces/${workspaceId}/connections`,
2006
+ request,
2007
+ );
2008
+ return response.connection;
2009
+ }
2010
+
2011
+ async updateConnection(
2012
+ workspaceId: string,
2013
+ connectionId: string,
2014
+ request: UpdateConnectionRequest,
2015
+ ): Promise<ConnectionMetadata> {
2016
+ const response = await this.requestJson<ConnectionResponse>(
2017
+ "PATCH",
2018
+ `/v1/workspaces/${workspaceId}/connections/${connectionId}`,
2019
+ request,
2020
+ );
2021
+ return response.connection;
2022
+ }
2023
+
2024
+ async deleteConnection(workspaceId: string, connectionId: string): Promise<ConnectionMetadata> {
2025
+ const response = await this.requestJson<ConnectionResponse>(
2026
+ "DELETE",
2027
+ `/v1/workspaces/${workspaceId}/connections/${connectionId}`,
2028
+ );
2029
+ return response.connection;
2030
+ }
2031
+
2032
+ /** Start an OAuth connection flow; redirect the user to the returned `authorizationUrl`. */
2033
+ async startConnectionOAuth(
2034
+ workspaceId: string,
2035
+ request: OAuthStartRequest,
2036
+ ): Promise<OAuthStartResponse> {
2037
+ return await this.requestJson<OAuthStartResponse>(
2038
+ "POST",
2039
+ `/v1/workspaces/${workspaceId}/connections/oauth/start`,
2040
+ request,
2041
+ );
2042
+ }
2043
+
2044
+ /** Public, immutably-cached URL for a catalog item's logo, or null when the item has none. */
2045
+ catalogAssetUrl(logoAssetPath: string | null): string | null {
2046
+ return logoAssetPath ? `${this.baseUrl}/v1/${logoAssetPath}` : null;
2047
+ }
2048
+
1131
2049
  // --- GitHub ----------------------------------------------------------------------------------
1132
2050
 
1133
2051
  /** GitHub App configuration status + a signed install URL when configured. */
@@ -1145,12 +2063,18 @@ export class OpenGeniClient {
1145
2063
  }
1146
2064
 
1147
2065
  async listGitHubRepositories(workspaceId: string): Promise<GitHubRepositoriesResponse> {
1148
- return await this.requestJson<GitHubRepositoriesResponse>("GET", `/v1/workspaces/${workspaceId}/github/repositories`);
2066
+ return await this.requestJson<GitHubRepositoriesResponse>(
2067
+ "GET",
2068
+ `/v1/workspaces/${workspaceId}/github/repositories`,
2069
+ );
1149
2070
  }
1150
2071
 
1151
2072
  /** Re-sync the installation's repository list from GitHub. */
1152
2073
  async syncGitHubRepositories(workspaceId: string): Promise<GitHubRepositoriesResponse> {
1153
- return await this.requestJson<GitHubRepositoriesResponse>("POST", `/v1/workspaces/${workspaceId}/github/repositories/sync`);
2074
+ return await this.requestJson<GitHubRepositoriesResponse>(
2075
+ "POST",
2076
+ `/v1/workspaces/${workspaceId}/github/repositories/sync`,
2077
+ );
1154
2078
  }
1155
2079
 
1156
2080
  /** Build a GitHub App manifest + the GitHub URL to submit it to. */
@@ -1168,18 +2092,31 @@ export class OpenGeniClient {
1168
2092
  // --- API keys ----------------------------------------------------------------------------------
1169
2093
 
1170
2094
  async listApiKeys(workspaceId: string): Promise<ApiKey[]> {
1171
- const response = await this.requestJson<ListApiKeysResponse>("GET", `/v1/workspaces/${workspaceId}/api-keys`);
2095
+ const response = await this.requestJson<ListApiKeysResponse>(
2096
+ "GET",
2097
+ `/v1/workspaces/${workspaceId}/api-keys`,
2098
+ );
1172
2099
  return response.apiKeys;
1173
2100
  }
1174
2101
 
1175
2102
  /** The returned `token` is shown once; only its prefix is stored. */
1176
- async createApiKey(workspaceId: string, request: CreateApiKeyRequest): Promise<CreateApiKeyResponse> {
1177
- return await this.requestJson<CreateApiKeyResponse>("POST", `/v1/workspaces/${workspaceId}/api-keys`, request);
2103
+ async createApiKey(
2104
+ workspaceId: string,
2105
+ request: CreateApiKeyRequest,
2106
+ ): Promise<CreateApiKeyResponse> {
2107
+ return await this.requestJson<CreateApiKeyResponse>(
2108
+ "POST",
2109
+ `/v1/workspaces/${workspaceId}/api-keys`,
2110
+ request,
2111
+ );
1178
2112
  }
1179
2113
 
1180
2114
  /** Revoke an API key. Returns the revoked key. */
1181
2115
  async deleteApiKey(workspaceId: string, apiKeyId: string): Promise<ApiKey> {
1182
- return await this.requestJson<ApiKey>("DELETE", `/v1/workspaces/${workspaceId}/api-keys/${apiKeyId}`);
2116
+ return await this.requestJson<ApiKey>(
2117
+ "DELETE",
2118
+ `/v1/workspaces/${workspaceId}/api-keys/${apiKeyId}`,
2119
+ );
1183
2120
  }
1184
2121
 
1185
2122
  // --- Billing (account-scoped) --------------------------------------------------------------------
@@ -1190,17 +2127,26 @@ export class OpenGeniClient {
1190
2127
  });
1191
2128
  }
1192
2129
 
1193
- async getBillingUsage(options: { accountId?: string; workspaceId?: string } = {}): Promise<BillingUsageResponse> {
2130
+ async getBillingUsage(
2131
+ options: { accountId?: string; workspaceId?: string } = {},
2132
+ ): Promise<BillingUsageResponse> {
1194
2133
  return await this.requestJson<BillingUsageResponse>("GET", "/v1/billing/usage", undefined, {
1195
2134
  ...(options.accountId !== undefined ? { accountId: options.accountId } : {}),
1196
2135
  ...(options.workspaceId !== undefined ? { workspaceId: options.workspaceId } : {}),
1197
2136
  });
1198
2137
  }
1199
2138
 
1200
- async getBillingEntitlements(options: { accountId?: string } = {}): Promise<BillingEntitlementsResponse> {
1201
- return await this.requestJson<BillingEntitlementsResponse>("GET", "/v1/billing/entitlements", undefined, {
1202
- ...(options.accountId !== undefined ? { accountId: options.accountId } : {}),
1203
- });
2139
+ async getBillingEntitlements(
2140
+ options: { accountId?: string } = {},
2141
+ ): Promise<BillingEntitlementsResponse> {
2142
+ return await this.requestJson<BillingEntitlementsResponse>(
2143
+ "GET",
2144
+ "/v1/billing/entitlements",
2145
+ undefined,
2146
+ {
2147
+ ...(options.accountId !== undefined ? { accountId: options.accountId } : {}),
2148
+ },
2149
+ );
1204
2150
  }
1205
2151
 
1206
2152
  /** Start a Stripe checkout for prepaid credits. */
@@ -1211,10 +2157,12 @@ export class OpenGeniClient {
1211
2157
  // --- Internals -------------------------------------------------------------
1212
2158
 
1213
2159
  private headers(): Record<string, string> {
1214
- const extra = typeof this.options.headers === "function" ? this.options.headers() : this.options.headers;
2160
+ const extra =
2161
+ typeof this.options.headers === "function" ? this.options.headers() : this.options.headers;
1215
2162
  return {
1216
2163
  ...(this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}),
1217
2164
  ...extra,
2165
+ [OPENGENI_API_CONTRACT_HEADER]: OPENGENI_API_CONTRACT_REVISION,
1218
2166
  };
1219
2167
  }
1220
2168
 
@@ -1227,17 +2175,27 @@ export class OpenGeniClient {
1227
2175
 
1228
2176
  /** Connection state + the codex models the workspace may select (empty until connected). */
1229
2177
  async codexStatus(workspaceId: string): Promise<CodexConnectionStatus> {
1230
- return await this.requestJson<CodexConnectionStatus>("GET", `/v1/workspaces/${workspaceId}/codex/status`);
2178
+ return await this.requestJson<CodexConnectionStatus>(
2179
+ "GET",
2180
+ `/v1/workspaces/${workspaceId}/codex/status`,
2181
+ );
1231
2182
  }
1232
2183
 
1233
2184
  /** Begin device-code login: show `userCode` at `verificationUri`, then poll with `state`. */
1234
2185
  async codexConnectStart(workspaceId: string): Promise<CodexConnectStart> {
1235
- return await this.requestJson<CodexConnectStart>("POST", `/v1/workspaces/${workspaceId}/codex/connect/start`);
2186
+ return await this.requestJson<CodexConnectStart>(
2187
+ "POST",
2188
+ `/v1/workspaces/${workspaceId}/codex/connect/start`,
2189
+ );
1236
2190
  }
1237
2191
 
1238
2192
  /** Poll device-code authorization with the `state` from {@link codexConnectStart}. */
1239
2193
  async codexConnectPoll(workspaceId: string, state: string): Promise<CodexConnectPoll> {
1240
- return await this.requestJson<CodexConnectPoll>("POST", `/v1/workspaces/${workspaceId}/codex/connect/poll`, { state });
2194
+ return await this.requestJson<CodexConnectPoll>(
2195
+ "POST",
2196
+ `/v1/workspaces/${workspaceId}/codex/connect/poll`,
2197
+ { state },
2198
+ );
1241
2199
  }
1242
2200
 
1243
2201
  /** Remaining usage / limits for the connected (ACTIVE) subscription. Back-compat. */
@@ -1247,53 +2205,105 @@ export class OpenGeniClient {
1247
2205
 
1248
2206
  /** Live per-account usage read (refreshes THIS account's bearer; writes the cache). */
1249
2207
  async codexAccountUsage(workspaceId: string, accountId: string): Promise<CodexUsage> {
1250
- return await this.requestJson<CodexUsage>("GET", `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}/usage`);
2208
+ return await this.requestJson<CodexUsage>(
2209
+ "GET",
2210
+ `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}/usage`,
2211
+ );
1251
2212
  }
1252
2213
 
1253
2214
  /** Batched live refresh across every connected account, keyed by credential id. */
1254
2215
  async refreshCodexUsage(workspaceId: string): Promise<{ usage: CodexUsageMap }> {
1255
- return await this.requestJson<{ usage: CodexUsageMap }>("POST", `/v1/workspaces/${workspaceId}/codex/usage/refresh`);
2216
+ return await this.requestJson<{ usage: CodexUsageMap }>(
2217
+ "POST",
2218
+ `/v1/workspaces/${workspaceId}/codex/usage/refresh`,
2219
+ );
1256
2220
  }
1257
2221
 
1258
2222
  /** Disconnect ALL accounts (legacy workspace-wide). Prefer `disconnectCodexAccount`. */
1259
2223
  async codexDisconnect(workspaceId: string): Promise<{ disconnected: boolean }> {
1260
- return await this.requestJson<{ disconnected: boolean }>("DELETE", `/v1/workspaces/${workspaceId}/codex`);
2224
+ return await this.requestJson<{ disconnected: boolean }>(
2225
+ "DELETE",
2226
+ `/v1/workspaces/${workspaceId}/codex`,
2227
+ );
1261
2228
  }
1262
2229
 
1263
2230
  /** List every connected Codex account + the workspace active pointer + settings. */
1264
2231
  async listCodexAccounts(workspaceId: string): Promise<CodexAccountsResponse> {
1265
- return await this.requestJson<CodexAccountsResponse>("GET", `/v1/workspaces/${workspaceId}/codex/accounts`);
2232
+ return await this.requestJson<CodexAccountsResponse>(
2233
+ "GET",
2234
+ `/v1/workspaces/${workspaceId}/codex/accounts`,
2235
+ );
1266
2236
  }
1267
2237
 
1268
2238
  /** Switch the workspace ACTIVE Codex account (the one unpinned sessions use). */
1269
- async activateCodexAccount(workspaceId: string, accountId: string): Promise<{ activated: boolean; accountId: string }> {
1270
- return await this.requestJson<{ activated: boolean; accountId: string }>("POST", `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}/activate`);
2239
+ async activateCodexAccount(
2240
+ workspaceId: string,
2241
+ accountId: string,
2242
+ ): Promise<{ activated: boolean; accountId: string }> {
2243
+ return await this.requestJson<{ activated: boolean; accountId: string }>(
2244
+ "POST",
2245
+ `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}/activate`,
2246
+ );
1271
2247
  }
1272
2248
 
1273
2249
  /** P3: enable/disable Codex auto-rotation and/or pick the strategy. Returns the effective settings. */
1274
2250
  async setCodexRotationSettings(
1275
2251
  workspaceId: string,
1276
- patch: { rotationEnabled?: boolean; rotationStrategy?: CodexRotationSettings["rotationStrategy"] },
2252
+ patch: {
2253
+ rotationEnabled?: boolean;
2254
+ rotationStrategy?: CodexRotationSettings["rotationStrategy"];
2255
+ },
1277
2256
  ): Promise<CodexRotationSettings> {
1278
- return await this.requestJson<CodexRotationSettings>("PATCH", `/v1/workspaces/${workspaceId}/codex/settings`, patch);
2257
+ return await this.requestJson<CodexRotationSettings>(
2258
+ "PATCH",
2259
+ `/v1/workspaces/${workspaceId}/codex/settings`,
2260
+ patch,
2261
+ );
1279
2262
  }
1280
2263
 
1281
2264
  /** Disconnect ONE Codex account by id (re-picks active when the removed one was active). */
1282
- async disconnectCodexAccount(workspaceId: string, accountId: string): Promise<{ disconnected: boolean; newActiveId: string | null }> {
1283
- return await this.requestJson<{ disconnected: boolean; newActiveId: string | null }>("DELETE", `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}`);
2265
+ async disconnectCodexAccount(
2266
+ workspaceId: string,
2267
+ accountId: string,
2268
+ ): Promise<{ disconnected: boolean; newActiveId: string | null }> {
2269
+ return await this.requestJson<{ disconnected: boolean; newActiveId: string | null }>(
2270
+ "DELETE",
2271
+ `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}`,
2272
+ );
1284
2273
  }
1285
2274
 
1286
2275
  /** Rename a Codex account (label only in P1). */
1287
- async renameCodexAccount(workspaceId: string, accountId: string, label: string | null): Promise<CodexAccount> {
1288
- return await this.requestJson<CodexAccount>("PATCH", `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}`, { label });
2276
+ async renameCodexAccount(
2277
+ workspaceId: string,
2278
+ accountId: string,
2279
+ label: string | null,
2280
+ ): Promise<CodexAccount> {
2281
+ return await this.requestJson<CodexAccount>(
2282
+ "PATCH",
2283
+ `/v1/workspaces/${workspaceId}/codex/accounts/${accountId}`,
2284
+ { label },
2285
+ );
1289
2286
  }
1290
2287
 
1291
2288
  /** Pin (or unpin via "auto") a session's Codex account. Applies on the next turn. */
1292
- async pinSessionCodexAccount(workspaceId: string, sessionId: string, target: string): Promise<{ pinned: string }> {
1293
- return await this.requestJson<{ pinned: string }>("POST", `/v1/workspaces/${workspaceId}/sessions/${sessionId}/codex-account`, { target });
2289
+ async pinSessionCodexAccount(
2290
+ workspaceId: string,
2291
+ sessionId: string,
2292
+ target: string,
2293
+ ): Promise<{ pinned: string }> {
2294
+ return await this.requestJson<{ pinned: string }>(
2295
+ "POST",
2296
+ `/v1/workspaces/${workspaceId}/sessions/${sessionId}/codex-account`,
2297
+ { target },
2298
+ );
1294
2299
  }
1295
2300
 
1296
- private async requestJson<T>(method: string, path: string, body?: unknown, query: Record<string, string> = {}): Promise<T> {
2301
+ private async requestJson<T>(
2302
+ method: string,
2303
+ path: string,
2304
+ body?: unknown,
2305
+ query: Record<string, string> = {},
2306
+ ): Promise<T> {
1297
2307
  const response = await this.fetchImpl(this.url(path, query), {
1298
2308
  method,
1299
2309
  headers: {
@@ -1303,6 +2313,7 @@ export class OpenGeniClient {
1303
2313
  },
1304
2314
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
1305
2315
  });
2316
+ assertApiContractResponse(response);
1306
2317
  if (!response.ok) {
1307
2318
  throw new OpenGeniApiError(response.status, await safeText(response));
1308
2319
  }
@@ -1320,12 +2331,20 @@ export class OpenGeniClient {
1320
2331
  },
1321
2332
  ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
1322
2333
  });
2334
+ assertApiContractResponse(response);
1323
2335
  if (!response.ok) {
1324
2336
  throw new OpenGeniApiError(response.status, await safeText(response));
1325
2337
  }
1326
2338
  }
1327
2339
  }
1328
2340
 
2341
+ function assertApiContractResponse(response: Response): void {
2342
+ const actual = response.headers.get(OPENGENI_API_CONTRACT_HEADER);
2343
+ if (actual && actual !== OPENGENI_API_CONTRACT_REVISION) {
2344
+ throw new OpenGeniApiContractMismatchError(OPENGENI_API_CONTRACT_REVISION, actual);
2345
+ }
2346
+ }
2347
+
1329
2348
  async function safeText(response: Response): Promise<string> {
1330
2349
  try {
1331
2350
  return await response.text();
@@ -1333,7 +2352,3 @@ async function safeText(response: Response): Promise<string> {
1333
2352
  return "";
1334
2353
  }
1335
2354
  }
1336
-
1337
- function delay(ms: number): Promise<void> {
1338
- return new Promise((resolve) => setTimeout(resolve, ms));
1339
- }