@cabane/companion 0.6.15 → 0.6.16

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.js CHANGED
@@ -4918,6 +4918,7 @@ function parseCodexModel(model) {
4918
4918
  var CABANE_MCP_SERVER3 = "cabane";
4919
4919
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4920
4920
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4921
+ var ENV_ENVELOPE_KEYS = ["CABANE_ENV_TIER", "CABANE_ENV_KEY", "CABANE_ENV_BINDING"];
4921
4922
  function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4922
4923
  const { policy, config } = req;
4923
4924
  const directory = req.local.cwd ?? "";
@@ -5004,6 +5005,12 @@ function buildConfig(req, baseInstructionsFile) {
5004
5005
  }
5005
5006
  const policy = codexToolPolicy(req.policy);
5006
5007
  const tmpDir = req.local.env?.TMPDIR;
5008
+ const shellEnv = {};
5009
+ if (tmpDir) shellEnv.TMPDIR = tmpDir;
5010
+ for (const key of ENV_ENVELOPE_KEYS) {
5011
+ const value = req.local.env?.[key];
5012
+ if (value !== void 0) shellEnv[key] = value;
5013
+ }
5007
5014
  const gateLockDir = req.local.env?.CABANE_GATE_LOCK_DIR;
5008
5015
  return {
5009
5016
  mcp_servers,
@@ -5012,7 +5019,7 @@ function buildConfig(req, baseInstructionsFile) {
5012
5019
  // becomes Codex's base instructions, and Codex's own environment preamble goes
5013
5020
  // with the harness prompt it belonged to.
5014
5021
  ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
5015
- ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
5022
+ ...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {},
5016
5023
  ...policy.permissionProfile ? {
5017
5024
  // CT733: named permission profiles are Codex's split-filesystem path.
5018
5025
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -7581,6 +7588,8 @@ ${reason}`,
7581
7588
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7582
7589
  event: "settled",
7583
7590
  turnId,
7591
+ agentId: payload.agentId,
7592
+ conversationId: payload.conversationId,
7584
7593
  ok,
7585
7594
  reason
7586
7595
  })}
@@ -7595,50 +7604,63 @@ ${reason}`,
7595
7604
  }
7596
7605
  };
7597
7606
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
7598
- const proof = await proveWorkspaceTools(request, adapter.name, {
7599
- ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7600
- harnessFingerprint: turnContext.runtime
7601
- });
7602
7607
  const checkout = checkoutState(effectiveCwd);
7603
- turnLog[proof.ok && checkout.ok ? "info" : "error"](
7604
- { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7605
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7606
- );
7607
- if (effectiveCwd && checkout.ok) {
7608
+ if (!effectiveCwd || !checkout.ok) {
7609
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7610
+ turnLog.error({ checkout: effectiveCwd ?? null, checkoutState: checkout }, reason);
7608
7611
  try {
7609
- const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7610
- mkdirSync10(diagnosticDir, { recursive: true });
7611
- const receiptPath = join13(diagnosticDir, "readiness.jsonl");
7612
- appendFileSync2(
7613
- receiptPath,
7614
- `${JSON.stringify({
7615
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7616
- taskId: hookEnv.CABANE_TASK_ID,
7617
- binding: hookEnv.CABANE_TASK_BINDING ?? null,
7618
- checkout: effectiveCwd,
7619
- classification: proof.ok ? "ready" : "workspace_tools_missing",
7620
- failedCapability: proof.failedCapability,
7621
- workspaceTools: proof,
7622
- // CT1022: the two fields the environment reaper reads — which turn
7623
- // this is (so its settle can be matched among interleaved agents)
7624
- // and how long it may legitimately run (so an unclosed receipt
7625
- // expires on this turn's real deadline, not the reaper's guess).
7626
- turnId,
7627
- totalTimeoutMs
7628
- })}
7629
- `,
7630
- { mode: 384 }
7631
- );
7632
- turnReceiptPath = receiptPath;
7633
- } catch (error) {
7612
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7613
+ body: `**Couldn't prepare your environment.** ${reason}`,
7614
+ kind: "final",
7615
+ turnId,
7616
+ parentMessageId: payload.messageId
7617
+ });
7618
+ } catch (postErr) {
7634
7619
  turnLog.warn(
7635
- { err: error instanceof Error ? error.message : String(error) },
7636
- "dispatcher: workspace-proof diagnostic write failed"
7620
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7621
+ "dispatcher: checkout-missing notice post failed"
7637
7622
  );
7638
7623
  }
7624
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7639
7625
  }
7640
- if (!checkout.ok) {
7641
- const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7626
+ const receiptPath = join13(effectiveCwd, ".git", "cabane", "readiness.jsonl");
7627
+ const receiptLine = (fields) => `${JSON.stringify({
7628
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7629
+ taskId: hookEnv.CABANE_TASK_ID,
7630
+ binding: hookEnv.CABANE_TASK_BINDING ?? null,
7631
+ checkout: effectiveCwd,
7632
+ // CT1022: the two fields the environment reaper reads — which turn this
7633
+ // is (so its settle can be matched among interleaved agents) and how long
7634
+ // it may legitimately run (so an unclosed receipt expires on this turn's
7635
+ // real deadline, not the reaper's guess).
7636
+ turnId,
7637
+ totalTimeoutMs,
7638
+ // CT1062: WHOSE turn. A task env is shared — between agents, and between
7639
+ // an agent's own sequential conversations — so a reader asking "is a turn
7640
+ // of THIS agent, other than mine, running here?" (the host's run-lock
7641
+ // does, before it lets a second conversation into the tree) can only
7642
+ // answer it if the line says who. An unattributed open line has to count
7643
+ // for everyone, which refuses work that should have been admitted.
7644
+ agentId: payload.agentId,
7645
+ conversationId: payload.conversationId,
7646
+ ...fields
7647
+ })}
7648
+ `;
7649
+ try {
7650
+ mkdirSync10(join13(effectiveCwd, ".git", "cabane"), { recursive: true });
7651
+ appendFileSync2(
7652
+ receiptPath,
7653
+ // `starting` is the honest classification before the proof has run. The
7654
+ // line the proof appends below carries the same `turnId`, so a reader
7655
+ // replaying the file sees one turn, not two.
7656
+ receiptLine({ classification: "starting" }),
7657
+ { mode: 384 }
7658
+ );
7659
+ turnReceiptPath = receiptPath;
7660
+ } catch (error) {
7661
+ const detail = error instanceof Error ? error.message : String(error);
7662
+ const reason = `turn_receipt_unwritable: ${detail}; task=${hookEnv.CABANE_TASK_ID}; checkout=${effectiveCwd}; recovery=restore write access to the checkout's .git/cabane, then re-dispatch`;
7663
+ turnLog.error({ err: detail, receiptPath }, reason);
7642
7664
  try {
7643
7665
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7644
7666
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7649,13 +7671,37 @@ ${reason}`,
7649
7671
  } catch (postErr) {
7650
7672
  turnLog.warn(
7651
7673
  { err: postErr instanceof Error ? postErr.message : String(postErr) },
7652
- "dispatcher: checkout-missing notice post failed"
7674
+ "dispatcher: turn-receipt failure notice post failed"
7653
7675
  );
7654
7676
  }
7655
7677
  return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7656
7678
  }
7679
+ const proof = await proveWorkspaceTools(request, adapter.name, {
7680
+ ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7681
+ harnessFingerprint: turnContext.runtime
7682
+ });
7683
+ turnLog[proof.ok ? "info" : "error"](
7684
+ { workspaceProof: proof, checkout: effectiveCwd },
7685
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout usable`
7686
+ );
7687
+ try {
7688
+ appendFileSync2(
7689
+ receiptPath,
7690
+ receiptLine({
7691
+ classification: proof.ok ? "ready" : "workspace_tools_missing",
7692
+ failedCapability: proof.failedCapability,
7693
+ workspaceTools: proof
7694
+ }),
7695
+ { mode: 384 }
7696
+ );
7697
+ } catch (error) {
7698
+ turnLog.warn(
7699
+ { err: error instanceof Error ? error.message : String(error) },
7700
+ "dispatcher: workspace-proof diagnostic write failed (the turn receipt is open)"
7701
+ );
7702
+ }
7657
7703
  if (!proof.ok) {
7658
- const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7704
+ const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7659
7705
  closeTurnReceipt(false, reason);
7660
7706
  try {
7661
7707
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
package/dist/runtime.js CHANGED
@@ -4571,6 +4571,7 @@ function parseCodexModel(model) {
4571
4571
  var CABANE_MCP_SERVER3 = "cabane";
4572
4572
  var TURN_CONTROL_MCP_SERVER2 = "cabane_companion";
4573
4573
  var ACTIVE_CONVERSATION_HEADER4 = "x-cabane-active-conversation";
4574
+ var ENV_ENVELOPE_KEYS = ["CABANE_ENV_TIER", "CABANE_ENV_KEY", "CABANE_ENV_BINDING"];
4574
4575
  function buildRunSpec2(req, resumeThreadId, instructionsFile = null) {
4575
4576
  const { policy, config } = req;
4576
4577
  const directory = req.local.cwd ?? "";
@@ -4657,6 +4658,12 @@ function buildConfig(req, baseInstructionsFile) {
4657
4658
  }
4658
4659
  const policy = codexToolPolicy(req.policy);
4659
4660
  const tmpDir = req.local.env?.TMPDIR;
4661
+ const shellEnv = {};
4662
+ if (tmpDir) shellEnv.TMPDIR = tmpDir;
4663
+ for (const key of ENV_ENVELOPE_KEYS) {
4664
+ const value = req.local.env?.[key];
4665
+ if (value !== void 0) shellEnv[key] = value;
4666
+ }
4660
4667
  const gateLockDir = req.local.env?.CABANE_GATE_LOCK_DIR;
4661
4668
  return {
4662
4669
  mcp_servers,
@@ -4665,7 +4672,7 @@ function buildConfig(req, baseInstructionsFile) {
4665
4672
  // becomes Codex's base instructions, and Codex's own environment preamble goes
4666
4673
  // with the harness prompt it belonged to.
4667
4674
  ...baseInstructionsFile ? { model_instructions_file: baseInstructionsFile, include_environment_context: false } : {},
4668
- ...tmpDir ? { shell_environment_policy: { set: { TMPDIR: tmpDir } } } : {},
4675
+ ...Object.keys(shellEnv).length ? { shell_environment_policy: { set: shellEnv } } : {},
4669
4676
  ...policy.permissionProfile ? {
4670
4677
  // CT733: named permission profiles are Codex's split-filesystem path.
4671
4678
  // `:root = read` preserves coding-mode host reads; the one explicit
@@ -7234,6 +7241,8 @@ ${reason}`,
7234
7241
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7235
7242
  event: "settled",
7236
7243
  turnId,
7244
+ agentId: payload.agentId,
7245
+ conversationId: payload.conversationId,
7237
7246
  ok,
7238
7247
  reason
7239
7248
  })}
@@ -7248,50 +7257,63 @@ ${reason}`,
7248
7257
  }
7249
7258
  };
7250
7259
  if (prepareHook && hookEnv?.CABANE_TASK_ID) {
7251
- const proof = await proveWorkspaceTools(request, adapter.name, {
7252
- ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7253
- harnessFingerprint: turnContext.runtime
7254
- });
7255
7260
  const checkout = checkoutState(effectiveCwd);
7256
- turnLog[proof.ok && checkout.ok ? "info" : "error"](
7257
- { workspaceProof: proof, checkout: effectiveCwd ?? null, checkoutState: checkout },
7258
- `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout ${checkout.ok ? "usable" : checkout.reason}`
7259
- );
7260
- if (effectiveCwd && checkout.ok) {
7261
+ if (!effectiveCwd || !checkout.ok) {
7262
+ const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7263
+ turnLog.error({ checkout: effectiveCwd ?? null, checkoutState: checkout }, reason);
7261
7264
  try {
7262
- const diagnosticDir = join13(effectiveCwd, ".git", "cabane");
7263
- mkdirSync9(diagnosticDir, { recursive: true });
7264
- const receiptPath = join13(diagnosticDir, "readiness.jsonl");
7265
- appendFileSync2(
7266
- receiptPath,
7267
- `${JSON.stringify({
7268
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7269
- taskId: hookEnv.CABANE_TASK_ID,
7270
- binding: hookEnv.CABANE_TASK_BINDING ?? null,
7271
- checkout: effectiveCwd,
7272
- classification: proof.ok ? "ready" : "workspace_tools_missing",
7273
- failedCapability: proof.failedCapability,
7274
- workspaceTools: proof,
7275
- // CT1022: the two fields the environment reaper reads — which turn
7276
- // this is (so its settle can be matched among interleaved agents)
7277
- // and how long it may legitimately run (so an unclosed receipt
7278
- // expires on this turn's real deadline, not the reaper's guess).
7279
- turnId,
7280
- totalTimeoutMs
7281
- })}
7282
- `,
7283
- { mode: 384 }
7284
- );
7285
- turnReceiptPath = receiptPath;
7286
- } catch (error) {
7265
+ await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7266
+ body: `**Couldn't prepare your environment.** ${reason}`,
7267
+ kind: "final",
7268
+ turnId,
7269
+ parentMessageId: payload.messageId
7270
+ });
7271
+ } catch (postErr) {
7287
7272
  turnLog.warn(
7288
- { err: error instanceof Error ? error.message : String(error) },
7289
- "dispatcher: workspace-proof diagnostic write failed"
7273
+ { err: postErr instanceof Error ? postErr.message : String(postErr) },
7274
+ "dispatcher: checkout-missing notice post failed"
7290
7275
  );
7291
7276
  }
7277
+ return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7292
7278
  }
7293
- if (!checkout.ok) {
7294
- const reason = `checkout_missing: ${checkout.reason}; task=${hookEnv.CABANE_TASK_ID}; recovery=re-dispatch this conversation (the prepare hook re-provisions the environment)`;
7279
+ const receiptPath = join13(effectiveCwd, ".git", "cabane", "readiness.jsonl");
7280
+ const receiptLine = (fields) => `${JSON.stringify({
7281
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7282
+ taskId: hookEnv.CABANE_TASK_ID,
7283
+ binding: hookEnv.CABANE_TASK_BINDING ?? null,
7284
+ checkout: effectiveCwd,
7285
+ // CT1022: the two fields the environment reaper reads — which turn this
7286
+ // is (so its settle can be matched among interleaved agents) and how long
7287
+ // it may legitimately run (so an unclosed receipt expires on this turn's
7288
+ // real deadline, not the reaper's guess).
7289
+ turnId,
7290
+ totalTimeoutMs,
7291
+ // CT1062: WHOSE turn. A task env is shared — between agents, and between
7292
+ // an agent's own sequential conversations — so a reader asking "is a turn
7293
+ // of THIS agent, other than mine, running here?" (the host's run-lock
7294
+ // does, before it lets a second conversation into the tree) can only
7295
+ // answer it if the line says who. An unattributed open line has to count
7296
+ // for everyone, which refuses work that should have been admitted.
7297
+ agentId: payload.agentId,
7298
+ conversationId: payload.conversationId,
7299
+ ...fields
7300
+ })}
7301
+ `;
7302
+ try {
7303
+ mkdirSync9(join13(effectiveCwd, ".git", "cabane"), { recursive: true });
7304
+ appendFileSync2(
7305
+ receiptPath,
7306
+ // `starting` is the honest classification before the proof has run. The
7307
+ // line the proof appends below carries the same `turnId`, so a reader
7308
+ // replaying the file sees one turn, not two.
7309
+ receiptLine({ classification: "starting" }),
7310
+ { mode: 384 }
7311
+ );
7312
+ turnReceiptPath = receiptPath;
7313
+ } catch (error) {
7314
+ const detail = error instanceof Error ? error.message : String(error);
7315
+ const reason = `turn_receipt_unwritable: ${detail}; task=${hookEnv.CABANE_TASK_ID}; checkout=${effectiveCwd}; recovery=restore write access to the checkout's .git/cabane, then re-dispatch`;
7316
+ turnLog.error({ err: detail, receiptPath }, reason);
7295
7317
  try {
7296
7318
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
7297
7319
  body: `**Couldn't prepare your environment.** ${reason}`,
@@ -7302,13 +7324,37 @@ ${reason}`,
7302
7324
  } catch (postErr) {
7303
7325
  turnLog.warn(
7304
7326
  { err: postErr instanceof Error ? postErr.message : String(postErr) },
7305
- "dispatcher: checkout-missing notice post failed"
7327
+ "dispatcher: turn-receipt failure notice post failed"
7306
7328
  );
7307
7329
  }
7308
7330
  return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
7309
7331
  }
7332
+ const proof = await proveWorkspaceTools(request, adapter.name, {
7333
+ ...this.opts.workspaceProofFetch ? { fetchImpl: this.opts.workspaceProofFetch } : {},
7334
+ harnessFingerprint: turnContext.runtime
7335
+ });
7336
+ turnLog[proof.ok ? "info" : "error"](
7337
+ { workspaceProof: proof, checkout: effectiveCwd },
7338
+ `dispatcher: workspace tool proof ${proof.ok ? "passed" : "failed"}, checkout usable`
7339
+ );
7340
+ try {
7341
+ appendFileSync2(
7342
+ receiptPath,
7343
+ receiptLine({
7344
+ classification: proof.ok ? "ready" : "workspace_tools_missing",
7345
+ failedCapability: proof.failedCapability,
7346
+ workspaceTools: proof
7347
+ }),
7348
+ { mode: 384 }
7349
+ );
7350
+ } catch (error) {
7351
+ turnLog.warn(
7352
+ { err: error instanceof Error ? error.message : String(error) },
7353
+ "dispatcher: workspace-proof diagnostic write failed (the turn receipt is open)"
7354
+ );
7355
+ }
7310
7356
  if (!proof.ok) {
7311
- const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd ?? "unknown"}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7357
+ const reason = `workspace_tools_missing: ${proof.failedCapability}; checkout=${effectiveCwd}; runtime=${adapter.name}; recovery=restart the connector after restoring the Cabane workspace tool mount`;
7312
7358
  closeTurnReceipt(false, reason);
7313
7359
  try {
7314
7360
  await this.opts.api.postTurnMessage(workspaceId, payload.conversationId, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",