@pleri/olam-cli 0.1.174 → 0.1.180
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 +19 -0
- package/bin/olam.cjs +22 -0
- package/dist/commands/flywheel/index.d.ts.map +1 -1
- package/dist/commands/flywheel/index.js +4 -0
- package/dist/commands/flywheel/index.js.map +1 -1
- package/dist/commands/flywheel/install-sessionstart-hook.d.ts +64 -0
- package/dist/commands/flywheel/install-sessionstart-hook.d.ts.map +1 -0
- package/dist/commands/flywheel/install-sessionstart-hook.js +197 -0
- package/dist/commands/flywheel/install-sessionstart-hook.js.map +1 -0
- package/dist/commands/flywheel/session-start.d.ts +26 -0
- package/dist/commands/flywheel/session-start.d.ts.map +1 -0
- package/dist/commands/flywheel/session-start.js +119 -0
- package/dist/commands/flywheel/session-start.js.map +1 -0
- package/dist/commands/host-cp.d.ts +0 -3
- package/dist/commands/host-cp.d.ts.map +1 -1
- package/dist/commands/host-cp.js +27 -2
- package/dist/commands/host-cp.js.map +1 -1
- package/dist/commands/yolo.d.ts +99 -0
- package/dist/commands/yolo.d.ts.map +1 -0
- package/dist/commands/yolo.js +377 -0
- package/dist/commands/yolo.js.map +1 -0
- package/dist/image-digests.json +8 -8
- package/dist/index.js +808 -207
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-remote.d.ts +0 -17
- package/dist/lib/auth-remote.d.ts.map +1 -1
- package/dist/lib/auth-remote.js +6 -16
- package/dist/lib/auth-remote.js.map +1 -1
- package/dist/mcp-server.js +26 -0
- package/hermes-bundle/version.json +1 -1
- package/host-cp/k8s/manifests/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +1 -1
- package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +1 -1
- package/host-cp/src/bootstrap-selective.mjs +56 -0
- package/host-cp/src/plan-chat-service.mjs +51 -0
- package/host-cp/src/redirect.mjs +152 -0
- package/host-cp/src/resolver.mjs +121 -0
- package/host-cp/src/server.mjs +57 -16
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// resolver.mjs — Phase A A1 (plan-chat-spa-supersedes-control-plane).
|
|
2
|
+
//
|
|
3
|
+
// Disambiguates a single opaque :id supplied on /plan/:id between
|
|
4
|
+
//
|
|
5
|
+
// - a planning session (planning_sessions.session_id), or
|
|
6
|
+
// - a crystallized world (planning_artifacts.crystallized_world_id), or
|
|
7
|
+
// - unresolvable (returns {kind:'unresolved', canonical_id:null}).
|
|
8
|
+
//
|
|
9
|
+
// Used by plan-chat-spa's useResolveId hook (Phase A A2) so the SPA's
|
|
10
|
+
// cold-open path can mount the correct surface without trusting the
|
|
11
|
+
// id-shape (sentinel `sess_*` prefix is a hint, not authority — see
|
|
12
|
+
// plan-chat-spa-supersedes-control-plane.md K1 SEC-1).
|
|
13
|
+
//
|
|
14
|
+
// Single SQL query (UNION ALL) so resolution costs one round-trip even
|
|
15
|
+
// when the id misses both tables. Bearer auth + rate-limit live in the
|
|
16
|
+
// HTTP handler in plan-chat-service.mjs; this helper is pool-pure for
|
|
17
|
+
// unit testability.
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate the resolver :id shape. Mirrors plan-chat-service.mjs's
|
|
21
|
+
* SCOPE_ID_RE; tightened to 6-80 chars so an enumeration attacker can't
|
|
22
|
+
* grind through 1-5 char shapes.
|
|
23
|
+
*/
|
|
24
|
+
export const RESOLVE_ID_RE = /^[A-Za-z0-9._-]{6,80}$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve an opaque id against the chunks substrate.
|
|
28
|
+
*
|
|
29
|
+
* @param {{ query: (sql: string, params: unknown[]) => Promise<{ rows: unknown[] }> }} pool
|
|
30
|
+
* A pg-shaped pool. Tests pass a stub; production passes pg.Pool.
|
|
31
|
+
* @param {string} id The candidate id.
|
|
32
|
+
* @returns {Promise<{ kind: 'session' | 'world' | 'unresolved', canonical_id: string | null }>}
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveId(pool, id) {
|
|
35
|
+
if (typeof id !== 'string' || !RESOLVE_ID_RE.test(id)) {
|
|
36
|
+
return { kind: 'unresolved', canonical_id: null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Single round-trip. Both branches return the same shape
|
|
40
|
+
// (kind, canonical_id) so PG can UNION ALL them without coercion.
|
|
41
|
+
//
|
|
42
|
+
// Session branch wins on tie (LIMIT 1 + session ordered first) — a
|
|
43
|
+
// session id colliding with a world id is unlikely in practice
|
|
44
|
+
// (worldId is the random docker name; sessionId is uuid-shaped),
|
|
45
|
+
// but the deterministic ordering closes the K1 collision risk
|
|
46
|
+
// surfaced in pass 3 review.
|
|
47
|
+
const sql = `
|
|
48
|
+
SELECT kind, canonical_id FROM (
|
|
49
|
+
SELECT 'session' AS kind, session_id AS canonical_id, 1 AS rank
|
|
50
|
+
FROM planning_sessions
|
|
51
|
+
WHERE session_id = $1
|
|
52
|
+
UNION ALL
|
|
53
|
+
SELECT 'world' AS kind, crystallized_world_id AS canonical_id, 2 AS rank
|
|
54
|
+
FROM planning_artifacts
|
|
55
|
+
WHERE crystallized_world_id = $1
|
|
56
|
+
) AS resolved
|
|
57
|
+
ORDER BY rank
|
|
58
|
+
LIMIT 1
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const result = await pool.query(sql, [id]);
|
|
62
|
+
const row = result.rows && result.rows[0];
|
|
63
|
+
if (!row) return { kind: 'unresolved', canonical_id: null };
|
|
64
|
+
|
|
65
|
+
// Pool stub-friendly: tolerate column names emerging from pg's
|
|
66
|
+
// case-insensitive identifier handling.
|
|
67
|
+
const kind = row.kind ?? row.KIND;
|
|
68
|
+
const canonical_id = row.canonical_id ?? row.CANONICAL_ID;
|
|
69
|
+
if (kind !== 'session' && kind !== 'world') {
|
|
70
|
+
return { kind: 'unresolved', canonical_id: null };
|
|
71
|
+
}
|
|
72
|
+
if (typeof canonical_id !== 'string' || canonical_id.length === 0) {
|
|
73
|
+
return { kind: 'unresolved', canonical_id: null };
|
|
74
|
+
}
|
|
75
|
+
return { kind, canonical_id };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Token-bucket rate limiter, per bearer principal. Closes the brute-
|
|
80
|
+
* force enumeration vector that bearer auth alone leaves open (an
|
|
81
|
+
* authenticated caller could otherwise grind through ids at
|
|
82
|
+
* line-rate).
|
|
83
|
+
*
|
|
84
|
+
* 60 req/min per bearer. Single-process in-memory map (one host-cp
|
|
85
|
+
* per host); a multi-instance deployment would need a shared store,
|
|
86
|
+
* but plan-chat-service is single-tenant single-host by design.
|
|
87
|
+
*/
|
|
88
|
+
export function createRateLimiter({
|
|
89
|
+
capacity = 60,
|
|
90
|
+
windowMs = 60_000,
|
|
91
|
+
now = () => Date.now(),
|
|
92
|
+
} = {}) {
|
|
93
|
+
const buckets = new Map(); // key -> { tokens, lastRefill }
|
|
94
|
+
|
|
95
|
+
function take(key) {
|
|
96
|
+
const t = now();
|
|
97
|
+
let bucket = buckets.get(key);
|
|
98
|
+
if (!bucket) {
|
|
99
|
+
bucket = { tokens: capacity, lastRefill: t };
|
|
100
|
+
buckets.set(key, bucket);
|
|
101
|
+
}
|
|
102
|
+
// Refill proportional to elapsed time.
|
|
103
|
+
const elapsed = t - bucket.lastRefill;
|
|
104
|
+
if (elapsed > 0) {
|
|
105
|
+
const refill = (elapsed / windowMs) * capacity;
|
|
106
|
+
bucket.tokens = Math.min(capacity, bucket.tokens + refill);
|
|
107
|
+
bucket.lastRefill = t;
|
|
108
|
+
}
|
|
109
|
+
if (bucket.tokens < 1) {
|
|
110
|
+
return { allowed: false, retryAfterMs: Math.ceil((1 - bucket.tokens) * (windowMs / capacity)) };
|
|
111
|
+
}
|
|
112
|
+
bucket.tokens -= 1;
|
|
113
|
+
return { allowed: true, retryAfterMs: 0 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function reset() {
|
|
117
|
+
buckets.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { take, reset };
|
|
121
|
+
}
|
package/host-cp/src/server.mjs
CHANGED
|
@@ -48,7 +48,9 @@ import {
|
|
|
48
48
|
import { betaResponseEmitter } from '@olam/auth-client';
|
|
49
49
|
import { attemptRecovery, findScenarioForKind } from '../recovery/index.mjs';
|
|
50
50
|
import { detectHaltChunk } from './halt-detect.mjs';
|
|
51
|
+
import { evaluateRedirect, applyRedirect } from './redirect.mjs';
|
|
51
52
|
import { spawnUpgraderContainer } from './upgrade-spawner.mjs';
|
|
53
|
+
import { isPlanningPath } from './bootstrap-selective.mjs';
|
|
52
54
|
import { parseProxyPath, perWorldBase, proxyToWorld } from './proxy.mjs';
|
|
53
55
|
import { resolveHostCpEngine } from './engine-identity.mjs';
|
|
54
56
|
import { StartupToken } from './auth.mjs';
|
|
@@ -896,6 +898,18 @@ const server = http.createServer(instrumentHandler('host-cp', async (req, res) =
|
|
|
896
898
|
});
|
|
897
899
|
}
|
|
898
900
|
|
|
901
|
+
// Phase B3 (plan-chat-spa-supersedes-control-plane): 301 redirect layer.
|
|
902
|
+
// Runs BEFORE static-serve so legacy `/world/:id` catch-all URLs that
|
|
903
|
+
// would otherwise be served as the SPA shell (and then 404 inside the
|
|
904
|
+
// SPA router after Phase B4 deletes the route) get redirected to
|
|
905
|
+
// their canonical successor. Allow-listed; closed set; security
|
|
906
|
+
// gated against SEC-2 (no caller-controlled Location, regex-validated
|
|
907
|
+
// ids, hardcoded prefixes). See packages/host-cp/src/redirect.mjs.
|
|
908
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
909
|
+
const redirectVerdict = evaluateRedirect(url.pathname);
|
|
910
|
+
if (applyRedirect(res, redirectVerdict)) return;
|
|
911
|
+
}
|
|
912
|
+
|
|
899
913
|
// Phase F-2-D dogfood fix: serve the SPA dist/ for non-API GET requests
|
|
900
914
|
// BEFORE auth gate. The SPA itself is the auth gate — it loads, fetches
|
|
901
915
|
// /api/bootstrap (unauthed), sets the cookie, then makes authed API calls.
|
|
@@ -3032,7 +3046,7 @@ async function tryServeStatic(req, res, pathname) {
|
|
|
3032
3046
|
// Without this the SPA loads but every fetch 401s and the operator
|
|
3033
3047
|
// sees "Could not load worlds — HTTP 401".
|
|
3034
3048
|
if (isSpaShell) {
|
|
3035
|
-
const html = await renderSpaShell(filePath);
|
|
3049
|
+
const html = await renderSpaShell(filePath, pathname);
|
|
3036
3050
|
res.writeHead(200, {
|
|
3037
3051
|
'Content-Type': 'text/html; charset=utf-8',
|
|
3038
3052
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
@@ -3062,11 +3076,12 @@ async function tryServeStatic(req, res, pathname) {
|
|
|
3062
3076
|
return true;
|
|
3063
3077
|
}
|
|
3064
3078
|
|
|
3065
|
-
// Memoized injected SPA
|
|
3066
|
-
// memory thereafter. Cache invalidates
|
|
3067
|
-
// rebuilt bundle is picked up without
|
|
3068
|
-
|
|
3069
|
-
|
|
3079
|
+
// Memoized injected SPA shells. Read once per (mtime, bearer-len,
|
|
3080
|
+
// bootstrap-bit) tuple; serve from memory thereafter. Cache invalidates
|
|
3081
|
+
// on dist/ mtime change so a rebuilt bundle is picked up without
|
|
3082
|
+
// restart. Phase D1 — keyed map (not single slot) so /plan and
|
|
3083
|
+
// /workspaces don't trash each other's cached HTML.
|
|
3084
|
+
const _spaCacheByKey = new Map();
|
|
3070
3085
|
|
|
3071
3086
|
/**
|
|
3072
3087
|
* Bootstrap script injected into the SPA shell. Two responsibilities:
|
|
@@ -3166,14 +3181,37 @@ function buildPlanChatBearerInjection() {
|
|
|
3166
3181
|
}
|
|
3167
3182
|
}
|
|
3168
3183
|
|
|
3169
|
-
|
|
3184
|
+
// Phase D1 — Selective BOOTSTRAP_SCRIPT no-op.
|
|
3185
|
+
//
|
|
3186
|
+
// Planning paths use plan-chat-spa's own readBearer() resolver
|
|
3187
|
+
// (lib/bearer.ts) which reads window.__OLAM_PLAN_CHAT_BEARER__ injected
|
|
3188
|
+
// inline OR falls back to the URL hash channel. They DO NOT need the
|
|
3189
|
+
// host-cp bootstrap's cookie+fetch-rewrite shim. Non-planning surfaces
|
|
3190
|
+
// (/workspaces, /repos, /runbooks, /design, /inbox, /world/:id/editor,
|
|
3191
|
+
// /world/:id/events) still rely on bootstrap-injected cookie + the
|
|
3192
|
+
// monkey-patched fetch/EventSource that rewrites world-scoped paths
|
|
3193
|
+
// to /api/world/<id>/... — keep injecting for them until Phase E
|
|
3194
|
+
// migrates each to a bootstrap-free pattern.
|
|
3195
|
+
//
|
|
3196
|
+
// Reversal: edit BOOTSTRAP_NOOP_PLANNING_PATHS in bootstrap-selective.mjs
|
|
3197
|
+
// to [] to restore universal injection. Single-line change.
|
|
3198
|
+
//
|
|
3199
|
+
// Per K1 SCP-3 + phase-d-tasks D1 acceptance.
|
|
3200
|
+
|
|
3201
|
+
async function renderSpaShell(filePath, pathname) {
|
|
3170
3202
|
const stat = fs.statSync(filePath);
|
|
3171
3203
|
const bearerInjection = buildPlanChatBearerInjection();
|
|
3172
|
-
//
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3204
|
+
// Path-selective: planning paths skip the bootstrap shim entirely
|
|
3205
|
+
// (plan-chat-spa's readBearer handles auth); non-planning paths
|
|
3206
|
+
// retain it (Phase E will migrate them).
|
|
3207
|
+
const skipBootstrap = isPlanningPath(pathname);
|
|
3208
|
+
const bootstrapPart = skipBootstrap ? '' : BOOTSTRAP_SCRIPT;
|
|
3209
|
+
// Cache key includes bearer length AND the bootstrap-presence bit so
|
|
3210
|
+
// /plan and /workspaces don't share a cached shell.
|
|
3211
|
+
const cacheKey =
|
|
3212
|
+
stat.mtimeMs + ':' + bearerInjection.length + ':' + (skipBootstrap ? '0' : '1');
|
|
3213
|
+
const cached = _spaCacheByKey.get(cacheKey);
|
|
3214
|
+
if (cached !== undefined) return cached;
|
|
3177
3215
|
let html = fs.readFileSync(filePath, 'utf-8');
|
|
3178
3216
|
// Vite emits relative asset paths (`./assets/...`) so the SPA bundle
|
|
3179
3217
|
// is portable across deploy paths. But under host-cp's path-segment
|
|
@@ -3184,10 +3222,13 @@ async function renderSpaShell(filePath) {
|
|
|
3184
3222
|
// Inject right after <head> so the bootstrap runs before any other
|
|
3185
3223
|
// script tag on the page. Bearer injection runs after the host-cp
|
|
3186
3224
|
// bootstrap so window.__OLAM_PLAN_CHAT_BEARER__ is set before the
|
|
3187
|
-
// SPA bundle reads it.
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3225
|
+
// SPA bundle reads it. On planning paths the bootstrap is empty —
|
|
3226
|
+
// bearer injection still runs (plan-chat-spa reads it directly).
|
|
3227
|
+
html = html.replace(
|
|
3228
|
+
/<head>/i,
|
|
3229
|
+
`<head>\n ${bootstrapPart}\n ${bearerInjection}`,
|
|
3230
|
+
);
|
|
3231
|
+
_spaCacheByKey.set(cacheKey, html);
|
|
3191
3232
|
return html;
|
|
3192
3233
|
}
|
|
3193
3234
|
|