Skip to content

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

Closed
jonastemplestein wants to merge 1 commit into
medieval-fibrefrom
itx-captarget
Closed

itx kernel: CapTarget — capabilities are a name plus a typed target#1433
jonastemplestein wants to merge 1 commit into
medieval-fibrefrom
itx-captarget

Conversation

@jonastemplestein

@jonastemplestein jonastemplestein commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

What

First implementation slice of the CapTarget design from #1428 (stacked on that branch — 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.
  • New e2e test (pnpm e2e:itx): raw itx.ai.models(), the wrapped equivalent, allowlist refusals, and describe() shape. Not yet run against a deployment — needs this branch deployed to a dev/preview stage first.

🤖 Generated with Claude Code


Note

High Risk
Changes capability registration and invoke resolution in the itx kernel, including new platform binding/loopback dial paths gated by hardcoded allowlists; misconfiguration or allowlist gaps could expose env bindings or break existing caps until legacy normalization is exercised.

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

The registry borrowTarget / resolveTarget split replaces the old kind switch: live connections vs resolving stored targets at invoke time. SQLite gains target_json (with legacy read path 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 DIALABLE_BINDINGS / DIALABLE_LOOPBACKS enforced at define and dial time. CapSource gains cacheKey (alias codeId) and exportType for worker vs durable-object facets.

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

Reviewed by Cursor Bugbot for commit bc7396e. Bugbot is set up for automated code reviews on this repo. Configure here.

Environment Config Lease

Lease: preview-9
Doppler config: preview_9
Type: environment-config-lease
Leased until: 2026-06-10T12:53:19.666Z

OS

Status: deployed
Commit: bc7396e
Preview: https://os.iterate-preview-9.com
Workflow run
Updated: 2026-06-10T11:56:06.745Z

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

Copy link
Copy Markdown
Contributor Author

e2e validated: ran the full pnpm e2e:itx suite (19 tests, 5 files) against the dev_jonas tunnel serving this branch — all green, including the new "platform bindings are dialable capabilities" test exercising itx.ai.models() through the real AI binding (raw ref + BindingCapability wrapper) and the allowlist refusals. All pre-existing egress/facet/fork/HTTP tests pass too — no regressions.

@jonastemplestein jonastemplestein deleted the branch medieval-fibre June 10, 2026 12:05
jonastemplestein added a commit that referenced this pull request Jun 10, 2026
…1436)

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:

```ts
// 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](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!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.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
13ab9e5. 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-10T12:11:14.008Z",
      "headSha": "13ab9e5ccc39a1348d29b74fd55086747b727bd6",
      "message": null,
      "publicUrl": "https://os.iterate-preview-2.com",
"runUrl": "https://github.com/iterate/iterate/actions/runs/27275038072",
      "shortSha": "13ab9e5"
    }
  },
  "environmentConfigLease": {
    "dopplerConfig": "preview_2",
    "leasedUntil": 1781096892787,
    "leaseId": "f42f975c-7672-4369-bcb7-2db7a29b7c06",
    "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-10T13:08:12.787Z

### OS
Status: deployed
Commit: `13ab9e5`
Preview: https://os.iterate-preview-2.com
[Workflow
run](https://github.com/iterate/iterate/actions/runs/27275038072)
Updated: 2026-06-10T12:11:14.008Z
<!-- /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