Skip to content

release: v1.6.0 — make the queue an authenticated channel, and make BOOT_CACHE work - #124

Merged
hakeemRash merged 4 commits into
mainfrom
dev-mac
Aug 29, 2026
Merged

release: v1.6.0 — make the queue an authenticated channel, and make BOOT_CACHE work#124
hakeemRash merged 4 commits into
mainfrom
dev-mac

Conversation

@hakeemRash

@hakeemRash hakeemRash commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Why

Four things in the kernel were documented as working and were not. Each is a case where a declaration read as a guarantee and compiled to nothing.

The queue was not an authenticated channel

WorkerLoop has always carried a signature check, but the kernel had no way to give it a key — $signingSecret defaulted to '', was never passed at construction, and no builder method existed. In every deployment that has ever run, it was dead code and the worker executed whatever it was handed.

And the signature it would have checked covered data alone, leaving jobClass — the field deciding which code runs — unauthenticated. Capturing one legitimately signed envelope and swapping its class for any other JobContract was enough; no forgery required.

Kernel::withWorkerSecret() makes the check reachable, and the material is now jobId | jobClass | queue | maxAttempts | canonical(data). Off by default, and deliberately not falling back to APP_KEY — that would switch verification on everywhere at once and reject every job in flight.

A payload that failed verification used to return skipped(), which processWithPort then acked — deleting the only evidence something is writing to your queue. It now dead-letters through the ErrorPipeline.

BOOT_CACHE never hit

build() computed buildHash() twice, before and after resolveEssentialModules() rewrites essentials from proj.json DOMAINS into provider CLASSES. Written under one hash, read under another — so every request recompiled all ten manifests and rewrote the stamp, worse than leaving the flag off.

BOOT_CACHE on, essentials=domain:  2865 us  ->  39 us   (per request, PHP-FPM)

BootStampTest tests the stamp in isolation and could not see this; KernelBootCacheTest builds twice through the real Kernel::build() and watches the manifest inode.

Also

  • No pcntl anywhere — SIGTERM killed the worker mid-flight, including between handle() returning and ack(), so a job that had run its side effects came back and ran them again.
  • retry/timeout in module.json compiled to nothing — every job shared one hardcoded strategy.
  • Request clonesResolveStage 10.02 -> 3.57 us; SecurityGateway 4.17 -> 0.87 us.
  • hkm run ignored its documented ./ default — an args.len <= 2 guard fired before the resolver, which broke hkm run --dev specifically (--dev is stripped before parsing, so it arrived as exactly ["hkm","run"]).

Depends on

AlfaCode-Team/http#7ResolveStage calls Request::withAttributes(), which exists only there. The submodule is pinned to c4fe527 from that PR's branch. If #7 is squash-merged the SHA changes and this pin must be repointed before merging.

Verification

352 tests (from 312), PHPStan clean, launcher builds, and the dev server serves 200 on both routes. Benchmarks re-measured, not recalled.

Risk

Enabling job signing is a two-sided change: the QueuePort adapter must stamp JobPayload::signatureFor() at push() time. Until it does, every payload is rejected — correct, but roll it out producer-first. No migration is needed for the algorithm change, since nothing signed before.

…OOT_CACHE work

Folds the outstanding Unreleased entries into 1.6.0 and adds this cycle's work.

A MINOR bump, not a patch: it adds public API callers can now depend on —
Kernel::withWorkerSecret(), Request::withAttributes(),
SecurityVerdict::allowWithIdentity(), and a memoryLimitMb parameter on
WorkerLoop::run().

The job-signature material changed shape, which would normally be breaking. It
is not: verification was unreachable before this release (WorkerLoop's secret
had no path in from the Kernel and was always ''), so no deployment has signed
payloads in flight to migrate.
The pointer was d1b1366, which lived only in a local checkout on a detached
HEAD. Nothing could resolve it: `git submodule update --init` fails for a clone
and for CI, and `Kernel\Http\Request` never loads — while ResolveStage now calls
withAttributes(), which exists in no pushed commit at all. Releasing against
that pin would have shipped a kernel whose routing calls a method the published
http package does not have.

c4fe527 is the same change cherry-picked onto origin/main (AlfaCode-Team/http#7)
and pushed, so the pin resolves.
@hakeemRash
hakeemRash merged commit 89c2249 into main Aug 29, 2026
11 checks passed
hakeemRash added a commit that referenced this pull request Aug 30, 2026
…e thing (#126)

Two bug fixes found while reading `src/Kernel/` end to end. Merging this
fires `auto-release.yml`: it reads `## [1.6.1]` off the top of the
CHANGELOG, tags `v1.6.1`, builds, publishes, and then the `homebrew` job
bumps the formula's `url` + `sha256` on its own.

## Essential modules never reached a queued job

`HttpPipeline` passed its essentials into `OnDemandLoader`; `WorkerLoop`
built its loader with none, and `Kernel::materialize()` had no way to
hand them over. A module the project declared app-wide in `proj.json`
`"essentials"` was app-wide for requests and **absent from every job**.

For an essential that rebinds a port per scope — tenancy rebinding
`DatabasePort` — the failure is silent rather than loud: the binding
still resolves, just to the wrong connection.

The worker now registers essentials into every job container **and**
seeds their domains into the job's graph — the same two steps
`LoadStage` performs for a request, so transitive `requires[]` come with
them. A job whose class the manifest does not know now also gets a
container rather than the bare `CoreContainer`, since "essential" means
every unit of work. An application declaring no essentials keeps its
previous behaviour, that fallback included.

The class→domain mapping both surfaces need moved to
`DependencyGraphCalculator::domainsFor()`; a private copy in each
pipeline is how they drifted apart in the first place.

## `APP_DEBUG` meant two different things in one file

`ErrorStage::isDebug()` parsed the value with `FILTER_VALIDATE_BOOL`
while `publicError()` compared it `=== 'true'`. So `APP_DEBUG=1` served
the HTML debug page — stack trace and source excerpt — to anything
sending `Accept: text/html`, while every JSON response still masked its
message as "An internal error occurred.". One flag, two behaviours, and
the more revealing of the two was the one that engaged.

There is now one `isDebug()`, used by both. `FILTER_VALIDATE_BOOL` is
the surviving parse because it is what every other kernel flag uses
(`HttpPipeline::flag()`), so `1`, `on`, `yes` and `true` mean the same
thing throughout. It also reads through `env()` rather than
`$_ENV`/`getenv()`: the environment loader deliberately skips
`putenv()`, so `getenv()` is not the source of truth for a `.env` value.

**Note the direction** — with `APP_DEBUG=1` the JSON path now reveals
exception messages, which is what the flag was asked for. Unset or falsy
masks exactly as before.

## Verification

- 359 tests, 664 assertions (was 352; +7 new). PHPStan clean on all five
changed files, no baseline additions.
- Both new tests were run against the **pre-fix** code.
`ErrorStageDebugTest` fails on `APP_DEBUG=1 must reveal the message on
the JSON path` while its three masking assertions still pass — so the
fix does not loosen masking. `WorkerLoopEssentialsTest` errors on
`Unknown named parameter $essentialModules`, which proves it targets the
new capability rather than a changed behaviour: the old loop had no way
to be told about essentials at all.
- Branched from `origin/main` rather than `dev-mac`, which still carries
the unsquashed 1.6.0 commits already merged as #124.

Not touched: `HomebrewFormula/hkm.rb`. Its `sha256` is written by the
release job after publishing, and a digest cannot exist before the
release does.
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.

3 participants