mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-19 09:45:07 +00:00
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:
+61
-5
@@ -52,10 +52,12 @@ import {
|
||||
loadFallbackConfigSync,
|
||||
} from './lib/fallback/engine.mjs';
|
||||
// Phase 2 / D45 — multi-key auth integration per ADR 0007.
|
||||
// D69 (ADR 0011): findAdvertisedKey for /health.anonymousKey opt-in surface.
|
||||
import {
|
||||
validateKey,
|
||||
touchLastUsed,
|
||||
loadAuthConfigSync,
|
||||
findAdvertisedKey,
|
||||
ANONYMOUS_KEY_ID,
|
||||
ENV_OWNER_KEY_ID,
|
||||
} from './lib/keys.mjs';
|
||||
@@ -231,10 +233,37 @@ if (_authConfig.allow_anonymous === true) {
|
||||
message: 'config.json auth.allow_anonymous is true — all routes accept requests without an OLP API key; production deployments should set this to false (ADR 0007 § 7).',
|
||||
});
|
||||
}
|
||||
// D69 (ADR 0011): startup-time prerequisite validation for /health.anonymousKey
|
||||
// advertisement. The field is emitted ONLY when all three prerequisites hold:
|
||||
// (1) auth.advertise_anonymous_key === true (config opt-in)
|
||||
// (2) auth.allow_anonymous === true (anonymous tier must be reachable
|
||||
// for the advertised key to be
|
||||
// meaningful to zero-config callers)
|
||||
// (3) at least one advertised key exists on disk (manifest with
|
||||
// plaintext_advertise field, not revoked)
|
||||
// Violations log warn at startup but the server still boots; the runtime
|
||||
// handleHealth checks the same conditions and simply omits the field.
|
||||
if (_authConfig.advertise_anonymous_key === true) {
|
||||
if (_authConfig.allow_anonymous !== true) {
|
||||
logEvent('warn', 'anonymous_key_advertised_but_denied', {
|
||||
message: 'auth.advertise_anonymous_key=true but auth.allow_anonymous=false — the advertised key would not be usable anonymously. /health.anonymousKey will NOT be emitted until allow_anonymous=true. See ADR 0011.',
|
||||
});
|
||||
} else if (findAdvertisedKey() === null) {
|
||||
logEvent('warn', 'anonymous_key_advertised_but_no_anonymous_key_exists', {
|
||||
message: 'auth.advertise_anonymous_key=true but no active key with plaintext_advertise exists. Run `olp-keys keygen --anonymous --advertise` to create one. /health.anonymousKey will NOT be emitted until then. See ADR 0011.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — test seam: inject a synthetic auth config (no file I/O). */
|
||||
export function __setAuthConfig(config) {
|
||||
_authConfig = config ?? { allow_anonymous: false, owner_only_endpoints: ['/health'], fallback_detail_header_policy: 'owner_only' };
|
||||
_authConfig = config ?? { allow_anonymous: false, owner_only_endpoints: ['/health'], fallback_detail_header_policy: 'owner_only', advertise_anonymous_key: false };
|
||||
// D69: defensively normalize so tests passing partial configs still get a
|
||||
// boolean advertise_anonymous_key value (downstream code treats undefined
|
||||
// as falsy but explicit normalisation matches loadAuthConfigSync's contract).
|
||||
if (typeof _authConfig.advertise_anonymous_key !== 'boolean') {
|
||||
_authConfig.advertise_anonymous_key = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — reset auth config to file-loaded state. */
|
||||
@@ -751,9 +780,34 @@ async function handleHealth(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
// D69 (ADR 0011): compute opt-in /health.anonymousKey. The field is added
|
||||
// to BOTH trimmed and full payloads (the trimmed payload's whole purpose
|
||||
// is to be readable by anonymous clients so they can self-bootstrap into
|
||||
// a real key). Three prerequisites must hold (see startup warns above):
|
||||
// (1) auth.advertise_anonymous_key === true
|
||||
// (2) auth.allow_anonymous === true
|
||||
// (3) findAdvertisedKey() returns a non-null active manifest
|
||||
// When any fails, the field is absent (NOT null) — preserves the v0.3.x
|
||||
// /health payload shape for clients that strict-validate keys.
|
||||
let anonymousKey = null;
|
||||
if (_authConfig.advertise_anonymous_key === true && _authConfig.allow_anonymous === true) {
|
||||
try {
|
||||
const adv = findAdvertisedKey();
|
||||
if (adv && typeof adv.plaintext_advertise === 'string' && adv.plaintext_advertise.length > 0) {
|
||||
anonymousKey = adv.plaintext_advertise;
|
||||
}
|
||||
} catch {
|
||||
// Filesystem error reading manifests is non-fatal; just omit the field.
|
||||
anonymousKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isGated && !isOwner) {
|
||||
// Trimmed payload per § 7.1.
|
||||
return sendJSON(res, 200, { ok: true, version: VERSION });
|
||||
// Trimmed payload per § 7.1. D69: include anonymousKey here too — zero-
|
||||
// config olp-connect callers read it BEFORE they have a key.
|
||||
const trimmed = { ok: true, version: VERSION };
|
||||
if (anonymousKey !== null) trimmed.anonymousKey = anonymousKey;
|
||||
return sendJSON(res, 200, trimmed);
|
||||
}
|
||||
|
||||
// Full payload (owner OR /health removed from owner_only_endpoints).
|
||||
@@ -774,11 +828,13 @@ async function handleHealth(req, res) {
|
||||
providerStatuses[name] = { ok: false, error: e.message, activeSpawns };
|
||||
}
|
||||
}
|
||||
sendJSON(res, 200, {
|
||||
const fullPayload = {
|
||||
ok: true,
|
||||
version: VERSION,
|
||||
providers: { enabled, available, status: providerStatuses },
|
||||
});
|
||||
};
|
||||
if (anonymousKey !== null) fullPayload.anonymousKey = anonymousKey;
|
||||
sendJSON(res, 200, fullPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user