| title | Security |
|---|
This document focuses on:
- process isolation and filesystem sandboxing for long-lived runtime processes such as
mistermorph telegram,mistermorph slack,mistermorph line,mistermorph lark, ormistermorph console serve - secret handling for authenticated outbound HTTP calls (profile-based credential injection)
- Guard (M1): outbound allowlists, redaction, async approvals, and audit logs
mistermorph is an agent that can:
- make outbound HTTP(S) calls (LLM provider, web_search, url_fetch, Telegram)
- read local files (read_file tool, skill discovery)
- optionally execute shell commands (bash tool, if enabled)
The main security risks are:
- data exfiltration (sending local data to external services)
- secret leakage (prompts, tool params, logs, traces)
- over-broad capabilities (shell access, unrestricted networking, unrestricted filesystem reads/writes)
Guard is a lightweight, content/workflow safety layer designed to complement (not replace) OS/container sandboxing.
M1 focuses on three high-value capabilities:
- outbound destination allowlists (primarily
url_fetch) - redaction for tool outputs, final outputs, and audit summaries
- async approvals and an audit trail (JSONL)
Guard can enforce a global allowlist for unauthenticated url_fetch calls (i.e. calls without auth_profile).
Config (example):
guard:
enabled: true
network:
url_fetch:
allowed_url_prefixes:
- "https://api.example.com/v1/"
deny_private_ips: true
allow_proxy: false
follow_redirects: falseBehavior:
- If
guard.network.url_fetch.allowed_url_prefixesis empty, unauthenticatedurl_fetchis blocked (fail-closed). - If
url_fetchusesauth_profile, the destination boundary is enforced byauth_profiles.<id>.allow.url_prefixes(Guard still audits, but does not add an extra global allowlist layer by default).
Hardening toggles:
deny_private_ips: blocks literallocalhost/127.0.0.1/ RFC1918 private IP targets to reduce SSRF risk. M1 does not resolve hostnames to IPs; enforce egress at the network/OS layer if you need stronger guarantees.allow_proxy: when false, the HTTP client ignoresHTTP_PROXY/HTTPS_PROXY/NO_PROXYto avoid unexpected routing/MITM.
Guard redacts secret-like content before it enters context/logs:
- tool observations (
ToolCallPost) - final output (
OutputPublish) - redacted audit summaries for selected actions (
read_filepath,web_searchquery,url_fetchURL,bashcommand, andOutputPublish)
ASCII flow:
+----------------------+
| tool pre summary |
| read_file/web_search |
| url_fetch/bash params|
+----------+-----------+
|
v
+----------------------+
| redactAuditValue(...)|
+----------+-----------+
|
v
audit JSONL
tool output / final output
|
v
+----------------------+
| Guard Redactor |
| - private keys |
| - JWT-like strings |
| - Bearer tokens |
| - sensitive key=value|
| - MISTER_MORPH_* env |
+----------+-----------+
|
v
allow_with_redaction + redacted content
|
+--> model context / final publish / audit
Built-in redactors cover common patterns:
- private key blocks
- JWT-like strings
- bearer tokens
- sensitive
key=valueforms MISTER_MORPH_*environment variable names and assignments
For MISTER_MORPH_*, Guard redacts both the variable name and its value. For example:
MISTER_MORPH_API_KEY=...
becomes:
[redacted_env]=[redacted]
You can add extra regex patterns under guard.redaction.patterns.
Guard approvals are asynchronous by design:
- When an action requires approval (M1 default:
bashtool when enabled), the run pauses and returns afinal.outputobject like:{ "status": "pending", "approval_request_id": "apr_...", "message": "..." }
- Approval state is stored in file state (
<file_state_dir>/<guard.dir_name>/approvals/guard_approvals.jsonby default). - Approval expiry is hard-coded to 5 minutes in M1.
Long-running modes that start the embedded admin server expose management endpoints under the standard /runtime API base, authenticated with server.auth_token. The paths below are relative to that base:
GET /approvals/{id}(status + metadata; never returnsresume_state)POST /approvals/{id}/approvePOST /approvals/{id}/denyPOST /approvals/{id}/resume(re-queues the paused task)POST /poke- creates one awareness task from the textual request body
- returns
400 Bad Requestwhen the body is empty or non-text - returns
413 Request Entity Too Largewhen the body exceeds 10 KB - returns
409 Conflictif an awareness task is already in progress
GET /settings/agentPUT /settings/agentPOST /settings/agent/modelsPOST /settings/agent/testGET /auth/codex/statusPOST /auth/codex/refreshPOST /auth/codex/login/startPOST /auth/codex/login/pollPOST /auth/codex/logout
The Codex OAuth API never returns token values. Login sessions are process-local, short-lived, and bound to the runtime that created them.
Audit:
- Guard emits structured audit events to an append-only JSONL log.
- Configure via
guard.audit.jsonl_path(default:<file_state_dir>/<guard.dir_name>/audit/guard_audit.jsonl) andguard.audit.rotate_max_bytes.
Because of those capabilities, long-lived runtime processes are a good candidate for a deny-by-default runtime profile:
- keep the root filesystem read-only
- allow writes only to explicitly declared directories
- reduce kernel / device / namespace surface
- run without elevated privileges
systemd provides a first-class “service hardening” feature set for this. Unlike chroot, it does not require building a separate root filesystem; it applies restrictions directly to the unit.
The recommended unit assumes:
- Binary:
/opt/morph/mistermorph - Config:
/opt/morph/config.yaml - Skills:
/opt/morph/skills(setfile_state_dir: /opt/morphandskills.dir_name: skills) - Persistent state (guard approvals, memory, contacts, etc.):
/var/lib/morph/ - Ephemeral cache (file_cache_dir, Telegram downloads):
/var/cache/morph/ - write_file tool output:
/var/cache/morph/or/var/lib/morph/(file_cache_dir or file_state_dir) - Non-secret env/config:
/opt/morph/morph.env(mode0640, owned by root ormorph) - Secrets env:
/opt/morph/morph.secrets.env(mode0600, owned by root ormorph)
See the example unit file: deploy/systemd/mister-morph.service.
Agents are extremely good at “accidentally” leaking secrets if you ever put them into:
- prompts / skill docs
- tool parameters (especially headers)
- logs / traces
To avoid this, mistermorph supports profile-based credential injection:
- Skills/LLM only reference a profile id (e.g.
auth_profile: "jsonbill"). - The host resolves the real secret value at config load time. Use
${ENV_VAR}syntax incredential.secretto reference environment variables. - The tool injects the credential into the actual HTTP request (e.g.
Authorization: Bearer …) without logging it.
In /opt/morph/config.yaml:
secrets:
allow_profiles: ["jsonbill"]
auth_profiles:
jsonbill:
credential:
kind: api_key
secret: "${JSONBILL_API_KEY}"
allow:
url_prefixes: ["https://api.jsonbill.com/tasks"]
methods: ["POST", "GET"]
follow_redirects: false
allow_proxy: false
deny_private_ips: true
bindings:
url_fetch:
inject:
location: header
name: Authorization
format: bearer
allow_user_headers: true
user_header_allowlist: ["Accept", "Content-Type", "User-Agent"]In /opt/morph/morph.secrets.env:
JSONBILL_API_KEY="..."
MISTER_MORPH_LLM_API_KEY="..."
MISTER_MORPH_SERVER_AUTH_TOKEN="..."url_fetchsupportsauth_profileand injects credentials server-side.url_fetchrejects sensitive headers in user-providedheadersto reduce accidental leaks.url_fetchsupports saving binary responses tofile_cache_dir(instead of inlining bytes in the LLM context), which is recommended for PDFs.- When at least one allowlisted auth profile is configured,
bashcan still be enabled for local automation, butcurlis rejected by default to avoid “bash + curl” carrying authenticated HTTP requests. bashdoes not inherit the full parent environment. It runs with a small built-in allowlist (PATH, locale, shell/home, temp, XDG, and SSL cert vars) soMISTER_MORPH_*secrets are not exposed to subprocesses by default.- If a local workflow needs extra variables, inject them explicitly with
tools.bash.injected_env_vars.
ProtectSystem=strictmakes the system directories effectively read-only for the service (with a small set of unavoidable exceptions handled by systemd).- This prevents accidental or malicious writes to places like
/etc,/usr,/bin, etc.
ProtectHome=truehides/home,/root, and/run/userfrom the service.- This is a strong default: it prevents the agent from reading shell history, SSH keys, dotfiles, and other sensitive user data.
If you need access to specific content, prefer moving it under /opt/morph/... or using explicit bind mounts (see below) rather than opening up all of /home.
systemd can create and manage service-owned directories under /var/lib, /var/cache, and /var/log:
StateDirectory=morph→ writable/var/lib/morph(persistent state)CacheDirectory=morph→ writable/var/cache/morph(ephemeral cache)
For mistermorph, this split is recommended because guard approvals and other state are persistent and should not be treated as disposable.
The example unit additionally pins the agent’s paths via env vars:
MISTER_MORPH_FILE_CACHE_DIR=/var/cache/morph
WorkingDirectory is not a workspace setting. For a writable default project directory, set MISTER_MORPH_WORKSPACE_DIR and allow that directory explicitly:
Environment=MISTER_MORPH_WORKSPACE_DIR=/srv/morph-workspaceReadWritePaths=/srv/morph-workspace
If you want the agent to read a specific project directory, allowlist it explicitly:
- Read-only:
BindReadOnlyPaths=/some/dir:/workspace/dir - Read-write:
BindPaths=/some/dir:/workspace/dir
Prefer bind mounts over weakening ProtectSystem/ProtectHome.
The example unit also enables common isolation options:
NoNewPrivileges=true: prevents gaining extra privileges (e.g. via setuid binaries).PrivateTmp=true: private/tmpfor the service.PrivateDevices=true+DevicePolicy=closed: blocks access to device nodes.ProtectProc=invisible+ProcSubset=pid: reduce process info leakage.RestrictNamespaces=true: reduces attack surface from namespace creation.MemoryDenyWriteExecute=true: blocks W+X memory mappings (mitigates some exploit classes).RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6: allow only typical networking families (needed for outbound HTTP(S) and local sockets).
- systemd hardening is not a perfect sandbox. If you need stronger isolation, consider running in a container/VM.
- If you enable the
bashtool, treat it as high risk. Prefer keeping it disabled in long-lived runtime processes, or requiring confirmations and using a strict allowlist of read-only bind mounts. - Even with profile-based auth, avoid enabling arbitrary outbound execution paths (e.g. shelling out to network tools). Prefer structured tools with explicit allowlists and fail-closed policy.
- Guard M1 is intentionally small: Telegram approval UX and durable task storage across daemon restarts are not implemented yet.