Using AgentDock
Users and roles
Admins, operators and viewers, registration and audit.
AgentDock has local accounts (email and password), three roles, and per-project membership. Every request and every runner command is checked against the caller's role on the project, and everything that changes state is written to a tamper-evident audit log.
There is no SSO or OAuth login.
The first admin
A fresh install has no users: the database seed creates none. Create the first admin from the command line.
From a source checkout:
bun run admin:createIt prompts for the email, an optional name and the password (twice). For a non-interactive setup, pass them as environment variables (and remove them afterwards):
ADMIN_EMAIL=[email protected] ADMIN_PASSWORD='a long passphrase' ADMIN_NAME='You' \
bun run admin:createInside the Docker stack, run the compiled CLI in the API container:
docker compose exec api node dist/src/cli/admin-create.jsThe command creates an active admin and refuses an email that already exists. See Docker Compose.
Registration
Registration is closed by default. While it is closed, the sign-up form answers registration_closed. An admin opens it under Admin → Settings → Open registration.
Even with registration open, nobody gets in on their own:
- A person registers at
/registerwith an email, an optional name and a password (12 to 256 characters). - The account is
pending. Signing in sends them to/pending, which says they are waiting for an administrator. - An admin opens Admin → Users → Pending and clicks Approve, which activates the account and assigns its role in one step. Or Reject.
A rejected email cannot register again until an admin deletes that account. Open registration only while you are onboarding people, then close it.
Account statuses are pending, active, rejected and disabled. Only active accounts can sign in.
Roles
Roles are global, and an admin picks one per user: admin, operator or viewer.
| Viewer | Operator | Admin | |
|---|---|---|---|
| Projects visible | Where they are a member | Where they are a member | All, no membership needed |
| Fleet, queue, approvals list, history, activity, sessions, usage | Read | Read | Read |
| Live worker pane | Yes | Yes | Yes |
| Start and stop the orchestrator, stop a slot, message a worker | No | Yes | Yes |
Choose bypassPermissions | No | No | Yes |
| Approve or request changes on a pull request | No | Yes | Yes |
| Create issues, refresh the queue or the project | No | Yes | Yes |
| Search, inspect, install (to the project) and run skills | No | Yes | Yes |
| Create and manage schedules | No | Yes | Yes |
| Terminal attach | No | No | Yes |
| Install skills into a runtime profile | No | No | Yes |
| Connect and remove projects, project settings, members | No | No | Yes |
| Machine-wide sessions with no project | No | No | Yes |
| Admin pages: runners, users, settings, integrations, budgets, prices, schedules, audit | No | No | Yes |
In short: viewers read, operators steer the projects they belong to, admins run the instance.
Per-project overrides
Membership is per project (Project → Settings → Members). A member can carry a Role override, and the effective role on that project is the lower of the global role and the override. An override can only lower a role, never raise it:
- an operator with a
vieweroverride is a viewer on that project; - a viewer with an
operatoroverride is still a viewer.
Admins are admins on every project, whatever their membership row says.
Hidden projects return 404
A project you are not a member of is invisible. Its pages and API routes answer 404, not 403, so its existence is not revealed. Admin-only routes answer 403 to non-admins whether or not the project exists.
Changing roles and accounts
On Admin → Users:
- A change to or from
adminasks for confirmation. - Disable signs the person out everywhere at once and keeps everything they own. Enable brings them back.
- Delete removes the account with its memberships, notification settings, Telegram link, personal budgets and the schedules they created. Their work (sessions, runs, approvals, audit records) stays.
- You cannot disable or delete your own account; another admin can.
- The last active admin cannot be demoted, disabled or deleted.
Sign-in and sessions
| Rule | Value |
|---|---|
| Password hashing | argon2id |
| Session cookie | ad_session, httpOnly, SameSite=Lax, Secure when APP_ENV=production |
| CSRF | ad_csrf cookie echoed in the X-CSRF-Token header on every write |
| Idle expiry | 7 days |
| Absolute expiry | 30 days |
| Rate limit | 10 requests per minute per IP on login and register |
| Lockout | 10 failed logins in a row lock the account for 15 minutes |
A wrong email and a wrong password give the same error, so the login form does not reveal which accounts exist. Changing your password signs out your other sessions. Disabling an account or changing its role signs that person out everywhere. Account lists your signed-in devices and lets you revoke any of them.
Behind a reverse proxy, set TRUST_PROXY so the rate limit sees the real client IP. See Configuration reference.
Audit log
Every state-changing action is recorded: sign-ins and failed sign-ins, user and role changes, project and member changes, every runner command (start, stop, message, skill install and run, approvals, terminal attach), schedule and budget changes, integration changes and more. A record holds the actor, action, target, project, before and after values, the result (ok, denied or error), and the IP and user agent for actions a person took.
Secrets never enter the log: fields such as password, token, secret and code are replaced with [redacted] before the record is written. The full text of a message to a worker is stored only as a hash.
Tamper evidence
The audit_records table is append-only twice over:
- A hash chain. Each record stores
hash = sha256(prevHash ‖ canonical(record)), with the first record chained to 64 zeros. Changing or deleting any row breaks every hash after it. - A database trigger rejects
UPDATE,DELETEandTRUNCATEon the table. Anyone who bypasses it (a superuser dropping the trigger) still breaks the chain.
No role can edit or delete audit records through the API, and no retention job ever removes them.
Verification
A nightly job walks the whole chain and recomputes every hash. It runs at AUDIT_VERIFY_CRON (default 0 3 * * *) and stores the result. Admin → Audit shows the last verification and a Verify now button. A broken chain names the first broken record and explains what it means.
The same page filters records by time range, action, result, actor, target and project, and Export CSV downloads the matching rows (up to 100,000 per export, with spreadsheet formula injection neutralised).