@opengeni/contracts 0.15.0 → 0.18.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
@@ -22,6 +22,30 @@ export {
22
22
  type SessionEventPayloadTruncation,
23
23
  } from "./event-preview";
24
24
 
25
+ export {
26
+ RETAINED_OUTPUT_DEFAULT_PAGE_BYTES,
27
+ RETAINED_OUTPUT_MAX_PAGE_BYTES,
28
+ RETAINED_OUTPUT_RECEIPT_MAX_BYTES,
29
+ RetainedArtifactMetadataSchema,
30
+ RetainedArtifactReferenceSchema,
31
+ RetainedArtifactUnavailableSchema,
32
+ RetainedOutputEvidenceSchema,
33
+ RetainedOutputKind,
34
+ RetainedOutputUnavailableReason,
35
+ retainedArtifactReferenceFromFile,
36
+ retainedOutputUnavailable,
37
+ resolveRetainedOutputRange,
38
+ validateRetainedOutputEvidence,
39
+ type RetainedArtifactFileInput,
40
+ type RetainedArtifactMetadata,
41
+ type RetainedArtifactReference,
42
+ type RetainedArtifactUnavailable,
43
+ type RetainedOutputAvailableEvidence,
44
+ type RetainedOutputEvidence,
45
+ type RetainedOutputRangeResolution,
46
+ type RetainedOutputResolvedRange,
47
+ } from "./retained-output";
48
+
25
49
  export const SessionStatus = z.enum([
26
50
  "queued",
27
51
  "running",
@@ -535,6 +559,30 @@ export const Permission = z.enum([
535
559
  ]);
536
560
  export type Permission = z.infer<typeof Permission>;
537
561
 
562
+ /**
563
+ * Capability-first permissions signed into a session's first-party OpenGeni
564
+ * MCP token when a top-level creator does not explicitly narrow them.
565
+ *
566
+ * Keep this contract shared by admission and runtime signing: a worker-signed
567
+ * child whose parent was narrowed must inherit the parent's effective subset,
568
+ * never fall back to a different runtime-local default.
569
+ */
570
+ export const DEFAULT_FIRST_PARTY_MCP_PERMISSIONS = [
571
+ "workspace:read",
572
+ "files:read",
573
+ "documents:search",
574
+ "scheduled_tasks:manage",
575
+ "scheduled_tasks:run",
576
+ "goals:manage",
577
+ "sessions:read",
578
+ "sessions:create",
579
+ "sessions:control",
580
+ "variable-sets:use",
581
+ "variable-sets:manage",
582
+ "rigs:use",
583
+ "github:use",
584
+ ] as const satisfies readonly Permission[];
585
+
538
586
  export function prefixedMcpToolName(registryId: string, toolName: string): string {
539
587
  return `${registryId}__${toolName}`;
540
588
  }
@@ -1737,11 +1785,39 @@ export type GitCredentialsRequest = {
1737
1785
  repositoryIds: number[];
1738
1786
  };
1739
1787
 
1788
+ /**
1789
+ * One exact repository route exposed by a host-owned HTTPS smart-Git broker.
1790
+ *
1791
+ * `repositoryUri` must echo one URI from the request's `repositoryRefs`.
1792
+ * `brokerUri` is a stable, credential-free HTTPS remote. The rotating bearer
1793
+ * remains separate in `GitCredentials.token`, so it cannot leak through Git
1794
+ * configuration, provider-CLI arguments, manifests, or repository metadata.
1795
+ */
1796
+ export type GitHttpBrokerRepositoryRoute = {
1797
+ repositoryUri: string;
1798
+ brokerUri: string;
1799
+ };
1800
+
1801
+ /**
1802
+ * Optional transport override for credentials that cannot be safely narrowed
1803
+ * into a provider token. Omission retains the provider-token behavior.
1804
+ */
1805
+ export type GitCredentialTransport = {
1806
+ kind: "http_broker";
1807
+ repositories: GitHttpBrokerRepositoryRoute[];
1808
+ };
1809
+
1740
1810
  export type GitCredentials = {
1741
- // The minted provider token. Required for purpose="token"; optional for
1742
- // purpose="identity" so hosts can return only stable git identity before lazy
1743
- // sandbox provision. The value never enters the manifest.
1811
+ // The minted secret. For the default transport this is a provider token; for
1812
+ // `http_broker` it is the broker bearer. Required for purpose="token";
1813
+ // optional for purpose="identity" so hosts can return only stable git identity
1814
+ // before lazy sandbox provision. The value never enters the manifest.
1744
1815
  token?: string;
1816
+ // A host-owned exact smart-Git transport for providers whose available token
1817
+ // cannot be constrained to the selected repositories. OpenGeni rewrites only
1818
+ // the echoed repository remotes and never exposes this bearer to provider
1819
+ // CLIs. Omitted means the token is a direct provider credential.
1820
+ transport?: GitCredentialTransport;
1745
1821
  // workspace-scope cross-check echo: the workspace the provider scoped this token to. The activity
1746
1822
  // asserts `workspaceId === request.workspaceId` before injecting.
1747
1823
  workspaceId: string;
@@ -1964,6 +2040,8 @@ export type McpCredentialsRequest = {
1964
2040
  /** Immediate technical caller, retained only as non-authoritative audit context. */
1965
2041
  callerSubjectId?: string;
1966
2042
  surface: "model" | "toolspace";
2043
+ /** Canonical MCP destination that will receive the resolved headers. */
2044
+ destinationUrl: string;
1967
2045
  serverId: string;
1968
2046
  toolName?: string;
1969
2047
  connectionRef: McpServerConnectionRef;
@@ -2604,6 +2682,63 @@ export const ToolRef = z.object({
2604
2682
  export type ToolRef = z.infer<typeof ToolRef>;
2605
2683
 
2606
2684
  const registryId = /^[A-Za-z0-9_-]+$/;
2685
+ export const SessionMcpServerId = z.string().min(1).regex(registryId);
2686
+ export type SessionMcpServerId = z.infer<typeof SessionMcpServerId>;
2687
+
2688
+ // How a session's persisted `tools` snapshot was selected. `legacy` is
2689
+ // reserved for rows written before this descriptor existed; those rows must
2690
+ // keep their materialized historical allow-list rather than being guessed to
2691
+ // mean either omitted or explicitly empty.
2692
+ export const SessionToolPolicy = z.object({
2693
+ mode: z.enum(["workspace_default", "explicit", "inherited", "legacy"]),
2694
+ inheritedFromSessionId: z.string().uuid().nullable(),
2695
+ });
2696
+ export type SessionToolPolicy = z.infer<typeof SessionToolPolicy>;
2697
+
2698
+ export const SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT = 64;
2699
+ export const SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH = 200;
2700
+ const SessionEffectiveToolPolicyId = z
2701
+ .string()
2702
+ .min(1)
2703
+ .max(SESSION_EFFECTIVE_TOOL_POLICY_ID_MAX_LENGTH)
2704
+ .regex(registryId);
2705
+ const SessionEffectiveToolPolicyIds = z
2706
+ .array(SessionEffectiveToolPolicyId)
2707
+ .max(SESSION_EFFECTIVE_TOOL_POLICY_ID_LIMIT);
2708
+
2709
+ // Secret-safe, read-time policy truth. This projection contains only bounded
2710
+ // MCP registry ids and exact counts: never URLs, names, headers, credentials,
2711
+ // connector configuration, or tool schemas. IDs are samples when capped;
2712
+ // counts remain exact and idsTruncated makes that explicit to clients.
2713
+ export const SessionEffectiveToolPolicy = z
2714
+ .object({
2715
+ mode: z.enum(["workspace_default", "explicit", "inherited", "legacy"]),
2716
+ inheritedFromSessionId: z.string().uuid().nullable(),
2717
+ selectedIds: SessionEffectiveToolPolicyIds,
2718
+ effectiveIds: SessionEffectiveToolPolicyIds,
2719
+ mandatoryIds: SessionEffectiveToolPolicyIds,
2720
+ lazyRouter: z
2721
+ .object({
2722
+ state: z.enum(["required", "disabled"]),
2723
+ deferredIds: SessionEffectiveToolPolicyIds,
2724
+ })
2725
+ .strict(),
2726
+ configuredIds: SessionEffectiveToolPolicyIds,
2727
+ droppedIds: SessionEffectiveToolPolicyIds,
2728
+ counts: z
2729
+ .object({
2730
+ selected: z.number().int().nonnegative(),
2731
+ effective: z.number().int().nonnegative(),
2732
+ mandatory: z.number().int().nonnegative(),
2733
+ deferred: z.number().int().nonnegative(),
2734
+ configured: z.number().int().nonnegative(),
2735
+ dropped: z.number().int().nonnegative(),
2736
+ })
2737
+ .strict(),
2738
+ idsTruncated: z.boolean(),
2739
+ })
2740
+ .strict();
2741
+ export type SessionEffectiveToolPolicy = z.infer<typeof SessionEffectiveToolPolicy>;
2607
2742
  const httpsUrl = z
2608
2743
  .string()
2609
2744
  .url()
@@ -2618,19 +2753,54 @@ const httpsUrl = z
2618
2753
  { message: "URL must use https" },
2619
2754
  );
2620
2755
 
2756
+ /**
2757
+ * Human-approval policy for one MCP server. `true` gates every tool, `false`
2758
+ * gates none, and a list gates only those unprefixed names.
2759
+ */
2760
+ export const SESSION_MCP_APPROVAL_POLICY_MAX_TOOL_NAMES = 2_048;
2761
+ export const SESSION_MCP_APPROVAL_POLICY_MAX_BYTES = 256 * 1024;
2762
+ export const SESSION_MCP_APPROVAL_TOOL_NAME_MAX_BYTES = 1_024;
2763
+ export const SESSION_MCP_SERVERS_MAX = 64;
2764
+
2765
+ const sessionMcpApprovalToolName = z
2766
+ .string()
2767
+ .min(1)
2768
+ .superRefine((name, ctx) => {
2769
+ if (new TextEncoder().encode(name).byteLength > SESSION_MCP_APPROVAL_TOOL_NAME_MAX_BYTES) {
2770
+ ctx.addIssue({
2771
+ code: z.ZodIssueCode.custom,
2772
+ message: `MCP approval tool names must be at most ${SESSION_MCP_APPROVAL_TOOL_NAME_MAX_BYTES} UTF-8 bytes`,
2773
+ });
2774
+ }
2775
+ });
2776
+ const selectiveSessionMcpApprovalPolicy = z
2777
+ .array(sessionMcpApprovalToolName)
2778
+ .max(SESSION_MCP_APPROVAL_POLICY_MAX_TOOL_NAMES)
2779
+ .superRefine((names, ctx) => {
2780
+ const bytes = names.reduce(
2781
+ (total, name) => total + new TextEncoder().encode(name).byteLength,
2782
+ 0,
2783
+ );
2784
+ if (bytes > SESSION_MCP_APPROVAL_POLICY_MAX_BYTES) {
2785
+ ctx.addIssue({
2786
+ code: z.ZodIssueCode.custom,
2787
+ message: `MCP approval policies must be at most ${SESSION_MCP_APPROVAL_POLICY_MAX_BYTES} UTF-8 bytes`,
2788
+ });
2789
+ }
2790
+ })
2791
+ .transform((names) => [...new Set(names)].sort());
2792
+ export const SessionMcpApprovalPolicy = z.union([z.boolean(), selectiveSessionMcpApprovalPolicy]);
2793
+ export type SessionMcpApprovalPolicy = z.infer<typeof SessionMcpApprovalPolicy>;
2794
+
2621
2795
  export const SessionMcpServerInput = z.object({
2622
- id: z.string().min(1).regex(registryId),
2796
+ id: SessionMcpServerId,
2623
2797
  name: z.string().min(1).optional(),
2624
2798
  url: httpsUrl,
2625
2799
  allowedTools: z.array(z.string().min(1)).optional(),
2626
2800
  timeoutMs: z.number().int().positive().optional(),
2627
2801
  cacheToolsList: z.boolean().optional(),
2628
- // Human-approval policy for this server's tools. `true` = every tool of this
2629
- // server requires approval before it runs (a `session.requiresAction` pause
2630
- // the caller resolves with `user.approvalDecision`); a string[] = ONLY the
2631
- // listed UNPREFIXED tool names require approval (e.g. reads auto-run, writes
2632
- // ask); absent / `false` = auto-run everything (the historical default).
2633
- requireApproval: z.union([z.boolean(), z.array(z.string().min(1))]).optional(),
2802
+ // The caller resolves an approval pause with `user.approvalDecision`.
2803
+ requireApproval: SessionMcpApprovalPolicy.optional(),
2634
2804
  // Write-only credential headers. Values are encrypted at rest and never
2635
2805
  // returned in session responses or events; response metadata exposes names.
2636
2806
  headers: z.record(z.string(), z.string()).optional(),
@@ -2641,23 +2811,43 @@ export const SessionMcpServerInput = z.object({
2641
2811
  export type SessionMcpServerInput = z.infer<typeof SessionMcpServerInput>;
2642
2812
 
2643
2813
  export const SessionMcpCredentialUpdateInput = z.object({
2644
- id: z.string().min(1).regex(registryId),
2814
+ id: SessionMcpServerId,
2645
2815
  headers: z.record(z.string(), z.string()),
2646
2816
  });
2647
2817
  export type SessionMcpCredentialUpdateInput = z.infer<typeof SessionMcpCredentialUpdateInput>;
2648
2818
 
2649
2819
  export const SessionMcpServerMetadata = z
2650
2820
  .object({
2651
- id: z.string().min(1).regex(registryId),
2821
+ id: SessionMcpServerId,
2652
2822
  name: z.string().min(1).nullable(),
2653
2823
  url: httpsUrl,
2654
2824
  headerNames: z.array(z.string()).default([]),
2655
2825
  credentialVersion: z.number().int().positive(),
2826
+ requireApproval: SessionMcpApprovalPolicy.default(false),
2656
2827
  connectionRef: McpServerConnectionRef.nullable().default(null),
2657
2828
  })
2658
2829
  .strict();
2659
2830
  export type SessionMcpServerMetadata = z.infer<typeof SessionMcpServerMetadata>;
2660
2831
 
2832
+ export const UpdateSessionMcpApprovalPolicyRequest = z
2833
+ .object({
2834
+ requireApproval: SessionMcpApprovalPolicy,
2835
+ })
2836
+ .strict();
2837
+ export type UpdateSessionMcpApprovalPolicyRequest = z.infer<
2838
+ typeof UpdateSessionMcpApprovalPolicyRequest
2839
+ >;
2840
+
2841
+ export const UpdateSessionMcpApprovalPolicyResponse = z
2842
+ .object({
2843
+ server: SessionMcpServerMetadata,
2844
+ effectiveFrom: z.literal("next_attempt"),
2845
+ })
2846
+ .strict();
2847
+ export type UpdateSessionMcpApprovalPolicyResponse = z.infer<
2848
+ typeof UpdateSessionMcpApprovalPolicyResponse
2849
+ >;
2850
+
2661
2851
  export class ResourceRefConflictError extends Error {
2662
2852
  constructor(message: string) {
2663
2853
  super(message);
@@ -3001,6 +3191,7 @@ export const SessionAuthorizationOperation = z.enum([
3001
3191
  "session.human_input.read",
3002
3192
  "session.human_input.write",
3003
3193
  "session.title.write",
3194
+ "session.mcp.approval_policy.write",
3004
3195
  "session.goal.read",
3005
3196
  "session.goal.write",
3006
3197
  "session.child.create",
@@ -3115,6 +3306,10 @@ export const SessionTurn = z.object({
3115
3306
  prompt: z.string().min(1),
3116
3307
  resources: z.array(ResourceRef),
3117
3308
  tools: z.array(ToolRef),
3309
+ // Omitted/default discovery and explicit `tools: []` are distinct. False
3310
+ // inherits the durable session policy; true replaces it for this turn after
3311
+ // admission proves the selection is a subset.
3312
+ toolsProvided: z.boolean().optional(),
3118
3313
  model: z.string().min(1),
3119
3314
  reasoningEffort: ReasoningEffort,
3120
3315
  sandboxBackend: SandboxBackend,
@@ -3204,6 +3399,9 @@ export const ComposerDraft = z.object({
3204
3399
  text: z.string(),
3205
3400
  resources: z.array(ResourceRef),
3206
3401
  tools: z.array(ToolRef),
3402
+ // False means the draft inherits the session policy. True preserves an
3403
+ // explicit array, including [], across autosave/reload and queue checkout.
3404
+ toolsProvided: z.boolean().default(false),
3207
3405
  model: z.string().min(1),
3208
3406
  reasoningEffort: ReasoningEffort,
3209
3407
  sourceTurnId: z.string().uuid().nullable(),
@@ -3260,6 +3458,7 @@ export const SaveComposerDraftRequest = ComposerDraft.pick({
3260
3458
  text: true,
3261
3459
  resources: true,
3262
3460
  tools: true,
3461
+ toolsProvided: true,
3263
3462
  model: true,
3264
3463
  reasoningEffort: true,
3265
3464
  }).extend({ expectedRevision: z.number().int().nonnegative() });
@@ -4355,6 +4554,19 @@ export const CapabilityRuntime = z.object({
4355
4554
  mcpServerId: z.string().min(1).optional(),
4356
4555
  transport: z.string().min(1).optional(),
4357
4556
  notes: z.string().nullable().default(null),
4557
+ // Registry exposure provenance is server-derived and contains no endpoint or
4558
+ // credential material.
4559
+ catalogTrust: z
4560
+ .object({
4561
+ state: z.enum(["trusted", "legacy_active", "unverified"]),
4562
+ reason: z.enum([
4563
+ "trusted_source",
4564
+ "verified_probe",
4565
+ "active_installation_compatibility",
4566
+ "missing_verification",
4567
+ ]),
4568
+ })
4569
+ .optional(),
4358
4570
  });
4359
4571
  export type CapabilityRuntime = z.infer<typeof CapabilityRuntime>;
4360
4572
 
@@ -4406,6 +4618,34 @@ export const CapabilityCatalogItem = z.object({
4406
4618
  });
4407
4619
  export type CapabilityCatalogItem = z.infer<typeof CapabilityCatalogItem>;
4408
4620
 
4621
+ /**
4622
+ * Shared trust gate for catalog visibility and runtime selection. Registry rows
4623
+ * remain durable for provenance and audit, but only a reviewed real-MCP probe
4624
+ * with known authentication is exposable. API-key rows additionally need a
4625
+ * machine-actionable header contract; prose credential instructions are not a
4626
+ * runtime contract and must fail closed.
4627
+ */
4628
+ export function capabilityCatalogItemIsTrustedForExposure(
4629
+ item: Pick<CapabilityCatalogItem, "source" | "stale" | "authKind" | "metadata">,
4630
+ ): boolean {
4631
+ if (item.stale) return false;
4632
+ if (item.source !== "registry") return true;
4633
+ const probe = item.metadata.mcpProbe;
4634
+ if (!probe || typeof probe !== "object" || Array.isArray(probe)) return false;
4635
+ if ((probe as Record<string, unknown>).status !== "real") return false;
4636
+ if (item.authKind === null || item.authKind === "unknown") return false;
4637
+ if (item.authKind !== "api_key") return true;
4638
+ const contract = item.metadata.authContract;
4639
+ if (!contract || typeof contract !== "object" || Array.isArray(contract)) return false;
4640
+ const record = contract as Record<string, unknown>;
4641
+ return (
4642
+ typeof record.headerName === "string" &&
4643
+ /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(record.headerName) &&
4644
+ typeof record.scheme === "string" &&
4645
+ record.scheme.trim().length > 0
4646
+ );
4647
+ }
4648
+
4409
4649
  export const CapabilityInstallation = z.object({
4410
4650
  id: z.string().uuid(),
4411
4651
  accountId: z.string().uuid(),
@@ -4485,6 +4725,13 @@ export const Session = z.object({
4485
4725
  instructions: z.string().nullable(),
4486
4726
  resources: z.array(ResourceRef),
4487
4727
  tools: z.array(ToolRef),
4728
+ // Origin of the persisted tool allow-list. Optional for rolling client
4729
+ // compatibility; current servers emit it and legacy rows map to `legacy`.
4730
+ toolPolicy: SessionToolPolicy.optional(),
4731
+ // Secret-safe current resolution, computed at an API/read or execution
4732
+ // boundary from IDs only. Optional because internal DB readers need not load
4733
+ // the workspace runtime registry.
4734
+ effectiveToolPolicy: SessionEffectiveToolPolicy.optional(),
4488
4735
  metadata: z.record(z.string(), z.unknown()),
4489
4736
  /** Frozen creator fact used only for creation attribution/idempotent repair. */
4490
4737
  createdBy: TurnInitiator,
@@ -4656,6 +4903,9 @@ export const SessionEventType = z.enum([
4656
4903
  "agent.reasoning.delta",
4657
4904
  "agent.toolCall.created",
4658
4905
  "agent.toolCall.output",
4906
+ // Attempt-fenced Codex Responses lifecycle metadata (request identity,
4907
+ // deadlines, first-byte/terminal phase, provider request id). Never body/auth.
4908
+ "agent.model.request",
4659
4909
  "agent.model.usage",
4660
4910
  "tool.auth_needed",
4661
4911
  "credential.auth_needed",
@@ -4719,6 +4969,7 @@ export const SessionEventType = z.enum([
4719
4969
  "terminal.pty.output.delta", // PTY stdout/stderr bytes (separate from command.output)
4720
4970
  "terminal.pty.exited", // PTY session ended (exitCode/reason)
4721
4971
  "session.title_set",
4972
+ "session.mcp.approval_policy.updated",
4722
4973
  // Multi-account Codex (P1): the account a session's turn runs on changed
4723
4974
  // (manual switch in P1; failover/rotation in P3 reuse the same event). Drives
4724
4975
  // the in-session "Running on:" indicator's live flip.
@@ -4813,9 +5064,35 @@ export const SessionEventSemanticClass = z.enum([
4813
5064
  ]);
4814
5065
  export type SessionEventSemanticClass = z.infer<typeof SessionEventSemanticClass>;
4815
5066
 
5067
+ /**
5068
+ * The semantic classes accepted by an exclusive latest lookup. `receipt` is
5069
+ * the concise public spelling for the historical `tool_receipt` class; the
5070
+ * latter remains accepted everywhere for backwards compatibility.
5071
+ */
5072
+ export const SessionEventLatestClass = z.enum([
5073
+ "control",
5074
+ "terminal",
5075
+ "failure",
5076
+ "checkpoint",
5077
+ "tool_receipt",
5078
+ "provider_account",
5079
+ "receipt",
5080
+ ]);
5081
+ export type SessionEventLatestClass = z.infer<typeof SessionEventLatestClass>;
5082
+
5083
+ export function sessionEventLatestClassToSemanticClass(
5084
+ value: SessionEventLatestClass,
5085
+ ): SessionEventSemanticClass {
5086
+ return value === "receipt" ? "tool_receipt" : value;
5087
+ }
5088
+
4816
5089
  export const SessionEventPayloadMode = z.enum(["none", "summary", "full"]);
4817
5090
  export type SessionEventPayloadMode = z.infer<typeof SessionEventPayloadMode>;
4818
5091
 
5092
+ /** Select the compact semantic-result projection instead of an event array. */
5093
+ export const SessionEventResultMode = z.enum(["events", "compact"]);
5094
+ export type SessionEventResultMode = z.infer<typeof SessionEventResultMode>;
5095
+
4819
5096
  export const SessionEventReadMode = z.enum(["monitoring", "forensic"]);
4820
5097
  export type SessionEventReadMode = z.infer<typeof SessionEventReadMode>;
4821
5098
 
@@ -4853,9 +5130,11 @@ export const SESSION_EVENT_SEMANTIC_CLASS_TYPES = {
4853
5130
  "workspace.inference.resumed",
4854
5131
  "session.queue.changed",
4855
5132
  "session.queue.prompt.cancelled",
5133
+ "session.mcp.approval_policy.updated",
4856
5134
  ],
4857
5135
  terminal: [
4858
5136
  "turn.completed",
5137
+ "agent.message.completed",
4859
5138
  "turn.failed",
4860
5139
  "turn.cancelled",
4861
5140
  "turn.superseded",
@@ -5709,6 +5988,354 @@ export const SessionEvent = z.object({
5709
5988
  });
5710
5989
  export type SessionEvent = z.infer<typeof SessionEvent>;
5711
5990
 
5991
+ export type SessionEventCompactResult = {
5992
+ version: 1;
5993
+ semanticClass: SessionEventSemanticClass;
5994
+ source: {
5995
+ id: string;
5996
+ type: SessionEventType;
5997
+ sequence: number;
5998
+ occurredAt: string;
5999
+ turnId: string | null;
6000
+ turnGeneration: number | null;
6001
+ turnAttemptId: string | null;
6002
+ turnAssociation: SessionEvent["turnAssociation"];
6003
+ };
6004
+ // These identity fields are repeated at the top level intentionally: an
6005
+ // MCP caller can act on the result without unpacking the source envelope.
6006
+ id: string;
6007
+ type: SessionEventType;
6008
+ sequence: number;
6009
+ occurredAt: string;
6010
+ turnId: string | null;
6011
+ turnGeneration: number | null;
6012
+ turnAttemptId: string | null;
6013
+ turnAssociation: SessionEvent["turnAssociation"];
6014
+ coveredSequence: { first: number; last: number };
6015
+ status:
6016
+ | "completed"
6017
+ | "failed"
6018
+ | "cancelled"
6019
+ | "superseded"
6020
+ | "checkpoint"
6021
+ | "receipt"
6022
+ | "unknown";
6023
+ text: string | null;
6024
+ output: unknown;
6025
+ result: unknown;
6026
+ failure: {
6027
+ error: string | null;
6028
+ code: string | null;
6029
+ retryable: boolean | null;
6030
+ recovery: string | null;
6031
+ } | null;
6032
+ checkpoint: unknown;
6033
+ receipt: unknown;
6034
+ truncation: {
6035
+ truncated: boolean;
6036
+ fields: string[];
6037
+ originalBytes: number | null;
6038
+ deliveredBytes: number;
6039
+ };
6040
+ };
6041
+
6042
+ const SESSION_EVENT_COMPACT_RESULT_TEXT_MAX_BYTES = 12 * 1024;
6043
+ // Five independently bounded slots plus identity/metadata must fit below the
6044
+ // 64 KiB MCP envelope even when a pathological producer supplies every slot.
6045
+ const SESSION_EVENT_COMPACT_RESULT_VALUE_MAX_BYTES = 8 * 1024;
6046
+
6047
+ type CompactValue = {
6048
+ value: unknown;
6049
+ truncated: boolean;
6050
+ originalBytes: number | null;
6051
+ };
6052
+
6053
+ type JsonRecord = Record<string, unknown>;
6054
+
6055
+ /**
6056
+ * Build the bounded semantic result used by `latest + result=compact`.
6057
+ *
6058
+ * This is intentionally a pure projection over one already-authoritative
6059
+ * event. It never reads history, invokes a model, follows a URL, or stores an
6060
+ * artifact. The DB/API/MCP layers decide which event is authoritative; this
6061
+ * helper only extracts the small result facts that can cross a client boundary.
6062
+ */
6063
+ export function compactSessionEventResult(
6064
+ event: SessionEvent,
6065
+ semanticClass: SessionEventSemanticClass,
6066
+ coveredSequence: { first: number; last: number } = {
6067
+ first: event.sequence,
6068
+ last: event.sequence,
6069
+ },
6070
+ ): SessionEventCompactResult {
6071
+ const payload = isSessionEventJsonRecord(event.payload) ? event.payload : {};
6072
+ const fields: string[] = [];
6073
+ let originalBytes = 0;
6074
+
6075
+ const textCandidate = typeof payload.text === "string" ? payload.text : null;
6076
+ const outputCandidate = Object.prototype.hasOwnProperty.call(payload, "output")
6077
+ ? payload.output
6078
+ : null;
6079
+ const resultCandidate = Object.prototype.hasOwnProperty.call(payload, "result")
6080
+ ? payload.result
6081
+ : undefined;
6082
+ const textValue = textCandidate ?? (typeof outputCandidate === "string" ? outputCandidate : null);
6083
+ const text = textValue === null ? null : compactResultText(textValue);
6084
+ if (text && text.truncated) {
6085
+ fields.push("text");
6086
+ originalBytes += text.originalBytes ?? 0;
6087
+ }
6088
+
6089
+ const output = compactResultValue(outputCandidate);
6090
+ if (outputCandidate !== null && output.truncated) {
6091
+ fields.push("output");
6092
+ originalBytes += output.originalBytes ?? 0;
6093
+ }
6094
+
6095
+ const result = compactResultValue(
6096
+ resultCandidate === undefined ? (textValue ?? outputCandidate) : resultCandidate,
6097
+ );
6098
+ if (resultCandidate !== undefined && result.truncated) {
6099
+ fields.push("result");
6100
+ originalBytes += result.originalBytes ?? 0;
6101
+ }
6102
+
6103
+ const checkpointField = firstOwnPayloadValue(payload, ["checkpoint", "summary", "snapshot"]);
6104
+ const checkpointCandidate =
6105
+ checkpointField !== undefined
6106
+ ? checkpointField
6107
+ : semanticClass === "checkpoint"
6108
+ ? payload
6109
+ : null;
6110
+ const checkpoint = compactResultValue(checkpointCandidate);
6111
+ if (checkpointCandidate !== null && checkpoint.truncated) {
6112
+ fields.push("checkpoint");
6113
+ originalBytes += checkpoint.originalBytes ?? 0;
6114
+ }
6115
+
6116
+ const receiptCandidate = firstOwnPayloadValue(payload, ["receipt", "receiptData"]);
6117
+ const receipt = compactResultValue(
6118
+ receiptCandidate !== undefined
6119
+ ? receiptCandidate
6120
+ : semanticClass === "tool_receipt"
6121
+ ? payload
6122
+ : null,
6123
+ );
6124
+ if (receipt.truncated) {
6125
+ fields.push("receipt");
6126
+ originalBytes += receipt.originalBytes ?? 0;
6127
+ }
6128
+
6129
+ const failure = compactFailure(payload, event.type);
6130
+ if (failure.truncated) {
6131
+ fields.push("failure");
6132
+ originalBytes += failure.originalBytes ?? 0;
6133
+ }
6134
+
6135
+ if (isSessionEventJsonRecord(payload.truncation) && payload.truncation.truncated === true) {
6136
+ fields.push("payload");
6137
+ }
6138
+
6139
+ const source = {
6140
+ id: event.id,
6141
+ type: event.type,
6142
+ sequence: event.sequence,
6143
+ occurredAt: event.occurredAt,
6144
+ turnId: event.turnId ?? null,
6145
+ turnGeneration: event.turnGeneration ?? null,
6146
+ turnAttemptId: event.turnAttemptId ?? null,
6147
+ turnAssociation: event.turnAssociation ?? null,
6148
+ };
6149
+ const status = compactResultStatus(event.type, semanticClass, payload);
6150
+ const outputValue = outputCandidate === null ? null : output.value;
6151
+ const resultValue = result.value;
6152
+ const checkpointValue = checkpointCandidate === null ? null : checkpoint.value;
6153
+ const receiptValue =
6154
+ receiptCandidate === null && semanticClass !== "tool_receipt" ? null : receipt.value;
6155
+ const compact: SessionEventCompactResult = {
6156
+ version: 1,
6157
+ semanticClass,
6158
+ source,
6159
+ id: source.id,
6160
+ type: source.type,
6161
+ sequence: source.sequence,
6162
+ occurredAt: source.occurredAt,
6163
+ turnId: source.turnId,
6164
+ turnGeneration: source.turnGeneration,
6165
+ turnAttemptId: source.turnAttemptId,
6166
+ turnAssociation: source.turnAssociation,
6167
+ coveredSequence,
6168
+ status,
6169
+ text: text?.value ?? null,
6170
+ output: outputValue,
6171
+ result: resultValue,
6172
+ failure: failure.value,
6173
+ checkpoint: checkpointValue,
6174
+ receipt: receiptValue,
6175
+ truncation: {
6176
+ truncated: fields.length > 0,
6177
+ fields: [...new Set(fields)],
6178
+ originalBytes: fields.length > 0 ? originalBytes || null : null,
6179
+ deliveredBytes: sessionEventJsonBytes({
6180
+ text: text?.value ?? null,
6181
+ output: outputValue,
6182
+ result: resultValue,
6183
+ failure: failure.value,
6184
+ checkpoint: checkpointValue,
6185
+ receipt: receiptValue,
6186
+ }),
6187
+ },
6188
+ };
6189
+ return compact;
6190
+ }
6191
+
6192
+ function isSessionEventJsonRecord(value: unknown): value is JsonRecord {
6193
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6194
+ }
6195
+
6196
+ function firstOwnPayloadValue(payload: JsonRecord, keys: readonly string[]): unknown | undefined {
6197
+ for (const key of keys) {
6198
+ if (Object.prototype.hasOwnProperty.call(payload, key)) return payload[key];
6199
+ }
6200
+ return undefined;
6201
+ }
6202
+
6203
+ function compactResultText(value: string): CompactValue & { value: string } {
6204
+ const originalBytes = new TextEncoder().encode(value).byteLength;
6205
+ if (originalBytes <= SESSION_EVENT_COMPACT_RESULT_TEXT_MAX_BYTES) {
6206
+ return { value, truncated: false, originalBytes };
6207
+ }
6208
+ let omittedBytes = originalBytes - SESSION_EVENT_COMPACT_RESULT_TEXT_MAX_BYTES;
6209
+ let projected = value;
6210
+ for (let attempt = 0; attempt < 4; attempt += 1) {
6211
+ const marker = `…[${omittedBytes} UTF-8 bytes omitted from compact result]…`;
6212
+ const budget = Math.max(0, SESSION_EVENT_COMPACT_RESULT_TEXT_MAX_BYTES - utf8Bytes(marker));
6213
+ const head = utf8PrefixForResult(value, Math.floor(budget * 0.7));
6214
+ const tail = utf8SuffixForResult(value, budget - utf8Bytes(head));
6215
+ projected = `${head}${marker}${tail}`;
6216
+ const nextOmitted = Math.max(0, originalBytes - utf8Bytes(head) - utf8Bytes(tail));
6217
+ if (nextOmitted === omittedBytes) break;
6218
+ omittedBytes = nextOmitted;
6219
+ }
6220
+ return { value: projected, truncated: true, originalBytes };
6221
+ }
6222
+
6223
+ function compactResultValue(value: unknown): CompactValue {
6224
+ if (value === null || value === undefined) {
6225
+ return { value: null, truncated: false, originalBytes: null };
6226
+ }
6227
+ const measurement = measureSessionEventJson(value);
6228
+ const bounded = boundSessionEventPayload(value, {
6229
+ surface: "http_projection",
6230
+ maxBytes: SESSION_EVENT_COMPACT_RESULT_VALUE_MAX_BYTES,
6231
+ });
6232
+ const deliveredBytes = measureSessionEventJson(bounded).bytes;
6233
+ return {
6234
+ value: bounded,
6235
+ truncated:
6236
+ measurement.bytes === null || deliveredBytes === null || measurement.bytes !== deliveredBytes,
6237
+ originalBytes: measurement.bytes,
6238
+ };
6239
+ }
6240
+
6241
+ function compactFailure(
6242
+ payload: JsonRecord,
6243
+ eventType: SessionEventType,
6244
+ ): CompactValue & {
6245
+ value: SessionEventCompactResult["failure"];
6246
+ } {
6247
+ const isFailure =
6248
+ eventType === "turn.failed" ||
6249
+ eventType === "turn.cancelled" ||
6250
+ eventType === "turn.superseded";
6251
+ const hasFailureField = ["error", "code", "retryable", "recovery"].some((key) =>
6252
+ Object.prototype.hasOwnProperty.call(payload, key),
6253
+ );
6254
+ if (!isFailure && !hasFailureField) {
6255
+ return { value: null, truncated: false, originalBytes: null };
6256
+ }
6257
+ const error = compactResultStringField(payload.error);
6258
+ const code = compactResultStringField(payload.code);
6259
+ const recovery = compactResultStringField(payload.recovery);
6260
+ const retryable = typeof payload.retryable === "boolean" ? payload.retryable : null;
6261
+ const value = { error: error.value, code: code.value, retryable, recovery: recovery.value };
6262
+ const originalBytes = [error, code, recovery]
6263
+ .map((field) => field.originalBytes ?? 0)
6264
+ .reduce((sum, bytes) => sum + bytes, 0);
6265
+ return {
6266
+ value,
6267
+ truncated: error.truncated || code.truncated || recovery.truncated,
6268
+ originalBytes: originalBytes || null,
6269
+ };
6270
+ }
6271
+
6272
+ function compactResultStringField(value: unknown): CompactValue & { value: string | null } {
6273
+ if (typeof value !== "string") {
6274
+ return { value: null, truncated: false, originalBytes: null };
6275
+ }
6276
+ return compactResultText(value);
6277
+ }
6278
+
6279
+ function compactResultStatus(
6280
+ eventType: SessionEventType,
6281
+ semanticClass: SessionEventSemanticClass,
6282
+ payload: JsonRecord,
6283
+ ): SessionEventCompactResult["status"] {
6284
+ if (eventType === "turn.failed") return "failed";
6285
+ if (eventType === "turn.cancelled") return "cancelled";
6286
+ if (eventType === "turn.superseded") return "superseded";
6287
+ if (eventType === "turn.completed" || eventType === "agent.message.completed") {
6288
+ return "completed";
6289
+ }
6290
+ if (semanticClass === "checkpoint") return "checkpoint";
6291
+ if (
6292
+ semanticClass === "tool_receipt" ||
6293
+ eventType === "artifact.created" ||
6294
+ eventType === "recording.available"
6295
+ ) {
6296
+ return "receipt";
6297
+ }
6298
+ if (payload.status === "failed") return "failed";
6299
+ if (payload.status === "completed") return "completed";
6300
+ return "unknown";
6301
+ }
6302
+
6303
+ function utf8Bytes(value: string): number {
6304
+ return new TextEncoder().encode(value).byteLength;
6305
+ }
6306
+
6307
+ function utf8PrefixForResult(value: string, maxBytes: number): string {
6308
+ let bytes = 0;
6309
+ let index = 0;
6310
+ while (index < value.length) {
6311
+ const codePoint = value.codePointAt(index);
6312
+ if (codePoint === undefined) break;
6313
+ const character = String.fromCodePoint(codePoint);
6314
+ const next = utf8Bytes(character);
6315
+ if (bytes + next > maxBytes) break;
6316
+ bytes += next;
6317
+ index += character.length;
6318
+ }
6319
+ return value.slice(0, index);
6320
+ }
6321
+
6322
+ function utf8SuffixForResult(value: string, maxBytes: number): string {
6323
+ let bytes = 0;
6324
+ let index = value.length;
6325
+ while (index > 0) {
6326
+ const width =
6327
+ index > 1 && value.charCodeAt(index - 1) >= 0xdc00 && value.charCodeAt(index - 1) <= 0xdfff
6328
+ ? 2
6329
+ : 1;
6330
+ const character = value.slice(index - width, index);
6331
+ const next = utf8Bytes(character);
6332
+ if (bytes + next > maxBytes) break;
6333
+ bytes += next;
6334
+ index -= width;
6335
+ }
6336
+ return value.slice(index);
6337
+ }
6338
+
5712
6339
  // --- Durable host export ------------------------------------------------------
5713
6340
 
5714
6341
  /** Wire revision for the durable host event/usage export stream. */
@@ -6401,7 +7028,7 @@ export const CreateSessionRequest = withVariableSetIdAlias({
6401
7028
  // including [], are authoritative; non-empty explicit arrays require attach
6402
7029
  // permission. Credential headers are write-only: create responses and events
6403
7030
  // expose only SessionMcpServerMetadata.
6404
- mcpServers: z.array(SessionMcpServerInput).default([]),
7031
+ mcpServers: z.array(SessionMcpServerInput).max(SESSION_MCP_SERVERS_MAX).default([]),
6405
7032
  // Shared-sandbox placement (addendum 05 §D.1). Three-way union; OMITTED ⇒
6406
7033
  // today's behavior (a context-dependent default resolved server-side: from
6407
7034
  // inside a session → "shared" with the creator's box, top-level → "new").
@@ -7310,6 +7937,239 @@ export const MachineMetricsSeriesResponse = z.object({
7310
7937
  });
7311
7938
  export type MachineMetricsSeriesResponse = z.infer<typeof MachineMetricsSeriesResponse>;
7312
7939
 
7940
+ /**
7941
+ * Keep this server-facing schema graph eager when imported while allowing
7942
+ * browser bundlers to discard it when contracts is used only for unrelated
7943
+ * helpers. Keep each call site annotated as pure; the factory argument itself
7944
+ * is side-effect-free until invoked.
7945
+ */
7946
+ function defineModelContractSchema<Schema>(factory: () => Schema): Schema {
7947
+ return factory();
7948
+ }
7949
+
7950
+ export const ModelCapabilitySupportV1 = /* @__PURE__ */ defineModelContractSchema(() =>
7951
+ z.enum(["supported", "unsupported", "unknown"]),
7952
+ );
7953
+ export type ModelCapabilitySupportV1 = z.infer<typeof ModelCapabilitySupportV1>;
7954
+
7955
+ export const ModelCapabilityStateV1 = /* @__PURE__ */ defineModelContractSchema(() =>
7956
+ z.object({
7957
+ upstream: ModelCapabilitySupportV1,
7958
+ runnable: z.boolean(),
7959
+ }),
7960
+ );
7961
+ export type ModelCapabilityStateV1 = z.infer<typeof ModelCapabilityStateV1>;
7962
+
7963
+ export const ModelCapabilitiesV1 = /* @__PURE__ */ defineModelContractSchema(() =>
7964
+ z.object({
7965
+ reasoning: ModelCapabilityStateV1.extend({
7966
+ efforts: z.array(ReasoningEffort),
7967
+ defaultEffort: ReasoningEffort.nullable(),
7968
+ required: z.boolean(),
7969
+ }),
7970
+ functionCalling: ModelCapabilityStateV1,
7971
+ structuredOutput: ModelCapabilityStateV1,
7972
+ hostedTools: z.object({
7973
+ webSearch: ModelCapabilityStateV1,
7974
+ xSearch: ModelCapabilityStateV1,
7975
+ codeExecution: ModelCapabilityStateV1,
7976
+ }),
7977
+ inputModalities: z.array(z.enum(["text", "image", "audio"])),
7978
+ outputModalities: z.array(z.enum(["text", "image", "audio"])),
7979
+ transports: z.object({
7980
+ sse: ModelCapabilityStateV1,
7981
+ responsesWebSocket: ModelCapabilityStateV1,
7982
+ realtimeAudio: ModelCapabilityStateV1,
7983
+ }),
7984
+ latencyModes: z.array(
7985
+ z.object({
7986
+ id: z.enum(["standard", "priority", "fast"]),
7987
+ upstream: ModelCapabilitySupportV1,
7988
+ runnable: z.boolean(),
7989
+ billingMultiplierBps: z.number().int().positive().optional(),
7990
+ }),
7991
+ ),
7992
+ }),
7993
+ );
7994
+ export type ModelCapabilitiesV1 = z.infer<typeof ModelCapabilitiesV1>;
7995
+
7996
+ export const ModelCredentialSourceV1 = /* @__PURE__ */ defineModelContractSchema(() =>
7997
+ z.union([
7998
+ z
7999
+ .object({ kind: z.literal("deployment"), mechanism: z.enum(["api_key", "azure_ad_bearer"]) })
8000
+ .strict(),
8001
+ z.object({ kind: z.literal("connected_subscription"), provider: z.literal("codex") }).strict(),
8002
+ z.object({ kind: z.literal("workspace_connection"), mechanism: z.literal("api_key") }).strict(),
8003
+ ]),
8004
+ );
8005
+ export type ModelCredentialSourceV1 = z.infer<typeof ModelCredentialSourceV1>;
8006
+
8007
+ export const ModelBillingAttributionV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8008
+ z
8009
+ .object({
8010
+ upstreamPayer: z.enum(["deployment", "workspace", "connected_subscription"]),
8011
+ metering: z.enum(["opengeni_credits", "external"]),
8012
+ })
8013
+ .strict(),
8014
+ );
8015
+ export type ModelBillingAttributionV1 = z.infer<typeof ModelBillingAttributionV1>;
8016
+
8017
+ export const TURN_EXECUTION_POLICY_METADATA_KEY = "turnExecutionPolicyV1" as const;
8018
+
8019
+ export const TurnExecutionModelSourceV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8020
+ z.enum(["explicit", "session", "deployment", "continuation"]),
8021
+ );
8022
+ export type TurnExecutionModelSourceV1 = z.infer<typeof TurnExecutionModelSourceV1>;
8023
+
8024
+ export const TurnExecutionReasoningSourceV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8025
+ z.enum(["explicit", "session", "deployment", "continuation"]),
8026
+ );
8027
+ export type TurnExecutionReasoningSourceV1 = z.infer<typeof TurnExecutionReasoningSourceV1>;
8028
+
8029
+ /**
8030
+ * Secret-safe execution identity frozen onto one accepted logical turn.
8031
+ *
8032
+ * This is deliberately a strict, normalized reference to the deployment
8033
+ * definition rather than a serialized provider client. It must never contain
8034
+ * a key/token, concrete connected credential id, account label, authorization
8035
+ * header, or credential-bearing URL/query value.
8036
+ */
8037
+ export const TurnExecutionPolicyV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8038
+ z
8039
+ .object({
8040
+ schemaVersion: z.literal(1),
8041
+ productModelId: z.string().min(1),
8042
+ requestedModelId: z.string().min(1).nullable(),
8043
+ modelSource: TurnExecutionModelSourceV1,
8044
+ reasoningEffort: ReasoningEffort,
8045
+ reasoningSource: TurnExecutionReasoningSourceV1,
8046
+ providerId: z.string().min(1),
8047
+ upstreamModelId: z.string().min(1),
8048
+ wireApi: z.enum(["responses", "chat"]),
8049
+ credentialSource: ModelCredentialSourceV1,
8050
+ billing: ModelBillingAttributionV1,
8051
+ definitionVersion: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
8052
+ })
8053
+ .strict()
8054
+ .superRefine((policy, context) => {
8055
+ if (policy.modelSource === "explicit" && policy.requestedModelId === null) {
8056
+ context.addIssue({
8057
+ code: "custom",
8058
+ path: ["requestedModelId"],
8059
+ message: "an explicit model source requires a requested model id",
8060
+ });
8061
+ }
8062
+ if (policy.modelSource !== "explicit" && policy.requestedModelId !== null) {
8063
+ context.addIssue({
8064
+ code: "custom",
8065
+ path: ["requestedModelId"],
8066
+ message: "only an explicit model source may retain a requested model id",
8067
+ });
8068
+ }
8069
+ }),
8070
+ );
8071
+ export type TurnExecutionPolicyV1 = z.infer<typeof TurnExecutionPolicyV1>;
8072
+
8073
+ export type TurnExecutionPolicyReadV1 =
8074
+ | { kind: "absent" }
8075
+ | { kind: "valid"; policy: TurnExecutionPolicyV1 };
8076
+
8077
+ /**
8078
+ * Read the policy from turn metadata. Only a literally absent key is legacy;
8079
+ * null, undefined, an unknown schema version, extra fields, and every other
8080
+ * malformed present value fail closed. Error text reports paths only and never
8081
+ * reflects the untrusted value into logs or events.
8082
+ */
8083
+ export function readTurnExecutionPolicyV1(metadata: unknown): TurnExecutionPolicyReadV1 {
8084
+ if (metadata === null || metadata === undefined) {
8085
+ return { kind: "absent" };
8086
+ }
8087
+ if (typeof metadata !== "object" || Array.isArray(metadata)) {
8088
+ throw new Error("Malformed turn execution policy metadata: turn metadata is not an object");
8089
+ }
8090
+ const record = metadata as Record<string, unknown>;
8091
+ if (!Object.prototype.hasOwnProperty.call(record, TURN_EXECUTION_POLICY_METADATA_KEY)) {
8092
+ return { kind: "absent" };
8093
+ }
8094
+ const parsed = TurnExecutionPolicyV1.safeParse(record[TURN_EXECUTION_POLICY_METADATA_KEY]);
8095
+ if (!parsed.success) {
8096
+ const paths = [
8097
+ ...new Set(
8098
+ parsed.error.issues.map((issue) =>
8099
+ issue.path.length === 0 ? "policy" : `policy.${issue.path.join(".")}`,
8100
+ ),
8101
+ ),
8102
+ ].join(", ");
8103
+ throw new Error(`Malformed turn execution policy metadata at ${paths || "policy"}`);
8104
+ }
8105
+ return { kind: "valid", policy: parsed.data };
8106
+ }
8107
+
8108
+ /** Merge a trusted policy into metadata without disturbing dispatch/recovery state. */
8109
+ export function metadataWithTurnExecutionPolicyV1(
8110
+ metadata: Readonly<Record<string, unknown>> | null | undefined,
8111
+ policy: TurnExecutionPolicyV1,
8112
+ ): Record<string, unknown> {
8113
+ return {
8114
+ ...(metadata ?? {}),
8115
+ [TURN_EXECUTION_POLICY_METADATA_KEY]: TurnExecutionPolicyV1.parse(policy),
8116
+ };
8117
+ }
8118
+
8119
+ /**
8120
+ * Minimal, stable evidence projection for command receipts and audit events.
8121
+ * It intentionally excludes aliases, URLs, request metadata, and all concrete
8122
+ * credential-selection identity.
8123
+ */
8124
+ export function turnExecutionPolicyAuditMetadata(
8125
+ policy: TurnExecutionPolicyV1,
8126
+ turnId: string,
8127
+ ): Record<string, unknown> {
8128
+ const parsed = TurnExecutionPolicyV1.parse(policy);
8129
+ return {
8130
+ turnId,
8131
+ requestedModelId: parsed.requestedModelId,
8132
+ effectiveModelId: parsed.productModelId,
8133
+ modelSource: parsed.modelSource,
8134
+ effectiveReasoningEffort: parsed.reasoningEffort,
8135
+ reasoningSource: parsed.reasoningSource,
8136
+ providerId: parsed.providerId,
8137
+ credentialSourceKind: parsed.credentialSource.kind,
8138
+ credentialSourceMechanism:
8139
+ parsed.credentialSource.kind === "connected_subscription"
8140
+ ? parsed.credentialSource.provider
8141
+ : parsed.credentialSource.mechanism,
8142
+ billingOwner: parsed.billing.upstreamPayer,
8143
+ billingMetering: parsed.billing.metering,
8144
+ definitionVersion: parsed.definitionVersion,
8145
+ };
8146
+ }
8147
+
8148
+ export const ModelPricingV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8149
+ z.object({
8150
+ inputMicrosPerMillionTokens: z.number().int().nonnegative(),
8151
+ cachedInputMicrosPerMillionTokens: z.number().int().nonnegative().optional(),
8152
+ outputMicrosPerMillionTokens: z.number().int().nonnegative(),
8153
+ marginBps: z.number().int().min(0).max(100_000).optional(),
8154
+ }),
8155
+ );
8156
+ export type ModelPricingV1 = z.infer<typeof ModelPricingV1>;
8157
+
8158
+ export const ModelPricingScheduleV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8159
+ z.object({
8160
+ default: ModelPricingV1,
8161
+ inputTokenTiers: z
8162
+ .array(
8163
+ z.object({
8164
+ minimumInputTokens: z.number().int().nonnegative(),
8165
+ pricing: ModelPricingV1,
8166
+ }),
8167
+ )
8168
+ .optional(),
8169
+ }),
8170
+ );
8171
+ export type ModelPricingScheduleV1 = z.infer<typeof ModelPricingScheduleV1>;
8172
+
7313
8173
  /**
7314
8174
  * A single host-exposed model + the provider that serves it, as surfaced to
7315
8175
  * clients (SDK + React composer) by GET /v1/config/client. The wire `api`
@@ -7317,16 +8177,134 @@ export type MachineMetricsSeriesResponse = z.infer<typeof MachineMetricsSeriesRe
7317
8177
  * provider id/label drive the picker's grouping. This mirrors the runtime's
7318
8178
  * ConfiguredModel (packages/config) projected to the client-safe fields.
7319
8179
  */
7320
- export const ClientModel = z.object({
7321
- id: z.string(),
7322
- label: z.string(),
7323
- provider: z.string(), // provider id
7324
- providerLabel: z.string(),
7325
- api: z.enum(["responses", "chat"]),
7326
- contextWindowTokens: z.number().int().positive().optional(),
7327
- });
8180
+ export const ClientModel = /* @__PURE__ */ defineModelContractSchema(() =>
8181
+ z.object({
8182
+ id: z.string(),
8183
+ label: z.string(),
8184
+ provider: z.string(), // provider id
8185
+ providerLabel: z.string(),
8186
+ api: z.enum(["responses", "chat"]),
8187
+ contextWindowTokens: z.number().int().positive().optional(),
8188
+ // Additive normalized definition metadata. Optional so older server payloads
8189
+ // remain parseable; current servers project the complete V1 set.
8190
+ schemaVersion: z.literal(1).optional(),
8191
+ aliases: z.array(z.string()).optional(),
8192
+ deployment: z
8193
+ .object({
8194
+ upstreamModelId: z.string().min(1),
8195
+ wireApi: z.enum(["responses", "chat"]),
8196
+ })
8197
+ .optional(),
8198
+ executionLimits: z
8199
+ .object({
8200
+ contextWindowTokens: z.number().int().positive().nullable(),
8201
+ effectiveContextWindowTokens: z.number().int().positive().nullable(),
8202
+ autoCompactTokenLimit: z.number().int().positive().nullable(),
8203
+ toolOutputTruncationTokens: z.number().int().positive().nullable(),
8204
+ })
8205
+ .optional(),
8206
+ credentialSource: ModelCredentialSourceV1.optional(),
8207
+ billing: ModelBillingAttributionV1.optional(),
8208
+ capabilities: ModelCapabilitiesV1.optional(),
8209
+ pricing: ModelPricingScheduleV1.optional(),
8210
+ definitionVersion: z
8211
+ .string()
8212
+ .regex(/^sha256:[a-f0-9]{64}$/u)
8213
+ .optional(),
8214
+ }),
8215
+ );
7328
8216
  export type ClientModel = z.infer<typeof ClientModel>;
7329
8217
 
8218
+ export const ModelCredentialReadinessV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8219
+ z
8220
+ .object({
8221
+ status: z.enum(["ready", "not_ready", "error"]),
8222
+ reason: z
8223
+ .enum([
8224
+ "missing_credential",
8225
+ "needs_reauth",
8226
+ "prerequisites_missing",
8227
+ "resolver_error",
8228
+ "observation_stale",
8229
+ ])
8230
+ .nullable(),
8231
+ basis: z.enum(["configuration", "connection", "resolver"]),
8232
+ checkedAt: z.string().datetime().nullable(),
8233
+ })
8234
+ .strict()
8235
+ .superRefine((readiness, context) => {
8236
+ if ((readiness.status === "ready") !== (readiness.reason === null)) {
8237
+ context.addIssue({
8238
+ code: "custom",
8239
+ path: ["reason"],
8240
+ message: "ready credential state requires no reason; non-ready state requires a reason",
8241
+ });
8242
+ }
8243
+ if ((readiness.status === "error") !== (readiness.reason === "resolver_error")) {
8244
+ context.addIssue({
8245
+ code: "custom",
8246
+ path: ["reason"],
8247
+ message:
8248
+ "credential errors require resolver_error and resolver_error requires error status",
8249
+ });
8250
+ }
8251
+ if (
8252
+ readiness.basis === "resolver" &&
8253
+ readiness.status === "ready" &&
8254
+ readiness.checkedAt === null
8255
+ ) {
8256
+ context.addIssue({
8257
+ code: "custom",
8258
+ path: ["checkedAt"],
8259
+ message: "resolver readiness requires an observation timestamp",
8260
+ });
8261
+ }
8262
+ if (readiness.reason === "observation_stale" && readiness.checkedAt === null) {
8263
+ context.addIssue({
8264
+ code: "custom",
8265
+ path: ["checkedAt"],
8266
+ message: "a stale observation requires its observation timestamp",
8267
+ });
8268
+ }
8269
+ }),
8270
+ );
8271
+ export type ModelCredentialReadinessV1 = z.infer<typeof ModelCredentialReadinessV1>;
8272
+
8273
+ export const ModelAvailabilityV1 = /* @__PURE__ */ defineModelContractSchema(() =>
8274
+ z.object({
8275
+ status: z.enum(["available", "unavailable", "degraded", "unknown"]),
8276
+ selectable: z.boolean(),
8277
+ reason: z
8278
+ .enum([
8279
+ "missing_credential",
8280
+ "needs_reauth",
8281
+ "credential_not_ready",
8282
+ "not_entitled",
8283
+ "provider_unhealthy",
8284
+ "policy_blocked",
8285
+ "unsupported",
8286
+ ])
8287
+ .nullable(),
8288
+ checkedAt: z.string().datetime().nullable(),
8289
+ }),
8290
+ );
8291
+ export type ModelAvailabilityV1 = z.infer<typeof ModelAvailabilityV1>;
8292
+
8293
+ export const WorkspaceModelCatalogModel = /* @__PURE__ */ defineModelContractSchema(() =>
8294
+ ClientModel.extend({
8295
+ credentialReadiness: ModelCredentialReadinessV1,
8296
+ availability: ModelAvailabilityV1,
8297
+ }),
8298
+ );
8299
+ export type WorkspaceModelCatalogModel = z.infer<typeof WorkspaceModelCatalogModel>;
8300
+
8301
+ export const WorkspaceModelCatalogResponse = /* @__PURE__ */ defineModelContractSchema(() =>
8302
+ z.object({
8303
+ models: z.array(WorkspaceModelCatalogModel),
8304
+ }),
8305
+ );
8306
+ export type WorkspaceModelCatalogResponse = z.infer<typeof WorkspaceModelCatalogResponse>;
8307
+
7330
8308
  /**
7331
8309
  * Exact public HTTP protocol revision spoken by this release train.
7332
8310
  *
@@ -7338,46 +8316,48 @@ export type ClientModel = z.infer<typeof ClientModel>;
7338
8316
  export const OPENGENI_API_CONTRACT_REVISION = "2026-07-turn-instructions-v1" as const;
7339
8317
  export const OPENGENI_API_CONTRACT_HEADER = "x-opengeni-api-contract" as const;
7340
8318
 
7341
- export const ClientConfig = z.object({
7342
- deploymentRevision: z.string(),
7343
- apiContractRevision: z.literal(OPENGENI_API_CONTRACT_REVISION),
7344
- // Release-train version of the server (absent on dev/source builds). The
7345
- // compatibility policy lives in docs/architecture.md clients within the
7346
- // same major are supported; evolution is additive within a major.
7347
- serverVersion: z.string().optional(),
7348
- defaultModel: z.string(),
7349
- allowedModels: z.array(z.string()).min(1),
7350
- // Richer model list (provider-grouped) for the picker. Defaults to [] for
7351
- // back-compat: callers that only read allowedModels are unaffected.
7352
- models: z.array(ClientModel).default([]),
7353
- defaultReasoningEffort: ReasoningEffort,
7354
- allowedReasoningEfforts: z.array(ReasoningEffort).min(1),
7355
- mcpServers: z
7356
- .array(
7357
- z.object({
7358
- id: z.string(),
7359
- name: z.string(),
7360
- }),
7361
- )
7362
- .default([]),
7363
- fileUploads: z.object({
7364
- enabled: z.boolean(),
7365
- maxSizeBytes: z.number().int().positive(),
8319
+ export const ClientConfig = /* @__PURE__ */ defineModelContractSchema(() =>
8320
+ z.object({
8321
+ deploymentRevision: z.string(),
8322
+ apiContractRevision: z.literal(OPENGENI_API_CONTRACT_REVISION),
8323
+ // Release-train version of the server (absent on dev/source builds). The
8324
+ // compatibility policy lives in docs/architecture.md clients within the
8325
+ // same major are supported; evolution is additive within a major.
8326
+ serverVersion: z.string().optional(),
8327
+ defaultModel: z.string(),
8328
+ allowedModels: z.array(z.string()).min(1),
8329
+ // Richer model list (provider-grouped) for the picker. Defaults to [] for
8330
+ // back-compat: callers that only read allowedModels are unaffected.
8331
+ models: z.array(ClientModel).default([]),
8332
+ defaultReasoningEffort: ReasoningEffort,
8333
+ allowedReasoningEfforts: z.array(ReasoningEffort).min(1),
8334
+ mcpServers: z
8335
+ .array(
8336
+ z.object({
8337
+ id: z.string(),
8338
+ name: z.string(),
8339
+ }),
8340
+ )
8341
+ .default([]),
8342
+ fileUploads: z.object({
8343
+ enabled: z.boolean(),
8344
+ maxSizeBytes: z.number().int().positive(),
8345
+ }),
8346
+ productAccessMode: ProductAccessMode,
8347
+ auth: ClientAuthConfig.default({ mode: "none" }),
8348
+ // Server-wide hint: does this deployment support Channel-A structured services
8349
+ // at all (P4.4). Per-session availability is negotiated on /stream-capabilities
8350
+ // (it depends on the session's pinned backend); this is the coarse on/off the
8351
+ // client uses to decide whether to even attempt the fs/git/terminal panels.
8352
+ structuredServices: z
8353
+ .object({
8354
+ fileSystem: z.boolean(),
8355
+ git: z.boolean(),
8356
+ terminalEvents: z.boolean(),
8357
+ })
8358
+ .default({ fileSystem: false, git: false, terminalEvents: false }),
7366
8359
  }),
7367
- productAccessMode: ProductAccessMode,
7368
- auth: ClientAuthConfig.default({ mode: "none" }),
7369
- // Server-wide hint: does this deployment support Channel-A structured services
7370
- // at all (P4.4). Per-session availability is negotiated on /stream-capabilities
7371
- // (it depends on the session's pinned backend); this is the coarse on/off the
7372
- // client uses to decide whether to even attempt the fs/git/terminal panels.
7373
- structuredServices: z
7374
- .object({
7375
- fileSystem: z.boolean(),
7376
- git: z.boolean(),
7377
- terminalEvents: z.boolean(),
7378
- })
7379
- .default({ fileSystem: false, git: false, terminalEvents: false }),
7380
- });
8360
+ );
7381
8361
  export type ClientConfig = z.infer<typeof ClientConfig>;
7382
8362
 
7383
8363
  function base64UrlEncode(value: string): string {