Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
019ea07
init attempt on guardrail interface
Reapor-Yurnero Feb 1, 2026
1ae9c11
fix some typing errors
Reapor-Yurnero Feb 1, 2026
4a6da7a
fix bug that would hang on pre request violation indefinitely
Reapor-Yurnero Feb 1, 2026
84055e5
fix web browser not seeing blocked message bug
Reapor-Yurnero Feb 1, 2026
6b0d748
fix an error from the previous fix
Reapor-Yurnero Feb 1, 2026
6d7f989
add some doc
Reapor-Yurnero Feb 1, 2026
85ab3a2
refactor: migrate guardrails to plugin hook system
Scrattlebeard Feb 1, 2026
5c74c85
Add LlamaGuard and gpt-oss-safeguard plugins. Remove guardrails confi…
Scrattlebeard Feb 1, 2026
7ff2716
refactor: add createGuardrailPlugin factory for unified guardrail API
Scrattlebeard Feb 1, 2026
965dbc0
feat: add command-safety-guard and security-audit plugins
Scrattlebeard Feb 1, 2026
fd6b621
Plugins: fix guardrail hook sequencing and short circuiting
Reapor-Yurnero Feb 2, 2026
e2fd8a1
change plugin naming to follow conventions
Reapor-Yurnero Feb 2, 2026
efc7065
remove redundant config enabled key
Reapor-Yurnero Feb 2, 2026
f646657
revert some unnecesary validation after moving to plugins
Reapor-Yurnero Feb 2, 2026
d1e1e31
remove redundant config.enable schema
Reapor-Yurnero Feb 2, 2026
6b5719b
Guardrails: GPT-OSS safeguard defaults and append mode
Reapor-Yurnero Feb 2, 2026
ce2a526
remove llama guard for the moment
Reapor-Yurnero Feb 2, 2026
211dde8
Guardrails: skip hooks for internal runs
Reapor-Yurnero Feb 2, 2026
c2bdd7d
polish the doc
Reapor-Yurnero Feb 2, 2026
1bb5876
Guardrails: add priority and attribution
Reapor-Yurnero Feb 2, 2026
a646c9a
Enhance command-safety-guard and security-audit plugins with UI hints…
Scrattlebeard Feb 2, 2026
70edfa0
Merge branch 'feat/guardrail_interface' into bugfix/command-safety-an…
Scrattlebeard Feb 2, 2026
87c6051
Fix implementations to use the correct tool names.
Scrattlebeard Feb 2, 2026
664fdcc
Merge pull request #5 from grayswansecurity/bugfix/fix-tool-names-in-…
Scrattlebeard Feb 2, 2026
a8f0f7c
Merge upstream/main
Reapor-Yurnero Feb 2, 2026
d020951
fix pnpm lint & pnpm build errors
Reapor-Yurnero Feb 2, 2026
43f4fc2
Merge grayswansecurity/openclaw#2
Reapor-Yurnero Feb 2, 2026
6b7c542
add readme for cygnal and gpt-oss-safeguard extensions
Reapor-Yurnero Feb 2, 2026
0e9293b
update doc about restart option
Reapor-Yurnero Feb 2, 2026
3b1e194
fix a typo introduced in command-safety-guard
Reapor-Yurnero Feb 2, 2026
e37120d
Add OpenClaw-specific API link
nwinter Feb 3, 2026
020918e
Update pnpm-lock.yaml for command-safety-guard extension
nwinter Feb 3, 2026
8dc07d2
Format extension files and escape pipe in fork-bomb pattern
nwinter Feb 3, 2026
de84696
Update test to match hook contract requiring toolCallId and messages
nwinter Feb 3, 2026
ed99104
Fix tests: use lowercase tool names and updated DEFAULT_POLICY content
nwinter Feb 3, 2026
aae4098
Skip path tests on Windows (plugin only supports Unix paths)
nwinter Feb 3, 2026
df0ffd9
address greptile comments
Reapor-Yurnero Feb 3, 2026
87e03a9
address false positves in security-audit extension for redirection
Reapor-Yurnero Feb 3, 2026
0b6d963
fix formatting issue in previous commit
Reapor-Yurnero Feb 3, 2026
495f859
address dropped tool return metadata on block
Reapor-Yurnero Feb 3, 2026
d4bea63
address more comments
Reapor-Yurnero Feb 3, 2026
51fcebe
fix type error
Reapor-Yurnero Feb 3, 2026
f1aac5f
Merge upstream/main
Reapor-Yurnero Feb 9, 2026
0a3e77d
Merge upstream/main
Reapor-Yurnero Feb 12, 2026
1141b52
Merge remote-tracking branch 'upstream/main' into feat/guardrail_inte…
Reapor-Yurnero Feb 14, 2026
8c94a4a
Merge remote-tracking branch 'upstream/main' into feat/guardrail_inte…
Reapor-Yurnero Feb 14, 2026
a5c1013
test(memory): stabilize qmd-manager wait
Reapor-Yurnero Feb 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Merge remote-tracking branch 'upstream/main' into feat/guardrail_inte…
…rface

# Conflicts:
#	src/agents/pi-embedded-runner/run.ts
#	src/agents/pi-embedded-runner/run/attempt.ts
#	src/agents/pi-tool-definition-adapter.ts
#	src/plugins/hooks.ts
#	src/plugins/types.ts
  • Loading branch information
Reapor-Yurnero committed Feb 14, 2026
commit 1141b52d6ce99caa2f8f4c59abf53f69143bc7b7
10 changes: 6 additions & 4 deletions src/agents/pi-embedded-runner/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,12 @@ export async function runEmbeddedPiAgent(
}
: undefined;

mergeUsageIntoAccumulator(
usageAccumulator,
attempt.attemptUsage ?? normalizeUsage(lastAssistant?.usage as UsageLike),
);
const lastAssistantUsage = normalizeUsage(lastAssistant?.usage as UsageLike);
const attemptUsage = attempt.attemptUsage ?? lastAssistantUsage;
mergeUsageIntoAccumulator(usageAccumulator, attemptUsage);
// Keep prompt size from the latest model call so session totalTokens
// reflects current context usage, not accumulated tool-loop usage.
lastRunPromptUsage = lastAssistantUsage ?? attemptUsage;
autoCompactionCount += Math.max(0, attempt.compactionCount ?? 0);

const formattedAssistantErrorText = lastAssistant
Expand Down
95 changes: 20 additions & 75 deletions src/agents/pi-embedded-runner/run/attempt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,69 +150,6 @@ export function injectHistoryImagesIntoMessages(
return didMutate;
}

function summarizeMessagePayload(msg: AgentMessage): { textChars: number; imageBlocks: number } {
const content = (msg as { content?: unknown }).content;
if (typeof content === "string") {
return { textChars: content.length, imageBlocks: 0 };
}
if (!Array.isArray(content)) {
return { textChars: 0, imageBlocks: 0 };
}

let textChars = 0;
let imageBlocks = 0;
for (const block of content) {
if (!block || typeof block !== "object") {
continue;
}
const typedBlock = block as { type?: unknown; text?: unknown };
if (typedBlock.type === "image") {
imageBlocks++;
continue;
}
if (typeof typedBlock.text === "string") {
textChars += typedBlock.text.length;
}
}

return { textChars, imageBlocks };
}

function summarizeSessionContext(messages: AgentMessage[]): {
roleCounts: string;
totalTextChars: number;
totalImageBlocks: number;
maxMessageTextChars: number;
} {
const roleCounts = new Map<string, number>();
let totalTextChars = 0;
let totalImageBlocks = 0;
let maxMessageTextChars = 0;

for (const msg of messages) {
const role = typeof msg.role === "string" ? msg.role : "unknown";
roleCounts.set(role, (roleCounts.get(role) ?? 0) + 1);

const payload = summarizeMessagePayload(msg);
totalTextChars += payload.textChars;
totalImageBlocks += payload.imageBlocks;
if (payload.textChars > maxMessageTextChars) {
maxMessageTextChars = payload.textChars;
}
}

return {
roleCounts:
[...roleCounts.entries()]
.toSorted((a, b) => a[0].localeCompare(b[0]))
.map(([role, count]) => `${role}:${count}`)
.join(",") || "none",
totalTextChars,
totalImageBlocks,
maxMessageTextChars,
};
}

export async function runEmbeddedAttempt(
params: EmbeddedRunAttemptParams,
): Promise<EmbeddedRunAttemptResult> {
Expand Down Expand Up @@ -542,11 +479,18 @@ export async function runEmbeddedAttempt(
model: params.model,
});

const toolHookOptions = {
context: toolHookContext,
getMessages: () => sessionManager?.buildSessionContext().messages ?? [],
systemPrompt: appendPrompt,
};
const skipGuardrailHooks =
isGuardrailRunId(params.sessionId) || isGuardrailRunId(params.runId);
// Get hook runner early so it's available when creating tools.
// When invoked by a guardrail itself, skip hooks to prevent recursion.
const hookRunner = skipGuardrailHooks ? null : getGlobalHookRunner();
const toolHookOptions = skipGuardrailHooks
? undefined
: {
context: toolHookContext,
getMessages: () => sessionManager?.buildSessionContext().messages ?? [],
systemPrompt: appendPrompt,
};
const { builtInTools, customTools } = splitSdkTools({
tools,
sandboxEnabled: !!sandbox?.enabled,
Expand Down Expand Up @@ -742,7 +686,7 @@ export async function runEmbeddedAttempt(
const subscription = subscribeEmbeddedPiSession({
session: activeSession,
runId: params.runId,
hookRunner: getGlobalHookRunner() ?? undefined,
hookRunner: hookRunner ?? undefined,
verboseLevel: params.verboseLevel,
reasoningMode: params.reasoningLevel ?? "off",
toolResultFormat: params.toolResultFormat,
Expand Down Expand Up @@ -831,10 +775,7 @@ export async function runEmbeddedAttempt(
}
}

// Get hook runner once for both before_agent_start and agent_end hooks
const skipGuardrailHooks =
isGuardrailRunId(params.sessionId) || isGuardrailRunId(params.runId);
const hookRunner = skipGuardrailHooks ? null : getGlobalHookRunner();
// Hook runner was already obtained earlier before tool creation
const hookAgentId =
typeof params.agentId === "string" && params.agentId.trim()
? normalizeAgentId(params.agentId)
Expand Down Expand Up @@ -904,8 +845,9 @@ export async function runEmbeddedAttempt(
},
{
agentId: sessionAgentId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
workspaceDir: params.workspaceDir,
workspaceDir: effectiveWorkspace,
messageProvider: params.messageProvider ?? undefined,
},
);
Expand Down Expand Up @@ -978,7 +920,10 @@ export async function runEmbeddedAttempt(
historyMessages: activeSession.messages,
maxBytes: MAX_IMAGE_BYTES,
// Enforce sandbox path restrictions when sandbox is enabled
sandboxRoot: sandbox?.enabled ? sandbox.workspaceDir : undefined,
sandbox:
sandbox?.enabled && sandbox?.fsBridge
? { root: sandbox.workspaceDir, bridge: sandbox.fsBridge }
: undefined,
});

// Inject history images into their original message positions.
Expand Down
14 changes: 11 additions & 3 deletions src/agents/pi-embedded-subscribe.handlers.tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AgentEvent } from "@mariozechner/pi-agent-core";
import type { AgentEvent, AgentToolResult } from "@mariozechner/pi-agent-core";
import type { PluginHookAfterToolCallEvent } from "../plugins/types.js";
import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js";
import { emitAgentEvent } from "../infra/agent-events.js";
Expand All @@ -13,6 +13,7 @@ import {
sanitizeToolResult,
} from "./pi-embedded-subscribe.tools.js";
import { inferToolMetaFromArgs } from "./pi-embedded-utils.js";
import { consumeAfterToolCallHookHandled } from "./pi-tools.before-tool-call.js";
import { normalizeToolName } from "./tool-policy.js";

/** Track tool execution start times and args for after_tool_call hook */
Expand Down Expand Up @@ -237,22 +238,29 @@ export async function handleToolExecutionEnd(
// Run after_tool_call plugin hook (fire-and-forget)
const hookRunnerAfter = ctx.hookRunner ?? getGlobalHookRunner();
if (hookRunnerAfter?.hasHooks("after_tool_call")) {
if (consumeAfterToolCallHookHandled(toolCallId)) {
toolStartData.delete(toolCallId);
return;
}

const startData = toolStartData.get(toolCallId);
toolStartData.delete(toolCallId);
const durationMs = startData?.startTime != null ? Date.now() - startData.startTime : undefined;
const toolArgs = startData?.args;
const hookEvent: PluginHookAfterToolCallEvent = {
toolName,
toolCallId,
params: (toolArgs && typeof toolArgs === "object" ? toolArgs : {}) as Record<string, unknown>,
result: sanitizedResult,
result: sanitizedResult as AgentToolResult<unknown>,
error: isToolError ? extractToolErrorMessage(sanitizedResult) : undefined,
durationMs,
messages: ctx.params.session?.messages ?? [],
};
void hookRunnerAfter
.runAfterToolCall(hookEvent, {
toolName,
agentId: undefined,
sessionKey: undefined,
sessionKey: ctx.params.sessionKey,
})
.catch((err) => {
ctx.log.warn(`after_tool_call hook failed: tool=${toolName} error=${String(err)}`);
Expand Down
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.