Fix trigger matching and add network policies
- Trigger only matches when nick is at start of message, not mid-text Fixes: "coder: say hi to worker" no longer triggers worker - Network policies per agent: "full" (default), "local" (LAN only), "none" (IRC+Ollama only) Configured via template "network" field, applied as iptables rules per agent IP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -365,11 +365,22 @@ def build_messages(question, channel):
|
|||||||
|
|
||||||
|
|
||||||
def should_trigger(text):
|
def should_trigger(text):
|
||||||
"""Check if this message should trigger a response."""
|
"""Check if this message should trigger a response.
|
||||||
|
Only triggers when nick is at the start of the message (e.g. 'worker: hello')
|
||||||
|
not when nick appears elsewhere (e.g. 'coder: say hi to worker')."""
|
||||||
if RUNTIME["trigger"] == "all":
|
if RUNTIME["trigger"] == "all":
|
||||||
return True
|
return True
|
||||||
lower = text.lower()
|
lower = text.lower()
|
||||||
return NICK.lower() in lower or text.startswith("!ask ")
|
nick = NICK.lower()
|
||||||
|
# Match: "nick: ...", "nick, ...", "nick ...", "@nick ..."
|
||||||
|
return (
|
||||||
|
lower.startswith(f"{nick}:") or
|
||||||
|
lower.startswith(f"{nick},") or
|
||||||
|
lower.startswith(f"{nick} ") or
|
||||||
|
lower.startswith(f"@{nick}") or
|
||||||
|
lower == nick or
|
||||||
|
text.startswith("!ask ")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def extract_question(text):
|
def extract_question(text):
|
||||||
|
|||||||
+10
-1
@@ -19,6 +19,9 @@ import {
|
|||||||
createTap,
|
createTap,
|
||||||
deleteTap,
|
deleteTap,
|
||||||
macFromOctet,
|
macFromOctet,
|
||||||
|
applyNetworkPolicy,
|
||||||
|
removeNetworkPolicy,
|
||||||
|
type NetworkPolicy,
|
||||||
} from "./network.js";
|
} from "./network.js";
|
||||||
import * as api from "./firecracker-api.js";
|
import * as api from "./firecracker-api.js";
|
||||||
|
|
||||||
@@ -42,6 +45,7 @@ interface AgentTemplate {
|
|||||||
model: string;
|
model: string;
|
||||||
trigger: string;
|
trigger: string;
|
||||||
persona: string;
|
persona: string;
|
||||||
|
network?: NetworkPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AGENTS_FILE = join(CONFIG.baseDir, "agents.json");
|
const AGENTS_FILE = join(CONFIG.baseDir, "agents.json");
|
||||||
@@ -299,6 +303,10 @@ export async function startAgent(
|
|||||||
);
|
);
|
||||||
await api.startInstance(socketPath);
|
await api.startInstance(socketPath);
|
||||||
|
|
||||||
|
// Apply network policy
|
||||||
|
const networkPolicy: NetworkPolicy = template.network ?? "full";
|
||||||
|
applyNetworkPolicy(ip, networkPolicy);
|
||||||
|
|
||||||
const info: AgentInfo = {
|
const info: AgentInfo = {
|
||||||
name,
|
name,
|
||||||
nick,
|
nick,
|
||||||
@@ -366,10 +374,11 @@ export async function stopAgent(name: string) {
|
|||||||
// Small delay to let kernel release the tap device
|
// Small delay to let kernel release the tap device
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
|
||||||
// Cleanup with retry for tap
|
// Cleanup
|
||||||
try {
|
try {
|
||||||
unlinkSync(info.socketPath);
|
unlinkSync(info.socketPath);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
removeNetworkPolicy(info.ip);
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
try {
|
try {
|
||||||
deleteTap(info.tapDevice);
|
deleteTap(info.tapDevice);
|
||||||
|
|||||||
@@ -111,6 +111,74 @@ export function deleteTap(tapName: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NetworkPolicy = "full" | "local" | "none";
|
||||||
|
|
||||||
|
export function applyNetworkPolicy(ip: string, policy: NetworkPolicy) {
|
||||||
|
if (policy === "full") return; // Default, no restrictions
|
||||||
|
|
||||||
|
// Block outbound internet — only allow LAN, bridge, Ollama, IRC
|
||||||
|
const allowedDests = [
|
||||||
|
CONFIG.bridge.subnet, // bridge network (other VMs, IRC, Ollama)
|
||||||
|
"192.168.0.0/16", // LAN
|
||||||
|
];
|
||||||
|
|
||||||
|
if (policy === "none") {
|
||||||
|
// Only allow bridge subnet (IRC + Ollama)
|
||||||
|
allowedDests.length = 1; // keep only bridge subnet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow established connections back
|
||||||
|
sudo([
|
||||||
|
"iptables", "-I", "FORWARD",
|
||||||
|
"-s", ip,
|
||||||
|
"-m", "state", "--state", "ESTABLISHED,RELATED",
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Allow specific destinations
|
||||||
|
for (const dest of allowedDests) {
|
||||||
|
sudo([
|
||||||
|
"iptables", "-I", "FORWARD",
|
||||||
|
"-s", ip, "-d", dest,
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop everything else from this IP
|
||||||
|
sudo([
|
||||||
|
"iptables", "-A", "FORWARD",
|
||||||
|
"-s", ip,
|
||||||
|
"-j", "DROP",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeNetworkPolicy(ip: string) {
|
||||||
|
// Remove all FORWARD rules mentioning this IP
|
||||||
|
// Run in a loop since there may be multiple rules
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
try {
|
||||||
|
sudo([
|
||||||
|
"iptables", "-D", "FORWARD",
|
||||||
|
"-s", ip,
|
||||||
|
"-j", "DROP",
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
try {
|
||||||
|
sudo([
|
||||||
|
"iptables", "-D", "FORWARD",
|
||||||
|
"-s", ip,
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function macFromOctet(octet: number): string {
|
export function macFromOctet(octet: number): string {
|
||||||
return `AA:FC:00:00:00:${octet.toString(16).padStart(2, "0").toUpperCase()}`;
|
return `AA:FC:00:00:00:${octet.toString(16).padStart(2, "0").toUpperCase()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user