pantheon-opencode 1.5.0-beta.1 → 1.5.0-beta.11

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 (83) hide show
  1. package/.pantheon/code-mode/manifest.json +21 -0
  2. package/CHANGELOG.md +1054 -0
  3. package/README.md +171 -16
  4. package/README.pt-BR.md +99 -13
  5. package/bin/pantheon-init.mjs +178 -21
  6. package/package.json +23 -15
  7. package/plugin.json +1 -1
  8. package/pyproject.toml +65 -0
  9. package/scripts/code_mode_server.py +431 -55
  10. package/scripts/cost.mjs +30 -14
  11. package/scripts/dev-tui.sh +48 -0
  12. package/scripts/doctor.mjs +207 -9
  13. package/scripts/install/cli-ui.mjs +17 -11
  14. package/scripts/install/config-migration.mjs +4 -6
  15. package/scripts/install/opencode.mjs +137 -35
  16. package/scripts/install/plugin.mjs +3 -33
  17. package/scripts/install/shared.mjs +43 -2
  18. package/scripts/install/strings.mjs +129 -0
  19. package/scripts/install-mcp.mjs +1 -0
  20. package/scripts/manifest-inventory.mjs +410 -0
  21. package/scripts/mcp_persistence_server.py +537 -128
  22. package/scripts/package-evidence.mjs +242 -0
  23. package/scripts/probe-context-rehydrate.mjs +240 -0
  24. package/scripts/probe-pantheon-cost.mjs +313 -0
  25. package/scripts/release-beta-version.mjs +56 -60
  26. package/scripts/sync-artifacts.mjs +169 -0
  27. package/scripts/sync-tui.mjs +121 -53
  28. package/scripts/test-opencode-v1-v2-sandbox.sh +367 -96
  29. package/scripts/validation-policy.mjs +7 -7
  30. package/scripts/version-check.mjs +113 -108
  31. package/scripts/versioning.mjs +230 -86
  32. package/src/agents/mnemosyne.md +1 -1
  33. package/src/agents/zeus.md +2 -2
  34. package/src/mcp/code_mode_server.py +431 -55
  35. package/src/mcp/mcp_persistence_server.py +537 -128
  36. package/src/pantheon/auto-wake.ts +1 -5
  37. package/src/pantheon/background-job-board.ts +43 -1
  38. package/src/pantheon/compaction-handoff.ts +304 -0
  39. package/src/pantheon/cost-command.ts +345 -62
  40. package/src/pantheon/delegate-manager.ts +390 -8
  41. package/src/pantheon/delegation-finalize.ts +32 -0
  42. package/src/pantheon/delegation.ts +235 -18
  43. package/src/pantheon/file-persistence-salvage.ts +64 -0
  44. package/src/pantheon/file-persistence.ts +194 -45
  45. package/src/pantheon/native-probe.ts +234 -0
  46. package/src/pantheon/native-task-status.ts +139 -0
  47. package/src/pantheon/provider-capabilities.ts +221 -0
  48. package/src/pantheon/provider-stream-tracker.ts +182 -0
  49. package/src/pantheon/shared-board.ts +37 -0
  50. package/src/pantheon/task-result-guard.ts +107 -0
  51. package/src/pantheon/token-opt.ts +146 -12
  52. package/src/pantheon/tool-ceiling.ts +159 -0
  53. package/src/pantheon/v2-events.ts +18 -1
  54. package/src/plugin-v2.ts +2 -14
  55. package/src/plugin.ts +28 -17
  56. package/src/plugins/pantheon-hooks.ts +51 -0
  57. package/src/plugins/tui/dist/server.d.ts +1 -1
  58. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  59. package/src/plugins/tui/dist/server.js +1 -1
  60. package/src/plugins/tui/dist/server.js.map +1 -1
  61. package/src/plugins/tui/dist/tui.d.ts +137 -27
  62. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  63. package/src/plugins/tui/dist/tui.js +459 -163
  64. package/src/plugins/tui/dist/tui.js.map +1 -1
  65. package/src/plugins/tui/dist/tui.tsx +506 -103
  66. package/src/plugins/tui/package-lock.json +2 -2
  67. package/src/plugins/tui/package.json +1 -1
  68. package/src/plugins/tui/src/index.tsx +506 -103
  69. package/.pantheon/code-mode/.gitkeep +0 -0
  70. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +0 -8
  71. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +0 -8
  72. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +0 -8
  73. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +0 -8
  74. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +0 -8
  75. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +0 -8
  76. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +0 -8
  77. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +0 -8
  78. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +0 -8
  79. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +0 -8
  80. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +0 -8
  81. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +0 -8
  82. package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +0 -11
  83. package/src/pantheon/delegate-task-adapter.ts +0 -444
@@ -16,6 +16,8 @@ Or via MCP client (stdio transport):
16
16
  from __future__ import annotations
17
17
 
18
18
  import asyncio
19
+ import hashlib
20
+ import json
19
21
  import logging
20
22
  import os
21
23
  import shutil
@@ -38,21 +40,47 @@ SCRIPT_TIMEOUT: int = 30
38
40
  # executor for longer than 5 minutes.
39
41
  MAX_SCRIPT_TIMEOUT: int = 300
40
42
 
43
+ # ── Common status contract ────────────────────────────────────────────────────
44
+ # Mirrors the nine-code `NativeTaskStatus` contract introduced in B3-05. Every
45
+ # code-mode result is labelled with one of these statuses so callers can apply
46
+ # the same retry/skip/escalate strategy across the whole platform.
47
+ CONTRACT_STATUSES: frozenset[str] = frozenset(
48
+ {
49
+ "OK",
50
+ "UNSUPPORTED",
51
+ "UNAVAILABLE",
52
+ "INVALID_INPUT",
53
+ "INVALID_STATE",
54
+ "CONFLICT",
55
+ "CORRUPT_DATA",
56
+ "TIMEOUT",
57
+ "ESCALATE",
58
+ }
59
+ )
60
+
61
+ # Explicit installation mode: a script only runs when it is listed in this
62
+ # manifest with a matching SHA-256. No manifest → nothing executes.
63
+ MANIFEST_FILENAME: str = "manifest.json"
64
+ MANIFEST_VERSION: int = 1
65
+ _SHA256_HEX_LEN: int = 64
66
+
41
67
  # ── Env Allowlist ──────────────────────────────────────────────────────────────
42
68
  # Only these variables are passed to script subprocesses. Everything else
43
69
  # (secrets, cloud credentials, etc.) is stripped to limit blast radius.
44
- _CODE_MODE_ENV_ALLOWLIST: frozenset[str] = frozenset({
45
- "PATH",
46
- "HOME",
47
- "LANG",
48
- "PANTHEON_HOME",
49
- "PANTHEON_PROJECT",
50
- "OPENAI_API_KEY",
51
- "OPENAI_BASE_URL",
52
- "EVAL_JUDGE_MODEL",
53
- "SHELL",
54
- "USER",
55
- })
70
+ _CODE_MODE_ENV_ALLOWLIST: frozenset[str] = frozenset(
71
+ {
72
+ "PATH",
73
+ "HOME",
74
+ "LANG",
75
+ "PANTHEON_HOME",
76
+ "PANTHEON_PROJECT",
77
+ "OPENAI_API_KEY",
78
+ "OPENAI_BASE_URL",
79
+ "EVAL_JUDGE_MODEL",
80
+ "SHELL",
81
+ "USER",
82
+ }
83
+ )
56
84
 
57
85
  # Optional extra vars that callers may opt-in to pass through. Populate
58
86
  # this set at startup if needed (e.g. ``CODE_MODE_EXTRA_ENV.add("MY_VAR")``).
@@ -127,13 +155,54 @@ def _detect_prlimit() -> str | None:
127
155
  return None
128
156
  return shutil.which("prlimit")
129
157
 
158
+
130
159
  # ── Scripts Directory Resolution ─────────────────────────────────────────────
131
- # Priority:
132
- # 1. /.opencode/.pantheon/code-mode/ (project install)
133
- # 2. /.pantheon/code-mode/ (legacy fallback)
134
- # 3. /.pantheon/code-mode/ (global fallback)
135
- # 4. .pantheon/code-mode/ shipped inside the installed package (tarball
136
- # fallback — package.json `files` includes .pantheon/code-mode/**)
160
+ # Project directories are selected before global directories. Once a usable
161
+ # project directory is selected, manifest errors are deliberately fail-closed;
162
+ # execution must never silently cross the project/global trust boundary.
163
+
164
+
165
+ def _is_within(path: Path, root: Path) -> bool:
166
+ """Return whether a resolved *path* is contained by a resolved *root*."""
167
+ try:
168
+ return os.path.commonpath((str(root), str(path))) == str(root)
169
+ except (OSError, ValueError):
170
+ return False
171
+
172
+
173
+ def _has_usable_scripts(scripts_dir: Path) -> bool:
174
+ """Return whether *scripts_dir* contains an executable code-mode target.
175
+
176
+ A directory is only a valid resolver candidate when it contains a regular
177
+ ``.py`` or ``.sh`` file that can pass the same direct-child and path
178
+ containment rules used by script execution. This prevents an empty
179
+ project overlay (or a directory containing only metadata) from masking a
180
+ lower-priority installation.
181
+ """
182
+ try:
183
+ if scripts_dir.is_symlink():
184
+ return False
185
+ resolved_dir = scripts_dir.resolve(strict=True)
186
+ if not resolved_dir.is_dir():
187
+ return False
188
+ for script_path in scripts_dir.iterdir():
189
+ if script_path.name.startswith("."):
190
+ continue
191
+ if script_path.suffix.lower() not in ALLOWED_EXTENSIONS:
192
+ continue
193
+ try:
194
+ resolved_script = script_path.resolve(strict=True)
195
+ except (OSError, RuntimeError):
196
+ continue
197
+ if (
198
+ resolved_script.is_file()
199
+ and resolved_script.parent == resolved_dir
200
+ and _is_within(resolved_script, resolved_dir)
201
+ ):
202
+ return True
203
+ except (OSError, RuntimeError):
204
+ return False
205
+ return False
137
206
 
138
207
 
139
208
  def _packaged_scripts_dir() -> Path | None:
@@ -144,39 +213,51 @@ def _packaged_scripts_dir() -> Path | None:
144
213
  Returns None when no packaged copy exists.
145
214
  """
146
215
  try:
147
- here = Path(__file__).resolve()
148
- except Exception:
216
+ here = Path(__file__).resolve(strict=True)
217
+ except (OSError, RuntimeError):
149
218
  return None
150
219
  for parent in [here.parent, *here.parents]:
151
220
  try:
152
221
  candidate = parent / ".pantheon" / "code-mode"
153
222
  except Exception:
154
223
  continue
155
- try:
156
- if candidate.is_dir():
157
- return candidate
158
- except OSError:
159
- continue
224
+ if _has_usable_scripts(candidate):
225
+ return candidate
160
226
  return None
161
227
 
162
228
 
229
+ def _resolve_scripts_dir(candidates: list[Path], *, allow_packaged: bool = True) -> Path | None:
230
+ """Select the first usable candidate, optionally trying the package copy."""
231
+ for candidate in candidates:
232
+ if _has_usable_scripts(candidate):
233
+ return candidate
234
+ return _packaged_scripts_dir() if allow_packaged else None
235
+
236
+
237
+ def _resolve_code_mode_dir() -> Path:
238
+ """Resolve code-mode scripts project-first, with a global-only fallback.
239
+
240
+ A project directory is a trust boundary: if one is selected, callers use
241
+ its manifest even when that manifest is missing or corrupt. Global users
242
+ retain the packaged/global fallback when no usable project directory is
243
+ available.
244
+ """
245
+ project = pantheon_project()
246
+ if project is not None:
247
+ project_candidates = [
248
+ project / ".opencode" / ".pantheon" / "code-mode",
249
+ project / ".pantheon" / "code-mode",
250
+ ]
251
+ selected = _resolve_scripts_dir(project_candidates, allow_packaged=False)
252
+ if selected is not None:
253
+ return selected
254
+
255
+ global_dir = pantheon_home() / ".pantheon" / "code-mode"
256
+ return _resolve_scripts_dir([global_dir]) or global_dir
257
+
258
+
163
259
  _PANTHEON_HOME: Path = pantheon_home()
164
- _SCRIPTS_DIR_CANDIDATES: list[Path] = []
165
- _proj = pantheon_project()
166
- if _proj is not None:
167
- _SCRIPTS_DIR_CANDIDATES.append(_proj / ".opencode" / ".pantheon" / "code-mode")
168
- _SCRIPTS_DIR_CANDIDATES.append(_proj / ".pantheon" / "code-mode")
169
- _SCRIPTS_DIR_CANDIDATES.append(_PANTHEON_HOME / ".pantheon" / "code-mode")
170
-
171
- SCRIPTS_DIR: Path = _PANTHEON_HOME / ".pantheon" / "code-mode" # default
172
- for _candidate in _SCRIPTS_DIR_CANDIDATES:
173
- if _candidate.is_dir():
174
- SCRIPTS_DIR = _candidate
175
- break
176
- else:
177
- _packaged = _packaged_scripts_dir()
178
- if _packaged is not None:
179
- SCRIPTS_DIR = _packaged
260
+ SCRIPTS_DIR: Path = _resolve_code_mode_dir()
180
261
 
181
262
  # ── FastMCP App ───────────────────────────────────────────────────────────────
182
263
  mcp = FastMCP(
@@ -188,12 +269,19 @@ mcp = FastMCP(
188
269
  # ── Helpers ───────────────────────────────────────────────────────────────────
189
270
 
190
271
 
191
- def _validate_script_name(script_name: str) -> Path:
272
+ def _validate_script_name(script_name: str, scripts_dir: Path | None = None) -> Path:
192
273
  """Validate a script name and return its resolved path.
193
274
 
194
- Raises ValueError if the name is invalid, traverses paths, or
195
- has a disallowed extension.
275
+ Args:
276
+ script_name: Bare file name of a script in the code-mode directory.
277
+ scripts_dir: Directory to resolve against. Defaults to ``SCRIPTS_DIR``.
278
+
279
+ Raises:
280
+ ValueError: If the name is invalid, traverses paths, or has a
281
+ disallowed extension.
196
282
  """
283
+ scripts_dir = scripts_dir or SCRIPTS_DIR
284
+
197
285
  if not script_name:
198
286
  raise ValueError("Script name cannot be empty")
199
287
 
@@ -208,15 +296,238 @@ def _validate_script_name(script_name: str) -> Path:
208
296
  allowed = ", ".join(sorted(ALLOWED_EXTENSIONS))
209
297
  raise ValueError(f"Extension '{ext}' not allowed. Allowed: {allowed}")
210
298
 
211
- script_path = (SCRIPTS_DIR / name).resolve()
212
- if not str(script_path).startswith(str(SCRIPTS_DIR.resolve())):
299
+ if scripts_dir.is_symlink():
300
+ raise ValueError(f"Invalid script name: '{script_name}'")
301
+
302
+ try:
303
+ scripts_root = scripts_dir.resolve(strict=True)
304
+ script_path = (scripts_dir / name).resolve(strict=True)
305
+ except (FileNotFoundError, OSError, RuntimeError):
306
+ raise ValueError(f"Script '{script_name}' not found") from None
307
+
308
+ if (
309
+ not _is_within(script_path, scripts_root)
310
+ or script_path.parent != scripts_root
311
+ or not script_path.is_file()
312
+ ):
213
313
  raise ValueError(f"Invalid script name: '{script_name}'")
214
- if not script_path.exists():
215
- raise ValueError(f"Script '{script_name}' not found")
216
314
 
217
315
  return script_path
218
316
 
219
317
 
318
+ # ── Installation Mode: Manifest + SHA-256 ─────────────────────────────────────
319
+ # The manifest is the explicit opt-in. A script is executable only when its
320
+ # bare name is present in `manifest.json` AND the SHA-256 of the on-disk file
321
+ # matches the recorded digest. Any deviation fails closed with a contract
322
+ # status instead of executing.
323
+
324
+
325
+ class ManifestError(Exception):
326
+ """Raised when the code-mode manifest cannot authorise execution."""
327
+
328
+ def __init__(self, status: str, message: str) -> None:
329
+ super().__init__(message)
330
+ self.status = status
331
+ self.message = message
332
+
333
+
334
+ def _manifest_path(scripts_dir: Path | None = None) -> Path:
335
+ """Return the manifest path for *scripts_dir* (defaults to SCRIPTS_DIR)."""
336
+ return (scripts_dir or SCRIPTS_DIR) / MANIFEST_FILENAME
337
+
338
+
339
+ def _sha256_file(path: Path) -> str:
340
+ """Return the hex SHA-256 digest of *path* (streamed, binary-safe)."""
341
+ digest = hashlib.sha256()
342
+ with path.open("rb") as handle:
343
+ for chunk in iter(lambda: handle.read(65536), b""):
344
+ digest.update(chunk)
345
+ return digest.hexdigest()
346
+
347
+
348
+ def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
349
+ """Load and validate the manifest, returning ``{script_name: sha256}``.
350
+
351
+ Raises:
352
+ ManifestError: With ``INVALID_STATE`` when the manifest is absent (no
353
+ explicit opt-in), or ``CORRUPT_DATA`` when it is unreadable or
354
+ malformed.
355
+ """
356
+ manifest_file = _manifest_path(scripts_dir)
357
+ if not manifest_file.is_file():
358
+ raise ManifestError(
359
+ "INVALID_STATE",
360
+ f"Code-mode manifest not found at {manifest_file}. No script executes "
361
+ "without explicit opt-in — run approve_code_script first.",
362
+ )
363
+
364
+ try:
365
+ raw = manifest_file.read_text(encoding="utf-8")
366
+ except OSError as exc:
367
+ raise ManifestError(
368
+ "CORRUPT_DATA", f"Code-mode manifest unreadable: {exc}"
369
+ ) from None
370
+
371
+ try:
372
+ data = json.loads(raw)
373
+ except (json.JSONDecodeError, ValueError):
374
+ raise ManifestError(
375
+ "CORRUPT_DATA",
376
+ f"Code-mode manifest is malformed JSON: {manifest_file}",
377
+ ) from None
378
+
379
+ if not isinstance(data, dict):
380
+ raise ManifestError(
381
+ "CORRUPT_DATA", "Code-mode manifest must be a JSON object."
382
+ )
383
+
384
+ if data.get("version") != 1:
385
+ raise ManifestError(
386
+ "CORRUPT_DATA", "Code-mode manifest malformed: version must be 1."
387
+ )
388
+
389
+ scripts = data.get("scripts")
390
+ if not isinstance(scripts, dict):
391
+ raise ManifestError(
392
+ "CORRUPT_DATA",
393
+ "Code-mode manifest is malformed: missing 'scripts' object.",
394
+ )
395
+
396
+ approved: dict[str, str] = {}
397
+ for name, digest in scripts.items():
398
+ if (
399
+ not isinstance(name, str)
400
+ or not isinstance(digest, str)
401
+ or len(digest) != _SHA256_HEX_LEN
402
+ or any(char not in "0123456789abcdefABCDEF" for char in digest)
403
+ ):
404
+ raise ManifestError(
405
+ "CORRUPT_DATA",
406
+ f"Code-mode manifest entry for '{name}' is malformed "
407
+ "(expected a 64-character SHA-256 hex digest).",
408
+ )
409
+ approved[name] = digest.lower()
410
+ return approved
411
+
412
+
413
+ def _verify_approved(script_path: Path, scripts_dir: Path | None = None) -> None:
414
+ """Authorise *script_path* against the manifest.
415
+
416
+ Raises:
417
+ ManifestError: ``CONFLICT`` when the script is not listed and
418
+ ``CORRUPT_DATA`` when its hash does not match the recorded digest.
419
+ """
420
+ approved = _load_manifest(scripts_dir)
421
+ name = script_path.name
422
+ if name not in approved:
423
+ raise ManifestError(
424
+ "CONFLICT",
425
+ f"Script '{name}' is not approved in the code-mode manifest. "
426
+ "Run approve_code_script to opt in.",
427
+ )
428
+ actual = _sha256_file(script_path)
429
+ if actual != approved[name]:
430
+ raise ManifestError(
431
+ "CORRUPT_DATA",
432
+ f"Script '{name}' hash mismatch (expected {approved[name]}, got "
433
+ f"{actual}). Re-approve the script after verifying the change.",
434
+ )
435
+
436
+
437
+ def _approve_script(
438
+ script_name: str, scripts_dir: Path | None = None
439
+ ) -> tuple[str, str]:
440
+ """Add or refresh *script_name* in the manifest with its SHA-256.
441
+
442
+ Returns:
443
+ A ``(status, message)`` tuple; the status is a contract code and is
444
+ ``OK`` on success.
445
+ """
446
+ target_dir = scripts_dir or SCRIPTS_DIR
447
+ try:
448
+ script_path = _validate_script_name(script_name, target_dir)
449
+ except ValueError as exc:
450
+ return "INVALID_INPUT", str(exc)
451
+
452
+ try:
453
+ approved = _load_manifest(target_dir)
454
+ except ManifestError as exc:
455
+ if exc.status == "INVALID_STATE":
456
+ approved = {}
457
+ else:
458
+ return exc.status, exc.message
459
+
460
+ approved[script_path.name] = _sha256_file(script_path)
461
+ payload = {
462
+ "version": MANIFEST_VERSION,
463
+ "scripts": dict(sorted(approved.items())),
464
+ }
465
+ try:
466
+ _manifest_path(target_dir).write_text(
467
+ json.dumps(payload, indent=2) + "\n", encoding="utf-8"
468
+ )
469
+ except OSError as exc:
470
+ return "UNAVAILABLE", f"Failed to write code-mode manifest: {exc}"
471
+
472
+ digest = approved[script_path.name]
473
+ return "OK", f"Approved '{script_path.name}' with SHA-256 {digest}"
474
+
475
+
476
+ def _generate_manifest(scripts_dir: Path | None = None) -> int:
477
+ """Write a manifest covering every valid script in *scripts_dir*.
478
+
479
+ Used by the installer to (re)seed the explicit opt-in for bundled scripts.
480
+
481
+ Returns:
482
+ The number of scripts recorded.
483
+ """
484
+ target_dir = scripts_dir or SCRIPTS_DIR
485
+ try:
486
+ resolved_dir = target_dir.resolve(strict=True)
487
+ except (OSError, RuntimeError):
488
+ return 0
489
+
490
+ approved: dict[str, str] = {}
491
+ if resolved_dir.is_dir() and not resolved_dir.is_symlink():
492
+ for candidate in sorted(resolved_dir.iterdir()):
493
+ if candidate.name.startswith("."):
494
+ continue
495
+ if candidate.suffix.lower() not in ALLOWED_EXTENSIONS:
496
+ continue
497
+ try:
498
+ resolved = candidate.resolve(strict=True)
499
+ except (OSError, RuntimeError):
500
+ continue
501
+ if (
502
+ resolved.is_file()
503
+ and resolved.parent == resolved_dir
504
+ and _is_within(resolved, resolved_dir)
505
+ ):
506
+ approved[candidate.name] = _sha256_file(resolved)
507
+
508
+ payload = {
509
+ "version": MANIFEST_VERSION,
510
+ "scripts": dict(sorted(approved.items())),
511
+ }
512
+ try:
513
+ _manifest_path(target_dir).write_text(
514
+ json.dumps(payload, indent=2) + "\n", encoding="utf-8"
515
+ )
516
+ except OSError:
517
+ return 0
518
+ return len(approved)
519
+
520
+
521
+ def _contract_result(
522
+ status: str, message: str, json_output: bool
523
+ ) -> str | dict[str, Any]:
524
+ """Build a contract-labelled error result (text or structured JSON)."""
525
+ if json_output:
526
+ return {"status": status, "error": message}
527
+ return f"[{status}] {message}"
528
+
529
+
530
+
220
531
  def _format_output(
221
532
  stdout: str,
222
533
  stderr: str,
@@ -286,7 +597,9 @@ def _parse_frontmatter(script_path: Path) -> dict[str, Any]:
286
597
  break
287
598
  if not stripped.startswith("#"):
288
599
  return {} # non-comment line inside the block → not frontmatter
289
- body_lines.append(stripped[1:].lstrip() if stripped.startswith("# ") else stripped[1:])
600
+ body_lines.append(
601
+ stripped[1:].lstrip() if stripped.startswith("# ") else stripped[1:]
602
+ )
290
603
  else:
291
604
  return {} # no closing delimiter
292
605
 
@@ -338,10 +651,12 @@ def _build_result(
338
651
  metadata: dict[str, Any],
339
652
  json_output: bool,
340
653
  timeout_s: int,
654
+ status: str = "OK",
341
655
  ) -> str | dict[str, Any]:
342
656
  """Build the tool result in plain-text or structured JSON form."""
343
657
  if json_output:
344
658
  return {
659
+ "status": status,
345
660
  "stdout": stdout,
346
661
  "stderr": stderr,
347
662
  "exit_code": exit_code,
@@ -378,7 +693,9 @@ async def list_code_mode_scripts() -> str:
378
693
 
379
694
  @mcp.resource(
380
695
  "pantheon://code-mode/scripts/{script_name}",
381
- description="Content of a code-mode script by name, with parsed frontmatter metadata",
696
+ description=(
697
+ "Content of a code-mode script by name, with parsed frontmatter metadata"
698
+ ),
382
699
  )
383
700
  async def get_code_mode_script(script_name: str) -> str:
384
701
  """Return the source content of a code-mode script plus its metadata."""
@@ -425,12 +742,17 @@ async def execute_code_script(
425
742
  try:
426
743
  script_path = _validate_script_name(script_name)
427
744
  except ValueError as e:
428
- return str(e)
745
+ return _contract_result("INVALID_INPUT", str(e), json_output)
429
746
 
430
747
  metadata = _parse_frontmatter(script_path)
431
748
  denied = _validate_args(script_path, args)
432
749
  if denied is not None:
433
- return denied
750
+ return _contract_result("INVALID_INPUT", denied, json_output)
751
+
752
+ try:
753
+ _verify_approved(script_path)
754
+ except ManifestError as e:
755
+ return _contract_result(e.status, e.message, json_output)
434
756
 
435
757
  timeout_s = _script_timeout(script_path)
436
758
 
@@ -486,8 +808,15 @@ async def execute_code_script(
486
808
  await proc.wait()
487
809
  duration_ms = int((time.monotonic() - started) * 1000)
488
810
  return _build_result(
489
- "", "", -1, timed_out=True, duration_ms=duration_ms,
490
- metadata=metadata, json_output=json_output, timeout_s=timeout_s,
811
+ "",
812
+ "",
813
+ -1,
814
+ timed_out=True,
815
+ duration_ms=duration_ms,
816
+ metadata=metadata,
817
+ json_output=json_output,
818
+ timeout_s=timeout_s,
819
+ status="TIMEOUT",
491
820
  )
492
821
  except FileNotFoundError:
493
822
  return _build_result(
@@ -499,6 +828,7 @@ async def execute_code_script(
499
828
  metadata=metadata,
500
829
  json_output=json_output,
501
830
  timeout_s=timeout_s,
831
+ status="UNAVAILABLE",
502
832
  )
503
833
  except OSError as e:
504
834
  return _build_result(
@@ -510,10 +840,56 @@ async def execute_code_script(
510
840
  metadata=metadata,
511
841
  json_output=json_output,
512
842
  timeout_s=timeout_s,
843
+ status="UNAVAILABLE",
513
844
  )
514
845
 
515
846
 
847
+ @mcp.tool(
848
+ name="approve_code_script",
849
+ description="Approve a .sh/.py script for execution by adding it to the "
850
+ "code-mode manifest with its SHA-256. Execution is opt-in: only approved "
851
+ "scripts with a matching hash run. Set json_output=true for structured JSON.",
852
+ )
853
+ async def approve_code_script(
854
+ script_name: str,
855
+ json_output: bool = False,
856
+ ) -> str | dict[str, Any]:
857
+ """Approve (or re-approve) a code-mode script.
858
+
859
+ Args:
860
+ script_name: Name of the script in the code-mode directory.
861
+ json_output: When True, return ``{"status", "message"}`` instead of a
862
+ ``[STATUS] message`` string.
863
+
864
+ Returns:
865
+ A contract-labelled result. ``OK`` on success, ``INVALID_INPUT`` when
866
+ the script name is invalid, or ``CORRUPT_DATA``/``UNAVAILABLE`` when
867
+ the manifest cannot be read or written.
868
+ """
869
+ status, message = _approve_script(script_name)
870
+ if json_output:
871
+ return {"status": status, "message": message}
872
+ return f"[{status}] {message}"
873
+
874
+
516
875
  # ── Main Entrypoint ───────────────────────────────────────────────────────────
517
876
 
518
- if __name__ == "__main__":
877
+
878
+ def _cli() -> int:
879
+ """Handle CLI subcommands used by the installer; fall back to serving MCP."""
880
+ argv = sys.argv[1:]
881
+ if "--generate-manifest" in argv:
882
+ scripts_dir: Path | None = None
883
+ if "--scripts-dir" in argv:
884
+ index = argv.index("--scripts-dir")
885
+ if index + 1 < len(argv):
886
+ scripts_dir = Path(argv[index + 1])
887
+ count = _generate_manifest(scripts_dir)
888
+ print(f"code-mode manifest: {count} script(s) approved")
889
+ return 0
519
890
  mcp.run()
891
+ return 0
892
+
893
+
894
+ if __name__ == "__main__":
895
+ raise SystemExit(_cli())