@runcore-sh/runcore 0.2.1 → 0.2.2
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/brain-template/knowledge/notes/tier-gated-ui-spec.md +187 -0
- package/brain-template/settings.json +3 -49
- package/dictionary.json +2 -2
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +3 -2
- package/dist/instance.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +12 -1
- package/dist/server.js.map +1 -1
- package/dist/tier/types.d.ts +3 -0
- package/dist/tier/types.d.ts.map +1 -1
- package/dist/tier/types.js +12 -0
- package/dist/tier/types.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +62 -7
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Tier-Gated UI
|
|
2
|
+
|
|
3
|
+
> Status: **Approved by Dash**
|
|
4
|
+
> Created: 2026-03-08
|
|
5
|
+
|
|
6
|
+
## Problem
|
|
7
|
+
|
|
8
|
+
A fresh `npx @runcore-sh/runcore` install shows the full settings panel — mesh config, LLM providers, voice settings, Google Workspace, Linear board integration, service registry, API key vault, and navigation to observatory, ops, roadmap, registry pages. None of these work on local tier. The UI promises capabilities the system cannot deliver.
|
|
9
|
+
|
|
10
|
+
Worse: the UI implies infrastructure connectivity that doesn't exist. A fresh local install is a stranger — no bond, no governance, no membrane, no trust chain. The interface should reflect that.
|
|
11
|
+
|
|
12
|
+
## Principle
|
|
13
|
+
|
|
14
|
+
**The UI is a symptom of unresolved autonomy.** If the system can't do it, the button shouldn't exist. Progressive disclosure applies to UI surfaces the same way it applies to brain context — show what's relevant, hide what isn't, unlock as capability grows.
|
|
15
|
+
|
|
16
|
+
## Tier Capability Matrix
|
|
17
|
+
|
|
18
|
+
| Capability | Local | BYOK | Spawn | Hosted |
|
|
19
|
+
|---|---|---|---|---|
|
|
20
|
+
| Chat | yes | yes | yes | yes |
|
|
21
|
+
| Brain (memory, knowledge) | yes | yes | yes | yes |
|
|
22
|
+
| Ollama (local inference) | yes | yes | yes | yes |
|
|
23
|
+
| Identity (name, personality) | yes | yes | yes | yes |
|
|
24
|
+
| Safe word | yes | yes | yes | yes |
|
|
25
|
+
| HTTP server + UI | yes | yes | yes | yes |
|
|
26
|
+
| API key vault | no | yes | yes | yes |
|
|
27
|
+
| Cloud LLM providers | no | yes | yes | yes |
|
|
28
|
+
| Mesh (LAN discovery) | no | yes | yes | yes |
|
|
29
|
+
| Voice (TTS/STT) | no | yes | yes | yes |
|
|
30
|
+
| Integrations (Google, Linear, etc.) | no | yes | yes | yes |
|
|
31
|
+
| Service registry | no | yes | yes | yes |
|
|
32
|
+
| Agent spawning | no | no | yes | yes |
|
|
33
|
+
| Governance (vouchers, policies) | no | no | yes | yes |
|
|
34
|
+
| Alerting (SMS, email, webhook) | no | no | yes | yes |
|
|
35
|
+
| Observatory (system metrics) | no | no | yes | yes |
|
|
36
|
+
|
|
37
|
+
## UI Surface by Tier
|
|
38
|
+
|
|
39
|
+
### Local Tier (fresh install)
|
|
40
|
+
|
|
41
|
+
**Header nav:** None. No page links. Just the agent name and the chat.
|
|
42
|
+
|
|
43
|
+
**Settings panel shows:**
|
|
44
|
+
- Agent name (editable)
|
|
45
|
+
- Personality / custom rules
|
|
46
|
+
- Safe word mode
|
|
47
|
+
- Ollama model selection (local models only)
|
|
48
|
+
- Airplane mode toggle (locked ON, greyed out with "Local tier" label)
|
|
49
|
+
|
|
50
|
+
**Settings panel hides:**
|
|
51
|
+
- Mesh settings section
|
|
52
|
+
- LLM provider section (OpenRouter, API keys)
|
|
53
|
+
- Voice settings section
|
|
54
|
+
- Google Workspace section
|
|
55
|
+
- Task board / Linear section
|
|
56
|
+
- Key vault section
|
|
57
|
+
- "Manage Services & Capabilities" link
|
|
58
|
+
|
|
59
|
+
**Pages accessible:** `/` (chat) only. All other pages return 404 or redirect to `/`.
|
|
60
|
+
|
|
61
|
+
**Upgrade prompt:** Small, non-intrusive text at bottom of settings: "Unlock cloud models, voice, and integrations → `runcore register`"
|
|
62
|
+
|
|
63
|
+
### BYOK Tier
|
|
64
|
+
|
|
65
|
+
**Header nav:** `/library`, `/life`
|
|
66
|
+
|
|
67
|
+
**Settings panel adds:**
|
|
68
|
+
- API key vault
|
|
69
|
+
- LLM provider selection (cloud models)
|
|
70
|
+
- Airplane mode toggle (now functional)
|
|
71
|
+
- Voice settings (if sidecar detected)
|
|
72
|
+
- Integrations section (Google, Linear, etc.)
|
|
73
|
+
- "Manage Services" link → `/registry`
|
|
74
|
+
|
|
75
|
+
**Pages accessible:** `/`, `/library`, `/life`, `/registry`, `/help`
|
|
76
|
+
|
|
77
|
+
### Spawn Tier
|
|
78
|
+
|
|
79
|
+
**Header nav:** Full nav — `/library`, `/personal`, `/life`, `/registry`, `/observatory`, `/ops`, `/board`, `/roadmap`
|
|
80
|
+
|
|
81
|
+
**Settings panel adds:**
|
|
82
|
+
- Mesh settings (LAN announce, allow incoming)
|
|
83
|
+
- Agent spawning controls
|
|
84
|
+
- Governance dashboard link
|
|
85
|
+
|
|
86
|
+
**Pages accessible:** All pages.
|
|
87
|
+
|
|
88
|
+
### Hosted Tier
|
|
89
|
+
|
|
90
|
+
Same as Spawn. No additional UI — hosted is about who runs the infrastructure, not what the UI shows.
|
|
91
|
+
|
|
92
|
+
## Implementation
|
|
93
|
+
|
|
94
|
+
### 1. Server: expose tier to client
|
|
95
|
+
|
|
96
|
+
**Route:** `GET /api/tier` (already partially exists via settings)
|
|
97
|
+
|
|
98
|
+
Response:
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"tier": "local",
|
|
102
|
+
"capabilities": {
|
|
103
|
+
"brain": true,
|
|
104
|
+
"ollama": true,
|
|
105
|
+
"server": true,
|
|
106
|
+
"ui": true,
|
|
107
|
+
"vault": false,
|
|
108
|
+
"mesh": false,
|
|
109
|
+
"spawning": false,
|
|
110
|
+
"governance": false,
|
|
111
|
+
"alerting": false,
|
|
112
|
+
"voice": false,
|
|
113
|
+
"integrations": false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Client: fetch tier on load, gate UI
|
|
119
|
+
|
|
120
|
+
On page load (after auth), fetch `/api/tier`. Store in `window.__TIER__`.
|
|
121
|
+
|
|
122
|
+
Each settings section gets a `data-requires` attribute:
|
|
123
|
+
```html
|
|
124
|
+
<div id="mesh-settings-section" data-requires="mesh">
|
|
125
|
+
<div id="llm-settings-section" data-requires="vault">
|
|
126
|
+
<div id="voice-settings-section" data-requires="voice">
|
|
127
|
+
<div id="google-settings-section" data-requires="integrations">
|
|
128
|
+
<div id="board-settings-section" data-requires="integrations">
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
On tier load:
|
|
132
|
+
```javascript
|
|
133
|
+
function applyTierGating(caps) {
|
|
134
|
+
document.querySelectorAll('[data-requires]').forEach(el => {
|
|
135
|
+
const cap = el.dataset.requires;
|
|
136
|
+
if (!caps[cap]) el.remove(); // remove, not hide — don't tease
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 3. Navigation: tier-gated links
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
const NAV_BY_TIER = {
|
|
145
|
+
local: [],
|
|
146
|
+
byok: ['library', 'life', 'registry', 'help'],
|
|
147
|
+
spawn: ['library', 'personal', 'life', 'registry', 'observatory', 'ops', 'board', 'roadmap'],
|
|
148
|
+
hosted: ['library', 'personal', 'life', 'registry', 'observatory', 'ops', 'board', 'roadmap'],
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Build nav dynamically from this map. No hardcoded links in HTML.
|
|
153
|
+
|
|
154
|
+
### 4. Page-level gating
|
|
155
|
+
|
|
156
|
+
Each sub-page (library.html, ops.html, etc.) also fetches `/api/tier` and redirects to `/` if the tier doesn't support it. Defense in depth — even if someone types the URL directly.
|
|
157
|
+
|
|
158
|
+
### 5. Settings.json template cleanup
|
|
159
|
+
|
|
160
|
+
The brain-template `settings.json` should reflect local tier defaults:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"airplaneMode": true,
|
|
165
|
+
"models": { "chat": "auto", "utility": "auto" },
|
|
166
|
+
"encryptBrainFiles": false,
|
|
167
|
+
"safeWordMode": "always",
|
|
168
|
+
"instanceName": "Core",
|
|
169
|
+
"integrations": { "enabled": false, "services": {} }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
No TTS/STT/avatar/backup/pulse/mesh config — those get created when the tier unlocks them.
|
|
174
|
+
|
|
175
|
+
## What This Does NOT Change
|
|
176
|
+
|
|
177
|
+
- The server still boots all routes regardless of tier (defense in depth is at the UI and middleware level, not route registration)
|
|
178
|
+
- The `requireSurface()` middleware already gates some pages — this complements it
|
|
179
|
+
- Tier upgrades take effect immediately without restart (client re-fetches `/api/tier`)
|
|
180
|
+
- The brain-template stays minimal — no personal data, no service configs
|
|
181
|
+
|
|
182
|
+
## Verification
|
|
183
|
+
|
|
184
|
+
1. Fresh `npx @runcore-sh/runcore` → chat screen only, minimal settings, no nav links
|
|
185
|
+
2. `runcore register` + `runcore activate <token>` → settings expand, nav appears
|
|
186
|
+
3. Direct URL to `/ops` on local tier → redirects to `/`
|
|
187
|
+
4. Settings panel never shows capabilities the tier can't deliver
|
|
@@ -1,60 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"airplaneMode":
|
|
3
|
-
"privateMode": false,
|
|
2
|
+
"airplaneMode": true,
|
|
4
3
|
"models": {
|
|
5
4
|
"chat": "auto",
|
|
6
5
|
"utility": "auto"
|
|
7
6
|
},
|
|
8
7
|
"encryptBrainFiles": false,
|
|
9
8
|
"safeWordMode": "always",
|
|
10
|
-
"tts": {
|
|
11
|
-
"enabled": false,
|
|
12
|
-
"port": 3579,
|
|
13
|
-
"voice": "en_US-lessac-medium",
|
|
14
|
-
"autoPlay": true
|
|
15
|
-
},
|
|
16
|
-
"stt": {
|
|
17
|
-
"enabled": false,
|
|
18
|
-
"port": 3580,
|
|
19
|
-
"model": "ggml-base.en.bin"
|
|
20
|
-
},
|
|
21
|
-
"avatar": {
|
|
22
|
-
"enabled": false,
|
|
23
|
-
"port": 3581,
|
|
24
|
-
"musetalkPath": "",
|
|
25
|
-
"photoPath": "public/avatar/photo.png"
|
|
26
|
-
},
|
|
27
|
-
"backup": {
|
|
28
|
-
"enabled": false,
|
|
29
|
-
"schedule": "daily",
|
|
30
|
-
"providers": [
|
|
31
|
-
"local"
|
|
32
|
-
],
|
|
33
|
-
"localBackupDir": ".core-backups",
|
|
34
|
-
"maxBackups": 7,
|
|
35
|
-
"backupHour": 3
|
|
36
|
-
},
|
|
37
|
-
"pulse": {
|
|
38
|
-
"threshold": 60,
|
|
39
|
-
"mode": "hybrid"
|
|
40
|
-
},
|
|
41
|
-
"mesh": {
|
|
42
|
-
"lanAnnounce": false,
|
|
43
|
-
"allowIncoming": false
|
|
44
|
-
},
|
|
45
9
|
"instanceName": "Core",
|
|
46
10
|
"integrations": {
|
|
47
|
-
"enabled":
|
|
48
|
-
"services": {
|
|
49
|
-
"google": false,
|
|
50
|
-
"github": true,
|
|
51
|
-
"slack": false,
|
|
52
|
-
"twilio": false,
|
|
53
|
-
"resend": true,
|
|
54
|
-
"openai": true,
|
|
55
|
-
"anthropic": true,
|
|
56
|
-
"openrouter": true,
|
|
57
|
-
"perplexity": false
|
|
58
|
-
}
|
|
11
|
+
"enabled": false,
|
|
12
|
+
"services": {}
|
|
59
13
|
}
|
|
60
14
|
}
|
package/dictionary.json
CHANGED
package/dist/instance.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAWvC;AAED,kEAAkE;AAClE,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,oEAAoE;AACpE,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE7D;AAED,yDAAyD;AACzD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpD"}
|
package/dist/instance.js
CHANGED
|
@@ -15,8 +15,9 @@ export function initInstanceName() {
|
|
|
15
15
|
try {
|
|
16
16
|
const raw = readFileSync(join(BRAIN_DIR, "settings.json"), "utf-8");
|
|
17
17
|
const parsed = JSON.parse(raw);
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const name = parsed.instanceName ?? parsed.agentName;
|
|
19
|
+
if (typeof name === "string" && name.trim()) {
|
|
20
|
+
instanceName = name.trim();
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
catch {
|
package/dist/instance.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,IAAI,YAAY,GAAG,MAAM,CAAC;AAE1B;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EAAE,OAAO,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,
|
|
1
|
+
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,IAAI,YAAY,GAAG,MAAM,CAAC;AAE1B;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,EAAE,OAAO,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,SAAS,CAAC;QACrD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAC5C,YAAY,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,6CAA6C;IAC/C,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,eAAe;IAC7B,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,oBAAoB;IAClC,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC;AACpC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,MAAM,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,CAAC,kBAAkB,CAAC,IAAI,GAAG,oBAAoB,EAAE,YAAY,CAAC;AACjF,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,eAAe;IAC7B,OAAO,UAAU,CAAC,gBAAgB,CAAC,CAAC;AACtC,CAAC"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuTH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuTH,wBAAgB,eAAe,IAAI,MAAM,GAAG,IAAI,CAE/C;AAyhKD,iBAAe,KAAK,CAAC,IAAI,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,iBAAiB,EAAE,QAAQ,CAAA;CAAE,iBAihBxE;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,OAAO,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -344,7 +344,7 @@ async function getOrCreateChatSession(sessionId, name) {
|
|
|
344
344
|
chatSessions.set(sessionId, cs);
|
|
345
345
|
return cs;
|
|
346
346
|
}
|
|
347
|
-
|
|
347
|
+
let activeTier = "local";
|
|
348
348
|
const app = new Hono();
|
|
349
349
|
// Global error handler — return JSON with details instead of plain "Internal Server Error"
|
|
350
350
|
app.onError((err, c) => {
|
|
@@ -459,6 +459,15 @@ app.get("/api/status", async (c) => {
|
|
|
459
459
|
agentName: settings.agentName || "Core",
|
|
460
460
|
});
|
|
461
461
|
});
|
|
462
|
+
// Tier: current tier + capability matrix for UI gating
|
|
463
|
+
app.get("/api/tier", async (c) => {
|
|
464
|
+
const { TIER_CAPS } = await import("./tier/types.js");
|
|
465
|
+
const tier = activeTier;
|
|
466
|
+
return c.json({
|
|
467
|
+
tier,
|
|
468
|
+
capabilities: TIER_CAPS[tier] ?? TIER_CAPS.local,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
462
471
|
// Pairing ceremony
|
|
463
472
|
app.post("/api/pair", async (c) => {
|
|
464
473
|
const body = await c.req.json();
|
|
@@ -482,6 +491,7 @@ app.post("/api/pair", async (c) => {
|
|
|
482
491
|
const settingsPath = join(BRAIN_DIR, "settings.json");
|
|
483
492
|
const raw = await readFile(settingsPath, "utf-8");
|
|
484
493
|
const settings = JSON.parse(raw);
|
|
494
|
+
settings.instanceName = agentName.trim();
|
|
485
495
|
settings.agentName = agentName.trim();
|
|
486
496
|
await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
487
497
|
}
|
|
@@ -4916,6 +4926,7 @@ app.post("/api/import/files", async (c) => {
|
|
|
4916
4926
|
// --- Startup ---
|
|
4917
4927
|
async function start(opts) {
|
|
4918
4928
|
const tier = opts?.tier ?? "byok";
|
|
4929
|
+
activeTier = tier;
|
|
4919
4930
|
const tierGate = await import("./tier/gate.js");
|
|
4920
4931
|
// Initialize instance name before anything else
|
|
4921
4932
|
initInstanceName();
|