Skip to content

Policies expose the full storage stack with target selector#513

Draft
RhysSullivan wants to merge 8 commits intors/cloud-workspaces-12-secrets-connectionsfrom
rs/cloud-workspaces-13-policies
Draft

Policies expose the full storage stack with target selector#513
RhysSullivan wants to merge 8 commits intors/cloud-workspaces-12-secrets-connectionsfrom
rs/cloud-workspaces-13-policies

Conversation

@RhysSullivan
Copy link
Copy Markdown
Owner

Policies expose the full storage stack with target selector

The SDK's executor.policies.create and policies.list already accept
every scope in the URL-resolved stack (assertScopedWrite validates
membership; resolveToolPolicy ranks by scopeRank so the innermost
matching row wins). What was missing was the visible UI surface — the
add-policy form pinned writes to the active write scope.

AddPolicyForm now renders a CredentialTargetSelector ("Apply to") so
users can target any of the four levels in workspace context (or both in
global). The page-level scopeId continues to drive the optimistic
list/cache family; the form passes the explicit target through to the
API call.

policies-stack.node.test.ts pins both invariants:

  • All four scope levels accept policy writes from workspace context,
    and listing tags each row with its owning scope.
  • When the same pattern is written at multiple scopes, policies.list
    returns rows sorted innermost-first — the executor's invocation
    path consumes that ordering for "innermost wins" precedence.

Build HttpApiBuilder.layer per protected mount so workspace prefix registers

Effect's Layer system memoizes one ProtectedCloudApiLive instance, so
piping it through both the /:org and /:org/:workspace prefixed router
layers only registered the first prefix's routes — workspace requests
fell through to RouteNotFound. Each mount now constructs its own
HttpApiBuilder.layer(ProtectedCloudApi) so both prefixes install.

Also extends the McpExtension surface and the matching api/group entry
for refreshSource to allow InvalidSourceWriteTargetError, mirroring the
addSource failure union added in chunk 11.

Mount OrgHttpApi at workspace prefix too

The URL-context fetch wrapper in the react package always prefixes
outgoing /api/... requests with the page's full URL handle pair, so
from a /:org/:workspace page even org-level endpoints (workspaces list,
org members) come through with the workspace prefix. Mount OrgHttpApi
at both /:org and /:org/:workspace so those requests resolve.

Each mount builds its own HttpApiBuilder.layer instance — Effect's
Layer system memoizes one instance otherwise and only the first prefix
registers routes (same trick we needed for the protected API).

Resolve active workspace from URL params for callers above the layout

Shell + UserFooter live in the /$org layout's render tree, ABOVE the
WorkspaceRouteProvider rendered by /$org/$workspace. So the trigger
label always saw a null context and showed "Global" even when the URL
had a workspace segment.

useOptionalWorkspaceRoute now falls back to URL params + workspacesAtom
when the React context isn't set, so any caller rendered under /$org
sees the active workspace as long as the URL has the slug. Direct
context lookup is preferred when the layout has already resolved the
workspace, to skip the atom roundtrip.

Prefix shared-package routes with active URL context via useAppHref

The shared @executor-js/react pages and the command palette hard-coded
absolute Link paths (/sources/add/$pluginKey, /sources/$namespace,
/policies) against the local app's flat route tree. On cloud those
routes live under /:org or /:org/:workspace, so clicking landed
nowhere matching and routes/index.tsx redirected back to the user's
first org.

useAppHref(path, params?, search?) reads the active prefix from
useLocation().pathname and returns a fully-qualified href, falling back
to the unmodified path on local. command-palette, sources page (3
links + the navigate call), and tools page now route through it.

Reserve org sub-route names so /:org/sources isn't read as a workspace

The fetch wrapper and href helpers split window.location.pathname into
:org / :workspace assuming the second segment is a workspace slug. On
/rhys-org/sources/add/openapi that turned 'sources' into a workspace,
so client requests for /api/scope ended up at /api/rhys-org/sources/scope
and 404'd.

Adds RESERVED_SECOND_SEGMENTS (sources, connections, secrets, policies,
tools, plus the org-admin marker '-') in a shared parseUrlContext.
Workspace slugs can't collide with these reserved names — same
constraint TanStack's file-routes already imposes — and both the fetch
prefixer and the new AppLink resolve URL context the same way.

Also introduces as a TanStack Link wrapper that prepends the
context prefix to absolute 'to' strings. The cloud-shared package now
uses AppLink instead of hand-spliced strings via useAppHref for static
hrefs (sources detail, source-add presets, manage-policies button).

Make ContextAwareHttpClient the single source of truth for /api prefixing

Plugin clients (openapi, mcp, graphql, onepassword, google-discovery)
all snapshot getBaseUrl() at module load. routes/$org.tsx + routes/
$org/$workspace.tsx then mutated the same module variable via
setBaseUrl, racing module load order — when a plugin module loaded
AFTER setBaseUrl fired it baked /api/${handle} into outgoing URLs.
The fetch wrapper then prefixed AGAIN, producing
/api/${handle}/${workspace}/${handle}/scopes/... and 404s.

createPluginAtomClient now accepts an httpClient option. Each plugin
client passes ContextAwareHttpClient so URL-rewriting is consistent
across the executor + cloud auth + plugin clients. Drops setBaseUrl
entirely; getBaseUrl is back to a pure function returning
${origin}/api. Removes the setBaseUrl calls from /$org.tsx and
/$org/$workspace.tsx — they're no longer needed since the wrapper
handles per-request prefixing.

Refresh scopeAtom when URL context changes

scopeAtom caches /scope responses for 5 minutes. The cloud middleware
returns different scope stacks for /api/:org/scope (global) vs
/api/:org/:workspace/scope (workspace), so the cached value is stale
the moment the user navigates between contexts — the SourceTargetSelector
on /:org/:workspace/sources/add/openapi only saw the org stack and
showed Global as the only option.

ScopeProvider now reads useLocation(), derives a context key
(org-handle / workspace-slug pair), and triggers useAtomRefresh on
change. Stays cached within a single context — same-context page
navigation doesn't refetch.

The SDK's `executor.policies.create` and `policies.list` already accept
every scope in the URL-resolved stack (`assertScopedWrite` validates
membership; `resolveToolPolicy` ranks by `scopeRank` so the innermost
matching row wins). What was missing was the visible UI surface — the
add-policy form pinned writes to the active write scope.

`AddPolicyForm` now renders a `CredentialTargetSelector` ("Apply to") so
users can target any of the four levels in workspace context (or both in
global). The page-level `scopeId` continues to drive the optimistic
list/cache family; the form passes the explicit target through to the
API call.

`policies-stack.node.test.ts` pins both invariants:

  - All four scope levels accept policy writes from workspace context,
    and listing tags each row with its owning scope.
  - When the same pattern is written at multiple scopes, `policies.list`
    returns rows sorted innermost-first — the executor's invocation
    path consumes that ordering for "innermost wins" precedence.
…gisters

Effect's Layer system memoizes one ProtectedCloudApiLive instance, so
piping it through both the /:org and /:org/:workspace prefixed router
layers only registered the first prefix's routes — workspace requests
fell through to RouteNotFound. Each mount now constructs its own
HttpApiBuilder.layer(ProtectedCloudApi) so both prefixes install.

Also extends the McpExtension surface and the matching api/group entry
for refreshSource to allow InvalidSourceWriteTargetError, mirroring the
addSource failure union added in chunk 11.
The URL-context fetch wrapper in the react package always prefixes
outgoing /api/... requests with the page's full URL handle pair, so
from a /:org/:workspace page even org-level endpoints (workspaces list,
org members) come through with the workspace prefix. Mount OrgHttpApi
at both /:org and /:org/:workspace so those requests resolve.

Each mount builds its own HttpApiBuilder.layer instance — Effect's
Layer system memoizes one instance otherwise and only the first prefix
registers routes (same trick we needed for the protected API).
Shell + UserFooter live in the /$org layout's render tree, ABOVE the
WorkspaceRouteProvider rendered by /$org/$workspace. So the trigger
label always saw a null context and showed "Global" even when the URL
had a workspace segment.

useOptionalWorkspaceRoute now falls back to URL params + workspacesAtom
when the React context isn't set, so any caller rendered under /$org
sees the active workspace as long as the URL has the slug. Direct
context lookup is preferred when the layout has already resolved the
workspace, to skip the atom roundtrip.
The shared @executor-js/react pages and the command palette hard-coded
absolute Link paths (/sources/add/$pluginKey, /sources/$namespace,
/policies) against the local app's flat route tree. On cloud those
routes live under /:org or /:org/:workspace, so clicking landed
nowhere matching and routes/index.tsx redirected back to the user's
first org.

useAppHref(path, params?, search?) reads the active prefix from
useLocation().pathname and returns a fully-qualified href, falling back
to the unmodified path on local. command-palette, sources page (3
links + the navigate call), and tools page now route through it.
The fetch wrapper and href helpers split window.location.pathname into
:org / :workspace assuming the second segment is a workspace slug. On
/rhys-org/sources/add/openapi that turned 'sources' into a workspace,
so client requests for /api/scope ended up at /api/rhys-org/sources/scope
and 404'd.

Adds RESERVED_SECOND_SEGMENTS (sources, connections, secrets, policies,
tools, plus the org-admin marker '-') in a shared parseUrlContext.
Workspace slugs can't collide with these reserved names — same
constraint TanStack's file-routes already imposes — and both the fetch
prefixer and the new AppLink resolve URL context the same way.

Also introduces <AppLink> as a TanStack Link wrapper that prepends the
context prefix to absolute 'to' strings. The cloud-shared package now
uses AppLink instead of hand-spliced strings via useAppHref for static
hrefs (sources detail, source-add presets, manage-policies button).
…xing

Plugin clients (openapi, mcp, graphql, onepassword, google-discovery)
all snapshot getBaseUrl() at module load. routes/$org.tsx + routes/
$org/$workspace.tsx then mutated the same module variable via
setBaseUrl, racing module load order — when a plugin module loaded
AFTER setBaseUrl fired it baked /api/${handle} into outgoing URLs.
The fetch wrapper then prefixed AGAIN, producing
/api/${handle}/${workspace}/${handle}/scopes/... and 404s.

createPluginAtomClient now accepts an httpClient option. Each plugin
client passes ContextAwareHttpClient so URL-rewriting is consistent
across the executor + cloud auth + plugin clients. Drops setBaseUrl
entirely; getBaseUrl is back to a pure function returning
${origin}/api. Removes the setBaseUrl calls from /$org.tsx and
/$org/$workspace.tsx — they're no longer needed since the wrapper
handles per-request prefixing.
scopeAtom caches /scope responses for 5 minutes. The cloud middleware
returns different scope stacks for /api/:org/scope (global) vs
/api/:org/:workspace/scope (workspace), so the cached value is stale
the moment the user navigates between contexts — the SourceTargetSelector
on /:org/:workspace/sources/add/openapi only saw the org stack and
showed Global as the only option.

ScopeProvider now reads useLocation(), derives a context key
(org-handle / workspace-slug pair), and triggers useAtomRefresh on
change. Stays cached within a single context — same-context page
navigation doesn't refetch.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 4, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud cfc3b42 May 04 2026, 05:13 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing cfc3b42 Commit Preview URL

Branch Preview URL
May 04 2026, 05:10 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 4, 2026

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@513

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@513

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@513

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@513

@executor-js/storage-core

npm i https://pkg.pr.new/@executor-js/storage-core@513

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@513

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@513

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@513

@executor-js/plugin-google-discovery

npm i https://pkg.pr.new/@executor-js/plugin-google-discovery@513

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@513

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@513

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@513

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@513

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@513

executor

npm i https://pkg.pr.new/executor@513

commit: cfc3b42

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant