typeclaw 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/auth.schema.json +66 -0
  2. package/cron.schema.json +26 -2
  3. package/package.json +1 -1
  4. package/secrets.schema.json +66 -0
  5. package/src/agent/index.ts +7 -3
  6. package/src/agent/session-origin.ts +17 -0
  7. package/src/agent/subagent-completion-reminder.ts +14 -1
  8. package/src/agent/subagent-drain.ts +2 -0
  9. package/src/agent/subagents.ts +21 -7
  10. package/src/agent/tools/channel-disengage.ts +66 -0
  11. package/src/agent/tools/channel-log.ts +3 -2
  12. package/src/agent/tools/spawn-subagent.ts +25 -5
  13. package/src/agent/tools/subagent-output.ts +13 -1
  14. package/src/bundled-plugins/guard/policies/managed-config.ts +1 -1
  15. package/src/bundled-plugins/memory/memory-logger.ts +7 -0
  16. package/src/bundled-plugins/researcher/researcher.ts +14 -11
  17. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +1 -0
  18. package/src/channels/adapters/line-channel-resolver.ts +129 -0
  19. package/src/channels/adapters/line-classify.ts +80 -0
  20. package/src/channels/adapters/line-format.ts +11 -0
  21. package/src/channels/adapters/line.ts +350 -0
  22. package/src/channels/engagement.ts +4 -2
  23. package/src/channels/manager.ts +65 -6
  24. package/src/channels/router.ts +186 -41
  25. package/src/channels/schema.ts +6 -1
  26. package/src/cli/channel.ts +112 -1
  27. package/src/cli/cron.ts +22 -4
  28. package/src/cli/oauth-callbacks.ts +5 -4
  29. package/src/config/providers.ts +62 -0
  30. package/src/cron/consumer.ts +33 -0
  31. package/src/cron/count-state.ts +208 -0
  32. package/src/cron/index.ts +4 -17
  33. package/src/cron/list.ts +24 -6
  34. package/src/cron/scheduler.ts +84 -9
  35. package/src/cron/schema.ts +100 -13
  36. package/src/doctor/channel-checks.ts +28 -0
  37. package/src/hostd/daemon.ts +14 -6
  38. package/src/hostd/protocol.ts +6 -2
  39. package/src/init/gitignore.ts +1 -1
  40. package/src/init/index.ts +36 -3
  41. package/src/init/line-auth.ts +98 -0
  42. package/src/init/models-dev.ts +1 -0
  43. package/src/init/run-owner-claim.ts +1 -0
  44. package/src/init/validate-api-key.ts +2 -0
  45. package/src/inspect/label.ts +1 -0
  46. package/src/permissions/match-rule.ts +28 -12
  47. package/src/permissions/resolve.ts +8 -1
  48. package/src/role-claim/match-rule.ts +5 -1
  49. package/src/run/index.ts +41 -4
  50. package/src/secrets/line-store.ts +112 -0
  51. package/src/secrets/oauth-xai.ts +1 -1
  52. package/src/secrets/schema.ts +25 -0
  53. package/src/server/index.ts +17 -4
  54. package/src/shared/protocol.ts +4 -1
  55. package/src/skills/typeclaw-channel-line/SKILL.md +46 -0
  56. package/src/skills/typeclaw-channels/SKILL.md +153 -0
  57. package/src/skills/typeclaw-config/SKILL.md +54 -184
  58. package/src/skills/typeclaw-config/references/dockerfile.md +66 -0
  59. package/src/skills/typeclaw-cron/SKILL.md +68 -14
  60. package/src/skills/typeclaw-permissions/SKILL.md +3 -3
  61. package/typeclaw.schema.json +167 -3
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: typeclaw-config
3
- description: "Read or edit typeclaw.json: model, port, mounts, plugins, channels (per-adapter engagement and history; access control lives in roles — see typeclaw-permissions), portForward (auto port forwarding policy), docker.file (tmux/gh/python/ffmpeg toggles + append), git.ignore.append. Also: any question about a default value or whether a behavior is already on by defaultport forwarding, channel visibility, model choice, container packages (tmux/gh/python on by default; ffmpeg off), anything ending in 'by default', 'automatically', 'out of the box', 'do I need to configure', 'is X on', 'what does X default to', '기본값', '기본적으로', '자동으로', '디폴트'. MUST load before saying you do not know what X defaults to, or proposing to add a field whose default the user is asking about most fields already default to the behavior the user expects (portForward defaults to forwarding every container LISTEN; tmux/gh/python are pre-installed in the container; no edit needed). Also covers recommended host paths to mount for common use cases (voice memos for STT, screenshots, mail, iMessage, notes vaults, downloads) with macOS/Linux/WSL paths and TCC/Full-Disk-Access gotchas load when user describes a use case like 'transcribe my voice memos', 'triage my mail', 'mount my notes', 'let you see my screenshots', or asks 'what should I mount?'. Read it before touching typeclaw.json strict schema, mix of live-reloadable and restart-required fields."
3
+ description: "Read or edit typeclaw.json — the host-stage runtime config: model, port, mounts, plugins, alias, channels, portForward, docker.file (container package toggles + append), git.ignore.append, plus provider credentials (secrets.json/.env) and the allowed-models registry. Strict schema with a mix of live-reloadable and restart-required fieldsload before touching the file or you risk corrupting it or promising a behavior the runtime won't deliver. Also the authority on what a field defaults to and whether a behavior is already on out of the box (port forwarding, container packages, model choice) load before saying you don't know what X defaults to, or before proposing to add a field whose default the user is asking about; most fields already default to the expected behavior, so the answer is usually 'no edit needed'. Owns the GitHub channel config which repos it watches (channels.github.repos), the code-review trigger (channels.github.review.on/approve), webhook auto-registration via a tunnel load on 'github channel', 'github webhook', 'review these repos', 'watch repo X', 'set up code review', 'stop reviewing repo Y'; GitHub events are an inbound channel the agent engages directly, there is NO 'forward webhooks to a Slack channel' flow, do not invent destinations. Covers recommended host paths to mount for common use cases (references/recommended-mounts.md). For messenger-channel engagement BEHAVIOR (when the agent replies vs. observes, triggers, stickiness, alias matching, suppressors) load typeclaw-channels; for who is admitted to a channel load typeclaw-permissions."
4
4
  ---
5
5
 
6
6
  # typeclaw-config
@@ -18,8 +18,8 @@ The runtime reads `typeclaw.json` at container startup. Some fields are picked u
18
18
  - `mounts` — additional host directories the user has chosen to expose to you. Each entry produces a `docker run -v <hostPath>:/agent/mounts/<name>` flag at `typeclaw start` time, so the directory shows up at `mounts/<name>` inside your agent folder. **The launcher reads this; the running container does not.** Editing `mounts` only takes effect on the next `typeclaw start`. **Restart-required.**
19
19
  - `plugins` — array of plugin package names loaded at server boot. **Restart-required.**
20
20
  - `alias` — additional names the agent answers to when a channel message contains its name in plain text (no `<@id>` mention). The agent folder's directory name (`basename(agentDir)`) is always implicit; `alias` adds further forms (Latin transliteration, nicknames, Korean particles, etc.). Used by the channel engagement layer alongside the structural mention/reply/dm triggers. **Live-reloadable.**
21
- - `channels` — per-adapter engagement triggers and history-prefetch knobs for external messengers (Discord, Slack, Telegram, KakaoTalk). Access control lives in `roles`, not here. **Live-reloadable** — edits take effect on the next `reload` without a container restart.
22
- - `docker.file` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated package installs (`tmux`, `gh`, `python`, `cjkFonts`, `cloudflared`, `xvfb` default `true`; `ffmpeg`, `claudeCode` default `false`) — set the toggle to `false` to omit, or to a version string like `"2.40.0"` to apt-pin (`python`, `cjkFonts`, `cloudflared`, `xvfb`, and `claudeCode` are boolean-only). Most toggles install apt packages with BuildKit cache mounts; `cloudflared` and `claudeCode` are exceptions — `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's official `curl | bash` installer. (2) **`append`** — extra Dockerfile lines spliced in right before `ENTRYPOINT` for anything the toggles don't cover. The whole Dockerfile is rewritten on every `start` from the typeclaw template. Lives under the `docker` namespace alongside future Docker-related blocks (e.g. `docker.compose`). **Restart-required** (next `typeclaw start` rebuilds the image).
21
+ - `channels` — per-adapter engagement triggers and history-prefetch knobs for external messengers (Discord, Slack, Telegram, KakaoTalk), plus the GitHub channel (a webhook-driven adapter that watches repos and reviews PRs — see **GitHub channel** below). Access control lives in `roles`, not here. **Live-reloadable** — edits take effect on the next `reload` without a container restart.
22
+ - `docker.file` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated package installs `tmux`, `gh`, `python`, `xvfb` default on (`true`); `cjkFonts` defaults to `"auto"` (resolved from host locale at start); `ffmpeg`, `cloudflared`, `claudeCode`, `codexCli` default off (`false`) — set a toggle to `false` to omit, or to a version string like `"2.40.0"` to apt-pin (`python`, `cjkFonts`, `cloudflared`, `xvfb`, `claudeCode`, and `codexCli` are boolean-only). Most toggles install apt packages with BuildKit cache mounts; `cloudflared`, `claudeCode`, and `codexCli` are exceptions — `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's official `curl | bash` installer, `codexCli` `bun install`s the `@openai/codex` npm package. (2) **`append`** — extra Dockerfile lines spliced in right before `ENTRYPOINT` for anything the toggles don't cover. The whole Dockerfile is rewritten on every `start` from the typeclaw template. Lives under the `docker` namespace alongside future Docker-related blocks (e.g. `docker.compose`). **Restart-required** (next `typeclaw start` rebuilds the image).
23
23
  - `git.ignore.append` — extra `.gitignore` patterns `typeclaw start` splices into the TypeClaw-owned `.gitignore` before the protected TypeClaw rules. The whole `.gitignore` is rewritten and auto-committed on every `start` when it changes; `append` is the supported escape hatch for local ignore patterns without editing the managed file by hand. Lives under the `git` namespace. **Restart-required** (next `typeclaw start` refreshes and commits `.gitignore`).
24
24
  - `portForward` — allow/deny policy for the auto port-forwarder (the host-stage `_hostd` daemon's portbroker). When the agent runs a server inside the container that LISTENs on a TCP port, the broker proxies it to the same port number on `127.0.0.1` of the host so the user can hit it directly. `portForward` decides which ports are allowed through. **Restart-required** — the broker captures the policy at register time on `typeclaw start`.
25
25
 
@@ -39,18 +39,18 @@ You yourself cannot run `typeclaw restart` — that is a host-stage command and
39
39
 
40
40
  `typeclaw.json` is a single JSON object with these fields:
41
41
 
42
- | Field | Required | Type | Notes |
43
- | ------------- | -------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
- | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
- | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
- | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
- | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
- | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
- | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Alias** section below. **Live-reloadable.** |
50
- | `channels` | no | object | Per-adapter engagement triggers and history-prefetch knobs for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds an empty block per requested adapter (e.g. `"discord-bot": {}`) and the schema fills in defaults. Channel access control lives in `roles` — see the `typeclaw-permissions` skill. **Live-reloadable.** See **Channels** section below. |
51
- | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
- | `docker` | no | object | Namespace for Docker-related blocks. Today the only child is `docker.file` — toggles (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`, `codexCli`) gate opinionated package installs; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: 'auto', cloudflared: false, claudeCode: false, codexCli: false, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
- | `git` | no | object | Namespace for git-related blocks. Today the only child is `git.ignore` — extra patterns spliced into the autogenerated `.gitignore` before TypeClaw's protected rules. `git.ignore` defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
42
+ | Field | Required | Type | Notes |
43
+ | ------------- | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
44
+ | `$schema` | no | string | Path to `typeclaw.schema.json` for editor autocompletion. Scaffolded as `./node_modules/typeclaw/typeclaw.schema.json`. Leave it alone unless the user moves it. |
45
+ | `port` | no | integer | 1–65535. Defaults to `8973` (T9 spelling of "TYPE"). Change only if the default collides with something on the user's host. **Restart-required.** |
46
+ | `model` | no | string | Must be one of the values listed in the **Allowed models** section below. Defaults to `openai/gpt-5.4-nano`. **Live-reloadable.** |
47
+ | `mounts` | no | array of objects | Host directories bind-mounted into your container. Defaults to `[]` (no host paths exposed). Omitted from scaffolded `typeclaw.json` — add it only when the user wants host paths exposed. See **Mounts** section below. **Restart-required.** |
48
+ | `plugins` | no | array of strings | Plugin package names loaded at server boot. Defaults to `[]`. **Restart-required.** Plugin-owned config blocks live alongside as additional top-level keys; see **Plugin config blocks**. |
49
+ | `alias` | no | array of strings | Additional names the agent answers to in channel engagement, on top of the implicit `basename(agentDir)`. Each entry is a non-empty trimmed string matched case-insensitively as a substring of the inbound text. Defaults to `[]`. Hatching populates this with the agent's chosen name. See **Channels and Alias** below for schema/edit mechanics; the matching behavior lives in the `typeclaw-channels` skill. **Live-reloadable.** |
50
+ | `channels` | no | object | Per-adapter engagement triggers and history-prefetch knobs for external messengers (plus the `github` webhook channel — see **GitHub channel** below). Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds an empty block per requested adapter (e.g. `"discord-bot": {}`) and the schema fills in defaults. Channel access control lives in `roles` — see the `typeclaw-permissions` skill; engagement behavior lives in `typeclaw-channels`. **Live-reloadable.** See **Channels and Alias** below. |
51
+ | `portForward` | no | object | Allow/deny policy for the host-stage portbroker that auto-forwards container LISTEN ports to `127.0.0.1` on the host. Defaults to `{ "allow": "*" }` (forward everything). Omitted from scaffolded `typeclaw.json`. **Restart-required.** See **portForward** section below. |
52
+ | `docker` | no | object | Namespace for Docker-related blocks. Today the only child is `docker.file` — toggles (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `xvfb`, `claudeCode`, `codexCli`) gate opinionated package installs; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. `docker.file` defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, cjkFonts: 'auto', cloudflared: false, xvfb: true, claudeCode: false, codexCli: false, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
53
+ | `git` | no | object | Namespace for git-related blocks. Today the only child is `git.ignore` — extra patterns spliced into the autogenerated `.gitignore` before TypeClaw's protected rules. `git.ignore` defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
54
54
 
55
55
  > **Top-level keys not in this table are not "ignored unknowns" anymore** — they are reserved for **plugin config blocks**. The schema's `catchall(z.unknown())` preserves them, and the plugin loader hands each block to its owning plugin's `configSchema` for validation. The bundled memory plugin owns `memory` at the top level — see the `typeclaw-memory` skill for that block's semantics. Do not write a top-level key unless you know which plugin owns it.
56
56
 
@@ -65,7 +65,7 @@ A scaffolded `typeclaw.json` looks like:
65
65
  }
66
66
  ```
67
67
 
68
- The runtime fills in defaults for any omitted field: `port` → `8973`, `mounts` → `[]` (no host paths exposed), `plugins` → `[]`, `channels` → `{}` (no adapters configured), `portForward` → `{ "allow": "*" }` (forward every container LISTEN port), `docker` → `{ "file": { "ffmpeg": false, "gh": true, "python": true, "tmux": true, "cjkFonts": true, "cloudflared": true, "claudeCode": false, "append": [] } }` (tmux/gh/python/cjkFonts/cloudflared pre-installed; ffmpeg and claudeCode off; no custom build steps), `git` → `{ "ignore": { "append": [] } }` (no custom ignore patterns). `typeclaw init` deliberately omits any field whose default is owned elsewhere — `mounts`, `portForward`, `docker`, and `git` default via `configSchema`, and the bundled memory plugin owns its own `memory` defaults — so the scaffolded file stays minimal and the user sees only fields they actually need to think about. Add a `memory` block (a **plugin config block** owned by the bundled memory plugin) only when overriding its defaults; see the `typeclaw-memory` skill for the schema.
68
+ The runtime fills in defaults for any omitted field: `port` → `8973`, `mounts` → `[]` (no host paths exposed), `plugins` → `[]`, `channels` → `{}` (no adapters configured), `portForward` → `{ "allow": "*" }` (forward every container LISTEN port), `docker` → `{ "file": { "ffmpeg": false, "gh": true, "python": true, "tmux": true, "cjkFonts": "auto", "cloudflared": false, "xvfb": true, "claudeCode": false, "codexCli": false, "append": [] } }` (tmux/gh/python/xvfb pre-installed; cjkFonts auto-detected from host locale; ffmpeg, cloudflared, claudeCode, and codexCli off; no custom build steps), `git` → `{ "ignore": { "append": [] } }` (no custom ignore patterns). `typeclaw init` deliberately omits any field whose default is owned elsewhere — `mounts`, `portForward`, `docker`, and `git` default via `configSchema`, and the bundled memory plugin owns its own `memory` defaults — so the scaffolded file stays minimal and the user sees only fields they actually need to think about. Add a `memory` block (a **plugin config block** owned by the bundled memory plugin) only when overriding its defaults; see the `typeclaw-memory` skill for the schema.
69
69
 
70
70
  If the user said yes to "Wire a Discord bot?" during `typeclaw init`, the scaffold also includes:
71
71
 
@@ -132,137 +132,63 @@ When the user describes a use case rather than naming a path — "transcribe my
132
132
 
133
133
  The reference is **a lookup table, not a wishlist** — recommending a path there is not a license to add the mount silently. The user still has to ask, you still follow the standard procedure (read file, check collisions, pick name, append, write, commit, restart-required), and you still surface the TCC/FDA requirement before promising the agent can read FDA-gated data.
134
134
 
135
- ## Channels
135
+ ## Channels and Alias
136
136
 
137
- `channels` configures which external messenger adapters are enabled and how the engagement layer should behave on each. **Access control lives in `roles`, not here** — to admit a chat, declare a role match-rule that covers it (see `typeclaw-permissions`). The shape is `channels: { "<adapter-id>": { engagement, history, enabled } }`. Today the adapters are `discord-bot`, `slack-bot`, `telegram-bot`, and `kakaotalk`.
137
+ `channels` configures which external adapters (`discord-bot`, `slack-bot`, `telegram-bot`, `kakaotalk`, and `github`) are enabled and how the engagement layer behaves on each; `alias` lists plain-text names the agent answers to. Both are **live-reloadable** edits take effect on the next `reload`, no container restart.
138
138
 
139
- The channels block is **live-reloadable** edits take effect on the next `reload`, no container restart.
139
+ This skill owns only the **schema and edit mechanics** of these two fields (see the schema table above): `channels: { "<adapter-id>": { engagement, history, enabled } }` and `alias: [...]`. The **behavioral contract** for the messenger adapters — when the agent wakes to reply vs. observes, engagement triggers (mention/reply/dm), reply stickiness, the non-configurable solo-human fallback, alias substring-match semantics, and peer-name suppressors — lives in the **`typeclaw-channels`** skill. **Load `typeclaw-channels` before answering any "why did/didn't the agent respond", "make it quieter", "answer to this nickname", or engagement/alias-behavior question.** Editing the fields here still follows the standard safe-edit workflow (read whole file, validate, write back, commit); since both are live-reloadable, tell the user the change takes effect on the next `reload` no container restart.
140
140
 
141
- ### Adapter block
141
+ `github` is **not a messenger** — it is a webhook-driven channel that watches repositories and reviews pull requests. It has its own fields (`repos`, `review`, …) on top of the common `engagement`/`history`/`enabled` shape, and depends on a tunnel to receive webhooks. Its configuration is documented in the **GitHub channel** section below.
142
142
 
143
- Each entry in `channels` is keyed by adapter id and has this shape:
143
+ **Access control is separate again**: whether an inbound is admitted at all lives in `roles`, not `channels`. By default an adapter with no `roles` block matching the speaker resolves to `guest` and the router drops every inbound. See the `typeclaw-permissions` skill.
144
144
 
145
- | Field | Required | Type | Notes |
146
- | ------------ | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147
- | `engagement` | no | object | When the agent should auto-reply vs. stay silent. Defaults to mention/reply/dm with 15-minute reply stickiness. See **Engagement** below. |
148
- | `history` | no | object | Cold-start prefetch windows for `(thread.head, thread.tail, channel.tail)`. Set any to `0` to disable that side. Defaults to `{ thread: { head: 3, tail: 10 }, channel: { tail: 10 } }`. |
149
- | `enabled` | no | boolean | Defaults to `true`. Set `false` to disable the adapter entirely without removing its config. |
145
+ ## GitHub channel
150
146
 
151
- To stop the agent answering in a specific channel, narrow the `roles` block so the speaking author's role no longer carries `channel.respond` engagement triggers gate wake-up _given_ the message is admitted; `channel.respond` gates whether the message is admitted at all.
147
+ The `github` adapter is a **webhook-driven inbound channel**, not a messenger. GitHub posts events (PR opened, review requested, issue/PR comments, discussion comments) to a webhook URL; the runtime turns each event into a channel inbound and the agent engages on it directly — reviewing the PR, answering the comment, etc. There is **no "forward to a Slack channel" step**: the GitHub channel _is_ the destination. If a user asks you to "set up a GitHub webhook to send to a channel", they are describing a flow that does not exist — clarify that the GitHub channel reviews PRs in place, and ask which repos they want it to watch.
152
148
 
153
- ### Engagement
149
+ Its config block adds these fields on top of the common `engagement`/`history`/`enabled` shape:
154
150
 
155
- `engagement` controls when the agent's loop wakes up to reply on an inbound message it has permission to read. Two fields:
151
+ | Field | Required | Type | Notes |
152
+ | ---------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
153
+ | `repos` | no | string[] | Repositories the adapter watches, as `owner/name` slugs. Default `[]` (watches nothing). On start the adapter registers (or updates/claims) a managed webhook on each repo; on stop it deletes the hooks it managed during that start. |
154
+ | `review` | no | object | Code-review policy: `{ on, approve }`. See below. Default `{ on: "review_requested", approve: true }`. |
155
+ | `webhookPort` | no | number | Container port the webhook server binds to. Default `8975`. Rarely changed. |
156
+ | `webhookUrl` | no | string | Explicit public webhook URL. **Usually omit** — when a `tunnels[]` entry targets the github channel, the URL is resolved from the tunnel automatically (see below). |
157
+ | `eventAllowlist` | no | string[] | Which webhook events are accepted. Has a sane default; leave it unless the user has a specific reason. |
156
158
 
157
- ```json
158
- "engagement": {
159
- "trigger": ["mention", "reply", "dm"],
160
- "stickiness": { "perReply": { "window": 900000 } }
161
- }
162
- ```
163
-
164
- - **`trigger`** — array of one or more of `"mention"`, `"reply"`, `"dm"`. Default: all three.
165
- - `mention` — explicit `@bot` mentions.
166
- - `reply` — message is a Discord reply pointed at the agent's own message.
167
- - `dm` — any message in a DM channel.
168
- - **`stickiness`** — either the literal string `"off"`, or `{ perReply: { window: <ms> } }`. Default: 15-minute reply stickiness (`window: 900000`).
169
- - `perReply` means: after the agent replies to a user, follow-up messages from that same user in that same channel within the window also wake the loop, even without a mention. The window is bounded server-side (`1` to `86_400_000` ms — 1 ms to 24 hours).
170
- - `"off"` disables stickiness — the agent only wakes on explicit triggers.
171
-
172
- There is also a **solo-human fallback** built into the runtime that is **not configurable** through `engagement`: in any channel where the participants cache currently holds at most one distinct human author, every admitted inbound wakes the loop, regardless of `trigger` or `stickiness`. The fallback turns off the moment a second distinct human posts in that channel. This makes "private dev channel with one human and the bot" work without forcing an `@mention` on every message; clearing `trigger` to `[]` does **not** override it.
173
-
174
- **Engagement does not gate access.** Access is gated by `permissions.has(origin, 'channel.respond')` — see the `typeclaw-permissions` skill. Engagement decides whether an _admitted_ inbound wakes the loop or sits in the context buffer.
175
-
176
- ### Example
177
-
178
- ```json
179
- "channels": {
180
- "discord-bot": {
181
- "engagement": {
182
- "trigger": ["mention", "reply", "dm"],
183
- "stickiness": { "perReply": { "window": 900000 } }
184
- },
185
- "enabled": true
186
- }
187
- },
188
- "roles": {
189
- "member": { "match": ["discord:123456789012345678/987654321098765432", "discord:dm/*"] }
190
- }
191
- ```
192
-
193
- This says: the `discord-bot` adapter is enabled with default engagement; one specific channel in one specific guild plus all DMs admit speakers as `member` (which carries `channel.respond` by default).
194
-
195
- ### When the user asks "let me talk to you in this channel"
196
-
197
- This is a **`roles`** edit, not a `channels` edit. See the `typeclaw-permissions` skill for the full procedure. Short version:
198
-
199
- 1. Get the platform ID (Discord channel ID, Slack channel ID, Telegram chat ID, KakaoTalk chat ID).
200
- 2. Append a match-rule to `roles.member.match` using the canonical DSL (`discord:<guild>/<channel>`, `slack:<team>/<channel>`, `telegram:<chat>`, `kakao:<chat>`). Pass `acknowledgeGuards: { rolePromotion: true }` in the `write`/`edit` args — the `rolePromotion` security guard blocks any widening of `roles.<role>.match` without an ack (see `typeclaw-permissions`).
201
- 3. **`roles` is restart-required** — `typeclaw reload` won't apply it; the user needs `typeclaw restart`.
202
-
203
- ### When the user asks "stop replying in this channel"
204
-
205
- Two interpretations — ask if unclear:
206
-
207
- - **"Stop everything"** — remove the match-rule from `roles.<role>.match`. The agent loses both inbound visibility and outbound posting on that channel.
208
- - **"Just stop auto-replying"** — leave the match-rule, but adjust `engagement` on the adapter (set `trigger: []` and/or `stickiness: "off"`). The agent can still receive the channel and can still post if you tell it to. Caveat: this approach does NOT silence the agent in a channel that currently has only one human posting — the solo-human fallback (see Engagement) overrides `trigger: []`. In that case the only way to go silent today is to remove the match-rule.
209
-
210
- The second is usually what people mean by "be quieter".
211
-
212
- ### When the user asks "what channels can you see / are you in"
213
-
214
- 1. **Read `typeclaw.json`**, list each adapter under `channels`: which is enabled, the engagement triggers and stickiness window.
215
- 2. Also read `roles.<role>.match` for every role — those are the actual admit lists.
216
- 3. Note that the live runtime may have a different view if `typeclaw.json` was edited but `reload` hasn't run yet — say so when relevant.
217
-
218
- ## Alias
219
-
220
- `alias` is an array of plain-text names the agent answers to when a channel message contains the name without using the platform's `<@id>` mention syntax. It is independent from `channels.<adapter>.engagement.trigger`: the structural triggers (`mention`, `reply`, `dm`) gate engagement on platform-rendered events; `alias` gates engagement on the message text itself.
221
-
222
- The agent folder's directory name (`basename(agentDir)`) is **always** an implicit alias — the runtime adds it automatically. `alias` adds further forms on top: Latin transliteration of a Korean nickname, casual short forms, alternative spellings, etc. **You only need to add the dir-name explicitly when you want a variation of it** (different casing, a different word entirely, or extra forms beyond the dir name).
159
+ **`review.on`** — which `pull_request` action triggers an automatic code review:
223
160
 
224
- ### Match semantics
161
+ - `"review_requested"` (default) — review only when the bot is added as a reviewer.
162
+ - `"opened"` — review every non-draft PR as soon as it opens (a draft is reviewed once it turns ready, or the bot is requested).
163
+ - `"off"` — disable automatic code review entirely (the channel still receives comment events).
225
164
 
226
- - **Substring** match against the inbound text. `"토토"` matches `"토토아 cron"`, `"토토씨 안녕"`, `"누가 토토을 불러"`, all of them. Korean particles aren't stripped substring is enough because the bot name appears at the start of every particled form.
227
- - **Case-insensitive** via `toLocaleLowerCase()` on both sides. `"Toto"` in the alias list matches `"TOTO"`, `"toto"`, `"ToTo"`.
228
- - **No word-boundary detection.** A short or generic alias like `"bot"` will match every message containing `"robot"` or `"bottom"`. Pick distinctive names — the operator owns curation.
165
+ **`review.approve`** when `true` (default) the agent may submit a formal approving review; when `false` it downgrades an approve verdict to a plain `COMMENT` (findings still posted, no formal approval).
229
166
 
230
- ### Engagement priority
167
+ Engagement, stickiness, and the solo-human fallback behave the same as for messenger adapters — see the `typeclaw-channels` skill for that behavioral model; this section covers only the github-specific config.
231
168
 
232
- The alias path runs **after** explicit triggers (mention/reply/dm) and the sticky check. So a message with both an `<@id>` mention and an alias substring engages once, normally. A message with only the alias substring engages on the alias path. The alias path is **NOT suppressed by `mentionsOthers`**: addressing two bots in one message (`"토토아 라라아 둘 다 봐"`) engages both bots — each on their own alias.
169
+ ### Adding or removing watched repos
233
170
 
234
- There's also a symmetric **peer-name suppressor**: if the message contains a peer bot's observed display name (from `participants[]`, populated as peers speak in the channel) and **does not** contain any of this agent's aliases, the solo-human fallback is suppressed and the agent observes. This is what makes `"라라아 cron 좀"` in a 1-human-multi-bot channel correctly observe instead of all bots replying. First-time addressing of a never-seen peer slips through; the suppressor catches it after the peer's first message.
171
+ This is the most common request ("review repo X too", "stop watching repo Y"). It is a `channels.github.repos` edit **non-secret**, but read the reload caveat carefully:
235
172
 
236
- ### Example
173
+ 1. Read `typeclaw.json`, find `channels.github.repos`, add/remove the `owner/name` slug, write the whole file back.
174
+ 2. **Webhook registration is restart-required, not reload.** The adapter registers GitHub webhooks only when it `start()`s, and `reload` does not re-run start for an already-running adapter (it only handles enable/disable and credential rotation). So although `channels` is broadly "live-reloadable", a `repos` change does **not** create the new repo's webhook on `reload` — the operator must run `typeclaw restart` (host stage) for the adapter to register the hook on the added repo and remove the hook it created for a removed one.
175
+ 3. Tell the user accurately: "Added `owner/name` to `channels.github.repos`. This needs `typeclaw restart` (not just `reload`) for the webhook to be registered on that repo." Do not claim it took effect on `reload`.
237
176
 
238
- ```json
239
- {
240
- "alias": ["toto", "토토"]
241
- }
242
- ```
177
+ > Why restart and not reload: GitHub webhooks are created/removed only in the adapter's `start()`/`stop()`. `reload` does not restart a running adapter for a `repos` change (it only handles enable/disable and credential rotation), so the new repo's webhook simply isn't created until the container restarts. `typeclaw restart` is the honest answer for "start reviewing repo X".
243
178
 
244
- The agent in folder `토토/` already answers to `"토토"` from the dir name. This adds the Latin transliteration so users can also write `"Hey toto, deploy?"`.
179
+ **Webhook delivery requires a public URL.** GitHub must be able to reach the container. That URL comes from one of two places; without one the repo can be listed in config, but the adapter **skips webhook registration entirely** (it logs the skip) and no events arrive until a tunnel or `webhookUrl` is in place:
245
180
 
246
- ### When the user asks "respond to my casual nickname for you" / "I want to call you X"
181
+ - A **tunnel** entry: `tunnels: [{ name: "github-webhook", provider: "cloudflare-quick", for: { kind: "channel", name: "github" } }]`. The adapter pulls its URL from the tunnel manager automatically — leave `webhookUrl` unset. This is the normal setup. Adding/removing a tunnel is **restart-required** (`tunnels` is not live-reloadable) — see the `typeclaw-tunnels` skill.
182
+ - An explicit `channels.github.webhookUrl` the operator manages by hand.
247
183
 
248
- 1. **Read `typeclaw.json`.**
249
- 2. **If `alias` exists**, append the new name (preserve existing entries; dedupe trivially — the runtime also dedupes).
250
- 3. **If `alias` is absent**, create it as `["<new name>"]`.
251
- 4. **You don't need to add the dir name** unless the new name IS a variation of the dir name itself (e.g. dir is `toto` and the user wants `Toto` casing — the implicit dir alias matches case-insensitively, so this isn't needed either).
252
- 5. **Trim whitespace** before adding. The schema rejects empty/whitespace-only entries; the runtime trims surrounding whitespace from valid entries.
253
- 6. **Write, commit**: "Edited `alias` — live-reloadable. Run `reload` to pick up the change without restart."
254
-
255
- ### When the user asks "stop responding to <name>"
184
+ If neither exists, say so plainly: the repo is configured but events won't arrive until a tunnel (or `webhookUrl`) is in place.
256
185
 
257
- 1. **Read `typeclaw.json`.**
258
- 2. **Remove the entry** from `alias`. If the entry IS the dir name, removing it from `alias` does nothing — the dir name is implicit and can't be turned off this way. The right answer there is "to stop responding to your dir name, rename the agent folder, which is a host-stage operation outside this container."
259
- 3. **Write, commit, reload-required.**
186
+ ### Initial setup and auth — this is a host-stage step, not yours
260
187
 
261
- ### When the user asks "what names do you respond to"
188
+ The **first-time** GitHub channel setup (creating the `channels.github` block, supplying the GitHub auth — a PAT or App private key — and the webhook secret, and optionally provisioning the tunnel) is done by the operator on the host with `typeclaw channel add github`. You cannot run that: it is a host-stage CLI, and the credentials live in `secrets.json` / `.env`, which you must never write. So:
262
189
 
263
- 1. **Read `typeclaw.json`** and report `alias`.
264
- 2. **Always also report `basename(agentDir)`** (the implicit dir-name alias)the user might not realize it's automatic.
265
- 3. Mention that channel addressing also engages on `<@id>` mentions and replies regardless of alias config (those are separate triggers in `channels.<adapter>.engagement`).
190
+ - If there is **no `channels.github` block yet**, do not try to bootstrap it by hand. Tell the operator to run `typeclaw channel add github` from the agent folder, which collects the auth + webhook secret and wires the tunnel.
191
+ - Once the block exists, **repo and review changes are yours** they are plain `typeclaw.json` edits (above), no secrets involved.
266
192
 
267
193
  ## portForward
268
194
 
@@ -343,66 +269,10 @@ Off switch — the broker is constructed but never opens a WS, no LISTEN gets fo
343
269
 
344
270
  The `docker.file` block has two layers of customization:
345
271
 
346
- 1. **Toggles** for opinionated package installs typeclaw knows how to layer correctly (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `claudeCode`, `codexCli`). Most are apt packages — boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`) — and benefit from BuildKit cache mounts. `cloudflared`, `claudeCode`, and `codexCli` are the exceptions: `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's `curl | bash` installer, `codexCli` `bun install`s the `@openai/codex` npm package; all three are boolean-only. Use a toggle whenever it covers what the user wants over a hand-rolled `append` entry.
272
+ 1. **Toggles** for opinionated package installs typeclaw knows how to layer correctly (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `xvfb`, `claudeCode`, `codexCli`). Most are apt packages — boolean for on/off, version string for an apt pin — and benefit from BuildKit cache mounts. Use a toggle whenever it covers what the user wants over a hand-rolled `append` entry.
347
273
  2. **`append`** is the escape hatch for everything the toggles don't cover. An array of single-line Dockerfile instructions spliced in right before `ENTRYPOINT`, prefixed with a `# Custom lines from typeclaw.json#docker.file.append.` comment.
348
274
 
349
- ### Fields
350
-
351
- | Field | Required | Type | Notes |
352
- | ------------- | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
353
- | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
354
- | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
355
- | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
356
- | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
357
- | `cjkFonts` | no | boolean or `"auto"` | Default `"auto"`. Installs `fonts-noto-cjk` (~89 MB) so Chromium (used by `agent-browser`) renders Korean/Japanese/Chinese glyphs correctly in screenshots, `page.pdf()`, and other raster output. `"auto"` resolves at `typeclaw start` from the host locale (`LANG`/`LC_ALL`/`Intl`): a CJK host (ja/ko/zh) installs the fonts, any other host skips them. An explicit `true`/`false` forces the decision. `false` skips the layer entirely (DOM/innerText scraping is unaffected by font absence — only raster output shows tofu boxes). |
358
- | `cloudflared` | no | boolean | Default `false`. Downloads the pinned `cloudflared` GitHub release (~38 MB) into the image so `cloudflare-quick` tunnels work. Default `false` skips the layer on agents that don't use tunnels; `typeclaw tunnel add` / `channel add github` with a Cloudflare provider flip it to `true` automatically and prompt for a restart, so the happy path needs no manual edit. If the binary is absent when a tunnel starts, the tunnel goes `permanently-failed` with a "set docker.file.cloudflared: true and run typeclaw restart" message. Boolean-only — pinning is owned by the typeclaw release. |
359
- | `xvfb` | no | boolean | Default `true`. Installs `xvfb` (~5 MB) so the entrypoint shim can spawn a virtual X server and export `DISPLAY=:99`, giving headed Chrome (agent-browser `--headed`, headful Playwright) a real X11 display to defeat headless-mode WAF fingerprinting. `false` skips the layer; the shim self-heals (no `Xvfb` on PATH → execs the agent without `DISPLAY`). Boolean-only — xvfb tracks the upstream X server release with no useful apt pin. |
360
- | `claudeCode` | no | boolean | Default `false`. `true` runs Anthropic's official `curl -fsSL https://claude.ai/install.sh \| bash` in a dedicated layer (between agent-browser and the entrypoint shim) and pre-seeds `~/.claude.json` to skip the TTY-only theme picker on first launch (without it the agent's `tmux send-keys` would be eaten by the picker). Not apt: no version-pin variant; the upstream installer manages channels via env vars. Pairs with the `typeclaw-claude-code` skill, which documents the auth + tmux-driven usage flow including how to clear the post-seed API-key/trust dialogs. |
361
- | `codexCli` | no | boolean | Default `false`. `true` runs `bun install -g @openai/codex` in a dedicated layer (after `claudeCode`, before the entrypoint shim) and pre-writes `~/.codex/hooks.json` registering `SessionStart` + `Stop` hooks so the operator can detect turn boundaries the same way as Claude Code (sentinel files, `.session-id` discovery). Not apt: no version-pin variant. Codex CLI has NO theme picker so no onboarding seed is needed, but auth (`codex login` or `OPENAI_API_KEY`) and the per-project trust dialog are still required at runtime — handled by the `typeclaw-codex-cli` skill. |
362
- | `append` | no | array of strings | Each entry is a single Dockerfile line — schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
363
-
364
- Toggle version strings reject whitespace and `=` (apt-injection guard) — pass just the version, not `pkg=ver`.
365
-
366
- ### The single-line constraint (`append` only)
367
-
368
- Each entry of `append` must be one Dockerfile instruction's worth of source — a `RUN`, `ENV`, `COPY`, `ARG`, etc. The schema enforces "no embedded newlines" because a multiline string in the JSON would silently break Dockerfile syntax (Dockerfile line continuations require backslashes at end-of-line, and a JSON multiline doesn't carry those). If the user wants a logically multi-step instruction, give them two entries:
369
-
370
- ```json
371
- "docker": {
372
- "file": {
373
- "append": [
374
- "RUN apt-get update && apt-get install -y --no-install-recommends ripgrep fd-find",
375
- "ENV CUSTOM_TOOL=1"
376
- ]
377
- }
378
- }
379
- ```
380
-
381
- A single `RUN` with `&&`-chained shell commands is fine and idiomatic — that's still a single Dockerfile line. What's rejected is a literal newline inside the JSON string.
382
-
383
- ### Where things land in the build
384
-
385
- The template's last layers are roughly:
386
-
387
- ```
388
- RUN apt-get install ... <baseline + enabled toggle packages> ← toggles fan out into this line
389
- ...
390
- ENV NODE_ENV=production
391
- # Custom lines from typeclaw.json#docker.file.append. ← only emitted when append is non-empty
392
- <your appended lines>
393
- ENTRYPOINT ["/usr/local/bin/typeclaw-entrypoint"]
394
- CMD ["run"]
395
- ```
396
-
397
- The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/var/cache/apt` and `/var/lib/apt/lists`, so toggling `ffmpeg: true` (or pinning `gh: "2.40.0"`) only re-fetches what changed. The `gh` keyring bootstrap is in its own earlier layer that's gated on `gh` being enabled — turning `gh: false` saves the network roundtrip even on cold builds.
398
-
399
- `append` runs after every cache-friendly base layer (apt setup, the toggle-driven apt install, `agent-browser`, Chrome for Testing on amd64), so changing `append` invalidates only the final layer. Conversely, putting `apt-get install` in `append` is **slower than using a toggle** (no BuildKit cache mount) — and if the package you want is `tmux/gh/python/ffmpeg/cjkFonts`, just use the toggle.
400
-
401
- ### Restart and rebuild semantics
402
-
403
- - **Restart-required.** `docker.file` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired` and the live container keeps running on the old image.
404
- - **The next `typeclaw start` rebuilds the image automatically.** No `--build` flag is needed; the CLI re-runs `docker build` whenever the Dockerfile content has changed (it rewrites the file from the current template + current `docker.file` block every start). Tell the user: "Edited `docker.file` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image."
405
- - **Pre-existing host-side edits to the Dockerfile are clobbered.** If the user manually edited the Dockerfile before, the next `start` overwrites it and (if the working tree was dirty) auto-commits the cleanup. This is by design; don't try to preserve manual edits.
275
+ For the full toggle catalog (per-toggle defaults, types, version-pin rules, what each installs and why), the `append` single-line constraint, where things land in the build, and the restart/rebuild semantics, consult `references/dockerfile.md`. The playbooks below are the entry point; open the reference when you need the specifics of a toggle or the build-layer details. **`docker.file` is restart-required** — the next `typeclaw start` rewrites the Dockerfile and rebuilds the image automatically (no `--build` flag needed).
406
276
 
407
277
  ### When the user asks "install <package> in the container" / "add a Dockerfile line"
408
278
 
@@ -646,7 +516,7 @@ Never echo, log, or commit values from `secrets.json` or `.env`. Both are gitign
646
516
  - **Do not promise to write to a `readOnly: true` mount.** Docker enforces it via `:ro`; writes will fail with EROFS. If the user wants you to edit a read-only mount, the fix is to flip `readOnly` to `false` in `typeclaw.json` and restart, not to retry the write.
647
517
  - **Do not invent mount entries the user did not request.** Mounts expose host paths to your container; adding them silently is a security surprise.
648
518
  - **Do not add `roles.<role>.match` entries the user did not request, especially `*` or platform-wildcards (`discord:*`, `slack:*`).** Match-rules grant the agent visibility (and, for outbound, posting permission) on real channels with real people in them. Widening them silently is the same class of security surprise as adding a mount. See the `typeclaw-permissions` skill.
649
- - **Do not promise the user that a `roles` edit took effect immediately just because you wrote the file.** `roles` is **restart-required** — `typeclaw reload` returns it under `restartRequired`; the live runtime keeps the old role table until `typeclaw restart`.
519
+ - **Do not conflate the two `roles` edit effects.** `roles.<role>.match[]` edits are **live-reloadable** `typeclaw reload` rebuilds the live role table (the classifier marks `roles.match` as `applied`). `roles.<role>.permissions[]` edits are **restart-required** — `reload` returns them under `restartRequired` and the live runtime keeps the old permissions until `typeclaw restart`. Don't promise a `permissions` change took effect on `reload`, and don't tell the user to restart for a pure `match` change.
650
520
  - **Do not promise to post to a channel the speaker's role does not cover.** The router drops every inbound where the speaking author resolves to a role without `channel.respond`. If the user wants you to post somewhere new, the prerequisite is a `roles` edit + restart, not a retry.
651
521
  - **Do not conflate "stop replying" with "remove the role's match-rule".** Removing the match-rule cuts off both inbound visibility and outbound posting. If the user just wants quieter behavior, edit `engagement` instead.
652
522
  - **Do not edit the `Dockerfile` directly.** It is autogenerated and rewritten on every `typeclaw start` from `src/init/dockerfile.ts` in the typeclaw repo. Manual edits will be silently overwritten (and auto-committed away if the working tree is dirty). Customizations belong in the `docker.file` block (toggles or `append`).
@@ -0,0 +1,66 @@
1
+ # `docker.file` reference — toggle catalog and build internals
2
+
3
+ Companion to the **Dockerfile** section of the `typeclaw-config` SKILL.md. The SKILL.md owns the entry point and the per-question playbooks; this file is the lookup table for the individual `docker.file` toggles and the build-layer internals. Consult it when the user wants a specific package toggled/pinned, or asks how the toggles land in the image.
4
+
5
+ ## Toggle fields
6
+
7
+ `docker.file` has two layers of customization:
8
+
9
+ 1. **Toggles** for opinionated package installs typeclaw knows how to layer correctly (`tmux`, `gh`, `python`, `ffmpeg`, `cjkFonts`, `cloudflared`, `xvfb`, `claudeCode`, `codexCli`). Most are apt packages — boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`) — and benefit from BuildKit cache mounts. `cloudflared`, `claudeCode`, and `codexCli` are the exceptions: `cloudflared` downloads the pinned GitHub release, `claudeCode` runs Anthropic's `curl | bash` installer, `codexCli` `bun install`s the `@openai/codex` npm package; all three are boolean-only. Use a toggle whenever it covers what the user wants over a hand-rolled `append` entry.
10
+ 2. **`append`** is the escape hatch for everything the toggles don't cover. An array of single-line Dockerfile instructions spliced in right before `ENTRYPOINT`, prefixed with a `# Custom lines from typeclaw.json#docker.file.append.` comment.
11
+
12
+ | Field | Required | Type | Notes |
13
+ | ------------- | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | `tmux` | no | boolean \| string | Default `true`. `false` omits tmux from the apt install. String pins the Debian package version (e.g. `"3.3a-3"` → `tmux=3.3a-3`). |
15
+ | `gh` | no | boolean \| string | Default `true`. `false` omits **both** the `gh` package and the GitHub CLI keyring bootstrap layer (skipping the network roundtrip on cold builds). String pins the version. |
16
+ | `python` | no | boolean | Default `true`. Fans out to `python3 python3-pip python3-venv python-is-python3` (the bundle that makes `python` and `pip` resolve correctly inside the container). Boolean-only — no version pin, because Debian's `python3` is a meta-package that doesn't accept a useful pin. |
17
+ | `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
18
+ | `cjkFonts` | no | boolean or `"auto"` | Default `"auto"`. Installs `fonts-noto-cjk` (~89 MB) so Chromium (used by `agent-browser`) renders Korean/Japanese/Chinese glyphs correctly in screenshots, `page.pdf()`, and other raster output. `"auto"` resolves at `typeclaw start` from the host locale (`LANG`/`LC_ALL`/`Intl`): a CJK host (ja/ko/zh) installs the fonts, any other host skips them. An explicit `true`/`false` forces the decision. `false` skips the layer entirely (DOM/innerText scraping is unaffected by font absence — only raster output shows tofu boxes). |
19
+ | `cloudflared` | no | boolean | Default `false`. Downloads the pinned `cloudflared` GitHub release (~38 MB) into the image so `cloudflare-quick` tunnels work. Default `false` skips the layer on agents that don't use tunnels; `typeclaw tunnel add` / `channel add github` with a Cloudflare provider flip it to `true` automatically and prompt for a restart, so the happy path needs no manual edit. If the binary is absent when a tunnel starts, the tunnel goes `permanently-failed` with a "set docker.file.cloudflared: true and run typeclaw restart" message. Boolean-only — pinning is owned by the typeclaw release. |
20
+ | `xvfb` | no | boolean | Default `true`. Installs `xvfb` (~5 MB) so the entrypoint shim can spawn a virtual X server and export `DISPLAY=:99`, giving headed Chrome (agent-browser `--headed`, headful Playwright) a real X11 display to defeat headless-mode WAF fingerprinting. `false` skips the layer; the shim self-heals (no `Xvfb` on PATH → execs the agent without `DISPLAY`). Boolean-only — xvfb tracks the upstream X server release with no useful apt pin. |
21
+ | `claudeCode` | no | boolean | Default `false`. `true` runs Anthropic's official `curl -fsSL https://claude.ai/install.sh \| bash` in a dedicated layer (between agent-browser and the entrypoint shim) and pre-seeds `~/.claude.json` to skip the TTY-only theme picker on first launch (without it the agent's `tmux send-keys` would be eaten by the picker). Not apt: no version-pin variant; the upstream installer manages channels via env vars. Pairs with the `typeclaw-claude-code` skill, which documents the auth + tmux-driven usage flow including how to clear the post-seed API-key/trust dialogs. |
22
+ | `codexCli` | no | boolean | Default `false`. `true` runs `bun install -g @openai/codex` in a dedicated layer (after `claudeCode`, before the entrypoint shim) and pre-writes `~/.codex/hooks.json` registering `SessionStart` + `Stop` hooks so the operator can detect turn boundaries the same way as Claude Code (sentinel files, `.session-id` discovery). Not apt: no version-pin variant. Codex CLI has NO theme picker so no onboarding seed is needed, but auth (`codex login` or `OPENAI_API_KEY`) and the per-project trust dialog are still required at runtime — handled by the `typeclaw-codex-cli` skill. |
23
+ | `append` | no | array of strings | Each entry is a single Dockerfile line — schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens just before `ENTRYPOINT`, after `ENV NODE_ENV=production`. |
24
+
25
+ Toggle version strings reject whitespace and `=` (apt-injection guard) — pass just the version, not `pkg=ver`.
26
+
27
+ ## The single-line constraint (`append` only)
28
+
29
+ Each entry of `append` must be one Dockerfile instruction's worth of source — a `RUN`, `ENV`, `COPY`, `ARG`, etc. The schema enforces "no embedded newlines" because a multiline string in the JSON would silently break Dockerfile syntax (Dockerfile line continuations require backslashes at end-of-line, and a JSON multiline doesn't carry those). If the user wants a logically multi-step instruction, give them two entries:
30
+
31
+ ```json
32
+ "docker": {
33
+ "file": {
34
+ "append": [
35
+ "RUN apt-get update && apt-get install -y --no-install-recommends ripgrep fd-find",
36
+ "ENV CUSTOM_TOOL=1"
37
+ ]
38
+ }
39
+ }
40
+ ```
41
+
42
+ A single `RUN` with `&&`-chained shell commands is fine and idiomatic — that's still a single Dockerfile line. What's rejected is a literal newline inside the JSON string.
43
+
44
+ ## Where things land in the build
45
+
46
+ The template's last layers are roughly:
47
+
48
+ ```
49
+ RUN apt-get install ... <baseline + enabled toggle packages> ← toggles fan out into this line
50
+ ...
51
+ ENV NODE_ENV=production
52
+ # Custom lines from typeclaw.json#docker.file.append. ← only emitted when append is non-empty
53
+ <your appended lines>
54
+ ENTRYPOINT ["/usr/local/bin/typeclaw-entrypoint"]
55
+ CMD ["run"]
56
+ ```
57
+
58
+ The toggle-driven apt install benefits from BuildKit `--mount=type=cache` on `/var/cache/apt` and `/var/lib/apt/lists`, so toggling `ffmpeg: true` (or pinning `gh: "2.40.0"`) only re-fetches what changed. The `gh` keyring bootstrap is in its own earlier layer that's gated on `gh` being enabled — turning `gh: false` saves the network roundtrip even on cold builds.
59
+
60
+ `append` runs after every cache-friendly base layer (apt setup, the toggle-driven apt install, `agent-browser`, Chrome for Testing on amd64), so changing `append` invalidates only the final layer. Conversely, putting `apt-get install` in `append` is **slower than using a toggle** (no BuildKit cache mount) — and if the package you want is `tmux/gh/python/ffmpeg/cjkFonts`, just use the toggle.
61
+
62
+ ## Restart and rebuild semantics
63
+
64
+ - **Restart-required.** `docker.file` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired` and the live container keeps running on the old image.
65
+ - **The next `typeclaw start` rebuilds the image automatically.** No `--build` flag is needed; the CLI re-runs `docker build` whenever the Dockerfile content has changed (it rewrites the file from the current template + current `docker.file` block every start). Tell the user: "Edited `docker.file` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image."
66
+ - **Pre-existing host-side edits to the Dockerfile are clobbered.** If the user manually edited the Dockerfile before, the next `start` overwrites it and (if the working tree was dirty) auto-commits the cleanup. This is by design; don't try to preserve manual edits.