openclacky 1.5.6 → 1.5.8
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.
- checksums.yaml +4 -4
- data/.clacky/skills/gem-release/SKILL.md +18 -1
- data/.clackyrules +25 -1
- data/CHANGELOG.md +59 -0
- data/CONTRIBUTING.md +9 -0
- data/README.md +2 -2
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/Rakefile +15 -0
- data/docs/design-emit-and-background-skills.md +253 -0
- data/docs/mcp-architecture.md +48 -9
- data/docs/mcp.example.json +9 -0
- data/docs/superpowers/plans/2026-08-09-chatcut-extension-bridge.md +191 -0
- data/docs/superpowers/plans/2026-08-09-native-mcp-oauth.md +243 -0
- data/docs/superpowers/specs/2026-08-09-mcp-oauth-extension-fallback-design.md +115 -0
- data/lib/clacky/agent/cost_tracker.rb +17 -2
- data/lib/clacky/agent/hook_manager.rb +14 -3
- data/lib/clacky/agent/memory_updater.rb +17 -18
- data/lib/clacky/agent/message_compressor_helper.rb +23 -0
- data/lib/clacky/agent/session_serializer.rb +136 -22
- data/lib/clacky/agent/skill_auto_creator.rb +1 -2
- data/lib/clacky/agent/skill_evolution.rb +11 -7
- data/lib/clacky/agent/skill_manager.rb +20 -18
- data/lib/clacky/agent/skill_reflector.rb +3 -6
- data/lib/clacky/agent.rb +113 -23
- data/lib/clacky/agent_config.rb +40 -18
- data/lib/clacky/anthropic_stream_aggregator.rb +6 -1
- data/lib/clacky/cli.rb +4 -1
- data/lib/clacky/client.rb +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +23 -10
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +16 -7
- data/lib/clacky/default_extensions/general/ext.yml +1 -1
- data/lib/clacky/extension/api_extension.rb +11 -3
- data/lib/clacky/extension/api_loader.rb +4 -11
- data/lib/clacky/extension/packager.rb +1 -1
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/fanout.rb +100 -0
- data/lib/clacky/locales/en.rb +2 -0
- data/lib/clacky/locales/zh.rb +2 -0
- data/lib/clacky/mcp/cli_commands.rb +61 -0
- data/lib/clacky/mcp/client.rb +18 -2
- data/lib/clacky/mcp/connection_manager.rb +85 -0
- data/lib/clacky/mcp/http_transport.rb +25 -9
- data/lib/clacky/mcp/oauth/authorization_manager.rb +257 -0
- data/lib/clacky/mcp/oauth/config.rb +51 -0
- data/lib/clacky/mcp/oauth/credential_store.rb +82 -0
- data/lib/clacky/mcp/oauth/session.rb +88 -0
- data/lib/clacky/media/generator.rb +3 -3
- data/lib/clacky/message_format/anthropic.rb +27 -13
- data/lib/clacky/message_history.rb +33 -2
- data/lib/clacky/providers.rb +13 -6
- data/lib/clacky/server/http_server.rb +102 -98
- data/lib/clacky/server/session_registry.rb +11 -15
- data/lib/clacky/server/web_ui_controller.rb +1 -1
- data/lib/clacky/session_manager.rb +21 -12
- data/lib/clacky/shell_hook_loader.rb +14 -8
- data/lib/clacky/tools/invoke_skill.rb +3 -2
- data/lib/clacky/tools/terminal.rb +11 -4
- data/lib/clacky/ui2/components/inline_input.rb +1 -0
- data/lib/clacky/ui2/components/input_area.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +0 -1
- data/lib/clacky/ui2/components/todo_area.rb +1 -0
- data/lib/clacky/ui2/line_editor.rb +1 -0
- data/lib/clacky/ui2/markdown_renderer.rb +5 -1
- data/lib/clacky/ui2/progress_handle.rb +27 -3
- data/lib/clacky/ui2/ui_controller.rb +207 -6
- data/lib/clacky/ui_interface.rb +13 -3
- data/lib/clacky/utils/arguments_parser.rb +73 -4
- data/lib/clacky/utils/model_pricing.rb +21 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +144 -213
- data/lib/clacky/web/components/chat-navigator.js +1 -1
- data/lib/clacky/web/features/billing/store.js +3 -3
- data/lib/clacky/web/features/billing/view.js +5 -61
- data/lib/clacky/web/features/extensions/store.js +1 -1
- data/lib/clacky/web/features/extensions/view.js +28 -5
- data/lib/clacky/web/features/new-session/view.js +43 -2
- data/lib/clacky/web/i18n.js +38 -22
- data/lib/clacky/web/index.html +19 -24
- data/lib/clacky/web/projects.js +96 -279
- data/lib/clacky/web/sessions.js +169 -119
- data/lib/clacky/web/settings.js +7 -1
- data/lib/clacky/web/ws-dispatcher.js +61 -34
- data/lib/clacky.rb +2 -0
- metadata +12 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# ChatCut Extension Bridge Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use box:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Build a standalone ChatCut extension that works on existing OpenClacky clients through an OAuth-aware stdio bridge and automatically switches to native OAuth when available.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A setup command owns conflict-safe MCP configuration, capability detection, and cleanup. A focused Ruby bridge composes OAuth discovery/session classes with an MCP HTTP forwarder while keeping stdout protocol-only. Extension skills call the setup command and delegate all video work to `mcp:chatcut` and ChatCut's remote playbooks.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby 2.6+, Net::HTTP, WEBrick, JSON, minitest, OpenClacky `ext.yml` and MCP configuration.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Standalone extension skeleton
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Create: `openclacky-chatcut/ext.yml`
|
|
17
|
+
- Create: `openclacky-chatcut/README.md`
|
|
18
|
+
- Create: `openclacky-chatcut/LICENSE`
|
|
19
|
+
- Create: `openclacky-chatcut/Rakefile`
|
|
20
|
+
- Create: `openclacky-chatcut/test/test_helper.rb`
|
|
21
|
+
|
|
22
|
+
- [ ] **Step 1: Write a manifest validation test**
|
|
23
|
+
|
|
24
|
+
Create a minitest that loads `ext.yml`, asserts `id == "chatcut"`, and asserts the three skill directories exist.
|
|
25
|
+
|
|
26
|
+
- [ ] **Step 2: Run the test to verify RED**
|
|
27
|
+
|
|
28
|
+
Run: `ruby -Itest test/manifest_test.rb`
|
|
29
|
+
Expected: FAIL because `ext.yml` does not exist.
|
|
30
|
+
|
|
31
|
+
- [ ] **Step 3: Add the minimal extension skeleton**
|
|
32
|
+
|
|
33
|
+
Declare `chatcut-setup`, `chatcut`, and `chatcut-disconnect` under `contributes.skills`, add MIT project metadata for original adapter code, and add a Rake test task.
|
|
34
|
+
|
|
35
|
+
- [ ] **Step 4: Run the test to verify GREEN**
|
|
36
|
+
|
|
37
|
+
Run: `ruby -Itest test/manifest_test.rb`
|
|
38
|
+
Expected: PASS.
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 5: Commit**
|
|
41
|
+
|
|
42
|
+
Run: `git add . && git commit -m "chore: scaffold chatcut extension"`
|
|
43
|
+
|
|
44
|
+
### Task 2: Conflict-safe MCP configuration
|
|
45
|
+
|
|
46
|
+
**Files:**
|
|
47
|
+
- Create: `openclacky-chatcut/lib/chatcut_extension/configurator.rb`
|
|
48
|
+
- Create: `openclacky-chatcut/bin/chatcut-extension`
|
|
49
|
+
- Create: `openclacky-chatcut/test/configurator_test.rb`
|
|
50
|
+
|
|
51
|
+
- [ ] **Step 1: Write failing configuration tests**
|
|
52
|
+
|
|
53
|
+
Cover merging a bridge entry while preserving another server, selecting native mode from `{"remote_oauth":true}`, falling back when the command is absent, refusing to replace an unowned conflicting `chatcut` entry, and disconnecting only an owned entry.
|
|
54
|
+
|
|
55
|
+
- [ ] **Step 2: Verify RED**
|
|
56
|
+
|
|
57
|
+
Run: `ruby -Itest test/configurator_test.rb`
|
|
58
|
+
Expected: FAIL because `ChatcutExtension::Configurator` is undefined.
|
|
59
|
+
|
|
60
|
+
- [ ] **Step 3: Implement the configurator and executable**
|
|
61
|
+
|
|
62
|
+
Implement `setup`, `status`, and `disconnect`. Probe `clacky mcp capabilities --json`; native mode writes `type`, `url`, and `auth`, while fallback writes `command` and absolute bridge path. Save a SHA-256 fingerprint of the managed entry under the extension data directory and chmod configuration/state files to 0600.
|
|
63
|
+
|
|
64
|
+
- [ ] **Step 4: Verify GREEN**
|
|
65
|
+
|
|
66
|
+
Run: `ruby -Itest test/configurator_test.rb`
|
|
67
|
+
Expected: PASS.
|
|
68
|
+
|
|
69
|
+
- [ ] **Step 5: Commit**
|
|
70
|
+
|
|
71
|
+
Run: `git add bin lib test && git commit -m "feat: configure chatcut mcp with native fallback"`
|
|
72
|
+
|
|
73
|
+
### Task 3: OAuth discovery and credential storage
|
|
74
|
+
|
|
75
|
+
**Files:**
|
|
76
|
+
- Create: `openclacky-chatcut/lib/chatcut_bridge/credential_store.rb`
|
|
77
|
+
- Create: `openclacky-chatcut/lib/chatcut_bridge/oauth_client.rb`
|
|
78
|
+
- Create: `openclacky-chatcut/test/credential_store_test.rb`
|
|
79
|
+
- Create: `openclacky-chatcut/test/oauth_client_test.rb`
|
|
80
|
+
|
|
81
|
+
- [ ] **Step 1: Write failing OAuth unit tests**
|
|
82
|
+
|
|
83
|
+
Test mode-0600 atomic persistence, protected-resource and authorization-server discovery, PKCE S256 calculation, state mismatch rejection, successful code exchange, refresh-token rotation, and redacted HTTP errors using a local WEBrick server.
|
|
84
|
+
|
|
85
|
+
- [ ] **Step 2: Verify RED**
|
|
86
|
+
|
|
87
|
+
Run: `ruby -Itest test/credential_store_test.rb test/oauth_client_test.rb`
|
|
88
|
+
Expected: FAIL because the bridge classes are undefined.
|
|
89
|
+
|
|
90
|
+
- [ ] **Step 3: Implement minimal OAuth classes**
|
|
91
|
+
|
|
92
|
+
Use `SecureRandom.urlsafe_base64`, SHA-256 PKCE, loopback callbacks on `127.0.0.1`, `Net::HTTP`, bounded JSON reads, endpoint HTTPS validation, and atomic JSON writes. Return an authorization header without logging credential values.
|
|
93
|
+
|
|
94
|
+
- [ ] **Step 4: Verify GREEN**
|
|
95
|
+
|
|
96
|
+
Run: `ruby -Itest test/credential_store_test.rb test/oauth_client_test.rb`
|
|
97
|
+
Expected: PASS.
|
|
98
|
+
|
|
99
|
+
- [ ] **Step 5: Commit**
|
|
100
|
+
|
|
101
|
+
Run: `git add lib test && git commit -m "feat: add chatcut bridge oauth session"`
|
|
102
|
+
|
|
103
|
+
### Task 4: MCP stdio-to-HTTP bridge
|
|
104
|
+
|
|
105
|
+
**Files:**
|
|
106
|
+
- Create: `openclacky-chatcut/lib/chatcut_bridge/http_forwarder.rb`
|
|
107
|
+
- Create: `openclacky-chatcut/lib/chatcut_bridge/server.rb`
|
|
108
|
+
- Create: `openclacky-chatcut/bridge/chatcut_mcp_bridge.rb`
|
|
109
|
+
- Create: `openclacky-chatcut/test/http_forwarder_test.rb`
|
|
110
|
+
- Create: `openclacky-chatcut/test/server_test.rb`
|
|
111
|
+
|
|
112
|
+
- [ ] **Step 1: Write failing bridge tests**
|
|
113
|
+
|
|
114
|
+
Test JSON-RPC line forwarding, SSE `data:` extraction, session-id propagation, bearer injection, proactive refresh, a single refresh-and-retry on 401, notification handling, protocol-only stdout, and redacted diagnostics.
|
|
115
|
+
|
|
116
|
+
- [ ] **Step 2: Verify RED**
|
|
117
|
+
|
|
118
|
+
Run: `ruby -Itest test/http_forwarder_test.rb test/server_test.rb`
|
|
119
|
+
Expected: FAIL because forwarder and server classes are undefined.
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 3: Implement the bridge**
|
|
122
|
+
|
|
123
|
+
Read one JSON-RPC object per stdin line, POST it to ChatCut, emit response objects as compact JSON lines, send diagnostics only to stderr, preserve `Mcp-Session-Id`, and use the OAuth client for authorization.
|
|
124
|
+
|
|
125
|
+
- [ ] **Step 4: Verify GREEN**
|
|
126
|
+
|
|
127
|
+
Run: `ruby -Itest test/http_forwarder_test.rb test/server_test.rb`
|
|
128
|
+
Expected: PASS.
|
|
129
|
+
|
|
130
|
+
- [ ] **Step 5: Commit**
|
|
131
|
+
|
|
132
|
+
Run: `git add bridge lib test && git commit -m "feat: bridge chatcut remote mcp over stdio"`
|
|
133
|
+
|
|
134
|
+
### Task 5: Skills, packaging, and local installation
|
|
135
|
+
|
|
136
|
+
**Files:**
|
|
137
|
+
- Create: `openclacky-chatcut/skills/chatcut-setup/SKILL.md`
|
|
138
|
+
- Create: `openclacky-chatcut/skills/chatcut/SKILL.md`
|
|
139
|
+
- Create: `openclacky-chatcut/skills/chatcut-disconnect/SKILL.md`
|
|
140
|
+
- Create: `openclacky-chatcut/test/skills_test.rb`
|
|
141
|
+
|
|
142
|
+
- [ ] **Step 1: Write failing skill contract tests**
|
|
143
|
+
|
|
144
|
+
Assert setup runs the extension executable, chatcut invokes `mcp:chatcut` and instructs the subagent to use `list_skills`/`load_skill`, and disconnect runs conflict-safe cleanup.
|
|
145
|
+
|
|
146
|
+
- [ ] **Step 2: Verify RED**
|
|
147
|
+
|
|
148
|
+
Run: `ruby -Itest test/skills_test.rb`
|
|
149
|
+
Expected: FAIL because the skills do not exist.
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 3: Write the three skills and local install instructions**
|
|
152
|
+
|
|
153
|
+
Keep host-specific instructions in setup/disconnect and all editing knowledge remote. Document the local symlink into `~/.clacky/ext/local/chatcut` and the requirement to start a new session after first registration.
|
|
154
|
+
|
|
155
|
+
- [ ] **Step 4: Verify GREEN and package**
|
|
156
|
+
|
|
157
|
+
Run: `bundle exec rake test` and `bundle exec ruby bin/clacky ext verify` from the OpenClacky worktree with the symlink installed.
|
|
158
|
+
Expected: all extension tests pass and verifier reports no ChatCut errors.
|
|
159
|
+
|
|
160
|
+
- [ ] **Step 5: Commit**
|
|
161
|
+
|
|
162
|
+
Run: `git add skills test README.md && git commit -m "feat: add chatcut setup and editing skills"`
|
|
163
|
+
|
|
164
|
+
### Task 6: End-to-end fallback and native-mode verification
|
|
165
|
+
|
|
166
|
+
**Files:**
|
|
167
|
+
- Create: `openclacky-chatcut/test/integration/setup_modes_test.rb`
|
|
168
|
+
- Create: `openclacky-chatcut/test/integration/bridge_mcp_test.rb`
|
|
169
|
+
|
|
170
|
+
- [ ] **Step 1: Write integration tests**
|
|
171
|
+
|
|
172
|
+
Run setup against fake old/new `clacky` executables, launch the bridge against a local OAuth/MCP fixture, complete the callback programmatically, call `initialize`, `tools/list`, and `tools/call`, expire the token, and verify refresh.
|
|
173
|
+
|
|
174
|
+
- [ ] **Step 2: Run and fix only implementation defects**
|
|
175
|
+
|
|
176
|
+
Run: `bundle exec rake test`
|
|
177
|
+
Expected: all tests pass with no token values in captured stdout/stderr.
|
|
178
|
+
|
|
179
|
+
- [ ] **Step 3: Create the authorized local symlink**
|
|
180
|
+
|
|
181
|
+
Run: `ln -sfn /Users/seng/Documents/GitHub/clacky/openclacky-chatcut ~/.clacky/ext/local/chatcut`
|
|
182
|
+
Expected: the symlink resolves to the standalone repository.
|
|
183
|
+
|
|
184
|
+
- [ ] **Step 4: Verify with local OpenClacky**
|
|
185
|
+
|
|
186
|
+
Run: `bundle exec ruby bin/clacky ext verify` and inspect `bundle exec ruby bin/clacky ext list`.
|
|
187
|
+
Expected: ChatCut and all three skills are resolved without errors.
|
|
188
|
+
|
|
189
|
+
- [ ] **Step 5: Commit**
|
|
190
|
+
|
|
191
|
+
Run: `git add test && git commit -m "test: verify chatcut bridge fallback modes"`
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Native Remote MCP OAuth Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use box:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add provider-neutral OAuth 2.1 support for remote MCP servers to OpenClacky, including secure credentials, CLI lifecycle commands, automatic refresh, and transport integration.
|
|
6
|
+
|
|
7
|
+
**Architecture:** OAuth metadata/session logic is isolated from MCP JSON-RPC transport. A credential store persists per-server grants outside `mcp.json`; an authorization manager discovers metadata, performs DCR and PKCE, and supplies valid bearer tokens. HTTP transport accepts an authorization provider and retries exactly once after an authenticated 401.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby 2.6+, Thor, Net::HTTP, WEBrick, JSON, RSpec.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: OAuth configuration and capability contract
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Create: `lib/clacky/mcp/oauth/config.rb`
|
|
17
|
+
- Create: `spec/clacky/mcp/oauth/config_spec.rb`
|
|
18
|
+
- Modify: `lib/clacky/mcp/client.rb`
|
|
19
|
+
- Modify: `lib/clacky/mcp/skill_provider.rb`
|
|
20
|
+
- Modify: `lib/clacky/mcp/registry.rb`
|
|
21
|
+
|
|
22
|
+
- [ ] **Step 1: Write failing configuration specs**
|
|
23
|
+
|
|
24
|
+
Cover `auth: {type: oauth}`, explicit resource parsing, default resource equal to MCP URL, rejection of OAuth on non-HTTPS resources, and preservation of static-header servers.
|
|
25
|
+
|
|
26
|
+
- [ ] **Step 2: Verify RED**
|
|
27
|
+
|
|
28
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/config_spec.rb`
|
|
29
|
+
Expected: FAIL because `Clacky::Mcp::OAuth::Config` is undefined.
|
|
30
|
+
|
|
31
|
+
- [ ] **Step 3: Implement minimal configuration parsing**
|
|
32
|
+
|
|
33
|
+
Add a value object with `enabled?`, `resource`, and validation. Pass parsed OAuth configuration from registry specs into HTTP client construction without provider-specific constants.
|
|
34
|
+
|
|
35
|
+
- [ ] **Step 4: Verify GREEN**
|
|
36
|
+
|
|
37
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/config_spec.rb spec/clacky/mcp/skill_provider_spec.rb`
|
|
38
|
+
Expected: PASS.
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 5: Commit**
|
|
41
|
+
|
|
42
|
+
Run: `git add lib spec && git commit -m "feat(mcp): define remote oauth configuration"`
|
|
43
|
+
|
|
44
|
+
### Task 2: Secure OAuth credential store
|
|
45
|
+
|
|
46
|
+
**Files:**
|
|
47
|
+
- Create: `lib/clacky/mcp/oauth/credential_store.rb`
|
|
48
|
+
- Create: `spec/clacky/mcp/oauth/credential_store_spec.rb`
|
|
49
|
+
|
|
50
|
+
- [ ] **Step 1: Write failing credential specs**
|
|
51
|
+
|
|
52
|
+
Cover deterministic per-server lookup, mode-0600 writes, atomic replacement, refresh-token rotation, deletion, malformed-file recovery, and absence of token values from `inspect`.
|
|
53
|
+
|
|
54
|
+
- [ ] **Step 2: Verify RED**
|
|
55
|
+
|
|
56
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/credential_store_spec.rb`
|
|
57
|
+
Expected: FAIL because the store is undefined.
|
|
58
|
+
|
|
59
|
+
- [ ] **Step 3: Implement the file-backed store**
|
|
60
|
+
|
|
61
|
+
Persist JSON below `~/.clacky/mcp/oauth/`, sanitize server names for paths, serialize writes with a lock file, write to a same-directory temporary file, chmod 0600, fsync, and rename atomically.
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 4: Verify GREEN**
|
|
64
|
+
|
|
65
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/credential_store_spec.rb`
|
|
66
|
+
Expected: PASS.
|
|
67
|
+
|
|
68
|
+
- [ ] **Step 5: Commit**
|
|
69
|
+
|
|
70
|
+
Run: `git add lib spec && git commit -m "feat(mcp): store oauth credentials securely"`
|
|
71
|
+
|
|
72
|
+
### Task 3: Metadata discovery, DCR, and PKCE authorization
|
|
73
|
+
|
|
74
|
+
**Files:**
|
|
75
|
+
- Create: `lib/clacky/mcp/oauth/http_client.rb`
|
|
76
|
+
- Create: `lib/clacky/mcp/oauth/authorization_manager.rb`
|
|
77
|
+
- Create: `spec/clacky/mcp/oauth/authorization_manager_spec.rb`
|
|
78
|
+
- Create: `spec/support/oauth_mcp_server.rb`
|
|
79
|
+
|
|
80
|
+
- [ ] **Step 1: Write failing authorization specs**
|
|
81
|
+
|
|
82
|
+
Use a local HTTPS-capable fixture abstraction to test challenge parsing, RFC 9728 protected-resource discovery, RFC 8414 authorization-server discovery, DCR request fields, S256 PKCE, loopback-only callbacks, state verification, code exchange, denied consent, bounded responses, and redacted errors.
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 2: Verify RED**
|
|
85
|
+
|
|
86
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/authorization_manager_spec.rb`
|
|
87
|
+
Expected: FAIL because the manager is undefined.
|
|
88
|
+
|
|
89
|
+
- [ ] **Step 3: Implement authorization manager**
|
|
90
|
+
|
|
91
|
+
Generate a random loopback port and state, register `application_type: native`, open the system browser, capture the callback, exchange the code, persist expiry and refresh credentials, and validate all remote endpoints as HTTPS.
|
|
92
|
+
|
|
93
|
+
- [ ] **Step 4: Verify GREEN**
|
|
94
|
+
|
|
95
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/authorization_manager_spec.rb`
|
|
96
|
+
Expected: PASS.
|
|
97
|
+
|
|
98
|
+
- [ ] **Step 5: Commit**
|
|
99
|
+
|
|
100
|
+
Run: `git add lib spec && git commit -m "feat(mcp): authorize remote servers with pkce"`
|
|
101
|
+
|
|
102
|
+
### Task 4: Token refresh session
|
|
103
|
+
|
|
104
|
+
**Files:**
|
|
105
|
+
- Create: `lib/clacky/mcp/oauth/session.rb`
|
|
106
|
+
- Create: `spec/clacky/mcp/oauth/session_spec.rb`
|
|
107
|
+
|
|
108
|
+
- [ ] **Step 1: Write failing session specs**
|
|
109
|
+
|
|
110
|
+
Cover valid-token reuse, refresh within a one-minute expiry skew, refresh-token rotation, concurrent refresh serialization, invalid-grant cleanup, missing-login errors, one forced refresh, and secret-redacted exceptions.
|
|
111
|
+
|
|
112
|
+
- [ ] **Step 2: Verify RED**
|
|
113
|
+
|
|
114
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/session_spec.rb`
|
|
115
|
+
Expected: FAIL because `Session` is undefined.
|
|
116
|
+
|
|
117
|
+
- [ ] **Step 3: Implement the session**
|
|
118
|
+
|
|
119
|
+
Expose `authorization_headers` and `invalidate!`; refresh through the token endpoint when needed and persist only successful responses.
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 4: Verify GREEN**
|
|
122
|
+
|
|
123
|
+
Run: `bundle exec rspec spec/clacky/mcp/oauth/session_spec.rb`
|
|
124
|
+
Expected: PASS.
|
|
125
|
+
|
|
126
|
+
- [ ] **Step 5: Commit**
|
|
127
|
+
|
|
128
|
+
Run: `git add lib spec && git commit -m "feat(mcp): refresh oauth sessions automatically"`
|
|
129
|
+
|
|
130
|
+
### Task 5: HTTP transport authentication integration
|
|
131
|
+
|
|
132
|
+
**Files:**
|
|
133
|
+
- Modify: `lib/clacky/mcp/http_transport.rb`
|
|
134
|
+
- Modify: `lib/clacky/mcp/client.rb`
|
|
135
|
+
- Modify: `spec/clacky/mcp/http_transport_spec.rb`
|
|
136
|
+
|
|
137
|
+
- [ ] **Step 1: Write failing transport specs**
|
|
138
|
+
|
|
139
|
+
Cover bearer injection without mutating static headers, proactive refresh use, exactly one invalidate-and-retry on 401, no retry for non-authenticated servers, no retry after the second 401, and redacted transport errors.
|
|
140
|
+
|
|
141
|
+
- [ ] **Step 2: Verify RED**
|
|
142
|
+
|
|
143
|
+
Run: `bundle exec rspec spec/clacky/mcp/http_transport_spec.rb`
|
|
144
|
+
Expected: FAIL because transport has no authorization provider.
|
|
145
|
+
|
|
146
|
+
- [ ] **Step 3: Integrate OAuth session**
|
|
147
|
+
|
|
148
|
+
Accept an optional authorization provider, merge its headers per request, capture the 401 challenge, invalidate once, retry the same body once, and preserve MCP session headers and existing static-header behavior.
|
|
149
|
+
|
|
150
|
+
- [ ] **Step 4: Verify GREEN**
|
|
151
|
+
|
|
152
|
+
Run: `bundle exec rspec spec/clacky/mcp/http_transport_spec.rb spec/clacky/mcp/skill_provider_spec.rb`
|
|
153
|
+
Expected: PASS.
|
|
154
|
+
|
|
155
|
+
- [ ] **Step 5: Commit**
|
|
156
|
+
|
|
157
|
+
Run: `git add lib spec && git commit -m "feat(mcp): authenticate remote http transports"`
|
|
158
|
+
|
|
159
|
+
### Task 6: MCP OAuth CLI
|
|
160
|
+
|
|
161
|
+
**Files:**
|
|
162
|
+
- Create: `lib/clacky/mcp/cli_commands.rb`
|
|
163
|
+
- Create: `spec/clacky/mcp/cli_commands_spec.rb`
|
|
164
|
+
- Modify: `lib/clacky/cli.rb`
|
|
165
|
+
- Modify: `lib/clacky.rb`
|
|
166
|
+
|
|
167
|
+
- [ ] **Step 1: Write failing CLI specs**
|
|
168
|
+
|
|
169
|
+
Cover `mcp capabilities --json`, `mcp login SERVER`, `mcp status SERVER --json`, `mcp logout SERVER`, unknown servers, non-OAuth servers, and output that contains status metadata but no tokens.
|
|
170
|
+
|
|
171
|
+
- [ ] **Step 2: Verify RED**
|
|
172
|
+
|
|
173
|
+
Run: `bundle exec rspec spec/clacky/mcp/cli_commands_spec.rb`
|
|
174
|
+
Expected: FAIL because `Clacky::Mcp::CliCommands` is undefined.
|
|
175
|
+
|
|
176
|
+
- [ ] **Step 3: Implement and register the Thor subcommand**
|
|
177
|
+
|
|
178
|
+
Expose `remote_oauth: true` as a capability, authorize configured servers, report connected/expired/disconnected states, and delete credentials on logout.
|
|
179
|
+
|
|
180
|
+
- [ ] **Step 4: Verify GREEN**
|
|
181
|
+
|
|
182
|
+
Run: `bundle exec rspec spec/clacky/mcp/cli_commands_spec.rb`
|
|
183
|
+
Expected: PASS.
|
|
184
|
+
|
|
185
|
+
- [ ] **Step 5: Commit**
|
|
186
|
+
|
|
187
|
+
Run: `git add lib spec && git commit -m "feat(mcp): add oauth lifecycle commands"`
|
|
188
|
+
|
|
189
|
+
### Task 7: Documentation and complete verification
|
|
190
|
+
|
|
191
|
+
**Files:**
|
|
192
|
+
- Modify: `docs/mcp-architecture.md`
|
|
193
|
+
- Modify: `docs/mcp.example.json`
|
|
194
|
+
- Modify: `README.md`
|
|
195
|
+
- Modify: `README_CN.md`
|
|
196
|
+
- Create: `spec/clacky/mcp/oauth/integration_spec.rb`
|
|
197
|
+
|
|
198
|
+
- [ ] **Step 1: Write the end-to-end integration spec**
|
|
199
|
+
|
|
200
|
+
Exercise login callback, credential persistence, MCP initialize/tools list/tool call, expiry refresh, 401 retry, status, and logout against the local fixture.
|
|
201
|
+
|
|
202
|
+
- [ ] **Step 2: Verify the integration RED then GREEN**
|
|
203
|
+
|
|
204
|
+
Run before final wiring: `bundle exec rspec spec/clacky/mcp/oauth/integration_spec.rb`; confirm the expected missing wiring failure. Complete wiring and rerun for PASS.
|
|
205
|
+
|
|
206
|
+
- [ ] **Step 3: Document configuration and security behavior**
|
|
207
|
+
|
|
208
|
+
Add a generic OAuth MCP example, CLI instructions, credential location, HTTPS and loopback rules, and automatic refresh behavior in English and Chinese user-facing documentation.
|
|
209
|
+
|
|
210
|
+
- [ ] **Step 4: Run full verification**
|
|
211
|
+
|
|
212
|
+
Run: `bundle exec rspec`, `bundle exec rubocop`, and `bundle exec rake build`.
|
|
213
|
+
Expected: zero test failures, zero lint errors, and a successfully built gem.
|
|
214
|
+
|
|
215
|
+
- [ ] **Step 5: Commit**
|
|
216
|
+
|
|
217
|
+
Run: `git add docs README.md README_CN.md spec lib && git commit -m "docs(mcp): document remote oauth connections"`
|
|
218
|
+
|
|
219
|
+
### Task 8: Upstream PR preparation
|
|
220
|
+
|
|
221
|
+
**Files:**
|
|
222
|
+
- Inspect: all files changed from `upstream/main`
|
|
223
|
+
|
|
224
|
+
- [ ] **Step 1: Perform structured self-review**
|
|
225
|
+
|
|
226
|
+
Run: `git diff upstream/main...HEAD`, inspect every changed file for provider-specific code, token leakage, compatibility regressions, unsupported Ruby syntax, debug output, and missing tests.
|
|
227
|
+
|
|
228
|
+
- [ ] **Step 2: Run clean-room verification**
|
|
229
|
+
|
|
230
|
+
Run the full test, lint, build, and gem-content checks from a clean checkout of the branch.
|
|
231
|
+
|
|
232
|
+
- [ ] **Step 3: Push the feature branch**
|
|
233
|
+
|
|
234
|
+
Run: `git push -u origin feature/mcp-oauth`.
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 4: Create the upstream PR**
|
|
237
|
+
|
|
238
|
+
Run `gh pr create --repo clacky-ai/openclacky --base main --head RLBox:feature/mcp-oauth` with a summary of protocol support, security boundaries, tests, and ChatCut as an external validation case.
|
|
239
|
+
|
|
240
|
+
- [ ] **Step 5: Inspect PR state**
|
|
241
|
+
|
|
242
|
+
Run: `gh pr view --repo clacky-ai/openclacky --json url,state,headRefName,baseRefName,statusCheckRollup`.
|
|
243
|
+
Expected: an OPEN PR targeting `main` from `feature/mcp-oauth`, with the URL recorded for handoff.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Remote MCP OAuth and Extension Fallback Design
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
OpenClacky extensions need to connect users to OAuth-protected remote MCP servers without embedding access tokens in `mcp.json`. ChatCut is the first integration, but the client capability must remain provider-neutral.
|
|
6
|
+
|
|
7
|
+
The delivery has two independently usable versions:
|
|
8
|
+
|
|
9
|
+
1. A pure ChatCut extension that works on current OpenClacky releases by registering a local stdio bridge. The bridge owns OAuth and forwards MCP traffic to ChatCut's hosted Streamable HTTP endpoint.
|
|
10
|
+
2. Native OpenClacky remote MCP OAuth. Once present, the same extension detects it and registers ChatCut as a direct HTTP MCP server; on older clients it falls back to the bridge.
|
|
11
|
+
|
|
12
|
+
## User Experience
|
|
13
|
+
|
|
14
|
+
Installing the ChatCut extension adds setup, status, and disconnect skills. Setup examines the installed OpenClacky capability instead of asking the user which mode to use.
|
|
15
|
+
|
|
16
|
+
- On a client with native OAuth support, setup writes a direct HTTP server entry with `auth.type: oauth`, then runs the native login command.
|
|
17
|
+
- On an older client, setup writes a stdio server entry pointing at the extension bridge. The bridge opens the browser when authorization is first required.
|
|
18
|
+
- Both modes expose the same `mcp:chatcut` virtual skill and the same remote ChatCut tools and playbooks.
|
|
19
|
+
- Setup merges the `chatcut` entry without overwriting other MCP servers. Disconnect removes only the entry owned by the extension and deletes only ChatCut credentials.
|
|
20
|
+
- A newly registered MCP is used from a new session on current clients. Native hot reload may be added independently and is not required for protocol correctness.
|
|
21
|
+
|
|
22
|
+
## Shared Configuration Contract
|
|
23
|
+
|
|
24
|
+
Native mode uses a direct remote server:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"chatcut": {
|
|
30
|
+
"type": "http",
|
|
31
|
+
"url": "https://api.chatcut.io/api/external-mcp/mcp",
|
|
32
|
+
"auth": {
|
|
33
|
+
"type": "oauth",
|
|
34
|
+
"resource": "https://api.chatcut.io/api/external-mcp/mcp"
|
|
35
|
+
},
|
|
36
|
+
"description": "Edit and create videos through ChatCut"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Fallback mode uses the installed extension bridge:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"chatcut": {
|
|
48
|
+
"command": "ruby",
|
|
49
|
+
"args": ["/absolute/extension/path/bridge/chatcut_mcp_bridge.rb"],
|
|
50
|
+
"description": "Edit and create videos through ChatCut"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The extension records its selected mode and managed configuration fingerprint under `~/.clacky/ext-data/chatcut/`. It does not silently replace a user-managed `chatcut` entry that differs from the recorded fingerprint.
|
|
57
|
+
|
|
58
|
+
## Extension Bridge
|
|
59
|
+
|
|
60
|
+
The bridge is a provider-specific compatibility adapter distributed with the extension. It speaks MCP JSON-RPC over stdin/stdout toward OpenClacky and Streamable HTTP toward ChatCut.
|
|
61
|
+
|
|
62
|
+
The bridge performs OAuth protected-resource discovery, authorization-server discovery, dynamic client registration, Authorization Code with PKCE S256, loopback callback handling, access-token injection, refresh-token rotation, and one refresh-and-retry after a 401. It stores credentials in a mode-0600 JSON file below `~/.clacky/ext-data/chatcut/`; stdout remains protocol-only and diagnostics go to stderr with secrets redacted.
|
|
63
|
+
|
|
64
|
+
The bridge validates that discovered authorization endpoints are HTTPS and belong to the authorization server advertised by the protected resource. Its callback binds only to `127.0.0.1`, uses a random available port, verifies `state`, and applies timeouts and response-size limits.
|
|
65
|
+
|
|
66
|
+
## Native OpenClacky OAuth
|
|
67
|
+
|
|
68
|
+
Native OAuth adds a provider-neutral authorization layer around HTTP MCP transport:
|
|
69
|
+
|
|
70
|
+
- `clacky mcp login SERVER`, `status SERVER`, and `logout SERVER` commands.
|
|
71
|
+
- OAuth metadata discovery from a 401 `WWW-Authenticate` challenge or an explicit resource URL.
|
|
72
|
+
- Dynamic Client Registration and Authorization Code with PKCE for public native clients.
|
|
73
|
+
- A credential store outside `mcp.json`, with mode-0600 files as the portable baseline.
|
|
74
|
+
- Automatic bearer injection, proactive refresh near expiry, and one refresh-and-retry after a 401.
|
|
75
|
+
- Secret-safe exceptions and logs.
|
|
76
|
+
|
|
77
|
+
The HTTP transport remains responsible only for MCP HTTP semantics. An OAuth session supplies headers and handles authorization-specific retries. Static `headers` remain supported for non-OAuth servers.
|
|
78
|
+
|
|
79
|
+
## Capability Detection
|
|
80
|
+
|
|
81
|
+
The native client exposes capability detection through `clacky mcp capabilities --json`. The extension selects native mode only when the response includes `remote_oauth: true`. Absence of the command, invalid JSON, or a false value selects bridge mode.
|
|
82
|
+
|
|
83
|
+
This avoids version string comparisons and permits backports. The bridge remains installed even in native mode so downgrading OpenClacky and rerunning setup produces a working fallback.
|
|
84
|
+
|
|
85
|
+
## Security and Failure Behavior
|
|
86
|
+
|
|
87
|
+
- Tokens, authorization codes, PKCE verifiers, client secrets, and refresh tokens never appear in `mcp.json`, agent prompts, or stdout protocol frames.
|
|
88
|
+
- OAuth is refused for non-HTTPS remote resources and authorization endpoints; loopback HTTP is allowed only for the local callback.
|
|
89
|
+
- A mismatched OAuth `state`, missing code, registration failure, denied consent, invalid refresh response, or malformed metadata produces an actionable error without leaking secrets.
|
|
90
|
+
- Concurrent processes serialize credential updates and write through a temporary file followed by an atomic rename.
|
|
91
|
+
- Refresh-token rotation replaces the stored refresh token only after a successful token response.
|
|
92
|
+
- A user-managed conflicting MCP entry is preserved and setup stops with a clear conflict message.
|
|
93
|
+
|
|
94
|
+
## Compatibility and Distribution
|
|
95
|
+
|
|
96
|
+
The extension package contains Ruby source, skills, manifest, icon, tests, and documentation only. It does not bundle FFmpeg or copy ChatCut's Codex plugin. ChatCut playbooks are loaded from the MCP server through `list_skills` and `load_skill`.
|
|
97
|
+
|
|
98
|
+
The OpenClacky change remains generic and carries no ChatCut constants, UI labels, endpoints, or business logic.
|
|
99
|
+
|
|
100
|
+
## Acceptance Scenarios
|
|
101
|
+
|
|
102
|
+
1. On an unmodified current OpenClacky build, extension setup installs a stdio `chatcut` entry without disturbing another MCP entry.
|
|
103
|
+
2. First bridge use opens ChatCut authorization, persists protected credentials, lists ChatCut tools, and keeps stdout valid JSON-RPC.
|
|
104
|
+
3. An expired access token is refreshed without user action, with rotated refresh credentials persisted.
|
|
105
|
+
4. On a native-OAuth OpenClacky build, extension setup installs a direct HTTP entry and does not invoke the bridge.
|
|
106
|
+
5. Native `mcp login`, `status`, and `logout` work against a standards-compliant local test authorization server.
|
|
107
|
+
6. Native MCP calls inject the bearer token, refresh once after 401, and never expose tokens in errors.
|
|
108
|
+
7. Downgrading to a client without the capability and rerunning setup changes the extension-managed entry back to bridge mode.
|
|
109
|
+
8. Disconnect removes the managed ChatCut entry and ChatCut credentials while preserving unrelated MCP configuration.
|
|
110
|
+
|
|
111
|
+
## Assumptions
|
|
112
|
+
|
|
113
|
+
- ChatCut continues to support MCP protocol version `2024-11-05`, Dynamic Client Registration, PKCE S256, refresh tokens, `list_skills`, and `load_skill` as documented in its WorkBuddy integration.
|
|
114
|
+
- The first release uses a protected file credential store for portability. OS keychain integration can be added behind the same store interface later.
|
|
115
|
+
- Publishing the standalone extension repository is separate from the OpenClacky upstream PR; both artifacts are committed and testable locally before publication.
|
|
@@ -13,6 +13,21 @@ module Clacky
|
|
|
13
13
|
@billing_store ||= Billing::BillingStore.new
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# Merge a finished subagent's spend into this agent's cumulative total.
|
|
17
|
+
# Fan-out runs several subagents on separate threads against the same
|
|
18
|
+
# parent, so the read-modify-write must be atomic.
|
|
19
|
+
# @return [Float] the cost that was absorbed
|
|
20
|
+
def absorb_subagent_cost(result, notify_ui: true)
|
|
21
|
+
cost = result && result[:total_cost_usd] || 0.0
|
|
22
|
+
add_cost(cost)
|
|
23
|
+
@ui&.update_sessionbar(cost: @total_cost, cost_source: @cost_source) if notify_ui
|
|
24
|
+
cost
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private def add_cost(amount)
|
|
28
|
+
@cost_mutex.synchronize { @total_cost += amount }
|
|
29
|
+
end
|
|
30
|
+
|
|
16
31
|
# Track cost from API usage
|
|
17
32
|
# Updates total cost and displays iteration statistics
|
|
18
33
|
# @param usage [Hash] Usage data from API response
|
|
@@ -22,7 +37,7 @@ module Clacky
|
|
|
22
37
|
# Priority 1: Use API-provided cost if available (OpenRouter, LiteLLM, etc.)
|
|
23
38
|
iteration_cost = nil
|
|
24
39
|
if usage[:api_cost]
|
|
25
|
-
|
|
40
|
+
add_cost(usage[:api_cost])
|
|
26
41
|
@cost_source = :api
|
|
27
42
|
@task_cost_source = :api
|
|
28
43
|
iteration_cost = usage[:api_cost]
|
|
@@ -39,7 +54,7 @@ module Clacky
|
|
|
39
54
|
# Only accumulate cost when the model has known pricing.
|
|
40
55
|
# Unknown models return nil — display N/A, don't add to total.
|
|
41
56
|
if cost
|
|
42
|
-
|
|
57
|
+
add_cost(cost)
|
|
43
58
|
iteration_cost = cost
|
|
44
59
|
@cost_source = pricing_source
|
|
45
60
|
@task_cost_source = pricing_source
|
|
@@ -12,8 +12,11 @@ module Clacky
|
|
|
12
12
|
:session_rollback
|
|
13
13
|
].freeze
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
attr_accessor :agent
|
|
16
|
+
|
|
17
|
+
def initialize(agent: nil)
|
|
16
18
|
@hooks = Hash.new { |h, k| h[k] = [] }
|
|
19
|
+
@agent = agent
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def add(event, &block)
|
|
@@ -21,20 +24,28 @@ module Clacky
|
|
|
21
24
|
@hooks[event] << block
|
|
22
25
|
end
|
|
23
26
|
|
|
27
|
+
# @return [Hash] `{action: :allow}`, `{action: :deny, reason:}`, or
|
|
28
|
+
# `{action: :handled, result:}` when a hook fulfilled the call itself.
|
|
29
|
+
# Extra trailing arg: the agent that owns this hook chain, so ext hooks can
|
|
30
|
+
# call `agent.emit_event(...)`. Blocks are procs — those declaring fewer
|
|
31
|
+
# params (`|call|`, `|call, result|`) silently ignore it.
|
|
24
32
|
def trigger(event, *args)
|
|
25
33
|
validate_event!(event)
|
|
26
34
|
result = { action: :allow }
|
|
27
35
|
|
|
28
36
|
@hooks[event].each do |hook|
|
|
29
37
|
begin
|
|
30
|
-
hook_result = hook.call(*args)
|
|
38
|
+
hook_result = hook.call(*args, @agent)
|
|
31
39
|
next unless hook_result.is_a?(Hash)
|
|
32
40
|
# First deny wins and stops the chain: a weaker later verdict must
|
|
33
41
|
# never clobber a stronger earlier one, and the first deny's reason
|
|
34
42
|
# is the one that reaches the agent. Rewrite hooks mutate `call` in
|
|
35
43
|
# place (chained rewrite), so for non-deny results there's nothing to
|
|
36
44
|
# merge — we just keep going.
|
|
37
|
-
|
|
45
|
+
#
|
|
46
|
+
# :handled short-circuits the same way — the hook has already produced
|
|
47
|
+
# the tool's result, so later hooks have nothing left to act on.
|
|
48
|
+
if hook_result[:action] == :deny || hook_result[:action] == :handled
|
|
38
49
|
result = hook_result
|
|
39
50
|
break
|
|
40
51
|
end
|