Imported from fancyboi999/open-tag (
AGENTS.md). Install upstream withnpx skills add fancyboi999/open-tag. Copyright stays with the author.
open-tag — guide for AI coding agents
open-tag is an open-source, self-hosted alternative to Claude Tag — a Slack-style multi-agent workspace where humans and AI agents collaborate as teammates in channels, threads, and DMs. Agents are persistent teammates with their own memory, running on a daemon on a machine you control; data stays in your network.
The project's own mission: accumulate memory, keep docs in sync with code, and drive its own iterative improvement — autonomously.
This file is a map, not a manual
Harness engineering principle: give the agent a map, not a thousand-page handbook. Architecture details, data models, and contracts live in the files this map points to — don't pile detail here (it goes stale, bloats context, and can't be mechanically verified). When you change the architecture, update
ARCHITECTURE.md, not this file.
Read these first (jump as needed):
docs/MISSION.md— North star / working directive: what open-tag is building toward; evidence-driven slices, browser-verified. Read before adding any feature.ARCHITECTURE.md— Repository codemap: three planes, what every file does, architectural invariants, module boundaries. "Where does X live?" — check here first.docs/core-beliefs.md— Load-bearing project beliefs:src/is canonical, three-plane auth, credential hygiene, etc. Scan before you touch anything.docs/authorization.md— The authoritative access-control model: three auth planes, role→capability- agent scope tables, the four invariants every route must obey (tenant isolation, resource-access checks,
channel visibility), and the hardening roadmap. Read before touching any route,
resolveAgent/resolveTarget, or anything that reads a resource by a client-supplied id. 越权很危险.
- agent scope tables, the four invariants every route must obey (tenant isolation, resource-access checks,
channel visibility), and the hardening roadmap. Read before touching any route,
docs/tech-debt-tracker.md— Known doc/implementation drift and debt.docs/generated/db-schema.md— Ground truth for the data model (fromsrc/db/schema.ts).docs/PLANS.md— Plan conventions + in-progress plans + roadmap index.README.md— How to run + verified evidence of what works.
Conventions
- TypeScript throughout. Run
npm run typecheck(root + web) before committing. - Agent workspace lives at
~/.open-tag/agents/<agent-id>/with aMEMORY.mdper agent. Optionally, apersonality.mdfile in the same directory overrides the agent'sdescriptionfield — place an agency-agents personality file (e.g. from~/.open-tag/agency-agents/) there to inject a detailed persona into the system prompt and MEMORY.md## Rolesection. - Screenshots / browser-verification captures go in
.shots/(gitignored — never commit them).
Parallel development (worktrees)
- Default workflow: do your work in a worktree, not the main checkout. Start any
feature, multi-file change, or task that needs an isolated stack (agent runtime,
realtime, DB) with
npm run wt:add -- <name>, and open the PR from there. The main checkout stays onmain(it's where prod runs). Exception — trivial changes (a doc edit, a one/two-line fix) may use a plain branch offorigin/mainin the main checkout; use judgment, don't spin up a worktree's whole DB+seed for a typo. A soft, non-blockingSessionStartreminder fires once per session when the session starts onmainin the main checkout (.claude/hooks/worktree-reminder.sh, wired in.claude/settings.json). - A
SessionStarthook (.claude/hooks/pull-main-on-session-start.sh) keeps the main checkout'smainfresh by fast-forwarding it toorigin/mainonce per session — but only when it's a zero-risk FF: it skips inside a worktree, offmain, on a dirty tree, or when localmainhas diverged. It never merges, rewrites, or touches a feature branch. (Deliberately not a pull-on-every-edit hook — that would clobber in-progress work.) - Use
npm run wt:add -- <name>to spin up an isolated git worktree (its own ports +opentag_<name>database + redis index +OPEN_TAG_HOME=~/.open-tag-<name>data dir + seeded data);npm run wt:rm -- <name>tears it down (and cleans the data dir + db). Lets several features (or agents) run side by side without port, database, or daemon/agent workspace collisions. wt:add branches each worktree fromorigin/main(not your current HEAD), so PRs made from it never inherit an unrelated branch — setWT_BASE=HEADto stack on the current branch instead. (vite.config+src/env.tsreadPORT/VITE_PORT/ENV_FILE;src/paths.tsreadsOPEN_TAG_HOME— so each worktree is fully isolated.) - Browser verification: check your own web UI with the chrome-devtools MCP. When several
agents or worktrees run in parallel, start chrome-devtools with
--isolatedso each gets its own Chrome instance instead of fighting over a shared one.
Isolated dev E2E (on demand)
When a task touches the agent runtime / human↔agent loop / realtime delivery to agents / agent memory, verify it in an isolated live stack instead of poking prod or hand-wiring JWTs:
npm run wt:add -- <task>— isolated worktree (own DB, ports, redis, data dir).cd ../open-tag-<task>and do the work.npm run dev:e2e:up— builds web, starts server + daemon (background), seeds a realclaude/sonnet@dev-botin#all, and prints the dev-login URLhttp://localhost:$PORT/?as=you(the server serves the built web, so no separate vite).- Verify (browser dev-login →
@dev-bot, or curl). npm run dev:e2e:down, then from the main reponpm run wt:rm -- <task>.
This needs the claude CLI installed + authenticated (it runs a real agent). If your task
does not involve the agent runtime (docs, pure REST, UI-only), skip it — it's wasted
setup otherwise. Decide per task.
Doc-sync discipline (highest priority: code change = doc change)
Docs naturally lag behind code. This project treats doc/code sync as a hard rule: every change must update the corresponding docs in the same commit. Doc lag = an unfinished bug. Self-check with the table below before marking done, then run
/doc-syncfor a full audit (canonical skill at.agents/skills/doc-sync/, exposed to Claude Code via the.claude/skills/doc-syncsymlink — same file, any runtime).
| You changed… | Must also update |
|---|---|
src/db/schema.ts (tables / columns) |
docs/generated/db-schema.md — and the prod DB gets migrated on deploy (prod:up now runs db:push:prod; see Release discipline). |
Routes/endpoints (routes-api / routes-agent), CLI sub-commands, daemon protocol |
ARCHITECTURE.md codemap / boundaries / contracts |
| Module purpose / boundary / architectural invariant | ARCHITECTURE.md §II–IV |
| A feature (completed or modified) | FEATURES.md checkbox + README.md "Verified" section if relevant |
| Doc/code mismatch, or a TODO / tech debt left behind | docs/tech-debt-tracker.md — add an entry, don't let it rot silently |
| A complex change with a plan | docs/PLANS.md (convention) |
src/daemon/** that ships in the bundle (runtime / CLI / daemon protocol) |
Publish a new daemon release — bump packages/daemon/package.json + cut a GitHub Release + add the version's CHANGELOG.md entry. See Release discipline below. Merged ≠ shipped. |
Keep this file in "map" form: details go into their respective files. Don't accumulate history or changelogs here — that's what
git logis for.
Release discipline (the daemon ships as an npm package — merged ≠ shipped)
Prod / self-host machines run the compute-plane daemon as
npx @fancyboi999/open-tag-daemon— the published npm package, not this repo'ssrc/. Merging a daemon change tomaindoes not reach those machines until a new package is published; a green CI and synced docs can still leave prod running stale code. (This is exactly how #44'scopilot/opencode/ … runtimes were live insrc/yet prod reportedno runtime: copilot— the daemon was still on the0.1.0package, which was cut before #44 merged.)
- Changed anything under
src/daemon/**that ships in the bundle (a runtime, the CLI, the daemon protocol)? Bumppackages/daemon/package.jsonversion, then publish a GitHub Release (vX.Y.Z). That — and only that — fires.github/workflows/publish-daemon.yml, which builds the bundle and publishes to npm via OIDC Trusted Publishing (token-less). A plain merge / tag / push publishes nothing. (New runtime → minor bump; bugfix → patch.) Add the version'sCHANGELOG.mdentry in the same PR — the changelog tracks this package, and it silently rotted from 0.4.0 to 0.8.1 while this step wasn't on the list. - A long-lived daemon keeps running the old bundle until restarted — bounce it
(
npx @fancyboi999/open-tag-daemon@latest) on each prod machine after publishing.
Server-side: code ≠ deployed until the DB is migrated too. A change to src/db/schema.ts
(new column / index / onConflict target) makes the new server code expect a schema the prod DB
may not have yet — once a merged-but-unmigrated partial unique index made agent-create 500 in prod.
scripts/prod-up.sh now runs db:push:prod between the web build and the server start, so a
normal deploy migrates the DB before the new code serves. Don't hand-restart the prod server and
skip it. (db:push is additive-safe and prompts before any destructive change.)
Dependency updates — deliberate and manual
Automated Dependabot version-update PRs are disabled. Do not add .github/dependabot.yml
without an explicit maintainer decision. Dependabot vulnerability alerts may remain enabled as
a read-only security signal; they do not authorize an automatic upgrade or merge.
- Update dependencies only for an explicit maintenance task or an actionable security alert.
- Keep each change scoped to one ecosystem and a reviewable dependency group; regenerate the
matching lockfile from current
main, then run the repository's full applicable checks. - Treat major upgrades as product changes. Read the release notes and verify real runtime paths; green typecheck/build/unit checks alone do not prove compatibility.
Code quality (load-bearing — full text in docs/code-quality.md)
Three rules gate every change. The why + full detail live in
docs/code-quality.md; the must-obey core:
- Eight shalls / shall-nots (craft): understand an interface before using it; reuse > invent; verify edge / error / concurrency paths, not just the happy one; surgical diffs (every line traces to the task — no drive-by cleanup); when two patterns conflict, pick the newer / more-central one and say why — never write "satisfies-both" hybrids.
- Agent-prompt red line:
src/daemon/prompt.tsis the standing prompt shared by every runtime — keep it runtime-agnostic. No provider-specific tool names (Read/cat/grep/ vision hints); describe capabilities generically; after editing,grepfor provider tool names → expect zero hits. - Verification before "done": code complete ≠ task done. Run every applicable layer (unit → integration / E2E → real-run: curl / browser / CLI) and post the evidence. Fail loud: every "done" must list what was skipped, what warned, what wasn't verified.
Human auth & first deploy
Full access-control model (capability/scope tables, the four invariants, per-plane enforcement, and the hardening roadmap of known gaps) lives in
docs/authorization.md— this section is just the deploy-facing summary.
Three separate auth planes — do not conflate them (src/server/auth.ts):
- human → JWT (
signUser/verifyUser), endpoints under/api/auth/*. - agent → per-agent token (
Bearer sk_agent_*+x-agent-id),resolveAgent,/agent-api/*. - daemon → bootstrap/machine key over WS
/daemon/connect?key=(ws.ts).
Resource-control env vars (optional):
OPEN_TAG_PRESSURE_MEM_MB(default 500) — when free system memory drops below this (MB), new agents are queued and running agents receive a per-process cap at their current RSS + fair-share margin via the Job Object or cgroup. Seesrc/daemon/resourceBudget.ts.
Required env vars — server will not start without these:
JWT_SECRET— signing key for human session JWTs. Generate:openssl rand -hex 32.DAEMON_BOOTSTRAP_KEY— pre-shared key for daemon WS handshake. Generate:openssl rand -hex 32. (These replaced the old weak fallbacksdev-secret-change-me/poc-secret-key.)
Human-auth env flags (.env / .env.prod):
ALLOW_DEV_LOGIN— whentrue,POST /api/auth/dev-loginmints a username→JWT with no password. Development only; default isfalse. The endpoint 404s when off. Defense in depth:NODE_ENV=production(set by the Dockerfile runtime stage) force-disables dev-login even if the flag is mistakenly set, so the env flag is not the only line of defense. The frontend never silently falls back to dev-login; an anonymous visitor to/s/*is redirected to/loginby the route guard inweb/src/main.tsx.ADMIN_SETUP_TOKEN— one-time first-deploy admin bootstrap. The seeded owner has no password, so afternpm run seedset this to a long random value and call once:curl -X POST $URL/api/auth/setup -d '{"token":"<token>","email":"admin@you","password":"<≥8>"}'. It sets the owner's password and self-closes (410 already initialized) once a password exists. Disabled (404) when the token is unset.
Transport-layer env flags:
ALLOWED_ORIGIN— comma-separated allowed browser origins for CORS. Dev default (unset): anylocalhost/127.0.0.1origin. Production: must be set to frontend URL(s).TRUST_PROXY— set totrueonly when a single controlled reverse proxy (Railway, nginx, Caddy) rewrites the client-IP headers.clientIp()prefersX-Real-IP(a single clean value the proxy sets and overwrites if forged), falling back to the firstX-Forwarded-Forhop (the proxy prepends the real client and strips client-supplied XFF). Both verified against Railway: a forgedX-Real-IP+X-Forwarded-Forarrive overwritten, real IP leftmost. Do not use the rightmost XFF entry — on Railway that is the proxy's rotating edge IP, which gives every request a fresh rate-limit bucket and defeats the limit. Assumes exactly one trusted hop that overwrites/prepends; a proxy that blindly appends leaves the leftmost spoofable, and multi-hop chains (CDN → nginx → app) need hop-count-aware parsing (e.g.proxy-addr). Without this flag,clientIp()uses the TCP socket address (unforgeable).