typeclaw 0.1.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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typeclaw-config
|
|
3
|
+
description: "Read or edit typeclaw.json: model, port, mounts, plugins, channels (Discord allow rules + engagement), portForward (auto port forwarding policy), dockerfile (tmux/gh/python/ffmpeg toggles + append), gitignore.append. Also: any question about a default value or whether a behavior is already on by default — port 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). Read it before touching typeclaw.json — strict schema, mix of live-reloadable and restart-required fields."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# typeclaw-config
|
|
7
|
+
|
|
8
|
+
You have a runtime config file at `./typeclaw.json` in your agent folder. It tells the typeclaw runtime which model powers you, which port the websocket server listens on, which host directories are bind-mounted into your container, which plugins to load, and which external messenger channels you can read from and post to. This skill exists so you do not corrupt the file, do not promise behavior the runtime cannot deliver, and do not surprise the user.
|
|
9
|
+
|
|
10
|
+
This file is **not** about who you are — that is `IDENTITY.md`, `SOUL.md`, etc. This file is about the machine you run on.
|
|
11
|
+
|
|
12
|
+
## What `typeclaw.json` actually controls
|
|
13
|
+
|
|
14
|
+
The runtime reads `typeclaw.json` at container startup. Some fields are picked up live on `reload`; others require a restart. It controls:
|
|
15
|
+
|
|
16
|
+
- `port` — the TCP port the websocket server binds to inside the container. The TUI on the host stage connects to this. Default `8973`. **Restart-required.**
|
|
17
|
+
- `model` — a fully-qualified `<provider>/<model-id>` string. The runtime resolves this against the built-in provider registry to decide which API to call for every turn. **Live-reloadable.**
|
|
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
|
+
- `plugins` — array of plugin package names loaded at server boot. **Restart-required.**
|
|
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 allow rules and engagement triggers that gate which external messenger channels (today: Discord) you can read from and post to. **Live-reloadable** — edits take effect on the next `reload` without a container restart.
|
|
22
|
+
- `dockerfile` — controls what ships in the autogenerated container image. Two layers: (1) **toggles** for opinionated apt packages (`tmux`, `gh`, `python` default `true`; `ffmpeg` defaults `false`) — set the toggle to `false` to omit, or to a version string like `"2.40.0"` to apt-pin (`python` is boolean-only). (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. **Restart-required** (next `typeclaw start` rebuilds the image).
|
|
23
|
+
- `gitignore.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. **Restart-required** (next `typeclaw start` refreshes and commits `.gitignore`).
|
|
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
|
+
|
|
26
|
+
### Reload vs. restart
|
|
27
|
+
|
|
28
|
+
There is no file watcher, but there is a `reload` mechanism. When `typeclaw.json` changes:
|
|
29
|
+
|
|
30
|
+
- **Live-reloadable fields** (`model`, `alias`, `channels`) take effect on the next `reload` — no container restart.
|
|
31
|
+
- **Restart-required fields** (`port`, `mounts`, `plugins`, `portForward`, `dockerfile`, `gitignore`) are reported as "reload landed but change won't apply until restart". The diff returns success; the runtime still has the old value in memory. Tell the user explicitly which one they're hitting. `dockerfile` additionally requires an image rebuild — that happens automatically on the next `typeclaw start`, no extra flag needed. `gitignore` refreshes the managed `.gitignore` and auto-commits it on the next `typeclaw start` if content changed.
|
|
32
|
+
- **`$schema`** changes are ignored.
|
|
33
|
+
|
|
34
|
+
When you edit `typeclaw.json`, name the effect: "Edited `channels` — live-reloadable, takes effect on the next `reload`." vs. "Edited `port` — restart-required, run `typeclaw restart` (host stage) to pick up the change." Conflating the two misleads the user into restarting unnecessarily, or worse, into believing a restart-required edit took effect when it did not.
|
|
35
|
+
|
|
36
|
+
You yourself cannot run `typeclaw restart` — that is a host-stage command and you live inside the container. Only the user can restart you. Do not try.
|
|
37
|
+
|
|
38
|
+
## The schema (this is the whole thing today)
|
|
39
|
+
|
|
40
|
+
`typeclaw.json` is a single JSON object with these fields:
|
|
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 allow rules and engagement triggers for external messengers. Defaults to `{}` (no adapters configured). `typeclaw init` scaffolds a `discord-bot` block only if the user said yes to "Wire a Discord bot?" during the wizard and supplied a token. **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
|
+
| `dockerfile` | no | object | Customizations for the autogenerated container image build. Toggles (`tmux`, `gh`, `python`, `ffmpeg`) gate opinionated apt packages; `append` adds custom Dockerfile lines just before `ENTRYPOINT`. Defaults to `{ ffmpeg: false, gh: true, python: true, tmux: true, append: [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` rebuilds the image). See **Dockerfile** section below. |
|
|
53
|
+
| `gitignore` | no | object | Customizations for the autogenerated `.gitignore`. Today the only field is `append` — extra patterns spliced in before TypeClaw's protected ignore rules. Defaults to `{ "append": [] }`. Omitted from scaffolded `typeclaw.json`. **Restart-required** (next `typeclaw start` refreshes `.gitignore`). See **Gitignore** section below. |
|
|
54
|
+
|
|
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
|
+
|
|
57
|
+
Within the well-known ten (`$schema`, `port`, `model`, `mounts`, `plugins`, `alias`, `channels`, `portForward`, `dockerfile`, `gitignore`), **fields the schema doesn't predeclare are silently dropped**. Do not invent runtime fields like `provider`, `apiKey`, `temperature`, `maxTokens`, `systemPrompt`, `tools`, `timeout`, etc. — those are not plugin blocks, they are imaginary. If the user asks for one, say it is not yet supported and (if it makes sense) suggest they file a request.
|
|
58
|
+
|
|
59
|
+
A scaffolded `typeclaw.json` looks like:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"$schema": "./node_modules/typeclaw/typeclaw.schema.json",
|
|
64
|
+
"model": "openai/gpt-5.4-nano"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
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), `dockerfile` → `{ "ffmpeg": false, "gh": true, "python": true, "tmux": true, "append": [] }` (tmux/gh/python pre-installed, ffmpeg off, no custom build steps), `gitignore` → `{ "append": [] }` (no custom ignore patterns). `typeclaw init` deliberately omits any field whose default is owned elsewhere — `mounts`, `portForward`, `dockerfile`, and `gitignore` 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
|
+
|
|
70
|
+
If the user said yes to "Wire a Discord bot?" during `typeclaw init`, the scaffold also includes:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"channels": {
|
|
74
|
+
"discord-bot": { "allow": ["*"] }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`allow: ["*"]` means "every guild channel and every DM" — appropriate for a single-user dev setup. The wizard asks the user to confirm `["*"]` and warns them when they decline, but the current scaffold writes `["*"]` either way and expects the user to narrow it by hand afterwards. **If the user said they wanted a narrower allow list during init, do not assume the scaffold honored that — read `typeclaw.json` first.** For shared servers, narrow the allow list before joining (see **Channels** below).
|
|
79
|
+
|
|
80
|
+
## Mounts
|
|
81
|
+
|
|
82
|
+
Each entry in `mounts` is an object with:
|
|
83
|
+
|
|
84
|
+
| Field | Required | Type | Notes |
|
|
85
|
+
| ------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
86
|
+
| `name` | yes | string | Lowercase alphanumeric with `-` or `_`, must start with a letter or digit. Becomes the directory name under `mounts/` inside your agent folder. Must be unique within the array. |
|
|
87
|
+
| `path` | yes | string | Host path to expose. Absolute (`/Users/foo/proj`), `~`-prefixed (`~/proj` — expands on the host, not in the container), or relative to the agent folder. Must be non-empty. |
|
|
88
|
+
| `readOnly` | no | boolean | Defaults to `false` (read-write). Set `true` to bind-mount with the `:ro` Docker flag so you cannot accidentally write to it. |
|
|
89
|
+
| `description` | no | string | Free text for human and agent context. Surfaced nowhere by the runtime today; useful as a comment for future you. |
|
|
90
|
+
|
|
91
|
+
Example with mounts:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"$schema": "./node_modules/typeclaw/typeclaw.schema.json",
|
|
96
|
+
"model": "openai/gpt-5.4-nano",
|
|
97
|
+
"mounts": [
|
|
98
|
+
{ "name": "typeclaw", "path": "~/workspace/typeclaw", "description": "the typeclaw source repo" },
|
|
99
|
+
{ "name": "notes", "path": "~/notes", "readOnly": true, "description": "personal notes (read-only)" }
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
After `typeclaw restart`, the agent folder gains:
|
|
105
|
+
|
|
106
|
+
- `mounts/typeclaw/` → bind-mounted to `~/workspace/typeclaw` on the host (read-write)
|
|
107
|
+
- `mounts/notes/` → bind-mounted to `~/notes` on the host (read-only)
|
|
108
|
+
|
|
109
|
+
You access these like any other directory under your cwd: `read mounts/typeclaw/src/foo.ts`, `bash cd mounts/typeclaw && bun test`, etc. **Writes to `:ro` mounts will fail with EROFS — do not promise the user you can edit a read-only mount.**
|
|
110
|
+
|
|
111
|
+
The `mounts/` directory itself is **gitignored** in your agent folder. The mount _contents_ live on the host (and likely have their own VCS); your agent folder commits do not capture them. If a user asks "did you commit my changes to `mounts/x/...`", the answer is: those changes are inside `mounts/x` which is the host repo, not your agent folder. Suggest they commit there.
|
|
112
|
+
|
|
113
|
+
### When the user asks you to mount a host path
|
|
114
|
+
|
|
115
|
+
1. **Read `typeclaw.json`** (the entire file, not just `mounts`).
|
|
116
|
+
2. **Check the existing `mounts` array** for name collisions. Names must be unique.
|
|
117
|
+
3. **Pick a `name`** that follows the regex `^[a-z0-9][a-z0-9-_]*$`. If the user gave you one, validate it; if not, derive a sensible kebab-case name from the path's last segment.
|
|
118
|
+
4. **Decide `readOnly`**. If the user says "let me show you my notes" or anything sounding read-only, set `readOnly: true`. If they say "let me code on X", leave it default (false). When unsure, ask.
|
|
119
|
+
5. **Append the entry** to `mounts`. Preserve existing entries.
|
|
120
|
+
6. **Write the file back** (pretty-printed, 2-space indent, trailing newline).
|
|
121
|
+
7. **Commit** with a message explaining which mount was added and why (`typeclaw-git` skill).
|
|
122
|
+
8. **Tell the user to restart**: "Added mount `<name>` → `<path>`. Run `typeclaw restart` (host stage). The mount will appear at `mounts/<name>/` after the next start."
|
|
123
|
+
|
|
124
|
+
### When the user asks "what can you see / what's mounted"
|
|
125
|
+
|
|
126
|
+
1. **Read `typeclaw.json`**, list each mount: `name`, `path`, `readOnly`, `description`.
|
|
127
|
+
2. Optionally `ls mounts/` to confirm what is actually present right now (a mount won't appear until the next `typeclaw start` after it was added).
|
|
128
|
+
|
|
129
|
+
## Channels
|
|
130
|
+
|
|
131
|
+
`channels` configures which external messenger channels you can read from and post to. **Today the only adapter is `discord-bot`.** The shape is `channels: { "<adapter-id>": { allow, engagement, enabled } }`.
|
|
132
|
+
|
|
133
|
+
The channels block is **live-reloadable** — edits take effect on the next `reload`, no container restart. This is intentional: allow-rule changes need to feel immediate, otherwise the user has to restart you every time they want to add a channel.
|
|
134
|
+
|
|
135
|
+
### Adapter block
|
|
136
|
+
|
|
137
|
+
Each entry in `channels` is keyed by adapter id and has this shape:
|
|
138
|
+
|
|
139
|
+
| Field | Required | Type | Notes |
|
|
140
|
+
| ------------ | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
141
|
+
| `allow` | no | array of strings | List of allow rules. Defaults to `[]`, which means **the agent cannot post anywhere and the inbound bridge gates every message**. See **Allow rules** below. |
|
|
142
|
+
| `engagement` | no | object | When the agent should auto-reply vs. stay silent. Defaults to mention/reply/dm with 5-minute reply stickiness. See **Engagement** below. |
|
|
143
|
+
| `enabled` | no | boolean | Defaults to `true`. Set `false` to disable the adapter entirely without removing its config. |
|
|
144
|
+
|
|
145
|
+
`allow: []` is **not** the same as "deny everything from being delivered to the agent" — the inbound side may still hand you events for processing depending on engagement, but the `channel_send` tool will refuse to post anywhere. Conversely, `allow: ["*"]` does not turn engagement off; engagement and allow are independent gates.
|
|
146
|
+
|
|
147
|
+
### Allow rules
|
|
148
|
+
|
|
149
|
+
Each entry in `allow` is a string matching one of the patterns below. Workspace `@dm` is the literal placeholder for direct messages (Discord doesn't expose a guild id for DMs).
|
|
150
|
+
|
|
151
|
+
| Rule | Matches |
|
|
152
|
+
| ---------------------- | ---------------------------------------------------------------------- |
|
|
153
|
+
| `*` | Every guild channel **and** every DM (full firehose). |
|
|
154
|
+
| `guild:*` | Every channel in every guild. Does **not** include DMs. |
|
|
155
|
+
| `guild:<id>` | Every channel in guild `<id>`. |
|
|
156
|
+
| `guild:<id>/<channel>` | Channel `<channel>` in guild `<id>` only. |
|
|
157
|
+
| `channel:<id>` | Channel `<id>` in any guild (Discord channel IDs are globally unique). |
|
|
158
|
+
| `dm:*` | Every DM channel. Does **not** include guild channels. |
|
|
159
|
+
| `dm:<id>` | DM channel `<id>` only. |
|
|
160
|
+
|
|
161
|
+
The schema validates each rule string at load. **Bad rule = config load fails**, the runtime refuses to boot or refuses the reload. Don't fudge this; the regex is strict (numeric IDs only).
|
|
162
|
+
|
|
163
|
+
`channel:<id>` is the most surgical option and the right default when the user says "let me talk to you in #the-channel-i'm-pointing-at" — channel IDs are globally unique so you don't need the guild id. `guild:<id>` is convenient on a server you fully trust. `*` and `guild:*` are firehose patterns; only set them in single-user setups.
|
|
164
|
+
|
|
165
|
+
### Engagement
|
|
166
|
+
|
|
167
|
+
`engagement` controls when the agent's loop wakes up to reply on an inbound message it has permission to read. Two fields:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
"engagement": {
|
|
171
|
+
"trigger": ["mention", "reply", "dm"],
|
|
172
|
+
"stickiness": { "perReply": { "window": 300000 } }
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- **`trigger`** — array of one or more of `"mention"`, `"reply"`, `"dm"`. Default: all three.
|
|
177
|
+
- `mention` — explicit `@bot` mentions.
|
|
178
|
+
- `reply` — message is a Discord reply pointed at the agent's own message.
|
|
179
|
+
- `dm` — any message in a DM channel.
|
|
180
|
+
- **`stickiness`** — either the literal string `"off"`, or `{ perReply: { window: <ms> } }`. Default: 5-minute reply stickiness (`window: 300000`).
|
|
181
|
+
- `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).
|
|
182
|
+
- `"off"` disables stickiness — the agent only wakes on explicit triggers.
|
|
183
|
+
|
|
184
|
+
There is also a **solo-human fallback** built into the runtime that is **not configurable** through `engagement`: in any allow-listed 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. If a user wants strict mention-only behavior in a one-human channel today, the answer is "wait for the post-v0.3 engagement-config work" — not a config edit.
|
|
185
|
+
|
|
186
|
+
**Engagement does not gate posting.** It gates whether you wake up. If you decide to call `channel_send` to a channel that isn't in `allow`, the tool returns `{ ok: false, error }` regardless of engagement.
|
|
187
|
+
|
|
188
|
+
To make the agent silent in a channel without removing it from `allow`, the right move is usually `engagement: { trigger: [], stickiness: "off" }`, **not** removing the allow rule — removing the allow rule cuts off both inbound visibility and outbound posting, which is rarely what the user means by "stop replying". Caveat: in a one-human channel the solo-human fallback overrides `trigger: []` and the agent will still wake; the only way to silence the bot in that case is to remove the allow rule (or add a second human to the channel).
|
|
189
|
+
|
|
190
|
+
### Example
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
"channels": {
|
|
194
|
+
"discord-bot": {
|
|
195
|
+
"allow": [
|
|
196
|
+
"guild:123456789012345678/987654321098765432",
|
|
197
|
+
"dm:*"
|
|
198
|
+
],
|
|
199
|
+
"engagement": {
|
|
200
|
+
"trigger": ["mention", "reply", "dm"],
|
|
201
|
+
"stickiness": { "perReply": { "window": 300000 } }
|
|
202
|
+
},
|
|
203
|
+
"enabled": true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This says: only one specific channel in one specific guild plus all DMs are visible/postable; auto-reply on mentions, replies, or DMs; sticky for 5 minutes after replying to a user.
|
|
209
|
+
|
|
210
|
+
### When the user asks "let me talk to you in this channel"
|
|
211
|
+
|
|
212
|
+
1. **Read `typeclaw.json`.**
|
|
213
|
+
2. **Identify the adapter.** Today this is always `discord-bot`. If `channels["discord-bot"]` is missing, create it: `{ "allow": [], "engagement": { ... defaults ... }, "enabled": true }`.
|
|
214
|
+
3. **Get the channel ID from the user.** Discord channel IDs are 17–20 digit numbers. If they only gave a `#channel-name`, ask for the ID — names aren't unique and the schema needs IDs.
|
|
215
|
+
4. **Pick the rule shape:**
|
|
216
|
+
- DM: `dm:<channelId>` (or `dm:*` if they want all DMs).
|
|
217
|
+
- Guild channel they fully trust: `guild:<guildId>/<channelId>` is most explicit; `channel:<channelId>` is shorter and equivalent.
|
|
218
|
+
- Whole guild: `guild:<guildId>` — confirm explicitly that they want every channel.
|
|
219
|
+
5. **Append to `allow`.** Preserve existing entries.
|
|
220
|
+
6. **Write the file back** (pretty-printed, 2-space indent, trailing newline).
|
|
221
|
+
7. **Commit** with a message naming the rule and why (`typeclaw-git` skill).
|
|
222
|
+
8. **Tell the user the effect:** "Added `<rule>` to `channels.discord-bot.allow`. This is live-reloadable — it takes effect on the next `reload`, no restart needed."
|
|
223
|
+
|
|
224
|
+
### When the user asks "stop replying in this channel"
|
|
225
|
+
|
|
226
|
+
Two interpretations — ask if unclear:
|
|
227
|
+
|
|
228
|
+
- **"Stop everything"** — remove the matching allow rule. The agent loses both inbound visibility and outbound `channel_send` permission for that channel.
|
|
229
|
+
- **"Just stop auto-replying"** — leave the allow rule, but adjust `engagement` (set `trigger: []` and/or `stickiness: "off"`). The agent can still see 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 allow rule.
|
|
230
|
+
|
|
231
|
+
The second is usually what people mean by "be quieter".
|
|
232
|
+
|
|
233
|
+
### When the user asks "what channels can you see / are you in"
|
|
234
|
+
|
|
235
|
+
1. **Read `typeclaw.json`**, list each adapter under `channels`: which is enabled, the full `allow` list, the engagement triggers and stickiness window.
|
|
236
|
+
2. 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.
|
|
237
|
+
|
|
238
|
+
## Alias
|
|
239
|
+
|
|
240
|
+
`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.
|
|
241
|
+
|
|
242
|
+
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).
|
|
243
|
+
|
|
244
|
+
### Match semantics
|
|
245
|
+
|
|
246
|
+
- **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.
|
|
247
|
+
- **Case-insensitive** via `toLocaleLowerCase()` on both sides. `"Bongbong"` in the alias list matches `"BONGBONG"`, `"bongbong"`, `"BongBong"`.
|
|
248
|
+
- **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.
|
|
249
|
+
|
|
250
|
+
### Engagement priority
|
|
251
|
+
|
|
252
|
+
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.
|
|
253
|
+
|
|
254
|
+
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.
|
|
255
|
+
|
|
256
|
+
### Example
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"alias": ["bongbong", "봉봉"]
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The agent in folder `봉봉/` already answers to `"봉봉"` from the dir name. This adds the Latin transliteration so users can also write `"Hey bongbong, deploy?"`.
|
|
265
|
+
|
|
266
|
+
### When the user asks "respond to my casual nickname for you" / "I want to call you X"
|
|
267
|
+
|
|
268
|
+
1. **Read `typeclaw.json`.**
|
|
269
|
+
2. **If `alias` exists**, append the new name (preserve existing entries; dedupe trivially — the runtime also dedupes).
|
|
270
|
+
3. **If `alias` is absent**, create it as `["<new name>"]`.
|
|
271
|
+
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 `bongbong` and the user wants `Bongbong` casing — the implicit dir alias matches case-insensitively, so this isn't needed either).
|
|
272
|
+
5. **Trim whitespace** before adding. The schema rejects empty/whitespace-only entries; the runtime trims surrounding whitespace from valid entries.
|
|
273
|
+
6. **Write, commit**: "Edited `alias` — live-reloadable. Run `reload` to pick up the change without restart."
|
|
274
|
+
|
|
275
|
+
### When the user asks "stop responding to <name>"
|
|
276
|
+
|
|
277
|
+
1. **Read `typeclaw.json`.**
|
|
278
|
+
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."
|
|
279
|
+
3. **Write, commit, reload-required.**
|
|
280
|
+
|
|
281
|
+
### When the user asks "what names do you respond to"
|
|
282
|
+
|
|
283
|
+
1. **Read `typeclaw.json`** and report `alias`.
|
|
284
|
+
2. **Always also report `basename(agentDir)`** (the implicit dir-name alias) — the user might not realize it's automatic.
|
|
285
|
+
3. Mention that channel addressing also engages on `<@id>` mentions and replies regardless of alias config (those are separate triggers in `channels.<adapter>.engagement`).
|
|
286
|
+
|
|
287
|
+
## portForward
|
|
288
|
+
|
|
289
|
+
`portForward` is the policy for the **host-stage portbroker** — the in-`_hostd` userland TCP proxy that forwards ports your container LISTENs on to the same port number on `127.0.0.1` of the host. It exists because Docker fundamentally cannot publish new ports on a running container (`HostConfig.PortBindings` is create-time-only) and because dev servers that bind `127.0.0.1` inside the container's netns are unreachable through `docker run -p` even if the port had been published up front. The broker solves both: when you `bun run dev` and Vite LISTENs on `5173`, the broker auto-opens `127.0.0.1:5173` on the host and pumps bytes to your in-container `127.0.0.1:5173` — the user can hit `http://localhost:5173/` from their host browser without any flag, no Dockerfile change, no `docker stop && docker run -p` dance.
|
|
290
|
+
|
|
291
|
+
| Field | Required | Type | Notes |
|
|
292
|
+
| ------- | -------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
293
|
+
| `allow` | yes | `"*"` _or_ array of port integers | The discriminator. `"*"` = forward every container LISTEN. `[]` = the off switch (broker still constructed, but no WS opened). `[5173, 3000]` = strict allowlist, only those ports. |
|
|
294
|
+
| `deny` | no | array of port integers | Only meaningful when `allow: "*"`. Subtracts ports from the firehose. **Schema rejects** `deny` combined with a number-array `allow` — that combo is almost always a typo. |
|
|
295
|
+
|
|
296
|
+
The runtime quietly enforces three additional rules regardless of policy:
|
|
297
|
+
|
|
298
|
+
- **`port` (the websocket server, default 8973) is always implicitly excluded.** The host port mapping for `8973` is owned by `docker run -p ${hostPort}:8973`; forwarding it again would fight the published port and break the TUI connection. Don't list `8973` in `allow` or `deny` — it's dropped either way, but listing it is misleading.
|
|
299
|
+
- **Host port equals container port for forwarded ports.** Always, no exceptions. There is no random-port fallback for forwarded ports. If `5173` is already bound on the host (another dev server, a previous typeclaw container that didn't clean up), the forward fails; it is logged and the port is just not forwarded. Suggest the user free the port or change the in-container LISTEN port.
|
|
300
|
+
- **`portForward` is `restart-required`.** The broker captures the policy at register time on `typeclaw start`. Editing `portForward` and running `reload` will land in `restartRequired`; the live broker keeps the old policy until the next `typeclaw start`.
|
|
301
|
+
|
|
302
|
+
### Examples
|
|
303
|
+
|
|
304
|
+
Default (no `portForward` field at all): forward every LISTEN.
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"$schema": "./node_modules/typeclaw/typeclaw.schema.json",
|
|
309
|
+
"model": "openai/gpt-5.4-nano"
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Forward everything except a couple of ports the user wants to keep private:
|
|
314
|
+
|
|
315
|
+
```json
|
|
316
|
+
"portForward": {
|
|
317
|
+
"allow": "*",
|
|
318
|
+
"deny": [5432, 6379]
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Strict allowlist — only these ports get auto-forwarded, nothing else:
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
"portForward": {
|
|
326
|
+
"allow": [5173, 3000]
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Off switch — the broker is constructed but never opens a WS, no LISTEN gets forwarded:
|
|
331
|
+
|
|
332
|
+
```json
|
|
333
|
+
"portForward": {
|
|
334
|
+
"allow": []
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### When the user asks "expose port <N>" or "forward port <N> to the host"
|
|
339
|
+
|
|
340
|
+
1. **Read `typeclaw.json`.**
|
|
341
|
+
2. **Check the current `portForward`.** If absent, the default is already `{ "allow": "*" }` — every LISTEN is already forwarded. Tell them the port will appear on `127.0.0.1:<N>` of the host **as soon as something inside the container starts LISTENing on it** (the broker polls `/proc/net/tcp` every 500 ms). No config edit needed.
|
|
342
|
+
3. **If `portForward.allow` is a number array**, append `<N>` to it.
|
|
343
|
+
4. **If `portForward.allow` is `"*"` and `<N>` is in `deny`**, remove `<N>` from `deny`.
|
|
344
|
+
5. **Write the file back, commit, and tell the user**: "Edited `portForward` — restart-required. Run `typeclaw restart` (host stage) so the broker picks up the new policy."
|
|
345
|
+
|
|
346
|
+
### When the user asks "stop forwarding port <N>" or "don't expose <N> to the host"
|
|
347
|
+
|
|
348
|
+
1. **Read `typeclaw.json`.**
|
|
349
|
+
2. **Identify the right narrowing:**
|
|
350
|
+
- `allow: "*"` → add `<N>` to `deny` (preserve existing entries).
|
|
351
|
+
- `allow: [..., <N>, ...]` → remove `<N>` from the allow array.
|
|
352
|
+
- `allow: []` → already off; nothing to do.
|
|
353
|
+
3. **Write, commit, restart-required.**
|
|
354
|
+
|
|
355
|
+
### When the user asks "what ports are forwarded right now"
|
|
356
|
+
|
|
357
|
+
1. **Read `typeclaw.json`** and report the policy.
|
|
358
|
+
2. **You cannot enumerate the live forwarded set from inside the container.** That state lives in the `_hostd` daemon on the host and isn't surfaced through any tool you have. Say so honestly: "Per `typeclaw.json` the policy is `<...>`; for the live list of forwards the user should check `~/.typeclaw/log/hostd.log` or run a host-stage tool that queries the daemon."
|
|
359
|
+
|
|
360
|
+
## Dockerfile
|
|
361
|
+
|
|
362
|
+
`typeclaw start` rewrites the agent folder's `Dockerfile` from a template baked into the typeclaw CLI on **every** invocation — not just on `init`. The Dockerfile is in the truly-ignored `.gitignore` category specifically because it's regenerated; the source of truth for the template is `src/init/dockerfile.ts` in the typeclaw repo, not the agent folder. This means: editing the Dockerfile by hand inside the agent folder is pointless (the next `typeclaw start` overwrites it), and a clean clone of an agent folder onto a fresh machine works only because `start` materializes the Dockerfile before `docker build` reads it.
|
|
363
|
+
|
|
364
|
+
The `dockerfile` block has two layers of customization:
|
|
365
|
+
|
|
366
|
+
1. **Toggles** for opinionated apt packages typeclaw knows how to install with proper layer caching (`tmux`, `gh`, `python`, `ffmpeg`). Boolean for on/off, version string for an apt pin (e.g. `"gh": "2.40.0"` → `gh=2.40.0`). Use these whenever they cover what the user wants — they get BuildKit cache-mount benefits and, for `gh`, automatic keyring layer gating.
|
|
367
|
+
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#dockerfile.append.` comment.
|
|
368
|
+
|
|
369
|
+
### Fields
|
|
370
|
+
|
|
371
|
+
| Field | Required | Type | Notes |
|
|
372
|
+
| -------- | -------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
373
|
+
| `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`). |
|
|
374
|
+
| `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. |
|
|
375
|
+
| `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. |
|
|
376
|
+
| `ffmpeg` | no | boolean \| string | Default `false`. `true` apt-installs ffmpeg (~80 MB of codecs). String pins the version. |
|
|
377
|
+
| `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`. |
|
|
378
|
+
|
|
379
|
+
Toggle version strings reject whitespace and `=` (apt-injection guard) — pass just the version, not `pkg=ver`.
|
|
380
|
+
|
|
381
|
+
### The single-line constraint (`append` only)
|
|
382
|
+
|
|
383
|
+
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:
|
|
384
|
+
|
|
385
|
+
```json
|
|
386
|
+
"dockerfile": {
|
|
387
|
+
"append": [
|
|
388
|
+
"RUN apt-get update && apt-get install -y --no-install-recommends ripgrep fd-find",
|
|
389
|
+
"ENV CUSTOM_TOOL=1"
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
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.
|
|
395
|
+
|
|
396
|
+
### Where things land in the build
|
|
397
|
+
|
|
398
|
+
The template's last layers are roughly:
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
RUN apt-get install ... <baseline + enabled toggle packages> ← toggles fan out into this line
|
|
402
|
+
...
|
|
403
|
+
ENV NODE_ENV=production
|
|
404
|
+
# Custom lines from typeclaw.json#dockerfile.append. ← only emitted when append is non-empty
|
|
405
|
+
<your appended lines>
|
|
406
|
+
ENTRYPOINT ["bun", "run", "typeclaw"]
|
|
407
|
+
CMD ["run"]
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
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.
|
|
411
|
+
|
|
412
|
+
`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`, just use the toggle.
|
|
413
|
+
|
|
414
|
+
### Restart and rebuild semantics
|
|
415
|
+
|
|
416
|
+
- **Restart-required.** `dockerfile` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired` and the live container keeps running on the old image.
|
|
417
|
+
- **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 `dockerfile` block every start). Tell the user: "Edited `dockerfile` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image."
|
|
418
|
+
- **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.
|
|
419
|
+
|
|
420
|
+
### When the user asks "install <package> in the container" / "add a Dockerfile line"
|
|
421
|
+
|
|
422
|
+
1. **Read `typeclaw.json`.**
|
|
423
|
+
2. **Check if a toggle covers it.** If the package is `tmux`, `gh`, `python`, or `ffmpeg`, prefer the toggle: `"dockerfile": { "ffmpeg": true }`. For a pinned version, pass the version string: `"gh": "2.40.0"`. This is faster (BuildKit cache mount) and clearer than `append`.
|
|
424
|
+
3. **Otherwise, use `append`.** Decide on a single-line entry — for apt installs, prefer one `RUN apt-get update && apt-get install -y --no-install-recommends <pkg> && rm -rf /var/lib/apt/lists/*` line. For env vars, one `ENV` line per variable.
|
|
425
|
+
4. **Validate no embedded newlines** (`append` only). Multi-step logic must be `&&`-chained on one line, not split across array entries unless those entries are independent Dockerfile instructions.
|
|
426
|
+
5. **Append to `dockerfile.append`** (creating the field if it doesn't exist). Preserve existing entries.
|
|
427
|
+
6. **Write, commit, restart-required**: "Edited `dockerfile` — restart-required. The next `typeclaw start` will rewrite the Dockerfile and rebuild the image. The new layer will be at the end of the build, so unrelated cache layers stay valid."
|
|
428
|
+
|
|
429
|
+
### When the user asks "uninstall <package>" / "make the image smaller"
|
|
430
|
+
|
|
431
|
+
1. **Read `typeclaw.json`.**
|
|
432
|
+
2. **If the package is one of the toggles**, set it to `false`: `"dockerfile": { "tmux": false }`. Don't try to remove it via `append` — the toggle is the only way to omit a baseline package from the apt install line.
|
|
433
|
+
3. **If it's an `append` entry**, remove that entry from the array.
|
|
434
|
+
4. **Write, commit, restart-required.** Same rebuild story.
|
|
435
|
+
|
|
436
|
+
### When the user asks "show me the Dockerfile" or "what's in the image"
|
|
437
|
+
|
|
438
|
+
1. **Read `Dockerfile` directly** (it lives at the agent folder root, autogenerated). It's the full materialized template with toggles applied plus any `append` lines.
|
|
439
|
+
2. **Don't promise stability.** The template can change between typeclaw releases; the `Dockerfile` you read today may differ after the next `typeclaw start` even with no `typeclaw.json` change.
|
|
440
|
+
3. For the abstract template (without per-agent customizations), the source of truth is `src/init/dockerfile.ts` in the typeclaw repo — pointing the user there is fine if they want to understand the layer strategy.
|
|
441
|
+
|
|
442
|
+
### When the user asks "remove that custom Dockerfile line"
|
|
443
|
+
|
|
444
|
+
1. **Read `typeclaw.json`.**
|
|
445
|
+
2. **Remove the entry from `dockerfile.append`.** If the resulting array is empty AND no toggles are overridden, you may either leave it as `"append": []` or drop the whole `dockerfile` block — both are equivalent. Dropping it keeps the file minimal and matches the scaffold convention.
|
|
446
|
+
3. **Write, commit, restart-required.** Same restart story as adding: next `typeclaw start` rebuilds.
|
|
447
|
+
|
|
448
|
+
## Gitignore
|
|
449
|
+
|
|
450
|
+
`typeclaw start` rewrites the agent folder's `.gitignore` from a template baked into the typeclaw CLI on **every** invocation, then auto-commits it when the agent folder is a git repo and the file changed. The template protects two categories: truly-ignored paths (`.env`, `node_modules/`, `workspace/`, `mounts/`, `Dockerfile`, `.DS_Store`) and system-managed runtime state (`sessions/`, `memory/`, `channels/`) that TypeClaw, not the agent, commits on its own schedule. Editing `.gitignore` by hand is temporary; the next `typeclaw start` overwrites it.
|
|
451
|
+
|
|
452
|
+
The `gitignore.append` field is the supported escape hatch for additional local ignore patterns. It is an array of strings, each treated as a single `.gitignore` line. The CLI splices them into the autogenerated `.gitignore` before TypeClaw's protected rules, prefixed with a `# Custom entries from typeclaw.json#gitignore.append.` comment.
|
|
453
|
+
|
|
454
|
+
### Field
|
|
455
|
+
|
|
456
|
+
| Field | Required | Type | Notes |
|
|
457
|
+
| -------- | -------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
458
|
+
| `append` | yes | array of strings | Each entry is a single `.gitignore` line — schema **rejects** entries containing `\n` or `\r`. Defaults to `[]`. Splice happens before TypeClaw-owned ignore rules so custom negation patterns cannot unignore protected paths. |
|
|
459
|
+
|
|
460
|
+
### Ordering and protected paths
|
|
461
|
+
|
|
462
|
+
`.gitignore` is order-sensitive: later `!` negation rules can unignore earlier ignore rules. TypeClaw therefore renders `gitignore.append` **before** its own truly-ignored and system-managed entries, so even a custom `!sessions/` or `!.env` cannot override TypeClaw's protections. Custom ordinary ignore patterns still work because they add additional ignores; they just do not get the final word over TypeClaw-owned paths.
|
|
463
|
+
|
|
464
|
+
Materialized shape when `append` is non-empty:
|
|
465
|
+
|
|
466
|
+
```gitignore
|
|
467
|
+
# Custom entries from typeclaw.json#gitignore.append.
|
|
468
|
+
scratch/
|
|
469
|
+
*.local.log
|
|
470
|
+
|
|
471
|
+
# Truly ignored: ...
|
|
472
|
+
.env
|
|
473
|
+
Dockerfile
|
|
474
|
+
|
|
475
|
+
# System-managed: ...
|
|
476
|
+
sessions/
|
|
477
|
+
memory/
|
|
478
|
+
channels/
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Restart semantics
|
|
482
|
+
|
|
483
|
+
- **Restart-required.** `gitignore` is in `FIELD_EFFECTS` as restart-required. `reload` reports the change as `restartRequired`; the already-materialized `.gitignore` on disk remains unchanged until the next host-stage start.
|
|
484
|
+
- **The next `typeclaw start` refreshes and auto-commits `.gitignore`.** Tell the user: "Edited `gitignore.append` — restart-required. The next `typeclaw start` will rewrite `.gitignore` and TypeClaw will auto-commit it if the file changes."
|
|
485
|
+
|
|
486
|
+
### When the user asks "ignore this path" / "add a gitignore entry"
|
|
487
|
+
|
|
488
|
+
1. **Read `typeclaw.json`.**
|
|
489
|
+
2. **Decide on single-line patterns.** Use one array entry per `.gitignore` pattern. Do not embed newlines.
|
|
490
|
+
3. **Append to `gitignore.append`** (creating the field if it doesn't exist). Preserve existing entries.
|
|
491
|
+
4. **Do not edit `.gitignore` directly.** It is managed and will be overwritten on `typeclaw start`.
|
|
492
|
+
5. **Write, commit, restart-required**: "Edited `gitignore.append` — restart-required. The next `typeclaw start` will rewrite and auto-commit `.gitignore`."
|
|
493
|
+
|
|
494
|
+
### When the user asks "remove that custom ignore entry"
|
|
495
|
+
|
|
496
|
+
1. **Read `typeclaw.json`.**
|
|
497
|
+
2. **Remove the entry from `gitignore.append`.** If the resulting array is empty, you may either leave it as `"append": []` or drop the whole `gitignore` block — both are equivalent. Dropping it keeps the file minimal and matches the scaffold convention.
|
|
498
|
+
3. **Write, commit, restart-required.** Same refresh story as adding: next `typeclaw start` rewrites and auto-commits `.gitignore` if content changed.
|
|
499
|
+
|
|
500
|
+
## Plugin config blocks
|
|
501
|
+
|
|
502
|
+
Top-level keys in `typeclaw.json` that are **not** in the well-known ten (`$schema`, `port`, `model`, `mounts`, `plugins`, `alias`, `channels`, `portForward`, `dockerfile`, `gitignore`) are treated as plugin config blocks. The schema preserves them via `catchall(z.unknown())`, and `extractPluginConfigs` hands each block to the owning plugin's `configSchema` for validation at boot.
|
|
503
|
+
|
|
504
|
+
This skill does **not** document individual plugin blocks. For schema, defaults, and reload semantics of a specific plugin's config, defer to that plugin's own skill:
|
|
505
|
+
|
|
506
|
+
- `memory` (idle/dreaming subagent settings) → `typeclaw-memory` skill.
|
|
507
|
+
- Plugin authoring patterns (name derivation, config-block keying, `restart-required` semantics for plugin code and per-plugin config) → `typeclaw-plugins` skill.
|
|
508
|
+
|
|
509
|
+
Three rules apply to every plugin block, regardless of which plugin owns it:
|
|
510
|
+
|
|
511
|
+
1. **The block key is the plugin's derived name** (scope-stripped, `typeclaw-plugin-` prefix stripped). Getting the key wrong means the plugin sees an empty config and silently uses defaults.
|
|
512
|
+
2. **The plugin reads its config once at factory time**, so plugin block edits are effectively `restart-required` even though core's `FIELD_EFFECTS` table doesn't classify them — the well-known ten are the only entries `reloadConfig` diffs against.
|
|
513
|
+
3. **Inventing a plugin block for a plugin that isn't loaded is silent.** `extractPluginConfigs` will preserve it across reloads; the runtime will never validate it; nothing happens.
|
|
514
|
+
|
|
515
|
+
Do **not** invent plugin blocks; their existence is determined by the plugins listed in `plugins[]` (plus bundled plugins like `memory`), not by the user or by you.
|
|
516
|
+
|
|
517
|
+
## Allowed models
|
|
518
|
+
|
|
519
|
+
The model registry currently has these entries:
|
|
520
|
+
|
|
521
|
+
| `model` value | Display name | Provider | Auth | Notes |
|
|
522
|
+
| ------------------------------------------------------ | --------------- | ------------ | ------------------- | ---------------------------------------------------------------------------------------- |
|
|
523
|
+
| `openai/gpt-5.4-nano` | GPT-5.4 nano | OpenAI | API key | Default. Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 400K context. |
|
|
524
|
+
| `openai/gpt-5.4-mini` | GPT-5.4 mini | OpenAI | API key | Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 400K context. |
|
|
525
|
+
| `openai/gpt-5.4` | GPT-5.4 | OpenAI | API key | Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 1.05M context. |
|
|
526
|
+
| `openai/gpt-5.5` | GPT-5.5 | OpenAI | API key | Flagship. Requires `OPENAI_API_KEY` in `.env`. Reasoning model, 1.05M context. |
|
|
527
|
+
| `openai-codex/gpt-5.4-mini` | GPT-5.4 mini | OpenAI Codex | OAuth (ChatGPT P/P) | Cheaper Codex tier. Requires OAuth login at init. Persisted to `secrets.json`. 272K ctx. |
|
|
528
|
+
| `openai-codex/gpt-5.4` | GPT-5.4 | OpenAI Codex | OAuth (ChatGPT P/P) | Codex mid-tier. Requires OAuth login at init. Persisted to `secrets.json`. 272K context. |
|
|
529
|
+
| `openai-codex/gpt-5.5` | GPT-5.5 | OpenAI Codex | OAuth (ChatGPT P/P) | Flagship Codex. Requires OAuth login at init. Persisted to `secrets.json`. 272K context. |
|
|
530
|
+
| `fireworks/accounts/fireworks/routers/kimi-k2p6-turbo` | Kimi K2.6 Turbo | Fireworks | API key | Requires `FIREWORKS_API_KEY` in `.env`. Reasoning model, 256K context. |
|
|
531
|
+
|
|
532
|
+
**Do not write any other value into `model`.** The schema enum will reject the file at load, and the runtime will refuse to boot the agent process. If the user names a model that isn't in this table — "use Claude", "switch to o3" — be honest:
|
|
533
|
+
|
|
534
|
+
> "My registry has OpenAI's GPT-5.4 / 5.5 family (API key), the same family via ChatGPT subscription (OAuth Codex), and Fireworks' Kimi K2.6 Turbo. Other providers (Anthropic, etc.) aren't wired up yet — that needs a typeclaw release, not a config edit."
|
|
535
|
+
|
|
536
|
+
Do **not** edit `typeclaw.json` to a model the registry doesn't know, even if the user insists. That bricks the agent on next restart.
|
|
537
|
+
|
|
538
|
+
## Provider credentials
|
|
539
|
+
|
|
540
|
+
`typeclaw.json` does **not** hold API keys or OAuth tokens. Credentials live in two gitignored files:
|
|
541
|
+
|
|
542
|
+
- **`./.env`** (API key providers): the env var depends on which provider's model you've selected.
|
|
543
|
+
- `OPENAI_API_KEY` — required for any `openai/...` model.
|
|
544
|
+
- `FIREWORKS_API_KEY` — required for any `fireworks/...` model.
|
|
545
|
+
- **`./secrets.json`** (OAuth providers): structured JSON file managed by `pi-coding-agent`'s `AuthStorage`, wrapped by `SecretsBackend`. Contains refresh + access tokens. The container refreshes tokens on its own with file locking; the host writes once at `typeclaw init` time when the user picks "OAuth (browser login)". (Pre-rename agent folders may carry the file as `auth.json`; it is migrated to `secrets.json` on the next agent boot.)
|
|
546
|
+
- `openai-codex/...` models — credentials persisted under the `llm` slice as `{ "llm": { "openai-codex": { "type": "oauth", ... } } }`.
|
|
547
|
+
|
|
548
|
+
If a user wants to switch from API key to OAuth (or vice versa) for a provider that supports both, the easiest path is to delete the relevant entry from `.env` / `secrets.json` and re-run `typeclaw init` from inside the agent folder — it'll prompt for the auth method again.
|
|
549
|
+
|
|
550
|
+
If the user wants to rotate or change the key, edit `.env`, not `typeclaw.json`. After editing `.env`, the same restart rule applies: `typeclaw restart` on the host stage.
|
|
551
|
+
|
|
552
|
+
Never echo, log, or commit values from `.env`. `.env` is gitignored by default — keep it that way.
|
|
553
|
+
|
|
554
|
+
## Editing `typeclaw.json` safely
|
|
555
|
+
|
|
556
|
+
`typeclaw.json` is a single canonical file at the agent folder root. It is committed to git (not gitignored). Treat it like a config file you own.
|
|
557
|
+
|
|
558
|
+
### Workflow
|
|
559
|
+
|
|
560
|
+
1. **Read the whole file first** with the `read` tool. Don't assume what's in it — the user may have customized it.
|
|
561
|
+
2. **Modify in memory.** Change only the field(s) the user asked about. Leave `$schema` alone.
|
|
562
|
+
3. **Write the whole file back** with the `write` tool. Always pretty-printed (2-space indent), trailing newline, fields in stable order: `$schema` first, then alphabetical for the rest (`alias`, `channels`, `dockerfile`, `gitignore`, `model`, `mounts`, `plugins`, `port`, `portForward`, then any plugin config blocks like `memory`).
|
|
563
|
+
4. **Validate before declaring done.** A malformed `typeclaw.json` will refuse to boot the agent on next restart, and a malformed reload-time edit will be rejected by `reload`. Sanity-check your JSON manually or with `bash` (`cat typeclaw.json | jq .`) before considering the edit done.
|
|
564
|
+
5. **Commit the change.** See the `typeclaw-git` skill for the commit-message rule (decision context required). `typeclaw.json` is not gitignored, so an uncommitted edit will pollute your next commit.
|
|
565
|
+
6. **Tell the user the right next step.** Match the field's effect class:
|
|
566
|
+
- `model`, `alias`, `channels` → "Live-reloadable, takes effect on the next `reload`."
|
|
567
|
+
- `port`, `mounts`, `plugins`, `portForward` → "Restart-required. Run `typeclaw restart` (host stage) to pick up the change."
|
|
568
|
+
- `dockerfile` → "Restart-required, and the next `typeclaw start` will rebuild the image automatically (no `--build` flag needed)."
|
|
569
|
+
- `gitignore` → "Restart-required, and the next `typeclaw start` will rewrite and auto-commit `.gitignore` if content changed."
|
|
570
|
+
- Plugin config blocks (e.g. `memory`) → restart-required by convention because plugins read their config once at boot. Defer to the plugin's own skill for the exact semantics.
|
|
571
|
+
- Mixed edits in one go → spell out which is which; do not collapse to "restart" if part of the change is live.
|
|
572
|
+
|
|
573
|
+
### Required-shape checklist (catch this before writing)
|
|
574
|
+
|
|
575
|
+
- The file parses as JSON
|
|
576
|
+
- Top-level is an object (not an array, not a string)
|
|
577
|
+
- If `mounts` is present, it is an array (omit it or use `[]` if no host paths are exposed)
|
|
578
|
+
- Each `mounts[].name` matches `^[a-z0-9][a-z0-9-_]*$` and is unique within the array
|
|
579
|
+
- Each `mounts[].path` is a non-empty string
|
|
580
|
+
- If `port` is set: integer, 1–65535
|
|
581
|
+
- If `model` is set: exactly one of the values in **Allowed models** above
|
|
582
|
+
- If `plugins` is set: array of non-empty strings
|
|
583
|
+
- If `alias` is set: array of strings, each non-empty after trimming surrounding whitespace
|
|
584
|
+
- If `channels.discord-bot.allow` is set: every entry matches one of the **Allow rules** patterns above (numeric IDs only)
|
|
585
|
+
- If `channels.discord-bot.engagement.trigger` is set: array of `"mention"`, `"reply"`, `"dm"` (any subset, including empty)
|
|
586
|
+
- If `channels.discord-bot.engagement.stickiness` is set: either the literal `"off"` or `{ "perReply": { "window": <int 1..86400000> } }`
|
|
587
|
+
- If `portForward` is set: `allow` is either `"*"` or an array of integers (1–65535); `deny`, if present, is an array of integers and **only valid when `allow` is `"*"`** (the schema rejects `deny` paired with a number-array `allow`)
|
|
588
|
+
- If `dockerfile.append` is set: array of strings, each with no embedded `\n` or `\r` (multi-step shell logic goes in a single `&&`-chained `RUN` entry)
|
|
589
|
+
- If any `dockerfile` toggle is set: `tmux`/`gh`/`ffmpeg` are boolean or version string (no whitespace, no `=`); `python` is boolean only
|
|
590
|
+
- No unknown top-level keys you invented — keys outside the well-known ten are interpreted as **plugin config blocks** and only do something if a plugin owns them. Inventing one means the user thinks it took effect and it did not.
|
|
591
|
+
|
|
592
|
+
## Things you must not do
|
|
593
|
+
|
|
594
|
+
- **Do not invent fields the schema doesn't support** (no `provider`, `apiKey`, `temperature`, `maxTokens`, `systemPrompt`, `tools`, `timeout`, `retry`, etc.). They will be silently dropped or, worse, mistaken for a plugin config block. Lying to the user that "I added a temperature field" when the runtime ignores it is a worse failure than refusing.
|
|
595
|
+
- **Do not move secrets into `typeclaw.json`.** It is committed to git. API keys belong in `.env`.
|
|
596
|
+
- **Do not change `port` casually.** The host-stage `typeclaw start` launcher publishes a port mapping it learned at `start` time. Changing the port in `typeclaw.json` without re-running `typeclaw start` (which re-reads it) means the TUI will connect to the wrong port and silently fail. If you change `port`, tell the user explicitly that the next `typeclaw start` will pick the new mapping.
|
|
597
|
+
- **Do not change `model` to something not in the registry.** The schema enum will reject the file at load, and the runtime will refuse to boot the agent process. If the user wants a model that isn't there, this is a typeclaw-side change, not a config edit.
|
|
598
|
+
- **Do not edit `typeclaw.json` from inside an `exec` cron job's `command`.** That mutates the file behind the runtime's back. Live-reloadable fields still won't update until something triggers a `reload`, and restart-required fields are guaranteed wrong.
|
|
599
|
+
- **Do not delete `$schema`.** It powers editor autocompletion for the user. Leaving it in costs nothing.
|
|
600
|
+
- **Do not re-add `"mounts": []` "for clarity" if the user has none.** The scaffold deliberately omits it; defaults live in `configSchema`. Re-emitting it adds maintenance noise (the user has to keep two sources of truth in sync) without changing behavior.
|
|
601
|
+
- **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.
|
|
602
|
+
- **Do not invent mount entries the user did not request.** Mounts expose host paths to your container; adding them silently is a security surprise.
|
|
603
|
+
- **Do not add channel allow rules the user did not request, especially `*` or `guild:*`.** Allow rules grant the agent visibility (and, for outbound, posting permission) on real Discord channels with real people in them. Widening the allow list silently is the same class of security surprise as adding a mount.
|
|
604
|
+
- **Do not promise the user that an allow-rule edit took effect immediately just because you wrote the file.** Live-reloadable means "applied on the next `reload`", not "applied the instant the file changes". Until `reload` runs (or the container restarts), the runtime is still using the old `channels` config.
|
|
605
|
+
- **Do not promise to post to a channel that isn't in `allow`.** `channel_send` will refuse with `{ ok: false, error }` regardless of what you tell the user. If they want you to post somewhere new, the prerequisite is an allow-rule edit, not a retry.
|
|
606
|
+
- **Do not conflate "stop replying" with "remove allow rule".** Removing the allow rule cuts off both inbound visibility and outbound posting. If the user just wants quieter behavior, edit `engagement` instead.
|
|
607
|
+
- **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 `dockerfile` block (toggles or `append`).
|
|
608
|
+
- **Do not reach for `dockerfile.append` when a toggle covers it.** If the user wants tmux, gh, python, or ffmpeg installed (or removed, or pinned), use the toggle — it's the cache-mounted path. `append` for these is slower and harder to read.
|
|
609
|
+
- **Do not use `dockerfile.append` for things that belong in the template.** If the user wants a system package _every_ typeclaw user should have, that's a typeclaw release, not a per-agent `append`. Suggest filing an issue.
|
|
610
|
+
- **Do not put multiline strings in `dockerfile.append`.** The schema rejects entries with embedded `\n`/`\r`. Use one entry per Dockerfile instruction; chain shell logic with `&&` on one line.
|
|
611
|
+
- **Do not pass `pkg=ver` as a toggle version string.** The schema rejects `=` in version strings. Pass just the version (`"gh": "2.40.0"`); the renderer prepends `pkg=` itself. Same for whitespace — version strings cannot contain spaces.
|
|
612
|
+
- **Do not list `8973` (or whatever `port` is set to) in `portForward.allow`/`deny`.** That port is owned by `docker run -p`; the broker quietly excludes it regardless. Listing it is misleading.
|
|
613
|
+
- **Do not combine `portForward.deny` with a number-array `allow`.** The schema rejects this; the deny rule would have no effect even if the schema allowed it. `deny` is only meaningful with `allow: "*"`.
|
|
614
|
+
- **Do not promise "live forwarding will start the moment you set `portForward`".** `portForward` is restart-required; the broker captures the policy at register time. Until the next `typeclaw start`, the live broker keeps the old policy.
|
|
615
|
+
|
|
616
|
+
## When the user says "what model are you running"
|
|
617
|
+
|
|
618
|
+
1. **Read `typeclaw.json`.** Don't guess from prior conversation — the user may have changed it since you last looked.
|
|
619
|
+
2. Report the `model` field verbatim, plus the human-readable name from the **Allowed models** table.
|
|
620
|
+
3. If `model` is missing from the file, say so and report the default (`openai/gpt-5.4-nano` → GPT-5.4 nano).
|
|
621
|
+
|
|
622
|
+
## When the user says "switch to <model>"
|
|
623
|
+
|
|
624
|
+
1. **Check the Allowed models table.** Is the requested model in it?
|
|
625
|
+
2. **If yes:** read `typeclaw.json`, change `model`, write it back, commit, and tell the user: "Edited `model` — live-reloadable, takes effect on the next `reload`. New sessions will use it; the current in-flight prompt (if any) finishes on the old model."
|
|
626
|
+
3. **If no:** do not edit anything. Tell the user the registry doesn't have it yet, and that adding a model is a typeclaw release, not a config change.
|
|
627
|
+
|
|
628
|
+
## When the user says "change the port"
|
|
629
|
+
|
|
630
|
+
1. Confirm the new port is 1–65535 and not in the privileged range (<1024) unless the user explicitly knows they need it.
|
|
631
|
+
2. Read `typeclaw.json`, set `port`, write it back, commit.
|
|
632
|
+
3. Tell the user: "The next `typeclaw start` (host stage) will publish the new port mapping. The current container will keep running on the old port until then."
|
|
633
|
+
|
|
634
|
+
## What this skill does _not_ cover
|
|
635
|
+
|
|
636
|
+
- **Cron jobs** (`cron.json`) — see the `typeclaw-cron` skill.
|
|
637
|
+
- **Plugin authoring** (`definePlugin`, hooks, contributions, name derivation) — see the `typeclaw-plugins` skill.
|
|
638
|
+
- **The `memory` plugin config block** (`idleMs`, `dreaming.schedule`, what the memory-logger and dreaming subagents do) — see the `typeclaw-memory` skill.
|
|
639
|
+
- **Identity files** (`IDENTITY.md`, `SOUL.md`, `USER.md`, `AGENTS.md`) — these are not runtime config; they are _you_. Edit them directly when relevant; no skill needed.
|
|
640
|
+
- **`MEMORY.md` and `memory/`** — explicit exception to the line above. `MEMORY.md` is **dreaming-owned** and you must not write to it directly; the `memory/` directory holds runtime-managed daily streams and muscle-memory skills. See the `typeclaw-memory` skill before touching anything memory-shaped.
|
|
641
|
+
- **Skills directories** (`.agents/skills/`, `memory/skills/`, the bundled `src/skills/`) — these are loaded from disk by the runtime; they are not driven by `typeclaw.json`. See the `typeclaw-skills` skill for the three layers, the `bunx skills` CLI, and the lockfile-based "downloaded vs hand-authored" rule.
|
|
642
|
+
- **The Dockerfile template itself** (the autogenerated layers in `Dockerfile`: bun base image, apt setup, GitHub CLI, `agent-browser`, Chrome for Testing) — that is host-stage, controlled by `src/init/dockerfile.ts` in the typeclaw repo, not by `typeclaw.json`. `typeclaw.json#dockerfile.append` (covered above) is the only piece of the build customizable per-agent; everything else requires a typeclaw release.
|
|
643
|
+
- **The host-stage launcher's invocation flags** (`docker run` arguments synthesized by `typeclaw start`, the `_hostd` daemon's lifecycle, the host port allocation that maps to `port` inside the container) — those are host-stage code, not config. The pieces of that flow that **are** user-configurable through `typeclaw.json` (`port`, `mounts`, `portForward`) are documented above; the rest is not.
|