Skip to main content

Provider protocol and capability admission

Design index · Architecture

aw-provider defines the experimental aw-provider/v1alpha1 interface for external policy programs and checks their messages and requested capabilities offline. It connects the unified configuration to a Provider contract without starting a process, installing an Agent Hook or establishing a runtime binding. Native integration belongs to aw-adapter, orchestrated by aw-cli; see the capability matrix for supported capabilities.

Delivery boundary​

LayerCurrent responsibility
aw-configParse one configuration and check its structure and static references
aw-providerValidate protocol messages, correlate responses and check configured steps against supplied Provider and Adapter evidence
aw-contracts / aw-coreValidate and execute their existing capability profiles through a trusted Host and Journal
aw-hostExecute Provider commands, enforce deadlines and output limits, handle cancellation and return correlated call reports
Adapter, aw-cli and aw-serviceAuthenticate native capabilities, register callbacks, translate effects, manage bindings and audit native adoption

Configuration fields are unchanged. A named object under spec.providers selects aw-provider/v1alpha1, a literal transport.argv and private config. A script, binary or CLI client of a background service can implement this interface. AW does not infer or install that program's dependencies.

The configuration example illustrates all 16 event names and later capabilities. Static validation of that example is not a promise that its enabled events, guard or replacement effects pass the narrower admission checks described here.

Transport contract​

The execution contract is one process per method: the caller launches the configured argv, writes one UTF-8 JSON object to stdin and closes stdin. The Provider returns exactly one JSON object on stdout; diagnostic text belongs on stderr. There is no implicit shell expansion. aw-provider specifies and validates the messages; the local Provider Host executes them through aw-exec.

A policy block is a successful result and requires exit status zero. A nonzero exit means execution failed, regardless of stdout. Invalid JSON, oversized messages, mismatched identities and invalid output effects are failures, not policy decisions. The Host must distinguish those failures when applying the configured failure policy; this offline library does not apply native effects.

The request and response schemas are bundled with the crate:

Protocol::new compiles those schemas without network access. Protocol::parse_request validates a request; Request::as_value exposes the checked value. Protocol::check_response checks a response against that request and returns Reply::Description, Reply::Configuration or Reply::Invocation. Messages are limited to 1 MiB and 32 nesting levels; a Host may impose a smaller output limit. Parsing rejects duplicate keys and additional JSON documents. Public envelopes reject unknown fields; Provider-owned configuration and native payloads remain JSON data rather than executable or interpreted settings.

Three methods​

Every request carries api_version, method and a request ID. The Provider must return the same version and request ID. The caller supplies the ID and remains responsible for its lifecycle and correlation with the chosen Provider instance.

describe​

{"api_version":"aw-provider/v1alpha1","method":"describe","request_id":"describe-1"}
{"api_version":"aw-provider/v1alpha1","request_id":"describe-1","status":"ok","operations":[{"name":"check","events":["tool.before"],"effects":["observe","block"]},{"name":"record","events":["tool.after"],"effects":["observe"]}]}

The description declares up to 64 operations and their supported events and effects. Operation names must be nonempty and unique. A declaration is supplied by the Provider; it cannot expand the Adapter's trusted capabilities or establish runtime identity. The vocabulary recognizes all 16 configuration events, while this admission profile permits enabled execution only at tool.before and tool.after.

validate_config​

{"api_version":"aw-provider/v1alpha1","method":"validate_config","request_id":"validate-1","config":{"rule_sets":["command-safety"]}}
{"api_version":"aw-provider/v1alpha1","request_id":"validate-1","status":"ok"}

config is a required JSON object. The Provider owns its schema and must reject unsupported or invalid settings. AW checks the response and retains its association with the exact validated configuration. A successful response does not validate Agent settings, prove that rules were installed or authenticate the Provider executable.

invoke​

An invocation supplies the configured operation, private configuration, configuration revision, remaining budget, admitted effects, normalized event and an opaque input digest. A checked successful response returns that same digest and a list of candidate effects. An empty list imposes no additional restriction; observe requests observation and block requests prevention of the pending tool call. Both are candidate effects; the library itself writes no audit record. None of these responses grants native permission. Outcome::requests_block() reports only whether that candidate effect is present.

Invocation fieldMeaning
operation, configSelected Provider operation and its validated private settings
config_revisionSHA-256 of the caller's exact configuration document, encoded as 64 lowercase hex characters
budget_msPositive remaining execution budget supplied and enforced by the Host
allowed_effectsEffects admitted for this operation and event
event.agentAdapter, configured binding_id and nullable runtime instance_id
event.session_id, event.tool.call_idNative identifiers, or null when unavailable
event.toolname, native_name, call_id, input and result; before-tool result must be null
event.nativeOriginal native JSON object, retained without inventing additional authority
input_digestOpaque binding generated for the complete invocation

The event preserves the configured Adapter and binding, native session and call identifiers when available, the arbitrary tool name, JSON arguments, tool result and original native payload. Missing identifiers remain null. JSON object keys may contain Unicode. Negative integer literals must fit i64, and nonnegative integer literals must fit u64; out-of-range integer literals are rejected. Decimal and exponent forms use finite IEEE-754 f64 values. Arbitrary-precision numbers and lossless round trips of the original numeric spelling are not supported; large identifiers represented as strings remain strings.

Private values and tool results retain their JSON types and Unicode data within these numeric limits. A string result is not silently parsed as another object, and a null result does not certify success. This protocol does not infer that an unknown tool is a shell command or certify the provenance of its arguments.

Protocol::bind_invocation accepts a local invocation without input_digest, hashes its compact JSON serialization and adds the sha256:-prefixed binding. parse_request verifies that binding; the Provider echoes it in its response. Providers need not reproduce the serialization. This digest domain is separate from the stricter canonical wire encoding in aw-contracts. Matching digests do not authenticate the sender or prove adoption. config_revision identifies the configuration supplied by the caller; the library checks its shape, not the existence or contents of a configuration file.

An error response uses status: "error" and error_code, without success fields. A malformed response or a Provider error produces a protocol error. It cannot be converted into a successful block result by the Provider protocol checker. At most 64 effects may be returned. reason_code and error_code contain 1–128 ASCII letters, digits, underscores, hyphens or periods. The library's error display omits untrusted codes and payloads; callers control any diagnostic use.

Offline admission​

admission::preflight(config, target, capabilities) checks enabled configuration requirements against the supported profile and trusted Adapter capabilities without discovery or process execution. The Host runs it before starting any Provider. Its returned steps remain candidates; preflight does not establish Provider support.

admission::admit accepts a statically checked aw_config::Configuration, a target ID, caller-trusted AdapterCapabilities and Provider evidence keyed by the configured Provider IDs. Each Provider entry includes a checked description and checked private-configuration validation. Adapter evidence identifies the Adapter, version, entrypoint and supported event/effect pairs; the caller must obtain those facts from a trusted source appropriate to the actual runtime.

Admission verifies that every enabled selected step has a supported operation, event and effect, and that its validation evidence matches the Provider's exact private configuration. Successful admission returns checked steps. It does not return a Core PreparedPlan, install a binding, start an Agent or report Ready. The current transport profile requires stdio at location agent. Disabled events and steps need no discovery evidence. Step order within an event is preserved; admission does not schedule distinct events or choose parallelism.

RequestAdmission result
tool.before: observe, blockAccepted when the selected operation and Adapter both support every requested effect
tool.after: observeAccepted when both sides support it
tool.after: replace_resultOne structured step with trusted text-replacement capability
ask, input replacement or a block after executionRejected
An enabled event outside the two tool eventsRejected, including when required: false
A guard such as security.violationRejected; final-check ordering is not implemented by this profile
Event and step match.toolsAccepted; the Host invokes only tools matching both selectors
A guarantee beyond native_hookRejected
A missing Provider, operation, capability or matching configuration validationRejected with an explicit reason

Before-tool on_error may be report, or block when the Adapter supports blocking. After-tool failures support report, or withhold_result when the prepared Adapter explicitly declares that event-scoped failure action. Currently only the ordinary QwenPaw resident entrypoint declares it. Other actions are rejected.

required: false does not authorize silently dropping an enabled unsupported step. Every enabled structured Provider still supplies discovery and exact configuration-validation evidence before runtime tool selection.

Event and step selectors intersect; omission or ["*"] covers all tools at that level. Standard selectors bash, file_read and file_write match normalized event.tool.name. Exact native:<adapter>:<name> selectors match event.agent.adapter and retained event.tool.native_name; names are case-sensitive and no glob or regex patterns apply. See the configuration reference. After normalization, binding checks and the once-only claim, an unmatched step returns a Host-owned skipped result with no Provider invocation or effects. Event validation, shared deadlines, cancellation, ordering and completion audit still apply. The Provider receives only selected invocations and owns its private tool extraction and backend policy.

Native denial and pending permission requests retain precedence over neutral or skipped steps. Unsupported argument rewrites remain rejected; a Provider result grants no native permission. permission.request remains vocabulary without an AW interactive approval implementation.

aw-host enforces timeout, event budget and output-limit fields during local execution. Offline admission itself cannot enforce elapsed time, process cleanup, native scheduling or failure handling. Passing admission is therefore only one prerequisite for a runnable native binding.

Relationship to Core and security enforcement​

The current Core profile admits four capabilities: security.command.inspect/v2, security.code.inspect/v2, security.content.inspect/v2 and context.projection.prepare/v2. Its pre-tool plan requires a mandatory command inspection and a required_final_guard; dispatch validation additionally checks the plan's OS requirement against fresh native evidence. Those contracts remain unchanged.

A generic native Hook Provider returning block does not satisfy that command inspection or final-guard contract. Its describe response is not an admitted provider-descriptor-v1, and its invocation response is not a Host-generated provider-receipt-v1. The denied receipt disposition represents an unavailable or denied capability execution, not a successful security verdict: a successful security inspection that rejects a command produces an output with verdict: "deny" and receipt disposition produced.

Wiring these generic operations into Core still requires an explicitly reviewed, versioned native-Hook profile and authenticated Provider identity. aw-host executes transport and validates candidate effects; it does not translate a Core profile or create evidence for Core and Journal. The existing security profile retains its stronger final-dispatch requirements. Native readback is still required to establish that the framework adopted a returned effect. Neither a Provider response nor a journaled call proves that.

Standalone stdio example​

policy.rs implements all three methods as a standalone program. From the repository root, run:

cd src/aw
printf '%s\n' '{"api_version":"aw-provider/v1alpha1","method":"describe","request_id":"demo-describe"}' | \
cargo run --locked -q -p aw-provider --example policy
printf '%s\n' '{"api_version":"aw-provider/v1alpha1","method":"validate_config","request_id":"demo-config","config":{"blocked_tools":["example_tool"]}}' | \
cargo run --locked -q -p aw-provider --example policy

Each command starts a separate Provider process. printf adds a newline and closes the pipe; the example reads stdin through EOF before parsing its single JSON request. The newline is JSON whitespace, not a second message. The first command prints a JSON description of check; the second prints this JSON acknowledgement on stdout, with field order immaterial:

{"api_version":"aw-provider/v1alpha1","request_id":"demo-config","status":"ok"}

The example accepts exactly one private setting, blocked_tools, an array of tool names. Its invoke method compares event.tool.native_name before execution and requests block for a matching name when that effect was admitted. Other calls, including after-tool events, return an empty effect list. This is a sample policy, not a sec-core implementation or a built-in security rule set. Running the example does not start an Agent or cause the AW library to execute commands.

Joint delivery with sec-core​

sec-core can implement the three protocol methods and private policy schema against these schemas and use the local Host for protocol integration. It owns built-in rules, custom policy evaluation and the meaning of a tool-specific safety verdict. A CLI wrapper may connect to a separately managed sec-core service. The wrapper must translate scanner outcomes into declared Provider effects and separate scanner failure from a successful policy block.

AW provides protocol validation, admission and the local bounded Host. Later deliveries add service lifecycle, authenticated Adapter capabilities, native Hook translation and audit integration. Final security.violation composition and its relation to sec-core are part of that later enforcement design; this slice does not claim to supply a last, non-bypassable security check.

The four-framework fixtures verify one common message shape and capability rejection behavior. They do not establish that all native entrypoints load Hooks, that a framework honors a returned block, or that execution cannot bypass a callback. Those claims require separate runtime acceptance.

PostTool text replacement​

A structured tool.after operation may request replace_result when its trusted Adapter evidence supports it. It must be the event's last enabled step; earlier observers, boolean Hooks and native Hooks are retained. A second replacement or any enabled step after replacement is rejected. Boolean Hooks cannot request replacement. Provider and Adapter evidence remain separately checked. Tool selectors preserve this admission order. An unmatched replacement step returns skipped and leaves the original result intact. Only the admitted replacement step receives the optional projection; existing observers keep their original event shape. The Adapter supplies event.tool.result_projection containing text, status, content_origin and an optional shell command. The original result and native payload remain available and participate in the request binding.

A replace_result effect requires a string text. A block effect may include a nonempty bounded text reason; all other effect types reject that field. Responses allow at most one replacement and require an input projection. A missing projection permits only a no-replacement result. The Adapter replaces its authorized text slot and preserves the native envelope and failure status. This is a candidate, not proof that a model consumed it; AW's journal excludes the text. The schema additions retain the experimental v1alpha1 protocol. Deploy matching CLI, service and Providers from the same Preview build; older implementations reject unsupported projections/effects rather than ignoring them.

PreTool retry and PostTool ownership​

The Tokenless Provider admits Protocol v2 pre_tool on tool.before with the existing block effect. For supported shell tools with an object command, it requests replace_arguments: false, block_and_suggest: true. Only Core may select a rewrite. AW returns the full suggested arguments as a model-visible block reason, never updatedInput, native modify, approval, or a shell invocation. Passthrough must preserve the original arguments. A direct-replacement action, changes to non-command fields or an invalid response is a Provider failure.

The model must submit a fresh call to use the suggestion. Both the original and retry traverse selected sec-core before steps and the native permission pipeline; Tokenless neutrality cannot override a denial or a fail-closed scanner error. The combined template retains scanning first, appends PreTool, and retains its completion observer before the final PostTool step. AW does not reorder external Hooks. External mutating Hooks still follow their host's permission contract.

A blocked attempt's call ID cannot identify the retry. The Tokenless integration instead recognizes the standalone attributed RTK wrapper in the arguments actually executed (Agent/session/data directory, absolute rtk executable) and supplies output_optimization: rtk to PostTool. It also passes such retries through PreTool. Recognition decodes quoted/escaped literals and compares state paths through their canonical existing ancestor without creating directories. It skips only the documented execution prefix forms, so an attributed wrapper appearing in another command's arguments is not sufficient. Bare rtk, foreign attribution, unknown or flag-bearing wrappers and compound shell commands are not inferred as RTK-owned. This is an optimization hint, never authorization or recovery access. No persistent AW optimization/recovery store or second compression policy is added. RTK resolution and rewriting remain in the public Tokenless CLI; RTK is an external PreTool dependency, not part of the current AW package payload. CLI failures stay visible under on_error: report; sec-core retains on_error: block.

Lifecycle expansion boundary​

The bridge implements pre_tool by retry and post_tool text replacement. It does not implement transparent argument replacement or before_model Schema replacement. Recovery, Stash, reachable retrieval entry points and retrieval-result bypass belong to the Tokenless integration layer; current requests continue to declare recovery none. Merely enabling PreTool does not advertise complete Tokenless capabilities.

Recovery requires a real Agent-facing entry with the native authorization and scope required by Tokenless, plus Retrieve-output bypass to prevent recompression. Schema replacement requires a model-request seam and its own recovery capability. OpenClaw and Hermes currently lack that schema seam in the supported native integration; advertise partial capabilities instead of claiming four-framework parity. Failed-output diagnostics also need a separate additive result contract: the current bridge intentionally retains failed results unchanged. Tool Ready is hard-disabled upstream and is not part of an active-capability completeness claim.