securityclaw 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +135 -0
  4. package/README.zh-CN.md +135 -0
  5. package/admin/public/app.js +148 -0
  6. package/admin/public/favicon.svg +21 -0
  7. package/admin/public/index.html +31 -0
  8. package/admin/public/styles.css +2715 -0
  9. package/admin/server.ts +1053 -0
  10. package/bin/install-lib.mjs +88 -0
  11. package/bin/securityclaw.mjs +66 -0
  12. package/config/policy.default.yaml +520 -0
  13. package/index.ts +2662 -0
  14. package/install.sh +22 -0
  15. package/openclaw.plugin.json +60 -0
  16. package/package.json +69 -0
  17. package/src/admin/build.ts +113 -0
  18. package/src/admin/console_notice.ts +195 -0
  19. package/src/admin/dashboard_url_state.ts +80 -0
  20. package/src/admin/openclaw_session_catalog.ts +137 -0
  21. package/src/admin/runtime_guard.ts +51 -0
  22. package/src/admin/skill_interception_store.ts +1606 -0
  23. package/src/application/commands/approval_commands.ts +189 -0
  24. package/src/approvals/chat_approval_store.ts +433 -0
  25. package/src/config/live_config.ts +144 -0
  26. package/src/config/loader.ts +168 -0
  27. package/src/config/runtime_override.ts +66 -0
  28. package/src/config/strategy_store.ts +121 -0
  29. package/src/config/validator.ts +222 -0
  30. package/src/domain/models/resource_context.ts +31 -0
  31. package/src/domain/ports/approval_repository.ts +40 -0
  32. package/src/domain/ports/notification_port.ts +29 -0
  33. package/src/domain/ports/openclaw_adapter.ts +22 -0
  34. package/src/domain/services/account_policy_engine.ts +163 -0
  35. package/src/domain/services/approval_service.ts +336 -0
  36. package/src/domain/services/approval_subject_resolver.ts +37 -0
  37. package/src/domain/services/context_inference_service.ts +502 -0
  38. package/src/domain/services/file_rule_registry.ts +171 -0
  39. package/src/domain/services/formatting_service.ts +101 -0
  40. package/src/domain/services/path_candidate_inference.ts +111 -0
  41. package/src/domain/services/sensitive_path_registry.ts +288 -0
  42. package/src/domain/services/sensitivity_label_inference.ts +161 -0
  43. package/src/domain/services/shell_filesystem_inference.ts +360 -0
  44. package/src/engine/approval_fsm.ts +104 -0
  45. package/src/engine/decision_engine.ts +39 -0
  46. package/src/engine/dlp_engine.ts +91 -0
  47. package/src/engine/rule_engine.ts +208 -0
  48. package/src/events/emitter.ts +86 -0
  49. package/src/events/schema.ts +27 -0
  50. package/src/hooks/context_guard.ts +36 -0
  51. package/src/hooks/output_guard.ts +66 -0
  52. package/src/hooks/persist_guard.ts +69 -0
  53. package/src/hooks/policy_guard.ts +222 -0
  54. package/src/hooks/result_guard.ts +88 -0
  55. package/src/i18n/locale.ts +36 -0
  56. package/src/index.ts +255 -0
  57. package/src/infrastructure/adapters/notification_adapter.ts +173 -0
  58. package/src/infrastructure/adapters/openclaw_adapter_impl.ts +59 -0
  59. package/src/infrastructure/config/plugin_config_parser.ts +105 -0
  60. package/src/monitoring/status_store.ts +612 -0
  61. package/src/types.ts +409 -0
  62. package/src/utils.ts +97 -0
@@ -0,0 +1,222 @@
1
+ import type {
2
+ ApprovalRecord,
3
+ BeforeToolCallInput,
4
+ DecisionContext,
5
+ FileRule,
6
+ GuardComputation,
7
+ SecurityContext,
8
+ SensitivePathRule,
9
+ } from "../types.ts";
10
+ import type { ApprovalFsm } from "../engine/approval_fsm.ts";
11
+ import type { DecisionEngine } from "../engine/decision_engine.ts";
12
+ import type { RuleEngine } from "../engine/rule_engine.ts";
13
+ import { defaultFileRuleReasonCode, matchFileRule } from "../domain/services/file_rule_registry.ts";
14
+ import { inferSensitivityLabels } from "../domain/services/sensitivity_label_inference.ts";
15
+
16
+ function buildSecurityContext(
17
+ input: BeforeToolCallInput,
18
+ policyVersion: string,
19
+ traceId: string,
20
+ nowIso: string,
21
+ ): SecurityContext {
22
+ return {
23
+ trace_id: input.security_context?.trace_id ?? traceId,
24
+ actor_id: input.actor_id,
25
+ workspace: input.workspace,
26
+ policy_version: input.security_context?.policy_version ?? policyVersion,
27
+ untrusted: input.security_context?.untrusted ?? false,
28
+ tags: [...(input.security_context?.tags ?? input.tags ?? [])],
29
+ created_at: input.security_context?.created_at ?? nowIso
30
+ };
31
+ }
32
+
33
+ function invalidApprovalResult(
34
+ mutatedPayload: BeforeToolCallInput,
35
+ securityContext: SecurityContext,
36
+ approval: ApprovalRecord,
37
+ reasonCodes: string[],
38
+ ): GuardComputation<BeforeToolCallInput> {
39
+ return {
40
+ mutated_payload: mutatedPayload,
41
+ decision: "block",
42
+ decision_source: "approval",
43
+ reason_codes: reasonCodes,
44
+ sanitization_actions: [],
45
+ security_context: securityContext,
46
+ approval,
47
+ };
48
+ }
49
+
50
+ function validateApprovedReplay(approval: ApprovalRecord, context: DecisionContext): string[] {
51
+ const reasons: string[] = [];
52
+ if (approval.request_context.actor_id !== context.actor_id) {
53
+ reasons.push("APPROVAL_ACTOR_MISMATCH");
54
+ }
55
+ if (approval.request_context.scope !== context.scope) {
56
+ reasons.push("APPROVAL_SCOPE_MISMATCH");
57
+ }
58
+ if (
59
+ approval.request_context.tool_name !== undefined &&
60
+ context.tool_name !== undefined &&
61
+ approval.request_context.tool_name !== context.tool_name
62
+ ) {
63
+ reasons.push("APPROVAL_TOOL_MISMATCH");
64
+ }
65
+ if (approval.request_context.resource_scope !== context.resource_scope) {
66
+ reasons.push("APPROVAL_RESOURCE_SCOPE_MISMATCH");
67
+ }
68
+
69
+ const requirements = approval.approval_requirements;
70
+ if (requirements?.trace_binding === "trace" && approval.request_context.trace_id !== context.security_context.trace_id) {
71
+ reasons.push("APPROVAL_TRACE_SCOPE_MISMATCH");
72
+ }
73
+ if (requirements?.ticket_required === true && !approval.ticket_id) {
74
+ reasons.push("APPROVAL_TICKET_REQUIRED");
75
+ }
76
+ if (
77
+ requirements?.approver_roles?.length &&
78
+ (!approval.approver_role || !requirements.approver_roles.includes(approval.approver_role))
79
+ ) {
80
+ reasons.push("APPROVAL_ROLE_MISMATCH");
81
+ }
82
+ if (requirements?.single_use === true && approval.used_at) {
83
+ reasons.push("APPROVAL_ALREADY_USED");
84
+ }
85
+
86
+ return reasons;
87
+ }
88
+
89
+ export function runPolicyGuard(
90
+ input: BeforeToolCallInput,
91
+ policyVersion: string,
92
+ traceId: string,
93
+ nowIso: string,
94
+ ruleEngine: RuleEngine,
95
+ decisionEngine: DecisionEngine,
96
+ approvals: ApprovalFsm,
97
+ sensitivePathRules: SensitivePathRule[] = [],
98
+ fileRules: FileRule[] = [],
99
+ ): GuardComputation<BeforeToolCallInput> {
100
+ const securityContext = buildSecurityContext(input, policyVersion, traceId, nowIso);
101
+ const inferredLabels = inferSensitivityLabels(
102
+ input.tool_group,
103
+ input.resource_paths ?? [],
104
+ input.tool_args_summary,
105
+ sensitivePathRules,
106
+ );
107
+ const assetLabels = [...new Set([...(input.asset_labels ?? []), ...inferredLabels.assetLabels])];
108
+ const dataLabels = [...new Set([...(input.data_labels ?? []), ...inferredLabels.dataLabels])];
109
+ const mutatedPayload = {
110
+ ...input,
111
+ asset_labels: assetLabels,
112
+ data_labels: dataLabels,
113
+ security_context: securityContext,
114
+ } as BeforeToolCallInput;
115
+ const context: DecisionContext = {
116
+ actor_id: input.actor_id,
117
+ scope: input.scope,
118
+ tool_name: input.tool_name,
119
+ ...(input.tool_group !== undefined ? { tool_group: input.tool_group } : {}),
120
+ ...(input.operation !== undefined ? { operation: input.operation } : {}),
121
+ tags: [...new Set([...(input.tags ?? []), ...securityContext.tags])],
122
+ resource_scope: input.resource_scope ?? "none",
123
+ resource_paths: [...(input.resource_paths ?? [])],
124
+ ...(input.file_type !== undefined ? { file_type: input.file_type } : {}),
125
+ asset_labels: assetLabels,
126
+ data_labels: dataLabels,
127
+ trust_level: input.trust_level ?? (securityContext.untrusted ? "untrusted" : "trusted"),
128
+ ...(input.destination_type !== undefined ? { destination_type: input.destination_type } : {}),
129
+ ...(input.dest_domain !== undefined ? { dest_domain: input.dest_domain } : {}),
130
+ ...(input.dest_ip_class !== undefined ? { dest_ip_class: input.dest_ip_class } : {}),
131
+ ...(input.tool_args_summary !== undefined ? { tool_args_summary: input.tool_args_summary } : {}),
132
+ volume: { ...(input.volume ?? {}) },
133
+ security_context: securityContext
134
+ };
135
+ const matchedFileRule = matchFileRule(context.resource_paths, fileRules);
136
+ const fileRuleReasonCodes = matchedFileRule
137
+ ? matchedFileRule.reason_codes?.length
138
+ ? [...matchedFileRule.reason_codes]
139
+ : [defaultFileRuleReasonCode(matchedFileRule.decision)]
140
+ : [];
141
+
142
+ if (matchedFileRule && matchedFileRule.decision !== "challenge") {
143
+ return {
144
+ mutated_payload: mutatedPayload,
145
+ decision: matchedFileRule.decision,
146
+ decision_source: "file_rule",
147
+ reason_codes: fileRuleReasonCodes,
148
+ sanitization_actions: [],
149
+ security_context: securityContext,
150
+ };
151
+ }
152
+
153
+ if (input.approval_id) {
154
+ const approval = approvals.getApprovalStatus(input.approval_id);
155
+ if (approval?.status === "approved") {
156
+ const replayViolations = validateApprovedReplay(approval, context);
157
+ if (replayViolations.length > 0) {
158
+ return invalidApprovalResult(mutatedPayload, securityContext, approval, replayViolations);
159
+ }
160
+ if (approval.approval_requirements?.single_use === true) {
161
+ approvals.markApprovalUsed(approval.approval_id);
162
+ }
163
+ const result: GuardComputation<BeforeToolCallInput> = {
164
+ mutated_payload: mutatedPayload,
165
+ decision: "allow",
166
+ decision_source: "approval",
167
+ reason_codes: ["APPROVAL_GRANTED"],
168
+ sanitization_actions: [],
169
+ security_context: securityContext
170
+ };
171
+ result.approval = approval;
172
+ return result;
173
+ }
174
+ if (approval && approval.status !== "pending") {
175
+ const result: GuardComputation<BeforeToolCallInput> = {
176
+ mutated_payload: mutatedPayload,
177
+ decision: "block",
178
+ decision_source: "approval",
179
+ reason_codes: [`APPROVAL_${approval.status.toUpperCase()}`],
180
+ sanitization_actions: [],
181
+ security_context: securityContext
182
+ };
183
+ result.approval = approval;
184
+ return result;
185
+ }
186
+ }
187
+
188
+ const matches = matchedFileRule ? [] : ruleEngine.match(context);
189
+ const outcome = matchedFileRule
190
+ ? {
191
+ decision: matchedFileRule.decision,
192
+ decision_source: "file_rule" as const,
193
+ reason_codes: fileRuleReasonCodes,
194
+ matched_rules: [],
195
+ challenge_ttl_seconds: decisionEngine.config.defaults.approval_ttl_seconds,
196
+ }
197
+ : decisionEngine.evaluate(context, matches);
198
+
199
+ let approval: ApprovalRecord | undefined;
200
+ if (outcome.decision === "challenge") {
201
+ const requestOptions = {
202
+ reason_codes: outcome.reason_codes,
203
+ rule_ids: matchedFileRule ? [`file_rule:${matchedFileRule.id}`] : outcome.matched_rules.map((rule) => rule.rule_id),
204
+ ...(outcome.challenge_ttl_seconds !== undefined ? { ttl_seconds: outcome.challenge_ttl_seconds } : {}),
205
+ ...(outcome.approval_requirements ? { approval_requirements: outcome.approval_requirements } : {}),
206
+ };
207
+ approval = approvals.requestApproval(context, requestOptions);
208
+ }
209
+
210
+ const result: GuardComputation<BeforeToolCallInput> = {
211
+ mutated_payload: mutatedPayload,
212
+ decision: outcome.decision,
213
+ decision_source: outcome.decision_source,
214
+ reason_codes: outcome.reason_codes,
215
+ sanitization_actions: [],
216
+ security_context: securityContext
217
+ };
218
+ if (approval !== undefined) {
219
+ result.approval = approval;
220
+ }
221
+ return result;
222
+ }
@@ -0,0 +1,88 @@
1
+ import type { AfterToolCallInput, DlpFinding, GuardComputation, SanitizationAction, SchemaExpectation, SecurityContext } from "../types.ts";
2
+ import type { DlpEngine } from "../engine/dlp_engine.ts";
3
+
4
+ function buildSecurityContext(input: AfterToolCallInput, traceId: string, policyVersion: string, nowIso: string): SecurityContext {
5
+ return {
6
+ trace_id: input.security_context?.trace_id ?? traceId,
7
+ actor_id: input.actor_id,
8
+ workspace: input.workspace,
9
+ policy_version: input.security_context?.policy_version ?? policyVersion,
10
+ untrusted: input.security_context?.untrusted ?? false,
11
+ tags: [...(input.security_context?.tags ?? [])],
12
+ created_at: input.security_context?.created_at ?? nowIso
13
+ };
14
+ }
15
+
16
+ function matchesType(value: unknown, expected: SchemaExpectation["type"]): boolean {
17
+ if (expected === "array") {
18
+ return Array.isArray(value);
19
+ }
20
+ if (expected === "object") {
21
+ return value !== null && typeof value === "object" && !Array.isArray(value);
22
+ }
23
+ return typeof value === expected;
24
+ }
25
+
26
+ function validateSchema(value: unknown, schema: SchemaExpectation, path = "result"): string[] {
27
+ const errors: string[] = [];
28
+ if (!matchesType(value, schema.type)) {
29
+ errors.push(`SCHEMA_TYPE_MISMATCH:${path}`);
30
+ return errors;
31
+ }
32
+ if (schema.type === "object" && schema.required && value && typeof value === "object") {
33
+ const record = value as Record<string, unknown>;
34
+ for (const [key, child] of Object.entries(schema.required)) {
35
+ if (!(key in record)) {
36
+ errors.push(`SCHEMA_MISSING_FIELD:${path}.${key}`);
37
+ continue;
38
+ }
39
+ errors.push(...validateSchema(record[key], child, `${path}.${key}`));
40
+ }
41
+ }
42
+ return errors;
43
+ }
44
+
45
+ function findingsToActions(findings: DlpFinding[]): SanitizationAction[] {
46
+ return findings.map((finding) => ({
47
+ path: finding.path,
48
+ action: finding.action,
49
+ detail: `${finding.pattern_name}:${finding.type}`
50
+ }));
51
+ }
52
+
53
+ export function runResultGuard(
54
+ input: AfterToolCallInput,
55
+ traceId: string,
56
+ policyVersion: string,
57
+ nowIso: string,
58
+ dlpEngine: DlpEngine,
59
+ ): GuardComputation<AfterToolCallInput> {
60
+ const securityContext = buildSecurityContext(input, traceId, policyVersion, nowIso);
61
+ const schemaErrors = input.expected_schema ? validateSchema(input.result, input.expected_schema) : [];
62
+ const findings = dlpEngine.scan(input.result);
63
+ const hasSchemaErrors = schemaErrors.length > 0;
64
+ const hasFindings = findings.length > 0;
65
+ const decision =
66
+ hasSchemaErrors || dlpEngine.config.on_dlp_hit === "block"
67
+ ? hasFindings || hasSchemaErrors
68
+ ? "block"
69
+ : "allow"
70
+ : hasFindings
71
+ ? dlpEngine.config.on_dlp_hit === "warn"
72
+ ? "warn"
73
+ : "allow"
74
+ : "allow";
75
+
76
+ const sanitizedResult =
77
+ hasFindings && dlpEngine.config.on_dlp_hit === "sanitize"
78
+ ? dlpEngine.sanitize(input.result, findings, "sanitize")
79
+ : input.result;
80
+
81
+ return {
82
+ mutated_payload: { ...input, result: sanitizedResult, security_context: securityContext } as AfterToolCallInput,
83
+ decision,
84
+ reason_codes: [...schemaErrors, ...(hasFindings ? ["DLP_HIT"] : ["RESULT_OK"])],
85
+ sanitization_actions: findingsToActions(findings),
86
+ security_context: securityContext
87
+ };
88
+ }
@@ -0,0 +1,36 @@
1
+ export type SecurityClawLocale = "zh-CN" | "en";
2
+
3
+ export const DEFAULT_SECURITYCLAW_LOCALE: SecurityClawLocale = "en";
4
+
5
+ function normalizeLocaleTag(value: string): string {
6
+ return value.trim().replace(/_/g, "-").toLowerCase();
7
+ }
8
+
9
+ export function resolveSecurityClawLocale(
10
+ value: string | undefined,
11
+ fallback: SecurityClawLocale = DEFAULT_SECURITYCLAW_LOCALE,
12
+ ): SecurityClawLocale {
13
+ const normalized = value ? normalizeLocaleTag(value) : "";
14
+ if (!normalized) {
15
+ return fallback;
16
+ }
17
+ if (normalized === "zh" || normalized.startsWith("zh-")) {
18
+ return "zh-CN";
19
+ }
20
+ if (normalized === "en" || normalized.startsWith("en-")) {
21
+ return "en";
22
+ }
23
+ return fallback;
24
+ }
25
+
26
+ export function isChineseLocale(locale: SecurityClawLocale): boolean {
27
+ return locale === "zh-CN";
28
+ }
29
+
30
+ export function localeForIntl(locale: SecurityClawLocale): string {
31
+ return isChineseLocale(locale) ? "zh-CN" : "en-US";
32
+ }
33
+
34
+ export function pickLocalized(locale: SecurityClawLocale, zhText: string, enText: string): string {
35
+ return isChineseLocale(locale) ? zhText : enText;
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,255 @@
1
+ import { ConfigManager } from "./config/loader.ts";
2
+ import { ApprovalFsm } from "./engine/approval_fsm.ts";
3
+ import { DecisionEngine } from "./engine/decision_engine.ts";
4
+ import { DlpEngine } from "./engine/dlp_engine.ts";
5
+ import { RuleEngine } from "./engine/rule_engine.ts";
6
+ import { EventEmitter, HttpEventSink } from "./events/emitter.ts";
7
+ import { runContextGuard } from "./hooks/context_guard.ts";
8
+ import { runOutputGuard } from "./hooks/output_guard.ts";
9
+ import { runPersistGuard } from "./hooks/persist_guard.ts";
10
+ import { runPolicyGuard } from "./hooks/policy_guard.ts";
11
+ import { runResultGuard } from "./hooks/result_guard.ts";
12
+ import type {
13
+ AfterToolCallInput,
14
+ BeforePromptBuildInput,
15
+ BeforeToolCallInput,
16
+ EventSink,
17
+ GuardComputation,
18
+ HookName,
19
+ HookResult,
20
+ MessageSendingInput,
21
+ PluginHooks,
22
+ SecurityClawPluginOptions,
23
+ SecurityDecisionEvent,
24
+ ToolResultPersistInput
25
+ } from "./types.ts";
26
+ import { generateTraceId, nowIso, withTimeout } from "./utils.ts";
27
+
28
+ export * from "./types.ts";
29
+ export { ConfigManager } from "./config/loader.ts";
30
+ export { securityDecisionEventSchema } from "./events/schema.ts";
31
+
32
+ type PluginResult = {
33
+ hooks: PluginHooks;
34
+ approvals: ApprovalFsm;
35
+ config: ConfigManager;
36
+ events: EventEmitter;
37
+ };
38
+
39
+ type RuntimeDependencies = {
40
+ config: ReturnType<ConfigManager["getConfig"]>;
41
+ ruleEngine: RuleEngine;
42
+ decisionEngine: DecisionEngine;
43
+ dlpEngine: DlpEngine;
44
+ };
45
+
46
+ function buildEvent(
47
+ hook: HookName,
48
+ traceId: string,
49
+ result: GuardComputation,
50
+ latencyMs: number,
51
+ now: () => number,
52
+ ): SecurityDecisionEvent {
53
+ return {
54
+ schema_version: "1.0",
55
+ event_type: "SecurityDecisionEvent",
56
+ trace_id: traceId,
57
+ hook,
58
+ decision: result.decision,
59
+ reason_codes: result.reason_codes,
60
+ latency_ms: latencyMs,
61
+ ts: nowIso(now),
62
+ ...(result.decision_source !== undefined ? { decision_source: result.decision_source } : {})
63
+ };
64
+ }
65
+
66
+ async function executeGuard<TInput>(
67
+ hook: HookName,
68
+ input: TInput,
69
+ handler: () => Promise<GuardComputation<TInput>> | GuardComputation<TInput>,
70
+ dependencies: {
71
+ eventEmitter: EventEmitter;
72
+ configManager: ConfigManager;
73
+ now: () => number;
74
+ },
75
+ ): Promise<HookResult<TInput>> {
76
+ const config = dependencies.configManager.getConfig();
77
+ const controls = config.hooks[hook];
78
+ const traceId =
79
+ (input as { security_context?: { trace_id?: string }; trace_id?: string }).security_context?.trace_id ??
80
+ (input as { trace_id?: string }).trace_id ??
81
+ generateTraceId();
82
+ const startedAt = dependencies.now();
83
+
84
+ if (!controls.enabled) {
85
+ const latencyMs = dependencies.now() - startedAt;
86
+ return {
87
+ mutated_payload: input,
88
+ decision: "allow",
89
+ reason_codes: ["HOOK_DISABLED"],
90
+ sanitization_actions: [],
91
+ latency_ms: latencyMs
92
+ };
93
+ }
94
+
95
+ try {
96
+ const computation = await withTimeout(
97
+ Promise.resolve(handler()),
98
+ controls.timeout_ms,
99
+ `${hook} timed out`,
100
+ );
101
+ const latencyMs = dependencies.now() - startedAt;
102
+ const event = buildEvent(hook, traceId, computation, latencyMs, dependencies.now);
103
+ await dependencies.eventEmitter.emitSecurityEvent(event);
104
+ return {
105
+ ...computation,
106
+ latency_ms: latencyMs
107
+ };
108
+ } catch (error) {
109
+ const latencyMs = dependencies.now() - startedAt;
110
+ const decision = controls.fail_mode === "close" ? "block" : "allow";
111
+ const reason = error instanceof Error && error.message.includes("timed out") ? "HOOK_TIMEOUT" : "HOOK_ERROR";
112
+ const fallback: GuardComputation<TInput> = {
113
+ mutated_payload: input,
114
+ decision,
115
+ reason_codes: [reason],
116
+ sanitization_actions: []
117
+ };
118
+ const event = buildEvent(hook, traceId, fallback, latencyMs, dependencies.now);
119
+ await dependencies.eventEmitter.emitSecurityEvent(event);
120
+ return {
121
+ ...fallback,
122
+ latency_ms: latencyMs
123
+ };
124
+ }
125
+ }
126
+
127
+ export function createSecurityClawPlugin(options: SecurityClawPluginOptions = {}): PluginResult {
128
+ const now = options.now ?? Date.now;
129
+ const configManager = options.config
130
+ ? new ConfigManager(options.config)
131
+ : ConfigManager.fromFile(options.config_path ?? "./config/policy.default.yaml");
132
+ const initialConfig = configManager.getConfig();
133
+ const sink: EventSink | undefined =
134
+ options.event_sink ??
135
+ (initialConfig.event_sink.webhook_url
136
+ ? new HttpEventSink(initialConfig.event_sink.webhook_url, initialConfig.event_sink.timeout_ms)
137
+ : undefined);
138
+ const eventEmitter = new EventEmitter(
139
+ sink,
140
+ initialConfig.event_sink.max_buffer,
141
+ initialConfig.event_sink.retry_limit,
142
+ );
143
+ const approvals = new ApprovalFsm(now);
144
+ const traceGenerator = options.generate_trace_id ?? generateTraceId;
145
+
146
+ function buildRuntime(config: ReturnType<ConfigManager["getConfig"]>): RuntimeDependencies {
147
+ return {
148
+ config,
149
+ ruleEngine: new RuleEngine(config.policies),
150
+ decisionEngine: new DecisionEngine(config),
151
+ dlpEngine: new DlpEngine(config.dlp)
152
+ };
153
+ }
154
+
155
+ let runtime = buildRuntime(configManager.getConfig());
156
+ function getRuntime(): RuntimeDependencies {
157
+ const latest = configManager.getConfig();
158
+ if (latest !== runtime.config) {
159
+ runtime = buildRuntime(latest);
160
+ }
161
+ return runtime;
162
+ }
163
+
164
+ return {
165
+ hooks: {
166
+ before_prompt_build: (input: BeforePromptBuildInput) =>
167
+ executeGuard(
168
+ "before_prompt_build",
169
+ input,
170
+ () => {
171
+ const current = getRuntime();
172
+ return runContextGuard(
173
+ input,
174
+ current.config.policy_version,
175
+ input.trace_id ?? traceGenerator(),
176
+ nowIso(now),
177
+ );
178
+ },
179
+ { eventEmitter, configManager, now },
180
+ ),
181
+ before_tool_call: (input: BeforeToolCallInput) =>
182
+ executeGuard(
183
+ "before_tool_call",
184
+ input,
185
+ () => {
186
+ const current = getRuntime();
187
+ return runPolicyGuard(
188
+ input,
189
+ current.config.policy_version,
190
+ input.security_context?.trace_id ?? traceGenerator(),
191
+ nowIso(now),
192
+ current.ruleEngine,
193
+ current.decisionEngine,
194
+ approvals,
195
+ current.config.sensitivity.path_rules,
196
+ current.config.file_rules,
197
+ );
198
+ },
199
+ { eventEmitter, configManager, now },
200
+ ),
201
+ after_tool_call: (input: AfterToolCallInput) =>
202
+ executeGuard(
203
+ "after_tool_call",
204
+ input,
205
+ () => {
206
+ const current = getRuntime();
207
+ return runResultGuard(
208
+ input,
209
+ input.security_context?.trace_id ?? traceGenerator(),
210
+ current.config.policy_version,
211
+ nowIso(now),
212
+ current.dlpEngine,
213
+ );
214
+ },
215
+ { eventEmitter, configManager, now },
216
+ ),
217
+ tool_result_persist: (input: ToolResultPersistInput) =>
218
+ executeGuard(
219
+ "tool_result_persist",
220
+ input,
221
+ () => {
222
+ const current = getRuntime();
223
+ return runPersistGuard(
224
+ input,
225
+ input.security_context?.trace_id ?? traceGenerator(),
226
+ current.config.policy_version,
227
+ nowIso(now),
228
+ current.dlpEngine,
229
+ current.config.defaults.persist_mode,
230
+ );
231
+ },
232
+ { eventEmitter, configManager, now },
233
+ ),
234
+ message_sending: (input: MessageSendingInput) =>
235
+ executeGuard(
236
+ "message_sending",
237
+ input,
238
+ () => {
239
+ const current = getRuntime();
240
+ return runOutputGuard(
241
+ input,
242
+ input.security_context?.trace_id ?? traceGenerator(),
243
+ current.config.policy_version,
244
+ nowIso(now),
245
+ current.dlpEngine,
246
+ );
247
+ },
248
+ { eventEmitter, configManager, now },
249
+ )
250
+ },
251
+ approvals,
252
+ config: configManager,
253
+ events: eventEmitter
254
+ };
255
+ }