graphite-code 0.3.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.
Files changed (112) hide show
  1. graphite/__init__.py +41 -0
  2. graphite/__main__.py +7 -0
  3. graphite/_cleanup_worker.py +525 -0
  4. graphite/activation.py +164 -0
  5. graphite/agent_hooks.py +577 -0
  6. graphite/agent_settings.py +226 -0
  7. graphite/analyze.py +146 -0
  8. graphite/answer_contract.py +420 -0
  9. graphite/bootstrap.py +210 -0
  10. graphite/buildlock.py +99 -0
  11. graphite/cache.py +131 -0
  12. graphite/channel.py +1325 -0
  13. graphite/cli.py +3053 -0
  14. graphite/cluster.py +111 -0
  15. graphite/config.py +209 -0
  16. graphite/context.py +355 -0
  17. graphite/daemon.py +745 -0
  18. graphite/daemon_health.py +733 -0
  19. graphite/debt.py +118 -0
  20. graphite/dependency_install.py +1597 -0
  21. graphite/detach.py +33 -0
  22. graphite/doctor.py +678 -0
  23. graphite/doctor_probes.py +2100 -0
  24. graphite/engine_identity.py +238 -0
  25. graphite/export/__init__.py +6 -0
  26. graphite/export/html.py +244 -0
  27. graphite/export/json.py +39 -0
  28. graphite/export/md.py +68 -0
  29. graphite/extract/__init__.py +4 -0
  30. graphite/extract/ast.py +1964 -0
  31. graphite/freshness.py +127 -0
  32. graphite/git.py +406 -0
  33. graphite/graph.py +117 -0
  34. graphite/graph_io.py +188 -0
  35. graphite/health.py +147 -0
  36. graphite/hook_entry.py +68 -0
  37. graphite/hookinstall.py +224 -0
  38. graphite/hookshim.py +86 -0
  39. graphite/incident_ledger.py +247 -0
  40. graphite/ingest.py +279 -0
  41. graphite/init.py +791 -0
  42. graphite/io.py +32 -0
  43. graphite/listing.py +51 -0
  44. graphite/llm.py +518 -0
  45. graphite/llm_probe.py +157 -0
  46. graphite/mcp.py +7 -0
  47. graphite/mcp_server.py +450 -0
  48. graphite/natural_query.py +252 -0
  49. graphite/overlays.py +713 -0
  50. graphite/probe_process.py +879 -0
  51. graphite/probe_workspace.py +728 -0
  52. graphite/process_contracts.py +22 -0
  53. graphite/provider_observer.py +397 -0
  54. graphite/query.py +646 -0
  55. graphite/query_plan.py +97 -0
  56. graphite/replacement_audit.py +291 -0
  57. graphite/resolve.py +660 -0
  58. graphite/review.py +782 -0
  59. graphite/routing/__init__.py +5 -0
  60. graphite/routing/approval.py +362 -0
  61. graphite/routing/classifier.py +169 -0
  62. graphite/routing/claude_executor.py +419 -0
  63. graphite/routing/claude_probe.py +102 -0
  64. graphite/routing/cli_identity.py +84 -0
  65. graphite/routing/codex_executor.py +383 -0
  66. graphite/routing/codex_probe.py +93 -0
  67. graphite/routing/context_builder.py +327 -0
  68. graphite/routing/contracts.py +802 -0
  69. graphite/routing/diff_policy.py +468 -0
  70. graphite/routing/edit_apply.py +166 -0
  71. graphite/routing/effort.py +43 -0
  72. graphite/routing/lifecycle.py +771 -0
  73. graphite/routing/lifecycle_operator.py +227 -0
  74. graphite/routing/lifecycle_service.py +555 -0
  75. graphite/routing/lifecycle_storage.py +977 -0
  76. graphite/routing/ollama_executor.py +341 -0
  77. graphite/routing/ollama_probe.py +72 -0
  78. graphite/routing/openrouter_executor.py +338 -0
  79. graphite/routing/openrouter_probe.py +188 -0
  80. graphite/routing/policy.py +815 -0
  81. graphite/routing/probe_runner.py +543 -0
  82. graphite/routing/process_runner.py +523 -0
  83. graphite/routing/profiles.py +554 -0
  84. graphite/routing/prompt.py +58 -0
  85. graphite/routing/registry.py +444 -0
  86. graphite/routing/route_pool.py +629 -0
  87. graphite/routing/route_pool_execution.py +275 -0
  88. graphite/routing/schema_validation.py +169 -0
  89. graphite/routing/service.py +1263 -0
  90. graphite/routing/settings.py +99 -0
  91. graphite/routing/shadow.py +201 -0
  92. graphite/routing/storage.py +4001 -0
  93. graphite/routing/telemetry.py +346 -0
  94. graphite/routing/worktree.py +259 -0
  95. graphite/routing/zai_edit.py +113 -0
  96. graphite/routing/zai_executor.py +191 -0
  97. graphite/routing/zai_probe.py +126 -0
  98. graphite/savings.py +84 -0
  99. graphite/ts_bridge.py +142 -0
  100. graphite/ts_resolver.mjs +314 -0
  101. graphite/typescript_activation.py +1586 -0
  102. graphite/usage_ledger.py +156 -0
  103. graphite/validation.py +148 -0
  104. graphite/watch.py +167 -0
  105. graphite/windows_job.py +368 -0
  106. graphite/windows_startup.py +144 -0
  107. graphite/windows_task.py +212 -0
  108. graphite_code-0.3.0.dist-info/METADATA +743 -0
  109. graphite_code-0.3.0.dist-info/RECORD +112 -0
  110. graphite_code-0.3.0.dist-info/WHEEL +4 -0
  111. graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
  112. graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1597 @@
1
+ """Fail-closed primitives for consent-gated local dependency installation.
2
+
3
+ This module deliberately contains no activation policy or user interaction. It
4
+ only prepares and validates immutable commands, files, environments, and bounded
5
+ process results for the higher-level activation service.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import math
12
+ import os
13
+ import re
14
+ import stat
15
+ from collections import deque
16
+ from collections.abc import Callable, Iterable, Mapping
17
+ from dataclasses import dataclass
18
+ from enum import StrEnum
19
+ from pathlib import Path
20
+ from typing import Any
21
+ from urllib.parse import urlsplit
22
+
23
+ from .probe_process import ProbeProcessError, ProbeProcessResult, run_bounded_process
24
+
25
+ TRUSTED_REGISTRY = "https://registry.npmjs.org/"
26
+ INSTALL_OUTPUT_LIMIT = 64 * 1024
27
+ #: Budget for `<manager> --version`. Sized for the NOISE, not for the answer:
28
+ #: `run_bounded_process` applies its limit per stream, so whatever the child
29
+ #: writes to stderr competes with the same number, and overflow is reported as
30
+ #: an unavailable manager. A version string needs tens of bytes; npm notices,
31
+ #: Node deprecation warnings and interpreter warnings need hundreds to a few
32
+ #: thousand, and `_minimal_node_environment` silences none of them. Still a
33
+ #: hard flood bound -- three orders of magnitude under the install budget.
34
+ MANAGER_VERSION_OUTPUT_LIMIT = 8 * 1024
35
+ MAX_CONTROL_FILE_BYTES = 8 * 1024 * 1024
36
+ MAX_TRUSTED_FILE_BYTES = 256 * 1024 * 1024
37
+ MAX_TRUSTED_LAUNCHER_LINKS = 8
38
+ MAX_TRUSTED_LINK_TARGET_BYTES = 4096
39
+ MAX_TRUSTED_ROUTE_COMPONENTS = 256
40
+ MAX_TRUSTED_EXECUTABLE_PREFIX_BYTES = 256
41
+ ACTIVATION_MAX_FILES = 100_000
42
+
43
+ _VERSION_RE = re.compile(
44
+ r"v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
45
+ r"(?:-(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[A-Za-z-][0-9A-Za-z-]*))*)?"
46
+ r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
47
+ )
48
+ _LOCALE_ENVIRONMENT = ("LANG", "LC_ALL")
49
+ _DEPENDENCY_FIELDS = ("dependencies", "devDependencies", "optionalDependencies", "peerDependencies")
50
+ _FORBIDDEN_MANIFEST_FIELDS = frozenset(
51
+ {"workspaces", "resolutions", "overrides", "pnpm", "installConfig", "publishConfig"}
52
+ )
53
+ _URI_RE = re.compile(r"([A-Za-z][A-Za-z0-9+.-]*)://[^\s\"'<>}\]]+")
54
+ _LOCKFILE_LINE_LIMIT = 64 * 1024
55
+ _MAPPING_LINE_RE = re.compile(
56
+ r"^(?:\"[^\"]+\"|'(?:[^']|'')+'|[A-Za-z0-9_./@+*^~<>=,!|()-]+):(?:\s+(.+)|\s*)$"
57
+ )
58
+ _SOURCE_FIELD_KEYS = frozenset({"resolved", "tarball", "fetch", "source", "path", "url"})
59
+ _LOCAL_TYPESCRIPT_SCRIPT = (
60
+ "const p=require.resolve('typescript/package.json',{paths:[process.cwd()]});"
61
+ "process.stdout.write(JSON.stringify({resolved:p}));"
62
+ )
63
+
64
+
65
+ class Manager(StrEnum):
66
+ NPM = "npm"
67
+ PNPM = "pnpm"
68
+ YARN = "yarn"
69
+ BUN = "bun"
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class Version:
74
+ major: int
75
+ minor: int
76
+ patch: int
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class ManagerAdapter:
81
+ manager: Manager
82
+ lockfiles: tuple[str, ...]
83
+ supported_majors: frozenset[int]
84
+ command_builder: Callable[[str], tuple[str, ...]]
85
+ unsafe_root_files: tuple[str, ...]
86
+ automatic: bool = True
87
+
88
+ def supports(self, version: Version | None) -> bool:
89
+ return self.automatic and version is not None and version.major in self.supported_majors
90
+
91
+ def argument_tail(self, registry: str) -> tuple[str, ...]:
92
+ return self.command_builder(registry)
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class TrustedPathComponent:
97
+ path: Path
98
+ identity: tuple[int, int]
99
+ mode: int
100
+ owner: int
101
+ link_target: str | None
102
+ link_count: int | None
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class TrustedFile:
107
+ path: Path
108
+ identity: tuple[int, int]
109
+ size: int
110
+ mtime_ns: int
111
+ ctime_ns: int | None
112
+ sha256: str
113
+ launcher_path: Path | None = None
114
+ launcher_route: tuple[TrustedPathComponent, ...] = ()
115
+ #: The launcher name is trusted by the TARGET's identity and content rather
116
+ #: than by path trust over every route component, so it carries no route.
117
+ #: Legitimate only for the interpreter this process is already running --
118
+ #: see `_trusted_identity_launcher`. Anything else must earn a full route.
119
+ launcher_identity_only: bool = False
120
+ prefix: bytes = b""
121
+
122
+ def __post_init__(self) -> None:
123
+ if self.launcher_identity_only:
124
+ # A route would be meaningless here, and its absence is the marker
125
+ # revalidation reads, so an identity launcher must carry a name and
126
+ # no route -- not merely "one or the other".
127
+ paired = self.launcher_path is not None and not self.launcher_route
128
+ else:
129
+ paired = (self.launcher_path is None) == (not self.launcher_route)
130
+ if (
131
+ not paired
132
+ or not isinstance(self.prefix, bytes)
133
+ or len(self.prefix) > MAX_TRUSTED_EXECUTABLE_PREFIX_BYTES
134
+ ):
135
+ raise ValueError("trusted_file_launcher_invalid")
136
+
137
+ @property
138
+ def command_path(self) -> Path:
139
+ return self.launcher_path if self.launcher_path is not None else self.path
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class FileSnapshot:
144
+ relative_path: str
145
+ identity: tuple[int, int]
146
+ sha256: str
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class StepResult:
151
+ ok: bool
152
+ reason: str
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class VersionResult:
157
+ ok: bool
158
+ reason: str
159
+ version: Version | None = None
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class TrustedCommand:
164
+ """An argv prefix and every external file on which that prefix depends.
165
+
166
+ The first reference is always the OS executable. Remaining references are
167
+ pinned files consumed by it, including a POSIX manager's lexical script
168
+ route or the Windows npm CLI.
169
+ """
170
+
171
+ argv: tuple[str, ...]
172
+ references: tuple[TrustedFile, ...]
173
+
174
+ def __post_init__(self) -> None:
175
+ reference_arguments = tuple(str(reference.command_path) for reference in self.references)
176
+ if (
177
+ not self.argv
178
+ or not self.references
179
+ or self.argv != reference_arguments
180
+ or (os.name == "nt" and self.references[0].path.suffix.lower() not in {".exe", ".com"})
181
+ ):
182
+ raise ValueError("trusted_command_invalid")
183
+
184
+
185
+ Runner = Callable[..., ProbeProcessResult]
186
+
187
+
188
+ def _npm_tail(registry: str) -> tuple[str, ...]:
189
+ return (
190
+ "install",
191
+ "--save-dev",
192
+ "--ignore-scripts",
193
+ "--no-audit",
194
+ "--no-fund",
195
+ f"--registry={registry}",
196
+ "typescript",
197
+ )
198
+
199
+
200
+ def _pnpm_tail(registry: str) -> tuple[str, ...]:
201
+ return (
202
+ "add",
203
+ "--save-dev",
204
+ "--ignore-scripts",
205
+ "--ignore-workspace-root-check",
206
+ f"--registry={registry}",
207
+ "typescript",
208
+ )
209
+
210
+
211
+ def _yarn_tail(_registry: str) -> tuple[str, ...]:
212
+ return ("add", "--dev", "--mode=skip-build", "typescript")
213
+
214
+
215
+ def _bun_tail(registry: str) -> tuple[str, ...]:
216
+ return ("add", "--dev", "--ignore-scripts", "--registry", registry, "typescript")
217
+
218
+
219
+ _ADAPTERS = {
220
+ Manager.NPM: ManagerAdapter(
221
+ Manager.NPM,
222
+ ("package-lock.json",),
223
+ frozenset(range(8, 12)),
224
+ _npm_tail,
225
+ (".npmrc", "npm-shrinkwrap.json"),
226
+ ),
227
+ Manager.PNPM: ManagerAdapter(
228
+ Manager.PNPM,
229
+ ("pnpm-lock.yaml",),
230
+ frozenset({11}),
231
+ _pnpm_tail,
232
+ (".npmrc", "pnpm-workspace.yaml", ".pnpmfile.cjs", ".pnpmfile.mjs"),
233
+ ),
234
+ Manager.YARN: ManagerAdapter(
235
+ Manager.YARN,
236
+ ("yarn.lock",),
237
+ frozenset(),
238
+ _yarn_tail,
239
+ (".yarnrc.yml", ".yarnrc", ".yarn/plugins"),
240
+ automatic=False,
241
+ ),
242
+ Manager.BUN: ManagerAdapter(
243
+ Manager.BUN, ("bun.lock", "bun.lockb"), frozenset({1}), _bun_tail, (".npmrc", "bunfig.toml")
244
+ ),
245
+ }
246
+
247
+
248
+ def adapter_for(manager: Manager) -> ManagerAdapter:
249
+ return _ADAPTERS[Manager(manager)]
250
+
251
+
252
+ def parse_version(value: str | bytes) -> Version | None:
253
+ try:
254
+ text = value.decode("ascii") if isinstance(value, bytes) else value
255
+ except (UnicodeDecodeError, AttributeError):
256
+ return None
257
+ match = _VERSION_RE.fullmatch(text.strip())
258
+ if match is None:
259
+ return None
260
+ return Version(*(int(part) for part in match.groups()[:3]))
261
+
262
+
263
+ def _is_reparse(stat_result: os.stat_result) -> bool:
264
+ attributes = getattr(stat_result, "st_file_attributes", 0)
265
+ reparse_flag = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
266
+ return bool(attributes & reparse_flag)
267
+
268
+
269
+ def _is_under(path: Path, root: Path) -> bool:
270
+ try:
271
+ path.relative_to(root)
272
+ except ValueError:
273
+ return False
274
+ return True
275
+
276
+
277
+ def _path_has_symlink(path: Path) -> bool:
278
+ current = Path(path.anchor)
279
+ try:
280
+ for part in path.parts[1:]:
281
+ current /= part
282
+ details = current.lstat()
283
+ if stat.S_ISLNK(details.st_mode) or _is_reparse(details):
284
+ return True
285
+ except OSError:
286
+ return True
287
+ return False
288
+
289
+
290
+ def _trusted_posix_owner(owner: int) -> bool:
291
+ try:
292
+ effective_uid = os.geteuid()
293
+ except AttributeError:
294
+ return False
295
+ return owner in {0, effective_uid}
296
+
297
+
298
+ def _capture_posix_component(path: Path) -> TrustedPathComponent | None:
299
+ try:
300
+ lexical = Path(os.path.abspath(path))
301
+ before = lexical.lstat()
302
+ if _is_reparse(before):
303
+ return None
304
+ link_target = None
305
+ if stat.S_ISLNK(before.st_mode):
306
+ if before.st_size > MAX_TRUSTED_LINK_TARGET_BYTES:
307
+ return None
308
+ link_target = os.readlink(lexical)
309
+ if len(os.fsencode(link_target)) > MAX_TRUSTED_LINK_TARGET_BYTES:
310
+ return None
311
+ after = lexical.lstat()
312
+ except (OSError, RuntimeError, ValueError):
313
+ return None
314
+ stable_fields = (
315
+ "st_dev",
316
+ "st_ino",
317
+ "st_mode",
318
+ "st_size",
319
+ "st_mtime_ns",
320
+ "st_ctime_ns",
321
+ "st_nlink",
322
+ "st_uid",
323
+ )
324
+ if any(getattr(before, field, None) != getattr(after, field, None) for field in stable_fields):
325
+ return None
326
+ return TrustedPathComponent(
327
+ lexical,
328
+ (before.st_dev, before.st_ino),
329
+ before.st_mode,
330
+ before.st_uid,
331
+ link_target,
332
+ None if stat.S_ISDIR(before.st_mode) else before.st_nlink,
333
+ )
334
+
335
+
336
+ def _trusted_posix_component(
337
+ component: TrustedPathComponent, *, directory: bool
338
+ ) -> bool:
339
+ if not _trusted_posix_owner(component.owner):
340
+ return False
341
+ if component.link_target is not None:
342
+ return component.link_count == 1
343
+ if directory:
344
+ if not stat.S_ISDIR(component.mode):
345
+ return False
346
+ writable = bool(component.mode & (stat.S_IWGRP | stat.S_IWOTH))
347
+ return not writable or bool(component.mode & stat.S_ISVTX)
348
+ return (
349
+ stat.S_ISREG(component.mode)
350
+ and component.link_count == 1
351
+ and not component.mode & (stat.S_IWGRP | stat.S_IWOTH)
352
+ )
353
+
354
+
355
+ def _trusted_posix_route(
356
+ path: Path, root: Path
357
+ ) -> tuple[Path, tuple[TrustedPathComponent, ...]] | None:
358
+ try:
359
+ lexical = Path(os.path.abspath(path))
360
+ root_path = root.resolve(strict=True)
361
+ except (OSError, RuntimeError):
362
+ return None
363
+ if not lexical.is_absolute() or _is_under(lexical, root_path):
364
+ return None
365
+ anchor = Path(lexical.anchor)
366
+ anchor_component = _capture_posix_component(anchor)
367
+ if anchor_component is None or not _trusted_posix_component(
368
+ anchor_component, directory=True
369
+ ):
370
+ return None
371
+ route = [anchor_component]
372
+ pending = deque(lexical.parts[1:])
373
+ current = anchor
374
+ seen_links: set[tuple[int, int]] = set()
375
+ followed_links = 0
376
+ while pending:
377
+ part = pending.popleft()
378
+ if part in {"", "."}:
379
+ continue
380
+ if part == "..":
381
+ current = current.parent
382
+ continue
383
+ candidate = current / part
384
+ if _is_under(candidate, root_path):
385
+ return None
386
+ component = _capture_posix_component(candidate)
387
+ if component is None or len(route) >= MAX_TRUSTED_ROUTE_COMPONENTS:
388
+ return None
389
+ route.append(component)
390
+ if component.link_target is None:
391
+ if not _trusted_posix_component(component, directory=bool(pending)):
392
+ return None
393
+ current = candidate
394
+ continue
395
+ followed_links += 1
396
+ if (
397
+ followed_links > MAX_TRUSTED_LAUNCHER_LINKS
398
+ or component.identity in seen_links
399
+ or not _trusted_posix_component(component, directory=False)
400
+ ):
401
+ return None
402
+ seen_links.add(component.identity)
403
+ target = Path(component.link_target)
404
+ remaining = tuple(pending)
405
+ if target.is_absolute():
406
+ target_anchor = Path(target.anchor)
407
+ target_anchor_component = _capture_posix_component(target_anchor)
408
+ if (
409
+ target_anchor_component is None
410
+ or len(route) >= MAX_TRUSTED_ROUTE_COMPONENTS
411
+ or not _trusted_posix_component(target_anchor_component, directory=True)
412
+ ):
413
+ return None
414
+ route.append(target_anchor_component)
415
+ current = target_anchor
416
+ target_parts = target.parts[1:]
417
+ else:
418
+ current = candidate.parent
419
+ target_parts = target.parts
420
+ pending = deque((*target_parts, *remaining))
421
+ if _is_under(current, root_path):
422
+ return None
423
+ return current, tuple(route)
424
+
425
+
426
+ def _trusted_posix_launcher(path: Path, root: Path) -> TrustedFile | None:
427
+ try:
428
+ lexical = Path(os.path.abspath(path))
429
+ root_path = root.resolve(strict=True)
430
+ except (OSError, RuntimeError):
431
+ return None
432
+ resolved_route = _trusted_posix_route(lexical, root_path)
433
+ if resolved_route is None:
434
+ return None
435
+ current, route = resolved_route
436
+ target_reference = _trusted_file(current, root_path, executable=True)
437
+ if target_reference is None:
438
+ return None
439
+ final_component = route[-1]
440
+ if (
441
+ target_reference.path != current
442
+ or target_reference.identity != final_component.identity
443
+ ):
444
+ return None
445
+ return TrustedFile(
446
+ path=target_reference.path,
447
+ identity=target_reference.identity,
448
+ size=target_reference.size,
449
+ mtime_ns=target_reference.mtime_ns,
450
+ ctime_ns=target_reference.ctime_ns,
451
+ sha256=target_reference.sha256,
452
+ launcher_path=lexical,
453
+ launcher_route=route,
454
+ prefix=target_reference.prefix,
455
+ )
456
+
457
+
458
+ def _trusted_identity_launcher(path: Path, root: Path) -> TrustedFile | None:
459
+ """Trust a launcher by its target's identity and content, not by path trust.
460
+
461
+ `_trusted_posix_launcher` requires every component of the route to be
462
+ unwritable by anyone but us, which defends against a hop being swapped
463
+ between validation and exec. That rule is right for a file we are choosing
464
+ to execute. It is the wrong question for the interpreter **we are already
465
+ running**: whoever could substitute `sys.executable` did so before this
466
+ process existed, so the substitution is already decided, and refusing it
467
+ only pushes the caller onto a fallback that launches the *resolved* path --
468
+ a different `sys.prefix`, without the venv's `site-packages`. That is a
469
+ hazard which can still happen, traded for one which cannot.
470
+
471
+ Measured on a GitHub hosted runner: `/opt` through `bin/python3.12` are all
472
+ mode 0777, so all seven components are refused while `_trusted_file` accepts
473
+ the very same binary. This closes that gap deliberately rather than by
474
+ accident.
475
+
476
+ What is NOT relaxed: the target is still pinned by `_trusted_file` (identity,
477
+ size, times, full digest, `O_NOFOLLOW`, `nlink == 1`), and a launcher inside
478
+ the selected repository is still refused, so this cannot widen what a
479
+ repository can aim us at.
480
+ """
481
+ if os.name == "nt":
482
+ return None
483
+ try:
484
+ lexical = Path(os.path.abspath(path))
485
+ root_path = root.resolve(strict=True)
486
+ target = lexical.resolve(strict=True)
487
+ except (OSError, RuntimeError):
488
+ return None
489
+ if not lexical.is_absolute() or _is_under(lexical, root_path):
490
+ return None
491
+ reference = _trusted_file(target, root_path, executable=True)
492
+ if reference is None or reference.path != target:
493
+ return None
494
+ return TrustedFile(
495
+ path=reference.path,
496
+ identity=reference.identity,
497
+ size=reference.size,
498
+ mtime_ns=reference.mtime_ns,
499
+ ctime_ns=reference.ctime_ns,
500
+ sha256=reference.sha256,
501
+ launcher_path=lexical,
502
+ launcher_identity_only=True,
503
+ prefix=reference.prefix,
504
+ )
505
+
506
+
507
+ def _trusted_file(path: Path, root: Path, *, executable: bool) -> TrustedFile | None:
508
+ if not path.is_absolute():
509
+ return None
510
+ try:
511
+ lexical = path.absolute()
512
+ if _path_has_symlink(lexical):
513
+ return None
514
+ canonical = lexical.resolve(strict=True)
515
+ root_path = root.resolve(strict=True)
516
+ initial = canonical.lstat()
517
+ except (OSError, RuntimeError):
518
+ return None
519
+ if (
520
+ _is_under(canonical, root_path)
521
+ or not stat.S_ISREG(initial.st_mode)
522
+ or _is_reparse(initial)
523
+ or initial.st_size > MAX_TRUSTED_FILE_BYTES
524
+ ):
525
+ return None
526
+ if executable:
527
+ if os.name == "nt" and canonical.suffix.lower() not in {".exe", ".com"}:
528
+ return None
529
+ if os.name != "nt" and not os.access(canonical, os.X_OK):
530
+ return None
531
+ descriptor = -1
532
+ try:
533
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
534
+ descriptor = os.open(canonical, flags)
535
+ before = os.fstat(descriptor)
536
+ if (
537
+ not stat.S_ISREG(before.st_mode)
538
+ or getattr(before, "st_nlink", 1) != 1
539
+ or before.st_size > MAX_TRUSTED_FILE_BYTES
540
+ ):
541
+ return None
542
+ digest = hashlib.sha256()
543
+ prefix = bytearray()
544
+ total_read = 0
545
+ while chunk := os.read(descriptor, 64 * 1024):
546
+ total_read += len(chunk)
547
+ if total_read > MAX_TRUSTED_FILE_BYTES:
548
+ return None
549
+ if len(prefix) < MAX_TRUSTED_EXECUTABLE_PREFIX_BYTES:
550
+ remaining = MAX_TRUSTED_EXECUTABLE_PREFIX_BYTES - len(prefix)
551
+ prefix.extend(chunk[:remaining])
552
+ digest.update(chunk)
553
+ after = os.fstat(descriptor)
554
+ except OSError:
555
+ return None
556
+ finally:
557
+ if descriptor >= 0:
558
+ os.close(descriptor)
559
+ stable_fields = (
560
+ "st_dev",
561
+ "st_ino",
562
+ "st_size",
563
+ "st_mtime_ns",
564
+ "st_nlink",
565
+ )
566
+ if os.name != "nt":
567
+ stable_fields += ("st_ctime_ns",)
568
+ if total_read != before.st_size or any(
569
+ getattr(initial, field, 1) != getattr(before, field, 1) for field in stable_fields
570
+ ) or any(
571
+ getattr(before, field, 1) != getattr(after, field, 1) for field in stable_fields
572
+ ):
573
+ return None
574
+ return TrustedFile(
575
+ path=canonical,
576
+ identity=(before.st_dev, before.st_ino),
577
+ size=before.st_size,
578
+ mtime_ns=before.st_mtime_ns,
579
+ ctime_ns=None if os.name == "nt" else before.st_ctime_ns,
580
+ sha256=digest.hexdigest(),
581
+ prefix=bytes(prefix),
582
+ )
583
+
584
+
585
+ def revalidate_trusted_file(reference: TrustedFile, root: Path, executable: bool) -> bool:
586
+ if reference.launcher_path is not None:
587
+ if os.name == "nt" or not executable:
588
+ return False
589
+ # Revalidate the way the reference was BUILT. Re-running the route check
590
+ # over an identity-anchored launcher would refuse it every time, turning
591
+ # every cleanup on a world-writable toolchain into `cleanup_failed`.
592
+ if reference.launcher_identity_only:
593
+ current = _trusted_identity_launcher(reference.launcher_path, root)
594
+ else:
595
+ current = _trusted_posix_launcher(reference.launcher_path, root)
596
+ else:
597
+ current = _trusted_file(reference.path, root, executable=executable)
598
+ return current == reference
599
+
600
+
601
+ def resolve_trusted_file(
602
+ path: Path, root: Path, *, executable: bool, follow_launcher: bool = False
603
+ ) -> TrustedFile | None:
604
+ """Resolve one caller-selected external file into an immutable identity reference.
605
+
606
+ ``follow_launcher`` admits a POSIX symlink route to the file: trust is still
607
+ anchored in the resolved target, but the supplied name is recorded as
608
+ ``launcher_path`` so the caller can launch what it was given rather than
609
+ what the name points at (see ``TrustedFile.command_path``). Plain resolution
610
+ cannot express that -- it rejects any path crossing a symlink outright.
611
+
612
+ The flag is deliberately ignored on Windows and for non-executables, because
613
+ ``revalidate_trusted_file`` refuses a launcher reference in both cases. A
614
+ launcher reference produced there would pass resolution and then fail closed
615
+ on its first revalidation, with nothing naming the cause.
616
+ """
617
+ if follow_launcher and executable and os.name != "nt":
618
+ return _trusted_posix_launcher(path, root)
619
+ return _trusted_file(path, root, executable=executable)
620
+
621
+
622
+ def _path_entries(path_source: str | Iterable[str | Path]) -> tuple[Path, ...]:
623
+ raw_entries = path_source.split(os.pathsep) if isinstance(path_source, str) else path_source
624
+ entries: list[Path] = []
625
+ for raw in raw_entries:
626
+ entry = Path(raw)
627
+ if not entry.is_absolute():
628
+ continue
629
+ try:
630
+ if _path_has_symlink(entry.absolute()):
631
+ continue
632
+ canonical = entry.resolve(strict=True)
633
+ except (OSError, RuntimeError):
634
+ continue
635
+ if canonical.is_dir():
636
+ entries.append(canonical)
637
+ return tuple(entries)
638
+
639
+
640
+ def _posix_manager_path_entries(
641
+ path_source: str | Iterable[str | Path],
642
+ ) -> tuple[Path, ...]:
643
+ raw_entries = path_source.split(os.pathsep) if isinstance(path_source, str) else path_source
644
+ entries: list[Path] = []
645
+ for raw in raw_entries:
646
+ entry = Path(raw)
647
+ if entry.is_absolute():
648
+ entries.append(Path(os.path.abspath(entry)))
649
+ return tuple(entries)
650
+
651
+
652
+ def resolve_trusted_executable(
653
+ name: str,
654
+ root: Path,
655
+ path_source: str | Iterable[str | Path],
656
+ *,
657
+ windows: bool | None = None,
658
+ ) -> TrustedFile | None:
659
+ use_windows_rules = os.name == "nt" if windows is None else windows
660
+ supplied = Path(name)
661
+ if supplied.name != name or supplied.is_absolute():
662
+ return None
663
+ if use_windows_rules:
664
+ suffix = supplied.suffix.lower()
665
+ names = (name,) if suffix in {".exe", ".com"} else (f"{name}.exe", f"{name}.com")
666
+ else:
667
+ names = (name,)
668
+ posix_manager = (
669
+ os.name != "nt"
670
+ and not use_windows_rules
671
+ and name in {manager.value for manager in Manager}
672
+ )
673
+ directories = (
674
+ _posix_manager_path_entries(path_source)
675
+ if posix_manager
676
+ else _path_entries(path_source)
677
+ )
678
+ for directory in directories:
679
+ for candidate_name in names:
680
+ candidate = directory / candidate_name
681
+ if posix_manager:
682
+ reference = _trusted_posix_launcher(candidate, root)
683
+ else:
684
+ reference = _trusted_file(candidate, root, executable=not use_windows_rules)
685
+ if reference is not None:
686
+ return reference
687
+ return None
688
+
689
+
690
+ def resolve_windows_npm_prefix(
691
+ root: Path, path_source: str | Iterable[str | Path]
692
+ ) -> TrustedCommand | None:
693
+ for directory in _path_entries(path_source):
694
+ node = _trusted_file(directory / "node.exe", root, executable=True)
695
+ cli = _trusted_file(directory / "node_modules" / "npm" / "bin" / "npm-cli.js", root, executable=False)
696
+ if node is not None and cli is not None:
697
+ return TrustedCommand((str(node.path), str(cli.path)), (node, cli))
698
+ return None
699
+
700
+
701
+ _NODE_SHEBANGS = frozenset(
702
+ {
703
+ b"#!/usr/bin/env node",
704
+ b"#!/usr/bin/node",
705
+ b"#!/bin/node",
706
+ }
707
+ )
708
+ _POSIX_NATIVE_MAGICS = (
709
+ b"\x7fELF",
710
+ b"\xfe\xed\xfa\xce",
711
+ b"\xce\xfa\xed\xfe",
712
+ b"\xfe\xed\xfa\xcf",
713
+ b"\xcf\xfa\xed\xfe",
714
+ b"\xca\xfe\xba\xbe",
715
+ b"\xbe\xba\xfe\xca",
716
+ b"\xca\xfe\xba\xbf",
717
+ b"\xbf\xba\xfe\xca",
718
+ )
719
+
720
+
721
+ def _posix_manager_execution_kind(reference: TrustedFile) -> str | None:
722
+ prefix = reference.prefix
723
+ if prefix.startswith(b"#!"):
724
+ newline = prefix.find(b"\n")
725
+ if newline < 0:
726
+ return None
727
+ shebang = prefix[:newline]
728
+ if shebang.endswith(b"\r"):
729
+ shebang = shebang[:-1]
730
+ return "node" if shebang in _NODE_SHEBANGS else None
731
+ return "native" if prefix.startswith(_POSIX_NATIVE_MAGICS) else None
732
+
733
+
734
+ def command_for(
735
+ reference: TrustedFile, node: TrustedFile | None = None
736
+ ) -> TrustedCommand | None:
737
+ if os.name != "nt" and reference.launcher_path is not None:
738
+ execution_kind = _posix_manager_execution_kind(reference)
739
+ if execution_kind == "node":
740
+ if node is None or node.launcher_path is not None:
741
+ return None
742
+ return TrustedCommand(
743
+ (str(node.path), str(reference.command_path)), (node, reference)
744
+ )
745
+ if execution_kind != "native":
746
+ return None
747
+ return TrustedCommand((str(reference.command_path),), (reference,))
748
+
749
+
750
+ def _trusted_windows_system_environment() -> dict[str, str]:
751
+ if os.name != "nt":
752
+ return {}
753
+ try:
754
+ import ctypes
755
+
756
+ buffer = ctypes.create_unicode_buffer(32768)
757
+ length = ctypes.windll.kernel32.GetWindowsDirectoryW(buffer, len(buffer))
758
+ if length <= 0 or length >= len(buffer):
759
+ raise OSError("windows_directory_unavailable")
760
+ windows = Path(buffer.value).resolve(strict=True)
761
+ system32 = (windows / "System32").resolve(strict=True)
762
+ command = (system32 / "cmd.exe").resolve(strict=True)
763
+ if not system32.is_dir() or not command.is_file():
764
+ raise OSError("windows_system_paths_invalid")
765
+ except (AttributeError, OSError, RuntimeError):
766
+ raise ValueError("windows_system_paths_unavailable") from None
767
+ return {
768
+ "SYSTEMROOT": str(windows),
769
+ "WINDIR": str(windows),
770
+ "COMSPEC": str(command),
771
+ "PATHEXT": ".COM;.EXE",
772
+ }
773
+
774
+
775
+ def build_install_environment(
776
+ manager: Manager,
777
+ isolated_home: Path,
778
+ executable_directories: Iterable[Path],
779
+ registry: str,
780
+ source: Mapping[str, str],
781
+ ) -> dict[str, str]:
782
+ base = isolated_home.resolve()
783
+ environment = {name: source[name] for name in _LOCALE_ENVIRONMENT if name in source}
784
+ trusted_directories: list[str] = []
785
+ for directory in executable_directories:
786
+ if not directory.is_absolute():
787
+ continue
788
+ try:
789
+ canonical = directory.resolve(strict=True)
790
+ except OSError:
791
+ continue
792
+ if canonical.is_dir() and str(canonical) not in trusted_directories:
793
+ trusted_directories.append(str(canonical))
794
+ if os.name == "nt":
795
+ trusted_windows = _trusted_windows_system_environment()
796
+ environment.update(trusted_windows)
797
+ trusted_directories.append(str(Path(trusted_windows["SYSTEMROOT"]) / "System32"))
798
+ else:
799
+ trusted_directories.extend(("/usr/bin", "/bin"))
800
+ environment["PATH"] = os.pathsep.join(trusted_directories)
801
+ environment.update(
802
+ {
803
+ "HOME": str(base / "home"),
804
+ "USERPROFILE": str(base / "home"),
805
+ "XDG_CONFIG_HOME": str(base / "config"),
806
+ "XDG_CACHE_HOME": str(base / "cache"),
807
+ "TEMP": str(base / "tmp"),
808
+ "TMP": str(base / "tmp"),
809
+ "APPDATA": str(base / "appdata"),
810
+ "LOCALAPPDATA": str(base / "localappdata"),
811
+ }
812
+ )
813
+ selected = Manager(manager)
814
+ if selected in {Manager.NPM, Manager.PNPM}:
815
+ environment.update(
816
+ {
817
+ "NPM_CONFIG_USERCONFIG": str(base / "npm-config" / "user.npmrc"),
818
+ "NPM_CONFIG_GLOBALCONFIG": str(base / "npm-config" / "global.npmrc"),
819
+ "NPM_CONFIG_CACHE": str(base / "npm-cache"),
820
+ "NPM_CONFIG_PREFIX": str(base / "npm-prefix"),
821
+ "NPM_CONFIG_REGISTRY": registry,
822
+ "NPM_CONFIG_IGNORE_SCRIPTS": "true",
823
+ "NPM_CONFIG_AUDIT": "false",
824
+ "NPM_CONFIG_FUND": "false",
825
+ "npm_config_registry": registry,
826
+ "npm_config_ignore_scripts": "true",
827
+ }
828
+ )
829
+ if selected is Manager.PNPM:
830
+ environment.update(
831
+ {
832
+ "PNPM_HOME": str(base / "pnpm-home"),
833
+ "PNPM_STORE_DIR": str(base / "pnpm-store"),
834
+ "PNPM_CONFIG_DIR": str(base / "pnpm-config"),
835
+ "npm_config_store_dir": str(base / "pnpm-store"),
836
+ }
837
+ )
838
+ elif selected is Manager.YARN:
839
+ environment.update(
840
+ {
841
+ "YARN_NPM_REGISTRY_SERVER": registry,
842
+ "YARN_ENABLE_SCRIPTS": "false",
843
+ "YARN_ENABLE_TELEMETRY": "0",
844
+ "YARN_GLOBAL_FOLDER": str(base / "yarn-global"),
845
+ }
846
+ )
847
+ else:
848
+ environment["BUN_INSTALL_CACHE_DIR"] = str(base / "bun-cache")
849
+ return environment
850
+
851
+
852
+ def _prepare_isolated_install_home(root: Path, isolated_home: Path, manager: Manager) -> bool:
853
+ if not isolated_home.is_absolute():
854
+ return False
855
+ try:
856
+ canonical_root = root.resolve(strict=True)
857
+ lexical_base = isolated_home.absolute()
858
+ if _is_under(lexical_base, canonical_root):
859
+ return False
860
+ parent = lexical_base.parent.resolve(strict=True)
861
+ if _path_has_symlink(parent) or _is_under(parent, canonical_root):
862
+ return False
863
+ if lexical_base.exists() or lexical_base.is_symlink():
864
+ details = lexical_base.lstat()
865
+ if stat.S_ISLNK(details.st_mode) or _is_reparse(details) or not stat.S_ISDIR(details.st_mode):
866
+ return False
867
+ else:
868
+ lexical_base.mkdir(mode=0o700)
869
+ canonical_base = lexical_base.resolve(strict=True)
870
+ if canonical_base != lexical_base or _is_under(canonical_base, canonical_root):
871
+ return False
872
+ directory_names = {
873
+ "home",
874
+ "config",
875
+ "cache",
876
+ "tmp",
877
+ "appdata",
878
+ "localappdata",
879
+ }
880
+ selected = Manager(manager)
881
+ if selected in {Manager.NPM, Manager.PNPM}:
882
+ directory_names.update({"npm-config", "npm-cache", "npm-prefix"})
883
+ if selected is Manager.PNPM:
884
+ directory_names.update({"pnpm-home", "pnpm-store", "pnpm-config"})
885
+ elif selected is Manager.YARN:
886
+ directory_names.add("yarn-global")
887
+ elif selected is Manager.BUN:
888
+ directory_names.add("bun-cache")
889
+ for name in directory_names:
890
+ directory = canonical_base / name
891
+ directory.mkdir(mode=0o700, exist_ok=True)
892
+ details = directory.lstat()
893
+ if (
894
+ directory.resolve(strict=True) != directory
895
+ or not _is_under(directory, canonical_base)
896
+ or stat.S_ISLNK(details.st_mode)
897
+ or _is_reparse(details)
898
+ or not stat.S_ISDIR(details.st_mode)
899
+ ):
900
+ return False
901
+ if selected in {Manager.NPM, Manager.PNPM}:
902
+ for name in ("user.npmrc", "global.npmrc"):
903
+ config = canonical_base / "npm-config" / name
904
+ if config.exists() or config.is_symlink():
905
+ existing = config.lstat()
906
+ if (
907
+ stat.S_ISLNK(existing.st_mode)
908
+ or _is_reparse(existing)
909
+ or not stat.S_ISREG(existing.st_mode)
910
+ or getattr(existing, "st_nlink", 1) != 1
911
+ ):
912
+ return False
913
+ flags = (
914
+ os.O_RDWR
915
+ | os.O_CREAT
916
+ | getattr(os, "O_BINARY", 0)
917
+ | getattr(os, "O_NOFOLLOW", 0)
918
+ )
919
+ descriptor = os.open(config, flags, 0o600)
920
+ try:
921
+ details = os.fstat(descriptor)
922
+ if not stat.S_ISREG(details.st_mode) or getattr(details, "st_nlink", 1) != 1:
923
+ return False
924
+ os.ftruncate(descriptor, 0)
925
+ finally:
926
+ os.close(descriptor)
927
+ return True
928
+ except (OSError, RuntimeError, ValueError):
929
+ return False
930
+
931
+
932
+ def snapshot_control_file(root: Path, relative_path: str) -> FileSnapshot:
933
+ if (
934
+ not relative_path
935
+ or relative_path in {".", ".."}
936
+ or "/" in relative_path
937
+ or "\\" in relative_path
938
+ or Path(relative_path).is_absolute()
939
+ ):
940
+ raise ValueError("control_file_invalid")
941
+ try:
942
+ canonical_root = root.resolve(strict=True)
943
+ path = canonical_root / relative_path
944
+ initial = path.lstat()
945
+ if stat.S_ISLNK(initial.st_mode) or _is_reparse(initial) or not stat.S_ISREG(initial.st_mode):
946
+ raise ValueError("control_file_invalid")
947
+ canonical = path.resolve(strict=True)
948
+ if not _is_under(canonical, canonical_root) or canonical != path:
949
+ raise ValueError("control_file_invalid")
950
+ if initial.st_size > MAX_CONTROL_FILE_BYTES:
951
+ raise ValueError("control_file_invalid")
952
+ flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
953
+ descriptor = os.open(path, flags)
954
+ try:
955
+ before = os.fstat(descriptor)
956
+ content = os.read(descriptor, MAX_CONTROL_FILE_BYTES + 1)
957
+ after = os.fstat(descriptor)
958
+ finally:
959
+ os.close(descriptor)
960
+ except ValueError:
961
+ raise
962
+ except (OSError, RuntimeError):
963
+ raise ValueError("control_file_invalid") from None
964
+ stable_fields = ("st_dev", "st_ino", "st_size", "st_mtime_ns")
965
+ if (
966
+ len(content) > MAX_CONTROL_FILE_BYTES
967
+ or len(content) != before.st_size
968
+ or any(getattr(initial, field) != getattr(before, field) for field in stable_fields)
969
+ or any(getattr(before, field) != getattr(after, field) for field in stable_fields)
970
+ ):
971
+ raise ValueError("control_file_changed")
972
+ return FileSnapshot(relative_path, (before.st_dev, before.st_ino), hashlib.sha256(content).hexdigest())
973
+
974
+
975
+ def _dependency_spec_is_safe(specification: str) -> bool:
976
+ value = specification.strip()
977
+ lower = value.lower()
978
+ if not value or value != specification:
979
+ return False
980
+ if lower.startswith(("file:", "link:", "git:", "git+", "ssh:", "http:", "https:", "workspace:")):
981
+ return False
982
+ if lower.endswith((".tgz", ".tar.gz")):
983
+ return False
984
+ if lower.startswith("git@") or any(separator in value for separator in ("/", "\\", ":")):
985
+ return False
986
+ if value.startswith("."):
987
+ return False
988
+ return True
989
+
990
+
991
+ def _is_canonical_registry_url(value: str) -> bool:
992
+ try:
993
+ parsed = urlsplit(value)
994
+ except ValueError:
995
+ return False
996
+ return (
997
+ parsed.scheme == "https"
998
+ and parsed.netloc == "registry.npmjs.org"
999
+ and parsed.path.startswith("/")
1000
+ and not parsed.query
1001
+ and not parsed.fragment
1002
+ and parsed.username is None
1003
+ and parsed.password is None
1004
+ and parsed.port is None
1005
+ )
1006
+
1007
+
1008
+ def _json_source_fields_are_trusted(value: Any) -> bool:
1009
+ if isinstance(value, list):
1010
+ return all(_json_source_fields_are_trusted(item) for item in value)
1011
+ if not isinstance(value, dict):
1012
+ return True
1013
+ for key, item in value.items():
1014
+ lowered = key.lower()
1015
+ if lowered in _SOURCE_FIELD_KEYS:
1016
+ if not isinstance(item, str) or not _is_canonical_registry_url(item):
1017
+ return False
1018
+ elif lowered == "resolution" and isinstance(item, dict):
1019
+ if any(nested.lower() not in {"integrity", "tarball"} for nested in item):
1020
+ return False
1021
+ if not _json_source_fields_are_trusted(item):
1022
+ return False
1023
+ return True
1024
+
1025
+
1026
+ def _text_source_fields_are_trusted(text: str) -> bool:
1027
+ for line in text.splitlines():
1028
+ content = line.strip()
1029
+ mapping = re.fullmatch(
1030
+ r"(?P<key>\"[^\"]*\"|'(?:[^']|'')*'|[A-Za-z][A-Za-z0-9_-]*):\s+(?P<value>.+)",
1031
+ content,
1032
+ )
1033
+ if mapping is not None:
1034
+ key = _normalize_yaml_mapping_key(mapping.group("key"))
1035
+ if key is None:
1036
+ return False
1037
+ if key.lower() not in _SOURCE_FIELD_KEYS:
1038
+ continue
1039
+ source = mapping.group("value").strip()
1040
+ else:
1041
+ classic = re.fullmatch(r"(?i)(resolved|tarball|fetch|source|path|url)\s+(.+)", content)
1042
+ if classic is None:
1043
+ continue
1044
+ source = classic.group(2).strip()
1045
+ if source.startswith(("'", '"')) and source.endswith(source[0]) and len(source) >= 2:
1046
+ source = source[1:-1]
1047
+ if not _is_canonical_registry_url(source):
1048
+ return False
1049
+ return True
1050
+
1051
+
1052
+ def _normalize_yaml_mapping_key(value: str) -> str | None:
1053
+ if value.startswith('"'):
1054
+ if len(value) < 2 or not value.endswith('"') or '"' in value[1:-1]:
1055
+ return None
1056
+ return value[1:-1]
1057
+ if value.startswith("'"):
1058
+ if len(value) < 2 or not value.endswith("'"):
1059
+ return None
1060
+ inner = value[1:-1]
1061
+ result: list[str] = []
1062
+ index = 0
1063
+ while index < len(inner):
1064
+ if inner[index] == "'":
1065
+ if index + 1 >= len(inner) or inner[index + 1] != "'":
1066
+ return None
1067
+ result.append("'")
1068
+ index += 2
1069
+ else:
1070
+ result.append(inner[index])
1071
+ index += 1
1072
+ return "".join(result)
1073
+ return value if re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]*", value) else None
1074
+
1075
+
1076
+ def _normalize_text_lockfile_escapes(text: str) -> str | None:
1077
+ """Decode a bounded subset of JSON escapes exactly once, rejecting ambiguity."""
1078
+ output: list[str] = []
1079
+ index = 0
1080
+ while index < len(text):
1081
+ character = text[index]
1082
+ if character != "\\":
1083
+ output.append(character)
1084
+ index += 1
1085
+ continue
1086
+ if index + 1 >= len(text):
1087
+ return None
1088
+ escape = text[index + 1]
1089
+ if escape in {"/", "\\"}:
1090
+ output.append(escape)
1091
+ index += 2
1092
+ continue
1093
+ if escape not in {"u", "U"} or index + 6 > len(text):
1094
+ return None
1095
+ digits = text[index + 2 : index + 6]
1096
+ if re.fullmatch(r"[0-9A-Fa-f]{4}", digits) is None:
1097
+ return None
1098
+ decoded = chr(int(digits, 16))
1099
+ if ord(decoded) < 32 or 0xD800 <= ord(decoded) <= 0xDFFF:
1100
+ return None
1101
+ output.append(decoded)
1102
+ index += 6
1103
+ normalized = "".join(output)
1104
+ if re.search(r"\\[uU][0-9A-Fa-f]{4}", normalized):
1105
+ return None
1106
+ return normalized
1107
+
1108
+
1109
+ def _line_has_balanced_structures(content: str) -> bool:
1110
+ quote: str | None = None
1111
+ brackets: list[str] = []
1112
+ pairs = {")": "(", "]": "[", "}": "{"}
1113
+ for character in content:
1114
+ if quote is not None:
1115
+ if character == quote:
1116
+ quote = None
1117
+ continue
1118
+ if character in {"'", '"'}:
1119
+ quote = character
1120
+ elif character in "([{":
1121
+ brackets.append(character)
1122
+ elif character in ")]}":
1123
+ if not brackets or brackets.pop() != pairs[character]:
1124
+ return False
1125
+ return quote is None and not brackets
1126
+
1127
+
1128
+ def _mapping_line(content: str) -> tuple[bool, bool]:
1129
+ match = _MAPPING_LINE_RE.fullmatch(content)
1130
+ if match is None:
1131
+ return False, False
1132
+ value = match.group(1)
1133
+ if value is not None and not _yaml_scalar_is_valid(value):
1134
+ return False, False
1135
+ return True, value is None
1136
+
1137
+
1138
+ def _yaml_scalar_remainder_is_valid(remainder: str) -> bool:
1139
+ return not remainder or (remainder[0].isspace() and remainder.lstrip().startswith("#"))
1140
+
1141
+
1142
+ def _reject_json_constant(_constant: str) -> None:
1143
+ raise ValueError("invalid_json_constant")
1144
+
1145
+
1146
+ def _reject_duplicate_json_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
1147
+ result: dict[str, Any] = {}
1148
+ for key, value in pairs:
1149
+ if key in result:
1150
+ raise ValueError("duplicate_json_key")
1151
+ result[key] = value
1152
+ return result
1153
+
1154
+
1155
+ def _parse_bounded_json_int(value: str) -> int:
1156
+ if len(value.lstrip("-")) > 1024:
1157
+ raise ValueError("json_integer_too_long")
1158
+ return int(value)
1159
+
1160
+
1161
+ def _parse_finite_json_float(value: str) -> float:
1162
+ parsed = float(value)
1163
+ if not math.isfinite(parsed):
1164
+ raise ValueError("json_float_not_finite")
1165
+ return parsed
1166
+
1167
+
1168
+ def _strict_json_loads(value: bytes | str) -> Any:
1169
+ return json.loads(
1170
+ value,
1171
+ parse_constant=_reject_json_constant,
1172
+ object_pairs_hook=_reject_duplicate_json_keys,
1173
+ parse_int=_parse_bounded_json_int,
1174
+ parse_float=_parse_finite_json_float,
1175
+ )
1176
+
1177
+
1178
+ def _yaml_scalar_is_valid(value: str) -> bool:
1179
+ if not value:
1180
+ return False
1181
+ if value[0] in {"'", '"'}:
1182
+ closing = value.find(value[0], 1)
1183
+ return closing >= 1 and _yaml_scalar_remainder_is_valid(value[closing + 1 :])
1184
+ if value[0] in "[{":
1185
+ pairs = {"]": "[", "}": "{"}
1186
+ stack = [value[0]]
1187
+ quote: str | None = None
1188
+ for index, character in enumerate(value[1:], start=1):
1189
+ if quote is not None:
1190
+ if character == quote:
1191
+ quote = None
1192
+ continue
1193
+ if character in {"'", '"'}:
1194
+ quote = character
1195
+ elif character in "[{":
1196
+ stack.append(character)
1197
+ elif character in "]}":
1198
+ if not stack or stack.pop() != pairs[character]:
1199
+ return False
1200
+ if not stack:
1201
+ flow_text = value[: index + 1]
1202
+ try:
1203
+ parsed_flow = _strict_json_loads(flow_text)
1204
+ except (ValueError, RecursionError):
1205
+ return False
1206
+ expected_type = dict if value[0] == "{" else list
1207
+ return (
1208
+ isinstance(parsed_flow, expected_type)
1209
+ and _json_source_fields_are_trusted(parsed_flow)
1210
+ and _yaml_scalar_remainder_is_valid(value[index + 1 :])
1211
+ )
1212
+ return False
1213
+ plain_value = value
1214
+ for index, character in enumerate(value):
1215
+ if character == "#" and (index == 0 or value[index - 1].isspace()):
1216
+ plain_value = value[:index].rstrip()
1217
+ break
1218
+ if (
1219
+ not plain_value
1220
+ or plain_value.endswith(":")
1221
+ or any(character in plain_value for character in "[]{}")
1222
+ ):
1223
+ return False
1224
+ return not any(
1225
+ character == ":" and index + 1 < len(plain_value) and plain_value[index + 1].isspace()
1226
+ for index, character in enumerate(plain_value)
1227
+ )
1228
+
1229
+
1230
+ def _validate_mapping_lockfile(lines: list[str], *, berry: bool) -> bool:
1231
+ previous_indent = 0
1232
+ previous_opens = False
1233
+ saw_significant = False
1234
+ saw_pnpm_section = False
1235
+ saw_metadata_version = False
1236
+ saw_package_entry = False
1237
+ saw_package_version = False
1238
+ for line in lines:
1239
+ if not line.strip() or line.lstrip().startswith("#"):
1240
+ continue
1241
+ indent = len(line) - len(line.lstrip(" "))
1242
+ content = line[indent:].rstrip()
1243
+ if indent % 2 or indent > 40 or not _line_has_balanced_structures(content):
1244
+ return False
1245
+ if saw_significant and indent > previous_indent:
1246
+ if indent != previous_indent + 2 or not previous_opens:
1247
+ return False
1248
+ valid, opens = _mapping_line(content)
1249
+ if not valid:
1250
+ return False
1251
+ if indent == 0 and content.split(":", 1)[0] in {"importers", "packages", "snapshots"}:
1252
+ saw_pnpm_section = True
1253
+ if berry:
1254
+ if indent == 2 and re.fullmatch(r"version:\s*\d+(?:\.\d+)*", content):
1255
+ if not saw_package_entry:
1256
+ saw_metadata_version = True
1257
+ else:
1258
+ saw_package_version = True
1259
+ if indent == 0 and content != "__metadata:":
1260
+ saw_package_entry = True
1261
+ previous_indent = indent
1262
+ previous_opens = opens
1263
+ saw_significant = True
1264
+ if berry:
1265
+ return saw_metadata_version and saw_package_entry and saw_package_version
1266
+ return saw_pnpm_section
1267
+
1268
+
1269
+ def _validate_yarn_classic(lines: list[str]) -> bool:
1270
+ previous_indent = 0
1271
+ previous_opens = False
1272
+ saw_significant = False
1273
+ saw_entry = False
1274
+ current_entry_has_version = False
1275
+ for line in lines[1:]:
1276
+ if not line.strip() or line.lstrip().startswith("#"):
1277
+ continue
1278
+ indent = len(line) - len(line.lstrip(" "))
1279
+ content = line[indent:].rstrip()
1280
+ if indent not in {0, 2, 4} or not _line_has_balanced_structures(content):
1281
+ return False
1282
+ if saw_significant and indent > previous_indent:
1283
+ if indent != previous_indent + 2 or not previous_opens:
1284
+ return False
1285
+ opens = False
1286
+ if indent == 0:
1287
+ if saw_entry and not current_entry_has_version:
1288
+ return False
1289
+ valid, opens = _mapping_line(content)
1290
+ if not valid or not opens:
1291
+ return False
1292
+ saw_entry = True
1293
+ current_entry_has_version = False
1294
+ elif indent == 2:
1295
+ block_match = re.fullmatch(r"(?:dependencies|optionalDependencies|peerDependencies):", content)
1296
+ field_match = re.fullmatch(r"(?:version|resolved|integrity|uid)\s+\S.*", content)
1297
+ if block_match is None and field_match is None:
1298
+ return False
1299
+ opens = block_match is not None
1300
+ if content.startswith("version "):
1301
+ current_entry_has_version = True
1302
+ elif re.fullmatch(r"(?:\"[^\"]+\"|'[^']+'|[^\s:]+)\s+\S.*", content) is None:
1303
+ return False
1304
+ previous_indent = indent
1305
+ previous_opens = opens
1306
+ saw_significant = True
1307
+ return saw_entry and current_entry_has_version
1308
+
1309
+
1310
+ def _normalized_lockfile_text(lockfile_bytes: bytes) -> str | None:
1311
+ """Return decoded text only for a conservatively recognized lockfile format."""
1312
+ try:
1313
+ text = lockfile_bytes.decode("utf-8")
1314
+ except UnicodeDecodeError:
1315
+ return None
1316
+ if any(ord(character) < 32 and character not in "\r\n" for character in text):
1317
+ return None
1318
+ text = text.replace("\r\n", "\n")
1319
+ if "\r" in text:
1320
+ return None
1321
+ stripped = text.lstrip("\ufeff \n")
1322
+ if not stripped:
1323
+ return None
1324
+ if stripped.startswith("{"):
1325
+ try:
1326
+ parsed = _strict_json_loads(stripped)
1327
+ except (UnicodeDecodeError, ValueError, RecursionError):
1328
+ return None
1329
+ if not isinstance(parsed, dict):
1330
+ return None
1331
+ if not _json_source_fields_are_trusted(parsed):
1332
+ return None
1333
+ lockfile_version = parsed.get("lockfileVersion")
1334
+ if isinstance(lockfile_version, bool) or not (
1335
+ isinstance(lockfile_version, int)
1336
+ or (isinstance(lockfile_version, str) and re.fullmatch(r"\d+(?:\.\d+)?", lockfile_version))
1337
+ ):
1338
+ return None
1339
+ return json.dumps(parsed, ensure_ascii=False, separators=(",", ":"))
1340
+ stripped = _normalize_text_lockfile_escapes(stripped)
1341
+ if stripped is None:
1342
+ return None
1343
+ lines = stripped.splitlines()
1344
+ if any(len(line) > _LOCKFILE_LINE_LIMIT for line in lines):
1345
+ return None
1346
+ if lines[0].startswith("lockfileVersion:"):
1347
+ if not re.fullmatch(r"lockfileVersion:\s*['\"]?\d+(?:\.\d+)?['\"]?\s*", lines[0]):
1348
+ return None
1349
+ return stripped if _validate_mapping_lockfile(lines, berry=False) else None
1350
+ if lines[0].strip() == "# yarn lockfile v1":
1351
+ return stripped if _validate_yarn_classic(lines) else None
1352
+ first_content_lines = [line.strip() for line in lines[:10] if line.strip() and not line.lstrip().startswith("#")]
1353
+ if first_content_lines and first_content_lines[0] == "__metadata:":
1354
+ return stripped if _validate_mapping_lockfile(lines, berry=True) else None
1355
+ return None
1356
+
1357
+
1358
+ def _lockfile_uses_trusted_sources(lockfile_bytes: bytes) -> bool:
1359
+ normalized = _normalized_lockfile_text(lockfile_bytes)
1360
+ if normalized is None:
1361
+ return False
1362
+ normalized = normalized.replace("\\/", "/")
1363
+ lower = normalized.lower()
1364
+ if not _text_source_fields_are_trusted(normalized):
1365
+ return False
1366
+ if any(
1367
+ marker in lower
1368
+ for marker in ("git+", "git://", "git@", "ssh:", "file:", "link:", "local:")
1369
+ ):
1370
+ return False
1371
+ if re.search(r"[\"'](?:\.\.?[/\\]|[/\\]|[A-Za-z]:[/\\])", normalized) or re.search(
1372
+ r"(?m)^\s*[A-Za-z][\w-]*:\s*(?:\.\.?[/\\]|[/\\]{1,2}|[A-Za-z]:[/\\])",
1373
+ normalized,
1374
+ ):
1375
+ return False
1376
+ uri_matches = tuple(_URI_RE.finditer(normalized))
1377
+ for match in uri_matches:
1378
+ try:
1379
+ parsed = urlsplit(match.group())
1380
+ except ValueError:
1381
+ return False
1382
+ if (
1383
+ match.group(1).lower() != "https"
1384
+ or parsed.scheme.lower() != "https"
1385
+ or parsed.hostname != "registry.npmjs.org"
1386
+ or parsed.username is not None
1387
+ or parsed.password is not None
1388
+ or parsed.port is not None
1389
+ ):
1390
+ return False
1391
+ without_valid_url_shapes = _URI_RE.sub("", normalized).lower()
1392
+ if "http:" in without_valid_url_shapes or "https:" in without_valid_url_shapes:
1393
+ return False
1394
+ return True
1395
+
1396
+
1397
+ def control_files_use_trusted_sources(manifest_bytes: bytes, lockfile_bytes: bytes) -> bool:
1398
+ if len(manifest_bytes) > MAX_CONTROL_FILE_BYTES or len(lockfile_bytes) > MAX_CONTROL_FILE_BYTES:
1399
+ return False
1400
+ try:
1401
+ manifest = _strict_json_loads(manifest_bytes)
1402
+ except (UnicodeDecodeError, ValueError, RecursionError):
1403
+ return False
1404
+ if not isinstance(manifest, dict) or _FORBIDDEN_MANIFEST_FIELDS.intersection(manifest):
1405
+ return False
1406
+ for field in _DEPENDENCY_FIELDS:
1407
+ dependencies = manifest.get(field, {})
1408
+ if not isinstance(dependencies, dict):
1409
+ return False
1410
+ if any(not isinstance(name, str) or not isinstance(spec, str) or not _dependency_spec_is_safe(spec) for name, spec in dependencies.items()):
1411
+ return False
1412
+ return _lockfile_uses_trusted_sources(lockfile_bytes)
1413
+
1414
+
1415
+ def _minimal_node_environment(source: Mapping[str, str] | None = None) -> dict[str, str]:
1416
+ ambient = os.environ if source is None else source
1417
+ environment = {name: ambient[name] for name in _LOCALE_ENVIRONMENT if name in ambient}
1418
+ if os.name == "nt":
1419
+ trusted_windows = _trusted_windows_system_environment()
1420
+ environment.update(trusted_windows)
1421
+ environment["PATH"] = str(Path(trusted_windows["SYSTEMROOT"]) / "System32")
1422
+ else:
1423
+ environment["PATH"] = "/usr/bin:/bin"
1424
+ return environment
1425
+
1426
+
1427
+ def _command_revalidation_reason(command: TrustedCommand, root: Path) -> str | None:
1428
+ for index, reference in enumerate(command.references):
1429
+ executable = index == 0 or reference.launcher_path is not None
1430
+ if not revalidate_trusted_file(reference, root, executable=executable):
1431
+ return "executable_changed" if index == 0 else "command_changed"
1432
+ return None
1433
+
1434
+
1435
+ def run_validator(
1436
+ root: Path,
1437
+ node: TrustedFile,
1438
+ validator: TrustedFile,
1439
+ timeout: float,
1440
+ runner: Runner = run_bounded_process,
1441
+ ) -> StepResult:
1442
+ if not math.isfinite(timeout) or timeout <= 0:
1443
+ return StepResult(False, "validator_rejected")
1444
+ if not revalidate_trusted_file(node, root, executable=True):
1445
+ return StepResult(False, "executable_changed")
1446
+ if not revalidate_trusted_file(validator, root, executable=False):
1447
+ return StepResult(False, "validator_changed")
1448
+ try:
1449
+ result = runner(
1450
+ [str(node.path), str(validator.path), "typescript"],
1451
+ cwd=root,
1452
+ stdin=None,
1453
+ timeout_seconds=timeout,
1454
+ max_output_bytes=INSTALL_OUTPUT_LIMIT,
1455
+ check=False,
1456
+ environment=_minimal_node_environment(),
1457
+ )
1458
+ except ProbeProcessError as error:
1459
+ return StepResult(False, "validator_timeout" if error.code == "timeout" else "validator_rejected")
1460
+ except Exception:
1461
+ return StepResult(False, "validator_rejected")
1462
+ return StepResult(result.returncode == 0, "validated" if result.returncode == 0 else "validator_rejected")
1463
+
1464
+
1465
+ def run_manager_version(
1466
+ command: TrustedCommand,
1467
+ root: Path,
1468
+ timeout: float,
1469
+ runner: Runner = run_bounded_process,
1470
+ ) -> VersionResult:
1471
+ if not math.isfinite(timeout) or timeout <= 0:
1472
+ return VersionResult(False, "manager_unavailable")
1473
+ # Returned as-is, the way `run_install` already does. Flattened into
1474
+ # `manager_unavailable`, a command refused for changing under us read
1475
+ # exactly like one that was never installed -- two opposite operator
1476
+ # actions behind one string, on a security check.
1477
+ provenance_reason = _command_revalidation_reason(command, root)
1478
+ if provenance_reason is not None:
1479
+ return VersionResult(False, provenance_reason)
1480
+ try:
1481
+ result = runner(
1482
+ [*command.argv, "--version"],
1483
+ cwd=root,
1484
+ stdin=None,
1485
+ timeout_seconds=timeout,
1486
+ max_output_bytes=MANAGER_VERSION_OUTPUT_LIMIT,
1487
+ check=False,
1488
+ environment=_minimal_node_environment(),
1489
+ )
1490
+ except ProbeProcessError as error:
1491
+ reason = "manager_timeout" if error.code == "timeout" else "manager_unavailable"
1492
+ return VersionResult(False, reason)
1493
+ except Exception:
1494
+ return VersionResult(False, "manager_unavailable")
1495
+ if result.returncode != 0:
1496
+ return VersionResult(False, "manager_unavailable")
1497
+ version = parse_version(result.stdout)
1498
+ if version is None:
1499
+ return VersionResult(False, "manager_version_invalid")
1500
+ return VersionResult(True, "manager_versioned", version)
1501
+
1502
+
1503
+ def run_install(
1504
+ root: Path,
1505
+ command: TrustedCommand,
1506
+ adapter: ManagerAdapter,
1507
+ registry: str,
1508
+ isolated_home: Path,
1509
+ timeout: float,
1510
+ runner: Runner = run_bounded_process,
1511
+ ) -> StepResult:
1512
+ if not math.isfinite(timeout) or timeout <= 0:
1513
+ return StepResult(False, "install_failed")
1514
+ if registry != TRUSTED_REGISTRY:
1515
+ return StepResult(False, "install_failed")
1516
+ try:
1517
+ canonical_adapter = adapter_for(adapter.manager)
1518
+ if adapter is not canonical_adapter or not canonical_adapter.automatic:
1519
+ return StepResult(False, "install_failed")
1520
+ except (KeyError, ValueError):
1521
+ return StepResult(False, "install_failed")
1522
+ if not _prepare_isolated_install_home(root, isolated_home, adapter.manager):
1523
+ return StepResult(False, "install_failed")
1524
+ path_reference = (
1525
+ command.references[-1]
1526
+ if os.name != "nt" and command.references[-1].launcher_path is not None
1527
+ else command.references[0]
1528
+ )
1529
+ directories = (path_reference.command_path.parent,)
1530
+ try:
1531
+ environment = build_install_environment(
1532
+ adapter.manager, isolated_home, directories, TRUSTED_REGISTRY, os.environ
1533
+ )
1534
+ except (OSError, RuntimeError, ValueError):
1535
+ return StepResult(False, "install_failed")
1536
+ provenance_reason = _command_revalidation_reason(command, root)
1537
+ if provenance_reason is not None:
1538
+ return StepResult(False, provenance_reason)
1539
+ try:
1540
+ result = runner(
1541
+ [*command.argv, *adapter.argument_tail(TRUSTED_REGISTRY)],
1542
+ cwd=root,
1543
+ stdin=None,
1544
+ timeout_seconds=timeout,
1545
+ max_output_bytes=INSTALL_OUTPUT_LIMIT,
1546
+ check=False,
1547
+ environment=environment,
1548
+ )
1549
+ except ProbeProcessError as error:
1550
+ return StepResult(False, "install_timeout" if error.code == "timeout" else "install_failed")
1551
+ except Exception:
1552
+ return StepResult(False, "install_failed")
1553
+ return StepResult(result.returncode == 0, "installed_command" if result.returncode == 0 else "install_failed")
1554
+
1555
+
1556
+ def probe_local_typescript(
1557
+ root: Path,
1558
+ node: TrustedFile,
1559
+ timeout: float,
1560
+ runner: Runner = run_bounded_process,
1561
+ ) -> bool:
1562
+ if not math.isfinite(timeout) or timeout <= 0:
1563
+ return False
1564
+ if not revalidate_trusted_file(node, root, executable=True):
1565
+ return False
1566
+ try:
1567
+ result = runner(
1568
+ [str(node.path), "-e", _LOCAL_TYPESCRIPT_SCRIPT],
1569
+ cwd=root,
1570
+ stdin=None,
1571
+ timeout_seconds=timeout,
1572
+ max_output_bytes=4096,
1573
+ check=False,
1574
+ environment=_minimal_node_environment(),
1575
+ )
1576
+ payload: Any = json.loads(result.stdout)
1577
+ resolved_value = payload.get("resolved") if isinstance(payload, dict) else None
1578
+ if result.returncode != 0 or not isinstance(resolved_value, str):
1579
+ return False
1580
+ canonical_root = root.resolve(strict=True)
1581
+ package_root = canonical_root / "node_modules" / "typescript"
1582
+ candidate = Path(resolved_value)
1583
+ if not candidate.is_absolute() or _path_has_symlink(candidate.absolute()):
1584
+ return False
1585
+ canonical_candidate = candidate.resolve(strict=True)
1586
+ details = candidate.lstat()
1587
+ return (
1588
+ candidate.absolute() == canonical_candidate
1589
+ and _is_under(canonical_candidate, package_root)
1590
+ and stat.S_ISREG(details.st_mode)
1591
+ and not stat.S_ISLNK(details.st_mode)
1592
+ and not _is_reparse(details)
1593
+ )
1594
+ except (OSError, RuntimeError, UnicodeDecodeError, json.JSONDecodeError, ProbeProcessError, RecursionError, ValueError):
1595
+ return False
1596
+ except Exception:
1597
+ return False