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,103 @@
|
|
|
1
|
+
"""Discover and load host adapters through entry points."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
|
+
|
|
8
|
+
from sequential_hooks.hosts import API_VERSION, HostSpec
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from importlib.metadata import EntryPoints
|
|
12
|
+
|
|
13
|
+
_ENTRY_POINT_GROUP = 'sequential_hooks.hosts'
|
|
14
|
+
_REQUIRED_ATTRIBUTES = (
|
|
15
|
+
'adapter',
|
|
16
|
+
'configuration_response',
|
|
17
|
+
'doctor_loader',
|
|
18
|
+
'events',
|
|
19
|
+
'name',
|
|
20
|
+
'payload_event',
|
|
21
|
+
'requires_event',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HostLoadError(Exception):
|
|
26
|
+
"""Report that no functioning host adapter exists for the selected name."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _entry_points() -> EntryPoints:
|
|
30
|
+
"""Read host entry points without loading metadata during CLI import."""
|
|
31
|
+
# Delay importing metadata until the caller selects or inventories hosts.
|
|
32
|
+
module = import_module('importlib.metadata')
|
|
33
|
+
return cast('EntryPoints', module.entry_points(group=_ENTRY_POINT_GROUP))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _registered_hosts() -> list[str]:
|
|
37
|
+
"""Return sorted names in the host entry-point group."""
|
|
38
|
+
return sorted({entry.name for entry in _entry_points()})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_host(name: str, spec: object) -> HostSpec:
|
|
42
|
+
"""Validate and return one constructed host specification."""
|
|
43
|
+
for attribute in _REQUIRED_ATTRIBUTES:
|
|
44
|
+
if not hasattr(spec, attribute):
|
|
45
|
+
raise HostLoadError(f'host {name!r} does not provide {attribute}')
|
|
46
|
+
version = getattr(spec, 'api_version', None)
|
|
47
|
+
if version != API_VERSION:
|
|
48
|
+
raise HostLoadError(
|
|
49
|
+
f'host {name!r} implements host API {version!r}; '
|
|
50
|
+
f'sequential-hooks requires {API_VERSION}'
|
|
51
|
+
)
|
|
52
|
+
host_spec = cast('HostSpec', spec)
|
|
53
|
+
reported_name = host_spec.name
|
|
54
|
+
if reported_name != name:
|
|
55
|
+
raise HostLoadError(f'host {name!r} reports the name {reported_name!r}')
|
|
56
|
+
return host_spec
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_host(name: str) -> HostSpec:
|
|
60
|
+
"""Load the host registered under one entry-point name.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
name: Host name given to `--agent`.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The registered host specification.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
HostLoadError: If the name is unregistered or registered more than
|
|
70
|
+
once, the entry point cannot be loaded, the object lacks a contract
|
|
71
|
+
attribute, its `api_version` differs from `API_VERSION`, or its
|
|
72
|
+
reported `name` differs from the entry-point name.
|
|
73
|
+
"""
|
|
74
|
+
matches = [entry for entry in _entry_points() if entry.name == name]
|
|
75
|
+
if not matches:
|
|
76
|
+
hosts = ', '.join(_registered_hosts()) or 'none'
|
|
77
|
+
raise HostLoadError(f'unknown host {name!r}; registered hosts: {hosts}')
|
|
78
|
+
if len(matches) > 1:
|
|
79
|
+
raise HostLoadError(f'host {name!r} is registered {len(matches)} times')
|
|
80
|
+
try:
|
|
81
|
+
factory = matches[0].load()
|
|
82
|
+
spec = _validate_host(name, factory())
|
|
83
|
+
except HostLoadError:
|
|
84
|
+
raise
|
|
85
|
+
except Exception as error:
|
|
86
|
+
raise HostLoadError(f'host {name!r} failed to load: {error}') from error
|
|
87
|
+
return spec
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def host_requirements() -> dict[str, HostSpec]:
|
|
91
|
+
"""Return each loadable host specification keyed by its registered name.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Names in sorted order; hosts that fail to load are omitted.
|
|
95
|
+
"""
|
|
96
|
+
requirements: dict[str, HostSpec] = {}
|
|
97
|
+
for name in _registered_hosts():
|
|
98
|
+
try:
|
|
99
|
+
spec = load_host(name)
|
|
100
|
+
except HostLoadError:
|
|
101
|
+
continue
|
|
102
|
+
requirements[name] = spec
|
|
103
|
+
return requirements
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Run ordered child steps through one host adapter."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from sequential_hooks import _executor
|
|
6
|
+
from sequential_hooks._types import Step
|
|
7
|
+
from sequential_hooks.hosts import FinalResponse, HostAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_pipeline(
|
|
11
|
+
adapter: HostAdapter,
|
|
12
|
+
steps: Iterable[Step],
|
|
13
|
+
executor: _executor.ProcessExecutor,
|
|
14
|
+
step_timeout: float | None,
|
|
15
|
+
*,
|
|
16
|
+
allow_on_failure: bool,
|
|
17
|
+
) -> FinalResponse:
|
|
18
|
+
"""Run ordered steps until the adapter stops or finalizes.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
adapter: Host-neutral interface to native protocol state.
|
|
22
|
+
steps: Child commands in execution order.
|
|
23
|
+
executor: Bounded process executor for each child.
|
|
24
|
+
step_timeout: Independent timeout in seconds for each child, or `None`
|
|
25
|
+
for no step execution deadline.
|
|
26
|
+
allow_on_failure: Whether explicit uncontained fallback is allowed.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Terminal or finalized native host response.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
_executor.ForwardedSignal: If the executor forwards a handled wrapper
|
|
33
|
+
signal, propagated unchanged.
|
|
34
|
+
Exception: Any executor or adapter invariant exception, propagated
|
|
35
|
+
unchanged.
|
|
36
|
+
"""
|
|
37
|
+
if not adapter.should_run_steps:
|
|
38
|
+
return adapter.finalize()
|
|
39
|
+
for step in steps:
|
|
40
|
+
result = executor.run(
|
|
41
|
+
step,
|
|
42
|
+
adapter.step_input(),
|
|
43
|
+
step_timeout,
|
|
44
|
+
allow_uncontained=allow_on_failure,
|
|
45
|
+
)
|
|
46
|
+
if adapter.consume(step, result) == 'stop':
|
|
47
|
+
break
|
|
48
|
+
return adapter.finalize()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Define the runner's concrete step, result, and failure records."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ContainmentState(StrEnum):
|
|
9
|
+
"""Describe whether a child owns native process-tree containment.
|
|
10
|
+
|
|
11
|
+
The values are the documented `CONTAINMENT_STATES` strings that adapters
|
|
12
|
+
compare.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
CONTAINED: Native process-tree containment is active.
|
|
16
|
+
UNCONTAINED: The child runs through an explicitly allowed fallback
|
|
17
|
+
without process-tree containment.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
CONTAINED = 'contained'
|
|
21
|
+
UNCONTAINED = 'uncontained'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FailureKind(StrEnum):
|
|
25
|
+
"""Classify an infrastructure failure before adapter interpretation.
|
|
26
|
+
|
|
27
|
+
The values are the documented `FAILURE_KINDS` strings that adapters
|
|
28
|
+
compare.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
NOT_FOUND: The executable could not be resolved or started.
|
|
32
|
+
SPAWN: The operating system refused process creation.
|
|
33
|
+
CRASH: The child terminated with a native crash status.
|
|
34
|
+
TIMEOUT: The child exceeded its execution deadline.
|
|
35
|
+
OUTPUT_LIMIT: Standard output exceeded its byte budget.
|
|
36
|
+
OUTPUT_WORKER_TIMEOUT: A pipe worker did not stop within cleanup
|
|
37
|
+
bounds.
|
|
38
|
+
CONTAINMENT: Required process-tree containment could not be
|
|
39
|
+
established.
|
|
40
|
+
UNSUPPORTED_EXECUTABLE: Resolution found an unsupported executable
|
|
41
|
+
shim.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
NOT_FOUND = 'not_found'
|
|
45
|
+
SPAWN = 'spawn'
|
|
46
|
+
CRASH = 'crash'
|
|
47
|
+
TIMEOUT = 'timeout'
|
|
48
|
+
OUTPUT_LIMIT = 'output_limit'
|
|
49
|
+
OUTPUT_WORKER_TIMEOUT = 'output_worker_timeout'
|
|
50
|
+
CONTAINMENT = 'containment'
|
|
51
|
+
UNSUPPORTED_EXECUTABLE = 'unsupported_executable'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Step:
|
|
56
|
+
"""Describe one child command in its configured chain position.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
index: One-based position used in diagnostics.
|
|
60
|
+
argv: Literal executable token and arguments.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
index: One-based position used in diagnostics.
|
|
64
|
+
argv: Literal executable token and arguments.
|
|
65
|
+
name: Executable basename used in diagnostics.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
index: int
|
|
69
|
+
argv: tuple[str, ...]
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def name(self) -> str:
|
|
73
|
+
"""Return the executable basename used in diagnostics.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Basename, or the original executable token when it has no basename.
|
|
77
|
+
"""
|
|
78
|
+
basename = Path(self.argv[0]).name
|
|
79
|
+
return basename or self.argv[0]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class StepFailure:
|
|
84
|
+
"""Describe one bounded infrastructure failure.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
kind: Stable infrastructure failure category.
|
|
88
|
+
message: Safe user-facing failure detail.
|
|
89
|
+
returncode: Child status when one was observed.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
kind: Stable infrastructure failure category.
|
|
93
|
+
message: Safe user-facing failure detail.
|
|
94
|
+
returncode: Child status when one was observed.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
kind: FailureKind
|
|
98
|
+
message: str
|
|
99
|
+
returncode: int | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class StepResult:
|
|
104
|
+
"""Carry one child's bounded process outcome to an adapter.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
returncode: Child status when one was observed.
|
|
108
|
+
stdout: Bounded child standard-output bytes.
|
|
109
|
+
blocking_stderr: Bounded decoded stderr retained for native responses.
|
|
110
|
+
failure: Infrastructure failure, if one occurred.
|
|
111
|
+
containment: Effective process-tree containment state.
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
returncode: Child status when one was observed.
|
|
115
|
+
stdout: Bounded child standard-output bytes.
|
|
116
|
+
blocking_stderr: Bounded decoded stderr retained for native responses.
|
|
117
|
+
failure: Infrastructure failure, if one occurred.
|
|
118
|
+
containment: Effective process-tree containment state.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
returncode: int | None
|
|
122
|
+
stdout: bytes
|
|
123
|
+
blocking_stderr: str
|
|
124
|
+
failure: StepFailure | None = None
|
|
125
|
+
containment: ContainmentState = ContainmentState.CONTAINED
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Public host adapter contract, doctor records, and composition skeleton.
|
|
2
|
+
|
|
3
|
+
An adapter package depends on this subpackage alone. It publishes the
|
|
4
|
+
structural types a `sequential_hooks.hosts` entry point must satisfy, the
|
|
5
|
+
doctor record types a loader produces, and `Composer`, the shared response
|
|
6
|
+
composition skeleton a host may subclass instead of implementing `HostAdapter`
|
|
7
|
+
directly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from sequential_hooks.hosts._contract import (
|
|
11
|
+
API_VERSION,
|
|
12
|
+
CONTAINMENT_STATES,
|
|
13
|
+
FAILURE_KINDS,
|
|
14
|
+
ChainControl,
|
|
15
|
+
Diagnostics,
|
|
16
|
+
DoctorLoader,
|
|
17
|
+
FinalResponse,
|
|
18
|
+
HostAdapter,
|
|
19
|
+
HostInputError,
|
|
20
|
+
HostSpec,
|
|
21
|
+
JsonObject,
|
|
22
|
+
JsonValue,
|
|
23
|
+
Step,
|
|
24
|
+
StepFailure,
|
|
25
|
+
StepResult,
|
|
26
|
+
)
|
|
27
|
+
from sequential_hooks.hosts._inspection import (
|
|
28
|
+
deduplicate_source_paths,
|
|
29
|
+
parse_timeout,
|
|
30
|
+
project_layers,
|
|
31
|
+
)
|
|
32
|
+
from sequential_hooks.hosts._json import (
|
|
33
|
+
JsonObjectExpectedError,
|
|
34
|
+
JsonProtocolError,
|
|
35
|
+
load_object,
|
|
36
|
+
load_object_text,
|
|
37
|
+
)
|
|
38
|
+
from sequential_hooks.hosts._records import (
|
|
39
|
+
ConfigSource,
|
|
40
|
+
ContractError,
|
|
41
|
+
Finding,
|
|
42
|
+
FindingSeverity,
|
|
43
|
+
InspectedCommand,
|
|
44
|
+
LaunchSemantics,
|
|
45
|
+
Registration,
|
|
46
|
+
SourceKind,
|
|
47
|
+
WrapperInvocation,
|
|
48
|
+
WrapperStep,
|
|
49
|
+
)
|
|
50
|
+
from sequential_hooks.hosts._skeleton import (
|
|
51
|
+
DIAGNOSTIC_LIMIT_CHARACTERS,
|
|
52
|
+
FINAL_RESPONSE_LIMIT_BYTES,
|
|
53
|
+
TERMINATION_FAILURES,
|
|
54
|
+
TRUNCATION_MARKER,
|
|
55
|
+
UNCONTAINED_WARNING,
|
|
56
|
+
AdapterOutputError,
|
|
57
|
+
Composer,
|
|
58
|
+
CompositionState,
|
|
59
|
+
Contribution,
|
|
60
|
+
NativeResponse,
|
|
61
|
+
Outcome,
|
|
62
|
+
OutputFailureKind,
|
|
63
|
+
UserContribution,
|
|
64
|
+
bounded_contributions,
|
|
65
|
+
bounded_diagnostic,
|
|
66
|
+
continue_after_failure,
|
|
67
|
+
encode_plain_response,
|
|
68
|
+
encode_response,
|
|
69
|
+
labeled,
|
|
70
|
+
optional_string,
|
|
71
|
+
reject_unsupported,
|
|
72
|
+
unsupported_fields,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
'API_VERSION',
|
|
77
|
+
'CONTAINMENT_STATES',
|
|
78
|
+
'DIAGNOSTIC_LIMIT_CHARACTERS',
|
|
79
|
+
'FAILURE_KINDS',
|
|
80
|
+
'FINAL_RESPONSE_LIMIT_BYTES',
|
|
81
|
+
'TERMINATION_FAILURES',
|
|
82
|
+
'TRUNCATION_MARKER',
|
|
83
|
+
'UNCONTAINED_WARNING',
|
|
84
|
+
'AdapterOutputError',
|
|
85
|
+
'ChainControl',
|
|
86
|
+
'Composer',
|
|
87
|
+
'CompositionState',
|
|
88
|
+
'ConfigSource',
|
|
89
|
+
'ContractError',
|
|
90
|
+
'Contribution',
|
|
91
|
+
'Diagnostics',
|
|
92
|
+
'DoctorLoader',
|
|
93
|
+
'FinalResponse',
|
|
94
|
+
'Finding',
|
|
95
|
+
'FindingSeverity',
|
|
96
|
+
'HostAdapter',
|
|
97
|
+
'HostInputError',
|
|
98
|
+
'HostSpec',
|
|
99
|
+
'InspectedCommand',
|
|
100
|
+
'JsonObject',
|
|
101
|
+
'JsonObjectExpectedError',
|
|
102
|
+
'JsonProtocolError',
|
|
103
|
+
'JsonValue',
|
|
104
|
+
'LaunchSemantics',
|
|
105
|
+
'NativeResponse',
|
|
106
|
+
'Outcome',
|
|
107
|
+
'OutputFailureKind',
|
|
108
|
+
'Registration',
|
|
109
|
+
'SourceKind',
|
|
110
|
+
'Step',
|
|
111
|
+
'StepFailure',
|
|
112
|
+
'StepResult',
|
|
113
|
+
'UserContribution',
|
|
114
|
+
'WrapperInvocation',
|
|
115
|
+
'WrapperStep',
|
|
116
|
+
'bounded_contributions',
|
|
117
|
+
'bounded_diagnostic',
|
|
118
|
+
'continue_after_failure',
|
|
119
|
+
'deduplicate_source_paths',
|
|
120
|
+
'encode_plain_response',
|
|
121
|
+
'encode_response',
|
|
122
|
+
'labeled',
|
|
123
|
+
'load_object',
|
|
124
|
+
'load_object_text',
|
|
125
|
+
'optional_string',
|
|
126
|
+
'parse_timeout',
|
|
127
|
+
'project_layers',
|
|
128
|
+
'reject_unsupported',
|
|
129
|
+
'unsupported_fields',
|
|
130
|
+
]
|