@commonlyai/cli 0.1.185 → 0.1.186

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.185",
3
+ "version": "0.1.186",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -1563,13 +1563,15 @@ export const performRun = ({
1563
1563
  return { environment: seatEnvironment, cwd: agentCwd, binding: sessionBinding };
1564
1564
  }
1565
1565
  let installations = null;
1566
+ let readError = null;
1566
1567
  try {
1567
1568
  const identity = await client.get('/api/agents/runtime/installations', {}, {
1568
1569
  signal: AbortSignal.timeout(5000),
1569
1570
  });
1570
1571
  installations = Array.isArray(identity?.installations) ? identity.installations : [];
1571
- } catch {
1572
+ } catch (err) {
1572
1573
  installations = null;
1574
+ readError = err;
1573
1575
  }
1574
1576
  const placement = workspaceTurnPlacement({
1575
1577
  environment: seatEnvironment, workspacePath: agentCwd, installations, scratchDir: scratchCwd,
@@ -1582,7 +1584,7 @@ export const performRun = ({
1582
1584
  }
1583
1585
  if (!existsSync(placement.cwd)) mkdirSync(placement.cwd, { recursive: true });
1584
1586
  const where = `${placement.environment.sandbox.trust}:${placement.cwd}`;
1585
- return { ...placement, binding: sessionBinding ? `${sessionBinding}:${where}` : where };
1587
+ return { ...placement, readError, binding: sessionBinding ? `${sessionBinding}:${where}` : where };
1586
1588
  };
1587
1589
  const batchLimit = Math.min(50, Math.max(1, Number(inboxBatchLimit) || 10));
1588
1590
 
@@ -1835,6 +1837,33 @@ export const performRun = ({
1835
1837
  event, eventPodId, prompt, preSpawnIds, snapshotMessages, claimKeeper, trigger,
1836
1838
  }) => {
1837
1839
  const placement = await placeTurn();
1840
+ // A workspace turn placed public (its pod turned public, or its pods could
1841
+ // not be read) is confined as a public seat, which this host may not be
1842
+ // able to do: Claude Code on Linux has only the workspace jail. Refuse it
1843
+ // as the run start would, before anything is read or spawned. The model
1844
+ // never runs and nothing is posted. A public pod is a settled answer, so a
1845
+ // human's message is handed to another listener (claim-outcome.js). A
1846
+ // failed read is not: it throws, so the event stays unacked and comes back
1847
+ // through the spawn-retry backoff instead of being refused for good.
1848
+ if (placement.reason) {
1849
+ const turnRefusal = hostSandboxRefusal({ environment: placement.environment, adapterName: adapter.name });
1850
+ if (turnRefusal && placement.readError) {
1851
+ throw Object.assign(
1852
+ new Error(`turn deferred, because its pods could not be read: ${placement.readError.message}`, {
1853
+ cause: placement.readError,
1854
+ }),
1855
+ { status: placement.readError.status },
1856
+ );
1857
+ }
1858
+ if (turnRefusal) {
1859
+ log(`turn refused, because ${placement.reason}: ${turnRefusal.replace(/\n\s*/g, ' ')}`);
1860
+ return {
1861
+ outcome: 'no_action',
1862
+ refused: { reason: 'delivery-refused' },
1863
+ reason: 'host-cannot-confine-turn',
1864
+ };
1865
+ }
1866
+ }
1838
1867
  // TASK-440: a cold, large session starts fresh at wake instead of being
1839
1868
  // resumed. The restart note leads the prompt; memory still reaches the
1840
1869
  // fresh session through the adapter's preamble.
@@ -788,6 +788,25 @@ const resolveClaudePath = (env = process.env) => {
788
788
  return 'claude';
789
789
  };
790
790
 
791
+ // Where the claude this host would run is installed: the folder the Linux
792
+ // workspace jail has to bind, since it binds the system binary folders only.
793
+ // Null when the binary cannot be located (a bare name, a dangling link).
794
+ const resolveClaudeInstall = (env) => {
795
+ const claudeBin = resolveClaudePath(env);
796
+ if (!isAbsolute(claudeBin)) return { claudeBin, claudeInstall: null };
797
+ try {
798
+ return { claudeBin, claudeInstall: dirname(realpathSync(claudeBin)) };
799
+ } catch {
800
+ return { claudeBin, claudeInstall: null };
801
+ }
802
+ };
803
+
804
+ // A jail that cannot carry claude would fail inside at every spawn.
805
+ const claudeInstallRefusal = (claudeBin) => new Error(
806
+ 'workspace Claude agents on Linux need Claude Code on PATH, so the sandbox can carry its install; '
807
+ + `\`${claudeBin}\` could not be resolved`,
808
+ );
809
+
791
810
  const prepareArgv = async (innerArgv, ctx) => {
792
811
  const env = ctx.environment;
793
812
  const claudeEnv = withClaudePath(ctx.claudeEnv);
@@ -941,12 +960,9 @@ const prepareArgv = async (innerArgv, ctx) => {
941
960
  + `repository (run \`git init\` in ${ctx.cwd}), so its .git can be held read-only`);
942
961
  }
943
962
  }
944
- const claudeBin = resolveClaudePath(claudeEnv);
963
+ const { claudeBin, claudeInstall } = resolveClaudeInstall(claudeEnv);
964
+ if (!claudeInstall) throw claudeInstallRefusal(claudeBin);
945
965
  const { statePath } = ctx.publicClaudeState;
946
- let claudeInstall = null;
947
- try {
948
- claudeInstall = dirname(realpathSync(claudeBin));
949
- } catch { /* an unresolvable binary fails at spawn with its own error */ }
950
966
  const wrapped = wrapArgvWithBwrap([claudeBin, ...innerArgv], env, {
951
967
  workspacePath: ctx.cwd,
952
968
  workspaceReadOnly: !grants.includes('write'),
@@ -1125,6 +1141,8 @@ export default {
1125
1141
  // jail never gets a copy of the login.
1126
1142
  const bwrap = (ctx._detectBwrap || detectBwrap)();
1127
1143
  if (!bwrap.available) throw new Error(`workspace Claude agents require bwrap: ${bwrap.error}`);
1144
+ const { claudeBin, claudeInstall } = resolveClaudeInstall(withClaudePath(ctx.env));
1145
+ if (!claudeInstall) throw claudeInstallRefusal(claudeBin);
1128
1146
  publicClaudeState = await prepareWorkspaceBwrapClaudeState(ctx);
1129
1147
  }
1130
1148
  if (publicTrust) {
@@ -46,15 +46,17 @@ export const windowsSandboxMessage = (adapterName) => {
46
46
  };
47
47
 
48
48
  /**
49
- * Claude Code on Linux (WSL included). The bubblewrap namespace binds the
50
- * system binary folders only, and Claude Code installs under the person's
51
- * home or /usr/local, so a confined spawn could not even start it; its login
52
- * and settings live under that home too (Ubuntu run, 2026-10-16). Until the
53
- * namespace carries what Claude needs, the seat stops here with its options.
49
+ * Claude Code on Linux (WSL included) for any seat but a workspace one. Only
50
+ * the workspace jail (#2784) carries what Claude needs: its install, a HOME
51
+ * of its own and a copy of the login. The public jail binds the system binary
52
+ * folders only, so a confined spawn could not even start Claude (Ubuntu run,
53
+ * 2026-10-16), and giving it the login would hand strangers a jail that holds
54
+ * it. A workspace seat needs every pod it answers in to be invite-only.
54
55
  */
55
56
  export const linuxClaudeMessage = () => [
56
- 'Commonly runs a connected Claude Code inside a sandbox, and on Linux (WSL included) that sandbox',
57
- 'cannot run Claude Code yet, so this agent cannot listen for @mentions here. Instead:',
57
+ 'Commonly runs a connected Claude Code inside a sandbox. On Linux (WSL included) it does that only',
58
+ 'for an agent whose pods are all invite-only, and this one is not, so it cannot listen for @mentions here. Instead:',
59
+ ' - From an invite-only pod: run the commands its Connect page shows, from your project\'s folder.',
58
60
  ' - Use Commonly from your own Claude Code sessions: the Connect page shows a `claude mcp add` command.',
59
61
  HOSTED_LINE,
60
62
  ].join('\n');
@@ -76,7 +78,11 @@ export const hostSandboxRefusal = ({
76
78
  if (mode !== 'bwrap' && !SEATBELT_MODES.has(mode)) return null;
77
79
 
78
80
  if (platform === 'win32') return windowsSandboxMessage(adapterName);
79
- if (mode === 'bwrap' && adapterName === 'claude') return linuxClaudeMessage();
81
+ // A workspace claude seat falls through to the bubblewrap probe below; the
82
+ // probe failing, for any reason, is a refusal like any other adapter's.
83
+ if (mode === 'bwrap' && adapterName === 'claude' && sandbox.trust !== 'workspace') {
84
+ return linuxClaudeMessage();
85
+ }
80
86
  if (mode === 'bwrap') {
81
87
  const bwrap = detectBwrap();
82
88
  return bwrap.available ? null : `This seat runs in a bubblewrap sandbox, which is not available here. ${bwrap.error}`;
@@ -73,9 +73,10 @@ export const workspaceStartDirRefusal = (dir, { home = homedir() } = {}) => {
73
73
  };
74
74
 
75
75
  const PUBLIC_POD_REASON = 'a pod it answers in is publicly readable or open to join, or did not report whether it is';
76
- // claude.js refuses a workspace seat under bwrap, so off macOS the default
76
+ // claude.js runs a workspace seat under Seatbelt on macOS and under bwrap on
77
+ // Linux (#2784). Native Windows has no sandbox for it, so there the default
77
78
  // would be a seat that fails every turn rather than one that reads a project.
78
- const NOT_MACOS_REASON = 'an agent works in its folder only on macOS for now';
79
+ const UNSUPPORTED_OS_REASON = 'an agent works in its folder only on macOS and Linux for now';
79
80
 
80
81
  /**
81
82
  * Where a first-run claude seat may read (TASK-294), from three facts the
@@ -90,7 +91,9 @@ export const firstRunWorkspace = ({
90
91
  adapterName, installations, startDir, home, platform = process.platform,
91
92
  } = {}) => {
92
93
  if (adapterName !== 'claude') return null;
93
- if (platform !== 'darwin') return { workspacePath: null, reason: NOT_MACOS_REASON };
94
+ if (platform !== 'darwin' && platform !== 'linux') {
95
+ return { workspacePath: null, reason: UNSUPPORTED_OS_REASON };
96
+ }
94
97
  if (!allPodsConfirmedPrivate(installations)) {
95
98
  return { workspacePath: null, reason: PUBLIC_POD_REASON };
96
99
  }