Skip to content

Commit 901e592

Browse files
committed
Merge remote-tracking branch 'origin/main' into try-1129
# Conflicts: # packages/react/src/components/add-account-modal.tsx
2 parents 653c2c7 + 3356740 commit 901e592

276 files changed

Lines changed: 13977 additions & 1070 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.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
name: self-contained-modals
3+
description: "Build modals/dialogs self-contained: form and in-flight state lives inside, closing unmounts it. Use when writing or reviewing a modal/dialog, especially one that owns async work (OAuth popups, timers, subscriptions, AbortControllers). Catches the stuck-on-Connecting class of lifecycle-leak bugs."
4+
---
5+
6+
# Self-contained modals
7+
8+
A modal's **form state and in-flight work live INSIDE the modal**, and **closing
9+
the modal UNMOUNTS that state** so it is destroyed, not hand-reset. Only the
10+
**open/route intent** belongs to the parent (deep links, programmatic open,
11+
reconnect handoffs need it).
12+
13+
## Why
14+
15+
A hand-written `reset()` has to enumerate every field, and it silently drifts out
16+
of sync with state owned by **child hooks the parent can't see**. Unmounting
17+
resets everything for free, including child-hook cleanup effects (cancelling a
18+
dangling server session, clearing a timer, aborting a fetch).
19+
20+
Concrete bug this prevents (executor, add-account-modal): the modal was mounted
21+
unconditionally, so `useOAuthPopupFlow`'s `busy` survived close. `reset()` zeroed
22+
its own booleans but never called `oauthPopup.cancel()`. Abandon the OAuth popup,
23+
close, reopen, and `oauthBusy = false || busy(true) = true`, so the footer is
24+
wedged on "Connecting…" with Close disabled. Unmounting would have cleared `busy`
25+
AND run the hook's cleanup that cancels the server OAuth session.
26+
27+
## How to apply
28+
29+
Default to **genuine conditional unmount**, state inside. When closed the
30+
component returns `null`, so React destroys all of it and runs every child
31+
hook's cleanup. This is the cleanest fix and the one to reach for first:
32+
33+
```tsx
34+
function Parent() {
35+
const [open, setOpen] = useState(false);
36+
return open ? <Modal onClose={() => setOpen(false)} /> : null;
37+
}
38+
```
39+
40+
A key bump (`<Body key={openCount} />`) is **still a manual reset**, just
41+
spelled as a remount. Prefer real unmount; only reach for keyed remount in the
42+
one case below.
43+
44+
That case: **Radix Dialog** (this repo's `components/dialog.tsx`) Content/Overlay
45+
use `data-[state=closed]:animate-out` exit animations, so unmounting the whole
46+
`Dialog` drops the close animation. If you must keep that animation, keep the
47+
`Dialog` + `DialogContent` shell mounted and remount only the state-bearing
48+
**body** per open:
49+
50+
```tsx
51+
function Parent() {
52+
const [open, setOpen] = useState(false);
53+
const [openCount, setOpenCount] = useState(0);
54+
return (
55+
<Dialog open={open} onOpenChange={setOpen}>
56+
<DialogContent>{open ? <ModalBody key={openCount} /> : null}</DialogContent>
57+
</Dialog>
58+
);
59+
}
60+
```
61+
62+
This only works when `DialogContent` is **independent of body state**. If the
63+
shell depends on the body (e.g. a width className driven by the body's current
64+
sub-view), the body must own `DialogContent`, so there is no stable shell to
65+
keep, and genuine unmount (losing the exit animation) is the right call. That is
66+
exactly the executor add-account-modal: it genuinely unmounts and accepts the
67+
lost animation rather than plumbing body state up to a shell.
68+
69+
## Reviewing: flag these smells
70+
71+
1. A hand-written `reset()` exists. Its presence means state outlives the modal;
72+
ask why the modal isn't just unmounted.
73+
2. An always-mounted dialog (rendered unconditionally with an `open` prop) that
74+
owns async/in-flight state: popups, timers, subscriptions, AbortControllers,
75+
server sessions.
76+
3. A busy/loading flag composed from a child hook (e.g. `ccBusy || someHook.busy`)
77+
where `reset()` clears only part of it.

RUNNING.md

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ working instance of X" — read them before inventing a boot path.
4848
screenshots, `session.mp4` + `trace.zip` for browser scenarios, and the
4949
scenario source as `test.ts`.
5050
- `cd e2e && bun run serve` builds the viewer and serves the scenario ×
51-
target matrix over HTTP, bound to all interfaces (reachable over the
52-
tailnet). It prefers port 8901 but walks forward to the next free port if
51+
target matrix over HTTP, bound to all interfaces. It prefers port 8901
52+
but walks forward to the next free port if
5353
that's taken (so concurrent worktrees, or a leaked previous viewer, never
5454
wedge each other) — read the printed `e2e viewer → …` URL for the actual
5555
port. `PORT=…` pins a port explicitly and fails loudly if it's busy. The
@@ -77,14 +77,14 @@ emulator ledger — develop interactively, then crystallize the journey into
7777
a scenario.
7878

7979
```sh
80-
bun run cli up selfhost --share # boot, reachable over the tailnet, stays up
81-
bun run cli up cloud --share # emulated WorkOS+Autumn, tailscale-HTTPS fronted
80+
bun run cli up selfhost --share # boot, shared, stays up
81+
bun run cli up cloud --share # emulated WorkOS+Autumn
8282
bun run cli status # what's running, URLs, creds
8383
bun run cli identity selfhost # fresh identity (headers / cookies / creds)
8484
bun run cli api selfhost tools.list
8585
bun run cli mcp selfhost call execute '{"code":"return 1+1;"}'
8686
bun run cli ledger cloud workos # what hit the emulator
87-
bun run cli down selfhost # tear down (also removes tailscale serves)
87+
bun run cli down selfhost # tear down
8888
```
8989

9090
Instances persist until `down``up --share` IS the "touch it" handoff
@@ -93,15 +93,6 @@ state (API/MCP/UI), hand across the URL. State files in `e2e/.dev/` mark
9393
deliberate long-lived instances (vs leaks); attach scenarios to a running
9494
instance with `E2E_<TARGET>_URL`.
9595

96-
Why cloud `--share` is more involved (encoded in the CLI, kept here for
97-
when you hit it manually): the cloud app sets `secure: true` auth cookies,
98-
so login breaks over plain http from any non-localhost origin ("Invalid
99-
login state"). Both the app AND the WorkOS emulator get fronted with
100-
`tailscale serve` HTTPS, the emulator advertises its public URL on both
101-
sides (its `baseUrl` and the app's `WORKOS_API_URL` — the browser-facing
102-
authorize URL derives from the latter), and Vite must allow the public
103-
hostname (`__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS`).
104-
10596
## Environment gotchas (learned the hard way)
10697

10798
- The shell is fish, and the working directory resets between Bash calls.
@@ -113,10 +104,6 @@ hostname (`__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS`).
113104
(symptom: behavior matching old code only in dev servers, while unit
114105
tests pass). Kill the server, clear `node_modules/.vite` /
115106
`.tanstack`-adjacent caches, reboot.
116-
- The real Tailscale CLI on this machine is
117-
`/opt/homebrew/opt/tailscale/bin/tailscale`; `/usr/local/bin/tailscale`
118-
is a broken shim pointing at a deleted app. The tailnet IP is on the
119-
`utun` interface (100.x.y.z) if the CLI fails.
120107
- `bun.lock` conflicts on rebase: take either side, re-run `bun install`,
121108
never hand-merge.
122109
- Long-lived demo servers you left up for the user look like leaks to

apps/cli/CHANGELOG.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,54 @@
11
# executor
22

3+
## 1.5.21
4+
5+
### Patch Changes
6+
7+
- [#1134](https://github.com/RhysSullivan/executor/pull/1134) [`78aa871`](https://github.com/RhysSullivan/executor/commit/78aa8710d774d552d6030eca060c5e72f0899461) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Fix OAuth callbacks in cloud so they preserve the URL-selected organization when the session cookie points at another org.
8+
9+
- Updated dependencies []:
10+
- @executor-js/local@1.4.4
11+
- @executor-js/sdk@1.5.21
12+
- @executor-js/runtime-quickjs@1.5.21
13+
- @executor-js/api@1.4.41
14+
15+
## 1.5.20
16+
17+
### Patch Changes
18+
19+
- [#1132](https://github.com/RhysSullivan/executor/pull/1132) [`580fc7f`](https://github.com/RhysSullivan/executor/commit/580fc7f8b2615a0d7760b3a4daddf8d45673ef3f) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Fix the PostHog custom MCP OAuth setup flow so Add connection opens PostHog authorization instead of falling back to manual OAuth app registration.
20+
21+
- Updated dependencies []:
22+
- @executor-js/sdk@1.5.20
23+
- @executor-js/runtime-quickjs@1.5.20
24+
- @executor-js/local@1.4.4
25+
- @executor-js/api@1.4.40
26+
27+
## 1.5.19
28+
29+
### Patch Changes
30+
31+
- [#1115](https://github.com/RhysSullivan/executor/pull/1115) [`92bd86c`](https://github.com/RhysSullivan/executor/commit/92bd86cb975ce867b3002ae9bcb6bf60da67cc48) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Google media downloads (Drive file contents, exports, and other binary
32+
endpoints) are now returned as binary responses instead of being decoded as
33+
text, so files come back intact. Emit them with `emit(result.data)`.
34+
35+
- [#1115](https://github.com/RhysSullivan/executor/pull/1115) [`92bd86c`](https://github.com/RhysSullivan/executor/commit/92bd86cb975ce867b3002ae9bcb6bf60da67cc48) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - The CLI now validates that a URL is `http`/`https` before handing it to the
36+
operating system's browser opener, and on Windows opens it via
37+
`rundll32 url.dll,FileProtocolHandler` instead of `cmd /c start`. This removes a
38+
path where a crafted URL could be interpreted as a shell command. `executor
39+
login` and the "open in browser" prompts behave the same for normal URLs.
40+
41+
- [#1115](https://github.com/RhysSullivan/executor/pull/1115) [`92bd86c`](https://github.com/RhysSullivan/executor/commit/92bd86cb975ce867b3002ae9bcb6bf60da67cc48) Thanks [@RhysSullivan](https://github.com/RhysSullivan)! - Hardened the hosted egress guard. Outbound requests from OAuth token exchanges,
42+
MCP transports, and GraphQL/Google/Microsoft discovery now all route through the
43+
guard, and the guard resolves DNS before connecting so a hostname that points at
44+
a private or loopback address is blocked rather than only literal private IPs.
45+
This tightens SSRF protection for hosted and cloud execution.
46+
- Updated dependencies []:
47+
- @executor-js/sdk@1.5.19
48+
- @executor-js/runtime-quickjs@1.5.19
49+
- @executor-js/local@1.4.4
50+
- @executor-js/api@1.4.39
51+
352
## 1.5.18
453

554
### Patch Changes

apps/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "executor",
3-
"version": "1.5.18",
3+
"version": "1.5.21",
44
"private": true,
55
"bin": {
66
"executor": "./bin/executor.ts"

apps/cli/src/main.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ import embeddedWebUI from "./embedded-web-ui.gen";
175175

176176
const { version: CLI_VERSION } = await import("../package.json");
177177
const DEFAULT_PORT = 4788;
178+
/** Canonical public docs (Mintlify), matching the web shell's DEFAULT_DOCS_URL. */
179+
const DOCS_URL = "https://executor.sh/docs";
178180
const DEFAULT_BASE_URL = `http://localhost:${DEFAULT_PORT}`;
179181
const DAEMON_BOOT_TIMEOUT_MS = 15_000;
180182
const DAEMON_BOOT_POLL_MS = 150;
@@ -2350,6 +2352,19 @@ const webCommand = Command.make(
23502352
({ foreground, port, scope, hostname, allowedHost, authToken }) =>
23512353
Effect.gen(function* () {
23522354
if (!foreground) {
2355+
// `--scope` can ONLY be honored by a foreground server we boot here.
2356+
// Without `--foreground` we just open whatever background service is
2357+
// already running, which uses the scope IT was started with — so a
2358+
// `--scope` here would be silently ignored and the user could land on a
2359+
// different workspace ("where did my config go?"). Say so loudly and
2360+
// point at the flag that actually applies it.
2361+
if (Option.isSome(scope)) {
2362+
console.warn(
2363+
`Ignoring --scope ${scope.value}: it only applies with --foreground. ` +
2364+
`The running web app uses the scope it was started with. ` +
2365+
`Run \`executor web --foreground --scope ${scope.value}\` to serve that workspace.`,
2366+
);
2367+
}
23532368
yield* openRunningLocalWebApp();
23542369
return;
23552370
}
@@ -2863,6 +2878,17 @@ const openCommand = Command.make("open", {}, () => openRunningLocalWebApp()).pip
28632878
Command.withDescription("Open the running Executor web app in your browser, already signed in"),
28642879
);
28652880

2881+
/**
2882+
* `executor docs` — open the documentation in the browser. The URL is printed
2883+
* first so it stays usable on headless machines where no opener is available.
2884+
*/
2885+
const docsCommand = Command.make("docs", {}, () =>
2886+
Effect.gen(function* () {
2887+
console.log(`Opening ${DOCS_URL}`);
2888+
yield* openInBrowser(DOCS_URL);
2889+
}),
2890+
).pipe(Command.withDescription("Open the Executor documentation in your browser"));
2891+
28662892
const root = Command.make("executor").pipe(
28672893
Command.withSubcommands([
28682894
callCommand,
@@ -2878,6 +2904,7 @@ const root = Command.make("executor").pipe(
28782904
serviceCommand,
28792905
mcpCommand,
28802906
openCommand,
2907+
docsCommand,
28812908
] as const),
28822909
Command.withDescription("Executor local CLI"),
28832910
);

apps/cloud/CHANGELOG.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,69 @@
11
# @executor-js/cloud
22

3+
## 1.4.39
4+
5+
### Patch Changes
6+
7+
- Updated dependencies [[`4b361b9`](https://github.com/RhysSullivan/executor/commit/4b361b9f7220f679f582137f5375b29c3b72f919)]:
8+
- @executor-js/plugin-openapi@1.5.21
9+
- @executor-js/runtime-dynamic-worker@1.4.4
10+
- @executor-js/plugin-google@1.5.20
11+
- @executor-js/plugin-microsoft@1.5.20
12+
- @executor-js/sdk@1.5.21
13+
- @executor-js/runtime-quickjs@1.5.21
14+
- @executor-js/execution@1.5.21
15+
- @executor-js/plugin-graphql@1.5.21
16+
- @executor-js/plugin-mcp@1.5.21
17+
- @executor-js/api@1.4.41
18+
- @executor-js/vite-plugin@0.0.38
19+
- @executor-js/cloudflare@0.0.20
20+
- @executor-js/host-mcp@1.4.4
21+
- @executor-js/plugin-toolkits@1.5.13
22+
- @executor-js/plugin-workos-vault@0.0.2
23+
- @executor-js/react@1.4.41
24+
25+
## 1.4.38
26+
27+
### Patch Changes
28+
29+
- Updated dependencies []:
30+
- @executor-js/sdk@1.5.20
31+
- @executor-js/runtime-quickjs@1.5.20
32+
- @executor-js/execution@1.5.20
33+
- @executor-js/plugin-graphql@1.5.20
34+
- @executor-js/plugin-mcp@1.5.20
35+
- @executor-js/plugin-openapi@1.5.20
36+
- @executor-js/api@1.4.40
37+
- @executor-js/vite-plugin@0.0.37
38+
- @executor-js/cloudflare@0.0.19
39+
- @executor-js/host-mcp@1.4.4
40+
- @executor-js/runtime-dynamic-worker@1.4.4
41+
- @executor-js/plugin-google@1.5.19
42+
- @executor-js/plugin-microsoft@1.5.19
43+
- @executor-js/plugin-workos-vault@0.0.2
44+
- @executor-js/react@1.4.40
45+
46+
## 1.4.37
47+
48+
### Patch Changes
49+
50+
- Updated dependencies []:
51+
- @executor-js/sdk@1.5.19
52+
- @executor-js/runtime-quickjs@1.5.19
53+
- @executor-js/execution@1.5.19
54+
- @executor-js/plugin-graphql@1.5.19
55+
- @executor-js/plugin-mcp@1.5.19
56+
- @executor-js/plugin-openapi@1.5.19
57+
- @executor-js/api@1.4.39
58+
- @executor-js/vite-plugin@0.0.36
59+
- @executor-js/cloudflare@0.0.18
60+
- @executor-js/host-mcp@1.4.4
61+
- @executor-js/runtime-dynamic-worker@1.4.4
62+
- @executor-js/plugin-google@1.5.18
63+
- @executor-js/plugin-microsoft@1.5.18
64+
- @executor-js/plugin-workos-vault@0.0.2
65+
- @executor-js/react@1.4.39
66+
367
## 1.4.36
468

569
### Patch Changes

apps/cloud/executor.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { microsoftHttpPlugin } from "@executor-js/plugin-microsoft/api";
55
import { mcpHttpPlugin } from "@executor-js/plugin-mcp/api";
66
import { graphqlHttpPlugin } from "@executor-js/plugin-graphql/api";
77
import { workosVaultPlugin, type WorkOSVaultClient } from "@executor-js/plugin-workos-vault";
8+
import { toolkitsPlugin } from "@executor-js/plugin-toolkits/server";
89

910
// ---------------------------------------------------------------------------
1011
// Single source of truth for the cloud app's plugin list.
@@ -40,10 +41,11 @@ interface CloudPluginDeps {
4041
* bypass the real WorkOS API. Production leaves this undefined and
4142
* falls back to the credential-driven default. */
4243
readonly workosVaultClient?: WorkOSVaultClient;
44+
readonly activeToolkitSlug?: string;
4345
}
4446

4547
export default defineExecutorConfig({
46-
plugins: ({ workosCredentials, workosVaultClient }: CloudPluginDeps = {}) =>
48+
plugins: ({ workosCredentials, workosVaultClient, activeToolkitSlug }: CloudPluginDeps = {}) =>
4749
[
4850
openApiHttpPlugin(),
4951
googleHttpPlugin(),
@@ -52,6 +54,7 @@ export default defineExecutorConfig({
5254
dangerouslyAllowStdioMCP: false,
5355
}),
5456
graphqlHttpPlugin(),
57+
toolkitsPlugin({ activeToolkitSlug }),
5558
workosVaultPlugin({
5659
credentials: workosCredentials ?? { apiKey: "", clientId: "" },
5760
...(workosVaultClient ? { client: workosVaultClient } : {}),

apps/cloud/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@executor-js/cloud",
3-
"version": "1.4.36",
3+
"version": "1.4.39",
44
"private": true,
55
"type": "module",
66
"scripts": {
@@ -46,6 +46,7 @@
4646
"@executor-js/plugin-mcp": "workspace:*",
4747
"@executor-js/plugin-microsoft": "workspace:*",
4848
"@executor-js/plugin-openapi": "workspace:*",
49+
"@executor-js/plugin-toolkits": "workspace:*",
4950
"@executor-js/plugin-workos-vault": "workspace:*",
5051
"@executor-js/react": "workspace:*",
5152
"@executor-js/runtime-dynamic-worker": "workspace:*",

0 commit comments

Comments
 (0)