create-walle 0.9.19 → 0.9.21

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 (31) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/db.js +131 -0
  4. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +58 -50
  5. package/template/claude-task-manager/docs/phone-access-design.md +23 -7
  6. package/template/claude-task-manager/docs/walle-session-model-preferences.md +119 -0
  7. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +32 -48
  8. package/template/claude-task-manager/lib/remote-relay-protocol.js +5 -0
  9. package/template/claude-task-manager/lib/walle-external-actions.js +20 -3
  10. package/template/claude-task-manager/public/index.html +25 -0
  11. package/template/claude-task-manager/public/js/setup.js +16 -12
  12. package/template/claude-task-manager/public/js/walle-session.js +31 -3
  13. package/template/claude-task-manager/public/js/walle.js +93 -23
  14. package/template/claude-task-manager/public/m/app.css +417 -21
  15. package/template/claude-task-manager/public/m/app.js +831 -44
  16. package/template/claude-task-manager/public/m/claim.html +1 -1
  17. package/template/claude-task-manager/public/m/index.html +41 -7
  18. package/template/claude-task-manager/public/m/sw.js +1 -1
  19. package/template/claude-task-manager/server.js +377 -30
  20. package/template/claude-task-manager/workers/state-detectors/codex.js +18 -3
  21. package/template/package.json +1 -1
  22. package/template/wall-e/chat.js +32 -2
  23. package/template/wall-e/coding/stream-processor.js +36 -0
  24. package/template/wall-e/coding-orchestrator.js +45 -0
  25. package/template/wall-e/deploy.sh +1 -1
  26. package/template/wall-e/docs/external-action-controller.md +60 -2
  27. package/template/wall-e/external-action-controller.js +23 -1
  28. package/template/wall-e/external-action-gateway.js +163 -0
  29. package/template/wall-e/fly.toml +1 -0
  30. package/template/wall-e/tools/local-tools.js +122 -4
  31. package/template/website/index.html +2 -2
package/README.md CHANGED
@@ -12,7 +12,7 @@ A web dashboard for running and managing AI coding sessions across multiple prov
12
12
  - **Prompt Editor** — Save, version, and organize prompts with folders, tags, chains, templates, and AI search
13
13
  - **Task Queue** — Queue prompts for sequential execution with auto-advance when the agent finishes, or step through manually
14
14
  - **Approval Workflows** — Auto-approve tool-use requests based on learned rules; uncertain cases escalate to you
15
- - **Remote Phone Access** — Pair your phone with a QR code and use a mobile CTM UI over Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote
15
+ - **Remote Phone Access** — Pair your phone with a QR code and use a mobile CTM UI over Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote, with live prompts and model controls
16
16
  - **Code & Doc Review** — Review git diffs and Markdown docs side by side, add anchored comments, and send feedback into an agent session or queue
17
17
  - **Model Registry** — Manage providers (Anthropic, OpenAI, Google, DeepSeek, Ollama, LM Studio, MLX, and CLI subscription providers), compare pricing, switch models per session
18
18
  - **Session Insights** — Analyze patterns across sessions to optimize prompts and workflows
@@ -62,7 +62,7 @@ On first launch, the browser setup page guides you through:
62
62
  1. **Owner name** — auto-detected from `git config`
63
63
  2. **API key** — enter manually, or click "Auto-detect" to find it from your shell environment, Claude Code OAuth, or corporate devbox
64
64
  3. **Integrations** — connect Slack (OAuth), email and calendar auto-detected on macOS
65
- 4. **Remote phone access** — optional QR pairing with Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote
65
+ 4. **Remote phone access** — optional QR pairing with Microsoft Dev Tunnels, Tailscale, Cloudflare Tunnel, or Walle Remote, including phone-friendly prompts and model controls
66
66
 
67
67
  ## Custom Port
68
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-walle",
3
- "version": "0.9.19",
3
+ "version": "0.9.21",
4
4
  "description": "CTM + Wall-E \u2014 AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini, Aider, OpenCode, and more, plus prompt editor, task queue, remote phone access, code/doc review, and an agent that learns from Slack, email & calendar.",
5
5
  "bin": {
6
6
  "create-walle": "bin/create-walle.js"
@@ -1459,6 +1459,47 @@ function migrateSchemaIfNeeded() {
1459
1459
  // column presence before writing title-generation status.
1460
1460
  }
1461
1461
  }
1462
+ if (getSchemaVersion() < 6) {
1463
+ try {
1464
+ migrateToV6();
1465
+ } catch (e) {
1466
+ console.error('[db] Schema migration to v6 FAILED:', e.message);
1467
+ console.error('[db] Stack:', e.stack);
1468
+ // Non-fatal: Wall-E can still fall back to startup_tasks/model defaults,
1469
+ // but per-session model preferences will not persist until this succeeds.
1470
+ }
1471
+ }
1472
+ }
1473
+
1474
+ /**
1475
+ * Schema v6: Persist session-scoped model preferences.
1476
+ *
1477
+ * startup_tasks.model_id is a restore hint and intentionally has no provider
1478
+ * identity. Wall-E session model changes need a CTM-owned durable preference so
1479
+ * one tab's model switch cannot mutate Wall-E global defaults or another tab.
1480
+ */
1481
+ function migrateToV6() {
1482
+ const d = getDb();
1483
+ d.exec(`
1484
+ CREATE TABLE IF NOT EXISTS session_model_preferences (
1485
+ ctm_session_id TEXT PRIMARY KEY,
1486
+ agent_type TEXT NOT NULL DEFAULT 'walle',
1487
+ provider_type TEXT NOT NULL DEFAULT '',
1488
+ provider_id TEXT NOT NULL DEFAULT '',
1489
+ model_id TEXT NOT NULL DEFAULT '',
1490
+ registry_id TEXT NOT NULL DEFAULT '',
1491
+ scope TEXT NOT NULL DEFAULT 'session',
1492
+ source TEXT NOT NULL DEFAULT 'user',
1493
+ pinned INTEGER NOT NULL DEFAULT 1,
1494
+ created_at TEXT DEFAULT (datetime('now')),
1495
+ updated_at TEXT DEFAULT (datetime('now')),
1496
+ FOREIGN KEY (ctm_session_id) REFERENCES ctm_sessions(id) ON DELETE CASCADE
1497
+ );
1498
+ CREATE INDEX IF NOT EXISTS idx_session_model_preferences_agent
1499
+ ON session_model_preferences(agent_type, updated_at DESC);
1500
+ `);
1501
+ setSchemaVersion(6);
1502
+ console.log('[db] Schema migrated to v6 (session model preferences)');
1462
1503
  }
1463
1504
 
1464
1505
  /**
@@ -3268,6 +3309,95 @@ function getInsightsData(includeInternal) {
3268
3309
  };
3269
3310
  }
3270
3311
 
3312
+ // --- Session Model Preferences ---
3313
+ function _cleanSessionModelPreferenceInput(input = {}) {
3314
+ const clean = (value, max) => String(value || '').trim().slice(0, max);
3315
+ return {
3316
+ ctmSessionId: clean(input.ctmSessionId || input.ctm_session_id || input.sessionId || input.id, 160),
3317
+ agentType: clean(input.agentType || input.agent_type || 'walle', 60) || 'walle',
3318
+ providerType: clean(input.providerType || input.provider_type || input.model_provider || input.provider, 120),
3319
+ providerId: clean(input.providerId || input.provider_id, 160),
3320
+ modelId: clean(input.modelId || input.model_id || input.model, 256),
3321
+ registryId: clean(input.registryId || input.registry_id || input.model_registry_id, 256),
3322
+ scope: clean(input.scope || 'session', 40) || 'session',
3323
+ source: clean(input.source || 'user', 80) || 'user',
3324
+ pinned: input.pinned === false || input.pinned === 0 ? 0 : 1,
3325
+ };
3326
+ }
3327
+
3328
+ function upsertSessionModelPreference(input = {}) {
3329
+ const item = _cleanSessionModelPreferenceInput(input);
3330
+ if (!item.ctmSessionId) throw new Error('ctmSessionId is required');
3331
+ if (!item.modelId) {
3332
+ clearSessionModelPreference(item.ctmSessionId);
3333
+ return null;
3334
+ }
3335
+ const d = getDb();
3336
+ d.prepare(
3337
+ "INSERT OR IGNORE INTO ctm_sessions (id, provider, updated_at) VALUES (?, ?, datetime('now'))"
3338
+ ).run(item.ctmSessionId, item.agentType || 'walle');
3339
+ d.prepare(`
3340
+ INSERT INTO session_model_preferences (
3341
+ ctm_session_id, agent_type, provider_type, provider_id, model_id,
3342
+ registry_id, scope, source, pinned, updated_at
3343
+ )
3344
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
3345
+ ON CONFLICT(ctm_session_id) DO UPDATE SET
3346
+ agent_type = excluded.agent_type,
3347
+ provider_type = excluded.provider_type,
3348
+ provider_id = excluded.provider_id,
3349
+ model_id = excluded.model_id,
3350
+ registry_id = excluded.registry_id,
3351
+ scope = excluded.scope,
3352
+ source = excluded.source,
3353
+ pinned = excluded.pinned,
3354
+ updated_at = excluded.updated_at
3355
+ `).run(
3356
+ item.ctmSessionId, item.agentType, item.providerType, item.providerId,
3357
+ item.modelId, item.registryId, item.scope, item.source, item.pinned
3358
+ );
3359
+ flushWal();
3360
+ return getSessionModelPreference(item.ctmSessionId);
3361
+ }
3362
+
3363
+ function getSessionModelPreference(ctmSessionId) {
3364
+ const id = String(ctmSessionId || '').trim();
3365
+ if (!id) return null;
3366
+ const row = getDb().prepare(
3367
+ 'SELECT * FROM session_model_preferences WHERE ctm_session_id = ?'
3368
+ ).get(id);
3369
+ if (!row) return null;
3370
+ return {
3371
+ ctm_session_id: row.ctm_session_id,
3372
+ ctmSessionId: row.ctm_session_id,
3373
+ agent_type: row.agent_type || 'walle',
3374
+ agentType: row.agent_type || 'walle',
3375
+ provider_type: row.provider_type || '',
3376
+ providerType: row.provider_type || '',
3377
+ provider_id: row.provider_id || '',
3378
+ providerId: row.provider_id || '',
3379
+ model_id: row.model_id || '',
3380
+ modelId: row.model_id || '',
3381
+ registry_id: row.registry_id || '',
3382
+ registryId: row.registry_id || '',
3383
+ scope: row.scope || 'session',
3384
+ source: row.source || 'user',
3385
+ pinned: row.pinned !== 0,
3386
+ created_at: row.created_at || '',
3387
+ updated_at: row.updated_at || '',
3388
+ };
3389
+ }
3390
+
3391
+ function clearSessionModelPreference(ctmSessionId) {
3392
+ const id = String(ctmSessionId || '').trim();
3393
+ if (!id) return 0;
3394
+ const changes = getDb().prepare(
3395
+ 'DELETE FROM session_model_preferences WHERE ctm_session_id = ?'
3396
+ ).run(id).changes || 0;
3397
+ if (changes) flushWal();
3398
+ return changes;
3399
+ }
3400
+
3271
3401
  // --- Startup Tasks (crash-safe session restore) ---
3272
3402
  const CTM_INSTANCE_ID = `${require('os').hostname()}:${process.pid}:${Date.now()}`;
3273
3403
 
@@ -5013,6 +5143,7 @@ module.exports = {
5013
5143
  replaceInsightRecommendations, listInsightRecommendations,
5014
5144
  startAnalysisRun, completeAnalysisRun, getLastAnalysisRun,
5015
5145
  getInsightsData,
5146
+ upsertSessionModelPreference, getSessionModelPreference, clearSessionModelPreference,
5016
5147
  addStartupTask, updateStartupTaskLabel, updateStartupTaskClaudeSession, updateStartupTaskAgentSession, updateStartupTaskBranch, updateStartupTaskCwd, removeStartupTask, markStartupTaskExited, heartbeatStartupTasks, getStartupTask, listStartupTasks, clearStartupTasks,
5017
5148
  createSessionWithStartupTask,
5018
5149
  appendScrollbackBatch, cleanupScrollbackChunkSeqIntegrity, getNextScrollbackSeq, loadScrollback, loadScrollbackTail, clearScrollback,
@@ -1,7 +1,7 @@
1
1
  # Microsoft Dev Tunnel Phone Access Design
2
2
 
3
- Status: Draft for implementation review
4
- Date: 2026-05-10
3
+ Status: Implementation update
4
+ Date: 2026-05-19
5
5
  Owner: CTM / Wall-E
6
6
  Related docs:
7
7
  - `claude-task-manager/docs/phone-access-design.md`
@@ -30,11 +30,12 @@ product direction. It should sit in `Setup -> Access` as a practical fallback:
30
30
 
31
31
  ```text
32
32
  iPhone browser
33
- -> devtunnels.ms browser URL
33
+ -> private devtunnels.ms browser URL
34
+ -> Microsoft/GitHub Dev Tunnels login gate
34
35
  -> Azure Dev Tunnels service
35
36
  -> devtunnel host process on Mac
36
37
  -> local CTM on 127.0.0.1:3456
37
- -> CTM mobile device claim + passkey pairing
38
+ -> CTM mobile device claim + optional passkey step-up
38
39
  ```
39
40
 
40
41
  The design goal is to make the flow feel like "sign in on the Mac, start
@@ -67,23 +68,23 @@ It is not best when:
67
68
  - the user requires relay-blind E2E payload privacy;
68
69
  - long-running production availability is required;
69
70
  - company policy blocks Microsoft Dev Tunnels domains;
70
- - the user cannot accept a public browser tunnel that is protected at the CTM
71
- application layer.
71
+ - the user cannot sign into the same Microsoft/GitHub identity on the phone;
72
+ - company policy blocks Microsoft Dev Tunnels domains or browser sign-in.
72
73
 
73
74
  ## Design Principles
74
75
 
75
- 1. **Keep Microsoft/GitHub sign-in on the Mac.** The user signs in once to
76
- manage Dev Tunnels. The phone should not need a second Microsoft/GitHub
77
- browser login.
78
- 2. **Use a port-scoped browser access rule.** CTM enables anonymous `connect`
79
- only for the CTM tunnel port, because normal mobile Safari/Chrome navigation
80
- cannot attach Dev Tunnel access headers. CTM auth remains the security
81
- boundary.
76
+ 1. **Keep the tunnel private.** Microsoft Dev Tunnels are private by default.
77
+ CTM must not create anonymous `connect` access automatically. The phone
78
+ signs into Microsoft/GitHub before Dev Tunnels forwards anything to CTM.
79
+ 2. **Use CTM auth after the Microsoft gate.** Microsoft/GitHub login decides
80
+ whether the phone can reach the tunnel. CTM device pairing, scopes, audit,
81
+ and revocation decide what that browser is allowed to do after it reaches
82
+ CTM.
82
83
  3. **Use a stable tunnel origin.** CTM device cookies and WebAuthn passkeys are
83
84
  origin-scoped. Temporary tunnel URLs create confusing re-pairing failures.
84
85
  4. **Make Microsoft gate failures diagnosable.** If Microsoft returns 401 or an
85
- auth redirect, the phone request has not reached CTM and the access rule must
86
- be repaired.
86
+ auth redirect, the phone request has not reached CTM. In private mode that
87
+ is expected until the phone signs in with the same tunnel identity.
87
88
  5. **Separate tunnel readiness from phone pairing.** A running tunnel only
88
89
  means the network path exists. The phone still needs CTM pairing and local
89
90
  Mac approval.
@@ -114,8 +115,8 @@ It is not best when:
114
115
  - `Use device code`
115
116
  8. CTM creates or reuses a persistent CTM tunnel ID.
116
117
  9. CTM creates or verifies port `3456` with protocol `http`.
117
- 10. CTM creates or verifies an anonymous `connect` access entry scoped to port
118
- `3456`.
118
+ 10. CTM resets port access to the Dev Tunnels default so stale anonymous access
119
+ is removed.
119
120
  11. CTM starts `devtunnel host <tunnel-id>` as a managed child process.
120
121
  12. CTM parses the `https://...devtunnels.ms` URL from stdout.
121
122
  13. CTM updates the phone origin allowlist for that exact URL.
@@ -131,8 +132,9 @@ It is not best when:
131
132
  URL.
132
133
  3. Microsoft may show:
133
134
  - anti-phishing interstitial;
134
- - a tunnel auth error if the port-scoped access entry was removed or expired.
135
- 4. Microsoft forwards the browser to CTM.
135
+ - Microsoft/GitHub sign-in;
136
+ - access denied if the phone signs into the wrong account.
137
+ 4. After the private gate passes, Microsoft forwards the browser to CTM.
136
138
  5. CTM mobile claim page says:
137
139
 
138
140
  ```text
@@ -140,7 +142,8 @@ It is not best when:
140
142
  This phone still needs CTM pairing.
141
143
  ```
142
144
 
143
- 6. User registers Face ID/passkey for this CTM origin.
145
+ 6. CTM issues a device token for this phone. Passkey registration is used for
146
+ high-risk step-up, not as the primary proof that the phone can reach CTM.
144
147
  7. Mac shows:
145
148
 
146
149
  ```text
@@ -338,15 +341,16 @@ Quick fallback
338
341
  Body:
339
342
 
340
343
  ```text
341
- Use a Microsoft-hosted browser tunnel to open CTM from your phone. No DNS, VPN,
342
- or phone-side Microsoft/GitHub login.
344
+ Use a private Microsoft-hosted browser tunnel to open CTM from your phone. No
345
+ DNS or VPN; the phone signs into the same Microsoft/GitHub tunnel account.
343
346
  ```
344
347
 
345
348
  Security note:
346
349
 
347
350
  ```text
348
- This is a tunnel fallback, not Walle Remote. CTM pairing, passkey, and device
349
- permissions are still required.
351
+ This is a tunnel fallback, not Walle Remote. Keep anonymous access off. CTM
352
+ pairing, device permissions, and passkey step-up still apply after the private
353
+ Microsoft gate.
350
354
  ```
351
355
 
352
356
  Primary button by state:
@@ -437,7 +441,7 @@ This account manages the tunnel on this Mac only.
437
441
 
438
442
  Default settings:
439
443
 
440
- - Access: `Port-scoped browser access`
444
+ - Access: `Private Microsoft/GitHub account access`
441
445
  - Tunnel kind: `Persistent`
442
446
  - Port: `3456`
443
447
  - Protocol: `http`
@@ -453,26 +457,27 @@ Running state:
453
457
 
454
458
  ```text
455
459
  Tunnel running
456
- Phone access is protected by CTM pairing and passkey.
460
+ Phone access requires the same Microsoft/GitHub account plus CTM pairing.
457
461
  ```
458
462
 
459
- Do not offer broad tunnel-level public mode. CTM should create only the
460
- port-scoped anonymous `connect` rule required for browser navigation.
463
+ Do not offer broad tunnel-level public mode. CTM should reset stale anonymous
464
+ rules back to Microsoft Dev Tunnels defaults rather than creating new anonymous
465
+ rules.
461
466
 
462
467
  ### Step: Pair Phone
463
468
 
464
- Show only after the tunnel is running, the CTM port access rule is present, and
465
- CTM has an exact allowed origin.
469
+ Show only after the tunnel is running, the tunnel access is private, and CTM has
470
+ an exact allowed origin.
466
471
 
467
472
  Content:
468
473
 
469
474
  ```text
470
- Scan with iPhone. Face ID/passkey pairing protects this device.
475
+ Scan with iPhone. First sign into the tunnel account below if Microsoft asks.
471
476
 
472
477
  Use this account:
473
478
  user@example.com
474
479
 
475
- Then CTM will ask this Mac to approve the phone.
480
+ Then CTM pairs this browser and records its device permissions.
476
481
  ```
477
482
 
478
483
  QR payload:
@@ -524,7 +529,7 @@ Tunnel account: user@example.com
524
529
  Mac: User's MacBook
525
530
  Access: Read, Respond
526
531
 
527
- [Pair with Face ID]
532
+ [Pair this phone]
528
533
  ```
529
534
 
530
535
  If the user is on an unexpected origin:
@@ -560,7 +565,8 @@ not the task.
560
565
  | --- | --- | --- |
561
566
  | CLI missing | `Microsoft devtunnel CLI is not installed.` | Install, then refresh |
562
567
  | Auth missing | `Sign into Microsoft or GitHub before starting a tunnel.` | Sign in / device code |
563
- | Access rule missing | `Microsoft tunnel access is blocking the phone URL.` | Recover Now re-applies the port-scoped access rule |
568
+ | Microsoft sign-in required | `Microsoft is asking the phone to sign in before CTM can load.` | Sign in on the phone with the account shown on the Mac |
569
+ | Anonymous access found | `This tunnel has public browser access enabled.` | CTM resets the port access to private before showing Ready |
564
570
  | Tunnel start failed | `Tunnel did not start.` | Retry, copy diagnostics |
565
571
  | Temporary URL changed | `This phone link belongs to an old tunnel.` | Reuse persistent tunnel or re-pair |
566
572
  | Origin not allowed | `CTM has not trusted this tunnel URL yet.` | Refresh setup, save origin |
@@ -573,14 +579,16 @@ not the task.
573
579
 
574
580
  ### Required
575
581
 
576
- - Microsoft/GitHub account required on the Mac for tunnel management only.
577
- - The phone can open the browser URL without Microsoft/GitHub tunnel sign-in.
578
- - CTM device claim and passkey pairing still required.
582
+ - Microsoft/GitHub account required on the Mac for tunnel management.
583
+ - The phone must pass the Microsoft/GitHub private tunnel gate before CTM sees
584
+ the request.
585
+ - CTM device claim is still required so CTM can name, scope, revoke, and audit
586
+ the browser/device.
579
587
  - Local Mac approval required for first phone pairing.
580
588
  - CTM route authorization registry still applies.
581
- - High-risk CTM actions still require step-up.
582
- - Anonymous access is port-scoped to the CTM tunnel and is not a substitute for
583
- CTM device auth.
589
+ - High-risk CTM actions still require passkey step-up when the browser has a
590
+ CTM passkey registered for this origin.
591
+ - Anonymous access is disabled by default and should not be created by CTM.
584
592
  - No tunnel access tokens in QR codes.
585
593
  - No inspect URL shown in the primary UI.
586
594
 
@@ -593,10 +601,10 @@ trusted transport path.
593
601
 
594
602
  That is acceptable for a fallback tunnel if:
595
603
 
604
+ - the Microsoft/GitHub private gate remains on;
596
605
  - CTM app-layer auth remains strong;
597
- - only the CTM port has anonymous browser connect access;
598
- - the user understands that the tunnel URL is publicly reachable but not usable
599
- without CTM pairing/passkey/device auth;
606
+ - the user understands that Dev Tunnels is a Microsoft-hosted relay, not an E2E
607
+ blind relay;
600
608
  - sensitive long-running remote access moves to Walle Remote once available.
601
609
 
602
610
  ## Implementation Notes
@@ -610,7 +618,7 @@ Store:
610
618
  - port;
611
619
  - protocol;
612
620
  - signed-in account display;
613
- - port-scoped anonymous connect access status;
621
+ - private access status and whether stale anonymous access was removed;
614
622
  - expiration timestamp if available;
615
623
  - managed process PID;
616
624
  - last stdout/stderr lines for diagnostics;
@@ -639,7 +647,7 @@ the Cloudflare fallback process model:
639
647
  - user signed in;
640
648
  - persistent tunnel exists;
641
649
  - port `3456` configured;
642
- - port `3456` has an anonymous browser `connect` access entry;
650
+ - stale anonymous browser `connect` access is absent;
643
651
  - host process running;
644
652
  - CTM allowed origin includes the tunnel URL;
645
653
  - CTM device claim can be generated for that origin.
@@ -677,7 +685,7 @@ Negative cases:
677
685
  - tunnel host process crash;
678
686
  - tunnel URL changes;
679
687
  - CTM allowed origin missing;
680
- - Microsoft Dev Tunnel access entry removed or expired;
688
+ - stale anonymous access left by an older CTM build;
681
689
  - high-risk action without CTM step-up.
682
690
 
683
691
  ## MVP Cut Line
@@ -688,7 +696,7 @@ MVP includes:
688
696
  - CLI detection;
689
697
  - sign-in status and account display;
690
698
  - persistent tunnel create/reuse;
691
- - port-scoped anonymous `connect` access create/reuse;
699
+ - private access reset/check so stale anonymous browser access is removed;
692
700
  - managed `devtunnel host` process;
693
701
  - exact origin allowlist;
694
702
  - phone pairing QR;
@@ -697,7 +705,7 @@ MVP includes:
697
705
 
698
706
  MVP excludes:
699
707
 
700
- - broad tunnel-level anonymous mode;
708
+ - anonymous browser access;
701
709
  - team/tenant/org access controls;
702
710
  - tunnel access tokens for phone;
703
711
  - traffic inspection UI;
@@ -708,5 +716,5 @@ MVP excludes:
708
716
  Build Microsoft Dev Tunnel as a polished fallback because it removes most of
709
717
  the Cloudflare setup burden. Do not position it as the primary Walle Remote
710
718
  solution. The UI should be honest: it is fast and browser-only, but it is still
711
- a Microsoft-hosted public browser tunnel protected by CTM's own auth, not an
712
- E2E typed relay.
719
+ a Microsoft-hosted private browser tunnel plus CTM device auth, not an E2E
720
+ typed relay.
@@ -25,6 +25,12 @@ browser fallback that avoids VPN, DNS, and Cloudflare setup, but it is still a
25
25
  tunnel transport rather than the Walle Relay product path. See
26
26
  `claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md`.
27
27
 
28
+ 2026-05-19 private Microsoft Tunnel update: Microsoft Dev Tunnel must stay
29
+ private by default. CTM no longer creates anonymous Dev Tunnel browser access.
30
+ The phone signs into the same Microsoft/GitHub identity used by `devtunnel` on
31
+ the Mac before Microsoft forwards traffic to CTM. CTM still issues its own
32
+ device token for scopes, revocation, audit, and optional passkey step-up.
33
+
28
34
  ---
29
35
 
30
36
  ## 1. Goals, non-goals, success criteria
@@ -457,6 +463,9 @@ The QR flow must never put the raw long-lived device token in the URL.
457
463
  WebAuthn RP ID rule:
458
464
  - A passkey registered on `https://<tailnet-host>:3456` is scoped to that
459
465
  tailnet hostname. It will not automatically work on `https://ctm.example.com`.
466
+ - A passkey registered on `https://<tunnel>.devtunnels.ms` is scoped to that
467
+ Dev Tunnel hostname. It verifies a CTM-registered credential for this origin;
468
+ it does not prove which Microsoft/GitHub account passed the Dev Tunnel gate.
460
469
  - If Cloudflare fallback is enabled, the phone must enroll a second credential
461
470
  for the Cloudflare origin, tied to the same `device_token_id`.
462
471
  - Store `rp_id` and `origin` per credential and pick the verification options
@@ -489,9 +498,12 @@ explicit upgrade. The desktop loopback is always all-scope.
489
498
 
490
499
  ### 5.4 WebAuthn passkey for mutations (G3)
491
500
 
492
- Even with a valid token, **mutating** routes require a fresh WebAuthn
501
+ Even with a valid token, **high-risk** routes require a fresh WebAuthn
493
502
  assertion (a "step-up" challenge). The phone's iOS Face ID / Secure Enclave
494
- makes this near-frictionless.
503
+ makes this near-frictionless. In private Microsoft Tunnel mode, passkey is not
504
+ the primary login proof; Microsoft/GitHub gates reachability before CTM sees the
505
+ request. Passkey is an app-layer confirmation for sensitive CTM actions and a
506
+ backup if a CTM device token/cookie is copied out of the browser.
495
507
 
496
508
  **Library**: `@simplewebauthn/server` + `@simplewebauthn/browser` (mature,
497
509
  Node-native, used by Auth.js).
@@ -506,15 +518,17 @@ Node-native, used by Auth.js).
506
518
  6. Mutating routes require both `ctm_token` AND `ctm_step_up`.
507
519
 
508
520
  **Operations that require step-up**
509
- - Every remote WS `input`.
510
- - Every remote approval / denial / permission response.
511
- - Every `walle-message` that can invoke tools or skills.
521
+ - Remote approval / denial / permission response.
522
+ - `walle-message` only when Wall-E will execute tools, skills, or external
523
+ actions rather than plain chat.
512
524
  - Spawn, kill, restart, cancel.
513
525
  - Worktree create / delete / merge / create-pr.
514
526
  - Settings, token, notification, and Cloudflare Access configuration changes.
515
527
 
516
- Read-only routes (status, search, messages, watch streams) do **not** require
517
- step-up. The friction budget belongs to actions that can change machine state.
528
+ Read-only routes (status, search, messages, watch streams) and low-risk replies
529
+ do **not** require step-up when the transport is private and the CTM device
530
+ token is valid. The friction budget belongs to actions that can change machine
531
+ state or approve privileged agent behavior.
518
532
 
519
533
  Step-up UX:
520
534
  - The phone presents an inline "Confirm with Face ID" sheet only after the user
@@ -1216,6 +1230,8 @@ banner appears within 5 s; tap → unlocks → opens to detail.
1216
1230
  | R14 | Idle hook creates noisy or false high-priority notifications | High-priority push only from `waiting_input`, approval detection, crash, or explicit task transition |
1217
1231
  | R15 | Offline phone replays stale mutation after reconnect | Do not queue mutations offline; user must re-open action and step up again |
1218
1232
  | R16 | Passkey registered on tailnet origin fails on Cloudflare origin | Store `rp_id`/origin per credential; require separate credential enrollment per origin |
1233
+ | R17 | Anonymous Dev Tunnel access exposes CTM pairing and app auth to the public internet | Do not create anonymous access; reset stale port access to Dev Tunnels defaults; UI labels Microsoft Tunnel as private |
1234
+ | R18 | User mistakes passkey for Microsoft account verification | UI and docs say Microsoft/GitHub identity is checked by Dev Tunnels; CTM passkey only proves a registered credential for this origin |
1219
1235
 
1220
1236
  ### 10.2 Decisions from review
1221
1237
 
@@ -0,0 +1,119 @@
1
+ # Wall-E Session Model Preferences
2
+
3
+ ## Problem
4
+
5
+ Wall-E coding sessions need model switches to behave like session state, not
6
+ global Wall-E configuration. A user changing one CTM Wall-E tab from DeepSeek to
7
+ Kimi must not change the model used by any other Wall-E tab, future Wall-E chat,
8
+ or the Wall-E brain default provider.
9
+
10
+ The current CTM paths keep only partial state:
11
+
12
+ - The browser stores the picker selection in `walleState`.
13
+ - Each Wall-E JSONL turn records the provider/model used for that turn.
14
+ - `startup_tasks.model_id` may preserve a model restore hint, but it has no
15
+ provider column and is lifecycle cache rather than session truth.
16
+ - Wall-E brain metadata stores global defaults such as `walle_provider` and
17
+ `walle_model`.
18
+
19
+ These are useful but not the right owner for a durable per-tab preference.
20
+
21
+ ## Ownership
22
+
23
+ CTM owns the tab and its session-scoped model preference.
24
+
25
+ Wall-E owns provider registry, provider credentials, scorecards, global defaults,
26
+ and the chat runtime. Wall-E should receive an explicit provider/model when CTM
27
+ has a session preference, but CTM must not mutate Wall-E global defaults from the
28
+ session picker.
29
+
30
+ Wall-E JSONL owns audit history. It records which provider/model was used for a
31
+ turn, but it is not the preference source of truth.
32
+
33
+ `startup_tasks` owns crash restore lifecycle. It may mirror a model id for
34
+ backward compatibility, but it must not become the authoritative provider/model
35
+ mapping.
36
+
37
+ ## Data Model
38
+
39
+ CTM persists Wall-E session preferences in `session_model_preferences`:
40
+
41
+ ```sql
42
+ CREATE TABLE session_model_preferences (
43
+ ctm_session_id TEXT PRIMARY KEY,
44
+ agent_type TEXT NOT NULL DEFAULT 'walle',
45
+ provider_type TEXT NOT NULL DEFAULT '',
46
+ provider_id TEXT NOT NULL DEFAULT '',
47
+ model_id TEXT NOT NULL DEFAULT '',
48
+ registry_id TEXT NOT NULL DEFAULT '',
49
+ scope TEXT NOT NULL DEFAULT 'session',
50
+ source TEXT NOT NULL DEFAULT 'user',
51
+ pinned INTEGER NOT NULL DEFAULT 1,
52
+ created_at TEXT DEFAULT (datetime('now')),
53
+ updated_at TEXT DEFAULT (datetime('now')),
54
+ FOREIGN KEY (ctm_session_id) REFERENCES ctm_sessions(id) ON DELETE CASCADE
55
+ );
56
+ ```
57
+
58
+ The source-of-truth columns are `ctm_session_id`, `provider_type`, `model_id`,
59
+ and optional `registry_id`. `provider_id` is kept for exact provider-route
60
+ diagnostics when a provider type has multiple routes.
61
+
62
+ ## Resolution Order
63
+
64
+ When CTM sends a Wall-E prompt, model selection resolves in this order:
65
+
66
+ 1. Explicit per-message override from the client.
67
+ 2. CTM `session_model_preferences` for the `ctm_session_id`.
68
+ 3. Hydrated in-memory session model fields.
69
+ 4. Wall-E global default for new/unconfigured sessions.
70
+ 5. Wall-E scorecard/provider fallback.
71
+
72
+ Pinned manual session preferences disable provider fallback for that turn unless
73
+ the user explicitly opts into fallback.
74
+
75
+ ## Events
76
+
77
+ The Wall-E model picker emits a session-scoped `model-change` message:
78
+
79
+ ```json
80
+ {
81
+ "type": "model-change",
82
+ "id": "ctm-session-id",
83
+ "agent_type": "walle",
84
+ "model_id": "kimi-k2.6",
85
+ "model_provider": "moonshot",
86
+ "model_registry_id": "moonshot-default:kimi-k2.6",
87
+ "scope": "session"
88
+ }
89
+ ```
90
+
91
+ The server validates and persists the preference, updates only that in-memory
92
+ session, refreshes the startup restore hint, and broadcasts `walle-model` to the
93
+ affected session clients.
94
+
95
+ ## Invariants
96
+
97
+ - Changing the model in Wall-E session A never updates Wall-E brain keys
98
+ `walle_provider`, `walle_model`, or `walle_model_*`.
99
+ - Changing the model in Wall-E session A never changes session B's
100
+ `session_model_preferences` row.
101
+ - Restore hydrates provider and model from CTM session preference before using a
102
+ lifecycle hint or Wall-E global default.
103
+ - JSONL turn metadata can explain historical routing but does not overwrite the
104
+ session preference.
105
+ - `startup_tasks.model_id` is a compatibility hint only; missing provider
106
+ information there must not cause cross-provider routing.
107
+
108
+ ## Tests
109
+
110
+ Coverage should include:
111
+
112
+ - DB preference upsert/read/delete with cascade behavior.
113
+ - Wall-E picker sends provider-native model id plus provider type.
114
+ - A manual selection persists in CTM and is reused on the next prompt.
115
+ - Restored Wall-E sessions hydrate provider/model from
116
+ `session_model_preferences`, not only `startup_tasks`.
117
+ - Two Wall-E sessions can choose different providers without cross-session
118
+ mutation.
119
+ - Wall-E global default keys remain unchanged by session picker changes.