openhome-cli 0.1.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/README.md +470 -0
- package/bin/openhome.js +2 -0
- package/dist/chunk-Q4UKUXDB.js +164 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3184 -0
- package/dist/store-DR7EKQ5T.js +16 -0
- package/package.json +44 -0
- package/src/api/client.ts +231 -0
- package/src/api/contracts.ts +103 -0
- package/src/api/endpoints.ts +19 -0
- package/src/api/mock-client.ts +145 -0
- package/src/cli.ts +339 -0
- package/src/commands/agents.ts +88 -0
- package/src/commands/assign.ts +123 -0
- package/src/commands/chat.ts +265 -0
- package/src/commands/config-edit.ts +163 -0
- package/src/commands/delete.ts +107 -0
- package/src/commands/deploy.ts +430 -0
- package/src/commands/init.ts +895 -0
- package/src/commands/list.ts +78 -0
- package/src/commands/login.ts +54 -0
- package/src/commands/logout.ts +14 -0
- package/src/commands/logs.ts +174 -0
- package/src/commands/status.ts +174 -0
- package/src/commands/toggle.ts +118 -0
- package/src/commands/trigger.ts +193 -0
- package/src/commands/validate.ts +53 -0
- package/src/commands/whoami.ts +54 -0
- package/src/config/keychain.ts +62 -0
- package/src/config/store.ts +137 -0
- package/src/ui/format.ts +95 -0
- package/src/util/zip.ts +74 -0
- package/src/validation/rules.ts +71 -0
- package/src/validation/validator.ts +204 -0
- package/tasks/feature-request-sdk-api.md +246 -0
- package/tasks/prd-openhome-cli.md +605 -0
- package/templates/api/README.md.tmpl +11 -0
- package/templates/api/__init__.py.tmpl +0 -0
- package/templates/api/config.json.tmpl +4 -0
- package/templates/api/main.py.tmpl +30 -0
- package/templates/basic/README.md.tmpl +7 -0
- package/templates/basic/__init__.py.tmpl +0 -0
- package/templates/basic/config.json.tmpl +4 -0
- package/templates/basic/main.py.tmpl +22 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# PRD: OpenHome CLI Tool
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
The OpenHome CLI (`openhome`) lets developers manage voice AI abilities entirely from the terminal. No dashboard, no manual zipping, no browser tab switching. Create, validate, deploy, and monitor abilities with one tool.
|
|
6
|
+
|
|
7
|
+
This PRD covers what exists today (v0.1.0 MVP), what needs server-side support, and a proposed browser-based authentication system to replace manual API key entry.
|
|
8
|
+
|
|
9
|
+
**Target users:** OpenHome ability developers (internal team + community)
|
|
10
|
+
**Platform:** Node.js 18+, macOS primary, Linux/Windows supported
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Replace the dashboard-driven dev loop (zip, delete, upload, toggle, wait) with a single CLI command
|
|
17
|
+
- Match the UX quality of GitHub CLI, Vercel CLI, and Claude Code
|
|
18
|
+
- Define API contracts the backend team can implement
|
|
19
|
+
- Ship an auth flow that works without copy-pasting API keys
|
|
20
|
+
- Validate abilities locally before upload to catch errors early
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Current State (v0.1.0)
|
|
25
|
+
|
|
26
|
+
### What Works Today
|
|
27
|
+
|
|
28
|
+
| Command | Status | Notes |
|
|
29
|
+
|---------|--------|-------|
|
|
30
|
+
| `openhome` (interactive menu) | Working | Arrow-key navigable, loops after each command |
|
|
31
|
+
| `openhome init [name]` | Working | Scaffolds ability with templates, validates on create |
|
|
32
|
+
| `openhome validate [path]` | Working | 12 blocked patterns, 5 required patterns, config checks |
|
|
33
|
+
| `openhome login` | Working | Manual API key paste, verifies via `get_personalities` |
|
|
34
|
+
| `openhome deploy [path]` | Partial | Validates + zips. Upload endpoint not yet live on server. `--dry-run` and `--mock` work. |
|
|
35
|
+
| `openhome list` | Stubbed | Returns mock data. Server endpoint not yet live. |
|
|
36
|
+
| `openhome status [name]` | Stubbed | Returns mock data. Server endpoint not yet live. |
|
|
37
|
+
|
|
38
|
+
### What Does Not Exist Yet
|
|
39
|
+
|
|
40
|
+
| Feature | Blocked By |
|
|
41
|
+
|---------|-----------|
|
|
42
|
+
| Browser-based auth | Server OAuth support needed |
|
|
43
|
+
| Ability upload | `POST /api/sdk/abilities` not implemented |
|
|
44
|
+
| Ability listing | `GET /api/sdk/abilities` not implemented |
|
|
45
|
+
| Ability detail/history | `GET /api/sdk/abilities/:id` not implemented |
|
|
46
|
+
| Log streaming | WebSocket endpoint not implemented |
|
|
47
|
+
| Ability deletion | `DELETE /api/sdk/abilities/:id` not implemented |
|
|
48
|
+
| Local testing | No mock CapabilityWorker runtime exists |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## User Stories
|
|
53
|
+
|
|
54
|
+
### US-001: Interactive Menu Navigation
|
|
55
|
+
**Description:** As a developer, I want to run `openhome` and navigate commands with arrow keys so I do not need to memorize subcommands.
|
|
56
|
+
|
|
57
|
+
**Acceptance Criteria:**
|
|
58
|
+
- [x] Running `openhome` with no args shows scrollable menu
|
|
59
|
+
- [x] Arrow keys navigate, Enter selects
|
|
60
|
+
- [x] Menu loops after each command
|
|
61
|
+
- [x] Ctrl+C exits cleanly
|
|
62
|
+
- [x] Direct subcommands (`openhome deploy ./path`) still work
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### US-002: Scaffold New Ability
|
|
67
|
+
**Description:** As a developer, I want to scaffold a new ability with one command so I start with valid boilerplate.
|
|
68
|
+
|
|
69
|
+
**Acceptance Criteria:**
|
|
70
|
+
- [x] `openhome init my-ability` creates directory with `main.py`, `config.json`, `__init__.py`, `README.md`
|
|
71
|
+
- [x] Prompts for ability type (Skill, Brain Skill, Background Daemon)
|
|
72
|
+
- [x] Prompts for template (Basic, API)
|
|
73
|
+
- [x] Prompts for trigger words
|
|
74
|
+
- [x] Auto-validates after generation
|
|
75
|
+
- [x] Generated code passes all validation rules
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### US-003: Validate Ability Locally
|
|
80
|
+
**Description:** As a developer, I want to check my ability for errors before deploying so I do not waste time uploading broken code.
|
|
81
|
+
|
|
82
|
+
**Acceptance Criteria:**
|
|
83
|
+
- [x] `openhome validate` checks current directory
|
|
84
|
+
- [x] `openhome validate ./path` checks specific directory
|
|
85
|
+
- [x] Reports all errors (red) and warnings (yellow)
|
|
86
|
+
- [x] Blocks on errors, passes with warnings
|
|
87
|
+
- [x] Checks: required files, config schema, Python patterns, blocked imports
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### US-004: Deploy Ability
|
|
92
|
+
**Description:** As a developer, I want to deploy an ability with one command so I do not need to manually zip and upload.
|
|
93
|
+
|
|
94
|
+
**Acceptance Criteria:**
|
|
95
|
+
- [x] `openhome deploy` validates, zips, and uploads
|
|
96
|
+
- [x] `--dry-run` shows what would deploy without uploading
|
|
97
|
+
- [x] `--mock` uses fake API for testing
|
|
98
|
+
- [x] `--personality <id>` overrides default agent
|
|
99
|
+
- [x] Confirmation prompt before real deploy
|
|
100
|
+
- [ ] Real upload works (blocked: server endpoint)
|
|
101
|
+
- [x] Graceful fallback when endpoint returns NOT_IMPLEMENTED (saves zip, shows manual instructions)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### US-005: Manual API Key Login
|
|
106
|
+
**Description:** As a developer, I want to authenticate with my API key so the CLI can make authenticated requests.
|
|
107
|
+
|
|
108
|
+
**Acceptance Criteria:**
|
|
109
|
+
- [x] `openhome login` prompts for API key (masked input)
|
|
110
|
+
- [x] Verifies key against `get_personalities` endpoint
|
|
111
|
+
- [x] Stores key in macOS Keychain (config file fallback)
|
|
112
|
+
- [x] Lists agents and lets user set a default
|
|
113
|
+
- [x] Clear error on invalid key
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### US-006: Browser-Based Login (Proposed)
|
|
118
|
+
**Description:** As a developer, I want to log in by clicking a link and confirming in my browser so I do not need to find and paste API keys.
|
|
119
|
+
|
|
120
|
+
**Acceptance Criteria:**
|
|
121
|
+
- [ ] `openhome login` opens browser to OpenHome auth page
|
|
122
|
+
- [ ] User clicks "Authorize" in browser
|
|
123
|
+
- [ ] CLI receives token automatically (no paste)
|
|
124
|
+
- [ ] Fallback: display URL + code if browser cannot open
|
|
125
|
+
- [ ] Manual key paste available via `openhome login --token`
|
|
126
|
+
- [ ] Token stored securely (Keychain primary, file fallback with 0600 permissions)
|
|
127
|
+
- [ ] Auth session expires and can be refreshed
|
|
128
|
+
- [ ] Works in SSH/headless environments (device flow)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### US-007: List Abilities
|
|
133
|
+
**Description:** As a developer, I want to see all my deployed abilities so I know what is live.
|
|
134
|
+
|
|
135
|
+
**Acceptance Criteria:**
|
|
136
|
+
- [x] `openhome list` shows table with name, version, status, last update
|
|
137
|
+
- [x] Status is color-coded (green=active, yellow=processing, red=failed)
|
|
138
|
+
- [x] `--mock` works for testing
|
|
139
|
+
- [ ] Real data from API (blocked: server endpoint)
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### US-008: Ability Status and History
|
|
144
|
+
**Description:** As a developer, I want to check one ability's status and deploy history so I can debug failed deployments.
|
|
145
|
+
|
|
146
|
+
**Acceptance Criteria:**
|
|
147
|
+
- [x] `openhome status my-ability` shows detail panel
|
|
148
|
+
- [x] Shows: name, status badge, version, timestamps, linked agents
|
|
149
|
+
- [x] Shows validation errors if any
|
|
150
|
+
- [x] Shows deploy history with version, status, message, timestamp
|
|
151
|
+
- [x] Reads from local `config.json` if no name given
|
|
152
|
+
- [x] `--mock` works for testing
|
|
153
|
+
- [ ] Real data from API (blocked: server endpoint)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### US-009: Log Streaming (Planned)
|
|
158
|
+
**Description:** As a developer, I want to stream my ability's logs in real-time so I can debug issues without using the dashboard.
|
|
159
|
+
|
|
160
|
+
**Acceptance Criteria:**
|
|
161
|
+
- [ ] `openhome logs my-ability` streams logs to terminal
|
|
162
|
+
- [ ] `--follow` keeps connection open for new logs
|
|
163
|
+
- [ ] `--tail 100` shows last N lines
|
|
164
|
+
- [ ] Color-coded log levels (error=red, warn=yellow, info=white)
|
|
165
|
+
- [ ] Ctrl+C cleanly disconnects
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### US-010: Ability Deletion (Planned)
|
|
170
|
+
**Description:** As a developer, I want to delete a deployed ability from the CLI so I do not need to use the dashboard.
|
|
171
|
+
|
|
172
|
+
**Acceptance Criteria:**
|
|
173
|
+
- [ ] `openhome delete my-ability` removes the ability
|
|
174
|
+
- [ ] Confirmation prompt before deletion
|
|
175
|
+
- [ ] Clear success/error message
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Functional Requirements
|
|
180
|
+
|
|
181
|
+
### FR-1: Authentication (Current)
|
|
182
|
+
The CLI stores an API key via `openhome login`. Key is verified against `POST /api/sdk/get_personalities`. Stored in macOS Keychain (service: `openhome-cli`, account: `api-key`) with plaintext config file fallback.
|
|
183
|
+
|
|
184
|
+
### FR-2: Authentication (Proposed — Browser-Based)
|
|
185
|
+
See [Authentication Architecture](#authentication-architecture) section below.
|
|
186
|
+
|
|
187
|
+
### FR-3: Ability Scaffolding
|
|
188
|
+
`openhome init [name]` creates a directory with four files from templates. Supports two templates (Basic, API) and three ability categories (Skill, Brain Skill, Background Daemon). Auto-validates after creation.
|
|
189
|
+
|
|
190
|
+
### FR-4: Local Validation
|
|
191
|
+
`openhome validate [path]` runs all rules from `validation/rules.ts`. 12 blocked Python patterns, 4 blocked imports, 5 required patterns, config schema check, hardcoded key warning, multiple class warning. Returns structured `{passed, errors, warnings}`.
|
|
192
|
+
|
|
193
|
+
### FR-5: Deploy Pipeline
|
|
194
|
+
`openhome deploy [path]` runs: validate, read config, create ZIP (excludes `__pycache__`, `.pyc`, `.git`), confirm, upload. Supports `--dry-run` (no zip, no upload), `--mock` (fake API), `--personality <id>` (override agent).
|
|
195
|
+
|
|
196
|
+
### FR-6: Ability Listing
|
|
197
|
+
`openhome list` calls `GET /api/sdk/abilities` and renders a table. Supports `--mock`.
|
|
198
|
+
|
|
199
|
+
### FR-7: Ability Status
|
|
200
|
+
`openhome status [name]` calls `GET /api/sdk/abilities/:id` and renders detail panels. Falls back to local `config.json` for ability name. Supports `--mock`.
|
|
201
|
+
|
|
202
|
+
### FR-8: Interactive Menu
|
|
203
|
+
Bare `openhome` shows a `@clack/prompts` select menu with all commands. Loops after each command. Prompts for required arguments inline.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Authentication Architecture
|
|
208
|
+
|
|
209
|
+
### Recommended: OAuth Device Flow (RFC 8628)
|
|
210
|
+
|
|
211
|
+
This is the same pattern GitHub CLI uses. It works everywhere: local machines, SSH sessions, containers, CI.
|
|
212
|
+
|
|
213
|
+
### How It Works
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
Developer CLI OpenHome Server
|
|
217
|
+
| | |
|
|
218
|
+
| openhome login | |
|
|
219
|
+
|------------------------>| |
|
|
220
|
+
| | POST /oauth/device/code |
|
|
221
|
+
| | {client_id} |
|
|
222
|
+
| |------------------------------>|
|
|
223
|
+
| | |
|
|
224
|
+
| | {device_code, user_code, |
|
|
225
|
+
| | verification_uri, |
|
|
226
|
+
| | interval, expires_in} |
|
|
227
|
+
| |<------------------------------|
|
|
228
|
+
| | |
|
|
229
|
+
| "Open this URL and | |
|
|
230
|
+
| enter code: ABCD-1234"| |
|
|
231
|
+
|<------------------------| |
|
|
232
|
+
| (browser auto-opens) | |
|
|
233
|
+
| | |
|
|
234
|
+
| User visits URL -------|----> Enters code, clicks OK |
|
|
235
|
+
| | |
|
|
236
|
+
| | POST /oauth/token |
|
|
237
|
+
| | {device_code, grant_type= |
|
|
238
|
+
| | device_authorization} |
|
|
239
|
+
| |------------------------------>|
|
|
240
|
+
| | (polls every 5s) |
|
|
241
|
+
| | |
|
|
242
|
+
| | {access_token, |
|
|
243
|
+
| | refresh_token, expires_in} |
|
|
244
|
+
| |<------------------------------|
|
|
245
|
+
| | |
|
|
246
|
+
| "Logged in as Brady!" | Store in Keychain |
|
|
247
|
+
|<------------------------| |
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Server-Side Requirements
|
|
251
|
+
|
|
252
|
+
The OpenHome backend needs to implement these endpoints:
|
|
253
|
+
|
|
254
|
+
#### `POST /oauth/device/code`
|
|
255
|
+
|
|
256
|
+
Request:
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"client_id": "openhome-cli"
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Response (200):
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"device_code": "random-uuid-device-code",
|
|
267
|
+
"user_code": "ABCD-1234",
|
|
268
|
+
"verification_uri": "https://app.openhome.com/device",
|
|
269
|
+
"interval": 5,
|
|
270
|
+
"expires_in": 900
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
- `device_code` — unique identifier for this auth session (server keeps it, CLI polls with it)
|
|
275
|
+
- `user_code` — short human-readable code the user types into the browser (8 chars, uppercase + digits)
|
|
276
|
+
- `verification_uri` — URL where user enters the code
|
|
277
|
+
- `interval` — seconds between poll attempts
|
|
278
|
+
- `expires_in` — seconds until the device code expires (15 minutes recommended)
|
|
279
|
+
|
|
280
|
+
#### `POST /oauth/token`
|
|
281
|
+
|
|
282
|
+
Request (polling):
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"client_id": "openhome-cli",
|
|
286
|
+
"device_code": "random-uuid-device-code",
|
|
287
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code"
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Response (pending — user has not authorized yet):
|
|
292
|
+
```json
|
|
293
|
+
{
|
|
294
|
+
"error": "authorization_pending"
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Response (success — user authorized):
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"access_token": "oh_abc123...",
|
|
302
|
+
"refresh_token": "oh_ref456...",
|
|
303
|
+
"token_type": "Bearer",
|
|
304
|
+
"expires_in": 86400
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Response (expired — user took too long):
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"error": "expired_token"
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Response (denied — user clicked Deny):
|
|
316
|
+
```json
|
|
317
|
+
{
|
|
318
|
+
"error": "access_denied"
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### `POST /oauth/token` (refresh)
|
|
323
|
+
|
|
324
|
+
Request:
|
|
325
|
+
```json
|
|
326
|
+
{
|
|
327
|
+
"client_id": "openhome-cli",
|
|
328
|
+
"grant_type": "refresh_token",
|
|
329
|
+
"refresh_token": "oh_ref456..."
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Response:
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"access_token": "oh_new789...",
|
|
337
|
+
"refresh_token": "oh_newref...",
|
|
338
|
+
"token_type": "Bearer",
|
|
339
|
+
"expires_in": 86400
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### Web Page: `https://app.openhome.com/device`
|
|
344
|
+
|
|
345
|
+
A simple page where the user:
|
|
346
|
+
1. Sees "Enter the code shown in your terminal"
|
|
347
|
+
2. Types the 8-character code
|
|
348
|
+
3. Sees their account info and what the CLI is requesting
|
|
349
|
+
4. Clicks "Authorize" or "Deny"
|
|
350
|
+
|
|
351
|
+
This page should work on mobile too (user might be SSHed from a phone).
|
|
352
|
+
|
|
353
|
+
### CLI-Side Implementation
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
openhome login
|
|
357
|
+
|
|
|
358
|
+
├── Request device code from server
|
|
359
|
+
├── Display: "Open https://app.openhome.com/device"
|
|
360
|
+
├── Display: "Enter code: ABCD-1234"
|
|
361
|
+
├── Try to open browser (macOS: open, Linux: xdg-open)
|
|
362
|
+
├── Poll /oauth/token every 5s
|
|
363
|
+
│ ├── "authorization_pending" → keep polling
|
|
364
|
+
│ ├── "slow_down" → increase interval
|
|
365
|
+
│ ├── "expired_token" → error, ask to retry
|
|
366
|
+
│ ├── "access_denied" → error, exit
|
|
367
|
+
│ └── success → store tokens
|
|
368
|
+
├── Store access_token in Keychain
|
|
369
|
+
├── Store refresh_token in Keychain
|
|
370
|
+
├── Fetch user info / personalities
|
|
371
|
+
└── Display: "Logged in as [name]!"
|
|
372
|
+
|
|
373
|
+
openhome login --token
|
|
374
|
+
|
|
|
375
|
+
└── (fallback) Prompt for manual API key paste (current flow)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Token Lifecycle
|
|
379
|
+
|
|
380
|
+
- Access token expires after 24 hours (configurable by server)
|
|
381
|
+
- Before each API call, check if token is expired
|
|
382
|
+
- If expired, use refresh token to get new access token
|
|
383
|
+
- If refresh fails, prompt user to `openhome login` again
|
|
384
|
+
- `openhome logout` clears all tokens from Keychain and config
|
|
385
|
+
|
|
386
|
+
### Why Device Flow Over PKCE
|
|
387
|
+
|
|
388
|
+
| Factor | Device Flow | PKCE + Localhost |
|
|
389
|
+
|--------|------------|-----------------|
|
|
390
|
+
| Works in SSH/containers | Yes | No |
|
|
391
|
+
| Needs local HTTP server | No | Yes |
|
|
392
|
+
| Port conflicts | None | Possible |
|
|
393
|
+
| Corporate firewalls | Works | May block localhost |
|
|
394
|
+
| Implementation complexity | Simpler | More complex |
|
|
395
|
+
| UX | Type short code | Click authorize (slightly easier) |
|
|
396
|
+
|
|
397
|
+
GitHub CLI chose device flow. It is battle-tested at scale. OpenHome developers often work via SSH or in constrained environments. Device flow works everywhere.
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## API Contracts (Server-Side)
|
|
402
|
+
|
|
403
|
+
These endpoints need to be implemented by the OpenHome backend team.
|
|
404
|
+
|
|
405
|
+
### Existing (Working)
|
|
406
|
+
|
|
407
|
+
| Endpoint | Method | Auth | Purpose |
|
|
408
|
+
|----------|--------|------|---------|
|
|
409
|
+
| `/api/sdk/get_personalities` | POST | API key in body | List user's agents |
|
|
410
|
+
|
|
411
|
+
### Needed for CLI
|
|
412
|
+
|
|
413
|
+
| Endpoint | Method | Auth | Purpose | Priority |
|
|
414
|
+
|----------|--------|------|---------|----------|
|
|
415
|
+
| `POST /api/sdk/abilities` | POST | Bearer token | Upload ability ZIP | High |
|
|
416
|
+
| `GET /api/sdk/abilities` | GET | Bearer token | List user's abilities | High |
|
|
417
|
+
| `GET /api/sdk/abilities/:id` | GET | Bearer token | Ability detail + deploy history | Medium |
|
|
418
|
+
| `DELETE /api/sdk/abilities/:id` | DELETE | Bearer token | Remove ability | Medium |
|
|
419
|
+
| `POST /oauth/device/code` | POST | None (public) | Start device auth flow | High |
|
|
420
|
+
| `POST /oauth/token` | POST | None (public) | Exchange/refresh tokens | High |
|
|
421
|
+
| `GET /api/sdk/abilities/:id/logs` | WebSocket | Bearer token | Stream ability logs | Low |
|
|
422
|
+
|
|
423
|
+
### Upload Endpoint Detail
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
POST /api/sdk/abilities
|
|
427
|
+
Content-Type: multipart/form-data
|
|
428
|
+
|
|
429
|
+
Fields:
|
|
430
|
+
ability: (binary ZIP file)
|
|
431
|
+
personality_id: (optional string — agent to attach to)
|
|
432
|
+
|
|
433
|
+
Response 200:
|
|
434
|
+
{
|
|
435
|
+
"ability_id": "abl_abc123",
|
|
436
|
+
"unique_name": "my-weather-bot",
|
|
437
|
+
"version": 3,
|
|
438
|
+
"status": "processing",
|
|
439
|
+
"validation_errors": [],
|
|
440
|
+
"created_at": "2026-03-18T12:00:00Z",
|
|
441
|
+
"message": "Upload received, processing..."
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
Response 400:
|
|
445
|
+
{
|
|
446
|
+
"error": {
|
|
447
|
+
"code": "VALIDATION_FAILED",
|
|
448
|
+
"message": "Missing required file: main.py",
|
|
449
|
+
"details": { "missing_files": ["main.py"] }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Error Format (All Endpoints)
|
|
455
|
+
|
|
456
|
+
```json
|
|
457
|
+
{
|
|
458
|
+
"error": {
|
|
459
|
+
"code": "UNAUTHORIZED | VALIDATION_FAILED | NOT_FOUND | NOT_IMPLEMENTED",
|
|
460
|
+
"message": "Human-readable error message",
|
|
461
|
+
"details": {}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The CLI handles `NOT_IMPLEMENTED` gracefully — shows a helpful message and saves the request locally instead of crashing.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Security Issues to Fix (Before Public Release)
|
|
471
|
+
|
|
472
|
+
### HIGH: Shell Injection in Keychain Storage
|
|
473
|
+
|
|
474
|
+
**File:** `src/config/keychain.ts`
|
|
475
|
+
**Problem:** API key is interpolated directly into a shell command string via `execSync`. A malicious key value could execute arbitrary commands.
|
|
476
|
+
**Fix:** Replace `execSync` with `execFileSync` using an argument array (bypasses shell entirely).
|
|
477
|
+
|
|
478
|
+
### HIGH: API Key Sent in Request Body
|
|
479
|
+
|
|
480
|
+
**File:** `src/api/client.ts` (`getPersonalities` method)
|
|
481
|
+
**Problem:** API key is sent both in the `Authorization` header AND in the POST body. Body values appear in server logs, proxy logs, and request inspection tools.
|
|
482
|
+
**Fix:** Remove `api_key` from request body. The header is sufficient.
|
|
483
|
+
**Server action needed:** Deprecate `api_key` body parameter in `get_personalities`, accept header-only auth.
|
|
484
|
+
|
|
485
|
+
### MEDIUM: Config File Permissions
|
|
486
|
+
|
|
487
|
+
**File:** `src/config/store.ts`
|
|
488
|
+
**Problem:** `~/.openhome/config.json` is created with default permissions (world-readable). If API key falls back to config file, any local user can read it.
|
|
489
|
+
**Fix:** Create directory with `mode: 0o700`, write file with `mode: 0o600`.
|
|
490
|
+
|
|
491
|
+
### MEDIUM: No HTTPS Enforcement
|
|
492
|
+
|
|
493
|
+
**File:** `src/api/client.ts`
|
|
494
|
+
**Problem:** Custom `api_base_url` accepts `http://`. All requests including auth headers would be sent unencrypted.
|
|
495
|
+
**Fix:** Validate that base URL starts with `https://` in the ApiClient constructor.
|
|
496
|
+
|
|
497
|
+
### MEDIUM: ZIP Includes Sensitive Files
|
|
498
|
+
|
|
499
|
+
**File:** `src/util/zip.ts`
|
|
500
|
+
**Problem:** Only excludes `__pycache__`, `.pyc`, `.git`. Does not exclude `.env`, `secrets.json`, `*.key`, `*.pem`.
|
|
501
|
+
**Fix:** Add `.env`, `.env.*`, `secrets.*`, `*.key`, `*.pem` to exclusion list. Add a validator warning when these files exist.
|
|
502
|
+
|
|
503
|
+
### MEDIUM: Narrow Hardcoded Key Detection
|
|
504
|
+
|
|
505
|
+
**File:** `src/validation/rules.ts`
|
|
506
|
+
**Problem:** Only catches keys starting with `sk_`, `sk-`, `key_`. Misses AWS keys (`AKIA...`), GitHub PATs (`ghp_...`), and generic `API_KEY = "..."` patterns.
|
|
507
|
+
**Fix:** Expand pattern list to cover common credential formats.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Non-Goals (Out of Scope)
|
|
512
|
+
|
|
513
|
+
- **Local ability testing runtime** — No mock CapabilityWorker. Too complex for MVP. Deploy to test.
|
|
514
|
+
- **GUI/TUI dashboard** — CLI only. No Ink/Blessed terminal UI.
|
|
515
|
+
- **Multi-language abilities** — Python only. OpenHome does not support other languages yet.
|
|
516
|
+
- **Marketplace publishing** — Not in CLI scope. Use dashboard.
|
|
517
|
+
- **Team/org management** — Not in CLI scope.
|
|
518
|
+
- **Ability versioning/rollback** — Server would need to support this first.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Technical Considerations
|
|
523
|
+
|
|
524
|
+
### Dependencies (Current)
|
|
525
|
+
|
|
526
|
+
| Package | Version | Purpose |
|
|
527
|
+
|---------|---------|---------|
|
|
528
|
+
| commander | 12.x | CLI argument parsing |
|
|
529
|
+
| @clack/prompts | 1.x | Interactive menus, spinners, prompts |
|
|
530
|
+
| chalk | 5.x | Terminal colors |
|
|
531
|
+
| archiver | 7.x | ZIP creation |
|
|
532
|
+
|
|
533
|
+
### Dependencies (Needed for Auth)
|
|
534
|
+
|
|
535
|
+
| Package | Purpose |
|
|
536
|
+
|---------|---------|
|
|
537
|
+
| `open` | Cross-platform browser opening |
|
|
538
|
+
| None for device flow | Just fetch + setInterval polling |
|
|
539
|
+
|
|
540
|
+
### Build
|
|
541
|
+
|
|
542
|
+
- TypeScript compiled via tsup (ESM output)
|
|
543
|
+
- Node.js 18+ required
|
|
544
|
+
- `npm link` for global install during development
|
|
545
|
+
- Eventually publish to npm as `openhome-cli`
|
|
546
|
+
|
|
547
|
+
### Compatibility
|
|
548
|
+
|
|
549
|
+
| Platform | Keychain | Browser Open | Status |
|
|
550
|
+
|----------|----------|-------------|--------|
|
|
551
|
+
| macOS | Yes (security CLI) | Yes (`open`) | Full support |
|
|
552
|
+
| Linux | No (config fallback) | Yes (`xdg-open`) | Works, less secure storage |
|
|
553
|
+
| Windows | No (config fallback) | Yes (`start`) | Works, less secure storage |
|
|
554
|
+
| SSH/headless | N/A | No | Device flow works, manual token fallback |
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## Success Metrics
|
|
559
|
+
|
|
560
|
+
- Developer can go from `openhome init` to deployed ability in under 5 minutes
|
|
561
|
+
- Zero dashboard visits required for standard ability dev loop
|
|
562
|
+
- Auth flow completes in under 30 seconds (browser-based)
|
|
563
|
+
- All abilities pass validation before upload (no server-side validation failures for structure issues)
|
|
564
|
+
- CLI handles all API errors gracefully (no crashes, clear messages)
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Implementation Priority
|
|
569
|
+
|
|
570
|
+
### Phase 1: Security Fixes (Do First)
|
|
571
|
+
1. Fix shell injection in `keychain.ts`
|
|
572
|
+
2. Harden config file permissions
|
|
573
|
+
3. Add HTTPS enforcement
|
|
574
|
+
4. Expand ZIP exclusion list
|
|
575
|
+
|
|
576
|
+
### Phase 2: Server Endpoints (Backend Team)
|
|
577
|
+
5. `POST /api/sdk/abilities` (upload)
|
|
578
|
+
6. `GET /api/sdk/abilities` (list)
|
|
579
|
+
7. `GET /api/sdk/abilities/:id` (detail)
|
|
580
|
+
8. Wire CLI to real endpoints (remove mock fallback as default)
|
|
581
|
+
|
|
582
|
+
### Phase 3: Browser Auth (Requires Server + CLI)
|
|
583
|
+
9. Server: Implement `/oauth/device/code` and `/oauth/token`
|
|
584
|
+
10. Server: Build device authorization web page
|
|
585
|
+
11. CLI: Implement device flow in `openhome login`
|
|
586
|
+
12. CLI: Token refresh before API calls
|
|
587
|
+
13. CLI: `openhome logout` command
|
|
588
|
+
|
|
589
|
+
### Phase 4: Extended Features
|
|
590
|
+
14. `openhome logs` (WebSocket streaming)
|
|
591
|
+
15. `openhome delete` (ability removal)
|
|
592
|
+
16. `openhome watch` (auto-deploy on file changes)
|
|
593
|
+
17. Publish to npm
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Open Questions
|
|
598
|
+
|
|
599
|
+
1. **Token format:** What format will OpenHome access tokens use? JWT? Opaque? This affects whether the CLI can check expiry locally.
|
|
600
|
+
2. **Rate limits:** What are the API rate limits? The CLI should show clear messages when rate-limited.
|
|
601
|
+
3. **Ability size limit:** Is there a max ZIP size the server accepts? The CLI should enforce this client-side.
|
|
602
|
+
4. **Multi-agent deploy:** Can one ability be attached to multiple agents in a single deploy? Or does the user need to deploy once per agent?
|
|
603
|
+
5. **Versioning:** Does re-deploying the same `unique_name` auto-increment the version? Can the user roll back?
|
|
604
|
+
6. **WebSocket auth:** How will the log streaming endpoint authenticate? Bearer token in query param? Upgrade header?
|
|
605
|
+
7. **OAuth client registration:** Does the device flow need a registered OAuth client ID, or can it use a hardcoded public client ID (`openhome-cli`)?
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from src.agent.capability import MatchingCapability
|
|
3
|
+
from src.main import AgentWorker
|
|
4
|
+
from src.agent.capability_worker import CapabilityWorker
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class {{CLASS_NAME}}(MatchingCapability):
|
|
8
|
+
worker: AgentWorker = None
|
|
9
|
+
capability_worker: CapabilityWorker = None
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def register_capability(cls) -> "MatchingCapability":
|
|
13
|
+
# {{register_capability}}
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def call(self, worker: AgentWorker):
|
|
17
|
+
self.worker = worker
|
|
18
|
+
self.capability_worker = CapabilityWorker(self.worker)
|
|
19
|
+
self.worker.session_tasks.create(self.run())
|
|
20
|
+
|
|
21
|
+
async def run(self):
|
|
22
|
+
api_key = self.capability_worker.get_single_key("api_key")
|
|
23
|
+
response = requests.get(
|
|
24
|
+
"https://api.example.com/data",
|
|
25
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
26
|
+
timeout=10,
|
|
27
|
+
)
|
|
28
|
+
data = response.json()
|
|
29
|
+
await self.capability_worker.speak(f"Here's what I found: {data.get('result', 'nothing')}")
|
|
30
|
+
self.capability_worker.resume_normal_flow()
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from src.agent.capability import MatchingCapability
|
|
2
|
+
from src.main import AgentWorker
|
|
3
|
+
from src.agent.capability_worker import CapabilityWorker
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class {{CLASS_NAME}}(MatchingCapability):
|
|
7
|
+
worker: AgentWorker = None
|
|
8
|
+
capability_worker: CapabilityWorker = None
|
|
9
|
+
|
|
10
|
+
@classmethod
|
|
11
|
+
def register_capability(cls) -> "MatchingCapability":
|
|
12
|
+
# {{register_capability}}
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def call(self, worker: AgentWorker):
|
|
16
|
+
self.worker = worker
|
|
17
|
+
self.capability_worker = CapabilityWorker(self.worker)
|
|
18
|
+
self.worker.session_tasks.create(self.run())
|
|
19
|
+
|
|
20
|
+
async def run(self):
|
|
21
|
+
await self.capability_worker.speak("Hello! This ability is working.")
|
|
22
|
+
self.capability_worker.resume_normal_flow()
|