sequential-hooks 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.
- plugins/agy/_sequential_hooks/__init__.py +1 -0
- plugins/agy/_sequential_hooks/_adapter.py +566 -0
- plugins/agy/_sequential_hooks/_doctor.py +413 -0
- plugins/claude/_sequential_hooks/__init__.py +1 -0
- plugins/claude/_sequential_hooks/_adapter.py +560 -0
- plugins/claude/_sequential_hooks/_doctor.py +455 -0
- plugins/codex/_sequential_hooks/__init__.py +1 -0
- plugins/codex/_sequential_hooks/_adapter.py +466 -0
- plugins/codex/_sequential_hooks/_doctor.py +563 -0
- sequential_hooks/__init__.py +3 -0
- sequential_hooks/__main__.py +5 -0
- sequential_hooks/_arguments.py +222 -0
- sequential_hooks/_cleanup.py +88 -0
- sequential_hooks/_cli.py +440 -0
- sequential_hooks/_containment/__init__.py +410 -0
- sequential_hooks/_containment/_posix.py +236 -0
- sequential_hooks/_containment/_uncontained.py +239 -0
- sequential_hooks/_containment/_windows.py +853 -0
- sequential_hooks/_containment/_windows_api.py +570 -0
- sequential_hooks/_containment/_windows_launcher.py +189 -0
- sequential_hooks/_diagnostics.py +265 -0
- sequential_hooks/_doctor/__init__.py +342 -0
- sequential_hooks/_doctor/_command.py +469 -0
- sequential_hooks/_doctor/_common.py +448 -0
- sequential_hooks/_doctor/_types.py +94 -0
- sequential_hooks/_downstream.py +181 -0
- sequential_hooks/_executable.py +187 -0
- sequential_hooks/_executor.py +825 -0
- sequential_hooks/_registry.py +103 -0
- sequential_hooks/_runner.py +48 -0
- sequential_hooks/_types.py +125 -0
- sequential_hooks/hosts/__init__.py +130 -0
- sequential_hooks/hosts/_contract.py +434 -0
- sequential_hooks/hosts/_inspection.py +78 -0
- sequential_hooks/hosts/_json.py +150 -0
- sequential_hooks/hosts/_records.py +273 -0
- sequential_hooks/hosts/_skeleton.py +1326 -0
- sequential_hooks/py.typed +0 -0
- sequential_hooks-0.1.0.dist-info/METADATA +90 -0
- sequential_hooks-0.1.0.dist-info/RECORD +43 -0
- sequential_hooks-0.1.0.dist-info/WHEEL +4 -0
- sequential_hooks-0.1.0.dist-info/entry_points.txt +7 -0
- sequential_hooks-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
|
|
3
|
+
"""Analyze shared doctor registration and platform contracts."""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import cast
|
|
12
|
+
|
|
13
|
+
from sequential_hooks._containment import probe_current_containment
|
|
14
|
+
from sequential_hooks._doctor._command import is_windows_remote_executable
|
|
15
|
+
from sequential_hooks._doctor._types import (
|
|
16
|
+
ContractError,
|
|
17
|
+
Finding,
|
|
18
|
+
FindingSeverity,
|
|
19
|
+
InspectedCommand,
|
|
20
|
+
PlatformReport,
|
|
21
|
+
Registration,
|
|
22
|
+
)
|
|
23
|
+
from sequential_hooks._executable import ExecutableStatus, resolve_executable
|
|
24
|
+
from sequential_hooks.hosts import HostSpec
|
|
25
|
+
|
|
26
|
+
_ALL_MATCHERS = frozenset({'', '*', '.*'})
|
|
27
|
+
_MINIMUM_OUTER_TIMEOUT = 5.0
|
|
28
|
+
_SAFE_SHIM_BASENAME_LIMIT = 40
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class _ExecutableEnvironment:
|
|
33
|
+
"""Describe the environment used to resolve inspected executables."""
|
|
34
|
+
|
|
35
|
+
_path_ext: str | None
|
|
36
|
+
_path_value: str | None
|
|
37
|
+
_windows: bool
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _expected_contract_category(
|
|
41
|
+
category: ContractError,
|
|
42
|
+
*,
|
|
43
|
+
requires_event: bool,
|
|
44
|
+
) -> ContractError:
|
|
45
|
+
"""Prioritize the expected event-requiring host over an embedded host's event rule."""
|
|
46
|
+
if requires_event and category is ContractError.EVENT_FORBIDDEN:
|
|
47
|
+
return ContractError.WRONG_HOST
|
|
48
|
+
return category
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _finding(
|
|
52
|
+
severity: FindingSeverity,
|
|
53
|
+
code: str,
|
|
54
|
+
message: str,
|
|
55
|
+
correction: str | None = None,
|
|
56
|
+
) -> Finding:
|
|
57
|
+
"""Build one bounded finding."""
|
|
58
|
+
return Finding(severity, code, message, correction)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _contract_finding(
|
|
62
|
+
category: ContractError,
|
|
63
|
+
spec: HostSpec,
|
|
64
|
+
registration: Registration,
|
|
65
|
+
) -> Finding:
|
|
66
|
+
"""Build shared wrapper-contract text for complete and partial invocations."""
|
|
67
|
+
corrections = {
|
|
68
|
+
ContractError.EVENT_FORBIDDEN: (f'Use --agent {spec.name} and remove the --event option.'),
|
|
69
|
+
ContractError.EVENT_MISSING: f'Use --event {registration.event}.',
|
|
70
|
+
ContractError.HOST_MISSING: f'Use --agent {spec.name}.',
|
|
71
|
+
ContractError.WRONG_EVENT: f'Use --event {registration.event}.',
|
|
72
|
+
ContractError.WRONG_HOST: f'Use --agent {spec.name}.',
|
|
73
|
+
}
|
|
74
|
+
messages = {
|
|
75
|
+
ContractError.EVENT_FORBIDDEN: 'wrapper event is not accepted for this agent',
|
|
76
|
+
ContractError.EVENT_MISSING: 'wrapper event is missing',
|
|
77
|
+
ContractError.HOST_MISSING: 'wrapper agent is missing',
|
|
78
|
+
ContractError.WRONG_EVENT: 'wrapper event does not match the registered event',
|
|
79
|
+
ContractError.WRONG_HOST: 'wrapper agent does not match the configured agent',
|
|
80
|
+
}
|
|
81
|
+
return _finding(
|
|
82
|
+
FindingSeverity.ERROR,
|
|
83
|
+
category.value,
|
|
84
|
+
messages[category],
|
|
85
|
+
corrections[category],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _invocation_contract(
|
|
90
|
+
spec: HostSpec,
|
|
91
|
+
registration: Registration,
|
|
92
|
+
invocation_host: str,
|
|
93
|
+
invocation_event: str | None,
|
|
94
|
+
) -> tuple[Finding, ...]:
|
|
95
|
+
"""Analyze one wrapper agent and event contract."""
|
|
96
|
+
findings: list[Finding] = []
|
|
97
|
+
if invocation_host != spec.name:
|
|
98
|
+
findings.append(_contract_finding(ContractError.WRONG_HOST, spec, registration))
|
|
99
|
+
if registration.event not in spec.events:
|
|
100
|
+
findings.append(
|
|
101
|
+
_finding(
|
|
102
|
+
FindingSeverity.ERROR,
|
|
103
|
+
'native_event_unsupported',
|
|
104
|
+
'wrapper is registered under a native event it cannot serve',
|
|
105
|
+
f'Register the sequential-hooks wrapper only under {", ".join(spec.events)}.',
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return tuple(findings)
|
|
109
|
+
if spec.requires_event and invocation_event is None:
|
|
110
|
+
findings.append(_contract_finding(ContractError.EVENT_MISSING, spec, registration))
|
|
111
|
+
elif spec.requires_event and invocation_event != registration.event:
|
|
112
|
+
findings.append(_contract_finding(ContractError.WRONG_EVENT, spec, registration))
|
|
113
|
+
elif not spec.requires_event and invocation_event is not None:
|
|
114
|
+
findings.append(_contract_finding(ContractError.EVENT_FORBIDDEN, spec, registration))
|
|
115
|
+
return tuple(findings)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _matcher_overlap(first: str | None, second: str | None) -> str:
|
|
119
|
+
"""Classify a matcher pair without evaluating either expression."""
|
|
120
|
+
if first == second or first is None or second is None:
|
|
121
|
+
return 'definite'
|
|
122
|
+
if first in _ALL_MATCHERS or second in _ALL_MATCHERS:
|
|
123
|
+
return 'definite'
|
|
124
|
+
return 'possible'
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _registration_state(spec: HostSpec, registration: Registration) -> tuple[Finding, ...]:
|
|
128
|
+
"""Analyze one registration's enabled and outer-timeout state."""
|
|
129
|
+
findings: list[Finding] = []
|
|
130
|
+
timeout = cast('object', registration.timeout)
|
|
131
|
+
if registration.enabled is False:
|
|
132
|
+
findings.append(
|
|
133
|
+
_finding(FindingSeverity.INFO, 'registration_disabled', 'registration is disabled')
|
|
134
|
+
)
|
|
135
|
+
elif registration.enabled is None:
|
|
136
|
+
findings.append(
|
|
137
|
+
_finding(
|
|
138
|
+
FindingSeverity.WARNING,
|
|
139
|
+
'enabled_unknown',
|
|
140
|
+
'registration enabled state cannot be inspected statically',
|
|
141
|
+
(
|
|
142
|
+
'Reconcile disableAllHooks values in the discovered Claude settings sources.'
|
|
143
|
+
if spec.name == 'claude'
|
|
144
|
+
else 'Reconcile hook activation settings in the discovered sources.'
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
if timeout is None:
|
|
149
|
+
findings.append(
|
|
150
|
+
_finding(
|
|
151
|
+
FindingSeverity.WARNING,
|
|
152
|
+
'outer_timeout_missing',
|
|
153
|
+
'outer handler timeout is missing',
|
|
154
|
+
f'Set an outer handler timeout of at least {_MINIMUM_OUTER_TIMEOUT:g} seconds.',
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
elif not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout <= 0:
|
|
158
|
+
findings.append(
|
|
159
|
+
_finding(
|
|
160
|
+
FindingSeverity.ERROR,
|
|
161
|
+
'outer_timeout_malformed',
|
|
162
|
+
'outer handler timeout is not a positive finite number',
|
|
163
|
+
f'Set an outer handler timeout of at least {_MINIMUM_OUTER_TIMEOUT:g} seconds.',
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
elif timeout < _MINIMUM_OUTER_TIMEOUT:
|
|
167
|
+
findings.append(
|
|
168
|
+
_finding(
|
|
169
|
+
FindingSeverity.WARNING,
|
|
170
|
+
'outer_timeout_too_small',
|
|
171
|
+
'outer handler timeout may expire before cleanup completes',
|
|
172
|
+
f'Set an outer handler timeout of at least {_MINIMUM_OUTER_TIMEOUT:g} seconds.',
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return tuple(findings)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _safe_shim_path(resolved: Path | None, *, wrapper: bool) -> str:
|
|
179
|
+
"""Return a bounded shim identifier without directory disclosure."""
|
|
180
|
+
if resolved is None:
|
|
181
|
+
return '[redacted]/[shim]'
|
|
182
|
+
name = resolved.name
|
|
183
|
+
suffix = resolved.suffix.casefold()
|
|
184
|
+
if wrapper and name.casefold() in {
|
|
185
|
+
'sequential-hooks.bat',
|
|
186
|
+
'sequential-hooks.cmd',
|
|
187
|
+
'sequential-hooks.ps1',
|
|
188
|
+
}:
|
|
189
|
+
basename = name[:_SAFE_SHIM_BASENAME_LIMIT]
|
|
190
|
+
else:
|
|
191
|
+
basename = f'[shim]{suffix}' if suffix in {'.bat', '.cmd', '.ps1'} else '[shim]'
|
|
192
|
+
return f'[redacted]/{basename}'
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _analyze_executable(
|
|
196
|
+
executable: str,
|
|
197
|
+
environment: _ExecutableEnvironment,
|
|
198
|
+
*,
|
|
199
|
+
wrapper: bool,
|
|
200
|
+
) -> Finding | None:
|
|
201
|
+
"""Return one executable resolution finding, if needed."""
|
|
202
|
+
if environment._windows and is_windows_remote_executable(executable):
|
|
203
|
+
return _finding(
|
|
204
|
+
FindingSeverity.WARNING,
|
|
205
|
+
'command_uninspectable',
|
|
206
|
+
'remote or device executable is not inspected statically',
|
|
207
|
+
'Use a local executable path for static inspection; '
|
|
208
|
+
'verify the remote or device command separately.',
|
|
209
|
+
)
|
|
210
|
+
resolution = resolve_executable(
|
|
211
|
+
executable,
|
|
212
|
+
path_value=environment._path_value,
|
|
213
|
+
windows=environment._windows,
|
|
214
|
+
pathext_value=environment._path_ext,
|
|
215
|
+
)
|
|
216
|
+
label = 'wrapper' if wrapper else 'child'
|
|
217
|
+
if resolution.status is ExecutableStatus.NOT_FOUND:
|
|
218
|
+
return _finding(
|
|
219
|
+
FindingSeverity.ERROR,
|
|
220
|
+
f'{label}_executable_missing',
|
|
221
|
+
f'{label} executable cannot be resolved',
|
|
222
|
+
f'Install or configure a runnable {label} executable.',
|
|
223
|
+
)
|
|
224
|
+
if resolution.status is ExecutableStatus.UNSUPPORTED_SHIM:
|
|
225
|
+
safe_path = _safe_shim_path(resolution.resolved, wrapper=wrapper)
|
|
226
|
+
return _finding(
|
|
227
|
+
FindingSeverity.ERROR,
|
|
228
|
+
f'{label}_executable_unsupported',
|
|
229
|
+
f'{label} executable resolves to an unsupported Windows shim at {safe_path}',
|
|
230
|
+
'Replace the Windows batch shim with a real .exe or .com executable entry point.',
|
|
231
|
+
)
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _analyze_invocation(
|
|
236
|
+
spec: HostSpec,
|
|
237
|
+
registration: Registration,
|
|
238
|
+
inspected: InspectedCommand,
|
|
239
|
+
environment: _ExecutableEnvironment,
|
|
240
|
+
) -> tuple[Finding, ...]:
|
|
241
|
+
"""Analyze one statically inspected registration."""
|
|
242
|
+
findings: list[Finding] = []
|
|
243
|
+
if inspected.dynamic:
|
|
244
|
+
findings.append(
|
|
245
|
+
_finding(
|
|
246
|
+
FindingSeverity.WARNING,
|
|
247
|
+
'command_dynamic',
|
|
248
|
+
'dynamic command cannot be judged healthy statically',
|
|
249
|
+
'Use a loader-documented literal launch form.',
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
return tuple(findings)
|
|
253
|
+
if inspected.contract_error is not None:
|
|
254
|
+
category = _expected_contract_category(
|
|
255
|
+
inspected.contract_error,
|
|
256
|
+
requires_event=spec.requires_event,
|
|
257
|
+
)
|
|
258
|
+
findings.append(_contract_finding(category, spec, registration))
|
|
259
|
+
return tuple(findings)
|
|
260
|
+
if inspected.error is not None:
|
|
261
|
+
findings.append(
|
|
262
|
+
_finding(
|
|
263
|
+
FindingSeverity.WARNING,
|
|
264
|
+
'command_uninspectable',
|
|
265
|
+
'registration command cannot be inspected statically',
|
|
266
|
+
'Use a loader-documented literal launch form '
|
|
267
|
+
'and check the wrapper argument grammar.',
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
return tuple(findings)
|
|
271
|
+
if inspected.argv is None:
|
|
272
|
+
return tuple(findings)
|
|
273
|
+
|
|
274
|
+
wrapper = inspected.invocation is not None
|
|
275
|
+
executable_finding = _analyze_executable(
|
|
276
|
+
inspected.argv[0],
|
|
277
|
+
environment,
|
|
278
|
+
wrapper=wrapper,
|
|
279
|
+
)
|
|
280
|
+
if executable_finding is not None:
|
|
281
|
+
findings.append(executable_finding)
|
|
282
|
+
if inspected.invocation is None:
|
|
283
|
+
return tuple(findings)
|
|
284
|
+
|
|
285
|
+
invocation = inspected.invocation
|
|
286
|
+
findings.extend(
|
|
287
|
+
_invocation_contract(
|
|
288
|
+
spec,
|
|
289
|
+
registration,
|
|
290
|
+
invocation.host,
|
|
291
|
+
invocation.event,
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
for step in invocation.steps:
|
|
295
|
+
child_finding = _analyze_executable(
|
|
296
|
+
step.argv[0],
|
|
297
|
+
environment,
|
|
298
|
+
wrapper=False,
|
|
299
|
+
)
|
|
300
|
+
if child_finding is not None:
|
|
301
|
+
findings.append(child_finding)
|
|
302
|
+
if invocation.step_timeout is None:
|
|
303
|
+
findings.append(
|
|
304
|
+
_finding(
|
|
305
|
+
FindingSeverity.INFO,
|
|
306
|
+
'outer_timeout_only',
|
|
307
|
+
'no internal step timeout is configured; only the outer handler timeout applies',
|
|
308
|
+
'Add --step-timeout if each child needs an independent bound.',
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
return tuple(findings)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _same_child_argv(
|
|
315
|
+
first: tuple[str, ...],
|
|
316
|
+
second: tuple[str, ...],
|
|
317
|
+
*,
|
|
318
|
+
windows: bool,
|
|
319
|
+
) -> bool:
|
|
320
|
+
"""Compare child vectors using only executable platform folding."""
|
|
321
|
+
if not windows:
|
|
322
|
+
return first == second
|
|
323
|
+
return first[0].casefold() == second[0].casefold() and first[1:] == second[1:]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def analyze_registration(
|
|
327
|
+
spec: HostSpec,
|
|
328
|
+
inspected_registration: tuple[Registration, InspectedCommand],
|
|
329
|
+
*,
|
|
330
|
+
path_value: str | None,
|
|
331
|
+
path_ext: str | None,
|
|
332
|
+
windows: bool,
|
|
333
|
+
) -> tuple[Finding, ...]:
|
|
334
|
+
"""Return state and invocation findings for one inspected registration.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
spec: Loaded host owning the event domain and event option rule.
|
|
338
|
+
inspected_registration: Registration and its existing command
|
|
339
|
+
inspection.
|
|
340
|
+
path_value: Search path used to resolve executables.
|
|
341
|
+
path_ext: Windows executable extensions used during resolution.
|
|
342
|
+
windows: Whether to use Windows launch and executable semantics.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Registration state findings followed by invocation findings.
|
|
346
|
+
"""
|
|
347
|
+
registration, inspected = inspected_registration
|
|
348
|
+
environment = _ExecutableEnvironment(
|
|
349
|
+
_path_ext=path_ext,
|
|
350
|
+
_path_value=path_value,
|
|
351
|
+
_windows=windows,
|
|
352
|
+
)
|
|
353
|
+
return (
|
|
354
|
+
*_registration_state(spec, registration),
|
|
355
|
+
*_analyze_invocation(
|
|
356
|
+
spec,
|
|
357
|
+
registration,
|
|
358
|
+
inspected,
|
|
359
|
+
environment,
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def analyze_registration_pairs(
|
|
365
|
+
inspected_registrations: Iterable[tuple[Registration, InspectedCommand]],
|
|
366
|
+
*,
|
|
367
|
+
windows: bool,
|
|
368
|
+
) -> tuple[Finding, ...]:
|
|
369
|
+
"""Return cross-registration findings from existing command inspections.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
inspected_registrations: Registrations and inspections in loader order.
|
|
373
|
+
windows: Whether executable comparisons use Windows semantics.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Findings for each earlier registration paired with each later one,
|
|
377
|
+
preserving input order.
|
|
378
|
+
"""
|
|
379
|
+
inspected_registrations = tuple(inspected_registrations)
|
|
380
|
+
findings: list[Finding] = []
|
|
381
|
+
for first_index, (first, first_inspected) in enumerate(inspected_registrations):
|
|
382
|
+
for second_index in range(first_index + 1, len(inspected_registrations)):
|
|
383
|
+
second, second_inspected = inspected_registrations[second_index]
|
|
384
|
+
if first.event != second.event:
|
|
385
|
+
continue
|
|
386
|
+
if first_inspected.invocation is not None and second_inspected.invocation is not None:
|
|
387
|
+
if first.matcher == second.matcher:
|
|
388
|
+
findings.append(
|
|
389
|
+
_finding(
|
|
390
|
+
FindingSeverity.WARNING,
|
|
391
|
+
'duplicate_wrapper',
|
|
392
|
+
'event has repeated wrapper registrations',
|
|
393
|
+
'Keep one wrapper registration for each event and matcher.',
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
overlap = _matcher_overlap(first.matcher, second.matcher)
|
|
397
|
+
findings.append(
|
|
398
|
+
_finding(
|
|
399
|
+
FindingSeverity.WARNING if overlap == 'definite' else FindingSeverity.INFO,
|
|
400
|
+
f'{overlap}_matcher_overlap',
|
|
401
|
+
f'{overlap} same-event matcher overlap',
|
|
402
|
+
'Make same-event matcher scopes disjoint.',
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
wrapper, literal = (
|
|
406
|
+
(first_inspected, second_inspected)
|
|
407
|
+
if first_inspected.invocation is not None
|
|
408
|
+
else (second_inspected, first_inspected)
|
|
409
|
+
)
|
|
410
|
+
if (
|
|
411
|
+
wrapper.invocation is None
|
|
412
|
+
or literal.invocation is not None
|
|
413
|
+
or literal.argv is None
|
|
414
|
+
):
|
|
415
|
+
continue
|
|
416
|
+
if any(
|
|
417
|
+
_same_child_argv(step.argv, literal.argv, windows=windows)
|
|
418
|
+
for step in wrapper.invocation.steps
|
|
419
|
+
):
|
|
420
|
+
findings.append(
|
|
421
|
+
_finding(
|
|
422
|
+
FindingSeverity.WARNING,
|
|
423
|
+
'duplicate_child_registration',
|
|
424
|
+
'literal child duplicates a wrapper step',
|
|
425
|
+
'Remove the separately registered literal child.',
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
return tuple(findings)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def inspect_platform() -> PlatformReport:
|
|
432
|
+
"""Report interpreter and static containment availability.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Current interpreter, containment classification, configuration result,
|
|
436
|
+
and bounded limitations.
|
|
437
|
+
"""
|
|
438
|
+
interpreter = platform.python_implementation()
|
|
439
|
+
containment, configurable, limitations = probe_current_containment(
|
|
440
|
+
implementation=interpreter,
|
|
441
|
+
platform_name=sys.platform,
|
|
442
|
+
)
|
|
443
|
+
return PlatformReport(
|
|
444
|
+
interpreter,
|
|
445
|
+
containment.value,
|
|
446
|
+
configurable,
|
|
447
|
+
limitations,
|
|
448
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Define the doctor report types and re-export the published record types."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from sequential_hooks.hosts import (
|
|
7
|
+
ConfigSource,
|
|
8
|
+
ContractError,
|
|
9
|
+
Finding,
|
|
10
|
+
FindingSeverity,
|
|
11
|
+
InspectedCommand,
|
|
12
|
+
LaunchSemantics,
|
|
13
|
+
Registration,
|
|
14
|
+
SourceKind,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'ConfigSource',
|
|
19
|
+
'ContractError',
|
|
20
|
+
'DoctorReport',
|
|
21
|
+
'Finding',
|
|
22
|
+
'FindingSeverity',
|
|
23
|
+
'InspectedCommand',
|
|
24
|
+
'LaunchSemantics',
|
|
25
|
+
'PlatformReport',
|
|
26
|
+
'Registration',
|
|
27
|
+
'SourceKind',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PlatformReport:
|
|
33
|
+
"""Describe an immutable static containment probe.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
interpreter: Running Python implementation name.
|
|
37
|
+
containment: Selected containment-platform classification.
|
|
38
|
+
job_object_configurable: Whether Windows Job Object containment can be
|
|
39
|
+
configured, or `None` when inapplicable.
|
|
40
|
+
limitations: Bounded containment limitations in report order.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
interpreter: Running Python implementation name.
|
|
44
|
+
containment: Selected containment-platform classification.
|
|
45
|
+
job_object_configurable: Whether Windows Job Object containment can be
|
|
46
|
+
configured, or `None` when inapplicable.
|
|
47
|
+
limitations: Bounded containment limitations in report order.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
interpreter: str
|
|
51
|
+
containment: str
|
|
52
|
+
job_object_configurable: bool | None
|
|
53
|
+
limitations: tuple[str, ...]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class DoctorReport:
|
|
58
|
+
"""Carry one immutable, ordered host inspection report.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
host: Host configuration contract that was inspected.
|
|
62
|
+
executable: Resolved wrapper path when found, including an unsupported
|
|
63
|
+
shim.
|
|
64
|
+
version: Installed package version, `0.0.0` without installed metadata,
|
|
65
|
+
or `None` when unavailable.
|
|
66
|
+
platform: Static containment probe result.
|
|
67
|
+
sources: Configuration sources in discovery order.
|
|
68
|
+
registrations: Accepted registrations in loader order.
|
|
69
|
+
findings: Source problems first, then findings grouped by registration,
|
|
70
|
+
then cross-registration and aggregate findings.
|
|
71
|
+
limitations: Host inspection limitations in report order.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
host: Host configuration contract that was inspected.
|
|
75
|
+
executable: Resolved wrapper path when found, including an unsupported
|
|
76
|
+
shim.
|
|
77
|
+
version: Installed package version, `0.0.0` without installed metadata,
|
|
78
|
+
or `None` when unavailable.
|
|
79
|
+
platform: Static containment probe result.
|
|
80
|
+
sources: Configuration sources in discovery order.
|
|
81
|
+
registrations: Accepted registrations in loader order.
|
|
82
|
+
findings: Source problems first, then findings grouped by registration,
|
|
83
|
+
then cross-registration and aggregate findings.
|
|
84
|
+
limitations: Host inspection limitations in report order.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
host: str
|
|
88
|
+
executable: Path | None
|
|
89
|
+
version: str | None
|
|
90
|
+
platform: PlatformReport
|
|
91
|
+
sources: tuple[ConfigSource, ...]
|
|
92
|
+
registrations: tuple[Registration, ...]
|
|
93
|
+
findings: tuple[Finding, ...]
|
|
94
|
+
limitations: tuple[str, ...]
|