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.
Files changed (91) hide show
  1. package/README.md +74 -20
  2. package/dist/cli/main.js +152 -28
  3. package/dist/cli/main.js.map +1 -1
  4. package/dist/cli/menu.js +2 -2
  5. package/dist/cli/menu.js.map +1 -1
  6. package/dist/config/globalConfig.js +7 -3
  7. package/dist/config/globalConfig.js.map +1 -1
  8. package/dist/config/tunnelConfig.js +2 -1
  9. package/dist/config/tunnelConfig.js.map +1 -1
  10. package/dist/projects/ownerAdmin.js +86 -0
  11. package/dist/projects/ownerAdmin.js.map +1 -0
  12. package/dist/projects/registry.js +90 -9
  13. package/dist/projects/registry.js.map +1 -1
  14. package/dist/security/projectAccess.js +69 -0
  15. package/dist/security/projectAccess.js.map +1 -0
  16. package/dist/security/projectAuthority.js +5 -1
  17. package/dist/security/projectAuthority.js.map +1 -1
  18. package/dist/server/appServer.js +23 -9
  19. package/dist/server/appServer.js.map +1 -1
  20. package/dist/server/bootstrap.js +9 -1
  21. package/dist/server/bootstrap.js.map +1 -1
  22. package/dist/server/configUi/app.css +8 -0
  23. package/dist/server/configUi/app.js +142 -18
  24. package/dist/server/configUi/index.html +50 -13
  25. package/dist/server/installationRuntime.js +6 -2
  26. package/dist/server/installationRuntime.js.map +1 -1
  27. package/dist/server/instructions.js +1 -1
  28. package/dist/server/instructions.js.map +1 -1
  29. package/dist/server/ipcDispatch.js +3 -0
  30. package/dist/server/ipcDispatch.js.map +1 -1
  31. package/dist/server/localConfig.js +80 -13
  32. package/dist/server/localConfig.js.map +1 -1
  33. package/dist/services/ai/subagents.js +1 -1
  34. package/dist/services/ai/subagents.js.map +1 -1
  35. package/dist/services/android/adbBackend.js +106 -0
  36. package/dist/services/android/adbBackend.js.map +1 -0
  37. package/dist/services/android/androidPolicy.js +61 -0
  38. package/dist/services/android/androidPolicy.js.map +1 -0
  39. package/dist/services/android/androidService.js +444 -0
  40. package/dist/services/android/androidService.js.map +1 -0
  41. package/dist/services/android/protocol.js +39 -0
  42. package/dist/services/android/protocol.js.map +1 -0
  43. package/dist/services/multimodal/storage.js +10 -1
  44. package/dist/services/multimodal/storage.js.map +1 -1
  45. package/dist/services/schedules/scheduleService.js +8 -1
  46. package/dist/services/schedules/scheduleService.js.map +1 -1
  47. package/dist/setup/componentProbes.js +7 -1
  48. package/dist/setup/componentProbes.js.map +1 -1
  49. package/dist/setup/setup.js +11 -1
  50. package/dist/setup/setup.js.map +1 -1
  51. package/dist/store/db.js +14 -0
  52. package/dist/store/db.js.map +1 -1
  53. package/dist/tools/agentRuntimeTools.js +2 -3
  54. package/dist/tools/agentRuntimeTools.js.map +1 -1
  55. package/dist/tools/androidTools.js +113 -0
  56. package/dist/tools/androidTools.js.map +1 -0
  57. package/dist/tools/catalog.js +3 -0
  58. package/dist/tools/catalog.js.map +1 -1
  59. package/dist/tools/context.js +85 -5
  60. package/dist/tools/context.js.map +1 -1
  61. package/dist/tools/projectTools.js +1 -1
  62. package/dist/tools/projectTools.js.map +1 -1
  63. package/dist/tools/surface.js +18 -11
  64. package/dist/tools/surface.js.map +1 -1
  65. package/dist/tunnel/control.js +11 -4
  66. package/dist/tunnel/control.js.map +1 -1
  67. package/dist/tunnel/log.js +32 -0
  68. package/dist/tunnel/log.js.map +1 -1
  69. package/dist/tunnel/supervisor.js +103 -24
  70. package/dist/tunnel/supervisor.js.map +1 -1
  71. package/docs/ANDROID.md +105 -28
  72. package/docs/ARCHITECTURE.md +16 -7
  73. package/docs/AUTH.md +6 -0
  74. package/docs/COMPATIBILITY.md +14 -7
  75. package/docs/MANUAL_ACCEPTANCE.md +40 -6
  76. package/docs/PROJECTS.md +73 -2
  77. package/docs/RELEASE_1.1.0.md +69 -0
  78. package/docs/RELEASE_1.2.0.md +75 -0
  79. package/docs/RELEASE_NOTES.md +30 -0
  80. package/docs/SECURITY.md +45 -6
  81. package/docs/TEST_REPORT.md +96 -0
  82. package/docs/TUNNEL.md +79 -16
  83. package/docs/adr/048-persistent-connection-mode.md +3 -1
  84. package/docs/adr/049-android-adb-control.md +52 -0
  85. package/docs/adr/050-cloudflare-local-and-project-access.md +67 -0
  86. package/docs/adr/README.md +2 -0
  87. package/package.json +6 -2
  88. package/schemas/global-config.schema.json +1 -0
  89. package/schemas/tools.compact.json +312 -9
  90. package/schemas/tools.hybrid.json +485 -152
  91. package/schemas/tools.json +3610 -534
package/README.md CHANGED
@@ -1,17 +1,18 @@
1
1
  # DODO MCP
2
2
 
3
- **DODO MCP 1.0.6** คือ MCP server แบบ local-first สำหรับให้ AI ช่วยพัฒนา software โดยทำงานกับ workspace ที่เจ้าของเลือก ค่าเริ่มต้นเป็นโหมดส่วนตัวแบบเพิ่มโปรเจกต์แล้วใช้ได้ทันที และยังมีโหมด managed สำหรับแยก workspace ACL/trust แบบละเอียด
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 19 tools เพื่อลดภาระการโหลด schema
10
- - STDIO ใช้ Full Tool Surface; มี capability definitions ทั้งหมด 125 รายการ แต่ค่าเริ่มต้นซ่อน Sub-agent 4 operations จาก MCP จึงเห็น 121 tools
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 1.0.6 ไม่โหลด Sharp ตั้งแต่เริ่ม CLI อีกต่อไป จึงใช้คำสั่งพื้นฐานและ MCP
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 ประมาณ 19
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` หรือ `tunnel` ค่าที่เลือก
332
- มีผลกับทุก `dodo start` และไม่มี fallback อัตโนมัติ หากเลือก Tunnel แล้ว credential หรือ
333
- readiness ไม่พร้อม DODO จะหยุด startup แทนการเปิด local โดยไม่แจ้ง
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 mode:
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
- Token ถูกบันทึกใน macOS Keychain, Windows Credential Manager หรือ Linux Secret Service;
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 คำสั่งนี้ใช้ได้เฉพาะเมื่อ persistent mode เป็น `tunnel` และ
373
- DODO-owned Tunnel กำลังทำงาน; มันไม่รับ token ใหม่ผ่าน IPC และไม่เปลี่ยนจาก Local เอง
374
-
375
- หน้า Local Config ที่ `127.0.0.1:21731` เลือก Local/Tunnel, ตั้ง public origin และบันทึก
376
- token แบบ write-only เข้า OS credential store ได้ ค่าใหม่มีผลหลัง restart หน้าเว็บแสดง
377
- Active MCP URL จาก runtime จริงและไม่กล่าวว่า Tunnel หรือ AI client เชื่อมแล้วหากไม่มี
378
- หลักฐาน ดูสถานะด้วย `dodo tunnel status`, ตรวจ connectivity ด้วย `dodo tunnel doctor`
379
- และดู log ที่ redacted ด้วย `dodo tunnel logs`
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 === 'tunnel') {
156
+ if (connectionMode !== 'local') {
149
157
  if (!publicUrl)
150
- fail('Tunnel mode requires a public HTTPS origin; run dodo tunnel configure --tunnel --os-credential --public-url https://your-host');
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 Tunnel mode; configure Tunnel, restart DODO, then run dodo --web');
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 === 'tunnel' ? publicUrl : `http://127.0.0.1:${selectedPort}`;
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 if (!opts.quiet) {
207
- console.log(`[dodo] Local connection selected: ${endpointOrigin}/mcp`);
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 without changing trust or client access')
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 result = withProjectRegistry((registry) => registry.add(projectPath, opts.name));
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
- 'Trust, OAuth grants and workspace client access were not changed.',
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 persistent local or DODO-owned Tunnel mode; tokens are never written to config')
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', 'select local-only MCP for every dodo start', false)
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', 'deprecated alias for --local', false)
373
- .option('--public-url <url>', 'public HTTPS origin used by the selected DODO Tunnel')
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 chooseLocal = opts.local || opts.external;
387
- if (chooseTunnel && chooseLocal)
388
- fail('choose --tunnel or --local');
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
- const connectionMode = chooseTunnel ? 'tunnel' : chooseLocal ? 'local' : methods > 0 ? 'tunnel' : current.tunnel.connectionMode;
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 === 'tunnel' && !publicUrl)
404
- fail('Tunnel mode requires --public-url https://your-host (or a previously saved public origin)');
405
- if (connectionMode === 'tunnel' && publicUrl)
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 = { connectionMode: next.tunnel.connectionMode, publicOrigin: next.tunnel.connectionMode === 'tunnel' ? next.publicUrl : null, credential: next.tunnel.credentialRef ? 'configured' : 'not-configured', metricsPort: next.tunnel.metricsPort, maxRestarts: next.tunnel.maxRestarts, cloudflared: next.tunnel.executable ? 'owner-selected' : 'trusted-PATH', started: false };
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
- console.log(`Readiness: 127.0.0.1:${result.metricsPort} Restarts: ${result.maxRestarts}`);
455
- console.log(result.connectionMode === 'tunnel' ? 'Saved. Every dodo start now owns this Tunnel and fails closed if it cannot start.' : 'Saved. Every dodo start now uses only the local MCP endpoint.');
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 --local select local-only MCP
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`);