@workweave/router 0.1.8 → 0.2.1

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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Registers the `weave` provider with the per-process header policy.
3
+ *
4
+ * Re-registered on each `session_start` (and once at load) so the right knob
5
+ * headers are always live and the provider survives `/reload`. We register a
6
+ * new provider named "weave" rather than overriding the built-in "anthropic"
7
+ * provider — overriding "anthropic" would hijack the Claude OAuth token.
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import {
12
+ getRole,
13
+ getRouterBaseUrl,
14
+ isSubagent,
15
+ PROVIDER_NAME,
16
+ providerHeaders,
17
+ resolveRouterKey,
18
+ WEAVE_MODELS,
19
+ } from "./config.js";
20
+
21
+ export function registerWeave(pi: ExtensionAPI): void {
22
+ const key = resolveRouterKey();
23
+ const role = getRole();
24
+
25
+ if (!key) {
26
+ // The main loop can still run off the installer-written models.json
27
+ // provider. A subagent MUST apply the speed/cheap knobs, so a missing
28
+ // key there is fatal rather than silently routing on quality knobs.
29
+ if (isSubagent()) {
30
+ throw new Error(
31
+ "Weave: no router key found (set WEAVE_ROUTER_KEY or write ~/.pi/agent/.weave_router_key).",
32
+ );
33
+ }
34
+ return;
35
+ }
36
+
37
+ pi.registerProvider(PROVIDER_NAME, {
38
+ name: "Weave Router",
39
+ // Root URL, no /v1: the anthropic-messages provider uses @anthropic-ai/sdk,
40
+ // which appends /v1/messages to baseUrl. A /v1 here yields /v1/v1/messages.
41
+ baseUrl: getRouterBaseUrl(),
42
+ // Planted to satisfy pi's "is auth configured" check. The router ignores
43
+ // it (auth runs off X-Weave-Router-Key); authHeader:false keeps
44
+ // Authorization free for BYOK.
45
+ apiKey: key,
46
+ api: "anthropic-messages",
47
+ authHeader: false,
48
+ headers: providerHeaders(role, key),
49
+ models: WEAVE_MODELS,
50
+ });
51
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Surfaces which model the router actually picked for each request.
3
+ *
4
+ * The router sets `x-router-model` on every response (streaming, non-streaming,
5
+ * and cache hits). In the interactive UI we show it in the status bar and
6
+ * notify on change. In a headless child (print/RPC — e.g. a dispatch subagent)
7
+ * there is no UI, so we print a marker to stderr that the parent dispatch tool
8
+ * parses to attribute each subagent's work to a model.
9
+ */
10
+
11
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
+ import { ROUTED_MODEL_HEADER, ROUTED_MODEL_STDERR_PREFIX } from "./config.js";
13
+
14
+ const STATUS_KEY = "weave";
15
+
16
+ export function registerRoutedModel(pi: ExtensionAPI): void {
17
+ let last: string | undefined;
18
+
19
+ pi.on("after_provider_response", (event, ctx: ExtensionContext) => {
20
+ const model = event.headers?.[ROUTED_MODEL_HEADER];
21
+ if (!model || model === last) return;
22
+ last = model;
23
+
24
+ if (ctx.hasUI) {
25
+ ctx.ui.setStatus(STATUS_KEY, `routed: ${model}`);
26
+ ctx.ui.notify(`Weave routed to ${model}`, "info");
27
+ } else {
28
+ process.stderr.write(`${ROUTED_MODEL_STDERR_PREFIX} ${model}\n`);
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Last-resort guard against a handful of catastrophic, effectively
3
+ * irreversible shell commands. pi already confirms normal tool calls; this
4
+ * matters most in non-interactive subagents where there is no human to
5
+ * confirm. It blocks ONLY the obviously-destructive forms below — it is a
6
+ * backstop, not a sandbox. Disable with WEAVE_NO_SAFETY=1.
7
+ */
8
+
9
+ import { type ExtensionAPI, type ExtensionContext, isToolCallEventType, type ToolCallEvent } from "@mariozechner/pi-coding-agent";
10
+
11
+ interface Rule {
12
+ test: (cmd: string) => boolean;
13
+ reason: string;
14
+ }
15
+
16
+ const hasRecursive = (c: string) => /(?:\s|^)-\w*r/i.test(c) || /--recursive\b/.test(c);
17
+ const hasForce = (c: string) => /(?:\s|^)-\w*f/i.test(c) || /--force\b/.test(c);
18
+ const targetsRoot = (c: string) =>
19
+ /--no-preserve-root\b/.test(c) || /(?:\s|^)(?:\/|~|\$HOME|\/\*)(?:\s|$)/.test(c);
20
+
21
+ const RULES: Rule[] = [
22
+ {
23
+ test: (c) => /\brm\b/.test(c) && hasRecursive(c) && hasForce(c) && targetsRoot(c),
24
+ reason: "recursive force-remove of a root/home path",
25
+ },
26
+ {
27
+ test: (c) => /\bmkfs(\.\w+)?\b/.test(c),
28
+ reason: "filesystem format (mkfs)",
29
+ },
30
+ {
31
+ test: (c) => /\bdd\b[^\n]*\bof=\/dev\//.test(c),
32
+ reason: "dd writing directly to a device",
33
+ },
34
+ {
35
+ test: (c) => />\s*\/dev\/(?:sd|nvme|disk|hd|mapper)/.test(c),
36
+ reason: "redirect overwriting a raw block device",
37
+ },
38
+ {
39
+ test: (c) => /:\s*\(\s*\)\s*\{[^}]*\|[^}]*&[^}]*\}\s*;\s*:/.test(c),
40
+ reason: "fork bomb",
41
+ },
42
+ {
43
+ test: (c) =>
44
+ /\bgit\s+push\b/.test(c) &&
45
+ (/(?:--force(?!-with-lease)|\s-f\b)/.test(c) || /\+(?:main|master)\b/.test(c)) &&
46
+ /\b(?:main|master)\b/.test(c),
47
+ reason: "force-push to main/master",
48
+ },
49
+ ];
50
+
51
+ function catastrophicReason(command: string): string | undefined {
52
+ const cmd = command.trim();
53
+ for (const rule of RULES) {
54
+ if (rule.test(cmd)) return rule.reason;
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ export function registerSafety(pi: ExtensionAPI): void {
60
+ pi.on("tool_call", (event: ToolCallEvent, _ctx: ExtensionContext) => {
61
+ if (!isToolCallEventType("bash", event)) return undefined;
62
+ const reason = catastrophicReason(event.input.command ?? "");
63
+ if (!reason) return undefined;
64
+ return { block: true, reason: `Weave safety: blocked ${reason}. Set WEAVE_NO_SAFETY=1 to override.` };
65
+ });
66
+ }
package/uninstall.sh CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # Weave Router uninstaller for Claude Code, Codex, and opencode.
3
+ # Weave Router uninstaller for Claude Code, Codex, opencode, and pi.
4
4
  #
5
5
  # Default target is Claude Code: removes the env vars, statusLine, and local
6
6
  # router auth that install.sh added; leaves the rest of settings.json
@@ -9,12 +9,15 @@
9
9
  # config.toml — anything outside the markers is preserved. Pass --opencode
10
10
  # to strip the `provider.weave` block (and the top-level `model` key when
11
11
  # it points at the router) from opencode.json; other providers and user
12
- # settings are preserved.
12
+ # settings are preserved. Pass --pi to strip the `weave` provider from
13
+ # pi's models.json, drop @workweave/router from settings.json, revert the
14
+ # weave defaults, and remove the router key file.
13
15
  #
14
16
  # Usage:
15
17
  # npx @workweave/router --uninstall # Claude Code, user scope
16
18
  # npx @workweave/router --uninstall --codex # Codex, user scope
17
19
  # npx @workweave/router --uninstall --opencode # opencode, user scope
20
+ # npx @workweave/router --uninstall --pi # pi, user scope
18
21
  # npx @workweave/router --uninstall --scope project # run inside the repo
19
22
  # npx @workweave/router --uninstall --dir /tmp/test # --dir alone (user scope, .weave/)
20
23
  # npx @workweave/router --uninstall --scope project --dir /tmp # --dir + project scope (.claude/)
@@ -59,6 +62,9 @@ while [ $# -gt 0 ]; do
59
62
  --opencode)
60
63
  target="opencode"; shift
61
64
  ;;
65
+ --pi)
66
+ target="pi"; shift
67
+ ;;
62
68
  --claude)
63
69
  # No-op selector for symmetry with --codex / --opencode and install.sh's
64
70
  # --claude. Lets `./install.sh --uninstall --claude` (which forwards
@@ -77,7 +83,7 @@ while [ $# -gt 0 ]; do
77
83
  esac
78
84
  done
79
85
 
80
- if { [ "$target" = "claude" ] || [ "$target" = "opencode" ]; } && ! command -v jq >/dev/null 2>&1; then
86
+ if { [ "$target" = "claude" ] || [ "$target" = "opencode" ] || [ "$target" = "pi" ]; } && ! command -v jq >/dev/null 2>&1; then
81
87
  err "jq is required for the $target uninstall path."
82
88
  exit 1
83
89
  fi
@@ -189,6 +195,111 @@ if [ "$target" = "opencode" ]; then
189
195
  exit 0
190
196
  fi
191
197
 
198
+ # ---------- pi uninstall path ----------
199
+
200
+ if [ "$target" = "pi" ]; then
201
+ # Resolve the pi agent dir based on scope/dir. Mirrors install.sh: user scope
202
+ # is pi's default ~/.pi/agent; project/--dir scope is a repo-local .pi.
203
+ if [ -n "$install_dir" ]; then
204
+ install_dir="$(cd "$install_dir" 2>/dev/null && pwd || echo "$install_dir")"
205
+ pi_dir="$install_dir/.pi"
206
+ refuse_if_symlink "$pi_dir"
207
+ elif [ "$scope" = "user" ]; then
208
+ pi_dir="$HOME/.pi/agent"
209
+ else
210
+ # Project scope: same prompt + git-root fallback as the opencode path.
211
+ project_dir=""
212
+ if [ "$scope_explicit" = "false" ] && [ -r /dev/tty ]; then
213
+ default_project_dir="$(pwd)"
214
+ printf "Project directory to uninstall from [default: %s]: " "$default_project_dir"
215
+ read -r project_dir_choice </dev/tty || project_dir_choice=""
216
+ project_dir="${project_dir_choice:-$default_project_dir}"
217
+ case "$project_dir" in
218
+ "~") project_dir="$HOME" ;;
219
+ "~/"*) project_dir="$HOME/${project_dir#~/}" ;;
220
+ esac
221
+ if [ ! -d "$project_dir" ]; then
222
+ err "directory does not exist: $project_dir"
223
+ exit 1
224
+ fi
225
+ project_dir="$(cd "$project_dir" && pwd)"
226
+ fi
227
+ if [ -n "${project_dir:-}" ]; then
228
+ pi_dir="$project_dir/.pi"
229
+ else
230
+ if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
231
+ err "--scope project must be run inside a git repo, or use --dir <path>."
232
+ exit 1
233
+ fi
234
+ pi_dir="$git_root/.pi"
235
+ fi
236
+ refuse_if_symlink "$pi_dir"
237
+ fi
238
+
239
+ pi_models_file="$pi_dir/models.json"
240
+ pi_settings_file="$pi_dir/settings.json"
241
+ pi_key_file="$pi_dir/.weave_router_key"
242
+ refuse_if_symlink "$pi_models_file"
243
+ refuse_if_symlink "$pi_settings_file"
244
+ refuse_if_symlink "$pi_key_file"
245
+
246
+ # models.json: drop provider.weave; remove the file if nothing else remains.
247
+ # Other providers/models the user added are preserved.
248
+ if [ -f "$pi_models_file" ]; then
249
+ cleaned="$(jq '
250
+ (if .providers.weave then del(.providers.weave) else . end)
251
+ | (if (.providers // {}) == {} then del(.providers) else . end)
252
+ ' "$pi_models_file")"
253
+ printf '%s\n' "$cleaned" >"$pi_models_file"
254
+ if [ "$(jq -r 'keys | length' "$pi_models_file" 2>/dev/null || echo 0)" = "0" ]; then
255
+ rm -f "$pi_models_file"
256
+ ok "Removed empty $pi_models_file"
257
+ else
258
+ ok "Cleaned $pi_models_file"
259
+ fi
260
+ else
261
+ info "No pi models config at $pi_models_file (already uninstalled?)"
262
+ fi
263
+
264
+ # settings.json: drop our package and revert defaults that still point at the
265
+ # router. Leaving defaultProvider="weave" after removing the provider would
266
+ # break pi startup, so reverting is the correct reverse of the install.
267
+ # defaultModel is reverted ONLY when defaultProvider was "weave" (the state
268
+ # install creates): install sets defaultModel only when it was empty, so a user
269
+ # who independently picked claude-sonnet-4-6 with their own provider keeps it.
270
+ if [ -f "$pi_settings_file" ]; then
271
+ cleaned="$(jq '
272
+ (if .packages then .packages -= ["npm:@workweave/router", "npm:@workweave/pi-router"] else . end)
273
+ | (if (.packages // []) == [] then del(.packages) else . end)
274
+ | (if .defaultProvider == "weave"
275
+ then del(.defaultProvider)
276
+ | (if .defaultModel == "claude-sonnet-4-6" then del(.defaultModel) else . end)
277
+ else . end)
278
+ ' "$pi_settings_file")"
279
+ printf '%s\n' "$cleaned" >"$pi_settings_file"
280
+ if [ "$(jq -r 'keys | length' "$pi_settings_file" 2>/dev/null || echo 0)" = "0" ]; then
281
+ rm -f "$pi_settings_file"
282
+ ok "Removed empty $pi_settings_file"
283
+ else
284
+ ok "Cleaned $pi_settings_file"
285
+ fi
286
+ else
287
+ info "No pi settings at $pi_settings_file (already uninstalled?)"
288
+ fi
289
+
290
+ if [ -f "$pi_key_file" ]; then
291
+ rm -f "$pi_key_file"
292
+ ok "Removed $pi_key_file"
293
+ fi
294
+
295
+ if [ -n "$install_dir" ]; then
296
+ ok "Weave Router uninstalled from $install_dir (pi)."
297
+ else
298
+ ok "Weave Router uninstalled (pi, scope=$scope)."
299
+ fi
300
+ exit 0
301
+ fi
302
+
192
303
  # ---------- codex uninstall path ----------
193
304
 
194
305
  if [ "$target" = "codex" ]; then