Commit Graph
145 Commits
Author SHA1 Message Date
ZacharyZhang-NY f8cd33d19d fix(tui): the Shift+Enter hint follows the protocol, not a brand list
`shift_enter_unavailable` kept its own list of brands and missed every
other reason the Kitty keyboard protocol gets skipped: JetBrains, screen,
tmux below 3.3 and tmux with `extended-keys off` all swallow it, yet the
prompt still advertised Shift+Enter there. It now reads
`kitty_skip_reason` directly. Two skips deliver the modifier another way
and stay available: Apple Terminal, where `route_enter` polls
CoreGraphics, and Windows Terminal, where the console key record carries
SHIFT — a bare ConHost, only optimistically refined to Windows Terminal,
keeps advertising Alt+Enter.

The old list also carried a `VTE_VERSION >= 8200` cutoff, citing VTE
0.82.0 as the first release with the protocol. It is not: at tag 0.82.0
the string "kitty" appears nowhere in `src/parser-seq.py`, `src/vte.cc`
or `src/widget.cc`, and `_vte_keymap_GDK_Return` encodes only Alt
(`ESC CR`), so Shift+Enter leaves a VTE terminal as the same bare CR as
Enter. Every VTE now advertises Alt+Enter, and `/terminal-setup` stops
telling GNOME Terminal users to upgrade to a version that would not
help. It also stops advertising Alt+Enter to WezTerm, which binds that
chord to ToggleFullScreen.
2026-09-21 19:19:21 -04:00
ZacharyZhang-NY fcb7588312 feat(models): gpt-6-astra joins the compiled-in codex catalog
The Codex backend served five `visibility=list` + `supported_in_api`
models on 2026-09-21: `gpt-6-astra` ahead of `gpt-5.6-sol/terra/luna`
and `gpt-5.5`. Astra carries context 272000 and efforts
low/medium/high/xhigh/max, default medium; the `ultra` tier it also
advertises has no kigi level and is left out. `gpt-reserve` and
`codex-auto-review` stay hidden.

Inference checked against the live backend: `POST
/backend-api/codex/responses` with `model: gpt-6-astra` and the codex
`User-Agent` kigi sends answers 200 and streams, so the model works on
the compiled-in path.
2026-09-21 18:42:22 -04:00
ZacharyZhang-NY a690987ea2 revert: openai-codex goes back to the compiled-in catalog
Reverts c61977c. The live listing needed no codex CLI on the user's
machine — kigi issued the request itself and `client_version` was a
compiled-in string — but the project keeps the catalog in the binary, so
the fetch path, the `OpenaiCodex` listing dialect, its parser and the
pinned client version are gone again, and the codex `User-Agent` returns
to its previous value.

The models.dev provider rename `kimi-for-coding` → `kimi-code-plan-cn`
rode in the same commit and stays: the regenerated snapshot ships under
the new id.
2026-09-21 18:36:46 -04:00
ZacharyZhang-NY 0c580afb5f chore(models): regenerate the models.dev enrichment snapshot
`xai` and `openai` carry `restrict_to_enriched`, so a live listing id the
enrichment catalog does not know is dropped. The bundled snapshot knew
neither `grok-4.7` nor `gpt-6-astra`, which left both invisible offline
and until the 24h models.dev refresh landed.

Regeneration also tripped the generator's fail-fast: models.dev renamed
the Kimi coding provider. `kimi-for-coding` is now `kimi-code-plan-cn`
(api.kimi.com — kigi's endpoint), with `kimi-code-plan-global` the
api.kimi.ai twin; the model set is unchanged. Both registry rows, the
generator target and the two snapshot lookups follow the new id, and the
cache `keep_set` guard invalidates any cache filtered under the old one.
2026-09-21 15:13:00 -04:00
ZacharyZhang-NY c61977cb70 feat(models): fetch the openai-codex catalog live
The ChatGPT Codex catalog was compiled in: four models read once from the
Codex CLI cache. The backend moved on — `gpt-6-astra` shipped, and
`gpt-5.3-codex-spark` left — so the picker showed a table that no longer
matched the account.

`GET https://chatgpt.com/backend-api/codex/models?client_version=<v>`
serves that table. Verified against a live ChatGPT account: the OAuth
Bearer alone returns 200, the query is required (400 without it), and the
backend gates the catalog on it — `0.104.0` answers with
`codex-auto-review` alone, `0.150.0` omits `gpt-6-astra`, `0.155.1`
serves the full set. `CODEX_CLIENT_VERSION` pins the current upstream
release (openai/codex rust-v0.155.1) and the codex `User-Agent` now
derives from it, so the two can never drift.

`ListingDialect::OpenaiCodex` + `parse_openai_codex_listing` keep the
`visibility == "list" && supported_in_api` models and carry the wire
context window and per-model reasoning levels; `hardcoded_catalog` and
its short-circuit in the fetch path are gone. The catalog advertises an
`ultra` tier kigi has no level for — `think_efforts_to_options` drops it
with a warning, as it does any unknown token.
2026-09-21 15:12:54 -04:00
ZacharyZhang-NY 256ff2528e test(config): two e2e routing tests stop racing KIGI_CODE_BASE_URL
`e2e_default_model_with_session_routes_to_proxy` and
`e2e_duplicate_model_field_both_entries_survive` assert the default
model's base URL. They were not serial and read `KIGI_CODE_BASE_URL`
through `EndpointsConfig`, while two serial tests (auth_method.rs,
models_fetch.rs) point that variable at wiremock. When the runs
overlapped, the base URL came back as the wiremock URI: the Linux suite
failed in 2 of 13 full runs. Presetting the variable fails both tests
every time, on v0.1.15 as well.

Both tests are now `#[serial]` and unset the variable for their scope,
the same cure `e2e_enterprise_endpoints_only_no_model_override` got for
the moonshot variable. With the variable preset they pass.
2026-09-19 04:16:35 -04:00
ZacharyZhang-NY bc93b540cd fix(clipboard): the macOS image probe writes into a private scratch dir
The osascript fallback hands the pasted raster to kigi through a file.
Every kigi process used the same three paths in the shared temp dir
(`kigi-clipboard-probe.{png,tiff,jpg}`). Two sessions pasting at once, or
a stale file from an earlier probe, could swap the file between the
AppleScript's write and kigi's read: the wrong image went to the model.

`ProbeTemps` owns a fresh 0700 directory per probe and removes it on
drop, so the hand-written removals are gone. The script builder, the
raster reader, and the read flow move out of the cfg(macos) module into
`clipboard/osascript_probe.rs`, which compiles under test on every
platform; the flow takes the script runner as a closure. The script text
with a scratch dir is byte-identical to the old builder's (checked once
against the old function, then the copy was dropped).

A file paste needs no scratch file. When the directory cannot be made,
the probe warns, sends the file-URL half of the script only, and answers
file URLs as before; with no file URL either, the scratch error surfaces
instead of an empty board. `get_image` needs the file and fails fast.

Tests (all platforms): probes never share a path, the directory goes
away with the probe, mode 0700 on unix (Linux under WSL), the reported
class is read from this probe's file, file URLs survive a missing
scratch dir, and no dir plus no file URL is an error. The remaining
cfg(macos) callers were type-checked in a stubbed scratch crate; the
warm-cache workflow builds both darwin targets before the tag.

Ported from upstream (grok-build shared/clipboard.rs ProbeTemps and
osascript_attachments, 1.0.33).
2026-09-19 03:33:02 -04:00
ZacharyZhang-NY 0189009822 fix(config): a symlinked ~/.kigi/config.toml survives a save
Every config.toml writer wrote a tmp beside the path and renamed it over
the path. With config.toml symlinked into a dotfiles repo, the rename
replaced the link with a regular file: the repo copy went stale and the
link was gone.

`config_write_dest` resolves the destination before a writer names its
tmp. For the user's own config path a symlink resolves to its referent,
so the tmp sits on the referent's filesystem and the rename stays atomic.
A dangling link is an error, not a clobber. All eight writers call it:
`save_config`, the four MCP writers, and the three import writers.

Only the user's config path is followed, and only while
`user_kigi_home()` resolves. `kigi mcp add --scope project` and the
Claude project import write `.kigi/config.toml` inside the repo; a
hostile repo could ship that file as a symlink to any file the user
owns. With no home, `kigi_home()` falls back to `./.kigi`, which is that
same project tree, so nothing is followed then. Untrusted paths come
back unchanged and the link itself is replaced, as before. AGENTS.md
records the rule.

Tests (unix; run on Linux under WSL): helper cases including the no-home
case, a project-scope save that spares the link's target, and an
end-to-end save under KIGI_SHARE_DIR. The end-to-end test fails with
following disabled. Windows symlinks were not exercised: the host lacks
the privilege.

Ported from upstream (grok-build config/fs_atomic.rs LeafSymlink::Follow,
1.0.35): the rule, not the 1375-line module.
2026-09-19 03:15:01 -04:00
ZacharyZhang-NY 206711bed5 fix(skills): a skill body stops at the 25k-token read cap
`read_file` exempted SKILL.md from every limit, and both injection paths
(`/skill` expansion and the `skill` tool) pasted the whole body. A 220 KB
skill put 55k tokens into the context on each call.

`cap_skill_body` cuts an over-cap body at the last line boundary that
fits (a char boundary when the body is one long line) and appends a note
that points at offset and limit. It runs before `apply_substitutions`, so
the arguments land after the cut. Both injection paths call it and log
the cut.

`read_file` returns a SKILL.md whole only while it fits the cap. Over the
cap it reads like any other file: offset and limit apply, and a window
that is still too large answers FileTooLarge. Without that the note would
send the model to a read that ignores its range.

`skill_file_skips_token_limit` pinned the exemption; it gives way to
`skill_file_over_cap_returns_file_too_large` and
`skill_file_over_cap_honors_offset_and_limit`, as upstream did.

Ported from upstream (grok-build tools skills/skill.rs cap_skill_body and
read_file skill_full, 1.0.33). Upstream's `max_output_bytes` read budget
in the same hunk is a feature and stays out.
2026-09-19 02:31:24 -04:00
ZacharyZhang-NY 443de79e2f fix(textarea): Up and Down land on the soft-wrapped row they target
A soft-wrapped row and the next row share a byte: the row's exclusive end
is the next row's start. Up or Down with a goal column past the target
row's width put the cursor on that byte, so it drew on the wrong row. Up
made no progress and Down skipped a row.

`row_with_end` tells a soft wrap from a hard break (byte-adjacent rows
are soft). `move_to_display_col_on_line` takes the row and its `RowEnd`
and clamps a soft row to its last grapheme, or to the start of an element
that closes the row. A hard-break row still reaches its newline.
`clamp_to_line` now cuts on graphemes, not chars, so a ZWJ cluster stays
whole.

Kigi differs from upstream's premise that elements wrap whole: with no
display override in the buffer, `wrapped_lines` uses plain textwrap and
an element can span rows. Snapping to such an element's start would
leave the target row (Down from byte 0 stuck at 0), so an element that
began on an earlier row keeps the boundary `display_col_to_buffer_pos`
chose, as before this change.

`wrapped_navigation_with_zwj_graphemes` asserted the bug: Up from the
end stayed on the last row (byte 22). It now expects row 1's cluster
start (byte 11), as upstream does.

Tests: upstream's four cases, the row-spanning element, and a seeded
property test (500 cases): each Up or Down moves exactly one row. The
property test fails with the clamp disabled.

Ported from upstream (grok-build ratatui-textarea/textarea.rs, 1.0.28).
2026-09-19 02:17:19 -04:00
ZacharyZhang-NY be0983237e fix(mcp): an abandoned tool call tells the server to stop
`try_call_tool` wrapped `call_tool` in a bare `tokio::time::timeout`.
On a timeout or a cancelled turn the future dropped and the server kept
working: no `notifications/cancelled` ever left the client.

`call_tool_cancel_aware` sends the request through
`send_cancellable_request` and holds a guard armed with the request id.
A settled call disarms it. A dropped future (cancelled turn) spawns the
cancel from the guard's drop. A timeout sends the cancel inline, before
returning: the HTTP timeout arm resets the transport next, and dropping
the last service handle killed a spawned cancel before it left.

The inline send waits 500 ms at most. rmcp 2.2.0 awaits its own
timeout-cancel without a bound, and its streamable-HTTP worker awaits
each POST in turn, so against a JSON-mode server the cancel queues
behind the hung call: with rmcp's timeout path the call returned after
3.0 s against a 1 s budget. That server class cannot receive the cancel
until rmcp 3.x; the budget elapses, a warn is logged, and AGENTS.md
records the limitation. stdio and SSE-mode servers receive it.

Tests: dropped call, timed-out call followed by a service drop, settled
call; the HTTP outer-timeout test now asserts the return time.

Ported from upstream (grok-build mcp/servers.rs call_tool_cancel_aware,
1.0.33); upstream leans on rmcp 3.2's timeout path.
2026-09-19 02:07:30 -04:00
ZacharyZhang-NY 3ccb6e5e91 fix(config): reasoning_effort accepts mixed case
`reasoning_effort = "High"` in config.toml failed the whole parse: the
derived Deserialize matched lowercase variants only, while FromStr
lowercased its input. Deserialize now routes through FromStr, so both
paths share one vocabulary. Unknown tokens and non-strings still error.

Ported from upstream (grok-build sampling-types/types.rs, 1.0.35).
2026-09-19 01:51:55 -04:00
雷电芽衣 d331b0b2a3 fix(goal-classifier): git diff headers keep a/ b/ prefixes under any host gitconfig
`extract_changed_files` parses `diff --git a/<old> b/<new>` headers.
A host `diff.mnemonicprefix=true` or `diff.noprefix=true` rewrote them
(`c/ w/`, or none), so every tracked change vanished from `changed_files`
and the two evidence git-diff tests failed on such hosts. Both capture
paths now pass `--src-prefix=a/ --dst-prefix=b/`, which override both
config keys. Regression test sets each key in the temp repo.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 13:36:58 -04:00
雷电芽衣 09867915ef fix(sandbox): the devbox /data write-deny makes bubblewrap mandatory
A devbox-based profile on a host with /data warned and fell back to
Landlock when bubblewrap was missing or failed to exec, and Landlock
grants the workspace under /data. requires_data_write_deny (devbox
inheritance and an existing /data; a stat error other than NotFound
counts as present) now joins requires_read_deny in the fail-closed
startup decision, and the /data ro-bind is added only under that
predicate, so a missing /data no longer makes bwrap fail to exec.

Ported from upstream (grok-build sandbox/lib.rs requires_data_write_deny).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 13:17:52 -04:00
雷电芽衣 edce333620 fix(subagent): project-agent mcpServers follow folder trust; agent names are one path component
A project-scoped agent definition from an untrusted repository could
declare inline stdio mcpServers and the child session started that
command, while inline hooks on the same definition were already gated on
folder trust. The mcpServers list now uses the same gate.

By-name agent lookup joined the requested name onto each searched
directory, so a name carrying a separator or ".." resolved a file outside
that directory under its trust scope; both lookup entry points now reject
any name that is not a single path component.

Ported from upstream (grok-build subagent/handle_request.rs mcpServers
gate), extended with the name guard.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 13:04:46 -04:00
雷电芽衣 d1c29a576c fix(compaction): failures carry the real error, with endpoint credentials redacted
The pager rendered "Compaction failed." with the error dropped, and the
shell's unsuppressed auto-compaction failure sent an empty error. Both
now carry the decoded first line of the failure (retry guidance plus the
detail), decoded from the ACP data rather than its JSON-escaped Display,
propagated unchanged through the compact extension, capped for a banner.

Endpoint credentials never reach the banner: the sampler redacts the
parsed endpoint (userinfo, sensitive query values, fragment via the
repository sanitizer) in every 401 message and the appended request URL,
and the shell redacts every URL in the error text before line selection,
including URLs whose password holds whitespace or that follow a
multi-byte delimiter.

Ported from upstream 1.0.13 (grok-build dispatch/prompt.rs,
session/compaction.rs), extended with the redaction.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 12:12:00 -04:00
雷电芽衣 17d4ba08f9 fix(exit): diagnostics after the terminal may be gone are best-effort writes
With fd 2 dead (a closed pane, an exited pipe reader) every write fails,
eprintln! panics on that failure, and under panic = "abort" the process
dies by SIGABRT with a crash report on the next launch instead of its
exit code. Exit reports, headless plain-format diagnostics, update-exit,
setup-result and leader-management messages now write through
best_effort_stderr::eprint_line: one attempt, failure reported, never a
panic.

Ported from upstream 1.0.19 (grok-build best_effort_stderr.rs).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 10:39:20 -04:00
雷电芽衣 901203c2b2 fix(tui): pasted U+2028 and U+2029 become line breaks
str::lines() splits on neither separator, so a paste carrying them counted
as one line and the invisible characters broke the textarea's width model.
Bare \r, U+2028 and U+2029 now normalize to \n on every paste route (the
prompt, the inline paste key and the previous-message editor); dropped
file paths keep their bytes verbatim because a separator in a filename is
payload.

Ported from upstream 1.0.18 (grok-build prompt_widget normalize_line_breaks).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 10:16:22 -04:00
雷电芽衣 d8609267a1 fix(images): resize over-cap attachments even when the re-encode saves no bytes
An attachment over the side or area caps whose re-encode did not shrink
the byte count was returned unchanged at its original dimensions, so the
provider still received an over-cap image. The byte-saving short-circuit
now applies only within the caps; a resize that would fall under the
vision minimums keeps the original on the existing fallback path.

Ported from upstream 1.0.13 (grok-build session/image_normalize.rs).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 09:53:11 -04:00
雷电芽衣 a7b8438881 fix(tui): refresh the context bar when auto-compaction starts
AutoCompactStarted carries the count the trigger fired on, but only
AutoCompactCompleted refreshed the context bar, so the "N% full" banner
sat next to a stale pre-turn number until the next stream update. Both
lifecycle updates now refresh the bar, on the root view and on subagent
views.

Ported from upstream 1.0.12 (grok-build acp_handler/session_notification.rs).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 09:35:27 -04:00
雷电芽衣 584d0fb77e fix(tui): /compact <instructions> reach the compaction request
The queued /compact command drained into Effect::Compact with only the
session id, so the instructions typed after the command never reached the
shell's compaction prompt (which already accepts userContext). The effect
carries the parsed instructions, trimmed so an edited queue row with
leading whitespace still counts.

Ported from upstream 1.0.22 (grok-build dispatch/queue.rs).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 09:23:39 -04:00
雷电芽衣 4512df79e6 fix(tui): terminal escapes ride the writer queue and teardown is bounded
A terminal that stopped draining the pty parked the writer thread inside
its tty write while it held the stderr lock; the next inline stderr write
from the event loop (mouse toggle, notification, kitty clear) deadlocked
the loop, and /quit hung on the unbounded writer join and the teardown
writes.

EscapeWriter queues out-of-band escapes on the frame writer: the mouse
capture toggle, notifications (title, progress, protocol), the auth
cursor, gboom keyboard flags, the wrap clipboard request, and the kitty
clears (queued per agent view and merged into the frame's post-flush,
nested subagent views included). The $EDITOR/$PAGER handoff aborts when
the writer does not drain. WriterThread::join_within and the teardown
run under a 2 s grace; after a stall fd 2 is silenced, the exit hints,
relaunch and update prints are skipped, and the panic hook keeps only its
log report.

Ported from upstream 1.0.19 (grok-build EscapeWriter, WriterJoin,
run_bounded_teardown), extended to the remaining inline writes.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 09:08:09 -04:00
雷电芽衣 91f14112bf fix(subagent): bound the completion-path awaits so teardown reaches Shutdown
A completed child parked forever behind a parent that never serviced its
command channel, or behind its own frozen chat-state actor, leaking the
child session thread, fs watchers and fds. Every await in the completion
path that rides another actor is now bounded: child chat-state reads
degrade to their fallbacks after 10 s, the usage-fold and not-applied acks
give up after 30 s (an unacked mark falls back to the coordinator's sticky
mark), list_tasks and reparent_notifications are bounded, and the
post-Shutdown worktree snapshot is bounded while removal is awaited in
full so no detached deletion can race a resume.

Ported from upstream 1.0.14 (grok-build subagent/handle_request.rs
PARENT_ACK_TIMEOUT / child_actor_query), reshaped without the drain-budget
const assert.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 07:57:20 -04:00
雷电芽衣 fc7113ba3c fix(chat-state): reasoning estimate and calibration reseed on every rewrite
A Reasoning item counted its text and its encrypted blob as two separate
payloads, inflating the estimate for retained reasoning; the estimate now
takes the larger of the two (base64 runs 4/3 over the raw bytes).

Only compaction carried the provider-confirmed ratio into the reseeded
total; a rewind or a non-compaction replace sprang the context meter back
to the raw estimate. reseed_total_tokens applies the ratio (capped at the
pre-rewrite total) to every rewrite, and the truncate path uses it too.

Ported from upstream 1.0.1x (grok-build xai-chat-state actor/state.rs,
mutations.rs, queries.rs).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 07:24:07 -04:00
雷电芽衣 00a5e318bb fix(config): an explicit [model.X] api_backend survives same-slug propagation
The slug-match step treated an entry whose api_backend equals the default
as unset and overwrote it with a same-slug sibling's backend, so
`api_backend = "chat_completions"` silently became messages/responses.
Config keys whose override sets api_backend are recorded and skipped by
the donor step; an unset backend still inherits.

Ported from upstream 1.0.22 (grok-build agent/config.rs
explicit_api_backend_keys).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 07:13:47 -04:00
雷电芽衣 017a570d7d fix(sampler): a max_tokens stop after complete tool calls runs them
The Messages stream failed every max_tokens stop with MaxTokensTruncation,
so a response that had already emitted complete tool calls before the
output budget ran out lost those calls and failed the turn. The stream
now completes with StopReason::ToolCalls when every collected call's
arguments parse (or are empty, the zero-argument convention); a cut call
or a text-only truncation still fails so compact-on-error keeps working.
The ChatCompletions and Responses streams already behaved this way.

Ported from upstream 1.0.1x LengthPolicy::CompleteToolCalls, reshaped
into the stream layer because Kigi has no caller that chooses a policy.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 07:02:05 -04:00
雷电芽衣 63d26d4a3e fix(persistence): FlushAndAck is a durability barrier that reports lost writes
The persistence actor acked FlushAndAck unconditionally after writing its
pending merged notification: nothing was fsynced and every storage failure
was only warn-logged, so persist_ack, the reconnect flush and history
repair treated lost writes as persisted.

The barrier now writes the pending notification, reports the first storage
failure latched since the last barrier (every actor arm latches), and
fsyncs the whole session directory tree (files, directories, deletions).
Session directories are created durably (init and fork copy), and the
shared summary writer syncs its staged bytes before publishing and the
parent afterwards, so a concurrent /rename cannot publish unsynced bytes
over a synced summary.json. FlushComplete and the persist_ack, history
repair and end-of-turn callers propagate the io::Result; CopyFile fails on
a sync error but keeps the latch for the barrier.

Ported from upstream 1.0.1x (grok-build persistence.rs pending_write_error
+ durable storage helpers), reshaped without the dirty-file set.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 06:51:27 -04:00
雷电芽衣 5f7416f3ef fix(permissions): native path deny/ask rules apply to the file the tool opens
Edit/Read/Grep rules matched only the raw argument string, so a workspace
symlink (or a quoted, `~`, `./`, `..`, or absolute spelling) reached a
denied file. The policy now resolves the path exactly as the tools do
(sanitize, expand `~`, display-cwd remap, join to the requesting session's
cwd), follows symlinks parent-first, and re-checks deny/ask rules on the
opened path and its target as absolute, `./`-relative and relative
spellings. Escalation only: no allow is granted through a link. A link
that cannot be followed forces a prompt that survives YOLO, auto mode and
the session edit grant, with event reason `unresolved_link`.

Permission requests now carry the requesting tool's cwd and display cwd
so a child worktree session sharing its parent's handle is checked where
it runs.

Ported from upstream 1.0.14 (grok-build permission/policy.rs symlink
re-check), reshaped for Kigi's lexical rule matcher.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 05:48:06 -04:00
雷电芽衣 fb3fe89d33 fix(permissions): rg exec flags cannot ride an allowlist, and argv is decoded
Port of upstream grok-build 3794978 (snap6 "Reject ripgrep --hostname-bin
on always-safe and Auto routine paths"), widened where review showed the
same hole one layer down.

- `rg --pre` / `--hostname-bin` execute a program per file. The safe
  list refused only `--pre`, and the Auto-mode routine classifier had
  no rg flag check at all. One shared `rg_has_unsafe_flag` now refuses
  both flags (bare or `=VALUE`) on a lowercased-basename head, scanning
  every argument: rg's grammar (`--replace -- --pre`, `-ne --`) makes a
  partial arity model bypassable, so a literal search for the flag name
  costs a prompt instead of the auto-allow.
- The shell splitter handed allowlists undecoded source: a concatenation
  (`--host""name-bin`, `''--hostname-bin`, `--p're'`) kept its quotes,
  and double-quoted escapes (`"--host\<newline>name-bin"`) survived.
  `PlainCommand` words are now the decoded argv (bash double-quote
  rules, whole-node text minus the quotes so literal newlines and the
  whitespace tree-sitter folds into the closing quote survive), with a
  per-word `expandable` flag for unquoted glob/brace/tilde/backslash
  syntax the shell would still expand (`--{pre,pre}`, a glob that can
  match a file named `--pre`); the rg guard treats those as unsafe.
- Every prefix-based decision matches per argv element through one
  matcher (built-in safe list, always-safe list, dangerous list, Auto
  routine prefixes): a decoded word with inner whitespace (`rg    `,
  `rg /../payload`, one word spelled `git status`) can no longer pose
  as a prefix. Saved grants match token for word (a grant saved from a
  spaced argument prompts again). Saved denies keep the pre-0.1.15
  check verbatim on a `legacy_words` spelling (raw content and raw
  concatenations, wrappers peeled independently, space-boundary prefix)
  so no deny narrows on upgrade.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 04:47:21 -04:00
雷电芽衣 aa9a5dbe72 fix(auto-mode): git checkout/switch/stash no longer ride a blanket prefix
Port of upstream grok-build 7581004 (1.0.22 "Auto mode no longer
instantly runs destructive git checkout -- commands"), routine_git.rs.

`ROUTINE_PREFIXES` auto-allowed `git checkout`, `git switch` and
`git stash` wholesale, so Auto mode ran `git checkout -- <file>` (a
working-tree discard) and `git stash drop`/`clear` without review.
Those verbs now go through a fail-closed operand classifier: `switch`
is routine with one ref operand and benign flags; `stash` only for
push/save/pop/apply/list/show/branch; `checkout` additionally needs a
branch-mode flag (`-b`, `-B`, `--orphan`, `--detach`, `-t`) anywhere in
the argument list before an operand counts, because a bare
`git checkout <name>` — `-` included — restores a tracked path when no
such ref resolves (a deliberate deviation from upstream, which allowed
`git checkout main` outright). Path-like operands, pathspec flags and
`--` fail closed. What is not routine goes to the model side-query, not
to a hard block.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 02:36:14 -04:00
雷电芽衣 7a60cd42ac fix(trust): gate directory-typed project hook sources
Port of upstream grok-build 7581004 (1.0.22 window) folder_trust.rs.

`collect_repo_config_kinds` gated `.kigi/hooks` with `is_dir()` and
`.cursor/hooks.json` with `is_file()`, and `.claude/settings*.json` via
the settings-file scan, while the hook loader (`path_to_source`) takes
a directory at any of the four project hook paths as a hook source and
parses its JSON children. A clone carrying `.cursor/hooks.json/evil.json`
or `.claude/settings.json/evil.json` and nothing else resolved as
trusted through the no-config branch and ran its SessionStart hook.

The gate now treats any present path (file, directory, dangling
symlink) or an unreadable one at the four sources
`discover_hook_source_paths` loads from the git root as a hit.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 02:17:25 -04:00
雷电芽衣 97c934a647 fix(sandbox): strict profile no longer lets a child rewrite ~/.kigi
Port of upstream grok-build 1.0.14 ("--sandbox strict now restricts
writes to ~/.grok/sessions only"), shaped for Kigi.

The strict profile granted the whole ~/.kigi read-write, so a sandboxed
command could plant a global SessionStart hook or flip
`[sandbox].profile` in config.toml for the next launch. Landlock cannot
carve a child out of a write grant, so the home is now read-only
except an explicit allowlist of state subtrees (run, sessions,
worktrees, worktree_pool, tasks, memory, logs, debug, crash, memtrace,
trace-exports); config.toml, auth.json, sandbox.toml, hooks/, bin/ and
vendor/ stay read-only. A symlinked state dir under the home is
followed only to the default ~/.kigi/sessions.

What the process itself writes there after enforcement moves or is
minted first:
- kigi-shell `apply_sandbox` mints the device id, extracts the bundled
  rg and the bundled skills BEFORE enforcement for every profile that
  restricts home writes (`restricts_home_writes`, custom profiles
  extending strict included).
- The sandbox audit log lives at ~/.kigi/sessions/sandbox-events.jsonl.
- Leader lock/socket default to ~/.kigi/run/, the leader stderr log to
  ~/.kigi/logs/leader.log; discovery scans run/ and the legacy root,
  and the default target prefers the current socket.
Root-level caches (version.json, agent_id, slash-mru.json, pager.toml,
claude_import_state.json) degrade best-effort under strict; documented.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 02:08:42 -04:00
雷电芽衣 cadc46f5dc fix(sandbox): close io_uring and sendmmsg holes in the child network filter
Port of upstream grok-build 9684fa3 (1.0.11 window) `child_net.rs`.

The per-child seccomp filter matched seven socket syscalls by number and
allowed everything else, so a restricted child could still reach the
network through `sendmmsg` or an io_uring submission queue, and a
syscall issued through another arch table (x32 on x86_64) never matched
the deny list at all.

- Gate on the audit arch before matching `nr` and reject x32 numbers on
  x86_64 (foreign-table syscalls now get EPERM instead of passing).
- Add `sendmmsg`, `io_uring_setup`, `io_uring_enter`, `io_uring_register`.
- Build the program once (`prebuilt_child_network_filter`) and hand the
  slice to the `pre_exec` installer; the old code allocated the Vec after
  `fork`, which is not async-signal-safe. All three callers updated.
- Tests evaluate the BPF program over synthetic `seccomp_data`.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 01:19:36 -04:00
雷电芽衣 b957e12370 fix(tui): repaint the header context window on a /model switch
After `/model`, the header kept the previous model's window ("15K /
1.0M" on a 262K model) until the next turn or a restart. The pager
caches the window inside its context snapshot (streaming `totalTokens`
updates refresh it from the catalog) and nothing refreshed the cache
when the session's model changed. Comparing cached numbers against
catalog windows cannot tell a catalog-derived total from a shell
effective limit (KIGI_DEBUG_CONTEXT_WINDOW, header upgrades), so the
shell, which installs the effective window, now reports it:

- kigi-shell: `handle_set_session_model` returns the installed window;
  `model_switch::apply` stamps it as `totalContextTokens` in the
  `session/set_model` response meta and carries it on the
  `ModelChanged` broadcast (`context_window`, serde-default: old pagers
  ignore it, old leaders omit it). The response meta previously
  serialized the actor's inner `Result` and reported success on an
  actor failure; that error now propagates.
- kigi-tui: `Effect::SwitchModel` reads the meta; `SwitchModelComplete`
  carries `Ok(Option<u64>)`; the completion handler and the follower
  `ModelChanged` arm call `AgentView::apply_context_window`, which
  re-derives the snapshot (pct, free tokens) and reports whether it
  changed so a follower redraws on a window-only change.
- Tests: the bare `/model <name>` dispatch path asserts the snapshot and
  the drawn header; follower with/without the field; same-model new
  window redraw; actor returns the window; serde round trip.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 00:15:14 -04:00
雷电芽衣 ff92a00fea chore(fmt): reflow three link constants rustfmt rejects
f7a4cb6 repointed the GitHub links without running cargo fmt; the
fmt gate has been red since.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-12 23:17:44 -04:00
ZacharyZhang-NY f7a4cb64ab Return the forge to GitHub: releases, self-update, links, CI
Release / build (x86_64-unknown-linux-gnu) (push) Failing after 6s
Release / build (aarch64-apple-darwin) (push) Canceled after 0s
Release / build (aarch64-unknown-linux-gnu) (push) Canceled after 0s
Release / build (x86_64-apple-darwin) (push) Canceled after 0s
Release / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
Release / publish GitHub Release (push) Canceled after 0s
The GitHub account is restored, so github.com/ZacharyZhang-NY/Kigi-CLI is
the canonical repo again. Reverse the 2026-08 Gitea migration:

- .github/workflows/{release,warm-cache}.yml: restore the GitHub-hosted
  pipeline dropped in 3e1652c, unchanged — hosted runners, rust-cache,
  softprops release with every asset attached atomically.
- .gitea/workflows/release.yml: retire the Gitea edition (the runners are
  being deregistered; git.zacharyzhang.com keeps the v0.1.12/v0.1.13
  releases for old clients).
- kigi-env: update_base_url now defaults to the GitHub Releases API. The
  updater already speaks GitHub wire format and sends a User-Agent; v0.1.14
  is published to BOTH forges with identical assets so pre-0.1.14 installs
  polling Gitea self-update onto the GitHub channel.
- install.sh / install.ps1: resolve releases from api.github.com.
- feedback, /docs, SECURITY.md, frontend worker + site: repo, issues and
  raw-file links point at github.com / raw.githubusercontent.com.
- docs/RELEASE.md: GitHub edition of the checklist; Gitea runner sections
  retired.
- version: 0.1.13 -> 0.1.14 (the cross-forge transition release).

Claude-Session: https://claude.ai/code/session_01A6RcwHhhsFYYrNdQB4ASuP
2026-09-01 01:05:22 -04:00
ZacharyZhang-NYandClaude Fable 5 fd5cff2de6 chore(gates): finish the Windows sweep — zero clippy findings workspace-wide
Second layer of platform gaps the mac/linux-only gate history left:
unix-only test imports/helpers now cfg-gated (mermaid, fast-worktree,
hunk-tracker, pager-render link test, three pty_e2e module decls),
clippy-suggested cleanups in Windows-cfg code (file-utils classifier,
tui console-mode restore, shared clipboard arboard-error flow, tools
terminal closure, workspace capability tail), and `cargo update` of h2
(RUSTSEC-2026-0258) and webbrowser (RUSTSEC-2026-0257) so `cargo deny
check advisories` passes again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 12:04:32 -04:00
ZacharyZhang-NYandClaude Fable 5 50669bf302 chore(gates): zero clippy warnings and a buildable sandbox example on Windows
The full-workspace gates had never run on this platform: collapsible
ifs in the Windows shell probe, a same-type cast in the crash handler,
an unused AsRawHandle import, and the sandbox smoke-test example
building its unix-only body (libc, kernel enforcement) unconditionally.
The example now stubs main off-unix; formatting reflow from cargo fmt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 11:55:19 -04:00
ZacharyZhang-NYandClaude Fable 5 25d1a4f980 fix(sampler): retry Cloudflare 52x and other 5xx during outages
Port of upstream grok-build 1.0.0/1.0.1-window retry policy.

- is_retryable_api_status: 429 plus any 5xx except Cloudflare 525/526
  (origin TLS handshake / invalid certificate — never clears on its
  own), replacing the 500|502|503|504|520 allowlist so the 521-524,
  529, and 530 edge pages an outage serves actually retry.
- The generic retry path clamps a server Retry-After to
  MAX_RETRY_BACKOFF (30s) and jitters it: Cloudflare 52x pages ask for
  60-120s, which across the budget stalled a turn ~28 minutes. The 429
  path keeps the full (parse-capped) Retry-After under its low attempt
  cap, and jitter is shared via jittered().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 11:51:36 -04:00
ZacharyZhang-NYandClaude Fable 5 a441b23a6d fix(mcp): kigi mcp add <name> <url> infers an HTTP server
Port of upstream grok-build 1.0.9. A bare positional http(s):// URL
with no transport flag was silently added as a stdio command whose
"command" was the URL. It now infers the HTTP transport with a notice;
extra args, --env, legacy --command/--url, and every explicit flag keep
their prior meaning, and scheme-less URL-looking commands still stay
stdio with the existing warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 11:42:47 -04:00
ZacharyZhang-NYandClaude Fable 5 574122884a fix(windows): resolve the home directory via the standard lookup
Port of upstream grok-build 1.0.4/1.0.5. Two paths read $HOME only,
which is usually unset on Windows outside Git Bash:

- the global-gitignore fallback (prompt/ignore.rs) now uses
  std::env::home_dir(), the crate-wide idiom;
- fast-worktree's resolve_kigi_home falls back to std::env::home_dir()
  with the same dunce canonicalization, eliminating the documented
  divergence from kigi_config::default_kigi_home. On Unix home_dir()
  reads $HOME first, so behavior is unchanged where HOME is set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 11:34:45 -04:00
ZacharyZhang-NYandClaude Fable 5 97de661554 fix(startup): thread-exhaustion resilience — cap workers, never panic
Port of upstream grok-build 0.2.113/0.2.114 fixes, extended to Kigi's
startup order.

- kigi-tty-utils runtime module: worker-cap policy (8 max) — tokio's
  per-core default pins 100+ thread slots on many-core shared hosts
  against RLIMIT_NPROC / pids.max, and later spawns die with EAGAIN.
- kigi-bin builds the main runtime with capped workers and reports an
  out-of-threads host with a diagnostic + exit(1) instead of a panic.
- The pre-runtime optimization threads (HTTP warmup, early model
  prefetch) spawn recoverably and degrade — their panic would have
  preempted the runtime's own diagnostic.
- The per-session actor thread and its runtime report spawn/build
  failure through the existing acp::Error / AgentBuildError boundaries
  (new RuntimeBuild variant) instead of panicking the agent host.
- fsnotify's watcher thread spawn maps into FsNotifyError::WatcherStart
  so callers degrade instead of crashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 11:28:17 -04:00
ZacharyZhang-NYandClaude Fable 5 bf93091416 fix(agents): bound how many subagents one session runs at once
Port of upstream grok-build 1.0.1 (landed upstream 2026-08-06). The
coordinator fire-and-forgot every spawn, so a wide fan-out (graph
waves, swarm bursts, model-driven Task storms) could exhaust file
descriptors.

- kigi-tools task/admission: SubagentLimits — 32 concurrent per session
  by default (KIGI_MAX_CONCURRENT_SUBAGENTS), queue|fail behavior
  (KIGI_SUBAGENT_LIMIT_BEHAVIOR), zero clamped to 1, read once at the
  composition root; parse_positive_env helper.
- Each parent session gets one spawn semaphore (map entries live for
  the process so a resumed session shares its predecessor's limit);
  handle_subagent_request holds an owned permit for the whole child
  run — queue awaits FIFO with a unified-log notice at saturation,
  fail routes through send_pre_spawn_failure. After a queued wait, a
  closed parent command channel (teardown) fails the spawn instead of
  running against a dead parent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 11:09:24 -04:00
ZacharyZhang-NYandClaude Fable 5 26d26e15c9 fix(exec): bounded post-kill reaps — D-state children can't wedge a turn
Port of upstream grok-build 1.0.1 (landed upstream 2026-08-06).

A child wedged in an uninterruptible kernel syscall (D-state) never
observes SIGKILL, and a background descendant can hold the output pipes
open past the kill; both previously stalled tool futures without bound.

- kigi-tty-utils: KILL_REAP_TIMEOUT (5s) + reap_killed_bounded — wait
  for an already-killed child at most the bound, then abandon the corpse
  to tokio's orphan reaper.
- local_terminal: pipe readers share their buffer with the spawner and
  join under the reap bound on both exit arms (aborted readers are
  awaited so no tail bytes race the take); the whole tree is enrolled in
  a ProcessGroup and group-killed on timeout; the timeout arm returns
  the synthetic result instead of waiting on the corpse. Grandchild
  kill + prompt-return pinned by a ported unix regression test.
- kigi-tools: reap_killed_search_child wrapper; grep's four kill sites
  reap bounded; glob's stderr goes to null (an unread pipe blocks rg on
  warning backpressure) and its truncation kill reaps bounded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 10:45:17 -04:00
ZacharyZhang-NYandClaude Fable 5 1ae803519e fix(compaction): configurable summarizer tool_choice; wider overflow detection
Port of upstream grok-build 0.2.105 + 0.2.118.

- CompactionToolChoice (auto default | none), resolved env
  KIGI_COMPACTION_TOOL_CHOICE > [features].compaction_tool_choice >
  remote settings. Some OpenAI-compatible servers reject
  tool_choice:"none" with tools attached, failing every long-session
  compaction; auto keeps them working (the summarization prompt already
  forbids tool use). Threaded from agent + subagent spawn through
  CompactionConfig into generate_session_compact; the Responses gate
  counts hosted tools too, so a hosted-only request still carries the
  configured choice.
- is_context_length_error recognizes the "current message ... exceeds
  budget" phrasing, so that overflow is Fatal instead of burning the
  retry budget on a deterministic failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 10:22:18 -04:00
ZacharyZhang-NYandClaude Fable 5 6a4bff0d45 fix(agent): a racing cancel can no longer wedge the session's turn slot
Port of upstream grok-build 0.2.103. session/prompt runs an async intake
preamble before enqueueing SessionCommand::Prompt on the actor mailbox; a
concurrent cancel could enqueue Cancel first, land on an idle actor as a
no-op, and the prompt then started a turn nothing would ever cancel.

The per-session prompt-intake lock becomes the dispatch lock: prompt()
holds it across intake as before, and cancel() now holds it around its
Cancel send, so a cancel orders behind the prompt it targets. Regression
test ported verbatim (unix-only harness, like its siblings).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 09:53:09 -04:00
ZacharyZhang-NYandClaude Fable 5 6e0464fa74 fix(auth): unique tmp per auth.json write; tighten loose credential perms
Port of upstream grok-build 0.2.113-window fixes:
- write_auth_json_atomic tmp path gains a monotonic sequence next to the
  pid, so two concurrent in-process writers (background mint + proactive
  refresher) cannot share one tmp path and tear each other's bytes; a tmp
  write that fails partway is reclaimed instead of orphaned.
- New secure_file::ensure_owner_only_permissions (0o600 / Windows ACL)
  tightens pre-existing world-readable credential files on read, backup,
  write, replace, and restore. Best-effort with warns after durable
  content — a chmod-only failure must not masquerade as a failed write —
  and propagating in restore_prior_bytes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
2026-08-27 09:31:21 -04:00
ZacharyZhang-NYandClaude Opus 5 9fb82291b2 chore(migration): repoint every published link off GitHub
The canonical repo is git.zacharyzhang.com now, so every URL a user can
actually reach had to move with it.

Self-updater: `PRODUCTION_ENDPOINTS.update_base_url` targets Gitea's
`/api/v1/repos/.../releases`, which serves the same wire format GitHub's
releases API did — `Release`/asset deserialisation is unchanged. The alpha
channel requests `?per_page=30&limit=30` because GitHub reads the page size
from `per_page` and Gitea from `limit`, and each ignores the other's, so one
URL stays correct on either.

Installers: same API base, and the documented one-liners become
`https://kigicli.dev/install.{sh,ps1}`. That domain's Worker proxies both
files from Gitea raw `main`, so the published install command survives any
future forge move without being reprinted everywhere. install.ps1 also drops
the GitHub-specific `Accept: application/vnd.github+json`.

Everything else user-facing: `/feedback` and the shell's feedback fallback
point at the Gitea issue tracker, `/docs` at the Gitea-rendered README, and
web_fetch's User-Agent at kigicli.dev. SECURITY.md now routes reports to the
issue tracker with a warning to withhold exploitable detail, because Gitea
has no equivalent of GitHub's private vulnerability reporting. Comments
naming `.github/workflows/release.yml` name `.gitea/` now.

Tests assert against these constants rather than literal URLs, so they
follow automatically; the one URL-shape assertion (production endpoints are
https) still holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 21:13:19 -04:00
ZacharyZhang-NY f656627c10 fix(tui): start the session a sessionless model switch defers into
Launching kigi in a non-project directory (~/Downloads, ~, /tmp — anywhere
`is_project_dir` rejects) leaves the agent view session-less behind the
project-picker question, which only a PLAIN prompt can open. Slash
commands still execute there, so `/model <name> <effort>` stashed its
switch in `deferred_model_switch` — a stash that assumes a create is in
flight — and with none pending it dangled forever with zero feedback:
the picker rendered, the user chose model and effort, and nothing
changed. Not a 0.1.10 regression: 0.1.9 pty-reproduces identically; the
report correlated with the upgrade only because every earlier launch
happened to be from a project directory.

Apply the QueueCommand precedent ("queued slash work bypasses the
picker, so create the deferred session or it never drains") to both
deferral sites — Action::SwitchModel in the router and
set_default_model's no-session branch — via
skip_picker_and_create_session, whose in-flight guard already makes it a
no-op while a create is pending, so the racing-create case is unchanged.
SessionCreated then applies the stash through the existing
apply_deferred_model_switch path; pty-verified end-to-end from
~/Downloads (session created, model changed, effort applied).
2026-07-27 15:55:57 -04:00
ZacharyZhang-NY bac8470c80 fix(hooks): share one SSRF policy instead of a second, stale copy
The hook runner carried its own `is_blocked_ip`, a line-for-line copy of
the pre-hardening web_fetch predicate: loopback allowed unconditionally,
and no TEST-NET, 198.18/15, 240/4, 0.0.0.0/8, multicast, IPv6 site-local
or embedded-v4 wrapper coverage. Hook URLs come from settings, and
project settings from an untrusted repo are loaded today, so the gap is
reachable.

`kigi-hooks` already depends on `kigi-tools`, so the fix is to delete the
copy and call the shared predicate — no new crate, and no third
implementation to drift.

`allow_local` is on for hooks: a loopback hook receiver is a legitimate
local setup, which the old copy also allowed. It is now allowed only
when the URL names the host literally, so a public name resolving to
loopback is refused.

Range coverage now lives with the policy; the runner's tests pin what it
adds on top. The URL-scrubbing regression test moves off TEST-NET-1,
which the policy now blocks, onto a closed loopback port — faster, and
no real egress from a test.
2026-07-27 13:01:50 -04:00