@commonlyai/cli 0.1.1 → 0.1.4
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 +2 -2
- package/src/commands/agent.js +122 -32
- package/src/lib/adapters/claude.js +320 -64
- package/src/lib/adapters/codex.js +234 -11
- package/src/lib/environment.js +10 -2
- package/src/lib/sandbox/bwrap.js +11 -0
- package/src/lib/sandbox/seatbelt.js +417 -0
- package/src/lib/session-store.js +63 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonlyai/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"description": "The Commonly CLI
|
|
5
|
+
"description": "The Commonly CLI — connect agents, manage pods, iterate fast",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.js",
|
|
8
8
|
"bin": {
|
package/src/commands/agent.js
CHANGED
|
@@ -21,12 +21,19 @@ import { getToken, resolveInstanceUrl } from '../lib/config.js';
|
|
|
21
21
|
import { startPoller } from '../lib/poller.js';
|
|
22
22
|
import { startWebhookServer, forwardToLocalWebhook } from '../lib/webhook-server.js';
|
|
23
23
|
import { getAdapter, listAdapterNames } from '../lib/adapters/index.js';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getSession,
|
|
26
|
+
setSession,
|
|
27
|
+
clearSessions,
|
|
28
|
+
wasEventHandled,
|
|
29
|
+
recordHandledEvent,
|
|
30
|
+
} from '../lib/session-store.js';
|
|
25
31
|
import { readLongTerm, syncBack } from '../lib/memory-bridge.js';
|
|
26
32
|
import { detectMemorySources, composeImport, importMemory } from '../lib/memory-import.js';
|
|
27
33
|
import { detectSkills, importSkills } from '../lib/skills-import.js';
|
|
28
34
|
import { parseEnvironmentFile, resolveWorkspace } from '../lib/environment.js';
|
|
29
35
|
import { detectBwrap } from '../lib/sandbox/bwrap.js';
|
|
36
|
+
import { detectSeatbelt } from '../lib/sandbox/seatbelt.js';
|
|
30
37
|
|
|
31
38
|
// ── Token file I/O — ~/.commonly/tokens/<name>.json (ADR-005) ───────────────
|
|
32
39
|
|
|
@@ -100,20 +107,41 @@ export const listLocalAgents = () => {
|
|
|
100
107
|
.filter(Boolean);
|
|
101
108
|
};
|
|
102
109
|
|
|
103
|
-
// Event types that carry a
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
const
|
|
110
|
+
// Event types that carry a prompt the wrapper should forward to the CLI.
|
|
111
|
+
// Other event types (heartbeat, delivery, etc.) are acked as no_action even
|
|
112
|
+
// if they happen to carry `content` in their payload.
|
|
113
|
+
const PROMPT_EVENT_TYPES = new Set([
|
|
114
|
+
'chat.mention',
|
|
115
|
+
'message.posted',
|
|
116
|
+
'dm.message',
|
|
117
|
+
'first_contact',
|
|
118
|
+
]);
|
|
107
119
|
|
|
108
120
|
// ── default environment for adapters that benefit from auto-MCP wiring ─────
|
|
109
121
|
|
|
110
122
|
// Adapters that can consume `mcp[]` from the resolved environment spec.
|
|
111
|
-
// `claude` reads it via --mcp-config (see adapters/claude.js)
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
123
|
+
// `claude` reads it via --mcp-config (see adapters/claude.js); `codex` via
|
|
124
|
+
// `-c mcp_servers.*` config overrides (see adapters/codex.js — added after
|
|
125
|
+
// the 2026-07-22 as-operator attribution incident, where an MCP-less codex
|
|
126
|
+
// agent posted through the operator's CLI profile because it had no
|
|
127
|
+
// commonly_* tools of its own). `stub` does not. Returning null means "no
|
|
128
|
+
// default" — the wrapper proceeds with environment=null exactly like before.
|
|
129
|
+
const ADAPTERS_WITH_DEFAULT_MCP = new Set(['claude', 'codex']);
|
|
130
|
+
const CODEX_PERMISSION_PROFILE_MIN_VERSION = [0, 138, 0];
|
|
131
|
+
|
|
132
|
+
const versionAtLeast = (version, minimum) => {
|
|
133
|
+
const parts = String(version || '')
|
|
134
|
+
.match(/\d+(?:\.\d+){1,2}/)?.[0]
|
|
135
|
+
.split('.')
|
|
136
|
+
.map(Number);
|
|
137
|
+
if (!parts) return false;
|
|
138
|
+
for (let i = 0; i < minimum.length; i += 1) {
|
|
139
|
+
const current = parts[i] || 0;
|
|
140
|
+
if (current > minimum[i]) return true;
|
|
141
|
+
if (current < minimum[i]) return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
};
|
|
117
145
|
|
|
118
146
|
// The commonly behavior skill ships inside this package (cli/skills/commonly)
|
|
119
147
|
// so a fresh attach can mount it into the agent without a network fetch.
|
|
@@ -193,6 +221,12 @@ export const performAttach = async ({
|
|
|
193
221
|
log(`workspace: ${workspace.path}${workspace.created ? ' (created)' : ''}`);
|
|
194
222
|
|
|
195
223
|
const sandboxMode = environment.sandbox?.mode || 'none';
|
|
224
|
+
const sandboxTrust = environment.sandbox?.trust;
|
|
225
|
+
if (sandboxTrust === 'public' && sandboxMode === 'none') {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'sandbox.trust=public requires an enforced sandbox mode; refusing to attach unsandboxed',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
196
230
|
if (sandboxMode === 'bwrap') {
|
|
197
231
|
const bwrap = detectBwrap();
|
|
198
232
|
if (!bwrap.available) {
|
|
@@ -204,10 +238,41 @@ export const performAttach = async ({
|
|
|
204
238
|
+ 'are advisory in Phase 1. Use sandbox.mode=container for enforced policy.',
|
|
205
239
|
);
|
|
206
240
|
}
|
|
241
|
+
} else if (sandboxMode === 'workspace' || sandboxMode === 'read-only') {
|
|
242
|
+
if (sandboxTrust !== 'public' || !['codex', 'claude'].includes(adapterName)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`sandbox.mode=${sandboxMode} is currently implemented only for public `
|
|
245
|
+
+ `codex or Claude adapters`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (adapterName === 'codex') {
|
|
249
|
+
if (!versionAtLeast(detected.version, CODEX_PERMISSION_PROFILE_MIN_VERSION)) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`public codex sandbox requires Codex >=0.138.0 permission profiles; `
|
|
252
|
+
+ `detected ${detected.version || 'unknown'}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
log(
|
|
256
|
+
`sandbox: public ${sandboxMode} via Codex native permission profile `
|
|
257
|
+
+ '(deny-by-default filesystem + network)',
|
|
258
|
+
);
|
|
259
|
+
} else {
|
|
260
|
+
const seatbelt = detectSeatbelt();
|
|
261
|
+
if (!seatbelt.available) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`public Claude sandbox.mode=${sandboxMode} requires macOS Seatbelt: `
|
|
264
|
+
+ `${seatbelt.error}. On Linux use sandbox.mode=bwrap.`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
log(
|
|
268
|
+
`sandbox: public ${sandboxMode} via macOS Seatbelt `
|
|
269
|
+
+ '(deny-by-default host filesystem; Claude/MCP network retained)',
|
|
270
|
+
);
|
|
271
|
+
}
|
|
207
272
|
} else if (sandboxMode !== 'none' && sandboxMode !== undefined) {
|
|
208
273
|
throw new Error(
|
|
209
274
|
`sandbox.mode=${sandboxMode} is not yet implemented in the local-CLI driver. `
|
|
210
|
-
+ `
|
|
275
|
+
+ `Supported locally: none, bwrap, and public codex/Claude workspace/read-only.`,
|
|
211
276
|
);
|
|
212
277
|
}
|
|
213
278
|
} else {
|
|
@@ -366,7 +431,7 @@ export const runMemoryImport = async ({
|
|
|
366
431
|
// ── run: local-CLI wrapper loop (ADR-005) ────────────────────────────────────
|
|
367
432
|
|
|
368
433
|
const extractPrompt = (event) => {
|
|
369
|
-
if (!
|
|
434
|
+
if (!PROMPT_EVENT_TYPES.has(event.type)) return null;
|
|
370
435
|
const p = event.payload || {};
|
|
371
436
|
return p.content || p.prompt || p.text || null;
|
|
372
437
|
};
|
|
@@ -432,7 +497,7 @@ export const performRun = ({
|
|
|
432
497
|
// and (if the adapter returns a summary) patch-sync back after.
|
|
433
498
|
const memoryLongTerm = await readLongTerm(client, { onError });
|
|
434
499
|
|
|
435
|
-
// Snapshot the pod's recent
|
|
500
|
+
// Snapshot the pod's recent messages so that, after the spawn, we can
|
|
436
501
|
// tell whether the agent posted itself via commonly_post_message. If it
|
|
437
502
|
// did, its final CLI text is a narration/log — echoing it would duplicate
|
|
438
503
|
// the message and re-fire any @mention. This is the wrapper-side guarantee
|
|
@@ -440,17 +505,20 @@ export const performRun = ({
|
|
|
440
505
|
// without relying on the agent to emit NO_REPLY. (A rare concurrent post by
|
|
441
506
|
// another member during the spawn window can suppress a genuine wrapper
|
|
442
507
|
// reply — an acceptable trade against a guaranteed double-post.)
|
|
443
|
-
const
|
|
508
|
+
const snapshotMessages = async () => {
|
|
444
509
|
try {
|
|
445
510
|
const { messages = [] } = await client.get(
|
|
446
511
|
`/api/agents/runtime/pods/${eventPodId}/messages`, { limit: 10 },
|
|
447
512
|
);
|
|
448
|
-
return
|
|
513
|
+
return messages;
|
|
449
514
|
} catch {
|
|
450
515
|
return null; // detection unavailable — fall back to posting the reply
|
|
451
516
|
}
|
|
452
517
|
};
|
|
453
|
-
const
|
|
518
|
+
const preSpawn = await snapshotMessages();
|
|
519
|
+
const preSpawnIds = preSpawn
|
|
520
|
+
? new Set(preSpawn.map((m) => String(m._id || m.id)))
|
|
521
|
+
: null;
|
|
454
522
|
|
|
455
523
|
log(`[${event.type}] spawning ${adapter.name}`);
|
|
456
524
|
const result = await adapter.spawn(prompt, {
|
|
@@ -459,11 +527,13 @@ export const performRun = ({
|
|
|
459
527
|
env: process.env,
|
|
460
528
|
memoryLongTerm,
|
|
461
529
|
environment,
|
|
462
|
-
// Runtime context the
|
|
463
|
-
//
|
|
464
|
-
//
|
|
530
|
+
// Runtime context the Claude/Codex adapters expose only to their
|
|
531
|
+
// per-spawn MCP environment. Claude keeps ${COMMONLY_*} placeholders
|
|
532
|
+
// literal on disk and lets its native MCP parser expand them, so the
|
|
533
|
+
// bearer token never enters the generated config file.
|
|
465
534
|
runtimeToken: token,
|
|
466
535
|
instanceUrl,
|
|
536
|
+
agentName,
|
|
467
537
|
metadata: { event },
|
|
468
538
|
});
|
|
469
539
|
|
|
@@ -482,10 +552,19 @@ export const performRun = ({
|
|
|
482
552
|
const replyText = (result.text || '').trim();
|
|
483
553
|
let agentPostedItself = false;
|
|
484
554
|
if (preSpawnIds) {
|
|
485
|
-
const
|
|
486
|
-
if (
|
|
487
|
-
for (const
|
|
488
|
-
|
|
555
|
+
const postSpawn = await snapshotMessages();
|
|
556
|
+
if (postSpawn) {
|
|
557
|
+
for (const m of postSpawn) {
|
|
558
|
+
// Only a NEW message from a bot user counts as "the agent posted
|
|
559
|
+
// itself". A new human-authored message must NOT suppress the echo:
|
|
560
|
+
// it is either a human typing mid-turn, or the agent misusing an
|
|
561
|
+
// operator CLI profile / human token (the 2026-07-22 as-operator
|
|
562
|
+
// attribution incident) — in both cases the wrapper still delivers
|
|
563
|
+
// the reply under the agent's own identity.
|
|
564
|
+
if (!preSpawnIds.has(String(m._id || m.id)) && m.isBot) {
|
|
565
|
+
agentPostedItself = true;
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
489
568
|
}
|
|
490
569
|
}
|
|
491
570
|
}
|
|
@@ -524,13 +603,23 @@ export const performRun = ({
|
|
|
524
603
|
for (const event of events) {
|
|
525
604
|
if (!running) break;
|
|
526
605
|
let result;
|
|
527
|
-
|
|
528
|
-
result =
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
606
|
+
if (wasEventHandled(agentName, event._id)) {
|
|
607
|
+
result = { outcome: 'no_action', reason: 'duplicate-delivery' };
|
|
608
|
+
log(`[${event.type}] duplicate delivery ${event._id} — skipping spawn and re-acking`);
|
|
609
|
+
} else {
|
|
610
|
+
try {
|
|
611
|
+
result = await processEvent(event);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
// Spawn failed — do not record or ack, so the kernel can re-deliver
|
|
614
|
+
// the event after the local runtime recovers (ADR-005).
|
|
615
|
+
log(`[${event.type}] spawn error: ${err.message}`);
|
|
616
|
+
onError?.(err);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
// Record after successful processing but before ack. If the ack
|
|
620
|
+
// fails, the next delivery is skipped and re-acked instead of
|
|
621
|
+
// burning a second model turn for work that already completed.
|
|
622
|
+
recordHandledEvent(agentName, event._id);
|
|
534
623
|
}
|
|
535
624
|
try {
|
|
536
625
|
await client.post(`/api/agents/runtime/events/${event._id}/ack`, { result });
|
|
@@ -685,7 +774,8 @@ export const performInit = async ({
|
|
|
685
774
|
* 7 days after all installs go inactive — we don't force-revoke here
|
|
686
775
|
* because the token may legitimately still be in use for another pod.
|
|
687
776
|
* 2. Local token file at ~/.commonly/tokens/<name>.json
|
|
688
|
-
* 3. Local session
|
|
777
|
+
* 3. Local session + handled-event stores at
|
|
778
|
+
* ~/.commonly/sessions/<name>{,.events}.json
|
|
689
779
|
*
|
|
690
780
|
* Idempotent: if the backend returns 404 (already uninstalled elsewhere), we
|
|
691
781
|
* still clean up local files so the CLI state stays in sync with reality.
|