@commonlyai/cli 0.1.69 → 0.1.71
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
|
@@ -32,8 +32,9 @@ import { readLongTerm, syncBack } from '../lib/memory-bridge.js';
|
|
|
32
32
|
import { pollRetryPolicy } from '../lib/poll-retry.js';
|
|
33
33
|
import { detectMemorySources, composeImport, importMemory } from '../lib/memory-import.js';
|
|
34
34
|
import { detectSkills, importSkills } from '../lib/skills-import.js';
|
|
35
|
-
import { parseEnvironmentFile, resolveWorkspace, validateEnvironmentSpec } from '../lib/environment.js';
|
|
35
|
+
import { normalizeSandboxTrust, 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,
|
|
@@ -330,8 +331,14 @@ export const assertSandboxDeclaredForPublicPod = async ({
|
|
|
330
331
|
}) => {
|
|
331
332
|
if (!podId || !client) return;
|
|
332
333
|
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
// Normalized FIRST (Vera 71715, Wren 71718): the predicate below reads a
|
|
335
|
+
// legacy `internal` record as the `public` it means, the same mapping the
|
|
336
|
+
// attach gate and both adapters use. Reading it raw refused a record that
|
|
337
|
+
// means public one call before the gate #1840 fixes — TASK-113 claims
|
|
338
|
+
// attach's inconsistent derivation is closed, and this path is attach's.
|
|
339
|
+
const sandbox = normalizeSandboxTrust(environment?.sandbox);
|
|
340
|
+
const mode = sandbox?.mode;
|
|
341
|
+
const trust = sandbox?.trust;
|
|
335
342
|
// An ENFORCED declaration is one the adapters act on: a public trust (whose
|
|
336
343
|
// mode they resolve per host at spawn) or an explicit non-'none' mode. This
|
|
337
344
|
// mirrors the daemon's predicate in lib/default-environment.js, so the shape
|
|
@@ -476,6 +483,70 @@ export const updateAgentConfiguration = async ({
|
|
|
476
483
|
|
|
477
484
|
// ── attach: register a local-CLI-wrapped agent (ADR-005) ────────────────────
|
|
478
485
|
|
|
486
|
+
/**
|
|
487
|
+
* The sandbox an attach will actually run under — derived, then gated.
|
|
488
|
+
*
|
|
489
|
+
* The trust is normalized ONCE, before it is used for anything. A stored
|
|
490
|
+
* `internal` means `public` (environment.js, Wren 69585), and resolving the mode
|
|
491
|
+
* off the raw value made this gate disagree with the adapters in BOTH
|
|
492
|
+
* directions: `{ trust: 'internal' }` with no declared mode left the attach-time
|
|
493
|
+
* mode at `'none'` and refused nothing here, and `internal` beside
|
|
494
|
+
* `mode: 'workspace'` was refused outright as "implemented only for public codex
|
|
495
|
+
* or Claude adapters" — turning away a record both adapters confine happily.
|
|
496
|
+
*
|
|
497
|
+
* This does NOT take the confinement decision from the adapters. They normalize
|
|
498
|
+
* and re-derive from the environment themselves (claude.js:615, codex.js:546) and
|
|
499
|
+
* the attach-time value never reaches them, so a legacy record was confined
|
|
500
|
+
* either way; what was wrong is that this gate's verdict contradicted theirs, and
|
|
501
|
+
* its refusal for the `mode: 'none'` shape landed later, at adapter spawn, after
|
|
502
|
+
* attach had already published and minted. Normalizing here makes the two agree
|
|
503
|
+
* and puts the refusal where the user can see it (Kai, TASK-113; re-measured at
|
|
504
|
+
* source after Vera 71664).
|
|
505
|
+
*
|
|
506
|
+
* Whether the resolved mode is AVAILABLE here — bwrap installed, macOS for
|
|
507
|
+
* Seatbelt, the codex permission-profile version — is deliberately not this
|
|
508
|
+
* function's question: that needs the host and the detected version and stays at
|
|
509
|
+
* the call site. What an adapter can honour at all is just a mode and a name, so
|
|
510
|
+
* that check is in here, with witnesses of both signs.
|
|
511
|
+
*
|
|
512
|
+
* Why this gate was the one that disagreed: the resolver compared the RAW trust,
|
|
513
|
+
* so a caller that did not normalize first read a legacy `internal` record as
|
|
514
|
+
* non-public. The adapters normalize at their own entry (claude.js:615,
|
|
515
|
+
* codex.js:546) and passed the normalized object on, which is why their suites
|
|
516
|
+
* confined a legacy record all along; the grant-broker guard normalized for its
|
|
517
|
+
* own trust check and then handed the resolver the raw object, so its symptom was
|
|
518
|
+
* a false `sandbox_mode_unenforceable` refusal (Vera 71676). Both are settled on
|
|
519
|
+
* main now — the resolver reads the effective trust (mode.js:38) and the guard
|
|
520
|
+
* imports the same reader from environment.js (grant-broker-guard.js:186) — so
|
|
521
|
+
* this helper needs no coupling to them: it normalizes once, here, before its own
|
|
522
|
+
* two gates, because those gates compare `trust` directly.
|
|
523
|
+
*/
|
|
524
|
+
export const resolveAttachSandbox = ({
|
|
525
|
+
environment, adapterName, platform = process.platform,
|
|
526
|
+
} = {}) => {
|
|
527
|
+
const sandbox = normalizeSandboxTrust(environment?.sandbox);
|
|
528
|
+
const trust = sandbox?.trust;
|
|
529
|
+
const mode = sandbox?.mode
|
|
530
|
+
|| (trust === 'public' ? resolvePublicSandboxMode(sandbox, platform) : 'none');
|
|
531
|
+
if (trust === 'public' && mode === 'none') {
|
|
532
|
+
throw new Error(
|
|
533
|
+
'sandbox.trust=public requires an enforced sandbox mode; refusing to attach unsandboxed',
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
// Moved in from the call site (Vera 71675): this is the check the raw compare
|
|
537
|
+
// used to fail closed on, refusing `internal` beside `mode: 'workspace'`. It is
|
|
538
|
+
// pure — a mode and an adapter name — so it belongs with the derivation, and
|
|
539
|
+
// its witnesses live beside it.
|
|
540
|
+
if ((mode === 'workspace' || mode === 'read-only')
|
|
541
|
+
&& (trust !== 'public' || !['codex', 'claude'].includes(adapterName))) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`sandbox.mode=${mode} is currently implemented only for public `
|
|
544
|
+
+ 'codex or Claude adapters',
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return { mode, trust };
|
|
548
|
+
};
|
|
549
|
+
|
|
479
550
|
/**
|
|
480
551
|
* Publish, install, and mint a runtime token for a local-CLI-wrapped agent.
|
|
481
552
|
* Pure core — the commander action wraps this with config loading + logging.
|
|
@@ -512,16 +583,7 @@ export const performAttach = async ({
|
|
|
512
583
|
workspace = await resolveWorkspace(environment, agentName, dirname(envPath));
|
|
513
584
|
log(`workspace: ${workspace.path}${workspace.created ? ' (created)' : ''}`);
|
|
514
585
|
|
|
515
|
-
const sandboxMode = environment
|
|
516
|
-
|| (environment.sandbox?.trust === 'public'
|
|
517
|
-
? resolvePublicSandboxMode(environment.sandbox)
|
|
518
|
-
: 'none');
|
|
519
|
-
const sandboxTrust = environment.sandbox?.trust;
|
|
520
|
-
if (sandboxTrust === 'public' && sandboxMode === 'none') {
|
|
521
|
-
throw new Error(
|
|
522
|
-
'sandbox.trust=public requires an enforced sandbox mode; refusing to attach unsandboxed',
|
|
523
|
-
);
|
|
524
|
-
}
|
|
586
|
+
const { mode: sandboxMode } = resolveAttachSandbox({ environment, adapterName });
|
|
525
587
|
if (sandboxMode === 'bwrap') {
|
|
526
588
|
const bwrap = detectBwrap();
|
|
527
589
|
if (!bwrap.available) {
|
|
@@ -534,12 +596,6 @@ export const performAttach = async ({
|
|
|
534
596
|
);
|
|
535
597
|
}
|
|
536
598
|
} else if (sandboxMode === 'workspace' || sandboxMode === 'read-only') {
|
|
537
|
-
if (sandboxTrust !== 'public' || !['codex', 'claude'].includes(adapterName)) {
|
|
538
|
-
throw new Error(
|
|
539
|
-
`sandbox.mode=${sandboxMode} is currently implemented only for public `
|
|
540
|
-
+ `codex or Claude adapters`,
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
599
|
if (adapterName === 'codex') {
|
|
544
600
|
if (!versionAtLeast(detected.version, CODEX_PERMISSION_PROFILE_MIN_VERSION)) {
|
|
545
601
|
throw new Error(
|
|
@@ -872,6 +928,27 @@ export const performRun = ({
|
|
|
872
928
|
sleepImpl = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }),
|
|
873
929
|
}) => {
|
|
874
930
|
const client = createClient({ instance: instanceUrl, token });
|
|
931
|
+
|
|
932
|
+
// THE SEAT'S OWN RECORD IS THE ONLY DECLARATION THERE IS. The broker reaches a
|
|
933
|
+
// seat as an injected MCP entry written by hand into its environment — no
|
|
934
|
+
// server projection puts it there (`GRANT_BROKER_URL` is imported by one
|
|
935
|
+
// route, the daemon assignment, and that one already refuses). So a seat run
|
|
936
|
+
// by `commonly agent run` under launchd spawns the entry verbatim, and the
|
|
937
|
+
// confinement it promises is declared in a different field (`sandbox`), which
|
|
938
|
+
// can disagree. The daemon half of this refusal already runs
|
|
939
|
+
// (`daemon-supervisor.js`); this is the same predicate at the only other site
|
|
940
|
+
// that can see the record — same code, so one refusal with two emitters.
|
|
941
|
+
//
|
|
942
|
+
// IN MEMORY, RECORD UNTOUCHED (wren 71641): the withheld environment is what
|
|
943
|
+
// the adapter spawns with, and the stored record keeps its broker, so the
|
|
944
|
+
// daemon stays the layer that judges a host it knows. Applied once for the
|
|
945
|
+
// run rather than per turn: `agent run` reads its record once at boot.
|
|
946
|
+
const seatEnvironment = withholdGrantBroker(environment, adapter?.name, {
|
|
947
|
+
instanceUrl,
|
|
948
|
+
onRefuse: (refusal, names) => log(
|
|
949
|
+
`${refusal.code} (${refusal.reason}) — withholding ${names.join(', ')}: ${refusal.detail}`,
|
|
950
|
+
),
|
|
951
|
+
});
|
|
875
952
|
let running = true;
|
|
876
953
|
// Stop-after-N-auth-failures: without this, a revoked token leaves the
|
|
877
954
|
// poller hammering 401s forever at 60s backoff — invisible to the user.
|
|
@@ -1195,7 +1272,7 @@ export const performRun = ({
|
|
|
1195
1272
|
cwd: agentCwd,
|
|
1196
1273
|
env: process.env,
|
|
1197
1274
|
memoryLongTerm,
|
|
1198
|
-
environment,
|
|
1275
|
+
environment: seatEnvironment,
|
|
1199
1276
|
// Runtime context the Claude/Codex adapters expose only to their
|
|
1200
1277
|
// per-spawn MCP environment. Claude keeps ${COMMONLY_*} placeholders
|
|
1201
1278
|
// literal on disk and lets its native MCP parser expand them, so the
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* declaration itself carries no secret and is safe to persist to a token file.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { normalizeSandboxTrust } from './environment.js';
|
|
26
|
+
|
|
25
27
|
export const ADAPTERS_WITH_DEFAULT_MCP = new Set(['claude', 'codex', 'pi']);
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -129,7 +131,12 @@ export const withDefaultSandbox = (environment) => {
|
|
|
129
131
|
&& (typeof environment !== 'object' || Array.isArray(environment))) {
|
|
130
132
|
return environment;
|
|
131
133
|
}
|
|
132
|
-
|
|
134
|
+
// Normalized first (Wren 71719): `assertSandboxDeclaredForPublicPod`'s own
|
|
135
|
+
// comment says this predicate mirrors it, and a legacy `internal` record
|
|
136
|
+
// means `public` to both. Read raw, this one replaced a record the attach
|
|
137
|
+
// gate accepts — benign (same effective trust) but not a mirror, and the
|
|
138
|
+
// comment would be false. The ORIGINAL block is what gets returned below.
|
|
139
|
+
const sandbox = normalizeSandboxTrust(environment?.sandbox);
|
|
133
140
|
// An ENFORCED declaration is one the adapters act on: a public trust (whose
|
|
134
141
|
// mode they resolve, and refuse to spawn without) or an explicit non-'none'
|
|
135
142
|
// mode. `mode: 'none'`, an empty block and a missing one are all the same
|
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
|
};
|