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