Venom
RFC-0005venom-spec

Caller identity and capability grants

Field Value
Status Accepted
Authors commixon (commixon)
Created 2026-11-06
Accepted 2026-11-06, approved by the maintainer
Linear COM-204; section 3.7 daemon exception COM-342
Supersedes None
Superseded by None

History

  • 2026-11-08 (COM-342, normative, compatible addition; explicitly accepted by the maintainer): section 3.7 lists one daemon that may present another tool’s owner token, venom serve to venomx, under fixed limits. Every other caller behaves as before.

  • 2026-11-08 (venom-org/venom#119, normative; explicitly accepted by the maintainer on 2026-11-09): section 3.8 refuses the owner token to a process below another tool’s managed tree. Until now a venomx pane could read venom’s owner file, and an agent of venom could read venomx’s. Sections 3.4 and 3.7 and the threat model follow. A human in a pane loses @owner and uses an issued token instead.

  • 2026-11-08 (venom-org/venom#122, normative; explicitly accepted by the maintainer on 2026-11-09): grant.issue with pid makes a new kind of principal, process, bound to that process with no token, and a daemon keeps these across its restart. Section 3.8’s control pane uses it instead of an unbound token. Sections 2, 3.2, 3.3, 3.4, 3.8, 9, 10 and 12 follow. Tokens and every other caller behave as before.

  • 2026-11-08 (venom-spec#92, normative, compatible addition; explicitly accepted by the maintainer on 2026-11-09): section 3.8 says a launcher refuses below a manager, which RFC-0009 section 2 defines. The alternatives record why the marker stays a deletable file.

  • 2026-11-09 (venom-spec#113, normative, compatible addition, in the PR that accepts RFC-0020 and flagged there for the maintainer): for venom-mem only, RFC-0020 section 4 adds durable clients: token principals with read and write at most, issued only with admin, stored by the product, valid across its restarts (section 3.6), with no forced expiry (6.2), and kept out of “dies with the daemon” (section 2). Every other product and grant.issue behave as before.

  • 2026-11-13 (venom-spec#174, normative; explicitly accepted by the maintainer on 2026-11-13): new section 11.1, grants.isolation. Each product picks its default: off for venomx, on for every other product. With it off, unknown and interactive panes hold read, write and spawn on *, so the user’s own scripts drive the daemon with nothing set, as in tmux. A process below another tool’s manager, issued tokens and spawned children are capped as before. With it on, nothing changes. This answers Open questions 2 and 6 for venomx. Sections 2, 6.2, 11, 12 and Agent-contract impact follow, and RFC-0001 section 7 gains the exception.

Summary

This RFC defines who is calling a venom daemon and what that caller may do. It covers:

  • Principals: the owner, panes, agents, bearer tokens and unknown.
  • Identity resolution: on Linux, SO_PEERPIDFD/SO_PEERCRED plus a walk of the peer’s process ancestry; a per-daemon token in hello.auth; an owner token file for the human’s own clients.
  • Rights: the meaning of the seven grant names that RFC-0004 already fixes (none, read, write, respond, files, spawn, admin).
  • Resource scopes, how grants are issued by attenuation, and how they expire and are revoked.
  • Verbs and audit: shared whoami, grant.* and audit verbs, the audit log, and a threat model.

It is implemented once, in venom-std’s proto module (COM-209). Every product then supplies two things: its resource containment rules and its per-resource checks inside handlers. Section 13 is the checklist that COM-209’s tests follow.

Motivation

RFC-0001 section 7, RFC-0002 section 2.4, RFC-0003 section 3, RFC-0004 section 2 and RFC-0006 section 9 already require the same things. The daemon authorizes before it runs a handler. Unknown callers get at most read-only rights. Error class denied (exit 4) means a missing grant. hello.auth carries credentials. Each of those RFCs leaves to this one the question of who a caller is and which resources a grant covers. Until this RFC exists:

  • COM-209 (proto identity and grant checks) cannot start, and neither can venom-echo conformance item AC-09 (COM-216).
  • venomx cannot tell a watcher from a writer for anyone but unknown (RFC-0006 section 9). The P1 goal, “Claude Code agent teams run inside venomx”, needs agents that can split and drive their own panes and nothing else.
  • venom (P2) cannot give the agents it spawns scoped credentials (the t3code lesson in docs/ecosystem.md).

Without it, every product would either trust every same-UID caller fully, which RFC-0001 forbids, or invent its own token scheme.

Design

1. Overview by example

The human starts venomx and opens a pane from the TUI. venomx registers the pane as a principal and puts its credentials into the pane’s environment:

env | grep '^VENOMX_'
VENOMX_SOCKET=/run/user/1000/venom/venomx.sock
VENOMX_PRINCIPAL=g3
VENOMX_PANE=p3
VENOMX_TOKEN=vnt1.venomx.g3.Q2hhbmdlIG1lOiB0aGlzIGlzIG5vdCBhIHJlYWwgdG9rZW4

(The token is illustrative.) An agent started in that pane asks who it is:

$ venomx whoami --json
{"principal":"g3","kind":"pane","pane":"p3","parent":"owner","grants":[{"right":"read","scope":"*"},{"right":"write","scope":"self"},{"right":"spawn","scope":"self"}],"expires":null}

It can split its own pane. A split pane belongs to the caller’s self scope, and the new pane gets the caller’s grants (section 6.2):

$ venomx pane split --pane p3 --json
{"pane":"p4","principal":"g4"}

It cannot type into a pane the human opened elsewhere, and it cannot change config:

$ venomx pane send --pane p1 --text 'rm -rf ~' --json
{"error":{"code":"denied.write","message":"principal g3 lacks 'write' on pane:p1","data":{"right":"write","resource":"pane:p1","principal":"g3"}}}
$ echo $?
4
$ venomx config set --key appearance.theme --value '"light"' --json
{"error":{"code":"denied.admin","message":"principal g3 lacks 'admin'","data":{"right":"admin","principal":"g3"}}}
$ echo $?
4

(pane.split and pane.send are illustrative venomx verbs.) Both denials are appended to the audit log (section 10):

{"ts":"2026-11-06T12:00:03.000Z","source":"venomx/01234567-89ab-4cde-8fab-0123456789ab","event":"denied","principal":"g3","peer":{"pid":48211,"exe":"/usr/bin/node"},"verb":"pane.send","right":"write","resource":"pane:p1","code":"denied.write"}

A process that venomx does not know about, such as a shell in another terminal emulator, is unknown. By default it can read but not write:

$ venomx whoami --json
{"principal":"unknown","kind":"unknown","parent":null,"grants":[{"right":"read","scope":"*"}],"expires":null}

2. Principals

A principal is an entry in one daemon’s in-memory registry:

Member Type Meaning
principal string ID. owner, unknown, or g<N> allocated from a counter that only increases for the daemon’s lifetime (never reused within one source, RFC-0003)
kind string owner, unknown, pane, agent, token or process
parent string or null The principal that issued it. null only for owner and unknown
grants array Grant entries {"right":…,"scope":…} (sections 4–5), as issued
expires string or null RFC 3339 UTC time after which it is invalid, or null
root (internal) Optional bound process (pid, starttime) (section 3.3). Not exposed on the wire.
token_hash (internal) SHA-256 of the token secret, if a token was issued. Never exposed.
label string Optional human label, ≤ 64 bytes, no control characters
pane string Only for kind = pane: the pane ID

Principal IDs match ^(owner|unknown|g[1-9][0-9]*)$, so they never contain a colon, and principal:<id> is a valid RFC-0003 resource string.

  • owner is the human who runs the daemon. It holds every right on every scope (admin on *, section 4). It is fixed and cannot be revoked.
  • unknown is any caller that resolves to no other principal. Its grants come from config grants.unknown, default read on * (section 11). It can never hold more than read (RFC-0001 section 7); config that tries to give it more MUST fail config check. The one exception is isolation off (section 11.1), which is a switch, not a grants.unknown list.
  • pane is created by a product for a process it starts in a pane (venomx). It is bound to the pane’s root process.
  • agent is created by an orchestrator (venom) for an agent it spawns. It is bound to the agent’s root process when the orchestrator knows that PID.
  • token is created by grant.issue (section 9). It is a bearer credential that is not bound to any process.
  • process is created by grant.issue with pid (section 3.3). It is bound to that process and has no token at all, so being the peer itself is the only way to it.

Principals form a tree rooted at owner through parent. The descendants of P are its children, their children, and so on.

The registry lives in memory and dies with the daemon, except process principals (3.3). Each daemon has its own registry. A token is valid only on the daemon (source) that issued it.

3. Identity resolution

Identity is resolved once per connection, at accept, before the first byte is read. RFC-0002 section 2.4 already reads peer credentials at that point. The result is cached on the connection as the resolved principal plus a floor (3.4). Authorization (section 7) is still evaluated on every request, against the current registry, so a revocation takes effect at once.

3.1 Peer process

Platform Mechanism
Linux ≥ 6.5 getsockopt(SO_PEERPIDFD) gives a pidfd for the peer, and SO_PEERCRED gives its PID and UID. The pidfd is used in 3.2 to detect PID reuse.
Linux < 6.5 SO_PEERCRED only. Reuse detection is weaker (3.2). doctor MUST report this as warn.
macOS, BSD getpeereid gives the UID. LOCAL_PEERPID (macOS) gives the PID where the platform has it. Without a PID, the ancestry walk is skipped and the floor is OUTSIDE.

The peer UID check from RFC-0002 section 2.4 comes first and is unchanged: a peer with a different UID is disconnected without a reply. A peer PID of 0 (the peer is in a PID namespace the daemon cannot see) means there is no ancestry, and the floor is OUTSIDE.

3.2 Ancestry walk

Starting at the peer PID, the daemon follows parent PIDs. On Linux it reads fields 4 (ppid) and 22 (starttime) of /proc/<pid>/stat. On macOS it uses proc_pidinfo(PROC_PIDTBSDINFO) (pbi_ppid, pbi_start_tvsec and pbi_start_tvusec). At each step:

walk(peer):
  pid ← peer pid; depth ← 0
  loop:
    st ← starttime(pid)                    # failure (process gone) → floor ORPHAN
    if some live principal P has root == (pid, st),
       and P is not kind process or depth == 0 → floor P; stop
    if pid == own daemon pid                → floor ORPHAN; stop
    if pid == 1 or pid == 0                 → floor OUTSIDE; stop
    depth ← depth + 1; if depth > 64        → floor ORPHAN; stop
    pid ← ppid(pid)
  if the pidfd from 3.1 shows the peer has exited → floor ORPHAN
  • A process principal (3.3) matches only the peer itself, never an entry above it. Every other bound principal covers its subtree.
  • The nearest bound principal wins. A pane principal’s subtree can contain more specific bound principals (an agent inside a pane, a sub-agent inside that agent).
  • A root is matched on (pid, starttime), never on PID alone, so a dead pane’s PID reused by an unrelated process does not inherit the pane’s grants.
  • Subreaper. On Linux, a daemon that spawns processes (venomx, venom) MUST call prctl(PR_SET_CHILD_SUBREAPER, 1) at serve. A process that double-forks out of its pane is then reparented to the daemon instead of PID 1. Its walk reaches the daemon’s own PID and gives ORPHAN, so it does not look like an unrelated outside process. macOS has no subreaper, so there an escaped process looks OUTSIDE (section 12, T6).
  • Fail closed. If any read fails part-way through the walk, the result is ORPHAN, which carries at most unknown’s grants.
  • After the walk, if the pidfd from 3.1 reports that the peer has exited, the floor is ORPHAN. Without a pidfd, the daemon re-reads the peer’s starttime and uses ORPHAN if it changed.

3.3 Binding a principal to a process

A principal is bound when the registry records its root (pid, starttime):

  • venomx binds each pane principal to the process it forks for the pane. It records the starttime after fork and before exec. Reading /proc/<child>/stat from the parent is enough.
  • An orchestrator that starts an agent through another daemon learns the PID from that daemon’s spawn result. For example, venom calls venomx pane.create, and the result includes pid. The orchestrator MUST bind its own agent principal to that PID when the PID is visible to it, and SHOULD otherwise. A bound principal cannot be borrowed by processes outside its subtree (3.4).
  • grant.issue creates unbound token principals (bearer tokens), unless it is given pid.
  • grant.issue with pid creates a process principal, for a process someone else started and only learned the PID of. For example, a TUI outside venomx calls window.create, and the result includes pid.

A bound principal is revoked automatically when its root process exits (for a pane, when the pane is destroyed).

process principals.

# Outside every tree, as the owner. No token comes back.
$ venom grant issue --pid=48213 --namespace=person --label="venom control pane" --json \
    --grants='[{"right":"read","scope":"*"},{"right":"spawn","scope":"*"}]'
{"principal":"g9","expires":null}
# As process 48213 itself, with nothing set. Its children are `unknown`.
$ venom whoami --json
{"principal":"g9","kind":"process","parent":"owner","grants":[…],"expires":null,"label":"venom control pane","namespace":"person"}
  • Who may bind: owner, a principal with admin, or a process principal. Anyone else gets denied.bind, before the PID is looked at. spawn is not enough: an agent with spawn could otherwise make a principal with no expiry that survives a restart, which outlives everything else it can issue (6.2). The grants are checked as in 6.2. A process principal may bind so that a control pane can give the panes it opens what it has, and an agent it starts is kind agent, so that agent may not.
  • Which process: the daemon reads starttime when it handles the call. A PID that doesn’t exist is not_found.process; the daemon’s own is invalid_params.pid; a process that is already some live principal’s root is conflict.process_bound, so that resolve never has to choose. A process with a manager’s marker (3.8) is conflict.process_manager. An agent view takes its marker only after it is bound, to run its own shells, so a restored entry isn’t checked for one.
  • Waiting. A connection’s principal is resolved at accept (section 3), and the process may connect before it is bound. A process that expects a binding has to reconnect until whoami shows it. venom’s TUI does this for at most five seconds, and then runs as unknown and says so. The wait is not a credential.
  • A restart. No one is left to bind again when a daemon restarts: the outside process has long since become venomx attach. So the daemon keeps these in the bound file, <tool>.bound next to the socket (the suffix rule of 3.7, mode 0600, O_NOFOLLOW, replaced by rename), with each one’s (pid, starttime), grants, label, namespace, expiry and parent. It survives a clean stop as well as a crash. At serve, before the socket exists, the daemon restores an entry if its process is alive with that starttime and its parent is owner or a restored entry, under a new ID (2). Principals with a cap from their creator’s floor (6.1) and any holding admin are not kept. The file holds no secret. Other principals still die with the registry (Open question 3). A restored one is not an issue by the owner, because anyone with the UID can write the file (T16), so it says so: audit event restored with the process’s (pid, start) and exe (section 10), restored: true in whoami and grant.list, and doctor lists the process principals as grants.process_principals, to the owner or an admin, who see the whole tree in grant.list too.

The process alone. A process principal is the rights of that one process, which is the difference from pane and agent. Children get nothing from it, and resolve as if it weren’t there: unknown, with whatever manager is above them still refusing the owner token, unless the process itself execs venom: the pid and starttime stay, so exec keeps the rights, and fork-then-exec is unknown. This is what makes it safe to bind a person’s client that also runs other code. venom’s agent view opens a shell or a take-over as its own child, and a model’s Bash runs there, so these must not inherit a person’s rights. It also limits a wrong binding: binding a PID changes what that process resolves to and nothing below it, so a human’s shell keeps @owner in the commands it runs.

3.4 Combining floor and token

The floor F is the outcome of the walk: a bound principal, ORPHAN or OUTSIDE. The optional token T comes from hello.auth (3.5). They combine like this:

Floor No token Token T (not owner) Owner token
Bound principal P P T, capped by P (6.1). If T is bound, its root MUST be on the peer’s ancestry chain, otherwise denied.token (binding) denied.token (owner_context)
ORPHAN unknown T, capped by unknown. A bound T fails binding (its root is not on the chain). denied.token (owner_context)
OUTSIDE unknown T. A bound T fails binding. owner, or denied.token (owner_context) below another manager (3.8)

To check binding, the walk continues past the nearest bound principal to its stop condition and records the whole chain. Only the nearest bound principal becomes the floor. The connection’s resolved principal is T if a token was accepted, otherwise the floor (unknown for ORPHAN and OUTSIDE). The floor stays on the connection as a cap (6.1).

Consequences, which the conformance tests check:

  • A token stolen from another process (for example by reading /proc/<pid>/environ) gives no extra rights to a process inside a managed tree. Its effective grants are capped by the thief’s own floor.
  • A bound token is useless outside its subtree, and a process principal has no token at all (3.3). Process-bound credentials are the default in v1. Bearer tokens exist only for callers with no usable ancestry: sandboxes in another PID namespace, macOS without LOCAL_PEERPID, and the future remote transport.
  • The owner token works only from outside every managed tree, whichever tool manages it (3.8). An agent in a pane cannot become owner even if it reads the owner file.

3.5 hello.auth

RFC-0002 section 6.1 left auth opaque. This RFC defines it as an object with one optional member:

{"id":1,"verb":"hello","params":{"proto":[1],"client":{"name":"venomx","version":"0.1.0"},"auth":{"token":"vnt1.venomx.g3.Q2hhbmdlIG1lOiB0aGlzIGlzIG5vdCBhIHJlYWwgdG9rZW4"}}}

The hello result gains an optional member principal. RFC-0002 section 6.5 lets results gain optional members compatibly. principal holds the same object that whoami returns:

{"id":1,"ok":{"proto":1,"server":{"name":"venomx","version":"0.1.0"},"caps":["cancel"],"max_frame":1048576,"principal":{"principal":"g3","kind":"pane","pane":"p3","parent":"owner","grants":[{"right":"read","scope":"*"},{"right":"write","scope":"self"},{"right":"spawn","scope":"self"}],"expires":null}}}

If the token is malformed, unknown, expired or revoked, or fails the binding or owner-context rules in 3.4, hello MUST fail with denied.token (exit 4), and the server closes the connection. data.reason is one of invalid, expired, revoked, binding or owner_context. The daemon MUST NOT quietly fall back to unknown: an agent with a bad token has to find out. On a binary connection the error goes in an ERROR frame on stream 0.

A connection that skips hello (the implicit hello in RFC-0002 section 6.4) has no token, so it gets the floor alone.

3.6 Tokens and the environment

Token syntax:

vnt1.<tool>.<principal>.<secret>

<tool> is the RFC-0002 tool name and <principal> is the principal ID. <secret> is 32 bytes from the OS CSPRNG, encoded as base64url without padding (43 characters). The prefix lets a daemon reject another tool’s token without looking anything up, and lets a secret scanner recognise leaked tokens. The daemon stores only SHA-256(secret) and compares it in constant time. Tokens MUST NOT appear in logs, audit records, events, doctor output or error data.

Environment variables follow RFC-0002’s <TOOL>_SOCKET pattern, because one process can hold credentials for several daemons:

Variable Set by Meaning
<TOOL>_TOKEN The daemon that spawned the process (or the human) Token presented in hello.auth by the generated CLI and MCP adapter. The special value @owner means “read the owner token file” (3.7).
<TOOL>_PRINCIPAL The spawning daemon Informational. Never trusted, never sent.
VENOMX_PANE venomx Informational pane ID, for scripts. Never trusted.

For example VENOMX_TOKEN, VENOM_TOKEN and VENOM_MEM_TOKEN. tuios uses a single VENOM_TOKEN and VENOM_PANE_ID (COM-204’s starting point). Venom has several daemons, so the names carry the tool, and VENOM_TOKEN is just the venom orchestrator’s variable.

The generated CLI and the mcp adapter (RFC-0004) MUST read <TOOL>_TOKEN and present it in hello.auth. The MCP adapter runs as a child of the agent that launched it, so its floor and token are the agent’s own. That is how RFC-0001’s “the adapter MUST preserve the upstream caller’s rights” is met. The adapter MUST NOT read the owner file unless <TOOL>_TOKEN=@owner is set.

A spawning daemon MUST remove from the child’s environment every *_TOKEN variable that the child would inherit from the daemon’s own environment or from the requesting client. It then adds its own token. A pane MUST NOT inherit the token of whoever created it, because the pane gets its own principal. The exception is a token passed explicitly in the spawn verb’s environment parameter, such as venom handing its VENOM_TOKEN to an agent pane (6.3). Such a token is kept, and it is still subject to the binding and floor rules.

3.7 Owner token

At serve, after taking the lock (RFC-0002 section 2.5, step 2), the daemon writes a fresh vnt1.<tool>.owner.<secret> to the owner file <tool>.owner, next to the socket and lock. It uses the same suffix rule as the lock file: the socket path with .sock replaced by .owner. The file is created with O_CREAT|O_EXCL|O_NOFOLLOW, mode 0600, after unlinking any stale one. On clean shutdown the daemon unlinks it. A new secret is generated on every start.

The owner token is presented by:

  • TUI presentation clients (venomx attach, venom’s TUI). These are always driven by a human. They read the owner file by default. Keybindings are verbs sent through such a client, so they run as owner when the client is outside every managed tree. That keeps RFC-0001’s rule that a keybinding goes through the same authorization as the CLI.

  • The CLI, only when <TOOL>_TOKEN=@owner is set explicitly. An ordinary venomx … from an unmanaged shell is unknown, as RFC-0001 section 7 requires (“not merely by sharing a UID”).

  • venom serve, to venomx only. RFC-0018 section 3 has it post a notification for a request left unanswered. notify.post and its dismissal need write on server:root (RFC-0016 section 6), which no pane may hold, and venomx has no narrower right. The daemon stands in for the human who started it, so:

    • it MUST use the owner token only for one grant.issue per token, on a connection it then closes;
    • that call issues write on server:root, in the namespace venom, with a TTL of at most one hour, and nothing wider;
    • the issued token stays in the daemon’s memory. It MUST NOT be written to disk or a log, or reach a harness, a pane or any other child;
    • inside a managed tree the owner token is refused like any other (3.4), so venom serve started in a pane gets no token and does not post.

    This is a closed list. Another daemon, or a wider token, needs its own amendment.

Inside a managed tree the owner token is refused (3.4, 3.8). A TUI nested in a venomx pane therefore runs with that pane’s rights on venomx, and on any other daemon with unknown’s rights or the token it was given.

3.8 Managed trees of other tools

The walk in 3.2 only knows the daemon’s own principals, so a process another tool manages looks OUTSIDE. Left at that, the owner rule has a hole:

# As an agent of venom. venomx does not know this process, so it is OUTSIDE there.
$ VENOMX_TOKEN=@owner venomx window create --session s0 --argv '["/bin/sh"]' --json
{"pane":"p7","window":"w3","principal":"g5"}
# In p7. venom does not know this process either.
$ VENOM_TOKEN=@owner venom whoami --json
{"principal":"owner","kind":"owner","parent":null,"grants":[{"right":"admin","scope":"*"}],"expires":null}

Rule. A daemon MUST refuse its owner token, with denied.token (owner_context), to a peer that has a manager above it, as well as in the cases of 3.4. Nothing else about the caller changes: with no token it is unknown, and a non-owner token works as for OUTSIDE (3.4). This applies to both directions of the chain above: the agent is refused venomx’s owner token, and a pane’s process is refused venom’s.

Managers. A manager is a process that starts other processes for people or agents:

  • every daemon that sets the subreaper (3.2), which today is venomx and venom;
  • a presentation client that runs shells or harnesses as its own children, which today is venom’s TUI in own mode. It holds the marker while it runs in that mode, so its splits are treated as a pane’s process is. It MUST also set the subreaper before it starts any child. Otherwise a background job whose parent shell exits reparents out of the marked chain. Like a daemon, it waits only for the pids it started.

venom serve MUST set the subreaper like any other daemon that spawns (3.2). Otherwise a double fork leaves an agent’s descendant OUTSIDE on every daemon.

A supervisor of daemons is not a manager, even with a subreaper, and holds no marker. It starts daemons, not processes for people or agents. If it held one, a venom serve it started would be refused venomx’s owner token (3.7). The exception covers only this: a supervisor that also runs a person’s or an agent’s process is a manager.

Marker. While it runs, a manager keeps an empty file manager-<pid>-<starttime>, mode 0600, in the directory of the socket it serves or talks to. A daemon writes it before its owner file and unlinks it on clean shutdown. At accept, the daemon looks up each entry of the peer’s chain except the peer itself, by (pid, starttime), in its own socket’s directory. A marker that can’t be checked, for any reason but its absence, counts as present.

  • The peer does not count. venom serve is a manager and may present venomx’s owner token from outside every tree (3.7). Started in a pane, it has venomx above it and is refused as before.
  • Only a denial follows from a marker, but anyone can cause that denial. A marker left by a crash matches no live process. A forged one is a denial of service: any same-UID process, an agent included, can write manager-<pid>-<starttime> for the human’s shell or for pid 1. The human’s @owner is then refused on every daemon using that directory, with only denied.token. doctor, run with no token, reports a marker above the caller as grants.below_manager (warn). Deleting the file ends the denial.
  • The directory is the scope. Daemons under one XDG_RUNTIME_DIR see each other, and a throwaway daemon on a temp socket, such as a test’s in a real pane, sees only its own. Starting venomx and venom with sockets in different directories therefore turns this rule off between them.
  • It needs no connection between the daemons and no running venomx, so venom stays headless. It is checked once per connection, with one stat per process in the chain.
  • A launcher checks too. A process below a manager can’t use the owner token, so it also MUST NOT start a daemon that would need it (RFC-0009 section 2).

The human’s ways in. A human can always use venom, in one of these ways:

Where How Rights on venom
A terminal outside venomx venom, or VENOM_TOKEN=@owner venom … owner
The control pane that start makes (host mode) Nothing to do: that outside process binds a process principal to the pid window.create returns (3.3). It passes no credential read, write, respond, files, spawn on *; no admin
An agent view that control pane opens Nothing to do: the control pane binds it the same way, under itself The same, and it ends with the control pane
A pane of the human’s own Issue a token outside, then set it there (below) What the token says
A pane with nothing set venom unknown: it reads, and says how to do more

The token shows wherever it is printed, and a pane’s screen can be read by anyone with read, so issue it outside and enter it unseen in the pane. If it did show in a pane, clear that pane’s screen and history.

# In a terminal outside venomx (detach first if venomx is the only one you have):
$ VENOM_TOKEN=@owner venom grant issue --namespace=person --label=my-pane --json \
    --grants='[{"right":"read","scope":"*"},{"right":"write","scope":"*"},{"right":"respond","scope":"*"},{"right":"files","scope":"*"},{"right":"spawn","scope":"*"}]'
{"principal":"g9","token":"vnt1.venom.g9.…","expires":"…"}
# In the pane: paste it at the silent prompt.
$ read -rs VENOM_TOKEN; export VENOM_TOKEN
$ venom

Namespace person. On venom, a token or process principal the owner issues with --namespace=person stands for the human. An agent it starts is issued under owner, with no expiry, after the same coverage check. Without this a control pane’s agents would end with it. Any other token’s expiry still bounds its agents (6.2). Only the owner, an admin or a holder of a person token can give this namespace (RFC-0016 section 5.1). A holder’s child token is still covered by what the holder has.

What the human loses. Deny by default wins here:

  • VENOM_TOKEN=@owner and a nested TUI’s automatic owner rights no longer work in a pane or a split, on any daemon. VENOMX_TOKEN=@owner no longer works in an own-mode split or for anything venom started.
  • No admin in a pane by default. The control pane can’t save settings, run audit or stop the daemon. It says what to do instead: venom settings in a terminal outside venomx. The owner can issue a pane an admin token, and that is a decision to trust everything that can read it.
  • Popups only read. A key table can’t hold a credential (RFC-0016 section 7.4) and a popup is a pane, so the inbox and settings popups connect as unknown, and venom never learns a popup’s pid to bind it. Answering and saving happen in the control pane, or from outside.
  • A token issued for a pane ends. It lasts at most grants.max_token_ttl, 24 hours by default, and dies with the daemon that issued it. The TUI says so, and the human issues another. Agents keep running. A control pane has no token, so this doesn’t apply to it: its principal lasts as long as its process.
  • A shell in a pane can’t configure a daemon, for the same reason an agent can’t.

What it does not stop. The rule looks at the processes above the peer at accept, so it does not hold for a process that has none of them:

  • A process that outlives its manager. Once a daemon stops, cleanly or by kill -9, its surviving children reparent away from it, and its marker no longer matches anything above them. Such a process is owner on the other daemon at once, and on the stopped one after it restarts. The same goes for a process left behind when an own-mode TUI exits.
  • A process with an outside parent. systemd-run --user from an agent starts a process that has no manager above it, and it got owner on both daemons. tmux, ssh and cron-style parents are expected to behave the same, since each hands the work to a parent outside the tree. This has the same cause as reading the owner file from a second login.

Grants are not a sandbox (section 12): stopping these takes an OS sandbox that hides the owner file. Also:

  • An issued token is unbound, so a process below a manager that reads it from /proc/<pid>/environ can use it (T2, T15). A control pane has none to read.
  • The bound file (3.3) can be edited by any same-UID process, which then gets an entry’s rights, short of admin, within seconds, since it can restart the daemon itself (T16).
  • Removing marker files is outside the sanctioned channel, like reading another process’s memory (section 12).
  • macOS has no subreaper, so a process that escapes its tree there also escapes this rule (T6).

4. Rights

RFC-0004 fixes the right names. This RFC fixes what they mean. Every right except none and admin is held on a scope (section 5).

Right Allows Examples
none Nothing. Marks a verb that needs no grant. ping, whoami
read Observing state in scope: queries, events, pane contents, redacted config, skill, doctor events, pane.read, RFC-0006 attach as a watcher
write Changing the product’s own state in scope: creating, changing or destroying its resources, sending input pane.send, pane.kill, RFC-0006 INPUT
respond Answering approvals or questions raised by other principals (an inbox) Approving a tool call for an agent
files Reading or writing host files outside the product’s own state, within path scopes skill.install, file transfer, worktree creation
spawn Starting processes and creating principals (always attenuated, section 6) Running a command in a new pane, starting an agent, grant.issue
admin Everything, on every scope. This includes config.set, config.reload, audit, revoking any principal, and daemon shutdown. owner, a principal the owner explicitly made admin

Implications. These are the only ones, and an implementation MUST compute them exactly:

  1. Any of write, respond, files or spawn on scope S implies read on S. You cannot sensibly change what you cannot see.
  2. admin implies every right on *.
  3. Nothing else implies anything. In particular, write does not imply spawn, and spawn does not imply write.

respond and self-approval. A principal MUST NOT respond to an item raised by itself or by any of its descendants. The check is responder ∉ {raiser} ∪ descendants(raiser), so an agent cannot approve its own request through a sub-agent it spawned. Violations fail with denied.self_approval (exit 4). A product with an inbox MUST record the raising principal on each item, and the raiser MUST come from the connection’s resolved principal, never from request params. The inbox verbs themselves belong to the product’s RFC. This rule applies to all of them.

5. Scopes

A grant entry is {"right": <right>, "scope": <selector>}. Selectors:

Selector Covers
* Every resource of the daemon, including resources created later
<kind>:* Every resource of that kind, for example pane:*
<kind>:<id> That resource and its descendants under the product’s containment relation, including ones created later
self The caller’s own resources (below)
path:<absolute-dir> Only for files: that directory and everything below it

<kind>:<id> uses RFC-0003’s resource syntax. There are no globs other than the two * forms above. Malformed selectors fail with invalid_params.grant_scope.

Containment. Each product declares a containment relation in its own RFC or spec (for venomx, session ⊃ window ⊃ pane). It does so through a parentOf(resource) → ?resource callback that proto calls. A grant on session:s1 covers every window and pane in s1, now and later. Unlike RFC-0003 event selectors, grant scopes do include descendants. RFC-0003 section 3’s rule that a resource selector does not include descendants is about filtering, and it is unchanged.

self. Every product resource records its owning principal: the resolved principal of the request that created it (owner for resources created at startup or from config). self covers:

  • the principal’s own pane (pane member), if it has one;
  • every resource whose owner is the caller or one of its descendants;
  • the descendants of those resources under containment.

self is evaluated when a request is checked, so resources created later are included.

path: scopes. Before checking a path, the daemon resolves it with realpath (symlinks followed, .. removed). The resolved path MUST be the scope directory or lie under it, compared by whole path components, so /home/u/proj does not cover /home/u/project2. A path that does not exist yet is checked through its nearest existing ancestor and then created with O_NOFOLLOW. This is the “destination scope” that RFC-0001 section 4 requires for skill.install. files on * covers every path.

Principals as resources. principal:<id> is a resource with containment along the principal tree. A principal can always read its own record and its descendants’ records. That is what grant.list returns (section 9).

6. Issuance and attenuation

6.1 Effective grants

Grants are stored as issued and intersected at check time, per resource. A connection whose resolved principal is P, with floor F, holds right R on resource X exactly when every principal in the cap set has an issued grant that covers R on X (directly or by the implications in section 4). The cap set is:

  • P, and every ancestor of P up to (not including) owner;
  • F, when F is a bound principal, and every ancestor of F up to owner;
  • unknown, when the floor is ORPHAN.

For the dispatch gate (section 7), “holds R on some scope” means every member of the cap set has some issued grant that gives R (directly or by implication), whatever its scope.

Each principal’s self is resolved against that principal itself. For example, a child issued write self under a parent holding write pane:* may write the resources the child owns, because each is in the child’s self and also in pane:*. Effective rights are recomputed for every request. Narrowing or revoking any member of the cap set therefore narrows the connection immediately, with no need to rewrite records.

6.2 Who can issue what

New principals come from three places:

  1. Spawning a process-backed principal. A product verb that starts a process under a new principal requires spawn on the target scope. A venomx pane split requires spawn on the session or source pane; starting a venom agent requires spawn on the thread. Such a verb SHOULD accept an optional grants parameter: an array of {right, scope} objects in RFC-0004 format 1, with right a string with the seven choices and scope a string. If the parameter is omitted, the child gets the creator’s issued grants unchanged. Like environment inheritance, this can never escalate, because effective grants are intersected with the creator’s (6.1). If the parameter is given, each entry MUST be covered by the creator’s effective grants, otherwise the verb fails with denied.delegation (exit 4, data: {"right":…,"scope":…}) before it spawns anything.
  2. Interactive panes. A pane created by owner with no grants parameter (the usual case: a keybinding in the TUI) gets the config profile grants.interactive. The default is read on * plus write and spawn on self (section 11). A human who starts an agent by hand in that shell gives the agent those rights. The human can make the pane narrower or wider, up to admin if they choose. With isolation off, the profile is the fixed one in section 11.1 instead.
  3. grant.issue (section 9) creates an unbound token principal. It requires spawn. The requested grants MUST be covered by the caller’s effective grants, as in item 1.

A requested entry {R, S} is covered when every member of the caller’s cap set (6.1) has a grant giving R on a scope that contains S. Containment between selectors: * contains everything; <kind>:* contains every <kind>:<id>; <kind>:<id> contains the selectors of its containment descendants; path:A contains path:B when B is A or lies under A; and a principal’s self contains the new child’s self. Anything else is not covered.

Only owner, or a principal that itself holds admin, can issue admin.

Expiry. ttl (seconds, optional) sets expires. A child’s expires is clamped to its parent’s. Unbound token principals MUST have an expiry: the default and maximum are config grants.max_token_ttl, default 86400 seconds (section 11). Bound principals have no expiry by default, because they end with their root process.

6.3 Cross-daemon delegation (confused deputy)

When a daemon acts in another daemon on behalf of a caller, it MUST use a credential no stronger than that caller’s. For example, venom performs a venomx action because an agent asked for it through venom’s verbs. It MUST NOT use its own, broader principal there. Concretely:

  • When venom starts an agent pane through venomx, it passes grants to the venomx spawn verb. venomx registers the pane principal (bound to the pane’s process) and puts its VENOMX_TOKEN into the agent’s environment. venom adds the agent’s VENOM_TOKEN through the spawn verb’s environment parameter.
  • When venom later acts in venomx on the agent’s behalf, it MUST present that agent’s venomx credential. That is either a child token venom issued under the agent’s venomx principal, or the agent’s own venomx principal if the agent called venomx directly. It MUST NOT present venom’s own.
  • A hook (RFC-0001 section 8) runs as a child principal of the principal whose request triggered it, with that principal’s grants.

proto provides issueChild(parent, grants, ttl) so that a daemon can mint the per-caller credential for these cases.

6.4 Revocation

grant.revoke and automatic revocation (a bound root exits, a pane is destroyed, a principal expires) remove the principal and all its descendants. Afterwards:

  • New requests on a connection whose resolved principal is revoked fail with denied.revoked (exit 4). The connection stays open, so the client can report the error.
  • In-flight non-streaming requests that already passed authorization complete.
  • Streams MUST end within one second. Event subscriptions end with denied.event_scope (RFC-0003 section 3). Pane streams (RFC-0006) get ERROR with denied.read on the stream. A writer whose write was revoked but who keeps read is closed with denied.write. It may re-attach as a watcher, because RFC-0006 fixes the role at attach.
  • The audit log records each revocation, with the number of cascaded descendants.

Narrowing a principal’s grants in place is not supported in v1. To narrow, revoke and re-issue.

7. Authorization at dispatch

Every request goes through two checks:

  1. Dispatch gate (in proto, before the handler). Let R be the verb’s schema grant (RFC-0004). If R is none, allow. Otherwise the caller’s effective grants MUST contain R on some scope, or R must be implied (section 4). If not, reply denied.<R> without calling the handler. Verbs whose right is not scoped (admin) are fully decided here.
  2. Resource check (in the handler, through proto). Before acting on or revealing resource X, the handler calls authorize(conn, R, X). That call MUST happen before the handler looks X up, so a caller without rights gets denied.<R> whether or not X exists. Existence of a resource is never revealed to a caller without at least read on it (as RFC-0003 section 3 already requires for events). After it passes, an absent X gives the usual not_found.<kind>. Containment cannot be resolved for an absent X, so an absent X is covered only by *, <kind>:*, or a selector naming X itself. Any other caller gets denied.<R>.

A handler that lists or streams many resources MUST filter by read on each one. It never fails the whole list because some items are not visible. An explicit resource list in params is authorized in full before any output, as RFC-0003 section 3 already says for events.

denied.<right> error data:

{"right":"write","resource":"pane:p1","principal":"g3"}

resource is present only when the caller named it in params. principal is the caller’s own ID. When the caller is unknown, data.fix SHOULD carry a runnable hint that does not reveal any secret. For example:

{"right":"write","resource":"pane:p1","principal":"unknown","fix":"venomx whoami --json  # run inside a venomx pane, or set VENOMX_TOKEN"}

Error codes added by this RFC (all exit 4 unless noted):

Code When
denied.read, denied.write, denied.respond, denied.files, denied.spawn, denied.admin Missing right (dispatch gate or resource check)
denied.token hello.auth rejected; data.reason as in 3.5
denied.revoked The connection’s principal was revoked or expired after hello
denied.delegation Requested child grants exceed the caller’s effective grants
denied.self_approval respond on an item raised by the caller or one of its descendants
invalid_params.grant_scope (exit 2) Malformed scope selector; data.param names the field
not_found.principal (exit 3) grant.revoke or grant.list on an absent principal the caller may read

RFC-0006’s denied.read and denied.write, and RFC-0003’s denied.event_scope, are used unchanged.

8. Interaction with existing RFCs

  • RFC-0001 section 2 table: unchanged. skill.install holds files scoped to the resolved destination via path:.
  • RFC-0002: hello.auth is defined (3.5). hello, cancel and ping stay ungated. A connection’s principal does not change after hello.
  • RFC-0003: “caller scope” means the read scopes of the effective grants, evaluated live. Revocation ends subscriptions (6.4).
  • RFC-0004: a verb’s grant member is the dispatch-gate right (section 7). Generated docs and skill MUST explain the read-only default and that denied.* exits 4. skill MUST NOT mention the owner file or @owner. Agents are not meant to elevate themselves, and the owner path is for humans (it is documented in the man page).
  • RFC-0006: a writer is an attachment whose effective grants include write on the pane at ATTACH time.

9. Shared verbs

The grants bundle is merged into every daemon’s schema, like the RFC-0001 standard bundle:

Verb / CLI Right Params Result
whoami none — Principal object (3.5) for the caller
grant.list / grant list read optional string principal (default: caller) {"principals":[…]}: that principal and its descendants that the caller may read. owner and admin see the whole tree from owner.
grant.issue / grant issue spawn required grants (array of {right, scope}); optional integer ttl; optional string label; optional integer pid (3.3) {"principal":"g9","token":"vnt1.…","expires":"…"}. The token is returned once. With pid: {"principal":"g9","expires":null}, no token, and expires only if ttl was given.
grant.revoke / grant revoke spawn required string principal {"revoked":["g9","g12"]} (the principal and cascaded descendants)
audit admin optional integer limit (1–1000, default 100) {"records":[…]}, newest last (section 10)
  • grant.revoke is allowed on the caller’s own descendants with spawn, and on any principal except owner and unknown with admin. Revoking yourself is allowed, and the connection then gets denied.revoked.
  • In the whoami principal object, grants is the caller’s grants as issued. When the caller is capped by something besides its own ancestors (6.1), the optional member capped_by lists the floor’s principal ID (or unknown for ORPHAN). Clients MUST NOT assume grants alone is the full answer when capped_by is present.
  • A session issuing a short-lived token for a sandboxed tool:
$ venomx grant issue --grants '[{"right":"read","scope":"session:s1"}]' --ttl 600 --label sandbox --json
{"principal":"g9","token":"vnt1.venomx.g9.c2FuZGJveCB0b2tlbiwgaWxsdXN0cmF0aXZlIG9ubHkh","expires":"2026-11-06T12:10:00Z"}
$ venomx grant revoke --principal g9 --json
{"revoked":["g9"]}

Events (RFC-0003 catalog additions; resource principal:<id>):

Type Data
principal.created kind, parent, grants, expires, optional pane
principal.revoked reason: revoked, expired or exited; cascade (integer)
principal.denied verb, right, optional resource

These are visible through the principal-resource rules in section 5: to the principal itself, its ancestors, and admin. principal.denied lets an orchestrator or the TUI show “agent g7 tried to write pane p1”. It is the documented hook through which RFC-0001 section 7 allows approval UX. It never blocks the request, which has already failed. Events MUST NOT contain tokens or request params.

10. Audit log

Location: $XDG_STATE_HOME/venom/<tool>/audit.ndjson, or ~/.local/state/venom/<tool>/audit.ndjson if XDG_STATE_HOME is unset or empty. The directory is 0700 and the file 0600, with the same lstat checks as RFC-0002 section 2.4. Override: config grants.audit.path. The log is in the state directory, not the runtime directory, so it survives a reboot.

One compact JSON object per line, appended with O_APPEND. Records MUST be written for:

event When
denied Any denied.* reply, including hello token failures and the dispatch gate
admin Every request whose dispatch-gate right is admin, allowed or not
owner_auth Every connection that resolves to owner
issued / revoked Every principal creation and revocation, including automatic ones
restored A process principal read back from the bound file at serve (3.3). peer names that process, and has start. principal is the parent in the file, which proves nothing about who wrote it

Fields: ts, source, event, principal, peer (pid, and exe from /proc/<pid>/exe or proc_pidpath, for attribution only), verb, right, optional resource, optional code, optional target (the principal issued or revoked), optional cascade. Records MUST NOT contain tokens, secrets or request params other than the resource named.

Rotation: when the file exceeds grants.audit.max_bytes (default 16 MiB), it is renamed to audit.ndjson.1, shifting older files up, and the daemon keeps grants.audit.keep (default 2) old files. If a write fails, the daemon continues, and doctor reports fail for check grants.audit_writable. Authorization never depends on the audit log succeeding: a failed write neither allows nor denies anything extra.

audit (section 9) returns the most recent records from the current file.

11. Configuration

The shared grants section, validated by cfg (RFC-0001 section 5):

.{
    .grants = .{
        .unknown = .{
            .{ .right = .read, .scope = "*" },
        },
        .interactive = .{
            .{ .right = .read, .scope = "*" },
            .{ .right = .write, .scope = "self" },
            .{ .right = .spawn, .scope = "self" },
        },
        .max_token_ttl = 86400,
        .audit = .{ .path = "", .max_bytes = 16777216, .keep = 2 },
        .isolation = true, // the product's default; venomx: false (11.1)
    },
}
  • unknown may contain only read entries, or be empty (.{}), which means unknown gets nothing beyond none verbs. Anything else fails with invalid_params in config check/config set.
  • grants.* changes apply on config reload. Changes to unknown and interactive take effect for subsequent requests on existing connections; principals already issued keep their issued grants.
  • Changing grants requires admin (it goes through config.set).

11.1 Isolation

grants.isolation (bool) says whether the daemon keeps its principals apart. The product sets the default and documents it: venomx defaults to off, because a terminal multiplexer is mostly used by one person whose own scripts should just work, as they do in tmux. Every other product defaults to on.

On is the model the rest of this RFC describes, unchanged.

Off trusts the UID:

  • unknown (OUTSIDE and ORPHAN floors, no token) holds read, write and spawn on *. So does the interactive profile (6.2 item 2). respond, files and admin are not included: approvals, host files, config set and audit still need the owner or an issued principal.
  • grants.unknown and grants.interactive are not used. They keep their validation, so turning isolation on brings them back as written. doctor reports grants.isolation (info when off) and warns when either list differs from its default while isolation is off.
  • Below another manager, as before. A peer with no token that has another tool’s manager on its chain (3.8) is capped by grants.unknown as if isolation were on. An agent that venom starts outside venomx panes therefore gains nothing from this switch.
  • Attenuation is unchanged. A token is exactly as narrow as it was issued, a principal is capped by its floor (3.4, 6.1), and a pane created with a grants parameter, or by a principal other than owner, gets what 6.2 item 1 says. Isolation off only widens the two defaults.
  • Namespaces (RFC-0016 section 5.1) become names only: see RFC-0016.
  • The setting applies on config reload to later requests on existing connections, like the two lists. Changing it requires admin.

A product that runs agents on top of a venomx daemon (venom, venom-os) SHOULD turn isolation on there, or warn in doctor when it is off. With it off, an agent that escapes its pane’s tree resolves to ORPHAN and gets the wider unknown.

12. Threat model

Assets: the PTYs, sessions and state of each daemon; the ability to spawn processes and write files on the host; approvals (respond); and the owner role.

Trust boundary. Every caller has the daemon’s UID (RFC-0002 section 2.4). Grants are not a sandbox. A same-UID process that deliberately attacks the user can already read ~/.ssh, ptrace (subject to Yama), edit the config file, or read the owner file. Grants protect the sanctioned channel, meaning verbs over the socket, against:

  • agents that are confused, prompt-injected or overreaching, while they use that channel as documented;
  • escalation between principals that the daemon itself created;
  • mistakes, by making every denial visible and attributable.

Strong isolation of an agent needs an OS sandbox (Landlock, bubblewrap or seatbelt) that hides the owner file and other users’ state. That is out of scope for v1, but this design does not prevent it: the owner token lives in a file, process-bound credentials use ancestry, and bearer tokens cover callers in another PID namespace.

# Threat Mitigation Residual
T1 Malicious pane writes to another pane, or kills it write scoped to self by default (6.2, 11); resource check before lookup (7) None within the channel while isolation is on. With isolation off (11.1), every pane, and every same-UID process outside venomx, may write anywhere.
T2 Pane steals another pane’s token from /proc/<pid>/environ Bound tokens are useless outside their subtree, and effective grants are capped by the thief’s floor (3.4) An OUTSIDE process can use a stolen unbound token until it expires (max_token_ttl)
T3 Pane spoofs VENOMX_PANE or VENOMX_PRINCIPAL These are informational and never trusted (3.6) None
T4 Agent in a pane reads the owner file and presents it owner_context refusal inside managed trees (3.4), its own daemon’s or another tool’s (3.8) Sockets in different directories, and processes with no manager above them (3.8)
T5 PID reuse: the peer exits and its PID is recycled into a privileged pane Identity resolved at accept; pidfd liveness check; (pid, starttime) root matching (3.2) On Linux < 6.5, a narrow race between accept and the walk; doctor warns
T6 Escaping the tree (double fork, setsid) to look OUTSIDE Subreaper: escapees resolve to ORPHAN, which has at most unknown rights (3.2) On macOS, escapees look OUTSIDE (unknown unless they have an owner or unbound token)
T7 Confused-deputy agent: venom performs an agent’s request with venom’s broader rights On-behalf-of rule (6.3); proto.issueChild; MCP adapter runs as the agent’s child (3.6) Depends on each orchestrator following 6.3; COM-216 conformance tests it for the shared paths
T8 Agent approves its own permission request, directly or through a sub-agent denied.self_approval (4) A colluding sibling outside the raiser’s subtree that holds respond. Do not give agents respond by default.
T9 Delegating more than you have Attenuation check at issue time and intersection at use time (6.1, 6.2) None
T10 OSC 7501 or OSC 133 claims identity or approval Observations only (RFC-0001 section 8, RFC-0003 section 7); never an input to 3.x None
T11 Token leaks through logs, events, doctor, skill or error data Explicit MUST NOTs (3.6, 9, 10); vnt1. prefix for scanners Leaks by the holder itself (shell history, agent transcripts)
T12 Path scope escaped through symlinks or .. realpath, component-wise prefix, O_NOFOLLOW creation (5) TOCTOU between check and use for directories the attacker controls; handlers SHOULD use openat from the checked directory
T13 Audit-log flooding to hide activity Size-based rotation with keep files; doctor reports rotation rate An attacker can push old records out; ship logs elsewhere if that matters
T14 Unknown local process reads secrets in pane contents grants.unknown is configurable down to .{} Default is read on * (Open question 2). With isolation off it can also write and spawn (11.1).
T17 Isolation off (11.1): a confused or injected program writes to or spawns in panes it never created Off is a deliberate, product-documented default for single-user use. admin, respond and files stay out of reach; peers below another manager, tokens and spawned children are capped as before; doctor reports the mode Accepted for venomx, as in tmux: the boundary is the UID. Turn isolation on when agents run in panes.
T15 Chained owner files: an agent of one tool reads another tool’s owner file, opens a pane, and that pane reads the first tool’s Refused at both links (3.8); subreaper on every spawning daemon (3.2) A process that outlives its manager, or starts with an outside parent (systemd-run --user, and in principle tmux, ssh or cron), has no manager above it and gets owner. A process that deletes the markers, or reads an unbound token a human gave a pane from /proc/<pid>/environ. A forged marker denies the human’s @owner; doctor reports it (3.8).
T16 Forged bound file: a process in a pane adds an entry to <tool>.bound for its own pid, or for another live process, and needs a restart. It need not wait: it can SIGTERM the daemon and run venom serve itself, which has no launcher check, and has those rights in about 1.5 s. Since children get nothing (3.3), it speaks on the socket itself, or execs venom on the pid it wrote The file is 0600, and writing it, like signalling the daemon, is outside the sanctioned channel. A restored entry never holds admin, and only that process holds it, not its children (3.3). restored in the audit log, grant.list and doctor shows that an entry came from the file, after the fact. Accepted. It gets read, write, respond, files, spawn on *, so it can approve for other agents. “Not its children” doesn’t limit a forger: holding the entry, it can call grant.issue --pid=<its own child>, and the child has the same rights, recorded as issued with nothing saying restored. restored doesn’t separate a forged entry either, since after any restart the real entries say it too. Only the exe and pid in the audit record do, and a forger that execs venom defeats exe. Same tamper class as deleting a manager marker (T15), which needs no restart and gives owner, so this widens nothing. The limit of venom-org/venom#122: it removes a credential readable by anyone who looks at /proc/<pid>/environ, so the threat moves from reading a variable to tampering with state on purpose. It doesn’t stop tampering. That takes an OS sandbox (section 12).

Not covered: other UIDs and root (rejected at the socket, RFC-0002 section 2.4; Open question 4), kernel compromise, and remote peers (COM-234).

13. Implementation checklist (COM-209)

proto MUST provide, and COM-209’s tests MUST cover:

  • ID-01 — Resolve at accept: peer UID check, then the pidfd (Linux ≥ 6.5) or PID, then the walk (3.2). The result is cached on the connection and never re-resolved.
  • ID-02 — Ancestry mapping: a test spawns a bound root, a child and a grandchild, and checks that each resolves to the root’s principal. A nested bound principal wins over its ancestor’s.
  • ID-03 — Starttime binding: a root that has exited, with its PID faked in the registry for an unrelated process, does not match.
  • ID-04 — Orphan: a double-forked descendant (with subreaper set) resolves to ORPHAN, which has at most unknown’s grants.
  • ID-05 — Outside: an unrelated process resolves to unknown without a token.
  • ID-06 — Token fallback: an OUTSIDE caller with a valid unbound token gets the token’s grants. Malformed, unknown, expired, revoked and other-tool tokens each give denied.token with the right reason, and the connection closes.
  • ID-07 — Floor cap: a bound caller presenting another principal’s unbound token gets the intersection. A bound token presented outside its subtree gives binding.
  • ID-08 — Owner: the owner file is created 0600 with O_EXCL and a new secret on every start, and unlinked on shutdown. The owner token is accepted from OUTSIDE and gives owner_context from inside a managed tree or from ORPHAN.
  • ID-09 — Other managers: the owner token gives owner_context to a peer with a marked process above it, in the socket’s directory. It does not when only the peer is marked, when the marker’s starttime differs, or when the marker is gone. doctor warns for that peer. A daemon with the subreaper holds a 0600 marker while it runs. With no token that peer is unknown.
  • GR-01 — Gate before handler: a verb with write called by unknown returns denied.write and the handler is not called (a counter stays 0). none verbs pass for everyone.
  • GR-02 — Implications: exactly the three rules in section 4.
  • GR-03 — Scopes: *, kind:*, kind:id with containment through a test parentOf, self with creator tracking, and path: with symlink and .. cases and the /proj versus /project2 case.
  • GR-04 — No existence leak: for a caller without rights, a missing and an existing resource give the same denied.<right>.
  • GR-05 — Attenuation: grant.issue beyond the caller’s grants gives denied.delegation. Intersection along the chain is applied at use time after an ancestor is revoked.
  • GR-06 — Revocation: cascades to descendants; a later request gives denied.revoked; an events stream ends with denied.event_scope within one second.
  • GR-07 — Expiry: a token past expires is rejected at hello and, on a live connection, at the next request.
  • GR-08 — Self-approval: the helper canRespond(responder, raiser) rejects the raiser itself and its descendants.
  • AU-01 — Audit: records for denied, admin, owner_auth, issued and revoked, with no token bytes. A test greps the log for vnt1. and for the test secret. Rotation at max_bytes.
  • AU-02 — Audit failure: an unwritable audit path leaves authorization results unchanged and makes doctor report fail.
  • CF-01 — Config: grants.unknown with a non-read entry fails config check.

Non-normative API sketch for proto:

pub const Right = enum { none, read, write, respond, files, spawn, admin };
pub const Resource = struct { kind: []const u8, id: []const u8 };

pub const Containment = struct {
    ctx: *anyopaque,
    parentOf: *const fn (ctx: *anyopaque, r: Resource) ?Resource,
    ownerOf: *const fn (ctx: *anyopaque, r: Resource) ?PrincipalId,
};

// Called by handlers; returns error.Denied → `denied.<right>`.
pub fn authorize(conn: *Conn, right: Right, r: Resource) error{Denied}!void;
pub fn authorizePath(conn: *Conn, right: Right, path: []const u8) error{Denied}![]const u8; // returns the realpath
pub fn issueChild(reg: *Registry, parent: PrincipalId, grants: []const Grant, ttl: ?u32, root: ?ProcRef) !Issued;
pub fn canRespond(reg: *Registry, responder: PrincipalId, raiser: PrincipalId) bool;

Agent-contract impact

  • Verbs: adds the grants bundle: whoami (none), grant.list (read), grant.issue and grant.revoke (spawn), and audit (admin). Spawning verbs in products SHOULD accept the grants param shape from 6.2. The schema’s existing grant member is the dispatch-gate right.
  • --json and exit codes: no new classes. Adds denied.* details, invalid_params.grant_scope and not_found.principal (section 7). All denials exit 4.
  • Events: adds principal.created, principal.revoked and principal.denied (section 9), with resource principal:<id>. Revocation ends affected events subscriptions.
  • skill, doctor, config: skill explains the read-only default and whoami, and never the owner path. doctor adds the checks grants.peerpidfd (warn on fallback), grants.subreaper, grants.owner_file (mode and owner), grants.audit_writable and grants.unknown_policy (warn if unknown can read *) and grants.isolation (11.1). config gains the grants section (section 11), which needs admin to change.
  • Capability grants: this RFC defines them. Unknown callers are read-only at most while isolation is on. Mutation requires an issued principal. With isolation off (11.1), the default in venomx, unknown callers and interactive panes may also write and spawn.
  • Headless: no TTY is needed anywhere. Identity comes from the process tree and tokens, never from prompts. Approval UX is events plus respond verbs, never stdin.
  • OSC 7501 / OSC 133: no change. Terminal sequences never carry identity or grants (T10).

Alternatives

  • Tokens only, no ancestry (pure bearer credentials in the environment). Simpler, but any same-UID process that reads /proc/<pid>/environ takes over a pane’s rights, and a child can drop its token by unsetting a variable but cannot drop its ancestry. Ancestry gives an unforgeable floor within the managed tree. Tokens remain for callers with no usable ancestry.
  • Ancestry only, no tokens (tuios-style PID mapping). Fails for sandboxed agents in their own PID namespace, for daemons that did not spawn the caller (venom versus venomx), and for remote bridges.
  • Trust every same-UID caller (tmux). Explicitly forbidden by RFC-0001 section 7. It makes a prompt-injected agent equal to the human.
  • Auto-present the owner token from any CLI outside a managed tree. Convenient for the human’s other terminals, but it gives every agent that runs outside venom full rights by default. Opting in with @owner keeps that a deliberate act. See Open question 1.
  • cgroup-based identity (one cgroup per pane; read /proc/<pid>/cgroup). Robust against double-fork escapes, but Linux-only, it needs systemd or cgroup delegation, and it is heavier than v1 needs. It can later be added as another floor source without changing the wire.
  • Macaroons or biscuits (attenuable tokens that clients can verify offline). Elegant attenuation, but every daemon is the sole verifier of its own tokens, and the registry already gives attenuation and instant revocation. Not worth a dependency in every Zig repo.
  • Hierarchical rights (admin > write > read). Too coarse: spawn, files and respond are independently dangerous and must be grantable separately (a respond-only approver, a files-only installer).
  • Persisting principals and tokens across daemon restarts. Persistent bearer tokens are a standing liability. Durable policy belongs in config profiles, and durable sessions get fresh principals when panes are restored. Deferred (Open question 3).
  • Ask venomx whether a pid is in a pane (for 3.8). Needs venomx up and reachable when venom accepts a connection, which headless venom can’t assume, and only answers for venomx. A marker needs neither.
  • Look for VENOMX_PANE in the ancestors’ environments (for 3.8). Those variables are never trusted (3.6), a double-forked process with the variable unset has none above it but the daemon, and ancestors of another UID can’t be read.
  • Only venomx refuses venom’s agents (for 3.8). Leaves a take-over’s model in a pane, and every other process in a pane, able to become venom owner.
  • Only venom refuses panes (for 3.8). Leaves an agent as venomx owner, able to pane send into the human’s shells.
  • Name the process (PR_SET_NAME) instead of a marker file (for 3.8). Can’t be removed by same-UID file access, but has no scope: every test daemon started inside a real pane would be refused its own owner token.
  • An environment switch that lets a human’s pane through. An agent can set it too. A human’s pane and an agent’s look the same to a daemon, and that is the whole problem.
  • Issue the control pane an unbound token (for 3.8; what venom-org/venom#119 shipped). It expires, dies with the daemon, and a process below a manager can read it from /proc/<pid>/environ.
  • Have venomx say which pane is the control pane after a restart (for 3.3). Needs venomx up, which headless venom can’t assume, and a tag only the owner can set. It is also a claim by another daemon about who holds these rights.
  • Have the TUI hold a secret to bind itself again (for 3.3), that is, make a restored process prove it is the one. The daemon would have to keep the hash in a file, and a same-UID process could write its own into that, so it has the weakness of the bound file and a credential too.
  • Sign the bound file with the owner token (for 3.3). The owner token changes at each start, so a key would have to outlive it, in a file. And a process below a manager can read the owner file today, only presenting it is refused (3.8), so it could sign too.
  • Check the restored process’s parent chain (for 3.3), for example that venomx’s daemon is above it. A process in any pane has that same chain, and this would tie venom to venomx, which headless venom can’t assume.
  • Check the restored process’s executable (for 3.3). The forger can name a child that execs venom, or venom mcp, whose pid and starttime stay the same, and it would be driving that from outside.
  • Let a process principal cover its subtree, like pane and agent (for 3.3). A shell or take-over harness below a bound agent view would hold a person’s rights, and could approve for other agents. Binding venomx’s daemon would give them to every new pane.
  • A marker that can’t be deleted (for 3.8), such as an abstract socket name the manager holds (venom-spec#92). Rejected: deleting the marker is not the weakest route, so this closes nothing an agent needs. Without touching a marker, a same-UID process can start outside the tree with systemd-run --user, kill its manager and outlive it, or read an unbound token from /proc/<pid>/environ (T15). An abstract name would also cost more than it gives. It exists only on Linux, so macOS still needs the file. It lives in the network namespace, so a caller in unshare -n finds no name and looks clean, which fails open where a file fails closed (3.8). Any UID can bind one, and a forged one can’t be ended by deleting a file, only by finding and killing its holder. Looking one up costs more than the one stat per process that 3.8 promises. This threat is the OS sandbox’s (section 12), which can also make the directory read-only for the agent, with no new mechanism. Revisit it if deliberately hostile same-UID agents without a sandbox enter the threat model, and then for all these routes together.
  • Do nothing. Blocks COM-209, AC-09 and the P1 agent-teams goal, and leaves denied with no producer.

Open questions

All six were deferred by the maintainer at acceptance (2026-11-06). v1 targets a single local user, so the choices in this RFC stand as v1 behaviour. They are revisited together in COM-235 (Open questions & revamps), whose trigger is remote attach, multi-user use, sandboxed agents, or the defaults getting in the way in daily use.

  1. Owner access from the CLI. Deferred to COM-235. v1: explicit opt-in with <TOOL>_TOKEN=@owner (3.7).
  2. Default for unknown. Deferred to COM-235. v1: read on *, configurable down to .{} (section 11). For venomx, isolation off (section 11.1, venom-spec#174) answers it.
  3. Persistent principals. Deferred to COM-235. v1: a restored pane gets a freshly issued principal.
  4. Other UIDs and root (RFC-0002 Open question 6). Deferred to COM-235. v1: same UID only, rejected at the socket.
  5. Remote peers. Deferred to COM-235 and the remote-transport RFC (COM-234). The remote end MUST require hello.auth (RFC-0002 section 8). v1: the owner token is never valid over a bridge.
  6. Interactive profile defaults. Deferred to COM-235. v1: read *, write self, spawn self (section 11). For venomx, isolation off (section 11.1) answers it.