@opengeni/contracts 0.10.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/dist/index.d.ts +5193 -533
- package/dist/index.js +4969 -1798
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/event-preview.ts +928 -0
- package/src/index.ts +3711 -245
- package/src/retained-output.ts +339 -0
package/src/index.ts
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
boundSessionEventPayload,
|
|
4
|
+
measureSessionEventJson,
|
|
5
|
+
sessionEventJsonBytes,
|
|
6
|
+
type SessionEventBoundarySurface,
|
|
7
|
+
} from "./event-preview";
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
SESSION_EVENT_PAYLOAD_MAX_BYTES,
|
|
11
|
+
approximateSessionEventTokens,
|
|
12
|
+
boundSessionEventPayload,
|
|
13
|
+
measureSessionEventJson,
|
|
14
|
+
sessionEventJsonBytes,
|
|
15
|
+
sessionEventMediaPreview,
|
|
16
|
+
sessionEventMediaPreviewFromDataUrl,
|
|
17
|
+
sessionEventPayloadTruncation,
|
|
18
|
+
type BoundSessionEventPayloadOptions,
|
|
19
|
+
type SessionEventBoundarySurface,
|
|
20
|
+
type SessionEventMediaPreview,
|
|
21
|
+
type SessionEventJsonMeasurement,
|
|
22
|
+
type SessionEventPayloadTruncation,
|
|
23
|
+
} from "./event-preview";
|
|
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";
|
|
2
48
|
|
|
3
49
|
export const SessionStatus = z.enum([
|
|
4
50
|
"queued",
|
|
@@ -7,7 +53,6 @@ export const SessionStatus = z.enum([
|
|
|
7
53
|
"requires_action",
|
|
8
54
|
"recovering",
|
|
9
55
|
"waiting_capacity",
|
|
10
|
-
"paused",
|
|
11
56
|
"failed",
|
|
12
57
|
"cancelled",
|
|
13
58
|
]);
|
|
@@ -62,9 +107,16 @@ export type CapabilityDescriptor = {
|
|
|
62
107
|
os: { supported: SandboxOs[]; default: SandboxOs };
|
|
63
108
|
capabilities: {
|
|
64
109
|
FileSystem: { available: boolean; readOnly: boolean };
|
|
65
|
-
Terminal: {
|
|
110
|
+
Terminal: {
|
|
111
|
+
available: boolean;
|
|
112
|
+
transport: "sse-events" | "pty-ws" | null;
|
|
113
|
+
pty: boolean;
|
|
114
|
+
};
|
|
66
115
|
Git: { available: boolean };
|
|
67
|
-
DesktopStream: {
|
|
116
|
+
DesktopStream: {
|
|
117
|
+
available: boolean;
|
|
118
|
+
transport: "vnc-ws" | "rdp-ws" | "webrtc" | null;
|
|
119
|
+
};
|
|
68
120
|
// Feasibility only (== DesktopStream.available && os==linux); NOT a request.
|
|
69
121
|
Recording: { available: boolean };
|
|
70
122
|
};
|
|
@@ -97,7 +149,7 @@ export const DESKTOP_STREAM_PORT = 6080;
|
|
|
97
149
|
// Terminal cell's `url` is the tunnel address resolved against this port.
|
|
98
150
|
export const TERMINAL_STREAM_PORT = 7681;
|
|
99
151
|
|
|
100
|
-
// The
|
|
152
|
+
// The provider capability matrix (sandbox contract PART D + module 03-providers). One row per
|
|
101
153
|
// backend (10 rows). v1 reachable cells are all Linux; macos/windows are seam
|
|
102
154
|
// placeholders (no enum members shipped). Reading rule: a capability cell is
|
|
103
155
|
// `available:false` + a reason in the negotiated doc, never absent.
|
|
@@ -444,7 +496,7 @@ export const Permission = z.enum([
|
|
|
444
496
|
"sessions:create",
|
|
445
497
|
"sessions:read",
|
|
446
498
|
"sessions:control",
|
|
447
|
-
//
|
|
499
|
+
// sandbox workspace (sandbox contract §C.3 / crosscut PART 1.2). stream:view is a
|
|
448
500
|
// REAL, distinct permission — strictly BROADER than sessions:read — because the
|
|
449
501
|
// pixel plane (Channel B) is UN-REDACTED: a viewer of raw pixels can see cloud
|
|
450
502
|
// creds the agent cat's into a terminal, which the redacted Channel-A event log
|
|
@@ -507,6 +559,30 @@ export const Permission = z.enum([
|
|
|
507
559
|
]);
|
|
508
560
|
export type Permission = z.infer<typeof Permission>;
|
|
509
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
|
+
|
|
510
586
|
export function prefixedMcpToolName(registryId: string, toolName: string): string {
|
|
511
587
|
return `${registryId}__${toolName}`;
|
|
512
588
|
}
|
|
@@ -553,11 +629,13 @@ export const Workspace = z.object({
|
|
|
553
629
|
// validated by WorkspaceSettingsSchema; unknown keys are preserved across
|
|
554
630
|
// PATCH merges so newer settings survive an older server.
|
|
555
631
|
settings: z.record(z.string(), z.unknown()),
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
632
|
+
inferenceControl: z.object({
|
|
633
|
+
state: z.enum(["active", "paused"]),
|
|
634
|
+
revision: z.number().int().nonnegative(),
|
|
635
|
+
reason: z.string().nullable(),
|
|
636
|
+
changedBy: z.string().nullable(),
|
|
637
|
+
changedAt: z.string().nullable(),
|
|
638
|
+
}),
|
|
561
639
|
// Workspace default rig used by session/scheduled-task create fallback.
|
|
562
640
|
defaultRigId: z.string().uuid().nullable(),
|
|
563
641
|
createdAt: z.string(),
|
|
@@ -565,12 +643,297 @@ export const Workspace = z.object({
|
|
|
565
643
|
});
|
|
566
644
|
export type Workspace = z.infer<typeof Workspace>;
|
|
567
645
|
|
|
646
|
+
export const WorkspaceTranscriptionTarget = z
|
|
647
|
+
.object({
|
|
648
|
+
provider: z.string().trim().min(1).max(128),
|
|
649
|
+
model: z.string().trim().min(1).max(256).nullable(),
|
|
650
|
+
credentialMode: z.enum(["managed", "byok"]),
|
|
651
|
+
// A workspace-scoped connection reference, never credential material.
|
|
652
|
+
credentialConnectionId: z.string().uuid().nullable(),
|
|
653
|
+
region: z.string().trim().min(1).max(128).nullable(),
|
|
654
|
+
})
|
|
655
|
+
.strict()
|
|
656
|
+
.superRefine((target, context) => {
|
|
657
|
+
if (target.provider === "azure-speech" && target.credentialMode !== "byok") {
|
|
658
|
+
context.addIssue({
|
|
659
|
+
code: "custom",
|
|
660
|
+
path: ["credentialMode"],
|
|
661
|
+
message: "Azure Speech is supported only through workspace BYOK",
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (target.credentialMode === "byok" && target.credentialConnectionId === null) {
|
|
665
|
+
context.addIssue({
|
|
666
|
+
code: "custom",
|
|
667
|
+
path: ["credentialConnectionId"],
|
|
668
|
+
message: "BYOK transcription targets require a workspace connection reference",
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (target.credentialMode === "managed" && target.credentialConnectionId !== null) {
|
|
672
|
+
context.addIssue({
|
|
673
|
+
code: "custom",
|
|
674
|
+
path: ["credentialConnectionId"],
|
|
675
|
+
message: "managed transcription targets cannot name a BYOK connection",
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
export type WorkspaceTranscriptionTarget = z.infer<typeof WorkspaceTranscriptionTarget>;
|
|
680
|
+
|
|
681
|
+
export const TranscriptionErrorCode = z.enum([
|
|
682
|
+
"permission_denied",
|
|
683
|
+
"not_supported",
|
|
684
|
+
"network",
|
|
685
|
+
"provider",
|
|
686
|
+
"policy_blocked",
|
|
687
|
+
"timeout",
|
|
688
|
+
"cancelled",
|
|
689
|
+
"unknown",
|
|
690
|
+
]);
|
|
691
|
+
export type TranscriptionErrorCode = z.infer<typeof TranscriptionErrorCode>;
|
|
692
|
+
|
|
693
|
+
export const TranscriptionTimeSpan = z
|
|
694
|
+
.object({
|
|
695
|
+
startMilliseconds: z.number().finite().nonnegative(),
|
|
696
|
+
endMilliseconds: z.number().finite().nonnegative(),
|
|
697
|
+
})
|
|
698
|
+
.strict()
|
|
699
|
+
.superRefine((span, context) => {
|
|
700
|
+
if (span.endMilliseconds < span.startMilliseconds) {
|
|
701
|
+
context.addIssue({
|
|
702
|
+
code: "custom",
|
|
703
|
+
path: ["endMilliseconds"],
|
|
704
|
+
message: "transcription spans must not end before they start",
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
export type TranscriptionTimeSpan = z.infer<typeof TranscriptionTimeSpan>;
|
|
709
|
+
|
|
710
|
+
export const TranscriptionSpeaker = z
|
|
711
|
+
.object({
|
|
712
|
+
id: z.string().trim().min(1).max(128),
|
|
713
|
+
label: z.string().trim().min(1).max(128).optional(),
|
|
714
|
+
})
|
|
715
|
+
.strict();
|
|
716
|
+
export type TranscriptionSpeaker = z.infer<typeof TranscriptionSpeaker>;
|
|
717
|
+
|
|
718
|
+
export const TranscriptionWord = z
|
|
719
|
+
.object({
|
|
720
|
+
text: z.string().min(1).max(4096),
|
|
721
|
+
span: TranscriptionTimeSpan,
|
|
722
|
+
confidence: z.number().finite().min(0).max(1).optional(),
|
|
723
|
+
speaker: TranscriptionSpeaker.optional(),
|
|
724
|
+
})
|
|
725
|
+
.strict();
|
|
726
|
+
export type TranscriptionWord = z.infer<typeof TranscriptionWord>;
|
|
727
|
+
|
|
728
|
+
export const TranscriptionResultMetadata = z
|
|
729
|
+
.object({
|
|
730
|
+
detectedLanguage: z.string().trim().min(1).max(64).optional(),
|
|
731
|
+
span: TranscriptionTimeSpan.optional(),
|
|
732
|
+
confidence: z.number().finite().min(0).max(1).optional(),
|
|
733
|
+
speaker: TranscriptionSpeaker.optional(),
|
|
734
|
+
words: z.array(TranscriptionWord).max(10_000).optional(),
|
|
735
|
+
})
|
|
736
|
+
.strict()
|
|
737
|
+
.superRefine((metadata, context) => {
|
|
738
|
+
let previousStart = -1;
|
|
739
|
+
for (const [index, word] of (metadata.words ?? []).entries()) {
|
|
740
|
+
if (word.span.startMilliseconds < previousStart) {
|
|
741
|
+
context.addIssue({
|
|
742
|
+
code: "custom",
|
|
743
|
+
path: ["words", index, "span", "startMilliseconds"],
|
|
744
|
+
message: "transcription words must be ordered by start time",
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
previousStart = word.span.startMilliseconds;
|
|
748
|
+
if (
|
|
749
|
+
metadata.span &&
|
|
750
|
+
(word.span.startMilliseconds < metadata.span.startMilliseconds ||
|
|
751
|
+
word.span.endMilliseconds > metadata.span.endMilliseconds)
|
|
752
|
+
) {
|
|
753
|
+
context.addIssue({
|
|
754
|
+
code: "custom",
|
|
755
|
+
path: ["words", index, "span"],
|
|
756
|
+
message: "transcription word spans must fall within the result span",
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
export type TranscriptionResultMetadata = z.infer<typeof TranscriptionResultMetadata>;
|
|
762
|
+
|
|
763
|
+
const TranscriptionEventBase = z
|
|
764
|
+
.object({
|
|
765
|
+
localSessionId: z.string().min(1).max(256),
|
|
766
|
+
sequence: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
767
|
+
occurredAt: z.string().datetime({ offset: true }),
|
|
768
|
+
})
|
|
769
|
+
.strict();
|
|
770
|
+
|
|
771
|
+
/** Strict provider-neutral event surface; provider payload bags are rejected. */
|
|
772
|
+
export const TranscriptionEvent = z.discriminatedUnion("type", [
|
|
773
|
+
TranscriptionEventBase.extend({ type: z.literal("permission.requested") }),
|
|
774
|
+
TranscriptionEventBase.extend({
|
|
775
|
+
type: z.literal("session.opened"),
|
|
776
|
+
providerSessionId: z.string().min(1).max(512),
|
|
777
|
+
}),
|
|
778
|
+
TranscriptionEventBase.extend({
|
|
779
|
+
type: z.literal("transcript.partial"),
|
|
780
|
+
segmentId: z.string().min(1).max(512),
|
|
781
|
+
text: z.string().max(1_000_000),
|
|
782
|
+
metadata: TranscriptionResultMetadata.optional(),
|
|
783
|
+
}),
|
|
784
|
+
TranscriptionEventBase.extend({
|
|
785
|
+
type: z.literal("transcript.final"),
|
|
786
|
+
segmentId: z.string().min(1).max(512),
|
|
787
|
+
text: z.string().max(1_000_000),
|
|
788
|
+
providerAcceptanceId: z.string().min(1).max(512),
|
|
789
|
+
metadata: TranscriptionResultMetadata.optional(),
|
|
790
|
+
}),
|
|
791
|
+
TranscriptionEventBase.extend({
|
|
792
|
+
type: z.literal("usage"),
|
|
793
|
+
audioMilliseconds: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
794
|
+
costUsd: z.number().finite().nonnegative().max(1_000_000_000).nullable(),
|
|
795
|
+
}),
|
|
796
|
+
TranscriptionEventBase.extend({
|
|
797
|
+
type: z.literal("session.reconnecting"),
|
|
798
|
+
attempt: z.number().int().nonnegative().max(10_000),
|
|
799
|
+
reason: z.string().min(1).max(256),
|
|
800
|
+
}),
|
|
801
|
+
TranscriptionEventBase.extend({
|
|
802
|
+
type: z.literal("session.error"),
|
|
803
|
+
code: TranscriptionErrorCode,
|
|
804
|
+
recoverable: z.boolean(),
|
|
805
|
+
}),
|
|
806
|
+
TranscriptionEventBase.extend({
|
|
807
|
+
type: z.literal("session.closed"),
|
|
808
|
+
reason: z.enum(["completed", "cancelled", "error", "replaced"]),
|
|
809
|
+
}),
|
|
810
|
+
]);
|
|
811
|
+
export type TranscriptionEvent = z.infer<typeof TranscriptionEvent>;
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Workspace-only policy for the distinct speech-to-text capability. It never
|
|
815
|
+
* authorizes a turn model/provider and contains connection references rather
|
|
816
|
+
* than secrets. `acceptanceId` changes whenever an admin accepts a new target
|
|
817
|
+
* set, so clients can bind a microphone session to one exact policy revision.
|
|
818
|
+
*/
|
|
819
|
+
export const WorkspaceTranscriptionPolicy = z
|
|
820
|
+
.object({
|
|
821
|
+
enabled: z.boolean(),
|
|
822
|
+
acceptanceId: z.string().uuid().nullable(),
|
|
823
|
+
primary: WorkspaceTranscriptionTarget.nullable(),
|
|
824
|
+
language: z.string().trim().min(1).max(64).nullable(),
|
|
825
|
+
autoDetectLanguage: z.boolean(),
|
|
826
|
+
diarization: z
|
|
827
|
+
.object({
|
|
828
|
+
enabled: z.boolean(),
|
|
829
|
+
maxSpeakers: z.number().int().min(2).max(100).nullable(),
|
|
830
|
+
})
|
|
831
|
+
.strict(),
|
|
832
|
+
retention: z
|
|
833
|
+
.object({
|
|
834
|
+
mode: z.enum(["none", "provider-policy"]),
|
|
835
|
+
maxDays: z.number().int().nonnegative().max(3650).nullable(),
|
|
836
|
+
})
|
|
837
|
+
.strict(),
|
|
838
|
+
privacy: z
|
|
839
|
+
.object({
|
|
840
|
+
allowProviderLogging: z.boolean(),
|
|
841
|
+
allowProviderTraining: z.boolean(),
|
|
842
|
+
})
|
|
843
|
+
.strict(),
|
|
844
|
+
fallback: z
|
|
845
|
+
.object({
|
|
846
|
+
mode: z.enum(["disabled", "explicit"]),
|
|
847
|
+
targets: z.array(WorkspaceTranscriptionTarget).max(8),
|
|
848
|
+
})
|
|
849
|
+
.strict(),
|
|
850
|
+
cost: z
|
|
851
|
+
.object({
|
|
852
|
+
currency: z.literal("USD"),
|
|
853
|
+
maxPerHour: z.number().finite().nonnegative().max(10_000).nullable(),
|
|
854
|
+
maxPerMonth: z.number().finite().nonnegative().max(1_000_000).nullable(),
|
|
855
|
+
})
|
|
856
|
+
.strict(),
|
|
857
|
+
})
|
|
858
|
+
.strict()
|
|
859
|
+
.superRefine((policy, context) => {
|
|
860
|
+
if (policy.enabled && policy.acceptanceId === null) {
|
|
861
|
+
context.addIssue({
|
|
862
|
+
code: "custom",
|
|
863
|
+
path: ["acceptanceId"],
|
|
864
|
+
message: "enabled transcription requires an accepted policy identity",
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
if (policy.enabled && policy.primary === null) {
|
|
868
|
+
context.addIssue({
|
|
869
|
+
code: "custom",
|
|
870
|
+
path: ["primary"],
|
|
871
|
+
message: "enabled transcription requires a primary target",
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
if (policy.enabled && !policy.autoDetectLanguage && policy.language === null) {
|
|
875
|
+
context.addIssue({
|
|
876
|
+
code: "custom",
|
|
877
|
+
path: ["language"],
|
|
878
|
+
message: "enabled transcription requires a language or accepted automatic detection",
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (policy.autoDetectLanguage && policy.language !== null) {
|
|
882
|
+
context.addIssue({
|
|
883
|
+
code: "custom",
|
|
884
|
+
path: ["language"],
|
|
885
|
+
message: "automatic language detection and a fixed language are mutually exclusive",
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (!policy.diarization.enabled && policy.diarization.maxSpeakers !== null) {
|
|
889
|
+
context.addIssue({
|
|
890
|
+
code: "custom",
|
|
891
|
+
path: ["diarization", "maxSpeakers"],
|
|
892
|
+
message: "disabled diarization cannot retain a speaker limit",
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
if (policy.fallback.mode === "disabled" && policy.fallback.targets.length > 0) {
|
|
896
|
+
context.addIssue({
|
|
897
|
+
code: "custom",
|
|
898
|
+
path: ["fallback", "targets"],
|
|
899
|
+
message: "disabled fallback cannot retain accepted targets",
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (policy.fallback.mode === "explicit" && policy.fallback.targets.length === 0) {
|
|
903
|
+
context.addIssue({
|
|
904
|
+
code: "custom",
|
|
905
|
+
path: ["fallback", "targets"],
|
|
906
|
+
message: "explicit fallback requires at least one accepted target",
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
const targetKeys = [policy.primary, ...policy.fallback.targets]
|
|
910
|
+
.filter((target): target is WorkspaceTranscriptionTarget => target !== null)
|
|
911
|
+
.map((target) =>
|
|
912
|
+
[
|
|
913
|
+
target.provider,
|
|
914
|
+
target.model ?? "",
|
|
915
|
+
target.credentialMode,
|
|
916
|
+
target.credentialConnectionId ?? "",
|
|
917
|
+
target.region ?? "",
|
|
918
|
+
].join("\u0000"),
|
|
919
|
+
);
|
|
920
|
+
if (new Set(targetKeys).size !== targetKeys.length) {
|
|
921
|
+
context.addIssue({
|
|
922
|
+
code: "custom",
|
|
923
|
+
path: ["fallback", "targets"],
|
|
924
|
+
message: "transcription targets must be unique",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
export type WorkspaceTranscriptionPolicy = z.infer<typeof WorkspaceTranscriptionPolicy>;
|
|
929
|
+
|
|
568
930
|
// Validates the KNOWN keys of workspaces.settings; passthrough keeps unknown
|
|
569
|
-
// (future) keys rather than stripping them. memoryEnabled
|
|
570
|
-
//
|
|
931
|
+
// (future) keys rather than stripping them. memoryEnabled and transcription are
|
|
932
|
+
// both default-off capabilities.
|
|
571
933
|
export const WorkspaceSettingsSchema = z
|
|
572
934
|
.object({
|
|
573
935
|
memoryEnabled: z.boolean().optional(),
|
|
936
|
+
transcription: WorkspaceTranscriptionPolicy.optional(),
|
|
574
937
|
})
|
|
575
938
|
.passthrough();
|
|
576
939
|
export type WorkspaceSettings = z.infer<typeof WorkspaceSettingsSchema>;
|
|
@@ -581,12 +944,13 @@ export function resolveWorkspaceMemoryEnabled(settings: unknown): boolean {
|
|
|
581
944
|
return parsed.success ? parsed.data.memoryEnabled === true : false;
|
|
582
945
|
}
|
|
583
946
|
|
|
584
|
-
// PATCH body for workspace settings: a partial patch that
|
|
585
|
-
// stored bag.
|
|
586
|
-
// forward-compatible unknown keys
|
|
947
|
+
// PATCH body for workspace settings: a partial top-level patch that merges into
|
|
948
|
+
// the stored bag. Nested transcription policy updates are therefore full
|
|
949
|
+
// replacements; passthrough carries forward-compatible unknown keys.
|
|
587
950
|
export const UpdateWorkspaceSettingsRequest = z
|
|
588
951
|
.object({
|
|
589
952
|
memoryEnabled: z.boolean().optional(),
|
|
953
|
+
transcription: WorkspaceTranscriptionPolicy.optional(),
|
|
590
954
|
})
|
|
591
955
|
.passthrough();
|
|
592
956
|
export type UpdateWorkspaceSettingsRequest = z.infer<typeof UpdateWorkspaceSettingsRequest>;
|
|
@@ -606,6 +970,78 @@ export const UpdateWorkspaceModelPolicyRequest = z.object({
|
|
|
606
970
|
});
|
|
607
971
|
export type UpdateWorkspaceModelPolicyRequest = z.infer<typeof UpdateWorkspaceModelPolicyRequest>;
|
|
608
972
|
|
|
973
|
+
const turnInitiatorIdentityFields = {
|
|
974
|
+
subjectId: z.string().min(1),
|
|
975
|
+
/** Immutable display snapshot; never an authorization input. */
|
|
976
|
+
label: z.string().min(1).optional(),
|
|
977
|
+
} as const;
|
|
978
|
+
|
|
979
|
+
/** Reserved creator/initiator id used only by legacy-row migration defaults. */
|
|
980
|
+
export const UNATTRIBUTED_LEGACY_INITIATOR_SUBJECT_ID = "unattributed-legacy" as const;
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* A named machine/service principal asserted by a trusted embedding host. This
|
|
984
|
+
* deliberately excludes `kind: "subject"`: a delegated service assertion may
|
|
985
|
+
* describe causal machine work, but it is not a generic human-impersonation
|
|
986
|
+
* mechanism. The authenticated grant remains the authorization boundary.
|
|
987
|
+
*/
|
|
988
|
+
export const ServiceTurnInitiator = z.object({
|
|
989
|
+
kind: z.literal("service"),
|
|
990
|
+
subjectId: z
|
|
991
|
+
.string()
|
|
992
|
+
.min(1)
|
|
993
|
+
.max(1024)
|
|
994
|
+
.refine((value) => value !== UNATTRIBUTED_LEGACY_INITIATOR_SUBJECT_ID, {
|
|
995
|
+
message: "unattributed-legacy is reserved for migrated rows",
|
|
996
|
+
}),
|
|
997
|
+
/** Immutable display snapshot; never an authorization input. */
|
|
998
|
+
label: z.string().min(1).max(256).optional(),
|
|
999
|
+
});
|
|
1000
|
+
export type ServiceTurnInitiator = z.infer<typeof ServiceTurnInitiator>;
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Immutable, non-secret provenance captured with an initiator. This is audit
|
|
1004
|
+
* context (for example an external occurrence id), not a second identity or
|
|
1005
|
+
* authorization surface.
|
|
1006
|
+
*/
|
|
1007
|
+
export const TurnInitiatorContext = z.record(z.string(), z.unknown());
|
|
1008
|
+
export type TurnInitiatorContext = z.infer<typeof TurnInitiatorContext>;
|
|
1009
|
+
|
|
1010
|
+
const reservedServiceTurnInitiatorContextKeys = new Set([
|
|
1011
|
+
"backfill",
|
|
1012
|
+
"label",
|
|
1013
|
+
"provenanceError",
|
|
1014
|
+
"via",
|
|
1015
|
+
"viaTruncated",
|
|
1016
|
+
]);
|
|
1017
|
+
|
|
1018
|
+
/** Bounded host provenance that cannot forge OpenGeni-owned lineage fields. */
|
|
1019
|
+
export const ServiceTurnInitiatorContext = TurnInitiatorContext.superRefine((value, ctx) => {
|
|
1020
|
+
for (const key of reservedServiceTurnInitiatorContextKeys) {
|
|
1021
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
1022
|
+
ctx.addIssue({
|
|
1023
|
+
code: z.ZodIssueCode.custom,
|
|
1024
|
+
path: [key],
|
|
1025
|
+
message: `${key} is reserved OpenGeni initiator context`,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
try {
|
|
1030
|
+
if (new TextEncoder().encode(JSON.stringify(value)).byteLength > 4096) {
|
|
1031
|
+
ctx.addIssue({
|
|
1032
|
+
code: z.ZodIssueCode.custom,
|
|
1033
|
+
message: "service initiator context exceeds 4096 UTF-8 bytes",
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
ctx.addIssue({
|
|
1038
|
+
code: z.ZodIssueCode.custom,
|
|
1039
|
+
message: "service initiator context must be JSON-serializable",
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
export type ServiceTurnInitiatorContext = z.infer<typeof ServiceTurnInitiatorContext>;
|
|
1044
|
+
|
|
609
1045
|
export const AccountGrant = z.object({
|
|
610
1046
|
accountId: z.string().uuid(),
|
|
611
1047
|
subjectId: z.string().min(1),
|
|
@@ -623,6 +1059,10 @@ export const AccessGrant = z.object({
|
|
|
623
1059
|
subjectLabel: z.string().optional(),
|
|
624
1060
|
permissions: z.array(Permission),
|
|
625
1061
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1062
|
+
// Optional trusted causal principal for a command submitted by an embedding
|
|
1063
|
+
// host. Authorization still uses subjectId + permissions above.
|
|
1064
|
+
serviceInitiator: ServiceTurnInitiator.optional(),
|
|
1065
|
+
serviceInitiatorContext: ServiceTurnInitiatorContext.optional(),
|
|
626
1066
|
});
|
|
627
1067
|
export type AccessGrant = z.infer<typeof AccessGrant>;
|
|
628
1068
|
|
|
@@ -637,34 +1077,76 @@ export const AccessContext = z.object({
|
|
|
637
1077
|
});
|
|
638
1078
|
export type AccessContext = z.infer<typeof AccessContext>;
|
|
639
1079
|
|
|
640
|
-
export const DelegatedAccessTokenPayload = z
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1080
|
+
export const DelegatedAccessTokenPayload = z
|
|
1081
|
+
.object({
|
|
1082
|
+
accountId: z.string().uuid(),
|
|
1083
|
+
workspaceId: z.string().uuid(),
|
|
1084
|
+
subjectId: z.string().min(1),
|
|
1085
|
+
subjectLabel: z.string().optional(),
|
|
1086
|
+
permissions: z.array(Permission).min(1),
|
|
1087
|
+
// Trusted embedding hosts can sign a causal service principal separately
|
|
1088
|
+
// from the grant subject that authorizes the request. The claim is consumed
|
|
1089
|
+
// only when a command creates a new session/turn.
|
|
1090
|
+
serviceInitiator: ServiceTurnInitiator.optional(),
|
|
1091
|
+
serviceInitiatorContext: ServiceTurnInitiatorContext.optional(),
|
|
1092
|
+
// Worker-asserted session scope for first-party MCP calls (HMAC-signed, not
|
|
1093
|
+
// agent-controlled); enables session-scoped tools such as goal management.
|
|
1094
|
+
sessionId: z.string().uuid().optional(),
|
|
1095
|
+
// The turn making the call (the caller's identity), HMAC-signed by the worker
|
|
1096
|
+
// at turn setup. Lets a tool classify WHO is calling from the token itself,
|
|
1097
|
+
// instead of racily re-reading the session's live active_turn_id — e.g. the
|
|
1098
|
+
// sacred-pause guard must know if the CALLER is a machine child-notification
|
|
1099
|
+
// turn, and the active pointer can flip to another turn mid-check.
|
|
1100
|
+
turnId: z.string().uuid().optional(),
|
|
1101
|
+
// Exact execution owner. Agent control commands are accepted only while this
|
|
1102
|
+
// attempt still owns the signed turn.
|
|
1103
|
+
attemptId: z.string().uuid().optional(),
|
|
1104
|
+
executionGeneration: z.number().int().positive().optional(),
|
|
1105
|
+
exp: z.number().int().positive(),
|
|
1106
|
+
})
|
|
1107
|
+
.superRefine((payload, ctx) => {
|
|
1108
|
+
if (payload.serviceInitiatorContext && !payload.serviceInitiator) {
|
|
1109
|
+
ctx.addIssue({
|
|
1110
|
+
code: z.ZodIssueCode.custom,
|
|
1111
|
+
path: ["serviceInitiatorContext"],
|
|
1112
|
+
message: "serviceInitiatorContext requires serviceInitiator",
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
if (
|
|
1116
|
+
payload.serviceInitiator &&
|
|
1117
|
+
(payload.turnId !== undefined ||
|
|
1118
|
+
payload.attemptId !== undefined ||
|
|
1119
|
+
payload.executionGeneration !== undefined)
|
|
1120
|
+
) {
|
|
1121
|
+
ctx.addIssue({
|
|
1122
|
+
code: z.ZodIssueCode.custom,
|
|
1123
|
+
path: ["serviceInitiator"],
|
|
1124
|
+
message: "serviceInitiator cannot replace an exact agent-attempt initiator",
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
657
1128
|
export type DelegatedAccessTokenPayload = z.infer<typeof DelegatedAccessTokenPayload>;
|
|
658
1129
|
|
|
1130
|
+
const delegatedAccessTokenPrefix = "ogd_";
|
|
1131
|
+
const delegatedServiceAccessTokenPrefix = "ogd2_";
|
|
1132
|
+
|
|
659
1133
|
export async function signDelegatedAccessToken(
|
|
660
1134
|
secret: string,
|
|
661
1135
|
payload: DelegatedAccessTokenPayload,
|
|
662
1136
|
): Promise<string> {
|
|
663
|
-
const
|
|
664
|
-
|
|
1137
|
+
const parsed = DelegatedAccessTokenPayload.parse(payload);
|
|
1138
|
+
const prefix = parsed.serviceInitiator
|
|
1139
|
+
? delegatedServiceAccessTokenPrefix
|
|
1140
|
+
: delegatedAccessTokenPrefix;
|
|
1141
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(parsed));
|
|
1142
|
+
// The service-capable envelope binds its prefix into the signature. An old
|
|
1143
|
+
// verifier accepts only ogd_ and therefore fails closed during a rolling
|
|
1144
|
+
// deploy; changing ogd2_ to ogd_ cannot turn provenance loss into success.
|
|
1145
|
+
const signature = await hmacSha256Base64Url(
|
|
1146
|
+
secret,
|
|
1147
|
+
prefix === delegatedServiceAccessTokenPrefix ? `${prefix}${encodedPayload}` : encodedPayload,
|
|
665
1148
|
);
|
|
666
|
-
|
|
667
|
-
return `ogd_${encodedPayload}.${signature}`;
|
|
1149
|
+
return `${prefix}${encodedPayload}.${signature}`;
|
|
668
1150
|
}
|
|
669
1151
|
|
|
670
1152
|
export async function verifyDelegatedAccessToken(
|
|
@@ -672,30 +1154,48 @@ export async function verifyDelegatedAccessToken(
|
|
|
672
1154
|
token: string,
|
|
673
1155
|
nowSeconds = Math.floor(Date.now() / 1000),
|
|
674
1156
|
): Promise<DelegatedAccessTokenPayload | null> {
|
|
675
|
-
|
|
1157
|
+
const prefix = token.startsWith(delegatedServiceAccessTokenPrefix)
|
|
1158
|
+
? delegatedServiceAccessTokenPrefix
|
|
1159
|
+
: token.startsWith(delegatedAccessTokenPrefix)
|
|
1160
|
+
? delegatedAccessTokenPrefix
|
|
1161
|
+
: null;
|
|
1162
|
+
if (!prefix) {
|
|
676
1163
|
return null;
|
|
677
1164
|
}
|
|
678
|
-
const withoutPrefix = token.slice(
|
|
1165
|
+
const withoutPrefix = token.slice(prefix.length);
|
|
679
1166
|
const dot = withoutPrefix.lastIndexOf(".");
|
|
680
1167
|
if (dot <= 0) {
|
|
681
1168
|
return null;
|
|
682
1169
|
}
|
|
683
1170
|
const encodedPayload = withoutPrefix.slice(0, dot);
|
|
684
1171
|
const signature = withoutPrefix.slice(dot + 1);
|
|
685
|
-
const expected = await hmacSha256Base64Url(
|
|
1172
|
+
const expected = await hmacSha256Base64Url(
|
|
1173
|
+
secret,
|
|
1174
|
+
prefix === delegatedServiceAccessTokenPrefix ? `${prefix}${encodedPayload}` : encodedPayload,
|
|
1175
|
+
);
|
|
686
1176
|
if (!constantTimeEqual(signature, expected)) {
|
|
687
1177
|
return null;
|
|
688
1178
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1179
|
+
let decoded: unknown;
|
|
1180
|
+
try {
|
|
1181
|
+
decoded = JSON.parse(base64UrlDecode(encodedPayload));
|
|
1182
|
+
} catch {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
const payload = DelegatedAccessTokenPayload.safeParse(decoded);
|
|
692
1186
|
if (!payload.success || payload.data.exp < nowSeconds) {
|
|
693
1187
|
return null;
|
|
694
1188
|
}
|
|
1189
|
+
if (
|
|
1190
|
+
(prefix === delegatedServiceAccessTokenPrefix) !==
|
|
1191
|
+
(payload.data.serviceInitiator !== undefined)
|
|
1192
|
+
) {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
695
1195
|
return payload.data;
|
|
696
1196
|
}
|
|
697
1197
|
|
|
698
|
-
// --- Enrollment bearer credential (bring-your-own-compute M5
|
|
1198
|
+
// --- Enrollment bearer credential (bring-your-own-compute M5) ---
|
|
699
1199
|
//
|
|
700
1200
|
// The signed bearer the agent presents to the control plane after enrollment (the
|
|
701
1201
|
// EnrollmentCredentials.bearer the poll returns). REUSES the SAME HMAC envelope as
|
|
@@ -828,7 +1328,7 @@ export async function verifyEnrollToken(
|
|
|
828
1328
|
return payload.data;
|
|
829
1329
|
}
|
|
830
1330
|
|
|
831
|
-
// --- Scoped data-plane stream token (
|
|
1331
|
+
// --- Scoped data-plane stream token (sandbox contract §C.3 / crosscut PART 1.3) ---
|
|
832
1332
|
//
|
|
833
1333
|
// REUSES the existing HMAC envelope (sign/verifyDelegatedAccessToken's
|
|
834
1334
|
// base64Url + hmacSha256Base64Url) — NOT a second crypto — but with a distinct
|
|
@@ -913,7 +1413,7 @@ export async function verifyStreamToken(
|
|
|
913
1413
|
return payload.data;
|
|
914
1414
|
}
|
|
915
1415
|
|
|
916
|
-
// --- Relay PRODUCER token (bring-your-own-compute M8b
|
|
1416
|
+
// --- Relay PRODUCER token (bring-your-own-compute M8b) ---
|
|
917
1417
|
//
|
|
918
1418
|
// The token the AGENT presents to the relay edge when it registers a pty/desktop
|
|
919
1419
|
// stream channel (role=AGENT) — distinct from the viewer's `ogs_` token. It is
|
|
@@ -1147,7 +1647,11 @@ export type Entitlements = z.infer<typeof Entitlements>;
|
|
|
1147
1647
|
|
|
1148
1648
|
export const LimitDecision = z.discriminatedUnion("allowed", [
|
|
1149
1649
|
z.object({ allowed: z.literal(true) }),
|
|
1150
|
-
z.object({
|
|
1650
|
+
z.object({
|
|
1651
|
+
allowed: z.literal(false),
|
|
1652
|
+
code: z.string(),
|
|
1653
|
+
message: z.string(),
|
|
1654
|
+
}),
|
|
1151
1655
|
]);
|
|
1152
1656
|
export type LimitDecision = z.infer<typeof LimitDecision>;
|
|
1153
1657
|
|
|
@@ -1195,10 +1699,21 @@ export type EntitlementsPort = {
|
|
|
1195
1699
|
export const GitCredentialProvider = z.enum(["github", "gitlab", "azure_devops"]);
|
|
1196
1700
|
export type GitCredentialProvider = z.infer<typeof GitCredentialProvider>;
|
|
1197
1701
|
|
|
1702
|
+
// Host-opaque identity for one independently mintable Git credential. It is
|
|
1703
|
+
// deliberately NOT constrained to a filesystem-safe alphabet: runtimes hash it
|
|
1704
|
+
// before using it in paths, command text, or environment variable names.
|
|
1705
|
+
export const GitCredentialBindingId = z.string().min(1).max(256);
|
|
1706
|
+
export type GitCredentialBindingId = z.infer<typeof GitCredentialBindingId>;
|
|
1707
|
+
|
|
1708
|
+
export const GitRepositoryAccess = z.enum(["read", "write"]);
|
|
1709
|
+
export type GitRepositoryAccess = z.infer<typeof GitRepositoryAccess>;
|
|
1710
|
+
|
|
1198
1711
|
const GitProviderRepositoryId = z.union([z.number().int().positive(), z.string().min(1)]);
|
|
1199
1712
|
|
|
1200
1713
|
export const GitCredentialRepositoryRef = z.object({
|
|
1201
1714
|
provider: GitCredentialProvider.optional(),
|
|
1715
|
+
credentialBindingId: GitCredentialBindingId.optional(),
|
|
1716
|
+
access: GitRepositoryAccess.optional(),
|
|
1202
1717
|
uri: z.string().min(1),
|
|
1203
1718
|
ref: z.string().min(1),
|
|
1204
1719
|
repositoryId: GitProviderRepositoryId.optional(),
|
|
@@ -1208,10 +1723,10 @@ export const GitCredentialRepositoryRef = z.object({
|
|
|
1208
1723
|
});
|
|
1209
1724
|
export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryRef>;
|
|
1210
1725
|
|
|
1211
|
-
// ============
|
|
1726
|
+
// ============ connection-credential provider — Connection-credential provider (§7.6) ============
|
|
1212
1727
|
//
|
|
1213
|
-
// The host-providable
|
|
1214
|
-
//
|
|
1728
|
+
// The host-providable credential seam over OpenGeni's run-scoped credential
|
|
1729
|
+
// sites in the worker and API:
|
|
1215
1730
|
// - GIT credentials: run-scoped provider tokens minted in
|
|
1216
1731
|
// `sandboxEnvironmentForRun` (standalone self-mints GitHub App tokens from
|
|
1217
1732
|
// `settings`; embedded hosts can broker GitHub, GitLab, and Azure DevOps)
|
|
@@ -1219,6 +1734,8 @@ export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryR
|
|
|
1219
1734
|
// - SANDBOX secrets: the decrypted variable set values loaded in
|
|
1220
1735
|
// `loadVariableSetForRun` (today decrypted with
|
|
1221
1736
|
// `environmentsEncryptionKeyBytes(settings)`).
|
|
1737
|
+
// - MCP credentials: request-time transport headers for connection-backed
|
|
1738
|
+
// servers, shared by normal model tools and Toolspace/Code Mode.
|
|
1222
1739
|
//
|
|
1223
1740
|
// In embedded/separate topologies the HOST owns these external connections
|
|
1224
1741
|
// (its GitHub App, its secret vault + encryption key). When a host binds this
|
|
@@ -1226,7 +1743,7 @@ export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryR
|
|
|
1226
1743
|
// from `settings`. Unset (standalone default) → byte-for-byte today's
|
|
1227
1744
|
// self-mint.
|
|
1228
1745
|
//
|
|
1229
|
-
//
|
|
1746
|
+
// Workspace-scope cross-check (the host-mapping safety guardrail): a credential
|
|
1230
1747
|
// provider returns the `workspaceId` it scoped the credential to, and the
|
|
1231
1748
|
// activity ASSERTS it agrees with the run's workspace BEFORE injecting
|
|
1232
1749
|
// any git provider token seed (or applying decrypted environment values). A host mapping bug that
|
|
@@ -1236,10 +1753,26 @@ export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryR
|
|
|
1236
1753
|
export type GitCredentialsRequest = {
|
|
1237
1754
|
accountId: string;
|
|
1238
1755
|
workspaceId: string;
|
|
1756
|
+
/** Immutable authority admitted with the turn requesting this credential. */
|
|
1757
|
+
sessionId: string;
|
|
1758
|
+
rootSessionId: string;
|
|
1759
|
+
turnId: string;
|
|
1760
|
+
attemptId: string;
|
|
1761
|
+
executionGeneration: number;
|
|
1762
|
+
initiator: TurnInitiator;
|
|
1763
|
+
initiatorContext: TurnInitiatorContext;
|
|
1239
1764
|
// Provider defaults to "github" for the legacy request shape. GitHub-only
|
|
1240
1765
|
// hosts can keep reading installationId/repositoryIds exactly as before;
|
|
1241
1766
|
// provider-aware hosts should branch on this and repositoryRefs.
|
|
1242
1767
|
provider?: GitCredentialProvider;
|
|
1768
|
+
// Present when the host supplied an explicit binding or when more than one
|
|
1769
|
+
// independently mintable credential exists for this provider. A host must
|
|
1770
|
+
// mint only this binding; OpenGeni never treats provider identity as enough
|
|
1771
|
+
// to select among multiple accounts/installations.
|
|
1772
|
+
credentialBindingId?: GitCredentialBindingId;
|
|
1773
|
+
// Canonical lower-case host shared by this binding's repository refs when
|
|
1774
|
+
// there is exactly one. Binding-aware providers echo it when present.
|
|
1775
|
+
providerHost?: string;
|
|
1243
1776
|
// Token requests are the existing behavior. Identity requests let lazy
|
|
1244
1777
|
// sandbox provisioning resolve stable git author/committer identity before
|
|
1245
1778
|
// the box exists while deferring the rotating token value to first provision.
|
|
@@ -1252,14 +1785,49 @@ export type GitCredentialsRequest = {
|
|
|
1252
1785
|
repositoryIds: number[];
|
|
1253
1786
|
};
|
|
1254
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
|
+
|
|
1255
1810
|
export type GitCredentials = {
|
|
1256
|
-
// The minted
|
|
1257
|
-
//
|
|
1258
|
-
//
|
|
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.
|
|
1259
1815
|
token?: string;
|
|
1260
|
-
//
|
|
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;
|
|
1821
|
+
// workspace-scope cross-check echo: the workspace the provider scoped this token to. The activity
|
|
1261
1822
|
// asserts `workspaceId === request.workspaceId` before injecting.
|
|
1262
1823
|
workspaceId: string;
|
|
1824
|
+
// Strict request echoes for binding-aware requests. OpenGeni validates these
|
|
1825
|
+
// before accepting a token, preventing a host routing bug from returning a
|
|
1826
|
+
// sibling connection's credential. They remain optional for legacy single-
|
|
1827
|
+
// binding/provider hosts.
|
|
1828
|
+
credentialBindingId?: GitCredentialBindingId;
|
|
1829
|
+
provider?: GitCredentialProvider;
|
|
1830
|
+
providerHost?: string;
|
|
1263
1831
|
// Optional provider expiry for host-managed proactive renewal. ISO-8601;
|
|
1264
1832
|
// null/omitted means the host does not expose a deadline and OpenGeni uses
|
|
1265
1833
|
// its conservative bounded refresh cadence instead.
|
|
@@ -1282,7 +1850,7 @@ export type SandboxSecrets = {
|
|
|
1282
1850
|
// `environmentsEncryptionKeyBytes` decrypt. Same shape the self-mint path
|
|
1283
1851
|
// produces (plaintext name→value).
|
|
1284
1852
|
values: Record<string, string>;
|
|
1285
|
-
//
|
|
1853
|
+
// workspace-scope cross-check echo: the workspace the provider scoped these secrets to.
|
|
1286
1854
|
workspaceId: string;
|
|
1287
1855
|
// Optional variableSet metadata; when omitted the activity uses the
|
|
1288
1856
|
// variableSetId as both id and name (the local decrypt carries the row's
|
|
@@ -1292,24 +1860,268 @@ export type SandboxSecrets = {
|
|
|
1292
1860
|
description?: string | null;
|
|
1293
1861
|
};
|
|
1294
1862
|
|
|
1863
|
+
export type CredentialAuthNeededReason =
|
|
1864
|
+
| "missing_connection"
|
|
1865
|
+
| "expired"
|
|
1866
|
+
| "insufficient_scope"
|
|
1867
|
+
| "refresh_failed";
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Host-owned run credentials are materialized below one OpenGeni-owned sandbox
|
|
1871
|
+
* directory. Paths are relative POSIX names; the runtime validates traversal,
|
|
1872
|
+
* collisions, bounds, and modes before any content reaches a sandbox.
|
|
1873
|
+
*/
|
|
1874
|
+
export type RunCredentialFile = {
|
|
1875
|
+
path: string;
|
|
1876
|
+
content: string;
|
|
1877
|
+
mode?: "0400" | "0600";
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
export type RunCredentialAuthNeeded = {
|
|
1881
|
+
reason: CredentialAuthNeededReason;
|
|
1882
|
+
providerDomain?: string;
|
|
1883
|
+
connectionId?: string;
|
|
1884
|
+
scopes?: string[];
|
|
1885
|
+
resource?: string;
|
|
1886
|
+
authorizationUrl?: string;
|
|
1887
|
+
/** Bounded non-secret guidance. Never place credential material here. */
|
|
1888
|
+
message?: string;
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
export type RunCredentialRedaction = {
|
|
1892
|
+
/** Bounded diagnostic label used only in the replacement marker. */
|
|
1893
|
+
name: string;
|
|
1894
|
+
/** One atomic secret value that must be removed from streamed/audit output. */
|
|
1895
|
+
value: string;
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1898
|
+
export type RunCredentialsRequest = {
|
|
1899
|
+
accountId: string;
|
|
1900
|
+
workspaceId: string;
|
|
1901
|
+
sessionId: string;
|
|
1902
|
+
parentSessionId: string | null;
|
|
1903
|
+
rootSessionId: string;
|
|
1904
|
+
/** All sessions sharing this sandbox group share one OS/filesystem trust boundary. */
|
|
1905
|
+
sandboxGroupId: string;
|
|
1906
|
+
turnId: string;
|
|
1907
|
+
attemptId: string;
|
|
1908
|
+
executionGeneration: number;
|
|
1909
|
+
/** Immutable authority admitted with this turn. */
|
|
1910
|
+
initiator: TurnInitiator;
|
|
1911
|
+
initiatorContext: TurnInitiatorContext;
|
|
1912
|
+
effectiveSandboxBackend: SandboxBackend;
|
|
1913
|
+
sandboxOs: SandboxOs;
|
|
1914
|
+
purpose: "provision" | "renewal";
|
|
1915
|
+
forceRefresh: boolean;
|
|
1916
|
+
/** Informational standalone variable-set identity; never gates host resolution. */
|
|
1917
|
+
variableSet: { id: string; name: string } | null;
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
export type RunCredentialsResolution =
|
|
1921
|
+
| {
|
|
1922
|
+
/**
|
|
1923
|
+
* The frozen target/attempt must not receive host material. Hosts use
|
|
1924
|
+
* this for unsupported OSes/backends and policy-based opt-out; the
|
|
1925
|
+
* decision must remain stable for the attempt.
|
|
1926
|
+
*/
|
|
1927
|
+
status: "not_applicable";
|
|
1928
|
+
accountId: string;
|
|
1929
|
+
workspaceId: string;
|
|
1930
|
+
sessionId: string;
|
|
1931
|
+
}
|
|
1932
|
+
| {
|
|
1933
|
+
status: "ok";
|
|
1934
|
+
/** Scope echoes are mandatory and checked before materialization. */
|
|
1935
|
+
accountId: string;
|
|
1936
|
+
workspaceId: string;
|
|
1937
|
+
sessionId: string;
|
|
1938
|
+
/** Secret environment values. Always delivered off-manifest. */
|
|
1939
|
+
environment: Record<string, string>;
|
|
1940
|
+
files?: RunCredentialFile[];
|
|
1941
|
+
/** Environment name to one returned relative file path. */
|
|
1942
|
+
fileEnvironment?: Record<string, string>;
|
|
1943
|
+
/**
|
|
1944
|
+
* Atomic sensitive values embedded inside credential files or derived
|
|
1945
|
+
* material. Environment values are registered automatically; hosts list
|
|
1946
|
+
* additional file-contained values here so chunked output is redacted.
|
|
1947
|
+
*/
|
|
1948
|
+
redactions?: RunCredentialRedaction[];
|
|
1949
|
+
/** Earliest material expiry. Null/omitted uses a bounded refresh cadence. */
|
|
1950
|
+
expiresAt?: string | null;
|
|
1951
|
+
/** Partial degradation: usable material may coexist with reconnect notices. */
|
|
1952
|
+
authNeeded?: RunCredentialAuthNeeded[];
|
|
1953
|
+
}
|
|
1954
|
+
| {
|
|
1955
|
+
status: "auth_needed";
|
|
1956
|
+
accountId: string;
|
|
1957
|
+
workspaceId: string;
|
|
1958
|
+
sessionId: string;
|
|
1959
|
+
authNeeded: RunCredentialAuthNeeded[];
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
export const McpConnectionResourceScope = z
|
|
1963
|
+
.object({
|
|
1964
|
+
/** Provider-stable repository identity, serialized as a string on the wire. */
|
|
1965
|
+
id: z.string().min(1).max(512),
|
|
1966
|
+
kind: z.literal("repository"),
|
|
1967
|
+
})
|
|
1968
|
+
.strict();
|
|
1969
|
+
export type McpConnectionResourceScope = z.infer<typeof McpConnectionResourceScope>;
|
|
1970
|
+
|
|
1971
|
+
const McpConnectionResourceScopes = z
|
|
1972
|
+
.array(McpConnectionResourceScope)
|
|
1973
|
+
.min(1)
|
|
1974
|
+
.max(256)
|
|
1975
|
+
.superRefine((resources, context) => {
|
|
1976
|
+
const seen = new Set<string>();
|
|
1977
|
+
for (const [index, resource] of resources.entries()) {
|
|
1978
|
+
const key = `${resource.kind}\0${resource.id}`;
|
|
1979
|
+
if (seen.has(key)) {
|
|
1980
|
+
context.addIssue({
|
|
1981
|
+
code: "custom",
|
|
1982
|
+
message: "selectedResources must not contain duplicates",
|
|
1983
|
+
path: [index],
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
seen.add(key);
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
export const McpServerConnectionRef = z
|
|
1991
|
+
.object({
|
|
1992
|
+
/** Opaque host or standalone connection identifier. */
|
|
1993
|
+
connectionId: z.string().min(1).optional(),
|
|
1994
|
+
/** Stable provider family (for example github, gitlab, or azure_devops). */
|
|
1995
|
+
provider: z.string().min(1).max(128).optional(),
|
|
1996
|
+
/** Provider host or tenant domain. */
|
|
1997
|
+
providerDomain: z.string().min(1),
|
|
1998
|
+
kind: z.enum(["oauth2", "api_key", "app_install", "delegated"]).optional(),
|
|
1999
|
+
scopes: z.array(z.string().min(1)).optional(),
|
|
2000
|
+
/** OAuth resource indicator. This is distinct from selectedResources. */
|
|
2001
|
+
resource: z.string().min(1).optional(),
|
|
2002
|
+
/** Exact provider resources this MCP binding is allowed to operate on. */
|
|
2003
|
+
selectedResources: McpConnectionResourceScopes.optional(),
|
|
2004
|
+
subjectScope: z.enum(["workspace", "subject"]).optional(),
|
|
2005
|
+
})
|
|
2006
|
+
.strict()
|
|
2007
|
+
.superRefine((reference, context) => {
|
|
2008
|
+
if (!reference.selectedResources) return;
|
|
2009
|
+
if (!reference.connectionId) {
|
|
2010
|
+
context.addIssue({
|
|
2011
|
+
code: "custom",
|
|
2012
|
+
message: "selectedResources requires connectionId",
|
|
2013
|
+
path: ["connectionId"],
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
if (!reference.provider) {
|
|
2017
|
+
context.addIssue({
|
|
2018
|
+
code: "custom",
|
|
2019
|
+
message: "selectedResources requires provider",
|
|
2020
|
+
path: ["provider"],
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRef>;
|
|
2025
|
+
|
|
2026
|
+
export type McpCredentialsRequest = {
|
|
2027
|
+
accountId: string;
|
|
2028
|
+
workspaceId: string;
|
|
2029
|
+
/** Immediate session whose model or Toolspace call needs the credential. */
|
|
2030
|
+
sessionId: string;
|
|
2031
|
+
/** Workspace-scoped lineage root for host authorization and binding lookup. */
|
|
2032
|
+
rootSessionId: string;
|
|
2033
|
+
turnId: string;
|
|
2034
|
+
/** Null only while a durable turn exists without a currently executing attempt. */
|
|
2035
|
+
attemptId: string | null;
|
|
2036
|
+
executionGeneration: number;
|
|
2037
|
+
/** The immutable authority that admitted this turn. Never substitute the sandbox caller. */
|
|
2038
|
+
initiator: TurnInitiator;
|
|
2039
|
+
initiatorContext: TurnInitiatorContext;
|
|
2040
|
+
/** Immediate technical caller, retained only as non-authoritative audit context. */
|
|
2041
|
+
callerSubjectId?: string;
|
|
2042
|
+
surface: "model" | "toolspace";
|
|
2043
|
+
/** Canonical MCP destination that will receive the resolved headers. */
|
|
2044
|
+
destinationUrl: string;
|
|
2045
|
+
serverId: string;
|
|
2046
|
+
toolName?: string;
|
|
2047
|
+
connectionRef: McpServerConnectionRef;
|
|
2048
|
+
forceRefresh: boolean;
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
export type McpCredentialAuthNeededReason =
|
|
2052
|
+
| CredentialAuthNeededReason
|
|
2053
|
+
| "unsupported_auth"
|
|
2054
|
+
| "resource_scope_unavailable";
|
|
2055
|
+
|
|
2056
|
+
export type McpCredentialResolution =
|
|
2057
|
+
| {
|
|
2058
|
+
status: "ok";
|
|
2059
|
+
/** Scope echoes are mandatory and verified before any header is used. */
|
|
2060
|
+
accountId: string;
|
|
2061
|
+
workspaceId: string;
|
|
2062
|
+
sessionId: string;
|
|
2063
|
+
headers: Record<string, string>;
|
|
2064
|
+
connectionId: string;
|
|
2065
|
+
providerDomain: string;
|
|
2066
|
+
provider?: string;
|
|
2067
|
+
scopes?: string[];
|
|
2068
|
+
resource?: string;
|
|
2069
|
+
selectedResources?: McpConnectionResourceScope[];
|
|
2070
|
+
expiresAt?: string | null;
|
|
2071
|
+
}
|
|
2072
|
+
| {
|
|
2073
|
+
status: "auth_needed";
|
|
2074
|
+
/** Scope echoes are mandatory even when the credential cannot be resolved. */
|
|
2075
|
+
accountId: string;
|
|
2076
|
+
workspaceId: string;
|
|
2077
|
+
sessionId: string;
|
|
2078
|
+
reason: McpCredentialAuthNeededReason;
|
|
2079
|
+
providerDomain: string;
|
|
2080
|
+
provider?: string;
|
|
2081
|
+
connectionId?: string;
|
|
2082
|
+
scopes?: string[];
|
|
2083
|
+
resource?: string;
|
|
2084
|
+
selectedResources?: McpConnectionResourceScope[];
|
|
2085
|
+
authorizationUrl?: string;
|
|
2086
|
+
};
|
|
2087
|
+
|
|
1295
2088
|
export type ConnectionCredentialsPort = {
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
2089
|
+
// Every leg is optional: a host may drive only the credential classes it
|
|
2090
|
+
// owns. An unset leg falls through to today's standalone implementation for
|
|
2091
|
+
// that leg only.
|
|
1299
2092
|
gitCredentials?(input: GitCredentialsRequest): Promise<GitCredentials>;
|
|
1300
2093
|
sandboxSecrets?(input: SandboxSecretsRequest): Promise<SandboxSecrets>;
|
|
2094
|
+
/**
|
|
2095
|
+
* Resolve host-owned, session-aware sandbox credentials independently of an
|
|
2096
|
+
* OpenGeni variable set. OpenGeni transports and renews the material; the host
|
|
2097
|
+
* remains the sole owner of connection selection and credential policy.
|
|
2098
|
+
*/
|
|
2099
|
+
runCredentials?(input: RunCredentialsRequest): Promise<RunCredentialsResolution>;
|
|
2100
|
+
/**
|
|
2101
|
+
* Resolve rotating MCP transport credentials at request time. Embedded hosts
|
|
2102
|
+
* use this to keep their provider connection as the sole credential source;
|
|
2103
|
+
* OpenGeni never requires a duplicate connection record. The same resolver is
|
|
2104
|
+
* used by model-visible MCP tools and the additive Toolspace/Code Mode proxy.
|
|
2105
|
+
*/
|
|
2106
|
+
mcpCredentials?(input: McpCredentialsRequest): Promise<McpCredentialResolution>;
|
|
1301
2107
|
};
|
|
1302
2108
|
|
|
1303
|
-
// ============
|
|
2109
|
+
// ============ connection-credential provider — GitHub App API port (BYO-App, §7.6 / GitHub credential prototype remainder) ===
|
|
1304
2110
|
//
|
|
1305
|
-
// The host-driven GitHub-API credential leg.
|
|
1306
|
-
// gate (storage) axis; this closes the credential leg by making the
|
|
2111
|
+
// The host-driven GitHub-API credential leg. GitHub credential prototype closed the establishment +
|
|
2112
|
+
// gate (storage) axis; this closes the credential leg by making the live
|
|
1307
2113
|
// GitHub-API calls host-PROVIDABLE so a BYO-GitHub-App host drives its OWN App
|
|
1308
2114
|
// credentials (its own JWT-signing key, its own OAuth client) instead of
|
|
1309
2115
|
// OpenGeni self-minting from `settings`:
|
|
1310
|
-
// -
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
2116
|
+
// - authorizeUser: OAuth code exchange + user-visible installation and
|
|
2117
|
+
// repository permission discovery. Retained for provider ABI compatibility;
|
|
2118
|
+
// visibility is not proof of installation authority and core does not use
|
|
2119
|
+
// this method for new workspace binding.
|
|
2120
|
+
// - verifyInstallationAccessForUser: OAuth code→token + installation lookup,
|
|
2121
|
+
// also retained for provider ABI compatibility and not used for binding.
|
|
2122
|
+
// - getInstallation: retained in the provider ABI for compatibility. Direct
|
|
2123
|
+
// existing-installation selection is fail-closed and core does not call
|
|
2124
|
+
// this method for binding.
|
|
1313
2125
|
// - listRepositories: the installation-scoped repo listing behind
|
|
1314
2126
|
// `GET /v1/workspaces/:id/github/repositories` (today
|
|
1315
2127
|
// `listGitHubAppRepositories(settings, …)`).
|
|
@@ -1324,11 +2136,31 @@ export type GitHubInstallationSummary = {
|
|
|
1324
2136
|
suspended: boolean;
|
|
1325
2137
|
};
|
|
1326
2138
|
|
|
2139
|
+
export type GitHubRepositoryPermissions = {
|
|
2140
|
+
admin: boolean;
|
|
2141
|
+
maintain: boolean;
|
|
2142
|
+
push: boolean;
|
|
2143
|
+
triage: boolean;
|
|
2144
|
+
pull: boolean;
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2147
|
+
export type GitHubUserRepositoryAccess = GitHubRepository & {
|
|
2148
|
+
permissions: GitHubRepositoryPermissions;
|
|
2149
|
+
};
|
|
2150
|
+
|
|
2151
|
+
export type GitHubUserInstallationAccess = GitHubInstallationSummary & {
|
|
2152
|
+
repositories: GitHubUserRepositoryAccess[];
|
|
2153
|
+
};
|
|
2154
|
+
|
|
1327
2155
|
export type GitHubAppApiPort = {
|
|
2156
|
+
authorizeUser?: (input: { code: string }) => Promise<GitHubUserInstallationAccess[]>;
|
|
1328
2157
|
verifyInstallationAccessForUser?: (input: {
|
|
1329
2158
|
code: string;
|
|
1330
2159
|
installationId: number;
|
|
1331
2160
|
}) => Promise<GitHubInstallationSummary>;
|
|
2161
|
+
getInstallation?: (input: {
|
|
2162
|
+
installationId: number;
|
|
2163
|
+
}) => Promise<GitHubInstallationSummary | null>;
|
|
1332
2164
|
listRepositories?: (input: { installationIds?: number[] }) => Promise<GitHubRepository[]>;
|
|
1333
2165
|
};
|
|
1334
2166
|
|
|
@@ -1368,6 +2200,8 @@ export const RepositoryResourceRef = z.object({
|
|
|
1368
2200
|
mountPath: z.string().min(1).optional(),
|
|
1369
2201
|
subpath: z.string().min(1).optional(),
|
|
1370
2202
|
provider: GitCredentialProvider.optional(),
|
|
2203
|
+
credentialBindingId: GitCredentialBindingId.optional(),
|
|
2204
|
+
access: GitRepositoryAccess.optional(),
|
|
1371
2205
|
repositoryId: GitProviderRepositoryId.optional(),
|
|
1372
2206
|
installationId: GitProviderRepositoryId.optional(),
|
|
1373
2207
|
projectId: GitProviderRepositoryId.optional(),
|
|
@@ -1377,15 +2211,165 @@ export const RepositoryResourceRef = z.object({
|
|
|
1377
2211
|
});
|
|
1378
2212
|
export type RepositoryResourceRef = z.infer<typeof RepositoryResourceRef>;
|
|
1379
2213
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
2214
|
+
function positiveGitProviderInteger(value: unknown): number | null {
|
|
2215
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
2216
|
+
if (typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0) {
|
|
2217
|
+
return Number(value);
|
|
2218
|
+
}
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* Resolve whether a repository participates in platform-brokered Git auth.
|
|
2224
|
+
* Provider-less public repositories return null; legacy GitHub aliases infer
|
|
2225
|
+
* GitHub only when both positive installation and repository ids are present.
|
|
2226
|
+
*/
|
|
2227
|
+
export function gitCredentialProviderForRepository(
|
|
2228
|
+
resource: RepositoryResourceRef,
|
|
2229
|
+
): GitCredentialProvider | null {
|
|
2230
|
+
if (resource.provider) return resource.provider;
|
|
2231
|
+
if (
|
|
2232
|
+
positiveGitProviderInteger(resource.githubInstallationId) &&
|
|
2233
|
+
positiveGitProviderInteger(resource.githubRepositoryId)
|
|
2234
|
+
) {
|
|
2235
|
+
return "github";
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
/**
|
|
2241
|
+
* Derive the one canonical runtime/broker identity for a repository credential.
|
|
2242
|
+
* Every consumer must use this helper so mint grouping, token filenames, and
|
|
2243
|
+
* credential-helper routing cannot diverge on legacy provider ids.
|
|
2244
|
+
*/
|
|
2245
|
+
export function gitCredentialBindingIdForRepository(
|
|
2246
|
+
resource: RepositoryResourceRef,
|
|
2247
|
+
provider: GitCredentialProvider | null = gitCredentialProviderForRepository(resource),
|
|
2248
|
+
): GitCredentialBindingId | null {
|
|
2249
|
+
if (!provider) return null;
|
|
2250
|
+
const installationId =
|
|
2251
|
+
provider === "github"
|
|
2252
|
+
? positiveGitProviderInteger(resource.githubInstallationId ?? resource.installationId)
|
|
2253
|
+
: null;
|
|
2254
|
+
return (
|
|
2255
|
+
resource.credentialBindingId ??
|
|
2256
|
+
resource.connectionId ??
|
|
2257
|
+
(installationId ? `github-installation:${installationId}` : provider)
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
export const FileResourceRef = z.object({
|
|
2262
|
+
kind: z.literal("file"),
|
|
2263
|
+
fileId: z.string().uuid(),
|
|
2264
|
+
mountPath: z.string().min(1).optional(),
|
|
2265
|
+
});
|
|
2266
|
+
export type FileResourceRef = z.infer<typeof FileResourceRef>;
|
|
2267
|
+
|
|
2268
|
+
export const ResourceRef = z.discriminatedUnion("kind", [RepositoryResourceRef, FileResourceRef]);
|
|
2269
|
+
export type ResourceRef = z.infer<typeof ResourceRef>;
|
|
2270
|
+
|
|
2271
|
+
export class ResourceMountPathError extends Error {
|
|
2272
|
+
constructor(message: string) {
|
|
2273
|
+
super(message);
|
|
2274
|
+
this.name = "ResourceMountPathError";
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Normalize one workspace-relative resource mount path for every runtime.
|
|
2280
|
+
*
|
|
2281
|
+
* Backslashes are treated as separators so a path cannot be harmless on Linux
|
|
2282
|
+
* but become traversal on a connected Windows machine. Empty, absolute,
|
|
2283
|
+
* drive-qualified, dot-segment, NUL-containing, and repeated-separator paths
|
|
2284
|
+
* fail closed instead of being silently reinterpreted.
|
|
2285
|
+
*/
|
|
2286
|
+
export function normalizeResourceMountPath(path: string): string {
|
|
2287
|
+
const normalizedSeparators = path.trim().replace(/\\/g, "/");
|
|
2288
|
+
if (
|
|
2289
|
+
!normalizedSeparators ||
|
|
2290
|
+
normalizedSeparators.startsWith("/") ||
|
|
2291
|
+
/^[A-Za-z]:\//.test(normalizedSeparators) ||
|
|
2292
|
+
normalizedSeparators.includes("\0")
|
|
2293
|
+
) {
|
|
2294
|
+
throw new ResourceMountPathError(`invalid resource mount path: ${path}`);
|
|
2295
|
+
}
|
|
2296
|
+
const segments = normalizedSeparators.split("/");
|
|
2297
|
+
if (
|
|
2298
|
+
segments.some(
|
|
2299
|
+
(segment) =>
|
|
2300
|
+
!segment ||
|
|
2301
|
+
segment === "." ||
|
|
2302
|
+
segment === ".." ||
|
|
2303
|
+
/[<>:"|?*\u0000-\u001f]/.test(segment) ||
|
|
2304
|
+
/[ .]$/.test(segment) ||
|
|
2305
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment),
|
|
2306
|
+
)
|
|
2307
|
+
) {
|
|
2308
|
+
throw new ResourceMountPathError(`invalid resource mount path: ${path}`);
|
|
2309
|
+
}
|
|
2310
|
+
return segments.join("/");
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
/** Normalize a repository-internal subpath while preserving legacy `/path/` input. */
|
|
2314
|
+
export function normalizeRepositorySubpath(path: string): string {
|
|
2315
|
+
const relative = path
|
|
2316
|
+
.trim()
|
|
2317
|
+
.replace(/\\/g, "/")
|
|
2318
|
+
.replace(/^\/+|\/+$/g, "");
|
|
2319
|
+
return normalizeResourceMountPath(relative);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/** A conservative collision identity that is portable to case-insensitive hosts. */
|
|
2323
|
+
export function resourceMountPathCollisionKey(path: string): string {
|
|
2324
|
+
return normalizeResourceMountPath(path).normalize("NFKC").toLowerCase();
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Default repository mount identity. The normalized remote host (including a
|
|
2329
|
+
* non-default port) is part of the path, so equal owner/repo names on GitHub,
|
|
2330
|
+
* GitLab, Azure DevOps, or a custom host do not collide. Encoding the host keeps
|
|
2331
|
+
* IPv6/custom-port identities inside one portable path segment.
|
|
2332
|
+
*/
|
|
2333
|
+
export function defaultRepositoryMountPath(uri: string): string {
|
|
2334
|
+
let url: URL;
|
|
2335
|
+
try {
|
|
2336
|
+
url = new URL(uri);
|
|
2337
|
+
} catch {
|
|
2338
|
+
throw new ResourceMountPathError(`invalid repository URI for mount path: ${uri}`);
|
|
2339
|
+
}
|
|
2340
|
+
if (url.protocol !== "https:" || !url.host) {
|
|
2341
|
+
throw new ResourceMountPathError(`invalid repository URI for mount path: ${uri}`);
|
|
2342
|
+
}
|
|
2343
|
+
const repositoryPath = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
2344
|
+
const segments = repositoryPath.split("/").filter(Boolean);
|
|
2345
|
+
if (segments.length < 2) {
|
|
2346
|
+
throw new ResourceMountPathError(`repository URI must include owner and repo: ${uri}`);
|
|
2347
|
+
}
|
|
2348
|
+
return normalizeResourceMountPath(
|
|
2349
|
+
`repos/${encodeURIComponent(url.host.toLowerCase())}/${segments.join("/")}`,
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
/** Resolve the exact mount used by API normalization, manifests, and clone hooks. */
|
|
2354
|
+
export function resourceMountPath(resource: ResourceRef): string {
|
|
2355
|
+
if (resource.mountPath) return normalizeResourceMountPath(resource.mountPath);
|
|
2356
|
+
return resource.kind === "file"
|
|
2357
|
+
? normalizeResourceMountPath(`files/${resource.fileId}`)
|
|
2358
|
+
: defaultRepositoryMountPath(resource.uri);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
/** Fail before sandbox execution when two resources share a portable path. */
|
|
2362
|
+
export function assertUniqueResourceMountPaths(resources: readonly ResourceRef[]): void {
|
|
2363
|
+
const mounted = new Set<string>();
|
|
2364
|
+
for (const resource of resources) {
|
|
2365
|
+
const path = resourceMountPath(resource);
|
|
2366
|
+
const key = resourceMountPathCollisionKey(path);
|
|
2367
|
+
if (mounted.has(key)) {
|
|
2368
|
+
throw new ResourceRefConflictError(`resource mount path is already attached: ${path}`);
|
|
2369
|
+
}
|
|
2370
|
+
mounted.add(key);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
1389
2373
|
|
|
1390
2374
|
export const FileStatus = z.enum(["pending_upload", "ready", "failed", "expired", "deleted"]);
|
|
1391
2375
|
export type FileStatus = z.infer<typeof FileStatus>;
|
|
@@ -1698,6 +2682,63 @@ export const ToolRef = z.object({
|
|
|
1698
2682
|
export type ToolRef = z.infer<typeof ToolRef>;
|
|
1699
2683
|
|
|
1700
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>;
|
|
1701
2742
|
const httpsUrl = z
|
|
1702
2743
|
.string()
|
|
1703
2744
|
.url()
|
|
@@ -1712,42 +2753,101 @@ const httpsUrl = z
|
|
|
1712
2753
|
{ message: "URL must use https" },
|
|
1713
2754
|
);
|
|
1714
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
|
+
|
|
1715
2795
|
export const SessionMcpServerInput = z.object({
|
|
1716
|
-
id:
|
|
2796
|
+
id: SessionMcpServerId,
|
|
1717
2797
|
name: z.string().min(1).optional(),
|
|
1718
2798
|
url: httpsUrl,
|
|
1719
2799
|
allowedTools: z.array(z.string().min(1)).optional(),
|
|
1720
2800
|
timeoutMs: z.number().int().positive().optional(),
|
|
1721
2801
|
cacheToolsList: z.boolean().optional(),
|
|
1722
|
-
//
|
|
1723
|
-
|
|
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(),
|
|
2802
|
+
// The caller resolves an approval pause with `user.approvalDecision`.
|
|
2803
|
+
requireApproval: SessionMcpApprovalPolicy.optional(),
|
|
1728
2804
|
// Write-only credential headers. Values are encrypted at rest and never
|
|
1729
2805
|
// returned in session responses or events; response metadata exposes names.
|
|
1730
2806
|
headers: z.record(z.string(), z.string()).optional(),
|
|
2807
|
+
// Non-secret opaque pointer resolved at request time by the standalone
|
|
2808
|
+
// connection broker or an embedding host's mcpCredentials port.
|
|
2809
|
+
connectionRef: McpServerConnectionRef.optional(),
|
|
1731
2810
|
});
|
|
1732
2811
|
export type SessionMcpServerInput = z.infer<typeof SessionMcpServerInput>;
|
|
1733
2812
|
|
|
1734
2813
|
export const SessionMcpCredentialUpdateInput = z.object({
|
|
1735
|
-
id:
|
|
2814
|
+
id: SessionMcpServerId,
|
|
1736
2815
|
headers: z.record(z.string(), z.string()),
|
|
1737
2816
|
});
|
|
1738
2817
|
export type SessionMcpCredentialUpdateInput = z.infer<typeof SessionMcpCredentialUpdateInput>;
|
|
1739
2818
|
|
|
1740
2819
|
export const SessionMcpServerMetadata = z
|
|
1741
2820
|
.object({
|
|
1742
|
-
id:
|
|
2821
|
+
id: SessionMcpServerId,
|
|
1743
2822
|
name: z.string().min(1).nullable(),
|
|
1744
2823
|
url: httpsUrl,
|
|
1745
2824
|
headerNames: z.array(z.string()).default([]),
|
|
1746
2825
|
credentialVersion: z.number().int().positive(),
|
|
2826
|
+
requireApproval: SessionMcpApprovalPolicy.default(false),
|
|
2827
|
+
connectionRef: McpServerConnectionRef.nullable().default(null),
|
|
1747
2828
|
})
|
|
1748
2829
|
.strict();
|
|
1749
2830
|
export type SessionMcpServerMetadata = z.infer<typeof SessionMcpServerMetadata>;
|
|
1750
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
|
+
|
|
1751
2851
|
export class ResourceRefConflictError extends Error {
|
|
1752
2852
|
constructor(message: string) {
|
|
1753
2853
|
super(message);
|
|
@@ -1783,10 +2883,14 @@ export function mergeResourceRefs(
|
|
|
1783
2883
|
additions: ResourceRef[],
|
|
1784
2884
|
options: { rejectConflicts?: boolean } = {},
|
|
1785
2885
|
): ResourceRef[] {
|
|
2886
|
+
if (options.rejectConflicts) {
|
|
2887
|
+
assertUniqueResourceMountPaths(existing);
|
|
2888
|
+
}
|
|
1786
2889
|
const out = [...existing];
|
|
1787
2890
|
const mountPaths = new Map(
|
|
1788
|
-
existing.
|
|
1789
|
-
|
|
2891
|
+
existing.map(
|
|
2892
|
+
(resource) =>
|
|
2893
|
+
[resourceMountPathCollisionKey(resourceMountPath(resource)), stableJson(resource)] as const,
|
|
1790
2894
|
),
|
|
1791
2895
|
);
|
|
1792
2896
|
const identities = new Map(
|
|
@@ -1800,11 +2904,10 @@ export function mergeResourceRefs(
|
|
|
1800
2904
|
continue;
|
|
1801
2905
|
}
|
|
1802
2906
|
if (options.rejectConflicts) {
|
|
1803
|
-
const
|
|
2907
|
+
const mountPath = resourceMountPath(resource);
|
|
2908
|
+
const existingAtMount = mountPaths.get(resourceMountPathCollisionKey(mountPath));
|
|
1804
2909
|
if (existingAtMount && existingAtMount !== serialized) {
|
|
1805
|
-
throw new ResourceRefConflictError(
|
|
1806
|
-
`resource mount path is already attached: ${resource.mountPath}`,
|
|
1807
|
-
);
|
|
2910
|
+
throw new ResourceRefConflictError(`resource mount path is already attached: ${mountPath}`);
|
|
1808
2911
|
}
|
|
1809
2912
|
const identity = resourceIdentityKey(resource);
|
|
1810
2913
|
const existingIdentity = identities.get(identity);
|
|
@@ -1817,9 +2920,7 @@ export function mergeResourceRefs(
|
|
|
1817
2920
|
out.push(resource);
|
|
1818
2921
|
exact.add(serialized);
|
|
1819
2922
|
identities.set(resourceIdentityKey(resource), serialized);
|
|
1820
|
-
|
|
1821
|
-
mountPaths.set(resource.mountPath, serialized);
|
|
1822
|
-
}
|
|
2923
|
+
mountPaths.set(resourceMountPathCollisionKey(resourceMountPath(resource)), serialized);
|
|
1823
2924
|
}
|
|
1824
2925
|
return out;
|
|
1825
2926
|
}
|
|
@@ -1874,6 +2975,7 @@ export const SessionTurnStatus = z.enum([
|
|
|
1874
2975
|
"failed",
|
|
1875
2976
|
"cancelled",
|
|
1876
2977
|
"superseded",
|
|
2978
|
+
"withdrawn_for_edit",
|
|
1877
2979
|
]);
|
|
1878
2980
|
export type SessionTurnStatus = z.infer<typeof SessionTurnStatus>;
|
|
1879
2981
|
|
|
@@ -1987,7 +3089,9 @@ export type ClearSessionContextRequest = z.infer<typeof ClearSessionContextReque
|
|
|
1987
3089
|
export const CLEARED_RUN_STATE_MARKER = "$opengeniCleared" as const;
|
|
1988
3090
|
|
|
1989
3091
|
/** The canonical sentinel serializedRunState value a context clear stores. */
|
|
1990
|
-
export const CLEARED_RUN_STATE_BLOB = JSON.stringify({
|
|
3092
|
+
export const CLEARED_RUN_STATE_BLOB = JSON.stringify({
|
|
3093
|
+
[CLEARED_RUN_STATE_MARKER]: true,
|
|
3094
|
+
});
|
|
1991
3095
|
|
|
1992
3096
|
/**
|
|
1993
3097
|
* True when a serialized run-state blob is the cleared sentinel rather than a
|
|
@@ -2027,6 +3131,169 @@ export const CompactSessionContextResult = z.object({
|
|
|
2027
3131
|
});
|
|
2028
3132
|
export type CompactSessionContextResult = z.infer<typeof CompactSessionContextResult>;
|
|
2029
3133
|
|
|
3134
|
+
/**
|
|
3135
|
+
* The principal whose authority accepted a session or turn. `subjectId` is an
|
|
3136
|
+
* opaque host/standalone identity and therefore must never encode `kind` by
|
|
3137
|
+
* convention: embedding hosts own their subject namespace.
|
|
3138
|
+
*/
|
|
3139
|
+
export const TurnInitiator = z.object({
|
|
3140
|
+
kind: z.enum(["subject", "service"]),
|
|
3141
|
+
...turnInitiatorIdentityFields,
|
|
3142
|
+
});
|
|
3143
|
+
export type TurnInitiator = z.infer<typeof TurnInitiator>;
|
|
3144
|
+
|
|
3145
|
+
// ============ embedding host session authorization ============
|
|
3146
|
+
//
|
|
3147
|
+
// Workspace permissions answer whether a principal may use an OpenGeni
|
|
3148
|
+
// capability. An embedding host can additionally own per-session visibility
|
|
3149
|
+
// (ownership, sharing, nested workspaces, revocation). This port is the one
|
|
3150
|
+
// host-neutral boundary for that second decision. Inputs contain OpenGeni ids
|
|
3151
|
+
// and immutable, non-secret authority only; host records and policy details
|
|
3152
|
+
// never cross the boundary.
|
|
3153
|
+
|
|
3154
|
+
export const SessionAuthorizationSurface = z.enum([
|
|
3155
|
+
"http",
|
|
3156
|
+
"core",
|
|
3157
|
+
"stream",
|
|
3158
|
+
"first_party_mcp",
|
|
3159
|
+
"toolspace",
|
|
3160
|
+
]);
|
|
3161
|
+
export type SessionAuthorizationSurface = z.infer<typeof SessionAuthorizationSurface>;
|
|
3162
|
+
|
|
3163
|
+
export const SessionAuthorizationOperation = z.enum([
|
|
3164
|
+
"session.read",
|
|
3165
|
+
"session.events.read",
|
|
3166
|
+
"session.stream.read",
|
|
3167
|
+
"session.stream.acknowledge",
|
|
3168
|
+
"session.turns.read",
|
|
3169
|
+
"session.append",
|
|
3170
|
+
"session.steer",
|
|
3171
|
+
"session.control",
|
|
3172
|
+
"session.queue.read",
|
|
3173
|
+
"session.queue.control",
|
|
3174
|
+
"session.composer.read",
|
|
3175
|
+
"session.composer.write",
|
|
3176
|
+
"session.lineage.read",
|
|
3177
|
+
"session.capture.read",
|
|
3178
|
+
"session.files.read",
|
|
3179
|
+
"session.files.write",
|
|
3180
|
+
"session.git.read",
|
|
3181
|
+
"session.terminal.read",
|
|
3182
|
+
"session.terminal.control",
|
|
3183
|
+
"session.viewer.read",
|
|
3184
|
+
"session.viewer.control",
|
|
3185
|
+
"session.first_party_mcp.call",
|
|
3186
|
+
"session.toolspace.call",
|
|
3187
|
+
"session.pin.write",
|
|
3188
|
+
"session.codex_account.write",
|
|
3189
|
+
"session.context.write",
|
|
3190
|
+
"session.approval.write",
|
|
3191
|
+
"session.human_input.read",
|
|
3192
|
+
"session.human_input.write",
|
|
3193
|
+
"session.title.write",
|
|
3194
|
+
"session.mcp.approval_policy.write",
|
|
3195
|
+
"session.goal.read",
|
|
3196
|
+
"session.goal.write",
|
|
3197
|
+
"session.child.create",
|
|
3198
|
+
]);
|
|
3199
|
+
export type SessionAuthorizationOperation = z.infer<typeof SessionAuthorizationOperation>;
|
|
3200
|
+
|
|
3201
|
+
export const SessionAuthorizationActor = z.discriminatedUnion("kind", [
|
|
3202
|
+
z.object({
|
|
3203
|
+
kind: z.literal("subject"),
|
|
3204
|
+
subjectId: z.string().min(1),
|
|
3205
|
+
subjectLabel: z.string().min(1).optional(),
|
|
3206
|
+
}),
|
|
3207
|
+
z.object({
|
|
3208
|
+
kind: z.literal("agent_attempt"),
|
|
3209
|
+
/** Technical, authenticated first-party caller (not the host authority). */
|
|
3210
|
+
subjectId: z.string().min(1),
|
|
3211
|
+
callerSessionId: z.string().uuid(),
|
|
3212
|
+
callerRootSessionId: z.string().uuid(),
|
|
3213
|
+
turnId: z.string().uuid(),
|
|
3214
|
+
attemptId: z.string().uuid(),
|
|
3215
|
+
executionGeneration: z.number().int().positive(),
|
|
3216
|
+
/** Frozen authority that admitted the calling turn. */
|
|
3217
|
+
initiator: TurnInitiator,
|
|
3218
|
+
initiatorContext: TurnInitiatorContext,
|
|
3219
|
+
}),
|
|
3220
|
+
]);
|
|
3221
|
+
export type SessionAuthorizationActor = z.infer<typeof SessionAuthorizationActor>;
|
|
3222
|
+
|
|
3223
|
+
export const SessionAuthorizationTarget = z.object({
|
|
3224
|
+
sessionId: z.string().uuid(),
|
|
3225
|
+
/** Server-resolved workspace lineage root; never accepted from a caller. */
|
|
3226
|
+
rootSessionId: z.string().uuid(),
|
|
3227
|
+
});
|
|
3228
|
+
export type SessionAuthorizationTarget = z.infer<typeof SessionAuthorizationTarget>;
|
|
3229
|
+
|
|
3230
|
+
export type AuthorizeSessionInput = {
|
|
3231
|
+
accountId: string;
|
|
3232
|
+
workspaceId: string;
|
|
3233
|
+
actor: SessionAuthorizationActor;
|
|
3234
|
+
target: SessionAuthorizationTarget;
|
|
3235
|
+
operation: SessionAuthorizationOperation;
|
|
3236
|
+
surface: SessionAuthorizationSurface;
|
|
3237
|
+
};
|
|
3238
|
+
|
|
3239
|
+
export const SessionAuthorizationDecision = z.discriminatedUnion("allowed", [
|
|
3240
|
+
z.object({
|
|
3241
|
+
allowed: z.literal(true),
|
|
3242
|
+
/**
|
|
3243
|
+
* Whether related-session metadata may be projected with the target.
|
|
3244
|
+
* `target` is the fail-closed default for exact shares; `root` permits the
|
|
3245
|
+
* target's full lineage tree. This does not authorize a separate operation
|
|
3246
|
+
* against another session, which always requires its own decision.
|
|
3247
|
+
*/
|
|
3248
|
+
relatedSessionAccess: z.enum(["target", "root"]).optional(),
|
|
3249
|
+
/** A host may request a tighter stream reauthorization bound. */
|
|
3250
|
+
reauthorizeAfterMs: z.number().int().min(1_000).max(60_000).optional(),
|
|
3251
|
+
}),
|
|
3252
|
+
z.object({
|
|
3253
|
+
allowed: z.literal(false),
|
|
3254
|
+
reason: z.enum(["not_found", "forbidden", "revoked"]),
|
|
3255
|
+
}),
|
|
3256
|
+
]);
|
|
3257
|
+
export type SessionAuthorizationDecision = z.infer<typeof SessionAuthorizationDecision>;
|
|
3258
|
+
|
|
3259
|
+
/**
|
|
3260
|
+
* A database-applicable listing scope. `rootSessionIds` includes every
|
|
3261
|
+
* descendant of those lineage anchors; `sessionIds` authorizes only the exact
|
|
3262
|
+
* sessions. Supplying neither is an explicit empty scope. OpenGeni intersects
|
|
3263
|
+
* every id with the requested workspace and never trusts a host scope as
|
|
3264
|
+
* session existence evidence.
|
|
3265
|
+
*/
|
|
3266
|
+
export const SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS = 10_000;
|
|
3267
|
+
|
|
3268
|
+
export const SessionAuthorizationListScope = z.discriminatedUnion("kind", [
|
|
3269
|
+
z.object({ kind: z.literal("all") }),
|
|
3270
|
+
z.object({
|
|
3271
|
+
kind: z.literal("scoped"),
|
|
3272
|
+
rootSessionIds: z.array(z.string().uuid()).max(SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS),
|
|
3273
|
+
sessionIds: z.array(z.string().uuid()).max(SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS),
|
|
3274
|
+
}),
|
|
3275
|
+
]);
|
|
3276
|
+
export type SessionAuthorizationListScope = z.infer<typeof SessionAuthorizationListScope>;
|
|
3277
|
+
|
|
3278
|
+
export type ResolveSessionAuthorizationListScopeInput = {
|
|
3279
|
+
accountId: string;
|
|
3280
|
+
workspaceId: string;
|
|
3281
|
+
actor: SessionAuthorizationActor;
|
|
3282
|
+
surface: SessionAuthorizationSurface;
|
|
3283
|
+
};
|
|
3284
|
+
|
|
3285
|
+
export type SessionAuthorizationPort = {
|
|
3286
|
+
authorizeSession(input: AuthorizeSessionInput): Promise<SessionAuthorizationDecision>;
|
|
3287
|
+
/**
|
|
3288
|
+
* Return the complete current scope used inside OpenGeni's cursor query.
|
|
3289
|
+
* This is deliberately not a post-filter callback: search, pinning, ordering,
|
|
3290
|
+
* totals, and cursor advancement must all operate on authorized rows.
|
|
3291
|
+
*/
|
|
3292
|
+
resolveListScope(
|
|
3293
|
+
input: ResolveSessionAuthorizationListScopeInput,
|
|
3294
|
+
): Promise<SessionAuthorizationListScope>;
|
|
3295
|
+
};
|
|
3296
|
+
|
|
2030
3297
|
export const SessionTurn = z.object({
|
|
2031
3298
|
id: z.string().uuid(),
|
|
2032
3299
|
workspaceId: z.string().uuid(),
|
|
@@ -2039,6 +3306,10 @@ export const SessionTurn = z.object({
|
|
|
2039
3306
|
prompt: z.string().min(1),
|
|
2040
3307
|
resources: z.array(ResourceRef),
|
|
2041
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(),
|
|
2042
3313
|
model: z.string().min(1),
|
|
2043
3314
|
reasoningEffort: ReasoningEffort,
|
|
2044
3315
|
sandboxBackend: SandboxBackend,
|
|
@@ -2049,6 +3320,8 @@ export const SessionTurn = z.object({
|
|
|
2049
3320
|
executionGeneration: z.number().int().nonnegative(),
|
|
2050
3321
|
activeAttemptId: z.string().uuid().nullable(),
|
|
2051
3322
|
lineage: z.record(z.string(), z.unknown()),
|
|
3323
|
+
initiator: TurnInitiator,
|
|
3324
|
+
initiatorContext: TurnInitiatorContext,
|
|
2052
3325
|
cancelledBy: z.string().nullable(),
|
|
2053
3326
|
cancelReason: z.string().nullable(),
|
|
2054
3327
|
startedAt: z.string().nullable(),
|
|
@@ -2058,70 +3331,413 @@ export const SessionTurn = z.object({
|
|
|
2058
3331
|
});
|
|
2059
3332
|
export type SessionTurn = z.infer<typeof SessionTurn>;
|
|
2060
3333
|
|
|
3334
|
+
export const EffectiveControlBlocker = z.object({
|
|
3335
|
+
kind: z.enum(["session", "workspace"]),
|
|
3336
|
+
sessionId: z.string().uuid().optional(),
|
|
3337
|
+
displayName: z.string().min(1),
|
|
3338
|
+
actor: z.string().nullable(),
|
|
3339
|
+
reason: z.string().nullable(),
|
|
3340
|
+
changedAt: z.string().nullable(),
|
|
3341
|
+
revision: z.number().int().nonnegative(),
|
|
3342
|
+
});
|
|
3343
|
+
export type EffectiveControlBlocker = z.infer<typeof EffectiveControlBlocker>;
|
|
3344
|
+
|
|
3345
|
+
export const EffectiveControlResumeOption = z.object({
|
|
3346
|
+
scope: z.enum(["selected", "session", "workspace"]),
|
|
3347
|
+
targetId: z.string().uuid().optional(),
|
|
3348
|
+
selectedStateAfter: SessionControlState,
|
|
3349
|
+
remainingPrimaryBlocker: EffectiveControlBlocker.optional(),
|
|
3350
|
+
impactCopy: z.string().min(1),
|
|
3351
|
+
});
|
|
3352
|
+
export type EffectiveControlResumeOption = z.infer<typeof EffectiveControlResumeOption>;
|
|
3353
|
+
|
|
3354
|
+
export const EffectiveSessionControl = z.object({
|
|
3355
|
+
state: SessionControlState,
|
|
3356
|
+
controlVersion: z.number().int().nonnegative(),
|
|
3357
|
+
controlEtag: z.string().min(1),
|
|
3358
|
+
directState: SessionControlState,
|
|
3359
|
+
primaryBlocker: EffectiveControlBlocker.nullable(),
|
|
3360
|
+
additionalBlockerCount: z.number().int().nonnegative(),
|
|
3361
|
+
blockers: z.array(EffectiveControlBlocker),
|
|
3362
|
+
resumeOptions: z.array(EffectiveControlResumeOption),
|
|
3363
|
+
override: z
|
|
3364
|
+
.object({
|
|
3365
|
+
rootSessionId: z.string().uuid(),
|
|
3366
|
+
revision: z.number().int().nonnegative(),
|
|
3367
|
+
})
|
|
3368
|
+
.nullable(),
|
|
3369
|
+
settlement: z
|
|
3370
|
+
.object({
|
|
3371
|
+
state: z.literal("stopping"),
|
|
3372
|
+
attemptCount: z.number().int().positive(),
|
|
3373
|
+
interruptionPendingCount: z.number().int().nonnegative(),
|
|
3374
|
+
quiescencePendingCount: z.number().int().nonnegative(),
|
|
3375
|
+
})
|
|
3376
|
+
.nullable(),
|
|
3377
|
+
});
|
|
3378
|
+
export type EffectiveSessionControl = z.infer<typeof EffectiveSessionControl>;
|
|
3379
|
+
|
|
3380
|
+
export const SESSION_OPERATION_KEY_MAX_CHARS = 256;
|
|
3381
|
+
const SessionOperationKey = z.string().min(1).max(SESSION_OPERATION_KEY_MAX_CHARS);
|
|
3382
|
+
|
|
3383
|
+
export const SessionCommandReceipt = z.object({
|
|
3384
|
+
id: z.string().uuid(),
|
|
3385
|
+
action: z.string().min(1),
|
|
3386
|
+
operationKey: z.string().min(1).max(SESSION_OPERATION_KEY_MAX_CHARS),
|
|
3387
|
+
targetSessionId: z.string().uuid().nullable(),
|
|
3388
|
+
targetTurnId: z.string().uuid().nullable(),
|
|
3389
|
+
appliedControlRevision: z.number().int().nonnegative().nullable(),
|
|
3390
|
+
appliedQueueVersion: z.number().int().nonnegative().nullable(),
|
|
3391
|
+
appliedTurnVersion: z.number().int().positive().nullable(),
|
|
3392
|
+
appliedDraftRevision: z.number().int().positive().nullable(),
|
|
3393
|
+
createdAt: z.string(),
|
|
3394
|
+
});
|
|
3395
|
+
export type SessionCommandReceipt = z.infer<typeof SessionCommandReceipt>;
|
|
3396
|
+
|
|
3397
|
+
export const ComposerDraft = z.object({
|
|
3398
|
+
revision: z.number().int().nonnegative(),
|
|
3399
|
+
text: z.string(),
|
|
3400
|
+
resources: z.array(ResourceRef),
|
|
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),
|
|
3405
|
+
model: z.string().min(1),
|
|
3406
|
+
reasoningEffort: ReasoningEffort,
|
|
3407
|
+
sourceTurnId: z.string().uuid().nullable(),
|
|
3408
|
+
sourceTurnVersion: z.number().int().positive().nullable(),
|
|
3409
|
+
updatedAt: z.string().nullable(),
|
|
3410
|
+
});
|
|
3411
|
+
export type ComposerDraft = z.infer<typeof ComposerDraft>;
|
|
3412
|
+
|
|
2061
3413
|
export const SessionQueueSnapshot = z.object({
|
|
2062
3414
|
version: z.number().int().nonnegative(),
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
3415
|
+
effectiveControl: EffectiveSessionControl,
|
|
3416
|
+
/**
|
|
3417
|
+
* True while the latest attempt is interrupted but has not durably proved
|
|
3418
|
+
* quiescence: no more inference, user-visible output, or workspace-persistence
|
|
3419
|
+
* authority. Temporal cancellation/terminalization is not that proof. This is
|
|
3420
|
+
* distinct from ordinary capacity queueing, remains accurate with an empty
|
|
3421
|
+
* visible queue, and is independent of Steer-row metadata or withdrawal.
|
|
3422
|
+
*/
|
|
3423
|
+
stoppingPreviousAttempt: z.boolean(),
|
|
2068
3424
|
items: z.array(SessionTurn),
|
|
2069
3425
|
});
|
|
2070
3426
|
export type SessionQueueSnapshot = z.infer<typeof SessionQueueSnapshot>;
|
|
2071
3427
|
|
|
2072
|
-
export const
|
|
3428
|
+
export const MoveSessionQueueItemRequest = z.object({
|
|
3429
|
+
clientEventId: SessionOperationKey,
|
|
2073
3430
|
expectedQueueVersion: z.number().int().nonnegative(),
|
|
2074
|
-
|
|
3431
|
+
beforeTurnId: z.string().uuid().nullable(),
|
|
3432
|
+
});
|
|
3433
|
+
export type MoveSessionQueueItemRequest = z.infer<typeof MoveSessionQueueItemRequest>;
|
|
3434
|
+
|
|
3435
|
+
export const EditSessionQueueItemRequest = z.object({
|
|
3436
|
+
clientEventId: SessionOperationKey,
|
|
3437
|
+
expectedTurnVersion: z.number().int().positive(),
|
|
3438
|
+
expectedDraftRevision: z.number().int().nonnegative(),
|
|
3439
|
+
replaceDraft: z.boolean(),
|
|
3440
|
+
});
|
|
3441
|
+
export type EditSessionQueueItemRequest = z.infer<typeof EditSessionQueueItemRequest>;
|
|
3442
|
+
|
|
3443
|
+
export const SteerSessionQueueItemRequest = z.object({
|
|
3444
|
+
clientEventId: SessionOperationKey,
|
|
3445
|
+
expectedTurnVersion: z.number().int().positive(),
|
|
3446
|
+
controlEtag: z.string().min(1).optional(),
|
|
3447
|
+
});
|
|
3448
|
+
export type SteerSessionQueueItemRequest = z.infer<typeof SteerSessionQueueItemRequest>;
|
|
3449
|
+
|
|
3450
|
+
export const DeleteSessionQueueItemRequest = z.object({
|
|
3451
|
+
clientEventId: SessionOperationKey,
|
|
3452
|
+
expectedTurnVersion: z.number().int().positive(),
|
|
2075
3453
|
reason: z.string().min(1).optional(),
|
|
2076
3454
|
});
|
|
2077
|
-
export type
|
|
3455
|
+
export type DeleteSessionQueueItemRequest = z.infer<typeof DeleteSessionQueueItemRequest>;
|
|
3456
|
+
|
|
3457
|
+
export const SaveComposerDraftRequest = ComposerDraft.pick({
|
|
3458
|
+
text: true,
|
|
3459
|
+
resources: true,
|
|
3460
|
+
tools: true,
|
|
3461
|
+
toolsProvided: true,
|
|
3462
|
+
model: true,
|
|
3463
|
+
reasoningEffort: true,
|
|
3464
|
+
}).extend({ expectedRevision: z.number().int().nonnegative() });
|
|
3465
|
+
export type SaveComposerDraftRequest = z.infer<typeof SaveComposerDraftRequest>;
|
|
3466
|
+
|
|
3467
|
+
export const WORKSPACE_CONTROL_REASON_MAX_BYTES = 8 * 1024;
|
|
3468
|
+
export const WORKSPACE_CONTROL_ACTOR_MAX_BYTES = 1024;
|
|
3469
|
+
export const WORKSPACE_CONTROL_EVENT_MAX_BYTES = 16 * 1024;
|
|
3470
|
+
|
|
3471
|
+
const WorkspaceControlReason = z
|
|
3472
|
+
.string()
|
|
3473
|
+
.min(1)
|
|
3474
|
+
.refine((value) => !value.includes("\u0000"), "reason must not contain NUL bytes")
|
|
3475
|
+
.refine(
|
|
3476
|
+
(value) => workspaceControlUtf8Bytes(value) <= WORKSPACE_CONTROL_REASON_MAX_BYTES,
|
|
3477
|
+
`reason must not exceed ${WORKSPACE_CONTROL_REASON_MAX_BYTES} UTF-8 bytes`,
|
|
3478
|
+
);
|
|
2078
3479
|
|
|
2079
3480
|
export const SessionControlRequest = z.object({
|
|
2080
|
-
|
|
2081
|
-
reason:
|
|
2082
|
-
clientEventId:
|
|
2083
|
-
|
|
2084
|
-
expectedControlGeneration: z.number().int().nonnegative().optional(),
|
|
2085
|
-
expectedWorkspaceInferenceGeneration: z.number().int().nonnegative().optional(),
|
|
3481
|
+
action: z.enum(["pause", "resume"]),
|
|
3482
|
+
reason: WorkspaceControlReason.optional(),
|
|
3483
|
+
clientEventId: SessionOperationKey,
|
|
3484
|
+
expectedControlEtag: z.string().min(1).optional(),
|
|
2086
3485
|
});
|
|
2087
3486
|
export type SessionControlRequest = z.infer<typeof SessionControlRequest>;
|
|
2088
3487
|
|
|
2089
3488
|
export const WorkspaceInferenceControlRequest = z.object({
|
|
2090
|
-
|
|
2091
|
-
reason:
|
|
2092
|
-
clientEventId:
|
|
2093
|
-
|
|
2094
|
-
expectedGeneration: z.number().int().nonnegative(),
|
|
2095
|
-
exceptSessionIds: z.array(z.string().uuid()).default([]),
|
|
3489
|
+
action: z.enum(["pause", "resume"]),
|
|
3490
|
+
reason: WorkspaceControlReason.optional(),
|
|
3491
|
+
clientEventId: SessionOperationKey,
|
|
3492
|
+
expectedRevision: z.number().int().nonnegative().optional(),
|
|
2096
3493
|
});
|
|
2097
3494
|
export type WorkspaceInferenceControlRequest = z.infer<typeof WorkspaceInferenceControlRequest>;
|
|
2098
3495
|
|
|
2099
3496
|
export const WorkspaceInferenceControlResponse = z.object({
|
|
2100
|
-
|
|
2101
|
-
state:
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
exceptionSessionIds: z.array(z.string().uuid()),
|
|
3497
|
+
receipt: SessionCommandReceipt,
|
|
3498
|
+
state: SessionControlState,
|
|
3499
|
+
revision: z.number().int().nonnegative(),
|
|
3500
|
+
interruptionCount: z.number().int().nonnegative(),
|
|
3501
|
+
wakeCount: z.number().int().nonnegative(),
|
|
2106
3502
|
});
|
|
2107
3503
|
export type WorkspaceInferenceControlResponse = z.infer<typeof WorkspaceInferenceControlResponse>;
|
|
2108
3504
|
|
|
3505
|
+
/**
|
|
3506
|
+
* One durable workspace-wide invalidation for one committed control revision.
|
|
3507
|
+
* It is not conversation history and never becomes queue work; clients use it
|
|
3508
|
+
* only to refetch authoritative workspace/session projections.
|
|
3509
|
+
*/
|
|
3510
|
+
export const WorkspaceControlEventTruncation = z.object({
|
|
3511
|
+
truncated: z.literal(true),
|
|
3512
|
+
surface: z.enum([
|
|
3513
|
+
"durable_control",
|
|
3514
|
+
"database_guard",
|
|
3515
|
+
"http_projection",
|
|
3516
|
+
"nats_legacy_guard",
|
|
3517
|
+
"sse_legacy_guard",
|
|
3518
|
+
]),
|
|
3519
|
+
deliveredBytes: z.number().int().nonnegative(),
|
|
3520
|
+
fields: z.array(
|
|
3521
|
+
z.object({
|
|
3522
|
+
field: z.enum(["reason", "actor"]),
|
|
3523
|
+
originalBytes: z.number().int().nonnegative(),
|
|
3524
|
+
deliveredBytes: z.number().int().nonnegative(),
|
|
3525
|
+
omittedBytes: z.number().int().nonnegative(),
|
|
3526
|
+
}),
|
|
3527
|
+
),
|
|
3528
|
+
fullEvidence: z.object({
|
|
3529
|
+
available: z.literal(false),
|
|
3530
|
+
reason: z.literal("not_retained"),
|
|
3531
|
+
}),
|
|
3532
|
+
});
|
|
3533
|
+
export type WorkspaceControlEventTruncation = z.infer<typeof WorkspaceControlEventTruncation>;
|
|
3534
|
+
|
|
3535
|
+
export const WorkspaceControlEvent = z.object({
|
|
3536
|
+
id: z.string().uuid(),
|
|
3537
|
+
workspaceId: z.string().uuid(),
|
|
3538
|
+
sequence: z.number().int().positive(),
|
|
3539
|
+
revision: z.number().int().positive(),
|
|
3540
|
+
type: z.literal("workspace.control.changed"),
|
|
3541
|
+
scope: z.enum(["workspace", "session"]),
|
|
3542
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
3543
|
+
action: z.enum(["pause", "resume"]),
|
|
3544
|
+
automatic: z.boolean(),
|
|
3545
|
+
reason: z.string().nullable(),
|
|
3546
|
+
actor: z.string().min(1),
|
|
3547
|
+
occurredAt: z.string(),
|
|
3548
|
+
truncation: WorkspaceControlEventTruncation.nullable().optional(),
|
|
3549
|
+
});
|
|
3550
|
+
export type WorkspaceControlEvent = z.infer<typeof WorkspaceControlEvent>;
|
|
3551
|
+
|
|
3552
|
+
export type WorkspaceControlBoundarySurface = WorkspaceControlEventTruncation["surface"];
|
|
3553
|
+
|
|
3554
|
+
export type BoundWorkspaceControlEventOptions = {
|
|
3555
|
+
surface?: WorkspaceControlBoundarySurface;
|
|
3556
|
+
reasonOriginalBytes?: number | null;
|
|
3557
|
+
actorOriginalBytes?: number | null;
|
|
3558
|
+
};
|
|
3559
|
+
|
|
3560
|
+
/** UTF-8 byte count used by workspace-control storage and transport guards. */
|
|
3561
|
+
export function workspaceControlUtf8Bytes(value: string): number {
|
|
3562
|
+
return new TextEncoder().encode(value).byteLength;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
/**
|
|
3566
|
+
* Canonical bounded invalidation event. The event is not a full evidence store:
|
|
3567
|
+
* when a producer or legacy row exceeds a field cap, the retained head carries
|
|
3568
|
+
* a visible marker and structured exact byte-loss facts.
|
|
3569
|
+
*/
|
|
3570
|
+
export function boundWorkspaceControlEvent(
|
|
3571
|
+
event: WorkspaceControlEvent,
|
|
3572
|
+
options: BoundWorkspaceControlEventOptions = {},
|
|
3573
|
+
): WorkspaceControlEvent {
|
|
3574
|
+
const existingFields = new Map(
|
|
3575
|
+
(event.truncation?.fields ?? []).map((field) => [field.field, field] as const),
|
|
3576
|
+
);
|
|
3577
|
+
const reason =
|
|
3578
|
+
event.reason === null
|
|
3579
|
+
? null
|
|
3580
|
+
: boundWorkspaceControlText(event.reason, WORKSPACE_CONTROL_REASON_MAX_BYTES);
|
|
3581
|
+
const actor = boundWorkspaceControlText(event.actor, WORKSPACE_CONTROL_ACTOR_MAX_BYTES);
|
|
3582
|
+
const reasonBytes = reason === null ? 0 : workspaceControlUtf8Bytes(reason);
|
|
3583
|
+
const actorBytes = workspaceControlUtf8Bytes(actor);
|
|
3584
|
+
const reasonOriginalBytes =
|
|
3585
|
+
event.reason === null
|
|
3586
|
+
? null
|
|
3587
|
+
: Math.max(
|
|
3588
|
+
workspaceControlUtf8Bytes(event.reason),
|
|
3589
|
+
normalizedWorkspaceControlOriginalBytes(options.reasonOriginalBytes),
|
|
3590
|
+
existingFields.get("reason")?.originalBytes ?? 0,
|
|
3591
|
+
);
|
|
3592
|
+
const actorOriginalBytes = Math.max(
|
|
3593
|
+
workspaceControlUtf8Bytes(event.actor),
|
|
3594
|
+
normalizedWorkspaceControlOriginalBytes(options.actorOriginalBytes),
|
|
3595
|
+
existingFields.get("actor")?.originalBytes ?? 0,
|
|
3596
|
+
);
|
|
3597
|
+
const fields: WorkspaceControlEventTruncation["fields"] = [];
|
|
3598
|
+
if (reasonOriginalBytes !== null && reasonOriginalBytes > reasonBytes) {
|
|
3599
|
+
fields.push({
|
|
3600
|
+
field: "reason",
|
|
3601
|
+
originalBytes: reasonOriginalBytes,
|
|
3602
|
+
deliveredBytes: reasonBytes,
|
|
3603
|
+
omittedBytes: reasonOriginalBytes - reasonBytes,
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
if (actorOriginalBytes > actorBytes) {
|
|
3607
|
+
fields.push({
|
|
3608
|
+
field: "actor",
|
|
3609
|
+
originalBytes: actorOriginalBytes,
|
|
3610
|
+
deliveredBytes: actorBytes,
|
|
3611
|
+
omittedBytes: actorOriginalBytes - actorBytes,
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
if (fields.length === 0 && event.truncation == null) {
|
|
3615
|
+
if (sessionEventJsonBytes(event) > WORKSPACE_CONTROL_EVENT_MAX_BYTES) {
|
|
3616
|
+
throw new RangeError("Workspace control event exceeds its bounded envelope");
|
|
3617
|
+
}
|
|
3618
|
+
return event;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
const truncation: WorkspaceControlEventTruncation = {
|
|
3622
|
+
truncated: true,
|
|
3623
|
+
surface: event.truncation?.surface ?? options.surface ?? "durable_control",
|
|
3624
|
+
deliveredBytes: 0,
|
|
3625
|
+
fields,
|
|
3626
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
3627
|
+
};
|
|
3628
|
+
const bounded: WorkspaceControlEvent = {
|
|
3629
|
+
...event,
|
|
3630
|
+
reason,
|
|
3631
|
+
actor,
|
|
3632
|
+
truncation,
|
|
3633
|
+
};
|
|
3634
|
+
settleWorkspaceControlDeliveredBytes(bounded, truncation);
|
|
3635
|
+
const deliveredBytes = sessionEventJsonBytes(bounded);
|
|
3636
|
+
if (deliveredBytes > WORKSPACE_CONTROL_EVENT_MAX_BYTES) {
|
|
3637
|
+
throw new RangeError(
|
|
3638
|
+
`Bounded workspace control event exceeds its final envelope (${deliveredBytes} > ${WORKSPACE_CONTROL_EVENT_MAX_BYTES} bytes)`,
|
|
3639
|
+
);
|
|
3640
|
+
}
|
|
3641
|
+
return bounded;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
function boundWorkspaceControlText(value: string, maxBytes: number): string {
|
|
3645
|
+
const encoder = new TextEncoder();
|
|
3646
|
+
const decoder = new TextDecoder();
|
|
3647
|
+
const bytes = encoder.encode(value);
|
|
3648
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
3649
|
+
const marker = "…[truncated]";
|
|
3650
|
+
const prefixBudget = Math.max(0, maxBytes - encoder.encode(marker).byteLength);
|
|
3651
|
+
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
3652
|
+
while (prefixEnd > 0 && prefixEnd < bytes.byteLength && (bytes[prefixEnd]! & 0xc0) === 0x80) {
|
|
3653
|
+
prefixEnd -= 1;
|
|
3654
|
+
}
|
|
3655
|
+
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
function normalizedWorkspaceControlOriginalBytes(value: number | null | undefined): number {
|
|
3659
|
+
return value === null || value === undefined || !Number.isFinite(value)
|
|
3660
|
+
? 0
|
|
3661
|
+
: Math.max(0, Math.floor(value));
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
function settleWorkspaceControlDeliveredBytes(
|
|
3665
|
+
event: WorkspaceControlEvent,
|
|
3666
|
+
truncation: WorkspaceControlEventTruncation,
|
|
3667
|
+
): void {
|
|
3668
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
3669
|
+
const deliveredBytes = sessionEventJsonBytes(event);
|
|
3670
|
+
if (truncation.deliveredBytes === deliveredBytes) return;
|
|
3671
|
+
truncation.deliveredBytes = deliveredBytes;
|
|
3672
|
+
}
|
|
3673
|
+
const deliveredBytes = sessionEventJsonBytes(event);
|
|
3674
|
+
if (truncation.deliveredBytes !== deliveredBytes) {
|
|
3675
|
+
throw new RangeError("Workspace control event byte accounting did not converge");
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
|
|
2109
3679
|
export const SystemUpdateClassification = z.enum(["success", "failure", "action_required", "info"]);
|
|
2110
3680
|
export type SystemUpdateClassification = z.infer<typeof SystemUpdateClassification>;
|
|
2111
3681
|
|
|
2112
3682
|
export const SessionSystemUpdateKind = z.enum([
|
|
2113
|
-
"
|
|
2114
|
-
"
|
|
2115
|
-
"
|
|
2116
|
-
"
|
|
3683
|
+
"scheduled_occurrence",
|
|
3684
|
+
"goal_continuation",
|
|
3685
|
+
"agent_message",
|
|
3686
|
+
"agent_steer_instruction",
|
|
3687
|
+
"child_terminal_result",
|
|
2117
3688
|
]);
|
|
2118
3689
|
export type SessionSystemUpdateKind = z.infer<typeof SessionSystemUpdateKind>;
|
|
2119
3690
|
|
|
3691
|
+
export const SessionSystemUpdatePayload = z.discriminatedUnion("type", [
|
|
3692
|
+
z
|
|
3693
|
+
.object({
|
|
3694
|
+
type: z.literal("scheduled_occurrence"),
|
|
3695
|
+
text: z.string().min(1),
|
|
3696
|
+
scheduledTaskId: z.string().uuid(),
|
|
3697
|
+
scheduledTaskRunId: z.string().uuid(),
|
|
3698
|
+
resources: z.array(ResourceRef).optional(),
|
|
3699
|
+
tools: z.array(ToolRef).optional(),
|
|
3700
|
+
})
|
|
3701
|
+
.passthrough(),
|
|
3702
|
+
z
|
|
3703
|
+
.object({
|
|
3704
|
+
type: z.literal("goal_continuation"),
|
|
3705
|
+
goalId: z.string().uuid(),
|
|
3706
|
+
goalVersion: z.number().int().positive(),
|
|
3707
|
+
prompt: z.string().min(1),
|
|
3708
|
+
reason: z.string().optional(),
|
|
3709
|
+
})
|
|
3710
|
+
.passthrough(),
|
|
3711
|
+
z
|
|
3712
|
+
.object({
|
|
3713
|
+
type: z.literal("agent_message"),
|
|
3714
|
+
text: z.string().min(1),
|
|
3715
|
+
operationId: z.string().uuid(),
|
|
3716
|
+
})
|
|
3717
|
+
.passthrough(),
|
|
3718
|
+
z
|
|
3719
|
+
.object({
|
|
3720
|
+
type: z.literal("agent_steer_instruction"),
|
|
3721
|
+
instruction: z.string().min(1),
|
|
3722
|
+
operationId: z.string().uuid(),
|
|
3723
|
+
})
|
|
3724
|
+
.passthrough(),
|
|
3725
|
+
z
|
|
3726
|
+
.object({
|
|
3727
|
+
type: z.literal("child_terminal_result"),
|
|
3728
|
+
childSessionId: z.string().uuid(),
|
|
3729
|
+
status: z.enum(["idle", "failed"]),
|
|
3730
|
+
})
|
|
3731
|
+
.passthrough(),
|
|
3732
|
+
]);
|
|
3733
|
+
export type SessionSystemUpdatePayload = z.infer<typeof SessionSystemUpdatePayload>;
|
|
3734
|
+
|
|
2120
3735
|
export const SessionSystemUpdateState = z.enum([
|
|
2121
3736
|
"pending",
|
|
2122
3737
|
"deferred",
|
|
2123
3738
|
"delivered",
|
|
2124
3739
|
"cancelled",
|
|
3740
|
+
"superseded",
|
|
2125
3741
|
"failed",
|
|
2126
3742
|
]);
|
|
2127
3743
|
export type SessionSystemUpdateState = z.infer<typeof SessionSystemUpdateState>;
|
|
@@ -2134,7 +3750,7 @@ export const SessionSystemUpdate = z.object({
|
|
|
2134
3750
|
sourceId: z.string(),
|
|
2135
3751
|
dedupeKey: z.string(),
|
|
2136
3752
|
summary: z.string(),
|
|
2137
|
-
payload:
|
|
3753
|
+
payload: SessionSystemUpdatePayload,
|
|
2138
3754
|
lineage: z.record(z.string(), z.unknown()),
|
|
2139
3755
|
state: SessionSystemUpdateState,
|
|
2140
3756
|
deliveredTurnId: z.string().uuid().nullable(),
|
|
@@ -2365,7 +3981,10 @@ export type RigDefinitionEditPayload = z.infer<typeof RigDefinitionEditPayload>;
|
|
|
2365
3981
|
|
|
2366
3982
|
export const ProposeRigChangeRequest = z.discriminatedUnion("kind", [
|
|
2367
3983
|
z.object({ kind: z.literal("setup_append"), payload: RigSetupAppendPayload }),
|
|
2368
|
-
z.object({
|
|
3984
|
+
z.object({
|
|
3985
|
+
kind: z.literal("definition_edit"),
|
|
3986
|
+
payload: RigDefinitionEditPayload,
|
|
3987
|
+
}),
|
|
2369
3988
|
]);
|
|
2370
3989
|
export type ProposeRigChangeRequest = z.infer<typeof ProposeRigChangeRequest>;
|
|
2371
3990
|
|
|
@@ -2795,18 +4414,6 @@ export type ConnectionKind = z.infer<typeof ConnectionKind>;
|
|
|
2795
4414
|
export const ConnectionStatus = z.enum(["active", "needs_reauth", "revoked", "error"]);
|
|
2796
4415
|
export type ConnectionStatus = z.infer<typeof ConnectionStatus>;
|
|
2797
4416
|
|
|
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
4417
|
export const ConnectionMetadata = z.object({
|
|
2811
4418
|
id: z.string().uuid(),
|
|
2812
4419
|
accountId: z.string().uuid(),
|
|
@@ -2925,6 +4532,7 @@ export type CapabilityKind = z.infer<typeof CapabilityKind>;
|
|
|
2925
4532
|
|
|
2926
4533
|
export const CapabilitySource = z.enum([
|
|
2927
4534
|
"built_in",
|
|
4535
|
+
"library",
|
|
2928
4536
|
"configured",
|
|
2929
4537
|
"public_registry",
|
|
2930
4538
|
"registry",
|
|
@@ -2946,6 +4554,19 @@ export const CapabilityRuntime = z.object({
|
|
|
2946
4554
|
mcpServerId: z.string().min(1).optional(),
|
|
2947
4555
|
transport: z.string().min(1).optional(),
|
|
2948
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(),
|
|
2949
4570
|
});
|
|
2950
4571
|
export type CapabilityRuntime = z.infer<typeof CapabilityRuntime>;
|
|
2951
4572
|
|
|
@@ -2997,6 +4618,34 @@ export const CapabilityCatalogItem = z.object({
|
|
|
2997
4618
|
});
|
|
2998
4619
|
export type CapabilityCatalogItem = z.infer<typeof CapabilityCatalogItem>;
|
|
2999
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
|
+
|
|
3000
4649
|
export const CapabilityInstallation = z.object({
|
|
3001
4650
|
id: z.string().uuid(),
|
|
3002
4651
|
accountId: z.string().uuid(),
|
|
@@ -3076,7 +4725,17 @@ export const Session = z.object({
|
|
|
3076
4725
|
instructions: z.string().nullable(),
|
|
3077
4726
|
resources: z.array(ResourceRef),
|
|
3078
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(),
|
|
3079
4735
|
metadata: z.record(z.string(), z.unknown()),
|
|
4736
|
+
/** Frozen creator fact used only for creation attribution/idempotent repair. */
|
|
4737
|
+
createdBy: TurnInitiator,
|
|
4738
|
+
createdByContext: TurnInitiatorContext,
|
|
3080
4739
|
model: z.string(),
|
|
3081
4740
|
sandboxBackend: SandboxBackend,
|
|
3082
4741
|
// The OS the session's box runs. Defaults to 'linux' (today's only OS).
|
|
@@ -3126,12 +4785,7 @@ export const Session = z.object({
|
|
|
3126
4785
|
queueVersion: z.number().int().nonnegative(),
|
|
3127
4786
|
queueHeadPosition: z.number().int(),
|
|
3128
4787
|
queueTailPosition: z.number().int(),
|
|
3129
|
-
|
|
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(),
|
|
4788
|
+
effectiveControl: EffectiveSessionControl,
|
|
3135
4789
|
lastSequence: z.number().int().nonnegative(),
|
|
3136
4790
|
// Multi-account Codex (P1). codexPinnedCredentialId: the account this session is
|
|
3137
4791
|
// manually PINNED to (null ⇒ follow the workspace active pointer).
|
|
@@ -3159,6 +4813,8 @@ export const Session = z.object({
|
|
|
3159
4813
|
attentionDescendants: z.number().int().nonnegative(),
|
|
3160
4814
|
pausedDescendants: z.number().int().nonnegative(),
|
|
3161
4815
|
failedDescendants: z.number().int().nonnegative(),
|
|
4816
|
+
/** Counts are lower bounds rather than exact totals when true. */
|
|
4817
|
+
truncated: z.boolean().default(false),
|
|
3162
4818
|
})
|
|
3163
4819
|
.optional(),
|
|
3164
4820
|
createdAt: z.string(),
|
|
@@ -3166,16 +4822,30 @@ export const Session = z.object({
|
|
|
3166
4822
|
});
|
|
3167
4823
|
export type Session = z.infer<typeof Session>;
|
|
3168
4824
|
|
|
4825
|
+
/**
|
|
4826
|
+
* Additive receipt returned only by session creation. `activeTurnId` remains an
|
|
4827
|
+
* execution pointer and is correctly null while the first turn is queued;
|
|
4828
|
+
* embedders use this immutable identity to correlate their preallocated run.
|
|
4829
|
+
*/
|
|
4830
|
+
export const CreateSessionResponse = Session.extend({
|
|
4831
|
+
initialTurnId: z.string().uuid().nullable(),
|
|
4832
|
+
});
|
|
4833
|
+
export type CreateSessionResponse = z.infer<typeof CreateSessionResponse>;
|
|
4834
|
+
|
|
3169
4835
|
export type SessionSummary = Session;
|
|
3170
4836
|
|
|
3171
4837
|
/**
|
|
3172
4838
|
* The canonical session-list page. Pinned rows are returned separately and are
|
|
3173
4839
|
* excluded from `sessions`, so a cursor can page ordinary recency rows without
|
|
3174
|
-
* duplicating a pin.
|
|
3175
|
-
*
|
|
4840
|
+
* duplicating a pin. The newest 100 matching pins are returned, ordered by
|
|
4841
|
+
* pinnedAt DESC, id DESC; `pinnedTruncated` makes an older-pin omission
|
|
4842
|
+
* explicit. Pins are filtered by the same parent/search predicates as ordinary
|
|
4843
|
+
* rows.
|
|
3176
4844
|
*/
|
|
3177
4845
|
export const SessionListResponse = z.object({
|
|
3178
4846
|
pinned: z.array(Session),
|
|
4847
|
+
/** True when older matching pins were omitted from this bounded page. */
|
|
4848
|
+
pinnedTruncated: z.boolean().optional(),
|
|
3179
4849
|
sessions: z.array(Session),
|
|
3180
4850
|
nextCursor: z.string().nullable(),
|
|
3181
4851
|
});
|
|
@@ -3204,8 +4874,14 @@ export type SessionLineageResponse = z.infer<typeof SessionLineageResponse>;
|
|
|
3204
4874
|
|
|
3205
4875
|
export const SessionEventType = z.enum([
|
|
3206
4876
|
"session.created",
|
|
4877
|
+
// Defensive read/transport projection for a malformed or historically
|
|
4878
|
+
// oversized retained event envelope. The original row stays durable; this
|
|
4879
|
+
// explicit synthetic type prevents unbounded free-form envelope fields from
|
|
4880
|
+
// crossing NATS, SSE, REST, or browser boundaries.
|
|
4881
|
+
"session.event.envelope_omitted",
|
|
3207
4882
|
"session.status.changed",
|
|
3208
4883
|
"session.requiresAction",
|
|
4884
|
+
"session.humanInput.requested",
|
|
3209
4885
|
"session.context.compaction.requested",
|
|
3210
4886
|
"session.context.compacted",
|
|
3211
4887
|
"session.context.compaction.skipped",
|
|
@@ -3213,6 +4889,7 @@ export const SessionEventType = z.enum([
|
|
|
3213
4889
|
"user.message",
|
|
3214
4890
|
"user.pause",
|
|
3215
4891
|
"user.approvalDecision",
|
|
4892
|
+
"user.humanInputResponse",
|
|
3216
4893
|
"turn.queued",
|
|
3217
4894
|
"turn.started",
|
|
3218
4895
|
"turn.completed",
|
|
@@ -3226,8 +4903,12 @@ export const SessionEventType = z.enum([
|
|
|
3226
4903
|
"agent.reasoning.delta",
|
|
3227
4904
|
"agent.toolCall.created",
|
|
3228
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",
|
|
3229
4909
|
"agent.model.usage",
|
|
3230
4910
|
"tool.auth_needed",
|
|
4911
|
+
"credential.auth_needed",
|
|
3231
4912
|
"agent.updated",
|
|
3232
4913
|
"rig.setup.started",
|
|
3233
4914
|
"rig.setup.completed",
|
|
@@ -3252,6 +4933,7 @@ export const SessionEventType = z.enum([
|
|
|
3252
4933
|
"session.control.steer_requested",
|
|
3253
4934
|
"workspace.inference.paused",
|
|
3254
4935
|
"workspace.inference.resumed",
|
|
4936
|
+
"session.queue.changed",
|
|
3255
4937
|
"session.queue.prompt.cancelled",
|
|
3256
4938
|
"session.queue.history",
|
|
3257
4939
|
// A terminal/stale activity callback is retained as an audit wrapper rather
|
|
@@ -3266,15 +4948,15 @@ export const SessionEventType = z.enum([
|
|
|
3266
4948
|
"stream.opened", // a viewer attached (audit + refcount visibility)
|
|
3267
4949
|
"stream.closed", // a viewer detached / was reaped
|
|
3268
4950
|
"stream.revoked", // a grant was revoked → connected clients MUST disconnect now
|
|
3269
|
-
//
|
|
3270
|
-
//
|
|
4951
|
+
// Desktop recording signals. The capture loop records the same display humans
|
|
4952
|
+
// watch, then stores the finalized artifact for replay.
|
|
3271
4953
|
// → storage. The artifact ref rides the AVAILABLE event (storageKey, NOT a
|
|
3272
4954
|
// long-lived URL — clients mint a short-TTL signed GET via the route).
|
|
3273
4955
|
"recording.started", // ffmpeg launched on :0 (mode/codec/dimensions)
|
|
3274
4956
|
"recording.available", // finalized: bytes PUT to storage, replayable
|
|
3275
4957
|
"recording.failed", // ffmpeg/box-death/rollover/upload error — no artifact
|
|
3276
|
-
//
|
|
3277
|
-
//
|
|
4958
|
+
// Structured-service notifications. File, Git, and terminal reads are
|
|
4959
|
+
// synchronous API-direct point
|
|
3278
4960
|
// queries (their result is the HTTP response, NEVER an event). What rides A1
|
|
3279
4961
|
// here are the side-effect NOTIFICATIONS — a path changed, git state changed,
|
|
3280
4962
|
// a pty opened/printed/exited — durable, sequenced, gap-filled like every
|
|
@@ -3287,14 +4969,15 @@ export const SessionEventType = z.enum([
|
|
|
3287
4969
|
"terminal.pty.output.delta", // PTY stdout/stderr bytes (separate from command.output)
|
|
3288
4970
|
"terminal.pty.exited", // PTY session ended (exitCode/reason)
|
|
3289
4971
|
"session.title_set",
|
|
4972
|
+
"session.mcp.approval_policy.updated",
|
|
3290
4973
|
// Multi-account Codex (P1): the account a session's turn runs on changed
|
|
3291
4974
|
// (manual switch in P1; failover/rotation in P3 reuse the same event). Drives
|
|
3292
4975
|
// the in-session "Running on:" indicator's live flip.
|
|
3293
4976
|
"codex.account.switched",
|
|
3294
|
-
//
|
|
4977
|
+
// credential allocator per-turn selection audit. Payload is metadata only: credential row
|
|
3295
4978
|
// id, bounded strategy/reason, and pool counts — never token material.
|
|
3296
4979
|
"codex.credential.selected",
|
|
3297
|
-
//
|
|
4980
|
+
// credential allocator durable zero-capacity wait lifecycle. Runtime/system events only;
|
|
3298
4981
|
// no synthetic user message is created when capacity returns.
|
|
3299
4982
|
"codex.capacity.waiting",
|
|
3300
4983
|
"codex.capacity.resumed",
|
|
@@ -3319,12 +5002,12 @@ export const SessionEventType = z.enum([
|
|
|
3319
5002
|
// target id or command content. Announce-only; hits the timeline projection default
|
|
3320
5003
|
// (no rendered item) like the other sandbox.* diagnostics.
|
|
3321
5004
|
"session.route.reconciled",
|
|
3322
|
-
// Workbench v2 turn-end workspace capture
|
|
5005
|
+
// Workbench v2 turn-end workspace capture. ANNOUNCE-ONLY: a new
|
|
3323
5006
|
// capture revision was persisted at turn end; the client refetches the latest
|
|
3324
5007
|
// capture. It carries metadata only (revision/turnId/capturedAt/leaseEpoch/stats),
|
|
3325
5008
|
// never file content. Hits the timeline projection default case (ignored) — it
|
|
3326
5009
|
// must NEVER gain a rendered timeline item without regenerating the golden
|
|
3327
|
-
// snapshots (
|
|
5010
|
+
// snapshots (golden-grammar gate).
|
|
3328
5011
|
"workspace.revision.captured",
|
|
3329
5012
|
// Repository discovery could not prove a complete capture. The worker
|
|
3330
5013
|
// persisted a failed/degraded revision marker and clients must fall back to
|
|
@@ -3366,19 +5049,225 @@ export const SessionEventType = z.enum([
|
|
|
3366
5049
|
]);
|
|
3367
5050
|
export type SessionEventType = z.infer<typeof SessionEventType>;
|
|
3368
5051
|
|
|
5052
|
+
/**
|
|
5053
|
+
* Stable semantic groups for bounded session monitoring. These are a read
|
|
5054
|
+
* projection only: an event keeps its canonical durable `type`, and callers
|
|
5055
|
+
* can always combine a class with explicit type include/exclude filters.
|
|
5056
|
+
*/
|
|
5057
|
+
export const SessionEventSemanticClass = z.enum([
|
|
5058
|
+
"control",
|
|
5059
|
+
"terminal",
|
|
5060
|
+
"failure",
|
|
5061
|
+
"checkpoint",
|
|
5062
|
+
"tool_receipt",
|
|
5063
|
+
"provider_account",
|
|
5064
|
+
]);
|
|
5065
|
+
export type SessionEventSemanticClass = z.infer<typeof SessionEventSemanticClass>;
|
|
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
|
+
|
|
5089
|
+
export const SessionEventPayloadMode = z.enum(["none", "summary", "full"]);
|
|
5090
|
+
export type SessionEventPayloadMode = z.infer<typeof SessionEventPayloadMode>;
|
|
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
|
+
|
|
5096
|
+
export const SessionEventReadMode = z.enum(["monitoring", "forensic"]);
|
|
5097
|
+
export type SessionEventReadMode = z.infer<typeof SessionEventReadMode>;
|
|
5098
|
+
|
|
5099
|
+
export const SessionEventReadDirection = z.enum(["after", "before"]);
|
|
5100
|
+
export type SessionEventReadDirection = z.infer<typeof SessionEventReadDirection>;
|
|
5101
|
+
|
|
5102
|
+
export const SESSION_EVENT_RAW_DELTA_TYPES = [
|
|
5103
|
+
"agent.message.delta",
|
|
5104
|
+
"agent.reasoning.delta",
|
|
5105
|
+
"sandbox.command.output.delta",
|
|
5106
|
+
"terminal.pty.output.delta",
|
|
5107
|
+
] as const satisfies readonly SessionEventType[];
|
|
5108
|
+
|
|
5109
|
+
export const SESSION_EVENT_SEMANTIC_CLASS_TYPES = {
|
|
5110
|
+
control: [
|
|
5111
|
+
"session.status.changed",
|
|
5112
|
+
"session.requiresAction",
|
|
5113
|
+
"session.humanInput.requested",
|
|
5114
|
+
"user.pause",
|
|
5115
|
+
"user.approvalDecision",
|
|
5116
|
+
"user.humanInputResponse",
|
|
5117
|
+
"goal.set",
|
|
5118
|
+
"goal.updated",
|
|
5119
|
+
"goal.completed",
|
|
5120
|
+
"goal.paused",
|
|
5121
|
+
"goal.resumed",
|
|
5122
|
+
"goal.cleared",
|
|
5123
|
+
"goal.continuation",
|
|
5124
|
+
"system.update.pending",
|
|
5125
|
+
"system.update.delivered",
|
|
5126
|
+
"session.control.paused",
|
|
5127
|
+
"session.control.resumed",
|
|
5128
|
+
"session.control.steer_requested",
|
|
5129
|
+
"workspace.inference.paused",
|
|
5130
|
+
"workspace.inference.resumed",
|
|
5131
|
+
"session.queue.changed",
|
|
5132
|
+
"session.queue.prompt.cancelled",
|
|
5133
|
+
"session.mcp.approval_policy.updated",
|
|
5134
|
+
],
|
|
5135
|
+
terminal: [
|
|
5136
|
+
"turn.completed",
|
|
5137
|
+
"agent.message.completed",
|
|
5138
|
+
"turn.failed",
|
|
5139
|
+
"turn.cancelled",
|
|
5140
|
+
"turn.superseded",
|
|
5141
|
+
"goal.completed",
|
|
5142
|
+
"goal.paused",
|
|
5143
|
+
"rig.setup.completed",
|
|
5144
|
+
"rig.setup.skipped",
|
|
5145
|
+
"rig.setup.failed",
|
|
5146
|
+
"sandbox.operation.completed",
|
|
5147
|
+
"sandbox.operation.failed",
|
|
5148
|
+
"recording.available",
|
|
5149
|
+
"recording.failed",
|
|
5150
|
+
"terminal.pty.exited",
|
|
5151
|
+
],
|
|
5152
|
+
failure: [
|
|
5153
|
+
"session.event.envelope_omitted",
|
|
5154
|
+
"turn.failed",
|
|
5155
|
+
"tool.auth_needed",
|
|
5156
|
+
"credential.auth_needed",
|
|
5157
|
+
"rig.setup.failed",
|
|
5158
|
+
"sandbox.operation.failed",
|
|
5159
|
+
"recording.failed",
|
|
5160
|
+
"sandbox.box.lost",
|
|
5161
|
+
"workspace.revision.degraded",
|
|
5162
|
+
"machine.op.failed",
|
|
5163
|
+
"machine.link.lost",
|
|
5164
|
+
],
|
|
5165
|
+
checkpoint: [
|
|
5166
|
+
"session.context.compaction.requested",
|
|
5167
|
+
"session.context.compacted",
|
|
5168
|
+
"session.context.compaction.skipped",
|
|
5169
|
+
"session.context.cleared",
|
|
5170
|
+
"turn.recovery.requested",
|
|
5171
|
+
"session.queue.history",
|
|
5172
|
+
"sandbox.box.snapshot",
|
|
5173
|
+
"workspace.revision.captured",
|
|
5174
|
+
],
|
|
5175
|
+
tool_receipt: [
|
|
5176
|
+
"agent.toolCall.created",
|
|
5177
|
+
"agent.toolCall.output",
|
|
5178
|
+
"tool.auth_needed",
|
|
5179
|
+
"artifact.created",
|
|
5180
|
+
],
|
|
5181
|
+
provider_account: [
|
|
5182
|
+
"agent.model.usage",
|
|
5183
|
+
"codex.account.switched",
|
|
5184
|
+
"codex.credential.selected",
|
|
5185
|
+
"codex.capacity.waiting",
|
|
5186
|
+
"codex.capacity.resumed",
|
|
5187
|
+
"codex.capacity.superseded",
|
|
5188
|
+
"sandbox.box.created",
|
|
5189
|
+
"sandbox.box.lost",
|
|
5190
|
+
"sandbox.box.terminated",
|
|
5191
|
+
"sandbox.box.snapshot",
|
|
5192
|
+
"sandbox.env.drift",
|
|
5193
|
+
"session.route.reconciled",
|
|
5194
|
+
"machine.op.failed",
|
|
5195
|
+
"machine.op.recovered",
|
|
5196
|
+
"machine.link.lost",
|
|
5197
|
+
"machine.link.restored",
|
|
5198
|
+
"machine.runner.restarted",
|
|
5199
|
+
],
|
|
5200
|
+
} as const satisfies Record<SessionEventSemanticClass, readonly SessionEventType[]>;
|
|
5201
|
+
|
|
5202
|
+
export type ResolveSessionEventTypeFiltersInput = {
|
|
5203
|
+
includeTypes?: readonly SessionEventType[] | undefined;
|
|
5204
|
+
excludeTypes?: readonly SessionEventType[] | undefined;
|
|
5205
|
+
includeClasses?: readonly SessionEventSemanticClass[] | undefined;
|
|
5206
|
+
excludeClasses?: readonly SessionEventSemanticClass[] | undefined;
|
|
5207
|
+
/** Applied unless the same type was explicitly included by type or class. */
|
|
5208
|
+
defaultExcludeTypes?: readonly SessionEventType[] | undefined;
|
|
5209
|
+
};
|
|
5210
|
+
|
|
5211
|
+
/** Resolve class/type filter algebra once so every read surface behaves alike. */
|
|
5212
|
+
export function resolveSessionEventTypeFilters(input: ResolveSessionEventTypeFiltersInput): {
|
|
5213
|
+
includeTypes: SessionEventType[];
|
|
5214
|
+
excludeTypes: SessionEventType[];
|
|
5215
|
+
} {
|
|
5216
|
+
const included = new Set<SessionEventType>(input.includeTypes ?? []);
|
|
5217
|
+
for (const semanticClass of input.includeClasses ?? []) {
|
|
5218
|
+
for (const type of SESSION_EVENT_SEMANTIC_CLASS_TYPES[semanticClass]) included.add(type);
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
const excluded = new Set<SessionEventType>(input.excludeTypes ?? []);
|
|
5222
|
+
for (const semanticClass of input.excludeClasses ?? []) {
|
|
5223
|
+
for (const type of SESSION_EVENT_SEMANTIC_CLASS_TYPES[semanticClass]) excluded.add(type);
|
|
5224
|
+
}
|
|
5225
|
+
for (const type of input.defaultExcludeTypes ?? []) {
|
|
5226
|
+
if (!included.has(type)) excluded.add(type);
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
// An explicit exclusion always wins over a positive selector.
|
|
5230
|
+
for (const type of excluded) included.delete(type);
|
|
5231
|
+
return { includeTypes: [...included], excludeTypes: [...excluded] };
|
|
5232
|
+
}
|
|
5233
|
+
|
|
3369
5234
|
export const ToolAuthNeededPayload = z.object({
|
|
3370
5235
|
serverId: z.string().min(1),
|
|
3371
5236
|
toolName: z.string().min(1).nullable().optional(),
|
|
3372
5237
|
providerDomain: z.string().min(1),
|
|
3373
|
-
|
|
3374
|
-
|
|
5238
|
+
provider: z.string().min(1).max(128).optional(),
|
|
5239
|
+
// Embedded hosts may use an opaque connection identity; never assume an
|
|
5240
|
+
// OpenGeni UUID on the public event wire.
|
|
5241
|
+
connectionId: z.string().min(1).nullable().optional(),
|
|
5242
|
+
reason: z.enum([
|
|
5243
|
+
"missing_connection",
|
|
5244
|
+
"expired",
|
|
5245
|
+
"insufficient_scope",
|
|
5246
|
+
"refresh_failed",
|
|
5247
|
+
"unsupported_auth",
|
|
5248
|
+
"resource_scope_unavailable",
|
|
5249
|
+
]),
|
|
3375
5250
|
scopes: z.array(z.string().min(1)).optional(),
|
|
3376
5251
|
resource: z.string().min(1).optional(),
|
|
5252
|
+
selectedResources: McpConnectionResourceScopes.optional(),
|
|
3377
5253
|
authorizationUrl: z.string().url().optional(),
|
|
3378
5254
|
subjectId: z.string().min(1).nullable().optional(),
|
|
3379
5255
|
});
|
|
3380
5256
|
export type ToolAuthNeededPayload = z.infer<typeof ToolAuthNeededPayload>;
|
|
3381
5257
|
|
|
5258
|
+
/** A host-owned non-tool credential needed by the active run. */
|
|
5259
|
+
export const CredentialAuthNeededPayload = z.object({
|
|
5260
|
+
credentialClass: z.literal("run"),
|
|
5261
|
+
providerDomain: z.string().min(1).optional(),
|
|
5262
|
+
connectionId: z.string().min(1).optional(),
|
|
5263
|
+
reason: z.enum(["missing_connection", "expired", "insufficient_scope", "refresh_failed"]),
|
|
5264
|
+
scopes: z.array(z.string().min(1)).optional(),
|
|
5265
|
+
resource: z.string().min(1).optional(),
|
|
5266
|
+
authorizationUrl: z.string().url().optional(),
|
|
5267
|
+
message: z.string().min(1).optional(),
|
|
5268
|
+
});
|
|
5269
|
+
export type CredentialAuthNeededPayload = z.infer<typeof CredentialAuthNeededPayload>;
|
|
5270
|
+
|
|
3382
5271
|
// Channel-B stream-event payloads (07-channel-b §1.2). SessionEvent.payload is
|
|
3383
5272
|
// z.unknown() (NOT a discriminated union) — these are standalone schemas parsed
|
|
3384
5273
|
// explicitly at the producer (the API-direct handshake/rotation) and the SDK/
|
|
@@ -3480,7 +5369,7 @@ export const RecordingFailedPayload = z.object({
|
|
|
3480
5369
|
});
|
|
3481
5370
|
export type RecordingFailedPayload = z.infer<typeof RecordingFailedPayload>;
|
|
3482
5371
|
|
|
3483
|
-
// ──
|
|
5372
|
+
// ── Structured sandbox services ─────────────────────────────────────────────
|
|
3484
5373
|
// Two transports on one spine: the A2 request/response shapes (FsNode tree,
|
|
3485
5374
|
// GitDiff hunks, terminal exec) are returned INLINE on synchronous API-direct
|
|
3486
5375
|
// routes (never the bus); the A1 notification payloads below ride the durable
|
|
@@ -3647,7 +5536,9 @@ export const FsDeleteRequest = z.object({
|
|
|
3647
5536
|
recursive: z.boolean().default(false), // required true to delete a non-empty dir
|
|
3648
5537
|
});
|
|
3649
5538
|
export type FsDeleteRequest = z.infer<typeof FsDeleteRequest>;
|
|
3650
|
-
export const FsDeleteResponse = z.object({
|
|
5539
|
+
export const FsDeleteResponse = z.object({
|
|
5540
|
+
revision: z.number().int().nonnegative(),
|
|
5541
|
+
});
|
|
3651
5542
|
export type FsDeleteResponse = z.infer<typeof FsDeleteResponse>;
|
|
3652
5543
|
|
|
3653
5544
|
export const FsMoveRequest = z.object({
|
|
@@ -3748,6 +5639,9 @@ export const GitDiffRequest = z.object({
|
|
|
3748
5639
|
path: z.string().default(""), // repo root
|
|
3749
5640
|
// diff selectors, mutually exclusive precedence: refs > staged > worktree
|
|
3750
5641
|
staged: z.boolean().default(false), // --cached (index vs HEAD)
|
|
5642
|
+
// Workspace review includes after-images that ordinary `git diff` omits.
|
|
5643
|
+
// Explicit so commit/staged consumers keep native Git semantics by default.
|
|
5644
|
+
includeUntracked: z.boolean().default(false),
|
|
3751
5645
|
fromRef: z.string().optional(),
|
|
3752
5646
|
toRef: z.string().optional(),
|
|
3753
5647
|
pathspec: z.array(z.string()).default([]),
|
|
@@ -3766,7 +5660,7 @@ export const GitDiffResponse = z.object({
|
|
|
3766
5660
|
});
|
|
3767
5661
|
export type GitDiffResponse = z.infer<typeof GitDiffResponse>;
|
|
3768
5662
|
|
|
3769
|
-
// ─── Workbench v2 turn-end workspace capture
|
|
5663
|
+
// ─── Workbench v2 turn-end workspace capture ────────────
|
|
3770
5664
|
// A capture is a point-in-time snapshot of the session workspace's CHANGES,
|
|
3771
5665
|
// probed live off the box at turn end (detectRepos → gitStatus/gitDiff → fsRead
|
|
3772
5666
|
// after-images → fsList tree index). It is the cold/offline read source that
|
|
@@ -3783,7 +5677,7 @@ export const WorkspaceCaptureFile = z.object({
|
|
|
3783
5677
|
status: GitFileStatusCode,
|
|
3784
5678
|
// sha256 of the captured after-image bytes; null when deleted / tooLarge.
|
|
3785
5679
|
hash: z.string().nullable(),
|
|
3786
|
-
// git blob sha of the HEAD version — the wake-on-edit flush guard (
|
|
5680
|
+
// git blob sha of the HEAD version — the wake-on-edit flush guard (design
|
|
3787
5681
|
// §10.1). null when the path is new/untracked (no HEAD blob).
|
|
3788
5682
|
baseHash: z.string().nullable(),
|
|
3789
5683
|
// Content-addressed storage key of the after-image; null when deleted /
|
|
@@ -3821,7 +5715,7 @@ export const WorkspaceCaptureDegradedReason = z.enum([
|
|
|
3821
5715
|
export type WorkspaceCaptureDegradedReason = z.infer<typeof WorkspaceCaptureDegradedReason>;
|
|
3822
5716
|
|
|
3823
5717
|
// Rollup counters — carried on the row (jsonb) and the announce event so the UI
|
|
3824
|
-
// can reserve layout (
|
|
5718
|
+
// can reserve layout (no layout shift) before fetching the manifest.
|
|
3825
5719
|
export const WorkspaceCaptureStats = z.object({
|
|
3826
5720
|
repoCount: z.number().int().nonnegative(),
|
|
3827
5721
|
fileCount: z.number().int().nonnegative(),
|
|
@@ -3858,7 +5752,7 @@ export const WorkspaceCaptureManifest = z.object({
|
|
|
3858
5752
|
});
|
|
3859
5753
|
export type WorkspaceCaptureManifest = z.infer<typeof WorkspaceCaptureManifest>;
|
|
3860
5754
|
|
|
3861
|
-
// Announce-only event payload
|
|
5755
|
+
// Announce-only event payload. Metadata only — never content.
|
|
3862
5756
|
export const WorkspaceRevisionCapturedPayload = z.object({
|
|
3863
5757
|
revision: z.number().int().nonnegative(),
|
|
3864
5758
|
turnId: z.string().nullable(),
|
|
@@ -3877,7 +5771,7 @@ export const WorkspaceRevisionDegradedPayload = z.object({
|
|
|
3877
5771
|
});
|
|
3878
5772
|
export type WorkspaceRevisionDegradedPayload = z.infer<typeof WorkspaceRevisionDegradedPayload>;
|
|
3879
5773
|
|
|
3880
|
-
// --- M2 capture READ API
|
|
5774
|
+
// --- M2 capture READ API -------------------------------------
|
|
3881
5775
|
// A short-TTL signed GET URL minted PER REQUEST (never stored). The manifest is
|
|
3882
5776
|
// served inline for the ≤2MB common case (the <200ms one-round-trip paint); a
|
|
3883
5777
|
// >2MB manifest and a >256KB single-file after-image fall back to one of these.
|
|
@@ -3954,14 +5848,25 @@ export const GitCommit = z.object({
|
|
|
3954
5848
|
sha: z.string(),
|
|
3955
5849
|
shortSha: z.string(),
|
|
3956
5850
|
parents: z.array(z.string()),
|
|
3957
|
-
author: z.object({
|
|
3958
|
-
|
|
5851
|
+
author: z.object({
|
|
5852
|
+
name: z.string(),
|
|
5853
|
+
email: z.string(),
|
|
5854
|
+
timestamp: z.number().int(),
|
|
5855
|
+
}),
|
|
5856
|
+
committer: z.object({
|
|
5857
|
+
name: z.string(),
|
|
5858
|
+
email: z.string(),
|
|
5859
|
+
timestamp: z.number().int(),
|
|
5860
|
+
}),
|
|
3959
5861
|
subject: z.string(),
|
|
3960
5862
|
body: z.string(),
|
|
3961
5863
|
refs: z.array(z.string()).default([]), // decorations: branch/tag pointers
|
|
3962
5864
|
});
|
|
3963
5865
|
export type GitCommit = z.infer<typeof GitCommit>;
|
|
3964
|
-
export const GitLogResponse = z.object({
|
|
5866
|
+
export const GitLogResponse = z.object({
|
|
5867
|
+
commits: z.array(GitCommit),
|
|
5868
|
+
hasMore: z.boolean(),
|
|
5869
|
+
});
|
|
3965
5870
|
export type GitLogResponse = z.infer<typeof GitLogResponse>;
|
|
3966
5871
|
|
|
3967
5872
|
export const GitShowRequest = z.object({
|
|
@@ -4033,7 +5938,10 @@ export const PtyOpenResponse = z.object({
|
|
|
4033
5938
|
supportsInput: z.boolean(), // false on backends without writeStdin
|
|
4034
5939
|
});
|
|
4035
5940
|
export type PtyOpenResponse = z.infer<typeof PtyOpenResponse>;
|
|
4036
|
-
export const PtyWriteRequest = z.object({
|
|
5941
|
+
export const PtyWriteRequest = z.object({
|
|
5942
|
+
ptyId: z.string().uuid(),
|
|
5943
|
+
data: z.string(),
|
|
5944
|
+
}); // utf-8 stdin
|
|
4037
5945
|
export type PtyWriteRequest = z.infer<typeof PtyWriteRequest>;
|
|
4038
5946
|
export const PtyResizeRequest = z.object({
|
|
4039
5947
|
ptyId: z.string().uuid(),
|
|
@@ -4048,7 +5956,11 @@ export type PtyCloseRequest = z.infer<typeof PtyCloseRequest>;
|
|
|
4048
5956
|
// negotiation). The full SessionCapabilities doc already carries FileSystem /
|
|
4049
5957
|
// Terminal / Git blocks (P0.1); this is the compact projection the SDK mirrors.
|
|
4050
5958
|
export const SessionStructuredCapabilities = z.object({
|
|
4051
|
-
FileSystem: z.object({
|
|
5959
|
+
FileSystem: z.object({
|
|
5960
|
+
available: z.boolean(),
|
|
5961
|
+
readOnly: z.boolean(),
|
|
5962
|
+
root: z.string(),
|
|
5963
|
+
}),
|
|
4052
5964
|
Terminal: z.object({
|
|
4053
5965
|
events: z.boolean(), // command.output firehose (always on if a box exists)
|
|
4054
5966
|
exec: z.boolean(), // synchronous terminal exec
|
|
@@ -4066,39 +5978,992 @@ export const SessionEvent = z.object({
|
|
|
4066
5978
|
type: SessionEventType,
|
|
4067
5979
|
payload: z.unknown().default({}),
|
|
4068
5980
|
occurredAt: z.string(),
|
|
4069
|
-
clientEventId:
|
|
5981
|
+
clientEventId: SessionOperationKey.nullable().optional(),
|
|
4070
5982
|
turnId: z.string().uuid().nullable().optional(),
|
|
4071
5983
|
turnGeneration: z.number().int().nonnegative().nullable().optional(),
|
|
4072
5984
|
turnAttemptId: z.string().uuid().nullable().optional(),
|
|
4073
5985
|
turnAssociation: z.enum(["current", "late_rejected", "duplicate"]).nullable().optional(),
|
|
4074
5986
|
duplicateOfEventId: z.string().uuid().nullable().optional(),
|
|
4075
|
-
duplicateReason: z.string().min(1).nullable().optional(),
|
|
5987
|
+
duplicateReason: z.string().min(1).max(1024).nullable().optional(),
|
|
4076
5988
|
});
|
|
4077
5989
|
export type SessionEvent = z.infer<typeof SessionEvent>;
|
|
4078
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
|
+
|
|
6339
|
+
// --- Durable host export ------------------------------------------------------
|
|
6340
|
+
|
|
6341
|
+
/** Wire revision for the durable host event/usage export stream. */
|
|
6342
|
+
export const OPENGENI_HOST_EXPORT_SCHEMA_REVISION = "2026-07-host-export-v1" as const;
|
|
6343
|
+
|
|
6344
|
+
/**
|
|
6345
|
+
* Decimal string rather than a JavaScript number: export cursors are PostgreSQL
|
|
6346
|
+
* bigint values and must remain exact beyond Number.MAX_SAFE_INTEGER.
|
|
6347
|
+
*/
|
|
6348
|
+
export const HostExportCursor = z.string().regex(/^(0|[1-9][0-9]*)$/);
|
|
6349
|
+
export type HostExportCursor = z.infer<typeof HostExportCursor>;
|
|
6350
|
+
|
|
6351
|
+
export const HostExportConsumerId = z
|
|
6352
|
+
.string()
|
|
6353
|
+
.min(1)
|
|
6354
|
+
.max(128)
|
|
6355
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
6356
|
+
export type HostExportConsumerId = z.infer<typeof HostExportConsumerId>;
|
|
6357
|
+
|
|
6358
|
+
export const HostExportInitiator = TurnInitiator.extend({
|
|
6359
|
+
subjectId: z.string().min(1).max(1024),
|
|
6360
|
+
label: z.string().min(1).max(256).optional(),
|
|
6361
|
+
});
|
|
6362
|
+
export type HostExportInitiator = z.infer<typeof HostExportInitiator>;
|
|
6363
|
+
|
|
6364
|
+
export const HostExportInitiatorContext = TurnInitiatorContext.refine(
|
|
6365
|
+
(value) => {
|
|
6366
|
+
try {
|
|
6367
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength <= 4096;
|
|
6368
|
+
} catch {
|
|
6369
|
+
return false;
|
|
6370
|
+
}
|
|
6371
|
+
},
|
|
6372
|
+
{ message: "Host export initiator context exceeds 4096 UTF-8 bytes" },
|
|
6373
|
+
);
|
|
6374
|
+
export type HostExportInitiatorContext = z.infer<typeof HostExportInitiatorContext>;
|
|
6375
|
+
|
|
6376
|
+
const HostExportAttribution = {
|
|
6377
|
+
initiator: HostExportInitiator.nullable(),
|
|
6378
|
+
initiatorContext: HostExportInitiatorContext,
|
|
6379
|
+
origin: SessionTurnSource.nullable(),
|
|
6380
|
+
} as const;
|
|
6381
|
+
|
|
6382
|
+
/**
|
|
6383
|
+
* Host streams are deliberately forward-tolerant across rolling upgrades.
|
|
6384
|
+
* OpenGeni's application contract enumerates the event types known to this
|
|
6385
|
+
* build, while the durable export may be read by an older host consumer after
|
|
6386
|
+
* a newer writer has committed a bounded type. The database remains the
|
|
6387
|
+
* authority for the byte bounds on these persisted strings.
|
|
6388
|
+
*/
|
|
6389
|
+
export const HostSessionEvent = SessionEvent.extend({
|
|
6390
|
+
type: z.string().min(1).max(256),
|
|
6391
|
+
clientEventId: z.string().max(1024).nullable().optional(),
|
|
6392
|
+
turnAssociation: z.string().min(1).max(64).nullable().optional(),
|
|
6393
|
+
duplicateReason: z.string().max(4096).nullable().optional(),
|
|
6394
|
+
});
|
|
6395
|
+
export type HostSessionEvent = z.infer<typeof HostSessionEvent>;
|
|
6396
|
+
|
|
6397
|
+
/** Export-bounded usage fact; custom bounded metric names remain supported. */
|
|
6398
|
+
export const HostUsageEvent = UsageEvent.extend({
|
|
6399
|
+
subjectId: z.string().max(1024).nullable(),
|
|
6400
|
+
eventType: z.string().min(1).max(256),
|
|
6401
|
+
unit: z.string().min(1).max(128),
|
|
6402
|
+
sourceResourceType: z.string().max(256).nullable(),
|
|
6403
|
+
sourceResourceId: z.string().max(2048).nullable(),
|
|
6404
|
+
idempotencyKey: z.string().min(1).max(2048),
|
|
6405
|
+
billingProviderEventId: z.string().max(2048).nullable(),
|
|
6406
|
+
});
|
|
6407
|
+
export type HostUsageEvent = z.infer<typeof HostUsageEvent>;
|
|
6408
|
+
|
|
6409
|
+
/**
|
|
6410
|
+
* One immutable, bounded session-event snapshot from the transactional host
|
|
6411
|
+
* outbox. Cross-session cursor order is stable but deliberately non-causal;
|
|
6412
|
+
* within a session, `event.sequence` remains authoritative and monotonic.
|
|
6413
|
+
*/
|
|
6414
|
+
export const HostEventExport = z.object({
|
|
6415
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
6416
|
+
cursor: HostExportCursor,
|
|
6417
|
+
idempotencyKey: z.string().min(1).max(2048),
|
|
6418
|
+
accountId: z.string().uuid(),
|
|
6419
|
+
workspaceId: z.string().uuid(),
|
|
6420
|
+
/**
|
|
6421
|
+
* Immutable root of event.sessionId's session lineage at capture time. Null
|
|
6422
|
+
* only for an unresolved pre-lineage/legacy export row.
|
|
6423
|
+
*/
|
|
6424
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
6425
|
+
...HostExportAttribution,
|
|
6426
|
+
event: HostSessionEvent,
|
|
6427
|
+
});
|
|
6428
|
+
export type HostEventExport = z.infer<typeof HostEventExport>;
|
|
6429
|
+
|
|
6430
|
+
/** One exact, idempotency-keyed usage fact from the same ordered outbox. */
|
|
6431
|
+
export const HostUsageExport = z.object({
|
|
6432
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
6433
|
+
cursor: HostExportCursor,
|
|
6434
|
+
accountId: z.string().uuid(),
|
|
6435
|
+
workspaceId: z.string().uuid(),
|
|
6436
|
+
sessionId: z.string().uuid().nullable(),
|
|
6437
|
+
/** Null when sessionId is null or an unresolved pre-lineage legacy row. */
|
|
6438
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
6439
|
+
turnId: z.string().uuid().nullable(),
|
|
6440
|
+
turnAttemptId: z.string().uuid().nullable(),
|
|
6441
|
+
...HostExportAttribution,
|
|
6442
|
+
usage: HostUsageEvent,
|
|
6443
|
+
});
|
|
6444
|
+
export type HostUsageExport = z.infer<typeof HostUsageExport>;
|
|
6445
|
+
|
|
6446
|
+
export const HostEventExportBatch = z.object({
|
|
6447
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
6448
|
+
consumerId: HostExportConsumerId,
|
|
6449
|
+
leaseToken: z.string().uuid(),
|
|
6450
|
+
checkpoint: HostExportCursor,
|
|
6451
|
+
throughCursor: HostExportCursor,
|
|
6452
|
+
events: z.array(HostEventExport).min(1).max(256),
|
|
6453
|
+
});
|
|
6454
|
+
export type HostEventExportBatch = z.infer<typeof HostEventExportBatch>;
|
|
6455
|
+
|
|
6456
|
+
export const HostUsageExportBatch = z.object({
|
|
6457
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
6458
|
+
consumerId: HostExportConsumerId,
|
|
6459
|
+
leaseToken: z.string().uuid(),
|
|
6460
|
+
checkpoint: HostExportCursor,
|
|
6461
|
+
throughCursor: HostExportCursor,
|
|
6462
|
+
events: z.array(HostUsageExport).min(1).max(256),
|
|
6463
|
+
});
|
|
6464
|
+
export type HostUsageExportBatch = z.infer<typeof HostUsageExportBatch>;
|
|
6465
|
+
|
|
6466
|
+
/**
|
|
6467
|
+
* Optional embedded-host sinks. Delivery is at least once: the same batch may
|
|
6468
|
+
* be repeated after a process dies between sink success and checkpoint commit,
|
|
6469
|
+
* so sinks must deduplicate by event/usage idempotency key.
|
|
6470
|
+
*/
|
|
6471
|
+
export type HostEventSink = {
|
|
6472
|
+
consumerId: HostExportConsumerId;
|
|
6473
|
+
deliverEvents: (batch: HostEventExportBatch) => Promise<void>;
|
|
6474
|
+
};
|
|
6475
|
+
|
|
6476
|
+
export type HostUsageSink = {
|
|
6477
|
+
consumerId: HostExportConsumerId;
|
|
6478
|
+
deliverUsage: (batch: HostUsageExportBatch) => Promise<void>;
|
|
6479
|
+
};
|
|
6480
|
+
|
|
6481
|
+
export const SESSION_EVENT_TYPE_MAX_BYTES = 256;
|
|
6482
|
+
export const SESSION_EVENT_CLIENT_EVENT_ID_MAX_BYTES = SESSION_OPERATION_KEY_MAX_CHARS * 4;
|
|
6483
|
+
export const SESSION_EVENT_TURN_ASSOCIATION_MAX_BYTES = 64;
|
|
6484
|
+
export const SESSION_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
|
|
6485
|
+
export const SESSION_EVENT_ENVELOPE_MAX_BYTES = 80 * 1024;
|
|
6486
|
+
|
|
6487
|
+
export type BoundSessionEventOptions = {
|
|
6488
|
+
surface?: SessionEventBoundarySurface;
|
|
6489
|
+
maxBytes?: number;
|
|
6490
|
+
};
|
|
6491
|
+
|
|
6492
|
+
/**
|
|
6493
|
+
* Canonical lossy projection for a complete session event. Payload bounds alone
|
|
6494
|
+
* are insufficient: a malformed retained row can also carry an oversized type,
|
|
6495
|
+
* client id, or duplicate diagnostic. Keep cursor/UUID identity intact, bound
|
|
6496
|
+
* every free-form envelope string, and assert the exact final JSON envelope.
|
|
6497
|
+
*/
|
|
6498
|
+
export function boundSessionEvent(
|
|
6499
|
+
event: SessionEvent,
|
|
6500
|
+
options: BoundSessionEventOptions = {},
|
|
6501
|
+
): SessionEvent {
|
|
6502
|
+
const surface = options.surface ?? "durable_audit";
|
|
6503
|
+
const maxBytes = Math.max(8 * 1024, options.maxBytes ?? SESSION_EVENT_ENVELOPE_MAX_BYTES);
|
|
6504
|
+
// Never stringify the untrusted complete event. Measurement has one global
|
|
6505
|
+
// work budget and never invokes accessors/custom toJSON; serialization is
|
|
6506
|
+
// permitted only after the compact projection below has been constructed.
|
|
6507
|
+
const originalBytes = measureSessionEventJson(event).bytes;
|
|
6508
|
+
const source = sessionEventOwnDataFields(event);
|
|
6509
|
+
const id = canonicalSessionEventUuid(source.id, SESSION_EVENT_ZERO_UUID);
|
|
6510
|
+
const workspaceId = canonicalSessionEventUuid(source.workspaceId, SESSION_EVENT_ZERO_UUID);
|
|
6511
|
+
const sessionId = canonicalSessionEventUuid(source.sessionId, SESSION_EVENT_ZERO_UUID);
|
|
6512
|
+
const sequence =
|
|
6513
|
+
source.sequence.readable &&
|
|
6514
|
+
typeof source.sequence.value === "number" &&
|
|
6515
|
+
Number.isSafeInteger(source.sequence.value) &&
|
|
6516
|
+
source.sequence.value > 0
|
|
6517
|
+
? source.sequence.value
|
|
6518
|
+
: 1;
|
|
6519
|
+
const occurredAt =
|
|
6520
|
+
source.occurredAt.readable &&
|
|
6521
|
+
typeof source.occurredAt.value === "string" &&
|
|
6522
|
+
sessionEventUtf8Bytes(source.occurredAt.value) <= 256
|
|
6523
|
+
? source.occurredAt.value
|
|
6524
|
+
: "1970-01-01T00:00:00.000Z";
|
|
6525
|
+
const rawType = source.type.readable ? source.type.value : undefined;
|
|
6526
|
+
const typeIsSafe =
|
|
6527
|
+
typeof rawType === "string" &&
|
|
6528
|
+
sessionEventUtf8Bytes(rawType) <= SESSION_EVENT_TYPE_MAX_BYTES &&
|
|
6529
|
+
!rawType.includes("\n") &&
|
|
6530
|
+
!rawType.includes("\r");
|
|
6531
|
+
const rawClientEventId = source.clientEventId.readable ? source.clientEventId.value : undefined;
|
|
6532
|
+
const clientEventId = boundOptionalSessionEventText(
|
|
6533
|
+
typeof rawClientEventId === "string" || rawClientEventId === null
|
|
6534
|
+
? rawClientEventId
|
|
6535
|
+
: undefined,
|
|
6536
|
+
SESSION_EVENT_CLIENT_EVENT_ID_MAX_BYTES,
|
|
6537
|
+
);
|
|
6538
|
+
const rawTurnAssociation = source.turnAssociation.readable
|
|
6539
|
+
? source.turnAssociation.value
|
|
6540
|
+
: undefined;
|
|
6541
|
+
const turnAssociation =
|
|
6542
|
+
rawTurnAssociation === null ||
|
|
6543
|
+
rawTurnAssociation === undefined ||
|
|
6544
|
+
rawTurnAssociation === "current" ||
|
|
6545
|
+
rawTurnAssociation === "late_rejected" ||
|
|
6546
|
+
rawTurnAssociation === "duplicate"
|
|
6547
|
+
? rawTurnAssociation
|
|
6548
|
+
: null;
|
|
6549
|
+
const rawDuplicateReason = source.duplicateReason.readable
|
|
6550
|
+
? source.duplicateReason.value
|
|
6551
|
+
: undefined;
|
|
6552
|
+
const duplicateReason = boundOptionalSessionEventText(
|
|
6553
|
+
typeof rawDuplicateReason === "string" || rawDuplicateReason === null
|
|
6554
|
+
? rawDuplicateReason
|
|
6555
|
+
: undefined,
|
|
6556
|
+
SESSION_EVENT_DUPLICATE_REASON_MAX_BYTES,
|
|
6557
|
+
);
|
|
6558
|
+
const turnId = canonicalOptionalSessionEventUuid(source.turnId);
|
|
6559
|
+
const turnGeneration = canonicalSessionEventGeneration(source.turnGeneration);
|
|
6560
|
+
const turnAttemptId = canonicalOptionalSessionEventUuid(source.turnAttemptId);
|
|
6561
|
+
const duplicateOfEventId = canonicalOptionalSessionEventUuid(source.duplicateOfEventId);
|
|
6562
|
+
const envelopeFields = [
|
|
6563
|
+
sessionEventCustomSerializerProjection(event),
|
|
6564
|
+
sessionEventAdditionalTopLevelFieldProjection(event),
|
|
6565
|
+
!typeIsSafe
|
|
6566
|
+
? sessionEventEnvelopeFieldProjection(
|
|
6567
|
+
"type",
|
|
6568
|
+
rawType,
|
|
6569
|
+
"session.event.envelope_omitted",
|
|
6570
|
+
source.type.readable,
|
|
6571
|
+
)
|
|
6572
|
+
: null,
|
|
6573
|
+
!source.clientEventId.readable || rawClientEventId !== clientEventId
|
|
6574
|
+
? sessionEventEnvelopeFieldProjection(
|
|
6575
|
+
"clientEventId",
|
|
6576
|
+
rawClientEventId,
|
|
6577
|
+
clientEventId,
|
|
6578
|
+
source.clientEventId.readable,
|
|
6579
|
+
)
|
|
6580
|
+
: null,
|
|
6581
|
+
!source.turnAssociation.readable || rawTurnAssociation !== turnAssociation
|
|
6582
|
+
? sessionEventEnvelopeFieldProjection(
|
|
6583
|
+
"turnAssociation",
|
|
6584
|
+
rawTurnAssociation,
|
|
6585
|
+
turnAssociation,
|
|
6586
|
+
source.turnAssociation.readable,
|
|
6587
|
+
)
|
|
6588
|
+
: null,
|
|
6589
|
+
!source.duplicateReason.readable || rawDuplicateReason !== duplicateReason
|
|
6590
|
+
? sessionEventEnvelopeFieldProjection(
|
|
6591
|
+
"duplicateReason",
|
|
6592
|
+
rawDuplicateReason,
|
|
6593
|
+
duplicateReason,
|
|
6594
|
+
source.duplicateReason.readable,
|
|
6595
|
+
)
|
|
6596
|
+
: null,
|
|
6597
|
+
...sessionEventCanonicalFieldProjections(source, {
|
|
6598
|
+
id,
|
|
6599
|
+
workspaceId,
|
|
6600
|
+
sessionId,
|
|
6601
|
+
sequence,
|
|
6602
|
+
occurredAt,
|
|
6603
|
+
}),
|
|
6604
|
+
...sessionEventOptionalFieldProjections(source, {
|
|
6605
|
+
turnId,
|
|
6606
|
+
turnGeneration,
|
|
6607
|
+
turnAttemptId,
|
|
6608
|
+
duplicateOfEventId,
|
|
6609
|
+
}),
|
|
6610
|
+
!source.payload.readable
|
|
6611
|
+
? sessionEventEnvelopeFieldProjection("payload", undefined, null, false)
|
|
6612
|
+
: null,
|
|
6613
|
+
].filter((field) => field !== null);
|
|
6614
|
+
const rawPayload = source.payload.readable
|
|
6615
|
+
? source.payload.value
|
|
6616
|
+
: "[event payload accessor omitted at bounded projection boundary]";
|
|
6617
|
+
const payload =
|
|
6618
|
+
envelopeFields.length === 0
|
|
6619
|
+
? boundSessionEventPayload(rawPayload, { surface })
|
|
6620
|
+
: boundSessionEventPayload(
|
|
6621
|
+
{
|
|
6622
|
+
preview: "[legacy event envelope normalized at bounded projection boundary]",
|
|
6623
|
+
originalEventBytes: originalBytes,
|
|
6624
|
+
originalType: typeof rawType === "string" ? boundSessionEventText(rawType, 256) : null,
|
|
6625
|
+
envelopeProjection: {
|
|
6626
|
+
truncated: true,
|
|
6627
|
+
surface,
|
|
6628
|
+
fields: envelopeFields,
|
|
6629
|
+
},
|
|
6630
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
6631
|
+
},
|
|
6632
|
+
{ surface, maxBytes: 8 * 1024 },
|
|
6633
|
+
);
|
|
6634
|
+
const bounded: SessionEvent = {
|
|
6635
|
+
id,
|
|
6636
|
+
workspaceId,
|
|
6637
|
+
sessionId,
|
|
6638
|
+
sequence,
|
|
6639
|
+
type: typeIsSafe ? (rawType as SessionEvent["type"]) : "session.event.envelope_omitted",
|
|
6640
|
+
payload,
|
|
6641
|
+
occurredAt,
|
|
6642
|
+
...(sessionEventShouldEmitOptionalField(source.clientEventId) ? { clientEventId } : {}),
|
|
6643
|
+
...(sessionEventShouldEmitOptionalField(source.turnId) ? { turnId } : {}),
|
|
6644
|
+
...(sessionEventShouldEmitOptionalField(source.turnGeneration) ? { turnGeneration } : {}),
|
|
6645
|
+
...(sessionEventShouldEmitOptionalField(source.turnAttemptId) ? { turnAttemptId } : {}),
|
|
6646
|
+
...(sessionEventShouldEmitOptionalField(source.turnAssociation) ? { turnAssociation } : {}),
|
|
6647
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateOfEventId)
|
|
6648
|
+
? { duplicateOfEventId }
|
|
6649
|
+
: {}),
|
|
6650
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateReason) ? { duplicateReason } : {}),
|
|
6651
|
+
};
|
|
6652
|
+
if (sessionEventJsonBytes(bounded) <= maxBytes) return bounded;
|
|
6653
|
+
|
|
6654
|
+
const fallback: SessionEvent = {
|
|
6655
|
+
id,
|
|
6656
|
+
workspaceId,
|
|
6657
|
+
sessionId,
|
|
6658
|
+
sequence,
|
|
6659
|
+
type: "session.event.envelope_omitted",
|
|
6660
|
+
payload: boundSessionEventPayload(
|
|
6661
|
+
{
|
|
6662
|
+
preview: "[legacy event envelope omitted at bounded projection boundary]",
|
|
6663
|
+
originalEventBytes: originalBytes,
|
|
6664
|
+
originalType: typeof rawType === "string" ? boundSessionEventText(rawType, 256) : null,
|
|
6665
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
6666
|
+
},
|
|
6667
|
+
{ surface, maxBytes: 4 * 1024 },
|
|
6668
|
+
),
|
|
6669
|
+
occurredAt,
|
|
6670
|
+
...(sessionEventShouldEmitOptionalField(source.clientEventId) ? { clientEventId } : {}),
|
|
6671
|
+
...(sessionEventShouldEmitOptionalField(source.turnId) ? { turnId } : {}),
|
|
6672
|
+
...(sessionEventShouldEmitOptionalField(source.turnGeneration) ? { turnGeneration } : {}),
|
|
6673
|
+
...(sessionEventShouldEmitOptionalField(source.turnAttemptId) ? { turnAttemptId } : {}),
|
|
6674
|
+
...(sessionEventShouldEmitOptionalField(source.turnAssociation) ? { turnAssociation } : {}),
|
|
6675
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateOfEventId)
|
|
6676
|
+
? { duplicateOfEventId }
|
|
6677
|
+
: {}),
|
|
6678
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateReason) ? { duplicateReason } : {}),
|
|
6679
|
+
};
|
|
6680
|
+
const deliveredBytes = sessionEventJsonBytes(fallback);
|
|
6681
|
+
if (deliveredBytes > maxBytes) {
|
|
6682
|
+
throw new RangeError(
|
|
6683
|
+
`Bounded session event exceeds its final envelope (${deliveredBytes} > ${maxBytes} bytes)`,
|
|
6684
|
+
);
|
|
6685
|
+
}
|
|
6686
|
+
return fallback;
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
function sessionEventEnvelopeFieldProjection(
|
|
6690
|
+
field: string,
|
|
6691
|
+
original: unknown,
|
|
6692
|
+
delivered: unknown,
|
|
6693
|
+
originalReadable = true,
|
|
6694
|
+
): { field: string; originalBytes: number | null; deliveredBytes: number } {
|
|
6695
|
+
return {
|
|
6696
|
+
field,
|
|
6697
|
+
originalBytes: originalReadable
|
|
6698
|
+
? typeof original === "string"
|
|
6699
|
+
? sessionEventUtf8Bytes(original)
|
|
6700
|
+
: typeof original === "number" || typeof original === "boolean"
|
|
6701
|
+
? sessionEventJsonBytes(original)
|
|
6702
|
+
: original === null || original === undefined
|
|
6703
|
+
? 0
|
|
6704
|
+
: null
|
|
6705
|
+
: null,
|
|
6706
|
+
deliveredBytes:
|
|
6707
|
+
typeof delivered === "string"
|
|
6708
|
+
? sessionEventUtf8Bytes(delivered)
|
|
6709
|
+
: typeof delivered === "number" || typeof delivered === "boolean"
|
|
6710
|
+
? sessionEventJsonBytes(delivered)
|
|
6711
|
+
: 0,
|
|
6712
|
+
};
|
|
6713
|
+
}
|
|
6714
|
+
|
|
6715
|
+
function sessionEventCustomSerializerProjection(
|
|
6716
|
+
event: SessionEvent,
|
|
6717
|
+
): { field: string; originalBytes: null; deliveredBytes: 0 } | null {
|
|
6718
|
+
const projection = {
|
|
6719
|
+
field: "toJSON",
|
|
6720
|
+
originalBytes: null,
|
|
6721
|
+
deliveredBytes: 0,
|
|
6722
|
+
} as const;
|
|
6723
|
+
let candidate: object | null = event;
|
|
6724
|
+
try {
|
|
6725
|
+
for (let depth = 0; depth <= SESSION_EVENT_PROTOTYPE_MAX_DEPTH; depth += 1) {
|
|
6726
|
+
if (candidate === null) return null;
|
|
6727
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, "toJSON");
|
|
6728
|
+
if (descriptor) {
|
|
6729
|
+
// JSON.stringify performs an ordinary lookup, so an accessor is both
|
|
6730
|
+
// executable behavior and an unknown possible serializer. A data
|
|
6731
|
+
// property shadows the rest of the chain and is relevant only when it
|
|
6732
|
+
// is callable.
|
|
6733
|
+
return !("value" in descriptor) || typeof descriptor.value === "function"
|
|
6734
|
+
? projection
|
|
6735
|
+
: null;
|
|
6736
|
+
}
|
|
6737
|
+
candidate = Object.getPrototypeOf(candidate);
|
|
6738
|
+
}
|
|
6739
|
+
// A hostile or malformed prototype chain that exceeds the fixed lookup
|
|
6740
|
+
// budget cannot prove the absence of inherited serialization behavior.
|
|
6741
|
+
return projection;
|
|
6742
|
+
} catch {
|
|
6743
|
+
return projection;
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
|
|
6747
|
+
const SESSION_EVENT_PROTOTYPE_MAX_DEPTH = 32;
|
|
6748
|
+
const SESSION_EVENT_ZERO_UUID = "00000000-0000-4000-8000-000000000000";
|
|
6749
|
+
const SESSION_EVENT_UUID_PATTERN =
|
|
6750
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6751
|
+
|
|
6752
|
+
const SESSION_EVENT_OWN_DATA_FIELDS = [
|
|
6753
|
+
"id",
|
|
6754
|
+
"workspaceId",
|
|
6755
|
+
"sessionId",
|
|
6756
|
+
"sequence",
|
|
6757
|
+
"type",
|
|
6758
|
+
"payload",
|
|
6759
|
+
"occurredAt",
|
|
6760
|
+
"clientEventId",
|
|
6761
|
+
"turnId",
|
|
6762
|
+
"turnGeneration",
|
|
6763
|
+
"turnAttemptId",
|
|
6764
|
+
"turnAssociation",
|
|
6765
|
+
"duplicateOfEventId",
|
|
6766
|
+
"duplicateReason",
|
|
6767
|
+
] as const satisfies readonly (keyof SessionEvent)[];
|
|
6768
|
+
const SESSION_EVENT_KNOWN_ENUMERABLE_FIELDS = new Set<string>([
|
|
6769
|
+
...SESSION_EVENT_OWN_DATA_FIELDS,
|
|
6770
|
+
"toJSON",
|
|
6771
|
+
]);
|
|
6772
|
+
|
|
6773
|
+
/**
|
|
6774
|
+
* Detect future/legacy own enumerable envelope fields without reading their
|
|
6775
|
+
* values. There can be at most the fixed known-key cardinality before an
|
|
6776
|
+
* additional key must be observed, so the source-level iterator is bounded.
|
|
6777
|
+
* A proxy/enumeration failure is conservatively surfaced as unknown loss.
|
|
6778
|
+
*/
|
|
6779
|
+
function sessionEventAdditionalTopLevelFieldProjection(
|
|
6780
|
+
event: SessionEvent,
|
|
6781
|
+
): { field: string; originalBytes: null; deliveredBytes: 0 } | null {
|
|
6782
|
+
const projection = {
|
|
6783
|
+
field: "additionalTopLevelFields",
|
|
6784
|
+
originalBytes: null,
|
|
6785
|
+
deliveredBytes: 0,
|
|
6786
|
+
} as const;
|
|
6787
|
+
let inspected = 0;
|
|
6788
|
+
try {
|
|
6789
|
+
for (const key in event as SessionEvent & Record<string, unknown>) {
|
|
6790
|
+
inspected += 1;
|
|
6791
|
+
if (inspected > SESSION_EVENT_KNOWN_ENUMERABLE_FIELDS.size + 1) return projection;
|
|
6792
|
+
const descriptor = Object.getOwnPropertyDescriptor(event, key);
|
|
6793
|
+
if (descriptor?.enumerable && !SESSION_EVENT_KNOWN_ENUMERABLE_FIELDS.has(key)) {
|
|
6794
|
+
return projection;
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
return null;
|
|
6798
|
+
} catch {
|
|
6799
|
+
return projection;
|
|
6800
|
+
}
|
|
6801
|
+
}
|
|
6802
|
+
|
|
6803
|
+
type SessionEventOwnField = { readable: true; value: unknown } | { readable: false };
|
|
6804
|
+
type SessionEventOwnDataFields = Record<keyof SessionEvent, SessionEventOwnField>;
|
|
6805
|
+
|
|
6806
|
+
function sessionEventOwnDataFields(event: SessionEvent): SessionEventOwnDataFields {
|
|
6807
|
+
return Object.fromEntries(
|
|
6808
|
+
SESSION_EVENT_OWN_DATA_FIELDS.map((key) => {
|
|
6809
|
+
try {
|
|
6810
|
+
const descriptor = Object.getOwnPropertyDescriptor(event, key);
|
|
6811
|
+
if (!descriptor) return [key, { readable: true, value: undefined }];
|
|
6812
|
+
return [
|
|
6813
|
+
key,
|
|
6814
|
+
"value" in descriptor ? { readable: true, value: descriptor.value } : { readable: false },
|
|
6815
|
+
];
|
|
6816
|
+
} catch {
|
|
6817
|
+
return [key, { readable: false }];
|
|
6818
|
+
}
|
|
6819
|
+
}),
|
|
6820
|
+
) as SessionEventOwnDataFields;
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6823
|
+
function canonicalSessionEventUuid(field: SessionEventOwnField, fallback: string): string {
|
|
6824
|
+
return field.readable &&
|
|
6825
|
+
typeof field.value === "string" &&
|
|
6826
|
+
SESSION_EVENT_UUID_PATTERN.test(field.value)
|
|
6827
|
+
? field.value
|
|
6828
|
+
: fallback;
|
|
6829
|
+
}
|
|
6830
|
+
|
|
6831
|
+
function canonicalOptionalSessionEventUuid(field: SessionEventOwnField): string | null {
|
|
6832
|
+
return field.readable &&
|
|
6833
|
+
typeof field.value === "string" &&
|
|
6834
|
+
SESSION_EVENT_UUID_PATTERN.test(field.value)
|
|
6835
|
+
? field.value
|
|
6836
|
+
: null;
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6839
|
+
function canonicalSessionEventGeneration(field: SessionEventOwnField): number | null {
|
|
6840
|
+
return field.readable &&
|
|
6841
|
+
typeof field.value === "number" &&
|
|
6842
|
+
Number.isSafeInteger(field.value) &&
|
|
6843
|
+
field.value >= 0
|
|
6844
|
+
? field.value
|
|
6845
|
+
: null;
|
|
6846
|
+
}
|
|
6847
|
+
|
|
6848
|
+
function sessionEventShouldEmitOptionalField(field: SessionEventOwnField): boolean {
|
|
6849
|
+
return !field.readable || field.value !== undefined;
|
|
6850
|
+
}
|
|
6851
|
+
|
|
6852
|
+
function sessionEventCanonicalFieldProjections(
|
|
6853
|
+
source: SessionEventOwnDataFields,
|
|
6854
|
+
delivered: {
|
|
6855
|
+
id: string;
|
|
6856
|
+
workspaceId: string;
|
|
6857
|
+
sessionId: string;
|
|
6858
|
+
sequence: number;
|
|
6859
|
+
occurredAt: string;
|
|
6860
|
+
},
|
|
6861
|
+
): Array<{
|
|
6862
|
+
field: string;
|
|
6863
|
+
originalBytes: number | null;
|
|
6864
|
+
deliveredBytes: number;
|
|
6865
|
+
}> {
|
|
6866
|
+
return (["id", "workspaceId", "sessionId", "sequence", "occurredAt"] as const).flatMap(
|
|
6867
|
+
(field) => {
|
|
6868
|
+
const original = source[field].readable ? source[field].value : undefined;
|
|
6869
|
+
return source[field].readable && original === delivered[field]
|
|
6870
|
+
? []
|
|
6871
|
+
: [
|
|
6872
|
+
sessionEventEnvelopeFieldProjection(
|
|
6873
|
+
field,
|
|
6874
|
+
original,
|
|
6875
|
+
delivered[field],
|
|
6876
|
+
source[field].readable,
|
|
6877
|
+
),
|
|
6878
|
+
];
|
|
6879
|
+
},
|
|
6880
|
+
);
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
function sessionEventOptionalFieldProjections(
|
|
6884
|
+
source: SessionEventOwnDataFields,
|
|
6885
|
+
delivered: {
|
|
6886
|
+
turnId: string | null;
|
|
6887
|
+
turnGeneration: number | null;
|
|
6888
|
+
turnAttemptId: string | null;
|
|
6889
|
+
duplicateOfEventId: string | null;
|
|
6890
|
+
},
|
|
6891
|
+
): Array<{
|
|
6892
|
+
field: string;
|
|
6893
|
+
originalBytes: number | null;
|
|
6894
|
+
deliveredBytes: number;
|
|
6895
|
+
}> {
|
|
6896
|
+
return (["turnId", "turnGeneration", "turnAttemptId", "duplicateOfEventId"] as const).flatMap(
|
|
6897
|
+
(field) => {
|
|
6898
|
+
const original = source[field].readable ? source[field].value : undefined;
|
|
6899
|
+
const canonicalOriginal = original ?? null;
|
|
6900
|
+
return source[field].readable && canonicalOriginal === delivered[field]
|
|
6901
|
+
? []
|
|
6902
|
+
: [
|
|
6903
|
+
sessionEventEnvelopeFieldProjection(
|
|
6904
|
+
field,
|
|
6905
|
+
original,
|
|
6906
|
+
delivered[field],
|
|
6907
|
+
source[field].readable,
|
|
6908
|
+
),
|
|
6909
|
+
];
|
|
6910
|
+
},
|
|
6911
|
+
);
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
function boundOptionalSessionEventText<T extends string | null | undefined>(
|
|
6915
|
+
value: T,
|
|
6916
|
+
maxBytes: number,
|
|
6917
|
+
): T {
|
|
6918
|
+
return (typeof value === "string" ? boundSessionEventText(value, maxBytes) : value) as T;
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6921
|
+
function boundSessionEventText(value: string, maxBytes: number): string {
|
|
6922
|
+
const encoder = new TextEncoder();
|
|
6923
|
+
const decoder = new TextDecoder();
|
|
6924
|
+
const bytes = encoder.encode(value);
|
|
6925
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
6926
|
+
const marker = "…[truncated]";
|
|
6927
|
+
const markerBytes = encoder.encode(marker).byteLength;
|
|
6928
|
+
const prefixBudget = Math.max(0, maxBytes - markerBytes);
|
|
6929
|
+
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
6930
|
+
while (prefixEnd > 0 && prefixEnd < bytes.byteLength && (bytes[prefixEnd]! & 0xc0) === 0x80) {
|
|
6931
|
+
prefixEnd -= 1;
|
|
6932
|
+
}
|
|
6933
|
+
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6936
|
+
function sessionEventUtf8Bytes(value: string): number {
|
|
6937
|
+
return new TextEncoder().encode(value).byteLength;
|
|
6938
|
+
}
|
|
6939
|
+
|
|
4079
6940
|
export const SessionQueueMutationResponse = z.object({
|
|
6941
|
+
receipt: SessionCommandReceipt,
|
|
4080
6942
|
snapshot: SessionQueueSnapshot,
|
|
4081
|
-
|
|
4082
|
-
shouldWake: z.boolean(),
|
|
6943
|
+
draft: ComposerDraft.optional(),
|
|
4083
6944
|
});
|
|
4084
6945
|
export type SessionQueueMutationResponse = z.infer<typeof SessionQueueMutationResponse>;
|
|
4085
6946
|
|
|
4086
6947
|
export const SessionControlResponse = z.object({
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
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(),
|
|
6948
|
+
receipt: SessionCommandReceipt,
|
|
6949
|
+
effectiveControl: EffectiveSessionControl,
|
|
6950
|
+
interruptionCount: z.number().int().nonnegative(),
|
|
6951
|
+
wakeCount: z.number().int().nonnegative(),
|
|
4097
6952
|
});
|
|
4098
6953
|
export type SessionControlResponse = z.infer<typeof SessionControlResponse>;
|
|
4099
6954
|
|
|
4100
6955
|
export const CreateSessionRequest = withVariableSetIdAlias({
|
|
6956
|
+
/**
|
|
6957
|
+
* Optional UUID preallocated by an embedding host. This lets the host durably
|
|
6958
|
+
* link its own projection before OpenGeni admits the initial turn. Replays
|
|
6959
|
+
* must pair it with the same idempotency key; OpenGeni never derives host
|
|
6960
|
+
* identity or authorization from the UUID.
|
|
6961
|
+
*/
|
|
6962
|
+
requestedSessionId: z.string().uuid().optional(),
|
|
4101
6963
|
initialMessage: z.string().min(1),
|
|
6964
|
+
// System-level host context for the initial turn only. Unlike `instructions`,
|
|
6965
|
+
// this does not persist into later turns and is never emitted as a user event.
|
|
6966
|
+
turnInstructions: z.string().trim().min(1).max(32768).optional(),
|
|
4102
6967
|
// Per-session agent persona/system instructions (org-visible metadata, NOT a
|
|
4103
6968
|
// secret). Rides the SAME system-level instructions channel the per-workspace
|
|
4104
6969
|
// agentInstructions rides, composed AFTER the workspace persona so it refines
|
|
@@ -4108,7 +6973,14 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4108
6973
|
// matches the codebase's largest free-form string convention (workspace
|
|
4109
6974
|
// variable set variable values). Absent ⇒ byte-identical to today.
|
|
4110
6975
|
instructions: z.string().trim().min(1).max(32768).optional(),
|
|
6976
|
+
// For an agent-created child, omission inherits the trusted immediate
|
|
6977
|
+
// parent's repository/file context. An explicit array, including [], is
|
|
6978
|
+
// authoritative. Top-level omission remains []. Presence is resolved from
|
|
6979
|
+
// the raw request because this Zod default erases absent-vs-empty.
|
|
4111
6980
|
resources: z.array(ResourceRef).default([]),
|
|
6981
|
+
// The same child omission rule applies to selected MCP tool refs. Top-level
|
|
6982
|
+
// omission still applies workspace-default capability MCP tools; explicit []
|
|
6983
|
+
// suppresses those defaults (the first-party OpenGeni server remains added).
|
|
4112
6984
|
tools: z.array(ToolRef).default([]),
|
|
4113
6985
|
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
4114
6986
|
model: z.string().min(1).optional(),
|
|
@@ -4135,7 +7007,7 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4135
7007
|
// behavior). An id that does not name a rig in the workspace is a 422.
|
|
4136
7008
|
rigId: z.string().uuid().optional(),
|
|
4137
7009
|
goal: GoalSpec.optional(),
|
|
4138
|
-
clientEventId:
|
|
7010
|
+
clientEventId: SessionOperationKey.optional(),
|
|
4139
7011
|
// Workspace-scoped CREATE idempotency key: collapses concurrent/retried
|
|
4140
7012
|
// create calls carrying the same key to a single session (partial unique
|
|
4141
7013
|
// index on (workspace_id, create_idempotency_key)). Distinct from
|
|
@@ -4143,14 +7015,20 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4143
7015
|
// creation of a brand-new session. Absent means no create-dedup (each call
|
|
4144
7016
|
// is an independent create).
|
|
4145
7017
|
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
4146
|
-
// Permissions the session's first-party MCP token should carry
|
|
4147
|
-
// the
|
|
4148
|
-
// the
|
|
4149
|
-
// requested permission must be held by the creating grant
|
|
7018
|
+
// Permissions the session's first-party MCP token should carry. A top-level
|
|
7019
|
+
// omission uses the deployment's worker default; a child omission inherits
|
|
7020
|
+
// the creating session's effective grant. An explicit set is capped at
|
|
7021
|
+
// creation: every requested permission must be held by the creating grant.
|
|
7022
|
+
// A goal-bearing session whose explicit/effective set omits goals:manage is
|
|
7023
|
+
// rejected; creation never silently expands a child beyond that set.
|
|
4150
7024
|
firstPartyMcpPermissions: z.array(Permission).optional(),
|
|
4151
|
-
// Third-party MCP servers attached only to this session.
|
|
4152
|
-
//
|
|
4153
|
-
|
|
7025
|
+
// Third-party MCP servers attached only to this session. For an agent-created
|
|
7026
|
+
// child, omission snapshots its trusted immediate parent's server definitions,
|
|
7027
|
+
// policies, connection refs, and encrypted credentials. Explicit arrays,
|
|
7028
|
+
// including [], are authoritative; non-empty explicit arrays require attach
|
|
7029
|
+
// permission. Credential headers are write-only: create responses and events
|
|
7030
|
+
// expose only SessionMcpServerMetadata.
|
|
7031
|
+
mcpServers: z.array(SessionMcpServerInput).max(SESSION_MCP_SERVERS_MAX).default([]),
|
|
4154
7032
|
// Shared-sandbox placement (addendum 05 §D.1). Three-way union; OMITTED ⇒
|
|
4155
7033
|
// today's behavior (a context-dependent default resolved server-side: from
|
|
4156
7034
|
// inside a session → "shared" with the creator's box, top-level → "new").
|
|
@@ -4174,16 +7052,198 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4174
7052
|
});
|
|
4175
7053
|
export type CreateSessionRequest = z.infer<typeof CreateSessionRequest>;
|
|
4176
7054
|
|
|
7055
|
+
// Generic, host-neutral structured human input. One model tool call creates one
|
|
7056
|
+
// request containing one or more questions; the durable response resumes that
|
|
7057
|
+
// exact call. This is deliberately distinct from tool approval: an answer,
|
|
7058
|
+
// skip, or expiry is structured tool output, never an approve/reject decision.
|
|
7059
|
+
export const HumanInputQuestionKind = z.enum(["text", "single_select", "multi_select"]);
|
|
7060
|
+
export type HumanInputQuestionKind = z.infer<typeof HumanInputQuestionKind>;
|
|
7061
|
+
|
|
7062
|
+
export const HumanInputOption = z.object({
|
|
7063
|
+
id: z.string().min(1).max(64),
|
|
7064
|
+
label: z.string().min(1).max(256),
|
|
7065
|
+
description: z.string().max(2048).nullable().optional(),
|
|
7066
|
+
});
|
|
7067
|
+
export type HumanInputOption = z.infer<typeof HumanInputOption>;
|
|
7068
|
+
|
|
7069
|
+
export const HumanInputQuestion = z
|
|
7070
|
+
.object({
|
|
7071
|
+
id: z.string().min(1).max(64),
|
|
7072
|
+
kind: HumanInputQuestionKind,
|
|
7073
|
+
prompt: z.string().min(1).max(4096),
|
|
7074
|
+
label: z.string().min(1).max(128).nullable().optional(),
|
|
7075
|
+
helpText: z.string().max(2048).nullable().optional(),
|
|
7076
|
+
options: z.array(HumanInputOption).max(20).default([]),
|
|
7077
|
+
required: z.boolean().default(true),
|
|
7078
|
+
allowOther: z.boolean().default(false),
|
|
7079
|
+
validation: z
|
|
7080
|
+
.object({
|
|
7081
|
+
minLength: z.number().int().nonnegative().max(8192).nullable().optional(),
|
|
7082
|
+
maxLength: z.number().int().positive().max(8192).nullable().optional(),
|
|
7083
|
+
minSelections: z.number().int().nonnegative().max(20).nullable().optional(),
|
|
7084
|
+
maxSelections: z.number().int().positive().max(20).nullable().optional(),
|
|
7085
|
+
})
|
|
7086
|
+
.nullable()
|
|
7087
|
+
.optional(),
|
|
7088
|
+
})
|
|
7089
|
+
.superRefine((question, ctx) => {
|
|
7090
|
+
const optionIds = new Set(question.options.map((option) => option.id));
|
|
7091
|
+
if (optionIds.size !== question.options.length) {
|
|
7092
|
+
ctx.addIssue({
|
|
7093
|
+
code: "custom",
|
|
7094
|
+
path: ["options"],
|
|
7095
|
+
message: "option ids must be unique",
|
|
7096
|
+
});
|
|
7097
|
+
}
|
|
7098
|
+
if (question.kind === "text") {
|
|
7099
|
+
if (question.options.length > 0) {
|
|
7100
|
+
ctx.addIssue({
|
|
7101
|
+
code: "custom",
|
|
7102
|
+
path: ["options"],
|
|
7103
|
+
message: "text questions cannot have options",
|
|
7104
|
+
});
|
|
7105
|
+
}
|
|
7106
|
+
if (question.allowOther) {
|
|
7107
|
+
ctx.addIssue({
|
|
7108
|
+
code: "custom",
|
|
7109
|
+
path: ["allowOther"],
|
|
7110
|
+
message: "text questions do not use Other",
|
|
7111
|
+
});
|
|
7112
|
+
}
|
|
7113
|
+
} else if (question.options.length === 0) {
|
|
7114
|
+
ctx.addIssue({
|
|
7115
|
+
code: "custom",
|
|
7116
|
+
path: ["options"],
|
|
7117
|
+
message: "select questions require options",
|
|
7118
|
+
});
|
|
7119
|
+
}
|
|
7120
|
+
const validation = question.validation;
|
|
7121
|
+
if (
|
|
7122
|
+
validation?.minLength != null &&
|
|
7123
|
+
validation?.maxLength != null &&
|
|
7124
|
+
validation.minLength > validation.maxLength
|
|
7125
|
+
) {
|
|
7126
|
+
ctx.addIssue({
|
|
7127
|
+
code: "custom",
|
|
7128
|
+
path: ["validation"],
|
|
7129
|
+
message: "minLength exceeds maxLength",
|
|
7130
|
+
});
|
|
7131
|
+
}
|
|
7132
|
+
if (
|
|
7133
|
+
validation?.minSelections != null &&
|
|
7134
|
+
validation?.maxSelections != null &&
|
|
7135
|
+
validation.minSelections > validation.maxSelections
|
|
7136
|
+
) {
|
|
7137
|
+
ctx.addIssue({
|
|
7138
|
+
code: "custom",
|
|
7139
|
+
path: ["validation"],
|
|
7140
|
+
message: "minSelections exceeds maxSelections",
|
|
7141
|
+
});
|
|
7142
|
+
}
|
|
7143
|
+
});
|
|
7144
|
+
export type HumanInputQuestion = z.infer<typeof HumanInputQuestion>;
|
|
7145
|
+
|
|
7146
|
+
export const HumanInputRequestStatus = z.enum([
|
|
7147
|
+
"pending",
|
|
7148
|
+
"answered",
|
|
7149
|
+
"skipped",
|
|
7150
|
+
"expired",
|
|
7151
|
+
"cancelled",
|
|
7152
|
+
]);
|
|
7153
|
+
export type HumanInputRequestStatus = z.infer<typeof HumanInputRequestStatus>;
|
|
7154
|
+
|
|
7155
|
+
export const RequestHumanInputToolInput = z.object({
|
|
7156
|
+
questions: z.array(HumanInputQuestion).min(1).max(20),
|
|
7157
|
+
allowSkip: z.boolean().default(false),
|
|
7158
|
+
expiresInSeconds: z
|
|
7159
|
+
.number()
|
|
7160
|
+
.int()
|
|
7161
|
+
.positive()
|
|
7162
|
+
.max(30 * 24 * 60 * 60)
|
|
7163
|
+
.nullable()
|
|
7164
|
+
.optional(),
|
|
7165
|
+
});
|
|
7166
|
+
export type RequestHumanInputToolInput = z.infer<typeof RequestHumanInputToolInput>;
|
|
7167
|
+
|
|
7168
|
+
export const HumanInputAnswer = z.object({
|
|
7169
|
+
questionId: z.string().min(1).max(64),
|
|
7170
|
+
values: z.array(z.string().max(8192)).max(20),
|
|
7171
|
+
other: z.string().max(8192).nullable().optional(),
|
|
7172
|
+
});
|
|
7173
|
+
export type HumanInputAnswer = z.infer<typeof HumanInputAnswer>;
|
|
7174
|
+
|
|
7175
|
+
export const HumanInputResponse = z.discriminatedUnion("outcome", [
|
|
7176
|
+
z.object({
|
|
7177
|
+
outcome: z.literal("answered"),
|
|
7178
|
+
answers: z.array(HumanInputAnswer).max(20),
|
|
7179
|
+
}),
|
|
7180
|
+
z.object({ outcome: z.literal("skipped") }),
|
|
7181
|
+
z.object({ outcome: z.literal("expired") }),
|
|
7182
|
+
z.object({ outcome: z.literal("cancelled") }),
|
|
7183
|
+
]);
|
|
7184
|
+
export type HumanInputResponse = z.infer<typeof HumanInputResponse>;
|
|
7185
|
+
|
|
7186
|
+
export const SubmitHumanInputResponseRequest = z.discriminatedUnion("outcome", [
|
|
7187
|
+
z.object({
|
|
7188
|
+
outcome: z.literal("answered"),
|
|
7189
|
+
answers: z.array(HumanInputAnswer).max(20),
|
|
7190
|
+
}),
|
|
7191
|
+
z.object({ outcome: z.literal("skipped") }),
|
|
7192
|
+
]);
|
|
7193
|
+
export type SubmitHumanInputResponseRequest = z.infer<typeof SubmitHumanInputResponseRequest>;
|
|
7194
|
+
|
|
7195
|
+
export const SessionHumanInputRequest = z.object({
|
|
7196
|
+
id: z.string().uuid(),
|
|
7197
|
+
workspaceId: z.string().uuid(),
|
|
7198
|
+
sessionId: z.string().uuid(),
|
|
7199
|
+
turnId: z.string().uuid(),
|
|
7200
|
+
turnGeneration: z.number().int().positive(),
|
|
7201
|
+
creationAttemptId: z.string().uuid(),
|
|
7202
|
+
toolCallId: z.string().min(1).max(1024),
|
|
7203
|
+
status: HumanInputRequestStatus,
|
|
7204
|
+
questions: z.array(HumanInputQuestion).min(1).max(20),
|
|
7205
|
+
allowSkip: z.boolean(),
|
|
7206
|
+
response: HumanInputResponse.nullable(),
|
|
7207
|
+
respondedBy: z.string().max(1024).nullable(),
|
|
7208
|
+
respondedAt: z.string().nullable(),
|
|
7209
|
+
expiresAt: z.string().nullable(),
|
|
7210
|
+
createdAt: z.string(),
|
|
7211
|
+
updatedAt: z.string(),
|
|
7212
|
+
});
|
|
7213
|
+
export type SessionHumanInputRequest = z.infer<typeof SessionHumanInputRequest>;
|
|
7214
|
+
|
|
7215
|
+
/**
|
|
7216
|
+
* Extract the stable approval identity used by both durable admission and
|
|
7217
|
+
* runtime resume. Serialized SDK interruptions may place it on the wrapper or
|
|
7218
|
+
* its raw item; malformed entries fail closed instead of inventing an id.
|
|
7219
|
+
*/
|
|
7220
|
+
export function approvalIdentifier(value: unknown): string | null {
|
|
7221
|
+
if (!value || typeof value !== "object") return null;
|
|
7222
|
+
const approval = value as Record<string, unknown>;
|
|
7223
|
+
const rawItem =
|
|
7224
|
+
approval.rawItem && typeof approval.rawItem === "object"
|
|
7225
|
+
? (approval.rawItem as Record<string, unknown>)
|
|
7226
|
+
: null;
|
|
7227
|
+
const candidate = rawItem?.callId ?? rawItem?.id ?? approval.id ?? approval.name;
|
|
7228
|
+
if (typeof candidate !== "string" && typeof candidate !== "number") return null;
|
|
7229
|
+
return String(candidate);
|
|
7230
|
+
}
|
|
7231
|
+
|
|
4177
7232
|
export const ClientSessionEvent = z.discriminatedUnion("type", [
|
|
4178
7233
|
z.object({
|
|
4179
7234
|
type: z.literal("user.message"),
|
|
4180
|
-
clientEventId:
|
|
7235
|
+
clientEventId: SessionOperationKey.optional(),
|
|
4181
7236
|
payload: z.object({
|
|
4182
7237
|
text: z.string().min(1),
|
|
7238
|
+
// System-level host context for this exact turn only. Persisted on the
|
|
7239
|
+
// turn for retry/recovery, never copied into the visible user message.
|
|
7240
|
+
turnInstructions: z.string().trim().min(1).max(32768).optional(),
|
|
4183
7241
|
resources: z.array(ResourceRef).default([]),
|
|
4184
7242
|
tools: z.array(ToolRef).default([]),
|
|
4185
7243
|
model: z.string().min(1).optional(),
|
|
4186
7244
|
reasoningEffort: ReasoningEffort.optional(),
|
|
7245
|
+
controlEtag: z.string().min(1).optional(),
|
|
7246
|
+
expectedDraftRevision: z.number().int().nonnegative().optional(),
|
|
4187
7247
|
// Header-value rotation only. URL/name/tool settings are immutable after
|
|
4188
7248
|
// session create; persisted events expose metadata, never header values.
|
|
4189
7249
|
mcpCredentialUpdates: z.array(SessionMcpCredentialUpdateInput).optional(),
|
|
@@ -4191,25 +7251,35 @@ export const ClientSessionEvent = z.discriminatedUnion("type", [
|
|
|
4191
7251
|
}),
|
|
4192
7252
|
z.object({
|
|
4193
7253
|
type: z.literal("user.approvalDecision"),
|
|
4194
|
-
clientEventId:
|
|
7254
|
+
clientEventId: SessionOperationKey.optional(),
|
|
4195
7255
|
payload: z.object({
|
|
4196
|
-
approvalId: z.string().min(1),
|
|
7256
|
+
approvalId: z.string().min(1).max(SESSION_OPERATION_KEY_MAX_CHARS),
|
|
4197
7257
|
decision: z.enum(["approve", "reject"]),
|
|
4198
7258
|
message: z.string().optional(),
|
|
4199
7259
|
}),
|
|
4200
7260
|
}),
|
|
7261
|
+
z.object({
|
|
7262
|
+
type: z.literal("user.humanInputResponse"),
|
|
7263
|
+
clientEventId: SessionOperationKey.optional(),
|
|
7264
|
+
payload: z.object({
|
|
7265
|
+
requestId: z.string().uuid(),
|
|
7266
|
+
response: SubmitHumanInputResponseRequest,
|
|
7267
|
+
}),
|
|
7268
|
+
}),
|
|
4201
7269
|
]);
|
|
4202
7270
|
export type ClientSessionEvent = z.infer<typeof ClientSessionEvent>;
|
|
4203
7271
|
|
|
4204
7272
|
export const SteerSessionMessageRequest = z.object({
|
|
4205
7273
|
text: z.string().min(1),
|
|
7274
|
+
// Same per-turn system-level context as a queued user.message.
|
|
7275
|
+
turnInstructions: z.string().trim().min(1).max(32768).optional(),
|
|
4206
7276
|
resources: z.array(ResourceRef).default([]),
|
|
4207
7277
|
tools: z.array(ToolRef).default([]),
|
|
4208
7278
|
model: z.string().min(1).optional(),
|
|
4209
7279
|
reasoningEffort: ReasoningEffort.optional(),
|
|
4210
|
-
clientEventId:
|
|
4211
|
-
|
|
4212
|
-
|
|
7280
|
+
clientEventId: SessionOperationKey.optional(),
|
|
7281
|
+
controlEtag: z.string().min(1).optional(),
|
|
7282
|
+
expectedDraftRevision: z.number().int().nonnegative().optional(),
|
|
4213
7283
|
mcpCredentialUpdates: z.array(SessionMcpCredentialUpdateInput).optional(),
|
|
4214
7284
|
});
|
|
4215
7285
|
export type SteerSessionMessageRequest = z.infer<typeof SteerSessionMessageRequest>;
|
|
@@ -4249,6 +7319,37 @@ export const GitHubRepository = z.object({
|
|
|
4249
7319
|
});
|
|
4250
7320
|
export type GitHubRepository = z.infer<typeof GitHubRepository>;
|
|
4251
7321
|
|
|
7322
|
+
export const GitHubRepositoryScope = z.enum(["all", "selected"]);
|
|
7323
|
+
export type GitHubRepositoryScope = z.infer<typeof GitHubRepositoryScope>;
|
|
7324
|
+
|
|
7325
|
+
export const GitHubInstallationBinding = z.object({
|
|
7326
|
+
installationId: z.number().int().positive(),
|
|
7327
|
+
accountLogin: z.string().nullable(),
|
|
7328
|
+
accountType: z.string().nullable(),
|
|
7329
|
+
repositoryScope: GitHubRepositoryScope,
|
|
7330
|
+
repositoryCount: z.number().int().nonnegative(),
|
|
7331
|
+
createdAt: z.string(),
|
|
7332
|
+
updatedAt: z.string(),
|
|
7333
|
+
});
|
|
7334
|
+
export type GitHubInstallationBinding = z.infer<typeof GitHubInstallationBinding>;
|
|
7335
|
+
|
|
7336
|
+
export const GitHubAppInfo = z.object({
|
|
7337
|
+
configured: z.boolean(),
|
|
7338
|
+
appId: z.string().nullable(),
|
|
7339
|
+
clientId: z.string().nullable(),
|
|
7340
|
+
appSlug: z.string().nullable(),
|
|
7341
|
+
installUrl: z.string().nullable(),
|
|
7342
|
+
linkUrl: z.string().nullable(),
|
|
7343
|
+
installations: z.array(GitHubInstallationBinding),
|
|
7344
|
+
missing: z.array(z.string()),
|
|
7345
|
+
});
|
|
7346
|
+
export type GitHubAppInfo = z.infer<typeof GitHubAppInfo>;
|
|
7347
|
+
|
|
7348
|
+
export const GitHubRepositoriesResponse = z.object({
|
|
7349
|
+
repositories: z.array(GitHubRepository),
|
|
7350
|
+
});
|
|
7351
|
+
export type GitHubRepositoriesResponse = z.infer<typeof GitHubRepositoriesResponse>;
|
|
7352
|
+
|
|
4252
7353
|
export const ClientAuthConfig = z.discriminatedUnion("mode", [
|
|
4253
7354
|
z.object({
|
|
4254
7355
|
mode: z.literal("none"),
|
|
@@ -4269,7 +7370,7 @@ export const ClientAuthConfig = z.discriminatedUnion("mode", [
|
|
|
4269
7370
|
]);
|
|
4270
7371
|
export type ClientAuthConfig = z.infer<typeof ClientAuthConfig>;
|
|
4271
7372
|
|
|
4272
|
-
// The negotiated capability handshake document (
|
|
7373
|
+
// The negotiated capability handshake document (sandbox contract C.3). ONE shape;
|
|
4273
7374
|
// collapses the parallel per-module definitions. A capability cell is always
|
|
4274
7375
|
// present with `available`/`transport` + a `reason` when unavailable — never
|
|
4275
7376
|
// absent.
|
|
@@ -4402,14 +7503,14 @@ export const ViewerHolder = z.object({
|
|
|
4402
7503
|
leaseEpoch: z.number().int().nonnegative(),
|
|
4403
7504
|
viewerHeartbeatIntervalMs: z.number().int().positive(),
|
|
4404
7505
|
// The desktop pixel tunnel URL the viewer connects to directly; null until
|
|
4405
|
-
//
|
|
7506
|
+
// a viewer grant is minted (gated until then).
|
|
4406
7507
|
dataPlaneUrl: z.string().nullable(),
|
|
4407
7508
|
});
|
|
4408
7509
|
export type ViewerHolder = z.infer<typeof ViewerHolder>;
|
|
4409
7510
|
|
|
4410
7511
|
// POST .../stream-capabilities/acknowledge — record the calling principal's
|
|
4411
|
-
// acknowledgment of the un-redacted pixel plane
|
|
4412
|
-
//
|
|
7512
|
+
// acknowledgment of the un-redacted pixel plane. Reuses the acknowledgment
|
|
7513
|
+
// machinery — no new endpoint
|
|
4413
7514
|
// shape beyond this body, no new permission beyond stream:acknowledge.
|
|
4414
7515
|
//
|
|
4415
7516
|
// `acknowledgeShared` MUST be true when the box is shared (the group has >1
|
|
@@ -4453,7 +7554,7 @@ export type ViewerHeartbeatResponse = z.infer<typeof ViewerHeartbeatResponse>;
|
|
|
4453
7554
|
// (DeviceAuthStart*, DeviceAuthPoll*, EnrollmentCredentials) so the Rust agent's
|
|
4454
7555
|
// `enroll` command (which runs the flow over HTTP before it has NATS creds)
|
|
4455
7556
|
// decodes the SAME field names (the proto's ts-proto JSON is camelCase). The
|
|
4456
|
-
// request bodies additionally carry the consent-relevant fields the
|
|
7557
|
+
// request bodies additionally carry the consent-relevant fields the design brief
|
|
4457
7558
|
// mandates (the agent ed25519 pubkey + can-offer-display + requests-screen-control).
|
|
4458
7559
|
// =============================================================================
|
|
4459
7560
|
|
|
@@ -4699,7 +7800,7 @@ export const EnrollTokenExchangeResponse = z.object({
|
|
|
4699
7800
|
});
|
|
4700
7801
|
export type EnrollTokenExchangeResponse = z.infer<typeof EnrollTokenExchangeResponse>;
|
|
4701
7802
|
|
|
4702
|
-
// ── Machines dashboard + per-machine metrics (M10
|
|
7803
|
+
// ── Machines dashboard + per-machine metrics (M10) ────────────
|
|
4703
7804
|
//
|
|
4704
7805
|
// The SHARED data contract M10 (backend) implements + M9 (UI) renders. THE
|
|
4705
7806
|
// orchestrator owns this shape; M9 imports these types so the dashboard never
|
|
@@ -4836,6 +7937,239 @@ export const MachineMetricsSeriesResponse = z.object({
|
|
|
4836
7937
|
});
|
|
4837
7938
|
export type MachineMetricsSeriesResponse = z.infer<typeof MachineMetricsSeriesResponse>;
|
|
4838
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
|
+
|
|
4839
8173
|
/**
|
|
4840
8174
|
* A single host-exposed model + the provider that serves it, as surfaced to
|
|
4841
8175
|
* clients (SDK + React composer) by GET /v1/config/client. The wire `api`
|
|
@@ -4843,55 +8177,187 @@ export type MachineMetricsSeriesResponse = z.infer<typeof MachineMetricsSeriesRe
|
|
|
4843
8177
|
* provider id/label drive the picker's grouping. This mirrors the runtime's
|
|
4844
8178
|
* ConfiguredModel (packages/config) projected to the client-safe fields.
|
|
4845
8179
|
*/
|
|
4846
|
-
export const ClientModel =
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
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
|
+
);
|
|
4854
8216
|
export type ClientModel = z.infer<typeof ClientModel>;
|
|
4855
8217
|
|
|
4856
|
-
export const
|
|
4857
|
-
|
|
4858
|
-
// Release-train version of the server (absent on dev/source builds). The
|
|
4859
|
-
// compatibility policy lives in docs/architecture.md — clients within the
|
|
4860
|
-
// same major are supported; evolution is additive within a major.
|
|
4861
|
-
serverVersion: z.string().optional(),
|
|
4862
|
-
defaultModel: z.string(),
|
|
4863
|
-
allowedModels: z.array(z.string()).min(1),
|
|
4864
|
-
// Richer model list (provider-grouped) for the picker. Defaults to [] for
|
|
4865
|
-
// back-compat: callers that only read allowedModels are unaffected.
|
|
4866
|
-
models: z.array(ClientModel).default([]),
|
|
4867
|
-
defaultReasoningEffort: ReasoningEffort,
|
|
4868
|
-
allowedReasoningEfforts: z.array(ReasoningEffort).min(1),
|
|
4869
|
-
mcpServers: z
|
|
4870
|
-
.array(
|
|
4871
|
-
z.object({
|
|
4872
|
-
id: z.string(),
|
|
4873
|
-
name: z.string(),
|
|
4874
|
-
}),
|
|
4875
|
-
)
|
|
4876
|
-
.default([]),
|
|
4877
|
-
fileUploads: z.object({
|
|
4878
|
-
enabled: z.boolean(),
|
|
4879
|
-
maxSizeBytes: z.number().int().positive(),
|
|
4880
|
-
}),
|
|
4881
|
-
productAccessMode: ProductAccessMode,
|
|
4882
|
-
auth: ClientAuthConfig.default({ mode: "none" }),
|
|
4883
|
-
// Server-wide hint: does this deployment support Channel-A structured services
|
|
4884
|
-
// at all (P4.4). Per-session availability is negotiated on /stream-capabilities
|
|
4885
|
-
// (it depends on the session's pinned backend); this is the coarse on/off the
|
|
4886
|
-
// client uses to decide whether to even attempt the fs/git/terminal panels.
|
|
4887
|
-
structuredServices: z
|
|
8218
|
+
export const ModelCredentialReadinessV1 = /* @__PURE__ */ defineModelContractSchema(() =>
|
|
8219
|
+
z
|
|
4888
8220
|
.object({
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
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(),
|
|
4892
8233
|
})
|
|
4893
|
-
.
|
|
4894
|
-
|
|
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
|
+
|
|
8308
|
+
/**
|
|
8309
|
+
* Exact public HTTP protocol revision spoken by this release train.
|
|
8310
|
+
*
|
|
8311
|
+
* This is deliberately independent from a deployment SHA: API and web may roll
|
|
8312
|
+
* at different instants, while incompatible request shapes must never cross
|
|
8313
|
+
* that rollout boundary. Mutating clients send this value in
|
|
8314
|
+
* `x-opengeni-api-contract`; the API rejects any other value before routing.
|
|
8315
|
+
*/
|
|
8316
|
+
export const OPENGENI_API_CONTRACT_REVISION = "2026-07-turn-instructions-v1" as const;
|
|
8317
|
+
export const OPENGENI_API_CONTRACT_HEADER = "x-opengeni-api-contract" as const;
|
|
8318
|
+
|
|
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 }),
|
|
8359
|
+
}),
|
|
8360
|
+
);
|
|
4895
8361
|
export type ClientConfig = z.infer<typeof ClientConfig>;
|
|
4896
8362
|
|
|
4897
8363
|
function base64UrlEncode(value: string): string {
|