Skip to content

Commit 908155d

Browse files
committed
Merge remote-tracking branch 'origin/main' into mcp-oauth-health-reconnect-repro
2 parents 844a1a3 + 4a84b0d commit 908155d

67 files changed

Lines changed: 5499 additions & 413 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish-desktop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ jobs:
172172
apps/desktop/dist/*.AppImage
173173
apps/desktop/dist/*.deb
174174
apps/desktop/dist/*.rpm
175+
apps/desktop/dist/*.blockmap
175176
apps/desktop/dist/latest*.yml
176177
if-no-files-found: warn
177178

@@ -223,6 +224,7 @@ jobs:
223224
done < <(find artifacts -type f \
224225
\( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" \
225226
-o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" \
227+
-o -name "*.blockmap" \
226228
-o -name "latest*.yml" \))
227229
228230
# Flip draft → published only after every desktop asset is uploaded —

.oxlintrc.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"executor/no-match-orelse": "error",
2121
"executor/no-promise-catch": "error",
2222
"executor/no-promise-reject": "error",
23+
"executor/no-raw-durable-object-id": "error",
2324
"executor/no-raw-fetch": "error",
2425
"executor/no-redundant-primitive-cast": "error",
2526
"executor/no-redundant-error-factory": "error",

apps/cloud/src/api/error-response.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Cause, Data, Effect, Predicate, Result } from "effect";
22
import { HttpServerRespondable, HttpServerResponse } from "effect/unstable/http";
33

4-
import { captureCause } from "../observability";
4+
import { captureCause, captureCauseEffect } from "../observability";
55

66
// Implements `Respondable` so the framework's default cause→response
77
// pipeline (`HttpServerRespondable.toResponseOrElse`) renders this as the
@@ -13,13 +13,14 @@ export class HttpResponseError extends Data.TaggedError("HttpResponseError")<{
1313
readonly message: string;
1414
}> {
1515
[HttpServerRespondable.symbol](): Effect.Effect<HttpServerResponse.HttpServerResponse> {
16-
if (this.status >= 500) captureCause(this);
17-
return Effect.succeed(
18-
HttpServerResponse.jsonUnsafe(
19-
{ error: this.message, code: this.code },
20-
{ status: this.status },
21-
),
22-
);
16+
const self = this;
17+
return Effect.gen(function* () {
18+
if (self.status >= 500) yield* captureCauseEffect(self);
19+
return HttpServerResponse.jsonUnsafe(
20+
{ error: self.message, code: self.code },
21+
{ status: self.status },
22+
);
23+
});
2324
}
2425
}
2526

@@ -65,3 +66,15 @@ export const toErrorServerResponse = (error: unknown): HttpServerResponse.HttpSe
6566
{ status: mapped.status },
6667
);
6768
};
69+
70+
export const toErrorServerResponseEffect = (
71+
error: unknown,
72+
): Effect.Effect<HttpServerResponse.HttpServerResponse> =>
73+
Effect.gen(function* () {
74+
const mapped = toHttpResponseError(error);
75+
if (mapped.status >= 500) yield* captureCauseEffect(mapped);
76+
return HttpServerResponse.jsonUnsafe(
77+
{ error: mapped.message, code: mapped.code },
78+
{ status: mapped.status },
79+
);
80+
});

apps/cloud/src/auth/handlers.ts

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ import {
3232
authorizeOrganizationSelector,
3333
resolveOrganization,
3434
} from "./organization";
35-
import type {
36-
McpSessionApprovalResult,
37-
McpSessionResumeApprovalResult,
38-
} from "../mcp/session-durable-object";
35+
import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";
3936

4037
const COOKIE_OPTIONS = {
4138
path: "/",
@@ -122,23 +119,7 @@ const requireSelectedOrganization = Effect.gen(function* () {
122119
};
123120
});
124121

125-
const getMcpSessionStub = (mcpSessionId: string) =>
126-
Effect.try({
127-
try: () => {
128-
const ns = env.MCP_SESSION;
129-
return ns.get(ns.idFromString(mcpSessionId));
130-
},
131-
catch: () => undefined,
132-
}).pipe(Effect.orElseSucceed(() => null));
133-
134-
const requireMcpSessionStub = (mcpSessionId: string, executionId: string) =>
135-
Effect.gen(function* () {
136-
const stub = yield* getMcpSessionStub(mcpSessionId);
137-
if (!stub) {
138-
return yield* new McpExecutionNotFoundError({ executionId });
139-
}
140-
return stub;
141-
});
122+
const getMcpSessionStub = (mcpSessionId: string) => mcpSessionStub(env.MCP_SESSION, mcpSessionId);
142123

143124
const failMcpApprovalResult = (
144125
result: { readonly status: "not_found" | "forbidden" },
@@ -528,13 +509,12 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
528509
.handle("getMcpPaused", ({ params }) =>
529510
Effect.gen(function* () {
530511
const owner = yield* requireSelectedOrganization;
531-
const stub = yield* requireMcpSessionStub(params.mcpSessionId, params.executionId);
532-
const result = yield* Effect.promise(
533-
() =>
534-
stub.getPausedExecutionForApproval(params.executionId, {
535-
accountId: owner.accountId,
536-
organizationId: owner.organizationId,
537-
}) as Promise<McpSessionApprovalResult>,
512+
const stub = getMcpSessionStub(params.mcpSessionId);
513+
const result = yield* Effect.promise(() =>
514+
stub.getPausedExecutionForApproval(params.executionId, {
515+
accountId: owner.accountId,
516+
organizationId: owner.organizationId,
517+
}),
538518
);
539519

540520
if (result.status !== "ok") {
@@ -550,20 +530,19 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
550530
.handle("resumeMcpExecution", ({ params, payload }) =>
551531
Effect.gen(function* () {
552532
const owner = yield* requireSelectedOrganization;
553-
const stub = yield* requireMcpSessionStub(params.mcpSessionId, params.executionId);
554-
const result = yield* Effect.promise(
555-
() =>
556-
stub.resumeExecutionForApproval(
557-
params.executionId,
558-
{
559-
accountId: owner.accountId,
560-
organizationId: owner.organizationId,
561-
},
562-
{
563-
action: payload.action,
564-
content: payload.content as Record<string, unknown> | undefined,
565-
},
566-
) as Promise<McpSessionResumeApprovalResult>,
533+
const stub = getMcpSessionStub(params.mcpSessionId);
534+
const result = yield* Effect.promise(() =>
535+
stub.resumeExecutionForApproval(
536+
params.executionId,
537+
{
538+
accountId: owner.accountId,
539+
organizationId: owner.organizationId,
540+
},
541+
{
542+
action: payload.action,
543+
content: payload.content as Record<string, unknown> | undefined,
544+
},
545+
),
567546
);
568547

569548
if (result.status !== "ok") {

apps/cloud/src/engine/execution-gate.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@
2424
// `CodeCompilationError` / `SandboxRuntimeError` into `ExecuteResult.error`.
2525
// ---------------------------------------------------------------------------
2626

27-
import * as Sentry from "@sentry/cloudflare";
2827
import { Data, Effect } from "effect";
2928
import type * as Cause from "effect/Cause";
3029

3130
import type { ExecutionEngine, ExecutionResult } from "@executor-js/execution";
3231

32+
import { captureCauseEffect } from "../observability";
3333
import { EXECUTION_LIMIT_BLOCKED_MESSAGE } from "./execution-limit-messages";
3434

3535
// The engine's completed-result payload (`ExecuteResult` in codemode-core),
@@ -166,10 +166,12 @@ export const makeExecutionLimitGate = (checkBalance: ExecutionBalanceCheck) => {
166166
// must never block executions. Reported like `trackExecution` so a
167167
// billing outage still pages; the error outcome is never cached.
168168
Effect.catch((error: unknown) =>
169-
Effect.sync((): GateDecision => {
170-
console.warn("[billing] execution balance check failed open:", error);
171-
Sentry.captureException(error);
172-
return { blocked: false };
169+
Effect.gen(function* () {
170+
yield* Effect.sync(() => {
171+
console.warn("[billing] execution balance check failed open:", error);
172+
});
173+
yield* captureCauseEffect(error);
174+
return { blocked: false } as const satisfies GateDecision;
173175
}),
174176
),
175177
);

apps/cloud/src/engine/execution-rate-limit.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
// ---------------------------------------------------------------------------
1515

1616
import { DurableObject, env } from "cloudflare:workers";
17-
import * as Sentry from "@sentry/cloudflare";
1817
import { Data, Effect } from "effect";
1918
import type * as Cause from "effect/Cause";
2019

2120
import type { ExecutionEngine } from "@executor-js/execution";
2221

22+
import { captureCauseEffect } from "../observability";
2323
import { withPreExecutionGate, type GateDecision } from "./execution-gate";
2424
import { RATE_LIMIT_BLOCKED_MESSAGE } from "./execution-limit-messages";
2525

@@ -149,10 +149,12 @@ export const makeExecutionRateLimiter = (
149149
// FAIL OPEN: the backstop must never block executions because its
150150
// counter is unreachable or slow.
151151
Effect.catch((error: unknown) =>
152-
Effect.sync((): GateDecision => {
153-
console.warn("[rate-limit] execution rate limit check failed open:", error);
154-
Sentry.captureException(error);
155-
return { blocked: false };
152+
Effect.gen(function* () {
153+
yield* Effect.sync(() => {
154+
console.warn("[rate-limit] execution rate limit check failed open:", error);
155+
});
156+
yield* captureCauseEffect(error);
157+
return { blocked: false } as const satisfies GateDecision;
156158
}),
157159
),
158160
);

apps/cloud/src/env-augment.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ declare global {
1111
AXIOM_TRACES_URL?: string;
1212
AXIOM_TRACES_SAMPLE_RATIO?: string;
1313
SENTRY_DSN?: string;
14+
SENTRY_OTEL_LOG_PAYLOAD?: string;
15+
SENTRY_OTEL_VERIFY?: string;
1416
VITE_PUBLIC_SENTRY_DSN?: string;
1517
VITE_PUBLIC_POSTHOG_KEY?: string;
1618
VITE_PUBLIC_POSTHOG_HOST?: string;
@@ -38,6 +40,7 @@ declare global {
3840
// workers and older local setups run without the binding, and the
3941
// limiter degrades to disabled when absent.
4042
EXECUTION_RATE_LIMITER?: import("@cloudflare/workers-types").DurableObjectNamespace;
43+
MCP_EXECUTION_OWNER?: import("@cloudflare/workers-types").DurableObjectNamespace;
4144

4245
// Optional per-org hourly execution rate-limit override, parsed as an
4346
// integer (defaults to EXECUTIONS_PER_ORG_PER_HOUR = 1000 when unset or

apps/cloud/src/extensions/billing/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { autumnHandler } from "autumn-js/backend";
55

66
import { WorkOSClient } from "../../auth/workos";
77
import { ORG_SELECTOR_HEADER, authorizeOrganizationSelector } from "../../auth/organization";
8-
import { HttpResponseError, isServerError, toErrorServerResponse } from "../../api/error-response";
8+
import {
9+
HttpResponseError,
10+
isServerError,
11+
toErrorServerResponseEffect,
12+
} from "../../api/error-response";
913

1014
type BillingSession = {
1115
readonly userId: string;
@@ -111,7 +115,7 @@ const handler = Effect.gen(function* () {
111115
if (isServerError(err)) {
112116
console.error("[autumn] request failed:", Cause.pretty(err));
113117
}
114-
return Effect.succeed(toErrorServerResponse(err));
118+
return toErrorServerResponseEffect(err);
115119
}),
116120
);
117121

apps/cloud/src/extensions/billing/service.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
// ---------------------------------------------------------------------------
44

55
import { env } from "cloudflare:workers";
6-
import * as Sentry from "@sentry/cloudflare";
76
import { Autumn } from "autumn-js";
87
import { Context, Data, Effect, Layer } from "effect";
98

9+
import { captureCauseEffect } from "../../observability";
10+
1011
// ---------------------------------------------------------------------------
1112
// Errors
1213
// ---------------------------------------------------------------------------
@@ -72,12 +73,12 @@ const make = Effect.sync(() => {
7273
).pipe(
7374
Effect.catchTag("AutumnError", (error) =>
7475
Effect.gen(function* () {
75-
// Silent billing data loss is worth paging on autumn.trackExecution
76+
// Silent billing data loss is worth paging on: autumn.trackExecution
7677
// is fire-and-forget so the caller doesn't handle it themselves.
7778
yield* Effect.sync(() => {
7879
console.error("[billing] track failed:", error);
79-
Sentry.captureException(error);
8080
});
81+
yield* captureCauseEffect(error);
8182
yield* Effect.annotateCurrentSpan({ "autumn.track.failed": true });
8283
}),
8384
),

apps/cloud/src/mcp/agent-handler.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,12 @@ import {
1313
withVerifiedIdentityHeaders,
1414
} from "@executor-js/cloudflare/mcp/do-headers";
1515
import type { McpSessionProps } from "@executor-js/cloudflare/mcp/agent-durable-object";
16+
import { mcpSessionStub } from "@executor-js/cloudflare/mcp/session-stub";
1617

1718
import { wrapMcpSseResponse } from "../observability/memory-metrics";
1819
import { cloudMcpAuth } from "./auth-provider";
1920
import { McpSessionDOSqlite } from "./session-durable-object";
2021

21-
interface McpAgentSessionStub {
22-
readonly validateMcpSessionOwner: (identity: {
23-
readonly accountId: string;
24-
readonly organizationId: string;
25-
}) => Promise<"ok" | "not_found" | "forbidden">;
26-
readonly _cf_scheduleDestroy: () => Promise<void>;
27-
}
28-
2922
const corsPreflightResponse = (): Response =>
3023
new Response(null, {
3124
status: 204,
@@ -67,12 +60,6 @@ const renderAuthError = (
6760
return jsonRpcResponse(503, -32001, outcome.message);
6861
};
6962

70-
const sessionStub = (env: Env, sessionId: string): McpAgentSessionStub =>
71-
// oxlint-disable-next-line executor/no-double-cast -- boundary: Workers types expose only DurableObjectStub, but RPC methods are generated from the bound DO class.
72-
env.MCP_SESSION.get(
73-
env.MCP_SESSION.idFromName(`streamable-http:${sessionId}`),
74-
) as unknown as McpAgentSessionStub;
75-
7663
const authenticate = (request: Request) =>
7764
Effect.gen(function* () {
7865
const auth = yield* McpAuthProvider;
@@ -141,7 +128,11 @@ export const makeCloudMcpAgentHandler = () => {
141128
if (!Predicate.isTagged(outcome, "Authenticated")) {
142129
if (Predicate.isTagged(outcome, "Forbidden") && sessionId) {
143130
await Effect.runPromise(
144-
Effect.ignore(Effect.tryPromise(() => sessionStub(env, sessionId)._cf_scheduleDestroy())),
131+
Effect.ignore(
132+
Effect.tryPromise(() =>
133+
mcpSessionStub(env.MCP_SESSION, sessionId)._cf_scheduleDestroy(),
134+
),
135+
),
145136
);
146137
}
147138
return renderAuthError(auth, request, outcome);
@@ -155,7 +146,7 @@ export const makeCloudMcpAgentHandler = () => {
155146
}
156147

157148
if (sessionId) {
158-
const owner = await sessionStub(env, sessionId).validateMcpSessionOwner({
149+
const owner = await mcpSessionStub(env.MCP_SESSION, sessionId).validateMcpSessionOwner({
159150
accountId: outcome.principal.accountId,
160151
organizationId: outcome.principal.organizationId,
161152
});

0 commit comments

Comments
 (0)