@commonlyai/cli 0.1.69 → 0.1.70
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 +1 -1
- package/src/commands/agent.js +23 -1
- package/src/lib/environment.js +16 -0
- package/src/lib/grant-broker-guard.js +3 -10
- package/src/lib/sandbox/mode.js +14 -7
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -34,6 +34,7 @@ import { detectMemorySources, composeImport, importMemory } from '../lib/memory-
|
|
|
34
34
|
import { detectSkills, importSkills } from '../lib/skills-import.js';
|
|
35
35
|
import { parseEnvironmentFile, resolveWorkspace, validateEnvironmentSpec } from '../lib/environment.js';
|
|
36
36
|
import { ADAPTERS_WITH_DEFAULT_MCP, defaultMcpServers } from '../lib/default-environment.js';
|
|
37
|
+
import { withholdGrantBroker } from '../lib/grant-broker-guard.js';
|
|
37
38
|
import {
|
|
38
39
|
FOCUS_FRAME_MAX_CODE_POINTS,
|
|
39
40
|
formatPodFocusFrame,
|
|
@@ -872,6 +873,27 @@ export const performRun = ({
|
|
|
872
873
|
sleepImpl = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }),
|
|
873
874
|
}) => {
|
|
874
875
|
const client = createClient({ instance: instanceUrl, token });
|
|
876
|
+
|
|
877
|
+
// THE SEAT'S OWN RECORD IS THE ONLY DECLARATION THERE IS. The broker reaches a
|
|
878
|
+
// seat as an injected MCP entry written by hand into its environment — no
|
|
879
|
+
// server projection puts it there (`GRANT_BROKER_URL` is imported by one
|
|
880
|
+
// route, the daemon assignment, and that one already refuses). So a seat run
|
|
881
|
+
// by `commonly agent run` under launchd spawns the entry verbatim, and the
|
|
882
|
+
// confinement it promises is declared in a different field (`sandbox`), which
|
|
883
|
+
// can disagree. The daemon half of this refusal already runs
|
|
884
|
+
// (`daemon-supervisor.js`); this is the same predicate at the only other site
|
|
885
|
+
// that can see the record — same code, so one refusal with two emitters.
|
|
886
|
+
//
|
|
887
|
+
// IN MEMORY, RECORD UNTOUCHED (wren 71641): the withheld environment is what
|
|
888
|
+
// the adapter spawns with, and the stored record keeps its broker, so the
|
|
889
|
+
// daemon stays the layer that judges a host it knows. Applied once for the
|
|
890
|
+
// run rather than per turn: `agent run` reads its record once at boot.
|
|
891
|
+
const seatEnvironment = withholdGrantBroker(environment, adapter?.name, {
|
|
892
|
+
instanceUrl,
|
|
893
|
+
onRefuse: (refusal, names) => log(
|
|
894
|
+
`${refusal.code} (${refusal.reason}) — withholding ${names.join(', ')}: ${refusal.detail}`,
|
|
895
|
+
),
|
|
896
|
+
});
|
|
875
897
|
let running = true;
|
|
876
898
|
// Stop-after-N-auth-failures: without this, a revoked token leaves the
|
|
877
899
|
// poller hammering 401s forever at 60s backoff — invisible to the user.
|
|
@@ -1195,7 +1217,7 @@ export const performRun = ({
|
|
|
1195
1217
|
cwd: agentCwd,
|
|
1196
1218
|
env: process.env,
|
|
1197
1219
|
memoryLongTerm,
|
|
1198
|
-
environment,
|
|
1220
|
+
environment: seatEnvironment,
|
|
1199
1221
|
// Runtime context the Claude/Codex adapters expose only to their
|
|
1200
1222
|
// per-spawn MCP environment. Claude keeps ${COMMONLY_*} placeholders
|
|
1201
1223
|
// literal on disk and lets its native MCP parser expand them, so the
|
package/src/lib/environment.js
CHANGED
|
@@ -69,6 +69,22 @@ export const normalizeSandboxTrust = (sandbox) => (
|
|
|
69
69
|
? { ...sandbox, trust: LEGACY_SANDBOX_TRUST[sandbox.trust] }
|
|
70
70
|
: sandbox
|
|
71
71
|
);
|
|
72
|
+
/**
|
|
73
|
+
* A declared trust resolved THROUGH the legacy table: `internal` → `public`,
|
|
74
|
+
* anything else (including absent) is itself.
|
|
75
|
+
*
|
|
76
|
+
* Exported so every gate reads the mapping once. `normalizeSandboxTrust` above
|
|
77
|
+
* rewrites a whole sandbox object; this is the scalar form for a site that only
|
|
78
|
+
* compares the trust and must not write anything back. The server spells the
|
|
79
|
+
* same predicate `effectiveSandboxTrust` (`grantBrokerConfinement.ts`) — the two
|
|
80
|
+
* sides have to read `internal` the same way or one of them confines a seat the
|
|
81
|
+
* other withholds from (Wren 71662).
|
|
82
|
+
*/
|
|
83
|
+
export const effectiveSandboxTrust = (trust) => (
|
|
84
|
+
typeof trust === 'string' && Object.prototype.hasOwnProperty.call(LEGACY_SANDBOX_TRUST, trust)
|
|
85
|
+
? LEGACY_SANDBOX_TRUST[trust]
|
|
86
|
+
: trust
|
|
87
|
+
);
|
|
72
88
|
|
|
73
89
|
const expandHome = (p) => {
|
|
74
90
|
if (!p || typeof p !== 'string') return p;
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
*/
|
|
75
75
|
import { isGrantBrokerUrl } from './adapters/pi-mcp-client.mjs';
|
|
76
76
|
import { ADAPTERS_WITH_DEFAULT_SANDBOX } from './default-environment.js';
|
|
77
|
-
import {
|
|
77
|
+
import { effectiveSandboxTrust } from './environment.js';
|
|
78
78
|
import { PUBLIC_SANDBOX_MODES, resolvePublicSandboxMode } from './sandbox/mode.js';
|
|
79
79
|
|
|
80
80
|
/** One typed code, two emitters (`decidedBy` says which one spoke). */
|
|
@@ -92,13 +92,6 @@ export const ENFORCING_MODES = new Set([...PUBLIC_SANDBOX_MODES, 'bwrap']);
|
|
|
92
92
|
const URL_PLACEHOLDERS = ['${COMMONLY_API_URL}', '${COMMONLY_INSTANCE_URL}'];
|
|
93
93
|
const PARSE_ANCHOR = 'https://grant-declaration.invalid';
|
|
94
94
|
|
|
95
|
-
/** `internal` → `public`; anything else (including absent) is itself. */
|
|
96
|
-
const effectiveTrust = (trust) => (
|
|
97
|
-
typeof trust === 'string' && Object.prototype.hasOwnProperty.call(LEGACY_SANDBOX_TRUST, trust)
|
|
98
|
-
? LEGACY_SANDBOX_TRUST[trust]
|
|
99
|
-
: trust
|
|
100
|
-
);
|
|
101
|
-
|
|
102
95
|
/**
|
|
103
96
|
* A bound instance is only trimmed here (`agent.js` does the same). Any
|
|
104
97
|
* trailing slash it carries is deliberately left alone: the doubled slash it
|
|
@@ -190,7 +183,7 @@ export const confinementReason = (environment, adapter, platform = process.platf
|
|
|
190
183
|
if (sandbox === null || typeof sandbox !== 'object' || Array.isArray(sandbox)) return 'sandbox_absent';
|
|
191
184
|
const declared = sandbox;
|
|
192
185
|
if (declared.mode === 'none') return 'sandbox_mode_none';
|
|
193
|
-
if (
|
|
186
|
+
if (effectiveSandboxTrust(declared.trust) !== 'public') return 'sandbox_trust_not_public';
|
|
194
187
|
const mode = resolvePublicSandboxMode(declared, platform);
|
|
195
188
|
if (typeof mode !== 'string' || !ENFORCING_MODES.has(mode)) return 'sandbox_mode_unenforceable';
|
|
196
189
|
return null;
|
|
@@ -220,7 +213,7 @@ const detailFor = (reason, adapter, environment) => {
|
|
|
220
213
|
}
|
|
221
214
|
const trust = environment?.sandbox?.trust;
|
|
222
215
|
const shown = trust === undefined ? 'absent' : `'${String(trust)}'`;
|
|
223
|
-
return `the declared sandbox.trust is ${shown}${shown === 'absent' ? '' : ` (effective '${String(
|
|
216
|
+
return `the declared sandbox.trust is ${shown}${shown === 'absent' ? '' : ` (effective '${String(effectiveSandboxTrust(trust))}')`},`
|
|
224
217
|
+ " and no host confines a seat whose trust is not 'public'; declare sandbox.trust 'public'" + ` ${drop}`;
|
|
225
218
|
};
|
|
226
219
|
|
package/src/lib/sandbox/mode.js
CHANGED
|
@@ -12,23 +12,30 @@
|
|
|
12
12
|
* darwin → 'workspace' (the claude adapter maps it to Seatbelt)
|
|
13
13
|
* else → 'bwrap' (bubblewrap; attach checks it is installed)
|
|
14
14
|
*
|
|
15
|
-
* Resolution is PUBLIC-only on purpose
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* Resolution is PUBLIC-only on purpose, and PUBLIC IS THE EFFECTIVE TRUST: a
|
|
16
|
+
* legacy `internal` declaration reads as `public` (environment.js, Wren 69585),
|
|
17
|
+
* so the mapping is applied before this comparison. Comparing the raw value
|
|
18
|
+
* made `{ trust: 'internal' }` resolve to no mode at all — the mapping was
|
|
19
|
+
* applied in `confinementReason` and not here, which is half a rule, and the
|
|
20
|
+
* half that failed was the one that WITHHOLDS: the grant broker was taken from
|
|
21
|
+
* a seat this daemon would have confined. A non-public declaration keeps
|
|
22
|
+
* whatever mode it wrote, and an adapter that cannot enforce it is expected to
|
|
23
|
+
* refuse rather than quietly spawn unconfined. An explicit mode on a public
|
|
24
|
+
* declaration still wins — including a wrong one, which the caller then refuses
|
|
25
|
+
* (Vera 69548: an unresolvable public mode must not fall through to a bare,
|
|
26
|
+
* unconfined spawn).
|
|
21
27
|
*
|
|
22
28
|
* NOT used by the codex adapter. Codex's `mode` is read-vs-write access, not a
|
|
23
29
|
* host mechanism — its permission profiles run on both platforms — so its
|
|
24
30
|
* derived default is 'workspace' everywhere.
|
|
25
31
|
*/
|
|
32
|
+
import { effectiveSandboxTrust } from '../environment.js';
|
|
26
33
|
|
|
27
34
|
export const PUBLIC_SANDBOX_MODES = new Set(['workspace', 'read-only']);
|
|
28
35
|
|
|
29
36
|
export const resolvePublicSandboxMode = (sandbox, platform = process.platform) => {
|
|
30
37
|
const declared = sandbox?.mode;
|
|
31
|
-
if (sandbox?.trust !== 'public') return declared;
|
|
38
|
+
if (effectiveSandboxTrust(sandbox?.trust) !== 'public') return declared;
|
|
32
39
|
if (declared !== undefined && declared !== null) return declared;
|
|
33
40
|
return platform === 'darwin' ? 'workspace' : 'bwrap';
|
|
34
41
|
};
|