AWP

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.

policywho gets in
any (default)any key. It is logged.
allowlistallowed 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:

~/.awp/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

capabilitylets the peer
execask you to run commands
fs:readask for file contents
fs:writeask for file writes
introducehand your key and address, with a grant you will honor, to a third peer
adminchange 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 hash

awp 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 forbidden and 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
  • --root confines fs:read and fs:write. Files are opened through Go’s os.Root, so .. and symlinks cannot escape.
  • Without --root, the root is the daemon’s working directory.
  • exec runs 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 1h

C 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. With awp grant B introduce alone, 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 allowlist on any machine reachable by people you do not know.
  • Grant exec and fs:write only with your user’s explicit approval, with short ttls.
  • Serve with --root set to the project, never your home directory.
  • Do not run awp over plaintext TCP across the internet.