@pleri/olam-cli 0.1.144 → 0.1.146
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/dist/commands/doctor.d.ts +53 -23
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +117 -46
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/logs.d.ts +17 -3
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +38 -35
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/memory/bridge.d.ts +57 -0
- package/dist/commands/memory/bridge.d.ts.map +1 -0
- package/dist/commands/memory/bridge.js +156 -0
- package/dist/commands/memory/bridge.js.map +1 -0
- package/dist/commands/memory/index.d.ts +3 -0
- package/dist/commands/memory/index.d.ts.map +1 -1
- package/dist/commands/memory/index.js +10 -1
- package/dist/commands/memory/index.js.map +1 -1
- package/dist/commands/memory/reclassify.d.ts +56 -0
- package/dist/commands/memory/reclassify.d.ts.map +1 -0
- package/dist/commands/memory/reclassify.js +177 -0
- package/dist/commands/memory/reclassify.js.map +1 -0
- package/dist/commands/memory/stats.d.ts +69 -0
- package/dist/commands/memory/stats.d.ts.map +1 -0
- package/dist/commands/memory/stats.js +164 -0
- package/dist/commands/memory/stats.js.map +1 -0
- package/dist/commands/skills-doctor.d.ts +14 -0
- package/dist/commands/skills-doctor.d.ts.map +1 -0
- package/dist/commands/skills-doctor.js +126 -0
- package/dist/commands/skills-doctor.js.map +1 -0
- package/dist/commands/skills-hook.d.ts +19 -0
- package/dist/commands/skills-hook.d.ts.map +1 -0
- package/dist/commands/skills-hook.js +99 -0
- package/dist/commands/skills-hook.js.map +1 -0
- package/dist/commands/skills-migrate-back.d.ts +21 -0
- package/dist/commands/skills-migrate-back.d.ts.map +1 -0
- package/dist/commands/skills-migrate-back.js +222 -0
- package/dist/commands/skills-migrate-back.js.map +1 -0
- package/dist/commands/skills-migrate.d.ts +33 -0
- package/dist/commands/skills-migrate.d.ts.map +1 -0
- package/dist/commands/skills-migrate.js +216 -0
- package/dist/commands/skills-migrate.js.map +1 -0
- package/dist/commands/skills-onboard.d.ts +26 -0
- package/dist/commands/skills-onboard.d.ts.map +1 -0
- package/dist/commands/skills-onboard.js +227 -0
- package/dist/commands/skills-onboard.js.map +1 -0
- package/dist/commands/skills-shadow-backups.d.ts +15 -0
- package/dist/commands/skills-shadow-backups.d.ts.map +1 -0
- package/dist/commands/skills-shadow-backups.js +132 -0
- package/dist/commands/skills-shadow-backups.js.map +1 -0
- package/dist/commands/skills-source.d.ts +37 -0
- package/dist/commands/skills-source.d.ts.map +1 -0
- package/dist/commands/skills-source.js +431 -0
- package/dist/commands/skills-source.js.map +1 -0
- package/dist/commands/skills.d.ts +11 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +170 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.d.ts +27 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +102 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/substrate-audit-log.d.ts +49 -0
- package/dist/commands/substrate-audit-log.d.ts.map +1 -0
- package/dist/commands/substrate-audit-log.js +148 -0
- package/dist/commands/substrate-audit-log.js.map +1 -0
- package/dist/commands/substrate.d.ts +60 -0
- package/dist/commands/substrate.d.ts.map +1 -0
- package/dist/commands/substrate.js +175 -0
- package/dist/commands/substrate.js.map +1 -0
- package/dist/commands/upgrade.d.ts +10 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +30 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/image-digests.json +7 -7
- package/dist/index.js +8394 -3876
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +69 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +146 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/health-probes.d.ts +72 -0
- package/dist/lib/health-probes.d.ts.map +1 -1
- package/dist/lib/health-probes.js +218 -0
- package/dist/lib/health-probes.js.map +1 -1
- package/dist/lib/instrumentation.d.ts +85 -0
- package/dist/lib/instrumentation.d.ts.map +1 -0
- package/dist/lib/instrumentation.js +104 -0
- package/dist/lib/instrumentation.js.map +1 -0
- package/dist/lib/kubectl-wrap.d.ts +59 -0
- package/dist/lib/kubectl-wrap.d.ts.map +1 -0
- package/dist/lib/kubectl-wrap.js +130 -0
- package/dist/lib/kubectl-wrap.js.map +1 -0
- package/dist/lib/manifest-refresh.d.ts +95 -0
- package/dist/lib/manifest-refresh.d.ts.map +1 -0
- package/dist/lib/manifest-refresh.js +222 -0
- package/dist/lib/manifest-refresh.js.map +1 -0
- package/dist/lib/port-forward.d.ts +101 -0
- package/dist/lib/port-forward.d.ts.map +1 -0
- package/dist/lib/port-forward.js +240 -0
- package/dist/lib/port-forward.js.map +1 -0
- package/dist/lib/upgrade-kubernetes.d.ts +77 -0
- package/dist/lib/upgrade-kubernetes.d.ts.map +1 -0
- package/dist/lib/upgrade-kubernetes.js +277 -0
- package/dist/lib/upgrade-kubernetes.js.map +1 -0
- package/dist/mcp-server.js +3328 -1166
- package/host-cp/k8s/manifests/00-namespace.yaml +7 -0
- package/host-cp/k8s/manifests/10-serviceaccount.yaml +8 -0
- package/host-cp/k8s/manifests/20-rbac.yaml +34 -0
- package/host-cp/k8s/manifests/30-configmap.yaml +30 -0
- package/host-cp/k8s/manifests/45-pvc.yaml +27 -0
- package/host-cp/k8s/manifests/50-deployment.yaml +148 -0
- package/host-cp/k8s/manifests/60-service.yaml +22 -0
- package/host-cp/k8s/templates/40-secret-template.yaml +32 -0
- package/host-cp/src/agent-runtime-trigger.mjs +74 -4
- package/host-cp/src/engine-identity.mjs +32 -0
- package/host-cp/src/plan-chat-service.mjs +31 -7
- package/host-cp/src/server.mjs +219 -9
- package/package.json +3 -2
package/host-cp/src/server.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import { subscribeDockerEvents } from './docker-events.mjs';
|
|
|
37
37
|
import { createHostStream, newStreamId } from './host-stream.mjs';
|
|
38
38
|
import { spawnUpgraderContainer } from './upgrade-spawner.mjs';
|
|
39
39
|
import { parseProxyPath, perWorldBase, proxyToWorld } from './proxy.mjs';
|
|
40
|
+
import { resolveHostCpEngine } from './engine-identity.mjs';
|
|
40
41
|
import { StartupToken } from './auth.mjs';
|
|
41
42
|
import { SseGate, isSsePath, wireRelease } from './sse-gate.mjs';
|
|
42
43
|
import {
|
|
@@ -88,6 +89,11 @@ const HOST_CP_MODE = process.env.OLAM_HOST_CP_MODE
|
|
|
88
89
|
?? (fs.existsSync('/.dockerenv') ? 'container' : 'bare');
|
|
89
90
|
const WORLD_HOST = HOST_CP_MODE === 'container' ? 'host.docker.internal' : '127.0.0.1';
|
|
90
91
|
|
|
92
|
+
// Container-engine identity, surfaced to olam-cli via the X-Olam-Engine
|
|
93
|
+
// response header on /health. Resolution lives in engine-identity.mjs so
|
|
94
|
+
// unit tests can import the pure function without triggering server startup.
|
|
95
|
+
const HOST_CP_ENGINE = resolveHostCpEngine();
|
|
96
|
+
|
|
91
97
|
const PORT = parseInt(process.env.OLAM_HOST_CP_PORT ?? '19000', 10);
|
|
92
98
|
// In container mode the host-cp talks to the docker daemon via the
|
|
93
99
|
// socket-proxy sidecar (the proxy enforces the read-only API allow-list).
|
|
@@ -664,7 +670,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
664
670
|
// /health: fast diagnostics, no auth, no proxying. Docker healthcheck
|
|
665
671
|
// hits this; SPA pre-load may also poll. Stays unauth so the container
|
|
666
672
|
// healthcheck doesn't need to know the token.
|
|
673
|
+
//
|
|
674
|
+
// X-Olam-Engine header surfaces the active container-engine adapter to
|
|
675
|
+
// olam-cli (consumed by `olam doctor`, `olam logs`, `olam status`). Lives
|
|
676
|
+
// in the header (NOT the body) so the response body shape stays unchanged
|
|
677
|
+
// and probes don't accidentally leak engine identity to body parsers.
|
|
667
678
|
if (url.pathname === '/health') {
|
|
679
|
+
res.setHeader('X-Olam-Engine', HOST_CP_ENGINE);
|
|
668
680
|
return jsonReply(res, 200, {
|
|
669
681
|
status: 'ok',
|
|
670
682
|
phase: 'B4',
|
|
@@ -725,6 +737,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
725
737
|
if (served) return;
|
|
726
738
|
}
|
|
727
739
|
|
|
740
|
+
// /api/plan-chat/* bypasses the host-cp session-token gate because the
|
|
741
|
+
// plan-chat-service it proxies to has its own Bearer auth (the
|
|
742
|
+
// plan-chat-secret). The proxy handler is registered later in the file;
|
|
743
|
+
// dispatch by continuing past the gate.
|
|
744
|
+
if (url.pathname.startsWith('/api/plan-chat/') || url.pathname === '/api/plan-chat') {
|
|
745
|
+
// fall through to the proxy route below
|
|
746
|
+
} else
|
|
728
747
|
// ALL OTHER ROUTES require auth. Reject with 401 if neither cookie
|
|
729
748
|
// nor Bearer header matches.
|
|
730
749
|
if (!auth.isAuthorized(req)) {
|
|
@@ -1875,7 +1894,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1875
1894
|
if (!await requirePlanCredential(res)) return;
|
|
1876
1895
|
let body;
|
|
1877
1896
|
try {
|
|
1878
|
-
body =
|
|
1897
|
+
body = await readRequestBody(req);
|
|
1879
1898
|
} catch (err) {
|
|
1880
1899
|
return jsonReply(res, 400, { error: 'invalid_json', message: err.message });
|
|
1881
1900
|
}
|
|
@@ -1920,6 +1939,66 @@ const server = http.createServer(async (req, res) => {
|
|
|
1920
1939
|
}
|
|
1921
1940
|
}
|
|
1922
1941
|
|
|
1942
|
+
// /api/plan-chat/* — passthrough proxy to plan-chat-service.
|
|
1943
|
+
// The sidecar runs on PLAN_CHAT_SERVICE_URL (default http://127.0.0.1:3112).
|
|
1944
|
+
// Strips the /api/plan-chat prefix; forwards method, headers, body, and
|
|
1945
|
+
// query verbatim. Streams the response (Electric SQL long-poll friendly).
|
|
1946
|
+
// Auth: client supplies Bearer; we don't add or strip it.
|
|
1947
|
+
if (url.pathname.startsWith('/api/plan-chat/') || url.pathname === '/api/plan-chat') {
|
|
1948
|
+
const upstreamBase =
|
|
1949
|
+
process.env.PLAN_CHAT_SERVICE_URL ??
|
|
1950
|
+
// Default depends on where host-cp runs. In-container = host.docker.internal;
|
|
1951
|
+
// bare-node = 127.0.0.1. DOCKER_HOST=tcp://* implies container mode.
|
|
1952
|
+
((process.env.DOCKER_HOST ?? '').startsWith('tcp://')
|
|
1953
|
+
? 'http://host.docker.internal:3112'
|
|
1954
|
+
: 'http://127.0.0.1:3112');
|
|
1955
|
+
const subPath = url.pathname === '/api/plan-chat'
|
|
1956
|
+
? '/'
|
|
1957
|
+
: url.pathname.slice('/api/plan-chat'.length);
|
|
1958
|
+
const upstreamUrl = new URL(subPath + url.search, upstreamBase);
|
|
1959
|
+
try {
|
|
1960
|
+
const headers = {};
|
|
1961
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
1962
|
+
if (k === 'host' || k === 'connection' || k === 'content-length') continue;
|
|
1963
|
+
if (Array.isArray(v)) headers[k] = v.join(', ');
|
|
1964
|
+
else if (typeof v === 'string') headers[k] = v;
|
|
1965
|
+
}
|
|
1966
|
+
// Buffer body for non-GET/HEAD; GET shouldn't carry one.
|
|
1967
|
+
let body;
|
|
1968
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
1969
|
+
const chunks = [];
|
|
1970
|
+
for await (const c of req) chunks.push(c);
|
|
1971
|
+
body = Buffer.concat(chunks);
|
|
1972
|
+
}
|
|
1973
|
+
const upstreamRes = await fetch(upstreamUrl.toString(), {
|
|
1974
|
+
method: req.method,
|
|
1975
|
+
headers,
|
|
1976
|
+
body,
|
|
1977
|
+
redirect: 'manual',
|
|
1978
|
+
});
|
|
1979
|
+
res.statusCode = upstreamRes.status;
|
|
1980
|
+
upstreamRes.headers.forEach((value, name) => {
|
|
1981
|
+
if (name === 'content-encoding' || name === 'transfer-encoding' || name === 'connection') return;
|
|
1982
|
+
res.setHeader(name, value);
|
|
1983
|
+
});
|
|
1984
|
+
if (upstreamRes.body) {
|
|
1985
|
+
const reader = upstreamRes.body.getReader();
|
|
1986
|
+
while (true) {
|
|
1987
|
+
const { value, done } = await reader.read();
|
|
1988
|
+
if (done) break;
|
|
1989
|
+
res.write(value);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return res.end();
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
return jsonReply(res, 502, {
|
|
1995
|
+
error: 'plan_chat_proxy_failed',
|
|
1996
|
+
upstream: upstreamUrl.toString(),
|
|
1997
|
+
message: err.message,
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1923
2002
|
// GET /api/worlds/:id/processes
|
|
1924
2003
|
// GET /api/worlds/:id/processes/stream — SSE fanout (5s cadence, per-world)
|
|
1925
2004
|
// Handler: routes/process-port.mjs → handleListProcesses
|
|
@@ -1992,6 +2071,85 @@ const server = http.createServer(async (req, res) => {
|
|
|
1992
2071
|
}
|
|
1993
2072
|
}
|
|
1994
2073
|
|
|
2074
|
+
// ── /v1/worlds/:id/{status,logs} (Phase C / C1) ─────────────────
|
|
2075
|
+
//
|
|
2076
|
+
// Engine-agnostic per-world surfaces consumed by `olam status <world>`
|
|
2077
|
+
// and `olam logs <world>`. Delegates to the active ContainerEngine
|
|
2078
|
+
// (DockerEngine or KubernetesEngine) so the wire shape is identical
|
|
2079
|
+
// regardless of the underlying runtime per Decision 17.
|
|
2080
|
+
//
|
|
2081
|
+
// Auth: this branch is INSIDE the `auth.isAuthorized(req)` gate above
|
|
2082
|
+
// (line 742) — any caller without a valid token has already received
|
|
2083
|
+
// 401. The v1-worlds-endpoints.test.mjs source-asserts that this
|
|
2084
|
+
// route is registered after the auth gate, NOT outside it
|
|
2085
|
+
// (Decision 18 — security-critical positional contract).
|
|
2086
|
+
//
|
|
2087
|
+
// worldId hardening: the K8s engine uses worldId as a labelSelector
|
|
2088
|
+
// value (`olam-world-id=<worldId>`). A worldId containing `,` could
|
|
2089
|
+
// synthesise additional label clauses ("comma-injection"). A token-
|
|
2090
|
+
// holding caller already has full host-cp access so this is
|
|
2091
|
+
// defense-in-depth, but we still reject non-canonical values to keep
|
|
2092
|
+
// the engine's external query shape predictable.
|
|
2093
|
+
const V1_WORLD_ID = /^[A-Za-z0-9._-]+$/;
|
|
2094
|
+
const v1WorldsStatusMatch = /^\/v1\/worlds\/([^/?#]+)\/status\/?$/.exec(url.pathname);
|
|
2095
|
+
if (v1WorldsStatusMatch && req.method === 'GET') {
|
|
2096
|
+
const worldId = decodeURIComponent(v1WorldsStatusMatch[1]);
|
|
2097
|
+
if (!V1_WORLD_ID.test(worldId)) {
|
|
2098
|
+
return jsonReply(res, 400, { error: 'invalid_world_id', worldId });
|
|
2099
|
+
}
|
|
2100
|
+
try {
|
|
2101
|
+
const report = await hostCpEngine.getWorldStatus(worldId);
|
|
2102
|
+
return jsonReply(res, 200, report);
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
return jsonReply(res, 500, {
|
|
2105
|
+
error: 'engine_error',
|
|
2106
|
+
worldId,
|
|
2107
|
+
message: err?.message ?? String(err),
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
const v1WorldsLogsMatch = /^\/v1\/worlds\/([^/?#]+)\/logs\/?$/.exec(url.pathname);
|
|
2113
|
+
if (v1WorldsLogsMatch && req.method === 'GET') {
|
|
2114
|
+
const worldId = decodeURIComponent(v1WorldsLogsMatch[1]);
|
|
2115
|
+
if (!V1_WORLD_ID.test(worldId)) {
|
|
2116
|
+
return jsonReply(res, 400, { error: 'invalid_world_id', worldId });
|
|
2117
|
+
}
|
|
2118
|
+
const tailParam = url.searchParams.get('tail');
|
|
2119
|
+
const followParam = url.searchParams.get('follow');
|
|
2120
|
+
const serviceParam = url.searchParams.get('service');
|
|
2121
|
+
const opts = {
|
|
2122
|
+
tail: tailParam != null ? parseInt(tailParam, 10) : 200,
|
|
2123
|
+
follow: followParam === '1' || followParam === 'true',
|
|
2124
|
+
...(serviceParam ? { service: serviceParam } : {}),
|
|
2125
|
+
};
|
|
2126
|
+
const abortController = new AbortController();
|
|
2127
|
+
req.on('close', () => abortController.abort());
|
|
2128
|
+
res.writeHead(200, {
|
|
2129
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
2130
|
+
'Cache-Control': 'no-cache',
|
|
2131
|
+
});
|
|
2132
|
+
try {
|
|
2133
|
+
// Engine yields canonical { ts, container, subContainer, line }
|
|
2134
|
+
// records; server formats one line per record: `<ts> <pod>/<container> <line>`.
|
|
2135
|
+
const iterable = hostCpEngine.getWorldLogs(worldId, { ...opts, signal: abortController.signal });
|
|
2136
|
+
for await (const rec of iterable) {
|
|
2137
|
+
if (res.writableEnded || abortController.signal.aborted) break;
|
|
2138
|
+
const ts = rec.ts || '-';
|
|
2139
|
+
const container = rec.container || '-';
|
|
2140
|
+
const sub = rec.subContainer || container;
|
|
2141
|
+
res.write(`${ts} ${container}/${sub} ${rec.line}\n`);
|
|
2142
|
+
}
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
if (!res.writableEnded) {
|
|
2145
|
+
res.write(`# engine error: ${err?.message ?? String(err)}\n`);
|
|
2146
|
+
}
|
|
2147
|
+
} finally {
|
|
2148
|
+
if (!res.writableEnded) res.end();
|
|
2149
|
+
}
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
1995
2153
|
// Anything else → 404. B4 ships static SPA serving + auth.
|
|
1996
2154
|
jsonReply(res, 404, {
|
|
1997
2155
|
error: 'not_found',
|
|
@@ -2376,8 +2534,8 @@ const DIST_DIR = (() => {
|
|
|
2376
2534
|
return '/app/dist'; // fallback; readFile will surface ENOENT
|
|
2377
2535
|
})();
|
|
2378
2536
|
|
|
2379
|
-
const SPA_ROUTES = new Set(['/', '/worlds', '/workspaces', '/inbox', '/repos', '/runbooks']);
|
|
2380
|
-
const SPA_PREFIX = ['/world/', '/worlds/', '/workspaces/', '/inbox/', '/repos/', '/runbooks/'];
|
|
2537
|
+
const SPA_ROUTES = new Set(['/', '/worlds', '/workspaces', '/inbox', '/repos', '/runbooks', '/plan']);
|
|
2538
|
+
const SPA_PREFIX = ['/world/', '/worlds/', '/workspaces/', '/inbox/', '/repos/', '/runbooks/', '/session/', '/plan/'];
|
|
2381
2539
|
|
|
2382
2540
|
// Top-level path segments that are NOT world IDs. Mirrors RESERVED_SEGMENTS
|
|
2383
2541
|
// in lib/worldId.ts — keep in sync.
|
|
@@ -2517,7 +2675,7 @@ async function tryServeStatic(req, res, pathname) {
|
|
|
2517
2675
|
// memory thereafter. Cache invalidates on dist/ mtime change so a
|
|
2518
2676
|
// rebuilt bundle is picked up without restart.
|
|
2519
2677
|
let _spaCache = null;
|
|
2520
|
-
let
|
|
2678
|
+
let _spaCacheKey = '';
|
|
2521
2679
|
|
|
2522
2680
|
/**
|
|
2523
2681
|
* Bootstrap script injected into the SPA shell. Two responsibilities:
|
|
@@ -2595,11 +2753,34 @@ let _spaCacheMtime = 0;
|
|
|
2595
2753
|
// and the token-comparison check skips reload when the cookie
|
|
2596
2754
|
// already matches (so non-rotation 401s — e.g. genuine auth
|
|
2597
2755
|
// failures — don't cause a refresh loop).
|
|
2598
|
-
const BOOTSTRAP_SCRIPT = `<script>(function(){function ck(){var m=document.cookie.match(/olam_host_cp_token=([^;]+)/);return m?m[1]:'';}function sw(t){document.cookie='olam_host_cp_token='+t+'; path=/; samesite=strict';}try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);sw(d.token);}}catch(e){console.error('[host-cp bootstrap]',e);}var reloading=false;function recover(){if(reloading)return;try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);if(d.token&&ck()!==d.token){reloading=true;sw(d.token);console.warn('[host-cp auth recover] token rotated; reloading');location.reload();}}}catch(e){console.error('[host-cp auth recover]',e);}}var HN=['/api/bootstrap','/api/worlds','/api/projects','/api/workspaces','/api/workspaces/match','/api/repos','/api/runbooks','/api/auth','/api/host-stream','/health'];var WP=['/api/','/session/','/hooks/','/dispatch','/lanes','/codex/','/review/'];function sr(p){if(typeof p!=='string')return false;if(p.startsWith('/api/world/'))return false;for(var i=0;i<HN.length;i++){var n=HN[i];if(p===n||p.startsWith(n+'?')||p.startsWith(n+'/'))return false;}for(var j=0;j<WP.length;j++){var w=WP[j];if(p===w||p===w.replace(/\\/$/,'')||p.startsWith(w)||p.startsWith(w.replace(/\\/$/,'')+'?')||p.startsWith(w.replace(/\\/$/,'')+'/'))return true;}return false;}function wid(){var p=location.pathname;var m=p.match(/^\\/(world|inbox)\\/([^/?#]+)/);if(m)return m[2];if(/^\\/(?:worlds?|workspaces?|world|sandbox|inbox|plan|design|repos|runbooks|assets|api|health|favicon)($|\\/|\\?)/.test(p))return null;var r=p.match(/^\\/([a-z][a-z0-9-]+)(?:\\/|$|\\?)/);return r?r[1]:null;}function rw(p){var w=wid();return w?'/api/world/'+w+p:p;}var of=window.fetch.bind(window);window.fetch=function(input,init){var pr;if(typeof input==='string'&&sr(input))pr=of(rw(input),init);else if(input&&typeof input.url==='string'&&sr(input.url))pr=of(new Request(rw(input.url),input),init);else pr=of(input,init);return pr.then(function(res){if(res&&res.status===401)recover();return res;});};var OE=window.EventSource;if(OE){window.EventSource=function(u,i){var s=u;if(typeof s==='string'&&sr(s))s=rw(s);var es=new OE(s,i);es.addEventListener('error',function(){if(es.readyState===OE.CLOSED)recover();});return es;};window.EventSource.prototype=OE.prototype;window.EventSource.CONNECTING=OE.CONNECTING;window.EventSource.OPEN=OE.OPEN;window.EventSource.CLOSED=OE.CLOSED;}})();</script>`;
|
|
2756
|
+
const BOOTSTRAP_SCRIPT = `<script>(function(){function ck(){var m=document.cookie.match(/olam_host_cp_token=([^;]+)/);return m?m[1]:'';}function sw(t){document.cookie='olam_host_cp_token='+t+'; path=/; samesite=strict';}try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);sw(d.token);}}catch(e){console.error('[host-cp bootstrap]',e);}var reloading=false;function recover(){if(reloading)return;try{var x=new XMLHttpRequest();x.open('GET','/api/bootstrap',false);x.send();if(x.status===200){var d=JSON.parse(x.responseText);if(d.token&&ck()!==d.token){reloading=true;sw(d.token);console.warn('[host-cp auth recover] token rotated; reloading');location.reload();}}}catch(e){console.error('[host-cp auth recover]',e);}}var HN=['/api/bootstrap','/api/worlds','/api/projects','/api/workspaces','/api/workspaces/match','/api/repos','/api/runbooks','/api/auth','/api/host-stream','/api/plan-chat','/api/plan/agent-runtime','/health'];var WP=['/api/','/session/','/hooks/','/dispatch','/lanes','/codex/','/review/'];function sr(p){if(typeof p!=='string')return false;if(p.startsWith('/api/world/'))return false;for(var i=0;i<HN.length;i++){var n=HN[i];if(p===n||p.startsWith(n+'?')||p.startsWith(n+'/'))return false;}for(var j=0;j<WP.length;j++){var w=WP[j];if(p===w||p===w.replace(/\\/$/,'')||p.startsWith(w)||p.startsWith(w.replace(/\\/$/,'')+'?')||p.startsWith(w.replace(/\\/$/,'')+'/'))return true;}return false;}function wid(){var p=location.pathname;var m=p.match(/^\\/(world|inbox|session)\\/([^/?#]+)/);if(m)return m[2];if(/^\\/(?:worlds?|workspaces?|world|sandbox|session|inbox|plan|design|repos|runbooks|assets|api|health|favicon)($|\\/|\\?)/.test(p))return null;var r=p.match(/^\\/([a-z][a-z0-9-]+)(?:\\/|$|\\?)/);return r?r[1]:null;}function rw(p){var w=wid();return w?'/api/world/'+w+p:p;}var of=window.fetch.bind(window);window.fetch=function(input,init){var pr;if(typeof input==='string'&&sr(input))pr=of(rw(input),init);else if(input&&typeof input.url==='string'&&sr(input.url))pr=of(new Request(rw(input.url),input),init);else pr=of(input,init);return pr.then(function(res){if(res&&res.status===401)recover();return res;});};var OE=window.EventSource;if(OE){window.EventSource=function(u,i){var s=u;if(typeof s==='string'&&sr(s))s=rw(s);var es=new OE(s,i);es.addEventListener('error',function(){if(es.readyState===OE.CLOSED)recover();});return es;};window.EventSource.prototype=OE.prototype;window.EventSource.CONNECTING=OE.CONNECTING;window.EventSource.OPEN=OE.OPEN;window.EventSource.CLOSED=OE.CLOSED;}})();</script>`;
|
|
2757
|
+
|
|
2758
|
+
/**
|
|
2759
|
+
* Build the plan-chat bearer injection script. Reads
|
|
2760
|
+
* ~/.olam/plan-chat-secret on every shell render (cheap; tiny file)
|
|
2761
|
+
* so secret rotation propagates without restart. Missing secret →
|
|
2762
|
+
* no injection; PlanChatRoute falls back to the URL-hash channel.
|
|
2763
|
+
*
|
|
2764
|
+
* Operator-local single-user mode only. Multi-tenant deploys will
|
|
2765
|
+
* replace this with one of the 3 candidates in chunks-collection.ts.
|
|
2766
|
+
*/
|
|
2767
|
+
function buildPlanChatBearerInjection() {
|
|
2768
|
+
try {
|
|
2769
|
+
const bearer = readPlanChatSecret();
|
|
2770
|
+
if (!bearer) return '';
|
|
2771
|
+
// JSON.stringify escapes for safe embedding inside the <script>.
|
|
2772
|
+
return `<script>window.__OLAM_PLAN_CHAT_BEARER__=${JSON.stringify(bearer)};</script>`;
|
|
2773
|
+
} catch {
|
|
2774
|
+
return '';
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2599
2777
|
|
|
2600
2778
|
async function renderSpaShell(filePath) {
|
|
2601
2779
|
const stat = fs.statSync(filePath);
|
|
2602
|
-
|
|
2780
|
+
const bearerInjection = buildPlanChatBearerInjection();
|
|
2781
|
+
// Cache key must include the bearer so rotation invalidates correctly.
|
|
2782
|
+
const cacheKey = stat.mtimeMs + ':' + bearerInjection.length;
|
|
2783
|
+
if (_spaCache !== null && _spaCacheKey === cacheKey) {
|
|
2603
2784
|
return _spaCache;
|
|
2604
2785
|
}
|
|
2605
2786
|
let html = fs.readFileSync(filePath, 'utf-8');
|
|
@@ -2610,10 +2791,12 @@ async function renderSpaShell(filePath) {
|
|
|
2610
2791
|
// (/, /worlds, /workspaces, /world/<id>) reference the same bundle.
|
|
2611
2792
|
html = html.replace(/(href|src)="\.\/assets\//g, '$1="/assets/');
|
|
2612
2793
|
// Inject right after <head> so the bootstrap runs before any other
|
|
2613
|
-
// script tag on the page.
|
|
2614
|
-
|
|
2794
|
+
// script tag on the page. Bearer injection runs after the host-cp
|
|
2795
|
+
// bootstrap so window.__OLAM_PLAN_CHAT_BEARER__ is set before the
|
|
2796
|
+
// SPA bundle reads it.
|
|
2797
|
+
html = html.replace(/<head>/i, `<head>\n ${BOOTSTRAP_SCRIPT}\n ${bearerInjection}`);
|
|
2615
2798
|
_spaCache = html;
|
|
2616
|
-
|
|
2799
|
+
_spaCacheKey = cacheKey;
|
|
2617
2800
|
return html;
|
|
2618
2801
|
}
|
|
2619
2802
|
|
|
@@ -2701,12 +2884,39 @@ startWorldsSnapshotLoop();
|
|
|
2701
2884
|
startTunnelsSnapshotLoop();
|
|
2702
2885
|
startListeningSnapshotLoop();
|
|
2703
2886
|
|
|
2887
|
+
// ── Phase 1a / B1 (PR3): engine-select + await-before-listen ─────
|
|
2888
|
+
//
|
|
2889
|
+
// Decision 15: the async KubernetesEngine factory MUST be fully awaited
|
|
2890
|
+
// BEFORE `server.listen()`. If the factory throws (context-allowlist guard
|
|
2891
|
+
// rejects a non-local kubectl context), startup aborts via the top-level
|
|
2892
|
+
// await's unhandled rejection — the HTTP server never binds. No half-live
|
|
2893
|
+
// state.
|
|
2894
|
+
//
|
|
2895
|
+
// Selection precedence matches the existing resolveHostCpEngine():
|
|
2896
|
+
// 1. OLAM_HOST_CP_ENGINE=kubernetes → KubernetesEngine
|
|
2897
|
+
// 2. KUBERNETES_SERVICE_HOST set → KubernetesEngine (autodetect)
|
|
2898
|
+
// 3. otherwise → DockerEngine
|
|
2899
|
+
//
|
|
2900
|
+
// DockerEngine ships as a sync factory (no await needed), but we still
|
|
2901
|
+
// resolve through the same async branch for symmetry — the call-site
|
|
2902
|
+
// migration to engine.* methods is a downstream task; today the engine
|
|
2903
|
+
// instance is held for /health diagnostic + future use.
|
|
2904
|
+
const hostCpEngine = await (async () => {
|
|
2905
|
+
if (HOST_CP_ENGINE === 'kubernetes') {
|
|
2906
|
+
const { createKubernetesEngine } = await import('./engines/kubernetes.mjs');
|
|
2907
|
+
return createKubernetesEngine({ env: process.env });
|
|
2908
|
+
}
|
|
2909
|
+
const { createDockerEngine } = await import('./engines/docker.mjs');
|
|
2910
|
+
return createDockerEngine({ dockerHost: DOCKER_HOST });
|
|
2911
|
+
})();
|
|
2912
|
+
|
|
2704
2913
|
server.listen(PORT, '0.0.0.0', () => {
|
|
2705
2914
|
console.log(`olam-host-cp B3 listening on :${PORT}`);
|
|
2706
2915
|
console.log(` DOCKER_HOST=${DOCKER_HOST}`);
|
|
2707
2916
|
console.log(` cache TTL=${TTL_SEC}s`);
|
|
2708
2917
|
console.log(` worlds known: ${Object.keys(WORLDS).join(', ') || '(none)'}`);
|
|
2709
2918
|
console.log(` mode=${HOST_CP_MODE} world-host=${HOST_FOR_WORLD}`);
|
|
2919
|
+
console.log(` engine=${hostCpEngine.engineName}${hostCpEngine.context ? ` ctx=${hostCpEngine.context}` : ''}`);
|
|
2710
2920
|
console.log(` (override: OLAM_HOST_CP_MODE=container|bare, OLAM_HOST_FOR_WORLD=<host>)`);
|
|
2711
2921
|
// Surface the auth wiring state at boot. An empty OLAM_AUTH_SECRET
|
|
2712
2922
|
// here is silently fatal for the credential surfaces — the operator
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pleri/olam-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.146",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"olam": "./bin/olam.cjs"
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"test": "vitest run --passWithNoTests",
|
|
32
32
|
"test:ci": "vitest run --reporter=basic --passWithNoTests",
|
|
33
33
|
"test:docker": "vitest run --config vitest.config.docker.ts",
|
|
34
|
-
"audit:publish-deps": "node scripts/audit-publish-deps.mjs"
|
|
34
|
+
"audit:publish-deps": "node scripts/audit-publish-deps.mjs",
|
|
35
|
+
"audit:cli-bundle-k8s": "node scripts/audit-cli-bundle-k8s.mjs"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"better-sqlite3": "^12.0.0",
|