dodo-mcp 1.1.0 → 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 (69) hide show
  1. package/README.md +55 -14
  2. package/dist/cli/main.js +92 -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 +6 -2
  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 +5 -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 +93 -18
  24. package/dist/server/configUi/index.html +22 -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.map +1 -1
  30. package/dist/server/localConfig.js +55 -13
  31. package/dist/server/localConfig.js.map +1 -1
  32. package/dist/services/ai/subagents.js +1 -1
  33. package/dist/services/ai/subagents.js.map +1 -1
  34. package/dist/services/multimodal/storage.js +10 -1
  35. package/dist/services/multimodal/storage.js.map +1 -1
  36. package/dist/services/schedules/scheduleService.js +8 -1
  37. package/dist/services/schedules/scheduleService.js.map +1 -1
  38. package/dist/store/db.js +14 -0
  39. package/dist/store/db.js.map +1 -1
  40. package/dist/tools/agentRuntimeTools.js +2 -3
  41. package/dist/tools/agentRuntimeTools.js.map +1 -1
  42. package/dist/tools/context.js +85 -5
  43. package/dist/tools/context.js.map +1 -1
  44. package/dist/tools/surface.js +2 -6
  45. package/dist/tools/surface.js.map +1 -1
  46. package/dist/tunnel/control.js +11 -4
  47. package/dist/tunnel/control.js.map +1 -1
  48. package/dist/tunnel/log.js +32 -0
  49. package/dist/tunnel/log.js.map +1 -1
  50. package/dist/tunnel/supervisor.js +103 -24
  51. package/dist/tunnel/supervisor.js.map +1 -1
  52. package/docs/ARCHITECTURE.md +12 -4
  53. package/docs/AUTH.md +6 -0
  54. package/docs/COMPATIBILITY.md +3 -2
  55. package/docs/MANUAL_ACCEPTANCE.md +22 -6
  56. package/docs/PROJECTS.md +73 -2
  57. package/docs/RELEASE_1.2.0.md +75 -0
  58. package/docs/RELEASE_NOTES.md +15 -0
  59. package/docs/SECURITY.md +27 -6
  60. package/docs/TEST_REPORT.md +74 -0
  61. package/docs/TUNNEL.md +79 -16
  62. package/docs/adr/048-persistent-connection-mode.md +3 -1
  63. package/docs/adr/050-cloudflare-local-and-project-access.md +67 -0
  64. package/docs/adr/README.md +1 -0
  65. package/package.json +2 -1
  66. package/schemas/global-config.schema.json +1 -0
  67. package/schemas/tools.compact.json +123 -3
  68. package/schemas/tools.hybrid.json +297 -3
  69. package/schemas/tools.json +828 -0
package/README.md CHANGED
@@ -169,6 +169,35 @@ Chat & Tasks · Runs & Jobs · Approvals · Knowledge · Settings) หน้า
169
169
  สร้าง MCP app ใหม่ตามพฤติกรรมของ client การตั้งค่านี้เปลี่ยนเฉพาะการมองเห็น tools
170
170
  และไม่เพิ่ม OAuth scope, trust, approval หรือสิทธิ์ของ profile
171
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
+
172
201
  ลงทะเบียนโปรเจกต์ที่ต้องการใช้งานบ่อยได้โดยไม่เริ่ม server:
173
202
 
174
203
  ```bash
@@ -341,14 +370,22 @@ command sandbox, idempotency และ audit เดิม `agent_exec` ใช้
341
370
 
342
371
  ### เชื่อม Remote MCP ผ่าน Cloudflare Tunnel
343
372
 
344
- DODO เลือกวิธีเชื่อมต่อระดับ installation ได้หนึ่งแบบ: `local` หรือ `tunnel` ค่าที่เลือก
345
- มีผลกับทุก `dodo start` และไม่มี fallback อัตโนมัติ หากเลือก Tunnel แล้ว credential หรือ
346
- 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
+ ```
347
383
 
348
- Local mode:
384
+ Cloudflare Local (ติดตั้งในเครื่อง) — ผู้ใช้ดูแล process/token เอง:
349
385
 
350
386
  ```bash
351
- dodo tunnel configure --local
387
+ dodo tunnel configure --cloudflare-local --public-url https://mcp.example.com
388
+ # เริ่ม cloudflared ที่ติดตั้งในเครื่องด้วยวิธีของผู้ใช้
352
389
  dodo start
353
390
  ```
354
391
 
@@ -365,7 +402,8 @@ dodo start
365
402
 
366
403
  เจ้าของสร้าง remotely-managed Tunnel, public hostname และ DNS ใน Cloudflare และ route
367
404
  **ทุก path** มาที่ `http://127.0.0.1:21730` DODO ไม่จัดการ Cloudflare account/DNS
368
- Token ถูกบันทึกใน macOS Keychain, Windows Credential Manager หรือ Linux Secret Service;
405
+ เฉพาะ DODO Tunnel เท่านั้นที่ใช้ token ซึ่งถูกบันทึกใน macOS Keychain, Windows
406
+ Credential Manager หรือ Linux Secret Service;
369
407
  global config เก็บเพียง opaque reference ทุกครั้งที่เปิด DODO ระบบจะเริ่ม
370
408
  `cloudflared` เป็น child และหยุด child พร้อม DODO Token ไม่อยู่ใน argv, config, log,
371
409
  audit, browser storage, MCP response หรือ environment ของ MCP jobs
@@ -382,14 +420,17 @@ dodo web --close # ปิดหน้า Remote Config แต่ MCP/Tunnel
382
420
  terminal หลังจับคู่ browser จะใช้ cookie ที่เป็น `Secure`, `HttpOnly`,
383
421
  `SameSite=Strict` และจำกัด path ที่ `/config` หน้า Remote Config ปิดอัตโนมัติภายใน
384
422
  1 ชั่วโมง การเปิดใหม่ออก code/session ใหม่และไม่ต้อง restart MCP ไม่มี token หรือ
385
- credential อยู่ใน URL คำสั่งนี้ใช้ได้เฉพาะเมื่อ persistent mode เป็น `tunnel` และ
386
- DODO-owned Tunnel กำลังทำงาน; มันไม่รับ token ใหม่ผ่าน IPC และไม่เปลี่ยนจาก Local เอง
387
-
388
- หน้า Local Config ที่ `127.0.0.1:21731` เลือก Local/Tunnel, ตั้ง public origin และบันทึก
389
- token แบบ write-only เข้า OS credential store ได้ ค่าใหม่มีผลหลัง restart หน้าเว็บแสดง
390
- Active MCP URL จาก runtime จริงและไม่กล่าวว่า Tunnel หรือ AI client เชื่อมแล้วหากไม่มี
391
- หลักฐาน ดูสถานะด้วย `dodo tunnel status`, ตรวจ connectivity ด้วย `dodo tunnel doctor`
392
- และดู 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 เท่านั้น)
393
434
 
394
435
  บน macOS `dodo setup --yes` เลือก component `cloudflared` อยู่ในชุด `all` แล้ว
395
436
  Tunnel route ต้องชี้เฉพาะ MCP/OAuth listener `21730` ห้ามชี้ Local Config `21731`
package/dist/cli/main.js CHANGED
@@ -25,6 +25,8 @@ import { DesktopPolicyInputSchema, saveDesktopPolicy } from '../services/desktop
25
25
  import { AndroidPolicyInputSchema, saveAndroidPolicy } from '../services/android/androidPolicy.js';
26
26
  import { INPUT_LIMIT_PROFILES } from '../config/limits.js';
27
27
  import { ProjectRegistry } from '../projects/registry.js';
28
+ import { ProjectAdmin } from '../projects/ownerAdmin.js';
29
+ import { LEVEL_SUMMARY, parseProjectAccessLevel } from '../security/projectAccess.js';
28
30
  import { runCliMenu } from './menu.js';
29
31
  import { TunnelRuntime } from '../tunnel/runtime.js';
30
32
  import { INSTALLATION_AUTHORITY_ID } from '../auth/constants.js';
@@ -85,11 +87,16 @@ function withProjectRegistry(fn) {
85
87
  db.close();
86
88
  }
87
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
+ }
88
94
  function projectLines(project) {
89
95
  return [
90
96
  `${project.displayName} ${project.projectId}`,
91
97
  ` Path: ${project.root}`,
92
98
  ` Workspace: ${project.workspaceId}`,
99
+ ` Access: ${project.accessLevel} — ${LEVEL_SUMMARY[project.accessLevel]}`,
93
100
  ` Status: ${project.availability} — ${project.statusText}`,
94
101
  ` Updated: ${new Date(project.updatedAt).toISOString()}`,
95
102
  ...(project.removedAt === null ? [] : [` Removed: ${new Date(project.removedAt).toISOString()}`]),
@@ -146,20 +153,22 @@ async function launchHttp(opts) {
146
153
  const connectionMode = config.tunnel.connectionMode;
147
154
  const publicUrl = config.publicUrl;
148
155
  const selectedPort = opts.port ?? config.port;
149
- if (connectionMode === 'tunnel') {
156
+ if (connectionMode !== 'local') {
150
157
  if (!publicUrl)
151
- 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');
152
159
  validatePublicUrl(publicUrl, false);
160
+ }
161
+ if (connectionMode === 'tunnel') {
153
162
  if (!config.tunnel.credentialRef)
154
163
  fail('Tunnel mode requires a saved credential; run dodo tunnel configure --tunnel --os-credential --public-url https://your-host');
155
164
  }
156
- else {
165
+ else if (connectionMode === 'local') {
157
166
  if (opts.web)
158
- 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');
159
168
  if (selectedPort === 0)
160
169
  fail('local connection mode requires a concrete MCP port');
161
170
  }
162
- const endpointOrigin = connectionMode === 'tunnel' ? publicUrl : `http://127.0.0.1:${selectedPort}`;
171
+ const endpointOrigin = connectionMode === 'local' ? `http://127.0.0.1:${selectedPort}` : publicUrl;
163
172
  const startOpts = {
164
173
  configPort: config.configPort,
165
174
  ...(opts.bypass ? { runMode: 'bypass' } : opts.allow ? { runMode: 'allow-all' } : {}),
@@ -204,8 +213,13 @@ async function launchHttp(opts) {
204
213
  throw error;
205
214
  }
206
215
  }
207
- else if (!opts.quiet) {
208
- 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`);
209
223
  }
210
224
  async function stop(signal) {
211
225
  if (shutdownPromise)
@@ -288,16 +302,40 @@ program.command('setup')
288
302
  // trust/client access; those remain keyed by the referenced workspace ID.
289
303
  const project = program.command('project').description('manage the local owner-only project registry');
290
304
  project.command('add <path>')
291
- .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')
292
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)')
293
308
  .option('--json', 'machine-readable result', false)
294
309
  .action((projectPath, opts) => {
295
310
  try {
296
- 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 } : {}) }));
297
313
  printJsonOrLines(opts.json, result, () => [
298
314
  result.relocated ? 'Updated the location of an existing project identity.' : result.changed ? 'Project registered.' : 'Project was already registered; nothing changed.',
299
315
  ...projectLines(result.project),
300
- '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.',
301
339
  ]);
302
340
  }
303
341
  catch (error) {
@@ -307,7 +345,7 @@ project.command('add <path>')
307
345
  }
308
346
  });
309
347
  project.command('list')
310
- .description('list registered projects and current path readiness')
348
+ .description('list registered projects, their access level and current path readiness')
311
349
  .option('--all', 'include reviewed removals', false)
312
350
  .option('--json', 'machine-readable output', false)
313
351
  .action((opts) => {
@@ -366,12 +404,14 @@ project.command('remove <projectId>')
366
404
  // --------------------------------------------------------------- tunnel ----
367
405
  const tunnel = program.command('tunnel').description('manage or inspect a Cloudflare remotely-managed Tunnel without managing DNS or the Cloudflare account');
368
406
  tunnel.command('configure')
369
- .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')
370
408
  .option('--tunnel', 'select the DODO-owned Tunnel for every dodo start', false)
371
- .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)
372
412
  .option('--managed', 'deprecated alias for --tunnel', false)
373
- .option('--external', 'deprecated alias for --local', false)
374
- .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')
375
415
  .option('--os-credential', 'prompt through the reviewed OS credential provider', false)
376
416
  .option('--token-env <name>', 'reference an uppercase environment variable that already contains the token')
377
417
  .option('--token-file <absolute-path>', 'reference an owner-private absolute token file')
@@ -384,9 +424,10 @@ tunnel.command('configure')
384
424
  .action(async (opts) => {
385
425
  try {
386
426
  const chooseTunnel = opts.tunnel || opts.managed;
387
- const chooseLocal = opts.local || opts.external;
388
- if (chooseTunnel && chooseLocal)
389
- 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');
390
431
  const methods = [opts.osCredential, opts.tokenEnv !== undefined, opts.tokenFile !== undefined].filter(Boolean).length;
391
432
  if (methods > 1)
392
433
  fail('choose only one of --os-credential, --token-env or --token-file');
@@ -397,16 +438,18 @@ tunnel.command('configure')
397
438
  const { dir } = resolveConfigDir(process.env);
398
439
  ensureConfigDir(dir);
399
440
  const paths = statePaths(dir), current = loadGlobalConfig(paths.configFile);
400
- 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;
401
444
  const publicUrl = opts.publicUrl !== undefined
402
445
  ? validatePublicUrl(opts.publicUrl, false).origin
403
446
  : current.publicUrl;
404
- if (connectionMode === 'tunnel' && !publicUrl)
405
- fail('Tunnel mode requires --public-url https://your-host (or a previously saved public origin)');
406
- 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)
407
450
  validatePublicUrl(publicUrl, false);
408
451
  if (connectionMode === 'local' && opts.publicUrl !== undefined)
409
- fail('--public-url is used only with --tunnel');
452
+ fail('--public-url is used only with --cloudflare-local or --tunnel');
410
453
  const credentials = await import('../tunnel/credentials.js');
411
454
  const { resolveCloudflared } = await import('../tunnel/supervisor.js');
412
455
  const previousCredentialRef = current.tunnel.credentialRef;
@@ -446,14 +489,33 @@ tunnel.command('configure')
446
489
  saveGlobalConfig(paths.configFile, next);
447
490
  if (opts.removeCredential && previousCredentialRef)
448
491
  credentials.deleteOsTunnelCredential(previousCredentialRef);
449
- 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
+ };
450
507
  if (opts.json)
451
508
  console.log(JSON.stringify(result, null, 2));
452
509
  else {
453
510
  console.log(`Connection mode: ${result.connectionMode}`);
454
- console.log(`Credential: ${result.credential} (value is not stored in config or displayed)`);
455
- console.log(`Readiness: 127.0.0.1:${result.metricsPort} Restarts: ${result.maxRestarts}`);
456
- 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.');
457
519
  }
458
520
  }
459
521
  catch (error) {
@@ -1498,9 +1560,11 @@ Android (ADB host authorization and exact-device permission):
1498
1560
  dodo android disable revoke DODO Android access
1499
1561
  Remote setup:
1500
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
1501
1565
  dodo tunnel configure --tunnel --os-credential --public-url https://...
1502
1566
  select the persistent DODO Tunnel
1503
- dodo tunnel configure --local select local-only MCP
1567
+ dodo tunnel configure --loopback select loopback-only MCP
1504
1568
  dodo start use exactly the saved connection mode
1505
1569
  dodo --web start/renew Remote Config through that tunnel for 1 hour
1506
1570
  dodo web --close close Remote Config; keep MCP/Tunnel running`);