feat+test+docs: D68-D70 — bin/olp-connect + /health.anonymousKey + ADR 0011 (#43)

* feat+test+docs: D68+D69+D70 — bin/olp-connect + /health.anonymousKey + ADR 0011

Third substantive Phase 4 implementation. 3 D-days bundled per Iron Rule
11 IDR — olp-connect consumes /health.anonymousKey for zero-config
client setup, both governed by ADR 0011's trusted-LAN-only invariant.

## D68 — bin/olp-connect (zero-config client setup)

Ports OCP ocp-connect (721 lines) → OLP olp-connect (564 lines, pure bash).
Bash over Node (per ADR 0010 § Notes) because client machines may lack
recent Node; bash + curl + python3 = max portability.

CLI: `olp-connect <host-ip> [--port PORT] [--key API_KEY] [--no-system-env]
                            [--dry-run] [--help] [--version]`

Workflow:
1. Connectivity probe (curl /health, 5s timeout, distinguishes TCP
   unreachable from auth-required)
2. Auth resolution: --key flag → /health.anonymousKey (D69) → interactive
   prompt fallback
3. Smoke test (GET /v1/models with bearer)
4. IDE detection + per-IDE config:
   - Claude Code: detect + warn (NOT supported as OLP client per ADR 0010)
   - Cline: detect + print manual VSCode-settings snippet
   - Continue.dev: detect (extension OR ~/.continue/config.yaml) + write
     idempotent models: entry
   - Cursor: detect + print snippet + WARNING (per prior-art known
     base-URL fragility)
   - Aider: detect + write OPENAI_API_BASE + OPENAI_API_KEY to rc files
   - OpenClaw: detect + print "install /olp plugin (D71-D73 deliverable)"
5. System-level env: macOS launchctl setenv / Linux ~/.config/environment.d
   (so VSCode/Cursor started via Dock inherit)
6. Summary + test command

Idempotent (bracketed `# OLP LAN (added by olp-connect)` ... `# /OLP LAN`
blocks in rc files). --dry-run exercises every state-change site without
modifying anything. Exit 0/1/2 conventions.

Installed via package.json bin so `npx olp-connect` works.

## D69 — /health.anonymousKey + auth.advertise_anonymous_key

server.mjs handleHealth emits OPTIONAL `anonymousKey: "olp_..."` field
when ALL THREE prerequisites hold:
1. config.json auth.advertise_anonymous_key === true
2. config.json auth.allow_anonymous === true (per ADR 0007 § 7)
3. At least one non-revoked key has plaintext_advertise field set

Default-off: field is ABSENT (not null) — preserves v0.3.x /health shape;
existing tests don't regress.

bin/olp-keys.mjs new flags: `keygen --anonymous --advertise` writes the
plaintext into the manifest's optional `plaintext_advertise` field AND
prints a WARNING about disk-storage + /health exposure + ADR 0011
pointer. Owner-tier --advertise rejected at BOTH CLI + lib layers.

Implementation note: reused existing guest tier (no new owner_tier:
'anonymous'); plaintext_advertise is a forward-compat optional manifest
field per ADR 0007 § 4 unknown-fields-allowed convention. Cleaner than
introducing a new tier.

anonymousKey appears in BOTH trimmed AND full /health payloads — the
trimmed payload's purpose is to be readable by anonymous clients so they
can self-bootstrap. Tested.

Startup warns on prereq failure (anonymous_key_advertised_but_denied /
anonymous_key_advertised_but_no_anonymous_key_exists) so the relaxed-
posture failure mode is observable. Graceful-degrade: server still
boots; handleHealth re-checks at request time and silently omits the
field when any prereq fails (defense-in-depth).

## D70 — ADR 0011 (anonymous-key deployment-context limits)

New ADR codifying the trusted-LAN-only invariant.

Trade-off documented: anonymous key advertised via /health = anyone who
can reach the server can read /health and use the key. Acceptable ONLY
when "anyone who can reach the server" ≈ "trusted family on the LAN".
Public-internet deployment = instant compromise.

Soft enforcement: server logs startup warn if BIND_ADDRESS resolves to
a public IP AND advertise_anonymous_key: true. No hard allowlist (TLS-
fronted private networks indistinguishable from public from server's
perspective).

Re-evaluation trigger: any time OLP gains "expose to public internet"
deployment mode (e.g., Cloudflare Tunnel guidance in README), revisit.

References ADR 0007 § 7 (identity classes), ADR 0010 § Phase 4 charter
D68-D70 line, OCP server.mjs:148/1454/1488/1555 (PROXY_ANONYMOUS_KEY
reference).

## Test count

658 → 672 (+14 D68-D70 tests across Suite 34: 5 keys.mjs unit + 6 /health
HTTP integration + 3 CLI integration).

## Scope discipline

NO /v1/messages entry surface (out of Phase 4 per ADR 0010).
NO olp-plugin/ Telegram plugin (D71-D73).
NO docs/integrations/*.md files (D71-D73).
NO CHANGELOG / package.json version bump (Phase 4 close handles versioning;
only package.json bin entry for olp-connect added).
NO new npm deps.

## Authority

- ADR 0010 § Phase 4 D-day plan D68-D70 line
- ADR 0011 (this commit — new ADR)
- ADR 0007 § 4 (manifest forward-compat unknown fields) + § 7 (identity
  classes) + § 9 (keygen flow) — extended by D69 plaintext_advertise
- OCP ocp-connect /Users/taodeng/ocp/ocp-connect (port reference)
- OCP server.mjs:148, 1454, 1488, 1555 (PROXY_ANONYMOUS_KEY reference)
- 2026-05-26 brainstorm (Top 5 OCP inheritance candidates, item 3:
  /health.anonymousKey + olp-connect zero-config UX)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: D68-D70 reviewer P1 + P2 fold-in — README impact note + listKeys redaction + schema_version note

Reviewer APPROVE WITH MINOR — 0 P0, 1 P1, 2 P2; all three folded in.

P1 — README impact note for new Phase 4 user-visible surfaces.

Per CLAUDE.md release_kit.new_feature_doc_expectations:
- new env / config knob → README § Environment Variables
- new endpoint or response field → README § API Endpoints
- new CLI surface → dedicated §

README now documents:
- /health.anonymousKey optional field (in API Endpoints table) with cross-
  ref to ADR 0011 + the three-prereq gate
- streaming.heartbeat_interval_ms config (D61) + auth.advertise_anonymous_key
  config (D69) under new "config.json keys introduced at Phase 4" subsection
- Operator CLI surfaces summary: olp / olp-connect / olp-keys keygen
  --anonymous --advertise, with cross-refs to ADR 0010 + 0002 Amendment 7

P2-1 — lib/keys.mjs listKeys() now strips plaintext_advertise alongside
token_hash. Callers wanting the advertised plaintext for the /health
publication path MUST go through findAdvertisedKey() — the only sanctioned
read site. Defends against a future caller of listKeys() leaking the
plaintext into logs / HTTP responses / dashboards. Tests still pass
(no in-repo caller of listKeys depends on plaintext_advertise being
present).

P2-2 — ADR 0011 now documents the schema_version-stays-at-1 decision
explicitly. Additive optional fields don't require bump per ADR 0007 § 4,
but a future archaeologist asking "why didn't D69 bump schema_version?"
now has a one-line answer. Same paragraph documents the listKeys()
redaction policy in plain text alongside the manifest-field contract.

672/672 tests still pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-05-26 09:15:09 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.7
parent e69e908dae
commit 0bdecd1235
9 changed files with 1433 additions and 16 deletions
+75 -4
View File
@@ -249,7 +249,7 @@ async function _withKeyLock(id, fn) {
* never logged. The manifest contains only the hash.
*/
export function createKey(args = {}) {
const { name, owner_tier = 'guest', providers_enabled = '*', notes = '', olpHome } = args;
const { name, owner_tier = 'guest', providers_enabled = '*', notes = '', olpHome, plaintext_advertise = false } = args;
if (typeof name !== 'string' || name.length === 0) {
throw new Error('createKey: name is required (non-empty string)');
}
@@ -259,6 +259,15 @@ export function createKey(args = {}) {
if (!(providers_enabled === '*' || Array.isArray(providers_enabled))) {
throw new Error('createKey: providers_enabled must be "*" or string array');
}
// D69 plaintext_advertise (ADR 0011): only valid on guest tier — see ADR
// 0011 § "Trusted-LAN invariant + tier restriction". Owner-tier advertisement
// is rejected because exposing the owner identity unauthenticated would
// grant unauthenticated callers /health full payload, /v0/management/* access,
// and X-OLP-Fallback-Detail visibility — the inverse of the advertise key's
// intent (a low-privilege zero-config tier).
if (plaintext_advertise && owner_tier !== 'guest') {
throw new Error('createKey: plaintext_advertise requires owner_tier="guest" (ADR 0011)');
}
const id = generateKeyId();
const plaintext_token = generateToken();
@@ -276,10 +285,59 @@ export function createKey(args = {}) {
last_used_at: null,
notes,
};
// D69 (ADR 0011): when the operator explicitly opts in via --advertise on
// keygen, the plaintext token is co-located with the hash so the server can
// surface it via /health.anonymousKey for zero-config family-LAN setup.
// This is the ONLY place plaintext ever lands on disk; see ADR 0011 for
// the trusted-LAN-only invariant + threat model.
if (plaintext_advertise) {
manifest.plaintext_advertise = plaintext_token;
}
writeManifestAtomic(id, manifest, { olpHome });
return { id, plaintext_token, manifest };
}
// ── D69 advertise-key discovery (ADR 0011) ───────────────────────────────
/**
* Find the active key marked for /health advertisement. Returns the manifest
* (including `plaintext_advertise`) or null when no such key exists.
*
* Scans every manifest under ~/.olp/keys/; selects the FIRST active
* (revoked_at === null) manifest that carries a non-empty `plaintext_advertise`
* string. Deterministic ordering is unstable across filesystems — operators
* are expected to keep at most one advertised key on disk at a time.
*
* Returns null if:
* - the keys directory doesn't exist
* - no manifest carries plaintext_advertise
* - the only matching manifest is revoked
*
* Used by server.mjs handleHealth (D69) + olp-keys CLI 'list' subcommand
* (advertise badge).
*
* @param {object} [opts]
* @param {string} [opts.olpHome] - test override; defaults to ~/.olp
* @returns {object|null} manifest object (NOT redacted; carries plaintext_advertise)
*/
export function findAdvertisedKey(opts = {}) {
const dir = _keysDir(opts);
if (!existsSync(dir)) return null;
let entries;
try { entries = readdirSync(dir); } catch { return null; }
for (const id of entries) {
if (id.startsWith('.')) continue;
let m;
try { m = readManifest(id, opts); } catch { continue; }
if (m === null) continue;
if (m.revoked_at !== null) continue;
if (typeof m.plaintext_advertise === 'string' && m.plaintext_advertise.length > 0) {
return m;
}
}
return null;
}
/**
* List all keys. Returns array of manifest objects with `token_hash` redacted
* (kept on disk; omitted from list output per common operational hygiene —
@@ -299,8 +357,14 @@ export function listKeys(opts = {}) {
try {
const m = readManifest(id, opts);
if (m === null) continue;
// Redact token_hash from list output (keep on disk).
const { token_hash, ...rest } = m;
// D69 reviewer P2-1 (footgun-removal): strip BOTH `token_hash` AND
// `plaintext_advertise` from list output. Callers wanting the
// advertised plaintext for the /health publication path must go
// through `findAdvertisedKey()` instead, which is the only sanctioned
// read site. Future callers of `listKeys()` that emit results into
// logs / HTTP responses / dashboards therefore can't accidentally
// leak the advertised plaintext.
const { token_hash, plaintext_advertise, ...rest } = m;
out.push(rest);
} catch {
// Skip invalid manifest; production impl would log warn.
@@ -456,12 +520,17 @@ export async function touchLastUsed(id, opts = {}) {
* - owner_only_endpoints: ['/health'] (D46 consumes; D45 only loads)
* - fallback_detail_header_policy: 'owner_only' (D46 consumes; D45 only loads)
*
* D69 / ADR 0011:
* - advertise_anonymous_key: false (default off; opt-in surfaces
* findAdvertisedKey() plaintext via
* /health.anonymousKey)
*
* Returns the auth config object. Never throws — missing file / parse
* error / missing `auth` key all fall back to defaults.
*
* @param {object} [opts]
* @param {string} [opts.olpHome] - test override; defaults to ~/.olp
* @returns {{ allow_anonymous: boolean, owner_only_endpoints: string[], fallback_detail_header_policy: 'owner_only'|'all'|'none' }}
* @returns {{ allow_anonymous: boolean, owner_only_endpoints: string[], fallback_detail_header_policy: 'owner_only'|'all'|'none', advertise_anonymous_key: boolean }}
*/
export function loadAuthConfigSync(opts = {}) {
const olpHome = _resolveOlpHome(opts);
@@ -470,6 +539,7 @@ export function loadAuthConfigSync(opts = {}) {
allow_anonymous: false,
owner_only_endpoints: ['/health'],
fallback_detail_header_policy: 'owner_only',
advertise_anonymous_key: false,
};
if (!existsSync(path)) return { ...DEFAULTS };
try {
@@ -484,6 +554,7 @@ export function loadAuthConfigSync(opts = {}) {
fallback_detail_header_policy: ['owner_only', 'all', 'none'].includes(auth.fallback_detail_header_policy)
? auth.fallback_detail_header_policy
: DEFAULTS.fallback_detail_header_policy,
advertise_anonymous_key: typeof auth.advertise_anonymous_key === 'boolean' ? auth.advertise_anonymous_key : DEFAULTS.advertise_anonymous_key,
};
} catch {
// Malformed JSON / unreadable file → safe defaults