kward 0.77.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +1 -0
  5. data/doc/code-search.md +9 -6
  6. data/doc/configuration.md +39 -0
  7. data/doc/permissions.md +179 -0
  8. data/doc/plugins.md +58 -0
  9. data/doc/rpc.md +61 -0
  10. data/doc/security.md +4 -0
  11. data/doc/tabs.md +4 -3
  12. data/doc/web-search.md +4 -2
  13. data/lib/kward/auth/anthropic_oauth.rb +2 -2
  14. data/lib/kward/auth/github_oauth.rb +3 -3
  15. data/lib/kward/auth/oauth_helpers.rb +4 -2
  16. data/lib/kward/auth/openai_oauth.rb +2 -1
  17. data/lib/kward/cli/plugins.rb +22 -0
  18. data/lib/kward/cli/rendering.rb +7 -1
  19. data/lib/kward/cli/runtime_helpers.rb +14 -0
  20. data/lib/kward/cli/tabs.rb +140 -34
  21. data/lib/kward/cli.rb +29 -9
  22. data/lib/kward/config_files.rb +10 -0
  23. data/lib/kward/hooks/http_handler.rb +2 -1
  24. data/lib/kward/http.rb +18 -0
  25. data/lib/kward/model/client.rb +33 -11
  26. data/lib/kward/model/payloads.rb +6 -1
  27. data/lib/kward/openrouter_model_cache.rb +2 -1
  28. data/lib/kward/permissions/policy.rb +171 -0
  29. data/lib/kward/plugin_registry.rb +53 -1
  30. data/lib/kward/prompt_interface/approval_prompt.rb +62 -0
  31. data/lib/kward/prompt_interface/question_prompt.rb +12 -3
  32. data/lib/kward/prompt_interface.rb +2 -0
  33. data/lib/kward/rpc/plugin_chat_manager.rb +299 -0
  34. data/lib/kward/rpc/server.rb +35 -0
  35. data/lib/kward/rpc/session_manager.rb +1 -0
  36. data/lib/kward/rpc/transcript_normalizer.rb +14 -5
  37. data/lib/kward/starter_pack_installer.rb +3 -1
  38. data/lib/kward/tab_driver.rb +87 -0
  39. data/lib/kward/tab_store.rb +74 -12
  40. data/lib/kward/tools/code_search.rb +8 -2
  41. data/lib/kward/tools/fetch_content.rb +4 -2
  42. data/lib/kward/tools/fetch_raw.rb +3 -1
  43. data/lib/kward/tools/registry.rb +37 -4
  44. data/lib/kward/tools/search/code.rb +46 -12
  45. data/lib/kward/tools/search/web.rb +60 -17
  46. data/lib/kward/tools/search/web_fetch.rb +206 -38
  47. data/lib/kward/tools/web_search.rb +3 -1
  48. data/lib/kward/update_check.rb +2 -1
  49. data/lib/kward/version.rb +1 -1
  50. data/templates/default/kward_navigation.rb +2 -1
  51. data/templates/default/layout/html/layout.erb +2 -0
  52. data/templates/default/layout/html/setup.rb +1 -0
  53. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a18a2a0c800780596999ee897ed61afcef22c964012f2b2ed3f51a3a66239c5
4
- data.tar.gz: 83badb23b87cae5e3198e00c65f0c53acbe2e0561f00164c4e5b40aacb3d10d4
3
+ metadata.gz: e77436e5f94e314fcab35e347c16994eb8f92f990ad4c3ebdc6ab76a393cb1b4
4
+ data.tar.gz: 50b73b48c76c737d5b7d2534380547d94e357cac6dec4528a80cfe36ea8f6bdf
5
5
  SHA512:
6
- metadata.gz: 5c4bd15eb61a2b62b5165b46d393fa60608dc702b773cbb0596bdd64deed1799332f22a5b271c146142f8f1f2769111e3e0075db34626818a71c4e502a14b49d
7
- data.tar.gz: 9953137b41415eff742904f3a4f4d63a610da01351000d5b08d4191ad180bfbdff5c001315513bbf4fabd1918843a6c214712f9af737c0025c5494e6c84494f8
6
+ metadata.gz: bc17d7227fa1910ee107b840ec138ee19c8ed905d137d7aeed3fc552e2ec6d3aca1fb3d8df4febce7c7e84380a7e4f4378c3177415b3ab7a59fb22d344b92d2c
7
+ data.tar.gz: 56521cdb264f5745f2023a010892b99ec91693f4444c17c0f1aa7b9375e9d0727ba7c9ce280d4e0af9141ae5466073393fbf9d112d53bf7ea12872aa68c7f617
data/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to Kward will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.78.0] - 2026-07-15
8
+
9
+ ### Added
10
+
11
+ - Added opt-in transcript-observer delivery for plugin-owned tabs, including RPC plugin chats, so local integrations such as text-to-speech can receive permitted streamed replies.
12
+ - Added opt-in RPC support for plugin-owned chats, including explicit subscriptions, transcript snapshots, attachment-capable turns, event replay, and cancellation. Plugins remain disabled from this surface unless their tab type explicitly opts in.
13
+ - Added plugin-owned tab types, opened with `/tab open <plugin-tab>`, with typed tab persistence and the normal interactive composer, transcript, streaming, and image-input behavior. Plugin tabs are CLI-only in this release and own their own storage rather than Kward sessions.
14
+ - Added an opt-in permission policy for model-requested tools, with allow/ask/deny rules, read-only and workspace-write modes, write scopes, and interactive approval overlays. Permissions remain disabled by default and are not an OS sandbox.
15
+ - Added a custom-response path to permission approval overlays: choosing `Type something` denies the requested tool call and returns the entered guidance to the agent.
16
+
17
+ ### Changed
18
+
19
+ - Plugin-chat transcript RPC supports optional bounded, cursor-based pages for plugin drivers that opt in, avoiding full-history payloads for long-lived chats.
20
+ - Code search now synchronizes mutable repository refs before every read and search, reports the resolved commit, and accepts GitHub blob URLs as file inputs.
21
+ - Web and code research tools now cooperate with turn cancellation between requests, provider attempts, redirects, and repository scan files.
22
+ - Kward-owned HTTP requests now identify themselves with `User-Agent: Kward/<version>`.
23
+
24
+ ### Fixed
25
+
26
+ - Fixed `fetch_content` site navigation by preferring semantic main content over nested article cards, preserving inline and standalone link destinations, listing bounded discovered navigation links, and extracting ordered lists and simple tables.
27
+ - Prevented duplicate session-backed tabs from restoring as mirrored conversations after restart.
28
+ - Fixed `fetch_content` on documentation pages with large scripts or metadata before the main content by separating bounded page downloads from extracted output limits; plain-text extraction now parses HTML instead of returning cleaned markup.
29
+ - Fixed raw fetches so their response limit is enforced while reading the network body.
30
+ - Fixed stale code-search reads and searches by treating cached repositories as reusable local storage rather than authoritative snapshots.
31
+ - Restored GPT-5.6 Luna requests through Codex with a scoped Responses Lite compatibility workaround; normal Codex requests retain Kward's own identity.
32
+
7
33
  ## [0.77.0] - 2026-07-11
8
34
 
9
35
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kward (0.77.0)
4
+ kward (0.78.0)
5
5
  base64
6
6
  nokogiri
7
7
  tiktoken_ruby
@@ -146,7 +146,7 @@ CHECKSUMS
146
146
  html-proofer (5.2.1) sha256=fdd958a7cbf9c3255fb96fe7cfc4e611f64e2706e469488a3326309ad007d2fd
147
147
  io-event (1.16.2) sha256=9f9cb0a96ea5c3850a672606c65f27bc96d7621399ef6196acbfe2be0cd1279c
148
148
  json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
149
- kward (0.77.0)
149
+ kward (0.78.0)
150
150
  logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
151
151
  metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072
152
152
  minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
data/README.md CHANGED
@@ -76,6 +76,7 @@ Start here:
76
76
  - [Configuration](doc/configuration.md): config files, providers, models, web search, logging, and color output.
77
77
  - [Authentication](doc/authentication.md): OpenAI OAuth, Anthropic OAuth, OpenRouter API keys, and Copilot/GitHub setup.
78
78
  - [Security and trust](doc/security.md): local permissions, external data flow, trusted extensions, and safe work in unfamiliar repositories.
79
+ - [Permissions](doc/permissions.md): opt-in tool approval, write scopes, policy rules, and current limits.
79
80
  - [Troubleshooting](doc/troubleshooting.md): environment-specific install and runtime issues.
80
81
 
81
82
  Feature guides:
data/doc/code-search.md CHANGED
@@ -85,9 +85,9 @@ The tool uses an `action` parameter:
85
85
  | --- | --- |
86
86
  | `package_search` | find package metadata and likely source repositories. |
87
87
  | `github_search` | search public GitHub repositories. |
88
- | `repo_clone` | clone a GitHub repository into cache. |
89
- | `repo_search` | search files in a cached repository. |
90
- | `repo_read` | read a bounded line range from a cached file. |
88
+ | `repo_clone` | prewarm the local Git repository store. |
89
+ | `repo_search` | synchronize and search files in a repository. |
90
+ | `repo_read` | synchronize and read a bounded line range from a file. |
91
91
  | `list_cache` | show cached repositories. |
92
92
  | `refresh_cache` | fetch updates for a cached repository. |
93
93
  | `clear_cache` | remove a cached repository. |
@@ -99,13 +99,16 @@ The tool uses an `action` parameter:
99
99
  | `package_search` | `ecosystem`, `package` | — |
100
100
  | `github_search` | `query` | `ecosystem`, `max_results` |
101
101
  | `repo_clone` | `repo` | — |
102
- | `repo_search` | `repo`, `query` | `max_results`, `context_lines` |
103
- | `repo_read` | `repo`, `path` | `start_line`, `line_count` |
102
+ | `repo_search` | `repo`, `query` | `ref`, `max_results`, `context_lines` |
103
+ | `repo_read` | `repo` | `path`, `ref`, `start_line`, `line_count` |
104
104
  | `list_cache` | — | — |
105
105
  | `refresh_cache` | `repo` | — |
106
106
  | `clear_cache` | `repo` | — |
107
107
 
108
- - `repo` is a GitHub URL or `owner/name`.
108
+ - `repo` is a GitHub URL or `owner/name`. A GitHub `blob` URL can supply the file path and ref for `repo_read`.
109
+ - `ref` selects a branch, tag, or commit. Mutable refs are fetched before every read or search; without `ref`, Kward synchronizes the remote default branch.
110
+ - Successful reads and searches report the exact commit revision used. The local Git store reduces repeated downloads but is never silently treated as current source.
111
+ - If synchronization fails, Kward returns an error instead of silently answering from stale files.
109
112
  - `query` is a plain-text search string (not regex). Matching is case-sensitive substring.
110
113
  - `start_line` is 1-indexed.
111
114
  - `context_lines` controls how many lines before and after a match are included in search snippets.
data/doc/configuration.md CHANGED
@@ -184,6 +184,8 @@ Model settings:
184
184
 
185
185
  Set `codex_show_raw_reasoning` to `true` to display raw Codex `reasoning_text` when the API does not provide reasoning summary text. It defaults to `false`; raw reasoning can include internal or unstable model output, so enable it only when you explicitly want to inspect that stream.
186
186
 
187
+ `stream_idle_timeout_seconds` limits how long a streamed Codex or Anthropic response may go without receiving data. It defaults to `120`; set a positive value to override it. When the provider is silent longer than this limit, Kward closes the request and applies its normal transient-network retry behavior.
188
+
187
189
  Defaults:
188
190
 
189
191
  - OpenAI/Codex: `gpt-5.6-sol`
@@ -528,6 +530,43 @@ Workspace guardrails are enabled by default. File tools such as `read_file`, `wr
528
530
 
529
531
  This is not a sandbox setting. Shell commands already run as your OS user from the workspace directory and can access anything that user can access.
530
532
 
533
+ ## Permissions
534
+
535
+ Permissions are off by default. When enabled, Kward evaluates model-requested tools before execution. This is a permission policy, not an operating-system sandbox: permitted shell commands still run with your user account's access.
536
+
537
+ ```json
538
+ {
539
+ "permissions": {
540
+ "enabled": true,
541
+ "mode": "ask"
542
+ }
543
+ }
544
+ ```
545
+
546
+ Available modes are:
547
+
548
+ | Mode | Behavior |
549
+ | --- | --- |
550
+ | `ask` | Read-only tools run normally; file changes, shell commands, web tools, and MCP tools need approval. |
551
+ | `workspace-write` | File changes within `write_scopes` run without approval; shell and network tools still need approval. |
552
+ | `read-only` | Denies file changes, shell commands, web tools, and MCP tools. |
553
+ | `deny-by-default` | Denies risky tools unless an `allow` rule matches. |
554
+
555
+ `allow`, `ask`, and `deny` rules are arrays of objects matching `tool`, `path`, `host`, `command`, or `source`. Deny rules always take precedence, then ask, then allow. Use `write_scopes` to restrict writes in `workspace-write` mode:
556
+
557
+ ```json
558
+ {
559
+ "permissions": {
560
+ "enabled": true,
561
+ "mode": "workspace-write",
562
+ "write_scopes": ["lib/**", "test/**"],
563
+ "deny": [{ "tool": "run_shell_command", "command": "git push*" }]
564
+ }
565
+ }
566
+ ```
567
+
568
+ The interactive CLI presents an approval overlay for an `ask` decision. RPC clients can use their existing `approvalMode: "ask"` bridge. Frontends without an approval bridge, including Pan, fail closed for policy approvals.
569
+
531
570
  ## Logging and stats
532
571
 
533
572
  Local telemetry logs are off by default. Enable logging with the master flag and each category you want:
@@ -0,0 +1,179 @@
1
+ # Permissions
2
+
3
+ Kward's permission policy lets you put a human decision between a model-requested tool call and its side effect. It is useful when you want the agent to inspect a project freely, but want to confirm edits, commands, web requests, or MCP calls as they arise.
4
+
5
+ Permissions are **off by default**. Enabling them does not sandbox Kward or its subprocesses: an allowed shell command still runs with your operating-system user permissions.
6
+
7
+ ## Start with approval mode
8
+
9
+ Add this to `~/.kward/config.json`, or enable it through your usual configuration workflow:
10
+
11
+ ```json
12
+ {
13
+ "permissions": {
14
+ "enabled": true,
15
+ "mode": "ask"
16
+ }
17
+ }
18
+ ```
19
+
20
+ In `ask` mode, Kward allows ordinary read-only tools and asks before the agent:
21
+
22
+ - writes or edits a workspace file,
23
+ - runs `run_shell_command`,
24
+ - searches or fetches content on the web,
25
+ - calls an MCP tool.
26
+
27
+ When approval is required in the interactive CLI, Kward opens a modal overlay. It shows the complete tool arguments, so an approval for `read_skill` includes the requested skill name and relative path, and an approval for a write includes its path and content. Choose **Allow once** to run that one call, **Allow this tool for this session** to allow later calls to the same tool in the current Kward session, or **Deny** to stop it. Pressing `Esc`, cancelling the overlay, or losing the approval bridge denies the call.
28
+
29
+ ### Steer the agent with `Type something`
30
+
31
+ **Type something** is more than a denial. It stops the requested tool call and sends your text back to the agent as the tool result, so you can redirect its workflow without waiting for it to finish a wrong turn.
32
+
33
+ For example, the agent may propose this command:
34
+
35
+ ```text
36
+ run_shell_command: grep -R "Permission" lib test
37
+ ```
38
+
39
+ Choose **Type something** and enter:
40
+
41
+ ```text
42
+ Use ack instead of grep; it is installed here and respects our ignore files.
43
+ ```
44
+
45
+ Kward does not run `grep`. The agent receives:
46
+
47
+ ```text
48
+ Declined: Use ack instead of grep; it is installed here and respects our ignore files.
49
+ ```
50
+
51
+ It can then continue by choosing an `ack` command. This is useful for steering details that are difficult to encode in a static rule, such as:
52
+
53
+ - use the project’s preferred formatter or test runner;
54
+ - run a focused test rather than the full suite;
55
+ - use a local mirror or a documented API endpoint;
56
+ - explain a proposed database command before running it;
57
+ - choose an existing project script instead of inventing a new command.
58
+
59
+ The typed response is part of the conversation, just like a normal follow-up message. It is stored in the session and may be sent to the active model provider. Do not put secrets in it unless that is acceptable for your selected provider and retained session data.
60
+
61
+ Use this mode first when working in an unfamiliar repository or when you want to review an agent's actions without preventing ordinary exploration.
62
+
63
+ ## Choose a mode
64
+
65
+ Set `permissions.mode` to one of these values:
66
+
67
+ | Mode | Good for | Default behavior |
68
+ | --- | --- | --- |
69
+ | `ask` | Interactive supervised work | Asks before file changes, shell commands, web tools, and MCP tools. |
70
+ | `workspace-write` | Routine edits in selected paths | Allows file changes in `write_scopes`; still asks before shell, web, and MCP tools. |
71
+ | `read-only` | Code review and investigation | Denies risky tools by default. |
72
+ | `deny-by-default` | Automation or tightly controlled runs | Denies risky tools unless an `allow` rule matches. |
73
+
74
+ All modes leave ordinary read-only Kward tools available. Rules can make a decision more specific; see [Rules and precedence](#Rules_and_precedence).
75
+
76
+ ## Let Kward edit only selected paths
77
+
78
+ Use `workspace-write` when you want routine changes in known source and test directories without an approval prompt for each edit:
79
+
80
+ ```json
81
+ {
82
+ "permissions": {
83
+ "enabled": true,
84
+ "mode": "workspace-write",
85
+ "write_scopes": ["lib/**", "test/**"]
86
+ }
87
+ }
88
+ ```
89
+
90
+ With this configuration, the agent may edit `lib/` and `test/` without asking. A proposed edit to `README.md` or `config/` is denied by the policy.
91
+
92
+ `write_scopes` narrows the policy's default workspace-write behavior; it does not broaden Kward's built-in workspace boundary. File tools still stay inside the active workspace by default, and existing files must still be read before Kward can edit them. See [Workspace tools](workspace-tools.md) for those guardrails.
93
+
94
+ An empty `write_scopes` array permits no default workspace writes. Omit `write_scopes` if you want the `workspace-write` mode to cover the normal workspace file-tool boundary.
95
+
96
+ ## Rules and precedence
97
+
98
+ Use `allow`, `ask`, and `deny` arrays to describe exceptions. A rule can match these fields:
99
+
100
+ - `tool` — a model tool name, such as `run_shell_command` or `write_file`;
101
+ - `path` — the file-tool path supplied by the model;
102
+ - `command` — the requested shell command text;
103
+ - `host` — the host in a `fetch_content` or `fetch_raw` URL;
104
+ - `source` — currently useful for `mcp` tools.
105
+
106
+ Patterns support `*` within a path segment and `**` across directories. Rule matching is case-sensitive.
107
+
108
+ ```json
109
+ {
110
+ "permissions": {
111
+ "enabled": true,
112
+ "mode": "ask",
113
+ "allow": [
114
+ { "tool": "write_file", "path": "doc/**" },
115
+ { "tool": "fetch_content", "host": "docs.ruby-lang.org" }
116
+ ],
117
+ "ask": [
118
+ { "tool": "run_shell_command", "command": "bundle exec *" }
119
+ ],
120
+ "deny": [
121
+ { "tool": "run_shell_command", "command": "git push*" },
122
+ { "tool": "write_file", "path": "**/.env" }
123
+ ]
124
+ }
125
+ }
126
+ ```
127
+
128
+ Kward evaluates matching rules in this order:
129
+
130
+ 1. `deny`
131
+ 2. `ask`
132
+ 3. `allow`
133
+ 4. the selected mode's default
134
+
135
+ A matching deny always wins. An ask rule wins over an allow rule. Keep rules narrow and easy to explain; prefer an exact command or a small path scope over a broad wildcard.
136
+
137
+ ## A practical workflow
138
+
139
+ For a normal coding task:
140
+
141
+ 1. Enable `ask` mode.
142
+ 2. Ask Kward to inspect the project and propose a change.
143
+ 3. Approve the file edit after checking its path and intent.
144
+ 4. Approve a focused test command, or choose **Type something** to redirect it—for example, “run `bin/test unit` instead of the full suite.”
145
+ 5. Review the diff and `git status` as usual.
146
+
147
+ If you repeatedly work in one source tree, move to `workspace-write` with narrow write scopes. Keep shell and network calls in approval mode unless you have a well-understood policy for them.
148
+
149
+ ## RPC and Pan
150
+
151
+ RPC clients can continue to use the existing per-turn approval bridge:
152
+
153
+ ```json
154
+ {
155
+ "approvalMode": "ask"
156
+ }
157
+ ```
158
+
159
+ That requests an approval notification for every tool call in that turn. The global permission policy is an additional restriction: a deny rule prevents the call even if the RPC client would approve it. If the global policy requires approval, a frontend without an approval bridge fails closed.
160
+
161
+ Pan currently has no dedicated approval interface. With permissions enabled, a Pan session cannot approve a policy `ask` decision, so Kward denies it. Use `read-only`, explicit allows, or a CLI/RPC frontend when approvals are needed.
162
+
163
+ ## Limits: policy is not a sandbox
164
+
165
+ The policy runs before Kward dispatches a model-requested tool. It does not constrain what happens after a shell command starts. In particular:
166
+
167
+ - a permitted shell command can access files, processes, credentials, and network services available to your user account;
168
+ - command-text rules are useful review controls, not a reliable way to enforce network destinations or all subprocess behavior;
169
+ - direct commands that you type yourself—`!command`, `/shell`, and `/pty`—are treated as your actions and are outside this first policy scope;
170
+ - plugins, hooks, and MCP servers are trusted local extensions with their own process access.
171
+
172
+ For sensitive work, use a restricted operating-system account, container, virtual machine, or disposable checkout. A future OS-enforced sandbox may provide stronger filesystem and network isolation; it is not part of the current permissions feature.
173
+
174
+ ## Related guides
175
+
176
+ - [Security and trust](security.md): trust boundaries, extensions, and safe work in unfamiliar repositories.
177
+ - [Configuration](configuration.md#Permissions): the complete configuration reference.
178
+ - [Lifecycle hooks](lifecycle-hooks.md): add trusted local policy or automation around Kward events.
179
+ - [RPC protocol](rpc.md#Tool_approval_bridge): implement the approval bridge in an RPC client.
data/doc/plugins.md CHANGED
@@ -178,6 +178,64 @@ Returning `:exit` from the tick callback ends the loop, same as calling
178
178
  Interactive commands require the TUI prompt interface. They are not available
179
179
  in piped/non-interactive mode or through RPC.
180
180
 
181
+ ## Add a plugin-owned tab
182
+
183
+ A plugin can provide a persistent tab with Kward's normal composer, transcript
184
+ rendering, streaming, image input, cancellation, and tab switching. The plugin
185
+ owns its transcript, storage, model behavior, and any global state; it does not
186
+ need to use a Kward workspace session.
187
+
188
+ ```ruby
189
+ Kward.plugin do |plugin|
190
+ plugin.tab_type "example", id: "com.example.chat", title: "Example", singleton: :global do |host, descriptor|
191
+ ExampleChat.new(client: host.client, descriptor: descriptor)
192
+ end
193
+ end
194
+ ```
195
+
196
+ Open it from interactive Kward:
197
+
198
+ ```text
199
+ /tab open example
200
+ ```
201
+
202
+ `id` is a stable persisted identifier: do not change it after release.
203
+ Use `singleton: :global` for one plugin-managed chat shared by all tab views.
204
+
205
+ Plugin tabs do not notify global transcript observers by default. Set
206
+ `transcript_events: true` only when the tab explicitly permits its streamed
207
+ content to be delivered to every installed `on_transcript_event` handler, such
208
+ as a local text-to-speech plugin:
209
+
210
+ ```ruby
211
+ plugin.tab_type "example", id: "com.example.chat", transcript_events: true do |host, descriptor|
212
+ ExampleChat.new(client: host.client, descriptor: descriptor)
213
+ end
214
+ ```
215
+
216
+ The observer context exposes the plugin tab's `messages` through
217
+ `ctx.transcript.messages`; it has no workspace session. The tab driver returned by the block must provide:
218
+
219
+ - `messages` — renderable transcript messages;
220
+ - `submit(input, display_input:, cancellation:, steering:)` — a turn method
221
+ that returns the final response and yields stream events;
222
+ - `descriptor` — the durable tab descriptor;
223
+ - `supports_steering?` and `assistant_label`.
224
+
225
+ A driver may optionally implement `handles_command?(input)` and
226
+ `handle_command(input)` for its own slash commands. Other Kward session and
227
+ workspace commands stay unavailable inside plugin tabs.
228
+
229
+ Set `rpc: true` when the plugin tab can also be exposed as a trusted local RPC chat:
230
+
231
+ ```ruby
232
+ plugin.tab_type "example", id: "com.example.chat", rpc: true do |host, descriptor|
233
+ ExampleChat.new(client: host.client, descriptor: descriptor)
234
+ end
235
+ ```
236
+
237
+ RPC clients discover opted-in types through `initialize.capabilities.pluginChats`, open a chat with `pluginChats/open`, and must explicitly subscribe before receiving live `pluginChat/event` notifications. See [RPC](rpc.md) for the protocol. Plugin tabs remain CLI-only unless they opt in.
238
+
181
239
  ## Observe transcript events
182
240
 
183
241
  Use transcript events when you need to log or react to live activity:
data/doc/rpc.md CHANGED
@@ -52,6 +52,7 @@ Detailed capability fields include:
52
52
  - `transcript`: Kward transcript format support, including normalized messages, image/tool support, compaction summaries, and restored assistant reasoning as Pi-compatible `thinking` content blocks.
53
53
  - `sessions`: explicit RPC session mode, JSONL persistence, supported session methods, startup auto-resume capability/default, immediate transcript support for auto-resume, RPC list support, active live-session discovery, supported linear-session fork methods, supported compaction, supported tree navigation with labels and branch summarization, explicit unsupported import support, and unsupported live session updates reported with `notification: "session/updated"`.
54
54
  - `turns`: async turn mode, per-session concurrency, active/recent turn listing, provider-gated native busy-input steering, queued follow-up input, best-effort cancellation, recent in-memory event replay behavior, per-turn options for model/reasoning/tool scope/tool approval, and structured client context for editor integrations.
55
+ - `pluginChats`: optional plugin-owned chats. The capability lists opted-in chat types and methods. Clients must explicitly subscribe before receiving `pluginChat/event` notifications; plugin chats are independent from workspace sessions.
55
56
  - `events`: `turn/event` notification details, assistant/reasoning event names, normalized tool metadata, tool update/result events, diff result support, configured workspace guardrail status, focused context and context-budget stats tool support, and explicit unsupported shell changed-file detection/session update flags.
56
57
  - `attachments`: supported input attachment contract for `turns/start`, with accepted base64 image MIME types and a stable max byte value.
57
58
  - `models`: model/reasoning RPC methods, explicit OpenRouter catalog listing, exposed model fields, and no scoped model support.
@@ -276,6 +277,66 @@ Moves the active branch to the selected tree entry. If the entry is a user messa
276
277
 
277
278
  Returns `{ "session": {}, "editorText": "...", "cancelled": false, "aborted": false }`. Fields with no value are omitted.
278
279
 
280
+ ## Plugin chat methods
281
+
282
+ Plugin chats are optional trusted-plugin capabilities, not Kward workspace sessions. When `initialize.capabilities.pluginChats.supported` is true, use `pluginChats/list` to discover available types.
283
+
284
+ ### `pluginChats/open`
285
+
286
+ Params:
287
+
288
+ - `typeId`: an opted-in plugin chat type, such as `kward.maria`.
289
+
290
+ Opens the plugin-owned chat in this RPC process and returns its metadata plus normalized transcript.
291
+
292
+ ### `pluginChats/transcript`
293
+
294
+ Params:
295
+
296
+ - `chatId`;
297
+ - `limit`: optional bounded page size for plugin drivers that support transcript paging;
298
+ - `before`: optional opaque cursor returned by a preceding page.
299
+
300
+ Returns chat metadata and its normalized transcript. Paging-capable plugin chats return the newest page first plus `hasMore` and, when more history is available, `nextBefore`. Clients send that cursor as `before` to load the preceding page. Plugins that do not opt in retain the original complete-transcript behavior.
301
+
302
+ ### `pluginChats/subscribe` and `pluginChats/unsubscribe`
303
+
304
+ Params:
305
+
306
+ - `chatId`.
307
+
308
+ Subscriptions are opt-in. `pluginChats/subscribe` enables live `pluginChat/event` notifications for that chat on the current RPC connection. `pluginChats/unsubscribe` disables them without affecting the chat or archive.
309
+
310
+ ### `pluginChats/turns/start`
311
+
312
+ Params:
313
+
314
+ - `chatId`;
315
+ - `input`;
316
+ - `attachments`: optional base64 image attachments using the same MIME and size limits as `turns/start`.
317
+
318
+ Queues a plugin-chat turn and returns `{ id, chatId, status, ... }`. Plugin chat turns are serialized per chat and do not use workspace sessions, agents, or model overrides.
319
+
320
+ ### `pluginChats/turns/cancel`, `pluginChats/turns/status`, `pluginChats/turns/events`, `pluginChats/turns/list`, `pluginChats/turns/listActive`
321
+
322
+ These methods mirror the corresponding `turns/*` lifecycle methods, using `chatId` where a list filter is needed. Event replay is bounded in memory and subscriptions are required for live notifications.
323
+
324
+ ## Plugin chat notifications
325
+
326
+ Subscribed clients receive `pluginChat/event` notifications:
327
+
328
+ ```json
329
+ {
330
+ "chatId": "kward.maria",
331
+ "turnId": "...",
332
+ "sequence": 1,
333
+ "type": "assistantDelta",
334
+ "payload": { "delta": "text" }
335
+ }
336
+ ```
337
+
338
+ Known event types are `turnQueued`, `turnStarted`, `reasoningDelta`, `reasoningBoundary`, `assistantDelta`, `assistantMessage`, `toolCall`, `toolResult`, `answer`, `turnCancelRequested`, and `turnFinished`.
339
+
279
340
  ## Turn methods
280
341
 
281
342
  Turns are asynchronous. A session queues turns sequentially; only one turn runs per session at a time.
data/doc/security.md CHANGED
@@ -69,6 +69,10 @@ Pan mode does not provide a dedicated hook-approval UI. Use `deny` or `warn` for
69
69
 
70
70
  See [Lifecycle hooks](lifecycle-hooks.md) for policy examples and [RPC protocol](rpc.md#Tool_approval_bridge) for the approval contract.
71
71
 
72
+ ## Opt-in permission policy
73
+
74
+ `permissions.enabled` is off by default. When enabled, Kward can allow, ask, or deny model-requested file changes, shell commands, web tools, and MCP tools before execution. The interactive CLI uses an approval overlay; an unavailable approval bridge fails closed. The policy is not a sandbox: after Kward permits a shell command, it still runs with the permissions of your user account. See [Permissions](permissions.md) for workflows, modes, rules, and limits.
75
+
72
76
  ## Know what you are trusting
73
77
 
74
78
  ### Workspace instructions
data/doc/tabs.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use tabs when you want more than one Kward conversation open in the same interactive terminal session.
4
4
 
5
- Each tab has its own session-backed conversation, transcript, composer state, and running agent turn. That makes tabs useful for splitting work without losing your place: one tab can investigate a bug, another can draft docs, and a third can wait on a long-running answer.
5
+ Normal tabs have their own session-backed conversation, transcript, composer state, and running agent turn. Plugins can also provide their own persistent tab types while Kward continues to supply the same composer and tab behavior. This makes tabs useful for splitting work without losing your place: one tab can investigate a bug, another can draft docs, and a third can wait on a long-running answer.
6
6
 
7
7
  ## Quick start
8
8
 
@@ -56,6 +56,7 @@ Tabs keep the conversations separate, so context from one tab does not automatic
56
56
  | Command | Action |
57
57
  | ------- | ------ |
58
58
  | `/tab new` | Open a new tab with a fresh conversation |
59
+ | `/tab open <plugin-tab>` | Open a plugin-provided tab, such as a persistent companion chat. |
59
60
  | `/tab 1` through `/tab 9` | Switch to a numbered tab |
60
61
  | `/tab close` | Close the current tab |
61
62
  | `/tab name <label>` | Rename the current tab |
@@ -110,9 +111,9 @@ Slash commands such as `/tab` still work while a tab is busy, so you can switch
110
111
 
111
112
  ## Persistence
112
113
 
113
- Tabs are backed by normal Kward sessions. Kward stores the open tab list, labels, and active tab per workspace, then restores them the next time you start interactive Kward in that workspace.
114
+ Normal tabs are backed by Kward sessions. Plugins may also provide their own tab types; those tabs own their transcript and persistence independently of Kward sessions.
114
115
 
115
- The tab layout is stored in Kward's config directory under `tabs/`. Session transcripts stay in the normal sessions directory. See [Sessions](session-management.md) for session naming, resuming, forking, exporting, and cleanup.
116
+ Kward stores the open tab list, labels, and active tab per workspace, then restores them the next time you start interactive Kward in that workspace. The tab layout is stored in Kward's config directory under `tabs/`. Session transcripts stay in the normal sessions directory. See [Sessions](session-management.md) for session naming, resuming, forking, exporting, and cleanup.
116
117
 
117
118
  ## Notes and limitations
118
119
 
data/doc/web-search.md CHANGED
@@ -85,7 +85,9 @@ Arguments:
85
85
  - `max_bytes`: default 16384, capped at 131072.
86
86
  - `extract`: optional `auto`, `text`, or `markdown`.
87
87
 
88
- In `auto` mode (default), Kward detects HTML content and extracts readable text by stripping scripts, styles, navigation, and forms, preserving headings, paragraphs, lists, code blocks, and blockquotes. Non-HTML content is returned as cleaned text. Use `markdown` to format extracted headings and code blocks as Markdown, or `text` for plain text without formatting.
88
+ In `auto` mode (default), Kward detects HTML content and extracts readable text while stripping scripts, styles, navigation prose, and forms. It preserves headings, paragraphs, ordered and unordered lists, code blocks, blockquotes, simple tables, and inline link destinations. A bounded `Links` section lists discovered main-content and navigation destinations so an agent can continue through a site without raw HTML. Relative links are resolved against the final fetched URL. Non-HTML content is returned as cleaned text. Use `markdown` to retain Markdown structure, or `text` for plain text with link destinations in parentheses.
89
+
90
+ Kward downloads and parses up to 2 MB of a page before applying `max_bytes` to the extracted result. This lets pages with large scripts or metadata before their main content remain readable while keeping model-facing output bounded. Responses above the download limit are rejected rather than parsed as incomplete HTML.
89
91
 
90
92
  Fetches follow up to 5 redirects and use a 10-second HTTP timeout.
91
93
 
@@ -99,4 +101,4 @@ Arguments:
99
101
  - `max_bytes`: default 16384, capped at 131072.
100
102
  - `accept`: optional HTTP `Accept` header.
101
103
 
102
- Fetches follow up to 5 redirects and use a 10-second HTTP timeout.
104
+ Fetches follow up to 5 redirects, enforce the requested byte limit while reading the response, and use a 10-second HTTP timeout.
@@ -202,8 +202,8 @@ module Kward
202
202
  parse_successful_json(response, "Anthropic OAuth token exchange")
203
203
  end
204
204
 
205
- def post_json(uri, params)
206
- super(uri, params, headers: { "Accept" => "application/json" })
205
+ def post_json(uri, params = nil, **keyword_params)
206
+ super(uri, params, headers: { "Accept" => "application/json" }, **keyword_params)
207
207
  end
208
208
  end
209
209
  end
@@ -4,6 +4,7 @@ require "time"
4
4
  require "uri"
5
5
  require_relative "file"
6
6
  require_relative "../config_files"
7
+ require_relative "../http"
7
8
 
8
9
  # Namespace for the Kward CLI agent runtime.
9
10
  module Kward
@@ -15,7 +16,6 @@ module Kward
15
16
  DEFAULT_SCOPE = "read:user"
16
17
  DEFAULT_CLIENT_ID = "Iv1.b507a08c87ecfe98"
17
18
  COPILOT_HEADERS = {
18
- "User-Agent" => "GitHubCopilotChat/0.35.0",
19
19
  "Editor-Version" => "vscode/1.107.0",
20
20
  "Editor-Plugin-Version" => "copilot-chat/0.35.0",
21
21
  "Copilot-Integration-Id" => "vscode-chat"
@@ -150,7 +150,7 @@ module Kward
150
150
  end
151
151
 
152
152
  def post_form(uri, params)
153
- request = Net::HTTP::Post.new(uri)
153
+ request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
154
154
  request["Content-Type"] = "application/x-www-form-urlencoded"
155
155
  request["Accept"] = "application/json"
156
156
  request.body = URI.encode_www_form(params)
@@ -160,7 +160,7 @@ module Kward
160
160
  end
161
161
 
162
162
  def get_json(uri, headers = {})
163
- request = Net::HTTP::Get.new(uri)
163
+ request = Http.apply_user_agent(Net::HTTP::Get.new(uri))
164
164
  request["Accept"] = "application/json"
165
165
  COPILOT_HEADERS.merge(headers).each { |key, value| request[key] = value }
166
166
 
@@ -6,6 +6,7 @@ require "securerandom"
6
6
  require "socket"
7
7
  require "time"
8
8
  require "uri"
9
+ require_relative "../http"
9
10
 
10
11
  # Namespace for the Kward CLI agent runtime.
11
12
  module Kward
@@ -58,8 +59,9 @@ module Kward
58
59
  socket&.close
59
60
  end
60
61
 
61
- def post_json(uri, params, headers: {})
62
- request = Net::HTTP::Post.new(uri)
62
+ def post_json(uri, params = nil, headers: {}, **keyword_params)
63
+ params = (params || {}).merge(keyword_params)
64
+ request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
63
65
  request["Content-Type"] = "application/json"
64
66
  headers.each { |key, value| request[key] = value }
65
67
  request.body = JSON.dump(params)
@@ -5,6 +5,7 @@ require "time"
5
5
  require "uri"
6
6
  require_relative "file"
7
7
  require_relative "oauth_helpers"
8
+ require_relative "../http"
8
9
 
9
10
  # Namespace for the Kward CLI agent runtime.
10
11
  module Kward
@@ -215,7 +216,7 @@ module Kward
215
216
  end
216
217
 
217
218
  def post_form(uri, params)
218
- request = Net::HTTP::Post.new(uri)
219
+ request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
219
220
  request["Content-Type"] = "application/x-www-form-urlencoded"
220
221
  request.body = URI.encode_www_form(params)
221
222