@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
package/src/commands/agent.js
CHANGED
|
@@ -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 =
|
|
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)
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
|
57
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
|
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'
|
|
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
|
}
|