1import * as Context from "effect/Context";
2import type * as DateTime from "effect/DateTime";
3import * as Schema from "effect/Schema";
4import * as HttpApi from "effect/http-api/HttpApi";
5import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint";
6import * as HttpApiGroup from "effect/http-api/HttpApiGroup";
7import * as HttpApiMiddleware from "effect/http-api/HttpApiMiddleware";
8import * as HttpApiSchema from "effect/http-api/HttpApiSchema";
9import * as HttpServerRespondable from "effect/http/HttpServerRespondable";
10import * as HttpServerResponse from "effect/http/HttpServerResponse";
11
12import {
13 AuthAccessTokenResult,
14 AuthBrowserSessionRequest,
15 AuthBrowserSessionResult,
16 AuthClientSession,
17 AuthCreatePairingCredentialInput,
18 AuthCreateDelegatedSessionInput,
19 AuthMcpApprovalDecisionRequest,
20 AuthMcpApprovalDetails,
21 AuthMcpApprovalError,
22 AuthMcpApprovalRedirect,
23 AuthMcpAuthorizationRequest,
24 AuthMcpAuthorizationServerMetadata,
25 AuthMcpClientRegistration,
26 AuthMcpProtectedResourceMetadata,
27 AuthMcpRegisteredClient,
28 AuthMcpRegistrationError,
29 AuthMcpTokenError,
30 AuthMcpTokenRequest,
31 AuthMcpTokenResult,
32 AuthPairingCredentialResult,
33 AuthPairingLink,
34 AuthRevokeClientSessionInput,
35 AuthRevokePairingLinkInput,
36 AuthEnvironmentScope,
37 AuthTokenExchangeRequest,
38 AuthSessionState,
39 AuthWebSocketTicketResult,
40 ServerAuthSessionMethod,
41} from "./auth.ts";
42import {
43 ExecutionEnvironmentDescriptor,
44 ORCHESTRATION_PROTOCOL_HEADER,
45 ORCHESTRATION_PROTOCOL_VERSION_TEXT,
46} from "./environment.ts";
47import {
48 DpopFailureReason,
49 AuthSessionId,
50 ThreadId,
51 TrimmedNonEmptyString,
52} from "./baseSchemas.ts";
53import {
54 OrchestrationV2ShellSnapshot,
55 OrchestrationV2ThreadBoundedSnapshot,
56 OrchestrationV2ThreadDetailSnapshot,
57 OrchestrationV2ThreadHistoryPage,
58 OrchestrationV2ThreadTranscript,
59} from "./orchestrationV2.ts";
60import { Project, ProjectMutation, ProjectSnapshot } from "./project.ts";
61import {
62 PullRequestDiffInput,
63 PullRequestDiffResult,
64 PullRequestOperationError,
65 PullRequestUnavailableError,
66} from "./pullRequest.ts";
67import {
68 RelayCloudEnvironmentHealthRequest,
69 RelayCloudMintCredentialRequest,
70 RelayEnvironmentConfigRequest,
71 RelayEnvironmentHealthResponse,
72 RelayEnvironmentLinkProof,
73 RelayEnvironmentMintResponse,
74 RelayLinkProofRequest,
75} from "./relay.ts";
76
77const OptionalBearerHeaders = Schema.Struct({
78 "x-csrf-token": Schema.optionalKey(Schema.String),
79 authorization: Schema.optionalKey(Schema.String),
80 dpop: Schema.optionalKey(Schema.String),
81});
82
83const OrchestrationProtocolHeaders = Schema.Struct({
84 "x-csrf-token": Schema.optionalKey(Schema.String),
85 authorization: Schema.optionalKey(Schema.String),
86 dpop: Schema.optionalKey(Schema.String),
87 [ORCHESTRATION_PROTOCOL_HEADER]: Schema.Literal(ORCHESTRATION_PROTOCOL_VERSION_TEXT),
88});
89
90const OptionalDpopProofHeaders = Schema.Struct({
91 "x-csrf-token": Schema.optionalKey(Schema.String),
92 dpop: Schema.optionalKey(Schema.String),
93});
94
95export const EnvironmentRequestInvalidReason = Schema.Literals([
96 "invalid_scope",
97 "scope_not_granted",
98 "invalid_command",
99 "invalid_history_cursor",
100 "thread_transcript_too_large",
101]);
102export type EnvironmentRequestInvalidReason = typeof EnvironmentRequestInvalidReason.Type;
103
104export const EnvironmentAuthInvalidReason = Schema.Literals([
105 "missing_credential",
106 "invalid_credential",
107]);
108export type EnvironmentAuthInvalidReason = typeof EnvironmentAuthInvalidReason.Type;
109
110export const EnvironmentOperationForbiddenReason = Schema.Literals([
111 "current_session_revoke_not_allowed",
112]);
113export type EnvironmentOperationForbiddenReason = typeof EnvironmentOperationForbiddenReason.Type;
114
115export const EnvironmentInternalErrorReason = Schema.Literals([
116 "bootstrap_validation_failed",
117 "browser_session_issuance_failed",
118 "browser_session_cookie_failed",
119 "access_token_issuance_failed",
120 "websocket_ticket_issuance_failed",
121 "pairing_credential_issuance_failed",
122 "pairing_links_load_failed",
123 "pairing_link_revoke_failed",
124 "client_sessions_load_failed",
125 "client_session_revoke_failed",
126 "project_snapshot_failed",
127 "project_mutation_failed",
128 "orchestration_snapshot_failed",
129 "orchestration_thread_snapshot_failed",
130 "orchestration_thread_bounded_snapshot_failed",
131 "orchestration_thread_history_failed",
132 "internal_error",
133]);
134export type EnvironmentInternalErrorReason = typeof EnvironmentInternalErrorReason.Type;
135
136export class EnvironmentRequestInvalidError extends Schema.TaggedError<EnvironmentRequestInvalidError>()(
137 "EnvironmentRequestInvalidError",
138 {
139 code: Schema.Literal("invalid_request"),
140 reason: EnvironmentRequestInvalidReason,
141 traceId: TrimmedNonEmptyString,
142 },
143 { httpApiStatus: 400 },
144) {
145 [HttpServerRespondable.symbol]() {
146 return HttpServerResponse.schemaJson(EnvironmentRequestInvalidError)(this, { status: 400 });
147 }
148
149 override get message(): string {
150 return `The environment rejected the request (${this.reason}).`;
151 }
152}
153
154export class EnvironmentAuthInvalidError extends Schema.TaggedError<EnvironmentAuthInvalidError>()(
155 "EnvironmentAuthInvalidError",
156 {
157 code: Schema.Literal("auth_invalid"),
158 reason: EnvironmentAuthInvalidReason,
159 // Older servers do not send a DPoP failure category.
160 dpopFailureReason: Schema.optionalKey(DpopFailureReason),
161 traceId: TrimmedNonEmptyString,
162 },
163 { httpApiStatus: 401 },
164) {
165 [HttpServerRespondable.symbol]() {
166 return HttpServerResponse.schemaJson(EnvironmentAuthInvalidError)(this, { status: 401 });
167 }
168
169 override get message(): string {
170 return `The environment rejected this client's credentials (${this.reason}).`;
171 }
172}
173
174export class EnvironmentScopeRequiredError extends Schema.TaggedError<EnvironmentScopeRequiredError>()(
175 "EnvironmentScopeRequiredError",
176 {
177 code: Schema.Literal("insufficient_scope"),
178 requiredScope: AuthEnvironmentScope,
179 requiredPermission: Schema.optionalKey(Schema.String),
180 traceId: TrimmedNonEmptyString,
181 },
182 { httpApiStatus: 403 },
183) {
184 [HttpServerRespondable.symbol]() {
185 return HttpServerResponse.schemaJson(EnvironmentScopeRequiredError)(this, { status: 403 });
186 }
187
188 override get message(): string {
189 return `This request needs the ${this.requiredPermission ?? this.requiredScope} scope, which this client does not have.`;
190 }
191}
192
193export class EnvironmentOperationForbiddenError extends Schema.TaggedError<EnvironmentOperationForbiddenError>()(
194 "EnvironmentOperationForbiddenError",
195 {
196 code: Schema.Literal("operation_forbidden"),
197 reason: EnvironmentOperationForbiddenReason,
198 traceId: TrimmedNonEmptyString,
199 },
200 { httpApiStatus: 403 },
201) {
202 [HttpServerRespondable.symbol]() {
203 return HttpServerResponse.schemaJson(EnvironmentOperationForbiddenError)(this, { status: 403 });
204 }
205
206 override get message(): string {
207 return `The environment refused this operation (${this.reason}).`;
208 }
209}
210
211export class EnvironmentInternalError extends Schema.TaggedError<EnvironmentInternalError>()(
212 "EnvironmentInternalError",
213 {
214 code: Schema.Literal("internal_error"),
215 reason: EnvironmentInternalErrorReason,
216 traceId: TrimmedNonEmptyString,
217 },
218 { httpApiStatus: 500 },
219) {
220 [HttpServerRespondable.symbol]() {
221 return HttpServerResponse.schemaJson(EnvironmentInternalError)(this, { status: 500 });
222 }
223
224 override get message(): string {
225 return `The environment failed to answer this request (${this.reason}).`;
226 }
227}
228
229export const EnvironmentResourceNotFoundReason = Schema.Literals(["thread_not_found"]);
230export type EnvironmentResourceNotFoundReason = typeof EnvironmentResourceNotFoundReason.Type;
231
232export class EnvironmentResourceNotFoundError extends Schema.TaggedError<EnvironmentResourceNotFoundError>()(
233 "EnvironmentResourceNotFoundError",
234 {
235 code: Schema.Literal("not_found"),
236 reason: EnvironmentResourceNotFoundReason,
237 traceId: TrimmedNonEmptyString,
238 },
239 { httpApiStatus: 404 },
240) {
241 [HttpServerRespondable.symbol]() {
242 return HttpServerResponse.schemaJson(EnvironmentResourceNotFoundError)(this, { status: 404 });
243 }
244
245 override get message(): string {
246 return `The environment could not find what this request named (${this.reason}).`;
247 }
248}
249
250export const EnvironmentHttpCommonError = Schema.Union([
251 EnvironmentRequestInvalidError,
252 EnvironmentAuthInvalidError,
253 EnvironmentScopeRequiredError,
254 EnvironmentOperationForbiddenError,
255 EnvironmentResourceNotFoundError,
256 EnvironmentInternalError,
257]);
258export type EnvironmentHttpCommonError = typeof EnvironmentHttpCommonError.Type;
259
260const EnvironmentAuthenticationErrors = [
261 EnvironmentAuthInvalidError,
262 EnvironmentInternalError,
263] as const;
264
265export class EnvironmentHttpBadRequestError extends Schema.TaggedError<EnvironmentHttpBadRequestError>()(
266 "EnvironmentHttpBadRequestError",
267 {
268 message: Schema.String,
269 },
270 { httpApiStatus: 400 },
271) {
272 [HttpServerRespondable.symbol]() {
273 return HttpServerResponse.schemaJson(EnvironmentHttpBadRequestError)(this, { status: 400 });
274 }
275}
276
277export class EnvironmentHttpUnauthorizedError extends Schema.TaggedError<EnvironmentHttpUnauthorizedError>()(
278 "EnvironmentHttpUnauthorizedError",
279 {
280 message: Schema.String,
281 },
282 { httpApiStatus: 401 },
283) {
284 [HttpServerRespondable.symbol]() {
285 return HttpServerResponse.schemaJson(EnvironmentHttpUnauthorizedError)(this, { status: 401 });
286 }
287}
288
289export class EnvironmentHttpForbiddenError extends Schema.TaggedError<EnvironmentHttpForbiddenError>()(
290 "EnvironmentHttpForbiddenError",
291 {
292 message: Schema.String,
293 },
294 { httpApiStatus: 403 },
295) {
296 [HttpServerRespondable.symbol]() {
297 return HttpServerResponse.schemaJson(EnvironmentHttpForbiddenError)(this, { status: 403 });
298 }
299}
300
301export class EnvironmentHttpInternalServerError extends Schema.TaggedError<EnvironmentHttpInternalServerError>()(
302 "EnvironmentHttpInternalServerError",
303 {
304 message: Schema.String,
305 },
306 { httpApiStatus: 500 },
307) {
308 [HttpServerRespondable.symbol]() {
309 return HttpServerResponse.schemaJson(EnvironmentHttpInternalServerError)(this, { status: 500 });
310 }
311}
312
313export class EnvironmentHttpConflictError extends Schema.TaggedError<EnvironmentHttpConflictError>()(
314 "EnvironmentHttpConflictError",
315 {
316 message: Schema.String,
317 },
318 { httpApiStatus: 409 },
319) {
320 [HttpServerRespondable.symbol]() {
321 return HttpServerResponse.schemaJson(EnvironmentHttpConflictError)(this, { status: 409 });
322 }
323}
324
325export class EnvironmentCloudEndpointUnavailableError extends Schema.TaggedError<EnvironmentCloudEndpointUnavailableError>()(
326 "EnvironmentCloudEndpointUnavailableError",
327 {
328 message: Schema.String,
329 endpointRuntimeStatus: Schema.Unknown,
330 },
331 { httpApiStatus: 503 },
332) {
333 [HttpServerRespondable.symbol]() {
334 return HttpServerResponse.schemaJson(EnvironmentCloudEndpointUnavailableError)(this, {
335 status: 503,
336 });
337 }
338}
339const EnvironmentSessionCreationErrors = [
340 EnvironmentAuthInvalidError,
341 EnvironmentInternalError,
342] as const;
343const EnvironmentTokenExchangeErrors = [
344 EnvironmentRequestInvalidError,
345 EnvironmentAuthInvalidError,
346 EnvironmentInternalError,
347] as const;
348const EnvironmentScopedOperationErrors = [
349 EnvironmentScopeRequiredError,
350 EnvironmentInternalError,
351] as const;
352const EnvironmentPairingCredentialErrors = [
353 EnvironmentRequestInvalidError,
354 ...EnvironmentScopedOperationErrors,
355] as const;
356const EnvironmentSessionRevokeErrors = [
357 EnvironmentScopeRequiredError,
358 EnvironmentOperationForbiddenError,
359 EnvironmentInternalError,
360] as const;
361const EnvironmentProjectSnapshotErrors = [
362 EnvironmentScopeRequiredError,
363 EnvironmentInternalError,
364] as const;
365const EnvironmentOrchestrationSnapshotErrors = [
366 EnvironmentScopeRequiredError,
367 EnvironmentInternalError,
368] as const;
369const EnvironmentOrchestrationThreadSnapshotErrors = [
370 EnvironmentScopeRequiredError,
371 EnvironmentResourceNotFoundError,
372 EnvironmentInternalError,
373] as const;
374const EnvironmentProjectMutationErrors = [
375 EnvironmentRequestInvalidError,
376 EnvironmentScopeRequiredError,
377 EnvironmentInternalError,
378] as const;
379
380export interface EnvironmentSessionPrincipalShape {
381 readonly parentSessionId?: AuthSessionId;
382 readonly sessionId: AuthSessionId;
383 readonly subject: string;
384 readonly method: ServerAuthSessionMethod;
385 readonly scopes: ReadonlySet<AuthEnvironmentScope>;
386 readonly proofKeyThumbprint?: string;
387 readonly expiresAt?: DateTime.DateTime;
388}
389
390export class EnvironmentAuthenticatedPrincipal extends Context.Service<
391 EnvironmentAuthenticatedPrincipal,
392 EnvironmentSessionPrincipalShape
393>()("@t3tools/contracts/environmentHttp/EnvironmentAuthenticatedPrincipal") {}
394
395export class EnvironmentAuthenticatedAuth extends HttpApiMiddleware.Service<
396 EnvironmentAuthenticatedAuth,
397 { provides: EnvironmentAuthenticatedPrincipal }
398>()("EnvironmentAuthenticatedAuth", {
399 error: EnvironmentAuthenticationErrors,
400}) {}
401
402const EnvironmentHttpCloudErrors = [
403 EnvironmentHttpBadRequestError,
404 EnvironmentHttpUnauthorizedError,
405 EnvironmentHttpForbiddenError,
406 EnvironmentHttpConflictError,
407 EnvironmentHttpInternalServerError,
408 EnvironmentScopeRequiredError,
409] as const;
410
411export const EnvironmentCloudRelayConfigResult = Schema.Struct({
412 ok: Schema.Boolean,
413 endpointRuntimeStatus: Schema.Unknown,
414});
415export type EnvironmentCloudRelayConfigResult = typeof EnvironmentCloudRelayConfigResult.Type;
416
417export const EnvironmentCloudLinkStateResult = Schema.Struct({
418 linked: Schema.Boolean,
419 cloudUserId: Schema.NullOr(Schema.String),
420 relayUrl: Schema.NullOr(Schema.String),
421 relayIssuer: Schema.NullOr(Schema.String),
422 // A managed Cloudflare tunnel is provisioned for this link. False for a
423 // publish-only link (activity publishing without a relay-managed tunnel), so
424 // clients can present the two capabilities as independent settings.
425 // Optional so newer clients tolerate older environment servers.
426 managedTunnelActive: Schema.optional(Schema.Boolean),
427 publishAgentActivity: Schema.Boolean,
428 // Opt-in: T3 Connect holds webhook requests while this environment is
429 // offline. Optional so newer clients tolerate older environment servers.
430 holdWebhooksWhileOffline: Schema.optional(Schema.Boolean),
431 // The relay rejected this link's credential, usually because the environment
432 // was deregistered from T3 Connect. The link stays stored until it is linked
433 // again. Optional so newer clients tolerate older environment servers.
434 linkRevoked: Schema.optional(Schema.Boolean),
435});
436export type EnvironmentCloudLinkStateResult = typeof EnvironmentCloudLinkStateResult.Type;
437
438export const EnvironmentCloudPreferencesRequest = Schema.Struct({
439 publishAgentActivity: Schema.Boolean,
440 // Omit to leave the current value unchanged.
441 holdWebhooksWhileOffline: Schema.optional(Schema.Boolean),
442});
443export type EnvironmentCloudPreferencesRequest = typeof EnvironmentCloudPreferencesRequest.Type;
444
445export const AuthPairingLinkRevokeResult = Schema.Struct({
446 revoked: Schema.Boolean,
447});
448export type AuthPairingLinkRevokeResult = typeof AuthPairingLinkRevokeResult.Type;
449
450export const AuthClientSessionRevokeResult = Schema.Struct({
451 revoked: Schema.Boolean,
452});
453export type AuthClientSessionRevokeResult = typeof AuthClientSessionRevokeResult.Type;
454
455export const AuthOtherClientSessionsRevokeResult = Schema.Struct({
456 revokedCount: Schema.Number,
457});
458export type AuthOtherClientSessionsRevokeResult = typeof AuthOtherClientSessionsRevokeResult.Type;
459
460class EnvironmentMetadataHttpApi extends HttpApiGroup.make("metadata").add(
461 HttpApiEndpoint.get("descriptor", "/.well-known/t3/environment", {
462 success: ExecutionEnvironmentDescriptor,
463 }),
464) {}
465
466class EnvironmentAuthHttpApi extends HttpApiGroup.make("auth")
467 .add(
468 HttpApiEndpoint.get("session", "/api/auth/session", {
469 headers: OptionalBearerHeaders,
470 success: AuthSessionState,
471 error: [EnvironmentInternalError],
472 }),
473 )
474 .add(
475 HttpApiEndpoint.post("browserSession", "/api/auth/browser-session", {
476 payload: AuthBrowserSessionRequest,
477 success: AuthBrowserSessionResult,
478 error: EnvironmentSessionCreationErrors,
479 }),
480 )
481 .add(
482 HttpApiEndpoint.post("token", "/oauth/token", {
483 headers: OptionalDpopProofHeaders,
484 payload: AuthTokenExchangeRequest,
485 success: AuthAccessTokenResult,
486 error: EnvironmentTokenExchangeErrors,
487 }),
488 )
489 .add(
490 HttpApiEndpoint.post("webSocketTicket", "/api/auth/websocket-ticket", {
491 headers: OptionalBearerHeaders,
492 success: AuthWebSocketTicketResult,
493 error: [EnvironmentInternalError],
494 }).middleware(EnvironmentAuthenticatedAuth),
495 )
496 .add(
497 HttpApiEndpoint.post("pairingCredential", "/api/auth/pairing-token", {
498 headers: OptionalBearerHeaders,
499 payload: AuthCreatePairingCredentialInput,
500 success: AuthPairingCredentialResult,
501 error: EnvironmentPairingCredentialErrors,
502 }).middleware(EnvironmentAuthenticatedAuth),
503 )
504 .add(
505 HttpApiEndpoint.post("delegatedSession", "/api/auth/delegated-session", {
506 headers: OptionalBearerHeaders,
507 payload: AuthCreateDelegatedSessionInput,
508 success: AuthAccessTokenResult,
509 error: EnvironmentPairingCredentialErrors,
510 }).middleware(EnvironmentAuthenticatedAuth),
511 )
512 .add(
513 HttpApiEndpoint.get("pairingLinks", "/api/auth/pairing-links", {
514 headers: OptionalBearerHeaders,
515 success: Schema.Array(AuthPairingLink),
516 error: EnvironmentScopedOperationErrors,
517 }).middleware(EnvironmentAuthenticatedAuth),
518 )
519 .add(
520 HttpApiEndpoint.post("revokePairingLink", "/api/auth/pairing-links/revoke", {
521 headers: OptionalBearerHeaders,
522 payload: AuthRevokePairingLinkInput,
523 success: AuthPairingLinkRevokeResult,
524 error: EnvironmentScopedOperationErrors,
525 }).middleware(EnvironmentAuthenticatedAuth),
526 )
527 .add(
528 HttpApiEndpoint.get("clients", "/api/auth/clients", {
529 headers: OptionalBearerHeaders,
530 success: Schema.Array(AuthClientSession),
531 error: EnvironmentScopedOperationErrors,
532 }).middleware(EnvironmentAuthenticatedAuth),
533 )
534 .add(
535 HttpApiEndpoint.post("revokeClient", "/api/auth/clients/revoke", {
536 headers: OptionalBearerHeaders,
537 payload: AuthRevokeClientSessionInput,
538 success: AuthClientSessionRevokeResult,
539 error: EnvironmentSessionRevokeErrors,
540 }).middleware(EnvironmentAuthenticatedAuth),
541 )
542 .add(
543 HttpApiEndpoint.post("revokeOtherClients", "/api/auth/clients/revoke-others", {
544 headers: OptionalBearerHeaders,
545 success: AuthOtherClientSessionsRevokeResult,
546 error: EnvironmentScopedOperationErrors,
547 }).middleware(EnvironmentAuthenticatedAuth),
548 ) {}
549
550/**
551 * The OAuth authorization server outside agents use to sign in to `/mcp`.
552 * `authorize` is where the agent sends the browser: it answers a redirect to
553 * the web app's approval page, or a plain error page for a request that names
554 * an unverified client or redirect, so the response is not a schema.
555 */
556class EnvironmentMcpOAuthHttpApi extends HttpApiGroup.make("mcpOAuth")
557 .add(
558 HttpApiEndpoint.get("protectedResource", "/.well-known/oauth-protected-resource", {
559 success: AuthMcpProtectedResourceMetadata,
560 }),
561 )
562 .add(
563 HttpApiEndpoint.get("mcpProtectedResource", "/.well-known/oauth-protected-resource/mcp", {
564 success: AuthMcpProtectedResourceMetadata,
565 }),
566 )
567 .add(
568 HttpApiEndpoint.get("authorizationServer", "/.well-known/oauth-authorization-server", {
569 success: AuthMcpAuthorizationServerMetadata,
570 }),
571 )
572 .add(
573 HttpApiEndpoint.post("register", "/oauth/mcp/register", {
574 payload: AuthMcpClientRegistration,
575 success: AuthMcpRegisteredClient,
576 error: AuthMcpRegistrationError,
577 }),
578 )
579 .add(HttpApiEndpoint.get("authorize", "/oauth/mcp/authorize"))
580 .add(
581 HttpApiEndpoint.post("approval", "/oauth/mcp/approval", {
582 payload: AuthMcpAuthorizationRequest,
583 success: [AuthMcpApprovalDetails, AuthMcpApprovalRedirect],
584 error: AuthMcpApprovalError,
585 }),
586 )
587 .add(
588 HttpApiEndpoint.post("decision", "/oauth/mcp/decision", {
589 payload: AuthMcpApprovalDecisionRequest,
590 success: AuthMcpApprovalRedirect,
591 error: AuthMcpApprovalError,
592 }),
593 )
594 .add(
595 HttpApiEndpoint.post("token", "/oauth/mcp/token", {
596 payload: AuthMcpTokenRequest,
597 success: AuthMcpTokenResult,
598 error: AuthMcpTokenError,
599 }),
600 ) {}
601
602const EnvironmentOrchestrationThreadSnapshotParams = Schema.Struct({
603 threadId: ThreadId,
604});
605
606/**
607 * `compactTurnItems=1` opts into `turnItemsOmitLocalVisible`. Other values are
608 * ignored, and older servers ignore the whole query.
609 */
610const EnvironmentOrchestrationThreadBoundedSnapshotQuery = Schema.Struct({
611 compactTurnItems: Schema.optionalKey(Schema.String),
612});
613
614const EnvironmentOrchestrationThreadHistoryQuery = Schema.Struct({
615 cursor: TrimmedNonEmptyString,
616 throughEntryId: Schema.optional(TrimmedNonEmptyString),
617 view: Schema.optional(Schema.Literals(["conversation", "activity"])),
618});
619
620const EnvironmentOrchestrationThreadHistoryErrors = [
621 EnvironmentRequestInvalidError,
622 EnvironmentScopeRequiredError,
623 EnvironmentResourceNotFoundError,
624 EnvironmentInternalError,
625] as const;
626
627class EnvironmentOrchestrationHttpApi extends HttpApiGroup.make("orchestration")
628 .add(
629 HttpApiEndpoint.get("shellSnapshot", "/api/orchestration/shell", {
630 headers: OrchestrationProtocolHeaders,
631 success: OrchestrationV2ShellSnapshot,
632 error: EnvironmentOrchestrationSnapshotErrors,
633 }).middleware(EnvironmentAuthenticatedAuth),
634 )
635 .add(
636 HttpApiEndpoint.get("threadSnapshot", "/api/orchestration/threads/:threadId", {
637 headers: OrchestrationProtocolHeaders,
638 params: EnvironmentOrchestrationThreadSnapshotParams,
639 success: OrchestrationV2ThreadDetailSnapshot,
640 error: EnvironmentOrchestrationThreadSnapshotErrors,
641 }).middleware(EnvironmentAuthenticatedAuth),
642 )
643 .add(
644 HttpApiEndpoint.get("threadTranscript", "/api/orchestration/threads/:threadId/transcript", {
645 headers: OrchestrationProtocolHeaders,
646 params: EnvironmentOrchestrationThreadSnapshotParams,
647 success: OrchestrationV2ThreadTranscript,
648 error: [EnvironmentRequestInvalidError, ...EnvironmentOrchestrationThreadSnapshotErrors],
649 }).middleware(EnvironmentAuthenticatedAuth),
650 )
651 .add(
652 HttpApiEndpoint.get("threadBoundedSnapshot", "/api/orchestration/threads/:threadId/bounded", {
653 headers: OrchestrationProtocolHeaders,
654 params: EnvironmentOrchestrationThreadSnapshotParams,
655 query: EnvironmentOrchestrationThreadBoundedSnapshotQuery,
656 success: OrchestrationV2ThreadBoundedSnapshot,
657 error: EnvironmentOrchestrationThreadSnapshotErrors,
658 }).middleware(EnvironmentAuthenticatedAuth),
659 )
660 .add(
661 HttpApiEndpoint.get("threadHistoryPage", "/api/orchestration/threads/:threadId/history", {
662 headers: OrchestrationProtocolHeaders,
663 params: EnvironmentOrchestrationThreadSnapshotParams,
664 query: EnvironmentOrchestrationThreadHistoryQuery,
665 success: OrchestrationV2ThreadHistoryPage,
666 error: EnvironmentOrchestrationThreadHistoryErrors,
667 }).middleware(EnvironmentAuthenticatedAuth),
668 ) {}
669
670class EnvironmentProjectsHttpApi extends HttpApiGroup.make("projects")
671 .add(
672 HttpApiEndpoint.get("snapshot", "/api/projects", {
673 headers: OptionalBearerHeaders,
674 success: ProjectSnapshot,
675 error: EnvironmentProjectSnapshotErrors,
676 }).middleware(EnvironmentAuthenticatedAuth),
677 )
678 .add(
679 HttpApiEndpoint.post("mutate", "/api/projects/mutate", {
680 headers: OptionalBearerHeaders,
681 payload: ProjectMutation,
682 success: Project,
683 error: EnvironmentProjectMutationErrors,
684 }).middleware(EnvironmentAuthenticatedAuth),
685 ) {}
686
687/** Large, compressible pull-request payloads travel over HTTP rather than the RPC socket. */
688class EnvironmentPullRequestsHttpApi extends HttpApiGroup.make("pullRequests").add(
689 HttpApiEndpoint.post("diff", "/api/pull-requests/diff", {
690 headers: OptionalBearerHeaders,
691 payload: PullRequestDiffInput,
692 success: PullRequestDiffResult,
693 error: [
694 PullRequestUnavailableError,
695 PullRequestOperationError,
696 EnvironmentAuthInvalidError,
697 EnvironmentScopeRequiredError,
698 EnvironmentInternalError,
699 ],
700 }).middleware(EnvironmentAuthenticatedAuth),
701) {}
702
703class EnvironmentConnectHttpApi extends HttpApiGroup.make("connect")
704 .add(
705 HttpApiEndpoint.post("linkProof", "/api/connect/link-proof", {
706 headers: OptionalBearerHeaders,
707 payload: RelayLinkProofRequest,
708 success: RelayEnvironmentLinkProof,
709 error: EnvironmentHttpCloudErrors,
710 }).middleware(EnvironmentAuthenticatedAuth),
711 )
712 .add(
713 HttpApiEndpoint.post("relayConfig", "/api/connect/relay-config", {
714 headers: OptionalBearerHeaders,
715 payload: RelayEnvironmentConfigRequest,
716 success: EnvironmentCloudRelayConfigResult,
717 error: [...EnvironmentHttpCloudErrors, EnvironmentCloudEndpointUnavailableError],
718 }).middleware(EnvironmentAuthenticatedAuth),
719 )
720 .add(
721 HttpApiEndpoint.get("linkState", "/api/connect/link-state", {
722 headers: OptionalBearerHeaders,
723 success: EnvironmentCloudLinkStateResult,
724 error: EnvironmentHttpCloudErrors,
725 }).middleware(EnvironmentAuthenticatedAuth),
726 )
727 .add(
728 HttpApiEndpoint.post("unlink", "/api/connect/unlink", {
729 headers: OptionalBearerHeaders,
730 success: EnvironmentCloudRelayConfigResult,
731 error: EnvironmentHttpCloudErrors,
732 }).middleware(EnvironmentAuthenticatedAuth),
733 )
734 .add(
735 HttpApiEndpoint.post("preferences", "/api/connect/preferences", {
736 headers: OptionalBearerHeaders,
737 payload: EnvironmentCloudPreferencesRequest,
738 success: EnvironmentCloudLinkStateResult,
739 error: EnvironmentHttpCloudErrors,
740 }).middleware(EnvironmentAuthenticatedAuth),
741 )
742 .add(
743 HttpApiEndpoint.post("health", "/api/t3-connect/health", {
744 payload: RelayCloudEnvironmentHealthRequest,
745 success: RelayEnvironmentHealthResponse,
746 error: EnvironmentHttpCloudErrors,
747 }),
748 )
749 .add(
750 HttpApiEndpoint.post("mintCredential", "/api/connect/mint-credential", {
751 payload: RelayCloudMintCredentialRequest,
752 success: RelayEnvironmentMintResponse,
753 error: EnvironmentHttpCloudErrors,
754 }),
755 )
756 .add(
757 HttpApiEndpoint.post("t3MintCredential", "/api/t3-connect/mint-credential", {
758 payload: RelayCloudMintCredentialRequest,
759 success: RelayEnvironmentMintResponse,
760 error: EnvironmentHttpCloudErrors,
761 }),
762 ) {}
763
764/**
765 * Public entry point for webhook tasks. Unauthenticated by design: the token
766 * in the path, and an optional body signature, are the credential. The handler
767 * reads the raw body itself so a signature is checked over the exact bytes.
768 */
769const WebhookParams = Schema.Struct({
770 hookId: TrimmedNonEmptyString,
771 token: TrimmedNonEmptyString,
772});
773const WebhookAccepted = Schema.Struct({ deliveryId: TrimmedNonEmptyString }).pipe(
774 HttpApiSchema.status(202),
775);
776const webhookEndpoint = {
777 params: WebhookParams,
778 success: WebhookAccepted,
779} as const;
780const WEBHOOK_PATH = "/api/hooks/:hookId/:token";
781
782class EnvironmentWebhooksHttpApi extends HttpApiGroup.make("webhooks")
783 .add(HttpApiEndpoint.post("webhookPost", WEBHOOK_PATH, webhookEndpoint))
784 .add(HttpApiEndpoint.put("webhookPut", WEBHOOK_PATH, webhookEndpoint))
785 .add(HttpApiEndpoint.patch("webhookPatch", WEBHOOK_PATH, webhookEndpoint))
786 .add(HttpApiEndpoint.get("webhookGet", WEBHOOK_PATH, webhookEndpoint)) {}
787
788export class EnvironmentHttpApi extends HttpApi.make("environment")
789 .add(EnvironmentMetadataHttpApi)
790 .add(EnvironmentAuthHttpApi)
791 .add(EnvironmentMcpOAuthHttpApi)
792 .add(EnvironmentOrchestrationHttpApi)
793 .add(EnvironmentPullRequestsHttpApi)
794 .add(EnvironmentProjectsHttpApi)
795 .add(EnvironmentConnectHttpApi)
796 .add(EnvironmentWebhooksHttpApi) {}