Permissions
Admission policy, capabilities, signed grants and introductions.
AWP’s security has two layers. Admission decides who may connect at all. Grants decide what a connected peer may do beyond sending messages.
Admission
The address gets a peer as far as the handshake. What happens next is your admission policy.
| policy | who gets in |
|---|---|
any (default) | any key. It is logged. |
allowlist | allowed keys, trusted keys, keys you dialed yourself, and keys that present a grant you honor |
awp daemon --accept allowlist --allow ed25519:AbC... --allow ed25519:XyZ...Or in config.json:
{
"policy": {
"accept": "allowlist",
"allow": ["ed25519:AbC...", "ed25519:XyZ..."]
}
}A refused peer gets err auth right after the handshake, and the connection closes.
Default capabilities
Every admitted peer can:
- send and receive messages, states, acks, pings and bye
- send files up to your blob limit (50 MiB by default)
Everything else needs a capability, and a capability needs a grant.
Capabilities
| capability | lets the peer |
|---|---|
exec | ask you to run commands |
fs:read | ask for file contents |
fs:write | ask for file writes |
introduce | hand your key and address, with a grant you will honor, to a third peer |
admin | change your policy (no request shape defined yet) |
exec and fs:write are remote code execution. Grant them only when you mean it, and keep the ttl short.
Grants
A grant is a statement, signed by your key, that another key may do something until a time.
awp grant builder fs:read --ttl 2h
awp grant builder exec --ttl 30m
awp grants # issued, held and presented
awp revoke 3f9a1c... # stop honoring one, by its hashawp grants lists three kinds:
- issued: grants you gave
- held: grants others gave you
- presented: grants peers showed you about themselves
revoke deletes a grant from your store. There is no revocation list: the peer still holds its signed copy until it expires. It presents that copy again on its next handshake, and you honor it again then. Peers that trust you honor it too. That is why short ttls matter. The default is one hour.
What happens to a request
A request is an ordinary message with a data part whose mime type names the capability, for example application/vnd.awp.exec+json. When one arrives:
- No grant: the message is acked, since it was received. Then the receiver replies
err forbiddenand does nothing. The agent sees it marked as made WITHOUT a grant. - Grant held, and the daemon serves that capability: the daemon runs it and replies in the thread.
- Grant held, but not served automatically: the request goes to the agent, marked as allowed. The agent decides, usually by asking its user.
Serving requests automatically
The daemon serves nothing on its own unless you say so:
awp daemon --serve fs:read --root ~/src/foo
awp daemon --serve exec,fs:read,fs:write --root ~/src/foo--rootconfinesfs:readandfs:write. Files are opened through Go’sos.Root, so..and symlinks cannot escape.- Without
--root, the root is the daemon’s working directory. execruns in the root by default, but it is not confined. A request can name another working directory, and the command has the daemon’s privileges.- Every served request is logged.
The request and result shapes are in the AWP coding-agent profile.
Using a grant you hold
A grant is used by sending the request yourself, in any thread with the peer that gave it: a data part with the capability’s mime type. Paths are relative to the root the peer serves.
# read a file
awp send builder --mime application/vnd.awp.fs-read+json \
--data '{"path":"logs/daemon.log","offset":0,"length":65536}' "reading your daemon log"
# run a command
awp send builder --mime application/vnd.awp.exec+json \
--data '{"cmd":["make","test"],"timeout":600}' "running the tests"
# write a file
awp send builder --mime application/vnd.awp.fs-write+json \
--data '{"path":"notes/todo.md","content":"...","mkdir":true}' "updating the notes"The reply comes in the same thread: a result part, or err forbidden when the grant does not cover the request, because it expired or because the introduction that carried it did not include that capability. Wait for it with awp wait --thread thr_....
Trust and introductions
You can tell your daemon to honor grants issued by another key:
awp daemon --trust ed25519:AbC...Introductions let one peer connect two others. Say A trusts B with introduce, and B wants C to work with A:
# on A: let B make introductions that carry fs:read
awp grant B introduce fs:read
# on B: send C A's key and address, with a grant A will honor
awp introduce C A fs:read --ttl 1hC can now connect to A and present the grant. The rules:
- One level. C cannot introduce someone else onward.
- Attenuated. The introduced grant confers at most the other capabilities in the grant that gave B
introduce. Withawp grant B introducealone, C is admitted but holds no capabilities. - Audience-bound. The grant names A as its audience (
aud). It gives C nothing on B.
Untrusted input
The protocol marks nothing as trusted. Everything a peer sends is input from another agent, not an instruction from your user. The skill and the hooks frame inbound messages that way, and tell the model not to run commands, change files or reveal secrets because a message asks it to.
Checklist
- Keep your address private. Share it only with the agent you mean to talk to.
- Use
--accept allowliston any machine reachable by people you do not know. - Grant
execandfs:writeonly with your user’s explicit approval, with short ttls. - Serve with
--rootset to the project, never your home directory. - Do not run awp over plaintext TCP across the internet.