Skip to content

Commit d3f533e

Browse files
committed
fix(api): enforce one leader per runtime
1 parent ec7df3a commit d3f533e

19 files changed

Lines changed: 81 additions & 10 deletions

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Agent Kanban is that workspace. Every agent gets an Ed25519 identity — a crypt
2828

2929
```
3030
Human talks to an agent runtime (Claude Code, Codex, Gemini CLI)
31-
Agent auto-registers as a leader via `ak` CLI
31+
Leader agent uses `ak` with its own identity
3232
→ Leader breaks the goal into tasks and assigns to workers
3333
→ Daemon dispatches workers, each in its own worktree
3434
→ Workers claim, implement, and open PRs
@@ -116,7 +116,15 @@ The `-g` flag installs globally so the skills are available across all your repo
116116

117117
### 4. Use your agent runtime
118118

119-
Open any agent runtime (Claude Code, Codex, Gemini CLI) in a repo. The first `ak` call auto-registers the runtime as a leader agent with its own Ed25519 identity. Use the installed skills to manage your AI team:
119+
Open any agent runtime (Claude Code, Codex, Gemini CLI) in a repo.
120+
121+
A leader agent can create its own identity:
122+
123+
```bash
124+
ak identity create --username alex --name "Alex Chen"
125+
```
126+
127+
After that, `ak` reuses that leader identity across sessions for the same runtime. Then use the installed skills to manage your AI team:
120128

121129
- **`/ak-plan v1.0 <goals>`** — analyze the codebase, create a board with tasks and dependencies, assign to agents
122130
- **`/ak-task fix the login redirect bug`** — create a single task, assign it, monitor → review → merge
@@ -127,7 +135,8 @@ The leader creates and assigns tasks; the daemon picks them up and dispatches wo
127135

128136
Every agent gets a unique cryptographic identity:
129137

130-
- **Ed25519 keypair** — generated per agent spawn
138+
- **Leader identity** — created explicitly once per runtime, then reused across sessions
139+
- **Ed25519 keypair** — generated per agent session
131140
- **Fingerprint** — derived from the public key
132141
- **Identicon** — visual representation of the fingerprint
133142
- **JWT auth** — agents sign their own tokens, verified server-side
@@ -178,6 +187,10 @@ Task Lifecycle:
178187
task cancel <id> Cancel a task
179188
task release <id> Release back to todo
180189
190+
Identity:
191+
identity create Create a leader identity for the current runtime
192+
whoami Show the current runtime's agent identity
193+
181194
Output:
182195
-o json|yaml|wide Output format (default: text table)
183196
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Enforce a single leader per owner/runtime pair while allowing many workers.
2+
CREATE UNIQUE INDEX idx_agents_owner_runtime_leader ON agents(owner_id, runtime)
3+
WHERE kind = 'leader' AND runtime IS NOT NULL;

apps/web/server/routes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,14 @@ api.post("/api/agents", async (c) => {
421421
// Validate username uniqueness before GPG mutation
422422
const taken = await c.env.DB.prepare("SELECT 1 FROM agents WHERE username = ?").bind(body.username).first();
423423
if (taken) throw new HTTPException(409, { message: `Username "${body.username}" is already taken` });
424+
if (body.kind === "leader") {
425+
const existingLeader = await c.env.DB.prepare("SELECT 1 FROM agents WHERE owner_id = ? AND runtime = ? AND kind = 'leader'")
426+
.bind(ownerId, body.runtime)
427+
.first();
428+
if (existingLeader) {
429+
throw new HTTPException(409, { message: `Leader agent for runtime "${body.runtime}" already exists` });
430+
}
431+
}
424432

425433
// GPG subkey — its Ed25519 material becomes the agent's unified key
426434
const email = agentEmail(body.username);

docs/designs/agent-kanban-v1.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,15 @@ The skill must explicitly document that agents can **create tasks** — not just
211211
| Shared types | Proper workspace package with build step | Works with CF bundler + npm publish |
212212
| Skill location | `packages/skill/` in pnpm workspace | Version-controlled with CLI, install script copies to `~/.claude/skills/` |
213213
| Claim atomicity | `db.batch()` for atomic claim | Prevents race condition on concurrent /claim |
214-
| Agent identity | API key = Machine (not Agent). Agents auto-register on claim. | One key per computer, zero per-agent config. Forward-compatible with v2 roles. |
214+
| Agent identity | API key = Machine (not Agent). Leader identity is created explicitly, then reused by runtime. | One key per computer with stable per-runtime agent identity. |
215215

216216
### Identity & Auth Model (revised)
217217

218-
The original design had one API key per agent. Revised to Machine-level auth with auto-registered agent instances.
218+
The original design had one API key per agent. Revised to Machine-level auth with an explicitly created leader identity per runtime.
219219

220220
**API key = Machine.** One key per computer. Configured once via `agent-kanban config set api-key`. All agents on that machine share the same key.
221221

222-
**Agent = auto-registered on first claim/create.** No pre-registration needed.
222+
**Leader identity = created explicitly once per runtime.** After that, the CLI reuses the local identity cache and restores the unique server-side leader for that runtime if the local cache is missing.
223223

224224
```
225225
api_keys (represents a Machine)

docs/designs/vision.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,21 +197,21 @@ Agent Instance Layer (execution)
197197

198198
## Identity & Auth Architecture
199199

200-
### v1 (current): Machine-Level Auth + Auto-Registered Agents
200+
### v1 (current): Machine-Level Auth + Explicit Leader Identity
201201

202202
```
203203
api_keys (represents a Machine, not an Agent)
204204
id, key_hash, name (machine name), created_at
205205
206-
agents (lightweight, auto-registered on claim)
207-
id, machine_id → api_keys.id, name (auto-generated), role_id (null), created_at
206+
agents
207+
id, name, username, runtime, kind, created_at
208208
209209
tasks
210210
assigned_to → agents.id (not api_key name)
211211
created_by → agents.id or "human"
212212
```
213213

214-
One API key per Machine. One Machine can have many concurrent agent instances. Agent instances are auto-created when they first claim or create a task — zero manual setup.
214+
One API key per Machine. One Machine can have many concurrent agent instances. Leader identities are created explicitly once per runtime, then reused across sessions. If the local identity cache is missing, the CLI restores the unique server-side leader for that runtime.
215215

216216
### v2: Add Roles
217217

skills/ak-plan/SKILL.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ allowed-tools:
2222

2323
Plan and create a board with tasks — for a new version release or a new product from scratch.
2424

25+
## Identity
26+
27+
This is a leader workflow.
28+
29+
If `ak` says no leader identity exists for the current runtime, create one first:
30+
31+
```bash
32+
ak identity create --username <username> [--name <name>]
33+
```
34+
35+
The leader chooses its own username and optional full name.
36+
2537
## Input
2638

2739
Parse the user's input:

skills/ak-task/SKILL.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ allowed-tools:
1919

2020
Create a task, assign it, then monitor → review → reject/complete.
2121

22+
## Identity
23+
24+
This is a leader workflow.
25+
26+
If `ak` says no leader identity exists for the current runtime, create one first:
27+
28+
```bash
29+
ak identity create --username <username> [--name <name>]
30+
```
31+
32+
The leader chooses its own username and optional full name.
33+
2234
## Input
2335

2436
Parse the user's input:

tests/agent-auth.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ async function applyMigrations(db: D1Database) {
3434
"0014_agent_mailbox_token.sql",
3535
"0015_username_global_unique.sql",
3636
"0016_task_actions_session_id.sql",
37+
"0017_unique_leader_per_runtime.sql",
3738
];
3839
for (const file of files) {
3940
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");

tests/agent-json-fields.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async function applyMigrations(db: D1Database) {
2727
"0014_agent_mailbox_token.sql",
2828
"0015_username_global_unique.sql",
2929
"0016_task_actions_session_id.sql",
30+
"0017_unique_leader_per_runtime.sql",
3031
];
3132
for (const file of files) {
3233
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");

tests/agent-redesign.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async function applyMigrations(db: D1Database) {
4747
"0014_agent_mailbox_token.sql",
4848
"0015_username_global_unique.sql",
4949
"0016_task_actions_session_id.sql",
50+
"0017_unique_leader_per_runtime.sql",
5051
];
5152
for (const file of files) {
5253
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");

0 commit comments

Comments
 (0)