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:

  1. Telegram/Lark input vs authorized chat
  2. Local bus caller vs trusted peer instance
  3. Local web-console caller vs privileged configuration API
  4. Model output vs filesystem egress
  5. Per-instance state vs operational artifacts
  6. Bot workspace vs shared engine-global config
  7. 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 pairing or allowlist
  • 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 internal bus chat 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/talk enforces JSON shape, body size, peer allowlist, and max delegation depth
  • each process admits at most 8 active /api/talk requests and returns retryable server_busy when 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/health and 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"; bus is 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=Strict session cookie, and removed from the visible URL with history.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.md teaches 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 /resume workspace 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, and id_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
  • /resume intentionally 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 retained board.json migration backups, exports, and kanban-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, or DSH_HOME selected 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_HOME and 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-auto or bypass mode 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.
  • /resume intentionally expands the workspace trust boundary to a real project root.
  • CLAUDE_CONFIG_DIR and CODEX_HOME are 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:

  1. Keep shrinking cross-entry drift by centralizing usage, budget, audit, and post-turn bookkeeping.
  2. Maintain regression tests around file delivery, /resume, bus auth, and provider output parsing.
  3. Keep security assumptions documented when changing shared engine-home behavior.
  4. Prefer adding narrow shared helpers over duplicating boundary logic in command-specific flows.