Knocking: asking to be let in
A knock asks an endpoint to let you reach it. It is a request, never a grant: answering it is what writes an access rule, and the answer belongs to the endpoint being asked.
spt knock wanda --surfaces MSG --send-only # ask
spt knock list --for wanda # see what is waiting for you
spt knock approve k-4f2a --approve-requested
spt knock deny k-4f2a
A knock asks for MSG by default — only use --surfaces if you need more
than MSG. The commonest knock is “let me talk to you”, and asking for every
surface would put a whole-machine grant in front of someone who only meant to
answer a message. --surfaces ALL still asks for everything, out loud.
spt knock <target> is spt knock send <target> — the same command by a
shorter name, with the same flags, the same defaults and the same knocker. Use
whichever reads better.
The one place they differ is a name collision: a subcommand always wins over
the target, so an endpoint whose id happens to be send, list, approve,
deny, new-code or redeem cannot be reached by the bare form. Knock it as
spt knock send <id>.
Knocks work between machines as well as within one. If the endpoint you name lives on another node, the knock travels there and lands in that node’s inbox.
Surface names are case-insensitive, and what you type is stored in the
table’s own spelling: msg, Msg and MSG all become MSG, and all in any
case is the every-surface sentinel rather than a surface literally named “all”.
You do not have to remember the vocabulary. Every verb whose invocation names a
surface prints a Control surfaces: section in its own --help, composed
from the surface table at render time — spt knock and its send, approve,
new-code and list forms; spt endpoint access (the listing and its
allow/deny/remove); spt node access and the same three; and
spt subnet create, whose open/closed choice is a posture over that whole
vocabulary. Each line names the traffic and what granting it admits: a
surface that carries a proven sender binds a grant to that single sender, and
one that does not admits the sender’s whole machine. spt knock redeem and
spt knock deny print no such section on purpose — by then the surfaces are
already fixed by the code, or by the knock you are answering, so offering the
vocabulary there would suggest a choice you do not have.
A token that matches no known surface is refused where you typed it, naming the surfaces that do exist — before the knock is sent, so a typo costs nothing rather than travelling to another machine, waiting to be read, and being approved into a grant nobody asked for.
Nobody is interrupted
A knock lands in a queryable inbox and is never pushed at the receiving agent. Somebody has to look — you, or another agent asking on your behalf. That is deliberate: an unsolicited request that could interrupt an agent’s work would be a channel worth spamming.
The inbox is owned by the daemon, not by a perch, so a knock survives the target’s agent being asleep, detached, or absent entirely. Purging the knocker’s endpoint does not invalidate a knock they already sent.
Two things do notify, and only these two, because each is invited by something you did yourself — each is an answer to a request you sent:
- an approval notifies the knocker — they asked;
- a redemption notifies the code’s minter — they minted it.
A knock-back does not notify. It is a new request from the other side, not an answer to yours, and new requests land in the inbox like every other.
Who you knock as
There is no --from. The knocker is classified at invocation and stamped by
the daemon:
- an agent knocks as its own endpoint, and an approval whitelists exactly that endpoint;
- a person at a terminal knocks as the humans on their machine — the
request is
subject <node>, origin user, deliberately narrower than admitting the whole machine, so the commonest approval admits people and not their agents.
--for <endpoint> knocks on behalf of an endpoint that lives on your
machine. Naming one that does not exist here is refused rather than sent.
Two-way reach is offered, never imposed
You say which way reach runs when you ask for it, and only then. Every
reach-requesting command — spt knock <target>, spt knock send and
spt knock redeem — requires exactly one of --send-only or
--send-receive. There is no default: a bare invocation refuses and names
both, and passing both refuses too, because they are opposite answers to the
same question.
The flag describes your own side, never theirs:
--send-onlyasks to reach them, and deliberately not the reverse. It is the explicit decline, and it is recorded, so a decision to keep reach one-directional reads back later as a decision and not as a flag nobody typed.--send-receivealso pre-authorizes the reverse on your own side. Nobody writes another endpoint’s rules; your daemon writes it if and when they answer. It is consumed exactly once, and an unanswered knock leaves it unconsumed — a conditional intention, not a standing grant.
What --send-only does not bar: replies. A reply needs no rule of its own,
on any endpoint. The access gate implements a stateful firewall: a message
correlated to the receiving endpoint’s own outbound is admitted on that
correlation, before any rule or mode is consulted — and this holds even when
the access store cannot be read, because the exemption precedes the store. The
engine room is the strictest consumer of the same rule, not its owner: even the
seat that refuses all inbound still answers replies to its own outbound. So a
--send-only grant means the far side cannot initiate traffic toward you;
it can always answer the traffic you start. Declining reverse reach declines
unsolicited sends, never the conversation you open.
Answering carries no directionality at all. spt knock approve and
spt knock new-code take no directionality flag, because accepting is your
own side’s act — there is nothing further to declare. If you approve someone and
want to reach them as well, knock back:
spt knock send flynn --surfaces MSG --send-only
That is the same verb anybody else uses to ask for reach, which is the point: one instrument, one grammar.
Renamed in v0.54.0.
--mutualis now--send-receiveand--one-wayis now--send-only, and both moved from the answering verbs to the asking ones. The old spellings are parse errors that name their replacement — there are no aliases. Pre-authorizations you armed under the old flags are untouched and still honored.
Two-way reach works across machines. A pre-authorization is still consumed only on the node that armed it — nobody writes another endpoint’s rules — and what crosses is the answer, not the rule:
- an approval or denial travels back to the knocker’s node as an answer receipt, and their own daemon writes the reverse rule (approval) or retires the intention (denial). A receipt acts only on a record bound at knock time to the node being knocked, so nothing else on the network can fire or destroy it;
- a knock-back is an ordinary knock and rides the wire any first-order knock rides, into the other side’s inbox on their own machine.
If the far machine never answers, nothing opens and nothing closes: the pre-authorization stays armed and expires with the knock, and every surface says so rather than claiming an outcome.
Invite codes
A code is a pre-approval you can hand to someone, with the granted surfaces baked in at mint. Presenting it reaches your endpoint even if it is undiscoverable, and auto-approves with exactly those surfaces.
spt knock new-code --surfaces MSG # mint (from that endpoint's session)
spt knock redeem <code> --send-only # present one you were given
A code carries its target. Possessing one tells you nothing about any other endpoint, so a code can never be used to enumerate what exists on a node.
Codes are single-use and expire. A redemption whose grant fails to write does not spend the code — repair the problem and redeem the same code again.
Redeeming from another machine
A code can be presented from any machine in the subnet it was sealed to, not only from the one that minted it. You do not have to be able to see the minting machine first, and you never name it yourself:
spt knock redeem sptkc_...
A sealed code carries its own route. Sealed inside the sptkc_ string,
alongside the secret, is the machine that minted it. Opening the seal takes a
key only members of that subnet hold — so opening it is itself the proof that
you belong there — and what comes out names both the subnet to look in and the
machine to carry the redemption to. Your machine then finds that minter among
the subnet members it already knows of.
It never looks the endpoint up, and that is the point: the endpoint you are redeeming against is usually one that cannot be looked up at all. Being unreachable by discovery is the ordinary reason somebody hands you a code in the first place, so a redemption that depended on discovery would fail exactly where it is needed.
Codes minted before sealed codes existed are plain strings with no route inside them. They still work, and they still redeem on the machine that minted them.
What you can be told
| outcome | what it means |
|---|---|
| redeemed | the grant is written. The reply names the target, the surfaces you were granted, and whether reach is now one- or two-directional |
| refused | the code was seen and turned down. One message covers every cause — unknown, expired, already used, too many attempts — because saying which would let a stranger learn from refusals which codes exist |
| unconfirmed | no answer came back at all |
| peer silent | the machine took the redemption and then said nothing — it is still holding the exchange open, which points at a machine that is wedged or overloaded rather than one too old to understand redemptions. Nothing was decided and the code was not spent |
The last two rows are the ones to remember: silence is not a refusal. The minting machine may be off, unreachable, running a version old enough that it does not understand redemptions, or wedged mid-exchange, and an answer can simply be lost. Nothing was decided about your code and it was not spent, so it is still worth presenting again. A presenter told “refused” here would throw away a good code that was never even seen.
Two refusals arrive before anything is sent, and both are about the route rather than the code:
- it cannot be opened — no subnet this machine belongs to holds a key that opens it. Either it was sealed to a subnet you are not in, or the string is damaged; nothing else about it can be told apart from here.
- there is no route — the code names a machine this subnet has not seen yet. That machine has to appear in the subnet before it can be reached.
A code whose route is ambiguous is refused rather than guessed at — presenting a live code to the wrong machine is not a mistake worth risking to save a prompt.
Opening your own side, when you redeem
--send-receive arms the reverse on your own side, against the code you
are presenting, and it does that whether the code was minted on this machine or on
another one. Nobody writes another endpoint’s rules here either: what the flag
buys you is a rule your own daemon writes, and only once the far side has
actually let you in.
- the code was minted on the machine you are redeeming from — the arm is consumed in the same invocation, so reach is two-directional immediately.
- the code was minted elsewhere — the arm is recorded before the redemption is sent, and written only when the redeemed reply comes back node-proven. That reply is also what fills the rule in: the subject and the surfaces are the ones the answer carried, never a guess made here from a code that says nothing about who will answer it.
It is armed before the send rather than after because an intention written only once the answer arrives could not survive an answer that never comes. So the three outcomes in the table above are told apart on your own record and not merely in the sentence printed beside them: a redemption consumes the arm, a refusal disarms it, and silence leaves it armed — the same “silence is not a refusal” rule the table keeps, applied to your side of the exchange.
Limits that stop knocking being a nuisance
These are enforced by the receiving node, against its own store and clock — a limit the sender honoured would only bind the honest:
| limit | value | why |
|---|---|---|
| one pending knock per (knocker, target) | — | re-knocking updates your request in place instead of queueing another, so an inbox cannot be filled by repetition |
| time to live | 24 hours | an unanswered knock expires; the expiry is checked when you answer, so a stale listing can never make an expired knock approvable |
| inbound knocks per machine | ~10/hour | a flood from one node is refused before it reaches anyone’s inbox |
| code redemption attempts | rate-limited per machine | guessing a code is bounded by this together with the TTL |
An expired knock reads as expired, not as though nobody ever asked.
What an approval may and may not write
Approving names the surfaces you are granting — either --approve-requested to
grant what was asked, or --surfaces to grant less. An approval can narrow a
request, never widen it.
Which subject the grant binds depends on whether the surface can identify its sender:
- on a surface whose traffic carries a proven sender (today
MSG), the grant names exactly that endpoint; - on a surface that carries none, the only subject that can match is the
knocker’s whole machine. Approving such a surface therefore admits every
endpoint on it, whatever the knock named, so it requires
--admit-nodeand is gated by the node’s policy.
When a single knock mixes both kinds and the policy forbids the widening half, the approval is partial: the identifiable half is granted, the rest stays pending, and the output names the engine room as the seat that can answer it. A partial approval is reported as partial — never as approved, and never as denied.
Imparting a note about whoever you admit
An answer can also record what you have decided about the person it lets in — a monic, which is what stops them arriving as a stranger:
spt knock approve k-4f2a --approve-requested --monic "flynn's field-verify seat"
spt knock new-code --surfaces MSG --monic "whoever redeems this is a courier"
The code path is deferred by necessity, not by design: at mint there is nobody to write a classification about yet, so a note typed at mint is imparted when the code is redeemed, once the redeemer’s identity is stamped and real.
The note is about a daemon-stamped id, never a supplied one — the same id the grant is written for, so a note can no more be planted on an innocent peer than a grant can. A knocker with no endpoint id — a bare-terminal human, stamped as their node — cannot be classified: the note is refused loudly and the grant stands, because a classification filed under a node id would be a record no delivery ever matches while reading back as though somebody had been classified.
A note you already hold wins. An approval must not silently rewrite what you already decided about a peer, and a note typed at code-mint hours earlier certainly must not. You are told which happened, and given the command to replace it if that is what you meant. A record that is present but unreadable is not a classification, so imparting replaces it.
The note rides the grant and never changes it. It is imparted only after the grant has committed, is skipped entirely on a refusal, and a mind that cannot be opened costs a loud diagnostic rather than a retracted approval.
A knocker admitted without a note still gets in — they simply arrive as an unclassified peer, and their first message carries the trust warning.
Refusals you can predict
These exist so that a rule you can read back is a rule that actually governs.
A rule about nobody is refused when you type it. Every mutation names a
subject — --endpoint, --node, or --any-of. The error arrives before any
store is touched.
A rule that could never match is refused rather than stored. Naming a sender endpoint on a surface that carries no proven sender is rejected at the CLI: such a rule would sit in the view looking like protection while matching nothing. Rule about the machine instead, or drop that surface.
A rule naming no surfaces must satisfy the node policy on every surface.
Omitting --surfaces (or passing ALL, in any case) covers all of them, so if
any single surface is closed to that kind of grant, the whole blanket rule is
refused — otherwise the widest possible grant would be the easiest one to slip
past a closed posture.
Where rules end up
Everything a knock produces is an ordinary access rule, visible in the usual
views with its origin recorded — knock-approve or code-redeem — and with
the exact command to remove it printed beside it. Rules born of a knock have no
special lifecycle afterwards; they are removed like any other.