Repository navigation
feat(server): serve a read-only REST API over the SDK in the OCI image (ADR-0055, #296) - #314
Merged
Merged
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Root confinement is bypassable through a symlinked ADR directory, and several API and ADR-0055 contract issues remain.
6 open findings
ADR directory symlink escapes corpus confinement · New Bracketed IPv6 loopback host fails server binding · New ETag omits representation changes · New Server path view omits governing route · New If-None-Match ignores weak tags and wildcard · New Date validation rejects years 0000 through 0099 · New
What changed in this PR
Adds the ADR-0055 read-only REST server and portal to the OCI image.
Changes:
- Implements REST routes, corpus loading, commit sidecars, and portal serving.
- Adds container wiring, documentation, and dependency boundaries.
- Adds comprehensive server and security tests.
Decision-context MCP was unavailable; ADR-0055 was checked against repository documentation.
| File | Description |
|---|---|
.dockerignore |
Includes server build sources. |
.github/workflows/ci.yml |
Builds and smokes the server image. |
AGENTS.md |
Documents the server contract. |
CHANGELOG.md |
Announces the REST server. |
Containerfile |
Builds and packages the server. |
README.md |
Documents configuration and deployment. |
bun.lock |
Registers the server workspace. |
packages/cli/test/node-compatibility.test.ts |
Extends Node compatibility checks. |
packages/server/package.json |
Defines the private server package. |
packages/server/src/bin.ts |
Implements startup and shutdown. |
packages/server/src/config.ts |
Parses and confines configuration. |
packages/server/src/corpus.ts |
Loads and swaps committed corpora. |
packages/server/src/messages.ts |
Defines fixed public messages. |
packages/server/src/openapi.ts |
Generates OpenAPI metadata. |
packages/server/src/routes.ts |
Defines the route table. |
packages/server/src/server.ts |
Implements HTTP request handling. |
packages/server/src/shapes.ts |
Projects stable response shapes. |
packages/server/src/validate.ts |
Validates request parameters. |
packages/server/test/bin.test.ts |
Tests process behavior. |
packages/server/test/config.test.ts |
Tests configuration parsing. |
packages/server/test/error-text.test.ts |
Tests error-data containment. |
packages/server/test/helpers.ts |
Provides server test fixtures. |
packages/server/test/methods.test.ts |
Enforces read-only methods. |
packages/server/test/openapi.test.ts |
Checks route/OpenAPI agreement. |
packages/server/test/portal.test.ts |
Tests portal delivery. |
packages/server/test/routes.test.ts |
Tests REST behavior. |
packages/server/test/side-effect-denial-preload.mjs |
Traps prohibited side effects. |
packages/server/test/side-effect-denial.test.ts |
Exercises side-effect restrictions. |
packages/server/test/sidecar.test.ts |
Tests sidecar-driven reloads. |
packages/server/test/writing-verbs.test.ts |
Blocks writing capabilities. |
scripts/check-deps.test.ts |
Tests the dependency boundary. |
scripts/check-deps.ts |
Adds the server allowlist. |
scripts/container-contract.test.ts |
Tests container integration. |
scripts/container-entrypoint.sh |
Adds the server selector. |
🧠 Review effort: Balanced
mbeacom
force-pushed
the
feat/rest-server
branch
from
October 10, 2026 22:16
e1f1a6c to
5afd9fd
Compare
Decisions governing this change14 accepted decisions govern this change · 1 active proposal Show all 14 governing decisions and why each applies
Active proposals touching this change (1)These are not yet ratified and do not bind this change:
Links point at |
mbeacom
added a commit
that referenced
this pull request
Oct 10, 2026
…ags, RFC 9110 If-None-Match, and core's asOf rule Review on #314 found five gaps: - Confinement covered only the corpus directory, so an in-root corpus with `docs/adr -> /outside` served outside records. The actual records directory is now realpath-checked inside ADRKIT_SERVER_ROOT before every open and reopen (fixed corpus-escapes-root 503). - `[::1]` passed config but Node's listen rejects the brackets; they are stripped for the bind and kept only in the printed URL. - The ETag now hashes the serialized body too, so a server or SDK upgrade at the same commit can never answer 304 with stale data. - If-None-Match follows RFC 9110 for GET/HEAD: `*`, comma lists, and weak comparison; it applies only to 200 responses. - asOf validation round-trips the ISO date as core does, so years 0000-0099 are accepted exactly as DecisionSet.queue accepts them. Each new test was observed failing first (ADR-0016). Signed-off-by: Mark Beacom <m@beacom.dev>
…e (ADR-0055, #296) @adrkit/server (private) is a node:http server built on @adrkit/sdk and @adrkit/portal's pure builder alone, run by a new `server` selector in the all-in-one image. - GET/HEAD only (405 with Allow otherwise; request bodies are never read), under /api/v1/corpora/{corpus}: records, records/{id}, graph?kinds=, queue?asOf=, issues, governing?path= (markers not read; resolution withheld with the issues on a corpus with errors), plus /api/v1/corpora, /openapi.json generated from the route table, /healthz, and a portal per corpus at /portal/{corpus}/. - Every response is { corpus, commit, data }, with the commit from the `.adrkit-corpus.json` sidecar (503 without a valid one); a new commit opens a fresh handle, and a briefly absent tree (ADR-0056's two-move swap) keeps serving the last one. Fixed error text, fixed-text 400s. - Binds 127.0.0.1 by default; ADRKIT_SERVER_HOST overrides it with a fixed "authenticates no one" warning. Corpora come from ADRKIT_SERVER_ROOT + ADRKIT_SERVER_CORPORA (a key list or JSON), each confined through realpath. No git, no spawn, no outbound network: a side-effect-denial test allows exactly one listener, under Bun on the source and under Node on the bundle. - check-deps admits only @adrkit/sdk and @adrkit/portal; a CI step smokes the image's `server` selector. Every check was observed failing against a planted break (ADR-0016). Signed-off-by: Mark Beacom <m@beacom.dev>
…ags, RFC 9110 If-None-Match, and core's asOf rule Review on #314 found five gaps: - Confinement covered only the corpus directory, so an in-root corpus with `docs/adr -> /outside` served outside records. The actual records directory is now realpath-checked inside ADRKIT_SERVER_ROOT before every open and reopen (fixed corpus-escapes-root 503). - `[::1]` passed config but Node's listen rejects the brackets; they are stripped for the bind and kept only in the printed URL. - The ETag now hashes the serialized body too, so a server or SDK upgrade at the same commit can never answer 304 with stale data. - If-None-Match follows RFC 9110 for GET/HEAD: `*`, comma lists, and weak comparison; it applies only to 200 responses. - asOf validation round-trips the ISO date as core does, so years 0000-0099 are accepted exactly as DecisionSet.queue accepts them. Each new test was observed failing first (ADR-0016). Signed-off-by: Mark Beacom <m@beacom.dev>
mbeacom
force-pushed
the
feat/rest-server
branch
from
October 10, 2026 23:24
ce4355c to
18ed334
Compare
mbeacom
added a commit
that referenced
this pull request
Oct 10, 2026
…est the portal bundle's datetime as-of packages/ci/dist rebuilt under linux/amd64 Bun 1.4.2 on top of main (after #307, #308, #314). The portal bundle now takes the SDK's --as-of grammar: a new bundle test resolves 2026-01-08T23:30:00-05:00 to 2026-01-09, and the usage-error test uses a timezone-less datetime with core's message. Both failed against main's bundle (ADR-0016). Signed-off-by: Mark Beacom <m@beacom.dev>
mbeacom
added a commit
that referenced
this pull request
Oct 10, 2026
…ut publishing it (#295) (#312) * feat(core,cli,sdk): close the SDK's remaining publication gates without publishing it (#295) - ADR-0031 item 8 / ADR-0053 item 3: core exports todayUtcDate, the absent-input `--as-of` default, and replaces every inline copy: adr queue, the queue Action (bundle regenerated), adr new's scaffold, and the SDK's queue(). The portal now takes the SDK's echoed default (queue().asOf) instead of computing its own. Tests ban the inline expression in all of them. - ADR-0031 item 4 / ADR-0053 item 8: `adr explain`, `adr lint`, and `adr new --json` now come from core formatters (packages/core/src/ report). Old and new CLIs produced byte-identical stdout and exit codes on 53 command lines over this corpus and the SDK fixtures; committed goldens hold it. - ADR-0031 item 6 / ADR-0053 item 5: a conformance fixture under packages/core/test/conformance/golden: the CLI must reproduce its queue and check goldens byte for byte, and the SDK must agree with them. - ADR-0031 item 5: docs/RELEASING.md documents the SDK and CLI JSON consumer contracts. Nothing is published: RELEASE_PACKAGES, exports, files, and the no-release assertions are untouched. packages/ci/dist rebuilt under linux/amd64 Bun 1.4.2. Every new test was observed failing first (ADR-0016). Ticked: ADR-0031 items 4, 5, 6, 8 and ADR-0053 items 3, 5, 8. Signed-off-by: Mark Beacom <m@beacom.dev> * fix(sdk,portal): accept the CLI's --as-of grammar in queue(), so the portal takes ISO datetimes Review on #312 found three gaps: - #295 promised the portal would accept ISO datetimes once the default moved to core, but it still refused them. The SDK's queue({ asOf }) now resolves a supplied value through core's resolveAsOf, the grammar `adr queue --as-of` uses (a bare date, or an ISO datetime with an explicit zone), and echoes the resolved UTC date in QueueView.asOf; anything else throws RangeError. The portal passes the caller's asOf to queue() and builds from view.asOf, so it accepts datetimes while importing only the SDK. No new SDK symbol; the doc no longer tells callers to reach into core. - The portal's default-date test could fail across UTC midnight; it now brackets both readings and compares only when no midnight passed. - docs/sdk-surface.md and the ADR-0031 item 8 note still described the portal's removed copy and bare-date rule. New tests were observed failing first (ADR-0016), including SDK/CLI equivalence for a datetime asOf on all four corpora. Signed-off-by: Mark Beacom <m@beacom.dev> * build(ci): rebuild the Action bundles after rebasing onto main, and test the portal bundle's datetime as-of packages/ci/dist rebuilt under linux/amd64 Bun 1.4.2 on top of main (after #307, #308, #314). The portal bundle now takes the SDK's --as-of grammar: a new bundle test resolves 2026-01-08T23:30:00-05:00 to 2026-01-09, and the usage-error test uses a timezone-less datetime with core's message. Both failed against main's bundle (ADR-0016). Signed-off-by: Mark Beacom <m@beacom.dev> --------- Signed-off-by: Mark Beacom <m@beacom.dev>
2 tasks done
mbeacom
added a commit
that referenced
this pull request
Oct 11, 2026
…tion evidence (#316) * docs(adr): record ADR-0054's rung-2 dogfood and ADR-0055's implementation evidence - ADR-0054 item 8 ticked: https://adrkit.dev/decisions/ serves this repository's portal from site.yml via `uses: ./packages/ci/portal` (#310), verified by curl (version "1", 59 records, a real commit). Evidence rung is now 2 (maintainer-owned dogfood); rung 3 not claimed. - ADR-0055 items 1-7 and 9 ticked against #314 and the CI container smoke on main; item 8 (APIM measurement) stays open. Evidence rung 1. - ADR-0053 item 9 stays open: the publication record decides it. docs/sdk-surface.md now names the REST server as the long-lived consumer that exercises every handle member, beside the build-time portal. - AGENTS.md rung notes updated, and the broken ADR-0055 link (`-th.md`) fixed in AGENTS.md, README.md, CHANGELOG.md, and nine packages/server/src header comments. Signed-off-by: Mark Beacom <m@beacom.dev> * docs(adr): keep ADR-0054 at rung 1; the adrkit.dev deploy is dogfood, not rung 2 Copilot pointed out that ADR-0014 defines rung 2 as a run in a separate, isolated reference repository with reproducible, self-verifying evidence, and ADR-0053 records that an in-repository consumer stays rung 1. Item 8 stays ticked because it was done as written, but the evidence rung is 1, item 8's own 'rung-2' label is noted as an overstatement, and rung 2 is named as a portal Action run in an isolated reference repository such as adrkit-t018-dogfood. AGENTS.md matches. Signed-off-by: Mark Beacom <m@beacom.dev> --------- Signed-off-by: Mark Beacom <m@beacom.dev>
mbeacom
added a commit
to mbeacom/adrkit-azure
that referenced
this pull request
Oct 11, 2026
…OpenAPI document (#5) * feat: wire the server's corpora config, portal path, and OpenAPI document Refs #3. Uses the adrkit `server` selector as merged in mbeacom/adrkit#314 (not yet released; v0.20.0 expected). - container_apps: set ADRKIT_SERVER_ROOT=/corpora (the read-only share mount), ADRKIT_SERVER_CORPORA to the repos[].key list (the server's `key[=dir],...` form, bare keys), and ADRKIT_SERVER_PORT from one server_port local shared with ingress and probes. The sync job's contract now names the layout the server reads: <root>/<key>/docs/adr/... and <root>/<key>/.adrkit-corpus.json. - apim: import the server's real OpenAPI 3.0.3 document, vendored from adrkit e491a785 (source and regeneration in modules/apim/openapi/README.md). /healthz is never imported; the /portal/... operations are replaced by one private-mode wildcard GET /portal/*; / stays private-mode only. MCP tools are the seven /api/v1 data operations, now read from mcp-tools.json. - ci: scripts/check-openapi-tools.sh fails if a tool's operationId is missing from the vendored document, maps to anything other than GET /api/v1/..., or if the document has a non-GET operation. Observed failing on planted cases before passing. - variables: image_tag refuses tags below v0.20.0; repos requires at least one entry, because the server refuses to start with no corpus. - docs: README TODO table drops server-config, openapi, and portal-path; measure/ refers to the operation-naming assumption instead of the retired TODO(openapi) tag. Signed-off-by: Mark Beacom <m@beacom.dev> * feat: pin the v0.20.0 server and regenerate its OpenAPI document Closes #3 Signed-off-by: Mark Beacom <m@beacom.dev> --------- Signed-off-by: Mark Beacom <m@beacom.dev>
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.


Closes #296.
What and why
This builds the read-only REST server the accepted ADR-0055 decides.
mbeacom/adrkit-azuredeploys it behind APIM (ADR-0056).@adrkit/serveris private and built only on@adrkit/sdkand@adrkit/portal's pure builder (clause 10). It runs from a newserverselector in the all-in-one image.Allow, and request bodies are never read./api/v1/corpora/{corpus}:records,records/{id}(with body),graph?kinds=,queue?asOf=,issues,governing?path=.governingdoesn't read markers. On a corpus with errors it returnsresolution: withheldwith the issues./api/v1/corpora,/openapi.json(OpenAPI 3.0.3 generated from the route table, with stable operationIds), and/healthz.{ corpus, commit, data }, and the commit comes from.adrkit-corpus.json, the sidecar ADR-0055 and ADR-0056 define./portal/{corpus}/, with a CSP header that includesframe-ancestors 'none'. With one corpus,/is a relative redirect to it.127.0.0.1by default.ADRKIT_SERVER_HOSToverrides that with a fixed warning that the server authenticates no one.Config (answers
adrkit-azure'sTODO(server-config))ADRKIT_SERVER_ROOT(default: the working directory) andADRKIT_SERVER_CORPORA, which takes either akey[=dir],...list or JSON ({"key":"dir"}or{"key":{"dir":…,"adrDir":…}}). Rules:<root>/<key>.<dir>/docs/adrunlessadrDirsays otherwise.<dir>/.adrkit-corpus.json.In Terraform that's
ADRKIT_SERVER_ROOT=<mount>andADRKIT_SERVER_CORPORA=join(",", keys(var.repos)).Checklist
packages/ci/ core: n/a. The CI bundles are unchanged.readMarkersremoved;frame-ancestorsdropped;String(error)returned in a 500;execSyncin a handler (caught under both runtimes);Bun.versionin the source;bun test: 4406 pass.typecheck,lint,check:deps,check:stale-refs,check:changelog,check:doc-pins,check:freeze-hashes, andadr lintpass.Notes for reviewers
.github/workflows/ci.ymlandscripts/.podman build --target adrkitsucceeded, and I ranserverread-only with a:rofixture corpus and sidecar./healthz,/api/v1/corpora(with the commit), andrecords(59) all answered./redirected to the portal, which rendered with no CSP errors andpathTable: absent.ADRKIT_SERVER_HOST, the server was unreachable from the host.governingroute. That text lives in the shared portal page, and changing it means rebuildingpackages/ci/dist.adrkit-azure's harness.adrkit-azure'sTODO(openapi)must filter out/healthzand the/portal/...operations.