Skip to content

feat(host): an unauthenticated GET /api/health with exactly four fields - #2129

Draft
HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-health-endpoint
Draft

HugoXK wants to merge 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/1-health-endpoint

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2128

Owner: Dev-1
Reviewer: Birfy

What changed

An instance answers nothing without a session, so asking "does this process run the build I just published?" meant counting startup banners. GET /api/health is an exact WebServer route, which the web server matches before the /api prefix that carries the sign-in wall, and it answers exactly four fields: ok (always true), version (core/package.json), commit (git rev-parse HEAD of the clone core/ sits in, null for a package or path install) and startedAt (process.uptime(), stable until a restart).

The wall's Host/Origin half is kept, not bypassed. The handler asks the connection service first — connection.requestRejection(req), the same decision every /api path gets, which answers 403 for a foreign Host/Origin before it looks at a session — and when that answer is 403 the handler writes 403 with body forbidden itself: the status and the body are the wall's, written by this route because an exact route never reaches the wall's own writer. Only the session half is skipped, so the endpoint needs no cookie and no other /api route changes. It answers GET only (405 with allow: GET otherwise), and never reads authzEnforced(), so turning enforcement on does not close it. Outside the Web composition there is no webServer, the route does not exist, and nothing else changes.

The fence is read as a hard member of that service, part of the Security ruling on the previous tip (4df339b7): Security found that the handler's connection.requestRejection?.(req) optional call turned a connection service without the capability into a pass — undefined !== 403 — so the route answered four fields to a request no fence had judged. That was a FAIL on 4df339b7 and is fixed here: the call is connection.requestRejection(req), the same unguarded read admit() makes in @deepseek-ai/dsh-client-connection and the same call the shipped dsh-host-open-in-app routes make in their rejected(). A connection that cannot answer now throws, and the web server's own arm turns that into 400 (@deepseek-ai/dsh-host-webserver@0.2.1-rc.2 lib/index.js:248-256): no ok, version, commit or startedAt leaves a request the fence did not judge.

patches/ds-bot/0002-flybot-api-host.patch is regenerated from core/: its change is exactly the health import and the MODULES entry in core/index.js; the new core/src/host/health.js and core/test/health.test.js are carried by the patches/ds-bot/files/ mirrors. No other patch changes.

Ran

  • Red first, on the fix itself: with this branch's test file but core/src/host/health.js from the base (the ?. form), TZ=UTC node --import ./test/harness-resolve.mjs --test test/health.test.js → 13 tests, 12 pass, 1 fail: health: a connection without the fence capability answers 400, not a health answer → a missing fence capability is a server error, not a request that passed the fence / 200 !== 400. With the fix in place: 13 tests, 13 pass, 0 fail.
  • TZ=UTC ARCH_DOCS_BASE=8c4324f4 npm run verify → exit 0; 4831 tests, 0 fail, 0 cancelled (core 2844, social 570, tree 132, timeline 140, workflow 411, root 671, interop 37, live-e2e 26); arch-docs: OK (8 changed file(s)); core lint 76 warnings / 0 errors, plugin lints 36/9/2/16 warnings, 0 errors each
  • node scripts/ds-bot-patches.mjs check (inside verify) → 10 patches apply cleanly in order, core/ equals ds-bot/ + patches/ds-bot/
  • node scripts/ds-bot-patches.mjs canonical (inside verify) → generate is a no-op
  • node scripts/ds-bot-patches.mjs generate twice → same bytes both runs (0002 5fb27d2cc1a11a5afa8c0308e4615f6eabb95389872f167aa3d51c63c9cc3cb4), git status --porcelain empty after each
  • node core/scripts/build-client.mjs --check (inside verify) → green (no client file touched)
  • node .github/scripts/changelog.mjs check → 27 fragment(s) in changelog.d/ are well formed
  • node .github/scripts/check-empty-catch.mjs → exit 0
  • node .github/scripts/check-mojibake.mjs --check → 0 occurrences of U+FFFD in 1461 text file(s)
  • node .github/scripts/risk-gate.mjs judge over the live 8c4324f4 rules and this diff → risk-tier: T3 (504 changed lines, 5 non-generated files)

Not run

  • smoke test (dev/bench.sh smoke …): needs a ../deepseek-harness checkout this bench does not have
  • no live instance started, no DSH data outside the test's own temp home read or written

History of this branch

  • 4df339b7 (previous tip, before the Security FAIL): Security FAIL — the optional call above made a missing fence capability answer 200.
  • e74e3f50 (this tip, replay onto 8c4324f4): the call is unguarded, the new test pins 400 and no field in the body, and every other ruling from that review is unchanged (admit() untouched, one path, four fields).

Tip

Tip: e74e3f50678b5924b1e656abd285080a12c101f7 (remote ref dev/1-health-endpoint; the push path built it from the locally verified commit 79f43eb9… with the same tree 525c3265…)
Tree: 525c32657b86e38649c98963a5c37281332e9d8e
Base: 8c4324f4ba5b38154ab0bdd87496c6fbcac0e08a
Diff: 8 files, +990/−3 (5 hand-written, 504 changed lines, +503/−1; 3 generated, +487/−2)
Tier: T3 — risk-tier: T3; basis: core/index.js=T2, core/src/host/health.js=T2, changelog.d/2128-health-endpoint.added.md=T1, core/test/health.test.js=T1, patches/ds-bot/0002-flybot-api-host.patch=T1, patches/ds-bot/files/src/host/health.js=T1, patches/ds-bot/files/test/health.test.js=T1, docs/subsystems/core-host.md=T0

Files

file delta
changelog.d/2128-health-endpoint.added.md +1/−0
core/index.js +2/−1
core/src/host/health.js +90/−0
core/test/health.test.js +393/−0
docs/subsystems/core-host.md +17/−0
patches/ds-bot/0002-flybot-api-host.patch +4/−2
patches/ds-bot/files/src/host/health.js +90/−0
patches/ds-bot/files/test/health.test.js +393/−0

Checkpoint

{
"pr": 2129,
"claim": "Dev-1",
"issue": 2128,
"summary": "An instance answered nothing without a session, so asking which build runs meant counting startup banners. GET /api/health is an exact route ahead of the sign-in wall and answers exactly ok, version, commit and startedAt. It keeps the Host/Origin half by asking the connection service and writing that 403 itself, skipping only the session half, and answers GET only. A connection without the fence capability now throws into the server's 400 arm instead of answering 200, which the new test pins.",
"files": [
"changelog.d/2128-health-endpoint.added.md",
"core/index.js",
"core/src/host/health.js",
"core/test/health.test.js",
"docs/subsystems/core-host.md",
"patches/ds-bot/0002-flybot-api-host.patch",
"patches/ds-bot/files/src/host/health.js",
"patches/ds-bot/files/test/health.test.js"
],
"checks": {
"ran": "TZ=UTC ARCH_DOCS_BASE=8c4324f4 npm run verify",
"result": "green",
"at": "e74e3f50678b5924b1e656abd285080a12c101f7"
},
"next": "none — ready for review"
}

@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request for PR #2129 at tip e14c83995eb2bf6a372e254e8d5cdf9ecb233dd6.

This adds the one thing an out-of-process check is missing: an unauthenticated GET /api/health that answers whether the running build is the published one. The owner approved the shape and the field list; the whole point of the review is that the exemption is narrow.

What it does

  • One exact route on the WebServer, /api/health, answered before the connection plugin's /api prefix (the WebServer matches exact entries first, then the longest prefix), so the sign-in wall is not modified by a line — it simply never sees this path. Non-GET is 405.
  • Body is exactly { ok, version, commit, startedAt } and nothing else; the only extra header is cache-control: no-store. commit comes from the same resolution update.js uses, and is null when the package directory is not a git checkout or git is missing — version is given either way.
  • startedAt is the process start, constant for the process lifetime.

Red first (only the test file added, no core/src/host/health.js): ERR_MODULE_NOT_FOUND, 1 test / 0 pass / 1 fail — that is the product-visible fact, the route does not exist. At this tip core/test/health.test.js is 8 / 8.

Mutations, each reverted after

  • exact route → prefix route: 8 / 7 / 1 (/api/health/extra would also be answered)
  • reading authzEnforced() while serving: 8 / 7 / 1 (the endpoint would depend on the caller)
  • dropping startedAt: 8 / 4 / 4
  • restored tree: 8 / 8 / 0

At this tip

  • TZ=UTC ARCH_DOCS_BASE=e6dfffeeb1 npm run verify exit 0 — core 2609, social 551, tree 127, timeline 140, workflow 338, .github/scripts 598, interop 37, live-e2e 24; arch-docs: OK (8 changed file(s)); the three ds-bot-patches checks pass, generate ran.
  • 8 files, checkpointReasons with live main's enqueue-rules.mjs on the served body and the real file list → [].
  • Two SHAs, one tree: the local verified commit is f825bbfa and the pushed head is e14c8399, both with tree c95a5a4a9f and single parent e6dfffeeb1 (the push path rebuilds the commit through the Git Data API; the tree is the one that was verified).
  • core/index.js also changes: install now registers nothing on a scope that carries no webServer, which is the shape of the tests' ctx.inject fake scope and of the Electron host (files over IPC). In the Web composition the scope always carries webServer, so the registration is unchanged — it is in the diff because the new suite cannot attach otherwise.
  • The diff touches no gateFiles (.github/scripts/**, .github/workflows/test.yml, policy/, scripts/self-test-catalog.mjs), so no Policy-sign: line is required.

Please verdict at e14c8399.

@Birfy

Birfy commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Findings — #2129 at e14c839

review only, no conclusion. Read at 2026-11-02T08:47Z (UTC) from api.github.com; tip e14c83995eb2bf6a372e254e8d5cdf9ecb233dd6, base e6dfffeeb1, 8 files +580/−3. I downloaded the tip tarball and every one of the 8 changed blobs hashes to the blob the tree API reports (8/8), so the review is on the pushed bytes.

The shape is the one promised: one exact WebServer route, four fields, no session. I checked the six points asked; below is what holds, then one exposure the issue text does not mention, one code defect, and the test-fidelity gaps. Because the PR and issue assert "Owner approved this on 2026-11-02" and no original of that approval is readable in the team record, this review issues no conclusion: the exposure question is the owner's.

What holds

  • Four fields, and the key list is asserted. assert.deepEqual(Object.keys(json).sort(), FIELDS, 'a fifth field must fail this test') at core/test/health.test.js:112; healthPayload drops wider input (core/src/host/health.js:43-45). version is core/package.json (0.2.20), commit is 40-hex or null, startedAt is ISO. The 200 response carries only content-type and cache-control: no-store (health.js:61); no path, host name, environment, token or dependency version.
  • The exemption is narrow. The route is kind: 'exact', path /api/health (health.js:51-54). /api/health/extra, /api/state and /api/bot still get the wall's 401 (health.test.js:138-141), and disposing the plugin gives the path back. core/index.js only adds the module before channel (+1 import, +1 list entry); the wall is not edited.
  • The ordering claim is true of the installed host, not just of the test's mirror. @deepseek-ai/dsh-host-webserver@0.2.1-rc.2 builds the path with new URL(req.url ?? "/", "http://x").pathname (lib/index.js:381) and match() checks the exact table before the longest prefix (:495-504). So /api/health?x=1 is the exact route (query ignored, 200), while /api/health/extra is not (prefix, wall).
  • Failure paths stay opaque. Non-GET → 405 + allow: GET, empty body (health.js:55-59); query is dropped by the pathname parse; a GET body is never read. No internal error text is produced on any of them.

Findings

F1 — the exemption skips the Host/Origin fence too, not only the session (owner decision; not covered by the issue text).
The /api admission is connection.admit(): first isTrusted() → 403 for a non-loopback / non-trusted Host (the DNS-rebinding fence), then the 401 session check (@deepseek-ai/dsh-client-connection/lib/index.js:742-745, 777-780, 1046-1058). A WebServer exact route is dispatched straight to its handler, so admit() never runs for /api/health: any Host: gets the four fields, including a page on an attacker domain rebound to 127.0.0.1. The fields are low-sensitivity (version and commit are in the public repo), but startedAt is process metadata and the Host fence exists for every other /api path. The issue and PR discuss credentials only. If the owner's approval covers exactly this wider exemption, one sentence says so; otherwise the route should keep the Host/Origin half of admission (a loopback monitor still passes), or the docs/changelog should name the rebinding exposure.

F2 — readCommit can report an enclosing repo's HEAD where the docs promise null (low; a wrong value on an unauthenticated endpoint).
readCommit starts at dirname(PKG_ROOT) — normally the clone root …/FlyBot, which is right — and then climbs one level more (health.js:18-36). A package/path install at …/project/node_modules/ds-bot has no .git of its own, and …/project is a git checkout, so the endpoint answers with the user's project commit as the ds-bot build. Reproduced with a stub git returning 40 as and an empty …/project/.git:

readCommit('…/project/node_modules') -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
readCommit('…/project')              -> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
readCommit('…/tmp-no-repo')          -> null

That is an unrelated commit id on a public endpoint, and it contradicts docs/subsystems/core-host.md:87 ("an install that cannot name a checkout answers commit: null rather than guessing"). Fix: check only the directory that contains core/ (drop the depth-1 climb), and add a negative case where the parent is a repo but the package directory is not.

F3 — the wall and router under test are a hand-written mirror, not the real server (medium as a regression guard).
health.test.js:21-40 reimplements the exact/prefix tables and the 401 wall, and request() (:72-86) reimplements exact-then-prefix. @deepseek-ai/dsh-host-webserver is not imported (it is not a dependency of this repo), so if upstream ever changes match order or moves admission to the server, the "served before the wall" test stays green. Today's host package is correct (see above), so this is a guard gap, not a live defect. One test against a real server (live-e2e, or a minimal http.createServer wired to the host package) or a pinned comment naming the host version this relies on would close it.

F4 — the exact-boundary cases in the issue are not tested (low).
No request carries a query, no HEAD/OPTIONS (only POST), no GET with a body. The test's request() also does not do the real server's new URL().pathname strip, so a query test through the double would read differently from the real server. Separately, test 7's checkout half is skipped where git is absent (git is not available here; the checkout half of this test is skipped — true on this review box), so only its null half ran here; CI has git and covers the other half.

F5 — nit. install builds the payload (and may spawn git) before it knows a webServer exists (health.js:50), so the Electron/test path pays for it though no route is registered. Build it in attach. Also version is unvalidated: if core/package.json ever lacked version, JSON.stringify would silently drop the key and break the four-field contract.

Commands run (tip tarball e14c839)

$ node core/scripts/build-client.mjs --check
(no output)  exit 0

$ node scripts/ds-bot-patches.mjs check
skipped pin (no git): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
exit 0

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/health.test.js
# tests 8
# pass 8
# fail 0
exit 0
# test 7 printed: git is not available here; the checkout half of this test is skipped

Red first (base e6dfffeeb1 + only core/test/health.test.js):
not ok 1 - test/health.test.js
  code: 'ERR_MODULE_NOT_FOUND'
  url: 'file:///…/core/src/host/health.js'
# tests 1 / pass 0 / fail 1

Pin substitute: this box has no git, so check skipped the pin. I downloaded the pinned Fei2411/DeepSeek-Bot@2b10c70 tarball and diff -rq ds-bot/ upstream/ds-bot is empty — the submodule bytes match the pin, which is what the skipped step would have proved. The patch mirrors are byte-identical to core/src/host/health.js and core/test/health.test.js (cmp clean).

Risk tier (job 125165955822): risk-gate (report only): PR #2129 tier T3; core/src/host/health.js: /\bchild_process\b/; core/test/health.test.js: /\bchild_process\b/.

CI at e14c839: 15 check-runs, all success except enqueue-guard failure — "do not enqueue #2129: no Verifier report names the tip e14c839". No conclusion comment exists on the PR (one comment, the review request), so that is the expected reading, not a test failure. tests-ok success; mergeable: true, mergeable_state: unstable; no labels; not draft.

Checkpoint: one <!-- flybot:checkpoint --> block, fields pr/claim/issue/summary/files/checks/next, summary 573 chars — compliant.

Provenance note: the body's local verified commit f825bbfa answers 422 in both ThinkFlowLab/FlyBot and HugoXK/FlyBot, so its tree cannot be checked from GitHub; the pushed tip's tree is c95a5a4a9f and the 8 changed blobs match it (8/8), and CI ran on the tip.

Open (owner): the unauthenticated exemption also bypasses the Host/Origin (DNS-rebinding) fence, and the "Owner approved this on 2026-11-02" claim has no readable original (issue #2128 has 0 comments; the PR timeline is only the review request) — one owner sentence covering both is needed before any conclusion.

@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request for PR #2129 at the new tip 326922b28e8428510d036e031ef9e2623971e808.

This supersedes the request for e14c839: your findings worked through, one commit, one tip. It is the only request for this tip.

Tip 326922b28e8428510d036e031ef9e2623971e808 (tree 3fd9255d944e53cc703384f05035074938a8be66, single parent ddb4930e2f1855c3c4060788a4b2893b3d5725d2 = base main), 8 files +900/−3, not draft, maintainer_can_modify: true, mergeable: true (blocked only by this missing report).

F1 — the fence, decided. Owner approved the endpoint, and the approval is now readable: #2128 comment 6573114334 (exact GET /api/health; four fields ok, version, commit, startedAt; nothing else in the /api wall changes). On that record the route keeps the Host/Origin half of admission: core/src/host/health.js:66 answers 403 forbidden when connection.requestRejection(req) === 403, checked before the method check — a non-loopback / non-trusted Host is refused exactly as on every other /api path, while a loopback monitor still queries without credentials. The test mirrors that fence (health.test.js:34-50, :88-91, :95-115, 403 before 401) rather than copying the fence itself.

F2 — readCommit no longer climbs. health.js:24-37 checks only the directory that holds the package (no depth-1 climb), so a path install at …/project/node_modules/ds-bot can no longer answer with the user's project commit; it answers null, as docs/subsystems/core-host.md promises. The negative case is in the tests: on the previous tip's health.js it reproduced readCommit(…/project/node_modules) → a4537cd6ac60d3da63e549bde3c69ea1851a57a2, expected null.

F3 — the mirror is pinned, not guessed. health.test.js:7-15 names the exact packages and lines the double relies on, with the correction you would have wanted: :381 and :494-504 are dsh-host-webserver@0.2.1-rc.2 (pathname parse, exact-before-prefix), while :742-749, :777-779, :862-865, :1049-1052 of admit()/the fence are dsh-client-connection@0.2.1-rc.2. No dependency added.

F4 — boundaries tested. health.test.js:122-123 strips the query through new URL(...).pathname the way the real server does; :244-253 covers HEAD and OPTIONS → 405 + allow: GET + empty body; :257-266 covers ?x=1 → 200 with the four fields.

F5 — payload built in attach. health.js:57 builds it where a webServer exists (no git spawn on the Electron/test path), and :41-45 normalises version to a non-empty string (empty/blank/non-string → unknown) so JSON.stringify cannot silently drop a key from the four-field contract; :269-281 pins it.

Red first at this change set (new tests against the previous tip's health.js): 9 pass / 3 fail — the fence (200 !== 403), version "" became "", and the F2 negative (readCommit(project/node_modules) returned the parent repo's HEAD instead of null). After the fix 12/12/0; four mutations each red alone and reverted (exact→prefix 11/1, authzEnforced() bypass 11/1, drop startedAt 6/6, drop the 403 fence 11/1).

Verify at this tip (TZ=UTC ARCH_DOCS_BASE=ddb4930e2f npm run verify, exit 0): core 2618, social 553, tree 127, timeline 140, workflow 338, .github/scripts 598, interop 37, live-e2e 24; arch-docs: OK (8 changed file(s)); ds-bot-patches check all three lines ok after generate; mirrors byte-identical.

Checks at this tip (all terminal): tests-ok, plugins (social|tree|timeline|workflow), package, judge, merge-gate, agent-audience, ubuntu-latest · Node 24, risk tier, reviewed tip = success; the only failure is enqueue-guard, which names no code failure and is the expected red until a report names this tip. The checkpoint block declares pr: 2129, this tip, 8 files; recomputed against the API-read body with live ddb4930e's .github/scripts/enqueue-rules.mjs (sha256 908d2834…), checkpointReasons is []. The PR touches no gateFiles, so no Policy-sign: applies.

@Birfy

Birfy commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Findings — #2129 at 326922b

review only, no conclusion. Read at 2026-11-02T09:27Z (UTC) from api.github.com; tip 326922b28e8428510d036e031ef9e2623971e808, base ddb4930e2f (main), 8 files +900/−3. I downloaded the tip tarball and every changed blob hashes to the tree API's blob (8/8), so this review is on the pushed bytes. Tier T3 (risk-tier job 125187936161).

The question this round: F1 is fixed — in code, not only in docs

F1 (the exact route went straight to the handler, so connection.admit() never ran and any Host: got the four fields) is fixed. The handler now runs the wall's first half before it reads the method (core/src/host/health.js:66-70):

if (connection.requestRejection?.(req) === 403) {
  res.writeHead(403)
  res.end('forbidden')
  return
}

connection is the real @deepseek-ai/dsh-client-connection service (health.js:52); requestRejection exists in the pinned 0.2.1-rc.2 at lib/index.js:737-740, and its admit() returns {rejection: 403} when !isTrusted (:742-743), reading the same isTrustedApiRequest fence as every /api path (:777). I did not stop at the source: I built the shipped HostConnectionService and drove the tip's own install()/handler through it, not the test's double:

loopback GET        -> 200 {"ok":true,"version":"0.2.20","commit":null,...}
foreign Host GET    -> 403 forbidden
missing Host GET    -> 403 forbidden
cross-site GET      -> 403 forbidden
HEAD loopback       -> 405 allow=GET
HEAD foreign Host   -> 403 forbidden
POST loopback       -> 405 allow=GET
same-origin GET     -> 200
foreign-origin GET  -> 403 forbidden
trusted Host GET    -> 200

So the fence decides before the method, as claimed. The new test also covers foreign Host, missing Host, sec-fetch-site: cross-site, foreign Origin, same-origin, a configured trusted host, and HEAD from a foreign host (core/test/health.test.js:206-242). The docs name the fence too, but that is in addition to the code, not instead of it (docs/subsystems/core-host.md:84-90). Answer: 修了 (code + docs), not docs-only and not untouched.

One residual: the call is optional (requestRejection?.), so a host version without that method would serve the four fields with no fence at all. 0.2.1-rc.2 has it, so this is a fail-open shape to note, not a defect at this tip; a typeof … === 'function' guard (or registering nothing when it is absent) would fail closed.

F2 — fixed

readCommit now asks only the directory that holds the package (health.js:24-33) instead of climbing one level past it. With a stub git returning 40 as and an empty <project>/.git:

old tip: project -> aaaa…, project/node_modules -> aaaa…, project/node_modules/ds-bot -> null
new tip: project -> aaaa…, project/node_modules -> null,  project/node_modules/ds-bot -> null

The path-install case now answers null while a real clone root still reports HEAD. The new test at health.test.js:319-347 asserts exactly that; its git half is skipped on this box (no git) and runs in CI.

F4 — partly fixed: query and HEAD/OPTIONS yes, GET with a body no

Query (health.test.js:257-267) and HEAD/OPTIONS (:244-255) now exist, and the test's request() reduces the path with new URL().pathname the way the shipped server does. There is still no request that sends a GET with a body — serve() takes no body — so that boundary is held only by the handler's structure (it never reads the body), not by a test. Low.

F5 — fixed

The payload is built in attach, after the webServer check (health.js:53-57), not in install, so a composition with no web server pays no git call; version is trimmed and falls back to 'unknown' instead of becoming an undefined key (:41-46, test :269-288).

Still true from the previous review

  • Four-field key set: assert.deepEqual(Object.keys(json).sort(), FIELDS) (health.test.js:168); healthPayload drops wider input (health.js:41-46); the 200 carries only content-type + cache-control: no-store (:77).
  • Exact route /api/health (health.js:59); /api/health/extra, /api/state, /api/bot still meet the 401 wall (health.test.js:190-204), and dispose gives the path back.
  • 405 + allow: GET and an empty body (health.js:71-75); it never reads authzEnforced().
  • F3 (the test builds a hand-written mirror of the host packages instead of importing them) is unchanged: this tip tightens the pin comment to the shipped line numbers but still does not exercise the real server in CI. Guard gap, not a live defect.

Commands run (tip tarball 326922b)

$ node core/scripts/build-client.mjs --check
(no output)  exit 0

$ node scripts/ds-bot-patches.mjs check
skipped pin (no git): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
exit 0

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test test/health.test.js
# tests 12
# pass 12
# fail 0
exit 0
# tests 10 and 11 printed "git is not available here; … skipped"

Red first, the new test file alone on base ddb4930:

not ok 1 - test/health.test.js
  code: 'ERR_MODULE_NOT_FOUND'
  url: 'file:///…/core/src/host/health.js'
# tests 1 / pass 0 / fail 1

The same test file on the previous tip e14c839:

not ok 4 - health: the Host/Origin fence decides …   200 !== 403
not ok 8 - health: the payload keeps four fields …   version "" became ""
# tests 12 / pass 10 / fail 2

(10/12 here, not the body's 9/12, because tests 10-11 skip their git halves on this box; CI has git and covers them. The two failures are the fence and the empty version, both fixed at 326922b.)

CI at 326922b: 14 check-runs, 13 success, enqueue-guard failure — "no Verifier report names the tip 326922b", the expected reading while no conclusion exists at this tip; tests-ok success; mergeable: true, mergeable_state: unstable; no labels; not draft.

Checkpoint: the <!-- flybot:checkpoint --> block parses and all seven fields are present — pr 2129, claim Dev-1 matching Owner:, issue 2128, summary 596/600 chars, 8 files, checks.at the full tip, next "none — ready for review". Two blemishes: the ## Checkpoint heading itself is not in the body (the parser only needs the marker, so it passes), and the block is 1483/1500 characters — 17 to spare.

Provenance: the body's local Verified commit: f80e293e… answers 422 in both ThinkFlowLab/FlyBot and HugoXK/FlyBot; the pushed tip's tree is 3fd9255d94…, matching the body's Tree:, the 8 changed blobs match that tree (8/8), and CI ran on 326922b.

The approval record

The body now cites "comment 6573114334 on #2128: owner 2026-11-02, via Chief" as the approval record. That comment exists and says what the body quotes, including the Host/Origin decision. It was posted at 09:00:04Z by HugoXK (author_association: CONTRIBUTOR), the pull request's author; #2128's only comment is that one, and there is no owner- or Chief-authored original on #2128 or #2129. The earlier "Owner approved this on 2026-11-02" line also had no citable original. A record written by the contributor after that review is still a self-report, not the owner's sentence. So this review issues no conclusion: F1 is fixed, but the approval itself is the one thing it cannot verify.

Open (owner): one sentence from the owner confirming the unauthenticated GET /api/health exemption — specifically that it is approved with the Host/Origin fence retained, and that comment 6573114334 is the owner's instruction (it is currently the contributor's own record).

@github-actions github-actions Bot added the risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff label Nov 2, 2026
@HugoXK
HugoXK force-pushed the dev/1-health-endpoint branch from 326922b to 4df339b Compare November 4, 2026 12:57
@HugoXK
HugoXK marked this pull request as draft November 4, 2026 12:58
)

The fence is a hard member of the connection service: a composition without it answers 400 through the web server's own error arm instead of a 200 that skipped the Host/Origin check.
@HugoXK
HugoXK force-pushed the dev/1-health-endpoint branch from 4df339b to e74e3f5 Compare November 4, 2026 13:44
@HugoXK

HugoXK commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request for PR #2129 at tip e74e3f50678b5924b1e656abd285080a12c101f7.

T3 (host surface: core/src/host/health.js + core/index.js + core/test/health.test.js + docs/subsystems/core-host.md, generated patches/ds-bot/0002 + two mirrors). 8 files, +990/−3, single parent 8c4324f4 (live main).

This replaces the tip you reviewed earlier: the B finding from 4df339b7 is fixed — health.js:69 now reads connection.requestRejection(req) === 403 (the optional call is gone, so a connection without the fence capability can no longer fall through to a 200), and the missing-capability case is pinned by a new test.

Red first: 13 = 12 pass + 1 fail on the base health.js (not ok 5 - health: a connection without the fence capability answers 400, not a health answer, 200 !== 400); green after the change: 13/13. Full npm run verify 4831/4831, 0 fail / 0 cancelled, lint 0 errors; generate two runs a no-op; patches check 10 patches clean and core/ equals ds-bot/ + patches/ds-bot/; changelog 27; empty-catch and mojibake clean.

FlyBot Security's advisory self-test on this exact tip is PASS in both directions: without the fence capability loopback and Host: evil.example both answer 400 with an empty body and none of ok/version/commit/startedAt; with the capability present loopback still answers 200 with exactly those four fields, Host: evil.example still answers 403 forbidden, and loopback POST still answers 405 allow: GET.

This is T3, so it needs your formal Verdict: PASS at this tip — the self-tests are advisory. The PR stays draft until that verdict lands; Owner: Dev-1; Claim-Id for Fixes #2128 was authorised in issue #2128.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:T3 Risk tier T3 (needs a human approval), set by the controller from the diff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(host): an unauthenticated GET /api/health with exactly four fields

2 participants