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 @@
|
|
|
1
|
+
"""Provide the private Antigravity adapter implementation."""
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"""Validate and compose Antigravity hook events."""
|
|
2
|
+
|
|
3
|
+
# pyright: reportPrivateUsage=false
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from importlib import import_module
|
|
9
|
+
from typing import TYPE_CHECKING, cast
|
|
10
|
+
|
|
11
|
+
from sequential_hooks.hosts import (
|
|
12
|
+
API_VERSION,
|
|
13
|
+
TERMINATION_FAILURES,
|
|
14
|
+
UNCONTAINED_WARNING,
|
|
15
|
+
AdapterOutputError,
|
|
16
|
+
HostInputError,
|
|
17
|
+
JsonProtocolError,
|
|
18
|
+
NativeResponse,
|
|
19
|
+
OutputFailureKind,
|
|
20
|
+
continue_after_failure,
|
|
21
|
+
encode_response,
|
|
22
|
+
labeled,
|
|
23
|
+
load_object,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Mapping
|
|
28
|
+
|
|
29
|
+
from plugins.agy._sequential_hooks._doctor import AgyDoctorLoader
|
|
30
|
+
from sequential_hooks.hosts import (
|
|
31
|
+
ChainControl,
|
|
32
|
+
Diagnostics,
|
|
33
|
+
DoctorLoader,
|
|
34
|
+
FinalResponse,
|
|
35
|
+
JsonObject,
|
|
36
|
+
JsonValue,
|
|
37
|
+
Step,
|
|
38
|
+
StepResult,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
_AGY_PRE_RANK = {
|
|
42
|
+
'neutral': 0,
|
|
43
|
+
'allow': 1,
|
|
44
|
+
'ask': 2,
|
|
45
|
+
'force_ask': 3,
|
|
46
|
+
'deny': 4,
|
|
47
|
+
}
|
|
48
|
+
_EVENTS = ('PreToolUse', 'PostToolUse')
|
|
49
|
+
_MINIMUM_CONFLICTING_OVERRIDE_COUNT = 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class _Contribution:
|
|
54
|
+
"""Carry one fully validated Antigravity contribution."""
|
|
55
|
+
|
|
56
|
+
_decision: str
|
|
57
|
+
_permission_overrides: list[str] | None
|
|
58
|
+
_reason: str | None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class _RankedContribution:
|
|
63
|
+
"""Associate one accepted contribution with its child step."""
|
|
64
|
+
|
|
65
|
+
_contribution: _Contribution
|
|
66
|
+
_step: Step
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AgyAdapter:
|
|
70
|
+
"""Compose child results for one validated Antigravity event.
|
|
71
|
+
|
|
72
|
+
Child results are consumed in step order and composed into an event-
|
|
73
|
+
specific final response. The original strict JSON input remains the input
|
|
74
|
+
for every child.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
event: Validated pre-tool or post-tool event.
|
|
78
|
+
should_run_steps: Whether the invocation should execute child steps.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
event: str,
|
|
84
|
+
raw_input: bytes,
|
|
85
|
+
payload: JsonObject,
|
|
86
|
+
diagnostics: Diagnostics,
|
|
87
|
+
*,
|
|
88
|
+
allow_on_failure: bool,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Validate native input and initialize one Antigravity event adapter.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
event: Exact CLI event selected by the caller.
|
|
94
|
+
raw_input: Original strict JSON bytes received from Antigravity.
|
|
95
|
+
payload: Decoded strict JSON object matching `raw_input`.
|
|
96
|
+
diagnostics: Developer-tier diagnostic sink.
|
|
97
|
+
allow_on_failure: Whether approved pre-tool failures may continue.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
HostInputError: If required Antigravity input is missing, has the
|
|
101
|
+
wrong type, or is invalid.
|
|
102
|
+
"""
|
|
103
|
+
if event not in _EVENTS:
|
|
104
|
+
raise HostInputError('Antigravity input has an unsupported event')
|
|
105
|
+
step_index = payload.get('stepIdx')
|
|
106
|
+
if isinstance(step_index, bool) or not isinstance(step_index, int):
|
|
107
|
+
raise HostInputError('Antigravity input requires an integer stepIdx')
|
|
108
|
+
if event == 'PreToolUse':
|
|
109
|
+
tool_call = payload.get('toolCall')
|
|
110
|
+
if not isinstance(tool_call, dict):
|
|
111
|
+
raise HostInputError('Antigravity PreToolUse input requires object toolCall')
|
|
112
|
+
tool_name = tool_call.get('name')
|
|
113
|
+
if not isinstance(tool_name, str):
|
|
114
|
+
raise HostInputError('Antigravity PreToolUse input requires string toolCall.name')
|
|
115
|
+
if not tool_name:
|
|
116
|
+
raise HostInputError(
|
|
117
|
+
'Antigravity PreToolUse input requires non-empty toolCall.name'
|
|
118
|
+
)
|
|
119
|
+
if not isinstance(tool_call.get('args'), dict):
|
|
120
|
+
raise HostInputError('Antigravity PreToolUse input requires object toolCall.args')
|
|
121
|
+
self._should_run_steps = not (
|
|
122
|
+
event == 'PostToolUse' and 'toolCall' in payload and payload['toolCall'] is None
|
|
123
|
+
)
|
|
124
|
+
self._allow_on_failure = allow_on_failure
|
|
125
|
+
self._diagnostics = diagnostics
|
|
126
|
+
self._event = event
|
|
127
|
+
self._raw_input = raw_input
|
|
128
|
+
self._response_bytes: bytes | None = None
|
|
129
|
+
self._uncontained_warning_stored = False
|
|
130
|
+
self._warnings: list[str] = []
|
|
131
|
+
self._winning: list[_RankedContribution] = []
|
|
132
|
+
self._winning_rank = 0
|
|
133
|
+
|
|
134
|
+
def _classify_pre(self, value: JsonObject) -> _Contribution:
|
|
135
|
+
"""Validate one complete Antigravity pre-tool object."""
|
|
136
|
+
allowed_fields = {'decision', 'permissionOverrides', 'reason'}
|
|
137
|
+
unknown_fields = set(value) - allowed_fields
|
|
138
|
+
if unknown_fields:
|
|
139
|
+
field_count = len(unknown_fields)
|
|
140
|
+
field_label = 'field' if field_count == 1 else 'fields'
|
|
141
|
+
raise AdapterOutputError(
|
|
142
|
+
OutputFailureKind.UNSUPPORTED,
|
|
143
|
+
f'Antigravity output has {field_count} unsupported {field_label}',
|
|
144
|
+
)
|
|
145
|
+
decision = value.get('decision')
|
|
146
|
+
if not isinstance(decision, str) or decision not in _AGY_PRE_RANK:
|
|
147
|
+
raise AdapterOutputError(
|
|
148
|
+
OutputFailureKind.MALFORMED,
|
|
149
|
+
'Antigravity pre-tool output requires a supported decision',
|
|
150
|
+
)
|
|
151
|
+
reason_value = value.get('reason')
|
|
152
|
+
if 'reason' in value and not isinstance(reason_value, str):
|
|
153
|
+
raise AdapterOutputError(
|
|
154
|
+
OutputFailureKind.MALFORMED,
|
|
155
|
+
'Antigravity reason must be a string',
|
|
156
|
+
)
|
|
157
|
+
reason = reason_value if isinstance(reason_value, str) else None
|
|
158
|
+
overrides_value = value.get('permissionOverrides')
|
|
159
|
+
overrides: list[str] | None = None
|
|
160
|
+
if 'permissionOverrides' in value:
|
|
161
|
+
if not isinstance(overrides_value, list) or not all(
|
|
162
|
+
isinstance(item, str) for item in overrides_value
|
|
163
|
+
):
|
|
164
|
+
raise AdapterOutputError(
|
|
165
|
+
OutputFailureKind.MALFORMED,
|
|
166
|
+
'Antigravity permissionOverrides must be an array of strings',
|
|
167
|
+
)
|
|
168
|
+
overrides = cast('list[str]', overrides_value)
|
|
169
|
+
return _Contribution(decision, overrides, reason)
|
|
170
|
+
|
|
171
|
+
def _conflicting_override_steps(
|
|
172
|
+
self,
|
|
173
|
+
contributions: list[_RankedContribution],
|
|
174
|
+
) -> list[Step]:
|
|
175
|
+
"""Return winning steps when their non-empty override arrays conflict."""
|
|
176
|
+
with_overrides = [
|
|
177
|
+
item for item in contributions if item._contribution._permission_overrides
|
|
178
|
+
]
|
|
179
|
+
if len(with_overrides) < _MINIMUM_CONFLICTING_OVERRIDE_COUNT:
|
|
180
|
+
return []
|
|
181
|
+
first = with_overrides[0]._contribution._permission_overrides
|
|
182
|
+
if all(item._contribution._permission_overrides == first for item in with_overrides):
|
|
183
|
+
return []
|
|
184
|
+
return [item._step for item in with_overrides]
|
|
185
|
+
|
|
186
|
+
def _winning_overrides(
|
|
187
|
+
self,
|
|
188
|
+
contributions: list[_RankedContribution],
|
|
189
|
+
) -> list[str] | None:
|
|
190
|
+
"""Return the first non-empty override array at the winning rank."""
|
|
191
|
+
return next(
|
|
192
|
+
(
|
|
193
|
+
item._contribution._permission_overrides
|
|
194
|
+
for item in contributions
|
|
195
|
+
if item._contribution._permission_overrides
|
|
196
|
+
),
|
|
197
|
+
None,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _pre_object(
|
|
201
|
+
self,
|
|
202
|
+
contributions: list[_RankedContribution],
|
|
203
|
+
warnings: list[str],
|
|
204
|
+
*,
|
|
205
|
+
shed_reasons: bool = False,
|
|
206
|
+
) -> JsonObject:
|
|
207
|
+
"""Build one native pre-tool response from immutable candidate data."""
|
|
208
|
+
winning_decision = contributions[0]._contribution._decision if contributions else 'allow'
|
|
209
|
+
decision = 'allow' if winning_decision == 'neutral' else winning_decision
|
|
210
|
+
response: JsonObject = {'decision': decision}
|
|
211
|
+
overrides = None if decision == 'deny' else self._winning_overrides(contributions)
|
|
212
|
+
reasons = [
|
|
213
|
+
labeled(item._step, item._contribution._reason)
|
|
214
|
+
for item in contributions
|
|
215
|
+
if item._contribution._reason
|
|
216
|
+
]
|
|
217
|
+
if reasons and decision in ('ask', 'force_ask', 'deny'):
|
|
218
|
+
reasons.extend(warnings)
|
|
219
|
+
if reasons and not shed_reasons:
|
|
220
|
+
response['reason'] = '\n\n'.join(reasons)
|
|
221
|
+
if overrides is not None:
|
|
222
|
+
response['permissionOverrides'] = cast('list[JsonValue]', overrides)
|
|
223
|
+
return response
|
|
224
|
+
|
|
225
|
+
def _encode_pre_candidate(
|
|
226
|
+
self,
|
|
227
|
+
contributions: list[_RankedContribution],
|
|
228
|
+
warnings: list[str],
|
|
229
|
+
*,
|
|
230
|
+
shed_reasons: bool = False,
|
|
231
|
+
) -> bytes:
|
|
232
|
+
"""Build and encode one native pre-tool response candidate."""
|
|
233
|
+
return encode_response(
|
|
234
|
+
self._pre_object(
|
|
235
|
+
contributions,
|
|
236
|
+
warnings,
|
|
237
|
+
shed_reasons=shed_reasons,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _fit_pre_candidate(
|
|
242
|
+
self,
|
|
243
|
+
contributions: list[_RankedContribution],
|
|
244
|
+
warnings: list[str],
|
|
245
|
+
) -> bytes:
|
|
246
|
+
"""Build and encode the bounded native pre-tool response candidate."""
|
|
247
|
+
try:
|
|
248
|
+
return self._encode_pre_candidate(contributions, warnings)
|
|
249
|
+
except AdapterOutputError as error:
|
|
250
|
+
if error.kind is not OutputFailureKind.OVERSIZED:
|
|
251
|
+
raise
|
|
252
|
+
return self._encode_pre_candidate(
|
|
253
|
+
contributions,
|
|
254
|
+
warnings,
|
|
255
|
+
shed_reasons=True,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def _apply_failure(
|
|
259
|
+
self,
|
|
260
|
+
step: Step,
|
|
261
|
+
failure: str,
|
|
262
|
+
message: str,
|
|
263
|
+
*,
|
|
264
|
+
uncontained: bool = False,
|
|
265
|
+
) -> ChainControl:
|
|
266
|
+
"""Apply the common policy to one classified child failure."""
|
|
267
|
+
candidate_warnings = [*self._warnings]
|
|
268
|
+
warning_stored = self._uncontained_warning_stored
|
|
269
|
+
if uncontained and failure in TERMINATION_FAILURES:
|
|
270
|
+
self._diagnostics.write_step(step, UNCONTAINED_WARNING)
|
|
271
|
+
if not warning_stored:
|
|
272
|
+
candidate_warnings.append(labeled(step, UNCONTAINED_WARNING))
|
|
273
|
+
warning_stored = True
|
|
274
|
+
category = failure.replace('_', ' ')
|
|
275
|
+
detail = message or f'{category} child result'
|
|
276
|
+
self._diagnostics.write_step(step, f'{category}: {detail}')
|
|
277
|
+
warning = labeled(step, f'{category}: {detail}')
|
|
278
|
+
should_continue = continue_after_failure(
|
|
279
|
+
self._event == 'PreToolUse',
|
|
280
|
+
failure,
|
|
281
|
+
allow_on_failure=self._allow_on_failure,
|
|
282
|
+
)
|
|
283
|
+
if should_continue:
|
|
284
|
+
candidate_warnings.append(warning)
|
|
285
|
+
candidate_winning = self._winning
|
|
286
|
+
candidate_rank = self._winning_rank
|
|
287
|
+
else:
|
|
288
|
+
candidate_winning = [
|
|
289
|
+
_RankedContribution(
|
|
290
|
+
_Contribution('deny', None, f'{category}: {detail}'),
|
|
291
|
+
step,
|
|
292
|
+
)
|
|
293
|
+
]
|
|
294
|
+
candidate_rank = _AGY_PRE_RANK['deny']
|
|
295
|
+
try:
|
|
296
|
+
if self._event == 'PostToolUse':
|
|
297
|
+
response_bytes = encode_response({})
|
|
298
|
+
else:
|
|
299
|
+
response_bytes = self._fit_pre_candidate(
|
|
300
|
+
candidate_winning,
|
|
301
|
+
candidate_warnings,
|
|
302
|
+
)
|
|
303
|
+
except AdapterOutputError as error:
|
|
304
|
+
if failure in OutputFailureKind:
|
|
305
|
+
raise
|
|
306
|
+
return self._apply_failure(step, error.kind.value, str(error))
|
|
307
|
+
self._uncontained_warning_stored = warning_stored
|
|
308
|
+
self._warnings = candidate_warnings
|
|
309
|
+
self._winning = candidate_winning
|
|
310
|
+
self._winning_rank = candidate_rank
|
|
311
|
+
if should_continue:
|
|
312
|
+
return 'continue'
|
|
313
|
+
self._response_bytes = response_bytes
|
|
314
|
+
return 'stop'
|
|
315
|
+
|
|
316
|
+
def _apply_pre(self, step: Step, contribution: _Contribution) -> ChainControl:
|
|
317
|
+
"""Apply one fully validated pre-tool contribution."""
|
|
318
|
+
new_rank = _AGY_PRE_RANK[contribution._decision]
|
|
319
|
+
current_rank = self._winning_rank
|
|
320
|
+
terminal = contribution._decision == 'deny'
|
|
321
|
+
if new_rank >= current_rank:
|
|
322
|
+
prior = [] if new_rank > current_rank else self._winning
|
|
323
|
+
candidate = [*prior, _RankedContribution(contribution, step)]
|
|
324
|
+
conflict_steps = self._conflicting_override_steps(candidate)
|
|
325
|
+
if conflict_steps:
|
|
326
|
+
indexes = ', '.join(f'step {item.index}' for item in conflict_steps)
|
|
327
|
+
reason = f'conflicting permissionOverrides from contributing steps: {indexes}'
|
|
328
|
+
new_rank = _AGY_PRE_RANK['deny']
|
|
329
|
+
candidate = [
|
|
330
|
+
_RankedContribution(
|
|
331
|
+
_Contribution('deny', None, reason),
|
|
332
|
+
step,
|
|
333
|
+
)
|
|
334
|
+
]
|
|
335
|
+
terminal = True
|
|
336
|
+
try:
|
|
337
|
+
response_bytes = self._encode_pre_candidate(
|
|
338
|
+
candidate,
|
|
339
|
+
self._warnings,
|
|
340
|
+
)
|
|
341
|
+
except AdapterOutputError as error:
|
|
342
|
+
if error.kind is not OutputFailureKind.OVERSIZED:
|
|
343
|
+
return self._apply_failure(step, error.kind.value, str(error))
|
|
344
|
+
try:
|
|
345
|
+
response_bytes = self._encode_pre_candidate(
|
|
346
|
+
candidate,
|
|
347
|
+
self._warnings,
|
|
348
|
+
shed_reasons=True,
|
|
349
|
+
)
|
|
350
|
+
except AdapterOutputError:
|
|
351
|
+
failure = (
|
|
352
|
+
OutputFailureKind.UNDELIVERABLE_MUTATION
|
|
353
|
+
if self._winning_overrides(candidate) is not None
|
|
354
|
+
else OutputFailureKind.OVERSIZED
|
|
355
|
+
)
|
|
356
|
+
return self._apply_failure(
|
|
357
|
+
step,
|
|
358
|
+
failure.value,
|
|
359
|
+
'accepted Antigravity contribution cannot fit in the final response',
|
|
360
|
+
)
|
|
361
|
+
self._winning_rank = new_rank
|
|
362
|
+
self._winning = candidate
|
|
363
|
+
if terminal:
|
|
364
|
+
self._response_bytes = response_bytes
|
|
365
|
+
return 'stop'
|
|
366
|
+
return 'continue'
|
|
367
|
+
|
|
368
|
+
def consume(self, step: Step, result: StepResult) -> ChainControl:
|
|
369
|
+
"""Apply one child result and choose whether the runner continues.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
step: Child that produced `result`.
|
|
373
|
+
result: Bounded process outcome to interpret.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
`'continue'` or `'stop'`.
|
|
377
|
+
"""
|
|
378
|
+
if result.failure is not None:
|
|
379
|
+
control = self._apply_failure(
|
|
380
|
+
step,
|
|
381
|
+
str(result.failure.kind),
|
|
382
|
+
result.failure.message,
|
|
383
|
+
uncontained=str(result.containment) == 'uncontained',
|
|
384
|
+
)
|
|
385
|
+
elif result.returncode not in (None, 0):
|
|
386
|
+
control = self._apply_failure(
|
|
387
|
+
step,
|
|
388
|
+
OutputFailureKind.MALFORMED.value,
|
|
389
|
+
f'child exited with status {result.returncode}',
|
|
390
|
+
)
|
|
391
|
+
elif not result.stdout.lstrip(b' \t\r\n'):
|
|
392
|
+
control = 'continue'
|
|
393
|
+
else:
|
|
394
|
+
try:
|
|
395
|
+
value = load_object(result.stdout)
|
|
396
|
+
except JsonProtocolError as error:
|
|
397
|
+
control = self._apply_failure(
|
|
398
|
+
step,
|
|
399
|
+
OutputFailureKind.MALFORMED.value,
|
|
400
|
+
str(error),
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
if self._event == 'PostToolUse':
|
|
404
|
+
control = (
|
|
405
|
+
self._apply_failure(
|
|
406
|
+
step,
|
|
407
|
+
OutputFailureKind.UNSUPPORTED.value,
|
|
408
|
+
'Antigravity post-tool output must be an empty object',
|
|
409
|
+
)
|
|
410
|
+
if value
|
|
411
|
+
else 'continue'
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
try:
|
|
415
|
+
contribution = self._classify_pre(value)
|
|
416
|
+
except AdapterOutputError as error:
|
|
417
|
+
control = self._apply_failure(step, error.kind.value, str(error))
|
|
418
|
+
else:
|
|
419
|
+
control = self._apply_pre(step, contribution)
|
|
420
|
+
return control
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def event(self) -> str:
|
|
424
|
+
"""Return the validated native event.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Current pre-tool or post-tool event.
|
|
428
|
+
"""
|
|
429
|
+
return self._event
|
|
430
|
+
|
|
431
|
+
def finalize(self) -> NativeResponse:
|
|
432
|
+
"""Return the final native Antigravity response.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
The terminal response retained by a stop, otherwise exactly one
|
|
436
|
+
host response with one trailing line feed.
|
|
437
|
+
"""
|
|
438
|
+
if self._response_bytes is not None:
|
|
439
|
+
return NativeResponse(self._response_bytes)
|
|
440
|
+
if self._event == 'PostToolUse':
|
|
441
|
+
response_bytes = encode_response({})
|
|
442
|
+
else:
|
|
443
|
+
response_bytes = self._fit_pre_candidate(
|
|
444
|
+
self._winning,
|
|
445
|
+
self._warnings,
|
|
446
|
+
)
|
|
447
|
+
return NativeResponse(response_bytes)
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def should_run_steps(self) -> bool:
|
|
451
|
+
"""Return whether this invocation should execute child steps.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
`False` only for an explicit-null Antigravity post-tool callback.
|
|
455
|
+
"""
|
|
456
|
+
return self._should_run_steps
|
|
457
|
+
|
|
458
|
+
def step_input(self) -> bytes:
|
|
459
|
+
"""Return the unchanged native JSON input for the next child.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Original strict JSON input bytes.
|
|
463
|
+
"""
|
|
464
|
+
return self._raw_input
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class AgyHost:
|
|
468
|
+
"""Describe the Antigravity host for entry-point discovery.
|
|
469
|
+
|
|
470
|
+
Attributes:
|
|
471
|
+
api_version: Host contract version implemented here.
|
|
472
|
+
events: Native events the wrapper serves.
|
|
473
|
+
name: Entry-point name selected by `--agent`.
|
|
474
|
+
requires_event: `True`; Antigravity payloads carry no event name.
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
api_version = API_VERSION
|
|
478
|
+
events = _EVENTS
|
|
479
|
+
name = 'agy'
|
|
480
|
+
requires_event = True
|
|
481
|
+
|
|
482
|
+
def adapter(
|
|
483
|
+
self,
|
|
484
|
+
event: str,
|
|
485
|
+
raw_input: bytes,
|
|
486
|
+
payload: JsonObject,
|
|
487
|
+
diagnostics: Diagnostics,
|
|
488
|
+
*,
|
|
489
|
+
allow_on_failure: bool,
|
|
490
|
+
) -> AgyAdapter:
|
|
491
|
+
"""Validate native input and construct the adapter for one invocation.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
event: Trusted event from `--event`.
|
|
495
|
+
raw_input: Original native strict JSON hook input.
|
|
496
|
+
payload: Parsed native strict JSON object.
|
|
497
|
+
diagnostics: Bounded developer diagnostic sink.
|
|
498
|
+
allow_on_failure: Whether approved pre-tool failures may continue.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Adapter ready to drive the chain.
|
|
502
|
+
|
|
503
|
+
Raises:
|
|
504
|
+
HostInputError: If the payload is not valid input for `event`.
|
|
505
|
+
"""
|
|
506
|
+
return AgyAdapter(
|
|
507
|
+
event,
|
|
508
|
+
raw_input,
|
|
509
|
+
payload,
|
|
510
|
+
diagnostics,
|
|
511
|
+
allow_on_failure=allow_on_failure,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
def configuration_response(
|
|
515
|
+
self,
|
|
516
|
+
event: str,
|
|
517
|
+
reason: str,
|
|
518
|
+
diagnostics: Diagnostics,
|
|
519
|
+
) -> FinalResponse:
|
|
520
|
+
"""Return the top-level deny or the empty post-tool object.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
event: Trusted event, one of `events`.
|
|
524
|
+
reason: Wrapper diagnostic, published after response resolution.
|
|
525
|
+
diagnostics: Bounded developer diagnostic sink.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
PreToolUse deny JSON, or `{}` for PostToolUse, which does not
|
|
529
|
+
block.
|
|
530
|
+
|
|
531
|
+
Raises:
|
|
532
|
+
ValueError: If `event` is not one of `events`.
|
|
533
|
+
"""
|
|
534
|
+
del reason
|
|
535
|
+
if event not in self.events:
|
|
536
|
+
raise ValueError(f'Antigravity has no configuration response for {event!r}')
|
|
537
|
+
message = '[step 1: configuration]\nspawn: sequential-hooks could not safely run this hook'
|
|
538
|
+
diagnostics.write_global(
|
|
539
|
+
'configuration: spawn: sequential-hooks could not safely run this hook'
|
|
540
|
+
)
|
|
541
|
+
value: JsonObject = (
|
|
542
|
+
{'decision': 'deny', 'reason': message} if event == 'PreToolUse' else {}
|
|
543
|
+
)
|
|
544
|
+
return NativeResponse(encode_response(value))
|
|
545
|
+
|
|
546
|
+
def doctor_loader(self) -> DoctorLoader:
|
|
547
|
+
"""Return the static Antigravity configuration loader.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Loader over the documented Antigravity hooks files.
|
|
551
|
+
"""
|
|
552
|
+
module = import_module('plugins.agy._sequential_hooks._doctor')
|
|
553
|
+
loader_type: type[AgyDoctorLoader] = module.AgyDoctorLoader
|
|
554
|
+
return loader_type()
|
|
555
|
+
|
|
556
|
+
def payload_event(self, payload: Mapping[str, object]) -> str | None:
|
|
557
|
+
"""Return `None`; Antigravity payloads never carry the event.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
payload: Parsed native strict JSON object.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Always `None`.
|
|
564
|
+
"""
|
|
565
|
+
del payload
|
|
566
|
+
return None
|