@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.
- package/install.sh +253 -9
- package/package.json +25 -3
- package/pi-router/README.md +70 -0
- package/pi-router/package.json +13 -0
- package/pi-router/src/compaction.ts +26 -0
- package/pi-router/src/config.ts +261 -0
- package/pi-router/src/dispatch.ts +344 -0
- package/pi-router/src/index.ts +49 -0
- package/pi-router/src/metadata.ts +32 -0
- package/pi-router/src/provider.ts +51 -0
- package/pi-router/src/routed-model.ts +31 -0
- package/pi-router/src/safety.ts +66 -0
- package/uninstall.sh +114 -3
|
@@ -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
|
|
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
|