@pleri/olam-cli 0.1.145 → 0.1.147

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 (90) hide show
  1. package/dist/commands/skills-doctor.d.ts +14 -0
  2. package/dist/commands/skills-doctor.d.ts.map +1 -0
  3. package/dist/commands/skills-doctor.js +126 -0
  4. package/dist/commands/skills-doctor.js.map +1 -0
  5. package/dist/commands/skills-hook.d.ts +19 -0
  6. package/dist/commands/skills-hook.d.ts.map +1 -0
  7. package/dist/commands/skills-hook.js +99 -0
  8. package/dist/commands/skills-hook.js.map +1 -0
  9. package/dist/commands/skills-migrate-back.d.ts +21 -0
  10. package/dist/commands/skills-migrate-back.d.ts.map +1 -0
  11. package/dist/commands/skills-migrate-back.js +222 -0
  12. package/dist/commands/skills-migrate-back.js.map +1 -0
  13. package/dist/commands/skills-migrate-hooks-back.d.ts +19 -0
  14. package/dist/commands/skills-migrate-hooks-back.d.ts.map +1 -0
  15. package/dist/commands/skills-migrate-hooks-back.js +83 -0
  16. package/dist/commands/skills-migrate-hooks-back.js.map +1 -0
  17. package/dist/commands/skills-migrate-hooks.d.ts +40 -0
  18. package/dist/commands/skills-migrate-hooks.d.ts.map +1 -0
  19. package/dist/commands/skills-migrate-hooks.js +178 -0
  20. package/dist/commands/skills-migrate-hooks.js.map +1 -0
  21. package/dist/commands/skills-migrate.d.ts +33 -0
  22. package/dist/commands/skills-migrate.d.ts.map +1 -0
  23. package/dist/commands/skills-migrate.js +216 -0
  24. package/dist/commands/skills-migrate.js.map +1 -0
  25. package/dist/commands/skills-onboard.d.ts +26 -0
  26. package/dist/commands/skills-onboard.d.ts.map +1 -0
  27. package/dist/commands/skills-onboard.js +227 -0
  28. package/dist/commands/skills-onboard.js.map +1 -0
  29. package/dist/commands/skills-shadow-backups.d.ts +15 -0
  30. package/dist/commands/skills-shadow-backups.d.ts.map +1 -0
  31. package/dist/commands/skills-shadow-backups.js +132 -0
  32. package/dist/commands/skills-shadow-backups.js.map +1 -0
  33. package/dist/commands/skills-source.d.ts +25 -0
  34. package/dist/commands/skills-source.d.ts.map +1 -1
  35. package/dist/commands/skills-source.js +305 -7
  36. package/dist/commands/skills-source.js.map +1 -1
  37. package/dist/commands/skills.d.ts.map +1 -1
  38. package/dist/commands/skills.js +62 -7
  39. package/dist/commands/skills.js.map +1 -1
  40. package/dist/commands/substrate-audit-log.d.ts +49 -0
  41. package/dist/commands/substrate-audit-log.d.ts.map +1 -0
  42. package/dist/commands/substrate-audit-log.js +148 -0
  43. package/dist/commands/substrate-audit-log.js.map +1 -0
  44. package/dist/commands/substrate.d.ts +60 -0
  45. package/dist/commands/substrate.d.ts.map +1 -0
  46. package/dist/commands/substrate.js +175 -0
  47. package/dist/commands/substrate.js.map +1 -0
  48. package/dist/commands/upgrade.d.ts +10 -0
  49. package/dist/commands/upgrade.d.ts.map +1 -1
  50. package/dist/commands/upgrade.js +30 -0
  51. package/dist/commands/upgrade.js.map +1 -1
  52. package/dist/image-digests.json +7 -7
  53. package/dist/index.js +8687 -4713
  54. package/dist/index.js.map +1 -1
  55. package/dist/lib/config.d.ts +69 -0
  56. package/dist/lib/config.d.ts.map +1 -0
  57. package/dist/lib/config.js +146 -0
  58. package/dist/lib/config.js.map +1 -0
  59. package/dist/lib/instrumentation.d.ts +85 -0
  60. package/dist/lib/instrumentation.d.ts.map +1 -0
  61. package/dist/lib/instrumentation.js +104 -0
  62. package/dist/lib/instrumentation.js.map +1 -0
  63. package/dist/lib/kubectl-wrap.d.ts +59 -0
  64. package/dist/lib/kubectl-wrap.d.ts.map +1 -0
  65. package/dist/lib/kubectl-wrap.js +130 -0
  66. package/dist/lib/kubectl-wrap.js.map +1 -0
  67. package/dist/lib/manifest-refresh.d.ts +95 -0
  68. package/dist/lib/manifest-refresh.d.ts.map +1 -0
  69. package/dist/lib/manifest-refresh.js +222 -0
  70. package/dist/lib/manifest-refresh.js.map +1 -0
  71. package/dist/lib/port-forward.d.ts +101 -0
  72. package/dist/lib/port-forward.d.ts.map +1 -0
  73. package/dist/lib/port-forward.js +240 -0
  74. package/dist/lib/port-forward.js.map +1 -0
  75. package/dist/lib/upgrade-kubernetes.d.ts +77 -0
  76. package/dist/lib/upgrade-kubernetes.d.ts.map +1 -0
  77. package/dist/lib/upgrade-kubernetes.js +277 -0
  78. package/dist/lib/upgrade-kubernetes.js.map +1 -0
  79. package/dist/mcp-server.js +20262 -18211
  80. package/host-cp/k8s/manifests/00-namespace.yaml +7 -0
  81. package/host-cp/k8s/manifests/10-serviceaccount.yaml +8 -0
  82. package/host-cp/k8s/manifests/20-rbac.yaml +34 -0
  83. package/host-cp/k8s/manifests/30-configmap.yaml +30 -0
  84. package/host-cp/k8s/manifests/45-pvc.yaml +27 -0
  85. package/host-cp/k8s/manifests/50-deployment.yaml +148 -0
  86. package/host-cp/k8s/manifests/60-service.yaml +22 -0
  87. package/host-cp/k8s/templates/40-secret-template.yaml +32 -0
  88. package/host-cp/src/plan-chat-service.mjs +31 -7
  89. package/host-cp/src/server.mjs +32 -7
  90. package/package.json +3 -2
@@ -0,0 +1,7 @@
1
+ apiVersion: v1
2
+ kind: Namespace
3
+ metadata:
4
+ name: olam
5
+ labels:
6
+ name: olam
7
+ olam.io/component: host-stack
@@ -0,0 +1,8 @@
1
+ apiVersion: v1
2
+ kind: ServiceAccount
3
+ metadata:
4
+ name: olam-host-cp
5
+ namespace: olam
6
+ labels:
7
+ app: olam-host-cp
8
+ olam.io/component: host-stack
@@ -0,0 +1,34 @@
1
+ # Phase 1b Decision 19: Role scoped to resourceNames: ["olam-host-cp"] on
2
+ # apps/v1 deployments. Without this scope, the in-cluster ServiceAccount
3
+ # could patch ANY Deployment in the namespace. This is the load-bearing
4
+ # security guardrail — preserve verbatim.
5
+ apiVersion: rbac.authorization.k8s.io/v1
6
+ kind: Role
7
+ metadata:
8
+ name: olam-host-cp
9
+ namespace: olam
10
+ labels:
11
+ app: olam-host-cp
12
+ olam.io/component: host-stack
13
+ rules:
14
+ - apiGroups: ["apps"]
15
+ resources: ["deployments"]
16
+ resourceNames: ["olam-host-cp"]
17
+ verbs: ["get", "patch", "watch"]
18
+ ---
19
+ apiVersion: rbac.authorization.k8s.io/v1
20
+ kind: RoleBinding
21
+ metadata:
22
+ name: olam-host-cp
23
+ namespace: olam
24
+ labels:
25
+ app: olam-host-cp
26
+ olam.io/component: host-stack
27
+ subjects:
28
+ - kind: ServiceAccount
29
+ name: olam-host-cp
30
+ namespace: olam
31
+ roleRef:
32
+ kind: Role
33
+ name: olam-host-cp
34
+ apiGroup: rbac.authorization.k8s.io
@@ -0,0 +1,30 @@
1
+ # ConfigMap for olam-host-cp environment. Sensitive values (OLAM_AUTH_SECRET,
2
+ # GH_TOKEN) are NOT here — they live in the Secret (see templates/40-secret-template.yaml).
3
+ # Operators apply the Secret separately before applying the manifests.
4
+ apiVersion: v1
5
+ kind: ConfigMap
6
+ metadata:
7
+ name: olam-host-cp-env
8
+ namespace: olam
9
+ labels:
10
+ app: olam-host-cp
11
+ olam.io/component: host-stack
12
+ data:
13
+ # Auth service URL. Default targets host.docker.internal for Colima/Docker
14
+ # Desktop k3d setups. Override when auth-service runs elsewhere (e.g. via
15
+ # an ExternalName Service pointing at the host gateway).
16
+ OLAM_AUTH_SERVICE_URL: "http://host.docker.internal:8000"
17
+ # Docker socket proxy — ClusterIP Service DNS inside the namespace.
18
+ DOCKER_HOST: "tcp://docker-socket-proxy:2375"
19
+ # Host-cp server port — must match the Service targetPort in 60-service.yaml.
20
+ OLAM_HOST_CP_PORT: "19000"
21
+ # Operator state paths (resolved inside the K3s node via hostPath volumes).
22
+ OLAM_HOST_CP_TOKEN_PATH: "/data/host-cp.token"
23
+ OLAM_WORKSPACES_DIR: "/data/workspaces"
24
+ OLAM_WORLDS_DB: "/data/worlds.db"
25
+ OLAM_PLAN_DB_PATH: "/data/plan.db"
26
+ OLAM_PLAN_DIR: "/data/plan"
27
+ # Tunable defaults.
28
+ OLAM_SECRET_CACHE_TTL_SEC: "300"
29
+ OLAM_PR_POLL_INTERVAL_MS: "300000"
30
+ OLAM_MERGE_GRACE_MS: "600000"
@@ -0,0 +1,27 @@
1
+ # PersistentVolumeClaim for olam-host-cp /data volume.
2
+ #
3
+ # Why PVC instead of hostPath:
4
+ # hostPath volumes on k3d nodes resolve to paths INSIDE the k3d node
5
+ # container — not the operator's host filesystem. A bare k3d cluster has
6
+ # an empty node filesystem, so a hostPath at /host/.olam is always empty.
7
+ # Additionally, fsGroup does NOT relabel hostPath volumes (only PVCs /
8
+ # emptyDir / projected volumes), so UID-1000 pods cannot write to
9
+ # root-owned hostPath mounts even when fsGroup: 1000 is set.
10
+ #
11
+ # local-path StorageClass ships with k3d by default (rancher/local-path-provisioner).
12
+ # On non-k3d clusters, substitute with the appropriate StorageClass name.
13
+ apiVersion: v1
14
+ kind: PersistentVolumeClaim
15
+ metadata:
16
+ name: olam-host-cp-data
17
+ namespace: olam
18
+ labels:
19
+ app: olam-host-cp
20
+ olam.io/component: host-stack
21
+ spec:
22
+ accessModes:
23
+ - ReadWriteOnce
24
+ storageClassName: local-path
25
+ resources:
26
+ requests:
27
+ storage: 5Gi
@@ -0,0 +1,148 @@
1
+ # Deployment for olam-host-cp.
2
+ #
3
+ # Image: pinned to sha256 digest (not :latest or named tag) per T4 threat model.
4
+ # Digest resolves to ghcr.io/pleri/olam-host-cp:0.1.143 (multi-arch index).
5
+ # To update: resolve the new tag's digest via:
6
+ # TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:pleri/olam-host-cp:pull&service=ghcr.io" | jq -r .token)
7
+ # curl -sI -H "Authorization: Bearer $TOKEN" \
8
+ # -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" \
9
+ # https://ghcr.io/v2/pleri/olam-host-cp/manifests/<tag> | grep docker-content-digest
10
+ #
11
+ # securityContext: conservative defaults per T6/T7 threat model.
12
+ # Operators who need to relax these (e.g. for debugging) must pass
13
+ # --accept-security-regression (Phase C, Decision D14) — out of scope here.
14
+ #
15
+ # Volume requirements for k3d:
16
+ # olam-home (/data): backed by a PersistentVolumeClaim (45-pvc.yaml).
17
+ # An init container (chown-data) runs `chown -R 1000:1000 /data` as root
18
+ # before the main container starts, granting UID-1000 write access on the
19
+ # freshly-provisioned PV. fsGroup alone is insufficient for hostPath volumes.
20
+ #
21
+ # gh-config (/gh-config) and operator-repo (/operator-repo) remain hostPath
22
+ # volumes that resolve to paths inside the k3d node container.
23
+ # OPERATORS MUST pass these volume mounts when creating the k3d cluster:
24
+ #
25
+ # k3d cluster create olam-host \
26
+ # --volume ~/.config/gh:/host/.config/gh \
27
+ # --volume <olam-repo-root>:/host/olam \
28
+ # --wait --timeout 90s
29
+ #
30
+ # Without these flags the gh-config and operator-repo mounts will be empty
31
+ # (the paths exist inside the node container but contain no data from the
32
+ # operator's host). The pod will still start — features that depend on GitHub
33
+ # auth or the operator repo will fail gracefully. The Phase D install guide
34
+ # surfaces this requirement prominently.
35
+ apiVersion: apps/v1
36
+ kind: Deployment
37
+ metadata:
38
+ name: olam-host-cp
39
+ namespace: olam
40
+ labels:
41
+ app: olam-host-cp
42
+ olam.io/component: host-stack
43
+ spec:
44
+ replicas: 1
45
+ strategy:
46
+ type: RollingUpdate
47
+ rollingUpdate:
48
+ maxSurge: 1
49
+ maxUnavailable: 0
50
+ selector:
51
+ matchLabels:
52
+ app: olam-host-cp
53
+ template:
54
+ metadata:
55
+ labels:
56
+ app: olam-host-cp
57
+ spec:
58
+ serviceAccountName: olam-host-cp
59
+ securityContext:
60
+ runAsNonRoot: true
61
+ runAsUser: 1000
62
+ runAsGroup: 1000
63
+ fsGroup: 1000
64
+ initContainers:
65
+ - name: chown-data
66
+ # busybox:1.36 — sha256-pinned per T4 threat model.
67
+ # To update: docker pull busybox:1.36 && docker inspect busybox:1.36 --format '{{index .RepoDigests 0}}'
68
+ image: busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
69
+ imagePullPolicy: IfNotPresent
70
+ # Run as root to chown the freshly-provisioned PV to UID 1000.
71
+ # The pod-level runAsNonRoot: true is overridden here deliberately.
72
+ # The main container still runs as UID 1000 with all security defaults intact.
73
+ securityContext:
74
+ runAsUser: 0
75
+ runAsNonRoot: false
76
+ allowPrivilegeEscalation: false
77
+ command: ["chown", "-R", "1000:1000", "/data"]
78
+ volumeMounts:
79
+ - name: olam-home
80
+ mountPath: /data
81
+ containers:
82
+ - name: olam-host-cp
83
+ image: ghcr.io/pleri/olam-host-cp@sha256:513b16e1c36c96f4a03b431445da45cabf83c85b5761d1c93fab684a13c7354b
84
+ imagePullPolicy: IfNotPresent
85
+ securityContext:
86
+ runAsNonRoot: true
87
+ runAsUser: 1000
88
+ readOnlyRootFilesystem: true
89
+ allowPrivilegeEscalation: false
90
+ capabilities:
91
+ drop: ["ALL"]
92
+ ports:
93
+ - name: http
94
+ containerPort: 19000
95
+ protocol: TCP
96
+ envFrom:
97
+ - configMapRef:
98
+ name: olam-host-cp-env
99
+ - secretRef:
100
+ name: olam-host-cp-secret
101
+ volumeMounts:
102
+ - name: olam-home
103
+ mountPath: /data
104
+ - name: gh-config
105
+ mountPath: /gh-config
106
+ readOnly: true
107
+ - name: operator-repo
108
+ mountPath: /operator-repo
109
+ readOnly: true
110
+ - name: tmp
111
+ mountPath: /tmp
112
+ readinessProbe:
113
+ httpGet:
114
+ path: /api/version/status
115
+ port: 19000
116
+ initialDelaySeconds: 5
117
+ periodSeconds: 5
118
+ timeoutSeconds: 3
119
+ failureThreshold: 6
120
+ livenessProbe:
121
+ httpGet:
122
+ path: /api/version/status
123
+ port: 19000
124
+ initialDelaySeconds: 30
125
+ periodSeconds: 20
126
+ timeoutSeconds: 5
127
+ failureThreshold: 3
128
+ resources:
129
+ requests:
130
+ cpu: "50m"
131
+ memory: "256Mi"
132
+ limits:
133
+ cpu: "1000m"
134
+ memory: "1Gi"
135
+ volumes:
136
+ - name: olam-home
137
+ persistentVolumeClaim:
138
+ claimName: olam-host-cp-data
139
+ - name: gh-config
140
+ hostPath:
141
+ path: /host/.config/gh
142
+ type: DirectoryOrCreate
143
+ - name: operator-repo
144
+ hostPath:
145
+ path: /host/olam
146
+ type: DirectoryOrCreate
147
+ - name: tmp
148
+ emptyDir: {}
@@ -0,0 +1,22 @@
1
+ # ClusterIP Service for olam-host-cp.
2
+ # Operator surfaces the SPA externally via:
3
+ # kubectl port-forward -n olam svc/olam-host-cp 19000:19000
4
+ # This keeps the "127.0.0.1-only" single-user-per-host invariant
5
+ # (NodePort would bind on all interfaces; port-forward keeps it local).
6
+ apiVersion: v1
7
+ kind: Service
8
+ metadata:
9
+ name: olam-host-cp
10
+ namespace: olam
11
+ labels:
12
+ app: olam-host-cp
13
+ olam.io/component: host-stack
14
+ spec:
15
+ type: ClusterIP
16
+ selector:
17
+ app: olam-host-cp
18
+ ports:
19
+ - name: http
20
+ port: 19000
21
+ targetPort: 19000
22
+ protocol: TCP
@@ -0,0 +1,32 @@
1
+ # Secret TEMPLATE for olam-host-cp.
2
+ #
3
+ # This file is a TEMPLATE — it MUST NOT be applied directly without substituting
4
+ # the placeholder values. The placeholders are intentionally invalid; a raw
5
+ # `kubectl apply` will result in auth-service 401s rather than silently shipping
6
+ # fake credentials.
7
+ #
8
+ # Preferred substitution (keeps secrets out of git):
9
+ # kubectl create secret generic olam-host-cp-secret -n olam \
10
+ # --from-literal=OLAM_AUTH_SECRET=$(cat ~/.olam/auth-secret) \
11
+ # --from-literal=GH_TOKEN=$(gh auth token) \
12
+ # --dry-run=client -o yaml | kubectl apply -f -
13
+ #
14
+ # This template lives in packages/host-cp/k8s/templates/ (NOT manifests/)
15
+ # so that `kubectl apply -f manifests/` does NOT apply it — operators must
16
+ # explicitly handle Secret provisioning before applying the manifests.
17
+ apiVersion: v1
18
+ kind: Secret
19
+ metadata:
20
+ name: olam-host-cp-secret
21
+ namespace: olam
22
+ labels:
23
+ app: olam-host-cp
24
+ olam.io/component: host-stack
25
+ type: Opaque
26
+ stringData:
27
+ # Shared bearer secret between host-cp and the long-lived olam-auth process.
28
+ # Source: cat ~/.olam/auth-secret
29
+ OLAM_AUTH_SECRET: "REPLACE_ME_FROM_HOME_DOTOLAM_AUTH_SECRET"
30
+ # GitHub token for GHCR image pulls and the /api/prs endpoint.
31
+ # Source: gh auth token
32
+ GH_TOKEN: "REPLACE_ME_FROM_GH_AUTH_TOKEN"
@@ -80,19 +80,38 @@ const SCOPED_QUERY_PARAMS = new Set(['world_id', 'session_id', 'where']);
80
80
  * resolution is wired here. Remove the throw when this function actually
81
81
  * inspects the bearer.
82
82
  */
83
- function principalFromBearer(bearer) {
83
+ /**
84
+ * G1-demo lift (2026-05-16): trust the client-supplied `actor_type` when
85
+ * present. The original A2 design hardcoded ('agent','agent') with T3
86
+ * mitigation explicitly requiring server-injection — but that collapsed
87
+ * the operator + driver + codex roles into one and broke the driver
88
+ * pickup loop (which filters on `actor_type === 'operator'`).
89
+ *
90
+ * Single-operator local mode has no multi-tenant threat, so trusting the
91
+ * client field is safe pragmatic. The original silent-collapse safeguard
92
+ * for OLAM_PLAN_CHAT_SECRETS stays — multi-secret mode still needs A4
93
+ * proper bearer→principal resolution before it can land.
94
+ */
95
+ function principalFromBearer(bearer, body) {
84
96
  if (process.env.OLAM_PLAN_CHAT_SECRETS) {
85
- // Multi-secret mode reserved for A4 / A6.1; minimal A2 doesn't support it.
86
97
  throw new Error(
87
98
  'plan-chat-service: OLAM_PLAN_CHAT_SECRETS is set but principalFromBearer ' +
88
- "still hardcodes ('agent','agent'). Wire multi-principal resolution before " +
89
- 'enabling multi-secret mode (see plan A4 / A6.1).',
99
+ "still trusts client-supplied actor_type. Wire bearer→principal " +
100
+ 'resolution before enabling multi-secret mode (see plan A4 / A6.1).',
90
101
  );
91
102
  }
92
103
  if (typeof bearer !== 'string' || bearer.length === 0) {
93
104
  throw new Error('plan-chat-service: principalFromBearer called with empty bearer');
94
105
  }
95
- return { actorId: 'agent', actorType: 'agent' };
106
+ // Trust client field if supplied (SPA sends 'operator'; SDK adapter omits
107
+ // and inherits 'agent' default; codex-runner sends 'codex').
108
+ const claimed = body && typeof body.actor_type === 'string' && body.actor_type.length > 0
109
+ ? body.actor_type
110
+ : 'agent';
111
+ const claimedId = body && typeof body.actor_id === 'string' && body.actor_id.length > 0
112
+ ? body.actor_id
113
+ : claimed;
114
+ return { actorId: claimedId, actorType: claimed };
96
115
  }
97
116
 
98
117
  function readJson(req, limit = 65536) {
@@ -182,7 +201,7 @@ export function createHandler({ pool, bearer, electricUrl }) {
182
201
  }
183
202
  const invalid = validateChunkInput(body);
184
203
  if (invalid) return badRequest(res, invalid);
185
- const principal = principalFromBearer(bearer);
204
+ const principal = principalFromBearer(bearer, body);
186
205
  try {
187
206
  await pool.query(
188
207
  `INSERT INTO chunks
@@ -251,9 +270,14 @@ export function createHandler({ pool, bearer, electricUrl }) {
251
270
  }
252
271
  // PB2 — server-derived `where` clause; safe because SCOPE_ID_RE has
253
272
  // already rejected single-quotes, semicolons, and SQL meta-characters.
273
+ //
274
+ // Predicate order: session_id FIRST. Electric SQL caches shapes by the
275
+ // exact predicate string; flipping the original (world_id, session_id)
276
+ // order forces a fresh shape and avoids a known cache-staleness bug
277
+ // where an empty-result shape persists after new rows land.
254
278
  upstream.searchParams.set(
255
279
  'where',
256
- `world_id='${worldId}' AND session_id='${sessionId}'`,
280
+ `session_id='${sessionId}' AND world_id='${worldId}'`,
257
281
  );
258
282
 
259
283
  let upstreamRes;
@@ -2534,8 +2534,8 @@ const DIST_DIR = (() => {
2534
2534
  return '/app/dist'; // fallback; readFile will surface ENOENT
2535
2535
  })();
2536
2536
 
2537
- const SPA_ROUTES = new Set(['/', '/worlds', '/workspaces', '/inbox', '/repos', '/runbooks']);
2538
- const SPA_PREFIX = ['/world/', '/worlds/', '/workspaces/', '/inbox/', '/repos/', '/runbooks/', '/session/'];
2537
+ const SPA_ROUTES = new Set(['/', '/worlds', '/workspaces', '/inbox', '/repos', '/runbooks', '/plan']);
2538
+ const SPA_PREFIX = ['/world/', '/worlds/', '/workspaces/', '/inbox/', '/repos/', '/runbooks/', '/session/', '/plan/'];
2539
2539
 
2540
2540
  // Top-level path segments that are NOT world IDs. Mirrors RESERVED_SEGMENTS
2541
2541
  // in lib/worldId.ts — keep in sync.
@@ -2675,7 +2675,7 @@ async function tryServeStatic(req, res, pathname) {
2675
2675
  // memory thereafter. Cache invalidates on dist/ mtime change so a
2676
2676
  // rebuilt bundle is picked up without restart.
2677
2677
  let _spaCache = null;
2678
- let _spaCacheMtime = 0;
2678
+ let _spaCacheKey = '';
2679
2679
 
2680
2680
  /**
2681
2681
  * Bootstrap script injected into the SPA shell. Two responsibilities:
@@ -2755,9 +2755,32 @@ let _spaCacheMtime = 0;
2755
2755
  // failures — don't cause a refresh loop).
2756
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
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
+ }
2777
+
2758
2778
  async function renderSpaShell(filePath) {
2759
2779
  const stat = fs.statSync(filePath);
2760
- if (_spaCache !== null && stat.mtimeMs === _spaCacheMtime) {
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) {
2761
2784
  return _spaCache;
2762
2785
  }
2763
2786
  let html = fs.readFileSync(filePath, 'utf-8');
@@ -2768,10 +2791,12 @@ async function renderSpaShell(filePath) {
2768
2791
  // (/, /worlds, /workspaces, /world/<id>) reference the same bundle.
2769
2792
  html = html.replace(/(href|src)="\.\/assets\//g, '$1="/assets/');
2770
2793
  // Inject right after <head> so the bootstrap runs before any other
2771
- // script tag on the page.
2772
- html = html.replace(/<head>/i, `<head>\n ${BOOTSTRAP_SCRIPT}`);
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}`);
2773
2798
  _spaCache = html;
2774
- _spaCacheMtime = stat.mtimeMs;
2799
+ _spaCacheKey = cacheKey;
2775
2800
  return html;
2776
2801
  }
2777
2802
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pleri/olam-cli",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
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",