swarph-cli 0.13.0__tar.gz → 0.13.2__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.13.0/src/swarph_cli.egg-info → swarph_cli-0.13.2}/PKG-INFO +1 -1
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/pyproject.toml +1 -1
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/__init__.py +1 -1
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/spawn.py +227 -86
- {swarph_cli-0.13.0 → swarph_cli-0.13.2/src/swarph_cli.egg-info}/PKG-INFO +1 -1
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_spawn_command.py +195 -83
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_spawn_tmux_session.py +51 -14
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/LICENSE +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/README.md +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/setup.cfg +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/caller.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/capture/__init__.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/capture/harden.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/capture/lineage.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/capture/liveness.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/capture/manifest.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/capture/paths.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/capture/verify.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/cell.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/__init__.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/add.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/cell.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/chat.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/compress.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/daemon.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/hook_output.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/hooks.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/import_session.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/init.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/install_hook.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/mcp_server.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/memory_sync.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/mesh.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/onboard.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/protocol_handler.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/ratify.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/security.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/commands/watchdog.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/compress/__init__.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/compress/levers.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/compress/marker.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/compress/verify.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/main.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/parsers/__init__.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/parsers/claude.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/entry_points.txt +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/requires.txt +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/src/swarph_cli.egg-info/top_level.txt +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_add_security_gate.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_artifact_add.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_artifact_hash.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_artifact_lib.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_artifact_mcp.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_artifact_skill.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_artifact_tool.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_artifact_uri.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_capture_lineage.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_capture_liveness.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_capture_manifest.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_capture_paths.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_capture_security.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_cell_command_dispatch.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_cell_harden.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_cell_loader.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_cell_verify.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_chat_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_claude_parser.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_claude_tmux_template.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_compress_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_compress_levers.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_compress_marker.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_compress_verify.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_daemon_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_feature_to_uri.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_hook_output.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_hooks_add.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_hooks_bundle.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_hooks_init.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_hooks_lifecycle.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_hooks_merge.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_import_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_init_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_install_hook.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_main.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_mcp_server.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_memory_sync.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_mesh_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_mesh_sidecar.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_onboard_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_protocol_handler.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_ratify_command.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_security.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_session_path_role_gate.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_smoke_chat.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_smoke_one_shot.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_smoke_phase_5_5.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_spawn_live_pin.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_spawn_mitosis_lineage.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_spawn_windows_relaunch.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_watchdog.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_watchdog_dm_wake_wiring.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_watchdog_model_rung.py +0 -0
- {swarph_cli-0.13.0 → swarph_cli-0.13.2}/tests/test_watchdog_stale_peers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swarph-cli"
|
|
7
|
-
version = "0.13.
|
|
7
|
+
version = "0.13.2"
|
|
8
8
|
description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -28,8 +28,10 @@ import os
|
|
|
28
28
|
import shutil
|
|
29
29
|
import subprocess
|
|
30
30
|
import sys
|
|
31
|
+
import time
|
|
31
32
|
from pathlib import Path
|
|
32
33
|
from typing import Optional
|
|
34
|
+
from urllib.parse import quote
|
|
33
35
|
|
|
34
36
|
from swarph_cli import __version__
|
|
35
37
|
from swarph_cli.cell import (
|
|
@@ -490,89 +492,165 @@ def _scrubbed_codex_env() -> dict[str, str]:
|
|
|
490
492
|
#: inside cwd)").
|
|
491
493
|
_GROK_CELL_HOME_SUBDIR = ".grok-cell"
|
|
492
494
|
|
|
493
|
-
#:
|
|
494
|
-
#:
|
|
495
|
-
#:
|
|
496
|
-
#:
|
|
497
|
-
#:
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
)
|
|
495
|
+
#: Grok cell env: DENY-BY-DEFAULT for the grok/xai namespace. A cell has its own
|
|
496
|
+
#: ISOLATED HOME + file-based config, so it must inherit NOTHING grok-specific
|
|
497
|
+
#: from the operator's interactive env — every ``GROK_*``/``XAI_*`` var is a
|
|
498
|
+
#: potential redirect: ``GROK_HOME``/``GROK_AUTH_PATH``/``GROK_AUTH`` override the
|
|
499
|
+
#: isolated HOME and auth path ENTIRELY (so an inherited one silently bypasses
|
|
500
|
+
#: the whole isolation scheme — grok honors GROK_HOME over $HOME);
|
|
501
|
+
#: ``GROK_AUTH_PROVIDER_COMMAND`` runs an arbitrary auth command;
|
|
502
|
+
#: ``GROK_MANAGED_CONFIG_URL`` + the ``GROK_OAUTH2_*`` / ``*_URL`` family redirect
|
|
503
|
+
#: endpoints. Enumerating them is whack-a-mole, so scrub the whole namespace and
|
|
504
|
+
#: let grok fall back to its built-in xAI $0 defaults + the symlinked auth.json.
|
|
505
|
+
#: Allowlist only a var the cell genuinely needs to RECEIVE from the parent —
|
|
506
|
+
#: none today.
|
|
507
|
+
_GROK_ENV_ALLOWLIST: frozenset = frozenset()
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _scrub_grok_namespace(env: dict) -> None:
|
|
511
|
+
for key in [
|
|
512
|
+
k for k in env
|
|
513
|
+
if (k.startswith("GROK_") or k.startswith("XAI_"))
|
|
514
|
+
and k not in _GROK_ENV_ALLOWLIST
|
|
515
|
+
]:
|
|
516
|
+
env.pop(key, None)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
#: Default grok sandbox profile. Built-in profiles: off, workspace, devbox,
|
|
520
|
+
#: read-only, strict. ``workspace`` confines filesystem writes to the cwd while
|
|
521
|
+
#: keeping network (mesh) reachable — empirically Landlock restrict_network=false
|
|
522
|
+
#: for workspace (strict/read-only request restrict_network=true, which blocks
|
|
523
|
+
#: child network on kernels with Landlock V4+/full seccomp TCP filtering; on
|
|
524
|
+
#: older kernels the seccomp-fallback localhost rules still let the mesh through).
|
|
525
|
+
#: Override via top-level ``sandbox`` in cell.yaml (``sandbox: off`` disables).
|
|
526
|
+
_GROK_DEFAULT_SANDBOX = "workspace"
|
|
506
527
|
|
|
507
528
|
|
|
508
529
|
def _grok_env(cell: Cell) -> dict[str, str]:
|
|
509
530
|
"""Subscription/OIDC env for a local ``grok`` CELL session ($0 path).
|
|
510
531
|
|
|
511
532
|
On top of the canonical billing scrub:
|
|
512
|
-
* the
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
operator's ``~/.grok/auth.json`` symlinked in for $0 OIDC, so the cell's
|
|
533
|
+
* DENY-BY-DEFAULT scrub of the whole ``GROK_*``/``XAI_*`` namespace (see
|
|
534
|
+
``_scrub_grok_namespace``) — closes the redirect class incl
|
|
535
|
+
``GROK_HOME``/``GROK_AUTH_PATH`` (which would bypass the isolated HOME)
|
|
536
|
+
and ``GROK_AUTH_PROVIDER_COMMAND``; grok falls back to its built-in xAI
|
|
537
|
+
$0 defaults + the symlinked auth.json.
|
|
538
|
+
* HOME → an ISOLATED dir inside the cell cwd, with the operator's
|
|
539
|
+
``~/.grok/auth.json`` symlinked to ``<HOME>/.grok/auth.json`` (grok reads
|
|
540
|
+
auth at ``$HOME/.grok/auth.json`` — strace-verified; the link MUST be in
|
|
541
|
+
the ``.grok`` subdir, not at ``$HOME/auth.json``), so the cell's
|
|
522
542
|
sessions/memory/auth never mix with the operator's personal ``~/.grok``.
|
|
523
543
|
* SWARPH_SPAWN marker — the tmux re-entry loop-guard (see
|
|
524
544
|
``_launch_via_tmux``) and the install-hook double-inject guard.
|
|
545
|
+
|
|
546
|
+
MESH_GATEWAY_TOKEN is deliberately NOT popped — same as the claude/codex/agy
|
|
547
|
+
membranes, the cell inherits the gateway token so its mesh DMs work out of
|
|
548
|
+
the box. (A per-peer-token cutover is a separate, explicit feature: it
|
|
549
|
+
requires placing the per-peer token file in the cell HOME, which this
|
|
550
|
+
membrane does not do, so popping the shared token here would silently mute
|
|
551
|
+
the cell on the mesh.)
|
|
525
552
|
"""
|
|
526
553
|
env = scrub_env_for_subprocess()
|
|
527
|
-
|
|
528
|
-
env.pop(key, None)
|
|
529
|
-
env.pop("MESH_GATEWAY_TOKEN", None)
|
|
554
|
+
_scrub_grok_namespace(env)
|
|
530
555
|
env["SWARPH_SPAWN"] = "1"
|
|
531
556
|
|
|
532
557
|
cell_home = cell.cwd / _GROK_CELL_HOME_SUBDIR
|
|
533
|
-
cell_home.
|
|
558
|
+
grok_dir = cell_home / ".grok"
|
|
559
|
+
grok_dir.mkdir(parents=True, exist_ok=True)
|
|
560
|
+
_link_grok_auth(grok_dir / "auth.json")
|
|
561
|
+
env["HOME"] = str(cell_home)
|
|
562
|
+
return env
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _link_grok_auth(link: Path) -> None:
|
|
566
|
+
"""Symlink the operator's ``~/.grok/auth.json`` to ``link`` for $0 OIDC.
|
|
567
|
+
|
|
568
|
+
Robust + idempotent: skips when ``link`` already resolves to the operator
|
|
569
|
+
auth; replaces a STALE/foreign/dangling link (validated via ``is_symlink`` +
|
|
570
|
+
``readlink``, which a dangling link's ``exists()`` reports False for);
|
|
571
|
+
never clobbers a real file; never crashes the spawn (best-effort — grok
|
|
572
|
+
falls back to its own auth flow on any failure).
|
|
573
|
+
"""
|
|
574
|
+
op_auth = Path.home() / ".grok" / "auth.json"
|
|
575
|
+
if not op_auth.exists():
|
|
576
|
+
return
|
|
534
577
|
try:
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
link.
|
|
578
|
+
if link.is_symlink():
|
|
579
|
+
if link.readlink() == op_auth:
|
|
580
|
+
return # already correct
|
|
581
|
+
link.unlink() # stale/foreign/dangling link → replace
|
|
582
|
+
elif link.exists():
|
|
583
|
+
return # a real file is present — do not clobber
|
|
584
|
+
link.symlink_to(op_auth)
|
|
539
585
|
except OSError:
|
|
540
|
-
# Best-effort: a pre-existing link / race / read-only HOME must not
|
|
541
|
-
# crash the spawn — grok will fall back to its own auth flow.
|
|
542
586
|
pass
|
|
543
|
-
|
|
544
|
-
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _grok_session_exists(cell: Cell) -> bool:
|
|
590
|
+
"""True if grok already has a session for this cell's (HOME, cwd).
|
|
591
|
+
|
|
592
|
+
grok keys sessions by url-encoded cwd under
|
|
593
|
+
``$HOME/.grok/sessions/<quote(cwd)>/<uuid>/``. Used to decide genesis vs
|
|
594
|
+
continue: grok's ``--resume`` REQUIRES a pre-existing session (unlike claude
|
|
595
|
+
``--session-id`` which MINTS one), so passing a swarph-minted UUID on genesis
|
|
596
|
+
errors "Session does not exist". Detecting a prior session lets us pass
|
|
597
|
+
``--continue`` (resume most-recent for cwd) only when it's safe.
|
|
598
|
+
"""
|
|
599
|
+
enc = quote(str(cell.cwd), safe="")
|
|
600
|
+
sdir = cell.cwd / _GROK_CELL_HOME_SUBDIR / ".grok" / "sessions" / enc
|
|
601
|
+
try:
|
|
602
|
+
return sdir.is_dir() and any(p.is_dir() for p in sdir.iterdir())
|
|
603
|
+
except OSError:
|
|
604
|
+
return False
|
|
545
605
|
|
|
546
606
|
|
|
547
607
|
def _build_grok_argv(
|
|
548
608
|
cell: Cell,
|
|
549
|
-
|
|
550
|
-
effective_role: Optional[str],
|
|
609
|
+
no_starter: bool,
|
|
551
610
|
passthrough: list[str],
|
|
552
611
|
) -> list[str]:
|
|
553
|
-
"""``grok
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
612
|
+
"""Build the ``grok`` cell argv. Grounded against real grok 0.2.54.
|
|
613
|
+
|
|
614
|
+
- ``--cwd <cwd>`` keys grok's cwd-scoped session + memory to the cell dir.
|
|
615
|
+
- ``--continue`` (resume most-recent session for the cwd) ONLY when a prior
|
|
616
|
+
grok session exists. grok mints + owns its own session UUIDs and
|
|
617
|
+
``--resume`` REQUIRES an existing one, so on genesis we pass NO session
|
|
618
|
+
flag and let grok mint (a swarph-minted UUID would error "Session does not
|
|
619
|
+
exist"). grok's own durable cross-session memory carries identity either
|
|
620
|
+
way.
|
|
621
|
+
- ``--system-prompt-override <starter>`` carries the cell's swarph starter /
|
|
622
|
+
identity when one is configured (the real flag; NOT ``--agent``, which is
|
|
623
|
+
grok's agent-PROFILE selector — an unknown swarph role name is silently
|
|
624
|
+
ignored, giving the cell no identity).
|
|
625
|
+
- ``--sandbox <profile>`` confines the cell (default ``workspace``); the
|
|
626
|
+
ONLY sibling that is both auto-approve AND unconfined is a security
|
|
627
|
+
escalation, and ``--sandbox`` is an independent axis from approval. Set
|
|
628
|
+
top-level ``sandbox: off`` in cell.yaml to disable.
|
|
629
|
+
- ``--always-approve`` (default on) is the unattended-cell autonomy posture;
|
|
630
|
+
opt out via top-level ``always_approve: false`` in cell.yaml.
|
|
567
631
|
"""
|
|
568
632
|
argv = ["grok", "--cwd", str(cell.cwd)]
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
633
|
+
|
|
634
|
+
if _grok_session_exists(cell):
|
|
635
|
+
argv.append("--continue")
|
|
636
|
+
|
|
637
|
+
if not no_starter and cell.starter_prompt_path:
|
|
638
|
+
starter = read_starter_prompt(cell)
|
|
639
|
+
if starter:
|
|
640
|
+
argv.extend(["--system-prompt-override", starter])
|
|
641
|
+
|
|
642
|
+
# cell.sandbox is the canonical field (codex uses it too); grok built-in
|
|
643
|
+
# profiles are off/workspace/devbox/read-only/strict. Default workspace
|
|
644
|
+
# (keeps mesh network; strict/read-only restrict it on Landlock V4+ kernels).
|
|
645
|
+
sandbox = getattr(cell, "sandbox", None)
|
|
646
|
+
if sandbox is None:
|
|
647
|
+
sandbox = _GROK_DEFAULT_SANDBOX
|
|
648
|
+
if sandbox and str(sandbox).lower() not in ("off", "false", "none"):
|
|
649
|
+
argv.extend(["--sandbox", str(sandbox)])
|
|
650
|
+
|
|
574
651
|
if cell.extra.get("always_approve", True) is not False:
|
|
575
652
|
argv.append("--always-approve")
|
|
653
|
+
|
|
576
654
|
argv.extend(passthrough)
|
|
577
655
|
return argv
|
|
578
656
|
|
|
@@ -837,6 +915,54 @@ def _tmux_has_session(tmux: str, name: str) -> bool:
|
|
|
837
915
|
return result.returncode == 0
|
|
838
916
|
|
|
839
917
|
|
|
918
|
+
def _tmux_create_session(tmux: str, name: str, cwd: Path) -> bool:
|
|
919
|
+
"""Create the durable detached session that runs ``swarph spawn <name>``.
|
|
920
|
+
|
|
921
|
+
Robust against a STALE session registration. When a multiplexer server dies
|
|
922
|
+
uncleanly (operator closed the owning console, ``kill``, a crash) it can leave
|
|
923
|
+
the session NAME registered with no live server behind it. On the native-Windows
|
|
924
|
+
psmux build (``marlocarlo.psmux``) this poisons re-creation: ``new-session``
|
|
925
|
+
becomes a SILENT no-op — it returns rc=0 but creates nothing — so the name can
|
|
926
|
+
never come back. That is the exact failure that strands a peer's cell ("the
|
|
927
|
+
session got killed and won't return"; verified on workstation-lc 2026-06-18).
|
|
928
|
+
Real tmux on Linux/mac does not hit this, and the extra steps are a harmless
|
|
929
|
+
no-op there (the create succeeds first try and the loop breaks immediately).
|
|
930
|
+
|
|
931
|
+
So: clear any stale registration first (``kill-session`` — a no-op if the name
|
|
932
|
+
is truly absent), then create. Because psmux clears the stale lock
|
|
933
|
+
ASYNCHRONOUSLY (~1s observed), do NOT trust the create's exit code (rc=0 even on
|
|
934
|
+
the silent no-op): VERIFY with ``has-session`` and retry until it actually
|
|
935
|
+
materialises. Returns True once the session exists, False if it never appears.
|
|
936
|
+
"""
|
|
937
|
+
create_cmd = [
|
|
938
|
+
tmux, "new-session", "-d", "-s", name,
|
|
939
|
+
"-c", str(cwd), "-e", "SWARPH_SPAWN=1",
|
|
940
|
+
"swarph", "spawn", name,
|
|
941
|
+
]
|
|
942
|
+
# Clear a stale (server-less) registration; harmless if the name is truly absent.
|
|
943
|
+
try:
|
|
944
|
+
subprocess.run(
|
|
945
|
+
[tmux, "kill-session", "-t", f"={name}"],
|
|
946
|
+
stdout=subprocess.DEVNULL,
|
|
947
|
+
stderr=subprocess.DEVNULL,
|
|
948
|
+
)
|
|
949
|
+
except OSError:
|
|
950
|
+
return False
|
|
951
|
+
for attempt in range(6):
|
|
952
|
+
try:
|
|
953
|
+
subprocess.run(
|
|
954
|
+
create_cmd,
|
|
955
|
+
stdout=subprocess.DEVNULL,
|
|
956
|
+
stderr=subprocess.PIPE,
|
|
957
|
+
)
|
|
958
|
+
except OSError:
|
|
959
|
+
return False
|
|
960
|
+
if _tmux_has_session(tmux, name):
|
|
961
|
+
return True
|
|
962
|
+
time.sleep(0.4) # let psmux's async stale-lock clear catch up, then retry
|
|
963
|
+
return False
|
|
964
|
+
|
|
965
|
+
|
|
840
966
|
def _launch_via_tmux(
|
|
841
967
|
binary: str, argv: list[str], cwd: Path, session_name: str,
|
|
842
968
|
) -> bool:
|
|
@@ -901,24 +1027,15 @@ def _launch_via_tmux(
|
|
|
901
1027
|
interactive = sys.stdout.isatty()
|
|
902
1028
|
|
|
903
1029
|
if not _tmux_has_session(tmux, session_name):
|
|
904
|
-
# Create the durable session detached. The command re-enters spawn so
|
|
905
|
-
#
|
|
906
|
-
#
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
tmux, "new-session", "-d", "-s", session_name,
|
|
911
|
-
"-c", str(cwd), "-e", "SWARPH_SPAWN=1",
|
|
912
|
-
"swarph", "spawn", session_name,
|
|
913
|
-
],
|
|
914
|
-
check=True,
|
|
915
|
-
stdout=subprocess.DEVNULL,
|
|
916
|
-
stderr=subprocess.PIPE,
|
|
917
|
-
)
|
|
918
|
-
except (OSError, subprocess.CalledProcessError) as exc:
|
|
1030
|
+
# Create the durable session detached. The command re-enters spawn so the
|
|
1031
|
+
# membrane env-scrub + starter injection apply inside the pane; the inherited
|
|
1032
|
+
# $TMUX (and SWARPH_SPAWN) stop it from re-deciding. _tmux_create_session
|
|
1033
|
+
# also clears a stale (server-less) registration that would otherwise make
|
|
1034
|
+
# psmux's new-session a silent no-op and strand the cell.
|
|
1035
|
+
if not _tmux_create_session(tmux, session_name, cwd):
|
|
919
1036
|
print(
|
|
920
|
-
|
|
921
|
-
"back to the standard launch.",
|
|
1037
|
+
"swarph spawn: tmux session create failed (stale registration "
|
|
1038
|
+
"could not be cleared?); falling back to the standard launch.",
|
|
922
1039
|
file=sys.stderr,
|
|
923
1040
|
)
|
|
924
1041
|
return False
|
|
@@ -1219,18 +1336,18 @@ class GrokMembrane(ProviderMembrane):
|
|
|
1219
1336
|
claude cell) so it is durable + supervisable and lands in its OWN session,
|
|
1220
1337
|
never a window of another cell's session.
|
|
1221
1338
|
|
|
1222
|
-
|
|
1223
|
-
cwd-keyed session
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1339
|
+
Session model (``uses_pinned_session`` False): grok mints and OWNS its own
|
|
1340
|
+
cwd-keyed session UUIDs (``$HOME/.grok/sessions/<enc-cwd>/<uuid>``) and its
|
|
1341
|
+
``--resume`` REQUIRES a pre-existing one — unlike claude ``--session-id``,
|
|
1342
|
+
which mints. So the cell does NOT carry a swarph-pinned UUID; it relies on
|
|
1343
|
+
grok's own ``--continue`` (resume most-recent for the cwd) + grok's durable
|
|
1344
|
+
cross-session memory for continuity, and lets grok mint on genesis.
|
|
1228
1345
|
"""
|
|
1229
1346
|
|
|
1230
1347
|
name = "grok"
|
|
1231
1348
|
|
|
1232
1349
|
def uses_pinned_session(self) -> bool:
|
|
1233
|
-
return
|
|
1350
|
+
return False
|
|
1234
1351
|
|
|
1235
1352
|
def build_argv(
|
|
1236
1353
|
self,
|
|
@@ -1241,7 +1358,7 @@ class GrokMembrane(ProviderMembrane):
|
|
|
1241
1358
|
passthrough: list[str],
|
|
1242
1359
|
effective_role: Optional[str],
|
|
1243
1360
|
) -> list[str]:
|
|
1244
|
-
return _build_grok_argv(cell,
|
|
1361
|
+
return _build_grok_argv(cell, no_starter, passthrough)
|
|
1245
1362
|
|
|
1246
1363
|
def resolve_binary(self) -> Optional[str]:
|
|
1247
1364
|
provider_bin = shutil.which("grok")
|
|
@@ -1278,11 +1395,23 @@ class GrokMembrane(ProviderMembrane):
|
|
|
1278
1395
|
except OSError as exc:
|
|
1279
1396
|
print(f"swarph spawn: cannot chdir to {cell.cwd}: {exc}", file=sys.stderr)
|
|
1280
1397
|
return 1
|
|
1281
|
-
#
|
|
1282
|
-
#
|
|
1283
|
-
|
|
1398
|
+
# Isolated-HOME + billing-scrubbed env carried to grok without mutating
|
|
1399
|
+
# this process's os.environ first (a failed exec leaves us intact).
|
|
1400
|
+
env = _grok_env(cell)
|
|
1401
|
+
# Per-OS launch — the SAME split as claude.launch (v0.12.1 fix): on
|
|
1402
|
+
# Windows os.exec* is emulated as spawn-and-exit (not a real replace),
|
|
1403
|
+
# which collapses the tmux pane (its root command exits, orphaning grok);
|
|
1404
|
+
# a BLOCKING subprocess.run keeps THIS process as the pane root with grok
|
|
1405
|
+
# as its child until grok exits. POSIX uses execve for a true in-place
|
|
1406
|
+
# replace so the pane keeps running grok under the same PID.
|
|
1407
|
+
if sys.platform == "win32":
|
|
1408
|
+
try:
|
|
1409
|
+
return subprocess.run([binary, *argv[1:]], env=env).returncode
|
|
1410
|
+
except OSError as exc:
|
|
1411
|
+
print(f"swarph spawn: launch failed: {exc}", file=sys.stderr)
|
|
1412
|
+
return 1
|
|
1284
1413
|
try:
|
|
1285
|
-
os.execve(binary, argv,
|
|
1414
|
+
os.execve(binary, argv, env)
|
|
1286
1415
|
except OSError as exc:
|
|
1287
1416
|
print(f"swarph spawn: exec failed: {exc}", file=sys.stderr)
|
|
1288
1417
|
return 1
|
|
@@ -1417,7 +1546,11 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
|
|
|
1417
1546
|
file=sys.stderr,
|
|
1418
1547
|
)
|
|
1419
1548
|
|
|
1420
|
-
elif cell.provider
|
|
1549
|
+
elif cell.provider in ("antigravity", "grok"):
|
|
1550
|
+
# Fresh-session providers (no swarph-pinned UUID): grok mints + owns its
|
|
1551
|
+
# own session ids (continuity via --continue + grok memory). Still want
|
|
1552
|
+
# the operator-typed slot-role as effective_role for the named tmux
|
|
1553
|
+
# session + future sibling slots.
|
|
1421
1554
|
session_id = "(fresh-session-per-spawn, no pinned id)"
|
|
1422
1555
|
was_generated = True
|
|
1423
1556
|
sidecar_role = requested_role if requested_role else cell.role
|
|
@@ -1490,6 +1623,14 @@ def run_spawn(argv: Optional[list[str]] = None) -> int:
|
|
|
1490
1623
|
inject_text = f"Your active task is in CURRENT_TASK.md — read it first:\n\n{current_task_text}"
|
|
1491
1624
|
if cell.provider == "claude":
|
|
1492
1625
|
spawn_argv.extend(["--append-system-prompt", inject_text])
|
|
1626
|
+
elif cell.provider == "grok":
|
|
1627
|
+
# --rules (extra rules appended to the system prompt), NOT a
|
|
1628
|
+
# second --system-prompt-override: grok's clap REJECTS a
|
|
1629
|
+
# repeated flag, and _build_grok_argv may already have emitted
|
|
1630
|
+
# --system-prompt-override for the starter → a second one
|
|
1631
|
+
# would refuse to launch. (claude concatenates repeated
|
|
1632
|
+
# --append-system-prompt; grok rejects-on-repeat — the delta.)
|
|
1633
|
+
spawn_argv.extend(["--rules", inject_text])
|
|
1493
1634
|
elif cell.provider == "antigravity":
|
|
1494
1635
|
spawn_argv.extend(["--prompt-interactive", inject_text])
|
|
1495
1636
|
elif cell.provider == "codex":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swarph-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION).
|
|
5
5
|
Author: Pierre Samson, Claude Opus
|
|
6
6
|
License: MIT
|