dodo-mcp 1.0.6 → 1.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/README.md +74 -20
- package/dist/cli/main.js +152 -28
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/menu.js +2 -2
- package/dist/cli/menu.js.map +1 -1
- package/dist/config/globalConfig.js +7 -3
- package/dist/config/globalConfig.js.map +1 -1
- package/dist/config/tunnelConfig.js +2 -1
- package/dist/config/tunnelConfig.js.map +1 -1
- package/dist/projects/ownerAdmin.js +86 -0
- package/dist/projects/ownerAdmin.js.map +1 -0
- package/dist/projects/registry.js +90 -9
- package/dist/projects/registry.js.map +1 -1
- package/dist/security/projectAccess.js +69 -0
- package/dist/security/projectAccess.js.map +1 -0
- package/dist/security/projectAuthority.js +5 -1
- package/dist/security/projectAuthority.js.map +1 -1
- package/dist/server/appServer.js +23 -9
- package/dist/server/appServer.js.map +1 -1
- package/dist/server/bootstrap.js +9 -1
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/server/configUi/app.css +8 -0
- package/dist/server/configUi/app.js +142 -18
- package/dist/server/configUi/index.html +50 -13
- package/dist/server/installationRuntime.js +6 -2
- package/dist/server/installationRuntime.js.map +1 -1
- package/dist/server/instructions.js +1 -1
- package/dist/server/instructions.js.map +1 -1
- package/dist/server/ipcDispatch.js +3 -0
- package/dist/server/ipcDispatch.js.map +1 -1
- package/dist/server/localConfig.js +80 -13
- package/dist/server/localConfig.js.map +1 -1
- package/dist/services/ai/subagents.js +1 -1
- package/dist/services/ai/subagents.js.map +1 -1
- package/dist/services/android/adbBackend.js +106 -0
- package/dist/services/android/adbBackend.js.map +1 -0
- package/dist/services/android/androidPolicy.js +61 -0
- package/dist/services/android/androidPolicy.js.map +1 -0
- package/dist/services/android/androidService.js +444 -0
- package/dist/services/android/androidService.js.map +1 -0
- package/dist/services/android/protocol.js +39 -0
- package/dist/services/android/protocol.js.map +1 -0
- package/dist/services/multimodal/storage.js +10 -1
- package/dist/services/multimodal/storage.js.map +1 -1
- package/dist/services/schedules/scheduleService.js +8 -1
- package/dist/services/schedules/scheduleService.js.map +1 -1
- package/dist/setup/componentProbes.js +7 -1
- package/dist/setup/componentProbes.js.map +1 -1
- package/dist/setup/setup.js +11 -1
- package/dist/setup/setup.js.map +1 -1
- package/dist/store/db.js +14 -0
- package/dist/store/db.js.map +1 -1
- package/dist/tools/agentRuntimeTools.js +2 -3
- package/dist/tools/agentRuntimeTools.js.map +1 -1
- package/dist/tools/androidTools.js +113 -0
- package/dist/tools/androidTools.js.map +1 -0
- package/dist/tools/catalog.js +3 -0
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/context.js +85 -5
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/projectTools.js +1 -1
- package/dist/tools/projectTools.js.map +1 -1
- package/dist/tools/surface.js +18 -11
- package/dist/tools/surface.js.map +1 -1
- package/dist/tunnel/control.js +11 -4
- package/dist/tunnel/control.js.map +1 -1
- package/dist/tunnel/log.js +32 -0
- package/dist/tunnel/log.js.map +1 -1
- package/dist/tunnel/supervisor.js +103 -24
- package/dist/tunnel/supervisor.js.map +1 -1
- package/docs/ANDROID.md +105 -28
- package/docs/ARCHITECTURE.md +16 -7
- package/docs/AUTH.md +6 -0
- package/docs/COMPATIBILITY.md +14 -7
- package/docs/MANUAL_ACCEPTANCE.md +40 -6
- package/docs/PROJECTS.md +73 -2
- package/docs/RELEASE_1.1.0.md +69 -0
- package/docs/RELEASE_1.2.0.md +75 -0
- package/docs/RELEASE_NOTES.md +30 -0
- package/docs/SECURITY.md +45 -6
- package/docs/TEST_REPORT.md +96 -0
- package/docs/TUNNEL.md +79 -16
- package/docs/adr/048-persistent-connection-mode.md +3 -1
- package/docs/adr/049-android-adb-control.md +52 -0
- package/docs/adr/050-cloudflare-local-and-project-access.md +67 -0
- package/docs/adr/README.md +2 -0
- package/package.json +6 -2
- package/schemas/global-config.schema.json +1 -0
- package/schemas/tools.compact.json +312 -9
- package/schemas/tools.hybrid.json +485 -152
- package/schemas/tools.json +3610 -534
package/README.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# DODO MCP
|
|
2
2
|
|
|
3
|
-
**DODO MCP
|
|
3
|
+
**DODO MCP** คือ MCP server แบบ local-first สำหรับให้ AI ช่วยพัฒนา software โดยทำงานกับ workspace ที่เจ้าของเลือก ค่าเริ่มต้นเป็นโหมดส่วนตัวแบบเพิ่มโปรเจกต์แล้วใช้ได้ทันที และยังมีโหมด managed สำหรับแยก workspace ACL/trust แบบละเอียด
|
|
4
4
|
|
|
5
5
|
## จุดเด่น
|
|
6
6
|
|
|
7
7
|
- MCP ผ่าน HTTP ที่ `127.0.0.1:21730/mcp` พร้อม OAuth และ PKCE
|
|
8
8
|
- Local Config แบบ loopback ที่ `127.0.0.1:21731` และ Remote Config ชั่วคราวผ่าน tunnel เมื่อเจ้าของสั่ง `dodo --web`
|
|
9
|
-
- HTTP ใช้ Compact Tool Surface
|
|
10
|
-
- STDIO ใช้ Full Tool Surface; มี capability definitions ทั้งหมด
|
|
9
|
+
- HTTP ใช้ Compact Tool Surface 20 tools เพื่อลดภาระการโหลด schema
|
|
10
|
+
- STDIO ใช้ Full Tool Surface; มี capability definitions ทั้งหมด 138 รายการ แต่ค่าเริ่มต้นซ่อน Sub-agent 4 operations จาก MCP จึงเห็น 134 tools
|
|
11
11
|
- Hybrid Surface 49 tools สำหรับ client ที่รับ catalog ขนาดกลาง
|
|
12
12
|
- อ่าน ค้นหา สร้าง แก้ ย้าย ลบไฟล์ พร้อม expected hash, journal และ rollback
|
|
13
13
|
- รันคำสั่ง งานแบบขนาน jobs, Git, TypeScript/JavaScript intelligence, LSP และ task assistance
|
|
14
14
|
- รองรับภาพ เสียง วิดีโอ เบราว์เซอร์ เกม และ workflow ตาม dependency และ permission ที่เจ้าของเปิดใช้
|
|
15
|
+
- ตรวจและควบคุมมือถือ Android ผ่าน ADB ได้ตั้งแต่ภาพหน้าจอ/UI/logcat ไปจนถึง input, apps, APK, file transfer และ device-side shell โดยจำกัดเฉพาะ serial ที่เจ้าของอนุญาต ([คู่มือ](docs/ANDROID.md))
|
|
15
16
|
- มี Universal Resource Layer + CAS สำหรับ text/binary/image/audio/video/PDF/ZIP/WASM พร้อม SHA-256, dedup, bounded range/resume และ MCP image/audio blocks
|
|
16
17
|
- มี Project Brain ที่ทำ incremental AST index สำหรับ symbols, references, imports, routes, tests และ dependencies พร้อม source-hash freshness
|
|
17
18
|
- มี Context Engine สำหรับ goal-driven retrieval แบบมี budget, provenance, confidence, freshness และ L0–L6 dependency cache
|
|
@@ -35,9 +36,9 @@ dodo setup --check
|
|
|
35
36
|
dodo --cli
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
### Android / Termux
|
|
39
|
+
### Android / ADB และ Termux
|
|
39
40
|
|
|
40
|
-
DODO
|
|
41
|
+
DODO ไม่โหลด Sharp ตั้งแต่เริ่ม CLI จึงใช้คำสั่งพื้นฐานและ MCP
|
|
41
42
|
สำหรับ text/code ได้แม้เครื่องไม่มี native Sharp build แพ็กเกจมี WebAssembly image backend
|
|
42
43
|
เป็น optional dependency; หาก npm ข้าม optional dependency ให้ติดตั้งเพิ่มแล้วเปิด DODO ใหม่:
|
|
43
44
|
|
|
@@ -51,6 +52,18 @@ dodo --version
|
|
|
51
52
|
OS credential store, sandbox, desktop control และ browser automation ยังไม่ได้ผ่าน
|
|
52
53
|
Android acceptance ครบ ดูข้อจำกัดและวิธีตรวจรับที่ [Android / Termux](docs/ANDROID.md)
|
|
53
54
|
|
|
55
|
+
ถ้าต้องการให้ AI ตรวจหรือควบคุมมือถือที่เชื่อมกับเครื่องนี้ ให้ติดตั้ง Platform-Tools,
|
|
56
|
+
เปิด USB/Wireless debugging และยอมรับ RSA prompt บนมือถือ จากนั้นอนุญาต exact serial:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
dodo setup --check --components adb
|
|
60
|
+
dodo android devices
|
|
61
|
+
dodo android allow --device SERIAL --mode control --persist --yes
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Full surface มี `android_*` 13 operations ส่วน HTTP Compact ใช้ `dodo_mobile` gateway
|
|
65
|
+
สิทธิ์นี้ไม่ปิด OAuth, project context, trust/approval, expected hash หรือ audit
|
|
66
|
+
|
|
54
67
|
## เชื่อม ChatGPT ภายในไม่กี่นาที
|
|
55
68
|
|
|
56
69
|
ChatGPT ต้องเชื่อมผ่าน **Public MCP URL** ของ DODO ซึ่งลงท้ายด้วย `/mcp` ส่วน
|
|
@@ -102,7 +115,7 @@ dodo auth approve REQUEST_ID
|
|
|
102
115
|
```
|
|
103
116
|
|
|
104
117
|
กลับไปที่ ChatGPT รอให้ scan เสร็จ เริ่มแชตใหม่ เลือก DODO ใน Developer mode แล้ว
|
|
105
|
-
ทดสอบด้วย `ใช้ DODO เรียก project_overview` HTTP จะแสดง Compact surface
|
|
118
|
+
ทดสอบด้วย `ใช้ DODO เรียก project_overview` HTTP จะแสดง Compact surface 20
|
|
106
119
|
tools และเข้าถึง operations ที่เหลือผ่าน `dodo_discover` กับ gateway ตามสิทธิ์เดิม
|
|
107
120
|
|
|
108
121
|
ดูขั้นตอนเต็มและวิธีแก้ OAuth/Tunnel ที่ [เชื่อม Web clients](docs/WEB_CLIENTS.md)
|
|
@@ -156,6 +169,35 @@ Chat & Tasks · Runs & Jobs · Approvals · Knowledge · Settings) หน้า
|
|
|
156
169
|
สร้าง MCP app ใหม่ตามพฤติกรรมของ client การตั้งค่านี้เปลี่ยนเฉพาะการมองเห็น tools
|
|
157
170
|
และไม่เพิ่ม OAuth scope, trust, approval หรือสิทธิ์ของ profile
|
|
158
171
|
|
|
172
|
+
### เพิ่มโปรเจกต์ครั้งเดียว แล้วสั่งงานด้วยชื่อ
|
|
173
|
+
|
|
174
|
+
เพิ่มโปรเจกต์จากหน้า Projects (หรือ CLI) โดยกรอกแค่ **ชื่อ**, **absolute path** และ
|
|
175
|
+
**ระดับการเข้าถึง** หนึ่งค่า:
|
|
176
|
+
|
|
177
|
+
| ระดับ | AI ทำอะไรได้ |
|
|
178
|
+
|---|---|
|
|
179
|
+
| อ่านอย่างเดียว | อ่าน ค้นหา วิเคราะห์ |
|
|
180
|
+
| แก้ไข | อ่านและแก้ไฟล์ |
|
|
181
|
+
| ทำงานเต็มรูปแบบ | อ่าน แก้ไฟล์ และรันคำสั่ง/ทดสอบ |
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
dodo project add "C:\Users\User\Desktop\auto-upload" --name "auto-upload" --access full
|
|
185
|
+
dodo project access "auto-upload" --mode edit
|
|
186
|
+
dodo project list
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
ในโหมดส่วนตัว (ค่าเริ่มต้น) เท่านี้ก็พร้อมใช้ — ไม่ต้องตั้ง Client ACL, Trust หรือ
|
|
190
|
+
profile allowlist ซ้ำอีกหลายหน้า ระดับที่เลือกเป็น **เพดาน** สิทธิ์จริงคือส่วนที่ทับกับ
|
|
191
|
+
OAuth scope ของ client นั้น (token ที่อ่านได้อย่างเดียวจะยังอ่านได้อย่างเดียวเสมอ)
|
|
192
|
+
|
|
193
|
+
จากนั้นสั่ง AI ด้วยชื่อโปรเจกต์ได้ตรง ๆ:
|
|
194
|
+
|
|
195
|
+
> "ใช้ DODO แก้โปรเจกต์ auto-upload"
|
|
196
|
+
|
|
197
|
+
AI จะ resolve ชื่อเอง (`project_overview` ด้วย `targetProject: "auto-upload"`) รับ
|
|
198
|
+
`workspaceId`/`workspaceEpoch` ของโปรเจกต์นั้น แล้วส่ง `targetProject` ไปกับทุกคำสั่งถัดไป
|
|
199
|
+
เจ้าของไม่ต้องรู้หรือพิมพ์ project ID เลย ดู [docs/PROJECTS.md](docs/PROJECTS.md)
|
|
200
|
+
|
|
159
201
|
ลงทะเบียนโปรเจกต์ที่ต้องการใช้งานบ่อยได้โดยไม่เริ่ม server:
|
|
160
202
|
|
|
161
203
|
```bash
|
|
@@ -328,14 +370,22 @@ command sandbox, idempotency และ audit เดิม `agent_exec` ใช้
|
|
|
328
370
|
|
|
329
371
|
### เชื่อม Remote MCP ผ่าน Cloudflare Tunnel
|
|
330
372
|
|
|
331
|
-
DODO เลือกวิธีเชื่อมต่อระดับ installation ได้หนึ่งแบบ: `local`
|
|
332
|
-
|
|
333
|
-
|
|
373
|
+
DODO เลือกวิธีเชื่อมต่อระดับ installation ได้หนึ่งแบบ: `local` (loopback เท่านั้น),
|
|
374
|
+
`external` (Cloudflare ที่เจ้าของติดตั้ง/รันเอง) หรือ `tunnel` (DODO ดูแล cloudflared)
|
|
375
|
+
ค่าที่เลือกมีผลกับทุก `dodo start` และไม่มี fallback อัตโนมัติ
|
|
376
|
+
|
|
377
|
+
เฉพาะเครื่อง:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
dodo tunnel configure --loopback
|
|
381
|
+
dodo start
|
|
382
|
+
```
|
|
334
383
|
|
|
335
|
-
Local
|
|
384
|
+
Cloudflare Local (ติดตั้งในเครื่อง) — ผู้ใช้ดูแล process/token เอง:
|
|
336
385
|
|
|
337
386
|
```bash
|
|
338
|
-
dodo tunnel configure --local
|
|
387
|
+
dodo tunnel configure --cloudflare-local --public-url https://mcp.example.com
|
|
388
|
+
# เริ่ม cloudflared ที่ติดตั้งในเครื่องด้วยวิธีของผู้ใช้
|
|
339
389
|
dodo start
|
|
340
390
|
```
|
|
341
391
|
|
|
@@ -352,7 +402,8 @@ dodo start
|
|
|
352
402
|
|
|
353
403
|
เจ้าของสร้าง remotely-managed Tunnel, public hostname และ DNS ใน Cloudflare และ route
|
|
354
404
|
**ทุก path** มาที่ `http://127.0.0.1:21730` DODO ไม่จัดการ Cloudflare account/DNS
|
|
355
|
-
|
|
405
|
+
เฉพาะ DODO Tunnel เท่านั้นที่ใช้ token ซึ่งถูกบันทึกใน macOS Keychain, Windows
|
|
406
|
+
Credential Manager หรือ Linux Secret Service;
|
|
356
407
|
global config เก็บเพียง opaque reference ทุกครั้งที่เปิด DODO ระบบจะเริ่ม
|
|
357
408
|
`cloudflared` เป็น child และหยุด child พร้อม DODO Token ไม่อยู่ใน argv, config, log,
|
|
358
409
|
audit, browser storage, MCP response หรือ environment ของ MCP jobs
|
|
@@ -369,14 +420,17 @@ dodo web --close # ปิดหน้า Remote Config แต่ MCP/Tunnel
|
|
|
369
420
|
terminal หลังจับคู่ browser จะใช้ cookie ที่เป็น `Secure`, `HttpOnly`,
|
|
370
421
|
`SameSite=Strict` และจำกัด path ที่ `/config` หน้า Remote Config ปิดอัตโนมัติภายใน
|
|
371
422
|
1 ชั่วโมง การเปิดใหม่ออก code/session ใหม่และไม่ต้อง restart MCP ไม่มี token หรือ
|
|
372
|
-
credential อยู่ใน URL
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
423
|
+
credential อยู่ใน URL ใช้ได้ทั้งโหมด **Cloudflare Local** ที่เจ้าของรัน
|
|
424
|
+
`cloudflared` เอง และ **DODO Tunnel** ที่ DODO ดูแล process; โหมดเฉพาะเครื่องไม่มี
|
|
425
|
+
public route จึงเปิด Remote Config ไม่ได้
|
|
426
|
+
|
|
427
|
+
หน้า Local Config ที่ `127.0.0.1:21731` แยกตัวเลือกชัดเจนเป็น
|
|
428
|
+
**เฉพาะเครื่อง (Loopback)**, **Cloudflare Local (ติดตั้งในเครื่อง)** และ
|
|
429
|
+
**DODO Tunnel** ตั้ง public origin ได้ และรับ token แบบ write-only เฉพาะ DODO Tunnel
|
|
430
|
+
ค่าใหม่มีผลหลัง restart หน้าเว็บแสดง Active MCP URL จาก runtime จริงและไม่กล่าวว่า
|
|
431
|
+
Tunnel หรือ AI client เชื่อมแล้วหากไม่มีหลักฐาน ดูสถานะด้วย `dodo tunnel status`,
|
|
432
|
+
ตรวจ connectivity ด้วย `dodo tunnel doctor` และดู log ที่ redacted ด้วย
|
|
433
|
+
`dodo tunnel logs` (log/supervisor ใช้กับ DODO Tunnel เท่านั้น)
|
|
380
434
|
|
|
381
435
|
บน macOS `dodo setup --yes` เลือก component `cloudflared` อยู่ในชุด `all` แล้ว
|
|
382
436
|
Tunnel route ต้องชี้เฉพาะ MCP/OAuth listener `21730` ห้ามชี้ Local Config `21731`
|
package/dist/cli/main.js
CHANGED
|
@@ -22,8 +22,11 @@ import { TRUST_MODE_DESCRIPTIONS } from '../security/policy.js';
|
|
|
22
22
|
import { DodoError } from '../errors.js';
|
|
23
23
|
import { sandboxAvailability } from '../services/jobs/sandbox.js';
|
|
24
24
|
import { DesktopPolicyInputSchema, saveDesktopPolicy } from '../services/desktop/desktopPolicy.js';
|
|
25
|
+
import { AndroidPolicyInputSchema, saveAndroidPolicy } from '../services/android/androidPolicy.js';
|
|
25
26
|
import { INPUT_LIMIT_PROFILES } from '../config/limits.js';
|
|
26
27
|
import { ProjectRegistry } from '../projects/registry.js';
|
|
28
|
+
import { ProjectAdmin } from '../projects/ownerAdmin.js';
|
|
29
|
+
import { LEVEL_SUMMARY, parseProjectAccessLevel } from '../security/projectAccess.js';
|
|
27
30
|
import { runCliMenu } from './menu.js';
|
|
28
31
|
import { TunnelRuntime } from '../tunnel/runtime.js';
|
|
29
32
|
import { INSTALLATION_AUTHORITY_ID } from '../auth/constants.js';
|
|
@@ -84,11 +87,16 @@ function withProjectRegistry(fn) {
|
|
|
84
87
|
db.close();
|
|
85
88
|
}
|
|
86
89
|
}
|
|
90
|
+
/** The CLI and Local Config share ONE owner-administration service. */
|
|
91
|
+
function withProjectAdmin(fn) {
|
|
92
|
+
return withProjectRegistry((_registry, store) => fn(new ProjectAdmin(store), store));
|
|
93
|
+
}
|
|
87
94
|
function projectLines(project) {
|
|
88
95
|
return [
|
|
89
96
|
`${project.displayName} ${project.projectId}`,
|
|
90
97
|
` Path: ${project.root}`,
|
|
91
98
|
` Workspace: ${project.workspaceId}`,
|
|
99
|
+
` Access: ${project.accessLevel} — ${LEVEL_SUMMARY[project.accessLevel]}`,
|
|
92
100
|
` Status: ${project.availability} — ${project.statusText}`,
|
|
93
101
|
` Updated: ${new Date(project.updatedAt).toISOString()}`,
|
|
94
102
|
...(project.removedAt === null ? [] : [` Removed: ${new Date(project.removedAt).toISOString()}`]),
|
|
@@ -145,20 +153,22 @@ async function launchHttp(opts) {
|
|
|
145
153
|
const connectionMode = config.tunnel.connectionMode;
|
|
146
154
|
const publicUrl = config.publicUrl;
|
|
147
155
|
const selectedPort = opts.port ?? config.port;
|
|
148
|
-
if (connectionMode
|
|
156
|
+
if (connectionMode !== 'local') {
|
|
149
157
|
if (!publicUrl)
|
|
150
|
-
fail('
|
|
158
|
+
fail('Cloudflare mode requires a public HTTPS origin; configure Cloudflare Local or DODO Tunnel with --public-url https://your-host');
|
|
151
159
|
validatePublicUrl(publicUrl, false);
|
|
160
|
+
}
|
|
161
|
+
if (connectionMode === 'tunnel') {
|
|
152
162
|
if (!config.tunnel.credentialRef)
|
|
153
163
|
fail('Tunnel mode requires a saved credential; run dodo tunnel configure --tunnel --os-credential --public-url https://your-host');
|
|
154
164
|
}
|
|
155
|
-
else {
|
|
165
|
+
else if (connectionMode === 'local') {
|
|
156
166
|
if (opts.web)
|
|
157
|
-
fail('Remote Config requires
|
|
167
|
+
fail('Remote Config requires a public Cloudflare connection; select Cloudflare Local or DODO Tunnel, restart DODO, then run dodo --web');
|
|
158
168
|
if (selectedPort === 0)
|
|
159
169
|
fail('local connection mode requires a concrete MCP port');
|
|
160
170
|
}
|
|
161
|
-
const endpointOrigin = connectionMode === '
|
|
171
|
+
const endpointOrigin = connectionMode === 'local' ? `http://127.0.0.1:${selectedPort}` : publicUrl;
|
|
162
172
|
const startOpts = {
|
|
163
173
|
configPort: config.configPort,
|
|
164
174
|
...(opts.bypass ? { runMode: 'bypass' } : opts.allow ? { runMode: 'allow-all' } : {}),
|
|
@@ -203,8 +213,13 @@ async function launchHttp(opts) {
|
|
|
203
213
|
throw error;
|
|
204
214
|
}
|
|
205
215
|
}
|
|
206
|
-
else
|
|
207
|
-
|
|
216
|
+
else {
|
|
217
|
+
if (connectionMode === 'external' && opts.web)
|
|
218
|
+
printRemoteConfig(await ipcForInstallation('remoteConfig.open'));
|
|
219
|
+
if (!opts.quiet)
|
|
220
|
+
console.log(connectionMode === 'external'
|
|
221
|
+
? `[dodo] Cloudflare Local selected: ${endpointOrigin}/mcp (cloudflared is owner-managed)`
|
|
222
|
+
: `[dodo] Loopback-only connection selected: ${endpointOrigin}/mcp`);
|
|
208
223
|
}
|
|
209
224
|
async function stop(signal) {
|
|
210
225
|
if (shutdownPromise)
|
|
@@ -287,16 +302,40 @@ program.command('setup')
|
|
|
287
302
|
// trust/client access; those remain keyed by the referenced workspace ID.
|
|
288
303
|
const project = program.command('project').description('manage the local owner-only project registry');
|
|
289
304
|
project.command('add <path>')
|
|
290
|
-
.description('register an absolute project directory
|
|
305
|
+
.description('register an absolute project directory and choose what AI clients may do in it')
|
|
291
306
|
.option('--name <display-name>', 'local display name (defaults to the directory name)')
|
|
307
|
+
.option('--access <level>', 'read | edit | full (read = read/search only, edit = also change files, full = also run commands)')
|
|
292
308
|
.option('--json', 'machine-readable result', false)
|
|
293
309
|
.action((projectPath, opts) => {
|
|
294
310
|
try {
|
|
295
|
-
const
|
|
311
|
+
const access = opts.access === undefined ? undefined : parseProjectAccessLevel(opts.access);
|
|
312
|
+
const result = withProjectAdmin((admin) => admin.add({ path: projectPath, ...(opts.name ? { name: opts.name } : {}), ...(access ? { access } : {}) }));
|
|
296
313
|
printJsonOrLines(opts.json, result, () => [
|
|
297
314
|
result.relocated ? 'Updated the location of an existing project identity.' : result.changed ? 'Project registered.' : 'Project was already registered; nothing changed.',
|
|
298
315
|
...projectLines(result.project),
|
|
299
|
-
|
|
316
|
+
result.project.requiresClientAcl
|
|
317
|
+
? 'Managed mode: also allow a client for this project in Local Config before it can be used.'
|
|
318
|
+
: 'Ready to use. A client may do at most what this level allows, still bounded by its own OAuth scopes.',
|
|
319
|
+
]);
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error instanceof DodoError)
|
|
323
|
+
fail(`${error.code}: ${error.message}${error.recovery ? `\n → ${error.recovery}` : ''}`);
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
project.command('access <project>')
|
|
328
|
+
.description('set what AI clients may do in a registered project (by name or ID)')
|
|
329
|
+
.requiredOption('--mode <level>', 'read | edit | full')
|
|
330
|
+
.option('--json', 'machine-readable result', false)
|
|
331
|
+
.action((reference, opts) => {
|
|
332
|
+
try {
|
|
333
|
+
const level = parseProjectAccessLevel(opts.mode);
|
|
334
|
+
const updated = withProjectAdmin((admin) => admin.setAccess(admin.resolve(reference).projectId, level));
|
|
335
|
+
printJsonOrLines(opts.json, updated, () => [
|
|
336
|
+
`Access level for ${updated.displayName} is now ${updated.accessLevel}.`,
|
|
337
|
+
...projectLines(updated),
|
|
338
|
+
'OAuth scopes, trust, approvals, path/secret guards and the command sandbox are unchanged.',
|
|
300
339
|
]);
|
|
301
340
|
}
|
|
302
341
|
catch (error) {
|
|
@@ -306,7 +345,7 @@ project.command('add <path>')
|
|
|
306
345
|
}
|
|
307
346
|
});
|
|
308
347
|
project.command('list')
|
|
309
|
-
.description('list registered projects and current path readiness')
|
|
348
|
+
.description('list registered projects, their access level and current path readiness')
|
|
310
349
|
.option('--all', 'include reviewed removals', false)
|
|
311
350
|
.option('--json', 'machine-readable output', false)
|
|
312
351
|
.action((opts) => {
|
|
@@ -365,12 +404,14 @@ project.command('remove <projectId>')
|
|
|
365
404
|
// --------------------------------------------------------------- tunnel ----
|
|
366
405
|
const tunnel = program.command('tunnel').description('manage or inspect a Cloudflare remotely-managed Tunnel without managing DNS or the Cloudflare account');
|
|
367
406
|
tunnel.command('configure')
|
|
368
|
-
.description('select
|
|
407
|
+
.description('select loopback, owner-managed Cloudflare Local, or DODO-owned Tunnel mode; tokens are never written to config')
|
|
369
408
|
.option('--tunnel', 'select the DODO-owned Tunnel for every dodo start', false)
|
|
370
|
-
.option('--local', '
|
|
409
|
+
.option('--cloudflare-local', 'use cloudflared installed and run by the owner; DODO advertises only the saved public origin', false)
|
|
410
|
+
.option('--loopback', 'select loopback-only MCP for every dodo start', false)
|
|
411
|
+
.option('--local', 'deprecated alias for --loopback', false)
|
|
371
412
|
.option('--managed', 'deprecated alias for --tunnel', false)
|
|
372
|
-
.option('--external', '
|
|
373
|
-
.option('--public-url <url>', 'public HTTPS origin used by
|
|
413
|
+
.option('--external', 'alias for --cloudflare-local', false)
|
|
414
|
+
.option('--public-url <url>', 'public HTTPS origin used by Cloudflare Local or DODO Tunnel')
|
|
374
415
|
.option('--os-credential', 'prompt through the reviewed OS credential provider', false)
|
|
375
416
|
.option('--token-env <name>', 'reference an uppercase environment variable that already contains the token')
|
|
376
417
|
.option('--token-file <absolute-path>', 'reference an owner-private absolute token file')
|
|
@@ -383,9 +424,10 @@ tunnel.command('configure')
|
|
|
383
424
|
.action(async (opts) => {
|
|
384
425
|
try {
|
|
385
426
|
const chooseTunnel = opts.tunnel || opts.managed;
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
427
|
+
const chooseExternal = opts.cloudflareLocal || opts.external;
|
|
428
|
+
const chooseLoopback = opts.loopback || opts.local;
|
|
429
|
+
if ([chooseTunnel, chooseExternal, chooseLoopback].filter(Boolean).length > 1)
|
|
430
|
+
fail('choose only one of --loopback, --cloudflare-local or --tunnel');
|
|
389
431
|
const methods = [opts.osCredential, opts.tokenEnv !== undefined, opts.tokenFile !== undefined].filter(Boolean).length;
|
|
390
432
|
if (methods > 1)
|
|
391
433
|
fail('choose only one of --os-credential, --token-env or --token-file');
|
|
@@ -396,16 +438,18 @@ tunnel.command('configure')
|
|
|
396
438
|
const { dir } = resolveConfigDir(process.env);
|
|
397
439
|
ensureConfigDir(dir);
|
|
398
440
|
const paths = statePaths(dir), current = loadGlobalConfig(paths.configFile);
|
|
399
|
-
|
|
441
|
+
if (chooseExternal && methods > 0)
|
|
442
|
+
fail('Cloudflare Local is owner-managed and does not accept a Tunnel credential in DODO');
|
|
443
|
+
const connectionMode = chooseTunnel ? 'tunnel' : chooseExternal ? 'external' : chooseLoopback ? 'local' : methods > 0 ? 'tunnel' : current.tunnel.connectionMode;
|
|
400
444
|
const publicUrl = opts.publicUrl !== undefined
|
|
401
445
|
? validatePublicUrl(opts.publicUrl, false).origin
|
|
402
446
|
: current.publicUrl;
|
|
403
|
-
if (connectionMode
|
|
404
|
-
fail('
|
|
405
|
-
if (connectionMode
|
|
447
|
+
if (connectionMode !== 'local' && !publicUrl)
|
|
448
|
+
fail('Cloudflare mode requires --public-url https://your-host (or a previously saved public origin)');
|
|
449
|
+
if (connectionMode !== 'local' && publicUrl)
|
|
406
450
|
validatePublicUrl(publicUrl, false);
|
|
407
451
|
if (connectionMode === 'local' && opts.publicUrl !== undefined)
|
|
408
|
-
fail('--public-url is used only with --tunnel');
|
|
452
|
+
fail('--public-url is used only with --cloudflare-local or --tunnel');
|
|
409
453
|
const credentials = await import('../tunnel/credentials.js');
|
|
410
454
|
const { resolveCloudflared } = await import('../tunnel/supervisor.js');
|
|
411
455
|
const previousCredentialRef = current.tunnel.credentialRef;
|
|
@@ -445,14 +489,33 @@ tunnel.command('configure')
|
|
|
445
489
|
saveGlobalConfig(paths.configFile, next);
|
|
446
490
|
if (opts.removeCredential && previousCredentialRef)
|
|
447
491
|
credentials.deleteOsTunnelCredential(previousCredentialRef);
|
|
448
|
-
const result = {
|
|
492
|
+
const result = {
|
|
493
|
+
connectionMode: next.tunnel.connectionMode,
|
|
494
|
+
publicOrigin: next.tunnel.connectionMode === 'local' ? null : next.publicUrl,
|
|
495
|
+
credential: next.tunnel.connectionMode === 'tunnel'
|
|
496
|
+
? next.tunnel.credentialRef ? 'configured' : 'not-configured'
|
|
497
|
+
: 'not-used',
|
|
498
|
+
metricsPort: next.tunnel.metricsPort,
|
|
499
|
+
maxRestarts: next.tunnel.maxRestarts,
|
|
500
|
+
cloudflared: next.tunnel.connectionMode === 'local'
|
|
501
|
+
? 'not-used'
|
|
502
|
+
: next.tunnel.connectionMode === 'external'
|
|
503
|
+
? 'owner-managed'
|
|
504
|
+
: next.tunnel.executable ? 'owner-selected' : 'trusted-PATH',
|
|
505
|
+
started: false,
|
|
506
|
+
};
|
|
449
507
|
if (opts.json)
|
|
450
508
|
console.log(JSON.stringify(result, null, 2));
|
|
451
509
|
else {
|
|
452
510
|
console.log(`Connection mode: ${result.connectionMode}`);
|
|
453
|
-
console.log(`Credential: ${result.credential} (value is not stored in config or displayed)`);
|
|
454
|
-
|
|
455
|
-
|
|
511
|
+
console.log(`Credential: ${result.credential}${result.connectionMode === 'tunnel' ? ' (value is not stored in config or displayed)' : ''}`);
|
|
512
|
+
if (result.connectionMode === 'tunnel')
|
|
513
|
+
console.log(`Readiness: 127.0.0.1:${result.metricsPort} Restarts: ${result.maxRestarts}`);
|
|
514
|
+
console.log(result.connectionMode === 'tunnel'
|
|
515
|
+
? 'Saved. Every dodo start now owns this Tunnel and fails closed if it cannot start.'
|
|
516
|
+
: result.connectionMode === 'external'
|
|
517
|
+
? 'Saved. DODO advertises the public origin; you must start and maintain cloudflared on this machine.'
|
|
518
|
+
: 'Saved. Every dodo start now uses only the loopback MCP endpoint.');
|
|
456
519
|
}
|
|
457
520
|
}
|
|
458
521
|
catch (error) {
|
|
@@ -1422,6 +1485,61 @@ desktop.command('disable').description('revoke and forget installation-wide desk
|
|
|
1422
1485
|
console.log(JSON.stringify(saved, null, 2));
|
|
1423
1486
|
console.log('Desktop access disabled and forgotten for this DODO installation.');
|
|
1424
1487
|
});
|
|
1488
|
+
function saveInstallationAndroidPolicy(input) {
|
|
1489
|
+
const args = AndroidPolicyInputSchema.parse(input);
|
|
1490
|
+
const { dir } = resolveConfigDir(process.env);
|
|
1491
|
+
ensureConfigDir(dir);
|
|
1492
|
+
const db = openDatabase(statePaths(dir).dbFile);
|
|
1493
|
+
try {
|
|
1494
|
+
const store = new Store(db);
|
|
1495
|
+
store.installSecret();
|
|
1496
|
+
return db.transaction(() => saveAndroidPolicy(store, INSTALLATION_AUTHORITY_ID, 'local-cli', args, Date.now()))();
|
|
1497
|
+
}
|
|
1498
|
+
finally {
|
|
1499
|
+
db.close();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
const android = program.command('android').description('inspect and authorize Android devices connected through ADB');
|
|
1503
|
+
android.command('devices').description('list devices visible to the local ADB host; does not grant AI access')
|
|
1504
|
+
.action(async () => {
|
|
1505
|
+
const { NativeAdbBackend, parseAdbDevices } = await import('../services/android/adbBackend.js');
|
|
1506
|
+
const backend = new NativeAdbBackend(process.cwd());
|
|
1507
|
+
const result = await backend.run(['devices', '-l'], { maxStdoutBytes: 128 * 1024 });
|
|
1508
|
+
if (result.code !== 0)
|
|
1509
|
+
fail('ADB could not list devices. Install Platform-Tools and authorize this host on the phone.');
|
|
1510
|
+
console.log(JSON.stringify(parseAdbDevices(result.stdout.toString('utf8')), null, 2));
|
|
1511
|
+
});
|
|
1512
|
+
android.command('status').description('show Android policy and ADB status for the running workspace')
|
|
1513
|
+
.action(async () => console.log(JSON.stringify(await ipcForCwd('android.status'), null, 2)));
|
|
1514
|
+
android.command('allow')
|
|
1515
|
+
.description('allow exact connected ADB serials; --persist remembers once for this DODO installation')
|
|
1516
|
+
.requiredOption('--device <serials...>', 'exact serials from dodo android devices')
|
|
1517
|
+
.option('--mode <mode>', 'view | control', 'view')
|
|
1518
|
+
.option('--minutes <n>', 'temporary permission lifetime 1..480 minutes; excludes --persist', (v) => Number(v))
|
|
1519
|
+
.option('--persist', 'remember these device serials until dodo android disable', false)
|
|
1520
|
+
.option('--yes', 'acknowledge that ADB can expose private data and control the selected devices', false)
|
|
1521
|
+
.action(async (opts) => {
|
|
1522
|
+
if (!['view', 'control'].includes(opts.mode))
|
|
1523
|
+
fail('Android mode must be view or control');
|
|
1524
|
+
if (!opts.yes)
|
|
1525
|
+
fail('ADB can read private device data and CONTROL can change apps/files with the Android shell user. Repeat with --yes to authorize exact serials.');
|
|
1526
|
+
if (opts.persist && opts.minutes !== undefined)
|
|
1527
|
+
fail('use --persist or --minutes, not both');
|
|
1528
|
+
const input = { mode: opts.mode, allowedDevices: opts.device, persistent: opts.persist, ...(opts.minutes !== undefined ? { minutes: opts.minutes } : {}) };
|
|
1529
|
+
if (opts.persist) {
|
|
1530
|
+
console.log(JSON.stringify(saveInstallationAndroidPolicy(input), null, 2));
|
|
1531
|
+
console.log('Remembered for this DODO installation. OAuth dodo:exec, project access, trust/approval and exact serial checks still apply.');
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
console.log(JSON.stringify(await ipcForCwd('android.policy', input), null, 2));
|
|
1535
|
+
console.log('Temporary Android grant: expires or resets on workspace epoch change.');
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
android.command('disable').description('revoke and forget Android ADB access installation-wide')
|
|
1539
|
+
.action(() => {
|
|
1540
|
+
console.log(JSON.stringify(saveInstallationAndroidPolicy({ mode: 'off' }), null, 2));
|
|
1541
|
+
console.log('Android ADB access disabled and forgotten for this DODO installation.');
|
|
1542
|
+
});
|
|
1425
1543
|
// Static-client helpers are imported lazily to keep CLI startup light.
|
|
1426
1544
|
import { addStaticClient as addStaticClientLocal, listStaticClients as listStaticClientsLocal } from '../auth/clients.js';
|
|
1427
1545
|
program.addHelpText('after', `
|
|
@@ -1436,11 +1554,17 @@ MCP subprocess clients:
|
|
|
1436
1554
|
Desktop (platform helper and explicit app permission required):
|
|
1437
1555
|
dodo desktop setup prepare the platform desktop backend
|
|
1438
1556
|
dodo desktop disable revoke desktop access
|
|
1557
|
+
Android (ADB host authorization and exact-device permission):
|
|
1558
|
+
dodo android devices list connected/authorized device serials
|
|
1559
|
+
dodo android allow --device SERIAL --mode control --persist --yes
|
|
1560
|
+
dodo android disable revoke DODO Android access
|
|
1439
1561
|
Remote setup:
|
|
1440
1562
|
dodo init --public-url https://... configure public origin once
|
|
1563
|
+
dodo tunnel configure --cloudflare-local --public-url https://...
|
|
1564
|
+
use cloudflared you install/run yourself
|
|
1441
1565
|
dodo tunnel configure --tunnel --os-credential --public-url https://...
|
|
1442
1566
|
select the persistent DODO Tunnel
|
|
1443
|
-
dodo tunnel configure --
|
|
1567
|
+
dodo tunnel configure --loopback select loopback-only MCP
|
|
1444
1568
|
dodo start use exactly the saved connection mode
|
|
1445
1569
|
dodo --web start/renew Remote Config through that tunnel for 1 hour
|
|
1446
1570
|
dodo web --close close Remote Config; keep MCP/Tunnel running`);
|