typeclaw 0.3.1 → 0.5.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/README.md +20 -15
- package/auth.schema.json +113 -0
- package/package.json +1 -1
- package/secrets.schema.json +113 -0
- package/src/agent/auth.ts +4 -2
- package/src/agent/index.ts +16 -28
- package/src/agent/model-fallback.ts +127 -0
- package/src/agent/session-meta.ts +1 -1
- package/src/agent/session-origin.ts +3 -2
- package/src/agent/tools/curl-impersonate.ts +300 -0
- package/src/agent/tools/ddg.ts +13 -88
- package/src/agent/tools/webfetch/fetch.ts +105 -2
- package/src/agent/tools/webfetch/tool.ts +4 -0
- package/src/bundled-plugins/agent-browser/shim.ts +47 -0
- package/src/bundled-plugins/backup/subagents.ts +2 -0
- package/src/bundled-plugins/memory/README.md +49 -12
- package/src/bundled-plugins/memory/citation-superset.ts +63 -0
- package/src/bundled-plugins/memory/dreaming.ts +105 -17
- package/src/bundled-plugins/memory/index.ts +2 -2
- package/src/bundled-plugins/memory/memory-logger.ts +45 -26
- package/src/bundled-plugins/memory/strength.ts +127 -0
- package/src/bundled-plugins/memory/topics.ts +75 -0
- package/src/bundled-plugins/security/index.ts +88 -43
- package/src/bundled-plugins/security/permissions.ts +36 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +20 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +12 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +23 -3
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +7 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +6 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +9 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +6 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +7 -0
- package/src/channels/adapters/github/auth-app.ts +120 -0
- package/src/channels/adapters/github/auth-pat.ts +50 -0
- package/src/channels/adapters/github/auth.ts +33 -0
- package/src/channels/adapters/github/channel-resolver.ts +30 -0
- package/src/channels/adapters/github/dedup.ts +26 -0
- package/src/channels/adapters/github/event-allowlist.ts +8 -0
- package/src/channels/adapters/github/fetch-attachment.ts +5 -0
- package/src/channels/adapters/github/history.ts +63 -0
- package/src/channels/adapters/github/inbound.ts +286 -0
- package/src/channels/adapters/github/index.ts +370 -0
- package/src/channels/adapters/github/managed-path.ts +54 -0
- package/src/channels/adapters/github/membership.ts +35 -0
- package/src/channels/adapters/github/outbound.ts +145 -0
- package/src/channels/adapters/github/webhook-register.ts +349 -0
- package/src/channels/manager.ts +94 -9
- package/src/channels/router.ts +194 -28
- package/src/channels/schema.ts +31 -1
- package/src/channels/tunnel-bridge.ts +51 -0
- package/src/channels/types.ts +3 -1
- package/src/cli/builtins.ts +28 -0
- package/src/cli/channel.ts +511 -25
- package/src/cli/container-command-client.ts +244 -0
- package/src/cli/cron.ts +173 -0
- package/src/cli/host-command-runner.ts +150 -0
- package/src/cli/index.ts +42 -1
- package/src/cli/init.ts +400 -67
- package/src/cli/model.ts +14 -4
- package/src/cli/oauth-callbacks.ts +49 -0
- package/src/cli/plugin-command-help.ts +49 -0
- package/src/cli/plugin-commands-dispatch.ts +112 -0
- package/src/cli/plugin-commands.ts +118 -0
- package/src/cli/provider.ts +3 -20
- package/src/cli/tui.ts +10 -2
- package/src/cli/tunnel.ts +533 -0
- package/src/cli/ui.ts +8 -3
- package/src/config/config.ts +134 -24
- package/src/config/models-mutation.ts +42 -8
- package/src/config/providers-mutation.ts +12 -8
- package/src/container/start.ts +48 -4
- package/src/cron/bridge.ts +136 -0
- package/src/cron/consumer.ts +174 -48
- package/src/cron/index.ts +19 -2
- package/src/cron/list.ts +105 -0
- package/src/cron/scheduler.ts +12 -3
- package/src/cron/schema.ts +11 -3
- package/src/doctor/checks.ts +0 -50
- package/src/init/dockerfile.ts +165 -13
- package/src/init/ensure-deps.ts +15 -4
- package/src/init/github-webhook-install.ts +109 -0
- package/src/init/hatching.ts +2 -2
- package/src/init/index.ts +519 -12
- package/src/init/oauth-login.ts +17 -3
- package/src/init/run-bun-install.ts +17 -3
- package/src/init/run-owner-claim.ts +11 -2
- package/src/permissions/builtins.ts +29 -2
- package/src/permissions/match-rule.ts +24 -2
- package/src/permissions/permissions.ts +24 -7
- package/src/permissions/resolve.ts +1 -0
- package/src/plugin/define.ts +44 -1
- package/src/plugin/index.ts +18 -3
- package/src/plugin/manager.ts +16 -0
- package/src/plugin/registry.ts +85 -3
- package/src/plugin/types.ts +144 -1
- package/src/plugin/zod-introspect.ts +100 -0
- package/src/role-claim/match-rule.ts +2 -1
- package/src/run/index.ts +112 -4
- package/src/secrets/index.ts +1 -1
- package/src/secrets/schema.ts +21 -0
- package/src/server/command-runner.ts +476 -0
- package/src/server/index.ts +388 -5
- package/src/shared/index.ts +8 -0
- package/src/shared/protocol.ts +80 -1
- package/src/skills/typeclaw-channel-github/SKILL.md +24 -0
- package/src/skills/typeclaw-config/SKILL.md +27 -26
- package/src/skills/typeclaw-cron/SKILL.md +234 -3
- package/src/skills/typeclaw-memory/SKILL.md +25 -15
- package/src/skills/typeclaw-monorepo/SKILL.md +2 -2
- package/src/skills/typeclaw-permissions/SKILL.md +35 -16
- package/src/skills/typeclaw-plugins/SKILL.md +251 -5
- package/src/skills/typeclaw-tunnels/SKILL.md +111 -0
- package/src/test-helpers/wait-for.ts +50 -0
- package/src/tui/index.ts +70 -7
- package/src/tunnels/__fixtures__/cloudflared-quick-stderr.txt +11 -0
- package/src/tunnels/events.ts +14 -0
- package/src/tunnels/index.ts +12 -0
- package/src/tunnels/log-ring.ts +54 -0
- package/src/tunnels/manager.ts +139 -0
- package/src/tunnels/providers/cloudflare-quick.ts +189 -0
- package/src/tunnels/providers/external.ts +53 -0
- package/src/tunnels/quick-url-parser.ts +5 -0
- package/src/tunnels/types.ts +43 -0
- package/src/usage/report.ts +15 -12
- package/typeclaw.schema.json +311 -26
package/src/init/dockerfile.ts
CHANGED
|
@@ -27,6 +27,12 @@ export type BuildDockerfileOptions = {
|
|
|
27
27
|
// `util-linux` carries `setpriv`, which the shim uses to drop CAP_NET_ADMIN
|
|
28
28
|
// from the bounding set before exec'ing the agent. Listed first in the
|
|
29
29
|
// apt-get install line so the package set is self-documenting at a glance.
|
|
30
|
+
//
|
|
31
|
+
// xvfb is intentionally NOT in baseline — it's a toggle (`xvfb: true` by
|
|
32
|
+
// default, opt-out via `docker.file.xvfb: false`) because the shim
|
|
33
|
+
// self-heals: it spawns Xvfb (and exports DISPLAY) if the binary is on
|
|
34
|
+
// PATH, and execs the agent directly otherwise. See APT_FEATURES.xvfb
|
|
35
|
+
// below and `buildEntrypointShim`.
|
|
30
36
|
const BASELINE_APT_PACKAGES = ['git', 'ca-certificates', 'curl', 'gnupg', 'iptables', 'util-linux'] as const
|
|
31
37
|
|
|
32
38
|
// curl-impersonate is the only currently-working way to query DuckDuckGo from
|
|
@@ -56,6 +62,19 @@ export const CURL_IMPERSONATE_SHA256_ARM64 = '6766bc67fd3e8e2313875f32b36b5a3fab
|
|
|
56
62
|
// the impersonation to whatever `curl_chrome` resolves to.
|
|
57
63
|
export const CURL_IMPERSONATE_PROFILE = 'chrome136'
|
|
58
64
|
|
|
65
|
+
// cloudflared powers `cloudflare-quick` tunnels. Pinned-version + per-arch
|
|
66
|
+
// SHA256 mirrors the curl-impersonate pattern above: bumping requires updating
|
|
67
|
+
// all three constants in the same commit, and the build fails loudly at
|
|
68
|
+
// `sha256sum -c` if either hash is wrong for the version. To bump: pick a
|
|
69
|
+
// release from https://github.com/cloudflare/cloudflared/releases, then
|
|
70
|
+
// curl -fsSLO .../cloudflared-linux-amd64 && shasum -a 256 cloudflared-linux-amd64
|
|
71
|
+
// for each architecture. The version literal is the release tag exactly as it
|
|
72
|
+
// appears on GitHub (no `v` prefix).
|
|
73
|
+
export const CLOUDFLARED_VERSION = '2025.5.0'
|
|
74
|
+
export const CLOUDFLARED_SHA256_AMD64 = 'a62266fd02041374f1fca0d85694aafdf7e26e171a314467356b471d4ebb2393'
|
|
75
|
+
export const CLOUDFLARED_SHA256_ARM64 = '47e55e6eba2755239f641c2c4f89878643ac0d9eaa127a6c84a2cb43fa2e0f03'
|
|
76
|
+
export const CLOUDFLARED_RELEASE_URL_BASE = 'https://github.com/cloudflare/cloudflared/releases/download'
|
|
77
|
+
|
|
59
78
|
export const TYPECLAW_ENTRYPOINT_PATH = '/usr/local/bin/typeclaw-entrypoint'
|
|
60
79
|
|
|
61
80
|
// IPv4 networks the container is forbidden to egress to when
|
|
@@ -206,7 +225,96 @@ export function buildEntrypointShim(): string {
|
|
|
206
225
|
# Source: src/init/dockerfile.ts \`buildEntrypointShim()\`.
|
|
207
226
|
set -eu
|
|
208
227
|
|
|
228
|
+
# start_xvfb launches Xvfb in the background under a stripped capability
|
|
229
|
+
# bounding set so headed Chrome (agent-browser --headed, Playwright
|
|
230
|
+
# headful) has a real X11 display to connect to. Headless containers
|
|
231
|
+
# have no display server; Chrome --headless / --headless=new is
|
|
232
|
+
# fingerprinted by modern bot detection (Akamai / Cloudflare BM)
|
|
233
|
+
# regardless of UA spoof, so real headed Chrome under a virtual
|
|
234
|
+
# framebuffer is the only path to a passing sensor score from a
|
|
235
|
+
# server-side container.
|
|
236
|
+
#
|
|
237
|
+
# Two correctness invariants this function enforces:
|
|
238
|
+
#
|
|
239
|
+
# 1. Xvfb never holds CAP_NET_ADMIN. The shim runs as PID 1 with the
|
|
240
|
+
# container's full capability set (including NET_ADMIN when
|
|
241
|
+
# network.blockInternal=true). If we backgrounded Xvfb naked, it
|
|
242
|
+
# would inherit NET_ADMIN and keep it for the container's lifetime
|
|
243
|
+
# — defeating the capability-drop contract that setpriv applies to
|
|
244
|
+
# the agent process. Routing Xvfb through the same setpriv invocation
|
|
245
|
+
# we use for the agent strips NET_ADMIN before Xvfb's first exec.
|
|
246
|
+
# On the off-path (blockInternal=false) the bounding-set drop is a
|
|
247
|
+
# no-op (NET_ADMIN was never granted), but the call is harmless.
|
|
248
|
+
#
|
|
249
|
+
# 2. Xvfb startup failure is loud, not silent. \`Xvfb ... >/dev/null &\`
|
|
250
|
+
# under \`set -e\` does not fail the script if Xvfb exits immediately
|
|
251
|
+
# (missing library, port conflict, malformed args). Without the
|
|
252
|
+
# explicit liveness probe below, the shim would then export DISPLAY
|
|
253
|
+
# and exec bun, agent-browser launches would die with "cannot open
|
|
254
|
+
# display", and the operator would chase a phantom bug. We capture
|
|
255
|
+
# $! and \`kill -0\` it on every poll iteration so an early exit
|
|
256
|
+
# becomes a clear stderr line and a non-zero shim exit.
|
|
257
|
+
#
|
|
258
|
+
# We DO NOT use \`xvfb-run\`. xvfb-run hangs forever when it runs as
|
|
259
|
+
# PID 1 inside a container: its SIGUSR1-based ready handshake races
|
|
260
|
+
# and stalls because PID 1 ignores signals without explicit handlers,
|
|
261
|
+
# so the \`trap : USR1 ; wait || :\` dance never wakes up. Observed in
|
|
262
|
+
# practice: container alive, Xvfb running, PID 1 stuck in
|
|
263
|
+
# \`rt_sigsuspend\`, no agent process ever spawns, \`docker logs\` empty.
|
|
264
|
+
# Documented industry workarounds are tini-as-PID-1 or direct Xvfb
|
|
265
|
+
# spawn; we pick the latter (no new dep).
|
|
266
|
+
#
|
|
267
|
+
# Xvfb args:
|
|
268
|
+
# :99 fixed display number. Filesystem
|
|
269
|
+
# (/tmp/.X11-unix/X99) and abstract
|
|
270
|
+
# (\\0/tmp/.X11-unix/X99) sockets are both
|
|
271
|
+
# network-namespace-scoped, so :99 is safe
|
|
272
|
+
# across all Compose'd containers.
|
|
273
|
+
# -screen 0 1920x1080x24 desktop viewport agent-browser advertises;
|
|
274
|
+
# mismatched geometry is itself a fingerprint
|
|
275
|
+
# signal.
|
|
276
|
+
# -ac disable host-based X access control so
|
|
277
|
+
# Chrome connects without XAUTHORITY plumbing.
|
|
278
|
+
# +extension RANDR expose the RandR extension; Chrome queries
|
|
279
|
+
# it for screen geometry, and without it
|
|
280
|
+
# \`screen.*\` values come back inconsistent.
|
|
281
|
+
# -nolisten tcp refuse TCP connections (Unix socket only).
|
|
282
|
+
# Defense-in-depth — we are in a netns with
|
|
283
|
+
# no inbound exposure anyway.
|
|
284
|
+
start_xvfb() {
|
|
285
|
+
if ! command -v Xvfb >/dev/null 2>&1; then
|
|
286
|
+
return 0
|
|
287
|
+
fi
|
|
288
|
+
setpriv --bounding-set -net_admin --inh-caps -net_admin --ambient-caps -net_admin \\
|
|
289
|
+
-- Xvfb :99 -screen 0 1920x1080x24 -ac +extension RANDR -nolisten tcp \\
|
|
290
|
+
>/dev/null 2>&1 &
|
|
291
|
+
xvfb_pid=$!
|
|
292
|
+
export DISPLAY=:99
|
|
293
|
+
# Poll the socket every 10ms up to ~3s. Xvfb cold start is typically
|
|
294
|
+
# ~20-50ms on a modern host; 3s covers slow Docker Desktop VMs,
|
|
295
|
+
# Rosetta/QEMU emulation, and loaded CI runners. We also \`kill -0\`
|
|
296
|
+
# the pid each iteration so an Xvfb that died immediately surfaces
|
|
297
|
+
# as a clear error instead of a 3-second hang followed by silent
|
|
298
|
+
# "cannot open display" downstream.
|
|
299
|
+
i=0
|
|
300
|
+
while [ $i -lt 300 ]; do
|
|
301
|
+
if [ -S /tmp/.X11-unix/X99 ]; then
|
|
302
|
+
unset i xvfb_pid
|
|
303
|
+
return 0
|
|
304
|
+
fi
|
|
305
|
+
if ! kill -0 "$xvfb_pid" 2>/dev/null; then
|
|
306
|
+
echo "typeclaw-entrypoint: Xvfb exited immediately; cannot start headed display (docker.file.xvfb=true)" >&2
|
|
307
|
+
exit 1
|
|
308
|
+
fi
|
|
309
|
+
sleep 0.01
|
|
310
|
+
i=$((i + 1))
|
|
311
|
+
done
|
|
312
|
+
echo "typeclaw-entrypoint: Xvfb did not create /tmp/.X11-unix/X99 within 3s; refusing to continue (docker.file.xvfb=true)" >&2
|
|
313
|
+
exit 1
|
|
314
|
+
}
|
|
315
|
+
|
|
209
316
|
if [ "\${TYPECLAW_NETWORK_BLOCK_INTERNAL:-0}" != "1" ]; then
|
|
317
|
+
start_xvfb
|
|
210
318
|
exec bun run typeclaw "$@"
|
|
211
319
|
fi
|
|
212
320
|
|
|
@@ -251,6 +359,7 @@ ip6tables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
|
251
359
|
ip6tables -A OUTPUT -o lo -j ACCEPT
|
|
252
360
|
${ipv6Rules.join('\n')}
|
|
253
361
|
|
|
362
|
+
start_xvfb
|
|
254
363
|
exec setpriv --bounding-set -net_admin --inh-caps -net_admin --ambient-caps -net_admin -- bun run typeclaw "$@"
|
|
255
364
|
`
|
|
256
365
|
}
|
|
@@ -283,9 +392,11 @@ RUN echo "${encoded}" | base64 -d > ${TYPECLAW_ENTRYPOINT_PATH} \\
|
|
|
283
392
|
// names are the trixie-renamed variants from the 64-bit time_t ABI
|
|
284
393
|
// transition; SONAMEs (libglib-2.0.so.0 etc.) are unchanged. Packages
|
|
285
394
|
// without t64 here have no t64 sibling on trixie — verified against
|
|
286
|
-
// packages.debian.org/trixie. Fonts are intentionally omitted
|
|
287
|
-
//
|
|
288
|
-
//
|
|
395
|
+
// packages.debian.org/trixie. Fonts are intentionally omitted from this
|
|
396
|
+
// list: the failure these packages address is launch-time linker errors,
|
|
397
|
+
// not rendering glyphs. CJK glyph rendering is a separate concern handled
|
|
398
|
+
// by the `cjkFonts` toggle (see CJK_FONTS_PACKAGE / APT_FEATURES below),
|
|
399
|
+
// which layers `fonts-noto-cjk` on top via the toggle apt install path.
|
|
289
400
|
export const CHROME_RUNTIME_APT_PACKAGES_AMD64 = [
|
|
290
401
|
'libasound2t64',
|
|
291
402
|
'libatk-bridge2.0-0t64',
|
|
@@ -310,17 +421,27 @@ export const CHROME_RUNTIME_APT_PACKAGES_AMD64 = [
|
|
|
310
421
|
'libxrandr2',
|
|
311
422
|
] as const
|
|
312
423
|
|
|
424
|
+
// `fonts-noto-cjk` provides CJK glyphs for Chromium-rendered output
|
|
425
|
+
// (screenshots, page.pdf()). Without it CJK text in agent-browser output
|
|
426
|
+
// renders as `.notdef` tofu boxes. Treated as a toggle apt package (like
|
|
427
|
+
// gh/tmux) rather than a base-image staple so users with `cjkFonts: false`
|
|
428
|
+
// genuinely skip the ~56MB layer; baking into the base image would force
|
|
429
|
+
// every GHCR-base user to ship the fonts regardless of their opt-out.
|
|
430
|
+
export const CJK_FONTS_PACKAGE = 'fonts-noto-cjk'
|
|
431
|
+
|
|
313
432
|
type AptFeature = {
|
|
314
433
|
toAptArgs: (toggle: DockerfileFeatureToggle) => string[]
|
|
315
434
|
}
|
|
316
435
|
|
|
317
|
-
const APT_FEATURES: Record<'ffmpeg' | 'gh' | 'tmux' | 'python', AptFeature> = {
|
|
436
|
+
const APT_FEATURES: Record<'ffmpeg' | 'gh' | 'tmux' | 'python' | 'cjkFonts' | 'xvfb', AptFeature> = {
|
|
318
437
|
ffmpeg: { toAptArgs: (v) => singlePackageArgs('ffmpeg', v) },
|
|
319
438
|
gh: { toAptArgs: (v) => singlePackageArgs('gh', v) },
|
|
320
439
|
tmux: { toAptArgs: (v) => singlePackageArgs('tmux', v) },
|
|
321
440
|
python: {
|
|
322
441
|
toAptArgs: (v) => (v === true ? ['python3', 'python3-pip', 'python3-venv', 'python-is-python3'] : []),
|
|
323
442
|
},
|
|
443
|
+
cjkFonts: { toAptArgs: (v) => (v === true ? [CJK_FONTS_PACKAGE] : []) },
|
|
444
|
+
xvfb: { toAptArgs: (v) => (v === true ? ['xvfb'] : []) },
|
|
324
445
|
}
|
|
325
446
|
|
|
326
447
|
export function buildDockerfile(
|
|
@@ -329,13 +450,14 @@ export function buildDockerfile(
|
|
|
329
450
|
): string {
|
|
330
451
|
const toggleAptArgs = collectToggleAptArgs(config)
|
|
331
452
|
const ghKeyringLayer = renderGhKeyringLayer(config.gh)
|
|
453
|
+
const cloudflaredLayer = renderCloudflaredLayer(config.cloudflared)
|
|
332
454
|
const customLines = renderCustomDockerfileLines(config.append)
|
|
333
455
|
const baseImageVersion = options.baseImageVersion ?? null
|
|
334
456
|
|
|
335
457
|
const fromAndHeavyLayers =
|
|
336
458
|
baseImageVersion !== null
|
|
337
|
-
? renderVersionedHead(baseImageVersion, ghKeyringLayer, toggleAptArgs)
|
|
338
|
-
: renderInlineHead(ghKeyringLayer, toggleAptArgs)
|
|
459
|
+
? renderVersionedHead(baseImageVersion, ghKeyringLayer, toggleAptArgs, cloudflaredLayer)
|
|
460
|
+
: renderInlineHead(ghKeyringLayer, toggleAptArgs, cloudflaredLayer)
|
|
339
461
|
|
|
340
462
|
return `${BUILDKIT_HEADER}
|
|
341
463
|
# AUTOGENERATED by typeclaw — do not edit.
|
|
@@ -377,7 +499,12 @@ CMD ["run"]
|
|
|
377
499
|
// npm + `typeclaw start --build` immediately, instead of being blocked on a
|
|
378
500
|
// fresh base-image release. The base image's copy is harmlessly overwritten
|
|
379
501
|
// by this RUN — same path, same chmod.
|
|
380
|
-
function renderVersionedHead(
|
|
502
|
+
function renderVersionedHead(
|
|
503
|
+
baseImageVersion: string,
|
|
504
|
+
ghKeyringLayer: string,
|
|
505
|
+
toggleAptArgs: string[],
|
|
506
|
+
cloudflaredLayer: string,
|
|
507
|
+
): string {
|
|
381
508
|
const toggleAptLayer = toggleAptArgs.length === 0 ? '' : `${renderToggleAptInstallLayer(toggleAptArgs)}\n\n`
|
|
382
509
|
return `FROM ${GHCR_BASE_IMAGE_REPO}:${baseImageVersion}
|
|
383
510
|
|
|
@@ -385,7 +512,7 @@ WORKDIR /agent
|
|
|
385
512
|
|
|
386
513
|
ARG TARGETARCH
|
|
387
514
|
|
|
388
|
-
${ghKeyringLayer}${toggleAptLayer}${renderEntrypointShimLayer()}
|
|
515
|
+
${ghKeyringLayer}${toggleAptLayer}${cloudflaredLayer}${renderEntrypointShimLayer()}
|
|
389
516
|
|
|
390
517
|
`
|
|
391
518
|
}
|
|
@@ -394,7 +521,7 @@ ${ghKeyringLayer}${toggleAptLayer}${renderEntrypointShimLayer()}
|
|
|
394
521
|
// dev-mode runs (typeclaw installed via file: / link: spec) where the
|
|
395
522
|
// matching :version GHCR tag does not yet exist, and by the test suite to
|
|
396
523
|
// keep coverage of the full-stack layers independent of GHCR availability.
|
|
397
|
-
function renderInlineHead(ghKeyringLayer: string, toggleAptArgs: string[]): string {
|
|
524
|
+
function renderInlineHead(ghKeyringLayer: string, toggleAptArgs: string[], cloudflaredLayer: string): string {
|
|
398
525
|
const baselineAndToggleArgs = [...BASELINE_APT_PACKAGES, ...toggleAptArgs]
|
|
399
526
|
return `${FROM_AND_WORKDIR}
|
|
400
527
|
|
|
@@ -436,7 +563,23 @@ ${LAYER_4_AGENT_BROWSER_INSTALL}
|
|
|
436
563
|
|
|
437
564
|
${LAYER_5_CHROME_FOR_TESTING}
|
|
438
565
|
|
|
439
|
-
${renderEntrypointShimLayer()}
|
|
566
|
+
${cloudflaredLayer}${renderEntrypointShimLayer()}
|
|
567
|
+
|
|
568
|
+
`
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function renderCloudflaredLayer(enabled: boolean): string {
|
|
572
|
+
if (!enabled) return ''
|
|
573
|
+
return `# Layer 5.5 (optional): pinned cloudflared for cloudflare-quick tunnels.
|
|
574
|
+
RUN ARCH_BIN="$(if [ "$TARGETARCH" = "arm64" ]; then echo arm64; else echo amd64; fi)" \
|
|
575
|
+
&& ARCH_SHA="$(if [ "$TARGETARCH" = "arm64" ]; then echo ${CLOUDFLARED_SHA256_ARM64}; else echo ${CLOUDFLARED_SHA256_AMD64}; fi)" \
|
|
576
|
+
&& cd /tmp \
|
|
577
|
+
&& curl -fsSL -o cloudflared \
|
|
578
|
+
"${CLOUDFLARED_RELEASE_URL_BASE}/${CLOUDFLARED_VERSION}/cloudflared-linux-\${ARCH_BIN}" \
|
|
579
|
+
&& echo "\${ARCH_SHA} cloudflared" | sha256sum -c - \
|
|
580
|
+
&& chmod +x cloudflared \
|
|
581
|
+
&& mv cloudflared /usr/local/bin/cloudflared \
|
|
582
|
+
&& /usr/local/bin/cloudflared --version > /dev/null
|
|
440
583
|
|
|
441
584
|
`
|
|
442
585
|
}
|
|
@@ -444,7 +587,7 @@ ${renderEntrypointShimLayer()}
|
|
|
444
587
|
function renderToggleAptInstallLayer(toggleAptArgs: string[]): string {
|
|
445
588
|
return `# Layer 1 (toggle apt install): packages requested via typeclaw.json
|
|
446
589
|
# #docker.file toggles. Baseline + Chrome runtime libs are already in the
|
|
447
|
-
# base image; this layer only adds gh/tmux/python/ffmpeg if enabled.
|
|
590
|
+
# base image; this layer only adds gh/tmux/python/ffmpeg/cjkFonts if enabled.
|
|
448
591
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
449
592
|
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \\
|
|
450
593
|
apt-get update \\
|
|
@@ -570,12 +713,21 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
|
|
|
570
713
|
fi`
|
|
571
714
|
|
|
572
715
|
function defaultConfig(): DockerfileConfig {
|
|
573
|
-
return {
|
|
716
|
+
return {
|
|
717
|
+
ffmpeg: false,
|
|
718
|
+
gh: true,
|
|
719
|
+
python: true,
|
|
720
|
+
tmux: true,
|
|
721
|
+
cjkFonts: true,
|
|
722
|
+
cloudflared: true,
|
|
723
|
+
xvfb: true,
|
|
724
|
+
append: [],
|
|
725
|
+
}
|
|
574
726
|
}
|
|
575
727
|
|
|
576
728
|
function collectToggleAptArgs(config: DockerfileConfig): string[] {
|
|
577
729
|
const args: string[] = []
|
|
578
|
-
for (const key of ['ffmpeg', 'gh', 'python', 'tmux'] as const) {
|
|
730
|
+
for (const key of ['ffmpeg', 'gh', 'python', 'tmux', 'cjkFonts', 'xvfb'] as const) {
|
|
579
731
|
args.push(...APT_FEATURES[key].toAptArgs(config[key]))
|
|
580
732
|
}
|
|
581
733
|
return args
|
package/src/init/ensure-deps.ts
CHANGED
|
@@ -15,24 +15,35 @@ export type EnsureDepsOptions = {
|
|
|
15
15
|
cwd: string
|
|
16
16
|
install?: InstallRunner
|
|
17
17
|
detect?: (cwd: string) => Promise<readonly string[]>
|
|
18
|
+
// Skip the pre-install drift detection and run `bun install --force`
|
|
19
|
+
// unconditionally. Used by `typeclaw start --build` when the agent declares
|
|
20
|
+
// typeclaw via `file:` or `link:`: bun's file-dep cache otherwise treats
|
|
21
|
+
// unchanged name+version as a cache hit even after the source on disk has
|
|
22
|
+
// changed, so the post-install re-detect at the bottom (the silent-no-op
|
|
23
|
+
// guard) stays — `--force` does NOT excuse us from verifying the install
|
|
24
|
+
// actually populated the agent's node_modules/.
|
|
25
|
+
force?: boolean
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export async function ensureDepsInstalled(options: EnsureDepsOptions): Promise<EnsureDepsResult> {
|
|
21
29
|
const { cwd } = options
|
|
22
30
|
const install = options.install ?? runBunInstall
|
|
23
31
|
const detect = options.detect ?? detectMissingDeps
|
|
32
|
+
const force = options.force ?? false
|
|
24
33
|
|
|
25
|
-
const missing = await detect(cwd)
|
|
26
|
-
if (missing.length === 0) return { ok: true, installed: false }
|
|
34
|
+
const missing = force ? [] : await detect(cwd)
|
|
35
|
+
if (!force && missing.length === 0) return { ok: true, installed: false }
|
|
27
36
|
|
|
28
|
-
const result = await install(cwd)
|
|
37
|
+
const result = await install(cwd, { force })
|
|
29
38
|
if (!result.ok) return { ok: false, reason: result.reason, missing }
|
|
30
39
|
|
|
31
40
|
// Re-probe: `bun install` returns 0 even when a file:-linked dep's own
|
|
32
41
|
// package.json is unreachable (it silently no-ops on the target). Without
|
|
33
42
|
// this check, we'd proceed to `docker run` with a known-broken
|
|
34
43
|
// node_modules/ and the agent would crash with a confusing in-container
|
|
35
|
-
// `Cannot find package 'x'`.
|
|
44
|
+
// `Cannot find package 'x'`. This guard remains under `force` too —
|
|
45
|
+
// --force bypasses the cache but does not guarantee the install actually
|
|
46
|
+
// landed every declared dep.
|
|
36
47
|
const stillMissing = await detect(cwd)
|
|
37
48
|
if (stillMissing.length > 0) {
|
|
38
49
|
return {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { buildAuthStrategy } from '@/channels/adapters/github/auth'
|
|
2
|
+
import { applyManagedPath, buildManagedPath, resolveAgentId } from '@/channels/adapters/github/managed-path'
|
|
3
|
+
import { registerGithubWebhooks, type WebhookRegistrationResult } from '@/channels/adapters/github/webhook-register'
|
|
4
|
+
import { DEFAULT_GITHUB_EVENT_ALLOWLIST } from '@/channels/schema'
|
|
5
|
+
|
|
6
|
+
import type { GithubInitCredentials } from './index'
|
|
7
|
+
|
|
8
|
+
// Host-side webhook install for `typeclaw channel add github` (and the
|
|
9
|
+
// init-time GitHub branch). The container-side adapter still re-runs this on
|
|
10
|
+
// every start so a missing/rotated tunnel URL eventually catches up, but
|
|
11
|
+
// doing it eagerly here means the user sees the install succeed at CLI
|
|
12
|
+
// time — no more "I added the channel, why isn't GitHub delivering events?"
|
|
13
|
+
// when the URL is already known (external provider, or a user-set
|
|
14
|
+
// webhookUrl).
|
|
15
|
+
//
|
|
16
|
+
// Only fires when an effective webhook URL is known up front: external
|
|
17
|
+
// tunnel provider, or an explicit `webhookUrl`. Cloudflare quick tunnels
|
|
18
|
+
// don't resolve until cloudflared boots inside the container, so they
|
|
19
|
+
// stay on the existing deferred (tunnel-bridge → restartAdapter) path.
|
|
20
|
+
|
|
21
|
+
export type EagerGithubWebhookInstallOptions = {
|
|
22
|
+
webhookUrl: string
|
|
23
|
+
webhookSecret: string
|
|
24
|
+
repos: readonly string[]
|
|
25
|
+
events?: readonly string[]
|
|
26
|
+
auth: GithubInitCredentials['auth']
|
|
27
|
+
// Agent folder (or container name). Used to derive a stable webhook URL
|
|
28
|
+
// path marker so this eager-installed hook is recognizable to the
|
|
29
|
+
// container-side adapter on every subsequent start, even after the
|
|
30
|
+
// public URL's hostname rotates. Optional only because legacy direct
|
|
31
|
+
// callers (host-side init flows on stable user-set webhookUrls) may
|
|
32
|
+
// omit it; production wiring in src/init/index.ts always passes it.
|
|
33
|
+
agentDir?: string
|
|
34
|
+
fetchImpl?: typeof fetch
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type EagerGithubWebhookInstallResult = WebhookRegistrationResult | { error: string; repos: [] }
|
|
38
|
+
|
|
39
|
+
export async function installGithubWebhooksEagerly(
|
|
40
|
+
options: EagerGithubWebhookInstallOptions,
|
|
41
|
+
): Promise<EagerGithubWebhookInstallResult> {
|
|
42
|
+
if (options.repos.length === 0) return { repos: [] }
|
|
43
|
+
|
|
44
|
+
let strategy: ReturnType<typeof buildAuthStrategy>
|
|
45
|
+
try {
|
|
46
|
+
strategy = buildAuthStrategy({
|
|
47
|
+
auth: authToSecretBlock(options.auth),
|
|
48
|
+
...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
|
|
49
|
+
})
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return { error: describe(err), repos: [] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const managedPath =
|
|
55
|
+
options.agentDir !== undefined ? buildManagedPath(resolveAgentId({ agentDir: options.agentDir })) : undefined
|
|
56
|
+
const webhookUrl = managedPath !== undefined ? applyManagedPath(options.webhookUrl, managedPath) : options.webhookUrl
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await registerGithubWebhooks({
|
|
60
|
+
token: () => strategy.token(),
|
|
61
|
+
webhookUrl,
|
|
62
|
+
webhookSecret: options.webhookSecret,
|
|
63
|
+
repos: options.repos,
|
|
64
|
+
events: options.events ?? DEFAULT_GITHUB_EVENT_ALLOWLIST,
|
|
65
|
+
...(managedPath !== undefined ? { managedPath } : {}),
|
|
66
|
+
...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
|
|
67
|
+
})
|
|
68
|
+
return result
|
|
69
|
+
} finally {
|
|
70
|
+
// PatAuthStrategy.dispose() is a no-op and AppAuthStrategy clears its
|
|
71
|
+
// cached installation token. Either way, releasing it here keeps the
|
|
72
|
+
// host CLI from holding onto credentials longer than needed.
|
|
73
|
+
await strategy.dispose().catch(() => {})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Bridge the wizard/CLI's plaintext credentials union into the Secret-wrapped
|
|
78
|
+
// shape buildAuthStrategy expects. Plain strings are wrapped as `{ value }`
|
|
79
|
+
// so the underlying PatAuthStrategy resolver doesn't try (and fail) to read
|
|
80
|
+
// from process.env.
|
|
81
|
+
function authToSecretBlock(auth: GithubInitCredentials['auth']) {
|
|
82
|
+
if (auth.type === 'pat') {
|
|
83
|
+
return { type: 'pat' as const, token: { value: auth.pat } }
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
type: 'app' as const,
|
|
87
|
+
appId: auth.appId,
|
|
88
|
+
privateKey: { value: auth.privateKey },
|
|
89
|
+
...(auth.installationId !== undefined ? { installationId: auth.installationId } : {}),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function describe(err: unknown): string {
|
|
94
|
+
return err instanceof Error ? err.message : String(err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatEagerGithubWebhookInstallResult(result: EagerGithubWebhookInstallResult): string {
|
|
98
|
+
if ('error' in result) return `GitHub webhook install failed: ${result.error}`
|
|
99
|
+
const created = result.repos.filter((r) => r.action === 'created').length
|
|
100
|
+
const updated = result.repos.filter((r) => r.action === 'updated').length
|
|
101
|
+
const failed = result.repos.filter((r) => r.action === 'failed')
|
|
102
|
+
const parts: string[] = []
|
|
103
|
+
if (created > 0) parts.push(`${created} created`)
|
|
104
|
+
if (updated > 0) parts.push(`${updated} updated`)
|
|
105
|
+
if (failed.length > 0) parts.push(`${failed.length} failed`)
|
|
106
|
+
const summary = parts.length > 0 ? parts.join(', ') : 'no repos'
|
|
107
|
+
const tail = failed.length > 0 ? ` (${failed.map((f) => `${f.repo}: ${f.error}`).join('; ')})` : ''
|
|
108
|
+
return `GitHub webhooks: ${summary}.${tail}`
|
|
109
|
+
}
|
package/src/init/hatching.ts
CHANGED
|
@@ -45,9 +45,9 @@ Do these in order. Do **not** ask further questions.
|
|
|
45
45
|
2. Write one short paragraph in \`MEMORY.md\` marking this moment: the date, how you came to be, what you and the user agreed on.
|
|
46
46
|
3. Configure local git identity with \`bash\`: \`git config user.name "<your name>"\` and \`git config user.email "<reasonable placeholder>@typeclaw.local"\` (unless the user provided an email).
|
|
47
47
|
4. Stage and commit **only the files you authored** with commit message \`Hatched 🐣\`. This is the hatching-specific commit message — it overrides the normal version-control style guidance for this one commit.
|
|
48
|
-
5. Send **one final short message** — two sentences at most — telling the user hatching is complete and they can \`/quit\`
|
|
48
|
+
5. Send **one final short message** — two sentences at most — telling the user hatching is complete and they can leave the TUI with \`/quit\` (or Ctrl+C). Do not ask further questions. Do not offer more work. The container keeps running once they quit; keeping the TUI open here wastes time.
|
|
49
49
|
|
|
50
|
-
After that final message, stop. If the user keeps talking, answer briefly and remind them they can \`/quit\` whenever they are ready.
|
|
50
|
+
After that final message, stop. If the user keeps talking, answer briefly and remind them they can \`/quit\` (or Ctrl+C) whenever they are ready.
|
|
51
51
|
|
|
52
52
|
This is the only time you will receive these instructions. After the \`Hatched 🐣\` commit, your identity takes over and you run as yourself.`
|
|
53
53
|
|