pantheon-opencode 1.5.0-beta.3 → 1.5.0-beta.5
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 +871 -0
- package/README.md +101 -1
- package/README.pt-BR.md +55 -1
- package/bin/pantheon-init.mjs +178 -21
- package/package.json +14 -10
- package/plugin.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/code_mode_server.py +339 -28
- package/scripts/doctor.mjs +178 -9
- package/scripts/install/cli-ui.mjs +17 -11
- package/scripts/install/config-migration.mjs +4 -6
- package/scripts/install/opencode.mjs +116 -35
- package/scripts/install/shared.mjs +43 -2
- package/scripts/install/strings.mjs +129 -0
- package/scripts/install-mcp.mjs +1 -0
- package/scripts/probe-pantheon-cost.mjs +10 -1
- package/scripts/release-beta-version.mjs +2 -7
- package/scripts/sync-artifacts.mjs +169 -0
- package/scripts/sync-tui.mjs +124 -34
- package/src/agents/zeus.md +2 -2
- package/src/mcp/code_mode_server.py +339 -28
- package/src/pantheon/auto-wake.ts +1 -5
- package/src/pantheon/background-job-board.ts +14 -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/shared-board.ts +37 -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 +25 -9
- package/src/plugins/pantheon-hooks.ts +51 -0
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +1 -1
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/dist/tui.js.map +1 -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,6 +40,30 @@ 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.
|
|
@@ -131,12 +157,9 @@ def _detect_prlimit() -> str | None:
|
|
|
131
157
|
|
|
132
158
|
|
|
133
159
|
# ── Scripts Directory Resolution ─────────────────────────────────────────────
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
# 3. /.pantheon/code-mode/ (global fallback)
|
|
138
|
-
# 4. .pantheon/code-mode/ shipped inside the installed package (tarball
|
|
139
|
-
# 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.
|
|
140
163
|
|
|
141
164
|
|
|
142
165
|
def _is_within(path: Path, root: Path) -> bool:
|
|
@@ -203,25 +226,38 @@ def _packaged_scripts_dir() -> Path | None:
|
|
|
203
226
|
return None
|
|
204
227
|
|
|
205
228
|
|
|
206
|
-
def _resolve_scripts_dir(candidates: list[Path]) -> Path | None:
|
|
207
|
-
"""Select the first usable candidate,
|
|
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."""
|
|
208
231
|
for candidate in candidates:
|
|
209
232
|
if _has_usable_scripts(candidate):
|
|
210
233
|
return candidate
|
|
211
|
-
return _packaged_scripts_dir()
|
|
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
|
|
212
257
|
|
|
213
258
|
|
|
214
259
|
_PANTHEON_HOME: Path = pantheon_home()
|
|
215
|
-
|
|
216
|
-
_proj = pantheon_project()
|
|
217
|
-
if _proj is not None:
|
|
218
|
-
_SCRIPTS_DIR_CANDIDATES.append(_proj / ".opencode" / ".pantheon" / "code-mode")
|
|
219
|
-
_SCRIPTS_DIR_CANDIDATES.append(_proj / ".pantheon" / "code-mode")
|
|
220
|
-
_SCRIPTS_DIR_CANDIDATES.append(_PANTHEON_HOME / ".pantheon" / "code-mode")
|
|
221
|
-
|
|
222
|
-
SCRIPTS_DIR: Path = _resolve_scripts_dir(_SCRIPTS_DIR_CANDIDATES) or (
|
|
223
|
-
_PANTHEON_HOME / ".pantheon" / "code-mode"
|
|
224
|
-
)
|
|
260
|
+
SCRIPTS_DIR: Path = _resolve_code_mode_dir()
|
|
225
261
|
|
|
226
262
|
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
227
263
|
mcp = FastMCP(
|
|
@@ -233,12 +269,19 @@ mcp = FastMCP(
|
|
|
233
269
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
234
270
|
|
|
235
271
|
|
|
236
|
-
def _validate_script_name(script_name: str) -> Path:
|
|
272
|
+
def _validate_script_name(script_name: str, scripts_dir: Path | None = None) -> Path:
|
|
237
273
|
"""Validate a script name and return its resolved path.
|
|
238
274
|
|
|
239
|
-
|
|
240
|
-
|
|
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.
|
|
241
282
|
"""
|
|
283
|
+
scripts_dir = scripts_dir or SCRIPTS_DIR
|
|
284
|
+
|
|
242
285
|
if not script_name:
|
|
243
286
|
raise ValueError("Script name cannot be empty")
|
|
244
287
|
|
|
@@ -253,12 +296,12 @@ def _validate_script_name(script_name: str) -> Path:
|
|
|
253
296
|
allowed = ", ".join(sorted(ALLOWED_EXTENSIONS))
|
|
254
297
|
raise ValueError(f"Extension '{ext}' not allowed. Allowed: {allowed}")
|
|
255
298
|
|
|
256
|
-
if
|
|
299
|
+
if scripts_dir.is_symlink():
|
|
257
300
|
raise ValueError(f"Invalid script name: '{script_name}'")
|
|
258
301
|
|
|
259
302
|
try:
|
|
260
|
-
scripts_root =
|
|
261
|
-
script_path = (
|
|
303
|
+
scripts_root = scripts_dir.resolve(strict=True)
|
|
304
|
+
script_path = (scripts_dir / name).resolve(strict=True)
|
|
262
305
|
except (FileNotFoundError, OSError, RuntimeError):
|
|
263
306
|
raise ValueError(f"Script '{script_name}' not found") from None
|
|
264
307
|
|
|
@@ -272,6 +315,219 @@ def _validate_script_name(script_name: str) -> Path:
|
|
|
272
315
|
return script_path
|
|
273
316
|
|
|
274
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
|
+
|
|
275
531
|
def _format_output(
|
|
276
532
|
stdout: str,
|
|
277
533
|
stderr: str,
|
|
@@ -395,10 +651,12 @@ def _build_result(
|
|
|
395
651
|
metadata: dict[str, Any],
|
|
396
652
|
json_output: bool,
|
|
397
653
|
timeout_s: int,
|
|
654
|
+
status: str = "OK",
|
|
398
655
|
) -> str | dict[str, Any]:
|
|
399
656
|
"""Build the tool result in plain-text or structured JSON form."""
|
|
400
657
|
if json_output:
|
|
401
658
|
return {
|
|
659
|
+
"status": status,
|
|
402
660
|
"stdout": stdout,
|
|
403
661
|
"stderr": stderr,
|
|
404
662
|
"exit_code": exit_code,
|
|
@@ -484,12 +742,17 @@ async def execute_code_script(
|
|
|
484
742
|
try:
|
|
485
743
|
script_path = _validate_script_name(script_name)
|
|
486
744
|
except ValueError as e:
|
|
487
|
-
return str(e)
|
|
745
|
+
return _contract_result("INVALID_INPUT", str(e), json_output)
|
|
488
746
|
|
|
489
747
|
metadata = _parse_frontmatter(script_path)
|
|
490
748
|
denied = _validate_args(script_path, args)
|
|
491
749
|
if denied is not None:
|
|
492
|
-
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)
|
|
493
756
|
|
|
494
757
|
timeout_s = _script_timeout(script_path)
|
|
495
758
|
|
|
@@ -553,6 +816,7 @@ async def execute_code_script(
|
|
|
553
816
|
metadata=metadata,
|
|
554
817
|
json_output=json_output,
|
|
555
818
|
timeout_s=timeout_s,
|
|
819
|
+
status="TIMEOUT",
|
|
556
820
|
)
|
|
557
821
|
except FileNotFoundError:
|
|
558
822
|
return _build_result(
|
|
@@ -564,6 +828,7 @@ async def execute_code_script(
|
|
|
564
828
|
metadata=metadata,
|
|
565
829
|
json_output=json_output,
|
|
566
830
|
timeout_s=timeout_s,
|
|
831
|
+
status="UNAVAILABLE",
|
|
567
832
|
)
|
|
568
833
|
except OSError as e:
|
|
569
834
|
return _build_result(
|
|
@@ -575,10 +840,56 @@ async def execute_code_script(
|
|
|
575
840
|
metadata=metadata,
|
|
576
841
|
json_output=json_output,
|
|
577
842
|
timeout_s=timeout_s,
|
|
843
|
+
status="UNAVAILABLE",
|
|
578
844
|
)
|
|
579
845
|
|
|
580
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
|
+
|
|
581
875
|
# ── Main Entrypoint ───────────────────────────────────────────────────────────
|
|
582
876
|
|
|
583
|
-
|
|
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
|
|
584
890
|
mcp.run()
|
|
891
|
+
return 0
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
if __name__ == "__main__":
|
|
895
|
+
raise SystemExit(_cli())
|