Release v0.36.0: authenticated hook transport + acton-service 0.35.0 - #127
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rolls
devup tomainfor the v0.36.0 tag.What's in it
#125 —
fix(hooks): authenticate and encrypt the hook transport. Both halves of the hook path were open. Thehooks generatescaffold emitted a baretonic::transport::Serverbound to0.0.0.0:9090with no auth layer, and the forge-side dispatcher sent plaintext HTTP/2 with no credential. The scaffold now boots throughServiceBuilderwith a live[token]section, so acton auto-appliesGrpcTokenAuthLayerto 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 isclient:schema-forgeso 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.#126 —
chore(deps)!: acton-service 0.34.1 -> 0.35.0.AppState::actorreturns ownedOption<ActorHandle>(18 call sites). More consequentially,ActorExtension::restart_policyis now actually consumed — before 0.35.0 the spawner used the legacysupervise(), which never read the policy, so the declared value was inert and no extension could restart.HookDispatchActorkeeps the defaultPermanent(stateless: every input rides on the message, so aDefault-rebuilt replacement is identical).ForgeActoropts out withTemporary— all of its state arrives in one boot-timeInitForgeand 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
ForgeActorgenuinely restartable (re-initialising from the backend) is a real design change, deliberately not smuggled into a version bump.Release
chore(release)bumpsschema-forge-cli0.35.0 -> 0.36.0 andschema-forge-acton0.34.0 -> 0.35.0. Minor on both: new public surface in acton (hooks::credential, twoHooksConfigfields,HookError::InsecureEndpoint) and changedhooks generateoutput in the CLI.Checks
cargo clippy --workspace --all-targets --features surrealdb: zero warningscargo nextest run --workspace --features surrealdb: 2175 passed, 4 skipped