clean-room-skill 0.1.0
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/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/plugin.json +36 -0
- package/LICENSE +21 -0
- package/README.md +376 -0
- package/agents/clean-architect.md +27 -0
- package/agents/clean-qa-editor.md +27 -0
- package/agents/contaminated-manager-verifier.md +35 -0
- package/agents/contaminated-source-analyst.md +26 -0
- package/bin/install.js +535 -0
- package/examples/codex/.codex/agents/clean-architect.toml +17 -0
- package/examples/codex/.codex/agents/clean-qa-editor.toml +17 -0
- package/examples/codex/.codex/agents/contaminated-manager-verifier.toml +21 -0
- package/examples/codex/.codex/agents/contaminated-source-analyst.toml +17 -0
- package/hooks/check-artifact-leakage.py +317 -0
- package/hooks/clean-room-hook.py +88 -0
- package/hooks/clean_room_paths.py +130 -0
- package/hooks/deny-clean-room-shell.py +30 -0
- package/hooks/deny-clean-source-read.py +104 -0
- package/hooks/deny-contaminated-clean-write.py +134 -0
- package/hooks/hooks.json +44 -0
- package/hooks/require-clean-room-env.py +127 -0
- package/hooks/validate-handoff-package.py +140 -0
- package/hooks/validate-json-schema.py +283 -0
- package/lib/fs-utils.cjs +123 -0
- package/lib/hooks.cjs +214 -0
- package/package.json +49 -0
- package/plugin.json +20 -0
- package/skills/attended/SKILL.md +25 -0
- package/skills/clean-room/SKILL.md +134 -0
- package/skills/clean-room/assets/behavior-spec.schema.json +367 -0
- package/skills/clean-room/assets/contamination-incident.schema.json +60 -0
- package/skills/clean-room/assets/coverage-ledger.schema.json +139 -0
- package/skills/clean-room/assets/evidence-ledger.schema.json +80 -0
- package/skills/clean-room/assets/handoff-package.schema.json +114 -0
- package/skills/clean-room/assets/qc-report.schema.json +248 -0
- package/skills/clean-room/assets/skeleton-manifest.schema.json +239 -0
- package/skills/clean-room/assets/source-index.schema.json +622 -0
- package/skills/clean-room/assets/task-manifest.schema.json +593 -0
- package/skills/clean-room/examples/README.md +18 -0
- package/skills/clean-room/examples/minimal-spec-package/behavior-spec.json +61 -0
- package/skills/clean-room/examples/minimal-spec-package/coverage-ledger.json +27 -0
- package/skills/clean-room/examples/minimal-spec-package/evidence-ledger.json +17 -0
- package/skills/clean-room/examples/minimal-spec-package/handoff-package.json +26 -0
- package/skills/clean-room/examples/minimal-spec-package/qc-report.json +25 -0
- package/skills/clean-room/examples/minimal-spec-package/skeleton-manifest.json +45 -0
- package/skills/clean-room/examples/minimal-spec-package/source-index.json +156 -0
- package/skills/clean-room/examples/minimal-spec-package/task-manifest.json +220 -0
- package/skills/clean-room/references/LEAKAGE-RULES.md +92 -0
- package/skills/clean-room/references/PROCESS.md +185 -0
- package/skills/clean-room/references/SPEC-SCHEMA.md +185 -0
- package/skills/clean-room/references/TARGET-LANGUAGE-GUIDE.md +43 -0
- package/skills/clean-room/scripts/build_source_index.py +1253 -0
- package/skills/clean-room/scripts/clean_room_tool_manager.py +199 -0
- package/skills/clean-room/scripts/clean_room_tooling.py +370 -0
- package/skills/unattended/SKILL.md +26 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Explicit local tool setup for clean-room source-index preflight."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import clean_room_tooling
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LOCAL_INSTALL_TIMEOUT_SECONDS = 600
|
|
16
|
+
NPM_TOOLS = {
|
|
17
|
+
"ast-grep": {
|
|
18
|
+
"package": "@ast-grep/cli",
|
|
19
|
+
"source": "https://www.npmjs.com/package/@ast-grep/cli",
|
|
20
|
+
"bin": "ast-grep",
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
STATUS_TOOLS = [
|
|
24
|
+
"node",
|
|
25
|
+
"npm",
|
|
26
|
+
"ast-grep",
|
|
27
|
+
"sg",
|
|
28
|
+
"ctags",
|
|
29
|
+
"universal-ctags",
|
|
30
|
+
"scip",
|
|
31
|
+
]
|
|
32
|
+
STATUS_PACKAGES = ["@ast-grep/cli"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_args() -> argparse.Namespace:
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
description="Report or explicitly install local clean-room source-index helper tools into the user cache."
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("--status", action="store_true", help="Print tool discovery status as JSON")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--install-local",
|
|
42
|
+
choices=sorted(NPM_TOOLS),
|
|
43
|
+
help="Install one approved npm-backed tool into ~/.cache/re-skills/clean-room-tools/npm",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--version",
|
|
47
|
+
help="Exact npm package version to install. Required with --install-local.",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--allow-npm-scripts",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="Allow npm lifecycle scripts during local install. Default is --ignore-scripts.",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--allow-working-project-tools",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Include project-local .local/bin, .bin, node_modules/.bin, and npm prefix/global tools in --status output.",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--probe-tools",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Execute discovered tools with version commands in --status output. Default is stat-only.",
|
|
63
|
+
)
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
if not args.status and not args.install_local:
|
|
66
|
+
parser.error("choose --status or --install-local")
|
|
67
|
+
if args.install_local and not args.version:
|
|
68
|
+
parser.error("--install-local requires --version")
|
|
69
|
+
return args
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_json(data: dict[str, Any]) -> None:
|
|
73
|
+
json.dump(data, sys.stdout, indent=2, sort_keys=True)
|
|
74
|
+
sys.stdout.write("\n")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def package_status(allow_project_tools: bool = False) -> dict[str, Any]:
|
|
78
|
+
status: dict[str, Any] = {}
|
|
79
|
+
for package in STATUS_PACKAGES:
|
|
80
|
+
parts = package.split("/")
|
|
81
|
+
checked = [
|
|
82
|
+
(root / "node_modules").joinpath(*parts)
|
|
83
|
+
for root in clean_room_tooling.node_resolver_roots(allow_project_tools)
|
|
84
|
+
]
|
|
85
|
+
found = next((path for path in checked if path.exists()), None)
|
|
86
|
+
status[package] = (
|
|
87
|
+
clean_room_tooling.observed(found.as_posix())
|
|
88
|
+
if found
|
|
89
|
+
else clean_room_tooling.unknown(
|
|
90
|
+
"package unavailable",
|
|
91
|
+
value={"checked_locations": [path.as_posix() for path in checked]},
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
return status
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def status_report(allow_project_tools: bool = False, probe_tools: bool = False) -> dict[str, Any]:
|
|
98
|
+
dependency_report = clean_room_tooling.dependency_report(allow_project_tools, probe_tools)
|
|
99
|
+
return {
|
|
100
|
+
"schema_version": 1,
|
|
101
|
+
"policy": dependency_report["external_tools_policy"],
|
|
102
|
+
"tool_probe_mode": dependency_report["tool_probe_mode"],
|
|
103
|
+
"tool_trust_mode": clean_room_tooling.tool_trust_mode(allow_project_tools),
|
|
104
|
+
"local_cache": clean_room_tooling.observed(clean_room_tooling.USER_TOOLS_DIR.as_posix()),
|
|
105
|
+
"installable_local_tools": {
|
|
106
|
+
name: {"package": item["package"], "source": item["source"]}
|
|
107
|
+
for name, item in sorted(NPM_TOOLS.items())
|
|
108
|
+
},
|
|
109
|
+
"tools": {
|
|
110
|
+
name: clean_room_tooling.executable_status(
|
|
111
|
+
name,
|
|
112
|
+
allow_project_tools=allow_project_tools,
|
|
113
|
+
probe_tools=probe_tools,
|
|
114
|
+
)
|
|
115
|
+
for name in STATUS_TOOLS
|
|
116
|
+
},
|
|
117
|
+
"node_packages": package_status(allow_project_tools),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def npm_package_spec(package: str, version: str) -> str:
|
|
122
|
+
return f"{package}@{version}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def install_npm_tool(tool: str, version: str, allow_npm_scripts: bool = False) -> dict[str, Any]:
|
|
126
|
+
spec = NPM_TOOLS[tool]
|
|
127
|
+
npm = clean_room_tooling.find_executable("npm")
|
|
128
|
+
if npm is None:
|
|
129
|
+
return clean_room_tooling.error_fact(
|
|
130
|
+
"npm unavailable; install Node/npm first or set NPM_BIN",
|
|
131
|
+
evidence={"checked_locations": clean_room_tooling.checked_executable_locations("npm")},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
prefix = clean_room_tooling.USER_NPM_PREFIX
|
|
135
|
+
prefix.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
package_spec = npm_package_spec(str(spec["package"]), version)
|
|
137
|
+
argv = [
|
|
138
|
+
npm.path.as_posix(),
|
|
139
|
+
"install",
|
|
140
|
+
"--prefix",
|
|
141
|
+
prefix.as_posix(),
|
|
142
|
+
"--save-exact",
|
|
143
|
+
]
|
|
144
|
+
if not allow_npm_scripts:
|
|
145
|
+
argv.append("--ignore-scripts")
|
|
146
|
+
argv.append(package_spec)
|
|
147
|
+
|
|
148
|
+
completed = subprocess.run(
|
|
149
|
+
argv,
|
|
150
|
+
capture_output=True,
|
|
151
|
+
text=True,
|
|
152
|
+
timeout=LOCAL_INSTALL_TIMEOUT_SECONDS,
|
|
153
|
+
check=False,
|
|
154
|
+
)
|
|
155
|
+
status = "observed" if completed.returncode == 0 else "error"
|
|
156
|
+
result: dict[str, Any] = {
|
|
157
|
+
"schema_version": 1,
|
|
158
|
+
"tool": tool,
|
|
159
|
+
"source": spec["source"],
|
|
160
|
+
"version": version,
|
|
161
|
+
"package_spec": package_spec,
|
|
162
|
+
"install_root": clean_room_tooling.observed(prefix.as_posix()),
|
|
163
|
+
"install_trust_mode": clean_room_tooling.observed(
|
|
164
|
+
"explicit-version",
|
|
165
|
+
evidence={"npm_lifecycle_scripts": "allowed" if allow_npm_scripts else "ignored"},
|
|
166
|
+
),
|
|
167
|
+
"command": clean_room_tooling.fact(
|
|
168
|
+
status,
|
|
169
|
+
{
|
|
170
|
+
"argv": argv,
|
|
171
|
+
"returncode": completed.returncode,
|
|
172
|
+
"stdout": completed.stdout.strip(),
|
|
173
|
+
"stderr": completed.stderr.strip(),
|
|
174
|
+
},
|
|
175
|
+
),
|
|
176
|
+
}
|
|
177
|
+
resolved = clean_room_tooling.find_executable(str(spec["bin"]))
|
|
178
|
+
result["resolved_tool"] = (
|
|
179
|
+
clean_room_tooling.observed({"path": resolved.path.as_posix(), "source": resolved.source})
|
|
180
|
+
if resolved
|
|
181
|
+
else clean_room_tooling.unknown("tool installed but executable was not resolved")
|
|
182
|
+
)
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main() -> int:
|
|
187
|
+
args = parse_args()
|
|
188
|
+
if args.status:
|
|
189
|
+
write_json(status_report(args.allow_working_project_tools, args.probe_tools))
|
|
190
|
+
if args.install_local:
|
|
191
|
+
result = install_npm_tool(args.install_local, args.version, args.allow_npm_scripts)
|
|
192
|
+
write_json(result)
|
|
193
|
+
if result.get("command", {}).get("status") == "error":
|
|
194
|
+
return 1
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Tool discovery helpers for clean-room source-index preflight."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
COMMAND_TIMEOUT_SECONDS = 30
|
|
13
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
+
PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
15
|
+
SKILL_TOOLS_DIR = SKILL_ROOT / "tools"
|
|
16
|
+
USER_TOOLS_DIR = Path.home() / ".cache" / "re-skills" / "clean-room-tools"
|
|
17
|
+
USER_NPM_PREFIX = USER_TOOLS_DIR / "npm"
|
|
18
|
+
PROJECT_TOOLS_ENV = "RE_SKILLS_TRUST_PROJECT_TOOLS"
|
|
19
|
+
TRUSTED_PATH_PREFIXES = (
|
|
20
|
+
Path("/bin"),
|
|
21
|
+
Path("/usr/bin"),
|
|
22
|
+
Path("/usr/sbin"),
|
|
23
|
+
Path("/sbin"),
|
|
24
|
+
Path("/Library/Apple/usr/bin"),
|
|
25
|
+
Path("/opt/homebrew"),
|
|
26
|
+
Path("/usr/local"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
TOOL_ENV = {
|
|
30
|
+
"ast-grep": "AST_GREP_BIN",
|
|
31
|
+
"ctags": "CTAGS_BIN",
|
|
32
|
+
"node": "NODE_BIN",
|
|
33
|
+
"npm": "NPM_BIN",
|
|
34
|
+
"scip": "SCIP_BIN",
|
|
35
|
+
"sg": "SG_BIN",
|
|
36
|
+
"universal-ctags": "UNIVERSAL_CTAGS_BIN",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
BREW_HINTS = {
|
|
40
|
+
"ast-grep": "Offer to run: brew install ast-grep",
|
|
41
|
+
"ctags": "Offer to run: brew install universal-ctags",
|
|
42
|
+
"node": "Offer to run: brew install node",
|
|
43
|
+
"npm": "Offer to run: brew install node",
|
|
44
|
+
"scip": "Install scip only when source-index export is explicitly needed.",
|
|
45
|
+
"sg": "Offer to run: brew install ast-grep",
|
|
46
|
+
"universal-ctags": "Offer to run: brew install universal-ctags",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
LOCAL_HINTS = {
|
|
50
|
+
"ast-grep": "Or run scripts/clean_room_tool_manager.py --install-local ast-grep --version <exact-version>.",
|
|
51
|
+
"ctags": "Or place ctags under ~/.cache/re-skills/clean-room-tools/bin/ or set CTAGS_BIN.",
|
|
52
|
+
"node": "Or place node under ~/.cache/re-skills/clean-room-tools/bin/ or set NODE_BIN.",
|
|
53
|
+
"npm": "Or place npm under ~/.cache/re-skills/clean-room-tools/bin/ or set NPM_BIN.",
|
|
54
|
+
"scip": "Or place scip under ~/.cache/re-skills/clean-room-tools/bin/ or set SCIP_BIN.",
|
|
55
|
+
"sg": "Or place sg under ~/.cache/re-skills/clean-room-tools/bin/ or set SG_BIN.",
|
|
56
|
+
"universal-ctags": "Or place universal-ctags under ~/.cache/re-skills/clean-room-tools/bin/ or set UNIVERSAL_CTAGS_BIN.",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class ResolvedTool:
|
|
62
|
+
path: Path
|
|
63
|
+
source: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def path_is_relative_to(path: Path, root: Path) -> bool:
|
|
67
|
+
try:
|
|
68
|
+
path.relative_to(root)
|
|
69
|
+
except ValueError:
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def project_tools_allowed(allow_project_tools: bool = False) -> bool:
|
|
75
|
+
env_value = os.environ.get(PROJECT_TOOLS_ENV, "")
|
|
76
|
+
return allow_project_tools or env_value.lower() in {"1", "true", "yes", "on"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def trust_mode_name(allow_project_tools: bool = False) -> str:
|
|
80
|
+
return "project-tools" if project_tools_allowed(allow_project_tools) else "trusted-only"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fact(status: str, value: Any = None, evidence: Any = None, note: str | None = None) -> dict[str, Any]:
|
|
84
|
+
item: dict[str, Any] = {"status": status, "value": value}
|
|
85
|
+
if evidence is not None:
|
|
86
|
+
item["evidence"] = evidence
|
|
87
|
+
if note:
|
|
88
|
+
item["note"] = note
|
|
89
|
+
return item
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def observed(value: Any, evidence: Any = None, note: str | None = None) -> dict[str, Any]:
|
|
93
|
+
return fact("observed", value, evidence, note)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def unknown(note: str, value: Any = None) -> dict[str, Any]:
|
|
97
|
+
return fact("unknown", value, note=note)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def error_fact(message: str, evidence: Any = None) -> dict[str, Any]:
|
|
101
|
+
return fact("error", None, evidence=evidence, note=message)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_command(argv: list[str], timeout: int = COMMAND_TIMEOUT_SECONDS) -> dict[str, Any]:
|
|
105
|
+
try:
|
|
106
|
+
completed = subprocess.run(
|
|
107
|
+
argv,
|
|
108
|
+
capture_output=True,
|
|
109
|
+
text=True,
|
|
110
|
+
timeout=timeout,
|
|
111
|
+
check=False,
|
|
112
|
+
)
|
|
113
|
+
except subprocess.TimeoutExpired:
|
|
114
|
+
return error_fact(f"command timed out after {timeout} seconds", evidence=argv)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
return error_fact(str(exc), evidence=argv)
|
|
117
|
+
|
|
118
|
+
return fact(
|
|
119
|
+
"observed" if completed.returncode == 0 else "error",
|
|
120
|
+
{
|
|
121
|
+
"argv": argv,
|
|
122
|
+
"returncode": completed.returncode,
|
|
123
|
+
"stdout": completed.stdout.strip(),
|
|
124
|
+
"stderr": completed.stderr.strip(),
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def probe_command(argv: list[str], timeout: int = COMMAND_TIMEOUT_SECONDS) -> str | None:
|
|
130
|
+
try:
|
|
131
|
+
completed = subprocess.run(
|
|
132
|
+
argv,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
timeout=timeout,
|
|
136
|
+
check=False,
|
|
137
|
+
)
|
|
138
|
+
except Exception:
|
|
139
|
+
return None
|
|
140
|
+
if completed.returncode != 0:
|
|
141
|
+
return None
|
|
142
|
+
value = completed.stdout.strip()
|
|
143
|
+
return value or None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def local_executable_candidates(name: str) -> list[tuple[Path, str]]:
|
|
147
|
+
candidates: list[tuple[Path, str]] = []
|
|
148
|
+
for root, source in [(USER_TOOLS_DIR, "user-cache"), (SKILL_TOOLS_DIR, "skill-local")]:
|
|
149
|
+
candidates.extend(
|
|
150
|
+
[
|
|
151
|
+
(root / name, source),
|
|
152
|
+
(root / "bin" / name, source),
|
|
153
|
+
(root / name / "bin" / name, source),
|
|
154
|
+
(root / "npm" / "node_modules" / ".bin" / name, source),
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
return candidates
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def project_executable_candidates(name: str) -> list[tuple[Path, str]]:
|
|
161
|
+
return [
|
|
162
|
+
(Path.cwd() / ".local" / "bin" / name, "working-project-local"),
|
|
163
|
+
(Path.cwd() / ".bin" / name, "working-project-bin"),
|
|
164
|
+
(Path.cwd() / "node_modules" / ".bin" / name, "working-project-node-modules"),
|
|
165
|
+
(SKILL_ROOT / "node_modules" / ".bin" / name, "skill-node-modules"),
|
|
166
|
+
(PLUGIN_ROOT / "node_modules" / ".bin" / name, "plugin-local"),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def trusted_path_candidates(name: str) -> list[tuple[Path, str]]:
|
|
171
|
+
candidates: list[tuple[Path, str]] = []
|
|
172
|
+
for raw_dir in os.environ.get("PATH", "").split(os.pathsep):
|
|
173
|
+
if not raw_dir:
|
|
174
|
+
continue
|
|
175
|
+
directory = Path(raw_dir).expanduser()
|
|
176
|
+
candidate = directory / name
|
|
177
|
+
try:
|
|
178
|
+
normalized_dir = directory.resolve(strict=False)
|
|
179
|
+
except RuntimeError:
|
|
180
|
+
normalized_dir = directory.absolute()
|
|
181
|
+
if any(path_is_relative_to(normalized_dir, prefix) for prefix in TRUSTED_PATH_PREFIXES):
|
|
182
|
+
candidates.append((candidate, "trusted-path"))
|
|
183
|
+
return candidates
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def npm_path(command: list[str], allow_project_tools: bool = False) -> Path | None:
|
|
187
|
+
npm = find_executable("npm", allow_project_tools)
|
|
188
|
+
if npm is None:
|
|
189
|
+
return None
|
|
190
|
+
value = probe_command([npm.path.as_posix(), *command])
|
|
191
|
+
if value is None:
|
|
192
|
+
return None
|
|
193
|
+
return Path(value).expanduser()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def executable_candidates(
|
|
197
|
+
name: str,
|
|
198
|
+
allow_project_tools: bool = False,
|
|
199
|
+
probe_tools: bool = False,
|
|
200
|
+
) -> list[tuple[Path, str]]:
|
|
201
|
+
candidates: list[tuple[Path, str]] = []
|
|
202
|
+
env_name = TOOL_ENV.get(name)
|
|
203
|
+
if env_name:
|
|
204
|
+
env_value = os.environ.get(env_name)
|
|
205
|
+
if env_value:
|
|
206
|
+
candidates.append((Path(env_value), env_name))
|
|
207
|
+
|
|
208
|
+
candidates.extend(local_executable_candidates(name))
|
|
209
|
+
|
|
210
|
+
if project_tools_allowed(allow_project_tools):
|
|
211
|
+
candidates.extend(project_executable_candidates(name))
|
|
212
|
+
if probe_tools:
|
|
213
|
+
prefix = npm_path(["prefix"], allow_project_tools=False)
|
|
214
|
+
if prefix is not None:
|
|
215
|
+
candidates.append((prefix / "node_modules" / ".bin" / name, "npm-prefix"))
|
|
216
|
+
root = npm_path(["root"], allow_project_tools=False)
|
|
217
|
+
if root is not None:
|
|
218
|
+
candidates.append((root / ".bin" / name, "npm-prefix"))
|
|
219
|
+
global_prefix = npm_path(["prefix", "-g"], allow_project_tools=False)
|
|
220
|
+
if global_prefix is not None:
|
|
221
|
+
candidates.append((global_prefix / "bin" / name, "npm-global"))
|
|
222
|
+
|
|
223
|
+
candidates.extend(trusted_path_candidates(name))
|
|
224
|
+
return candidates
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def find_executable(
|
|
228
|
+
name: str,
|
|
229
|
+
allow_project_tools: bool = False,
|
|
230
|
+
probe_tools: bool = False,
|
|
231
|
+
) -> ResolvedTool | None:
|
|
232
|
+
seen: set[Path] = set()
|
|
233
|
+
for candidate, source in executable_candidates(name, allow_project_tools, probe_tools):
|
|
234
|
+
candidate = candidate.expanduser()
|
|
235
|
+
try:
|
|
236
|
+
normalized = candidate.resolve(strict=False)
|
|
237
|
+
except RuntimeError:
|
|
238
|
+
normalized = candidate.absolute()
|
|
239
|
+
if normalized in seen:
|
|
240
|
+
continue
|
|
241
|
+
seen.add(normalized)
|
|
242
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
243
|
+
return ResolvedTool(candidate.resolve(), source)
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def checked_executable_locations(
|
|
248
|
+
name: str,
|
|
249
|
+
allow_project_tools: bool = False,
|
|
250
|
+
probe_tools: bool = False,
|
|
251
|
+
) -> list[str]:
|
|
252
|
+
locations: list[str] = []
|
|
253
|
+
env_name = TOOL_ENV.get(name)
|
|
254
|
+
if env_name:
|
|
255
|
+
env_value = os.environ.get(env_name)
|
|
256
|
+
locations.append(f"${env_name}" if not env_value else f"${env_name}={env_value}")
|
|
257
|
+
locations.extend(path.as_posix() for path, _source in local_executable_candidates(name))
|
|
258
|
+
if project_tools_allowed(allow_project_tools):
|
|
259
|
+
locations.extend(path.as_posix() for path, _source in project_executable_candidates(name))
|
|
260
|
+
locations.append("npm prefix/global bins" if probe_tools else "npm prefix/global bins require --probe-tools")
|
|
261
|
+
locations.append("trusted PATH allowlist")
|
|
262
|
+
return locations
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def missing_tool_hint(name: str) -> str:
|
|
266
|
+
brew = BREW_HINTS.get(name, f"Install {name} with Homebrew if available.")
|
|
267
|
+
local = LOCAL_HINTS.get(name, f"Or place {name} under {USER_TOOLS_DIR.as_posix()} or set an explicit env var.")
|
|
268
|
+
return f"{name} unavailable. {brew}. {local}"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def executable_status(
|
|
272
|
+
name: str,
|
|
273
|
+
version_args: list[str] | None = None,
|
|
274
|
+
allow_project_tools: bool = False,
|
|
275
|
+
probe_tools: bool = False,
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
resolved = find_executable(name, allow_project_tools, probe_tools)
|
|
278
|
+
if resolved is None:
|
|
279
|
+
return unknown(
|
|
280
|
+
missing_tool_hint(name),
|
|
281
|
+
value={
|
|
282
|
+
"checked_locations": checked_executable_locations(name, allow_project_tools, probe_tools),
|
|
283
|
+
"brew_option": BREW_HINTS.get(name),
|
|
284
|
+
"local_option": LOCAL_HINTS.get(name),
|
|
285
|
+
"tool_trust_mode": trust_mode_name(allow_project_tools),
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
if not probe_tools:
|
|
289
|
+
return observed(
|
|
290
|
+
{
|
|
291
|
+
"path": resolved.path.as_posix(),
|
|
292
|
+
"source": resolved.source,
|
|
293
|
+
"version": unknown("not probed; pass --probe-tools to execute version commands"),
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
argv = [resolved.path.as_posix(), *(version_args or ["--version"])]
|
|
297
|
+
return observed(
|
|
298
|
+
{
|
|
299
|
+
"path": resolved.path.as_posix(),
|
|
300
|
+
"source": resolved.source,
|
|
301
|
+
"version": run_command(argv),
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def node_resolver_roots(allow_project_tools: bool = False) -> list[Path]:
|
|
307
|
+
roots = [
|
|
308
|
+
USER_NPM_PREFIX,
|
|
309
|
+
USER_TOOLS_DIR,
|
|
310
|
+
SKILL_TOOLS_DIR,
|
|
311
|
+
]
|
|
312
|
+
if project_tools_allowed(allow_project_tools):
|
|
313
|
+
roots.extend([Path.cwd(), SKILL_ROOT, PLUGIN_ROOT])
|
|
314
|
+
return roots
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def tool_trust_mode(allow_project_tools: bool = False) -> dict[str, Any]:
|
|
318
|
+
return observed(
|
|
319
|
+
trust_mode_name(allow_project_tools),
|
|
320
|
+
evidence={
|
|
321
|
+
"explicit_allow_project_tools": bool(allow_project_tools),
|
|
322
|
+
"env": PROJECT_TOOLS_ENV if os.environ.get(PROJECT_TOOLS_ENV) else None,
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def dependency_report(allow_project_tools: bool = False, probe_tools: bool = False) -> dict[str, Any]:
|
|
328
|
+
npm_prefix = (
|
|
329
|
+
npm_path(["prefix"], allow_project_tools=False)
|
|
330
|
+
if probe_tools and project_tools_allowed(allow_project_tools)
|
|
331
|
+
else None
|
|
332
|
+
)
|
|
333
|
+
npm_root = (
|
|
334
|
+
npm_path(["root"], allow_project_tools=False)
|
|
335
|
+
if probe_tools and project_tools_allowed(allow_project_tools)
|
|
336
|
+
else None
|
|
337
|
+
)
|
|
338
|
+
return {
|
|
339
|
+
"external_tools_policy": observed(
|
|
340
|
+
"Source-index preflight detects optional AST and indexing tools with filesystem checks by default. "
|
|
341
|
+
"It executes version probes only when --probe-tools is set, and does not consider project-local "
|
|
342
|
+
f"executables unless --allow-working-project-tools or {PROJECT_TOOLS_ENV}=1 is set."
|
|
343
|
+
),
|
|
344
|
+
"tool_trust_mode": tool_trust_mode(allow_project_tools),
|
|
345
|
+
"tool_locations": observed(
|
|
346
|
+
{
|
|
347
|
+
"user_cache": USER_TOOLS_DIR.as_posix(),
|
|
348
|
+
"user_npm_prefix": USER_NPM_PREFIX.as_posix(),
|
|
349
|
+
"skill_local_tools": SKILL_TOOLS_DIR.as_posix(),
|
|
350
|
+
"trusted_path_prefixes": [path.as_posix() for path in TRUSTED_PATH_PREFIXES],
|
|
351
|
+
"project_local_candidates": [
|
|
352
|
+
".local/bin",
|
|
353
|
+
".bin",
|
|
354
|
+
"node_modules/.bin",
|
|
355
|
+
],
|
|
356
|
+
}
|
|
357
|
+
),
|
|
358
|
+
"tool_probe_mode": observed("execute-version" if probe_tools else "stat-only"),
|
|
359
|
+
"node": executable_status("node", allow_project_tools=allow_project_tools, probe_tools=probe_tools),
|
|
360
|
+
"npm": executable_status("npm", allow_project_tools=allow_project_tools, probe_tools=probe_tools),
|
|
361
|
+
"ast_grep": executable_status("ast-grep", allow_project_tools=allow_project_tools, probe_tools=probe_tools),
|
|
362
|
+
"sg": executable_status("sg", allow_project_tools=allow_project_tools, probe_tools=probe_tools),
|
|
363
|
+
"ctags": executable_status("ctags", allow_project_tools=allow_project_tools, probe_tools=probe_tools),
|
|
364
|
+
"universal_ctags": executable_status(
|
|
365
|
+
"universal-ctags", allow_project_tools=allow_project_tools, probe_tools=probe_tools
|
|
366
|
+
),
|
|
367
|
+
"scip": executable_status("scip", allow_project_tools=allow_project_tools, probe_tools=probe_tools),
|
|
368
|
+
"npm_prefix": observed(npm_prefix.as_posix()) if npm_prefix else unknown("npm prefix not probed"),
|
|
369
|
+
"npm_root": observed(npm_root.as_posix()) if npm_root else unknown("npm root not probed"),
|
|
370
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unattended
|
|
3
|
+
description: Starts the Clean Room startup wizard in bounded unattended controller mode for authorized spec-only source-to-spec work with finite loop limits and safety stops.
|
|
4
|
+
argument-hint: [authorized source scope, separated output roots, optional max iterations]
|
|
5
|
+
disable-model-invocation: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Clean Room Unattended
|
|
9
|
+
|
|
10
|
+
Start the clean-room startup wizard with `controller_policy.mode` fixed to `unattended`.
|
|
11
|
+
|
|
12
|
+
Use the canonical `clean-room` skill workflow and references in this plugin. Preserve the same spec-only boundary, role separation, artifact schemas, leakage rules, and hook expectations.
|
|
13
|
+
|
|
14
|
+
Gather only required setup facts:
|
|
15
|
+
|
|
16
|
+
- Authorization statement, requester, allowed actions, prohibited actions, and evidence handling.
|
|
17
|
+
- Source roots, contaminated artifact root, clean root, and optional public or destination reference roots.
|
|
18
|
+
- Target language or destination constraints, if known.
|
|
19
|
+
- Target schema profile: `openspec-delta`, `gsd-planning-package`, `speckit-feature-folder`, or `kiro-spec-folder`.
|
|
20
|
+
- Finite maximum iteration count. Use `10` when the user does not provide a value.
|
|
21
|
+
|
|
22
|
+
Before indexing or artifact generation, confirm that source roots, contaminated artifact roots, and clean roots are separate paths. Stop if authorization is unclear, if the requested output includes replacement implementation code, or if clean and contaminated roots overlap.
|
|
23
|
+
|
|
24
|
+
Record `controller_policy.mode` as `unattended`, `max_units_per_iteration` as `1`, and `max_iterations` as the selected finite value. Include these stop conditions: `authorization-missing`, `scope-change`, `contamination-suspected`, `schema-validation-failed`, `leakage-scan-failed`, `unit-blocked`, `coverage-complete`, and `iteration-limit-reached`.
|
|
25
|
+
|
|
26
|
+
For multi-file source scope, guide agent zero/controller to run `skills/clean-room/scripts/build_source_index.py` as preflight outside clean-room role sessions. Store `source-index.json` only under the contaminated artifact root and never include it in clean handoff packages.
|