opencode-ext-connector 0.6.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 +23 -0
- package/README.md +62 -3
- package/dist/core/options.d.ts +6 -0
- package/dist/core/options.js +7 -0
- 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 +2 -1
- package/dist/opencode/host-options.js +1 -0
- 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 +62 -3
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
3
26
|
## 0.6.0 - 2026-09-16
|
|
4
27
|
|
|
5
28
|
- Add the recommended `credentialRole: "owner" | "reader"` option for shared
|
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
|
|
|
@@ -78,6 +78,7 @@ Omitted `providers` enables all four. An explicit list is a strict allow-list. E
|
|
|
78
78
|
| `writeBackCredentials` | `false` | **Deprecated:** accepted alone for one migration cycle; controls Claude writeback after refresh |
|
|
79
79
|
| `credentialRefresh.mode` | `"auto"` | **Deprecated:** accepted alone for one migration cycle; controls Claude `"auto"` or `"never"` refresh behavior |
|
|
80
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. |
|
|
81
82
|
| `credentialAuthority.claudeCli.enabled` | `false` | Claude-only opt-in: see [Opt-in Claude CLI authority timer](#opt-in-claude-cli-authority-timer-claude-only) |
|
|
82
83
|
| `credentialAuthority.claudeCli.leadMs` | `300000` | Claude-only opt-in: milliseconds before credential expiry at which the timer invokes `claude`; non-negative integer |
|
|
83
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 |
|
|
@@ -211,6 +212,58 @@ Each invocation is a real model request and can count against the account's subs
|
|
|
211
212
|
|
|
212
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.
|
|
213
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
|
+
|
|
214
267
|
## Host/Guest Sandbox Setup
|
|
215
268
|
|
|
216
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.
|
|
@@ -223,6 +276,7 @@ The safest shared-session layout keeps each vendor login owned and refreshed by
|
|
|
223
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 |
|
|
224
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 |
|
|
225
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 |
|
|
226
280
|
|
|
227
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:
|
|
228
282
|
|
|
@@ -311,11 +365,14 @@ Ollama `/connect` probes the configured daemon and stores the exact session mark
|
|
|
311
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`. |
|
|
312
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`. |
|
|
313
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). |
|
|
314
369
|
|
|
315
370
|
Provider health is isolated: one provider failing does not remove the others.
|
|
316
371
|
|
|
317
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.
|
|
318
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
|
+
|
|
319
376
|
## Troubleshooting
|
|
320
377
|
|
|
321
378
|
| Symptom | What to check |
|
|
@@ -329,6 +386,8 @@ The standalone SDK entry is `opencode-ext-connector/ollama`; pass `{ ollamaBaseU
|
|
|
329
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. |
|
|
330
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. |
|
|
331
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. |
|
|
332
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. |
|
|
333
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. |
|
|
334
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
|
@@ -3,6 +3,10 @@ import type { HealthPolicy } from "./health.js";
|
|
|
3
3
|
export type CredentialRefreshMode = "auto" | "never";
|
|
4
4
|
export type CredentialManagement = "connector" | "external";
|
|
5
5
|
export type CredentialRole = "owner" | "reader";
|
|
6
|
+
export type XaiOAuthMode = "authority" | "consumer";
|
|
7
|
+
export type XaiOAuthOptions = {
|
|
8
|
+
readonly mode: XaiOAuthMode;
|
|
9
|
+
};
|
|
6
10
|
export type CredentialRefreshPolicy = {
|
|
7
11
|
readonly mode: CredentialRefreshMode;
|
|
8
12
|
readonly leadMs: number;
|
|
@@ -19,6 +23,7 @@ export type ConnectorOptionsInput = {
|
|
|
19
23
|
readonly providers?: readonly ("claude" | "cursor" | "command-code" | "ollama")[] | undefined;
|
|
20
24
|
readonly snapshotTimeoutMs?: number | undefined;
|
|
21
25
|
readonly credentialRole?: CredentialRole | undefined;
|
|
26
|
+
readonly xaiOAuth?: XaiOAuthOptions | undefined;
|
|
22
27
|
readonly credentialManagement?: CredentialManagement | undefined;
|
|
23
28
|
readonly credentialAuthority?: {
|
|
24
29
|
readonly claudeCli: {
|
|
@@ -46,6 +51,7 @@ export type ConnectorOptions = {
|
|
|
46
51
|
readonly writeBackCredentials: boolean;
|
|
47
52
|
readonly credentialRefresh: CredentialRefreshPolicy;
|
|
48
53
|
readonly credentialAuthority: CredentialAuthority;
|
|
54
|
+
readonly xaiOAuth: XaiOAuthOptions | null;
|
|
49
55
|
readonly catalogReloadMs: number;
|
|
50
56
|
readonly health: HealthPolicy;
|
|
51
57
|
};
|
package/dist/core/options.js
CHANGED
|
@@ -6,6 +6,10 @@ 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
8
|
const CredentialRoleSchema = z.enum(["owner", "reader"]);
|
|
9
|
+
const XaiOAuthSchema = z
|
|
10
|
+
.object({ mode: z.enum(["authority", "consumer"]) })
|
|
11
|
+
.strict()
|
|
12
|
+
.readonly();
|
|
9
13
|
const SafeIntegerSchema = z.number().int().safe();
|
|
10
14
|
const DefaultProviders = [
|
|
11
15
|
"claude",
|
|
@@ -54,6 +58,7 @@ const ConnectorOptionsInputSchema = z
|
|
|
54
58
|
providers: z.array(ProviderSchema).optional(),
|
|
55
59
|
snapshotTimeoutMs: PositiveSafeIntegerSchema.optional(),
|
|
56
60
|
credentialRole: CredentialRoleSchema.optional(),
|
|
61
|
+
xaiOAuth: XaiOAuthSchema.optional(),
|
|
57
62
|
credentialManagement: CredentialManagementSchema.optional(),
|
|
58
63
|
credentialAuthority: CredentialAuthorityInputSchema.optional(),
|
|
59
64
|
writeBackCredentials: z.boolean().optional(),
|
|
@@ -132,12 +137,14 @@ export const ConnectorOptionsSchema = ConnectorOptionsInputSchema.transform((inp
|
|
|
132
137
|
retryMs: input.credentialAuthority?.claudeCli.retryMs ?? 300_000,
|
|
133
138
|
}),
|
|
134
139
|
});
|
|
140
|
+
const xaiOAuth = input.xaiOAuth === undefined ? null : Object.freeze(input.xaiOAuth);
|
|
135
141
|
return Object.freeze({
|
|
136
142
|
providers: Object.freeze(input.providers ?? DefaultProviders),
|
|
137
143
|
snapshotTimeoutMs: input.snapshotTimeoutMs ?? 30_000,
|
|
138
144
|
writeBackCredentials: credentialOptions.writeBackCredentials,
|
|
139
145
|
credentialRefresh: credentialOptions.credentialRefresh,
|
|
140
146
|
credentialAuthority,
|
|
147
|
+
xaiOAuth,
|
|
141
148
|
catalogReloadMs: input.catalogReloadMs ?? 300_000,
|
|
142
149
|
health,
|
|
143
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,9 +1,10 @@
|
|
|
1
1
|
import type { ConnectorOptionsInput } from "../core/options.js";
|
|
2
|
-
type HostConnectorOptionsInput = Omit<ConnectorOptionsInput, "credentialAuthority" | "credentialManagement" | "credentialRefresh" | "credentialRole" | "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
6
|
readonly credentialRole?: unknown;
|
|
7
|
+
readonly xaiOAuth?: unknown;
|
|
7
8
|
readonly writeBackCredentials?: unknown;
|
|
8
9
|
};
|
|
9
10
|
export declare function pickConnectorOptionsInput(input: unknown): HostConnectorOptionsInput;
|
|
@@ -59,6 +59,7 @@ export function pickConnectorOptionsInput(input) {
|
|
|
59
59
|
...(!("credentialManagement" in input) || input.credentialManagement === undefined
|
|
60
60
|
? {}
|
|
61
61
|
: { credentialManagement: input.credentialManagement }),
|
|
62
|
+
...(!("xaiOAuth" in input) || input.xaiOAuth === undefined ? {} : { xaiOAuth: input.xaiOAuth }),
|
|
62
63
|
...(!("credentialAuthority" in input) || input.credentialAuthority === undefined
|
|
63
64
|
? {}
|
|
64
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>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const XaiAccessRecordSchema = z.discriminatedUnion("state", [
|
|
6
|
+
z
|
|
7
|
+
.object({
|
|
8
|
+
schema_version: z.literal(1),
|
|
9
|
+
provider: z.literal("xai"),
|
|
10
|
+
state: z.literal("ready"),
|
|
11
|
+
access: z.string().min(1),
|
|
12
|
+
expires: z.number().int().nonnegative(),
|
|
13
|
+
})
|
|
14
|
+
.strict()
|
|
15
|
+
.readonly(),
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
schema_version: z.literal(1),
|
|
19
|
+
provider: z.literal("xai"),
|
|
20
|
+
state: z.literal("unavailable"),
|
|
21
|
+
})
|
|
22
|
+
.strict()
|
|
23
|
+
.readonly(),
|
|
24
|
+
]);
|
|
25
|
+
const UnavailableState = Object.freeze({ kind: "unavailable" });
|
|
26
|
+
class XaiAccessStateInvariantError extends Error {
|
|
27
|
+
name = "XaiAccessStateInvariantError";
|
|
28
|
+
constructor() {
|
|
29
|
+
super("unexpected xAI access state");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function assertNeverAccessState(_state) {
|
|
33
|
+
throw new XaiAccessStateInvariantError();
|
|
34
|
+
}
|
|
35
|
+
function currentProcessUid() {
|
|
36
|
+
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
37
|
+
}
|
|
38
|
+
async function openAccessFile(path) {
|
|
39
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
40
|
+
return {
|
|
41
|
+
stat: async () => {
|
|
42
|
+
const metadata = await handle.stat();
|
|
43
|
+
return {
|
|
44
|
+
regular: metadata.isFile(),
|
|
45
|
+
links: metadata.nlink,
|
|
46
|
+
mode: metadata.mode & 0o7777,
|
|
47
|
+
ownerUid: metadata.uid,
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
readText: () => handle.readFile("utf8"),
|
|
51
|
+
close: () => handle.close(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function resolveXaiAccessPath(env) {
|
|
55
|
+
const dataHome = env["XDG_DATA_HOME"];
|
|
56
|
+
if (dataHome !== undefined && dataHome.length > 0) {
|
|
57
|
+
return isAbsolute(dataHome) ? join(dataHome, "opencode", "xai-access.json") : null;
|
|
58
|
+
}
|
|
59
|
+
const home = env["HOME"];
|
|
60
|
+
return home !== undefined && home.length > 0 && isAbsolute(home)
|
|
61
|
+
? join(home, ".local", "share", "opencode", "xai-access.json")
|
|
62
|
+
: null;
|
|
63
|
+
}
|
|
64
|
+
function parsedState(raw) {
|
|
65
|
+
const parsedJson = JSON.parse(raw);
|
|
66
|
+
const record = XaiAccessRecordSchema.safeParse(parsedJson);
|
|
67
|
+
if (!record.success)
|
|
68
|
+
return UnavailableState;
|
|
69
|
+
switch (record.data.state) {
|
|
70
|
+
case "ready":
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
kind: "ready",
|
|
73
|
+
access: record.data.access,
|
|
74
|
+
expires: record.data.expires,
|
|
75
|
+
});
|
|
76
|
+
case "unavailable":
|
|
77
|
+
return UnavailableState;
|
|
78
|
+
default:
|
|
79
|
+
return assertNeverAccessState(record.data);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function readXaiAccessState(options) {
|
|
83
|
+
const path = resolveXaiAccessPath(options.env);
|
|
84
|
+
if (path === null)
|
|
85
|
+
return UnavailableState;
|
|
86
|
+
const openFile = options.openFile ?? openAccessFile;
|
|
87
|
+
let file;
|
|
88
|
+
try {
|
|
89
|
+
file = await openFile(path);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof Error)
|
|
93
|
+
return UnavailableState;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
let state = UnavailableState;
|
|
97
|
+
try {
|
|
98
|
+
const metadata = await file.stat();
|
|
99
|
+
const runtimeUid = (options.currentUid ?? currentProcessUid)();
|
|
100
|
+
const ownerMatches = runtimeUid === undefined || metadata.ownerUid === runtimeUid;
|
|
101
|
+
if (metadata.regular && metadata.links === 1 && metadata.mode === 0o600 && ownerMatches) {
|
|
102
|
+
state = parsedState(await file.readText());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!(error instanceof Error))
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await file.close();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof Error)
|
|
114
|
+
return UnavailableState;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Clock } from "../../core/clock.js";
|
|
2
|
+
import { type AsyncDisposableHandle } from "../../core/lifecycle.js";
|
|
3
|
+
import type { ProcessSupervisor } from "../../core/process.js";
|
|
4
|
+
export type XaiAuthorityObserverOptions = {
|
|
5
|
+
readonly enabled: boolean;
|
|
6
|
+
readonly clock: Clock;
|
|
7
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
8
|
+
readonly processSupervisor: ProcessSupervisor;
|
|
9
|
+
readonly pollMs?: number;
|
|
10
|
+
readonly retryMs?: number;
|
|
11
|
+
readonly readAuthFile?: (path: string) => Promise<string>;
|
|
12
|
+
};
|
|
13
|
+
export declare function createXaiAuthorityObserver(options: XaiAuthorityObserverOptions): AsyncDisposableHandle;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { InvalidArgumentError, OperationCancelledError } from "../../core/errors.js";
|
|
5
|
+
import { createAsyncDisposable } from "../../core/lifecycle.js";
|
|
6
|
+
import { opencodeAuthJsonPaths } from "../../opencode/auth-store.js";
|
|
7
|
+
const AUTH_SYNC_HELPER = "opensandbox-xai-auth-sync";
|
|
8
|
+
const AUTH_SYNC_PATH = "/usr/local/bin:/usr/bin:/bin";
|
|
9
|
+
const AuthRootSchema = z.record(z.string(), z.unknown());
|
|
10
|
+
function errorCode(error) {
|
|
11
|
+
return "code" in error ? Reflect.get(error, "code") : undefined;
|
|
12
|
+
}
|
|
13
|
+
function commandEnvironment(env, home) {
|
|
14
|
+
const dataHome = env["XDG_DATA_HOME"];
|
|
15
|
+
return dataHome === undefined || dataHome.length === 0 || !isAbsolute(dataHome)
|
|
16
|
+
? Object.freeze({ HOME: home, PATH: AUTH_SYNC_PATH })
|
|
17
|
+
: Object.freeze({ HOME: home, PATH: AUTH_SYNC_PATH, XDG_DATA_HOME: dataHome });
|
|
18
|
+
}
|
|
19
|
+
class XaiAuthorityInvariantError extends Error {
|
|
20
|
+
name = "XaiAuthorityInvariantError";
|
|
21
|
+
constructor() {
|
|
22
|
+
super("unexpected xAI authority state");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function assertNeverAuthorityState(_state) {
|
|
26
|
+
throw new XaiAuthorityInvariantError();
|
|
27
|
+
}
|
|
28
|
+
export function createXaiAuthorityObserver(options) {
|
|
29
|
+
const pollMs = options.pollMs ?? 1_000;
|
|
30
|
+
const retryMs = options.retryMs ?? 5_000;
|
|
31
|
+
if (!Number.isSafeInteger(pollMs) || pollMs <= 0)
|
|
32
|
+
throw new InvalidArgumentError("pollMs");
|
|
33
|
+
if (!Number.isSafeInteger(retryMs) || retryMs <= 0)
|
|
34
|
+
throw new InvalidArgumentError("retryMs");
|
|
35
|
+
if (!options.enabled)
|
|
36
|
+
return createAsyncDisposable(() => undefined);
|
|
37
|
+
const home = options.env["HOME"];
|
|
38
|
+
if (home === undefined || home.length === 0 || !isAbsolute(home)) {
|
|
39
|
+
return createAsyncDisposable(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
const dataHome = options.env["XDG_DATA_HOME"];
|
|
42
|
+
if (dataHome !== undefined && dataHome.length > 0 && !isAbsolute(dataHome)) {
|
|
43
|
+
return createAsyncDisposable(() => undefined);
|
|
44
|
+
}
|
|
45
|
+
const [authPath] = opencodeAuthJsonPaths(options.env);
|
|
46
|
+
if (authPath === undefined || !isAbsolute(authPath))
|
|
47
|
+
return createAsyncDisposable(() => undefined);
|
|
48
|
+
const helper = join(home, ".local", "bin", AUTH_SYNC_HELPER);
|
|
49
|
+
const environment = commandEnvironment(options.env, home);
|
|
50
|
+
const loadAuth = options.readAuthFile ?? ((path) => readFile(path, "utf8"));
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
let scheduled;
|
|
53
|
+
let activeProcess;
|
|
54
|
+
let activeEvaluation;
|
|
55
|
+
let synchronizedFingerprint;
|
|
56
|
+
let disposalStarted = false;
|
|
57
|
+
const arm = (delayMs) => {
|
|
58
|
+
if (disposalStarted)
|
|
59
|
+
return;
|
|
60
|
+
scheduled?.cancel();
|
|
61
|
+
scheduled = options.clock.schedule(delayMs, () => {
|
|
62
|
+
scheduled = undefined;
|
|
63
|
+
startEvaluation();
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const observe = async () => {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await loadAuth(authPath);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof Error && errorCode(error) === "ENOENT") {
|
|
73
|
+
return { kind: "ready", fingerprint: "null" };
|
|
74
|
+
}
|
|
75
|
+
if (error instanceof Error)
|
|
76
|
+
return { kind: "retry" };
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const parsedJson = JSON.parse(raw);
|
|
81
|
+
const root = AuthRootSchema.safeParse(parsedJson);
|
|
82
|
+
return root.success
|
|
83
|
+
? { kind: "ready", fingerprint: JSON.stringify(root.data["xai"] ?? null) }
|
|
84
|
+
: { kind: "retry" };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof SyntaxError)
|
|
88
|
+
return { kind: "retry" };
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const invoke = async () => {
|
|
93
|
+
const process = await options.processSupervisor.start({ executable: helper, arguments: [], cwd: null, environment }, controller.signal);
|
|
94
|
+
activeProcess = process;
|
|
95
|
+
try {
|
|
96
|
+
const exit = await process.wait(controller.signal);
|
|
97
|
+
switch (exit.kind) {
|
|
98
|
+
case "code":
|
|
99
|
+
return exit.code === 0;
|
|
100
|
+
case "signal":
|
|
101
|
+
return false;
|
|
102
|
+
default:
|
|
103
|
+
return assertNeverAuthorityState(exit);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
activeProcess = undefined;
|
|
108
|
+
await process.dispose();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const evaluate = async () => {
|
|
112
|
+
const observation = await observe();
|
|
113
|
+
if (disposalStarted)
|
|
114
|
+
return;
|
|
115
|
+
switch (observation.kind) {
|
|
116
|
+
case "retry":
|
|
117
|
+
arm(retryMs);
|
|
118
|
+
return;
|
|
119
|
+
case "ready":
|
|
120
|
+
if (observation.fingerprint === synchronizedFingerprint) {
|
|
121
|
+
arm(pollMs);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (await invoke())
|
|
125
|
+
synchronizedFingerprint = observation.fingerprint;
|
|
126
|
+
if (!disposalStarted) {
|
|
127
|
+
arm(observation.fingerprint === synchronizedFingerprint ? pollMs : retryMs);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
default:
|
|
131
|
+
return assertNeverAuthorityState(observation);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const startEvaluation = () => {
|
|
135
|
+
if (disposalStarted || activeEvaluation !== undefined)
|
|
136
|
+
return;
|
|
137
|
+
const operation = evaluate().catch((error) => {
|
|
138
|
+
if (error instanceof OperationCancelledError && disposalStarted)
|
|
139
|
+
return;
|
|
140
|
+
if (error instanceof Error) {
|
|
141
|
+
arm(retryMs);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
});
|
|
146
|
+
activeEvaluation = operation;
|
|
147
|
+
void operation.then(() => {
|
|
148
|
+
if (activeEvaluation === operation)
|
|
149
|
+
activeEvaluation = undefined;
|
|
150
|
+
}, () => {
|
|
151
|
+
if (activeEvaluation === operation)
|
|
152
|
+
activeEvaluation = undefined;
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const disposal = createAsyncDisposable(async () => {
|
|
156
|
+
disposalStarted = true;
|
|
157
|
+
scheduled?.cancel();
|
|
158
|
+
scheduled = undefined;
|
|
159
|
+
controller.abort();
|
|
160
|
+
await activeProcess?.terminate();
|
|
161
|
+
await activeEvaluation;
|
|
162
|
+
});
|
|
163
|
+
startEvaluation();
|
|
164
|
+
return { dispose: disposal.dispose, [Symbol.asyncDispose]: disposal[Symbol.asyncDispose] };
|
|
165
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Clock } from "../../core/clock.js";
|
|
2
|
+
export type NetworkFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
export type XaiConsumerAuthOptions = {
|
|
4
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
5
|
+
readonly clock: Clock;
|
|
6
|
+
readonly networkFetch: NetworkFetch;
|
|
7
|
+
};
|
|
8
|
+
export type XaiConsumerAuthHook = {
|
|
9
|
+
readonly provider: "xai";
|
|
10
|
+
readonly loader: (getAuth: () => Promise<unknown>) => Promise<{
|
|
11
|
+
readonly apiKey: string;
|
|
12
|
+
readonly fetch: NetworkFetch;
|
|
13
|
+
} | {
|
|
14
|
+
readonly apiKey?: never;
|
|
15
|
+
readonly fetch?: never;
|
|
16
|
+
}>;
|
|
17
|
+
readonly methods: [];
|
|
18
|
+
};
|
|
19
|
+
export declare class XaiAccessUnavailableError extends Error {
|
|
20
|
+
readonly name = "XaiAccessUnavailableError";
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
23
|
+
export declare function createXaiConsumerAuth(options: XaiConsumerAuthOptions): XaiConsumerAuthHook;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readXaiAccessState } from "./access-state.js";
|
|
3
|
+
const XAI_CONSUMER_MARKER = "cli-session:xai";
|
|
4
|
+
const XAI_DUMMY_API_KEY = "xai-access-file";
|
|
5
|
+
const XaiConsumerMarkerSchema = z
|
|
6
|
+
.object({ type: z.literal("api"), key: z.literal(XAI_CONSUMER_MARKER) })
|
|
7
|
+
.strict();
|
|
8
|
+
export class XaiAccessUnavailableError extends Error {
|
|
9
|
+
name = "XaiAccessUnavailableError";
|
|
10
|
+
constructor() {
|
|
11
|
+
super("xAI consumer access is unavailable");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function assertNeverAccessState(_state) {
|
|
15
|
+
throw new XaiAccessUnavailableError();
|
|
16
|
+
}
|
|
17
|
+
function createConsumerFetch(options) {
|
|
18
|
+
return async (input, init) => {
|
|
19
|
+
const state = await readXaiAccessState({ env: options.env });
|
|
20
|
+
let access;
|
|
21
|
+
switch (state.kind) {
|
|
22
|
+
case "ready":
|
|
23
|
+
if (state.expires <= options.clock.nowMs())
|
|
24
|
+
throw new XaiAccessUnavailableError();
|
|
25
|
+
access = state.access;
|
|
26
|
+
break;
|
|
27
|
+
case "unavailable":
|
|
28
|
+
throw new XaiAccessUnavailableError();
|
|
29
|
+
default:
|
|
30
|
+
return assertNeverAccessState(state);
|
|
31
|
+
}
|
|
32
|
+
const headers = new Headers(input instanceof Request ? input.headers : undefined);
|
|
33
|
+
const initHeaders = new Headers(init?.headers);
|
|
34
|
+
initHeaders.forEach((value, key) => {
|
|
35
|
+
headers.set(key, value);
|
|
36
|
+
});
|
|
37
|
+
headers.set("authorization", `Bearer ${access}`);
|
|
38
|
+
return options.networkFetch(input, init === undefined ? { headers } : { ...init, headers });
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function createXaiConsumerAuth(options) {
|
|
42
|
+
return {
|
|
43
|
+
provider: "xai",
|
|
44
|
+
loader: async (getAuth) => {
|
|
45
|
+
const marker = XaiConsumerMarkerSchema.safeParse(await getAuth());
|
|
46
|
+
return marker.success
|
|
47
|
+
? { apiKey: XAI_DUMMY_API_KEY, fetch: createConsumerFetch(options) }
|
|
48
|
+
: {};
|
|
49
|
+
},
|
|
50
|
+
methods: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const claudeAuthServer: V1Plugin;
|
|
|
4
4
|
export declare const cursorAuthServer: V1Plugin;
|
|
5
5
|
export declare const commandCodeAuthServer: V1Plugin;
|
|
6
6
|
export declare const ollamaAuthServer: V1Plugin;
|
|
7
|
+
export declare const xaiAuthServer: V1Plugin;
|
|
7
8
|
export type ConnectorPluginModule = {
|
|
8
9
|
readonly id: "opencode-ext-connector";
|
|
9
10
|
readonly server: V1Plugin;
|
package/dist/server.js
CHANGED
|
@@ -14,6 +14,8 @@ import { createProductionProcessSupervisor } from "./process/production-supervis
|
|
|
14
14
|
import { createClaudeCredentialAuthorityScheduler } from "./providers/claude/credential-authority-scheduler.js";
|
|
15
15
|
import { writeClaudeCredentials } from "./providers/claude/writeback.js";
|
|
16
16
|
import { productionOllamaFetch } from "./providers/ollama/http.js";
|
|
17
|
+
import { createXaiAuthorityObserver } from "./providers/xai/authority-observer.js";
|
|
18
|
+
import { createXaiConsumerAuth } from "./providers/xai/consumer-auth.js";
|
|
17
19
|
const env = process.env;
|
|
18
20
|
const transport = createFetchHttpTransport();
|
|
19
21
|
const authStore = createOpenCodeAuthStore({ env });
|
|
@@ -86,10 +88,17 @@ export const connectorServer = async (input, options) => {
|
|
|
86
88
|
processSupervisor,
|
|
87
89
|
logger,
|
|
88
90
|
});
|
|
91
|
+
const xaiAuthority = createXaiAuthorityObserver({
|
|
92
|
+
enabled: connectorOptions.xaiOAuth?.mode === "authority",
|
|
93
|
+
clock,
|
|
94
|
+
env,
|
|
95
|
+
processSupervisor,
|
|
96
|
+
});
|
|
89
97
|
const dispose = hooks.dispose;
|
|
90
98
|
const disposal = createAsyncDisposable(async () => {
|
|
91
99
|
const results = await Promise.allSettled([
|
|
92
100
|
Promise.resolve().then(() => credentialAuthority.dispose()),
|
|
101
|
+
Promise.resolve().then(() => xaiAuthority.dispose()),
|
|
93
102
|
Promise.resolve().then(() => processSupervisor.dispose()),
|
|
94
103
|
Promise.resolve().then(() => dispose?.()),
|
|
95
104
|
Promise.resolve().then(disposeV1LanguageRuntime),
|
|
@@ -129,6 +138,12 @@ export const ollamaAuthServer = async (_input, options) => {
|
|
|
129
138
|
}).find((candidate) => candidate.id === "ollama");
|
|
130
139
|
return entry === undefined ? {} : buildV1AuthHooks(entry, providerDeps, options);
|
|
131
140
|
};
|
|
141
|
+
export const xaiAuthServer = async (_input, options) => {
|
|
142
|
+
const connectorOptions = parseConnectorOptions(pickConnectorOptionsInput(options));
|
|
143
|
+
return connectorOptions.xaiOAuth?.mode === "consumer"
|
|
144
|
+
? { auth: createXaiConsumerAuth({ env, clock, networkFetch: globalThis.fetch }) }
|
|
145
|
+
: {};
|
|
146
|
+
};
|
|
132
147
|
export const plugin = {
|
|
133
148
|
id: "opencode-ext-connector",
|
|
134
149
|
server: connectorServer,
|
package/dist/xai.d.ts
ADDED
package/dist/xai.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const xaiAuthServer = async (input, options) => (await import("./server.js")).xaiAuthServer(input, options);
|
package/docs/README.ko.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
## ์ํ
|
|
21
21
|
|
|
22
|
-
> ๋
๋ฆฝ์ ์ธ ๋น๊ณต์ ์ปค๋ฎค๋ํฐ ํ๋ฌ๊ทธ์ธ, ๋ฒ์ **0.
|
|
22
|
+
> ๋
๋ฆฝ์ ์ธ ๋น๊ณต์ ์ปค๋ฎค๋ํฐ ํ๋ฌ๊ทธ์ธ, ๋ฒ์ **0.7.0**. CI์ ์ค์น๋ OpenCode CLI๋ฅผ ๋์์ผ๋ก legacy multi-function ๋ก๋๋ฅผ ํจํค์ง E2E ํ
์คํธ๋ก ๊ฒ์ฆํฉ๋๋ค. `@opencode-ai/plugin@1.18.18`์ ์ปดํ์ผ ์ ์ฌ์ฉํ๋ ํ๋ฌ๊ทธ์ธ API ๋์์ด๋ฉฐ OpenCode ๋ฐํ์ ๋ฒ์ ๊ณ ์ ์ด ์๋๋๋ค. ์์ค๋ BSD-3-Clause์
๋๋ค. ์ด ํ๋ก์ ํธ๋ OpenCode ๋๋ ์ด๋ค ํ๋ก๋ฐ์ด๋์๋ ์ ํด, ๋ณด์ฆ, ํ์, ์น์ธ ๊ด๊ณ๊ฐ ์์ต๋๋ค. ์ ์ฒด ์กฐ๊ฑด์ [๋ผ์ด์ ์ค ๋ฐ ๋ฉด์ฑ
์กฐํญ](#๋ผ์ด์ ์ค-๋ฐ-๋ฉด์ฑ
-์กฐํญ)์ ์์ต๋๋ค.
|
|
23
23
|
|
|
24
24
|
์ด๋ฏธ ๊ฐ์ง๊ณ ์๋ Claude, Cursor, Command Code, Ollama ์ธ์
์ ์ฌ์ฌ์ฉํฉ๋๋ค. `opencode.json` ํ๋ฌ๊ทธ์ธ ํญ๋ชฉ ํ๋๊ฐ ๋ผ์ด๋ธ ์นดํ๋ก๊ทธ๋ฅผ OpenCode์ ๊ณต๊ฐํฉ๋๋ค. Claude์ Cursor๋ OpenCode์ ๋ง์ปค ๋๋ OAuth ๋ ์ฝ๋๊ฐ ์๊ณ ๋ฒค๋ ์ธ์
์ด ์์ ๋๊น์ง ์ฐ๊ฒฐ๋์ง ์์ ์ํ๋ก ์ ์ง๋ฉ๋๋ค. Command Code๋ OpenCode์ ์ ์ฅ๋ ์ง์ API ํค ๋๋ ๊ธฐ์กด CLI ์ธ์
/ํค๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. Ollama๋ ์ ํํ ์ธ์
๋ง์ปค์ ์๋ตํ๋ ์ ๋ขฐ๋ ๋ฐ๋ชฌ์ด ํ์ํฉ๋๋ค.
|
|
25
25
|
|
|
@@ -55,13 +55,13 @@ OpenCode๋ ์์ ์ Bun์ผ๋ก ์ค์ ๋ npm ํ๋ฌ๊ทธ์ธ์ ์ค์นํ๊ณ ์บ
|
|
|
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
|
ํญ๋ชฉ์ ์ถ๊ฐํ๊ฑฐ๋ ๋ณ๊ฒฝํ ๋ค OpenCode๋ฅผ ์์ ํ ์ข
๋ฃํ๊ณ ๋ค์ ์์ํ์ญ์์ค. ๋ฆฌ๋ก๋๋ง์ผ๋ก๋ ์ถฉ๋ถํ์ง ์์ต๋๋ค.
|
|
63
63
|
|
|
64
|
-
ํจํค์ง ํญ๋ชฉ ํ๋๊ฐ ์นดํ๋ก๊ทธ ํ๋ฌ๊ทธ์ธ๊ณผ Claude, Cursor, Command Code, Ollama ์ธ์ฆ hook์ ๋
ธ์ถํฉ๋๋ค. ํ๋ก๋ฐ์ด๋ id: `claude`, `cursor`, `command-code`, `ollama`. ๋ชจ๋ธ id๋ ๊ฐ ํ๋ก๋ฐ์ด๋์ ๋ผ์ด๋ธ ์นดํ๋ก๊ทธ์์ ๊ฐ์ ธ์ค๋ฉฐ, ๋ผ์ด๋ธ ๋ชฉ๋ก์ด ๋น์ด ์์ผ๋ฉด ๋ฌธ์ํ๋ fallback์ Cursor์ `default`์ Command Code์ `Qwen/Qwen3.8-Max`์
๋๋ค.
|
|
64
|
+
ํจํค์ง ํญ๋ชฉ ํ๋๊ฐ ์นดํ๋ก๊ทธ ํ๋ฌ๊ทธ์ธ๊ณผ Claude, Cursor, Command Code, Ollama ์ธ์ฆ hook์ ๋
ธ์ถํฉ๋๋ค. ํ๋ก๋ฐ์ด๋ id: `claude`, `cursor`, `command-code`, `ollama`. ๋ชจ๋ธ id๋ ๊ฐ ํ๋ก๋ฐ์ด๋์ ๋ผ์ด๋ธ ์นดํ๋ก๊ทธ์์ ๊ฐ์ ธ์ค๋ฉฐ, ๋ผ์ด๋ธ ๋ชฉ๋ก์ด ๋น์ด ์์ผ๋ฉด ๋ฌธ์ํ๋ fallback์ Cursor์ `default`์ Command Code์ `Qwen/Qwen3.8-Max`์
๋๋ค. ์ตํธ์ธ xAI ์ปจ์๋จธ๋ ํจํค์ง subpath `opencode-ext-connector/xai`๋ก ๋
ธ์ถ๋ฉ๋๋ค. [์ตํธ์ธ xAI OAuth ๊ถํ๊ณผ ์ปจ์๋จธ](#์ตํธ์ธ-xai-oauth-๊ถํ๊ณผ-์ปจ์๋จธ-ํธ์คํธ์-๊ฒ์คํธ)๋ฅผ ์ฐธ์กฐํ์ญ์์ค.
|
|
65
65
|
|
|
66
66
|
## ์ค์
|
|
67
67
|
|
|
@@ -78,6 +78,7 @@ OpenCode๋ ํ๋ฌ๊ทธ์ธ ์ต์
์ ๋ ์์ ํํ์ ๋ ๋ฒ์งธ ํญ๋ชฉ์ผ๋ก
|
|
|
78
78
|
| `writeBackCredentials` | `false` | **์ฌ์ฉ ์ค๋จ ์์ :** ํ ๋ฒ์ ๋ง์ด๊ทธ๋ ์ด์
์ฃผ๊ธฐ ๋์ ๋จ๋
์ฌ์ฉ ์ ํ์ฉ๋๋ฉฐ, ๊ฐฑ์ ํ Claude writeback์ ์ ์ด |
|
|
79
79
|
| `credentialRefresh.mode` | `"auto"` | **์ฌ์ฉ ์ค๋จ ์์ :** ํ ๋ฒ์ ๋ง์ด๊ทธ๋ ์ด์
์ฃผ๊ธฐ ๋์ ๋จ๋
์ฌ์ฉ ์ ํ์ฉ๋๋ฉฐ, Claude์ `"auto"` ๋๋ `"never"` ๊ฐฑ์ ๋์์ ์ ์ด |
|
|
80
80
|
| `credentialRefresh.leadMs` | `60000` | **์ฌ์ฉ ์ค๋จ ์์ :** ํ ๋ฒ์ ๋ง์ด๊ทธ๋ ์ด์
์ฃผ๊ธฐ ๋์ ๋จ๋
์ฌ์ฉ ์ ํ์ฉ๋๋ฉฐ, ์ฌ์ฉ์ ์ง์ ๋ฆฌ๋ ํ์์๋ ์ฌ์ ํ ์ด ๋ ๊ฑฐ์ ์ค์ ์ด ํ์ |
|
|
81
|
+
| `xaiOAuth.mode` | ์๋ต | ์ตํธ์ธ xAI ์ญํ . `"authority"`๋ ํธ์คํธ `connectorServer` ํญ๋ชฉ์์ ํธ์คํธ ์ต์ ๋ฒ๋ฅผ ํ์ฑํํ๊ณ , `"consumer"`๋ ์ ์ฉ `opencode-ext-connector/xai` ์ปจ์๋จธ๋ฅผ ํ์ฑํํฉ๋๋ค. `xaiOAuth`๋ฅผ ์๋ตํ๋ฉด ํตํฉ์ ์กฐ์ฉํ ๋นํ์ฑํ๋ฉ๋๋ค. `xaiOAuth`๊ฐ ์กด์ฌํ์ง๋ง `mode`๊ฐ ์๊ฑฐ๋ `"authority" | "consumer"` ์ค ํ๋๊ฐ ์๋๋ฉด ํ์ฑ ์์ ์ ๊ฑฐ๋ถ๋ฉ๋๋ค. ๋ชจ๋ Claude ์๊ฒฉ ์ฆ๋ช
์ต์
๊ณผ ๋
๋ฆฝ์ ์
๋๋ค. |
|
|
81
82
|
| `credentialAuthority.claudeCli.enabled` | `false` | Claude ์ ์ฉ ์ตํธ์ธ: [์ตํธ์ธ Claude CLI ๊ถํ ํ์ด๋จธ](#์ตํธ์ธ-claude-cli-๊ถํ-ํ์ด๋จธ-claude-์ ์ฉ) ์ฐธ์กฐ |
|
|
82
83
|
| `credentialAuthority.claudeCli.leadMs` | `300000` | Claude ์ ์ฉ ์ตํธ์ธ: ์๊ฒฉ ์ฆ๋ช
๋ง๋ฃ ์ ์ด ๋ฐ๋ฆฌ์ด๋งํผ ๋จ์์ ๋ ํ์ด๋จธ๊ฐ `claude`๋ฅผ ํธ์ถ; ์์๊ฐ ์๋ ์ ์ |
|
|
83
84
|
| `credentialAuthority.claudeCli.retryMs` | `300000` | Claude ์ ์ฉ ์ตํธ์ธ: 0์ด ์๋ ์ข
๋ฃ, ์ ๊ธ ์ถฉ๋, ์๊ทธ๋, supervisor ์คํจ ํ ์ฌ์๋๊น์ง์ ๋ฐ๋ฆฌ์ด; ์์ ์ ์ |
|
|
@@ -211,6 +212,58 @@ Anthropic์ ๊ฐฑ์ ํ ๋๋ง๋ค refresh ํ ํฐ์ ํ์ ์ํค๊ณ ์ด์ ํ ํฐ
|
|
|
211
212
|
|
|
212
213
|
ํ์ด๋จธ๊ฐ ์คํ๋์ง ์์ผ๋ฉด `flock --version`, `claude --version`, ์ํ ๋๋ ํฐ๋ฆฌ ๊ถํ, ๋ ํ์ ์ค์ ๊ฐ์ ํ์ธํ์ญ์์ค. ๋ฐ๋ณต ๊ฒฝ๊ณ ๋ CLI ์คํ ์คํจ, ์๊ทธ๋ ์ข
๋ฃ, ๋๋ ์์ ์คํจ๋ฅผ ๋ปํฉ๋๋ค. ๋นํ์ฑํํ๋ ค๋ฉด `credentialAuthority`๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋ `enabled`๋ฅผ `false`๋ก ์ค์ ํ ๋ค OpenCode๋ฅผ ์์ ํ ์ฌ์์ํ์ญ์์ค.
|
|
213
214
|
|
|
215
|
+
### ์ตํธ์ธ xAI OAuth ๊ถํ๊ณผ ์ปจ์๋จธ (ํธ์คํธ์ ๊ฒ์คํธ)
|
|
216
|
+
|
|
217
|
+
์ด ์ปค๋ฅํฐ๋ ์ตํธ์ธ xAI ๊ฒฝ๋ก๋ฅผ ํธ์คํธ ์ธก **authority**์ ๊ฒ์คํธ ์ธก **consumer** ์์ผ๋ก ์ ๊ณตํฉ๋๋ค. ๋ ์ญํ ์ `credentialRole`, `credentialManagement`, `credentialAuthority`, `credentialRefresh`, `writeBackCredentials`์ ๋
๋ฆฝ์ ์ด๋ฉฐ ์ด๋ค Claude ์ ์ฑ
๋ ๋ณ๊ฒฝํ๊ฑฐ๋ ๋์ฒดํ์ง ์์ต๋๋ค. `xaiOAuth`๋ฅผ ์๋ตํ๋ฉด ํตํฉ์ ์กฐ์ฉํ ๋นํ์ฑํ๋ฉ๋๋ค. `xaiOAuth`๊ฐ ์กด์ฌํ์ง๋ง `mode`๊ฐ ์๊ฑฐ๋ `"authority" | "consumer"` ์ค ํ๋๊ฐ ์๋๋ฉด ํ์ฑ ์์ ์ ๊ฑฐ๋ถ๋ฉ๋๋ค.
|
|
218
|
+
|
|
219
|
+
๋ ์ญํ ์ ์๋ก ๋ค๋ฅธ OpenCode ํ๊ฒฝ์ ์ค์ ํ์ญ์์ค. ํธ์คํธ์์๋:
|
|
220
|
+
|
|
221
|
+
```jsonc
|
|
222
|
+
{
|
|
223
|
+
"$schema": "https://opencode.ai/config.json",
|
|
224
|
+
"plugin": [["opencode-ext-connector", { "xaiOAuth": { "mode": "authority" } }]]
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
๊ฒ์คํธ์์๋:
|
|
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 (ํธ์คํธ).** ํธ์คํธ `connectorServer` ํญ๋ชฉ์ `xaiOAuth.mode: "authority"`๋ฅผ ์ค์ ํ์ญ์์ค. ์ต์ ๋ฒ๋ `XDG_DATA_HOME`์ด ์ค์ ๋์ด ์ ๋ ๊ฒฝ๋ก์ผ ๋ `${XDG_DATA_HOME}/opencode/auth.json`์ ์ฝ๊ณ , Linux์์ `XDG_DATA_HOME`์ด ๋ฏธ์ค์ ๋๋ ๋น ๋ฌธ์์ด์ผ ๋๋ง `${HOME}/.local/share/opencode/auth.json`์ผ๋ก fallbackํฉ๋๋ค. ์๋ ๊ฒฝ๋ก์ `XDG_DATA_HOME`(์ค์ ๋์์ง๋ง ์ ๋ ๊ฒฝ๋ก๊ฐ ์๋)์ ๊ถํ์ ๋นํ์ฑํํฉ๋๋ค โ `auth.json`์ ์ฝ์ง ์๊ณ ํฌํผ๋ ํธ์ถํ์ง ์์ต๋๋ค. ํ์ฑํ๋ ๊ฒฝ์ฐ ์ต์ ๋ฒ๋ **์ค์ง** `xai` ๋ ์ฝ๋๋ง ๊ฒ์ฌํฉ๋๋ค. ๊ทธ ๋ ์ฝ๋์ ์ง๋ ฌํ fingerprint๊ฐ ๋ฐ๋๋ฉด `${HOME}/.local/bin/opensandbox-xai-auth-sync` ๊ณ ์ ํฌํผ๋ฅผ ์ธ์ ์์ด, `PATH`๋ `/usr/local/bin:/usr/bin:/bin`, `HOME`์ ํญ์ ์ค์ ํ๊ณ `XDG_DATA_HOME`์ ์ ๋ ๊ฒฝ๋ก์ผ ๋๋ง ์ ๋ฌํ์ฌ ํธ์ถํฉ๋๋ค. ํฌํผ๋ OpenSandbox manager์ projection์ ์์ํฉ๋๋ค. ์ปค๋ฅํฐ๋ ๊ฒ์คํธ ์ ๊ทผ ํ์ผ์ ๊ธฐ๋กํ๊ฑฐ๋ xAI OAuth๋ฅผ ๋ฐ๊ธยท๊ฐฑ์ ํ์ง ์๊ณ , ๋จธ์ ๊ฐ์ ํ ํฐ์ ์ ์กํ๊ฑฐ๋ refresh ํ ํฐ์ ๊ฒ์คํธ์ ๋์ง ์์ต๋๋ค. 0์ด ์๋ ์ข
๋ฃ๋ ์ผ์์ ์ฌ๋ก๋ก ์ฒ๋ฆฌ๋์ด xAI ํ๋ก๋ฐ์ด๋๋ฅผ ์ ๊ฑฐํ์ง ์์ ์ฑ `5000` ms ํ ์ฌ์๋๋ฉ๋๋ค.
|
|
238
|
+
|
|
239
|
+
**Consumer (๊ฒ์คํธ).** ๋ณ๋์ OpenCode ํ๋ฌ๊ทธ์ธ ํญ๋ชฉ์ผ๋ก `["opencode-ext-connector/xai", { "xaiOAuth": { "mode": "consumer" } }]` ํํ์ ๋ก๋ํ์ญ์์ค. mode๋ ํ์์
๋๋ค. subpath ํญ๋ชฉ์ mode๊ฐ ์์ผ๋ฉด ๋น hook์ ๋ฐํํ๊ณ xAI ํ๋ก๋ฐ์ด๋๋ ์ฐ๊ฒฐ๋์ง ์์ ์ํ๋ก ์ ์ง๋ฉ๋๋ค. `"consumer"` ๋ชจ๋์์ ์ปค๋ฅํฐ๋ xAI auth hook์ ๋ฐํํ๋ฉฐ `/connect` ๋ฉ์๋๋ฅผ ๋
ธ์ถํ์ง ์์ต๋๋ค โ `methods: []`๊ฐ ๊ณต๊ฐ ๊ณ์ฝ์ ์ผ๋ถ์
๋๋ค. ์ปจ์๋จธ๋ `xai` ํ๋ก๋ฐ์ด๋์ OpenCode auth ๋ ์ฝ๋์ ๋ค์ ์ ํํ ๋ง์ปค๋ฅผ ์๊ตฌํฉ๋๋ค:
|
|
240
|
+
|
|
241
|
+
```jsonc
|
|
242
|
+
{ "type": "api", "key": "cli-session:xai" }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
์ปค๋ฅํฐ๋ ์ด ๋ง์ปค๋ฅผ ๊ธฐ๋กํ์ง ์์ผ๋ฉฐ ๋ง์ปค ๋ฐฐ์น๋ ์ด์์์ ์ฑ
์์
๋๋ค. ๋ค๋ฅธ ํํ๋ฉด loader๋ auth๋ฅผ ๋ฐํํ์ง ์์ต๋๋ค. ์ผํฐ๋๋ก loader๋ ์๋ฆฌ ํ์์ `apiKey`์ ๋ํ๋ `fetch`๋ฅผ ๋ฐํํฉ๋๋ค. ์ค์ bearer๋ ๋ชจ๋ ์ธ๋ถ ์์ฒญ๋ง๋ค ์ ๊ทผ ํ์ผ์์ ๊ฐ์ ธ์ต๋๋ค.
|
|
246
|
+
|
|
247
|
+
**์ ๊ทผ ํ์ผ.** ์ปจ์๋จธ๋ **๋ชจ๋ ** ์์ฒญ๋ง๋ค ์ ๊ทผ ํ์ผ์ ๋ค์ ์ฝ์ผ๋ฉฐ ๋ฉ๋ชจ๋ฆฌ ๋ด ํ ํฐ์ ์ ๋ขฐํ์ง ์์ต๋๋ค:
|
|
248
|
+
|
|
249
|
+
- ๊ฒฝ๋ก: `XDG_DATA_HOME`์ด ์ค์ ๋์ด ์ ๋ ๊ฒฝ๋ก์ผ ๋ `${XDG_DATA_HOME}/opencode/xai-access.json`, Linux์์ `XDG_DATA_HOME`์ด ๋ฏธ์ค์ ๋๋ ๋น ๋ฌธ์์ด์ผ ๋๋ง `${HOME}/.local/share/opencode/xai-access.json`. ์๋ ๊ฒฝ๋ก์ `XDG_DATA_HOME`์ HOME fallback์ ๊ฒ์ฌํ์ง ์๊ณ fail-closedํฉ๋๋ค.
|
|
250
|
+
- ํ์ผ ๊ฒ์ดํธ: ์ ๊ท ํ์ผ, mode `0600`, link ์ `1`, ํ์ฌ ์ฌ์ฉ์ ์์ . ํ์ผ ๋ถ์ฌ, ๋ถ์ ํฉํ stat, ์ฌ๋ณผ๋ฆญ ๋งํฌ, ๋ค์ค ๋งํฌ ํ์ผ, ์๋ชป๋ ์์ ์, ์๋ชป๋ mode๋ ๋ชจ๋ unavailable ์ํ๋ฅผ ๋ฐํํฉ๋๋ค.
|
|
251
|
+
- ์คํค๋ง (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
|
+
`"state": "unavailable"` ๋ ์ฝ๋, malformed JSON, ์คํค๋ง ๋ถ์ผ์น, ํ์ผ ๋ถ์ฌ, stat ์คํจ, ๋๋ `expires`๊ฐ ํ์ฌ ์๊ฐ ์ดํ๊ฐ ์๋ ๊ฒฝ์ฐ ๋ชจ๋ **๋ชจ๋ ** ๋คํธ์ํฌ ํธ์ถ **์ด์ **์ ์คํจํ๋ฉฐ ํด๋น ์์ฒญ์ ๋ํด `XaiAccessUnavailableError`๋ฅผ ๋์ง๋๋ค. bearer ํ ํฐ์ ํ๋ฌ๊ทธ์ธ ์ธ์, ์ปค๋ฅํฐ stdout/stderr ์ง๋จ, ๋๋ ์์ฒญ URL์ ๋ค์ด๊ฐ์ง ์์ต๋๋ค.
|
|
264
|
+
|
|
265
|
+
**๋ฒ์ ๋ฐ.** OpenCode auth์ ์ง์ ์ ์ฅ๋ ์ผ๋ฐ xAI API ํค(๋ด์ฅ `XAI_API_KEY` ๋ฐฉ์ ๊ฒฝ๋ก)๋ ์ด ๊ถํ/์ปจ์๋จธ ์์ด ์๋๋ผ OpenCode์ ๋ด์ฅ `xai` ํ๋ก๋ฐ์ด๋๋ฅผ ๋ฐ๋ฆ
๋๋ค. ์ปค๋ฅํฐ๋ ๊ทธ๋ฌํ ํค๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋, ๋ฏธ๋ฌ๋งํ๊ฑฐ๋, ํ๋ก์ํ์ง ์์ต๋๋ค. ํฌํผ๋ idempotentํด์ผ ํฉ๋๋ค. ๊ฐ์ ๊ฒ์คํธ์์ ๊ฐ๋ฐ์ Claude Code ์ธ์
์ ์คํํด๋ projected ์ ๊ทผ ํ์ผ์ ๋ํ ๊ถํ์ ๊ฐ์ง ์์ต๋๋ค.
|
|
266
|
+
|
|
214
267
|
## ํธ์คํธ/๊ฒ์คํธ ์๋๋ฐ์ค ์ค์
|
|
215
268
|
|
|
216
269
|
OpenCode๊ฐ ์ปจํ
์ด๋, VM ๋๋ ๋ค๋ฅธ ์๋๋ฐ์ค์์ ์คํ๋ ๋ ๊ทธ ๋ฐํ์์ **๊ฒ์คํธ**, ๋ฒค๋ ๋ก๊ทธ์ธ๊ณผ Ollama ๋ฐ๋ชฌ์ ์์ ํ ๋จธ์ ์ **ํธ์คํธ**๋ก ๋ด
๋๋ค. ๊ฒ์คํธ์๋ ์์ฒด `localhost`, ํ ๋๋ ํฐ๋ฆฌ, ํ๊ฒฝ, keychain, ํ์ผ ๊ถํ, ๋คํธ์ํฌ namespace๊ฐ ์์ต๋๋ค. ํ์ผ์ mountํ๊ฑฐ๋ ํ๊ฒฝ ๊ฐ์ ๋ช
์์ ์ผ๋ก ์ฃผ์
ํ์ง ์์ผ๋ฉด ํธ์คํธ ์ธ์
์ ๊ฒ์คํธ์ ๋ณด์ด์ง ์์ต๋๋ค.
|
|
@@ -223,6 +276,7 @@ OpenCode๊ฐ ์ปจํ
์ด๋, VM ๋๋ ๋ค๋ฅธ ์๋๋ฐ์ค์์ ์คํ๋ ๋ ๊ทธ
|
|
|
223
276
|
| Cursor | Cursor CLI ๋ก๊ทธ์ธ์ ์์ | ์๊ฒฉ ์ฆ๋ช
ํ์ผ์ ๊ฒ์คํธ์ `${HOME}/.config/cursor/auth.json`์ read-only๋ก mountํ๊ฑฐ๋ ์๋๋ฐ์ค secret ๊ธฐ๋ฅ์ผ๋ก `CURSOR_ACCESS_TOKEN` ์ฃผ์
; ๊ฒ์คํธ์ Node.js 22 ์ด์ ์ค์น |
|
|
224
277
|
| Command Code | CLI ๋ก๊ทธ์ธ ๋๋ API ํค๋ฅผ ์์ | `${HOME}/.commandcode/auth.json`์ read-only๋ก mountํ๊ฑฐ๋ `COMMAND_CODE_API_KEY` ์ฃผ์
; `COMMAND_CODE_CLI_VERSION`, ์ค์น๋ `command-code` ๋ฐ์ด๋๋ฆฌ ๋๋ npm registry ์ ๊ทผ์ผ๋ก ํด๋ผ์ด์ธํธ ๋ฒ์ ํ์ธ |
|
|
225
278
|
| Ollama | ์ ๋ขฐํ๋ ๋ฐ๋ชฌ์ ์คํํ๊ณ Cloud ์ ๊ทผ์ด ํ์ํ๋ฉด ๊ทธ๊ณณ์์ `ollama signin` ์คํ | Ollama ์๊ฒฉ ์ฆ๋ช
์ ๋ณต์ฌํ์ง ์๊ณ `ollamaBaseURL`๋ก ์ ํํ ๋ฐ๋ชฌ์๋ง ์ฐ๊ฒฐ |
|
|
279
|
+
| xAI | ํธ์คํธ ๊ถํ์ด `~/.local/bin/opensandbox-xai-auth-sync`์ ํธ์ถํ๊ณ OpenSandbox manager๊ฐ access-only ๊ฒ์คํธ ์ํ๋ฅผ projection | manager๊ฐ projectionํ `{"type":"api","key":"cli-session:xai"}` ๋ง์ปค์ `${XDG_DATA_HOME}/opencode/xai-access.json`(๋๋ XDG๊ฐ ๋ฏธ์ค์ ๋๋ ๋น ๋ฌธ์์ด์ผ ๋ HOME fallback)์ ์ฌ์ฉํ๊ณ , `xaiOAuth.mode: "consumer"`๋ก `opencode-ext-connector/xai`๋ฅผ ๋ก๋; refresh ํ ํฐ์ ์ ๋ ๋ณต์ฌํ๊ฑฐ๋ mountํ์ง ์์ |
|
|
226
280
|
|
|
227
281
|
์๋ฅผ ๋ค์ด Linux ๊ฒ์คํธ๋ ๋ค์ ๊ฒฝ๋ก์ ์ ํ์ ์ธ secret/version override๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. Mount source์ destination์ ์๋๋ฐ์ค ๋ฐํ์์ ๋ง๊ฒ ์กฐ์ ํ์ญ์์ค:
|
|
228
282
|
|
|
@@ -311,11 +365,14 @@ Ollama `/connect`๋ ์ค์ ๋ ๋ฐ๋ชฌ์ ์กฐ์ฌํ๊ณ ์ ํํ ์ธ์
๋ง์ปค๋ฅผ
|
|
|
311
365
|
| **Cursor** | CLI ์ก์ธ์ค ํ ํฐ์ผ๋ก Cursor์ ๋ฏธ๊ณต๊ฐ ํด๋ผ์ด์ธํธ ํ๋กํ ์ฝ(`api2.cursor.sh` `AgentService`, HTTP/2 ์์ Connect+protobuf)์ ํธ์ถํฉ๋๋ค. ๋ ์๊ฒฉ ์ฆ๋ช
๊ด๋ฆฌ ๋ชจ๋ ๋ชจ๋ ์๊ฒฉ ์ฆ๋ช
์ ์ฝ๊ธฐ ์ ์ฉ์
๋๋ค. direct ์์ฑ์ ์ ํํ HTTP 401์ด ์ถ๋ ฅ์ด๋ ํจ๊ณผ ์ ์ ๋ฐ์ํ ๋๋ง null์ด ์๋๋ฉฐ ๋ณ๊ฒฝ๋ ์๊ฒฉ ์ฆ๋ช
์ ๋ค์ ์ฝ๊ณ ํ ๋ฒ ์ฌ์๋ํ ์ ์์ผ๋ฉฐ, ์ด๋ ๊ฐฑ์ ์ด๋ writeback์ด ์๋๋๋ค. legacy/compatibility ์์ฑ์ ํ ๋ฒ๋ง ์๋ํฉ๋๋ค. ํ๋ฌ๊ทธ์ธ์ด ์์ ํ Node ์์ ํ๋ก์ธ์ค๊ฐ private stdio๋ก ํต์ ํ๊ณ , ํด ๊ฒฐ๊ณผ๋ฅผ ๊ฐ์ bidi Run์ ์ ์งํ๋ฉฐ, parked call์ ์ ๋ ์ฌ์คํํ์ง ์๊ณ , ์ฌ์ฉ์ ๋๋ฉด ๋ฐ๋ชฌ์ ์ด์ง ์์ผ๋ฉฐ, ์์ฑ์ `cursor-agent`๋ฅผ ์ ๋ spawnํ์ง ์์ต๋๋ค. ๋น๊ณต์์ด๋ฉฐ ๊ณต๊ฐ Cursor API๊ฐ ์๋๋๋ค. ํ๋กํ ์ฝ์ด ์ด๊ธ๋ ๋ค์๋ ์์์ fallback์ด ์์ต๋๋ค โ ํด๋น ํ๋ก๋ฐ์ด๋๊ฐ ์คํจํฉ๋๋ค. Node.js 22 ์ด์์ด ํ์ํฉ๋๋ค. ๋ผ์ด๋ธ ์นดํ๋ก๊ทธ id๊ฐ ์์ผ๋ฉด ๊ทธ๊ฒ์ ์ฐ๊ณ , ์์ผ๋ฉด ๋ฌธ์ํ๋ fallback์ `default`์
๋๋ค. |
|
|
312
366
|
| **Command Code** | CLI ํธํ ์์ฒญ ๋ฉํ๋ฐ์ดํฐ์ ํจ๊ป `/alpha/generate`๋ฅผ ํธ์ถํ๊ณ , ํ๋ก๋ฐ์ด๋ ๋ก์ปฌ NDJSON ํ
์คํธ์ ํด ์ด๋ฒคํธ๋ฅผ ์คํธ๋ฆผํฉ๋๋ค. ๋ ์๊ฒฉ ์ฆ๋ช
๊ด๋ฆฌ ๋ชจ๋ ๋ชจ๋ ์๊ฒฉ ์ฆ๋ช
์ ์ฝ๊ธฐ ์ ์ฉ์
๋๋ค. ์ ํํ HTTP 401์ด ์ถ๋ ฅ์ด๋ ํจ๊ณผ ์ ์ ๋ฐ์ํ๋ฉด null์ด ์๋๋ฉฐ ๋ณ๊ฒฝ๋ ์๊ฒฉ ์ฆ๋ช
์ ๋ค์ ์ฝ๊ณ ํ ๋ฒ๋ง ์ฌ์๋ํ ์ ์์ผ๋ฉฐ, ์ด๋ ๊ฐฑ์ ์ด๋ writeback์ด ์๋๋๋ค. ํด๋ผ์ด์ธํธ ๋ฒ์ ์ `COMMAND_CODE_CLI_VERSION`, ์ค์น๋ `command-code` ๋ฐ์ด๋๋ฆฌ, ๋๋ npm registry์์ ๊ฐ์ ธ์ต๋๋ค. ์์ฒญ ๋ฉํ๋ฐ์ดํฐ์๋ Node.js ๋ฒ์ , ํ๋ซํผ, ์ํคํ
์ฒ, ์ ๋ ์์
๋๋ ํฐ๋ฆฌ๊ฐ ํฌํจ๋ฉ๋๋ค. ๋ผ์ด๋ธ ์นดํ๋ก๊ทธ id๊ฐ ์์ผ๋ฉด ๊ทธ๊ฒ์ ์ฐ๊ณ , ์์ผ๋ฉด ๋ฌธ์ํ๋ fallback์ `Qwen/Qwen3.8-Max`์
๋๋ค. |
|
|
313
367
|
| **Ollama** | `credentialManagement`์ ์ํฅ์ ๋ฐ์ง ์์ต๋๋ค. `ollamaBaseURL`๋ก ์ ํํ ์ ๋ขฐ๋ ๋ฐ๋ชฌ(๊ธฐ๋ณธ๊ฐ `http://localhost:11434`)์ `/api/tags`, `/api/pull`, `/api/chat`์ ์ฌ์ฉํ๋ฉฐ ๊ฒฝ๋ก prefix๋ฅผ ๋ณด์กดํฉ๋๋ค. ์ด๋ฏธ pull๋ ๋ชจ๋ธ๊ณผ, ์ปค๋ฅํฐ ์๊ฒฉ ์ฆ๋ช
์์ด Ollama ๊ณต์ Cloud ๊ฒ์ ๋ฐ library ํ์ด์ง์์ ์ต๋ช
์ผ๋ก ๋ฐ๊ฒฌํ ์ ํํ Cloud ํ๊ทธ๋ฅผ ๊ณต๊ฐํฉ๋๋ค. ์ ํํ ์ค๋ณต๋๋ ํญ๋ชฉ์ ๋ก์ปฌ์ด ์ด๊น๋๋ค. ๋ถ์์ ํ Cloud ๊ฐฑ์ ์ ๋ง์ง๋ง ์์ ํ ๋ชฉ๋ก์ ์ ์งํฉ๋๋ค. ์๋ ์ธ๊ฐ๋ Cloud ํ๊ทธ๋ฅผ ์ ํํ๋ฉด ์ต์ด ์ฌ์ฉ ์ lightweight remote reference๋ฅผ pullํฉ๋๋ค. ๊ฐ์ ์ ๊ทํ base์ ํ๊ทธ์ ๋์ pull์ ํ๋์ in-flight ์์ฒญ์ ๊ณต์ ํ๋ฉฐ ์คํจํ pull์ ์ฌ์๋ํ ์ ์์ต๋๋ค. ๋ฐ๋ชฌ์ ์ฌ์ฉ์์ Ollama Cloud ๊ตฌ๋
์ผ๋ก Cloud ํ๊ทธ ํ๋กฌํํธ๋ฅผ proxyํ ์ ์์ต๋๋ค. ์ปค๋ฅํฐ๋ Ollama API ํค, ์ฌ์ฉ๋ ๊ณผ๊ธ direct Cloud API, `OLLAMA_HOST`, ์๊ฒฉ ์ฆ๋ช
/custom header, cookie, direct Cloud ์์ฑ endpoint๋ฅผ ์ฌ์ฉํ์ง ์์ต๋๋ค. |
|
|
368
|
+
| **xAI** | ํจํค์ง subpath `opencode-ext-connector/xai`๋ก๋ง ๋ก๋๋๋ ์ตํธ์ธ OAuth ์ปจ์๋จธ์
๋๋ค. ์ ๊ทผ ํ์ผ์ ๋ชจ๋ ์์ฒญ๋ง๋ค ๋ค์ ์ฝ๊ณ , `/connect` ๋ฉ์๋๋ฅผ ๋
ธ์ถํ์ง ์์ผ๋ฉฐ, xAI OAuth๋ฅผ ๋ฐ๊ธํ๊ฑฐ๋ ๊ฐฑ์ ํ์ง ์์ต๋๋ค. Claude `credentialRole`๊ณผ ์๊ฒฉ ์ฆ๋ช
์ ์ฑ
์์ ๋
๋ฆฝ์ ์
๋๋ค. [์ตํธ์ธ xAI OAuth ๊ถํ๊ณผ ์ปจ์๋จธ](#์ตํธ์ธ-xai-oauth-๊ถํ๊ณผ-์ปจ์๋จธ-ํธ์คํธ์-๊ฒ์คํธ) ์ฐธ์กฐ. |
|
|
314
369
|
|
|
315
370
|
ํ๋ก๋ฐ์ด๋ health๋ ๊ฒฉ๋ฆฌ๋ฉ๋๋ค. ํ ํ๋ก๋ฐ์ด๋๊ฐ ์คํจํด๋ ๋๋จธ์ง๋ ์ ๊ฑฐ๋์ง ์์ต๋๋ค.
|
|
316
371
|
|
|
317
372
|
๋
๋ฆฝ SDK entry๋ `opencode-ext-connector/ollama`์
๋๋ค. `{ ollamaBaseURL }`์ ์ ๋ฌํด ๊ฐ์ ์ ๋ขฐ๋ ๋ฐ๋ชฌ์ ์ ํํ ์ ์์ต๋๋ค. ํด๋น ๋ฐ๋ชฌ์ ์ด๋ฏธ ์๋ ๋ชจ๋ธ๋ก ์์ฑํ ์ ์์ผ๋ฉฐ, ์ปค๋ฅํฐ๊ฐ ๊ด๋ฆฌํ๋ Cloud ์๋ pull์ ๊ทธ ์ ๊ทํ base์ ํ์ฑ Ollama ์นดํ๋ก๊ทธ lease๊ฐ ํ์ํฉ๋๋ค.
|
|
318
373
|
|
|
374
|
+
์ ์ฉ ์ปจ์๋จธ entry๋ `opencode-ext-connector/xai`์
๋๋ค. `{ xaiOAuth: { mode: "consumer" } }`์ ์ ๋ฌํด ๋ชจ๋ธ ํ๋ก๋ฐ์ด๋, ๋ชจ๋ธ, `/connect` ๋ฉ์๋๋ฅผ ๋ฑ๋กํ์ง ์๊ณ xAI auth hook๋ง ๋ฑ๋กํฉ๋๋ค.
|
|
375
|
+
|
|
319
376
|
## ๋ฌธ์ ํด๊ฒฐ
|
|
320
377
|
|
|
321
378
|
| ์ฆ์ | ํ์ธํ ๊ฒ |
|
|
@@ -329,6 +386,8 @@ Ollama `/connect`๋ ์ค์ ๋ ๋ฐ๋ชฌ์ ์กฐ์ฌํ๊ณ ์ ํํ ์ธ์
๋ง์ปค๋ฅผ
|
|
|
329
386
|
| Cursor ์์ฑ์ด ์คํจํจ | Node.js 22 ์ด์์ด ํ์ํฉ๋๋ค. ์์ฑ์ `cursor-agent`๊ฐ ์๋๋ผ private Node ์์ ํ๋ก์ธ์ค๋ฅผ ํตํ ๋ฏธ๊ณต๊ฐ ํ๋กํ ์ฝ์ ์ฌ์ฉํฉ๋๋ค. ํ๋กํ ์ฝ์ด ์ด๊ธ๋๋ฉด ํด๋น ํ๋ก๋ฐ์ด๋๊ฐ ์คํจํ๋ฉฐ, ์์์ fallback์ ์์ต๋๋ค. |
|
|
330
387
|
| Command Code ์์ฑ์ด ์คํจํจ | ํด๋ผ์ด์ธํธ ๋ฒ์ ์ ํ์ธํ ์ ์์์ต๋๋ค: `COMMAND_CODE_CLI_VERSION`์ ์ค์ ํ๊ฑฐ๋, `command-code`๋ฅผ ์ค์นํ๊ฑฐ๋, `registry.npmjs.org` ์ ๊ทผ์ ํ์ฉํ์ญ์์ค. ์์ฒญ ๋ฉํ๋ฐ์ดํฐ์๋ Node.js ๋ฒ์ , ํ๋ซํผ, ์ํคํ
์ฒ, ์ ๋ ์์
๋๋ ํฐ๋ฆฌ๊ฐ ํฌํจ๋ฉ๋๋ค. |
|
|
331
388
|
| `opencode models ollama`์ Ollama๊ฐ ์์ | `ollamaBaseURL`(๋๋ ๊ธฐ๋ณธ `localhost:11434`)์์ ์ ๋ขฐํ๋ ๋ฐ๋ชฌ์ ์์ํ ๋ค `/connect`ํ์ญ์์ค. ๊ฒฝ๋ก prefix๊ฐ Ollama `/api/*` route์ ๋๋ฌํ๋์ง ํ์ธํ์ญ์์ค. Cloud ํ๊ทธ๋ ์ต๋ช
catalog ํญ๋ชฉ์ด๋ฉฐ `OLLAMA_HOST`, API ํค, credential header, redirect, direct Cloud ์์ฑ์ ์ฌ์ฉํ์ง ์์ต๋๋ค. |
|
|
389
|
+
| xAI ์์ฒญ์ด `XaiAccessUnavailableError`๋ก ์คํจํจ | ํธ์คํธ `connectorServer` ํญ๋ชฉ์ `xaiOAuth.mode`๊ฐ `"authority"`์ธ์ง, `opencode-ext-connector/xai` subpath ํญ๋ชฉ์ `xaiOAuth.mode`๊ฐ `"consumer"`์ธ์ง, OpenCode auth ๋ ์ฝ๋๊ฐ ์ ํํ `{"type":"api","key":"cli-session:xai"}`๋ฅผ ๋ด๊ณ ์๋์ง, ์ ๊ทผ ํ์ผ์ด `${XDG_DATA_HOME}/opencode/xai-access.json`(๋๋ Linux์์ `XDG_DATA_HOME`์ด ๋ฏธ์ค์ ๋๋ ๋น ๋ฌธ์์ด์ผ ๋ `${HOME}/.local/share/opencode/xai-access.json`)์ ํ์ฌ ์ฌ์ฉ์ ์์ ์ ์ ๊ท ๋จ์ผ ๋งํฌ mode `0600` ํ์ผ๋ก ์กด์ฌํ๊ณ ์ ํจํ closed v1 ์คํค๋ง์ ๋ฏธ๋์ `expires`๋ฅผ ๊ฐ์ง๋์ง ํ์ธํ์ญ์์ค. ์๋ ๊ฒฝ๋ก์ `XDG_DATA_HOME`์ HOME fallback ์์ด fail-closedํฉ๋๋ค. ์ค๋ฅ๋ ๋ชจ๋ ๋คํธ์ํฌ ํธ์ถ ์ ์ ๋ฐ์ํฉ๋๋ค. |
|
|
390
|
+
| ํธ์คํธ xAI ๊ถํ ํฌํผ๊ฐ ์คํ๋์ง ์์ | `${HOME}/.local/bin/opensandbox-xai-auth-sync`์ด ์กด์ฌํ๊ณ ์คํ ๊ฐ๋ฅํ๋ฉฐ ์ฑ๊ณต ์ `0`์ผ๋ก ์ข
๋ฃํ๋์ง ํ์ธํ์ญ์์ค. ์ต์ ๋ฒ๋ ๋ชจ๋ 0์ด ์๋ ์ข
๋ฃ๋ฅผ ์ผ์์ ์ฌ๋ก๋ก ์ฒ๋ฆฌํด `5000` ms ํ ์ฌ์๋ํ๋ฉฐ xAI ํ๋ก๋ฐ์ด๋๋ฅผ ์ ๊ฑฐํ์ง ์์ต๋๋ค. ํฌํผ๋ OpenSandbox manager์ projection์ ์์ํฉ๋๋ค. ์๋ ๊ฒฝ๋ก์ `XDG_DATA_HOME`(์ค์ ๋์์ง๋ง ์ ๋ ๊ฒฝ๋ก๊ฐ ์๋)์ ์ฝ๊ธฐ์ ํธ์ถ ์์ด ๊ถํ์ ๋นํ์ฑํํฉ๋๋ค. |
|
|
332
391
|
| ํธ์คํธ ์๊ฒฉ ์ฆ๋ช
์ด ์์ง๋ง ๊ฒ์คํธ ํ๋ก๋ฐ์ด๋๊ฐ ์ฐ๊ฒฐ๋์ง ์์ | mount ๋์๊ณผ ๊ถํ, ๊ฒ์คํธ์ `HOME`, `CLAUDE_CONFIG_DIR`, ์ฃผ์
ํ secret ํ๊ฒฝ, writable ๊ฒ์คํธ OpenCode `auth.json`, ๊ฒ์คํธ ์์์ `/connect`๊ฐ ์๋ฃ๋๋์ง ํ์ธํ์ญ์์ค. |
|
|
333
392
|
| Ollama๊ฐ ํธ์คํธ์์๋ ๋์ํ์ง๋ง ๊ฒ์คํธ์์๋ ๋์ํ์ง ์์ | ๊ฒ์คํธ `localhost`๋ ๋ณดํต ํธ์คํธ๊ฐ ์๋๋๋ค. `host.docker.internal` resolve, Linux `host-gateway` mapping, ๋ฐ๋ชฌ bind ์ฃผ์, firewall๊ณผ ์๋๋ฐ์ค egress, base path prefix๊ฐ Ollama `/api/*` route์ ๋๋ฌํ๋์ง ํ์ธํ์ญ์์ค. |
|
|
334
393
|
| ํ ํ๋ก๋ฐ์ด๋๊ฐ ๋ค์ด๋จ | ์คํจ๋ ๊ฒฉ๋ฆฌ๋ฉ๋๋ค. ์ผ์์ ์ค๋
์ท ์คํจ๋ ๋ง์ง๋ง์ผ๋ก ์๋ ค์ง ์นดํ๋ก๊ทธ๋ฅผ ์ ์งํ๊ณ , unavailable ์ค๋
์ท์ ํด๋น ์ปค๋ฅํฐ ์์ ํ๋ก๋ฐ์ด๋๋ง ์ ๊ฑฐํฉ๋๋ค. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-ext-connector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "External provider connector for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"import": "./dist/index.js"
|
|
22
22
|
},
|
|
23
|
+
"./xai": {
|
|
24
|
+
"types": "./dist/xai.d.ts",
|
|
25
|
+
"import": "./dist/xai.js"
|
|
26
|
+
},
|
|
23
27
|
"./command-code": {
|
|
24
28
|
"types": "./dist/sdk/command-code.d.ts",
|
|
25
29
|
"import": "./dist/sdk/command-code.js"
|