Repository navigation
feat(gateway): GatewayPolicy and declared adapter capabilities - #178
Conversation
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Ingest decision-model and Ollama research, add the decision-models concept page, note ollama.com cloud auth and stale defaults on the providers page, and link the fast lane from action-terminal mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Ingest a read of Hermes upstream at bed0d535 and record how it mixes models (fallback chain plus per-task auxiliary models), where embeddings live (memory plugins), and how its 14 community Jev plugins work. Add comparison rows and point decision models and plugin hooks at the revised #148/#149 scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
llama3.2 (3B) is weak at tool calling; qwen3:8b is the common local pick for agents. Switch the README, docs, persona example and the setup wizard default to qwen3:8b, keeping llama3.2 as the low-memory note. The Ollama provider already sends a Bearer header when api_key or api_key_env resolves, so Ollama cloud works today. Document it (LICH_BASE_URL=https://ollama.com, LICH_API_KEY_ENV=OLLAMA_API_KEY) and replace "unused by ollama" with "optional for ollama". Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…press Optional `models` block names provider chains per role. `chat` sets the main loop's failover order; `compress` routes context compression and falls back to the chat chain on failure. ProviderRouter.for_role shares built clients. Without `models`, behavior is unchanged. The TUI labels the first chat-role provider. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
… fallback Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…hook
Plugin entries may be { path, settings?, models? } beside a bare path.
Hooks and plugin tools receive the frozen settings and models.chat(role,
...), which refuses roles the entry was not granted. A new
before_llm_call hook may return a note that is capped, sent as a
trailing system message on that one main-loop call, and never saved to
history; throwing hooks fail open.
Part of #149.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…, deep-freeze loader settings Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…gin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
A plugin-owned client for Ollama's local /v1/systemone endpoint asks one choice question per enemy action (and target) before each LLM turn. Shadow mode logs one JSONL line per decision; act mode queues orders through game_bridge's enemy_actions tool and meteor veto when every answer clears the threshold, else the LLM decides. Requests are checked against Ollama's limits before sending; failures fall back and are logged. bench.mjs replays saved snapshots for latency, coverage and agreement with the LLM. Part of #148. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…old, keep log under .lich/game Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…ed; no tools on the last turn - A tools_enabled list (top-level or gateway) now applies to plugin tools, including the gatekeeper's git_commit. The commander persona lists its three game_bridge tools explicitly. - A before_tool_call hook that throws blocks the call (blocked_by_plugin: hook_error) instead of allowing it. - Tool calls on the max_turns turn are not executed; they are closed with a turn_budget_exhausted result so history stays valid. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…pts persist them Also document the last-turn skip in docs/architecture/agent-loop.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…st-turn tool skip) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
- CLI one-shot and chat pass an abort signal to agent.run. Ctrl+C aborts the one-shot run (exit 1) or the running chat turn (session kept); at the chat prompt it ends chat. A second Ctrl+C while cancelling exits 130. - TUI: Esc cancels a running turn; an aborted run shows "run cancelled". - write_lich_config update writes a temp file and renames it into place, keeping the existing mode, so readers never see a partial config. - gatekeeper, tools, run_tests, skills_search and kanban_audit tests remove the dirs they create under test/.tmp. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…old reply On abort the loop's `final` is the last assistant so far (often the previous turn's reply) and `messages` ends with the cancelled user line. Chat and the TUI now show only the cancel notice and keep history_after_abort(messages), which drops trailing user lines. The config temp file is created with the existing mode. Tests cover a second Ctrl+C (exit 130) and the chat turn after a cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
reply_after_abort(outcome) returns `final` only when it comes after the last user line, i.e. this run wrote it. Chat prints it and the TUI shows it before the cancel notice; an earlier turn's reply is still skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Roadmap map lists #133 (P0), #134 (P1), #117 (P2), #144, #148 and #149 with their wiki pages; the kanban skill names the phase children. Supersedes the stale #135. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…fig (#117) Phase 0 + 1 of #117. Discovery is now project .lich/config.json merged over ~/.lich/config.json (shallow, project wins per key). A project providers array replaces the global one and drops the global models unless the project sets its own. The global layer ignores work_dir and session_dir, and its relative plugin paths resolve against ~/.lich/. ~/.config/lich/config.json is read only when ~/.lich/config.json is absent, with a hint to move it; nothing writes there. --config still replaces the whole chain. Bare lich no longer pins an existing config as --config when it skips the wizard, so the merge applies there too. first_run tests use an empty HOME instead of mocking the home lookup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…n is absent With work_dir = home the global file is the project file; the legacy ~/.config/lich file was then picked as the base under it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…117) lich init --global writes the starter config to ~/.lich/config.json (never overwrites; no work_dir/session_dir). The setup wizard ends with "Save as the global default?" (default no). Yes writes the answers to ~/.lich/config.json; discovered .lich/plugins entries stay in the project file since they are project paths. The wizard no longer pins its written file as --config, so discovery merges project over global. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…s home With work_dir = home the project file is ~/.lich/config.json, so the plugins-only project write hit "already exists" and dropped plugins. Test also asserts the merged TUI plugins. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…json from home Other projects load that file as the global layer and resolve relative plugin paths against ~/.lich, so .lich/plugins/x would become ~/.lich/.lich/plugins/x. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Phase 2 of #117. ~/.lich/profiles/<name>.json, plus an optional <name>.md used as the system prompt, merges between the global and the project config (global < profile < project). Selection: --profile, then LICH_PROFILE, then a project `profile` key, then the default in ~/.lich/config.json. Profile plugin paths resolve against ~/.lich/profiles; work_dir/session_dir in a profile are ignored. New `lich profile list|show|create|use`: create runs the wizard into the profile file (never overwrites), use sets `profile` in the global file. A requested profile skips the first-run wizard; --profile with --config is an error. File tools now refuse .lich/profiles/. tui/chat/serve/gateway no longer replace every config error with "no model configured"; only that case gets the hint. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…t_dir/disk_usage - work_dir = home: ~/.lich/config.json is the project file, so the profile now merges over it instead of under it. - "profile not found" / "already exists" errors carry no absolute path. - list_dir and disk_usage check the root with assert_file_tool_access, and list_dir skips denied entries, so .lich/profiles is not listed. - Docs: LICH_PROFILE is ignored with --config. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…children The home-dir branch now applies only when ~/.lich/config.json exists, so a legacy ~/.config/lich file stays the base. disk_usage leaves out entries file tools may not read (.lich/profiles, .lich/config.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
New entities/lich-config.md: global < profile < project merge, writers, file-tool guard, and config profile vs runtime Profile. Guardrails page and runtime-profile-session updated; index providers line fixed (#165). SCHEMA gains the `config` tag. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…awaited shutdown, webhook usage/502 (#144) Slice 1 of #144 (A1 plus the G-10 and webhook fixes): - GatewayBus queues conversations on SessionManager instead of its own promise chains (one queue implementation). - max_conversations evicts the least recently used conversation. - Only telegram's /start (/start, /start@bot, /start <payload>) becomes "hello"; /started and other platforms pass through. - Shutdown awaits adapter stop (bounded by 5 s) before exit. - bus.reply() returns { text, usage, failed }; the webhook returns the run's usage and 502 {"error"} on agent failure. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Concurrent new chats could each pass the pre-run eviction and all store, leaving the map over the cap. store_history now trims least recently used entries after each write. Adds a keep-alive stop regression test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Slice 2 of #144 (section A, items 2-3): - access.ts gains GatewayPolicy (allowlists + gateway toolset), built once from config; the runner builds it and hands it to the bus, which accepts an injected policy. - PlatformAdapter declares capabilities { kind: "text", max_reply_chars }. telegram 4096, discord 2000 (was a bare literal), twitch 450, webhook uncapped; idle adapters keep theirs. The runner logs them at start. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
There was a problem hiding this comment.
PR #178 — Risk: HIGH (gateway sender allowlists and gateway.tools_enabled now go through GatewayPolicy: create_gateway_policy wraps check_gateway_sender and gateway_tools_enabled, the runner copies policy.tools_enabled onto the agent, and the bus checks policy.allows. Defaults stay fail-closed. PlatformAdapter now requires capabilities, and create_idle_adapter takes that as a third argument.)
Findings: Critical 0 / Warning 0 / Suggestion 0
Regressions of settled findings: none
Not reviewed: none
Action: escalated to @Moikapy
Log: 2026-10-05T14:38:04Z COMMENT, label needs-review, not merged. Same allowlist and toolset functions as before (DEFAULT_GATEWAY_TOOLS_ENABLED unless gateway.tools_enabled is set; telegram/discord/twitch still default-deny; webhook still allowed). Reply caps match the declared capabilities (telegram 4096, discord 2000, twitch 450, webhook uncapped). CI green: typecheck_and_test (20), typecheck_and_test (22), ossuary, wiki_lint, AccessLint. Auto-merge was off. Head commit author is claude, not @Moikapy. Static review only.
Sent by Cursor Automation: PR REVIEW


Part of #144. This is the second #144 PR, covering section A's items 2 and 3 in the minimal scope agreed for this PR: a Policy object, plus a reply-length capability. There are no streaming or client-tool flags until something can use them.
Changes
GatewayPolicy(src/gateway/access.ts):create_gateway_policy(config)returns{ tools_enabled, allows(platform, chat_id, user_id) }. It wraps the existing allowlist and toolset rules, so behavior is unchanged.GatewayBustakes an optionalpolicy, defaulting to the one built from config, and checks senders through it. Per-chat profiles can extend this later.src/gateway/types.ts):PlatformAdapternow declarescapabilities: { kind: "text", max_reply_chars? }.2000in its send loop. The webhook is uncapped.create_idle_adaptertakes them as a third argument.docs/architecture/extending.mdupdated (thePlatformAdaptercontract, the policy, and the idle-adapter call); the CHANGELOG notes the breaking change for custom adapters.Breaking for custom adapters
PlatformAdapternow requirescapabilities, andcreate_idle_adapterneeds the new third argument. All built-in adapters are updated.Tests
create_gateway_policy: the default toolset, and allowlist decisions for webhook (always allowed) and Discord/Telegram (default deny unless listed).Checks
tsc --noEmit: clean.serve_transport"accepts ::1 clients" (no IPv6) andtools_review_shouldfixS-6 (process-group zombies).vitepress build docs: passes.🤖 Generated with Claude Code
https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Generated by Claude Code