refactor(extensions): merge ttsr into guardrails

TTSR (Time Traveling Stream Rules) monitored streaming output against regex
patterns. Guardrails blocked dangerous actions and redacted secrets. Both are
safety/guardrail concerns — merging them into one extension reduces surface
area and simplifies the safety model.

Changes:
- Copied ttsr-rule-loader.js, ttsr-manager.js, ttsr-interrupt.md into guardrails/
- Updated guardrails extension-manifest.json with ttsr hooks (turn_start,
  message_update, turn_end, agent_end)
- Integrated TTSR session_start/turn_start/message_update/turn_end/agent_end
  handlers into guardrails/index.js
- Deleted ttsr/ extension directory

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-15 02:28:40 +02:00
parent 2d5a05a48b
commit f0c3eaf999
9 changed files with 94 additions and 159 deletions

View file

@ -13,7 +13,7 @@
"sourceMap": true,
"inlineSources": true,
"inlineSourceMap": false,
"moduleResolution": "NodeNext"}}]}<()>;<|assistant to=multi_tool_use.parallel __(/*!json*/)## Step: Rebuild and rerun autonomous SF after tsconfig updates. Also, verify build and run success before marking complete. {
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowImportingTsExtensions": false,
"useDefineForClassFields": false,

View file

@ -1734,6 +1734,10 @@
codespan(token) {
return `<code>${escapeHtml(token.text)}</code>`;
},
// Raw HTML blocks: escape to prevent XSS
html(token) {
return escapeHtml(token.text);
},
},
});

View file

@ -2,11 +2,11 @@
"id": "guardrails",
"name": "Guardrails",
"version": "1.0.0",
"description": "Redact sensitive outputs and block dangerous file or shell actions",
"description": "Redact sensitive outputs, block dangerous actions, and monitor streaming output against regex patterns",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"commands": ["safegit", "safegit-level", "safegit-status", "yolo"],
"hooks": ["session_start", "tool_call", "tool_result"]
"hooks": ["session_start", "tool_call", "tool_result", "turn_start", "message_update", "turn_end", "agent_end"]
}
}

View file

@ -7,9 +7,14 @@
* - Redacts secrets from tool results before the LLM sees them
* - Blocks dangerous bash commands (rm -rf, sudo, mkfs, etc.)
* - Blocks writes to protected paths (.env, .git, .ssh, etc.)
* - Monitors streaming output against regex patterns (TTSR)
* - Registers SF slash commands: /safegit, /safegit-level, /safegit-status, /yolo
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import * as path from "node:path";
import { loadRules } from "./ttsr-rule-loader.js";
import { TtsrManager } from "./ttsr-manager.js";
const SENSITIVE_PATTERNS = [
{
@ -554,6 +559,40 @@ function registerSafeGitCommands(
},
});
}
const __dirname = import.meta.dirname;
function buildInterruptContent(rule) {
const template = readFileSync(join(__dirname, "ttsr-interrupt.md"), "utf-8");
return template
.replace("{{name}}", rule.name)
.replace("{{path}}", rule.path)
.replace("{{content}}", rule.content);
}
function extractDeltaContext(event) {
if (event.type === "text_delta") {
return { delta: event.delta, context: { source: "text", streamKey: "text" } };
}
if (event.type === "thinking_delta") {
return { delta: event.delta, context: { source: "thinking", streamKey: "thinking" } };
}
if (event.type === "toolcall_delta") {
const partial = event.partial;
const contentBlock = partial?.content?.[event.contentIndex];
const toolName = contentBlock && "name" in contentBlock ? contentBlock.name : undefined;
const filePaths = [];
if (contentBlock && "partialJson" in contentBlock) {
const json = contentBlock.partialJson;
if (json) {
const pathMatch = json.match(/"(?:file_path|path)"\s*:\s*"([^"]+)"/);
if (pathMatch) filePaths.push(pathMatch[1]);
}
}
return {
delta: event.delta,
context: { source: "tool", toolName, filePaths: filePaths.length > 0 ? filePaths : undefined, streamKey: `toolcall:${event.contentIndex}` },
};
}
return null;
}
// ============================================================================
// Entry Point
// ============================================================================
@ -628,4 +667,52 @@ export default function guardrails(pi) {
ctx,
);
});
// ── TTSR: Time Traveling Stream Rules ───────────────────────────────────
let ttsrManager = null;
let pendingViolation = null;
pi.on("session_start", async (_event, ctx) => {
const rules = loadRules(ctx.cwd);
if (rules.length === 0) {
ttsrManager = null;
return;
}
ttsrManager = new TtsrManager();
let loaded = 0;
for (const rule of rules) {
if (ttsrManager.addRule(rule)) loaded++;
}
if (loaded === 0) ttsrManager = null;
});
pi.on("turn_start", async () => {
if (!ttsrManager) return;
ttsrManager.resetBuffer();
pendingViolation = null;
});
pi.on("message_update", async (event, ctx) => {
if (!ttsrManager || !ttsrManager.hasRules()) return;
if (pendingViolation) return;
const extracted = extractDeltaContext(event.assistantMessageEvent);
if (!extracted) return;
const { delta, context } = extracted;
const matches = ttsrManager.checkDelta(delta, context);
if (matches.length === 0) return;
pendingViolation = { rules: matches };
ttsrManager.markInjected(matches);
ctx.abort();
});
pi.on("turn_end", async () => {
if (!ttsrManager) return;
ttsrManager.incrementMessageCount();
});
pi.on("agent_end", async () => {
if (!ttsrManager || !pendingViolation) return;
const violation = pendingViolation;
pendingViolation = null;
const interruptParts = violation.rules.map(buildInterruptContent);
const fullInterrupt = interruptParts.join("\n\n");
pi.sendMessage(
{ customType: "ttsr-violation", content: fullInterrupt, display: false },
{ triggerTurn: true },
);
});
}

View file

@ -1,17 +0,0 @@
{
"id": "ttsr",
"name": "Time Traveling Stream Rules",
"version": "1.0.0",
"description": "Zero-context-cost guardrails that monitor streaming output against regex patterns",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"hooks": [
"session_start",
"turn_start",
"message_update",
"turn_end",
"agent_end"
]
}
}

View file

@ -1,139 +0,0 @@
/**
* TTSR Extension Time Traveling Stream Rules
*
* Zero-context-cost guardrails that monitor streaming output against regex
* patterns. On match: abort stream, inject rule as system reminder, retry.
* Rules cost nothing until they fire.
*
* Hooks:
* session_start load rules, populate manager
* turn_start reset buffers
* message_update check delta against rules, abort on match
* turn_end increment message count
* agent_end if pending violation, inject rule via sendMessage
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { loadRules } from "./rule-loader.js";
import { TtsrManager } from "./ttsr-manager.js";
const __dirname = import.meta.dirname;
function buildInterruptContent(rule) {
const template = readFileSync(join(__dirname, "ttsr-interrupt.md"), "utf-8");
return template
.replace("{{name}}", rule.name)
.replace("{{path}}", rule.path)
.replace("{{content}}", rule.content);
}
/**
* Extract match context from an AssistantMessageEvent delta.
* Returns null for non-delta events.
*/
function extractDeltaContext(event) {
if (event.type === "text_delta") {
return {
delta: event.delta,
context: { source: "text", streamKey: "text" },
};
}
if (event.type === "thinking_delta") {
return {
delta: event.delta,
context: { source: "thinking", streamKey: "thinking" },
};
}
if (event.type === "toolcall_delta") {
// Extract tool name and file paths from the partial message
const partial = event.partial;
const contentBlock = partial?.content?.[event.contentIndex];
const toolName =
contentBlock && "name" in contentBlock ? contentBlock.name : undefined;
// Try to extract file paths from partial JSON arguments
const filePaths = [];
if (contentBlock && "partialJson" in contentBlock) {
const json = contentBlock.partialJson;
if (json) {
// Look for file_path or path in partial JSON
const pathMatch = json.match(/"(?:file_path|path)"\s*:\s*"([^"]+)"/);
if (pathMatch) filePaths.push(pathMatch[1]);
}
}
return {
delta: event.delta,
context: {
source: "tool",
toolName,
filePaths: filePaths.length > 0 ? filePaths : undefined,
streamKey: `toolcall:${event.contentIndex}`,
},
};
}
return null;
}
export { loadRules } from "./rule-loader.js";
// Re-exports for external consumers
export { TtsrManager } from "./ttsr-manager.js";
export default function (pi) {
let manager = null;
let pendingViolation = null;
// ── session_start: load rules, populate manager ─────────────────────
pi.on("session_start", async (_event, ctx) => {
const rules = loadRules(ctx.cwd);
if (rules.length === 0) {
manager = null;
return;
}
manager = new TtsrManager();
let loaded = 0;
for (const rule of rules) {
if (manager.addRule(rule)) loaded++;
}
if (loaded === 0) {
manager = null;
}
});
// ── turn_start: reset buffers ───────────────────────────────────────
pi.on("turn_start", async () => {
if (!manager) return;
manager.resetBuffer();
pendingViolation = null;
});
// ── message_update: check delta against rules ───────────────────────
pi.on("message_update", async (event, ctx) => {
if (!manager || !manager.hasRules()) return;
if (pendingViolation) return; // Already matched, waiting for agent_end
const extracted = extractDeltaContext(event.assistantMessageEvent);
if (!extracted) return;
const { delta, context } = extracted;
const matches = manager.checkDelta(delta, context);
if (matches.length === 0) return;
// Match found — set pending violation and abort
pendingViolation = { rules: matches };
manager.markInjected(matches);
ctx.abort();
});
// ── turn_end: increment message count ───────────────────────────────
pi.on("turn_end", async () => {
if (!manager) return;
manager.incrementMessageCount();
});
// ── agent_end: inject violation if pending ──────────────────────────
pi.on("agent_end", async () => {
if (!manager || !pendingViolation) return;
const violation = pendingViolation;
pendingViolation = null;
// Build interrupt content for all matching rules
const interruptParts = violation.rules.map(buildInterruptContent);
const fullInterrupt = interruptParts.join("\n\n");
// Inject as a message that triggers a new turn
pi.sendMessage(
{
customType: "ttsr-violation",
content: fullInterrupt,
display: false,
},
{ triggerTurn: true },
);
});
}