@pleri/olam-cli 0.1.134 → 0.1.136

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.
@@ -11142,7 +11142,7 @@ function loadRepoManifest(repoDir) {
11142
11142
  }
11143
11143
  return { ...body, source };
11144
11144
  }
11145
- var runtimeSchema, appSchema, serviceSchema, deploySchema, BootstrapKindSchema, BootstrapStepSchema, RepoManifestSchema, KNOWN_TOP_LEVEL_KEYS, FORBIDDEN_KEYS;
11145
+ var runtimeSchema, appSchema, serviceSchema, deploySchema, planChatSchema, BootstrapKindSchema, BootstrapStepSchema, RepoManifestSchema, KNOWN_TOP_LEVEL_KEYS, FORBIDDEN_KEYS;
11146
11146
  var init_repo_manifest = __esm({
11147
11147
  "../core/dist/world/repo-manifest.js"() {
11148
11148
  "use strict";
@@ -11248,6 +11248,9 @@ var init_repo_manifest = __esm({
11248
11248
  deploySchema = external_exports.object({
11249
11249
  tags: external_exports.array(external_exports.string()).optional()
11250
11250
  }).passthrough();
11251
+ planChatSchema = external_exports.object({
11252
+ enabled: external_exports.boolean()
11253
+ }).strict();
11251
11254
  BootstrapKindSchema = external_exports.enum(["gems", "node", "pg"]);
11252
11255
  BootstrapStepSchema = external_exports.union([
11253
11256
  external_exports.string(),
@@ -11270,7 +11273,8 @@ var init_repo_manifest = __esm({
11270
11273
  secrets: external_exports.string().optional(),
11271
11274
  bootstrap: external_exports.array(BootstrapStepSchema).optional(),
11272
11275
  start: external_exports.string().optional(),
11273
- deploy: deploySchema.optional()
11276
+ deploy: deploySchema.optional(),
11277
+ plan_chat: planChatSchema.optional()
11274
11278
  // TODO(phase-C): runtime field consumed by stack-install.ts (T16).
11275
11279
  // TODO(phase-D): app.fixed (sticky port) consumed by port-allocation.
11276
11280
  // TODO(phase-D): idempotent_check on bootstrap steps in bootstrap-runner.
@@ -11278,11 +11282,11 @@ var init_repo_manifest = __esm({
11278
11282
  }).passthrough().superRefine((val, ctx) => {
11279
11283
  refineForbiddenKeys(val, [], ctx, true);
11280
11284
  }).superRefine((val, ctx) => {
11281
- const hasContent = val.version !== void 0 || val.runtime !== void 0 || val.app !== void 0 || val.services !== void 0 || val.env !== void 0 || val.secrets !== void 0 || val.bootstrap !== void 0 || val.start !== void 0 || val.deploy !== void 0;
11285
+ const hasContent = val.version !== void 0 || val.runtime !== void 0 || val.app !== void 0 || val.services !== void 0 || val.env !== void 0 || val.secrets !== void 0 || val.bootstrap !== void 0 || val.start !== void 0 || val.deploy !== void 0 || val.plan_chat !== void 0;
11282
11286
  if (!hasContent) {
11283
11287
  ctx.addIssue({
11284
11288
  code: external_exports.ZodIssueCode.custom,
11285
- message: "Manifest must declare at least one of: version, runtime, app, services, env, secrets, bootstrap, start, deploy"
11289
+ message: "Manifest must declare at least one of: version, runtime, app, services, env, secrets, bootstrap, start, deploy, plan_chat"
11286
11290
  });
11287
11291
  }
11288
11292
  });
@@ -11296,6 +11300,10 @@ var init_repo_manifest = __esm({
11296
11300
  "bootstrap",
11297
11301
  "start",
11298
11302
  "deploy",
11303
+ // olam-plan-chat-chunks-substrate Phase A task A4: opt-in for the chunks
11304
+ // thought substrate. Read by `resolveThoughtSubstrate` in
11305
+ // `packages/core/src/thought/substrate-router.ts`.
11306
+ "plan_chat",
11299
11307
  // F-7 (olam-hybrid-shared-postgres dogfood finding): native inheritance —
11300
11308
  // `.olam.yaml` may declare `inherits: .adb.yaml` to deep-merge an adb-shaped
11301
11309
  // base manifest into itself. Retires the atlas-one `bin/olam-create-wrap.sh`
@@ -11576,7 +11584,24 @@ var init_schema2 = __esm({
11576
11584
  * (B1/C4) reads this file and forwards the value as
11577
11585
  * `AGENTMEMORY_SECRET` into worlds.
11578
11586
  */
11579
- secret_ref: external_exports.string().min(1).optional().default("~/.olam/cloud-memory-secret")
11587
+ secret_ref: external_exports.string().min(1).optional().default("~/.olam/cloud-memory-secret"),
11588
+ /**
11589
+ * DO SQLite write-through bridge toggle. Defaults to `true` (bridge
11590
+ * active — every state-mutating call captures the engine's export to
11591
+ * a DO snapshot, restored on cold-start).
11592
+ *
11593
+ * Operators set `bridge: false` to bisect bridge vs engine bugs:
11594
+ * the CLI threads this into the Worker as `OLAM_BRIDGE_DISABLED=1`,
11595
+ * which makes `AgentMemoryContainer.fetch` a plain pass-through
11596
+ * (no capture, no restore). The next deploy with `bridge: true`
11597
+ * resumes captures automatically.
11598
+ *
11599
+ * See `docs/design/olam-agent-memory-do-bridge-schema.md` for the
11600
+ * full storage contract; the bridge is OFF only as a diagnosis aid.
11601
+ *
11602
+ * Plan reference: docs/plans/olam-agent-memory-do-sqlite-bridge/phase-c-tasks.md C3
11603
+ */
11604
+ bridge: external_exports.boolean().optional().default(true)
11580
11605
  });
11581
11606
  MemorySchema = external_exports.object({
11582
11607
  mode: external_exports.enum(["local", "cloud"]).optional().default("local"),
@@ -24222,6 +24247,10 @@ var createWorldContainer = async (docker, worldId, worldName, image, env, resour
24222
24247
  opts.push(`size=${m.size}`);
24223
24248
  if (m.mode !== void 0)
24224
24249
  opts.push(`mode=${m.mode.toString(8).padStart(4, "0")}`);
24250
+ if (m.uid !== void 0)
24251
+ opts.push(`uid=${m.uid}`);
24252
+ if (m.gid !== void 0)
24253
+ opts.push(`gid=${m.gid}`);
24225
24254
  acc[m.target] = opts.join(",");
24226
24255
  return acc;
24227
24256
  }, {})
@@ -30679,6 +30708,119 @@ function createServer4(ctx, initError) {
30679
30708
  return server;
30680
30709
  }
30681
30710
 
30711
+ // ../mcp-server/src/utils/native-probe.ts
30712
+ import { createRequire as createRequire4 } from "node:module";
30713
+ var PROBE_REQUIRE = createRequire4(import.meta.url);
30714
+ function runtimeModuleVersion() {
30715
+ return Number.parseInt(process.versions.modules, 10);
30716
+ }
30717
+ function parseAbiMessage(message) {
30718
+ const compiledMatch = message.match(/NODE_MODULE_VERSION\s+(\d+)/);
30719
+ const requiredMatch = message.match(/requires\s+NODE_MODULE_VERSION\s+(\d+)/);
30720
+ const compiledRaw = compiledMatch?.[1];
30721
+ const requiredRaw = requiredMatch?.[1];
30722
+ return {
30723
+ compiledFor: compiledRaw !== void 0 ? Number.parseInt(compiledRaw, 10) : void 0,
30724
+ required: requiredRaw !== void 0 ? Number.parseInt(requiredRaw, 10) : void 0
30725
+ };
30726
+ }
30727
+ function classifyLoadError(err, bindingPath) {
30728
+ const message = typeof err.message === "string" ? err.message : "";
30729
+ if (err.code === "MODULE_NOT_FOUND" || /Cannot find module 'better-sqlite3'/.test(message)) {
30730
+ return {
30731
+ kind: "missing",
30732
+ diagnostic: formatMissingPackage(err)
30733
+ };
30734
+ }
30735
+ const abi = parseAbiMessage(message);
30736
+ if (err.code === "ERR_DLOPEN_FAILED" || abi.compiledFor !== void 0 || abi.required !== void 0) {
30737
+ return {
30738
+ kind: "abi",
30739
+ diagnostic: formatAbiMismatch(err, abi.compiledFor, abi.required, bindingPath),
30740
+ compiledFor: abi.compiledFor,
30741
+ required: abi.required
30742
+ };
30743
+ }
30744
+ return { kind: "unclassified", diagnostic: "" };
30745
+ }
30746
+ function formatMissingPackage(err) {
30747
+ return [
30748
+ "",
30749
+ "\u274C MCP olam server cannot start.",
30750
+ " Reason: better-sqlite3 native module is not installed.",
30751
+ ` Runtime: ${process.version} (NODE_MODULE_VERSION ${runtimeModuleVersion()})`,
30752
+ ` Detail: ${err.message}`,
30753
+ "",
30754
+ "Fix:",
30755
+ " Run `npm install` in the workspace that hosts this binary,",
30756
+ " or reinstall the CLI: `npm install -g @pleri/olam-cli`.",
30757
+ ""
30758
+ ].join("\n");
30759
+ }
30760
+ function formatAbiMismatch(err, compiledFor, required2, bindingPath) {
30761
+ const lines = [
30762
+ "",
30763
+ "\u274C MCP olam server cannot start.",
30764
+ " Reason: better-sqlite3 native binding is ABI-incompatible.",
30765
+ ` Runtime: ${process.version} (NODE_MODULE_VERSION ${required2 ?? runtimeModuleVersion()})`
30766
+ ];
30767
+ if (compiledFor !== void 0) {
30768
+ lines.push(` Binding: compiled for NODE_MODULE_VERSION ${compiledFor}`);
30769
+ }
30770
+ lines.push(` Path: ${bindingPath}`);
30771
+ lines.push(` Node: ${process.execPath}`);
30772
+ lines.push("");
30773
+ lines.push("Fix:");
30774
+ lines.push(" Rebuild the native binding against the runtime Node version:");
30775
+ lines.push(" npm rebuild better-sqlite3");
30776
+ lines.push("");
30777
+ lines.push(" If you installed under a different Node version (via nvm/asdf/mise),");
30778
+ lines.push(" switch to that version first, then rebuild, OR reinstall the package");
30779
+ lines.push(" so prebuild-install fetches the correct binary for the current Node.");
30780
+ lines.push("");
30781
+ lines.push(` Underlying error: ${err.message.split("\n")[0]}`);
30782
+ lines.push("");
30783
+ return lines.join("\n");
30784
+ }
30785
+ function defaultResolver() {
30786
+ try {
30787
+ return PROBE_REQUIRE.resolve("better-sqlite3");
30788
+ } catch {
30789
+ return "(unresolved)";
30790
+ }
30791
+ }
30792
+ function defaultLoader() {
30793
+ return PROBE_REQUIRE("better-sqlite3");
30794
+ }
30795
+ function defaultEmit(message) {
30796
+ process.stderr.write(message.endsWith("\n") ? message : `${message}
30797
+ `);
30798
+ }
30799
+ var defaultExit = (code) => process.exit(code);
30800
+ function assertBetterSqlite3Loadable(deps = {}) {
30801
+ const loader = deps.loader ?? defaultLoader;
30802
+ const resolver = deps.resolver ?? defaultResolver;
30803
+ const emit = deps.emit ?? defaultEmit;
30804
+ const exit = deps.exit ?? defaultExit;
30805
+ try {
30806
+ const Database = loader();
30807
+ const db = new Database(":memory:");
30808
+ try {
30809
+ db.prepare("SELECT 1").get();
30810
+ } finally {
30811
+ db.close();
30812
+ }
30813
+ } catch (raw) {
30814
+ const err = raw;
30815
+ const classified = classifyLoadError(err, resolver());
30816
+ if (classified.kind === "unclassified") {
30817
+ throw err;
30818
+ }
30819
+ emit(classified.diagnostic);
30820
+ exit(1);
30821
+ }
30822
+ }
30823
+
30682
30824
  // ../mcp-server/src/index.ts
30683
30825
  init_loader();
30684
30826
 
@@ -31707,18 +31849,18 @@ function computeFingerprint(runtimes) {
31707
31849
  const joined = parts.join("_");
31708
31850
  return sanitizeTag(joined);
31709
31851
  }
31710
- function computeImageTag(runtimes) {
31711
- const baseDigest = getBaseImageDigest();
31852
+ function computeImageTag(runtimes, baseImageRef = `${BASE_IMAGE}:latest`) {
31853
+ const baseDigest = getImageDigest(baseImageRef);
31712
31854
  const prefix = baseDigest.slice(0, 8);
31713
31855
  const fingerprint = computeFingerprint(runtimes);
31714
31856
  const tag = `${prefix}_${fingerprint}`;
31715
31857
  return sanitizeTag(tag);
31716
31858
  }
31717
- function computeImageName(runtimes) {
31718
- return `${BASE_IMAGE}:${computeImageTag(runtimes)}`;
31859
+ function computeImageName(runtimes, baseImageRef = `${BASE_IMAGE}:latest`) {
31860
+ return `${BASE_IMAGE}:${computeImageTag(runtimes, baseImageRef)}`;
31719
31861
  }
31720
- function lookupCachedImage(runtimes) {
31721
- const tag = computeImageTag(runtimes);
31862
+ function lookupCachedImage(runtimes, baseImageRef = `${BASE_IMAGE}:latest`) {
31863
+ const tag = computeImageTag(runtimes, baseImageRef);
31722
31864
  const imageName = `${BASE_IMAGE}:${tag}`;
31723
31865
  try {
31724
31866
  execSync3(`docker image inspect ${imageName} > /dev/null 2>&1`, {
@@ -31735,19 +31877,25 @@ function commitAsImage(containerName, imageName) {
31735
31877
  const now = (/* @__PURE__ */ new Date()).toISOString();
31736
31878
  execSync3(`docker commit --change 'LABEL ${LABEL_PREFIX}=true' --change 'LABEL ${LABEL_PREFIX}.created-at=${now}' --change 'LABEL ${LABEL_PREFIX}.base-digest=${baseDigest}' ${containerName} ${imageName}`, { stdio: "pipe", timeout: 12e4 });
31737
31879
  }
31738
- var cachedBaseDigest;
31739
- function getBaseImageDigest() {
31740
- if (cachedBaseDigest)
31741
- return cachedBaseDigest;
31880
+ var cachedImageDigests = /* @__PURE__ */ new Map();
31881
+ function getImageDigest(imageRef) {
31882
+ const memo = cachedImageDigests.get(imageRef);
31883
+ if (memo)
31884
+ return memo;
31742
31885
  try {
31743
- const digest = execSync3(`docker inspect ${BASE_IMAGE}:latest --format '{{.Id}}'`, { encoding: "utf-8", timeout: 5e3 }).trim();
31744
- cachedBaseDigest = digest.replace("sha256:", "").slice(0, 16);
31745
- return cachedBaseDigest;
31886
+ const digest = execSync3(`docker inspect ${imageRef} --format '{{.Id}}'`, { encoding: "utf-8", timeout: 5e3 }).trim();
31887
+ const short = digest.replace("sha256:", "").slice(0, 16);
31888
+ cachedImageDigests.set(imageRef, short);
31889
+ return short;
31746
31890
  } catch {
31747
- cachedBaseDigest = crypto4.createHash("sha256").update("unknown").digest("hex").slice(0, 16);
31748
- return cachedBaseDigest;
31891
+ const fallback = crypto4.createHash("sha256").update(imageRef).digest("hex").slice(0, 16);
31892
+ cachedImageDigests.set(imageRef, fallback);
31893
+ return fallback;
31749
31894
  }
31750
31895
  }
31896
+ function getBaseImageDigest() {
31897
+ return getImageDigest(`${BASE_IMAGE}:latest`);
31898
+ }
31751
31899
  function sanitizeTag(raw) {
31752
31900
  let tag = raw.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
31753
31901
  if (tag.length > MAX_TAG_LENGTH) {
@@ -32664,6 +32812,75 @@ function formatPoliciesBrief(policies) {
32664
32812
  return lines.join("\n");
32665
32813
  }
32666
32814
 
32815
+ // ../core/dist/world/auto-dispatch-task.js
32816
+ var TaskDispatchError = class extends Error {
32817
+ kind;
32818
+ detail;
32819
+ constructor(message, kind, detail) {
32820
+ super(message);
32821
+ this.kind = kind;
32822
+ this.detail = detail;
32823
+ this.name = "TaskDispatchError";
32824
+ }
32825
+ };
32826
+ var DEFAULT_SLEEP = (ms) => new Promise((resolve10) => setTimeout(resolve10, ms));
32827
+ async function probeHealth(containerName, dockerExec, budgetMs, sleep3) {
32828
+ const deadline = Date.now() + budgetMs;
32829
+ const cadenceMs = 100;
32830
+ let lastErr = "";
32831
+ while (Date.now() < deadline) {
32832
+ try {
32833
+ const out = dockerExec(containerName, `curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/health`).trim();
32834
+ if (out === "200")
32835
+ return;
32836
+ lastErr = `HTTP ${out || "(no response)"}`;
32837
+ } catch (err) {
32838
+ lastErr = err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200);
32839
+ }
32840
+ await sleep3(cadenceMs);
32841
+ }
32842
+ throw new TaskDispatchError(`container-cp /health did not return 200 within ${budgetMs}ms`, "health-probe", lastErr || "no response");
32843
+ }
32844
+ function startAgent(containerName, dockerExec) {
32845
+ const out = dockerExec(containerName, `curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:8080/session/start-agent`).trim();
32846
+ if (out !== "200" && out !== "202") {
32847
+ throw new TaskDispatchError(`/session/start-agent returned HTTP ${out || "(no response)"}`, "start-agent", `out=${out}`);
32848
+ }
32849
+ }
32850
+ function dispatch2(containerName, prompt, dockerExec) {
32851
+ const body = JSON.stringify({ prompt });
32852
+ const b64 = Buffer.from(body, "utf8").toString("base64");
32853
+ const tmpFile = "/tmp/olam-auto-dispatch.json";
32854
+ dockerExec(containerName, `echo '${b64}' | base64 -d > ${tmpFile}`);
32855
+ const out = dockerExec(containerName, `curl -s -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d @${tmpFile} http://localhost:8080/dispatch`).trim();
32856
+ if (out !== "202" && out !== "200") {
32857
+ throw new TaskDispatchError(`/dispatch returned HTTP ${out || "(no response)"}`, "dispatch-post", `out=${out}`);
32858
+ }
32859
+ }
32860
+ function composeTaskPrompt(task, policies, formatPoliciesBrief2) {
32861
+ if (policies.length === 0 || !formatPoliciesBrief2)
32862
+ return task;
32863
+ const seen = /* @__PURE__ */ new Set();
32864
+ const unique = policies.filter((p) => {
32865
+ if (seen.has(p.id))
32866
+ return false;
32867
+ seen.add(p.id);
32868
+ return true;
32869
+ });
32870
+ if (unique.length === 0)
32871
+ return task;
32872
+ return `${formatPoliciesBrief2(unique)}## Your task
32873
+ ${task}`;
32874
+ }
32875
+ async function autoDispatchTask(opts) {
32876
+ const { containerName, task, policies, dockerExec, healthBudgetMs = 15e3, sleep: sleep3 = DEFAULT_SLEEP, logger: logger2 = console, formatPoliciesBrief: formatPoliciesBrief2 } = opts;
32877
+ const finalPrompt = composeTaskPrompt(task, policies, formatPoliciesBrief2);
32878
+ await probeHealth(containerName, dockerExec, healthBudgetMs, sleep3);
32879
+ startAgent(containerName, dockerExec);
32880
+ dispatch2(containerName, finalPrompt, dockerExec);
32881
+ logger2.log(`[world] Task auto-dispatched (${finalPrompt.length} chars)`);
32882
+ }
32883
+
32667
32884
  // ../core/dist/global-config/runbook-resolver.js
32668
32885
  import * as fs23 from "node:fs";
32669
32886
  import * as os14 from "node:os";
@@ -33567,9 +33784,32 @@ ${detail}`);
33567
33784
  }
33568
33785
  }
33569
33786
  const repoSecretUrls = enrichedRepos.filter((r) => typeof r.manifest?.secrets === "string" && r.manifest.secrets.length > 0).map((r) => ({ repoName: r.name, secretsUrl: r.manifest.secrets }));
33570
- let stackCacheHit = false;
33571
33787
  let selectedImage;
33788
+ let cacheArchOverride;
33789
+ const selected = selectDevboxImageForWorld({
33790
+ config: this.config,
33791
+ repos,
33792
+ worldspec: opts.worldspec
33793
+ });
33794
+ if (selected) {
33795
+ selectedImage = selected.image;
33796
+ cacheArchOverride = selected.cacheArch;
33797
+ if (selected.source === "worldspec") {
33798
+ console.log(`[WorldManager] worldspec override \u2014 using ${selected.image} (tag=${selected.tag})`);
33799
+ } else {
33800
+ console.log(`[WorldManager] image_selector matched \u2014 using ${selected.image} (tag=${selected.tag}${selected.cacheArch ? `, cache_arch=${selected.cacheArch}` : ""})`);
33801
+ }
33802
+ } else {
33803
+ const hasRailsRepo = repos.some((r) => r.type === "rails");
33804
+ if (hasRailsRepo) {
33805
+ selectedImage = resolveDevboxImage(this.config, "amd64");
33806
+ cacheArchOverride = "x64";
33807
+ console.log(`[WorldManager] Rails repo detected \u2014 using ${selectedImage} + x64 mise-cache (Rosetta path)`);
33808
+ }
33809
+ }
33810
+ let stackCacheHit = false;
33572
33811
  const preDetectedStacks = /* @__PURE__ */ new Map();
33812
+ const cacheBaseRef = selectedImage ?? "olam-devbox:latest";
33573
33813
  if (this.provider.capabilities.supportsCustomImages) {
33574
33814
  try {
33575
33815
  const hostExec = makeHostExecFn();
@@ -33584,7 +33824,7 @@ ${detail}`);
33584
33824
  }
33585
33825
  const runtimes = collectUniqueRuntimes(Array.from(preDetectedStacks.values()));
33586
33826
  if (runtimes.size > 0) {
33587
- const cacheResult = lookupCachedImage(runtimes);
33827
+ const cacheResult = lookupCachedImage(runtimes, cacheBaseRef);
33588
33828
  if (cacheResult.hit) {
33589
33829
  selectedImage = cacheResult.imageName;
33590
33830
  stackCacheHit = true;
@@ -33598,30 +33838,6 @@ ${detail}`);
33598
33838
  console.warn(`[WorldManager] host-side stack detection failed: ${msg}`);
33599
33839
  }
33600
33840
  }
33601
- let cacheArchOverride;
33602
- if (!stackCacheHit) {
33603
- const selected = selectDevboxImageForWorld({
33604
- config: this.config,
33605
- repos,
33606
- worldspec: opts.worldspec
33607
- });
33608
- if (selected) {
33609
- selectedImage = selected.image;
33610
- cacheArchOverride = selected.cacheArch;
33611
- if (selected.source === "worldspec") {
33612
- console.log(`[WorldManager] worldspec override \u2014 using ${selected.image} (tag=${selected.tag})`);
33613
- } else {
33614
- console.log(`[WorldManager] image_selector matched \u2014 using ${selected.image} (tag=${selected.tag}${selected.cacheArch ? `, cache_arch=${selected.cacheArch}` : ""})`);
33615
- }
33616
- } else {
33617
- const hasRailsRepo = repos.some((r) => r.type === "rails");
33618
- if (hasRailsRepo) {
33619
- selectedImage = resolveDevboxImage(this.config, "amd64");
33620
- cacheArchOverride = "x64";
33621
- console.log(`[WorldManager] Rails repo detected \u2014 using ${selectedImage} + x64 mise-cache (Rosetta path)`);
33622
- }
33623
- }
33624
- }
33625
33841
  const appPorts = [];
33626
33842
  for (const repo of enrichedRepos) {
33627
33843
  const manifestPort = repo.manifest?.app?.port;
@@ -33775,12 +33991,22 @@ ${detail}`);
33775
33991
  ...extraNetworks ? { extraNetworks } : {},
33776
33992
  // Closes SEC-002 residual: hybrid worlds get a tmpfs mount at
33777
33993
  // /run/olam to receive the per-world postgres credentials. Size
33778
- // 256K is generous for a few env files; mode 0700 means only
33779
- // root (the container's PID 1, which then drops to the app user
33780
- // via the entrypoint) can list the directory. Non-hybrid worlds
33781
- // skip this entirely no behavior change.
33994
+ // 256K is generous for a few env files; mode 0700 keeps the dir
33995
+ // owner-only. uid/gid 999 matches the `olam` user from the Phase
33996
+ // E E4 base image contract — without setting these, the tmpfs is
33997
+ // root-owned and `docker exec` (which runs as the image's USER,
33998
+ // i.e. olam) hits EACCES on credential writes. Non-hybrid worlds
33999
+ // skip the mount entirely — no behavior change.
33782
34000
  ...tmpfsPostgresCredContent ? {
33783
- tmpfsMounts: [{ target: "/run/olam", size: 256 * 1024, mode: 448 }],
34001
+ tmpfsMounts: [
34002
+ {
34003
+ target: "/run/olam",
34004
+ size: 256 * 1024,
34005
+ mode: 448,
34006
+ uid: 999,
34007
+ gid: 999
34008
+ }
34009
+ ],
33784
34010
  tmpfsCredentialWrites: [
33785
34011
  { path: "/run/olam/postgres.env", content: tmpfsPostgresCredContent }
33786
34012
  ]
@@ -33992,48 +34218,38 @@ ${detail}`);
33992
34218
  }
33993
34219
  }
33994
34220
  }
33995
- if (credentialsInjected.claude) {
33996
- try {
33997
- execSync5(`docker exec ${containerName} curl -sf -X POST http://localhost:8080/session/start-agent 2>/dev/null || true`, { stdio: "pipe", timeout: 45e3 });
33998
- } catch {
33999
- }
34000
- if (opts.task) {
34001
- let taskWithPolicies = opts.task;
34221
+ if (opts.task) {
34222
+ const allPolicies = repos.flatMap((repo) => {
34223
+ const repoWorktree = path28.join(workspacePath, repo.name);
34002
34224
  try {
34003
- const allPolicies = repos.flatMap((repo) => {
34004
- const repoWorktree = path28.join(workspacePath, repo.name);
34005
- return loadPolicies(repoWorktree);
34006
- });
34007
- const seen = /* @__PURE__ */ new Set();
34008
- const uniquePolicies = allPolicies.filter((p) => {
34009
- if (seen.has(p.id))
34010
- return false;
34011
- seen.add(p.id);
34012
- return true;
34013
- });
34014
- if (uniquePolicies.length > 0) {
34015
- const brief = formatPoliciesBrief(uniquePolicies);
34016
- taskWithPolicies = `${brief}## Your task
34017
- ${opts.task}`;
34018
- execSync5(`docker exec ${containerName} mkdir -p /home/olam/.olam/policies`, { stdio: "pipe", timeout: 1e4 });
34019
- for (const repo of repos) {
34020
- const policiesDir = path28.join(workspacePath, repo.name, ".olam", "policies");
34021
- if (fs25.existsSync(policiesDir)) {
34022
- execSync5(`docker cp "${policiesDir}/." "${containerName}:/home/olam/.olam/policies/"`, { stdio: "pipe", timeout: 15e3 });
34023
- }
34024
- }
34025
- }
34225
+ return loadPolicies(repoWorktree);
34026
34226
  } catch (err) {
34027
34227
  const msg = err instanceof Error ? err.message : String(err);
34028
- console.warn(`[world] policy injection failed (non-fatal): ${msg}`);
34228
+ console.warn(`[world] policy load failed for ${repo.name} (non-fatal): ${msg}`);
34229
+ return [];
34029
34230
  }
34231
+ });
34232
+ if (allPolicies.length > 0) {
34030
34233
  try {
34031
- const payload = JSON.stringify({ prompt: taskWithPolicies }).replace(/'/g, "'\\''");
34032
- execSync5(`docker exec ${containerName} curl -sf -X POST -H 'Content-Type: application/json' -d '${payload}' http://localhost:8080/dispatch 2>/dev/null || true`, { stdio: "pipe", timeout: 3e4 });
34033
- console.log("[world] Task auto-dispatched");
34034
- } catch {
34234
+ execSync5(`docker exec ${containerName} mkdir -p /home/olam/.olam/policies`, { stdio: "pipe", timeout: 1e4 });
34235
+ for (const repo of repos) {
34236
+ const policiesDir = path28.join(workspacePath, repo.name, ".olam", "policies");
34237
+ if (fs25.existsSync(policiesDir)) {
34238
+ execSync5(`docker cp "${policiesDir}/." "${containerName}:/home/olam/.olam/policies/"`, { stdio: "pipe", timeout: 15e3 });
34239
+ }
34240
+ }
34241
+ } catch (err) {
34242
+ const msg = err instanceof Error ? err.message : String(err);
34243
+ console.warn(`[world] policy copy failed (non-fatal): ${msg}`);
34035
34244
  }
34036
34245
  }
34246
+ await autoDispatchTask({
34247
+ containerName,
34248
+ task: opts.task,
34249
+ policies: allPolicies,
34250
+ dockerExec: this.dockerExec,
34251
+ formatPoliciesBrief
34252
+ });
34037
34253
  }
34038
34254
  const controlPlanePort = 19080 + portOffset;
34039
34255
  const dashboardPort = controlPlanePort;
@@ -35856,6 +36072,7 @@ function loadProjectEnv(startDir = process.cwd()) {
35856
36072
 
35857
36073
  // ../mcp-server/src/index.ts
35858
36074
  async function main() {
36075
+ assertBetterSqlite3Loadable();
35859
36076
  let ctx;
35860
36077
  let initError;
35861
36078
  try {
@@ -0,0 +1,92 @@
1
+ // Bearer-secret management for plan-chat-service.mjs.
2
+ //
3
+ // Mirrors the agent-memory-service pattern from the sibling olam-agent-memory
4
+ // repo: a single 0600 file at ~/.olam/plan-chat-secret holds the bearer
5
+ // hex string. Helpers generate, read, and rotate atomically. Rotation
6
+ // writes to a tmpfile and renames; mid-rotation reads see either the old
7
+ // or new value, never a partial write.
8
+
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import crypto from 'node:crypto';
13
+
14
+ export const SECRET_DIR = path.join(os.homedir(), '.olam');
15
+ export const SECRET_PATH = path.join(SECRET_DIR, 'plan-chat-secret');
16
+ const SECRET_BYTES = 32; // 64 hex chars
17
+ const SECRET_MODE = 0o600;
18
+
19
+ /**
20
+ * Generate a fresh hex bearer (64 chars; 256 bits of entropy).
21
+ */
22
+ export function generateSecret() {
23
+ return crypto.randomBytes(SECRET_BYTES).toString('hex');
24
+ }
25
+
26
+ /**
27
+ * Read the on-disk bearer. Returns null if absent. Throws on permission errors.
28
+ */
29
+ export function readSecret(secretPath = SECRET_PATH) {
30
+ try {
31
+ const value = fs.readFileSync(secretPath, 'utf8').trim();
32
+ if (!value) return null;
33
+ return value;
34
+ } catch (err) {
35
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') return null;
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Write the bearer to disk atomically. Creates `~/.olam` if missing. Enforces
42
+ * 0600 perms on the destination (older mode permissions on the tmpfile are
43
+ * tightened immediately after write).
44
+ */
45
+ export function writeSecret(value, secretPath = SECRET_PATH) {
46
+ if (typeof value !== 'string' || value.length === 0) {
47
+ throw new Error('plan-chat-secret: refusing to write empty bearer');
48
+ }
49
+ fs.mkdirSync(path.dirname(secretPath), { recursive: true, mode: 0o700 });
50
+ const tmp = `${secretPath}.tmp-${process.pid}-${Date.now()}`;
51
+ fs.writeFileSync(tmp, value + '\n', { mode: SECRET_MODE });
52
+ try {
53
+ fs.chmodSync(tmp, SECRET_MODE);
54
+ fs.renameSync(tmp, secretPath);
55
+ } catch (err) {
56
+ try { fs.unlinkSync(tmp); } catch { /* swallow */ }
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Read the bearer if it exists, else generate, write, and return it.
63
+ * Idempotent across processes; first writer wins (rename is atomic).
64
+ */
65
+ export function ensureSecret(secretPath = SECRET_PATH) {
66
+ const existing = readSecret(secretPath);
67
+ if (existing) return existing;
68
+ const fresh = generateSecret();
69
+ writeSecret(fresh, secretPath);
70
+ return fresh;
71
+ }
72
+
73
+ /**
74
+ * Rotate: generate a new bearer, write atomically, return the new value.
75
+ * Callers should restart any running plan-chat-service so it re-reads.
76
+ */
77
+ export function rotateSecret(secretPath = SECRET_PATH) {
78
+ const fresh = generateSecret();
79
+ writeSecret(fresh, secretPath);
80
+ return fresh;
81
+ }
82
+
83
+ /**
84
+ * Constant-time compare. Returns true iff both strings are non-empty and
85
+ * byte-equal. Avoids leaking timing on bearer comparison.
86
+ */
87
+ export function timingSafeEqual(a, b) {
88
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
89
+ if (a.length === 0 || b.length === 0) return false;
90
+ if (a.length !== b.length) return false;
91
+ return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
92
+ }