@wipcomputer/wip-ldm-os 0.4.85-alpha.9 → 0.4.87-alpha.1
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 +976 -75
- package/lib/deploy.mjs +36 -1
- 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-commit-deployed.mjs +300 -0
- package/scripts/test-doctor-hook-dedupe.mjs +188 -0
- package/scripts/test-install-prompt-policy.mjs +84 -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
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
<!-- View 3: success -->
|
|
75
75
|
<div id="success-view" style="display: none;">
|
|
76
76
|
<h1>Paired.</h1>
|
|
77
|
-
<div class="sub">Your laptop will pick this up in a few seconds.</div>
|
|
77
|
+
<div id="success-sub" class="sub">Your laptop will pick this up in a few seconds.</div>
|
|
78
78
|
<div class="footer">You can close this tab.</div>
|
|
79
79
|
</div>
|
|
80
80
|
</div>
|
|
@@ -86,6 +86,7 @@ var PAIR_CODE_REGEX = /^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]{6}$/;
|
|
|
86
86
|
|
|
87
87
|
function getApiKey() { return sessionStorage.getItem('wip_api_key'); }
|
|
88
88
|
function getHandle() { return sessionStorage.getItem('wip_handle') || '(unknown)'; }
|
|
89
|
+
function getPairPresenceToken() { return sessionStorage.getItem('wip_codex_pair_presence_token'); }
|
|
89
90
|
|
|
90
91
|
function setStatus(elId, msg, kind) {
|
|
91
92
|
var el = document.getElementById(elId);
|
|
@@ -104,7 +105,9 @@ function readCodeFromPath() {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
// POST the code to /api/codex-relay/pair-complete using the api_key
|
|
107
|
-
// from sessionStorage as Bearer token.
|
|
108
|
+
// from sessionStorage as Bearer token. Daemon key pairing also sends the
|
|
109
|
+
// short-lived token minted by the passkey verify step, so a stale ck- token
|
|
110
|
+
// alone cannot replace a daemon. On success, show the success view.
|
|
108
111
|
async function submitPair(code, statusElId) {
|
|
109
112
|
if (!PAIR_CODE_REGEX.test(code)) {
|
|
110
113
|
setStatus(statusElId, 'That does not look like a valid code.', 'error');
|
|
@@ -118,7 +121,10 @@ async function submitPair(code, statusElId) {
|
|
|
118
121
|
'Content-Type': 'application/json',
|
|
119
122
|
'Authorization': 'Bearer ' + getApiKey(),
|
|
120
123
|
},
|
|
121
|
-
body: JSON.stringify({
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
code: code,
|
|
126
|
+
codex_pair_presence_token: getPairPresenceToken(),
|
|
127
|
+
}),
|
|
122
128
|
});
|
|
123
129
|
var data = await res.json();
|
|
124
130
|
if (!res.ok) {
|
|
@@ -127,9 +133,21 @@ async function submitPair(code, statusElId) {
|
|
|
127
133
|
if (res.status === 410 || res.status === 404) {
|
|
128
134
|
msg += ' Run codex-daemon link again on your laptop to get a fresh code.';
|
|
129
135
|
}
|
|
136
|
+
if (res.status === 403 && data && data.error === 'fresh_presence_required') {
|
|
137
|
+
msg = 'Sign in again with your passkey to relink this daemon.';
|
|
138
|
+
sessionStorage.removeItem('wip_api_key');
|
|
139
|
+
sessionStorage.removeItem('wip_codex_pair_presence_token');
|
|
140
|
+
setTimeout(function() {
|
|
141
|
+
location.replace('/login?next=' + encodeURIComponent('/pair/' + code));
|
|
142
|
+
}, 1200);
|
|
143
|
+
}
|
|
130
144
|
setStatus(statusElId, msg, 'error');
|
|
131
145
|
return false;
|
|
132
146
|
}
|
|
147
|
+
sessionStorage.removeItem('wip_codex_pair_presence_token');
|
|
148
|
+
document.getElementById('success-sub').textContent = data.replaced_daemon_key
|
|
149
|
+
? 'Remote Control relinked this laptop. Existing browser control tabs need to reconnect.'
|
|
150
|
+
: 'Your laptop will pick this up in a few seconds.';
|
|
133
151
|
document.getElementById('confirm-view').style.display = 'none';
|
|
134
152
|
document.getElementById('manual-view').style.display = 'none';
|
|
135
153
|
document.getElementById('success-view').style.display = 'block';
|
|
Binary file
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function normalizeCodexCryptoVersions(versions) {
|
|
4
|
+
const out = Array.isArray(versions)
|
|
5
|
+
? versions.filter((v) => typeof v === "string" && v.length > 0 && v.length <= 32).slice(0, 8)
|
|
6
|
+
: [];
|
|
7
|
+
return out.length ? out : ["e2ee-v1"];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function codexDaemonPubkeyFingerprint(pubkey) {
|
|
11
|
+
if (typeof pubkey !== "string" || !pubkey) return null;
|
|
12
|
+
return "sha256:" + createHash("sha256").update(pubkey).digest("base64url").slice(0, 16);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function evaluateCodexDaemonReconnectPubkey(existingKey, incomingPubkey) {
|
|
16
|
+
const existingPubkey = typeof existingKey?.pubkey === "string" && existingKey.pubkey ? existingKey.pubkey : null;
|
|
17
|
+
const nextPubkey = typeof incomingPubkey === "string" && incomingPubkey && incomingPubkey.length <= 1024 ? incomingPubkey : null;
|
|
18
|
+
const oldFingerprint = codexDaemonPubkeyFingerprint(existingPubkey);
|
|
19
|
+
const newFingerprint = codexDaemonPubkeyFingerprint(nextPubkey);
|
|
20
|
+
|
|
21
|
+
if (!nextPubkey) {
|
|
22
|
+
return {
|
|
23
|
+
allowed: false,
|
|
24
|
+
reason: "invalid_daemon_pubkey",
|
|
25
|
+
replaced: false,
|
|
26
|
+
old_fingerprint: oldFingerprint,
|
|
27
|
+
new_fingerprint: newFingerprint,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!existingPubkey || existingPubkey === nextPubkey) {
|
|
31
|
+
return {
|
|
32
|
+
allowed: true,
|
|
33
|
+
reason: null,
|
|
34
|
+
replaced: false,
|
|
35
|
+
old_fingerprint: oldFingerprint,
|
|
36
|
+
new_fingerprint: newFingerprint,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
allowed: false,
|
|
41
|
+
reason: "fresh_pair_required",
|
|
42
|
+
replaced: true,
|
|
43
|
+
old_fingerprint: oldFingerprint,
|
|
44
|
+
new_fingerprint: newFingerprint,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildCodexBootstrapPayload({ identity, threadId, daemonOnline, daemonKey }) {
|
|
49
|
+
return {
|
|
50
|
+
handle: identity.handle,
|
|
51
|
+
thread_id: threadId,
|
|
52
|
+
daemon_online: daemonOnline,
|
|
53
|
+
daemon_public_key: daemonKey ? daemonKey.pubkey : null,
|
|
54
|
+
daemon_crypto_versions: daemonKey ? daemonKey.crypto_versions : null,
|
|
55
|
+
supported_crypto_versions: ["e2ee-v1"],
|
|
56
|
+
e2ee_available: !!daemonKey,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createCodexDaemonPubkeyRegistry({
|
|
61
|
+
usePrisma,
|
|
62
|
+
prisma,
|
|
63
|
+
devMode = false,
|
|
64
|
+
logger = console,
|
|
65
|
+
} = {}) {
|
|
66
|
+
const pubkeys = new Map();
|
|
67
|
+
const auditLog = [];
|
|
68
|
+
|
|
69
|
+
async function ensureStore() {
|
|
70
|
+
if (!usePrisma) return;
|
|
71
|
+
await prisma.$executeRawUnsafe(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS codex_daemon_e2ee_keys (
|
|
73
|
+
tenant_id TEXT PRIMARY KEY,
|
|
74
|
+
pubkey TEXT NOT NULL,
|
|
75
|
+
crypto_versions_json TEXT NOT NULL,
|
|
76
|
+
registered_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function ensureAuditStore() {
|
|
82
|
+
if (!usePrisma) return;
|
|
83
|
+
await prisma.$executeRawUnsafe(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS codex_daemon_e2ee_key_audit (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
tenant_id TEXT NOT NULL,
|
|
87
|
+
source TEXT NOT NULL,
|
|
88
|
+
old_pubkey_fingerprint TEXT,
|
|
89
|
+
new_pubkey_fingerprint TEXT,
|
|
90
|
+
replaced BOOLEAN NOT NULL,
|
|
91
|
+
registered_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
92
|
+
)
|
|
93
|
+
`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function loadFromDb() {
|
|
97
|
+
if (!usePrisma) return;
|
|
98
|
+
try {
|
|
99
|
+
await ensureStore();
|
|
100
|
+
const rows = await prisma.$queryRawUnsafe(`
|
|
101
|
+
SELECT tenant_id, pubkey, crypto_versions_json, registered_at
|
|
102
|
+
FROM codex_daemon_e2ee_keys
|
|
103
|
+
`);
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
let cryptoVersions = ["e2ee-v1"];
|
|
106
|
+
try { cryptoVersions = normalizeCodexCryptoVersions(JSON.parse(row.crypto_versions_json)); } catch {}
|
|
107
|
+
pubkeys.set(row.tenant_id, {
|
|
108
|
+
pubkey: row.pubkey,
|
|
109
|
+
crypto_versions: cryptoVersions,
|
|
110
|
+
registered_at: row.registered_at instanceof Date ? row.registered_at.toISOString() : String(row.registered_at),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
logger.log("codex-relay: loaded " + rows.length + " persisted E2EE daemon pubkey(s)");
|
|
114
|
+
} catch (err) {
|
|
115
|
+
logger.error("codex-relay: failed to load persisted E2EE daemon pubkeys:", err.message);
|
|
116
|
+
if (!devMode) process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function persist(agentId, pubkey, cryptoVersions) {
|
|
121
|
+
if (!usePrisma) return;
|
|
122
|
+
await ensureStore();
|
|
123
|
+
await prisma.$executeRawUnsafe(
|
|
124
|
+
`INSERT INTO codex_daemon_e2ee_keys
|
|
125
|
+
(tenant_id, pubkey, crypto_versions_json, registered_at)
|
|
126
|
+
VALUES ($1, $2, $3, now())
|
|
127
|
+
ON CONFLICT (tenant_id)
|
|
128
|
+
DO UPDATE SET
|
|
129
|
+
pubkey = EXCLUDED.pubkey,
|
|
130
|
+
crypto_versions_json = EXCLUDED.crypto_versions_json,
|
|
131
|
+
registered_at = EXCLUDED.registered_at`,
|
|
132
|
+
agentId,
|
|
133
|
+
pubkey,
|
|
134
|
+
JSON.stringify(cryptoVersions),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function recordAudit(agentId, previousPubkey, nextPubkey, source, registeredAt) {
|
|
139
|
+
const oldFingerprint = codexDaemonPubkeyFingerprint(previousPubkey);
|
|
140
|
+
const newFingerprint = codexDaemonPubkeyFingerprint(nextPubkey);
|
|
141
|
+
const replaced = !!(previousPubkey && previousPubkey !== nextPubkey);
|
|
142
|
+
const entry = {
|
|
143
|
+
tenant_id: agentId,
|
|
144
|
+
source,
|
|
145
|
+
old_pubkey_fingerprint: oldFingerprint,
|
|
146
|
+
new_pubkey_fingerprint: newFingerprint,
|
|
147
|
+
replaced,
|
|
148
|
+
registered_at: registeredAt,
|
|
149
|
+
};
|
|
150
|
+
auditLog.push(entry);
|
|
151
|
+
if (!usePrisma) return;
|
|
152
|
+
await ensureAuditStore();
|
|
153
|
+
await prisma.$executeRawUnsafe(
|
|
154
|
+
`INSERT INTO codex_daemon_e2ee_key_audit
|
|
155
|
+
(id, tenant_id, source, old_pubkey_fingerprint, new_pubkey_fingerprint, replaced, registered_at)
|
|
156
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7::timestamptz)`,
|
|
157
|
+
randomUUID(),
|
|
158
|
+
agentId,
|
|
159
|
+
source,
|
|
160
|
+
oldFingerprint,
|
|
161
|
+
newFingerprint,
|
|
162
|
+
replaced,
|
|
163
|
+
registeredAt,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function register(agentId, pubkey, cryptoVersions, source) {
|
|
168
|
+
if (typeof agentId !== "string" || !agentId) return Promise.resolve(false);
|
|
169
|
+
if (typeof pubkey !== "string" || !pubkey || pubkey.length > 1024) return Promise.resolve(false);
|
|
170
|
+
const previous = pubkeys.get(agentId) || null;
|
|
171
|
+
const normalizedVersions = normalizeCodexCryptoVersions(cryptoVersions);
|
|
172
|
+
const registeredAt = new Date().toISOString();
|
|
173
|
+
pubkeys.set(agentId, {
|
|
174
|
+
pubkey,
|
|
175
|
+
crypto_versions: normalizedVersions,
|
|
176
|
+
registered_at: registeredAt,
|
|
177
|
+
});
|
|
178
|
+
logger.log("codex-relay: registered E2EE pubkey for " + agentId + " via " + source);
|
|
179
|
+
return persist(agentId, pubkey, normalizedVersions)
|
|
180
|
+
.then(() => recordAudit(agentId, previous?.pubkey || null, pubkey, source, registeredAt))
|
|
181
|
+
.then(() => ({
|
|
182
|
+
registered: true,
|
|
183
|
+
replaced: !!(previous?.pubkey && previous.pubkey !== pubkey),
|
|
184
|
+
old_fingerprint: codexDaemonPubkeyFingerprint(previous?.pubkey || null),
|
|
185
|
+
new_fingerprint: codexDaemonPubkeyFingerprint(pubkey),
|
|
186
|
+
}))
|
|
187
|
+
.catch((err) => {
|
|
188
|
+
logger.error("codex-relay: failed to persist E2EE pubkey for " + agentId + ":", err.message);
|
|
189
|
+
if (!devMode) throw err;
|
|
190
|
+
return false;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
pubkeys,
|
|
196
|
+
auditLog,
|
|
197
|
+
ensureStore,
|
|
198
|
+
ensureAuditStore,
|
|
199
|
+
loadFromDb,
|
|
200
|
+
register,
|
|
201
|
+
get(agentId) {
|
|
202
|
+
return pubkeys.get(agentId) || null;
|
|
203
|
+
},
|
|
204
|
+
clearMemoryForTest() {
|
|
205
|
+
pubkeys.clear();
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const DEFAULTS = {
|
|
2
|
+
maxFrameBytes: 256 * 1024,
|
|
3
|
+
rateWindowMs: 10_000,
|
|
4
|
+
maxMessagesPerWindow: 120,
|
|
5
|
+
maxBytesPerWindow: 1024 * 1024,
|
|
6
|
+
maxBrowserSocketsPerThread: 8,
|
|
7
|
+
idleTtlMs: 30 * 60 * 1000,
|
|
8
|
+
maxMalformedFrames: 3,
|
|
9
|
+
maxPendingBytes: 1024 * 1024,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const CODEX_WS_CLOSE_CODES = Object.freeze({
|
|
13
|
+
oversizedFrame: 4400,
|
|
14
|
+
rateLimited: 4401,
|
|
15
|
+
tooManySockets: 4402,
|
|
16
|
+
idleTimeout: 4403,
|
|
17
|
+
malformedFrames: 4404,
|
|
18
|
+
operatorDisabled: 4405,
|
|
19
|
+
pendingBytes: 4406,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function positiveInt(value, fallback) {
|
|
23
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
24
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseAgentSet(value) {
|
|
28
|
+
if (!value) return new Set();
|
|
29
|
+
return new Set(
|
|
30
|
+
String(value)
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((part) => part.trim())
|
|
33
|
+
.filter(Boolean),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createCodexWsAbuseLimitConfig(env = process.env) {
|
|
38
|
+
return {
|
|
39
|
+
maxFrameBytes: positiveInt(env.LDM_CODEX_WS_MAX_FRAME_BYTES, DEFAULTS.maxFrameBytes),
|
|
40
|
+
rateWindowMs: positiveInt(env.LDM_CODEX_WS_RATE_WINDOW_MS, DEFAULTS.rateWindowMs),
|
|
41
|
+
maxMessagesPerWindow: positiveInt(env.LDM_CODEX_WS_MAX_MESSAGES_PER_WINDOW, DEFAULTS.maxMessagesPerWindow),
|
|
42
|
+
maxBytesPerWindow: positiveInt(env.LDM_CODEX_WS_MAX_BYTES_PER_WINDOW, DEFAULTS.maxBytesPerWindow),
|
|
43
|
+
maxBrowserSocketsPerThread: positiveInt(
|
|
44
|
+
env.LDM_CODEX_WS_MAX_BROWSER_SOCKETS_PER_THREAD,
|
|
45
|
+
DEFAULTS.maxBrowserSocketsPerThread,
|
|
46
|
+
),
|
|
47
|
+
idleTtlMs: positiveInt(env.LDM_CODEX_WS_IDLE_TTL_MS, DEFAULTS.idleTtlMs),
|
|
48
|
+
maxMalformedFrames: positiveInt(env.LDM_CODEX_WS_MAX_MALFORMED_FRAMES, DEFAULTS.maxMalformedFrames),
|
|
49
|
+
maxPendingBytes: positiveInt(env.LDM_CODEX_WS_MAX_PENDING_BYTES, DEFAULTS.maxPendingBytes),
|
|
50
|
+
killSwitchAll: env.LDM_CODEX_WS_KILL_SWITCH_ALL === "1",
|
|
51
|
+
killSwitchAgents: parseAgentSet(env.LDM_CODEX_WS_KILL_SWITCH_AGENTS),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isCodexWsAgentDisabled(config, agentId) {
|
|
56
|
+
return !!(
|
|
57
|
+
config?.killSwitchAll
|
|
58
|
+
|| (typeof agentId === "string" && config?.killSwitchAgents?.has(agentId))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rejected(code, reason) {
|
|
63
|
+
return { ok: false, code, reason };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createCodexWsConnectionGuard({ config, agentId, now = Date.now }) {
|
|
67
|
+
let windowStartMs = now();
|
|
68
|
+
let messagesInWindow = 0;
|
|
69
|
+
let bytesInWindow = 0;
|
|
70
|
+
let malformedFrames = 0;
|
|
71
|
+
let lastActivityMs = windowStartMs;
|
|
72
|
+
|
|
73
|
+
function resetWindow(nowMs) {
|
|
74
|
+
windowStartMs = nowMs;
|
|
75
|
+
messagesInWindow = 0;
|
|
76
|
+
bytesInWindow = 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
observeFrame(byteLength, nowMs = now()) {
|
|
81
|
+
if (isCodexWsAgentDisabled(config, agentId)) {
|
|
82
|
+
return rejected(CODEX_WS_CLOSE_CODES.operatorDisabled, "operator disabled");
|
|
83
|
+
}
|
|
84
|
+
if (byteLength > config.maxFrameBytes) {
|
|
85
|
+
return rejected(CODEX_WS_CLOSE_CODES.oversizedFrame, "frame too large");
|
|
86
|
+
}
|
|
87
|
+
if (nowMs - windowStartMs > config.rateWindowMs) {
|
|
88
|
+
resetWindow(nowMs);
|
|
89
|
+
}
|
|
90
|
+
messagesInWindow += 1;
|
|
91
|
+
bytesInWindow += byteLength;
|
|
92
|
+
lastActivityMs = nowMs;
|
|
93
|
+
if (messagesInWindow > config.maxMessagesPerWindow) {
|
|
94
|
+
return rejected(CODEX_WS_CLOSE_CODES.rateLimited, "message rate limit");
|
|
95
|
+
}
|
|
96
|
+
if (bytesInWindow > config.maxBytesPerWindow) {
|
|
97
|
+
return rejected(CODEX_WS_CLOSE_CODES.rateLimited, "byte rate limit");
|
|
98
|
+
}
|
|
99
|
+
return { ok: true };
|
|
100
|
+
},
|
|
101
|
+
observeMalformed(nowMs = now()) {
|
|
102
|
+
lastActivityMs = nowMs;
|
|
103
|
+
malformedFrames += 1;
|
|
104
|
+
if (malformedFrames > config.maxMalformedFrames) {
|
|
105
|
+
return rejected(CODEX_WS_CLOSE_CODES.malformedFrames, "malformed frame limit");
|
|
106
|
+
}
|
|
107
|
+
return { ok: true };
|
|
108
|
+
},
|
|
109
|
+
observePendingBytes(bufferedAmount) {
|
|
110
|
+
if (bufferedAmount > config.maxPendingBytes) {
|
|
111
|
+
return rejected(CODEX_WS_CLOSE_CODES.pendingBytes, "pending byte limit");
|
|
112
|
+
}
|
|
113
|
+
return { ok: true };
|
|
114
|
+
},
|
|
115
|
+
observeIdle(nowMs = now()) {
|
|
116
|
+
if (nowMs - lastActivityMs > config.idleTtlMs) {
|
|
117
|
+
return rejected(CODEX_WS_CLOSE_CODES.idleTimeout, "idle timeout");
|
|
118
|
+
}
|
|
119
|
+
return { ok: true };
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function codexWsFrameByteLength(data) {
|
|
125
|
+
if (typeof data === "string") return Buffer.byteLength(data);
|
|
126
|
+
if (Buffer.isBuffer(data)) return data.length;
|
|
127
|
+
if (data instanceof ArrayBuffer) return data.byteLength;
|
|
128
|
+
if (ArrayBuffer.isView(data)) return data.byteLength;
|
|
129
|
+
return Buffer.byteLength(String(data ?? ""));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function formatCodexWsLimitLog({ agentId, threadId, connectionId, reason }) {
|
|
133
|
+
return (
|
|
134
|
+
"codex-relay: websocket limit"
|
|
135
|
+
+ " reason=" + String(reason || "unknown").slice(0, 64)
|
|
136
|
+
+ " agent=" + String(agentId || "<none>").slice(0, 96)
|
|
137
|
+
+ " thread=" + String(threadId || "<none>").slice(0, 96)
|
|
138
|
+
+ " conn=" + String(connectionId || "<none>").slice(0, 64)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -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> | '
|