Kiosk — OSS reference implementation
Apache-2.0 monorepo for Kiosk — the standards-based open protocol that lets the AI assistants of visitors and customers discover, transact with, and pay an operator's service, and its Ruby reference implementation (one HTTP API, several assistants on one customer account, app-layer-authorized data plane with opt-in Postgres RLS defense-in-depth, AP2 mandate trail).
Install
Every gem in the tables below is on RubyGems. The canonical install is one line, the meta-gem:
# Gemfile
gem "kiosk-all"bundle install
bin/rails generate kiosk:install # the initializer + the kiosk migrations
bin/rails db:migratekiosk-all is kiosk-core + kiosk-server — the data plane, and nothing else.
The RLS backstop, the proof-of-work toll (kiosk-reputation and
kiosk-pow-equihash) and the identity and payment adapters are separate gems
that you add for your stack; kiosk-all/README.md says which and why.
What an adopting app needs before that line
| Requirement | Why, and what it rules out |
|---|---|
Rails ~> 8.1 |
kiosk-server declares railties, actionpack, actioncable, activerecord and activesupport at ~> 8.1, and kiosk-pay-stripe declares activerecord at ~> 8.1. No other gemspec names a Rails gem. That excludes Rails 7.x and 8.0.x — bundler will refuse to resolve against an app on either, so this is a precondition and not a footnote. Older Rails lines are untested here, so they are not claimed; widening the floor means adding a CI job first. |
Ruby >= 4.0 |
Every gemspec declares it, and CI runs on 4.0. |
| PostgreSQL | Kiosk's kiosk_* tables and the optional RLS backstop are Postgres. No other database is supported. |
| An existing account model |
c.user_model names it, and c.user_id_type must match its primary key. Kiosk adds an assistant identity beside each account; it does not replace the account. |
No toolchain pin ships in this repository, and that is a decision rather than an
omission: .ruby-version, mise.toml and .mise.toml are gitignored so each
developer manages their own (kiosk-demo-getgrocery/mise.toml.example is the
tracked template). The range above is the contract, both ends of it are on the
merge gate, and that is worth more to a contributor than one pinned patch
release a version manager may or may not honour.
Inside this repository it is path:, and that form does not travel
Every Gemfile here consumes its siblings by path: — gem "kiosk-server", path: "../kiosk-server" — which serves the working tree rather than a built
package. That is deliberate: it is how a change across gems is tested end to
end. It is also a blind spot, because nothing resolving by path: can tell a
file that EXISTS from a file that is PACKAGED. Do not copy the path: form into an
app outside this repository.
The step-by-step operator walkthrough — install through a served protocol, with a worked operation — is kiosk.tech/onboarding.html.
Layout
The gem tables and the demo table are scored on DIFFERENT axes, so their values
are not comparable and the columns are named apart below. The gems carry API
stability: alpha is pre-v1.0 — the surface may still change between releases —
which is the same thing each gem's own README says about itself. The demos carry
deployment: active means the demo is gated on every CI run and live at its
own subdomain. A demo being active says nothing about how
settled the alpha engine underneath it is.
Core framework
| Gem | Purpose | API stability |
|---|---|---|
kiosk-core |
Value types, abstract bases, GUC constants, configuration. No Rails dep. | alpha |
kiosk-rls |
Opt-in RLS DSL + migration helpers; declares policies in a migration and compiles them to PostgreSQL DDL | alpha |
kiosk-server |
Rails engine, routes, OAuth client registration and DPoP token endpoint, executor | alpha |
kiosk-all |
Meta-gem; bundle add kiosk-all installs core + server |
alpha |
kiosk-test-support |
Conformance checks, the assistant-side protocol client and a KYC provider stand-in for tests | alpha |
Plugins & adapters
| Gem | Purpose | API stability |
|---|---|---|
kiosk-rls-rspec |
RSpec journey-test helpers for RLS policies | alpha |
kiosk-rls-minitest |
Minitest journey-test helpers for RLS policies | alpha |
kiosk-user-idp-devise |
Customer sign-in adapter (Devise) | alpha |
kiosk-pay-stripe |
PSP adapter — Stripe, AP2 mandate trail | alpha |
kiosk-kyc-proveli |
KYC provider adapter — the Proveli anonymizing broker | alpha |
Proof-of-work
| Gem | Algorithm | Solver memory | Default? |
|---|---|---|---|
kiosk-pow-equihash |
Equihash (n=168, k=7) | ~1.3 GiB | ✅ |
kiosk-pow-argon2id |
Argon2id (D=0..256) | 64 MiB | no |
kiosk-pow-cuckoo |
Cuckatoo29 | not measured | no |
Each backend is built on kiosk-pow, which holds the shared BLAKE2b-256.
kiosk-pow/README.md tells how to add a backend. See
kiosk-pow-equihash/README.md for the full comparison and rationale.
Reputation & security
| Gem | Purpose | API stability |
|---|---|---|
kiosk-reputation |
Customer reputation factors | alpha |
kiosk-redteam |
Red-team scenarios, adversarial test harness | alpha |
Demo operators
| Demo | Vertical | Deployment |
|---|---|---|
kiosk-demo-getgrocery |
Grocery delivery | active |
kiosk-demo-atablefor |
Restaurant table-booking — non-commerce (no payments) | active |
kiosk-demo-hoteling |
Hotel booking | active |
kiosk-demo-skooti |
Scooter rental | active |
kiosk-demo-stylish |
Salon/appointment booking (Combette on Park) — non-commerce (no payments) | active |
kiosk-demo-philslist |
Classifieds board — non-commerce (no payments) | active |
kiosk-demo-tudu |
Collaborative todo — non-commerce (no payments) | active |
kiosk-demo-proveli is not an operator. It is the KYC broker: a service outside
the operators. A demo connects to it to confirm facts about a customer, for
example an age over 18 or a driving licence. It serves no Kiosk operation, and it
deploys at kyc.demo.kiosk.tech.
e2e/ is not in that table, because it is not an eighth operator. It is the
end-to-end test harness — fixtures, a stripe-mock-backed pay flow and the AI-assistant drivers —
so it serves no vertical and deploys nowhere. It runs as a CI gate
(./e2e/run.sh) and nothing else.
Four more top-level directories are neither a gem nor a demo. bin/ holds
the checks that gate this repository — one check-* script per property,
each exiting non-zero when that property breaks; CI runs them as jobs of
their own, not through rake. deploy/ is the runbook for hosting the
live demo fleet — Caddy, Postgres, systemd and the scripts that provision and
smoke-test the box (deploy/README.md). spec/ holds three GENERATED
indexes the publication and conformance checks read and regenerate — not a
test suite; each gem's own tests live under its own spec/. .github/
holds the one CI pipeline, workflows/ci.yml — the gems matrix, the demos
matrix and e2e/run.sh, on every push and pull request.
Each demo's tests drive it over HTTP the way an assistant does; CI runs them
with bin/rails test (bundle exec rspec in hoteling).
Four of the demos — atablefor, getgrocery, hoteling, skooti — also
carry a before-after.md: a contrast between what an AI assistant can do at
that business today and what the same errand looks like once Kiosk is
installed, followed by the operator-side adoption recipe. Four rather than all
seven operators is deliberate. Every fenced block in one declares what produced it. The
rest say it shorter: philslist carries an inline Before / after section in
its README; stylish and tudu carry neither.
The demos are standalone Rails apps, so a helper two of them need is COPIED, not
shared. bin/check-demo-copies — its own CI job too — declares every
hand-written Ruby file that exists in more than one demo, plus .gitignore, as
kept identical, kept identical apart from its header prose, or a deliberate
per-demo variant, each with the reason; it fails the build when copies that must
agree stop agreeing, and when a new duplicate turns up undeclared. Header prose
may vary, but a magic comment may not: # frozen_string_literal: true changes
how the file runs, so it is compared. The Rails skeleton each demo edits for its
own port and host is out of scope on purpose, and that exclusion is recorded
file by file, with its reason, in the same script.
Everything in this repo consumes the gems by path:, which serves the working
tree — so nothing here can tell a file that EXISTS from a file that is
PACKAGED. A file outside lib/ that a gem reads at runtime must be in that
gem's spec.files, or the published gem does not carry it.
Run the fleet in containers
docker compose up at the repository root runs the seven operator demos, the
KYC broker, stripe-mock with its relay and one Postgres, on the ports each demo
README names. All containers share one network, so the apps reach each other on
127.0.0.1 as they do on the deployed box. Payment and the identity check work.
A demo's battery runs against it from the host, for example in
kiosk-demo-skooti:
SERVER_URL=http://localhost:3004 bundle exec ruby ../kiosk-redteam/suites/skooti.rb
What still differs from the deployed box, and why:
- The apps run in Rails development mode. Production mode needs the secrets the box keeps in its env files (see "The secrets of production mode" below) and sends its session cookie only over TLS.
- No TLS: each origin is
http://localhost:<port>. The box serves HTTPS through Caddy. - Each start resets every database and makes a new signing key, so assistant registrations and tokens do not survive a restart. The box migrates and keeps both.
- Payment goes to stripe-mock. The box charges Stripe in test mode, which needs a Stripe key.
- The KYC broker signs with the development key this repository tracks. The box's broker has its own key.
The secrets of production mode
deploy/env/<app>.env.example names each variable of an app. On the box,
deploy/rollout.sh makes each missing value that nothing off the box holds. For
a value that someone else holds it prints MISSING, and the operator supplies
it: deploy/README.md lists these
values. To make a value by hand:
| Variable | What it is | How to make it |
|---|---|---|
SECRET_KEY_BASE |
The Rails key that signs and encrypts cookies | bin/rails secret |
KIOSK_<APP>_DB_PASSWORD |
The password of the app's Postgres role, the same value that deploy/postgres-init.sql sets |
openssl rand -hex 32 |
KIOSK_SIGNING_KEY_B64 |
The Ed25519 key that signs access tokens, checkouts and receipts | openssl genpkey -algorithm ed25519 | openssl base64 -A |
KIOSK_POW_SECRET |
The key that signs each proof-of-work challenge, 32 bytes or more | openssl rand -hex 32 |
KIOSK_PROVELI_INTAKE_SECRET |
The secret an operator and the KYC broker share. The broker keeps the same value as KIOSK_PROVELI_<OPERATOR>_SECRET, for example KIOSK_PROVELI_SKOOTI_SECRET
|
openssl rand -hex 32, then set the value on both sides |
PROVELI_KEY_PEM |
The broker's Ed25519 key that signs each attestation | openssl genpkey -algorithm ed25519 |
KIOSK_PROVELI_PUBLIC_KEY_PEM |
The public half of the broker's key, which an operator uses to verify an attestation | openssl pkey -in proveli_key.pem -pubout |
STRIPE_SECRET_KEY |
The Stripe key of an operator that takes payment, in test mode | Stripe Dashboard, Developers, API keys, with "Test mode" on: the secret key that starts with sk_test_
|
KIOSK_UNLOCK_SIGNING_KEY_PEM |
skooti only: the Ed25519 key that signs each unlock token for a scooter lock | openssl genpkey -algorithm ed25519 |
Contributing
CONTRIBUTING.md is the whole of it: how to run one gem,
one demo, the end-to-end harness, bin/lint (RuboCop, bundler-audit, Brakeman)
and the bin/check-* checks; what the merge
gate is; and what this repository expects of a changelog entry and a comment.
A security flaw goes through a GitHub issue — read
SECURITY.md first, for which repository takes it and for the
fact that filing one is public disclosure.
Before 1.0 every gem carries the protocol's full version — the version parity
the spec promises (protocol §14.1): the
protocol, every gem here and the published skill read the same MAJOR.MINOR.PATCH,
and a new cut of any one of them moves all of them. From 1.0 they share
MAJOR.MINOR. The number is Kiosk::Protocol::API_VERSION, and the ~> MAJOR.MINOR.0
inter-gem constraints, the lockfiles, kiosk-server's skill_url and every demo's
skill pin test follow it. A release is a commit on main that moves every
gemspec to the new version and gives each changelog the version heading. The
maintainer then builds each gem from its *.gemspec and pushes it to RubyGems.
No git tag marks a release.
The two development keypairs this repo tracks on purpose
kiosk-demo-skooti/config/dev_unlock_key.pem (Ed25519) and
kiosk-demo-proveli/config/dev_proveli_key.pem (Ed25519) are private keys, they are
committed, and that is deliberate. Read the header of either file: it says what
the key is for, that it is world-readable, and that it must never sign
anything real. Both applications enforce that rather than asking — production
refuses to boot without an explicit key in the environment, and refuses again
if the key it is given is the shipped one, compared on the public half in DER
so a re-serialised copy cannot slip past.
The skooti key, dev_unlock_key.pem, cannot be generated per machine: it is a
KNOWN-ANSWER VECTOR.
Its public half and one signature are reproduced in the lock firmware, in the C
host test that firmware ships with, and in the demo's own known-answer test, and
the crosscheck target signs a live message with the private half for the C
verifier to check. A per-machine key turns all of that red on a fresh clone.
Do not read this as licence to commit a key. A scanner pointed at this repo
will flag both files, and that is the correct behaviour — which is why
.github/secret_scanning.yml names exactly these two paths and nothing else.
bin/check-publication-paths fails on any OTHER tracked private key, and on
either of these two if it ever loses its do-not-use banner; it re-proves that
detector against a freshly minted key on every run, so it cannot rot into a
gate that quietly matches nothing. And note the part that no later commit
repairs: this project does not rewrite published history, so a key that reaches
a pushed commit stays reachable in that history whatever a subsequent commit
deletes. Generate at setup or read from the environment; if a fixed vector is
genuinely unavoidable, declare it in that script with the reason.
License
Apache-2.0 for every gem in this repo. See each gem's LICENSE.txt.
Every Kiosk gem lives in this repo and under that licence: ls */*.gemspec is the whole list, and each one declares spec.license = "Apache-2.0".
Links
- kiosk.tech — landing page + AI-assistant skill
- kiosk.tech/skill.md — universal AI-assistant skill
- ROADMAP.md — what 0.6 ships, what comes next, what is out of scope
- Issue tracker