comisai 1.0.22 → 1.0.23

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.
@@ -449,6 +449,24 @@ export function createPiExecutor(config, deps) {
449
449
  }
450
450
  const resourceLoader = new DefaultResourceLoader(resourceLoaderOptions);
451
451
  await resourceLoader.reload();
452
+ // The SDK's `tools` is an allowlist of tool *names* (not definitions).
453
+ // An empty array is treated as a non-empty allowlist that allows zero
454
+ // tools, including all customTools — which is why the agent ran
455
+ // tool-less from every entry point (chat API, SSE, Telegram, etc.):
456
+ // every Comis tool was filtered out of the SDK's tool registry, the
457
+ // Anthropic API request went out with `tools: []`, and the model
458
+ // emitted `<tool_call>...</tool_call>` markup as plaintext that
459
+ // Comis's loop never parsed back.
460
+ //
461
+ // Pass our customTool names as the explicit allowlist so:
462
+ // 1. All customTools land in the SDK's tool registry (their names
463
+ // pass `isAllowedTool`).
464
+ // 2. SDK built-ins like `bash` that conflict with Comis's policy
465
+ // controls are filtered out (Comis uses `exec` instead, with
466
+ // its own sandbox/audit hooks).
467
+ // 3. Where names overlap (read/edit/write), Comis's customTools
468
+ // override the SDK built-ins via Map.set() in the registry
469
+ // build (`agent-session.js:1810-1813` in pi-coding-agent@0.68.0).
452
470
  const sessionOptions = {
453
471
  cwd: deps.workspaceDir,
454
472
  authStorage: deps.authStorage,
@@ -457,7 +475,7 @@ export function createPiExecutor(config, deps) {
457
475
  sessionManager: sm,
458
476
  settingsManager,
459
477
  resourceLoader,
460
- tools: [],
478
+ tools: mergedCustomTools.map((t) => t.name),
461
479
  customTools: mergedCustomTools,
462
480
  };
463
481
  const { session, modelFallbackMessage } = await createAgentSession(sessionOptions);
@@ -654,11 +672,20 @@ export function createPiExecutor(config, deps) {
654
672
  const postActiveNames = session.getActiveToolNames?.() ?? [];
655
673
  if (postActiveNames.length < mergedToolNames.length) {
656
674
  const rejected = mergedToolNames.filter(n => !postActiveNames.includes(n));
675
+ const allRejected = postActiveNames.length === 0 && rejected.length === mergedToolNames.length;
657
676
  deps.logger.warn({
658
677
  rejected,
659
- hint: "SDK filtered some tools that Comis registered; check tool name collisions with SDK built-ins",
678
+ rejectedCount: rejected.length,
679
+ registeredCount: mergedToolNames.length,
680
+ postActiveCount: postActiveNames.length,
681
+ allRejected,
682
+ hint: allRejected
683
+ ? "SDK has 0 active tools after setActiveToolsByName -- not a name collision (empty active list, every Comis tool dropped). Indicates the SDK ResourceLoader / agent.tools handoff is broken; the LLM will receive no structured tool definitions and may emit `<tool_call>` markup as plaintext instead of using tool_use content blocks."
684
+ : "SDK filtered some Comis tools; likely name collisions with SDK built-ins (e.g. SDK reserves `bash`, `read_file`, etc.). Rename or omit the listed tools to avoid the conflict.",
660
685
  errorKind: "validation",
661
- }, "SDK rejected some tool registrations");
686
+ }, allRejected
687
+ ? "SDK rejected ALL tool registrations -- agent will run with no tools"
688
+ : "SDK rejected some tool registrations");
662
689
  }
663
690
  }
664
691
  catch (toolMgmtError) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/agent",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "AI agent executor, budget control, and session management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/channels",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Chat platform adapters — Discord, Telegram, Slack, WhatsApp, Signal, iMessage, IRC, LINE",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/cli",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Command-line interface for the Comis AI agent platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/core",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Core domain types, ports, event bus, security, and config for Comis",
@@ -182,13 +182,20 @@ export async function main(overrides = {}) {
182
182
  // better-sqlite3 'bindings' module fails fast with a clear repair hint
183
183
  // instead of cascading into a systemd restart loop.
184
184
  await _preflightDoctor(exitFn);
185
- // 0. Load secrets from .env
186
- const envPath = safePath(safePath(os.homedir(), ".comis"), ".env");
187
- loadEnvFile(envPath);
188
- // 0.5. Decrypt secrets, merge with env, scrub process.env
185
+ // 0. Resolve data directory, then load secrets from <dataDir>/.env.
186
+ // The env file always lives alongside the data dir, so it follows
187
+ // COMIS_DATA_DIR — set to /data inside the Docker container (matches
188
+ // the compose mount of ${COMIS_ENV_FILE:-~/.comis/.env}:/data/.env:ro),
189
+ // unset on bare-metal so it falls back to ~/.comis/.env. This is what
190
+ // makes the legacy "credentials in a flat .env file" workflow the
191
+ // default for both deployment modes; secrets.db is opt-in via
192
+ // SECRETS_MASTER_KEY.
189
193
  // eslint-disable-next-line no-restricted-syntax -- process.env access needed before SecretManager is initialized
190
194
  const dataDir = process.env["COMIS_DATA_DIR"]
191
195
  ?? safePath(os.homedir(), ".comis");
196
+ const envPath = safePath(dataDir, ".env");
197
+ loadEnvFile(envPath);
198
+ // 0.5. Decrypt secrets, merge with env, scrub process.env
192
199
  // Scan and correct permissions on known sensitive files
193
200
  const permissionCorrections = hardenDataDirPermissions(dataDir);
194
201
  const secretsBootResult = _setupSecrets({
@@ -14,6 +14,28 @@ import type { MemoryApi, SqliteMemoryAdapter, createEmbeddingQueue, createSessio
14
14
  import type { RpcCall } from "@comis/skills";
15
15
  import { createGatewayServer, WsConnectionManager, type GatewayServerHandle } from "@comis/gateway";
16
16
  import type { RpcDispatchDeps } from "../rpc/rpc-dispatch.js";
17
+ /**
18
+ * Build the structured log fields for the gateway "Agent execution requested"
19
+ * INFO line. Replaces the previous behavior of logging the first 200 chars
20
+ * of the raw user message, which violated AGENTS.md §2.2 (no message bodies
21
+ * in logs at any level). Emits message length plus a short SHA-256 prefix
22
+ * for correlation, never the body itself.
23
+ *
24
+ * @param input.agentId Resolved agent ID (already trust-derived).
25
+ * @param input.message Raw user message (may be empty / undefined).
26
+ * @param input.connectionId Optional WebSocket connection ID.
27
+ * @returns Object suitable for `logger.info(obj, "Agent execution requested")`.
28
+ */
29
+ export declare function buildExecutionRequestedLogFields(input: {
30
+ agentId: string;
31
+ message: string | undefined;
32
+ connectionId: string | undefined;
33
+ }): {
34
+ agentId: string;
35
+ messageLen: number;
36
+ messageHash?: string;
37
+ connectionId?: string;
38
+ };
17
39
  /** All services produced by the RPC bridge setup phase. */
18
40
  export interface RpcBridgeResult {
19
41
  /** The rpcCall function usable immediately (delegates to inner dispatch once wired). */
@@ -15,10 +15,39 @@ import { suppressError } from "@comis/shared";
15
15
  import { readFileSync, existsSync } from "node:fs";
16
16
  import { parseSlashCommand, createCommandHandler, createGreetingGenerator, } from "@comis/agent";
17
17
  import { createDynamicMethodRouter, createRpcAdapters, createTokenStore, WsConnectionManager, } from "@comis/gateway";
18
- import { randomUUID } from "node:crypto";
18
+ import { createHash, randomUUID } from "node:crypto";
19
19
  import { dirname, join, resolve } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { createRpcDispatch, classifyRpcError } from "../rpc/rpc-dispatch.js";
22
+ // ===========================================================================
23
+ // Execution-request log redaction helper
24
+ // ===========================================================================
25
+ /**
26
+ * Build the structured log fields for the gateway "Agent execution requested"
27
+ * INFO line. Replaces the previous behavior of logging the first 200 chars
28
+ * of the raw user message, which violated AGENTS.md §2.2 (no message bodies
29
+ * in logs at any level). Emits message length plus a short SHA-256 prefix
30
+ * for correlation, never the body itself.
31
+ *
32
+ * @param input.agentId Resolved agent ID (already trust-derived).
33
+ * @param input.message Raw user message (may be empty / undefined).
34
+ * @param input.connectionId Optional WebSocket connection ID.
35
+ * @returns Object suitable for `logger.info(obj, "Agent execution requested")`.
36
+ */
37
+ export function buildExecutionRequestedLogFields(input) {
38
+ const raw = input.message ?? "";
39
+ const fields = {
40
+ agentId: input.agentId,
41
+ messageLen: raw.length,
42
+ };
43
+ if (raw.length > 0) {
44
+ fields.messageHash = createHash("sha256").update(raw).digest("hex").slice(0, 12);
45
+ }
46
+ if (input.connectionId !== undefined) {
47
+ fields.connectionId = input.connectionId;
48
+ }
49
+ return fields;
50
+ }
22
51
  /**
23
52
  * Create the rpcCall wrapper and deferred dispatch mechanism.
24
53
  * The returned rpcCall can be passed to setupTools immediately. After
@@ -296,14 +325,11 @@ export async function setupGateway(deps) {
296
325
  // Admin scope or wildcard -> admin trust; otherwise -> user trust (fail-closed).
297
326
  const trustLevel = deriveTrustLevel(params.scopes);
298
327
  gatewayLogger.debug({ scopes: params.scopes, trustLevel, agentId: execAgentId }, "Trust level derived from token scopes");
299
- const rawMsg = params.message ?? "";
300
- const truncated = rawMsg.length > 200;
301
- gatewayLogger.info({
328
+ gatewayLogger.info(buildExecutionRequestedLogFields({
302
329
  agentId: execAgentId,
303
- message: rawMsg.slice(0, 200),
304
- ...(truncated && { messageTruncated: true }),
305
- ...(connectionId && { connectionId }),
306
- }, "Agent execution requested");
330
+ message: params.message,
331
+ connectionId,
332
+ }), "Agent execution requested");
307
333
  // Link understanding preprocessing: enrich message text with fetched URL content
308
334
  const enrichedText = await preprocessMessageText(params.message);
309
335
  const msg = {
@@ -28,6 +28,11 @@ export function setupTools(deps) {
28
28
  const { rpcCall, agents, defaultAgentId, workspaceDirs, defaultWorkspaceDir, dataDir, secretManager, platformSecretNames, eventBus, skillsLogger, linkRunner, approvalGate, subprocessEnv, credentialMappingStore, onSuspiciousContent, mcpClientManager, sandboxProvider, sessionTrackerRegistry, } = deps;
29
29
  /** Per-agent ProcessRegistry instances for background process lifecycle management. */
30
30
  const processRegistries = new Map();
31
+ /** Agents we've already logged the no-sandbox WARN for. Per-agent assembly
32
+ * runs on every session/heartbeat/cron tick; without this guard the WARN
33
+ * repeats on every LLM call even though the underlying state is fixed at
34
+ * daemon startup (detectSandboxProvider runs once). */
35
+ const warnedNoSandboxAgents = new Set();
31
36
  function getOrCreateRegistry(agentId) {
32
37
  let registry = processRegistries.get(agentId);
33
38
  if (!registry) {
@@ -238,7 +243,15 @@ export function setupTools(deps) {
238
243
  }
239
244
  : undefined;
240
245
  if (!sandboxCfg && skillsConfig.execSandbox.enabled === "always") {
241
- skillsLogger.warn({ agentId, hint: "Sandbox enabled in config but no provider available -- exec tool will run without OS sandbox", errorKind: "config" }, "Exec tool running without OS sandbox");
246
+ if (warnedNoSandboxAgents.has(agentId)) {
247
+ // Already warned for this agent at WARN level — drop to DEBUG so
248
+ // every per-call assembly doesn't re-log the same fact.
249
+ skillsLogger.debug({ agentId }, "Exec tool running without OS sandbox (already warned at startup; per-call DEBUG)");
250
+ }
251
+ else {
252
+ skillsLogger.warn({ agentId, hint: "Sandbox enabled in config but no provider available -- exec tool will run without OS sandbox", errorKind: "config" }, "Exec tool running without OS sandbox");
253
+ warnedNoSandboxAgents.add(agentId);
254
+ }
242
255
  }
243
256
  // Exec tool -- always instantiated; builtinTools ceiling applied after profile filtering
244
257
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/daemon",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Background daemon and orchestrator for the Comis platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/gateway",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "HTTP, JSON-RPC, and WebSocket gateway for Comis",
@@ -119,10 +119,10 @@ export interface LogFields {
119
119
  closeReason: string;
120
120
  /** Semantic categorization of the WebSocket close code (e.g., "normal", "abnormal", "no-status"). */
121
121
  closeType: string;
122
- /** Whether the logged message text was truncated from the original. */
123
- messageTruncated: boolean;
124
122
  /** Input message character length. */
125
123
  messageLen: number;
124
+ /** First 12 hex chars of SHA-256 of input message; omitted when empty. Stable per content. */
125
+ messageHash: string;
126
126
  /** Output response character length. */
127
127
  responseLen: number;
128
128
  /** Flat input token count for easy aggregation. */
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/infra",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Structured logging infrastructure for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/memory",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "SQLite memory, embeddings, and RAG storage for Comis agents",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/scheduler",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Task scheduling and cron management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/shared",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Shared types and utilities for the Comis platform",
@@ -10,6 +10,7 @@
10
10
  import type { SandboxProvider } from "./types.js";
11
11
  /** Minimal logger interface for sandbox detection. */
12
12
  export interface DetectLogger {
13
+ info(obj: Record<string, unknown>, msg: string): void;
13
14
  warn(obj: Record<string, unknown>, msg: string): void;
14
15
  }
15
16
  /**
@@ -8,8 +8,40 @@
8
8
  *
9
9
  * @module
10
10
  */
11
+ import { existsSync } from "node:fs";
12
+ import { spawnSync } from "node:child_process";
11
13
  import { BwrapProvider } from "./bwrap-provider.js";
12
14
  import { SandboxExecProvider } from "./sandbox-exec-provider.js";
15
+ /**
16
+ * True when the daemon is running inside a Linux container. Docker writes
17
+ * `/.dockerenv` on container creation; Podman writes `/run/.containerenv`.
18
+ * One sync stat per daemon boot — runs once at sandbox detection.
19
+ */
20
+ function isContainer() {
21
+ return existsSync("/.dockerenv") || existsSync("/run/.containerenv");
22
+ }
23
+ /**
24
+ * Smoke-test the bwrap binary against the isolation flags BwrapProvider
25
+ * actually uses (--unshare-pid + --proc /proc). On Docker Desktop's linuxkit
26
+ * kernel and similar restricted environments this combo EPERMs at the
27
+ * procfs mount step, even with apparmor/seccomp unconfined — every later
28
+ * exec call would silently fail. `available()` only checks if `bwrap` is on
29
+ * PATH, so without this probe the daemon would log "provider: bwrap" even
30
+ * when bwrap is non-functional. ~50ms one-shot at startup.
31
+ */
32
+ function bwrapSmokeTest() {
33
+ const r = spawnSync("bwrap", [
34
+ "--unshare-user",
35
+ "--unshare-pid",
36
+ "--proc", "/proc",
37
+ "--ro-bind", "/usr", "/usr",
38
+ "--ro-bind", "/bin", "/bin",
39
+ "--ro-bind", "/lib", "/lib",
40
+ "--tmpfs", "/tmp",
41
+ "/bin/true",
42
+ ], { encoding: "utf8", timeout: 5000 });
43
+ return r.status === 0;
44
+ }
13
45
  /**
14
46
  * Detect and return the best available sandbox provider for this platform.
15
47
  * Returns undefined if no sandbox runtime is available -- caller decides
@@ -18,12 +50,53 @@ import { SandboxExecProvider } from "./sandbox-exec-provider.js";
18
50
  export function detectSandboxProvider(logger) {
19
51
  if (process.platform === "linux") {
20
52
  const bwrap = new BwrapProvider();
21
- if (bwrap.available())
53
+ if (bwrap.available()) {
54
+ if (!bwrapSmokeTest()) {
55
+ // bwrap is on PATH but the kernel rejects the isolation flags
56
+ // (typically Docker Desktop's linuxkit on macOS/Windows). Behaviour
57
+ // diverges by environment:
58
+ //
59
+ // - Inside a container: the project already declares macOS/Windows
60
+ // Docker Desktop as dev/testing only (CLAUDE.md, README, docs).
61
+ // Returning bwrap would just make every exec call fail and
62
+ // leave the agent useless for local testing. We disable the
63
+ // sandbox so exec runs unsandboxed inside the container,
64
+ // accepting the documented trust-boundary trade-off, and warn
65
+ // loudly. /data and /etc/comis are reachable from agent exec
66
+ // in this mode — never use it in production.
67
+ //
68
+ // - Bare metal: a non-functional bwrap is a real misconfiguration
69
+ // (rare on stock Linux). Surface it loudly and return the
70
+ // provider so exec fails via bwrap's stderr until the operator
71
+ // fixes the kernel/userns config — never silently degrade
72
+ // sandboxing on a bare-metal host.
73
+ if (isContainer()) {
74
+ logger?.warn({
75
+ hint: "Kernel rejected --unshare-pid + --proc /proc (typically Docker Desktop linuxkit on macOS/Windows). Sandbox auto-disabled so agent exec is functional for development. PRODUCTION DEPLOYMENTS MUST USE A REAL LINUX HOST — see docs/operations/docker.mdx → Platform Support.",
76
+ errorKind: "config",
77
+ }, "Exec sandbox DISABLED (kernel limitation; container host) -- shell commands will run UNSANDBOXED. Dev/testing only.");
78
+ return undefined;
79
+ }
80
+ logger?.warn({
81
+ hint: "Kernel rejected --unshare-pid + --proc /proc on a bare-metal host. Check `kernel.unprivileged_userns_clone` and AppArmor's `apparmor_restrict_unprivileged_userns`. Exec calls will fail until bwrap can run.",
82
+ errorKind: "config",
83
+ }, "bwrap installed but smoke test failed -- exec sandbox is non-functional on this kernel");
84
+ }
22
85
  return bwrap;
23
- logger?.warn({
24
- hint: "Install bubblewrap for OS-level exec sandboxing: apt install bubblewrap",
25
- errorKind: "config",
26
- }, "bwrap not found -- exec tool will run without OS sandbox");
86
+ }
87
+ if (isContainer()) {
88
+ // Container deployments treat the container itself as the trust boundary;
89
+ // bwrap is intentionally absent. See docs/operations/docker.mdx Trust boundary.
90
+ logger?.info({
91
+ hint: "Container runtime detected; intra-container exec sandboxing is opt-in. To enable, install bubblewrap and run with security_opt: apparmor=unconfined / seccomp=unconfined.",
92
+ }, "Exec OS sandbox not present (container runtime) -- relying on container isolation");
93
+ }
94
+ else {
95
+ logger?.warn({
96
+ hint: "Install bubblewrap for OS-level exec sandboxing: apt install bubblewrap",
97
+ errorKind: "config",
98
+ }, "bwrap not found -- exec tool will run without OS sandbox");
99
+ }
27
100
  return undefined;
28
101
  }
29
102
  if (process.platform === "darwin") {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/skills",
3
3
  "private": true,
4
- "version": "1.0.22",
4
+ "version": "1.0.23",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Skill system, MCP integration, and tool sandbox for Comis agents",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comis/web",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "description": "Web dashboard SPA for Comis agent management",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comisai",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "author": "Moshe Anconina",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Security-first AI agent platform — connects AI agents to Discord, Telegram, Slack, WhatsApp, and more",
@@ -111,18 +111,18 @@
111
111
  "@comis/web"
112
112
  ],
113
113
  "dependencies": {
114
- "@comis/shared": "1.0.22",
115
- "@comis/core": "1.0.22",
116
- "@comis/infra": "1.0.22",
117
- "@comis/memory": "1.0.22",
118
- "@comis/gateway": "1.0.22",
119
- "@comis/skills": "1.0.22",
120
- "@comis/scheduler": "1.0.22",
121
- "@comis/agent": "1.0.22",
122
- "@comis/channels": "1.0.22",
123
- "@comis/cli": "1.0.22",
124
- "@comis/daemon": "1.0.22",
125
- "@comis/web": "1.0.22",
114
+ "@comis/shared": "1.0.23",
115
+ "@comis/core": "1.0.23",
116
+ "@comis/infra": "1.0.23",
117
+ "@comis/memory": "1.0.23",
118
+ "@comis/gateway": "1.0.23",
119
+ "@comis/skills": "1.0.23",
120
+ "@comis/scheduler": "1.0.23",
121
+ "@comis/agent": "1.0.23",
122
+ "@comis/channels": "1.0.23",
123
+ "@comis/cli": "1.0.23",
124
+ "@comis/daemon": "1.0.23",
125
+ "@comis/web": "1.0.23",
126
126
  "@agentclientprotocol/sdk": "^0.19.0",
127
127
  "@clack/core": "^1.1.0",
128
128
  "@clack/prompts": "^1.1.0",