Policies expose the full storage stack with target selector#513
Draft
RhysSullivan wants to merge 8 commits intors/cloud-workspaces-12-secrets-connectionsfrom
Draft
Policies expose the full storage stack with target selector#513RhysSullivan wants to merge 8 commits intors/cloud-workspaces-12-secrets-connectionsfrom
RhysSullivan wants to merge 8 commits intors/cloud-workspaces-12-secrets-connectionsfrom
Conversation
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.
This was referenced May 4, 2026
Owner
Author
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | cfc3b42 | May 04 2026, 05:13 PM |
Deploying with
|
| 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 |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/storage-core
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-google-discovery
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Policies expose the full storage stack with target selector
The SDK's
executor.policies.createandpolicies.listalready acceptevery scope in the URL-resolved stack (
assertScopedWritevalidatesmembership;
resolveToolPolicyranks byscopeRankso the innermostmatching row wins). What was missing was the visible UI surface — the
add-policy form pinned writes to the active write scope.
AddPolicyFormnow renders aCredentialTargetSelector("Apply to") sousers can target any of the four levels in workspace context (or both in
global). The page-level
scopeIdcontinues to drive the optimisticlist/cache family; the form passes the explicit target through to the
API call.
policies-stack.node.test.tspins both invariants:and listing tags each row with its owning scope.
policies.listreturns 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.