mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
68d58e7df4ac46227704c828a3c742f20f547e24
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
68d58e7df4 |
fix: CLI/installer hardening — restart labels, key permissions, unit-secret escaping (#113) (#120)
Three CLI/installer findings from the 2026-05-31 audit (no server.mjs): 1. ocp-plugin `cmdRestart` hardcoded uid 501 + the legacy `ai.openclaw.proxy` label, and fell through to a dangerous `pkill -f 'node.*server.mjs' && cd ~/.openclaw/projects/*/; node server.mjs &` that could kill an unrelated node process and relaunch an env-less ghost proxy from a glob-ambiguous dir. Now uses process.getuid() + the live labels (dev.ocp.proxy on macOS, ocp-proxy on Linux systemd; the OpenClaw gateway label is unchanged) and drops the pkill fallback entirely (returns a manual `ocp restart` message on failure). 2. ocp-connect wrote the quota key unquoted into rc files and a world-readable environment.d/ocp.conf. Now single-quotes the value and chmod 600s the rc files and ocp.conf (matching the existing auth-profiles.json 0o600 convention). 3. setup.mjs interpolated the injected service-unit secrets (CLAUDE_BIN, OCP_ADMIN_KEY, PROXY_ANONYMOUS_KEY) raw into plist <string> and systemd Environment= lines. Added xmlEscape() for all plist <string> values and assertSafeInjectValue() which rejects control characters (\x00-\x1f — newline, CR, tab) before any unit is written, blocking a newline-injected rogue Environment= directive. Spaces are intentionally allowed (CLAUDE_BIN paths may contain them). XML-escaping on write also resolves plist-merge.mjs's [^<]* regex concern transitively (no raw < reaches it) — comment added, logic unchanged. ALIGNMENT.md: CLI/installer scripts only, no Anthropic operation forwarded → cli.js citation N/A. No blacklisted tokens or port literals introduced; alignment.yml passes. Independent fresh-context reviewer (opus): APPROVE (Iron Rule 10) — verified the control-char regex byte-exact via od (no space-rejection regression), the pkill fallback fully removed, restart labels match setup.mjs ground truth, OCP keys (base64url) cannot break the single-quoting, and the validator runs before any write. Closes #113. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4a7d79c330 |
fix: record OAuth-host verification + models.json SPOT for usage-probe/default (#112) (#119)
Three alignment/SPOT findings from the 2026-05-31 audit: 1. The OAuth token-refresh host (platform.claude.com/v1/oauth/token, a Class A surface) was introduced in the 2026-04-11 drift commit and had no verification record. Verified against the compiled cli.js (claude.exe v2.1.154) via `strings`: OAUTH_TOKEN_URL and OAUTH_CLIENT_ID both appear in the binary byte-for-byte (in the same `prod` config object), and the legacy host console.anthropic.com/v1/oauth is absent (0 hits). Recorded this as an inline ALIGNMENT citation comment above the constants. No live OAuth probe was run — a refresh-token grant would rotate the operator's real credentials; the strings-on-binary evidence is decisive. (cli.js: verified against compiled claude.exe v2.1.154, 2026-05-31.) 2. fetchUsageFromApi() hardcoded the haiku model ID; now derives from modelsConfig.aliases.haiku (ADR 0003 SPOT). Prevents a silent /usage break on a future haiku ID bump. 3. [P3] The default request model hardcoded the sonnet ID; now derives from modelsConfig.aliases.sonnet (ADR 0003 SPOT). Both SPOT values are byte-identical to the literals they replace today, so zero behavior change — only drift-resistance. The alignment.yml blacklist pin for the wrong-host variant was deliberately NOT included here: extending the blacklist is a governance-layer change (alignment.yml inline policy) and belongs in its own PR. ALIGNMENT.md: finding 1 IS the verification (cli.js citation recorded inline); findings 2-3 are SPOT hygiene that forward no new operation. No blacklisted tokens or port literals introduced; alignment.yml passes. Independent fresh-context reviewer (opus) INDEPENDENTLY re-ran `strings` on the binary and confirmed the host/client_id present and the legacy host absent — APPROVE (Iron Rule 10; alignment hard-requirement #3 satisfied). Closes #112. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c3b1f32c86 |
fix: error-output sanitization + process-lifecycle hardening (#111) (#118)
Four findings from the 2026-05-31 audit (3×P2 + 1×P3), all in the error/process
lifecycle layer:
1. sanitizeError() helper — streaming error paths sent raw claude error_message /
stderr to the client, leaking home-dir / credential-file paths that the
non-streaming path already redacted. Factored the path-strip regex into one
helper and applied it at all 9 client-facing jsonResponse/sendSSE error emits;
de-duped the 3 pre-existing inline .replace() sites. Operator-log calls
(logEvent/trackError) and admin-gated endpoints left raw by design.
2. res.on("close") SIGKILL escalation — a client disconnect sent only SIGTERM; a
SIGTERM-resistant child held its concurrency slot until the request timeout
(narrow #37 on the hottest exit path). Now escalates to SIGKILL 5s after SIGTERM,
cleared on proc exit. Per review: gated on the child still being alive
(exitCode===null && signalCode===null) so the normal-success close no longer
fires a spurious SIGTERM or leaks the 5s timer; killTimer.unref() so a genuine
disconnect timer never delays graceful shutdown.
3. Per-key quota TOCTOU — documented as best-effort/eventually-consistent (inline
comment + README note): concurrent requests at the boundary can overshoot by up
to MAX_CONCURRENT and cache hits are uncounted. Chose documentation over an
in-flight counter to avoid a decrement-on-all-paths liability (the #37 class) on
a low-blast-radius internal family rate-limiter — not a payment boundary.
4. [P3] overallTimer cleared on semantic completion — the request timer was cleared
only on proc exit, so a streamed response that res.end()'d before the child
exited could record a spurious post-success timeout. New clearOverallTimer()
(clears the timer ONLY, never touches the `cleaned` slot-accounting flag — no
slot leak) is called in the streaming stop-success path; cleanup() on exit still
clears it idempotently and decrements the slot.
ALIGNMENT.md: error-shaping / process-lifecycle / rate-limit documentation forward
no Anthropic operation, so a cli.js citation is N/A under Rule 2. No blacklisted
tokens or port literals introduced; alignment.yml passes.
Independent fresh-context reviewer (opus): APPROVE WITH MINOR (Iron Rule 10) — the
two critical concerns (clearOverallTimer slot-leak class, SIGKILL double-kill) were
verified clean; MINOR #1 (kill-timer leak on success path) folded in; MINOR #2
(pre-existing regex over-redaction of ratios/URLs) left as out-of-scope.
Closes #111.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
4458490caa |
fix: request validation + OpenAI-compat correctness (#110) (#117)
Three correctness/compat defects on the request path:
1. Non-array `messages` (e.g. {"messages":"x"}) passed the `!messages?.length`
guard but threw in `messages.reduce(...)`; since the handler runs without
await, the rejection silently hung the client until socket timeout. Now
guarded with `Array.isArray(messages) && length>0` → 400 invalid_request_error,
placed before the first use of `messages`.
2. OpenAI array `content` ([{type:"text",text},{type:"image_url",...}]) was
JSON.stringify'd into the prompt as literal noise. New `contentToText()` helper
flattens text parts and replaces non-text parts with a placeholder; used in
messagesToPrompt, extractSystemPrompt, and all promptChars char-count sites
(zero `JSON.stringify(m.content)` remain).
3. A streamed upstream error arriving after eager SSE headers emitted a bare
finish_reason:"stop" + [DONE] — byte-identical to a successful empty completion.
Both streaming error paths (the parsed.error result branch AND the non-zero-exit
close-handler branch — the latter folded in per reviewer) now emit an SSE
`data:{"error":{...}}` frame so clients can distinguish failure from empty.
Error-text sanitization across all emit sites is intentionally deferred to #110's
sibling issue #111 (security layer).
ALIGNMENT.md: these are OpenAI-compat shim + input-validation behaviors; OCP does
not forward, add, or alter any Anthropic API operation here (cli.js does not speak
the OpenAI wire format), so a cli.js citation is N/A under Rule 2. No blacklisted
tokens or port literals introduced; alignment.yml passes.
Independent fresh-context reviewer (opus): APPROVE WITH MINOR (Iron Rule 10) —
the close-handler sibling fold-in addresses MINOR #1; this commit body addresses
MINOR #2.
Closes #110.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
36be723198 |
fix(security): gate /health anonymousKey behind opt-in PROXY_ADVERTISE_ANON_KEY (#109) (#116)
/health is an unauthenticated, LAN-reachable endpoint that returned the live anonymous bearer key (anonymousKey: PROXY_ANONYMOUS_KEY). Any device that could reach the port harvested a working, quota-spending credential (P0). The anonymousKey field is now included only when the caller is localhost (already fully trusted by the auth path, via the unspoofable req.socket.remoteAddress) OR the admin explicitly opts in with the new PROXY_ADVERTISE_ANON_KEY=1 env var (default off). ocp-connect's absent-field fallback (interactive --key / anonymous access) is unchanged — comment-only update there. ALIGNMENT.md: this gates an OCP proxy-policy field in /health; it does NOT forward, add, or alter any Anthropic API operation, so a cli.js citation is N/A under Rule 2. No blacklisted tokens introduced; alignment.yml passes. Independent fresh-context reviewer (opus): APPROVE WITH MINOR (Iron Rule 10). Closes #109. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
05a984df89 |
fix: OCP code-audit P1+P2 hardening (crash bugs + multi-tenant gates) (#106)
* fix(daemon): P1-1 guard proc.stdin against EPIPE crash
In spawnClaudeProcess, attach an error listener on proc.stdin BEFORE
the write/end calls so an EPIPE (child closed stdin mid-write) is
swallowed and logged rather than thrown as an unhandled exception.
The existing proc.on("error") listener is on the ChildProcess object,
NOT on the stdin Writable — it does not catch stdin write errors.
Hardening per OCP code audit; entry-surface contract unchanged for
single-user default path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tui): P1-3 remove tool_use from isTerminalLine; only turn_duration is terminal
In interactive TUI mode, stop_reason=tool_use does NOT mean the turn is
complete. Claude handles the tool call internally and continues generating —
the transcript advances to another assistant entry. Treating tool_use as
terminal truncated tool-using turns mid-flight.
Only {type:"system", subtype:"turn_duration"} is the authoritative
completion marker (claude CLI v2.1.157+ interactive session transcript).
Updated two unit tests that previously asserted tool_use → true; they now
assert false (the correct behaviour). The real-fixture terminal detection
test is unaffected because the fixture uses turn_duration.
Hardening per OCP code audit; TUI path behaviour fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(tui): P2-5 add -l (literal) flag to send-keys prompt paste
A prompt that equals a tmux key token (e.g. "C-c", "Escape") would be
interpreted as that key binding rather than typed as literal text.
The -l flag forces literal character-by-character input. The separate
Enter key event afterward deliberately omits -l so tmux sends a real
carriage-return keypress to submit the prompt line.
Authority: tmux send-keys(1) § -l flag. Hardening per OCP code audit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(audit): String-coerce parsed.error + stale tool_use comment (review fast-follow)
Folds the 2 minor findings from the independent review of the audit fixes:
- String(parsed.error) before .slice/message in callClaude + callClaudeStreaming
(defensive: claude could emit a non-string result/error_message).
- correct the readTuiTranscript comment that still listed tool_use as terminal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
74260d7f6f |
feat(tui): opt-in CLAUDE_TUI_MODE — interactive subscription-pool bridge (PR-1..PR-4 squashed) (#104)
Reader + driver + home modes + server wiring + entrypoint hardening. 5 independent reviews folded; e2e green; default stream-json path byte-identical when flag off. See PR description + ADR 0007 for the full layer/authority/security detail. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
885f62addf |
feat: Phase 6c — claude stream-json port + --system-prompt + opus 4.8 (#103)
* wip(rescue): OCP Phase 6c stream-json port + opus 4.8 (uncommitted /tmp work) Rescue commit — preserves the subagent-produced Phase 6c port (claude -p → stream-json + --system-prompt + NDJSON parser) and opus 4.8 model addition done 2026-05-30 in a /tmp clone (volatile). NOT reviewed-for-merge; this is preservation only on a WIP branch so a /tmp reboot does not lose the work. Strategy context: OCP is being made the first-mover for TUI-mode (users are on OCP). Phase 6c here is the DEFAULT-path foundation (stream-json = sdk-cli = the "Max-user stable mode" fallback). TUI-mode (cc_entrypoint=cli subscription bridge) lands on top as an opt-in. Re-review before merging to main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(phase6c): fold review findings — scrub hostname, README prose, parser tests, version citation P3-1 (binding): scrub private hostname "PI231" from server.mjs comment; replace with role-based term "the test server". Repo is public — no machine-specific hostnames may appear. P3-2: update README.md "How It Works" diagram and prose from stale "claude -p" to "claude --output-format stream-json", matching the Phase 6c spawn change already in server.mjs and CHANGELOG. P2-2: update all five authority citations in server.mjs from bare "claude CLI v2.1.104" to "claude CLI § <flag> (ported from OLP, verified v2.1.104; behavior stable through v2.1.158)" — honest about OLP provenance and version range, without inventing new facts. P2-1: add "Stream-JSON parsers" suite to test-features.mjs (17 new tests, 84 → 101 total). Copies parseStreamJsonLines and parseStreamJsonEvent verbatim from server.mjs with a MIRRORS header comment; logEvent stubbed to avoid live-server side-effects. Covers: content_block_delta deltas, assistant-aggregate fallback, no-double-count guard, aggregate-only short responses, partial-line buffering across two chunks, is_error result variants, malformed/non-JSON lines, system/user/unknown event types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7a69d72886 |
feat(snapshot): gcSnapshots + ocp update --rollback --gc + auto-GC; v3.16.0 (#94)
Adds snapshot garbage collection with retention policy: keep last 5, keep snapshots within 30 days, always keep the most recent. Configurable via keepCount / keepDays opts. Wire-up: - ocp update --rollback --gc (manual trigger; --dry-run supported) - runFullUpgrade auto-GC after successful upgrade (best-effort, swallows errors) 4 unit tests: keepCount enforcement, keepDays override, never-delete- most-recent safety, dry-run mode. Bumps to v3.16.0 (bundles PR #93 --check oauth + this GC feature). No cli.js citation needed: this is OCP-internal snapshot lifecycle with no corresponding cli.js operation. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
a8601a6d30 |
feat(doctor): --check oauth fast path (#93)
* feat(doctor): --check oauth fast path Implements the --check oauth fast path documented in cmd_doctor_help but previously unimplemented. Skips version detection, from-version check, git operations, and models endpoint — runs only the curl against /health + auth.ok extraction. Use cases: - After `claude auth login`, fast verify OCP can spawn cli.js - After a known service blip, quick health gate before larger ops - AI agent's setup-repair loop: ./ocp doctor --check oauth in a retry-after-fix step 3 unit tests cover: PASS path, OAuth FAIL → fix_oauth, service down → fix_service. No cli.js citation needed: this is OCP-internal doctor logic with no corresponding cli.js operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(doctor): nit fixes for --check oauth (N3 body=null test + N4 skipped sentinel comment) --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
cd6ec2a212 |
fix(doctor): dynamic latest_version from origin/main; release v3.15.1 (#92)
v3.15.0 doctor used a hard-coded `latest = "v3.14.0"` fallback, causing any v3.15.0+ install to report kind=upgrade against a stale value. `ocp update` would then attempt `git checkout v3.14.0` — a downgrade. Doctor now fetches `git -C ~/ocp show origin/main:package.json` to determine the actual latest. On failure (offline, fresh clone, no remote), falls back to currentVersion so kind=noop instead of recommending a downgrade. Regression test added: doctor with unreachable ocpDir falls back to currentVersion as latest (not the old hardcoded v3.14.0). Caught during v3.15.0 post-deploy verification on home-mac: ./ocp doctor reported `kind=upgrade` immediately after v3.15.0 install, which would have been a critical user-facing bug. No cli.js citation needed: this is OCP-internal doctor logic with no corresponding cli.js operation. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ab03c13332 |
feat(upgrade): ocp doctor + cross-version ocp update + rollback + AI prompts (#91)
* feat(doctor): add ocp doctor with --json + next_action contract Implements scripts/doctor.mjs with semver-aware path selection (noop/update/upgrade/fresh_install/fix_oauth/fix_service) and the JSON contract documented in the design spec. Service health + OAuth checks integrated; mockable via opts.mockHealth for unit tests. 8 unit tests cover the kind dispatch tree and the next_action shape for each kind. No cli.js citation needed: this is OCP-internal tooling with no corresponding cli.js operation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ocp): wire cmd_doctor into bash CLI; dispatch to scripts/doctor.mjs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(doctor): handle unparseable version + empty health body Three issues raised by code-quality reviewer on |
||
|
|
55c576bbb1 |
fix(setup): preserve user-customised plist/systemd env vars on re-setup (#90)
* fix(setup): merge plist/systemd env vars instead of overwriting
setup.mjs previously wrote the launchd plist and the Linux systemd unit
with writeFileSync(path, NEW_TEMPLATE_STRING), which silently dropped any
user-customised env vars (CLAUDE_HEARTBEAT_INTERVAL, CLAUDE_CACHE_TTL,
etc.) on every re-setup or upgrade. This change introduces
scripts/lib/plist-merge.mjs which preserves keys present only in the
existing file and lets the template's known keys win. Linux variant uses
the same logic against Environment=KEY=VALUE lines.
Tests added in test-features.mjs cover preserve / override / first-install
for both formats.
No cli.js citation needed: this is OCP-internal installer behaviour with
no corresponding cli.js operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(setup): plist-merge code-quality follow-up
Three issues raised by the code-quality reviewer on
|
||
|
|
5ff30ac9b6 |
feat(cache): singleflight stampede protection on non-streaming path (#66)
cli.js does not perform proxy-layer stampede protection. The singleflight layer is a value-add proxy operation that exists only inside OCP, between concurrent client requests and the single upstream cli.js spawn. It does not introduce, alter, or remove any endpoint, header, request field, or response field that cli.js emits or expects — no client-observable wire shape change. Justification under ALIGNMENT.md Rule 2: the singleflight Map deduplicates concurrent identical non-streaming cache-miss requests so only one cli.js spawn runs per unique hash window. All followers receive the same resolved (or rejected) content. This is a proxy-internal concurrency optimization, not a wire-level protocol change. Spec: docs/superpowers/specs/2026-05-07-cache-upgrade-design.md (D4) Changes: - keys.mjs: add singleflight(hash, fn) + getInflightStats() exports; in-memory Map cleared via Promise.finally() on each settlement - server.mjs: import singleflight + getInflightStats; wrap non-streaming cache-enabled path through singleflight with inner recheck; add TODO comment in callClaudeStreaming (streaming-path dedup is explicitly out of scope for v3.13.0, see spec D4 streaming caveat); extend /cache/stats to return inflight + requesters fields (additive, no removed fields) - test-features.mjs: 7 new PR-B singleflight tests covering basic dedup, failure fan-out, map cleanup (success + failure), different-hash independence, getInflightStats shape, and sequential-call non-sharing; all 31 tests pass (24 existing + 7 new) Streaming-path singleflight is explicitly out of scope; TODO left in callClaudeStreaming for a future follow-up ticket. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
16eeb66557 |
feat(cache): per-key isolation, cache_control bypass, chunked stream replay (#65)
* docs(governance): ADR 0005 (no multi-provider) + cache upgrade spec Governance prelude for the cache upgrade work: - docs/adr/0005-no-multi-provider.md — locks in the decision that OCP stays single-provider (Anthropic via cli.js spawn). Cache improvements are explicitly in scope (decision §3); multi-provider refactor is explicitly out of scope, with three documented trigger conditions for revisiting. - docs/adr/README.md — index updated to reference 0005. - docs/superpowers/specs/2026-05-07-cache-upgrade-design.md — design for the cache upgrade work split into PR-A (per-key isolation, cache_control bypass, chunked stream replay) and PR-B (singleflight stampede protection). Each design decision has a written rationale. server.mjs is not modified; this commit is doc-only and therefore exempt from the cli.js citation requirement (ALIGNMENT.md Rule 5 applies only to commits that touch server.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cache): per-key isolation, cache_control bypass, chunked stream replay cli.js does not perform response caching at the proxy layer. OCP's response cache is a value-add operation internal to OCP, between the wire and the cli.js spawn. It does not introduce, rename, or alter any endpoint, header, request field, or response field that cli.js emits or expects — this change qualifies under ALIGNMENT.md Rule 2's value-add carve-out for non-wire- affecting proxy operations. no client-observable wire shape change. Spec: docs/superpowers/specs/2026-05-07-cache-upgrade-design.md D1 — Per-key cache isolation (cacheHash v2 format) keys.mjs: prepend `v2|k:<keyId or "anon">|` before existing hash fields. Backward-compatible: absent/null/empty keyId folds to "anon". v1-format rows in response_cache are abandoned naturally; TTL cleanup at server.mjs:185 reaps them within one window. No migration needed. server.mjs: pass keyId: req._authKeyId at the single cacheHash call site (line ~1221). D2 — cache_control bypass keys.mjs: export hasCacheControl(messages) — walks messages and nested content arrays for presence of cache_control field. server.mjs: if hasCacheControl(messages) is true, set req._cacheHash = null and log cache_skipped{reason: cache_control_present}; existing `if (CACHE_TTL > 0 && req._cacheHash)` guards on write-back handle the skip. D3 — Chunked stream replay (80 codepoints/chunk, no artificial delay) server.mjs: replace single-chunk cached.response emission with an Array.from(cached.response) loop in steps of CACHE_REPLAY_CHUNK_SIZE=80. Array.from ensures multibyte UTF-8 codepoints (e.g. CJK) are never split. Tests: 12 new cases in test-features.mjs (36 total, 0 failed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
97ca91341c |
feat(server): per-key quota + response cache (v3.8.0) (#18)
Add two new features for LAN sharing governance:
Quota (budget control):
- Per-key daily/weekly/monthly request limits (NULL = unlimited)
- Idempotent schema migration for quota columns
- Single-query check (SUM/CASE) for all 3 periods — no N+1
- PATCH /api/keys/:id/quota (partial update, input validation)
- GET /api/keys/:id/quota (current limits + usage)
- 429 with structured error when exceeded
- Only applies to identified per-key users, not admin/anonymous
Response cache:
- SHA-256 hash of model + messages + temperature/max_tokens/top_p
- Opt-in via CLAUDE_CACHE_TTL env var (0 = disabled, default)
- Cache hit serves both streaming (simulated SSE) and non-streaming
- Streaming responses accumulated and cached on success
- Skips multi-turn sessions (conversationId present)
- GET /cache/stats, DELETE /cache admin endpoints
- Runtime-tunable cacheTTL via PATCH /settings
- 10-minute periodic cleanup of expired entries
Bug fix discovered during testing:
- SQLite datetime('now') stores 'YYYY-MM-DD HH:MM:SS' but JS
.toISOString() produces 'YYYY-MM-DDTHH:MM:SS.sssZ'. String
comparison breaks for same-day ranges. Added sqliteDatetime()
helper for correct format matching.
Code review fixes:
- DELETE /api/keys/:id no longer shadows /quota sub-routes
- updateKeyQuota uses partial UPDATE (only SET provided fields)
- cacheHash includes temperature/max_tokens/top_p in hash
- Replaced raw getDb() in server.mjs with findKey() encapsulation
- Unified UTC midnight calculation across checkQuota/getKeyQuota
Includes 24-test integration suite (test-features.mjs).
Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|