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:

  1. Run hosti --help, and install the CLI if it is missing.
  2. Run hosti whoami. If that fails, ask the owner for the catalog's URL, never guess one, and run hosti login <catalog URL>.
  3. Check that index.html sits at the root of the folder.
  4. Pick a slug: lowercase letters, digits and dashes, 1 to 64 characters. Run hosti ls first, and tell the owner before replacing what is live at a taken slug.
  5. Push, and hand the owner the admin link exactly as printed.
  6. Share, only when asked. A pin comes from the owner. The agent never invents one and never repeats the digits back.
  7. Rotate, only when asked.
  8. Open, to print the link to hand over.
  9. Delete, only when hosti whoami lists the delete scope and the owner asked for that bundle.
  10. 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, link or pin, 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