nexus-coding-agent 0.1.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 (131) hide show
  1. nexus/__init__.py +4 -0
  2. nexus/application/__init__.py +2 -0
  3. nexus/application/approval_service.py +130 -0
  4. nexus/application/diff_service.py +177 -0
  5. nexus/application/event_publisher.py +180 -0
  6. nexus/application/execution_context.py +29 -0
  7. nexus/application/execution_ledger.py +126 -0
  8. nexus/application/model_call_context.py +29 -0
  9. nexus/application/observed_model_gateway.py +194 -0
  10. nexus/application/plan_approval_service.py +250 -0
  11. nexus/application/planning.py +668 -0
  12. nexus/application/runtime.py +624 -0
  13. nexus/application/session_service.py +411 -0
  14. nexus/application/structured_output.py +96 -0
  15. nexus/application/telemetry.py +396 -0
  16. nexus/application/telemetry_subscriber.py +216 -0
  17. nexus/application/tool_runtime.py +496 -0
  18. nexus/application/tracing_dispatcher.py +283 -0
  19. nexus/application/validation.py +293 -0
  20. nexus/config/__init__.py +17 -0
  21. nexus/config/loader.py +253 -0
  22. nexus/config/models.py +310 -0
  23. nexus/context/__init__.py +6 -0
  24. nexus/context/budget.py +24 -0
  25. nexus/context/builder.py +92 -0
  26. nexus/context/chunking.py +65 -0
  27. nexus/context/explorer.py +246 -0
  28. nexus/context/integration.py +63 -0
  29. nexus/context/manager.py +328 -0
  30. nexus/context/retrieval.py +261 -0
  31. nexus/domain/__init__.py +2 -0
  32. nexus/domain/agent_decision.py +113 -0
  33. nexus/domain/agent_state.py +49 -0
  34. nexus/domain/approvals.py +64 -0
  35. nexus/domain/context.py +108 -0
  36. nexus/domain/exploration.py +86 -0
  37. nexus/domain/mcp.py +51 -0
  38. nexus/domain/model.py +173 -0
  39. nexus/domain/observability.py +326 -0
  40. nexus/domain/persistence.py +122 -0
  41. nexus/domain/planning.py +329 -0
  42. nexus/domain/ports/__init__.py +61 -0
  43. nexus/domain/ports/agent_decision.py +9 -0
  44. nexus/domain/ports/approval_unit_of_work.py +27 -0
  45. nexus/domain/ports/checkpoint_provider.py +11 -0
  46. nexus/domain/ports/context.py +71 -0
  47. nexus/domain/ports/events.py +21 -0
  48. nexus/domain/ports/graph_runtime.py +30 -0
  49. nexus/domain/ports/mcp.py +23 -0
  50. nexus/domain/ports/model_gateway.py +21 -0
  51. nexus/domain/ports/model_input_budget.py +15 -0
  52. nexus/domain/ports/planning.py +62 -0
  53. nexus/domain/ports/repositories.py +54 -0
  54. nexus/domain/ports/repository_context.py +18 -0
  55. nexus/domain/ports/session_unit_of_work.py +33 -0
  56. nexus/domain/ports/skills.py +41 -0
  57. nexus/domain/ports/tooling.py +39 -0
  58. nexus/domain/ports/tracer.py +13 -0
  59. nexus/domain/ports/validation.py +32 -0
  60. nexus/domain/runtime_events.py +445 -0
  61. nexus/domain/skills.py +129 -0
  62. nexus/domain/tooling.py +163 -0
  63. nexus/domain/validation.py +161 -0
  64. nexus/errors/__init__.py +82 -0
  65. nexus/evaluation/__init__.py +7 -0
  66. nexus/evaluation/assertions.py +4 -0
  67. nexus/evaluation/evaluator.py +263 -0
  68. nexus/evaluation/fixtures.py +36 -0
  69. nexus/evaluation/harness.py +606 -0
  70. nexus/evaluation/loader.py +415 -0
  71. nexus/evaluation/metrics.py +27 -0
  72. nexus/evaluation/models.py +362 -0
  73. nexus/evaluation/report.py +43 -0
  74. nexus/evaluation/runner.py +480 -0
  75. nexus/infrastructure/__init__.py +2 -0
  76. nexus/infrastructure/asyncio_compat.py +13 -0
  77. nexus/infrastructure/bootstrap/__init__.py +17 -0
  78. nexus/infrastructure/bootstrap/composition.py +663 -0
  79. nexus/infrastructure/checkpoint/__init__.py +5 -0
  80. nexus/infrastructure/checkpoint/postgres.py +62 -0
  81. nexus/infrastructure/database.py +39 -0
  82. nexus/infrastructure/embedding.py +61 -0
  83. nexus/infrastructure/graph/__init__.py +6 -0
  84. nexus/infrastructure/graph/day4_runtime.py +966 -0
  85. nexus/infrastructure/graph/langgraph_runtime.py +172 -0
  86. nexus/infrastructure/mcp/__init__.py +5 -0
  87. nexus/infrastructure/mcp/manager.py +380 -0
  88. nexus/infrastructure/model_gateway/__init__.py +6 -0
  89. nexus/infrastructure/model_gateway/openai_compatible.py +345 -0
  90. nexus/infrastructure/observability/__init__.py +7 -0
  91. nexus/infrastructure/observability/console.py +31 -0
  92. nexus/infrastructure/observability/langsmith.py +236 -0
  93. nexus/infrastructure/observability/structured_logger.py +70 -0
  94. nexus/infrastructure/persistence/__init__.py +13 -0
  95. nexus/infrastructure/persistence/approval_unit_of_work.py +74 -0
  96. nexus/infrastructure/persistence/models.py +146 -0
  97. nexus/infrastructure/persistence/repositories.py +326 -0
  98. nexus/infrastructure/persistence/unit_of_work.py +82 -0
  99. nexus/infrastructure/repository_files.py +117 -0
  100. nexus/infrastructure/sandbox/__init__.py +5 -0
  101. nexus/infrastructure/sandbox/local_process.py +353 -0
  102. nexus/infrastructure/semantic.py +276 -0
  103. nexus/infrastructure/semantic_models.py +56 -0
  104. nexus/interfaces/__init__.py +2 -0
  105. nexus/interfaces/cli/__init__.py +2 -0
  106. nexus/interfaces/cli/__main__.py +6 -0
  107. nexus/interfaces/cli/app.py +266 -0
  108. nexus/interfaces/cli/renderer.py +118 -0
  109. nexus/security/__init__.py +14 -0
  110. nexus/security/approval_policies.py +73 -0
  111. nexus/security/command_policy.py +117 -0
  112. nexus/security/executables.py +107 -0
  113. nexus/security/git_commands.py +65 -0
  114. nexus/security/workspace.py +93 -0
  115. nexus/skills/__init__.py +7 -0
  116. nexus/skills/builtin/__init__.py +1 -0
  117. nexus/skills/builtin/debug-python/SKILL.md +26 -0
  118. nexus/skills/builtin/review-repository/SKILL.md +26 -0
  119. nexus/skills/builtin/write-tests/SKILL.md +26 -0
  120. nexus/skills/loader.py +439 -0
  121. nexus/skills/registry.py +146 -0
  122. nexus/skills/selector.py +152 -0
  123. nexus/tools/__init__.py +6 -0
  124. nexus/tools/editing.py +312 -0
  125. nexus/tools/mcp.py +160 -0
  126. nexus/tools/native.py +654 -0
  127. nexus/tools/registry.py +27 -0
  128. nexus_coding_agent-0.1.0.dist-info/METADATA +142 -0
  129. nexus_coding_agent-0.1.0.dist-info/RECORD +131 -0
  130. nexus_coding_agent-0.1.0.dist-info/WHEEL +4 -0
  131. nexus_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,117 @@
1
+ """Day 5 read-only index filesystem access, contained and gitignore-aware."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from pathspec import GitIgnoreSpec
8
+
9
+ from nexus.context.chunking import normalize_text
10
+ from nexus.errors import NexusError
11
+ from nexus.security.workspace import WorkspaceGuard
12
+
13
+ EXCLUDED_DIRECTORIES = frozenset(
14
+ {
15
+ ".git",
16
+ "node_modules",
17
+ ".venv",
18
+ "venv",
19
+ "__pycache__",
20
+ "dist",
21
+ "build",
22
+ "target",
23
+ "coverage",
24
+ ".pytest_cache",
25
+ ".mypy_cache",
26
+ ".ruff_cache",
27
+ }
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class _Scan:
33
+ files: tuple[tuple[str, str], ...]
34
+ scanned: int
35
+ skipped: int
36
+
37
+
38
+ class RepositoryFiles:
39
+ def __init__(self, workspace: Path, max_file_size_bytes: int = 1048576) -> None:
40
+ self.guard = WorkspaceGuard(workspace)
41
+ self._maximum = max_file_size_bytes
42
+
43
+ def read(self, path: str) -> str:
44
+ resolved = self.guard.resolve_existing(path, require_file=True)
45
+ if resolved.is_symlink() or resolved.is_junction():
46
+ raise ValueError("Linked files are not indexable.")
47
+ with resolved.open("rb") as stream:
48
+ raw = stream.read(self._maximum + 1)
49
+ if len(raw) > self._maximum or b"\0" in raw:
50
+ raise ValueError("Unsupported or oversized source.")
51
+ return normalize_text(raw.decode("utf-8-sig"))
52
+
53
+ def allowed(self, relative: str) -> bool:
54
+ path = Path(relative)
55
+ if (
56
+ path.is_absolute()
57
+ or ".." in path.parts
58
+ or any(part in EXCLUDED_DIRECTORIES for part in path.parts)
59
+ or path.name == "AGENTS.md"
60
+ ):
61
+ return False
62
+ try:
63
+ self.guard.resolve_existing(relative, require_file=True)
64
+ rules: list[str] = []
65
+ current = Path()
66
+ for part in (*path.parts[:-1], None):
67
+ ignore = current / ".gitignore"
68
+ try:
69
+ content = self.read(ignore.as_posix())
70
+ # Match relative to each ignore file's directory. A parent directory
71
+ # exclusion is final: Git cannot reinclude children of an excluded dir.
72
+ spec = GitIgnoreSpec.from_lines(content.splitlines())
73
+ suffix = path.relative_to(current).as_posix()
74
+ components = suffix.split("/")
75
+ for length in range(1, len(components)):
76
+ if spec.match_file("/".join(components[:length]) + "/"):
77
+ return False
78
+ result = spec.check_file(suffix)
79
+ if result.include is not None:
80
+ rules.append("ignored" if result.include else "included")
81
+ except (OSError, ValueError, NexusError):
82
+ pass
83
+ if part is not None:
84
+ current /= part
85
+ return not rules or rules[-1] == "included"
86
+ except (OSError, ValueError, NexusError):
87
+ return False
88
+
89
+ def scan(self) -> _Scan:
90
+ files: list[tuple[str, str]] = []
91
+ scanned = skipped = 0
92
+ for current, directories, names in os.walk(self.guard.root, followlinks=False):
93
+ folder = Path(current)
94
+ directories[:] = sorted(
95
+ name
96
+ for name in directories
97
+ if name not in EXCLUDED_DIRECTORIES
98
+ and not (folder / name).is_symlink()
99
+ and not (folder / name).is_junction()
100
+ )
101
+ for name in sorted(names):
102
+ candidate = folder / name
103
+ relative = candidate.relative_to(self.guard.root).as_posix()
104
+ scanned += 1
105
+ if candidate.is_symlink() or candidate.is_junction() or not self.allowed(relative):
106
+ skipped += 1
107
+ continue
108
+ try:
109
+ content = self.read(relative)
110
+ except (OSError, ValueError, NexusError):
111
+ skipped += 1
112
+ continue
113
+ if not content:
114
+ skipped += 1
115
+ continue
116
+ files.append((relative, content))
117
+ return _Scan(tuple(files), scanned, skipped)
@@ -0,0 +1,5 @@
1
+ """Day 3 local process sandbox adapter."""
2
+
3
+ from nexus.infrastructure.sandbox.local_process import LocalProcessSandbox
4
+
5
+ __all__ = ["LocalProcessSandbox"]
@@ -0,0 +1,353 @@
1
+ """Standard-library local process isolation behind the SandboxExecutor port."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import signal
8
+ import subprocess
9
+ import time
10
+ from asyncio.subprocess import Process
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import cast
14
+
15
+ from nexus.domain.ports.tooling import CommandPolicy
16
+ from nexus.domain.tooling import (
17
+ PolicyDecision,
18
+ RiskLevel,
19
+ SandboxRequest,
20
+ SandboxResult,
21
+ ToolError,
22
+ )
23
+ from nexus.errors import NexusError
24
+ from nexus.security.executables import TrustedExecutables
25
+ from nexus.security.workspace import WorkspaceGuard
26
+
27
+ _OUTPUT_LIMIT_BYTES = 1_048_576
28
+
29
+
30
+ class LocalProcessSandbox:
31
+ def __init__(
32
+ self,
33
+ workspace_guard: WorkspaceGuard,
34
+ command_policy: CommandPolicy,
35
+ executables: TrustedExecutables,
36
+ ) -> None:
37
+ self._workspace_guard = workspace_guard
38
+ self._command_policy = command_policy
39
+ self._executables = executables
40
+
41
+ async def execute(self, request: SandboxRequest) -> SandboxResult:
42
+ started = time.perf_counter()
43
+ normalized_argv = self._executables.normalize_argv(request.argv)
44
+ arguments: dict[str, object] = {
45
+ "argv": normalized_argv,
46
+ "cwd": request.cwd,
47
+ "timeout_seconds": request.timeout_seconds,
48
+ }
49
+ proposed_risk = self._command_policy.classify(
50
+ operation=request.operation,
51
+ arguments=arguments,
52
+ )
53
+ if proposed_risk is RiskLevel.DANGEROUS:
54
+ return _denied_result(request, normalized_argv, started)
55
+ try:
56
+ cwd = self._workspace_guard.resolve_existing(
57
+ request.cwd,
58
+ require_directory=True,
59
+ )
60
+ except NexusError as exc:
61
+ return SandboxResult(
62
+ argv=normalized_argv,
63
+ cwd=request.cwd,
64
+ risk_level=RiskLevel.DANGEROUS,
65
+ policy_decision=PolicyDecision.DENIED,
66
+ exit_code=None,
67
+ stdout="",
68
+ stderr="",
69
+ duration_ms=_duration_ms(started),
70
+ timed_out=False,
71
+ output_truncated=False,
72
+ error=_tool_error(exc),
73
+ )
74
+
75
+ if os.name == "nt" and isinstance(
76
+ asyncio.get_running_loop(), asyncio.SelectorEventLoop
77
+ ):
78
+ return await asyncio.to_thread(
79
+ self._execute_on_windows_proactor,
80
+ request,
81
+ normalized_argv,
82
+ cwd,
83
+ proposed_risk,
84
+ started,
85
+ )
86
+ return await self._execute_allowed(
87
+ request,
88
+ normalized_argv,
89
+ cwd,
90
+ proposed_risk,
91
+ started,
92
+ )
93
+
94
+ def _execute_on_windows_proactor(
95
+ self,
96
+ request: SandboxRequest,
97
+ normalized_argv: list[str],
98
+ cwd: Path,
99
+ proposed_risk: RiskLevel,
100
+ started: float,
101
+ ) -> SandboxResult:
102
+ """Isolate Windows subprocess support from Day 2's selector loop."""
103
+
104
+ loop_factory = cast(
105
+ Callable[[], asyncio.AbstractEventLoop],
106
+ vars(asyncio)["ProactorEventLoop"],
107
+ )
108
+ loop = loop_factory()
109
+ try:
110
+ return loop.run_until_complete(
111
+ self._execute_allowed(
112
+ request,
113
+ normalized_argv,
114
+ cwd,
115
+ proposed_risk,
116
+ started,
117
+ )
118
+ )
119
+ finally:
120
+ loop.close()
121
+
122
+ async def _execute_allowed(
123
+ self,
124
+ request: SandboxRequest,
125
+ normalized_argv: list[str],
126
+ cwd: Path,
127
+ proposed_risk: RiskLevel,
128
+ started: float,
129
+ ) -> SandboxResult:
130
+
131
+ process: Process | None = None
132
+ stdout_task: asyncio.Task[tuple[bytes, bool]] | None = None
133
+ stderr_task: asyncio.Task[tuple[bytes, bool]] | None = None
134
+ try:
135
+ process = await self._start_process(normalized_argv, cwd)
136
+ assert process.stdout is not None
137
+ assert process.stderr is not None
138
+ stdout_task = asyncio.create_task(_read_bounded(process.stdout))
139
+ stderr_task = asyncio.create_task(_read_bounded(process.stderr))
140
+ try:
141
+ await asyncio.wait_for(process.wait(), timeout=request.timeout_seconds)
142
+ except TimeoutError:
143
+ await _terminate(process)
144
+ stdout, stdout_truncated = await _finish_reader(stdout_task)
145
+ stderr, stderr_truncated = await _finish_reader(stderr_task)
146
+ return SandboxResult(
147
+ argv=normalized_argv,
148
+ cwd=self._workspace_guard.relative_display(cwd),
149
+ risk_level=proposed_risk,
150
+ policy_decision=PolicyDecision.ALLOWED,
151
+ exit_code=None,
152
+ stdout=_decode(stdout),
153
+ stderr=_decode(stderr),
154
+ duration_ms=_duration_ms(started),
155
+ timed_out=True,
156
+ output_truncated=stdout_truncated or stderr_truncated,
157
+ error=ToolError(
158
+ "SANDBOX_TIMEOUT",
159
+ "The allowed command exceeded its timeout.",
160
+ True,
161
+ ),
162
+ )
163
+ stdout, stdout_truncated = await _finish_reader(stdout_task)
164
+ stderr, stderr_truncated = await _finish_reader(stderr_task)
165
+ exit_code = process.returncode
166
+ error = (
167
+ None
168
+ if exit_code == 0
169
+ else ToolError(
170
+ "COMMAND_EXIT_NONZERO",
171
+ "The allowed command exited with a non-zero status.",
172
+ False,
173
+ )
174
+ )
175
+ return SandboxResult(
176
+ argv=normalized_argv,
177
+ cwd=self._workspace_guard.relative_display(cwd),
178
+ risk_level=proposed_risk,
179
+ policy_decision=PolicyDecision.ALLOWED,
180
+ exit_code=exit_code,
181
+ stdout=_decode(stdout),
182
+ stderr=_decode(stderr),
183
+ duration_ms=_duration_ms(started),
184
+ timed_out=False,
185
+ output_truncated=stdout_truncated or stderr_truncated,
186
+ error=error,
187
+ )
188
+ except asyncio.CancelledError:
189
+ if process is not None and process.returncode is None:
190
+ await _terminate(process)
191
+ raise
192
+ except Exception:
193
+ if process is not None and process.returncode is None:
194
+ await _terminate(process)
195
+ _cancel_reader(stdout_task)
196
+ _cancel_reader(stderr_task)
197
+ return SandboxResult(
198
+ argv=normalized_argv,
199
+ cwd=self._workspace_guard.relative_display(cwd),
200
+ risk_level=proposed_risk,
201
+ policy_decision=PolicyDecision.ALLOWED,
202
+ exit_code=None,
203
+ stdout="",
204
+ stderr="",
205
+ duration_ms=_duration_ms(started),
206
+ timed_out=False,
207
+ output_truncated=False,
208
+ error=ToolError(
209
+ "SANDBOX_EXECUTION_ERROR",
210
+ "The allowed command could not be safely executed.",
211
+ False,
212
+ ),
213
+ )
214
+
215
+ async def _start_process(self, argv: list[str], cwd: Path) -> Process:
216
+ environment = _minimum_environment(self._workspace_guard.root)
217
+ if os.name == "nt":
218
+ return await asyncio.create_subprocess_exec(
219
+ *argv,
220
+ cwd=cwd,
221
+ env=environment,
222
+ stdin=subprocess.DEVNULL,
223
+ stdout=asyncio.subprocess.PIPE,
224
+ stderr=asyncio.subprocess.PIPE,
225
+ creationflags=cast(
226
+ int,
227
+ vars(subprocess)["CREATE_NEW_PROCESS_GROUP"],
228
+ ),
229
+ )
230
+ return await asyncio.create_subprocess_exec(
231
+ *argv,
232
+ cwd=cwd,
233
+ env=environment,
234
+ stdin=subprocess.DEVNULL,
235
+ stdout=asyncio.subprocess.PIPE,
236
+ stderr=asyncio.subprocess.PIPE,
237
+ start_new_session=True,
238
+ )
239
+
240
+
241
+ async def _read_bounded(stream: asyncio.StreamReader) -> tuple[bytes, bool]:
242
+ retained = bytearray()
243
+ truncated = False
244
+ while chunk := await stream.read(65_536):
245
+ remaining = _OUTPUT_LIMIT_BYTES - len(retained)
246
+ if remaining > 0:
247
+ retained.extend(chunk[:remaining])
248
+ if len(chunk) > remaining:
249
+ truncated = True
250
+ return bytes(retained), truncated
251
+
252
+
253
+ async def _finish_reader(task: asyncio.Task[tuple[bytes, bool]]) -> tuple[bytes, bool]:
254
+ try:
255
+ return await asyncio.wait_for(task, timeout=2.0)
256
+ except TimeoutError:
257
+ task.cancel()
258
+ return b"", True
259
+
260
+
261
+ def _cancel_reader(task: asyncio.Task[tuple[bytes, bool]] | None) -> None:
262
+ if task is not None and not task.done():
263
+ task.cancel()
264
+
265
+
266
+ async def _terminate(process: Process) -> None:
267
+ if process.returncode is not None:
268
+ return
269
+ try:
270
+ if os.name != "nt":
271
+ kill_process_group = getattr(os, "killpg") # noqa: B009
272
+ kill_process_group(process.pid, getattr(signal, "SIGKILL")) # noqa: B009
273
+ else:
274
+ process.kill()
275
+ except ProcessLookupError:
276
+ pass
277
+ await process.wait()
278
+
279
+
280
+ def _minimum_environment(workspace: Path) -> dict[str, str]:
281
+ keys = ["PATH"]
282
+ keys.extend(
283
+ ["SystemRoot", "WINDIR", "PATHEXT", "TEMP", "TMP"]
284
+ if os.name == "nt"
285
+ else ["LANG", "LC_ALL", "TMPDIR"]
286
+ )
287
+ environment = {key: os.environ[key] for key in keys if key in os.environ}
288
+ if "PATH" in environment:
289
+ entries = []
290
+ for value in environment["PATH"].split(os.pathsep):
291
+ if not value or not Path(value).is_absolute():
292
+ continue
293
+ resolved = Path(value).resolve(strict=False)
294
+ if not _is_within(resolved, workspace):
295
+ entries.append(str(resolved))
296
+ environment["PATH"] = os.pathsep.join(entries)
297
+ environment.update(
298
+ {
299
+ "PYTHONIOENCODING": "utf-8",
300
+ "GIT_TERMINAL_PROMPT": "0",
301
+ "GIT_CONFIG_NOSYSTEM": "1",
302
+ "GIT_OPTIONAL_LOCKS": "0",
303
+ "GIT_CONFIG_GLOBAL": os.devnull,
304
+ }
305
+ )
306
+ return environment
307
+
308
+
309
+ def _denied_result(
310
+ request: SandboxRequest,
311
+ argv: list[str],
312
+ started: float,
313
+ ) -> SandboxResult:
314
+ return SandboxResult(
315
+ argv=argv,
316
+ cwd=request.cwd,
317
+ risk_level=RiskLevel.DANGEROUS,
318
+ policy_decision=PolicyDecision.DENIED,
319
+ exit_code=None,
320
+ stdout="",
321
+ stderr="",
322
+ duration_ms=_duration_ms(started),
323
+ timed_out=False,
324
+ output_truncated=False,
325
+ error=ToolError(
326
+ "COMMAND_DENIED",
327
+ "The command is not allowed by the Day 3 security policy.",
328
+ False,
329
+ ),
330
+ )
331
+
332
+
333
+ def _tool_error(error: NexusError) -> ToolError:
334
+ return ToolError(error.code, str(error), error.retryable)
335
+
336
+
337
+ def _duration_ms(started: float) -> int:
338
+ return max(0, round((time.perf_counter() - started) * 1000))
339
+
340
+
341
+ def _decode(value: bytes) -> str:
342
+ return value.decode("utf-8", errors="replace")
343
+
344
+
345
+ def _is_within(path: Path, workspace: Path) -> bool:
346
+ normalized_path = os.path.normcase(str(path))
347
+ normalized_workspace = os.path.normcase(str(workspace))
348
+ try:
349
+ return os.path.commonpath([normalized_path, normalized_workspace]) == (
350
+ normalized_workspace
351
+ )
352
+ except ValueError:
353
+ return False