🚩 gremlins: Windows-only gate in where, init and launches
This commit is contained in:
@@ -119,6 +119,14 @@ USER_SETTINGS_PATH: str | None = None
|
||||
PHASE2_COMMANDS = () # commands that tolerate unknown arguments (none: all are implemented)
|
||||
# N3: a gremlin may only run where, list, usage and wait (`tick` launches pings).
|
||||
CHILD_REFUSED = ("train", "spawn", "continue", "rebase", "kill", "expire", "init", "tick")
|
||||
# The platform gate (digest §4, amendment A1): on an unsupported platform these launch nothing.
|
||||
# `init` checks the gate itself, as its first step (B1); `where` reports it.
|
||||
GATED_COMMANDS = ("train", "spawn", "continue", "rebase", "tick")
|
||||
# Porting aid only (C12, documented in the platform handoff, not in SKILL.md): `=1` opens the
|
||||
# gate for that call on an unsupported platform.
|
||||
UNSUPPORTED_OK_ENV = "SPIRE_GREMLIN_UNSUPPORTED_OK"
|
||||
GATE_OVERRIDE_WARNING = ("platform gate overridden (%s=1); this platform is untested, use it only "
|
||||
"to port and test spire:gremlins" % UNSUPPORTED_OK_ENV)
|
||||
|
||||
# Warmth and keep-warm (design 5, block 2.1). Module-level so tests can shorten them.
|
||||
TICK_SUP_S = 60.0 # a supervisor runs one tick pass this often
|
||||
@@ -726,6 +734,38 @@ PLATFORM = select_platform()
|
||||
# --- platform seam: end ---------------------------------------------------------------
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Platform gate (digest §4): fail closed where the helper is untested
|
||||
# --------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def gate_overridden() -> bool:
|
||||
"""A12: the override counts only where the gate is closed (on a supported platform the env
|
||||
var changes nothing)."""
|
||||
return os.environ.get(UNSUPPORTED_OK_ENV) == "1" and not PLATFORM.supported
|
||||
|
||||
|
||||
def platform_report() -> dict:
|
||||
"""`where`'s `platform` block: `PLATFORM.describe()` plus `override`."""
|
||||
return {**PLATFORM.describe(), "override": gate_overridden()}
|
||||
|
||||
|
||||
def platform_gate() -> str | None:
|
||||
"""None when the platform is supported or the gate is overridden; else the reason."""
|
||||
if PLATFORM.supported or gate_overridden():
|
||||
return None
|
||||
d = PLATFORM.describe()
|
||||
return ("spire:gremlins is not tested on %s%s yet; use fresh agents for every block "
|
||||
"(see SKILL.md, Platforms)." % (d.get("sys_platform"), ", WSL" if d.get("wsl") else ""))
|
||||
|
||||
|
||||
def require_platform() -> None:
|
||||
"""Refuse (exit 2) on a closed gate: `init` and the launching commands (A1)."""
|
||||
reason = platform_gate()
|
||||
if reason is not None:
|
||||
raise GremlinsError("platform not supported", reason=reason, platform=platform_report())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# Paths
|
||||
# --------------------------------------------------------------------------------------
|
||||
@@ -2216,29 +2256,45 @@ def build_parser() -> _Parser:
|
||||
|
||||
|
||||
def cmd_where(args) -> tuple[dict, int]:
|
||||
"""The preflight report. On a closed platform gate: `ok: false` with the gate reason first,
|
||||
and every other part still filled (the porting hosts need it)."""
|
||||
reasons = []
|
||||
gate = platform_gate()
|
||||
if gate is not None:
|
||||
reasons.append(gate)
|
||||
py_ok = sys.version_info >= (3, 10)
|
||||
if not py_ok:
|
||||
reasons.append("python < 3.10")
|
||||
py = {"version": platform.python_version(), "executable": sys.executable,
|
||||
"base_executable": getattr(sys, "_base_executable", sys.executable),
|
||||
"supervisor_interpreter": supervisor_interpreter(), "ok": py_ok}
|
||||
path, how = resolve_binary(args.binary)
|
||||
binary = {"path": path, "how": how}
|
||||
if path is None:
|
||||
reasons.append("no claude binary (no --binary, no $CLAUDE_CODE_EXECPATH, none on PATH)")
|
||||
try:
|
||||
path, how = resolve_binary(args.binary)
|
||||
except GremlinsError as e: # A11: a path form this platform refuses
|
||||
path = None
|
||||
how = ("--binary" if args.binary else
|
||||
"CLAUDE_CODE_EXECPATH" if os.environ.get("CLAUDE_CODE_EXECPATH") else "PATH")
|
||||
binary = {"path": path, "how": how, "error": e.payload["error"],
|
||||
"given": e.payload.get("path", args.binary)}
|
||||
reasons.append("%s: %s" % (e.payload["error"], binary["given"]))
|
||||
else:
|
||||
try:
|
||||
binary.update(binary_info(path))
|
||||
except GremlinsError as e:
|
||||
binary["error"] = e.payload["error"]
|
||||
reasons.append("%s: %s" % (e.payload["error"], path))
|
||||
binary = {"path": path, "how": how}
|
||||
if path is None:
|
||||
reasons.append("no claude binary (no --binary, no $CLAUDE_CODE_EXECPATH, none on PATH)")
|
||||
else:
|
||||
try:
|
||||
binary.update(binary_info(path))
|
||||
except GremlinsError as e:
|
||||
binary["error"] = e.payload["error"]
|
||||
reasons.append("%s: %s" % (e.payload["error"], path))
|
||||
warnings = []
|
||||
base_url = os.environ.get("ANTHROPIC_BASE_URL")
|
||||
if base_url is not None and base_url.strip().rstrip("/").lower() != DEFAULT_BASE_URL:
|
||||
warnings.append("ANTHROPIC_BASE_URL is %r, not the default %s; it is passed to every "
|
||||
"base and gremlin, so they authenticate and bill through that endpoint"
|
||||
% (base_url, DEFAULT_BASE_URL))
|
||||
if gate_overridden():
|
||||
warnings.append(GATE_OVERRIDE_WARNING)
|
||||
out = {
|
||||
"script": script_path(),
|
||||
"python": py,
|
||||
@@ -2246,7 +2302,7 @@ def cmd_where(args) -> tuple[dict, int]:
|
||||
"binary": binary,
|
||||
"git": _git_report(),
|
||||
"scrub": scrub_report(),
|
||||
"platform": PLATFORM.describe(),
|
||||
"platform": platform_report(),
|
||||
"warnings": warnings,
|
||||
"ok": not reasons,
|
||||
}
|
||||
@@ -2417,6 +2473,7 @@ def _diff_fields(a, b, prefix="") -> list[str]:
|
||||
|
||||
|
||||
def cmd_init(args) -> tuple[dict, int]:
|
||||
require_platform() # B1: the first step, before any path is read or written
|
||||
state = state_dir_for(args.run)
|
||||
snap = read_registry(state) if os.path.isdir(state) else None
|
||||
old_cfg = (snap or {}).get("config")
|
||||
@@ -3250,7 +3307,10 @@ def tick_pass(state: str) -> dict:
|
||||
"""One pass of the ping rule over every base of the run. The decision and the `ping_in_flight`
|
||||
mark are made under the registry lock (C3d), so concurrent tickers launch one ping per base.
|
||||
The binary check runs before the lock for the bases that look eligible. Pings go through the
|
||||
same hidden spawn as any job. Returns {"pinged": [job...], "bases": [decision...]}."""
|
||||
same hidden spawn as any job. Returns {"pinged": [job...], "bases": [decision...]}.
|
||||
On a closed platform gate (A1) it returns at once: no ping, nothing read or written."""
|
||||
if platform_gate() is not None:
|
||||
return {"pinged": [], "bases": []}
|
||||
reg = read_registry(state)
|
||||
if not reg or not reg.get("config"):
|
||||
return {"pinged": [], "bases": []}
|
||||
@@ -3977,6 +4037,8 @@ def run(argv) -> tuple[dict, int]:
|
||||
if args.command in CHILD_REFUSED and in_child():
|
||||
raise GremlinsError("`%s` is refused inside a gremlin (%s is set); a gremlin may only run where, "
|
||||
"list, usage and wait" % (args.command, CHILD_MARKER), command=args.command)
|
||||
if args.command in GATED_COMMANDS:
|
||||
require_platform() # A1: an existing registry launches nothing on a closed gate
|
||||
return HANDLERS[args.command](args)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user