Skip to main content
Version: Target · roadmap

Asking for authentication

A skill can be reserved to an authenticated person. The model does not guess it, and does not ask for a code out loud: Core knows it from the config, announces it, and asks itself, at the right moment, with the right flow for the surface.

Nothing new is written for this. An authentication request follows from what the station already declares: the grants and permissions, the profiles, the providers and the provider each portal offers.

When Core asks​

Core asks for authentication when two conditions hold:

  1. the current identity does not pass a rule: a missing grant, a permission, a require;
  2. a proof this portal can run could open it. For example, the rule reads 'email' in identity.verified, and the portal offers a provider that verifies: email.

If no proof possible on this portal can open the rule, Core asks nothing: it is a refusal, or a hidden item. Nobody is made to prove an e-mail for nothing.

If the proof is made but is not enough (the person is verified, but not premium), the rule refuses as usual, and the request is not repeated.

The moment depends on the phase​

the rule bears onCore askswhat the person sees before
invoke (or the grant of the operation read)before the call: the skill does not runnothing of the skill
present (a reserved kind)before the call, as for invokenothing of the skill; they may continue without, and then see what is not reserved
select (choosing a kind)when they choose the itemthe items, including the one they want
submit (the grant of the write, submitted_by: owner)when they submittheir whole selection

For present, the request is decided on the config, not on the result: it says the skill has a reserved part, never that this search found something hidden. Reserved items are still removed before the model, and counts are made after the filter (Permissions).

The call resumes by itself​

Once the proof is made, Core runs the pending call again, with the same arguments: the search, the choice, or the submit. The person has nothing to ask again.

  • A room has one pending request; a new one replaces the old.
  • It expires with the request: an abandoned call is not run an hour later.
  • The re-run goes through every rule again: the proof opens access, it does not force it.

The label, written by Core​

A reserved skill is announced as such, to the model and on screen, on both doors. Core writes the label from the rules; the tenant does not write it, so it cannot lie.

phaselabel
invoke"Reserved: prove your e-mail to use it."
present"Part of it is reserved to customers who proved their e-mail."
select"Prove your e-mail to choose this option."
submit"Prove your e-mail to submit."

The name of the data comes from the portal's provider (verifies:); the text comes from Core's text catalogue, in the tenant's language.

The flow: an authentication playbook​

A request is shown like a playbook: steps on screen, one after the other, then what follows. It is not a playbook of the config — the tenant does not write it, and it does not count among its playbooks — but it reads and displays the same way. Its steps depend on the provider's mechanism.

otp — three steps.

  1. The data. "Your e-mail". Pre-filled when the room already holds it (declared by a form); the person confirms or corrects it. It is written in its one spelling (e-mail lower-cased, phone in international form) before sending.
  2. The code. "Enter the code sent to a***@mail.com." With "Send again", and the tenant's own sentence when it refuses (too many codes asked, code expired). Convoy sets its own limits, per datum and across both doors at once: 5 wrong codes block the tries for 15 minutes, and at most 5 codes are sent per hour. A new code resets the tries, not the sends.
  3. Done. "Your e-mail is verified." Then the pending call resumes.

oidc — one step. A "Sign in with …" button opens the provider's page. On return, it is done, and the call resumes.

assertion — one step, or none. If the person is signed in on the tenant's site, they are already verified: nothing shows. Otherwise, a link to the sign-in page the tenant declares; Convoy never invents an address on its domain.

By surface​

The steps are the same everywhere; where they show depends on the surface. In the widget and the PWA, in the frame. On Claude and ChatGPT, in the host's sign-in window, which leads to the tenant's Convoy page; the host runs the call again itself. On WhatsApp, in the conversation, led by Core. See The authentication flow.

The model never starts a proof where the surface can show the steps, and it sees neither the datum nor the code. It only receives "the person is signing in; wait", then the result of the call run again. Where the surface has neither a frame nor a sign-in window (WhatsApp), Core leads the steps, not the model.

Authentication belongs to whoever speaks on this surface (their Claude account, their browser, their number), not to the room: a room's code gives no rights.

What loading checks​

The permission graph checks requests too:

  • a rule that no provider of the station can satisfy ('phone' in identity.verified with no provider that verifies: phone) is refused: the request would be impossible everywhere;
  • a skill exposed on a portal whose provider cannot open its rule gives a warning: on that portal, the skill is simply unavailable, with no request;
  • an assertion provider used by a request must declare its sign-in page.

Example​

Anonymous visitors see the grocery. Proving one's e-mail gives the order history, and the products reserved to customers.

station:
identity:
providers:
email: {mechanism: otp, verifies: email, via: tenant,
send: sendVerificationCode, check: checkVerificationCode}

profiles:
anonyme:
when: "identity.authenticated == false"
grants: [boutique.searchProducts, boutique.createOrder]
client:
when: "'email' in identity.verified"
grants: [boutique.searchProducts, boutique.createOrder, boutique.listOrders]

skills:
- name: commandes # listOrders: only `client` received it → request at invoke
invoke: {method: listOrders}
- name: produits
invoke: {method: searchProducts}
kinds:
reserve: # request before the call; "continue without" shows the rest
when: "$fields.tier == 'client'"
present: {guards: [{permission: {profiles: [client]}}]}
present:
fields:
name: {from: name, type: text, role: title}
tier: {from: tier, type: text}

agent:
portals:
web:
widget: {identity: {auth: email}}

An anonymous visitor asks for their orders. The model calls commandes; Core does not run it and shows the flow: the e-mail, the code, "verified". Then commandes resumes and shows the history. The model saw neither the address nor the code.

If reserve required a subscription ('abonne' in identity.plans), an e-mail code could not open it: Core would ask nothing, and the reserved products would stay hidden.