Skip to content
0056 · Local-first factory UX

The factory is driven from the developer's local agent, and a room is as visible as its repo

Status: Accepted Date: 2026-10-08 Deciders: Smana (Platform Owner) Related Spec: Agent factory: local-first developer UX


Context

Developers live in a local coding agent or IDE and will not open a browser room to hand off a side task. The room shows raw events, and one broker serves every repo, where any agents-member reads every room. A room holds the issue, code the agent read, diffs and tool output, so with a second team or a private repo it becomes a way to read a repo around GitHub’s own permissions. Three choices had credible alternatives; the rest of the design did not.


Decision Drivers

  • One artefact every local agent reads, with no new server or auth path
  • Each call visible in the developer’s own transcript
  • A “where are we” view that is exact, cheap and cannot hallucinate
  • One source of truth for who may read a room, where developers’ access already lives

Considered Options

D2: how a local agent talks to the factory

OptionVerdict
Agent Skill (.agents/skills) + roomctlChosen: one open format, one binary, the procedure (draft, file, never label) lives in the skill
A local MCP server (roomctl mcp)Deferred, not in v1: a second server and auth path to run; calls hidden behind a tool boundary; revisit only if a target agent cannot run a shell
No local integration, browser room only (status quo)Rejected: developers will not leave their agent or IDE for a browser to hand off or follow a side task

D3: the room’s top layer

OptionVerdict
Deterministic fold of the room log, plus agents’ progress notesChosen: exact, cheap, testable; the notes carry the why without a second model
An LLM narrating the roomRejected: costs tokens, lags the run, and summarises untrusted agent text
Raw event stream only (status quo)Rejected: too much noise, no “where are we” view, unclear what the developer can do

D7: who may read a room

OptionVerdict
Follow GitHub: readable if and only if the repo is readable; admins bypassChosen: a room is derived from its repo, so it inherits the repo’s permission; no second system to drift
Per-repo groups mirrored in ZITADELRejected: a second permission system to keep in step with GitHub
Every agents-member reads all rooms (today)Rejected: a room leaks a private repo to anyone in the group
Identity source: a github_login token claim (ZITADEL Action over user metadata)Rejected: no Action can read IdP links at token time, and user metadata is writable by machine users for themselves and by user.write holders, so the claim is forgeable
Identity source: the user’s GitHub IdP link, read by the brokerChosen: a link can be added only by authenticating at GitHub, or by an admin; the numeric id survives renames

Decision Outcome

Chosen: the skill plus roomctl (D2), the deterministic summary plus notes (D3), and GitHub-backed room visibility (D7).

    flowchart LR
  T["token: sub"] --> B{"broker: read room R (repo X)"}
  B -->|agents-admin| OK["allowed"]
  B -->|cache hit < 5 min| C{"can read X?"}
  B -->|cache miss| L["ZITADEL link -> GitHub id -> login"] --> G["GitHub: permission of login on X"] --> C
  L -->|ZITADEL / GitHub unreachable: fail closed| D
  G -->|unreachable: fail closed| D
  C -->|yes| S["standing rules: watch / steer / approve"]
  C -->|no| D["404: no such room"]
  

D7 in one line: GitHub is a link-only IdP in ZITADEL. On a cache miss the broker lists the caller’s IdP links (ListIDPLinks, read-only ORG_OWNER_VIEWER machine user), takes the GitHub link’s numeric id, resolves it to the current login (GET /user/{id}), and asks GitHub with the factory App’s installation token whether that login can read the room’s repo. Each answer is cached for at most 5 minutes, and non-admins fail closed when ZITADEL or GitHub cannot be reached. GitHub read access lets you see a room; it never grants steering or approving, which stay with the existing standings. Details: spec, “Rooms across repos”.


Consequences

Positive

  • One skill file, no server to run locally. .agents/skills is the open-standard location most agents read; Claude Code reads only .claude/skills, which this repo symlinks to it (roomctl skill install writes .agents/skills, so elsewhere the symlink is yours to add).
  • The summary is a versioned contract (summary/v1) tested with golden fixtures.
  • A room is never more visible than its repo; revocation lags by at most the TTL (5 minutes), plus one 30 s ping for a socket that is already open.

Negative

CostMitigation
Users must link their GitHub account to their ZITADEL user once (GitHub is link-only: no sign-up and no auto-creation through it); without a link non-admins see no roomsroomctl rooms explains how to link it
Linked users can also sign in with GitHub and receive their project roles (EKS RBAC, Grafana, Harbor, Headlamp), because ZITADEL needs the IdP on the login policy for linking. Accepted riskOffboarding must deactivate the ZITADEL user, not only the Google account
The broker holds an org-wide read-only ZITADEL credential (ORG_OWNER_VIEWER machine user)Used only for ListIDPLinks. Not harmless if leaked: its PAT can mint more credentials for its own user, so revoking means deleting all of its PATs and keys, or deactivating the user
Reads depend on the GitHub API, and on ZITADEL on a cache miss5-minute cache; fail closed for non-admins past it
The WebSocket answers 404 no such room instead of 403 not_permitted for a known room the caller may not readIntended: a 403 would confirm the room exists
Progress notes are untrusted text, and the developer’s own local agent is now a readerReturned under notes.untrusted: true, the skill treats them as data, the web page escapes them
The “never apply a label” rule in the skill is advice, not a controlThe enforceable gate is server-side: only a maintainer’s label starts work

Neutral

  • Rooms created before this change carry the CRD default spec.repository, Smana/cloud-native-ref, which the API server applies. Anyone who can read that public repo can read them until the factory backfills the real repository.
  • An MCP server stays a later option, not a rejected one.

References