Skip to content

Commit f9558f6

Browse files
committed
Scrub residual cloud / claudeproxy references from container payload
Cleans up the dormant CLAUDEPROXY_SERVICE_TOKEN / x-service-auth / x-claude-proxy-bypass auth-header plumbing in power-steering.py (the OSS build talks to api.anthropic.com directly), and removes references to otel.back2base.net, memory-mcp-watcher, R2, and CLAUDEBOX_CLOUD_SYNC from Dockerfile / entrypoint.sh / CLAUDE.md.template comments. OTel becomes opt-in via BACK2BASE_OTEL_ENDPOINT with optional mTLS. go build / go test / python -m py_compile / bash -n all pass.
1 parent 902def7 commit f9558f6

5 files changed

Lines changed: 47 additions & 111 deletions

File tree

back2base-container/Dockerfile

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@ COPY --chown=node:node defaults/settings.json /opt/back2base/defaults/settings.j
1717
COPY --chown=node:node defaults/hooks.json /opt/back2base/defaults/hooks.json
1818

1919
# Copy default skills bundle into the image. Used by entrypoint's
20-
# seed_skills_if_missing as the offline first-install fallback when
21-
# CLAUDEBOX_CLOUD_SYNC is off OR the cloud is unreachable on first run.
20+
# seed_skills_if_missing as the first-install fallback.
2221
COPY --chown=node:node skills/ /opt/back2base/defaults/skills/
2322

24-
# Copy default slash commands. Seeded to ~/.claude/commands/ on first run;
25-
# may be replaced by cloud sync on subsequent starts.
23+
# Copy default slash commands. Seeded to ~/.claude/commands/ on first run.
2624
COPY --chown=node:node commands/ /opt/back2base/defaults/commands/
2725

2826
# Copy CLAUDE.md template. Assembled into ~/.claude/CLAUDE.md at every

back2base-container/defaults/CLAUDE.md.template

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ Auto-detected from the git remote of `/workspace`; falls back to `basename /work
114114
| Tool | What it is |
115115
|---|---|
116116
| `/fast` | Slash command — drops to Opus 4.6 for tight iteration; `/model claude-opus-4-7[1m]` to return |
117-
| `recall <query>` | Memory MCP tool — search past memories from R2 (the authoritative read path; the local `memory/` dir is write-only) |
118117
| `claude --version` | Confirm which Claude Code build the container shipped with |
119118

120119
## Prompt caching and cache blocks

back2base-container/entrypoint.sh

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,9 @@ if [ -d /repos ]; then
112112
fi
113113
fi
114114

115-
# Memory MCP namespace detection. The dockerized memory-mcp-proxy runs as a
116-
# sibling container and can't see /workspace, so it can't run its own
117-
# auto-detection (git remote → /repos scan → basename /workspace). Compute
118-
# the namespace once here and pass it through via -e MEMORY_NAMESPACE in
119-
# defaults/mcp.json. User-supplied MEMORY_NAMESPACE always wins.
115+
# Memory namespace detection. Auto-derived from the workspace identity:
116+
# git remote basename → /repos scan → basename /workspace. User-supplied
117+
# MEMORY_NAMESPACE always wins.
120118
# (Must stay in parent shell so MEMORY_NAMESPACE is exported for downstream.)
121119
if [ -z "${MEMORY_NAMESPACE:-}" ] && [ -d /workspace ]; then
122120
# WORKSPACE_NAME is the canonical project identity — set by the back2base
@@ -167,16 +165,15 @@ _phase2_workspace_setup() {
167165
# Two paths are involved and they MUST point at the same physical files:
168166
#
169167
# 1. The "namespaced" path under MEMORY_NAMESPACE (human-friendly, derived
170-
# from the git remote basename, e.g. ~/.claude/projects/back2base/memory).
171-
# The push/pull daemon and the cloud sync code operate on this path.
168+
# from the git remote basename, e.g. ~/.claude/projects/myrepo/memory).
172169
#
173170
# 2. Claude Code's auto-memory path, which it derives from the current
174171
# working directory by replacing every '/' with '-'
175172
# (e.g. /workspace → ~/.claude/projects/-workspace/memory). This is
176173
# where Claude Code reads MEMORY.md from at session start and writes
177174
# new memories to during the session.
178175
#
179-
# Without alignment, memories pulled from the cloud land in (1) but Claude
176+
# Without alignment, memories saved under (1) land outside what Claude
180177
# Code only reads (2) — they ships past each other and "memory doesn't
181178
# load". We make (1) the canonical store and symlink (2) → (1). If a user
182179
# already has real files at (2) from a previous version, migrate them into
@@ -281,9 +278,6 @@ fi
281278
# Seed user-state config files from image defaults. Only writes if the
282279
# target doesn't already exist — user customizations are preserved across
283280
# container runs. Defaults live inside the image at /opt/back2base/defaults/.
284-
# Runs AFTER cloud_sync has been reaped so we don't race on .mcp.json /
285-
# settings.json writes; if cloud_sync produced files, seed_if_missing is a
286-
# no-op.
287281
seed_if_missing() {
288282
local default_path="$1" target_path="$2"
289283
if [ ! -e "$target_path" ] && [ -e "$default_path" ]; then
@@ -297,8 +291,7 @@ mkdir -p "$HOME/.claude"
297291

298292
# ── Phase 2.5: Seeding settings + MCP defaults ──────────────────────────────
299293
# Runs BEFORE MCP profile filtering (phase 3) so the filter has a file to
300-
# operate on. seed_if_missing is a no-op when cloud_sync already wrote the
301-
# target; this is the offline fallback only.
294+
# operate on.
302295
_phase2_5_seed_settings() {
303296
seed_if_missing /opt/back2base/defaults/mcp.json "$HOME/.claude/.mcp.json"
304297
seed_if_missing /opt/back2base/defaults/settings.json "$HOME/.claude/settings.json"
@@ -408,12 +401,9 @@ pull_mcp_images() {
408401

409402
_phase "Pulling MCP server images" pull_mcp_images
410403

411-
# Skills now live directly inside the persistent state dir at
412-
# ~/.claude/skills. They are managed by:
413-
# 1. cloud_sync (above) — replaces them on every start when the cloud
414-
# is reachable AND BACK2BASE_CLOUD_SYNC=1
415-
# 2. seed_skills_if_missing (below) — first-install fallback that copies
416-
# from the image-baked default at /opt/back2base/defaults/skills/
404+
# Skills live directly inside the persistent state dir at ~/.claude/skills.
405+
# On first install, seed_skills_if_missing copies the image-baked defaults
406+
# from /opt/back2base/defaults/skills/. After that the user owns the tree.
417407
seed_skills_if_missing() {
418408
if [ ! -d "$HOME/.claude/skills" ] || [ -z "$(ls -A "$HOME/.claude/skills" 2>/dev/null)" ]; then
419409
if [ -d /opt/back2base/defaults/skills ]; then
@@ -428,8 +418,8 @@ seed_skills_if_missing() {
428418
fi
429419
}
430420

431-
# Commands live at ~/.claude/commands/ — seeded from image defaults the same
432-
# way skills are. cloud_sync may replace them; the seed is the offline fallback.
421+
# Commands live at ~/.claude/commands/ — seeded from image defaults on first
422+
# install, then user-owned.
433423
seed_commands_if_missing() {
434424
if [ ! -d "$HOME/.claude/commands" ] || [ -z "$(ls -A "$HOME/.claude/commands" 2>/dev/null)" ]; then
435425
if [ -d /opt/back2base/defaults/commands ]; then
@@ -459,8 +449,7 @@ seed_plugins_if_missing() {
459449

460450
# ── Phase 5: Seeding image defaults ─────────────────────────────────────────
461451
# Skills, commands, and plugins are seeded AFTER image pulls (phase 4) so we
462-
# don't race on directories that might be populated by cloud_sync or the
463-
# pulled images themselves.
452+
# don't race on directories that might be populated by the pulled images.
464453
_phase5_seed_image_defaults() {
465454
seed_skills_if_missing
466455
seed_commands_if_missing
Binary file not shown.

back2base-container/lib/power-steering.py

Lines changed: 33 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
that content as a system-reminder on the next user turn and truncates
1111
the file.
1212
13-
Designed as a sibling to memory-mcp-watcher and session-snapshot — same
14-
shape, same log convention. Stdlib only (no anthropic SDK dependency).
13+
Designed as a sibling to session-snapshot — same shape, same log
14+
convention. Stdlib only (no anthropic SDK dependency).
1515
1616
Configuration (env vars; sensible defaults):
1717
BACK2BASE_POWER_STEERING "off" / "0" / "false" disables the daemon
@@ -39,14 +39,8 @@
3939
4040
Auth (mirrors what Claude Code itself sends; daemon idles if no creds):
4141
42-
Layer 1 — back2base proxy identity (when ANTHROPIC_BASE_URL points
43-
at the proxy, which entrypoint.sh sets it to by default):
44-
CLAUDEPROXY_SERVICE_TOKEN → x-service-auth: <token>
45-
BACK2BASE_CONFIG_BYPASS_HEADER → x-claude-proxy-bypass: <value>
46-
Mandatory in proxy deployments — without x-service-auth the proxy
47-
returns 401 regardless of any Anthropic credential supplied.
48-
49-
Layer 2 — Anthropic credential (forwarded by the proxy in
42+
Anthropic credential (forwarded directly, since the OSS build has no
43+
proxy in front of api.anthropic.com):
5044
pass-through mode, or used directly when no proxy):
5145
ANTHROPIC_API_KEY → x-api-key: <key>
5246
ANTHROPIC_AUTH_TOKEN → Authorization: Bearer <token>
@@ -58,8 +52,7 @@
5852
Routing:
5953
ANTHROPIC_BASE_URL is honored. The host-side
6054
BACK2BASE_ANTHROPIC_BASE_URL is the only source — compose forwards
61-
it as the unprefixed name. entrypoint.sh defaults it to the
62-
back2base proxy URL when unset.
55+
it as the unprefixed name. Defaults to api.anthropic.com when unset.
6356
6457
Runtime overrides (hot-reloaded each poll):
6558
~/.claude/power-steering/config.json may set any of {model,
@@ -86,10 +79,8 @@
8679
import uuid
8780
from pathlib import Path
8881

89-
# Identifies this daemon to back2base's gateway-proxy worker, which
90-
# attaches it to the OTel span emitted for every supervisor /v1/messages
91-
# call. Bumped only when the daemon's request shape or telemetry schema
92-
# changes — not on every back2base release.
82+
# Identifies this daemon in OTel spans (when an endpoint is configured).
83+
# Bumped only when the daemon's request shape or telemetry schema changes.
9384
TELEMETRY_SOURCE = "power-steering"
9485
TELEMETRY_VERSION = "1"
9586

@@ -100,20 +91,12 @@
10091
SESSION_ID = (os.environ.get("BACK2BASE_TELEMETRY_SESSION_ID", "").strip()
10192
or uuid.uuid4().hex)
10293

103-
# OTel: ship spans to otel.back2base.net via OTLP/HTTP + mTLS. Cert + key
104-
# arrive in the image at /opt/back2base/certs/ from the release pipeline
105-
# (see .github/workflows/release.yml + back2base-container/certs/README.md).
106-
# Disabled gracefully when:
107-
# - opentelemetry packages aren't installed (older base image)
108-
# - cert/key files are missing (local builds without CI secrets)
109-
# - BACK2BASE_OTEL=off
110-
# Override paths for tests via BACK2BASE_OTEL_CERT / BACK2BASE_OTEL_KEY.
111-
OTEL_CERT_PATH = Path(
112-
os.environ.get("BACK2BASE_OTEL_CERT", "/opt/back2base/certs/client.cert.pem"),
113-
)
114-
OTEL_KEY_PATH = Path(
115-
os.environ.get("BACK2BASE_OTEL_KEY", "/opt/back2base/certs/client.key.pem"),
116-
)
94+
# OTel: optional. Set BACK2BASE_OTEL_ENDPOINT to an OTLP/HTTP collector URL
95+
# to enable. Optional mTLS via BACK2BASE_OTEL_CERT / BACK2BASE_OTEL_KEY.
96+
# Disabled gracefully when opentelemetry packages aren't installed, when
97+
# no endpoint is set, or when BACK2BASE_OTEL=off.
98+
OTEL_CERT_PATH = Path(os.environ.get("BACK2BASE_OTEL_CERT", ""))
99+
OTEL_KEY_PATH = Path(os.environ.get("BACK2BASE_OTEL_KEY", ""))
117100
OTEL_ENDPOINT = os.environ.get("BACK2BASE_OTEL_ENDPOINT", "")
118101
_TELEMETRY_INITIALIZED = False
119102

@@ -172,9 +155,8 @@ def init_telemetry():
172155
if not OTEL_ENDPOINT:
173156
log("otel: no BACK2BASE_OTEL_ENDPOINT set; telemetry disabled")
174157
return False
175-
if not OTEL_CERT_PATH.exists() or not OTEL_KEY_PATH.exists():
176-
log(f"otel: cert/key missing under {OTEL_CERT_PATH.parent}; telemetry disabled")
177-
return False
158+
mtls_enabled = bool(str(OTEL_CERT_PATH)) and bool(str(OTEL_KEY_PATH)) \
159+
and OTEL_CERT_PATH.exists() and OTEL_KEY_PATH.exists()
178160
try:
179161
from opentelemetry import trace as otel_trace
180162
from opentelemetry.sdk.resources import Resource
@@ -187,15 +169,14 @@ def init_telemetry():
187169
log(f"otel: SDK import failed ({e}); telemetry disabled")
188170
return False
189171
try:
190-
exporter = OTLPSpanExporter(
191-
endpoint=f"{OTEL_ENDPOINT.rstrip('/')}/v1/traces",
192-
client_certificate_file=str(OTEL_CERT_PATH),
193-
client_key_file=str(OTEL_KEY_PATH),
194-
)
172+
exporter_kwargs = {"endpoint": f"{OTEL_ENDPOINT.rstrip('/')}/v1/traces"}
173+
if mtls_enabled:
174+
exporter_kwargs["client_certificate_file"] = str(OTEL_CERT_PATH)
175+
exporter_kwargs["client_key_file"] = str(OTEL_KEY_PATH)
176+
exporter = OTLPSpanExporter(**exporter_kwargs)
195177
resource = Resource.create({
196178
"service.name": TELEMETRY_SOURCE,
197179
"service.version": TELEMETRY_VERSION,
198-
"back2base.source": TELEMETRY_SOURCE,
199180
"back2base.session": SESSION_ID,
200181
"back2base.user_id": os.environ.get("MEMORY_USER_ID", ""),
201182
})
@@ -386,44 +367,22 @@ def build_request_target():
386367
rides the exact same auth path the user's interactive session uses.
387368
Two layers, stacked when both are available:
388369
389-
1. back2base proxy identity (when ANTHROPIC_BASE_URL points at the
390-
proxy — which entrypoint.sh sets it to by default):
391-
x-service-auth: <CLAUDEPROXY_SERVICE_TOKEN>
392-
x-claude-proxy-bypass: <BACK2BASE_CONFIG_BYPASS_HEADER or "back2base">
393-
The proxy worker rejects /v1/messages without `x-service-auth`,
394-
so this layer is mandatory in proxy deployments.
395-
396-
2. Anthropic credential (forwarded as-is by the proxy in
397-
pass-through mode, or used directly when ANTHROPIC_BASE_URL is
398-
unset). Probed in order:
399-
ANTHROPIC_API_KEY → x-api-key
400-
ANTHROPIC_AUTH_TOKEN → Authorization: Bearer
401-
CLAUDE_CODE_OAUTH_TOKEN → Authorization: Bearer
402-
Each name is read from the container's process env (the
403-
unprefixed form), populated by docker-compose from the matching
404-
BACK2BASE_<NAME> host-side variable. The BACK2BASE_ prefix is
405-
the only source the host honors.
406-
407-
Returns (url, headers, mode) on success or (None, None, None) if
408-
no auth is configured. `mode` is "proxy" when x-service-auth is
409-
sent, "direct" otherwise — informational only, used for logging.
370+
Anthropic credential probed in order:
371+
ANTHROPIC_API_KEY → x-api-key
372+
ANTHROPIC_AUTH_TOKEN → Authorization: Bearer
373+
CLAUDE_CODE_OAUTH_TOKEN → Authorization: Bearer
374+
Each name is read from the container's process env (the unprefixed
375+
form), populated by docker-compose from the matching BACK2BASE_<NAME>
376+
host-side variable.
377+
378+
Returns (url, headers, mode) on success or (None, None, None) if no
379+
auth is configured. `mode` is always "direct" in the OSS build.
410380
"""
411381
base = (_env_with_prefix("ANTHROPIC_BASE_URL")
412382
or DEFAULT_API_BASE).rstrip("/")
413383
url = f"{base}/v1/messages"
414384
headers = {}
415385

416-
# Layer 1: back2base proxy identity.
417-
svc = _env_with_prefix("CLAUDEPROXY_SERVICE_TOKEN")
418-
if svc:
419-
headers["x-service-auth"] = svc
420-
# BACK2BASE_CONFIG_BYPASS_HEADER is back2base-owned (no upstream
421-
# counterpart), so a plain getenv is correct.
422-
bypass = os.environ.get("BACK2BASE_CONFIG_BYPASS_HEADER", "back2base").strip()
423-
if bypass:
424-
headers["x-claude-proxy-bypass"] = bypass
425-
426-
# Layer 2: Anthropic credential.
427386
api_key = _env_with_prefix("ANTHROPIC_API_KEY")
428387
if api_key:
429388
headers["x-api-key"] = api_key
@@ -436,16 +395,7 @@ def build_request_target():
436395
if not headers:
437396
return None, None, None
438397

439-
# Layer 3: telemetry identity. Read by the gateway-proxy worker and
440-
# attached to the OTel span it emits for every /v1/messages call.
441-
# Set unconditionally — even on auth misconfiguration we want the
442-
# 4xx visible in observability tagged correctly.
443-
headers["x-back2base-source"] = TELEMETRY_SOURCE
444-
headers["x-back2base-version"] = TELEMETRY_VERSION
445-
headers["x-back2base-session"] = SESSION_ID
446-
447-
mode = "proxy" if "x-service-auth" in headers else "direct"
448-
return url, headers, mode
398+
return url, headers, "direct"
449399

450400

451401
# --- logging ---------------------------------------------------------
@@ -1139,8 +1089,8 @@ def main():
11391089
return
11401090
url, headers, mode = build_request_target()
11411091
if headers is None:
1142-
log("no auth env var set (CLAUDEPROXY_SERVICE_TOKEN / ANTHROPIC_API_KEY / "
1143-
"ANTHROPIC_AUTH_TOKEN / CLAUDE_CODE_OAUTH_TOKEN); daemon idle")
1092+
log("no auth env var set (ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / "
1093+
"CLAUDE_CODE_OAUTH_TOKEN); daemon idle")
11441094
return
11451095
if not NS:
11461096
log("MEMORY_NAMESPACE not set; daemon idle")

0 commit comments

Comments
 (0)