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 signsInstall 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 webhookCreate 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 deploysWhat 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.
link-time
When the post was signed, in seconds since 1970. A post signed more than 5 minutes from now, either side, is refused.
link-id
The post's own id, never used twice: a post whose id the webhook took in the last 30 days is refused.
link-signature
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 githubA 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:
| Post | Answer |
|---|---|
| Body over 1 MiB | 413 |
| Not signed as its sender signs | 400 or 401 |
| Signed, but neither JSON nor text | 415 |
| Signed with another secret, or 5 minutes off | 401 |
| Id taken in the last 30 days (GitHub's: its body) | 401 |
| Address with no webhook | 404 |
| Any post while your agent can't be reached | 503, 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 5Give 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 nowList 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 deploysKept apart
How webhooks are kept apart from everything else is in Security, and why they work this way in Decisions.