pantheon-opencode 1.5.0-beta.2 → 1.5.0-beta.4
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.
- package/.pantheon/code-mode/manifest.json +21 -0
- package/CHANGELOG.md +835 -0
- package/README.md +103 -15
- package/README.pt-BR.md +57 -13
- package/package.json +8 -3
- package/plugin.json +1 -1
- package/pyproject.toml +57 -0
- package/scripts/code_mode_server.py +403 -42
- package/scripts/install/opencode.mjs +42 -0
- package/scripts/install/plugin.mjs +3 -33
- package/scripts/manifest-inventory.mjs +410 -0
- package/scripts/mcp_persistence_server.py +516 -163
- package/scripts/package-evidence.mjs +242 -0
- package/scripts/probe-pantheon-cost.mjs +10 -1
- package/scripts/release-beta-version.mjs +61 -60
- package/scripts/sync-tui.mjs +76 -54
- package/scripts/test-opencode-v1-v2-sandbox.sh +79 -161
- package/scripts/validation-policy.mjs +7 -7
- package/scripts/version-check.mjs +113 -108
- package/scripts/versioning.mjs +131 -88
- package/src/agents/zeus.md +2 -2
- package/src/mcp/code_mode_server.py +403 -42
- package/src/mcp/mcp_persistence_server.py +516 -163
- package/src/pantheon/background-job-board.ts +14 -0
- package/src/pantheon/compaction-handoff.ts +304 -0
- package/src/pantheon/cost-command.ts +62 -42
- package/src/pantheon/delegate-manager.ts +109 -12
- package/src/pantheon/native-probe.ts +234 -0
- package/src/pantheon/native-task-status.ts +139 -0
- package/src/pantheon/provider-capabilities.ts +221 -0
- package/src/pantheon/provider-stream-tracker.ts +182 -0
- package/src/pantheon/task-result-guard.ts +107 -0
- package/src/pantheon/token-opt.ts +18 -110
- package/src/pantheon/tool-ceiling.ts +159 -0
- package/src/pantheon/v2-events.ts +10 -1
- package/src/plugin.ts +19 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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,6 +155,7 @@ def _detect_prlimit() -> str | None:
|
|
|
127
155
|
return None
|
|
128
156
|
return shutil.which("prlimit")
|
|
129
157
|
|
|
158
|
+
|
|
130
159
|
# ── Scripts Directory Resolution ─────────────────────────────────────────────
|
|
131
160
|
# Priority:
|
|
132
161
|
# 1. /.opencode/.pantheon/code-mode/ (project install)
|
|
@@ -136,6 +165,49 @@ def _detect_prlimit() -> str | None:
|
|
|
136
165
|
# fallback — package.json `files` includes .pantheon/code-mode/**)
|
|
137
166
|
|
|
138
167
|
|
|
168
|
+
def _is_within(path: Path, root: Path) -> bool:
|
|
169
|
+
"""Return whether a resolved *path* is contained by a resolved *root*."""
|
|
170
|
+
try:
|
|
171
|
+
return os.path.commonpath((str(root), str(path))) == str(root)
|
|
172
|
+
except (OSError, ValueError):
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _has_usable_scripts(scripts_dir: Path) -> bool:
|
|
177
|
+
"""Return whether *scripts_dir* contains an executable code-mode target.
|
|
178
|
+
|
|
179
|
+
A directory is only a valid resolver candidate when it contains a regular
|
|
180
|
+
``.py`` or ``.sh`` file that can pass the same direct-child and path
|
|
181
|
+
containment rules used by script execution. This prevents an empty
|
|
182
|
+
project overlay (or a directory containing only metadata) from masking a
|
|
183
|
+
lower-priority installation.
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
if scripts_dir.is_symlink():
|
|
187
|
+
return False
|
|
188
|
+
resolved_dir = scripts_dir.resolve(strict=True)
|
|
189
|
+
if not resolved_dir.is_dir():
|
|
190
|
+
return False
|
|
191
|
+
for script_path in scripts_dir.iterdir():
|
|
192
|
+
if script_path.name.startswith("."):
|
|
193
|
+
continue
|
|
194
|
+
if script_path.suffix.lower() not in ALLOWED_EXTENSIONS:
|
|
195
|
+
continue
|
|
196
|
+
try:
|
|
197
|
+
resolved_script = script_path.resolve(strict=True)
|
|
198
|
+
except (OSError, RuntimeError):
|
|
199
|
+
continue
|
|
200
|
+
if (
|
|
201
|
+
resolved_script.is_file()
|
|
202
|
+
and resolved_script.parent == resolved_dir
|
|
203
|
+
and _is_within(resolved_script, resolved_dir)
|
|
204
|
+
):
|
|
205
|
+
return True
|
|
206
|
+
except (OSError, RuntimeError):
|
|
207
|
+
return False
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
139
211
|
def _packaged_scripts_dir() -> Path | None:
|
|
140
212
|
"""Return the code-mode dir shipped inside the installed package, if any.
|
|
141
213
|
|
|
@@ -144,22 +216,27 @@ def _packaged_scripts_dir() -> Path | None:
|
|
|
144
216
|
Returns None when no packaged copy exists.
|
|
145
217
|
"""
|
|
146
218
|
try:
|
|
147
|
-
here = Path(__file__).resolve()
|
|
148
|
-
except
|
|
219
|
+
here = Path(__file__).resolve(strict=True)
|
|
220
|
+
except (OSError, RuntimeError):
|
|
149
221
|
return None
|
|
150
222
|
for parent in [here.parent, *here.parents]:
|
|
151
223
|
try:
|
|
152
224
|
candidate = parent / ".pantheon" / "code-mode"
|
|
153
225
|
except Exception:
|
|
154
226
|
continue
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return candidate
|
|
158
|
-
except OSError:
|
|
159
|
-
continue
|
|
227
|
+
if _has_usable_scripts(candidate):
|
|
228
|
+
return candidate
|
|
160
229
|
return None
|
|
161
230
|
|
|
162
231
|
|
|
232
|
+
def _resolve_scripts_dir(candidates: list[Path]) -> Path | None:
|
|
233
|
+
"""Select the first usable candidate, then try the packaged fallback."""
|
|
234
|
+
for candidate in candidates:
|
|
235
|
+
if _has_usable_scripts(candidate):
|
|
236
|
+
return candidate
|
|
237
|
+
return _packaged_scripts_dir()
|
|
238
|
+
|
|
239
|
+
|
|
163
240
|
_PANTHEON_HOME: Path = pantheon_home()
|
|
164
241
|
_SCRIPTS_DIR_CANDIDATES: list[Path] = []
|
|
165
242
|
_proj = pantheon_project()
|
|
@@ -168,15 +245,9 @@ if _proj is not None:
|
|
|
168
245
|
_SCRIPTS_DIR_CANDIDATES.append(_proj / ".pantheon" / "code-mode")
|
|
169
246
|
_SCRIPTS_DIR_CANDIDATES.append(_PANTHEON_HOME / ".pantheon" / "code-mode")
|
|
170
247
|
|
|
171
|
-
SCRIPTS_DIR: Path =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
SCRIPTS_DIR = _candidate
|
|
175
|
-
break
|
|
176
|
-
else:
|
|
177
|
-
_packaged = _packaged_scripts_dir()
|
|
178
|
-
if _packaged is not None:
|
|
179
|
-
SCRIPTS_DIR = _packaged
|
|
248
|
+
SCRIPTS_DIR: Path = _resolve_scripts_dir(_SCRIPTS_DIR_CANDIDATES) or (
|
|
249
|
+
_PANTHEON_HOME / ".pantheon" / "code-mode"
|
|
250
|
+
)
|
|
180
251
|
|
|
181
252
|
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
182
253
|
mcp = FastMCP(
|
|
@@ -188,12 +259,19 @@ mcp = FastMCP(
|
|
|
188
259
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
189
260
|
|
|
190
261
|
|
|
191
|
-
def _validate_script_name(script_name: str) -> Path:
|
|
262
|
+
def _validate_script_name(script_name: str, scripts_dir: Path | None = None) -> Path:
|
|
192
263
|
"""Validate a script name and return its resolved path.
|
|
193
264
|
|
|
194
|
-
|
|
195
|
-
|
|
265
|
+
Args:
|
|
266
|
+
script_name: Bare file name of a script in the code-mode directory.
|
|
267
|
+
scripts_dir: Directory to resolve against. Defaults to ``SCRIPTS_DIR``.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
ValueError: If the name is invalid, traverses paths, or has a
|
|
271
|
+
disallowed extension.
|
|
196
272
|
"""
|
|
273
|
+
scripts_dir = scripts_dir or SCRIPTS_DIR
|
|
274
|
+
|
|
197
275
|
if not script_name:
|
|
198
276
|
raise ValueError("Script name cannot be empty")
|
|
199
277
|
|
|
@@ -208,15 +286,232 @@ def _validate_script_name(script_name: str) -> Path:
|
|
|
208
286
|
allowed = ", ".join(sorted(ALLOWED_EXTENSIONS))
|
|
209
287
|
raise ValueError(f"Extension '{ext}' not allowed. Allowed: {allowed}")
|
|
210
288
|
|
|
211
|
-
|
|
212
|
-
|
|
289
|
+
if scripts_dir.is_symlink():
|
|
290
|
+
raise ValueError(f"Invalid script name: '{script_name}'")
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
scripts_root = scripts_dir.resolve(strict=True)
|
|
294
|
+
script_path = (scripts_dir / name).resolve(strict=True)
|
|
295
|
+
except (FileNotFoundError, OSError, RuntimeError):
|
|
296
|
+
raise ValueError(f"Script '{script_name}' not found") from None
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
not _is_within(script_path, scripts_root)
|
|
300
|
+
or script_path.parent != scripts_root
|
|
301
|
+
or not script_path.is_file()
|
|
302
|
+
):
|
|
213
303
|
raise ValueError(f"Invalid script name: '{script_name}'")
|
|
214
|
-
if not script_path.exists():
|
|
215
|
-
raise ValueError(f"Script '{script_name}' not found")
|
|
216
304
|
|
|
217
305
|
return script_path
|
|
218
306
|
|
|
219
307
|
|
|
308
|
+
# ── Installation Mode: Manifest + SHA-256 ─────────────────────────────────────
|
|
309
|
+
# The manifest is the explicit opt-in. A script is executable only when its
|
|
310
|
+
# bare name is present in `manifest.json` AND the SHA-256 of the on-disk file
|
|
311
|
+
# matches the recorded digest. Any deviation fails closed with a contract
|
|
312
|
+
# status instead of executing.
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ManifestError(Exception):
|
|
316
|
+
"""Raised when the code-mode manifest cannot authorise execution."""
|
|
317
|
+
|
|
318
|
+
def __init__(self, status: str, message: str) -> None:
|
|
319
|
+
super().__init__(message)
|
|
320
|
+
self.status = status
|
|
321
|
+
self.message = message
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _manifest_path(scripts_dir: Path | None = None) -> Path:
|
|
325
|
+
"""Return the manifest path for *scripts_dir* (defaults to SCRIPTS_DIR)."""
|
|
326
|
+
return (scripts_dir or SCRIPTS_DIR) / MANIFEST_FILENAME
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _sha256_file(path: Path) -> str:
|
|
330
|
+
"""Return the hex SHA-256 digest of *path* (streamed, binary-safe)."""
|
|
331
|
+
digest = hashlib.sha256()
|
|
332
|
+
with path.open("rb") as handle:
|
|
333
|
+
for chunk in iter(lambda: handle.read(65536), b""):
|
|
334
|
+
digest.update(chunk)
|
|
335
|
+
return digest.hexdigest()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
|
|
339
|
+
"""Load and validate the manifest, returning ``{script_name: sha256}``.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
ManifestError: With ``INVALID_STATE`` when the manifest is absent (no
|
|
343
|
+
explicit opt-in), or ``CORRUPT_DATA`` when it is unreadable or
|
|
344
|
+
malformed.
|
|
345
|
+
"""
|
|
346
|
+
manifest_file = _manifest_path(scripts_dir)
|
|
347
|
+
if not manifest_file.is_file():
|
|
348
|
+
raise ManifestError(
|
|
349
|
+
"INVALID_STATE",
|
|
350
|
+
f"Code-mode manifest not found at {manifest_file}. No script executes "
|
|
351
|
+
"without explicit opt-in — run approve_code_script first.",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
raw = manifest_file.read_text(encoding="utf-8")
|
|
356
|
+
except OSError as exc:
|
|
357
|
+
raise ManifestError(
|
|
358
|
+
"CORRUPT_DATA", f"Code-mode manifest unreadable: {exc}"
|
|
359
|
+
) from None
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
data = json.loads(raw)
|
|
363
|
+
except (json.JSONDecodeError, ValueError):
|
|
364
|
+
raise ManifestError(
|
|
365
|
+
"CORRUPT_DATA",
|
|
366
|
+
f"Code-mode manifest is malformed JSON: {manifest_file}",
|
|
367
|
+
) from None
|
|
368
|
+
|
|
369
|
+
if not isinstance(data, dict):
|
|
370
|
+
raise ManifestError(
|
|
371
|
+
"CORRUPT_DATA", "Code-mode manifest must be a JSON object."
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
scripts = data.get("scripts")
|
|
375
|
+
if not isinstance(scripts, dict):
|
|
376
|
+
raise ManifestError(
|
|
377
|
+
"CORRUPT_DATA",
|
|
378
|
+
"Code-mode manifest is malformed: missing 'scripts' object.",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
approved: dict[str, str] = {}
|
|
382
|
+
for name, digest in scripts.items():
|
|
383
|
+
if (
|
|
384
|
+
not isinstance(name, str)
|
|
385
|
+
or not isinstance(digest, str)
|
|
386
|
+
or len(digest) != _SHA256_HEX_LEN
|
|
387
|
+
):
|
|
388
|
+
raise ManifestError(
|
|
389
|
+
"CORRUPT_DATA",
|
|
390
|
+
f"Code-mode manifest entry for '{name}' is malformed "
|
|
391
|
+
"(expected a 64-character SHA-256 hex digest).",
|
|
392
|
+
)
|
|
393
|
+
approved[name] = digest.lower()
|
|
394
|
+
return approved
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _verify_approved(script_path: Path, scripts_dir: Path | None = None) -> None:
|
|
398
|
+
"""Authorise *script_path* against the manifest.
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
ManifestError: ``CONFLICT`` when the script is not listed and
|
|
402
|
+
``CORRUPT_DATA`` when its hash does not match the recorded digest.
|
|
403
|
+
"""
|
|
404
|
+
approved = _load_manifest(scripts_dir)
|
|
405
|
+
name = script_path.name
|
|
406
|
+
if name not in approved:
|
|
407
|
+
raise ManifestError(
|
|
408
|
+
"CONFLICT",
|
|
409
|
+
f"Script '{name}' is not approved in the code-mode manifest. "
|
|
410
|
+
"Run approve_code_script to opt in.",
|
|
411
|
+
)
|
|
412
|
+
actual = _sha256_file(script_path)
|
|
413
|
+
if actual != approved[name]:
|
|
414
|
+
raise ManifestError(
|
|
415
|
+
"CORRUPT_DATA",
|
|
416
|
+
f"Script '{name}' hash mismatch (expected {approved[name]}, got "
|
|
417
|
+
f"{actual}). Re-approve the script after verifying the change.",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _approve_script(
|
|
422
|
+
script_name: str, scripts_dir: Path | None = None
|
|
423
|
+
) -> tuple[str, str]:
|
|
424
|
+
"""Add or refresh *script_name* in the manifest with its SHA-256.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
A ``(status, message)`` tuple; the status is a contract code and is
|
|
428
|
+
``OK`` on success.
|
|
429
|
+
"""
|
|
430
|
+
target_dir = scripts_dir or SCRIPTS_DIR
|
|
431
|
+
try:
|
|
432
|
+
script_path = _validate_script_name(script_name, target_dir)
|
|
433
|
+
except ValueError as exc:
|
|
434
|
+
return "INVALID_INPUT", str(exc)
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
approved = _load_manifest(target_dir)
|
|
438
|
+
except ManifestError as exc:
|
|
439
|
+
if exc.status == "INVALID_STATE":
|
|
440
|
+
approved = {}
|
|
441
|
+
else:
|
|
442
|
+
return exc.status, exc.message
|
|
443
|
+
|
|
444
|
+
approved[script_path.name] = _sha256_file(script_path)
|
|
445
|
+
payload = {
|
|
446
|
+
"version": MANIFEST_VERSION,
|
|
447
|
+
"scripts": dict(sorted(approved.items())),
|
|
448
|
+
}
|
|
449
|
+
try:
|
|
450
|
+
_manifest_path(target_dir).write_text(
|
|
451
|
+
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
|
|
452
|
+
)
|
|
453
|
+
except OSError as exc:
|
|
454
|
+
return "UNAVAILABLE", f"Failed to write code-mode manifest: {exc}"
|
|
455
|
+
|
|
456
|
+
digest = approved[script_path.name]
|
|
457
|
+
return "OK", f"Approved '{script_path.name}' with SHA-256 {digest}"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _generate_manifest(scripts_dir: Path | None = None) -> int:
|
|
461
|
+
"""Write a manifest covering every valid script in *scripts_dir*.
|
|
462
|
+
|
|
463
|
+
Used by the installer to (re)seed the explicit opt-in for bundled scripts.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
The number of scripts recorded.
|
|
467
|
+
"""
|
|
468
|
+
target_dir = scripts_dir or SCRIPTS_DIR
|
|
469
|
+
try:
|
|
470
|
+
resolved_dir = target_dir.resolve(strict=True)
|
|
471
|
+
except (OSError, RuntimeError):
|
|
472
|
+
return 0
|
|
473
|
+
|
|
474
|
+
approved: dict[str, str] = {}
|
|
475
|
+
if resolved_dir.is_dir() and not resolved_dir.is_symlink():
|
|
476
|
+
for candidate in sorted(resolved_dir.iterdir()):
|
|
477
|
+
if candidate.name.startswith("."):
|
|
478
|
+
continue
|
|
479
|
+
if candidate.suffix.lower() not in ALLOWED_EXTENSIONS:
|
|
480
|
+
continue
|
|
481
|
+
try:
|
|
482
|
+
resolved = candidate.resolve(strict=True)
|
|
483
|
+
except (OSError, RuntimeError):
|
|
484
|
+
continue
|
|
485
|
+
if (
|
|
486
|
+
resolved.is_file()
|
|
487
|
+
and resolved.parent == resolved_dir
|
|
488
|
+
and _is_within(resolved, resolved_dir)
|
|
489
|
+
):
|
|
490
|
+
approved[candidate.name] = _sha256_file(resolved)
|
|
491
|
+
|
|
492
|
+
payload = {
|
|
493
|
+
"version": MANIFEST_VERSION,
|
|
494
|
+
"scripts": dict(sorted(approved.items())),
|
|
495
|
+
}
|
|
496
|
+
try:
|
|
497
|
+
_manifest_path(target_dir).write_text(
|
|
498
|
+
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
|
|
499
|
+
)
|
|
500
|
+
except OSError:
|
|
501
|
+
return 0
|
|
502
|
+
return len(approved)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _contract_result(
|
|
506
|
+
status: str, message: str, json_output: bool
|
|
507
|
+
) -> str | dict[str, Any]:
|
|
508
|
+
"""Build a contract-labelled error result (text or structured JSON)."""
|
|
509
|
+
if json_output:
|
|
510
|
+
return {"status": status, "error": message}
|
|
511
|
+
return f"[{status}] {message}"
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
220
515
|
def _format_output(
|
|
221
516
|
stdout: str,
|
|
222
517
|
stderr: str,
|
|
@@ -286,7 +581,9 @@ def _parse_frontmatter(script_path: Path) -> dict[str, Any]:
|
|
|
286
581
|
break
|
|
287
582
|
if not stripped.startswith("#"):
|
|
288
583
|
return {} # non-comment line inside the block → not frontmatter
|
|
289
|
-
body_lines.append(
|
|
584
|
+
body_lines.append(
|
|
585
|
+
stripped[1:].lstrip() if stripped.startswith("# ") else stripped[1:]
|
|
586
|
+
)
|
|
290
587
|
else:
|
|
291
588
|
return {} # no closing delimiter
|
|
292
589
|
|
|
@@ -338,10 +635,12 @@ def _build_result(
|
|
|
338
635
|
metadata: dict[str, Any],
|
|
339
636
|
json_output: bool,
|
|
340
637
|
timeout_s: int,
|
|
638
|
+
status: str = "OK",
|
|
341
639
|
) -> str | dict[str, Any]:
|
|
342
640
|
"""Build the tool result in plain-text or structured JSON form."""
|
|
343
641
|
if json_output:
|
|
344
642
|
return {
|
|
643
|
+
"status": status,
|
|
345
644
|
"stdout": stdout,
|
|
346
645
|
"stderr": stderr,
|
|
347
646
|
"exit_code": exit_code,
|
|
@@ -378,7 +677,9 @@ async def list_code_mode_scripts() -> str:
|
|
|
378
677
|
|
|
379
678
|
@mcp.resource(
|
|
380
679
|
"pantheon://code-mode/scripts/{script_name}",
|
|
381
|
-
description=
|
|
680
|
+
description=(
|
|
681
|
+
"Content of a code-mode script by name, with parsed frontmatter metadata"
|
|
682
|
+
),
|
|
382
683
|
)
|
|
383
684
|
async def get_code_mode_script(script_name: str) -> str:
|
|
384
685
|
"""Return the source content of a code-mode script plus its metadata."""
|
|
@@ -425,12 +726,17 @@ async def execute_code_script(
|
|
|
425
726
|
try:
|
|
426
727
|
script_path = _validate_script_name(script_name)
|
|
427
728
|
except ValueError as e:
|
|
428
|
-
return str(e)
|
|
729
|
+
return _contract_result("INVALID_INPUT", str(e), json_output)
|
|
429
730
|
|
|
430
731
|
metadata = _parse_frontmatter(script_path)
|
|
431
732
|
denied = _validate_args(script_path, args)
|
|
432
733
|
if denied is not None:
|
|
433
|
-
return denied
|
|
734
|
+
return _contract_result("INVALID_INPUT", denied, json_output)
|
|
735
|
+
|
|
736
|
+
try:
|
|
737
|
+
_verify_approved(script_path)
|
|
738
|
+
except ManifestError as e:
|
|
739
|
+
return _contract_result(e.status, e.message, json_output)
|
|
434
740
|
|
|
435
741
|
timeout_s = _script_timeout(script_path)
|
|
436
742
|
|
|
@@ -486,8 +792,15 @@ async def execute_code_script(
|
|
|
486
792
|
await proc.wait()
|
|
487
793
|
duration_ms = int((time.monotonic() - started) * 1000)
|
|
488
794
|
return _build_result(
|
|
489
|
-
"",
|
|
490
|
-
|
|
795
|
+
"",
|
|
796
|
+
"",
|
|
797
|
+
-1,
|
|
798
|
+
timed_out=True,
|
|
799
|
+
duration_ms=duration_ms,
|
|
800
|
+
metadata=metadata,
|
|
801
|
+
json_output=json_output,
|
|
802
|
+
timeout_s=timeout_s,
|
|
803
|
+
status="TIMEOUT",
|
|
491
804
|
)
|
|
492
805
|
except FileNotFoundError:
|
|
493
806
|
return _build_result(
|
|
@@ -499,6 +812,7 @@ async def execute_code_script(
|
|
|
499
812
|
metadata=metadata,
|
|
500
813
|
json_output=json_output,
|
|
501
814
|
timeout_s=timeout_s,
|
|
815
|
+
status="UNAVAILABLE",
|
|
502
816
|
)
|
|
503
817
|
except OSError as e:
|
|
504
818
|
return _build_result(
|
|
@@ -510,10 +824,57 @@ async def execute_code_script(
|
|
|
510
824
|
metadata=metadata,
|
|
511
825
|
json_output=json_output,
|
|
512
826
|
timeout_s=timeout_s,
|
|
827
|
+
status="UNAVAILABLE",
|
|
513
828
|
)
|
|
514
829
|
|
|
515
830
|
|
|
831
|
+
@mcp.tool(
|
|
832
|
+
name="approve_code_script",
|
|
833
|
+
description="Approve a .sh/.py script for execution by adding it to the "
|
|
834
|
+
"code-mode manifest with its SHA-256. Execution is opt-in: only approved "
|
|
835
|
+
"scripts with a matching hash run. Set json_output=true for structured JSON.",
|
|
836
|
+
)
|
|
837
|
+
async def approve_code_script(
|
|
838
|
+
script_name: str,
|
|
839
|
+
json_output: bool = False,
|
|
840
|
+
) -> str | dict[str, Any]:
|
|
841
|
+
"""Approve (or re-approve) a code-mode script.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
script_name: Name of the script in the code-mode directory.
|
|
845
|
+
json_output: When True, return ``{"status", "message"}`` instead of a
|
|
846
|
+
``[STATUS] message`` string.
|
|
847
|
+
|
|
848
|
+
Returns:
|
|
849
|
+
A contract-labelled result. ``OK`` on success, ``INVALID_INPUT`` when
|
|
850
|
+
the script name is invalid, or ``CORRUPT_DATA``/``UNAVAILABLE`` when
|
|
851
|
+
the manifest cannot be read or written.
|
|
852
|
+
"""
|
|
853
|
+
status, message = _approve_script(script_name)
|
|
854
|
+
if json_output:
|
|
855
|
+
return {"status": status, "message": message}
|
|
856
|
+
return f"[{status}] {message}"
|
|
857
|
+
|
|
858
|
+
|
|
516
859
|
# ── Main Entrypoint ───────────────────────────────────────────────────────────
|
|
517
860
|
|
|
518
|
-
|
|
861
|
+
|
|
862
|
+
def _cli() -> int:
|
|
863
|
+
"""Handle CLI subcommands used by the installer; fall back to serving MCP."""
|
|
864
|
+
argv = sys.argv[1:]
|
|
865
|
+
if "--generate-manifest" in argv:
|
|
866
|
+
scripts_dir: Path | None = None
|
|
867
|
+
if "--scripts-dir" in argv:
|
|
868
|
+
index = argv.index("--scripts-dir")
|
|
869
|
+
if index + 1 < len(argv):
|
|
870
|
+
scripts_dir = Path(argv[index + 1])
|
|
871
|
+
count = _generate_manifest(scripts_dir)
|
|
872
|
+
print(f"code-mode manifest: {count} script(s) approved")
|
|
873
|
+
return 0
|
|
519
874
|
mcp.run()
|
|
875
|
+
return 0
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
if __name__ == "__main__":
|
|
879
|
+
raise SystemExit(_cli())
|
|
880
|
+
|