Security Boundaries
September 9, 2026 ยท View on GitHub
This document explains the trust boundaries in TaroCub.
The goal is not to claim the system is "secure" in the abstract. The goal is to make the current security model explicit enough that future changes do not quietly widen trust, weaken isolation, or reintroduce file- or state-handling bugs.
This document should be read together with:
Threat Model
The project is a local-first bot bridge, not a public SaaS control plane.
The practical threat model is:
- untrusted Telegram or Feishu/Lark messages reaching a local coding-agent CLI
- untrusted model output being turned into user-visible replies and file reads
- multiple local instances delegating work over loopback HTTP
- sensitive state and credentials living on disk
- engine subprocesses sharing some state with the user's real local CLI
The project is not designed as a hardened multi-tenant server.
Reasonable assumptions today:
- the operator controls the host machine
- the bus server listens only on loopback
- Telegram and Feishu/Lark are the remote chat entry points
- state directories are private to the local user
If those assumptions change, this document is no longer enough. The code will need a different security model.
Boundary Map
The current system has seven important boundaries:
- Telegram/Lark input vs authorized chat
- Local bus caller vs trusted peer instance
- Local web-console caller vs privileged configuration API
- Model output vs filesystem egress
- Per-instance state vs operational artifacts
- Bot workspace vs shared engine-global config
- User intent orchestration vs provider CLI side effects
Not all of these are equally strong.
The strongest boundaries today are:
- Telegram/Lark access control
- loopback-only bus exposure
- file delivery sandboxing to workspace-like roots
- owner-only permissions on structured state
The weakest or most intentionally-permeable boundaries today are:
- shared
CLAUDE_CONFIG_DIR/CODEX_HOME - any engine behavior that can modify files inside the allowed workspace
- fallback behavior when optional config/state is missing or malformed
1. Remote Chat Boundary
Telegram and Feishu/Lark are untrusted remote input sources until access control passes.
Trusted
- chats that pass the configured access policy
- normalized message metadata after the channel SDK/API has parsed it
Untrusted
- message text
- attachments
- reply context
- callback payloads
- any attempt to smuggle file paths, shell intent, or prompt injection
Current enforcement
- src/runtime/bridge.ts rejects or challenges normal chats according to
pairingorallowlist - non-private chats require both an authorized Telegram user and an explicitly allowed group chat
- ordinary group messages are ignored unless they mention the bot or reply to the bot
- unauthorized group inputs are silent and audited; private unauthorized inputs are answered before engine execution
- Telegram and Lark messages are normalized before command handling
- external Telegram updates are allowed only for Telegram chat types (
private,group,supergroup,channel); the internalbuschat type is rejected at normalization
Residual risk
- once a chat is authorized, the engine still receives raw user text
- prompt injection is a product-level risk, not something access control solves
- features that bypass the normal message path must preserve the same access semantics
Design rule
Any new Telegram/Lark command or callback path must either:
- call the existing access check path, or
- very explicitly justify why it is safe without it
2. Local Bus Boundary
The Agent Bus is a privileged local control plane, not a public API.
Trusted
- loopback-bound bus servers started by known bridge instances
- peers explicitly allowed by
bus.peers - requests that pass secret validation when a secret is configured
Untrusted
- arbitrary local processes
- stale or forged registry entries
- wrong local listeners occupying an expected port
- peer instances not listed in the allowed peer set
Current enforcement
- src/bus/bus-server.ts serves only on
127.0.0.1 /api/talkenforces JSON shape, body size, peer allowlist, and max delegation depth- each process admits at most 8 active
/api/talkrequests and returns retryableserver_busywhen saturated - shutdown drains in-flight HTTP requests and force-closes remaining connections after 5 seconds
- bus auth uses a bearer secret when configured
- src/bus/bus-registry.ts probes
/api/healthand validates the bridge fingerprint before treating a registry entry as alive - src/runtime/bridge.ts auto-allows
chatType === "bus"only because bus auth is supposed to have already happened at the server boundary - src/telegram/update-normalizer.ts rejects external Telegram updates claiming
chat.type === "bus";busis an internal synthetic chat type, not a Telegram API value
Residual risk
- local same-user compromise is still high impact
- a leaked bus secret is effectively local remote-control capability for that instance
- bus requests are privileged enough that logging and usage/budget semantics must stay aligned with Telegram
Design rule
Treat bus changes as security-sensitive even when they are "only local". A loopback API with delegated model execution is still a real authority boundary.
3. Local Web Console Boundary
The optional web console is a privileged local configuration surface. Loopback binding reduces exposure, but it does not replace authentication.
Trusted
- a browser opened by the local operator with the random token generated for the current console process
- same-origin API requests authenticated by that token or its short-lived browser-session cookie
Untrusted
- arbitrary local web pages and browser extensions
- unauthenticated local processes
- stale, copied, or externally shared console URLs
Current enforcement
- the console server binds only to loopback
- the initial URL token is accepted as a bearer credential, exchanged for an
HttpOnly,SameSite=Strictsession cookie, and removed from the visible URL withhistory.replaceState - the client keeps a session-scoped token copy for explicit API authorization; same-origin cookie authentication also preserves refresh behavior after URL cleanup
- shell responses set
no-store,no-referrer,nosniff, frame denial, a restrictive permissions policy, and a content security policy
Residual risk
- the first navigation still carries the token locally before client-side cleanup
- same-user process or browser compromise can recover local console authority
- the console uses loopback HTTP rather than TLS and must never be rebound to a non-loopback interface without a new threat model
Design rule
Do not add an unauthenticated console endpoint or persist the console token beyond the browser session. Any new shell asset must remain compatible with the restrictive response headers.
4. File Delivery Boundary
File delivery is the highest-risk boundary in the product because it turns model output into filesystem reads and Telegram/Lark egress.
Trusted
- canonical files under the instance workspace
- canonical files under an explicitly resumed project root
- inline
```file:...blocks for small text/code artifacts
Untrusted
- arbitrary absolute paths emitted by the engine
- local Markdown links in model output
- symlink paths before canonical resolution
- any file outside the allowed workspace roots
Current enforcement
- Instance
agent.mdteaches the engine to use registered[tool:{"name":"send.file",...}]/[tool:{"name":"send.image",...}]delivery tags for binary delivery - src/telegram/tool-tags.ts parses generic tool tags, and src/telegram/legacy-delivery-tool-tags.ts normalizes legacy
[send-file:]/[send-image:]tags into the same tool layer - src/telegram/delivery.ts extracts legacy
[send-file:], Markdown image, and Markdown local-link references - src/telegram/delivery.ts resolves
realpath()before policy checks - delivery only permits canonical paths under the bot workspace or the active
/resumeworkspace override - non-files, oversized files, missing files, and permission failures are rejected and surfaced back to the user
- both channels refuse credential-shaped basenames and extensions, including
.env*,*.pem,*.key,id_rsa, andid_ed25519 - Lark batches are rejected before upload when they exceed 120 MiB in aggregate
- Lark image batches are preflighted first and then read/uploaded one image at a time, so one request does not retain every image buffer simultaneously
Residual risk
- the engine can still read and exfiltrate any file that is already readable inside the allowed workspace tree
/resumeintentionally widens the file-delivery root to a real project directory- users should assume that authorizing a bot on a project means the bot may send non-credential project files back over Telegram or Lark if prompted
Design rule
Any change touching file extraction, [tool:] send tags, legacy [send-file:], /resume, workspace roots, or canonical path checks requires explicit security review and regression tests.
5. Per-Instance State Boundary
Each instance has a private state root under ~/.cctb/<instance>/.
Trusted
- owner-only structured state files created by the bridge
- append-only operational logs written by the local service
Untrusted
- malformed or manually edited state files
- partial operational artifacts
- restored archives from unknown provenance
Current enforcement
- structured state uses atomic temp-file + rename writes
- private state is written with owner-only permissions
- corrupt JSON state can be quarantined rather than silently overwritten
- read-modify-write and quarantine/reset recovery paths use the same cross-process file mutex, preventing a repair from renaming a newer concurrent write
- restore logic preserves private permissions on sensitive state
- backup creation and restore stream file bodies and enforce file-count, compressed-size, expanded-size, and per-file bounds before untrusted archives can exhaust memory or disk
- lock files and runtime metadata are kept separate from authoritative config/state
Residual risk
- some operational files remain best-effort rather than transactional
- malformed optional config may still trigger fallback behavior instead of hard failure
- durable planning state in
kanban.sqlite, its retainedboard.jsonmigration backups, exports, andkanban-assets/may contain task intent, summaries, chat IDs, user IDs, topic IDs, and local paths; known credential patterns are redacted before Board persistence and export, but the entire boundary remains high sensitivity - state consistency bugs are more likely to be logic regressions than raw permission bugs now
Design rule
When adding a new state file, define up front:
- whether it is authoritative or derived
- who owns writes
- whether writes must be serialized
- what recovery behavior is allowed
- whether the file is credential-bearing or privacy-sensitive
6. Shared Or Linked Engine-Home Boundary
The bot no longer has a fully isolated engine-global home.
Trusted
- the operator's own Claude/Codex/Kimi/DeepSeek CLI environment
- the real
CLAUDE_CONFIG_DIR,CODEX_HOME,KIMI_CODE_HOME, orDSH_HOMEselected by the parent process
Untrusted
- any assumption that bot isolation fully contains engine-global side effects
- any feature that writes engine-global settings, caches, MCP config, plugin state, or auth artifacts
Current enforcement
- src/service.ts intentionally inherits the real Claude/Codex config home instead of forcing a per-instance engine home
- Kimi retains its native user-level home; DeepSeek receives a private writable home but links authenticated credentials/profiles from the selected shared
DSH_HOMEand copies mutable settings - workspace paths remain per-instance, so normal conversation history stays split by workspace in practice
- legacy Claude project files are migrated into the shared config home so upgrades do not silently drop prior history
Residual risk
- blast radius is wider than the per-instance state directory
- a bot running in
full-autoorbypassmode can affect engine-global state shared with the user's main CLI - this is a deliberate trade-off to avoid auth-refresh token races, not a free isolation win
Design rule
Any feature that touches provider config roots should document whether it is:
- per-instance state
- per-workspace state
- shared engine-global state
Do not describe the system as "fully isolated" without qualifying this boundary.
7. Engine Process Boundary
The provider CLI is not a pure function. It is a privileged local subprocess with filesystem and config side effects.
Trusted
- adapter-level protocol handling
- explicit bridge instructions injected by the runtime
Untrusted
- provider CLI output shape
- provider auth/session expiry behavior
- any tool or plugin execution performed by the engine
Current enforcement
- adapter interfaces keep provider-specific parsing contained
- runtime normalizes auth errors, session handling, and usage accounting above the adapter layer
- Telegram-specific instructions explicitly forbid interactive prompt tools and define the file-delivery contract
- Claude Telegram approvals use a loopback MCP bridge with a per-request random URL token; this defends against blind local port probes, not against same-user process inspection
Residual risk
- provider behavior can change underneath the bridge
- same-user local processes may inspect provider CLI command lines, including transient MCP config used for Claude approvals
- array/object output formats, auth-expiry paths, and session-rebind semantics need regression coverage
- bugs at this layer usually become product-level reliability issues quickly
Design rule
Provider integration changes are boundary changes. Treat them as such even when the patch looks like "just parsing".
Operational Rules
These rules are the shortest useful version of the security model.
- Telegram is untrusted until access control passes.
- Bus is privileged local control-plane traffic, not a convenience API.
[send-file:]is a filesystem egress mechanism and must stay sandboxed./resumeintentionally expands the workspace trust boundary to a real project root.CLAUDE_CONFIG_DIRandCODEX_HOMEare shared-engine boundaries, not per-instance boundaries.- Sensitive state belongs under the instance state root with owner-only permissions.
- Cross-entry behavior must stay aligned across Telegram, bus,
/fan,/verify,/btw, and future entry points.
Review Checklist For Future Changes
Use this checklist when reviewing changes in security-sensitive areas.
Access and entry points
- Does the new path enforce the same access semantics as the existing Telegram path?
- If it is bus-only, is the bus boundary still authenticated before
chatType === "bus"is trusted?
File handling
- Can model output cause the bridge to read or send a new class of local file?
- Are canonical path checks still in place after symlink resolution?
- Are rejection cases visible to the user and test-covered?
State
- Is the new file authoritative or derived?
- Are writes atomic and, if needed, serialized?
- Are private files still private after backup/restore/migration?
Engine integration
- Does the change widen shared engine-home side effects?
- Does it preserve auth-expiry, session-rebind, and malformed-output behavior?
Cross-entry consistency
- Do Telegram, bus,
/fan,/verify, and other entry points still share the same accounting, audit, and access rules?
Next Hardening Work
The project does not need a giant security rewrite. It does need discipline around a few seams.
Most valuable next steps:
- Keep shrinking cross-entry drift by centralizing usage, budget, audit, and post-turn bookkeeping.
- Maintain regression tests around file delivery,
/resume, bus auth, and provider output parsing. - Keep security assumptions documented when changing shared engine-home behavior.
- Prefer adding narrow shared helpers over duplicating boundary logic in command-specific flows.