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>
This commit is contained in:
dckcandClaude Opus 5.5 committed 2026-10-09 01:00:15 -05:00
1 parent 8c110098ff
commit efb54ad4cc
2 files changed
+167 -1

No files matched your search

+166
View File
@@ -0,0 +1,166 @@
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...
```js
client.chat.completions.create({
model: 'llama3.1:8b',
messages: [{ role: 'user', content: 'Tell me a story' }],
stream: true,
});
```
but with a twist:
```js
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](https://thehackernews.com/2026/02/researchers-find-341-malicious-clawhub.html), 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](https://www.bitdefender.com/en-us/blog/labs/helpful-skills-or-hidden-payloads-bitdefender-labs-dives-deep-into-the-openclaw-malicious-skill-trap), Bitdefender, Feb 2026
+1 -1
View File
@@ -1,6 +1,6 @@
# Lend the mower
The journey from [Good Fences Make Good Agents](../../../a2030.md): I lend
The journey from [Good Fences Make Good Agents](../../docs/good-fences-make-good-agents.md): I lend
my neighbor a governed mower; the neighbor's tenant, a little tipsy,
pushes it until the fence stops it. The design decisions behind each actor are in that
doc's Design IBIS (numbers in parentheses below).