@opengeni/contracts 0.3.0 → 0.5.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 +677 -1
- package/dist/index.js +545 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +883 -11
package/src/index.ts
CHANGED
|
@@ -10,9 +10,11 @@ export const SessionStatus = z.enum([
|
|
|
10
10
|
]);
|
|
11
11
|
export type SessionStatus = z.infer<typeof SessionStatus>;
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
// `packages/sdk/test/contract-parity.test.ts`.
|
|
15
|
-
// positions
|
|
13
|
+
// 11 backends; 3-way enum parity (contracts / sdk / deployment) is pinned by
|
|
14
|
+
// `packages/sdk/test/contract-parity.test.ts`. Every member is ADDITIVE AT THE
|
|
15
|
+
// END (the parity test pins positions): the original four, then the six cloud
|
|
16
|
+
// backends, then `selfhosted` (bring-your-own-compute — a user's own machine
|
|
17
|
+
// enrolled as a first-class sandbox).
|
|
16
18
|
export const SandboxBackend = z.enum([
|
|
17
19
|
"docker",
|
|
18
20
|
"modal",
|
|
@@ -24,6 +26,7 @@ export const SandboxBackend = z.enum([
|
|
|
24
26
|
"blaxel",
|
|
25
27
|
"cloudflare",
|
|
26
28
|
"vercel",
|
|
29
|
+
"selfhosted",
|
|
27
30
|
]);
|
|
28
31
|
export type SandboxBackend = z.infer<typeof SandboxBackend>;
|
|
29
32
|
|
|
@@ -350,6 +353,53 @@ export const CAPABILITY_DESCRIPTORS: Record<SandboxBackend, CapabilityDescriptor
|
|
|
350
353
|
persistable: false,
|
|
351
354
|
supportsRunAs: false,
|
|
352
355
|
},
|
|
356
|
+
// Bring-your-own-compute: the user's OWN machine, enrolled via a Rust agent,
|
|
357
|
+
// becomes ONE shared whole-machine sandbox (the agent IS the box). It is the
|
|
358
|
+
// first backend to make macOS/Windows reachable (default linux). Desktop is
|
|
359
|
+
// capability-PROCLAIMED ("vnc-ws") — the agent serves a native display stack
|
|
360
|
+
// (Linux X11/Xvfb, macOS CGEvent/ScreenCaptureKit) consent-gated at enroll;
|
|
361
|
+
// the online/offline/consent/display negotiation lives in select.ts (M3), this
|
|
362
|
+
// row is the static feasibility ceiling. Always-on (process-lifetime, never
|
|
363
|
+
// idle-reaped) and NOT persistable — OpenGeni cannot snapshot the user's disk,
|
|
364
|
+
// so resume = "is the agent's subject live?", never a cold re-create. Ports
|
|
365
|
+
// surface on-demand through the stateless relay edge, which lands behind the
|
|
366
|
+
// `resolveExposedPort` swap-seam later; until then it reuses the existing
|
|
367
|
+
// `provider-tunnel` exposure kind (the relay IS the provider tunnel for the
|
|
368
|
+
// agent) so no new PortExposureKind literal — and no new switch arms — are
|
|
369
|
+
// introduced. supportsOnDemandPorts:true: the agent opens a stream channel for
|
|
370
|
+
// a port on request rather than pre-declaring 6080/7681 at construction.
|
|
371
|
+
selfhosted: {
|
|
372
|
+
backend: "selfhosted",
|
|
373
|
+
backendId: "selfhosted",
|
|
374
|
+
tier: "desktop",
|
|
375
|
+
os: { supported: ["linux", "macos", "windows"], default: "linux" },
|
|
376
|
+
capabilities: {
|
|
377
|
+
FileSystem: { available: true, readOnly: false },
|
|
378
|
+
Terminal: { available: true, transport: "pty-ws", pty: true }, // real PTY over the relay
|
|
379
|
+
Git: { available: true },
|
|
380
|
+
DesktopStream: { available: true, transport: "vnc-ws" }, // proclaimed; consent-gated at enroll
|
|
381
|
+
Recording: { available: true }, // boot invariant: == DesktopStream.available
|
|
382
|
+
},
|
|
383
|
+
lifetime: {
|
|
384
|
+
// Whole-machine, always-there: online while the agent process runs, offline
|
|
385
|
+
// when it stops. The lease is NEVER idle-killed (it's the user's machine,
|
|
386
|
+
// not a reapable cloud box) and there is nothing to suspend/resume — the
|
|
387
|
+
// machine simply is or isn't reachable.
|
|
388
|
+
requiresSnapshotRollover: false,
|
|
389
|
+
hasIdleKiller: false,
|
|
390
|
+
supportsSuspendResume: false,
|
|
391
|
+
resumeIsLockFree: true, // resume = address the live NATS subject; no provider lock
|
|
392
|
+
},
|
|
393
|
+
// persistable:false forces snapshot.kind:"none" (the descriptor invariant
|
|
394
|
+
// `persistable ⇒ snapshot.kind!=="none"`): OpenGeni cannot snapshot the
|
|
395
|
+
// user's disk — the machine itself is the persistence.
|
|
396
|
+
snapshot: { kind: "none", hasTarFallback: false },
|
|
397
|
+
portExposure: { kind: "provider-tunnel", supportsOnDemandPorts: true },
|
|
398
|
+
workspaceRoot: "/", // agent-reported machine root (the whole machine is the sandbox)
|
|
399
|
+
nativeBucketMount: false,
|
|
400
|
+
persistable: false,
|
|
401
|
+
supportsRunAs: false,
|
|
402
|
+
},
|
|
353
403
|
};
|
|
354
404
|
|
|
355
405
|
export const ReasoningEffort = z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]);
|
|
@@ -436,7 +486,18 @@ export const Permission = z.enum([
|
|
|
436
486
|
"api_keys:manage",
|
|
437
487
|
"environments:manage",
|
|
438
488
|
"environments:use",
|
|
489
|
+
// Attach or rotate per-session third-party MCP server credentials. Deliberately
|
|
490
|
+
// not part of the worker's default first-party MCP permission set: a sandboxed
|
|
491
|
+
// agent must not be able to hand itself new bearer credentials.
|
|
492
|
+
"mcp_servers:attach",
|
|
439
493
|
"goals:manage",
|
|
494
|
+
// Bring-your-own-compute (M5). enrollments:read lists a workspace's machines;
|
|
495
|
+
// enrollments:manage approves a device-flow enrollment (the LOUD whole-machine
|
|
496
|
+
// consent) + revokes a machine. Distinct from sessions/stream perms because an
|
|
497
|
+
// enrollment grants WHOLE-MACHINE access to a user's own hardware — a high-trust,
|
|
498
|
+
// admin-shaped action. workspace:admin is the super-wildcard over both.
|
|
499
|
+
"enrollments:read",
|
|
500
|
+
"enrollments:manage",
|
|
440
501
|
]);
|
|
441
502
|
export type Permission = z.infer<typeof Permission>;
|
|
442
503
|
|
|
@@ -555,6 +616,125 @@ export async function verifyDelegatedAccessToken(secret: string, token: string,
|
|
|
555
616
|
return payload.data;
|
|
556
617
|
}
|
|
557
618
|
|
|
619
|
+
// --- Enrollment bearer credential (bring-your-own-compute M5, dossier §10.2) ---
|
|
620
|
+
//
|
|
621
|
+
// The signed bearer the agent presents to the control plane after enrollment (the
|
|
622
|
+
// EnrollmentCredentials.bearer the poll returns). REUSES the SAME HMAC envelope as
|
|
623
|
+
// the delegated/stream tokens (base64Url payload + hmacSha256Base64Url) with a
|
|
624
|
+
// distinct `oge_` prefix so it can never be confused with an `ogd_` access token or
|
|
625
|
+
// an `ogs_` stream token. It binds (workspaceId, agentId, enrollmentId) so the
|
|
626
|
+
// control plane can verify the agent owns the subject `agent.<ws>.<id>` it
|
|
627
|
+
// subscribes to. Signed with resolveEnrollmentSigningSecret; the secret value is
|
|
628
|
+
// NEVER logged. The real per-workspace NATS Account creds binding is infra-deferred
|
|
629
|
+
// (M4/relay) — this bearer is the application-tier identity proof.
|
|
630
|
+
export const EnrollmentBearerPayload = z.object({
|
|
631
|
+
workspaceId: z.string().uuid(),
|
|
632
|
+
agentId: z.string().uuid(),
|
|
633
|
+
enrollmentId: z.string().uuid(),
|
|
634
|
+
// The Account-scoped control-plane subject prefix the agent subscribes to.
|
|
635
|
+
subjectPrefix: z.string().min(1),
|
|
636
|
+
exp: z.number().int().positive(),
|
|
637
|
+
});
|
|
638
|
+
export type EnrollmentBearerPayload = z.infer<typeof EnrollmentBearerPayload>;
|
|
639
|
+
|
|
640
|
+
export async function signEnrollmentBearer(secret: string, payload: EnrollmentBearerPayload): Promise<string> {
|
|
641
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(EnrollmentBearerPayload.parse(payload)));
|
|
642
|
+
const signature = await hmacSha256Base64Url(secret, encodedPayload);
|
|
643
|
+
return `oge_${encodedPayload}.${signature}`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export async function verifyEnrollmentBearer(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<EnrollmentBearerPayload | null> {
|
|
647
|
+
if (!token.startsWith("oge_")) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
const withoutPrefix = token.slice("oge_".length);
|
|
651
|
+
const dot = withoutPrefix.lastIndexOf(".");
|
|
652
|
+
if (dot <= 0) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
const encodedPayload = withoutPrefix.slice(0, dot);
|
|
656
|
+
const signature = withoutPrefix.slice(dot + 1);
|
|
657
|
+
const expected = await hmacSha256Base64Url(secret, encodedPayload);
|
|
658
|
+
if (!constantTimeEqual(signature, expected)) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
const payload = EnrollmentBearerPayload.safeParse(JSON.parse(base64UrlDecode(encodedPayload)));
|
|
662
|
+
if (!payload.success || payload.data.exp < nowSeconds) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
return payload.data;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// --- Non-interactive enroll token (self-hosted enrollment UX §A2.1) ----------
|
|
669
|
+
//
|
|
670
|
+
// The SHORT-TTL, secret, workspace-scoped token the headless/fleet enroll path
|
|
671
|
+
// presents to /v1/enrollments/token/exchange. The token IS the grant — there is
|
|
672
|
+
// no human approve step — so it is stateless-signed (no DB row): the holder of an
|
|
673
|
+
// unexpired token can enroll ONE machine identity into ONE workspace.
|
|
674
|
+
//
|
|
675
|
+
// It REUSES the SAME HMAC envelope as signEnrollmentBearer (base64Url payload +
|
|
676
|
+
// hmacSha256Base64Url) with a DISTINCT `oget_` prefix and a `typ: "enroll"` claim.
|
|
677
|
+
// DOMAIN SEPARATION: even though it shares the signing secret with the `oge_`
|
|
678
|
+
// bearer, the prefix + typ claim make an enroll token unusable as an `oge_`
|
|
679
|
+
// bearer (verifyEnrollmentBearer's `oge_` prefix check rejects it) and vice-versa
|
|
680
|
+
// (verifyEnrollToken's `oget_` prefix + typ check rejects an `oge_` bearer). The
|
|
681
|
+
// secret value is NEVER logged.
|
|
682
|
+
export const EnrollTokenPayload = z.object({
|
|
683
|
+
// Domain-separation claim — fixed "enroll" so an `oge_`/`ogd_`/`ogs_` payload (no
|
|
684
|
+
// typ, or a different typ) can never satisfy verifyEnrollToken even past the prefix.
|
|
685
|
+
typ: z.literal("enroll"),
|
|
686
|
+
workspaceId: z.string().uuid(),
|
|
687
|
+
accountId: z.string().uuid(),
|
|
688
|
+
// The screen-control consent baked into the token at mint (the minting user's
|
|
689
|
+
// decision); the exchange records it as consentedScreenControl on the enrollment.
|
|
690
|
+
allowScreenControl: z.boolean(),
|
|
691
|
+
iat: z.number().int().nonnegative(),
|
|
692
|
+
exp: z.number().int().positive(),
|
|
693
|
+
});
|
|
694
|
+
export type EnrollTokenPayload = z.infer<typeof EnrollTokenPayload>;
|
|
695
|
+
|
|
696
|
+
export async function signEnrollToken(secret: string, payload: EnrollTokenPayload): Promise<string> {
|
|
697
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(EnrollTokenPayload.parse(payload)));
|
|
698
|
+
const signature = await hmacSha256Base64Url(secret, encodedPayload);
|
|
699
|
+
return `oget_${encodedPayload}.${signature}`;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Verify an enroll token: rejects (returns null) on a bad prefix (NOT `oget_`),
|
|
704
|
+
* a malformed envelope, a bad HMAC signature (constant-time), schema-invalid
|
|
705
|
+
* claims (which includes `typ !== "enroll"` — the `z.literal` rejects it), or an
|
|
706
|
+
* expired token (`exp < now`). Mirrors verifyEnrollmentBearer exactly. An `oge_`
|
|
707
|
+
* bearer fails the prefix gate; a same-secret token that lacks the typ claim fails
|
|
708
|
+
* the schema gate — both halves of the domain separation are enforced here.
|
|
709
|
+
*/
|
|
710
|
+
export async function verifyEnrollToken(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<EnrollTokenPayload | null> {
|
|
711
|
+
if (!token.startsWith("oget_")) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
const withoutPrefix = token.slice("oget_".length);
|
|
715
|
+
const dot = withoutPrefix.lastIndexOf(".");
|
|
716
|
+
if (dot <= 0) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
const encodedPayload = withoutPrefix.slice(0, dot);
|
|
720
|
+
const signature = withoutPrefix.slice(dot + 1);
|
|
721
|
+
const expected = await hmacSha256Base64Url(secret, encodedPayload);
|
|
722
|
+
if (!constantTimeEqual(signature, expected)) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
let decoded: unknown;
|
|
726
|
+
try {
|
|
727
|
+
decoded = JSON.parse(base64UrlDecode(encodedPayload));
|
|
728
|
+
} catch {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
const payload = EnrollTokenPayload.safeParse(decoded);
|
|
732
|
+
if (!payload.success || payload.data.exp < nowSeconds) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
return payload.data;
|
|
736
|
+
}
|
|
737
|
+
|
|
558
738
|
// --- Scoped data-plane stream token (master-spine §C.3 / crosscut PART 1.3) ---
|
|
559
739
|
//
|
|
560
740
|
// REUSES the existing HMAC envelope (sign/verifyDelegatedAccessToken's
|
|
@@ -633,6 +813,83 @@ export async function verifyStreamToken(secret: string, token: string, nowSecond
|
|
|
633
813
|
return payload.data;
|
|
634
814
|
}
|
|
635
815
|
|
|
816
|
+
// --- Relay PRODUCER token (bring-your-own-compute M8b, dossier §10.5) ---
|
|
817
|
+
//
|
|
818
|
+
// The token the AGENT presents to the relay edge when it registers a pty/desktop
|
|
819
|
+
// stream channel (role=AGENT) — distinct from the viewer's `ogs_` token. It is
|
|
820
|
+
// minted by the control plane at enrollment and threaded into EnrollmentCredentials
|
|
821
|
+
// (proto field `relay_token`); the relay verifies it on its own merits, then pairs
|
|
822
|
+
// the producer with the consumer by the shared channel key.
|
|
823
|
+
//
|
|
824
|
+
// REUSES the EXACT SAME HMAC envelope as the `ogs_`/`ogd_`/`oge_` tokens
|
|
825
|
+
// (base64Url JSON payload + hmacSha256Base64Url) — NOT a second crypto — with a
|
|
826
|
+
// distinct `ogr_` prefix so it can never be confused with the others. The claim
|
|
827
|
+
// set binds (workspaceId, agentId): the relay reads the channel-key's ws+agent
|
|
828
|
+
// from the StreamOpen and asserts the producer token claims the SAME pair, so a
|
|
829
|
+
// producer token for workspace A can never register a channel for workspace B.
|
|
830
|
+
// Signed with resolveRelayTokenSecret (the relay-token HMAC secret); the value is
|
|
831
|
+
// NEVER logged. Long-lived by design (it is enrollment-scoped, not per-stream —
|
|
832
|
+
// the agent presents it on every channel registration for the life of the
|
|
833
|
+
// enrollment); the relay additionally validates the channel key + (for the
|
|
834
|
+
// viewer's `ogs_`) the lease/active-epoch fence.
|
|
835
|
+
//
|
|
836
|
+
// The Rust relay re-implements this verify (the same base64url(JSON) + HMAC-SHA256
|
|
837
|
+
// + prefix split) so TS-mint and Rust-verify provably agree — see the cross-stack
|
|
838
|
+
// fixture in agent/crates/opengeni-relay/tests and the relay's `token` module doc.
|
|
839
|
+
export const RelayTokenPayload = z.object({
|
|
840
|
+
// The workspace the agent (and its channels) belong to — the relay asserts this
|
|
841
|
+
// equals the channel-key's ws so a producer can only register its own channels.
|
|
842
|
+
workspaceId: z.string().uuid(),
|
|
843
|
+
// The agent (machine) id — the relay asserts this equals the channel-key's agent.
|
|
844
|
+
agentId: z.string().uuid(),
|
|
845
|
+
// Expiry (unix seconds). Enrollment-scoped horizon (re-minted on re-enroll).
|
|
846
|
+
exp: z.number().int().positive(),
|
|
847
|
+
});
|
|
848
|
+
export type RelayTokenPayload = z.infer<typeof RelayTokenPayload>;
|
|
849
|
+
|
|
850
|
+
export async function signRelayToken(secret: string, payload: RelayTokenPayload): Promise<string> {
|
|
851
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(RelayTokenPayload.parse(payload)));
|
|
852
|
+
const signature = await hmacSha256Base64Url(secret, encodedPayload);
|
|
853
|
+
return `ogr_${encodedPayload}.${signature}`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Verify a relay producer token: rejects (returns null) on a bad prefix, malformed
|
|
858
|
+
* envelope, bad HMAC signature (constant-time), schema-invalid claims, or expiry.
|
|
859
|
+
* Mirrors verifyStreamToken exactly. The relay (Rust) re-implements this verify;
|
|
860
|
+
* the TS verify here proves the format for the cross-stack fixture + any TS caller.
|
|
861
|
+
*
|
|
862
|
+
* The channel-key scope (claim.workspaceId/agentId vs the StreamOpen channel key)
|
|
863
|
+
* is enforced by the relay at USE — verify proves authenticity + freshness only.
|
|
864
|
+
*/
|
|
865
|
+
export async function verifyRelayToken(secret: string, token: string, nowSeconds = Math.floor(Date.now() / 1000)): Promise<RelayTokenPayload | null> {
|
|
866
|
+
if (!token.startsWith("ogr_")) {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
const withoutPrefix = token.slice("ogr_".length);
|
|
870
|
+
const dot = withoutPrefix.lastIndexOf(".");
|
|
871
|
+
if (dot <= 0) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
const encodedPayload = withoutPrefix.slice(0, dot);
|
|
875
|
+
const signature = withoutPrefix.slice(dot + 1);
|
|
876
|
+
const expected = await hmacSha256Base64Url(secret, encodedPayload);
|
|
877
|
+
if (!constantTimeEqual(signature, expected)) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
let decoded: unknown;
|
|
881
|
+
try {
|
|
882
|
+
decoded = JSON.parse(base64UrlDecode(encodedPayload));
|
|
883
|
+
} catch {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
const payload = RelayTokenPayload.safeParse(decoded);
|
|
887
|
+
if (!payload.success || payload.data.exp < nowSeconds) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
return payload.data;
|
|
891
|
+
}
|
|
892
|
+
|
|
636
893
|
export const CreateWorkspaceRequest = z.object({
|
|
637
894
|
accountId: z.string().uuid().optional(),
|
|
638
895
|
name: z.string().min(1),
|
|
@@ -790,6 +1047,155 @@ export const LimitDecision = z.discriminatedUnion("allowed", [
|
|
|
790
1047
|
]);
|
|
791
1048
|
export type LimitDecision = z.infer<typeof LimitDecision>;
|
|
792
1049
|
|
|
1050
|
+
// ============ P3 — Entitlements port (§7.5) ============
|
|
1051
|
+
//
|
|
1052
|
+
// The host-providable admission seam over OpenGeni's TWO existing admission
|
|
1053
|
+
// sites: the API edge (`checkLimit`/`requireLimit`, billing/limits.ts) AND the
|
|
1054
|
+
// worker edge (`ensureRunAllowed`, agent-turn.ts — both turn-entry and the
|
|
1055
|
+
// mid-stream budget valve). A host that owns its OWN ledger/meter binds this to
|
|
1056
|
+
// keep OpenGeni from re-deriving admission from its local ledger.
|
|
1057
|
+
//
|
|
1058
|
+
// CRITICAL CONTRACT: `admitRun` returns a transport-neutral allow/deny decision
|
|
1059
|
+
// (+ optional structured reason + the echoed quantity it admitted) and NEVER
|
|
1060
|
+
// exposes `getBillingBalance` or any ledger internals — the host's balance math
|
|
1061
|
+
// stays on the host side of the boundary. This is what lets the same port serve
|
|
1062
|
+
// both PUSH (host funds OpenGeni's ledger; admission is a LOCAL read of that
|
|
1063
|
+
// funded ledger) and PULL (a network callback to the host's own meter).
|
|
1064
|
+
//
|
|
1065
|
+
// `action` is a free `string` (NOT the internal `LimitAction` enum) so a host
|
|
1066
|
+
// meter can key on actions OpenGeni does not model. `quantity` is the units the
|
|
1067
|
+
// caller is about to consume (tokens, bytes, 1 run, …); the decision MAY echo
|
|
1068
|
+
// the admitted quantity so a PULL host can grant a partial allowance.
|
|
1069
|
+
export const EntitlementDecision = z.discriminatedUnion("allowed", [
|
|
1070
|
+
z.object({ allowed: z.literal(true), quantity: z.number().optional() }),
|
|
1071
|
+
z.object({ allowed: z.literal(false), reason: z.string(), code: z.string().optional(), quantity: z.number().optional() }),
|
|
1072
|
+
]);
|
|
1073
|
+
export type EntitlementDecision = z.infer<typeof EntitlementDecision>;
|
|
1074
|
+
|
|
1075
|
+
export type AdmitRunInput = {
|
|
1076
|
+
accountId: string;
|
|
1077
|
+
workspaceId: string;
|
|
1078
|
+
action: string;
|
|
1079
|
+
quantity: number;
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
export type EntitlementsPort = {
|
|
1083
|
+
admitRun(input: AdmitRunInput): Promise<EntitlementDecision>;
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// ============ P4a — Connection-credential provider (§7.6) ============
|
|
1087
|
+
//
|
|
1088
|
+
// The host-providable per-run credential-mint seam over OpenGeni's TWO
|
|
1089
|
+
// run-scoped credential sites in the worker:
|
|
1090
|
+
// - GIT credentials: the GitHub App installation token minted in
|
|
1091
|
+
// `sandboxEnvironmentForRun` (today `createGitHubAppInstallationToken`
|
|
1092
|
+
// from `settings`) and injected as `GH_TOKEN`/`GITHUB_TOKEN`/the git
|
|
1093
|
+
// extraheader.
|
|
1094
|
+
// - SANDBOX secrets: the decrypted workspace environment values loaded in
|
|
1095
|
+
// `loadWorkspaceEnvironmentForRun` (today decrypted with
|
|
1096
|
+
// `environmentsEncryptionKeyBytes(settings)`).
|
|
1097
|
+
//
|
|
1098
|
+
// In embedded/separate topologies the HOST owns these external connections
|
|
1099
|
+
// (its GitHub App, its secret vault + encryption key). When a host binds this
|
|
1100
|
+
// port, OpenGeni asks the host to mint/decrypt per-run instead of self-minting
|
|
1101
|
+
// from `settings`. Unset (standalone default) → byte-for-byte today's
|
|
1102
|
+
// self-mint.
|
|
1103
|
+
//
|
|
1104
|
+
// FORK-7 CROSS-CHECK (the host-mapping safety guardrail): a credential
|
|
1105
|
+
// provider returns the `workspaceId` it scoped the credential to, and the
|
|
1106
|
+
// activity ASSERTS it agrees with the run's workspace BEFORE injecting
|
|
1107
|
+
// `GH_TOKEN` (or applying the decrypted values). A host mapping bug that
|
|
1108
|
+
// returns tenant B's creds while the run is tenant A is thereby caught at the
|
|
1109
|
+
// seam, never silently injected into tenant A's sandbox.
|
|
1110
|
+
|
|
1111
|
+
export type GitCredentialsRequest = {
|
|
1112
|
+
accountId: string;
|
|
1113
|
+
workspaceId: string;
|
|
1114
|
+
// The GitHub App installation the run's repository resources resolved to,
|
|
1115
|
+
// and the specific repositories the token must be scoped to. Mirrors the
|
|
1116
|
+
// shape `createGitHubAppInstallationToken` consumes today.
|
|
1117
|
+
installationId: number;
|
|
1118
|
+
repositoryIds: number[];
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
export type GitCredentials = {
|
|
1122
|
+
// The minted installation token the activity injects as GH_TOKEN/GITHUB_TOKEN
|
|
1123
|
+
// and into the git http extraheader (identical downstream handling to the
|
|
1124
|
+
// self-mint path).
|
|
1125
|
+
token: string;
|
|
1126
|
+
// FORK-7 echo: the workspace the provider scoped this token to. The activity
|
|
1127
|
+
// asserts `workspaceId === request.workspaceId` before injecting.
|
|
1128
|
+
workspaceId: string;
|
|
1129
|
+
// Optional git identity override. When omitted the activity falls back to
|
|
1130
|
+
// today's `githubAppBotIdentity(settings)`.
|
|
1131
|
+
identity?: { name: string; email: string } | null;
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
export type SandboxSecretsRequest = {
|
|
1135
|
+
accountId: string;
|
|
1136
|
+
workspaceId: string;
|
|
1137
|
+
// The workspace environment the run's session declares (null = unattached;
|
|
1138
|
+
// the provider, like the self-mint path, returns null values for it).
|
|
1139
|
+
environmentId: string;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
export type SandboxSecrets = {
|
|
1143
|
+
// The decrypted environment values the run injects, replacing the local
|
|
1144
|
+
// `environmentsEncryptionKeyBytes` decrypt. Same shape the self-mint path
|
|
1145
|
+
// produces (plaintext name→value).
|
|
1146
|
+
values: Record<string, string>;
|
|
1147
|
+
// FORK-7 echo: the workspace the provider scoped these secrets to.
|
|
1148
|
+
workspaceId: string;
|
|
1149
|
+
// Optional environment metadata; when omitted the activity uses the
|
|
1150
|
+
// environmentId as both id and name (the local decrypt carries the row's
|
|
1151
|
+
// id/name/description, but only `id` is load-bearing downstream).
|
|
1152
|
+
id?: string;
|
|
1153
|
+
name?: string;
|
|
1154
|
+
description?: string | null;
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
export type ConnectionCredentialsPort = {
|
|
1158
|
+
// Both legs are optional: a host may drive ONLY git creds (BYO-GitHub-App)
|
|
1159
|
+
// and leave sandbox secrets to OpenGeni's local decrypt, or vice-versa. An
|
|
1160
|
+
// unset leg falls through to today's self-mint for THAT leg only.
|
|
1161
|
+
gitCredentials?: (input: GitCredentialsRequest) => Promise<GitCredentials>;
|
|
1162
|
+
sandboxSecrets?: (input: SandboxSecretsRequest) => Promise<SandboxSecrets>;
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
// ============ P4a — GitHub App API port (BYO-App, §7.6 / SPIKE-2 remainder) ===
|
|
1166
|
+
//
|
|
1167
|
+
// The host-driven GitHub-API credential leg. SPIKE-2 closed the establishment +
|
|
1168
|
+
// gate (storage) axis; this closes the credential leg by making the two live
|
|
1169
|
+
// GitHub-API calls host-PROVIDABLE so a BYO-GitHub-App host drives its OWN App
|
|
1170
|
+
// credentials (its own JWT-signing key, its own OAuth client) instead of
|
|
1171
|
+
// OpenGeni self-minting from `settings`:
|
|
1172
|
+
// - verifyInstallationAccessForUser: the OAuth code→token + installation
|
|
1173
|
+
// lookup that PROVES the install is real (today
|
|
1174
|
+
// `verifyGitHubInstallationAccessForUser(settings, …)`).
|
|
1175
|
+
// - listRepositories: the installation-scoped repo listing behind
|
|
1176
|
+
// `GET /v1/workspaces/:id/github/repositories` (today
|
|
1177
|
+
// `listGitHubAppRepositories(settings, …)`).
|
|
1178
|
+
//
|
|
1179
|
+
// Unset (standalone default) → today's `settings`-based self-mint runs
|
|
1180
|
+
// byte-for-byte (the live GitHub-API verify/list against OpenGeni's own App).
|
|
1181
|
+
|
|
1182
|
+
export type GitHubInstallationSummary = {
|
|
1183
|
+
installationId: number;
|
|
1184
|
+
accountLogin: string | null;
|
|
1185
|
+
accountType: string | null;
|
|
1186
|
+
suspended: boolean;
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
export type GitHubAppApiPort = {
|
|
1190
|
+
verifyInstallationAccessForUser?: (input: {
|
|
1191
|
+
code: string;
|
|
1192
|
+
installationId: number;
|
|
1193
|
+
}) => Promise<GitHubInstallationSummary>;
|
|
1194
|
+
listRepositories?: (input: {
|
|
1195
|
+
installationIds?: number[];
|
|
1196
|
+
}) => Promise<GitHubRepository[]>;
|
|
1197
|
+
};
|
|
1198
|
+
|
|
793
1199
|
export const BillingBalance = z.object({
|
|
794
1200
|
accountId: z.string().uuid(),
|
|
795
1201
|
balanceMicros: z.number().int(),
|
|
@@ -949,9 +1355,53 @@ export type DocumentSearchRequest = z.infer<typeof DocumentSearchRequest>;
|
|
|
949
1355
|
export const ToolRef = z.object({
|
|
950
1356
|
kind: z.literal("mcp"),
|
|
951
1357
|
id: z.string().min(1),
|
|
1358
|
+
// Non-fatal-on-connect marker for MCP server refs that can degrade
|
|
1359
|
+
// gracefully. Absent/false is STRICT: the id must be configured and an
|
|
1360
|
+
// unavailable server fails the turn. `optional:true` is preserved for known
|
|
1361
|
+
// servers and makes runtime connect/list failures skip that server; if the
|
|
1362
|
+
// deployment does not configure the id, validation drops the ref. The server
|
|
1363
|
+
// also sets this for auto-attached workspace-default capability MCPs.
|
|
1364
|
+
optional: z.boolean().optional(),
|
|
952
1365
|
});
|
|
953
1366
|
export type ToolRef = z.infer<typeof ToolRef>;
|
|
954
1367
|
|
|
1368
|
+
const registryId = /^[A-Za-z0-9_-]+$/;
|
|
1369
|
+
const httpsUrl = z.string().url().refine((value) => {
|
|
1370
|
+
try {
|
|
1371
|
+
return new URL(value).protocol === "https:";
|
|
1372
|
+
} catch {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
}, { message: "URL must use https" });
|
|
1376
|
+
|
|
1377
|
+
export const SessionMcpServerInput = z.object({
|
|
1378
|
+
id: z.string().min(1).regex(registryId),
|
|
1379
|
+
name: z.string().min(1).optional(),
|
|
1380
|
+
url: httpsUrl,
|
|
1381
|
+
allowedTools: z.array(z.string().min(1)).optional(),
|
|
1382
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
1383
|
+
cacheToolsList: z.boolean().optional(),
|
|
1384
|
+
// Write-only credential headers. Values are encrypted at rest and never
|
|
1385
|
+
// returned in session responses or events; response metadata exposes names.
|
|
1386
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
1387
|
+
});
|
|
1388
|
+
export type SessionMcpServerInput = z.infer<typeof SessionMcpServerInput>;
|
|
1389
|
+
|
|
1390
|
+
export const SessionMcpCredentialUpdateInput = z.object({
|
|
1391
|
+
id: z.string().min(1).regex(registryId),
|
|
1392
|
+
headers: z.record(z.string(), z.string()),
|
|
1393
|
+
});
|
|
1394
|
+
export type SessionMcpCredentialUpdateInput = z.infer<typeof SessionMcpCredentialUpdateInput>;
|
|
1395
|
+
|
|
1396
|
+
export const SessionMcpServerMetadata = z.object({
|
|
1397
|
+
id: z.string().min(1).regex(registryId),
|
|
1398
|
+
name: z.string().min(1).nullable(),
|
|
1399
|
+
url: httpsUrl,
|
|
1400
|
+
headerNames: z.array(z.string()).default([]),
|
|
1401
|
+
credentialVersion: z.number().int().positive(),
|
|
1402
|
+
}).strict();
|
|
1403
|
+
export type SessionMcpServerMetadata = z.infer<typeof SessionMcpServerMetadata>;
|
|
1404
|
+
|
|
955
1405
|
export class ResourceRefConflictError extends Error {
|
|
956
1406
|
constructor(message: string) {
|
|
957
1407
|
super(message);
|
|
@@ -960,17 +1410,26 @@ export class ResourceRefConflictError extends Error {
|
|
|
960
1410
|
}
|
|
961
1411
|
|
|
962
1412
|
export function mergeToolRefs(existing: ToolRef[], additions: ToolRef[]): ToolRef[] {
|
|
963
|
-
const
|
|
964
|
-
const
|
|
1413
|
+
const byKey = new Map<string, ToolRef>();
|
|
1414
|
+
const order: string[] = [];
|
|
965
1415
|
for (const tool of [...existing, ...additions]) {
|
|
966
1416
|
const key = `${tool.kind}:${tool.id}`;
|
|
967
|
-
|
|
1417
|
+
const prior = byKey.get(key);
|
|
1418
|
+
if (!prior) {
|
|
1419
|
+
byKey.set(key, tool);
|
|
1420
|
+
order.push(key);
|
|
968
1421
|
continue;
|
|
969
1422
|
}
|
|
970
|
-
|
|
971
|
-
|
|
1423
|
+
// Strict wins: if the same server appears both optional and strict, the
|
|
1424
|
+
// strict occurrence upgrades the merged ref so an unavailable server fails
|
|
1425
|
+
// the turn. This preserves the fail-loud default when defaults, packs, and
|
|
1426
|
+
// per-turn tool selections are combined.
|
|
1427
|
+
if (prior.optional === true && tool.optional !== true) {
|
|
1428
|
+
const { optional: _dropped, ...strict } = prior;
|
|
1429
|
+
byKey.set(key, strict);
|
|
1430
|
+
}
|
|
972
1431
|
}
|
|
973
|
-
return
|
|
1432
|
+
return order.map((key) => byKey.get(key)!);
|
|
974
1433
|
}
|
|
975
1434
|
|
|
976
1435
|
export function mergeResourceRefs(
|
|
@@ -1735,10 +2194,20 @@ export const Session = z.object({
|
|
|
1735
2194
|
// own id for a singleton group (today's 1:1 default); equals the parent's
|
|
1736
2195
|
// group when spawned shared (both sessions run in ONE box).
|
|
1737
2196
|
sandboxGroupId: z.string().uuid(),
|
|
2197
|
+
// The first-class swappable-sandbox POINTER (bring-your-own-compute M2). NULL
|
|
2198
|
+
// resolves to the session's own group sandbox (the backward-compat default);
|
|
2199
|
+
// a swap sets it to the target sandbox row. active_epoch is the second epoch
|
|
2200
|
+
// ABOVE the lease epoch, bumped on every swap so the routing proxy can fence a
|
|
2201
|
+
// stale in-flight op and retry against the new active sandbox.
|
|
2202
|
+
activeSandboxId: z.string().uuid().nullable(),
|
|
2203
|
+
activeEpoch: z.number().int().nonnegative(),
|
|
1738
2204
|
environmentId: z.string().uuid().nullable(),
|
|
1739
2205
|
// Non-default first-party MCP token permissions (manager-style sessions);
|
|
1740
2206
|
// null means the fixed worker default set.
|
|
1741
2207
|
firstPartyMcpPermissions: z.array(Permission).nullable(),
|
|
2208
|
+
// Per-session third-party MCP servers, metadata only. Credential values are
|
|
2209
|
+
// write-only and never appear here.
|
|
2210
|
+
mcpServers: z.array(SessionMcpServerMetadata).default([]),
|
|
1742
2211
|
// The manager session that spawned this one via session_create (set only
|
|
1743
2212
|
// when the creating grant carried a worker-signed sessionId claim); null for
|
|
1744
2213
|
// direct API creates and scheduled-task runs. When set, this session's
|
|
@@ -1755,6 +2224,12 @@ export const Session = z.object({
|
|
|
1755
2224
|
// signal. Null until a turn with usage has completed.
|
|
1756
2225
|
lastInputTokens: z.number().int().nonnegative().nullable(),
|
|
1757
2226
|
lastSequence: z.number().int().nonnegative(),
|
|
2227
|
+
// Multi-account Codex (P1). codexPinnedCredentialId: the account this session is
|
|
2228
|
+
// manually PINNED to (null ⇒ follow the workspace active pointer).
|
|
2229
|
+
// codexLastCredentialId: the account the most recent turn actually ran on (the
|
|
2230
|
+
// "Running on:" indicator's source). Both are credential-row ids, null until set.
|
|
2231
|
+
codexPinnedCredentialId: z.string().uuid().nullable(),
|
|
2232
|
+
codexLastCredentialId: z.string().uuid().nullable(),
|
|
1758
2233
|
createdAt: z.string(),
|
|
1759
2234
|
updatedAt: z.string(),
|
|
1760
2235
|
});
|
|
@@ -1821,6 +2296,10 @@ export const SessionEventType = z.enum([
|
|
|
1821
2296
|
"terminal.pty.output.delta", // PTY stdout/stderr bytes (separate from command.output)
|
|
1822
2297
|
"terminal.pty.exited", // PTY session ended (exitCode/reason)
|
|
1823
2298
|
"session.title_set",
|
|
2299
|
+
// Multi-account Codex (P1): the account a session's turn runs on changed
|
|
2300
|
+
// (manual switch in P1; failover/rotation in P3 reuse the same event). Drives
|
|
2301
|
+
// the in-session "Running on:" indicator's live flip.
|
|
2302
|
+
"codex.account.switched",
|
|
1824
2303
|
]);
|
|
1825
2304
|
export type SessionEventType = z.infer<typeof SessionEventType>;
|
|
1826
2305
|
|
|
@@ -2306,6 +2785,17 @@ export const CreateSessionRequest = z.object({
|
|
|
2306
2785
|
model: z.string().min(1).optional(),
|
|
2307
2786
|
reasoningEffort: ReasoningEffort.optional(),
|
|
2308
2787
|
sandboxBackend: SandboxBackend.optional(),
|
|
2788
|
+
// The enrolled machine (a sandbox id) to run this session on; seeds the
|
|
2789
|
+
// active-sandbox pointer at creation so the FIRST turn routes to the chosen
|
|
2790
|
+
// machine (race-free: the pointer is committed before the worker turn
|
|
2791
|
+
// workflow can read it). An invalid/unowned/offline target fails the create.
|
|
2792
|
+
targetSandboxId: z.string().uuid().optional(),
|
|
2793
|
+
// The working directory the targeted machine runs the session under — the
|
|
2794
|
+
// path/cwd base for its agent exec, terminal, and file dock. Free-form pass-
|
|
2795
|
+
// through: a launch-workspace_root-relative subdir or an absolute machine path
|
|
2796
|
+
// (the agent's resolve_cwd handles both). Only valid WITH targetSandboxId
|
|
2797
|
+
// (workingDir alone is a 422); omitted ⇒ the machine's default workspace_root.
|
|
2798
|
+
workingDir: z.string().min(1).optional(),
|
|
2309
2799
|
// Workspace environment attachment is fixed at session creation; follow-up
|
|
2310
2800
|
// user.message events cannot switch or add one.
|
|
2311
2801
|
environmentId: z.string().uuid().optional(),
|
|
@@ -2323,6 +2813,9 @@ export const CreateSessionRequest = z.object({
|
|
|
2323
2813
|
// the orchestration/environment/github tools. Capped at creation: every
|
|
2324
2814
|
// requested permission must be held by the creating grant (no escalation).
|
|
2325
2815
|
firstPartyMcpPermissions: z.array(Permission).optional(),
|
|
2816
|
+
// Third-party MCP servers attached only to this session. Credential headers are
|
|
2817
|
+
// write-only: create responses and events expose only SessionMcpServerMetadata.
|
|
2818
|
+
mcpServers: z.array(SessionMcpServerInput).default([]),
|
|
2326
2819
|
// Shared-sandbox placement (addendum 05 §D.1). Three-way union; OMITTED ⇒
|
|
2327
2820
|
// today's behavior (a context-dependent default resolved server-side: from
|
|
2328
2821
|
// inside a session → "shared" with the creator's box, top-level → "new").
|
|
@@ -2335,6 +2828,11 @@ export const CreateSessionRequest = z.object({
|
|
|
2335
2828
|
// A shared spawn inherits the box's (backend, os) — it is literally the same
|
|
2336
2829
|
// box; the child cannot pick its own backend. Cross-workspace sharing is
|
|
2337
2830
|
// forbidden by construction (the parent/group reads are RLS-workspace-scoped).
|
|
2831
|
+
// ENV-AWARE: the box's environment is fixed at creation, so a share requires
|
|
2832
|
+
// the SAME environmentId as the creator's box. On a mismatch the inherited
|
|
2833
|
+
// default silently falls back to an own box; an explicit "shared"/{groupId}
|
|
2834
|
+
// request 422s at create (instead of the first turn dying on the SDK's
|
|
2835
|
+
// manifest-env guard).
|
|
2338
2836
|
sandbox: z.union([
|
|
2339
2837
|
z.literal("shared"),
|
|
2340
2838
|
z.literal("new"),
|
|
@@ -2353,6 +2851,9 @@ export const ClientSessionEvent = z.discriminatedUnion("type", [
|
|
|
2353
2851
|
tools: z.array(ToolRef).default([]),
|
|
2354
2852
|
model: z.string().min(1).optional(),
|
|
2355
2853
|
reasoningEffort: ReasoningEffort.optional(),
|
|
2854
|
+
// Header-value rotation only. URL/name/tool settings are immutable after
|
|
2855
|
+
// session create; persisted events expose metadata, never header values.
|
|
2856
|
+
mcpCredentialUpdates: z.array(SessionMcpCredentialUpdateInput).optional(),
|
|
2356
2857
|
}),
|
|
2357
2858
|
}),
|
|
2358
2859
|
z.object({
|
|
@@ -2432,6 +2933,12 @@ export const CapabilityUnavailableReason = z.enum([
|
|
|
2432
2933
|
"disabled_by_policy",
|
|
2433
2934
|
"lease_cold",
|
|
2434
2935
|
"tier_headless",
|
|
2936
|
+
// Selfhosted (bring-your-own-compute) negotiation states (M1 additive; the
|
|
2937
|
+
// selfhosted negotiation in select.ts wires them in M3):
|
|
2938
|
+
"agent_offline", // the enrolled agent process is not running / unreachable
|
|
2939
|
+
"agent_reconnecting", // a transient blip — the agent is reconnecting (warmable)
|
|
2940
|
+
"consent_required", // whole-machine / screen-control consent not yet acknowledged
|
|
2941
|
+
"display_unavailable", // headless machine with no display stack (no DesktopStream)
|
|
2435
2942
|
]);
|
|
2436
2943
|
export type CapabilityUnavailableReason = z.infer<typeof CapabilityUnavailableReason>;
|
|
2437
2944
|
|
|
@@ -2472,8 +2979,11 @@ export const SessionCapabilities = z.object({
|
|
|
2472
2979
|
reason: CapabilityUnavailableReason.nullable(),
|
|
2473
2980
|
}),
|
|
2474
2981
|
DesktopStream: z.object({
|
|
2475
|
-
|
|
2476
|
-
|
|
2982
|
+
// "relay-frames" is the selfhosted framebuffer stream: PNG-per-frame protobuf
|
|
2983
|
+
// datagrams spliced over the relay (NOT RFB). The viewer renders it with the
|
|
2984
|
+
// "frames" client (a canvas painter), distinct from Modal's "vnc-ws"/"novnc".
|
|
2985
|
+
transport: z.enum(["vnc-ws", "rdp-ws", "webrtc", "relay-frames"]).nullable(),
|
|
2986
|
+
client: z.enum(["novnc", "web-rdp", "frames"]).nullable(),
|
|
2477
2987
|
mode: z.enum(["read-only", "interactive"]).default("read-only"),
|
|
2478
2988
|
url: z.string().url().nullable(),
|
|
2479
2989
|
token: z.string().nullable(),
|
|
@@ -2589,6 +3099,368 @@ export const ViewerHeartbeatResponse = z.object({
|
|
|
2589
3099
|
});
|
|
2590
3100
|
export type ViewerHeartbeatResponse = z.infer<typeof ViewerHeartbeatResponse>;
|
|
2591
3101
|
|
|
3102
|
+
// =============================================================================
|
|
3103
|
+
// Bring-your-own-compute (M5) — enrollment device-flow HTTP contract.
|
|
3104
|
+
//
|
|
3105
|
+
// The HTTP shapes mirror the @opengeni/agent-proto device-flow messages
|
|
3106
|
+
// (DeviceAuthStart*, DeviceAuthPoll*, EnrollmentCredentials) so the Rust agent's
|
|
3107
|
+
// `enroll` command (which runs the flow over HTTP before it has NATS creds)
|
|
3108
|
+
// decodes the SAME field names (the proto's ts-proto JSON is camelCase). The
|
|
3109
|
+
// request bodies additionally carry the consent-relevant fields the dossier brief
|
|
3110
|
+
// mandates (the agent ed25519 pubkey + can-offer-display + requests-screen-control).
|
|
3111
|
+
// =============================================================================
|
|
3112
|
+
|
|
3113
|
+
export const EnrollmentOs = z.enum(["linux", "macos", "windows"]);
|
|
3114
|
+
export type EnrollmentOs = z.infer<typeof EnrollmentOs>;
|
|
3115
|
+
export const EnrollmentArch = z.enum(["x86_64", "aarch64"]);
|
|
3116
|
+
export type EnrollmentArch = z.infer<typeof EnrollmentArch>;
|
|
3117
|
+
|
|
3118
|
+
// POST /enrollments/device/start (agent-side, unauthenticated-at-the-user-level,
|
|
3119
|
+
// rate-limited). The agent presents its ed25519 public key + os/arch + the
|
|
3120
|
+
// requested whole-machine exposure + whether it can offer a display + whether it
|
|
3121
|
+
// requests screen control.
|
|
3122
|
+
export const DeviceEnrollmentStartRequest = z.object({
|
|
3123
|
+
// The agent's ed25519 public key (the machine identity the enrollment binds to).
|
|
3124
|
+
publicKey: z.string().min(1).max(1024),
|
|
3125
|
+
os: EnrollmentOs.default("linux"),
|
|
3126
|
+
arch: EnrollmentArch.default("x86_64"),
|
|
3127
|
+
// Human-friendly machine name (hostname by default).
|
|
3128
|
+
machineName: z.string().min(1).max(256).optional(),
|
|
3129
|
+
// v1 only supports whole-machine; kept explicit so the consent is recorded.
|
|
3130
|
+
exposure: z.literal("whole-machine").default("whole-machine"),
|
|
3131
|
+
// The agent can offer a display (a real screen / Xvfb is available).
|
|
3132
|
+
canOfferDisplay: z.boolean().default(false),
|
|
3133
|
+
// The agent requests screen control (computer-use); the user's allow_screen_control
|
|
3134
|
+
// at approve is the AUTHORITATIVE consent.
|
|
3135
|
+
requestsScreenControl: z.boolean().default(false),
|
|
3136
|
+
// The workspace this machine is enrolling into. The agent is told this at install
|
|
3137
|
+
// (the user picks the workspace, or the install/enroll token carries it). The user
|
|
3138
|
+
// who approves must hold a grant in THIS workspace — that binding is what makes
|
|
3139
|
+
// the (user-unauthenticated) start safe: it cannot grant access to a workspace no
|
|
3140
|
+
// authorized user later approves in.
|
|
3141
|
+
workspaceId: z.string().uuid(),
|
|
3142
|
+
});
|
|
3143
|
+
export type DeviceEnrollmentStartRequest = z.infer<typeof DeviceEnrollmentStartRequest>;
|
|
3144
|
+
|
|
3145
|
+
// The DeviceAuthStart response (field names match the proto's JSON).
|
|
3146
|
+
export const DeviceEnrollmentStartResponse = z.object({
|
|
3147
|
+
deviceCode: z.string(),
|
|
3148
|
+
userCode: z.string(),
|
|
3149
|
+
verificationUri: z.string(),
|
|
3150
|
+
verificationUriComplete: z.string(),
|
|
3151
|
+
intervalSeconds: z.number().int().positive(),
|
|
3152
|
+
expiresInSeconds: z.number().int().positive(),
|
|
3153
|
+
});
|
|
3154
|
+
export type DeviceEnrollmentStartResponse = z.infer<typeof DeviceEnrollmentStartResponse>;
|
|
3155
|
+
|
|
3156
|
+
// POST /enrollments/device/approve (USER-authenticated, workspace-gated). The
|
|
3157
|
+
// LOUD CONSENT step. whole-machine is mandatory (implicit); screen-control is
|
|
3158
|
+
// opt-in per allow_screen_control.
|
|
3159
|
+
export const DeviceEnrollmentApproveRequest = z.object({
|
|
3160
|
+
userCode: z.string().min(1).max(64),
|
|
3161
|
+
allowScreenControl: z.boolean().default(false),
|
|
3162
|
+
});
|
|
3163
|
+
export type DeviceEnrollmentApproveRequest = z.infer<typeof DeviceEnrollmentApproveRequest>;
|
|
3164
|
+
|
|
3165
|
+
export const DeviceEnrollmentApproveResponse = z.object({
|
|
3166
|
+
approved: z.boolean(),
|
|
3167
|
+
enrollmentId: z.string().uuid(),
|
|
3168
|
+
sandboxId: z.string().uuid(),
|
|
3169
|
+
allowScreenControl: z.boolean(),
|
|
3170
|
+
});
|
|
3171
|
+
export type DeviceEnrollmentApproveResponse = z.infer<typeof DeviceEnrollmentApproveResponse>;
|
|
3172
|
+
|
|
3173
|
+
// POST /enrollments/device/poll (agent-side). The poll state machine.
|
|
3174
|
+
export const DeviceEnrollmentPollRequest = z.object({
|
|
3175
|
+
deviceCode: z.string().min(1).max(256),
|
|
3176
|
+
});
|
|
3177
|
+
export type DeviceEnrollmentPollRequest = z.infer<typeof DeviceEnrollmentPollRequest>;
|
|
3178
|
+
|
|
3179
|
+
export const DeviceEnrollmentState = z.enum(["pending", "authorized", "denied", "expired", "disabled"]);
|
|
3180
|
+
export type DeviceEnrollmentState = z.infer<typeof DeviceEnrollmentState>;
|
|
3181
|
+
|
|
3182
|
+
// The EnrollmentCredentials (field names match the proto's JSON). natsAccountCreds
|
|
3183
|
+
// is a PLACEHOLDER — the real per-workspace NATS Account creds binding is
|
|
3184
|
+
// infra-deferred (M4/relay); the bearer + subjectPrefix are the application-tier
|
|
3185
|
+
// identity the agent presents today.
|
|
3186
|
+
export const EnrollmentCredentialsResponse = z.object({
|
|
3187
|
+
agentId: z.string().uuid(),
|
|
3188
|
+
workspaceId: z.string().uuid(),
|
|
3189
|
+
// The signed bearer the agent presents to the control plane (the `oge_` token).
|
|
3190
|
+
bearer: z.string(),
|
|
3191
|
+
// The Account-scoped control-plane subject prefix the agent subscribes to:
|
|
3192
|
+
// agent.<workspaceId>.<agentId>.
|
|
3193
|
+
subjectPrefix: z.string(),
|
|
3194
|
+
// Connect info for the control plane + stream relay (may be empty when not yet
|
|
3195
|
+
// configured for this deployment — the agent surfaces "control plane unconfigured").
|
|
3196
|
+
natsUrls: z.array(z.string()),
|
|
3197
|
+
relayUrl: z.string(),
|
|
3198
|
+
// The agent's PRODUCER token for the relay edge (the `ogr_` token; M8b). Presented
|
|
3199
|
+
// as StreamOpen.token when the agent registers a pty/desktop channel; the relay
|
|
3200
|
+
// verifies it then pairs the producer with the viewer (whose `ogs_` token the
|
|
3201
|
+
// relay also verifies). Empty when the relay-token plane is unconfigured for this
|
|
3202
|
+
// deployment (graceful degrade — the agent then presents an empty token the relay
|
|
3203
|
+
// rejects, surfacing the gap loudly rather than silently producing a dead stream).
|
|
3204
|
+
relayToken: z.string(),
|
|
3205
|
+
// VESTIGIAL (M-AUTH): there is no per-machine NATS Account creds file. The agent
|
|
3206
|
+
// presents the `bearer` above as the NATS connect AUTH-TOKEN; the server's
|
|
3207
|
+
// auth-callout responder validates it and mints a workspace-scoped user JWT. This
|
|
3208
|
+
// field echoes the bearer so a consumer reading it as the connect credential still
|
|
3209
|
+
// works; new consumers should read `bearer` directly.
|
|
3210
|
+
natsAccountCreds: z.string(),
|
|
3211
|
+
// The minisign public key the agent pins for self-update verification.
|
|
3212
|
+
updatePublicKey: z.string(),
|
|
3213
|
+
consentedWholeMachine: z.boolean(),
|
|
3214
|
+
consentedScreenControl: z.boolean(),
|
|
3215
|
+
});
|
|
3216
|
+
export type EnrollmentCredentialsResponse = z.infer<typeof EnrollmentCredentialsResponse>;
|
|
3217
|
+
|
|
3218
|
+
export const DeviceEnrollmentPollResponse = z.object({
|
|
3219
|
+
state: DeviceEnrollmentState,
|
|
3220
|
+
// Present only when state === "authorized".
|
|
3221
|
+
credentials: EnrollmentCredentialsResponse.optional(),
|
|
3222
|
+
});
|
|
3223
|
+
export type DeviceEnrollmentPollResponse = z.infer<typeof DeviceEnrollmentPollResponse>;
|
|
3224
|
+
|
|
3225
|
+
// GET /enrollments — a workspace's machines (the Machines dashboard surface).
|
|
3226
|
+
export const EnrollmentSummary = z.object({
|
|
3227
|
+
id: z.string().uuid(),
|
|
3228
|
+
pubkey: z.string(),
|
|
3229
|
+
exposure: z.literal("whole-machine"),
|
|
3230
|
+
hasDisplay: z.boolean(),
|
|
3231
|
+
allowScreenControl: z.boolean(),
|
|
3232
|
+
status: z.enum(["active", "revoked"]),
|
|
3233
|
+
os: EnrollmentOs,
|
|
3234
|
+
arch: z.string(),
|
|
3235
|
+
lastSeenAt: z.string().nullable(),
|
|
3236
|
+
createdAt: z.string(),
|
|
3237
|
+
revokedAt: z.string().nullable(),
|
|
3238
|
+
});
|
|
3239
|
+
export type EnrollmentSummary = z.infer<typeof EnrollmentSummary>;
|
|
3240
|
+
|
|
3241
|
+
export const ListEnrollmentsResponse = z.object({
|
|
3242
|
+
enrollments: z.array(EnrollmentSummary),
|
|
3243
|
+
});
|
|
3244
|
+
export type ListEnrollmentsResponse = z.infer<typeof ListEnrollmentsResponse>;
|
|
3245
|
+
|
|
3246
|
+
export const RevokeEnrollmentResponse = z.object({
|
|
3247
|
+
revoked: z.boolean(),
|
|
3248
|
+
});
|
|
3249
|
+
export type RevokeEnrollmentResponse = z.infer<typeof RevokeEnrollmentResponse>;
|
|
3250
|
+
|
|
3251
|
+
// =============================================================================
|
|
3252
|
+
// Enrollment UX (self-hosted enrollment UX, design 11): the click-Grant approve
|
|
3253
|
+
// page lookup/deny + the headless enroll-token mint/exchange. These sit beside the
|
|
3254
|
+
// device-flow contracts above and REUSE EnrollmentCredentialsResponse for the
|
|
3255
|
+
// exchange's credential payload (identical shape to the poll authorized branch).
|
|
3256
|
+
// =============================================================================
|
|
3257
|
+
|
|
3258
|
+
// POST /v1/enrollments/device/lookup (USER-authenticated, NO workspace in the
|
|
3259
|
+
// path). The approve page (EnrollmentConsent) needs the machine details for a
|
|
3260
|
+
// user_code WITHOUT consuming the request. The user_code is globally unique among
|
|
3261
|
+
// pending rows; the route resolves its workspace, authorizes (enrollments:read),
|
|
3262
|
+
// and returns the machine details — or 404 (never revealing cross-workspace
|
|
3263
|
+
// existence) when the grant check fails or no live pending row matches.
|
|
3264
|
+
export const DeviceEnrollmentLookupRequest = z.object({
|
|
3265
|
+
userCode: z.string().min(1).max(64),
|
|
3266
|
+
});
|
|
3267
|
+
export type DeviceEnrollmentLookupRequest = z.infer<typeof DeviceEnrollmentLookupRequest>;
|
|
3268
|
+
|
|
3269
|
+
// The presentational machine details the consent screen renders (a subset of the
|
|
3270
|
+
// pending request — NO secrets, NO device_code).
|
|
3271
|
+
export const DeviceEnrollmentLookupMachine = z.object({
|
|
3272
|
+
machineName: z.string().nullable(),
|
|
3273
|
+
os: EnrollmentOs,
|
|
3274
|
+
arch: z.string(),
|
|
3275
|
+
canOfferDisplay: z.boolean(),
|
|
3276
|
+
requestsScreenControl: z.boolean(),
|
|
3277
|
+
});
|
|
3278
|
+
export type DeviceEnrollmentLookupMachine = z.infer<typeof DeviceEnrollmentLookupMachine>;
|
|
3279
|
+
|
|
3280
|
+
export const DeviceEnrollmentLookupResponse = z.object({
|
|
3281
|
+
workspaceId: z.string().uuid(),
|
|
3282
|
+
userCode: z.string(),
|
|
3283
|
+
machine: DeviceEnrollmentLookupMachine,
|
|
3284
|
+
expiresAt: z.string(),
|
|
3285
|
+
});
|
|
3286
|
+
export type DeviceEnrollmentLookupResponse = z.infer<typeof DeviceEnrollmentLookupResponse>;
|
|
3287
|
+
|
|
3288
|
+
// POST /v1/workspaces/:workspaceId/enrollments/device/deny (USER-authenticated,
|
|
3289
|
+
// enrollments:manage). The explicit "no" at the approve page — mirrors approve.
|
|
3290
|
+
export const DeviceEnrollmentDenyRequest = z.object({
|
|
3291
|
+
userCode: z.string().min(1).max(64),
|
|
3292
|
+
});
|
|
3293
|
+
export type DeviceEnrollmentDenyRequest = z.infer<typeof DeviceEnrollmentDenyRequest>;
|
|
3294
|
+
|
|
3295
|
+
export const DeviceEnrollmentDenyResponse = z.object({
|
|
3296
|
+
denied: z.boolean(),
|
|
3297
|
+
});
|
|
3298
|
+
export type DeviceEnrollmentDenyResponse = z.infer<typeof DeviceEnrollmentDenyResponse>;
|
|
3299
|
+
|
|
3300
|
+
// POST /v1/workspaces/:workspaceId/enrollments/token (USER-authenticated,
|
|
3301
|
+
// enrollments:manage). Mints the short-TTL headless enroll token (the `oget_`
|
|
3302
|
+
// token). allowScreenControl bakes the screen-control consent into the token.
|
|
3303
|
+
export const MintEnrollTokenRequest = z.object({
|
|
3304
|
+
allowScreenControl: z.boolean().default(false),
|
|
3305
|
+
});
|
|
3306
|
+
export type MintEnrollTokenRequest = z.infer<typeof MintEnrollTokenRequest>;
|
|
3307
|
+
|
|
3308
|
+
export const MintEnrollTokenResponse = z.object({
|
|
3309
|
+
// The `oget_` token. SECRET — the UI shows it once with a copy-now warning.
|
|
3310
|
+
token: z.string(),
|
|
3311
|
+
expiresAt: z.string(),
|
|
3312
|
+
expiresInSeconds: z.number().int().positive(),
|
|
3313
|
+
});
|
|
3314
|
+
export type MintEnrollTokenResponse = z.infer<typeof MintEnrollTokenResponse>;
|
|
3315
|
+
|
|
3316
|
+
// POST /v1/enrollments/token/exchange (UNAUTHENTICATED — the token IS the auth).
|
|
3317
|
+
// The agent presents the same identity fields it sends to device/start plus the
|
|
3318
|
+
// enroll token. On a valid token the control plane performs the SAME finalize as
|
|
3319
|
+
// approve and returns the IDENTICAL EnrollmentCredentialsResponse shape (so the
|
|
3320
|
+
// agent's existing credential parsing is reused).
|
|
3321
|
+
export const EnrollTokenExchangeRequest = z.object({
|
|
3322
|
+
// The `oget_` enroll token (the auth + the workspace/account/consent grant).
|
|
3323
|
+
token: z.string().min(1),
|
|
3324
|
+
// The agent's ed25519 public key (the machine identity the enrollment binds to).
|
|
3325
|
+
publicKey: z.string().min(1).max(1024),
|
|
3326
|
+
os: EnrollmentOs.default("linux"),
|
|
3327
|
+
arch: EnrollmentArch.default("x86_64"),
|
|
3328
|
+
machineName: z.string().min(1).max(256).optional(),
|
|
3329
|
+
// v1 only supports whole-machine; kept explicit so the consent is recorded.
|
|
3330
|
+
exposure: z.literal("whole-machine").default("whole-machine"),
|
|
3331
|
+
canOfferDisplay: z.boolean().default(false),
|
|
3332
|
+
// The agent's REQUEST; the token's allowScreenControl is the AUTHORITATIVE consent.
|
|
3333
|
+
requestsScreenControl: z.boolean().default(false),
|
|
3334
|
+
});
|
|
3335
|
+
export type EnrollTokenExchangeRequest = z.infer<typeof EnrollTokenExchangeRequest>;
|
|
3336
|
+
|
|
3337
|
+
// The exchange wraps the EXISTING EnrollmentCredentialsResponse — IDENTICAL to the
|
|
3338
|
+
// poll authorized branch's `credentials` (NOT a redefined credential shape).
|
|
3339
|
+
export const EnrollTokenExchangeResponse = z.object({
|
|
3340
|
+
credentials: EnrollmentCredentialsResponse,
|
|
3341
|
+
});
|
|
3342
|
+
export type EnrollTokenExchangeResponse = z.infer<typeof EnrollTokenExchangeResponse>;
|
|
3343
|
+
|
|
3344
|
+
// ── Machines dashboard + per-machine metrics (M10, dossier §10.7) ────────────
|
|
3345
|
+
//
|
|
3346
|
+
// The SHARED data contract M10 (backend) implements + M9 (UI) renders. THE
|
|
3347
|
+
// orchestrator owns this shape; M9 imports these types so the dashboard never
|
|
3348
|
+
// drifts from the API. The fields mirror the agent's MetricsSample wire shape
|
|
3349
|
+
// (`@opengeni/agent-proto`) projected to the dashboard's JSON, plus the derived
|
|
3350
|
+
// machine state matrix (the M3 liveness + the consent/display reasons).
|
|
3351
|
+
|
|
3352
|
+
/**
|
|
3353
|
+
* A point-in-time machine metrics sample as the dashboard reads it. `cpuPct` and
|
|
3354
|
+
* the load averages are 0..N doubles; the byte figures are integers; `gpuUtilPct`
|
|
3355
|
+
* / `gpuMemBytes` are null when no GPU was present at sample time (the wire
|
|
3356
|
+
* contract: absence == not-reported, NEVER a real zero). `runQueue` is the
|
|
3357
|
+
* runnable-count contention signal. `sampledAt` is an ISO-8601 instant.
|
|
3358
|
+
*/
|
|
3359
|
+
export const MetricSample = z.object({
|
|
3360
|
+
cpuPct: z.number(),
|
|
3361
|
+
load1: z.number(),
|
|
3362
|
+
load5: z.number(),
|
|
3363
|
+
load15: z.number(),
|
|
3364
|
+
memUsedBytes: z.number().int(),
|
|
3365
|
+
memTotalBytes: z.number().int(),
|
|
3366
|
+
diskUsedBytes: z.number().int(),
|
|
3367
|
+
diskTotalBytes: z.number().int(),
|
|
3368
|
+
gpuUtilPct: z.number().nullable(),
|
|
3369
|
+
gpuMemBytes: z.number().int().nullable(),
|
|
3370
|
+
runQueue: z.number(),
|
|
3371
|
+
sampledAt: z.string(),
|
|
3372
|
+
});
|
|
3373
|
+
export type MetricSample = z.infer<typeof MetricSample>;
|
|
3374
|
+
|
|
3375
|
+
/** The derived dashboard state of a machine. The M3 liveness
|
|
3376
|
+
* (online/reconnecting/offline) plus the enrollment-derived consent/display
|
|
3377
|
+
* reasons (consent_required / display_unavailable) and the in-flight device-flow
|
|
3378
|
+
* (enrolling). */
|
|
3379
|
+
export const MachineState = z.enum([
|
|
3380
|
+
"online",
|
|
3381
|
+
"reconnecting",
|
|
3382
|
+
"offline",
|
|
3383
|
+
"consent_required",
|
|
3384
|
+
"display_unavailable",
|
|
3385
|
+
"enrolling",
|
|
3386
|
+
]);
|
|
3387
|
+
export type MachineState = z.infer<typeof MachineState>;
|
|
3388
|
+
|
|
3389
|
+
export const MachineKind = z.enum(["modal", "selfhosted"]);
|
|
3390
|
+
export type MachineKind = z.infer<typeof MachineKind>;
|
|
3391
|
+
|
|
3392
|
+
/**
|
|
3393
|
+
* A machine as the Machines dashboard renders it. The workspace's enrolled
|
|
3394
|
+
* selfhosted machines PLUS the session's synthetic Modal group box
|
|
3395
|
+
* (`isSessionGroup: true`). `active` marks the session's currently-active
|
|
3396
|
+
* routing target. `sharedSessionCount` is the lease refcount (how many sessions
|
|
3397
|
+
* share this whole machine). `metrics` is the latest sample, or null when none
|
|
3398
|
+
* has landed yet (just enrolled / offline before a first heartbeat).
|
|
3399
|
+
*/
|
|
3400
|
+
export const MachineView = z.object({
|
|
3401
|
+
sandboxId: z.string(),
|
|
3402
|
+
enrollmentId: z.string().nullable(),
|
|
3403
|
+
name: z.string(),
|
|
3404
|
+
kind: MachineKind,
|
|
3405
|
+
state: MachineState,
|
|
3406
|
+
active: z.boolean(),
|
|
3407
|
+
isSessionGroup: z.boolean(),
|
|
3408
|
+
os: z.string(),
|
|
3409
|
+
arch: z.string(),
|
|
3410
|
+
hasDisplay: z.boolean(),
|
|
3411
|
+
allowScreenControl: z.boolean(),
|
|
3412
|
+
sharedSessionCount: z.number().int(),
|
|
3413
|
+
lastSeenAt: z.string().nullable(),
|
|
3414
|
+
metrics: MetricSample.nullable(),
|
|
3415
|
+
});
|
|
3416
|
+
export type MachineView = z.infer<typeof MachineView>;
|
|
3417
|
+
|
|
3418
|
+
/**
|
|
3419
|
+
* GET /v1/workspaces/:ws/machines — the dashboard list. `activeSandboxId` /
|
|
3420
|
+
* `activeEpoch` echo the session's epoch-fenced active-sandbox pointer (null
|
|
3421
|
+
* activeSandboxId == the session's own group box is active).
|
|
3422
|
+
*/
|
|
3423
|
+
export const MachinesResponse = z.object({
|
|
3424
|
+
activeSandboxId: z.string().nullable(),
|
|
3425
|
+
activeEpoch: z.number().int(),
|
|
3426
|
+
machines: z.array(MachineView),
|
|
3427
|
+
});
|
|
3428
|
+
export type MachinesResponse = z.infer<typeof MachinesResponse>;
|
|
3429
|
+
|
|
3430
|
+
/**
|
|
3431
|
+
* POST /v1/workspaces/:ws/sessions/:sessionId/active-sandbox — the user-
|
|
3432
|
+
* authenticated swap of a session's active sandbox (the same epoch-fenced
|
|
3433
|
+
* mechanic the M7 `sandbox_swap` MCP tool exposes to the agent). `target` is a
|
|
3434
|
+
* `MachinesResponse` machine's `sandboxId`, or "session"/"default" to swap back
|
|
3435
|
+
* to the session's own group box.
|
|
3436
|
+
*/
|
|
3437
|
+
export const SwapActiveSandboxRequest = z.object({
|
|
3438
|
+
target: z.string().min(1),
|
|
3439
|
+
});
|
|
3440
|
+
export type SwapActiveSandboxRequest = z.infer<typeof SwapActiveSandboxRequest>;
|
|
3441
|
+
|
|
3442
|
+
/**
|
|
3443
|
+
* The swap outcome (mirrors the server `FleetSwapResult`). `swapped` is true on a
|
|
3444
|
+
* successful repoint OR a no-op (already pointed there); `reason` carries the
|
|
3445
|
+
* failure detail (unowned/offline target, or a lost epoch fence) when false.
|
|
3446
|
+
*/
|
|
3447
|
+
export const SwapActiveSandboxResponse = z.object({
|
|
3448
|
+
swapped: z.boolean(),
|
|
3449
|
+
activeSandboxId: z.string().nullable(),
|
|
3450
|
+
activeEpoch: z.number().int(),
|
|
3451
|
+
reason: z.string().optional(),
|
|
3452
|
+
});
|
|
3453
|
+
export type SwapActiveSandboxResponse = z.infer<typeof SwapActiveSandboxResponse>;
|
|
3454
|
+
|
|
3455
|
+
/**
|
|
3456
|
+
* GET /v1/workspaces/:ws/machines/:enrollmentId/metrics/series?window=1h — the
|
|
3457
|
+
* downsampled (~1/min) history the dashboard time-range reads.
|
|
3458
|
+
*/
|
|
3459
|
+
export const MachineMetricsSeriesResponse = z.object({
|
|
3460
|
+
samples: z.array(MetricSample),
|
|
3461
|
+
});
|
|
3462
|
+
export type MachineMetricsSeriesResponse = z.infer<typeof MachineMetricsSeriesResponse>;
|
|
3463
|
+
|
|
2592
3464
|
/**
|
|
2593
3465
|
* A single host-exposed model + the provider that serves it, as surfaced to
|
|
2594
3466
|
* clients (SDK + React composer) by GET /v1/config/client. The wire `api`
|