opencode-ext-connector 0.5.0 → 0.7.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.
- package/CHANGELOG.md +32 -0
- package/README.md +103 -15
- package/dist/core/options.d.ts +8 -0
- package/dist/core/options.js +33 -2
- package/dist/core/process.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/opencode/host-options.d.ts +3 -1
- package/dist/opencode/host-options.js +8 -2
- package/dist/process/production-supervisor.d.ts +1 -0
- package/dist/process/production-supervisor.js +4 -1
- package/dist/providers/xai/access-state.d.ts +25 -0
- package/dist/providers/xai/access-state.js +118 -0
- package/dist/providers/xai/authority-observer.d.ts +13 -0
- package/dist/providers/xai/authority-observer.js +165 -0
- package/dist/providers/xai/consumer-auth.d.ts +23 -0
- package/dist/providers/xai/consumer-auth.js +52 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +15 -0
- package/dist/xai.d.ts +2 -0
- package/dist/xai.js +1 -0
- package/docs/README.ko.md +103 -15
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 - 2026-09-23
|
|
4
|
+
|
|
5
|
+
- Add the opt-in `xaiOAuth.mode: "authority" | "consumer"` package subpath
|
|
6
|
+
pairing a host-side authority with a guest-side consumer; omitting
|
|
7
|
+
`xaiOAuth` disables the integration silently, while a present `xaiOAuth`
|
|
8
|
+
whose `mode` is missing or unknown is rejected at parse time
|
|
9
|
+
- Ship the dedicated `./xai` consumer as a separate OpenCode plugin entry
|
|
10
|
+
that returns the xAI auth hook only with `methods: []`
|
|
11
|
+
- Invoke the fixed helper at `${HOME}/.local/bin/opensandbox-xai-auth-sync`
|
|
12
|
+
with no arguments, `PATH=/usr/local/bin:/usr/bin:/bin`, `HOME` always
|
|
13
|
+
set, and `XDG_DATA_HOME` forwarded only when absolute; the helper
|
|
14
|
+
delegates projection to the OpenSandbox manager, and a non-zero exit
|
|
15
|
+
retries after `5000` ms without removing the xAI provider
|
|
16
|
+
- Re-read the access file and `expires` on every outbound request and raise
|
|
17
|
+
`XaiAccessUnavailableError` before any network call on a missing file,
|
|
18
|
+
a stat that fails the closed-v1 `0600`/single-link/current-user gates,
|
|
19
|
+
a schema mismatch, a `"state": "unavailable"` record, malformed JSON,
|
|
20
|
+
or a non-future `expires`
|
|
21
|
+
- Never place a refresh token inside the guest; refresh stays on the host
|
|
22
|
+
authority only
|
|
23
|
+
- Keep existing Claude, Cursor, Command Code, and Ollama providers and
|
|
24
|
+
omitted-mode behavior unchanged
|
|
25
|
+
|
|
26
|
+
## 0.6.0 - 2026-09-16
|
|
27
|
+
|
|
28
|
+
- Add the recommended `credentialRole: "owner" | "reader"` option for shared
|
|
29
|
+
Claude logins: one Linux owner runs the existing Claude CLI authority while
|
|
30
|
+
readers consume externally managed credentials without refreshing them
|
|
31
|
+
- Keep the existing low-level credential policy options available for advanced
|
|
32
|
+
control, but reject combining any of them with `credentialRole`
|
|
33
|
+
- Keep omitted credential policy behavior and non-Claude providers unchanged
|
|
34
|
+
|
|
3
35
|
## 0.5.0 - 2026-09-15
|
|
4
36
|
|
|
5
37
|
- Add the disabled-by-default Claude CLI credential authority for Linux
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
## Status
|
|
21
21
|
|
|
22
|
-
> Independent unofficial community plugin, version **0.
|
|
22
|
+
> Independent unofficial community plugin, version **0.7.0**. Package E2E tests exercise the legacy multi-function loader with the OpenCode CLI installed in CI. `@opencode-ai/plugin@1.18.18` is the compile-time plugin API target, not a runtime pin. Source is BSD-3-Clause. This project is not affiliated with, endorsed by, sponsored by, or authorized by OpenCode or any provider. Full terms are in [License and Disclaimer](#license-and-disclaimer).
|
|
23
23
|
|
|
24
24
|
Reuse the Claude, Cursor, Command Code, and Ollama sessions you already have. One `opencode.json` plugin entry publishes live catalogs into OpenCode. Claude and Cursor stay disconnected until OpenCode has a marker or OAuth record and the vendor session is present. Command Code may use an OpenCode-stored direct API key or an existing CLI session/key. Ollama requires the exact session marker plus a responsive trusted daemon.
|
|
25
25
|
|
|
@@ -55,13 +55,13 @@ OpenCode installs configured npm plugins with Bun at startup and caches them. Fo
|
|
|
55
55
|
```jsonc
|
|
56
56
|
{
|
|
57
57
|
"$schema": "https://opencode.ai/config.json",
|
|
58
|
-
"plugin": ["opencode-ext-connector@0.
|
|
58
|
+
"plugin": ["opencode-ext-connector@0.7.0"]
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Fully quit and restart OpenCode after adding or changing the entry; a reload is not enough.
|
|
63
63
|
|
|
64
|
-
The one package entry exposes the catalog plugin plus the Claude, Cursor, Command Code, and Ollama auth hooks. Provider ids: `claude`, `cursor`, `command-code`, `ollama`. Model ids come from each provider's live catalog, with documented fallbacks `default` (Cursor) and `Qwen/Qwen3.8-Max` (Command Code) when a live list is empty.
|
|
64
|
+
The one package entry exposes the catalog plugin plus the Claude, Cursor, Command Code, and Ollama auth hooks. Provider ids: `claude`, `cursor`, `command-code`, `ollama`. Model ids come from each provider's live catalog, with documented fallbacks `default` (Cursor) and `Qwen/Qwen3.8-Max` (Command Code) when a live list is empty. The opt-in xAI consumer ships as the package subpath `opencode-ext-connector/xai`; see [Opt-in xAI OAuth authority and consumer](#opt-in-xai-oauth-authority-and-consumer-host-and-guest).
|
|
65
65
|
|
|
66
66
|
## Configuration
|
|
67
67
|
|
|
@@ -73,10 +73,12 @@ Omitted `providers` enables all four. An explicit list is a strict allow-list. E
|
|
|
73
73
|
| --- | --- | --- |
|
|
74
74
|
| `providers` | all four | Provider ids to register: `claude`, `cursor`, `command-code`, `ollama`; explicit `[]` disables all |
|
|
75
75
|
| `ollamaBaseURL` | `"http://localhost:11434"` | Absolute `http` or `https` base for the trusted Ollama daemon; path prefixes are preserved |
|
|
76
|
-
| `
|
|
76
|
+
| `credentialRole` | omitted | Recommended shared-login setup: `"owner"` runs the Claude CLI authority on the one credential-owning instance; `"reader"` only reads externally managed credentials |
|
|
77
|
+
| `credentialManagement` | omitted | Advanced policy: `"connector"` authorizes refresh and writeback where an adapter supports both; `"external"` prohibits connector refresh and writeback |
|
|
77
78
|
| `writeBackCredentials` | `false` | **Deprecated:** accepted alone for one migration cycle; controls Claude writeback after refresh |
|
|
78
79
|
| `credentialRefresh.mode` | `"auto"` | **Deprecated:** accepted alone for one migration cycle; controls Claude `"auto"` or `"never"` refresh behavior |
|
|
79
80
|
| `credentialRefresh.leadMs` | `60000` | **Deprecated:** accepted alone for one migration cycle; custom lead times still require this legacy configuration |
|
|
81
|
+
| `xaiOAuth.mode` | omitted | Opt-in xAI role; `"authority"` enables the host observer on the `connectorServer` entry and `"consumer"` activates the dedicated `opencode-ext-connector/xai` consumer. Omitting `xaiOAuth` disables the integration silently. A present `xaiOAuth` whose `mode` is missing or not one of `"authority" | "consumer"` is rejected at parse time. Independent of every Claude credential option. |
|
|
80
82
|
| `credentialAuthority.claudeCli.enabled` | `false` | Claude-only opt-in: see [Opt-in Claude CLI authority timer](#opt-in-claude-cli-authority-timer-claude-only) |
|
|
81
83
|
| `credentialAuthority.claudeCli.leadMs` | `300000` | Claude-only opt-in: milliseconds before credential expiry at which the timer invokes `claude`; non-negative integer |
|
|
82
84
|
| `credentialAuthority.claudeCli.retryMs` | `300000` | Claude-only opt-in: milliseconds to wait before retrying after a non-zero exit, lock conflict, signal, or supervisor failure; positive integer |
|
|
@@ -85,6 +87,34 @@ Omitted `providers` enables all four. An explicit list is a strict allow-list. E
|
|
|
85
87
|
| `health.initialBackoffMs` | `1000` | Health backoff after a failed snapshot |
|
|
86
88
|
| `health.maximumBackoffMs` | `60000` | Health backoff cap |
|
|
87
89
|
|
|
90
|
+
### Credential ownership (recommended)
|
|
91
|
+
|
|
92
|
+
Choose `"owner"` on the one Linux instance that owns and refreshes the shared Claude Code login:
|
|
93
|
+
|
|
94
|
+
```jsonc
|
|
95
|
+
{
|
|
96
|
+
"$schema": "https://opencode.ai/config.json",
|
|
97
|
+
"plugin": [["opencode-ext-connector", { "credentialRole": "owner" }]]
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Choose `"reader"` on every instance that only consumes synchronized credentials:
|
|
102
|
+
|
|
103
|
+
```jsonc
|
|
104
|
+
{
|
|
105
|
+
"$schema": "https://opencode.ai/config.json",
|
|
106
|
+
"plugin": [["opencode-ext-connector", { "credentialRole": "reader" }]]
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`"owner"` internally selects external credential management and enables the Claude CLI authority with its default timing. `"reader"` selects external credential management without starting the CLI authority. The role describes credential ownership, not whether OpenCode runs on a physical host or in a sandbox guest.
|
|
111
|
+
|
|
112
|
+
Set exactly one owner per shared Claude login. Owner mode requires Linux, util-linux `flock`, Claude Code `2.1.259` or later on `PATH`, and an authenticated Claude Code session. Each authority invocation is a real model request and may consume account usage. A revoked login still requires interactive `/login`.
|
|
113
|
+
|
|
114
|
+
Do not combine `credentialRole` with `credentialManagement`, `credentialAuthority`, `credentialRefresh`, or `writeBackCredentials`. Existing low-level configurations remain supported for advanced control.
|
|
115
|
+
|
|
116
|
+
### Advanced credential policy
|
|
117
|
+
|
|
88
118
|
`credentialManagement: "connector"` authorizes the connector to refresh and write back credentials where a provider adapter supports both. Today only Claude has that capability, mapping to automatic refresh with a `60_000` ms lead and writeback enabled:
|
|
89
119
|
|
|
90
120
|
```jsonc
|
|
@@ -143,17 +173,17 @@ With all credential-policy options omitted, refreshed Claude tokens stay in memo
|
|
|
143
173
|
|
|
144
174
|
Anthropic rotates the refresh token on every refresh and invalidates the previous one. Two copies of `~/.claude/.credentials.json` that both refresh will therefore break each other. Copying the file works only if exactly one machine refreshes and every other machine receives the result before its own copy expires:
|
|
145
175
|
|
|
146
|
-
- **
|
|
147
|
-
- **
|
|
176
|
+
- **Credential owner** (where you log in): use `credentialRole: "owner"` on one Linux instance. For advanced control, use `credentialManagement: "connector"`, or combine `credentialManagement: "external"` with the opt-in [Claude CLI authority timer](#opt-in-claude-cli-authority-timer-claude-only).
|
|
177
|
+
- **Credential readers**: use `credentialRole: "reader"`. They never contact the OAuth endpoint; when a request returns 401 they re-read externally managed credentials and retry once.
|
|
148
178
|
- Synchronize the externally managed credential material from the refresh-authority machine whenever it changes. The option itself does not synchronize machines or require file storage; if you copy `~/.claude/.credentials.json`, OpenCode's own `auth.json` only needs the `anthropic` record once and its other providers should remain untouched.
|
|
149
179
|
|
|
150
180
|
Machines that refresh on their own — including a Claude Code install that is used interactively — must not share the file. Log in separately there.
|
|
151
181
|
|
|
152
182
|
### Opt-in Claude CLI authority timer (Claude-only)
|
|
153
183
|
|
|
154
|
-
This
|
|
184
|
+
This is the advanced form of `credentialRole: "owner"`, useful when custom `leadMs` or `retryMs` values are required. It is disabled by default, does not log in or mint tokens, and requires `credentialManagement: "external"`.
|
|
155
185
|
|
|
156
|
-
|
|
186
|
+
Low-level equivalent:
|
|
157
187
|
|
|
158
188
|
```jsonc
|
|
159
189
|
{
|
|
@@ -182,6 +212,58 @@ Each invocation is a real model request and can count against the account's subs
|
|
|
182
212
|
|
|
183
213
|
If the timer does not run, check `flock --version`, `claude --version`, the state directory permissions, and both required configuration values. Repeated warnings indicate that the CLI failed, was signalled, or could not be started. To disable the timer, remove `credentialAuthority` or set `enabled` to `false`, then fully restart OpenCode.
|
|
184
214
|
|
|
215
|
+
### Opt-in xAI OAuth authority and consumer (host and guest)
|
|
216
|
+
|
|
217
|
+
This connector ships an opt-in xAI path that pairs a host-side **authority** with a guest-side **consumer**. The roles are independent of `credentialRole`, `credentialManagement`, `credentialAuthority`, `credentialRefresh`, and `writeBackCredentials`, and never modify or replace any Claude policy. Omitting `xaiOAuth` disables the integration silently. A present `xaiOAuth` whose `mode` is missing or not one of `"authority" | "consumer"` is rejected at parse time.
|
|
218
|
+
|
|
219
|
+
Configure the two roles in separate OpenCode environments. On the host:
|
|
220
|
+
|
|
221
|
+
```jsonc
|
|
222
|
+
{
|
|
223
|
+
"$schema": "https://opencode.ai/config.json",
|
|
224
|
+
"plugin": [["opencode-ext-connector", { "xaiOAuth": { "mode": "authority" } }]]
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
In the guest:
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{
|
|
232
|
+
"$schema": "https://opencode.ai/config.json",
|
|
233
|
+
"plugin": [["opencode-ext-connector/xai", { "xaiOAuth": { "mode": "consumer" } }]]
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Authority (host).** Set `xaiOAuth.mode: "authority"` on the host `connectorServer` entry. The observer reads `${XDG_DATA_HOME}/opencode/auth.json` when `XDG_DATA_HOME` is set and absolute, or `${HOME}/.local/share/opencode/auth.json` on Linux only when `XDG_DATA_HOME` is unset or empty. A relative `XDG_DATA_HOME` (set but not absolute) disables the authority: it neither reads `auth.json` nor invokes the helper. When enabled, the observer inspects **only** the `xai` record. When that record's serialized fingerprint changes, the observer invokes the fixed, no-argument helper at `${HOME}/.local/bin/opensandbox-xai-auth-sync` with `PATH` set to `/usr/local/bin:/usr/bin:/bin`, `HOME` always set, and `XDG_DATA_HOME` forwarded when absolute. The helper delegates projection to the OpenSandbox manager; the connector never writes the guest access file, mints or refreshes xAI OAuth, ships tokens across machines, or places a refresh token inside the guest. A non-zero helper exit is treated as transient and retried after `5000` ms without removing the xAI provider.
|
|
238
|
+
|
|
239
|
+
**Consumer (guest).** Load the consumer as a separate OpenCode plugin entry as the tuple `["opencode-ext-connector/xai", { "xaiOAuth": { "mode": "consumer" } }]`. The mode is required: a subpath entry without it returns an empty hook and the xAI provider stays disconnected. With mode `"consumer"` the connector returns the xAI auth hook and exposes no `/connect` methods — `methods: []` is part of the public contract. The consumer requires the exact marker
|
|
240
|
+
|
|
241
|
+
```jsonc
|
|
242
|
+
{ "type": "api", "key": "cli-session:xai" }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
in the OpenCode auth record for the `xai` provider. The connector does not place this marker; provisioning it is the operator's responsibility. Any other shape means the loader returns no auth. As a sentinel, the loader returns a placeholder `apiKey` plus a wrapped `fetch`; the real bearer is sourced from the access file on every outbound request.
|
|
246
|
+
|
|
247
|
+
**Access file.** The consumer re-reads the access file on **every** request and never trusts an in-memory token:
|
|
248
|
+
|
|
249
|
+
- Path: `${XDG_DATA_HOME}/opencode/xai-access.json` when `XDG_DATA_HOME` is set and absolute, or `${HOME}/.local/share/opencode/xai-access.json` on Linux when `XDG_DATA_HOME` is unset or empty. A relative `XDG_DATA_HOME` fails closed without inspecting the HOME fallback.
|
|
250
|
+
- File gates: regular file, mode `0600`, link count `1`, owned by the current user. A missing file, a non-conforming stat, a symlink, a multi-link file, the wrong owner, or the wrong mode all return the unavailable state.
|
|
251
|
+
- Schema (closed v1):
|
|
252
|
+
|
|
253
|
+
```jsonc
|
|
254
|
+
{
|
|
255
|
+
"schema_version": 1,
|
|
256
|
+
"provider": "xai",
|
|
257
|
+
"state": "ready",
|
|
258
|
+
"access": "<bearer-token>",
|
|
259
|
+
"expires": 1735689600000
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
A `"state": "unavailable"` record, malformed JSON, a schema mismatch, an absent file, a failed stat, or an `expires` not strictly greater than the current time all fail **before** any network call, raising `XaiAccessUnavailableError` for the request. The bearer token never enters plugin arguments, connector stdout/stderr diagnostics, or request URLs.
|
|
264
|
+
|
|
265
|
+
**Out of scope.** Ordinary xAI API keys stored directly in OpenCode auth (the built-in `XAI_API_KEY`-style path) follow OpenCode's built-in `xai` provider, not this authority/consumer pair. The connector never imports, mirrors, or proxies such keys. The helper must be idempotent; running a developer Claude Code session in the same guest does not give it authority over the projected access file.
|
|
266
|
+
|
|
185
267
|
## Host/Guest Sandbox Setup
|
|
186
268
|
|
|
187
269
|
When OpenCode runs in a container, VM, or another sandbox, treat that runtime as the **guest** and the machine that owns the vendor logins and Ollama daemon as the **host**. The guest has its own `localhost`, home directory, environment, keychains, filesystem permissions, and network namespace. Host sessions are not visible unless you mount their files or inject their environment values explicitly.
|
|
@@ -190,10 +272,11 @@ The safest shared-session layout keeps each vendor login owned and refreshed by
|
|
|
190
272
|
|
|
191
273
|
| Provider | Host | Guest |
|
|
192
274
|
| --- | --- | --- |
|
|
193
|
-
| Claude | Own and refresh the Claude Code login | Mount the Claude credential directory read-only, set `CLAUDE_CONFIG_DIR` to that guest path, and use `
|
|
275
|
+
| Claude | Own and refresh the Claude Code login | Mount the Claude credential directory read-only, set `CLAUDE_CONFIG_DIR` to that guest path, and use `credentialRole: "reader"`; a host macOS Keychain is not available inside a Linux guest; resolve the client version with `ANTHROPIC_CLI_VERSION`, an installed `claude` binary, or npm registry access |
|
|
194
276
|
| Cursor | Own the Cursor CLI login | Mount the credential file at the guest's `${HOME}/.config/cursor/auth.json` read-only, or inject `CURSOR_ACCESS_TOKEN` through the sandbox's secret mechanism; install Node.js 22 or later in the guest |
|
|
195
277
|
| Command Code | Own the CLI login or API key | Mount `${HOME}/.commandcode/auth.json` read-only, or inject `COMMAND_CODE_API_KEY`; resolve the client version with `COMMAND_CODE_CLI_VERSION`, an installed `command-code` binary, or npm registry access |
|
|
196
278
|
| Ollama | Run the trusted daemon and run `ollama signin` there when Cloud access is needed | Copy no Ollama credential; connect only to the daemon selected by `ollamaBaseURL` |
|
|
279
|
+
| xAI | Host authority invokes `~/.local/bin/opensandbox-xai-auth-sync`; the OpenSandbox manager projects access-only guest state | Use the manager-projected `{"type":"api","key":"cli-session:xai"}` marker and `${XDG_DATA_HOME}/opencode/xai-access.json` (or the HOME fallback when XDG is unset or empty), then load `opencode-ext-connector/xai` with `xaiOAuth.mode: "consumer"`; never copy or mount a refresh token |
|
|
197
280
|
|
|
198
281
|
For example, a Linux guest can use these paths and optional secret/version overrides; adapt the mount source and destination to the sandbox runtime:
|
|
199
282
|
|
|
@@ -220,7 +303,7 @@ Use this complete guest `opencode.json` when all four providers are enabled and
|
|
|
220
303
|
{
|
|
221
304
|
"providers": ["claude", "cursor", "command-code", "ollama"],
|
|
222
305
|
"ollamaBaseURL": "http://host.docker.internal:11434",
|
|
223
|
-
"
|
|
306
|
+
"credentialRole": "reader",
|
|
224
307
|
"catalogReloadMs": 300000,
|
|
225
308
|
"snapshotTimeoutMs": 30000,
|
|
226
309
|
"health": {
|
|
@@ -233,7 +316,7 @@ Use this complete guest `opencode.json` when all four providers are enabled and
|
|
|
233
316
|
}
|
|
234
317
|
```
|
|
235
318
|
|
|
236
|
-
`ollamaBaseURL` is a flat connector option in the package tuple, not an OpenCode provider option. The numeric values above are the connector defaults; `
|
|
319
|
+
`ollamaBaseURL` is a flat connector option in the package tuple, not an OpenCode provider option. The numeric values above are the connector defaults; `credentialRole: "reader"` and the host daemon URL are deliberate overrides for read-only host-owned credentials. Do not put vendor tokens in `opencode.json`; pass them through read-only mounts or the sandbox's secret injection mechanism.
|
|
237
320
|
|
|
238
321
|
For Docker Desktop, `host.docker.internal` normally resolves to the host. A Linux Docker bridge may also need `--add-host=host.docker.internal:host-gateway` or the Compose equivalent:
|
|
239
322
|
|
|
@@ -244,7 +327,7 @@ extra_hosts:
|
|
|
244
327
|
|
|
245
328
|
Ollama normally listens on host loopback. For bridge networking, the host may need to start it with `OLLAMA_HOST=0.0.0.0:11434`; restrict the exposed port with host firewall and sandbox network policy. `OLLAMA_HOST` configures the host daemon, while `ollamaBaseURL` configures this connector in the guest. Host networking makes guest `localhost` reach the host but reduces isolation and should be an explicit choice. Other sandbox runtimes need an equivalent host route and must allow outbound access to each enabled provider; allow `registry.npmjs.org` only when Claude or Command Code cannot resolve its client version from an environment value or installed binary.
|
|
246
329
|
|
|
247
|
-
Alternatively, the guest can own its vendor logins in persistent guest storage. In that mode, run vendor login flows in the guest instead of mounting host credentials. A guest that is the sole Claude refresh owner may use `credentialManagement: "connector"
|
|
330
|
+
Alternatively, the guest can own its vendor logins in persistent guest storage. In that mode, run vendor login flows in the guest instead of mounting host credentials. A Linux guest that is the sole Claude refresh owner may use `credentialRole: "owner"`; low-level `credentialManagement: "connector"` remains available for connector-managed OAuth refresh and writeback. Never let the host and guest independently refresh credentials descended from the same Claude refresh token.
|
|
248
331
|
|
|
249
332
|
## Update and Remove
|
|
250
333
|
|
|
@@ -278,15 +361,18 @@ Ollama `/connect` probes the configured daemon and stores the exact session mark
|
|
|
278
361
|
|
|
279
362
|
| Provider | What it does |
|
|
280
363
|
| --- | --- |
|
|
281
|
-
| **Claude** | Reuses existing Claude Code credentials. Does not mint OAuth. Compatibility fetch sends CLI-compatible request metadata and streams Anthropic SSE on the built-in `anthropic` path. `
|
|
364
|
+
| **Claude** | Reuses existing Claude Code credentials. Does not mint OAuth. Compatibility fetch sends CLI-compatible request metadata and streams Anthropic SSE on the built-in `anthropic` path. For shared logins, `credentialRole: "owner"` enables the Linux-only Claude CLI authority on one instance, while `"reader"` keeps every other instance read-only with a credential re-read after 401. Low-level `credentialManagement: "connector"` maps to auto-refresh with a `60_000` ms lead and writeback; `"external"` maps to never-refresh/no-write. Omitting all credential-policy options preserves legacy auto/`60_000` behavior without writeback; if only `credentialManagement` is omitted, supplied deprecated options still control behavior. |
|
|
282
365
|
| **Cursor** | Calls Cursor's unpublished client protocol (`api2.cursor.sh` `AgentService`, Connect+protobuf over HTTP/2) with the CLI access token. Credentials remain read-only under both credential-management modes. Direct generation may re-read a changed, non-null credential and retry once only on an exact HTTP 401 before output or effects; this is not refresh or writeback. Legacy/compatibility generation remains one-shot. A plugin-owned Node child communicates over private stdio, keeps tool results on the same bidi Run, never replays parked calls, opens no user-facing daemon, and never spawns `cursor-agent` for generation. Unofficial; not a public Cursor API. After protocol drift there is no implicit fallback — that provider fails. Requires Node.js 22 or later. Live catalog ids are used when present; otherwise the documented fallback is `default`. |
|
|
283
366
|
| **Command Code** | Calls `/alpha/generate` with CLI-compatible request metadata and streams provider-local NDJSON text and tool events. Credentials remain read-only under both credential-management modes. On an exact HTTP 401 before output or effects, it may re-read a changed, non-null credential and retry once only; this is not refresh or writeback. The client version comes from `COMMAND_CODE_CLI_VERSION`, an installed `command-code` binary, or the npm registry. Request metadata includes Node.js version, platform, architecture, and the absolute working directory. Live catalog ids are used when present; otherwise the documented fallback is `Qwen/Qwen3.8-Max`. |
|
|
284
367
|
| **Ollama** | Unaffected by `credentialManagement`. Uses the trusted daemon selected by `ollamaBaseURL` (default `http://localhost:11434`) with `/api/tags`, `/api/pull`, and `/api/chat`; path prefixes are preserved. Publishes models already pulled there, plus exact Cloud tags discovered anonymously from Ollama's official Cloud search and library pages, without connector-supplied credentials. Local entries win exact duplicates. Incomplete Cloud refreshes retain the last complete list. Selecting an absent authorized Cloud tag pulls its lightweight remote reference on first use; concurrent pulls of the same tag and normalized base share one in-flight request, and a failed pull can be retried later. The daemon may then proxy Cloud-tag prompts under the user's Ollama Cloud subscription. The connector never uses an Ollama API key, the usage-billed direct Cloud API, `OLLAMA_HOST`, credentials, custom headers, cookies, or a direct Cloud generation endpoint. |
|
|
368
|
+
| **xAI** | Opt-in OAuth consumer loaded only through the package subpath `opencode-ext-connector/xai`. The access file is re-read on every request, exposes no `/connect` methods, and never mints or refreshes xAI OAuth. Independent of Claude `credentialRole` and credential policy. See [Opt-in xAI OAuth authority and consumer](#opt-in-xai-oauth-authority-and-consumer-host-and-guest). |
|
|
285
369
|
|
|
286
370
|
Provider health is isolated: one provider failing does not remove the others.
|
|
287
371
|
|
|
288
372
|
The standalone SDK entry is `opencode-ext-connector/ollama`; pass `{ ollamaBaseURL }` to select the same trusted daemon. It can generate with models already present there; connector-managed Cloud auto-pull requires an active Ollama catalog lease for that normalized base.
|
|
289
373
|
|
|
374
|
+
The dedicated consumer entry is `opencode-ext-connector/xai`; pass `{ xaiOAuth: { mode: "consumer" } }` to register the xAI auth hook without registering any model providers, models, or `/connect` methods.
|
|
375
|
+
|
|
290
376
|
## Troubleshooting
|
|
291
377
|
|
|
292
378
|
| Symptom | What to check |
|
|
@@ -294,12 +380,14 @@ The standalone SDK entry is `opencode-ext-connector/ollama`; pass `{ ollamaBaseU
|
|
|
294
380
|
| `/connect` methods missing | Confirm `plugin` contains `"opencode-ext-connector"` or an exact published `"opencode-ext-connector@<version>"` spec, then fully restart OpenCode. |
|
|
295
381
|
| Provider enabled but no models | Omitted `providers` enables all four; an explicit list is a strict allow-list. Claude and Cursor need a marker or OAuth record plus the vendor session; Command Code may use an OpenCode-stored API key or a CLI session/key; Ollama needs the exact marker plus a responsive configured daemon. Fully restart after `/connect` so instance reconstruction picks up new membership. |
|
|
296
382
|
| Claude works until the next start | Omitting all credential-policy options preserves legacy in-memory refresh without writeback. A rotated refresh token can then fail on the next process start; use `credentialManagement: "connector"` when the connector should refresh and write back. If only `credentialManagement` is omitted, check supplied deprecated refresh/writeback options instead. |
|
|
297
|
-
| Claude reports `invalid_grant` on shared credentials | Another machine with the same login already refreshed and rotated the refresh token.
|
|
298
|
-
| Configuration rejects credential options | Do not combine
|
|
383
|
+
| Claude reports `invalid_grant` on shared credentials | Another machine with the same login already refreshed and rotated the refresh token. Set `credentialRole: "owner"` on exactly one Linux instance and `"reader"` everywhere else, or use exactly one documented low-level authority path. Every refresher counts, including connector OAuth refresh and the Claude CLI timer. |
|
|
384
|
+
| Configuration rejects credential options | Do not combine `credentialRole` with any low-level credential option. The exact role-conflict error is: `` `credentialRole` cannot be combined with `credentialManagement`, `credentialAuthority`, `credentialRefresh`, or `writeBackCredentials` ``. Low-level options remain accepted alone; `credentialManagement` still cannot be combined with deprecated refresh/writeback options. |
|
|
299
385
|
| `Claude Code client version is unavailable` | No `ANTHROPIC_CLI_VERSION`, no `claude` binary, and `registry.npmjs.org` was unreachable. Set the variable or allow registry access. |
|
|
300
386
|
| Cursor generation fails | Node.js 22 or later is required. Generation uses the unpublished protocol through a private Node child, not `cursor-agent`. Protocol drift fails that provider; there is no implicit fallback. |
|
|
301
387
|
| Command Code generation fails | The client version could not be resolved: set `COMMAND_CODE_CLI_VERSION`, install `command-code`, or allow access to `registry.npmjs.org`. Request metadata includes Node.js version, platform, architecture, and the absolute working directory. |
|
|
302
388
|
| Ollama missing from `opencode models ollama` | Start a daemon you trust at `ollamaBaseURL` (or the default `localhost:11434`), then `/connect` so the exact session marker can be stored. Confirm any path prefix reaches Ollama's `/api/*` routes. Cloud tags are anonymous catalog entries; `OLLAMA_HOST`, API keys, credential headers, redirects, and direct Cloud generation are not used. |
|
|
389
|
+
| Per-request xAI calls fail with `XaiAccessUnavailableError` | Confirm `xaiOAuth.mode` is `"authority"` on the host `connectorServer` entry and `"consumer"` on the `opencode-ext-connector/xai` subpath entry, the OpenCode auth record holds exactly `{"type":"api","key":"cli-session:xai"}`, and the access file at `${XDG_DATA_HOME}/opencode/xai-access.json` (or `${HOME}/.local/share/opencode/xai-access.json` on Linux when `XDG_DATA_HOME` is unset or empty) is a current-user regular single-link file with mode `0600`, valid closed v1, and a future `expires`. The error is raised before any network call. |
|
|
390
|
+
| Host xAI authority helper never runs | Verify `${HOME}/.local/bin/opensandbox-xai-auth-sync` exists, is executable, and exits `0` on success. The observer treats every non-zero exit as transient and retries after `5000` ms without removing the xAI provider. The helper delegates projection to the OpenSandbox manager. A relative `XDG_DATA_HOME` (set but not absolute) disables the authority without reading or invoking. |
|
|
303
391
|
| Host credentials exist but the guest provider is disconnected | Check the mount target and permissions, the guest's `HOME`, `CLAUDE_CONFIG_DIR`, injected secret environment, writable guest OpenCode `auth.json`, and whether `/connect` completed inside the guest. |
|
|
304
392
|
| Ollama works on the host but not in the guest | Guest `localhost` is usually not the host. Check `host.docker.internal` resolution, Linux `host-gateway` mapping, the daemon bind address, firewall and sandbox egress, and whether any base path prefix reaches Ollama's `/api/*` routes. |
|
|
305
393
|
| One provider is down | Failures are isolated. Transient snapshot failures keep the last-known catalog; an unavailable snapshot removes only that connector-owned provider. |
|
package/dist/core/options.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
import type { HealthPolicy } from "./health.js";
|
|
3
3
|
export type CredentialRefreshMode = "auto" | "never";
|
|
4
4
|
export type CredentialManagement = "connector" | "external";
|
|
5
|
+
export type CredentialRole = "owner" | "reader";
|
|
6
|
+
export type XaiOAuthMode = "authority" | "consumer";
|
|
7
|
+
export type XaiOAuthOptions = {
|
|
8
|
+
readonly mode: XaiOAuthMode;
|
|
9
|
+
};
|
|
5
10
|
export type CredentialRefreshPolicy = {
|
|
6
11
|
readonly mode: CredentialRefreshMode;
|
|
7
12
|
readonly leadMs: number;
|
|
@@ -17,6 +22,8 @@ export type CredentialAuthority = {
|
|
|
17
22
|
export type ConnectorOptionsInput = {
|
|
18
23
|
readonly providers?: readonly ("claude" | "cursor" | "command-code" | "ollama")[] | undefined;
|
|
19
24
|
readonly snapshotTimeoutMs?: number | undefined;
|
|
25
|
+
readonly credentialRole?: CredentialRole | undefined;
|
|
26
|
+
readonly xaiOAuth?: XaiOAuthOptions | undefined;
|
|
20
27
|
readonly credentialManagement?: CredentialManagement | undefined;
|
|
21
28
|
readonly credentialAuthority?: {
|
|
22
29
|
readonly claudeCli: {
|
|
@@ -44,6 +51,7 @@ export type ConnectorOptions = {
|
|
|
44
51
|
readonly writeBackCredentials: boolean;
|
|
45
52
|
readonly credentialRefresh: CredentialRefreshPolicy;
|
|
46
53
|
readonly credentialAuthority: CredentialAuthority;
|
|
54
|
+
readonly xaiOAuth: XaiOAuthOptions | null;
|
|
47
55
|
readonly catalogReloadMs: number;
|
|
48
56
|
readonly health: HealthPolicy;
|
|
49
57
|
};
|
package/dist/core/options.js
CHANGED
|
@@ -5,6 +5,11 @@ const NonNegativeSafeIntegerSchema = z.number().int().nonnegative().max(MaximumT
|
|
|
5
5
|
const ProviderSchema = z.enum(["claude", "cursor", "command-code", "ollama"]);
|
|
6
6
|
const CredentialRefreshModeSchema = z.enum(["auto", "never"]);
|
|
7
7
|
const CredentialManagementSchema = z.enum(["connector", "external"]);
|
|
8
|
+
const CredentialRoleSchema = z.enum(["owner", "reader"]);
|
|
9
|
+
const XaiOAuthSchema = z
|
|
10
|
+
.object({ mode: z.enum(["authority", "consumer"]) })
|
|
11
|
+
.strict()
|
|
12
|
+
.readonly();
|
|
8
13
|
const SafeIntegerSchema = z.number().int().safe();
|
|
9
14
|
const DefaultProviders = [
|
|
10
15
|
"claude",
|
|
@@ -26,7 +31,8 @@ const CredentialAuthorityInputSchema = z
|
|
|
26
31
|
.strict()
|
|
27
32
|
.readonly();
|
|
28
33
|
function resolveCredentialOptions(input) {
|
|
29
|
-
|
|
34
|
+
const credentialManagement = input.credentialRole === undefined ? input.credentialManagement : "external";
|
|
35
|
+
switch (credentialManagement) {
|
|
30
36
|
case undefined:
|
|
31
37
|
return {
|
|
32
38
|
credentialRefresh: Object.freeze({
|
|
@@ -51,6 +57,8 @@ const ConnectorOptionsInputSchema = z
|
|
|
51
57
|
.object({
|
|
52
58
|
providers: z.array(ProviderSchema).optional(),
|
|
53
59
|
snapshotTimeoutMs: PositiveSafeIntegerSchema.optional(),
|
|
60
|
+
credentialRole: CredentialRoleSchema.optional(),
|
|
61
|
+
xaiOAuth: XaiOAuthSchema.optional(),
|
|
54
62
|
credentialManagement: CredentialManagementSchema.optional(),
|
|
55
63
|
credentialAuthority: CredentialAuthorityInputSchema.optional(),
|
|
56
64
|
writeBackCredentials: z.boolean().optional(),
|
|
@@ -72,6 +80,17 @@ const ConnectorOptionsInputSchema = z
|
|
|
72
80
|
})
|
|
73
81
|
.strict()
|
|
74
82
|
.superRefine((input, context) => {
|
|
83
|
+
if (input.credentialRole !== undefined &&
|
|
84
|
+
(input.credentialManagement !== undefined ||
|
|
85
|
+
input.credentialAuthority !== undefined ||
|
|
86
|
+
input.credentialRefresh !== undefined ||
|
|
87
|
+
input.writeBackCredentials !== undefined)) {
|
|
88
|
+
context.addIssue({
|
|
89
|
+
code: "custom",
|
|
90
|
+
path: ["credentialRole"],
|
|
91
|
+
message: "`credentialRole` cannot be combined with `credentialManagement`, `credentialAuthority`, `credentialRefresh`, or `writeBackCredentials`",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
75
94
|
if (input.credentialManagement !== undefined &&
|
|
76
95
|
(input.credentialRefresh !== undefined || input.writeBackCredentials !== undefined)) {
|
|
77
96
|
context.addIssue({
|
|
@@ -89,6 +108,14 @@ const ConnectorOptionsInputSchema = z
|
|
|
89
108
|
message: "Claude CLI credential authority requires external credential management and the Claude provider",
|
|
90
109
|
});
|
|
91
110
|
}
|
|
111
|
+
if (input.credentialRole === "owner" &&
|
|
112
|
+
!(input.providers ?? DefaultProviders).includes("claude")) {
|
|
113
|
+
context.addIssue({
|
|
114
|
+
code: "custom",
|
|
115
|
+
path: ["credentialRole"],
|
|
116
|
+
message: "Credential owner role requires the Claude provider",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
92
119
|
const initialBackoffMs = input.health?.initialBackoffMs ?? 1_000;
|
|
93
120
|
const maximumBackoffMs = input.health?.maximumBackoffMs ?? 60_000;
|
|
94
121
|
if (initialBackoffMs > maximumBackoffMs) {
|
|
@@ -103,17 +130,21 @@ export const ConnectorOptionsSchema = ConnectorOptionsInputSchema.transform((inp
|
|
|
103
130
|
const credentialOptions = resolveCredentialOptions(input);
|
|
104
131
|
const credentialAuthority = Object.freeze({
|
|
105
132
|
claudeCli: Object.freeze({
|
|
106
|
-
enabled: input.
|
|
133
|
+
enabled: input.credentialRole === "owner"
|
|
134
|
+
? true
|
|
135
|
+
: (input.credentialAuthority?.claudeCli.enabled ?? false),
|
|
107
136
|
leadMs: input.credentialAuthority?.claudeCli.leadMs ?? 300_000,
|
|
108
137
|
retryMs: input.credentialAuthority?.claudeCli.retryMs ?? 300_000,
|
|
109
138
|
}),
|
|
110
139
|
});
|
|
140
|
+
const xaiOAuth = input.xaiOAuth === undefined ? null : Object.freeze(input.xaiOAuth);
|
|
111
141
|
return Object.freeze({
|
|
112
142
|
providers: Object.freeze(input.providers ?? DefaultProviders),
|
|
113
143
|
snapshotTimeoutMs: input.snapshotTimeoutMs ?? 30_000,
|
|
114
144
|
writeBackCredentials: credentialOptions.writeBackCredentials,
|
|
115
145
|
credentialRefresh: credentialOptions.credentialRefresh,
|
|
116
146
|
credentialAuthority,
|
|
147
|
+
xaiOAuth,
|
|
117
148
|
catalogReloadMs: input.catalogReloadMs ?? 300_000,
|
|
118
149
|
health,
|
|
119
150
|
});
|
package/dist/core/process.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export const claudeAuthServer = async (input, options) => (await import("./serve
|
|
|
3
3
|
export const cursorAuthServer = async (input, options) => (await import("./server.js")).cursorAuthServer(input, options);
|
|
4
4
|
export const commandCodeAuthServer = async (input, options) => (await import("./server.js")).commandCodeAuthServer(input, options);
|
|
5
5
|
export const ollamaAuthServer = async (input, options) => (await import("./server.js")).ollamaAuthServer(input, options);
|
|
6
|
+
export const xaiAuthServer = async (input, options) => (await import("./server.js")).xaiAuthServer(input, options);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ConnectorOptionsInput } from "../core/options.js";
|
|
2
|
-
type HostConnectorOptionsInput = Omit<ConnectorOptionsInput, "credentialAuthority" | "credentialManagement" | "credentialRefresh" | "writeBackCredentials"> & {
|
|
2
|
+
type HostConnectorOptionsInput = Omit<ConnectorOptionsInput, "credentialAuthority" | "credentialManagement" | "credentialRefresh" | "credentialRole" | "xaiOAuth" | "writeBackCredentials"> & {
|
|
3
3
|
readonly credentialAuthority?: unknown;
|
|
4
4
|
readonly credentialManagement?: unknown;
|
|
5
5
|
readonly credentialRefresh?: unknown;
|
|
6
|
+
readonly credentialRole?: unknown;
|
|
7
|
+
readonly xaiOAuth?: unknown;
|
|
6
8
|
readonly writeBackCredentials?: unknown;
|
|
7
9
|
};
|
|
8
10
|
export declare function pickConnectorOptionsInput(input: unknown): HostConnectorOptionsInput;
|
|
@@ -28,8 +28,10 @@ export function pickConnectorOptionsInput(input) {
|
|
|
28
28
|
if (typeof input !== "object" || input === null) {
|
|
29
29
|
return {};
|
|
30
30
|
}
|
|
31
|
-
const preservesCredentialPolicy = "credentialManagement" in input &&
|
|
32
|
-
(input.credentialManagement === "connector" || input.credentialManagement === "external")
|
|
31
|
+
const preservesCredentialPolicy = ("credentialManagement" in input &&
|
|
32
|
+
(input.credentialManagement === "connector" || input.credentialManagement === "external")) ||
|
|
33
|
+
("credentialRole" in input &&
|
|
34
|
+
(input.credentialRole === "owner" || input.credentialRole === "reader"));
|
|
33
35
|
const writeBackCredentials = "writeBackCredentials" in input && input.writeBackCredentials !== undefined
|
|
34
36
|
? preservesCredentialPolicy
|
|
35
37
|
? input.writeBackCredentials
|
|
@@ -51,9 +53,13 @@ export function pickConnectorOptionsInput(input) {
|
|
|
51
53
|
: undefined,
|
|
52
54
|
snapshotTimeoutMs: "snapshotTimeoutMs" in input ? positiveInteger(input.snapshotTimeoutMs) : undefined,
|
|
53
55
|
...(writeBackCredentials === undefined ? {} : { writeBackCredentials }),
|
|
56
|
+
...(!("credentialRole" in input) || input.credentialRole === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { credentialRole: input.credentialRole }),
|
|
54
59
|
...(!("credentialManagement" in input) || input.credentialManagement === undefined
|
|
55
60
|
? {}
|
|
56
61
|
: { credentialManagement: input.credentialManagement }),
|
|
62
|
+
...(!("xaiOAuth" in input) || input.xaiOAuth === undefined ? {} : { xaiOAuth: input.xaiOAuth }),
|
|
57
63
|
...(!("credentialAuthority" in input) || input.credentialAuthority === undefined
|
|
58
64
|
? {}
|
|
59
65
|
: { credentialAuthority: input.credentialAuthority }),
|
|
@@ -12,6 +12,7 @@ function spawnNodeChild(command, options) {
|
|
|
12
12
|
shell: options.shell,
|
|
13
13
|
stdio: options.stdio,
|
|
14
14
|
windowsHide: options.windowsHide,
|
|
15
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
15
16
|
});
|
|
16
17
|
return {
|
|
17
18
|
get exitCode() {
|
|
@@ -136,7 +137,9 @@ export function createProductionProcessSupervisor(options = {}) {
|
|
|
136
137
|
throw new ResourceDisposedError("process-supervisor");
|
|
137
138
|
let child;
|
|
138
139
|
try {
|
|
139
|
-
child = spawnProcess(command,
|
|
140
|
+
child = spawnProcess(command, command.environment === undefined
|
|
141
|
+
? spawnOptions
|
|
142
|
+
: { ...spawnOptions, env: command.environment });
|
|
140
143
|
}
|
|
141
144
|
catch (error) {
|
|
142
145
|
throw processFailure("spawn", error);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type XaiAccessState = {
|
|
2
|
+
readonly kind: "ready";
|
|
3
|
+
readonly access: string;
|
|
4
|
+
readonly expires: number;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: "unavailable";
|
|
7
|
+
};
|
|
8
|
+
export type XaiAccessFileMetadata = {
|
|
9
|
+
readonly regular: boolean;
|
|
10
|
+
readonly links: number;
|
|
11
|
+
readonly mode: number;
|
|
12
|
+
readonly ownerUid: number;
|
|
13
|
+
};
|
|
14
|
+
export interface XaiAccessFile {
|
|
15
|
+
stat(): Promise<XaiAccessFileMetadata>;
|
|
16
|
+
readText(): Promise<string>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export type XaiAccessStateOptions = {
|
|
20
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
21
|
+
readonly currentUid?: () => number | undefined;
|
|
22
|
+
readonly openFile?: (path: string) => Promise<XaiAccessFile>;
|
|
23
|
+
};
|
|
24
|
+
export declare function resolveXaiAccessPath(env: Readonly<Record<string, string | undefined>>): string | null;
|
|
25
|
+
export declare function readXaiAccessState(options: XaiAccessStateOptions): Promise<XaiAccessState>;
|