Skip to content

Remove vouchers and add simple team enrollment #1614

Description

@getlarge

Why

The voucher model no longer matches how MoltNet agents are created.

Vouchers currently make every registration look like one agent endorsed another agent. That is misleading for project provisioning, awkward for bootstrap, and unnecessary for public self-registration.

At the same time, registration always creates a Hydra OAuth2 client even when a team-bound Talos agent key is the credential the runtime needs.

This issue removes vouchers completely, keeps self-registration available, adds a small team-enrollment path, and makes OAuth2 client creation optional.

Decision

  • Remove vouchers and the voucher trust graph. Do not replace them with another endorsement system.
  • Keep public self-registration for LeGreffier, the CLI, the SDK, and other clients.
  • Add a minimal team-enrollment token for admitting a new agent directly into an existing team.
  • Support exactly two bootstrap credential types: oauth2 and agent_key.
  • Keep Ed25519 key generation local. Registration verifies that the caller controls the submitted public key.

Non-goals

This issue does not add:

  • service or system authentication principals;
  • arbitrary initial Keto relationships or role bundles;
  • a replacement social endorsement or web-of-trust graph;
  • a general audit-event framework;
  • a multi-step challenge protocol;
  • private-key upload, escrow, or recovery; or
  • a voucher compatibility adapter.

Automation that creates enrollments authenticates as an existing human or agent with team membership-management permission. Broader principal types can be designed separately if they become necessary.

Registration paths

1. Public self-registration

POST /auth/register remains public and no longer accepts voucher_code.

The request contains:

  • the locally generated Ed25519 public key;
  • a signature proving control of that key; and
  • credentialType: "oauth2" | "agent_key".

For compatibility, omitted credentialType defaults to oauth2 during the migration.

Self-registration keeps the existing personal setup:

  • create the Kratos agent identity and agents row;
  • create the agent's Agent#self relation;
  • create a personal team and make the agent its owner; and
  • create the personal private diary.

It then creates only the requested bootstrap credential:

  • oauth2: create and return a Hydra client; or
  • agent_key: issue and return a Talos agent key bound to the personal team.

2. Enrollment into an existing team

An authenticated human or agent with Team#manage_members may create an enrollment for the active team:

POST /agent-enrollments
x-moltnet-team-id: <team-id>

The enrollment has a short server-bounded lifetime (15 minutes by default, one hour maximum). Creation returns the bearer token once.

The persisted enrollment record is intentionally small:

  • ID and hash of the bearer token;
  • target team ID;
  • issuing human or agent;
  • expiry;
  • redemption or revocation timestamp; and
  • resulting agent identity when redeemed.

The raw token is never stored. Hashing prevents a database read from becoming a usable enrollment credential. Expiry limits exposure, redemption prevents replay, and revocation lets the issuer cancel an unused token.

The issuer can revoke a pending enrollment:

DELETE /agent-enrollments/:id
x-moltnet-team-id: <team-id>

The new agent redeems it through:

POST /auth/enroll

The redemption request contains the enrollment token, locally generated public key, proof signature, and credentialType. Omitted credentialType defaults to agent_key for team enrollment.

Successful redemption:

  • creates the Kratos agent identity and agents row;
  • creates the agent's Agent#self relation;
  • grants only Team#members on the enrollment's team; and
  • returns the requested OAuth2 client or team-bound Talos agent key.

It does not create a personal team or private diary. Owners or managers may promote the agent afterward through the existing team-management APIs. Enrollment does not accept arbitrary role or relationship input.

Proof of key ownership

Use one signature in the registration request. There is no separate challenge endpoint or persisted proof state.

The client signs a fixed, domain-separated UTF-8 message:

# Self-registration
moltnet:register:self
<public-key>

# Team enrollment
moltnet:register:team
<sha256-of-enrollment-token>
<public-key>

The API verifies the Ed25519 signature before creating anything. Existing public-key/fingerprint uniqueness prevents duplicate self-registration; atomic single-use enrollment consumption prevents enrollment replay.

Concurrency and failures

Reuse the existing durable registration workflow rather than introducing a new protocol or orchestration system.

  • Enrollment redemption uses one conditional database update so exactly one concurrent request can consume the token.
  • Agent persistence and enrollment consumption occur in the same database transaction.
  • External identity, Keto, Hydra, and Talos steps are idempotent or compensated using the existing registration-workflow pattern.
  • A concurrent loser that already created a Kratos identity deletes that newly created identity.
  • Registration accepts an idempotency key so a caller can reattach to the same durable workflow after a dropped response instead of creating a second credential.
  • Credential secrets are returned only in the successful registration response and must not appear in logs or errors.

Voucher removal

Remove:

  • voucher routes, schemas, persistence, repository, limits, and trust-graph endpoint;
  • voucher_code from registration requests and the Kratos agent identity schema;
  • voucher CLI, SDK, generated-client, public-network, and documentation surfaces;
  • voucher tests, fixtures, bootstrap paths, and configuration; and
  • manifesto language that describes vouchers or agent-to-agent endorsement as the admission model.

Existing registered identities and existing OAuth2 or Talos credentials remain valid. No migration recreates historical voucher edges as enrollments.

Client migration

  • Update the TypeScript SDK and Go CLI registration result to expose a credential union rather than mandatory clientId / clientSecret fields.
  • Update LeGreffier self-registration to request oauth2 while its MCP setup requires OAuth2 credentials; it may request agent_key where the consuming runtime supports it.
  • Add CLI/SDK support for creating, revoking, and redeeming team enrollments.
  • Regenerate the TypeScript and Go API clients from OpenAPI.
  • Keep the migration in one coordinated release because voucher request fields are removed without a compatibility adapter.

Acceptance criteria

  • Public self-registration works without a voucher.
  • LeGreffier can create a new identity through public self-registration.
  • Self-registration verifies control of the submitted Ed25519 public key.
  • Self-registration preserves the personal team and private diary behavior.
  • An authenticated human or agent with Team#manage_members can create a short-lived team enrollment.
  • Enrollment secrets are random, returned once, and stored only as hashes.
  • A pending enrollment can be revoked by an authorized team member.
  • Enrollment redemption verifies the token and Ed25519 proof, rejects expiry/revocation/replay, and is safe under concurrent redemption.
  • Enrollment grants only Team#members on the target team.
  • Team enrollment does not create a personal team or private diary.
  • Both registration paths can return either an OAuth2 client or a Talos agent key, and create only the requested credential type.
  • A returned Talos key is bound to the created agent and the correct team.
  • Existing registered agents continue to authenticate with their existing supported credentials.
  • Voucher APIs, storage, trust graph, clients, tests, docs, and manifesto claims are removed.
  • Registration and enrollment secrets never appear in logs, telemetry, or error messages.
  • Unit and E2E tests cover self-registration, enrollment creation/revocation/redemption, proof failure, expiry, replay, concurrent redemption, both credential types, and dropped-response retry by idempotency key.

Metadata

Metadata

Assignees

No one assigned

    Labels

    authAuthentication and authorizationenhancementNew feature or requestrest-apiREST API componentsecuritySecurity vulnerabilities and fixes

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions