dodo-mcp 1.0.2 → 1.0.4

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 (47) hide show
  1. package/README.md +37 -17
  2. package/dist/cli/main.js +68 -68
  3. package/dist/cli/main.js.map +1 -1
  4. package/dist/cli/menu.js +25 -20
  5. package/dist/cli/menu.js.map +1 -1
  6. package/dist/config/globalConfig.js +23 -1
  7. package/dist/config/globalConfig.js.map +1 -1
  8. package/dist/config/stateImport.js +2 -2
  9. package/dist/config/stateImport.js.map +1 -1
  10. package/dist/config/tunnelConfig.js +2 -3
  11. package/dist/config/tunnelConfig.js.map +1 -1
  12. package/dist/platform/privateFs.js +3 -1
  13. package/dist/platform/privateFs.js.map +1 -1
  14. package/dist/server/appServer.js +38 -26
  15. package/dist/server/appServer.js.map +1 -1
  16. package/dist/server/configUi/app.js +35 -58
  17. package/dist/server/configUi/index.html +21 -22
  18. package/dist/server/localConfig.js +50 -63
  19. package/dist/server/localConfig.js.map +1 -1
  20. package/dist/tunnel/control.js +4 -6
  21. package/dist/tunnel/control.js.map +1 -1
  22. package/dist/tunnel/credentials.js +5 -19
  23. package/dist/tunnel/credentials.js.map +1 -1
  24. package/dist/tunnel/runtime.js +3 -4
  25. package/dist/tunnel/runtime.js.map +1 -1
  26. package/dist/tunnel/supervisor.js +8 -10
  27. package/dist/tunnel/supervisor.js.map +1 -1
  28. package/docs/AI_PROVIDERS.md +1 -1
  29. package/docs/ARCHITECTURE.md +14 -14
  30. package/docs/COMPATIBILITY.md +10 -2
  31. package/docs/MANUAL_ACCEPTANCE.md +40 -16
  32. package/docs/RELEASE_1.0.0.md +3 -0
  33. package/docs/RELEASE_1.0.3.md +56 -0
  34. package/docs/RELEASE_1.0.4.md +34 -0
  35. package/docs/RELEASE_NOTES.md +32 -0
  36. package/docs/SECURITY.md +26 -10
  37. package/docs/TEST_REPORT.md +51 -1
  38. package/docs/TUNNEL.md +102 -80
  39. package/docs/WINDOWS.md +4 -1
  40. package/docs/WINDOWS_SETUP.md +182 -0
  41. package/docs/adr/031-owner-selected-tunnel-supervision.md +8 -2
  42. package/docs/adr/044-global-launcher-and-cli-menu.md +3 -3
  43. package/docs/adr/047-temporary-remote-config.md +4 -4
  44. package/docs/adr/048-persistent-connection-mode.md +58 -0
  45. package/docs/adr/README.md +1 -0
  46. package/package.json +4 -1
  47. package/schemas/global-config.schema.json +6 -12
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DODO MCP
2
2
 
3
- **DODO MCP 1.0.2** คือ MCP server แบบ local-first สำหรับให้ AI ช่วยพัฒนา software โดยทำงานกับ workspace ที่เจ้าของเลือก ค่าเริ่มต้นเป็นโหมดส่วนตัวแบบเพิ่มโปรเจกต์แล้วใช้ได้ทันที และยังมีโหมด managed สำหรับแยก workspace ACL/trust แบบละเอียด
3
+ **DODO MCP 1.0.4** คือ MCP server แบบ local-first สำหรับให้ AI ช่วยพัฒนา software โดยทำงานกับ workspace ที่เจ้าของเลือก ค่าเริ่มต้นเป็นโหมดส่วนตัวแบบเพิ่มโปรเจกต์แล้วใช้ได้ทันที และยังมีโหมด managed สำหรับแยก workspace ACL/trust แบบละเอียด
4
4
 
5
5
  ## จุดเด่น
6
6
 
@@ -57,6 +57,12 @@ expected hash, workspace context และ OAuth scopes หากต้องแ
57
57
 
58
58
  `dodo setup --check` และ `dodo setup --plan` เป็น read-only หากแผนมี dependency ที่ต้องติดตั้ง ให้ตรวจรายการก่อนแล้วจึงรัน `dodo setup --yes --components <list>` ระบบจะไม่เริ่ม installer หากไม่มี `--yes` และ `--yes` ไม่ข้าม sudo, OS permission หรือ owner consent
59
59
 
60
+ บน Windows ให้รัน DODO ด้วย account เดียวกับที่จะใช้งานประจำและเก็บ state บน local
61
+ NTFS หากพบ `private Windows state ACL could not be established or verified` ห้ามลด
62
+ ACL หรือลบ state เดิม ดูวิธีใช้ `DODO_CONFIG_DIR` เพื่อเริ่ม installation ใหม่อย่าง
63
+ ปลอดภัยและรายการ component ที่ต้องติดตั้งเองใน [Windows Setup](docs/WINDOWS_SETUP.md)
64
+ โดยเฉพาะ `cloudflared` ซึ่ง Windows ต้องติดตั้ง signed package จาก Cloudflare ก่อน
65
+
60
66
  เมื่อ DODO ตรวจพบ config เดิมในตำแหน่งมาตรฐาน สามารถใช้ `dodo setup --import-state` เพื่อนำเข้าเฉพาะ preference ที่ปลอดภัย เช่น port, search backend, retention และ tool surface ระบบจะสร้าง installation identity ใหม่เสมอและไม่คัดลอก OAuth keys/tokens, client grants, workspace ACL, trust, approvals, schedules, public origin, web/desktop permission, executable registration หรือฐานข้อมูลเดิม ต้นฉบับจะไม่ถูกแก้ไข
61
67
 
62
68
  เปิด Local Config จาก URL ที่ `dodo` แสดงใน terminal ใช้สำหรับเพิ่มโปรเจกต์, AI
@@ -250,20 +256,34 @@ command sandbox, idempotency และ audit เดิม `agent_exec` ใช้
250
256
 
251
257
  ### เชื่อม Remote MCP ผ่าน Cloudflare Tunnel
252
258
 
253
- DODO ไม่สร้าง Tunnel, DNS หรือ Cloudflare account ให้ ผู้ใช้สร้าง remotely-managed Tunnel และตั้ง public hostname ให้ route **ทุก path** มาที่ `http://127.0.0.1:21730` ก่อน จากนั้นตั้ง public origin ครั้งเดียว:
259
+ DODO เลือกวิธีเชื่อมต่อระดับ installation ได้หนึ่งแบบ: `local` หรือ `tunnel` ค่าที่เลือก
260
+ มีผลกับทุก `dodo start` และไม่มี fallback อัตโนมัติ หากเลือก Tunnel แล้ว credential หรือ
261
+ readiness ไม่พร้อม DODO จะหยุด startup แทนการเปิด local โดยไม่แจ้ง
262
+
263
+ Local mode:
264
+
265
+ ```bash
266
+ dodo tunnel configure --local
267
+ dodo start
268
+ ```
269
+
270
+ DODO Tunnel mode:
254
271
 
255
272
  ```bash
256
273
  dodo setup --check --components cloudflared
257
- dodo init --public-url https://mcp.example.com
274
+ dodo tunnel configure \
275
+ --tunnel \
276
+ --public-url https://mcp.example.com \
277
+ --os-credential
258
278
  dodo start
259
- # Cloudflare Tunnel token (temporary; Enter = local only):
260
279
  ```
261
280
 
262
- เมื่อกรอก token DODO จะเริ่ม `cloudflared` เป็น child process ของรอบนั้นและหยุดพร้อม
263
- DODO Token อยู่ในหน่วยความจำระหว่างรอบและ environment ของ child เท่านั้น ไม่อยู่ใน
264
- argv, config, Keychain, Credential Manager, Secret Service, tunnel log, MCP response
265
- หรือ environment ของ MCP jobs กด Enter หรือใช้ `dodo start --no-tunnel` เพื่อเปิด
266
- เฉพาะ local MCP
281
+ เจ้าของสร้าง remotely-managed Tunnel, public hostname และ DNS ใน Cloudflare และ route
282
+ **ทุก path** มาที่ `http://127.0.0.1:21730` DODO ไม่จัดการ Cloudflare account/DNS
283
+ Token ถูกบันทึกใน macOS Keychain, Windows Credential Manager หรือ Linux Secret Service;
284
+ global config เก็บเพียง opaque reference ทุกครั้งที่เปิด DODO ระบบจะเริ่ม
285
+ `cloudflared` เป็น child และหยุด child พร้อม DODO Token ไม่อยู่ใน argv, config, log,
286
+ audit, browser storage, MCP response หรือ environment ของ MCP jobs
267
287
 
268
288
  หากต้องตั้งค่าจากอุปกรณ์อื่น ให้เปิดหน้าเจ้าของแบบชั่วคราวผ่าน public listener เดียวกัน:
269
289
 
@@ -277,15 +297,14 @@ dodo web --close # ปิดหน้า Remote Config แต่ MCP/Tunnel
277
297
  terminal หลังจับคู่ browser จะใช้ cookie ที่เป็น `Secure`, `HttpOnly`,
278
298
  `SameSite=Strict` และจำกัด path ที่ `/config` หน้า Remote Config ปิดอัตโนมัติภายใน
279
299
  1 ชั่วโมง การเปิดใหม่ออก code/session ใหม่และไม่ต้อง restart MCP ไม่มี token หรือ
280
- credential อยู่ใน URL หาก process เดิมเริ่มแบบ local-only `dodo --web` จะถาม Tunnel
281
- token แบบซ่อนและเปิด `cloudflared` ของ process นั้นก่อน
300
+ credential อยู่ใน URL คำสั่งนี้ใช้ได้เฉพาะเมื่อ persistent mode เป็น `tunnel` และ
301
+ DODO-owned Tunnel กำลังทำงาน; มันไม่รับ token ใหม่ผ่าน IPC และไม่เปลี่ยนจาก Local เอง
282
302
 
283
- หน้า Local Config ที่ `127.0.0.1:21731` มีช่อง **Temporary Tunnel token** สำหรับเริ่ม
284
- และหยุด tunnel ของ DODO process ปัจจุบันได้จริง ช่องจะถูกล้างหลังส่งและ backend ไม่
285
- persist ค่า ดูสถานะด้วย `dodo tunnel status`, ตรวจ connectivity ด้วย
286
- `dodo tunnel doctor` และดู log ที่ redacted ด้วย `dodo tunnel logs` คำสั่ง
287
- `dodo tunnel configure` เดิมยังมีสำหรับ advanced/headless compatibility และต้องเรียก
288
- โดยเจ้าของอย่างชัดเจน; เส้นทาง `dodo start` และหน้าเว็บไม่ใช้ credential store
303
+ หน้า Local Config ที่ `127.0.0.1:21731` เลือก Local/Tunnel, ตั้ง public origin และบันทึก
304
+ token แบบ write-only เข้า OS credential store ได้ ค่าใหม่มีผลหลัง restart หน้าเว็บแสดง
305
+ Active MCP URL จาก runtime จริงและไม่กล่าวว่า Tunnel หรือ AI client เชื่อมแล้วหากไม่มี
306
+ หลักฐาน ดูสถานะด้วย `dodo tunnel status`, ตรวจ connectivity ด้วย `dodo tunnel doctor`
307
+ และดู log ที่ redacted ด้วย `dodo tunnel logs`
289
308
 
290
309
  บน macOS `dodo setup --yes` เลือก component `cloudflared` อยู่ในชุด `all` แล้ว
291
310
  Tunnel route ต้องชี้เฉพาะ MCP/OAuth listener `21730` ห้ามชี้ Local Config `21731`
@@ -394,6 +413,7 @@ dodo desktop allow --app com.google.Chrome --mode control --persist --yes
394
413
  - [Tunnel](docs/TUNNEL.md)
395
414
  - [Web clients](docs/WEB_CLIENTS.md)
396
415
  - [Windows compatibility](docs/WINDOWS.md)
416
+ - [Windows setup และ ACL troubleshooting](docs/WINDOWS_SETUP.md)
397
417
  - [Task assistance](docs/ASSISTANCE.md)
398
418
  - [Multimodal](docs/MULTIMODAL.md)
399
419
  - [Project Registry](docs/PROJECTS.md)
package/dist/cli/main.js CHANGED
@@ -142,21 +142,23 @@ async function launchHttp(opts) {
142
142
  const configDir = resolveConfigDir(process.env).dir;
143
143
  const configFile = statePaths(configDir).configFile;
144
144
  const config = loadGlobalConfig(configFile);
145
- const publicUrl = opts.publicUrl ?? config.publicUrl;
146
- if (opts.web && !publicUrl)
147
- fail('dodo --web requires a public HTTPS origin; run dodo init --public-url https://your-host first');
148
- let temporaryTunnelToken;
149
- const interactiveTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true;
150
- if (opts.web && opts.tunnel !== false && !interactiveTerminal) {
151
- fail('dodo --web needs an interactive terminal for temporary Tunnel token entry; use dodo start --web --no-tunnel only with an already-running external tunnel');
152
- }
153
- if (opts.tunnel !== false && (config.tunnel.startWithDodo || opts.web) && publicUrl && interactiveTerminal) {
154
- const { readTemporaryTunnelToken } = await import('../tunnel/credentials.js');
155
- temporaryTunnelToken = await readTemporaryTunnelToken();
156
- if (opts.web && !temporaryTunnelToken) {
157
- fail('Remote Config was not opened because no temporary Tunnel token was entered');
158
- }
145
+ const connectionMode = config.tunnel.connectionMode;
146
+ const publicUrl = config.publicUrl;
147
+ const selectedPort = opts.port ?? config.port;
148
+ if (connectionMode === 'tunnel') {
149
+ if (!publicUrl)
150
+ fail('Tunnel mode requires a public HTTPS origin; run dodo tunnel configure --tunnel --os-credential --public-url https://your-host');
151
+ validatePublicUrl(publicUrl, false);
152
+ if (!config.tunnel.credentialRef)
153
+ fail('Tunnel mode requires a saved credential; run dodo tunnel configure --tunnel --os-credential --public-url https://your-host');
154
+ }
155
+ else {
156
+ if (opts.web)
157
+ fail('Remote Config requires Tunnel mode; configure Tunnel, restart DODO, then run dodo --web');
158
+ if (selectedPort === 0)
159
+ fail('local connection mode requires a concrete MCP port');
159
160
  }
161
+ const endpointOrigin = connectionMode === 'tunnel' ? publicUrl : `http://127.0.0.1:${selectedPort}`;
160
162
  const startOpts = {
161
163
  configPort: config.configPort,
162
164
  ...(opts.bypass ? { runMode: 'bypass' } : opts.allow ? { runMode: 'allow-all' } : {}),
@@ -165,12 +167,11 @@ async function launchHttp(opts) {
165
167
  allowUnsafeRoot: opts.allowUnsafeRoot ?? false,
166
168
  quiet: opts.quiet ?? false,
167
169
  onLog: (line) => console.log(formatTerminalLine(line)),
168
- ...(opts.web ? { remoteConfig: true } : {}),
170
+ connectionMode,
171
+ publicUrlOverride: endpointOrigin,
169
172
  };
170
173
  if (opts.port !== undefined && !Number.isNaN(opts.port))
171
174
  startOpts.portOverride = opts.port;
172
- if (opts.publicUrl !== undefined)
173
- startOpts.publicUrlOverride = opts.publicUrl;
174
175
  if (opts.tools !== undefined)
175
176
  startOpts.toolSurface = opts.tools;
176
177
  const tunnelRuntime = new TunnelRuntime(configDir, line => console.log(`[dodo:tunnel] ${line}`));
@@ -184,32 +185,26 @@ async function launchHttp(opts) {
184
185
  void stop('server stop');
185
186
  },
186
187
  });
187
- if (temporaryTunnelToken) {
188
+ if (connectionMode === 'tunnel') {
188
189
  try {
189
- const transientConfig = GlobalConfigSchema.parse({
190
+ const selectedConfig = GlobalConfigSchema.parse({
190
191
  ...config,
191
192
  ...(publicUrl ? { publicUrl } : {}),
192
- tunnel: { ...config.tunnel, mode: 'managed' },
193
+ tunnel: { ...config.tunnel, connectionMode: 'tunnel' },
193
194
  });
194
- const status = await tunnelRuntime.start(transientConfig, temporaryTunnelToken);
195
- console.log(`[dodo] Cloudflare Tunnel attached for this run: ${status.publicOrigin}/mcp`);
195
+ const status = await tunnelRuntime.start(selectedConfig);
196
+ console.log(`[dodo] DODO Tunnel started: ${status.publicOrigin}/mcp`);
197
+ if (opts.web)
198
+ printRemoteConfig(await ipcForInstallation('remoteConfig.open'));
196
199
  }
197
200
  catch (error) {
198
201
  suppressOnStopped = true;
199
202
  await server.close();
200
203
  throw error;
201
204
  }
202
- finally {
203
- temporaryTunnelToken = undefined;
204
- }
205
205
  }
206
- else if (!opts.quiet && opts.tunnel !== false && config.tunnel.startWithDodo) {
207
- if (!publicUrl)
208
- console.log('[dodo] Tunnel skipped: configure the public HTTPS origin, then restart DODO.');
209
- else if (!interactiveTerminal)
210
- console.log('[dodo] Tunnel skipped: temporary token entry requires an interactive terminal.');
211
- else
212
- console.log('[dodo] Tunnel skipped for this run; local MCP remains available.');
206
+ else if (!opts.quiet) {
207
+ console.log(`[dodo] Local connection selected: ${endpointOrigin}/mcp`);
213
208
  }
214
209
  async function stop(signal) {
215
210
  if (shutdownPromise)
@@ -278,9 +273,12 @@ program.command('setup')
278
273
  }
279
274
  catch (error) {
280
275
  if (opts.json)
281
- console.log(JSON.stringify({ error: error instanceof DodoError ? error.code : 'SETUP_FAILED', message: error.message }));
282
- else
276
+ console.log(JSON.stringify({ error: error instanceof DodoError ? error.code : 'SETUP_FAILED', message: error.message, ...(error instanceof DodoError && error.recovery ? { recovery: error.recovery } : {}) }));
277
+ else {
283
278
  console.error(`dodo setup: ${error.message}`);
279
+ if (error instanceof DodoError && error.recovery)
280
+ console.error(`Recovery: ${error.recovery}`);
281
+ }
284
282
  process.exitCode = 1;
285
283
  }
286
284
  });
@@ -367,9 +365,12 @@ project.command('remove <projectId>')
367
365
  // --------------------------------------------------------------- tunnel ----
368
366
  const tunnel = program.command('tunnel').description('manage or inspect a Cloudflare remotely-managed Tunnel without managing DNS or the Cloudflare account');
369
367
  tunnel.command('configure')
370
- .description('save local tunnel mode and a credential reference; tokens are never written to config')
371
- .option('--managed', 'DODO supervises cloudflared in the foreground', false)
372
- .option('--external', 'an owner/system service manages the tunnel', false)
368
+ .description('select persistent local or DODO-owned Tunnel mode; tokens are never written to config')
369
+ .option('--tunnel', 'select the DODO-owned Tunnel for every dodo start', false)
370
+ .option('--local', 'select local-only MCP for every dodo start', false)
371
+ .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')
373
374
  .option('--os-credential', 'prompt through the reviewed OS credential provider', false)
374
375
  .option('--token-env <name>', 'reference an uppercase environment variable that already contains the token')
375
376
  .option('--token-file <absolute-path>', 'reference an owner-private absolute token file')
@@ -381,8 +382,10 @@ tunnel.command('configure')
381
382
  .option('--json', 'machine-readable non-secret result', false)
382
383
  .action(async (opts) => {
383
384
  try {
384
- if (opts.managed && opts.external)
385
- fail('choose --managed or --external');
385
+ const chooseTunnel = opts.tunnel || opts.managed;
386
+ const chooseLocal = opts.local || opts.external;
387
+ if (chooseTunnel && chooseLocal)
388
+ fail('choose --tunnel or --local');
386
389
  const methods = [opts.osCredential, opts.tokenEnv !== undefined, opts.tokenFile !== undefined].filter(Boolean).length;
387
390
  if (methods > 1)
388
391
  fail('choose only one of --os-credential, --token-env or --token-file');
@@ -393,6 +396,16 @@ tunnel.command('configure')
393
396
  const { dir } = resolveConfigDir(process.env);
394
397
  ensureConfigDir(dir);
395
398
  const paths = statePaths(dir), current = loadGlobalConfig(paths.configFile);
399
+ const connectionMode = chooseTunnel ? 'tunnel' : chooseLocal ? 'local' : methods > 0 ? 'tunnel' : current.tunnel.connectionMode;
400
+ const publicUrl = opts.publicUrl !== undefined
401
+ ? validatePublicUrl(opts.publicUrl, false).origin
402
+ : 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)
406
+ validatePublicUrl(publicUrl, false);
407
+ if (connectionMode === 'local' && opts.publicUrl !== undefined)
408
+ fail('--public-url is used only with --tunnel');
396
409
  const credentials = await import('../tunnel/credentials.js');
397
410
  const { resolveCloudflared } = await import('../tunnel/supervisor.js');
398
411
  const previousCredentialRef = current.tunnel.credentialRef;
@@ -401,7 +414,7 @@ tunnel.command('configure')
401
414
  credentialRef = undefined;
402
415
  const baseTunnelInput = {
403
416
  ...current.tunnel,
404
- mode: opts.managed ? 'managed' : opts.external ? 'external' : current.tunnel.mode,
417
+ connectionMode,
405
418
  ...(credentialRef ? { credentialRef } : {}),
406
419
  ...(opts.metricsPort !== undefined ? { metricsPort: opts.metricsPort } : {}),
407
420
  ...(opts.maxRestarts !== undefined ? { maxRestarts: opts.maxRestarts } : {}),
@@ -409,7 +422,7 @@ tunnel.command('configure')
409
422
  };
410
423
  if (!credentialRef)
411
424
  delete baseTunnelInput['credentialRef'];
412
- let next = GlobalConfigSchema.parse({ ...current, tunnel: baseTunnelInput });
425
+ let next = GlobalConfigSchema.parse({ ...current, ...(publicUrl ? { publicUrl } : {}), tunnel: baseTunnelInput });
413
426
  if (opts.cloudflared !== undefined) {
414
427
  const canonical = resolveCloudflared(next);
415
428
  next = GlobalConfigSchema.parse({ ...next, tunnel: { ...next.tunnel, executable: canonical } });
@@ -427,19 +440,19 @@ tunnel.command('configure')
427
440
  if (!credentialRef)
428
441
  delete withCredential['credentialRef'];
429
442
  next = GlobalConfigSchema.parse({ ...next, tunnel: withCredential });
430
- if (next.tunnel.mode === 'managed' && !next.tunnel.credentialRef)
431
- fail('managed mode requires --os-credential, --token-env or --token-file');
443
+ if (next.tunnel.connectionMode === 'tunnel' && !next.tunnel.credentialRef)
444
+ fail('Tunnel mode requires --os-credential, --token-env or --token-file');
432
445
  saveGlobalConfig(paths.configFile, next);
433
446
  if (opts.removeCredential && previousCredentialRef)
434
447
  credentials.deleteOsTunnelCredential(previousCredentialRef);
435
- const result = { mode: next.tunnel.mode, 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 };
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 };
436
449
  if (opts.json)
437
450
  console.log(JSON.stringify(result, null, 2));
438
451
  else {
439
- console.log(`Tunnel mode: ${result.mode}`);
452
+ console.log(`Connection mode: ${result.connectionMode}`);
440
453
  console.log(`Credential: ${result.credential} (value is not stored in config or displayed)`);
441
454
  console.log(`Readiness: 127.0.0.1:${result.metricsPort} Restarts: ${result.maxRestarts}`);
442
- console.log(result.mode === 'managed' ? 'Configuration saved. Start explicitly with: dodo tunnel start --yes' : 'External mode saved. DODO will observe health but will not start cloudflared.');
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.');
443
456
  }
444
457
  }
445
458
  catch (error) {
@@ -570,7 +583,7 @@ program.command('cli')
570
583
  startupProject,
571
584
  selectProject: selectStartupProject,
572
585
  addProject: addAndSelectProject,
573
- start: async (root, tunnel = true) => launchHttp({ ...(root === undefined ? {} : { root }), tunnel }),
586
+ start: async (root) => launchHttp({ ...(root === undefined ? {} : { root }) }),
574
587
  openRemoteConfig: openRemoteConfigFromCli,
575
588
  setupAll: () => setupFromMenu(false),
576
589
  checkSetup: () => setupFromMenu(true),
@@ -590,20 +603,7 @@ function printRemoteConfig(result) {
590
603
  }
591
604
  async function openRemoteConfigFromCli() {
592
605
  try {
593
- const status = await ipcForInstallation('remoteConfig.status');
594
- let tunnelToken;
595
- if (status.tunnel?.available === true && status.tunnel.running !== true) {
596
- const { readTemporaryTunnelToken } = await import('../tunnel/credentials.js');
597
- tunnelToken = await readTemporaryTunnelToken('Cloudflare Tunnel token (temporary; required for Remote Config): ');
598
- if (!tunnelToken)
599
- fail('Remote Config needs a temporary Tunnel token; use dodo start --no-tunnel for local-only MCP');
600
- }
601
- try {
602
- printRemoteConfig(await ipcForInstallation('remoteConfig.open', tunnelToken ? { tunnelToken } : {}));
603
- }
604
- finally {
605
- tunnelToken = undefined;
606
- }
606
+ printRemoteConfig(await ipcForInstallation('remoteConfig.open'));
607
607
  }
608
608
  catch (error) {
609
609
  if (!(error instanceof IpcError) || !error.message.includes('no running DODO installation'))
@@ -641,13 +641,11 @@ program
641
641
  .description('start the MCP server for the saved project, or wait for a project selection (foreground)')
642
642
  .option('--root <path>', 'select and remember an explicit workspace root (local CLI only)')
643
643
  .option('--port <n>', 'listen port (default from config, initially 21730)', (v) => Number.parseInt(v, 10))
644
- .option('--public-url <url>', 'public origin override for this run (local CLI only)')
645
644
  .option('--allow-unsafe-root', 'permit filesystem root / home / other unusually broad workspace roots', false)
646
645
  .option('--allow', 'allow task execution for this run (requires --all)', false)
647
646
  .option('--all', 'all local action classes (requires --allow)', false)
648
647
  .option('--bypass', 'trusted actions and no default job sandbox for this run; OAuth and file guards remain', false)
649
648
  .option('--tools <surface>', 'tool exposure for this run: compact (HTTP default), full, or hybrid (49 tools: gateways + common direct tools); permissions are unchanged')
650
- .option('--no-tunnel', 'start local MCP only; do not ask for a temporary Cloudflare Tunnel token')
651
649
  .option('--web', 'open Remote Config through the public tunnel for at most one hour', false)
652
650
  .option('--quiet', 'suppress the startup banner', false)
653
651
  .action(async (opts) => {
@@ -771,8 +769,8 @@ program
771
769
  console.log('');
772
770
  console.log('Next steps:');
773
771
  console.log(` 1. Point your own tunnel at 127.0.0.1:${next.port} for host ${url.hostname} (see docs/TUNNEL.md — route ALL paths, not just /mcp)`);
774
- console.log(' 2. Register your client callback: dodo auth add-client --redirect-uri <exact callback URL from your client UI>');
775
- console.log(' 3. cd into a project, set trust: dodo trust --mode edit');
772
+ console.log(` 2. Select it persistently: dodo tunnel configure --tunnel --public-url ${url.origin} --os-credential`);
773
+ console.log(' 3. Register your client callback: dodo auth add-client --redirect-uri <exact callback URL from your client UI>');
776
774
  console.log(' 4. dodo start');
777
775
  }
778
776
  catch (err) {
@@ -1440,10 +1438,12 @@ Desktop (platform helper and explicit app permission required):
1440
1438
  dodo desktop disable revoke desktop access
1441
1439
  Remote setup:
1442
1440
  dodo init --public-url https://... configure public origin once
1443
- dodo start prompt for a temporary Tunnel token
1441
+ dodo tunnel configure --tunnel --os-credential --public-url https://...
1442
+ select the persistent DODO Tunnel
1443
+ dodo tunnel configure --local select local-only MCP
1444
+ dodo start use exactly the saved connection mode
1444
1445
  dodo --web start/renew Remote Config through that tunnel for 1 hour
1445
- dodo web --close close Remote Config; keep MCP/Tunnel running
1446
- dodo start --no-tunnel run local MCP only`);
1446
+ dodo web --close close Remote Config; keep MCP/Tunnel running`);
1447
1447
  function effectiveArgv() {
1448
1448
  if (process.argv.length <= 2)
1449
1449
  return [...process.argv, 'start'];