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.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
"""Private, identity-bound workspace leases for destructive readiness probes.
|
|
2
|
+
|
|
3
|
+
The local OS user and same-user process namespace remain a trust boundary. The
|
|
4
|
+
lease provides best-effort identity checks and Windows rename pinning; it does
|
|
5
|
+
not claim complete protection from a malicious process running as that user.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ctypes
|
|
10
|
+
import os
|
|
11
|
+
import secrets
|
|
12
|
+
import shutil
|
|
13
|
+
import stat
|
|
14
|
+
import tempfile
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkspaceLeaseError(RuntimeError):
|
|
21
|
+
"""A workspace could not be acquired, revalidated, or safely cleaned."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, code: str) -> None:
|
|
24
|
+
super().__init__(code)
|
|
25
|
+
self.code = code
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class _PathSnapshot:
|
|
30
|
+
lexical: Path
|
|
31
|
+
canonical: Path
|
|
32
|
+
identity: tuple[int, int]
|
|
33
|
+
size: int
|
|
34
|
+
mtime_ns: int
|
|
35
|
+
mode: int
|
|
36
|
+
uid: int | None
|
|
37
|
+
gid: int | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ParentFactory = Callable[..., str]
|
|
41
|
+
OwnedDirectoryFactory = Callable[[Path], tuple[Path, tuple[int, int]]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProbeWorkspaceLease:
|
|
45
|
+
"""Own a private parent and fixed child workspace until bounded cleanup."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
temp_root: Path,
|
|
51
|
+
parent: _PathSnapshot,
|
|
52
|
+
workspace: _PathSnapshot,
|
|
53
|
+
temp_root_handle: int | None,
|
|
54
|
+
parent_handle: int,
|
|
55
|
+
workspace_handle: int,
|
|
56
|
+
windows_handle_identities: tuple[tuple[int, int, int], tuple[int, int, int]] | None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self.temp_root = temp_root
|
|
59
|
+
self._parent = parent
|
|
60
|
+
self._workspace = workspace
|
|
61
|
+
self._temp_root_handle: int | None = temp_root_handle
|
|
62
|
+
self._parent_handle: int | None = parent_handle
|
|
63
|
+
self._workspace_handle: int | None = workspace_handle
|
|
64
|
+
self._windows_handle_identities = windows_handle_identities
|
|
65
|
+
self._closed = False
|
|
66
|
+
self._cleaned = False
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def parent_path(self) -> Path:
|
|
70
|
+
return self._parent.lexical
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def path(self) -> Path:
|
|
74
|
+
return self._workspace.lexical
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def acquire(
|
|
78
|
+
cls,
|
|
79
|
+
*,
|
|
80
|
+
temp_root: Path | None = None,
|
|
81
|
+
_parent_factory: ParentFactory | None = None,
|
|
82
|
+
_owned_parent_factory: OwnedDirectoryFactory | None = None,
|
|
83
|
+
_workspace_factory: OwnedDirectoryFactory | None = None,
|
|
84
|
+
) -> ProbeWorkspaceLease:
|
|
85
|
+
"""Create and pin a private randomized parent with a fixed workspace child."""
|
|
86
|
+
try:
|
|
87
|
+
root = (temp_root or Path(tempfile.gettempdir())).resolve(strict=True)
|
|
88
|
+
if not root.is_dir():
|
|
89
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
90
|
+
except (OSError, RuntimeError) as exc:
|
|
91
|
+
raise WorkspaceLeaseError("workspace_isolation_failed") from exc
|
|
92
|
+
|
|
93
|
+
parent_path: Path | None = None
|
|
94
|
+
workspace_path: Path | None = None
|
|
95
|
+
parent_handle: int | None = None
|
|
96
|
+
workspace_handle: int | None = None
|
|
97
|
+
temp_root_handle: int | None = None
|
|
98
|
+
parent_owned: _PathSnapshot | None = None
|
|
99
|
+
workspace_owned: _PathSnapshot | None = None
|
|
100
|
+
try:
|
|
101
|
+
if os.name != "nt":
|
|
102
|
+
root_snapshot = _snapshot(root, root)
|
|
103
|
+
temp_root_handle, root_handle_identity = _open_directory_handle(root)
|
|
104
|
+
_assert_handle_matches_snapshot(
|
|
105
|
+
temp_root_handle,
|
|
106
|
+
root_handle_identity,
|
|
107
|
+
root_snapshot,
|
|
108
|
+
)
|
|
109
|
+
if _parent_factory is not None:
|
|
110
|
+
if _owned_parent_factory is not None:
|
|
111
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
112
|
+
parent_path = Path(
|
|
113
|
+
_parent_factory(prefix="graphite-doctor-", dir=str(root))
|
|
114
|
+
)
|
|
115
|
+
parent_path = Path(os.path.abspath(parent_path))
|
|
116
|
+
_snapshot(parent_path, root)
|
|
117
|
+
# A path-only factory provides no proof that this acquisition
|
|
118
|
+
# created the candidate. It is safe to inspect, never to adopt.
|
|
119
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
120
|
+
parent_creator = _owned_parent_factory or _create_owned_parent
|
|
121
|
+
parent_path, parent_identity = parent_creator(root)
|
|
122
|
+
parent_path = Path(os.path.abspath(parent_path))
|
|
123
|
+
parent_before = _snapshot(parent_path, root)
|
|
124
|
+
if parent_before.identity != parent_identity:
|
|
125
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
126
|
+
parent_owned = parent_before
|
|
127
|
+
if os.name != "nt" and parent_before.mode != 0o700:
|
|
128
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
129
|
+
if os.name == "nt" and _owned_parent_factory is not None:
|
|
130
|
+
_set_private_windows_dacl(parent_path)
|
|
131
|
+
parent_handle, parent_handle_identity = _open_directory_handle(parent_path)
|
|
132
|
+
_assert_handle_matches_snapshot(
|
|
133
|
+
parent_handle,
|
|
134
|
+
parent_handle_identity,
|
|
135
|
+
parent_before,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
workspace_path = parent_path / "workspace"
|
|
139
|
+
workspace_creator = _workspace_factory or _create_owned_workspace
|
|
140
|
+
if _workspace_factory is None and os.name != "nt":
|
|
141
|
+
created_workspace_path, workspace_identity = _create_owned_workspace(
|
|
142
|
+
parent_path,
|
|
143
|
+
parent_handle,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
created_workspace_path, workspace_identity = workspace_creator(parent_path)
|
|
147
|
+
created_workspace_path = Path(os.path.abspath(created_workspace_path))
|
|
148
|
+
if created_workspace_path != workspace_path:
|
|
149
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
150
|
+
workspace_owned = _snapshot(created_workspace_path, root)
|
|
151
|
+
if workspace_owned.identity != workspace_identity:
|
|
152
|
+
workspace_owned = None
|
|
153
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
154
|
+
if os.name != "nt" and workspace_owned.mode != 0o700:
|
|
155
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
156
|
+
if os.name == "nt" and _workspace_factory is not None:
|
|
157
|
+
_set_private_windows_dacl(workspace_path)
|
|
158
|
+
workspace_snapshot = _snapshot(workspace_path, root)
|
|
159
|
+
workspace_handle, workspace_handle_identity = _open_directory_handle(workspace_path)
|
|
160
|
+
_assert_handle_matches_snapshot(
|
|
161
|
+
workspace_handle,
|
|
162
|
+
workspace_handle_identity,
|
|
163
|
+
workspace_snapshot,
|
|
164
|
+
)
|
|
165
|
+
parent_snapshot = _snapshot(parent_path, root)
|
|
166
|
+
if parent_snapshot.identity != parent_before.identity:
|
|
167
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
168
|
+
_assert_handle_matches_snapshot(
|
|
169
|
+
parent_handle,
|
|
170
|
+
parent_handle_identity,
|
|
171
|
+
parent_snapshot,
|
|
172
|
+
)
|
|
173
|
+
_assert_handle_matches_snapshot(
|
|
174
|
+
workspace_handle,
|
|
175
|
+
workspace_handle_identity,
|
|
176
|
+
workspace_snapshot,
|
|
177
|
+
)
|
|
178
|
+
return cls(
|
|
179
|
+
temp_root=root,
|
|
180
|
+
parent=parent_snapshot,
|
|
181
|
+
workspace=workspace_snapshot,
|
|
182
|
+
temp_root_handle=temp_root_handle,
|
|
183
|
+
parent_handle=parent_handle,
|
|
184
|
+
workspace_handle=workspace_handle,
|
|
185
|
+
windows_handle_identities=(parent_handle_identity, workspace_handle_identity)
|
|
186
|
+
if os.name == "nt"
|
|
187
|
+
else None,
|
|
188
|
+
)
|
|
189
|
+
except WorkspaceLeaseError:
|
|
190
|
+
_close_raw_handle(workspace_handle)
|
|
191
|
+
_close_raw_handle(parent_handle)
|
|
192
|
+
_close_raw_handle(temp_root_handle)
|
|
193
|
+
_initial_cleanup(root, parent_owned, workspace_owned)
|
|
194
|
+
raise
|
|
195
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
196
|
+
_close_raw_handle(workspace_handle)
|
|
197
|
+
_close_raw_handle(parent_handle)
|
|
198
|
+
_close_raw_handle(temp_root_handle)
|
|
199
|
+
_initial_cleanup(root, parent_owned, workspace_owned)
|
|
200
|
+
raise WorkspaceLeaseError("workspace_isolation_failed") from exc
|
|
201
|
+
|
|
202
|
+
def validate(self) -> None:
|
|
203
|
+
"""Revalidate containment, type, identity, ownership, and pinned handles."""
|
|
204
|
+
if self._closed or self._cleaned:
|
|
205
|
+
raise WorkspaceLeaseError("workspace_isolation_changed")
|
|
206
|
+
try:
|
|
207
|
+
parent = _snapshot(self.parent_path, self.temp_root)
|
|
208
|
+
workspace = _snapshot(self.path, self.temp_root)
|
|
209
|
+
if not _same_binding(parent, self._parent) or not _same_binding(workspace, self._workspace):
|
|
210
|
+
raise WorkspaceLeaseError("workspace_isolation_changed")
|
|
211
|
+
if os.name == "nt":
|
|
212
|
+
assert self._windows_handle_identities is not None
|
|
213
|
+
current_parent = _windows_handle_identity(self._required_handle(self._parent_handle))
|
|
214
|
+
current_workspace = _windows_handle_identity(self._required_handle(self._workspace_handle))
|
|
215
|
+
if (current_parent, current_workspace) != self._windows_handle_identities:
|
|
216
|
+
raise WorkspaceLeaseError("workspace_isolation_changed")
|
|
217
|
+
_assert_handle_matches_snapshot(
|
|
218
|
+
self._required_handle(self._parent_handle),
|
|
219
|
+
current_parent,
|
|
220
|
+
self._parent,
|
|
221
|
+
)
|
|
222
|
+
_assert_handle_matches_snapshot(
|
|
223
|
+
self._required_handle(self._workspace_handle),
|
|
224
|
+
current_workspace,
|
|
225
|
+
self._workspace,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
root_stat = os.fstat(self._required_handle(self._temp_root_handle))
|
|
229
|
+
parent_stat = os.fstat(self._required_handle(self._parent_handle))
|
|
230
|
+
workspace_stat = os.fstat(self._required_handle(self._workspace_handle))
|
|
231
|
+
root_path_stat = self.temp_root.lstat()
|
|
232
|
+
if (root_stat.st_dev, root_stat.st_ino) != (
|
|
233
|
+
root_path_stat.st_dev,
|
|
234
|
+
root_path_stat.st_ino,
|
|
235
|
+
):
|
|
236
|
+
raise WorkspaceLeaseError("workspace_isolation_changed")
|
|
237
|
+
if (parent_stat.st_dev, parent_stat.st_ino) != self._parent.identity:
|
|
238
|
+
raise WorkspaceLeaseError("workspace_isolation_changed")
|
|
239
|
+
if (workspace_stat.st_dev, workspace_stat.st_ino) != self._workspace.identity:
|
|
240
|
+
raise WorkspaceLeaseError("workspace_isolation_changed")
|
|
241
|
+
except WorkspaceLeaseError:
|
|
242
|
+
raise
|
|
243
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
244
|
+
raise WorkspaceLeaseError("workspace_isolation_changed") from exc
|
|
245
|
+
|
|
246
|
+
def cleanup(self) -> None:
|
|
247
|
+
"""Delete only the still-bound lease target, otherwise deliberately leak it."""
|
|
248
|
+
if self._cleaned:
|
|
249
|
+
self.close()
|
|
250
|
+
return
|
|
251
|
+
try:
|
|
252
|
+
self.validate()
|
|
253
|
+
except WorkspaceLeaseError as exc:
|
|
254
|
+
self.close()
|
|
255
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked") from exc
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
if os.name == "nt":
|
|
259
|
+
self._cleanup_windows()
|
|
260
|
+
else:
|
|
261
|
+
if not getattr(shutil.rmtree, "avoids_symlink_attacks", False):
|
|
262
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked")
|
|
263
|
+
if os.rmdir not in os.supports_dir_fd:
|
|
264
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked")
|
|
265
|
+
hook = getattr(self, "_before_cleanup_delete", None)
|
|
266
|
+
if hook is not None:
|
|
267
|
+
hook()
|
|
268
|
+
try:
|
|
269
|
+
self.validate()
|
|
270
|
+
except WorkspaceLeaseError as exc:
|
|
271
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked") from exc
|
|
272
|
+
if self.parent_path.parent != self.temp_root or self.path.name != "workspace":
|
|
273
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked")
|
|
274
|
+
shutil.rmtree(
|
|
275
|
+
self.path.name,
|
|
276
|
+
dir_fd=self._required_handle(self._parent_handle),
|
|
277
|
+
)
|
|
278
|
+
self._validate_posix_parent_binding()
|
|
279
|
+
os.rmdir(
|
|
280
|
+
self.parent_path.name,
|
|
281
|
+
dir_fd=self._required_handle(self._temp_root_handle),
|
|
282
|
+
)
|
|
283
|
+
self._cleaned = True
|
|
284
|
+
self.close()
|
|
285
|
+
except WorkspaceLeaseError:
|
|
286
|
+
self.close()
|
|
287
|
+
raise
|
|
288
|
+
except OSError as exc:
|
|
289
|
+
self.close()
|
|
290
|
+
raise WorkspaceLeaseError("workspace_cleanup_failed") from exc
|
|
291
|
+
|
|
292
|
+
def _cleanup_windows(self) -> None:
|
|
293
|
+
for entry in os.scandir(self.path):
|
|
294
|
+
entry_path = Path(entry.path)
|
|
295
|
+
if entry.is_dir(follow_symlinks=False):
|
|
296
|
+
shutil.rmtree(entry_path)
|
|
297
|
+
else:
|
|
298
|
+
entry_path.unlink()
|
|
299
|
+
self.validate()
|
|
300
|
+
_close_raw_handle(self._workspace_handle)
|
|
301
|
+
self._workspace_handle = None
|
|
302
|
+
os.rmdir(self.path)
|
|
303
|
+
parent = _snapshot(self.parent_path, self.temp_root)
|
|
304
|
+
if not _same_binding(parent, self._parent):
|
|
305
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked")
|
|
306
|
+
if any(os.scandir(self.parent_path)):
|
|
307
|
+
raise WorkspaceLeaseError("workspace_cleanup_failed")
|
|
308
|
+
_close_raw_handle(self._parent_handle)
|
|
309
|
+
self._parent_handle = None
|
|
310
|
+
os.rmdir(self.parent_path)
|
|
311
|
+
self._cleaned = True
|
|
312
|
+
self.close()
|
|
313
|
+
|
|
314
|
+
def close(self) -> None:
|
|
315
|
+
"""Release lease handles; safe to call repeatedly."""
|
|
316
|
+
if self._closed:
|
|
317
|
+
return
|
|
318
|
+
_close_raw_handle(self._workspace_handle)
|
|
319
|
+
_close_raw_handle(self._parent_handle)
|
|
320
|
+
_close_raw_handle(self._temp_root_handle)
|
|
321
|
+
self._workspace_handle = None
|
|
322
|
+
self._parent_handle = None
|
|
323
|
+
self._temp_root_handle = None
|
|
324
|
+
self._closed = True
|
|
325
|
+
|
|
326
|
+
def _validate_posix_parent_binding(self) -> None:
|
|
327
|
+
parent = _snapshot(self.parent_path, self.temp_root)
|
|
328
|
+
if not _same_binding(parent, self._parent):
|
|
329
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked")
|
|
330
|
+
root_stat = os.fstat(self._required_handle(self._temp_root_handle))
|
|
331
|
+
path_root_stat = self.temp_root.lstat()
|
|
332
|
+
if (root_stat.st_dev, root_stat.st_ino) != (
|
|
333
|
+
path_root_stat.st_dev,
|
|
334
|
+
path_root_stat.st_ino,
|
|
335
|
+
):
|
|
336
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked")
|
|
337
|
+
parent_stat = os.fstat(self._required_handle(self._parent_handle))
|
|
338
|
+
if (parent_stat.st_dev, parent_stat.st_ino) != self._parent.identity:
|
|
339
|
+
raise WorkspaceLeaseError("workspace_cleanup_blocked")
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def _required_handle(handle: int | None) -> int:
|
|
343
|
+
if handle is None:
|
|
344
|
+
raise WorkspaceLeaseError("workspace_isolation_changed")
|
|
345
|
+
return handle
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _snapshot(path: Path, root: Path) -> _PathSnapshot:
|
|
349
|
+
lexical = Path(os.path.abspath(path))
|
|
350
|
+
info = lexical.lstat()
|
|
351
|
+
if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode) or _path_is_reparse(info):
|
|
352
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
353
|
+
canonical = lexical.resolve(strict=True)
|
|
354
|
+
try:
|
|
355
|
+
canonical.relative_to(root)
|
|
356
|
+
except ValueError as exc:
|
|
357
|
+
raise WorkspaceLeaseError("workspace_isolation_failed") from exc
|
|
358
|
+
if os.name == "nt" and _windows_path_is_reparse(lexical):
|
|
359
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
360
|
+
return _PathSnapshot(
|
|
361
|
+
lexical=lexical,
|
|
362
|
+
canonical=canonical,
|
|
363
|
+
identity=(info.st_dev, info.st_ino),
|
|
364
|
+
size=info.st_size,
|
|
365
|
+
mtime_ns=info.st_mtime_ns,
|
|
366
|
+
mode=stat.S_IMODE(info.st_mode),
|
|
367
|
+
uid=getattr(info, "st_uid", None),
|
|
368
|
+
gid=getattr(info, "st_gid", None),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _same_binding(current: _PathSnapshot, expected: _PathSnapshot) -> bool:
|
|
373
|
+
return (
|
|
374
|
+
current.lexical == expected.lexical
|
|
375
|
+
and current.canonical == expected.canonical
|
|
376
|
+
and current.identity == expected.identity
|
|
377
|
+
and current.mode == expected.mode
|
|
378
|
+
and current.uid == expected.uid
|
|
379
|
+
and current.gid == expected.gid
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _path_is_reparse(info: os.stat_result) -> bool:
|
|
384
|
+
return bool(getattr(info, "st_file_attributes", 0) & 0x400)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _create_owned_parent(root: Path) -> tuple[Path, tuple[int, int]]:
|
|
388
|
+
if os.name == "nt":
|
|
389
|
+
path = _create_private_windows_directory(root)
|
|
390
|
+
else:
|
|
391
|
+
path = Path(tempfile.mkdtemp(prefix="graphite-doctor-", dir=str(root)))
|
|
392
|
+
info = path.lstat()
|
|
393
|
+
return path, (info.st_dev, info.st_ino)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _create_owned_workspace(
|
|
397
|
+
parent: Path,
|
|
398
|
+
parent_handle: int | None = None,
|
|
399
|
+
) -> tuple[Path, tuple[int, int]]:
|
|
400
|
+
if os.name == "nt":
|
|
401
|
+
path = _create_private_windows_directory(parent, "workspace")
|
|
402
|
+
else:
|
|
403
|
+
path = parent / "workspace"
|
|
404
|
+
if parent_handle is None:
|
|
405
|
+
os.mkdir(path, 0o700)
|
|
406
|
+
info = path.lstat()
|
|
407
|
+
return path, (info.st_dev, info.st_ino)
|
|
408
|
+
os.mkdir(path.name, 0o700, dir_fd=parent_handle)
|
|
409
|
+
info = os.stat(path.name, dir_fd=parent_handle, follow_symlinks=False)
|
|
410
|
+
return path, (info.st_dev, info.st_ino)
|
|
411
|
+
info = path.lstat()
|
|
412
|
+
return path, (info.st_dev, info.st_ino)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _initial_cleanup(
|
|
416
|
+
root: Path,
|
|
417
|
+
parent: _PathSnapshot | None,
|
|
418
|
+
workspace: _PathSnapshot | None,
|
|
419
|
+
) -> None:
|
|
420
|
+
for owned in (workspace, parent):
|
|
421
|
+
if owned is None:
|
|
422
|
+
continue
|
|
423
|
+
try:
|
|
424
|
+
current = _snapshot(owned.lexical, root)
|
|
425
|
+
if _same_binding(current, owned):
|
|
426
|
+
os.rmdir(owned.lexical)
|
|
427
|
+
except (OSError, WorkspaceLeaseError):
|
|
428
|
+
# Acquisition failure cleanup is deliberately best effort. An
|
|
429
|
+
# uncertain binding is leaked rather than pathname-deleted.
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _open_directory_handle(path: Path) -> tuple[int, tuple[int, int, int]]:
|
|
434
|
+
if os.name != "nt":
|
|
435
|
+
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
436
|
+
handle = os.open(path, flags)
|
|
437
|
+
os.set_inheritable(handle, False)
|
|
438
|
+
info = os.fstat(handle)
|
|
439
|
+
return handle, (info.st_dev, 0, info.st_ino)
|
|
440
|
+
|
|
441
|
+
from ctypes import wintypes
|
|
442
|
+
|
|
443
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
444
|
+
create_file = kernel32.CreateFileW
|
|
445
|
+
create_file.argtypes = [
|
|
446
|
+
wintypes.LPCWSTR,
|
|
447
|
+
wintypes.DWORD,
|
|
448
|
+
wintypes.DWORD,
|
|
449
|
+
wintypes.LPVOID,
|
|
450
|
+
wintypes.DWORD,
|
|
451
|
+
wintypes.DWORD,
|
|
452
|
+
wintypes.HANDLE,
|
|
453
|
+
]
|
|
454
|
+
create_file.restype = wintypes.HANDLE
|
|
455
|
+
handle = create_file(
|
|
456
|
+
str(path),
|
|
457
|
+
0x80000000 | 0x80,
|
|
458
|
+
0x1 | 0x2,
|
|
459
|
+
None,
|
|
460
|
+
3,
|
|
461
|
+
0x02000000 | 0x00200000,
|
|
462
|
+
None,
|
|
463
|
+
)
|
|
464
|
+
invalid_handle = ctypes.c_void_p(-1).value
|
|
465
|
+
if handle == invalid_handle:
|
|
466
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
467
|
+
raw_handle = int(handle)
|
|
468
|
+
set_handle_information = kernel32.SetHandleInformation
|
|
469
|
+
set_handle_information.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD]
|
|
470
|
+
set_handle_information.restype = wintypes.BOOL
|
|
471
|
+
if not set_handle_information(raw_handle, 0x1, 0):
|
|
472
|
+
error = ctypes.get_last_error()
|
|
473
|
+
_close_raw_handle(raw_handle)
|
|
474
|
+
raise ctypes.WinError(error)
|
|
475
|
+
try:
|
|
476
|
+
identity = _windows_handle_identity(raw_handle)
|
|
477
|
+
_reject_windows_handle_reparse(raw_handle)
|
|
478
|
+
return raw_handle, identity
|
|
479
|
+
except Exception:
|
|
480
|
+
_close_raw_handle(raw_handle)
|
|
481
|
+
raise
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _close_raw_handle(handle: int | None) -> None:
|
|
485
|
+
if handle is None:
|
|
486
|
+
return
|
|
487
|
+
if os.name != "nt":
|
|
488
|
+
try:
|
|
489
|
+
os.close(handle)
|
|
490
|
+
except OSError:
|
|
491
|
+
pass
|
|
492
|
+
return
|
|
493
|
+
from ctypes import wintypes
|
|
494
|
+
|
|
495
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
496
|
+
close_handle = kernel32.CloseHandle
|
|
497
|
+
close_handle.argtypes = [wintypes.HANDLE]
|
|
498
|
+
close_handle.restype = wintypes.BOOL
|
|
499
|
+
close_handle(handle)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _windows_handle_identity(handle: int) -> tuple[int, int, int]:
|
|
503
|
+
from ctypes import wintypes
|
|
504
|
+
|
|
505
|
+
class ByHandleFileInformation(ctypes.Structure):
|
|
506
|
+
_fields_ = [
|
|
507
|
+
("file_attributes", wintypes.DWORD),
|
|
508
|
+
("creation_time", wintypes.FILETIME),
|
|
509
|
+
("last_access_time", wintypes.FILETIME),
|
|
510
|
+
("last_write_time", wintypes.FILETIME),
|
|
511
|
+
("volume_serial_number", wintypes.DWORD),
|
|
512
|
+
("file_size_high", wintypes.DWORD),
|
|
513
|
+
("file_size_low", wintypes.DWORD),
|
|
514
|
+
("number_of_links", wintypes.DWORD),
|
|
515
|
+
("file_index_high", wintypes.DWORD),
|
|
516
|
+
("file_index_low", wintypes.DWORD),
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
520
|
+
function = kernel32.GetFileInformationByHandle
|
|
521
|
+
function.argtypes = [wintypes.HANDLE, ctypes.POINTER(ByHandleFileInformation)]
|
|
522
|
+
function.restype = wintypes.BOOL
|
|
523
|
+
information = ByHandleFileInformation()
|
|
524
|
+
if not function(handle, ctypes.byref(information)):
|
|
525
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
526
|
+
return (
|
|
527
|
+
information.volume_serial_number,
|
|
528
|
+
information.file_index_high,
|
|
529
|
+
information.file_index_low,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _windows_handle_final_path(handle: int) -> Path:
|
|
534
|
+
from ctypes import wintypes
|
|
535
|
+
|
|
536
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
537
|
+
function = kernel32.GetFinalPathNameByHandleW
|
|
538
|
+
function.argtypes = [wintypes.HANDLE, wintypes.LPWSTR, wintypes.DWORD, wintypes.DWORD]
|
|
539
|
+
function.restype = wintypes.DWORD
|
|
540
|
+
buffer = ctypes.create_unicode_buffer(32768)
|
|
541
|
+
length = function(handle, buffer, len(buffer), 0)
|
|
542
|
+
if not length or length >= len(buffer):
|
|
543
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
544
|
+
value = buffer.value
|
|
545
|
+
if value.startswith("\\\\?\\UNC\\"):
|
|
546
|
+
value = "\\\\" + value[8:]
|
|
547
|
+
elif value.startswith("\\\\?\\"):
|
|
548
|
+
value = value[4:]
|
|
549
|
+
return Path(value).resolve(strict=True)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _assert_handle_matches_snapshot(
|
|
553
|
+
handle: int,
|
|
554
|
+
handle_identity: tuple[int, int, int],
|
|
555
|
+
snapshot: _PathSnapshot,
|
|
556
|
+
) -> None:
|
|
557
|
+
if os.name == "nt":
|
|
558
|
+
file_index = (handle_identity[1] << 32) | handle_identity[2]
|
|
559
|
+
if file_index != snapshot.identity[1]:
|
|
560
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
561
|
+
final_path = _windows_handle_final_path(handle)
|
|
562
|
+
if os.path.normcase(str(final_path)) != os.path.normcase(str(snapshot.canonical)):
|
|
563
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
564
|
+
return
|
|
565
|
+
info = os.fstat(handle)
|
|
566
|
+
if (info.st_dev, info.st_ino) != snapshot.identity:
|
|
567
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _reject_windows_handle_reparse(handle: int) -> None:
|
|
571
|
+
from ctypes import wintypes
|
|
572
|
+
|
|
573
|
+
class FileAttributeTagInformation(ctypes.Structure):
|
|
574
|
+
_fields_ = [("file_attributes", wintypes.DWORD), ("reparse_tag", wintypes.DWORD)]
|
|
575
|
+
|
|
576
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
577
|
+
function = kernel32.GetFileInformationByHandleEx
|
|
578
|
+
function.argtypes = [wintypes.HANDLE, ctypes.c_int, wintypes.LPVOID, wintypes.DWORD]
|
|
579
|
+
function.restype = wintypes.BOOL
|
|
580
|
+
information = FileAttributeTagInformation()
|
|
581
|
+
if not function(handle, 9, ctypes.byref(information), ctypes.sizeof(information)):
|
|
582
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
583
|
+
if information.file_attributes & 0x400 or information.reparse_tag:
|
|
584
|
+
raise WorkspaceLeaseError("workspace_isolation_failed")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _windows_path_is_reparse(path: Path) -> bool:
|
|
588
|
+
if os.name != "nt":
|
|
589
|
+
return False
|
|
590
|
+
from ctypes import wintypes
|
|
591
|
+
|
|
592
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
593
|
+
function = kernel32.GetFileAttributesW
|
|
594
|
+
function.argtypes = [wintypes.LPCWSTR]
|
|
595
|
+
function.restype = wintypes.DWORD
|
|
596
|
+
attributes = function(str(path))
|
|
597
|
+
if attributes == 0xFFFFFFFF:
|
|
598
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
599
|
+
return bool(attributes & 0x400)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _windows_private_security_descriptor() -> int:
|
|
603
|
+
"""Allocate a protected DACL granting full control only to the current user."""
|
|
604
|
+
from ctypes import wintypes
|
|
605
|
+
|
|
606
|
+
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
607
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
608
|
+
open_process_token = advapi32.OpenProcessToken
|
|
609
|
+
open_process_token.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)]
|
|
610
|
+
open_process_token.restype = wintypes.BOOL
|
|
611
|
+
get_token_information = advapi32.GetTokenInformation
|
|
612
|
+
get_token_information.argtypes = [
|
|
613
|
+
wintypes.HANDLE,
|
|
614
|
+
ctypes.c_int,
|
|
615
|
+
wintypes.LPVOID,
|
|
616
|
+
wintypes.DWORD,
|
|
617
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
618
|
+
]
|
|
619
|
+
get_token_information.restype = wintypes.BOOL
|
|
620
|
+
convert_sid = advapi32.ConvertSidToStringSidW
|
|
621
|
+
convert_sid.argtypes = [wintypes.LPVOID, ctypes.POINTER(wintypes.LPWSTR)]
|
|
622
|
+
convert_sid.restype = wintypes.BOOL
|
|
623
|
+
convert_descriptor = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW
|
|
624
|
+
convert_descriptor.argtypes = [
|
|
625
|
+
wintypes.LPCWSTR,
|
|
626
|
+
wintypes.DWORD,
|
|
627
|
+
ctypes.POINTER(wintypes.LPVOID),
|
|
628
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
629
|
+
]
|
|
630
|
+
convert_descriptor.restype = wintypes.BOOL
|
|
631
|
+
local_free = kernel32.LocalFree
|
|
632
|
+
local_free.argtypes = [wintypes.HLOCAL]
|
|
633
|
+
local_free.restype = wintypes.HLOCAL
|
|
634
|
+
get_current_process = kernel32.GetCurrentProcess
|
|
635
|
+
get_current_process.argtypes = []
|
|
636
|
+
get_current_process.restype = wintypes.HANDLE
|
|
637
|
+
|
|
638
|
+
token = wintypes.HANDLE()
|
|
639
|
+
if not open_process_token(get_current_process(), 0x0008, ctypes.byref(token)):
|
|
640
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
641
|
+
sid_text = wintypes.LPWSTR()
|
|
642
|
+
descriptor = wintypes.LPVOID()
|
|
643
|
+
try:
|
|
644
|
+
needed = wintypes.DWORD()
|
|
645
|
+
get_token_information(token, 1, None, 0, ctypes.byref(needed))
|
|
646
|
+
if not needed.value:
|
|
647
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
648
|
+
buffer = ctypes.create_string_buffer(needed.value)
|
|
649
|
+
if not get_token_information(token, 1, buffer, needed, ctypes.byref(needed)):
|
|
650
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
651
|
+
sid = ctypes.cast(buffer, ctypes.POINTER(wintypes.LPVOID))[0]
|
|
652
|
+
if not convert_sid(sid, ctypes.byref(sid_text)):
|
|
653
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
654
|
+
sddl = f"D:P(A;OICI;FA;;;{sid_text.value})"
|
|
655
|
+
if not convert_descriptor(sddl, 1, ctypes.byref(descriptor), None):
|
|
656
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
657
|
+
result = int(descriptor.value)
|
|
658
|
+
descriptor = wintypes.LPVOID()
|
|
659
|
+
return result
|
|
660
|
+
finally:
|
|
661
|
+
if descriptor:
|
|
662
|
+
local_free(descriptor)
|
|
663
|
+
if sid_text:
|
|
664
|
+
local_free(sid_text)
|
|
665
|
+
_close_raw_handle(int(token.value) if token.value else None)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _free_windows_security_descriptor(descriptor: int) -> None:
|
|
669
|
+
from ctypes import wintypes
|
|
670
|
+
|
|
671
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
672
|
+
local_free = kernel32.LocalFree
|
|
673
|
+
local_free.argtypes = [wintypes.HLOCAL]
|
|
674
|
+
local_free.restype = wintypes.HLOCAL
|
|
675
|
+
local_free(descriptor)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _create_private_windows_directory(parent: Path, name: str | None = None) -> Path:
|
|
679
|
+
"""Create a Windows directory with its protected DACL applied atomically."""
|
|
680
|
+
from ctypes import wintypes
|
|
681
|
+
|
|
682
|
+
class SecurityAttributes(ctypes.Structure):
|
|
683
|
+
_fields_ = [
|
|
684
|
+
("length", wintypes.DWORD),
|
|
685
|
+
("security_descriptor", wintypes.LPVOID),
|
|
686
|
+
("inherit_handle", wintypes.BOOL),
|
|
687
|
+
]
|
|
688
|
+
|
|
689
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
690
|
+
create_directory = kernel32.CreateDirectoryW
|
|
691
|
+
create_directory.argtypes = [wintypes.LPCWSTR, ctypes.POINTER(SecurityAttributes)]
|
|
692
|
+
create_directory.restype = wintypes.BOOL
|
|
693
|
+
descriptor = _windows_private_security_descriptor()
|
|
694
|
+
attributes = SecurityAttributes(
|
|
695
|
+
ctypes.sizeof(SecurityAttributes),
|
|
696
|
+
ctypes.c_void_p(descriptor),
|
|
697
|
+
False,
|
|
698
|
+
)
|
|
699
|
+
try:
|
|
700
|
+
for _ in range(64):
|
|
701
|
+
child_name = name or f"graphite-doctor-{secrets.token_hex(16)}"
|
|
702
|
+
path = parent / child_name
|
|
703
|
+
if create_directory(str(path), ctypes.byref(attributes)):
|
|
704
|
+
return path
|
|
705
|
+
error = ctypes.get_last_error()
|
|
706
|
+
if name is not None or error != 183:
|
|
707
|
+
raise ctypes.WinError(error)
|
|
708
|
+
raise FileExistsError("unable to allocate a unique probe workspace parent")
|
|
709
|
+
finally:
|
|
710
|
+
_free_windows_security_descriptor(descriptor)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _set_private_windows_dacl(path: Path) -> None:
|
|
714
|
+
"""Apply the protected DACL to an already identity-verified injected path."""
|
|
715
|
+
if os.name != "nt":
|
|
716
|
+
return
|
|
717
|
+
from ctypes import wintypes
|
|
718
|
+
|
|
719
|
+
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
720
|
+
set_file_security = advapi32.SetFileSecurityW
|
|
721
|
+
set_file_security.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.LPVOID]
|
|
722
|
+
set_file_security.restype = wintypes.BOOL
|
|
723
|
+
descriptor = _windows_private_security_descriptor()
|
|
724
|
+
try:
|
|
725
|
+
if not set_file_security(str(path), 0x80000004, descriptor):
|
|
726
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
727
|
+
finally:
|
|
728
|
+
_free_windows_security_descriptor(descriptor)
|