@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.69",
3
+ "version": "0.1.70",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -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
@@ -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 { LEGACY_SANDBOX_TRUST } from './environment.js';
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 (effectiveTrust(declared.trust) !== 'public') return 'sandbox_trust_not_public';
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(effectiveTrust(trust))}')`},`
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
 
@@ -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. A non-public declaration keeps whatever
16
- * mode it wrote, and an adapter that cannot enforce it is expected to refuse
17
- * rather than quietly spawn unconfined. An explicit mode on a public
18
- * declaration still wins — including a wrong one, which the caller then
19
- * refuses (Vera 69548: an unresolvable public mode must not fall through to a
20
- * bare, unconfined spawn).
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
  };