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:
@@ -365,11 +365,22 @@ def build_messages(question, channel):
|
||||
|
||||
|
||||
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":
|
||||
return True
|
||||
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):
|
||||
|
||||
@@ -19,6 +19,9 @@ import {
|
||||
createTap,
|
||||
deleteTap,
|
||||
macFromOctet,
|
||||
applyNetworkPolicy,
|
||||
removeNetworkPolicy,
|
||||
type NetworkPolicy,
|
||||
} from "./network.js";
|
||||
import * as api from "./firecracker-api.js";
|
||||
|
||||
@@ -42,6 +45,7 @@ interface AgentTemplate {
|
||||
model: string;
|
||||
trigger: string;
|
||||
persona: string;
|
||||
network?: NetworkPolicy;
|
||||
}
|
||||
|
||||
const AGENTS_FILE = join(CONFIG.baseDir, "agents.json");
|
||||
@@ -299,6 +303,10 @@ export async function startAgent(
|
||||
);
|
||||
await api.startInstance(socketPath);
|
||||
|
||||
// Apply network policy
|
||||
const networkPolicy: NetworkPolicy = template.network ?? "full";
|
||||
applyNetworkPolicy(ip, networkPolicy);
|
||||
|
||||
const info: AgentInfo = {
|
||||
name,
|
||||
nick,
|
||||
@@ -366,10 +374,11 @@ export async function stopAgent(name: string) {
|
||||
// Small delay to let kernel release the tap device
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
// Cleanup with retry for tap
|
||||
// Cleanup
|
||||
try {
|
||||
unlinkSync(info.socketPath);
|
||||
} catch {}
|
||||
removeNetworkPolicy(info.ip);
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
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 {
|
||||
return `AA:FC:00:00:00:${octet.toString(16).padStart(2, "0").toUpperCase()}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user