File size: 5,580 Bytes
9d0fd45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
"""Runtime discovery and prompt guidance for the baked document Node toolchain."""

from __future__ import annotations

import json
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path

from plugins.tools._sandbox import is_e2b_available

_DOCUMENT_PACKAGES = ("docx", "pptxgenjs")
_BWRAP_VISIBLE_ROOTS = (
    Path("/usr"),
    Path("/bin"),
    Path("/lib"),
    Path("/lib64"),
    Path("/etc"),
)


@dataclass(frozen=True)
class DocumentNodeToolchain:
    """Versions proven available to the active model-authored command backend."""

    node_version: str
    docx_version: str
    pptxgenjs_version: str


def _is_within(path: Path, roots: tuple[Path, ...]) -> bool:
    resolved = path.resolve()
    for root in roots:
        try:
            resolved.relative_to(root)
            return True
        except ValueError:
            continue
    return False


def _package_version(package: str, roots: tuple[Path, ...]) -> tuple[str, Path] | None:
    for root in roots:
        manifest = root / package / "package.json"
        if not manifest.is_file():
            continue
        try:
            data = json.loads(manifest.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError, TypeError):
            continue
        version = str(data.get("version") or "").strip()
        if version:
            return version, manifest
    return None


def discover_document_node_toolchain(
    *,
    sandbox_mode: str,
) -> DocumentNodeToolchain | None:
    """Return the document Node stack visible to model-authored commands.

    ``container`` executes in the current image. ``bwrap`` (and ``auto``
    without E2B) can see only paths mounted into the jail, so both the Node
    binary and module roots must live under its read-only system mounts.
    Explicit/auto E2B executes off-host and must never inherit a capability
    merely because the serving process itself has it installed.
    """
    mode = (sandbox_mode or "auto").strip().lower()
    if mode not in {"container", "native", "bwrap"} and is_e2b_available():
        return None

    node_path = os.environ.get("NODE_PATH", "").strip()
    path_env = os.environ.get("PATH", "").strip()
    if not node_path or not path_env:
        return None

    roots = tuple(
        Path(raw.strip()).expanduser()
        for raw in node_path.split(os.pathsep)
        if raw.strip()
    )
    node = shutil.which("node", path=path_env)
    if not roots or not node:
        return None

    packages = {
        package: _package_version(package, roots)
        for package in _DOCUMENT_PACKAGES
    }
    if any(value is None for value in packages.values()):
        return None

    if mode not in {"container", "native"}:
        visible_paths = [
            Path(node),
            *(value[1] for value in packages.values() if value is not None),
        ]
        if not all(_is_within(path, _BWRAP_VISIBLE_ROOTS) for path in visible_paths):
            return None

    try:
        result = subprocess.run(
            [node, "--version"],
            capture_output=True,
            text=True,
            timeout=2,
            check=False,
            env={"PATH": path_env, "NODE_PATH": node_path},
        )
    except (OSError, subprocess.SubprocessError):
        return None
    if result.returncode != 0:
        return None
    node_version = result.stdout.strip().removeprefix("v")
    if not node_version:
        return None

    docx = packages["docx"]
    pptxgenjs = packages["pptxgenjs"]
    assert docx is not None and pptxgenjs is not None
    return DocumentNodeToolchain(
        node_version=node_version,
        docx_version=docx[0],
        pptxgenjs_version=pptxgenjs[0],
    )


def render_document_node_toolchain_note(
    *,
    sandbox_mode: str,
    tool_names: list[str] | tuple[str, ...],
    audience: str = "agent",
) -> str:
    """Render a truthful prompt note for agents that can execute ``bash``."""
    names = {str(name) for name in tool_names}
    if "bash" not in names:
        return ""
    capability = discover_document_node_toolchain(sandbox_mode=sandbox_mode)
    if capability is None:
        return ""

    if "create_file" in names:
        fallback = (
            "Use this fallback order: (1) `create_file`; (2) Python libraries "
            "such as `python-docx` or `python-pptx` only after `create_file` "
            "explicitly reports the required operation unsupported; (3) these "
            "Node packages only when Python still cannot cover the operation, "
            "or when the user requires accurate preservation of an existing "
            "template that the Python path would not preserve."
        )
    else:
        fallback = (
            "`create_file` is not available in this tool set. Use Python "
            "libraries first; use these Node packages only when Python cannot "
            "cover the required operation, or when the user requires accurate "
            "preservation of an existing template that Python would not "
            "preserve."
        )
    scope = "sub-agent runtime" if audience == "coordinator" else "runtime-discovered"
    return (
        f"\n\nDOCUMENT TOOLCHAIN ({scope}): `bash` has Node.js "
        f"{capability.node_version}, `docx@{capability.docx_version}`, and "
        f"`pptxgenjs@{capability.pptxgenjs_version}`. `NODE_PATH` is already "
        "configured: load them as `require('docx')` and "
        "`require('pptxgenjs')`; do not hard-code `/usr/lib/node_modules`. "
        f"{fallback}"
    )