@prismer/runtime 2.0.2 → 2.0.3

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.3",
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",
@@ -6566,12 +6581,23 @@ async function handleDispatch(payload, requestId, deps) {
6566
6581
  let reply;
6567
6582
  const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
6568
6583
  const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
6584
+ const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
6569
6585
  if (outboxDir) {
6570
6586
  try {
6571
6587
  await import_node_fs12.promises.mkdir(outboxDir, { recursive: true });
6572
6588
  } catch (err) {
6573
6589
  process.stderr.write(
6574
6590
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
6591
+ `
6592
+ );
6593
+ }
6594
+ }
6595
+ if (workDir) {
6596
+ try {
6597
+ await import_node_fs12.promises.mkdir(workDir, { recursive: true });
6598
+ } catch (err) {
6599
+ process.stderr.write(
6600
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
6575
6601
  `
6576
6602
  );
6577
6603
  }
@@ -6654,7 +6680,8 @@ async function handleDispatch(payload, requestId, deps) {
6654
6680
  profile
6655
6681
  ),
6656
6682
  outboxDir,
6657
- profile.config
6683
+ profile.config,
6684
+ workDir
6658
6685
  );
6659
6686
  const cfg = profile.config;
6660
6687
  let profileSystemPrompt;
@@ -6676,12 +6703,16 @@ async function handleDispatch(payload, requestId, deps) {
6676
6703
  conversationId: payload.conversationId,
6677
6704
  prismerGoals: goalContext.map(toGoalMirrorPayload),
6678
6705
  ...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.
6706
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
6707
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
6708
+ // into the child env so the LLM tool can resolve the paths even if
6709
+ // it doesn't read the prompt instruction. Long-running adapters
6710
+ // (Hermes) ignore these and rely on the prompt-side instruction
6711
+ // added above — but Hermes is now cwd-sandboxed to its profile
6712
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
6713
+ // stay out of the user's source tree.
6684
6714
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
6715
+ ...workDir ? { prismerWorkDir: workDir } : {},
6685
6716
  prismerObservability: {
6686
6717
  identity: {
6687
6718
  loaded: Boolean(profileSystemPrompt),
@@ -7085,18 +7116,43 @@ function stringifyPrinciples(value) {
7085
7116
  }
7086
7117
  return void 0;
7087
7118
  }
7088
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
7119
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
7089
7120
  if (!outboxDir) return prompt;
7090
7121
  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.",
7122
+ const lines = [
7123
+ "[File system rules \u2014 MANDATORY, server-enforced]",
7124
+ "",
7125
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
7126
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
7127
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
7128
+ "directories below is treated as malformed.",
7129
+ "",
7130
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
7131
+ ` ${outboxDir}`,
7132
+ " Any file you place here will be uploaded and shown to the user as an",
7133
+ " attachment in your reply. Use this for the final artifacts the user",
7134
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
7135
+ ""
7136
+ ];
7137
+ if (workDir) {
7138
+ lines.push(
7139
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
7140
+ ` ${workDir}`,
7141
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
7142
+ " anything the user doesn't need to see. These files are deleted later",
7143
+ " and never become chat attachments.",
7144
+ ""
7145
+ );
7146
+ }
7147
+ lines.push(
7148
+ "If you produce a file but write it OUTSIDE both directories with a",
7149
+ "relative path, it will NOT become a chat attachment and may pollute the",
7150
+ "user's working directory. This is treated as a hard error.",
7151
+ "",
7152
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
7097
7153
  ""
7098
- ].join("\n");
7099
- return `${block}
7154
+ );
7155
+ return `${lines.join("\n")}
7100
7156
  ${prompt}`;
7101
7157
  }
7102
7158
  function appendChannelContext(prompt, payload, profile) {
@@ -14937,7 +14993,7 @@ async function readResponseError(res) {
14937
14993
 
14938
14994
  // src/cli/index.ts
14939
14995
  init_ui();
14940
- var VERSION = "2.0.2";
14996
+ var VERSION = "2.0.3";
14941
14997
  function buildProgram() {
14942
14998
  const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
14943
14999
  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.3",
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",
@@ -6550,12 +6565,23 @@ async function handleDispatch(payload, requestId, deps) {
6550
6565
  let reply;
6551
6566
  const outboxBase = deps.paths?.runsDir ? path2.join(deps.paths.runsDir, taskId) : null;
6552
6567
  const outboxDir = outboxBase ? path2.join(outboxBase, "_outbox") : null;
6568
+ const workDir = outboxBase ? path2.join(outboxBase, "workdir") : null;
6553
6569
  if (outboxDir) {
6554
6570
  try {
6555
6571
  await fsp3.mkdir(outboxDir, { recursive: true });
6556
6572
  } catch (err) {
6557
6573
  process.stderr.write(
6558
6574
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
6575
+ `
6576
+ );
6577
+ }
6578
+ }
6579
+ if (workDir) {
6580
+ try {
6581
+ await fsp3.mkdir(workDir, { recursive: true });
6582
+ } catch (err) {
6583
+ process.stderr.write(
6584
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
6559
6585
  `
6560
6586
  );
6561
6587
  }
@@ -6638,7 +6664,8 @@ async function handleDispatch(payload, requestId, deps) {
6638
6664
  profile
6639
6665
  ),
6640
6666
  outboxDir,
6641
- profile.config
6667
+ profile.config,
6668
+ workDir
6642
6669
  );
6643
6670
  const cfg = profile.config;
6644
6671
  let profileSystemPrompt;
@@ -6660,12 +6687,16 @@ async function handleDispatch(payload, requestId, deps) {
6660
6687
  conversationId: payload.conversationId,
6661
6688
  prismerGoals: goalContext.map(toGoalMirrorPayload),
6662
6689
  ...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.
6690
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
6691
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
6692
+ // into the child env so the LLM tool can resolve the paths even if
6693
+ // it doesn't read the prompt instruction. Long-running adapters
6694
+ // (Hermes) ignore these and rely on the prompt-side instruction
6695
+ // added above — but Hermes is now cwd-sandboxed to its profile
6696
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
6697
+ // stay out of the user's source tree.
6668
6698
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
6699
+ ...workDir ? { prismerWorkDir: workDir } : {},
6669
6700
  prismerObservability: {
6670
6701
  identity: {
6671
6702
  loaded: Boolean(profileSystemPrompt),
@@ -7069,18 +7100,43 @@ function stringifyPrinciples(value) {
7069
7100
  }
7070
7101
  return void 0;
7071
7102
  }
7072
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
7103
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
7073
7104
  if (!outboxDir) return prompt;
7074
7105
  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.",
7106
+ const lines = [
7107
+ "[File system rules \u2014 MANDATORY, server-enforced]",
7108
+ "",
7109
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
7110
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
7111
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
7112
+ "directories below is treated as malformed.",
7113
+ "",
7114
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
7115
+ ` ${outboxDir}`,
7116
+ " Any file you place here will be uploaded and shown to the user as an",
7117
+ " attachment in your reply. Use this for the final artifacts the user",
7118
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
7119
+ ""
7120
+ ];
7121
+ if (workDir) {
7122
+ lines.push(
7123
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
7124
+ ` ${workDir}`,
7125
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
7126
+ " anything the user doesn't need to see. These files are deleted later",
7127
+ " and never become chat attachments.",
7128
+ ""
7129
+ );
7130
+ }
7131
+ lines.push(
7132
+ "If you produce a file but write it OUTSIDE both directories with a",
7133
+ "relative path, it will NOT become a chat attachment and may pollute the",
7134
+ "user's working directory. This is treated as a hard error.",
7135
+ "",
7136
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
7081
7137
  ""
7082
- ].join("\n");
7083
- return `${block}
7138
+ );
7139
+ return `${lines.join("\n")}
7084
7140
  ${prompt}`;
7085
7141
  }
7086
7142
  function appendChannelContext(prompt, payload, profile) {
@@ -14920,7 +14976,7 @@ async function readResponseError(res) {
14920
14976
 
14921
14977
  // src/cli/index.ts
14922
14978
  init_ui();
14923
- var VERSION = "2.0.2";
14979
+ var VERSION = "2.0.3";
14924
14980
  function buildProgram() {
14925
14981
  const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
14926
14982
  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
  });
@@ -2280,7 +2295,7 @@ var require_package = __commonJS({
2280
2295
  "package.json"(exports2, module2) {
2281
2296
  module2.exports = {
2282
2297
  name: "@prismer/runtime",
2283
- version: "2.0.2",
2298
+ version: "2.0.3",
2284
2299
  description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
2285
2300
  type: "module",
2286
2301
  main: "dist/index.js",
@@ -4256,12 +4271,23 @@ async function handleDispatch(payload, requestId, deps) {
4256
4271
  let reply;
4257
4272
  const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
4258
4273
  const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
4274
+ const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
4259
4275
  if (outboxDir) {
4260
4276
  try {
4261
4277
  await import_node_fs8.promises.mkdir(outboxDir, { recursive: true });
4262
4278
  } catch (err) {
4263
4279
  process.stderr.write(
4264
4280
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
4281
+ `
4282
+ );
4283
+ }
4284
+ }
4285
+ if (workDir) {
4286
+ try {
4287
+ await import_node_fs8.promises.mkdir(workDir, { recursive: true });
4288
+ } catch (err) {
4289
+ process.stderr.write(
4290
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
4265
4291
  `
4266
4292
  );
4267
4293
  }
@@ -4344,7 +4370,8 @@ async function handleDispatch(payload, requestId, deps) {
4344
4370
  profile
4345
4371
  ),
4346
4372
  outboxDir,
4347
- profile.config
4373
+ profile.config,
4374
+ workDir
4348
4375
  );
4349
4376
  const cfg = profile.config;
4350
4377
  let profileSystemPrompt;
@@ -4366,12 +4393,16 @@ async function handleDispatch(payload, requestId, deps) {
4366
4393
  conversationId: payload.conversationId,
4367
4394
  prismerGoals: goalContext.map(toGoalMirrorPayload),
4368
4395
  ...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.
4396
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
4397
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
4398
+ // into the child env so the LLM tool can resolve the paths even if
4399
+ // it doesn't read the prompt instruction. Long-running adapters
4400
+ // (Hermes) ignore these and rely on the prompt-side instruction
4401
+ // added above — but Hermes is now cwd-sandboxed to its profile
4402
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
4403
+ // stay out of the user's source tree.
4374
4404
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
4405
+ ...workDir ? { prismerWorkDir: workDir } : {},
4375
4406
  prismerObservability: {
4376
4407
  identity: {
4377
4408
  loaded: Boolean(profileSystemPrompt),
@@ -4775,18 +4806,43 @@ function stringifyPrinciples(value) {
4775
4806
  }
4776
4807
  return void 0;
4777
4808
  }
4778
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
4809
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
4779
4810
  if (!outboxDir) return prompt;
4780
4811
  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.",
4812
+ const lines = [
4813
+ "[File system rules \u2014 MANDATORY, server-enforced]",
4814
+ "",
4815
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
4816
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
4817
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
4818
+ "directories below is treated as malformed.",
4819
+ "",
4820
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
4821
+ ` ${outboxDir}`,
4822
+ " Any file you place here will be uploaded and shown to the user as an",
4823
+ " attachment in your reply. Use this for the final artifacts the user",
4824
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
4825
+ ""
4826
+ ];
4827
+ if (workDir) {
4828
+ lines.push(
4829
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
4830
+ ` ${workDir}`,
4831
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
4832
+ " anything the user doesn't need to see. These files are deleted later",
4833
+ " and never become chat attachments.",
4834
+ ""
4835
+ );
4836
+ }
4837
+ lines.push(
4838
+ "If you produce a file but write it OUTSIDE both directories with a",
4839
+ "relative path, it will NOT become a chat attachment and may pollute the",
4840
+ "user's working directory. This is treated as a hard error.",
4841
+ "",
4842
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
4787
4843
  ""
4788
- ].join("\n");
4789
- return `${block}
4844
+ );
4845
+ return `${lines.join("\n")}
4790
4846
  ${prompt}`;
4791
4847
  }
4792
4848
  function appendChannelContext(prompt, payload, profile) {
@@ -15110,7 +15166,7 @@ async function readResponseError(res) {
15110
15166
 
15111
15167
  // src/cli/index.ts
15112
15168
  init_ui();
15113
- var VERSION = "2.0.2";
15169
+ var VERSION = "2.0.3";
15114
15170
  function buildProgram() {
15115
15171
  const program = new import_commander20.Command("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
15116
15172
  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
  });
@@ -2256,7 +2271,7 @@ var require_package = __commonJS({
2256
2271
  "package.json"(exports, module) {
2257
2272
  module.exports = {
2258
2273
  name: "@prismer/runtime",
2259
- version: "2.0.2",
2274
+ version: "2.0.3",
2260
2275
  description: "Prismer Cloud daemon runtime \u2014 TS-only adapter host for hosted IM agents",
2261
2276
  type: "module",
2262
2277
  main: "dist/index.js",
@@ -4182,12 +4197,23 @@ async function handleDispatch(payload, requestId, deps) {
4182
4197
  let reply;
4183
4198
  const outboxBase = deps.paths?.runsDir ? path.join(deps.paths.runsDir, taskId) : null;
4184
4199
  const outboxDir = outboxBase ? path.join(outboxBase, "_outbox") : null;
4200
+ const workDir = outboxBase ? path.join(outboxBase, "workdir") : null;
4185
4201
  if (outboxDir) {
4186
4202
  try {
4187
4203
  await fsp3.mkdir(outboxDir, { recursive: true });
4188
4204
  } catch (err) {
4189
4205
  process.stderr.write(
4190
4206
  `[daemon] outbox provision failed task=${taskId} dir=${outboxDir}: ${err.message}
4207
+ `
4208
+ );
4209
+ }
4210
+ }
4211
+ if (workDir) {
4212
+ try {
4213
+ await fsp3.mkdir(workDir, { recursive: true });
4214
+ } catch (err) {
4215
+ process.stderr.write(
4216
+ `[daemon] workdir provision failed task=${taskId} dir=${workDir}: ${err.message}
4191
4217
  `
4192
4218
  );
4193
4219
  }
@@ -4270,7 +4296,8 @@ async function handleDispatch(payload, requestId, deps) {
4270
4296
  profile
4271
4297
  ),
4272
4298
  outboxDir,
4273
- profile.config
4299
+ profile.config,
4300
+ workDir
4274
4301
  );
4275
4302
  const cfg = profile.config;
4276
4303
  let profileSystemPrompt;
@@ -4292,12 +4319,16 @@ async function handleDispatch(payload, requestId, deps) {
4292
4319
  conversationId: payload.conversationId,
4293
4320
  prismerGoals: goalContext.map(toGoalMirrorPayload),
4294
4321
  ...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.
4322
+ // Wave-9 / F18: spawn-style adapters (claude-code, codex, openclaw)
4323
+ // read these and inject `PRISMER_OUTBOX_DIR` / `PRISMER_WORKDIR`
4324
+ // into the child env so the LLM tool can resolve the paths even if
4325
+ // it doesn't read the prompt instruction. Long-running adapters
4326
+ // (Hermes) ignore these and rely on the prompt-side instruction
4327
+ // added above — but Hermes is now cwd-sandboxed to its profile
4328
+ // dir (~/.hermes/profiles/<name>/) so relative-path writes still
4329
+ // stay out of the user's source tree.
4300
4330
  ...outboxDir ? { prismerOutboxDir: outboxDir } : {},
4331
+ ...workDir ? { prismerWorkDir: workDir } : {},
4301
4332
  prismerObservability: {
4302
4333
  identity: {
4303
4334
  loaded: Boolean(profileSystemPrompt),
@@ -4701,18 +4732,43 @@ function stringifyPrinciples(value) {
4701
4732
  }
4702
4733
  return void 0;
4703
4734
  }
4704
- function appendOutboxInstruction(prompt, outboxDir, profileConfig) {
4735
+ function appendOutboxInstruction(prompt, outboxDir, profileConfig, workDir = null) {
4705
4736
  if (!outboxDir) return prompt;
4706
4737
  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.",
4738
+ const lines = [
4739
+ "[File system rules \u2014 MANDATORY, server-enforced]",
4740
+ "",
4741
+ "You have TWO designated directories for this task. ALWAYS use absolute paths.",
4742
+ "NEVER write files with relative paths \u2014 they resolve against the agent process",
4743
+ "cwd which is sandboxed but unpredictable, and any path outside the two",
4744
+ "directories below is treated as malformed.",
4745
+ "",
4746
+ `1. **Outbox (auto-uploaded as chat attachments)** \u2014 write deliverables here:`,
4747
+ ` ${outboxDir}`,
4748
+ " Any file you place here will be uploaded and shown to the user as an",
4749
+ " attachment in your reply. Use this for the final artifacts the user",
4750
+ " actually wants (PNG charts, DOCX reports, CSV exports, archives).",
4751
+ ""
4752
+ ];
4753
+ if (workDir) {
4754
+ lines.push(
4755
+ `2. **Workdir (scratch, NOT uploaded)** \u2014 write intermediate stuff here:`,
4756
+ ` ${workDir}`,
4757
+ " Use for draft scripts, log files, intermediate CSVs, temp downloads \u2014",
4758
+ " anything the user doesn't need to see. These files are deleted later",
4759
+ " and never become chat attachments.",
4760
+ ""
4761
+ );
4762
+ }
4763
+ lines.push(
4764
+ "If you produce a file but write it OUTSIDE both directories with a",
4765
+ "relative path, it will NOT become a chat attachment and may pollute the",
4766
+ "user's working directory. This is treated as a hard error.",
4767
+ "",
4768
+ "These paths are also exposed via env vars: PRISMER_OUTBOX_DIR" + (workDir ? " and PRISMER_WORKDIR" : "") + ".",
4713
4769
  ""
4714
- ].join("\n");
4715
- return `${block}
4770
+ );
4771
+ return `${lines.join("\n")}
4716
4772
  ${prompt}`;
4717
4773
  }
4718
4774
  function appendChannelContext(prompt, payload, profile) {
@@ -15042,7 +15098,7 @@ async function readResponseError(res) {
15042
15098
 
15043
15099
  // src/cli/index.ts
15044
15100
  init_ui();
15045
- var VERSION = "2.0.2";
15101
+ var VERSION = "2.0.3";
15046
15102
  function buildProgram() {
15047
15103
  const program = new Command20("prismer").description("Prismer Cloud daemon CLI (TS-only).").version(VERSION);
15048
15104
  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.3",
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",