itx: repos/workspace/worker become platform defaults; origin-carrying delegation; durable-object refs#1482
Merged
Merged
Conversation
…atform defaults; durable-object refs ship The §8 endgame plus the last WorkerRef kind, no backcompat: - repos, workspace, and worker are ordinary platform:project definitions now (ReposCapability/WorkspaceCapability loopbacks; the ProjectWorker forwarder with a members inner replay). Their handle getters, reserved names, and callWorkerFunction are deleted; shadowing works because shadowing already works. Remaining kernel: caps, streams, fetch, fork, project, projects, describe — the parts that ARE access checks and narrowing. - Chain delegation carries the ORIGINATING context (itxInvoke gains origin; the registry injects it as the context attribution prop), so the context-SCOPED workspace still resolves per caller: a forked child gets itx:ctx_…, the project gets itx, even though the definition lives two links up. e2e proves sibling forks cannot see each other's files. - durable-object WorkerRef refs are implemented (getByName + the normal invoke modes) behind an EMPTY namespace allowlist: every platform namespace is keyed across all projects, so deployments must opt namespaces in via APP_CONFIG_ITX.dialableDurableObjects. - Behavioral change: itx.worker.fetch no longer special-cases to project ingress — it replays fetch on the worker's default export like any member. - Deferred with reasons (itx-next.md): streams stays kernel (the access model lives there); REPL-consumes-types.ts; UrlDial-through-intercept (the capnweb fork only helps capnweb hops; UrlDial→DO is jsrpc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0c01a32. Configure here.
itxInvoke carries the trusted chain-delegation origin and itxProjectWorkerCall takes registry-merged props — reachable through itx.project's whole-surface proxy, either would let any handle holder spoof another context's identity (e.g. read a sibling fork's workspace). The proxy now refuses path heads matching itx[A-Z]*; the registry's reserved-segment gate stays as defense in depth for real chain traffic. e2e probes the spoof route directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein
added a commit
that referenced
this pull request
Jun 10, 2026
… move origin/main (#1482) moved repos off the handle's trust kernel onto the platform:project cap, so the stub's static type no longer knows itx.repos. The two repos routes now spell it itx.cap("repos") + Stubify — same runtime fallthrough, typed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein
added a commit
that referenced
this pull request
Jun 11, 2026
Main independently shipped repos/workspace/worker as platform defaults with origin-carrying chain delegation (the correct fix for inherited context-scoped caps) and config-gated durable-object refs — that implementation wins wholesale. Re-applied this branch's unique layers on top: the egress capability (EgressPipe default + dialable, registry- dispatching itx.fetch), the isolate-wiring unification in the registry's loadWorker, the workers-RPC-safe onRpcBroken guard in provide, and auth-routed id minting in ItxProjects.create. Dropped from this branch in deference to main's choices: project-as-default (project stays a hardwired built-in) and the PROJECT entry in DIALABLE_DURABLE_OBJECTS (allowlist stays empty by default, config-gated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein
added a commit
that referenced
this pull request
Jun 11, 2026
… names, REPL reads types.ts (#1490) The four follow-ups the consolidation arc left written down in itx-next.md, in one PR. Net **−578 lines** while adding a whole new capability surface. No backcompat anywhere. --- ## 1. The egress intercept tunnel is deleted — shadowing was already the feature **Motivation.** #1487 made `fetch` an ordinary shadowable capability. At that point the captun-based intercept tunnel was a second, hand-built implementation of the same idea: "while I'm connected, route the project's egress through me." It had its own DO field, accept route, keepalive plumbing, a special `projectEgressInterceptActive` flag threaded through secret substitution, a benchmark script, and a bespoke test harness. All of it is expressible as one `caps.define`. **Before** (the tunnel era): ```ts // Client: dial a special captun endpoint on the project's ingress const tunnel = await createCaptunTunnel({ url: `${ingressUrl}/__iterate/intercept-project-egress`, headers: { Authorization: `Bearer ${adminToken}` }, fetch: myFetch, }); // Server: #projectEgressInterceptTunnel field, accept/replace/teardown // lifecycle, an egressFetch branch, and a substitution mode that withheld // real secret material while a tunnel was connected. ``` **After** (it's just a cap): ```ts using itx = connectItx({ baseUrl, token, context: projectId }); class Interceptor extends RpcTarget { async call({ args }) { return await myFetch(args[0]); } // args[0] is the Request } await itx.caps.define({ name: "fetch", invoke: "path-call", target: new Interceptor() }); // ALL project egress — itx.fetch() and bare fetch() in every loaded isolate — // now flows through myFetch. Drop the session and the default pipe resurfaces. ``` The security property came along for free and got *simpler*: the tunnel needed an explicit "withhold secrets while intercepted" mode inside substitution; a shadow provider simply never reaches the substituting pipe, so it sees `getSecret(...)` placeholders verbatim. The flag is gone; substitution always yields real material on the one path that has any. The e2e fixture's `egressFetch` option keeps its exact API (now implemented as above), and the workerd ingress test was rewritten to the cap story — which caught a real bug: probing `onRpcBroken` on a Workers-RPC live provider rejects unhandled (jsrpc proxies every property as a remote method); the registry now treats it as best-effort. ## 2. `streams` joins the platform defaults **Motivation.** After #1482/#1487 the kernel was `caps, streams, fork, project, projects, describe`. `streams` was only still hardwired because its access checks live in the handle. But split the concern in two and the blocker dissolves: on a *project* context the namespace is **forced** to the project (there is no access decision to make — registry-injected `projectId` props pin it, definers can't point it elsewhere), and only the *global* namespace genuinely needs the connect-time access set. **Before:** `ItxStreams`/`ItxStream` hardwired into `handle.ts`, reserved name, not shadowable. **After:** ```ts // platform:project (code-contexts.ts) — just another definition: caps.define({ name: "streams", target: { type: "rpc", worker: { type: "loopback" }, entrypoint: "StreamsCap" }, }); // …which means a context can now shadow its own event-stream surface: await itx.caps.define({ name: "streams", invoke: "path-call", target: myStreamsFake }); ``` The collection/stream classes moved to `src/itx/caps/streams.ts`, parameterized by an explicit `StreamsScope { access, exports }`; the handle's getter branches — project handles resolve through the registry (shadowable), global handles keep the kernel branch for the deployment-wide `"global"` namespace gated on `access === "all"`. Everything else is unchanged: absolute refs (`"ns:/path"`) still go through the one access check with NOT_FOUND masking. Two things made this safe to ship rather than scary: - **Chained calls ride RPC promise pipelining.** `itx.streams.get("/x").append(e)` crosses a boundary in every real execution mode (capnweb from browsers/Node, jsrpc from loaded isolates), and both transports pipeline follow-up calls onto returned RpcTargets — the same shape `itx.agents.create().doThing()` already proved. - **Subscriptions survive the extra hop.** `subscribe` callbacks now cross client → registry DO → StreamsCap → Stream DO; the existing dup-discipline in StreamsCapability holds, proven by the subscribe e2e suite running unchanged. ## 3. Durable-object dials are name-scoped **Motivation.** #1482 shipped `{ type: "durable-object", binding, name }` refs behind an *empty* allowlist, because raw names meant an allowlisted namespace would let any project dial any other project's instances — documented as "the open design before any namespace can join." **Resolved:** the registry now dials ```ts namespace.getByName(`itx:${projectId}:${name}`) ``` so every allowlisted namespace's itx-reachable instances are **disjoint per project by construction** — the definer's `name` is a label inside their project's slice, not a global address. Deployments can now actually use `APP_CONFIG_ITX.dialableDurableObjects` for namespaces designed for itx use. (Namespaces whose *existing* instances matter — PROJECT, STREAM — still don't belong on the list: itx dials would reach fresh, empty objects, which is exactly the point.) ## 4. The REPL editor consumes types.ts — drift is now structurally impossible **Motivation.** `apps/os/src/itx/types.ts` is the handwritten design-of-record for the whole itx surface; the browser REPL's editor carried a second, hand-maintained 353-line ambient declaration with a "keep in sync" comment — the kind that's wrong within a week. **After:** the REPL's TypeScript virtual FS loads `types.ts` **verbatim** (`import source from "~/itx/types.ts?raw"`, which works in both the vite-bundled worker and vitest). The hand-written file shrank to a 120-line prelude declaring only what types.ts can't know: the session globals (`itx`, `vars`, `projectId`, `RpcTarget`, `$_`). A bonus improvement fell out: the capability fallthrough is now declared on the official `KnownCaps` merge point, so handles returned by `itx.fork()` / `itx.projects.get()` carry it too — `(await itx.projects.get(id)).slack.chat.postMessage` typechecks in the editor, which the old ambient got wrong. Tests assert the editor sees types.ts-only markers, so regressions are loud. --- ## The kernel after this PR ```text caps, fork, project, projects, describe ← the trust kernel streams (global namespace only) ← connect-time access, by nature ───────────────────────────────────────────── ai, fetch, streams, repos, workspace, worker ← platform:project definitions, i.e. literally the data structures caps.define takes, every one of them shadowable ``` ## Verification `pnpm typecheck` / `lint` / `knip` / `format` green; apps/os unit tests 222 green; workerd `test:project-ingress` 6/6 (including the rewritten fetch-shadow-sees-placeholders test); itx e2e — 32 tests across core/fork/http/subscribe suites, exercising streams through the registry path, the example catalogue in every runtime, fork workspace isolation, and the full fetch-shadow story — green against a local dev server. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches project egress, secret substitution, and ingress/DO routing paths used in production and e2e; behavior changes are intentional but need regression on fetch shadowing and streams via the registry. > > **Overview** > Removes the **Project Egress Intercept Tunnel** (captun route on the Project DO/ingress, tunnel state, intercept-specific secret withholding, benchmark script, and e2e captun helper) and replaces interception with a **session-bound live `fetch` capability shadow** on the project itx context—interceptors see `getSecret(...)` placeholders because substitution only runs on the default egress pipe. > > **`streams` becomes a shadowable `platform:project` default** (`StreamsCap` loopback in `caps/streams.ts`); project handles resolve `itx.streams` through the registry while the global `"global"` namespace stays a kernel branch gated on admin access. **Durable-object capability dials** now use `itx:<projectId>:<name>` so allowlisted namespaces are per-project disjoint. > > The **browser REPL editor** loads `~/itx/types.ts` verbatim via `?raw` instead of a large hand-maintained ambient file; docs/ADR/context and tests are updated accordingly, including registry best-effort `onRpcBroken` for Workers-RPC live providers. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 93efa03. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- CLOUDFLARE_PREVIEW --> ## Environment Config Lease <!-- CLOUDFLARE_PREVIEW_STATE --> <!-- { "apps": { "os": { "appDisplayName": "OS", "appSlug": "os", "status": "deployed", "updatedAt": "2026-06-11T07:07:45.989Z", "headSha": "93efa03e779290348755a99b9317ae9274c20261", "message": null, "publicUrl": "https://os.iterate-preview-2.com", "runUrl": "https://github.com/iterate/iterate/actions/runs/27329834559", "shortSha": "93efa03" } }, "environmentConfigLease": { "dopplerConfig": "preview_2", "leasedUntil": 1781164984175, "leaseId": "eae024be-b4d7-4976-92e4-0013205b125e", "slug": "preview-2", "type": "environment-config-lease" } } --> <!-- /CLOUDFLARE_PREVIEW_STATE --> Lease: `preview-2` Doppler config: `preview_2` Type: `environment-config-lease` Leased until: 2026-06-11T08:03:04.175Z ### OS Status: deployed Commit: `93efa03` Preview: https://os.iterate-preview-2.com [Workflow run](https://github.com/iterate/iterate/actions/runs/27329834559) Updated: 2026-06-11T07:07:45.989Z <!-- /CLOUDFLARE_PREVIEW --> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jonastemplestein
added a commit
that referenced
this pull request
Jun 11, 2026
…tercept dies, kernel shrinks, auth mints, legacy afterAppend deleted (#1485) ## What The remaining grand-cleanup workstreams in one deliberately breaking PR (prd gets redeployed). DECISIONS **D23** is the canonical record. Three main-side PRs landed mid-flight and overlap this work — all adopted wholesale in the merges: **#1482** (repos/workspace/worker as platform defaults with origin-carrying delegation), **#1487** (`fetch` is a shadowable cap, `define` absorbs `provide`, shared registry host), and **#1490** (intercept tunnel deleted, streams is a cap, best-effort `onRpcBroken`). This PR contributes the layers below on top of them. ### §9 finished: the egress pipe is stateless - #1487/#1490 made `fetch` a shadowable platform:project cap and deleted the tunnel, but kept the DEFAULT pipe inside the Project DO (`ProjectEgress.call` → `egressFetch`). This PR replaces that terminal with the stateless **`EgressPipe`** loopback. The Project DO still supervises every dispatch (live shadows resolve in its registry), but egress secrets are D1 rows scoped by the registry-injected `projectId`, so substitution + the real outbound fetch run in a plain isolate and **secret material never enters the DO**. - **The Project DO has no fetch surface at all** — no `fetch`, no `ingressFetch`, no `egressFetch`. ### Worker-loading unification - `itx/isolate.ts` is the ONE place the platform's trust posture (Law 4 ITERATE scoping, Law 5 egress outbound) is wired into loaded isolates; the registry's source caps and the project worker both use it. (The Workers-RPC-safe `onRpcBroken` guard this PR carried shipped independently in #1490 — main's version adopted.) ### `ProjectCapability` dissolved The hand-wired forwarder entrypoint is deleted; nothing called it. ### Auth is the ONLY project-id minter New auth internal route `POST /internal/project/mint-project-id` (service-authed); OS operator/recovery creates (project directory + `itx.projects.create`) round-trip through it. `mintProjectId` is deleted from OS — the `prj_` id space has exactly one source. ### Legacy afterAppend/runner-state deleted The agent, slack-agent, slack-integration, and repo DOs lose their `afterAppend` RPCs and fake runner shapes (delivery has been on the host model for a while). Agent runtime state is now the honest `{ agentPath, processors: { [slug]: snapshot } }`; slack `ensureReady` returns a plain snapshot; the agent-stream benchmark updated. ## Deferred to main's posture (from the original plan) - `project` stays a hardwired built-in (per #1482's kernel choice) rather than a durable-object default; `DIALABLE_DURABLE_OBJECTS` stays empty by default (config-gated). - The egress cap is named `fetch` (per #1487), not `egress`. ##⚠️ Merge order **#1489 must merge (and auth deploy) first** — this PR's create paths round-trip id minting through auth's new `/internal/project/mint-project-id`, and previews point at production auth. The preview e2e here 404s until that endpoint is live. ## Breaking changes (intended) - Agent `runtimeState` shape changed (consumers were shape-agnostic or updated). - `egressFetch` is gone from every surface; use `itx.fetch` / the `egress` cap. ## Testing - Full repo gates green (typecheck, lint, 35/35 apps/os test files). - Workers suites: project-ingress 6/6 (incl. live-shadow + revoke-restores-default), itx-stream-subscribe 13/13. - `project-mcp-server-connection` fails 2/3 **identically on the branch base** (verified in a clean worktree) — pre-existing. - Preview e2e exercises: the egress capability over capnweb (explicit + implicit doors), the new live-shadow helper, and auth-routed minting. ## Out of scope - Egress policy-as-data / hold-for-approval (the §9 follow-on). - Stream processors taking a synchronous SQL client (jam). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Breaking egress and secret-handling semantics (DO no longer substitutes secrets; interceptors see raw placeholders), new auth dependency for id minting, and changed agent runtimeState shape affect security-sensitive paths and deploy ordering. > > **Overview** > Completes **itx D23**: project egress is a shadowable **`fetch`** capability whose default terminal is the stateless **`EgressPipe`** (secret substitution + outbound fetch in a plain isolate), while the Project DO only supervises registry dispatch. **`fetch` / `egressFetch` are removed** from the Project DO; **`ProjectCapability`** is deleted. > > Adds **`itx/isolate.ts`** so project workers, source caps, and the run harness share one **ITERATE + `ProjectEgress` globalOutbound** wiring path. > > **Auth is the sole `prj_` minter**: OS drops local **`mintProjectId`**; operator/admin and **`itx.projects.create`** call auth’s **`mintProjectId`** internal route. > > Removes legacy **`afterAppend`** / runner-shaped RPCs on agent, slack, and repo DOs; agent **`runtimeState`** is **`{ agentPath, processors }`** (benchmark updated). Docs mark §8/§9 shipped; live **`fetch`** shadows see **raw** `getSecret(...)` placeholders (withheld-text mode removed). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit df5965b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- CLOUDFLARE_PREVIEW --> ## Environment Config Lease <!-- CLOUDFLARE_PREVIEW_STATE --> <!-- { "apps": { "os": { "appDisplayName": "OS", "appSlug": "os", "status": "deployed", "updatedAt": "2026-06-11T10:23:48.699Z", "headSha": "df5965b9948016c979fa5a71ae3b991f66e8c42c", "message": null, "publicUrl": "https://os.iterate-preview-6.com", "runUrl": "https://github.com/iterate/iterate/actions/runs/27340067055", "shortSha": "df5965b" } }, "environmentConfigLease": { "dopplerConfig": "preview_6", "leasedUntil": 1781176786363, "leaseId": "9c50031d-b4ce-4f00-a8fe-66a3ff9f9df5", "slug": "preview-6", "type": "environment-config-lease" } } --> <!-- /CLOUDFLARE_PREVIEW_STATE --> Lease: `preview-6` Doppler config: `preview_6` Type: `environment-config-lease` Leased until: 2026-06-11T11:19:46.363Z ### OS Status: deployed Commit: `df5965b` Preview: https://os.iterate-preview-6.com [Workflow run](https://github.com/iterate/iterate/actions/runs/27340067055) Updated: 2026-06-11T10:23:48.699Z <!-- /CLOUDFLARE_PREVIEW --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

The remaining itx roadmap on latest main, no backcompat (prd is redeployable): §8's endgame — the hardwired kernel becomes ordinary capability definitions — plus the last unimplemented WorkerRef kind.
repos / workspace / worker are platform defaults now
All three move into the
platform:projectcode context (src/itx/code-contexts.ts):reposandworkspaceas ReposCapability/WorkspaceCapability loopbacks,workerthrough the ProjectWorker forwarder (path-call hop, members inner replay against the default export). Their handle getters are deleted, their names leave the reserved list (shadowable — the point), andcallWorkerFunctionon the Project DO dies (the forwarder path covers it). What remains hardwired is exactly the composition a cap definition cannot express:caps,streams(the access model and global-namespace gating live there),fetch(Law 5),fork,project,projects,describe.Chain delegation carries the originating context
The enabling protocol change (allowed by no-backcompat):
itxInvokegainsorigin, set by the first delegating hop and preserved up the chain; the registry's dial-timecontextattribution prop uses it. This is what makes a context-SCOPED cap correct as an inherited definition —workspaceresolves toitx:ctx_…for a forked child anditxfor the project even though the definition lives two links up. WorkspaceCapability now derives its workspace id from the injected context (explicitworkspaceIdstill wins; the agent DO's pre-clone wiring is unchanged and uses the same derivation).New e2e: a child fork writes to its workspace; neither the project context nor a sibling fork can read the file.
durable-object WorkerRef refs ship, config-gated
{ type: "durable-object", binding, name }resolves viaenv[binding].getByName(name)with the normal members/path-call dispatch. The namespace allowlist (DIALABLE_DURABLE_OBJECTS) is empty by default: every platform namespace (PROJECT, STREAM, …) is keyed across all projects, so allowlisting one would let any project handle dial any other project's objects by name. Deployments opt in viaAPP_CONFIG_ITX.dialableDurableObjects; per-project name scoping is the documented open design before any namespace can join the defaults. e2e asserts the define-time refusal.Behavioral changes
itx.worker.fetchno longer special-cases to project ingress — it replaysfetchon the worker's default export like any other member. The project homepage is the ingress URL.describe()on a fresh project now lists four inherited caps (ai,repos,workspace,worker) withowner: "platform:project".Deferred, with reasons (in itx-next.md)
streamsstays kernel: resolution checks access, which a cap definition cannot express.Verification
pnpm typecheck/lint/knip/formatgreen; apps/os unit tests green (210); itx e2e (itx, fork incl. the new isolation test, http, subscribe — 21 tests) green against a local dev server.🤖 Generated with Claude Code
Note
High Risk
Protocol and dispatch changes (
origin, workspace scoping, removedcallWorkerFunction) affect fork isolation and worker routing; empty-default DO allowlist is security-critical if misconfigured.Overview
Continues §8 “cap #0 disappears”:
repos,workspace, andworkermove from hardwiredItxgetters into theplatform:projectcode context as ordinary loopback caps (ReposCapability,WorkspaceCapability,ProjectWorker). Those names leave the reserved list (shadowable);callWorkerFunctionon the Project DO is removed in favor of the existing forwarder path.Chain delegation now carries the originating context:
itxInvokeaccepts optionalorigin, set on the first delegating hop and forwarded up the chain; the registry uses it for dial-timecontextattribution so inherited caps stay context-scoped—notablyworkspace(itxvsitx:ctx_…per fork).WorkspaceCapabilityderivesworkspaceIdfrom injectedcontextwhen not explicit.durable-objectWorkerRefs are implemented behind an empty-by-defaultDIALABLE_DURABLE_OBJECTSallowlist, widened only viaAPP_CONFIG_ITX.dialableDurableObjects.Security / behavior:
itx.projectblocksitx*registry methods (spoofingorigin);itx.worker.fetchno longer routes to ingress—it replays the worker default export like any member. Docs, REPL typings, and e2e (workspace isolation, DO define refusal) updated.Reviewed by Cursor Bugbot for commit e3e0f0a. Bugbot is set up for automated code reviews on this repo. Configure here.
Environment Config Lease
No active environment config lease.
OS
Status: released
Commit:
e3e0f0aPreview: https://os.iterate-preview-5.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-10T22:06:09.374Z