@opengeni/contracts 0.7.0 → 0.10.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/index.ts CHANGED
@@ -5,6 +5,9 @@ export const SessionStatus = z.enum([
5
5
  "running",
6
6
  "idle",
7
7
  "requires_action",
8
+ "recovering",
9
+ "waiting_capacity",
10
+ "paused",
8
11
  "failed",
9
12
  "cancelled",
10
13
  ]);
@@ -470,12 +473,22 @@ export const Permission = z.enum([
470
473
  "github:manage",
471
474
  "github:use",
472
475
  "api_keys:manage",
476
+ "connections:read",
477
+ "connections:write",
478
+ /** @deprecated alias of variable-sets:manage */
473
479
  "environments:manage",
480
+ /** @deprecated alias of variable-sets:use */
474
481
  "environments:use",
482
+ "variable-sets:manage",
483
+ "variable-sets:use",
475
484
  // Attach or rotate per-session third-party MCP server credentials. Deliberately
476
485
  // not part of the worker's default first-party MCP permission set: a sandboxed
477
486
  // agent must not be able to hand itself new bearer credentials.
478
487
  "mcp_servers:attach",
488
+ // Programmatic sandbox -> tool access through the first-party MCP gate. This is
489
+ // intentionally narrow and is never part of first-party MCP defaults; callers
490
+ // must receive it through an explicit delegated `ogd_` mint carrying sessionId.
491
+ "toolspace:call",
479
492
  "goals:manage",
480
493
  // Bring-your-own-compute (M5). enrollments:read lists a workspace's machines;
481
494
  // enrollments:manage approves a device-flow enrollment (the LOUD whole-machine
@@ -484,9 +497,20 @@ export const Permission = z.enum([
484
497
  // admin-shaped action. workspace:admin is the super-wildcard over both.
485
498
  "enrollments:read",
486
499
  "enrollments:manage",
500
+ // Rigs (workspace-scoped, versioned sandbox machine definitions). rigs:use is
501
+ // read + propose-change (the agent-native, additive path a sandboxed session
502
+ // is trusted with); rigs:manage is create/edit/activate/promote/delete (the
503
+ // admin-shaped path that mints or rolls versions). workspace:admin is the
504
+ // super-wildcard over both.
505
+ "rigs:use",
506
+ "rigs:manage",
487
507
  ]);
488
508
  export type Permission = z.infer<typeof Permission>;
489
509
 
510
+ export function prefixedMcpToolName(registryId: string, toolName: string): string {
511
+ return `${registryId}__${toolName}`;
512
+ }
513
+
490
514
  export const ProductAccessMode = z.enum(["local", "configured", "managed"]);
491
515
  export type ProductAccessMode = z.infer<typeof ProductAccessMode>;
492
516
 
@@ -522,14 +546,66 @@ export const Workspace = z.object({
522
546
  // Per-workspace agent persona template (white-label override). null means
523
547
  // the deployment default (OPENGENI_AGENT_INSTRUCTIONS_TEMPLATE /
524
548
  // DEFAULT_AGENT_INSTRUCTIONS) is used. The runtime always injects the
525
- // non-bypassable CORE (goal-loop ownership + environment block), so an
549
+ // non-bypassable CORE (goal-loop ownership + variableSet block), so an
526
550
  // override restyles the persona without dropping that contract.
527
551
  agentInstructions: z.string().nullable(),
552
+ // Growth-ready per-workspace settings bag (migration 0045). Known keys are
553
+ // validated by WorkspaceSettingsSchema; unknown keys are preserved across
554
+ // PATCH merges so newer settings survive an older server.
555
+ settings: z.record(z.string(), z.unknown()),
556
+ inferenceState: z.enum(["active", "paused"]),
557
+ inferenceGeneration: z.number().int().nonnegative(),
558
+ inferenceReason: z.string().nullable(),
559
+ inferenceChangedBy: z.string().nullable(),
560
+ inferenceChangedAt: z.string().nullable(),
561
+ // Workspace default rig used by session/scheduled-task create fallback.
562
+ defaultRigId: z.string().uuid().nullable(),
528
563
  createdAt: z.string(),
529
564
  updatedAt: z.string(),
530
565
  });
531
566
  export type Workspace = z.infer<typeof Workspace>;
532
567
 
568
+ // Validates the KNOWN keys of workspaces.settings; passthrough keeps unknown
569
+ // (future) keys rather than stripping them. memoryEnabled gates Workspace Memory
570
+ // V1 agent surfaces (turn injection + first-party memory tools); default false.
571
+ export const WorkspaceSettingsSchema = z
572
+ .object({
573
+ memoryEnabled: z.boolean().optional(),
574
+ })
575
+ .passthrough();
576
+ export type WorkspaceSettings = z.infer<typeof WorkspaceSettingsSchema>;
577
+
578
+ // Resolve the effective memoryEnabled flag from a raw settings bag (default off).
579
+ export function resolveWorkspaceMemoryEnabled(settings: unknown): boolean {
580
+ const parsed = WorkspaceSettingsSchema.safeParse(settings ?? {});
581
+ return parsed.success ? parsed.data.memoryEnabled === true : false;
582
+ }
583
+
584
+ // PATCH body for workspace settings: a partial patch that deep-merges into the
585
+ // stored bag. memoryEnabled is the only typed key today; passthrough carries
586
+ // forward-compatible unknown keys through validation.
587
+ export const UpdateWorkspaceSettingsRequest = z
588
+ .object({
589
+ memoryEnabled: z.boolean().optional(),
590
+ })
591
+ .passthrough();
592
+ export type UpdateWorkspaceSettingsRequest = z.infer<typeof UpdateWorkspaceSettingsRequest>;
593
+
594
+ export const SetWorkspaceDefaultRigRequest = z.object({
595
+ rigId: z.string().uuid().nullable(),
596
+ });
597
+ export type SetWorkspaceDefaultRigRequest = z.infer<typeof SetWorkspaceDefaultRigRequest>;
598
+
599
+ // PUT body for the workspace model policy (full replace, not a merge). null (or
600
+ // omitted) = unrestricted for that dimension; an EMPTY array is a valid,
601
+ // explicit total block. Entries are provider ids / exact model ids as the
602
+ // router resolves them (see evaluateWorkspaceModelPolicy).
603
+ export const UpdateWorkspaceModelPolicyRequest = z.object({
604
+ allowedProviders: z.array(z.string().min(1).max(128)).max(64).nullable().optional(),
605
+ allowedModels: z.array(z.string().min(1).max(256)).max(256).nullable().optional(),
606
+ });
607
+ export type UpdateWorkspaceModelPolicyRequest = z.infer<typeof UpdateWorkspaceModelPolicyRequest>;
608
+
533
609
  export const AccountGrant = z.object({
534
610
  accountId: z.string().uuid(),
535
611
  subjectId: z.string().min(1),
@@ -570,17 +646,32 @@ export const DelegatedAccessTokenPayload = z.object({
570
646
  // Worker-asserted session scope for first-party MCP calls (HMAC-signed, not
571
647
  // agent-controlled); enables session-scoped tools such as goal management.
572
648
  sessionId: z.string().uuid().optional(),
649
+ // The turn making the call (the caller's identity), HMAC-signed by the worker
650
+ // at turn setup. Lets a tool classify WHO is calling from the token itself,
651
+ // instead of racily re-reading the session's live active_turn_id — e.g. the
652
+ // sacred-pause guard must know if the CALLER is a machine child-notification
653
+ // turn, and the active pointer can flip to another turn mid-check.
654
+ turnId: z.string().uuid().optional(),
573
655
  exp: z.number().int().positive(),
574
656
  });
575
657
  export type DelegatedAccessTokenPayload = z.infer<typeof DelegatedAccessTokenPayload>;
576
658
 
577
- export async function signDelegatedAccessToken(secret: string, payload: DelegatedAccessTokenPayload): Promise<string> {
578
- const encodedPayload = base64UrlEncode(JSON.stringify(DelegatedAccessTokenPayload.parse(payload)));
659
+ export async function signDelegatedAccessToken(
660
+ secret: string,
661
+ payload: DelegatedAccessTokenPayload,
662
+ ): Promise<string> {
663
+ const encodedPayload = base64UrlEncode(
664
+ JSON.stringify(DelegatedAccessTokenPayload.parse(payload)),
665
+ );
579
666
  const signature = await hmacSha256Base64Url(secret, encodedPayload);
580
667
  return `ogd_${encodedPayload}.${signature}`;
581
668
  }
582
669
 
583
- export async function verifyDelegatedAccessToken(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<DelegatedAccessTokenPayload | null> {
670
+ export async function verifyDelegatedAccessToken(
671
+ secret: string,
672
+ token: string,
673
+ nowSeconds = Math.floor(Date.now() / 1000),
674
+ ): Promise<DelegatedAccessTokenPayload | null> {
584
675
  if (!token.startsWith("ogd_")) {
585
676
  return null;
586
677
  }
@@ -595,7 +686,9 @@ export async function verifyDelegatedAccessToken(secret: string, token: string,
595
686
  if (!constantTimeEqual(signature, expected)) {
596
687
  return null;
597
688
  }
598
- const payload = DelegatedAccessTokenPayload.safeParse(JSON.parse(base64UrlDecode(encodedPayload)));
689
+ const payload = DelegatedAccessTokenPayload.safeParse(
690
+ JSON.parse(base64UrlDecode(encodedPayload)),
691
+ );
599
692
  if (!payload.success || payload.data.exp < nowSeconds) {
600
693
  return null;
601
694
  }
@@ -623,13 +716,20 @@ export const EnrollmentBearerPayload = z.object({
623
716
  });
624
717
  export type EnrollmentBearerPayload = z.infer<typeof EnrollmentBearerPayload>;
625
718
 
626
- export async function signEnrollmentBearer(secret: string, payload: EnrollmentBearerPayload): Promise<string> {
719
+ export async function signEnrollmentBearer(
720
+ secret: string,
721
+ payload: EnrollmentBearerPayload,
722
+ ): Promise<string> {
627
723
  const encodedPayload = base64UrlEncode(JSON.stringify(EnrollmentBearerPayload.parse(payload)));
628
724
  const signature = await hmacSha256Base64Url(secret, encodedPayload);
629
725
  return `oge_${encodedPayload}.${signature}`;
630
726
  }
631
727
 
632
- export async function verifyEnrollmentBearer(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<EnrollmentBearerPayload | null> {
728
+ export async function verifyEnrollmentBearer(
729
+ secret: string,
730
+ token: string,
731
+ nowSeconds = Math.floor(Date.now() / 1000),
732
+ ): Promise<EnrollmentBearerPayload | null> {
633
733
  if (!token.startsWith("oge_")) {
634
734
  return null;
635
735
  }
@@ -679,7 +779,10 @@ export const EnrollTokenPayload = z.object({
679
779
  });
680
780
  export type EnrollTokenPayload = z.infer<typeof EnrollTokenPayload>;
681
781
 
682
- export async function signEnrollToken(secret: string, payload: EnrollTokenPayload): Promise<string> {
782
+ export async function signEnrollToken(
783
+ secret: string,
784
+ payload: EnrollTokenPayload,
785
+ ): Promise<string> {
683
786
  const encodedPayload = base64UrlEncode(JSON.stringify(EnrollTokenPayload.parse(payload)));
684
787
  const signature = await hmacSha256Base64Url(secret, encodedPayload);
685
788
  return `oget_${encodedPayload}.${signature}`;
@@ -693,7 +796,11 @@ export async function signEnrollToken(secret: string, payload: EnrollTokenPayloa
693
796
  * bearer fails the prefix gate; a same-secret token that lacks the typ claim fails
694
797
  * the schema gate — both halves of the domain separation are enforced here.
695
798
  */
696
- export async function verifyEnrollToken(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<EnrollTokenPayload | null> {
799
+ export async function verifyEnrollToken(
800
+ secret: string,
801
+ token: string,
802
+ nowSeconds = Math.floor(Date.now() / 1000),
803
+ ): Promise<EnrollTokenPayload | null> {
697
804
  if (!token.startsWith("oget_")) {
698
805
  return null;
699
806
  }
@@ -755,7 +862,10 @@ export const StreamTokenPayload = z.object({
755
862
  });
756
863
  export type StreamTokenPayload = z.infer<typeof StreamTokenPayload>;
757
864
 
758
- export async function signStreamToken(secret: string, payload: StreamTokenPayload): Promise<string> {
865
+ export async function signStreamToken(
866
+ secret: string,
867
+ payload: StreamTokenPayload,
868
+ ): Promise<string> {
759
869
  const encodedPayload = base64UrlEncode(JSON.stringify(StreamTokenPayload.parse(payload)));
760
870
  const signature = await hmacSha256Base64Url(secret, encodedPayload);
761
871
  return `ogs_${encodedPayload}.${signature}`;
@@ -771,7 +881,11 @@ export async function signStreamToken(secret: string, payload: StreamTokenPayloa
771
881
  * lease + route params — verify proves the token is authentic + unexpired, the
772
882
  * caller proves it is for THIS box's current epoch and THIS workspace+session.
773
883
  */
774
- export async function verifyStreamToken(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<StreamTokenPayload | null> {
884
+ export async function verifyStreamToken(
885
+ secret: string,
886
+ token: string,
887
+ nowSeconds = Math.floor(Date.now() / 1000),
888
+ ): Promise<StreamTokenPayload | null> {
775
889
  if (!token.startsWith("ogs_")) {
776
890
  return null;
777
891
  }
@@ -848,7 +962,11 @@ export async function signRelayToken(secret: string, payload: RelayTokenPayload)
848
962
  * The channel-key scope (claim.workspaceId/agentId vs the StreamOpen channel key)
849
963
  * is enforced by the relay at USE — verify proves authenticity + freshness only.
850
964
  */
851
- export async function verifyRelayToken(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<RelayTokenPayload | null> {
965
+ export async function verifyRelayToken(
966
+ secret: string,
967
+ token: string,
968
+ nowSeconds = Math.floor(Date.now() / 1000),
969
+ ): Promise<RelayTokenPayload | null> {
852
970
  if (!token.startsWith("ogr_")) {
853
971
  return null;
854
972
  }
@@ -1054,7 +1172,12 @@ export type LimitDecision = z.infer<typeof LimitDecision>;
1054
1172
  // the admitted quantity so a PULL host can grant a partial allowance.
1055
1173
  export const EntitlementDecision = z.discriminatedUnion("allowed", [
1056
1174
  z.object({ allowed: z.literal(true), quantity: z.number().optional() }),
1057
- z.object({ allowed: z.literal(false), reason: z.string(), code: z.string().optional(), quantity: z.number().optional() }),
1175
+ z.object({
1176
+ allowed: z.literal(false),
1177
+ reason: z.string(),
1178
+ code: z.string().optional(),
1179
+ quantity: z.number().optional(),
1180
+ }),
1058
1181
  ]);
1059
1182
  export type EntitlementDecision = z.infer<typeof EntitlementDecision>;
1060
1183
 
@@ -1069,16 +1192,32 @@ export type EntitlementsPort = {
1069
1192
  admitRun(input: AdmitRunInput): Promise<EntitlementDecision>;
1070
1193
  };
1071
1194
 
1195
+ export const GitCredentialProvider = z.enum(["github", "gitlab", "azure_devops"]);
1196
+ export type GitCredentialProvider = z.infer<typeof GitCredentialProvider>;
1197
+
1198
+ const GitProviderRepositoryId = z.union([z.number().int().positive(), z.string().min(1)]);
1199
+
1200
+ export const GitCredentialRepositoryRef = z.object({
1201
+ provider: GitCredentialProvider.optional(),
1202
+ uri: z.string().min(1),
1203
+ ref: z.string().min(1),
1204
+ repositoryId: GitProviderRepositoryId.optional(),
1205
+ installationId: GitProviderRepositoryId.optional(),
1206
+ projectId: GitProviderRepositoryId.optional(),
1207
+ connectionId: z.string().min(1).optional(),
1208
+ });
1209
+ export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryRef>;
1210
+
1072
1211
  // ============ P4a — Connection-credential provider (§7.6) ============
1073
1212
  //
1074
1213
  // The host-providable per-run credential-mint seam over OpenGeni's TWO
1075
1214
  // run-scoped credential sites in the worker:
1076
- // - GIT credentials: the GitHub App installation token minted in
1077
- // `sandboxEnvironmentForRun` (today `createGitHubAppInstallationToken`
1078
- // from `settings`) and injected as `GH_TOKEN`/`GITHUB_TOKEN`/the git
1079
- // extraheader.
1080
- // - SANDBOX secrets: the decrypted workspace environment values loaded in
1081
- // `loadWorkspaceEnvironmentForRun` (today decrypted with
1215
+ // - GIT credentials: run-scoped provider tokens minted in
1216
+ // `sandboxEnvironmentForRun` (standalone self-mints GitHub App tokens from
1217
+ // `settings`; embedded hosts can broker GitHub, GitLab, and Azure DevOps)
1218
+ // and seeded off-manifest into sandbox token files for git + provider CLIs.
1219
+ // - SANDBOX secrets: the decrypted variable set values loaded in
1220
+ // `loadVariableSetForRun` (today decrypted with
1082
1221
  // `environmentsEncryptionKeyBytes(settings)`).
1083
1222
  //
1084
1223
  // In embedded/separate topologies the HOST owns these external connections
@@ -1090,28 +1229,41 @@ export type EntitlementsPort = {
1090
1229
  // FORK-7 CROSS-CHECK (the host-mapping safety guardrail): a credential
1091
1230
  // provider returns the `workspaceId` it scoped the credential to, and the
1092
1231
  // activity ASSERTS it agrees with the run's workspace BEFORE injecting
1093
- // `GH_TOKEN` (or applying the decrypted values). A host mapping bug that
1232
+ // any git provider token seed (or applying decrypted environment values). A host mapping bug that
1094
1233
  // returns tenant B's creds while the run is tenant A is thereby caught at the
1095
1234
  // seam, never silently injected into tenant A's sandbox.
1096
1235
 
1097
1236
  export type GitCredentialsRequest = {
1098
1237
  accountId: string;
1099
1238
  workspaceId: string;
1100
- // The GitHub App installation the run's repository resources resolved to,
1101
- // and the specific repositories the token must be scoped to. Mirrors the
1102
- // shape `createGitHubAppInstallationToken` consumes today.
1239
+ // Provider defaults to "github" for the legacy request shape. GitHub-only
1240
+ // hosts can keep reading installationId/repositoryIds exactly as before;
1241
+ // provider-aware hosts should branch on this and repositoryRefs.
1242
+ provider?: GitCredentialProvider;
1243
+ // Token requests are the existing behavior. Identity requests let lazy
1244
+ // sandbox provisioning resolve stable git author/committer identity before
1245
+ // the box exists while deferring the rotating token value to first provision.
1246
+ purpose?: "token" | "identity";
1247
+ // Provider-neutral repository refs for hosts that broker non-GitHub tokens.
1248
+ // For GitHub requests these are additive to the legacy fields below.
1249
+ repositoryRefs?: GitCredentialRepositoryRef[];
1250
+ // Legacy GitHub App installation shape retained for 0.x compatibility.
1103
1251
  installationId: number;
1104
1252
  repositoryIds: number[];
1105
1253
  };
1106
1254
 
1107
1255
  export type GitCredentials = {
1108
- // The minted installation token the activity injects as GH_TOKEN/GITHUB_TOKEN
1109
- // and into the git http extraheader (identical downstream handling to the
1110
- // self-mint path).
1111
- token: string;
1256
+ // The minted provider token. Required for purpose="token"; optional for
1257
+ // purpose="identity" so hosts can return only stable git identity before lazy
1258
+ // sandbox provision. The value never enters the manifest.
1259
+ token?: string;
1112
1260
  // FORK-7 echo: the workspace the provider scoped this token to. The activity
1113
1261
  // asserts `workspaceId === request.workspaceId` before injecting.
1114
1262
  workspaceId: string;
1263
+ // Optional provider expiry for host-managed proactive renewal. ISO-8601;
1264
+ // null/omitted means the host does not expose a deadline and OpenGeni uses
1265
+ // its conservative bounded refresh cadence instead.
1266
+ expiresAt?: string | null;
1115
1267
  // Optional git identity override. When omitted the activity falls back to
1116
1268
  // today's `githubAppBotIdentity(settings)`.
1117
1269
  identity?: { name: string; email: string } | null;
@@ -1120,20 +1272,20 @@ export type GitCredentials = {
1120
1272
  export type SandboxSecretsRequest = {
1121
1273
  accountId: string;
1122
1274
  workspaceId: string;
1123
- // The workspace environment the run's session declares (null = unattached;
1275
+ // The variable set the run's session declares (null = unattached;
1124
1276
  // the provider, like the self-mint path, returns null values for it).
1125
- environmentId: string;
1277
+ variableSetId: string;
1126
1278
  };
1127
1279
 
1128
1280
  export type SandboxSecrets = {
1129
- // The decrypted environment values the run injects, replacing the local
1281
+ // The decrypted variableSet values the run injects, replacing the local
1130
1282
  // `environmentsEncryptionKeyBytes` decrypt. Same shape the self-mint path
1131
1283
  // produces (plaintext name→value).
1132
1284
  values: Record<string, string>;
1133
1285
  // FORK-7 echo: the workspace the provider scoped these secrets to.
1134
1286
  workspaceId: string;
1135
- // Optional environment metadata; when omitted the activity uses the
1136
- // environmentId as both id and name (the local decrypt carries the row's
1287
+ // Optional variableSet metadata; when omitted the activity uses the
1288
+ // variableSetId as both id and name (the local decrypt carries the row's
1137
1289
  // id/name/description, but only `id` is load-bearing downstream).
1138
1290
  id?: string;
1139
1291
  name?: string;
@@ -1144,8 +1296,8 @@ export type ConnectionCredentialsPort = {
1144
1296
  // Both legs are optional: a host may drive ONLY git creds (BYO-GitHub-App)
1145
1297
  // and leave sandbox secrets to OpenGeni's local decrypt, or vice-versa. An
1146
1298
  // unset leg falls through to today's self-mint for THAT leg only.
1147
- gitCredentials?: (input: GitCredentialsRequest) => Promise<GitCredentials>;
1148
- sandboxSecrets?: (input: SandboxSecretsRequest) => Promise<SandboxSecrets>;
1299
+ gitCredentials?(input: GitCredentialsRequest): Promise<GitCredentials>;
1300
+ sandboxSecrets?(input: SandboxSecretsRequest): Promise<SandboxSecrets>;
1149
1301
  };
1150
1302
 
1151
1303
  // ============ P4a — GitHub App API port (BYO-App, §7.6 / SPIKE-2 remainder) ===
@@ -1177,9 +1329,7 @@ export type GitHubAppApiPort = {
1177
1329
  code: string;
1178
1330
  installationId: number;
1179
1331
  }) => Promise<GitHubInstallationSummary>;
1180
- listRepositories?: (input: {
1181
- installationIds?: number[];
1182
- }) => Promise<GitHubRepository[]>;
1332
+ listRepositories?: (input: { installationIds?: number[] }) => Promise<GitHubRepository[]>;
1183
1333
  };
1184
1334
 
1185
1335
  export const BillingBalance = z.object({
@@ -1192,10 +1342,14 @@ export type BillingBalance = z.infer<typeof BillingBalance>;
1192
1342
 
1193
1343
  export const CreateCheckoutRequest = z.object({
1194
1344
  accountId: z.string().uuid().optional(),
1195
- amountUsd: z.number().min(5).max(10_000).refine(
1196
- (value) => Number.isFinite(value) && Math.abs(value - Math.round(value * 100) / 100) < 1e-9,
1197
- { message: "amountUsd must use cent precision" },
1198
- ),
1345
+ amountUsd: z
1346
+ .number()
1347
+ .min(5)
1348
+ .max(10_000)
1349
+ .refine(
1350
+ (value) => Number.isFinite(value) && Math.abs(value - Math.round(value * 100) / 100) < 1e-9,
1351
+ { message: "amountUsd must use cent precision" },
1352
+ ),
1199
1353
  successUrl: z.string().url().optional(),
1200
1354
  cancelUrl: z.string().url().optional(),
1201
1355
  });
@@ -1213,6 +1367,11 @@ export const RepositoryResourceRef = z.object({
1213
1367
  ref: z.string().min(1),
1214
1368
  mountPath: z.string().min(1).optional(),
1215
1369
  subpath: z.string().min(1).optional(),
1370
+ provider: GitCredentialProvider.optional(),
1371
+ repositoryId: GitProviderRepositoryId.optional(),
1372
+ installationId: GitProviderRepositoryId.optional(),
1373
+ projectId: GitProviderRepositoryId.optional(),
1374
+ connectionId: z.string().min(1).optional(),
1216
1375
  githubInstallationId: z.number().int().positive().optional(),
1217
1376
  githubRepositoryId: z.number().int().positive().optional(),
1218
1377
  });
@@ -1231,7 +1390,13 @@ export type ResourceRef = z.infer<typeof ResourceRef>;
1231
1390
  export const FileStatus = z.enum(["pending_upload", "ready", "failed", "expired", "deleted"]);
1232
1391
  export type FileStatus = z.infer<typeof FileStatus>;
1233
1392
 
1234
- export const FileUploadStatus = z.enum(["pending", "completed", "expired", "failed"]);
1393
+ export const FileUploadStatus = z.enum([
1394
+ "pending",
1395
+ "cleanup_pending",
1396
+ "completed",
1397
+ "expired",
1398
+ "failed",
1399
+ ]);
1235
1400
  export type FileUploadStatus = z.infer<typeof FileUploadStatus>;
1236
1401
 
1237
1402
  export const FileAsset = z.object({
@@ -1282,6 +1447,21 @@ export type FileDownloadUrlResponse = z.infer<typeof FileDownloadUrlResponse>;
1282
1447
  export const DocumentStatus = z.enum(["queued", "indexing", "ready", "failed"]);
1283
1448
  export type DocumentStatus = z.infer<typeof DocumentStatus>;
1284
1449
 
1450
+ export const KnowledgeSourceKind = z.enum([
1451
+ "manual_upload",
1452
+ "meeting_transcript",
1453
+ "repository",
1454
+ "email",
1455
+ "chat",
1456
+ "document",
1457
+ "web",
1458
+ "other",
1459
+ ]);
1460
+ export type KnowledgeSourceKind = z.infer<typeof KnowledgeSourceKind>;
1461
+
1462
+ export const DocumentSearchMode = z.enum(["hybrid", "vector", "keyword"]);
1463
+ export type DocumentSearchMode = z.infer<typeof DocumentSearchMode>;
1464
+
1285
1465
  export const DocumentBase = z.object({
1286
1466
  id: z.string().uuid(),
1287
1467
  workspaceId: z.string().uuid(),
@@ -1302,6 +1482,15 @@ export const Document = z.object({
1302
1482
  parser: z.string(),
1303
1483
  chunkCount: z.number().int().nonnegative(),
1304
1484
  error: z.string().nullable(),
1485
+ sourceKind: KnowledgeSourceKind,
1486
+ sourceUri: z.string().nullable(),
1487
+ sourceExternalId: z.string().nullable(),
1488
+ sourceTitle: z.string().nullable(),
1489
+ sourceAuthor: z.string().nullable(),
1490
+ sourceCreatedAt: z.string().nullable(),
1491
+ sourceUpdatedAt: z.string().nullable(),
1492
+ sourceVersion: z.string().nullable(),
1493
+ aclTags: z.array(z.string()),
1305
1494
  createdAt: z.string(),
1306
1495
  updatedAt: z.string(),
1307
1496
  });
@@ -1316,8 +1505,20 @@ export const DocumentSearchResult = z.object({
1316
1505
  title: z.string(),
1317
1506
  text: z.string(),
1318
1507
  score: z.number(),
1508
+ matchType: DocumentSearchMode,
1509
+ vectorScore: z.number().nullable(),
1510
+ keywordScore: z.number().nullable(),
1319
1511
  chunkIndex: z.number().int().nonnegative(),
1320
1512
  metadata: z.record(z.string(), z.unknown()),
1513
+ sourceKind: KnowledgeSourceKind,
1514
+ sourceUri: z.string().nullable(),
1515
+ sourceExternalId: z.string().nullable(),
1516
+ sourceTitle: z.string().nullable(),
1517
+ sourceAuthor: z.string().nullable(),
1518
+ sourceCreatedAt: z.string().nullable(),
1519
+ sourceUpdatedAt: z.string().nullable(),
1520
+ sourceVersion: z.string().nullable(),
1521
+ aclTags: z.array(z.string()),
1321
1522
  });
1322
1523
  export type DocumentSearchResult = z.infer<typeof DocumentSearchResult>;
1323
1524
 
@@ -1329,15 +1530,160 @@ export type CreateDocumentBaseRequest = z.infer<typeof CreateDocumentBaseRequest
1329
1530
 
1330
1531
  export const AddDocumentRequest = z.object({
1331
1532
  fileId: z.string().uuid(),
1533
+ title: z.string().min(1).optional(),
1534
+ sourceKind: KnowledgeSourceKind.optional(),
1535
+ sourceUri: z.string().min(1).optional(),
1536
+ sourceExternalId: z.string().min(1).optional(),
1537
+ sourceTitle: z.string().min(1).optional(),
1538
+ sourceAuthor: z.string().min(1).optional(),
1539
+ sourceCreatedAt: z.string().datetime({ offset: true }).optional(),
1540
+ sourceUpdatedAt: z.string().datetime({ offset: true }).optional(),
1541
+ sourceVersion: z.string().min(1).optional(),
1542
+ aclTags: z.array(z.string().min(1)).optional(),
1332
1543
  });
1333
1544
  export type AddDocumentRequest = z.infer<typeof AddDocumentRequest>;
1334
1545
 
1335
1546
  export const DocumentSearchRequest = z.object({
1336
1547
  query: z.string().min(1),
1337
- limit: z.number().int().positive().max(20).default(5),
1548
+ baseIds: z.array(z.string().uuid()).optional(),
1549
+ mode: DocumentSearchMode.optional(),
1550
+ sourceKinds: z.array(KnowledgeSourceKind).optional(),
1551
+ aclTags: z.array(z.string().min(1)).optional(),
1552
+ limit: z.number().int().positive().max(50).default(5),
1338
1553
  });
1339
1554
  export type DocumentSearchRequest = z.infer<typeof DocumentSearchRequest>;
1340
1555
 
1556
+ // proposed/approved/rejected are the legacy curated-knowledge review states
1557
+ // (docs-MCP memory_propose lane). active/superseded/archived are Workspace
1558
+ // Memory V1: agent-written memories land `active` (usable immediately — human is
1559
+ // auditor, not gatekeeper), get `superseded` when replaced, `archived` when
1560
+ // retired. Agent-visible set = active ∪ approved.
1561
+ export const KnowledgeMemoryStatus = z.enum([
1562
+ "proposed",
1563
+ "approved",
1564
+ "rejected",
1565
+ "active",
1566
+ "superseded",
1567
+ "archived",
1568
+ ]);
1569
+ export type KnowledgeMemoryStatus = z.infer<typeof KnowledgeMemoryStatus>;
1570
+
1571
+ export const KnowledgeMemoryKind = z.enum([
1572
+ "semantic",
1573
+ "episodic",
1574
+ "procedural",
1575
+ "decision",
1576
+ "preference",
1577
+ ]);
1578
+ export type KnowledgeMemoryKind = z.infer<typeof KnowledgeMemoryKind>;
1579
+
1580
+ export const KnowledgeSourceRef = z.object({
1581
+ kind: z.enum(["document_chunk", "document", "session_event", "memory", "external"]),
1582
+ id: z.string().min(1),
1583
+ uri: z.string().min(1).optional(),
1584
+ title: z.string().min(1).optional(),
1585
+ metadata: z.record(z.string(), z.unknown()).default({}),
1586
+ });
1587
+ export type KnowledgeSourceRef = z.infer<typeof KnowledgeSourceRef>;
1588
+
1589
+ export const KnowledgeMemory = z.object({
1590
+ id: z.string().uuid(),
1591
+ workspaceId: z.string().uuid(),
1592
+ status: KnowledgeMemoryStatus,
1593
+ kind: KnowledgeMemoryKind,
1594
+ scope: z.string(),
1595
+ text: z.string(),
1596
+ sourceRefs: z.array(KnowledgeSourceRef),
1597
+ confidence: z.number().min(0).max(1),
1598
+ metadata: z.record(z.string(), z.unknown()),
1599
+ createdBySessionId: z.string().uuid().nullable(),
1600
+ reviewedBy: z.string().nullable(),
1601
+ reviewedAt: z.string().nullable(),
1602
+ // Workspace Memory V1 fields. usageCount/lastUsedAt feed end-state ranking and
1603
+ // decay; supersedesId/supersededById link correction chains; validFrom/validUntil
1604
+ // are the point-in-time window. embedding/embeddingModel/textHash are internal
1605
+ // and never exposed on the wire.
1606
+ pinned: z.boolean(),
1607
+ usageCount: z.number().int(),
1608
+ lastUsedAt: z.string().nullable(),
1609
+ supersedesId: z.string().uuid().nullable(),
1610
+ supersededById: z.string().uuid().nullable(),
1611
+ validFrom: z.string(),
1612
+ validUntil: z.string().nullable(),
1613
+ createdAt: z.string(),
1614
+ updatedAt: z.string(),
1615
+ });
1616
+ export type KnowledgeMemory = z.infer<typeof KnowledgeMemory>;
1617
+
1618
+ // Default status is `active`: a create through this request lands an
1619
+ // agent-visible memory via the one write gate (saveWorkspaceMemory). Passing an
1620
+ // explicit `proposed`/`approved`/`rejected` status routes to the legacy curated
1621
+ // create instead (the docs-MCP memory_propose lane). pinned/replacesId apply to
1622
+ // the active (memory) path.
1623
+ export const CreateKnowledgeMemoryRequest = z.object({
1624
+ status: KnowledgeMemoryStatus.default("active"),
1625
+ kind: KnowledgeMemoryKind.default("semantic"),
1626
+ scope: z.string().min(1).default("workspace"),
1627
+ text: z.string().min(1),
1628
+ sourceRefs: z.array(KnowledgeSourceRef).default([]),
1629
+ confidence: z.number().min(0).max(1).default(0.5),
1630
+ metadata: z.record(z.string(), z.unknown()).default({}),
1631
+ createdBySessionId: z.string().uuid().optional(),
1632
+ pinned: z.boolean().optional(),
1633
+ replacesId: z.string().min(1).optional(),
1634
+ });
1635
+ export type CreateKnowledgeMemoryRequest = z.infer<typeof CreateKnowledgeMemoryRequest>;
1636
+
1637
+ export const UpdateKnowledgeMemoryRequest = z.object({
1638
+ status: KnowledgeMemoryStatus.optional(),
1639
+ kind: KnowledgeMemoryKind.optional(),
1640
+ scope: z.string().min(1).optional(),
1641
+ text: z.string().min(1).optional(),
1642
+ sourceRefs: z.array(KnowledgeSourceRef).optional(),
1643
+ confidence: z.number().min(0).max(1).optional(),
1644
+ metadata: z.record(z.string(), z.unknown()).optional(),
1645
+ reviewedBy: z.string().min(1).optional(),
1646
+ // Human audit action: pin (never decays) / unpin.
1647
+ pinned: z.boolean().optional(),
1648
+ });
1649
+ export type UpdateKnowledgeMemoryRequest = z.infer<typeof UpdateKnowledgeMemoryRequest>;
1650
+
1651
+ // GET list/filter over knowledge memories (curated + memory).
1652
+ export const KnowledgeMemorySearchRequest = z.object({
1653
+ query: z.string().min(1).optional(),
1654
+ status: KnowledgeMemoryStatus.optional(),
1655
+ kind: KnowledgeMemoryKind.optional(),
1656
+ scope: z.string().min(1).optional(),
1657
+ limit: z.number().int().positive().max(100).default(20),
1658
+ });
1659
+ export type KnowledgeMemorySearchRequest = z.infer<typeof KnowledgeMemorySearchRequest>;
1660
+
1661
+ export const WorkspaceMemorySearchMode = z.enum(["hybrid", "vector", "keyword"]);
1662
+ export type WorkspaceMemorySearchMode = z.infer<typeof WorkspaceMemorySearchMode>;
1663
+
1664
+ // POST hybrid search over the workspace's agent-visible memory (active ∪ approved).
1665
+ export const WorkspaceMemorySearchRequest = z.object({
1666
+ query: z.string().min(1),
1667
+ kind: KnowledgeMemoryKind.optional(),
1668
+ limit: z.number().int().positive().max(20).optional(),
1669
+ mode: WorkspaceMemorySearchMode.optional(),
1670
+ });
1671
+ export type WorkspaceMemorySearchRequest = z.infer<typeof WorkspaceMemorySearchRequest>;
1672
+
1673
+ export const WorkspaceMemorySearchResult = z.object({
1674
+ memory: KnowledgeMemory,
1675
+ score: z.number(),
1676
+ matchType: WorkspaceMemorySearchMode,
1677
+ vectorScore: z.number().nullable(),
1678
+ keywordScore: z.number().nullable(),
1679
+ });
1680
+ export type WorkspaceMemorySearchResult = z.infer<typeof WorkspaceMemorySearchResult>;
1681
+
1682
+ export const WorkspaceMemorySearchResponse = z.object({
1683
+ results: z.array(WorkspaceMemorySearchResult),
1684
+ });
1685
+ export type WorkspaceMemorySearchResponse = z.infer<typeof WorkspaceMemorySearchResponse>;
1686
+
1341
1687
  export const ToolRef = z.object({
1342
1688
  kind: z.literal("mcp"),
1343
1689
  id: z.string().min(1),
@@ -1352,13 +1698,19 @@ export const ToolRef = z.object({
1352
1698
  export type ToolRef = z.infer<typeof ToolRef>;
1353
1699
 
1354
1700
  const registryId = /^[A-Za-z0-9_-]+$/;
1355
- const httpsUrl = z.string().url().refine((value) => {
1356
- try {
1357
- return new URL(value).protocol === "https:";
1358
- } catch {
1359
- return false;
1360
- }
1361
- }, { message: "URL must use https" });
1701
+ const httpsUrl = z
1702
+ .string()
1703
+ .url()
1704
+ .refine(
1705
+ (value) => {
1706
+ try {
1707
+ return new URL(value).protocol === "https:";
1708
+ } catch {
1709
+ return false;
1710
+ }
1711
+ },
1712
+ { message: "URL must use https" },
1713
+ );
1362
1714
 
1363
1715
  export const SessionMcpServerInput = z.object({
1364
1716
  id: z.string().min(1).regex(registryId),
@@ -1367,6 +1719,12 @@ export const SessionMcpServerInput = z.object({
1367
1719
  allowedTools: z.array(z.string().min(1)).optional(),
1368
1720
  timeoutMs: z.number().int().positive().optional(),
1369
1721
  cacheToolsList: z.boolean().optional(),
1722
+ // Human-approval policy for this server's tools. `true` = every tool of this
1723
+ // server requires approval before it runs (a `session.requiresAction` pause
1724
+ // the caller resolves with `user.approvalDecision`); a string[] = ONLY the
1725
+ // listed UNPREFIXED tool names require approval (e.g. reads auto-run, writes
1726
+ // ask); absent / `false` = auto-run everything (the historical default).
1727
+ requireApproval: z.union([z.boolean(), z.array(z.string().min(1))]).optional(),
1370
1728
  // Write-only credential headers. Values are encrypted at rest and never
1371
1729
  // returned in session responses or events; response metadata exposes names.
1372
1730
  headers: z.record(z.string(), z.string()).optional(),
@@ -1379,13 +1737,15 @@ export const SessionMcpCredentialUpdateInput = z.object({
1379
1737
  });
1380
1738
  export type SessionMcpCredentialUpdateInput = z.infer<typeof SessionMcpCredentialUpdateInput>;
1381
1739
 
1382
- export const SessionMcpServerMetadata = z.object({
1383
- id: z.string().min(1).regex(registryId),
1384
- name: z.string().min(1).nullable(),
1385
- url: httpsUrl,
1386
- headerNames: z.array(z.string()).default([]),
1387
- credentialVersion: z.number().int().positive(),
1388
- }).strict();
1740
+ export const SessionMcpServerMetadata = z
1741
+ .object({
1742
+ id: z.string().min(1).regex(registryId),
1743
+ name: z.string().min(1).nullable(),
1744
+ url: httpsUrl,
1745
+ headerNames: z.array(z.string()).default([]),
1746
+ credentialVersion: z.number().int().positive(),
1747
+ })
1748
+ .strict();
1389
1749
  export type SessionMcpServerMetadata = z.infer<typeof SessionMcpServerMetadata>;
1390
1750
 
1391
1751
  export class ResourceRefConflictError extends Error {
@@ -1424,8 +1784,14 @@ export function mergeResourceRefs(
1424
1784
  options: { rejectConflicts?: boolean } = {},
1425
1785
  ): ResourceRef[] {
1426
1786
  const out = [...existing];
1427
- const mountPaths = new Map(existing.flatMap((resource) => resource.mountPath ? [[resource.mountPath, stableJson(resource)] as const] : []));
1428
- const identities = new Map(existing.map((resource) => [resourceIdentityKey(resource), stableJson(resource)] as const));
1787
+ const mountPaths = new Map(
1788
+ existing.flatMap((resource) =>
1789
+ resource.mountPath ? [[resource.mountPath, stableJson(resource)] as const] : [],
1790
+ ),
1791
+ );
1792
+ const identities = new Map(
1793
+ existing.map((resource) => [resourceIdentityKey(resource), stableJson(resource)] as const),
1794
+ );
1429
1795
  const exact = new Set(existing.map(stableJson));
1430
1796
 
1431
1797
  for (const resource of additions) {
@@ -1436,12 +1802,16 @@ export function mergeResourceRefs(
1436
1802
  if (options.rejectConflicts) {
1437
1803
  const existingAtMount = resource.mountPath ? mountPaths.get(resource.mountPath) : undefined;
1438
1804
  if (existingAtMount && existingAtMount !== serialized) {
1439
- throw new ResourceRefConflictError(`resource mount path is already attached: ${resource.mountPath}`);
1805
+ throw new ResourceRefConflictError(
1806
+ `resource mount path is already attached: ${resource.mountPath}`,
1807
+ );
1440
1808
  }
1441
1809
  const identity = resourceIdentityKey(resource);
1442
1810
  const existingIdentity = identities.get(identity);
1443
1811
  if (existingIdentity && existingIdentity !== serialized) {
1444
- throw new ResourceRefConflictError(`resource is already attached with different settings: ${identity}`);
1812
+ throw new ResourceRefConflictError(
1813
+ `resource is already attached with different settings: ${identity}`,
1814
+ );
1445
1815
  }
1446
1816
  }
1447
1817
  out.push(resource);
@@ -1454,9 +1824,17 @@ export function mergeResourceRefs(
1454
1824
  return out;
1455
1825
  }
1456
1826
 
1457
- export function reasoningEffortForMetadata(metadata: Record<string, unknown>, fallback: ReasoningEffort): ReasoningEffort {
1827
+ export function reasoningEffortForMetadata(
1828
+ metadata: Record<string, unknown>,
1829
+ fallback: ReasoningEffort,
1830
+ ): ReasoningEffort {
1458
1831
  const value = metadata.reasoningEffort;
1459
- return value === "none" || value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh"
1832
+ return value === "none" ||
1833
+ value === "minimal" ||
1834
+ value === "low" ||
1835
+ value === "medium" ||
1836
+ value === "high" ||
1837
+ value === "xhigh"
1460
1838
  ? value
1461
1839
  : fallback;
1462
1840
  }
@@ -1477,17 +1855,44 @@ function sortJson(value: unknown): unknown {
1477
1855
  return value.map(sortJson);
1478
1856
  }
1479
1857
  if (value && typeof value === "object") {
1480
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => [key, sortJson(nested)]));
1858
+ return Object.fromEntries(
1859
+ Object.entries(value)
1860
+ .sort(([a], [b]) => a.localeCompare(b))
1861
+ .map(([key, nested]) => [key, sortJson(nested)]),
1862
+ );
1481
1863
  }
1482
1864
  return value;
1483
1865
  }
1484
1866
 
1485
- export const SessionTurnStatus = z.enum(["queued", "running", "requires_action", "completed", "failed", "cancelled"]);
1867
+ export const SessionTurnStatus = z.enum([
1868
+ "queued",
1869
+ "running",
1870
+ "requires_action",
1871
+ "recovering",
1872
+ "waiting_capacity",
1873
+ "completed",
1874
+ "failed",
1875
+ "cancelled",
1876
+ "superseded",
1877
+ ]);
1486
1878
  export type SessionTurnStatus = z.infer<typeof SessionTurnStatus>;
1487
1879
 
1488
- export const SessionTurnSource = z.enum(["user", "scheduled_task", "api", "goal"]);
1880
+ export const SessionTurnSource = z.enum([
1881
+ "user",
1882
+ "scheduled_task",
1883
+ "api",
1884
+ "goal",
1885
+ "system",
1886
+ "compaction",
1887
+ ]);
1489
1888
  export type SessionTurnSource = z.infer<typeof SessionTurnSource>;
1490
1889
 
1890
+ export const SessionControlState = z.enum(["active", "paused"]);
1891
+ export type SessionControlState = z.infer<typeof SessionControlState>;
1892
+
1893
+ export const WorkspaceInferenceState = z.enum(["active", "paused"]);
1894
+ export type WorkspaceInferenceState = z.infer<typeof WorkspaceInferenceState>;
1895
+
1491
1896
  export const SessionGoalStatus = z.enum(["active", "paused", "completed"]);
1492
1897
  export type SessionGoalStatus = z.infer<typeof SessionGoalStatus>;
1493
1898
 
@@ -1496,7 +1901,7 @@ export type SessionGoalCreatedBy = z.infer<typeof SessionGoalCreatedBy>;
1496
1901
 
1497
1902
  export const SessionGoalPausedReason = z.enum([
1498
1903
  "agent",
1499
- "user_interrupt",
1904
+ "user_pause",
1500
1905
  "api",
1501
1906
  "no_progress",
1502
1907
  "max_auto_continuations",
@@ -1544,6 +1949,18 @@ export const UpdateSessionRequest = z.object({
1544
1949
  });
1545
1950
  export type UpdateSessionRequest = z.infer<typeof UpdateSessionRequest>;
1546
1951
 
1952
+ /**
1953
+ * A member's personal pin preference for a session. `expectedVersion` is
1954
+ * optional: ordinary pin/unpin actions are idempotent last-write-wins, while a
1955
+ * client that has a known version can fail closed rather than overwrite a newer
1956
+ * action from another browser.
1957
+ */
1958
+ export const UpdateSessionPinRequest = z.object({
1959
+ pinned: z.boolean(),
1960
+ expectedVersion: z.number().int().nonnegative().optional(),
1961
+ });
1962
+ export type UpdateSessionPinRequest = z.infer<typeof UpdateSessionPinRequest>;
1963
+
1547
1964
  // Operator context controls (slash-command palette: /clear, /compact). These
1548
1965
  // are session/operator actions, NOT a structured way to talk to the agent —
1549
1966
  // the human↔agent channel stays plain chat. Both require `sessions:control`.
@@ -1586,9 +2003,11 @@ export function isClearedRunStateBlob(serialized: string | null | undefined): bo
1586
2003
  }
1587
2004
  try {
1588
2005
  const parsed = JSON.parse(serialized) as unknown;
1589
- return typeof parsed === "object"
1590
- && parsed !== null
1591
- && (parsed as Record<string, unknown>)[CLEARED_RUN_STATE_MARKER] === true;
2006
+ return (
2007
+ typeof parsed === "object" &&
2008
+ parsed !== null &&
2009
+ (parsed as Record<string, unknown>)[CLEARED_RUN_STATE_MARKER] === true
2010
+ );
1592
2011
  } catch {
1593
2012
  return false;
1594
2013
  }
@@ -1600,9 +2019,10 @@ export type CompactSessionContextRequest = z.infer<typeof CompactSessionContextR
1600
2019
 
1601
2020
  /** Outcome of a manual /compact trigger. */
1602
2021
  export const CompactSessionContextResult = z.object({
1603
- // queued: a client-side (Azure) compaction will run before the next turn.
1604
- // noop: nothing to do (server-managed provider, mode off, or no history).
1605
- status: z.enum(["queued", "noop"]),
2022
+ // pending: an active/paused session will compact at its next safe boundary.
2023
+ // completed: an idle compaction-only activity completed synchronously.
2024
+ // noop: there is no active history to compact.
2025
+ status: z.enum(["pending", "completed", "noop"]),
1606
2026
  message: z.string(),
1607
2027
  });
1608
2028
  export type CompactSessionContextResult = z.infer<typeof CompactSessionContextResult>;
@@ -1615,7 +2035,7 @@ export const SessionTurn = z.object({
1615
2035
  temporalWorkflowId: z.string(),
1616
2036
  status: SessionTurnStatus,
1617
2037
  source: SessionTurnSource,
1618
- position: z.number().int().positive(),
2038
+ position: z.number().int(),
1619
2039
  prompt: z.string().min(1),
1620
2040
  resources: z.array(ResourceRef),
1621
2041
  tools: z.array(ToolRef),
@@ -1625,6 +2045,12 @@ export const SessionTurn = z.object({
1625
2045
  // Per-turn OS override. NULL = inherit the session's sandboxOs.
1626
2046
  sandboxOs: SandboxOs.nullable(),
1627
2047
  metadata: z.record(z.string(), z.unknown()),
2048
+ version: z.number().int().positive(),
2049
+ executionGeneration: z.number().int().nonnegative(),
2050
+ activeAttemptId: z.string().uuid().nullable(),
2051
+ lineage: z.record(z.string(), z.unknown()),
2052
+ cancelledBy: z.string().nullable(),
2053
+ cancelReason: z.string().nullable(),
1628
2054
  startedAt: z.string().nullable(),
1629
2055
  finishedAt: z.string().nullable(),
1630
2056
  createdAt: z.string(),
@@ -1632,68 +2058,316 @@ export const SessionTurn = z.object({
1632
2058
  });
1633
2059
  export type SessionTurn = z.infer<typeof SessionTurn>;
1634
2060
 
1635
- export const UpdateSessionTurnRequest = z.object({
1636
- prompt: z.string().min(1).optional(),
1637
- resources: z.array(ResourceRef).optional(),
1638
- tools: z.array(ToolRef).optional(),
1639
- model: z.string().min(1).optional(),
1640
- reasoningEffort: ReasoningEffort.optional(),
1641
- sandboxBackend: SandboxBackend.optional(),
1642
- metadata: z.record(z.string(), z.unknown()).optional(),
2061
+ export const SessionQueueSnapshot = z.object({
2062
+ version: z.number().int().nonnegative(),
2063
+ controlState: SessionControlState,
2064
+ controlGeneration: z.number().int().nonnegative(),
2065
+ workspaceInferenceState: WorkspaceInferenceState,
2066
+ workspaceInferenceGeneration: z.number().int().nonnegative(),
2067
+ workspaceRunExceptionGeneration: z.number().int().nonnegative().nullable(),
2068
+ items: z.array(SessionTurn),
1643
2069
  });
1644
- export type UpdateSessionTurnRequest = z.infer<typeof UpdateSessionTurnRequest>;
2070
+ export type SessionQueueSnapshot = z.infer<typeof SessionQueueSnapshot>;
1645
2071
 
1646
- export const ReorderSessionTurnsRequest = z.object({
1647
- turnIds: z.array(z.string().uuid()).min(1),
2072
+ export const CancelSessionQueueItemRequest = z.object({
2073
+ expectedQueueVersion: z.number().int().nonnegative(),
2074
+ expectedItemVersion: z.number().int().positive(),
2075
+ reason: z.string().min(1).optional(),
1648
2076
  });
1649
- export type ReorderSessionTurnsRequest = z.infer<typeof ReorderSessionTurnsRequest>;
2077
+ export type CancelSessionQueueItemRequest = z.infer<typeof CancelSessionQueueItemRequest>;
1650
2078
 
1651
- export const WorkspaceEnvironmentVariableName = z.string().regex(/^[A-Z][A-Z0-9_]*$/).max(128);
1652
- export type WorkspaceEnvironmentVariableName = z.infer<typeof WorkspaceEnvironmentVariableName>;
2079
+ export const SessionControlRequest = z.object({
2080
+ mode: z.enum(["pause", "resume"]),
2081
+ reason: z.string().min(1).optional(),
2082
+ clientEventId: z.string().min(1).optional(),
2083
+ expectedControlState: SessionControlState.optional(),
2084
+ expectedControlGeneration: z.number().int().nonnegative().optional(),
2085
+ expectedWorkspaceInferenceGeneration: z.number().int().nonnegative().optional(),
2086
+ });
2087
+ export type SessionControlRequest = z.infer<typeof SessionControlRequest>;
2088
+
2089
+ export const WorkspaceInferenceControlRequest = z.object({
2090
+ state: WorkspaceInferenceState,
2091
+ reason: z.string().min(1),
2092
+ clientEventId: z.string().min(1),
2093
+ expectedState: WorkspaceInferenceState,
2094
+ expectedGeneration: z.number().int().nonnegative(),
2095
+ exceptSessionIds: z.array(z.string().uuid()).default([]),
2096
+ });
2097
+ export type WorkspaceInferenceControlRequest = z.infer<typeof WorkspaceInferenceControlRequest>;
2098
+
2099
+ export const WorkspaceInferenceControlResponse = z.object({
2100
+ operationId: z.string().uuid(),
2101
+ state: WorkspaceInferenceState,
2102
+ generation: z.number().int().nonnegative(),
2103
+ affectedSessionIds: z.array(z.string().uuid()),
2104
+ controlSessionIds: z.array(z.string().uuid()),
2105
+ exceptionSessionIds: z.array(z.string().uuid()),
2106
+ });
2107
+ export type WorkspaceInferenceControlResponse = z.infer<typeof WorkspaceInferenceControlResponse>;
2108
+
2109
+ export const SystemUpdateClassification = z.enum(["success", "failure", "action_required", "info"]);
2110
+ export type SystemUpdateClassification = z.infer<typeof SystemUpdateClassification>;
2111
+
2112
+ export const SessionSystemUpdateKind = z.enum([
2113
+ "child_session_update",
2114
+ "scheduled_wake",
2115
+ "lifecycle_event",
2116
+ "runtime_notice",
2117
+ ]);
2118
+ export type SessionSystemUpdateKind = z.infer<typeof SessionSystemUpdateKind>;
2119
+
2120
+ export const SessionSystemUpdateState = z.enum([
2121
+ "pending",
2122
+ "deferred",
2123
+ "delivered",
2124
+ "cancelled",
2125
+ "failed",
2126
+ ]);
2127
+ export type SessionSystemUpdateState = z.infer<typeof SessionSystemUpdateState>;
2128
+
2129
+ export const SessionSystemUpdate = z.object({
2130
+ id: z.string().uuid(),
2131
+ sessionId: z.string().uuid(),
2132
+ kind: SessionSystemUpdateKind,
2133
+ classification: SystemUpdateClassification,
2134
+ sourceId: z.string(),
2135
+ dedupeKey: z.string(),
2136
+ summary: z.string(),
2137
+ payload: z.record(z.string(), z.unknown()),
2138
+ lineage: z.record(z.string(), z.unknown()),
2139
+ state: SessionSystemUpdateState,
2140
+ deliveredTurnId: z.string().uuid().nullable(),
2141
+ deliveredAt: z.string().nullable(),
2142
+ createdAt: z.string(),
2143
+ });
2144
+ export type SessionSystemUpdate = z.infer<typeof SessionSystemUpdate>;
2145
+
2146
+ export const VariableSetVariableName = z
2147
+ .string()
2148
+ .regex(/^[A-Z][A-Z0-9_]*$/)
2149
+ .max(128);
2150
+ export type VariableSetVariableName = z.infer<typeof VariableSetVariableName>;
2151
+
2152
+ function withVariableSetIdAlias<T extends z.ZodRawShape>(shape: T) {
2153
+ return z.preprocess((input) => {
2154
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
2155
+ return input;
2156
+ }
2157
+ const record = input as Record<string, unknown>;
2158
+ if (record.variableSetId !== undefined || record.environmentId === undefined) {
2159
+ return record;
2160
+ }
2161
+ return { ...record, variableSetId: record.environmentId };
2162
+ }, z.object(shape));
2163
+ }
1653
2164
 
1654
2165
  // Metadata only by design: no schema in this file ever carries a variable value
1655
2166
  // back to a client. Values are write-only and decrypted exclusively inside the
1656
2167
  // worker at sandbox materialization time.
1657
- export const WorkspaceEnvironmentVariableMetadata = z.object({
1658
- name: WorkspaceEnvironmentVariableName,
2168
+ export const VariableSetVariableMetadata = z.object({
2169
+ name: VariableSetVariableName,
1659
2170
  version: z.number().int().positive(),
1660
2171
  createdAt: z.string(),
1661
2172
  updatedAt: z.string(),
1662
2173
  });
1663
- export type WorkspaceEnvironmentVariableMetadata = z.infer<typeof WorkspaceEnvironmentVariableMetadata>;
2174
+ export type VariableSetVariableMetadata = z.infer<typeof VariableSetVariableMetadata>;
2175
+ /** @deprecated use VariableSetVariableMetadata */
2176
+ export const WorkspaceEnvironmentVariableMetadata = VariableSetVariableMetadata;
2177
+ /** @deprecated use VariableSetVariableMetadata */
2178
+ export type WorkspaceEnvironmentVariableMetadata = VariableSetVariableMetadata;
1664
2179
 
1665
- export const WorkspaceEnvironment = z.object({
2180
+ export const VariableSet = z.object({
1666
2181
  id: z.string().uuid(),
1667
2182
  accountId: z.string().uuid(),
1668
2183
  workspaceId: z.string().uuid(),
1669
2184
  name: z.string(),
1670
2185
  description: z.string().nullable(),
1671
- variables: z.array(WorkspaceEnvironmentVariableMetadata),
2186
+ variables: z.array(VariableSetVariableMetadata),
1672
2187
  createdAt: z.string(),
1673
2188
  updatedAt: z.string(),
1674
2189
  });
1675
- export type WorkspaceEnvironment = z.infer<typeof WorkspaceEnvironment>;
2190
+ export type VariableSet = z.infer<typeof VariableSet>;
2191
+ /** @deprecated use VariableSet */
2192
+ export const WorkspaceEnvironment = VariableSet;
2193
+ /** @deprecated use VariableSet */
2194
+ export type WorkspaceEnvironment = VariableSet;
1676
2195
 
1677
- export const CreateWorkspaceEnvironmentRequest = z.object({
2196
+ export const CreateVariableSetRequest = z.object({
1678
2197
  name: z.string().min(1).max(120),
1679
2198
  description: z.string().max(2000).optional(),
1680
- variables: z.array(z.object({
1681
- name: WorkspaceEnvironmentVariableName,
1682
- value: z.string().min(1).max(32768),
1683
- })).default([]),
2199
+ variables: z
2200
+ .array(
2201
+ z.object({
2202
+ name: VariableSetVariableName,
2203
+ value: z.string().min(1).max(32768),
2204
+ }),
2205
+ )
2206
+ .default([]),
2207
+ });
2208
+ export type CreateVariableSetRequest = z.infer<typeof CreateVariableSetRequest>;
2209
+ /** @deprecated use CreateVariableSetRequest */
2210
+ export const CreateWorkspaceEnvironmentRequest = CreateVariableSetRequest;
2211
+ /** @deprecated use CreateVariableSetRequest */
2212
+ export type CreateWorkspaceEnvironmentRequest = CreateVariableSetRequest;
2213
+
2214
+ export const UpdateVariableSetRequest = z.object({
2215
+ name: z.string().min(1).max(120).optional(),
2216
+ description: z.string().max(2000).nullable().optional(),
2217
+ });
2218
+ export type UpdateVariableSetRequest = z.infer<typeof UpdateVariableSetRequest>;
2219
+ /** @deprecated use UpdateVariableSetRequest */
2220
+ export const UpdateWorkspaceEnvironmentRequest = UpdateVariableSetRequest;
2221
+ /** @deprecated use UpdateVariableSetRequest */
2222
+ export type UpdateWorkspaceEnvironmentRequest = UpdateVariableSetRequest;
2223
+
2224
+ export const SetVariableSetVariableRequest = z.object({
2225
+ value: z.string().min(1).max(32768),
2226
+ });
2227
+ export type SetVariableSetVariableRequest = z.infer<typeof SetVariableSetVariableRequest>;
2228
+ /** @deprecated use SetVariableSetVariableRequest */
2229
+ export const SetWorkspaceEnvironmentVariableRequest = SetVariableSetVariableRequest;
2230
+ /** @deprecated use SetVariableSetVariableRequest */
2231
+ export type SetWorkspaceEnvironmentVariableRequest = SetVariableSetVariableRequest;
2232
+
2233
+ // --- Rigs ---------------------------------------------------------------------
2234
+ // Workspace-scoped, versioned sandbox machine definitions. A rig is the named
2235
+ // truth; each sandbox is a disposable fork of a rig version. Versions are
2236
+ // append-only and content-immutable; exactly one is active per rig.
2237
+
2238
+ // A self-declared health check: a name + the shell command that must exit 0.
2239
+ export const RigCheck = z.object({
2240
+ name: z.string().min(1).max(120),
2241
+ command: z.string().min(1).max(8192),
1684
2242
  });
1685
- export type CreateWorkspaceEnvironmentRequest = z.infer<typeof CreateWorkspaceEnvironmentRequest>;
2243
+ export type RigCheck = z.infer<typeof RigCheck>;
1686
2244
 
1687
- export const UpdateWorkspaceEnvironmentRequest = z.object({
2245
+ export const RigVersion = z.object({
2246
+ id: z.string().uuid(),
2247
+ rigId: z.string().uuid(),
2248
+ version: z.number().int().positive(),
2249
+ image: z.string().nullable(),
2250
+ setupScript: z.string().nullable(),
2251
+ checks: z.array(RigCheck),
2252
+ credentialHooks: z.array(z.string()),
2253
+ defaultVariableSetIds: z.array(z.string().uuid()),
2254
+ changelog: z.string().nullable(),
2255
+ // Attribution: 'user:<subject>' | 'session:<id>' | 'system'.
2256
+ createdBy: z.string().nullable(),
2257
+ active: z.boolean(),
2258
+ createdAt: z.string(),
2259
+ });
2260
+ export type RigVersion = z.infer<typeof RigVersion>;
2261
+
2262
+ export const RigVerificationHealth = z.object({
2263
+ checkHealth: z.enum(["passing", "failing", "unknown"]),
2264
+ lastVerifiedAt: z.string().nullable(),
2265
+ });
2266
+ export type RigVerificationHealth = z.infer<typeof RigVerificationHealth>;
2267
+
2268
+ export const Rig = z.object({
2269
+ id: z.string().uuid(),
2270
+ accountId: z.string().uuid(),
2271
+ workspaceId: z.string().uuid(),
2272
+ name: z.string(),
2273
+ description: z.string().nullable(),
2274
+ createdBy: z.string().nullable(),
2275
+ // The rig's currently-active version (present after create; nullable so a
2276
+ // partial/list read can omit it without a schema change).
2277
+ activeVersion: RigVersion.nullable(),
2278
+ // Summary for the currently active version. null only when there is no active
2279
+ // version; otherwise "unknown" means the active version has no verification.
2280
+ activeVersionHealth: RigVerificationHealth.nullable(),
2281
+ versionCount: z.number().int().nonnegative(),
2282
+ createdAt: z.string(),
2283
+ updatedAt: z.string(),
2284
+ });
2285
+ export type Rig = z.infer<typeof Rig>;
2286
+
2287
+ export const RigChangeKind = z.enum(["setup_append", "definition_edit"]);
2288
+ export type RigChangeKind = z.infer<typeof RigChangeKind>;
2289
+
2290
+ export const RigChangeStatus = z.enum(["proposed", "verifying", "merged", "rejected", "failed"]);
2291
+ export type RigChangeStatus = z.infer<typeof RigChangeStatus>;
2292
+
2293
+ // A single check's outcome inside a verification run (populated in M4).
2294
+ export const RigCheckResult = z.object({
2295
+ name: z.string(),
2296
+ command: z.string(),
2297
+ exitCode: z.number().int().nullable(),
2298
+ output: z.string().optional(),
2299
+ });
2300
+ export type RigCheckResult = z.infer<typeof RigCheckResult>;
2301
+
2302
+ // The verification record a rig-CI run writes onto a change (M4). Open-ended
2303
+ // (passthrough) so M4 can enrich it without a contracts break.
2304
+ export const RigChangeVerification = z
2305
+ .object({
2306
+ startedAt: z.string().optional(),
2307
+ finishedAt: z.string().optional(),
2308
+ log: z.string().optional(),
2309
+ checkResults: z.array(RigCheckResult).optional(),
2310
+ })
2311
+ .passthrough();
2312
+ export type RigChangeVerification = z.infer<typeof RigChangeVerification>;
2313
+
2314
+ export const RigChange = z.object({
2315
+ id: z.string().uuid(),
2316
+ rigId: z.string().uuid(),
2317
+ baseVersionId: z.string().uuid().nullable(),
2318
+ kind: RigChangeKind,
2319
+ payload: z.record(z.string(), z.unknown()),
2320
+ status: RigChangeStatus,
2321
+ proposedBy: z.string().nullable(),
2322
+ verification: RigChangeVerification.nullable(),
2323
+ resultVersionId: z.string().uuid().nullable(),
2324
+ createdAt: z.string(),
2325
+ updatedAt: z.string(),
2326
+ });
2327
+ export type RigChange = z.infer<typeof RigChange>;
2328
+
2329
+ export const CreateRigRequest = z.object({
2330
+ name: z.string().min(1).max(120),
2331
+ description: z.string().max(2000).optional(),
2332
+ // Initial (version 1) content, inline.
2333
+ image: z.string().max(1024).optional(),
2334
+ setupScript: z.string().max(131072).optional(),
2335
+ checks: z.array(RigCheck).max(100).default([]),
2336
+ credentialHooks: z.array(z.string().min(1).max(200)).max(50).default([]),
2337
+ defaultVariableSetIds: z.array(z.string().uuid()).max(25).default([]),
2338
+ });
2339
+ export type CreateRigRequest = z.infer<typeof CreateRigRequest>;
2340
+
2341
+ export const UpdateRigRequest = z.object({
1688
2342
  name: z.string().min(1).max(120).optional(),
1689
2343
  description: z.string().max(2000).nullable().optional(),
1690
2344
  });
1691
- export type UpdateWorkspaceEnvironmentRequest = z.infer<typeof UpdateWorkspaceEnvironmentRequest>;
2345
+ export type UpdateRigRequest = z.infer<typeof UpdateRigRequest>;
1692
2346
 
1693
- export const SetWorkspaceEnvironmentVariableRequest = z.object({
1694
- value: z.string().min(1).max(32768),
2347
+ // setup_append: the exact command that already worked (+ an optional note).
2348
+ export const RigSetupAppendPayload = z.object({
2349
+ command: z.string().min(1).max(8192),
2350
+ note: z.string().max(2000).optional(),
1695
2351
  });
1696
- export type SetWorkspaceEnvironmentVariableRequest = z.infer<typeof SetWorkspaceEnvironmentVariableRequest>;
2352
+ export type RigSetupAppendPayload = z.infer<typeof RigSetupAppendPayload>;
2353
+
2354
+ // definition_edit: the full next-version content (all fields optional; unset
2355
+ // fields inherit from the base version at promote time).
2356
+ export const RigDefinitionEditPayload = z.object({
2357
+ image: z.string().max(1024).nullish(),
2358
+ setupScript: z.string().max(131072).nullish(),
2359
+ checks: z.array(RigCheck).max(100).optional(),
2360
+ credentialHooks: z.array(z.string().min(1).max(200)).max(50).optional(),
2361
+ defaultVariableSetIds: z.array(z.string().uuid()).max(25).optional(),
2362
+ changelog: z.string().max(4096).nullish(),
2363
+ });
2364
+ export type RigDefinitionEditPayload = z.infer<typeof RigDefinitionEditPayload>;
2365
+
2366
+ export const ProposeRigChangeRequest = z.discriminatedUnion("kind", [
2367
+ z.object({ kind: z.literal("setup_append"), payload: RigSetupAppendPayload }),
2368
+ z.object({ kind: z.literal("definition_edit"), payload: RigDefinitionEditPayload }),
2369
+ ]);
2370
+ export type ProposeRigChangeRequest = z.infer<typeof ProposeRigChangeRequest>;
1697
2371
 
1698
2372
  export const ScheduledTaskStatus = z.enum(["active", "paused"]);
1699
2373
  export type ScheduledTaskStatus = z.infer<typeof ScheduledTaskStatus>;
@@ -1727,7 +2401,10 @@ export const ScheduledTaskScheduleSpec = z.discriminatedUnion("type", [
1727
2401
  timeZone: z.string().min(1).default("UTC"),
1728
2402
  hour: z.number().int().min(0).max(23),
1729
2403
  minute: z.number().int().min(0).max(59),
1730
- daysOfWeek: z.array(z.enum(["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"])).min(1).optional(),
2404
+ daysOfWeek: z
2405
+ .array(z.enum(["SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"]))
2406
+ .min(1)
2407
+ .optional(),
1731
2408
  }),
1732
2409
  ]);
1733
2410
  export type ScheduledTaskScheduleSpec = z.infer<typeof ScheduledTaskScheduleSpec>;
@@ -1756,7 +2433,13 @@ export const ScheduledTask = z.object({
1756
2433
  overlapPolicy: ScheduledTaskOverlapPolicy,
1757
2434
  agentConfig: ScheduledTaskAgentConfig,
1758
2435
  reusableSessionId: z.string().uuid().nullable(),
1759
- environmentId: z.string().uuid().nullable(),
2436
+ variableSetId: z.string().uuid().nullable().default(null),
2437
+ /** @deprecated use variableSetId */
2438
+ environmentId: z.string().uuid().nullable().default(null),
2439
+ // The rig each run binds to (M3). Stored on the task; the ACTIVE version is
2440
+ // resolved PER FIRE (at dispatch), so a task always runs the rig's current
2441
+ // version rather than one frozen at task-create time. Null ⇒ rig-less runs.
2442
+ rigId: z.string().uuid().nullable().default(null),
1760
2443
  metadata: z.record(z.string(), z.unknown()),
1761
2444
  createdAt: z.string(),
1762
2445
  updatedAt: z.string(),
@@ -1780,26 +2463,33 @@ export const ScheduledTaskRun = z.object({
1780
2463
  });
1781
2464
  export type ScheduledTaskRun = z.infer<typeof ScheduledTaskRun>;
1782
2465
 
1783
- export const CreateScheduledTaskRequest = z.object({
2466
+ export const CreateScheduledTaskRequest = withVariableSetIdAlias({
1784
2467
  name: z.string().min(1),
1785
2468
  schedule: ScheduledTaskScheduleSpec,
1786
2469
  runMode: ScheduledTaskRunMode.default("new_session_per_run"),
1787
2470
  overlapPolicy: ScheduledTaskOverlapPolicy.default("allow_concurrent"),
1788
2471
  agentConfig: ScheduledTaskAgentConfig,
1789
2472
  status: ScheduledTaskStatus.default("active"),
2473
+ variableSetId: z.string().uuid().nullable().optional(),
1790
2474
  environmentId: z.string().uuid().nullable().optional(),
2475
+ // The rig each run binds to (M3); its active version is resolved per fire.
2476
+ rigId: z.string().uuid().nullable().optional(),
1791
2477
  metadata: z.record(z.string(), z.unknown()).default({}),
1792
2478
  });
1793
2479
  export type CreateScheduledTaskRequest = z.infer<typeof CreateScheduledTaskRequest>;
1794
2480
 
1795
- export const UpdateScheduledTaskRequest = z.object({
2481
+ export const UpdateScheduledTaskRequest = withVariableSetIdAlias({
1796
2482
  name: z.string().min(1).optional(),
1797
2483
  schedule: ScheduledTaskScheduleSpec.optional(),
1798
2484
  runMode: ScheduledTaskRunMode.optional(),
1799
2485
  overlapPolicy: ScheduledTaskOverlapPolicy.optional(),
1800
2486
  agentConfig: ScheduledTaskAgentConfig.optional(),
1801
2487
  status: ScheduledTaskStatus.optional(),
2488
+ variableSetId: z.string().uuid().nullable().optional(),
1802
2489
  environmentId: z.string().uuid().nullable().optional(),
2490
+ // The rig each run binds to (M3); null clears it. Its active version is
2491
+ // resolved per fire, so an update takes effect on the next dispatch.
2492
+ rigId: z.string().uuid().nullable().optional(),
1803
2493
  metadata: z.record(z.string(), z.unknown()).optional(),
1804
2494
  });
1805
2495
  export type UpdateScheduledTaskRequest = z.infer<typeof UpdateScheduledTaskRequest>;
@@ -1854,7 +2544,9 @@ export const CapabilityPackScheduledTaskTemplate = z.object({
1854
2544
  // instantiable templates; built-in packs may instead build prompts in code.
1855
2545
  prompt: z.string().min(1).optional(),
1856
2546
  });
1857
- export type CapabilityPackScheduledTaskTemplate = z.infer<typeof CapabilityPackScheduledTaskTemplate>;
2547
+ export type CapabilityPackScheduledTaskTemplate = z.infer<
2548
+ typeof CapabilityPackScheduledTaskTemplate
2549
+ >;
1858
2550
 
1859
2551
  // One file inside a pack skill directory. Paths are workspace-relative POSIX
1860
2552
  // paths inside the skill directory (for example "SKILL.md" or
@@ -1872,66 +2564,119 @@ export type CapabilityPackSkillFile = z.infer<typeof CapabilityPackSkillFile>;
1872
2564
  // A skill delivered by a capability pack. The name doubles as the skill
1873
2565
  // directory under the sandbox skill index (skills/<name>), so it must be a
1874
2566
  // single safe path segment. Every skill must ship a top-level SKILL.md.
1875
- export const CapabilityPackSkill = z.object({
1876
- name: z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
1877
- message: "skill name must be a single path segment of letters, digits, '.', '_' or '-'",
1878
- }),
1879
- description: z.string().min(1).max(2048).optional(),
1880
- files: z.array(CapabilityPackSkillFile).min(1).max(64),
1881
- }).superRefine((skill, ctx) => {
1882
- const seen = new Set<string>();
1883
- skill.files.forEach((file, index) => {
1884
- if (seen.has(file.path)) {
1885
- ctx.addIssue({ code: "custom", message: `duplicate skill file path: ${file.path}`, path: ["files", index, "path"] });
2567
+ export const CapabilityPackSkill = z
2568
+ .object({
2569
+ name: z
2570
+ .string()
2571
+ .min(1)
2572
+ .max(64)
2573
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, {
2574
+ message: "skill name must be a single path segment of letters, digits, '.', '_' or '-'",
2575
+ }),
2576
+ description: z.string().min(1).max(2048).optional(),
2577
+ files: z.array(CapabilityPackSkillFile).min(1).max(64),
2578
+ })
2579
+ .superRefine((skill, ctx) => {
2580
+ const seen = new Set<string>();
2581
+ skill.files.forEach((file, index) => {
2582
+ if (seen.has(file.path)) {
2583
+ ctx.addIssue({
2584
+ code: "custom",
2585
+ message: `duplicate skill file path: ${file.path}`,
2586
+ path: ["files", index, "path"],
2587
+ });
2588
+ }
2589
+ seen.add(file.path);
2590
+ });
2591
+ if (!skill.files.some((file) => file.path === "SKILL.md")) {
2592
+ ctx.addIssue({
2593
+ code: "custom",
2594
+ message: "skill must include a top-level SKILL.md file",
2595
+ path: ["files"],
2596
+ });
1886
2597
  }
1887
- seen.add(file.path);
1888
2598
  });
1889
- if (!skill.files.some((file) => file.path === "SKILL.md")) {
1890
- ctx.addIssue({ code: "custom", message: "skill must include a top-level SKILL.md file", path: ["files"] });
1891
- }
1892
- });
1893
2599
  export type CapabilityPackSkill = z.infer<typeof CapabilityPackSkill>;
1894
2600
 
1895
2601
  function isSafePackSkillRelativePath(path: string): boolean {
1896
2602
  if (path.startsWith("/") || path.includes("\\")) {
1897
2603
  return false;
1898
2604
  }
1899
- return path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
2605
+ return path
2606
+ .split("/")
2607
+ .every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
1900
2608
  }
1901
2609
 
1902
- export const CapabilityPack = z.object({
1903
- id: z.string().min(1),
1904
- name: z.string().min(1),
2610
+ const CapabilityPackVariableSet = z.object({
1905
2611
  description: z.string().min(1),
1906
- role: z.string().min(1),
1907
- category: z.string().min(1),
1908
- version: z.string().min(1),
1909
- // Container image ref (digest-pinned recommended) the pack's sessions run
1910
- // in. At most one enabled pack per workspace may declare one; with none,
1911
- // sessions use the deployment-wide image settings.
1912
- sandboxImage: z.string().trim().min(1).max(512).optional(),
1913
- // Skills delivered into the sandbox skill index when the pack is enabled.
1914
- skills: z.array(CapabilityPackSkill).max(32).superRefine((skills, ctx) => {
1915
- const seen = new Set<string>();
1916
- skills.forEach((skill, index) => {
1917
- const key = skill.name.toLowerCase();
1918
- if (seen.has(key)) {
1919
- ctx.addIssue({ code: "custom", message: `duplicate pack skill name: ${skill.name}`, path: [index, "name"] });
1920
- }
1921
- seen.add(key);
1922
- });
1923
- }).default([]),
1924
- tools: z.array(ToolRef).default([]),
1925
- connectors: z.array(CapabilityPackConnector).default([]),
1926
- knowledge: z.array(CapabilityPackKnowledge).default([]),
1927
- scheduledTaskTemplates: z.array(CapabilityPackScheduledTaskTemplate).default([]),
1928
- environment: z.object({
1929
- description: z.string().min(1),
1930
- requiredVariables: z.array(WorkspaceEnvironmentVariableName).default([]),
1931
- required: z.boolean().default(false),
1932
- }).optional(),
1933
- metadata: z.record(z.string(), z.unknown()).default({}),
2612
+ requiredVariables: z.array(VariableSetVariableName).default([]),
2613
+ required: z.boolean().default(false),
1934
2614
  });
2615
+
2616
+ export const CapabilityPack = z.preprocess(
2617
+ (input) => {
2618
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
2619
+ return input;
2620
+ }
2621
+ const record = input as Record<string, unknown>;
2622
+ if (record.variableSet !== undefined) {
2623
+ return record;
2624
+ }
2625
+ if (record.environment !== undefined) {
2626
+ const { environment: _environment, ...rest } = record;
2627
+ return { ...rest, variableSet: record.environment };
2628
+ }
2629
+ if (record.requiredVariables !== undefined) {
2630
+ const { requiredVariables: _requiredVariables, ...rest } = record;
2631
+ return {
2632
+ ...rest,
2633
+ variableSet: {
2634
+ description: "Required variables",
2635
+ requiredVariables: record.requiredVariables,
2636
+ required: Array.isArray(record.requiredVariables) && record.requiredVariables.length > 0,
2637
+ },
2638
+ };
2639
+ }
2640
+ return record;
2641
+ },
2642
+ z.object({
2643
+ id: z.string().min(1),
2644
+ name: z.string().min(1),
2645
+ description: z.string().min(1),
2646
+ role: z.string().min(1),
2647
+ category: z.string().min(1),
2648
+ version: z.string().min(1),
2649
+ // Container image ref (digest-pinned recommended) the pack's sessions run
2650
+ // in. At most one enabled pack per workspace may declare one; with none,
2651
+ // sessions use the deployment-wide image settings.
2652
+ sandboxImage: z.string().trim().min(1).max(512).optional(),
2653
+ // Skills delivered into the sandbox skill index when the pack is enabled.
2654
+ skills: z
2655
+ .array(CapabilityPackSkill)
2656
+ .max(32)
2657
+ .superRefine((skills, ctx) => {
2658
+ const seen = new Set<string>();
2659
+ skills.forEach((skill, index) => {
2660
+ const key = skill.name.toLowerCase();
2661
+ if (seen.has(key)) {
2662
+ ctx.addIssue({
2663
+ code: "custom",
2664
+ message: `duplicate pack skill name: ${skill.name}`,
2665
+ path: [index, "name"],
2666
+ });
2667
+ }
2668
+ seen.add(key);
2669
+ });
2670
+ })
2671
+ .default([]),
2672
+ tools: z.array(ToolRef).default([]),
2673
+ connectors: z.array(CapabilityPackConnector).default([]),
2674
+ knowledge: z.array(CapabilityPackKnowledge).default([]),
2675
+ scheduledTaskTemplates: z.array(CapabilityPackScheduledTaskTemplate).default([]),
2676
+ variableSet: CapabilityPackVariableSet.optional(),
2677
+ metadata: z.record(z.string(), z.unknown()).default({}),
2678
+ }),
2679
+ );
1935
2680
  export type CapabilityPack = z.infer<typeof CapabilityPack>;
1936
2681
 
1937
2682
  // Registering a pack stores the manifest itself; the request body is a full
@@ -1963,7 +2708,8 @@ export const PackInstallation = z.object({
1963
2708
  });
1964
2709
  export type PackInstallation = z.infer<typeof PackInstallation>;
1965
2710
 
1966
- export const EnablePackRequest = z.object({
2711
+ export const EnablePackRequest = withVariableSetIdAlias({
2712
+ variableSetId: z.string().uuid().optional(),
1967
2713
  environmentId: z.string().uuid().optional(),
1968
2714
  metadata: z.record(z.string(), z.unknown()).default({}),
1969
2715
  });
@@ -2043,6 +2789,123 @@ export const CreateSocialPostRequest = z.object({
2043
2789
  });
2044
2790
  export type CreateSocialPostRequest = z.infer<typeof CreateSocialPostRequest>;
2045
2791
 
2792
+ export const ConnectionKind = z.enum(["oauth2", "api_key", "app_install", "delegated"]);
2793
+ export type ConnectionKind = z.infer<typeof ConnectionKind>;
2794
+
2795
+ export const ConnectionStatus = z.enum(["active", "needs_reauth", "revoked", "error"]);
2796
+ export type ConnectionStatus = z.infer<typeof ConnectionStatus>;
2797
+
2798
+ export const McpServerConnectionRef = z
2799
+ .object({
2800
+ connectionId: z.string().uuid().optional(),
2801
+ providerDomain: z.string().min(1),
2802
+ kind: ConnectionKind.optional(),
2803
+ scopes: z.array(z.string().min(1)).optional(),
2804
+ resource: z.string().min(1).optional(),
2805
+ subjectScope: z.enum(["workspace", "subject"]).optional(),
2806
+ })
2807
+ .strict();
2808
+ export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRef>;
2809
+
2810
+ export const ConnectionMetadata = z.object({
2811
+ id: z.string().uuid(),
2812
+ accountId: z.string().uuid(),
2813
+ workspaceId: z.string().uuid(),
2814
+ subjectId: z.string().nullable(),
2815
+ providerDomain: z.string(),
2816
+ kind: ConnectionKind,
2817
+ status: ConnectionStatus,
2818
+ grantedScopes: z.array(z.string()),
2819
+ expiresAt: z.string().nullable(),
2820
+ lastRefreshAt: z.string().nullable(),
2821
+ lastUsedAt: z.string().nullable(),
2822
+ lastError: z.string().nullable(),
2823
+ version: z.number().int().positive(),
2824
+ metadata: z.record(z.string(), z.unknown()),
2825
+ createdBySubjectId: z.string().nullable(),
2826
+ updatedBySubjectId: z.string().nullable(),
2827
+ createdAt: z.string(),
2828
+ updatedAt: z.string(),
2829
+ });
2830
+ export type ConnectionMetadata = z.infer<typeof ConnectionMetadata>;
2831
+
2832
+ export const ConnectionCredentialBundle = z.record(z.string(), z.unknown());
2833
+ export type ConnectionCredentialBundle = z.infer<typeof ConnectionCredentialBundle>;
2834
+
2835
+ export const CreateConnectionRequest = z.object({
2836
+ providerDomain: z.string().min(1),
2837
+ kind: ConnectionKind,
2838
+ subjectId: z.string().min(1).nullable().optional(),
2839
+ credential: ConnectionCredentialBundle,
2840
+ grantedScopes: z.array(z.string().min(1)).default([]),
2841
+ expiresAt: z.string().datetime({ offset: true }).nullable().optional(),
2842
+ metadata: z.record(z.string(), z.unknown()).default({}),
2843
+ });
2844
+ export type CreateConnectionRequest = z.infer<typeof CreateConnectionRequest>;
2845
+
2846
+ export const UpdateConnectionRequest = z.object({
2847
+ providerDomain: z.string().min(1).optional(),
2848
+ subjectId: z.string().min(1).nullable().optional(),
2849
+ kind: ConnectionKind.optional(),
2850
+ status: ConnectionStatus.optional(),
2851
+ credential: ConnectionCredentialBundle.optional(),
2852
+ grantedScopes: z.array(z.string().min(1)).optional(),
2853
+ expiresAt: z.string().datetime({ offset: true }).nullable().optional(),
2854
+ metadata: z.record(z.string(), z.unknown()).optional(),
2855
+ });
2856
+ export type UpdateConnectionRequest = z.infer<typeof UpdateConnectionRequest>;
2857
+
2858
+ export const ConnectionResponse = z.object({
2859
+ connection: ConnectionMetadata,
2860
+ });
2861
+ export type ConnectionResponse = z.infer<typeof ConnectionResponse>;
2862
+
2863
+ export const ListConnectionsResponse = z.object({
2864
+ connections: z.array(ConnectionMetadata),
2865
+ });
2866
+ export type ListConnectionsResponse = z.infer<typeof ListConnectionsResponse>;
2867
+
2868
+ export const OAuthStartRequest = z
2869
+ .object({
2870
+ providerDomain: z.string().min(1).optional(),
2871
+ mcpUrl: z.string().url().optional(),
2872
+ resource: z.string().url().optional(),
2873
+ requestedScopes: z.array(z.string().min(1)).default([]),
2874
+ returnPath: z.string().min(1).optional(),
2875
+ connectionId: z.string().uuid().optional(),
2876
+ oauthClient: z
2877
+ .object({
2878
+ clientId: z.string().min(1),
2879
+ clientSecret: z.string().min(1).optional(),
2880
+ tokenEndpointAuthMethod: z
2881
+ .enum(["none", "client_secret_post", "client_secret_basic"])
2882
+ .optional(),
2883
+ })
2884
+ .optional(),
2885
+ })
2886
+ .refine((value) => Boolean(value.mcpUrl ?? value.resource), {
2887
+ message: "mcpUrl is required",
2888
+ path: ["mcpUrl"],
2889
+ });
2890
+ export type OAuthStartRequest = z.infer<typeof OAuthStartRequest>;
2891
+
2892
+ export const OAuthStartResponse = z.object({
2893
+ state: z.string().min(1),
2894
+ authorizationUrl: z.string().url().nullable(),
2895
+ expiresAt: z.string(),
2896
+ });
2897
+ export type OAuthStartResponse = z.infer<typeof OAuthStartResponse>;
2898
+
2899
+ export const IntegrationClientMetadata = z.object({
2900
+ client_id: z.string().url(),
2901
+ client_name: z.literal("OpenGeni"),
2902
+ redirect_uris: z.array(z.string().url()),
2903
+ token_endpoint_auth_method: z.literal("none"),
2904
+ grant_types: z.array(z.enum(["authorization_code", "refresh_token"])),
2905
+ response_types: z.array(z.literal("code")),
2906
+ });
2907
+ export type IntegrationClientMetadata = z.infer<typeof IntegrationClientMetadata>;
2908
+
2046
2909
  export const MarketingDailyAnalysisTaskRequest = z.object({
2047
2910
  name: z.string().min(1).optional(),
2048
2911
  connectionIds: z.array(z.string().uuid()).default([]),
@@ -2060,12 +2923,24 @@ export type MarketingDailyAnalysisTaskRequest = z.infer<typeof MarketingDailyAna
2060
2923
  export const CapabilityKind = z.enum(["pack", "mcp", "api", "skill", "plugin"]);
2061
2924
  export type CapabilityKind = z.infer<typeof CapabilityKind>;
2062
2925
 
2063
- export const CapabilitySource = z.enum(["built_in", "configured", "public_registry", "manual"]);
2926
+ export const CapabilitySource = z.enum([
2927
+ "built_in",
2928
+ "configured",
2929
+ "public_registry",
2930
+ "registry",
2931
+ "manual",
2932
+ ]);
2064
2933
  export type CapabilitySource = z.infer<typeof CapabilitySource>;
2065
2934
 
2066
2935
  export const CapabilityInstallationStatus = z.enum(["active", "disabled"]);
2067
2936
  export type CapabilityInstallationStatus = z.infer<typeof CapabilityInstallationStatus>;
2068
2937
 
2938
+ export const CapabilityCatalogAuthKind = z.enum(["oauth2", "api_key", "none", "unknown"]);
2939
+ export type CapabilityCatalogAuthKind = z.infer<typeof CapabilityCatalogAuthKind>;
2940
+
2941
+ export const CapabilityCatalogTier = z.enum(["verified", "community"]);
2942
+ export type CapabilityCatalogTier = z.infer<typeof CapabilityCatalogTier>;
2943
+
2069
2944
  export const CapabilityRuntime = z.object({
2070
2945
  available: z.boolean().default(false),
2071
2946
  mcpServerId: z.string().min(1).optional(),
@@ -2088,10 +2963,34 @@ export const CapabilityCatalogItem = z.object({
2088
2963
  endpointUrl: z.string().url().nullable().default(null),
2089
2964
  installUrl: z.string().url().nullable().default(null),
2090
2965
  authModel: z.string().min(1).nullable().default(null),
2966
+ providerDomain: z.string().min(1).nullable().default(null),
2967
+ surfaceType: z.string().min(1).nullable().default(null),
2968
+ transport: z.string().min(1).nullable().default(null),
2969
+ mcpUrl: z.string().url().nullable().default(null),
2970
+ authKind: CapabilityCatalogAuthKind.nullable().default(null),
2971
+ credentialFacts: z.array(z.record(z.string(), z.unknown())).default([]),
2972
+ tier: CapabilityCatalogTier.nullable().default(null),
2973
+ provenance: z.string().min(1).nullable().default(null),
2974
+ logoAssetPath: z.string().min(1).nullable().default(null),
2975
+ importBatchId: z.string().uuid().nullable().default(null),
2976
+ stale: z.boolean().default(false),
2977
+ staleAt: z.string().nullable().default(null),
2091
2978
  tools: z.array(ToolRef).default([]),
2092
2979
  runtime: CapabilityRuntime.default({ available: false, notes: null }),
2093
2980
  enabled: z.boolean().default(false),
2094
2981
  enabledReason: z.string().nullable().default(null),
2982
+ // The connection backing this enabled installation, when the enable-time
2983
+ // connectionRef resolved to one (null for header/credential-free items —
2984
+ // that means "no connection involved", not "broken"). Lets the UI match
2985
+ // connection health by id instead of guessing from providerDomain alone.
2986
+ connectionRef: z
2987
+ .object({
2988
+ connectionId: z.string().min(1),
2989
+ providerDomain: z.string().min(1),
2990
+ kind: z.string().min(1),
2991
+ })
2992
+ .nullable()
2993
+ .default(null),
2095
2994
  metadata: z.record(z.string(), z.unknown()).default({}),
2096
2995
  createdAt: z.string().optional(),
2097
2996
  updatedAt: z.string().optional(),
@@ -2128,22 +3027,24 @@ export const CreateCapabilityCatalogItemRequest = z.object({
2128
3027
  });
2129
3028
  export type CreateCapabilityCatalogItemRequest = z.infer<typeof CreateCapabilityCatalogItemRequest>;
2130
3029
 
2131
- export const EnableCapabilityRequest = z.object({
3030
+ export const EnableCapabilityRequest = withVariableSetIdAlias({
2132
3031
  config: z.record(z.string(), z.unknown()).default({}),
2133
3032
  metadata: z.record(z.string(), z.unknown()).default({}),
3033
+ connectionRef: McpServerConnectionRef.optional(),
2134
3034
  /**
2135
3035
  * Credential headers for remote MCP capabilities (for example an
2136
3036
  * Authorization bearer token). Values are encrypted at rest with the
2137
- * workspace-environments key, injected only into the runtime MCP client,
3037
+ * workspace-variable-sets key, injected only into the runtime MCP client,
2138
3038
  * and never returned by the API — responses expose header names only.
2139
3039
  */
2140
3040
  headers: z.record(z.string(), z.string()).default({}),
2141
3041
  /**
2142
- * Initial environment attachment for kind=pack capabilities. Mirrors the
3042
+ * Initial variableSet attachment for kind=pack capabilities. Mirrors the
2143
3043
  * dedicated POST /packs/:id/enable body: required to enable an
2144
- * environment.required pack through the unified capability-enable path,
3044
+ * variableSet.required pack through the unified capability-enable path,
2145
3045
  * optional otherwise. Ignored by non-pack capabilities.
2146
3046
  */
3047
+ variableSetId: z.string().uuid().optional(),
2147
3048
  environmentId: z.string().uuid().optional(),
2148
3049
  });
2149
3050
  export type EnableCapabilityRequest = z.infer<typeof EnableCapabilityRequest>;
@@ -2191,7 +3092,16 @@ export const Session = z.object({
2191
3092
  // stale in-flight op and retry against the new active sandbox.
2192
3093
  activeSandboxId: z.string().uuid().nullable(),
2193
3094
  activeEpoch: z.number().int().nonnegative(),
2194
- environmentId: z.string().uuid().nullable(),
3095
+ variableSetId: z.string().uuid().nullable().default(null),
3096
+ /** @deprecated use variableSetId */
3097
+ environmentId: z.string().uuid().nullable().default(null),
3098
+ // The rig this session rides (M3 runtime binding). Both are resolved and
3099
+ // FROZEN at session create: rigId names the rig, rigVersionId pins the exact
3100
+ // active version the session's box/env/setup/doctrine are built from for the
3101
+ // session's whole life (a later promote does NOT move an existing session).
3102
+ // Both null ⇒ a rig-less session (byte-for-byte today's behavior).
3103
+ rigId: z.string().uuid().nullable().default(null),
3104
+ rigVersionId: z.string().uuid().nullable().default(null),
2195
3105
  // Non-default first-party MCP token permissions (manager-style sessions);
2196
3106
  // null means the fixed worker default set.
2197
3107
  firstPartyMcpPermissions: z.array(Permission).nullable(),
@@ -2210,9 +3120,18 @@ export const Session = z.object({
2210
3120
  temporalWorkflowId: z.string().nullable(),
2211
3121
  activeTurnId: z.string().uuid().nullable(),
2212
3122
  // Actual input tokens of the last model call of the most recent turn; the
2213
- // pre-turn client-side context-compaction trigger reads it as its budget
3123
+ // pre-turn portable context-compaction trigger reads it as its budget
2214
3124
  // signal. Null until a turn with usage has completed.
2215
3125
  lastInputTokens: z.number().int().nonnegative().nullable(),
3126
+ queueVersion: z.number().int().nonnegative(),
3127
+ queueHeadPosition: z.number().int(),
3128
+ queueTailPosition: z.number().int(),
3129
+ controlState: SessionControlState,
3130
+ controlGeneration: z.number().int().nonnegative(),
3131
+ controlReason: z.string().nullable(),
3132
+ controlChangedBy: z.string().nullable(),
3133
+ controlChangedAt: z.string().nullable(),
3134
+ workspaceRunExceptionGeneration: z.number().int().nonnegative().nullable(),
2216
3135
  lastSequence: z.number().int().nonnegative(),
2217
3136
  // Multi-account Codex (P1). codexPinnedCredentialId: the account this session is
2218
3137
  // manually PINNED to (null ⇒ follow the workspace active pointer).
@@ -2220,33 +3139,100 @@ export const Session = z.object({
2220
3139
  // "Running on:" indicator's source). Both are credential-row ids, null until set.
2221
3140
  codexPinnedCredentialId: z.string().uuid().nullable(),
2222
3141
  codexLastCredentialId: z.string().uuid().nullable(),
3142
+ /** Personal (authenticated subject) workspace pin state, never workspace-global. */
3143
+ pinned: z.boolean().default(false),
3144
+ /** Stable pin ordering key; null when this subject has not pinned the session. */
3145
+ pinnedAt: z.string().nullable().default(null),
3146
+ /** Optimistic pin-state revision; zero represents an absent pin relation. */
3147
+ pinVersion: z.number().int().nonnegative().default(0),
3148
+ /**
3149
+ * Server-authoritative hierarchy summary populated on session-list reads.
3150
+ * Detail reads may omit it. The rail uses this instead of guessing a tree
3151
+ * from whichever global recency page happened to be loaded.
3152
+ */
3153
+ treeStats: z
3154
+ .object({
3155
+ directChildren: z.number().int().nonnegative(),
3156
+ totalDescendants: z.number().int().nonnegative(),
3157
+ runningDescendants: z.number().int().nonnegative(),
3158
+ queuedDescendants: z.number().int().nonnegative(),
3159
+ attentionDescendants: z.number().int().nonnegative(),
3160
+ pausedDescendants: z.number().int().nonnegative(),
3161
+ failedDescendants: z.number().int().nonnegative(),
3162
+ })
3163
+ .optional(),
2223
3164
  createdAt: z.string(),
2224
3165
  updatedAt: z.string(),
2225
3166
  });
2226
3167
  export type Session = z.infer<typeof Session>;
2227
3168
 
3169
+ export type SessionSummary = Session;
3170
+
3171
+ /**
3172
+ * The canonical session-list page. Pinned rows are returned separately and are
3173
+ * excluded from `sessions`, so a cursor can page ordinary recency rows without
3174
+ * duplicating a pin. Pins are filtered by the same parent/search predicates as
3175
+ * ordinary rows and ordered by pinnedAt DESC, id DESC.
3176
+ */
3177
+ export const SessionListResponse = z.object({
3178
+ pinned: z.array(Session),
3179
+ sessions: z.array(Session),
3180
+ nextCursor: z.string().nullable(),
3181
+ });
3182
+ export type SessionListResponse = z.infer<typeof SessionListResponse>;
3183
+
3184
+ // Recursive: the TS type is declared first so the schema annotation can carry
3185
+ // the FULL recursive shape (a shallow annotation loses type information for
3186
+ // contracts consumers after one level of nesting).
3187
+ export type LineageNode = {
3188
+ session: SessionSummary;
3189
+ children: LineageNode[];
3190
+ };
3191
+ export const LineageNode: z.ZodType<LineageNode> = z.lazy(() =>
3192
+ z.object({
3193
+ session: Session,
3194
+ children: z.array(LineageNode),
3195
+ }),
3196
+ );
3197
+
3198
+ export const SessionLineageResponse = z.object({
3199
+ ancestors: z.array(Session),
3200
+ children: z.array(LineageNode),
3201
+ truncated: z.boolean().default(false),
3202
+ });
3203
+ export type SessionLineageResponse = z.infer<typeof SessionLineageResponse>;
3204
+
2228
3205
  export const SessionEventType = z.enum([
2229
3206
  "session.created",
2230
3207
  "session.status.changed",
2231
3208
  "session.requiresAction",
3209
+ "session.context.compaction.requested",
2232
3210
  "session.context.compacted",
3211
+ "session.context.compaction.skipped",
2233
3212
  "session.context.cleared",
2234
3213
  "user.message",
2235
- "user.interrupt",
3214
+ "user.pause",
2236
3215
  "user.approvalDecision",
2237
3216
  "turn.queued",
2238
- "turn.updated",
2239
3217
  "turn.started",
2240
3218
  "turn.completed",
2241
3219
  "turn.failed",
2242
3220
  "turn.cancelled",
2243
- "turn.preempted",
3221
+ "turn.superseded",
3222
+ "turn.recovery.requested",
3223
+ "turn.capacity_waiting",
2244
3224
  "agent.message.delta",
2245
3225
  "agent.message.completed",
2246
3226
  "agent.reasoning.delta",
2247
3227
  "agent.toolCall.created",
2248
3228
  "agent.toolCall.output",
3229
+ "agent.model.usage",
3230
+ "tool.auth_needed",
2249
3231
  "agent.updated",
3232
+ "rig.setup.started",
3233
+ "rig.setup.completed",
3234
+ "rig.setup.skipped",
3235
+ "rig.setup.failed",
2250
3236
  "sandbox.operation.started",
2251
3237
  "sandbox.operation.completed",
2252
3238
  "sandbox.operation.failed",
@@ -2257,7 +3243,22 @@ export const SessionEventType = z.enum([
2257
3243
  "goal.completed",
2258
3244
  "goal.paused",
2259
3245
  "goal.resumed",
3246
+ "goal.cleared",
2260
3247
  "goal.continuation",
3248
+ "system.update.pending",
3249
+ "system.update.delivered",
3250
+ "session.control.paused",
3251
+ "session.control.resumed",
3252
+ "session.control.steer_requested",
3253
+ "workspace.inference.paused",
3254
+ "workspace.inference.resumed",
3255
+ "session.queue.prompt.cancelled",
3256
+ "session.queue.history",
3257
+ // A terminal/stale activity callback is retained as an audit wrapper rather
3258
+ // than being dropped or emitted as though it belonged to the current turn.
3259
+ "turn.event.rejected_late",
3260
+ "memory.saved",
3261
+ "memory.corrected",
2261
3262
  // Channel-B desktop pixel-plane signals (07-channel-b §1.2). The pixel socket
2262
3263
  // carries opaque RFB and cannot carry a control message the client can act on,
2263
3264
  // so these ride the durable, sequenced, gap-filled Channel-A SSE spine.
@@ -2290,9 +3291,94 @@ export const SessionEventType = z.enum([
2290
3291
  // (manual switch in P1; failover/rotation in P3 reuse the same event). Drives
2291
3292
  // the in-session "Running on:" indicator's live flip.
2292
3293
  "codex.account.switched",
3294
+ // OPE-21 per-turn selection audit. Payload is metadata only: credential row
3295
+ // id, bounded strategy/reason, and pool counts — never token material.
3296
+ "codex.credential.selected",
3297
+ // OPE-21 durable zero-capacity wait lifecycle. Runtime/system events only;
3298
+ // no synthetic user message is created when capacity returns.
3299
+ "codex.capacity.waiting",
3300
+ "codex.capacity.resumed",
3301
+ "codex.capacity.superseded",
3302
+ // Sandbox durability observability (sandbox-file-persistence). The 2026-07
3303
+ // incidents (mid-session box death with /workspace loss; a fatal manifest-env
3304
+ // delta on a live box) were near-unattributable because box lifecycle left no
3305
+ // durable trace — only worker logs, which rotate within hours. These events
3306
+ // make every box transition and env recomputation drift readable from the DB
3307
+ // alone. Payloads carry ids/flags/key NAMES only — never env values (secrets).
3308
+ "sandbox.box.created", // box cold-created/cold-restored ({hydrated: "archive"|"none"})
3309
+ "sandbox.box.lost", // resume-by-id found the box gone (provider NotFound)
3310
+ "sandbox.box.terminated", // reaper drain terminated the box ({actor, persisted})
3311
+ "sandbox.box.snapshot", // mid-session /workspace snapshot persisted ({trigger})
3312
+ "sandbox.env.drift", // recomputed manifest env != live box env (key names only)
3313
+ // Active-sandbox pointer reconcile (issue #341 invariant B). Turn start found the
3314
+ // persisted (active_sandbox_id, active_epoch) pointing at a target the turn cannot
3315
+ // establish — a deleted/absent sandbox, a Modal sibling with no establisher, or a
3316
+ // selfhosted sandbox with no enrollment — and reset it to the session HOME under
3317
+ // the epoch fence instead of routing every op into the dead target. A VISIBLE,
3318
+ // never-silent downgrade: payload carries the typed reason + from/to epoch, never a
3319
+ // target id or command content. Announce-only; hits the timeline projection default
3320
+ // (no rendered item) like the other sandbox.* diagnostics.
3321
+ "session.route.reconciled",
3322
+ // Workbench v2 turn-end workspace capture (dossier §10.1). ANNOUNCE-ONLY: a new
3323
+ // capture revision was persisted at turn end; the client refetches the latest
3324
+ // capture. It carries metadata only (revision/turnId/capturedAt/leaseEpoch/stats),
3325
+ // never file content. Hits the timeline projection default case (ignored) — it
3326
+ // must NEVER gain a rendered timeline item without regenerating the golden
3327
+ // snapshots (dossier §7.3 golden-grammar gate).
3328
+ "workspace.revision.captured",
3329
+ // Repository discovery could not prove a complete capture. The worker
3330
+ // persisted a failed/degraded revision marker and clients must fall back to
3331
+ // the live box rather than trust a zero-repository snapshot.
3332
+ "workspace.revision.degraded",
3333
+ // Connected Machine (selfhosted) op-outcome observability (failure-visibility
3334
+ // doctrine, out-of-band plane). SESSION-scoped facts only: these fire for the
3335
+ // session whose turn ran the op (the two-planes rule — machine-plane facts like
3336
+ // pressure live in the M10 metrics DB, never as session events). Payloads carry
3337
+ // the op kind + a typed fault class + attempt count — NEVER command content.
3338
+ //
3339
+ // `machine.op.failed` fires ONLY for INFRASTRUCTURE fault classes (offline,
3340
+ // draining-exhausted, payload-too-large, reconnecting-timeout, OS/stream/protocol)
3341
+ // — a semantic miss the model asked about (a missing path, a consent gate, a
3342
+ // nonzero exit) is an OUTCOME, not an infra fault, and never fires this.
3343
+ // `machine.op.recovered` is the healed-fault leading indicator (a blip/backpressure
3344
+ // the transport absorbed): announce-only, quiet. Both hit the timeline projection's
3345
+ // quiet status-tick tier (the severity split's "degraded"); adding a rendered item
3346
+ // requires regenerating the golden snapshots (the golden-grammar gate).
3347
+ "machine.op.failed",
3348
+ "machine.op.recovered",
3349
+ // Connected Machine (selfhosted) LINK-plane observability (failure-visibility
3350
+ // doctrine). SESSION-scoped, ANNOUNCE-ONLY facts fanned out to the sessions that
3351
+ // had an active op running on the machine when its control link changed — never
3352
+ // to idle/historical sessions. Payloads carry ids / a typed reason / key-names
3353
+ // only, NEVER command content.
3354
+ //
3355
+ // `machine.link.lost` — the machine announced a clean GoingOffline (its control
3356
+ // link is going away) while a session had a running turn on it. `machine.link.
3357
+ // restored` — a reconnect Hello re-established the link that was previously lost.
3358
+ // `machine.runner.restarted` — the additional signal that the going-offline was
3359
+ // a self-update restart specifically (link.lost also fires for it; this
3360
+ // distinguishes a restart from a plain stop / host shutdown). All three hit the
3361
+ // timeline projection's quiet default tier (no rendered item); adding a rendered
3362
+ // item requires regenerating the golden snapshots (the golden-grammar gate).
3363
+ "machine.link.lost",
3364
+ "machine.link.restored",
3365
+ "machine.runner.restarted",
2293
3366
  ]);
2294
3367
  export type SessionEventType = z.infer<typeof SessionEventType>;
2295
3368
 
3369
+ export const ToolAuthNeededPayload = z.object({
3370
+ serverId: z.string().min(1),
3371
+ toolName: z.string().min(1).nullable().optional(),
3372
+ providerDomain: z.string().min(1),
3373
+ connectionId: z.string().uuid().nullable().optional(),
3374
+ reason: z.enum(["missing_connection", "expired", "insufficient_scope", "refresh_failed"]),
3375
+ scopes: z.array(z.string().min(1)).optional(),
3376
+ resource: z.string().min(1).optional(),
3377
+ authorizationUrl: z.string().url().optional(),
3378
+ subjectId: z.string().min(1).nullable().optional(),
3379
+ });
3380
+ export type ToolAuthNeededPayload = z.infer<typeof ToolAuthNeededPayload>;
3381
+
2296
3382
  // Channel-B stream-event payloads (07-channel-b §1.2). SessionEvent.payload is
2297
3383
  // z.unknown() (NOT a discriminated union) — these are standalone schemas parsed
2298
3384
  // explicitly at the producer (the API-direct handshake/rotation) and the SDK/
@@ -2416,13 +3502,17 @@ export type SandboxCommandOutputDeltaPayload = z.infer<typeof SandboxCommandOutp
2416
3502
  export const FsChangeKind = z.enum(["created", "modified", "deleted", "renamed"]);
2417
3503
  export type FsChangeKind = z.infer<typeof FsChangeKind>;
2418
3504
  export const FsChangedPayload = z.object({
2419
- changes: z.array(z.object({
2420
- path: z.string(), // workspace-relative POSIX path
2421
- kind: FsChangeKind,
2422
- isDir: z.boolean().default(false),
2423
- sizeBytes: z.number().int().nonnegative().nullable().default(null),
2424
- oldPath: z.string().optional(), // for "renamed"
2425
- })).min(1),
3505
+ changes: z
3506
+ .array(
3507
+ z.object({
3508
+ path: z.string(), // workspace-relative POSIX path
3509
+ kind: FsChangeKind,
3510
+ isDir: z.boolean().default(false),
3511
+ sizeBytes: z.number().int().nonnegative().nullable().default(null),
3512
+ oldPath: z.string().optional(), // for "renamed"
3513
+ }),
3514
+ )
3515
+ .min(1),
2426
3516
  source: z.enum(["write", "watch", "agent"]).default("write"),
2427
3517
  // Monotonic FS revision (per-lease, paired with leaseEpoch for staleness).
2428
3518
  revision: z.number().int().nonnegative(),
@@ -2439,7 +3529,9 @@ export const GitChangedPayload = z.object({
2439
3529
  ahead: z.number().int().nonnegative().default(0),
2440
3530
  behind: z.number().int().nonnegative().default(0),
2441
3531
  changedFileCount: z.number().int().nonnegative(),
2442
- reason: z.enum(["commit", "checkout", "stage", "worktree", "fetch", "unknown"]).default("unknown"),
3532
+ reason: z
3533
+ .enum(["commit", "checkout", "stage", "worktree", "fetch", "unknown"])
3534
+ .default("unknown"),
2443
3535
  revision: z.number().int().nonnegative().default(0),
2444
3536
  leaseEpoch: z.number().int().nonnegative().default(0),
2445
3537
  });
@@ -2484,16 +3576,18 @@ export interface FsTreeNode {
2484
3576
  children?: FsTreeNode[] | undefined;
2485
3577
  truncated: boolean; // dir had more entries than the cap
2486
3578
  }
2487
- export const FsTreeNode: z.ZodType<FsTreeNode> = z.lazy(() => z.object({
2488
- name: z.string(),
2489
- path: z.string(),
2490
- type: FsNodeType,
2491
- sizeBytes: z.number().int().nonnegative().nullable(),
2492
- mtimeMs: z.number().int().nonnegative().nullable(),
2493
- mode: z.number().int().nullable(),
2494
- children: z.array(FsTreeNode).optional(),
2495
- truncated: z.boolean().default(false),
2496
- })) as z.ZodType<FsTreeNode>;
3579
+ export const FsTreeNode: z.ZodType<FsTreeNode> = z.lazy(() =>
3580
+ z.object({
3581
+ name: z.string(),
3582
+ path: z.string(),
3583
+ type: FsNodeType,
3584
+ sizeBytes: z.number().int().nonnegative().nullable(),
3585
+ mtimeMs: z.number().int().nonnegative().nullable(),
3586
+ mode: z.number().int().nullable(),
3587
+ children: z.array(FsTreeNode).optional(),
3588
+ truncated: z.boolean().default(false),
3589
+ }),
3590
+ ) as z.ZodType<FsTreeNode>;
2497
3591
 
2498
3592
  export const FsListRequest = z.object({
2499
3593
  path: z.string().default(""), // "" = workspace root
@@ -2514,7 +3608,12 @@ export type FsEncoding = z.infer<typeof FsEncoding>;
2514
3608
  export const FsReadRequest = z.object({
2515
3609
  path: z.string(),
2516
3610
  encoding: FsEncoding.default("utf8"),
2517
- maxBytes: z.number().int().positive().max(25 * 1024 * 1024).default(5 * 1024 * 1024),
3611
+ maxBytes: z
3612
+ .number()
3613
+ .int()
3614
+ .positive()
3615
+ .max(25 * 1024 * 1024)
3616
+ .default(5 * 1024 * 1024),
2518
3617
  });
2519
3618
  export type FsReadRequest = z.infer<typeof FsReadRequest>;
2520
3619
  export const FsReadResponse = z.object({
@@ -2578,7 +3677,15 @@ export type FsMkdirResponse = z.infer<typeof FsMkdirResponse>;
2578
3677
 
2579
3678
  // --- A2 Git request/response (read-only; feeds Pierre diff/tree) -------------
2580
3679
  export const GitFileStatusCode = z.enum([
2581
- "added", "modified", "deleted", "renamed", "copied", "untracked", "ignored", "conflicted", "typechange",
3680
+ "added",
3681
+ "modified",
3682
+ "deleted",
3683
+ "renamed",
3684
+ "copied",
3685
+ "untracked",
3686
+ "ignored",
3687
+ "conflicted",
3688
+ "typechange",
2582
3689
  ]);
2583
3690
  export type GitFileStatusCode = z.infer<typeof GitFileStatusCode>;
2584
3691
  export const GitFileStatus = z.object({
@@ -2645,7 +3752,12 @@ export const GitDiffRequest = z.object({
2645
3752
  toRef: z.string().optional(),
2646
3753
  pathspec: z.array(z.string()).default([]),
2647
3754
  contextLines: z.number().int().min(0).max(10).default(3),
2648
- maxBytesPerFile: z.number().int().positive().max(2 * 1024 * 1024).default(512 * 1024),
3755
+ maxBytesPerFile: z
3756
+ .number()
3757
+ .int()
3758
+ .positive()
3759
+ .max(2 * 1024 * 1024)
3760
+ .default(512 * 1024),
2649
3761
  });
2650
3762
  export type GitDiffRequest = z.infer<typeof GitDiffRequest>;
2651
3763
  export const GitDiffResponse = z.object({
@@ -2654,6 +3766,182 @@ export const GitDiffResponse = z.object({
2654
3766
  });
2655
3767
  export type GitDiffResponse = z.infer<typeof GitDiffResponse>;
2656
3768
 
3769
+ // ─── Workbench v2 turn-end workspace capture (dossier §10.1/§10.2) ────────────
3770
+ // A capture is a point-in-time snapshot of the session workspace's CHANGES,
3771
+ // probed live off the box at turn end (detectRepos → gitStatus/gitDiff → fsRead
3772
+ // after-images → fsList tree index). It is the cold/offline read source that
3773
+ // lets the workbench paint instantly with zero machine round-trips. Live always
3774
+ // wins when the box is warm; a capture is a labelled cache, never a replacement.
3775
+ // This is the shape the M1 worker writes and the M2 API serves inline.
3776
+
3777
+ // One touched file in the capture. `contentRef` is the content-addressed storage
3778
+ // key of its after-image blob (shared across revisions → the GC set-difference
3779
+ // key). Deleted / binary / >5MB (tooLarge) files carry no contentRef; the UI
3780
+ // renders "too large — open live" for tooLarge, and the diff hunks for the rest.
3781
+ export const WorkspaceCaptureFile = z.object({
3782
+ path: z.string(),
3783
+ status: GitFileStatusCode,
3784
+ // sha256 of the captured after-image bytes; null when deleted / tooLarge.
3785
+ hash: z.string().nullable(),
3786
+ // git blob sha of the HEAD version — the wake-on-edit flush guard (dossier
3787
+ // §10.1). null when the path is new/untracked (no HEAD blob).
3788
+ baseHash: z.string().nullable(),
3789
+ // Content-addressed storage key of the after-image; null when deleted /
3790
+ // tooLarge / binary (no inline content captured).
3791
+ contentRef: z.string().nullable(),
3792
+ sizeBytes: z.number().int().nonnegative(),
3793
+ isBinary: z.boolean().default(false),
3794
+ // >5MB per-file content guard tripped: content NOT captured, render "open live".
3795
+ tooLarge: z.boolean().default(false),
3796
+ deleted: z.boolean().default(false),
3797
+ });
3798
+ export type WorkspaceCaptureFile = z.infer<typeof WorkspaceCaptureFile>;
3799
+
3800
+ // One repo discovered in the workspace. `diff` is `git diff HEAD` (combined
3801
+ // staged+unstaged tracked changes vs HEAD — the review diff); `status` is the
3802
+ // full porcelain file list (drives the rail glyphs incl. untracked, which the
3803
+ // HEAD diff omits). root "" = the workspace root repo.
3804
+ export const WorkspaceCaptureRepo = z.object({
3805
+ root: z.string(),
3806
+ head: z.string().nullable(),
3807
+ detached: z.boolean().default(false),
3808
+ upstream: z.string().nullable(),
3809
+ ahead: z.number().int().nonnegative().default(0),
3810
+ behind: z.number().int().nonnegative().default(0),
3811
+ status: z.array(GitFileStatus),
3812
+ diff: z.array(GitFileDiff),
3813
+ });
3814
+ export type WorkspaceCaptureRepo = z.infer<typeof WorkspaceCaptureRepo>;
3815
+
3816
+ export const WorkspaceCaptureDegradedReason = z.enum([
3817
+ "repository_discovery_command_failed",
3818
+ "repository_discovery_timed_out",
3819
+ "repository_discovery_result_limit_exceeded",
3820
+ ]);
3821
+ export type WorkspaceCaptureDegradedReason = z.infer<typeof WorkspaceCaptureDegradedReason>;
3822
+
3823
+ // Rollup counters — carried on the row (jsonb) and the announce event so the UI
3824
+ // can reserve layout (dossier §12 no-layout-shift) before fetching the manifest.
3825
+ export const WorkspaceCaptureStats = z.object({
3826
+ repoCount: z.number().int().nonnegative(),
3827
+ fileCount: z.number().int().nonnegative(),
3828
+ additions: z.number().int().nonnegative(),
3829
+ deletions: z.number().int().nonnegative(),
3830
+ totalBytes: z.number().int().nonnegative(),
3831
+ tooLargeCount: z.number().int().nonnegative(),
3832
+ binaryCount: z.number().int().nonnegative(),
3833
+ treeEntryCount: z.number().int().nonnegative(),
3834
+ treeTruncated: z.boolean().default(false),
3835
+ durationMs: z.number().int().nonnegative(),
3836
+ // sha256 over the change surface (per-file path/hash/status + per-repo diff
3837
+ // summary, tree/mtime excluded). The empty-turn gate skips a capture whose
3838
+ // fingerprint equals the previous revision's — "no new revision when nothing
3839
+ // changed" holds even when the tree stays dirty across read-only turns.
3840
+ fingerprint: z.string().optional(),
3841
+ });
3842
+ export type WorkspaceCaptureStats = z.infer<typeof WorkspaceCaptureStats>;
3843
+
3844
+ // The single manifest blob (one per revision). Holds everything the workbench
3845
+ // needs for a cold paint: the tree index, per-repo status+diff, and the file
3846
+ // index (after-image refs). The M2 API serves this inline when small (≤2MB).
3847
+ export const WorkspaceCaptureManifest = z.object({
3848
+ version: z.literal(1),
3849
+ revision: z.number().int().nonnegative(),
3850
+ capturedAt: z.string(),
3851
+ turnId: z.string().nullable(),
3852
+ leaseEpoch: z.number().int().nonnegative(),
3853
+ treeIndex: FsTreeNode,
3854
+ treeTruncated: z.boolean().default(false),
3855
+ repos: z.array(WorkspaceCaptureRepo),
3856
+ files: z.array(WorkspaceCaptureFile),
3857
+ stats: WorkspaceCaptureStats,
3858
+ });
3859
+ export type WorkspaceCaptureManifest = z.infer<typeof WorkspaceCaptureManifest>;
3860
+
3861
+ // Announce-only event payload (dossier §10.1). Metadata only — never content.
3862
+ export const WorkspaceRevisionCapturedPayload = z.object({
3863
+ revision: z.number().int().nonnegative(),
3864
+ turnId: z.string().nullable(),
3865
+ capturedAt: z.string(),
3866
+ leaseEpoch: z.number().int().nonnegative(),
3867
+ stats: WorkspaceCaptureStats,
3868
+ });
3869
+ export type WorkspaceRevisionCapturedPayload = z.infer<typeof WorkspaceRevisionCapturedPayload>;
3870
+
3871
+ export const WorkspaceRevisionDegradedPayload = z.object({
3872
+ revision: z.number().int().nonnegative(),
3873
+ turnId: z.string().nullable(),
3874
+ capturedAt: z.string(),
3875
+ leaseEpoch: z.number().int().nonnegative(),
3876
+ reason: WorkspaceCaptureDegradedReason,
3877
+ });
3878
+ export type WorkspaceRevisionDegradedPayload = z.infer<typeof WorkspaceRevisionDegradedPayload>;
3879
+
3880
+ // --- M2 capture READ API (dossier §10.3) -------------------------------------
3881
+ // A short-TTL signed GET URL minted PER REQUEST (never stored). The manifest is
3882
+ // served inline for the ≤2MB common case (the <200ms one-round-trip paint); a
3883
+ // >2MB manifest and a >256KB single-file after-image fall back to one of these.
3884
+ export const WorkspaceCaptureSignedUrl = z.object({
3885
+ url: z.string().url(),
3886
+ expiresAt: z.string(),
3887
+ });
3888
+ export type WorkspaceCaptureSignedUrl = z.infer<typeof WorkspaceCaptureSignedUrl>;
3889
+
3890
+ // GET …/sessions/:sid/workspace/capture. `{available:false}` when no capture row
3891
+ // exists yet (or its manifest blob was GC'd) — the client falls back to the
3892
+ // live/wake path (status-quo behavior, NEVER an error → served 200). When
3893
+ // available: the row metadata (revision/turn/epoch/stats/size) is always inline;
3894
+ // the manifest is inline (`manifest`) for the ≤2MB common case and a signed GET
3895
+ // URL (`manifestUrl`) above that. Exactly one of manifest/manifestUrl is non-null.
3896
+ export const GetWorkspaceCaptureResponse = z.discriminatedUnion("available", [
3897
+ z.object({
3898
+ available: z.literal(false),
3899
+ // Optional for additive compatibility with older servers. New servers set
3900
+ // these fields when the newest durable revision is an explicit degraded
3901
+ // marker rather than "no capture exists yet".
3902
+ degradedReason: WorkspaceCaptureDegradedReason.nullable().optional(),
3903
+ revision: z.number().int().nonnegative().nullable().optional(),
3904
+ capturedAt: z.string().nullable().optional(),
3905
+ turnId: z.string().nullable().optional(),
3906
+ leaseEpoch: z.number().int().nonnegative().nullable().optional(),
3907
+ }),
3908
+ z.object({
3909
+ available: z.literal(true),
3910
+ revision: z.number().int().nonnegative(),
3911
+ capturedAt: z.string(),
3912
+ turnId: z.string().nullable(),
3913
+ leaseEpoch: z.number().int().nonnegative(),
3914
+ sizeBytes: z.number().int().nonnegative(),
3915
+ stats: WorkspaceCaptureStats,
3916
+ manifest: WorkspaceCaptureManifest.nullable().default(null),
3917
+ manifestUrl: WorkspaceCaptureSignedUrl.nullable().default(null),
3918
+ }),
3919
+ ]);
3920
+ export type GetWorkspaceCaptureResponse = z.infer<typeof GetWorkspaceCaptureResponse>;
3921
+
3922
+ // GET …/sessions/:sid/workspace/capture/file?path=…&revision=…. A single
3923
+ // after-image resolved from the (revision|latest) manifest. The file metadata
3924
+ // (from the manifest entry) is always present; `content` is inline for ≤256KB
3925
+ // (base64 for binary, utf8 otherwise), else a signed GET URL (`contentUrl`) to
3926
+ // the raw content-addressed blob. A tooLarge marker (or a captured file with no
3927
+ // content blob — e.g. the after-image was GC'd) returns metadata only, no
3928
+ // content and no URL. Path-not-in-manifest / deleted → 404 at the route (not
3929
+ // represented here).
3930
+ export const GetWorkspaceCaptureFileResponse = z.object({
3931
+ path: z.string(),
3932
+ revision: z.number().int().nonnegative(),
3933
+ status: GitFileStatusCode,
3934
+ hash: z.string().nullable(),
3935
+ baseHash: z.string().nullable(),
3936
+ sizeBytes: z.number().int().nonnegative(),
3937
+ isBinary: z.boolean(),
3938
+ tooLarge: z.boolean(),
3939
+ encoding: FsEncoding.nullable().default(null), // set iff content is inline
3940
+ content: z.string().nullable().default(null), // inline ≤256KB (per encoding)
3941
+ contentUrl: WorkspaceCaptureSignedUrl.nullable().default(null), // signed >256KB
3942
+ });
3943
+ export type GetWorkspaceCaptureFileResponse = z.infer<typeof GetWorkspaceCaptureFileResponse>;
3944
+
2657
3945
  export const GitLogRequest = z.object({
2658
3946
  path: z.string().default(""),
2659
3947
  ref: z.string().default("HEAD"),
@@ -2681,13 +3969,25 @@ export const GitShowRequest = z.object({
2681
3969
  ref: z.string(), // a commit/tag/tree-ish
2682
3970
  filePath: z.string().optional(), // ref + filePath => raw blob ("open file at commit")
2683
3971
  encoding: FsEncoding.default("utf8"),
2684
- maxBytesPerFile: z.number().int().positive().max(2 * 1024 * 1024).default(512 * 1024),
3972
+ maxBytesPerFile: z
3973
+ .number()
3974
+ .int()
3975
+ .positive()
3976
+ .max(2 * 1024 * 1024)
3977
+ .default(512 * 1024),
2685
3978
  });
2686
3979
  export type GitShowRequest = z.infer<typeof GitShowRequest>;
2687
3980
  export const GitShowResponse = z.object({
2688
3981
  commit: GitCommit.nullable(), // null when fetching a raw blob
2689
3982
  files: z.array(GitFileDiff), // commit diff vs first parent
2690
- blob: z.object({ content: z.string(), encoding: FsEncoding, sizeBytes: z.number().int(), truncated: z.boolean() }).nullable(),
3983
+ blob: z
3984
+ .object({
3985
+ content: z.string(),
3986
+ encoding: FsEncoding,
3987
+ sizeBytes: z.number().int(),
3988
+ truncated: z.boolean(),
3989
+ })
3990
+ .nullable(),
2691
3991
  revision: z.number().int().nonnegative(),
2692
3992
  });
2693
3993
  export type GitShowResponse = z.infer<typeof GitShowResponse>;
@@ -2735,7 +4035,11 @@ export const PtyOpenResponse = z.object({
2735
4035
  export type PtyOpenResponse = z.infer<typeof PtyOpenResponse>;
2736
4036
  export const PtyWriteRequest = z.object({ ptyId: z.string().uuid(), data: z.string() }); // utf-8 stdin
2737
4037
  export type PtyWriteRequest = z.infer<typeof PtyWriteRequest>;
2738
- export const PtyResizeRequest = z.object({ ptyId: z.string().uuid(), cols: z.number().int().positive(), rows: z.number().int().positive() });
4038
+ export const PtyResizeRequest = z.object({
4039
+ ptyId: z.string().uuid(),
4040
+ cols: z.number().int().positive(),
4041
+ rows: z.number().int().positive(),
4042
+ });
2739
4043
  export type PtyResizeRequest = z.infer<typeof PtyResizeRequest>;
2740
4044
  export const PtyCloseRequest = z.object({ ptyId: z.string().uuid() });
2741
4045
  export type PtyCloseRequest = z.infer<typeof PtyCloseRequest>;
@@ -2764,10 +4068,36 @@ export const SessionEvent = z.object({
2764
4068
  occurredAt: z.string(),
2765
4069
  clientEventId: z.string().min(1).nullable().optional(),
2766
4070
  turnId: z.string().uuid().nullable().optional(),
4071
+ turnGeneration: z.number().int().nonnegative().nullable().optional(),
4072
+ turnAttemptId: z.string().uuid().nullable().optional(),
4073
+ turnAssociation: z.enum(["current", "late_rejected", "duplicate"]).nullable().optional(),
4074
+ duplicateOfEventId: z.string().uuid().nullable().optional(),
4075
+ duplicateReason: z.string().min(1).nullable().optional(),
2767
4076
  });
2768
4077
  export type SessionEvent = z.infer<typeof SessionEvent>;
2769
4078
 
2770
- export const CreateSessionRequest = z.object({
4079
+ export const SessionQueueMutationResponse = z.object({
4080
+ snapshot: SessionQueueSnapshot,
4081
+ events: z.array(SessionEvent),
4082
+ shouldWake: z.boolean(),
4083
+ });
4084
+ export type SessionQueueMutationResponse = z.infer<typeof SessionQueueMutationResponse>;
4085
+
4086
+ export const SessionControlResponse = z.object({
4087
+ operationId: z.string().uuid(),
4088
+ event: SessionEvent,
4089
+ controlState: SessionControlState,
4090
+ controlGeneration: z.number().int().nonnegative(),
4091
+ expectedActiveTurnId: z.string().uuid().nullable(),
4092
+ expectedExecutionGeneration: z.number().int().nonnegative().nullable(),
4093
+ expectedAttemptId: z.string().uuid().nullable(),
4094
+ deliveryEventId: z.string().uuid().nullable(),
4095
+ shouldSignalControl: z.boolean(),
4096
+ shouldWake: z.boolean(),
4097
+ });
4098
+ export type SessionControlResponse = z.infer<typeof SessionControlResponse>;
4099
+
4100
+ export const CreateSessionRequest = withVariableSetIdAlias({
2771
4101
  initialMessage: z.string().min(1),
2772
4102
  // Per-session agent persona/system instructions (org-visible metadata, NOT a
2773
4103
  // secret). Rides the SAME system-level instructions channel the per-workspace
@@ -2776,7 +4106,7 @@ export const CreateSessionRequest = z.object({
2776
4106
  // leaking them into the user-visible timeline (it is NEVER emitted as an
2777
4107
  // event, unlike goal/initialMessage). Trimmed, non-empty. The 32768-char cap
2778
4108
  // matches the codebase's largest free-form string convention (workspace
2779
- // environment variable values). Absent ⇒ byte-identical to today.
4109
+ // variable set variable values). Absent ⇒ byte-identical to today.
2780
4110
  instructions: z.string().trim().min(1).max(32768).optional(),
2781
4111
  resources: z.array(ResourceRef).default([]),
2782
4112
  tools: z.array(ToolRef).default([]),
@@ -2795,9 +4125,15 @@ export const CreateSessionRequest = z.object({
2795
4125
  // (the agent's resolve_cwd handles both). Only valid WITH targetSandboxId
2796
4126
  // (workingDir alone is a 422); omitted ⇒ the machine's default workspace_root.
2797
4127
  workingDir: z.string().min(1).optional(),
2798
- // Workspace environment attachment is fixed at session creation; follow-up
4128
+ // Variable set attachment is fixed at session creation; follow-up
2799
4129
  // user.message events cannot switch or add one.
4130
+ variableSetId: z.string().uuid().optional(),
2800
4131
  environmentId: z.string().uuid().optional(),
4132
+ // The rig to bind this session to (M3). Its ACTIVE version is resolved and
4133
+ // FROZEN onto the session at create. Omitted ⇒ the workspace's default rig
4134
+ // (workspaces.default_rig_id) when set, else a rig-less session (today's
4135
+ // behavior). An id that does not name a rig in the workspace is a 422.
4136
+ rigId: z.string().uuid().optional(),
2801
4137
  goal: GoalSpec.optional(),
2802
4138
  clientEventId: z.string().min(1).optional(),
2803
4139
  // Workspace-scoped CREATE idempotency key: collapses concurrent/retried
@@ -2809,7 +4145,7 @@ export const CreateSessionRequest = z.object({
2809
4145
  idempotencyKey: z.string().min(1).max(200).optional(),
2810
4146
  // Permissions the session's first-party MCP token should carry instead of
2811
4147
  // the fixed worker default — how an operator hands a manager-style session
2812
- // the orchestration/environment/github tools. Capped at creation: every
4148
+ // the orchestration/variableSet/github tools. Capped at creation: every
2813
4149
  // requested permission must be held by the creating grant (no escalation).
2814
4150
  firstPartyMcpPermissions: z.array(Permission).optional(),
2815
4151
  // Third-party MCP servers attached only to this session. Credential headers are
@@ -2827,16 +4163,14 @@ export const CreateSessionRequest = z.object({
2827
4163
  // A shared spawn inherits the box's (backend, os) — it is literally the same
2828
4164
  // box; the child cannot pick its own backend. Cross-workspace sharing is
2829
4165
  // forbidden by construction (the parent/group reads are RLS-workspace-scoped).
2830
- // ENV-AWARE: the box's environment is fixed at creation, so a share requires
2831
- // the SAME environmentId as the creator's box. On a mismatch the inherited
4166
+ // ENV-AWARE: the box's variable set is fixed at creation, so a share requires
4167
+ // the SAME variableSetId as the creator's box. On a mismatch the inherited
2832
4168
  // default silently falls back to an own box; an explicit "shared"/{groupId}
2833
4169
  // request 422s at create (instead of the first turn dying on the SDK's
2834
4170
  // manifest-env guard).
2835
- sandbox: z.union([
2836
- z.literal("shared"),
2837
- z.literal("new"),
2838
- z.object({ groupId: z.string().uuid() }),
2839
- ]).optional(),
4171
+ sandbox: z
4172
+ .union([z.literal("shared"), z.literal("new"), z.object({ groupId: z.string().uuid() })])
4173
+ .optional(),
2840
4174
  });
2841
4175
  export type CreateSessionRequest = z.infer<typeof CreateSessionRequest>;
2842
4176
 
@@ -2855,11 +4189,6 @@ export const ClientSessionEvent = z.discriminatedUnion("type", [
2855
4189
  mcpCredentialUpdates: z.array(SessionMcpCredentialUpdateInput).optional(),
2856
4190
  }),
2857
4191
  }),
2858
- z.object({
2859
- type: z.literal("user.interrupt"),
2860
- clientEventId: z.string().min(1).optional(),
2861
- payload: z.object({ reason: z.string().optional() }).default({}),
2862
- }),
2863
4192
  z.object({
2864
4193
  type: z.literal("user.approvalDecision"),
2865
4194
  clientEventId: z.string().min(1).optional(),
@@ -2872,6 +4201,25 @@ export const ClientSessionEvent = z.discriminatedUnion("type", [
2872
4201
  ]);
2873
4202
  export type ClientSessionEvent = z.infer<typeof ClientSessionEvent>;
2874
4203
 
4204
+ export const SteerSessionMessageRequest = z.object({
4205
+ text: z.string().min(1),
4206
+ resources: z.array(ResourceRef).default([]),
4207
+ tools: z.array(ToolRef).default([]),
4208
+ model: z.string().min(1).optional(),
4209
+ reasoningEffort: ReasoningEffort.optional(),
4210
+ clientEventId: z.string().min(1).optional(),
4211
+ expectedControlGeneration: z.number().int().nonnegative().optional(),
4212
+ expectedWorkspaceInferenceGeneration: z.number().int().nonnegative().optional(),
4213
+ mcpCredentialUpdates: z.array(SessionMcpCredentialUpdateInput).optional(),
4214
+ });
4215
+ export type SteerSessionMessageRequest = z.infer<typeof SteerSessionMessageRequest>;
4216
+
4217
+ export const SteerSessionMessageResponse = z.object({
4218
+ accepted: SessionEvent,
4219
+ turn: SessionTurn,
4220
+ });
4221
+ export type SteerSessionMessageResponse = z.infer<typeof SteerSessionMessageResponse>;
4222
+
2875
4223
  export const SessionBusMessage = z.object({
2876
4224
  workspaceId: z.string().uuid(),
2877
4225
  sessionId: z.string().uuid(),
@@ -3175,7 +4523,13 @@ export const DeviceEnrollmentPollRequest = z.object({
3175
4523
  });
3176
4524
  export type DeviceEnrollmentPollRequest = z.infer<typeof DeviceEnrollmentPollRequest>;
3177
4525
 
3178
- export const DeviceEnrollmentState = z.enum(["pending", "authorized", "denied", "expired", "disabled"]);
4526
+ export const DeviceEnrollmentState = z.enum([
4527
+ "pending",
4528
+ "authorized",
4529
+ "denied",
4530
+ "expired",
4531
+ "disabled",
4532
+ ]);
3179
4533
  export type DeviceEnrollmentState = z.infer<typeof DeviceEnrollmentState>;
3180
4534
 
3181
4535
  // The EnrollmentCredentials (field names match the proto's JSON). natsAccountCreds
@@ -3227,6 +4581,11 @@ export const EnrollmentSummary = z.object({
3227
4581
  pubkey: z.string(),
3228
4582
  exposure: z.literal("whole-machine"),
3229
4583
  hasDisplay: z.boolean(),
4584
+ // Present (non-null) only when a display EXISTS but capture is blocked (macOS
4585
+ // Screen Recording / TCC not granted): a human, actionable reason so the UI can
4586
+ // show "display: capture not granted" instead of a bare "headless". null == capture
4587
+ // permitted OR genuinely headless.
4588
+ desktopUnavailableReason: z.string().nullish(),
3230
4589
  allowScreenControl: z.boolean(),
3231
4590
  status: z.enum(["active", "revoked"]),
3232
4591
  os: EnrollmentOs,
@@ -3407,6 +4766,10 @@ export const MachineView = z.object({
3407
4766
  os: z.string(),
3408
4767
  arch: z.string(),
3409
4768
  hasDisplay: z.boolean(),
4769
+ // Non-null only when a display exists but capture is blocked (macOS Screen
4770
+ // Recording / TCC not granted) — the UI can surface "display: capture not granted".
4771
+ // null == capture permitted OR headless.
4772
+ desktopUnavailableReason: z.string().nullish(),
3410
4773
  allowScreenControl: z.boolean(),
3411
4774
  sharedSessionCount: z.number().int(),
3412
4775
  lastSeenAt: z.string().nullable(),
@@ -3448,6 +4811,19 @@ export const SwapActiveSandboxResponse = z.object({
3448
4811
  activeSandboxId: z.string().nullable(),
3449
4812
  activeEpoch: z.number().int(),
3450
4813
  reason: z.string().optional(),
4814
+ // Typed rejection discriminant (issue #341). Present only when swapped is false,
4815
+ // so a client distinguishes a deleted/absent target from an unaddressable
4816
+ // enrollment from a backend the turn cannot establish from a lost epoch race —
4817
+ // without parsing the human reason string.
4818
+ code: z
4819
+ .enum([
4820
+ "stale_pointer",
4821
+ "offline_enrollment",
4822
+ "unsupported_backend_context",
4823
+ "transient_establishment",
4824
+ "concurrent_swap",
4825
+ ])
4826
+ .optional(),
3451
4827
  });
3452
4828
  export type SwapActiveSandboxResponse = z.infer<typeof SwapActiveSandboxResponse>;
3453
4829
 
@@ -3470,7 +4846,7 @@ export type MachineMetricsSeriesResponse = z.infer<typeof MachineMetricsSeriesRe
3470
4846
  export const ClientModel = z.object({
3471
4847
  id: z.string(),
3472
4848
  label: z.string(),
3473
- provider: z.string(), // provider id
4849
+ provider: z.string(), // provider id
3474
4850
  providerLabel: z.string(),
3475
4851
  api: z.enum(["responses", "chat"]),
3476
4852
  contextWindowTokens: z.number().int().positive().optional(),
@@ -3490,10 +4866,14 @@ export const ClientConfig = z.object({
3490
4866
  models: z.array(ClientModel).default([]),
3491
4867
  defaultReasoningEffort: ReasoningEffort,
3492
4868
  allowedReasoningEfforts: z.array(ReasoningEffort).min(1),
3493
- mcpServers: z.array(z.object({
3494
- id: z.string(),
3495
- name: z.string(),
3496
- })).default([]),
4869
+ mcpServers: z
4870
+ .array(
4871
+ z.object({
4872
+ id: z.string(),
4873
+ name: z.string(),
4874
+ }),
4875
+ )
4876
+ .default([]),
3497
4877
  fileUploads: z.object({
3498
4878
  enabled: z.boolean(),
3499
4879
  maxSizeBytes: z.number().int().positive(),
@@ -3504,11 +4884,13 @@ export const ClientConfig = z.object({
3504
4884
  // at all (P4.4). Per-session availability is negotiated on /stream-capabilities
3505
4885
  // (it depends on the session's pinned backend); this is the coarse on/off the
3506
4886
  // client uses to decide whether to even attempt the fs/git/terminal panels.
3507
- structuredServices: z.object({
3508
- fileSystem: z.boolean(),
3509
- git: z.boolean(),
3510
- terminalEvents: z.boolean(),
3511
- }).default({ fileSystem: false, git: false, terminalEvents: false }),
4887
+ structuredServices: z
4888
+ .object({
4889
+ fileSystem: z.boolean(),
4890
+ git: z.boolean(),
4891
+ terminalEvents: z.boolean(),
4892
+ })
4893
+ .default({ fileSystem: false, git: false, terminalEvents: false }),
3512
4894
  });
3513
4895
  export type ClientConfig = z.infer<typeof ClientConfig>;
3514
4896
 
@@ -3547,6 +4929,42 @@ function constantTimeEqual(actual: string, expected: string): boolean {
3547
4929
 
3548
4930
  export type HealthResponse = {
3549
4931
  service: string;
3550
- environment: string;
4932
+ variableSet: string;
3551
4933
  ok: boolean;
3552
4934
  };
4935
+
4936
+ /**
4937
+ * Per-workspace model/provider availability policy (see @opengeni/db
4938
+ * workspace_model_policies). NULL fields = unrestricted; a non-null
4939
+ * allowedProviders is a strict allowlist over RESOLVED provider identities
4940
+ * (the built-in OpenAI/Azure client's id — "openai"/"azure" — including the
4941
+ * legacy null-resolution fallback; "codex-subscription" for the ChatGPT/Codex
4942
+ * overlay; registry providers by their declared ids); a non-null allowedModels
4943
+ * is an additional exact-model-id allowlist. Pure and shared so the API edge
4944
+ * (422) and the worker's authoritative post-resolution gate (fail-loud
4945
+ * turn.failed, never a silent remap) can never disagree on semantics.
4946
+ */
4947
+ export type WorkspaceModelPolicyContract = {
4948
+ allowedProviders: string[] | null;
4949
+ allowedModels: string[] | null;
4950
+ };
4951
+
4952
+ export type WorkspaceModelPolicyVerdict =
4953
+ | { allowed: true }
4954
+ | { allowed: false; reason: "provider" | "model" };
4955
+
4956
+ export function evaluateWorkspaceModelPolicy(
4957
+ policy: WorkspaceModelPolicyContract | null | undefined,
4958
+ candidate: { providerId: string; modelId: string },
4959
+ ): WorkspaceModelPolicyVerdict {
4960
+ if (!policy) {
4961
+ return { allowed: true };
4962
+ }
4963
+ if (policy.allowedProviders !== null && !policy.allowedProviders.includes(candidate.providerId)) {
4964
+ return { allowed: false, reason: "provider" };
4965
+ }
4966
+ if (policy.allowedModels !== null && !policy.allowedModels.includes(candidate.modelId)) {
4967
+ return { allowed: false, reason: "model" };
4968
+ }
4969
+ return { allowed: true };
4970
+ }