Pr/3 models dev pricing - #22
Closed
purrfecto114-lgtm wants to merge 3 commits into
Closed
Conversation
- renderer windows: sandbox: true (preload only uses contextBridge/ipcRenderer, renderer JS has zero node API usage) + deny-by-default CSP meta on pet/panel/archive (connect-src none — all comms go over IPC) - command-safety: fail-closed single read-only command recognizer replacing the old prefix-only check; closes env-exec, git branch/remote mutation, git diff --output/--ext-diff, rg --pre, fd -x/-X, date -s and shell-grammar chaining/substitution/redirect bypasses - pretool-hook: Claude Code PreToolUse gate emitting the current hookSpecificOutput.permissionDecision structure (flat top-level fields are silently ignored by CC >= v1.0.59); no-op output leaves the normal permission flow in charge. stdin is buffer-concatenated once so CJK payloads split across chunk boundaries no longer corrupt the decoded JSON - hookinstall: registers the Bash-matcher PreToolUse gate alongside the state hooks; uninstall/hooksCurrent account for it - server: runtime.json ownership is now first-live-wins with stale-record healing — a probe-gated guard replaces the unconditional 15s re-assert that made two live instances flip-flop the file (each overwrite invalidating the other's token mid-flight); same-port stale tokens are rewritten immediately; guard interval injectable via runtimeGuardMs for tests - tests: command-safety bypass matrix (31 reject / 39 allow cases) + runtime-ownership four-scenario suite (live peer untouched, stale record taken over, same-port stale token healed, stop() never deletes a live peer's record) npm test exit 0.
purrfecto114-lgtm
force-pushed
the
pr/3-models-dev-pricing
branch
3 times, most recently
from
August 29, 2026 12:22
4367b6b to
5084e00
Compare
…aller
Verified against CodeWhale's actual hook contract (docs/HOOKS.md + executor
source, commit 7d942bd) — the half-finished refresh had invented event names
that do not exist upstream:
- hook/codewhale-hook.js: registers the 10 real lifecycle events
(session_start/session_end/message_submit/tool_call_before/tool_call_after/
turn_end/subagent_spawn/subagent_complete/on_error/mode_change); upstream's
turn_start/error DO NOT exist. tool_call_before reads its payload from the
DEEPSEEK_* env vars (there is no CODEWHALE_TOOL_NAME upstream); observer
events merge their stdin JSON. Fails closed to {decision:"ask"} whenever
LLMPET is unreachable/untrusted, because an unanswered CodeWhale hook
defaults to ALLOW on timeout. Observer hooks install with background=true
(state sync never rides the turn's critical path); the readStdin guard
timer is cleared+unref'd so hook processes exit ~30ms after their work
instead of idling 300ms; turn_end usage metadata (turn_id/status/provider)
is forwarded for the ledger. The permission gate auto-allows CodeWhale's
real shell tool name exec_shell (the Claude-era 'Bash' spelling never
arrives on this route and made the auto-allow dead code -> permission
fatigue). mode_change maps to a synthetic ModeChange event + 'attention'
oneshot — the adapter turns Notification into a needs-input card, wrong
for a user-initiated Plan/Work/Operate switch.
- backend/codewhale-provider.js: managed TOML block installer. The permission
gate is a strict gate (continue_on_error = false, timeout 600s) so an
unresponsive pet DENIES instead of CodeWhale's default allow-on-timeout;
observer hooks stay best-effort. User TOML outside BEGIN/END markers is
preserved; atomic 0600 writes.
- backend/codewhale-permission.js: bounded permission holder — 64 pending cap
(overflow -> ask), 8-minute auto-DENY (never ask: the un-answered upstream
timeout ALLOWS; honors the documented autoDenyMs option, invalid values
fall back to the 8-minute window), connection-close cleanup, session-end
sweep.
- server: /codewhale-permission endpoint behind the same loopback/Host/Origin/
per-boot token boundary as /state; agent_id routing tags sessions; turn_end
usage (incl. turn_id/provider/status metadata) is handed to a ledger
callback (wired in the pricing PR).
- main: cw-* permission ids route to the CodeWhale holder from the pet bubble;
pending lists merge on the main pet (no dedicated CodeWhale pet yet);
installer runs only when ~/.codewhale exists or LLMPET_ENABLE_CODEWHALE=1.
Permission cards identify the asking agent (CodeWhale · exec_shell) and
state the auto-deny window; the hint derives from the pending entry's own
createdAt/expiresAt so the first push and every stats-snapshot rebuild of
the same card agree; askPermission reports 'timed out' distinctly from
'not running'.
- adapter: agentOf/AGENT_NAME know 'codewhale' (first-class like dsh);
humanizeTool understands exec_shell/write_file/edit_file/apply_patch/
fim_edit/read_file (field names verified against CodeWhale's permission-rule
docs: exec_shell->command, file tools->path); onPermissionAdded falls back
to the entry's own agent identity when the session is not in the store yet
(a tool_call_before can beat session_start).
- transport: postState reuses the already-read runtime port (one readFileSync
per hook event instead of two).
npm test exit 0 — incl. the e2e suite driving the real HTTP server and both
hook subprocesses (env->/state routing, permission trust boundary, timeout
branch against a never-responding server, stdin->stdout protocol shapes).
Windows CI fix (follow-up to the initial red run): the test-suite HOME
isolation only overrode env.HOME, but Node's os.homedir() on Windows reads
USERPROFILE — so on windows-latest the hook subprocess resolved the REAL
user home, found the runtime.json the test server itself had just written
there, and the "LLMPET unreachable -> ask" case deterministically hung on
the permission bridge until the 10s SIGKILL (exit != 0). macOS/Linux stayed
green because HOME is honored there. Both test files now override
USERPROFILE alongside HOME — in the parent before the first backend require
(RUNTIME_PATH is frozen at module load) and in the unreachable-child env.
The observer-hook timing check also moved to a dual-clock scheme:
spawn->exit (<280ms) is only asserted off-Windows (cold-start noise has no
discriminative power there), while the new ack->exit measure (<150ms from
usage delivery to process exit) strips spawn cost and catches the 300ms
stdin-guard leak on every platform. Verified by simulating Windows homedir
semantics via an os.homedir shim reading USERPROFILE: old tree fails at the
unreachable case exactly as CI did; fixed tree passes both clocks.
Second Windows-only follow-up, found on the re-run: every assertion group
passed on windows-latest but the suite still exited 1 at the final sandbox
cleanup — fs.rmSync hit ENOTEMPTY on `.octopus` because the runtime.json it
had just unlinked was not yet gone from the directory listing (POSIX shows
deletions immediately, Windows does not; the file was written microseconds
earlier by the last runtime-ownership assertion). Node ships the exact
remedy for this race: rmSync's maxRetries/retryDelay options retry
ENOTEMPTY/EBUSY/EPERM with linear backoff. All sandbox cleanups in both
test files now pass maxRetries=10, retryDelay=50.
Third Windows-only follow-up: the re-run still died at sandbox cleanup with
ENOTEMPTY even with maxRetries=10 (all 11 attempts failed across the full
2.78s backoff window — timestamps prove the retries ran). Root cause: the
global logger (backend/log.js) keeps ONE ever-open append stream on
~/.octopus/octopus.log, and server.stop() does not close it. On POSIX an
unlink is just an inode refcount drop, so Linux/macOS never notice; on
Windows the open write handle pins the file's directory entry in a
delete-pending state, so rmdir reports ENOTEMPTY for as long as the handle
lives — no amount of rmSync retries can win. Fix: log.js now exports
shutdown() (ends the stream, resolves on close with a 500ms fallback, never
rejects; a later log() transparently re-opens). Both test suites await
shutdown() before deleting their fake HOME, and still keep maxRetries as a
belt for the residual post-unlink visibility race.
Fourth Windows-only fix, this one a genuine flake that pre-exists upstream
and can hit any CI run: the re-run crashed smoke.js itself with an
uncaught ECONNRESET. Timestamps in the job log show a 4-second stall
between two pure-function assertions (only possible on a contended
runner), putting the last HTTP request 6.4s in the past — past the Node
http server's default keepAliveTimeout of 5s. The server closed the idle
keep-alive socket; Node 19+'s globalAgent (keepAlive now defaults to
true) then reused the dead socket for the next post() and read a TCP RST.
Linux runners are fast enough to never open the window, which is why this
never fired on ubuntu. Fix: both HTTP-speaking tests (smoke.js and
e2e-codewhale.js) now route every request through an explicit
new http.Agent({ keepAlive: false }) — one fresh connection per request,
nothing pooled, nothing to go stale. (Runtime-ownership and pr3-smoke do
no direct HTTP.)
- models-dev-sync: independent price cache from models.dev/catalog.json
(verified shape: {models, providers} — only providers carries prices; the
362-row top-level models object is provider-independent metadata). Catalog
legitimately contains keys like __proto__/constructor as provider/model ids:
the cache is built with null-prototype dictionaries and unsafe keys are
rejected outright, all numbers bounds-checked (NaN/Inf/negative/absurd ->
null), 64 MiB body cap, 15s timeout, atomic 0600 write, 24h TTL. Network
failure never blocks startup. Unknown paths on models.dev return 200 + SPA
HTML, so non-JSON parse failures are handled as refresh errors. The cache
path follows LLMPET_CODEWHALE_HOME like the ledger. On-demand refresh only
(no timers beyond a TTL check at read time).
- codewhale-metering: consumes the verified turn_end usage payload
(input_tokens/output_tokens/prompt_cache_hit/miss/write, reasoning_tokens)
keyed by turn_id (retried hook deliveries cannot double-bill; bounded dedup
window). DeepSeek semantics honored: input_tokens already includes the
cache-hit portion, so cached tokens are billed at the cache-read rate and
never twice. reasoning_replay_tokens have ambiguous upstream pricing —
counted in diagnostics, deliberately not billed. Unknown models count tokens
at honest $0 and land in diagnostics instead of being guessed. seenTurns
pruning is O(1) on the common path (size counter + batch drop to half the
cap); a 20k-turn bulk load records in <5s.
- adapter/growth/main: the CodeWhale ledger joins the machine total and the
main pet's billing view (tagged 'codewhale' in byModel/todayByProvider);
machineGrowth totals include it. Panel split line becomes a three-way join
when CodeWhale has spend (Claude $x · Codex $y · CodeWhale $z); machines
without CodeWhale spend keep the upstream two-way string; totals never
contradict the split sum again.
- test/pr3-smoke: protocol-shaped assertions (special keys, bounds, turn_id
dedup, cache-aware pricing math, TOML idempotence + old-format upgrade with
user content preserved, env override actually overriding, event-name
validity against the real CodeWhale vocabulary).
npm test exit 0.
purrfecto114-lgtm
force-pushed
the
pr/3-models-dev-pricing
branch
from
August 29, 2026 12:38
5084e00 to
16987ce
Compare
Contributor
Author
|
同一分支与同一 head 提交(16987ce)已重新提交为 #25——补齐了规范标题与完整描述。关闭本 PR 以免重复占用 review 队列。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.