@praesidia/neurogent 0.1.0

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.
@@ -0,0 +1,214 @@
1
+ import {
2
+ executeAgent
3
+ } from "./chunk-ZC65T52L.js";
4
+
5
+ // src/types.ts
6
+ import { z } from "zod";
7
+ import { parse as parseYaml } from "yaml";
8
+ var neuroAgentSchema = z.object({
9
+ name: z.string(),
10
+ version: z.string(),
11
+ description: z.string(),
12
+ author: z.string(),
13
+ license: z.string(),
14
+ capabilities: z.array(z.string()),
15
+ model: z.object({
16
+ provider: z.string(),
17
+ name: z.string(),
18
+ temperature: z.number().optional().default(0.7),
19
+ max_tokens: z.number().optional().default(4096)
20
+ }),
21
+ memory: z.object({
22
+ type: z.enum(["conversation-buffer", "redis", "none"]),
23
+ max_messages: z.number().optional().default(50)
24
+ }).optional().default({ type: "conversation-buffer" }),
25
+ tools: z.array(
26
+ z.object({
27
+ name: z.string(),
28
+ description: z.string(),
29
+ type: z.enum(["inline", "mcp"]).optional().default("inline"),
30
+ mcp_server: z.string().optional()
31
+ })
32
+ ).optional().default([]),
33
+ praesidia: z.object({
34
+ policy: z.string().optional(),
35
+ enforce_on: z.array(z.string()).optional().default([])
36
+ }).optional(),
37
+ sub_agents: z.array(
38
+ z.object({
39
+ name: z.string(),
40
+ description: z.string(),
41
+ endpoint: z.string()
42
+ })
43
+ ).optional().default([]),
44
+ auth: z.object({
45
+ type: z.enum(["bearer", "none", "oauth2"])
46
+ }).optional().default({ type: "none" }),
47
+ endpoint: z.string().optional().default("/a2a")
48
+ });
49
+ function parseAgentConfig(yamlContent) {
50
+ const raw = parseYaml(yamlContent);
51
+ return neuroAgentSchema.parse(raw);
52
+ }
53
+ function compileToAgentCard(config, praesidiaHash) {
54
+ return {
55
+ name: config.name,
56
+ version: config.version,
57
+ description: config.description,
58
+ author: config.author,
59
+ license: config.license,
60
+ capabilities: config.capabilities,
61
+ tools: config.tools.map((t) => ({
62
+ name: t.name,
63
+ description: t.description,
64
+ type: t.type,
65
+ mcp_server: t.mcp_server
66
+ })),
67
+ endpoints: {
68
+ message: `${config.endpoint}/message`,
69
+ tasks: `${config.endpoint}/tasks`
70
+ },
71
+ sub_agents: config.sub_agents.map((sa) => ({
72
+ name: sa.name,
73
+ description: sa.description,
74
+ endpoint: sa.endpoint
75
+ })),
76
+ "x-praesidia-policy-hash": praesidiaHash
77
+ };
78
+ }
79
+
80
+ // src/zeroclaw/runtime.ts
81
+ import { Hono } from "hono";
82
+ import { serve } from "@hono/node-server";
83
+ function createAgentServer(agent, port, globalModel) {
84
+ const app = new Hono();
85
+ const tasks = /* @__PURE__ */ new Map();
86
+ const history = [];
87
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
88
+ let serverClose = null;
89
+ app.get("/health", (c) => {
90
+ return c.json({
91
+ status: "ok",
92
+ agent: agent.name,
93
+ role: agent.role,
94
+ port,
95
+ uptime: Math.floor((Date.now() - new Date(startedAt).getTime()) / 1e3),
96
+ tasks: tasks.size
97
+ });
98
+ });
99
+ app.get("/.well-known/agent.json", (c) => {
100
+ return c.json({
101
+ name: agent.name,
102
+ version: "1.0.0",
103
+ description: agent.role,
104
+ capabilities: agent.expertise,
105
+ endpoints: {
106
+ message: `http://localhost:${port}/webhook`,
107
+ tasks: `http://localhost:${port}/a2a/tasks`
108
+ }
109
+ });
110
+ });
111
+ app.post("/webhook", async (c) => {
112
+ const body = await c.req.json().catch(() => ({}));
113
+ const message = body.message ?? body.text ?? body.prompt ?? "";
114
+ const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
115
+ const stream = body.stream ?? false;
116
+ if (!message) {
117
+ return c.json({ error: "message field required" }, 400);
118
+ }
119
+ const record = {
120
+ taskId,
121
+ state: "WORKING",
122
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
123
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
124
+ };
125
+ tasks.set(taskId, record);
126
+ if (stream) {
127
+ const encoder = new TextEncoder();
128
+ const readable = new ReadableStream({
129
+ async start(controller) {
130
+ try {
131
+ let fullResponse = "";
132
+ for await (const chunk of executeAgent(agent, message, history, globalModel)) {
133
+ fullResponse += chunk;
134
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ chunk })}
135
+
136
+ `));
137
+ }
138
+ history.push({ role: "user", content: message });
139
+ history.push({ role: "assistant", content: fullResponse });
140
+ if (history.length > 20) history.splice(0, history.length - 20);
141
+ record.state = "COMPLETED";
142
+ record.result = fullResponse;
143
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
144
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true, taskId })}
145
+
146
+ `));
147
+ } catch (err) {
148
+ record.state = "FAILED";
149
+ record.error = String(err);
150
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
151
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: record.error })}
152
+
153
+ `));
154
+ }
155
+ controller.close();
156
+ }
157
+ });
158
+ return new Response(readable, {
159
+ headers: {
160
+ "Content-Type": "text/event-stream",
161
+ "Cache-Control": "no-cache",
162
+ "X-Task-Id": taskId
163
+ }
164
+ });
165
+ }
166
+ try {
167
+ let fullResponse = "";
168
+ for await (const chunk of executeAgent(agent, message, history, globalModel)) {
169
+ fullResponse += chunk;
170
+ }
171
+ history.push({ role: "user", content: message });
172
+ history.push({ role: "assistant", content: fullResponse });
173
+ if (history.length > 20) history.splice(0, history.length - 20);
174
+ record.state = "COMPLETED";
175
+ record.result = fullResponse;
176
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
177
+ return c.json({ taskId, state: "COMPLETED", result: fullResponse });
178
+ } catch (err) {
179
+ record.state = "FAILED";
180
+ record.error = String(err);
181
+ record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
182
+ return c.json({ taskId, state: "FAILED", error: record.error }, 500);
183
+ }
184
+ });
185
+ app.get("/a2a/tasks/:taskId", (c) => {
186
+ const taskId = c.req.param("taskId");
187
+ const task = tasks.get(taskId);
188
+ if (!task) return c.json({ error: "Task not found" }, 404);
189
+ return c.json(task);
190
+ });
191
+ app.get("/a2a/tasks", (c) => {
192
+ return c.json([...tasks.values()].slice(-20));
193
+ });
194
+ return {
195
+ async start() {
196
+ return new Promise((resolve) => {
197
+ const server = serve({ fetch: app.fetch, port }, () => {
198
+ resolve({ agent, port, url: `http://localhost:${port}`, startedAt });
199
+ });
200
+ serverClose = () => server.close();
201
+ });
202
+ },
203
+ stop() {
204
+ serverClose?.();
205
+ }
206
+ };
207
+ }
208
+
209
+ export {
210
+ neuroAgentSchema,
211
+ parseAgentConfig,
212
+ compileToAgentCard,
213
+ createAgentServer
214
+ };
@@ -0,0 +1,49 @@
1
+ // src/shell/config/schema.ts
2
+ import { z } from "zod";
3
+ var AgentModelSchema = z.object({
4
+ provider: z.enum(["openai", "anthropic", "ollama"]).default("openai"),
5
+ name: z.string(),
6
+ max_tokens: z.number().int().positive().default(1024),
7
+ temperature: z.number().min(0).max(2).default(0.7),
8
+ base_url: z.string().url().optional()
9
+ });
10
+ var AgentSchema = z.object({
11
+ id: z.string().regex(/^[a-z][a-z0-9_-]*$/, "Agent id must be lowercase alphanumeric with _ or -"),
12
+ name: z.string().min(1),
13
+ role: z.string().min(1),
14
+ emoji: z.string().default("\u{1F916}"),
15
+ color: z.enum([
16
+ "black",
17
+ "red",
18
+ "green",
19
+ "yellow",
20
+ "blue",
21
+ "magenta",
22
+ "cyan",
23
+ "white",
24
+ "blackBright",
25
+ "redBright",
26
+ "greenBright",
27
+ "yellowBright",
28
+ "blueBright",
29
+ "magentaBright",
30
+ "cyanBright",
31
+ "whiteBright",
32
+ "gray",
33
+ "grey"
34
+ ]).default("white"),
35
+ expertise: z.array(z.string().min(1)).min(1, "At least one expertise keyword required"),
36
+ system_prompt: z.string().min(1),
37
+ model: AgentModelSchema.optional()
38
+ });
39
+ var ShellYamlSchema = z.object({
40
+ shell: z.object({
41
+ name: z.string().default("Neuro Shell")
42
+ }).default({ name: "Neuro Shell" }),
43
+ model: AgentModelSchema.optional(),
44
+ agents: z.array(AgentSchema).min(1, "At least one agent required")
45
+ });
46
+
47
+ export {
48
+ ShellYamlSchema
49
+ };
@@ -0,0 +1,255 @@
1
+ // src/zeroclaw/client.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var RUNTIME_FILE = path.join(process.cwd(), ".neurogent", "runtime.json");
5
+ function loadRuntime() {
6
+ try {
7
+ const raw = fs.readFileSync(RUNTIME_FILE, "utf-8");
8
+ return JSON.parse(raw);
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+ function saveRuntime(reg) {
14
+ const dir = path.dirname(RUNTIME_FILE);
15
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
16
+ fs.writeFileSync(RUNTIME_FILE, JSON.stringify(reg, null, 2));
17
+ }
18
+ function clearRuntime() {
19
+ try {
20
+ fs.unlinkSync(RUNTIME_FILE);
21
+ } catch {
22
+ }
23
+ }
24
+ function getAgentUrl(agentId) {
25
+ const runtime = loadRuntime();
26
+ if (!runtime) return null;
27
+ const entry = runtime.agents.find((a) => a.id === agentId);
28
+ return entry?.url ?? null;
29
+ }
30
+ async function pingAgent(url) {
31
+ try {
32
+ const res = await fetch(`${url}/health`, {
33
+ signal: AbortSignal.timeout(3e3)
34
+ });
35
+ if (!res.ok) return { alive: false, error: `HTTP ${res.status}` };
36
+ const data = await res.json();
37
+ return { alive: true, agent: data.agent, uptime: data.uptime };
38
+ } catch (err) {
39
+ return { alive: false, error: err instanceof Error ? err.message : String(err) };
40
+ }
41
+ }
42
+ async function* sendMessage(url, message) {
43
+ try {
44
+ const res = await fetch(`${url}/webhook`, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({ message, stream: true }),
48
+ signal: AbortSignal.timeout(12e4)
49
+ });
50
+ if (!res.ok || !res.body) {
51
+ yield `[ZeroClaw error: HTTP ${res.status}]`;
52
+ return;
53
+ }
54
+ const contentType = res.headers.get("content-type") ?? "";
55
+ if (contentType.includes("text/event-stream")) {
56
+ const reader = res.body.getReader();
57
+ const decoder = new TextDecoder();
58
+ let buffer = "";
59
+ while (true) {
60
+ const { done, value } = await reader.read();
61
+ if (done) break;
62
+ buffer += decoder.decode(value, { stream: true });
63
+ const lines = buffer.split("\n");
64
+ buffer = lines.pop() ?? "";
65
+ for (const line of lines) {
66
+ if (!line.startsWith("data: ")) continue;
67
+ try {
68
+ const payload = JSON.parse(line.slice(6));
69
+ if (payload.error) {
70
+ yield `[Error: ${payload.error}]`;
71
+ return;
72
+ }
73
+ if (payload.chunk) yield payload.chunk;
74
+ if (payload.done) return;
75
+ } catch {
76
+ }
77
+ }
78
+ }
79
+ } else {
80
+ const data = await res.json();
81
+ if (data.error) {
82
+ yield `[Error: ${data.error}]`;
83
+ return;
84
+ }
85
+ if (data.result) yield data.result;
86
+ }
87
+ } catch (err) {
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ if (msg.includes("ECONNREFUSED")) {
90
+ yield `
91
+ [Agent offline \u2014 run: neurogent start]
92
+ `;
93
+ } else {
94
+ yield `
95
+ [ZeroClaw error: ${msg}]
96
+ `;
97
+ }
98
+ }
99
+ }
100
+
101
+ // src/shell/agents/executor.ts
102
+ import { execSync } from "child_process";
103
+ import * as fs2 from "fs";
104
+ import * as path2 from "path";
105
+ import { createOpenAI } from "@ai-sdk/openai";
106
+ import { createAnthropic } from "@ai-sdk/anthropic";
107
+ import { streamText } from "ai";
108
+ function detectGlobalProvider() {
109
+ if (process.env.ANTHROPIC_API_KEY) {
110
+ return { provider: "anthropic", name: "claude-3-5-sonnet-20241022" };
111
+ }
112
+ if (process.env.OPENAI_API_KEY) {
113
+ return { provider: "openai", name: "gpt-4o" };
114
+ }
115
+ return { provider: "ollama", name: "llama3.2" };
116
+ }
117
+ function buildModel(modelConfig) {
118
+ if (modelConfig.provider === "anthropic") {
119
+ const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY ?? "" });
120
+ return anthropic(modelConfig.name);
121
+ }
122
+ if (modelConfig.provider === "ollama") {
123
+ const ollama = createOpenAI({
124
+ apiKey: "ollama",
125
+ baseURL: modelConfig.baseUrl ?? process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1"
126
+ });
127
+ return ollama(modelConfig.name);
128
+ }
129
+ const openaiClient = createOpenAI({ apiKey: process.env.OPENAI_API_KEY ?? "" });
130
+ return openaiClient(modelConfig.name);
131
+ }
132
+ function getEffectiveModel(agent, globalModel) {
133
+ return agent.model ?? globalModel ?? detectGlobalProvider();
134
+ }
135
+ function getGitContext() {
136
+ try {
137
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
138
+ const diff = execSync("git diff --stat HEAD 2>/dev/null || git diff --stat", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
139
+ const log = execSync("git log --oneline -3", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
140
+ if (!branch || branch === "HEAD") return "";
141
+ let ctx = `
142
+
143
+ [Git context \u2014 Branch: ${branch}`;
144
+ if (log) ctx += ` | Recent: ${log.split("\n")[0]}`;
145
+ if (diff) ctx += `]
146
+ Changed files:
147
+ ${diff}`;
148
+ else ctx += "]";
149
+ return ctx;
150
+ } catch {
151
+ return "";
152
+ }
153
+ }
154
+ function resolveFileContext(message) {
155
+ return message.replace(/@file:([^\s]+)/g, (match, filePath) => {
156
+ try {
157
+ const resolved = path2.resolve(process.cwd(), filePath);
158
+ const content = fs2.readFileSync(resolved, "utf-8");
159
+ const ext = path2.extname(filePath).slice(1) || "text";
160
+ const lines = content.split("\n").length;
161
+ return `
162
+
163
+ [File: ${filePath} (${lines} lines)]
164
+ \`\`\`${ext}
165
+ ${content}
166
+ \`\`\``;
167
+ } catch {
168
+ return `[File not found: ${filePath}]`;
169
+ }
170
+ });
171
+ }
172
+ function calculateCost(model, inputTokens, outputTokens) {
173
+ const pricing = {
174
+ "claude-3-5-sonnet": { input: 3, output: 15 },
175
+ "claude-3-5-haiku": { input: 0.8, output: 4 },
176
+ "claude-3-opus": { input: 15, output: 75 },
177
+ "claude-3-haiku": { input: 0.25, output: 1.25 },
178
+ "gpt-4o": { input: 2.5, output: 10 },
179
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
180
+ "gpt-4-turbo": { input: 10, output: 30 }
181
+ };
182
+ const key = Object.keys(pricing).find((k) => model.name.includes(k));
183
+ if (!key) return 0;
184
+ const p = pricing[key];
185
+ return inputTokens / 1e6 * p.input + outputTokens / 1e6 * p.output;
186
+ }
187
+ async function* executeAgent(agent, userMessage, history, globalModel, onUsage) {
188
+ const zeroClawUrl = agent.zeroClawUrl ?? getAgentUrl(agent.id) ?? getAgentUrl("*");
189
+ if (zeroClawUrl) {
190
+ yield* sendMessage(zeroClawUrl, userMessage);
191
+ return;
192
+ }
193
+ const modelConfig = getEffectiveModel(agent, globalModel);
194
+ const messageWithFiles = resolveFileContext(userMessage);
195
+ const cleanMessage = messageWithFiles.replace(/@(?!file:)\S+/g, "").trim();
196
+ const messages = [
197
+ ...history.slice(-10).map((h) => ({ role: h.role, content: h.content })),
198
+ { role: "user", content: cleanMessage }
199
+ ];
200
+ let model;
201
+ try {
202
+ model = buildModel(modelConfig);
203
+ } catch (err) {
204
+ yield `[Provider init error: ${err instanceof Error ? err.message : String(err)}]`;
205
+ return;
206
+ }
207
+ try {
208
+ const result = await streamText({
209
+ model,
210
+ system: `${agent.systemPrompt}
211
+
212
+ You are in a terminal chat. Be concise (under 250 words unless asked for more). Markdown renders here.${getGitContext()}`,
213
+ messages,
214
+ maxTokens: modelConfig.maxTokens ?? 1024,
215
+ temperature: modelConfig.temperature ?? 0.7
216
+ });
217
+ for await (const chunk of result.textStream) {
218
+ yield chunk;
219
+ }
220
+ try {
221
+ const usage = await result.usage;
222
+ if (usage && onUsage) {
223
+ const cost = calculateCost(modelConfig, usage.promptTokens, usage.completionTokens);
224
+ onUsage(cost);
225
+ }
226
+ } catch {
227
+ }
228
+ } catch (err) {
229
+ const msg = err instanceof Error ? err.message : String(err);
230
+ if (msg.includes("401") || msg.toLowerCase().includes("api key")) {
231
+ yield `
232
+ \u26A0 API key missing or invalid. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.
233
+ `;
234
+ } else if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
235
+ yield `
236
+ \u26A0 Cannot reach ${modelConfig.provider}. Check your connection or Ollama setup.
237
+ `;
238
+ } else {
239
+ yield `
240
+ \u26A0 ${agent.name} error: ${msg}
241
+ `;
242
+ }
243
+ }
244
+ }
245
+
246
+ export {
247
+ loadRuntime,
248
+ saveRuntime,
249
+ clearRuntime,
250
+ getAgentUrl,
251
+ pingAgent,
252
+ sendMessage,
253
+ getEffectiveModel,
254
+ executeAgent
255
+ };
@@ -0,0 +1,108 @@
1
+ // src/security/policy.ts
2
+ import { z } from "zod";
3
+ import { parse as parseYaml } from "yaml";
4
+ var praesidiaPolicySchema = z.object({
5
+ schema_version: z.union([z.number(), z.string()]),
6
+ enforcement: z.enum(["strict", "permissive"]).default("strict"),
7
+ rules: z.object({
8
+ ingress: z.array(z.object({
9
+ rule: z.string(),
10
+ action: z.enum(["reject", "allow", "log"])
11
+ })).optional().default([]),
12
+ egress: z.array(z.object({
13
+ rule: z.string(),
14
+ types: z.array(z.string()).optional(),
15
+ action: z.enum(["modify", "reject", "allow"])
16
+ })).optional().default([]),
17
+ tools: z.array(z.object({
18
+ match: z.string(),
19
+ action: z.enum(["allow", "reject"]),
20
+ require_human_approval: z.boolean().optional()
21
+ })).optional().default([])
22
+ })
23
+ });
24
+ function parsePolicy(yamlContent) {
25
+ const raw = parseYaml(yamlContent);
26
+ return praesidiaPolicySchema.parse(raw);
27
+ }
28
+
29
+ // src/security/hooks.ts
30
+ import * as crypto from "crypto";
31
+ import * as fs from "fs";
32
+ var PraesidiaHooks = class _PraesidiaHooks {
33
+ policy = null;
34
+ isPermissive = false;
35
+ constructor(policy) {
36
+ if (policy) {
37
+ this.policy = policy;
38
+ this.isPermissive = policy.enforcement === "permissive";
39
+ }
40
+ }
41
+ static withPolicy(filePath) {
42
+ if (!fs.existsSync(filePath)) {
43
+ console.warn(`[Praesidia] Policy file not found at ${filePath}. Running with empty policy.`);
44
+ return new _PraesidiaHooks();
45
+ }
46
+ const content = fs.readFileSync(filePath, "utf-8");
47
+ const policy = parsePolicy(content);
48
+ return new _PraesidiaHooks(policy);
49
+ }
50
+ getPolicyHash() {
51
+ if (!this.policy) return "";
52
+ return crypto.createHash("sha256").update(JSON.stringify(this.policy)).digest("hex");
53
+ }
54
+ async checkIngress(payload) {
55
+ if (!this.policy?.rules.ingress.length) return { allowed: true };
56
+ const jailbreakPatterns = [
57
+ "ignore all previous instructions",
58
+ "disregard your instructions",
59
+ "you are now",
60
+ "act as if",
61
+ "pretend you are"
62
+ ];
63
+ const lower = payload.toLowerCase();
64
+ const triggered = jailbreakPatterns.some((p) => lower.includes(p));
65
+ if (triggered) {
66
+ const rejectRule = this.policy.rules.ingress.find((r) => r.action === "reject");
67
+ if (rejectRule && !this.isPermissive) {
68
+ return { allowed: false, reason: `Blocked by rule: ${rejectRule.rule}` };
69
+ }
70
+ }
71
+ return { allowed: true };
72
+ }
73
+ async checkEgress(chunk) {
74
+ if (!this.policy?.rules.egress.length) return { allowed: true, modifiedPayload: chunk };
75
+ let modified = chunk;
76
+ for (const rule of this.policy.rules.egress) {
77
+ if (rule.rule === "redact_pii" && rule.action === "modify") {
78
+ modified = modified.replace(/\b\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}\b/g, "[REDACTED-CC]");
79
+ modified = modified.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED-SSN]");
80
+ if (rule.types?.includes("EMAIL")) {
81
+ modified = modified.replace(/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g, "[REDACTED-EMAIL]");
82
+ }
83
+ }
84
+ }
85
+ return { allowed: true, modifiedPayload: modified };
86
+ }
87
+ async checkTool(toolName) {
88
+ if (!this.policy?.rules.tools.length) return { allowed: true };
89
+ for (const rule of this.policy.rules.tools) {
90
+ const isMatch = rule.match.endsWith("*") ? toolName.startsWith(rule.match.slice(0, -1)) : toolName === rule.match;
91
+ if (isMatch) {
92
+ if (rule.action === "reject" && !this.isPermissive) {
93
+ return { allowed: false, reason: `Tool blocked by policy rule: ${rule.match}` };
94
+ }
95
+ if (rule.require_human_approval) {
96
+ return { allowed: false, reason: "HUMAN_APPROVAL_REQUIRED" };
97
+ }
98
+ return { allowed: true };
99
+ }
100
+ }
101
+ return { allowed: true };
102
+ }
103
+ };
104
+
105
+ export {
106
+ parsePolicy,
107
+ PraesidiaHooks
108
+ };
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node