Compare commits

..
4 Commits
Author SHA1 Message Date
c4ch3c4d3 0f3fb06377 Ignore OpenChamber automatic config backups 2026-10-06 10:50:42 -06:00
c4ch3c4d3 c3954d1533 Break enterprise policy out into openchamber-policy.json
- New --enterprise <policy.json> flag installs a policy file from the
  cloned repo (openchamber-policy.json) instead of the built-in defaults
- --relay-url overrides the source file's relayUrl key; matching relay
  installs the file content verbatim
- Invalid or missing policy source fails closed with a clear error
- Arg parsing extracted into a testable parse_args function
2026-10-06 10:49:54 -06:00
c4ch3c4d3 778341b7a5 Fix review findings: fail-closed merge, die ordering, sudo-path idempotency
- merge_policies: interpreter failure now aborts with the temp file
  removed instead of moving an empty tmp into place (errexit is
  suppressed when the function is called in a || context, so the
  status is checked explicitly); config stays untouched on failure
- move info/ok/warn/die above arg parsing so --allow-provider without
  a value prints the intended error, not 'die: command not found'
- install_enterprise_policy: compare content before writing on the
  sudo path (skip when already enforced, warn on drift), repair the
  no-root-with-existing-file case instead of pinning env vars, chmod
  644 the policy file in both paths
- tests: in-place merge, fail-closed on bad JSON, clean arg error
2026-10-06 10:45:56 -06:00
c4ch3c4d3 c926361ccc Add enterprise mode + provider policy enforcement to bootstrap script
- Install /etc/openchamber/policy.json (enterpriseMode, organization,
  pinned relayUrl); sudo with OPENCHAMBER_ENTERPRISE_MODE=1 env fallback;
  --no-enterprise opt-out
- Inject OpenCode provider policies into the synced config: deny all
  providers, allow those derived from the config's own providers block,
  BASELINE_ALLOWED_PROVIDERS (openai via codex-auth plugin), and
  repeatable --allow-provider extras (e.g. constellation)
- Extend verification: policy file state, pinned relay, effective
  provider allow-list
- Restructure into sourceable functions with a main guard so the policy
  and merge logic is testable
2026-10-06 10:43:46 -06:00
3 changed files with 297 additions and 18 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# OpenChamber automatic config backups
*.openchamber.backup
+290 -18
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env bash
# openchamber-prereqs.sh — bootstrap a Linux host for opencode + openchamber
#
# Intent: run this once on a fresh Linux host, then connect from the
@@ -5,7 +6,8 @@
# and inherits the pinned environment. No UI password is used.
#
# Run as the user who will own the install. Idempotent: safe to re-run,
# every step checks before acting. No root required.
# every step checks before acting. No root required (enterprise policy
# file install uses sudo when available; see step 6).
#
# What it does:
# 1. Installs bun (if missing)
@@ -14,38 +16,271 @@
# non-interactive SSH sessions (OpenChamber's exec path) inherit them
# 4. Installs opencode (if missing)
# 5. Installs/updates @openchamber/web to latest via bun (every run)
# 6. Syncs ~/.config/opencode/opencode.json from CONFIG_URL
# 7. Verifies: versions + relay health
# 6. Installs the enterprise policy file (/etc/openchamber/policy.json):
# enterprise mode + organization + pinned relay. Pass
# --enterprise <file> to install your own policy (e.g.
# openchamber-policy.json from this repo); otherwise built-in
# defaults are used. --relay-url overrides the file's relayUrl.
# Without root it falls back to OPENCHAMBER_ENTERPRISE_MODE=1 in
# the rc files (weaker lock — the user can edit those). Skip with
# --no-enterprise.
# 7. Syncs ~/.config/opencode/opencode.json from CONFIG_URL and injects
# OpenCode provider policies: deny every provider, then allow the
# ones derived from the config's own providers, the baseline list
# (BASELINE_ALLOWED_PROVIDERS, for plugin-registered providers like
# openai), and any --allow-provider extras.
# 8. Verifies: versions + relay health + policy state
#
# Flags: --relay-url wss://host/ws (override the default relay endpoint)
# --invite [port] print an openchamber:// invite link (+QR)
# for importing this host into the desktop
# app; starts the instance on <port>
# (default 3000) if none is running
# --allow-provider <id> allow an extra model provider in the
# OpenCode policy (repeatable), e.g.
# --allow-provider constellation
# --enterprise <policy.json> install this file as the OpenChamber
# enterprise policy (e.g. the
# openchamber-policy.json in this repo);
# without it, built-in defaults apply
# --no-enterprise do not install the enterprise policy
# file / env pin
set -euo pipefail
RELAY_URL="wss://code.zuccaro.me/ws"
CONFIG_URL="https://git.zuccaro.me/bzuccaro/opencode-public/raw/branch/main/opencode.json"
ORGANIZATION="Zuccaro"
POLICY_FILE="/etc/openchamber/policy.json"
# Providers allowed in addition to those defined in the synced config.
# "openai" comes from the opencode-openai-codex-auth plugin, which never
# appears in the config's providers block.
BASELINE_ALLOWED_PROVIDERS="openai"
INVITE=0
INVITE_PORT=3000
while [ $# -gt 0 ]; do
case "$1" in
--relay-url) RELAY_URL="$2"; shift 2 ;;
--invite)
INVITE=1
if [ $# -ge 2 ] && [ "${2#--}" = "$2" ]; then INVITE_PORT="$2"; shift; fi
shift ;;
*) echo "unknown arg: $1 (usage: $0 [--relay-url wss://host/ws] [--invite [port]])" >&2; exit 1 ;;
esac
done
NO_ENTERPRISE=0
ALLOW_PROVIDERS_STR=""
POLICY_SOURCE=""
info() { printf '\033[1;34m[info]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[ ok ]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2; exit 1; }
# parse_args "$@" — set INVITE/INVITE_PORT/NO_ENTERPRISE/ALLOW_PROVIDERS_STR/
# POLICY_SOURCE/RELAY_URL from the command line.
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--relay-url) [ $# -ge 2 ] || die "--relay-url needs a value"; RELAY_URL="$2"; shift 2 ;;
--invite)
INVITE=1
if [ $# -ge 2 ] && [ "${2#--}" = "$2" ]; then INVITE_PORT="$2"; shift; fi
shift ;;
--allow-provider)
[ $# -ge 2 ] || die "--allow-provider needs a provider id"
ALLOW_PROVIDERS_STR="$ALLOW_PROVIDERS_STR $2"
shift 2 ;;
--enterprise)
[ $# -ge 2 ] || die "--enterprise needs a policy file path"
[ -r "$2" ] || die "enterprise policy file not found: $2"
POLICY_SOURCE="$2"
shift 2 ;;
--no-enterprise) NO_ENTERPRISE=1; shift ;;
*) echo "unknown arg: $1 (usage: $0 [--relay-url wss://host/ws] [--invite [port]] [--allow-provider <id>] [--enterprise <policy.json>] [--no-enterprise])" >&2; exit 1 ;;
esac
done
}
# ---- helpers ----------------------------------------------------------------
# policy_json — emit the enterprise policy file content on stdout.
# With POLICY_SOURCE set: the file is the source of truth, except --relay-url
# overrides its relayUrl key. Without it: built-in defaults.
policy_json() {
if [ -z "$POLICY_SOURCE" ]; then
printf '{\n "enterpriseMode": true,\n "organization": "%s",\n "relayUrl": "%s"\n}\n' \
"$ORGANIZATION" "$RELAY_URL"
return 0
fi
local tmp
tmp="$(mktemp)"
if command -v python3 >/dev/null 2>&1; then
python3 - "$POLICY_SOURCE" "$tmp" "$RELAY_URL" <<'PY' || { rm -f "$tmp"; die "invalid JSON in enterprise policy file: $POLICY_SOURCE"; }
import json, sys
src, dst, relay = sys.argv[1], sys.argv[2], sys.argv[3]
p = json.load(open(src))
if relay and p.get("relayUrl") != relay:
p["relayUrl"] = relay
with open(dst, "w") as f:
json.dump(p, f, indent=2)
f.write("\n")
PY
elif command -v bun >/dev/null 2>&1; then
IN="$POLICY_SOURCE" OUT="$tmp" RELAY="$RELAY_URL" bun -e '
const fs = require("fs");
const p = JSON.parse(fs.readFileSync(process.env.IN, "utf8"));
if (process.env.RELAY && p.relayUrl !== process.env.RELAY) p.relayUrl = process.env.RELAY;
fs.writeFileSync(process.env.OUT, JSON.stringify(p, null, 2) + "\n");
' || { rm -f "$tmp"; die "invalid JSON in enterprise policy file: $POLICY_SOURCE"; }
else
die "need python3 or bun to read the enterprise policy file"
fi
cat "$tmp"
rm -f "$tmp"
}
# write_policy_file <path> — idempotently install the enterprise policy file
# at <path>, which must already be writable. Reports what it did on stdout.
write_policy_file() {
local f="$1" tmp
tmp="$(mktemp)"
policy_json > "$tmp"
if [ -f "$f" ] && cmp -s "$tmp" "$f"; then
rm -f "$tmp"
ok "$(basename "$f"): already enforced"
return 0
fi
if [ -f "$f" ]; then
warn "$(basename "$f"): differs from enforced policy — overwriting"
fi
mv "$tmp" "$f"
chmod 644 "$f"
ok "$(basename "$f"): installed"
}
# install_enterprise_policy — put the policy at /etc/openchamber/policy.json
# (sudo when available); without root, pin OPENCHAMBER_ENTERPRISE_MODE=1 in
# the rc files instead and say so loudly.
install_enterprise_policy() {
local dir sudo="" tmp current
dir="$(dirname "$POLICY_FILE")"
tmp="$(mktemp)"
policy_json > "$tmp"
# Already correct? Then we're done regardless of root.
if [ -r "$POLICY_FILE" ]; then
current="$(cat "$POLICY_FILE" 2>/dev/null || true)"
if [ "$current" = "$(cat "$tmp")" ]; then
rm -f "$tmp"
ok "$(basename "$POLICY_FILE"): already enforced"
return 0
fi
fi
if [ -d "$dir" ] && [ -w "$dir" ]; then
write_policy_file "$POLICY_FILE"
rm -f "$tmp"
return 0
fi
if sudo -n true 2>/dev/null; then
sudo="sudo -n"
elif [ -t 0 ]; then
sudo="sudo"
fi
if [ -n "$sudo" ]; then
if [ -f "$POLICY_FILE" ]; then
warn "$(basename "$POLICY_FILE"): differs from enforced policy — overwriting"
fi
if $sudo mkdir -p "$dir" 2>/dev/null && $sudo tee "$POLICY_FILE" >/dev/null < "$tmp" && $sudo chmod 644 "$POLICY_FILE" 2>/dev/null; then
rm -f "$tmp"
ok "enterprise policy installed at $POLICY_FILE"
return 0
fi
fi
rm -f "$tmp"
if [ -f "$POLICY_FILE" ]; then
warn "no root: $POLICY_FILE exists but differs from enforced policy and could not be repaired"
return 1
fi
warn "no root: cannot write $POLICY_FILE — falling back to env-var mode"
warn "env-var mode is a weaker lock (the user can edit their own rc files)"
pin_enterprise_env
}
# pin_enterprise_env — pin OPENCHAMBER_ENTERPRISE_MODE=1 like the relay var
pin_enterprise_env() {
local line='export OPENCHAMBER_ENTERPRISE_MODE=1'
if grep -q "OPENCHAMBER_ENTERPRISE_MODE" ~/.bashrc 2>/dev/null; then
ok ".bashrc: enterprise mode already pinned"
else
pin_top ~/.bashrc "$line"
fi
pin_append ~/.zshenv "$line"
pin_append ~/.zshrc "$line"
pin_append ~/.profile "$line"
}
# merge_policies <in> <out> [extra provider ids...] — inject the OpenCode
# provider policy into a config: deny every provider, then allow those
# derived from the config's own providers block, the baseline list, and any
# extras (deduped, in that order). Uses python3, falls back to bun.
merge_policies() {
local in="$1" out="$2" tmp status=0
local extras="${*:3}"
tmp="$(mktemp)"
if command -v python3 >/dev/null 2>&1; then
python3 - "$in" "$tmp" "$BASELINE_ALLOWED_PROVIDERS" "$extras" <<'PY' || status=1
import json, sys
src, dst, baseline, extras = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
cfg = json.load(open(src))
derived = list((cfg.get("providers") or {}).keys())
rules = [{"action": "provider.use", "resource": "*", "effect": "deny"}]
seen = []
for p in derived + [b.strip() for b in baseline.split(",") if b.strip()] + extras.split():
if p and p not in seen:
seen.append(p)
rules.append({"action": "provider.use", "resource": p, "effect": "allow"})
cfg.setdefault("experimental", {})["policies"] = rules
with open(dst, "w") as f:
json.dump(cfg, f, indent=2)
f.write("\n")
PY
elif command -v bun >/dev/null 2>&1; then
IN="$in" OUT="$tmp" BASELINE="$BASELINE_ALLOWED_PROVIDERS" EXTRAS="$extras" bun -e '
const fs = require("fs");
const cfg = JSON.parse(fs.readFileSync(process.env.IN, "utf8"));
const derived = Object.keys(cfg.providers || {});
const rules = [{ action: "provider.use", resource: "*", effect: "deny" }];
const seen = [];
for (const p of [...derived,
...process.env.BASELINE.split(",").map(s => s.trim()).filter(Boolean),
...process.env.EXTRAS.split(/\s+/).filter(Boolean)]) {
if (p && !seen.includes(p)) { seen.push(p); rules.push({ action: "provider.use", resource: p, effect: "allow" }); }
}
cfg.experimental = cfg.experimental || {};
cfg.experimental.policies = rules;
fs.writeFileSync(process.env.OUT, JSON.stringify(cfg, null, 2) + "\n");
' || status=1
else
die "need python3 or bun to inject provider policies"
fi
if [ "$status" -ne 0 ]; then
rm -f "$tmp"
die "could not inject provider policies into $in (bad JSON or interpreter failure) — existing config left untouched"
fi
mv "$tmp" "$out"
}
# allowed_providers_from <config.json> — print the allow-listed provider ids
# from a config's experimental.policies as a comma-separated list, or "?" if
# the file can't be read/parsed.
allowed_providers_from() {
python3 -c '
import json,sys
try:
cfg=json.load(open(sys.argv[1]))
print(",".join(r["resource"] for r in cfg.get("experimental",{}).get("policies",[]) if r.get("effect")=="allow"))
except Exception:
print("?")
' "$1" 2>/dev/null
}
# ---- main -------------------------------------------------------------------
main() {
parse_args "$@"
info "host: $(uname -srm)"
[ "$(uname -s)" = "Linux" ] || die "this script targets Linux hosts"
@@ -121,11 +356,20 @@ info "installing/updating @openchamber/web to latest..."
bun install -g @openchamber/web@latest
ok "openchamber version: $(openchamber --version 2>/dev/null || echo '?')"
# ---- 6. opencode config sync ------------------------------------------------
# ---- 6. enterprise policy --------------------------------------------------
if [ "$NO_ENTERPRISE" = "1" ]; then
info "skipping enterprise policy (--no-enterprise)"
else
install_enterprise_policy
fi
# ---- 7. opencode config sync + provider policies ---------------------------
CFG_DIR="$HOME/.config/opencode"
CFG_FILE="$CFG_DIR/opencode.json"
TMP_CFG="$(mktemp)"
curl -fsSL --max-time 60 "$CONFIG_URL" -o "$TMP_CFG" || die "could not download config from $CONFIG_URL"
info "injecting provider policies (deny all; allow derived + baseline:${ALLOW_PROVIDERS_STR:+ extras:$ALLOW_PROVIDERS_STR})"
merge_policies "$TMP_CFG" "$TMP_CFG"
if [ -f "$CFG_FILE" ] && cmp -s "$TMP_CFG" "$CFG_FILE"; then
ok "opencode config already up to date"
rm -f "$TMP_CFG"
@@ -137,7 +381,7 @@ else
info "existing config backed up to $(basename "$BAK")"
fi
mv "$TMP_CFG" "$CFG_FILE"
ok "opencode config installed from CONFIG_URL"
ok "opencode config installed from CONFIG_URL (with provider policies)"
fi
if command -v python3 >/dev/null 2>&1; then
python3 -m json.tool "$CFG_FILE" >/dev/null && ok "config is valid JSON"
@@ -145,7 +389,7 @@ elif command -v bun >/dev/null 2>&1; then
bun -e "JSON.parse(require('fs').readFileSync('$CFG_FILE','utf8'))" && ok "config is valid JSON"
fi
# ---- 7. verify --------------------------------------------------------------
# ---- 8. verify --------------------------------------------------------------
echo
info "==== verification ===="
echo "bun: $(bun --version 2>/dev/null || echo MISSING)"
@@ -153,6 +397,28 @@ echo "node shim: $(env node -e 'console.log("exec-ok")' 2>/dev/null || echo BRO
echo "opencode: $(opencode --version 2>/dev/null || echo MISSING)"
echo "openchamber:$(openchamber --version 2>/dev/null || echo MISSING)"
if [ -f "$POLICY_FILE" ]; then
if command -v python3 >/dev/null 2>&1; then
if python3 -m json.tool "$POLICY_FILE" >/dev/null 2>&1; then
ok "enterprise policy: $POLICY_FILE (valid JSON, relay pinned: $(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("relayUrl","?"))' "$POLICY_FILE" 2>/dev/null || echo '?'))"
else
warn "enterprise policy: $POLICY_FILE is not valid JSON — OpenChamber stays in enterprise mode but pins nothing"
fi
else
ok "enterprise policy: $POLICY_FILE present"
fi
elif grep -q "OPENCHAMBER_ENTERPRISE_MODE" ~/.bashrc 2>/dev/null; then
warn "enterprise mode via env var only (weaker lock) — no policy file at $POLICY_FILE"
else
warn "enterprise mode not configured (skipped with --no-enterprise?)"
fi
if command -v python3 >/dev/null 2>&1; then
echo "allowed providers: $(allowed_providers_from "$CFG_FILE")"
else
warn "cannot read back provider allow-list (no python3)"
fi
HEALTH_URL="$(printf '%s' "$RELAY_URL" | sed 's|^wss://|https://|; s|/ws$|/health|')"
if curl -fsS --max-time 15 "$HEALTH_URL" >/dev/null 2>&1; then
ok "relay reachable: $HEALTH_URL"
@@ -160,7 +426,7 @@ else
warn "relay health check failed: $HEALTH_URL (may not be deployed yet)"
fi
# ---- 8. invite link (optional) ----------------------------------------------
# ---- 9. invite link (optional) ----------------------------------------------
if [ "$INVITE" = "1" ]; then
info "generating invite link (relay transport embedded)..."
if openchamber connect-url --relay -p "$INVITE_PORT" --qr; then
@@ -172,3 +438,9 @@ fi
echo
ok "prerequisites complete"
}
# Sourceable for testing: only run when executed directly.
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi
+5
View File
@@ -0,0 +1,5 @@
{
"enterpriseMode": true,
"organization": "Zuccaro",
"relayUrl": "wss://code.zuccaro.me/ws"
}