@commonlyai/cli 0.1.234 → 0.1.235
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
|
@@ -89,7 +89,7 @@ import { createClaimedWorkGate } from '../lib/claimed-work-gate.js';
|
|
|
89
89
|
import { prepareCommitAttribution } from '../lib/commit-attribution.js';
|
|
90
90
|
import { detectBwrap } from '../lib/sandbox/bwrap.js';
|
|
91
91
|
import { resolvePublicSandboxMode } from '../lib/sandbox/mode.js';
|
|
92
|
-
import { hostSandboxRefusal, windowsSandboxMessage } from '../lib/sandbox/host-check.js';
|
|
92
|
+
import { UNSANDBOXED_FLAG, hostSandboxRefusal, windowsSandboxMessage } from '../lib/sandbox/host-check.js';
|
|
93
93
|
import {
|
|
94
94
|
UNSANDBOXED_NOTICE, hostUnconfinedForRun, runOptedInUnsandboxed, unsandboxedFlagRefusal,
|
|
95
95
|
} from '../lib/sandbox/windows-unsandboxed.js';
|
|
@@ -1176,6 +1176,8 @@ export const performAttach = async ({
|
|
|
1176
1176
|
log = () => {},
|
|
1177
1177
|
adapterRegistry = { getAdapter, listAdapterNames },
|
|
1178
1178
|
seatbelt: probeSeatbelt = detectSeatbelt,
|
|
1179
|
+
bwrap: probeBwrap = detectBwrap,
|
|
1180
|
+
platform = process.platform,
|
|
1179
1181
|
}) => {
|
|
1180
1182
|
const adapter = adapterRegistry.getAdapter(adapterName);
|
|
1181
1183
|
if (!adapter) {
|
|
@@ -1210,10 +1212,37 @@ export const performAttach = async ({
|
|
|
1210
1212
|
}
|
|
1211
1213
|
log(`workspace: ${workspace.path}${workspace.created ? ' (created)' : ''}`);
|
|
1212
1214
|
|
|
1213
|
-
const { mode: sandboxMode } = resolveAttachSandbox({ environment, adapterName, adapter });
|
|
1215
|
+
const { mode: sandboxMode } = resolveAttachSandbox({ environment, adapterName, adapter, platform });
|
|
1216
|
+
// D245: native Windows has neither sandbox, so its refusal names Windows
|
|
1217
|
+
// and the user's own opt-in, never Seatbelt or bwrap advice. Attach still
|
|
1218
|
+
// refuses there, as before; only the words differ.
|
|
1219
|
+
const windowsRefusal = () => new Error(adapterName === 'dsh'
|
|
1220
|
+
? `confined dsh sandbox.mode=${sandboxMode} needs a sandbox, and Commonly has no sandbox for Windows yet, `
|
|
1221
|
+
+ 'so dsh cannot run confined on native Windows. Choose "Run it here" on the Connect page for a hosted agent.'
|
|
1222
|
+
: `This environment file asks for sandbox.mode=${sandboxMode}, which Commonly cannot enforce on Windows, `
|
|
1223
|
+
+ 'so attach stops here.\n'
|
|
1224
|
+
// This attach made no agent, so Claude's options name the path that works on
|
|
1225
|
+
// Windows: the Connect page's commands, which set the two env vars and run with
|
|
1226
|
+
// the flag, keeping the public trust and policy (#3407 review B1). Other
|
|
1227
|
+
// adapters keep the shared Windows text.
|
|
1228
|
+
+ (adapterName === 'claude' ? [
|
|
1229
|
+
'Instead:',
|
|
1230
|
+
' - Run it without an OS sandbox, which is your choice: use the commands on the Connect page.'
|
|
1231
|
+
+ ' They set COMMONLY_API_URL and COMMONLY_AGENT_TOKEN, then run'
|
|
1232
|
+
+ ` commonly agent run <name> --adapter claude ${UNSANDBOXED_FLAG}.`
|
|
1233
|
+
+ " It keeps Commonly's restricted tool settings and gets no shell, but nothing at the OS level"
|
|
1234
|
+
+ ' confines it, and it gets no connector grants.'
|
|
1235
|
+
// agent run prefers a saved record over the page's env vars, so an
|
|
1236
|
+
// older record of that name would run with its own settings (#3407).
|
|
1237
|
+
+ ' If this computer already has an agent saved under that name, the run uses that one and its'
|
|
1238
|
+
+ ' settings instead: remove it first with commonly agent detach <name> --force.',
|
|
1239
|
+
' - Use Commonly from your own Claude Code sessions: the Connect page shows a PowerShell `claude mcp add` command.',
|
|
1240
|
+
' - A hosted agent: choose "Run it here" on the Connect page. Nothing runs on this machine.',
|
|
1241
|
+
].join('\n') : windowsSandboxMessage(adapterName)));
|
|
1214
1242
|
if (sandboxMode === 'bwrap') {
|
|
1215
|
-
const bwrap =
|
|
1243
|
+
const bwrap = probeBwrap();
|
|
1216
1244
|
if (!bwrap.available) {
|
|
1245
|
+
if (platform === 'win32') throw windowsRefusal();
|
|
1217
1246
|
throw new Error(`sandbox.mode=bwrap requested but unavailable: ${bwrap.error}`);
|
|
1218
1247
|
}
|
|
1219
1248
|
if (environment.sandbox?.network?.policy === 'restricted') {
|
|
@@ -1239,6 +1268,7 @@ export const performAttach = async ({
|
|
|
1239
1268
|
|| adapterName;
|
|
1240
1269
|
const seatbelt = probeSeatbelt();
|
|
1241
1270
|
if (!seatbelt.available) {
|
|
1271
|
+
if (platform === 'win32') throw windowsRefusal();
|
|
1242
1272
|
// dsh refuses bwrap, so it gets no Linux hint that leads to a second refusal.
|
|
1243
1273
|
throw new Error(adapterName === 'dsh'
|
|
1244
1274
|
? `confined dsh sandbox.mode=${sandboxMode} requires macOS Seatbelt: ${seatbelt.error}.`
|
package/src/commands/daemon.js
CHANGED
|
@@ -300,6 +300,34 @@ export const resolveAdapterForRuntime = async (runtime, registry = { getAdapter
|
|
|
300
300
|
return null;
|
|
301
301
|
};
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* The daemon's first poll and heartbeat, with the Windows launcher watch armed
|
|
305
|
+
* BEFORE them (D245). Arming first puts the EPIPE guard on stdout and stderr
|
|
306
|
+
* before any supervision log line, and a launcher that is already gone stops
|
|
307
|
+
* the daemon here instead of after a tick logged into a dead pipe. Returns
|
|
308
|
+
* false when the daemon should not go on to its timers.
|
|
309
|
+
*/
|
|
310
|
+
export const firstSupervisionPass = async ({
|
|
311
|
+
supervisor,
|
|
312
|
+
onLauncherGone,
|
|
313
|
+
isEnded = () => false,
|
|
314
|
+
armLauncherWatch = watchWindowsLauncher,
|
|
315
|
+
}) => {
|
|
316
|
+
let launcherGone = false;
|
|
317
|
+
armLauncherWatch({
|
|
318
|
+
onGone: (pid) => {
|
|
319
|
+
launcherGone = true;
|
|
320
|
+
onLauncherGone(pid);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const stopped = () => launcherGone || isEnded();
|
|
324
|
+
if (stopped()) return false;
|
|
325
|
+
await supervisor.tick();
|
|
326
|
+
if (stopped()) return false;
|
|
327
|
+
await supervisor.heartbeat();
|
|
328
|
+
return !stopped();
|
|
329
|
+
};
|
|
330
|
+
|
|
303
331
|
export const registerDaemon = (program) => {
|
|
304
332
|
const daemon = program.command('daemon').description('Manage the local Commonly daemon');
|
|
305
333
|
|
|
@@ -654,10 +682,26 @@ Examples:
|
|
|
654
682
|
});
|
|
655
683
|
|
|
656
684
|
stampLog(`daemon supervising for ${record.machineName} — poll ${opts.poll}ms, heartbeat ${opts.heartbeat}ms (ctrl+c to stop)`);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
685
|
+
const shutdown = () => {
|
|
686
|
+
stampLog('daemon stopping — terminating supervised agents');
|
|
687
|
+
clearInterval(pollTimer);
|
|
688
|
+
clearInterval(heartbeatTimer);
|
|
689
|
+
supervisor.stop();
|
|
690
|
+
process.exit(0);
|
|
691
|
+
};
|
|
692
|
+
// D245: under the Windows launcher, the launcher's exit is the stop
|
|
693
|
+
// signal. Armed before the first tick, so the EPIPE guard covers its
|
|
694
|
+
// log lines and a launcher already gone stops the daemon at once.
|
|
695
|
+
const started = await firstSupervisionPass({
|
|
696
|
+
supervisor,
|
|
697
|
+
onLauncherGone: (pid) => {
|
|
698
|
+
// stdout and stderr already swallow EPIPE (armed with the watch).
|
|
699
|
+
try { stampLog(`launcher ${pid} has exited`); } catch { /* stdout is gone */ }
|
|
700
|
+
shutdown();
|
|
701
|
+
},
|
|
702
|
+
isEnded: () => terminalEnded,
|
|
703
|
+
});
|
|
704
|
+
if (!started) return;
|
|
661
705
|
const runtimeFailure = (error) => {
|
|
662
706
|
if (terminalEnded) return;
|
|
663
707
|
terminalEnded = true;
|
|
@@ -669,23 +713,8 @@ Examples:
|
|
|
669
713
|
};
|
|
670
714
|
pollTimer = setInterval(() => { void supervisor.tick().catch(runtimeFailure); }, Number(opts.poll) || DEFAULT_POLL_MS);
|
|
671
715
|
heartbeatTimer = setInterval(() => { void supervisor.heartbeat().catch(runtimeFailure); }, Number(opts.heartbeat) || DEFAULT_HEARTBEAT_MS);
|
|
672
|
-
const shutdown = () => {
|
|
673
|
-
stampLog('daemon stopping — terminating supervised agents');
|
|
674
|
-
clearInterval(pollTimer);
|
|
675
|
-
clearInterval(heartbeatTimer);
|
|
676
|
-
supervisor.stop();
|
|
677
|
-
process.exit(0);
|
|
678
|
-
};
|
|
679
716
|
process.on('SIGINT', shutdown);
|
|
680
717
|
process.on('SIGTERM', shutdown);
|
|
681
|
-
// D245: under the Windows launcher, the launcher's exit is the stop signal.
|
|
682
|
-
watchWindowsLauncher({
|
|
683
|
-
onGone: (pid) => {
|
|
684
|
-
// stdout and stderr already swallow EPIPE (armed with the watch).
|
|
685
|
-
try { stampLog(`launcher ${pid} has exited`); } catch { /* stdout is gone */ }
|
|
686
|
-
shutdown();
|
|
687
|
-
},
|
|
688
|
-
});
|
|
689
718
|
} catch (error) {
|
|
690
719
|
console.error(`Daemon run failed: ${error.message}`);
|
|
691
720
|
const badConfig = !record || error.message.startsWith('Daemon credential file');
|
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
} from '../environment.js';
|
|
85
85
|
import { detectBwrap, wrapArgvWithBwrap } from '../sandbox/bwrap.js';
|
|
86
86
|
import { PUBLIC_SANDBOX_MODES, resolvePublicSandboxMode } from '../sandbox/mode.js';
|
|
87
|
+
import { UNSANDBOXED_FLAG } from '../sandbox/host-check.js';
|
|
87
88
|
import {
|
|
88
89
|
publicClaudeStateRoot,
|
|
89
90
|
wrapArgvWithSeatbelt,
|
|
@@ -1154,6 +1155,16 @@ const prepareArgv = async (innerArgv, ctx) => {
|
|
|
1154
1155
|
// unconfined seat with a public record, which is how a record can claim
|
|
1155
1156
|
// confinement it never applies.
|
|
1156
1157
|
if (isConfinedTrust(sandboxTrust) && !publicNativeSandbox && sandboxMode !== 'bwrap') {
|
|
1158
|
+
// D245: on native Windows, name Windows and the user's own opt-in, never
|
|
1159
|
+
// macOS or bwrap advice (host-check passes an unrecognised mode through).
|
|
1160
|
+
if ((ctx._platform || process.platform) === 'win32') {
|
|
1161
|
+
throw new Error(
|
|
1162
|
+
`${sandboxTrust} Claude agents run inside a sandbox, and Commonly has no sandbox it can enforce on Windows `
|
|
1163
|
+
+ `(got sandbox.mode=${JSON.stringify(sandboxMode)}). To run this agent without one, which is your choice, `
|
|
1164
|
+
+ `run commonly agent run <name> ${UNSANDBOXED_FLAG} (or commonly daemon install ${UNSANDBOXED_FLAG} for the agents `
|
|
1165
|
+
+ 'this computer\'s daemon runs), or choose "Run it here" on the Connect page for a hosted agent.',
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1157
1168
|
throw new Error(
|
|
1158
1169
|
`${sandboxTrust} Claude agents require an enforced sandbox mode `
|
|
1159
1170
|
+ `(workspace or read-only on macOS, bwrap elsewhere), got ${JSON.stringify(sandboxMode)}`,
|
|
@@ -201,7 +201,8 @@ const pidAlive = (pid) => {
|
|
|
201
201
|
* keep running, unsupervised and invisible to the task (seen on windows-latest,
|
|
202
202
|
* 11-04: a stopped task left its daemon checking in). A daemon started by the
|
|
203
203
|
* launcher therefore polls the launcher's pid and calls `onGone` once it has
|
|
204
|
-
* exited
|
|
204
|
+
* exited, at once if it is already gone when the watch is armed. Returns a
|
|
205
|
+
* stop function; does nothing outside the launcher.
|
|
205
206
|
*/
|
|
206
207
|
export const watchWindowsLauncher = ({
|
|
207
208
|
env = process.env,
|
|
@@ -221,6 +222,12 @@ export const watchWindowsLauncher = ({
|
|
|
221
222
|
// swallow errors from the moment the watch is armed, not only after the
|
|
222
223
|
// launcher's exit is noticed up to two seconds later.
|
|
223
224
|
for (const stream of streams) stream?.on?.('error', () => {});
|
|
225
|
+
// Already gone when armed (the daemon is armed before its first tick): say
|
|
226
|
+
// so now, rather than after a poll and a heartbeat logged into a dead pipe.
|
|
227
|
+
if (!isAlive(pid)) {
|
|
228
|
+
onGone(pid);
|
|
229
|
+
return () => {};
|
|
230
|
+
}
|
|
224
231
|
const timer = setIntervalFn(() => {
|
|
225
232
|
if (isAlive(pid)) return;
|
|
226
233
|
clearIntervalFn(timer);
|
|
@@ -41,6 +41,7 @@ import { homedir } from 'os';
|
|
|
41
41
|
import { join } from 'path';
|
|
42
42
|
|
|
43
43
|
import { writeServiceFile } from '../daemon-service.js';
|
|
44
|
+
import { effectiveSandboxTrust } from '../environment.js';
|
|
44
45
|
import { UNSANDBOXED_FLAG } from './host-check.js';
|
|
45
46
|
|
|
46
47
|
export { UNSANDBOXED_FLAG };
|
|
@@ -100,7 +101,9 @@ export const POLICY_ONLY_ADAPTERS = new Set(['claude']);
|
|
|
100
101
|
export const hostUnconfinedForRun = ({
|
|
101
102
|
optedIn, adapterName, environment, platform = process.platform,
|
|
102
103
|
}) => {
|
|
103
|
-
|
|
104
|
+
// The effective trust, as host-check.js and the claude adapter read it: a
|
|
105
|
+
// legacy `internal` record means `public`, so it gets the same opt-in.
|
|
106
|
+
const trust = effectiveSandboxTrust(environment?.sandbox?.trust);
|
|
104
107
|
return platform === 'win32' && optedIn === true && POLICY_ONLY_ADAPTERS.has(adapterName)
|
|
105
108
|
&& (trust === 'public' || trust === 'workspace');
|
|
106
109
|
};
|