@wipcomputer/wip-ldm-os 0.4.85-alpha.8 → 0.4.86
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 +22 -2
- package/SKILL.md +137 -15
- package/bin/ldm.js +608 -75
- package/lib/deploy.mjs +90 -12
- package/lib/registry-migrations.mjs +296 -0
- package/package.json +21 -3
- package/scripts/test-boot-dir-truth.mjs +158 -0
- package/scripts/test-boot-hook-registration.mjs +136 -0
- package/scripts/test-boot-payload-trim.mjs +200 -0
- package/scripts/test-crc-agentid-tenant-boundary.mjs +2 -2
- package/scripts/test-crc-e2ee-key-persistence.mjs +150 -0
- package/scripts/test-crc-e2ee-session-route.mjs +9 -2
- package/scripts/test-crc-pair-login-flow.mjs +76 -1
- package/scripts/test-crc-pair-relink-audit-and-rotation.mjs +164 -0
- package/scripts/test-crc-websocket-abuse-limits.mjs +128 -0
- package/scripts/test-deploy-hook-ownership.mjs +160 -0
- package/scripts/test-doctor-hook-dedupe.mjs +188 -0
- package/scripts/test-install-prompt-policy.mjs +84 -0
- package/scripts/test-installer-skill-dry-run-destinations.mjs +100 -0
- package/scripts/test-installer-target-self-update.mjs +131 -0
- package/scripts/test-kaleidoscope-onboarding-copy.mjs +170 -0
- package/scripts/test-kaleidoscope-public-stats-baseline.mjs +129 -0
- package/scripts/test-kaleidoscope-qr-authenticator-confirmation.mjs +89 -0
- package/scripts/test-ldm-status-concurrency.mjs +118 -0
- package/scripts/test-ldm-status-timeout.mjs +96 -0
- package/scripts/test-legacy-npm-sources-migration.mjs +460 -0
- package/scripts/test-readme-install-prompt.mjs +66 -0
- package/shared/boot/boot-config.json +18 -8
- package/shared/docs/dev-guide-wipcomputerinc.md.tmpl +5 -4
- package/shared/rules/security.md +4 -0
- package/shared/templates/install-prompt.md +20 -2
- package/src/boot/README.md +24 -1
- package/src/boot/boot-config.json +18 -8
- package/src/boot/boot-hook.mjs +118 -28
- package/src/boot/installer.mjs +33 -10
- package/src/hosted-mcp/.env.example +4 -0
- package/src/hosted-mcp/README.md +37 -0
- package/src/hosted-mcp/app/footer.js +2 -2
- package/src/hosted-mcp/app/kaleidoscope-login.html +489 -42
- package/src/hosted-mcp/app/pair.html +21 -3
- package/src/hosted-mcp/app/wip-logo.png +0 -0
- package/src/hosted-mcp/codex-relay-e2ee-registry.mjs +208 -0
- package/src/hosted-mcp/codex-relay-ws-abuse-limits.mjs +140 -0
- package/src/hosted-mcp/demo/footer.js +2 -2
- package/src/hosted-mcp/demo/index.html +166 -44
- package/src/hosted-mcp/demo/login.html +87 -23
- package/src/hosted-mcp/demo/privacy.html +4 -214
- package/src/hosted-mcp/demo/tos.html +4 -189
- package/src/hosted-mcp/deploy.sh +2 -0
- package/src/hosted-mcp/docs/self-host.md +268 -0
- package/src/hosted-mcp/legal/internet-services/kaleidoscope/index.html +257 -0
- package/src/hosted-mcp/legal/internet-services/terms/site.html +224 -168
- package/src/hosted-mcp/legal/legal-footer.js +75 -0
- package/src/hosted-mcp/legal/legal.css +166 -0
- package/src/hosted-mcp/legal/privacy/en-ww/index.html +4 -221
- package/src/hosted-mcp/legal/privacy/index.html +253 -0
- package/src/hosted-mcp/server.mjs +920 -74
|
@@ -6,7 +6,19 @@ Open your AI and paste this:
|
|
|
6
6
|
|
|
7
7
|
Read https://wip.computer/install/wip-ldm-os.txt
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Use the install document and live local checks as the source of truth.
|
|
10
|
+
Do not search memory or prior notes for this install. Do not pre-load context from `MEMORY.md`, `crystal_search`, local skill dev guides, or other local memory before fetching the install document.
|
|
11
|
+
Do not run GitHub commands during this install flow. Do not run or request approval for `gh release`, `gh api`, or `gh search`. If release notes are not available from local or npm metadata, say that and do not fetch them from GitHub.
|
|
12
|
+
|
|
13
|
+
Check if LDM OS is installed (`which ldm && ldm --version`). Branch.
|
|
14
|
+
|
|
15
|
+
If installed: run `ldm status`, check available npm tracks from the install document, and show me the version table, what's new for each component, and which stable/current, beta, and alpha versions are available. Then ask:
|
|
16
|
+
- Do you have questions?
|
|
17
|
+
- Want to see a dry run?
|
|
18
|
+
|
|
19
|
+
If yes to dry run, use the selected track's dry-run path from the install document.
|
|
20
|
+
|
|
21
|
+
If I say install, use the selected track's install path from the install document, then run `ldm doctor`.
|
|
10
22
|
|
|
11
23
|
If not, walk me through setup and explain:
|
|
12
24
|
|
|
@@ -19,7 +31,13 @@ Then ask:
|
|
|
19
31
|
- Do you have questions?
|
|
20
32
|
- Want to see a dry run?
|
|
21
33
|
|
|
22
|
-
If
|
|
34
|
+
If yes to dry run, install the CLI first using the selected track's bootstrap command from the install document.
|
|
35
|
+
|
|
36
|
+
Then run:
|
|
37
|
+
`ldm init --dry-run`
|
|
38
|
+
|
|
39
|
+
If I say install, run:
|
|
40
|
+
`ldm init`
|
|
23
41
|
|
|
24
42
|
Show me exactly what will change. Don't install anything until I say "install".
|
|
25
43
|
|
package/src/boot/README.md
CHANGED
|
@@ -8,7 +8,24 @@ Reads 9 files from the Dream Weaver Boot Sequence (SHARED-CONTEXT.md, SOUL.md, C
|
|
|
8
8
|
|
|
9
9
|
## Content Budget
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Large files are line-capped and stale journals are dropped to path-only lines, so the payload stays small. Missing files are skipped gracefully. The last line of every boot payload is a one-line summary (`== Boot payload: N bytes, M lines, K sections. Capped: ... Stale/path-only: ... ==`) so future slowness triage is trivial.
|
|
12
|
+
|
|
13
|
+
### Per-step line caps
|
|
14
|
+
|
|
15
|
+
Every step has a line cap. If `boot-config.json` sets `maxLines` for a step, that wins. Otherwise these code defaults apply. They live in `boot-hook.mjs`, not just the config template, because the installer preserves an existing user `boot-config.json` and never overwrites it, so a config-only change would never reach existing installs:
|
|
16
|
+
|
|
17
|
+
- `sharedContext`: 80
|
|
18
|
+
- `soul`: 80
|
|
19
|
+
- `context`: 60
|
|
20
|
+
- `repoLocations`: 80
|
|
21
|
+
|
|
22
|
+
When a file is truncated, the injected content ends with a marker naming the full path so the rest can be read on demand.
|
|
23
|
+
|
|
24
|
+
### Staleness cutoffs
|
|
25
|
+
|
|
26
|
+
`most-recent` steps (journals) inject the newest dated file only if it is fresh. If the newest file is older than `stalenessDays` (default 14, overridable top-level or per-step), the body is NOT injected: a single line with the file path, date, and age is emitted instead. This stops a four-month-old "most recent" journal from being re-injected every session.
|
|
27
|
+
|
|
28
|
+
`daily-logs` steps have the same guard via `dailyLogStalenessDays` (default 2). Day files are today/yesterday by construction, so this is defensive.
|
|
12
29
|
|
|
13
30
|
## Deploy
|
|
14
31
|
|
|
@@ -49,6 +66,12 @@ Should output JSON with `hookSpecificOutput.additionalContext` containing all bo
|
|
|
49
66
|
|
|
50
67
|
`boot-config.json` defines paths and limits for each boot step. Uses `~` shorthand (resolved at runtime). To support a different agent (cc-air), deploy a different config alongside the same script.
|
|
51
68
|
|
|
69
|
+
Optional keys:
|
|
70
|
+
|
|
71
|
+
- Top-level `stalenessDays` (default 14) and `dailyLogStalenessDays` (default 2): staleness cutoffs for `most-recent` and `daily-logs` steps. Per-step keys of the same name override the top-level value.
|
|
72
|
+
- Per-step `maxLines`: line cap for that step. Overrides the code default (see Content Budget).
|
|
73
|
+
- Top-level `maxTotalLines` (default 2000): hard safety cap across all steps.
|
|
74
|
+
|
|
52
75
|
## Adding a Boot Step
|
|
53
76
|
|
|
54
77
|
1. Add an entry to `boot-config.json` under `steps`
|
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
"agentId": "cc-mini",
|
|
3
3
|
"timezone": "America/Los_Angeles",
|
|
4
4
|
"maxTotalLines": 2000,
|
|
5
|
+
"stalenessDays": 14,
|
|
6
|
+
"dailyLogStalenessDays": 2,
|
|
5
7
|
"steps": {
|
|
6
8
|
"sharedContext": {
|
|
7
9
|
"path": "~/.openclaw/workspace/SHARED-CONTEXT.md",
|
|
8
10
|
"label": "SHARED-CONTEXT.md",
|
|
9
11
|
"stepNumber": 2,
|
|
10
|
-
"critical": true
|
|
12
|
+
"critical": true,
|
|
13
|
+
"maxLines": 80
|
|
11
14
|
},
|
|
12
15
|
"journals": {
|
|
13
16
|
"dir": "~/Documents/wipcomputer--mac-mini-01/staff/Parker/Claude Code - Mini/documents/journals",
|
|
14
17
|
"label": "Most Recent Journal (Parker)",
|
|
15
18
|
"stepNumber": 3,
|
|
16
19
|
"maxLines": 80,
|
|
17
|
-
"strategy": "most-recent"
|
|
20
|
+
"strategy": "most-recent",
|
|
21
|
+
"stalenessDays": 14
|
|
18
22
|
},
|
|
19
23
|
"workspaceDailyLogs": {
|
|
20
24
|
"dir": "~/.openclaw/workspace/memory",
|
|
@@ -22,7 +26,8 @@
|
|
|
22
26
|
"stepNumber": 4,
|
|
23
27
|
"maxLines": 40,
|
|
24
28
|
"strategy": "daily-logs",
|
|
25
|
-
"days": ["today", "yesterday"]
|
|
29
|
+
"days": ["today", "yesterday"],
|
|
30
|
+
"dailyLogStalenessDays": 2
|
|
26
31
|
},
|
|
27
32
|
"fullHistory": {
|
|
28
33
|
"label": "Full History",
|
|
@@ -33,19 +38,22 @@
|
|
|
33
38
|
"path": "~/.ldm/agents/cc-mini/CONTEXT.md",
|
|
34
39
|
"label": "CC CONTEXT.md",
|
|
35
40
|
"stepNumber": 6,
|
|
36
|
-
"critical": true
|
|
41
|
+
"critical": true,
|
|
42
|
+
"maxLines": 60
|
|
37
43
|
},
|
|
38
44
|
"soul": {
|
|
39
45
|
"path": "~/.ldm/agents/cc-mini/SOUL.md",
|
|
40
46
|
"label": "CC SOUL.md",
|
|
41
|
-
"stepNumber": 7
|
|
47
|
+
"stepNumber": 7,
|
|
48
|
+
"maxLines": 80
|
|
42
49
|
},
|
|
43
50
|
"ccJournals": {
|
|
44
51
|
"dir": "~/.ldm/agents/cc-mini/memory/journals",
|
|
45
52
|
"label": "Most Recent CC Journal",
|
|
46
53
|
"stepNumber": 8,
|
|
47
54
|
"maxLines": 80,
|
|
48
|
-
"strategy": "most-recent"
|
|
55
|
+
"strategy": "most-recent",
|
|
56
|
+
"stalenessDays": 14
|
|
49
57
|
},
|
|
50
58
|
"ccDailyLog": {
|
|
51
59
|
"dir": "~/.ldm/agents/cc-mini/memory/daily",
|
|
@@ -53,13 +61,15 @@
|
|
|
53
61
|
"stepNumber": 9,
|
|
54
62
|
"maxLines": 60,
|
|
55
63
|
"strategy": "daily-logs",
|
|
56
|
-
"days": ["today", "yesterday"]
|
|
64
|
+
"days": ["today", "yesterday"],
|
|
65
|
+
"dailyLogStalenessDays": 2
|
|
57
66
|
},
|
|
58
67
|
"repoLocations": {
|
|
59
68
|
"path": "~/.claude/projects/-Users-lesa--openclaw/memory/repo-locations.md",
|
|
60
69
|
"label": "repo-locations.md",
|
|
61
70
|
"stepNumber": 10,
|
|
62
|
-
"critical": true
|
|
71
|
+
"critical": true,
|
|
72
|
+
"maxLines": 80
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
}
|
package/src/boot/boot-hook.mjs
CHANGED
|
@@ -38,18 +38,63 @@ function listDirSafe(dirPath) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Per-step default line caps applied when boot-config.json does not set
|
|
42
|
+
// maxLines for the step. Keyed by the step key in config.steps. Any of these
|
|
43
|
+
// can be overridden per-step via a maxLines value in boot-config.json.
|
|
44
|
+
// Lives in code (not just the config template) because the installer preserves
|
|
45
|
+
// an existing user boot-config.json, so config-template changes never reach
|
|
46
|
+
// existing installs. Defaults must ride along with the hook.
|
|
47
|
+
const DEFAULT_MAX_LINES = {
|
|
48
|
+
sharedContext: 80,
|
|
49
|
+
soul: 80,
|
|
50
|
+
context: 60,
|
|
51
|
+
repoLocations: 80,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Default staleness cutoffs. Overridable via top-level config keys
|
|
55
|
+
// (stalenessDays, dailyLogStalenessDays) or per-step keys of the same name.
|
|
56
|
+
const DEFAULT_STALENESS_DAYS = 14; // most-recent strategy (journals)
|
|
57
|
+
const DEFAULT_DAILY_LOG_STALENESS_DAYS = 2; // daily-logs strategy guard
|
|
58
|
+
|
|
59
|
+
function resolveMaxLines(key, step) {
|
|
60
|
+
if (step.maxLines != null) return step.maxLines;
|
|
61
|
+
if (DEFAULT_MAX_LINES[key] != null) return DEFAULT_MAX_LINES[key];
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Returns { text, capped }. When truncation happens, the marker names the
|
|
66
|
+
// full path so a future reader can pull the rest.
|
|
67
|
+
function truncateTop(content, maxLines, fullPath) {
|
|
68
|
+
if (!maxLines || !content) return { text: content, capped: false };
|
|
43
69
|
const lines = content.split('\n');
|
|
44
|
-
if (lines.length <= maxLines) return content;
|
|
45
|
-
|
|
70
|
+
if (lines.length <= maxLines) return { text: content, capped: false };
|
|
71
|
+
const where = fullPath || '(path unknown)';
|
|
72
|
+
const marker = `\n[... truncated at ${maxLines} of ${lines.length} lines. Read the rest: ${where} ...]`;
|
|
73
|
+
return { text: lines.slice(0, maxLines).join('\n') + marker, capped: true };
|
|
46
74
|
}
|
|
47
75
|
|
|
48
|
-
function truncateBottom(content, maxLines) {
|
|
49
|
-
if (!maxLines || !content) return content;
|
|
76
|
+
function truncateBottom(content, maxLines, fullPath) {
|
|
77
|
+
if (!maxLines || !content) return { text: content, capped: false };
|
|
50
78
|
const lines = content.split('\n');
|
|
51
|
-
if (lines.length <= maxLines) return content;
|
|
52
|
-
|
|
79
|
+
if (lines.length <= maxLines) return { text: content, capped: false };
|
|
80
|
+
const where = fullPath || '(path unknown)';
|
|
81
|
+
const marker = `[... showing last ${maxLines} of ${lines.length} lines. Read the rest: ${where} ...]\n`;
|
|
82
|
+
return { text: marker + lines.slice(-maxLines).join('\n'), capped: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Extract a YYYY-MM-DD prefix from a filename, or null if none.
|
|
86
|
+
function dateStrFromName(fileName) {
|
|
87
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(fileName || '');
|
|
88
|
+
return m ? m[1] : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Whole-day difference (todayStr - dateStr) in days. Both are YYYY-MM-DD.
|
|
92
|
+
// Positive means dateStr is in the past. null if either is unparseable.
|
|
93
|
+
function ageInDays(dateStr, todayStr) {
|
|
94
|
+
const a = Date.parse(`${dateStr}T00:00:00Z`);
|
|
95
|
+
const b = Date.parse(`${todayStr}T00:00:00Z`);
|
|
96
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return null;
|
|
97
|
+
return Math.round((b - a) / 86400000);
|
|
53
98
|
}
|
|
54
99
|
|
|
55
100
|
function getTodayAndYesterday(timezone) {
|
|
@@ -99,6 +144,8 @@ function getDefaultConfig() {
|
|
|
99
144
|
agentId: 'cc-mini',
|
|
100
145
|
timezone: 'America/Los_Angeles',
|
|
101
146
|
maxTotalLines: 2000,
|
|
147
|
+
stalenessDays: DEFAULT_STALENESS_DAYS,
|
|
148
|
+
dailyLogStalenessDays: DEFAULT_DAILY_LOG_STALENESS_DAYS,
|
|
102
149
|
steps: {
|
|
103
150
|
sharedContext: { path: '~/.openclaw/workspace/SHARED-CONTEXT.md', label: 'SHARED-CONTEXT.md', stepNumber: 2, critical: true },
|
|
104
151
|
journals: { dir: '~/.ldm/agents/cc-mini/memory/journals', label: 'Most Recent CC Journal (Legacy)', stepNumber: 3, maxLines: 80, strategy: 'most-recent' },
|
|
@@ -113,52 +160,79 @@ function getDefaultConfig() {
|
|
|
113
160
|
};
|
|
114
161
|
}
|
|
115
162
|
|
|
116
|
-
function processStep(key, step, dates) {
|
|
163
|
+
function processStep(key, step, dates, config) {
|
|
117
164
|
// Reminder-only step (e.g. full history)
|
|
118
165
|
if (step.reminder) {
|
|
119
|
-
return { content: step.reminder, loaded: true, fileName: null };
|
|
166
|
+
return { content: step.reminder, loaded: true, fileName: null, capped: false, stale: false };
|
|
120
167
|
}
|
|
121
168
|
|
|
169
|
+
const maxLines = resolveMaxLines(key, step);
|
|
170
|
+
|
|
122
171
|
// Single file step
|
|
123
172
|
if (step.path) {
|
|
173
|
+
const resolved = resolvePath(step.path);
|
|
124
174
|
const content = readFileSafe(step.path);
|
|
125
|
-
if (!content) return { content: null, loaded: false, fileName:
|
|
126
|
-
const
|
|
127
|
-
return { content:
|
|
175
|
+
if (!content) return { content: null, loaded: false, fileName: resolved, capped: false, stale: false };
|
|
176
|
+
const { text, capped } = truncateTop(content, maxLines, resolved);
|
|
177
|
+
return { content: text, loaded: true, fileName: resolved, capped, stale: false };
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
// Directory-based step
|
|
131
181
|
if (step.dir) {
|
|
132
182
|
if (step.strategy === 'most-recent') {
|
|
133
183
|
const fileName = findMostRecent(step.dir);
|
|
134
|
-
if (!fileName) return { content: null, loaded: false, fileName: resolvePath(step.dir) };
|
|
184
|
+
if (!fileName) return { content: null, loaded: false, fileName: resolvePath(step.dir), capped: false, stale: false };
|
|
135
185
|
const fullPath = join(resolvePath(step.dir), fileName);
|
|
136
186
|
const content = readFileSafe(fullPath);
|
|
137
|
-
if (!content) return { content: null, loaded: false, fileName: fullPath };
|
|
138
|
-
|
|
139
|
-
|
|
187
|
+
if (!content) return { content: null, loaded: false, fileName: fullPath, capped: false, stale: false };
|
|
188
|
+
|
|
189
|
+
// Staleness cutoff: if the newest file is older than the cutoff, do not
|
|
190
|
+
// inject its body. Emit a single line with the path and date instead.
|
|
191
|
+
const stalenessDays = step.stalenessDays ?? config.stalenessDays ?? DEFAULT_STALENESS_DAYS;
|
|
192
|
+
const fileDate = dateStrFromName(fileName);
|
|
193
|
+
const age = fileDate ? ageInDays(fileDate, dates.today) : null;
|
|
194
|
+
if (age != null && age > stalenessDays) {
|
|
195
|
+
const line = `[stale: newest file is ${fileDate} (${age}d old, cutoff ${stalenessDays}d). Body not injected. Read if needed: ${fullPath}]`;
|
|
196
|
+
return { content: line, loaded: true, fileName, capped: false, stale: true };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { text, capped } = truncateTop(content, maxLines, fullPath);
|
|
200
|
+
return { content: text, loaded: true, fileName, capped, stale: false };
|
|
140
201
|
}
|
|
141
202
|
|
|
142
203
|
if (step.strategy === 'daily-logs') {
|
|
204
|
+
const dailyStalenessDays = step.dailyLogStalenessDays ?? config.dailyLogStalenessDays ?? DEFAULT_DAILY_LOG_STALENESS_DAYS;
|
|
143
205
|
const parts = [];
|
|
144
206
|
let anyLoaded = false;
|
|
207
|
+
let anyCapped = false;
|
|
208
|
+
let anyStale = false;
|
|
145
209
|
for (const day of (step.days || ['today'])) {
|
|
146
210
|
const dateStr = day === 'today' ? dates.today : dates.yesterday;
|
|
147
211
|
const fileName = `${dateStr}.md`;
|
|
148
212
|
const fullPath = join(resolvePath(step.dir), fileName);
|
|
149
213
|
const content = readFileSafe(fullPath);
|
|
150
|
-
if (content)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
214
|
+
if (!content) continue;
|
|
215
|
+
anyLoaded = true;
|
|
216
|
+
|
|
217
|
+
// Guard: a day file dated older than the cutoff should not normally
|
|
218
|
+
// happen (days are today/yesterday), but if it does, emit path-only.
|
|
219
|
+
const age = ageInDays(dateStr, dates.today);
|
|
220
|
+
if (age != null && age > dailyStalenessDays) {
|
|
221
|
+
parts.push(`--- ${day} (${dateStr}) ---\n[stale: ${age}d old, cutoff ${dailyStalenessDays}d. Body not injected. Read if needed: ${fullPath}]`);
|
|
222
|
+
anyStale = true;
|
|
223
|
+
continue;
|
|
154
224
|
}
|
|
225
|
+
|
|
226
|
+
const { text, capped } = truncateBottom(content, maxLines, fullPath);
|
|
227
|
+
if (capped) anyCapped = true;
|
|
228
|
+
parts.push(`--- ${day} (${dateStr}) ---\n${text}`);
|
|
155
229
|
}
|
|
156
|
-
if (!anyLoaded) return { content: null, loaded: false, fileName: resolvePath(step.dir) };
|
|
157
|
-
return { content: parts.join('\n\n'), loaded: true, fileName: null };
|
|
230
|
+
if (!anyLoaded) return { content: null, loaded: false, fileName: resolvePath(step.dir), capped: false, stale: false };
|
|
231
|
+
return { content: parts.join('\n\n'), loaded: true, fileName: null, capped: anyCapped, stale: anyStale };
|
|
158
232
|
}
|
|
159
233
|
}
|
|
160
234
|
|
|
161
|
-
return { content: null, loaded: false, fileName: null };
|
|
235
|
+
return { content: null, loaded: false, fileName: null, capped: false, stale: false };
|
|
162
236
|
}
|
|
163
237
|
|
|
164
238
|
async function main() {
|
|
@@ -181,6 +255,8 @@ async function main() {
|
|
|
181
255
|
const sections = [];
|
|
182
256
|
const loaded = [];
|
|
183
257
|
const skipped = [];
|
|
258
|
+
const cappedSteps = [];
|
|
259
|
+
const staleSteps = [];
|
|
184
260
|
let totalLines = 0;
|
|
185
261
|
|
|
186
262
|
// Sort steps by stepNumber
|
|
@@ -189,7 +265,7 @@ async function main() {
|
|
|
189
265
|
);
|
|
190
266
|
|
|
191
267
|
for (const [key, step] of stepEntries) {
|
|
192
|
-
const result = processStep(key, step, dates);
|
|
268
|
+
const result = processStep(key, step, dates, config);
|
|
193
269
|
|
|
194
270
|
if (result.loaded && result.content) {
|
|
195
271
|
const criticalTag = step.critical ? ' (CRITICAL)' : '';
|
|
@@ -198,6 +274,8 @@ async function main() {
|
|
|
198
274
|
sections.push(`${header}\n${result.content}`);
|
|
199
275
|
loaded.push(`Step ${step.stepNumber}: ${step.label}`);
|
|
200
276
|
totalLines += result.content.split('\n').length;
|
|
277
|
+
if (result.capped) cappedSteps.push(`Step ${step.stepNumber}`);
|
|
278
|
+
if (result.stale) staleSteps.push(`Step ${step.stepNumber}`);
|
|
201
279
|
} else {
|
|
202
280
|
skipped.push(`Step ${step.stepNumber}: ${step.label}`);
|
|
203
281
|
if (result.fileName) {
|
|
@@ -275,16 +353,28 @@ async function main() {
|
|
|
275
353
|
} catch {}
|
|
276
354
|
|
|
277
355
|
const elapsed = Date.now() - startTime;
|
|
278
|
-
const footer = `== Boot complete. Loaded ${loaded.length}
|
|
356
|
+
const footer = `== Boot complete. Loaded ${loaded.length} sections in ${elapsed}ms. ==`;
|
|
279
357
|
if (skipped.length > 0) {
|
|
280
358
|
sections.push(`${footer}\nSkipped: ${skipped.join(', ')}`);
|
|
281
359
|
} else {
|
|
282
360
|
sections.push(footer);
|
|
283
361
|
}
|
|
284
362
|
|
|
285
|
-
|
|
363
|
+
// Body first, then a one-line payload summary so future slowness triage is
|
|
364
|
+
// trivial: total bytes/lines injected, which steps were line-capped, and
|
|
365
|
+
// which were skipped as stale (path-only).
|
|
366
|
+
const bodyText =
|
|
286
367
|
`== LDM OS BOOT SEQUENCE (loaded automatically by SessionStart hook) ==\n\n` +
|
|
287
368
|
sections.join('\n\n');
|
|
369
|
+
const payloadBytes = Buffer.byteLength(bodyText, 'utf-8');
|
|
370
|
+
const payloadLines = bodyText.split('\n').length;
|
|
371
|
+
const summary =
|
|
372
|
+
`== Boot payload: ${payloadBytes} bytes, ${payloadLines} lines, ${loaded.length} sections. ` +
|
|
373
|
+
`Capped: ${cappedSteps.length ? cappedSteps.join(', ') : 'none'}. ` +
|
|
374
|
+
`Stale/path-only: ${staleSteps.length ? staleSteps.join(', ') : 'none'}. ` +
|
|
375
|
+
`Missing: ${skipped.length ? skipped.length : 'none'}. ==`;
|
|
376
|
+
|
|
377
|
+
const additionalContext = `${bodyText}\n\n${summary}`;
|
|
288
378
|
|
|
289
379
|
const output = {
|
|
290
380
|
hookSpecificOutput: {
|
|
@@ -294,7 +384,7 @@ async function main() {
|
|
|
294
384
|
};
|
|
295
385
|
|
|
296
386
|
process.stdout.write(JSON.stringify(output));
|
|
297
|
-
process.stderr.write(`${TAG} loaded ${loaded.length}
|
|
387
|
+
process.stderr.write(`${TAG} loaded ${loaded.length} sections, ${payloadBytes} bytes in ${elapsed}ms\n`);
|
|
298
388
|
process.exit(0);
|
|
299
389
|
}
|
|
300
390
|
|
package/src/boot/installer.mjs
CHANGED
|
@@ -142,13 +142,20 @@ export function configureSessionStartHook() {
|
|
|
142
142
|
if (!settings.hooks) settings.hooks = {};
|
|
143
143
|
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
const existingIdx = settings.hooks.SessionStart.findIndex((entry) => {
|
|
145
|
+
const isBootEntry = (entry) => {
|
|
147
146
|
const hooks = entry?.hooks;
|
|
148
147
|
if (!Array.isArray(hooks)) return false;
|
|
149
148
|
return hooks.some((h) =>
|
|
150
149
|
h?.command?.includes('boot-hook') || h?.command?.includes('shared/boot')
|
|
151
150
|
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Find ALL boot-hook entries. Duplicate registrations accumulate across
|
|
154
|
+
// installs (10 copies observed 2026-07-04, each running the boot script
|
|
155
|
+
// once per session), so this owns the whole set, not just the first.
|
|
156
|
+
const ownedIdxs = [];
|
|
157
|
+
settings.hooks.SessionStart.forEach((entry, i) => {
|
|
158
|
+
if (isBootEntry(entry)) ownedIdxs.push(i);
|
|
152
159
|
});
|
|
153
160
|
|
|
154
161
|
const hookEntry = {
|
|
@@ -160,16 +167,32 @@ export function configureSessionStartHook() {
|
|
|
160
167
|
}],
|
|
161
168
|
};
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
// Collapse duplicates beyond the first. Walk right-to-left so earlier
|
|
171
|
+
// indices stay valid while splicing.
|
|
172
|
+
let removed = 0;
|
|
173
|
+
for (let j = ownedIdxs.length - 1; j >= 1; j--) {
|
|
174
|
+
settings.hooks.SessionStart.splice(ownedIdxs[j], 1);
|
|
175
|
+
removed++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ownedIdxs.length > 0) {
|
|
179
|
+
const idx = ownedIdxs[0];
|
|
180
|
+
const identical =
|
|
181
|
+
JSON.stringify(settings.hooks.SessionStart[idx]) === JSON.stringify(hookEntry);
|
|
182
|
+
if (identical && removed === 0) {
|
|
183
|
+
// Nothing to change; do not touch the file.
|
|
184
|
+
return 'SessionStart hook already configured';
|
|
185
|
+
}
|
|
186
|
+
settings.hooks.SessionStart[idx] = hookEntry;
|
|
170
187
|
writeJSON(CC_SETTINGS, settings);
|
|
171
|
-
return
|
|
188
|
+
return removed > 0
|
|
189
|
+
? `SessionStart hook updated in settings.json (removed ${removed} duplicate entr${removed === 1 ? 'y' : 'ies'})`
|
|
190
|
+
: 'SessionStart hook updated in settings.json';
|
|
172
191
|
}
|
|
192
|
+
|
|
193
|
+
settings.hooks.SessionStart.push(hookEntry);
|
|
194
|
+
writeJSON(CC_SETTINGS, settings);
|
|
195
|
+
return 'SessionStart hook added to settings.json';
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
// ── Registry ──
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
# Kaleidoscope server environment
|
|
2
2
|
# Copy to .env and fill in values. Never commit .env itself.
|
|
3
3
|
DATABASE_URL=postgresql://kaleidoscope:YOUR_PASSWORD@localhost:5432/kaleidoscope
|
|
4
|
+
KALEIDOSCOPE_LIVE_WALL_FILE=/var/lib/wip/kaleidoscope-live-wall.json
|
|
5
|
+
KALEIDOSCOPE_LIVE_WALL_MEDIA_DIR=/var/www/wip.computer/public_html/media/kaleidoscope/generated
|
|
6
|
+
KALEIDOSCOPE_LIVE_WALL_MAX_IMAGE_BYTES=10485760
|
|
7
|
+
KALEIDOSCOPE_LIVE_WALL_FETCH_TIMEOUT_MS=15000
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Hosted MCP And Relay
|
|
2
|
+
|
|
3
|
+
This directory contains the public source for the hosted WIP relay that serves `wip.computer`.
|
|
4
|
+
|
|
5
|
+
It includes:
|
|
6
|
+
|
|
7
|
+
- OAuth, passkey, and hosted MCP routes in `server.mjs`;
|
|
8
|
+
- Codex Remote Control relay routes under `/api/codex-relay/*`;
|
|
9
|
+
- nginx snippets for the relay, MCP, and site proxy;
|
|
10
|
+
- Prisma schema and migrations for Postgres-backed account, API key, passkey, device, and wallet state;
|
|
11
|
+
- PM2 and deploy helpers for the WIP-operated VPS.
|
|
12
|
+
|
|
13
|
+
WIP runs the production hosted relay so user setup is easy and works across networks. The source is public so users can inspect the relay path and build their own infrastructure.
|
|
14
|
+
|
|
15
|
+
## Codex Remote Control WebSocket Abuse Limits
|
|
16
|
+
|
|
17
|
+
The browser relay path enforces app-layer limits after a Remote Control ticket attaches:
|
|
18
|
+
|
|
19
|
+
- max browser frame bytes;
|
|
20
|
+
- max messages per rate window;
|
|
21
|
+
- max browser bytes per rate window;
|
|
22
|
+
- max malformed browser frames;
|
|
23
|
+
- max pending bytes on the daemon socket before forwarding;
|
|
24
|
+
- max browser sockets per `(tenant id, thread id)`;
|
|
25
|
+
- idle connection TTL;
|
|
26
|
+
- env-driven operator kill switch for all tenants or selected tenant ids.
|
|
27
|
+
|
|
28
|
+
Violation logs are metadata-only: reason, tenant id, thread id, and a generated connection id. The relay does not inspect decrypted Remote Control payloads.
|
|
29
|
+
|
|
30
|
+
Operational notes:
|
|
31
|
+
|
|
32
|
+
- rate windows are tumbling windows, not sliding windows, so short bursts can straddle a window boundary;
|
|
33
|
+
- kill switch environment changes take effect after the hosted relay process reloads;
|
|
34
|
+
- idle close runs on a timer, so the close can happen up to one polling interval after the configured TTL;
|
|
35
|
+
- daemon-to-browser Codex output is intentionally not throughput-limited in this browser-abuse slice.
|
|
36
|
+
|
|
37
|
+
For the self-hosting shape, read [docs/self-host.md](docs/self-host.md).
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
+ '<p style="margin:2px 0 0;">Learning Dreaming Machines</p>'
|
|
23
23
|
+ '<p style="margin:8px 0 0;">Copyright © 2026 WIP Computer, Inc. All rights reserved.</p>'
|
|
24
24
|
+ '<p style="margin:4px 0 0;">'
|
|
25
|
-
+ '<a href="/legal/privacy/
|
|
25
|
+
+ '<a href="/legal/privacy/" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> | '
|
|
26
26
|
+ '<a href="/legal/internet-services/terms/site.html" style="color:#a8a4a0;text-decoration:none;">Terms of Use</a></p>'
|
|
27
27
|
+ '<p style="margin:4px 0 0;">'
|
|
28
28
|
+ '<a href="/agent.txt" style="color:#a8a4a0;text-decoration:none;">Are you an AI Agent?</a></p>'
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
inner.innerHTML = '<p style="margin:0;">WIP Computer, Inc.</p>'
|
|
32
32
|
+ '<p style="margin:2px 0 0;">Learning Dreaming Machines</p>'
|
|
33
33
|
+ '<p style="margin:8px 0 0;">Copyright © 2026 WIP Computer, Inc. All rights reserved. '
|
|
34
|
-
+ '<a href="/legal/privacy/
|
|
34
|
+
+ '<a href="/legal/privacy/" style="color:#a8a4a0;text-decoration:none;">Privacy Policy</a> | '
|
|
35
35
|
+ '<a href="/legal/internet-services/terms/site.html" style="color:#a8a4a0;text-decoration:none;">Terms of Use</a></p>'
|
|
36
36
|
+ '<p style="margin:4px 0 0;">'
|
|
37
37
|
+ '<a href="/agent.txt" style="color:#a8a4a0;text-decoration:none;">Are you an AI Agent?</a> | '
|