@pleri/olam-cli 0.1.153 → 0.1.158
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/bootstrap.d.ts +2 -1
- package/dist/commands/bootstrap.d.ts.map +1 -1
- package/dist/commands/bootstrap.js +8 -10
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/doctor.d.ts +22 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +110 -7
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flywheel/check-persona-skeleton.d.ts +7 -0
- package/dist/commands/flywheel/check-persona-skeleton.d.ts.map +1 -0
- package/dist/commands/flywheel/check-persona-skeleton.js +14 -0
- package/dist/commands/flywheel/check-persona-skeleton.js.map +1 -0
- package/dist/commands/flywheel/diversity-check.d.ts +7 -0
- package/dist/commands/flywheel/diversity-check.d.ts.map +1 -0
- package/dist/commands/flywheel/diversity-check.js +14 -0
- package/dist/commands/flywheel/diversity-check.js.map +1 -0
- package/dist/commands/flywheel/emit-breadcrumb.d.ts +20 -0
- package/dist/commands/flywheel/emit-breadcrumb.d.ts.map +1 -0
- package/dist/commands/flywheel/emit-breadcrumb.js +137 -0
- package/dist/commands/flywheel/emit-breadcrumb.js.map +1 -0
- package/dist/commands/flywheel/index.d.ts +27 -0
- package/dist/commands/flywheel/index.d.ts.map +1 -0
- package/dist/commands/flywheel/index.js +48 -0
- package/dist/commands/flywheel/index.js.map +1 -0
- package/dist/commands/flywheel/install-shims.d.ts +8 -0
- package/dist/commands/flywheel/install-shims.d.ts.map +1 -0
- package/dist/commands/flywheel/install-shims.js +15 -0
- package/dist/commands/flywheel/install-shims.js.map +1 -0
- package/dist/commands/flywheel/k10-measure.d.ts +7 -0
- package/dist/commands/flywheel/k10-measure.d.ts.map +1 -0
- package/dist/commands/flywheel/k10-measure.js +14 -0
- package/dist/commands/flywheel/k10-measure.js.map +1 -0
- package/dist/commands/flywheel/k5-score.d.ts +14 -0
- package/dist/commands/flywheel/k5-score.d.ts.map +1 -0
- package/dist/commands/flywheel/k5-score.js +59 -0
- package/dist/commands/flywheel/k5-score.js.map +1 -0
- package/dist/commands/flywheel/k5-validate.d.ts +15 -0
- package/dist/commands/flywheel/k5-validate.d.ts.map +1 -0
- package/dist/commands/flywheel/k5-validate.js +185 -0
- package/dist/commands/flywheel/k5-validate.js.map +1 -0
- package/dist/commands/flywheel/ping.d.ts +21 -0
- package/dist/commands/flywheel/ping.d.ts.map +1 -0
- package/dist/commands/flywheel/ping.js +79 -0
- package/dist/commands/flywheel/ping.js.map +1 -0
- package/dist/commands/flywheel/sanitize-persona-output.d.ts +7 -0
- package/dist/commands/flywheel/sanitize-persona-output.d.ts.map +1 -0
- package/dist/commands/flywheel/sanitize-persona-output.js +14 -0
- package/dist/commands/flywheel/sanitize-persona-output.js.map +1 -0
- package/dist/commands/hermes-kg-hook.d.ts +36 -0
- package/dist/commands/hermes-kg-hook.d.ts.map +1 -0
- package/dist/commands/hermes-kg-hook.js +80 -0
- package/dist/commands/hermes-kg-hook.js.map +1 -0
- package/dist/commands/hermes.d.ts +46 -0
- package/dist/commands/hermes.d.ts.map +1 -0
- package/dist/commands/hermes.js +320 -0
- package/dist/commands/hermes.js.map +1 -0
- package/dist/commands/kg-install-hook.d.ts +7 -1
- package/dist/commands/kg-install-hook.d.ts.map +1 -1
- package/dist/commands/kg-install-hook.js +122 -6
- package/dist/commands/kg-install-hook.js.map +1 -1
- package/dist/commands/memory/_paths.d.ts +13 -3
- package/dist/commands/memory/_paths.d.ts.map +1 -1
- package/dist/commands/memory/_paths.js +25 -22
- package/dist/commands/memory/_paths.js.map +1 -1
- package/dist/commands/memory/logs.d.ts +8 -4
- package/dist/commands/memory/logs.d.ts.map +1 -1
- package/dist/commands/memory/logs.js +18 -13
- package/dist/commands/memory/logs.js.map +1 -1
- package/dist/commands/memory/mode.d.ts.map +1 -1
- package/dist/commands/memory/mode.js +7 -3
- package/dist/commands/memory/mode.js.map +1 -1
- package/dist/commands/memory/start.d.ts +16 -14
- package/dist/commands/memory/start.d.ts.map +1 -1
- package/dist/commands/memory/start.js +55 -189
- package/dist/commands/memory/start.js.map +1 -1
- package/dist/commands/memory/status.d.ts +10 -8
- package/dist/commands/memory/status.d.ts.map +1 -1
- package/dist/commands/memory/status.js +35 -38
- package/dist/commands/memory/status.js.map +1 -1
- package/dist/commands/memory/stop.d.ts +5 -4
- package/dist/commands/memory/stop.d.ts.map +1 -1
- package/dist/commands/memory/stop.js +26 -55
- package/dist/commands/memory/stop.js.map +1 -1
- package/dist/commands/memory-service-container.d.ts +78 -0
- package/dist/commands/memory-service-container.d.ts.map +1 -0
- package/dist/commands/memory-service-container.js +187 -0
- package/dist/commands/memory-service-container.js.map +1 -0
- package/dist/commands/services.d.ts +16 -1
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +88 -12
- package/dist/commands/services.js.map +1 -1
- package/dist/image-digests.json +7 -7
- package/dist/index.js +2570 -1384
- package/dist/index.js.map +1 -1
- package/dist/lib/k8s-secret-render.d.ts.map +1 -1
- package/dist/lib/k8s-secret-render.js +7 -4
- package/dist/lib/k8s-secret-render.js.map +1 -1
- package/dist/lib/memory-host-process-migration.d.ts +56 -0
- package/dist/lib/memory-host-process-migration.d.ts.map +1 -0
- package/dist/lib/memory-host-process-migration.js +156 -0
- package/dist/lib/memory-host-process-migration.js.map +1 -0
- package/dist/lib/upgrade-kubernetes.d.ts +22 -0
- package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
- package/dist/lib/upgrade-kubernetes.js +195 -2
- package/dist/lib/upgrade-kubernetes.js.map +1 -1
- package/dist/mcp-server.js +56 -22
- package/hermes-bundle/kg-first.sh +100 -0
- package/hermes-bundle/version.json +4 -0
- package/host-cp/k8s/manifests/50-deployment.yaml +54 -27
- package/host-cp/k8s/manifests/auth-service/30-configmap.yaml +5 -0
- package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +5 -1
- package/host-cp/k8s/manifests/kg-service/30-configmap.yaml +5 -0
- package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +5 -1
- package/host-cp/k8s/manifests/mcp-auth-service/30-configmap.yaml +4 -0
- package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +5 -1
- package/host-cp/k8s/manifests/memory-service/30-configmap.yaml +4 -0
- package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +5 -1
- package/host-cp/src/metrics.mjs +281 -0
- package/host-cp/src/server.mjs +22 -2
- package/package.json +3 -4
- package/memory-service-bundle/scripts/ensure-iii-engine.mjs +0 -179
|
@@ -43,6 +43,10 @@ spec:
|
|
|
43
43
|
# B9 (round 2 recovery): disable k8s automatic Service env injection.
|
|
44
44
|
# See packages/host-cp/k8s/manifests/50-deployment.yaml for rationale.
|
|
45
45
|
enableServiceLinks: false
|
|
46
|
+
# R3-C (Decision R3-#3): imagePullSecrets references the ghcr-pull Secret
|
|
47
|
+
# created by `olam upgrade` step 0.4 when GH_TOKEN is available.
|
|
48
|
+
imagePullSecrets:
|
|
49
|
+
- name: ghcr-pull
|
|
46
50
|
serviceAccountName: olam-mcp-auth-service
|
|
47
51
|
securityContext:
|
|
48
52
|
runAsNonRoot: true
|
|
@@ -64,7 +68,7 @@ spec:
|
|
|
64
68
|
mountPath: /data
|
|
65
69
|
containers:
|
|
66
70
|
- name: olam-mcp-auth-service
|
|
67
|
-
image: ghcr.io/pleri/olam-mcp-auth@sha256:
|
|
71
|
+
image: ghcr.io/pleri/olam-mcp-auth@sha256:4d4806e2aa7c782de60471a9742d9e85dcf9f5ba0af3c496c26ff7aab9847a43
|
|
68
72
|
imagePullPolicy: IfNotPresent
|
|
69
73
|
securityContext:
|
|
70
74
|
runAsNonRoot: true
|
|
@@ -18,3 +18,7 @@ data:
|
|
|
18
18
|
OLAM_AUTH_SERVICE_URL: "http://olam-auth-service.olam.svc.cluster.local:9999"
|
|
19
19
|
# Health path exposed at /agentmemory/livez (D15 — do not change).
|
|
20
20
|
OLAM_MEMORY_HEALTH_PATH: "/agentmemory/livez"
|
|
21
|
+
# R3-B defensive (Decision R3-#2): memory-service Dockerfile already sets
|
|
22
|
+
# AGENTMEMORY_HOST=0.0.0.0 but ConfigMap override is explicit defense against
|
|
23
|
+
# a future image regression reverting to 127.0.0.1.
|
|
24
|
+
AGENTMEMORY_HOST: "0.0.0.0"
|
|
@@ -40,6 +40,10 @@ spec:
|
|
|
40
40
|
# B9 (round 2 recovery): disable k8s automatic Service env injection.
|
|
41
41
|
# See packages/host-cp/k8s/manifests/50-deployment.yaml for rationale.
|
|
42
42
|
enableServiceLinks: false
|
|
43
|
+
# R3-C (Decision R3-#3): imagePullSecrets references the ghcr-pull Secret
|
|
44
|
+
# created by `olam upgrade` step 0.4 when GH_TOKEN is available.
|
|
45
|
+
imagePullSecrets:
|
|
46
|
+
- name: ghcr-pull
|
|
43
47
|
serviceAccountName: olam-memory-service
|
|
44
48
|
securityContext:
|
|
45
49
|
runAsNonRoot: true
|
|
@@ -66,7 +70,7 @@ spec:
|
|
|
66
70
|
# bootstrap-placeholder comment + run `npm run refresh:manifest-digests`
|
|
67
71
|
# once ghcr.io/pleri/olam-memory-service has a real published digest.
|
|
68
72
|
# bootstrap-placeholder: pre-publish; refresh after first release
|
|
69
|
-
image: ghcr.io/pleri/olam-memory-service@sha256:
|
|
73
|
+
image: ghcr.io/pleri/olam-memory-service@sha256:2c31c0f1f93c6b9a3a6d7e94db91dbc9f99cfe17aa8088e594ef4b484b039066
|
|
70
74
|
imagePullPolicy: IfNotPresent
|
|
71
75
|
securityContext:
|
|
72
76
|
runAsNonRoot: true
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Phase C Task C3 — hand-rolled Prometheus metrics registry for host-cp.
|
|
2
|
+
//
|
|
3
|
+
// Emits exactly two metric families:
|
|
4
|
+
// http_requests_total{service,route,method,status_code} counter
|
|
5
|
+
// http_request_duration_seconds{service,route,method} histogram
|
|
6
|
+
//
|
|
7
|
+
// TAXONOMY COMPLIANCE (NON-NEGOTIABLE):
|
|
8
|
+
// ONLY {service, route, method, status_code} labels allowed.
|
|
9
|
+
// BANNED: world_id, trace_id, user_id, request_id, operator_id.
|
|
10
|
+
// world_id surfaces via Prometheus exemplars in Phase D — NOT labels.
|
|
11
|
+
//
|
|
12
|
+
// No external npm deps — Prometheus text exposition is simple enough to
|
|
13
|
+
// produce with template literals. Avoids the prom-client footprint on a
|
|
14
|
+
// host-side service that has no other dependency on metrics tooling.
|
|
15
|
+
|
|
16
|
+
// ─── Route mapping ────────────────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// Raw req.url is a cardinality bomb: every unique URL is a new time series.
|
|
19
|
+
// We normalize dynamic path segments to stable patterns before labelling.
|
|
20
|
+
//
|
|
21
|
+
// RULES (first match wins):
|
|
22
|
+
// /health → /health
|
|
23
|
+
// /api/bootstrap → /api/bootstrap
|
|
24
|
+
// /metrics → /metrics
|
|
25
|
+
// /api/host-stream → /api/host-stream
|
|
26
|
+
// /api/worlds/{id}/credentials/... → /api/worlds/:id/credentials/:action
|
|
27
|
+
// /api/worlds/{id}/tunnels/... → /api/worlds/:id/tunnels
|
|
28
|
+
// /api/worlds/{id}/pr → /api/worlds/:id/pr
|
|
29
|
+
// /api/worlds/{id}/progress → /api/worlds/:id/progress
|
|
30
|
+
// /api/worlds (no id) → /api/worlds
|
|
31
|
+
// /api/world/{id}/** → /api/world/:id/* (proxy routes)
|
|
32
|
+
// /api/admin/registry/... → /api/admin/registry
|
|
33
|
+
// /api/admin/upgrade → /api/admin/upgrade
|
|
34
|
+
// /api/admin/world-pr → /api/admin/world-pr
|
|
35
|
+
// /api/admin/world-pr/{id} → /api/admin/world-pr/:id
|
|
36
|
+
// /api/auth/credentials/... → /api/auth/credentials
|
|
37
|
+
// /api/auth/... → /api/auth
|
|
38
|
+
// /api/plan/conversations/{id}/... → /api/plan/conversations/:id
|
|
39
|
+
// /api/plan/conversations → /api/plan/conversations
|
|
40
|
+
// /api/plan/** → /api/plan
|
|
41
|
+
// /api/auth/events → /api/auth/events
|
|
42
|
+
// /api/version/status → /api/version/status
|
|
43
|
+
// /api/repos → /api/repos
|
|
44
|
+
// /api/runbooks → /api/runbooks
|
|
45
|
+
// /api/workspaces/match → /api/workspaces/match
|
|
46
|
+
// /api/workspaces → /api/workspaces
|
|
47
|
+
// /api/projects → /api/projects
|
|
48
|
+
// /api/processes/** → /api/processes
|
|
49
|
+
// /v1/chunks/** → /v1/chunks
|
|
50
|
+
// /v1/worlds/** → /v1/worlds
|
|
51
|
+
// /assets/** → /assets (SPA static assets)
|
|
52
|
+
// (other GET to static paths) → /static
|
|
53
|
+
// (unknown) → /unknown
|
|
54
|
+
|
|
55
|
+
/** @param {string} pathname */
|
|
56
|
+
export function pathToRoute(pathname) {
|
|
57
|
+
// Normalize trailing slash for matching (keep bare / as /)
|
|
58
|
+
const p = pathname.length > 1 ? pathname.replace(/\/$/, '') : pathname;
|
|
59
|
+
|
|
60
|
+
if (p === '/health') return '/health';
|
|
61
|
+
if (p === '/api/bootstrap') return '/api/bootstrap';
|
|
62
|
+
if (p === '/metrics') return '/metrics';
|
|
63
|
+
if (p === '/api/host-stream') return '/api/host-stream';
|
|
64
|
+
if (p === '/api/auth/events') return '/api/auth/events';
|
|
65
|
+
if (p === '/api/version/status') return '/api/version/status';
|
|
66
|
+
if (p === '/api/repos') return '/api/repos';
|
|
67
|
+
if (p === '/api/runbooks') return '/api/runbooks';
|
|
68
|
+
if (p === '/api/workspaces/match') return '/api/workspaces/match';
|
|
69
|
+
if (p === '/api/workspaces') return '/api/workspaces';
|
|
70
|
+
if (p === '/api/projects') return '/api/projects';
|
|
71
|
+
if (p === '/api/worlds') return '/api/worlds';
|
|
72
|
+
if (p === '/api/plan/conversations' || p === '/api/plan/personas') return p;
|
|
73
|
+
if (p === '/api/admin/upgrade') return '/api/admin/upgrade';
|
|
74
|
+
if (p === '/api/admin/world-pr') return '/api/admin/world-pr';
|
|
75
|
+
if (p === '/api/admin/registry') return '/api/admin/registry';
|
|
76
|
+
if (p.startsWith('/api/worlds/')) {
|
|
77
|
+
if (p.includes('/credentials/')) return '/api/worlds/:id/credentials/:action';
|
|
78
|
+
if (p.includes('/tunnels')) return '/api/worlds/:id/tunnels';
|
|
79
|
+
if (p.endsWith('/pr')) return '/api/worlds/:id/pr';
|
|
80
|
+
if (p.endsWith('/progress')) return '/api/worlds/:id/progress';
|
|
81
|
+
return '/api/worlds/:id';
|
|
82
|
+
}
|
|
83
|
+
if (p.startsWith('/api/world/')) return '/api/world/:id/*';
|
|
84
|
+
if (p.startsWith('/api/admin/registry/')) return '/api/admin/registry';
|
|
85
|
+
if (p.startsWith('/api/admin/world-pr/')) return '/api/admin/world-pr/:id';
|
|
86
|
+
if (p.startsWith('/api/auth/credentials')) return '/api/auth/credentials';
|
|
87
|
+
if (p.startsWith('/api/auth/')) return '/api/auth';
|
|
88
|
+
if (p.startsWith('/api/plan/conversations/')) return '/api/plan/conversations/:id';
|
|
89
|
+
if (p.startsWith('/api/plan/')) return '/api/plan';
|
|
90
|
+
if (p.startsWith('/api/processes') || p.startsWith('/api/servers')) return '/api/processes';
|
|
91
|
+
if (p.startsWith('/v1/chunks')) return '/v1/chunks';
|
|
92
|
+
if (p.startsWith('/v1/worlds')) return '/v1/worlds';
|
|
93
|
+
if (p.startsWith('/assets/')) return '/assets';
|
|
94
|
+
// SPA HTML fallback routes (GET / and SPA sub-routes like /worlds, /plan/...)
|
|
95
|
+
if (p === '/' || p.startsWith('/worlds') || p.startsWith('/plan') || p.startsWith('/workspaces')) return '/static';
|
|
96
|
+
return '/unknown';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── In-memory registry ───────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const HISTOGRAM_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5];
|
|
102
|
+
|
|
103
|
+
/** @type {Map<string, number>} labelSet → count */
|
|
104
|
+
const _counters = new Map();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Per label-set histogram state.
|
|
108
|
+
* @type {Map<string, {buckets: number[], sum: number, count: number}>}
|
|
109
|
+
*/
|
|
110
|
+
const _histograms = new Map();
|
|
111
|
+
|
|
112
|
+
/** @param {string[]} parts label values in canonical order */
|
|
113
|
+
function _labelKey(parts) {
|
|
114
|
+
return parts.join('\x00');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reset all metrics. FOR TESTS ONLY — never call in production code.
|
|
119
|
+
* Exported as a separate name so it's invisible to consumers that only
|
|
120
|
+
* import the named exports they need.
|
|
121
|
+
*/
|
|
122
|
+
export function _resetForTest() {
|
|
123
|
+
_counters.clear();
|
|
124
|
+
_histograms.clear();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Increment http_requests_total counter.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} service
|
|
131
|
+
* @param {string} route — MUST be a normalized route pattern
|
|
132
|
+
* @param {string} method
|
|
133
|
+
* @param {string} statusCode
|
|
134
|
+
*/
|
|
135
|
+
export function incRequest(service, route, method, statusCode) {
|
|
136
|
+
const key = _labelKey([service, route, method, statusCode]);
|
|
137
|
+
_counters.set(key, (_counters.get(key) ?? 0) + 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Observe http_request_duration_seconds.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} service
|
|
144
|
+
* @param {string} route
|
|
145
|
+
* @param {string} method
|
|
146
|
+
* @param {number} seconds
|
|
147
|
+
*/
|
|
148
|
+
export function observeDuration(service, route, method, seconds) {
|
|
149
|
+
const key = _labelKey([service, route, method]);
|
|
150
|
+
let h = _histograms.get(key);
|
|
151
|
+
if (!h) {
|
|
152
|
+
// buckets[i] = count of observations where seconds <= HISTOGRAM_BUCKETS[i]
|
|
153
|
+
// but stored as INCREMENTAL per-range so cumulation happens on render.
|
|
154
|
+
// Each bucket[i] = count that fell in range (HISTOGRAM_BUCKETS[i-1], HISTOGRAM_BUCKETS[i]].
|
|
155
|
+
h = { buckets: new Array(HISTOGRAM_BUCKETS.length).fill(0), sum: 0, count: 0 };
|
|
156
|
+
_histograms.set(key, h);
|
|
157
|
+
}
|
|
158
|
+
// Find the first bucket boundary that accommodates this observation.
|
|
159
|
+
// Increment only that bucket; render accumulates for the exposition.
|
|
160
|
+
let placed = false;
|
|
161
|
+
for (let i = 0; i < HISTOGRAM_BUCKETS.length; i++) {
|
|
162
|
+
if (seconds <= HISTOGRAM_BUCKETS[i]) {
|
|
163
|
+
h.buckets[i]++;
|
|
164
|
+
placed = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Observations beyond the last bucket are counted in h.count only;
|
|
169
|
+
// the +Inf bucket in the exposition equals h.count.
|
|
170
|
+
if (!placed) {
|
|
171
|
+
// No bucket captured it — it lands in +Inf only.
|
|
172
|
+
}
|
|
173
|
+
h.sum += seconds;
|
|
174
|
+
h.count++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Prometheus text exposition ───────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/** Escape label value per Prometheus text format (backslash, newline, quote). */
|
|
180
|
+
function escapeLabelValue(v) {
|
|
181
|
+
return String(v).replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Build the `{k1="v1",k2="v2",...}` label-set string.
|
|
186
|
+
* @param {Record<string, string>} labels
|
|
187
|
+
*/
|
|
188
|
+
function labelSet(labels) {
|
|
189
|
+
const parts = Object.entries(labels).map(
|
|
190
|
+
([k, v]) => `${k}="${escapeLabelValue(v)}"`,
|
|
191
|
+
);
|
|
192
|
+
return `{${parts.join(',')}}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Render the complete Prometheus text exposition.
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function renderMetrics() {
|
|
200
|
+
const lines = [];
|
|
201
|
+
|
|
202
|
+
// ── http_requests_total ─────────────────────────────────────────────
|
|
203
|
+
lines.push('# HELP http_requests_total Total number of HTTP requests handled.');
|
|
204
|
+
lines.push('# TYPE http_requests_total counter');
|
|
205
|
+
for (const [key, count] of _counters) {
|
|
206
|
+
const [service, route, method, status_code] = key.split('\x00');
|
|
207
|
+
lines.push(
|
|
208
|
+
`http_requests_total${labelSet({ service, route, method, status_code })} ${count}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── http_request_duration_seconds ───────────────────────────────────
|
|
213
|
+
lines.push('# HELP http_request_duration_seconds HTTP request duration in seconds (histogram).');
|
|
214
|
+
lines.push('# TYPE http_request_duration_seconds histogram');
|
|
215
|
+
for (const [key, h] of _histograms) {
|
|
216
|
+
const [service, route, method] = key.split('\x00');
|
|
217
|
+
const base = { service, route, method };
|
|
218
|
+
// Cumulative buckets: le=X must be ≥ sum of all observations ≤ X.
|
|
219
|
+
let cumulative = 0;
|
|
220
|
+
for (let i = 0; i < HISTOGRAM_BUCKETS.length; i++) {
|
|
221
|
+
cumulative += h.buckets[i];
|
|
222
|
+
lines.push(
|
|
223
|
+
`http_request_duration_seconds_bucket${labelSet({ ...base, le: String(HISTOGRAM_BUCKETS[i]) })} ${cumulative}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
lines.push(
|
|
227
|
+
`http_request_duration_seconds_bucket${labelSet({ ...base, le: '+Inf' })} ${h.count}`,
|
|
228
|
+
);
|
|
229
|
+
lines.push(`http_request_duration_seconds_sum${labelSet(base)} ${h.sum}`);
|
|
230
|
+
lines.push(`http_request_duration_seconds_count${labelSet(base)} ${h.count}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
lines.push(''); // trailing newline
|
|
234
|
+
return lines.join('\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Request instrumentation wrapper ─────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Wrap an async request handler so every request is instrumented.
|
|
241
|
+
*
|
|
242
|
+
* The wrapper:
|
|
243
|
+
* 1. Derives a stable route pattern from req.url.
|
|
244
|
+
* 2. Starts a high-resolution timer.
|
|
245
|
+
* 3. Calls the original handler.
|
|
246
|
+
* 4. Records counter + histogram using the response's status code.
|
|
247
|
+
*
|
|
248
|
+
* Status code capture: we monkey-patch res.writeHead and res.end to intercept
|
|
249
|
+
* the status before it's sent. Falls back to res.statusCode (which Node sets
|
|
250
|
+
* implicitly on .end() when no explicit writeHead call was made).
|
|
251
|
+
*
|
|
252
|
+
* @param {string} serviceName — emitted as the `service` label
|
|
253
|
+
* @param {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>} handler
|
|
254
|
+
* @returns {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => Promise<void>}
|
|
255
|
+
*/
|
|
256
|
+
export function instrumentHandler(serviceName, handler) {
|
|
257
|
+
return async (req, res) => {
|
|
258
|
+
const start = performance.now();
|
|
259
|
+
|
|
260
|
+
// Intercept status code by wrapping writeHead.
|
|
261
|
+
let capturedStatus = null;
|
|
262
|
+
const origWriteHead = res.writeHead.bind(res);
|
|
263
|
+
res.writeHead = (status, ...rest) => {
|
|
264
|
+
capturedStatus = status;
|
|
265
|
+
return origWriteHead(status, ...rest);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await handler(req, res);
|
|
270
|
+
} finally {
|
|
271
|
+
const durationSec = (performance.now() - start) / 1000;
|
|
272
|
+
const urlObj = new URL(req.url ?? '/', `http://localhost`);
|
|
273
|
+
const route = pathToRoute(urlObj.pathname);
|
|
274
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
275
|
+
const statusCode = String(capturedStatus ?? res.statusCode ?? 200);
|
|
276
|
+
|
|
277
|
+
incRequest(serviceName, route, method, statusCode);
|
|
278
|
+
observeDuration(serviceName, route, method, durationSec);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
package/host-cp/src/server.mjs
CHANGED
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
handleListServers,
|
|
72
72
|
handleServerBridges,
|
|
73
73
|
} from './routes/process-port.mjs';
|
|
74
|
+
import { instrumentHandler, renderMetrics } from './metrics.mjs';
|
|
74
75
|
|
|
75
76
|
// ── Deployment-mode detection ─────────────────────────────────────
|
|
76
77
|
//
|
|
@@ -680,7 +681,10 @@ async function getSecret(worldId) {
|
|
|
680
681
|
|
|
681
682
|
// ── HTTP server ──────────────────────────────────────────────────────
|
|
682
683
|
|
|
683
|
-
|
|
684
|
+
// Phase C Task C3: wrap the raw handler with the Prometheus instrumentation
|
|
685
|
+
// wrapper. Every request increments http_requests_total and observes
|
|
686
|
+
// http_request_duration_seconds before the response is sent.
|
|
687
|
+
const server = http.createServer(instrumentHandler('host-cp', async (req, res) => {
|
|
684
688
|
const url = new URL(req.url ?? '/', `http://${req.headers.host}`);
|
|
685
689
|
|
|
686
690
|
// /health: fast diagnostics, no auth, no proxying. Docker healthcheck
|
|
@@ -717,6 +721,22 @@ const server = http.createServer(async (req, res) => {
|
|
|
717
721
|
});
|
|
718
722
|
}
|
|
719
723
|
|
|
724
|
+
// /metrics: Prometheus text exposition (Phase C Task C3).
|
|
725
|
+
// Unauthenticated — same rationale as /health: the Prometheus scraper
|
|
726
|
+
// in-cluster cannot carry the operator's session token.
|
|
727
|
+
// Returns only the 4 taxonomy-compliant labels {service,route,method,status_code}.
|
|
728
|
+
// BANNED labels (world_id, trace_id, user_id, request_id, operator_id)
|
|
729
|
+
// are never emitted here; layer-2 enforcement is the ServiceMonitor labeldrop.
|
|
730
|
+
if (url.pathname === '/metrics') {
|
|
731
|
+
const body = renderMetrics();
|
|
732
|
+
res.writeHead(200, {
|
|
733
|
+
'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
|
|
734
|
+
'Cache-Control': 'no-cache, no-store',
|
|
735
|
+
});
|
|
736
|
+
res.end(body);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
720
740
|
// /api/bootstrap: SPA reads the token at load time. Unauthed because
|
|
721
741
|
// anything local that can hit 127.0.0.1:19000 can also read the token
|
|
722
742
|
// file directly (same OS-level privilege boundary). Single-user-only;
|
|
@@ -2178,7 +2198,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2178
2198
|
pathname: url.pathname,
|
|
2179
2199
|
message: 'B3 ships /health + /api/world/<id>/*. B4-B9 ship the rest.',
|
|
2180
2200
|
});
|
|
2181
|
-
});
|
|
2201
|
+
}));
|
|
2182
2202
|
|
|
2183
2203
|
/**
|
|
2184
2204
|
* @param {import('node:http').ServerResponse} res
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pleri/olam-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.158",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"olam": "./bin/olam.cjs"
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"bin",
|
|
10
10
|
"dist",
|
|
11
|
+
"hermes-bundle",
|
|
11
12
|
"host-cp",
|
|
12
|
-
"memory-service-bundle",
|
|
13
13
|
"plugin",
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"@inquirer/prompts": "^7.0.0",
|
|
48
48
|
"zod-to-json-schema": "^3.24.0",
|
|
49
49
|
"playwright-core": "~1.59.0",
|
|
50
|
-
"@napi-rs/keyring": "^1.1.6"
|
|
51
|
-
"@agentmemory/agentmemory": "0.9.6"
|
|
50
|
+
"@napi-rs/keyring": "^1.1.6"
|
|
52
51
|
}
|
|
53
52
|
}
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* ensure-iii-engine.mjs — install (or verify) the iii-engine v0.11.2
|
|
4
|
-
* binary that the agentmemory CLI depends on.
|
|
5
|
-
*
|
|
6
|
-
* Why this exists:
|
|
7
|
-
* `agentmemory` (npm-installed) launches the iii-engine as a subprocess
|
|
8
|
-
* but does NOT bundle the engine binary — it expects a system-installed
|
|
9
|
-
* `iii` on PATH. Upstream's documented install path A is a `curl`
|
|
10
|
-
* recipe that drops the binary into `~/.local/bin`. We do the
|
|
11
|
-
* equivalent into `~/.olam/bin/iii` so the engine lives under olam's
|
|
12
|
-
* namespace + never collides with an operator's own iii install
|
|
13
|
-
* (OQ9 resolved pass 3).
|
|
14
|
-
*
|
|
15
|
-
* Pinned to iii v0.11.2 exactly. Upstream agentmemory's
|
|
16
|
-
* `docker-compose.yml` documents that v0.11.6+ breaks with
|
|
17
|
-
* "EPIPE reconnect loops and empty search after save" — bumping is
|
|
18
|
-
* not safe until agentmemory is refactored for the new sandbox-worker
|
|
19
|
-
* model.
|
|
20
|
-
*
|
|
21
|
-
* SHA256 integrity: each tarball is verified against a hash pinned
|
|
22
|
-
* in this file before extract. Mismatch → exit 1 (defends against
|
|
23
|
-
* github.com/iii-hq/iii release tampering — T9).
|
|
24
|
-
*
|
|
25
|
-
* Hashes captured 2026-05-12 from `.sha256` sidecars at
|
|
26
|
-
* https://github.com/iii-hq/iii/releases/tag/iii%2Fv0.11.2
|
|
27
|
-
*
|
|
28
|
-
* Plan reference: docs/plans/olam-agent-memory-distributed/phase-a-tasks.md A1
|
|
29
|
-
* Research: docs/research/agent-memory-distributed/REPORT.md Q1
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { existsSync, mkdirSync, writeFileSync, unlinkSync, chmodSync, statSync } from 'node:fs';
|
|
33
|
-
import { homedir, platform, arch } from 'node:os';
|
|
34
|
-
import { join } from 'node:path';
|
|
35
|
-
import { createHash } from 'node:crypto';
|
|
36
|
-
import { execSync } from 'node:child_process';
|
|
37
|
-
|
|
38
|
-
export const III_VERSION = '0.11.2';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Map of {process.platform-process.arch} → upstream Rust target triple.
|
|
42
|
-
* Only host platforms olam supports for local mode. Cloud-mode containers
|
|
43
|
-
* (Phase C Dockerfile) handle linux-only via TARGETARCH ARG separately.
|
|
44
|
-
*/
|
|
45
|
-
export const PLATFORM_ARCH_TO_TRIPLE = Object.freeze({
|
|
46
|
-
'darwin-arm64': 'aarch64-apple-darwin',
|
|
47
|
-
'darwin-x64': 'x86_64-apple-darwin',
|
|
48
|
-
'linux-arm64': 'aarch64-unknown-linux-gnu',
|
|
49
|
-
'linux-x64': 'x86_64-unknown-linux-gnu',
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* SHA256 hashes of the upstream iii v0.11.2 release tarballs.
|
|
54
|
-
* Captured from `.sha256` sidecars on the iii-hq/iii GitHub release.
|
|
55
|
-
* If these ever fail to match after `iii v0.11.2` is published, upstream
|
|
56
|
-
* has tampered with the release — refuse to install + open an issue.
|
|
57
|
-
*/
|
|
58
|
-
export const SHA256 = Object.freeze({
|
|
59
|
-
'aarch64-apple-darwin': 'e7834c44fefb2b5343d327102a941419245f7fff447f95373857a04b033fb1bd',
|
|
60
|
-
'x86_64-apple-darwin': '2b67e5f18833c415f4cb16a9e13b0e953555e0ca138682bf24894abe8b80b836',
|
|
61
|
-
'aarch64-unknown-linux-gnu': 'e0d35ee54a6b6c8a46576ab661e1711d11eb4dafeb1be8e1dbd1cc0ccb48b615',
|
|
62
|
-
'x86_64-unknown-linux-gnu': '9c83c47788b4ef4beeb65dd9bf37e94f993770cd3db874464c3ce1cdc92352cd',
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
export function detectTriple(plat = platform(), cpuArch = arch()) {
|
|
66
|
-
const key = `${plat}-${cpuArch}`;
|
|
67
|
-
const triple = PLATFORM_ARCH_TO_TRIPLE[key];
|
|
68
|
-
if (!triple) {
|
|
69
|
-
const supported = Object.keys(PLATFORM_ARCH_TO_TRIPLE).join(', ');
|
|
70
|
-
throw new Error(
|
|
71
|
-
`Unsupported host platform/arch combo: ${key}. ` +
|
|
72
|
-
`iii v${III_VERSION} ships for: ${supported}. ` +
|
|
73
|
-
`Run agentmemory directly via 'npx @agentmemory/agentmemory' if your platform is missing.`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
return triple;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function urlForTriple(triple) {
|
|
80
|
-
return `https://github.com/iii-hq/iii/releases/download/iii/v${III_VERSION}/iii-${triple}.tar.gz`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Idempotent install + verify. Returns { ok: true, path, cached } on
|
|
85
|
-
* success; throws on failure (caller decides exit code).
|
|
86
|
-
*
|
|
87
|
-
* deps is for testability:
|
|
88
|
-
* - fetch — substitutable in tests (default: global fetch)
|
|
89
|
-
* - logStream — process.stderr in prod; a fake in tests
|
|
90
|
-
*/
|
|
91
|
-
export async function ensureIiiEngine({
|
|
92
|
-
olamHome = join(homedir(), '.olam'),
|
|
93
|
-
triple = detectTriple(),
|
|
94
|
-
fetchImpl = globalThis.fetch,
|
|
95
|
-
logStream = process.stderr,
|
|
96
|
-
} = {}) {
|
|
97
|
-
const binDir = join(olamHome, 'bin');
|
|
98
|
-
const binPath = join(binDir, 'iii');
|
|
99
|
-
|
|
100
|
-
// Cached?
|
|
101
|
-
if (existsSync(binPath)) {
|
|
102
|
-
try {
|
|
103
|
-
const out = execSync(`${binPath} --version`, { encoding: 'utf8', timeout: 5_000 });
|
|
104
|
-
if (out.includes(III_VERSION)) {
|
|
105
|
-
return { ok: true, path: binPath, cached: true };
|
|
106
|
-
}
|
|
107
|
-
logStream.write(`iii at ${binPath} reports unexpected version: ${out.trim()}; re-installing\n`);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
logStream.write(`iii at ${binPath} not executable (${err.message}); re-installing\n`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const expectedSha = SHA256[triple];
|
|
114
|
-
if (!expectedSha) {
|
|
115
|
-
throw new Error(`No SHA256 pinned for triple ${triple} — update ensure-iii-engine.mjs.`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const url = urlForTriple(triple);
|
|
119
|
-
logStream.write(`Downloading iii v${III_VERSION} (${triple}) from ${url}\n`);
|
|
120
|
-
|
|
121
|
-
const response = await fetchImpl(url);
|
|
122
|
-
if (!response.ok) {
|
|
123
|
-
throw new Error(`Failed to download iii v${III_VERSION} (${triple}): HTTP ${response.status}`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const buf = Buffer.from(await response.arrayBuffer());
|
|
127
|
-
const actualSha = createHash('sha256').update(buf).digest('hex');
|
|
128
|
-
|
|
129
|
-
if (actualSha !== expectedSha) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`iii v${III_VERSION} tarball SHA256 mismatch for ${triple}:\n` +
|
|
132
|
-
` expected: ${expectedSha}\n` +
|
|
133
|
-
` got: ${actualSha}\n` +
|
|
134
|
-
`If this is a legitimate upstream change, update SHA256 in ${import.meta.url} ` +
|
|
135
|
-
`and rotate the pin (see plan T9).`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
mkdirSync(binDir, { recursive: true });
|
|
140
|
-
|
|
141
|
-
// Extract via system tar — saves bundling node-tar
|
|
142
|
-
const tarPath = join(binDir, '.iii.tar.gz');
|
|
143
|
-
writeFileSync(tarPath, buf);
|
|
144
|
-
try {
|
|
145
|
-
execSync(`tar -xzf '${tarPath}' -C '${binDir}'`, { stdio: 'pipe' });
|
|
146
|
-
} finally {
|
|
147
|
-
if (existsSync(tarPath)) unlinkSync(tarPath);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!existsSync(binPath)) {
|
|
151
|
-
throw new Error(`tar extract succeeded but iii binary not found at ${binPath}`);
|
|
152
|
-
}
|
|
153
|
-
chmodSync(binPath, 0o755);
|
|
154
|
-
|
|
155
|
-
// Smoke
|
|
156
|
-
const ver = execSync(`${binPath} --version`, { encoding: 'utf8', timeout: 5_000 });
|
|
157
|
-
if (!ver.includes(III_VERSION)) {
|
|
158
|
-
throw new Error(`iii binary installed but reports unexpected version: ${ver.trim()}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return { ok: true, path: binPath, cached: false };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// CLI entry — `node ensure-iii-engine.mjs`
|
|
165
|
-
const isDirect = import.meta.url === `file://${process.argv[1]}` ||
|
|
166
|
-
process.argv[1]?.endsWith('/ensure-iii-engine.mjs');
|
|
167
|
-
if (isDirect) {
|
|
168
|
-
ensureIiiEngine()
|
|
169
|
-
.then((r) => {
|
|
170
|
-
process.stderr.write(
|
|
171
|
-
`iii v${III_VERSION} ready at ${r.path}${r.cached ? ' (cached)' : ''}\n`,
|
|
172
|
-
);
|
|
173
|
-
process.exit(0);
|
|
174
|
-
})
|
|
175
|
-
.catch((err) => {
|
|
176
|
-
process.stderr.write(`ensure-iii-engine failed: ${err.message}\n`);
|
|
177
|
-
process.exit(1);
|
|
178
|
-
});
|
|
179
|
-
}
|