@weiyentan/opencode-plugin-awx 0.2.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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/auth.d.ts +112 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +180 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +148 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +334 -0
- package/dist/client.js.map +1 -0
- package/dist/contracts/job-detail.d.ts +141 -0
- package/dist/contracts/job-detail.d.ts.map +1 -0
- package/dist/contracts/job-detail.js +98 -0
- package/dist/contracts/job-detail.js.map +1 -0
- package/dist/contracts/sync-project.d.ts +31 -0
- package/dist/contracts/sync-project.d.ts.map +1 -0
- package/dist/contracts/sync-project.js +30 -0
- package/dist/contracts/sync-project.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +754 -0
- package/dist/index.js.map +1 -0
- package/dist/job-status.d.ts +116 -0
- package/dist/job-status.d.ts.map +1 -0
- package/dist/job-status.js +168 -0
- package/dist/job-status.js.map +1 -0
- package/dist/launch.d.ts +76 -0
- package/dist/launch.d.ts.map +1 -0
- package/dist/launch.js +133 -0
- package/dist/launch.js.map +1 -0
- package/dist/list-projects.d.ts +63 -0
- package/dist/list-projects.d.ts.map +1 -0
- package/dist/list-projects.js +84 -0
- package/dist/list-projects.js.map +1 -0
- package/dist/list-templates.d.ts +60 -0
- package/dist/list-templates.d.ts.map +1 -0
- package/dist/list-templates.js +120 -0
- package/dist/list-templates.js.map +1 -0
- package/dist/metrics.d.ts +174 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +275 -0
- package/dist/metrics.js.map +1 -0
- package/dist/transforms.d.ts +52 -0
- package/dist/transforms.d.ts.map +1 -0
- package/dist/transforms.js +108 -0
- package/dist/transforms.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 weiyentan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# AWX Plugin (`@weiyentan/opencode-plugin-awx`)
|
|
2
|
+
|
|
3
|
+
OpenCode server plugin for [AWX](https://github.com/ansible/awx) / Ansible Automation Platform (AAP). Provides native tool access to job templates, projects, and job lifecycle operations.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
✅ **Phase 0 — Repository Scaffolding** (complete)
|
|
8
|
+
✅ **Phase 1 — Client Infrastructure** (complete)
|
|
9
|
+
✅ **Phase 2 — Tool Implementation** (complete)
|
|
10
|
+
|
|
11
|
+
The AWX plugin delivers these modules:
|
|
12
|
+
|
|
13
|
+
| Module | File | Purpose |
|
|
14
|
+
|--------|------|---------|
|
|
15
|
+
| **Plugin entry** | `src/index.ts` | Registers all AWX tools (awx-list-templates, awx-list-projects, awx-launch-job, awx-job-status, awx-wait-job, awx-get-job-events, awx-sync-project) + hello-world scaffold; wires HTTP client, metrics lifecycle, and dispose hook |
|
|
16
|
+
| **Auth hook** | `src/auth.ts` | Bearer token / PAT authentication via OpenCode's `type: "api"` auth hook with init-time validation |
|
|
17
|
+
| **Output contract** | `src/contracts/job-detail.ts` | Zod schemas and TypeScript types (`JobDetailOutput`) matching `awx_job_detail.py` v1.0 |
|
|
18
|
+
| **Transforms** | `src/transforms.ts` | Pure functions: SSH→HTTPS URL conversion, git branch inference, required-var validation |
|
|
19
|
+
| **Client middleware** | `src/client.ts` | HTTP middleware pipeline: circuit breaker, retry/backoff, timeout via native `fetch` |
|
|
20
|
+
| **Metrics** | `src/metrics.ts` | Per-tool counters with file-backed durability for operational visibility |
|
|
21
|
+
| **Node shim** | `src/node-shim.d.ts` | Minimal Node.js built-in declarations (avoids `@types/node` dependency) |
|
|
22
|
+
| **Snapshot generator** | `scripts/generate-snapshots.py` | Python script that regenerates contract snapshots from fixture data |
|
|
23
|
+
|
|
24
|
+
Tool implementation (Phase 2) is complete — all 7 AWX tools are implemented and tested. See the [issue tracker](https://github.com/weiyentan/opencode-plugins/issues) for upcoming enhancements.
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
- **Node.js** >= 18.0.0 (Node 18 compatibility is handled transparently — the client middleware includes `anyAbortSignal()` and `createTimeoutSignal()` fallbacks)
|
|
29
|
+
- **npm** >= 9.0.0 (ships with Node.js 18)
|
|
30
|
+
- TypeScript knowledge for plugin development
|
|
31
|
+
- Access to an AAP instance for integration testing (optional — unit tests run offline)
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# From the monorepo root:
|
|
37
|
+
npm install
|
|
38
|
+
|
|
39
|
+
# Or from this package directly:
|
|
40
|
+
cd packages/awx
|
|
41
|
+
npm install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This package is consumed by the OpenCode plugin server as a dependency — no standalone runtime is needed.
|
|
45
|
+
|
|
46
|
+
## Scripts
|
|
47
|
+
|
|
48
|
+
| Command | Description |
|
|
49
|
+
|---------|-------------|
|
|
50
|
+
| `npm run build` | Compile TypeScript to `dist/` (`tsc`) |
|
|
51
|
+
| `npm test` | Run the Vitest test suite (`vitest run`) |
|
|
52
|
+
| `npm run test:watch` | Run tests in watch mode |
|
|
53
|
+
| `npm run lint` | Type-check without emitting (`tsc --noEmit`) |
|
|
54
|
+
| `npm run typecheck` | Alias for `lint` — strict type checking |
|
|
55
|
+
|
|
56
|
+
## Testing
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Unit tests (no AAP instance required)
|
|
60
|
+
npm test
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Tests follow TDD (test-driven development) with [Vitest](https://vitest.dev) and verify behavior through the public plugin interface.
|
|
64
|
+
|
|
65
|
+
### Running Integration Tests
|
|
66
|
+
|
|
67
|
+
Integration tests in `tests/integration/` exercise the plugin's tools against a **live AAP instance** through the plugin's own tool registration mechanism.
|
|
68
|
+
|
|
69
|
+
#### Read-Only Tools
|
|
70
|
+
|
|
71
|
+
The suite `tests/integration/read-only.test.ts` covers `awx-list-templates` and `awx-list-projects`.
|
|
72
|
+
|
|
73
|
+
#### Job Lifecycle Tools
|
|
74
|
+
|
|
75
|
+
The suite `tests/integration/job-lifecycle.test.ts` covers the full AWX job lifecycle:
|
|
76
|
+
|
|
77
|
+
- `awx-launch-job` → launches a job template
|
|
78
|
+
- `awx-job-status` → fetches structured job detail (v1.0 output contract)
|
|
79
|
+
- `awx-wait-job` → non-blocking status check (no polling)
|
|
80
|
+
- `awx-get-job-events` → retrieves job events
|
|
81
|
+
|
|
82
|
+
#### Prerequisites
|
|
83
|
+
|
|
84
|
+
| Env Var | Default | Required | Description |
|
|
85
|
+
|---------|---------|----------|-------------|
|
|
86
|
+
| `AWX_TOKEN` | — | **Yes** | Valid AAP Personal Access Token (PAT) |
|
|
87
|
+
| `AAP_BASE_URL` | `https://example.com` | No | Base URL of the AAP instance |
|
|
88
|
+
| `JOB_TEMPLATE_ID` | `10` | No | Non-production AWX job template ID to launch |
|
|
89
|
+
| `EXTRA_VARS_INVENTORY` | `"test"` | No | Inventory name for extra_vars |
|
|
90
|
+
| `EXTRA_VARS_SCM_URL` | `"https://github.com/example/repo.git"` | No | SCM URL for extra_vars |
|
|
91
|
+
| `EXTRA_VARS_SCM_BRANCH` | `"main"` | No | SCM branch for extra_vars |
|
|
92
|
+
|
|
93
|
+
> **Important:** Use a non-production job template. The launch tool starts a real job on AAP. The plugin's transforms pipeline requires `inventory`, `scm_url`, and `scm_branch` in extra_vars — configure them via env vars to match your template's expectations.
|
|
94
|
+
|
|
95
|
+
#### Run Command
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# From packages/awx/
|
|
99
|
+
export AWX_TOKEN=your_pat_token_here
|
|
100
|
+
npx vitest run tests/integration/
|
|
101
|
+
|
|
102
|
+
# Run a specific suite:
|
|
103
|
+
npx vitest run tests/integration/read-only.test.ts
|
|
104
|
+
npx vitest run tests/integration/job-lifecycle.test.ts
|
|
105
|
+
|
|
106
|
+
# With custom AAP URL:
|
|
107
|
+
export AWX_TOKEN=your_pat_token_here
|
|
108
|
+
export AAP_BASE_URL=https://my-aap.internal.example.com
|
|
109
|
+
npx vitest run tests/integration/
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> **Note**: Integration tests are gated behind `AWX_TOKEN`. When `AWX_TOKEN` is not set, the live AAP tests are silently skipped using `describe.skipIf(!process.env.AWX_TOKEN)`.
|
|
113
|
+
|
|
114
|
+
#### Agent-Side Polling Pattern
|
|
115
|
+
|
|
116
|
+
Job lifecycle tools use an **agent-side polling** pattern (see ADR 0004):
|
|
117
|
+
- `awx-launch-job` returns immediately with a job ID.
|
|
118
|
+
- `awx-job-status` / `awx-wait-job` return the current status — the agent must loop to poll for completion.
|
|
119
|
+
- `awx-get-job-events` retrieves events from a completed or running job.
|
|
120
|
+
|
|
121
|
+
No tool blocks waiting for job completion. This avoids hanging the agent's execution loop and gives the agent control over polling strategy (poll interval, max attempts, timeout).
|
|
122
|
+
|
|
123
|
+
### Contract Tests
|
|
124
|
+
|
|
125
|
+
Contract tests (`tests/contract.test.ts`) validate that the TypeScript `JobDetailOutput` interface and zod schema match the Python `awx_job_detail.py` v1.0 output contract. A **snapshot-based approach** is used for CI safety:
|
|
126
|
+
|
|
127
|
+
- Fixture JSON files in `tests/fixtures/` represent pre-baked snapshots of the Python output contract.
|
|
128
|
+
- Tests parse each fixture through the zod schema and assert structural correctness.
|
|
129
|
+
- No live Python subprocess is executed — the tests are pure TypeScript and run in any CI environment.
|
|
130
|
+
|
|
131
|
+
#### Fixture Files
|
|
132
|
+
|
|
133
|
+
| Fixture | Purpose |
|
|
134
|
+
|---------|---------|
|
|
135
|
+
| `awx_job_success.json` | A successful job with no failures or unreachable hosts |
|
|
136
|
+
| `awx_job_partial.json` | A job that completed with some unreachable hosts |
|
|
137
|
+
| `awx_job_failure.json` | A failed job with task-level errors |
|
|
138
|
+
|
|
139
|
+
#### Regenerating Contract Snapshots
|
|
140
|
+
|
|
141
|
+
When the Python `awx_job_detail.py` v1.0 output contract changes (e.g., new fields are added or field types change), the fixture snapshots must be regenerated:
|
|
142
|
+
|
|
143
|
+
1. Run the Python `awx_job_detail.py` module against a live AAP instance to produce updated JSON output for each representative job state (success, partial, failure).
|
|
144
|
+
2. Replace the corresponding fixture files in `tests/fixtures/` with the new output.
|
|
145
|
+
3. Update `src/contracts/job-detail.ts` if the schema has changed (add/modify zod fields and TypeScript types).
|
|
146
|
+
4. Run `npm test` to verify the new snapshots pass schema validation.
|
|
147
|
+
5. Commit the updated fixtures and schema together — they must stay in lockstep.
|
|
148
|
+
|
|
149
|
+
> **Important**: Fixtures are checked into the repository. They serve as the canonical reference for what the Python output contract produces. If you change the Python code without updating the fixtures, contract tests will catch the mismatch.
|
|
150
|
+
## Hot-Reload
|
|
151
|
+
|
|
152
|
+
The OpenCode plugin server watches plugin source files and **automatically reloads** when changes are detected — no server restart required. This was verified during initial scaffolding:
|
|
153
|
+
|
|
154
|
+
1. The plugin is registered by the OpenCode server (consuming `src/index.ts` as the entry point).
|
|
155
|
+
2. Modifying the tool's `description` field in `src/index.ts` (e.g., changing the hello-world description text) triggers a plugin reload.
|
|
156
|
+
3. The server picks up the new description on the next tool invocation.
|
|
157
|
+
|
|
158
|
+
### Known Limitation
|
|
159
|
+
|
|
160
|
+
Hot-reload verification is performed structurally (the `tsc --noEmit` / `vitest run` cycle confirms the module compiles and tool execute signature is correct) but end-to-end hot-reload testing requires a running OpenCode server instance. Full integration testing of hot-reload behavior is tracked for a future enhancement.
|
|
161
|
+
|
|
162
|
+
### Entry Points
|
|
163
|
+
|
|
164
|
+
The `package.json` `main`, `types`, and `exports` fields point to the compiled `dist/` output. This is the production-safe configuration — consumers import the compiled JavaScript with type declarations.
|
|
165
|
+
|
|
166
|
+
For local development, the OpenCode plugin server can consume TypeScript source directly by overriding the entry point (e.g., changing `main` to `./src/index.ts`). The server watches source files and reloads automatically on change — no server restart required.
|
|
167
|
+
|
|
168
|
+
## CI Requirements
|
|
169
|
+
|
|
170
|
+
CI pipelines must run the following in order:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm ci # Clean install
|
|
174
|
+
npm run lint # Type checking
|
|
175
|
+
npm test # Unit tests
|
|
176
|
+
npm run build # Production build
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Required CI Environment
|
|
180
|
+
|
|
181
|
+
- **Node.js** 18.x or 20.x LTS
|
|
182
|
+
- **npm** 9.x+
|
|
183
|
+
- No secrets required for unit tests (`npm test`)
|
|
184
|
+
- Integration tests are gated behind `AWX_TOKEN` and run separately
|
|
185
|
+
|
|
186
|
+
## Toolchain Decisions
|
|
187
|
+
|
|
188
|
+
### npm Workspaces (not pnpm)
|
|
189
|
+
|
|
190
|
+
This package lives inside an **npm workspaces** monorepo. The root `package.json` declares `packages/*` as workspaces. npm workspaces were chosen over pnpm for:
|
|
191
|
+
|
|
192
|
+
1. **Zero-install overhead** — npm ships with Node.js, no extra package manager needed.
|
|
193
|
+
2. **CI simplicity** — GitHub Actions runners include npm by default.
|
|
194
|
+
3. **Hoisting model** — npm's flat `node_modules` layout avoids pnpm's strict isolation issues with certain tool packages.
|
|
195
|
+
|
|
196
|
+
### Single tsconfig.json (not project references)
|
|
197
|
+
|
|
198
|
+
A single `tsconfig.json` at `packages/awx/tsconfig.json` is used instead of TypeScript project references (`references` + composite builds). This was chosen because:
|
|
199
|
+
|
|
200
|
+
1. **Single-package scope** — this is one package in a workspaces monorepo, not a multi-package composite build.
|
|
201
|
+
2. **Simplicity** — a single config is easier to author, debug, and integrate with Vitest (which resolves TypeScript via its own transform).
|
|
202
|
+
3. **Future readiness** — if the plugin is split into sub-packages (e.g., `packages/awx-core`, `packages/awx-transforms`), we will adopt project references at that point.
|
|
203
|
+
|
|
204
|
+
## Architecture
|
|
205
|
+
|
|
206
|
+
### Module Structure
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
packages/awx/
|
|
210
|
+
├── src/
|
|
211
|
+
│ ├── index.ts # Plugin entry point — Hooks (auth + tools + dispose); client wiring & metrics lifecycle
|
|
212
|
+
│ ├── auth.ts # Bearer token auth hook (type: "api")
|
|
213
|
+
│ ├── client.ts # HTTP middleware pipeline (circuit breaker, retry, timeout)
|
|
214
|
+
│ ├── metrics.ts # Per-tool counters with file-backed durability
|
|
215
|
+
│ ├── node-shim.d.ts # Minimal Node.js declarations (fs/promises, path)
|
|
216
|
+
│ └── contracts/
|
|
217
|
+
│ └── job-detail.ts # JobDetailOutput v1.0 TypeScript interface
|
|
218
|
+
├── tests/
|
|
219
|
+
│ ├── plugin.test.ts # Plugin registration and lifecycle tests
|
|
220
|
+
│ ├── client.test.ts # Client middleware pipeline tests
|
|
221
|
+
│ ├── lifecycle.test.ts # Lazy client/auth lifecycle tests (no-token → token → client-created)
|
|
222
|
+
│ ├── metrics.test.ts # MetricsStore persistence & counter tests incl. concurrent serialization
|
|
223
|
+
│ ├── plugin-init-timeout.test.ts # Init-time timeout cleanup tests (clear() called after validation)
|
|
224
|
+
│ ├── contracts/
|
|
225
|
+
│ │ ├── contract.test.ts # Contract compatibility tests
|
|
226
|
+
│ │ └── __snapshots__/ # Canonical contract output (ground truth)
|
|
227
|
+
│ └── fixtures/
|
|
228
|
+
│ ├── awx_job_success.json
|
|
229
|
+
│ ├── awx_job_partial.json
|
|
230
|
+
│ └── awx_job_failure.json
|
|
231
|
+
└── scripts/
|
|
232
|
+
└── generate-snapshots.py # Python script to regenerate snapshots
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Auth Hook
|
|
236
|
+
|
|
237
|
+
The plugin uses OpenCode's `type: "api"` auth hook for bearer token (Personal Access Token) authentication. On plugin load:
|
|
238
|
+
|
|
239
|
+
1. If a PAT was previously stored, init-time validation calls `GET /api/v2/me/` with a 10s timeout to verify the token is still active.
|
|
240
|
+
2. If validation fails, a clear actionable error is logged: "AWX token is invalid or expired."
|
|
241
|
+
3. If no token is stored yet, the plugin loads gracefully and the user is prompted when they first use an AWX tool.
|
|
242
|
+
|
|
243
|
+
### Output Contract
|
|
244
|
+
|
|
245
|
+
All job-related tools return output matching the `JobDetailOutput` interface (see `src/contracts/job-detail.ts`). This interface is the exact TypeScript representation of the `awx_job_detail.py` v1.0 schema. Key naming:
|
|
246
|
+
|
|
247
|
+
- Use `host_status_counts` — NOT `host_summary`
|
|
248
|
+
- Use `derived` — NOT `extra_vars_summary`
|
|
249
|
+
- `related` fields are resolved names, not raw URLs
|
|
250
|
+
|
|
251
|
+
See the [Architecture Decision Records](../../docs/adr/) in the monorepo for design rationale:
|
|
252
|
+
|
|
253
|
+
- **ADR 0001**: Auth strategy (bearer token / PAT)
|
|
254
|
+
- **ADR 0002**: Output contract schema
|
|
255
|
+
- **ADR 0003**: Resilience patterns (retry, timeout, circuit breaker)
|
|
256
|
+
- **ADR 0004**: Agent-side polling (job lifecycle)
|
|
257
|
+
- **ADR 0005**: Extra-variable transforms (SSH→HTTPS, branch inference)
|
|
258
|
+
- **ADR 0006**: Error taxonomy and structured error reporting
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWX Auth Hook — Bearer Token (PAT) Authentication
|
|
3
|
+
*
|
|
4
|
+
* Implements OpenCode's `type: "api"` auth hook pattern for AAP bearer token
|
|
5
|
+
* credential storage and injection. The user provides a Personal Access Token
|
|
6
|
+
* (PAT) generated from their AAP instance, and this hook stores it as the
|
|
7
|
+
* plugin's auth key.
|
|
8
|
+
*
|
|
9
|
+
* ## Auth Flow
|
|
10
|
+
*
|
|
11
|
+
* 1. On first plugin load, OpenCode prompts for a PAT via the auth hook text prompt.
|
|
12
|
+
* 2. The `authorize()` function returns the PAT as the secret key.
|
|
13
|
+
* 3. On every plugin load, init-time validation calls `GET /api/v2/me/`
|
|
14
|
+
* to verify the token is still active.
|
|
15
|
+
* 4. If validation fails, the user receives a clear, actionable error message.
|
|
16
|
+
*
|
|
17
|
+
* ## Token Validation
|
|
18
|
+
*
|
|
19
|
+
* Validation happens at plugin init time (NOT on first tool call) to provide
|
|
20
|
+
* immediate, clear feedback. Failed init-time validation logs an actionable
|
|
21
|
+
* error, but plugin initialization continues so the user can re-authenticate
|
|
22
|
+
* or fix configuration.
|
|
23
|
+
*
|
|
24
|
+
* ## Error Messages
|
|
25
|
+
*
|
|
26
|
+
* Failed auth produces user-actionable error messages:
|
|
27
|
+
* - No token configured: "AWX auth not configured. Set your Personal Access
|
|
28
|
+
* Token in the plugin settings."
|
|
29
|
+
* - Network failure: "Cannot reach AAP at <baseUrl>. Check your baseUrl in
|
|
30
|
+
* opencode.jsonc and ensure the AAP instance is accessible."
|
|
31
|
+
* - Invalid token (401): "AWX token is invalid or expired. Generate a new
|
|
32
|
+
* Personal Access Token at <baseUrl>/api/v2/tokens/ or Profile → Tokens."
|
|
33
|
+
* - Forbidden (403): "AWX token lacks sufficient permissions. Ensure the
|
|
34
|
+
* token has at least Read access."
|
|
35
|
+
*
|
|
36
|
+
* ## Reference
|
|
37
|
+
*
|
|
38
|
+
* - ADR 0001: Bearer Token Authentication for AWX Plugin
|
|
39
|
+
* - ADR 0003: Plugin API Surface Discovery
|
|
40
|
+
* - @opencode-ai/plugin auth hook docs
|
|
41
|
+
*/
|
|
42
|
+
/** Result of an auth validation attempt */
|
|
43
|
+
export interface AuthValidationResult {
|
|
44
|
+
/** Whether the token is valid and AAP is reachable */
|
|
45
|
+
valid: boolean;
|
|
46
|
+
/** User-facing error message if validation failed (null if valid) */
|
|
47
|
+
error: string | null;
|
|
48
|
+
/** HTTP status code from the validation request (null if network error) */
|
|
49
|
+
status: number | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validates the bearer token by making a GET request to /api/v2/me/.
|
|
53
|
+
*
|
|
54
|
+
* This is the init-time validation called on plugin load. It checks:
|
|
55
|
+
* 1. The AAP instance is reachable at the configured baseUrl
|
|
56
|
+
* 2. The bearer token is active and valid
|
|
57
|
+
*
|
|
58
|
+
* Returns a structured result so the caller can surface clear error
|
|
59
|
+
* messages to the user.
|
|
60
|
+
*
|
|
61
|
+
* @param baseUrl The configured AAP base URL (e.g., "https://example.com")
|
|
62
|
+
* @param token The bearer token (PAT) to validate
|
|
63
|
+
* @param signal Optional AbortSignal for timeout (recommend 10s)
|
|
64
|
+
*/
|
|
65
|
+
export declare function validateToken(baseUrl: string, token: string, signal?: AbortSignal): Promise<AuthValidationResult>;
|
|
66
|
+
/**
|
|
67
|
+
* Creates an AWX auth hook configuration for the OpenCode plugin server.
|
|
68
|
+
*
|
|
69
|
+
* Uses the `type: "api"` auth method — the standard OpenCode pattern for
|
|
70
|
+
* non-rotating, user-provided tokens (e.g., API keys, PATs).
|
|
71
|
+
*
|
|
72
|
+
* The `authorize()` function simply returns the user's PAT as the key.
|
|
73
|
+
* Validation is handled separately at init time via `validateToken()`.
|
|
74
|
+
*
|
|
75
|
+
* @returns Auth hook configuration compatible with OpenCode's Hooks.auth
|
|
76
|
+
*/
|
|
77
|
+
export declare function createAwxAuthHook(): {
|
|
78
|
+
provider: string;
|
|
79
|
+
methods: {
|
|
80
|
+
type: "api";
|
|
81
|
+
label: string;
|
|
82
|
+
prompts: {
|
|
83
|
+
type: "text";
|
|
84
|
+
key: string;
|
|
85
|
+
message: string;
|
|
86
|
+
}[];
|
|
87
|
+
/**
|
|
88
|
+
* Authorize the user's PAT.
|
|
89
|
+
*
|
|
90
|
+
* Returns the raw token as the secret key. OpenCode stores this
|
|
91
|
+
* securely and injects it into tool requests as the auth key.
|
|
92
|
+
*
|
|
93
|
+
* Empty or whitespace-only tokens fail authorization.
|
|
94
|
+
*
|
|
95
|
+
* We include a message field for better UX. If OpenCode ignores unknown
|
|
96
|
+
* fields, the prompt text above still provides actionable guidance.
|
|
97
|
+
*
|
|
98
|
+
* If the OpenCode auth type narrows this contract in future, remove the
|
|
99
|
+
* message field and rely on the prompt text.
|
|
100
|
+
*/
|
|
101
|
+
authorize(inputs: Record<string, string>): Promise<{
|
|
102
|
+
type: "failed";
|
|
103
|
+
message: string;
|
|
104
|
+
key?: undefined;
|
|
105
|
+
} | {
|
|
106
|
+
type: "success";
|
|
107
|
+
key: string;
|
|
108
|
+
message?: undefined;
|
|
109
|
+
}>;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,KAAK,EAAE,OAAO,CAAC;IACf,qEAAqE;IACrE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAyE/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB;;;;;;;;;;QAezB;;;;;;;;;;;;;WAaG;0BACqB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;;;;;;;;;;EAiBrD"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWX Auth Hook — Bearer Token (PAT) Authentication
|
|
3
|
+
*
|
|
4
|
+
* Implements OpenCode's `type: "api"` auth hook pattern for AAP bearer token
|
|
5
|
+
* credential storage and injection. The user provides a Personal Access Token
|
|
6
|
+
* (PAT) generated from their AAP instance, and this hook stores it as the
|
|
7
|
+
* plugin's auth key.
|
|
8
|
+
*
|
|
9
|
+
* ## Auth Flow
|
|
10
|
+
*
|
|
11
|
+
* 1. On first plugin load, OpenCode prompts for a PAT via the auth hook text prompt.
|
|
12
|
+
* 2. The `authorize()` function returns the PAT as the secret key.
|
|
13
|
+
* 3. On every plugin load, init-time validation calls `GET /api/v2/me/`
|
|
14
|
+
* to verify the token is still active.
|
|
15
|
+
* 4. If validation fails, the user receives a clear, actionable error message.
|
|
16
|
+
*
|
|
17
|
+
* ## Token Validation
|
|
18
|
+
*
|
|
19
|
+
* Validation happens at plugin init time (NOT on first tool call) to provide
|
|
20
|
+
* immediate, clear feedback. Failed init-time validation logs an actionable
|
|
21
|
+
* error, but plugin initialization continues so the user can re-authenticate
|
|
22
|
+
* or fix configuration.
|
|
23
|
+
*
|
|
24
|
+
* ## Error Messages
|
|
25
|
+
*
|
|
26
|
+
* Failed auth produces user-actionable error messages:
|
|
27
|
+
* - No token configured: "AWX auth not configured. Set your Personal Access
|
|
28
|
+
* Token in the plugin settings."
|
|
29
|
+
* - Network failure: "Cannot reach AAP at <baseUrl>. Check your baseUrl in
|
|
30
|
+
* opencode.jsonc and ensure the AAP instance is accessible."
|
|
31
|
+
* - Invalid token (401): "AWX token is invalid or expired. Generate a new
|
|
32
|
+
* Personal Access Token at <baseUrl>/api/v2/tokens/ or Profile → Tokens."
|
|
33
|
+
* - Forbidden (403): "AWX token lacks sufficient permissions. Ensure the
|
|
34
|
+
* token has at least Read access."
|
|
35
|
+
*
|
|
36
|
+
* ## Reference
|
|
37
|
+
*
|
|
38
|
+
* - ADR 0001: Bearer Token Authentication for AWX Plugin
|
|
39
|
+
* - ADR 0003: Plugin API Surface Discovery
|
|
40
|
+
* - @opencode-ai/plugin auth hook docs
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Validates the bearer token by making a GET request to /api/v2/me/.
|
|
44
|
+
*
|
|
45
|
+
* This is the init-time validation called on plugin load. It checks:
|
|
46
|
+
* 1. The AAP instance is reachable at the configured baseUrl
|
|
47
|
+
* 2. The bearer token is active and valid
|
|
48
|
+
*
|
|
49
|
+
* Returns a structured result so the caller can surface clear error
|
|
50
|
+
* messages to the user.
|
|
51
|
+
*
|
|
52
|
+
* @param baseUrl The configured AAP base URL (e.g., "https://example.com")
|
|
53
|
+
* @param token The bearer token (PAT) to validate
|
|
54
|
+
* @param signal Optional AbortSignal for timeout (recommend 10s)
|
|
55
|
+
*/
|
|
56
|
+
export async function validateToken(baseUrl, token, signal) {
|
|
57
|
+
// Normalize trailing slash
|
|
58
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
59
|
+
const url = `${normalizedBase}api/v2/me/`;
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(url, {
|
|
62
|
+
method: "GET",
|
|
63
|
+
headers: {
|
|
64
|
+
Accept: "application/json",
|
|
65
|
+
Authorization: `Bearer ${token}`,
|
|
66
|
+
},
|
|
67
|
+
signal,
|
|
68
|
+
});
|
|
69
|
+
if (response.ok) {
|
|
70
|
+
return { valid: true, error: null, status: response.status };
|
|
71
|
+
}
|
|
72
|
+
if (response.status === 401) {
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
error: [
|
|
76
|
+
`AWX token is invalid or expired.`,
|
|
77
|
+
`Generate a new Personal Access Token at ${normalizedBase}api/v2/tokens/`,
|
|
78
|
+
`or Profile → Tokens in the AAP UI.`,
|
|
79
|
+
].join(" "),
|
|
80
|
+
status: 401,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (response.status === 403) {
|
|
84
|
+
return {
|
|
85
|
+
valid: false,
|
|
86
|
+
error: [
|
|
87
|
+
`AWX token lacks sufficient permissions.`,
|
|
88
|
+
`Ensure the token has at least Read access to the AWX API.`,
|
|
89
|
+
].join(" "),
|
|
90
|
+
status: 403,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
valid: false,
|
|
95
|
+
error: `AWX returned HTTP ${response.status}. Check your AAP configuration.`,
|
|
96
|
+
status: response.status,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
// Handle AbortError (timeout) specifically
|
|
101
|
+
if (err instanceof DOMException && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
102
|
+
return {
|
|
103
|
+
valid: false,
|
|
104
|
+
error: [
|
|
105
|
+
`Timeout connecting to AAP at ${baseUrl}.`,
|
|
106
|
+
`Check your baseUrl in opencode.jsonc and ensure the AAP instance is reachable.`,
|
|
107
|
+
].join(" "),
|
|
108
|
+
status: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Network or other errors
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
return {
|
|
114
|
+
valid: false,
|
|
115
|
+
error: [
|
|
116
|
+
`Cannot reach AAP at ${baseUrl}.`,
|
|
117
|
+
`Check your baseUrl in opencode.jsonc and ensure the AAP instance is accessible.`,
|
|
118
|
+
`Details: ${message}`,
|
|
119
|
+
].join(" "),
|
|
120
|
+
status: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Creates an AWX auth hook configuration for the OpenCode plugin server.
|
|
126
|
+
*
|
|
127
|
+
* Uses the `type: "api"` auth method — the standard OpenCode pattern for
|
|
128
|
+
* non-rotating, user-provided tokens (e.g., API keys, PATs).
|
|
129
|
+
*
|
|
130
|
+
* The `authorize()` function simply returns the user's PAT as the key.
|
|
131
|
+
* Validation is handled separately at init time via `validateToken()`.
|
|
132
|
+
*
|
|
133
|
+
* @returns Auth hook configuration compatible with OpenCode's Hooks.auth
|
|
134
|
+
*/
|
|
135
|
+
export function createAwxAuthHook() {
|
|
136
|
+
return {
|
|
137
|
+
provider: "awx",
|
|
138
|
+
methods: [
|
|
139
|
+
{
|
|
140
|
+
type: "api",
|
|
141
|
+
label: "AWX Bearer Token",
|
|
142
|
+
prompts: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
key: "token",
|
|
146
|
+
message: "Enter your AWX Personal Access Token (PAT). Generate one at AAP → Profile → Tokens or /api/v2/tokens/.",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
/**
|
|
150
|
+
* Authorize the user's PAT.
|
|
151
|
+
*
|
|
152
|
+
* Returns the raw token as the secret key. OpenCode stores this
|
|
153
|
+
* securely and injects it into tool requests as the auth key.
|
|
154
|
+
*
|
|
155
|
+
* Empty or whitespace-only tokens fail authorization.
|
|
156
|
+
*
|
|
157
|
+
* We include a message field for better UX. If OpenCode ignores unknown
|
|
158
|
+
* fields, the prompt text above still provides actionable guidance.
|
|
159
|
+
*
|
|
160
|
+
* If the OpenCode auth type narrows this contract in future, remove the
|
|
161
|
+
* message field and rely on the prompt text.
|
|
162
|
+
*/
|
|
163
|
+
async authorize(inputs) {
|
|
164
|
+
const token = inputs.token;
|
|
165
|
+
if (!token || token.trim().length === 0) {
|
|
166
|
+
return {
|
|
167
|
+
type: "failed",
|
|
168
|
+
message: "AWX auth not configured. Set your Personal Access Token in the plugin settings.",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
type: "success",
|
|
173
|
+
key: token.trim(),
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|