comisai 1.0.22 → 1.0.24
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/node_modules/@comis/agent/dist/executor/pi-executor.js +47 -3
- package/node_modules/@comis/agent/dist/index.d.ts +2 -1
- package/node_modules/@comis/agent/dist/index.js +1 -1
- package/node_modules/@comis/agent/dist/model/auth-storage-adapter.d.ts +21 -0
- package/node_modules/@comis/agent/dist/model/auth-storage-adapter.js +15 -1
- package/node_modules/@comis/agent/dist/model/model-registry-adapter.d.ts +46 -0
- package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +108 -0
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/dist/daemon.js +11 -4
- package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +15 -3
- package/node_modules/@comis/daemon/dist/wiring/setup-gateway.d.ts +22 -0
- package/node_modules/@comis/daemon/dist/wiring/setup-gateway.js +34 -8
- package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +14 -1
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/dist/logging/log-fields.d.ts +2 -2
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/dist/builtin/sandbox/detect-provider.d.ts +1 -0
- package/node_modules/@comis/skills/dist/builtin/sandbox/detect-provider.js +78 -5
- package/node_modules/@comis/skills/package.json +1 -1
- package/node_modules/@comis/web/package.json +1 -1
- package/package.json +13 -13
|
@@ -324,6 +324,23 @@ export function createPiExecutor(config, deps) {
|
|
|
324
324
|
if (normalizedPrimary.normalized) {
|
|
325
325
|
deps.logger.debug({ original: config.model, resolved: normalizedPrimary.modelId }, "Model ID normalized via shortcut");
|
|
326
326
|
}
|
|
327
|
+
// Surface the silent-fallback case where pi-coding-agent picks a different
|
|
328
|
+
// provider than the user configured. When find() returns undefined for an
|
|
329
|
+
// explicit (non-default) provider/model, pi will silently shop `findInitialModel`
|
|
330
|
+
// and pick whatever built-in has env-var auth -- e.g., GEMINI_API_KEY → google.
|
|
331
|
+
// The wiring fix in setup-agents.ts should cover the YAML-provider case; this
|
|
332
|
+
// log catches stragglers (typos, disabled providers, missing API keys).
|
|
333
|
+
if (!resolvedModel
|
|
334
|
+
&& config.provider.toLowerCase() !== "default"
|
|
335
|
+
&& config.model.toLowerCase() !== "default") {
|
|
336
|
+
deps.logger.warn({
|
|
337
|
+
agentId,
|
|
338
|
+
configuredProvider: config.provider,
|
|
339
|
+
configuredModel: normalizedPrimary.modelId,
|
|
340
|
+
hint: "Provider not registered in pi ModelRegistry. Check providers.entries.<name> in config.yaml has type/baseUrl/apiKeyName set, the API key resolves via SecretManager, and the provider is enabled. Without a match, pi-coding-agent silently falls back to whatever built-in provider has env-var credentials.",
|
|
341
|
+
errorKind: "config",
|
|
342
|
+
}, "Configured provider/model not found in registry; pi-coding-agent will fall back");
|
|
343
|
+
}
|
|
327
344
|
if (executionOverrides?.model) {
|
|
328
345
|
// Model override format: "provider:modelId" (same as compactionModel pattern)
|
|
329
346
|
const parts = executionOverrides.model.split(":");
|
|
@@ -449,6 +466,24 @@ export function createPiExecutor(config, deps) {
|
|
|
449
466
|
}
|
|
450
467
|
const resourceLoader = new DefaultResourceLoader(resourceLoaderOptions);
|
|
451
468
|
await resourceLoader.reload();
|
|
469
|
+
// The SDK's `tools` is an allowlist of tool *names* (not definitions).
|
|
470
|
+
// An empty array is treated as a non-empty allowlist that allows zero
|
|
471
|
+
// tools, including all customTools — which is why the agent ran
|
|
472
|
+
// tool-less from every entry point (chat API, SSE, Telegram, etc.):
|
|
473
|
+
// every Comis tool was filtered out of the SDK's tool registry, the
|
|
474
|
+
// Anthropic API request went out with `tools: []`, and the model
|
|
475
|
+
// emitted `<tool_call>...</tool_call>` markup as plaintext that
|
|
476
|
+
// Comis's loop never parsed back.
|
|
477
|
+
//
|
|
478
|
+
// Pass our customTool names as the explicit allowlist so:
|
|
479
|
+
// 1. All customTools land in the SDK's tool registry (their names
|
|
480
|
+
// pass `isAllowedTool`).
|
|
481
|
+
// 2. SDK built-ins like `bash` that conflict with Comis's policy
|
|
482
|
+
// controls are filtered out (Comis uses `exec` instead, with
|
|
483
|
+
// its own sandbox/audit hooks).
|
|
484
|
+
// 3. Where names overlap (read/edit/write), Comis's customTools
|
|
485
|
+
// override the SDK built-ins via Map.set() in the registry
|
|
486
|
+
// build (`agent-session.js:1810-1813` in pi-coding-agent@0.68.0).
|
|
452
487
|
const sessionOptions = {
|
|
453
488
|
cwd: deps.workspaceDir,
|
|
454
489
|
authStorage: deps.authStorage,
|
|
@@ -457,7 +492,7 @@ export function createPiExecutor(config, deps) {
|
|
|
457
492
|
sessionManager: sm,
|
|
458
493
|
settingsManager,
|
|
459
494
|
resourceLoader,
|
|
460
|
-
tools:
|
|
495
|
+
tools: mergedCustomTools.map((t) => t.name),
|
|
461
496
|
customTools: mergedCustomTools,
|
|
462
497
|
};
|
|
463
498
|
const { session, modelFallbackMessage } = await createAgentSession(sessionOptions);
|
|
@@ -654,11 +689,20 @@ export function createPiExecutor(config, deps) {
|
|
|
654
689
|
const postActiveNames = session.getActiveToolNames?.() ?? [];
|
|
655
690
|
if (postActiveNames.length < mergedToolNames.length) {
|
|
656
691
|
const rejected = mergedToolNames.filter(n => !postActiveNames.includes(n));
|
|
692
|
+
const allRejected = postActiveNames.length === 0 && rejected.length === mergedToolNames.length;
|
|
657
693
|
deps.logger.warn({
|
|
658
694
|
rejected,
|
|
659
|
-
|
|
695
|
+
rejectedCount: rejected.length,
|
|
696
|
+
registeredCount: mergedToolNames.length,
|
|
697
|
+
postActiveCount: postActiveNames.length,
|
|
698
|
+
allRejected,
|
|
699
|
+
hint: allRejected
|
|
700
|
+
? "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."
|
|
701
|
+
: "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
702
|
errorKind: "validation",
|
|
661
|
-
},
|
|
703
|
+
}, allRejected
|
|
704
|
+
? "SDK rejected ALL tool registrations -- agent will run with no tools"
|
|
705
|
+
: "SDK rejected some tool registrations");
|
|
662
706
|
}
|
|
663
707
|
}
|
|
664
708
|
catch (toolMgmtError) {
|
|
@@ -135,7 +135,8 @@ export { createPiEventBridge } from "./bridge/pi-event-bridge.js";
|
|
|
135
135
|
export type { PiEventBridgeDeps, PiEventBridgeResult } from "./bridge/pi-event-bridge.js";
|
|
136
136
|
export { createAuthStorageAdapter, DEFAULT_PROVIDER_KEYS } from "./model/auth-storage-adapter.js";
|
|
137
137
|
export type { AuthStorageAdapterOptions } from "./model/auth-storage-adapter.js";
|
|
138
|
-
export { createModelRegistryAdapter, resolveInitialModel } from "./model/model-registry-adapter.js";
|
|
138
|
+
export { createModelRegistryAdapter, registerCustomProviders, resolveInitialModel } from "./model/model-registry-adapter.js";
|
|
139
|
+
export type { CustomProviderRegistration, CustomProviderLogger } from "./model/model-registry-adapter.js";
|
|
139
140
|
export { sessionKeyToPath, pathToSessionKey } from "./session/session-key-mapper.js";
|
|
140
141
|
export { detectBrokenFollowThrough, FOLLOW_THROUGH_PATTERNS } from "./safety/response-safety-checks.js";
|
|
141
142
|
export type { FollowThroughResult } from "./safety/response-safety-checks.js";
|
|
@@ -135,7 +135,7 @@ export { createPiEventBridge } from "./bridge/pi-event-bridge.js";
|
|
|
135
135
|
// Auth storage adapter (SecretManager to pi-coding-agent AuthStorage)
|
|
136
136
|
export { createAuthStorageAdapter, DEFAULT_PROVIDER_KEYS } from "./model/auth-storage-adapter.js";
|
|
137
137
|
// Model registry adapter (ModelRegistry creation + initial model resolution)
|
|
138
|
-
export { createModelRegistryAdapter, resolveInitialModel } from "./model/model-registry-adapter.js";
|
|
138
|
+
export { createModelRegistryAdapter, registerCustomProviders, resolveInitialModel } from "./model/model-registry-adapter.js";
|
|
139
139
|
// Session key mapper (SessionKey to/from filesystem path)
|
|
140
140
|
export { sessionKeyToPath, pathToSessionKey } from "./session/session-key-mapper.js";
|
|
141
141
|
// ---------------------------------------------------------------------------
|
|
@@ -10,12 +10,33 @@ import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
|
10
10
|
import type { SecretManager } from "@comis/core";
|
|
11
11
|
/** Default provider-to-env-var mapping for known LLM providers. */
|
|
12
12
|
export declare const DEFAULT_PROVIDER_KEYS: Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Custom YAML provider entry projection used to populate AuthStorage with
|
|
15
|
+
* runtime API keys for providers declared under `providers.entries.*`.
|
|
16
|
+
*
|
|
17
|
+
* Only the fields needed for credential wiring are included -- the full
|
|
18
|
+
* ProviderEntry lives in @comis/core but importing it here would pull
|
|
19
|
+
* the entire config domain into the agent package.
|
|
20
|
+
*/
|
|
21
|
+
export interface CustomProviderAuth {
|
|
22
|
+
/** SecretManager key name for the API key (e.g., "NVIDIA_API_KEY"). */
|
|
23
|
+
apiKeyName: string;
|
|
24
|
+
/** Whether the provider is enabled. Disabled entries are skipped. */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
}
|
|
13
27
|
/** Options for creating an AuthStorage adapter. */
|
|
14
28
|
export interface AuthStorageAdapterOptions {
|
|
15
29
|
/** SecretManager to read API keys from. */
|
|
16
30
|
secretManager: SecretManager;
|
|
17
31
|
/** Additional provider-to-env-var mappings beyond the defaults. */
|
|
18
32
|
additionalProviderKeys?: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Custom YAML provider entries (`providers.entries.*`). Each entry's
|
|
35
|
+
* `apiKeyName` is resolved through `secretManager` and registered as a
|
|
36
|
+
* runtime override on the returned AuthStorage. Disabled entries and
|
|
37
|
+
* entries with empty `apiKeyName` are skipped silently.
|
|
38
|
+
*/
|
|
39
|
+
customProviderEntries?: Record<string, CustomProviderAuth>;
|
|
19
40
|
}
|
|
20
41
|
/**
|
|
21
42
|
* Create an AuthStorage populated with API keys from SecretManager.
|
|
@@ -24,7 +24,7 @@ export const DEFAULT_PROVIDER_KEYS = {
|
|
|
24
24
|
* setRuntimeApiKey() for found keys. Missing keys are silently skipped.
|
|
25
25
|
*/
|
|
26
26
|
export function createAuthStorageAdapter(options) {
|
|
27
|
-
const { secretManager, additionalProviderKeys } = options;
|
|
27
|
+
const { secretManager, additionalProviderKeys, customProviderEntries } = options;
|
|
28
28
|
const storage = AuthStorage.fromStorage(new InMemoryAuthStorageBackend());
|
|
29
29
|
const allProviderKeys = { ...DEFAULT_PROVIDER_KEYS, ...additionalProviderKeys };
|
|
30
30
|
for (const [provider, envKey] of Object.entries(allProviderKeys)) {
|
|
@@ -33,5 +33,19 @@ export function createAuthStorageAdapter(options) {
|
|
|
33
33
|
storage.setRuntimeApiKey(provider, apiKey);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
// Custom YAML providers (providers.entries.*). Runtime overrides take
|
|
37
|
+
// priority over auth.json and env-var fallback in pi-coding-agent, so
|
|
38
|
+
// YAML config wins over any stray env keys (e.g., GEMINI_API_KEY) that
|
|
39
|
+
// might otherwise satisfy hasAuth() for an unrelated built-in provider.
|
|
40
|
+
if (customProviderEntries) {
|
|
41
|
+
for (const [providerName, entry] of Object.entries(customProviderEntries)) {
|
|
42
|
+
if (!entry.enabled || !entry.apiKeyName)
|
|
43
|
+
continue;
|
|
44
|
+
const apiKey = secretManager.get(entry.apiKeyName);
|
|
45
|
+
if (apiKey) {
|
|
46
|
+
storage.setRuntimeApiKey(providerName, apiKey);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
36
50
|
return storage;
|
|
37
51
|
}
|
|
@@ -12,6 +12,7 @@ import { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
|
12
12
|
import type { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
13
13
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
14
14
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
15
|
+
import type { SecretManager } from "@comis/core";
|
|
15
16
|
import type { ModelAllowlist } from "./model-allowlist.js";
|
|
16
17
|
/** Result of initial model resolution. */
|
|
17
18
|
export interface InitialModelResult {
|
|
@@ -30,6 +31,51 @@ export interface InitialModelResult {
|
|
|
30
31
|
* have API keys configured in AuthStorage.
|
|
31
32
|
*/
|
|
32
33
|
export declare function createModelRegistryAdapter(authStorage: AuthStorage): ModelRegistry;
|
|
34
|
+
/** Subset of `ProviderEntry` (from `@comis/core`) we read for pi registration. */
|
|
35
|
+
export interface CustomProviderRegistration {
|
|
36
|
+
type: string;
|
|
37
|
+
baseUrl: string;
|
|
38
|
+
apiKeyName: string;
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
headers: Record<string, string>;
|
|
41
|
+
models: ReadonlyArray<{
|
|
42
|
+
id: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
contextWindow?: number;
|
|
45
|
+
maxTokens?: number;
|
|
46
|
+
reasoning?: boolean;
|
|
47
|
+
input?: ReadonlyArray<"text" | "image">;
|
|
48
|
+
cost?: {
|
|
49
|
+
input?: number;
|
|
50
|
+
output?: number;
|
|
51
|
+
cacheRead?: number;
|
|
52
|
+
cacheWrite?: number;
|
|
53
|
+
};
|
|
54
|
+
}>;
|
|
55
|
+
}
|
|
56
|
+
/** Logger surface accepted by `registerCustomProviders`. Subset of Pino. */
|
|
57
|
+
export interface CustomProviderLogger {
|
|
58
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
59
|
+
debug(obj: Record<string, unknown>, msg: string): void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Register YAML `providers.entries.*` with pi-coding-agent's ModelRegistry.
|
|
63
|
+
*
|
|
64
|
+
* Without this, custom OpenAI-compatible providers (NVIDIA NIM, Together,
|
|
65
|
+
* ollama, etc.) are not findable via `registry.find(provider, modelId)`,
|
|
66
|
+
* which causes pi's `findInitialModel` to silently fall back to whatever
|
|
67
|
+
* built-in provider has env-var auth (e.g., GEMINI_API_KEY → google).
|
|
68
|
+
*
|
|
69
|
+
* Per-entry behavior:
|
|
70
|
+
* - Skipped if `enabled === false`.
|
|
71
|
+
* - Skipped if no models declared and no `baseUrl` override.
|
|
72
|
+
* - On `registerProvider` error (missing baseUrl, missing apiKey, etc.),
|
|
73
|
+
* a WARN is logged and the loop continues -- one bad entry must not
|
|
74
|
+
* prevent the daemon from starting.
|
|
75
|
+
*
|
|
76
|
+
* @returns Number of entries successfully registered.
|
|
77
|
+
*/
|
|
78
|
+
export declare function registerCustomProviders(registry: ModelRegistry, entries: Record<string, CustomProviderRegistration>, secretManager: SecretManager, logger: CustomProviderLogger): number;
|
|
33
79
|
/**
|
|
34
80
|
* Resolve the initial model for an agent session.
|
|
35
81
|
*
|
|
@@ -20,6 +20,114 @@ import { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
|
|
20
20
|
export function createModelRegistryAdapter(authStorage) {
|
|
21
21
|
return ModelRegistry.inMemory(authStorage);
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* YAML provider type → pi-ai API identifier. Mirrors the
|
|
25
|
+
* `OPENAI_COMPATIBLE_TYPES` set in `model-scanner.ts`. Unknown types
|
|
26
|
+
* default to `openai-completions` so arbitrary OpenAI-compatible
|
|
27
|
+
* proxies (NVIDIA NIM, Together, ollama, lm-studio, etc.) work without
|
|
28
|
+
* code changes.
|
|
29
|
+
*/
|
|
30
|
+
const PROVIDER_TYPE_TO_API = {
|
|
31
|
+
openai: "openai-completions",
|
|
32
|
+
groq: "openai-completions",
|
|
33
|
+
mistral: "openai-completions",
|
|
34
|
+
together: "openai-completions",
|
|
35
|
+
deepseek: "openai-completions",
|
|
36
|
+
cerebras: "openai-completions",
|
|
37
|
+
xai: "openai-completions",
|
|
38
|
+
openrouter: "openai-completions",
|
|
39
|
+
anthropic: "anthropic-messages",
|
|
40
|
+
google: "google-generative-ai",
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Register YAML `providers.entries.*` with pi-coding-agent's ModelRegistry.
|
|
44
|
+
*
|
|
45
|
+
* Without this, custom OpenAI-compatible providers (NVIDIA NIM, Together,
|
|
46
|
+
* ollama, etc.) are not findable via `registry.find(provider, modelId)`,
|
|
47
|
+
* which causes pi's `findInitialModel` to silently fall back to whatever
|
|
48
|
+
* built-in provider has env-var auth (e.g., GEMINI_API_KEY → google).
|
|
49
|
+
*
|
|
50
|
+
* Per-entry behavior:
|
|
51
|
+
* - Skipped if `enabled === false`.
|
|
52
|
+
* - Skipped if no models declared and no `baseUrl` override.
|
|
53
|
+
* - On `registerProvider` error (missing baseUrl, missing apiKey, etc.),
|
|
54
|
+
* a WARN is logged and the loop continues -- one bad entry must not
|
|
55
|
+
* prevent the daemon from starting.
|
|
56
|
+
*
|
|
57
|
+
* @returns Number of entries successfully registered.
|
|
58
|
+
*/
|
|
59
|
+
export function registerCustomProviders(registry, entries, secretManager, logger) {
|
|
60
|
+
let registered = 0;
|
|
61
|
+
for (const [providerName, entry] of Object.entries(entries)) {
|
|
62
|
+
if (!entry.enabled) {
|
|
63
|
+
logger.debug({ providerName }, "Custom provider skipped (disabled)");
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const hasModels = entry.models.length > 0;
|
|
67
|
+
const hasBaseUrlOverride = !!entry.baseUrl;
|
|
68
|
+
if (!hasModels && !hasBaseUrlOverride) {
|
|
69
|
+
logger.debug({ providerName }, "Custom provider skipped (no models and no baseUrl override)");
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const apiKey = entry.apiKeyName ? secretManager.get(entry.apiKeyName) : undefined;
|
|
73
|
+
if (hasModels && !apiKey) {
|
|
74
|
+
logger.warn({
|
|
75
|
+
providerName,
|
|
76
|
+
apiKeyName: entry.apiKeyName,
|
|
77
|
+
hint: "Set the named secret in ~/.comis/.env or remove the provider entry from config.yaml",
|
|
78
|
+
errorKind: "config",
|
|
79
|
+
}, "Custom provider has models but no API key -- skipping registration");
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const api = PROVIDER_TYPE_TO_API[entry.type] ?? "openai-completions";
|
|
83
|
+
const headersResolved = Object.keys(entry.headers).length > 0 ? entry.headers : undefined;
|
|
84
|
+
try {
|
|
85
|
+
registry.registerProvider(providerName, {
|
|
86
|
+
api,
|
|
87
|
+
baseUrl: entry.baseUrl || undefined,
|
|
88
|
+
apiKey,
|
|
89
|
+
headers: headersResolved,
|
|
90
|
+
// pi's ProviderModelConfig requires concrete values for name/cost/
|
|
91
|
+
// contextWindow/maxTokens. Comis's UserModelSchema lets users omit
|
|
92
|
+
// these (defaults to optional/undefined), so we fill in zeros and
|
|
93
|
+
// a generous default context window. Cost is informational only
|
|
94
|
+
// and our CostTracker uses pi-ai's own catalog where it can.
|
|
95
|
+
models: hasModels
|
|
96
|
+
? entry.models.map((m) => ({
|
|
97
|
+
id: m.id,
|
|
98
|
+
name: m.name ?? m.id,
|
|
99
|
+
contextWindow: m.contextWindow ?? 128_000,
|
|
100
|
+
maxTokens: m.maxTokens ?? 4_096,
|
|
101
|
+
reasoning: m.reasoning ?? false,
|
|
102
|
+
input: m.input ? [...m.input] : ["text"],
|
|
103
|
+
cost: {
|
|
104
|
+
input: m.cost?.input ?? 0,
|
|
105
|
+
output: m.cost?.output ?? 0,
|
|
106
|
+
cacheRead: m.cost?.cacheRead ?? 0,
|
|
107
|
+
cacheWrite: m.cost?.cacheWrite ?? 0,
|
|
108
|
+
},
|
|
109
|
+
}))
|
|
110
|
+
: undefined,
|
|
111
|
+
});
|
|
112
|
+
registered += 1;
|
|
113
|
+
logger.debug({
|
|
114
|
+
providerName,
|
|
115
|
+
api,
|
|
116
|
+
baseUrl: entry.baseUrl,
|
|
117
|
+
modelCount: entry.models.length,
|
|
118
|
+
}, "Custom provider registered with pi ModelRegistry");
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
logger.warn({
|
|
122
|
+
providerName,
|
|
123
|
+
err: error instanceof Error ? error.message : String(error),
|
|
124
|
+
hint: "Check providers.entries config: baseUrl required when defining models; apiKey required unless oauth configured",
|
|
125
|
+
errorKind: "config",
|
|
126
|
+
}, "Custom provider registration failed");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return registered;
|
|
130
|
+
}
|
|
23
131
|
/**
|
|
24
132
|
* Resolve the initial model for an agent session.
|
|
25
133
|
*
|
|
@@ -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.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
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({
|
|
@@ -12,7 +12,7 @@ import { createHmac } from "node:crypto";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { existsSync, mkdirSync } from "node:fs";
|
|
14
14
|
import { isAbsolute, resolve } from "node:path";
|
|
15
|
-
import { createCircuitBreaker, createBudgetGuard, createCostTracker, createStepCounter, createSessionLifecycle, ensureWorkspace, resolveWorkspaceDir, createPiExecutor, createComisSessionManager, cleanupStaleLocks, createAuthStorageAdapter, createModelRegistryAdapter, createProviderHealthMonitor, setSanitizeLogger, setToolNormalizationLogger, LEAN_TOOL_DESCRIPTIONS, resolveDescription, } from "@comis/agent";
|
|
15
|
+
import { createCircuitBreaker, createBudgetGuard, createCostTracker, createStepCounter, createSessionLifecycle, ensureWorkspace, resolveWorkspaceDir, createPiExecutor, createComisSessionManager, cleanupStaleLocks, createAuthStorageAdapter, createModelRegistryAdapter, registerCustomProviders, createProviderHealthMonitor, setSanitizeLogger, setToolNormalizationLogger, LEAN_TOOL_DESCRIPTIONS, resolveDescription, } from "@comis/agent";
|
|
16
16
|
import { agentToolsToToolDefinitions, createSkillRegistry, createRuntimeEligibilityContext, TOOL_PROFILES, } from "@comis/skills";
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Single-agent setup (extracted for hot-add reuse)
|
|
@@ -60,9 +60,21 @@ export async function setupSingleAgent(agentId, rawAgentConfig, deps) {
|
|
|
60
60
|
eventBus: container.eventBus,
|
|
61
61
|
});
|
|
62
62
|
agentLogger.debug({ agentId, allowPatterns: agentSecrets.allow }, "Per-agent ScopedSecretManager created");
|
|
63
|
-
// Per-agent auth + model registry (moved from shared to per-agent for credential isolation)
|
|
64
|
-
|
|
63
|
+
// Per-agent auth + model registry (moved from shared to per-agent for credential isolation).
|
|
64
|
+
// Custom YAML providers under `providers.entries.*` are wired into both auth (runtime API
|
|
65
|
+
// key overrides) and the registry (so `find(provider, modelId)` succeeds) -- without this,
|
|
66
|
+
// pi-coding-agent silently falls back to whatever built-in provider has env-var auth (e.g.,
|
|
67
|
+
// GEMINI_API_KEY → google), bypassing the configured provider entirely.
|
|
68
|
+
const customProviderEntries = container.config.providers?.entries ?? {};
|
|
69
|
+
const piAuthStorage = createAuthStorageAdapter({
|
|
70
|
+
secretManager: scopedManager,
|
|
71
|
+
customProviderEntries,
|
|
72
|
+
});
|
|
65
73
|
const piModelRegistry = createModelRegistryAdapter(piAuthStorage);
|
|
74
|
+
const customProviderCount = registerCustomProviders(piModelRegistry, customProviderEntries, scopedManager, agentLogger);
|
|
75
|
+
if (customProviderCount > 0) {
|
|
76
|
+
agentLogger.debug({ agentId, customProviderCount }, "Custom YAML providers registered with pi ModelRegistry");
|
|
77
|
+
}
|
|
66
78
|
// Create JSONL session adapter for this agent
|
|
67
79
|
const lockDir = safePath(dir, ".locks");
|
|
68
80
|
const sessionAdapter = createComisSessionManager({
|
|
@@ -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
|
-
|
|
300
|
-
const truncated = rawMsg.length > 200;
|
|
301
|
-
gatewayLogger.info({
|
|
328
|
+
gatewayLogger.info(buildExecutionRequestedLogFields({
|
|
302
329
|
agentId: execAgentId,
|
|
303
|
-
message:
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
{
|
|
@@ -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. */
|
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comisai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
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.
|
|
115
|
-
"@comis/core": "1.0.
|
|
116
|
-
"@comis/infra": "1.0.
|
|
117
|
-
"@comis/memory": "1.0.
|
|
118
|
-
"@comis/gateway": "1.0.
|
|
119
|
-
"@comis/skills": "1.0.
|
|
120
|
-
"@comis/scheduler": "1.0.
|
|
121
|
-
"@comis/agent": "1.0.
|
|
122
|
-
"@comis/channels": "1.0.
|
|
123
|
-
"@comis/cli": "1.0.
|
|
124
|
-
"@comis/daemon": "1.0.
|
|
125
|
-
"@comis/web": "1.0.
|
|
114
|
+
"@comis/shared": "1.0.24",
|
|
115
|
+
"@comis/core": "1.0.24",
|
|
116
|
+
"@comis/infra": "1.0.24",
|
|
117
|
+
"@comis/memory": "1.0.24",
|
|
118
|
+
"@comis/gateway": "1.0.24",
|
|
119
|
+
"@comis/skills": "1.0.24",
|
|
120
|
+
"@comis/scheduler": "1.0.24",
|
|
121
|
+
"@comis/agent": "1.0.24",
|
|
122
|
+
"@comis/channels": "1.0.24",
|
|
123
|
+
"@comis/cli": "1.0.24",
|
|
124
|
+
"@comis/daemon": "1.0.24",
|
|
125
|
+
"@comis/web": "1.0.24",
|
|
126
126
|
"@agentclientprotocol/sdk": "^0.19.0",
|
|
127
127
|
"@clack/core": "^1.1.0",
|
|
128
128
|
"@clack/prompts": "^1.1.0",
|