Trust Boundaries
| ID | Boundary | Description | Controls (Inside) | Does NOT Control (Outside) |
|---|---|---|---|---|
| TB1 | User Input → Agent Engine | Where user-typed prompts enter the agent graph | Prompt routing, session threading, UI rendering | Prompt content — any text accepted |
| TB2 | LLM Decision → Tool Execution | HITL gate on all side-effecting tool calls | Interrupt map, allow-list check, auto-approve toggle | LLM reasoning; what user approves |
| TB3 | Tool Result → LLM Context | Tool outputs re-enter the context window | Unicode warnings on URLs in args; markdownify HTML conversion | Content of fetched web pages, MCP responses, search results |
| TB4 | MCP Config → Process / Network | .mcp.json triggers subprocess spawn or network connection | Schema validation; per-server allow/deny lists + interactive approval prompt | What the MCP process does once trusted and running |
| TB5 | Hooks Config → Subprocess | hooks.json commands execute as local subprocesses | Workspace trust for project hooks, schema validation, bounded execution, sanitized environment | Command content (user-authored) |
| TB6 | Setup Script → Sandbox Execution | User-supplied script runs inside sandbox at startup | shlex.quote() wrapping; string.Template.safe_substitute | Script content (user-authored); sandbox network access |
| TB7 | External LLM API → Agent State | LLM API responses drive tool call decisions | LLM client configuration, model selection, request params | LLM output content; provider-side safety |
| TB8 | CLI → Server Subprocess IPC | Config passed from CLI to langgraph dev subprocess via DA_SERVER_* env vars | ServerConfig.to_env() serialization; env var scoping to parent+child process | Subprocess environment post-fork; /proc visibility to same-uid processes |
| TB9 | LocalContextMiddleware → Host Env | Bash detect script output (git info, project files, Makefile) injected into system prompt | Script is framework-generated static code; 30s timeout; exit-code check | Content of Makefile, pyproject.toml, git branch names, directory listing |
| TB10 | RemoteAgent → LangGraph Dev Server | CLI communicates with agent via HTTP+SSE on localhost | Server bound to 127.0.0.1 (client/launch/server.py:_DEFAULT_HOST); ephemeral per session | No authentication (LANGGRAPH_AUTH_TYPE=noop); any localhost process can reach the API |
| TB11 | Config File → Code Execution | class_path in config.toml triggers importlib.import_module(); project/global .env values are loaded into the process environment and can reach Bash startup hooks | Format validation (module:ClassName); issubclass(BaseChatModel) check; dotenv loading denies shell startup / environment-hijack keys (BASH_ENV, ENV) | Module-level side effects execute during import; user controls config file; project files in the working directory influence execution |
| TB12 | Goal/Rubric State → Model Context | Persisted user- and agent-controlled goal state becomes a synthetic HumanMessage in a primary-model request | State projection, lifecycle filtering, notice fingerprinting, raw-character limits, HTML escaping of boundary tags | Natural-language instructions, sensitivity, post-escape size, and provider-specific byte/token budgets |
| TB13 | Managed Config → Runtime | A fixed administrator-deployed TOML file overrides CLI, environment, and user preferences | Fixed non-redirectable path; the CLI never writes the file; fail-closed startup for every command except diagnostics; typed resolution; model-policy checks before credentials/imports/construction; diagnostics remain available | Filesystem ownership/mode and privileged deployment are outside the CLI; a host administrator can weaken or strengthen policy |
| TB14 | Launch Env → User Trust Root | Inherited DEEPAGENTS_HOME selects the profile whose config, credentials, and user MCP file are trusted | Captured and normalized once before dotenv loading; absolute/~/ validation; denied from every dotenv layer; propagated unchanged to the server | A user can deliberately select a profile inside or above a checkout; only the exact profile .mcp.json receives user provenance |
Boundary Details
TB1: User Input → Agent Engine
- Inside:
app.DeepAgentsApproutes keystrokes to message queue;client.non_interactive.run_non_interactivereads from stdin/argv. Both paths now go throughRemoteAgent.astreamto the server. Session ID assigned per thread (sessions.generate_thread_id). - Outside: Prompt content — the agent accepts any text the user types. No content filtering at this layer (HITL handles downstream tool calls, not prompt intent).
- Crossing mechanism: HTTP POST to
127.0.0.1:\{port\}viaclient.remote_client.RemoteAgent.astream.
TB2: LLM Decision → Tool Execution Gate (HITL)
- Inside:
agent._add_interrupt_onregisters interrupt configs forexecute,write_file,edit_file,web_search,fetch_url,task,compact_conversation,launch_async_subagent,update_async_subagent,cancel_async_subagent. In non-interactive mode,client.non_interactive._handle_action_requestenforces the shell allow-list viaconfig.is_shell_command_allowed. - Outside: Once the user clicks "approve" (interactive) or a command passes the allow-list check (non-interactive), the tool executes with no further framework-level gating.
- Crossing mechanism: LangGraph HITL interrupt routed through
RemoteAgentSSE stream. - Key note:
auto_approvemode bypasses all HITL approval prompts while still displaying Unicode/URL warnings. - Key note: This boundary gates the model-initiated
compact_conversationtool.- Authorization: The explicit
/offloadcommand does not cross it. C18 invokes the agent's shared compaction service directly, with no tool node and no synthetic message. The slash command is the authorization. - Hook events: The operation still dispatches
PreCompactandPreToolUseagainst an in-memory forced call. Hooks may veto or interrupt. The TUI returns opaque hook replies over the operation protocol. askis fail-closed: APreToolUseaskdecision cannot prompt on this path. The operation transport carries hook invocations, not HITL review requests, andinterrupt()requires a Pregel task._ask_permission_via_hitltherefore convertsaskinto a deny that carries the reason.- Archive write: It reaches
backend.awrite()without traversing tool approval. See DF25 and DF26. - Unsupported deployments: Local in-process
Pregelagents, including ACP mode, do not support/offload. Custom and older servers without C18 fail at the HTTP boundary rather than entering a client-driven tool path.
- Authorization: The explicit
- Key note: Only the pre hook events fire for
/offloadthrough C18.PostToolUse/PostToolUseFailure, whichServerHooksMiddlewarerecords inawrap_tool_calland dispatches from_before_modelvia_maybe_post_tool_useon the next model turn, do not fire: there is no tool node to record the pending entry, and no following model turn to drain it. Likewise an allowingPreToolUsehook'sadditionalContextis discarded (logged, not injected): there is no tool result to carry it.
TB3: Tool Result → LLM Context
- Inside:
agent._format_execute_description/_format_fetch_url_descriptionscan tool arguments (not results) for hidden Unicode and suspicious URLs.fetch_urlconverts HTML to markdown viamarkdownifybefore returning. - Outside: The content of tool results (fetched web pages, web search snippets, MCP tool responses,
executestdout) is passed verbatim into the LLM context window. No prompt-injection scanning of results. - Crossing mechanism: LangGraph
ToolMessagereturned to agent graph state; streamed to CLI via SSE.
TB4: MCP Config → Process / Network
- Inside:
mcp_tools._validate_server_configvalidates JSON structure and field types. Discovery emits typed(path, scope, project_root)records; runtime and login preserve that provenance instead of inferring trust from path containment. Only the exact configured profile.mcp.jsonis a user candidate. Standard project-root and.deepagents/.mcp.jsoncandidates remain project-scoped whenDEEPAGENTS_HOMEis an ancestor of, inside, or equal to the checkout; a user/project path or symlink collision fails closed to project scope. Project-level servers (stdio and remote) require approval: an interactive prompt (main._check_mcp_project_trust) that offers allow-for-this-session ("y"), remember a chosen subset of servers ("r"), or deny ("N") — unless--trust-project-mcpis set, which trusts the whole config for the run. Whole-config trust is never persisted; it exists only as in-memory run state, set by--trust-project-mcpor by an interactive allow-once/remember decision. The remembered subset governs only what is persisted for future runs — both allow-once and remember load every prompted server for the current session. Persistent "remember" (a.k.a. "always allow") decisions are stored in[mcp].enabled_project_server_approvalswith a local-project identity, server name, and server definition fingerprint. Remote server approvals with fixed URLs are shared by main checkouts and linked worktrees only through validated reciprocal Git common-directory metadata; independent clones remain separate, and non-Git or malformed/forged metadata falls back to the exact resolved root. Remote definitions with interpolated URLs remain exact-worktree scoped because project.envfiles can resolve the same template to different endpoints. Local stdio server approvals always use the exact resolved worktree root because the same command can execute different files in another checkout. A changed command, URL, or transport under the same name requires re-approval. Users can reject individual project server names via[mcp].disabled_project_servers(orDEEPAGENTS_CODE_DISABLED_PROJECT_MCP_SERVERS). There is also an explicit process-wide escape hatch,DEEPAGENTS_CODE_DANGEROUSLY_ENABLE_PROJECT_MCP_SERVERS, that approves matching names globally and intentionally bypasses the project/fingerprint binding. Reject wins over approval and over full trust, with one documented exception: if the user'sconfig.tomlis unreadable (see below), any deny defined only there is lost, so a name that is both TOML-disabledand exported inDEEPAGENTS_CODE_DANGEROUSLY_ENABLE_PROJECT_MCP_SERVERSsurvives — an accepted footgun (load_mcp_server_trust_listsdocuments it inline) that requires a self-contradicting config plus the explicit dangerous opt-in, and the read error is surfaced to the user. These policies are read bymodel_config.load_mcp_server_trust_listsonly from user-controlled sources — the user-level profileconfig.toml, its global.env, and shell-exported env — never from.mcp.jsonor any repo-committed file, so a committed config cannot self-approve its own servers. In particular, the trust-list env forms are denied from project dotenv, whileDEEPAGENTS_HOMEis denied from every dotenv source under TB14. The dangerous enable env var and scoped TOML approvals are independent grants: setting the env var, including to an empty value, does not suppress remembered approvals. Disabled unions TOML and env so a deny can never be silently emptied by the other source. If the user'sconfig.tomlexists but cannot be read or parsed, the loader fails closed (project configs are treated as untrusted and the error is surfaced) rather than proceeding with an empty deny list. Only scoped-approved, dangerously env-enabled, or fully trusted names survive into the merged config, so a non-approved remote entry is never preflighted and its interpolated headers are never resolved. - Outside: Stdio server
command,args, andenvfields are user-controlled strings passed directly toStdioConnection. Theenvdict from MCP config is forwarded without filtering — users can set arbitrary environment variables (includingPATH,LD_PRELOAD,PYTHONPATH) for the MCP subprocess. - Crossing mechanism:
subprocess.Popen(vialangchain_mcp_adapters) for stdio; HTTP/SSE for remote servers.
TB8: CLI → Server Subprocess IPC
- Inside:
ServerConfig.to_env()serializes all config (model, assistant_id, system_prompt, sandbox settings, cwd, MCP config path, shell-enable flags) toDA_SERVER_*env vars.client.launch.server_manager._apply_server_configwrites these toos.environbeforesubprocess.Popen.client/launch/server.py:_build_server_envstrips cloud auth vars (LANGGRAPH_CLOUD_LICENSE_KEY,LANGSMITH_CONTROL_PLANE_API_KEY,LANGSMITH_TENANT_ID,LANGGRAPH_AUTH). - Outside: The full parent environment including provider API keys flows to the child via
os.environ.copy(). The system prompt string becomesDA_SERVER_SYSTEM_PROMPT. After process start, the env is fixed — no runtime mutation across processes. - Crossing mechanism:
subprocess.Popenenv kwarg; subsequentos.environreads in_server_config.ServerConfig.from_env.
TB9: LocalContextMiddleware → Host Environment
- Inside:
local_context.build_detect_scriptgenerates a static bash heredoc that runs git commands, checks for lock files, reads Makefile headings, and produces a structured markdown summary. Script is static framework code — not constructed from user input. Exit code checked via_handle_detect_result. 30-second timeout. - Outside: The script reads and includes the first 20 lines of
Makefile(_section_makefile) and a directory listing. These file contents are not sanitized before injection into the system prompt. An attacker who can write to the CWD's Makefile can influence the system prompt. - Crossing mechanism:
backend.execute(DETECT_CONTEXT_SCRIPT)→ result appended tosystem_promptviaLocalContextMiddleware._get_modified_request.
TB10: RemoteAgent → LangGraph Dev Server
- Inside: Server bound to
127.0.0.1by default;client/launch/server.py:_DEFAULT_HOST = "127.0.0.1".RemoteAgentonly connects to the URL returned byServerProcess.url. Server is ephemeral — started at session start, stopped at session end. Binds a free ephemeral port by default (client/launch/server.py:_EPHEMERAL_PORT); an explicit port is honored but still falls back to a free port if occupied. - Outside:
LANGGRAPH_AUTH_TYPE=noopdisables all LangGraph server authentication. Any process on localhost that discovers the port can submit requests, read thread state, or inject messages. - Crossing mechanism: HTTP POST/GET to
http://127.0.0.1:\{port\}usinglanggraph.pregel.remote.RemoteGraphfor graph operations and the same configured HTTP client for C18. - Key note: Graph registration and the accepted payload are both narrowed here.
- Registration: The default built-in
graph_refregisters oneagentgraph plus the C18 custom HTTP app. A customgraph_refregisters only its graph and does not support/offload. - Accepted: Operation identity, model/hook context, and opaque hook replies.
- Rejected: Messages, checkpoint identifiers, graph names, and state updates. The server refuses any operation update whose channels fall outside
OffloadStateUpdate. - Checkpoint invariant: The server reads and hydrates checkpoint messages itself and rejects active, pending, or changed threads. Its final thread-state update is state-only and targets the latest checkpoint, so it cannot branch from a stale checkpoint and hide concurrently appended messages.
- Enforced by:
offload_api._execute_offloadandclient.remote_client.RemoteAgent.aoffload.
- Registration: The default built-in
TB11: Config File → Code Execution
- Inside:
config._create_model_from_classvalidatesclass_pathformat (module:ClassName), imports the module viaimportlib.import_module(), and checksissubclass(cls, BaseChatModel)before instantiation. - Outside: Module-level code in the imported module executes unconditionally during
import_module(). Theissubclasscheck only runs after import. Any side effects (file I/O, network calls, subprocess spawning) in the module's top-level scope execute before the type check. - Crossing mechanism:
importlib.import_module(module_path)inconfig._create_model_from_class. - Inside (dotenv):
config._load_dotenvloads project and global.envvalues withoverride=Falseand drops shell startup / environment-hijack keys (BASH_ENV,ENV, and related) so a project.envcannot register a script that Bash would source at startup.DEEPAGENTS_HOMEis also denied from both layers because profile selection has already crossed TB14. The denylist is best-effort for execution-hook variables: it enumerates variables known to reach a code-execution consumer (a shell's startup hook, the dynamic linker, an interpreter's startup path) and cannot claim completeness — any tooldcodespawns that honors an environment-driven execution hook not yet in the list remains a live vector until the key is added. The preview path (_preview_dotenv_environ) applies the same denylist so a dry-run config change cannot report a value a real reload would reject. - Outside (dotenv): All other
.envkeys are applied to the process environment, and any project file in the working directory (.env,Makefile, build scripts) can still influence execution. See T12.
TB12: Goal/Rubric State → Model Context
Inside:
goal_state_notice.project_goal_stateonly exposes an objective and status note while the goal is actionable, andbuild_goal_state_noticeHTML-escapes embedded text before wrapping it in boundary tags.GoalToolsMiddlewarefingerprints notices, appends a fresh notice when state is stale or compacted, and re-pins the notice into model requests when necessary. Earlier bounded notices remain unchanged for prompt-cache stability; oversized legacy notices are replaced transiently with bounded same-index stand-ins, and each fresh notice explicitly supersedes them.Outside: Goal objectives and criteria originate from user input.
/rubric filereads the selected local text file. Status notes can be model-authored throughupdate_goal.goal_state_limitsrejects raw text above these limits:Value Limit (characters) Goal objective 8,000 Rubric or acceptance criteria 12,000 Accepted objective and criteria combined 12,000 Status note or prior blocker 4,000 Total text across one notice 16,000 These counts do not account for expansion during HTML escaping, provider tokenization, or a provider-specific request budget. Labels such as "context data, not instructions" and escaping preserve message structure. They do not prevent the model from interpreting natural-language content as instructions. This flow has no provider-transmission confirmation.
Crossing mechanism:
DeepAgentsApp._persist_goal_rubric_statewrites state and notices to the checkpoint;GoalToolsMiddleware._notice_updateand_request_with_goal_noticeappend them to the persisted or transient model-message list.
TB13: Managed Config → Runtime
Inside: The resolver reads one fixed OS path. The writer rejects that path, so the managed source is read-only by guard and not only by convention. Valid managed values take the highest precedence. The rules are:
- Tables deep-merge. Deny lists union. An explicit managed allow or trust list replaces lower-precedence grants.
[models].allowedis a local-model ceiling of exactprovider:modelspecs andprovider:*wildcards. Discovery and selector filtering improve usability only.create_modelis authoritative. It checks the canonicalprovider:modelbefore credential bridging, provider hooks, imports, or constructors, so a blocked specification touches no stored key and runs no provider hook.- Preflight checks on text a user typed resolve a bare name to the same canonical form first, so they neither reject a model that construction would allow nor accept one it would block. A name whose provider cannot be established stays unmatchable.
create_cli_agentchecks every model string it forwards: the primary model, the Auto classifier, the rubric grader, and an explicit local-subagent model. The SDK resolves a string throughinit_chat_model, which does not pass throughcreate_model. A prebuilt model object came from a path that already checked.- Runtime-context switches are rechecked server-side. A policy denial propagates instead of falling back to the previous model.
- A malformed
[models].allowedblocks all model use at either layer. The managed layer also refuses to start. - Remote async-subagent deployments select their models outside this local boundary. Tool enumeration also skips the check, because it compiles a graph it never invokes.
- A managed scalar replaces a colliding user table at any depth, so a user cannot defeat policy by changing the shape of a key. This holds on the top-level merge and inside a structured table: both apply the manifest validator, so the effective value and the audited provenance agree.
- A wrong-typed managed scalar is skipped, and the lower-precedence value stays in effect.
- Inside a structured table (
[models.providers],[themes],[async_subagents],[sandboxes.providers],[ui.terminal_themes],[threads.columns]) the dedicated typed reader validates instead. A wrong-typed managed leaf there can displace a valid user leaf, after which the reader falls back to the built-in default. - An enforced key whose managed value cannot be applied stops every command except the diagnostics listed below. It also blocks
/reload. Skipping it would leave the user's CLI flag or environment variable in force. The enforced keys arestartup.mode,startup.yolo_switcher,shell.allow_list,skills.extra_allowed_dirs,interpreter.enable_interpreter,interpreter.ptc,interpreter.ptc_acknowledge_unsafe,models.allowed,models.auto_classifier,runtime.recursion_limit,sandboxes.default, andtracing.langsmith_redact. - Three conditions make an enforced key unapplicable: a wrong-typed value, a
runtime.recursion_limitoutside its bounds, and a key made unreachable by a scalar ancestor (startup = "manual"in place of[startup]andmode). A managed[sandboxes].defaultthat names an unavailable backend stops a sandboxed launch. - A scalar at a known section is rejected for the same reason, so it cannot replace the user's whole section.
[effort]is included, although it has no manifest option. - Any other rejected managed value is ignored, and
dcode doctoranddcode configname it. An ignored key is never silent. - A missing file is accepted and applies no policy.
- A present unreadable, undecodable, or syntactically corrupt file blocks every command except
--help,--version,help,config,doctor, andauth path. It also blocks/reload. - On Windows, a ProgramData directory that cannot be read from the registry blocks the same commands. The path would be a guess, and an empty read at a guessed path does not prove that no policy is deployed. Reporting it as a missing file made every managed setting silently inert on a host whose ProgramData is relocated.
- A failed
/reloadkeeps the last enforceable snapshot, so policy is never dropped mid-session. A snapshot that parses but cannot be enforced is not cached either, so a rejected edit cannot become the generation later readers observe. - An unusable user
config.tomldrops only the user layer, so managed policy still applies. - A deny list that cannot be read is treated as denying everything, never as empty. This covers a managed
[mcp].disabled_serversthat is neither an array of names nor a comma-separated string, and a[mcp]section that is not a table. A managed[mcp].enabled_project_server_approvalsthat is not an array is treated the same way: the key is present, so policy means to narrow access, and reading its presence as absence would keep both the user's approvals and theDEEPAGENTS_CODE_DANGEROUSLY_ENABLE_PROJECT_MCP_SERVERSbypass in force.
Outside: Ownership, permission-mode validation, privileged installation, and
sudopolicy are deployment responsibilities. Anyone who can replace the administrator-managed file can control model, sandbox, interpreter, MCP trust, and other supported runtime settings. Model policy constrains official dcode selection and construction paths; it does not stop a hostile local user from replacing the package, modifying the process, or calling a provider SDK directly. On macOS, note that stock/Library/Application Supportis group-writable byadmin, so any admin-group member can create the file and grant themselves policy. Deployments that rely on this boundary must tighten ownership and mode on thedcodedirectory.Remote source: The fixed local file may contain only
[managed_config].source. Its administrator-owned HTTPS URL is then the explicit destination allowlist. The remote document becomes the whole managed policy. It cannot chain to another source. URLs with userinfo, query strings, or fragments are rejected. So are URLs carrying whitespace, control characters, or any non-ASCII character: confusable host text must not reach operator output, andurllibdoes not encode a non-ASCII request target consistently.- Requests validate the certificate against Python's default TLS trust store, bypass environment proxies, and refuse redirects. One five-second budget covers the whole fetch: the connection, the TLS handshake, the headers, and the body. The fetch is abandoned once that budget is spent. At most 1 MiB is accepted.
- Only a complete document is accepted. The status must be 200. The media type must be
application/toml,text/plain, orapplication/octet-stream, and the body must not be compressed. A captive portal or gateway error page that answers 200 withtext/htmlis rejected before it is parsed. This is not a complete guard, and it is not relied on as one: a response with noContent-Type, or one claimingapplication/octet-stream, is read, because a bare object store answers that way. Such a body still has to parse as TOML and satisfy every rule below. Framing must show the body arrived whole: one matchingContent-Length, or chunked encoding. A response that sends both, or that repeatsContent-Lengthwith different values, is rejected — the first value wins inhttp.client, so a duplicate header would otherwise let a prefix pass as a whole document. A keyless document is rejected as a failed publish. Truncated TOML often still parses, so a partial document would otherwise enforce a policy with entries silently missing, and would replace the last enforceable generation. - The proxy bypass stops a local user redirecting the fetch through
HTTPS_PROXY. The trust store itself is not pinned.SSL_CERT_FILEandSSL_CERT_DIRstill select the certificate authorities, so the same actor can substitute the CA set for this request. This is the same deployment responsibility as the local file ownership in the Outside bullet above. This boundary does not apply on a host where an unprivileged user controls thedcodeprocess environment. - No disk cache or authentication material is stored. Private enterprise hosts remain reachable because the local trust anchor, not user input, selects the host. A fetch failure stops startup. A failed reload keeps the process's last enforceable snapshot, and
dcode doctorreports that the refresh failed so a host that stopped answering is not silent.
Crossing mechanism: Synchronous local file read through
configuration.TomlFileProvider, optionally followed by one bounded HTTPS fetch throughconfiguration.RemoteTomlProvider, then typed resolution and CLI/server startup gates.
TB14: Launch Environment → User Trust Root
- Inside:
_paths.PATHScapturesDEEPAGENTS_HOMEbefore either dotenv layer is inspected. Empty/unset selects the launch user's~/.deepagents; absolute paths and a leading~/are normalized without requiring the target to exist. Other relative paths and~userforms stop startup, as do a dangling symlink and a root that exists but denies traversal. The unreadable case is rejected explicitly:Path.is_symlinkreportsFalseunder EACCES, so it would otherwise pass every guard and fail one file at a time later. The normalized absolute value is frozen for the client and explicitly copied into every server subprocess environment, so reloads, cwd changes, and server cwd differences cannot move the profile. - Trust invariant: Project-controlled input cannot relocate the user trust root.
DEEPAGENTS_HOMEis in the deny set shared by project and global dotenv loading. MCP discovery separately records user/project provenance and gives project scope precedence when standard discovery locations collide.$HOMEitself is rejected at startup, in any spelling: the comparison uses device and inode, so a symlink or a case variant on a case-insensitive filesystem is caught too. The home comparison fails closed: when identity cannot be determined at all, startup stops rather than accepting the path. For the roots that are accepted — an ancestor of a checkout, the checkout root, or a directory inside it — collision handling keeps the checkout's standard MCP files project-scoped.PATHis treated the same way: the installation-scoped managed bin may be prepended directly, but a verifiedrgin the profile fallback is exposed through a process-private shim containing only that entrypoint. A repository-controlled<profile>/binand every sibling executable in it stay offPATHfor agent subprocesses. - Outside: A shell or process supervisor can deliberately select any accepted absolute profile path at launch. Protecting ownership and permissions on that selected directory is the operator's responsibility.
- Crossing mechanism: Inherited process environment → immutable standard-library-only path snapshot → normalized child environment.