Portal auth and capability model
Short-lived capability token permission model used by the mikan admin, login, and session portals.
The design goal is to let users conveniently open management, login, and session-view pages while avoiding mixing “read data”, “change settings”, and “write secrets” into one permission.
The formal Web adapter has a separate identity model: Google/GitHub sign-in creates an opaque, cookie-authenticated browser session that authorizes only workspaces owned by that Web account. It is not one of the capability links described below, and it does not grant vault-write authority. /pi-login keeps its short-lived one-time capability boundary even for a signed-in Web user.
Three portal links
Section titled “Three portal links”| Interface | How users get it | What it can do | Token lifetime | One-time token? |
|---|---|---|---|---|
| Admin portal | /admin / /pi-admin | Manage conversations, model, sandbox, auto-reply, workspace previews, and events. Can also generate session/login links. | 30 minutes | No |
| Login / vault portal | /login / /pi-login, or generated by the admin portal | Store API keys or complete built-in OAuth flows, writing credentials into the vault. | 15 minutes | Yes, on write |
| Session view | session / /session / /pi-session, or generated by the admin portal | View the session timeline; when interactive mode is available, also send messages from the web page back to that session. | 24 hours | No |
In short:
/admin → change settings, view workspace, generate other links/link → write vault secrets or OAuth credentials/session → view session; optionally send messages back to the sessionThese three pages share the same portal shell, but they do not share the same authorization token.
Permission boundaries
Section titled “Permission boundaries”Admin portal
Section titled “Admin portal”The admin portal is control-plane access. Anyone with an admin link can manage mikan settings and conversation state for a short time.
The admin portal can:
- view the current user and conversation identity
- list conversations from the office registry (the durable raw-id ↔ office mapping), not by scanning the workspace
- read and update conversation model, thinking level, workspace door policy and layout, auto-reply, and Slack reply mode
- read and update global model, sandbox resource defaults, global door policy, and Slack defaults
- view limited workspace files, skills, and events metadata/files, and create or edit skills at either level
- list and mutate the package sources for a scope
- view session and conversation usage
- delete events for the selected conversation
- generate a session view link or login/vault link for the target conversation
Door policy can also be set from chat with /pi-sandbox door, but never by the agent itself:
conversation settings live under the host-only state dir precisely because conversation directories
are bind-mounted read-write into the sandbox, and the settings file inside the mount is migrated
once and then never read again.
The admin portal does not directly write secret values. Even when it generates a login link, the real secret write still goes through the one-time token flow of the Login / vault portal.
Login / vault portal
Section titled “Login / vault portal”The Login / vault portal is the highest-risk action capability because it can write credentials.
The Login / vault portal can:
- show the credential or OAuth onboarding form for a specified vault
- write environment variables into that vault
- write credential files from a preset or OAuth flow, such as config files required by supported tools
- complete supported OAuth flows and save access tokens, refresh tokens, or credential files
- notify the source conversation after a successful write
Important login token behavior:
- opening the
/linkpage does not consume the token - starting OAuth does not consume the token
- completing a credential POST or OAuth callback consumes the token
- when the same platform user creates a new login token, the old login token becomes invalid
Additional protections:
- Credential POST routes require
Content-Type: application/json. - When
LINK_URL/MIKAN_LINK_URLis set, credential POST routes check same-originOriginorReferer. - OAuth state is independent from the login token, has a 10-minute TTL, and uses a PKCE verifier.
- Secret values are not re-rendered to the browser; existing vault summaries show only secret names and mount targets.
Session view
Section titled “Session view”Session view is session content access. It is mainly used to view a structured session timeline.
Session view can:
- render the session timeline
- navigate parent/thread session relationships
- subscribe to live status and timeline updates through SSE
- when interactive wiring is available, send messages from the web page back to the selected session
Session view is not purely read-only. As long as the /session/message route exists and interactive wiring is available, a session view token can send a session_view event and call the bot handler.
The session view token is anchored to the base session file. When navigating with /session?session=<file.jsonl>, it can only switch to session files in the same directory.
Route and token mapping
Section titled “Route and token mapping”| Route | Method | Token source | Validation | Notes |
|---|---|---|---|---|
/admin | GET | query token | adminTokenStore.peek() | Render admin portal. |
/admin/api/* | GET | query token | adminTokenStore.peek() | Unauthorized returns 403. |
/admin/api/* | POST | JSON body token | adminTokenStore.peek() | Unauthorized returns 403. |
/link | GET | query token | linkTokenStore.peek() | Render login/vault page; does not consume token. |
/api/link/complete | POST | JSON body token | linkTokenStore.consume() | Write credentials; consumes token. |
/api/oauth/start | POST | JSON body token | linkTokenStore.peek() + OAuth state | Create OAuth redirect; does not consume login token yet. |
/oauth/callback | GET | query state | OAuth state + linkTokenStore.consume() | Complete OAuth; consumes OAuth state and login token. |
/session | GET | query token | sessionViewTokenStore.peek() | Render session page. |
/session/stream | GET | query token | sessionViewTokenStore.peek() | Open SSE stream; requires interactive wiring. |
/session/message | POST | JSON body token | sessionViewTokenStore.peek() | Send session message; requires interactive wiring. |
Web account sessions
Section titled “Web account sessions”Web account sessions are durable browser identity, not bearer links to an existing IM conversation. They use the mikan_web_session HttpOnly cookie and a session-bound CSRF value for mutations. The durable registry stores only hashes of session, CSRF, and OAuth-state secrets.
An authenticated account can list and mutate only its owned Web workspaces. Each workspace maps to an independent OfficeAddress("web", workspaceId); provider identities and equal email addresses do not automatically link accounts or IM offices. Unknown and foreign workspace ids return the same not-found response.
Web account routes include /auth/{github,google}, their callbacks, /api/web/me, /api/web/logout, and /api/web/workspaces/*. Mutation routes require exact JSON content type, same-origin validation, and the CSRF header. The workspace SSE route is ownership-scoped and does not expose the global agent-events stream.
Provider tokens are used only for the profile lookup and then discarded. They are never saved to the Web registry or vault. To add an LLM or tool credential, the user must still open a separately authorized Login / vault portal.
Why not use one token type
Section titled “Why not use one token type”The three token types have different risks:
- Admin token: reusable short-lived management permission.
- Login token: can write secrets, so it has a shorter lifetime and is consumed on write.
- Session view token: useful for sharing and reviewing sessions, so it lasts longer, but permission is limited to the session view scope.
Even if a full dashboard is added later, these boundaries should remain:
- Dashboard identity may authorize viewing and settings operations.
- Secret writes should still require a short-lived one-time capability, or equivalent second confirmation.
- Standalone session links can remain capability links for session viewing.
Implementation locations
Section titled “Implementation locations”| Feature | Main code |
|---|---|
| Web account auth | src/web/auth/portal.ts, src/web/auth/registry.ts |
| Web workspace API | src/web/harness/portal.ts, src/web/harness/service.ts |
| Portal HTTP server | startWebServer() in src/web/server.ts |
| Admin portal | src/web/admin/portal.ts, src/web/admin/store.ts |
| Login / vault portal | src/web/login/portal.ts, src/web/login/store.ts |
| Session view | src/web/session-view/portal.ts, src/web/session-view/store.ts |
| Shared token store | src/web/token-store.ts |
| Shared portal shell | src/web/portal-shell.ts |
startWebServer() dispatches health and webhook/event routes first, then the authenticated Web workspace and account routes, followed by the legacy Admin, session-view, and Login / vault capability portals.
The server starts only when LINK_PORT / MIKAN_LINK_PORT can be parsed as a port. If LINK_URL / MIKAN_LINK_URL is set but no port is configured, mikan uses the default port 8181.
Capability token stores are currently in-memory and src/main.ts cleans expired tokens every five minutes, so a process restart invalidates their unexpired tokens. Web account sessions are different: their hash-only records and owned-workspace metadata are restart-safe in <state-dir>/web/registry.json.
These URLs are bearer capabilities. Query-string tokens can leak through browser history, screenshots, copied URLs, or proxy logs; share them only with the intended user and never publish them in chat channels or issue trackers.