@drakon-systems/multi-clawd 1.8.1 → 1.8.2
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 +11 -4
- package/SECURITY.md +4 -3
- package/dist/index.js +17 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/scripts/cli.mjs +0 -0
- package/scripts/hermes_bridge.py +27 -1
package/README.md
CHANGED
|
@@ -73,7 +73,10 @@ multi-clawd hermes sync --profile work --strategy least_used
|
|
|
73
73
|
multi-clawd hermes doctor --config ~/configs/openclaw.json
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
Validated against Hermes Agent **0.
|
|
76
|
+
Validated against Hermes Agent **0.20.6**. The bridge/core/CLI integration
|
|
77
|
+
tests exercise that installed release's real Python APIs in isolated temporary
|
|
78
|
+
homes; on machines without a compatible Hermes install, those integration tests
|
|
79
|
+
skip while the no-Hermes CLI safety tests still run.
|
|
77
80
|
|
|
78
81
|
### Only stable setup tokens are imported
|
|
79
82
|
|
|
@@ -94,7 +97,7 @@ Everything else is refused, on purpose:
|
|
|
94
97
|
on that source instead of duplicating the grant.
|
|
95
98
|
- **`configDir` logins are not importable, and Hermes cannot be pointed at
|
|
96
99
|
them either.** The same single-use-refresh-token problem applies, and unlike
|
|
97
|
-
a native login there is no Hermes-side fallback: as of Hermes Agent 0.
|
|
100
|
+
a native login there is no Hermes-side fallback: as of Hermes Agent 0.20.6,
|
|
98
101
|
its `claude_code` credential source reads only the native path above, never
|
|
99
102
|
an arbitrary `configDir`. A `configDir` account can only reach Hermes' pool
|
|
100
103
|
by getting its own `oauthTokenFile` (a `claude setup-token`, same as above),
|
|
@@ -364,7 +367,7 @@ openclaw plugins install (Get-Location).Path
|
|
|
364
367
|
**Or let your agent install it.** Running an OpenClaw assistant or Claude
|
|
365
368
|
Code on the target machine already? Paste it this and go make coffee:
|
|
366
369
|
|
|
367
|
-
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.
|
|
370
|
+
> Read https://raw.githubusercontent.com/Drakon-Systems-Ltd/multi-clawd/v1.8.2/SETUP-AGENT.md
|
|
368
371
|
> and follow it to set up multi-clawd on this machine. I own a second
|
|
369
372
|
> Claude account — ask me when you need me to log in.
|
|
370
373
|
|
|
@@ -377,7 +380,11 @@ happens to say today. Swap the tag if you want a different release, but
|
|
|
377
380
|
prefer a tag over a branch.
|
|
378
381
|
|
|
379
382
|
**Requirements:** OpenClaw ≥ 2026.6, the `claude` CLI on `PATH`, and a
|
|
380
|
-
second Claude subscription you own.
|
|
383
|
+
second Claude subscription you own. The current plugin SDK contract and runtime
|
|
384
|
+
registration path are tested against OpenClaw **2026.7.1**. The standalone
|
|
385
|
+
`multi-clawd` CLI and Hermes commands do not require the optional OpenClaw peer
|
|
386
|
+
to be resolvable; plugin loading and OpenClaw-backed commands still require the
|
|
387
|
+
host-provided peer.
|
|
381
388
|
|
|
382
389
|
**Upgrading:**
|
|
383
390
|
|
package/SECURITY.md
CHANGED
|
@@ -30,9 +30,10 @@ credit you (unless you'd rather we didn't), and note it in the CHANGELOG.
|
|
|
30
30
|
`oauthTokenFile`, multi-clawd warns (once per process) when the file is
|
|
31
31
|
readable beyond your own user account. It warns rather than refuses: the
|
|
32
32
|
credential still works, and the fix is yours to make — `chmod 600`.
|
|
33
|
-
- **Host credentials are stripped from child processes.**
|
|
34
|
-
environment variables
|
|
35
|
-
|
|
33
|
+
- **Host credentials are stripped from child processes.** 38 Claude/Anthropic
|
|
34
|
+
environment variables covering authentication, runtime, and telemetry are
|
|
35
|
+
cleared before each launch, so one account's credential cannot bleed into
|
|
36
|
+
another account's session.
|
|
36
37
|
- **State files are written `0600`,** atomically (temp file + rename), and
|
|
37
38
|
contain rate-limit telemetry only — never credentials.
|
|
38
39
|
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ const BASE_ARGS = [
|
|
|
34
34
|
"--disallowedTools",
|
|
35
35
|
"ScheduleWakeup,CronCreate,Bash(run_in_background:true),Monitor",
|
|
36
36
|
];
|
|
37
|
-
const CLEAR_ENV = [
|
|
37
|
+
export const CLEAR_ENV = [
|
|
38
38
|
"ANTHROPIC_API_KEY",
|
|
39
39
|
"ANTHROPIC_API_KEY_OLD",
|
|
40
40
|
"ANTHROPIC_API_TOKEN",
|
|
@@ -57,6 +57,22 @@ const CLEAR_ENV = [
|
|
|
57
57
|
"CLAUDE_CODE_USE_BEDROCK",
|
|
58
58
|
"CLAUDE_CODE_USE_FOUNDRY",
|
|
59
59
|
"CLAUDE_CODE_USE_VERTEX",
|
|
60
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
61
|
+
"OTEL_EXPORTER_OTLP_HEADERS",
|
|
62
|
+
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
|
63
|
+
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
|
|
64
|
+
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL",
|
|
65
|
+
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
|
|
66
|
+
"OTEL_EXPORTER_OTLP_METRICS_HEADERS",
|
|
67
|
+
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL",
|
|
68
|
+
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
|
69
|
+
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
70
|
+
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
|
|
71
|
+
"OTEL_EXPORTER_OTLP_TRACES_PROTOCOL",
|
|
72
|
+
"OTEL_LOGS_EXPORTER",
|
|
73
|
+
"OTEL_METRICS_EXPORTER",
|
|
74
|
+
"OTEL_SDK_DISABLED",
|
|
75
|
+
"OTEL_TRACES_EXPORTER",
|
|
60
76
|
];
|
|
61
77
|
function expandHome(p) {
|
|
62
78
|
if (p === "~")
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "multi-clawd",
|
|
3
3
|
"name": "multi-clawd",
|
|
4
|
-
"version": "1.8.
|
|
4
|
+
"version": "1.8.2",
|
|
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.8.
|
|
3
|
+
"version": "1.8.2",
|
|
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. Also imports those accounts' setup tokens into Hermes Agent's Anthropic credential pool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"openclaw": {
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.
|
|
24
|
+
"openclawVersion": "2026.7.1"
|
|
25
25
|
},
|
|
26
26
|
"extensions": [
|
|
27
27
|
"./src/index.ts"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"main": "./dist/index.js",
|
|
38
38
|
"bin": {
|
|
39
|
-
"multi-clawd": "
|
|
39
|
+
"multi-clawd": "scripts/cli.mjs"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
package/scripts/cli.mjs
CHANGED
|
File without changes
|
package/scripts/hermes_bridge.py
CHANGED
|
@@ -10,7 +10,7 @@ grant (a native or config-dir ``.credentials.json``) is single-use on refresh,
|
|
|
10
10
|
so duplicating one into a second store guarantees that one of the copies dies.
|
|
11
11
|
For a *native* login that copy is unnecessary anyway: Hermes' own
|
|
12
12
|
``claude_code`` credential source already reads that exact file directly. A
|
|
13
|
-
*config-dir* login has no such fallback — as of Hermes Agent 0.
|
|
13
|
+
*config-dir* login has no such fallback — as of Hermes Agent 0.20.6,
|
|
14
14
|
``claude_code`` only reads the native path, never an arbitrary config dir — so
|
|
15
15
|
it can only reach this bridge via its own setup token, never a duplicated
|
|
16
16
|
grant. Requests carrying refresh tokens or expiries are refused outright.
|
|
@@ -60,6 +60,24 @@ ACCOUNT_ID_ALPHABET = frozenset("abcdefghijklmnopqrstuvwxyz0123456789_-")
|
|
|
60
60
|
# Cleared whenever a managed row is written: a setup token never expires on a
|
|
61
61
|
# schedule, so a stale expiry copied from an older row would quarantine it.
|
|
62
62
|
STALE_EXPIRY_FIELDS = ("expires_at", "expires_at_ms", "last_refresh")
|
|
63
|
+
# Hermes' own runtime bookkeeping (agent/credential_pool.py PooledCredential).
|
|
64
|
+
# Cleared ONLY when the access token itself changes: Hermes' selection
|
|
65
|
+
# (CredentialPool._available_entries) skips an entry whose last_status is
|
|
66
|
+
# "exhausted" or "dead" until last_error_reset_at passes, which can be hours
|
|
67
|
+
# or days out. A rotated-in token proves the row is being actively managed
|
|
68
|
+
# again, so carrying the old verdict forward would leave a freshly-rotated
|
|
69
|
+
# credential unselectable until that window lapses on its own. A
|
|
70
|
+
# metadata-only update (label/priority) with the SAME token is not evidence
|
|
71
|
+
# of anything — Hermes' own telemetry for a healthy pool must survive it
|
|
72
|
+
# untouched.
|
|
73
|
+
STATUS_FIELDS = (
|
|
74
|
+
"last_status",
|
|
75
|
+
"last_status_at",
|
|
76
|
+
"last_error_code",
|
|
77
|
+
"last_error_reason",
|
|
78
|
+
"last_error_message",
|
|
79
|
+
"last_error_reset_at",
|
|
80
|
+
)
|
|
63
81
|
# Mirrors src/hermes-core.ts's parseClaudeSetupToken: ASCII-only, and shaped
|
|
64
82
|
# like the current `sk-ant-oat01-...` setup-token family. The version digits
|
|
65
83
|
# are intentionally unconstrained beyond "two or more" so a future
|
|
@@ -443,7 +461,12 @@ def merge_credential(existing: dict[str, Any], credential: DesiredCredential) ->
|
|
|
443
461
|
Expiry fields are cleared: a setup token carries no expiry, and a value left
|
|
444
462
|
over from an older row would make Hermes treat a perfectly good credential
|
|
445
463
|
as expired.
|
|
464
|
+
|
|
465
|
+
Status fields are cleared too, but only when the access token actually
|
|
466
|
+
changed — see STATUS_FIELDS. A metadata-only update (same token) preserves
|
|
467
|
+
them exactly.
|
|
446
468
|
"""
|
|
469
|
+
token_rotated = existing.get("access_token") != credential.access_token
|
|
447
470
|
updated = copy.deepcopy(existing)
|
|
448
471
|
updated.update(
|
|
449
472
|
{
|
|
@@ -458,6 +481,9 @@ def merge_credential(existing: dict[str, Any], credential: DesiredCredential) ->
|
|
|
458
481
|
updated.pop("refresh_token", None)
|
|
459
482
|
for field in STALE_EXPIRY_FIELDS:
|
|
460
483
|
updated.pop(field, None)
|
|
484
|
+
if token_rotated:
|
|
485
|
+
for field in STATUS_FIELDS:
|
|
486
|
+
updated.pop(field, None)
|
|
461
487
|
return updated
|
|
462
488
|
|
|
463
489
|
|