Skip to main content
explanation

Why can a dashboard read a personality but not expand its toolset?

Context​

A personality is a structural unit that binds prompt, tools, memory scope, and model into one configuration. Personality is architecture explains the internal design. This page is about the external surface: what a Mission Control dashboard can observe through the SDK, and where the boundaries are.

What a dashboard can read​

The personalities namespace in the contract exposes three read operations that matter for dashboard rendering:

personalities.list returns every personality with its id, name, description, model, provider, and toolset array. A dashboard uses this to populate a picker — the user selects which personality to chat with or inspect.

personalities.get takes a personality id and returns the full PersonalityConfig plus the raw Markdown body of SOUL.md. A dashboard can render the identity document directly — it is Markdown, designed to be read.

personalities.characterSheet takes a personality id and returns a generated Markdown character sheet — the same artifact ethos personality show prints in the CLI. This is the canonical one-screen summary of what a personality is: identity, model routing, memory scope, toolset, MCP servers, plugins, filesystem reach. The sheet is regenerated on each call via renderCharacterSheet in @ethosagent/personalities; it reflects the current on-disk state, not a cached snapshot.

A dashboard that wants to show "who is this personality" renders the character sheet. A dashboard that wants to let a user edit a personality reads get and presents the fields.

What a dashboard cannot change​

Toolset enforcement is server-side. DefaultToolRegistry.toDefinitions(allowedTools) in packages/core/src/tool-registry.ts filters which tool definitions the LLM sees, and executeParallel rejects calls outside the allowlist. The personality's toolset array declares which tool names are permitted.

A dashboard receives the toolset array from personalities.get, but listing tools in a response does not grant them. A malicious or buggy dashboard cannot expand a personality's capabilities by sending tool names that are not in the allowlist — the server-side registry ignores them. The enforcement is at the AgentLoop level: agent-loop.ts reads personality.toolset and passes it to toDefinitions. Tools outside the list produce an error tool_result with is_error: true.

This means a dashboard UI should display the toolset as a read-only badge list, not an editable checklist, unless the dashboard is specifically designed as a personality editor. Even then, saving a modified toolset through personalities.update only takes effect after the agent loop re-reads the personality — the running session's tool gate does not change mid-turn.

Why toDefinitions matters for UI​

When a dashboard renders a personality's capabilities, it should show the tools the personality actually has access to, not every tool registered in the system. The toolset array from personalities.get is the authoritative list.

Built-in tools are gated by exact name match against allowedTools. MCP and plugin tools are gated separately through passesFilter() — their names are dynamic (mcp__<server>__<tool>) and matched against the personality's mcp_servers and plugins arrays rather than the toolset.

A dashboard that wants to show "this personality can use: read_file, write_file, bash" reads personality.toolset. A dashboard that wants to show "this personality has access to MCP server X" reads personality.mcp_servers. The two filtering paths are independent.

The SOUL.md boundary​

SOUL.md is the first-person identity document — "who am I, how do I speak." A dashboard can read it via personalities.get and display it. A dashboard can write to it via personalities.update with the soulMd field.

The content of SOUL.md flows into the agent's system prompt. A dashboard that edits it is editing the personality's identity. This is powerful and intentional — the web UI is the editor for personality identity. But it also means a dashboard must treat SOUL.md as a privileged write. Validation is minimal (it is free-form Markdown); the governance constraint is social, not mechanical.

Skills per personality​

Each personality can have its own skill library, separate from the global ~/.ethos/skills/ directory. The contract exposes CRUD operations: skillsList, skillsGet, skillsCreate, skillsUpdate, skillsDelete, and skillsImportGlobal (which copies global skills into the personality's local skill directory).

A dashboard that manages per-personality skills uses these endpoints. The skills are Markdown files with optional YAML frontmatter. Importing a global skill does not create a live link — it copies the content at the time of import. Subsequent edits to the global skill do not propagate.

Memory scope from a dashboard perspective​

A personality has no memory scope field. Its memory is always its own: turn setup fixes the scope at personality:<id> (memScopeId in packages/core/src/agent-loop/stages/turn-setup.ts), so the researcher's MEMORY.md and USER.md are never the engineer's. memory.get therefore takes a personalityId and returns that personality's file. Passing a userId with store: 'user' returns a person's profile from user:<userId> instead — the copy a gateway turn reads for a sender its identity map resolved.

The SDK's memory namespace builds every path server-side — a dashboard does not construct file paths or manage directories. A dashboard that lets users switch personalities re-fetches memory content after the switch, because the backing file is a different one.

Create, duplicate, delete​

The contract supports full lifecycle management for personalities. personalities.create takes an id (lowercase, directory-safe), name, toolset array, and soulMd body. personalities.duplicate clones an existing personality to a new id. personalities.delete removes it.

These mutations affect the on-disk personality directory under ~/.ethos/personalities/. The FilePersonalityRegistry is mtime-cached — it re-reads a personality only when one of the files it fingerprints changes. A dashboard that creates or updates a personality sees the change reflected immediately in subsequent list or get calls because the server writes the file and the next registry load picks it up.

Summary of boundaries​

OperationDashboard canDashboard cannot
List personalitiesYes — personalities.list
Read identityYes — get returns SOUL.md
Read character sheetYes — characterSheet returns the generated summary
Edit identityYes — update with soulMd
See toolsetYes — toolset array in the response
Expand toolset at runtimeNo — server-side enforcement in toDefinitions
Manage per-personality skillsYes — skillsList, skillsCreate, etc.
Change model routing mid-turnNo — resolved once per turn in turn setup (resolveTurnModel)