osborn 0.9.46 → 0.9.48
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.sandbox +129 -91
- package/dist/claude-llm.js +13 -0
- package/package.json +1 -1
package/Dockerfile.sandbox
CHANGED
|
@@ -2,10 +2,42 @@
|
|
|
2
2
|
# Installs osborn as npm package (not from source) for lightweight per-user machines.
|
|
3
3
|
# Build: docker build -f Dockerfile.sandbox -t registry.fly.io/osborn-sandbox/agent:latest .
|
|
4
4
|
# Push: fly auth docker && docker push registry.fly.io/osborn-sandbox/agent:latest
|
|
5
|
+
#
|
|
6
|
+
# ARCHITECTURE — HOME-on-volume (no chroot)
|
|
7
|
+
# ==========================================
|
|
8
|
+
# The persistent Fly volume holds HOME and all user-mutable state, but osborn
|
|
9
|
+
# itself lives in the IMAGE. Image-swap brings new osborn versions; the volume
|
|
10
|
+
# preserves user OAuth, sessions, skills, gh tokens, ssh keys, git config, etc.
|
|
11
|
+
#
|
|
12
|
+
# Key env:
|
|
13
|
+
# - HOME=/workspace/home → all HOME-derived paths persist on volume
|
|
14
|
+
# - OSBORN_CWD=/workspace → osborn's project dir, also on volume
|
|
15
|
+
# - osborn comes from /usr/local/bin/osborn (image's native location)
|
|
16
|
+
#
|
|
17
|
+
# Updates:
|
|
18
|
+
# - Image-swap delivers new osborn binary + new system deps
|
|
19
|
+
# - No bind-mounts, no chroot, no runtime npm install
|
|
20
|
+
# - User state on volume survives image rebuilds
|
|
21
|
+
#
|
|
22
|
+
# WHY NOT CHROOT (history)
|
|
23
|
+
# ------------------------
|
|
24
|
+
# We built and verified a chroot architecture on 2026-05-28 that bind-mounted
|
|
25
|
+
# /usr /lib /lib64 /bin /sbin /opt from image into a chroot at /workspace/root-chroot,
|
|
26
|
+
# with HOME=/root pointing inside the chroot. Real Fly A/B test 2026-05-28 showed:
|
|
27
|
+
# - First-boot time: chroot 102s vs no-chroot 111s (within polling noise)
|
|
28
|
+
# - EBUSY on shutdown: chroot 8 vs no-chroot 8 (identical — not a chroot issue)
|
|
29
|
+
# - Persistence: both pass
|
|
30
|
+
# - LOC in entrypoint: chroot ~270 vs no-chroot ~165 (no-chroot saves ~100 LOC)
|
|
31
|
+
# - Path layout: chroot /workspace/root-chroot/root/... vs /workspace/home/...
|
|
32
|
+
# Subagent research confirmed chroot in our codebase solves only HOME-and-persistence
|
|
33
|
+
# layout, NOT library access (Linux dynamic linker is mount-agnostic per ld.so(8)).
|
|
34
|
+
# Since HOME=/workspace/home achieves the same persistence with ~100 fewer LOC and
|
|
35
|
+
# no bind-mount complexity, the chroot version was retired.
|
|
36
|
+
# Archive: docs/archive/Dockerfile.sandbox.chroot-2026-05-28.md
|
|
5
37
|
|
|
6
38
|
FROM node:22-slim
|
|
7
39
|
|
|
8
|
-
# Runtime deps
|
|
40
|
+
# Runtime deps (same as chroot variant — comes from image, image-swap upgrades)
|
|
9
41
|
RUN apt-get update -qq && \
|
|
10
42
|
apt-get install --no-install-recommends -y \
|
|
11
43
|
ca-certificates \
|
|
@@ -16,131 +48,137 @@ RUN apt-get update -qq && \
|
|
|
16
48
|
python-is-python3 && \
|
|
17
49
|
rm -rf /var/lib/apt/lists/*
|
|
18
50
|
|
|
19
|
-
# Pin osborn to an explicit version so Docker layer cache invalidates whenever
|
|
20
|
-
# the version changes. Earlier `RUN npm install -g osborn@latest` looked correct
|
|
21
|
-
# but it never invalidated: `latest` is a tag resolved by npm at install time,
|
|
22
|
-
# but Docker's layer cache keys on the literal RUN command string. So every
|
|
23
|
-
# rebuild reused the cached layer from when "latest" was a previous version,
|
|
24
|
-
# producing images labeled :0.9.21 that actually contained osborn 0.9.19 inside.
|
|
25
|
-
#
|
|
26
|
-
# Pass --build-arg OSBORN_VERSION=X.Y.Z when building. image-build-check.ts
|
|
27
|
-
# does this automatically using the npm-registry-resolved latest version.
|
|
28
51
|
ARG OSBORN_VERSION=latest
|
|
29
52
|
RUN npm install -g "osborn@${OSBORN_VERSION}" @anthropic-ai/claude-code
|
|
30
53
|
|
|
31
|
-
# Persistent
|
|
32
|
-
RUN mkdir -p /workspace
|
|
54
|
+
# Persistent volume mount point
|
|
55
|
+
RUN mkdir -p /workspace
|
|
33
56
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
RUN touch /etc/osborn-manifest-aware
|
|
57
|
+
# Markers (preserved for back-compat probes from machines.ts):
|
|
58
|
+
# /etc/osborn-manifest-aware — older marker (still here)
|
|
59
|
+
# /etc/osborn-option-d-aware — new marker; machines.ts can detect this variant
|
|
60
|
+
RUN touch /etc/osborn-manifest-aware /etc/osborn-option-d-aware
|
|
39
61
|
|
|
40
|
-
ENV OSBORN_CWD=/workspace
|
|
41
62
|
ENV OSBORN_API_PORT=8741
|
|
42
63
|
ENV NODE_ENV=production
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
64
|
+
ENV OSBORN_IMAGE_VERSION=${OSBORN_VERSION}
|
|
65
|
+
|
|
66
|
+
# THE KEY DIFFERENCE FROM CHROOT VARIANT:
|
|
67
|
+
# HOME and OSBORN_CWD are set as IMAGE-LEVEL ENV here. The entrypoint reads
|
|
68
|
+
# them, ensures the dirs exist on the volume, and execs osborn. No bind-mount
|
|
69
|
+
# trickery — HOME just IS on the volume because the path resolves to a volume
|
|
70
|
+
# subdir.
|
|
71
|
+
ENV HOME=/workspace/home
|
|
72
|
+
ENV OSBORN_CWD=/workspace
|
|
50
73
|
|
|
51
74
|
WORKDIR /workspace
|
|
52
|
-
|
|
53
75
|
EXPOSE 8741
|
|
54
76
|
|
|
55
|
-
# Entrypoint:
|
|
77
|
+
# Entrypoint: HOME-on-volume seed + onboarding + start
|
|
56
78
|
COPY <<'ENTRYPOINT' /entrypoint.sh
|
|
57
79
|
#!/bin/bash
|
|
58
80
|
set -e
|
|
59
81
|
|
|
60
|
-
# Persistent log capture
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# 404 error string was getting uploaded as "the log" for every session).
|
|
64
|
-
# Volume-backed /workspace/osborn.log survives reboots and is readable via
|
|
65
|
-
# the documented /exec endpoint (`tail -n 500 /workspace/osborn.log`).
|
|
66
|
-
#
|
|
67
|
-
# We use process substitution to tee output to BOTH the log file AND the
|
|
68
|
-
# original stdout (so `flyctl logs` keeps working for ad-hoc debugging).
|
|
69
|
-
# Requires bash, hence the #!/bin/bash shebang.
|
|
70
|
-
#
|
|
71
|
-
# Size cap: if log grows past 100 MB, keep only the last 50 MB. Prevents
|
|
72
|
-
# disk-fill from long-running retry loops (we saw 17h × ~1 line/min = ~1000
|
|
73
|
-
# lines today, but anything connecting to LiveKit produces orders of
|
|
74
|
-
# magnitude more output).
|
|
82
|
+
# === Persistent log capture ===
|
|
83
|
+
# Same as chroot variant: tee stdout/stderr to /workspace/osborn.log (volume).
|
|
84
|
+
# Rotates at 100MB → keeps last 50MB.
|
|
75
85
|
LOGFILE=/workspace/osborn.log
|
|
76
86
|
mkdir -p /workspace
|
|
77
87
|
if [ -f "$LOGFILE" ] && [ "$(stat -c%s "$LOGFILE" 2>/dev/null || echo 0)" -gt 104857600 ]; then
|
|
78
|
-
echo "[sandbox] Rotating /workspace/osborn.log (>100MB, keeping last 50MB)"
|
|
88
|
+
echo "[sandbox-d] Rotating /workspace/osborn.log (>100MB, keeping last 50MB)"
|
|
79
89
|
tail -c 52428800 "$LOGFILE" > "$LOGFILE.tmp" && mv "$LOGFILE.tmp" "$LOGFILE"
|
|
80
90
|
fi
|
|
81
|
-
echo "[sandbox] === boot at $(date -Iseconds) ===" >> "$LOGFILE"
|
|
82
|
-
# Redirect all subsequent stdout+stderr from this script (and the eventual
|
|
83
|
-
# `exec osborn`) to both the original fd (Fly stdout collector) AND the
|
|
84
|
-
# append-only log file. tee runs as a backgrounded subshell that survives
|
|
85
|
-
# the final exec replacement.
|
|
91
|
+
echo "[sandbox-d] === boot at $(date -Iseconds) ===" >> "$LOGFILE"
|
|
86
92
|
exec > >(tee -a "$LOGFILE") 2>&1
|
|
87
93
|
|
|
88
|
-
#
|
|
89
|
-
mkdir -p /workspace/.claude
|
|
90
|
-
rm -rf /root/.claude
|
|
91
|
-
ln -sf /workspace/.claude /root/.claude
|
|
92
|
-
|
|
93
|
-
# Suppress Claude Code interactive onboarding prompts
|
|
94
|
+
# Onboarding-suppression JSON
|
|
94
95
|
ONBOARDING_JSON='{"numStartups":10,"installMethod":"npm","autoUpdates":false,"hasCompletedOnboarding":true,"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true,"hasAcknowledgedCostThreshold":true,"effortCalloutV2Dismissed":true,"theme":"dark","projects":{"/workspace":{"hasTrustDialogAccepted":true,"hasTrustDialogHooksAccepted":true,"hasCompletedProjectOnboarding":true}}}'
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
echo "
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
96
|
+
|
|
97
|
+
# ============================================================
|
|
98
|
+
# === HOME-on-volume seed ===
|
|
99
|
+
# ============================================================
|
|
100
|
+
# HOME=/workspace/home, set via ENV in Dockerfile. Ensure the dir exists on
|
|
101
|
+
# the volume and seed Claude/onboarding/skills on first boot. Idempotent.
|
|
102
|
+
echo "[sandbox-d] HOME=$HOME OSBORN_CWD=$OSBORN_CWD"
|
|
103
|
+
mkdir -p "$HOME" "$HOME/.claude" "$HOME/.osborn"
|
|
104
|
+
|
|
105
|
+
# === Legacy migration ===
|
|
106
|
+
# Existing production sandboxes have credentials at /workspace/.claude/
|
|
107
|
+
# (the pre-D legacy symlink architecture). Migrate to HOME=/workspace/home/.claude/
|
|
108
|
+
# on first boot of the D image. Atomic mv — safe.
|
|
109
|
+
if [ -d /workspace/.claude ] && [ ! -d "$HOME/.claude/projects" ] && [ ! -f "$HOME/.claude/.credentials.json" ]; then
|
|
110
|
+
echo "[sandbox-d] migrating legacy /workspace/.claude → \$HOME/.claude"
|
|
111
|
+
# Move CONTENTS, not the dir itself (target may already exist with seeded skills)
|
|
112
|
+
for item in /workspace/.claude/.* /workspace/.claude/*; do
|
|
113
|
+
[ -e "$item" ] || continue
|
|
114
|
+
BASENAME=$(basename "$item")
|
|
115
|
+
[ "$BASENAME" = "." ] && continue
|
|
116
|
+
[ "$BASENAME" = ".." ] && continue
|
|
117
|
+
[ -e "$HOME/.claude/$BASENAME" ] && continue
|
|
118
|
+
mv "$item" "$HOME/.claude/$BASENAME" 2>/dev/null || true
|
|
119
|
+
done
|
|
120
|
+
rmdir /workspace/.claude 2>/dev/null || true
|
|
121
|
+
fi
|
|
122
|
+
if [ -f /workspace/.claude.json ] && [ ! -f "$HOME/.claude.json" ]; then
|
|
123
|
+
echo "[sandbox-d] migrating legacy /workspace/.claude.json → \$HOME/.claude.json"
|
|
124
|
+
mv /workspace/.claude.json "$HOME/.claude.json"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Onboarding config (overwrites every boot — intentional, deterministic state)
|
|
128
|
+
echo "$ONBOARDING_JSON" > "$HOME/.claude.json"
|
|
129
|
+
echo "$ONBOARDING_JSON" > "$HOME/.claude/.config.json"
|
|
130
|
+
echo "$ONBOARDING_JSON" > "$HOME/.claude/claude.json"
|
|
131
|
+
|
|
132
|
+
# Restore OAuth token if persisted
|
|
133
|
+
if [ -f "$HOME/.claude/.oauth-token" ]; then
|
|
134
|
+
export CLAUDE_CODE_OAUTH_TOKEN="$(cat "$HOME/.claude/.oauth-token")"
|
|
135
|
+
echo "[sandbox-d] restored CLAUDE_CODE_OAUTH_TOKEN from volume"
|
|
108
136
|
fi
|
|
109
137
|
|
|
110
|
-
# Seed default skills
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
HOME_SKILLS_DIR
|
|
115
|
-
PKG_SKILLS_DIR
|
|
138
|
+
# === Seed default skills with version-aware refresh (ported from chroot B6 fix) ===
|
|
139
|
+
# Same invariants as chroot variant:
|
|
140
|
+
# (1) USER-ADDED skills preserved across image upgrades
|
|
141
|
+
# (2) IMAGE-DEFAULT skills refreshed when image version changes
|
|
142
|
+
HOME_SKILLS_DIR="$HOME/.claude/skills"
|
|
143
|
+
PKG_SKILLS_DIR="/usr/local/lib/node_modules/osborn/.claude/skills"
|
|
144
|
+
SEED_VERSION_FILE="${HOME_SKILLS_DIR}/.seed-version"
|
|
116
145
|
mkdir -p "$HOME_SKILLS_DIR"
|
|
146
|
+
CURRENT_SEED_VERSION=$(cat "$SEED_VERSION_FILE" 2>/dev/null | tr -d '[:space:]' || echo "")
|
|
147
|
+
IMAGE_SEED_VERSION="${OSBORN_IMAGE_VERSION:-latest}"
|
|
117
148
|
if [ -d "$PKG_SKILLS_DIR" ]; then
|
|
149
|
+
REFRESHED=0
|
|
150
|
+
SEEDED=0
|
|
118
151
|
for d in "$PKG_SKILLS_DIR"/*/; do
|
|
119
152
|
[ -d "$d" ] || continue
|
|
120
153
|
NAME=$(basename "$d")
|
|
121
|
-
[ -d "$HOME_SKILLS_DIR/$NAME" ]
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
if [ ! -d "$HOME_SKILLS_DIR/$NAME" ]; then
|
|
155
|
+
cp -r "$d" "$HOME_SKILLS_DIR/$NAME"
|
|
156
|
+
echo "[sandbox-d] seeded default skill: $NAME"
|
|
157
|
+
SEEDED=$((SEEDED+1))
|
|
158
|
+
elif [ "$CURRENT_SEED_VERSION" != "$IMAGE_SEED_VERSION" ]; then
|
|
159
|
+
rm -rf "$HOME_SKILLS_DIR/$NAME"
|
|
160
|
+
cp -r "$d" "$HOME_SKILLS_DIR/$NAME"
|
|
161
|
+
echo "[sandbox-d] refreshed default skill: $NAME (image $CURRENT_SEED_VERSION → $IMAGE_SEED_VERSION)"
|
|
162
|
+
REFRESHED=$((REFRESHED+1))
|
|
163
|
+
fi
|
|
124
164
|
done
|
|
165
|
+
if [ "$CURRENT_SEED_VERSION" != "$IMAGE_SEED_VERSION" ]; then
|
|
166
|
+
echo "$IMAGE_SEED_VERSION" > "$SEED_VERSION_FILE"
|
|
167
|
+
[ "$REFRESHED" -gt 0 ] && echo "[sandbox-d] skills marker: $CURRENT_SEED_VERSION → $IMAGE_SEED_VERSION (refreshed $REFRESHED, seeded $SEEDED)"
|
|
168
|
+
fi
|
|
125
169
|
fi
|
|
126
170
|
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
WANT=$(cat /workspace/.osborn-want-version 2>/dev/null | tr -d '[:space:]')
|
|
136
|
-
if [ -n "$WANT" ]; then
|
|
137
|
-
CURRENT=$(osborn --version 2>/dev/null | head -1 | tr -d '[:space:]')
|
|
138
|
-
if [ "$WANT" != "$CURRENT" ]; then
|
|
139
|
-
echo "[sandbox] osborn ${CURRENT:-none} → ${WANT} (manifest install)"
|
|
140
|
-
npm install -g "osborn@${WANT}" || echo "[sandbox] install failed — running ${CURRENT:-image-baked} version"
|
|
141
|
-
fi
|
|
171
|
+
# Container-view session inventory (debugging)
|
|
172
|
+
if [ -d "$HOME/.claude/projects" ]; then
|
|
173
|
+
echo "[sandbox-d] Session inventory:"
|
|
174
|
+
for slug_dir in "$HOME/.claude/projects"/*/; do
|
|
175
|
+
[ -d "$slug_dir" ] || continue
|
|
176
|
+
count=$(find "$slug_dir" -maxdepth 1 -name '*.jsonl' 2>/dev/null | wc -l | tr -d ' ')
|
|
177
|
+
echo "[sandbox-d] $(basename "$slug_dir"): ${count} jsonl files"
|
|
178
|
+
done
|
|
142
179
|
fi
|
|
143
180
|
|
|
181
|
+
echo "[sandbox-d] exec'ing osborn (no chroot, HOME=$HOME)"
|
|
144
182
|
exec osborn
|
|
145
183
|
ENTRYPOINT
|
|
146
184
|
|
package/dist/claude-llm.js
CHANGED
|
@@ -211,6 +211,19 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
211
211
|
mcpServers: this.#mcpServers,
|
|
212
212
|
voiceMode: opts.voiceMode || 'realtime',
|
|
213
213
|
skipTTSQueue: opts.skipTTSQueue || false,
|
|
214
|
+
// CRITICAL: the PreCompact / PostCompact hooks call
|
|
215
|
+
// `this.#opts.onCompactionEvent?.(...)` to invoke the bridge to the
|
|
216
|
+
// frontend (chat-bubble + banner). Without including the callback in
|
|
217
|
+
// this whitelisted literal, callers can pass it correctly via opts but
|
|
218
|
+
// it's silently dropped during construction → hooks invoke undefined →
|
|
219
|
+
// no chat bubble appears. This was the real reason the compaction UI
|
|
220
|
+
// never showed up in 0.9.44–0.9.46 despite the wiring at every caller
|
|
221
|
+
// looking right. Confirmed 2026-05-28 by reading the live dist on Fly
|
|
222
|
+
// and seeing PreCompact/PostCompact emoji logs + the SDK iterator
|
|
223
|
+
// marker [COMPACT-SDK-ITER] firing while [COMPACT-AGENT-RX] never did.
|
|
224
|
+
// `onPermissionRequest` is handled separately via its own private field
|
|
225
|
+
// and does NOT need to be in this literal.
|
|
226
|
+
onCompactionEvent: opts.onCompactionEvent,
|
|
214
227
|
};
|
|
215
228
|
this.#eventEmitter = opts.eventEmitter || new EventEmitter();
|
|
216
229
|
console.log('🟠 ClaudeLLM initialized (Research Mode)');
|