Metadata-Version: 2.4
Name: agenthooksprotocol
Version: 0.8.0
Summary: Official non-normative Python SDK for the Agent Hooks Protocol
Author: Agent Hooks Protocol maintainers
License: 
                                         Apache License
                                   Version 2.0, January 2004
                                http://www.apache.org/licenses/
        
           TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
        
           1. Definitions.
        
              "License" shall mean the terms and conditions for use, reproduction,
              and distribution as defined by Sections 1 through 9 of this document.
        
              "Licensor" shall mean the copyright owner or entity authorized by
              the copyright owner that is granting the License.
        
              "Legal Entity" shall mean the union of the acting entity and all
              other entities that control, are controlled by, or are under common
              control with that entity. For the purposes of this definition,
              "control" means (i) the power, direct or indirect, to cause the
              direction or management of such entity, whether by contract or
              otherwise, or (ii) ownership of fifty percent (50%) or more of the
              outstanding shares, or (iii) beneficial ownership of such entity.
        
              "You" (or "Your") shall mean an individual or Legal Entity
              exercising permissions granted by this License.
        
              "Source" form shall mean the preferred form for making modifications,
              including but not limited to software source code, documentation
              source, and configuration files.
        
              "Object" form shall mean any form resulting from mechanical
              transformation or translation of a Source form, including but
              not limited to compiled object code, generated documentation,
              and conversions to other media types.
        
              "Work" shall mean the work of authorship, whether in Source or
              Object form, made available under the License, as indicated by a
              copyright notice that is included in or attached to the work
              (an example is provided in the Appendix below).
        
              "Derivative Works" shall mean any work, whether in Source or Object
              form, that is based on (or derived from) the Work and for which the
              editorial revisions, annotations, elaborations, or other modifications
              represent, as a whole, an original work of authorship. For the purposes
              of this License, Derivative Works shall not include works that remain
              separable from, or merely link (or bind by name) to the interfaces of,
              the Work and Derivative Works thereof.
        
              "Contribution" shall mean any work of authorship, including
              the original version of the Work and any modifications or additions
              to that Work or Derivative Works thereof, that is intentionally
              submitted to Licensor for inclusion in the Work by the copyright owner
              or by an individual or Legal Entity authorized to submit on behalf of
              the copyright owner. For the purposes of this definition, "submitted"
              means any form of electronic, verbal, or written communication sent
              to the Licensor or its representatives, including but not limited to
              communication on electronic mailing lists, source code control systems,
              and issue tracking systems that are managed by, or on behalf of, the
              Licensor for the purpose of discussing and improving the Work, but
              excluding communication that is conspicuously marked or otherwise
              designated in writing by the copyright owner as "Not a Contribution."
        
              "Contributor" shall mean Licensor and any individual or Legal Entity
              on behalf of whom a Contribution has been received by Licensor and
              subsequently incorporated within the Work.
        
           2. Grant of Copyright License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              copyright license to reproduce, prepare Derivative Works of,
              publicly display, publicly perform, sublicense, and distribute the
              Work and such Derivative Works in Source or Object form.
        
           3. Grant of Patent License. Subject to the terms and conditions of
              this License, each Contributor hereby grants to You a perpetual,
              worldwide, non-exclusive, no-charge, royalty-free, irrevocable
              (except as stated in this section) patent license to make, have made,
              use, offer to sell, sell, import, and otherwise transfer the Work,
              where such license applies only to those patent claims licensable
              by such Contributor that are necessarily infringed by their
              Contribution(s) alone or by combination of their Contribution(s)
              with the Work to which such Contribution(s) was submitted. If You
              institute patent litigation against any entity (including a
              cross-claim or counterclaim in a lawsuit) alleging that the Work
              or a Contribution incorporated within the Work constitutes direct
              or contributory patent infringement, then any patent licenses
              granted to You under this License for that Work shall terminate
              as of the date such litigation is filed.
        
           4. Redistribution. You may reproduce and distribute copies of the
              Work or Derivative Works thereof in any medium, with or without
              modifications, and in Source or Object form, provided that You
              meet the following conditions:
        
              (a) You must give any other recipients of the Work or
                  Derivative Works a copy of this License; and
        
              (b) You must cause any modified files to carry prominent notices
                  stating that You changed the files; and
        
              (c) You must retain, in the Source form of any Derivative Works
                  that You distribute, all copyright, patent, trademark, and
                  attribution notices from the Source form of the Work,
                  excluding those notices that do not pertain to any part of
                  the Derivative Works; and
        
              (d) If the Work includes a "NOTICE" text file as part of its
                  distribution, then any Derivative Works that You distribute must
                  include a readable copy of the attribution notices contained
                  within such NOTICE file, excluding those notices that do not
                  pertain to any part of the Derivative Works, in at least one
                  of the following places: within a NOTICE text file distributed
                  as part of the Derivative Works; within the Source form or
                  documentation, if provided along with the Derivative Works; or,
                  within a display generated by the Derivative Works, if and
                  wherever such third-party notices normally appear. The contents
                  of the NOTICE file are for informational purposes only and
                  do not modify the License. You may add Your own attribution
                  notices within Derivative Works that You distribute, alongside
                  or as an addendum to the NOTICE text from the Work, provided
                  that such additional attribution notices cannot be construed
                  as modifying the License.
        
              You may add Your own copyright statement to Your modifications and
              may provide additional or different license terms and conditions
              for use, reproduction, or distribution of Your modifications, or
              for any such Derivative Works as a whole, provided Your use,
              reproduction, and distribution of the Work otherwise complies with
              the conditions stated in this License.
        
           5. Submission of Contributions. Unless You explicitly state otherwise,
              any Contribution intentionally submitted for inclusion in the Work
              by You to the Licensor shall be under the terms and conditions of
              this License, without any additional terms or conditions.
              Notwithstanding the above, nothing herein shall supersede or modify
              the terms of any separate license agreement you may have executed
              with Licensor regarding such Contributions.
        
           6. Trademarks. This License does not grant permission to use the trade
              names, trademarks, service marks, or product names of the Licensor,
              except as required for reasonable and customary use in describing the
              origin of the Work and reproducing the content of the NOTICE file.
        
           7. Disclaimer of Warranty. Unless required by applicable law or
              agreed to in writing, Licensor provides the Work (and each
              Contributor provides its Contributions) on an "AS IS" BASIS,
              WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
              implied, including, without limitation, any warranties or conditions
              of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
              PARTICULAR PURPOSE. You are solely responsible for determining the
              appropriateness of using or redistributing the Work and assume any
              risks associated with Your exercise of permissions under this License.
        
           8. Limitation of Liability. In no event and under no legal theory,
              whether in tort (including negligence), contract, or otherwise,
              unless required by applicable law (such as deliberate and grossly
              negligent acts) or agreed to in writing, shall any Contributor be
              liable to You for damages, including any direct, indirect, special,
              incidental, or consequential damages of any character arising as a
              result of this License or out of the use or inability to use the
              Work (including but not limited to damages for loss of goodwill,
              work stoppage, computer failure or malfunction, or any and all
              other commercial damages or losses), even if such Contributor
              has been advised of the possibility of such damages.
        
           9. Accepting Warranty or Additional Liability. While redistributing
              the Work or Derivative Works thereof, You may choose to offer,
              and charge a fee for, acceptance of support, warranty, indemnity,
              or other liability obligations and/or rights consistent with this
              License. However, in accepting such obligations, You may act only
              on Your own behalf and on Your sole responsibility, not on behalf
              of any other Contributor, and only if You agree to indemnify,
              defend, and hold each Contributor harmless for any liability
              incurred by, or claims asserted against, such Contributor by reason
              of your accepting any such warranty or additional liability.
        
           END OF TERMS AND CONDITIONS
        
           APPENDIX: How to apply the Apache License to your work.
        
              To apply the Apache License to your work, attach the following
              boilerplate notice, with the fields enclosed by brackets "[]"
              replaced with your own identifying information. (Don't include
              the brackets!)  The text should be enclosed in the appropriate
              comment syntax for the file format. We also recommend that a
              file or class name and description of purpose be included on the
              same "printed page" as the copyright notice for easier
              identification within third-party archives.
        
           Copyright [yyyy] [name of copyright owner]
        
           Licensed under the Apache License, Version 2.0 (the "License");
           you may not use this file except in compliance with the License.
           You may obtain a copy of the License at
        
               http://www.apache.org/licenses/LICENSE-2.0
        
           Unless required by applicable law or agreed to in writing, software
           distributed under the License is distributed on an "AS IS" BASIS,
           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
           See the License for the specific language governing permissions and
           limitations under the License.
        
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jsonschema[format]<5,>=4.23
Requires-Dist: anyio<5,>=4.8
Requires-Dist: sniffio<2,>=1.3
Provides-Extra: http
Requires-Dist: httpx<1,>=0.28; extra == "http"
Provides-Extra: pydantic
Requires-Dist: pydantic<3,>=2.10; extra == "pydantic"
Dynamic: license-file

# Agent Hooks Protocol SDK for Python

A typed, asynchronous SDK for the [Agent Hooks Protocol](https://github.com/agenthooksprotocol/agent-hooks-protocol) draft. Python 3.11+; AnyIO supports asyncio and Trio. The import package is **`agenthooksprotocol`**, and the primary harness is **`Hooks`**.

## Install

Install the published `agenthooksprotocol` package from [PyPI](https://pypi.org/project/agenthooksprotocol/):

```sh
python -m pip install 'agenthooksprotocol[http]'
```

HTTPX is an opt-in dependency (`[http]`). Pydantic is an independent opt-in integration (`[pydantic]`); neither a web framework nor Pydantic is needed for the core SDK or stdio.

## Harness ownership

Read trusted registration JSON with the standard library and construct `Hooks` from the resulting dictionary. Registration validation occurs at construction; each asynchronous boundary ensures readiness. There is no mandatory separate initialization call or special configuration-file loader. Registration can launch processes: **do not load untrusted registrations**.

Use `async with` for a bounded lifetime. Each awaited hook call owns its content preparation, delivery, and observations; normal return leaves no detached observation work. Hosts own concurrency through ordinary task groups. `aclose()` stops admission, cancels and joins active owned I/O, releases sources, and reaps owned processes. Repeated/concurrent close is safe; calls after close raise `HooksClosedError`. Await calls before closing if graceful completion is wanted. Injected transports/HTTP clients and shared auth providers remain caller-owned.

```python
import json
from agenthooksprotocol import Hooks, Permission, capability, event, state

with open("hooks.json") as file:
    config = json.load(file)

async def review(existing_arguments):
    async with Hooks(
        config,
        source="urn:example:harness",
        harness=event.Harness(name="example", version="1"),
        capabilities=capability.HostCapabilities(
            tool_before=capability.ToolBeforeCapabilities(
                intercept=capability.ToolBeforeIntercept(
                    allow=True, deny=True, modify_input=capability.Modify.REPLACE,
                ),
            ),
        ),
    ) as hooks:
        result = await hooks.tool_before(event.ToolBeforeInput(
            call_id="call-1", name="read_file", input=existing_arguments,
            path="native", origin="native",
        ), initial_state=state.initial(Permission.NONE))
        return result  # Host validation and authorization still precede execution.
```

Generated `Input` objects are distinct from canonical wire events. They flatten only protocol-owned wrappers, never application arguments, and `to_wire()` performs the shared mapping. `path` and `origin` remain separate required facts. Optional host IDs, timestamps, parent IDs, and other facts are retained; `event_id=` overrides an input ID. The SDK supplies type/source, the configured harness identity, and the configured session manifest. Full canonical wire events must include a matching `harness` identity. Low-level `dispatch` accepts canonical dictionaries and an `event_name: EventType`. `from agenthooksprotocol.event import EventType` exposes the catalogue-derived literal type shared by `dispatch`, `subscribes`, and contextual response parsing. Canonical name literals are accepted directly; static checking rejects dynamic strings, unknown names, and selectors in these event-name positions. Registration selectors remain separate and support their documented wildcard matching.

Host capabilities are frozen, keyword-only dataclasses; there are no builder functions. Each event has its own `<Event>Capabilities` declaration. Events that support interception take `intercept=<Event>Intercept(...)` grants, and every declaration takes `observe`, which defaults to `False`, so to advertise both modes you must pass `observe=True` explicitly. Observation-only events such as `SessionEndCapabilities(observe=True)` accept only the flag. `modify_input=capability.Modify.REPLACE` supplies both the effect grant and its operation block, and form/URL elicitation grants are explicit (`elicitation=capability.Elicitation.FORM`, `URL`, or `FORM_AND_URL`). Interception without a control grant and declarations with neither mode are rejected at construction; boundary compatibility is validated before delivery. Omission never implies permission. Per-call `capabilities=` only narrows configured grants.

```python
from agenthooksprotocol import capability

capabilities = capability.HostCapabilities(
    tool_before=capability.ToolBeforeCapabilities(
        intercept=capability.ToolBeforeIntercept(
            allow=True, deny=True, modify_input=capability.Modify.REPLACE
        ),
        observe=True,  # intercept and observe
    ),
    session_end=capability.SessionEndCapabilities(observe=True),  # observe only
)
assert capabilities.to_wire()["tool.before"]["modes"] == ["intercept", "observe"]
assert capabilities.to_wire()["session.end"] == {"modes": ["observe"]}
```

Runnable, statically checked consumer programs live in [`examples/`](examples):

- `value_codecs.py`: independent argument/result/provenance codecs, admission-time invariants, and a form schema derived from its answer type.
- `typed_tool.py`: JSON registration, generated tool input, typed grants, initial state, real in-process ASGI handler, borrowed HTTP client, and explicit application decoding.
- `common_cases.py`: rewrite/allow, deny, invalid application shape, host policy, occurrence narrowing, native initial state, and fail-open/fail-closed reports. The host operation is an in-memory execution recorder, not a shell.
- `stdio_tool.py` / `stdio_backend.py`: a real owned backend process using the same public `hooks.Handlers`, denial, and child cleanup.
- `upload.py`: streamed binary upload, metadata no-read behavior, independent upload credentials, verified upload receipt, and caller-owned immutable storage.

- `lifecycle.py`: caller-owned task groups, operation-owned observations, interrupted requests, idempotent close, and proof that an owned subprocess was reaped.
- `http_auth.py`: real loopback HTTP, canonical backend callbacks, registration `tokenEnv` resolution, and separate content-upload authorization. Its standard-library HTTP routing is example host code, not a production server.

Run them with `uv run python examples/<name>.py`; `uv run mypy` checks the actual public imports and signatures. These examples do not claim an all-pairs cross-language matrix.

## Protocol admission and application input

Generated constructors build JSON-shaped protocol values. Generated `TypedDict` models provide static typing, **not runtime validation**. Structural `parse_*` codecs preserve unknown properties and values; they do not insert defaults, coerce values, or establish permission to execute an operation. Canonical validation and whole-response effect admission remain separate from application-specific input validation.

Capabilities explicitly grant effects and target operations. Supply event capabilities at harness construction; a per-occurrence capability override can narrow them, never widen them. `initial_state` carries the host's native prior decision for one occurrence. A hook-supplied result is a candidate, not an assertion that an operation executed. The host owns its ordinary input validation and actual execution.

Supply `input_codec=`, `result_codec=`, and optional `provenance_codec=` to a named hook method or `dispatch` to declare caller types. Complete staged values are decoded and validated before publication. Object merge is shallow: omitted fields remain unchanged, literal null is retained, and nested values replace whole values. An invalid response publishes none of its effects; earlier accepted responses remain visible to later interceptors. A codec must preserve the wire data it receives, including opaque extra fields. Lossy serialization rejects admission rather than dropping fields.

`result.input` exposes the declared argument value. `result.candidate` is an SDK-owned `Candidate[Result, Provenance]`, or `None` when there is no candidate. Its value can itself be `None`; optional provenance is omitted from `candidate.to_dict()` when absent. Both values are detached snapshots. Without caller codecs, protocol-owned candidate values use canonical typed models and application-owned values remain JSON. `result.event` is the canonical typed event; `result.accepted_responses` retains complete contextual response envelopes in settlement order. `result.messages` and `result.injections` contain typed effects, including injection delivery timing. `result.has_result` distinguishes absence from a supplied null deliverable; `result.result` raises `KeyError` when absent. `result.flow` is the settled `state.Flow`, or `None` when no flow state is supplied; explicit neutral flow is `state.Flow.NONE`. `result.instructions` includes inherited and accepted continuation instructions, with `None` when absent and `[]` for an effective empty list. `result.continuation_remaining` is the settled budget, or `None` when no budget is declared. For explicitly selected opaque host slots, use `event_codecs={("params",): codec}` with event-relative paths. Unspecified native, provider, task, permission, and discovery values are not interpreted.

For forms, construct `PydanticFormContract(TypeAdapter(Answer))` from one answer declaration. `request(message)` derives the restricted MCP schema; `result_codec()` decodes the complete MCP result and its accepted answer while preserving opaque result metadata. Unsupported shapes and schema constraints are rejected during construction. Decline, cancel, and URL results have no form content. See [`examples/value_codecs.py`](examples/value_codecs.py).

A codec implements `encode(value)` and `decode(json_value)`; `IdentityCodec` copies JSON. The optional `integrations.pydantic.PydanticCodec` accepts a Pydantic `TypeAdapter`. Hosts can call `codec.decode(result.input)` for an explicit application check when no input codec is declared. A post-settlement application error does not retroactively reject protocol effects: retain the settled result, accepted responses, and diagnostics, and report host-level rejection without executing.

`result.permission` is the generated `Permission` enum (`NONE`, `ALLOW`, `ASK`, `DENY`) read from canonical settlement, not an independent decision engine. `NONE` is not approval; `ASK` needs host approval; interruption never authorizes execution. `result.input` is the detached accepted argument value. Declared codecs validate accepted modifications; without a declared codec, the host supplies application validation. Candidate, canonical event, accepted responses, and diagnostics remain accessible.

`state.initial(Permission.ALLOW)` represents a native decision **already made for this exact occurrence**, not an authorization shortcut. Its default candidate is absent (`null`); `state.Candidate(value=None)` instead means a supplied JSON-null result. Optional typed provenance records facts, not authenticated identity or execution proof. Canonical modifications still invalidate candidates/approval as specified.

Generated backend helpers such as `effect.DenyEffect(reason="policy")`, `effect.replace_input(new_args)` and `effect.merge_input(changes)`, and `effect.ReturnToolEffect(value=...)` fill discriminants and return canonical effect models. They cannot grant themselves authority: unsupported compound responses are rejected atomically.

### Diagnostics and one call budget

`result.diagnostics` contains generated `diagnostics.Code` values with backend, subscription index, delivery mode, stage, failure policy, and synthetic-denial evidence. Timeout (`deadline_exceeded`), transport failure, validated remote JSON-RPC error (`remote_rpc`), protocol rejection, and content preparation remain distinct. Diagnostics never include backend error messages/data or credentials. Observation failure cannot reopen settlement. Application decode errors and operation exceptions are not recoverable delivery diagnostics.

Use one native deadline scope around the whole operation:

```python
import anyio

with anyio.fail_after(5):
    result = await hooks.tool_before(input)
```

The same remaining budget covers queue/readiness waits, provider waits, selected stream reads/uploads, safe auth retries, interception, and owned observations. Subscription timeouts can shorten it, never reset it. Outer timeout raises `TimeoutError`; explicit task cancellation propagates the native cancellation exception. No partial result is presented as permission to execute. Source/process safety cleanup can extend return beyond the deadline; it is not normal observer processing. Hosts may background the **whole call**, retain its task handle, and await its decision before acting.

### Registration-aware credentials

`Hooks(..., auth_provider=provider)` borrows one provider implementing asynchronous `credential(context)` and `challenge(context, challenge)`. It returns `auth.BearerCredential(token, identity=opaque_attempt_id)` or no credential for an anonymous binding. Context includes the complete selected backend and binding, destination, event/upload purpose, and native cancellation scope. Challenges contain actual status/headers and attempted credential identity, never secret tokens. The harness owns discovery/trust, consent, token exchange/refresh, rotation, caching, and provider lifetime; no SDK OAuth manager or browser worker is required.

The default `auth.EnvironmentAuthProvider` resolves bearer `tokenEnv` or optional `resolve_credential` tokenRef callbacks at delivery time. OAuth bindings require an explicit provider. Missing configured secrets and unsupported mechanisms fail closed. Anonymous success stays anonymous; an explicit provider can authorize challenge discovery under host trust policy. Interception may replay at most once after an eligible 401 Bearer challenge, retaining request identity and serialized body; observations/uploads are never automatically replayed. Upload bindings are independent: event credentials are never an upload fallback. Transport-owned TLS/workload integration is not replaced by this provider.

HTTP registrations accept `transport.headers`, for example:

```json
{"type":"http","url":"https://policy.example/hooks","headers":{"X-Tenant":"engineering","X-Api-Key":{"valueEnv":"POLICY_API_KEY"},"X-Gateway":{"valueRef":"vault/policy"}}}
```

All valid HTTP field names are supported, including `Authorization`, `Cookie`, `Origin`, and protocol headers. Literal API keys and cookies are supported. Values must be safe HTTP field values (no CR, LF, or NUL); this HTTPX transport supports Latin-1 header bytes, including non-ASCII values such as `café`. There is no additional SDK header-size limit. Duplicate names are compared case-insensitively: the last property in configuration iteration order wins.

Transport defaults and authentication are applied first, then configured registration headers override them case-insensitively, including `Content-Type` and `Authorization`. Configured `Authorization`, whether literal or referenced, suppresses provider acquisition, discovery, challenges, and retries. A failed explicit secret lookup fails delivery; it never falls back to OAuth.

`valueEnv` and `valueRef` reuse the bearer secret lookup helper and `Hooks(..., resolve_credential=callback)` callback. The optional contextual `header_resolver.resolve(context)` API remains available for harnesses needing backend, destination, purpose, cancellation, and deadline context. Values resolve once per event or upload operation, are reused on an allowed authentication retry, and rotate on subsequent operations. Errors redact values and references.

Configured headers apply to both event POSTs and backend content uploads, overriding upload defaults and authentication as well. They are not installed globally on borrowed clients, discovery/token requests, or other backends. Existing transport redirect behavior is unchanged (SDK HTTP transports already disable automatic redirects). Custom injected transports retain responsibility for their transport contract.


Registered stdio backends start with a minimal base environment rather than the full host environment. The base is an allowlist of host variables: `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `LANG`, `LC_*`, `TZ`, `TMPDIR`, `TEMP` and `TMP`, plus `SYSTEMROOT`, `WINDIR`, `COMSPEC`, `PATHEXT`, `USERPROFILE`, `APPDATA`, `LOCALAPPDATA` and `PROGRAMDATA` on Windows, where names match case-insensitively. The registration's `transport.env` entries are applied on top as literal strings, with no interpolation, and override base values. `tokenEnv` is still resolved in the host environment and credentials are never copied into the child. Env values are never logged. `Hooks(..., inherit_environment=True)` is an SDK-level opt-out that starts from the full host environment instead. `transports.StdioTransport(..., env=..., inherit_environment=...)` exposes the same controls.

## Static host manifest

`hooks.subscribes("tool.before")` is a pure synchronous static-interest lookup. Use it before expensive input preparation: `False` means no eligible intercept or observe subscription selects the configured event. Exact selectors, `*`, and prefix selectors such as `tool.*` use the same matching as dispatch. Unknown or unconfigured events return `False`. Payload filters are conservatively ignored, so `True` means potential interest, not permission or guaranteed delivery. The lookup does not prepare input, authenticate, upload content, initialize transports, or contact backends.

Configure `Hooks(config, *, source, harness=event.Harness(name="example", version="1"), capabilities=capability.HostCapabilities(...), manifest_metadata=HostManifestMetadata(...))`. The required `source`, `harness`, and `capabilities` configure one host. Harness name and version identify the emitter on every event; SDK-generated events use a detached snapshot of that identity. Typed host capabilities are the sole source of event modes and effect grants. Optional metadata declares transport/authentication facilities, tool paths, content categories, limits, managed policy, gaps, correlation fields, and opaque extras without granting event authority. Metadata cannot contain events or override known fields through extras. `hooks.manifest` returns a detached canonical snapshot.

The frozen, keyword-only host capability model advertises only explicitly declared events and modes. Each event has its own declaration type: `ToolBeforeCapabilities(intercept=..., observe=True)` accepts typed interception options and an observation flag, while observation-only events such as `SessionEndCapabilities(observe=True)` accept only the flag. Empty declarations, empty interception, and invalid option values are rejected at construction, while an empty `HostCapabilities()` grants nothing and elicitation-only interception is a valid facility declaration. `Modify.REPLACE`, `Modify.MERGE`, and `Modify.REPLACE_AND_MERGE` declare complete modification grants. `HostCapabilities.to_wire()` returns fresh canonical declarations directly; it does not parse JSON. Runtime admission still validates registration, request-time grants, and responses. The derived manifest records gaps for omitted events and host-level facilities not supplied through metadata. Explicit empty metadata collections declare no such facility; they do not mean universal support.

`await hooks.session_start(event.SessionStartInput(session=..., trigger="startup", permission_mode="default", items=[]))` needs only occurrence-specific host facts. The SDK supplies `manifest`, `harness`, `type`, and `source`, and supplies `id`/`time` when absent; `event_id=` takes precedence over a supplied ID. Named host inputs do not expose harness identity; the constructor supplies it. Caller payload fields cannot override the configured type, source, or manifest. Configuration, manifest snapshots, and delivered occurrences are independent copies. See [`examples/session_manifest.py`](examples/session_manifest.py) for a runnable, typechecked receiver example.

## Backend servers

`from agenthooksprotocol.server import hooks` exposes event-correlated callback maps. Configure `hooks.Handlers(intercept=hooks.InterceptHandlers(tool_before=before), observe=hooks.ObserveHandlers(tool_before=observer))`. A tool interception callback accepts `hooks.ToolBeforeContext` and returns `hooks.ToolBeforeReply`; an observation callback accepts `hooks.ToolBeforeObserveContext` and returns `None`. Callbacks can be synchronous or asynchronous. Contexts expose the typed event, full params/envelope, and detached raw evidence; interception contexts also expose request-time capabilities and state. Reply envelopes and correlation IDs belong to the server. `hooks.ObserveHandlers(any=observer)` adds an optional catch-all that receives a `hooks.AnyObserveContext` for observed events without their own observer field. `from agenthooksprotocol.server import serve` is the AnyIO stdio server (`server.stdio.serve`).

`hooks.ToolBeforeReply(effects=(hooks.ReplaceInput(arguments), effect.Allow()))` preserves effect order. `hooks.ToolBeforeReply()` is a valid no-op. Reply types constrain event-specific controls; runtime validation still checks canonical/contextual legality and dynamic request grants. Tool contexts provide `decode_input(codec)` and `decode_candidate(result_codec, provenance_codec)` for caller-owned application types without discarding original wire evidence. The same dispatcher serves stdio and the framework-neutral ASGI adapter; ASGI does not require Starlette or FastAPI.

Observation is one-way. It does not acquire effect authority and never waits for an acknowledgement. An optional static **harness** manifest can expose `hooks/capabilities`; backend callbacks are not capability discovery and do not manufacture grants.

The attachment receiver verifies byte length and digest through EOF before committing caller-provided immutable storage. Receiver-allocated references are published only after authorization and successful storage commit. Applications choose storage durability and retention.

## Inline messages and specialized boundaries

Model-visible inputs contain canonical messages with a `role` and ordered
`parts`. Supported roles are `system`, `developer`, `user`, `assistant`, and
`tool`. A text part has `kind="text"`, `mediaType="text/plain"`, and inline
`text`. Serialized JSON is ordinary text. An attachment part has
`kind="attachment"`, a non-text/non-JSON media type, and an immutable binary
body. Construction supplies missing message/part IDs and marks them
`synthesized=True`; explicit IDs remain stable.

```python
from agenthooksprotocol import Attachment

input = {
    "trigger": "manual",
    "items": [{"role": "user", "parts": [
        {"kind": "text", "text": "Review this report."},
        {"kind": "attachment", "mediaType": "application/pdf",
         "body": Attachment.from_bytes(pdf_bytes)},
    ]}],
}
result = await hooks.context_compact_before(input, uploads=authorized_uploads)
async with result:
    data = await result.attachments.read("context.compact.before.items_parts[0][1]")
```

Named hook methods also accept canonical dictionaries with the same direct
attachment bodies. The runtime collects sources and creates receiver-specific
references internally. Construction and metadata/omit projection do not read
or upload attachments. Only schema-owned slots are projected; native payloads,
extensions, and application arguments are opaque. Selection is a ceiling for
each receiver's view: metadata/omit views contain no inline text or attachment
references. Independent receivers apply their own content selections.

Compaction `instructions` and `summary` are canonical text-part lists.
Replacement substitutes the list; merge appends in order. Model-visible message
list targets (`prompt`, `request`, `response`, and tool `output`) have the same
list operations. Tool output edits operate on `tool.after.items`; application
structures are serialized as text parts. Standalone `content` edits require the
boundary context: `user.message.outbound` uses a message list, and accepted form
answers at `user.elicitation.result` use an MCP answer object. Object targets such as tool input and workspace changes use shallow object merge. A response settles
atomically, and each serial receiver sees the committed state. Changed
compaction instructions invalidate an earlier supplied summary.

MCP elicitation `request` and `result` contain selected text parts whose `text`
is the serialized complete MCP JSON payload. The SDK validates parsed payloads,
mode grants, form answers, and correlation without inserting defaults. Result
edits require `original_request=` with the original canonical intercept request
snapshot; answer-object merge is shallow. Elicitation request bodies are immutable
to modification effects. No text/JSON upload callback is needed. Metadata and omit selections
cannot authorize body-dependent effects.

For custom transports, `begin(canonical_request, value_codecs=ValueCodecs(...))` separates acquisition,
cancellation, and acceptance; `exchange(canonical_request)` uses the same
settlement engine. Elicitation correlation can be supplied through
`ContentContext(original_request=..., bindings={"content": ("elicitation", "result")},
principal=authenticated_identity)` and `await pending.accept_content()`.

## Generated models and provenance

Semantic namespaces (`event`, `tool`, `effect`, `capability`, and registration models) and all named event boundaries come from schema metadata, not a selected handwritten helper list. Constructors may supply fixed protocol literals; parsers never repair missing input. Open fields are retained for forward-compatible JSON round trips.

The `wire` namespace exposes canonical JSON-shaped request, notification, effect, and response types with structural codecs. Server callbacks use these raw wire contracts. The low-level `generated` module exports the full schema's typed models, `parse_<root>` / `encode_<root>` codecs, `PROTOCOL_VERSION`, `SCHEMA_REVISION`, and JSON types. `ahp-codegen.lock.json` records immutable generator provenance. Change canonical schemas and generator source in the protocol repository rather than editing generated files.

## Development

```sh
uv sync --group dev --extra http --extra pydantic
uv run python -m unittest discover -s tests
uv run mypy
uv run python -m build
```

Integration tests also use a matching sibling `../agent-hooks-protocol` checkout for shared fixtures and public synthetic certificates. The installed runtime uses its bundled schemas and does not need that checkout. No SDK API executes host tools on your behalf.

### Typed composed payloads

Generated facade constructors retain nested model types through schema
compositions. MCP connection `gaps` parameters accept lists of typed gap models,
and HTTP, SSE, stdio, and custom connection objects expose typed location and gap
attributes. `ModelVisibleItem` is a canonical message with a typed role and ordered parts.

Facade objects are mappings with read-only attributes.
Optional attributes return `None` when absent; mapping membership still records
presence. Omitted and explicit-empty response effects are both neutral; decoded effects provide an empty list without modifying the source wire object. Attributes conflicting with mapping methods use a trailing underscore
(such as `items_`), so `dict.items()` keeps working. Raw wire parsers return lossless mappings. `response.response_for_request(event_type, raw)` returns the generated contextual response selected by the originating event type, not by payload shape.

Use generated nested models for typed constructor arguments. Wire constructors, dictionary decoding, and runtime parsing
check location/evidence alternatives and other structural schema constraints. Application
JSON, native payloads, and extension values remain intentionally dynamic.

Upload receipts and event references are distinct. To attach a successful upload,
use `content.reference(receipt)` (or `{"ref": receipt["ref"]}`). Never attach the
receipt itself: body-selected content carries only the ref, with no outer `size`
or `sha256`. Metadata-only content and body gaps may still disclose size/digest.

### Structural model decoding and effect-family support

```python
from agenthooksprotocol import ContentReference, capability, effect
from agenthooksprotocol._models.capability import Capabilities

caps = Capabilities.from_dict({"effects": ["deny", "vendor.custom"]})
assert caps.supports(capability.EffectName.DENY)
assert caps.supports("vendor.custom")
assert effect.EffectName is capability.EffectName
reference = ContentReference.from_dict({"ref": "opaque", "vendor": {"version": 2}})
```

`supports(effect)` is available on generic and incoming capability models. It
checks the advertised `effects` list only. Nested grants do not imply family
support. The result is **not authorization**: it does not check target, operation,
mode, contextual restrictions, or permission. `EffectName` contains schema-derived
known identifiers; custom family identifiers remain ordinary strings.

Wire-model keyword constructors and `Model.from_dict(mapping)` validate with
the same structural descriptor engine as generated `parse_*` functions. The
original composition is retained, including required members, explicit nulls,
literals, closed enums, forbidden combinations, and union ambiguity. This is the
SDK's structural contract, not complete contextual protocol validation.
`from_dict` checks complete wire values and preserves optional-field presence. Decoded known nested values expose the same attributes as constructed values, such as `connection.gaps[0].reason`. Extension and application bags remain intact and do not acquire effect or attachment authority.

Invalid wire models raise `ValueError`; validation failures carry `result`,
`diagnostics`, and `raw` attributes. Use `parse_*` when you need the full result on
both success and failure, including unknown-variant warnings and lossless raw
values. Open extension values remain intact. Finite `Decimal` values are retained;
non-finite numbers and non-JSON values are rejected. Parsing raw JSON through
`parse_*` retains decimal precision; decoding it first with standard-library
`json.loads` cannot recover precision already lost to a float.

Provide valid enum/literal values and all required wire members to `from_dict`. A null candidate, a candidate with
`{"value": null}`, and a candidate with `{"value": 0}` remain distinct; omitting a
required candidate is an error. Generated input projections and frozen host capability
models are construction APIs, not wire decode entrypoints; their canonical wire
values are validated at the dispatch/parse boundary. Direct dictionary
mutation, standard-library `json.loads`, TypedDict annotations, and type assertions
are not SDK validation entrypoints. Re-parse after manually mutating a mapping.

Nested model attributes expose declared protocol fields. For example, `Capabilities(effects=["modify"], modify={}).modify.input` is `None` when that grant is absent. Family queries are available on generic and per-occurrence capability models.

Use the documented Python spelling for constructor parameters. Use `from_dict`
to decode complete wire-keyed objects. In `from_dict`, distinct keys such as
`toolName` and `tool_name` remain distinct wire/extension keys.

### Owned attachments

Use `Attachment.from_bytes(data)` for immutable Python `bytes`, or
`Attachment.lazy(async_loader, aclose=async_cleanup)` to defer reading. Put
`attachment` directly in an attachment part's `body`. Named `Hooks` methods
accept both dictionary inputs and typed event inputs with the same owner.
For a custom `OwnedContentSource`, use the host-only `content.OwnedAttachment(source)` wrapper in an attachment part's body. This wrapper does not read the source and is not a wire value. The SDK transfers and closes its source under the same invocation ownership rules.

Metadata belongs on the part. See the runnable
[file attachment example](examples/file_attachment.py).

Dispatch transfers ownership once. Successful results retain the exact owner,
including an unread loader, independently of `Hooks` shutdown. Callers close
returned content owners with `async with result:` or `await result.aclose()`.
Cleanup runs for unopened sources too. Create a new attachment for each
invocation; eager attachments can share the same immutable `bytes` object.

The default accepted size is 4 MiB and lazy loading timeout is 30 seconds.
Constructors accept `max_bytes`; lazy construction accepts `timeout`. Loaders
must bound their own allocations. `Attachment(async_stream, ...)` supports
bounded `read(size)`/`receive(max_bytes)` streams. Stream cleanup is shielded and
bounded to one second. Cancellation and failure never retry a loader. Mutable
buffers are rejected, and reads return immutable bytes.

Selected remote binary delivery requires an authorized uploader through
`uploads={backend_id: async_upload}`. Each uploader receives the exact owner's
immutable bytes and returns a committed receipt with `ref`, `size`, and `sha256`.
The SDK validates the receipt against the actual bytes before sending the event.
Upload callbacks remain caller-authorized and scoped to their backend; event
credentials do not authorize uploads to another destination.

`Hooks(..., max_concurrent_uploads=8)` bounds concurrent binary uploads.
The value must be a positive integer. Before serial interception
begins, the SDK prepares uploads for body-selected attachment slots in all
matched subscriptions, including observers. One attachment owner is read once
for multiple subscribers. Metadata, omit, and unmatched selections do not read
or upload its bytes. Result reads and metadata-only delivery require no uploader.

Only confirmed receipts are reused, for the exact attachment owner and backend
ID/subscription index. An uncalled interceptor receives a settled observation
with its confirmed reference after denial or stop. Upload failures are handled by
the affected subscription's failure policy when its delivery is reached; they
do not prevent independent destinations from preparing their uploads. An upload
can commit even if an earlier interceptor later removes the attachment or halts
the chain. Receiver applications manage separate storage durability and retention.
Cancellation cancels and joins pending upload work and closes invocation-owned
sources. Overlapping reads join one materialization and receive the same immutable
bytes or terminal error. Cancelling one reader leaves materialization active for
other readers; the last cancelled reader or owner closure cancels and joins it.
The attachment owns its single pending materialization independently of readers.
A cancelled reader returns and releases its upload permit while another reader
continues. The last reader or owner closure joins cleanup; completed workers are
not retained.

Attachments are the sole byte owners. Binary/file editing is not supported.
Results are invocation-local, not session archives.

### Advanced validation and dispatch

`runtime.Validator` validates bundled canonical schemas and structural codecs. It does not establish request correlation, effect grants, host approval, or permission to execute. Use `Hooks` for complete interception admission.

`server.hooks.Engine` is the framework-neutral backend dispatcher. Supply an explicit validator or static harness manifest when needed. Engine callbacks return protocol effects; the receiving harness validates authority and settles them atomically.

Host session counters (`turns`, `modelRequests`, `toolCalls`, `inputTokens`, `outputTokens`, and extra counter names) are exact nonnegative integers. The SDK preserves supplied values and does not infer counters from events.
