typeclaw 0.36.7 → 0.36.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeclaw",
3
- "version": "0.36.7",
3
+ "version": "0.36.8",
4
4
  "homepage": "https://github.com/typeclaw/typeclaw#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/typeclaw/typeclaw/issues"
@@ -293,9 +293,20 @@ set -eu
293
293
  # (missing library, port conflict, malformed args). Without the
294
294
  # explicit liveness probe below, the shim would then export DISPLAY
295
295
  # and exec bun, agent-browser launches would die with "cannot open
296
- # display", and the operator would chase a phantom bug. We capture
297
- # $! and \`kill -0\` it on every poll iteration so an early exit
298
- # becomes a clear stderr line and a non-zero shim exit.
296
+ # display", and the operator would chase a phantom bug. A monitor
297
+ # subshell owns Xvfb, \`wait\`s for it, and drops a status file the
298
+ # instant it exits; the poll loop checks that file (before the socket
299
+ # check) so an early exit becomes a clear stderr line and a non-zero
300
+ # shim exit.
301
+ #
302
+ # We do NOT probe liveness with \`kill -0 "\$xvfb_pid"\`. A backgrounded
303
+ # child that exits before the shell \`wait\`s for it becomes a zombie,
304
+ # and \`kill -0\` returns success on a zombie PID (it still exists in
305
+ # the process table). Under load the shell reaps the zombie lazily, so
306
+ # \`kill -0\` reported the dead Xvfb as alive for up to the full 3s
307
+ # window — the loop then timed out and printed the misleading "did not
308
+ # create socket within 3s" diagnostic instead of "exited immediately".
309
+ # The status-file handshake sidesteps zombie semantics entirely.
299
310
  #
300
311
  # We DO NOT use \`xvfb-run\`. xvfb-run hangs forever when it runs as
301
312
  # PID 1 inside a container: its SIGUSR1-based ready handshake races
@@ -451,30 +462,45 @@ start_xvfb() {
451
462
  if ! command -v Xvfb >/dev/null 2>&1; then
452
463
  return 0
453
464
  fi
454
- setpriv --bounding-set -net_admin --inh-caps -net_admin --ambient-caps -net_admin \\
455
- -- Xvfb :99 -screen 0 1920x1080x24 -ac +extension RANDR -nolisten tcp \\
456
- >/dev/null 2>&1 &
465
+ # A monitor subshell owns Xvfb and \`wait\`s for it (the bare command
466
+ # blocks until exit), then writes Xvfb's exit code to a status file.
467
+ # The poll loop below reads that file instead of probing \`kill -0\` —
468
+ # see invariant 2 above for why zombie semantics make \`kill -0\`
469
+ # unreliable. \`set +e\` inside the subshell keeps the outer \`set -e\`
470
+ # from killing the monitor before it records a non-zero Xvfb exit.
471
+ xvfb_status="/tmp/typeclaw-xvfb-status.$$"
472
+ rm -f "$xvfb_status"
473
+ (
474
+ set +e
475
+ setpriv --bounding-set -net_admin --inh-caps -net_admin --ambient-caps -net_admin \\
476
+ -- Xvfb :99 -screen 0 1920x1080x24 -ac +extension RANDR -nolisten tcp \\
477
+ >/dev/null 2>&1
478
+ printf '%s\\n' "$?" > "$xvfb_status"
479
+ ) &
457
480
  xvfb_pid=$!
458
481
  export DISPLAY=:99
459
- # Poll the socket every 10ms up to ~3s. Xvfb cold start is typically
460
- # ~20-50ms on a modern host; 3s covers slow Docker Desktop VMs,
461
- # Rosetta/QEMU emulation, and loaded CI runners. We also \`kill -0\`
462
- # the pid each iteration so an Xvfb that died immediately surfaces
463
- # as a clear error instead of a 3-second hang followed by silent
464
- # "cannot open display" downstream.
482
+ # Poll every 10ms up to ~3s. Xvfb cold start is typically ~20-50ms on
483
+ # a modern host; 3s covers slow Docker Desktop VMs, Rosetta/QEMU
484
+ # emulation, and loaded CI runners. The status-file check comes FIRST
485
+ # so an Xvfb that creates the socket and then immediately dies is still
486
+ # treated as a startup failure.
465
487
  i=0
466
488
  while [ $i -lt 300 ]; do
467
- if [ -S /tmp/.X11-unix/X99 ]; then
468
- unset i xvfb_pid
469
- return 0
470
- fi
471
- if ! kill -0 "$xvfb_pid" 2>/dev/null; then
489
+ if [ -f "$xvfb_status" ]; then
490
+ wait "$xvfb_pid" 2>/dev/null || true
491
+ rm -f "$xvfb_status"
472
492
  echo "typeclaw-entrypoint: Xvfb exited immediately; cannot start headed display (docker.file.xvfb=true)" >&2
473
493
  exit 1
474
494
  fi
495
+ if [ -S /tmp/.X11-unix/X99 ]; then
496
+ rm -f "$xvfb_status"
497
+ unset i xvfb_pid xvfb_status
498
+ return 0
499
+ fi
475
500
  sleep 0.01
476
501
  i=$((i + 1))
477
502
  done
503
+ rm -f "$xvfb_status"
478
504
  echo "typeclaw-entrypoint: Xvfb did not create /tmp/.X11-unix/X99 within 3s; refusing to continue (docker.file.xvfb=true)" >&2
479
505
  exit 1
480
506
  }
@@ -50,30 +50,49 @@ export function lineSecretsPath(agentDir: string): string {
50
50
  return join(agentDir, 'secrets.json')
51
51
  }
52
52
 
53
+ // The SDK persists E2EE (Letter-Sealing) key material under
54
+ // `<AGENT_MESSENGER_CONFIG_DIR>/line-storage/`. The container sets that env to
55
+ // the agent workspace (src/init/dockerfile.ts), but a host-stage login (init /
56
+ // `channel reauth line`) would otherwise fall back to `~/.config/agent-messenger`
57
+ // — so the E2EE key gets written somewhere the container never reads, and inbound
58
+ // Letter-Sealing messages stay undecryptable. Point the host login at the same
59
+ // per-agent dir the container uses so the key lands where the runtime reads it.
60
+ export function lineConfigDir(agentDir: string): string {
61
+ return join(agentDir, 'workspace', '.agent-messenger')
62
+ }
63
+
53
64
  export async function runLineBootstrap(input: LineLoginInput): Promise<LineBootstrapStatus> {
54
65
  try {
55
66
  const store = new SecretsLineCredentialStore({ mode: 'host', secretsPath: lineSecretsPath(input.agentDir) })
56
- // The LINE SDK persists the minted auth_token + certificate by calling
57
- // setAccount() on whatever credential manager the client was built with.
58
- // Wiring our secrets.json-backed store in here means a successful login
59
- // writes straight to secrets.json#channels.line no second copy in
60
- // ~/.config/agent-messenger to keep in sync.
61
- const client = input.client ?? buildLineClient(store)
62
-
63
- const result = await suppressLineTokenInfoDump(() =>
64
- input.method === 'qr'
65
- ? client.loginWithQR({
66
- onQRUrl: async (url) => {
67
- await input.callbacks.onQRUrl?.(url)
68
- },
69
- onPincode: input.callbacks.onPincode,
70
- })
71
- : client.loginWithEmail({
72
- email: input.email,
73
- password: input.password,
74
- onPincode: input.callbacks.onPincode,
75
- }),
76
- )
67
+
68
+ // The env is set only for the duration of client construction + login (when
69
+ // the SDK reads it to locate line-storage) and restored after, so a second
70
+ // bootstrap for a different agent in the same process can't inherit the
71
+ // first agent's path. An already-set value (the container's Dockerfile env)
72
+ // is left untouched.
73
+ const result = await withLineConfigDir(lineConfigDir(input.agentDir), () => {
74
+ // The LINE SDK persists the minted auth_token + certificate by calling
75
+ // setAccount() on whatever credential manager the client was built with.
76
+ // Wiring our secrets.json-backed store in here means a successful login
77
+ // writes straight to secrets.json#channels.line — no second copy in
78
+ // ~/.config/agent-messenger to keep in sync.
79
+ const client = input.client ?? buildLineClient(store)
80
+
81
+ return suppressLineTokenInfoDump(() =>
82
+ input.method === 'qr'
83
+ ? client.loginWithQR({
84
+ onQRUrl: async (url) => {
85
+ await input.callbacks.onQRUrl?.(url)
86
+ },
87
+ onPincode: input.callbacks.onPincode,
88
+ })
89
+ : client.loginWithEmail({
90
+ email: input.email,
91
+ password: input.password,
92
+ onPincode: input.callbacks.onPincode,
93
+ }),
94
+ )
95
+ })
77
96
 
78
97
  if (!result.authenticated || result.account_id === undefined) {
79
98
  const reason = result.message ?? result.error ?? 'LINE login did not authenticate'
@@ -105,6 +124,16 @@ function buildLineClient(store: SecretsLineCredentialStore): LineLoginClient {
105
124
  return new RealLineClient(credManager) as unknown as LineLoginClient
106
125
  }
107
126
 
127
+ async function withLineConfigDir<T>(dir: string, fn: () => Promise<T>): Promise<T> {
128
+ const previous = process.env.AGENT_MESSENGER_CONFIG_DIR
129
+ if (previous === undefined) process.env.AGENT_MESSENGER_CONFIG_DIR = dir
130
+ try {
131
+ return await fn()
132
+ } finally {
133
+ if (previous === undefined) delete process.env.AGENT_MESSENGER_CONFIG_DIR
134
+ }
135
+ }
136
+
108
137
  async function suppressLineTokenInfoDump<T>(fn: () => Promise<T>): Promise<T> {
109
138
  const previous = lineTokenInfoSuppressionQueue
110
139
  let release: () => void = () => {}