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.
Files changed (104) hide show
  1. fable_compressor.py +356 -0
  2. fable_engine/__init__.py +1 -0
  3. fable_engine/actions/__init__.py +291 -0
  4. fable_engine/actions/cas.py +182 -0
  5. fable_engine/actions/deliberation.py +523 -0
  6. fable_engine/actions/fleet.py +807 -0
  7. fable_engine/actions/lifecycle.py +298 -0
  8. fable_engine/actions/scrapers.py +116 -0
  9. fable_engine/actions/system3.py +815 -0
  10. fable_engine/browser.py +824 -0
  11. fable_engine/cas.py +974 -0
  12. fable_engine/fable_session.json +510 -0
  13. fable_engine/guards.py +283 -0
  14. fable_engine/schema.py +714 -0
  15. fable_engine/scrapers/__init__.py +32 -0
  16. fable_engine/scrapers/arxiv.py +115 -0
  17. fable_engine/scrapers/base.py +386 -0
  18. fable_engine/scrapers/github.py +129 -0
  19. fable_engine/scrapers/reddit.py +154 -0
  20. fable_engine/scrapers/web.py +120 -0
  21. fable_engine/scrapers/x.py +125 -0
  22. fable_engine/scrapers/youtube.py +132 -0
  23. fable_engine/server.py +414 -0
  24. fable_engine/session.py +1819 -0
  25. fable_engine/test_server.py +1362 -0
  26. fable_engine/updater.py +541 -0
  27. fable_engine-1.3.1.dist-info/LICENSE +22 -0
  28. fable_engine-1.3.1.dist-info/METADATA +173 -0
  29. fable_engine-1.3.1.dist-info/RECORD +104 -0
  30. fable_engine-1.3.1.dist-info/WHEEL +5 -0
  31. fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
  32. fable_engine-1.3.1.dist-info/top_level.txt +6 -0
  33. fable_mode/__init__.py +3 -0
  34. fable_mode/__main__.py +4 -0
  35. fable_mode/adapters.py +1014 -0
  36. fable_mode/installer.py +553 -0
  37. fable_mode/launcher.py +437 -0
  38. fable_mode/manifest.py +142 -0
  39. fable_mode/resources.json +114 -0
  40. fable_mode/safety.py +103 -0
  41. fable_mode_entry.py +10 -0
  42. fable_v2/__init__.py +146 -0
  43. fable_v2/adapters.py +151 -0
  44. fable_v2/coder_fleet/__init__.py +100 -0
  45. fable_v2/coder_fleet/ast_tools.py +158 -0
  46. fable_v2/coder_fleet/compute.py +199 -0
  47. fable_v2/coder_fleet/design_engine.py +1316 -0
  48. fable_v2/coder_fleet/diagnostics.py +293 -0
  49. fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
  50. fable_v2/coder_fleet/mock_auditor.py +306 -0
  51. fable_v2/coder_fleet/mutation.py +216 -0
  52. fable_v2/coder_fleet/property_oracle.py +260 -0
  53. fable_v2/coder_fleet/receipt_attestor.py +122 -0
  54. fable_v2/coder_fleet/red_team_swarm.py +908 -0
  55. fable_v2/coder_fleet/test_harness.py +198 -0
  56. fable_v2/coder_fleet/vector_engine.py +1287 -0
  57. fable_v2/coder_fleet/visual.py +357 -0
  58. fable_v2/coder_fleet/workspace.py +153 -0
  59. fable_v2/cortical/__init__.py +20 -0
  60. fable_v2/cortical/plasticity_engine.py +992 -0
  61. fable_v2/execution_broker.py +811 -0
  62. fable_v2/proof_engine.py +1141 -0
  63. fable_v2/protocol.py +485 -0
  64. fable_v2/runtime.py +1010 -0
  65. fable_v2/system3/__init__.py +204 -0
  66. fable_v2/system3/causal.py +558 -0
  67. fable_v2/system3/dialectical.py +577 -0
  68. fable_v2/system3/evolution.py +503 -0
  69. fable_v2/system3/executive.py +338 -0
  70. fable_v2/system3/free_energy.py +479 -0
  71. fable_v2/system3/hyperbolic.py +555 -0
  72. fable_v2/system3/induction.py +336 -0
  73. fable_v2/system3/kripke.py +548 -0
  74. fable_v2/system3/oracle.py +745 -0
  75. fable_v2/verifiers.py +72 -0
  76. tests/__init__.py +1 -0
  77. tests/test_anti_loop_circuit_breaker.py +64 -0
  78. tests/test_auto_updater.py +407 -0
  79. tests/test_coder_fleet.py +535 -0
  80. tests/test_delegation_compiler.py +54 -0
  81. tests/test_descriptor_boundaries.py +126 -0
  82. tests/test_design_engine.py +603 -0
  83. tests/test_epistemic_evidence_validator.py +66 -0
  84. tests/test_execution_broker.py +233 -0
  85. tests/test_fable_v2.py +406 -0
  86. tests/test_fleet_transitions.py +116 -0
  87. tests/test_fsm_redteam_evolution.py +406 -0
  88. tests/test_goal_rubric_and_pipeline.py +367 -0
  89. tests/test_hebbian_plasticity.py +585 -0
  90. tests/test_packaging_runtime.py +194 -0
  91. tests/test_proof_engine.py +259 -0
  92. tests/test_red_team_swarm.py +645 -0
  93. tests/test_redteam_remediation.py +169 -0
  94. tests/test_registration_transaction.py +375 -0
  95. tests/test_requested_regressions.py +467 -0
  96. tests/test_scrapers.py +370 -0
  97. tests/test_server_actions.py +93 -0
  98. tests/test_server_frontier_actions.py +269 -0
  99. tests/test_server_protocol.py +88 -0
  100. tests/test_stealth_browser.py +970 -0
  101. tests/test_system3.py +381 -0
  102. tests/test_system3_deep_integration.py +385 -0
  103. tests/test_system3_frontier.py +436 -0
  104. 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]