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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +131 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +58 -50
- package/template/claude-task-manager/docs/phone-access-design.md +23 -7
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +119 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +32 -48
- package/template/claude-task-manager/lib/remote-relay-protocol.js +5 -0
- package/template/claude-task-manager/lib/walle-external-actions.js +20 -3
- package/template/claude-task-manager/public/index.html +25 -0
- package/template/claude-task-manager/public/js/setup.js +16 -12
- package/template/claude-task-manager/public/js/walle-session.js +31 -3
- package/template/claude-task-manager/public/js/walle.js +93 -23
- package/template/claude-task-manager/public/m/app.css +417 -21
- package/template/claude-task-manager/public/m/app.js +831 -44
- package/template/claude-task-manager/public/m/claim.html +1 -1
- package/template/claude-task-manager/public/m/index.html +41 -7
- package/template/claude-task-manager/public/m/sw.js +1 -1
- package/template/claude-task-manager/server.js +377 -30
- package/template/claude-task-manager/workers/state-detectors/codex.js +18 -3
- package/template/package.json +1 -1
- package/template/wall-e/chat.js +32 -2
- package/template/wall-e/coding/stream-processor.js +36 -0
- package/template/wall-e/coding-orchestrator.js +45 -0
- package/template/wall-e/deploy.sh +1 -1
- package/template/wall-e/docs/external-action-controller.md +60 -2
- package/template/wall-e/external-action-controller.js +23 -1
- package/template/wall-e/external-action-gateway.js +163 -0
- package/template/wall-e/fly.toml +1 -0
- package/template/wall-e/tools/local-tools.js +122 -4
- 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.
|
|
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:
|
|
4
|
-
Date: 2026-05-
|
|
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
|
|
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
|
|
71
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
2. **Use
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
86
|
-
|
|
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
|
|
118
|
-
|
|
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
|
-
-
|
|
135
|
-
|
|
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.
|
|
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
|
|
342
|
-
or phone
|
|
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.
|
|
349
|
-
permissions
|
|
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: `
|
|
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
|
|
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
|
|
460
|
-
|
|
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
|
|
465
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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
|
|
577
|
-
- The phone
|
|
578
|
-
|
|
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
|
-
|
|
583
|
-
|
|
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
|
-
-
|
|
598
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
712
|
-
|
|
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, **
|
|
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
|
-
-
|
|
510
|
-
-
|
|
511
|
-
|
|
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)
|
|
517
|
-
step-up
|
|
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.
|