update AGENTS.md

Kim committed Mar 18, 2026 at 17:14 UTC ce8cb00186b8f050f495229ab2773dc68d34b9e9
3 files changed +79 -204
AGENTS.md
+30 -111
@@ -1,113 +1,32 @@
1 # AGENTS.md
2
3 -## Purpose
4 -
5 -This file is a high-signal rulebook for future agents.
6 -Include only constraints that are expensive to rediscover from quick code search.
7 -
8 -Source of truth for architecture decisions: `docs/adr/README.md` and linked ADRs.
9 -Descriptive docs under `docs/` should match current code paths.
10 -
11 -## Non-Negotiable Architecture Invariants
12 -
13 -1. **Raw TCP reverse-connect is the canonical transport.**
14 - - Why: keeps backend connectivity NAT-friendly and avoids parallel transport stacks.
15 -
16 -2. **Do not introduce websocket or legacy compatibility paths unless a new ADR supersedes ADR-0002.**
17 - - Why: dual transport paths increase security and test surface and reintroduce drift.
18 -
19 -3. **Derive routing hostnames from the full portal root host in `PORTAL_URL` (supports non-apex), not apex extraction.**
20 - - Why: prevents SNI/public URL mismatches in non-apex deployments.
21 -
22 -4. **Keep explicit root-domain fallback behavior through SNI no-route handling to the admin/API listener.**
23 - - Why: preserves the intended split between root-host control-plane traffic and tenant subdomain traffic.
24 -
25 -5. **All leases are TLS-only.** The register endpoint does not accept a non-TLS mode.
26 - - Why: all tenant routes are expected to stay on the TLS passthrough path.
27 -
28 -## TLS and Identity Invariants
29 -
30 -1. **Relay terminates admin/API TLS on the root host and also exposes `/v1/sign` for tenant-side keyless signing.**
31 - - Relay still does not terminate tenant TLS. It peeks ClientHello for SNI and bridges raw encrypted bytes.
32 - - SDK/tunnel endpoints terminate tenant TLS locally with a keyless-backed signer that calls the relay.
33 -
34 -2. **`/sdk/connect`, `/sdk/renew`, and `/sdk/unregister` are authorized by lease existence plus reverse token.**
35 - - `/sdk/register` requires the caller to provide a reverse token for later use, but registration itself is not separately authenticated by that token.
36 -
37 -3. **All relay URLs must be `https://`.**
38 - - Why: SDK and tunnel are expected to hard-fail on insecure relay URLs.
39 -
40 -4. **HTTP/2 is intentionally disabled on the admin/API TLS listener.**
41 - - Why: `/sdk/connect` depends on HTTP/1.1 hijacking semantics.
42 -
43 -## Reverse Session Protocol
44 -
45 -1. **SNI wildcard matching is one-level only.**
46 - - `*.parent.example.com` matches `foo.parent.example.com`, not arbitrary depth.
47 -
48 -2. **Protocol markers on reverse TCP connections are still meaningful protocol state.**
49 - - `0x00` = idle keepalive
50 - - `0x02` = TLS passthrough activation
51 - - Why: SDK waits on these bytes before switching an idle reverse session into a claimed tenant TLS session.
52 -
53 -3. **`/sdk/connect` must remain HTTP/1.1 only.**
54 - - Why: hijacking and long-lived reverse sessions depend on HTTP/1.1 connection ownership.
55 -
56 -## API Response Contract
57 -
58 -1. **All JSON control-plane responses use the `APIEnvelope` wrapper:** `{ ok: bool, data?: any, error?: { code, message } }` (defined in `types/api.go`).
59 - - Write responses through `writeAPIData()`, `writeAPIOK()`, or `writeAPIError()`.
60 - - Admin HTML pages, install script/binary responses, and other non-JSON endpoints are exceptions.
61 -
62 -## Shared Types Package
63 -
64 -1. **`types/` is reserved for shared wire/public types and cross-package constants only.**
65 - - Allowed: request/response DTOs, shared metadata structs, protocol markers, shared headers, shared public path constants.
66 - - Not allowed: relay runtime state, broker/session state, server config, SDK lifecycle state, generic helpers.
67 -
68 -2. **Shared control-plane and public route constants that cross package boundaries belong in `types/paths.go`.**
69 - - Examples: `/sdk/*`, `/v1/sign`, `/healthz`, `/admin`, `/admin/leases`, `/install.sh`, `/install.ps1`, `/install/bin/*`.
70 -
71 -3. **Relay-local frontend asset paths stay local to `cmd/relay-server`.**
72 - - Why: filenames like `favicon.svg` or `portal.jpg` are frontend serving details, not cross-package API contract.
73 -
74 -4. **Do not import `portal` from `cmd/*` or `sdk` just to reach shared DTOs or constants.**
75 - - Why: `portal` is relay runtime code; shared public shapes belong in `types/`.
76 -
77 -## Operational Truths (CI-Aligned, Minimal)
78 -
79 -1. **CI verification commands:** `make vet`, `make lint`, `make test`, `make vuln`.
80 - - `make tidy` is a local maintenance step, not part of CI.
81 -
82 -2. **`make build-server` does not build the frontend or installer binaries first.**
83 - - Why: `cmd/relay-server/dist/*` is embed input; build the frontend and CLI artifacts explicitly before packaging the relay binary.
84 -
85 -3. **ACME management supports only `cloudflare` and `route53`, and keeps both root and wildcard DNS A records in sync for non-localhost deployments.**
86 - - Certificates and keys live under `KEYLESS_DIR` as `fullchain.pem` and `privatekey.pem`.
87 - - Localhost uses the development certificate path instead of DNS-provider-managed ACME.
88 -
89 -## Change Discipline
90 -
91 -1. If a code change violates any invariant above, update or add ADR and AGENTS in the same change set.
92 - - Why: keeps architecture docs and implementation synchronized.
93 -
94 -2. Do not expand this file into repo summary, file tree guide, or generic handbook.
95 - - Why: high-noise AGENTS degrades future agent effectiveness.
96 -
97 -## Go Conventions
98 -
99 -**Imports:** stdlib -> external -> internal (blank-line separated), local prefix `github.com/gosuda/portal/v2`.
100 -**Concurrency:** `errgroup.Group` over `WaitGroup`; `errgroup.SetLimit` for bounded work; `context.WithTimeout` over `time.After` in loops; no bare `go func()` without owned lifecycle.
101 -**I/O:** prefer directory-scoped operations when touching filesystem trees.
102 -
103 ----
104 -
105 -## Agent Behavior
106 -
107 -**Verbalized sampling:** Before changes, sample 3-5 intent hypotheses (rank by likelihood, note one weakness each), assess coupling (structural/temporal/semantic), and tidy-first when coupling is high. Ask the human when trade-offs exist.
108 -
109 -**Project rules:**
110 -- No backward compatibility unless explicitly requested.
111 -- No wrapper functions without demonstrated value.
112 -- Consolidate changes in one pass; do not stack minimal patches.
113 -- Run tests only when requested, before handoff, or for high-risk changes.
3 +Keep this file short and behavioral.
4 +Architecture, product behavior, and design rationale belong in `docs/architecture.md` and `docs/adr/README.md`.
5 +
6 +## Development Principles
7 +
8 +- Minimizing concepts, duplication, and ceremony.
9 +- Prefer a single stable contract with one real owner.
10 +- Prefer local simplicity over premature or speculative abstraction.
11 +- Add indirection only when it removes real coupling or protects a real boundary.
12 +- Tests should protect stable contracts and invariants, not drive the spec.
13 +
14 +## Project Principles
15 +
16 +- No wrapper functions or helpers without demonstrated value.
17 +- Prefer fewer concepts and direct code over extra layers, facades, and indirection.
18 +- Prefer flattening and merging nearby responsibilities over splitting files or packages by default.
19 +- Remove dead fields, dead methods, dead config, and stale state while touching nearby code.
20 +- Avoid duplicate normalization, copying, and caching unless aliasing or trust boundaries require it.
21 +- Keep stable shared contracts, shared constants, and public paths in `types/`, not runtime state, package-local logic, or generic helpers.
22 +- Keep shared stateless transforms in `utils/`; keep stateful and domain-shaped logic with the real owner.
23 +- Resolve complexity in the lowest coherent owner and expose only the minimum necessary surface upward.
24 +- Shared runtime logic should live in one real owner and be reused, not mirrored by parallel helpers.
25 +- Prefer not preserving backward compatibility by default, but ask when breaking it may cause real downstream problems.
26 +
27 +## Verification
28 +
29 +- CI commands: `make vet`, `make lint`, `make test`, `make vuln`.
30 +- `make tidy` is local maintenance, not a CI requirement.
31 +- Run tests only when explicitly requested.
32 +- If verification seems necessary, ask before running it.
docs/architecture.md
+49 -1
@@ -15,6 +15,50 @@ Client (Browser)
15 -> Local service
16 ```
17
18 +## Architecture Invariants
19 +
20 +### Transport and Routing
21 +
22 +- Raw TCP reverse-connect is the canonical stream transport.
23 +- Do not introduce websocket or legacy compatibility paths unless a new ADR supersedes ADR-0002.
24 +- Derive lease hostnames from the full normalized `PORTAL_URL` host, not from apex extraction.
25 +- Preserve explicit root-host fallback through SNI no-route handling to the admin/API listener.
26 +- All leases are TLS-only. Registration does not negotiate or preserve a non-TLS mode.
27 +
28 +### TLS and Identity
29 +
30 +- Relay terminates admin/API TLS on the root host and exposes `/v1/sign` for tenant-side keyless signing.
31 +- Relay does not terminate tenant TLS. It peeks ClientHello for SNI and bridges raw encrypted bytes after routing.
32 +- SDK/tunnel endpoints terminate tenant TLS locally with a keyless-backed signer that calls the relay.
33 +- `/sdk/connect`, `/sdk/renew`, and `/sdk/unregister` are authorized by lease existence plus reverse token.
34 +- `/sdk/register` requires the caller to supply the reverse token that later authorizes the lease lifecycle, but registration itself is not separately authenticated by that token.
35 +- Relay URLs must use `https://`.
36 +- HTTP/2 stays disabled on the admin/API TLS listener because `/sdk/connect` depends on HTTP/1.1 hijacking semantics.
37 +
38 +### Reverse Session Protocol
39 +
40 +- SNI wildcard matching is one level only. `*.parent.example.com` matches `foo.parent.example.com`, not deeper labels.
41 +- Reverse TCP marker bytes remain protocol state:
42 + - `0x00` = idle keepalive
43 + - `0x02` = TLS passthrough activation
44 +- `/sdk/connect` remains HTTP/1.1 only.
45 +
46 +### JSON and Shared Contract
47 +
48 +- All JSON control-plane responses use `APIEnvelope`: `{ ok, data?, error? }`.
49 +- JSON handlers should write responses through the shared API helpers.
50 +- `types/` is reserved for shared wire/public types and cross-package constants only.
51 +- Shared control-plane and public route constants belong in `types/paths.go`.
52 +- Relay-local frontend asset filenames stay local to `cmd/relay-server`.
53 +- Do not import `portal` from `cmd/*` or `sdk` just to reach shared DTOs or constants.
54 +
55 +### Operational Constraints
56 +
57 +- For non-localhost deployments, ACME management supports only `cloudflare` and `route53`.
58 +- Non-localhost ACME keeps both root and wildcard DNS A records in sync.
59 +- Relay certificate material lives under `KEYLESS_DIR` as `fullchain.pem` and `privatekey.pem`.
60 +- Localhost uses the development certificate path instead of DNS-provider-managed ACME.
61 +
62 ## Connection Model
63
64 Portal has two distinct network roles:
@@ -100,6 +144,7 @@ Result: the relay decides routing, but tenant TLS termination still happens at t
144 - `reverse_token`
145 - optional `metadata`
146 - optional `ttl`
147 +- No non-TLS mode is accepted or negotiated
148 - `name` must be a valid single DNS label and relay publishes the lease at `<name>.<root host>`
149 - Registration reserves the hostname and publishes the route immediately; if no reverse session is ready yet, inbound SNI claims wait up to `ClaimTimeout`
150 - `PORTAL_URL` is normalized to its host component only; path/query segments are ignored for routing
@@ -112,6 +157,7 @@ Result: the relay decides routing, but tenant TLS termination still happens at t
157 - Relay validates:
158 - lease exists and is not expired
159 - reverse token matches the registered lease token
160 +- After claim, relay writes `0x02` before switching the session into tenant TLS passthrough
161 - After hijack, the connection becomes a broker-managed reverse session
162
163 ### 3. Renew
@@ -168,7 +214,9 @@ Cross-package public contract lives in:
214 - API envelope
215 - shared request/response DTOs
216 - lease metadata
171 - - reverse marker/header constants
217 +- `types/types.go`
218 + - shared headers
219 + - reverse marker constants
220 - `types/paths.go`
221 - shared `/sdk/*`, admin, health, install, and signer paths
222
docs/glossary.md deleted
-92
@@ -1,92 +0,0 @@
1 -# Glossary
2 -
3 -Key terms used in Portal.
4 -
5 -## Portal / Relay
6 -
7 -The central server that handles lease registration, SNI routing, reverse-session brokering, admin/API TLS, and keyless signing.
8 -It does not terminate tenant TLS.
9 -
10 -## App (Service Publisher)
11 -
12 -A backend service connected to Portal through the `portal` CLI (`cmd/portal-tunnel`) or the native Go SDK.
13 -An app publishes one or more leases and serves traffic from a local process.
14 -
15 -## Client (Service Consumer)
16 -
17 -A browser or external caller that accesses a published service through relay-managed hostnames.
18 -
19 -## Control-Plane Request
20 -
21 -An ordinary HTTPS request to the relay admin/API listener, such as:
22 -
23 -- `/sdk/register`
24 -- `/sdk/renew`
25 -- `/sdk/unregister`
26 -- `/sdk/domain`
27 -
28 -These use the JSON API envelope contract.
29 -
30 -## Reverse Session
31 -
32 -A long-lived raw TCP connection opened through `GET /sdk/connect?lease_id=...`.
33 -The relay hijacks it, keeps it idle in a lease broker, and later claims it for one tenant TLS passthrough connection.
34 -
35 -## Lease
36 -
37 -Portal's routing and advertisement unit.
38 -Each lease has an ID, display name, hostnames, metadata, expiry, reverse token, and a broker of ready reverse sessions.
39 -
40 -## Lease Name
41 -
42 -The canonical single DNS label for a lease. Portal publishes the lease at `<name>.<root host>` and also uses the same value for admin/UI display.
43 -
44 -## Lease Broker
45 -
46 -The relay-side owner of reverse session state for one lease.
47 -It manages the ready queue, claim/wakeup behavior, drop/stop lifecycle, and idle keepalive policy.
48 -
49 -## Reverse Token
50 -
51 -A per-lease secret supplied at registration time and later required by:
52 -
53 -- `/sdk/connect`
54 -- `/sdk/renew`
55 -- `/sdk/unregister`
56 -
57 -It authorizes reverse-session and lease-lifecycle operations.
58 -
59 -## Route Table
60 -
61 -The relay hostname map that resolves exact and single-label wildcard matches to a lease ID.
62 -
63 -## SNI Routing
64 -
65 -The relay reads ClientHello to extract the requested hostname, chooses a lease route, and then bridges the original encrypted TLS stream without terminating it.
66 -
67 -## Keyless TLS
68 -
69 -A mode where the SDK/tunnel terminates tenant TLS locally while delegating private-key signing to the relay `/v1/sign` endpoint.
70 -This keeps the relay out of the tenant data plane while avoiding direct private-key distribution to every backend host.
71 -
72 -## ACME DNS-01
73 -
74 -The relay certificate issuance and renewal path for non-localhost deployments.
75 -It currently supports `cloudflare` and `route53` to provision the root and wildcard certificate coverage used by the relay.
76 -
77 -## Base Domain / Root Host
78 -
79 -The host extracted from `PORTAL_URL` after removing scheme, port, path, query, and fragment.
80 -For `https://portal.example.com:8443/admin`, the root host is `portal.example.com`.
81 -
82 -## Admin/API Server
83 -
84 -The relay HTTPS listener (default `:4017`) serving:
85 -
86 -- `/sdk/*`
87 -- `/admin`
88 -- `/admin/snapshot`
89 -- `/admin/leases/*`
90 -- `/healthz`
91 -- `/v1/sign`
92 -- frontend root/app routes through root-host fallback