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 +1 -1
- package/src/init/dockerfile.ts +43 -17
- package/src/init/line-auth.ts +50 -21
package/package.json
CHANGED
package/src/init/dockerfile.ts
CHANGED
|
@@ -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.
|
|
297
|
-
#
|
|
298
|
-
#
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
460
|
-
#
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
# as a
|
|
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 [ -
|
|
468
|
-
|
|
469
|
-
|
|
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
|
}
|
package/src/init/line-auth.ts
CHANGED
|
@@ -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
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 = () => {}
|