app/worker/migrations/0006_identity_is_username.sql
zzmq.dev@gmail.com/zz / app / worker / migrations / 0006_identity_is_username.sql
@ 9b4341e0
Download
Ladder Summary
- file This migration drops the legacy tables that stored separate opaque `id` columns for agents, repos, sessions, tokens, and roles, then recreates the entire schema using human‑readable `username` (for agents) and `slug` (for repos) as the primary keys, eliminating the duplicated identifier bug. It defines new tables (`agents`, `agent_identities`, `agent_tokens`, `sessions`, `repos`, `repo_roles`, `cli_login_sessions`) with appropriate foreign‑key constraints, indexes, and uniqueness rules that rely on these immutable, display‑layer identifiers. The file does not export any functions or classes.
-- Drops the separate opaque `id` on both agents and repos -- per
-- direct instruction, after tracing through why it existed: `id`
-- predates `username`/`slug` entirely (the only identifier when
-- agents were first modeled), and `username`/`slug` got bolted on
-- later purely as a *display* layer rather than replacing it. Once
-- both are unique (DB constraint) and immutable (frozen at creation,
-- never recomputed -- migration 0003's whole point for usernames;
-- slugs already worked this way from the start), there's no remaining
-- reason for a second id behind them. Having both was exactly the
-- "two sources of truth for the same thing" bug class this session
-- kept finding (the stale "groq-test" principal, the agentId-vs-
-- username mixup setting up a manual credentials file) -- so instead
-- of migrating the old ids forward, this drops and recreates
-- everything, same as migration 0002 did for the same reason:
-- "every row here is this session's own test/verification data."
-- Existing Artifacts repos (shoes, numkit) are deleted out of band
-- (wrangler artifacts repos delete) since their committed
-- roles.json/meta.json reference the old agent ids directly and
-- there's nothing worth preserving in them.
DROP TABLE IF EXISTS repo_roles;
DROP TABLE IF EXISTS repos;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS agent_tokens;
DROP TABLE IF EXISTS agent_identities;
DROP TABLE IF EXISTS cli_login_sessions;
DROP TABLE IF EXISTS agents;
CREATE TABLE agents (
username TEXT PRIMARY KEY, -- a human's real email; a managed agent's "<creator's username>[<name>]" (accountsDb.ts's computeUsername)
display_name TEXT NOT NULL,
email TEXT,
created_at TEXT NOT NULL,
created_by TEXT REFERENCES agents(username) -- provenance only, nullable (a human's first login has no creator)
);
CREATE TABLE agent_identities (
id TEXT PRIMARY KEY, -- an identity row has no natural key of its own besides (provider, provider_subject), which is already the real uniqueness constraint below -- this stays a plain surrogate key since nothing else ever references an identity row by id
agent_username TEXT NOT NULL REFERENCES agents(username),
provider TEXT NOT NULL CHECK (provider IN ('cloudflare', 'google')),
provider_subject TEXT NOT NULL,
access_token_enc TEXT,
refresh_token_enc TEXT,
token_expires_at TEXT,
created_at TEXT NOT NULL,
UNIQUE (provider, provider_subject)
);
CREATE INDEX idx_agent_identities_agent ON agent_identities(agent_username);
CREATE TABLE agent_tokens (
id TEXT PRIMARY KEY,
agent_username TEXT NOT NULL REFERENCES agents(username),
token_hash TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
revoked_at TEXT,
last_used_at TEXT
);
CREATE INDEX idx_agent_tokens_agent ON agent_tokens(agent_username);
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
agent_username TEXT NOT NULL REFERENCES agents(username),
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL
);
CREATE INDEX idx_sessions_agent ON sessions(agent_username);
CREATE TABLE repos (
slug TEXT PRIMARY KEY, -- "<owner username>/<repo name>" -- frozen at creation (doCreateRepo), same immutability argument as agents.username
artifacts_name TEXT NOT NULL UNIQUE, -- NOT redundant with slug: this is the key into Cloudflare's own Artifacts namespace, a different system, deliberately decoupled from the slug
description TEXT,
owner_username TEXT NOT NULL REFERENCES agents(username),
visibility TEXT NOT NULL DEFAULT 'public' CHECK (visibility IN ('public', 'private')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX idx_repos_updated ON repos(updated_at DESC);
CREATE INDEX idx_repos_owner ON repos(owner_username);
-- D1-side cache of each repo's roles.json grants (source of truth
-- stays the zz-meta branch) -- see migration 0004's original header.
CREATE TABLE repo_roles (
repo_slug TEXT NOT NULL REFERENCES repos(slug),
principal TEXT NOT NULL, -- an agent's username -- same field, same meaning, just no longer a separate opaque id
role TEXT NOT NULL CHECK (role IN ('owner', 'maintainer', 'contributor', 'reader')),
granted_by TEXT NOT NULL,
at TEXT NOT NULL,
PRIMARY KEY (repo_slug, principal)
);
CREATE INDEX idx_repo_roles_principal ON repo_roles(principal);
-- `zz login`'s browser<->CLI bridge -- unchanged shape, just points at
-- a username instead of an id now.
CREATE TABLE cli_login_sessions (
nonce TEXT PRIMARY KEY,
agent_username TEXT REFERENCES agents(username),
token TEXT,
created_at TEXT NOT NULL,
consumed INTEGER NOT NULL DEFAULT 0
);