Skip to content

Pr/3 models dev pricing - #22

Closed
purrfecto114-lgtm wants to merge 3 commits into
myunwang:mainfrom
purrfecto114-lgtm:pr/3-models-dev-pricing
Closed

Pr/3 models dev pricing#22
purrfecto114-lgtm wants to merge 3 commits into
myunwang:mainfrom
purrfecto114-lgtm:pr/3-models-dev-pricing

Conversation

@purrfecto114-lgtm

Copy link
Copy Markdown
Contributor

No description provided.

- 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
purrfecto114-lgtm force-pushed the pr/3-models-dev-pricing branch 3 times, most recently from 4367b6b to 5084e00 Compare August 29, 2026 12:22
…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

Copy link
Copy Markdown
Contributor Author

同一分支与同一 head 提交(16987ce)已重新提交为 #25——补齐了规范标题与完整描述。关闭本 PR 以免重复占用 review 队列。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant