---
name: admin
surface: platform
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), publish-site (move an extracted static-site tree under <accountDir>/sites/<slug>/, emit one canonical path slug, and refresh llms.txt at the site root — typed refusal taxonomy replaces the prior skill-prose contract), brand-settings, account-manage, capabilities-here (deterministic install-introspection — returns enabledPlugins, tier, models, installed platform + premium specialists, registered Claude Code agent types, and the brand block as a single JSON object; the canonical tool for grounding any claim about what is enabled on the current account), account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and admin-identity-authenticate (native-CC PIN identity gate — the operator submits their PIN as the first action of every session to bind their authenticated userId; the PreToolUse pin-identity-gate hook blocks all other tools until this succeeds) for managing the Maxy platform."
tools:
  - name: system-status
    publicAllowlist: false
    adminAllowlist: false
  - name: public-hostname
    publicAllowlist: false
    adminAllowlist: false
  - name: publish-site
    publicAllowlist: false
    adminAllowlist: false
  - name: brand-settings
    publicAllowlist: false
    adminAllowlist: false
  - name: account-manage
    publicAllowlist: false
    adminAllowlist: false
  - name: capabilities-here
    publicAllowlist: false
    adminAllowlist: false
  - name: account-update
    publicAllowlist: false
    adminAllowlist: false
  - name: admin-add
    publicAllowlist: false
    adminAllowlist: true
  - name: admin-remove
    publicAllowlist: false
    adminAllowlist: true
  - name: admin-list
    publicAllowlist: false
    adminAllowlist: true
  - name: admin-update-pin
    publicAllowlist: false
    adminAllowlist: true
  - name: agent-list
    publicAllowlist: false
    adminAllowlist: true
  - name: agent-config-read
    publicAllowlist: false
    adminAllowlist: true
  - name: logs-read
    publicAllowlist: false
    adminAllowlist: false
  - name: plugin-read
    publicAllowlist: false
    adminAllowlist: false
  - name: skill-load
    publicAllowlist: false
    adminAllowlist: true
  - name: store-skill
    publicAllowlist: false
    adminAllowlist: true
  - name: session-reset
    publicAllowlist: false
    adminAllowlist: true
  - name: session-resume
    publicAllowlist: false
    adminAllowlist: true
  - name: action-pending
    publicAllowlist: false
    adminAllowlist: true
  - name: action-approve
    publicAllowlist: false
    adminAllowlist: true
  - name: action-reject
    publicAllowlist: false
    adminAllowlist: true
  - name: action-edit
    publicAllowlist: false
    adminAllowlist: true
  - name: admin-identity-authenticate
    publicAllowlist: false
    adminAllowlist: true
hidden:
  - remote-auth-status
  - remote-auth-set-password
  - qr-generate
  - wifi
always: true
embed: false
mcp:
  command: node
  args:
    - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
    - ${PLATFORM_ROOT}/plugins/admin/mcp/dist/index.js
  env:
    MCP_SPAWN_TEE_NAME: admin
    LOG_DIR: ${LOG_DIR}
    PLATFORM_ROOT: ${PLATFORM_ROOT}
    ACCOUNT_ID: ${ACCOUNT_ID}
    SESSION_ID: ${SESSION_ID}
    PLATFORM_PORT: ${PLATFORM_PORT}
    MAXY_BRAND_PORT: ${MAXY_BRAND_PORT}
mcp-manifest: auto
---

# Admin

Platform management tools for both the admin and public agents. The `plugin-read` tool is available to the public agent (read-only) for loading plugin definitions and references. `skill-load skillName=<name>` is the canonical resolver+reader for SKILL.md by skill name — one call returns the body with brand placeholders substituted; `plugin-read` retains exclusive ownership of `references/*` and `PLUGIN.md` reads (both inherently plugin-scoped). All other tools are admin-only.

Tools are available via the `admin` MCP server.

**Admin-seat authoring-skill gate (Task 516).** `skill-load` and `plugin-read` refuse to return the body of a content-producer-owned authoring skill — `professional-document`, `a4-print-documents`, `publish-site` — when the caller is the admin seat (`MAXY_SESSION_ROLE=admin` with no `MAXY_SPECIALIST`). The refusal relays one line — "This deliverable is owned by content-producer. Dispatch it with the Agent tool and subagent_type content-producer." — so the admin's next action is the dispatch, not inline authoring. `plugin-read` extracts the skill slug from a `skills/<slug>/…` file path (or a slug buried in `pluginName`), so the raw-SKILL.md read path can't bypass the gate; `PLUGIN.md` and plain `references/*` reads are untouched. content-producer's own spawns carry `MAXY_SPECIALIST=content-producer` and resolve these skills normally (it reaches them via the native `Skill` tool regardless), and the public seat is out of scope. The gate is a pure name+env check before any filesystem read and fails open for any non-admin or unidentified seat — it never bricks a legitimate load. Each block emits `[admin-skill-gate] role=admin skill=<name> decision=block reason=content-producer-owned`. This is the enforcement layer; the IDENTITY.md delegation paragraph documents the intent but is not the gate ([[feedback_deterministic_means_remove_llm]], [[feedback_doctrine_paragraph_is_not_a_gate]]). Skills the admin invokes directly are deliberately excluded: `unzip-attachment` (its SKILL.md says "Invoked by the admin agent directly — admin owns all unzipping", then routes the extracted tree to a specialist) and `deck-pages` (admin-owned, a content-producer refusal target). The set is each skill's own SKILL.md "Invoked by" declaration, not the admin plugin's skill catalogue (which lists these admin-hosted skills for discovery — the catalogue is the inline path this gate closes).

**Three-store admin auth invariant.** `admin-add` writes to all three identity stores (`users.json` PIN auth at the persistent `~/{configDir}/users.json` location , `account.json` `admins[]` role, Neo4j `:AdminUser`/`:Person` graph identity) with per-leg `[admin-auth-store]` log lines plus the `[admins-write]` chokepoint line, and returns `is_error: true` on any leg failure naming what's already written. `admin-update-pin` writes `users.json` only and emits the same `[admin-auth-store]` line. **Single chokepoint:** every `users.json` + `account.json admins[]` mutation routes through `platform/lib/admins-write` (`writeAdminEntry` for admin-add/set-pin, `removeAdminFromAccount` for admin-remove); the static check `grep -rnE 'admins\.push\|config\.admins\s*=' platform/ \| grep -v lib/admins-write` returns 0. Direct `Edit`/`Write` on `account.json` is forbidden by IDENTITY.md doctrine — mutations go through `account-update`, `plugin-toggle-enabled`, or the `admin-*` tools. **Install + boot invariants:** the installer and admin-server boot walk every account.json admins[] vs users.json; `[install-invariant]` / `[admin-invariant] direction=… userId=<id8> source=<file>` fires per divergence with a `check complete divergences=<n>` summary. Log-only — does not block. See `.docs/agents.md` § "Three-store admin auth invariant" for the full contract.

**AdminUser graph stamp + boot self-heal.** The seed cypher in `platform/scripts/seed-neo4j.sh` stamps `:AdminUser.accountId` + `:AdminUser.role='owner'` ON CREATE so the graph-write gate's account-scoped MATCH succeeds on the first user-domain write. Admin-server boot additionally runs `platform/ui/app/lib/adminuser-self-heal.ts` to repair legacy nodes whose `accountId` or `role` is null — emits `[adminuser-self-heal] healed=<N> userId=<8> accountId=<8>` once per boot, idempotent. The `graph-write-gate` reject log gains a `subReason=` field (`no-admin-user-node` / `admin-user-no-accountid` / `no-business-or-personal-profile`) for operator diagnosis. See `.docs/agents.md` § "AdminUser graph stamp contract" for the full contract.

`logs-read { type: "agent-stream" }` is the canonical name for the per-conversation tool-use/tool-result archive previously called `system`; both names work and the legacy alias is preserved.

**Stream log is the primary diagnostic surface for an in-progress session.** Every stream log is named by sessionKey and the stream-log file exists from the first token. The single-writer mandate (2026-05-14) mechanically enforces this contract: the single writer module at [`platform/ui/app/lib/claude-agent/stream-log-writer.ts`](../../ui/app/lib/claude-agent/stream-log-writer.ts) owns every open of `claude-agent-stream-*.log`, opens the file lazily on the first SDK token byte (via `streamLog.writeToken`), and buffers pre-token markers in memory while duplicating them to `server.log` via `console.error` so zero-token sessions retain observability. The build gate `platform/ui/scripts/check-stream-log-writer.mjs` rejects any external `appendFileSync`/`createWriteStream` against the `claude-agent-stream-*` pattern outside the writer module, plus any `claude-agent-stream-${sessionId}` literal (the pre-1013 wrong-identifier signature that produced two files for one session on the Beacons Pi 2026-05-14). The first-token invariant is bound by `platform/scripts/__tests__/first-token-creates-stream-log.test.sh`: one operator turn, one token, `claude-agent-stream-<sessionKey>.log` exists and contains the token bytes after the byte returns to the operator. The parent-process console fan-out tee in [`platform/ui/app/lib/claude-agent/logging.ts`](../../ui/app/lib/claude-agent/logging.ts) appends every `[<tag>]`-prefixed `console.error` / `console.log` line to every active session's stream log alongside `server.log`. For diagnosing an in-session issue (WhatsApp inbound, Cloudflare action, persist write, baileys error), call `logs-read { sessionKey: "<…>" }` first — the stream log carries both the agent lifecycle AND the parent-process events that occurred during the session window. The `sessionId` parameter remains a legacy alias resolving to the same sessionKey-named file. `logs-read { type: "server" }` is the cross-session escape hatch for events outside any single session window.

## Skills

| Task | When to use | Reference |
|------|-------------|-----------|
| Manage public agents | User asks to create, edit, clone, list, preview, or delete a public agent | `skills/public-agent-manager/SKILL.md` |
| Create a new skill | User asks to create, add, or teach the agent a new capability | `skills/skill-builder/SKILL.md` |
| Review stream logs | Admin asks to review, diagnose, or analyse a Claude agent stream log | `skills/stream-log-review/SKILL.md` |
| Set up business profile | Admin asks to set up, complete, or update business operational data | `skills/business-profile/SKILL.md` |
| Generate a QR code | User asks to generate, create, or make a QR code from a URL, Wi-Fi details, contact, or text | `skills/qr-code/SKILL.md` |
| Timezone queries | User asks for the time in another city, timezone conversion, UTC offset, or DST status | `skills/datetime/SKILL.md` |
| Manage access grants | Admin asks to invite visitors, revoke or extend access, list who has access, or set an agent's access mode | `skills/access-manager/SKILL.md` |
| Manage plugins and settings | User asks to install, enable, disable, or configure plugins, or change account settings | `skills/plugin-management/SKILL.md` |
| Manage specialists | User asks to install or remove a specialist subagent, or activate/deactivate premium plugin agents | `skills/specialist-management/SKILL.md` |
| Generate print-quality PDF | User asks to create a PDF document, one-pager, brochure, or any HTML intended for print/download | `skills/a4-print-documents/SKILL.md` |
| Compose a business document | User asks to write or draft a proposal, report (market / analysis / strategic), brief, memo, white paper, plan (non-fundraise), or case study | `skills/professional-document/SKILL.md` |
| Manage admin users | User asks to add, remove, or list admins on this account, or change an admin PIN | `skills/admin-user-management/SKILL.md` |
| Session reset / continue / resume | User asks to clear the session, start fresh, continue the last session, or pick up where they left off | `skills/session-management/SKILL.md` |
| Show or download a file | User asks to view, attach, or download a file or document, or the current turn produces a document the owner needs to review | `skills/file-presentation/SKILL.md` |
| Upgrade Maxy | User asks to upgrade, update, install the latest version, or accepts an upgrade offer | `skills/upgrade/SKILL.md` |
| Run a structured sprint without `gstack` | User asks to run a sprint, execute a `.tasks/NNN-*.md`, or invokes "superpowers sprint" / "sprint without gstack" | `skills/superpowers-sprint/SKILL.md` |
| Investigate a bug or unexpected behaviour | User asks to debug, fix a bug, find the root cause, or investigate an error | `skills/investigate/SKILL.md` |
| Capture work as a task before implementing | User asks to "task it", create a task, or describes work that should be a task file before a sprint | `skills/task/SKILL.md` |
| Run an insight pass on this session | Operator asks to run an insight pass, review this session, capture what we learned, or do a retrospective on the conversation | `skills/insight/SKILL.md` |

## Hooks

- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
- `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
- `hooks/mcp-tool-missing.sh` — **PostToolUse hook on `mcp__.*` (Task 502, directive 3).** Defence-in-depth for the `No such tool available: mcp__…` failure class that the Task 502 name-binding is built to eliminate. Fires on any MCP tool call; no-op unless the `tool_response` carries `No such tool available` AND the qualified name resolves to a maxy plugin (read from the generated `hooks/lib/maxy-mcp-plugins.txt`). On a maxy match it logs one deterministic `[mcp-tool-missing] server=<server> tool=<tool>` line and exits 2 with a fixed envelope on stderr, so the agent relays a named server-unavailable failure instead of narrating "warming up" or blind-retrying. A missing non-maxy bridge tool (Playwright etc., upstream-owned) passes through (exit 0). The maxy-plugin list is regenerated and gate-diffed by `platform/scripts/check-canonical-tool-names.mjs`.
- `hooks/post-tool-use-agent.sh` — **PostToolUse hook on `Agent` (Task 560).** Drains any subagent hook-decision buffers under `~/.maxy-code/logs/hook-decisions/` modified since this parent's previous PostToolUse-Agent fire (cursor file keyed by parent session id), prints one `[hook-propagate]` line per record to stdout — Claude Code attaches the stdout as a `hook_success` attachment on the parent JSONL, making the records grep-queryable from the parent session alone (Task 559 motivating case). Rotates consumed buffers to `consumed/`. Emits one `[hook-propagate-census] parentSession=<…> subagentHooksObserved=<N> attachmentsEmitted=<M>` line per fire to stdout and server.log; `N != M` is the propagation regression signal. The companion emitter library `hooks/lib/hook-emit.sh` is sourced by `post-tool-use-agent.sh` and any other hook that records a block decision (4 KB stderr truncation, `truncated=true` set on the record).
- `hooks/admin-authoring-observer.sh` — **PostToolUse hook on Write and Edit (Task 486).** Observation only — never blocks; exits 0 on every path. Fires when the admin agent (not a specialist subagent — gated by `MAXY_SPECIALIST` env) writes or edits a file under `<accountDir>/output/`. Walks the session transcript from the latest real-user turn forward to detect any prior `Task` `tool_use` whose `subagent_type` starts with `specialists:`. Emits one stderr line `[admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>`. A `false` value on a long-form prose file is the regression signal Task 486 was designed to make visible — the BioSymm proposal session (admin authored a customer-facing proposal inline despite content-producer being installed) is the failure mode this surfaces mechanically. Mechanical enforcement (refuse the write, force a re-spawn) is deferred per the task spec.
- **Turn recorder — removed entirely (Task 626).** The `turn-completed-graph-write.sh` Stop hook, the `/api/admin/claude-sessions` loopback bypass it relied on, the `[turn-recorder]` emitters, the envelope walker, and the recorder-auto-archive subscriber are deleted. It had been dormant since Task 214 (never re-registered in settings.json); the admin now writes to the graph by delegating to `database-operator` via the Task tool inside the live session, and the on-demand `/insight` pass (`skills/insight/SKILL.md`, a registered admin skill — Task 641) is the per-session review. There is no per-turn spawn.

## Session identifiers (Task 135)

The metadata pane and admin logs use these three identifier names, matching what Claude Code surfaces:

| Operator label | What it is | Manager wire field | Log key |
|---|---|---|---|
| `sessionId` | Claude's session — one identity with two phases the manager resolver maps to the same row: bridge suffix (`session_xxx` from `claude.ai/code/<session_xxx>`) and JSONL basename UUID on disk. The manager emits whichever phase is current (bridge preferred). | `sessionId` (collapsed from the earlier three-field surface (see Task 141)) | `sessionId=` |
| `accountId` | Account / workspace UUID. | `senderId` (channel-agnostic — same wire field carries phone/email/chat-id on non-admin channels, so its log key stays `senderId=` outside admin flow) | `accountId=` (admin flow) |

Full contract lives in [admin-session.md § Session identifiers](../docs/references/admin-session.md).

## SSE telemetry breadcrumbs

`POST /api/admin/sse-telemetry` writes `[sse-client]` lines to `claude-agent-stream-<sessionKey>.log` so one grep against that file reconstructs a failing turn. The admin agent reads these via `logs-read`.
