Skip to content

itx kernel: CapTarget — capabilities are a name plus a typed target#1436

Merged
jonastemplestein merged 1 commit into
mainfrom
itx-captarget
Jun 10, 2026
Merged

itx kernel: CapTarget — capabilities are a name plus a typed target#1436
jonastemplestein merged 1 commit into
mainfrom
itx-captarget

Conversation

@jonastemplestein

@jonastemplestein jonastemplestein commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Supersedes #1433, which GitHub auto-closed when its base branch (medieval-fibre, merged as #1428) was deleted. Same commit, now based on main.

What

First implementation slice of the CapTarget design from #1428 (design of record: src/itx/types.ts).

A capability is now a name plus a typed target. caps.define accepts:

// raw platform binding — members replay applies the path onto env.AI
await itx.caps.define({
  name: "ai",
  target: { type: "rpc", worker: { type: "binding", binding: "AI" } },
});
await itx.ai.models();

// the same binding through the thin policy wrapper (itx-next §2 pattern)
await itx.caps.define({
  invoke: "path-call",
  name: "aiWrapped",
  target: { type: "rpc", worker: { type: "loopback" }, entrypoint: "BindingCapability", props: { binding: "AI" } },
});
  • registry.borrowTarget() is the two-case switch the design promised: live table, or resolve the stored target.
  • Implemented worker refs: binding, loopback, source (the old worker/facet paths, unchanged underneath). url / durable-object / project-worker fail with informative not-implemented errors at define time.
  • BindingCapability: new loopback entrypoint, the thin policy wrapper for bindings — gateway/quota policy slots in there later.
  • kind: "facet" is gone from the API (legacy input still accepted): statefulness is source.exportType: "worker-entrypoint" | "durable-object". cacheKey replaces codeId (deprecated alias kept).
  • CapMeta is open metadata with the instructions convention, lifted into describe() output.

Security

binding and loopback refs reach platform resources (an open list would let any project handle reach the deployment D1, or mint itx handles on arbitrary projects via ItxEntrypoint props). They are gated on hardcoded allowlists — DIALABLE_BINDINGS = {AI}, DIALABLE_LOOPBACKS = {BindingCapability} — checked at define time (fail fast) and again at dial time (authoritative). BindingCapability re-checks the binding allowlist itself since its props are definer-controlled. Config-driven lists are a follow-up.

Compatibility

  • caps.define({ source, kind: "worker" | "facet" }) still works — normalizes to an rpc/source target.
  • Stored rows from before this PR (kind worker/facet + source_json) normalize on read; new rows also write source_json for source targets so a rollback can still read them. New target_json column added via guarded ALTER.
  • describe() now reports kind: "rpc" for stored-source caps (e2e assertion updated).

Testing

  • pnpm typecheck / lint / format / test (190 unit tests) all green.
  • Full pnpm e2e:itx suite ran green against a live deployment (19 tests, 5 files, via the dev_jonas tunnel serving this branch): itx.ai.models() through the real AI binding raw + wrapped, allowlist refusals, and all pre-existing egress/facet/fork/HTTP tests — no regressions.

🤖 Generated with Claude Code


Note

High Risk
Introduces platform binding and loopback dialing gated only on small hardcoded allowlists; misconfiguration or future allowlist expansion could expose env bindings like D1 or arbitrary loopback exports to project handles.

Overview
Capabilities are now a name plus a typed SerializableCapTarget, not legacy source + kind: worker | facet. caps.define accepts target (e.g. rpc with binding, loopback, or source worker refs); old source/kind input still normalizes to rpc/source.

Registry dispatch is refactored to the two-case model: live in-memory connections, or resolveTarget() at invoke time for stored targets. SQLite gains target_json (with legacy row normalization via targetOf()); describe() reports kind: "rpc" and lifts meta.instructions.

Platform binding exposure adds BindingCapability (loopback path-call wrapper), host binding resolvers on Project/Context DOs, and allowlists DIALABLE_BINDINGS / DIALABLE_LOOPBACKS enforced at define and dial time.

Protocol updates: CapKind becomes live | rpc | url; cacheKey replaces codeId; exportType replaces facet kind; url / project-worker / durable-object refs fail at define time until implemented.

E2E covers raw AI binding caps, wrapped BindingCapability, and allowlist rejections.

Reviewed by Cursor Bugbot for commit 13ab9e5. 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: 13ab9e5
Preview: https://os.iterate-preview-2.com
Summary: Preview app released.
Workflow run
Updated: 2026-06-10T12:13:49.919Z

Implements the first slice of itx-next.md §1/§2 (design of record:
src/itx/types.ts):

- protocol.ts: SerializableCapTarget (rpc | url) and WorkerRef (binding |
  loopback | project-worker | durable-object | source). CapKind becomes
  the target's type ("live" | "rpc" | "url"); legacy "worker"/"facet"
  rows normalize on read. CapSource gains cacheKey (codeId kept as
  deprecated alias) and exportType ("worker-entrypoint" |
  "durable-object", replacing kind: "facet"). CapMeta is open metadata
  with the `instructions` convention, lifted into CapDescription.

- registry.ts: borrowTarget is now the two-case switch — live table, or
  resolve the stored target. Implemented refs: binding (env lookup via a
  new host hook), loopback (entrypoint allowlist + attribution props),
  source (existing loader/facet paths). url / durable-object /
  project-worker refs fail with informative not-implemented errors at
  define time. New target_json column (guarded ALTER); source_json kept
  in sync for source targets so pre-CapTarget code can read rows written
  by this version.

- Security: binding and loopback refs reach PLATFORM resources, so they
  are gated on hardcoded allowlists (DIALABLE_BINDINGS = {AI},
  DIALABLE_LOOPBACKS = {BindingCapability}) at define time (fail fast)
  and dial time (authoritative). Config-driven lists are a follow-up.

- entrypoint.ts: BindingCapability — the thin policy wrapper for
  platform bindings (itx-next §2): a path-call loopback that replays the
  dotted path onto env[props.binding], with its own allowlist check
  (props are definer-controlled). Gateway/quota policy slots in here.

- e2e: new test proving itx.ai.models() via a raw binding ref, the same
  through the BindingCapability wrapper, allowlist refusals (DB binding,
  ItxEntrypoint loopback), and describe() reporting new kinds +
  instructions. Existing describe assertion updated ("worker" → "rpc").

Backward compatible: caps.define({ source, kind }) still works and
normalizes to an rpc/source target; existing stored rows keep working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jonastemplestein jonastemplestein merged commit 6525b22 into main Jun 10, 2026
9 checks passed
@jonastemplestein jonastemplestein deleted the itx-captarget branch June 10, 2026 12:12
jonastemplestein added a commit that referenced this pull request Jun 10, 2026
)

Stacks on the merged CapTarget kernel (#1436). Part of the codemode
rip-out sequence (itx-next.md §4).

## What

The first-party half of the §1 litmus test: **MCP is a client
implementation, not a transport** — so it's an ordinary
loopback-dialable RPC target, parameterized per server:

```ts
await itx.caps.define({
  invoke: "path-call",
  name: "docs",
  target: {
    type: "rpc",
    worker: { type: "loopback" },
    entrypoint: "McpClient",
    props: {
      serverUrl: "https://docs.example.com/mcp",
      headers: { authorization: 'Bearer getSecret({ key: "DOCS_TOKEN" })' },
    },
  },
});

await itx.docs.listTools();
await itx.docs.someToolName({ ... });
```

- **All transport HTTP rides project egress** via the cap's own itx
handle (the MCP SDK's custom-`fetch` option), so header values may carry
`getSecret()` placeholders — substitution happens in the Project DO, the
credential never exists in the isolate (Law 5).
- **Stateless by design**: connect → call → close per invocation. When
handshake latency matters, the same class becomes a `durable-object` ref
without changing callers (the connection-caching pattern of the old
`OutboundMcpFromOurClientCapability` DO).
- `listTools()` is an ordinary call, not a special discovery protocol —
same convention the codemode provider used.
- Added to `DIALABLE_LOOPBACKS`; result shaping reuses the existing
`outbound-mcp-from-our-client-capability-core` helpers.

In the upcoming codemode-drop PR this replaces `connectToMcpServer` and
`createOutboundMcpFromOurClientToolProviderRegistration`.

## Testing

typecheck / lint / format / unit green. New e2e test (`itx.e2e.test.ts`)
defines an McpClient cap, lists tools, and calls the first one — gated
on `OS_E2E_MCP_SERVER_URL` or `MOCK_PROVIDER_BASE_URL` (skips when no
MCP server is reachable).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Expands dialable loopback surface and routes all MCP HTTP through
project egress; behavior is gated by existing allowlists and attribution
checks but affects outbound networking for defined caps.
> 
> **Overview**
> Adds a first-party **`McpClient`** loopback RPC capability so projects
can define remote MCP servers as ordinary `path-call` caps
(`listTools()` plus dotted tool calls). Each invocation is **connect →
call → close**; transport HTTP is forced through **`itx.fetch`**
(project egress) so header secrets resolve in the Project DO, and tool
listing/execution reuse the existing outbound MCP core helpers.
> 
> **`McpClient`** is allowlisted in **`DIALABLE_LOOPBACKS`** and
exported from the main worker. Several itx entrypoints switch from
**`Reflect.get(this, "ctx")`** to **`this.ctx.props`**. E2E gains a
skipped-when-no-server test that defines an **`McpClient`** cap and
exercises list/call, plus **`createdProjectIds`** cleanup for the AI
bindings test project.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
236953d. 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-10T13:52:03.377Z",
      "headSha": "236953da701399b35300d23cc79c0a728f536743",
      "message": null,
      "publicUrl": "https://os.iterate-preview-5.com",
"runUrl": "https://github.com/iterate/iterate/actions/runs/27280672268",
      "shortSha": "236953d"
    }
  },
  "environmentConfigLease": {
    "dopplerConfig": "preview_5",
    "leasedUntil": 1781102759817,
    "leaseId": "8f0c5c8c-96b3-43bd-9ed2-1126964f26ec",
    "slug": "preview-5",
    "type": "environment-config-lease"
  }
}
-->
<!-- /CLOUDFLARE_PREVIEW_STATE -->
Lease: `preview-5`
Doppler config: `preview_5`
Type: `environment-config-lease`
Leased until: 2026-06-10T14:45:59.817Z

### OS
Status: deployed
Commit: `236953d`
Preview: https://os.iterate-preview-5.com
[Workflow
run](https://github.com/iterate/iterate/actions/runs/27280672268)
Updated: 2026-06-10T13:52:03.377Z
<!-- /CLOUDFLARE_PREVIEW -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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