@workweave/router 0.1.7 → 0.1.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.
Files changed (2) hide show
  1. package/install.sh +90 -10
  2. package/package.json +1 -1
package/install.sh CHANGED
@@ -1035,6 +1035,19 @@ json_get() {
1035
1035
  jq -r "${2} // empty" "$1" 2>/dev/null || true
1036
1036
  }
1037
1037
 
1038
+ # claude_key_present returns 0 when the given settings file's
1039
+ # env.ANTHROPIC_CUSTOM_HEADERS carries the router key header. "On" is only valid
1040
+ # when this is true: in project scope the committed settings.json holds the
1041
+ # router URL but the key header lives only in the per-teammate settings.local.json
1042
+ # (or the parked sidecar), so a router URL alone doesn't mean requests can
1043
+ # authenticate.
1044
+ claude_key_present() {
1045
+ case "$(json_get "$1" '.env.ANTHROPIC_CUSTOM_HEADERS')" in
1046
+ *X-Weave-Router-Key*) return 0 ;;
1047
+ *) return 1 ;;
1048
+ esac
1049
+ }
1050
+
1038
1051
  # gitignore_add appends an entry to the repo .gitignore in project scope so a
1039
1052
  # parked sidecar (which may carry the router key header) never gets committed.
1040
1053
  # No-op for user scope and --dir, matching how install handles its own ignores.
@@ -1056,6 +1069,15 @@ toggle_claude() {
1056
1069
  else
1057
1070
  active="$settings_file"
1058
1071
  fi
1072
+ # Symlink containment for the parked sidecar: project/--dir paths come from a
1073
+ # possibly-hostile repo, and `off` writes $parked via shell redirection (which
1074
+ # follows symlinks). A repo could pre-place it as a symlink to clobber an
1075
+ # arbitrary file or siphon the router-key-bearing parked data. The config
1076
+ # files themselves are already guarded during path resolution; this covers the
1077
+ # sidecar. User scope ($HOME) is trusted, matching the installer.
1078
+ if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
1079
+ refuse_if_symlink "$parked"
1080
+ fi
1059
1081
  local parked_present="false"
1060
1082
  if [ -f "$parked" ]; then parked_present="true"; fi
1061
1083
  committed_base="$(json_get "$settings_file" '.env.ANTHROPIC_BASE_URL')"
@@ -1071,12 +1093,23 @@ toggle_claude() {
1071
1093
  if [ -n "$local_base" ] && ! router_shaped_url "$local_base"; then
1072
1094
  ok "Claude Code (project): ${C_BOLD}off${C_RESET} — routing directly to Anthropic. Run '$on_hint' to re-enable."
1073
1095
  elif router_shaped_url "$committed_base"; then
1074
- ok "Claude Code (project): ${C_BOLD}on${C_RESET} routing through $committed_base."
1096
+ # Router URL is committed, but it only authenticates if this teammate's
1097
+ # settings.local.json carries the key header. A fresh clone (shared
1098
+ # settings.json, no personal local file) has the URL but no key.
1099
+ if claude_key_present "$local_settings_file"; then
1100
+ ok "Claude Code (project): ${C_BOLD}on${C_RESET} — routing through $committed_base."
1101
+ else
1102
+ warn "Claude Code (project): router URL is set but your personal router key is missing (no settings.local.json) — requests won't authenticate. Run the installer to add your key."
1103
+ fi
1075
1104
  else
1076
1105
  info "Claude Code (project): not configured for the router. Run the installer first."
1077
1106
  fi
1078
1107
  elif router_shaped_url "$committed_base"; then
1079
- ok "Claude Code: ${C_BOLD}on${C_RESET} — routing through $committed_base."
1108
+ if claude_key_present "$settings_file"; then
1109
+ ok "Claude Code: ${C_BOLD}on${C_RESET} — routing through $committed_base."
1110
+ else
1111
+ warn "Claude Code: router URL is set but the router key header is missing — requests won't authenticate. Run the installer to restore it."
1112
+ fi
1080
1113
  else
1081
1114
  info "Claude Code: not configured for the router. Run the installer first."
1082
1115
  fi
@@ -1125,22 +1158,53 @@ toggle_claude() {
1125
1158
  if [ "$proj" = "true" ]; then
1126
1159
  local_base="$(json_get "$local_settings_file" '.env.ANTHROPIC_BASE_URL')"
1127
1160
  if [ -n "$local_base" ] && ! router_shaped_url "$local_base"; then
1128
- merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL)
1129
- | (if (.env // {} | length) == 0 then del(.env) else . end)' "$local_settings_file")"
1130
- printf '%s\n' "$merged" >"$local_settings_file"
1131
- chmod 600 "$local_settings_file"
1132
- ok "Claude Code is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
1161
+ # We're off, but the parked sidecar is gone. The router key header
1162
+ # lives only in the local file / sidecar in project scope never in
1163
+ # committed settings.json — so we can only re-enable cleanly if the
1164
+ # header survived in the local file. If it didn't, clearing the
1165
+ # override would point Claude Code at the router with no auth
1166
+ # (401s); leave the working direct setup in place and tell the user
1167
+ # to reinstall instead of faking success.
1168
+ if claude_key_present "$local_settings_file"; then
1169
+ merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL)
1170
+ | (if (.env // {} | length) == 0 then del(.env) else . end)' "$local_settings_file")"
1171
+ printf '%s\n' "$merged" >"$local_settings_file"
1172
+ chmod 600 "$local_settings_file"
1173
+ ok "Claude Code is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
1174
+ else
1175
+ warn "Claude Code is off and the parked router key is missing (its sidecar was deleted). Re-run the installer to restore the router key — leaving the current direct-to-Anthropic setup in place so requests don't fail auth."
1176
+ fi
1133
1177
  return 0
1134
1178
  fi
1135
1179
  fi
1136
- if router_shaped_url "$committed_base"; then
1180
+ # No direct override (or user scope). "On" requires both the router URL
1181
+ # and the key header — a committed router URL with no local key (e.g. a
1182
+ # fresh clone) can't authenticate, so don't claim it's already on.
1183
+ if ! router_shaped_url "$committed_base"; then
1184
+ warn "No parked router config found. Run the installer to set up Claude Code."
1185
+ elif claude_key_present "$active"; then
1137
1186
  ok "Claude Code is already on — nothing to do."
1138
1187
  else
1139
- warn "No parked router config found. Run the installer to set up Claude Code."
1188
+ # $active is settings.local.json in project scope, settings.json for
1189
+ # user/--dir — name the right file so the hint isn't misleading.
1190
+ warn "Router URL is set but the router key is missing — run the installer to add your key (written to $(basename "$active"))."
1140
1191
  fi
1141
1192
  return 0
1142
1193
  fi
1143
- parked_env="$(jq '.env' "$parked")"
1194
+ # Sidecar present: restore it — but only if the result actually carries
1195
+ # the router key. An off that ran with an empty/absent settings.local.json
1196
+ # parks {"env":{}}; blindly restoring that would drop the direct override
1197
+ # and leave the committed router URL unauthenticated while printing
1198
+ # success. Refuse that and tell the user to reinstall.
1199
+ parked_env="$(jq -c '.env // {}' "$parked")"
1200
+ parked_has_key="false"
1201
+ if printf '%s' "$parked_env" | jq -e '(.ANTHROPIC_CUSTOM_HEADERS // "") | test("X-Weave-Router-Key")' >/dev/null 2>&1; then
1202
+ parked_has_key="true"
1203
+ fi
1204
+ if [ "$parked_has_key" != "true" ] && ! claude_key_present "$active"; then
1205
+ warn "Can't re-enable: the parked config has no router key (it was created without one). Re-run the installer to set up your router key — leaving the current direct-to-Anthropic setup in place."
1206
+ return 0
1207
+ fi
1144
1208
  merged="$(jq --argjson p "$parked_env" '.env = (((.env // {}) | del(.ANTHROPIC_BASE_URL)) + $p)' "$active")"
1145
1209
  printf '%s\n' "$merged" >"$active"
1146
1210
  [ "$proj" = "true" ] && chmod 600 "$active"
@@ -1206,6 +1270,12 @@ toggle_codex() {
1206
1270
  toggle_opencode() {
1207
1271
  local f="$opencode_config_file" parked="$opencode_dir/.weave-parked.json"
1208
1272
  local model="" has_weave="false" parked_present="false" on="false" restore_model merged
1273
+ # Symlink containment for the parked sidecar — `off` writes it via shell
1274
+ # redirection; a hostile project repo could pre-place it as a symlink. The
1275
+ # opencode.json itself is already guarded during path resolution.
1276
+ if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
1277
+ refuse_if_symlink "$parked"
1278
+ fi
1209
1279
  if [ -f "$parked" ]; then parked_present="true"; fi
1210
1280
  if [ -f "$f" ]; then
1211
1281
  model="$(jq -r '.model // empty' "$f" 2>/dev/null || true)"
@@ -1243,6 +1313,16 @@ toggle_opencode() {
1243
1313
  restore_model="$(jq -r '.model // "weave/claude-sonnet-4-6"' "$parked")"
1244
1314
  elif [ "$has_weave" != "true" ]; then
1245
1315
  warn "opencode isn't configured for the router. Run the installer first."; return 0
1316
+ else
1317
+ # No parked model (sidecar deleted by hand). Derive the default from the
1318
+ # installed provider.weave.models block rather than a hardcoded literal
1319
+ # that silently diverges when the installer's default changes — prefer a
1320
+ # sonnet entry, else the first model the installer registered.
1321
+ restore_model="$(jq -r '
1322
+ (.provider.weave.models // {} | keys) as $k
1323
+ | (([$k[] | select(test("sonnet"))] | first) // $k[0] // "claude-sonnet-4-6")
1324
+ | "weave/" + .
1325
+ ' "$f" 2>/dev/null || echo "weave/claude-sonnet-4-6")"
1246
1326
  fi
1247
1327
  merged="$(jq --arg m "$restore_model" '.model = $m' "$f")"
1248
1328
  printf '%s\n' "$merged" >"$f"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workweave/router",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "One-command installer that points Claude Code at the Weave Router.",
5
5
  "bin": {
6
6
  "weave-router": "bin.js"