@prismer/runtime 2.0.2 → 2.0.4

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/cli.cjs CHANGED
@@ -987,15 +987,30 @@ No @ needed; the human is the only other party.
987
987
  );
988
988
  }
989
989
  await stopStaleHermesGateways(profileName, portOverride, config.startupTimeoutMs);
990
+ const hermesProfileDir = getHermesProfileDir(profileName);
991
+ try {
992
+ (0, import_node_fs2.mkdirSync)(hermesProfileDir, { recursive: true });
993
+ } catch (err) {
994
+ process.stderr.write(
995
+ `[hermes-adapter] failed to ensure hermes profile dir ${hermesProfileDir}: ${err.message}
996
+ `
997
+ );
998
+ }
990
999
  (0, import_node_child_process3.spawn)("hermes", ["-p", profileName, "gateway", "run"], {
991
1000
  detached: false,
992
1001
  stdio: "ignore",
1002
+ cwd: hermesProfileDir,
993
1003
  env: {
994
1004
  ...process.env,
995
1005
  API_SERVER_ENABLED: "true",
996
1006
  API_SERVER_KEY: config.apiKey,
997
1007
  API_SERVER_PORT: String(portOverride),
998
1008
  API_SERVER_HOST: "127.0.0.1",
1009
+ // TERMINAL_CWD is the env that hermes file_tools._resolve_path
1010
+ // honors over `os.getcwd()` for relative paths. Pin it to the
1011
+ // profile dir so even non-LLM-initiated writes (e.g. hermes
1012
+ // internal book-keeping) land in the sandbox.
1013
+ TERMINAL_CWD: hermesProfileDir,
999
1014
  [config.prismerApiKeyEnv]: resolvePrismerApiKey(config)
1000
1015
  }
1001
1016
  });
@@ -1919,7 +1934,7 @@ var require_package = __commonJS({
1919
1934
  "package.json"(exports2, module2) {
1920
1935
  module2.exports = {
1921
1936
  name: "@prismer/runtime",
1922
- version: "2.0.2",
1937
+ version: "2.0.4",
1923
1938
  description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
1924
1939
  type: "module",
1925
1940
  main: "dist/index.js",
@@ -2208,11 +2223,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
2208
2223
  }
2209
2224
  async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
2210
2225
  if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
2211
- const data = await cloud.get(
2226
+ let data = await cloud.get(
2212
2227
  `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
2213
2228
  { signal }
2214
2229
  );
2215
- const entries = normalizeInstalledSkills(data);
2230
+ let entries = normalizeInstalledSkills(data);
2231
+ if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
2232
+ try {
2233
+ const backfill = await cloud.request(
2234
+ "POST",
2235
+ `/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
2236
+ { body: {}, signal }
2237
+ );
2238
+ if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
2239
+ const installed = backfill.data.data?.installed ?? 0;
2240
+ if (installed > 0) {
2241
+ process.stdout.write(
2242
+ `[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
2243
+ `
2244
+ );
2245
+ data = await cloud.get(
2246
+ `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
2247
+ { signal }
2248
+ );
2249
+ entries = normalizeInstalledSkills(data);
2250
+ }
2251
+ }
2252
+ } catch (err) {
2253
+ process.stderr.write(
2254
+ `[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
2255
+ `
2256
+ );
2257
+ }
2258
+ }
2216
2259
  const skillsRoot = resolveSkillsRoot(profile);
2217
2260
  if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
2218
2261
  let synced = 0;
@@ -6566,12 +6609,23 @@ async function handleDispatch(payload, requestId, deps) {
6566
6609
  let reply;
6567
6610
  const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
6568
6611
  const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
6612
+ const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
6569
6613
  if (outboxDir) {
6570
6614
  try {
6571
6615
  await import_node_fs12.promises.mkdir(outboxDir, { recursive: true });
6572
6616
  } catch (err) {
6573
6617
  process.stderr.write(
6574
6618
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
6619
+ `
6620
+ );
6621
+ }
6622
+ }
6623
+ if (workDir) {
6624
+ try {
6625
+ await import_node_fs12.promises.mkdir(workDir, { recursive: true });
6626
+ } catch (err) {
6627
+ process.stderr.write(
6628
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
6575
6629
  `
6576
6630
  );
6577
6631
  }
@@ -6654,7 +6708,8 @@ async function handleDispatch(payload, requestId, deps) {
6654
6708
  profile
6655
6709
  ),
6656
6710
  outboxDir,
6657
- profile.config
6711
+ profile.config,
6712
+ workDir
6658
6713
  );
6659
6714
  const cfg = profile.config;
6660
6715
  let profileSystemPrompt;
@@ -6676,12 +6731,16 @@ async function handleDispatch(payload, requestId, deps) {
6676
6731
  conversationId: payload.conversationId,
6677
6732
  prismerGoals: goalContext.map(toGoalMirrorPayload),
6678
6733
  ...composedSystemPrompt ? { systemPrompt: composedSystemPrompt } : {},
6679
- // Wave-9: spawn-style adapters (claude-code, codex, openclaw) read
6680
- // this and inject `PRISMER_OUTBOX_DIR=<abs>` into the child env so
6681
- // the LLM tool can resolve the path even if it doesn't read the
6682
- // prompt instruction. Long-running adapters (Hermes) ignore this
6683
- // and rely on the prompt-side instruction added above.
6734
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
6735
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
6736
+ // into the child env so the LLM tool can resolve the paths even if
6737
+ // it doesn't read the prompt instruction. Long-running adapters
6738
+ // (Hermes) ignore these and rely on the prompt-side instruction
6739
+ // added above — but Hermes is now cwd-sandboxed to its profile
6740
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
6741
+ // stay out of the user's source tree.
6684
6742
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
6743
+ ...workDir ? { prismerWorkDir: workDir } : {},
6685
6744
  prismerObservability: {
6686
6745
  identity: {
6687
6746
  loaded: Boolean(profileSystemPrompt),
@@ -7085,18 +7144,43 @@ function stringifyPrinciples(value) {
7085
7144
  }
7086
7145
  return void 0;
7087
7146
  }
7088
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
7147
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
7089
7148
  if (!outboxDir) return prompt;
7090
7149
  if (profileConfig?.disableOutboxHints === true) return prompt;
7091
- const block = [
7092
- "[Output Files]",
7093
- "If you produce any files for the user (documents, images, code archives, reports, generated media):",
7094
- ` 1. Write them to: ${outboxDir}`,
7095
- " 2. They will be uploaded automatically and shown to the user as chat attachments.",
7096
- "Use the directory path above verbatim. The same path is also exposed as the `PRISMER_OUTBOX_DIR` environment variable.",
7150
+ const lines = [
7151
+ "[File system rules \u2014 MANDATORY, server-enforced]",
7152
+ "",
7153
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
7154
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
7155
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
7156
+ "directories below is treated as malformed.",
7157
+ "",
7158
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
7159
+ ` ${outboxDir}`,
7160
+ " Any file you place here will be uploaded and shown to the user as an",
7161
+ " attachment in your reply. Use this for the final artifacts the user",
7162
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
7163
+ ""
7164
+ ];
7165
+ if (workDir) {
7166
+ lines.push(
7167
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
7168
+ ` ${workDir}`,
7169
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
7170
+ " anything the user doesn't need to see. These files are deleted later",
7171
+ " and never become chat attachments.",
7172
+ ""
7173
+ );
7174
+ }
7175
+ lines.push(
7176
+ "If you produce a file but write it OUTSIDE both directories with a",
7177
+ "relative path, it will NOT become a chat attachment and may pollute the",
7178
+ "user's working directory. This is treated as a hard error.",
7179
+ "",
7180
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
7097
7181
  ""
7098
- ].join("\n");
7099
- return `${block}
7182
+ );
7183
+ return `${lines.join("\n")}
7100
7184
  ${prompt}`;
7101
7185
  }
7102
7186
  function appendChannelContext(prompt, payload, profile) {
@@ -14937,7 +15021,7 @@ async function readResponseError(res) {
14937
15021
 
14938
15022
  // src/cli/index.ts
14939
15023
  init_ui();
14940
- var VERSION = "2.0.2";
15024
+ var VERSION = "2.0.4";
14941
15025
  function buildProgram() {
14942
15026
  const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
14943
15027
  program.addCommand(buildBannerCommand());
package/dist/cli.js CHANGED
@@ -965,15 +965,30 @@ No @ needed; the human is the only other party.
965
965
  );
966
966
  }
967
967
  await stopStaleHermesGateways(profileName, portOverride, config.startupTimeoutMs);
968
+ const hermesProfileDir = getHermesProfileDir(profileName);
969
+ try {
970
+ mkdirSync(hermesProfileDir, { recursive: true });
971
+ } catch (err) {
972
+ process.stderr.write(
973
+ `[hermes-adapter] failed to ensure hermes profile dir ${hermesProfileDir}: ${err.message}
974
+ `
975
+ );
976
+ }
968
977
  spawn3("hermes", ["-p", profileName, "gateway", "run"], {
969
978
  detached: false,
970
979
  stdio: "ignore",
980
+ cwd: hermesProfileDir,
971
981
  env: {
972
982
  ...process.env,
973
983
  API_SERVER_ENABLED: "true",
974
984
  API_SERVER_KEY: config.apiKey,
975
985
  API_SERVER_PORT: String(portOverride),
976
986
  API_SERVER_HOST: "127.0.0.1",
987
+ // TERMINAL_CWD is the env that hermes file_tools._resolve_path
988
+ // honors over `os.getcwd()` for relative paths. Pin it to the
989
+ // profile dir so even non-LLM-initiated writes (e.g. hermes
990
+ // internal book-keeping) land in the sandbox.
991
+ TERMINAL_CWD: hermesProfileDir,
977
992
  [config.prismerApiKeyEnv]: resolvePrismerApiKey(config)
978
993
  }
979
994
  });
@@ -1896,7 +1911,7 @@ var require_package = __commonJS({
1896
1911
  "package.json"(exports, module) {
1897
1912
  module.exports = {
1898
1913
  name: "@prismer/runtime",
1899
- version: "2.0.2",
1914
+ version: "2.0.4",
1900
1915
  description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
1901
1916
  type: "module",
1902
1917
  main: "dist/index.js",
@@ -2188,11 +2203,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
2188
2203
  }
2189
2204
  async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
2190
2205
  if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
2191
- const data = await cloud.get(
2206
+ let data = await cloud.get(
2192
2207
  `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
2193
2208
  { signal }
2194
2209
  );
2195
- const entries = normalizeInstalledSkills(data);
2210
+ let entries = normalizeInstalledSkills(data);
2211
+ if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
2212
+ try {
2213
+ const backfill = await cloud.request(
2214
+ "POST",
2215
+ `/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
2216
+ { body: {}, signal }
2217
+ );
2218
+ if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
2219
+ const installed = backfill.data.data?.installed ?? 0;
2220
+ if (installed > 0) {
2221
+ process.stdout.write(
2222
+ `[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
2223
+ `
2224
+ );
2225
+ data = await cloud.get(
2226
+ `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
2227
+ { signal }
2228
+ );
2229
+ entries = normalizeInstalledSkills(data);
2230
+ }
2231
+ }
2232
+ } catch (err) {
2233
+ process.stderr.write(
2234
+ `[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
2235
+ `
2236
+ );
2237
+ }
2238
+ }
2196
2239
  const skillsRoot = resolveSkillsRoot(profile);
2197
2240
  if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
2198
2241
  let synced = 0;
@@ -6550,12 +6593,23 @@ async function handleDispatch(payload, requestId, deps) {
6550
6593
  let reply;
6551
6594
  const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
6552
6595
  const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
6596
+ const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
6553
6597
  if (outboxDir) {
6554
6598
  try {
6555
6599
  await fsp3.mkdir(outboxDir, { recursive: true });
6556
6600
  } catch (err) {
6557
6601
  process.stderr.write(
6558
6602
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
6603
+ `
6604
+ );
6605
+ }
6606
+ }
6607
+ if (workDir) {
6608
+ try {
6609
+ await fsp3.mkdir(workDir, { recursive: true });
6610
+ } catch (err) {
6611
+ process.stderr.write(
6612
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
6559
6613
  `
6560
6614
  );
6561
6615
  }
@@ -6638,7 +6692,8 @@ async function handleDispatch(payload, requestId, deps) {
6638
6692
  profile
6639
6693
  ),
6640
6694
  outboxDir,
6641
- profile.config
6695
+ profile.config,
6696
+ workDir
6642
6697
  );
6643
6698
  const cfg = profile.config;
6644
6699
  let profileSystemPrompt;
@@ -6660,12 +6715,16 @@ async function handleDispatch(payload, requestId, deps) {
6660
6715
  conversationId: payload.conversationId,
6661
6716
  prismerGoals: goalContext.map(toGoalMirrorPayload),
6662
6717
  ...composedSystemPrompt ? { systemPrompt: composedSystemPrompt } : {},
6663
- // Wave-9: spawn-style adapters (claude-code, codex, openclaw) read
6664
- // this and inject `PRISMER_OUTBOX_DIR=<abs>` into the child env so
6665
- // the LLM tool can resolve the path even if it doesn't read the
6666
- // prompt instruction. Long-running adapters (Hermes) ignore this
6667
- // and rely on the prompt-side instruction added above.
6718
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
6719
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
6720
+ // into the child env so the LLM tool can resolve the paths even if
6721
+ // it doesn't read the prompt instruction. Long-running adapters
6722
+ // (Hermes) ignore these and rely on the prompt-side instruction
6723
+ // added above — but Hermes is now cwd-sandboxed to its profile
6724
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
6725
+ // stay out of the user's source tree.
6668
6726
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
6727
+ ...workDir ? { prismerWorkDir: workDir } : {},
6669
6728
  prismerObservability: {
6670
6729
  identity: {
6671
6730
  loaded: Boolean(profileSystemPrompt),
@@ -7069,18 +7128,43 @@ function stringifyPrinciples(value) {
7069
7128
  }
7070
7129
  return void 0;
7071
7130
  }
7072
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
7131
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
7073
7132
  if (!outboxDir) return prompt;
7074
7133
  if (profileConfig?.disableOutboxHints === true) return prompt;
7075
- const block = [
7076
- "[Output Files]",
7077
- "If you produce any files for the user (documents, images, code archives, reports, generated media):",
7078
- ` 1. Write them to: ${outboxDir}`,
7079
- " 2. They will be uploaded automatically and shown to the user as chat attachments.",
7080
- "Use the directory path above verbatim. The same path is also exposed as the `PRISMER_OUTBOX_DIR` environment variable.",
7134
+ const lines = [
7135
+ "[File system rules \u2014 MANDATORY, server-enforced]",
7136
+ "",
7137
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
7138
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
7139
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
7140
+ "directories below is treated as malformed.",
7141
+ "",
7142
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
7143
+ ` ${outboxDir}`,
7144
+ " Any file you place here will be uploaded and shown to the user as an",
7145
+ " attachment in your reply. Use this for the final artifacts the user",
7146
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
7147
+ ""
7148
+ ];
7149
+ if (workDir) {
7150
+ lines.push(
7151
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
7152
+ ` ${workDir}`,
7153
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
7154
+ " anything the user doesn't need to see. These files are deleted later",
7155
+ " and never become chat attachments.",
7156
+ ""
7157
+ );
7158
+ }
7159
+ lines.push(
7160
+ "If you produce a file but write it OUTSIDE both directories with a",
7161
+ "relative path, it will NOT become a chat attachment and may pollute the",
7162
+ "user's working directory. This is treated as a hard error.",
7163
+ "",
7164
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
7081
7165
  ""
7082
- ].join("\n");
7083
- return `${block}
7166
+ );
7167
+ return `${lines.join("\n")}
7084
7168
  ${prompt}`;
7085
7169
  }
7086
7170
  function appendChannelContext(prompt, payload, profile) {
@@ -14920,7 +15004,7 @@ async function readResponseError(res) {
14920
15004
 
14921
15005
  // src/cli/index.ts
14922
15006
  init_ui();
14923
- var VERSION = "2.0.2";
15007
+ var VERSION = "2.0.4";
14924
15008
  function buildProgram() {
14925
15009
  const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
14926
15010
  program.addCommand(buildBannerCommand());
package/dist/index.cjs CHANGED
@@ -987,15 +987,30 @@ No @ needed; the human is the only other party.
987
987
  );
988
988
  }
989
989
  await stopStaleHermesGateways(profileName, portOverride, config.startupTimeoutMs);
990
+ const hermesProfileDir = getHermesProfileDir(profileName);
991
+ try {
992
+ (0, import_node_fs2.mkdirSync)(hermesProfileDir, { recursive: true });
993
+ } catch (err) {
994
+ process.stderr.write(
995
+ `[hermes-adapter] failed to ensure hermes profile dir ${hermesProfileDir}: ${err.message}
996
+ `
997
+ );
998
+ }
990
999
  (0, import_node_child_process.spawn)("hermes", ["-p", profileName, "gateway", "run"], {
991
1000
  detached: false,
992
1001
  stdio: "ignore",
1002
+ cwd: hermesProfileDir,
993
1003
  env: {
994
1004
  ...process.env,
995
1005
  API_SERVER_ENABLED: "true",
996
1006
  API_SERVER_KEY: config.apiKey,
997
1007
  API_SERVER_PORT: String(portOverride),
998
1008
  API_SERVER_HOST: "127.0.0.1",
1009
+ // TERMINAL_CWD is the env that hermes file_tools._resolve_path
1010
+ // honors over `os.getcwd()` for relative paths. Pin it to the
1011
+ // profile dir so even non-LLM-initiated writes (e.g. hermes
1012
+ // internal book-keeping) land in the sandbox.
1013
+ TERMINAL_CWD: hermesProfileDir,
999
1014
  [config.prismerApiKeyEnv]: resolvePrismerApiKey(config)
1000
1015
  }
1001
1016
  });
@@ -1530,11 +1545,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
1530
1545
  }
1531
1546
  async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
1532
1547
  if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
1533
- const data = await cloud.get(
1548
+ let data = await cloud.get(
1534
1549
  `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
1535
1550
  { signal }
1536
1551
  );
1537
- const entries = normalizeInstalledSkills(data);
1552
+ let entries = normalizeInstalledSkills(data);
1553
+ if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
1554
+ try {
1555
+ const backfill = await cloud.request(
1556
+ "POST",
1557
+ `/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
1558
+ { body: {}, signal }
1559
+ );
1560
+ if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
1561
+ const installed = backfill.data.data?.installed ?? 0;
1562
+ if (installed > 0) {
1563
+ process.stdout.write(
1564
+ `[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
1565
+ `
1566
+ );
1567
+ data = await cloud.get(
1568
+ `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
1569
+ { signal }
1570
+ );
1571
+ entries = normalizeInstalledSkills(data);
1572
+ }
1573
+ }
1574
+ } catch (err) {
1575
+ process.stderr.write(
1576
+ `[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
1577
+ `
1578
+ );
1579
+ }
1580
+ }
1538
1581
  const skillsRoot = resolveSkillsRoot(profile);
1539
1582
  if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
1540
1583
  let synced = 0;
@@ -2280,7 +2323,7 @@ var require_package = __commonJS({
2280
2323
  "package.json"(exports2, module2) {
2281
2324
  module2.exports = {
2282
2325
  name: "@prismer/runtime",
2283
- version: "2.0.2",
2326
+ version: "2.0.4",
2284
2327
  description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
2285
2328
  type: "module",
2286
2329
  main: "dist/index.js",
@@ -4256,12 +4299,23 @@ async function handleDispatch(payload, requestId, deps) {
4256
4299
  let reply;
4257
4300
  const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
4258
4301
  const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
4302
+ const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
4259
4303
  if (outboxDir) {
4260
4304
  try {
4261
4305
  await import_node_fs8.promises.mkdir(outboxDir, { recursive: true });
4262
4306
  } catch (err) {
4263
4307
  process.stderr.write(
4264
4308
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
4309
+ `
4310
+ );
4311
+ }
4312
+ }
4313
+ if (workDir) {
4314
+ try {
4315
+ await import_node_fs8.promises.mkdir(workDir, { recursive: true });
4316
+ } catch (err) {
4317
+ process.stderr.write(
4318
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
4265
4319
  `
4266
4320
  );
4267
4321
  }
@@ -4344,7 +4398,8 @@ async function handleDispatch(payload, requestId, deps) {
4344
4398
  profile
4345
4399
  ),
4346
4400
  outboxDir,
4347
- profile.config
4401
+ profile.config,
4402
+ workDir
4348
4403
  );
4349
4404
  const cfg = profile.config;
4350
4405
  let profileSystemPrompt;
@@ -4366,12 +4421,16 @@ async function handleDispatch(payload, requestId, deps) {
4366
4421
  conversationId: payload.conversationId,
4367
4422
  prismerGoals: goalContext.map(toGoalMirrorPayload),
4368
4423
  ...composedSystemPrompt ? { systemPrompt: composedSystemPrompt } : {},
4369
- // Wave-9: spawn-style adapters (claude-code, codex, openclaw) read
4370
- // this and inject `PRISMER_OUTBOX_DIR=<abs>` into the child env so
4371
- // the LLM tool can resolve the path even if it doesn't read the
4372
- // prompt instruction. Long-running adapters (Hermes) ignore this
4373
- // and rely on the prompt-side instruction added above.
4424
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
4425
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
4426
+ // into the child env so the LLM tool can resolve the paths even if
4427
+ // it doesn't read the prompt instruction. Long-running adapters
4428
+ // (Hermes) ignore these and rely on the prompt-side instruction
4429
+ // added above — but Hermes is now cwd-sandboxed to its profile
4430
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
4431
+ // stay out of the user's source tree.
4374
4432
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
4433
+ ...workDir ? { prismerWorkDir: workDir } : {},
4375
4434
  prismerObservability: {
4376
4435
  identity: {
4377
4436
  loaded: Boolean(profileSystemPrompt),
@@ -4775,18 +4834,43 @@ function stringifyPrinciples(value) {
4775
4834
  }
4776
4835
  return void 0;
4777
4836
  }
4778
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
4837
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
4779
4838
  if (!outboxDir) return prompt;
4780
4839
  if (profileConfig?.disableOutboxHints === true) return prompt;
4781
- const block = [
4782
- "[Output Files]",
4783
- "If you produce any files for the user (documents, images, code archives, reports, generated media):",
4784
- ` 1. Write them to: ${outboxDir}`,
4785
- " 2. They will be uploaded automatically and shown to the user as chat attachments.",
4786
- "Use the directory path above verbatim. The same path is also exposed as the `PRISMER_OUTBOX_DIR` environment variable.",
4840
+ const lines = [
4841
+ "[File system rules \u2014 MANDATORY, server-enforced]",
4842
+ "",
4843
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
4844
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
4845
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
4846
+ "directories below is treated as malformed.",
4847
+ "",
4848
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
4849
+ ` ${outboxDir}`,
4850
+ " Any file you place here will be uploaded and shown to the user as an",
4851
+ " attachment in your reply. Use this for the final artifacts the user",
4852
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
4853
+ ""
4854
+ ];
4855
+ if (workDir) {
4856
+ lines.push(
4857
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
4858
+ ` ${workDir}`,
4859
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
4860
+ " anything the user doesn't need to see. These files are deleted later",
4861
+ " and never become chat attachments.",
4862
+ ""
4863
+ );
4864
+ }
4865
+ lines.push(
4866
+ "If you produce a file but write it OUTSIDE both directories with a",
4867
+ "relative path, it will NOT become a chat attachment and may pollute the",
4868
+ "user's working directory. This is treated as a hard error.",
4869
+ "",
4870
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
4787
4871
  ""
4788
- ].join("\n");
4789
- return `${block}
4872
+ );
4873
+ return `${lines.join("\n")}
4790
4874
  ${prompt}`;
4791
4875
  }
4792
4876
  function appendChannelContext(prompt, payload, profile) {
@@ -15110,7 +15194,7 @@ async function readResponseError(res) {
15110
15194
 
15111
15195
  // src/cli/index.ts
15112
15196
  init_ui();
15113
- var VERSION = "2.0.2";
15197
+ var VERSION = "2.0.4";
15114
15198
  function buildProgram() {
15115
15199
  const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
15116
15200
  program.addCommand(buildBannerCommand());
package/dist/index.d.cts CHANGED
@@ -2269,8 +2269,8 @@ declare const CodexConfigSchema: z.ZodObject<{
2269
2269
  apiKeyEnv: z.ZodDefault<z.ZodString>;
2270
2270
  }, "strip", z.ZodTypeAny, {
2271
2271
  model: string;
2272
- sandbox: "read-only" | "workspace-write" | "danger-full-access";
2273
2272
  cwd: string;
2273
+ sandbox: "read-only" | "workspace-write" | "danger-full-access";
2274
2274
  apiKeyEnv: string;
2275
2275
  systemPrompt?: string | undefined;
2276
2276
  envVars?: Record<string, string> | undefined;
package/dist/index.d.ts CHANGED
@@ -2269,8 +2269,8 @@ declare const CodexConfigSchema: z.ZodObject<{
2269
2269
  apiKeyEnv: z.ZodDefault<z.ZodString>;
2270
2270
  }, "strip", z.ZodTypeAny, {
2271
2271
  model: string;
2272
- sandbox: "read-only" | "workspace-write" | "danger-full-access";
2273
2272
  cwd: string;
2273
+ sandbox: "read-only" | "workspace-write" | "danger-full-access";
2274
2274
  apiKeyEnv: string;
2275
2275
  systemPrompt?: string | undefined;
2276
2276
  envVars?: Record<string, string> | undefined;
package/dist/index.js CHANGED
@@ -964,15 +964,30 @@ No @ needed; the human is the only other party.
964
964
  );
965
965
  }
966
966
  await stopStaleHermesGateways(profileName, portOverride, config.startupTimeoutMs);
967
+ const hermesProfileDir = getHermesProfileDir(profileName);
968
+ try {
969
+ mkdirSync(hermesProfileDir, { recursive: true });
970
+ } catch (err) {
971
+ process.stderr.write(
972
+ `[hermes-adapter] failed to ensure hermes profile dir ${hermesProfileDir}: ${err.message}
973
+ `
974
+ );
975
+ }
967
976
  spawn("hermes", ["-p", profileName, "gateway", "run"], {
968
977
  detached: false,
969
978
  stdio: "ignore",
979
+ cwd: hermesProfileDir,
970
980
  env: {
971
981
  ...process.env,
972
982
  API_SERVER_ENABLED: "true",
973
983
  API_SERVER_KEY: config.apiKey,
974
984
  API_SERVER_PORT: String(portOverride),
975
985
  API_SERVER_HOST: "127.0.0.1",
986
+ // TERMINAL_CWD is the env that hermes file_tools._resolve_path
987
+ // honors over `os.getcwd()` for relative paths. Pin it to the
988
+ // profile dir so even non-LLM-initiated writes (e.g. hermes
989
+ // internal book-keeping) land in the sandbox.
990
+ TERMINAL_CWD: hermesProfileDir,
976
991
  [config.prismerApiKeyEnv]: resolvePrismerApiKey(config)
977
992
  }
978
993
  });
@@ -1510,11 +1525,39 @@ async function syncAllAgentSkills(profiles, cloud, options = {}) {
1510
1525
  }
1511
1526
  async function syncInstalledSkillsForDispatch(profile, agentImUserId, cloud, signal) {
1512
1527
  if (!agentImUserId) return { synced: 0, skipped: 0, unchanged: 0 };
1513
- const data = await cloud.get(
1528
+ let data = await cloud.get(
1514
1529
  `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
1515
1530
  { signal }
1516
1531
  );
1517
- const entries = normalizeInstalledSkills(data);
1532
+ let entries = normalizeInstalledSkills(data);
1533
+ if (entries.length === 0 && (profile.adapterName === "hermes" || profile.adapterName === "openclaw")) {
1534
+ try {
1535
+ const backfill = await cloud.request(
1536
+ "POST",
1537
+ `/api/im/agents/${encodeURIComponent(agentImUserId)}/skills/install-builtins`,
1538
+ { body: {}, signal }
1539
+ );
1540
+ if (backfill.ok && backfill.data && typeof backfill.data === "object" && "data" in backfill.data) {
1541
+ const installed = backfill.data.data?.installed ?? 0;
1542
+ if (installed > 0) {
1543
+ process.stdout.write(
1544
+ `[daemon] skill sync: backfilled ${installed} built-in skills for agent ${agentImUserId.slice(-8)}
1545
+ `
1546
+ );
1547
+ data = await cloud.get(
1548
+ `/api/im/skills/installed?agentId=${encodeURIComponent(agentImUserId)}`,
1549
+ { signal }
1550
+ );
1551
+ entries = normalizeInstalledSkills(data);
1552
+ }
1553
+ }
1554
+ } catch (err) {
1555
+ process.stderr.write(
1556
+ `[daemon] skill sync: backfill skipped for ${agentImUserId.slice(-8)}: ${err.message}
1557
+ `
1558
+ );
1559
+ }
1560
+ }
1518
1561
  const skillsRoot = resolveSkillsRoot(profile);
1519
1562
  if (!skillsRoot) return { synced: 0, skipped: 0, unchanged: 0 };
1520
1563
  let synced = 0;
@@ -2256,7 +2299,7 @@ var require_package = __commonJS({
2256
2299
  "package.json"(exports, module) {
2257
2300
  module.exports = {
2258
2301
  name: "@prismer/runtime",
2259
- version: "2.0.2",
2302
+ version: "2.0.4",
2260
2303
  description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
2261
2304
  type: "module",
2262
2305
  main: "dist/index.js",
@@ -4182,12 +4225,23 @@ async function handleDispatch(payload, requestId, deps) {
4182
4225
  let reply;
4183
4226
  const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
4184
4227
  const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
4228
+ const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
4185
4229
  if (outboxDir) {
4186
4230
  try {
4187
4231
  await fsp3.mkdir(outboxDir, { recursive: true });
4188
4232
  } catch (err) {
4189
4233
  process.stderr.write(
4190
4234
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
4235
+ `
4236
+ );
4237
+ }
4238
+ }
4239
+ if (workDir) {
4240
+ try {
4241
+ await fsp3.mkdir(workDir, { recursive: true });
4242
+ } catch (err) {
4243
+ process.stderr.write(
4244
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
4191
4245
  `
4192
4246
  );
4193
4247
  }
@@ -4270,7 +4324,8 @@ async function handleDispatch(payload, requestId, deps) {
4270
4324
  profile
4271
4325
  ),
4272
4326
  outboxDir,
4273
- profile.config
4327
+ profile.config,
4328
+ workDir
4274
4329
  );
4275
4330
  const cfg = profile.config;
4276
4331
  let profileSystemPrompt;
@@ -4292,12 +4347,16 @@ async function handleDispatch(payload, requestId, deps) {
4292
4347
  conversationId: payload.conversationId,
4293
4348
  prismerGoals: goalContext.map(toGoalMirrorPayload),
4294
4349
  ...composedSystemPrompt ? { systemPrompt: composedSystemPrompt } : {},
4295
- // Wave-9: spawn-style adapters (claude-code, codex, openclaw) read
4296
- // this and inject `PRISMER_OUTBOX_DIR=<abs>` into the child env so
4297
- // the LLM tool can resolve the path even if it doesn't read the
4298
- // prompt instruction. Long-running adapters (Hermes) ignore this
4299
- // and rely on the prompt-side instruction added above.
4350
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
4351
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
4352
+ // into the child env so the LLM tool can resolve the paths even if
4353
+ // it doesn't read the prompt instruction. Long-running adapters
4354
+ // (Hermes) ignore these and rely on the prompt-side instruction
4355
+ // added above — but Hermes is now cwd-sandboxed to its profile
4356
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
4357
+ // stay out of the user's source tree.
4300
4358
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
4359
+ ...workDir ? { prismerWorkDir: workDir } : {},
4301
4360
  prismerObservability: {
4302
4361
  identity: {
4303
4362
  loaded: Boolean(profileSystemPrompt),
@@ -4701,18 +4760,43 @@ function stringifyPrinciples(value) {
4701
4760
  }
4702
4761
  return void 0;
4703
4762
  }
4704
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
4763
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
4705
4764
  if (!outboxDir) return prompt;
4706
4765
  if (profileConfig?.disableOutboxHints === true) return prompt;
4707
- const block = [
4708
- "[Output Files]",
4709
- "If you produce any files for the user (documents, images, code archives, reports, generated media):",
4710
- ` 1. Write them to: ${outboxDir}`,
4711
- " 2. They will be uploaded automatically and shown to the user as chat attachments.",
4712
- "Use the directory path above verbatim. The same path is also exposed as the `PRISMER_OUTBOX_DIR` environment variable.",
4766
+ const lines = [
4767
+ "[File system rules \u2014 MANDATORY, server-enforced]",
4768
+ "",
4769
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
4770
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
4771
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
4772
+ "directories below is treated as malformed.",
4773
+ "",
4774
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
4775
+ ` ${outboxDir}`,
4776
+ " Any file you place here will be uploaded and shown to the user as an",
4777
+ " attachment in your reply. Use this for the final artifacts the user",
4778
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
4779
+ ""
4780
+ ];
4781
+ if (workDir) {
4782
+ lines.push(
4783
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
4784
+ ` ${workDir}`,
4785
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
4786
+ " anything the user doesn't need to see. These files are deleted later",
4787
+ " and never become chat attachments.",
4788
+ ""
4789
+ );
4790
+ }
4791
+ lines.push(
4792
+ "If you produce a file but write it OUTSIDE both directories with a",
4793
+ "relative path, it will NOT become a chat attachment and may pollute the",
4794
+ "user's working directory. This is treated as a hard error.",
4795
+ "",
4796
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
4713
4797
  ""
4714
- ].join("\n");
4715
- return `${block}
4798
+ );
4799
+ return `${lines.join("\n")}
4716
4800
  ${prompt}`;
4717
4801
  }
4718
4802
  function appendChannelContext(prompt, payload, profile) {
@@ -15042,7 +15126,7 @@ async function readResponseError(res) {
15042
15126
 
15043
15127
  // src/cli/index.ts
15044
15128
  init_ui();
15045
- var VERSION = "2.0.2";
15129
+ var VERSION = "2.0.4";
15046
15130
  function buildProgram() {
15047
15131
  const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
15048
15132
  program.addCommand(buildBannerCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prismer/runtime",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Prismer Cloud daemon runtime — TS-only adapter host for hosted IM agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",