@drakon-systems/multi-clawd 1.5.2 → 1.5.4

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/README.md CHANGED
@@ -188,7 +188,7 @@ openclaw plugins install (Get-Location).Path
188
188
  **Or let your agent install it.** Running an OpenClaw assistant or Claude
189
189
  Code on the target machine already? Paste it this and go make coffee:
190
190
 
191
- > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.5.2/SETUP-AGENT.md
191
+ > Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.5.4/SETUP-AGENT.md
192
192
  > and follow it to set up multi-clawd on this machine. I own a second
193
193
  > Claude account — ask me when you need me to log in.
194
194
 
@@ -451,6 +451,10 @@ event, and no plugin API can force a rebuild. See `DESIGN.md`.
451
451
 
452
452
  ## Security
453
453
 
454
+ See **[SECURITY.md](SECURITY.md)** for how to report a vulnerability, exactly
455
+ how credentials are handled, and a table documenting every process-spawn
456
+ callsite (the ones static scanners flag) and why each exists.
457
+
454
458
  **Full declaration of what this plugin touches — nothing else:**
455
459
 
456
460
  | Surface | What multi-clawd does |
package/SECURITY.md ADDED
@@ -0,0 +1,82 @@
1
+ # Security
2
+
3
+ multi-clawd handles Claude Code login credentials. That makes it worth
4
+ explaining exactly what it touches, and worth telling you how to report a
5
+ problem.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Open a [security advisory](https://github.com/Drakon-Systems-Ltd/multi-clawd/security/advisories/new)
10
+ on the repository, or email **security@drakonsystems.com**. Please do not open
11
+ a public issue for anything exploitable.
12
+
13
+ We aim to acknowledge within 72 hours. If a fix is warranted we will ship it,
14
+ credit you (unless you'd rather we didn't), and note it in the CHANGELOG.
15
+
16
+ ## How credentials are handled
17
+
18
+ - **Tokens are never logged and never appear in a command line.** They are
19
+ passed to the Claude CLI child process through its environment only, so they
20
+ cannot leak into a process list, a shell history, or a log file.
21
+ - **Secret references are preferred over token files.** `oauthTokenRef` uses
22
+ the same `{source, provider, id}` shape as the rest of `openclaw.json` and
23
+ resolves through the gateway's own configured secret providers (1Password,
24
+ etc.), so the credential never sits in plaintext on disk.
25
+ - **Secret-resolution failures are redacted to a fixed reason code.** The
26
+ message that reaches your logs contains neither the token, nor the
27
+ provider/id being resolved, nor the provider's own exception text — only the
28
+ error class, for debuggability.
29
+ - **Plaintext token files are checked, not trusted.** If you use the legacy
30
+ `oauthTokenFile`, multi-clawd warns (once per process) when the file is
31
+ readable beyond your own user account. It warns rather than refuses: the
32
+ credential still works, and the fix is yours to make — `chmod 600`.
33
+ - **Host credentials are stripped from child processes.** 22 Claude/Anthropic
34
+ environment variables are cleared before each launch, so one account's
35
+ credential cannot bleed into another account's session.
36
+ - **State files are written `0600`,** atomically (temp file + rename), and
37
+ contain rate-limit telemetry only — never credentials.
38
+
39
+ ## Permissions
40
+
41
+ multi-clawd never grants itself more permission than OpenClaw core grants the
42
+ bundled `claude-cli` backend. It appends `--permission-mode bypassPermissions`
43
+ **only** when your own config already sets `tools.exec.mode: "full"` — which is
44
+ what core does for the bundled backend. Under any other exec mode it passes
45
+ nothing and Claude keeps its default prompt-honouring behaviour.
46
+
47
+ ## Why static scanners flag this package
48
+
49
+ Automated scans flag multi-clawd for `dangerous_exec`. That is expected and
50
+ correct: starting Claude Code processes *is* the product. Every such callsite
51
+ is listed here so you can check them yourself.
52
+
53
+ | Location | What it runs | Why |
54
+ | --- | --- | --- |
55
+ | `dist/shim.js` | the `claude` binary | The core function: launches Claude Code for the selected account. Argv is built from a fixed base list plus your configured model; no shell. |
56
+ | `dist/watchdog-schedule.js` | `node <bundled script>` | Runs the eviction watchdog on a timer. Path is the package's own script. |
57
+ | `scripts/cli.mjs` | `node <bundled script>` | The CLI dispatching to its own subcommands. |
58
+ | `scripts/setup.mjs` | `launchctl` / `systemctl` | Loads the watchdog timer during setup on macOS/Linux. |
59
+
60
+ None of these pass a string to a shell — they are direct process spawns with
61
+ argument arrays, so there is no quoting or injection surface. Paths are
62
+ package-internal or derived from your home directory, never from remote input.
63
+
64
+ ## Things you should decide for yourself
65
+
66
+ - **The watchdog is persistence.** `setup` schedules a user-level systemd timer
67
+ (or launchd agent) that can restart the OpenClaw gateway. It is optional —
68
+ skip it if you don't want that.
69
+ - **The setup wizard edits `~/.openclaw/openclaw.json`.** It backs the file up
70
+ first and merges rather than overwrites, and `--dry-run` shows you the
71
+ changes without writing. Use it.
72
+ - **The agent-install one-liner is pinned to a release tag,** not `master`, so
73
+ what your assistant reads and executes is fixed at a version you chose.
74
+ Inspect `SETUP-AGENT.md` before pointing an agent at it — that advice holds
75
+ for anyone's setup guide, including ours.
76
+
77
+ ## Scope
78
+
79
+ multi-clawd runs entirely on your machine against your own Claude
80
+ subscriptions. It has no server component, no telemetry, and **zero runtime
81
+ dependencies** — the only third-party code in the install is whatever OpenClaw
82
+ itself already provides.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "multi-clawd",
3
3
  "name": "multi-clawd",
4
- "version": "1.5.2",
4
+ "version": "1.5.4",
5
5
  "description": "Register additional Claude Code logins (Max/Pro accounts) as first-class OpenClaw CLI backends for cross-account failover, keeping the full skills/MCP harness on every account.",
6
6
  "cliBackends": [
7
7
  "claw1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakon-systems/multi-clawd",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Multi-account Claude Code failover for OpenClaw — register additional Claude (Max/Pro) logins as first-class CLI backends and keep the full skills/MCP harness across every account.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -46,6 +46,7 @@
46
46
  "scripts",
47
47
  "openclaw.plugin.json",
48
48
  "README.md",
49
+ "SECURITY.md",
49
50
  "LICENSE"
50
51
  ],
51
52
  "scripts": {
@@ -182,6 +182,9 @@ console.log("account credentials");
182
182
  const { checkAccountCredential } = await import(join(EXT_DIR, "dist", "login-health.js")).catch(
183
183
  () => import(join(REPO_DIR, "dist", "login-health.js")),
184
184
  );
185
+ const { summarizeWindowUsage } = await import(join(EXT_DIR, "dist", "health.js")).catch(() =>
186
+ import(join(REPO_DIR, "dist", "health.js")),
187
+ );
185
188
  const io = {
186
189
  readFile: (p) => readFileSync(expandHome(p), "utf8"),
187
190
  keychainHasClaudeCredentials: () => {
@@ -218,10 +221,32 @@ for (const account of accounts) {
218
221
  continue;
219
222
  }
220
223
  const ageMin = Math.round((Date.now() - (state.updatedAt ?? 0)) / 60000);
224
+ // A window whose own reset has PASSED describes the previous cycle, and the
225
+ // rotation logic already voids it ("a passed reset voids the observation",
226
+ // health.ts). Doctor used to print it raw beside a fresh `(0m old)` stamp,
227
+ // so a five-day-dead `seven_day@96%` read as "96% used right now" when
228
+ // nothing was acting on it. The age stamp is the age of the OBSERVATION,
229
+ // never the window's validity.
230
+ //
231
+ // `summarizeWindowUsage` is the same function `explain` uses, so all three
232
+ // surfaces — rotation, explain, doctor — agree on what counts as live
233
+ // rather than each carrying its own copy of the rule.
234
+ const live = new Set(
235
+ summarizeWindowUsage(state, { staleAfterMs: pluginConfig.pool?.staleAfterMs }, Date.now()).map(
236
+ (u) => u.window,
237
+ ),
238
+ );
221
239
  const windows = Object.entries(state.windows ?? {})
222
- .map(([w, d]) => `${w}:${d.status}${typeof d.utilization === "number" ? `@${Math.round(d.utilization * 100)}%` : ""}`)
240
+ .map(([w, d]) => {
241
+ const hasUtil = typeof d.utilization === "number";
242
+ const util = hasUtil ? `@${Math.round(d.utilization * 100)}%` : "";
243
+ // Only utilization-bearing account windows are summarised; a window
244
+ // carrying a number that did NOT survive is one the pool ignores.
245
+ const stale = hasUtil && !w.startsWith("model:") && !live.has(w);
246
+ return `${w}:${d.status}${util}${stale ? " (expired — ignored)" : ""}`;
247
+ })
223
248
  .join(" ");
224
- ok(`${account.id}: ${windows || "no windows"} (${ageMin}m old)`);
249
+ ok(`${account.id}: ${windows || "no windows"} (observed ${ageMin}m ago)`);
225
250
  }
226
251
 
227
252
  // ── 6. pool ─────────────────────────────────────────────────────────────────