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.
Files changed (43) hide show
  1. plugins/agy/_sequential_hooks/__init__.py +1 -0
  2. plugins/agy/_sequential_hooks/_adapter.py +566 -0
  3. plugins/agy/_sequential_hooks/_doctor.py +413 -0
  4. plugins/claude/_sequential_hooks/__init__.py +1 -0
  5. plugins/claude/_sequential_hooks/_adapter.py +560 -0
  6. plugins/claude/_sequential_hooks/_doctor.py +455 -0
  7. plugins/codex/_sequential_hooks/__init__.py +1 -0
  8. plugins/codex/_sequential_hooks/_adapter.py +466 -0
  9. plugins/codex/_sequential_hooks/_doctor.py +563 -0
  10. sequential_hooks/__init__.py +3 -0
  11. sequential_hooks/__main__.py +5 -0
  12. sequential_hooks/_arguments.py +222 -0
  13. sequential_hooks/_cleanup.py +88 -0
  14. sequential_hooks/_cli.py +440 -0
  15. sequential_hooks/_containment/__init__.py +410 -0
  16. sequential_hooks/_containment/_posix.py +236 -0
  17. sequential_hooks/_containment/_uncontained.py +239 -0
  18. sequential_hooks/_containment/_windows.py +853 -0
  19. sequential_hooks/_containment/_windows_api.py +570 -0
  20. sequential_hooks/_containment/_windows_launcher.py +189 -0
  21. sequential_hooks/_diagnostics.py +265 -0
  22. sequential_hooks/_doctor/__init__.py +342 -0
  23. sequential_hooks/_doctor/_command.py +469 -0
  24. sequential_hooks/_doctor/_common.py +448 -0
  25. sequential_hooks/_doctor/_types.py +94 -0
  26. sequential_hooks/_downstream.py +181 -0
  27. sequential_hooks/_executable.py +187 -0
  28. sequential_hooks/_executor.py +825 -0
  29. sequential_hooks/_registry.py +103 -0
  30. sequential_hooks/_runner.py +48 -0
  31. sequential_hooks/_types.py +125 -0
  32. sequential_hooks/hosts/__init__.py +130 -0
  33. sequential_hooks/hosts/_contract.py +434 -0
  34. sequential_hooks/hosts/_inspection.py +78 -0
  35. sequential_hooks/hosts/_json.py +150 -0
  36. sequential_hooks/hosts/_records.py +273 -0
  37. sequential_hooks/hosts/_skeleton.py +1326 -0
  38. sequential_hooks/py.typed +0 -0
  39. sequential_hooks-0.1.0.dist-info/METADATA +90 -0
  40. sequential_hooks-0.1.0.dist-info/RECORD +43 -0
  41. sequential_hooks-0.1.0.dist-info/WHEEL +4 -0
  42. sequential_hooks-0.1.0.dist-info/entry_points.txt +7 -0
  43. sequential_hooks-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,189 @@
1
+ """Launch one gated Windows child and report its status."""
2
+
3
+ _release = __import__('os').read(
4
+ (
5
+ _control_fd := __import__('msvcrt').open_osfhandle(
6
+ int(__import__('sys').argv[1]),
7
+ __import__('os').O_RDONLY | __import__('os').O_BINARY,
8
+ )
9
+ ),
10
+ 1,
11
+ )
12
+ if _release != b'R':
13
+ __import__('os').close(_control_fd)
14
+ raise SystemExit(0)
15
+
16
+ _status_fd = __import__('msvcrt').open_osfhandle(
17
+ int(__import__('sys').argv[2]),
18
+ __import__('os').O_WRONLY | __import__('os').O_BINARY,
19
+ )
20
+ _os = __import__('os')
21
+ try:
22
+ _contextlib = __import__('contextlib')
23
+ _ctypes = __import__('ctypes')
24
+ _json = __import__('json')
25
+ _signal = __import__('signal')
26
+ _subprocess = __import__('subprocess')
27
+ _typing = __import__('typing')
28
+ except ImportError:
29
+ _import_failure_message = b'launcher invariant failure'
30
+ _import_failure_frame = (
31
+ b'\x03' + len(_import_failure_message).to_bytes(4, 'little') + _import_failure_message
32
+ )
33
+ _import_failure_offset = 0
34
+ while _import_failure_offset < len(_import_failure_frame):
35
+ try:
36
+ _import_failure_written = _os.write(
37
+ _status_fd,
38
+ _import_failure_frame[_import_failure_offset:],
39
+ )
40
+ except OSError:
41
+ raise SystemExit(0) from None
42
+ if _import_failure_written <= 0:
43
+ raise SystemExit(0) from None
44
+ _import_failure_offset += _import_failure_written
45
+ for _descriptor in (_control_fd, _status_fd):
46
+ try:
47
+ _os.close(_descriptor)
48
+ except OSError:
49
+ continue
50
+ raise SystemExit(0) from None
51
+ _ARGV_LIMIT_BYTES = 1024 * 1024
52
+ _STATUS_LIMIT_BYTES = 4096
53
+ _control_state = [_control_fd]
54
+ _status_state = [_status_fd]
55
+ _status_written = [False]
56
+
57
+
58
+ class _InvariantReporter:
59
+ """Report and suppress post-release invariant exceptions as status 3."""
60
+
61
+ def __enter__(self) -> None:
62
+ """Begin guarded launcher execution."""
63
+
64
+ def __exit__(
65
+ self,
66
+ _error_type: type[BaseException] | None,
67
+ error: BaseException | None,
68
+ _traceback: object,
69
+ ) -> bool:
70
+ """Report and suppress one launcher invariant exception."""
71
+ if error is None:
72
+ return False
73
+ with _contextlib.suppress(BaseException):
74
+ _write_status(3, 'launcher invariant failure')
75
+ return True
76
+
77
+
78
+ def _close_control() -> None:
79
+ """Close the control descriptor before creating the real child."""
80
+ if _control_state[0] >= 0:
81
+ _os.close(_control_state[0])
82
+ _control_state[0] = -1
83
+
84
+
85
+ def _close_status() -> None:
86
+ """Close the status descriptor exactly once."""
87
+ if _status_state[0] >= 0:
88
+ _os.close(_status_state[0])
89
+ _status_state[0] = -1
90
+
91
+
92
+ def _ignore_break(_signum: int, _frame: object) -> None:
93
+ """Keep the launcher alive while its real child handles Ctrl+Break."""
94
+
95
+
96
+ def _read_exact(count: int) -> bytes:
97
+ """Read exactly one bounded control-frame component."""
98
+ data = bytearray()
99
+ while len(data) < count:
100
+ chunk = _os.read(_control_state[0], count - len(data))
101
+ if not chunk:
102
+ raise ValueError('truncated launcher control frame')
103
+ data.extend(chunk)
104
+ return bytes(data)
105
+
106
+
107
+ def _write_all(data: bytes) -> None:
108
+ """Write one exact status frame without reserving an exit value."""
109
+ offset = 0
110
+ while offset < len(data):
111
+ written = _os.write(_status_state[0], data[offset:])
112
+ if written <= 0:
113
+ raise OSError('launcher status write made no progress')
114
+ offset += written
115
+
116
+
117
+ def _write_status(status: int, message: str = '') -> None:
118
+ """Write one bounded status frame with sanitized diagnostic text."""
119
+ if _status_written[0]:
120
+ return
121
+ encoded = message.encode('utf-8')[: _STATUS_LIMIT_BYTES - 5]
122
+ while True:
123
+ try:
124
+ encoded.decode('utf-8')
125
+ except UnicodeDecodeError:
126
+ encoded = encoded[:-1]
127
+ continue
128
+ break
129
+ _write_all(bytes((status,)) + len(encoded).to_bytes(4, 'little') + encoded)
130
+ _status_written[0] = True
131
+
132
+
133
+ def _main() -> None:
134
+ """Validate the control frame, run one child, and relay exact status."""
135
+ argv_length = int.from_bytes(_read_exact(8), 'little')
136
+ if argv_length > _ARGV_LIMIT_BYTES:
137
+ raise ValueError('launcher argv frame exceeds its bound')
138
+ argv_value = _json.loads(_read_exact(argv_length).decode('utf-8'))
139
+ if not isinstance(argv_value, list):
140
+ raise TypeError('launcher argv must be a non-empty string list')
141
+ argv_items = _typing.cast(list[object], argv_value)
142
+ if not argv_items or any(not isinstance(value, str) for value in argv_items):
143
+ raise ValueError('launcher argv must be a non-empty string list')
144
+ argv = _typing.cast(list[str], argv_items)
145
+ if _os.read(_control_state[0], 1):
146
+ raise ValueError('launcher control frame contains trailing bytes')
147
+ _close_control()
148
+ break_signal = getattr(_signal, 'SIGBREAK', None)
149
+ if break_signal is not None:
150
+ _signal.signal(break_signal, _ignore_break)
151
+ kernel32 = _typing.cast(
152
+ _typing.Any,
153
+ vars(_ctypes)['WinDLL']('kernel32', use_last_error=True),
154
+ )
155
+ exit_process = kernel32.ExitProcess
156
+ exit_process.argtypes = [_ctypes.c_uint32]
157
+ exit_process.restype = None
158
+ try:
159
+ child = _subprocess.Popen(
160
+ argv,
161
+ stdin=0,
162
+ stdout=1,
163
+ stderr=2,
164
+ shell=False,
165
+ close_fds=True,
166
+ )
167
+ except FileNotFoundError:
168
+ _write_status(1, 'real step was not found')
169
+ return
170
+ except OSError:
171
+ _write_status(2, 'real step could not be spawned')
172
+ return
173
+ except ValueError:
174
+ _write_status(2, 'real step could not be spawned')
175
+ return
176
+ returncode = child.wait()
177
+ try:
178
+ _write_status(0)
179
+ _close_status()
180
+ finally:
181
+ exit_process(_ctypes.c_uint32(returncode).value)
182
+
183
+
184
+ with _InvariantReporter():
185
+ _main()
186
+ with _contextlib.suppress(OSError):
187
+ _close_control()
188
+ with _contextlib.suppress(OSError):
189
+ _close_status()
@@ -0,0 +1,265 @@
1
+ # pyright: reportPrivateUsage=false
2
+
3
+ """Write bounded, synchronized child diagnostics."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import threading
8
+ from collections import deque
9
+ from typing import TYPE_CHECKING
10
+
11
+ from sequential_hooks._downstream import is_downstream_closure, silence_descriptor
12
+
13
+ if TYPE_CHECKING:
14
+ from typing import TextIO
15
+
16
+ from sequential_hooks.hosts import Step
17
+
18
+ _STDERR_CHAIN_LIMIT_BYTES = 4 * 1024 * 1024
19
+ _STDERR_STEP_LIMIT_BYTES = 1024 * 1024
20
+
21
+
22
+ class _DiagnosticState:
23
+ """Own diagnostic output synchronization and the shared chain budget."""
24
+
25
+ def __init__(self, stream: TextIO) -> None:
26
+ """Initialize shared diagnostic budgets and synchronization."""
27
+ self._chain_bytes = 0
28
+ self._chain_notice_written = False
29
+ self._broken = False
30
+ self._output_draining = False
31
+ self._output_lines: deque[str] = deque()
32
+ self._state_lock = threading.Lock()
33
+ self._stream = stream
34
+
35
+ def _drain_output(self, owns_drain: bool) -> None:
36
+ """Drain queued lines when this caller owns diagnostic output."""
37
+ if not owns_drain:
38
+ return
39
+ completed = False
40
+ try:
41
+ while True:
42
+ with self._state_lock:
43
+ if not self._output_lines:
44
+ self._output_draining = False
45
+ completed = True
46
+ return
47
+ batch = self._output_lines.popleft()
48
+ if not self._broken:
49
+ try:
50
+ self._stream.write(batch)
51
+ self._stream.flush()
52
+ except OSError as error:
53
+ if not is_downstream_closure(error):
54
+ raise
55
+ self._broken = True
56
+ silence_descriptor(self._stream)
57
+ finally:
58
+ if not completed:
59
+ with self._state_lock:
60
+ self._output_draining = False
61
+
62
+ def _enqueue_lines_locked(self, lines: list[str]) -> bool:
63
+ """Queue complete lines and claim an idle drainer."""
64
+ if not lines:
65
+ return False
66
+ self._output_lines.append(''.join(lines))
67
+ if self._output_draining:
68
+ return False
69
+ self._output_draining = True
70
+ return True
71
+
72
+ def _enqueue_step_locked(self, step: Step, messages: list[str]) -> bool:
73
+ """Render and queue ordered step lines under the state lock."""
74
+ prefix = f'sequential-hooks: step {step.index} ({step.name}): '
75
+ lines = [prefix + message + '\n' for message in messages]
76
+ return self._enqueue_lines_locked(lines)
77
+
78
+ def _write_global(self, message: str) -> None:
79
+ """Queue one global line and drain it when output is idle."""
80
+ with self._state_lock:
81
+ owns_drain = self._enqueue_lines_locked(
82
+ [f'sequential-hooks: {message}\n'],
83
+ )
84
+ self._drain_output(owns_drain)
85
+
86
+ def _write_step(self, step: Step, messages: list[str]) -> None:
87
+ """Queue ordered step lines and drain them when output is idle."""
88
+ with self._state_lock:
89
+ owns_drain = self._enqueue_step_locked(step, messages)
90
+ self._drain_output(owns_drain)
91
+
92
+
93
+ class StderrRelay:
94
+ """Forward one child's bounded stderr and retain its native reason.
95
+
96
+ Feed raw chunks in arrival order, then call `finish()` after ordinary EOF
97
+ to flush a partial line. Call `close()` during cleanup to stop forwarding
98
+ without diagnostic I/O and retrieve the bounded captured text. Output and
99
+ flush failures other than downstream closure propagate from `feed()` and
100
+ `finish()`; downstream closure latches the shared output closed.
101
+ """
102
+
103
+ def __init__(self, state: _DiagnosticState, step: Step) -> None:
104
+ """Bind one child to the shared diagnostic state.
105
+
106
+ Args:
107
+ state: Shared diagnostic stream, synchronization, and chain budget.
108
+ step: Child whose stderr is forwarded and labeled.
109
+ """
110
+ self._captured = bytearray()
111
+ self._closed = False
112
+ self._line_buffer = bytearray()
113
+ self._state = state
114
+ self._step = step
115
+ self._step_bytes = 0
116
+ self._step_notice_written = False
117
+
118
+ def _decode_line(self, line: bytes | bytearray) -> str:
119
+ """Decode one safe user-facing line."""
120
+ raw_line = bytes(line)
121
+ try:
122
+ return raw_line.decode()
123
+ except UnicodeDecodeError:
124
+ return f'[invalid UTF-8 replaced] {raw_line.decode(errors="replace")}'
125
+
126
+ def _complete_line_messages_locked(self, accepted: bytes) -> list[str]:
127
+ """Capture every complete buffered line while holding the state lock."""
128
+ if b'\n' not in accepted:
129
+ return []
130
+ lines = self._line_buffer.split(b'\n')
131
+ self._line_buffer = bytearray(lines.pop())
132
+ return [self._decode_line(line) for line in lines]
133
+
134
+ def close(self) -> str:
135
+ """Close without diagnostic I/O and return bounded child stderr text.
136
+
137
+ Returns:
138
+ Replacement-decoded child text without diagnostic prefixes.
139
+ """
140
+ with self._state._state_lock:
141
+ self._closed = True
142
+ return self._captured.decode(errors='replace')
143
+
144
+ def feed(self, data: bytes) -> None:
145
+ """Forward and retain bytes that fit the step and chain budgets.
146
+
147
+ Args:
148
+ data: Next raw child stderr chunk.
149
+
150
+ Raises:
151
+ OSError: If writing or flushing diagnostic output fails for a
152
+ reason other than downstream closure. Downstream closure is
153
+ absorbed and latches output closed.
154
+ """
155
+ with self._state._state_lock:
156
+ if self._closed:
157
+ return
158
+ messages: list[str] = []
159
+ step_remaining = max(0, _STDERR_STEP_LIMIT_BYTES - self._step_bytes)
160
+ chain_remaining = max(
161
+ 0,
162
+ _STDERR_CHAIN_LIMIT_BYTES - self._state._chain_bytes,
163
+ )
164
+ accepted_count = min(len(data), step_remaining, chain_remaining)
165
+ accepted = data[:accepted_count]
166
+ self._captured.extend(accepted)
167
+ self._line_buffer.extend(accepted)
168
+ self._step_bytes += accepted_count
169
+ self._state._chain_bytes += accepted_count
170
+ messages.extend(self._complete_line_messages_locked(accepted))
171
+
172
+ if (
173
+ len(data) > step_remaining
174
+ and step_remaining <= chain_remaining
175
+ and not self._step_notice_written
176
+ ):
177
+ messages.append(
178
+ f'stderr truncated after {_STDERR_STEP_LIMIT_BYTES} bytes for this step',
179
+ )
180
+ self._step_notice_written = True
181
+ if (
182
+ len(data) > chain_remaining
183
+ and chain_remaining <= step_remaining
184
+ and not self._state._chain_notice_written
185
+ ):
186
+ messages.append(
187
+ f'stderr truncated after {_STDERR_CHAIN_LIMIT_BYTES} bytes for this chain',
188
+ )
189
+ self._state._chain_notice_written = True
190
+ owns_drain = self._state._enqueue_step_locked(self._step, messages)
191
+ self._state._drain_output(owns_drain)
192
+
193
+ def finish(self) -> None:
194
+ """Flush one partial line after ordinary reader EOF.
195
+
196
+ Raises:
197
+ OSError: If writing or flushing diagnostic output fails for a
198
+ reason other than downstream closure. Downstream closure is
199
+ absorbed and latches output closed.
200
+ """
201
+ with self._state._state_lock:
202
+ if self._closed:
203
+ return
204
+ messages: list[str] = []
205
+ if self._line_buffer:
206
+ messages.append(self._decode_line(self._line_buffer))
207
+ self._line_buffer.clear()
208
+ owns_drain = self._state._enqueue_step_locked(self._step, messages)
209
+ self._state._drain_output(owns_drain)
210
+
211
+
212
+ class Diagnostics:
213
+ """Write bounded, labeled child diagnostics to one text stream.
214
+
215
+ One instance synchronizes writes and shares the aggregate byte budget
216
+ across every relay it creates. Output and flush failures other than
217
+ downstream closure propagate from write operations; downstream closure
218
+ latches output closed.
219
+ """
220
+
221
+ def __init__(self, stream: TextIO) -> None:
222
+ """Bind diagnostics to one text output stream.
223
+
224
+ Args:
225
+ stream: Text stream that receives user-facing diagnostic lines.
226
+ """
227
+ self._state = _DiagnosticState(stream)
228
+
229
+ def relay(self, step: Step) -> StderrRelay:
230
+ """Create a bounded stderr relay for one child.
231
+
232
+ Args:
233
+ step: Child whose stderr will be forwarded.
234
+
235
+ Returns:
236
+ Relay sharing this diagnostic stream and chain budget.
237
+ """
238
+ return StderrRelay(self._state, step)
239
+
240
+ def write_global(self, message: str) -> None:
241
+ """Write a diagnostic that does not belong to one step.
242
+
243
+ Args:
244
+ message: Safe user-facing diagnostic text.
245
+
246
+ Raises:
247
+ OSError: If writing or flushing diagnostic output fails for a
248
+ reason other than downstream closure. Downstream closure is
249
+ absorbed and latches output closed.
250
+ """
251
+ self._state._write_global(message)
252
+
253
+ def write_step(self, step: Step, message: str) -> None:
254
+ """Write one labeled step diagnostic.
255
+
256
+ Args:
257
+ step: Child associated with the message.
258
+ message: Safe user-facing diagnostic text.
259
+
260
+ Raises:
261
+ OSError: If writing or flushing diagnostic output fails for a
262
+ reason other than downstream closure. Downstream closure is
263
+ absorbed and latches output closed.
264
+ """
265
+ self._state._write_step(step, [message])