Who Is Allowed to Act in Odysseus?
In Odysseus, authentication is only the first transition in an authority decision. This chapter follows a request from login or bearer-token arrival through identity, ownership, privilege, and tool selection, so the reader can judge why a caller may read one kind of data yet still be blocked from a more powerful action. The distinction matters because Odysseus is not only storing conversations; it is coordinating personal state, models, integrations, and tools that can change the host or communicate with other systems.
A login is a continuing state, not a one-time event
Odysseus begins with a conventional question: has this caller proved possession of the account password? Its AuthManager stores bcrypt password hashes and issues random session tokens after credentials have been verified. The sessions persist in a server-side file and last seven days unless they are revoked or expire. Optional TOTP adds a second transition: after the password succeeds, the account must also present a valid time-based code before a session is issued. Enabling that factor produces eight single-use backup codes, giving the owner a recovery path without making backup codes reusable passwords.
The important design choice is what happens after login. Every request that relies on a session can still encounter a changed account state. Session validation checks its expiry, but it also checks that the associated user record still exists. If an administrator deletes an account, the next request does not continue to trust the old cookie until its natural deadline. The session is removed and the caller is sent back through authentication. Account deletion also revokes the deleted user's API tokens and active sessions, closing two other ways stale authority could survive.
Role changes are handled differently. An administrator cannot demote the last remaining administrator, which prevents an instance from losing its only administrative path. If a user is promoted, the system saves the user's earlier privilege map; when they are later demoted, the narrower map is restored rather than leaving administrative powers behind. A demoted administrator's existing session need not be destroyed for the next admin check to fail: administrative status is read live when the protected operation is attempted. In other words, the account can remain logged in while its authority changes underneath it.
There are also names that cannot become ordinary accounts. internal-tool is used as a sentinel for in-process agent loopback, while api identifies a bearer-token path; other reserved names represent synthetic or local storage owners. Refusing those names is not cosmetic. If a real account could take over a sentinel, a later route might mistake an ordinary login for an internal request or assign data to the wrong identity. Account normalization and migration protect the same boundary when older configuration formats are loaded.
The lifecycle can therefore be summarized as a sequence of decisions rather than a password check:
- a first-run installation creates its initial administrator;
- a password, and where enabled a TOTP code, establish a session;
- each request revalidates the session against expiry and current account existence;
- the live role and privilege state determine what the session may attempt;
- deletion, demotion, or token revocation changes the result of later requests.
The list is short, but the meaning is substantial. Authentication answers “who has entered?” It does not yet answer “whose state is this?” or “what effect is this caller allowed to cause?”
Ownership is not the same as identity
The difference becomes clearer when a request arrives through an integration. A browser session can carry the logged-in username directly. A bearer token is handled as a different transport identity: the middleware represents it internally as the pseudo-user api, while an owner-aware route can resolve the token back to the real account that minted it. That lets an integration see or create data for the correct owner without allowing it to wander through ordinary browser-user routes.
This separation prevents a subtle but common mistake. Suppose an integration submits a chat request and names a session identifier. The identifier alone does not prove that the integration may resume that conversation. Odysseus's sync-chat path checks that the token's owner exactly matches the session owner; an unowned or cross-owner session is not treated as available. Its fallback model-endpoint selection also considers the token owner's rows and intentionally shared legacy rows, rather than silently selecting another user's private endpoint and spending that user's credentials or quota.
The same idea appears in the data model. Sessions, documents, model endpoints, email accounts, memories, calendars, images, tasks, and tokens carry owner information. Some older rows have a null owner and are treated as shared or legacy state where the relevant route permits it. That compatibility is useful during migration, but it means “the database has an owner column” is not enough to establish a uniform tenant boundary. The route performing the lookup must apply the appropriate owner filter, and the reader must know whether shared rows are part of that route's contract.
Odysseus has an explicit no-login mode as well. When authentication is disabled, local writes can use a stable local storage owner instead of pretending that an anonymous request is an authenticated person. That is a deliberate operator choice for a local single-user deployment, not evidence that an unauthenticated network service has acquired an identity. The setup and security guidance still recommend authentication for anything reachable beyond the operator's private machine.
A practical rule follows: first ask which person or delegated principal is represented, then ask which rows belong to that principal. Do not reverse those questions. A record being found through an application process does not make it shared, and a token being attributed to an owner does not make it equivalent to the owner's browser session.
The server, not the model, grants tools
Once identity and ownership are resolved, Odysseus makes a second authorization decision around the agent. The model may propose a tool by emitting a fenced block or a native function call, but that proposal is not authority. The server-side policy checks the tool name before dispatch. Non-admin owners are blocked from shell and Python execution, file mutation, memory and task management, email, MCP, settings, webhooks, model management, model serving, vault access, and other operations that expose runtime access, sensitive data, persistent state, or external side effects. Dynamically named mcp__... tools are blocked as a namespace for non-admins rather than relying on a complete list of current plugins.
The gate operates in two places. Odysseus can omit a tool from the prompt and function schemas, reducing the chance that a model will request it. It also rejects a request at execution time if the model emits a blocked name anyway. That duplication is important because prompt construction is not an access-control mechanism. A model can hallucinate a tool, reproduce a stale schema, or receive a name from external content. The dispatch layer remains the final decision point.
The policy also treats the pre-setup window cautiously. If authentication is enabled but no administrator has been created, the system does not interpret “no configured users” as permission for shell or Python. Explicit AUTH_ENABLED=false has a different meaning: the operator has chosen single-user mode, so the local owner can receive the corresponding access. These states may look similar from the absence of a username, but they have opposite authority implications.
Route-level checks reinforce the tool gate. Administrative endpoints call require_admin, and the agent's route-like tools are blocked for non-admin owners before they reach their handlers. This matters for operations such as changing endpoints, managing MCP servers, configuring webhooks or tokens, and altering settings. The model is not trusted to remember that an otherwise valid action belongs to an administrator; the request path checks it.
A server-side gate is not the same as a sandbox. It can decide that a non-admin should not receive Bash, but it does not make Bash safe for an administrator. It can hide MCP tools, but it does not make every allowed external server trustworthy. It can stop a settings action, but it does not turn a broadly privileged account into a narrowly scoped role. The gate answers “may this identity invoke this tool?” Later controls must answer what the tool can reach and what its result means.
Why a delegated token gets less authority than its owner
Odysseus's integration API makes this distinction explicit. API tokens can be created only through an admin-gated route. They are stored as bcrypt hashes, shown later only through a prefix, and assigned scopes such as chat, todo, document, email, calendar, memory, or Cookbook access. Write scopes automatically bring the related read scope with them. The /api/codex/* surface checks the required scope before resolving the token to its owner and passing that owner to the existing data handler. A caller can discover its available capability set and receives a 403 when the requested scope is missing.
This is more precise than giving an integration the owner's browser cookie, but it is not the same authority as the owner's interactive session. The agent loop marks bearer-token work as delegated. Because a token may be handed to a script, a third-party integration, or another automated process, there is no human at the keyboard who can grant an interactive continuation in the middle of a run. Odysseus therefore caps delegated agent runs at the non-admin tool policy even when the token was minted by an administrator. The owner is still used for data attribution; the owner's interactive powers are not inherited.
Consider two requests with the same owner. A token with document read and write scopes may create or edit an owner-scoped document through the integration API. That does not imply that it may open a shell, read arbitrary files, register an MCP server, or change all application settings. Similarly, a token with cookbook:launch can reach a powerful model-serving surface, but that scope is still a named capability rather than a general administrative session. The danger has been narrowed, not eliminated.
The trade-off is that the scopes remain coarse. They express useful domains, but not every individual privilege inside a domain or every constraint that a human administrator might want to impose. The source also retains null-owner shared rows for legacy compatibility, so ownership behavior is route-specific rather than a universal promise that every record is private. Even ordinary privilege evolution needs care: the shared route helper treats a missing privilege key as permitted, which makes schema migration convenient but means a new privilege should not be assumed to fail closed merely because it is absent from an old account record.
These are not reasons to discard scoped integrations. They identify the decision an operator is making. A token is safer when it has only the scopes the integration needs, is separated by integration, and can be revoked without giving the integration the browser session. But “scoped” should be read as “bounded by the listed contract,” not as “safe to expose without reviewing the effect of each scope.”
The diagram's two paths converge on owner-scoped work but not on identical authority. A browser session can be evaluated as an interactive account. A bearer token is a durable credential acting for that account, so its data ownership and its action budget must be kept separate.
The remaining boundary is content provenance. Even a permitted interactive user or a correctly scoped integration may ask an agent to read a web page, email, memory, skill, or tool result. That material can contain instructions aimed at the model. The next question is therefore not whether the caller is allowed to use the agent, but whether the agent should retain the same action budget after untrusted content has entered its context.