ixt-cli 0.8.0__py3-none-any.whl
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.
- ixt/__init__.py +8 -0
- ixt/__main__.py +8 -0
- ixt/backends/__init__.py +1 -0
- ixt/backends/binary.py +935 -0
- ixt/backends/binary_resolver.py +307 -0
- ixt/backends/node.py +490 -0
- ixt/backends/python.py +234 -0
- ixt/cli/__init__.py +31 -0
- ixt/cli/argparse_completion.py +557 -0
- ixt/cli/cmd_apply.py +404 -0
- ixt/cli/cmd_cache.py +86 -0
- ixt/cli/cmd_config.py +295 -0
- ixt/cli/cmd_info.py +116 -0
- ixt/cli/cmd_install.py +508 -0
- ixt/cli/cmd_misc.py +261 -0
- ixt/cli/cmd_registry.py +35 -0
- ixt/cli/cmd_upgrade.py +336 -0
- ixt/cli/commands.py +70 -0
- ixt/cli/parser.py +555 -0
- ixt/cli/render.py +85 -0
- ixt/config/__init__.py +5 -0
- ixt/config/asset_index.py +305 -0
- ixt/config/asset_pattern_cache.py +87 -0
- ixt/config/env_policy.py +340 -0
- ixt/config/flags.py +29 -0
- ixt/config/fs_policy.py +17 -0
- ixt/config/heuristics.py +465 -0
- ixt/config/models.py +176 -0
- ixt/config/registry.py +145 -0
- ixt/config/settings.py +173 -0
- ixt/config/setup_toml.py +179 -0
- ixt/config/toml.py +416 -0
- ixt/core/__init__.py +16 -0
- ixt/core/apply.py +564 -0
- ixt/core/apply_actions.py +106 -0
- ixt/core/backend.py +187 -0
- ixt/core/bootstrap.py +410 -0
- ixt/core/cache.py +332 -0
- ixt/core/discover.py +150 -0
- ixt/core/doctor.py +591 -0
- ixt/core/export.py +419 -0
- ixt/core/expose.py +350 -0
- ixt/core/extract.py +261 -0
- ixt/core/hooks.py +182 -0
- ixt/core/identity.py +148 -0
- ixt/core/inject.py +143 -0
- ixt/core/install.py +509 -0
- ixt/core/install_local.py +229 -0
- ixt/core/locks.py +54 -0
- ixt/core/pathlink.py +86 -0
- ixt/core/resolution_stats.py +191 -0
- ixt/core/resolve.py +150 -0
- ixt/core/resolve_cache.py +185 -0
- ixt/core/runtimes.py +192 -0
- ixt/core/save.py +237 -0
- ixt/core/setup_completions.py +11 -0
- ixt/core/setup_path.py +368 -0
- ixt/core/upgrade.py +596 -0
- ixt/data/__init__.py +10 -0
- ixt/data/asset_index.json +574 -0
- ixt/data/heuristics.toml +98 -0
- ixt/data/registry.toml +71 -0
- ixt/libs/__init__.py +3 -0
- ixt/libs/constants.py +4 -0
- ixt/libs/fmt.py +108 -0
- ixt/libs/logger.py +109 -0
- ixt/libs/output.py +25 -0
- ixt/libs/req_spec.py +115 -0
- ixt/libs/semver.py +149 -0
- ixt/libs/shell.py +126 -0
- ixt/libs/style.py +238 -0
- ixt/net/__init__.py +1 -0
- ixt/net/github_api.py +158 -0
- ixt/net/gitlab_api.py +149 -0
- ixt/net/http.py +194 -0
- ixt/net/npm.py +24 -0
- ixt/net/pypi.py +26 -0
- ixt/net/source.py +163 -0
- ixt/platform/__init__.py +131 -0
- ixt/platform/win.py +68 -0
- ixt_cli-0.8.0.dist-info/METADATA +294 -0
- ixt_cli-0.8.0.dist-info/RECORD +84 -0
- ixt_cli-0.8.0.dist-info/WHEEL +4 -0
- ixt_cli-0.8.0.dist-info/entry_points.txt +2 -0
ixt/config/env_policy.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Per-tool environment variable policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ixt.config.models import ToolRecord
|
|
13
|
+
|
|
14
|
+
# Public names of the env vars the shim and CLI honour. Single source
|
|
15
|
+
# of truth — referenced from tests, docs, and the warning text below.
|
|
16
|
+
ENV_POLICY_QUIET = "IXT_POLICY_QUIET"
|
|
17
|
+
ENV_DISABLE_BWRAP = "IXT_DISABLE_BWRAP"
|
|
18
|
+
|
|
19
|
+
# Lines printed when env policy is active without bwrap, both at config
|
|
20
|
+
# time (CLI) and at runtime (shim, throttled by a per-session marker
|
|
21
|
+
# file). Kept as data so the two emitters stay in sync verbatim.
|
|
22
|
+
DEGRADED_WARNING_LINES: list[str] = [
|
|
23
|
+
f"Warning: env policy active without bubblewrap (set {ENV_POLICY_QUIET}=1 to silence).",
|
|
24
|
+
" Effective protection: hygiene only (logs, traces, naive telemetry).",
|
|
25
|
+
" Active threats NOT blocked: /proc/$PPID/environ, /proc/*/environ",
|
|
26
|
+
" reads, secret files (~/.aws, ~/.netrc, ~/.ssh, ...).",
|
|
27
|
+
"",
|
|
28
|
+
" Install bubblewrap for effective protection:",
|
|
29
|
+
" sudo apt install bubblewrap # Debian/Ubuntu",
|
|
30
|
+
" sudo dnf install bubblewrap # Fedora/RHEL",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
ENV_MODELS: dict[str, list[str]] = {
|
|
34
|
+
"os-common": [
|
|
35
|
+
"HOME",
|
|
36
|
+
"PATH",
|
|
37
|
+
"XDG_*",
|
|
38
|
+
"NO_COLOR",
|
|
39
|
+
"FORCE_COLOR",
|
|
40
|
+
"TERM",
|
|
41
|
+
"COLORTERM",
|
|
42
|
+
"LANG",
|
|
43
|
+
"LC_*",
|
|
44
|
+
"USER",
|
|
45
|
+
"LOGNAME",
|
|
46
|
+
"TMPDIR",
|
|
47
|
+
"SHELL",
|
|
48
|
+
"TZ",
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _matches(name: str, patterns: list[str]) -> bool:
|
|
54
|
+
return any(fnmatch.fnmatch(name, p) for p in patterns)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_denied(name: str, deny: dict[str, dict]) -> bool:
|
|
58
|
+
for pattern, rule in deny.items():
|
|
59
|
+
exceptions: list[str] = rule.get("except", [])
|
|
60
|
+
if fnmatch.fnmatch(name, pattern) and not _matches(name, exceptions):
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compute_env(
|
|
66
|
+
environ: dict[str, str],
|
|
67
|
+
base: str,
|
|
68
|
+
allow: list[str],
|
|
69
|
+
deny: dict[str, dict],
|
|
70
|
+
) -> dict[str, str]:
|
|
71
|
+
"""Return the filtered env dict for a tool invocation."""
|
|
72
|
+
if base == "all":
|
|
73
|
+
result = dict(environ)
|
|
74
|
+
elif base == "none":
|
|
75
|
+
result = {}
|
|
76
|
+
else:
|
|
77
|
+
model_patterns = ENV_MODELS.get(base, [])
|
|
78
|
+
result = {k: v for k, v in environ.items() if _matches(k, model_patterns)}
|
|
79
|
+
|
|
80
|
+
result.update({k: v for k, v in environ.items() if _matches(k, allow)})
|
|
81
|
+
result = {k: v for k, v in result.items() if not _is_denied(k, deny)}
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def has_env_policy(record: ToolRecord) -> bool:
|
|
86
|
+
return record.env_base != "all" or bool(record.env_allow) or bool(record.env_deny)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_SHIM_TEMPLATE = """\
|
|
90
|
+
#!/usr/bin/env python3
|
|
91
|
+
# ixt shim: {name}
|
|
92
|
+
import fnmatch as _f, os as _os, shutil as _sh, sys as _sys
|
|
93
|
+
|
|
94
|
+
_HERE = _os.path.dirname(_os.path.abspath(__file__))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _abs(p):
|
|
98
|
+
return p if _os.path.isabs(p) else _os.path.abspath(_os.path.join(_HERE, p))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_R = _abs({real!r})
|
|
102
|
+
_ED = _abs({env_dir!r})
|
|
103
|
+
_B = {base!r}
|
|
104
|
+
_A = {allow!r}
|
|
105
|
+
_D = {deny!r}
|
|
106
|
+
_M = {model!r}
|
|
107
|
+
_FB = {fs_base!r}
|
|
108
|
+
_FRO = {fs_ro!r}
|
|
109
|
+
_FRW = {fs_rw!r}
|
|
110
|
+
_FSC = {fs_scratch!r}
|
|
111
|
+
|
|
112
|
+
# Read opt-out flags BEFORE any env filtering — they must work even with
|
|
113
|
+
# a strict env policy that would otherwise drop them.
|
|
114
|
+
_QUIET = bool(_os.environ.get({quiet_var!r}))
|
|
115
|
+
_DISABLE_BWRAP = bool(_os.environ.get({disable_bwrap_var!r}))
|
|
116
|
+
_WARNING_LINES = {warning_lines!r}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _m(n, ps):
|
|
120
|
+
return any(_f.fnmatch(n, p) for p in ps)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_src = _os.environ
|
|
124
|
+
if _B == "all":
|
|
125
|
+
_e = dict(_src)
|
|
126
|
+
elif _B == "none":
|
|
127
|
+
_e = {{}}
|
|
128
|
+
else:
|
|
129
|
+
_e = {{k: v for k, v in _src.items() if _m(k, _M)}}
|
|
130
|
+
_e.update({{k: v for k, v in _src.items() if _m(k, _A)}})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _denied(n, d):
|
|
134
|
+
for pat, rule in d.items():
|
|
135
|
+
excs = rule.get("except", [])
|
|
136
|
+
if _f.fnmatch(n, pat) and not any(_f.fnmatch(n, e) for e in excs):
|
|
137
|
+
return True
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
_e = {{k: v for k, v in _e.items() if not _denied(k, _D)}}
|
|
142
|
+
|
|
143
|
+
if _os.environ.get("IXT_SHIM_DEBUG") or _os.environ.get("IXT_SHIM_VERBOSE"):
|
|
144
|
+
_bl = {{k for k in _src if k not in _e}}
|
|
145
|
+
_n = {name!r}
|
|
146
|
+
print(f"[ixt shim: {{_n}}] env base={{_B!r}} allow={{_A}} deny={{_D}}", file=_sys.stderr)
|
|
147
|
+
_passed = ' '.join(sorted(_e))
|
|
148
|
+
print(f"[ixt shim: {{_n}}] env passed ({{len(_e)}} vars): {{_passed}}", file=_sys.stderr)
|
|
149
|
+
if _bl:
|
|
150
|
+
_blocked = ' '.join(sorted(_bl))
|
|
151
|
+
print(f"[ixt shim: {{_n}}] env blocked ({{len(_bl)}} vars): {{_blocked}}", file=_sys.stderr)
|
|
152
|
+
if _FB != "all":
|
|
153
|
+
_fs = f"ro={{_FRO}} rw={{_FRW}} scratch={{_FSC}}"
|
|
154
|
+
print(f"[ixt shim: {{_n}}] fs base={{_FB!r}} {{_fs}}", file=_sys.stderr)
|
|
155
|
+
|
|
156
|
+
def _warn_degraded_runtime():
|
|
157
|
+
if _QUIET:
|
|
158
|
+
return
|
|
159
|
+
_state = _os.environ.get("XDG_STATE_HOME") or _os.path.expanduser("~/.local/state")
|
|
160
|
+
_marker_dir = _os.path.join(_state, "ixt")
|
|
161
|
+
try:
|
|
162
|
+
_sid = _os.getsid(0)
|
|
163
|
+
except OSError:
|
|
164
|
+
_sid = _os.getpid() # rare fallback (no controlling tty)
|
|
165
|
+
_marker = _os.path.join(_marker_dir, "policy-warned-" + str(_sid))
|
|
166
|
+
if _os.path.exists(_marker):
|
|
167
|
+
return
|
|
168
|
+
for _line in _WARNING_LINES:
|
|
169
|
+
print("[ixt] " + _line if _line else "[ixt]", file=_sys.stderr)
|
|
170
|
+
try:
|
|
171
|
+
_os.makedirs(_marker_dir, exist_ok=True)
|
|
172
|
+
with open(_marker, "w"):
|
|
173
|
+
pass
|
|
174
|
+
except OSError:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
_has_env = _B != "all" or _A or _D
|
|
179
|
+
_has_fs = _FB != "all" or _FRO or _FRW or _FSC
|
|
180
|
+
_bw = None if _DISABLE_BWRAP else _sh.which("bwrap")
|
|
181
|
+
|
|
182
|
+
if (_has_env or _has_fs) and _bw:
|
|
183
|
+
# Sandbox path. Defense in depth: pass _e (filtered) to execve, not
|
|
184
|
+
# os.environ — so the bwrap process itself starts with the filtered
|
|
185
|
+
# env, and any /proc/<bwrap>/environ would also be clean.
|
|
186
|
+
|
|
187
|
+
# fs_base="none" with no extras → sandbox would be empty, bwrap can't
|
|
188
|
+
# exec the tool. Fail with a clear message.
|
|
189
|
+
if _FB == "none" and not _FRO and not _FRW and not _FSC:
|
|
190
|
+
print(
|
|
191
|
+
"[ixt] fs_base='none' requires at least one path via 'fs ro/rw/scratch'"
|
|
192
|
+
" — the sandbox would be empty.",
|
|
193
|
+
file=_sys.stderr,
|
|
194
|
+
)
|
|
195
|
+
print(
|
|
196
|
+
"[ixt] Use 'ixt tool config <target> fs base app-minimal' for a sane default,",
|
|
197
|
+
file=_sys.stderr,
|
|
198
|
+
)
|
|
199
|
+
print(
|
|
200
|
+
"[ixt] or 'ixt tool config <target> fs ro /usr/lib /lib /lib64 /usr/bin'"
|
|
201
|
+
" to build it manually.",
|
|
202
|
+
file=_sys.stderr,
|
|
203
|
+
)
|
|
204
|
+
_sys.exit(1)
|
|
205
|
+
|
|
206
|
+
_cwd = _os.getcwd()
|
|
207
|
+
# Sandbox constant — PID isolation + private /proc + die-with-parent.
|
|
208
|
+
# Applied for every code path that uses bwrap (fs preset OR env-only).
|
|
209
|
+
_ba = [_bw, "--unshare-pid", "--as-pid-1",
|
|
210
|
+
"--proc", "/proc",
|
|
211
|
+
"--die-with-parent"]
|
|
212
|
+
if _FB == "app-common":
|
|
213
|
+
# Almost-full host visibility, but tmpfs /tmp + ro-bind env_dir on
|
|
214
|
+
# top so the tool's own binary stays reachable when env_dir lives
|
|
215
|
+
# under /tmp (tempfile-based test setups, ephemeral CI runners).
|
|
216
|
+
_ba += ["--ro-bind", "/", "/",
|
|
217
|
+
"--bind", _cwd, _cwd,
|
|
218
|
+
"--tmpfs", "/tmp",
|
|
219
|
+
"--ro-bind", _ED, _ED,
|
|
220
|
+
"--dev", "/dev"]
|
|
221
|
+
elif _FB == "app-minimal":
|
|
222
|
+
# Strict minimum: env_dir + system libs + dynamic linker config.
|
|
223
|
+
# No $HOME, no cwd, no /etc complete, no /tmp, no /usr/bin.
|
|
224
|
+
# --ro-bind-try silently skips paths that don't exist on this host
|
|
225
|
+
# (e.g. /lib64 absent on pure-multilib distros) — saves stat() per
|
|
226
|
+
# invocation and keeps the option list static.
|
|
227
|
+
_ba += ["--ro-bind", _ED, _ED,
|
|
228
|
+
"--ro-bind-try", "/usr/lib", "/usr/lib",
|
|
229
|
+
"--ro-bind-try", "/usr/lib32", "/usr/lib32",
|
|
230
|
+
"--ro-bind-try", "/usr/lib64", "/usr/lib64",
|
|
231
|
+
"--ro-bind-try", "/lib", "/lib",
|
|
232
|
+
"--ro-bind-try", "/lib64", "/lib64",
|
|
233
|
+
"--ro-bind-try", "/etc/ld.so.cache", "/etc/ld.so.cache",
|
|
234
|
+
"--ro-bind-try", "/etc/ld.so.conf", "/etc/ld.so.conf",
|
|
235
|
+
"--ro-bind-try", "/etc/ld.so.conf.d", "/etc/ld.so.conf.d",
|
|
236
|
+
"--dev-bind", "/dev/null", "/dev/null",
|
|
237
|
+
"--dev-bind", "/dev/urandom", "/dev/urandom"]
|
|
238
|
+
elif _FB == "all":
|
|
239
|
+
# env-only via bwrap: full host fs visible (rw). The user opted
|
|
240
|
+
# in to env policy only; we add a sandbox just to close /proc
|
|
241
|
+
# leaks, not to restrict filesystem access.
|
|
242
|
+
_ba += ["--bind", "/", "/",
|
|
243
|
+
"--dev", "/dev"]
|
|
244
|
+
# _FB == "none" → expert mode: only the user-listed binds below.
|
|
245
|
+
for _p in _FRO:
|
|
246
|
+
_ap = _os.path.abspath(_os.path.expanduser(_p))
|
|
247
|
+
_ba += ["--ro-bind", _ap, _ap]
|
|
248
|
+
for _p in _FRW:
|
|
249
|
+
_ap = _os.path.abspath(_os.path.expanduser(_p))
|
|
250
|
+
_ba += ["--bind", _ap, _ap]
|
|
251
|
+
for _p in _FSC:
|
|
252
|
+
_ap = _os.path.abspath(_os.path.expanduser(_p))
|
|
253
|
+
_ba += ["--tmpfs", _ap]
|
|
254
|
+
_ba += ["--clearenv"]
|
|
255
|
+
for _k, _v in _e.items():
|
|
256
|
+
_ba += ["--setenv", _k, _v]
|
|
257
|
+
_ba += ["--", _R] + _sys.argv[1:]
|
|
258
|
+
_os.execve(_bw, _ba, _e)
|
|
259
|
+
elif _has_fs:
|
|
260
|
+
# fs policy was requested but bwrap is unavailable — fs cannot be
|
|
261
|
+
# enforced without it. Fail hard with install instructions.
|
|
262
|
+
print("[ixt] bwrap not found — install bubblewrap to use filesystem isolation",
|
|
263
|
+
file=_sys.stderr)
|
|
264
|
+
print("[ixt] Ubuntu/Debian: sudo apt install bubblewrap", file=_sys.stderr)
|
|
265
|
+
print("[ixt] Fedora/RHEL: sudo dnf install bubblewrap", file=_sys.stderr)
|
|
266
|
+
print("[ixt] Arch: sudo pacman -S bubblewrap", file=_sys.stderr)
|
|
267
|
+
print("[ixt] Alpine: sudo apk add bubblewrap", file=_sys.stderr)
|
|
268
|
+
_sys.exit(1)
|
|
269
|
+
elif _has_env:
|
|
270
|
+
# env-only without bwrap (or with IXT_DISABLE_BWRAP). The filtered
|
|
271
|
+
# env reaches the tool via execve, but /proc-based exfiltration is
|
|
272
|
+
# not blocked. Warn once per shell session.
|
|
273
|
+
_warn_degraded_runtime()
|
|
274
|
+
_os.execve(_R, [_R, *_sys.argv[1:]], _e)
|
|
275
|
+
else:
|
|
276
|
+
# No policy → shouldn't reach the shim (a symlink would handle this).
|
|
277
|
+
# Fallback for safety.
|
|
278
|
+
_os.execve(_R, [_R, *_sys.argv[1:]], _e)
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _path_from_shim_dir(path: str, shim_dir: Path | None) -> str:
|
|
283
|
+
if shim_dir is None:
|
|
284
|
+
return path
|
|
285
|
+
candidate = Path(path)
|
|
286
|
+
if not candidate.is_absolute():
|
|
287
|
+
return path
|
|
288
|
+
return os.path.relpath(candidate, shim_dir)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def generate_shim(
|
|
292
|
+
name: str,
|
|
293
|
+
real_path: str,
|
|
294
|
+
record: ToolRecord,
|
|
295
|
+
*,
|
|
296
|
+
shim_dir: Path | None = None,
|
|
297
|
+
) -> str:
|
|
298
|
+
return _SHIM_TEMPLATE.format(
|
|
299
|
+
name=name,
|
|
300
|
+
real=_path_from_shim_dir(real_path, shim_dir),
|
|
301
|
+
env_dir=_path_from_shim_dir(record.env_dir, shim_dir),
|
|
302
|
+
base=record.env_base,
|
|
303
|
+
allow=record.env_allow,
|
|
304
|
+
deny=record.env_deny,
|
|
305
|
+
model=ENV_MODELS.get(record.env_base, []),
|
|
306
|
+
fs_base=record.fs_base,
|
|
307
|
+
fs_ro=record.fs_ro,
|
|
308
|
+
fs_rw=record.fs_rw,
|
|
309
|
+
fs_scratch=record.fs_scratch,
|
|
310
|
+
quiet_var=ENV_POLICY_QUIET,
|
|
311
|
+
disable_bwrap_var=ENV_DISABLE_BWRAP,
|
|
312
|
+
warning_lines=DEGRADED_WARNING_LINES,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _needs_wrapper(record: ToolRecord) -> bool:
|
|
317
|
+
from ixt.config.fs_policy import has_fs_policy
|
|
318
|
+
|
|
319
|
+
return has_env_policy(record) or has_fs_policy(record)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def apply_policy(record: ToolRecord, bin_dir: Path) -> None:
|
|
323
|
+
"""Replace symlinks with wrappers (or back) for all exposed bins of *record*."""
|
|
324
|
+
use_wrapper = _needs_wrapper(record)
|
|
325
|
+
for exposed_name, real_path in record.exposed_bins.items():
|
|
326
|
+
target = bin_dir / exposed_name
|
|
327
|
+
if target.exists() or target.is_symlink():
|
|
328
|
+
target.unlink()
|
|
329
|
+
if use_wrapper:
|
|
330
|
+
source = generate_shim(exposed_name, real_path, record, shim_dir=bin_dir)
|
|
331
|
+
target.write_text(source, encoding="utf-8")
|
|
332
|
+
target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
333
|
+
else:
|
|
334
|
+
source_path = Path(real_path)
|
|
335
|
+
link_source = (
|
|
336
|
+
os.path.relpath(source_path, target.parent)
|
|
337
|
+
if source_path.is_absolute()
|
|
338
|
+
else real_path
|
|
339
|
+
)
|
|
340
|
+
os.symlink(link_source, target)
|
ixt/config/flags.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Feature flags for experimental or gated functionality.
|
|
2
|
+
|
|
3
|
+
Flags are opt-in via environment variables and default to OFF. They let
|
|
4
|
+
us ship code paths that are complete but lack full E2E validation yet,
|
|
5
|
+
without advertising them as stable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
_TRUE_VALUES = {"1", "true", "yes", "on"}
|
|
13
|
+
|
|
14
|
+
SETUP_TOML_ENV = "IXT_ENABLE_SETUP_TOML"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _is_env_flag_on(name: str) -> bool:
|
|
18
|
+
value = os.environ.get(name, "").strip().lower()
|
|
19
|
+
return value in _TRUE_VALUES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_setup_toml_enabled() -> bool:
|
|
23
|
+
"""Whether ``ixt.setup.toml`` support (tool-author contract) is active.
|
|
24
|
+
|
|
25
|
+
OFF by default in v0.1.0 — the feature is implemented and unit-tested
|
|
26
|
+
but lacks E2E coverage for hook execution. Set ``SETUP_TOML_ENV=1``
|
|
27
|
+
to enable both remote fetch and the ``--setup-file`` CLI flag.
|
|
28
|
+
"""
|
|
29
|
+
return _is_env_flag_on(SETUP_TOML_ENV)
|
ixt/config/fs_policy.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Per-tool filesystem policy (bwrap-based)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ixt.config.models import ToolRecord
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def has_fs_policy(record: ToolRecord) -> bool:
|
|
12
|
+
return (
|
|
13
|
+
record.fs_base != "all"
|
|
14
|
+
or bool(record.fs_ro)
|
|
15
|
+
or bool(record.fs_rw)
|
|
16
|
+
or bool(record.fs_scratch)
|
|
17
|
+
)
|