@ch4p/cli 0.1.3 → 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/dist/agent-6WIHK7NM.js +767 -0
- package/dist/agent-ANIZYPPF.js +767 -0
- package/dist/agent-HSAJ5EBN.js +761 -0
- package/dist/audit-HLOQBMBT.js +12 -0
- package/dist/audit-UIGPH3FK.js +12 -0
- package/dist/canvas-3VTC4XPV.js +313 -0
- package/dist/canvas-4FMNW6FZ.js +313 -0
- package/dist/canvas-XQHVCY27.js +313 -0
- package/dist/chunk-3XAW4XHG.js +185 -0
- package/dist/chunk-4IRZQCRN.js +1832 -0
- package/dist/chunk-AORLXQHZ.js +304 -0
- package/dist/chunk-BMEBRUYL.js +6995 -0
- package/dist/chunk-IN2I6XRM.js +185 -0
- package/dist/chunk-TB4IZ7F7.js +301 -0
- package/dist/chunk-U7S375OS.js +1841 -0
- package/dist/dist-37TB6EWP.js +25 -0
- package/dist/dist-CIJPZC2B.js +25 -0
- package/dist/doctor-5M3ZB435.js +274 -0
- package/dist/doctor-IQ3MWQSN.js +274 -0
- package/dist/gateway-DV5OL45G.js +2164 -0
- package/dist/gateway-LUCG72YX.js +2129 -0
- package/dist/gateway-O3QNSZKF.js +2123 -0
- package/dist/gateway-OJW7RY3H.js +2094 -0
- package/dist/gateway-PBLJEK5I.js +2165 -0
- package/dist/gateway-PHPRQTZP.js +2165 -0
- package/dist/gateway-YKKJ4DZE.js +2115 -0
- package/dist/gateway-Z65DCM2Q.js +2097 -0
- package/dist/gateway-ZSXTAYPF.js +2157 -0
- package/dist/identity-RHQFPSDS.js +215 -0
- package/dist/identity-VGDDAKBY.js +215 -0
- package/dist/index.js +12 -12
- package/dist/install-6LV7B2SV.js +378 -0
- package/dist/install-NAUPXVCI.js +378 -0
- package/dist/message-TGAPVVI4.js +189 -0
- package/dist/message-YQGIARNE.js +189 -0
- package/dist/onboard-CN56V5P6.js +849 -0
- package/dist/onboard-LJFC6HXD.js +849 -0
- package/dist/pairing-ARWQYATE.js +147 -0
- package/dist/pairing-PXCJMCT2.js +147 -0
- package/dist/skills-4EELFYO2.js +138 -0
- package/dist/skills-KXRTDSF2.js +138 -0
- package/dist/status-2ZJPK3VL.js +94 -0
- package/dist/status-W2OXOSH4.js +94 -0
- package/package.json +24 -24
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanvasSessionManager,
|
|
3
|
+
CanvasTool,
|
|
4
|
+
GatewayServer,
|
|
5
|
+
PairingManager,
|
|
6
|
+
SessionManager
|
|
7
|
+
} from "./chunk-XRUNSIVU.js";
|
|
8
|
+
import {
|
|
9
|
+
DefaultSecurityPolicy,
|
|
10
|
+
NativeEngine,
|
|
11
|
+
ProviderRegistry,
|
|
12
|
+
createClaudeCliEngine,
|
|
13
|
+
createCodexCliEngine,
|
|
14
|
+
createMemoryBackend,
|
|
15
|
+
createObserver
|
|
16
|
+
} from "./chunk-BMEBRUYL.js";
|
|
17
|
+
import {
|
|
18
|
+
LoadSkillTool,
|
|
19
|
+
ToolRegistry
|
|
20
|
+
} from "./chunk-PGZ24EFT.js";
|
|
21
|
+
import {
|
|
22
|
+
SkillRegistry
|
|
23
|
+
} from "./chunk-6BURGD2Y.js";
|
|
24
|
+
import {
|
|
25
|
+
AgentLoop
|
|
26
|
+
} from "./chunk-4IRZQCRN.js";
|
|
27
|
+
import {
|
|
28
|
+
generateId
|
|
29
|
+
} from "./chunk-YSCX2QQQ.js";
|
|
30
|
+
import {
|
|
31
|
+
getLogsDir,
|
|
32
|
+
loadConfig
|
|
33
|
+
} from "./chunk-NRFRTZVP.js";
|
|
34
|
+
import {
|
|
35
|
+
BOLD,
|
|
36
|
+
DIM,
|
|
37
|
+
GREEN,
|
|
38
|
+
RED,
|
|
39
|
+
RESET,
|
|
40
|
+
TEAL,
|
|
41
|
+
separator
|
|
42
|
+
} from "./chunk-NMGPBPNU.js";
|
|
43
|
+
|
|
44
|
+
// src/commands/canvas.ts
|
|
45
|
+
import { resolve, dirname } from "path";
|
|
46
|
+
import { fileURLToPath } from "url";
|
|
47
|
+
import { execSync } from "child_process";
|
|
48
|
+
async function canvas(args) {
|
|
49
|
+
let config;
|
|
50
|
+
try {
|
|
51
|
+
config = loadConfig();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
54
|
+
console.error(`
|
|
55
|
+
${RED}Failed to load config:${RESET} ${message}`);
|
|
56
|
+
console.error(` ${DIM}Run ${TEAL}ch4p onboard${DIM} to set up ch4p.${RESET}
|
|
57
|
+
`);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let port = config.canvas?.port ?? config.gateway.port ?? 4800;
|
|
62
|
+
let autoOpen = true;
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
65
|
+
const parsed = parseInt(args[i + 1], 10);
|
|
66
|
+
if (!isNaN(parsed) && parsed > 0 && parsed <= 65535) {
|
|
67
|
+
port = parsed;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (args[i] === "--no-open") {
|
|
71
|
+
autoOpen = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const host = "127.0.0.1";
|
|
75
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
const staticDir = resolve(__dirname, "..", "..", "..", "apps", "web", "dist");
|
|
77
|
+
const sessionManager = new SessionManager();
|
|
78
|
+
const pairingManager = config.canvas?.requirePairing ? new PairingManager() : void 0;
|
|
79
|
+
const canvasSessionManager = new CanvasSessionManager();
|
|
80
|
+
const engine = createCanvasEngine(config);
|
|
81
|
+
if (!engine) {
|
|
82
|
+
console.error(`
|
|
83
|
+
${RED}No LLM engine available.${RESET}`);
|
|
84
|
+
console.error(` ${DIM}Ensure an API key is configured. Run ${TEAL}ch4p onboard${DIM}.${RESET}
|
|
85
|
+
`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const obsCfg = {
|
|
90
|
+
observers: config.observability.observers ?? ["console"],
|
|
91
|
+
logLevel: config.observability.logLevel ?? "info",
|
|
92
|
+
logPath: `${getLogsDir()}/canvas.jsonl`
|
|
93
|
+
};
|
|
94
|
+
const observer = createObserver(obsCfg);
|
|
95
|
+
let memoryBackend;
|
|
96
|
+
try {
|
|
97
|
+
const memCfg = {
|
|
98
|
+
backend: config.memory.backend,
|
|
99
|
+
vectorWeight: config.memory.vectorWeight,
|
|
100
|
+
keywordWeight: config.memory.keywordWeight,
|
|
101
|
+
embeddingProvider: config.memory.embeddingProvider,
|
|
102
|
+
openaiApiKey: config.providers?.openai?.apiKey || void 0
|
|
103
|
+
};
|
|
104
|
+
memoryBackend = createMemoryBackend(memCfg);
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
let skillRegistry;
|
|
108
|
+
try {
|
|
109
|
+
if (config.skills?.enabled && config.skills?.paths?.length) {
|
|
110
|
+
skillRegistry = SkillRegistry.createFromPaths(config.skills.paths);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
const sessionId = generateId(16);
|
|
115
|
+
const defaultSessionConfig = {
|
|
116
|
+
engineId: config.engines?.default ?? "native",
|
|
117
|
+
model: config.agent.model,
|
|
118
|
+
provider: config.agent.provider,
|
|
119
|
+
systemPrompt: "You are ch4p, an AI assistant with an interactive canvas workspace. You can render visual components on the canvas using the canvas_render tool. Available component types: card, chart, form, button, text_field, data_table, code_block, markdown, image, progress, status. Components are placed at (x, y) positions on a spatial canvas. You can connect components with directional edges to show relationships. Use the canvas to create rich, visual responses when appropriate."
|
|
120
|
+
};
|
|
121
|
+
const server = new GatewayServer({
|
|
122
|
+
port,
|
|
123
|
+
host,
|
|
124
|
+
sessionManager,
|
|
125
|
+
pairingManager,
|
|
126
|
+
canvasSessionManager,
|
|
127
|
+
staticDir,
|
|
128
|
+
defaultSessionConfig,
|
|
129
|
+
onCanvasConnection: (connSessionId, bridge) => {
|
|
130
|
+
wireCanvasSession(
|
|
131
|
+
connSessionId,
|
|
132
|
+
bridge,
|
|
133
|
+
canvasSessionManager,
|
|
134
|
+
engine,
|
|
135
|
+
config,
|
|
136
|
+
observer,
|
|
137
|
+
memoryBackend,
|
|
138
|
+
skillRegistry,
|
|
139
|
+
defaultSessionConfig
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
console.log(`
|
|
144
|
+
${TEAL}${BOLD}ch4p Canvas${RESET}`);
|
|
145
|
+
console.log(separator());
|
|
146
|
+
console.log("");
|
|
147
|
+
try {
|
|
148
|
+
await server.start();
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
151
|
+
console.error(` ${RED}Failed to start server:${RESET} ${message}`);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const addr = server.getAddress();
|
|
156
|
+
const bindDisplay = addr ? `${addr.host}:${addr.port}` : `${host}:${port}`;
|
|
157
|
+
const url = `http://${bindDisplay}/?session=${sessionId}`;
|
|
158
|
+
console.log(` ${GREEN}${BOLD}Server listening${RESET} on ${bindDisplay}`);
|
|
159
|
+
console.log(` ${BOLD}Session${RESET} ${sessionId}`);
|
|
160
|
+
console.log(` ${BOLD}Engine${RESET} ${engine.name}`);
|
|
161
|
+
console.log(` ${BOLD}Static dir${RESET} ${DIM}${staticDir}${RESET}`);
|
|
162
|
+
console.log("");
|
|
163
|
+
console.log(` ${BOLD}Routes:${RESET}`);
|
|
164
|
+
console.log(` ${DIM} WS /ws/:sessionId - WebSocket canvas connection${RESET}`);
|
|
165
|
+
console.log(` ${DIM} GET /health - liveness probe${RESET}`);
|
|
166
|
+
console.log(` ${DIM} GET /* - static files (web UI)${RESET}`);
|
|
167
|
+
console.log("");
|
|
168
|
+
if (pairingManager) {
|
|
169
|
+
const code = pairingManager.generateCode("Canvas startup");
|
|
170
|
+
console.log(` ${BOLD}Pairing code:${RESET} ${TEAL}${BOLD}${code.code}${RESET}`);
|
|
171
|
+
console.log(` ${DIM}Add ?token=YOUR_TOKEN to the URL after pairing.${RESET}`);
|
|
172
|
+
console.log("");
|
|
173
|
+
}
|
|
174
|
+
console.log(` ${GREEN}${BOLD}Canvas URL:${RESET} ${TEAL}${url}${RESET}`);
|
|
175
|
+
console.log("");
|
|
176
|
+
if (autoOpen) {
|
|
177
|
+
try {
|
|
178
|
+
const platform = process.platform;
|
|
179
|
+
const openCmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
180
|
+
execSync(`${openCmd} "${url}"`, { stdio: "ignore" });
|
|
181
|
+
console.log(` ${DIM}Browser opened.${RESET}`);
|
|
182
|
+
} catch {
|
|
183
|
+
console.log(` ${DIM}Couldn't auto-open browser. Open the URL above manually.${RESET}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
console.log(` ${DIM}Press Ctrl+C to stop.${RESET}
|
|
187
|
+
`);
|
|
188
|
+
await new Promise((resolvePromise) => {
|
|
189
|
+
const shutdown = async () => {
|
|
190
|
+
console.log(`
|
|
191
|
+
${DIM}Shutting down canvas...${RESET}`);
|
|
192
|
+
canvasSessionManager.endAll();
|
|
193
|
+
if (memoryBackend) {
|
|
194
|
+
try {
|
|
195
|
+
await memoryBackend.close();
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await server.stop();
|
|
200
|
+
await observer.flush?.();
|
|
201
|
+
console.log(` ${DIM}Goodbye!${RESET}
|
|
202
|
+
`);
|
|
203
|
+
resolvePromise();
|
|
204
|
+
};
|
|
205
|
+
process.on("SIGINT", () => void shutdown());
|
|
206
|
+
process.on("SIGTERM", () => void shutdown());
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function createCanvasEngine(config) {
|
|
210
|
+
const engineId = config.engines?.default ?? "native";
|
|
211
|
+
const engineConfig = config.engines?.available?.[engineId];
|
|
212
|
+
if (engineId === "claude-cli") {
|
|
213
|
+
try {
|
|
214
|
+
return createClaudeCliEngine({
|
|
215
|
+
command: engineConfig?.command ?? void 0,
|
|
216
|
+
cwd: engineConfig?.cwd ?? void 0,
|
|
217
|
+
timeout: engineConfig?.timeout ?? void 0
|
|
218
|
+
});
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (engineId === "codex-cli") {
|
|
223
|
+
try {
|
|
224
|
+
return createCodexCliEngine({
|
|
225
|
+
command: engineConfig?.command ?? void 0,
|
|
226
|
+
cwd: engineConfig?.cwd ?? void 0,
|
|
227
|
+
timeout: engineConfig?.timeout ?? void 0
|
|
228
|
+
});
|
|
229
|
+
} catch {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const providerName = config.agent.provider;
|
|
233
|
+
const providerConfig = config.providers?.[providerName];
|
|
234
|
+
const apiKey = providerConfig?.apiKey;
|
|
235
|
+
if (providerName !== "ollama" && (!apiKey || apiKey.trim().length === 0)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const provider = ProviderRegistry.createProvider({
|
|
240
|
+
id: providerName,
|
|
241
|
+
type: providerName,
|
|
242
|
+
...providerConfig
|
|
243
|
+
});
|
|
244
|
+
return new NativeEngine({ provider, defaultModel: config.agent.model });
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function wireCanvasSession(sessionId, bridge, canvasSessionManager, engine, config, observer, memoryBackend, skillRegistry, defaultSessionConfig) {
|
|
250
|
+
const entry = canvasSessionManager.getSession(sessionId);
|
|
251
|
+
if (!entry) return;
|
|
252
|
+
const { canvasState, canvasChannel } = entry;
|
|
253
|
+
canvasChannel.start({ sessionId }).catch(() => {
|
|
254
|
+
});
|
|
255
|
+
canvasChannel.onMessage((msg) => {
|
|
256
|
+
void (async () => {
|
|
257
|
+
try {
|
|
258
|
+
if (msg.text.startsWith("[ABORT]")) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const session = new (await import("./dist-CIJPZC2B.js")).Session({
|
|
262
|
+
sessionId,
|
|
263
|
+
...defaultSessionConfig
|
|
264
|
+
});
|
|
265
|
+
const exclude = config.autonomy.level === "readonly" ? ["bash", "file_write", "file_edit", "delegate"] : ["delegate"];
|
|
266
|
+
if (!config.mesh?.enabled) {
|
|
267
|
+
exclude.push("mesh");
|
|
268
|
+
}
|
|
269
|
+
const tools = ToolRegistry.createDefault({ exclude });
|
|
270
|
+
tools.register(new CanvasTool());
|
|
271
|
+
if (skillRegistry && skillRegistry.size > 0) {
|
|
272
|
+
tools.register(new LoadSkillTool(skillRegistry));
|
|
273
|
+
}
|
|
274
|
+
const securityPolicy = new DefaultSecurityPolicy({
|
|
275
|
+
workspace: process.cwd(),
|
|
276
|
+
autonomyLevel: config.autonomy.level,
|
|
277
|
+
allowedCommands: config.autonomy.allowedCommands,
|
|
278
|
+
blockedPaths: config.security.blockedPaths
|
|
279
|
+
});
|
|
280
|
+
const toolContextExtensions = {
|
|
281
|
+
canvasState
|
|
282
|
+
};
|
|
283
|
+
if (config.search?.enabled && config.search.apiKey) {
|
|
284
|
+
toolContextExtensions.searchApiKey = config.search.apiKey;
|
|
285
|
+
toolContextExtensions.searchConfig = {
|
|
286
|
+
maxResults: config.search.maxResults,
|
|
287
|
+
country: config.search.country,
|
|
288
|
+
searchLang: config.search.searchLang
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const loop = new AgentLoop(session, engine, tools.list(), observer, {
|
|
292
|
+
maxIterations: 30,
|
|
293
|
+
maxRetries: 2,
|
|
294
|
+
enableStateSnapshots: true,
|
|
295
|
+
memoryBackend,
|
|
296
|
+
securityPolicy,
|
|
297
|
+
toolContextExtensions
|
|
298
|
+
});
|
|
299
|
+
for await (const event of loop.run(msg.text)) {
|
|
300
|
+
bridge.handleAgentEvent(event);
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
bridge.handleAgentEvent({
|
|
304
|
+
type: "error",
|
|
305
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
export {
|
|
312
|
+
canvas
|
|
313
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig
|
|
3
|
+
} from "./chunk-AORLXQHZ.js";
|
|
4
|
+
import {
|
|
5
|
+
BOLD,
|
|
6
|
+
DIM,
|
|
7
|
+
GREEN,
|
|
8
|
+
RED,
|
|
9
|
+
RESET,
|
|
10
|
+
TEAL,
|
|
11
|
+
YELLOW,
|
|
12
|
+
box
|
|
13
|
+
} from "./chunk-NMGPBPNU.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/audit.ts
|
|
16
|
+
function severityIcon(severity) {
|
|
17
|
+
switch (severity) {
|
|
18
|
+
case "pass":
|
|
19
|
+
return `${GREEN}PASS${RESET}`;
|
|
20
|
+
case "warn":
|
|
21
|
+
return `${YELLOW}WARN${RESET}`;
|
|
22
|
+
case "fail":
|
|
23
|
+
return `${RED}FAIL${RESET}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function severityPrefix(severity) {
|
|
27
|
+
switch (severity) {
|
|
28
|
+
case "pass":
|
|
29
|
+
return `${GREEN}+${RESET}`;
|
|
30
|
+
case "warn":
|
|
31
|
+
return `${YELLOW}~${RESET}`;
|
|
32
|
+
case "fail":
|
|
33
|
+
return `${RED}x${RESET}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function performAudit(config) {
|
|
37
|
+
const results = [];
|
|
38
|
+
let id = 0;
|
|
39
|
+
id++;
|
|
40
|
+
const port = config.gateway?.port ?? 18789;
|
|
41
|
+
const allowPublic = config.gateway?.allowPublicBind ?? false;
|
|
42
|
+
results.push({
|
|
43
|
+
id,
|
|
44
|
+
name: "Gateway binding",
|
|
45
|
+
severity: allowPublic ? "fail" : "pass",
|
|
46
|
+
message: allowPublic ? `Gateway allows public binding (0.0.0.0:${port}). Restrict to loopback.` : `Gateway bound to loopback (127.0.0.1:${port})`
|
|
47
|
+
});
|
|
48
|
+
id++;
|
|
49
|
+
const requirePairing = config.gateway?.requirePairing ?? true;
|
|
50
|
+
results.push({
|
|
51
|
+
id,
|
|
52
|
+
name: "Pairing required",
|
|
53
|
+
severity: requirePairing ? "pass" : "warn",
|
|
54
|
+
message: requirePairing ? "Gateway requires pairing for all connections" : "Pairing is disabled. Any local process can connect."
|
|
55
|
+
});
|
|
56
|
+
id++;
|
|
57
|
+
const workspaceOnly = config.security?.workspaceOnly ?? true;
|
|
58
|
+
results.push({
|
|
59
|
+
id,
|
|
60
|
+
name: "Workspace scoping",
|
|
61
|
+
severity: workspaceOnly ? "pass" : "warn",
|
|
62
|
+
message: workspaceOnly ? "Filesystem access restricted to workspace" : "Workspace scoping disabled. Agent can access files outside workspace."
|
|
63
|
+
});
|
|
64
|
+
id++;
|
|
65
|
+
const blockedPaths = config.security?.blockedPaths ?? [];
|
|
66
|
+
results.push({
|
|
67
|
+
id,
|
|
68
|
+
name: "Blocked paths",
|
|
69
|
+
severity: "pass",
|
|
70
|
+
message: blockedPaths.length > 0 ? `${blockedPaths.length} additional blocked path(s) configured` : "Using default system blocked paths (14 dirs + 4 dotfiles)"
|
|
71
|
+
});
|
|
72
|
+
id++;
|
|
73
|
+
const autonomy = config.autonomy?.level ?? "supervised";
|
|
74
|
+
results.push({
|
|
75
|
+
id,
|
|
76
|
+
name: "Autonomy level",
|
|
77
|
+
severity: autonomy === "full" ? "warn" : "pass",
|
|
78
|
+
message: autonomy === "full" ? "Full autonomy enabled. Agent will not ask for confirmation." : `Autonomy level: ${autonomy}`
|
|
79
|
+
});
|
|
80
|
+
id++;
|
|
81
|
+
const allowedCommands = config.autonomy?.allowedCommands ?? [];
|
|
82
|
+
results.push({
|
|
83
|
+
id,
|
|
84
|
+
name: "Command allowlist",
|
|
85
|
+
severity: allowedCommands.length > 0 ? "pass" : "warn",
|
|
86
|
+
message: allowedCommands.length > 0 ? `${allowedCommands.length} command(s) in allowlist` : "No command allowlist configured. All commands may be executed."
|
|
87
|
+
});
|
|
88
|
+
id++;
|
|
89
|
+
const encryptSecrets = config.secrets?.encrypt ?? true;
|
|
90
|
+
results.push({
|
|
91
|
+
id,
|
|
92
|
+
name: "Secrets encryption",
|
|
93
|
+
severity: encryptSecrets ? "pass" : "fail",
|
|
94
|
+
message: encryptSecrets ? "Secrets are encrypted at rest (AES-256-GCM)" : "Secrets encryption is disabled. Credentials stored in plaintext."
|
|
95
|
+
});
|
|
96
|
+
id++;
|
|
97
|
+
const engineDefault = config.engines?.default ?? "native";
|
|
98
|
+
const usesSubprocessEngine = engineDefault === "claude-cli" || engineDefault === "codex-cli";
|
|
99
|
+
const usesOllama = config.agent?.provider === "ollama";
|
|
100
|
+
if (usesSubprocessEngine) {
|
|
101
|
+
results.push({
|
|
102
|
+
id,
|
|
103
|
+
name: "API keys",
|
|
104
|
+
severity: "pass",
|
|
105
|
+
message: `Using ${engineDefault} engine. Auth handled by CLI tool.`
|
|
106
|
+
});
|
|
107
|
+
} else if (usesOllama) {
|
|
108
|
+
results.push({
|
|
109
|
+
id,
|
|
110
|
+
name: "API keys",
|
|
111
|
+
severity: "pass",
|
|
112
|
+
message: "Using Ollama provider. No API key required (local inference)."
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
const anthropicKey = config.providers?.["anthropic"]?.["apiKey"];
|
|
116
|
+
const openaiKey = config.providers?.["openai"]?.["apiKey"];
|
|
117
|
+
const hasAnthropicKey = typeof anthropicKey === "string" && anthropicKey.length > 0 && !anthropicKey.includes("${");
|
|
118
|
+
const hasOpenaiKey = typeof openaiKey === "string" && openaiKey.length > 0 && !openaiKey.includes("${");
|
|
119
|
+
const hasAnyKey = hasAnthropicKey || hasOpenaiKey;
|
|
120
|
+
results.push({
|
|
121
|
+
id,
|
|
122
|
+
name: "API keys",
|
|
123
|
+
severity: hasAnyKey ? "pass" : "warn",
|
|
124
|
+
message: hasAnyKey ? `API key(s) configured: ${[hasAnthropicKey && "Anthropic", hasOpenaiKey && "OpenAI"].filter(Boolean).join(", ")}` : "No API keys configured. Set keys via onboard or environment variables."
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
id++;
|
|
128
|
+
const tunnelProvider = config.tunnel?.provider ?? "none";
|
|
129
|
+
results.push({
|
|
130
|
+
id,
|
|
131
|
+
name: "Tunnel exposure",
|
|
132
|
+
severity: tunnelProvider === "none" ? "pass" : "warn",
|
|
133
|
+
message: tunnelProvider === "none" ? "No tunnel configured. Gateway is local only." : `Tunnel active via ${tunnelProvider}. Gateway is exposed to the internet.`
|
|
134
|
+
});
|
|
135
|
+
id++;
|
|
136
|
+
const observers = config.observability?.observers ?? [];
|
|
137
|
+
results.push({
|
|
138
|
+
id,
|
|
139
|
+
name: "Observability",
|
|
140
|
+
severity: observers.length > 0 ? "pass" : "warn",
|
|
141
|
+
message: observers.length > 0 ? `Observer(s) active: ${observers.join(", ")}` : "No observers configured. Security events may go unlogged."
|
|
142
|
+
});
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
function runAudit(config) {
|
|
146
|
+
const results = performAudit(config);
|
|
147
|
+
const lines = [];
|
|
148
|
+
for (const r of results) {
|
|
149
|
+
const padId = String(r.id).padStart(2, " ");
|
|
150
|
+
lines.push(`${severityPrefix(r.severity)} ${DIM}${padId}.${RESET} ${r.message}`);
|
|
151
|
+
}
|
|
152
|
+
const passed = results.filter((r) => r.severity === "pass").length;
|
|
153
|
+
const warned = results.filter((r) => r.severity === "warn").length;
|
|
154
|
+
const failed = results.filter((r) => r.severity === "fail").length;
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push(
|
|
157
|
+
`${severityIcon("pass")} ${passed} ${severityIcon("warn")} ${warned} ${severityIcon("fail")} ${failed} ${DIM}(${results.length} checks)${RESET}`
|
|
158
|
+
);
|
|
159
|
+
if (failed > 0) {
|
|
160
|
+
lines.push("");
|
|
161
|
+
lines.push(`${RED}${BOLD}Action required:${RESET} ${failed} check(s) failed. Review your config.`);
|
|
162
|
+
}
|
|
163
|
+
console.log(box("ch4p Security Audit", lines));
|
|
164
|
+
}
|
|
165
|
+
async function audit() {
|
|
166
|
+
try {
|
|
167
|
+
const config = loadConfig();
|
|
168
|
+
console.log("");
|
|
169
|
+
runAudit(config);
|
|
170
|
+
console.log("");
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
173
|
+
console.error(`
|
|
174
|
+
${RED}Failed to load config:${RESET} ${message}`);
|
|
175
|
+
console.error(` ${DIM}Run ${TEAL}ch4p onboard${DIM} to create a config file.${RESET}
|
|
176
|
+
`);
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export {
|
|
182
|
+
performAudit,
|
|
183
|
+
runAudit,
|
|
184
|
+
audit
|
|
185
|
+
};
|