mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 22:31:40 +00:00
* refactor(plugins): trim activation and contract exports * test(plugins): restore fixture cleanup * refactor(plugins): trim install and loader exports * test(plugins): fully reset loader caches * refactor(plugins): trim metadata and catalog exports * test(plugins): preserve catalog trust coverage * refactor(plugins): trim provider and plugin exports * refactor(plugins): trim runtime and tool exports * test(plugins): update dead-export consumers * test(plugins): remove empty dead-export suites * refactor(plugins): align exports with split registry * refactor(plugins): trim drifted loader exports * style(plugins): format test fixtures * refactor(scripts): use supported plugin APIs * refactor(plugins): finish dead export cleanup * chore(deadcode): refresh export baseline * test(cli): mock production memory state * chore(deadcode): sync latest export baseline * fix(tests): keep plugin fixtures inside core * chore(deadcode): refresh rebased export baseline * chore(deadcode): sync current ratchets * fix(plugins): retain reserved slot invariant * fix(plugins): preserve dead-export invariants * test(plugins): use neutral catalog query fixture * test(plugins): satisfy catalog lint * test(plugins): preserve integrity drift coverage * fix(ci): register skill experience live proof
97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
/**
|
|
* Structured decision returned by gate/policy hooks.
|
|
* Core is outcome-agnostic — it handles the mechanics of each outcome
|
|
* without knowing *why* the decision was made.
|
|
*/
|
|
type HookDecision = HookDecisionPass | HookDecisionBlock;
|
|
|
|
/** Content is fine. Proceed normally. */
|
|
type HookDecisionPass = {
|
|
outcome: "pass";
|
|
};
|
|
|
|
/** Prefix for user-facing replacement messages when a `block` decision stops a request. */
|
|
const BLOCK_MESSAGE_PREFIX = "Your message could not be sent";
|
|
|
|
/**
|
|
* Content is blocked. `reason` is internal plugin-local detail; core must not log,
|
|
* persist, broadcast, or expose it verbatim. `message` is user-facing detail.
|
|
*/
|
|
type HookDecisionBlock = {
|
|
outcome: "block";
|
|
/** Internal plugin-local reason. Do not log, persist, broadcast, or expose verbatim. */
|
|
reason: string;
|
|
/** Optional user-facing detail included in the block response envelope. */
|
|
message?: string;
|
|
/** Plugin-defined category for analytics (e.g. "violence", "pii", "cost_limit"). */
|
|
category?: string;
|
|
/** Opaque metadata for the plugin's own use. Core does not interpret it. */
|
|
metadata?: Record<string, unknown>;
|
|
};
|
|
|
|
export function resolveBlockMessage(
|
|
decision: HookDecisionBlock,
|
|
params: { blockedBy?: string } = {},
|
|
): string {
|
|
const message = typeof decision.message === "string" ? decision.message.trim() : "";
|
|
const blockedBy = params.blockedBy?.trim();
|
|
if (message) {
|
|
return blockedBy
|
|
? `${BLOCK_MESSAGE_PREFIX}: ${message} (blocked by ${blockedBy})`
|
|
: `${BLOCK_MESSAGE_PREFIX}: ${message}`;
|
|
}
|
|
return blockedBy
|
|
? `${BLOCK_MESSAGE_PREFIX}: blocked by ${blockedBy}`
|
|
: `${BLOCK_MESSAGE_PREFIX}: blocked`;
|
|
}
|
|
|
|
/**
|
|
* Type guard: does this object look like a HookDecision (has `outcome` field)?
|
|
*/
|
|
export function isHookDecision(value: unknown): value is HookDecision {
|
|
if (typeof value !== "object" || value === null) {
|
|
return false;
|
|
}
|
|
const v = value as Record<string, unknown>;
|
|
const keys = Object.keys(v);
|
|
if (v.outcome === "pass") {
|
|
return keys.length === 1;
|
|
}
|
|
if (v.outcome !== "block") {
|
|
return false;
|
|
}
|
|
const allowedBlockKeys = new Set(["outcome", "reason", "message", "category", "metadata"]);
|
|
if (keys.some((key) => !allowedBlockKeys.has(key))) {
|
|
return false;
|
|
}
|
|
if (typeof v.reason !== "string" || !v.reason.trim()) {
|
|
return false;
|
|
}
|
|
if ("message" in v && (typeof v.message !== "string" || !v.message.trim())) {
|
|
return false;
|
|
}
|
|
if ("category" in v && (typeof v.category !== "string" || !v.category.trim())) {
|
|
return false;
|
|
}
|
|
if (
|
|
"metadata" in v &&
|
|
(typeof v.metadata !== "object" || v.metadata === null || Array.isArray(v.metadata))
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Outcomes valid for input gates (before_agent_run). */
|
|
export type InputGateDecision = HookDecisionPass | HookDecisionBlock;
|
|
|
|
/**
|
|
* A gate hook decision paired with the pluginId that produced it.
|
|
* Returned by gate hook runners so callers can
|
|
* attribute blocked entries and audit events to the originating plugin.
|
|
*/
|
|
export type GateHookResult<TDecision extends HookDecision = HookDecision> = {
|
|
decision: TDecision;
|
|
pluginId: string;
|
|
};
|