Webhooks

Let a service post to your agent: GitHub, a script, anything that signs. Each post reaches your agent as a message from someone who isn't you, and nothing it says goes back.

lnk plugin add webhook                        # the plugin that checks each post, once
lnk agent webhook create deploys             # prints its address and its secret, once
lnk agent webhook create repo --sender github  # one GitHub signs

Install its plugin

Each post is checked by the webhook plugin, lnk-webhook: its signature, its size and that it carries JSON or text. Until it's installed, your agent's webhooks answer every post 503, so their senders try again.

lnk plugin add webhook

Create a webhook

create prints the webhook's address on this machine, http://127.0.0.1:<port>/h/<id>, and its secret. The secret is shown this once: keep it where its sender signs with it, since Link never shows it again. An agent has as many webhooks as you add, each with its own secret, all at one port that's chosen at the first and kept.

lnk agent webhook create deploys

What its harness needs

The agent's harness must say who each message is from and keep a conversation's tools, as Link Harness does: create refuses one that can't, since your agent would read the post as yours, or give it every tool it has. Your agent restarts if it runs, and from then on its bridge takes the webhook's posts.

Reach it from outside

The address takes posts only from this machine. A tunnel gives it a public one, with the same path, https://my-app.<user>.<domain>/h/<id>.

Without a tunnel

A release of Link without the tunnel plugin has no public address to give, and create says so rather than suggesting lnk tunnel open. Its webhooks then take posts from programs on this machine alone.

The same address across restarts

The port stays the same across restarts, so the tunnel's address keeps working. create never opens a tunnel itself (Tunnels).

Sign a post as Link signs

A webhook created without --sender takes posts signed with Link's own scheme: three headers, and an HMAC-SHA256 of the secret over the words link webhook, the time, the id and the body, each after a NUL byte.

When the post was signed, in seconds since 1970. A post signed more than 5 minutes from now, either side, is refused.

The post's own id, never used twice: a post whose id the webhook took in the last 30 days is refused.

v1= and the HMAC in hex, made with the webhook's secret over the words, the time, the id and the body. A post without one is refused.

From a shell

With the secret in SECRET and the address in ADDRESS, a shell signs and sends a post this way.

body='{"ref":"main"}'
time=$(date +%s)
id=$(openssl rand -hex 16)
signature=$(printf 'link webhook\0%s\0%s\0%s' "$time" "$id" "$body" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/.* //')
curl --data "$body" --header "content-type: application/json" --header "link-time: $time" --header "link-id: $id" --header "link-signature: v1=$signature" "$ADDRESS"

Take GitHub's posts

In the repository's settings on GitHub, under Webhooks, add one with the tunnel's address as its payload URL, application/json as its content type, and the secret. GitHub signs each post with x-hub-signature-256.

lnk agent webhook create repo --sender github

A delivery GitHub sends again

GitHub signs no delivery id or time, so a post is known by its body: Redeliver, in GitHub's list of deliveries, of one your agent already took is refused (401), as a replay would be. A delivery that wasn't taken (your agent couldn't be reached) is taken when it's redelivered.

What your agent sees

A post that's signed right reaches your agent as a message from webhook:<name>, in a conversation of that name, and is answered 202 once your agent's harness has taken it, without waiting for its answer. The message is the body as text, and your agent's answer goes nowhere: the poster is told only that it was taken.

A stranger's message

Link Harness quotes each of the post's lines and tells the model who it's from, so it never reads as yours. Its conversation has only the tools your agent's spec gives strangers ([tools] strangers), none unless it says, so the post can't make it act.

What a post carries

A post must carry JSON (application/json, or a type ending in +json, whose body is JSON) or text (text/plain, or any text/ type).

What's refused

Each post that's refused gets its own answer:

PostAnswer
Body over 1 MiB413
Not signed as its sender signs400 or 401
Signed, but neither JSON nor text415
Signed with another secret, or 5 minutes off401
Id taken in the last 30 days (GitHub's: its body)401
Address with no webhook404
Any post while your agent can't be reached503, and taken when it's sent again

Ask yourself a question

A program can ask you a yes or no question through a webhook, such as a deploy waiting for your go. A post whose body is {"question": "Deploy to production now?"} alone goes straight to your direct chats, never waking your agent, and is answered 202 with the question's id, status and expires. You answer it on your chat with its code, as your agent's bridge says.

Its status

The program fetches the answer with a GET of /h/<hook id>/questions/<id>, signed as a post is over an empty body (Sign a post as Link signs). Its status is waiting, allowed, refused or expired. A webhook GitHub signs takes no GET, since GitHub only posts.

What a question may be

Plain text of at most 600 characters, with no link in it (422), so a leaked secret can't send you somewhere. A webhook asks at most 20 a day (429), or as many as --questions-per-day says when it's added.

lnk agent webhook create deploys --questions-per-day 5

Give it a new secret

rotate prints a new secret once, and keeps taking the previous one for 24 hours while you give its sender the new one; the agent needs no restart. --done drops the previous secret at once.

lnk agent webhook rotate deploys           # a new secret, printed once
lnk agent webhook rotate deploys --done    # its sender has it: the previous one goes now

List and delete them

list shows each webhook's name, who signs it and its address, never a secret. delete forgets the webhook's secrets, so its address takes no more posts, and restarts your agent if it runs.

lnk agent webhook list
lnk agent webhook delete deploys

Kept apart

How webhooks are kept apart from everything else is in Security, and why they work this way in Decisions.