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,469 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
|
|
3
|
+
"""Inspect registered hook commands without executing them."""
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from itertools import pairwise
|
|
10
|
+
from pathlib import Path, PureWindowsPath
|
|
11
|
+
|
|
12
|
+
from sequential_hooks._arguments import InvocationError, parse_hook_argv
|
|
13
|
+
from sequential_hooks._doctor._types import (
|
|
14
|
+
ContractError,
|
|
15
|
+
InspectedCommand,
|
|
16
|
+
LaunchSemantics,
|
|
17
|
+
Registration,
|
|
18
|
+
)
|
|
19
|
+
from sequential_hooks._registry import host_requirements
|
|
20
|
+
|
|
21
|
+
_ENVIRONMENT_PREFIX = re.compile(r'^\s*[A-Za-z_][A-Za-z0-9_]*=')
|
|
22
|
+
_NEGATION_PREFIX = re.compile(r'^\s*!(?:\s|$)')
|
|
23
|
+
_SECRET_SEGMENTS = frozenset({'apikey', 'credential', 'password', 'secret', 'token'})
|
|
24
|
+
_SUMMARY_LIMIT = 240
|
|
25
|
+
_WRAPPER_NAMES = frozenset({'sequential-hooks', 'sequential-hooks.exe'})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class _CommandLexState:
|
|
30
|
+
"""Retain lexical state while reading one raw command token."""
|
|
31
|
+
|
|
32
|
+
_equals: int | None
|
|
33
|
+
_escaped: bool
|
|
34
|
+
_logical: list[str]
|
|
35
|
+
_outer_quote_end: int | None
|
|
36
|
+
_quote: str | None
|
|
37
|
+
_value_start: int | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class _CommandToken:
|
|
42
|
+
"""Describe one raw command token and its lexical content."""
|
|
43
|
+
|
|
44
|
+
_start: int
|
|
45
|
+
_end: int
|
|
46
|
+
_content_end: int
|
|
47
|
+
_text: str
|
|
48
|
+
_value_start: int | None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class _ContractState:
|
|
53
|
+
"""Retain a safely parsed leading wrapper-option prefix."""
|
|
54
|
+
|
|
55
|
+
_allow_seen: bool = False
|
|
56
|
+
_event_seen: bool = False
|
|
57
|
+
_event: str | None = None
|
|
58
|
+
_host_seen: bool = False
|
|
59
|
+
_host: str | None = None
|
|
60
|
+
_timeout_seen: bool = False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _append_token_character(state: _CommandLexState, character: str, index: int) -> None:
|
|
64
|
+
"""Append one logical token character and retain its raw equals span."""
|
|
65
|
+
state._logical.append(character)
|
|
66
|
+
if character == '=' and state._equals is None:
|
|
67
|
+
state._equals = index
|
|
68
|
+
state._value_start = index + 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _consume_contract_option(
|
|
72
|
+
state: _ContractState,
|
|
73
|
+
option: str,
|
|
74
|
+
value: str,
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Consume one value-bearing option when it is well formed."""
|
|
77
|
+
if not value:
|
|
78
|
+
return False
|
|
79
|
+
valid = True
|
|
80
|
+
if option in {'-e', '--event'}:
|
|
81
|
+
if state._event_seen:
|
|
82
|
+
valid = False
|
|
83
|
+
else:
|
|
84
|
+
state._event_seen = True
|
|
85
|
+
state._event = value
|
|
86
|
+
elif option in {'-a', '--agent'}:
|
|
87
|
+
if state._host_seen:
|
|
88
|
+
valid = False
|
|
89
|
+
else:
|
|
90
|
+
state._host_seen = True
|
|
91
|
+
state._host = value
|
|
92
|
+
elif state._timeout_seen:
|
|
93
|
+
valid = False
|
|
94
|
+
else:
|
|
95
|
+
state._timeout_seen = True
|
|
96
|
+
try:
|
|
97
|
+
timeout = float(value)
|
|
98
|
+
except ValueError:
|
|
99
|
+
valid = False
|
|
100
|
+
else:
|
|
101
|
+
valid = math.isfinite(timeout) and timeout > 0
|
|
102
|
+
return valid
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _consume_token_character(
|
|
106
|
+
state: _CommandLexState,
|
|
107
|
+
character: str,
|
|
108
|
+
index: int,
|
|
109
|
+
*,
|
|
110
|
+
leading_quote: str | None,
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""Consume one source character or identify an unquoted token boundary."""
|
|
113
|
+
if state._escaped:
|
|
114
|
+
if character != '\n':
|
|
115
|
+
_append_token_character(state, character, index)
|
|
116
|
+
state._escaped = False
|
|
117
|
+
elif character == '\\' and state._quote != "'":
|
|
118
|
+
state._escaped = True
|
|
119
|
+
elif character in {"'", '"'} and state._quote in {None, character}:
|
|
120
|
+
if state._quote is None:
|
|
121
|
+
state._quote = character
|
|
122
|
+
else:
|
|
123
|
+
if state._value_start == index:
|
|
124
|
+
state._value_start = index + 1
|
|
125
|
+
if leading_quote == character and state._outer_quote_end is None:
|
|
126
|
+
state._outer_quote_end = index
|
|
127
|
+
state._quote = None
|
|
128
|
+
elif state._quote is None and character.isspace():
|
|
129
|
+
return False
|
|
130
|
+
else:
|
|
131
|
+
_append_token_character(state, character, index)
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _effective_command(registration: Registration, *, windows: bool) -> str:
|
|
136
|
+
"""Return the command active on the selected platform."""
|
|
137
|
+
if windows and registration.command_windows is not None:
|
|
138
|
+
return registration.command_windows
|
|
139
|
+
return registration.command
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _is_unquoted_dynamic(character: str, *, word_start: bool) -> bool:
|
|
143
|
+
"""Return whether one unquoted character requires shell evaluation."""
|
|
144
|
+
return character in {'(', ')', '|', '>', '<', ';', '&', '*', '?', '['} or (
|
|
145
|
+
character == '~' and word_start
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _leading_contract(
|
|
150
|
+
argv: tuple[str, ...],
|
|
151
|
+
) -> tuple[str | None, bool, str | None] | None:
|
|
152
|
+
"""Parse a complete leading wrapper-option prefix conservatively."""
|
|
153
|
+
index = 1
|
|
154
|
+
state = _ContractState()
|
|
155
|
+
valid = True
|
|
156
|
+
while valid and index < len(argv) and argv[index].startswith('-'):
|
|
157
|
+
option = argv[index]
|
|
158
|
+
if option == '--allow-on-failure':
|
|
159
|
+
valid = not state._allow_seen
|
|
160
|
+
state._allow_seen = True
|
|
161
|
+
index += 1
|
|
162
|
+
continue
|
|
163
|
+
has_value = index + 1 < len(argv) and not argv[index + 1].startswith('-')
|
|
164
|
+
if option not in {'-a', '-e', '--event', '--agent', '--step-timeout'} or not has_value:
|
|
165
|
+
valid = False
|
|
166
|
+
continue
|
|
167
|
+
valid = _consume_contract_option(state, option, argv[index + 1])
|
|
168
|
+
index += 2
|
|
169
|
+
if not valid or index >= len(argv):
|
|
170
|
+
return None
|
|
171
|
+
return state._host, state._event_seen, state._event
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _next_command_token(command: str, start: int) -> tuple[_CommandToken | None, int]:
|
|
175
|
+
"""Read one quote- and escape-aware token without normalizing source text."""
|
|
176
|
+
index = start
|
|
177
|
+
while index < len(command) and command[index].isspace():
|
|
178
|
+
index += 1
|
|
179
|
+
if index == len(command):
|
|
180
|
+
return None, index
|
|
181
|
+
|
|
182
|
+
token_start = index
|
|
183
|
+
leading_quote = command[index] if command[index] in {"'", '"'} else None
|
|
184
|
+
state = _CommandLexState(
|
|
185
|
+
_equals=None,
|
|
186
|
+
_escaped=False,
|
|
187
|
+
_logical=[],
|
|
188
|
+
_outer_quote_end=None,
|
|
189
|
+
_quote=None,
|
|
190
|
+
_value_start=None,
|
|
191
|
+
)
|
|
192
|
+
while index < len(command) and _consume_token_character(
|
|
193
|
+
state,
|
|
194
|
+
command[index],
|
|
195
|
+
index,
|
|
196
|
+
leading_quote=leading_quote,
|
|
197
|
+
):
|
|
198
|
+
index += 1
|
|
199
|
+
if state._escaped:
|
|
200
|
+
state._logical.append('\\')
|
|
201
|
+
|
|
202
|
+
content_end = index - (1 if state._outer_quote_end == index - 1 else 0)
|
|
203
|
+
value_start = None if state._value_start is None else min(state._value_start, content_end)
|
|
204
|
+
return (
|
|
205
|
+
_CommandToken(
|
|
206
|
+
_start=token_start,
|
|
207
|
+
_end=index,
|
|
208
|
+
_content_end=content_end,
|
|
209
|
+
_text=''.join(state._logical),
|
|
210
|
+
_value_start=value_start,
|
|
211
|
+
),
|
|
212
|
+
index,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _normalized_secret_name(name: str) -> str:
|
|
217
|
+
"""Fold separator spellings before secret-marker comparison."""
|
|
218
|
+
return name.casefold().replace('_', '-').replace('.', '-')
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _is_secret_assignment_name(name: str) -> bool:
|
|
222
|
+
"""Return whether one non-option assignment name carries a secret."""
|
|
223
|
+
if name.startswith('--') or any(character.isspace() for character in name):
|
|
224
|
+
return False
|
|
225
|
+
folded = _normalized_secret_name(name)
|
|
226
|
+
return any(
|
|
227
|
+
marker in folded
|
|
228
|
+
for marker in ('api-key', 'apikey', 'credential', 'password', 'secret', 'token')
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_secret_option(name: str) -> bool:
|
|
233
|
+
"""Return whether one complete long-option name carries a secret."""
|
|
234
|
+
if not name.startswith('--'):
|
|
235
|
+
return False
|
|
236
|
+
segments = _normalized_secret_name(name[2:]).split('-')
|
|
237
|
+
if not segments or segments[0] == 'no':
|
|
238
|
+
return False
|
|
239
|
+
stems = [
|
|
240
|
+
segment[:-1] if len(segment) > 1 and segment.endswith('s') else segment
|
|
241
|
+
for segment in segments
|
|
242
|
+
]
|
|
243
|
+
if any(stem in _SECRET_SEGMENTS for stem in stems):
|
|
244
|
+
return True
|
|
245
|
+
return any(first == 'api' and second == 'key' for first, second in pairwise(stems))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _redact_command_text(command: str) -> str:
|
|
249
|
+
"""Redact secret value spans found by one bounded lexical pass."""
|
|
250
|
+
spans: list[tuple[int, int]] = []
|
|
251
|
+
pending_secret_option = False
|
|
252
|
+
position = 0
|
|
253
|
+
while True:
|
|
254
|
+
token, position = _next_command_token(command, position)
|
|
255
|
+
if token is None:
|
|
256
|
+
break
|
|
257
|
+
if pending_secret_option:
|
|
258
|
+
pending_secret_option = False
|
|
259
|
+
if not token._text.startswith('-'):
|
|
260
|
+
spans.append((token._start, token._end))
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
if token._value_start is None:
|
|
264
|
+
pending_secret_option = _is_secret_option(token._text)
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
name = token._text.partition('=')[0]
|
|
268
|
+
if _is_secret_option(name) or _is_secret_assignment_name(name):
|
|
269
|
+
spans.append((token._value_start, token._content_end))
|
|
270
|
+
|
|
271
|
+
parts: list[str] = []
|
|
272
|
+
cursor = 0
|
|
273
|
+
for start, end in spans:
|
|
274
|
+
parts.extend((command[cursor:start], '[REDACTED]'))
|
|
275
|
+
cursor = end
|
|
276
|
+
parts.append(command[cursor:])
|
|
277
|
+
return ''.join(parts)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _static_posix_command(command: str) -> str | None:
|
|
281
|
+
"""Return the statically inspectable prefix, or none for dynamic syntax."""
|
|
282
|
+
if _ENVIRONMENT_PREFIX.match(command) or _NEGATION_PREFIX.match(command):
|
|
283
|
+
return None
|
|
284
|
+
comment_start: int | None = None
|
|
285
|
+
quote: str | None = None
|
|
286
|
+
escaped = False
|
|
287
|
+
word_start = True
|
|
288
|
+
for index, character in enumerate(command):
|
|
289
|
+
if character == '\n':
|
|
290
|
+
return None
|
|
291
|
+
if escaped or comment_start is not None:
|
|
292
|
+
escaped = False
|
|
293
|
+
word_start = False
|
|
294
|
+
continue
|
|
295
|
+
if character == '\\' and quote != "'":
|
|
296
|
+
escaped = True
|
|
297
|
+
continue
|
|
298
|
+
if character in {"'", '"'} and quote in {None, character}:
|
|
299
|
+
quote = character if quote is None else None
|
|
300
|
+
word_start = False
|
|
301
|
+
continue
|
|
302
|
+
if character == '#' and quote is None and word_start:
|
|
303
|
+
comment_start = index
|
|
304
|
+
continue
|
|
305
|
+
if (character in {'$', '`'} and quote != "'") or (
|
|
306
|
+
quote is None and _is_unquoted_dynamic(character, word_start=word_start)
|
|
307
|
+
):
|
|
308
|
+
return None
|
|
309
|
+
word_start = quote is None and character.isspace()
|
|
310
|
+
return command[:comment_start]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _wrapper_contract_error(argv: tuple[str, ...]) -> ContractError | None:
|
|
314
|
+
"""Classify only safely recognizable wrapper contract failures."""
|
|
315
|
+
contract = _leading_contract(argv)
|
|
316
|
+
if contract is None:
|
|
317
|
+
return None
|
|
318
|
+
host, event_seen, event = contract
|
|
319
|
+
hosts = host_requirements()
|
|
320
|
+
category: ContractError | None = None
|
|
321
|
+
if host is None:
|
|
322
|
+
category = ContractError.HOST_MISSING
|
|
323
|
+
elif host not in hosts:
|
|
324
|
+
category = ContractError.WRONG_HOST
|
|
325
|
+
elif hosts[host].requires_event:
|
|
326
|
+
if not event_seen:
|
|
327
|
+
category = ContractError.EVENT_MISSING
|
|
328
|
+
elif event not in hosts[host].events:
|
|
329
|
+
category = ContractError.WRONG_EVENT
|
|
330
|
+
elif event_seen:
|
|
331
|
+
category = ContractError.EVENT_FORBIDDEN
|
|
332
|
+
return category
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _wrapper_name(executable: str, *, windows: bool) -> str:
|
|
336
|
+
"""Return the platform-correct executable basename."""
|
|
337
|
+
if windows:
|
|
338
|
+
return PureWindowsPath(executable).name.casefold()
|
|
339
|
+
return Path(executable).name
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _inspect_argv(argv: tuple[str, ...], *, windows: bool) -> InspectedCommand:
|
|
343
|
+
"""Inspect one exact argument vector."""
|
|
344
|
+
if not argv or not argv[0]:
|
|
345
|
+
return InspectedCommand(
|
|
346
|
+
argv=argv or None,
|
|
347
|
+
invocation=None,
|
|
348
|
+
dynamic=False,
|
|
349
|
+
error='registration has no executable',
|
|
350
|
+
)
|
|
351
|
+
if _wrapper_name(argv[0], windows=windows) not in _WRAPPER_NAMES:
|
|
352
|
+
return InspectedCommand(argv=argv, invocation=None, dynamic=False, error=None)
|
|
353
|
+
try:
|
|
354
|
+
invocation = parse_hook_argv(argv[1:])
|
|
355
|
+
except InvocationError as error:
|
|
356
|
+
return InspectedCommand(
|
|
357
|
+
argv=argv,
|
|
358
|
+
invocation=None,
|
|
359
|
+
dynamic=False,
|
|
360
|
+
error=str(error),
|
|
361
|
+
contract_error=_wrapper_contract_error(argv),
|
|
362
|
+
)
|
|
363
|
+
return InspectedCommand(argv=argv, invocation=invocation, dynamic=False, error=None)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _inspect_direct(
|
|
367
|
+
registration: Registration,
|
|
368
|
+
command: str,
|
|
369
|
+
*,
|
|
370
|
+
windows: bool,
|
|
371
|
+
) -> InspectedCommand:
|
|
372
|
+
"""Inspect one loader-verified direct argument vector."""
|
|
373
|
+
if registration.args is None:
|
|
374
|
+
return InspectedCommand(
|
|
375
|
+
argv=None,
|
|
376
|
+
invocation=None,
|
|
377
|
+
dynamic=False,
|
|
378
|
+
error='direct argv registration is missing args',
|
|
379
|
+
)
|
|
380
|
+
return _inspect_argv((command, *registration.args), windows=windows)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _inspect_posix_shell(command: str) -> InspectedCommand:
|
|
384
|
+
"""Inspect loader-verified static POSIX shell text."""
|
|
385
|
+
static_command = _static_posix_command(command)
|
|
386
|
+
if static_command is None:
|
|
387
|
+
return InspectedCommand(
|
|
388
|
+
argv=None,
|
|
389
|
+
invocation=None,
|
|
390
|
+
dynamic=True,
|
|
391
|
+
error='command contains dynamic shell syntax',
|
|
392
|
+
)
|
|
393
|
+
try:
|
|
394
|
+
argv = tuple(shlex.split(static_command, posix=True))
|
|
395
|
+
except ValueError:
|
|
396
|
+
return InspectedCommand(
|
|
397
|
+
argv=None,
|
|
398
|
+
invocation=None,
|
|
399
|
+
dynamic=False,
|
|
400
|
+
error='command has malformed POSIX quoting',
|
|
401
|
+
)
|
|
402
|
+
return _inspect_argv(argv, windows=False)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def inspect_command(
|
|
406
|
+
registration: Registration,
|
|
407
|
+
*,
|
|
408
|
+
windows: bool,
|
|
409
|
+
) -> InspectedCommand:
|
|
410
|
+
"""Inspect one literal registration without evaluating it.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
registration: Host registration to inspect.
|
|
414
|
+
windows: Whether to apply the registration's Windows launch semantics.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Static command inspection or a bounded reason it cannot be inspected.
|
|
418
|
+
"""
|
|
419
|
+
if windows:
|
|
420
|
+
if registration.windows_launch_semantics is None:
|
|
421
|
+
return InspectedCommand(
|
|
422
|
+
argv=None,
|
|
423
|
+
invocation=None,
|
|
424
|
+
dynamic=False,
|
|
425
|
+
error='registration has no documented Windows launch form',
|
|
426
|
+
)
|
|
427
|
+
semantics = registration.windows_launch_semantics
|
|
428
|
+
command = _effective_command(registration, windows=True)
|
|
429
|
+
else:
|
|
430
|
+
semantics = registration.launch_semantics
|
|
431
|
+
command = _effective_command(registration, windows=False)
|
|
432
|
+
|
|
433
|
+
if semantics is LaunchSemantics.DIRECT_ARGV:
|
|
434
|
+
return _inspect_direct(registration, command, windows=windows)
|
|
435
|
+
if semantics is LaunchSemantics.POSIX_SHELL:
|
|
436
|
+
return _inspect_posix_shell(command)
|
|
437
|
+
return InspectedCommand(
|
|
438
|
+
argv=None,
|
|
439
|
+
invocation=None,
|
|
440
|
+
dynamic=False,
|
|
441
|
+
error=f'{semantics.value} launch semantics cannot be inspected statically',
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def is_windows_remote_executable(executable: str) -> bool:
|
|
446
|
+
"""Identify explicit Windows UNC or device tokens without filesystem access."""
|
|
447
|
+
return executable.replace('\\', '/').startswith('//')
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def redact_command(registration: Registration, *, windows: bool = False) -> str:
|
|
451
|
+
"""Return a safe registration summary for display.
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
registration: Host registration whose effective command is displayed.
|
|
455
|
+
windows: Whether to select the registration's Windows command.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Redacted and length-bounded command summary.
|
|
459
|
+
"""
|
|
460
|
+
command = _effective_command(registration, windows=windows)
|
|
461
|
+
if windows and is_windows_remote_executable(command):
|
|
462
|
+
return '[REDACTED] remote or device command'
|
|
463
|
+
if _ENVIRONMENT_PREFIX.match(command):
|
|
464
|
+
return '[REDACTED] environment command'
|
|
465
|
+
|
|
466
|
+
redacted = _redact_command_text(command)
|
|
467
|
+
if len(redacted) > _SUMMARY_LIMIT:
|
|
468
|
+
return f'{redacted[: _SUMMARY_LIMIT - 3]}...'
|
|
469
|
+
return redacted
|