swarph-cli 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {swarph_cli-0.3.0/src/swarph_cli.egg-info → swarph_cli-0.4.0}/PKG-INFO +38 -6
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/README.md +36 -4
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/pyproject.toml +2 -2
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/__init__.py +1 -1
- swarph_cli-0.4.0/src/swarph_cli/commands/onboard.py +377 -0
- swarph_cli-0.4.0/src/swarph_cli/commands/ratify.py +283 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/main.py +10 -4
- {swarph_cli-0.3.0 → swarph_cli-0.4.0/src/swarph_cli.egg-info}/PKG-INFO +38 -6
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/SOURCES.txt +6 -1
- swarph_cli-0.4.0/tests/test_onboard_command.py +279 -0
- swarph_cli-0.4.0/tests/test_ratify_command.py +224 -0
- swarph_cli-0.4.0/tests/test_smoke_phase_5_5.py +144 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/LICENSE +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/setup.cfg +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_import_command.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_main.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.3.0 → swarph_cli-0.4.0}/tests/test_smoke_one_shot.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.4.0 ships Phase 5.5 `swarph onboard` + `swarph ratify` on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL (PLAN.md §13 / §15).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/darw007d/swarph-cli
|
|
@@ -49,13 +49,45 @@ This is one of three repos in the v0.3.x architecture:
|
|
|
49
49
|
|
|
50
50
|
## Status
|
|
51
51
|
|
|
52
|
-
**v0.
|
|
52
|
+
**v0.4.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify.** Five verbs ship:
|
|
53
53
|
|
|
54
54
|
1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
|
|
55
55
|
2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
|
|
56
56
|
3. `swarph import <path>` — Phase 2.5 session import (Claude JSONL → swarph-native, with `--report-only` for honest pre-commit inspection)
|
|
57
|
+
4. `swarph onboard <peer-name>` — **NEW** Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
|
|
58
|
+
5. `swarph ratify <peer-name>` — **NEW** Phase 5.5 witness ratification (PLAN.md §15.4a)
|
|
57
59
|
|
|
58
|
-
Subsequent phases extend the CLI surface (`--ask <peer>`,
|
|
60
|
+
Subsequent phases extend the CLI surface (`--ask <peer>`, daemon).
|
|
61
|
+
|
|
62
|
+
### `swarph onboard` + `swarph ratify` (Phase 5.5)
|
|
63
|
+
|
|
64
|
+
Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# New peer self-onboards
|
|
68
|
+
$ swarph onboard razorpeter
|
|
69
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
70
|
+
[2/6] prepare peer-registry row ok
|
|
71
|
+
[3/6] resolve MESH_GATEWAY_TOKEN ok
|
|
72
|
+
[4/6] POST .../peers/register ok (registered_unratified=true)
|
|
73
|
+
[5/6] verify_subscription_setup() ok
|
|
74
|
+
[6/6] scaffold ~/swarph_state/razorpeter/ ok
|
|
75
|
+
|
|
76
|
+
[manual] handshake template at /tmp/razorpeter-handshake.md
|
|
77
|
+
Edit each section in your own words, then send to your witness peer.
|
|
78
|
+
|
|
79
|
+
# After peer composes + sends handshake, witness ratifies
|
|
80
|
+
$ SWARPH_WITNESS=lab-ovh swarph ratify razorpeter \
|
|
81
|
+
--reason "handshake covers all four invariants in own words"
|
|
82
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
83
|
+
[2/6] verify witness 'lab-ovh' is ratified ok
|
|
84
|
+
[3/6] verify 'razorpeter' is registered_unratified ok
|
|
85
|
+
[4/6] PATCH .../peers/razorpeter ok
|
|
86
|
+
[5/6] verify peer_ratifications audit row ok (id=N reason='...')
|
|
87
|
+
[6/6] invalidate local TTL cache ok
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Server-side gating (mesh-gateway PR A): unratified peers can read inbox + send DMs (so the handshake itself works) but `task_claim` returns 403. Witness must itself be ratified — no self-ratification, no unratified-witnesses-ratifying-others. Audit log (`peer_ratifications`) is append-only.
|
|
59
91
|
|
|
60
92
|
### `swarph chat`
|
|
61
93
|
|
|
@@ -158,9 +190,9 @@ Pong!
|
|
|
158
190
|
| **0** | Scaffold — entry-point + status banner |
|
|
159
191
|
| **2** (v0.1.0) | One-shot mode: `swarph "hello" --provider gemini` |
|
|
160
192
|
| **2.5** (v0.2.0) | `swarph import` — Claude JSONL → swarph-native session format |
|
|
161
|
-
| **5** (v0.3.0
|
|
193
|
+
| **5** (v0.3.0) | `swarph chat` interactive REPL — multi-turn against any of five adapters + slash commands |
|
|
194
|
+
| **5.5** (v0.4.0 — this release) | **`swarph onboard <peer-name>` + `swarph ratify <peer-name>`** — six mechanics steps + handshake template + witness flip (PLAN.md §15) |
|
|
162
195
|
| **3** | `--ask <peer>` mesh-aware one-shot via MeshClient |
|
|
163
|
-
| **5.5** | `swarph onboard <peer-name>` + `swarph ratify <peer-name>` (PLAN.md §15) |
|
|
164
196
|
| **5.6** | `swarph daemon` foreground drain loop + REPL drain coroutine + `/inbox`, `/reply` (PLAN.md §16) |
|
|
165
197
|
| **6** | (already done) PyPI publish |
|
|
166
198
|
|
|
@@ -17,13 +17,45 @@ This is one of three repos in the v0.3.x architecture:
|
|
|
17
17
|
|
|
18
18
|
## Status
|
|
19
19
|
|
|
20
|
-
**v0.
|
|
20
|
+
**v0.4.0 — Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL + Phase 5.5 onboard/ratify.** Five verbs ship:
|
|
21
21
|
|
|
22
22
|
1. `swarph "prompt"` — Phase 2 one-shot mode (any of five providers)
|
|
23
23
|
2. `swarph chat` — Phase 5 interactive REPL with multi-turn history + slash commands
|
|
24
24
|
3. `swarph import <path>` — Phase 2.5 session import (Claude JSONL → swarph-native, with `--report-only` for honest pre-commit inspection)
|
|
25
|
+
4. `swarph onboard <peer-name>` — **NEW** Phase 5.5 mechanics-phase onboarding (PLAN.md §15.4)
|
|
26
|
+
5. `swarph ratify <peer-name>` — **NEW** Phase 5.5 witness ratification (PLAN.md §15.4a)
|
|
25
27
|
|
|
26
|
-
Subsequent phases extend the CLI surface (`--ask <peer>`,
|
|
28
|
+
Subsequent phases extend the CLI surface (`--ask <peer>`, daemon).
|
|
29
|
+
|
|
30
|
+
### `swarph onboard` + `swarph ratify` (Phase 5.5)
|
|
31
|
+
|
|
32
|
+
Per PLAN.md §15, onboarding splits into a **mechanics phase** (`swarph onboard`) that automates the boring parts (registry POST, scaffolding, token resolution) and a **manual contract phase** (the new peer composes the handshake DM in their own words). A witness peer judges the handshake and runs `swarph ratify <peer>` to flip `ratified=true`, gating `task_claim` server-side.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# New peer self-onboards
|
|
36
|
+
$ swarph onboard razorpeter
|
|
37
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
38
|
+
[2/6] prepare peer-registry row ok
|
|
39
|
+
[3/6] resolve MESH_GATEWAY_TOKEN ok
|
|
40
|
+
[4/6] POST .../peers/register ok (registered_unratified=true)
|
|
41
|
+
[5/6] verify_subscription_setup() ok
|
|
42
|
+
[6/6] scaffold ~/swarph_state/razorpeter/ ok
|
|
43
|
+
|
|
44
|
+
[manual] handshake template at /tmp/razorpeter-handshake.md
|
|
45
|
+
Edit each section in your own words, then send to your witness peer.
|
|
46
|
+
|
|
47
|
+
# After peer composes + sends handshake, witness ratifies
|
|
48
|
+
$ SWARPH_WITNESS=lab-ovh swarph ratify razorpeter \
|
|
49
|
+
--reason "handshake covers all four invariants in own words"
|
|
50
|
+
[1/6] validate_node_name('razorpeter') ok
|
|
51
|
+
[2/6] verify witness 'lab-ovh' is ratified ok
|
|
52
|
+
[3/6] verify 'razorpeter' is registered_unratified ok
|
|
53
|
+
[4/6] PATCH .../peers/razorpeter ok
|
|
54
|
+
[5/6] verify peer_ratifications audit row ok (id=N reason='...')
|
|
55
|
+
[6/6] invalidate local TTL cache ok
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Server-side gating (mesh-gateway PR A): unratified peers can read inbox + send DMs (so the handshake itself works) but `task_claim` returns 403. Witness must itself be ratified — no self-ratification, no unratified-witnesses-ratifying-others. Audit log (`peer_ratifications`) is append-only.
|
|
27
59
|
|
|
28
60
|
### `swarph chat`
|
|
29
61
|
|
|
@@ -126,9 +158,9 @@ Pong!
|
|
|
126
158
|
| **0** | Scaffold — entry-point + status banner |
|
|
127
159
|
| **2** (v0.1.0) | One-shot mode: `swarph "hello" --provider gemini` |
|
|
128
160
|
| **2.5** (v0.2.0) | `swarph import` — Claude JSONL → swarph-native session format |
|
|
129
|
-
| **5** (v0.3.0
|
|
161
|
+
| **5** (v0.3.0) | `swarph chat` interactive REPL — multi-turn against any of five adapters + slash commands |
|
|
162
|
+
| **5.5** (v0.4.0 — this release) | **`swarph onboard <peer-name>` + `swarph ratify <peer-name>`** — six mechanics steps + handshake template + witness flip (PLAN.md §15) |
|
|
130
163
|
| **3** | `--ask <peer>` mesh-aware one-shot via MeshClient |
|
|
131
|
-
| **5.5** | `swarph onboard <peer-name>` + `swarph ratify <peer-name>` (PLAN.md §15) |
|
|
132
164
|
| **5.6** | `swarph daemon` foreground drain loop + REPL drain coroutine + `/inbox`, `/reply` (PLAN.md §16) |
|
|
133
165
|
| **6** | (already done) PyPI publish |
|
|
134
166
|
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swarph-cli"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "The `swarph` binary — multi-LLM CLI with mesh-gateway integration. v0.4.0 ships Phase 5.5 `swarph onboard` + `swarph ratify` on top of Phase 2 one-shot + Phase 2.5 import + Phase 5 REPL (PLAN.md §13 / §15)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""``swarph onboard <peer-name>`` — Phase 5.5 mechanics-phase per PLAN.md §15.4.
|
|
2
|
+
|
|
3
|
+
Six mechanics steps execute automatically; the seventh — composing
|
|
4
|
+
and sending the handshake DM — is **manual by design** (§15.1) so the
|
|
5
|
+
new peer's own-words ack of the four invariants reflects active
|
|
6
|
+
understanding rather than boilerplate provisioning.
|
|
7
|
+
|
|
8
|
+
Idempotent: rerun safe. Each step's gateway call is upsert-shaped
|
|
9
|
+
(POST /peers/register on conflict updates) or guarded (scaffold dir
|
|
10
|
+
mkdir -p). Re-running on an already-onboarded peer surfaces "already
|
|
11
|
+
registered" without harming state.
|
|
12
|
+
|
|
13
|
+
Auth resolution (step 3):
|
|
14
|
+
1. ``MESH_GATEWAY_TOKEN`` env var
|
|
15
|
+
2. ``~/.swarph/secrets.toml`` mode 0600 (read-only — does not auto-create)
|
|
16
|
+
3. Interactive prompt
|
|
17
|
+
4. NEVER from argv (would land in shell history)
|
|
18
|
+
|
|
19
|
+
Cross-runtime (§15.6 #10): Claude-only in v0; Gemini/non-Claude
|
|
20
|
+
runtime scaffolding lands alongside that adapter's Phase 6 rollout.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.request
|
|
31
|
+
from getpass import getpass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Optional
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_HANDSHAKE_TEMPLATE = """\
|
|
37
|
+
# Handshake DM — {peer}
|
|
38
|
+
|
|
39
|
+
> **Manual step.** Per PLAN.md §15.1, the contract phase is preserved
|
|
40
|
+
> as a manual artifact so your own-words acknowledgement reflects active
|
|
41
|
+
> understanding, not provisioning. Edit each section below in your own
|
|
42
|
+
> words. Generic boilerplate will be flagged + rejected by the witness.
|
|
43
|
+
|
|
44
|
+
## 1. DM SEMANTICS
|
|
45
|
+
|
|
46
|
+
> Reference: hedge-fund-mcp `CLAUDE.md` Science Claude Mesh Bootstrap
|
|
47
|
+
> section, "DM semantics: AI-to-AI is the default…"
|
|
48
|
+
|
|
49
|
+
[your own-words ack here — what does AI-to-AI-by-default mean for
|
|
50
|
+
how you'll handle routine peer DMs vs. ones crossing a privilege
|
|
51
|
+
boundary?]
|
|
52
|
+
|
|
53
|
+
## 2. Framing-contagion
|
|
54
|
+
|
|
55
|
+
> Reference: auto-memory `project_peer_name_canonical.md`. Your
|
|
56
|
+
> canonical name in the registry is `{peer}`.
|
|
57
|
+
|
|
58
|
+
[your own-words ack — how will you stay canonical + flag wrong-name
|
|
59
|
+
DMs you receive?]
|
|
60
|
+
|
|
61
|
+
## 3. Transparency-by-default
|
|
62
|
+
|
|
63
|
+
> Reference: swarph paper main draft §3.7.
|
|
64
|
+
|
|
65
|
+
[your own-words ack — what does transparency-by-default look like
|
|
66
|
+
when you slip? When do you DM peers vs. self-fix?]
|
|
67
|
+
|
|
68
|
+
## 4. Mesh-secrets out-of-band
|
|
69
|
+
|
|
70
|
+
> Reference: hedge-fund-mcp `CLAUDE.md` Critical operational rules,
|
|
71
|
+
> "Mesh secrets out-of-band only" bullet.
|
|
72
|
+
|
|
73
|
+
[your own-words ack — what counts as a mesh secret + what's your
|
|
74
|
+
fallback channel when you must convey one?]
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
**To send:**
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
swarph "$(<{tmp_path})" --provider <your-llm> --caller {peer}.handshake.witness-{witness}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or paste the rendered text into a DM via the gateway's
|
|
85
|
+
``POST /messages`` API to your witness peer (default
|
|
86
|
+
``science-claude``). The witness will read both this DM AND any
|
|
87
|
+
imported session JSONL (§17.2a flow), then run
|
|
88
|
+
``swarph ratify {peer}`` to flip ``ratified=true``.
|
|
89
|
+
|
|
90
|
+
**Status:** registered_unratified=true. You can read inbox + send
|
|
91
|
+
DMs (so the handshake itself works), but ``task_claim`` is
|
|
92
|
+
gateway-refused until ratified.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
97
|
+
p = argparse.ArgumentParser(
|
|
98
|
+
prog="swarph onboard",
|
|
99
|
+
description="Phase 5.5 mechanics-phase peer onboarding per PLAN.md §15.4.",
|
|
100
|
+
)
|
|
101
|
+
p.add_argument("peer", help="canonical peer name (e.g. razorpeter)")
|
|
102
|
+
p.add_argument(
|
|
103
|
+
"--gateway",
|
|
104
|
+
default=os.environ.get("MESH_GATEWAY_URL", "http://localhost:8788"),
|
|
105
|
+
help="mesh-gateway base URL (default: $MESH_GATEWAY_URL or http://localhost:8788)",
|
|
106
|
+
)
|
|
107
|
+
p.add_argument(
|
|
108
|
+
"--token-file",
|
|
109
|
+
default=None,
|
|
110
|
+
help="explicit path to a secrets file (mode 0600 expected). "
|
|
111
|
+
"Default resolution order: $MESH_GATEWAY_TOKEN env → ~/.swarph/secrets.toml → prompt.",
|
|
112
|
+
)
|
|
113
|
+
p.add_argument(
|
|
114
|
+
"--state-dir",
|
|
115
|
+
default=None,
|
|
116
|
+
help="local state directory root (default: ~/swarph_state).",
|
|
117
|
+
)
|
|
118
|
+
p.add_argument(
|
|
119
|
+
"--url",
|
|
120
|
+
default=None,
|
|
121
|
+
help="this peer's HTTP URL for the registry (default: http://<peer>:8787).",
|
|
122
|
+
)
|
|
123
|
+
p.add_argument(
|
|
124
|
+
"--capability",
|
|
125
|
+
action="append",
|
|
126
|
+
default=[],
|
|
127
|
+
help="capability advert as KEY=VALUE (repeatable). VALUE parsed as JSON if possible. "
|
|
128
|
+
'Defaults to {"can_claim_tasks": true} if none given.',
|
|
129
|
+
)
|
|
130
|
+
return p
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _resolve_token(token_file_arg: Optional[str]) -> str:
|
|
134
|
+
"""Step 3 — token resolution per §15.4. Read-only on the secrets file
|
|
135
|
+
(does not auto-create per drop DM #726 #3 — privilege boundary)."""
|
|
136
|
+
env_tok = os.environ.get("MESH_GATEWAY_TOKEN")
|
|
137
|
+
if env_tok:
|
|
138
|
+
return env_tok
|
|
139
|
+
|
|
140
|
+
secrets_path = (
|
|
141
|
+
Path(token_file_arg).expanduser()
|
|
142
|
+
if token_file_arg
|
|
143
|
+
else Path.home() / ".swarph" / "secrets.toml"
|
|
144
|
+
)
|
|
145
|
+
if secrets_path.exists():
|
|
146
|
+
try:
|
|
147
|
+
mode = secrets_path.stat().st_mode & 0o777
|
|
148
|
+
if mode != 0o600:
|
|
149
|
+
print(
|
|
150
|
+
f"swarph onboard: WARNING: {secrets_path} mode is {oct(mode)}, "
|
|
151
|
+
f"expected 0600. Continuing — fix manually with `chmod 600 {secrets_path}`.",
|
|
152
|
+
file=sys.stderr,
|
|
153
|
+
)
|
|
154
|
+
content = secrets_path.read_text(encoding="utf-8")
|
|
155
|
+
for line in content.splitlines():
|
|
156
|
+
line = line.strip()
|
|
157
|
+
if line.startswith("#") or not line:
|
|
158
|
+
continue
|
|
159
|
+
if line.startswith("MESH_GATEWAY_TOKEN"):
|
|
160
|
+
val = line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
161
|
+
if val:
|
|
162
|
+
return val
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
print(
|
|
165
|
+
f"swarph onboard: failed to read {secrets_path}: {exc}", file=sys.stderr
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
print(
|
|
169
|
+
f"swarph onboard: MESH_GATEWAY_TOKEN not in env, not found in {secrets_path}.\n"
|
|
170
|
+
f" Canonical secrets.toml shape (mode 0600):\n"
|
|
171
|
+
f" MESH_GATEWAY_TOKEN=<your-token>\n"
|
|
172
|
+
f" Falling back to interactive prompt.",
|
|
173
|
+
file=sys.stderr,
|
|
174
|
+
)
|
|
175
|
+
return getpass("MESH_GATEWAY_TOKEN: ").strip()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _post_json(
|
|
179
|
+
url: str, body: dict, token: str, *, method: str = "POST"
|
|
180
|
+
) -> tuple[int, dict]:
|
|
181
|
+
"""Tiny stdlib HTTP client. Avoids httpx dep at the CLI layer.
|
|
182
|
+
|
|
183
|
+
Returns (status, parsed_body). On non-2xx, parsed_body is the error
|
|
184
|
+
JSON payload (best-effort) so callers can surface gateway error text."""
|
|
185
|
+
data = json.dumps(body).encode("utf-8")
|
|
186
|
+
req = urllib.request.Request(
|
|
187
|
+
url,
|
|
188
|
+
data=data,
|
|
189
|
+
method=method,
|
|
190
|
+
headers={
|
|
191
|
+
"Authorization": f"Bearer {token}",
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
try:
|
|
196
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
197
|
+
payload = json.loads(resp.read().decode("utf-8") or "{}")
|
|
198
|
+
return resp.status, payload
|
|
199
|
+
except urllib.error.HTTPError as exc:
|
|
200
|
+
try:
|
|
201
|
+
err_body = json.loads(exc.read().decode("utf-8") or "{}")
|
|
202
|
+
except Exception:
|
|
203
|
+
err_body = {"detail": str(exc)}
|
|
204
|
+
return exc.code, err_body
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_capability(spec: str) -> tuple[str, object]:
|
|
208
|
+
"""``KEY=VALUE`` → (key, value). VALUE parsed as JSON when possible
|
|
209
|
+
(so ``can_claim_tasks=true`` lands as bool, not string)."""
|
|
210
|
+
if "=" not in spec:
|
|
211
|
+
raise argparse.ArgumentTypeError(f"capability {spec!r} not KEY=VALUE shape")
|
|
212
|
+
k, v = spec.split("=", 1)
|
|
213
|
+
try:
|
|
214
|
+
return k.strip(), json.loads(v)
|
|
215
|
+
except json.JSONDecodeError:
|
|
216
|
+
return k.strip(), v
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def run_onboard(argv: list[str]) -> int:
|
|
220
|
+
"""Entry point invoked by ``swarph_cli.main`` verb dispatch.
|
|
221
|
+
|
|
222
|
+
Returns process exit code: 0 on success, 1 on validation fail,
|
|
223
|
+
2 on gateway error."""
|
|
224
|
+
args = _build_parser().parse_args(argv)
|
|
225
|
+
|
|
226
|
+
# ── Step 1: validate_node_name ───────────────────────────────────
|
|
227
|
+
print(f"[1/6] validate_node_name({args.peer!r})")
|
|
228
|
+
try:
|
|
229
|
+
from swarph_shared.peer_registry import (
|
|
230
|
+
validate_node_name,
|
|
231
|
+
NotInRegistry,
|
|
232
|
+
GatewayUnreachableError,
|
|
233
|
+
)
|
|
234
|
+
except ImportError as exc:
|
|
235
|
+
print(f"swarph onboard: missing swarph-shared>=0.2.0: {exc}", file=sys.stderr)
|
|
236
|
+
return 1
|
|
237
|
+
|
|
238
|
+
# NotInRegistry is expected here — onboard's whole point is that
|
|
239
|
+
# the peer doesn't exist yet. We only enforce the regex shape.
|
|
240
|
+
try:
|
|
241
|
+
from swarph_shared.peer_registry import NAMING_CONVENTION_REGEX, KNOWN_ALIASES
|
|
242
|
+
except ImportError:
|
|
243
|
+
print("swarph onboard: peer_registry primitives missing", file=sys.stderr)
|
|
244
|
+
return 1
|
|
245
|
+
|
|
246
|
+
canonical = KNOWN_ALIASES.get(args.peer, args.peer)
|
|
247
|
+
if canonical != args.peer:
|
|
248
|
+
print(
|
|
249
|
+
f" WARN: {args.peer!r} resolved to canonical {canonical!r} "
|
|
250
|
+
f"(contagion alias)",
|
|
251
|
+
file=sys.stderr,
|
|
252
|
+
)
|
|
253
|
+
if not NAMING_CONVENTION_REGEX.match(canonical):
|
|
254
|
+
print(
|
|
255
|
+
f"swarph onboard: {canonical!r} fails naming convention "
|
|
256
|
+
f"(^[a-z][a-z0-9-]*[a-z0-9]$)",
|
|
257
|
+
file=sys.stderr,
|
|
258
|
+
)
|
|
259
|
+
return 1
|
|
260
|
+
print(f" ok ({canonical!r})")
|
|
261
|
+
|
|
262
|
+
# ── Step 2: would-write peer-registry row (effectively step 4) ───
|
|
263
|
+
# The PLAN's step 2 is logically subsumed by step 4 (the gateway
|
|
264
|
+
# POST is the only persistent registry write). We surface it as a
|
|
265
|
+
# planning/dry-run line for operator clarity.
|
|
266
|
+
capabilities = dict(_parse_capability(c) for c in args.capability) if args.capability else {
|
|
267
|
+
"can_claim_tasks": True
|
|
268
|
+
}
|
|
269
|
+
print(f"[2/6] prepare peer-registry row (caps={capabilities})")
|
|
270
|
+
|
|
271
|
+
# ── Step 3: resolve MESH_GATEWAY_TOKEN ───────────────────────────
|
|
272
|
+
print("[3/6] resolve MESH_GATEWAY_TOKEN")
|
|
273
|
+
token = _resolve_token(args.token_file)
|
|
274
|
+
if not token:
|
|
275
|
+
print("swarph onboard: empty token", file=sys.stderr)
|
|
276
|
+
return 1
|
|
277
|
+
print(" ok")
|
|
278
|
+
|
|
279
|
+
# ── Step 4: POST /peers/register ─────────────────────────────────
|
|
280
|
+
peer_url = args.url or f"http://{canonical}:8787"
|
|
281
|
+
print(f"[4/6] POST {args.gateway}/peers/register")
|
|
282
|
+
status, body = _post_json(
|
|
283
|
+
f"{args.gateway}/peers/register",
|
|
284
|
+
{"name": canonical, "url": peer_url, "capabilities": capabilities},
|
|
285
|
+
token,
|
|
286
|
+
)
|
|
287
|
+
if status != 200:
|
|
288
|
+
print(
|
|
289
|
+
f"swarph onboard: gateway register failed: {status} {body}",
|
|
290
|
+
file=sys.stderr,
|
|
291
|
+
)
|
|
292
|
+
return 2
|
|
293
|
+
if body.get("registered_unratified") is False:
|
|
294
|
+
print(
|
|
295
|
+
f" ok (already ratified — peer existed pre-Phase-5.5 or was "
|
|
296
|
+
f"witness-flipped already)"
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
print(f" ok (registered_unratified=true)")
|
|
300
|
+
|
|
301
|
+
# ── Step 5: subscription auth check ──────────────────────────────
|
|
302
|
+
print("[5/6] verify_subscription_setup()")
|
|
303
|
+
try:
|
|
304
|
+
from swarph_shared import verify_subscription_setup
|
|
305
|
+
|
|
306
|
+
# The function returns either True or raises an informative error;
|
|
307
|
+
# catch broadly so onboarding doesn't blow up on Claude-runtime-only
|
|
308
|
+
# checks when the peer is non-Claude (§15.6 #10 deferred to Phase 6).
|
|
309
|
+
verify_subscription_setup()
|
|
310
|
+
print(" ok (Claude subscription credentials + binary verified)")
|
|
311
|
+
except Exception as exc:
|
|
312
|
+
print(
|
|
313
|
+
f" WARN: {type(exc).__name__}: {exc}\n"
|
|
314
|
+
f" Subscription path won't work for this peer until resolved. "
|
|
315
|
+
f"Non-Claude runtimes (Gemini, etc.) ship in Phase 6 per §15.6 #10.",
|
|
316
|
+
file=sys.stderr,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# ── Step 6: scaffold local state directory ───────────────────────
|
|
320
|
+
state_root = (
|
|
321
|
+
Path(args.state_dir).expanduser()
|
|
322
|
+
if args.state_dir
|
|
323
|
+
else Path.home() / "swarph_state"
|
|
324
|
+
)
|
|
325
|
+
peer_dir = state_root / canonical
|
|
326
|
+
print(f"[6/6] scaffold {peer_dir}")
|
|
327
|
+
peer_dir.mkdir(parents=True, exist_ok=True)
|
|
328
|
+
try:
|
|
329
|
+
peer_dir.chmod(0o700)
|
|
330
|
+
except OSError:
|
|
331
|
+
pass # best-effort; Windows or fs without POSIX modes
|
|
332
|
+
inbox_log = peer_dir / "inbox.log"
|
|
333
|
+
cursor_path = peer_dir / "cursor.json"
|
|
334
|
+
env_example = peer_dir / ".env.example"
|
|
335
|
+
daemon_sh = peer_dir / "run-daemon.sh"
|
|
336
|
+
|
|
337
|
+
if not inbox_log.exists():
|
|
338
|
+
inbox_log.touch()
|
|
339
|
+
if not cursor_path.exists():
|
|
340
|
+
cursor_path.write_text(
|
|
341
|
+
json.dumps({"last_msg_id": 0, "tasks_snapshot": {}}, indent=2),
|
|
342
|
+
encoding="utf-8",
|
|
343
|
+
)
|
|
344
|
+
if not env_example.exists():
|
|
345
|
+
env_example.write_text(
|
|
346
|
+
f"# swarph state for {canonical}\n"
|
|
347
|
+
f"MESH_GATEWAY_TOKEN=\n"
|
|
348
|
+
f"MESH_GATEWAY_URL={args.gateway}\n",
|
|
349
|
+
encoding="utf-8",
|
|
350
|
+
)
|
|
351
|
+
if not daemon_sh.exists():
|
|
352
|
+
daemon_sh.write_text(
|
|
353
|
+
f"#!/usr/bin/env bash\n"
|
|
354
|
+
f"# Phase 5.6 launcher — runs `swarph daemon` with this peer's state.\n"
|
|
355
|
+
f"# Pre-launch via: nohup ./run-daemon.sh &\n"
|
|
356
|
+
f"exec swarph daemon --state-dir {peer_dir}\n",
|
|
357
|
+
encoding="utf-8",
|
|
358
|
+
)
|
|
359
|
+
daemon_sh.chmod(0o755)
|
|
360
|
+
print(f" ok (inbox.log, cursor.json, .env.example, run-daemon.sh)")
|
|
361
|
+
|
|
362
|
+
# ── Step 7: handshake template (MANUAL) ──────────────────────────
|
|
363
|
+
tmp_path = Path(f"/tmp/{canonical}-handshake.md")
|
|
364
|
+
tmp_path.write_text(
|
|
365
|
+
_HANDSHAKE_TEMPLATE.format(
|
|
366
|
+
peer=canonical, witness="science-claude", tmp_path=tmp_path
|
|
367
|
+
),
|
|
368
|
+
encoding="utf-8",
|
|
369
|
+
)
|
|
370
|
+
print(
|
|
371
|
+
f"\n[manual] handshake template at {tmp_path}\n"
|
|
372
|
+
f" Edit each section in your own words, then send to your witness peer.\n"
|
|
373
|
+
f" After witness reads + judges sufficient, they run:\n"
|
|
374
|
+
f" swarph ratify {canonical} --reason \"<short text>\"\n"
|
|
375
|
+
f" to flip ratified=true.\n"
|
|
376
|
+
)
|
|
377
|
+
return 0
|