opencode-1password-auth 1.0.5 → 1.0.7
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/README.md +179 -16
- package/index.ts +78 -42
- package/package.json +1 -1
- package/index-v2.ts +0 -387
package/README.md
CHANGED
|
@@ -2,11 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
Authenticate LLM providers and inject MCP server secrets from 1Password environments.
|
|
4
4
|
|
|
5
|
+
## ⚠️ Important Note
|
|
6
|
+
|
|
7
|
+
**OpenCode does NOT resolve `{env:VAR}` syntax in `auth.json`**. This plugin works around this limitation by:
|
|
8
|
+
1. Setting API keys via `client.auth.set()` at runtime
|
|
9
|
+
2. Setting environment variables in `process.env` for MCP servers
|
|
10
|
+
3. Using `{env:VAR}` references in config files for MCP servers only
|
|
11
|
+
|
|
12
|
+
**Version 1.0.6+ includes critical timing fixes** - the plugin now initializes immediately when loaded, not waiting for the `server.connected` event.
|
|
13
|
+
|
|
14
|
+
## 🚀 Quick Start
|
|
15
|
+
|
|
16
|
+
1. **Run setup script**:
|
|
17
|
+
```powershell
|
|
18
|
+
# Windows
|
|
19
|
+
.\setup.ps1
|
|
20
|
+
|
|
21
|
+
# macOS/Linux
|
|
22
|
+
./setup.sh
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. **Update config files**:
|
|
26
|
+
```powershell
|
|
27
|
+
.\setup.ps1 -UpdateConfig
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. **Add to OpenCode config** (`~/.config/opencode/opencode.json`):
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"plugin": ["opencode-1password-auth"]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
4. **Restart OpenCode** completely
|
|
38
|
+
|
|
39
|
+
5. **Check debug logs** at `~/.opencode-1password-debug/debug.log`
|
|
40
|
+
|
|
5
41
|
## Features
|
|
6
42
|
|
|
7
|
-
- **Provider Authentication**: Automatically authenticate OpenCode providers (MiniMax, DeepSeek, OpenCode, etc.) from 1Password
|
|
43
|
+
- **Provider Authentication**: Automatically authenticate OpenCode providers (MiniMax, DeepSeek, OpenCode, etc.) from 1Password at runtime
|
|
8
44
|
- **MCP Secret Injection**: Inject secrets into MCP server environments from 1Password
|
|
9
45
|
- **.env Access Warning**: Warns when .env files are accessed
|
|
46
|
+
- **Config File Management**: Automatically update config files to use `{env:VAR}` references (for MCP servers)
|
|
47
|
+
- **Debug Logging**: Detailed logs at `~/.opencode-1password-debug/debug.log`
|
|
10
48
|
|
|
11
49
|
## Prerequisites
|
|
12
50
|
|
|
@@ -39,6 +77,24 @@ Create these environments in 1Password:
|
|
|
39
77
|
**MCPS Environment** (holds MCP secrets):
|
|
40
78
|
- Secret names matching your MCP server config (e.g., `MINIMAX_API_KEY`, `MINIMAX_API_HOST`)
|
|
41
79
|
|
|
80
|
+
### Variable Naming Guide
|
|
81
|
+
|
|
82
|
+
The plugin normalizes variable names to match auth.json provider IDs:
|
|
83
|
+
|
|
84
|
+
| 1Password Variable | Normalized Provider ID | auth.json Key |
|
|
85
|
+
|-------------------|------------------------|---------------|
|
|
86
|
+
| `MINIMAX_CODING_PLAN` | `minimax-coding-plan` | `minimax-coding-plan` |
|
|
87
|
+
| `OPENCODE` | `opencode` | `opencode` |
|
|
88
|
+
| `DEEPSEEK` | `deepseek` | `deepseek` |
|
|
89
|
+
| `minimax_coding_plan` | `minimax-coding-plan` | `minimax-coding-plan` |
|
|
90
|
+
| `open_code` | `open-code` | `open-code` |
|
|
91
|
+
|
|
92
|
+
**Rules:**
|
|
93
|
+
1. Variable names are **case-insensitive**
|
|
94
|
+
2. Underscores (`_`) are converted to hyphens (`-`)
|
|
95
|
+
3. The plugin sets environment variables in multiple formats for compatibility
|
|
96
|
+
4. MCP variable names should match exactly what your MCP server expects
|
|
97
|
+
|
|
42
98
|
## Setup
|
|
43
99
|
|
|
44
100
|
### 1. Run Setup Script
|
|
@@ -63,9 +119,13 @@ The script will:
|
|
|
63
119
|
- Show a full audit of your configuration
|
|
64
120
|
|
|
65
121
|
**Script Options:**
|
|
66
|
-
- `.\setup.ps1
|
|
122
|
+
- `.\setup.ps1` - Interactive setup (default)
|
|
123
|
+
- `.\setup.ps1 -Audit` - View current configuration without making changes
|
|
124
|
+
- `.\setup.ps1 -UpdateConfig` - **Critical**: Update `auth.json` and `opencode.json` to use `{env:VAR}` references
|
|
67
125
|
- `.\setup.ps1 -Uninstall` - Remove environment variables
|
|
68
126
|
|
|
127
|
+
> **⚠️ IMPORTANT**: You MUST run `.\setup.ps1 -UpdateConfig` after setting up environment variables to update your config files.
|
|
128
|
+
|
|
69
129
|
### 2. Install Plugin
|
|
70
130
|
|
|
71
131
|
Add to your `opencode.json`:
|
|
@@ -113,7 +173,7 @@ opencode-mcps
|
|
|
113
173
|
|
|
114
174
|
## MCP Configuration
|
|
115
175
|
|
|
116
|
-
In your `opencode.json`, use `{env:VARIABLE_NAME}` syntax to reference injected secrets:
|
|
176
|
+
In your `opencode.json`, use `{env:VARIABLE_NAME}` syntax to reference injected secrets. **This works for MCP servers** because the plugin injects environment variables via the `shell.env` hook:
|
|
117
177
|
|
|
118
178
|
```json
|
|
119
179
|
{
|
|
@@ -130,12 +190,41 @@ In your `opencode.json`, use `{env:VARIABLE_NAME}` syntax to reference injected
|
|
|
130
190
|
}
|
|
131
191
|
```
|
|
132
192
|
|
|
133
|
-
|
|
193
|
+
> **Note**: While `{env:VAR}` syntax works for MCP servers in `opencode.json`, it **does NOT work** for provider authentication in `auth.json`. For providers, the plugin uses `client.auth.set()` instead.
|
|
194
|
+
|
|
195
|
+
## How It Works (v1.0.6+)
|
|
196
|
+
|
|
197
|
+
1. **Immediate Initialization**: When OpenCode loads the plugin, it immediately:
|
|
198
|
+
- Creates 1Password SDK client using `OP_SERVICE_ACCOUNT_TOKEN`
|
|
199
|
+
- Reads bootstrap environment ID from `OP_CONFIG_ENV_ID`
|
|
200
|
+
- Retrieves provider and MCP environment IDs
|
|
201
|
+
2. **Provider Authentication**:
|
|
202
|
+
- Reads API keys from 1Password providers environment
|
|
203
|
+
- Sets `process.env` variables with multiple naming formats (e.g., `opencode`, `OPENCODE`, `opencode`, `OPENCODE`)
|
|
204
|
+
- Calls `client.auth.set()` to authenticate each provider at runtime
|
|
205
|
+
3. **MCP Secret Injection**:
|
|
206
|
+
- Reads secrets from 1Password MCP environment
|
|
207
|
+
- Injects them via the `shell.env` hook when MCP servers start
|
|
208
|
+
4. **Config File Management**:
|
|
209
|
+
- Updates `auth.json` to use `{env:providerId}` references (though OpenCode doesn't resolve these)
|
|
210
|
+
- Updates `opencode.json` MCP config to use `{env:VAR}` references (which DO work for MCP)
|
|
211
|
+
|
|
212
|
+
## Authentication Flow
|
|
213
|
+
|
|
214
|
+
```mermaid
|
|
215
|
+
graph TD
|
|
216
|
+
A[OpenCode starts] --> B[Load 1Password Plugin]
|
|
217
|
+
B --> C[Immediate initialization]
|
|
218
|
+
C --> D{Read 1Password config}
|
|
219
|
+
D --> E[Set process.env variables]
|
|
220
|
+
D --> F[Call client.auth.set()]
|
|
221
|
+
F --> G[Provider authenticated at runtime]
|
|
222
|
+
E --> H[MCP servers get env vars]
|
|
223
|
+
G --> I[Chat requests work]
|
|
224
|
+
H --> I
|
|
225
|
+
```
|
|
134
226
|
|
|
135
|
-
|
|
136
|
-
2. Reads environment IDs from your config environment
|
|
137
|
-
3. Authenticates providers using `client.auth.set()`
|
|
138
|
-
4. Injects MCP secrets via the `shell.env` hook
|
|
227
|
+
> **Note**: `client.auth.set()` happens AFTER OpenCode reads `auth.json`, so the plugin must initialize early enough to intercept authentication attempts.
|
|
139
228
|
|
|
140
229
|
## Requirements
|
|
141
230
|
|
|
@@ -145,31 +234,105 @@ In your `opencode.json`, use `{env:VARIABLE_NAME}` syntax to reference injected
|
|
|
145
234
|
|
|
146
235
|
## Troubleshooting
|
|
147
236
|
|
|
148
|
-
|
|
237
|
+
### Plugin not loading?
|
|
149
238
|
- Ensure environment variables are set system-wide (not just in terminal)
|
|
150
239
|
- Restart OpenCode completely after setting environment variables
|
|
240
|
+
- Check OpenCode console for plugin loading errors
|
|
151
241
|
|
|
152
|
-
|
|
153
|
-
- Verify service account has access to the vaults containing your environments
|
|
242
|
+
### Can't read 1Password environments?
|
|
243
|
+
- Verify service account has access to the vaults containing your environments
|
|
154
244
|
- Check that environment IDs in config are correct
|
|
245
|
+
- Run `.\setup.ps1 -Audit` to verify connection and configuration
|
|
246
|
+
|
|
247
|
+
### Authentication failing with "Your api key: ****eek} is invalid"?
|
|
248
|
+
This means OpenCode is reading the literal `{env:deepseek}` string from `auth.json`. The plugin needs to authenticate providers earlier.
|
|
249
|
+
|
|
250
|
+
**Solutions:**
|
|
251
|
+
1. **Update to v1.0.6+** - Includes critical timing fixes
|
|
252
|
+
2. **Check debug logs** - Look at `~/.opencode-1password-debug/debug.log`
|
|
253
|
+
3. **Verify plugin is in opencode.json** - Ensure `"opencode-1password-auth"` is in the plugin array
|
|
254
|
+
4. **Restart OpenCode** - Completely quit and restart to reload the plugin
|
|
255
|
+
|
|
256
|
+
### Debug Logging
|
|
257
|
+
The plugin writes detailed logs to `~/.opencode-1password-debug/debug.log`. Check this file for:
|
|
258
|
+
- Plugin initialization status
|
|
259
|
+
- 1Password connection attempts
|
|
260
|
+
- `client.auth.set()` calls and results
|
|
261
|
+
- Environment variable injections
|
|
262
|
+
|
|
263
|
+
### Common Issues
|
|
264
|
+
- **Timing**: If the plugin initializes too late, OpenCode already attempted authentication
|
|
265
|
+
- **Environment Variables**: Must be set system-wide, not just in current terminal
|
|
266
|
+
- **1Password Permissions**: Service account needs read access to environments
|
|
267
|
+
- **Variable Names**: Provider names in 1Password should match auth.json provider IDs (case-insensitive, underscores converted to hyphens)
|
|
268
|
+
|
|
269
|
+
## Current Status (v1.0.6)
|
|
270
|
+
|
|
271
|
+
### ✅ Implemented
|
|
272
|
+
- **Provider Authentication**: `client.auth.set()` at runtime
|
|
273
|
+
- **MCP Secret Injection**: Via `shell.env` hook
|
|
274
|
+
- **Config File Management**: Auto-update `auth.json` and `opencode.json` with `-UpdateConfig` flag
|
|
275
|
+
- **Debug Logging**: File-based logging at `~/.opencode-1password-debug/debug.log`
|
|
276
|
+
- **Variable Normalization**: Multiple naming formats supported
|
|
277
|
+
- **Immediate Initialization**: Plugin loads early in OpenCode lifecycle
|
|
278
|
+
|
|
279
|
+
### 🔄 Working Around OpenCode Limitations
|
|
280
|
+
The plugin works around OpenCode's limitation of not resolving `{env:VAR}` in `auth.json` by:
|
|
281
|
+
1. Calling `client.auth.set()` to authenticate providers at runtime
|
|
282
|
+
2. Setting `process.env` variables for MCP servers
|
|
283
|
+
3. Using timing fixes in v1.0.6+ to initialize before authentication attempts
|
|
155
284
|
|
|
156
285
|
## Roadmap
|
|
157
286
|
|
|
158
|
-
The following features are planned for future releases:
|
|
159
|
-
|
|
160
287
|
### Phase 2 - Enhanced Scripting
|
|
161
288
|
- [ ] **Automated service account creation** - Guide users through creating service accounts with correct permissions via 1Password API
|
|
162
289
|
- [ ] **Bootstrap environment management via scripts** - Add, list, remove environment references from the bootstrap environment through CLI
|
|
290
|
+
- [ ] **Cross-platform setup improvements** - Better support for macOS/Linux setup scripts
|
|
163
291
|
|
|
164
|
-
### Phase 3 - Advanced Plugin Features
|
|
292
|
+
### Phase 3 - Advanced Plugin Features
|
|
165
293
|
- [ ] **Dynamic environment detection** - Plugin automatically detects new environments added to bootstrap and integrates them
|
|
166
|
-
- [
|
|
294
|
+
- [x] **Config file injection** - ✅ Implemented (uses `-UpdateConfig` flag)
|
|
167
295
|
- [ ] **Custom environment support** - Support for additional environments beyond the standard opencode-providers, opencode-plugins, opencode-mcps pattern
|
|
168
296
|
- [ ] **Write support** - Ability to store secrets back to 1Password environments
|
|
297
|
+
- [ ] **Auth.json workaround** - Investigate alternative approaches if `client.auth.set()` timing issues persist
|
|
169
298
|
|
|
170
|
-
### Phase 4 -
|
|
299
|
+
### Phase 4 - Polish & Reliability
|
|
171
300
|
- [ ] **Uninstall script** - Clean removal of environment variables and plugin configuration
|
|
172
301
|
- [ ] **Configuration validation** - Validate OpenCode config files reference correct environment variables
|
|
302
|
+
- [ ] **Health checks** - Periodic verification that 1Password connection and authentication are working
|
|
303
|
+
- [ ] **Better error messages** - User-friendly error reporting for common issues
|
|
304
|
+
|
|
305
|
+
## Deployment
|
|
306
|
+
|
|
307
|
+
### Publishing New Versions
|
|
308
|
+
|
|
309
|
+
1. **Update version** in `package.json`
|
|
310
|
+
2. **Commit changes** with descriptive message:
|
|
311
|
+
```bash
|
|
312
|
+
git add .
|
|
313
|
+
git commit -m "Brief description of changes"
|
|
314
|
+
```
|
|
315
|
+
3. **Tag the release** (optional but recommended):
|
|
316
|
+
```bash
|
|
317
|
+
git tag v1.0.6
|
|
318
|
+
git push origin v1.0.6
|
|
319
|
+
```
|
|
320
|
+
4. **Publish to npm**:
|
|
321
|
+
```bash
|
|
322
|
+
npm publish
|
|
323
|
+
```
|
|
324
|
+
5. **Update GitHub**:
|
|
325
|
+
```bash
|
|
326
|
+
git push origin master
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Version History
|
|
330
|
+
- **v1.0.6**: Timing fixes - immediate initialization, config hook, enhanced logging
|
|
331
|
+
- **v1.0.5**: Debug logging and error handling improvements
|
|
332
|
+
- **v1.0.4**: Variable normalization and multiple env var formats
|
|
333
|
+
- **v1.0.3**: Setup scripts with config file modification
|
|
334
|
+
- **v1.0.2**: MCP secret injection and .env warnings
|
|
335
|
+
- **v1.0.1**: Initial release with basic provider authentication
|
|
173
336
|
|
|
174
337
|
## License
|
|
175
338
|
|
package/index.ts
CHANGED
|
@@ -252,17 +252,20 @@ export const OnePasswordAuthPlugin: Plugin = async ({ client, $ }) => {
|
|
|
252
252
|
|
|
253
253
|
try {
|
|
254
254
|
// Authenticate with OpenCode runtime
|
|
255
|
-
debugLog(`Calling client.auth.set for ${providerId}`);
|
|
256
|
-
await client.auth.set({
|
|
255
|
+
debugLog(`Calling client.auth.set for ${providerId} with key length ${apiKey.length}`);
|
|
256
|
+
const result = await client.auth.set({
|
|
257
257
|
path: { id: providerId },
|
|
258
258
|
body: { type: "api", key: apiKey },
|
|
259
259
|
});
|
|
260
|
-
debugLog(`✓ ${providerId} authenticated (from ${variableName})`);
|
|
260
|
+
debugLog(`✓ ${providerId} authenticated (from ${variableName}) - Result: ${JSON.stringify(result)}`);
|
|
261
261
|
} catch (err) {
|
|
262
262
|
debugLog(`Failed to authenticate ${providerId}: ${err}`);
|
|
263
263
|
if (err instanceof Error) {
|
|
264
264
|
debugLog(`Error message: ${err.message}`);
|
|
265
|
+
debugLog(`Error stack: ${err.stack}`);
|
|
265
266
|
}
|
|
267
|
+
// Also log to console for immediate visibility
|
|
268
|
+
console.error(`[1Password Plugin] Failed to authenticate ${providerId}: ${err}`);
|
|
266
269
|
}
|
|
267
270
|
}
|
|
268
271
|
};
|
|
@@ -311,52 +314,85 @@ export const OnePasswordAuthPlugin: Plugin = async ({ client, $ }) => {
|
|
|
311
314
|
return toInject;
|
|
312
315
|
};
|
|
313
316
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (event.type === "server.connected") {
|
|
318
|
-
debugLog("Initializing...");
|
|
317
|
+
// Initialize as soon as plugin loads (config hook runs early)
|
|
318
|
+
const initializePlugin = async () => {
|
|
319
|
+
debugLog("Initializing plugin...");
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
opClient = await initClient();
|
|
322
|
+
if (!opClient) {
|
|
323
|
+
debugLog("No client available");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
325
326
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
const configEnvId = process.env.OP_CONFIG_ENV_ID;
|
|
328
|
+
if (!configEnvId) {
|
|
329
|
+
debugLog("Missing OP_CONFIG_ENV_ID");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
331
332
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
333
|
+
debugLog(`Reading config from environment ${configEnvId}`);
|
|
334
|
+
const vars = await readEnvironmentVariables(configEnvId);
|
|
335
|
+
const configEnvIds: Record<string, string> = {};
|
|
336
|
+
for (const v of vars) {
|
|
337
|
+
if (v.name.endsWith("_ENV_ID") && v.value) {
|
|
338
|
+
configEnvIds[v.name] = v.value;
|
|
339
|
+
debugLog(`Found ${v.name} = ${v.value.substring(0, 10)}...`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
341
342
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
343
|
+
const providersEnvId = configEnvIds.OPENCODE_PROVIDERS_ENV_ID;
|
|
344
|
+
if (providersEnvId) {
|
|
345
|
+
// First update the config files to use env var references
|
|
346
|
+
await updateAuthJson(providersEnvId);
|
|
347
|
+
await authenticateProviders(providersEnvId);
|
|
348
|
+
}
|
|
348
349
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
350
|
+
const mcpEnvIdFromConfig = configEnvIds.OPENCODE_MCPS_ENV_ID;
|
|
351
|
+
if (mcpEnvIdFromConfig) {
|
|
352
|
+
mcpsEnvId = mcpEnvIdFromConfig;
|
|
353
|
+
await updateOpenCodeJsonMCP(mcpEnvIdFromConfig);
|
|
354
|
+
const toInject = await injectMCPSecrets(mcpEnvIdFromConfig);
|
|
355
|
+
if (Object.keys(toInject).length > 0) {
|
|
356
|
+
debugLog(`Injected ${Object.keys(toInject).join(", ")} for MCP`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
debugLog("Plugin initialization complete");
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Start initialization immediately
|
|
364
|
+
initializePlugin().catch(err => {
|
|
365
|
+
debugLog(`Failed to initialize plugin: ${err}`);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
async config(config: any) {
|
|
370
|
+
debugLog("Config hook called");
|
|
371
|
+
// Config hook runs early, we could also initialize here
|
|
372
|
+
// but we're already initializing above
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
async event({ event }: { event: { type: string } }) {
|
|
376
|
+
debugLog(`Received event: ${event.type}`);
|
|
377
|
+
if (event.type === "server.connected") {
|
|
378
|
+
debugLog("Server connected - rechecking authentication");
|
|
379
|
+
// Re-authenticate providers in case they weren't set earlier
|
|
380
|
+
if (opClient) {
|
|
381
|
+
const configEnvId = process.env.OP_CONFIG_ENV_ID;
|
|
382
|
+
if (configEnvId) {
|
|
383
|
+
const vars = await readEnvironmentVariables(configEnvId);
|
|
384
|
+
const configEnvIds: Record<string, string> = {};
|
|
385
|
+
for (const v of vars) {
|
|
386
|
+
if (v.name.endsWith("_ENV_ID") && v.value) {
|
|
387
|
+
configEnvIds[v.name] = v.value;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const providersEnvId = configEnvIds.OPENCODE_PROVIDERS_ENV_ID;
|
|
391
|
+
if (providersEnvId) {
|
|
392
|
+
await authenticateProviders(providersEnvId);
|
|
393
|
+
}
|
|
356
394
|
}
|
|
357
395
|
}
|
|
358
|
-
|
|
359
|
-
debugLog("Initialization complete");
|
|
360
396
|
}
|
|
361
397
|
},
|
|
362
398
|
|
package/package.json
CHANGED
package/index-v2.ts
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
import * as sdk from "@1password/sdk";
|
|
3
|
-
|
|
4
|
-
// Debug logging to file
|
|
5
|
-
const debugLog = (message: string) => {
|
|
6
|
-
const timestamp = new Date().toISOString();
|
|
7
|
-
const logMessage = `[${timestamp}] 1Password: ${message}\n`;
|
|
8
|
-
console.log(logMessage.trim());
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
// Try to write to a debug log file
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
15
|
-
const logDir = path.join(home, '.opencode-1password-debug');
|
|
16
|
-
if (!fs.existsSync(logDir)) {
|
|
17
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
const logFile = path.join(logDir, 'debug.log');
|
|
20
|
-
fs.appendFileSync(logFile, logMessage);
|
|
21
|
-
} catch (err) {
|
|
22
|
-
// Ignore file logging errors
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
interface MCPServerConfig {
|
|
27
|
-
environment?: Record<string, string>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface OpenCodeConfig {
|
|
31
|
-
mcp?: Record<string, MCPServerConfig>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface ProviderAuth {
|
|
35
|
-
type: string;
|
|
36
|
-
key: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface AuthJson {
|
|
40
|
-
[providerId: string]: ProviderAuth;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const OnePasswordAuthPlugin: Plugin = async ({ client, $ }) => {
|
|
44
|
-
let opClient: Awaited<ReturnType<typeof sdk.createClient>> | null = null;
|
|
45
|
-
let mcpsEnvId: string | undefined;
|
|
46
|
-
|
|
47
|
-
debugLog("Plugin loaded");
|
|
48
|
-
|
|
49
|
-
const normalizeProviderId = (variableName: string): string => {
|
|
50
|
-
// Convert common patterns to match auth.json provider IDs
|
|
51
|
-
// e.g., "MINIMAX_CODING_PLAN" -> "minimax-coding-plan"
|
|
52
|
-
// e.g., "OPENCODE" -> "opencode"
|
|
53
|
-
// e.g., "DEEPSEEK" -> "deepseek"
|
|
54
|
-
|
|
55
|
-
let normalized = variableName.toLowerCase();
|
|
56
|
-
|
|
57
|
-
// Convert underscores to hyphens
|
|
58
|
-
normalized = normalized.replace(/_/g, '-');
|
|
59
|
-
|
|
60
|
-
debugLog(`Normalized ${variableName} -> ${normalized}`);
|
|
61
|
-
return normalized;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const getAlternativeEnvVarNames = (providerId: string): string[] => {
|
|
65
|
-
// Generate all possible env var names that might be used
|
|
66
|
-
const names = new Set<string>();
|
|
67
|
-
|
|
68
|
-
// Original providerId (e.g., "minimax-coding-plan")
|
|
69
|
-
names.add(providerId);
|
|
70
|
-
|
|
71
|
-
// Uppercase version (e.g., "MINIMAX-CODING-PLAN")
|
|
72
|
-
names.add(providerId.toUpperCase());
|
|
73
|
-
|
|
74
|
-
// With underscores instead of hyphens (e.g., "minimax_coding_plan")
|
|
75
|
-
names.add(providerId.replace(/-/g, '_'));
|
|
76
|
-
|
|
77
|
-
// Uppercase with underscores (e.g., "MINIMAX_CODING_PLAN")
|
|
78
|
-
names.add(providerId.replace(/-/g, '_').toUpperCase());
|
|
79
|
-
|
|
80
|
-
// Just the first part (e.g., "minimax")
|
|
81
|
-
const firstPart = providerId.split('-')[0];
|
|
82
|
-
if (firstPart && firstPart !== providerId) {
|
|
83
|
-
names.add(firstPart);
|
|
84
|
-
names.add(firstPart.toUpperCase());
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return Array.from(names);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const getHomeDir = (): string => {
|
|
91
|
-
return process.env.HOME || process.env.USERPROFILE || "";
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const getAuthJsonPath = (): string => {
|
|
95
|
-
const home = getHomeDir();
|
|
96
|
-
return `${home}/.local/share/opencode/auth.json`;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const getOpenCodeJsonPath = (): string => {
|
|
100
|
-
const home = getHomeDir();
|
|
101
|
-
return `${home}/.config/opencode/opencode.json`;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const initClient = async () => {
|
|
105
|
-
const token = process.env.OP_SERVICE_ACCOUNT_TOKEN;
|
|
106
|
-
if (!token) {
|
|
107
|
-
debugLog("Missing OP_SERVICE_ACCOUNT_TOKEN");
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
return await sdk.createClient({
|
|
113
|
-
auth: token,
|
|
114
|
-
integrationName: "opencode-1password-env",
|
|
115
|
-
integrationVersion: "1.0.0",
|
|
116
|
-
});
|
|
117
|
-
} catch (err) {
|
|
118
|
-
debugLog(`Failed to create client: ${err}`);
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const readEnvironmentVariables = async (envId: string) => {
|
|
124
|
-
if (!opClient) return [];
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const { variables } = await opClient.environments.getVariables(envId);
|
|
128
|
-
debugLog(`Read ${variables?.length || 0} variables from environment ${envId}`);
|
|
129
|
-
return variables || [];
|
|
130
|
-
} catch (err) {
|
|
131
|
-
debugLog(`Failed to read environment ${envId}: ${err}`);
|
|
132
|
-
return [];
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const getSecretsFromEnvironment = async (envId: string): Promise<Record<string, string>> => {
|
|
137
|
-
const vars = await readEnvironmentVariables(envId);
|
|
138
|
-
const secrets: Record<string, string> = {};
|
|
139
|
-
for (const v of vars) {
|
|
140
|
-
if (v.value) {
|
|
141
|
-
secrets[v.name] = v.value;
|
|
142
|
-
debugLog(`Found secret: ${v.name} (${v.value.length} chars)`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return secrets;
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const updateAuthJson = async (providerEnvId: string): Promise<void> => {
|
|
149
|
-
const authJsonPath = getAuthJsonPath();
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
// Read current auth.json using cat
|
|
153
|
-
const catResult = await $`cat "${authJsonPath}"`;
|
|
154
|
-
const content = catResult.stdout;
|
|
155
|
-
let auth: AuthJson;
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
auth = JSON.parse(content);
|
|
159
|
-
} catch {
|
|
160
|
-
debugLog("auth.json is not valid JSON, skipping");
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
let modified = false;
|
|
165
|
-
|
|
166
|
-
for (const [providerId, authConfig] of Object.entries(auth)) {
|
|
167
|
-
if (authConfig.key && !authConfig.key.startsWith("{env:")) {
|
|
168
|
-
// Replace hardcoded key with env var reference
|
|
169
|
-
authConfig.key = `{env:${providerId}}`;
|
|
170
|
-
modified = true;
|
|
171
|
-
debugLog(`Updated auth.json - ${providerId} -> {env:${providerId}}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (modified) {
|
|
176
|
-
// Write updated content using a temp file and mv
|
|
177
|
-
const newContent = JSON.stringify(auth, null, 2);
|
|
178
|
-
// Use node to write the file since $ heredocs can be tricky
|
|
179
|
-
await $`node -e "const fs=require('fs'); fs.writeFileSync('${authJsonPath}', JSON.stringify(${JSON.stringify(auth)}, null, 2));"`;
|
|
180
|
-
debugLog("auth.json updated to use environment variables");
|
|
181
|
-
}
|
|
182
|
-
} catch (err) {
|
|
183
|
-
debugLog(`Failed to update auth.json: ${err}`);
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const updateOpenCodeJsonMCP = async (mcpEnvId: string): Promise<void> => {
|
|
188
|
-
const openCodeJsonPath = getOpenCodeJsonPath();
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
// Read current opencode.json using cat
|
|
192
|
-
const catResult = await $`cat "${openCodeJsonPath}"`;
|
|
193
|
-
const content = catResult.stdout;
|
|
194
|
-
let config: OpenCodeConfig;
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
config = JSON.parse(content);
|
|
198
|
-
} catch {
|
|
199
|
-
debugLog("opencode.json is not valid JSON, skipping");
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!config.mcp) {
|
|
204
|
-
debugLog("No MCP configuration found, skipping");
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let modified = false;
|
|
209
|
-
|
|
210
|
-
for (const [serverName, serverConfig] of Object.entries(config.mcp)) {
|
|
211
|
-
if (serverConfig?.environment) {
|
|
212
|
-
for (const [key, value] of Object.entries(serverConfig.environment)) {
|
|
213
|
-
if (typeof value === "string" && !value.startsWith("{env:") && !value.startsWith("$")) {
|
|
214
|
-
// Replace hardcoded value with env var reference
|
|
215
|
-
serverConfig.environment[key] = `{env:${key}}`;
|
|
216
|
-
modified = true;
|
|
217
|
-
debugLog(`Updated opencode.json - ${serverName}.${key} -> {env:${key}}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (modified) {
|
|
224
|
-
// Write updated content
|
|
225
|
-
await $`node -e "const fs=require('fs'); fs.writeFileSync('${openCodeJsonPath}', JSON.stringify(${JSON.stringify(config)}, null, 2));"`;
|
|
226
|
-
debugLog("opencode.json MCP config updated to use environment variables");
|
|
227
|
-
}
|
|
228
|
-
} catch (err) {
|
|
229
|
-
debugLog(`Failed to update opencode.json: ${err}`);
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const authenticateProviders = async (providerEnvId: string): Promise<void> => {
|
|
234
|
-
if (!opClient) return;
|
|
235
|
-
|
|
236
|
-
debugLog(`Reading provider secrets from environment ${providerEnvId}`);
|
|
237
|
-
const secrets = await getSecretsFromEnvironment(providerEnvId);
|
|
238
|
-
debugLog(`Found ${Object.keys(secrets).length} provider secrets`);
|
|
239
|
-
|
|
240
|
-
for (const [variableName, apiKey] of Object.entries(secrets)) {
|
|
241
|
-
if (!apiKey) continue;
|
|
242
|
-
|
|
243
|
-
const providerId = normalizeProviderId(variableName);
|
|
244
|
-
debugLog(`Processing ${variableName} -> ${providerId} (key length: ${apiKey.length})`);
|
|
245
|
-
|
|
246
|
-
// Set environment variables in multiple formats
|
|
247
|
-
const envVarNames = getAlternativeEnvVarNames(providerId);
|
|
248
|
-
for (const envVarName of envVarNames) {
|
|
249
|
-
process.env[envVarName] = apiKey;
|
|
250
|
-
debugLog(`Set process.env.${envVarName} = ${apiKey.substring(0, 8)}...`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
// Authenticate with OpenCode runtime
|
|
255
|
-
debugLog(`Calling client.auth.set for ${providerId}`);
|
|
256
|
-
await client.auth.set({
|
|
257
|
-
path: { id: providerId },
|
|
258
|
-
body: { type: "api", key: apiKey },
|
|
259
|
-
});
|
|
260
|
-
debugLog(`✓ ${providerId} authenticated (from ${variableName})`);
|
|
261
|
-
} catch (err) {
|
|
262
|
-
debugLog(`Failed to authenticate ${providerId}: ${err}`);
|
|
263
|
-
if (err instanceof Error) {
|
|
264
|
-
debugLog(`Error message: ${err.message}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const getConfiguredMCPVars = (): Set<string> => {
|
|
271
|
-
const vars = new Set<string>();
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const config = client.config as OpenCodeConfig;
|
|
275
|
-
if (config?.mcp) {
|
|
276
|
-
for (const [, serverConfig] of Object.entries(config.mcp)) {
|
|
277
|
-
if (serverConfig?.environment) {
|
|
278
|
-
for (const [key] of Object.entries(serverConfig.environment)) {
|
|
279
|
-
if (key.startsWith("{env:")) {
|
|
280
|
-
const envVar = key.slice(5, -1);
|
|
281
|
-
vars.add(envVar);
|
|
282
|
-
} else {
|
|
283
|
-
vars.add(key);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
} catch (err) {
|
|
290
|
-
debugLog(`Failed to read MCP config: ${err}`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return vars;
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const injectMCPSecrets = async (mcpEnvId: string): Promise<Record<string, string>> => {
|
|
297
|
-
const neededVars = getConfiguredMCPVars();
|
|
298
|
-
if (neededVars.size === 0) {
|
|
299
|
-
return {};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const secrets = await getSecretsFromEnvironment(mcpEnvId);
|
|
303
|
-
const toInject: Record<string, string> = {};
|
|
304
|
-
|
|
305
|
-
for (const varName of neededVars) {
|
|
306
|
-
if (secrets[varName]) {
|
|
307
|
-
toInject[varName] = secrets[varName];
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return toInject;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
async event({ event }: { event: { type: string } }) {
|
|
316
|
-
debugLog(`Received event: ${event.type}`);
|
|
317
|
-
if (event.type === "server.connected") {
|
|
318
|
-
debugLog("Initializing...");
|
|
319
|
-
|
|
320
|
-
opClient = await initClient();
|
|
321
|
-
if (!opClient) {
|
|
322
|
-
debugLog("No client available");
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const configEnvId = process.env.OP_CONFIG_ENV_ID;
|
|
327
|
-
if (!configEnvId) {
|
|
328
|
-
debugLog("Missing OP_CONFIG_ENV_ID");
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
debugLog(`Reading config from environment ${configEnvId}`);
|
|
333
|
-
const vars = await readEnvironmentVariables(configEnvId);
|
|
334
|
-
const configEnvIds: Record<string, string> = {};
|
|
335
|
-
for (const v of vars) {
|
|
336
|
-
if (v.name.endsWith("_ENV_ID") && v.value) {
|
|
337
|
-
configEnvIds[v.name] = v.value;
|
|
338
|
-
debugLog(`Found ${v.name} = ${v.value.substring(0, 10)}...`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const providersEnvId = configEnvIds.OPENCODE_PROVIDERS_ENV_ID;
|
|
343
|
-
if (providersEnvId) {
|
|
344
|
-
// First update the config files to use env var references
|
|
345
|
-
await updateAuthJson(providersEnvId);
|
|
346
|
-
await authenticateProviders(providersEnvId);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const mcpEnvIdFromConfig = configEnvIds.OPENCODE_MCPS_ENV_ID;
|
|
350
|
-
if (mcpEnvIdFromConfig) {
|
|
351
|
-
mcpsEnvId = mcpEnvIdFromConfig;
|
|
352
|
-
await updateOpenCodeJsonMCP(mcpEnvIdFromConfig);
|
|
353
|
-
const toInject = await injectMCPSecrets(mcpEnvIdFromConfig);
|
|
354
|
-
if (Object.keys(toInject).length > 0) {
|
|
355
|
-
debugLog(`Injected ${Object.keys(toInject).join(", ")} for MCP`);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
debugLog("Initialization complete");
|
|
360
|
-
}
|
|
361
|
-
},
|
|
362
|
-
|
|
363
|
-
async "shell.env"(input, output) {
|
|
364
|
-
if (!opClient || !mcpsEnvId) return;
|
|
365
|
-
|
|
366
|
-
const toInject = await injectMCPSecrets(mcpsEnvId);
|
|
367
|
-
|
|
368
|
-
for (const [varName, value] of Object.entries(toInject)) {
|
|
369
|
-
output.env[varName] = value;
|
|
370
|
-
debugLog(`Injected ${varName} into shell environment`);
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
async "tool.execute.before"(input) {
|
|
375
|
-
if (input.tool !== "read") return;
|
|
376
|
-
|
|
377
|
-
const fileArgs = input.args as { filePath?: string } | undefined;
|
|
378
|
-
if (fileArgs?.filePath && fileArgs.filePath.includes(".env")) {
|
|
379
|
-
debugLog(
|
|
380
|
-
"Warning - .env file access detected. Consider using 1Password for secrets management."
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
export default OnePasswordAuthPlugin;
|