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:
- mount only a disposable working directory and the agent socket directory;
- expose no broker state, operator socket, policy-writable mount, home directory, cloud credentials, SSH keys, keychain or privileged daemon socket;
- expose no authenticated official
wecom-clisession or equivalent WeCom connector to the channel worker: alternate credentials, browser sessions and tools could bypass this broker's channel policy and proposal review; - deny outbound network by default; if remote inference is required, use a separately controlled gateway with a narrowly scoped model credential and explicit endpoint policy, not general network access or host credentials;
- disable privilege escalation and container escape capabilities; restrict available tools to the task; keep approval controls outside model reach;
- apply CPU, memory, process and filesystem quotas; use short job deadlines;
- treat any model-produced result as untrusted when the supervisor receives it.
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.