Agents
An agent publishes through the hosti CLI with a push token the owner approved. The hosti-publish skill tells it how.
The hosti-publish skill
The skill is one file in the Hosti repository, skills/hosti-publish/SKILL.md. An agent on any machine installs it by repository name:
npx skills add JakubSzwajka/hosti
Your catalog serves nothing for this, and the file is the same text for every instance. It holds no URL and no token. The CLI's saved login carries both.
The skill walks the agent through these steps:
- Run
hosti --help, and install the CLI if it is missing. - Run
hosti whoami. If that fails, ask the owner for the catalog's URL, never guess one, and runhosti login <catalog URL>. - Check that
index.htmlsits at the root of the folder. - Pick a slug: lowercase letters, digits and dashes, 1 to 64 characters. Run
hosti lsfirst, and tell the owner before replacing what is live at a taken slug. - Push, and hand the owner the admin link exactly as printed.
- Share, only when asked. A pin comes from the owner. The agent never invents one and never repeats the digits back.
- Rotate, only when asked.
- Open, to print the link to hand over.
- Delete, only when
hosti whoamilists thedeletescope and the owner asked for that bundle. - When a command fails, read its last line before doing anything else.
The skill never prints the token, never reads it out of the config file, and never puts it in a file, a script, a command or a commit message.
Connecting an agent
The catalog's /tokens page, and the empty catalog, show the two commands an agent runs, with your instance's URL filled in:
npm install -g https://github.com/JakubSzwajka/hosti/releases/latest/download/hosti-cli.tgz
hosti login https://hosti.example
Under them is an agent prompt that says the same thing, ready to paste. Neither carries a token.
hosti login starts an agent connection:
agent POST /api/v1/agent-authorizations
sends two digests, gets an id,
a code and an approval link
|
owner opens /connect/<id>, checks the
code, ticks scopes, approves
|
agent polls until approved, then saves
the login:
GET /api/v1/agent-authorizations/<id>
The approval page shows the token name, the code, the scopes the agent asked for, and when the request runs out. publish and share are granted when asked. delete appears only when the agent ran hosti login --allow-delete, and its box starts unticked. You can grant less than was asked, never more.
A connection lives in server memory for ten minutes. A restart drops pending ones, and the agent then runs hosti login again. Approving needs no password when your browser already has a session.
Push tokens
A push token is a bearer secret an agent or the CLI uses to write. The agent makes it and sends only its digest, so Hosti never holds it in the clear and has nothing to show you. It never passes through your clipboard or the chat.
The /tokens list shows every push token, its scopes, when it was made and when it last pushed, with a revoke on each. Revoking drops the digest, so that token stops opening /api/v1/ from the next request on. Revisions it already pushed keep its name on the bundle's pushed by line. Nothing under /api/v1/ makes or revokes a token, so no push token can create another one.
Scopes
publish- List bundles, read one, push a revision (creating the bundle if new), prune. Every token has it.
share- Set the sharing state to
private,linkorpin, set a pin, rotate the share link. delete- Delete a bundle, with its revisions and files. Granted only when the agent asked for it and the owner ticked it.
A command the token lacks the scope for answers 403 missing_scope. The CLI prints the server's message on the last line and exits 1. Tokens made before scopes existed carry all three.
A token from a shell
The owner can also make a push token on the server. It prints the secret once, and Hosti stores only the digest:
docker compose exec web node scripts/new-token.mjs --name vps # publish and share
docker compose exec web node scripts/new-token.mjs --name vps --allow-delete # and delete
From a checkout of the repository, pnpm token:new -- --name laptop does the same.
Pushing over HTTP
The CLI is a thin client over these routes. Every one takes Authorization: Bearer <push token>, and cookies never open them.
POST /api/v1/bundles/<slug>/revisions publish push a gzipped tarball
GET /api/v1/bundles publish list bundles
GET /api/v1/bundles/<slug> publish one bundle and its sharing state
POST /api/v1/bundles/<slug>/prune publish prune on demand
PUT /api/v1/bundles/<slug>/sharing share set the state
POST /api/v1/bundles/<slug>/sharing/rotate share mint a fresh share slug
DELETE /api/v1/bundles/<slug> delete the bundle, files and all
GET /api/v1/whoami any the token's name and scopes
A push by hand:
tar czf /tmp/b.tgz -C ./out .
curl -X POST https://hosti.example/api/v1/bundles/squad-2026/revisions \
-H "Authorization: Bearer $HOSTI_PUSH_TOKEN" \
-H "X-Hosti-Title: Squad 2026" \
--data-binary @/tmp/b.tgz
It answers 201 with the revision number, the admin URL and the sharing state. X-Hosti-Collection sets a collection. Its name cannot be -, hold a slash, or run over 64 characters. A push naming - is refused 400 bad_collection before a byte reaches the disk.
A refused push leaves no partial directory and does not move current. The refusal names what went wrong, for example:
No index.html at the root of the pushed tree. Found: docs/, notes.txt