@vellumai/assistant 0.11.8-dev.202609022016.7bd8f87 → 0.11.8-dev.202609030046.e26b25a
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/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/package.json +1 -1
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/process-message-display-content.test.ts +4 -9
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/agent/attachments.ts +13 -2
- package/src/config/feature-flag-registry.json +1 -1
- package/src/daemon/conversation-messaging.ts +88 -25
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/live-voice/__tests__/live-voice-photo.test.ts +11 -11
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +12 -7
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +20 -20
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +212 -163
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +77 -77
- package/src/runtime/channel-retry-sweep.ts +1 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +28 -4
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +5 -7
- package/src/tools/terminal/safe-env.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
package/Dockerfile
CHANGED
|
@@ -186,53 +186,11 @@ RUN printf '%s\n' \
|
|
|
186
186
|
>> /home/assistant/.bashrc && \
|
|
187
187
|
chown assistant:assistant /home/assistant/.bashrc
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
' exec /usr/bin/apt-get "$@"' \
|
|
195
|
-
'fi' \
|
|
196
|
-
'export DEBIAN_FRONTEND=noninteractive' \
|
|
197
|
-
'DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"' \
|
|
198
|
-
'/app/assistant/docker-init-apt-root.sh' \
|
|
199
|
-
'if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/apt-get" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then' \
|
|
200
|
-
' exec chroot "${DATA_ROOT}" /usr/bin/apt-get "$@"' \
|
|
201
|
-
'fi' \
|
|
202
|
-
'exec /usr/bin/apt-get "$@"' \
|
|
203
|
-
> /usr/local/bin/apt-get && \
|
|
204
|
-
chmod +x /usr/local/bin/apt-get && \
|
|
205
|
-
printf '%s\n' \
|
|
206
|
-
'#!/usr/bin/env sh' \
|
|
207
|
-
'set -eu' \
|
|
208
|
-
'. /app/assistant/docker-kata-runtime-family.sh' \
|
|
209
|
-
'if ! vellum_is_kata_family_runtime; then' \
|
|
210
|
-
' exec /usr/bin/apt "$@"' \
|
|
211
|
-
'fi' \
|
|
212
|
-
'export DEBIAN_FRONTEND=noninteractive' \
|
|
213
|
-
'DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"' \
|
|
214
|
-
'/app/assistant/docker-init-apt-root.sh' \
|
|
215
|
-
'if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/apt" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then' \
|
|
216
|
-
' exec chroot "${DATA_ROOT}" /usr/bin/apt "$@"' \
|
|
217
|
-
'fi' \
|
|
218
|
-
'exec /usr/bin/apt "$@"' \
|
|
219
|
-
> /usr/local/bin/apt && \
|
|
220
|
-
chmod +x /usr/local/bin/apt && \
|
|
221
|
-
printf '%s\n' \
|
|
222
|
-
'#!/usr/bin/env sh' \
|
|
223
|
-
'set -eu' \
|
|
224
|
-
'. /app/assistant/docker-kata-runtime-family.sh' \
|
|
225
|
-
'if ! vellum_is_kata_family_runtime; then' \
|
|
226
|
-
' exec /usr/bin/dpkg "$@"' \
|
|
227
|
-
'fi' \
|
|
228
|
-
'DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"' \
|
|
229
|
-
'/app/assistant/docker-init-apt-root.sh' \
|
|
230
|
-
'if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/dpkg" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then' \
|
|
231
|
-
' exec chroot "${DATA_ROOT}" /usr/bin/dpkg "$@"' \
|
|
232
|
-
'fi' \
|
|
233
|
-
'exec /usr/bin/dpkg "$@"' \
|
|
234
|
-
> /usr/local/bin/dpkg && \
|
|
235
|
-
chmod +x /usr/local/bin/dpkg
|
|
189
|
+
# apt/apt-get/dpkg must persist like pip: on Kata-family runtimes the wrapper
|
|
190
|
+
# routes package operations into the persistent apt chroot.
|
|
191
|
+
RUN ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/apt-get && \
|
|
192
|
+
ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/apt && \
|
|
193
|
+
ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/dpkg
|
|
236
194
|
|
|
237
195
|
# pip must persist like apt: on Kata-family runtimes the wrapper routes root
|
|
238
196
|
# installs into the persistent apt chroot.
|
|
@@ -274,8 +232,10 @@ RUN chmod +x \
|
|
|
274
232
|
/app/assistant/docker-entrypoint.sh \
|
|
275
233
|
/app/assistant/docker-init-apt-root.sh \
|
|
276
234
|
/app/assistant/docker-kata-apt-env.sh \
|
|
235
|
+
/app/assistant/docker-kata-apt-shims.sh \
|
|
236
|
+
/app/assistant/docker-kata-apt-wrapper.sh \
|
|
237
|
+
/app/assistant/docker-kata-chroot-exec.sh \
|
|
277
238
|
/app/assistant/docker-kata-pip.sh \
|
|
278
|
-
/app/assistant/docker-kata-pip-chroot.sh \
|
|
279
239
|
/app/assistant/docker-kata-runtime-family.sh \
|
|
280
240
|
/usr/local/bin/vellum-block-volume-common.sh \
|
|
281
241
|
/usr/local/bin/vellum-block-volume-init.sh \
|
package/docker-entrypoint.sh
CHANGED
|
@@ -11,7 +11,11 @@ KATA_APT_INIT_PID=""
|
|
|
11
11
|
if vellum_is_kata_family_runtime && [ -x /app/assistant/docker-init-apt-root.sh ]; then
|
|
12
12
|
export VELLUM_APT_DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"
|
|
13
13
|
# Warm the chroot used by Kata-family apt wrappers without blocking assistant readiness.
|
|
14
|
-
|
|
14
|
+
(
|
|
15
|
+
/app/assistant/docker-init-apt-root.sh
|
|
16
|
+
# Refresh persisted wrapper-script shims before commands use the overlay.
|
|
17
|
+
/app/assistant/docker-kata-apt-shims.sh || true
|
|
18
|
+
) &
|
|
15
19
|
KATA_APT_INIT_PID="$!"
|
|
16
20
|
fi
|
|
17
21
|
|
package/docker-kata-apt-env.sh
CHANGED
|
@@ -36,6 +36,9 @@ _vellum_kata_prepend_library_path() {
|
|
|
36
36
|
esac
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
# Shims for chroot wrapper scripts with hardcoded absolute paths must shadow
|
|
40
|
+
# the broken originals in the chroot bin dirs below (docker-kata-apt-shims.sh).
|
|
41
|
+
_vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/.host-shims"
|
|
39
42
|
_vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/bin"
|
|
40
43
|
_vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/usr/local/sbin"
|
|
41
44
|
_vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/usr/local/bin"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Regenerates host-side shims for chroot-installed commands whose bin entry
|
|
3
|
+
# is a pure trampoline script exec'ing a hardcoded absolute path (Debian
|
|
4
|
+
# 7zip's /usr/bin/7z execs /usr/lib/7zip/7z, for example). Those targets only
|
|
5
|
+
# exist inside the chroot, so running the wrapper through the host PATH
|
|
6
|
+
# overlay fails; each shim execs the chroot-prefixed target instead.
|
|
7
|
+
# docker-kata-apt-env.sh and buildSanitizedEnv put the shim dir on PATH ahead
|
|
8
|
+
# of the chroot bin dirs so shims shadow the broken wrappers.
|
|
9
|
+
set -eu
|
|
10
|
+
|
|
11
|
+
DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"
|
|
12
|
+
SHIM_DIR="${DATA_ROOT}/.host-shims"
|
|
13
|
+
MARKER="# vellum-apt-shim:"
|
|
14
|
+
# dpkg-managed dirs that can hold trampolines (Debian policy keeps packages
|
|
15
|
+
# out of /usr/local; /bin and /sbin are usrmerge symlinks into these).
|
|
16
|
+
SCAN_DIRS="usr/bin usr/sbin usr/games"
|
|
17
|
+
# Every overlay bin dir in PATH priority order (usrmerge collapses bin/sbin
|
|
18
|
+
# into their usr counterparts): precedence must also see the /usr/local dirs
|
|
19
|
+
# that chroot pip installs populate, so a dpkg trampoline never shims over a
|
|
20
|
+
# higher-priority local command.
|
|
21
|
+
PRIORITY_DIRS="usr/bin usr/local/sbin usr/local/bin usr/sbin usr/games"
|
|
22
|
+
|
|
23
|
+
[ -d "${DATA_ROOT}/usr/bin" ] || exit 0
|
|
24
|
+
mkdir -p "${SHIM_DIR}"
|
|
25
|
+
|
|
26
|
+
# Scanning the bin dirs forks a few processes per file; skip it when the
|
|
27
|
+
# overlay state is unchanged (read-only invocations like `apt list` or
|
|
28
|
+
# `dpkg -l`). The stamp covers command names, links, effective modes, and small
|
|
29
|
+
# executable contents so reinstalls and pip changes invalidate stale shims.
|
|
30
|
+
STAMP_FILE="${SHIM_DIR}/.overlay-stamp"
|
|
31
|
+
STAMP="$(
|
|
32
|
+
{
|
|
33
|
+
md5sum "${DATA_ROOT}/var/lib/dpkg/status" 2>/dev/null
|
|
34
|
+
for d in ${PRIORITY_DIRS}; do
|
|
35
|
+
printf '%s\0' "${d}"
|
|
36
|
+
find "${DATA_ROOT}/${d}" -mindepth 1 -maxdepth 1 -printf '%P %y %m %l\0' 2>/dev/null | sort -z
|
|
37
|
+
find -L "${DATA_ROOT}/${d}" -mindepth 1 -maxdepth 1 -printf '%P %y %m\0' 2>/dev/null | sort -z
|
|
38
|
+
done
|
|
39
|
+
for d in ${SCAN_DIRS}; do
|
|
40
|
+
find -L "${DATA_ROOT}/${d}" -mindepth 1 -maxdepth 1 -type f -size -513c -perm /111 -print0 2>/dev/null |
|
|
41
|
+
sort -z |
|
|
42
|
+
xargs -0 -r md5sum -z
|
|
43
|
+
done
|
|
44
|
+
} | md5sum | cut -d' ' -f1
|
|
45
|
+
)"
|
|
46
|
+
if [ "${STAMP}" = "$(cat "${STAMP_FILE}" 2>/dev/null || true)" ]; then
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Prints the target of a pure trampoline: a tiny script whose body is nothing
|
|
51
|
+
# but the shebang, comments, blank lines, and a single `exec /abs/path "$@"`
|
|
52
|
+
# line. Wrappers that do anything else (export env vars, cd, pass extra
|
|
53
|
+
# flags) print nothing: a direct-exec shim would bypass that setup.
|
|
54
|
+
wrapper_target() {
|
|
55
|
+
[ -f "$1" ] && [ -x "$1" ] || return 0
|
|
56
|
+
[ "$(wc -c <"$1")" -le 512 ] || return 0
|
|
57
|
+
case "$(head -c 2 "$1" 2>/dev/null)" in
|
|
58
|
+
'#!') ;;
|
|
59
|
+
*) return 0 ;;
|
|
60
|
+
esac
|
|
61
|
+
body="$(sed -e '1d' -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' "$1")"
|
|
62
|
+
[ -n "${body}" ] || return 0
|
|
63
|
+
[ "$(printf '%s' "${body}" | wc -l)" -eq 0 ] || return 0
|
|
64
|
+
printf '%s\n' "${body}" | sed -n 's/^[[:space:]]*exec[[:space:]][[:space:]]*\(\/[^"[:space:]][^"[:space:]]*\)[[:space:]][[:space:]]*"\$@"[[:space:]]*$/\1/p'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Highest-priority overlay dir that holds an entry for this shim name.
|
|
68
|
+
shim_source() {
|
|
69
|
+
for d in ${PRIORITY_DIRS}; do
|
|
70
|
+
candidate="${DATA_ROOT}/${d}/$1"
|
|
71
|
+
if [ -f "${candidate}" ] && [ -x "${candidate}" ]; then
|
|
72
|
+
printf '%s\n' "${candidate}"
|
|
73
|
+
return 0
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
generation_failed=0
|
|
79
|
+
for d in ${SCAN_DIRS}; do
|
|
80
|
+
for bin in "${DATA_ROOT}/${d}/"*; do
|
|
81
|
+
# Shims outrank every chroot bin dir on PATH, so only the
|
|
82
|
+
# highest-priority entry for a name may produce one: a trampoline in a
|
|
83
|
+
# lower dir must not shadow a real command above it.
|
|
84
|
+
[ "$(shim_source "${bin##*/}")" = "${bin}" ] || continue
|
|
85
|
+
target="$(wrapper_target "${bin}")"
|
|
86
|
+
[ -n "${target}" ] || continue
|
|
87
|
+
# Only shim when the hardcoded path is broken on the host but real in
|
|
88
|
+
# the chroot.
|
|
89
|
+
if [ -e "${target}" ] || [ ! -x "${DATA_ROOT}${target}" ]; then
|
|
90
|
+
continue
|
|
91
|
+
fi
|
|
92
|
+
shim="${SHIM_DIR}/${bin##*/}"
|
|
93
|
+
if ! tmp="$(mktemp "${SHIM_DIR}/.shim.XXXXXX")"; then
|
|
94
|
+
generation_failed=1
|
|
95
|
+
continue
|
|
96
|
+
fi
|
|
97
|
+
if printf '%s\n' \
|
|
98
|
+
'#!/bin/sh' \
|
|
99
|
+
"${MARKER} ${target}" \
|
|
100
|
+
"exec \"${DATA_ROOT}${target}\" \"\$@\"" \
|
|
101
|
+
>"${tmp}" &&
|
|
102
|
+
chmod 0755 "${tmp}" &&
|
|
103
|
+
mv -f "${tmp}" "${shim}"; then
|
|
104
|
+
:
|
|
105
|
+
else
|
|
106
|
+
rm -f "${tmp}"
|
|
107
|
+
generation_failed=1
|
|
108
|
+
fi
|
|
109
|
+
done
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
if [ "${generation_failed}" -ne 0 ]; then
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Drop shims that remain stale after desired replacements are installed.
|
|
117
|
+
for shim in "${SHIM_DIR}"/*; do
|
|
118
|
+
[ -f "${shim}" ] || continue
|
|
119
|
+
grep -qs "^${MARKER}" "${shim}" || continue
|
|
120
|
+
target="$(sed -n "s|^${MARKER} ||p" "${shim}" | sed -n '1p')"
|
|
121
|
+
src="$(shim_source "$(basename "${shim}")")"
|
|
122
|
+
if [ -z "${target}" ] || [ -z "${src}" ] || [ ! -x "${DATA_ROOT}${target}" ] || [ "$(wrapper_target "${src}")" != "${target}" ]; then
|
|
123
|
+
rm -f "${shim}"
|
|
124
|
+
fi
|
|
125
|
+
done
|
|
126
|
+
|
|
127
|
+
printf '%s\n' "${STAMP}" >"${STAMP_FILE}"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# apt/apt-get/dpkg wrapper (symlinked from /usr/local/bin). On Kata-family
|
|
3
|
+
# runtimes, package operations are routed into the persistent apt chroot so
|
|
4
|
+
# they survive machine saves; the image rootfs is discarded on every save.
|
|
5
|
+
# The chroot runs through docker-kata-chroot-exec.sh in a private mount
|
|
6
|
+
# namespace with /dev, /dev/pts and /proc available, so apt can allocate a
|
|
7
|
+
# pty for term.log and maintainer scripts see a normal system.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
TOOL_NAME="$(basename "$0")"
|
|
11
|
+
case "${TOOL_NAME}" in
|
|
12
|
+
apt | apt-get | dpkg) ;;
|
|
13
|
+
*) TOOL_NAME="apt-get" ;;
|
|
14
|
+
esac
|
|
15
|
+
|
|
16
|
+
. /app/assistant/docker-kata-runtime-family.sh
|
|
17
|
+
|
|
18
|
+
if ! vellum_is_kata_family_runtime; then
|
|
19
|
+
exec "/usr/bin/${TOOL_NAME}" "$@"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [ "${TOOL_NAME}" != "dpkg" ]; then
|
|
23
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
24
|
+
fi
|
|
25
|
+
DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"
|
|
26
|
+
|
|
27
|
+
/app/assistant/docker-init-apt-root.sh
|
|
28
|
+
if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/${TOOL_NAME}" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then
|
|
29
|
+
rc=0
|
|
30
|
+
# Platform kata pods are privileged inside their per-assistant VM, so
|
|
31
|
+
# unshare -m works there; if this environment can't create a mount
|
|
32
|
+
# namespace, fall back to a bare chroot: installs still persist, but with
|
|
33
|
+
# no /dev/pts or /proc apt logs a pty warning and the caller's cwd is not
|
|
34
|
+
# restored.
|
|
35
|
+
if unshare -m true 2>/dev/null; then
|
|
36
|
+
unshare -m /app/assistant/docker-kata-chroot-exec.sh "${DATA_ROOT}" "${PWD}" "/usr/bin/${TOOL_NAME}" "$@" || rc=$?
|
|
37
|
+
else
|
|
38
|
+
chroot "${DATA_ROOT}" "/usr/bin/${TOOL_NAME}" "$@" || rc=$?
|
|
39
|
+
fi
|
|
40
|
+
# Packages can install wrapper scripts that hardcode absolute paths which
|
|
41
|
+
# only resolve inside the chroot; refresh host-side shims for those.
|
|
42
|
+
/app/assistant/docker-kata-apt-shims.sh || true
|
|
43
|
+
exit "${rc}"
|
|
44
|
+
fi
|
|
45
|
+
exec "/usr/bin/${TOOL_NAME}" "$@"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Runs a command inside the persistent apt chroot. Must be invoked under
|
|
3
|
+
# `unshare -m` (see docker-kata-pip.sh and docker-kata-apt-wrapper.sh): the
|
|
4
|
+
# bind mounts below live in a private mount namespace so they disappear with
|
|
5
|
+
# the process and are never visible to other processes; a lingering /data
|
|
6
|
+
# bind inside the chroot would create a path cycle (/data/system/data/system/...)
|
|
7
|
+
# for anything walking /data.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
DATA_ROOT="$1"
|
|
11
|
+
CALLER_CWD="$2"
|
|
12
|
+
shift 2
|
|
13
|
+
|
|
14
|
+
for dir in /workspace /data /tmp /var/tmp; do
|
|
15
|
+
[ -d "${dir}" ] || continue
|
|
16
|
+
mkdir -p "${DATA_ROOT}${dir}"
|
|
17
|
+
mount --bind "${dir}" "${DATA_ROOT}${dir}" 2>/dev/null || true
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
# /proc and /dev make the chroot behave like a normal system: apt needs a pty
|
|
21
|
+
# (posix_openpt via /dev/ptmx) to write /var/log/apt/term.log, and package
|
|
22
|
+
# maintainer scripts routinely read /proc. The recursive /dev bind carries the
|
|
23
|
+
# container's /dev/pts along; the devpts mount is a fallback for environments
|
|
24
|
+
# where it did not produce one.
|
|
25
|
+
mkdir -p "${DATA_ROOT}/proc" "${DATA_ROOT}/dev"
|
|
26
|
+
mount --bind /proc "${DATA_ROOT}/proc" 2>/dev/null || true
|
|
27
|
+
mount --rbind /dev "${DATA_ROOT}/dev" 2>/dev/null || true
|
|
28
|
+
if [ ! -e "${DATA_ROOT}/dev/pts/ptmx" ]; then
|
|
29
|
+
mkdir -p "${DATA_ROOT}/dev/pts" 2>/dev/null || true
|
|
30
|
+
mount -t devpts -o gid=5,mode=620,ptmxmode=666 devpts "${DATA_ROOT}/dev/pts" 2>/dev/null || true
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# chroot(1) always chdirs to /; restore the caller's cwd when it exists inside
|
|
34
|
+
# the chroot so relative paths keep working.
|
|
35
|
+
exec chroot "${DATA_ROOT}" /bin/sh -c 'cd "$1" 2>/dev/null || cd /; shift; exec "$@"' sh "${CALLER_CWD}" "$@"
|
package/docker-kata-pip.sh
CHANGED
|
@@ -38,10 +38,16 @@ if [ "$(id -u)" = "0" ]; then
|
|
|
38
38
|
# unshare -m works there; if this environment can't create a mount
|
|
39
39
|
# namespace, fall back to a bare chroot — index installs still persist,
|
|
40
40
|
# only caller-path installs lose visibility.
|
|
41
|
+
rc=0
|
|
41
42
|
if unshare -m true 2>/dev/null; then
|
|
42
|
-
|
|
43
|
+
unshare -m /app/assistant/docker-kata-chroot-exec.sh "${DATA_ROOT}" "${PWD}" "/usr/bin/${PIP_NAME}" "$@" || rc=$?
|
|
44
|
+
else
|
|
45
|
+
chroot "${DATA_ROOT}" /bin/sh -c 'cd "$1" 2>/dev/null || cd /; shift; exec "$@"' sh "${PWD}" "/usr/bin/${PIP_NAME}" "$@" || rc=$?
|
|
43
46
|
fi
|
|
44
|
-
|
|
47
|
+
# pip can add usr/local commands that outrank existing shims on the
|
|
48
|
+
# PATH overlay; refresh so a stale shim never shadows them.
|
|
49
|
+
/app/assistant/docker-kata-apt-shims.sh || true
|
|
50
|
+
exit "${rc}"
|
|
45
51
|
fi
|
|
46
52
|
fi
|
|
47
53
|
echo "Warning: persistent pip root unavailable; falling back to the image pip (installs will not survive a save)" >&2
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* stays bound to the oldest upload.
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
|
+
import { basename } from "node:path";
|
|
11
12
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
12
13
|
|
|
13
14
|
import { reinjectAttachmentPathAnnotations } from "../daemon/conversation-lifecycle.js";
|
|
@@ -119,6 +120,85 @@ describe("persistQueuedMessageBody stored path annotations", () => {
|
|
|
119
120
|
expect(rebuiltBlock.text).toBe(annotation.text);
|
|
120
121
|
});
|
|
121
122
|
|
|
123
|
+
test("offloaded oversized text records its stored path for history reload", async () => {
|
|
124
|
+
const conv = createConversation();
|
|
125
|
+
const ctx = makeCtx(conv.id);
|
|
126
|
+
const original = "x".repeat(8_000_001);
|
|
127
|
+
|
|
128
|
+
await persistQueuedMessageBody(ctx, { content: original });
|
|
129
|
+
|
|
130
|
+
const row = rawGet<{ metadata: string; content: string }>(
|
|
131
|
+
"test:offloadMessage",
|
|
132
|
+
"SELECT metadata, content FROM messages ORDER BY created_at DESC LIMIT 1",
|
|
133
|
+
);
|
|
134
|
+
const meta = JSON.parse(row!.metadata) as {
|
|
135
|
+
attachmentStoredPaths?: Record<string, string>;
|
|
136
|
+
};
|
|
137
|
+
const storedPaths = Object.values(meta.attachmentStoredPaths ?? {});
|
|
138
|
+
expect(storedPaths).toHaveLength(1);
|
|
139
|
+
const filename = basename(storedPaths[0]!);
|
|
140
|
+
expect(filename.startsWith("oversized-content-")).toBe(true);
|
|
141
|
+
expect(filename.endsWith(".txt")).toBe(true);
|
|
142
|
+
expect(readFileSync(storedPaths[0]!).toString("utf8")).toBe(original);
|
|
143
|
+
|
|
144
|
+
const annotation = lastAnnotationBlock(ctx);
|
|
145
|
+
expect(annotation.text).toBe(
|
|
146
|
+
`[Attachment "${filename}" is stored at: ${storedPaths[0]}]`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const persisted = JSON.parse(row!.content) as ContentBlock[];
|
|
150
|
+
const rebuilt = reinjectAttachmentPathAnnotations(
|
|
151
|
+
persisted,
|
|
152
|
+
"user",
|
|
153
|
+
row!.metadata,
|
|
154
|
+
);
|
|
155
|
+
const rebuiltBlock = rebuilt.at(-1) as { type: "text"; text: string };
|
|
156
|
+
expect(rebuiltBlock.text).toBe(annotation.text);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("two oversized messages in one conversation get distinct filenames", async () => {
|
|
160
|
+
const conv = createConversation();
|
|
161
|
+
const ctx = makeCtx(conv.id);
|
|
162
|
+
|
|
163
|
+
await persistQueuedMessageBody(ctx, { content: "a".repeat(8_000_001) });
|
|
164
|
+
await persistQueuedMessageBody(ctx, { content: "b".repeat(8_000_001) });
|
|
165
|
+
|
|
166
|
+
const first = (ctx.messages[0].content as ContentBlock[]).at(-1) as {
|
|
167
|
+
text: string;
|
|
168
|
+
};
|
|
169
|
+
const second = (ctx.messages[1].content as ContentBlock[]).at(-1) as {
|
|
170
|
+
text: string;
|
|
171
|
+
};
|
|
172
|
+
expect(first.text).toContain("oversized-content-");
|
|
173
|
+
expect(second.text).toContain("oversized-content-");
|
|
174
|
+
expect(first.text).not.toBe(second.text);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("display-only offload is not named on the model-facing list", async () => {
|
|
178
|
+
const conv = createConversation();
|
|
179
|
+
const ctx = makeCtx(conv.id);
|
|
180
|
+
const display = "y".repeat(8_000_001);
|
|
181
|
+
|
|
182
|
+
await persistQueuedMessageBody(ctx, {
|
|
183
|
+
content: "fenced model copy",
|
|
184
|
+
displayContent: display,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const llmContent = ctx.messages[0].content as ContentBlock[];
|
|
188
|
+
expect(JSON.stringify(llmContent)).not.toContain("oversized-content-");
|
|
189
|
+
expect(JSON.stringify(llmContent)).toContain("fenced model copy");
|
|
190
|
+
expect(JSON.stringify(llmContent)).not.toContain(display);
|
|
191
|
+
|
|
192
|
+
const row = rawGet<{ metadata: string }>(
|
|
193
|
+
"test:displayOnlyOffloadMetadata",
|
|
194
|
+
"SELECT metadata FROM messages ORDER BY created_at DESC LIMIT 1",
|
|
195
|
+
);
|
|
196
|
+
const meta = JSON.parse(row!.metadata) as {
|
|
197
|
+
attachmentStoredPaths?: Record<string, string>;
|
|
198
|
+
};
|
|
199
|
+
expect(meta.attachmentStoredPaths).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
122
202
|
test("messages without attachments get no annotation block", async () => {
|
|
123
203
|
const conv = createConversation();
|
|
124
204
|
const ctx = makeCtx(conv.id);
|
|
@@ -2,6 +2,10 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
offloadOversizedText,
|
|
7
|
+
OVERSIZED_CONTENT_FILENAME_PREFIX,
|
|
8
|
+
} from "../daemon/port-oversized-content.js";
|
|
5
9
|
import {
|
|
6
10
|
attachInlineAttachmentToMessage,
|
|
7
11
|
AttachmentUploadError,
|
|
@@ -456,6 +460,29 @@ describe("createInlineAttachment (workspace_ref persistence)", () => {
|
|
|
456
460
|
stored.id,
|
|
457
461
|
]);
|
|
458
462
|
});
|
|
463
|
+
|
|
464
|
+
test("offloadOversizedText writes the original bytes next to the conversation", async () => {
|
|
465
|
+
const conv = createConversation();
|
|
466
|
+
const original = "x".repeat(32);
|
|
467
|
+
const result = await offloadOversizedText(
|
|
468
|
+
original,
|
|
469
|
+
{
|
|
470
|
+
conversationId: conv.id,
|
|
471
|
+
conversationCreatedAt: conv.createdAt,
|
|
472
|
+
},
|
|
473
|
+
16,
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(result.attachmentId).toBeDefined();
|
|
477
|
+
const filePath = getFilePathForAttachment(result.attachmentId!);
|
|
478
|
+
expect(filePath).toBeTruthy();
|
|
479
|
+
expect(result.filename).toBeDefined();
|
|
480
|
+
expect(filePath!.endsWith(result.filename!)).toBe(true);
|
|
481
|
+
expect(result.filename!.startsWith(`${OVERSIZED_CONTENT_FILENAME_PREFIX}-`)).toBe(
|
|
482
|
+
true,
|
|
483
|
+
);
|
|
484
|
+
expect(readFileSync(filePath!).toString("utf8")).toBe(original);
|
|
485
|
+
});
|
|
459
486
|
});
|
|
460
487
|
|
|
461
488
|
describe("attachInlineAttachmentToMessage filename collisions", () => {
|
|
@@ -127,6 +127,32 @@ describe("attachmentsToReferenceBlocks", () => {
|
|
|
127
127
|
expect(JSON.stringify(blocks)).not.toContain("data");
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
test("omits extracted_text on a video workspace_ref", () => {
|
|
131
|
+
const blocks = attachmentsToReferenceBlocks([
|
|
132
|
+
{
|
|
133
|
+
attachmentId: "att-vid",
|
|
134
|
+
filename: "clip.mp4",
|
|
135
|
+
mimeType: "video/mp4",
|
|
136
|
+
sizeBytes: 40_000_000,
|
|
137
|
+
extractedText: "this must not dump into the prompt",
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
expect(blocks).toEqual([
|
|
142
|
+
{
|
|
143
|
+
type: "file",
|
|
144
|
+
source: {
|
|
145
|
+
type: "workspace_ref",
|
|
146
|
+
media_type: "video/mp4",
|
|
147
|
+
attachmentId: "att-vid",
|
|
148
|
+
sizeBytes: 40_000_000,
|
|
149
|
+
filename: "clip.mp4",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
expect(JSON.stringify(blocks)).not.toContain("extracted");
|
|
154
|
+
});
|
|
155
|
+
|
|
130
156
|
test("builds a file workspace_ref with filename and extracted text", () => {
|
|
131
157
|
const blocks = attachmentsToReferenceBlocks([
|
|
132
158
|
{
|
|
@@ -208,6 +234,23 @@ describe("attachmentsToContentBlocks", () => {
|
|
|
208
234
|
expect(blocks[0].type).toBe("image");
|
|
209
235
|
});
|
|
210
236
|
|
|
237
|
+
test("omits extracted_text on an inline video file block", async () => {
|
|
238
|
+
const blocks = await attachmentsToContentBlocks([
|
|
239
|
+
{
|
|
240
|
+
filename: "clip.mp4",
|
|
241
|
+
mimeType: "video/mp4",
|
|
242
|
+
data: "AAAA",
|
|
243
|
+
extractedText: "must not dump",
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
expect(blocks).toHaveLength(1);
|
|
247
|
+
expect(blocks[0]).toMatchObject({
|
|
248
|
+
type: "file",
|
|
249
|
+
source: { type: "base64", media_type: "video/mp4", filename: "clip.mp4" },
|
|
250
|
+
});
|
|
251
|
+
expect(blocks[0]).not.toHaveProperty("extracted_text");
|
|
252
|
+
});
|
|
253
|
+
|
|
211
254
|
test("creates file content block for non-image mime types", async () => {
|
|
212
255
|
const blocks = await attachmentsToContentBlocks([
|
|
213
256
|
{ filename: "doc.pdf", mimeType: "application/pdf", data: "pdfdata" },
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
const scriptPath = join(
|
|
16
|
+
import.meta.dir,
|
|
17
|
+
"..",
|
|
18
|
+
"..",
|
|
19
|
+
"docker-kata-apt-shims.sh",
|
|
20
|
+
);
|
|
21
|
+
const testRoots: string[] = [];
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
for (const root of testRoots.splice(0)) {
|
|
25
|
+
rmSync(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function writeExecutable(path: string, contents: string): void {
|
|
30
|
+
writeFileSync(path, contents);
|
|
31
|
+
chmodSync(path, 0o755);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createDataRoot(): string {
|
|
35
|
+
const dataRoot = mkdtempSync(join(tmpdir(), "kata-apt-shims-"));
|
|
36
|
+
testRoots.push(dataRoot);
|
|
37
|
+
for (const dir of [
|
|
38
|
+
"usr/bin",
|
|
39
|
+
"usr/sbin",
|
|
40
|
+
"usr/games",
|
|
41
|
+
"usr/local/bin",
|
|
42
|
+
"usr/local/sbin",
|
|
43
|
+
"var/lib/dpkg",
|
|
44
|
+
]) {
|
|
45
|
+
mkdirSync(join(dataRoot, dir), { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(join(dataRoot, "var/lib/dpkg/status"), "unchanged\n");
|
|
48
|
+
return dataRoot;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function refreshShims(dataRoot: string, pathPrefix?: string): void {
|
|
52
|
+
execFileSync("/bin/sh", [scriptPath], {
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
PATH: pathPrefix
|
|
56
|
+
? `${pathPrefix}:${process.env.PATH ?? ""}`
|
|
57
|
+
: process.env.PATH,
|
|
58
|
+
VELLUM_APT_DATA_ROOT: dataRoot,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("docker-kata-apt-shims", () => {
|
|
64
|
+
test.skipIf(process.platform !== "linux")(
|
|
65
|
+
"refreshes a shim when a same-version reinstall retargets its trampoline",
|
|
66
|
+
() => {
|
|
67
|
+
const dataRoot = createDataRoot();
|
|
68
|
+
|
|
69
|
+
const suffix = basename(dataRoot);
|
|
70
|
+
const firstTarget = `/vellum-apt-shim-test/${suffix}-1`;
|
|
71
|
+
const secondTarget = `/vellum-apt-shim-test/${suffix}-2`;
|
|
72
|
+
const source = join(dataRoot, "usr/bin/example-tool");
|
|
73
|
+
const shim = join(dataRoot, ".host-shims/example-tool");
|
|
74
|
+
|
|
75
|
+
for (const target of [firstTarget, secondTarget]) {
|
|
76
|
+
const chrootTarget = `${dataRoot}${target}`;
|
|
77
|
+
mkdirSync(join(chrootTarget, ".."), { recursive: true });
|
|
78
|
+
writeExecutable(chrootTarget, "#!/bin/sh\nexit 0\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writeExecutable(source, `#!/bin/sh\nexec ${firstTarget} "$@"\n`);
|
|
82
|
+
refreshShims(dataRoot);
|
|
83
|
+
expect(readFileSync(shim, "utf8")).toContain(firstTarget);
|
|
84
|
+
|
|
85
|
+
writeExecutable(source, `#!/bin/sh\nexec ${secondTarget} "$@"\n`);
|
|
86
|
+
const fakeBin = join(dataRoot, "fake-bin");
|
|
87
|
+
const failedOnce = join(dataRoot, "mv-failed-once");
|
|
88
|
+
mkdirSync(fakeBin);
|
|
89
|
+
writeExecutable(
|
|
90
|
+
join(fakeBin, "mv"),
|
|
91
|
+
`#!/bin/sh
|
|
92
|
+
if [ ! -e "${failedOnce}" ]; then
|
|
93
|
+
touch "${failedOnce}"
|
|
94
|
+
exit 91
|
|
95
|
+
fi
|
|
96
|
+
[ -f "$3" ] || exit 92
|
|
97
|
+
exec /bin/mv "$@"
|
|
98
|
+
`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(() => refreshShims(dataRoot, fakeBin)).toThrow();
|
|
102
|
+
expect(readFileSync(shim, "utf8")).toContain(firstTarget);
|
|
103
|
+
refreshShims(dataRoot, fakeBin);
|
|
104
|
+
|
|
105
|
+
const refreshedShim = readFileSync(shim, "utf8");
|
|
106
|
+
expect(refreshedShim).toContain(secondTarget);
|
|
107
|
+
expect(refreshedShim).not.toContain(firstTarget);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
test.skipIf(process.platform !== "linux")(
|
|
112
|
+
"ignores non-executable entries and refreshes when they become runnable",
|
|
113
|
+
() => {
|
|
114
|
+
const dataRoot = createDataRoot();
|
|
115
|
+
const suffix = basename(dataRoot);
|
|
116
|
+
const target = `/vellum-apt-shim-test/${suffix}`;
|
|
117
|
+
const chrootTarget = `${dataRoot}${target}`;
|
|
118
|
+
const lowerSource = join(dataRoot, "usr/sbin/example-tool");
|
|
119
|
+
const higherSource = join(dataRoot, "usr/local/bin/example-tool");
|
|
120
|
+
const shim = join(dataRoot, ".host-shims/example-tool");
|
|
121
|
+
|
|
122
|
+
mkdirSync(join(chrootTarget, ".."), { recursive: true });
|
|
123
|
+
writeExecutable(chrootTarget, "#!/bin/sh\nexit 0\n");
|
|
124
|
+
writeExecutable(lowerSource, `#!/bin/sh\nexec ${target} "$@"\n`);
|
|
125
|
+
writeFileSync(higherSource, "#!/bin/sh\nexit 0\n", { mode: 0o644 });
|
|
126
|
+
|
|
127
|
+
refreshShims(dataRoot);
|
|
128
|
+
expect(readFileSync(shim, "utf8")).toContain(target);
|
|
129
|
+
|
|
130
|
+
chmodSync(higherSource, 0o755);
|
|
131
|
+
refreshShims(dataRoot);
|
|
132
|
+
expect(existsSync(shim)).toBe(false);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
});
|