1"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([["4427"],{6058(e,s,t){t.r(s),t.d(s,{metadata:()=>n,default:()=>h,frontMatter:()=>i,contentTitle:()=>o,toc:()=>l,assets:()=>d});var n=JSON.parse('{"id":"building/explanation/personality-from-outside","title":"Why can a dashboard read a personality but not expand its toolset?","description":"What an external dashboard can read about a personality, what it cannot change, and why the toolset boundary matters for UI rendering.","source":"@site/content/building/explanation/personality-from-outside.md","sourceDirName":"building/explanation","slug":"/building/explanation/personality-from-outside","permalink":"/docs/building/explanation/personality-from-outside","draft":false,"unlisted":false,"editUrl":"https://github.com/ethosagent/ethos/tree/main/docs/content/building/explanation/personality-from-outside.md","tags":[],"version":"current","frontMatter":{"title":"Why can a dashboard read a personality but not expand its toolset?","description":"What an external dashboard can read about a personality, what it cannot change, and why the toolset boundary matters for UI rendering.","kind":"explanation","audience":"developer","slug":"personality-from-outside","updated":"2026-09-13T00:00:00.000Z"},"sidebar":"docsSidebar","previous":{"title":"Why oRPC over GraphQL, tRPC, or REST?","permalink":"/docs/building/explanation/why-orpc"},"next":{"title":"Why does a dashboard see four layers of memory instead of one?","permalink":"/docs/building/explanation/sdk-memory-model"}}'),r=t(8265),a=t(7214);let i={title:"Why can a dashboard read a personality but not expand its toolset?",description:"What an external dashboard can read about a personality, what it cannot change, and why the toolset boundary matters for UI rendering.",kind:"explanation",audience:"developer",slug:"personality-from-outside",updated:new Date("2026-09-13T00:00:00.000Z")},o,d={},l=[{value:"Context",id:"context",level:2},{value:"What a dashboard can read",id:"what-a-dashboard-can-read",level:2},{value:"What a dashboard cannot change",id:"what-a-dashboard-cannot-change",level:2},{value:"Why toDefinitions matters for UI",id:"why-todefinitions-matters-for-ui",level:2},{value:"The SOUL.md boundary",id:"the-soulmd-boundary",level:2},{value:"Skills per personality",id:"skills-per-personality",level:2},{value:"Memory scope from a dashboard perspective",id:"memory-scope-from-a-dashboard-perspective",level:2},{value:"Create, duplicate, delete",id:"create-duplicate-delete",level:2},{value:"Summary of boundaries",id:"summary-of-boundaries",level:2}];function c(e){let s={a:"a",code:"code",em:"em",h2:"h2",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.h2,{id:"context",children:"Context"}),"\n",(0,r.jsxs)(s.p,{children:["A ",(0,r.jsx)(s.a,{href:"/docs/getting-started/glossary#personality",children:"personality"})," is a structural unit that binds prompt, tools, memory scope, and model into one configuration. ",(0,r.jsx)(s.a,{href:"/docs/building/explanation/personality-as-architecture",children:"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."]}),"\n",(0,r.jsx)(s.h2,{id:"what-a-dashboard-can-read",children:"What a dashboard can read"}),"\n",(0,r.jsxs)(s.p,{children:["The ",(0,r.jsx)(s.code,{children:"personalities"})," namespace in the contract exposes three read operations that matter for dashboard rendering:"]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.code,{children:"personalities.list"})})," returns every personality with its ",(0,r.jsx)(s.code,{children:"id"}),", ",(0,r.jsx)(s.code,{children:"name"}),", ",(0,r.jsx)(s.code,{children:"description"}),", ",(0,r.jsx)(s.code,{children:"model"}),", ",(0,r.jsx)(s.code,{children:"provider"}),", and ",(0,r.jsx)(s.code,{children:"toolset"})," array. A dashboard uses this to populate a picker \u2014 the user selects which personality to chat with or inspect."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.code,{children:"personalities.get"})})," takes a personality ",(0,r.jsx)(s.code,{children:"id"})," and returns the full ",(0,r.jsx)(s.code,{children:"PersonalityConfig"})," plus the raw Markdown body of ",(0,r.jsx)(s.code,{children:"SOUL.md"}),". A dashboard can render the identity document directly \u2014 it is Markdown, designed to be read."]}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.strong,{children:(0,r.jsx)(s.code,{children:"personalities.characterSheet"})})," takes a personality ",(0,r.jsx)(s.code,{children:"id"})," and returns a generated Markdown character sheet \u2014 the same artifact ",(0,r.jsx)(s.code,{children:"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 ",(0,r.jsx)(s.code,{children:"renderCharacterSheet"})," in ",(0,r.jsx)(s.code,{children:"@ethosagent/personalities"}),"; it reflects the current on-disk state, not a cached snapshot."]}),"\n",(0,r.jsxs)(s.p,{children:['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 ',(0,r.jsx)(s.code,{children:"get"})," and presents the fields."]}),"\n",(0,r.jsx)(s.h2,{id:"what-a-dashboard-cannot-change",children:"What a dashboard cannot change"}),"\n",(0,r.jsxs)(s.p,{children:["Toolset enforcement is server-side. ",(0,r.jsx)(s.code,{children:"DefaultToolRegistry.toDefinitions(allowedTools)"})," in ",(0,r.jsx)(s.code,{children:"packages/core/src/tool-registry.ts"})," filters which tool definitions the LLM sees, and ",(0,r.jsx)(s.code,{children:"executeParallel"})," rejects calls outside the allowlist. The personality's ",(0,r.jsx)(s.code,{children:"toolset"})," array declares which tool names are permitted."]}),"\n",(0,r.jsxs)(s.p,{children:["A dashboard receives the ",(0,r.jsx)(s.code,{children:"toolset"})," array from ",(0,r.jsx)(s.code,{children:"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 \u2014 the server-side registry ignores them. The enforcement is at the ",(0,r.jsx)(s.code,{children:"AgentLoop"})," level: ",(0,r.jsx)(s.code,{children:"agent-loop.ts"})," reads ",(0,r.jsx)(s.code,{children:"personality.toolset"})," and passes it to ",(0,r.jsx)(s.code,{children:"toDefinitions"}),". Tools outside the list produce an error ",(0,r.jsx)(s.code,{children:"tool_result"})," with ",(0,r.jsx)(s.code,{children:"is_error: true"}),"."]}),"\n",(0,r.jsxs)(s.p,{children:["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 ",(0,r.jsx)(s.code,{children:"personalities.update"})," only takes effect after the agent loop re-reads the personality \u2014 the running session's tool gate does not change mid-turn."]}),"\n",(0,r.jsx)(s.h2,{id:"why-todefinitions-matters-for-ui",children:"Why toDefinitions matters for UI"}),"\n",(0,r.jsxs)(s.p,{children:["When a dashboard renders a personality's capabilities, it should show the tools the personality ",(0,r.jsx)(s.em,{children:"actually has access to"}),", not every tool registered in the system. The ",(0,r.jsx)(s.code,{children:"toolset"})," array from ",(0,r.jsx)(s.code,{children:"personalities.get"})," is the authoritative list."]}),"\n",(0,r.jsxs)(s.p,{children:["Built-in tools are gated by exact name match against ",(0,r.jsx)(s.code,{children:"allowedTools"}),". MCP and plugin tools are gated separately through ",(0,r.jsx)(s.code,{children:"passesFilter()"})," \u2014 their names are dynamic (",(0,r.jsx)(s.code,{children:"mcp__<server>__<tool>"}),") and matched against the personality's ",(0,r.jsx)(s.code,{children:"mcp_servers"})," and ",(0,r.jsx)(s.code,{children:"plugins"})," arrays rather than the toolset."]}),"\n",(0,r.jsxs)(s.p,{children:['A dashboard that wants to show "this personality can use: read_file, write_file, bash" reads ',(0,r.jsx)(s.code,{children:"personality.toolset"}),'. A dashboard that wants to show "this personality has access to MCP server X" reads ',(0,r.jsx)(s.code,{children:"personality.mcp_servers"}),". The two filtering paths are independent."]}),"\n",(0,r.jsx)(s.h2,{id:"the-soulmd-boundary",children:"The SOUL.md boundary"}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"SOUL.md"}),' is the first-person identity document \u2014 "who am I, how do I speak." A dashboard can read it via ',(0,r.jsx)(s.code,{children:"personalities.get"})," and display it. A dashboard can write to it via ",(0,r.jsx)(s.code,{children:"personalities.update"})," with the ",(0,r.jsx)(s.code,{children:"soulMd"})," field."]}),"\n",(0,r.jsxs)(s.p,{children:["The content of ",(0,r.jsx)(s.code,{children:"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 \u2014 the web UI is the editor for personality identity. But it also means a dashboard must treat ",(0,r.jsx)(s.code,{children:"SOUL.md"})," as a privileged write. Validation is minimal (it is free-form Markdown); the governance constraint is social, not mechanical."]}),"\n",(0,r.jsx)(s.h2,{id:"skills-per-personality",children:"Skills per personality"}),"\n",(0,r.jsxs)(s.p,{children:["Each personality can have its own skill library, separate from the global ",(0,r.jsx)(s.code,{children:"~/.ethos/skills/"})," directory. The contract exposes CRUD operations: ",(0,r.jsx)(s.code,{children:"skillsList"}),", ",(0,r.jsx)(s.code,{children:"skillsGet"}),", ",(0,r.jsx)(s.code,{children:"skillsCreate"}),", ",(0,r.jsx)(s.code,{children:"skillsUpdate"}),", ",(0,r.jsx)(s.code,{children:"skillsDelete"}),", and ",(0,r.jsx)(s.code,{children:"skillsImportGlobal"})," (which copies global skills into the personality's local skill directory)."]}),"\n",(0,r.jsx)(s.p,{children:"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 \u2014 it copies the content at the time of import. Subsequent edits to the global skill do not propagate."}),"\n",(0,r.jsx)(s.h2,{id:"memory-scope-from-a-dashboard-perspective",children:"Memory scope from a dashboard perspective"}),"\n",(0,r.jsxs)(s.p,{children:["A personality has no memory scope field. Its memory is always its own: turn setup fixes the scope at ",(0,r.jsx)(s.code,{children:"personality:<id>"})," (",(0,r.jsx)(s.code,{children:"memScopeId"})," in ",(0,r.jsx)(s.code,{children:"packages/core/src/agent-loop/stages/turn-setup.ts"}),"), so the researcher's ",(0,r.jsx)(s.code,{children:"MEMORY.md"})," and ",(0,r.jsx)(s.code,{children:"USER.md"})," are never the engineer's. ",(0,r.jsx)(s.code,{children:"memory.get"})," therefore takes a ",(0,r.jsx)(s.code,{children:"personalityId"}
1)," and returns that personality's file. Passing a ",(0,r.jsx)(s.code,{children:"userId"})," with ",(0,r.jsx)(s.code,{children:"store: 'user'"})," returns a person's profile from ",(0,r.jsx)(s.code,{children:"user:<userId>"})," instead \u2014 the copy a gateway turn reads for a sender its identity map resolved."]}),"\n",(0,r.jsxs)(s.p,{children:["The SDK's ",(0,r.jsx)(s.code,{children:"memory"})," namespace builds every path server-side \u2014 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."]}),"\n",(0,r.jsx)(s.h2,{id:"create-duplicate-delete",children:"Create, duplicate, delete"}),"\n",(0,r.jsxs)(s.p,{children:["The contract supports full lifecycle management for personalities. ",(0,r.jsx)(s.code,{children:"personalities.create"})," takes an ",(0,r.jsx)(s.code,{children:"id"})," (lowercase, directory-safe), ",(0,r.jsx)(s.code,{children:"name"}),", ",(0,r.jsx)(s.code,{children:"toolset"})," array, and ",(0,r.jsx)(s.code,{children:"soulMd"})," body. ",(0,r.jsx)(s.code,{children:"personalities.duplicate"})," clones an existing personality to a new ",(0,r.jsx)(s.code,{children:"id"}),". ",(0,r.jsx)(s.code,{children:"personalities.delete"})," removes it."]}),"\n",(0,r.jsxs)(s.p,{children:["These mutations affect the on-disk personality directory under ",(0,r.jsx)(s.code,{children:"~/.ethos/personalities/"}),". The ",(0,r.jsx)(s.code,{children:"FilePersonalityRegistry"})," is mtime-cached \u2014 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 ",(0,r.jsx)(s.code,{children:"list"})," or ",(0,r.jsx)(s.code,{children:"get"})," calls because the server writes the file and the next registry load picks it up."]}),"\n",(0,r.jsx)(s.h2,{id:"summary-of-boundaries",children:"Summary of boundaries"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Operation"}),(0,r.jsx)(s.th,{children:"Dashboard can"}),(0,r.jsx)(s.th,{children:"Dashboard cannot"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"List personalities"}),(0,r.jsxs)(s.td,{children:["Yes \u2014 ",(0,r.jsx)(s.code,{children:"personalities.list"})]}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Read identity"}),(0,r.jsxs)(s.td,{children:["Yes \u2014 ",(0,r.jsx)(s.code,{children:"get"})," returns SOUL.md"]}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Read character sheet"}),(0,r.jsxs)(s.td,{children:["Yes \u2014 ",(0,r.jsx)(s.code,{children:"characterSheet"})," returns the generated summary"]}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Edit identity"}),(0,r.jsxs)(s.td,{children:["Yes \u2014 ",(0,r.jsx)(s.code,{children:"update"})," with ",(0,r.jsx)(s.code,{children:"soulMd"})]}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"See toolset"}),(0,r.jsxs)(s.td,{children:["Yes \u2014 ",(0,r.jsx)(s.code,{children:"toolset"})," array in the response"]}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Expand toolset at runtime"}),(0,r.jsx)(s.td,{}),(0,r.jsxs)(s.td,{children:["No \u2014 server-side enforcement in ",(0,r.jsx)(s.code,{children:"toDefinitions"})]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Manage per-personality skills"}),(0,r.jsxs)(s.td,{children:["Yes \u2014 ",(0,r.jsx)(s.code,{children:"skillsList"}),", ",(0,r.jsx)(s.code,{children:"skillsCreate"}),", etc."]}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"Change model routing mid-turn"}),(0,r.jsx)(s.td,{}),(0,r.jsxs)(s.td,{children:["No \u2014 resolved once per turn in turn setup (",(0,r.jsx)(s.code,{children:"resolveTurnModel"}),")"]})]})]})]})]})}function h(e={}){let{wrapper:s}={...(0,a.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},7214(e,s,t){t.d(s,{R:()=>i,x:()=>o});var n=t(141);let r={},a=n.createContext(r);function i(e){let s=n.useContext(a);return n.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function o(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),n.createElement(a.Provider,{value:s},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.