@opengeni/contracts 0.10.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3270 -433
- package/dist/index.js +2430 -127
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/event-preview.ts +911 -0
- package/src/index.ts +2673 -187
package/src/index.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
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";
|
|
2
24
|
|
|
3
25
|
export const SessionStatus = z.enum([
|
|
4
26
|
"queued",
|
|
@@ -7,7 +29,6 @@ export const SessionStatus = z.enum([
|
|
|
7
29
|
"requires_action",
|
|
8
30
|
"recovering",
|
|
9
31
|
"waiting_capacity",
|
|
10
|
-
"paused",
|
|
11
32
|
"failed",
|
|
12
33
|
"cancelled",
|
|
13
34
|
]);
|
|
@@ -62,9 +83,16 @@ export type CapabilityDescriptor = {
|
|
|
62
83
|
os: { supported: SandboxOs[]; default: SandboxOs };
|
|
63
84
|
capabilities: {
|
|
64
85
|
FileSystem: { available: boolean; readOnly: boolean };
|
|
65
|
-
Terminal: {
|
|
86
|
+
Terminal: {
|
|
87
|
+
available: boolean;
|
|
88
|
+
transport: "sse-events" | "pty-ws" | null;
|
|
89
|
+
pty: boolean;
|
|
90
|
+
};
|
|
66
91
|
Git: { available: boolean };
|
|
67
|
-
DesktopStream: {
|
|
92
|
+
DesktopStream: {
|
|
93
|
+
available: boolean;
|
|
94
|
+
transport: "vnc-ws" | "rdp-ws" | "webrtc" | null;
|
|
95
|
+
};
|
|
68
96
|
// Feasibility only (== DesktopStream.available && os==linux); NOT a request.
|
|
69
97
|
Recording: { available: boolean };
|
|
70
98
|
};
|
|
@@ -97,7 +125,7 @@ export const DESKTOP_STREAM_PORT = 6080;
|
|
|
97
125
|
// Terminal cell's `url` is the tunnel address resolved against this port.
|
|
98
126
|
export const TERMINAL_STREAM_PORT = 7681;
|
|
99
127
|
|
|
100
|
-
// The
|
|
128
|
+
// The provider capability matrix (sandbox contract PART D + module 03-providers). One row per
|
|
101
129
|
// backend (10 rows). v1 reachable cells are all Linux; macos/windows are seam
|
|
102
130
|
// placeholders (no enum members shipped). Reading rule: a capability cell is
|
|
103
131
|
// `available:false` + a reason in the negotiated doc, never absent.
|
|
@@ -444,7 +472,7 @@ export const Permission = z.enum([
|
|
|
444
472
|
"sessions:create",
|
|
445
473
|
"sessions:read",
|
|
446
474
|
"sessions:control",
|
|
447
|
-
//
|
|
475
|
+
// sandbox workspace (sandbox contract §C.3 / crosscut PART 1.2). stream:view is a
|
|
448
476
|
// REAL, distinct permission — strictly BROADER than sessions:read — because the
|
|
449
477
|
// pixel plane (Channel B) is UN-REDACTED: a viewer of raw pixels can see cloud
|
|
450
478
|
// creds the agent cat's into a terminal, which the redacted Channel-A event log
|
|
@@ -553,11 +581,13 @@ export const Workspace = z.object({
|
|
|
553
581
|
// validated by WorkspaceSettingsSchema; unknown keys are preserved across
|
|
554
582
|
// PATCH merges so newer settings survive an older server.
|
|
555
583
|
settings: z.record(z.string(), z.unknown()),
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
584
|
+
inferenceControl: z.object({
|
|
585
|
+
state: z.enum(["active", "paused"]),
|
|
586
|
+
revision: z.number().int().nonnegative(),
|
|
587
|
+
reason: z.string().nullable(),
|
|
588
|
+
changedBy: z.string().nullable(),
|
|
589
|
+
changedAt: z.string().nullable(),
|
|
590
|
+
}),
|
|
561
591
|
// Workspace default rig used by session/scheduled-task create fallback.
|
|
562
592
|
defaultRigId: z.string().uuid().nullable(),
|
|
563
593
|
createdAt: z.string(),
|
|
@@ -565,12 +595,297 @@ export const Workspace = z.object({
|
|
|
565
595
|
});
|
|
566
596
|
export type Workspace = z.infer<typeof Workspace>;
|
|
567
597
|
|
|
598
|
+
export const WorkspaceTranscriptionTarget = z
|
|
599
|
+
.object({
|
|
600
|
+
provider: z.string().trim().min(1).max(128),
|
|
601
|
+
model: z.string().trim().min(1).max(256).nullable(),
|
|
602
|
+
credentialMode: z.enum(["managed", "byok"]),
|
|
603
|
+
// A workspace-scoped connection reference, never credential material.
|
|
604
|
+
credentialConnectionId: z.string().uuid().nullable(),
|
|
605
|
+
region: z.string().trim().min(1).max(128).nullable(),
|
|
606
|
+
})
|
|
607
|
+
.strict()
|
|
608
|
+
.superRefine((target, context) => {
|
|
609
|
+
if (target.provider === "azure-speech" && target.credentialMode !== "byok") {
|
|
610
|
+
context.addIssue({
|
|
611
|
+
code: "custom",
|
|
612
|
+
path: ["credentialMode"],
|
|
613
|
+
message: "Azure Speech is supported only through workspace BYOK",
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
if (target.credentialMode === "byok" && target.credentialConnectionId === null) {
|
|
617
|
+
context.addIssue({
|
|
618
|
+
code: "custom",
|
|
619
|
+
path: ["credentialConnectionId"],
|
|
620
|
+
message: "BYOK transcription targets require a workspace connection reference",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
if (target.credentialMode === "managed" && target.credentialConnectionId !== null) {
|
|
624
|
+
context.addIssue({
|
|
625
|
+
code: "custom",
|
|
626
|
+
path: ["credentialConnectionId"],
|
|
627
|
+
message: "managed transcription targets cannot name a BYOK connection",
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
export type WorkspaceTranscriptionTarget = z.infer<typeof WorkspaceTranscriptionTarget>;
|
|
632
|
+
|
|
633
|
+
export const TranscriptionErrorCode = z.enum([
|
|
634
|
+
"permission_denied",
|
|
635
|
+
"not_supported",
|
|
636
|
+
"network",
|
|
637
|
+
"provider",
|
|
638
|
+
"policy_blocked",
|
|
639
|
+
"timeout",
|
|
640
|
+
"cancelled",
|
|
641
|
+
"unknown",
|
|
642
|
+
]);
|
|
643
|
+
export type TranscriptionErrorCode = z.infer<typeof TranscriptionErrorCode>;
|
|
644
|
+
|
|
645
|
+
export const TranscriptionTimeSpan = z
|
|
646
|
+
.object({
|
|
647
|
+
startMilliseconds: z.number().finite().nonnegative(),
|
|
648
|
+
endMilliseconds: z.number().finite().nonnegative(),
|
|
649
|
+
})
|
|
650
|
+
.strict()
|
|
651
|
+
.superRefine((span, context) => {
|
|
652
|
+
if (span.endMilliseconds < span.startMilliseconds) {
|
|
653
|
+
context.addIssue({
|
|
654
|
+
code: "custom",
|
|
655
|
+
path: ["endMilliseconds"],
|
|
656
|
+
message: "transcription spans must not end before they start",
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
export type TranscriptionTimeSpan = z.infer<typeof TranscriptionTimeSpan>;
|
|
661
|
+
|
|
662
|
+
export const TranscriptionSpeaker = z
|
|
663
|
+
.object({
|
|
664
|
+
id: z.string().trim().min(1).max(128),
|
|
665
|
+
label: z.string().trim().min(1).max(128).optional(),
|
|
666
|
+
})
|
|
667
|
+
.strict();
|
|
668
|
+
export type TranscriptionSpeaker = z.infer<typeof TranscriptionSpeaker>;
|
|
669
|
+
|
|
670
|
+
export const TranscriptionWord = z
|
|
671
|
+
.object({
|
|
672
|
+
text: z.string().min(1).max(4096),
|
|
673
|
+
span: TranscriptionTimeSpan,
|
|
674
|
+
confidence: z.number().finite().min(0).max(1).optional(),
|
|
675
|
+
speaker: TranscriptionSpeaker.optional(),
|
|
676
|
+
})
|
|
677
|
+
.strict();
|
|
678
|
+
export type TranscriptionWord = z.infer<typeof TranscriptionWord>;
|
|
679
|
+
|
|
680
|
+
export const TranscriptionResultMetadata = z
|
|
681
|
+
.object({
|
|
682
|
+
detectedLanguage: z.string().trim().min(1).max(64).optional(),
|
|
683
|
+
span: TranscriptionTimeSpan.optional(),
|
|
684
|
+
confidence: z.number().finite().min(0).max(1).optional(),
|
|
685
|
+
speaker: TranscriptionSpeaker.optional(),
|
|
686
|
+
words: z.array(TranscriptionWord).max(10_000).optional(),
|
|
687
|
+
})
|
|
688
|
+
.strict()
|
|
689
|
+
.superRefine((metadata, context) => {
|
|
690
|
+
let previousStart = -1;
|
|
691
|
+
for (const [index, word] of (metadata.words ?? []).entries()) {
|
|
692
|
+
if (word.span.startMilliseconds < previousStart) {
|
|
693
|
+
context.addIssue({
|
|
694
|
+
code: "custom",
|
|
695
|
+
path: ["words", index, "span", "startMilliseconds"],
|
|
696
|
+
message: "transcription words must be ordered by start time",
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
previousStart = word.span.startMilliseconds;
|
|
700
|
+
if (
|
|
701
|
+
metadata.span &&
|
|
702
|
+
(word.span.startMilliseconds < metadata.span.startMilliseconds ||
|
|
703
|
+
word.span.endMilliseconds > metadata.span.endMilliseconds)
|
|
704
|
+
) {
|
|
705
|
+
context.addIssue({
|
|
706
|
+
code: "custom",
|
|
707
|
+
path: ["words", index, "span"],
|
|
708
|
+
message: "transcription word spans must fall within the result span",
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
export type TranscriptionResultMetadata = z.infer<typeof TranscriptionResultMetadata>;
|
|
714
|
+
|
|
715
|
+
const TranscriptionEventBase = z
|
|
716
|
+
.object({
|
|
717
|
+
localSessionId: z.string().min(1).max(256),
|
|
718
|
+
sequence: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
719
|
+
occurredAt: z.string().datetime({ offset: true }),
|
|
720
|
+
})
|
|
721
|
+
.strict();
|
|
722
|
+
|
|
723
|
+
/** Strict provider-neutral event surface; provider payload bags are rejected. */
|
|
724
|
+
export const TranscriptionEvent = z.discriminatedUnion("type", [
|
|
725
|
+
TranscriptionEventBase.extend({ type: z.literal("permission.requested") }),
|
|
726
|
+
TranscriptionEventBase.extend({
|
|
727
|
+
type: z.literal("session.opened"),
|
|
728
|
+
providerSessionId: z.string().min(1).max(512),
|
|
729
|
+
}),
|
|
730
|
+
TranscriptionEventBase.extend({
|
|
731
|
+
type: z.literal("transcript.partial"),
|
|
732
|
+
segmentId: z.string().min(1).max(512),
|
|
733
|
+
text: z.string().max(1_000_000),
|
|
734
|
+
metadata: TranscriptionResultMetadata.optional(),
|
|
735
|
+
}),
|
|
736
|
+
TranscriptionEventBase.extend({
|
|
737
|
+
type: z.literal("transcript.final"),
|
|
738
|
+
segmentId: z.string().min(1).max(512),
|
|
739
|
+
text: z.string().max(1_000_000),
|
|
740
|
+
providerAcceptanceId: z.string().min(1).max(512),
|
|
741
|
+
metadata: TranscriptionResultMetadata.optional(),
|
|
742
|
+
}),
|
|
743
|
+
TranscriptionEventBase.extend({
|
|
744
|
+
type: z.literal("usage"),
|
|
745
|
+
audioMilliseconds: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
746
|
+
costUsd: z.number().finite().nonnegative().max(1_000_000_000).nullable(),
|
|
747
|
+
}),
|
|
748
|
+
TranscriptionEventBase.extend({
|
|
749
|
+
type: z.literal("session.reconnecting"),
|
|
750
|
+
attempt: z.number().int().nonnegative().max(10_000),
|
|
751
|
+
reason: z.string().min(1).max(256),
|
|
752
|
+
}),
|
|
753
|
+
TranscriptionEventBase.extend({
|
|
754
|
+
type: z.literal("session.error"),
|
|
755
|
+
code: TranscriptionErrorCode,
|
|
756
|
+
recoverable: z.boolean(),
|
|
757
|
+
}),
|
|
758
|
+
TranscriptionEventBase.extend({
|
|
759
|
+
type: z.literal("session.closed"),
|
|
760
|
+
reason: z.enum(["completed", "cancelled", "error", "replaced"]),
|
|
761
|
+
}),
|
|
762
|
+
]);
|
|
763
|
+
export type TranscriptionEvent = z.infer<typeof TranscriptionEvent>;
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Workspace-only policy for the distinct speech-to-text capability. It never
|
|
767
|
+
* authorizes a turn model/provider and contains connection references rather
|
|
768
|
+
* than secrets. `acceptanceId` changes whenever an admin accepts a new target
|
|
769
|
+
* set, so clients can bind a microphone session to one exact policy revision.
|
|
770
|
+
*/
|
|
771
|
+
export const WorkspaceTranscriptionPolicy = z
|
|
772
|
+
.object({
|
|
773
|
+
enabled: z.boolean(),
|
|
774
|
+
acceptanceId: z.string().uuid().nullable(),
|
|
775
|
+
primary: WorkspaceTranscriptionTarget.nullable(),
|
|
776
|
+
language: z.string().trim().min(1).max(64).nullable(),
|
|
777
|
+
autoDetectLanguage: z.boolean(),
|
|
778
|
+
diarization: z
|
|
779
|
+
.object({
|
|
780
|
+
enabled: z.boolean(),
|
|
781
|
+
maxSpeakers: z.number().int().min(2).max(100).nullable(),
|
|
782
|
+
})
|
|
783
|
+
.strict(),
|
|
784
|
+
retention: z
|
|
785
|
+
.object({
|
|
786
|
+
mode: z.enum(["none", "provider-policy"]),
|
|
787
|
+
maxDays: z.number().int().nonnegative().max(3650).nullable(),
|
|
788
|
+
})
|
|
789
|
+
.strict(),
|
|
790
|
+
privacy: z
|
|
791
|
+
.object({
|
|
792
|
+
allowProviderLogging: z.boolean(),
|
|
793
|
+
allowProviderTraining: z.boolean(),
|
|
794
|
+
})
|
|
795
|
+
.strict(),
|
|
796
|
+
fallback: z
|
|
797
|
+
.object({
|
|
798
|
+
mode: z.enum(["disabled", "explicit"]),
|
|
799
|
+
targets: z.array(WorkspaceTranscriptionTarget).max(8),
|
|
800
|
+
})
|
|
801
|
+
.strict(),
|
|
802
|
+
cost: z
|
|
803
|
+
.object({
|
|
804
|
+
currency: z.literal("USD"),
|
|
805
|
+
maxPerHour: z.number().finite().nonnegative().max(10_000).nullable(),
|
|
806
|
+
maxPerMonth: z.number().finite().nonnegative().max(1_000_000).nullable(),
|
|
807
|
+
})
|
|
808
|
+
.strict(),
|
|
809
|
+
})
|
|
810
|
+
.strict()
|
|
811
|
+
.superRefine((policy, context) => {
|
|
812
|
+
if (policy.enabled && policy.acceptanceId === null) {
|
|
813
|
+
context.addIssue({
|
|
814
|
+
code: "custom",
|
|
815
|
+
path: ["acceptanceId"],
|
|
816
|
+
message: "enabled transcription requires an accepted policy identity",
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
if (policy.enabled && policy.primary === null) {
|
|
820
|
+
context.addIssue({
|
|
821
|
+
code: "custom",
|
|
822
|
+
path: ["primary"],
|
|
823
|
+
message: "enabled transcription requires a primary target",
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if (policy.enabled && !policy.autoDetectLanguage && policy.language === null) {
|
|
827
|
+
context.addIssue({
|
|
828
|
+
code: "custom",
|
|
829
|
+
path: ["language"],
|
|
830
|
+
message: "enabled transcription requires a language or accepted automatic detection",
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
if (policy.autoDetectLanguage && policy.language !== null) {
|
|
834
|
+
context.addIssue({
|
|
835
|
+
code: "custom",
|
|
836
|
+
path: ["language"],
|
|
837
|
+
message: "automatic language detection and a fixed language are mutually exclusive",
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
if (!policy.diarization.enabled && policy.diarization.maxSpeakers !== null) {
|
|
841
|
+
context.addIssue({
|
|
842
|
+
code: "custom",
|
|
843
|
+
path: ["diarization", "maxSpeakers"],
|
|
844
|
+
message: "disabled diarization cannot retain a speaker limit",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
if (policy.fallback.mode === "disabled" && policy.fallback.targets.length > 0) {
|
|
848
|
+
context.addIssue({
|
|
849
|
+
code: "custom",
|
|
850
|
+
path: ["fallback", "targets"],
|
|
851
|
+
message: "disabled fallback cannot retain accepted targets",
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (policy.fallback.mode === "explicit" && policy.fallback.targets.length === 0) {
|
|
855
|
+
context.addIssue({
|
|
856
|
+
code: "custom",
|
|
857
|
+
path: ["fallback", "targets"],
|
|
858
|
+
message: "explicit fallback requires at least one accepted target",
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
const targetKeys = [policy.primary, ...policy.fallback.targets]
|
|
862
|
+
.filter((target): target is WorkspaceTranscriptionTarget => target !== null)
|
|
863
|
+
.map((target) =>
|
|
864
|
+
[
|
|
865
|
+
target.provider,
|
|
866
|
+
target.model ?? "",
|
|
867
|
+
target.credentialMode,
|
|
868
|
+
target.credentialConnectionId ?? "",
|
|
869
|
+
target.region ?? "",
|
|
870
|
+
].join("\u0000"),
|
|
871
|
+
);
|
|
872
|
+
if (new Set(targetKeys).size !== targetKeys.length) {
|
|
873
|
+
context.addIssue({
|
|
874
|
+
code: "custom",
|
|
875
|
+
path: ["fallback", "targets"],
|
|
876
|
+
message: "transcription targets must be unique",
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
export type WorkspaceTranscriptionPolicy = z.infer<typeof WorkspaceTranscriptionPolicy>;
|
|
881
|
+
|
|
568
882
|
// Validates the KNOWN keys of workspaces.settings; passthrough keeps unknown
|
|
569
|
-
// (future) keys rather than stripping them. memoryEnabled
|
|
570
|
-
//
|
|
883
|
+
// (future) keys rather than stripping them. memoryEnabled and transcription are
|
|
884
|
+
// both default-off capabilities.
|
|
571
885
|
export const WorkspaceSettingsSchema = z
|
|
572
886
|
.object({
|
|
573
887
|
memoryEnabled: z.boolean().optional(),
|
|
888
|
+
transcription: WorkspaceTranscriptionPolicy.optional(),
|
|
574
889
|
})
|
|
575
890
|
.passthrough();
|
|
576
891
|
export type WorkspaceSettings = z.infer<typeof WorkspaceSettingsSchema>;
|
|
@@ -581,12 +896,13 @@ export function resolveWorkspaceMemoryEnabled(settings: unknown): boolean {
|
|
|
581
896
|
return parsed.success ? parsed.data.memoryEnabled === true : false;
|
|
582
897
|
}
|
|
583
898
|
|
|
584
|
-
// PATCH body for workspace settings: a partial patch that
|
|
585
|
-
// stored bag.
|
|
586
|
-
// forward-compatible unknown keys
|
|
899
|
+
// PATCH body for workspace settings: a partial top-level patch that merges into
|
|
900
|
+
// the stored bag. Nested transcription policy updates are therefore full
|
|
901
|
+
// replacements; passthrough carries forward-compatible unknown keys.
|
|
587
902
|
export const UpdateWorkspaceSettingsRequest = z
|
|
588
903
|
.object({
|
|
589
904
|
memoryEnabled: z.boolean().optional(),
|
|
905
|
+
transcription: WorkspaceTranscriptionPolicy.optional(),
|
|
590
906
|
})
|
|
591
907
|
.passthrough();
|
|
592
908
|
export type UpdateWorkspaceSettingsRequest = z.infer<typeof UpdateWorkspaceSettingsRequest>;
|
|
@@ -606,6 +922,78 @@ export const UpdateWorkspaceModelPolicyRequest = z.object({
|
|
|
606
922
|
});
|
|
607
923
|
export type UpdateWorkspaceModelPolicyRequest = z.infer<typeof UpdateWorkspaceModelPolicyRequest>;
|
|
608
924
|
|
|
925
|
+
const turnInitiatorIdentityFields = {
|
|
926
|
+
subjectId: z.string().min(1),
|
|
927
|
+
/** Immutable display snapshot; never an authorization input. */
|
|
928
|
+
label: z.string().min(1).optional(),
|
|
929
|
+
} as const;
|
|
930
|
+
|
|
931
|
+
/** Reserved creator/initiator id used only by legacy-row migration defaults. */
|
|
932
|
+
export const UNATTRIBUTED_LEGACY_INITIATOR_SUBJECT_ID = "unattributed-legacy" as const;
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* A named machine/service principal asserted by a trusted embedding host. This
|
|
936
|
+
* deliberately excludes `kind: "subject"`: a delegated service assertion may
|
|
937
|
+
* describe causal machine work, but it is not a generic human-impersonation
|
|
938
|
+
* mechanism. The authenticated grant remains the authorization boundary.
|
|
939
|
+
*/
|
|
940
|
+
export const ServiceTurnInitiator = z.object({
|
|
941
|
+
kind: z.literal("service"),
|
|
942
|
+
subjectId: z
|
|
943
|
+
.string()
|
|
944
|
+
.min(1)
|
|
945
|
+
.max(1024)
|
|
946
|
+
.refine((value) => value !== UNATTRIBUTED_LEGACY_INITIATOR_SUBJECT_ID, {
|
|
947
|
+
message: "unattributed-legacy is reserved for migrated rows",
|
|
948
|
+
}),
|
|
949
|
+
/** Immutable display snapshot; never an authorization input. */
|
|
950
|
+
label: z.string().min(1).max(256).optional(),
|
|
951
|
+
});
|
|
952
|
+
export type ServiceTurnInitiator = z.infer<typeof ServiceTurnInitiator>;
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Immutable, non-secret provenance captured with an initiator. This is audit
|
|
956
|
+
* context (for example an external occurrence id), not a second identity or
|
|
957
|
+
* authorization surface.
|
|
958
|
+
*/
|
|
959
|
+
export const TurnInitiatorContext = z.record(z.string(), z.unknown());
|
|
960
|
+
export type TurnInitiatorContext = z.infer<typeof TurnInitiatorContext>;
|
|
961
|
+
|
|
962
|
+
const reservedServiceTurnInitiatorContextKeys = new Set([
|
|
963
|
+
"backfill",
|
|
964
|
+
"label",
|
|
965
|
+
"provenanceError",
|
|
966
|
+
"via",
|
|
967
|
+
"viaTruncated",
|
|
968
|
+
]);
|
|
969
|
+
|
|
970
|
+
/** Bounded host provenance that cannot forge OpenGeni-owned lineage fields. */
|
|
971
|
+
export const ServiceTurnInitiatorContext = TurnInitiatorContext.superRefine((value, ctx) => {
|
|
972
|
+
for (const key of reservedServiceTurnInitiatorContextKeys) {
|
|
973
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
974
|
+
ctx.addIssue({
|
|
975
|
+
code: z.ZodIssueCode.custom,
|
|
976
|
+
path: [key],
|
|
977
|
+
message: `${key} is reserved OpenGeni initiator context`,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
if (new TextEncoder().encode(JSON.stringify(value)).byteLength > 4096) {
|
|
983
|
+
ctx.addIssue({
|
|
984
|
+
code: z.ZodIssueCode.custom,
|
|
985
|
+
message: "service initiator context exceeds 4096 UTF-8 bytes",
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
} catch {
|
|
989
|
+
ctx.addIssue({
|
|
990
|
+
code: z.ZodIssueCode.custom,
|
|
991
|
+
message: "service initiator context must be JSON-serializable",
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
export type ServiceTurnInitiatorContext = z.infer<typeof ServiceTurnInitiatorContext>;
|
|
996
|
+
|
|
609
997
|
export const AccountGrant = z.object({
|
|
610
998
|
accountId: z.string().uuid(),
|
|
611
999
|
subjectId: z.string().min(1),
|
|
@@ -623,6 +1011,10 @@ export const AccessGrant = z.object({
|
|
|
623
1011
|
subjectLabel: z.string().optional(),
|
|
624
1012
|
permissions: z.array(Permission),
|
|
625
1013
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
1014
|
+
// Optional trusted causal principal for a command submitted by an embedding
|
|
1015
|
+
// host. Authorization still uses subjectId + permissions above.
|
|
1016
|
+
serviceInitiator: ServiceTurnInitiator.optional(),
|
|
1017
|
+
serviceInitiatorContext: ServiceTurnInitiatorContext.optional(),
|
|
626
1018
|
});
|
|
627
1019
|
export type AccessGrant = z.infer<typeof AccessGrant>;
|
|
628
1020
|
|
|
@@ -637,34 +1029,76 @@ export const AccessContext = z.object({
|
|
|
637
1029
|
});
|
|
638
1030
|
export type AccessContext = z.infer<typeof AccessContext>;
|
|
639
1031
|
|
|
640
|
-
export const DelegatedAccessTokenPayload = z
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1032
|
+
export const DelegatedAccessTokenPayload = z
|
|
1033
|
+
.object({
|
|
1034
|
+
accountId: z.string().uuid(),
|
|
1035
|
+
workspaceId: z.string().uuid(),
|
|
1036
|
+
subjectId: z.string().min(1),
|
|
1037
|
+
subjectLabel: z.string().optional(),
|
|
1038
|
+
permissions: z.array(Permission).min(1),
|
|
1039
|
+
// Trusted embedding hosts can sign a causal service principal separately
|
|
1040
|
+
// from the grant subject that authorizes the request. The claim is consumed
|
|
1041
|
+
// only when a command creates a new session/turn.
|
|
1042
|
+
serviceInitiator: ServiceTurnInitiator.optional(),
|
|
1043
|
+
serviceInitiatorContext: ServiceTurnInitiatorContext.optional(),
|
|
1044
|
+
// Worker-asserted session scope for first-party MCP calls (HMAC-signed, not
|
|
1045
|
+
// agent-controlled); enables session-scoped tools such as goal management.
|
|
1046
|
+
sessionId: z.string().uuid().optional(),
|
|
1047
|
+
// The turn making the call (the caller's identity), HMAC-signed by the worker
|
|
1048
|
+
// at turn setup. Lets a tool classify WHO is calling from the token itself,
|
|
1049
|
+
// instead of racily re-reading the session's live active_turn_id — e.g. the
|
|
1050
|
+
// sacred-pause guard must know if the CALLER is a machine child-notification
|
|
1051
|
+
// turn, and the active pointer can flip to another turn mid-check.
|
|
1052
|
+
turnId: z.string().uuid().optional(),
|
|
1053
|
+
// Exact execution owner. Agent control commands are accepted only while this
|
|
1054
|
+
// attempt still owns the signed turn.
|
|
1055
|
+
attemptId: z.string().uuid().optional(),
|
|
1056
|
+
executionGeneration: z.number().int().positive().optional(),
|
|
1057
|
+
exp: z.number().int().positive(),
|
|
1058
|
+
})
|
|
1059
|
+
.superRefine((payload, ctx) => {
|
|
1060
|
+
if (payload.serviceInitiatorContext && !payload.serviceInitiator) {
|
|
1061
|
+
ctx.addIssue({
|
|
1062
|
+
code: z.ZodIssueCode.custom,
|
|
1063
|
+
path: ["serviceInitiatorContext"],
|
|
1064
|
+
message: "serviceInitiatorContext requires serviceInitiator",
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
if (
|
|
1068
|
+
payload.serviceInitiator &&
|
|
1069
|
+
(payload.turnId !== undefined ||
|
|
1070
|
+
payload.attemptId !== undefined ||
|
|
1071
|
+
payload.executionGeneration !== undefined)
|
|
1072
|
+
) {
|
|
1073
|
+
ctx.addIssue({
|
|
1074
|
+
code: z.ZodIssueCode.custom,
|
|
1075
|
+
path: ["serviceInitiator"],
|
|
1076
|
+
message: "serviceInitiator cannot replace an exact agent-attempt initiator",
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
657
1080
|
export type DelegatedAccessTokenPayload = z.infer<typeof DelegatedAccessTokenPayload>;
|
|
658
1081
|
|
|
1082
|
+
const delegatedAccessTokenPrefix = "ogd_";
|
|
1083
|
+
const delegatedServiceAccessTokenPrefix = "ogd2_";
|
|
1084
|
+
|
|
659
1085
|
export async function signDelegatedAccessToken(
|
|
660
1086
|
secret: string,
|
|
661
1087
|
payload: DelegatedAccessTokenPayload,
|
|
662
1088
|
): Promise<string> {
|
|
663
|
-
const
|
|
664
|
-
|
|
1089
|
+
const parsed = DelegatedAccessTokenPayload.parse(payload);
|
|
1090
|
+
const prefix = parsed.serviceInitiator
|
|
1091
|
+
? delegatedServiceAccessTokenPrefix
|
|
1092
|
+
: delegatedAccessTokenPrefix;
|
|
1093
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(parsed));
|
|
1094
|
+
// The service-capable envelope binds its prefix into the signature. An old
|
|
1095
|
+
// verifier accepts only ogd_ and therefore fails closed during a rolling
|
|
1096
|
+
// deploy; changing ogd2_ to ogd_ cannot turn provenance loss into success.
|
|
1097
|
+
const signature = await hmacSha256Base64Url(
|
|
1098
|
+
secret,
|
|
1099
|
+
prefix === delegatedServiceAccessTokenPrefix ? `${prefix}${encodedPayload}` : encodedPayload,
|
|
665
1100
|
);
|
|
666
|
-
|
|
667
|
-
return `ogd_${encodedPayload}.${signature}`;
|
|
1101
|
+
return `${prefix}${encodedPayload}.${signature}`;
|
|
668
1102
|
}
|
|
669
1103
|
|
|
670
1104
|
export async function verifyDelegatedAccessToken(
|
|
@@ -672,30 +1106,48 @@ export async function verifyDelegatedAccessToken(
|
|
|
672
1106
|
token: string,
|
|
673
1107
|
nowSeconds = Math.floor(Date.now() / 1000),
|
|
674
1108
|
): Promise<DelegatedAccessTokenPayload | null> {
|
|
675
|
-
|
|
1109
|
+
const prefix = token.startsWith(delegatedServiceAccessTokenPrefix)
|
|
1110
|
+
? delegatedServiceAccessTokenPrefix
|
|
1111
|
+
: token.startsWith(delegatedAccessTokenPrefix)
|
|
1112
|
+
? delegatedAccessTokenPrefix
|
|
1113
|
+
: null;
|
|
1114
|
+
if (!prefix) {
|
|
676
1115
|
return null;
|
|
677
1116
|
}
|
|
678
|
-
const withoutPrefix = token.slice(
|
|
1117
|
+
const withoutPrefix = token.slice(prefix.length);
|
|
679
1118
|
const dot = withoutPrefix.lastIndexOf(".");
|
|
680
1119
|
if (dot <= 0) {
|
|
681
1120
|
return null;
|
|
682
1121
|
}
|
|
683
1122
|
const encodedPayload = withoutPrefix.slice(0, dot);
|
|
684
1123
|
const signature = withoutPrefix.slice(dot + 1);
|
|
685
|
-
const expected = await hmacSha256Base64Url(
|
|
1124
|
+
const expected = await hmacSha256Base64Url(
|
|
1125
|
+
secret,
|
|
1126
|
+
prefix === delegatedServiceAccessTokenPrefix ? `${prefix}${encodedPayload}` : encodedPayload,
|
|
1127
|
+
);
|
|
686
1128
|
if (!constantTimeEqual(signature, expected)) {
|
|
687
1129
|
return null;
|
|
688
1130
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1131
|
+
let decoded: unknown;
|
|
1132
|
+
try {
|
|
1133
|
+
decoded = JSON.parse(base64UrlDecode(encodedPayload));
|
|
1134
|
+
} catch {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
const payload = DelegatedAccessTokenPayload.safeParse(decoded);
|
|
692
1138
|
if (!payload.success || payload.data.exp < nowSeconds) {
|
|
693
1139
|
return null;
|
|
694
1140
|
}
|
|
1141
|
+
if (
|
|
1142
|
+
(prefix === delegatedServiceAccessTokenPrefix) !==
|
|
1143
|
+
(payload.data.serviceInitiator !== undefined)
|
|
1144
|
+
) {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
695
1147
|
return payload.data;
|
|
696
1148
|
}
|
|
697
1149
|
|
|
698
|
-
// --- Enrollment bearer credential (bring-your-own-compute M5
|
|
1150
|
+
// --- Enrollment bearer credential (bring-your-own-compute M5) ---
|
|
699
1151
|
//
|
|
700
1152
|
// The signed bearer the agent presents to the control plane after enrollment (the
|
|
701
1153
|
// EnrollmentCredentials.bearer the poll returns). REUSES the SAME HMAC envelope as
|
|
@@ -828,7 +1280,7 @@ export async function verifyEnrollToken(
|
|
|
828
1280
|
return payload.data;
|
|
829
1281
|
}
|
|
830
1282
|
|
|
831
|
-
// --- Scoped data-plane stream token (
|
|
1283
|
+
// --- Scoped data-plane stream token (sandbox contract §C.3 / crosscut PART 1.3) ---
|
|
832
1284
|
//
|
|
833
1285
|
// REUSES the existing HMAC envelope (sign/verifyDelegatedAccessToken's
|
|
834
1286
|
// base64Url + hmacSha256Base64Url) — NOT a second crypto — but with a distinct
|
|
@@ -913,7 +1365,7 @@ export async function verifyStreamToken(
|
|
|
913
1365
|
return payload.data;
|
|
914
1366
|
}
|
|
915
1367
|
|
|
916
|
-
// --- Relay PRODUCER token (bring-your-own-compute M8b
|
|
1368
|
+
// --- Relay PRODUCER token (bring-your-own-compute M8b) ---
|
|
917
1369
|
//
|
|
918
1370
|
// The token the AGENT presents to the relay edge when it registers a pty/desktop
|
|
919
1371
|
// stream channel (role=AGENT) — distinct from the viewer's `ogs_` token. It is
|
|
@@ -1147,7 +1599,11 @@ export type Entitlements = z.infer<typeof Entitlements>;
|
|
|
1147
1599
|
|
|
1148
1600
|
export const LimitDecision = z.discriminatedUnion("allowed", [
|
|
1149
1601
|
z.object({ allowed: z.literal(true) }),
|
|
1150
|
-
z.object({
|
|
1602
|
+
z.object({
|
|
1603
|
+
allowed: z.literal(false),
|
|
1604
|
+
code: z.string(),
|
|
1605
|
+
message: z.string(),
|
|
1606
|
+
}),
|
|
1151
1607
|
]);
|
|
1152
1608
|
export type LimitDecision = z.infer<typeof LimitDecision>;
|
|
1153
1609
|
|
|
@@ -1195,10 +1651,21 @@ export type EntitlementsPort = {
|
|
|
1195
1651
|
export const GitCredentialProvider = z.enum(["github", "gitlab", "azure_devops"]);
|
|
1196
1652
|
export type GitCredentialProvider = z.infer<typeof GitCredentialProvider>;
|
|
1197
1653
|
|
|
1654
|
+
// Host-opaque identity for one independently mintable Git credential. It is
|
|
1655
|
+
// deliberately NOT constrained to a filesystem-safe alphabet: runtimes hash it
|
|
1656
|
+
// before using it in paths, command text, or environment variable names.
|
|
1657
|
+
export const GitCredentialBindingId = z.string().min(1).max(256);
|
|
1658
|
+
export type GitCredentialBindingId = z.infer<typeof GitCredentialBindingId>;
|
|
1659
|
+
|
|
1660
|
+
export const GitRepositoryAccess = z.enum(["read", "write"]);
|
|
1661
|
+
export type GitRepositoryAccess = z.infer<typeof GitRepositoryAccess>;
|
|
1662
|
+
|
|
1198
1663
|
const GitProviderRepositoryId = z.union([z.number().int().positive(), z.string().min(1)]);
|
|
1199
1664
|
|
|
1200
1665
|
export const GitCredentialRepositoryRef = z.object({
|
|
1201
1666
|
provider: GitCredentialProvider.optional(),
|
|
1667
|
+
credentialBindingId: GitCredentialBindingId.optional(),
|
|
1668
|
+
access: GitRepositoryAccess.optional(),
|
|
1202
1669
|
uri: z.string().min(1),
|
|
1203
1670
|
ref: z.string().min(1),
|
|
1204
1671
|
repositoryId: GitProviderRepositoryId.optional(),
|
|
@@ -1208,10 +1675,10 @@ export const GitCredentialRepositoryRef = z.object({
|
|
|
1208
1675
|
});
|
|
1209
1676
|
export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryRef>;
|
|
1210
1677
|
|
|
1211
|
-
// ============
|
|
1678
|
+
// ============ connection-credential provider — Connection-credential provider (§7.6) ============
|
|
1212
1679
|
//
|
|
1213
|
-
// The host-providable
|
|
1214
|
-
//
|
|
1680
|
+
// The host-providable credential seam over OpenGeni's run-scoped credential
|
|
1681
|
+
// sites in the worker and API:
|
|
1215
1682
|
// - GIT credentials: run-scoped provider tokens minted in
|
|
1216
1683
|
// `sandboxEnvironmentForRun` (standalone self-mints GitHub App tokens from
|
|
1217
1684
|
// `settings`; embedded hosts can broker GitHub, GitLab, and Azure DevOps)
|
|
@@ -1219,6 +1686,8 @@ export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryR
|
|
|
1219
1686
|
// - SANDBOX secrets: the decrypted variable set values loaded in
|
|
1220
1687
|
// `loadVariableSetForRun` (today decrypted with
|
|
1221
1688
|
// `environmentsEncryptionKeyBytes(settings)`).
|
|
1689
|
+
// - MCP credentials: request-time transport headers for connection-backed
|
|
1690
|
+
// servers, shared by normal model tools and Toolspace/Code Mode.
|
|
1222
1691
|
//
|
|
1223
1692
|
// In embedded/separate topologies the HOST owns these external connections
|
|
1224
1693
|
// (its GitHub App, its secret vault + encryption key). When a host binds this
|
|
@@ -1226,7 +1695,7 @@ export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryR
|
|
|
1226
1695
|
// from `settings`. Unset (standalone default) → byte-for-byte today's
|
|
1227
1696
|
// self-mint.
|
|
1228
1697
|
//
|
|
1229
|
-
//
|
|
1698
|
+
// Workspace-scope cross-check (the host-mapping safety guardrail): a credential
|
|
1230
1699
|
// provider returns the `workspaceId` it scoped the credential to, and the
|
|
1231
1700
|
// activity ASSERTS it agrees with the run's workspace BEFORE injecting
|
|
1232
1701
|
// any git provider token seed (or applying decrypted environment values). A host mapping bug that
|
|
@@ -1236,10 +1705,26 @@ export type GitCredentialRepositoryRef = z.infer<typeof GitCredentialRepositoryR
|
|
|
1236
1705
|
export type GitCredentialsRequest = {
|
|
1237
1706
|
accountId: string;
|
|
1238
1707
|
workspaceId: string;
|
|
1708
|
+
/** Immutable authority admitted with the turn requesting this credential. */
|
|
1709
|
+
sessionId: string;
|
|
1710
|
+
rootSessionId: string;
|
|
1711
|
+
turnId: string;
|
|
1712
|
+
attemptId: string;
|
|
1713
|
+
executionGeneration: number;
|
|
1714
|
+
initiator: TurnInitiator;
|
|
1715
|
+
initiatorContext: TurnInitiatorContext;
|
|
1239
1716
|
// Provider defaults to "github" for the legacy request shape. GitHub-only
|
|
1240
1717
|
// hosts can keep reading installationId/repositoryIds exactly as before;
|
|
1241
1718
|
// provider-aware hosts should branch on this and repositoryRefs.
|
|
1242
1719
|
provider?: GitCredentialProvider;
|
|
1720
|
+
// Present when the host supplied an explicit binding or when more than one
|
|
1721
|
+
// independently mintable credential exists for this provider. A host must
|
|
1722
|
+
// mint only this binding; OpenGeni never treats provider identity as enough
|
|
1723
|
+
// to select among multiple accounts/installations.
|
|
1724
|
+
credentialBindingId?: GitCredentialBindingId;
|
|
1725
|
+
// Canonical lower-case host shared by this binding's repository refs when
|
|
1726
|
+
// there is exactly one. Binding-aware providers echo it when present.
|
|
1727
|
+
providerHost?: string;
|
|
1243
1728
|
// Token requests are the existing behavior. Identity requests let lazy
|
|
1244
1729
|
// sandbox provisioning resolve stable git author/committer identity before
|
|
1245
1730
|
// the box exists while deferring the rotating token value to first provision.
|
|
@@ -1257,9 +1742,16 @@ export type GitCredentials = {
|
|
|
1257
1742
|
// purpose="identity" so hosts can return only stable git identity before lazy
|
|
1258
1743
|
// sandbox provision. The value never enters the manifest.
|
|
1259
1744
|
token?: string;
|
|
1260
|
-
//
|
|
1745
|
+
// workspace-scope cross-check echo: the workspace the provider scoped this token to. The activity
|
|
1261
1746
|
// asserts `workspaceId === request.workspaceId` before injecting.
|
|
1262
1747
|
workspaceId: string;
|
|
1748
|
+
// Strict request echoes for binding-aware requests. OpenGeni validates these
|
|
1749
|
+
// before accepting a token, preventing a host routing bug from returning a
|
|
1750
|
+
// sibling connection's credential. They remain optional for legacy single-
|
|
1751
|
+
// binding/provider hosts.
|
|
1752
|
+
credentialBindingId?: GitCredentialBindingId;
|
|
1753
|
+
provider?: GitCredentialProvider;
|
|
1754
|
+
providerHost?: string;
|
|
1263
1755
|
// Optional provider expiry for host-managed proactive renewal. ISO-8601;
|
|
1264
1756
|
// null/omitted means the host does not expose a deadline and OpenGeni uses
|
|
1265
1757
|
// its conservative bounded refresh cadence instead.
|
|
@@ -1282,7 +1774,7 @@ export type SandboxSecrets = {
|
|
|
1282
1774
|
// `environmentsEncryptionKeyBytes` decrypt. Same shape the self-mint path
|
|
1283
1775
|
// produces (plaintext name→value).
|
|
1284
1776
|
values: Record<string, string>;
|
|
1285
|
-
//
|
|
1777
|
+
// workspace-scope cross-check echo: the workspace the provider scoped these secrets to.
|
|
1286
1778
|
workspaceId: string;
|
|
1287
1779
|
// Optional variableSet metadata; when omitted the activity uses the
|
|
1288
1780
|
// variableSetId as both id and name (the local decrypt carries the row's
|
|
@@ -1292,24 +1784,266 @@ export type SandboxSecrets = {
|
|
|
1292
1784
|
description?: string | null;
|
|
1293
1785
|
};
|
|
1294
1786
|
|
|
1787
|
+
export type CredentialAuthNeededReason =
|
|
1788
|
+
| "missing_connection"
|
|
1789
|
+
| "expired"
|
|
1790
|
+
| "insufficient_scope"
|
|
1791
|
+
| "refresh_failed";
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Host-owned run credentials are materialized below one OpenGeni-owned sandbox
|
|
1795
|
+
* directory. Paths are relative POSIX names; the runtime validates traversal,
|
|
1796
|
+
* collisions, bounds, and modes before any content reaches a sandbox.
|
|
1797
|
+
*/
|
|
1798
|
+
export type RunCredentialFile = {
|
|
1799
|
+
path: string;
|
|
1800
|
+
content: string;
|
|
1801
|
+
mode?: "0400" | "0600";
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
export type RunCredentialAuthNeeded = {
|
|
1805
|
+
reason: CredentialAuthNeededReason;
|
|
1806
|
+
providerDomain?: string;
|
|
1807
|
+
connectionId?: string;
|
|
1808
|
+
scopes?: string[];
|
|
1809
|
+
resource?: string;
|
|
1810
|
+
authorizationUrl?: string;
|
|
1811
|
+
/** Bounded non-secret guidance. Never place credential material here. */
|
|
1812
|
+
message?: string;
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
export type RunCredentialRedaction = {
|
|
1816
|
+
/** Bounded diagnostic label used only in the replacement marker. */
|
|
1817
|
+
name: string;
|
|
1818
|
+
/** One atomic secret value that must be removed from streamed/audit output. */
|
|
1819
|
+
value: string;
|
|
1820
|
+
};
|
|
1821
|
+
|
|
1822
|
+
export type RunCredentialsRequest = {
|
|
1823
|
+
accountId: string;
|
|
1824
|
+
workspaceId: string;
|
|
1825
|
+
sessionId: string;
|
|
1826
|
+
parentSessionId: string | null;
|
|
1827
|
+
rootSessionId: string;
|
|
1828
|
+
/** All sessions sharing this sandbox group share one OS/filesystem trust boundary. */
|
|
1829
|
+
sandboxGroupId: string;
|
|
1830
|
+
turnId: string;
|
|
1831
|
+
attemptId: string;
|
|
1832
|
+
executionGeneration: number;
|
|
1833
|
+
/** Immutable authority admitted with this turn. */
|
|
1834
|
+
initiator: TurnInitiator;
|
|
1835
|
+
initiatorContext: TurnInitiatorContext;
|
|
1836
|
+
effectiveSandboxBackend: SandboxBackend;
|
|
1837
|
+
sandboxOs: SandboxOs;
|
|
1838
|
+
purpose: "provision" | "renewal";
|
|
1839
|
+
forceRefresh: boolean;
|
|
1840
|
+
/** Informational standalone variable-set identity; never gates host resolution. */
|
|
1841
|
+
variableSet: { id: string; name: string } | null;
|
|
1842
|
+
};
|
|
1843
|
+
|
|
1844
|
+
export type RunCredentialsResolution =
|
|
1845
|
+
| {
|
|
1846
|
+
/**
|
|
1847
|
+
* The frozen target/attempt must not receive host material. Hosts use
|
|
1848
|
+
* this for unsupported OSes/backends and policy-based opt-out; the
|
|
1849
|
+
* decision must remain stable for the attempt.
|
|
1850
|
+
*/
|
|
1851
|
+
status: "not_applicable";
|
|
1852
|
+
accountId: string;
|
|
1853
|
+
workspaceId: string;
|
|
1854
|
+
sessionId: string;
|
|
1855
|
+
}
|
|
1856
|
+
| {
|
|
1857
|
+
status: "ok";
|
|
1858
|
+
/** Scope echoes are mandatory and checked before materialization. */
|
|
1859
|
+
accountId: string;
|
|
1860
|
+
workspaceId: string;
|
|
1861
|
+
sessionId: string;
|
|
1862
|
+
/** Secret environment values. Always delivered off-manifest. */
|
|
1863
|
+
environment: Record<string, string>;
|
|
1864
|
+
files?: RunCredentialFile[];
|
|
1865
|
+
/** Environment name to one returned relative file path. */
|
|
1866
|
+
fileEnvironment?: Record<string, string>;
|
|
1867
|
+
/**
|
|
1868
|
+
* Atomic sensitive values embedded inside credential files or derived
|
|
1869
|
+
* material. Environment values are registered automatically; hosts list
|
|
1870
|
+
* additional file-contained values here so chunked output is redacted.
|
|
1871
|
+
*/
|
|
1872
|
+
redactions?: RunCredentialRedaction[];
|
|
1873
|
+
/** Earliest material expiry. Null/omitted uses a bounded refresh cadence. */
|
|
1874
|
+
expiresAt?: string | null;
|
|
1875
|
+
/** Partial degradation: usable material may coexist with reconnect notices. */
|
|
1876
|
+
authNeeded?: RunCredentialAuthNeeded[];
|
|
1877
|
+
}
|
|
1878
|
+
| {
|
|
1879
|
+
status: "auth_needed";
|
|
1880
|
+
accountId: string;
|
|
1881
|
+
workspaceId: string;
|
|
1882
|
+
sessionId: string;
|
|
1883
|
+
authNeeded: RunCredentialAuthNeeded[];
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
export const McpConnectionResourceScope = z
|
|
1887
|
+
.object({
|
|
1888
|
+
/** Provider-stable repository identity, serialized as a string on the wire. */
|
|
1889
|
+
id: z.string().min(1).max(512),
|
|
1890
|
+
kind: z.literal("repository"),
|
|
1891
|
+
})
|
|
1892
|
+
.strict();
|
|
1893
|
+
export type McpConnectionResourceScope = z.infer<typeof McpConnectionResourceScope>;
|
|
1894
|
+
|
|
1895
|
+
const McpConnectionResourceScopes = z
|
|
1896
|
+
.array(McpConnectionResourceScope)
|
|
1897
|
+
.min(1)
|
|
1898
|
+
.max(256)
|
|
1899
|
+
.superRefine((resources, context) => {
|
|
1900
|
+
const seen = new Set<string>();
|
|
1901
|
+
for (const [index, resource] of resources.entries()) {
|
|
1902
|
+
const key = `${resource.kind}\0${resource.id}`;
|
|
1903
|
+
if (seen.has(key)) {
|
|
1904
|
+
context.addIssue({
|
|
1905
|
+
code: "custom",
|
|
1906
|
+
message: "selectedResources must not contain duplicates",
|
|
1907
|
+
path: [index],
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
seen.add(key);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
export const McpServerConnectionRef = z
|
|
1915
|
+
.object({
|
|
1916
|
+
/** Opaque host or standalone connection identifier. */
|
|
1917
|
+
connectionId: z.string().min(1).optional(),
|
|
1918
|
+
/** Stable provider family (for example github, gitlab, or azure_devops). */
|
|
1919
|
+
provider: z.string().min(1).max(128).optional(),
|
|
1920
|
+
/** Provider host or tenant domain. */
|
|
1921
|
+
providerDomain: z.string().min(1),
|
|
1922
|
+
kind: z.enum(["oauth2", "api_key", "app_install", "delegated"]).optional(),
|
|
1923
|
+
scopes: z.array(z.string().min(1)).optional(),
|
|
1924
|
+
/** OAuth resource indicator. This is distinct from selectedResources. */
|
|
1925
|
+
resource: z.string().min(1).optional(),
|
|
1926
|
+
/** Exact provider resources this MCP binding is allowed to operate on. */
|
|
1927
|
+
selectedResources: McpConnectionResourceScopes.optional(),
|
|
1928
|
+
subjectScope: z.enum(["workspace", "subject"]).optional(),
|
|
1929
|
+
})
|
|
1930
|
+
.strict()
|
|
1931
|
+
.superRefine((reference, context) => {
|
|
1932
|
+
if (!reference.selectedResources) return;
|
|
1933
|
+
if (!reference.connectionId) {
|
|
1934
|
+
context.addIssue({
|
|
1935
|
+
code: "custom",
|
|
1936
|
+
message: "selectedResources requires connectionId",
|
|
1937
|
+
path: ["connectionId"],
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
if (!reference.provider) {
|
|
1941
|
+
context.addIssue({
|
|
1942
|
+
code: "custom",
|
|
1943
|
+
message: "selectedResources requires provider",
|
|
1944
|
+
path: ["provider"],
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRef>;
|
|
1949
|
+
|
|
1950
|
+
export type McpCredentialsRequest = {
|
|
1951
|
+
accountId: string;
|
|
1952
|
+
workspaceId: string;
|
|
1953
|
+
/** Immediate session whose model or Toolspace call needs the credential. */
|
|
1954
|
+
sessionId: string;
|
|
1955
|
+
/** Workspace-scoped lineage root for host authorization and binding lookup. */
|
|
1956
|
+
rootSessionId: string;
|
|
1957
|
+
turnId: string;
|
|
1958
|
+
/** Null only while a durable turn exists without a currently executing attempt. */
|
|
1959
|
+
attemptId: string | null;
|
|
1960
|
+
executionGeneration: number;
|
|
1961
|
+
/** The immutable authority that admitted this turn. Never substitute the sandbox caller. */
|
|
1962
|
+
initiator: TurnInitiator;
|
|
1963
|
+
initiatorContext: TurnInitiatorContext;
|
|
1964
|
+
/** Immediate technical caller, retained only as non-authoritative audit context. */
|
|
1965
|
+
callerSubjectId?: string;
|
|
1966
|
+
surface: "model" | "toolspace";
|
|
1967
|
+
serverId: string;
|
|
1968
|
+
toolName?: string;
|
|
1969
|
+
connectionRef: McpServerConnectionRef;
|
|
1970
|
+
forceRefresh: boolean;
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
export type McpCredentialAuthNeededReason =
|
|
1974
|
+
| CredentialAuthNeededReason
|
|
1975
|
+
| "unsupported_auth"
|
|
1976
|
+
| "resource_scope_unavailable";
|
|
1977
|
+
|
|
1978
|
+
export type McpCredentialResolution =
|
|
1979
|
+
| {
|
|
1980
|
+
status: "ok";
|
|
1981
|
+
/** Scope echoes are mandatory and verified before any header is used. */
|
|
1982
|
+
accountId: string;
|
|
1983
|
+
workspaceId: string;
|
|
1984
|
+
sessionId: string;
|
|
1985
|
+
headers: Record<string, string>;
|
|
1986
|
+
connectionId: string;
|
|
1987
|
+
providerDomain: string;
|
|
1988
|
+
provider?: string;
|
|
1989
|
+
scopes?: string[];
|
|
1990
|
+
resource?: string;
|
|
1991
|
+
selectedResources?: McpConnectionResourceScope[];
|
|
1992
|
+
expiresAt?: string | null;
|
|
1993
|
+
}
|
|
1994
|
+
| {
|
|
1995
|
+
status: "auth_needed";
|
|
1996
|
+
/** Scope echoes are mandatory even when the credential cannot be resolved. */
|
|
1997
|
+
accountId: string;
|
|
1998
|
+
workspaceId: string;
|
|
1999
|
+
sessionId: string;
|
|
2000
|
+
reason: McpCredentialAuthNeededReason;
|
|
2001
|
+
providerDomain: string;
|
|
2002
|
+
provider?: string;
|
|
2003
|
+
connectionId?: string;
|
|
2004
|
+
scopes?: string[];
|
|
2005
|
+
resource?: string;
|
|
2006
|
+
selectedResources?: McpConnectionResourceScope[];
|
|
2007
|
+
authorizationUrl?: string;
|
|
2008
|
+
};
|
|
2009
|
+
|
|
1295
2010
|
export type ConnectionCredentialsPort = {
|
|
1296
|
-
//
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
2011
|
+
// Every leg is optional: a host may drive only the credential classes it
|
|
2012
|
+
// owns. An unset leg falls through to today's standalone implementation for
|
|
2013
|
+
// that leg only.
|
|
1299
2014
|
gitCredentials?(input: GitCredentialsRequest): Promise<GitCredentials>;
|
|
1300
2015
|
sandboxSecrets?(input: SandboxSecretsRequest): Promise<SandboxSecrets>;
|
|
2016
|
+
/**
|
|
2017
|
+
* Resolve host-owned, session-aware sandbox credentials independently of an
|
|
2018
|
+
* OpenGeni variable set. OpenGeni transports and renews the material; the host
|
|
2019
|
+
* remains the sole owner of connection selection and credential policy.
|
|
2020
|
+
*/
|
|
2021
|
+
runCredentials?(input: RunCredentialsRequest): Promise<RunCredentialsResolution>;
|
|
2022
|
+
/**
|
|
2023
|
+
* Resolve rotating MCP transport credentials at request time. Embedded hosts
|
|
2024
|
+
* use this to keep their provider connection as the sole credential source;
|
|
2025
|
+
* OpenGeni never requires a duplicate connection record. The same resolver is
|
|
2026
|
+
* used by model-visible MCP tools and the additive Toolspace/Code Mode proxy.
|
|
2027
|
+
*/
|
|
2028
|
+
mcpCredentials?(input: McpCredentialsRequest): Promise<McpCredentialResolution>;
|
|
1301
2029
|
};
|
|
1302
2030
|
|
|
1303
|
-
// ============
|
|
2031
|
+
// ============ connection-credential provider — GitHub App API port (BYO-App, §7.6 / GitHub credential prototype remainder) ===
|
|
1304
2032
|
//
|
|
1305
|
-
// The host-driven GitHub-API credential leg.
|
|
1306
|
-
// gate (storage) axis; this closes the credential leg by making the
|
|
2033
|
+
// The host-driven GitHub-API credential leg. GitHub credential prototype closed the establishment +
|
|
2034
|
+
// gate (storage) axis; this closes the credential leg by making the live
|
|
1307
2035
|
// GitHub-API calls host-PROVIDABLE so a BYO-GitHub-App host drives its OWN App
|
|
1308
2036
|
// credentials (its own JWT-signing key, its own OAuth client) instead of
|
|
1309
2037
|
// OpenGeni self-minting from `settings`:
|
|
1310
|
-
// -
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
2038
|
+
// - authorizeUser: OAuth code exchange + user-visible installation and
|
|
2039
|
+
// repository permission discovery. Retained for provider ABI compatibility;
|
|
2040
|
+
// visibility is not proof of installation authority and core does not use
|
|
2041
|
+
// this method for new workspace binding.
|
|
2042
|
+
// - verifyInstallationAccessForUser: OAuth code→token + installation lookup,
|
|
2043
|
+
// also retained for provider ABI compatibility and not used for binding.
|
|
2044
|
+
// - getInstallation: retained in the provider ABI for compatibility. Direct
|
|
2045
|
+
// existing-installation selection is fail-closed and core does not call
|
|
2046
|
+
// this method for binding.
|
|
1313
2047
|
// - listRepositories: the installation-scoped repo listing behind
|
|
1314
2048
|
// `GET /v1/workspaces/:id/github/repositories` (today
|
|
1315
2049
|
// `listGitHubAppRepositories(settings, …)`).
|
|
@@ -1324,11 +2058,31 @@ export type GitHubInstallationSummary = {
|
|
|
1324
2058
|
suspended: boolean;
|
|
1325
2059
|
};
|
|
1326
2060
|
|
|
2061
|
+
export type GitHubRepositoryPermissions = {
|
|
2062
|
+
admin: boolean;
|
|
2063
|
+
maintain: boolean;
|
|
2064
|
+
push: boolean;
|
|
2065
|
+
triage: boolean;
|
|
2066
|
+
pull: boolean;
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
export type GitHubUserRepositoryAccess = GitHubRepository & {
|
|
2070
|
+
permissions: GitHubRepositoryPermissions;
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
export type GitHubUserInstallationAccess = GitHubInstallationSummary & {
|
|
2074
|
+
repositories: GitHubUserRepositoryAccess[];
|
|
2075
|
+
};
|
|
2076
|
+
|
|
1327
2077
|
export type GitHubAppApiPort = {
|
|
2078
|
+
authorizeUser?: (input: { code: string }) => Promise<GitHubUserInstallationAccess[]>;
|
|
1328
2079
|
verifyInstallationAccessForUser?: (input: {
|
|
1329
2080
|
code: string;
|
|
1330
2081
|
installationId: number;
|
|
1331
2082
|
}) => Promise<GitHubInstallationSummary>;
|
|
2083
|
+
getInstallation?: (input: {
|
|
2084
|
+
installationId: number;
|
|
2085
|
+
}) => Promise<GitHubInstallationSummary | null>;
|
|
1332
2086
|
listRepositories?: (input: { installationIds?: number[] }) => Promise<GitHubRepository[]>;
|
|
1333
2087
|
};
|
|
1334
2088
|
|
|
@@ -1368,6 +2122,8 @@ export const RepositoryResourceRef = z.object({
|
|
|
1368
2122
|
mountPath: z.string().min(1).optional(),
|
|
1369
2123
|
subpath: z.string().min(1).optional(),
|
|
1370
2124
|
provider: GitCredentialProvider.optional(),
|
|
2125
|
+
credentialBindingId: GitCredentialBindingId.optional(),
|
|
2126
|
+
access: GitRepositoryAccess.optional(),
|
|
1371
2127
|
repositoryId: GitProviderRepositoryId.optional(),
|
|
1372
2128
|
installationId: GitProviderRepositoryId.optional(),
|
|
1373
2129
|
projectId: GitProviderRepositoryId.optional(),
|
|
@@ -1377,6 +2133,53 @@ export const RepositoryResourceRef = z.object({
|
|
|
1377
2133
|
});
|
|
1378
2134
|
export type RepositoryResourceRef = z.infer<typeof RepositoryResourceRef>;
|
|
1379
2135
|
|
|
2136
|
+
function positiveGitProviderInteger(value: unknown): number | null {
|
|
2137
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
2138
|
+
if (typeof value === "string" && /^\d+$/.test(value) && Number(value) > 0) {
|
|
2139
|
+
return Number(value);
|
|
2140
|
+
}
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* Resolve whether a repository participates in platform-brokered Git auth.
|
|
2146
|
+
* Provider-less public repositories return null; legacy GitHub aliases infer
|
|
2147
|
+
* GitHub only when both positive installation and repository ids are present.
|
|
2148
|
+
*/
|
|
2149
|
+
export function gitCredentialProviderForRepository(
|
|
2150
|
+
resource: RepositoryResourceRef,
|
|
2151
|
+
): GitCredentialProvider | null {
|
|
2152
|
+
if (resource.provider) return resource.provider;
|
|
2153
|
+
if (
|
|
2154
|
+
positiveGitProviderInteger(resource.githubInstallationId) &&
|
|
2155
|
+
positiveGitProviderInteger(resource.githubRepositoryId)
|
|
2156
|
+
) {
|
|
2157
|
+
return "github";
|
|
2158
|
+
}
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* Derive the one canonical runtime/broker identity for a repository credential.
|
|
2164
|
+
* Every consumer must use this helper so mint grouping, token filenames, and
|
|
2165
|
+
* credential-helper routing cannot diverge on legacy provider ids.
|
|
2166
|
+
*/
|
|
2167
|
+
export function gitCredentialBindingIdForRepository(
|
|
2168
|
+
resource: RepositoryResourceRef,
|
|
2169
|
+
provider: GitCredentialProvider | null = gitCredentialProviderForRepository(resource),
|
|
2170
|
+
): GitCredentialBindingId | null {
|
|
2171
|
+
if (!provider) return null;
|
|
2172
|
+
const installationId =
|
|
2173
|
+
provider === "github"
|
|
2174
|
+
? positiveGitProviderInteger(resource.githubInstallationId ?? resource.installationId)
|
|
2175
|
+
: null;
|
|
2176
|
+
return (
|
|
2177
|
+
resource.credentialBindingId ??
|
|
2178
|
+
resource.connectionId ??
|
|
2179
|
+
(installationId ? `github-installation:${installationId}` : provider)
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
|
|
1380
2183
|
export const FileResourceRef = z.object({
|
|
1381
2184
|
kind: z.literal("file"),
|
|
1382
2185
|
fileId: z.string().uuid(),
|
|
@@ -1387,6 +2190,109 @@ export type FileResourceRef = z.infer<typeof FileResourceRef>;
|
|
|
1387
2190
|
export const ResourceRef = z.discriminatedUnion("kind", [RepositoryResourceRef, FileResourceRef]);
|
|
1388
2191
|
export type ResourceRef = z.infer<typeof ResourceRef>;
|
|
1389
2192
|
|
|
2193
|
+
export class ResourceMountPathError extends Error {
|
|
2194
|
+
constructor(message: string) {
|
|
2195
|
+
super(message);
|
|
2196
|
+
this.name = "ResourceMountPathError";
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/**
|
|
2201
|
+
* Normalize one workspace-relative resource mount path for every runtime.
|
|
2202
|
+
*
|
|
2203
|
+
* Backslashes are treated as separators so a path cannot be harmless on Linux
|
|
2204
|
+
* but become traversal on a connected Windows machine. Empty, absolute,
|
|
2205
|
+
* drive-qualified, dot-segment, NUL-containing, and repeated-separator paths
|
|
2206
|
+
* fail closed instead of being silently reinterpreted.
|
|
2207
|
+
*/
|
|
2208
|
+
export function normalizeResourceMountPath(path: string): string {
|
|
2209
|
+
const normalizedSeparators = path.trim().replace(/\\/g, "/");
|
|
2210
|
+
if (
|
|
2211
|
+
!normalizedSeparators ||
|
|
2212
|
+
normalizedSeparators.startsWith("/") ||
|
|
2213
|
+
/^[A-Za-z]:\//.test(normalizedSeparators) ||
|
|
2214
|
+
normalizedSeparators.includes("\0")
|
|
2215
|
+
) {
|
|
2216
|
+
throw new ResourceMountPathError(`invalid resource mount path: ${path}`);
|
|
2217
|
+
}
|
|
2218
|
+
const segments = normalizedSeparators.split("/");
|
|
2219
|
+
if (
|
|
2220
|
+
segments.some(
|
|
2221
|
+
(segment) =>
|
|
2222
|
+
!segment ||
|
|
2223
|
+
segment === "." ||
|
|
2224
|
+
segment === ".." ||
|
|
2225
|
+
/[<>:"|?*\u0000-\u001f]/.test(segment) ||
|
|
2226
|
+
/[ .]$/.test(segment) ||
|
|
2227
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment),
|
|
2228
|
+
)
|
|
2229
|
+
) {
|
|
2230
|
+
throw new ResourceMountPathError(`invalid resource mount path: ${path}`);
|
|
2231
|
+
}
|
|
2232
|
+
return segments.join("/");
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/** Normalize a repository-internal subpath while preserving legacy `/path/` input. */
|
|
2236
|
+
export function normalizeRepositorySubpath(path: string): string {
|
|
2237
|
+
const relative = path
|
|
2238
|
+
.trim()
|
|
2239
|
+
.replace(/\\/g, "/")
|
|
2240
|
+
.replace(/^\/+|\/+$/g, "");
|
|
2241
|
+
return normalizeResourceMountPath(relative);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
/** A conservative collision identity that is portable to case-insensitive hosts. */
|
|
2245
|
+
export function resourceMountPathCollisionKey(path: string): string {
|
|
2246
|
+
return normalizeResourceMountPath(path).normalize("NFKC").toLowerCase();
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* Default repository mount identity. The normalized remote host (including a
|
|
2251
|
+
* non-default port) is part of the path, so equal owner/repo names on GitHub,
|
|
2252
|
+
* GitLab, Azure DevOps, or a custom host do not collide. Encoding the host keeps
|
|
2253
|
+
* IPv6/custom-port identities inside one portable path segment.
|
|
2254
|
+
*/
|
|
2255
|
+
export function defaultRepositoryMountPath(uri: string): string {
|
|
2256
|
+
let url: URL;
|
|
2257
|
+
try {
|
|
2258
|
+
url = new URL(uri);
|
|
2259
|
+
} catch {
|
|
2260
|
+
throw new ResourceMountPathError(`invalid repository URI for mount path: ${uri}`);
|
|
2261
|
+
}
|
|
2262
|
+
if (url.protocol !== "https:" || !url.host) {
|
|
2263
|
+
throw new ResourceMountPathError(`invalid repository URI for mount path: ${uri}`);
|
|
2264
|
+
}
|
|
2265
|
+
const repositoryPath = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
2266
|
+
const segments = repositoryPath.split("/").filter(Boolean);
|
|
2267
|
+
if (segments.length < 2) {
|
|
2268
|
+
throw new ResourceMountPathError(`repository URI must include owner and repo: ${uri}`);
|
|
2269
|
+
}
|
|
2270
|
+
return normalizeResourceMountPath(
|
|
2271
|
+
`repos/${encodeURIComponent(url.host.toLowerCase())}/${segments.join("/")}`,
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
/** Resolve the exact mount used by API normalization, manifests, and clone hooks. */
|
|
2276
|
+
export function resourceMountPath(resource: ResourceRef): string {
|
|
2277
|
+
if (resource.mountPath) return normalizeResourceMountPath(resource.mountPath);
|
|
2278
|
+
return resource.kind === "file"
|
|
2279
|
+
? normalizeResourceMountPath(`files/${resource.fileId}`)
|
|
2280
|
+
: defaultRepositoryMountPath(resource.uri);
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/** Fail before sandbox execution when two resources share a portable path. */
|
|
2284
|
+
export function assertUniqueResourceMountPaths(resources: readonly ResourceRef[]): void {
|
|
2285
|
+
const mounted = new Set<string>();
|
|
2286
|
+
for (const resource of resources) {
|
|
2287
|
+
const path = resourceMountPath(resource);
|
|
2288
|
+
const key = resourceMountPathCollisionKey(path);
|
|
2289
|
+
if (mounted.has(key)) {
|
|
2290
|
+
throw new ResourceRefConflictError(`resource mount path is already attached: ${path}`);
|
|
2291
|
+
}
|
|
2292
|
+
mounted.add(key);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
1390
2296
|
export const FileStatus = z.enum(["pending_upload", "ready", "failed", "expired", "deleted"]);
|
|
1391
2297
|
export type FileStatus = z.infer<typeof FileStatus>;
|
|
1392
2298
|
|
|
@@ -1728,6 +2634,9 @@ export const SessionMcpServerInput = z.object({
|
|
|
1728
2634
|
// Write-only credential headers. Values are encrypted at rest and never
|
|
1729
2635
|
// returned in session responses or events; response metadata exposes names.
|
|
1730
2636
|
headers: z.record(z.string(), z.string()).optional(),
|
|
2637
|
+
// Non-secret opaque pointer resolved at request time by the standalone
|
|
2638
|
+
// connection broker or an embedding host's mcpCredentials port.
|
|
2639
|
+
connectionRef: McpServerConnectionRef.optional(),
|
|
1731
2640
|
});
|
|
1732
2641
|
export type SessionMcpServerInput = z.infer<typeof SessionMcpServerInput>;
|
|
1733
2642
|
|
|
@@ -1744,6 +2653,7 @@ export const SessionMcpServerMetadata = z
|
|
|
1744
2653
|
url: httpsUrl,
|
|
1745
2654
|
headerNames: z.array(z.string()).default([]),
|
|
1746
2655
|
credentialVersion: z.number().int().positive(),
|
|
2656
|
+
connectionRef: McpServerConnectionRef.nullable().default(null),
|
|
1747
2657
|
})
|
|
1748
2658
|
.strict();
|
|
1749
2659
|
export type SessionMcpServerMetadata = z.infer<typeof SessionMcpServerMetadata>;
|
|
@@ -1783,10 +2693,14 @@ export function mergeResourceRefs(
|
|
|
1783
2693
|
additions: ResourceRef[],
|
|
1784
2694
|
options: { rejectConflicts?: boolean } = {},
|
|
1785
2695
|
): ResourceRef[] {
|
|
2696
|
+
if (options.rejectConflicts) {
|
|
2697
|
+
assertUniqueResourceMountPaths(existing);
|
|
2698
|
+
}
|
|
1786
2699
|
const out = [...existing];
|
|
1787
2700
|
const mountPaths = new Map(
|
|
1788
|
-
existing.
|
|
1789
|
-
|
|
2701
|
+
existing.map(
|
|
2702
|
+
(resource) =>
|
|
2703
|
+
[resourceMountPathCollisionKey(resourceMountPath(resource)), stableJson(resource)] as const,
|
|
1790
2704
|
),
|
|
1791
2705
|
);
|
|
1792
2706
|
const identities = new Map(
|
|
@@ -1800,11 +2714,10 @@ export function mergeResourceRefs(
|
|
|
1800
2714
|
continue;
|
|
1801
2715
|
}
|
|
1802
2716
|
if (options.rejectConflicts) {
|
|
1803
|
-
const
|
|
2717
|
+
const mountPath = resourceMountPath(resource);
|
|
2718
|
+
const existingAtMount = mountPaths.get(resourceMountPathCollisionKey(mountPath));
|
|
1804
2719
|
if (existingAtMount && existingAtMount !== serialized) {
|
|
1805
|
-
throw new ResourceRefConflictError(
|
|
1806
|
-
`resource mount path is already attached: ${resource.mountPath}`,
|
|
1807
|
-
);
|
|
2720
|
+
throw new ResourceRefConflictError(`resource mount path is already attached: ${mountPath}`);
|
|
1808
2721
|
}
|
|
1809
2722
|
const identity = resourceIdentityKey(resource);
|
|
1810
2723
|
const existingIdentity = identities.get(identity);
|
|
@@ -1817,9 +2730,7 @@ export function mergeResourceRefs(
|
|
|
1817
2730
|
out.push(resource);
|
|
1818
2731
|
exact.add(serialized);
|
|
1819
2732
|
identities.set(resourceIdentityKey(resource), serialized);
|
|
1820
|
-
|
|
1821
|
-
mountPaths.set(resource.mountPath, serialized);
|
|
1822
|
-
}
|
|
2733
|
+
mountPaths.set(resourceMountPathCollisionKey(resourceMountPath(resource)), serialized);
|
|
1823
2734
|
}
|
|
1824
2735
|
return out;
|
|
1825
2736
|
}
|
|
@@ -1874,6 +2785,7 @@ export const SessionTurnStatus = z.enum([
|
|
|
1874
2785
|
"failed",
|
|
1875
2786
|
"cancelled",
|
|
1876
2787
|
"superseded",
|
|
2788
|
+
"withdrawn_for_edit",
|
|
1877
2789
|
]);
|
|
1878
2790
|
export type SessionTurnStatus = z.infer<typeof SessionTurnStatus>;
|
|
1879
2791
|
|
|
@@ -1987,7 +2899,9 @@ export type ClearSessionContextRequest = z.infer<typeof ClearSessionContextReque
|
|
|
1987
2899
|
export const CLEARED_RUN_STATE_MARKER = "$opengeniCleared" as const;
|
|
1988
2900
|
|
|
1989
2901
|
/** The canonical sentinel serializedRunState value a context clear stores. */
|
|
1990
|
-
export const CLEARED_RUN_STATE_BLOB = JSON.stringify({
|
|
2902
|
+
export const CLEARED_RUN_STATE_BLOB = JSON.stringify({
|
|
2903
|
+
[CLEARED_RUN_STATE_MARKER]: true,
|
|
2904
|
+
});
|
|
1991
2905
|
|
|
1992
2906
|
/**
|
|
1993
2907
|
* True when a serialized run-state blob is the cleared sentinel rather than a
|
|
@@ -2027,6 +2941,168 @@ export const CompactSessionContextResult = z.object({
|
|
|
2027
2941
|
});
|
|
2028
2942
|
export type CompactSessionContextResult = z.infer<typeof CompactSessionContextResult>;
|
|
2029
2943
|
|
|
2944
|
+
/**
|
|
2945
|
+
* The principal whose authority accepted a session or turn. `subjectId` is an
|
|
2946
|
+
* opaque host/standalone identity and therefore must never encode `kind` by
|
|
2947
|
+
* convention: embedding hosts own their subject namespace.
|
|
2948
|
+
*/
|
|
2949
|
+
export const TurnInitiator = z.object({
|
|
2950
|
+
kind: z.enum(["subject", "service"]),
|
|
2951
|
+
...turnInitiatorIdentityFields,
|
|
2952
|
+
});
|
|
2953
|
+
export type TurnInitiator = z.infer<typeof TurnInitiator>;
|
|
2954
|
+
|
|
2955
|
+
// ============ embedding host session authorization ============
|
|
2956
|
+
//
|
|
2957
|
+
// Workspace permissions answer whether a principal may use an OpenGeni
|
|
2958
|
+
// capability. An embedding host can additionally own per-session visibility
|
|
2959
|
+
// (ownership, sharing, nested workspaces, revocation). This port is the one
|
|
2960
|
+
// host-neutral boundary for that second decision. Inputs contain OpenGeni ids
|
|
2961
|
+
// and immutable, non-secret authority only; host records and policy details
|
|
2962
|
+
// never cross the boundary.
|
|
2963
|
+
|
|
2964
|
+
export const SessionAuthorizationSurface = z.enum([
|
|
2965
|
+
"http",
|
|
2966
|
+
"core",
|
|
2967
|
+
"stream",
|
|
2968
|
+
"first_party_mcp",
|
|
2969
|
+
"toolspace",
|
|
2970
|
+
]);
|
|
2971
|
+
export type SessionAuthorizationSurface = z.infer<typeof SessionAuthorizationSurface>;
|
|
2972
|
+
|
|
2973
|
+
export const SessionAuthorizationOperation = z.enum([
|
|
2974
|
+
"session.read",
|
|
2975
|
+
"session.events.read",
|
|
2976
|
+
"session.stream.read",
|
|
2977
|
+
"session.stream.acknowledge",
|
|
2978
|
+
"session.turns.read",
|
|
2979
|
+
"session.append",
|
|
2980
|
+
"session.steer",
|
|
2981
|
+
"session.control",
|
|
2982
|
+
"session.queue.read",
|
|
2983
|
+
"session.queue.control",
|
|
2984
|
+
"session.composer.read",
|
|
2985
|
+
"session.composer.write",
|
|
2986
|
+
"session.lineage.read",
|
|
2987
|
+
"session.capture.read",
|
|
2988
|
+
"session.files.read",
|
|
2989
|
+
"session.files.write",
|
|
2990
|
+
"session.git.read",
|
|
2991
|
+
"session.terminal.read",
|
|
2992
|
+
"session.terminal.control",
|
|
2993
|
+
"session.viewer.read",
|
|
2994
|
+
"session.viewer.control",
|
|
2995
|
+
"session.first_party_mcp.call",
|
|
2996
|
+
"session.toolspace.call",
|
|
2997
|
+
"session.pin.write",
|
|
2998
|
+
"session.codex_account.write",
|
|
2999
|
+
"session.context.write",
|
|
3000
|
+
"session.approval.write",
|
|
3001
|
+
"session.human_input.read",
|
|
3002
|
+
"session.human_input.write",
|
|
3003
|
+
"session.title.write",
|
|
3004
|
+
"session.goal.read",
|
|
3005
|
+
"session.goal.write",
|
|
3006
|
+
"session.child.create",
|
|
3007
|
+
]);
|
|
3008
|
+
export type SessionAuthorizationOperation = z.infer<typeof SessionAuthorizationOperation>;
|
|
3009
|
+
|
|
3010
|
+
export const SessionAuthorizationActor = z.discriminatedUnion("kind", [
|
|
3011
|
+
z.object({
|
|
3012
|
+
kind: z.literal("subject"),
|
|
3013
|
+
subjectId: z.string().min(1),
|
|
3014
|
+
subjectLabel: z.string().min(1).optional(),
|
|
3015
|
+
}),
|
|
3016
|
+
z.object({
|
|
3017
|
+
kind: z.literal("agent_attempt"),
|
|
3018
|
+
/** Technical, authenticated first-party caller (not the host authority). */
|
|
3019
|
+
subjectId: z.string().min(1),
|
|
3020
|
+
callerSessionId: z.string().uuid(),
|
|
3021
|
+
callerRootSessionId: z.string().uuid(),
|
|
3022
|
+
turnId: z.string().uuid(),
|
|
3023
|
+
attemptId: z.string().uuid(),
|
|
3024
|
+
executionGeneration: z.number().int().positive(),
|
|
3025
|
+
/** Frozen authority that admitted the calling turn. */
|
|
3026
|
+
initiator: TurnInitiator,
|
|
3027
|
+
initiatorContext: TurnInitiatorContext,
|
|
3028
|
+
}),
|
|
3029
|
+
]);
|
|
3030
|
+
export type SessionAuthorizationActor = z.infer<typeof SessionAuthorizationActor>;
|
|
3031
|
+
|
|
3032
|
+
export const SessionAuthorizationTarget = z.object({
|
|
3033
|
+
sessionId: z.string().uuid(),
|
|
3034
|
+
/** Server-resolved workspace lineage root; never accepted from a caller. */
|
|
3035
|
+
rootSessionId: z.string().uuid(),
|
|
3036
|
+
});
|
|
3037
|
+
export type SessionAuthorizationTarget = z.infer<typeof SessionAuthorizationTarget>;
|
|
3038
|
+
|
|
3039
|
+
export type AuthorizeSessionInput = {
|
|
3040
|
+
accountId: string;
|
|
3041
|
+
workspaceId: string;
|
|
3042
|
+
actor: SessionAuthorizationActor;
|
|
3043
|
+
target: SessionAuthorizationTarget;
|
|
3044
|
+
operation: SessionAuthorizationOperation;
|
|
3045
|
+
surface: SessionAuthorizationSurface;
|
|
3046
|
+
};
|
|
3047
|
+
|
|
3048
|
+
export const SessionAuthorizationDecision = z.discriminatedUnion("allowed", [
|
|
3049
|
+
z.object({
|
|
3050
|
+
allowed: z.literal(true),
|
|
3051
|
+
/**
|
|
3052
|
+
* Whether related-session metadata may be projected with the target.
|
|
3053
|
+
* `target` is the fail-closed default for exact shares; `root` permits the
|
|
3054
|
+
* target's full lineage tree. This does not authorize a separate operation
|
|
3055
|
+
* against another session, which always requires its own decision.
|
|
3056
|
+
*/
|
|
3057
|
+
relatedSessionAccess: z.enum(["target", "root"]).optional(),
|
|
3058
|
+
/** A host may request a tighter stream reauthorization bound. */
|
|
3059
|
+
reauthorizeAfterMs: z.number().int().min(1_000).max(60_000).optional(),
|
|
3060
|
+
}),
|
|
3061
|
+
z.object({
|
|
3062
|
+
allowed: z.literal(false),
|
|
3063
|
+
reason: z.enum(["not_found", "forbidden", "revoked"]),
|
|
3064
|
+
}),
|
|
3065
|
+
]);
|
|
3066
|
+
export type SessionAuthorizationDecision = z.infer<typeof SessionAuthorizationDecision>;
|
|
3067
|
+
|
|
3068
|
+
/**
|
|
3069
|
+
* A database-applicable listing scope. `rootSessionIds` includes every
|
|
3070
|
+
* descendant of those lineage anchors; `sessionIds` authorizes only the exact
|
|
3071
|
+
* sessions. Supplying neither is an explicit empty scope. OpenGeni intersects
|
|
3072
|
+
* every id with the requested workspace and never trusts a host scope as
|
|
3073
|
+
* session existence evidence.
|
|
3074
|
+
*/
|
|
3075
|
+
export const SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS = 10_000;
|
|
3076
|
+
|
|
3077
|
+
export const SessionAuthorizationListScope = z.discriminatedUnion("kind", [
|
|
3078
|
+
z.object({ kind: z.literal("all") }),
|
|
3079
|
+
z.object({
|
|
3080
|
+
kind: z.literal("scoped"),
|
|
3081
|
+
rootSessionIds: z.array(z.string().uuid()).max(SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS),
|
|
3082
|
+
sessionIds: z.array(z.string().uuid()).max(SESSION_AUTHORIZATION_LIST_SCOPE_MAX_IDS),
|
|
3083
|
+
}),
|
|
3084
|
+
]);
|
|
3085
|
+
export type SessionAuthorizationListScope = z.infer<typeof SessionAuthorizationListScope>;
|
|
3086
|
+
|
|
3087
|
+
export type ResolveSessionAuthorizationListScopeInput = {
|
|
3088
|
+
accountId: string;
|
|
3089
|
+
workspaceId: string;
|
|
3090
|
+
actor: SessionAuthorizationActor;
|
|
3091
|
+
surface: SessionAuthorizationSurface;
|
|
3092
|
+
};
|
|
3093
|
+
|
|
3094
|
+
export type SessionAuthorizationPort = {
|
|
3095
|
+
authorizeSession(input: AuthorizeSessionInput): Promise<SessionAuthorizationDecision>;
|
|
3096
|
+
/**
|
|
3097
|
+
* Return the complete current scope used inside OpenGeni's cursor query.
|
|
3098
|
+
* This is deliberately not a post-filter callback: search, pinning, ordering,
|
|
3099
|
+
* totals, and cursor advancement must all operate on authorized rows.
|
|
3100
|
+
*/
|
|
3101
|
+
resolveListScope(
|
|
3102
|
+
input: ResolveSessionAuthorizationListScopeInput,
|
|
3103
|
+
): Promise<SessionAuthorizationListScope>;
|
|
3104
|
+
};
|
|
3105
|
+
|
|
2030
3106
|
export const SessionTurn = z.object({
|
|
2031
3107
|
id: z.string().uuid(),
|
|
2032
3108
|
workspaceId: z.string().uuid(),
|
|
@@ -2049,6 +3125,8 @@ export const SessionTurn = z.object({
|
|
|
2049
3125
|
executionGeneration: z.number().int().nonnegative(),
|
|
2050
3126
|
activeAttemptId: z.string().uuid().nullable(),
|
|
2051
3127
|
lineage: z.record(z.string(), z.unknown()),
|
|
3128
|
+
initiator: TurnInitiator,
|
|
3129
|
+
initiatorContext: TurnInitiatorContext,
|
|
2052
3130
|
cancelledBy: z.string().nullable(),
|
|
2053
3131
|
cancelReason: z.string().nullable(),
|
|
2054
3132
|
startedAt: z.string().nullable(),
|
|
@@ -2058,70 +3136,409 @@ export const SessionTurn = z.object({
|
|
|
2058
3136
|
});
|
|
2059
3137
|
export type SessionTurn = z.infer<typeof SessionTurn>;
|
|
2060
3138
|
|
|
3139
|
+
export const EffectiveControlBlocker = z.object({
|
|
3140
|
+
kind: z.enum(["session", "workspace"]),
|
|
3141
|
+
sessionId: z.string().uuid().optional(),
|
|
3142
|
+
displayName: z.string().min(1),
|
|
3143
|
+
actor: z.string().nullable(),
|
|
3144
|
+
reason: z.string().nullable(),
|
|
3145
|
+
changedAt: z.string().nullable(),
|
|
3146
|
+
revision: z.number().int().nonnegative(),
|
|
3147
|
+
});
|
|
3148
|
+
export type EffectiveControlBlocker = z.infer<typeof EffectiveControlBlocker>;
|
|
3149
|
+
|
|
3150
|
+
export const EffectiveControlResumeOption = z.object({
|
|
3151
|
+
scope: z.enum(["selected", "session", "workspace"]),
|
|
3152
|
+
targetId: z.string().uuid().optional(),
|
|
3153
|
+
selectedStateAfter: SessionControlState,
|
|
3154
|
+
remainingPrimaryBlocker: EffectiveControlBlocker.optional(),
|
|
3155
|
+
impactCopy: z.string().min(1),
|
|
3156
|
+
});
|
|
3157
|
+
export type EffectiveControlResumeOption = z.infer<typeof EffectiveControlResumeOption>;
|
|
3158
|
+
|
|
3159
|
+
export const EffectiveSessionControl = z.object({
|
|
3160
|
+
state: SessionControlState,
|
|
3161
|
+
controlVersion: z.number().int().nonnegative(),
|
|
3162
|
+
controlEtag: z.string().min(1),
|
|
3163
|
+
directState: SessionControlState,
|
|
3164
|
+
primaryBlocker: EffectiveControlBlocker.nullable(),
|
|
3165
|
+
additionalBlockerCount: z.number().int().nonnegative(),
|
|
3166
|
+
blockers: z.array(EffectiveControlBlocker),
|
|
3167
|
+
resumeOptions: z.array(EffectiveControlResumeOption),
|
|
3168
|
+
override: z
|
|
3169
|
+
.object({
|
|
3170
|
+
rootSessionId: z.string().uuid(),
|
|
3171
|
+
revision: z.number().int().nonnegative(),
|
|
3172
|
+
})
|
|
3173
|
+
.nullable(),
|
|
3174
|
+
settlement: z
|
|
3175
|
+
.object({
|
|
3176
|
+
state: z.literal("stopping"),
|
|
3177
|
+
attemptCount: z.number().int().positive(),
|
|
3178
|
+
interruptionPendingCount: z.number().int().nonnegative(),
|
|
3179
|
+
quiescencePendingCount: z.number().int().nonnegative(),
|
|
3180
|
+
})
|
|
3181
|
+
.nullable(),
|
|
3182
|
+
});
|
|
3183
|
+
export type EffectiveSessionControl = z.infer<typeof EffectiveSessionControl>;
|
|
3184
|
+
|
|
3185
|
+
export const SESSION_OPERATION_KEY_MAX_CHARS = 256;
|
|
3186
|
+
const SessionOperationKey = z.string().min(1).max(SESSION_OPERATION_KEY_MAX_CHARS);
|
|
3187
|
+
|
|
3188
|
+
export const SessionCommandReceipt = z.object({
|
|
3189
|
+
id: z.string().uuid(),
|
|
3190
|
+
action: z.string().min(1),
|
|
3191
|
+
operationKey: z.string().min(1).max(SESSION_OPERATION_KEY_MAX_CHARS),
|
|
3192
|
+
targetSessionId: z.string().uuid().nullable(),
|
|
3193
|
+
targetTurnId: z.string().uuid().nullable(),
|
|
3194
|
+
appliedControlRevision: z.number().int().nonnegative().nullable(),
|
|
3195
|
+
appliedQueueVersion: z.number().int().nonnegative().nullable(),
|
|
3196
|
+
appliedTurnVersion: z.number().int().positive().nullable(),
|
|
3197
|
+
appliedDraftRevision: z.number().int().positive().nullable(),
|
|
3198
|
+
createdAt: z.string(),
|
|
3199
|
+
});
|
|
3200
|
+
export type SessionCommandReceipt = z.infer<typeof SessionCommandReceipt>;
|
|
3201
|
+
|
|
3202
|
+
export const ComposerDraft = z.object({
|
|
3203
|
+
revision: z.number().int().nonnegative(),
|
|
3204
|
+
text: z.string(),
|
|
3205
|
+
resources: z.array(ResourceRef),
|
|
3206
|
+
tools: z.array(ToolRef),
|
|
3207
|
+
model: z.string().min(1),
|
|
3208
|
+
reasoningEffort: ReasoningEffort,
|
|
3209
|
+
sourceTurnId: z.string().uuid().nullable(),
|
|
3210
|
+
sourceTurnVersion: z.number().int().positive().nullable(),
|
|
3211
|
+
updatedAt: z.string().nullable(),
|
|
3212
|
+
});
|
|
3213
|
+
export type ComposerDraft = z.infer<typeof ComposerDraft>;
|
|
3214
|
+
|
|
2061
3215
|
export const SessionQueueSnapshot = z.object({
|
|
2062
3216
|
version: z.number().int().nonnegative(),
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
3217
|
+
effectiveControl: EffectiveSessionControl,
|
|
3218
|
+
/**
|
|
3219
|
+
* True while the latest attempt is interrupted but has not durably proved
|
|
3220
|
+
* quiescence: no more inference, user-visible output, or workspace-persistence
|
|
3221
|
+
* authority. Temporal cancellation/terminalization is not that proof. This is
|
|
3222
|
+
* distinct from ordinary capacity queueing, remains accurate with an empty
|
|
3223
|
+
* visible queue, and is independent of Steer-row metadata or withdrawal.
|
|
3224
|
+
*/
|
|
3225
|
+
stoppingPreviousAttempt: z.boolean(),
|
|
2068
3226
|
items: z.array(SessionTurn),
|
|
2069
3227
|
});
|
|
2070
3228
|
export type SessionQueueSnapshot = z.infer<typeof SessionQueueSnapshot>;
|
|
2071
3229
|
|
|
2072
|
-
export const
|
|
3230
|
+
export const MoveSessionQueueItemRequest = z.object({
|
|
3231
|
+
clientEventId: SessionOperationKey,
|
|
2073
3232
|
expectedQueueVersion: z.number().int().nonnegative(),
|
|
2074
|
-
|
|
3233
|
+
beforeTurnId: z.string().uuid().nullable(),
|
|
3234
|
+
});
|
|
3235
|
+
export type MoveSessionQueueItemRequest = z.infer<typeof MoveSessionQueueItemRequest>;
|
|
3236
|
+
|
|
3237
|
+
export const EditSessionQueueItemRequest = z.object({
|
|
3238
|
+
clientEventId: SessionOperationKey,
|
|
3239
|
+
expectedTurnVersion: z.number().int().positive(),
|
|
3240
|
+
expectedDraftRevision: z.number().int().nonnegative(),
|
|
3241
|
+
replaceDraft: z.boolean(),
|
|
3242
|
+
});
|
|
3243
|
+
export type EditSessionQueueItemRequest = z.infer<typeof EditSessionQueueItemRequest>;
|
|
3244
|
+
|
|
3245
|
+
export const SteerSessionQueueItemRequest = z.object({
|
|
3246
|
+
clientEventId: SessionOperationKey,
|
|
3247
|
+
expectedTurnVersion: z.number().int().positive(),
|
|
3248
|
+
controlEtag: z.string().min(1).optional(),
|
|
3249
|
+
});
|
|
3250
|
+
export type SteerSessionQueueItemRequest = z.infer<typeof SteerSessionQueueItemRequest>;
|
|
3251
|
+
|
|
3252
|
+
export const DeleteSessionQueueItemRequest = z.object({
|
|
3253
|
+
clientEventId: SessionOperationKey,
|
|
3254
|
+
expectedTurnVersion: z.number().int().positive(),
|
|
2075
3255
|
reason: z.string().min(1).optional(),
|
|
2076
3256
|
});
|
|
2077
|
-
export type
|
|
3257
|
+
export type DeleteSessionQueueItemRequest = z.infer<typeof DeleteSessionQueueItemRequest>;
|
|
3258
|
+
|
|
3259
|
+
export const SaveComposerDraftRequest = ComposerDraft.pick({
|
|
3260
|
+
text: true,
|
|
3261
|
+
resources: true,
|
|
3262
|
+
tools: true,
|
|
3263
|
+
model: true,
|
|
3264
|
+
reasoningEffort: true,
|
|
3265
|
+
}).extend({ expectedRevision: z.number().int().nonnegative() });
|
|
3266
|
+
export type SaveComposerDraftRequest = z.infer<typeof SaveComposerDraftRequest>;
|
|
3267
|
+
|
|
3268
|
+
export const WORKSPACE_CONTROL_REASON_MAX_BYTES = 8 * 1024;
|
|
3269
|
+
export const WORKSPACE_CONTROL_ACTOR_MAX_BYTES = 1024;
|
|
3270
|
+
export const WORKSPACE_CONTROL_EVENT_MAX_BYTES = 16 * 1024;
|
|
3271
|
+
|
|
3272
|
+
const WorkspaceControlReason = z
|
|
3273
|
+
.string()
|
|
3274
|
+
.min(1)
|
|
3275
|
+
.refine((value) => !value.includes("\u0000"), "reason must not contain NUL bytes")
|
|
3276
|
+
.refine(
|
|
3277
|
+
(value) => workspaceControlUtf8Bytes(value) <= WORKSPACE_CONTROL_REASON_MAX_BYTES,
|
|
3278
|
+
`reason must not exceed ${WORKSPACE_CONTROL_REASON_MAX_BYTES} UTF-8 bytes`,
|
|
3279
|
+
);
|
|
2078
3280
|
|
|
2079
3281
|
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(),
|
|
3282
|
+
action: z.enum(["pause", "resume"]),
|
|
3283
|
+
reason: WorkspaceControlReason.optional(),
|
|
3284
|
+
clientEventId: SessionOperationKey,
|
|
3285
|
+
expectedControlEtag: z.string().min(1).optional(),
|
|
2086
3286
|
});
|
|
2087
3287
|
export type SessionControlRequest = z.infer<typeof SessionControlRequest>;
|
|
2088
3288
|
|
|
2089
3289
|
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([]),
|
|
3290
|
+
action: z.enum(["pause", "resume"]),
|
|
3291
|
+
reason: WorkspaceControlReason.optional(),
|
|
3292
|
+
clientEventId: SessionOperationKey,
|
|
3293
|
+
expectedRevision: z.number().int().nonnegative().optional(),
|
|
2096
3294
|
});
|
|
2097
3295
|
export type WorkspaceInferenceControlRequest = z.infer<typeof WorkspaceInferenceControlRequest>;
|
|
2098
3296
|
|
|
2099
3297
|
export const WorkspaceInferenceControlResponse = z.object({
|
|
2100
|
-
|
|
2101
|
-
state:
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
exceptionSessionIds: z.array(z.string().uuid()),
|
|
3298
|
+
receipt: SessionCommandReceipt,
|
|
3299
|
+
state: SessionControlState,
|
|
3300
|
+
revision: z.number().int().nonnegative(),
|
|
3301
|
+
interruptionCount: z.number().int().nonnegative(),
|
|
3302
|
+
wakeCount: z.number().int().nonnegative(),
|
|
2106
3303
|
});
|
|
2107
3304
|
export type WorkspaceInferenceControlResponse = z.infer<typeof WorkspaceInferenceControlResponse>;
|
|
2108
3305
|
|
|
3306
|
+
/**
|
|
3307
|
+
* One durable workspace-wide invalidation for one committed control revision.
|
|
3308
|
+
* It is not conversation history and never becomes queue work; clients use it
|
|
3309
|
+
* only to refetch authoritative workspace/session projections.
|
|
3310
|
+
*/
|
|
3311
|
+
export const WorkspaceControlEventTruncation = z.object({
|
|
3312
|
+
truncated: z.literal(true),
|
|
3313
|
+
surface: z.enum([
|
|
3314
|
+
"durable_control",
|
|
3315
|
+
"database_guard",
|
|
3316
|
+
"http_projection",
|
|
3317
|
+
"nats_legacy_guard",
|
|
3318
|
+
"sse_legacy_guard",
|
|
3319
|
+
]),
|
|
3320
|
+
deliveredBytes: z.number().int().nonnegative(),
|
|
3321
|
+
fields: z.array(
|
|
3322
|
+
z.object({
|
|
3323
|
+
field: z.enum(["reason", "actor"]),
|
|
3324
|
+
originalBytes: z.number().int().nonnegative(),
|
|
3325
|
+
deliveredBytes: z.number().int().nonnegative(),
|
|
3326
|
+
omittedBytes: z.number().int().nonnegative(),
|
|
3327
|
+
}),
|
|
3328
|
+
),
|
|
3329
|
+
fullEvidence: z.object({
|
|
3330
|
+
available: z.literal(false),
|
|
3331
|
+
reason: z.literal("not_retained"),
|
|
3332
|
+
}),
|
|
3333
|
+
});
|
|
3334
|
+
export type WorkspaceControlEventTruncation = z.infer<typeof WorkspaceControlEventTruncation>;
|
|
3335
|
+
|
|
3336
|
+
export const WorkspaceControlEvent = z.object({
|
|
3337
|
+
id: z.string().uuid(),
|
|
3338
|
+
workspaceId: z.string().uuid(),
|
|
3339
|
+
sequence: z.number().int().positive(),
|
|
3340
|
+
revision: z.number().int().positive(),
|
|
3341
|
+
type: z.literal("workspace.control.changed"),
|
|
3342
|
+
scope: z.enum(["workspace", "session"]),
|
|
3343
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
3344
|
+
action: z.enum(["pause", "resume"]),
|
|
3345
|
+
automatic: z.boolean(),
|
|
3346
|
+
reason: z.string().nullable(),
|
|
3347
|
+
actor: z.string().min(1),
|
|
3348
|
+
occurredAt: z.string(),
|
|
3349
|
+
truncation: WorkspaceControlEventTruncation.nullable().optional(),
|
|
3350
|
+
});
|
|
3351
|
+
export type WorkspaceControlEvent = z.infer<typeof WorkspaceControlEvent>;
|
|
3352
|
+
|
|
3353
|
+
export type WorkspaceControlBoundarySurface = WorkspaceControlEventTruncation["surface"];
|
|
3354
|
+
|
|
3355
|
+
export type BoundWorkspaceControlEventOptions = {
|
|
3356
|
+
surface?: WorkspaceControlBoundarySurface;
|
|
3357
|
+
reasonOriginalBytes?: number | null;
|
|
3358
|
+
actorOriginalBytes?: number | null;
|
|
3359
|
+
};
|
|
3360
|
+
|
|
3361
|
+
/** UTF-8 byte count used by workspace-control storage and transport guards. */
|
|
3362
|
+
export function workspaceControlUtf8Bytes(value: string): number {
|
|
3363
|
+
return new TextEncoder().encode(value).byteLength;
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
/**
|
|
3367
|
+
* Canonical bounded invalidation event. The event is not a full evidence store:
|
|
3368
|
+
* when a producer or legacy row exceeds a field cap, the retained head carries
|
|
3369
|
+
* a visible marker and structured exact byte-loss facts.
|
|
3370
|
+
*/
|
|
3371
|
+
export function boundWorkspaceControlEvent(
|
|
3372
|
+
event: WorkspaceControlEvent,
|
|
3373
|
+
options: BoundWorkspaceControlEventOptions = {},
|
|
3374
|
+
): WorkspaceControlEvent {
|
|
3375
|
+
const existingFields = new Map(
|
|
3376
|
+
(event.truncation?.fields ?? []).map((field) => [field.field, field] as const),
|
|
3377
|
+
);
|
|
3378
|
+
const reason =
|
|
3379
|
+
event.reason === null
|
|
3380
|
+
? null
|
|
3381
|
+
: boundWorkspaceControlText(event.reason, WORKSPACE_CONTROL_REASON_MAX_BYTES);
|
|
3382
|
+
const actor = boundWorkspaceControlText(event.actor, WORKSPACE_CONTROL_ACTOR_MAX_BYTES);
|
|
3383
|
+
const reasonBytes = reason === null ? 0 : workspaceControlUtf8Bytes(reason);
|
|
3384
|
+
const actorBytes = workspaceControlUtf8Bytes(actor);
|
|
3385
|
+
const reasonOriginalBytes =
|
|
3386
|
+
event.reason === null
|
|
3387
|
+
? null
|
|
3388
|
+
: Math.max(
|
|
3389
|
+
workspaceControlUtf8Bytes(event.reason),
|
|
3390
|
+
normalizedWorkspaceControlOriginalBytes(options.reasonOriginalBytes),
|
|
3391
|
+
existingFields.get("reason")?.originalBytes ?? 0,
|
|
3392
|
+
);
|
|
3393
|
+
const actorOriginalBytes = Math.max(
|
|
3394
|
+
workspaceControlUtf8Bytes(event.actor),
|
|
3395
|
+
normalizedWorkspaceControlOriginalBytes(options.actorOriginalBytes),
|
|
3396
|
+
existingFields.get("actor")?.originalBytes ?? 0,
|
|
3397
|
+
);
|
|
3398
|
+
const fields: WorkspaceControlEventTruncation["fields"] = [];
|
|
3399
|
+
if (reasonOriginalBytes !== null && reasonOriginalBytes > reasonBytes) {
|
|
3400
|
+
fields.push({
|
|
3401
|
+
field: "reason",
|
|
3402
|
+
originalBytes: reasonOriginalBytes,
|
|
3403
|
+
deliveredBytes: reasonBytes,
|
|
3404
|
+
omittedBytes: reasonOriginalBytes - reasonBytes,
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
if (actorOriginalBytes > actorBytes) {
|
|
3408
|
+
fields.push({
|
|
3409
|
+
field: "actor",
|
|
3410
|
+
originalBytes: actorOriginalBytes,
|
|
3411
|
+
deliveredBytes: actorBytes,
|
|
3412
|
+
omittedBytes: actorOriginalBytes - actorBytes,
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
if (fields.length === 0 && event.truncation == null) {
|
|
3416
|
+
if (sessionEventJsonBytes(event) > WORKSPACE_CONTROL_EVENT_MAX_BYTES) {
|
|
3417
|
+
throw new RangeError("Workspace control event exceeds its bounded envelope");
|
|
3418
|
+
}
|
|
3419
|
+
return event;
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
const truncation: WorkspaceControlEventTruncation = {
|
|
3423
|
+
truncated: true,
|
|
3424
|
+
surface: event.truncation?.surface ?? options.surface ?? "durable_control",
|
|
3425
|
+
deliveredBytes: 0,
|
|
3426
|
+
fields,
|
|
3427
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
3428
|
+
};
|
|
3429
|
+
const bounded: WorkspaceControlEvent = {
|
|
3430
|
+
...event,
|
|
3431
|
+
reason,
|
|
3432
|
+
actor,
|
|
3433
|
+
truncation,
|
|
3434
|
+
};
|
|
3435
|
+
settleWorkspaceControlDeliveredBytes(bounded, truncation);
|
|
3436
|
+
const deliveredBytes = sessionEventJsonBytes(bounded);
|
|
3437
|
+
if (deliveredBytes > WORKSPACE_CONTROL_EVENT_MAX_BYTES) {
|
|
3438
|
+
throw new RangeError(
|
|
3439
|
+
`Bounded workspace control event exceeds its final envelope (${deliveredBytes} > ${WORKSPACE_CONTROL_EVENT_MAX_BYTES} bytes)`,
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
return bounded;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function boundWorkspaceControlText(value: string, maxBytes: number): string {
|
|
3446
|
+
const encoder = new TextEncoder();
|
|
3447
|
+
const decoder = new TextDecoder();
|
|
3448
|
+
const bytes = encoder.encode(value);
|
|
3449
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
3450
|
+
const marker = "…[truncated]";
|
|
3451
|
+
const prefixBudget = Math.max(0, maxBytes - encoder.encode(marker).byteLength);
|
|
3452
|
+
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
3453
|
+
while (prefixEnd > 0 && prefixEnd < bytes.byteLength && (bytes[prefixEnd]! & 0xc0) === 0x80) {
|
|
3454
|
+
prefixEnd -= 1;
|
|
3455
|
+
}
|
|
3456
|
+
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
function normalizedWorkspaceControlOriginalBytes(value: number | null | undefined): number {
|
|
3460
|
+
return value === null || value === undefined || !Number.isFinite(value)
|
|
3461
|
+
? 0
|
|
3462
|
+
: Math.max(0, Math.floor(value));
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
function settleWorkspaceControlDeliveredBytes(
|
|
3466
|
+
event: WorkspaceControlEvent,
|
|
3467
|
+
truncation: WorkspaceControlEventTruncation,
|
|
3468
|
+
): void {
|
|
3469
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
3470
|
+
const deliveredBytes = sessionEventJsonBytes(event);
|
|
3471
|
+
if (truncation.deliveredBytes === deliveredBytes) return;
|
|
3472
|
+
truncation.deliveredBytes = deliveredBytes;
|
|
3473
|
+
}
|
|
3474
|
+
const deliveredBytes = sessionEventJsonBytes(event);
|
|
3475
|
+
if (truncation.deliveredBytes !== deliveredBytes) {
|
|
3476
|
+
throw new RangeError("Workspace control event byte accounting did not converge");
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
|
|
2109
3480
|
export const SystemUpdateClassification = z.enum(["success", "failure", "action_required", "info"]);
|
|
2110
3481
|
export type SystemUpdateClassification = z.infer<typeof SystemUpdateClassification>;
|
|
2111
3482
|
|
|
2112
3483
|
export const SessionSystemUpdateKind = z.enum([
|
|
2113
|
-
"
|
|
2114
|
-
"
|
|
2115
|
-
"
|
|
2116
|
-
"
|
|
3484
|
+
"scheduled_occurrence",
|
|
3485
|
+
"goal_continuation",
|
|
3486
|
+
"agent_message",
|
|
3487
|
+
"agent_steer_instruction",
|
|
3488
|
+
"child_terminal_result",
|
|
2117
3489
|
]);
|
|
2118
3490
|
export type SessionSystemUpdateKind = z.infer<typeof SessionSystemUpdateKind>;
|
|
2119
3491
|
|
|
3492
|
+
export const SessionSystemUpdatePayload = z.discriminatedUnion("type", [
|
|
3493
|
+
z
|
|
3494
|
+
.object({
|
|
3495
|
+
type: z.literal("scheduled_occurrence"),
|
|
3496
|
+
text: z.string().min(1),
|
|
3497
|
+
scheduledTaskId: z.string().uuid(),
|
|
3498
|
+
scheduledTaskRunId: z.string().uuid(),
|
|
3499
|
+
resources: z.array(ResourceRef).optional(),
|
|
3500
|
+
tools: z.array(ToolRef).optional(),
|
|
3501
|
+
})
|
|
3502
|
+
.passthrough(),
|
|
3503
|
+
z
|
|
3504
|
+
.object({
|
|
3505
|
+
type: z.literal("goal_continuation"),
|
|
3506
|
+
goalId: z.string().uuid(),
|
|
3507
|
+
goalVersion: z.number().int().positive(),
|
|
3508
|
+
prompt: z.string().min(1),
|
|
3509
|
+
reason: z.string().optional(),
|
|
3510
|
+
})
|
|
3511
|
+
.passthrough(),
|
|
3512
|
+
z
|
|
3513
|
+
.object({
|
|
3514
|
+
type: z.literal("agent_message"),
|
|
3515
|
+
text: z.string().min(1),
|
|
3516
|
+
operationId: z.string().uuid(),
|
|
3517
|
+
})
|
|
3518
|
+
.passthrough(),
|
|
3519
|
+
z
|
|
3520
|
+
.object({
|
|
3521
|
+
type: z.literal("agent_steer_instruction"),
|
|
3522
|
+
instruction: z.string().min(1),
|
|
3523
|
+
operationId: z.string().uuid(),
|
|
3524
|
+
})
|
|
3525
|
+
.passthrough(),
|
|
3526
|
+
z
|
|
3527
|
+
.object({
|
|
3528
|
+
type: z.literal("child_terminal_result"),
|
|
3529
|
+
childSessionId: z.string().uuid(),
|
|
3530
|
+
status: z.enum(["idle", "failed"]),
|
|
3531
|
+
})
|
|
3532
|
+
.passthrough(),
|
|
3533
|
+
]);
|
|
3534
|
+
export type SessionSystemUpdatePayload = z.infer<typeof SessionSystemUpdatePayload>;
|
|
3535
|
+
|
|
2120
3536
|
export const SessionSystemUpdateState = z.enum([
|
|
2121
3537
|
"pending",
|
|
2122
3538
|
"deferred",
|
|
2123
3539
|
"delivered",
|
|
2124
3540
|
"cancelled",
|
|
3541
|
+
"superseded",
|
|
2125
3542
|
"failed",
|
|
2126
3543
|
]);
|
|
2127
3544
|
export type SessionSystemUpdateState = z.infer<typeof SessionSystemUpdateState>;
|
|
@@ -2134,7 +3551,7 @@ export const SessionSystemUpdate = z.object({
|
|
|
2134
3551
|
sourceId: z.string(),
|
|
2135
3552
|
dedupeKey: z.string(),
|
|
2136
3553
|
summary: z.string(),
|
|
2137
|
-
payload:
|
|
3554
|
+
payload: SessionSystemUpdatePayload,
|
|
2138
3555
|
lineage: z.record(z.string(), z.unknown()),
|
|
2139
3556
|
state: SessionSystemUpdateState,
|
|
2140
3557
|
deliveredTurnId: z.string().uuid().nullable(),
|
|
@@ -2365,7 +3782,10 @@ export type RigDefinitionEditPayload = z.infer<typeof RigDefinitionEditPayload>;
|
|
|
2365
3782
|
|
|
2366
3783
|
export const ProposeRigChangeRequest = z.discriminatedUnion("kind", [
|
|
2367
3784
|
z.object({ kind: z.literal("setup_append"), payload: RigSetupAppendPayload }),
|
|
2368
|
-
z.object({
|
|
3785
|
+
z.object({
|
|
3786
|
+
kind: z.literal("definition_edit"),
|
|
3787
|
+
payload: RigDefinitionEditPayload,
|
|
3788
|
+
}),
|
|
2369
3789
|
]);
|
|
2370
3790
|
export type ProposeRigChangeRequest = z.infer<typeof ProposeRigChangeRequest>;
|
|
2371
3791
|
|
|
@@ -2785,27 +4205,15 @@ export const CreateSocialPostRequest = z.object({
|
|
|
2785
4205
|
text: z.string().min(1),
|
|
2786
4206
|
publishedAt: z.string().datetime({ offset: true }),
|
|
2787
4207
|
metrics: z.record(z.string(), z.number()).default({}),
|
|
2788
|
-
raw: z.record(z.string(), z.unknown()).default({}),
|
|
2789
|
-
});
|
|
2790
|
-
export type CreateSocialPostRequest = z.infer<typeof CreateSocialPostRequest>;
|
|
2791
|
-
|
|
2792
|
-
export const ConnectionKind = z.enum(["oauth2", "api_key", "app_install", "delegated"]);
|
|
2793
|
-
export type ConnectionKind = z.infer<typeof ConnectionKind>;
|
|
2794
|
-
|
|
2795
|
-
export const ConnectionStatus = z.enum(["active", "needs_reauth", "revoked", "error"]);
|
|
2796
|
-
export type ConnectionStatus = z.infer<typeof ConnectionStatus>;
|
|
2797
|
-
|
|
2798
|
-
export const McpServerConnectionRef = z
|
|
2799
|
-
.object({
|
|
2800
|
-
connectionId: z.string().uuid().optional(),
|
|
2801
|
-
providerDomain: z.string().min(1),
|
|
2802
|
-
kind: ConnectionKind.optional(),
|
|
2803
|
-
scopes: z.array(z.string().min(1)).optional(),
|
|
2804
|
-
resource: z.string().min(1).optional(),
|
|
2805
|
-
subjectScope: z.enum(["workspace", "subject"]).optional(),
|
|
2806
|
-
})
|
|
2807
|
-
.strict();
|
|
2808
|
-
export type McpServerConnectionRef = z.infer<typeof McpServerConnectionRef>;
|
|
4208
|
+
raw: z.record(z.string(), z.unknown()).default({}),
|
|
4209
|
+
});
|
|
4210
|
+
export type CreateSocialPostRequest = z.infer<typeof CreateSocialPostRequest>;
|
|
4211
|
+
|
|
4212
|
+
export const ConnectionKind = z.enum(["oauth2", "api_key", "app_install", "delegated"]);
|
|
4213
|
+
export type ConnectionKind = z.infer<typeof ConnectionKind>;
|
|
4214
|
+
|
|
4215
|
+
export const ConnectionStatus = z.enum(["active", "needs_reauth", "revoked", "error"]);
|
|
4216
|
+
export type ConnectionStatus = z.infer<typeof ConnectionStatus>;
|
|
2809
4217
|
|
|
2810
4218
|
export const ConnectionMetadata = z.object({
|
|
2811
4219
|
id: z.string().uuid(),
|
|
@@ -2925,6 +4333,7 @@ export type CapabilityKind = z.infer<typeof CapabilityKind>;
|
|
|
2925
4333
|
|
|
2926
4334
|
export const CapabilitySource = z.enum([
|
|
2927
4335
|
"built_in",
|
|
4336
|
+
"library",
|
|
2928
4337
|
"configured",
|
|
2929
4338
|
"public_registry",
|
|
2930
4339
|
"registry",
|
|
@@ -3077,6 +4486,9 @@ export const Session = z.object({
|
|
|
3077
4486
|
resources: z.array(ResourceRef),
|
|
3078
4487
|
tools: z.array(ToolRef),
|
|
3079
4488
|
metadata: z.record(z.string(), z.unknown()),
|
|
4489
|
+
/** Frozen creator fact used only for creation attribution/idempotent repair. */
|
|
4490
|
+
createdBy: TurnInitiator,
|
|
4491
|
+
createdByContext: TurnInitiatorContext,
|
|
3080
4492
|
model: z.string(),
|
|
3081
4493
|
sandboxBackend: SandboxBackend,
|
|
3082
4494
|
// The OS the session's box runs. Defaults to 'linux' (today's only OS).
|
|
@@ -3126,12 +4538,7 @@ export const Session = z.object({
|
|
|
3126
4538
|
queueVersion: z.number().int().nonnegative(),
|
|
3127
4539
|
queueHeadPosition: z.number().int(),
|
|
3128
4540
|
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(),
|
|
4541
|
+
effectiveControl: EffectiveSessionControl,
|
|
3135
4542
|
lastSequence: z.number().int().nonnegative(),
|
|
3136
4543
|
// Multi-account Codex (P1). codexPinnedCredentialId: the account this session is
|
|
3137
4544
|
// manually PINNED to (null ⇒ follow the workspace active pointer).
|
|
@@ -3159,6 +4566,8 @@ export const Session = z.object({
|
|
|
3159
4566
|
attentionDescendants: z.number().int().nonnegative(),
|
|
3160
4567
|
pausedDescendants: z.number().int().nonnegative(),
|
|
3161
4568
|
failedDescendants: z.number().int().nonnegative(),
|
|
4569
|
+
/** Counts are lower bounds rather than exact totals when true. */
|
|
4570
|
+
truncated: z.boolean().default(false),
|
|
3162
4571
|
})
|
|
3163
4572
|
.optional(),
|
|
3164
4573
|
createdAt: z.string(),
|
|
@@ -3166,16 +4575,30 @@ export const Session = z.object({
|
|
|
3166
4575
|
});
|
|
3167
4576
|
export type Session = z.infer<typeof Session>;
|
|
3168
4577
|
|
|
4578
|
+
/**
|
|
4579
|
+
* Additive receipt returned only by session creation. `activeTurnId` remains an
|
|
4580
|
+
* execution pointer and is correctly null while the first turn is queued;
|
|
4581
|
+
* embedders use this immutable identity to correlate their preallocated run.
|
|
4582
|
+
*/
|
|
4583
|
+
export const CreateSessionResponse = Session.extend({
|
|
4584
|
+
initialTurnId: z.string().uuid().nullable(),
|
|
4585
|
+
});
|
|
4586
|
+
export type CreateSessionResponse = z.infer<typeof CreateSessionResponse>;
|
|
4587
|
+
|
|
3169
4588
|
export type SessionSummary = Session;
|
|
3170
4589
|
|
|
3171
4590
|
/**
|
|
3172
4591
|
* The canonical session-list page. Pinned rows are returned separately and are
|
|
3173
4592
|
* excluded from `sessions`, so a cursor can page ordinary recency rows without
|
|
3174
|
-
* duplicating a pin.
|
|
3175
|
-
*
|
|
4593
|
+
* duplicating a pin. The newest 100 matching pins are returned, ordered by
|
|
4594
|
+
* pinnedAt DESC, id DESC; `pinnedTruncated` makes an older-pin omission
|
|
4595
|
+
* explicit. Pins are filtered by the same parent/search predicates as ordinary
|
|
4596
|
+
* rows.
|
|
3176
4597
|
*/
|
|
3177
4598
|
export const SessionListResponse = z.object({
|
|
3178
4599
|
pinned: z.array(Session),
|
|
4600
|
+
/** True when older matching pins were omitted from this bounded page. */
|
|
4601
|
+
pinnedTruncated: z.boolean().optional(),
|
|
3179
4602
|
sessions: z.array(Session),
|
|
3180
4603
|
nextCursor: z.string().nullable(),
|
|
3181
4604
|
});
|
|
@@ -3204,8 +4627,14 @@ export type SessionLineageResponse = z.infer<typeof SessionLineageResponse>;
|
|
|
3204
4627
|
|
|
3205
4628
|
export const SessionEventType = z.enum([
|
|
3206
4629
|
"session.created",
|
|
4630
|
+
// Defensive read/transport projection for a malformed or historically
|
|
4631
|
+
// oversized retained event envelope. The original row stays durable; this
|
|
4632
|
+
// explicit synthetic type prevents unbounded free-form envelope fields from
|
|
4633
|
+
// crossing NATS, SSE, REST, or browser boundaries.
|
|
4634
|
+
"session.event.envelope_omitted",
|
|
3207
4635
|
"session.status.changed",
|
|
3208
4636
|
"session.requiresAction",
|
|
4637
|
+
"session.humanInput.requested",
|
|
3209
4638
|
"session.context.compaction.requested",
|
|
3210
4639
|
"session.context.compacted",
|
|
3211
4640
|
"session.context.compaction.skipped",
|
|
@@ -3213,6 +4642,7 @@ export const SessionEventType = z.enum([
|
|
|
3213
4642
|
"user.message",
|
|
3214
4643
|
"user.pause",
|
|
3215
4644
|
"user.approvalDecision",
|
|
4645
|
+
"user.humanInputResponse",
|
|
3216
4646
|
"turn.queued",
|
|
3217
4647
|
"turn.started",
|
|
3218
4648
|
"turn.completed",
|
|
@@ -3228,6 +4658,7 @@ export const SessionEventType = z.enum([
|
|
|
3228
4658
|
"agent.toolCall.output",
|
|
3229
4659
|
"agent.model.usage",
|
|
3230
4660
|
"tool.auth_needed",
|
|
4661
|
+
"credential.auth_needed",
|
|
3231
4662
|
"agent.updated",
|
|
3232
4663
|
"rig.setup.started",
|
|
3233
4664
|
"rig.setup.completed",
|
|
@@ -3252,6 +4683,7 @@ export const SessionEventType = z.enum([
|
|
|
3252
4683
|
"session.control.steer_requested",
|
|
3253
4684
|
"workspace.inference.paused",
|
|
3254
4685
|
"workspace.inference.resumed",
|
|
4686
|
+
"session.queue.changed",
|
|
3255
4687
|
"session.queue.prompt.cancelled",
|
|
3256
4688
|
"session.queue.history",
|
|
3257
4689
|
// A terminal/stale activity callback is retained as an audit wrapper rather
|
|
@@ -3266,15 +4698,15 @@ export const SessionEventType = z.enum([
|
|
|
3266
4698
|
"stream.opened", // a viewer attached (audit + refcount visibility)
|
|
3267
4699
|
"stream.closed", // a viewer detached / was reaped
|
|
3268
4700
|
"stream.revoked", // a grant was revoked → connected clients MUST disconnect now
|
|
3269
|
-
//
|
|
3270
|
-
//
|
|
4701
|
+
// Desktop recording signals. The capture loop records the same display humans
|
|
4702
|
+
// watch, then stores the finalized artifact for replay.
|
|
3271
4703
|
// → storage. The artifact ref rides the AVAILABLE event (storageKey, NOT a
|
|
3272
4704
|
// long-lived URL — clients mint a short-TTL signed GET via the route).
|
|
3273
4705
|
"recording.started", // ffmpeg launched on :0 (mode/codec/dimensions)
|
|
3274
4706
|
"recording.available", // finalized: bytes PUT to storage, replayable
|
|
3275
4707
|
"recording.failed", // ffmpeg/box-death/rollover/upload error — no artifact
|
|
3276
|
-
//
|
|
3277
|
-
//
|
|
4708
|
+
// Structured-service notifications. File, Git, and terminal reads are
|
|
4709
|
+
// synchronous API-direct point
|
|
3278
4710
|
// queries (their result is the HTTP response, NEVER an event). What rides A1
|
|
3279
4711
|
// here are the side-effect NOTIFICATIONS — a path changed, git state changed,
|
|
3280
4712
|
// a pty opened/printed/exited — durable, sequenced, gap-filled like every
|
|
@@ -3291,10 +4723,10 @@ export const SessionEventType = z.enum([
|
|
|
3291
4723
|
// (manual switch in P1; failover/rotation in P3 reuse the same event). Drives
|
|
3292
4724
|
// the in-session "Running on:" indicator's live flip.
|
|
3293
4725
|
"codex.account.switched",
|
|
3294
|
-
//
|
|
4726
|
+
// credential allocator per-turn selection audit. Payload is metadata only: credential row
|
|
3295
4727
|
// id, bounded strategy/reason, and pool counts — never token material.
|
|
3296
4728
|
"codex.credential.selected",
|
|
3297
|
-
//
|
|
4729
|
+
// credential allocator durable zero-capacity wait lifecycle. Runtime/system events only;
|
|
3298
4730
|
// no synthetic user message is created when capacity returns.
|
|
3299
4731
|
"codex.capacity.waiting",
|
|
3300
4732
|
"codex.capacity.resumed",
|
|
@@ -3319,12 +4751,12 @@ export const SessionEventType = z.enum([
|
|
|
3319
4751
|
// target id or command content. Announce-only; hits the timeline projection default
|
|
3320
4752
|
// (no rendered item) like the other sandbox.* diagnostics.
|
|
3321
4753
|
"session.route.reconciled",
|
|
3322
|
-
// Workbench v2 turn-end workspace capture
|
|
4754
|
+
// Workbench v2 turn-end workspace capture. ANNOUNCE-ONLY: a new
|
|
3323
4755
|
// capture revision was persisted at turn end; the client refetches the latest
|
|
3324
4756
|
// capture. It carries metadata only (revision/turnId/capturedAt/leaseEpoch/stats),
|
|
3325
4757
|
// never file content. Hits the timeline projection default case (ignored) — it
|
|
3326
4758
|
// must NEVER gain a rendered timeline item without regenerating the golden
|
|
3327
|
-
// snapshots (
|
|
4759
|
+
// snapshots (golden-grammar gate).
|
|
3328
4760
|
"workspace.revision.captured",
|
|
3329
4761
|
// Repository discovery could not prove a complete capture. The worker
|
|
3330
4762
|
// persisted a failed/degraded revision marker and clients must fall back to
|
|
@@ -3366,19 +4798,197 @@ export const SessionEventType = z.enum([
|
|
|
3366
4798
|
]);
|
|
3367
4799
|
export type SessionEventType = z.infer<typeof SessionEventType>;
|
|
3368
4800
|
|
|
4801
|
+
/**
|
|
4802
|
+
* Stable semantic groups for bounded session monitoring. These are a read
|
|
4803
|
+
* projection only: an event keeps its canonical durable `type`, and callers
|
|
4804
|
+
* can always combine a class with explicit type include/exclude filters.
|
|
4805
|
+
*/
|
|
4806
|
+
export const SessionEventSemanticClass = z.enum([
|
|
4807
|
+
"control",
|
|
4808
|
+
"terminal",
|
|
4809
|
+
"failure",
|
|
4810
|
+
"checkpoint",
|
|
4811
|
+
"tool_receipt",
|
|
4812
|
+
"provider_account",
|
|
4813
|
+
]);
|
|
4814
|
+
export type SessionEventSemanticClass = z.infer<typeof SessionEventSemanticClass>;
|
|
4815
|
+
|
|
4816
|
+
export const SessionEventPayloadMode = z.enum(["none", "summary", "full"]);
|
|
4817
|
+
export type SessionEventPayloadMode = z.infer<typeof SessionEventPayloadMode>;
|
|
4818
|
+
|
|
4819
|
+
export const SessionEventReadMode = z.enum(["monitoring", "forensic"]);
|
|
4820
|
+
export type SessionEventReadMode = z.infer<typeof SessionEventReadMode>;
|
|
4821
|
+
|
|
4822
|
+
export const SessionEventReadDirection = z.enum(["after", "before"]);
|
|
4823
|
+
export type SessionEventReadDirection = z.infer<typeof SessionEventReadDirection>;
|
|
4824
|
+
|
|
4825
|
+
export const SESSION_EVENT_RAW_DELTA_TYPES = [
|
|
4826
|
+
"agent.message.delta",
|
|
4827
|
+
"agent.reasoning.delta",
|
|
4828
|
+
"sandbox.command.output.delta",
|
|
4829
|
+
"terminal.pty.output.delta",
|
|
4830
|
+
] as const satisfies readonly SessionEventType[];
|
|
4831
|
+
|
|
4832
|
+
export const SESSION_EVENT_SEMANTIC_CLASS_TYPES = {
|
|
4833
|
+
control: [
|
|
4834
|
+
"session.status.changed",
|
|
4835
|
+
"session.requiresAction",
|
|
4836
|
+
"session.humanInput.requested",
|
|
4837
|
+
"user.pause",
|
|
4838
|
+
"user.approvalDecision",
|
|
4839
|
+
"user.humanInputResponse",
|
|
4840
|
+
"goal.set",
|
|
4841
|
+
"goal.updated",
|
|
4842
|
+
"goal.completed",
|
|
4843
|
+
"goal.paused",
|
|
4844
|
+
"goal.resumed",
|
|
4845
|
+
"goal.cleared",
|
|
4846
|
+
"goal.continuation",
|
|
4847
|
+
"system.update.pending",
|
|
4848
|
+
"system.update.delivered",
|
|
4849
|
+
"session.control.paused",
|
|
4850
|
+
"session.control.resumed",
|
|
4851
|
+
"session.control.steer_requested",
|
|
4852
|
+
"workspace.inference.paused",
|
|
4853
|
+
"workspace.inference.resumed",
|
|
4854
|
+
"session.queue.changed",
|
|
4855
|
+
"session.queue.prompt.cancelled",
|
|
4856
|
+
],
|
|
4857
|
+
terminal: [
|
|
4858
|
+
"turn.completed",
|
|
4859
|
+
"turn.failed",
|
|
4860
|
+
"turn.cancelled",
|
|
4861
|
+
"turn.superseded",
|
|
4862
|
+
"goal.completed",
|
|
4863
|
+
"goal.paused",
|
|
4864
|
+
"rig.setup.completed",
|
|
4865
|
+
"rig.setup.skipped",
|
|
4866
|
+
"rig.setup.failed",
|
|
4867
|
+
"sandbox.operation.completed",
|
|
4868
|
+
"sandbox.operation.failed",
|
|
4869
|
+
"recording.available",
|
|
4870
|
+
"recording.failed",
|
|
4871
|
+
"terminal.pty.exited",
|
|
4872
|
+
],
|
|
4873
|
+
failure: [
|
|
4874
|
+
"session.event.envelope_omitted",
|
|
4875
|
+
"turn.failed",
|
|
4876
|
+
"tool.auth_needed",
|
|
4877
|
+
"credential.auth_needed",
|
|
4878
|
+
"rig.setup.failed",
|
|
4879
|
+
"sandbox.operation.failed",
|
|
4880
|
+
"recording.failed",
|
|
4881
|
+
"sandbox.box.lost",
|
|
4882
|
+
"workspace.revision.degraded",
|
|
4883
|
+
"machine.op.failed",
|
|
4884
|
+
"machine.link.lost",
|
|
4885
|
+
],
|
|
4886
|
+
checkpoint: [
|
|
4887
|
+
"session.context.compaction.requested",
|
|
4888
|
+
"session.context.compacted",
|
|
4889
|
+
"session.context.compaction.skipped",
|
|
4890
|
+
"session.context.cleared",
|
|
4891
|
+
"turn.recovery.requested",
|
|
4892
|
+
"session.queue.history",
|
|
4893
|
+
"sandbox.box.snapshot",
|
|
4894
|
+
"workspace.revision.captured",
|
|
4895
|
+
],
|
|
4896
|
+
tool_receipt: [
|
|
4897
|
+
"agent.toolCall.created",
|
|
4898
|
+
"agent.toolCall.output",
|
|
4899
|
+
"tool.auth_needed",
|
|
4900
|
+
"artifact.created",
|
|
4901
|
+
],
|
|
4902
|
+
provider_account: [
|
|
4903
|
+
"agent.model.usage",
|
|
4904
|
+
"codex.account.switched",
|
|
4905
|
+
"codex.credential.selected",
|
|
4906
|
+
"codex.capacity.waiting",
|
|
4907
|
+
"codex.capacity.resumed",
|
|
4908
|
+
"codex.capacity.superseded",
|
|
4909
|
+
"sandbox.box.created",
|
|
4910
|
+
"sandbox.box.lost",
|
|
4911
|
+
"sandbox.box.terminated",
|
|
4912
|
+
"sandbox.box.snapshot",
|
|
4913
|
+
"sandbox.env.drift",
|
|
4914
|
+
"session.route.reconciled",
|
|
4915
|
+
"machine.op.failed",
|
|
4916
|
+
"machine.op.recovered",
|
|
4917
|
+
"machine.link.lost",
|
|
4918
|
+
"machine.link.restored",
|
|
4919
|
+
"machine.runner.restarted",
|
|
4920
|
+
],
|
|
4921
|
+
} as const satisfies Record<SessionEventSemanticClass, readonly SessionEventType[]>;
|
|
4922
|
+
|
|
4923
|
+
export type ResolveSessionEventTypeFiltersInput = {
|
|
4924
|
+
includeTypes?: readonly SessionEventType[] | undefined;
|
|
4925
|
+
excludeTypes?: readonly SessionEventType[] | undefined;
|
|
4926
|
+
includeClasses?: readonly SessionEventSemanticClass[] | undefined;
|
|
4927
|
+
excludeClasses?: readonly SessionEventSemanticClass[] | undefined;
|
|
4928
|
+
/** Applied unless the same type was explicitly included by type or class. */
|
|
4929
|
+
defaultExcludeTypes?: readonly SessionEventType[] | undefined;
|
|
4930
|
+
};
|
|
4931
|
+
|
|
4932
|
+
/** Resolve class/type filter algebra once so every read surface behaves alike. */
|
|
4933
|
+
export function resolveSessionEventTypeFilters(input: ResolveSessionEventTypeFiltersInput): {
|
|
4934
|
+
includeTypes: SessionEventType[];
|
|
4935
|
+
excludeTypes: SessionEventType[];
|
|
4936
|
+
} {
|
|
4937
|
+
const included = new Set<SessionEventType>(input.includeTypes ?? []);
|
|
4938
|
+
for (const semanticClass of input.includeClasses ?? []) {
|
|
4939
|
+
for (const type of SESSION_EVENT_SEMANTIC_CLASS_TYPES[semanticClass]) included.add(type);
|
|
4940
|
+
}
|
|
4941
|
+
|
|
4942
|
+
const excluded = new Set<SessionEventType>(input.excludeTypes ?? []);
|
|
4943
|
+
for (const semanticClass of input.excludeClasses ?? []) {
|
|
4944
|
+
for (const type of SESSION_EVENT_SEMANTIC_CLASS_TYPES[semanticClass]) excluded.add(type);
|
|
4945
|
+
}
|
|
4946
|
+
for (const type of input.defaultExcludeTypes ?? []) {
|
|
4947
|
+
if (!included.has(type)) excluded.add(type);
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
// An explicit exclusion always wins over a positive selector.
|
|
4951
|
+
for (const type of excluded) included.delete(type);
|
|
4952
|
+
return { includeTypes: [...included], excludeTypes: [...excluded] };
|
|
4953
|
+
}
|
|
4954
|
+
|
|
3369
4955
|
export const ToolAuthNeededPayload = z.object({
|
|
3370
4956
|
serverId: z.string().min(1),
|
|
3371
4957
|
toolName: z.string().min(1).nullable().optional(),
|
|
3372
4958
|
providerDomain: z.string().min(1),
|
|
3373
|
-
|
|
3374
|
-
|
|
4959
|
+
provider: z.string().min(1).max(128).optional(),
|
|
4960
|
+
// Embedded hosts may use an opaque connection identity; never assume an
|
|
4961
|
+
// OpenGeni UUID on the public event wire.
|
|
4962
|
+
connectionId: z.string().min(1).nullable().optional(),
|
|
4963
|
+
reason: z.enum([
|
|
4964
|
+
"missing_connection",
|
|
4965
|
+
"expired",
|
|
4966
|
+
"insufficient_scope",
|
|
4967
|
+
"refresh_failed",
|
|
4968
|
+
"unsupported_auth",
|
|
4969
|
+
"resource_scope_unavailable",
|
|
4970
|
+
]),
|
|
3375
4971
|
scopes: z.array(z.string().min(1)).optional(),
|
|
3376
4972
|
resource: z.string().min(1).optional(),
|
|
4973
|
+
selectedResources: McpConnectionResourceScopes.optional(),
|
|
3377
4974
|
authorizationUrl: z.string().url().optional(),
|
|
3378
4975
|
subjectId: z.string().min(1).nullable().optional(),
|
|
3379
4976
|
});
|
|
3380
4977
|
export type ToolAuthNeededPayload = z.infer<typeof ToolAuthNeededPayload>;
|
|
3381
4978
|
|
|
4979
|
+
/** A host-owned non-tool credential needed by the active run. */
|
|
4980
|
+
export const CredentialAuthNeededPayload = z.object({
|
|
4981
|
+
credentialClass: z.literal("run"),
|
|
4982
|
+
providerDomain: z.string().min(1).optional(),
|
|
4983
|
+
connectionId: z.string().min(1).optional(),
|
|
4984
|
+
reason: z.enum(["missing_connection", "expired", "insufficient_scope", "refresh_failed"]),
|
|
4985
|
+
scopes: z.array(z.string().min(1)).optional(),
|
|
4986
|
+
resource: z.string().min(1).optional(),
|
|
4987
|
+
authorizationUrl: z.string().url().optional(),
|
|
4988
|
+
message: z.string().min(1).optional(),
|
|
4989
|
+
});
|
|
4990
|
+
export type CredentialAuthNeededPayload = z.infer<typeof CredentialAuthNeededPayload>;
|
|
4991
|
+
|
|
3382
4992
|
// Channel-B stream-event payloads (07-channel-b §1.2). SessionEvent.payload is
|
|
3383
4993
|
// z.unknown() (NOT a discriminated union) — these are standalone schemas parsed
|
|
3384
4994
|
// explicitly at the producer (the API-direct handshake/rotation) and the SDK/
|
|
@@ -3480,7 +5090,7 @@ export const RecordingFailedPayload = z.object({
|
|
|
3480
5090
|
});
|
|
3481
5091
|
export type RecordingFailedPayload = z.infer<typeof RecordingFailedPayload>;
|
|
3482
5092
|
|
|
3483
|
-
// ──
|
|
5093
|
+
// ── Structured sandbox services ─────────────────────────────────────────────
|
|
3484
5094
|
// Two transports on one spine: the A2 request/response shapes (FsNode tree,
|
|
3485
5095
|
// GitDiff hunks, terminal exec) are returned INLINE on synchronous API-direct
|
|
3486
5096
|
// routes (never the bus); the A1 notification payloads below ride the durable
|
|
@@ -3647,7 +5257,9 @@ export const FsDeleteRequest = z.object({
|
|
|
3647
5257
|
recursive: z.boolean().default(false), // required true to delete a non-empty dir
|
|
3648
5258
|
});
|
|
3649
5259
|
export type FsDeleteRequest = z.infer<typeof FsDeleteRequest>;
|
|
3650
|
-
export const FsDeleteResponse = z.object({
|
|
5260
|
+
export const FsDeleteResponse = z.object({
|
|
5261
|
+
revision: z.number().int().nonnegative(),
|
|
5262
|
+
});
|
|
3651
5263
|
export type FsDeleteResponse = z.infer<typeof FsDeleteResponse>;
|
|
3652
5264
|
|
|
3653
5265
|
export const FsMoveRequest = z.object({
|
|
@@ -3748,6 +5360,9 @@ export const GitDiffRequest = z.object({
|
|
|
3748
5360
|
path: z.string().default(""), // repo root
|
|
3749
5361
|
// diff selectors, mutually exclusive precedence: refs > staged > worktree
|
|
3750
5362
|
staged: z.boolean().default(false), // --cached (index vs HEAD)
|
|
5363
|
+
// Workspace review includes after-images that ordinary `git diff` omits.
|
|
5364
|
+
// Explicit so commit/staged consumers keep native Git semantics by default.
|
|
5365
|
+
includeUntracked: z.boolean().default(false),
|
|
3751
5366
|
fromRef: z.string().optional(),
|
|
3752
5367
|
toRef: z.string().optional(),
|
|
3753
5368
|
pathspec: z.array(z.string()).default([]),
|
|
@@ -3766,7 +5381,7 @@ export const GitDiffResponse = z.object({
|
|
|
3766
5381
|
});
|
|
3767
5382
|
export type GitDiffResponse = z.infer<typeof GitDiffResponse>;
|
|
3768
5383
|
|
|
3769
|
-
// ─── Workbench v2 turn-end workspace capture
|
|
5384
|
+
// ─── Workbench v2 turn-end workspace capture ────────────
|
|
3770
5385
|
// A capture is a point-in-time snapshot of the session workspace's CHANGES,
|
|
3771
5386
|
// probed live off the box at turn end (detectRepos → gitStatus/gitDiff → fsRead
|
|
3772
5387
|
// after-images → fsList tree index). It is the cold/offline read source that
|
|
@@ -3783,7 +5398,7 @@ export const WorkspaceCaptureFile = z.object({
|
|
|
3783
5398
|
status: GitFileStatusCode,
|
|
3784
5399
|
// sha256 of the captured after-image bytes; null when deleted / tooLarge.
|
|
3785
5400
|
hash: z.string().nullable(),
|
|
3786
|
-
// git blob sha of the HEAD version — the wake-on-edit flush guard (
|
|
5401
|
+
// git blob sha of the HEAD version — the wake-on-edit flush guard (design
|
|
3787
5402
|
// §10.1). null when the path is new/untracked (no HEAD blob).
|
|
3788
5403
|
baseHash: z.string().nullable(),
|
|
3789
5404
|
// Content-addressed storage key of the after-image; null when deleted /
|
|
@@ -3821,7 +5436,7 @@ export const WorkspaceCaptureDegradedReason = z.enum([
|
|
|
3821
5436
|
export type WorkspaceCaptureDegradedReason = z.infer<typeof WorkspaceCaptureDegradedReason>;
|
|
3822
5437
|
|
|
3823
5438
|
// Rollup counters — carried on the row (jsonb) and the announce event so the UI
|
|
3824
|
-
// can reserve layout (
|
|
5439
|
+
// can reserve layout (no layout shift) before fetching the manifest.
|
|
3825
5440
|
export const WorkspaceCaptureStats = z.object({
|
|
3826
5441
|
repoCount: z.number().int().nonnegative(),
|
|
3827
5442
|
fileCount: z.number().int().nonnegative(),
|
|
@@ -3858,7 +5473,7 @@ export const WorkspaceCaptureManifest = z.object({
|
|
|
3858
5473
|
});
|
|
3859
5474
|
export type WorkspaceCaptureManifest = z.infer<typeof WorkspaceCaptureManifest>;
|
|
3860
5475
|
|
|
3861
|
-
// Announce-only event payload
|
|
5476
|
+
// Announce-only event payload. Metadata only — never content.
|
|
3862
5477
|
export const WorkspaceRevisionCapturedPayload = z.object({
|
|
3863
5478
|
revision: z.number().int().nonnegative(),
|
|
3864
5479
|
turnId: z.string().nullable(),
|
|
@@ -3877,7 +5492,7 @@ export const WorkspaceRevisionDegradedPayload = z.object({
|
|
|
3877
5492
|
});
|
|
3878
5493
|
export type WorkspaceRevisionDegradedPayload = z.infer<typeof WorkspaceRevisionDegradedPayload>;
|
|
3879
5494
|
|
|
3880
|
-
// --- M2 capture READ API
|
|
5495
|
+
// --- M2 capture READ API -------------------------------------
|
|
3881
5496
|
// A short-TTL signed GET URL minted PER REQUEST (never stored). The manifest is
|
|
3882
5497
|
// served inline for the ≤2MB common case (the <200ms one-round-trip paint); a
|
|
3883
5498
|
// >2MB manifest and a >256KB single-file after-image fall back to one of these.
|
|
@@ -3954,14 +5569,25 @@ export const GitCommit = z.object({
|
|
|
3954
5569
|
sha: z.string(),
|
|
3955
5570
|
shortSha: z.string(),
|
|
3956
5571
|
parents: z.array(z.string()),
|
|
3957
|
-
author: z.object({
|
|
3958
|
-
|
|
5572
|
+
author: z.object({
|
|
5573
|
+
name: z.string(),
|
|
5574
|
+
email: z.string(),
|
|
5575
|
+
timestamp: z.number().int(),
|
|
5576
|
+
}),
|
|
5577
|
+
committer: z.object({
|
|
5578
|
+
name: z.string(),
|
|
5579
|
+
email: z.string(),
|
|
5580
|
+
timestamp: z.number().int(),
|
|
5581
|
+
}),
|
|
3959
5582
|
subject: z.string(),
|
|
3960
5583
|
body: z.string(),
|
|
3961
5584
|
refs: z.array(z.string()).default([]), // decorations: branch/tag pointers
|
|
3962
5585
|
});
|
|
3963
5586
|
export type GitCommit = z.infer<typeof GitCommit>;
|
|
3964
|
-
export const GitLogResponse = z.object({
|
|
5587
|
+
export const GitLogResponse = z.object({
|
|
5588
|
+
commits: z.array(GitCommit),
|
|
5589
|
+
hasMore: z.boolean(),
|
|
5590
|
+
});
|
|
3965
5591
|
export type GitLogResponse = z.infer<typeof GitLogResponse>;
|
|
3966
5592
|
|
|
3967
5593
|
export const GitShowRequest = z.object({
|
|
@@ -4033,7 +5659,10 @@ export const PtyOpenResponse = z.object({
|
|
|
4033
5659
|
supportsInput: z.boolean(), // false on backends without writeStdin
|
|
4034
5660
|
});
|
|
4035
5661
|
export type PtyOpenResponse = z.infer<typeof PtyOpenResponse>;
|
|
4036
|
-
export const PtyWriteRequest = z.object({
|
|
5662
|
+
export const PtyWriteRequest = z.object({
|
|
5663
|
+
ptyId: z.string().uuid(),
|
|
5664
|
+
data: z.string(),
|
|
5665
|
+
}); // utf-8 stdin
|
|
4037
5666
|
export type PtyWriteRequest = z.infer<typeof PtyWriteRequest>;
|
|
4038
5667
|
export const PtyResizeRequest = z.object({
|
|
4039
5668
|
ptyId: z.string().uuid(),
|
|
@@ -4048,7 +5677,11 @@ export type PtyCloseRequest = z.infer<typeof PtyCloseRequest>;
|
|
|
4048
5677
|
// negotiation). The full SessionCapabilities doc already carries FileSystem /
|
|
4049
5678
|
// Terminal / Git blocks (P0.1); this is the compact projection the SDK mirrors.
|
|
4050
5679
|
export const SessionStructuredCapabilities = z.object({
|
|
4051
|
-
FileSystem: z.object({
|
|
5680
|
+
FileSystem: z.object({
|
|
5681
|
+
available: z.boolean(),
|
|
5682
|
+
readOnly: z.boolean(),
|
|
5683
|
+
root: z.string(),
|
|
5684
|
+
}),
|
|
4052
5685
|
Terminal: z.object({
|
|
4053
5686
|
events: z.boolean(), // command.output firehose (always on if a box exists)
|
|
4054
5687
|
exec: z.boolean(), // synchronous terminal exec
|
|
@@ -4066,39 +5699,644 @@ export const SessionEvent = z.object({
|
|
|
4066
5699
|
type: SessionEventType,
|
|
4067
5700
|
payload: z.unknown().default({}),
|
|
4068
5701
|
occurredAt: z.string(),
|
|
4069
|
-
clientEventId:
|
|
5702
|
+
clientEventId: SessionOperationKey.nullable().optional(),
|
|
4070
5703
|
turnId: z.string().uuid().nullable().optional(),
|
|
4071
5704
|
turnGeneration: z.number().int().nonnegative().nullable().optional(),
|
|
4072
5705
|
turnAttemptId: z.string().uuid().nullable().optional(),
|
|
4073
5706
|
turnAssociation: z.enum(["current", "late_rejected", "duplicate"]).nullable().optional(),
|
|
4074
5707
|
duplicateOfEventId: z.string().uuid().nullable().optional(),
|
|
4075
|
-
duplicateReason: z.string().min(1).nullable().optional(),
|
|
5708
|
+
duplicateReason: z.string().min(1).max(1024).nullable().optional(),
|
|
4076
5709
|
});
|
|
4077
5710
|
export type SessionEvent = z.infer<typeof SessionEvent>;
|
|
4078
5711
|
|
|
5712
|
+
// --- Durable host export ------------------------------------------------------
|
|
5713
|
+
|
|
5714
|
+
/** Wire revision for the durable host event/usage export stream. */
|
|
5715
|
+
export const OPENGENI_HOST_EXPORT_SCHEMA_REVISION = "2026-07-host-export-v1" as const;
|
|
5716
|
+
|
|
5717
|
+
/**
|
|
5718
|
+
* Decimal string rather than a JavaScript number: export cursors are PostgreSQL
|
|
5719
|
+
* bigint values and must remain exact beyond Number.MAX_SAFE_INTEGER.
|
|
5720
|
+
*/
|
|
5721
|
+
export const HostExportCursor = z.string().regex(/^(0|[1-9][0-9]*)$/);
|
|
5722
|
+
export type HostExportCursor = z.infer<typeof HostExportCursor>;
|
|
5723
|
+
|
|
5724
|
+
export const HostExportConsumerId = z
|
|
5725
|
+
.string()
|
|
5726
|
+
.min(1)
|
|
5727
|
+
.max(128)
|
|
5728
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
|
|
5729
|
+
export type HostExportConsumerId = z.infer<typeof HostExportConsumerId>;
|
|
5730
|
+
|
|
5731
|
+
export const HostExportInitiator = TurnInitiator.extend({
|
|
5732
|
+
subjectId: z.string().min(1).max(1024),
|
|
5733
|
+
label: z.string().min(1).max(256).optional(),
|
|
5734
|
+
});
|
|
5735
|
+
export type HostExportInitiator = z.infer<typeof HostExportInitiator>;
|
|
5736
|
+
|
|
5737
|
+
export const HostExportInitiatorContext = TurnInitiatorContext.refine(
|
|
5738
|
+
(value) => {
|
|
5739
|
+
try {
|
|
5740
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength <= 4096;
|
|
5741
|
+
} catch {
|
|
5742
|
+
return false;
|
|
5743
|
+
}
|
|
5744
|
+
},
|
|
5745
|
+
{ message: "Host export initiator context exceeds 4096 UTF-8 bytes" },
|
|
5746
|
+
);
|
|
5747
|
+
export type HostExportInitiatorContext = z.infer<typeof HostExportInitiatorContext>;
|
|
5748
|
+
|
|
5749
|
+
const HostExportAttribution = {
|
|
5750
|
+
initiator: HostExportInitiator.nullable(),
|
|
5751
|
+
initiatorContext: HostExportInitiatorContext,
|
|
5752
|
+
origin: SessionTurnSource.nullable(),
|
|
5753
|
+
} as const;
|
|
5754
|
+
|
|
5755
|
+
/**
|
|
5756
|
+
* Host streams are deliberately forward-tolerant across rolling upgrades.
|
|
5757
|
+
* OpenGeni's application contract enumerates the event types known to this
|
|
5758
|
+
* build, while the durable export may be read by an older host consumer after
|
|
5759
|
+
* a newer writer has committed a bounded type. The database remains the
|
|
5760
|
+
* authority for the byte bounds on these persisted strings.
|
|
5761
|
+
*/
|
|
5762
|
+
export const HostSessionEvent = SessionEvent.extend({
|
|
5763
|
+
type: z.string().min(1).max(256),
|
|
5764
|
+
clientEventId: z.string().max(1024).nullable().optional(),
|
|
5765
|
+
turnAssociation: z.string().min(1).max(64).nullable().optional(),
|
|
5766
|
+
duplicateReason: z.string().max(4096).nullable().optional(),
|
|
5767
|
+
});
|
|
5768
|
+
export type HostSessionEvent = z.infer<typeof HostSessionEvent>;
|
|
5769
|
+
|
|
5770
|
+
/** Export-bounded usage fact; custom bounded metric names remain supported. */
|
|
5771
|
+
export const HostUsageEvent = UsageEvent.extend({
|
|
5772
|
+
subjectId: z.string().max(1024).nullable(),
|
|
5773
|
+
eventType: z.string().min(1).max(256),
|
|
5774
|
+
unit: z.string().min(1).max(128),
|
|
5775
|
+
sourceResourceType: z.string().max(256).nullable(),
|
|
5776
|
+
sourceResourceId: z.string().max(2048).nullable(),
|
|
5777
|
+
idempotencyKey: z.string().min(1).max(2048),
|
|
5778
|
+
billingProviderEventId: z.string().max(2048).nullable(),
|
|
5779
|
+
});
|
|
5780
|
+
export type HostUsageEvent = z.infer<typeof HostUsageEvent>;
|
|
5781
|
+
|
|
5782
|
+
/**
|
|
5783
|
+
* One immutable, bounded session-event snapshot from the transactional host
|
|
5784
|
+
* outbox. Cross-session cursor order is stable but deliberately non-causal;
|
|
5785
|
+
* within a session, `event.sequence` remains authoritative and monotonic.
|
|
5786
|
+
*/
|
|
5787
|
+
export const HostEventExport = z.object({
|
|
5788
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
5789
|
+
cursor: HostExportCursor,
|
|
5790
|
+
idempotencyKey: z.string().min(1).max(2048),
|
|
5791
|
+
accountId: z.string().uuid(),
|
|
5792
|
+
workspaceId: z.string().uuid(),
|
|
5793
|
+
/**
|
|
5794
|
+
* Immutable root of event.sessionId's session lineage at capture time. Null
|
|
5795
|
+
* only for an unresolved pre-lineage/legacy export row.
|
|
5796
|
+
*/
|
|
5797
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
5798
|
+
...HostExportAttribution,
|
|
5799
|
+
event: HostSessionEvent,
|
|
5800
|
+
});
|
|
5801
|
+
export type HostEventExport = z.infer<typeof HostEventExport>;
|
|
5802
|
+
|
|
5803
|
+
/** One exact, idempotency-keyed usage fact from the same ordered outbox. */
|
|
5804
|
+
export const HostUsageExport = z.object({
|
|
5805
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
5806
|
+
cursor: HostExportCursor,
|
|
5807
|
+
accountId: z.string().uuid(),
|
|
5808
|
+
workspaceId: z.string().uuid(),
|
|
5809
|
+
sessionId: z.string().uuid().nullable(),
|
|
5810
|
+
/** Null when sessionId is null or an unresolved pre-lineage legacy row. */
|
|
5811
|
+
rootSessionId: z.string().uuid().nullable(),
|
|
5812
|
+
turnId: z.string().uuid().nullable(),
|
|
5813
|
+
turnAttemptId: z.string().uuid().nullable(),
|
|
5814
|
+
...HostExportAttribution,
|
|
5815
|
+
usage: HostUsageEvent,
|
|
5816
|
+
});
|
|
5817
|
+
export type HostUsageExport = z.infer<typeof HostUsageExport>;
|
|
5818
|
+
|
|
5819
|
+
export const HostEventExportBatch = z.object({
|
|
5820
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
5821
|
+
consumerId: HostExportConsumerId,
|
|
5822
|
+
leaseToken: z.string().uuid(),
|
|
5823
|
+
checkpoint: HostExportCursor,
|
|
5824
|
+
throughCursor: HostExportCursor,
|
|
5825
|
+
events: z.array(HostEventExport).min(1).max(256),
|
|
5826
|
+
});
|
|
5827
|
+
export type HostEventExportBatch = z.infer<typeof HostEventExportBatch>;
|
|
5828
|
+
|
|
5829
|
+
export const HostUsageExportBatch = z.object({
|
|
5830
|
+
schemaRevision: z.literal(OPENGENI_HOST_EXPORT_SCHEMA_REVISION),
|
|
5831
|
+
consumerId: HostExportConsumerId,
|
|
5832
|
+
leaseToken: z.string().uuid(),
|
|
5833
|
+
checkpoint: HostExportCursor,
|
|
5834
|
+
throughCursor: HostExportCursor,
|
|
5835
|
+
events: z.array(HostUsageExport).min(1).max(256),
|
|
5836
|
+
});
|
|
5837
|
+
export type HostUsageExportBatch = z.infer<typeof HostUsageExportBatch>;
|
|
5838
|
+
|
|
5839
|
+
/**
|
|
5840
|
+
* Optional embedded-host sinks. Delivery is at least once: the same batch may
|
|
5841
|
+
* be repeated after a process dies between sink success and checkpoint commit,
|
|
5842
|
+
* so sinks must deduplicate by event/usage idempotency key.
|
|
5843
|
+
*/
|
|
5844
|
+
export type HostEventSink = {
|
|
5845
|
+
consumerId: HostExportConsumerId;
|
|
5846
|
+
deliverEvents: (batch: HostEventExportBatch) => Promise<void>;
|
|
5847
|
+
};
|
|
5848
|
+
|
|
5849
|
+
export type HostUsageSink = {
|
|
5850
|
+
consumerId: HostExportConsumerId;
|
|
5851
|
+
deliverUsage: (batch: HostUsageExportBatch) => Promise<void>;
|
|
5852
|
+
};
|
|
5853
|
+
|
|
5854
|
+
export const SESSION_EVENT_TYPE_MAX_BYTES = 256;
|
|
5855
|
+
export const SESSION_EVENT_CLIENT_EVENT_ID_MAX_BYTES = SESSION_OPERATION_KEY_MAX_CHARS * 4;
|
|
5856
|
+
export const SESSION_EVENT_TURN_ASSOCIATION_MAX_BYTES = 64;
|
|
5857
|
+
export const SESSION_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
|
|
5858
|
+
export const SESSION_EVENT_ENVELOPE_MAX_BYTES = 80 * 1024;
|
|
5859
|
+
|
|
5860
|
+
export type BoundSessionEventOptions = {
|
|
5861
|
+
surface?: SessionEventBoundarySurface;
|
|
5862
|
+
maxBytes?: number;
|
|
5863
|
+
};
|
|
5864
|
+
|
|
5865
|
+
/**
|
|
5866
|
+
* Canonical lossy projection for a complete session event. Payload bounds alone
|
|
5867
|
+
* are insufficient: a malformed retained row can also carry an oversized type,
|
|
5868
|
+
* client id, or duplicate diagnostic. Keep cursor/UUID identity intact, bound
|
|
5869
|
+
* every free-form envelope string, and assert the exact final JSON envelope.
|
|
5870
|
+
*/
|
|
5871
|
+
export function boundSessionEvent(
|
|
5872
|
+
event: SessionEvent,
|
|
5873
|
+
options: BoundSessionEventOptions = {},
|
|
5874
|
+
): SessionEvent {
|
|
5875
|
+
const surface = options.surface ?? "durable_audit";
|
|
5876
|
+
const maxBytes = Math.max(8 * 1024, options.maxBytes ?? SESSION_EVENT_ENVELOPE_MAX_BYTES);
|
|
5877
|
+
// Never stringify the untrusted complete event. Measurement has one global
|
|
5878
|
+
// work budget and never invokes accessors/custom toJSON; serialization is
|
|
5879
|
+
// permitted only after the compact projection below has been constructed.
|
|
5880
|
+
const originalBytes = measureSessionEventJson(event).bytes;
|
|
5881
|
+
const source = sessionEventOwnDataFields(event);
|
|
5882
|
+
const id = canonicalSessionEventUuid(source.id, SESSION_EVENT_ZERO_UUID);
|
|
5883
|
+
const workspaceId = canonicalSessionEventUuid(source.workspaceId, SESSION_EVENT_ZERO_UUID);
|
|
5884
|
+
const sessionId = canonicalSessionEventUuid(source.sessionId, SESSION_EVENT_ZERO_UUID);
|
|
5885
|
+
const sequence =
|
|
5886
|
+
source.sequence.readable &&
|
|
5887
|
+
typeof source.sequence.value === "number" &&
|
|
5888
|
+
Number.isSafeInteger(source.sequence.value) &&
|
|
5889
|
+
source.sequence.value > 0
|
|
5890
|
+
? source.sequence.value
|
|
5891
|
+
: 1;
|
|
5892
|
+
const occurredAt =
|
|
5893
|
+
source.occurredAt.readable &&
|
|
5894
|
+
typeof source.occurredAt.value === "string" &&
|
|
5895
|
+
sessionEventUtf8Bytes(source.occurredAt.value) <= 256
|
|
5896
|
+
? source.occurredAt.value
|
|
5897
|
+
: "1970-01-01T00:00:00.000Z";
|
|
5898
|
+
const rawType = source.type.readable ? source.type.value : undefined;
|
|
5899
|
+
const typeIsSafe =
|
|
5900
|
+
typeof rawType === "string" &&
|
|
5901
|
+
sessionEventUtf8Bytes(rawType) <= SESSION_EVENT_TYPE_MAX_BYTES &&
|
|
5902
|
+
!rawType.includes("\n") &&
|
|
5903
|
+
!rawType.includes("\r");
|
|
5904
|
+
const rawClientEventId = source.clientEventId.readable ? source.clientEventId.value : undefined;
|
|
5905
|
+
const clientEventId = boundOptionalSessionEventText(
|
|
5906
|
+
typeof rawClientEventId === "string" || rawClientEventId === null
|
|
5907
|
+
? rawClientEventId
|
|
5908
|
+
: undefined,
|
|
5909
|
+
SESSION_EVENT_CLIENT_EVENT_ID_MAX_BYTES,
|
|
5910
|
+
);
|
|
5911
|
+
const rawTurnAssociation = source.turnAssociation.readable
|
|
5912
|
+
? source.turnAssociation.value
|
|
5913
|
+
: undefined;
|
|
5914
|
+
const turnAssociation =
|
|
5915
|
+
rawTurnAssociation === null ||
|
|
5916
|
+
rawTurnAssociation === undefined ||
|
|
5917
|
+
rawTurnAssociation === "current" ||
|
|
5918
|
+
rawTurnAssociation === "late_rejected" ||
|
|
5919
|
+
rawTurnAssociation === "duplicate"
|
|
5920
|
+
? rawTurnAssociation
|
|
5921
|
+
: null;
|
|
5922
|
+
const rawDuplicateReason = source.duplicateReason.readable
|
|
5923
|
+
? source.duplicateReason.value
|
|
5924
|
+
: undefined;
|
|
5925
|
+
const duplicateReason = boundOptionalSessionEventText(
|
|
5926
|
+
typeof rawDuplicateReason === "string" || rawDuplicateReason === null
|
|
5927
|
+
? rawDuplicateReason
|
|
5928
|
+
: undefined,
|
|
5929
|
+
SESSION_EVENT_DUPLICATE_REASON_MAX_BYTES,
|
|
5930
|
+
);
|
|
5931
|
+
const turnId = canonicalOptionalSessionEventUuid(source.turnId);
|
|
5932
|
+
const turnGeneration = canonicalSessionEventGeneration(source.turnGeneration);
|
|
5933
|
+
const turnAttemptId = canonicalOptionalSessionEventUuid(source.turnAttemptId);
|
|
5934
|
+
const duplicateOfEventId = canonicalOptionalSessionEventUuid(source.duplicateOfEventId);
|
|
5935
|
+
const envelopeFields = [
|
|
5936
|
+
sessionEventCustomSerializerProjection(event),
|
|
5937
|
+
sessionEventAdditionalTopLevelFieldProjection(event),
|
|
5938
|
+
!typeIsSafe
|
|
5939
|
+
? sessionEventEnvelopeFieldProjection(
|
|
5940
|
+
"type",
|
|
5941
|
+
rawType,
|
|
5942
|
+
"session.event.envelope_omitted",
|
|
5943
|
+
source.type.readable,
|
|
5944
|
+
)
|
|
5945
|
+
: null,
|
|
5946
|
+
!source.clientEventId.readable || rawClientEventId !== clientEventId
|
|
5947
|
+
? sessionEventEnvelopeFieldProjection(
|
|
5948
|
+
"clientEventId",
|
|
5949
|
+
rawClientEventId,
|
|
5950
|
+
clientEventId,
|
|
5951
|
+
source.clientEventId.readable,
|
|
5952
|
+
)
|
|
5953
|
+
: null,
|
|
5954
|
+
!source.turnAssociation.readable || rawTurnAssociation !== turnAssociation
|
|
5955
|
+
? sessionEventEnvelopeFieldProjection(
|
|
5956
|
+
"turnAssociation",
|
|
5957
|
+
rawTurnAssociation,
|
|
5958
|
+
turnAssociation,
|
|
5959
|
+
source.turnAssociation.readable,
|
|
5960
|
+
)
|
|
5961
|
+
: null,
|
|
5962
|
+
!source.duplicateReason.readable || rawDuplicateReason !== duplicateReason
|
|
5963
|
+
? sessionEventEnvelopeFieldProjection(
|
|
5964
|
+
"duplicateReason",
|
|
5965
|
+
rawDuplicateReason,
|
|
5966
|
+
duplicateReason,
|
|
5967
|
+
source.duplicateReason.readable,
|
|
5968
|
+
)
|
|
5969
|
+
: null,
|
|
5970
|
+
...sessionEventCanonicalFieldProjections(source, {
|
|
5971
|
+
id,
|
|
5972
|
+
workspaceId,
|
|
5973
|
+
sessionId,
|
|
5974
|
+
sequence,
|
|
5975
|
+
occurredAt,
|
|
5976
|
+
}),
|
|
5977
|
+
...sessionEventOptionalFieldProjections(source, {
|
|
5978
|
+
turnId,
|
|
5979
|
+
turnGeneration,
|
|
5980
|
+
turnAttemptId,
|
|
5981
|
+
duplicateOfEventId,
|
|
5982
|
+
}),
|
|
5983
|
+
!source.payload.readable
|
|
5984
|
+
? sessionEventEnvelopeFieldProjection("payload", undefined, null, false)
|
|
5985
|
+
: null,
|
|
5986
|
+
].filter((field) => field !== null);
|
|
5987
|
+
const rawPayload = source.payload.readable
|
|
5988
|
+
? source.payload.value
|
|
5989
|
+
: "[event payload accessor omitted at bounded projection boundary]";
|
|
5990
|
+
const payload =
|
|
5991
|
+
envelopeFields.length === 0
|
|
5992
|
+
? boundSessionEventPayload(rawPayload, { surface })
|
|
5993
|
+
: boundSessionEventPayload(
|
|
5994
|
+
{
|
|
5995
|
+
preview: "[legacy event envelope normalized at bounded projection boundary]",
|
|
5996
|
+
originalEventBytes: originalBytes,
|
|
5997
|
+
originalType: typeof rawType === "string" ? boundSessionEventText(rawType, 256) : null,
|
|
5998
|
+
envelopeProjection: {
|
|
5999
|
+
truncated: true,
|
|
6000
|
+
surface,
|
|
6001
|
+
fields: envelopeFields,
|
|
6002
|
+
},
|
|
6003
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
6004
|
+
},
|
|
6005
|
+
{ surface, maxBytes: 8 * 1024 },
|
|
6006
|
+
);
|
|
6007
|
+
const bounded: SessionEvent = {
|
|
6008
|
+
id,
|
|
6009
|
+
workspaceId,
|
|
6010
|
+
sessionId,
|
|
6011
|
+
sequence,
|
|
6012
|
+
type: typeIsSafe ? (rawType as SessionEvent["type"]) : "session.event.envelope_omitted",
|
|
6013
|
+
payload,
|
|
6014
|
+
occurredAt,
|
|
6015
|
+
...(sessionEventShouldEmitOptionalField(source.clientEventId) ? { clientEventId } : {}),
|
|
6016
|
+
...(sessionEventShouldEmitOptionalField(source.turnId) ? { turnId } : {}),
|
|
6017
|
+
...(sessionEventShouldEmitOptionalField(source.turnGeneration) ? { turnGeneration } : {}),
|
|
6018
|
+
...(sessionEventShouldEmitOptionalField(source.turnAttemptId) ? { turnAttemptId } : {}),
|
|
6019
|
+
...(sessionEventShouldEmitOptionalField(source.turnAssociation) ? { turnAssociation } : {}),
|
|
6020
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateOfEventId)
|
|
6021
|
+
? { duplicateOfEventId }
|
|
6022
|
+
: {}),
|
|
6023
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateReason) ? { duplicateReason } : {}),
|
|
6024
|
+
};
|
|
6025
|
+
if (sessionEventJsonBytes(bounded) <= maxBytes) return bounded;
|
|
6026
|
+
|
|
6027
|
+
const fallback: SessionEvent = {
|
|
6028
|
+
id,
|
|
6029
|
+
workspaceId,
|
|
6030
|
+
sessionId,
|
|
6031
|
+
sequence,
|
|
6032
|
+
type: "session.event.envelope_omitted",
|
|
6033
|
+
payload: boundSessionEventPayload(
|
|
6034
|
+
{
|
|
6035
|
+
preview: "[legacy event envelope omitted at bounded projection boundary]",
|
|
6036
|
+
originalEventBytes: originalBytes,
|
|
6037
|
+
originalType: typeof rawType === "string" ? boundSessionEventText(rawType, 256) : null,
|
|
6038
|
+
fullEvidence: { available: false, reason: "not_retained" },
|
|
6039
|
+
},
|
|
6040
|
+
{ surface, maxBytes: 4 * 1024 },
|
|
6041
|
+
),
|
|
6042
|
+
occurredAt,
|
|
6043
|
+
...(sessionEventShouldEmitOptionalField(source.clientEventId) ? { clientEventId } : {}),
|
|
6044
|
+
...(sessionEventShouldEmitOptionalField(source.turnId) ? { turnId } : {}),
|
|
6045
|
+
...(sessionEventShouldEmitOptionalField(source.turnGeneration) ? { turnGeneration } : {}),
|
|
6046
|
+
...(sessionEventShouldEmitOptionalField(source.turnAttemptId) ? { turnAttemptId } : {}),
|
|
6047
|
+
...(sessionEventShouldEmitOptionalField(source.turnAssociation) ? { turnAssociation } : {}),
|
|
6048
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateOfEventId)
|
|
6049
|
+
? { duplicateOfEventId }
|
|
6050
|
+
: {}),
|
|
6051
|
+
...(sessionEventShouldEmitOptionalField(source.duplicateReason) ? { duplicateReason } : {}),
|
|
6052
|
+
};
|
|
6053
|
+
const deliveredBytes = sessionEventJsonBytes(fallback);
|
|
6054
|
+
if (deliveredBytes > maxBytes) {
|
|
6055
|
+
throw new RangeError(
|
|
6056
|
+
`Bounded session event exceeds its final envelope (${deliveredBytes} > ${maxBytes} bytes)`,
|
|
6057
|
+
);
|
|
6058
|
+
}
|
|
6059
|
+
return fallback;
|
|
6060
|
+
}
|
|
6061
|
+
|
|
6062
|
+
function sessionEventEnvelopeFieldProjection(
|
|
6063
|
+
field: string,
|
|
6064
|
+
original: unknown,
|
|
6065
|
+
delivered: unknown,
|
|
6066
|
+
originalReadable = true,
|
|
6067
|
+
): { field: string; originalBytes: number | null; deliveredBytes: number } {
|
|
6068
|
+
return {
|
|
6069
|
+
field,
|
|
6070
|
+
originalBytes: originalReadable
|
|
6071
|
+
? typeof original === "string"
|
|
6072
|
+
? sessionEventUtf8Bytes(original)
|
|
6073
|
+
: typeof original === "number" || typeof original === "boolean"
|
|
6074
|
+
? sessionEventJsonBytes(original)
|
|
6075
|
+
: original === null || original === undefined
|
|
6076
|
+
? 0
|
|
6077
|
+
: null
|
|
6078
|
+
: null,
|
|
6079
|
+
deliveredBytes:
|
|
6080
|
+
typeof delivered === "string"
|
|
6081
|
+
? sessionEventUtf8Bytes(delivered)
|
|
6082
|
+
: typeof delivered === "number" || typeof delivered === "boolean"
|
|
6083
|
+
? sessionEventJsonBytes(delivered)
|
|
6084
|
+
: 0,
|
|
6085
|
+
};
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
function sessionEventCustomSerializerProjection(
|
|
6089
|
+
event: SessionEvent,
|
|
6090
|
+
): { field: string; originalBytes: null; deliveredBytes: 0 } | null {
|
|
6091
|
+
const projection = {
|
|
6092
|
+
field: "toJSON",
|
|
6093
|
+
originalBytes: null,
|
|
6094
|
+
deliveredBytes: 0,
|
|
6095
|
+
} as const;
|
|
6096
|
+
let candidate: object | null = event;
|
|
6097
|
+
try {
|
|
6098
|
+
for (let depth = 0; depth <= SESSION_EVENT_PROTOTYPE_MAX_DEPTH; depth += 1) {
|
|
6099
|
+
if (candidate === null) return null;
|
|
6100
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, "toJSON");
|
|
6101
|
+
if (descriptor) {
|
|
6102
|
+
// JSON.stringify performs an ordinary lookup, so an accessor is both
|
|
6103
|
+
// executable behavior and an unknown possible serializer. A data
|
|
6104
|
+
// property shadows the rest of the chain and is relevant only when it
|
|
6105
|
+
// is callable.
|
|
6106
|
+
return !("value" in descriptor) || typeof descriptor.value === "function"
|
|
6107
|
+
? projection
|
|
6108
|
+
: null;
|
|
6109
|
+
}
|
|
6110
|
+
candidate = Object.getPrototypeOf(candidate);
|
|
6111
|
+
}
|
|
6112
|
+
// A hostile or malformed prototype chain that exceeds the fixed lookup
|
|
6113
|
+
// budget cannot prove the absence of inherited serialization behavior.
|
|
6114
|
+
return projection;
|
|
6115
|
+
} catch {
|
|
6116
|
+
return projection;
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
|
|
6120
|
+
const SESSION_EVENT_PROTOTYPE_MAX_DEPTH = 32;
|
|
6121
|
+
const SESSION_EVENT_ZERO_UUID = "00000000-0000-4000-8000-000000000000";
|
|
6122
|
+
const SESSION_EVENT_UUID_PATTERN =
|
|
6123
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6124
|
+
|
|
6125
|
+
const SESSION_EVENT_OWN_DATA_FIELDS = [
|
|
6126
|
+
"id",
|
|
6127
|
+
"workspaceId",
|
|
6128
|
+
"sessionId",
|
|
6129
|
+
"sequence",
|
|
6130
|
+
"type",
|
|
6131
|
+
"payload",
|
|
6132
|
+
"occurredAt",
|
|
6133
|
+
"clientEventId",
|
|
6134
|
+
"turnId",
|
|
6135
|
+
"turnGeneration",
|
|
6136
|
+
"turnAttemptId",
|
|
6137
|
+
"turnAssociation",
|
|
6138
|
+
"duplicateOfEventId",
|
|
6139
|
+
"duplicateReason",
|
|
6140
|
+
] as const satisfies readonly (keyof SessionEvent)[];
|
|
6141
|
+
const SESSION_EVENT_KNOWN_ENUMERABLE_FIELDS = new Set<string>([
|
|
6142
|
+
...SESSION_EVENT_OWN_DATA_FIELDS,
|
|
6143
|
+
"toJSON",
|
|
6144
|
+
]);
|
|
6145
|
+
|
|
6146
|
+
/**
|
|
6147
|
+
* Detect future/legacy own enumerable envelope fields without reading their
|
|
6148
|
+
* values. There can be at most the fixed known-key cardinality before an
|
|
6149
|
+
* additional key must be observed, so the source-level iterator is bounded.
|
|
6150
|
+
* A proxy/enumeration failure is conservatively surfaced as unknown loss.
|
|
6151
|
+
*/
|
|
6152
|
+
function sessionEventAdditionalTopLevelFieldProjection(
|
|
6153
|
+
event: SessionEvent,
|
|
6154
|
+
): { field: string; originalBytes: null; deliveredBytes: 0 } | null {
|
|
6155
|
+
const projection = {
|
|
6156
|
+
field: "additionalTopLevelFields",
|
|
6157
|
+
originalBytes: null,
|
|
6158
|
+
deliveredBytes: 0,
|
|
6159
|
+
} as const;
|
|
6160
|
+
let inspected = 0;
|
|
6161
|
+
try {
|
|
6162
|
+
for (const key in event as SessionEvent & Record<string, unknown>) {
|
|
6163
|
+
inspected += 1;
|
|
6164
|
+
if (inspected > SESSION_EVENT_KNOWN_ENUMERABLE_FIELDS.size + 1) return projection;
|
|
6165
|
+
const descriptor = Object.getOwnPropertyDescriptor(event, key);
|
|
6166
|
+
if (descriptor?.enumerable && !SESSION_EVENT_KNOWN_ENUMERABLE_FIELDS.has(key)) {
|
|
6167
|
+
return projection;
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
return null;
|
|
6171
|
+
} catch {
|
|
6172
|
+
return projection;
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
type SessionEventOwnField = { readable: true; value: unknown } | { readable: false };
|
|
6177
|
+
type SessionEventOwnDataFields = Record<keyof SessionEvent, SessionEventOwnField>;
|
|
6178
|
+
|
|
6179
|
+
function sessionEventOwnDataFields(event: SessionEvent): SessionEventOwnDataFields {
|
|
6180
|
+
return Object.fromEntries(
|
|
6181
|
+
SESSION_EVENT_OWN_DATA_FIELDS.map((key) => {
|
|
6182
|
+
try {
|
|
6183
|
+
const descriptor = Object.getOwnPropertyDescriptor(event, key);
|
|
6184
|
+
if (!descriptor) return [key, { readable: true, value: undefined }];
|
|
6185
|
+
return [
|
|
6186
|
+
key,
|
|
6187
|
+
"value" in descriptor ? { readable: true, value: descriptor.value } : { readable: false },
|
|
6188
|
+
];
|
|
6189
|
+
} catch {
|
|
6190
|
+
return [key, { readable: false }];
|
|
6191
|
+
}
|
|
6192
|
+
}),
|
|
6193
|
+
) as SessionEventOwnDataFields;
|
|
6194
|
+
}
|
|
6195
|
+
|
|
6196
|
+
function canonicalSessionEventUuid(field: SessionEventOwnField, fallback: string): string {
|
|
6197
|
+
return field.readable &&
|
|
6198
|
+
typeof field.value === "string" &&
|
|
6199
|
+
SESSION_EVENT_UUID_PATTERN.test(field.value)
|
|
6200
|
+
? field.value
|
|
6201
|
+
: fallback;
|
|
6202
|
+
}
|
|
6203
|
+
|
|
6204
|
+
function canonicalOptionalSessionEventUuid(field: SessionEventOwnField): string | null {
|
|
6205
|
+
return field.readable &&
|
|
6206
|
+
typeof field.value === "string" &&
|
|
6207
|
+
SESSION_EVENT_UUID_PATTERN.test(field.value)
|
|
6208
|
+
? field.value
|
|
6209
|
+
: null;
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
function canonicalSessionEventGeneration(field: SessionEventOwnField): number | null {
|
|
6213
|
+
return field.readable &&
|
|
6214
|
+
typeof field.value === "number" &&
|
|
6215
|
+
Number.isSafeInteger(field.value) &&
|
|
6216
|
+
field.value >= 0
|
|
6217
|
+
? field.value
|
|
6218
|
+
: null;
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
function sessionEventShouldEmitOptionalField(field: SessionEventOwnField): boolean {
|
|
6222
|
+
return !field.readable || field.value !== undefined;
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6225
|
+
function sessionEventCanonicalFieldProjections(
|
|
6226
|
+
source: SessionEventOwnDataFields,
|
|
6227
|
+
delivered: {
|
|
6228
|
+
id: string;
|
|
6229
|
+
workspaceId: string;
|
|
6230
|
+
sessionId: string;
|
|
6231
|
+
sequence: number;
|
|
6232
|
+
occurredAt: string;
|
|
6233
|
+
},
|
|
6234
|
+
): Array<{
|
|
6235
|
+
field: string;
|
|
6236
|
+
originalBytes: number | null;
|
|
6237
|
+
deliveredBytes: number;
|
|
6238
|
+
}> {
|
|
6239
|
+
return (["id", "workspaceId", "sessionId", "sequence", "occurredAt"] as const).flatMap(
|
|
6240
|
+
(field) => {
|
|
6241
|
+
const original = source[field].readable ? source[field].value : undefined;
|
|
6242
|
+
return source[field].readable && original === delivered[field]
|
|
6243
|
+
? []
|
|
6244
|
+
: [
|
|
6245
|
+
sessionEventEnvelopeFieldProjection(
|
|
6246
|
+
field,
|
|
6247
|
+
original,
|
|
6248
|
+
delivered[field],
|
|
6249
|
+
source[field].readable,
|
|
6250
|
+
),
|
|
6251
|
+
];
|
|
6252
|
+
},
|
|
6253
|
+
);
|
|
6254
|
+
}
|
|
6255
|
+
|
|
6256
|
+
function sessionEventOptionalFieldProjections(
|
|
6257
|
+
source: SessionEventOwnDataFields,
|
|
6258
|
+
delivered: {
|
|
6259
|
+
turnId: string | null;
|
|
6260
|
+
turnGeneration: number | null;
|
|
6261
|
+
turnAttemptId: string | null;
|
|
6262
|
+
duplicateOfEventId: string | null;
|
|
6263
|
+
},
|
|
6264
|
+
): Array<{
|
|
6265
|
+
field: string;
|
|
6266
|
+
originalBytes: number | null;
|
|
6267
|
+
deliveredBytes: number;
|
|
6268
|
+
}> {
|
|
6269
|
+
return (["turnId", "turnGeneration", "turnAttemptId", "duplicateOfEventId"] as const).flatMap(
|
|
6270
|
+
(field) => {
|
|
6271
|
+
const original = source[field].readable ? source[field].value : undefined;
|
|
6272
|
+
const canonicalOriginal = original ?? null;
|
|
6273
|
+
return source[field].readable && canonicalOriginal === delivered[field]
|
|
6274
|
+
? []
|
|
6275
|
+
: [
|
|
6276
|
+
sessionEventEnvelopeFieldProjection(
|
|
6277
|
+
field,
|
|
6278
|
+
original,
|
|
6279
|
+
delivered[field],
|
|
6280
|
+
source[field].readable,
|
|
6281
|
+
),
|
|
6282
|
+
];
|
|
6283
|
+
},
|
|
6284
|
+
);
|
|
6285
|
+
}
|
|
6286
|
+
|
|
6287
|
+
function boundOptionalSessionEventText<T extends string | null | undefined>(
|
|
6288
|
+
value: T,
|
|
6289
|
+
maxBytes: number,
|
|
6290
|
+
): T {
|
|
6291
|
+
return (typeof value === "string" ? boundSessionEventText(value, maxBytes) : value) as T;
|
|
6292
|
+
}
|
|
6293
|
+
|
|
6294
|
+
function boundSessionEventText(value: string, maxBytes: number): string {
|
|
6295
|
+
const encoder = new TextEncoder();
|
|
6296
|
+
const decoder = new TextDecoder();
|
|
6297
|
+
const bytes = encoder.encode(value);
|
|
6298
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
6299
|
+
const marker = "…[truncated]";
|
|
6300
|
+
const markerBytes = encoder.encode(marker).byteLength;
|
|
6301
|
+
const prefixBudget = Math.max(0, maxBytes - markerBytes);
|
|
6302
|
+
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
6303
|
+
while (prefixEnd > 0 && prefixEnd < bytes.byteLength && (bytes[prefixEnd]! & 0xc0) === 0x80) {
|
|
6304
|
+
prefixEnd -= 1;
|
|
6305
|
+
}
|
|
6306
|
+
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
6307
|
+
}
|
|
6308
|
+
|
|
6309
|
+
function sessionEventUtf8Bytes(value: string): number {
|
|
6310
|
+
return new TextEncoder().encode(value).byteLength;
|
|
6311
|
+
}
|
|
6312
|
+
|
|
4079
6313
|
export const SessionQueueMutationResponse = z.object({
|
|
6314
|
+
receipt: SessionCommandReceipt,
|
|
4080
6315
|
snapshot: SessionQueueSnapshot,
|
|
4081
|
-
|
|
4082
|
-
shouldWake: z.boolean(),
|
|
6316
|
+
draft: ComposerDraft.optional(),
|
|
4083
6317
|
});
|
|
4084
6318
|
export type SessionQueueMutationResponse = z.infer<typeof SessionQueueMutationResponse>;
|
|
4085
6319
|
|
|
4086
6320
|
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(),
|
|
6321
|
+
receipt: SessionCommandReceipt,
|
|
6322
|
+
effectiveControl: EffectiveSessionControl,
|
|
6323
|
+
interruptionCount: z.number().int().nonnegative(),
|
|
6324
|
+
wakeCount: z.number().int().nonnegative(),
|
|
4097
6325
|
});
|
|
4098
6326
|
export type SessionControlResponse = z.infer<typeof SessionControlResponse>;
|
|
4099
6327
|
|
|
4100
6328
|
export const CreateSessionRequest = withVariableSetIdAlias({
|
|
6329
|
+
/**
|
|
6330
|
+
* Optional UUID preallocated by an embedding host. This lets the host durably
|
|
6331
|
+
* link its own projection before OpenGeni admits the initial turn. Replays
|
|
6332
|
+
* must pair it with the same idempotency key; OpenGeni never derives host
|
|
6333
|
+
* identity or authorization from the UUID.
|
|
6334
|
+
*/
|
|
6335
|
+
requestedSessionId: z.string().uuid().optional(),
|
|
4101
6336
|
initialMessage: z.string().min(1),
|
|
6337
|
+
// System-level host context for the initial turn only. Unlike `instructions`,
|
|
6338
|
+
// this does not persist into later turns and is never emitted as a user event.
|
|
6339
|
+
turnInstructions: z.string().trim().min(1).max(32768).optional(),
|
|
4102
6340
|
// Per-session agent persona/system instructions (org-visible metadata, NOT a
|
|
4103
6341
|
// secret). Rides the SAME system-level instructions channel the per-workspace
|
|
4104
6342
|
// agentInstructions rides, composed AFTER the workspace persona so it refines
|
|
@@ -4108,7 +6346,14 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4108
6346
|
// matches the codebase's largest free-form string convention (workspace
|
|
4109
6347
|
// variable set variable values). Absent ⇒ byte-identical to today.
|
|
4110
6348
|
instructions: z.string().trim().min(1).max(32768).optional(),
|
|
6349
|
+
// For an agent-created child, omission inherits the trusted immediate
|
|
6350
|
+
// parent's repository/file context. An explicit array, including [], is
|
|
6351
|
+
// authoritative. Top-level omission remains []. Presence is resolved from
|
|
6352
|
+
// the raw request because this Zod default erases absent-vs-empty.
|
|
4111
6353
|
resources: z.array(ResourceRef).default([]),
|
|
6354
|
+
// The same child omission rule applies to selected MCP tool refs. Top-level
|
|
6355
|
+
// omission still applies workspace-default capability MCP tools; explicit []
|
|
6356
|
+
// suppresses those defaults (the first-party OpenGeni server remains added).
|
|
4112
6357
|
tools: z.array(ToolRef).default([]),
|
|
4113
6358
|
metadata: z.record(z.string(), z.unknown()).default({}),
|
|
4114
6359
|
model: z.string().min(1).optional(),
|
|
@@ -4135,7 +6380,7 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4135
6380
|
// behavior). An id that does not name a rig in the workspace is a 422.
|
|
4136
6381
|
rigId: z.string().uuid().optional(),
|
|
4137
6382
|
goal: GoalSpec.optional(),
|
|
4138
|
-
clientEventId:
|
|
6383
|
+
clientEventId: SessionOperationKey.optional(),
|
|
4139
6384
|
// Workspace-scoped CREATE idempotency key: collapses concurrent/retried
|
|
4140
6385
|
// create calls carrying the same key to a single session (partial unique
|
|
4141
6386
|
// index on (workspace_id, create_idempotency_key)). Distinct from
|
|
@@ -4143,13 +6388,19 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4143
6388
|
// creation of a brand-new session. Absent means no create-dedup (each call
|
|
4144
6389
|
// is an independent create).
|
|
4145
6390
|
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
|
|
6391
|
+
// Permissions the session's first-party MCP token should carry. A top-level
|
|
6392
|
+
// omission uses the deployment's worker default; a child omission inherits
|
|
6393
|
+
// the creating session's effective grant. An explicit set is capped at
|
|
6394
|
+
// creation: every requested permission must be held by the creating grant.
|
|
6395
|
+
// A goal-bearing session whose explicit/effective set omits goals:manage is
|
|
6396
|
+
// rejected; creation never silently expands a child beyond that set.
|
|
4150
6397
|
firstPartyMcpPermissions: z.array(Permission).optional(),
|
|
4151
|
-
// Third-party MCP servers attached only to this session.
|
|
4152
|
-
//
|
|
6398
|
+
// Third-party MCP servers attached only to this session. For an agent-created
|
|
6399
|
+
// child, omission snapshots its trusted immediate parent's server definitions,
|
|
6400
|
+
// policies, connection refs, and encrypted credentials. Explicit arrays,
|
|
6401
|
+
// including [], are authoritative; non-empty explicit arrays require attach
|
|
6402
|
+
// permission. Credential headers are write-only: create responses and events
|
|
6403
|
+
// expose only SessionMcpServerMetadata.
|
|
4153
6404
|
mcpServers: z.array(SessionMcpServerInput).default([]),
|
|
4154
6405
|
// Shared-sandbox placement (addendum 05 §D.1). Three-way union; OMITTED ⇒
|
|
4155
6406
|
// today's behavior (a context-dependent default resolved server-side: from
|
|
@@ -4174,16 +6425,198 @@ export const CreateSessionRequest = withVariableSetIdAlias({
|
|
|
4174
6425
|
});
|
|
4175
6426
|
export type CreateSessionRequest = z.infer<typeof CreateSessionRequest>;
|
|
4176
6427
|
|
|
6428
|
+
// Generic, host-neutral structured human input. One model tool call creates one
|
|
6429
|
+
// request containing one or more questions; the durable response resumes that
|
|
6430
|
+
// exact call. This is deliberately distinct from tool approval: an answer,
|
|
6431
|
+
// skip, or expiry is structured tool output, never an approve/reject decision.
|
|
6432
|
+
export const HumanInputQuestionKind = z.enum(["text", "single_select", "multi_select"]);
|
|
6433
|
+
export type HumanInputQuestionKind = z.infer<typeof HumanInputQuestionKind>;
|
|
6434
|
+
|
|
6435
|
+
export const HumanInputOption = z.object({
|
|
6436
|
+
id: z.string().min(1).max(64),
|
|
6437
|
+
label: z.string().min(1).max(256),
|
|
6438
|
+
description: z.string().max(2048).nullable().optional(),
|
|
6439
|
+
});
|
|
6440
|
+
export type HumanInputOption = z.infer<typeof HumanInputOption>;
|
|
6441
|
+
|
|
6442
|
+
export const HumanInputQuestion = z
|
|
6443
|
+
.object({
|
|
6444
|
+
id: z.string().min(1).max(64),
|
|
6445
|
+
kind: HumanInputQuestionKind,
|
|
6446
|
+
prompt: z.string().min(1).max(4096),
|
|
6447
|
+
label: z.string().min(1).max(128).nullable().optional(),
|
|
6448
|
+
helpText: z.string().max(2048).nullable().optional(),
|
|
6449
|
+
options: z.array(HumanInputOption).max(20).default([]),
|
|
6450
|
+
required: z.boolean().default(true),
|
|
6451
|
+
allowOther: z.boolean().default(false),
|
|
6452
|
+
validation: z
|
|
6453
|
+
.object({
|
|
6454
|
+
minLength: z.number().int().nonnegative().max(8192).nullable().optional(),
|
|
6455
|
+
maxLength: z.number().int().positive().max(8192).nullable().optional(),
|
|
6456
|
+
minSelections: z.number().int().nonnegative().max(20).nullable().optional(),
|
|
6457
|
+
maxSelections: z.number().int().positive().max(20).nullable().optional(),
|
|
6458
|
+
})
|
|
6459
|
+
.nullable()
|
|
6460
|
+
.optional(),
|
|
6461
|
+
})
|
|
6462
|
+
.superRefine((question, ctx) => {
|
|
6463
|
+
const optionIds = new Set(question.options.map((option) => option.id));
|
|
6464
|
+
if (optionIds.size !== question.options.length) {
|
|
6465
|
+
ctx.addIssue({
|
|
6466
|
+
code: "custom",
|
|
6467
|
+
path: ["options"],
|
|
6468
|
+
message: "option ids must be unique",
|
|
6469
|
+
});
|
|
6470
|
+
}
|
|
6471
|
+
if (question.kind === "text") {
|
|
6472
|
+
if (question.options.length > 0) {
|
|
6473
|
+
ctx.addIssue({
|
|
6474
|
+
code: "custom",
|
|
6475
|
+
path: ["options"],
|
|
6476
|
+
message: "text questions cannot have options",
|
|
6477
|
+
});
|
|
6478
|
+
}
|
|
6479
|
+
if (question.allowOther) {
|
|
6480
|
+
ctx.addIssue({
|
|
6481
|
+
code: "custom",
|
|
6482
|
+
path: ["allowOther"],
|
|
6483
|
+
message: "text questions do not use Other",
|
|
6484
|
+
});
|
|
6485
|
+
}
|
|
6486
|
+
} else if (question.options.length === 0) {
|
|
6487
|
+
ctx.addIssue({
|
|
6488
|
+
code: "custom",
|
|
6489
|
+
path: ["options"],
|
|
6490
|
+
message: "select questions require options",
|
|
6491
|
+
});
|
|
6492
|
+
}
|
|
6493
|
+
const validation = question.validation;
|
|
6494
|
+
if (
|
|
6495
|
+
validation?.minLength != null &&
|
|
6496
|
+
validation?.maxLength != null &&
|
|
6497
|
+
validation.minLength > validation.maxLength
|
|
6498
|
+
) {
|
|
6499
|
+
ctx.addIssue({
|
|
6500
|
+
code: "custom",
|
|
6501
|
+
path: ["validation"],
|
|
6502
|
+
message: "minLength exceeds maxLength",
|
|
6503
|
+
});
|
|
6504
|
+
}
|
|
6505
|
+
if (
|
|
6506
|
+
validation?.minSelections != null &&
|
|
6507
|
+
validation?.maxSelections != null &&
|
|
6508
|
+
validation.minSelections > validation.maxSelections
|
|
6509
|
+
) {
|
|
6510
|
+
ctx.addIssue({
|
|
6511
|
+
code: "custom",
|
|
6512
|
+
path: ["validation"],
|
|
6513
|
+
message: "minSelections exceeds maxSelections",
|
|
6514
|
+
});
|
|
6515
|
+
}
|
|
6516
|
+
});
|
|
6517
|
+
export type HumanInputQuestion = z.infer<typeof HumanInputQuestion>;
|
|
6518
|
+
|
|
6519
|
+
export const HumanInputRequestStatus = z.enum([
|
|
6520
|
+
"pending",
|
|
6521
|
+
"answered",
|
|
6522
|
+
"skipped",
|
|
6523
|
+
"expired",
|
|
6524
|
+
"cancelled",
|
|
6525
|
+
]);
|
|
6526
|
+
export type HumanInputRequestStatus = z.infer<typeof HumanInputRequestStatus>;
|
|
6527
|
+
|
|
6528
|
+
export const RequestHumanInputToolInput = z.object({
|
|
6529
|
+
questions: z.array(HumanInputQuestion).min(1).max(20),
|
|
6530
|
+
allowSkip: z.boolean().default(false),
|
|
6531
|
+
expiresInSeconds: z
|
|
6532
|
+
.number()
|
|
6533
|
+
.int()
|
|
6534
|
+
.positive()
|
|
6535
|
+
.max(30 * 24 * 60 * 60)
|
|
6536
|
+
.nullable()
|
|
6537
|
+
.optional(),
|
|
6538
|
+
});
|
|
6539
|
+
export type RequestHumanInputToolInput = z.infer<typeof RequestHumanInputToolInput>;
|
|
6540
|
+
|
|
6541
|
+
export const HumanInputAnswer = z.object({
|
|
6542
|
+
questionId: z.string().min(1).max(64),
|
|
6543
|
+
values: z.array(z.string().max(8192)).max(20),
|
|
6544
|
+
other: z.string().max(8192).nullable().optional(),
|
|
6545
|
+
});
|
|
6546
|
+
export type HumanInputAnswer = z.infer<typeof HumanInputAnswer>;
|
|
6547
|
+
|
|
6548
|
+
export const HumanInputResponse = z.discriminatedUnion("outcome", [
|
|
6549
|
+
z.object({
|
|
6550
|
+
outcome: z.literal("answered"),
|
|
6551
|
+
answers: z.array(HumanInputAnswer).max(20),
|
|
6552
|
+
}),
|
|
6553
|
+
z.object({ outcome: z.literal("skipped") }),
|
|
6554
|
+
z.object({ outcome: z.literal("expired") }),
|
|
6555
|
+
z.object({ outcome: z.literal("cancelled") }),
|
|
6556
|
+
]);
|
|
6557
|
+
export type HumanInputResponse = z.infer<typeof HumanInputResponse>;
|
|
6558
|
+
|
|
6559
|
+
export const SubmitHumanInputResponseRequest = z.discriminatedUnion("outcome", [
|
|
6560
|
+
z.object({
|
|
6561
|
+
outcome: z.literal("answered"),
|
|
6562
|
+
answers: z.array(HumanInputAnswer).max(20),
|
|
6563
|
+
}),
|
|
6564
|
+
z.object({ outcome: z.literal("skipped") }),
|
|
6565
|
+
]);
|
|
6566
|
+
export type SubmitHumanInputResponseRequest = z.infer<typeof SubmitHumanInputResponseRequest>;
|
|
6567
|
+
|
|
6568
|
+
export const SessionHumanInputRequest = z.object({
|
|
6569
|
+
id: z.string().uuid(),
|
|
6570
|
+
workspaceId: z.string().uuid(),
|
|
6571
|
+
sessionId: z.string().uuid(),
|
|
6572
|
+
turnId: z.string().uuid(),
|
|
6573
|
+
turnGeneration: z.number().int().positive(),
|
|
6574
|
+
creationAttemptId: z.string().uuid(),
|
|
6575
|
+
toolCallId: z.string().min(1).max(1024),
|
|
6576
|
+
status: HumanInputRequestStatus,
|
|
6577
|
+
questions: z.array(HumanInputQuestion).min(1).max(20),
|
|
6578
|
+
allowSkip: z.boolean(),
|
|
6579
|
+
response: HumanInputResponse.nullable(),
|
|
6580
|
+
respondedBy: z.string().max(1024).nullable(),
|
|
6581
|
+
respondedAt: z.string().nullable(),
|
|
6582
|
+
expiresAt: z.string().nullable(),
|
|
6583
|
+
createdAt: z.string(),
|
|
6584
|
+
updatedAt: z.string(),
|
|
6585
|
+
});
|
|
6586
|
+
export type SessionHumanInputRequest = z.infer<typeof SessionHumanInputRequest>;
|
|
6587
|
+
|
|
6588
|
+
/**
|
|
6589
|
+
* Extract the stable approval identity used by both durable admission and
|
|
6590
|
+
* runtime resume. Serialized SDK interruptions may place it on the wrapper or
|
|
6591
|
+
* its raw item; malformed entries fail closed instead of inventing an id.
|
|
6592
|
+
*/
|
|
6593
|
+
export function approvalIdentifier(value: unknown): string | null {
|
|
6594
|
+
if (!value || typeof value !== "object") return null;
|
|
6595
|
+
const approval = value as Record<string, unknown>;
|
|
6596
|
+
const rawItem =
|
|
6597
|
+
approval.rawItem && typeof approval.rawItem === "object"
|
|
6598
|
+
? (approval.rawItem as Record<string, unknown>)
|
|
6599
|
+
: null;
|
|
6600
|
+
const candidate = rawItem?.callId ?? rawItem?.id ?? approval.id ?? approval.name;
|
|
6601
|
+
if (typeof candidate !== "string" && typeof candidate !== "number") return null;
|
|
6602
|
+
return String(candidate);
|
|
6603
|
+
}
|
|
6604
|
+
|
|
4177
6605
|
export const ClientSessionEvent = z.discriminatedUnion("type", [
|
|
4178
6606
|
z.object({
|
|
4179
6607
|
type: z.literal("user.message"),
|
|
4180
|
-
clientEventId:
|
|
6608
|
+
clientEventId: SessionOperationKey.optional(),
|
|
4181
6609
|
payload: z.object({
|
|
4182
6610
|
text: z.string().min(1),
|
|
6611
|
+
// System-level host context for this exact turn only. Persisted on the
|
|
6612
|
+
// turn for retry/recovery, never copied into the visible user message.
|
|
6613
|
+
turnInstructions: z.string().trim().min(1).max(32768).optional(),
|
|
4183
6614
|
resources: z.array(ResourceRef).default([]),
|
|
4184
6615
|
tools: z.array(ToolRef).default([]),
|
|
4185
6616
|
model: z.string().min(1).optional(),
|
|
4186
6617
|
reasoningEffort: ReasoningEffort.optional(),
|
|
6618
|
+
controlEtag: z.string().min(1).optional(),
|
|
6619
|
+
expectedDraftRevision: z.number().int().nonnegative().optional(),
|
|
4187
6620
|
// Header-value rotation only. URL/name/tool settings are immutable after
|
|
4188
6621
|
// session create; persisted events expose metadata, never header values.
|
|
4189
6622
|
mcpCredentialUpdates: z.array(SessionMcpCredentialUpdateInput).optional(),
|
|
@@ -4191,25 +6624,35 @@ export const ClientSessionEvent = z.discriminatedUnion("type", [
|
|
|
4191
6624
|
}),
|
|
4192
6625
|
z.object({
|
|
4193
6626
|
type: z.literal("user.approvalDecision"),
|
|
4194
|
-
clientEventId:
|
|
6627
|
+
clientEventId: SessionOperationKey.optional(),
|
|
4195
6628
|
payload: z.object({
|
|
4196
|
-
approvalId: z.string().min(1),
|
|
6629
|
+
approvalId: z.string().min(1).max(SESSION_OPERATION_KEY_MAX_CHARS),
|
|
4197
6630
|
decision: z.enum(["approve", "reject"]),
|
|
4198
6631
|
message: z.string().optional(),
|
|
4199
6632
|
}),
|
|
4200
6633
|
}),
|
|
6634
|
+
z.object({
|
|
6635
|
+
type: z.literal("user.humanInputResponse"),
|
|
6636
|
+
clientEventId: SessionOperationKey.optional(),
|
|
6637
|
+
payload: z.object({
|
|
6638
|
+
requestId: z.string().uuid(),
|
|
6639
|
+
response: SubmitHumanInputResponseRequest,
|
|
6640
|
+
}),
|
|
6641
|
+
}),
|
|
4201
6642
|
]);
|
|
4202
6643
|
export type ClientSessionEvent = z.infer<typeof ClientSessionEvent>;
|
|
4203
6644
|
|
|
4204
6645
|
export const SteerSessionMessageRequest = z.object({
|
|
4205
6646
|
text: z.string().min(1),
|
|
6647
|
+
// Same per-turn system-level context as a queued user.message.
|
|
6648
|
+
turnInstructions: z.string().trim().min(1).max(32768).optional(),
|
|
4206
6649
|
resources: z.array(ResourceRef).default([]),
|
|
4207
6650
|
tools: z.array(ToolRef).default([]),
|
|
4208
6651
|
model: z.string().min(1).optional(),
|
|
4209
6652
|
reasoningEffort: ReasoningEffort.optional(),
|
|
4210
|
-
clientEventId:
|
|
4211
|
-
|
|
4212
|
-
|
|
6653
|
+
clientEventId: SessionOperationKey.optional(),
|
|
6654
|
+
controlEtag: z.string().min(1).optional(),
|
|
6655
|
+
expectedDraftRevision: z.number().int().nonnegative().optional(),
|
|
4213
6656
|
mcpCredentialUpdates: z.array(SessionMcpCredentialUpdateInput).optional(),
|
|
4214
6657
|
});
|
|
4215
6658
|
export type SteerSessionMessageRequest = z.infer<typeof SteerSessionMessageRequest>;
|
|
@@ -4249,6 +6692,37 @@ export const GitHubRepository = z.object({
|
|
|
4249
6692
|
});
|
|
4250
6693
|
export type GitHubRepository = z.infer<typeof GitHubRepository>;
|
|
4251
6694
|
|
|
6695
|
+
export const GitHubRepositoryScope = z.enum(["all", "selected"]);
|
|
6696
|
+
export type GitHubRepositoryScope = z.infer<typeof GitHubRepositoryScope>;
|
|
6697
|
+
|
|
6698
|
+
export const GitHubInstallationBinding = z.object({
|
|
6699
|
+
installationId: z.number().int().positive(),
|
|
6700
|
+
accountLogin: z.string().nullable(),
|
|
6701
|
+
accountType: z.string().nullable(),
|
|
6702
|
+
repositoryScope: GitHubRepositoryScope,
|
|
6703
|
+
repositoryCount: z.number().int().nonnegative(),
|
|
6704
|
+
createdAt: z.string(),
|
|
6705
|
+
updatedAt: z.string(),
|
|
6706
|
+
});
|
|
6707
|
+
export type GitHubInstallationBinding = z.infer<typeof GitHubInstallationBinding>;
|
|
6708
|
+
|
|
6709
|
+
export const GitHubAppInfo = z.object({
|
|
6710
|
+
configured: z.boolean(),
|
|
6711
|
+
appId: z.string().nullable(),
|
|
6712
|
+
clientId: z.string().nullable(),
|
|
6713
|
+
appSlug: z.string().nullable(),
|
|
6714
|
+
installUrl: z.string().nullable(),
|
|
6715
|
+
linkUrl: z.string().nullable(),
|
|
6716
|
+
installations: z.array(GitHubInstallationBinding),
|
|
6717
|
+
missing: z.array(z.string()),
|
|
6718
|
+
});
|
|
6719
|
+
export type GitHubAppInfo = z.infer<typeof GitHubAppInfo>;
|
|
6720
|
+
|
|
6721
|
+
export const GitHubRepositoriesResponse = z.object({
|
|
6722
|
+
repositories: z.array(GitHubRepository),
|
|
6723
|
+
});
|
|
6724
|
+
export type GitHubRepositoriesResponse = z.infer<typeof GitHubRepositoriesResponse>;
|
|
6725
|
+
|
|
4252
6726
|
export const ClientAuthConfig = z.discriminatedUnion("mode", [
|
|
4253
6727
|
z.object({
|
|
4254
6728
|
mode: z.literal("none"),
|
|
@@ -4269,7 +6743,7 @@ export const ClientAuthConfig = z.discriminatedUnion("mode", [
|
|
|
4269
6743
|
]);
|
|
4270
6744
|
export type ClientAuthConfig = z.infer<typeof ClientAuthConfig>;
|
|
4271
6745
|
|
|
4272
|
-
// The negotiated capability handshake document (
|
|
6746
|
+
// The negotiated capability handshake document (sandbox contract C.3). ONE shape;
|
|
4273
6747
|
// collapses the parallel per-module definitions. A capability cell is always
|
|
4274
6748
|
// present with `available`/`transport` + a `reason` when unavailable — never
|
|
4275
6749
|
// absent.
|
|
@@ -4402,14 +6876,14 @@ export const ViewerHolder = z.object({
|
|
|
4402
6876
|
leaseEpoch: z.number().int().nonnegative(),
|
|
4403
6877
|
viewerHeartbeatIntervalMs: z.number().int().positive(),
|
|
4404
6878
|
// The desktop pixel tunnel URL the viewer connects to directly; null until
|
|
4405
|
-
//
|
|
6879
|
+
// a viewer grant is minted (gated until then).
|
|
4406
6880
|
dataPlaneUrl: z.string().nullable(),
|
|
4407
6881
|
});
|
|
4408
6882
|
export type ViewerHolder = z.infer<typeof ViewerHolder>;
|
|
4409
6883
|
|
|
4410
6884
|
// POST .../stream-capabilities/acknowledge — record the calling principal's
|
|
4411
|
-
// acknowledgment of the un-redacted pixel plane
|
|
4412
|
-
//
|
|
6885
|
+
// acknowledgment of the un-redacted pixel plane. Reuses the acknowledgment
|
|
6886
|
+
// machinery — no new endpoint
|
|
4413
6887
|
// shape beyond this body, no new permission beyond stream:acknowledge.
|
|
4414
6888
|
//
|
|
4415
6889
|
// `acknowledgeShared` MUST be true when the box is shared (the group has >1
|
|
@@ -4453,7 +6927,7 @@ export type ViewerHeartbeatResponse = z.infer<typeof ViewerHeartbeatResponse>;
|
|
|
4453
6927
|
// (DeviceAuthStart*, DeviceAuthPoll*, EnrollmentCredentials) so the Rust agent's
|
|
4454
6928
|
// `enroll` command (which runs the flow over HTTP before it has NATS creds)
|
|
4455
6929
|
// decodes the SAME field names (the proto's ts-proto JSON is camelCase). The
|
|
4456
|
-
// request bodies additionally carry the consent-relevant fields the
|
|
6930
|
+
// request bodies additionally carry the consent-relevant fields the design brief
|
|
4457
6931
|
// mandates (the agent ed25519 pubkey + can-offer-display + requests-screen-control).
|
|
4458
6932
|
// =============================================================================
|
|
4459
6933
|
|
|
@@ -4699,7 +7173,7 @@ export const EnrollTokenExchangeResponse = z.object({
|
|
|
4699
7173
|
});
|
|
4700
7174
|
export type EnrollTokenExchangeResponse = z.infer<typeof EnrollTokenExchangeResponse>;
|
|
4701
7175
|
|
|
4702
|
-
// ── Machines dashboard + per-machine metrics (M10
|
|
7176
|
+
// ── Machines dashboard + per-machine metrics (M10) ────────────
|
|
4703
7177
|
//
|
|
4704
7178
|
// The SHARED data contract M10 (backend) implements + M9 (UI) renders. THE
|
|
4705
7179
|
// orchestrator owns this shape; M9 imports these types so the dashboard never
|
|
@@ -4853,8 +7327,20 @@ export const ClientModel = z.object({
|
|
|
4853
7327
|
});
|
|
4854
7328
|
export type ClientModel = z.infer<typeof ClientModel>;
|
|
4855
7329
|
|
|
7330
|
+
/**
|
|
7331
|
+
* Exact public HTTP protocol revision spoken by this release train.
|
|
7332
|
+
*
|
|
7333
|
+
* This is deliberately independent from a deployment SHA: API and web may roll
|
|
7334
|
+
* at different instants, while incompatible request shapes must never cross
|
|
7335
|
+
* that rollout boundary. Mutating clients send this value in
|
|
7336
|
+
* `x-opengeni-api-contract`; the API rejects any other value before routing.
|
|
7337
|
+
*/
|
|
7338
|
+
export const OPENGENI_API_CONTRACT_REVISION = "2026-07-turn-instructions-v1" as const;
|
|
7339
|
+
export const OPENGENI_API_CONTRACT_HEADER = "x-opengeni-api-contract" as const;
|
|
7340
|
+
|
|
4856
7341
|
export const ClientConfig = z.object({
|
|
4857
7342
|
deploymentRevision: z.string(),
|
|
7343
|
+
apiContractRevision: z.literal(OPENGENI_API_CONTRACT_REVISION),
|
|
4858
7344
|
// Release-train version of the server (absent on dev/source builds). The
|
|
4859
7345
|
// compatibility policy lives in docs/architecture.md — clients within the
|
|
4860
7346
|
// same major are supported; evolution is additive within a major.
|