typeclaw 0.1.1 → 0.1.3

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 (74) hide show
  1. package/README.md +16 -12
  2. package/auth.schema.json +238 -7
  3. package/package.json +1 -1
  4. package/secrets.schema.json +238 -7
  5. package/src/agent/auth.ts +19 -38
  6. package/src/agent/doctor.ts +173 -0
  7. package/src/agent/subagents.ts +24 -2
  8. package/src/agent/tools/channel-fetch-attachment.ts +6 -0
  9. package/src/agent/tools/channel-history.ts +10 -1
  10. package/src/agent/tools/channel-log.ts +32 -0
  11. package/src/agent/tools/channel-reply.ts +18 -1
  12. package/src/agent/tools/channel-send.ts +13 -1
  13. package/src/bundled-plugins/backup/README.md +81 -0
  14. package/src/bundled-plugins/backup/index.ts +209 -0
  15. package/src/bundled-plugins/backup/runner.ts +231 -0
  16. package/src/bundled-plugins/backup/subagents.ts +200 -0
  17. package/src/bundled-plugins/memory/index.ts +42 -1
  18. package/src/bundled-plugins/tool-result-cap/README.md +67 -0
  19. package/src/bundled-plugins/tool-result-cap/cap-result.ts +56 -0
  20. package/src/bundled-plugins/tool-result-cap/index.ts +51 -0
  21. package/src/channels/adapters/kakaotalk.ts +25 -16
  22. package/src/channels/manager.ts +47 -38
  23. package/src/channels/router.ts +29 -0
  24. package/src/cli/channel.ts +3 -3
  25. package/src/cli/compose.ts +92 -1
  26. package/src/cli/doctor.ts +100 -0
  27. package/src/cli/index.ts +4 -0
  28. package/src/cli/init.ts +2 -1
  29. package/src/cli/ui.ts +11 -0
  30. package/src/compose/doctor.ts +141 -0
  31. package/src/compose/index.ts +8 -0
  32. package/src/compose/logs.ts +32 -19
  33. package/src/config/config.ts +31 -0
  34. package/src/container/log-colors.ts +75 -0
  35. package/src/container/log-timestamps.ts +84 -0
  36. package/src/container/logs.ts +71 -5
  37. package/src/container/start.ts +113 -9
  38. package/src/cron/consumer.ts +29 -7
  39. package/src/doctor/checks.ts +426 -0
  40. package/src/doctor/commit.ts +71 -0
  41. package/src/doctor/index.ts +287 -0
  42. package/src/doctor/plugin-bridge.ts +147 -0
  43. package/src/doctor/report.ts +142 -0
  44. package/src/doctor/types.ts +87 -0
  45. package/src/hostd/daemon.ts +28 -3
  46. package/src/hostd/protocol.ts +7 -0
  47. package/src/init/auto-upgrade.ts +368 -0
  48. package/src/init/cli-version.ts +81 -0
  49. package/src/init/dockerfile.ts +234 -25
  50. package/src/init/index.ts +141 -87
  51. package/src/init/kakaotalk-auth.ts +9 -3
  52. package/src/init/run-bun-install.ts +34 -0
  53. package/src/plugin/hooks.ts +32 -0
  54. package/src/plugin/index.ts +7 -0
  55. package/src/plugin/manager.ts +2 -0
  56. package/src/plugin/registry.ts +32 -3
  57. package/src/plugin/types.ts +65 -0
  58. package/src/run/bundled-plugins.ts +15 -0
  59. package/src/run/index.ts +19 -5
  60. package/src/secrets/defaults.ts +67 -0
  61. package/src/secrets/hydrate.ts +99 -0
  62. package/src/secrets/index.ts +6 -12
  63. package/src/secrets/kakao-store.ts +129 -0
  64. package/src/secrets/migrate-kakaotalk.ts +82 -0
  65. package/src/secrets/migrate.ts +5 -4
  66. package/src/secrets/resolve.ts +57 -0
  67. package/src/secrets/schema.ts +162 -42
  68. package/src/secrets/storage.ts +253 -47
  69. package/src/server/index.ts +103 -5
  70. package/src/shared/index.ts +3 -0
  71. package/src/shared/protocol.ts +22 -0
  72. package/src/skills/typeclaw-config/SKILL.md +48 -9
  73. package/typeclaw.schema.json +84 -0
  74. package/src/secrets/env.ts +0 -43
@@ -1,12 +1,33 @@
1
1
  import type { DockerfileConfig, DockerfileFeatureToggle } from '@/config/config'
2
2
 
3
+ import { GHCR_BASE_IMAGE_REPO } from './cli-version'
4
+
3
5
  export const DOCKERFILE = 'Dockerfile'
4
6
 
7
+ export type BuildDockerfileOptions = {
8
+ // Null or omitted = emit the full inline heavy stack (dev mode, tests).
9
+ baseImageVersion?: string | null
10
+ }
11
+
5
12
  // Apt packages that EVERY image must have — git for the agent runtime,
6
13
  // curl/ca-certificates/gnupg for HTTPS and key fetches that downstream layers
7
- // (e.g. the gh keyring) depend on. Listed first in the apt-get install line so
8
- // the package set is self-documenting at a glance.
9
- const BASELINE_APT_PACKAGES = ['git', 'ca-certificates', 'curl', 'gnupg'] as const
14
+ // (e.g. the gh keyring) depend on. `iptables` and `util-linux` back the
15
+ // network egress entrypoint shim, installed unconditionally so that flipping
16
+ // `typeclaw.json#network.blockInternal` is a runtime toggle (re-run
17
+ // `typeclaw restart`) and not an image rebuild.
18
+ //
19
+ // On Debian trixie the single `iptables` package ships both the IPv4 nft
20
+ // frontend (`iptables-nft`, available as `iptables` through
21
+ // update-alternatives) and the IPv6 nft frontend (`ip6tables-nft`, available
22
+ // as `ip6tables`). The standalone `iptables-nft`/`ip6tables-nft` package
23
+ // names do NOT exist on trixie — `apt install iptables-nft` fails with
24
+ // "Unable to locate package". The shim invokes `iptables` and `ip6tables`
25
+ // which alternatives resolves to the nft variants.
26
+ //
27
+ // `util-linux` carries `setpriv`, which the shim uses to drop CAP_NET_ADMIN
28
+ // from the bounding set before exec'ing the agent. Listed first in the
29
+ // apt-get install line so the package set is self-documenting at a glance.
30
+ const BASELINE_APT_PACKAGES = ['git', 'ca-certificates', 'curl', 'gnupg', 'iptables', 'util-linux'] as const
10
31
 
11
32
  // curl-impersonate is the only currently-working way to query DuckDuckGo from
12
33
  // a non-browser client on residential IPs in 2026. DDG fingerprints incoming
@@ -35,6 +56,134 @@ export const CURL_IMPERSONATE_SHA256_ARM64 = '6766bc67fd3e8e2313875f32b36b5a3fab
35
56
  // the impersonation to whatever `curl_chrome` resolves to.
36
57
  export const CURL_IMPERSONATE_PROFILE = 'chrome136'
37
58
 
59
+ export const TYPECLAW_ENTRYPOINT_PATH = '/usr/local/bin/typeclaw-entrypoint'
60
+
61
+ // IPv4 networks the container is forbidden to egress to when
62
+ // `network.blockInternal` is true. Loopback (127/8) is NOT here — loopback
63
+ // traffic uses the `lo` interface, which the shim's first ACCEPT rule
64
+ // short-circuits. The agent inside the container needs loopback to dogfood
65
+ // its own `bun run dev` server. RFC1918 (10/8, 172.16/12, 192.168/16) covers
66
+ // router admin panels and home/office LANs. 169.254/16 covers cloud
67
+ // metadata (169.254.169.254 IMDS, 169.254.170.2 ECS task role) and Windows
68
+ // APIPA. 100.64/10 is CGNAT. 224/4 multicast and 240/4 reserved are belt-
69
+ // and-suspenders against creative exfil targets. host.docker.internal (in
70
+ // 172.16/12 on Docker Desktop/Linux) is re-allowed by the shim at runtime
71
+ // via getent so the agent's `restart` tool can still reach hostd.
72
+ export const NETWORK_BLOCK_IPV4_NETS = [
73
+ '10.0.0.0/8',
74
+ '172.16.0.0/12',
75
+ '192.168.0.0/16',
76
+ '169.254.0.0/16',
77
+ '100.64.0.0/10',
78
+ '224.0.0.0/4',
79
+ '240.0.0.0/4',
80
+ ] as const
81
+
82
+ // IPv6 mirrors of the IPv4 block list. fc00::/7 is unique-local (the IPv6
83
+ // equivalent of RFC1918), fe80::/10 is link-local (incl. SLAAC + IPv6 cloud
84
+ // metadata in fd00:ec2::/64 which fits inside fc00::/7), ff00::/8 is
85
+ // multicast, ::ffff:0:0/96 is IPv4-mapped IPv6 (an attacker could otherwise
86
+ // reach 192.168.x.x via [::ffff:192.168.0.1]).
87
+ export const NETWORK_BLOCK_IPV6_NETS = ['fc00::/7', 'fe80::/10', 'ff00::/8', '::ffff:0:0/96'] as const
88
+
89
+ // Renders the shell script that runs as PID 1 inside the container. Two
90
+ // modes, picked at boot time from `$TYPECLAW_NETWORK_BLOCK_INTERNAL`:
91
+ //
92
+ // off (env unset or != "1"): no rules installed, no setpriv. Just exec
93
+ // `bun run typeclaw "$@"`. Identical observable behavior to a container
94
+ // without this feature. This is the opt-out path for users who set
95
+ // `network.blockInternal: false` in their `typeclaw.json`.
96
+ //
97
+ // on (default, env = "1" via `network.blockInternal: true`): walks IPv4 +
98
+ // IPv6 block lists and installs
99
+ // REJECT rules in the OUTPUT chain. Loopback (`-o lo`) is ACCEPT'd first
100
+ // so dev-server dogfooding still works. The hostd HTTP control port on
101
+ // `host.docker.internal` is re-allowed at runtime — narrowly, single
102
+ // TCP destport, only when hostd is configured — so the agent's `restart`
103
+ // tool can still reach the daemon. The shim then drops CAP_NET_ADMIN
104
+ // from the bounding set AND from the inheritable + ambient sets via
105
+ // setpriv before exec'ing the agent. Bounding set is the hard ceiling
106
+ // enforced by execve; inheritable + ambient are cleared defensively to
107
+ // match setpriv(1)'s explicit warning about not dropping the bounding
108
+ // set alone.
109
+ //
110
+ // Carve-out is intentionally narrow: ACCEPT only `tcp --dport <hostd-port>`
111
+ // to the host gateway, never the gateway IP wholesale. Without the dport
112
+ // scope, a compromised agent could reach any host service via
113
+ // `host.docker.internal:22` (SSH), `:53` (DNS), `:5432` (postgres), etc.
114
+ // The gateway IP itself sits inside `172.16.0.0/12`, which the IPv4 reject
115
+ // rules below DROP — the narrow ACCEPT here is the only path through.
116
+ // When hostd is not configured (`TYPECLAW_HOSTD_URL` unset or unparseable),
117
+ // nothing is ACCEPT'd: the agent loses self-restart capability but the
118
+ // rest of the egress filter still works.
119
+ //
120
+ // IPv4-only carve-out uses `getent ahostsv4` to force the resolver into
121
+ // the A-record path. Without this, `getent hosts` would return whichever
122
+ // family the resolver prefers, and on systems that prefer AAAA we'd feed
123
+ // a v6 address to `iptables` and crash under `set -e`. host.docker.internal
124
+ // resolves to a bridge gateway that is IPv4-only on every Docker runtime
125
+ // we support (Docker Desktop, OrbStack, Docker on Linux with the
126
+ // `--add-host host.docker.internal:host-gateway` flag typeclaw injects).
127
+ //
128
+ // REJECT (not DROP) so the agent fails fast with an ICMP unreachable
129
+ // instead of hanging on a 30-second connect timeout — much friendlier
130
+ // debug UX and identical security posture.
131
+ //
132
+ // `set -eu` propagates rule-install failures up to PID 1 exit, which kills
133
+ // the container. Failing closed is the right thing: an unenforced
134
+ // blockInternal=true is worse than blockInternal=false.
135
+ export function buildEntrypointShim(): string {
136
+ const ipv4Rules = NETWORK_BLOCK_IPV4_NETS.map(
137
+ (net) => `iptables -A OUTPUT -d ${net} -j REJECT --reject-with icmp-port-unreachable`,
138
+ )
139
+ const ipv6Rules = NETWORK_BLOCK_IPV6_NETS.map(
140
+ (net) => `ip6tables -A OUTPUT -d ${net} -j REJECT --reject-with icmp6-port-unreachable`,
141
+ )
142
+ return `#!/bin/sh
143
+ # AUTOGENERATED by typeclaw — do not edit.
144
+ # Source: src/init/dockerfile.ts \`buildEntrypointShim()\`.
145
+ set -eu
146
+
147
+ if [ "\${TYPECLAW_NETWORK_BLOCK_INTERNAL:-0}" != "1" ]; then
148
+ exec bun run typeclaw "$@"
149
+ fi
150
+
151
+ iptables -A OUTPUT -o lo -j ACCEPT
152
+
153
+ # Hostd HTTP control carve-out: narrow ACCEPT, scoped to one TCP port on
154
+ # the host gateway. Skipped silently when hostd is not configured.
155
+ hostd_port=""
156
+ if [ -n "\${TYPECLAW_HOSTD_URL:-}" ]; then
157
+ hostd_port="$(printf '%s' "$TYPECLAW_HOSTD_URL" | sed -n 's#^https\\{0,1\\}://[^/:]\\{1,\\}:\\([0-9]\\{1,5\\}\\).*#\\1#p')"
158
+ fi
159
+ if [ -n "\${hostd_port:-}" ]; then
160
+ host_gw_ip="$(getent ahostsv4 host.docker.internal 2>/dev/null | awk '{print $1; exit}')"
161
+ if [ -n "\${host_gw_ip:-}" ]; then
162
+ iptables -A OUTPUT -p tcp -d "$host_gw_ip" --dport "$hostd_port" -j ACCEPT
163
+ fi
164
+ fi
165
+ ${ipv4Rules.join('\n')}
166
+
167
+ ip6tables -A OUTPUT -o lo -j ACCEPT
168
+ ${ipv6Rules.join('\n')}
169
+
170
+ exec setpriv --bounding-set -net_admin --inh-caps -net_admin --ambient-caps -net_admin -- bun run typeclaw "$@"
171
+ `
172
+ }
173
+
174
+ // Layer 6: install the network-egress entrypoint shim. Content is base64-
175
+ // encoded inline so the Dockerfile is fully self-contained — no second file
176
+ // in the build context, no COPY, no chicken-and-egg between init and start.
177
+ // Layer placement is intentionally late: shim source changes invalidate
178
+ // only this small layer (~1KB image impact), keeping Chrome and apt cached.
179
+ function renderEntrypointShimLayer(): string {
180
+ const encoded = Buffer.from(buildEntrypointShim(), 'utf8').toString('base64')
181
+ return `# Layer 6 (small, changes with the egress shim): install /usr/local/bin/typeclaw-entrypoint.
182
+ # The shim is a no-op unless \`network.blockInternal\` is true at runtime.
183
+ RUN echo "${encoded}" | base64 -d > ${TYPECLAW_ENTRYPOINT_PATH} \\
184
+ && chmod +x ${TYPECLAW_ENTRYPOINT_PATH}`
185
+ }
186
+
38
187
  // Shared-library runtime deps Chrome for Testing needs to launch on amd64
39
188
  // Debian trixie (base of `oven/bun:1-slim`). `agent-browser install
40
189
  // --with-deps` (v0.27.0) is supposed to install these but silently no-ops:
@@ -90,10 +239,19 @@ const APT_FEATURES: Record<'ffmpeg' | 'gh' | 'tmux' | 'python', AptFeature> = {
90
239
  },
91
240
  }
92
241
 
93
- export function buildDockerfile(config: DockerfileConfig = defaultConfig()): string {
94
- const aptArgs = collectAptArgs(config)
242
+ export function buildDockerfile(
243
+ config: DockerfileConfig = defaultConfig(),
244
+ options: BuildDockerfileOptions = {},
245
+ ): string {
246
+ const toggleAptArgs = collectToggleAptArgs(config)
95
247
  const ghKeyringLayer = renderGhKeyringLayer(config.gh)
96
248
  const customLines = renderCustomDockerfileLines(config.append)
249
+ const baseImageVersion = options.baseImageVersion ?? null
250
+
251
+ const fromAndHeavyLayers =
252
+ baseImageVersion !== null
253
+ ? renderVersionedHead(baseImageVersion, ghKeyringLayer, toggleAptArgs)
254
+ : renderInlineHead(ghKeyringLayer, toggleAptArgs)
97
255
 
98
256
  return `${BUILDKIT_HEADER}
99
257
  # AUTOGENERATED by typeclaw — do not edit.
@@ -101,7 +259,60 @@ export function buildDockerfile(config: DockerfileConfig = defaultConfig()): str
101
259
  # in the typeclaw repo. Local edits will be overwritten (and committed away if
102
260
  # the working tree is dirty). To change the template, edit dockerfile.ts there.
103
261
 
104
- ${FROM_AND_WORKDIR}
262
+ ${fromAndHeavyLayers}
263
+ # The agent folder (including node_modules) is bind-mounted at runtime by
264
+ # \`typeclaw start\`, so we do not COPY or install here. This keeps the image
265
+ # tiny and lets edits on the host take effect without rebuilds.
266
+
267
+ ENV NODE_ENV=production
268
+
269
+ # Keep agent-messenger's fallback config dir inside workspace/ for any future
270
+ # SDK fallback paths. TypeClaw's KakaoTalk adapter does not write there:
271
+ # credentials live in secrets.json#channels.kakaotalk and container writes go
272
+ # through hostd's secrets-patch RPC.
273
+ ENV AGENT_MESSENGER_CONFIG_DIR=/agent/workspace/.agent-messenger
274
+
275
+ ${customLines}ENTRYPOINT ["${TYPECLAW_ENTRYPOINT_PATH}"]
276
+ CMD ["run"]
277
+ `
278
+ }
279
+
280
+ // FROMs the prebuilt typeclaw-base image at the pinned version. Heavy
281
+ // layers (apt baseline, Chrome runtime libs, curl-impersonate, agent-browser,
282
+ // Chrome for Testing) are already in that image, so the per-agent head only
283
+ // re-runs the toggle apt install and (optionally) the gh keyring bootstrap.
284
+ //
285
+ // The entrypoint shim is ALSO re-emitted here, even though the base image
286
+ // already carries it. Two reasons: (1) older base images published before
287
+ // the shim landed (or before a shim source edit) don't have the up-to-date
288
+ // binary at TYPECLAW_ENTRYPOINT_PATH, and the per-agent ENTRYPOINT line
289
+ // would crash on startup with `stat: no such file or directory`. Re-emitting
290
+ // is ~1KB of image and keeps the contract local: whatever per-agent
291
+ // Dockerfile we emit guarantees the shim path exists, regardless of which
292
+ // base-image vintage we FROM. (2) Edits to `buildEntrypointShim()` ship via
293
+ // npm + `typeclaw start --build` immediately, instead of being blocked on a
294
+ // fresh base-image release. The base image's copy is harmlessly overwritten
295
+ // by this RUN — same path, same chmod.
296
+ function renderVersionedHead(baseImageVersion: string, ghKeyringLayer: string, toggleAptArgs: string[]): string {
297
+ const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs)}\n\n`
298
+ return `FROM ${GHCR_BASE_IMAGE_REPO}:${baseImageVersion}
299
+
300
+ WORKDIR /agent
301
+
302
+ ARG TARGETARCH
303
+
304
+ ${ghKeyringLayer}${toggleAptLayer}${renderEntrypointShimLayer()}
305
+
306
+ `
307
+ }
308
+
309
+ // FROMs oven/bun:1-slim and rebuilds the full heavy stack inline. Used by
310
+ // dev-mode runs (typeclaw installed via file: / link: spec) where the
311
+ // matching :version GHCR tag does not yet exist, and by the test suite to
312
+ // keep coverage of the full-stack layers independent of GHCR availability.
313
+ function renderInlineHead(ghKeyringLayer: string, toggleAptArgs: string[]): string {
314
+ const baselineAndToggleArgs = [...BASELINE_APT_PACKAGES, ...toggleAptArgs]
315
+ return `${FROM_AND_WORKDIR}
105
316
 
106
317
  # Layers are ordered most-stable first to maximize Docker layer cache hits on
107
318
  # rebuilds. Anything that pulls from npm (volatile) sits below anything that
@@ -125,7 +336,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
125
336
  --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
126
337
  apt-get update \\
127
338
  && apt-get install -y --no-install-recommends \\
128
- ${aptArgs.join(' ')} \\
339
+ ${baselineAndToggleArgs.join(' ')} \\
129
340
  && if [ "$TARGETARCH" = "arm64" ]; then \\
130
341
  apt-get install -y --no-install-recommends chromium; \\
131
342
  else \\
@@ -141,26 +352,22 @@ ${LAYER_4_AGENT_BROWSER_INSTALL}
141
352
 
142
353
  ${LAYER_5_CHROME_FOR_TESTING}
143
354
 
144
- # The agent folder (including node_modules) is bind-mounted at runtime by
145
- # \`typeclaw start\`, so we do not COPY or install here. This keeps the image
146
- # tiny and lets edits on the host take effect without rebuilds.
147
-
148
- ENV NODE_ENV=production
355
+ ${renderEntrypointShimLayer()}
149
356
 
150
- # Pin agent-messenger's config dir into the agent's workspace/ so KakaoTalk
151
- # (and any future agent-messenger-backed channel) reads/writes credentials
152
- # inside the bind-mounted agent folder. Without this, the SDK would default
153
- # to /root/.config/agent-messenger inside the container, which doesn't
154
- # survive container restarts and isn't visible from the host. The agent
155
- # folder's bind-mount maps /agent → host's agent dir, so the credentials
156
- # end up at <agentDir>/workspace/.agent-messenger/ on the host.
157
- ENV AGENT_MESSENGER_CONFIG_DIR=/agent/workspace/.agent-messenger
158
-
159
- ${customLines}ENTRYPOINT ["bun", "run", "typeclaw"]
160
- CMD ["run"]
161
357
  `
162
358
  }
163
359
 
360
+ function renderToggleAptInstallLayer(toggleAptArgs: string[]): string {
361
+ return `# Layer 1 (toggle apt install): packages requested via typeclaw.json
362
+ # #dockerfile toggles. Baseline + Chrome runtime libs are already in the
363
+ # base image; this layer only adds gh/tmux/python/ffmpeg if enabled.
364
+ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
365
+ --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
366
+ apt-get update \\
367
+ && apt-get install -y --no-install-recommends \\
368
+ ${toggleAptArgs.join(' ')}`
369
+ }
370
+
164
371
  // Recipe for the prebuilt typeclaw-base image published to
165
372
  // ghcr.io/typeclaw/typeclaw-base by .github/workflows/base-image.yml. Built
166
373
  // from the same constants and layer templates as buildDockerfile() so the
@@ -205,6 +412,8 @@ ${LAYER_3_AGENT_BROWSER_ARM64_CONFIG}
205
412
  ${LAYER_4_AGENT_BROWSER_INSTALL}
206
413
 
207
414
  ${LAYER_5_CHROME_FOR_TESTING}
415
+
416
+ ${renderEntrypointShimLayer()}
208
417
  `
209
418
  }
210
419
 
@@ -280,8 +489,8 @@ function defaultConfig(): DockerfileConfig {
280
489
  return { ffmpeg: false, gh: true, python: true, tmux: true, append: [] }
281
490
  }
282
491
 
283
- function collectAptArgs(config: DockerfileConfig): string[] {
284
- const args: string[] = [...BASELINE_APT_PACKAGES]
492
+ function collectToggleAptArgs(config: DockerfileConfig): string[] {
493
+ const args: string[] = []
285
494
  for (const key of ['ffmpeg', 'gh', 'python', 'tmux'] as const) {
286
495
  args.push(...APT_FEATURES[key].toAptArgs(config[key]))
287
496
  }