Format-aware TODO item tools
Lossless item edits, exact-document authority and original-base UI writes for zudo-text TODO documents (#7487).
This is an unreleased source capability. It reuses the restricted connection principal, grant revision, key lease and PAT machinery described in Restricted Kanban external access, adding a separate TODO resource family. It does not change Kanban grants.
Document format and the lossless planner
The tools operate only on existing zudo-text TODO documents: type: todo frontmatter (legacy todo: true is read but never rewritten), at most one ## TODO and one ## DONE section, - [ ] / - [x] item lines with an optional <!-- id: <12 base32> --> identity, and two-space-indented sub-lines (an optional first notify: line, then the body).
packages/ is the only code path the tools use. It does not round-trip source through serializeTodoMarkdown, which reorders frontmatter, drops legacy keys and re-emits sections:
readTodoSource(source)analyzes the bytes, cross-checks every item againstparseTodoMarkdown, and returns items with their persistedidStableornull. It never assigns identity or produces content.planTodoCommand(source, command, allocation)splices only the targeted item's bytes (or appends a new item), re-analyzes the result, and proves the frontmatter, preamble, extra sections and every untouched item are byte-identical. Any failure throws aTodoSourceErrorwith no output.
Refused with zero writes: CRLF, a non-blank line inside TODO/DONE that is not an item or indented sub-line (loose prose, * bullets, tab indentation), a checked item in TODO or an open item in DONE, duplicate TODO/DONE headings, duplicate identities, and a body whose first line the parser would promote into a notify: spec. Commands: create (append to TODO, one allocated identity), update (explicit title/body), set_done (move between sections; the current state is a no-op), reorder (within the item's section) and initialize_ids (append identities only where missing). There is no delete or archive.
Authority
A connection grant row (kanban_grants) carries exactly one resource family: Kanban scopes, or todo:read / todo:write. Mixed grants never authorize. todo_document_grants binds a grant to exact files by file_id, an HMAC of the exact path in its own domain (zudo-todo-location-v1), and the file lifecycle revision from kanban_manifest_lifecycles (which tracks every file row), so a move, delete or recreate ends the grant for that document.
todoAuthorityPredicate (in workers/) requires the grant id/user/client/workspace/revision, active status, unexpired grant and admission, the workspace encryption generation, the exact binding and lifecycle, and TODO-only scopes. It is evaluated at admission, embedded in the operation reservation and in the final commit statement, and re-checked before returning any read or replay. Generic documents, search, history, checkpoints, assets and agent:invoke stay unreachable: hasScope grants a restricted principal nothing outside its resource scopes, and full PATs cannot carry todo:*.
The lease holds the whole workspace's keys in Durable Object memory. The per-document limit is server authorization, not file-level cryptographic isolation. Keys never reach the model, and OAuth refresh never extends a lease.
Mutation commit
executeTodoCommand derives a scope key from the grant/document lifecycle and the caller's idempotencyKey, and a fingerprint from the expected revision and command. A repeated key returns the stored receipt after re-authorization; the same key with another payload fails with IDEMPOTENCY_CONFLICT. The command is validated before reservation; created identities are derived from the reserved operation id, so a replay allocates the same id.
One D1 batch inserts the receipt only when the authority predicate, the exact observed head (id, path, version, hash, size, lifecycle) and the staged blob all hold; the head update, file_versions row and changes row are guarded by that receipt's admission nonce. A no-op command commits a receipt without a new version. Receipts contain no titles, bodies or paths. The revision is whole-document: concurrent edits of different items conflict and must be reread.
Original-base UI writes
The TODO board writes through makeMarkdownFileProvider's opt-in revision seam (packages/). The base is captured from workspaceFiles.snapshot together with the exact text it describes — at load and on each local acceptance — and every debounced window is written with that base and expectedContent, never a base re-captured at flush. The captured outbox entry keeps the original base through offline, relaunch and reconnect. A conflicting source is retained for Review changes by the workspace store; the board then stops writing behind a Reload banner. Providers that do not opt in (mindmap) keep the plain read/write contract.
Human edits still serialize the whole document with serializeTodoMarkdown; only the tools are byte-preserving.
Settings connection surface
Settings → External Connections renders TodoConnectionsPanel (tauri-) as its own list below the Kanban connections. It talks to the management routes through TodoConnectionClient (@takazudo/), wired in tauri- onto the same guarded first-party session as the Kanban client (beginConnectionSession), so the account, workspace, encryption-generation and backend checks are shared. The first-party clients live next to the Kanban one in kanban-api rather than in backend-bridge, because they derive raw keys and must stay out of any tool registry.
Document picker: the client decrypts catalog paths locally and offers Markdown files (no
KANBAN.md, nothing under.zudotext/). On Connect, each selection goes throughPOST /todo/selection, which decrypts the file and refuses anything without TODO frontmatter. The UI names the refused file. A changed lifecycle revision is reported as changed, never silently re-bound.Scopes: read-only by default; edit permission (
todo:write) is an explicit checkbox. Connect starts locked. Unlock reserves a revision first and locks it again if enrollment fails.Validity:
GET /todo/connectionsreportsvalidper document. A moved, deleted or recreated file shows as Invalid; Unlock and token creation are disabled and the lease is never reported as unlocked.Tokens: a minted token is held only in component state while the token form is open and is cleared on Done/close.
Each grant card has a designed context menu (Lock, Disconnect, Refresh, Help).
Remote OAuth
TODO OAuth reuses the Kanban issuer, provider, token endpoint, consent pages, kanban_grants and kanban_oauth_bindings. workers/ declares one protected resource per connection family — Kanban at /, TODO at / — and resourceFamily() maps an exact canonical resource to its family. An omitted resource still defaults to Kanban (defaultResource), so existing Kanban clients are unchanged.
The token audience is the only family selector, and every other input must agree with it:
Authorize.
/derives the family from the provider-parsed request's canonicaloauth/ authorize resource. All requested scopes must belong to that family; a mixed set, a foreign set, or TODO scopes on the defaulted Kanban resource fail before a consent handle exists. The page rendersdata-family, which only chooses the browser flow.Consent.
/consumes the single-use, browser-bound handle, then re-derives the family from the approved request — never from the body — and parses the body with that family's schema. A TODO consent callsoauth/ consent createTodoGrantwith exactdocumentSelectionsfrom/(oauth/ todo/ selection getTodoDocumentSelection); submitted scopes must be a subset of the requested ones. The grant starts locked. IfcompleteAuthorizationfails, the grant is revoked withchangeTodoGrant; the browser's late-cancel compensation uses the revoke-onlyPATCH /, and neither family's revoke route accepts the other family's grant.oauth/ todo/ connections/ : grantId Token issue and refresh.
tokenExchangeCallbackrequires the audience's family to equal the bound grant's family and the OAuth grant's scopes to be that family's, so a forged cross-family binding never produces a token. Refresh narrows scopes only, and never unlocks, renews a lease or changes the selected documents. RFC 7009 revocation uses the sharedkanban_grantsbarrier and lease disposal.Resource.
authenticateTodovalidates the TODO audience, requires the bound grant and the token scopes to be TODO-only, and narrows the principal's scopes to the token's.authenticateKanbandoes the same for Kanban, so neither token family authenticates on the other's routes. Restricted TODO PATs are unchanged.401and403responses name the TODO protected-resource metadata inWWW-Authenticate.
Explicit unlock stays first-party: an OAuth-created TODO grant appears in Settings → External Connections and is unlocked there.
Routes
Tools:
POST /and Streamable HTTP MCP atapi/ v1/ kanban/ todo/ tools/ <tool_ name> /, both authenticated by a TODO-only restricted PAT or a TODO-audience OAuth access token.api/ v1/ kanban/ todo/ mcp OAuth: protected-resource metadata at
/; consent helpers. well- known/ oauth- protected- resource/ api/ v1/ kanban/ todo/ mcp POST /andoauth/ todo/ selection PATCH /(revoke only), alongside the sharedoauth/ todo/ connections/ : grantId /,oauth/ authorize /andoauth/ consent /.oauth/ token First-party management (interactive sessions only):
/andapi/ v1/ kanban/ todo/ selection /.api/ v1/ kanban/ todo/ connections[. . . ] Local stdio:
ZUDOTEXT_MCP_PROFILE=todoin@takazudo/zudotext-mcp.