@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.
Files changed (121) hide show
  1. package/dist/commands/bootstrap.d.ts +2 -1
  2. package/dist/commands/bootstrap.d.ts.map +1 -1
  3. package/dist/commands/bootstrap.js +8 -10
  4. package/dist/commands/bootstrap.js.map +1 -1
  5. package/dist/commands/doctor.d.ts +22 -0
  6. package/dist/commands/doctor.d.ts.map +1 -1
  7. package/dist/commands/doctor.js +110 -7
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/flywheel/check-persona-skeleton.d.ts +7 -0
  10. package/dist/commands/flywheel/check-persona-skeleton.d.ts.map +1 -0
  11. package/dist/commands/flywheel/check-persona-skeleton.js +14 -0
  12. package/dist/commands/flywheel/check-persona-skeleton.js.map +1 -0
  13. package/dist/commands/flywheel/diversity-check.d.ts +7 -0
  14. package/dist/commands/flywheel/diversity-check.d.ts.map +1 -0
  15. package/dist/commands/flywheel/diversity-check.js +14 -0
  16. package/dist/commands/flywheel/diversity-check.js.map +1 -0
  17. package/dist/commands/flywheel/emit-breadcrumb.d.ts +20 -0
  18. package/dist/commands/flywheel/emit-breadcrumb.d.ts.map +1 -0
  19. package/dist/commands/flywheel/emit-breadcrumb.js +137 -0
  20. package/dist/commands/flywheel/emit-breadcrumb.js.map +1 -0
  21. package/dist/commands/flywheel/index.d.ts +27 -0
  22. package/dist/commands/flywheel/index.d.ts.map +1 -0
  23. package/dist/commands/flywheel/index.js +48 -0
  24. package/dist/commands/flywheel/index.js.map +1 -0
  25. package/dist/commands/flywheel/install-shims.d.ts +8 -0
  26. package/dist/commands/flywheel/install-shims.d.ts.map +1 -0
  27. package/dist/commands/flywheel/install-shims.js +15 -0
  28. package/dist/commands/flywheel/install-shims.js.map +1 -0
  29. package/dist/commands/flywheel/k10-measure.d.ts +7 -0
  30. package/dist/commands/flywheel/k10-measure.d.ts.map +1 -0
  31. package/dist/commands/flywheel/k10-measure.js +14 -0
  32. package/dist/commands/flywheel/k10-measure.js.map +1 -0
  33. package/dist/commands/flywheel/k5-score.d.ts +14 -0
  34. package/dist/commands/flywheel/k5-score.d.ts.map +1 -0
  35. package/dist/commands/flywheel/k5-score.js +59 -0
  36. package/dist/commands/flywheel/k5-score.js.map +1 -0
  37. package/dist/commands/flywheel/k5-validate.d.ts +15 -0
  38. package/dist/commands/flywheel/k5-validate.d.ts.map +1 -0
  39. package/dist/commands/flywheel/k5-validate.js +185 -0
  40. package/dist/commands/flywheel/k5-validate.js.map +1 -0
  41. package/dist/commands/flywheel/ping.d.ts +21 -0
  42. package/dist/commands/flywheel/ping.d.ts.map +1 -0
  43. package/dist/commands/flywheel/ping.js +79 -0
  44. package/dist/commands/flywheel/ping.js.map +1 -0
  45. package/dist/commands/flywheel/sanitize-persona-output.d.ts +7 -0
  46. package/dist/commands/flywheel/sanitize-persona-output.d.ts.map +1 -0
  47. package/dist/commands/flywheel/sanitize-persona-output.js +14 -0
  48. package/dist/commands/flywheel/sanitize-persona-output.js.map +1 -0
  49. package/dist/commands/hermes-kg-hook.d.ts +36 -0
  50. package/dist/commands/hermes-kg-hook.d.ts.map +1 -0
  51. package/dist/commands/hermes-kg-hook.js +80 -0
  52. package/dist/commands/hermes-kg-hook.js.map +1 -0
  53. package/dist/commands/hermes.d.ts +46 -0
  54. package/dist/commands/hermes.d.ts.map +1 -0
  55. package/dist/commands/hermes.js +320 -0
  56. package/dist/commands/hermes.js.map +1 -0
  57. package/dist/commands/kg-install-hook.d.ts +7 -1
  58. package/dist/commands/kg-install-hook.d.ts.map +1 -1
  59. package/dist/commands/kg-install-hook.js +122 -6
  60. package/dist/commands/kg-install-hook.js.map +1 -1
  61. package/dist/commands/memory/_paths.d.ts +13 -3
  62. package/dist/commands/memory/_paths.d.ts.map +1 -1
  63. package/dist/commands/memory/_paths.js +25 -22
  64. package/dist/commands/memory/_paths.js.map +1 -1
  65. package/dist/commands/memory/logs.d.ts +8 -4
  66. package/dist/commands/memory/logs.d.ts.map +1 -1
  67. package/dist/commands/memory/logs.js +18 -13
  68. package/dist/commands/memory/logs.js.map +1 -1
  69. package/dist/commands/memory/mode.d.ts.map +1 -1
  70. package/dist/commands/memory/mode.js +7 -3
  71. package/dist/commands/memory/mode.js.map +1 -1
  72. package/dist/commands/memory/start.d.ts +16 -14
  73. package/dist/commands/memory/start.d.ts.map +1 -1
  74. package/dist/commands/memory/start.js +55 -189
  75. package/dist/commands/memory/start.js.map +1 -1
  76. package/dist/commands/memory/status.d.ts +10 -8
  77. package/dist/commands/memory/status.d.ts.map +1 -1
  78. package/dist/commands/memory/status.js +35 -38
  79. package/dist/commands/memory/status.js.map +1 -1
  80. package/dist/commands/memory/stop.d.ts +5 -4
  81. package/dist/commands/memory/stop.d.ts.map +1 -1
  82. package/dist/commands/memory/stop.js +26 -55
  83. package/dist/commands/memory/stop.js.map +1 -1
  84. package/dist/commands/memory-service-container.d.ts +78 -0
  85. package/dist/commands/memory-service-container.d.ts.map +1 -0
  86. package/dist/commands/memory-service-container.js +187 -0
  87. package/dist/commands/memory-service-container.js.map +1 -0
  88. package/dist/commands/services.d.ts +16 -1
  89. package/dist/commands/services.d.ts.map +1 -1
  90. package/dist/commands/services.js +88 -12
  91. package/dist/commands/services.js.map +1 -1
  92. package/dist/image-digests.json +7 -7
  93. package/dist/index.js +2570 -1384
  94. package/dist/index.js.map +1 -1
  95. package/dist/lib/k8s-secret-render.d.ts.map +1 -1
  96. package/dist/lib/k8s-secret-render.js +7 -4
  97. package/dist/lib/k8s-secret-render.js.map +1 -1
  98. package/dist/lib/memory-host-process-migration.d.ts +56 -0
  99. package/dist/lib/memory-host-process-migration.d.ts.map +1 -0
  100. package/dist/lib/memory-host-process-migration.js +156 -0
  101. package/dist/lib/memory-host-process-migration.js.map +1 -0
  102. package/dist/lib/upgrade-kubernetes.d.ts +22 -0
  103. package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
  104. package/dist/lib/upgrade-kubernetes.js +195 -2
  105. package/dist/lib/upgrade-kubernetes.js.map +1 -1
  106. package/dist/mcp-server.js +56 -22
  107. package/hermes-bundle/kg-first.sh +100 -0
  108. package/hermes-bundle/version.json +4 -0
  109. package/host-cp/k8s/manifests/50-deployment.yaml +54 -27
  110. package/host-cp/k8s/manifests/auth-service/30-configmap.yaml +5 -0
  111. package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +5 -1
  112. package/host-cp/k8s/manifests/kg-service/30-configmap.yaml +5 -0
  113. package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +5 -1
  114. package/host-cp/k8s/manifests/mcp-auth-service/30-configmap.yaml +4 -0
  115. package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +5 -1
  116. package/host-cp/k8s/manifests/memory-service/30-configmap.yaml +4 -0
  117. package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +5 -1
  118. package/host-cp/src/metrics.mjs +281 -0
  119. package/host-cp/src/server.mjs +22 -2
  120. package/package.json +3 -4
  121. 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:2d1b17fe2d53361a24ba9f137eaabc63df8c79f08d4eee93db42e5b909b41b14
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:36ee04cf04b62e1ac84473885abcbd8aabb0bbbd2ab0d4d15a5cfe9d0a3469fd
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
+ }
@@ -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
- const server = http.createServer(async (req, res) => {
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.153",
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
- }