Skip to content

Release v0.36.0: authenticated hook transport + acton-service 0.35.0 - #127

Merged
rrrodzilla merged 6 commits into
mainfrom
dev
Aug 4, 2026
Merged

Release v0.36.0: authenticated hook transport + acton-service 0.35.0#127
rrrodzilla merged 6 commits into
mainfrom
dev

Conversation

@rrrodzilla

Copy link
Copy Markdown
Contributor

Rolls dev up to main for the v0.36.0 tag.

What's in it

#125fix(hooks): authenticate and encrypt the hook transport. Both halves of the hook path were open. The hooks generate scaffold emitted a bare tonic::transport::Server bound to 0.0.0.0:9090 with no auth layer, and the forge-side dispatcher sent plaintext HTTP/2 with no credential. The scaffold now boots through ServiceBuilder with a live [token] section, so acton auto-applies GrpcTokenAuthLayer to every registered RPC; the dispatcher refuses non-https:// endpoints at construction and mints a 60s PASETO bearer per call from the generator already built for login. Subject is client:schema-forge so a hook service can tell a forge-originated call from a direct one.

Verified against a running generated service: no token -> grpc-status: 16, garbage token -> 16, /health -> 200.

#126chore(deps)!: acton-service 0.34.1 -> 0.35.0. AppState::actor returns owned Option<ActorHandle> (18 call sites). More consequentially, ActorExtension::restart_policy is now actually consumed — before 0.35.0 the spawner used the legacy supervise(), which never read the policy, so the declared value was inert and no extension could restart. HookDispatchActor keeps the default Permanent (stateless: every input rides on the message, so a Default-rebuilt replacement is identical). ForgeActor opts out with Temporary — all of its state arrives in one boot-time InitForge and nothing sends a second, so a restart would come back with an empty registry and no backend: a live process 404-ing every entity route. Fail-closed, but it would point an operator at a data problem when the fault is a dead actor.

Making ForgeActor genuinely restartable (re-initialising from the backend) is a real design change, deliberately not smuggled into a version bump.

Release

chore(release) bumps schema-forge-cli 0.35.0 -> 0.36.0 and schema-forge-acton 0.34.0 -> 0.35.0. Minor on both: new public surface in acton (hooks::credential, two HooksConfig fields, HookError::InsecureEndpoint) and changed hooks generate output in the CLI.

Checks

  • cargo clippy --workspace --all-targets --features surrealdb: zero warnings
  • cargo nextest run --workspace --features surrealdb: 2175 passed, 4 skipped

The `hooks generate` scaffold emitted a bare `tonic::transport::Server`
bound to 0.0.0.0:9090 with no interceptor and no acton-service
dependency at all, so every RPC answered whoever could reach the port.
The dispatcher matched it: no credential on the request, no TLS on the
channel, and `tonic` resolved without any TLS feature, so an `https://`
endpoint could not have connected even if one had been configured.

A hook invocation carries the entity's full field snapshot and the
subject claim of the user whose request triggered it, so this exposed
entity data with none of the Cedar policy that guards the equivalent
REST surface. `docs/hooks-reference.md` asserted the opposite -- that a
hook service "ships with the same observability, resilience, and auth
primitives as any other acton-service" -- which left no reason for an
operator to go looking.

The webhook subsystem already refused plaintext URLs
(`validate_url_rejects_http`). Hooks were the outlier.

Server: the scaffold now depends on acton-service and serves through
`ServiceBuilder` + `GrpcServicesBuilder`, so a `[token]` section applies
`GrpcTokenAuthLayer` to every registered service automatically. A
starter `config.toml` ships with `[token]` live rather than commented
out, plus commented `[tls]`/`[caller_auth]` for mutual-TLS SAN pinning.
Reflection stays off: it is auth-exempt, so enabling it would publish
the hook message definitions, and therefore the entity field names, to
unauthenticated callers.

Client: `TonicDispatcherConfig` gains a `HookCredentialSource`.
`serve` supplies a `PasetoHookCredential` over the generator it already
builds for login, minting a 60s token with subject
`client:schema-forge` and role `schema-forge-hook-caller` per call --
so a hook service validates with the same `[token]` section it would
use for any other surface, and there is no second key to distribute and
no long-lived shared secret in config. Endpoints must now be `https://`
unless `allow_plaintext` is set, and `client_identity` supplies a client
certificate for mutual-TLS meshes, resolved at startup so a bad key
fails the boot rather than the first hooked write.

The plaintext check runs at construction as well as at dispatch. At
dispatch alone, `required = false` would have downgraded a misconfigured
endpoint to a logged warning and let the hook silently never run, which
is the failure mode this is meant to close.

Verified against a generated scaffold: no token and a garbage token both
answer `grpc-status: 16`, `/health` stays `200`.

acton-service gains `grpc` + `tls`; `crypto-aws-lc-rs` then supplies
`tonic/tls-aws-lc`, and `tls-webpki-roots` supplies the default anchors.
0.35.0 bumps acton-reactive 8.1.1 -> 9.0.0. Two things reach this
workspace.

`AppState::actor` now returns an owned `Option<ActorHandle>` rather
than a borrow, because a restart replaces the actor and a stored handle
would silently go stale. Every route handler already resolved the
handle per request, which is the pattern that change is designed for,
so the migration is mechanical: 18 call sites in `routes/entities.rs`
and `routes/schemas.rs` now pass `&forge` to the helpers that borrow it.

`ActorExtension::restart_policy` is now actually read. Before 0.35.0 the
spawner used the legacy `supervise()`, which never consulted the policy
and registered children with no blueprint, so no extension could be
restarted at all. The declared policy was inert.

That makes the default `Permanent` live for the first time, and the two
extensions here want opposite answers:

`HookDispatchActor` stays `Permanent`. It is `struct HookDispatchActor;`
— every input travels with the `DispatchHook` message, so a replacement
rebuilt from `Default` is indistinguishable from the original. This is a
real gain: post-commit hook dispatch now survives a handler panic.

`ForgeActor` opts out with `Temporary`. Its `configure` registers
handlers but sets no state — registry, backend, tenant config, policy
store, storage registry and hook dispatcher all arrive in the single
`InitForge` that `serve` sends at boot, and nothing would ever send a
second one. A restarted `ForgeActor` would come back with an empty
registry and no backend, leaving a process that stays up and answers
404 on every entity route. That is fail-closed, but it points an
operator at a data problem when the fault is a dead actor. Declining
the restart keeps the failure legible and preserves 0.34.1 behaviour,
where no restart was possible. Making `ForgeActor` restartable means
giving it a way to re-initialise from the backend; that is its own
change, not a side effect of a version bump.

`a_restarted_forge_actor_would_be_unusable` pins the condition rather
than the policy value, so teaching the actor to self-initialise fails
the test and prompts revisiting the policy instead of quietly diverging
from its rationale.

2166 tests pass, clippy clean with --features surrealdb.
fix(hooks): authenticate and encrypt the hook transport
…35.0

# Conflicts:
#	crates/schema-forge-acton/Cargo.toml
chore(deps)!: upgrade acton-service 0.34.1 -> 0.35.0
Release v0.36.0 — authenticated hook transport (PR #125) and the
acton-service 0.34.1 -> 0.35.0 upgrade (PR #126).

Minor bumps, both crates. `schema-forge-acton` gains public surface
(`hooks::credential`, `HooksConfig::{allow_plaintext, client_identity}`,
`HookError::InsecureEndpoint`) and changes hook dispatch behaviour:
plaintext endpoints are now refused at startup and every hook RPC
carries a per-call PASETO bearer. `schema-forge-cli` changes what
`hooks generate` emits: the scaffold boots through `ServiceBuilder`
with a live `[token]` section instead of a bare tonic server, so a
generated hook service authenticates its callers out of the box.
@rrrodzilla
rrrodzilla merged commit 4bf9379 into main Aug 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant