Deferred integration seams
INTEGRATION-ONLY and KNOWN-LIMITATION are grep-able markers in the Go source. This file is the index of what each one currently means, so that reading the unfinished face of the system does not require re-deriving it from 34 comment sites.
It exists for two reasons. The markers do not all mean the same thing — four distinct states share them, and "declared, nothing implements it" reads exactly like "implemented, but its I/O cannot be exercised from this repo". And a marker outlives the condition it describes: two of them were stale when this index was first assembled, both claiming as future work something that had already shipped.
The pattern here is the one internal/updater/doc.go already uses for its own package — state the verification boundary in buckets, so a green test suite is not mistaken for a finished integration. This file is the same idea across the whole tree.
The marker collides with a different vocabulary in troubleshooting.md
docs/troubleshooting.md uses [INTEGRATION-ONLY] for something else, defined in its own opening at :19: the symptom is produced by the kubelet, kaniko or a live handshake, so it cannot be reproduced from the repository. Those twelve marks say where a failure comes from. They are not unfinished work and are not indexed below. A grep across *.md and *.go returns both sets; only the Go ones are seams.
Declared, nothing implements it
internal/updates/seams.go:32—Notifier.internal/mailsends OTP over SMTP, but nothing adapts it to this interface and no in-game channel exists.felis updatepasses nil deliberately. The notification is the panel instead:felis-update-check.timerrunsfelis update --recorddaily on the host, which stores the report underplatform_settings.update_report, and Admin → Updates → Component versions shows it with the command that applies each update.internal/updates/seams.go:43—Applier. Nothing applies an update anywhere. A nil applier is not silent —RunrecordserrNoApplieragainst every planned apply, so a mis-scheduled apply is loud rather than lost.internal/updater/gatherer_integration.go:22— the two current-version seamsNewSysGathererleaves nil, for the in-cluster path: the k8s read of the control-plane Deployment image, and the Velocity jar inspection. Both are answered on the host path (see "Built" below), so this gap is specific to a caller that has a cluster client instead of the node.internal/api/handlers_updates.go— the maintenance window is advisory: no in-cluster runner applies updates.felis updatereads the stored window, prints where now sits against it and warns before an apply outside it; the runner itself still runs with a zero window, so no path can claim an apply is under way.internal/submit/blobstore.go— CLOSED 2026-09-22. The uploads PVC still cannot cross namespaces, so the transport went through the API instead of a mount: the derived context ref is now the internal-face URL (/api/v1/internal/submissions/{id}/context, service-token gated), the build Job'scontext-fetchinitContainer streams it withfelis fetch-contextand extracts under a zip-slip guard into a size-limited emptyDir, and Kaniko builds--context=/context. The token reaches the build namespace through the same Secret-replica mechanism the login gate uses (bootstrap +felis setup), and the build egress lock allows exactly the control namespace on the internal port. Uniform for local and s3:// stores — neither hands the sandboxed build Pod a filesystem view or object-store credentials. Kaniko, Trivy and Trivy's two DBs come from the registry'smirror/copies, which the installer and felis-build-tools.timer keep current (felis mirror-build-tools, docs/troubleshooting.md §8e); the[registry]keys override them.
Built; only its I/O is unverifiable from this repo
Code exists and is unit-tested against fakes. What is missing is a host, a cluster or a real upstream account to run it against — not an implementation.
cmd/felis/tui_edge_apply.go:246,274,295— thenftedge fence, its idempotent teardown, and the cloudflared invocation.internal/cfsetup/runner.go:18andinternal/cfsetup/cfsetup.go:329— the real Cloudflare Tunnel and Access API calls;internal/cfsetup/cfsetup_test.go:11drives the whole flow through a fake.internal/api/console.go:39,internal/api/logstream.go:236,internal/api/logstream.go:306,internal/fileedit/k8sjobs.go:45— each needs a live cluster (RCON,pods/logfollow, a Job). Verified live 2026-09-22/23 (auditfix7–25): the RCON command spine (wake → probe →command/access mutations/stop), the log SSE stream, and the fileedit Job have each run end-to-end on the drill cluster.internal/api/handlers_access.go:170,490— parsing real vanilla and LuckPerms command output. Verified live 2026-09-23: players / whitelist / banlist parses matched a live Paper server's replies (LuckPerms not installed → the raw reply falls through as documented; the input guards held on four negative cases).
Deliberately accepted, not scheduled to close
These are decisions, not backlog. Each names the condition under which it would be worth revisiting.
Closed — the gate moved insideinternal/api/pgrepo.go:281— the quota check andClaimServerare two statements (audit #4 TOCTOU). Closeable only against a real Postgres.ClaimServer(advisory lock + re-check + UPDATE in one transaction), red-then-green in the pgint suite, which is exactly the real-Postgres harness this line was waiting for.internal/api/api.go:773—cooldownLimiteris process-local, so across N api replicas a caller could draw up to N OTP codes per window. The intra-replica burst is closed; cross-replica bounding needs a shared store, out of scope for a single-replica install. Revisit before the api Deployment runs more than one replica.internal/submit/submit.go:524— the per-user upload storage budget reads the stored bytes, then writes. On one replica the API's per-user upload reservation serializes it; across replicas a burst can overshoot by one blob per interleaved upload, each still under the single-blob cap. The pending-submission cap no longer has this shape:CreateSubmissioncounts and inserts under a per-submitter advisory lock (pgintTestSubmitPendingCapHoldsUnderConcurrency). Revisit with the cooldown above, before scaling api replicas: a reservation row per upload in the same kind of transaction closes it.internal/submit/submit.go:436andinternal/submit/submit_test.go:351— a post-CASApprovefailure leaves a row indistinguishable from the benign case, soApprovereturns a distinct error naming the running build rather than allowing a blind re-drive that would double-push. The alternative ordering is worse.cmd/felis/tui_edge_apply.go:246, second marker on the same site — the fence targets nftables. On a firewalld host a reload can flush the standalone table; firewalld-native coordination is not handled. A missingnftbinary fails loud rather than leaving the port open.internal/api/handlers_account.go:169— the reclaim "start fresh vs inherit" choice is CODE-ONLY on the Java/Velocity side; the link-status endpoint reports link completion only and does not surface it.
Wired since the marker was written
internal/api/handlers_email_otp.goandinternal/api/api.go— SMTP shipped on 2026-07-20 (internal/mail, wired incmd/felis/api.go). An install with no[smtp]section leaves theMailernil, and every door that mails a code answers 503mail_unavailable; codes are never logged. The reading "Felis cannot send mail" is stale.internal/config/config.go:117— was stale. It described the upload transport as a deferred integration after both backends had shipped (LocalContextStore,S3ContextStore, selected incmd/felis/api.goby the shape of the configured base). Corrected in the change that added this file; what remains deferred is only Kaniko's read, indexed above.internal/updater/doc.go:44— was stale. Its REMAINING INTEGRATION bullet listed thefelis updateCLI and the off-cluster Velocity jar read, both of which exist (cmd/felis/update.go,internal/updater/gatherer_host.go). Corrected in the same change; the two nil seams it also names are real and remain above.
Recorded outside the code
- The minecraft-namespace egress is locked (
felis-server-egress, DNS plus the public internet with every private range and the node's own global addresses excluded) andfelis-login-to-internal-apiopens the one platform path a game pod needs — login → felis-api:8081. Any new in-cluster service a game server must call needs its own allow policy next to that one (internal/platform/netpol.go).
Source: docs/deferred-seams.md.
