feat(hooks): daemon /hooks/eval fast-path + guard env-forwarding (B, stage 3a)#612
Merged
Conversation
…stage 3a) The server half of the hybrid dispatcher: a warm endpoint that evaluates the guard set in the already-running daemon, skipping the cold bun spawn + bundle parse the spawned path pays per fire. - POST /hooks/eval (apps/.../dashboard/router/routes/hooks.ts): reads the raw harness event, runs dispatchEvent warm, returns the merged ProcessOutcome. DB-free (only GitEnvLive), fail-open (unreadable body / any error -> allow). Capability `hooks_eval` advertised on /api/version for shim detection. - Env-forwarding (the daemon-correctness fix): a daemon-evaluated guard sees the DAEMON's process.env, not the agent's, so bypass flags would silently break. The payload now carries an `_ax_env` allowlist -> decode -> `event.env`; guards read `readEnv(event, NAME) = event.env?.[NAME] ?? process.env[NAME]` for ALLOW_MAIN_WRITE / ALLOW_BRANCH_CHECKOUT / ALLOW_DIRTY_MAIN_MUTATION + AX_SPEND_MODE. Additive - `process.env` still wins when nothing is forwarded, so the spawned path is byte-for-byte unchanged. Tested without a live serve/DB: - forwarded-env.test: decode `_ax_env` -> event.env (drops non-strings); readEnv precedence; a forwarded ALLOW_MAIN_WRITE=1 (in payload only, NOT process.env) unblocks an Edit-on-main that otherwise blocks - the daemon correctness proof. - hooks.test (route via handleDashboardRequest): no-match -> allow, garbage -> allow (fail-open), empty -> allow, /api/version advertises hooks_eval. - Full suites: 815 pass, 0 fail. typecheck (hooks-sdk + axctl) exit 0. INERT until the shim ships - nothing POSTs to the endpoint yet, so existing installs are unaffected. Stage 3b adds the effect-free bun shim (daemon-first, lazy-import the bundle on fallback) + an opt-in install that swaps `bun dispatch.js` for the shim, with live `ax serve` e2e. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying ax with
|
| Latest commit: |
6e9c4a2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9a964d07.ax-62d.pages.dev |
| Branch Preview URL: | https://feat-hook-eval-endpoint.ax-62d.pages.dev |
This was referenced Jun 25, 2026
|
dude all of this would be so much better if you had a cloud version and the local client just pings local diff... |
Necmttn
added a commit
that referenced
this pull request
Jun 25, 2026
#613) The client half of the hybrid - and the realized latency win. `ax hooks install --all --daemon` swaps the spawned dispatcher for an EFFECT-FREE shim that POSTs to the warm /hooks/eval (#612) and only pays the effect parse on fallback. - shim-core.ts (@ax/hooks-sdk/shim-core, effect-free): `withForwardedEnv` (inject the `_ax_env` bypass allowlist), `runShim` (POST daemon-first with a 2s timeout; on down/timeout/non-2xx LAZY-import the sibling dispatch bundle and run guards locally with the already-read stdin). Standalone bundle = 1.8 KB vs the dispatcher's ~0.9 MB - the fast path is a tiny spawn + a loopback fetch, no effect parse. - dispatch.ts: extract `runDispatchFromStdin` (the shim's fallback entry). - Scaffold + embed the shim (`SHIM_NAME`/`shimScaffoldContent`, sibling-ext aware so .ts falls back to dispatch.ts and the embedded .js to dispatch.js). - install: `--all --daemon` installs the shim; `dispatcherFamilyCommands` + `keepCommand` make a dispatch<->shim switch remove the other entry (no double-fire). Default `--all` (direct dispatcher) unchanged -> zero risk. LIVE-verified against a source-booted `ax serve`: - daemon up: Edit-on-main -> block (warm); + forwarded ALLOW_MAIN_WRITE=1 (in the payload, NOT process.env) -> allow; Read -> allow. - daemon killed: Edit-on-main -> still blocks via the local-bundle fallback. Plus unit tests (withForwardedEnv / isDaemonOutcome / hookEvalUrl, family-switch removal, shim scaffold). 414 hooks tests pass; typecheck (sdk + axctl) exit 0. Completes B. Default install is the dispatcher (size + correctness); `--daemon` is the opt-in latency path, transparent (falls back to the same bundle offline). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Context
Stage 3a of the hybrid dispatcher (B) — on #605/#606/#607. The server half of the daemon fast-path: a warm endpoint that evaluates the guard set inside the already-running
ax serve, skipping the coldbunspawn + ~0.9 MB bundle parse the spawned path pays per fire. This is where the latency win comes from.INERT until the shim ships — nothing POSTs to the endpoint yet, so existing installs are unaffected. Safe to land.
What
POST /hooks/eval(dashboard/router/routes/hooks.ts) — reads the raw harness event, runsdispatchEventwarm, returns the mergedProcessOutcome({exitCode, stdout?, stderr?}). DB-free (onlyGitEnvLive), so it works on source + compiled binary and answers when SurrealDB is down. Fail-open: unreadable body / any error → allow. Capabilityhooks_evaladvertised on/api/versionfor shim detection.Env-forwarding (the daemon-correctness fix) — a daemon-evaluated guard sees the daemon's
process.env, not the agent's, so bypass flags would silently break. The payload now carries an_ax_envallowlist →decode→event.env; guards readreadEnv(event, NAME) = event.env?.[NAME] ?? process.env[NAME]forALLOW_MAIN_WRITE/ALLOW_BRANCH_CHECKOUT/ALLOW_DIRTY_MAIN_MUTATION+AX_SPEND_MODE. Additive —process.envstill wins when nothing is forwarded, so the spawned path is byte-for-byte unchanged.Tested (no live serve/DB)
forwarded-env.test—decode_ax_env→event.env(drops non-strings);readEnvprecedence; a forwardedALLOW_MAIN_WRITE=1(in the payload only, NOTprocess.env) unblocks an Edit-on-main that otherwise blocks — the daemon-correctness proof.hooks.test(route viahandleDashboardRequest, no server boot) — no-match → allow, garbage → allow (fail-open), empty → allow,/api/versionadvertiseshooks_eval.typecheck(hooks-sdk + axctl) → exit 0.Roadmap
install --allto the dispatcher + migrate legacy (B, stage 2b) #607--all→dispatcher+migrate ✅bun dispatch.jsfor the shim, with liveax servee2e. That's where the latency win lands for users.🤖 Generated with Claude Code