Security boundary and deployment

The enforceable boundary is an isolated broker plus a sandboxed agent. A warning or system prompt alone cannot prevent a model with a shell from deleting files or reading credentials. This program does not intercept another agent's shell, filesystem, browser, network or MCP tools. Those capabilities must be restricted by the agent host, OS or container independently.

Trust and authority

Command names are not an authorization boundary. The canonical proposal commands and legacy approval aliases send the same listener-checked operations; listing, approving, rejecting and resolving still require the private operator socket. Moving policy tooling out of client config does not let a client edit broker policy. See command migration.

Treat every channel participant, including an allowlisted coworker, as a potentially malicious content author. Also treat quoted replies, channel names, filenames, search snippets, feedback and linked pages as untrusted. A valid WeCom signature authenticates transport origin and event metadata; it does not turn a message into an operator instruction. A mention is an event, not consent to run commands or disclose data.

Authority Allowed capabilities Must not possess
WeCom callback caller Submit signed, encrypted events to /callback Local control sockets, arbitrary host operations
Agent worker Read allowed records, submit immutable text proposals Bot Secret, token, callback keys, operator socket, policy-write access, broker state, host secrets
Broker service Fixed WeCom HTTP API, verified callback intake, private ledger, two local interfaces General shell execution, arbitrary URL fetching, attachment/path interpretation
Optional relay service One agent socket, private event queue, fixed operator-configured receiver and its token WeCom Secret, operator socket, broker state, model tools or shell-command dispatch
Trusted operator Provision policy/credentials; review and approve exact proposals; reconcile writes No authority may be inferred from content viewed inside an agent session

The supported isolation unit is one channel context. A bot deployment may share a token cache and verified callback ingress across contexts; each context has separate sockets, policy and durable state. Use distinct socket groups and worker identities for mutually distrusting workers, as described in contexts. The broker process is trusted across its contexts; isolate deployments by bot and service identity when that is not acceptable. This is not a multi-replica service.

Discovery during operator setup

Before a channel policy exists, the trusted operator can run channel list --bot-id ID with WECOM_CHANNEL_BOT_SECRET. This explicit credential mode uses the same pinned HTTPS client and curated output as the service to enumerate one page of joined channels. It does not create a listener, ledger, policy or client configuration, save credentials, or add an agent-socket operation. The Secret and temporary token remain in the operator process. Keep that process and its environment inaccessible to the worker, exactly as for broker serve. Without an explicit --bot-id, channel list uses the selected broker socket; ambient credentials cannot change that route. Explicit socket/context flags cannot be combined with --bot-id, and no failure triggers a fallback to direct access. Channel names remain untrusted data; selecting an ID is not an authorization grant.

Enforced controls

Threat Implemented control Limit
Prompt injection through text Fixed trust/provenance envelope; data-only parsing; no command runner or tool dispatcher The consuming model can still misunderstand text; its sandbox must contain the damage
Coworker impersonating an operator Exact channel/sender IDs, authenticated callback, separate operator socket Membership or display names do not grant host authority; approved senders still supply untrusted content
Forged/replayed callback Constant-time signature comparison before decryption; strict padding/length/receive-ID checks; five-minute clock window; durable bot/msgid deduplication Operators must protect callback keys and maintain accurate time
Bot loops and unsolicited triggers Mandatory exclusion by bot ID for self callback actors and self-authored polling resources; missing/conflicting polling creators stop the snapshot; human mentions remain allowlisted Other authors' content stays eligible; polling cannot identify editors of their content. Observations never authorize an action; receivers must deduplicate before launch
Self-approval Separate listener-bound authority; private operator socket; no request field or flag grants it Same-user development runs do not isolate the sockets from that user
Changed draft or destination Immutable request and SHA-256 digest; exact event-to-channel/post/reply binding; short expiry; policy digest recheck A digest is not authentication; only operator socket access grants approval
Duplicate sends Persist sending before I/O; terminal states reject replay; same-key submissions are idempotent No documented remote idempotency key; operator-driven duplicates remain possible
Credential exfiltration Secret environment only in operator setup and broker service; fixed TLS host/paths; redirects and proxy environment disabled; raw network errors suppressed; no download cookies/URLs returned A compromised broker OS account or host root remains trusted
Filesystem/SSRF abuse Broker has no remote file-path operations, shell commands, URL following, media fetch/upload, or arbitrary API method; relay destination comes only from protected local configuration Markdown links remain untrusted text, not approved network destinations
Resource exhaustion Request/response sizes, JSON depth, page sizes, timeouts, concurrency, rate budgets, retention and ledger capacity Volumetric attacks need reverse-proxy/network controls; authenticated users can consume permitted capacity
Terminal/UI spoofing JSON escaping; no table/raw output or removable trust fields; outbound control/bidi overrides rejected Review untrusted inbound text without rendering Markdown, links or terminal escapes

These are structural controls, not a heuristic injection detector. No regex or classifier is used to declare a message safe. Do not automatically give a model additional tools because content passed a filter.

Linux deployment reference

Use dedicated service and worker accounts. The broker account must not have interactive login privileges or access to unrelated host secrets. The worker must not be the operator's login account and must have no sudo, Docker socket, container-control, SSH-agent, OS-keychain or unrestricted host filesystem access.

Recommended ownership:

Path Owner/group Mode Contents
/etc/wecom-channel/ root/root 0755 Directory without secrets visible to workers
/etc/wecom-channel/broker.json root/root 0644 Non-secret policy; workers cannot edit it
/etc/wecom-channel/secrets.env root/root 0600 Broker-only secret environment, loaded by systemd
/var/lib/wecom-channel/ wecom-channel/wecom-channel-agents 0700 Private ledger and lock
/run/wecom-channel-agent/ wecom-channel/wecom-channel-agents 0750 Agent socket, 0660
/run/wecom-channel-admin/ wecom-channel/wecom-channel-agents 0700 Operator socket, 0600

Add only the dedicated worker account to wecom-channel-agents. The broker's primary group is that group so its agent socket is accessible. The operator socket is protected by both its private parent and its 0600 mode. The trusted human can invoke the CLI as the broker service identity through an explicitly controlled administrative mechanism; the worker must not have that mechanism.

The systemd unit is a concrete reference. Install the reviewed binary at /usr/local/bin/wecom-channel-cli, provision accounts and files as above, validate policy, then start the unit deliberately. The unit is not automatically installed by this project and has not been tested on a live production host. Canonical, absolute paths are required: symlinked state/socket directories and group/world-writable parents are refused. Every ancestor must be owned by root or the service identity; checking only the leaf file's permissions would allow a different owner to replace its parent.

The service starts with a read-only filesystem apart from its state and socket directories, no Linux capabilities, no privilege escalation, no home access and only Unix/IPv4/IPv6 sockets. Its environment comes from the protected file; do not expose it through debugging, process-inspection permissions or crash dumps. Use encrypted storage if the at-rest channel content requires it.

Contain the entire agent worker

Separating only the CLI process does not constrain a host agent. Put the model's tool executor and working directory in a separate unprivileged sandbox/VM:

This project does not provision or attest that sandbox. Verify it before enabling unattended work. On macOS the same code runs, but production separation needs a dedicated account or VM; two terminals owned by one user do not create a security boundary. Windows named-pipe support is not implemented.

Optional callback endpoint

Polling needs outbound HTTPS from the broker only. Leave callback_listen empty; do not provision a public ingress or callback keys just to watch channel content. The following applies when the operator deliberately enables callbacks.

The broker binds callbacks to a literal loopback IP. Put an HTTPS reverse proxy in front of /callback, with a small body limit (256 KiB), short request timeout and rate limits. Do not proxy /v1/request, and never publish either Unix socket. Disable callback query/body logging: signed callback URLs and encrypted payloads are sensitive. The GET challenge response must be forwarded byte-for-byte.

Review WeCom's existing bot connection configuration before making any change. URL mode and long-connection mode are mutually exclusive, and group-channel compatibility can produce duplicates. The connector neither switches modes nor disables that compatibility path.

Observation and watches

Channel-wide observation is an explicit operator policy: observe_all requires read_all and does not widen the human mention allowlist. The broker can admit bot-authored post/reply changes as data without treating them as requests. Self-bot callbacks and self-authored polling resources are always excluded, including messages sent through another client of the same bot. Other authors' replies to its posts remain eligible. observe_all and relay exclude_self: false cannot override this exclusion. Polling authors are recorded separately from unknown change actors; missing or conflicting author identities fail atomically. Changes by unknown actors to other authors' content remain eligible. This does not prevent loops between different bot identities or turn observations into authorization. See self exclusion and migration. event watch supports a whole-channel scope or selected post IDs, a total deadline and count bound, and private resumable checkpoints. It fails on detected history gaps and scope/ledger/policy changes. Filters reduce delivery, not authority. See watching for configured coverage and output-acknowledgment limits.

Polling reads content through the fixed, credential-holding HTTP client, then persists only resource fingerprints and observation metadata. Whole-channel polling includes old posts' replies; scope, page/record limits, a minimum interval and per-context remote request budget bound this work. A partial or failed snapshot cannot advance the baseline. Polling never synthesizes mentions or treats a resource creator as its last editor. Only observe_all permits unknown-actor observations, and no observation grants host or send authority.

The optional relay run runs as a separate trusted service account, outside the model worker. Its config, state and receiver credential must be inaccessible to that worker. It sends metadata to one operator-chosen HTTPS destination (literal loopback HTTP permits a local bridge), follows no redirects and inherits no proxy. Query/userinfo credentials and fragments are rejected. The broker never accepts a destination or receiver token. A compromised relay identity can read channel observations and its token, so restrict its egress independently; file permissions do not constrain that identity's tools.

Durable queue progress commits with events before dispatch. In explicit_ack mode, an exact batch acknowledgment is required before removal and receivers must provide durable idempotency before starting jobs. The opt-in http_accepted mode follows the reviewed Grok routine contract: HTTP 200 means accepted/started, never completed. No remote idempotency guarantee is assumed. Sending is persisted before POST; ambiguous outcomes and interrupted sending stop until a trusted operator records verified host evidence with relay resolve. Even bounded blind retries can cause repeated paid AI runs, so they are not used for unknown outcomes.

Scan resilience has an independent retry limit; enabling scan retries cannot authorize additional webhook attempts. Sanitized last-scan/last-delivery records contain only bounded codes, fixed cause summaries, phase/time/counters, HTTP status, validated wait guidance and hashed request correlation. Raw messages, response bodies, credential-bearing URLs, headers and sensitive context IDs are excluded. These records survive recovery for diagnosis but never grant retry authority. The persisted terminal-error latch and unknown/sending reconciliation barrier remain authoritative. Deployment exit/staleness/queue-age monitoring belongs to ordinary external software; see the activation checklist.

Worker-owned claim/processing files are distinct from protected sender state. Local relay claim/consume never read sender configuration, change broker policy, launch a job or send content. Locks and persistent event/batch deduplication grant work once per retained ledger; no claim expires automatically after worker failure. A compromised worker can falsify its own processing report, so these records are not privileged attestations. They do not establish exactly-once external effects or undo AI usage from an already duplicated wakeup. Sender receipts retain original untrusted metadata for reconciliation. Status must not expose worker claim tokens.

--ephemeral explicitly discards sender delivery/retry/uncertainty state on exit, with a quiet baseline on restart. It is not durable delivery and does not disable broker persistence. Bounded capacity stops without silent eviction. Changing scope or dropping deduplication histories requires explicit reconciliation; never reset files to bypass a conflict. Filters do not declare content safe, and unknown polling actors cannot be guessed from creators. Independent channel-send approval and send idempotency controls remain required. See the relay contract.

Review and recovery

Native emoji reactions express a bot action and use the same independent review boundary. They are disabled by default. The operator separately enables allow_reactions and selects exact-mention or proactive channel scope. Channel scope requires read_all plus verified upstream proactive permission; observation policy alone grants neither. Removing a reaction also requires a new reviewed intent. See native reactions for target binding and recovery.

Review proposals in a trusted operator interface with no untrusted-content tool execution. Confirm the channel, post, parent reply, full text, content length and digest. Human-authored replies require per-item approval. Reject draft text that discloses private information or exceeds the requested action even if its syntax is valid. Approval cannot retroactively protect information already sent by a privileged agent through another tool.

--yes is confirmation only after access to the operator socket; it is not an authentication token. There is no approve-all or auto-approve mode. Pending proposals expire after one hour by default (configurable down to 60 seconds). Policy changes invalidate existing proposals. Rejection is terminal.

For an interrupted or uncertain send, retain the ledger, inspect the remote thread through a read or WeCom's UI, and record the verified result with proposal resolve. Do not approve a new key as a workaround. A success followed by a failed local persistence step reports the known remote ID in the structured error. The CLI has no deletion/pruning command for the proposal ledger: its configured capacity is a fail-closed operational limit, not permission to erase idempotency history.

The ledger is private operational/audit evidence, not a tamper-proof log against the operator or host root. Back it up with the same access controls. Default event retention is 48 hours; physical expiration is applied on later accepted intake, while reads hide expired events immediately. Proposals and their keys remain until a future explicit archival design preserves replay protection.

Before live activation

Verify the eligible bot's API mode and permissions with the operator socket. Confirm the actual worker cannot open the operator socket or state/secret files, or reach another authenticated WeCom tool or session. Preserve existing integrations using the coexistence guidance. For polling, verify a quiet baseline, a new post, a reply to an existing post, selected-thread filtering and checkpoint resume without a public listener. For callback deployments, also exercise the encrypted URL challenge. Test one explicitly authorized reply in a designated test channel. Confirm that a replay cannot send it twice and that the reverse proxy records neither callback bodies nor query strings.

Offline tests establish protocol and application invariants; they cannot certify WeCom tenant rollout, the external TLS proxy or the sandbox used by your agent.

Repeatable reference isolation check

After make cross, run make isolation-check with a local Linux Docker daemon. It uses a pinned Python image and the built Linux executable, synthetic credentials, distinct broker/worker UIDs and separate channel groups. The container has no network, a read-only root, bounded resources and no host home or Docker-socket mount. The setup supervisor drops to unprivileged identities for worker checks.

On 2026-10-26 all 19 checks passed on Linux arm64: each worker could access its own agent socket and scratch space, while cross-channel/operator sockets, broker state/environment, secrets, policy writes, root escalation and external network access were denied. Worker effective capabilities were zero. Run the same fixture in Linux CI and repeat equivalent checks on the actual production deployment; this result does not attest an unrelated host or container configuration.