Files
captoys/docs/good-fences-make-good-agents.md
T
dckcandClaude Opus 5.5 efb54ad4cc docs: Good Fences Make Good Agents (design story + IBIS)
The story behind good-fences: lend a governed mower over the fence to a
neighbor whose tenants are model-written code, with the design IBIS
worked out so far. Previously an untracked note at the workspace root.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 01:00:15 -05:00

8.8 KiB
Raw Blame History

Good Fences Make Good Agents

with apologies to Robert Frost

Let's have a fence and a neighbor. I can talk to the neighbor much like the conventional...

client.chat.completions.create({
  model: 'llama3.1:8b',
  messages: [{ role: 'user', content: 'Tell me a story' }],
  stream: true,
});

but with a twist:

  messages: [{
    role: 'user',
    content: 'you can use this mower for the weekend',
    things: { mower },
    about: { mower: { ts: 'type Mower { start(), push(), turn(), stop() }' }}
  }],

I send mower by putting a webkey into a password field on the fence form, which fetches an RpcTarget stub to give to the neighbor.

The neighbor can make its own tenants: it can write code to compose the things I gave it, and load that code into a dynamic worker that is endowed with the things.

A Mower DO factory makes mowers that go all over the place (depicted as a table with green cells for grass). But with a little js playground, I can wrap it in a governor so it never goes more than 100 yards away. I give only the governed mower to my neighbor. Even though he has 50 acres, I know most of them are for tenants, and my neighbor has no particular need to let them use it.

Even if they gets a little tipsy and wander around, try as he might, my neighbor and his tennants can't mow more than 100 yards away.

Design IBIS

?! 1. Who is the neighbor, and where does it run?

:- 1a. an LLM (llama3.1:8b) alone
  - 1a-i. a model only sees text; it can't hold `things`
:+ 1b. an LLM plus a harness we write that holds `things` and acts for it (see 2)
  :+ 1b-i. harness and tenants run on my desktop under `wrangler dev`, for now
    + 1b-i-a. I want to learn the CF platform
    + 1b-i-b. CF is the only place to get dynamic workers
    . 1b-i-c. the Worker Loader binding works in local dev (cf-fun1)
  ?! 1b-ii. Where does inference run?
    : 1b-ii-a. local Ollama, via its OpenAI-compatible endpoint
      - 1b-ii-a-i. the local model may not be powerful enough
        . 1b-ii-a-i-a. I do have an M5 with 48GB RAM
    : 1b-ii-b. remote, on Workers AI, through the AI binding from `wrangler dev`
      + 1b-ii-b-i. I want to learn the CF platform
    :+ 1b-ii-c. either, behind one interface; start remote
      + 1b-ii-c-i. starting remote lowers the odds of failing because the model isn't powerful enough

?! 2. How do things get from a chat message to something the neighbor can call?

. 2-i. no chat-completions API has `things`/`about`; this is our extension
:+ 2a. the harness strips `things`, keeps the stubs, and shows the model only `about`
: 2b. expose each thing as a tool, one per method in its `about.ts`
:+ 2c. code mode: the model writes JS against the `about.ts` types; the harness runs it in a tenant (see 5)
  + 2c-i. it's the story: the neighbor writes code to compose the things and loads it into a dynamic worker endowed with them
? 2d. What does the model see when a call fails or the thing is revoked?

? 3. What is the fence?

: 3a. a web page: chat with the neighbor, plus a password field for pasting a webkey
?~ 3b. Which server resolves a webkey to an `RpcTarget` stub?
  :- 3b-i. cf-fun1's sturdyref registry (`resolveSturdyref`)
    - 3b-i-a. cf-fun1 got wrapped around the axle on this
  :~+ 3b-ii. diigo-cache's model: the webkey is the DO's id; a Cap'n Web `/api` trades it in-band for a facet (`CacheApi.cache(key)` → `CacheCap`)
    + 3b-ii-a. no registry: the DO namespace is the table; malformed and unknown keys get the same refusal, and the key is never logged
    + 3b-ii-b. stubs die with the session, but the key is data: the harness keeps the key and trades it again each session
    + 3b-ii-c. attenuation is a facet class (`FillCap` under `CacheCap`; `viewer()`)
    : 3b-ii-d. (postponed; see 8b-ii) the governed mower is its own DO, wired to the raw mower's key and the limit; its id is the webkey I hand over
      - 3b-ii-d-i. diigo-cache has no revocation: a DO id can't be withdrawn
        + 3b-ii-d-ii. the governor DO can drop its wiring, so its key gets "no such mower" (see 4)
?! 3c. Why a password field rather than a link?
  :+ 3c-a. so the password manager saves the key under a petname I choose (diigo-cache's `pmSave`), and the key stays out of URLs, history, and logs

? 4. How does "for the weekend" end?

. 4-i. postponed to a later scope, where we can inject a clock and such
: 4a. the governor checks a deadline (the clock is passed in, not ambient)
: 4b. I tell the governor to drop its wiring, by hand (see 3b-ii-d)
: 4c. the fence mints a webkey with an expiry

? 5. How does the neighbor make tenants?

: 5a. Worker Loader (dynamic workers), with the things passed in as `env`
  . 5a-i. `env` takes only structured-clonable values and service bindings, so a Cap'n Web stub can't go in directly
    : 5a-i-a. wrap each thing in a `WorkerEntrypoint` handed over as a `ctx.exports` loopback binding; it holds the stub and forwards calls
    : 5a-i-a-i. the loopback carries the thing's webkey in `props` (a string clones fine) and trades it for the DO stub on each call (see 3b-ii)
      + 5a-i-a-ii. the tenant never sees the key: `props` stay on the host side
      + 5a-i-a-iii. no stub outlives a request, so 5c's cross-request limit doesn't bite
  + 5a-ii. `globalOutbound: null` confines the tenant to what it's given
? 5b. Who sees or approves tenant code before it runs?
? 5c. Does a tenant outlive the neighbor's turn, e.g. one that mows on a schedule?

?! 6. What is a Mower, concretely?

:+ 6a. a DO per mower, made by a `Mower` DO factory (the story says so)
:+ 6b. one shared yard: an `RpcTarget` in my browser that renders the table; each mower is given the yard and calls `yard.cut(x, y)`
:+ 6c. one cell is 10 yards; the yard is 40×40 cells, so "100 yards" is 10 cells from where the mower started
  . 6c-i. the governor's limit is a square, max(|dx|, |dy|) ≤ 10: a fence
:+ 6d. Logo turtle moves: `start()` puts the mower down facing north and cuts its cell; `push()` is FORWARD 1 and cuts the cell it enters; `turn()` is RIGHT 90; `stop()` parks it
  . 6d-i. pushing past the yard's edge wraps to the other side, so an ungoverned mower really does go all over the place
    . 6d-ii. the governor counts its own unwrapped moves (see 7a), so wrapping can't sneak a mower past the fence

?! 7. How does the governor know how far the mower has gone?

:+ 7a. it proxies every Mower method and tracks position itself from `push()`/`turn()`
  - 7a-i. drifts if anyone else holds the raw mower
    + 7a-ii. nobody else does; the governor is the only one I hand out
?! 7b. What does the governor do at the limit: refuse silently, throw, or report?
  :+ 7b-i. it refuses that `push()` by throwing "fence: 10 cells from start"; the mower stays put, and `turn()` always works
    + 7b-ii. the tenant can catch it and turn around; the model sees why its code stopped (see 2d)
  :- 7b-iii. refuse silently
    - 7b-iii-a. a tipsy tenant would think it mowed cells it didn't

?~ 8. Where does the js playground run, and what can it reach?

: 8a. in the fence page; it writes the governor, which runs in its own dynamic worker holding the raw mower
:~+ 8b. in my browser, wrapping the stub on the client
  - 8b-i. the governor then lives only as long as my tab is open
    + 8b-i-a. a durable governor is out of scope for now
  . 8b-ii. then the neighbor holds a Cap'n Web stub to my browser, not a webkey, so 3b-ii-d (governor DO) and 5a-i-a-i (key in `props`) wait with the durable governor

?! 9. Who watches the yard table, and how does it update?

:+ 9a. I do, in my browser: the yard is an `RpcTarget` there (see 6b), and it repaints a cell on each `cut`
  - 9a-i. the mowers' stub to the yard dies when I close the tab
    + 9a-ii. fine for a proof of concept: keep the tab open (see 10-i)

?! 10. What is this for: a demo, a talk, or a product?

. 10-i. the answer decides how much of the above can be faked
:+ 10a. a proof of concept, for now
:+ 10b. toward a pattern for all sorts of rich cooperation with little vulnerability
  + 10b-i. an alternative to OpenClaw, where 12–17% of ClawHub skills were found malicious in Feb 2026 (sources below)

Sources for 10b-i:

"A security audit of 2,857 skills on ClawHub has found 341 malicious skills across multiple campaigns" (about 12%, from Koi Security) — Researchers Find 341 Malicious ClawHub Skills, The Hacker News, Feb 2026

"Around 17% of OpenClaw skills analyzed in the first week of February 2026 exhibit malicious behavior." — Helpful Skills or Hidden Payloads? Bitdefender Labs Dives Deep into the OpenClaw Malicious Skill Trap, Bitdefender, Feb 2026