fable-engine 1.3.1__py3-none-any.whl
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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- tests/test_vector_engine.py +608 -0
fable_mode/launcher.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Package-aware command line entry point for the portable Fable runtime."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import stat
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .adapters import (RegistrationError, cleanup_recorded_registrations,
|
|
18
|
+
detect_hosts, register_hosts, validate_registration_record)
|
|
19
|
+
from .installer import Installer, InstallError, verify_installation
|
|
20
|
+
from .manifest import ALLOWED_FILES, validate_manifest
|
|
21
|
+
from . import __version__
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _assert_private_path(path: Path) -> None:
|
|
25
|
+
cur = path
|
|
26
|
+
parts = []
|
|
27
|
+
while True:
|
|
28
|
+
parts.append(cur)
|
|
29
|
+
if cur.parent == cur:
|
|
30
|
+
break
|
|
31
|
+
cur = cur.parent
|
|
32
|
+
for part in reversed(parts):
|
|
33
|
+
try:
|
|
34
|
+
st = part.lstat()
|
|
35
|
+
except FileNotFoundError:
|
|
36
|
+
continue
|
|
37
|
+
attrs = int(getattr(st, "st_file_attributes", 0))
|
|
38
|
+
reparse = bool(attrs & 0x400)
|
|
39
|
+
junction = bool(getattr(part, "is_junction", lambda: False)())
|
|
40
|
+
trusted_macos_alias = (
|
|
41
|
+
sys.platform == "darwin" and str(part) in {"/var", "/tmp"}
|
|
42
|
+
and str(part.resolve()) in {"/private/var", "/private/tmp"}
|
|
43
|
+
)
|
|
44
|
+
if ((reparse or junction or stat.S_ISLNK(st.st_mode)) and not trusted_macos_alias) or stat.S_ISSOCK(st.st_mode) or stat.S_ISFIFO(st.st_mode) or stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
|
|
45
|
+
raise RuntimeError("state path contains a symlink, reparse point, or special file")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _data_dir(state_dir: str | None) -> Path:
|
|
49
|
+
if state_dir:
|
|
50
|
+
path = Path(state_dir).expanduser().absolute()
|
|
51
|
+
elif os.environ.get("FABLE_DATA_DIR"):
|
|
52
|
+
path = Path(os.environ["FABLE_DATA_DIR"]).expanduser().absolute()
|
|
53
|
+
elif os.name == "nt" and os.environ.get("LOCALAPPDATA"):
|
|
54
|
+
path = Path(os.environ["LOCALAPPDATA"]) / "FableMode" / "data"
|
|
55
|
+
else:
|
|
56
|
+
path = Path.home() / ".local" / "share" / "fable-mode" / "data"
|
|
57
|
+
# Do not follow an attacker-controlled final directory or parent.
|
|
58
|
+
_assert_private_path(path)
|
|
59
|
+
try:
|
|
60
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
_assert_private_path(path)
|
|
62
|
+
if path.is_symlink() or not path.is_dir():
|
|
63
|
+
raise RuntimeError("state directory must be a real directory")
|
|
64
|
+
os.chmod(path, 0o700)
|
|
65
|
+
except (OSError, RuntimeError) as exc:
|
|
66
|
+
if isinstance(exc, RuntimeError):
|
|
67
|
+
raise
|
|
68
|
+
raise RuntimeError("could not create the state directory") from exc
|
|
69
|
+
return path
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def serve(state_dir: str | None = None) -> int:
|
|
73
|
+
data = _data_dir(state_dir)
|
|
74
|
+
os.environ["FABLE_DATA_DIR"] = str(data)
|
|
75
|
+
server = importlib.import_module("fable_engine.server")
|
|
76
|
+
server.main()
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _kill_process(proc: subprocess.Popen[bytes]) -> None:
|
|
81
|
+
try:
|
|
82
|
+
if os.name == "posix":
|
|
83
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
84
|
+
else:
|
|
85
|
+
# CREATE_NEW_PROCESS_GROUP is used below. Terminate the direct
|
|
86
|
+
# child; Windows Job Objects are not available in stdlib, so this
|
|
87
|
+
# fallback deliberately fails safe rather than invoking a shell.
|
|
88
|
+
proc.kill()
|
|
89
|
+
except (OSError, ProcessLookupError):
|
|
90
|
+
try:
|
|
91
|
+
proc.kill()
|
|
92
|
+
except OSError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _smoke(argv: list[str], data_dir: Path) -> tuple[bool, str]:
|
|
97
|
+
"""Bounded MCP initialize/tools-list probe with a sanitized environment."""
|
|
98
|
+
if not argv or any(not isinstance(item, str) or "\x00" in item for item in argv):
|
|
99
|
+
return False, "MCP smoke check received invalid argv"
|
|
100
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
os.chmod(data_dir, 0o700)
|
|
102
|
+
probe_home = Path(tempfile.mkdtemp(prefix="fable-mcp-home-"))
|
|
103
|
+
os.chmod(probe_home, 0o700)
|
|
104
|
+
def finish(value: tuple[bool, str]) -> tuple[bool, str]:
|
|
105
|
+
import shutil
|
|
106
|
+
shutil.rmtree(probe_home, ignore_errors=True)
|
|
107
|
+
return value
|
|
108
|
+
env = {
|
|
109
|
+
"PATH": os.pathsep.join(dict.fromkeys([str(Path(sys.executable).parent), os.defpath])),
|
|
110
|
+
"HOME": str(probe_home),
|
|
111
|
+
"USERPROFILE": str(probe_home),
|
|
112
|
+
"XDG_CONFIG_HOME": str(probe_home / ".config"),
|
|
113
|
+
"FABLE_DATA_DIR": str(data_dir),
|
|
114
|
+
"PYTHONIOENCODING": "utf-8",
|
|
115
|
+
}
|
|
116
|
+
if os.name == "nt":
|
|
117
|
+
for var in ("SystemRoot", "SYSTEMROOT", "windir", "PATHEXT", "TEMP", "TMP"):
|
|
118
|
+
if var in os.environ:
|
|
119
|
+
env[var] = os.environ[var]
|
|
120
|
+
kwargs: dict = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "env": env, "cwd": str(data_dir)}
|
|
121
|
+
if os.name == "posix":
|
|
122
|
+
kwargs["start_new_session"] = True
|
|
123
|
+
elif hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
|
|
124
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
125
|
+
proc: subprocess.Popen[bytes] | None = None
|
|
126
|
+
captured = {"out": bytearray(), "err": bytearray()}
|
|
127
|
+
def drain(pipe, key: str) -> None:
|
|
128
|
+
try:
|
|
129
|
+
while True:
|
|
130
|
+
chunk = pipe.read(4096)
|
|
131
|
+
if not chunk:
|
|
132
|
+
return
|
|
133
|
+
remaining = 32768 - len(captured[key])
|
|
134
|
+
if remaining > 0:
|
|
135
|
+
captured[key].extend(chunk[:remaining])
|
|
136
|
+
finally:
|
|
137
|
+
pipe.close()
|
|
138
|
+
try:
|
|
139
|
+
proc = subprocess.Popen(argv, **kwargs)
|
|
140
|
+
request = (json.dumps({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}) + "\n" +
|
|
141
|
+
json.dumps({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}) + "\n").encode()
|
|
142
|
+
assert proc.stdin is not None
|
|
143
|
+
proc.stdin.write(request)
|
|
144
|
+
proc.stdin.flush()
|
|
145
|
+
proc.stdin.close()
|
|
146
|
+
readers = [threading.Thread(target=drain, args=(proc.stdout, "out"), daemon=True),
|
|
147
|
+
threading.Thread(target=drain, args=(proc.stderr, "err"), daemon=True)]
|
|
148
|
+
for reader in readers:
|
|
149
|
+
reader.start()
|
|
150
|
+
|
|
151
|
+
def _has_responses() -> bool:
|
|
152
|
+
lines = bytes(captured["out"]).splitlines()[:8]
|
|
153
|
+
seen_ids = set()
|
|
154
|
+
for line in lines:
|
|
155
|
+
if not line.strip():
|
|
156
|
+
continue
|
|
157
|
+
try:
|
|
158
|
+
item = json.loads(line)
|
|
159
|
+
if isinstance(item, dict) and "id" in item:
|
|
160
|
+
seen_ids.add(item["id"])
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return {1, 2}.issubset(seen_ids)
|
|
164
|
+
|
|
165
|
+
deadline = time.monotonic() + 8.0
|
|
166
|
+
while time.monotonic() < deadline:
|
|
167
|
+
if _has_responses() or proc.poll() is not None:
|
|
168
|
+
break
|
|
169
|
+
time.sleep(0.05)
|
|
170
|
+
|
|
171
|
+
if proc.poll() is None:
|
|
172
|
+
_kill_process(proc)
|
|
173
|
+
try:
|
|
174
|
+
proc.wait(timeout=2)
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
for reader in readers:
|
|
178
|
+
reader.join(timeout=2)
|
|
179
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
180
|
+
if proc is not None:
|
|
181
|
+
_kill_process(proc)
|
|
182
|
+
try:
|
|
183
|
+
proc.wait(timeout=2)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
return finish((False, "MCP smoke check failed: bounded process probe did not complete"))
|
|
187
|
+
# Never echo untrusted process output. JSON parsing is intentionally capped.
|
|
188
|
+
lines = bytes(captured["out"]).splitlines()[:8]
|
|
189
|
+
messages = []
|
|
190
|
+
for line in lines:
|
|
191
|
+
if not line.strip():
|
|
192
|
+
continue
|
|
193
|
+
try:
|
|
194
|
+
item = json.loads(line)
|
|
195
|
+
except (ValueError, UnicodeDecodeError):
|
|
196
|
+
return finish((False, "MCP smoke check returned invalid JSON"))
|
|
197
|
+
if not isinstance(item, dict) or item.get("jsonrpc") != "2.0" or "id" not in item:
|
|
198
|
+
return finish((False, "MCP smoke check returned an invalid JSON-RPC envelope"))
|
|
199
|
+
if "result" not in item or not isinstance(item["result"], dict) or "error" in item:
|
|
200
|
+
return finish((False, "MCP smoke check returned no valid result"))
|
|
201
|
+
messages.append(item)
|
|
202
|
+
ids = {m.get("id") for m in messages}
|
|
203
|
+
if {1, 2} - ids:
|
|
204
|
+
return finish((False, "MCP smoke check did not receive initialize/tools-list responses"))
|
|
205
|
+
by_id = {m["id"]: m for m in messages}
|
|
206
|
+
if not isinstance(by_id[1]["result"].get("protocolVersion"), str):
|
|
207
|
+
return finish((False, "MCP initialize response is missing protocolVersion"))
|
|
208
|
+
if not isinstance(by_id[2]["result"].get("tools"), list):
|
|
209
|
+
return finish((False, "MCP tools/list response is missing tools"))
|
|
210
|
+
return finish((True, "ok"))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _verify_packaged_manifest() -> tuple[bool, str]:
|
|
214
|
+
try:
|
|
215
|
+
from importlib.resources import files
|
|
216
|
+
payload = json.loads((files("fable_mode") / "resources.json").read_text(encoding="utf-8"))
|
|
217
|
+
validate_manifest(payload)
|
|
218
|
+
return True, "ok"
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
return False, f"packaged resource manifest unavailable: {exc}"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def verify(installer: Installer) -> int:
|
|
224
|
+
packaged_ok, packaged_detail = _verify_packaged_manifest()
|
|
225
|
+
if not packaged_ok:
|
|
226
|
+
print(f"verify: {packaged_detail}", file=sys.stderr)
|
|
227
|
+
return 1
|
|
228
|
+
ok, detail = verify_installation(installer.install_dir)
|
|
229
|
+
if not ok:
|
|
230
|
+
print(f"verify: {detail}", file=sys.stderr)
|
|
231
|
+
return 1
|
|
232
|
+
marker = json.loads((installer.install_dir / ".fable-install.json").read_text(encoding="utf-8"))
|
|
233
|
+
if marker.get("mode") == "source":
|
|
234
|
+
argv = [sys.executable, str(installer.install_dir / "runtime" / "fable_mode_entry.py"), "serve"]
|
|
235
|
+
else:
|
|
236
|
+
name = "fable-mode.exe" if os.name == "nt" else "fable-mode"
|
|
237
|
+
argv = [str(installer.install_dir / name), "serve"]
|
|
238
|
+
with tempfile.TemporaryDirectory(prefix="fable-verify-") as tmp:
|
|
239
|
+
ok, detail = _smoke(argv, Path(tmp) / "data")
|
|
240
|
+
if not ok:
|
|
241
|
+
print(f"verify: {detail}", file=sys.stderr)
|
|
242
|
+
return 1
|
|
243
|
+
print("Fable installation verified (manifest + MCP initialize/tools-list).")
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _is_frozen() -> bool:
|
|
248
|
+
return bool(getattr(sys, "frozen", False) or getattr(sys, "_MEIPASS", None))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _parser() -> argparse.ArgumentParser:
|
|
252
|
+
p = argparse.ArgumentParser(prog="fable-mode")
|
|
253
|
+
p.add_argument("--version", action="version", version=__version__)
|
|
254
|
+
p.add_argument("mode", nargs="?", choices=("install", "serve", "verify", "version", "uninstall"), default="install")
|
|
255
|
+
p.add_argument("--yes", action="store_true", help="confirm unattended installation")
|
|
256
|
+
p.add_argument("--register-hosts", action="store_true")
|
|
257
|
+
p.add_argument("--aliases", action="store_true", help="also probe legacy cc/antigravity aliases")
|
|
258
|
+
p.add_argument("--dry-run", action="store_true")
|
|
259
|
+
p.add_argument("--state-dir")
|
|
260
|
+
p.add_argument("--install-dir")
|
|
261
|
+
p.add_argument("--workspace")
|
|
262
|
+
return p
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _bind_registration_records(records: list[dict], install_dir: Path,
|
|
266
|
+
workspace: Path | None) -> None:
|
|
267
|
+
"""Bind host snapshots to this exact published install before persistence."""
|
|
268
|
+
install_st = install_dir.lstat()
|
|
269
|
+
if not stat.S_ISDIR(install_st.st_mode):
|
|
270
|
+
raise RuntimeError("published installation is not a directory")
|
|
271
|
+
for record in records:
|
|
272
|
+
if not isinstance(record, dict):
|
|
273
|
+
raise RuntimeError("host registration produced an invalid recovery record")
|
|
274
|
+
record["install_dir"] = str(install_dir)
|
|
275
|
+
record["install_identity"] = [install_st.st_dev, install_st.st_ino]
|
|
276
|
+
if record.get("kind") == "file" and Path(record.get("path", "")).parent.name == ".agents":
|
|
277
|
+
record["workspace"] = str(workspace) if workspace else ""
|
|
278
|
+
if not validate_registration_record(record, install_dir, Path.home(), transaction=True):
|
|
279
|
+
raise RuntimeError("host registration produced an invalid recovery record")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _records_for_marker(install_dir: Path, records: list[dict]) -> list[dict]:
|
|
283
|
+
"""Merge new records with still-owned records already in the marker."""
|
|
284
|
+
marker_data = json.loads((install_dir / ".fable-install.json").read_text(encoding="utf-8"))
|
|
285
|
+
prior_records = [record for record in marker_data.get("registrations", [])
|
|
286
|
+
if isinstance(record, dict)]
|
|
287
|
+
new_keys = {(record.get("kind"), record.get("host", record.get("path")))
|
|
288
|
+
for record in records}
|
|
289
|
+
retained = [record for record in prior_records
|
|
290
|
+
if (record.get("kind"), record.get("host", record.get("path"))) not in new_keys]
|
|
291
|
+
return retained + records
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def main(argv: list[str] | None = None) -> int:
|
|
295
|
+
args = _parser().parse_args(argv)
|
|
296
|
+
if args.mode == "version":
|
|
297
|
+
print(__version__)
|
|
298
|
+
return 0
|
|
299
|
+
if args.mode == "serve":
|
|
300
|
+
try:
|
|
301
|
+
return serve(args.state_dir)
|
|
302
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
303
|
+
print(f"serve: {exc}", file=sys.stderr)
|
|
304
|
+
return 1
|
|
305
|
+
installer = Installer(Path(args.install_dir) if args.install_dir else None)
|
|
306
|
+
if args.mode == "verify":
|
|
307
|
+
return verify(installer)
|
|
308
|
+
if args.mode == "uninstall":
|
|
309
|
+
if not args.dry_run and not args.yes and not sys.stdin.isatty():
|
|
310
|
+
print("uninstall requires --yes in unattended mode", file=sys.stderr)
|
|
311
|
+
return 2
|
|
312
|
+
try:
|
|
313
|
+
installer.uninstall(dry_run=args.dry_run)
|
|
314
|
+
print("Fable installation removed." if not args.dry_run else "Would remove Fable installation.")
|
|
315
|
+
return 0
|
|
316
|
+
except InstallError as exc:
|
|
317
|
+
print(f"uninstall: {exc}", file=sys.stderr)
|
|
318
|
+
return 1
|
|
319
|
+
if not args.dry_run and not (args.yes or args.register_hosts):
|
|
320
|
+
if not sys.stdin.isatty():
|
|
321
|
+
print("install requires --yes or --register-hosts in unattended mode", file=sys.stderr)
|
|
322
|
+
return 2
|
|
323
|
+
try:
|
|
324
|
+
confirmed = input(f"Install Fable to {installer.install_dir}? [y/N] ").strip().lower() in {"y", "yes"}
|
|
325
|
+
except EOFError:
|
|
326
|
+
print("install requires --yes or --register-hosts in unattended mode", file=sys.stderr)
|
|
327
|
+
return 2
|
|
328
|
+
except KeyboardInterrupt:
|
|
329
|
+
print("\nInstallation cancelled.")
|
|
330
|
+
return 0
|
|
331
|
+
if not confirmed:
|
|
332
|
+
print("Installation cancelled.")
|
|
333
|
+
return 0
|
|
334
|
+
registration_workspace: Path | None = None
|
|
335
|
+
try:
|
|
336
|
+
frozen = Path(sys.executable) if _is_frozen() else None
|
|
337
|
+
result = installer.install(frozen_executable=frozen, dry_run=args.dry_run)
|
|
338
|
+
if args.dry_run:
|
|
339
|
+
print(f"Would install Fable to {result.install_dir}; hosts are not probed.")
|
|
340
|
+
return 0
|
|
341
|
+
statuses = {}
|
|
342
|
+
registration_records: list[dict] = []
|
|
343
|
+
if args.register_hosts:
|
|
344
|
+
hosts = detect_hosts(aliases=args.aliases)
|
|
345
|
+
if sys.stdin.isatty() and not args.yes:
|
|
346
|
+
found = ", ".join(sorted(hosts)) or "none"
|
|
347
|
+
print(f"Detected host commands: {found}")
|
|
348
|
+
try:
|
|
349
|
+
reg_confirmed = input("Register Fable with these hosts? [y/N] ").strip().lower() in {"y", "yes"}
|
|
350
|
+
except (EOFError, KeyboardInterrupt):
|
|
351
|
+
reg_confirmed = False
|
|
352
|
+
if not reg_confirmed:
|
|
353
|
+
if result.transaction:
|
|
354
|
+
result.transaction.rollback()
|
|
355
|
+
print("Host registration cancelled.")
|
|
356
|
+
return 0
|
|
357
|
+
registration_workspace = Path(args.workspace).absolute() if args.workspace else None
|
|
358
|
+
statuses = register_hosts(hosts, result.executable_argv, workspace=registration_workspace,
|
|
359
|
+
records=registration_records,
|
|
360
|
+
owned_records=installer.previous_registrations)
|
|
361
|
+
if registration_records:
|
|
362
|
+
# Bind and validate marker records against this exact
|
|
363
|
+
# installation. Workspace config paths are also recorded
|
|
364
|
+
# explicitly so uninstall never accepts an arbitrary path from
|
|
365
|
+
# a marker.
|
|
366
|
+
_bind_registration_records(registration_records, result.install_dir,
|
|
367
|
+
registration_workspace)
|
|
368
|
+
# Keep ownership records for previously registered hosts that
|
|
369
|
+
# are now absent/unhealthy; otherwise a reinstall would orphan
|
|
370
|
+
# those old entries. Records for hosts touched successfully by
|
|
371
|
+
# this transaction are replaced by the new exact snapshots.
|
|
372
|
+
installer.record_registrations(
|
|
373
|
+
_records_for_marker(result.install_dir, registration_records))
|
|
374
|
+
if result.transaction:
|
|
375
|
+
result.transaction.commit()
|
|
376
|
+
print(f"Fable installed at {result.install_dir}")
|
|
377
|
+
if statuses:
|
|
378
|
+
print(json.dumps(statuses, sort_keys=True))
|
|
379
|
+
return 0
|
|
380
|
+
except Exception as exc:
|
|
381
|
+
# Host mutation and installation publication form one transaction. A
|
|
382
|
+
# marker write or backup commit can fail *after* register_hosts has
|
|
383
|
+
# successfully changed one or more hosts, so restore those hosts before
|
|
384
|
+
# removing the runtime they now reference. If restoration is not
|
|
385
|
+
# complete, deliberately retain the new installation and its marker so
|
|
386
|
+
# the registration remains usable/recoverable rather than leaving a
|
|
387
|
+
# silently stale host entry.
|
|
388
|
+
cleanup_errors: list[str] = []
|
|
389
|
+
persistence_error: Exception | None = None
|
|
390
|
+
preserve_install = False
|
|
391
|
+
if ('result' in locals() and result.transaction
|
|
392
|
+
and 'registration_records' in locals() and registration_records):
|
|
393
|
+
try:
|
|
394
|
+
cleanup_errors = cleanup_recorded_registrations(
|
|
395
|
+
registration_records, strict=True,
|
|
396
|
+
install_dir=result.install_dir, home=Path.home(),
|
|
397
|
+
_transaction=True)
|
|
398
|
+
except Exception as cleanup_exc:
|
|
399
|
+
cleanup_errors = [f"cleanup failed: {cleanup_exc}"]
|
|
400
|
+
preserve_install = bool(cleanup_errors)
|
|
401
|
+
if preserve_install:
|
|
402
|
+
# The first marker write may itself have failed after the
|
|
403
|
+
# hosts were changed. Keep the new install genuinely
|
|
404
|
+
# recoverable: persist the bound snapshots before returning
|
|
405
|
+
# the failure, even though the normal transaction rollback is
|
|
406
|
+
# being skipped. Rebinding also protects this recovery path
|
|
407
|
+
# if the original failure happened during record preparation.
|
|
408
|
+
try:
|
|
409
|
+
_bind_registration_records(
|
|
410
|
+
registration_records, result.install_dir,
|
|
411
|
+
registration_workspace)
|
|
412
|
+
installer.record_registrations(
|
|
413
|
+
_records_for_marker(result.install_dir, registration_records))
|
|
414
|
+
except Exception as persist_exc:
|
|
415
|
+
persistence_error = persist_exc
|
|
416
|
+
rollback_error: Exception | None = None
|
|
417
|
+
if not preserve_install and 'result' in locals() and result.transaction:
|
|
418
|
+
try:
|
|
419
|
+
result.transaction.rollback()
|
|
420
|
+
except Exception as rollback_exc:
|
|
421
|
+
rollback_error = rollback_exc
|
|
422
|
+
details = [str(exc)]
|
|
423
|
+
if cleanup_errors:
|
|
424
|
+
details.append("host registration cleanup incomplete; installation preserved for recovery: "
|
|
425
|
+
+ ", ".join(cleanup_errors))
|
|
426
|
+
if persistence_error:
|
|
427
|
+
details.append("could not persist registration recovery records; host state and installation were preserved: "
|
|
428
|
+
+ str(persistence_error))
|
|
429
|
+
if rollback_error:
|
|
430
|
+
details.append("installation rollback incomplete; partial state preserved")
|
|
431
|
+
print("install: " + "; ".join(details), file=sys.stderr)
|
|
432
|
+
return 1
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if __name__ == "__main__":
|
|
436
|
+
raise SystemExit(main())
|
|
437
|
+
|
fable_mode/manifest.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Explicit allowlist for portable release contents.
|
|
2
|
+
|
|
3
|
+
This list is deliberately boring and reviewable: no recursive source-tree copy is
|
|
4
|
+
performed by the installer or release build.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
ALLOWED_FILES: tuple[str, ...] = (
|
|
11
|
+
"fable_mode/__init__.py",
|
|
12
|
+
"fable_mode/__main__.py",
|
|
13
|
+
"fable_mode/adapters.py",
|
|
14
|
+
"fable_mode/installer.py",
|
|
15
|
+
"fable_mode/launcher.py",
|
|
16
|
+
"fable_mode/manifest.py",
|
|
17
|
+
"fable_mode/safety.py",
|
|
18
|
+
"fable_mode/resources.json",
|
|
19
|
+
"fable_engine/__init__.py",
|
|
20
|
+
"fable_engine/browser.py",
|
|
21
|
+
"fable_engine/actions/__init__.py",
|
|
22
|
+
"fable_engine/actions/cas.py",
|
|
23
|
+
"fable_engine/actions/deliberation.py",
|
|
24
|
+
"fable_engine/actions/fleet.py",
|
|
25
|
+
"fable_engine/actions/lifecycle.py",
|
|
26
|
+
"fable_engine/actions/scrapers.py",
|
|
27
|
+
"fable_engine/actions/system3.py",
|
|
28
|
+
"fable_engine/cas.py",
|
|
29
|
+
"fable_engine/scrapers/__init__.py",
|
|
30
|
+
"fable_engine/scrapers/arxiv.py",
|
|
31
|
+
"fable_engine/scrapers/base.py",
|
|
32
|
+
"fable_engine/scrapers/github.py",
|
|
33
|
+
"fable_engine/scrapers/reddit.py",
|
|
34
|
+
"fable_engine/scrapers/web.py",
|
|
35
|
+
"fable_engine/scrapers/x.py",
|
|
36
|
+
"fable_engine/scrapers/youtube.py",
|
|
37
|
+
"fable_engine/guards.py",
|
|
38
|
+
"fable_engine/schema.py",
|
|
39
|
+
"fable_engine/server.py",
|
|
40
|
+
"fable_engine/session.py",
|
|
41
|
+
"fable_engine/updater.py",
|
|
42
|
+
"fable_engine/fable_session.json",
|
|
43
|
+
"fable_v2/__init__.py",
|
|
44
|
+
"fable_v2/adapters.py",
|
|
45
|
+
"fable_v2/execution_broker.py",
|
|
46
|
+
"fable_v2/protocol.py",
|
|
47
|
+
"fable_v2/runtime.py",
|
|
48
|
+
"fable_v2/system3/__init__.py",
|
|
49
|
+
"fable_v2/system3/causal.py",
|
|
50
|
+
"fable_v2/system3/dialectical.py",
|
|
51
|
+
"fable_v2/system3/evolution.py",
|
|
52
|
+
"fable_v2/system3/executive.py",
|
|
53
|
+
"fable_v2/system3/free_energy.py",
|
|
54
|
+
"fable_v2/system3/hyperbolic.py",
|
|
55
|
+
"fable_v2/system3/induction.py",
|
|
56
|
+
"fable_v2/system3/kripke.py",
|
|
57
|
+
"fable_v2/system3/oracle.py",
|
|
58
|
+
"fable_v2/verifiers.py",
|
|
59
|
+
"fable_v2/proof_engine.py",
|
|
60
|
+
"fable_v2/coder_fleet/__init__.py",
|
|
61
|
+
"fable_v2/coder_fleet/ast_tools.py",
|
|
62
|
+
"fable_v2/coder_fleet/compute.py",
|
|
63
|
+
"fable_v2/coder_fleet/diagnostics.py",
|
|
64
|
+
"fable_v2/coder_fleet/fleet_dispatcher.py",
|
|
65
|
+
"fable_v2/coder_fleet/mock_auditor.py",
|
|
66
|
+
"fable_v2/coder_fleet/mutation.py",
|
|
67
|
+
"fable_v2/coder_fleet/property_oracle.py",
|
|
68
|
+
"fable_v2/coder_fleet/receipt_attestor.py",
|
|
69
|
+
"fable_v2/coder_fleet/test_harness.py",
|
|
70
|
+
"fable_v2/coder_fleet/visual.py",
|
|
71
|
+
"fable_v2/coder_fleet/workspace.py",
|
|
72
|
+
"fable_v2/coder_fleet/red_team_swarm.py",
|
|
73
|
+
"fable_v2/coder_fleet/vector_engine.py",
|
|
74
|
+
"fable_v2/coder_fleet/design_engine.py",
|
|
75
|
+
"fable_v2/cortical/__init__.py",
|
|
76
|
+
"fable_v2/cortical/plasticity_engine.py",
|
|
77
|
+
"fable_compressor.py",
|
|
78
|
+
"run_tests.py",
|
|
79
|
+
"rules/AGENTS.md",
|
|
80
|
+
"rules/GEMINI.md",
|
|
81
|
+
"rules/fable-mode.md",
|
|
82
|
+
"docs/fable-v2-architecture.md",
|
|
83
|
+
"docs/fable-v1-v2-migration.md",
|
|
84
|
+
"docs/system3-architecture.md",
|
|
85
|
+
"skills/fable-mode/SKILL.md",
|
|
86
|
+
"skills/fable-mode/examples/autonomous-agentic-migration.md",
|
|
87
|
+
"skills/fable-mode/examples/breakthrough-algorithm-synthesis.md",
|
|
88
|
+
"skills/fable-mode/examples/deepthink-analysis-proof.md",
|
|
89
|
+
"skills/fable-mode/examples/distributed-system-design.md",
|
|
90
|
+
"skills/fable-mode/examples/swe-bench-pro-debugging.md",
|
|
91
|
+
"skills/fable-mode/examples/weak_model_ollama_setup.md",
|
|
92
|
+
"skills/fable-mode/references/aaa-threejs-game-engine.md",
|
|
93
|
+
"skills/fable-mode/references/agentic-execution.md",
|
|
94
|
+
"skills/fable-mode/references/architectural-blueprinting.md",
|
|
95
|
+
"skills/fable-mode/references/cinematic-design-engine.md",
|
|
96
|
+
"skills/fable-mode/references/anti-slop-frontend-architecture.md",
|
|
97
|
+
"skills/fable-mode/references/cognitive-protocol.md",
|
|
98
|
+
"skills/fable-mode/references/deepthink-mode.md",
|
|
99
|
+
"skills/fable-mode/references/design-tokens-and-typographies.md",
|
|
100
|
+
"skills/fable-mode/references/goal-rubric-and-pipeline-automation.md",
|
|
101
|
+
"skills/fable-mode/references/innovation-engine.md",
|
|
102
|
+
"skills/fable-mode/references/interleaved-verification.md",
|
|
103
|
+
"skills/fable-mode/references/model-velocity-calibration.md",
|
|
104
|
+
"skills/fable-mode/references/prompt-scaffolds.md",
|
|
105
|
+
"skills/fable-mode/references/proof-architecture.md",
|
|
106
|
+
"skills/fable-mode/references/svg-craft-and-vector-design.md",
|
|
107
|
+
"skills/fable-mode/references/system2-session-engine.md",
|
|
108
|
+
"skills/fable-mode/references/system3-meta-cognition.md",
|
|
109
|
+
"skills/fable-mode/references/visual-imagination-engine.md",
|
|
110
|
+
"skills/fable-mode/references/weak-model-frontier-uplift.md",
|
|
111
|
+
"skills/fable-mode/references/adversarial-code-review-swarm.md",
|
|
112
|
+
"skills/fable-mode/references/hebbian-cortical-plasticity.md",
|
|
113
|
+
"skills/fable-mode/cortex/rust.md",
|
|
114
|
+
"skills/fable-mode/cortex/python.md",
|
|
115
|
+
"skills/fable-mode/cortex/design_3d.md",
|
|
116
|
+
"skills/fable-mode/cortex/frontend_design.md",
|
|
117
|
+
"skills/fable-mode/cortex/research.md",
|
|
118
|
+
"skills/fable-mode/cortex/concurrency.md",
|
|
119
|
+
"LICENSE",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_manifest(payload: object, allowed_files: tuple[str, ...] | list[str] | None = None) -> None:
|
|
124
|
+
"""Validate the packaged manifest as a strict, case-insensitive allowlist."""
|
|
125
|
+
if not isinstance(payload, dict) or payload.get("format") != 1:
|
|
126
|
+
raise ValueError("resource manifest format is unsupported")
|
|
127
|
+
files = payload.get("files")
|
|
128
|
+
target_allowed = ALLOWED_FILES if allowed_files is None else tuple(allowed_files)
|
|
129
|
+
if not isinstance(files, list) or set(files) != set(target_allowed) or len(files) != len(target_allowed):
|
|
130
|
+
raise ValueError("resource manifest does not match the canonical allowlist")
|
|
131
|
+
folded = [Path(str(item)).as_posix().casefold() for item in files]
|
|
132
|
+
if any(Path(str(item)).is_absolute() or ".." in Path(str(item)).parts for item in files) or len(set(folded)) != len(folded):
|
|
133
|
+
raise ValueError("resource manifest contains unsafe or colliding paths")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def source_root() -> Path:
|
|
137
|
+
return Path(__file__).resolve().parents[1]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def allowed_source_paths(root: Path | None = None) -> list[Path]:
|
|
141
|
+
root = Path(root or source_root())
|
|
142
|
+
return [root / rel for rel in ALLOWED_FILES]
|