feat(kinetic): the clutch — no couple, no move - #92
Open
mayoalexander wants to merge 3 commits into
Open
mayoalexander wants to merge 3 commits into
mayoalexander wants to merge 3 commits into
Conversation
KINETICS — THE CLUTCH (#184906) build order steps 1, 3 and 5. Until now anything that could reach `iris camera` / `iris obs` could drive the hardware: raw kinetic energy with no mass identity. Now a body moves for an agent only when a couple on THAT node says it may. A couple binds four things — a sealed agent hash, the node, the body (`camera:obsbot-tiny`, or `camera:*`), and the verbs blessed — plus the terms: a per-act ceiling, human confirmation, an expiry. Editing the agent changes its hash, so the couple stops matching by itself; that is the audit property minting (#184905) exists for, available before Mint issues hashes. - kinetic-couple.ts — pure: the record, matching, and `decide()`, which fails CLOSED. A malformed body, an unreadable expiry, a corrupt couples file and an unsealed caller are all refusals, never fall-throughs. 23 tests, every one mutation-checked (the first draft let a couples file carrying `agent: """ match a caller with no identity — both sides now checked). - kinetic-store.ts — couples live on the node that holds the body, because #184906's offline rule requires it: a cloud copy is a report, not permission. - kinetic-guard.ts — one await on the act path; exits 5 on a refusal and names the fix. - platform-kinetic.ts — iris kinetic seal | couple add/list/revoke | check | lock | log. - camera and obs act paths guarded; reads (list, pos, status) left open — looking is not moving. An operator at a terminal is not an agent act: still allowed, recorded as unbound, never booked to an agent that did not do it. `iris kinetic lock` makes a node strict about that too. Verified end to end on a real node key: uncoupled agent refused (exit 5, including through `iris obs record start` before OBS is touched), couple added, same act allowed, unblessed verb refused, wrong body refused, revoke takes effect immediately, act log carries both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9LZmfMCptv6tUqxryDrTx
The first cut guarded two act paths by hand. That left `iris device`, `iris hive run` and anything written next month unguarded — enforcement was opt-in, which is the failure Genesis invariant 4 names. Four gaps, closed: 1 ONE CHOKE POINT. kinetic-bodies.ts holds the route table and one middleware consults it before any handler runs, so a new act path is covered by adding a row. `iris hive run` — the epic's named danger, arbitrary shell on another machine — is now refused without a couple, and nothing in hive was touched to do it. Reads route to nothing. The command name comes from yargs' parsed path (a flag's VALUE looks like a word) and the target from raw argv (yargs eats positionals; `hive run studio-mac` arrives as [hive, run], without the machine it drives). 2 SIGNED COUPLES. An issuer signs; every node verifies locally with no network call, so a fleet is coupled centrally without giving up the offline rule. Every granting field is signed — agent, node, body, verbs, budgets, expiry — and the test tampers with each one. A node's own revoke outlives a re-import, because a revoked couple is still correctly signed. 3 BUDGETS THAT BITE. Per-act, per-run, per-UTC-day and acts-per-day, read from the act log. The per-act cap alone never fires on a night of cheap acts. Costs are DECLARED (IRIS_ACT_CENTS or --cents), never guessed, and only allowed acts count — being refused all day cannot exhaust a budget. Both gates share an act_id so one act is never counted twice. 4 VERBS ARE CHECKED AT COUPLING TIME against what the class declares, so a typo is a refusal now rather than a couple that silently never matches. A class IRIS does not model is left alone. Measured, not theorised: three 60c acts under a $1.50 day cap booked $0.00, because the choke point recorded refusals only and those paths have no act-path call site. Allows are recorded now. Also found by running it: `kinetic check` never passed the signature verifier, so it called a perfectly good issued couple unverifiable — the check must ask what the act path asks. 47 pure tests, 13 mutations applied to the guard's own lines; 12 were caught, and the one that was not is marked in the code as not load bearing rather than left to look guarded. Full cmd suite 1592 pass. Verified end to end across two node identities: issue on one machine, trust, import, act, revoke, re-import refused, untrust kills it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9LZmfMCptv6tUqxryDrTx
# Conflicts: # packages/opencode/capabilities.json
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.
KINETICS — THE CLUTCH (#184906), build order steps 1, 3 and 5. Epic for this build: Atlas item #186315.
The problem, as it stands on
mainiris camera,iris obsandiris devicedrive real hardware for whoever runs them, andiris hive runwill dispatch a shell command to any node. There is no identity anywhere in that path. The epic's own words: arbitrary shell is raw kinetic energy with no mass identity — dangerous and unauditable.What this adds
A body moves for an agent only when a couple on that node says it may. A couple binds four things, plus the terms:
camera:obsbot-tiny,obs:studio, orcamera:*for a classEditing the agent changes its hash, so the couple stops matching by itself. That is the audit property minting (#184905) is for, working before Mint issues hashes — and when it does, the couple carries Mint's hash instead with no change to the record.
Files
kinetic-couple.ts— pure. The record, matching, anddecide(). Fails closed: a malformed body, an unreadable expiry, an unsealed caller, a corrupt couples file and a couple for another node are all refusals, never fall-throughs.kinetic-store.ts— couples live on the node that holds the body. Forced by the epic's offline rule ("sealed hash + couple + device on the same node"): a cloud copy is a report, not permission. Node key is the hostname stem, because macOS rewrites the hostname on mDNS collisions and a couple keyed to a mutating string would silently stop matching.kinetic-guard.ts— oneawaiton the act path. Exit 5 on refusal, and the message names the exactcouple addthat would fix it.platform-kinetic.ts—iris kinetic seal | couple add/list/revoke | check | lock | log.list,posandstatusleft open: looking is not moving.scaffold/how-to/kinetics.md— build order step 6. Capability index rebuilt, so agents can discover all 9 entries.An operator is not an agent act
Your own
iris camera leftkeeps working and is recorded as unbound — never booked to an agent that did not do it.iris kinetic lockmakes a node require a couple from everyone, for a studio or an unattended machine.Verification
23 tests, and every one mutation-checked — each assertion was proven to fail when the line it guards is broken. That caught a real hole in the first draft: a couples file carrying
agent: ""matched a caller with no identity, the malformed record and the unsealed caller cancelling into an allow. Both sides are checked now.End to end against the real CLI on a real node key:
kinetic check … moveiris obs record startcouple addcouple revokeNot in this build
Booking acts to the Mint ledger as
expense:device:*(step 4), and the motion→mass return (#185317). Acts are recorded to~/.iris/kinetic/acts.jsonlon the node meanwhile.🤖 Generated with Claude Code
https://claude.ai/code/session_01K9LZmfMCptv6tUqxryDrTx
Update — the four gaps are closed (commit
70c2c960e)The first commit guarded two act paths by hand, which left
iris device,iris hive runand anything written next month unguarded. Enforcement was opt-in. Now:1 · One choke point, not two call sites
kinetic-bodies.tsholds the route table; one middleware consults it before any handler runs. A new act path is guarded by adding a row, not by remembering to call a function.iris hive run— the epic's named danger, arbitrary shell on another machine — is refused without a couple, and nothing in hive was touched to make that true.camera list,obs status,hive nodes, and adevice cleandry run).--log-level DEBUG camera leftwould otherwise read as the command "debug"), and the target from raw argv (yargs eats named positionals, sohive run studio-macarrives as["hive","run"]— without the machine it drives). Both are pinned by tests; a global flag can no longer hide an act.iris deviceturned out to be this machine's disk, not phones — its row was rewritten against the command's real subcommands rather than the verbs I had assumed.2 · Signed couples, so a fleet is coupled centrally without losing offline
An issuer signs; every node verifies locally, with no network call.
iris kinetic issuer new | trust | list | untrust,couple export | import.Every field that grants anything is signed — agent, node, body, verbs, budgets, expiry — and the test tampers with each one individually. The label is not signed, because renaming it grants nothing. The signature must verify against the issuer it names, not merely some key the node trusts (that one took two attempts to pin: the obvious test passed against a broken implementation).
Three ways to withdraw, increasing in size:
couple revoke(one couple, and that "no" survives a re-import — a revoked couple is still correctly signed, so the signature cannot be what withdraws it),issuer untrust(everything that issuer ever signed), and a short--expires(an unreachable node forgets by itself).3 · Budgets that bite
Per-act, per-run, per-UTC-day, and acts-per-day — read from the act log. Costs are declared (
IRIS_ACT_CENTS, or--centswhere a command defines it), never guessed, and only allowed acts count, so being refused all day cannot exhaust a budget.iris kinetic spend <couple>shows each window against its ceiling.Found by running it, not by reasoning: three 60c acts under a $1.50 day cap booked $0.00, because the choke point recorded refusals only and those paths have no act-path call site — the ceilings were unreachable for exactly the commands only the middleware covers. Allows are recorded now, and both gates share an
act_idso one act is never counted twice.4 · Verbs checked at coupling time
unknownVerbsrefuses a verb the class does not declare, so a typo is a refusal now instead of a couple that silently never matches. A class IRIS does not model yet is left alone rather than second-guessed.iris kinetic bodieslists what exists.Verification
47 pure tests. 13 mutations applied to the guard's own lines: 12 caught; the one that survived is redundant with another check and is now marked in the code as not load bearing, rather than left looking guarded. Full
src/cli/cmdsuite: 1592 pass, 0 fail.End to end across two node identities: issue on the studio machine → laptop refuses it (untrusted) → trust the issuer → import → act allowed → revoke → re-import refused → untrust kills it. Plus per-node enforcement (
node:studio-macallows that machine and refuses another) and the day ceiling stopping the third act of a run.Also caught by running it:
kinetic checknever passed the signature verifier, so it reported a perfectly good issued couple as unverifiable — a check must ask exactly what the act path asks, or it answers about a different system.