@x-otto/hooks 0.0.1-alpha.0

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.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @x-otto/hooks
2
+
3
+ ## 模块定位
4
+
5
+ 提供 Agent 生命周期各阶段可插拔的拦截和变换机制,内置权限策略体系。
6
+
7
+ ## 核心功能
8
+
9
+ | 模块 | 功能 |
10
+ | ------------------------ | ------------------------------------------------------------- |
11
+ | HookRegistry | 25 种时机的 Hook 注册、优先级排序、执行引擎 |
12
+ | PermissionPolicyRegistry | 权限策略注册与评估(allow/deny/escalate) |
13
+ | PermissionAuditLog | 权限决策审计轨迹 |
14
+ | PermissionGuardHook | 权限 -> tool:guard 集成 |
15
+ | 内置策略 | FILE_GUARD / DESTRUCTIVE_COMMAND / 模式策略 / 冻结 / 黑白名单 |
16
+ | 预设 | default / strict / minimal / none 四种权限模式 |
17
+ | 13 个内置 Hooks | 会话 / 工具防护 / 变换 / 质量 / 流 / 压缩 |
18
+
19
+ ## Hook 时机(25 种,HOOK_NODES 单源)
20
+
21
+ **Interceptor(8,可改写 output)**:`chat.message.before` / `chat.message.after` / `tool.execute.before` / `tool.execute.after` / `messages.transform` / `chat.params` / `memory.prune.before` / `system.prompt.transform`
22
+
23
+ **Observer(17,仅通知)**:`session.created` / `session.restored` / `session.deleted` / `session.idle` / `session.error` / `stream.start` / `stream.end` / `compaction.before` / `compaction.after` / `process.spawned` / `process.exited` / `task.created` / `task.started` / `task.completed` / `task.failed` / `task.cancelled` / `notification`
24
+
25
+ ## 安装
26
+
27
+ ```bash
28
+ pnpm add @x-otto/hooks
29
+ ```
30
+
31
+ ## 快速开始
32
+
33
+ ```ts
34
+ import { createHookRegistry, defineHook } from '@x-otto/hooks'
35
+
36
+ const registry = createHookRegistry({ preset: 'default' })
37
+
38
+ // 注册一个工具执行权限 guard
39
+ registry.register(
40
+ defineHook({
41
+ name: 'file-write-guard',
42
+ timing: 'tool.execute.before',
43
+ priority: 0,
44
+ handle: (input, output) => {
45
+ if (input.toolName === 'write' && input.params.path?.includes('/etc/')) {
46
+ output.decision = 'deny'
47
+ output.decisionReason = 'Writing to /etc/ is blocked'
48
+ }
49
+ },
50
+ }),
51
+ )
52
+ ```
53
+
54
+ ## 目录概览
55
+
56
+ ```
57
+ src/
58
+ types.ts # 核心类型
59
+ hook-registry.ts # Hook 注册与执行
60
+ permission/
61
+ permission-policy-registry.ts
62
+ permission-audit-log.ts
63
+ permission-guard-hook.ts
64
+ builtin-policies.ts
65
+ presets.ts
66
+ builtin-hooks/
67
+ session/ # 6 个会话 Hook
68
+ tool-guard/ # 6 个工具防护 Hook
69
+ transform/ # 4 个变换 Hook
70
+ quality/ # 3 个质量 Hook
71
+ stream/ # 2 个流 Hook
72
+ compaction/ # 2 个压缩 Hook
73
+ background/ # 2 个后台 Hook
74
+ index.ts
75
+ tests/ # 9 个测试文件
76
+ ```
77
+
78
+ ## 开发命令
79
+
80
+ ```bash
81
+ pnpm --filter @x-otto/hooks build
82
+ pnpm --filter @x-otto/hooks typecheck
83
+ pnpm --filter @x-otto/hooks clean
84
+ ```
85
+
86
+ ## 关联包
87
+
88
+ `@x-otto/shared`、`@x-otto/env`
@@ -0,0 +1,547 @@
1
+ import { ChatMessageInput, ChatMessageOutput, ChatParamsInput, ChatParamsOutput, HookHandle, HookInput, HookOutput, HookPayloadMap, HookTiming, InterceptorTiming, MessagesTransformInput, MessagesTransformOutput, ObserverTiming, PermissionMode, PermissionPolicySetting, PermissionRuleConfig, RuleEffect, RuleEffect as RuleEffect$1, SessionEventInput, SystemPromptTransformInput, SystemPromptTransformOutput, ToolExecuteAfterInput, ToolExecuteAfterOutput, ToolExecuteBeforeInput, ToolExecuteBeforeOutput } from "@x-otto/hook-contracts";
2
+
3
+ //#region src/core/hook-state.d.ts
4
+ interface StreamMetrics {
5
+ startTime: number;
6
+ model: string;
7
+ requestCount: number;
8
+ lastStopReason: string;
9
+ totalDurationMs: number;
10
+ }
11
+ interface CompactionStats {
12
+ compactionCount: number;
13
+ totalCompacted: number;
14
+ lastCompactionTime: number;
15
+ }
16
+ interface ToolErrorRecord {
17
+ toolName: string;
18
+ errorCount: number;
19
+ lastErrorTime: number;
20
+ consecutiveErrors: number;
21
+ }
22
+ declare class HookState {
23
+ readonly streamMetrics: Map<string, StreamMetrics>;
24
+ readonly compactionStats: Map<string, CompactionStats>;
25
+ readonly toolErrors: Map<string, Map<string, ToolErrorRecord>>;
26
+ /** session.deleted 统一驱逐:registry 生命周期调用,杜绝 per-session 状态泄漏。 */
27
+ clearSession(sessionId: string): void;
28
+ }
29
+ //#endregion
30
+ //#region src/hook-spec.d.ts
31
+ type BaseRuntimeHook = {
32
+ name: string;
33
+ timing: HookTiming;
34
+ priority: number;
35
+ enabled: boolean;
36
+ };
37
+ type ObserverRuntimeHook = BaseRuntimeHook & {
38
+ kind: 'observer';
39
+ handle: (input: unknown) => void | Promise<void>;
40
+ };
41
+ type InterceptorRuntimeHook = BaseRuntimeHook & {
42
+ kind: 'interceptor';
43
+ handle: (input: unknown, output: unknown) => void | Promise<void>;
44
+ };
45
+ type HookSpec = ObserverRuntimeHook | InterceptorRuntimeHook;
46
+ /**
47
+ * 所有 as 强转集中在此处:HookHandle<T> 因函数参数逆变无法自动赋给 (unknown) => void。
48
+ * factory 函数通过 timing 字面量驱动 handle 的类型推断,调用侧零 cast。
49
+ */
50
+ declare function defineHook<T extends HookTiming>(config: {
51
+ name: string;
52
+ timing: T;
53
+ handle: HookHandle<T>;
54
+ priority?: number;
55
+ enabled?: boolean;
56
+ }): HookSpec;
57
+ //#endregion
58
+ //#region src/hook-registry.d.ts
59
+ interface RegisteredHookInfo {
60
+ name: string;
61
+ timing: HookTiming;
62
+ priority: number;
63
+ enabled: boolean;
64
+ }
65
+ type HookPreset = 'default' | 'strict' | 'minimal' | 'none';
66
+ interface HookRegistryOptions {
67
+ preset?: HookPreset;
68
+ /** RFC-153 D1:interceptor/observer hook 超时时的旁路通知(可选,宿主层注入)。
69
+ * HookRegistry 本身不认识"插件"概念,只把 name/timing/timeoutMs 透传给调用方决定如何处理
70
+ * (如写入 PluginHealthTracker)——保持 packages/hooks 对插件生命周期零认知(规则 7)。 */
71
+ onHookTimeout?: (info: {
72
+ hookName: string;
73
+ timing: string;
74
+ timeoutMs: number;
75
+ }) => void;
76
+ }
77
+ declare class HookRegistry {
78
+ private readonly hooks;
79
+ private readonly disabled;
80
+ private readonly onHookTimeout?;
81
+ readonly state: HookState;
82
+ constructor(options?: HookRegistryOptions);
83
+ register(spec: HookSpec): void;
84
+ registerAll(specs: HookSpec[]): void;
85
+ on<T extends HookTiming>(timing: T, name: string, handle: HookHandle<T>, priority?: number): this;
86
+ has(name: string): boolean;
87
+ unregister(name: string): boolean;
88
+ disable(name: string): void;
89
+ enable(name: string): void;
90
+ getRegistered(timing?: HookTiming): RegisteredHookInfo[];
91
+ execute<T extends InterceptorTiming>(timing: T, input: HookInput<T>, output: HookOutput<T>): Promise<void>;
92
+ /**
93
+ * RFC-153 D1:interceptor hooks 挂起兜底——`execute()` 循环内此前是裸 `await hook.handle(...)`,
94
+ * 挂起(无限循环/无限 fetch/死锁)的 hook 会永久阻塞整个 agent turn。复用 observer 路径
95
+ * (`runObserverHookWithTimeout`)已验证的 `Promise.race` + `unref()` 模式,阈值改用两路径共享的
96
+ * `HOOK_TIMEOUT_MS` 常量(规则 1:超时不等于中止——不承诺真正终止 hook 内部执行)。
97
+ * 超时命中时通过 `onHookTimeout` 回调向上冒泡(规则 7:只携带领域无关字段,`packages/hooks`
98
+ * 不认识"插件"概念),调用方(`packages/coding`)负责解析 hookName 里的 pluginId 前缀。
99
+ */
100
+ private runInterceptorWithTimeout;
101
+ /**
102
+ * RFC-143 M0:observer hook 挂起兜底。`emit()` 此前的 try/catch 只吞同步抛错和 rejected
103
+ * promise,不吞「handler 挂起」(无限循环/无限 fetch/死锁)——`await hook.handle(input)` 会
104
+ * 无限期阻塞调用方(如用户显式 `removeSession`、task 生命周期判定、流式启动)。
105
+ * 超时阈值使用与 interceptor 路径(`runInterceptorWithTimeout`)共享的模块级常量
106
+ * `HOOK_TIMEOUT_MS`(RFC-153 D1:避免两条路径各自维护一份可能漂移的数字,此前本方法
107
+ * 独立持有的 `EMIT_TIMEOUT_MS` 已废弃合并)。`unref()` 避免这个定时器阻塞进程退出。
108
+ */
109
+ private runObserverHookWithTimeout;
110
+ /**
111
+ * RFC-143 R2a:`session.error` timing 的 input 契约上携带原始 `Error` 对象
112
+ * (`HookPayloadMap['session.error'].input.error: Error`,见 hook-contracts/timings.ts)。
113
+ * SDK 层(`module-wiring.ts`)已对 definePlugin 消费路径做 `error.message` 转换,但那只是
114
+ * 单一消费方的自觉行为——emit() 是所有 observer hook(含非 SDK 注册路径,如未来的引擎内部
115
+ * 钩子/测试工具)的唯一入口,必须在这里做结构式 sanitize,防止任何消费方读到含栈追踪/敏感
116
+ * 路径信息的原始 Error 对象。sanitize 后的 input 只对 `session.error` 生效,其余 timing 原样传递。
117
+ */
118
+ private sanitizeEmitInput;
119
+ emit<T extends ObserverTiming>(timing: T, input: HookInput<T>): Promise<void>;
120
+ clear(): void;
121
+ private getSortedInterceptorHooks;
122
+ private getSortedObserverHooks;
123
+ private applyPreset;
124
+ }
125
+ declare function createHookRegistry(options?: HookRegistryOptions): HookRegistry;
126
+ //#endregion
127
+ //#region src/core/session/first-message-variant.d.ts
128
+ declare function createFirstMessageVariantHook(): HookSpec;
129
+ //#endregion
130
+ //#region src/core/tool-guard/label-truncator.d.ts
131
+ declare function createLabelTruncatorHook(): HookSpec;
132
+ //#endregion
133
+ //#region src/core/tool-guard/output-truncation.d.ts
134
+ declare function createOutputTruncationHook(maxOutputSize?: number): HookSpec;
135
+ //#endregion
136
+ //#region src/core/tool-guard/tool-error-tracker.d.ts
137
+ interface ToolErrorTrackerConfig {
138
+ /** 默认 5。 */
139
+ circuitBreakerThreshold?: number;
140
+ /** 默认 120_000 ms。 */
141
+ decayWindowMs?: number;
142
+ }
143
+ declare function createToolErrorTrackerHook(state: HookState, config?: ToolErrorTrackerConfig): HookSpec;
144
+ //#endregion
145
+ //#region src/core/transform/thinking-validator.d.ts
146
+ declare function createThinkingValidatorHook(): HookSpec;
147
+ //#endregion
148
+ //#region src/core/transform/anthropic-effort.d.ts
149
+ declare function createAnthropicEffortHook(): HookSpec;
150
+ //#endregion
151
+ //#region src/core/transform/token-budget.d.ts
152
+ interface TokenBudgetConfig {
153
+ /** 默认 16384。 */
154
+ maxOutputTokens?: number;
155
+ }
156
+ declare function createTokenBudgetHook(config?: TokenBudgetConfig): HookSpec;
157
+ //#endregion
158
+ //#region src/core/quality/comment-checker.d.ts
159
+ declare function createCommentCheckerHook(): HookSpec;
160
+ //#endregion
161
+ //#region src/core/stream/stream-metrics.d.ts
162
+ declare function createStreamMetricsHook(state: HookState): HookSpec;
163
+ declare function createStreamEndMetricsHook(state: HookState): HookSpec;
164
+ //#endregion
165
+ //#region src/core/compaction/compaction-logger.d.ts
166
+ declare function createCompactionLoggerHook(state: HookState): HookSpec;
167
+ declare function createCompactionAfterHook(state: HookState): HookSpec;
168
+ //#endregion
169
+ //#region src/core/edit-files-panel.d.ts
170
+ /** 编辑文件记录的内部类型(hooks 层使用,不耦合 tui 的 EditedFileEntry)。 */
171
+ interface EditFileInfo {
172
+ path: string;
173
+ operation: 'edit' | 'write' | 'delete';
174
+ oldContent?: string;
175
+ newContent?: string;
176
+ addedLines: number;
177
+ removedLines: number;
178
+ timestamp: number;
179
+ toolCallId: string;
180
+ }
181
+ /** 文件修改回调(CLI 层负责适配到 TuiHandle.addEditedFile) */
182
+ type EditFileCallback = (info: EditFileInfo) => void;
183
+ interface EditFilesPanelHookOptions {
184
+ onFileEdited?: EditFileCallback;
185
+ /** 延迟获取回调——用于注册时 TUI 尚未就绪的场景(取值时机=emit 时而非 register 时)。优先级高于 onFileEdited。 */
186
+ getOnFileEdited?: () => EditFileCallback | undefined;
187
+ dedupCache?: Set<string> | null;
188
+ }
189
+ /**
190
+ * 创建 hook spec,注册后在每次 edit/write 工具执行完成后累积回写 TUI。
191
+ *
192
+ * 用法:
193
+ * const hook = createEditFilesPanelHook({
194
+ * onFileEdited: (info) => tuiHandle.addEditedFile({
195
+ * ...info,
196
+ * addedLines: info.addedLines,
197
+ * removedLines: info.removedLines,
198
+ * turnId: currentTurnId,
199
+ * }),
200
+ * })
201
+ * hookRegistry.register(hook)
202
+ */
203
+ declare function createEditFilesPanelHook(options?: EditFilesPanelHookOptions): HookSpec;
204
+ //#endregion
205
+ //#region src/core/permission/types.d.ts
206
+ interface PolicyScope {
207
+ /** 适用的 agent 名称, '*' 或缺省表示所有 */
208
+ agents?: string[];
209
+ sessions?: string[];
210
+ }
211
+ interface PermissionContext {
212
+ timing: HookTiming;
213
+ toolName: string;
214
+ args: Record<string, unknown>;
215
+ agentName: string;
216
+ sessionId: string;
217
+ filePath?: string;
218
+ metadata: Record<string, unknown>;
219
+ }
220
+ interface RuleMatch {
221
+ tools?: string[];
222
+ paths?: RegExp[];
223
+ /** 必须同步。 */
224
+ condition?: (context: PermissionContext) => boolean;
225
+ }
226
+ interface PermissionRule {
227
+ name: string;
228
+ effect: RuleEffect$1;
229
+ match: RuleMatch;
230
+ denyReason?: string;
231
+ askPrompt?: string;
232
+ /**
233
+ * 兜底规则(C1/RFC-016):true 表示本规则仅在**没有任何非兜底规则命中**时生效
234
+ * (如 strict 的 deny-unmatched、readonly 的 deny-writes 这类 match:{} 默认拒绝)。
235
+ * 在 deny-wins 评估下,兜底 deny 不得盖过显式 allow(否则 strict/readonly 自破)。
236
+ */
237
+ fallthrough?: boolean;
238
+ }
239
+ interface PermissionPolicy {
240
+ name: string;
241
+ priority: number;
242
+ enabled: boolean;
243
+ scope: PolicyScope;
244
+ rules: PermissionRule[];
245
+ /**
246
+ * 安全护栏标记(C1/RFC-016):true 表示本策略的 ask/deny 是安全护栏
247
+ * (受保护路径、破坏性命令),alwaysAllow 绝不升级其 ask,bypass 也不能盖过。
248
+ * 缺省 false(mode/user 策略的 ask 可被 alwaysAllow 升级)。
249
+ */
250
+ safety?: boolean;
251
+ }
252
+ interface PermissionDecision {
253
+ effect: RuleEffect$1;
254
+ policyName: string;
255
+ ruleName: string;
256
+ reason?: string;
257
+ timestamp: number;
258
+ evaluatedPolicies: number;
259
+ /**
260
+ * 命中策略的安全护栏位(透传自 PermissionPolicy.safety)——HITL 风险等级单一真源(RFC-077 M3)。
261
+ * 取代 riskFromDecision 靠正则匹配 policyName 子串(destructive|credential|file-guard)反推风险的
262
+ * 字符串考古:自定义安全策略名不含三魔词时不再被误判为 medium。仅 ask/deny(经 pick)携带;
263
+ * allow/默认放行不参与风险评估故可空。
264
+ */
265
+ safety?: boolean;
266
+ }
267
+ interface PermissionAuditEntry {
268
+ timestamp: number;
269
+ sessionId: string;
270
+ agentName: string;
271
+ toolName: string;
272
+ filePath?: string;
273
+ decision: PermissionDecision;
274
+ }
275
+ //#endregion
276
+ //#region src/core/permission/builtin-policies.d.ts
277
+ interface PermissionToolDescriptor {
278
+ name: string;
279
+ readonly?: boolean;
280
+ metadata?: {
281
+ category?: string;
282
+ };
283
+ }
284
+ interface PermissionToolSets {
285
+ readonlyTools: Set<string>;
286
+ writeTools: Set<string>;
287
+ fileWriteTools: Set<string>;
288
+ }
289
+ interface PermissionToolSetsInput {
290
+ readonlyTools?: Iterable<string>;
291
+ writeTools?: Iterable<string>;
292
+ fileWriteTools?: Iterable<string>;
293
+ }
294
+ declare function createPermissionToolSetsFromRegistry(tools: Iterable<PermissionToolDescriptor>): PermissionToolSets;
295
+ declare function createFileGuardPolicy(toolSets?: PermissionToolSetsInput): PermissionPolicy;
296
+ declare const DESTRUCTIVE_COMMAND_POLICY: PermissionPolicy;
297
+ declare const CREDENTIAL_READ_POLICY: PermissionPolicy;
298
+ declare function createOttoStateDomainPolicy(toolSets?: PermissionToolSetsInput): PermissionPolicy;
299
+ declare function createEgressAllowlistPolicy(allowedHosts: readonly string[]): PermissionPolicy;
300
+ declare function createDisabledToolsPolicy(disabledTools: string[]): PermissionPolicy;
301
+ declare function createPermissionModePolicy(mode: PermissionMode, toolSets?: PermissionToolSetsInput): PermissionPolicy;
302
+ //#endregion
303
+ //#region src/core/permission/register-builtin.d.ts
304
+ interface RegisterBuiltinPoliciesOptions {
305
+ toolSets?: PermissionToolSetsInput;
306
+ }
307
+ //#endregion
308
+ //#region src/core/permission/policy-registry.d.ts
309
+ declare class PermissionPolicyRegistry {
310
+ private policies;
311
+ /**
312
+ * 项目级「始终允许」的工具名(R8-B base)。语义:把该工具被评估出的 `ask` 升级为 `allow`,
313
+ * 但绝不把 `deny` 改为 allow——安全 deny(受保护路径/破坏性命令)始终优先。
314
+ * 来源 .otto/config.json,对工作区所有会话生效(手动配置的常驻放行基线)。
315
+ */
316
+ private alwaysAllow;
317
+ /**
318
+ * 会话级「始终允许」(弹窗里点「始终允许」的运行时授权):按 sessionId 隔离,
319
+ * 跟随会话持久化(落盘进 session 记录,--continue 时回灌),互不串味——
320
+ * 一次性的信任决策不再泄漏到项目其它会话/永久污染 config(修旧 R8-B 全局泄漏)。
321
+ */
322
+ private sessionAlwaysAllow;
323
+ setAlwaysAllow(tools: readonly string[]): void;
324
+ addAlwaysAllow(tool: string): void;
325
+ getAlwaysAllow(): string[];
326
+ /** 整体替换某会话的 always-allow 集合(会话恢复/回灌用)。空集合=移除该会话条目。 */
327
+ setSessionAlwaysAllow(sessionId: string, tools: readonly string[]): void;
328
+ addSessionAlwaysAllow(sessionId: string, tool: string): void;
329
+ /** 不含项目级基线。 */
330
+ getSessionAlwaysAllow(sessionId: string): string[];
331
+ clearSessionAlwaysAllow(sessionId: string): void;
332
+ register(policy: PermissionPolicy): void;
333
+ registerAll(policies: PermissionPolicy[]): void;
334
+ unregister(name: string): boolean;
335
+ has(name: string): boolean;
336
+ /**
337
+ * collect-all / deny-wins(C1/RFC-016,收集全部命中规则后按 effect 优先级裁决):
338
+ * 收集所有命中规则,决策优先级 deny > ask > allow,**与命中/优先级顺序无关**——
339
+ * priority 仅用于在同 effect 内择 reason。这样 `bypass` 的 allow-all 不再能盖过
340
+ * 安全护栏的 deny/ask(first-match 旧语义的倒置缺口)。
341
+ *
342
+ * alwaysAllow(R8-B):仅升级**非安全护栏**的 ask→allow;安全护栏(file-guard/
343
+ * destructive-guard,policy.safety=true)的 ask/deny 绝不被升级或盖过。
344
+ */
345
+ evaluate(context: PermissionContext): PermissionDecision;
346
+ getEffective(agentName: string): PermissionPolicy[];
347
+ getAll(): PermissionPolicy[];
348
+ clear(): void;
349
+ }
350
+ declare function createPermissionRegistry(options?: RegisterBuiltinPoliciesOptions): PermissionPolicyRegistry;
351
+ declare function compilePolicyFromSetting(setting: PermissionPolicySetting): PermissionPolicy;
352
+ //#endregion
353
+ //#region src/core/permission/audit-log.d.ts
354
+ declare class PermissionAuditLog {
355
+ private entries;
356
+ private readonly maxEntries;
357
+ constructor(maxEntries?: number);
358
+ record(context: PermissionContext, decision: PermissionDecision): void;
359
+ getEntries(filter?: {
360
+ sessionId?: string;
361
+ effect?: RuleEffect;
362
+ }): PermissionAuditEntry[];
363
+ getDenyCount(sessionId?: string): number;
364
+ getAskCount(sessionId?: string): number;
365
+ clear(sessionId?: string): void;
366
+ get size(): number;
367
+ }
368
+ //#endregion
369
+ //#region src/core/permission/permission-guard.d.ts
370
+ declare class PermissionGuardHook {
371
+ readonly name = "permission-guard";
372
+ readonly timing: "tool.execute.before";
373
+ readonly priority = 5;
374
+ enabled: boolean;
375
+ private readonly registry;
376
+ private readonly auditLog?;
377
+ constructor(registry: PermissionPolicyRegistry, auditLog?: PermissionAuditLog);
378
+ handle: (input: ToolExecuteBeforeInput, output: ToolExecuteBeforeOutput) => void;
379
+ }
380
+ declare function createPermissionGuardHook(registry: PermissionPolicyRegistry, auditLog?: PermissionAuditLog): HookSpec;
381
+ //#endregion
382
+ //#region src/core/permission/baseline-provisioning.d.ts
383
+ /**
384
+ * RFC-077 M3-B:bare-runtime(无工具注册表)的基线 `fileWriteTools`——`createFileGuardPolicy`
385
+ * 的 protect-system-paths 规则按 `tools: [...fileWriteTools]` 过滤,无注册表派生集时须给默认,
386
+ * 否则空集 → 该规则匹配不到任何工具 → protected-path 写不被拦。
387
+ *
388
+ * 与 standalone `createFileGuardHook` 的 `WRITE_TOOLS` 同源(canonical otto 文件写工具);bash 非
389
+ * path-based 故不入 fileWriteTools(其命令面由 destructive/credential 命令策略覆盖)。**宿主有工具
390
+ * 注册表时经 `createPermissionToolSetsFromRegistry` 派生集覆盖本默认**(coding 走该路径)。
391
+ */
392
+ declare const BASELINE_PERMISSION_TOOL_SETS: PermissionToolSetsInput;
393
+ /**
394
+ * RFC-077 M3-B(D6 §4.11「安全护栏先 provision 再删」):把权限策略引擎 + 基线安全策略
395
+ * (file-guard / credential / destructive,经 `createPermissionRegistry` 注册)下沉为 bare-runtime
396
+ * 的**默认提供**——注册 permission-guard hook(`tool.execute.before`),让无宿主 PermissionController
397
+ * 的裸运行时也 safe-by-default(安全是引擎核心能力,非宿主可选项)。
398
+ *
399
+ * **幂等**:宿主(coding)已注册 permission-guard 时直接返回,绝不双注册——bare 分支与宿主
400
+ * provision 二选一(createAgentRuntime 仅在自建 hookRegistry、未被宿主注入时调用本函数)。
401
+ *
402
+ * @returns 新建的基线 registry(宿主已 provision 时返回 undefined)。
403
+ */
404
+ declare function provisionBaselineSecurity(hookRegistry: HookRegistry): PermissionPolicyRegistry | undefined;
405
+ //#endregion
406
+ //#region src/core/permission/always-allow.d.ts
407
+ /**
408
+ * 条目是否格式良好——供 `/permissions add` 校验,拒绝静默持久化危险/无意义条目。
409
+ * 合法:`tool`(整工具)或 `tool(非空 content)`。
410
+ * 非法:空串、空括号 `tool()`(读法似"无参"实为整工具,塌缩陷阱,RFC-073 P3)、
411
+ * 括号不平衡/含元字符的垃圾(`bash(((`、`a b`)。
412
+ */
413
+ declare function isWellFormedAllowEntry(raw: string): boolean;
414
+ /**
415
+ * 为「始终允许」按钮派生一个 bash 命令前缀(程序名 + 子命令两段),供 UI 显式呈现 +
416
+ * 持久化为 `bash(<prefix>)`。复合/混淆/env 赋值命令返回 null → 调用方回退整工具放行
417
+ * (此时安全护栏仍 bypass-immune,不会因整工具放行而漏过危险叶子)。
418
+ *
419
+ * 例:`git status -s`→`git status`、`npm run build`→`npm run`、`ls -la`→`ls`、
420
+ * `cd /x && git status`→`git status`、`a && b`/`$()`/`FOO=1 cmd`→null。
421
+ */
422
+ declare function bashAlwaysAllowPrefix(command: string): string | null;
423
+ //#endregion
424
+ //#region src/core/permission/command-canonicalize.d.ts
425
+ /**
426
+ * 从一条 bash 命令启发式提取**写入目标路径**(M4 Phase 2 前置 / C2 加固)。
427
+ *
428
+ * bash 无 pathParams,沙箱 assess 看不到 shell 写目标 → 默认开会让越界 bash 写硬失败。
429
+ * 本函数解析每个子片段的写向量,喂给 assess 以便升级为 ask 而非硬失败:
430
+ * - 重定向 `> f` `>> f` `2> f` `&> f` `>| f`
431
+ * - `tee [-a] f...`、`cp/mv/install/ln ... dst`、`dd of=f`、`touch/rm/rmdir/mkdir f...`
432
+ * 启发式(非 tree-sitter):覆盖常见形态;复杂引用/变量间接漏网者仍由沙箱兜底(硬失败,
433
+ * 不弱于现状)。返回去重路径(相对/绝对原样,由 assess 解析)。
434
+ */
435
+ declare function extractWriteTargets(command: string): string[];
436
+ //#endregion
437
+ //#region src/sandbox-safety.d.ts
438
+ type SandboxVerdict = 'auto-allow' | 'ask' | 'deny';
439
+ interface SandboxAssessment {
440
+ verdict: SandboxVerdict;
441
+ reason?: string;
442
+ }
443
+ interface SandboxOp {
444
+ writePaths?: readonly string[];
445
+ network?: boolean;
446
+ }
447
+ interface SandboxAssessContext {
448
+ mode: PermissionMode;
449
+ projectRoot: string;
450
+ writableRoots: readonly string[];
451
+ deniedRoots: readonly string[];
452
+ networkAllowed: boolean;
453
+ /**
454
+ * bash 沙箱越界自动放行开关(opt-in,默认 false,见 SandboxConfig.autoAllowBashIfSandboxed)。
455
+ * 本字段只是透传给 sandbox-guard hook 消费——assessSandboxSafety 本身不读取它(保持纯函数
456
+ * 决策矩阵不变),由 sandbox-guard 在拿到 'ask' verdict 后结合此字段 + 权限层已有裁决
457
+ * (output.decision)判断是否降级为自动放行。
458
+ */
459
+ autoAllowBashIfSandboxed?: boolean;
460
+ }
461
+ declare function assessSandboxSafety(op: SandboxOp, ctx: SandboxAssessContext): SandboxAssessment;
462
+ declare function defaultDeniedRoots(workspaceDir: string, homeDir: string): string[];
463
+ /**
464
+ * 默认可写根:projectRoot + os.tmpdir() + 字面 `/tmp`。
465
+ *
466
+ * 参照 codex(openai/codex codex-rs/protocol/src/permissions.rs
467
+ * legacy_runtime_file_system_policy_for_cwd):SlashTmp 与 Tmpdir 是两个独立的默认可写
468
+ * special path,同时生效(`exclude_slash_tmp`/`exclude_tmpdir_env_var` 默认均 false)。
469
+ * macOS 上 `os.tmpdir()` 落在 `/var/folders/...`(进程级临时目录),与用户/shell 习惯手写的
470
+ * 字面 `/tmp`(→ 真实路径 `/private/tmp`,全系统共享临时目录)是两棵不同的目录树——只信
471
+ * os.tmpdir() 会让 `mkdir -p /tmp/xxx` 这类零风险操作被误判越界触发审批(见 sandbox-guard
472
+ * 越界升级事故复盘)。underAny() 的 realpath 归一化解决的是"同一路径不同书写形式",本条解决
473
+ * 的是"两个本就不同的物理临时目录都该被信任"。
474
+ */
475
+ declare function computeSandboxRoots(mode: PermissionMode, projectRoot: string, tmpDir: string, extra?: readonly string[]): string[];
476
+ //#endregion
477
+ //#region src/observability/failure-signals.d.ts
478
+ type FailureSignalKind = 'tool-error' | 'permission-denied';
479
+ interface FailureSignal {
480
+ kind: FailureSignalKind;
481
+ /** 聚合签名(如 toolName)——同签名复发即累加。 */
482
+ signature: string;
483
+ count: number;
484
+ firstSeen: number;
485
+ lastSeen: number;
486
+ /** 最近一次的简短细节(如错误首行,截断)。 */
487
+ detail?: string;
488
+ }
489
+ /** 跨会话失败信号存储:按 (kind,signature) 聚合计数。诊断据此只认硬信号。 */
490
+ declare class FailureSignalStore {
491
+ private readonly signals;
492
+ private readonly clock;
493
+ constructor(clock?: () => number);
494
+ record(kind: FailureSignalKind, signature: string, detail?: string): void;
495
+ /** 复发达阈值(默认 ≥3)的信号,按计数降序——诊断只在这些硬信号上提案。 */
496
+ recurring(minCount?: number): FailureSignal[];
497
+ all(): FailureSignal[];
498
+ clear(): void;
499
+ }
500
+ /** RFC-026 阶段3:tool.execute.after 把工具 isError 记成结构化失败信号。 */
501
+ declare function createFailureSignalHook(store: FailureSignalStore): HookSpec;
502
+ /**
503
+ * RFC-067 M121-04a:把**权限拒绝**记成 `permission-denied` 失败信号(补 FailureSignalKind 的另一半,
504
+ * 此前从无 caller 记录此 kind)。
505
+ *
506
+ * 为何在 tool.execute.before:denied 工具在 tool-executor 早返(errorKind:'permission'),**绕过
507
+ * tool.execute.after**,故 after-hook(createFailureSignalHook)看不到它。权限/沙箱守卫在 before 层
508
+ * 设 `output.decision='deny'`(如 sandbox-guard pri 6),所有 before-hook 跑完才判 deny 裁决——故本
509
+ * recorder 用**晚 priority(90)**确保跑在守卫之后、读到累积的 decision。
510
+ */
511
+ declare function createPermissionDenyHook(store: FailureSignalStore): HookSpec;
512
+ //#endregion
513
+ //#region src/observability/diagnoser.d.ts
514
+ interface Proposal {
515
+ /** 稳定 id(= kind:signature,便于去重)。 */
516
+ id: string;
517
+ signature: string;
518
+ kind: 'investigate-fix';
519
+ /** 改进指令(L1:用户审后可 /job 执行;诊断器**不**自动跑)。 */
520
+ directive: string;
521
+ /** 来源证据(provenance):哪个信号、复发几次、细节。 */
522
+ evidence: {
523
+ signalKind: FailureSignal['kind'];
524
+ count: number;
525
+ detail?: string;
526
+ };
527
+ createdAt: number;
528
+ }
529
+ interface DiagnoserConfig {
530
+ /** 复发阈值(信号 count ≥ 此才提案;默认 3)。 */
531
+ minCount?: number;
532
+ clock?: () => number;
533
+ }
534
+ declare class Diagnoser {
535
+ private readonly signals;
536
+ private readonly proposed;
537
+ private readonly minCount;
538
+ private readonly clock;
539
+ constructor(signals: FailureSignalStore, config?: DiagnoserConfig);
540
+ /** 扫硬信号(recurring ≥ minCount)→ **新**提案(已提过的签名跳过,防风暴)。 */
541
+ propose(): Proposal[];
542
+ /** 重置去重(用户处理完想重新评估时)。 */
543
+ reset(): void;
544
+ }
545
+ //#endregion
546
+ export { BASELINE_PERMISSION_TOOL_SETS, CREDENTIAL_READ_POLICY, type ChatMessageInput, type ChatMessageOutput, type ChatParamsInput, type ChatParamsOutput, type CompactionStats, DESTRUCTIVE_COMMAND_POLICY, Diagnoser, type DiagnoserConfig, type EditFileCallback, type EditFileInfo, type EditFilesPanelHookOptions, type FailureSignal, type FailureSignalKind, FailureSignalStore, type HookHandle, type HookInput, type HookOutput, type HookPayloadMap, type HookPreset, HookRegistry, type HookRegistryOptions, type HookSpec, HookState, type HookTiming, type InterceptorTiming, type MessagesTransformInput, type MessagesTransformOutput, type ObserverTiming, type PermissionAuditEntry, PermissionAuditLog, type PermissionContext, type PermissionDecision, PermissionGuardHook, type PermissionMode, type PermissionPolicy, PermissionPolicyRegistry, type PermissionPolicySetting, type PermissionRule, type PermissionRuleConfig, type PermissionToolDescriptor, type PermissionToolSets, type PermissionToolSetsInput, type PolicyScope, type Proposal, type RuleEffect, type RuleMatch, type SandboxAssessContext, type SandboxAssessment, type SandboxOp, type SandboxVerdict, type SessionEventInput, type StreamMetrics, type SystemPromptTransformInput, type SystemPromptTransformOutput, type TokenBudgetConfig, type ToolErrorRecord, type ToolErrorTrackerConfig, type ToolExecuteAfterInput, type ToolExecuteAfterOutput, type ToolExecuteBeforeInput, type ToolExecuteBeforeOutput, assessSandboxSafety, bashAlwaysAllowPrefix, compilePolicyFromSetting, computeSandboxRoots, createAnthropicEffortHook, createCommentCheckerHook, createCompactionAfterHook, createCompactionLoggerHook, createDisabledToolsPolicy, createEditFilesPanelHook, createEgressAllowlistPolicy, createFailureSignalHook, createFileGuardPolicy, createFirstMessageVariantHook, createHookRegistry, createLabelTruncatorHook, createOttoStateDomainPolicy, createOutputTruncationHook, createPermissionDenyHook, createPermissionGuardHook, createPermissionModePolicy, createPermissionRegistry, createPermissionToolSetsFromRegistry, createStreamEndMetricsHook, createStreamMetricsHook, createThinkingValidatorHook, createTokenBudgetHook, createToolErrorTrackerHook, defaultDeniedRoots, defineHook, extractWriteTargets, isWellFormedAllowEntry, provisionBaselineSecurity };
547
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/core/hook-state.ts","../src/hook-spec.ts","../src/hook-registry.ts","../src/core/session/first-message-variant.ts","../src/core/tool-guard/label-truncator.ts","../src/core/tool-guard/output-truncation.ts","../src/core/tool-guard/tool-error-tracker.ts","../src/core/transform/thinking-validator.ts","../src/core/transform/anthropic-effort.ts","../src/core/transform/token-budget.ts","../src/core/quality/comment-checker.ts","../src/core/stream/stream-metrics.ts","../src/core/compaction/compaction-logger.ts","../src/core/edit-files-panel.ts","../src/core/permission/types.ts","../src/core/permission/builtin-policies.ts","../src/core/permission/register-builtin.ts","../src/core/permission/policy-registry.ts","../src/core/permission/audit-log.ts","../src/core/permission/permission-guard.ts","../src/core/permission/baseline-provisioning.ts","../src/core/permission/always-allow.ts","../src/core/permission/command-canonicalize.ts","../src/sandbox-safety.ts","../src/observability/failure-signals.ts","../src/observability/diagnoser.ts"],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ import{createLogger as e}from"@x-otto/shared";import{normalizeEnv as t}from"@x-otto/env";import{resolve as n}from"node:path";import{realpathSync as r}from"node:fs";const i=[{id:`chat.message.before`,kind:`interceptor`},{id:`chat.message.after`,kind:`interceptor`},{id:`tool.execute.before`,kind:`interceptor`},{id:`tool.execute.after`,kind:`interceptor`},{id:`messages.transform`,kind:`interceptor`},{id:`chat.params`,kind:`interceptor`},{id:`system.prompt.transform`,kind:`interceptor`},{id:`session.created`,kind:`observer`},{id:`session.restored`,kind:`observer`},{id:`session.deleted`,kind:`observer`},{id:`session.idle`,kind:`observer`},{id:`session.error`,kind:`observer`},{id:`stream.start`,kind:`observer`},{id:`stream.end`,kind:`observer`},{id:`compaction.before`,kind:`observer`},{id:`compaction.after`,kind:`observer`},{id:`memory.prune.before`,kind:`interceptor`},{id:`process.spawned`,kind:`observer`},{id:`process.exited`,kind:`observer`},{id:`task.created`,kind:`observer`},{id:`task.started`,kind:`observer`},{id:`task.completed`,kind:`observer`},{id:`task.failed`,kind:`observer`},{id:`task.cancelled`,kind:`observer`},{id:`notification`,kind:`observer`}],a=i.map(e=>e.id),o=new Set(i.filter(e=>e.kind===`interceptor`).map(e=>e.id));function s(){return Object.fromEntries(a.map(e=>[e,[]]))}function c(e){let t={name:e.name,timing:e.timing,priority:e.priority??50,enabled:e.enabled??!0};return o.has(e.timing)?{...t,kind:`interceptor`,handle:e.handle}:{...t,kind:`observer`,handle:e.handle}}function l(e=61440){return c({name:`output-truncation`,timing:`tool.execute.after`,priority:10,handle(t,n){let{content:r}=n.result,i=0;for(let e of r)e.type===`text`&&(i+=e.text.length);if(i<=e)return;let a=e,o=r.map(t=>{if(t.type!==`text`)return t;if(a<=0)return{type:`text`,text:``};if(t.text.length<=a)return a-=t.text.length,t;let n=t.text.slice(0,a);return a=0,{type:`text`,text:`${n}\n\n... [output truncated: ${i} chars → ${e} chars]`}}).filter(e=>e.type!==`text`||e.text.length>0);n.result={...n.result,content:o},n.metadata.truncated=!0,n.metadata.originalSize=i}})}function u(){return c({name:`label-truncator`,timing:`tool.execute.before`,priority:20,handle(e,t){for(let e of[`label`,`description`,`title`]){let n=t.args[e];typeof n==`string`&&n.length>200&&(t.args[e]=`${n.slice(0,200)}...`)}}})}function d(){return c({name:`thinking-validator`,timing:`messages.transform`,priority:20,handle(e,t){t.messages=t.messages.map(e=>{if(!ee(e))return e;let t=e.content;if(!Array.isArray(t))return e;let n=t.filter(e=>!(e.type===`thinking`&&(typeof e.thinking!=`string`||e.thinking.trim()===``)));return{...e,content:n}})}})}function ee(e){return typeof e==`object`&&!!e&&`role`in e&&e.role===`assistant`}function f(){return c({name:`anthropic-effort`,timing:`chat.params`,priority:10,handle(e,t){e.provider===`anthropic`&&(t.thinkingLevel||(e.model.includes(`opus`)?t.thinkingLevel=`high`:e.model.includes(`sonnet`)?t.thinkingLevel=`medium`:e.model.includes(`haiku`)&&(t.thinkingLevel=`low`)))}})}function p(){return c({name:`first-message-variant`,timing:`chat.message.before`,priority:10,handle(e,t){e.isFirstMessage&&(t.metadata.isFirstMessage=!0,t.metadata.variant=`first-message`)}})}const te=[/\/\/\s*TODO:\s*implement/i,/\/\/\s*add\s+(your|the)\s+/i,/\/\/\s*placeholder/i,/\/\/\s*\.\.\.\s*(rest|more|other|remaining)/i,/\/\/\s*handle\s+error\s+here/i,/\/\*\s*\*\//];function m(){return c({name:`comment-checker`,timing:`tool.execute.after`,priority:20,handle(e,t){if(e.readonly)return;let n=re(e.args);if(!n)return;let r=ne(n);if(r.length>0){let e=`\n\n⚠️ AI-style comments detected:\n${r.map(e=>` - Line ${e.line}: "${e.text}"`).join(`
2
+ `)}\nPlease replace with meaningful implementation comments.`;t.result={...t.result,content:[...t.result.content,{type:`text`,text:e}]},t.metadata.aiCommentsDetected=r.length}}})}function ne(e){let t=[],n=e.split(`
3
+ `);for(let e=0;e<n.length;e++){let r=n[e]??``;for(let n of te)if(n.test(r)){t.push({line:e+1,text:r.trim()});break}}return t}function re(e){return typeof e.content==`string`?e.content:typeof e.newString==`string`?e.newString:typeof e.new_string==`string`?e.new_string:null}const h=e(`@x-otto/hooks:stream-metrics`);function g(e){return c({name:`stream-metrics`,timing:`stream.start`,priority:10,handle(t){let n=e.streamMetrics;n.set(t.sessionId,{startTime:Date.now(),model:t.model,requestCount:(n.get(t.sessionId)?.requestCount??0)+1,lastStopReason:``,totalDurationMs:n.get(t.sessionId)?.totalDurationMs??0}),h.debug(`Stream started: session=%s model=%s`,t.sessionId,t.model)}})}function _(e){return c({name:`stream-end-metrics`,timing:`stream.end`,priority:10,handle(t){let n=e.streamMetrics.get(t.sessionId);if(n){let e=Date.now()-n.startTime;n.totalDurationMs+=e,n.lastStopReason=t.stopReason,h.debug(`Stream ended: session=%s model=%s duration=%dms stopReason=%s total=%dms`,t.sessionId,t.model,e,t.stopReason,n.totalDurationMs)}}})}const v=e(`@x-otto/hooks:compaction`);function y(e){return c({name:`compaction-logger`,timing:`compaction.before`,priority:10,handle(t){let n=e.compactionStats.get(t.sessionId)??{compactionCount:0,totalCompacted:0,lastCompactionTime:0};n.compactionCount++,n.lastCompactionTime=Date.now(),e.compactionStats.set(t.sessionId,n),v.info(`Compaction started: session=%s messageCount=%d compaction#%d`,t.sessionId,t.messageCount,n.compactionCount)}})}function b(e){return c({name:`compaction-after-logger`,timing:`compaction.after`,priority:10,handle(t){let n=e.compactionStats.get(t.sessionId);if(n){let e=Date.now()-n.lastCompactionTime;n.totalCompacted+=t.retainedCount,v.info(`Compaction finished: session=%s retained=%d duration=%dms totalCompactions=%d`,t.sessionId,t.retainedCount,e,n.compactionCount)}}})}const ie=e(`@x-otto/hooks:tool-error-tracker`);function x(e,t){let n=t?.circuitBreakerThreshold??5,r=t?.decayWindowMs??12e4;return c({name:`tool-error-tracker`,timing:`tool.execute.after`,priority:15,handle(t,i){let a=e.toolErrors.get(t.sessionId)??new Map;e.toolErrors.set(t.sessionId,a);let o=a.get(t.toolName);if(o&&Date.now()-o.lastErrorTime>r&&(o=void 0),t.result.isError){if(o||={toolName:t.toolName,errorCount:0,lastErrorTime:0,consecutiveErrors:0},o.errorCount++,o.consecutiveErrors++,o.lastErrorTime=Date.now(),a.set(t.toolName,o),o.consecutiveErrors>=n){let e=`Tool "${t.toolName}" hit circuit-breaker: ${o.consecutiveErrors} consecutive errors`;ie.warn(e),i.metadata.toolCircuitBreaker={toolName:t.toolName,consecutiveErrors:o.consecutiveErrors,tripped:!0}}}else o&&(o.consecutiveErrors=0,a.set(t.toolName,o))}})}const ae=e(`@x-otto/hooks:token-budget`);function S(e){let t=e?.maxOutputTokens??16384;return c({name:`token-budget`,timing:`chat.params`,priority:20,handle(e,n){(!n.maxTokens||n.maxTokens>t)&&(n.maxTokens=t,ae.debug(`Capped maxTokens to %d for model=%s`,t,e.model))}})}var C=class{streamMetrics=new Map;compactionStats=new Map;toolErrors=new Map;clearSession(e){this.streamMetrics.delete(e),this.compactionStats.delete(e),this.toolErrors.delete(e)}};const w=e(`@x-otto/hooks`),oe=new Set([`tool.execute.before`]),T=t(process.env.OTTO_HOOK_TIMEOUT_MS,5e3),se=new Set([`chat.message.before`,`chat.message.after`,`tool.execute.before`,`memory.prune.before`]),ce=new Set([`tool.execute.before`]);var E=class{hooks=s();disabled=new Set;onHookTimeout;state=new C;constructor(e){this.onHookTimeout=e?.onHookTimeout,this.register(c({name:`hook-state-cleanup`,timing:`session.deleted`,priority:90,handle:e=>{this.state.clearSession(e.sessionId)}})),e?.preset&&e.preset!==`none`&&this.applyPreset(e.preset)}register(e){this.hooks[e.timing].push(e),w.debug({hookName:e.name,timing:e.timing,priority:e.priority},`Hook registered`)}registerAll(e){for(let t of e)this.register(t)}on(e,t,n,r=50){return this.register(c({name:t,timing:e,handle:n,priority:r})),this}has(e){return this.getRegistered().some(t=>t.name===e)}unregister(e){let t=!1;for(let n of a){let r=this.hooks[n],i=r.filter(t=>t.name!==e);i.length<r.length&&(this.hooks[n]=i,t=!0,w.debug({hookName:e,timing:n},`Hook unregistered`))}return t}disable(e){this.disabled.add(e),w.debug({hookName:e},`Hook disabled`)}enable(e){this.disabled.delete(e),w.debug({hookName:e},`Hook enabled`)}getRegistered(e){if(e)return this.hooks[e].map(D);let t=[];for(let e of a)t.push(...this.hooks[e].map(D));return t}async execute(e,t,n){let r=this.getSortedInterceptorHooks(e);if(r.length===0)return;let i=!1,a=!1;for(let o of r){let r=await this.runInterceptorWithTimeout(o,e,t,n);if(!r.ok&&oe.has(e)){let e=n;typeof e.cancelled==`boolean`&&(e.cancelled=!0,e.cancelReason=r.timedOut?`hook "${o.name}" timed out after ${T}ms`:`hook "${o.name}" failed: ${r.message}`)}if(se.has(e)){let e=n;e.cancelled===!0&&(i=!0),i&&(e.cancelled=!0)}if(ce.has(e)){let e=n;e.decision===`deny`&&(a=!0),a&&(e.decision=`deny`)}}}async runInterceptorWithTimeout(e,t,n,r){let i;try{return await Promise.race([e.handle(n,r),new Promise((t,n)=>{i=setTimeout(()=>{n(Error(`hook "${e.name}" timed out after ${T}ms`))},T),i.unref?.()})]),{ok:!0}}catch(n){let r=n instanceof Error?n.message:String(n),i=r.includes(`timed out after`);return w.error({name:e.name,timing:t,err:r,timedOut:i},`Hook failed or timed out`),i&&this.onHookTimeout?.({hookName:e.name,timing:t,timeoutMs:T}),{ok:!1,timedOut:i,message:r}}finally{i!==void 0&&clearTimeout(i)}}async runObserverHookWithTimeout(e,t,n){let r;try{await Promise.race([e.handle(n),new Promise((t,n)=>{r=setTimeout(()=>{n(Error(`hook "${e.name}" timed out after ${T}ms`))},T),r.unref?.()})])}catch(n){let r=n instanceof Error?n.message:String(n);w.error({name:e.name,timing:t,err:r},`Hook failed or timed out`)}finally{r!==void 0&&clearTimeout(r)}}sanitizeEmitInput(e,t){if(e!==`session.error`)return t;let n=t;return{sessionId:n.sessionId,metadata:n.metadata,error:n.error instanceof Error?n.error.message:String(n.error)}}async emit(e,t){let n=this.getSortedObserverHooks(e);if(n.length===0)return;let r=this.sanitizeEmitInput(e,t);for(let t of n)await this.runObserverHookWithTimeout(t,e,r)}clear(){for(let e of a)this.hooks[e]=[];this.disabled.clear(),w.debug(`All hooks cleared`)}getSortedInterceptorHooks(e){let t=this.hooks[e];return t?t.filter(e=>e.kind===`interceptor`&&e.enabled&&!this.disabled.has(e.name)).sort((e,t)=>e.priority-t.priority):(w.warn({timing:e},`Unknown hook timing (no bucket) — emit name mismatch with HOOK_NODES?`),[])}getSortedObserverHooks(e){let t=this.hooks[e];return t?t.filter(e=>e.kind===`observer`&&e.enabled&&!this.disabled.has(e.name)).sort((e,t)=>e.priority-t.priority):(w.warn({timing:e},`Unknown hook timing (no bucket) — emit name mismatch with HOOK_NODES?`),[])}applyPreset(e){let t=le(e,this.state);for(let e of t)this.register(e)}};function D(e){return{name:e.name,timing:e.timing,priority:e.priority,enabled:e.enabled}}function le(e,t){switch(e){case`default`:return O(t);case`strict`:return ue(t);case`minimal`:return de();default:return[]}}function O(e){return[l(),u(),d(),f(),p(),g(e),_(e),y(e),b(e),x(e),S()]}function ue(e){return[...O(e),m()]}function de(){return[l()]}function fe(e){return new E(e)}function k(e){return e?(e.endsWith(`
4
+ `)?e.slice(0,-1):e).split(`
5
+ `).length:0}function pe(e){let{toolCallId:t,args:n}=e,r=typeof n.path==`string`?n.path:void 0;if(!r)return null;let i=typeof n.oldContent==`string`?n.oldContent:void 0,a=typeof n.newContent==`string`?n.newContent:void 0;if(i!==void 0&&a!==void 0){let e=k(i),n=k(a);return{path:r,operation:`edit`,oldContent:i,newContent:a,addedLines:Math.max(0,n-e),removedLines:Math.max(0,e-n),timestamp:Date.now(),toolCallId:t}}let o=typeof n.content==`string`?n.content:void 0;return o===void 0?null:{path:r,operation:`write`,newContent:o,addedLines:k(o),removedLines:0,timestamp:Date.now(),toolCallId:t}}function me(e={}){let{onFileEdited:t,getOnFileEdited:n,dedupCache:r}=e;return c({name:`edit-files-panel`,timing:`tool.execute.after`,priority:90,handle(e){let i=n?.()??t;if(!i||e.readonly)return;let a=pe(e);if(!a)return;let o=a.toolCallId;if(r){if(r.has(o))return;r.add(o)}i(a)}})}const he=[/^\/etc\//,/^\/usr\//,/^\/sys\//,/^\/proc\//,/node_modules\//,/\.git\//,/\.env$/,/\.env\.local$/,/\/\.ssh\//,/\/\.aws\//,/\/\.(bash|zsh)rc$/,/\/\.(bash_profile|zprofile|profile)$/];function ge(e){let t=[],n=``,r=!1,i=!1;for(let a=0;a<e.length;a++){let o=e[a],s=e[a+1];if(r){o===`'`&&(r=!1),n+=o;continue}if(i){o===`"`&&(i=!1),n+=o;continue}if(o===`'`){r=!0,n+=o;continue}if(o===`"`){i=!0,n+=o;continue}if(o===`&`&&s===`&`||o===`|`&&s===`|`){t.push(n),n=``,a++;continue}if(o===`;`||o===`|`||o===`
6
+ `||o===`&`){t.push(n),n=``;continue}n+=o}return t.push(n),t.map(e=>e.trim()).filter(Boolean)}function A(e){let t=e.trim();return t.length>=2&&(t[0]===`"`&&t[t.length-1]===`"`||t[0]===`'`&&t[t.length-1]===`'`)?t.slice(1,-1):t}function _e(e){let t=e.match(/^(?:\S*\/)?(?:bash|sh|zsh|dash|ksh|ash|python[0-9.]*|node|nodejs|perl|ruby|php|deno|bun)\b[\s\S]*?\s-[a-z]*[cer]\b\s*([\s\S]+)$/i);return t?A(t[1]):null}function ve(e){let t=e.trim();for(let e=0;e<10;e++){let e=t;if(t=t.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S*)\s+/,``),t=t.replace(/^(?:sudo|env|nohup|setsid|ionice)\b(?:\s+-\S+|\s+[A-Za-z_][A-Za-z0-9_]*=\S+)*\s+/i,``),t=t.replace(/^(?:nice|stdbuf)\b(?:\s+-\S+)*\s+/i,``),t=t.replace(/^timeout\b(?:\s+-\S+)*\s+\S+\s+/i,``),t===e)break}return t}function ye(e){let t=[];for(let n=0;n<e.length;n++)if(e[n]===`$`&&e[n+1]===`(`){let r=1,i=n+2,a=i;for(;i<e.length&&r>0;)e[i]===`(`?r++:e[i]===`)`&&r--,i++;r===0&&t.push(e.slice(a,i-1))}let n=e.match(/`([^`]+)`/g);if(n)for(let e of n)t.push(e.slice(1,-1));return t}const be={n:`
7
+ `,t:` `,r:`\r`,a:`\x07`,b:`\b`,f:`\f`,v:`\v`,e:`\x1B`,E:`\x1B`,"\\":`\\`,"'":`'`,'"':`"`,"?":`?`};function j(e){return/[0-9a-fA-F]/.test(e)}function xe(e){let t=``,n=0;for(;n<e.length;){if(e[n]!==`\\`){t+=e[n],n++;continue}let r=e[n+1];if(r===void 0){t+=`\\`;break}if(r===`x`){let r=``;for(;r.length<2&&j(e[n+2+r.length]??``);)r+=e[n+2+r.length];if(r){t+=String.fromCharCode(parseInt(r,16)),n+=2+r.length;continue}}if(r===`u`||r===`U`){let i=r===`u`?4:8,a=``;for(;a.length<i&&j(e[n+2+a.length]??``);)a+=e[n+2+a.length];if(a){t+=String.fromCodePoint(parseInt(a,16)),n+=2+a.length;continue}}if(r>=`0`&&r<=`7`){let r=``;for(;r.length<3;){let t=e[n+1+r.length]??``;if(t<`0`||t>`7`)break;r+=t}t+=String.fromCharCode(parseInt(r,8)&255),n+=1+r.length;continue}t+=be[r]??r,n+=2}return t}function Se(e){return e.replace(/\$'((?:\\.|[^'\\])*)'/g,(e,t)=>xe(t))}function Ce(e){return e.replace(/\$\{IFS[^}]*\}/g,` `).replace(/\$IFS\b(?:\$[0-9@*])?/g,` `)}function we(e){return Ce(Se(e))}function M(e){let t=new Set,n=[],r=(e,i)=>{let a=e.trim();if(!a||i>12||t.has(a))return;t.add(a),n.push(a);let o=we(a);o!==a&&r(o,i+1);let s=a.match(/<<<\s*('[^']*'|"[^"]*"|[^\s|;&]+)/)?.[1];s&&r(A(s),i+1);for(let e of ye(a))r(e,i+1);let c=ge(a);if(c.length>1){for(let e of c)r(e,i+1);return}let l=_e(a);l&&r(l,i+1);let u=ve(a);u!==a&&r(u,i+1)};return r(e,0),n}function Te(e){return e.length>=2&&(e[0]===`"`&&e.endsWith(`"`)||e[0]===`'`&&e.endsWith(`'`))?e.slice(1,-1):e}function N(e){let t=[],n=``,r=!1,i=!1;for(let a=0;a<e.length;a++){let o=e[a];if(r){o===`'`?r=!1:n+=o;continue}if(i){o===`"`?i=!1:n+=o;continue}if(o===`'`){r=!0;continue}if(o===`"`){i=!0;continue}if(o===` `||o===` `){n&&=(t.push(n),``);continue}n+=o}return n&&t.push(n),t}const P=/(?:^|[^0-9&>])(?:[0-9]*&?>>?|>\|)\s*('[^']*'|"[^"]*"|[^\s;|&<>]+)/g;function Ee(e){let t=new Set,n=e=>{if(!e)return;let n=Te(e.trim());n&&!n.startsWith(`-`)&&!n.includes(`$`)&&n!==`/dev/null`&&n!==`/dev/stdout`&&n!==`/dev/stderr`&&t.add(n)};for(let t of M(e)){let e;for(P.lastIndex=0;(e=P.exec(t))!==null;)n(e[1]);let r=N(t);if(r.length===0)continue;let i=r[0].replace(/^.*\//,``),a=r.slice(1),o=a.filter(e=>!e.startsWith(`-`));if(i===`tee`)for(let e of o)n(e);else if(i===`cp`||i===`mv`||i===`install`||i===`ln`)o.length>=1&&n(o[o.length-1]);else if(i===`touch`||i===`rm`||i===`rmdir`||i===`mkdir`)for(let e of o)n(e);else if(i===`dd`)for(let e of a)e.startsWith(`of=`)&&n(e.slice(3))}return[...t]}function F(e){return{readonlyTools:new Set(e?.readonlyTools??[]),writeTools:new Set(e?.writeTools??[]),fileWriteTools:new Set(e?.fileWriteTools??[])}}function De(e){let t=new Set,n=new Set,r=new Set;for(let i of e)if(i.name){if(i.readonly===!0){t.add(i.name);continue}if(i.name===`bash`){n.add(i.name);continue}if(i.metadata?.category===`fs`){n.add(i.name),r.add(i.name);continue}i.name.startsWith(`mcp__`)&&n.add(i.name)}return{readonlyTools:t,writeTools:n,fileWriteTools:r}}function I(e){let{fileWriteTools:t}=F(e);return{name:`builtin::file-guard`,priority:10,enabled:!0,safety:!0,scope:{agents:[`*`]},rules:[{name:`protect-system-paths`,effect:`deny`,match:{tools:[...t],paths:he},denyReason:`Write to protected system path is not allowed`}]}}const Oe=/\b(rmdir\b|drop\s+table|git\s+push\s+(?:\S+\s+)*(?:--force\b|-f\b)|git\s+reset\s+--hard|git\s+clean\s+-[a-z]*f|truncate\s+table|dd\s+if=|mkfs\b|>\s*\/dev\/(?:sd|nvme|disk|hd))/i;function ke(e){let t=N(e);return t.length===0||t[0].replace(/^.*\//,``)!==`rm`?!1:t.slice(1).some(e=>e===`--recursive`||e===`--force`?!0:/^-[a-z]*[rf]/i.test(e))}const Ae=/(\beval\s|(?:curl|wget)\b[^|;&]*\|\s*(?:sudo\s+)?(?:ba|z|d)?sh\b|:\s*\(\s*\)\s*\{[^}]*:\s*\|\s*:|base64\s+(?:-d|--decode)\b[^|]*\|)/i,je=/\b(?:python[0-9.]*|pythonw|pypy[0-9]*|node|nodejs|deno|bun|perl|ruby|php|osascript|Rscript|pwsh|powershell(?:\.exe)?)\b[^|;&\n]*?(?:\s-{1,2}(?:c|e|r|E|p|eval|Command|EncodedCommand)\b|\s*<<)/i;function L(e){return e?M(e).some(e=>Oe.test(e)||ke(e)||Ae.test(e)||je.test(e)):!1}function Me(e,t){return e.startsWith(`mcp__`)?Object.values(t).some(e=>typeof e==`string`&&L(e)):!1}const R={name:`builtin::destructive-guard`,priority:15,enabled:!0,safety:!0,scope:{agents:[`*`]},rules:[{name:`ask-destructive-bash`,effect:`ask`,match:{condition:e=>L(String(e.args.command??``))},askPrompt:`This command may be destructive or runs code that cannot be statically checked. Continue?`},{name:`ask-destructive-mcp-args`,effect:`ask`,match:{condition:e=>Me(e.toolName,e.args)},askPrompt:`This MCP tool argument may run a destructive or unverifiable command. Continue?`}]},Ne=[/\/\.ssh(\/|$)/,/\/\.aws(\/|$)/,/\/\.gnupg(\/|$)/,/\/otto\/auth\.json$/],Pe=[/\/\.env(\.[\w.-]+)?$/],z=[/\.ssh(\/|\s|$)/,/\.aws(\/|\s|$)/,/\.gnupg(\/|\s|$)/,/otto\/auth\.json/],Fe=[/\.env(\b|\.[\w-]+)/];function B(e,t){return e?M(e).some(e=>t.some(t=>t.test(e))):!1}function V(e,t,n){if(!e.startsWith(`mcp__`))return!1;for(let e of Object.values(t))if(typeof e==`string`&&B(e,n))return!0;return!1}const H={name:`builtin::credential-guard`,priority:16,enabled:!0,safety:!0,scope:{agents:[`*`]},rules:[{name:`deny-credential-paths`,effect:`deny`,match:{paths:Ne},denyReason:`Reading credential/secret files (~/.ssh, ~/.aws, ~/.gnupg, auth store) is not allowed`},{name:`deny-credential-command`,effect:`deny`,match:{condition:e=>B(String(e.args.command??``),z)},denyReason:`Command references credential/secret paths (~/.ssh, ~/.aws, ~/.gnupg, auth store)`},{name:`ask-dotenv-paths`,effect:`ask`,match:{paths:Pe},askPrompt:`This reads a .env file which may contain secrets. Continue?`},{name:`ask-dotenv-command`,effect:`ask`,match:{condition:e=>B(String(e.args.command??``),Fe)},askPrompt:`This command references a .env file which may contain secrets. Continue?`},{name:`deny-mcp-credential-args`,effect:`deny`,match:{condition:e=>V(e.toolName,e.args,z)},denyReason:`MCP tool argument references credential/secret paths (~/.ssh, ~/.aws, ~/.gnupg, auth store)`},{name:`ask-mcp-dotenv-args`,effect:`ask`,match:{condition:e=>V(e.toolName,e.args,Fe)},askPrompt:`This MCP tool reads a .env file which may contain secrets. Continue?`}]},Ie=[/\.otto\/plugin-trust\.json$/,/\.otto\/claude-trust\.json$/,/\.otto\/config\.json$/,/\.otto\/schedules\//,/\.otto\/plugins\//,/\.otto\/timers\//],Le=[/\.otto\/sessions\//,/\.otto\/.*\.lock$/],Re=[/\.otto\/AGENTS\.md$/,/\.otto\/MEMORY\.md$/,/\.otto\/memory\//,/\.otto\/local\/MEMORY\.md$/,/\.otto\/local\/memory\//,/\.otto\/lessons\.json$/,/\.otto\/local\/lessons\.json$/],ze=[/\.otto\/(plugin-trust|claude-trust|config)\.json/,/\.otto\/(schedules|plugins|timers|sessions)\//,/\.otto\/.*\.lock\b/,/\.otto\/AGENTS\.md/,/\.otto\/(local\/)?MEMORY\.md/,/\.otto\/(local\/)?memory\//,/\.otto\/(local\/)?lessons\.json/];function U(e){let{fileWriteTools:t}=F(e),n=[...t];return{name:`builtin::otto-state-domain-guard`,priority:17,enabled:!0,safety:!0,scope:{agents:[`*`]},rules:[{name:`deny-otto-trust-and-execution-state`,effect:`deny`,match:{tools:n,paths:Ie},denyReason:`Writing to otto trust/config/schedule/plugin state is not allowed via file tools — use the dedicated command (/extension, schedule_create, settings) instead`},{name:`deny-otto-runtime-state`,effect:`deny`,match:{tools:n,paths:Le},denyReason:`otto runtime state (sessions/locks) must not be modified directly`},{name:`deny-otto-memory-state`,effect:`deny`,match:{tools:n,paths:Re},denyReason:`Cross-session memory must be written via the memory_record tool (not edit/write) — use scope: "workspace" | "local" | "project-shared"`},{name:`deny-otto-state-command-reference`,effect:`deny`,match:{condition:e=>B(String(e.args.command??``),ze)},denyReason:`Command references otto trust/config/schedule/plugin/session state — not allowed via bash`}]}}function Be(e){try{return new URL(e).hostname.toLowerCase()}catch{return null}}function Ve(e,t){return t.some(t=>{let n=t.trim().toLowerCase().replace(/^\*?\./,``);return n.length>0&&(e===n||e.endsWith(`.${n}`))})}function He(e){let t=e.map(e=>e.trim().toLowerCase()).filter(Boolean);return{name:`setting::egress-allowlist`,priority:8,enabled:!0,safety:!0,scope:{agents:[`*`]},rules:[{name:`ask-egress-offlist`,effect:`ask`,match:{condition:e=>{let n=e.args.url;if(typeof n!=`string`)return!1;let r=Be(n);return r?!Ve(r,t):!1}},askPrompt:`This requests a host not in the egress allowlist. Continue?`}]}}function Ue(e){return{name:`setting::disabled-tools`,priority:2,enabled:!0,scope:{agents:[`*`]},rules:[{name:`deny-disabled`,effect:`deny`,match:{tools:e},denyReason:`Tool disabled by configuration`}]}}function W(e,t){let{readonlyTools:n,writeTools:r}=F(t);switch(e){case`bypass`:return{name:`mode::bypass`,priority:1,enabled:!0,scope:{agents:[`*`]},rules:[{name:`allow-all`,effect:`allow`,match:{}}]};case`readonly`:return{name:`mode::readonly`,priority:1,enabled:!0,scope:{agents:[`*`]},rules:[{name:`allow-reads`,effect:`allow`,match:{condition:e=>n.has(e.toolName)}},{name:`deny-writes`,effect:`deny`,match:{},fallthrough:!0,denyReason:`Read-only mode: write operations are not allowed`}]};case`confirm`:return{name:`mode::confirm`,priority:1,enabled:!0,scope:{agents:[`*`]},rules:[{name:`allow-reads`,effect:`allow`,match:{condition:e=>n.has(e.toolName)}},{name:`ask-writes`,effect:`ask`,match:{tools:[...r]},askPrompt:`Confirm write operation?`}]};case`strict`:return{name:`mode::strict`,priority:9999,enabled:!0,scope:{agents:[`*`]},rules:[{name:`deny-unmatched`,effect:`deny`,match:{},fallthrough:!0,denyReason:`Strict mode: no matching permission policy`}]};default:return{name:`mode::auto`,priority:1,enabled:!0,scope:{agents:[`*`]},rules:[{name:`allow-reads`,effect:`allow`,match:{condition:e=>n.has(e.toolName)}},{name:`auto-writes`,effect:`allow`,match:{tools:[...r]}}]}}}function We(e,t={}){e.register(W(`auto`,t.toolSets)),e.register(I(t.toolSets)),e.register(R),e.register(H),e.register(U(t.toolSets))}function Ge(e){let t=e.trim(),n=/^([^()\s]+)\((.*)\)$/.exec(t);return n?{toolName:n[1]??t,content:(n[2]??``).trim()}:{toolName:t}}function Ke(e){let t=e.trim();if(!t)return!1;let n=/^([^()\s]+)\((.*)\)$/.exec(t);if(n){let e=(n[1]??``).trim(),t=(n[2]??``).trim();return e.length>0&&t.length>0}return/^[^()\s]+$/.test(t)}function qe(e,t){let n=e.trim(),r=t.trim();return r?n===r||n.startsWith(r+` `):!1}const Je=/^cd\s+\S+$/;function Ye(e,t){let n=t.filter(Boolean);if(n.length===0)return!1;let r=M(e);return r.length===0?!1:r.every(e=>{if(Je.test(e))return!0;let t=e.replace(/^cd\s+\S+\s*&&\s*/,``).trim();return n.some(e=>qe(t,e))})}function Xe(e,t){let n=[];for(let r of t){let t=Ge(r);t.toolName===e.toolName&&n.push(t)}if(n.length===0)return!1;if(n.some(e=>e.content===void 0))return!0;if(e.toolName===`bash`){let t=typeof e.args?.command==`string`?e.args.command:``;return t?Ye(t,n.map(e=>e.content)):!1}return!1}function Ze(e){let t=e.trim().replace(/^cd\s+\S+\s*&&\s*/,``).trim();if(/[;&|]|`|\$\(|<<|\beval\b/.test(t))return null;let n=t.split(/\s+/).filter(Boolean),r=n[0];if(!r||r.includes(`=`))return null;let i=n[1]&&!n[1].startsWith(`-`)?n[1]:null;return i?`${r} ${i}`:r}const G=e(`@x-otto/hooks:permission`);function K(e,t){return!e.agents||e.agents.length===0?!0:e.agents.includes(`*`)||e.agents.includes(t)}function Qe(e,t){return!e.sessions||e.sessions.length===0?!0:e.sessions.includes(`*`)||e.sessions.includes(t)}function $e(e,t){return K(e,t.agentName)&&Qe(e,t.sessionId)}function et(e,t){return!(e.tools&&e.tools.length>0&&!e.tools.includes(t.toolName)||e.paths&&e.paths.length>0&&(!t.filePath||!e.paths.some(e=>e.test(t.filePath)))||e.condition&&!e.condition(t))}const tt={effect:`allow`,policyName:`__default__`,ruleName:`__fallthrough__`,timestamp:0,evaluatedPolicies:0};var q=class{policies=[];alwaysAllow=new Set;sessionAlwaysAllow=new Map;setAlwaysAllow(e){this.alwaysAllow=new Set(e)}addAlwaysAllow(e){this.alwaysAllow.add(e)}getAlwaysAllow(){return[...this.alwaysAllow]}setSessionAlwaysAllow(e,t){if(e){if(t.length===0){this.sessionAlwaysAllow.delete(e);return}this.sessionAlwaysAllow.set(e,new Set(t))}}addSessionAlwaysAllow(e,t){if(!e||!t)return;let n=this.sessionAlwaysAllow.get(e)??new Set;n.add(t),this.sessionAlwaysAllow.set(e,n)}getSessionAlwaysAllow(e){return[...this.sessionAlwaysAllow.get(e)??[]]}clearSessionAlwaysAllow(e){this.sessionAlwaysAllow.delete(e)}register(e){this.unregister(e.name),this.policies.push(e),this.policies.sort((e,t)=>e.priority-t.priority),G.debug({name:e.name,priority:e.priority,rules:e.rules.length},`Permission policy registered`)}registerAll(e){for(let t of e)this.register(t)}unregister(e){let t=this.policies.findIndex(t=>t.name===e);return t>=0?(this.policies.splice(t,1),G.debug({name:e},`Permission policy unregistered`),!0):!1}has(e){return this.policies.some(t=>t.name===e)}evaluate(e){let t=0,n=[];for(let r of this.policies)if(r.enabled&&$e(r.scope,e)){t++;for(let t of r.rules)et(t.match,e)&&n.push({effect:t.effect,policyName:r.name,ruleName:t.name,priority:r.priority,reason:t.effect===`deny`?t.denyReason:t.askPrompt,safety:r.safety===!0,fallthrough:t.fallthrough===!0})}let r=n.some(e=>e.policyName===`mode::auto`||e.policyName===`mode::bypass`),i=(e,n)=>{let r=n.reduce((e,t)=>t.priority<e.priority?t:e);return{effect:e,policyName:r.policyName,ruleName:r.ruleName,reason:r.reason,timestamp:Date.now(),evaluatedPolicies:t,safety:r.safety}},a=n=>{if(n.length===0)return null;let a=n.filter(e=>e.effect===`deny`);if(a.length>0)return i(`deny`,a);let o=n.filter(e=>e.effect===`ask`);if(o.length>0){if(r)return{effect:`allow`,policyName:`mode::auto-override`,ruleName:o[0].ruleName,timestamp:Date.now(),evaluatedPolicies:t};let n=Xe(e,[...this.alwaysAllow,...this.sessionAlwaysAllow.get(e.sessionId)??[]]),a=o.filter(e=>e.safety||!n);return a.length===0?{effect:`allow`,policyName:`user::always-allow`,ruleName:e.toolName,timestamp:Date.now(),evaluatedPolicies:t}:i(`ask`,a)}let s=n.filter(e=>e.effect===`allow`);return s.length>0?i(`allow`,s):null},o=n.filter(e=>!e.fallthrough),s=n.filter(e=>e.fallthrough),c=a(o),l=a(s);return c?.effect===`deny`||c?.effect===`allow`?c:l?.effect===`deny`?l:(c??l)||{...tt,timestamp:Date.now(),evaluatedPolicies:t}}getEffective(e){return this.policies.filter(t=>t.enabled&&K(t.scope,e))}getAll(){return[...this.policies]}clear(){this.policies=[],G.debug(`All permission policies cleared`)}};function J(e={}){let t=new q;return We(t,e),t}const Y=1024,nt=/\([^)]*(?:[*+]|\{\d+,\d*\})[^)]*\)(?:[*+]|\{\d+,\d*\})/;function rt(e,t){if(e.length>Y)return G.warn({rule:t,length:e.length,max:Y},`Permission path pattern exceeds max length — rule skipped (ReDoS guard)`),null;if(nt.test(e))return G.warn({rule:t,pattern:e},`Permission path pattern has nested quantifiers (catastrophic backtracking risk) — rule skipped (ReDoS guard)`),null;try{return new RegExp(e)}catch(n){return G.warn({rule:t,pattern:e,error:n instanceof Error?n.message:String(n)},`Permission path pattern failed to compile — rule skipped`),null}}function it(e){let t=[];for(let n of e.rules){let e;if(n.paths&&n.paths.length>0){let t=[],r=!1;for(let e of n.paths){let i=rt(e,n.name);if(i===null){r=!0;break}t.push(i)}if(r)continue;e=t}t.push({name:n.name,effect:n.effect,match:{tools:n.tools,paths:e},denyReason:n.deny_reason,askPrompt:n.ask_prompt})}return{name:e.name,priority:e.priority??50,enabled:e.enabled??!0,scope:{agents:e.scope?.agents,sessions:e.scope?.sessions},rules:t}}var X=class{entries=[];maxEntries;constructor(e=1e3){this.maxEntries=e}record(e,t){this.entries.push({timestamp:t.timestamp,sessionId:e.sessionId,agentName:e.agentName,toolName:e.toolName,filePath:e.filePath,decision:t}),this.entries.length>this.maxEntries&&this.entries.splice(0,this.entries.length-this.maxEntries)}getEntries(e){let t=this.entries;return e?.sessionId&&(t=t.filter(t=>t.sessionId===e.sessionId)),e?.effect&&(t=t.filter(t=>t.decision.effect===e.effect)),t===this.entries?this.entries.slice():t}getDenyCount(e){return this.getEntries({sessionId:e,effect:`deny`}).length}getAskCount(e){return this.getEntries({sessionId:e,effect:`ask`}).length}clear(e){e?this.entries=this.entries.filter(t=>t.sessionId!==e):this.entries=[]}get size(){return this.entries.length}};function at(e){return e.safety?`high`:`medium`}function ot(e){let t=[];for(let r of e)if(t.push(r),typeof r==`string`){let e=n(r);e!==r&&t.push(e)}return t}var Z=class{name=`permission-guard`;timing=`tool.execute.before`;priority=5;enabled=!0;registry;auditLog;constructor(e,t){this.registry=e,this.auditLog=t}handle=(e,t)=>{let n={timing:`tool.execute.before`,toolName:e.toolName,args:e.args,agentName:e.agentName,sessionId:e.sessionId,metadata:{}},r=ot(e.paths&&e.paths.length>0?e.paths:[void 0]),i=this.registry.evaluate({...n,filePath:r[0]}),a=r[0];for(let e=1;e<r.length;e++){let t=this.registry.evaluate({...n,filePath:r[e]});(t.effect===`deny`||t.effect===`ask`&&i.effect===`allow`)&&(i=t,a=r[e])}let o={...n,filePath:a};if(this.auditLog?.record(o,i),i.effect===`deny`){t.cancelled=!0,t.decision=`deny`,t.decisionReason=i.reason??i.ruleName,t.cancelReason=`Permission denied: ${t.decisionReason}`;return}i.effect===`ask`?(t.decision=`ask`,t.decisionReason=i.reason??`Requires confirmation`,t.risk=at(i)):t.decision!==`ask`&&t.decision!==`deny`&&(t.decision=`allow`)}};function Q(e,t){let n=new Z(e,t);return c({name:n.name,timing:n.timing,priority:n.priority,enabled:n.enabled,handle:n.handle})}const $={fileWriteTools:[`write`,`edit`,`edit-diff`]};function st(e){if(e.has(`permission-guard`))return;let t=J({toolSets:$});return e.register(Q(t,new X)),t}function ct(e){try{return r(e)}catch{return}}function lt(e){let t=new Set;for(let r of e){let e=n(r);t.add(e);let i=ct(e);i&&i!==e&&t.add(i)}return[...t]}function ut(e,t){return e===t||e.startsWith(t.endsWith(`/`)?t:`${t}/`)}function dt(e,t){let n=lt(t);return lt([e]).some(e=>n.some(t=>ut(e,t)))}function ft(e,t){let n=e.writePaths??[],r=e.network===!0;for(let e of n)if(dt(e,t.deniedRoots))return{verdict:`deny`,reason:`write to protected path "${e}" is never allowed (escape guard)`};if(t.mode===`bypass`||n.length===0&&!r)return{verdict:`auto-allow`};let i=n.every(e=>dt(e,t.writableRoots)),a=!r||t.networkAllowed;return i&&a?{verdict:`auto-allow`}:t.mode===`strict`||t.mode===`readonly`?{verdict:`deny`,reason:`${i?`network access`:`write outside the sandbox writable roots`} is not permitted in ${t.mode} mode`}:{verdict:`ask`,reason:`${i?`network access`:`write outside the sandbox writable roots`} requires approval (will run with sandbox bypassed for this operation)`}}function pt(e,t){return[n(e,`.otto`),n(t,`.otto`)]}function mt(e,t,r,i=[]){let a=[n(t),n(r),`/tmp`];if(e!==`strict`&&e!==`readonly`)for(let e of i)a.push(n(e));return[...new Set(a)]}var ht=class{signals=new Map;clock;constructor(e=()=>Date.now()){this.clock=e}record(e,t,n){let r=`${e}:${t}`,i=this.clock(),a=this.signals.get(r);a?(a.count++,a.lastSeen=i,n&&(a.detail=n)):this.signals.set(r,{kind:e,signature:t,count:1,firstSeen:i,lastSeen:i,detail:n})}recurring(e=3){return[...this.signals.values()].filter(t=>t.count>=e).sort((e,t)=>t.count-e.count)}all(){return[...this.signals.values()].sort((e,t)=>t.count-e.count)}clear(){this.signals.clear()}};function gt(e){let t=e.content;if(!Array.isArray(t))return;let n=t.map(e=>e&&typeof e==`object`&&`text`in e?String(e.text??``):``).join(` `).trim();return n?n.split(`
8
+ `)[0].slice(0,120):void 0}function _t(e){return c({name:`failure-signal-recorder`,timing:`tool.execute.after`,priority:14,handle(t){t.result?.isError&&e.record(`tool-error`,t.toolName,gt(t.result))}})}function vt(e){return c({name:`failure-signal-permission-deny`,timing:`tool.execute.before`,priority:90,handle(t,n){n?.decision===`deny`&&e.record(`permission-denied`,t.toolName,n.decisionReason)}})}var yt=class{proposed=new Set;minCount;clock;constructor(e,t={}){this.signals=e,this.minCount=t.minCount??3,this.clock=t.clock??(()=>Date.now())}propose(){let e=[];for(let t of this.signals.recurring(this.minCount)){let n=`${t.kind}:${t.signature}`;this.proposed.has(n)||(this.proposed.add(n),e.push({id:n,signature:t.signature,kind:`investigate-fix`,directive:bt(t),evidence:{signalKind:t.kind,count:t.count,detail:t.detail},createdAt:this.clock()}))}return e}reset(){this.proposed.clear()}};function bt(e){return e.kind===`tool-error`?`工具「${e.signature}」反复失败(${e.count} 次${e.detail?`,如「${e.detail}」`:``})。调查根因并修复:定位失败路径、补健壮性或修 bug,跑测试验证。`:`反复出现「${e.kind}: ${e.signature}」(${e.count} 次)。调查并消除。`}export{$ as BASELINE_PERMISSION_TOOL_SETS,H as CREDENTIAL_READ_POLICY,R as DESTRUCTIVE_COMMAND_POLICY,yt as Diagnoser,ht as FailureSignalStore,E as HookRegistry,C as HookState,X as PermissionAuditLog,Z as PermissionGuardHook,q as PermissionPolicyRegistry,ft as assessSandboxSafety,Ze as bashAlwaysAllowPrefix,it as compilePolicyFromSetting,mt as computeSandboxRoots,f as createAnthropicEffortHook,m as createCommentCheckerHook,b as createCompactionAfterHook,y as createCompactionLoggerHook,Ue as createDisabledToolsPolicy,me as createEditFilesPanelHook,He as createEgressAllowlistPolicy,_t as createFailureSignalHook,I as createFileGuardPolicy,p as createFirstMessageVariantHook,fe as createHookRegistry,u as createLabelTruncatorHook,U as createOttoStateDomainPolicy,l as createOutputTruncationHook,vt as createPermissionDenyHook,Q as createPermissionGuardHook,W as createPermissionModePolicy,J as createPermissionRegistry,De as createPermissionToolSetsFromRegistry,_ as createStreamEndMetricsHook,g as createStreamMetricsHook,d as createThinkingValidatorHook,S as createTokenBudgetHook,x as createToolErrorTrackerHook,pt as defaultDeniedRoots,c as defineHook,Ee as extractWriteTargets,Ke as isWellFormedAllowEntry,st as provisionBaselineSecurity};
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["log","logger","log","logger"],"sources":["../src/hook-nodes.ts","../src/hook-spec.ts","../src/core/tool-guard/output-truncation.ts","../src/core/tool-guard/label-truncator.ts","../src/core/transform/thinking-validator.ts","../src/core/transform/anthropic-effort.ts","../src/core/session/first-message-variant.ts","../src/core/quality/comment-checker.ts","../src/core/stream/stream-metrics.ts","../src/core/compaction/compaction-logger.ts","../src/core/tool-guard/tool-error-tracker.ts","../src/core/transform/token-budget.ts","../src/core/hook-state.ts","../src/hook-registry.ts","../src/core/edit-files-panel.ts","../src/core/protected-paths.ts","../src/core/permission/command-canonicalize.ts","../src/core/permission/builtin-policies.ts","../src/core/permission/register-builtin.ts","../src/core/permission/always-allow.ts","../src/core/permission/policy-registry.ts","../src/core/permission/audit-log.ts","../src/core/permission/risk.ts","../src/core/permission/permission-guard.ts","../src/core/permission/baseline-provisioning.ts","../src/sandbox-safety.ts","../src/observability/failure-signals.ts","../src/observability/diagnoser.ts"],"sourcesContent":["import type { HookTiming, InterceptorTiming, ObserverTiming } from './types'\n\nexport interface HookNode {\n readonly id: HookTiming\n /** interceptor=可改写(HookPayloadMap output 非 void);observer=仅通知(output void)。 */\n readonly kind: 'interceptor' | 'observer'\n}\n\nexport const HOOK_NODES = [\n { id: 'chat.message.before', kind: 'interceptor' },\n { id: 'chat.message.after', kind: 'interceptor' },\n { id: 'tool.execute.before', kind: 'interceptor' },\n { id: 'tool.execute.after', kind: 'interceptor' },\n { id: 'messages.transform', kind: 'interceptor' },\n { id: 'chat.params', kind: 'interceptor' },\n { id: 'system.prompt.transform', kind: 'interceptor' },\n\n { id: 'session.created', kind: 'observer' },\n { id: 'session.restored', kind: 'observer' },\n { id: 'session.deleted', kind: 'observer' },\n { id: 'session.idle', kind: 'observer' },\n { id: 'session.error', kind: 'observer' },\n { id: 'stream.start', kind: 'observer' },\n { id: 'stream.end', kind: 'observer' },\n { id: 'compaction.before', kind: 'observer' },\n { id: 'compaction.after', kind: 'observer' },\n { id: 'memory.prune.before', kind: 'interceptor' },\n\n { id: 'process.spawned', kind: 'observer' },\n { id: 'process.exited', kind: 'observer' },\n\n { id: 'task.created', kind: 'observer' },\n { id: 'task.started', kind: 'observer' },\n { id: 'task.completed', kind: 'observer' },\n { id: 'task.failed', kind: 'observer' },\n { id: 'task.cancelled', kind: 'observer' },\n\n { id: 'notification', kind: 'observer' },\n] as const satisfies readonly HookNode[]\n\ntype HookNodeId = (typeof HOOK_NODES)[number]['id']\ntype InterceptorNodeId = Extract<(typeof HOOK_NODES)[number], { kind: 'interceptor' }>['id']\ntype ObserverNodeId = Extract<(typeof HOOK_NODES)[number], { kind: 'observer' }>['id']\n\nconst _driftGuard: [\n HookTiming extends HookNodeId ? true : never,\n HookNodeId extends HookTiming ? true : never,\n InterceptorNodeId extends InterceptorTiming ? true : never,\n InterceptorTiming extends InterceptorNodeId ? true : never,\n ObserverNodeId extends ObserverTiming ? true : never,\n ObserverTiming extends ObserverNodeId ? true : never,\n] = [true, true, true, true, true, true]\nvoid _driftGuard\n\nexport const HOOK_TIMINGS: readonly HookTiming[] = HOOK_NODES.map((node) => node.id)\n\nexport const INTERCEPTOR_TIMINGS: ReadonlySet<HookTiming> = new Set(\n HOOK_NODES.filter((node) => node.kind === 'interceptor').map((node) => node.id),\n)\n\nexport function createHookBuckets<T>(): Record<HookTiming, T[]> {\n return Object.fromEntries(HOOK_TIMINGS.map((timing) => [timing, [] as T[]])) as Record<\n HookTiming,\n T[]\n >\n}\n","import type { HookHandle, HookTiming } from './types'\nimport { INTERCEPTOR_TIMINGS } from './hook-nodes'\n\ntype BaseRuntimeHook = {\n name: string\n timing: HookTiming\n priority: number\n enabled: boolean\n}\n\nexport type ObserverRuntimeHook = BaseRuntimeHook & {\n kind: 'observer'\n handle: (input: unknown) => void | Promise<void>\n}\n\nexport type InterceptorRuntimeHook = BaseRuntimeHook & {\n kind: 'interceptor'\n handle: (input: unknown, output: unknown) => void | Promise<void>\n}\n\nexport type HookSpec = ObserverRuntimeHook | InterceptorRuntimeHook\n\nexport { INTERCEPTOR_TIMINGS }\n\n/**\n * 所有 as 强转集中在此处:HookHandle<T> 因函数参数逆变无法自动赋给 (unknown) => void。\n * factory 函数通过 timing 字面量驱动 handle 的类型推断,调用侧零 cast。\n */\nexport function defineHook<T extends HookTiming>(config: {\n name: string\n timing: T\n handle: HookHandle<T>\n priority?: number\n enabled?: boolean\n}): HookSpec {\n const base: BaseRuntimeHook = {\n name: config.name,\n timing: config.timing,\n priority: config.priority ?? 50,\n enabled: config.enabled ?? true,\n }\n\n if (INTERCEPTOR_TIMINGS.has(config.timing)) {\n return {\n ...base,\n kind: 'interceptor',\n handle: config.handle as InterceptorRuntimeHook['handle'],\n }\n }\n\n return {\n ...base,\n kind: 'observer',\n handle: config.handle as ObserverRuntimeHook['handle'],\n }\n}\n","import { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\n\nconst DEFAULT_MAX_OUTPUT_SIZE = 60 * 1024\n\nexport function createOutputTruncationHook(\n maxOutputSize: number = DEFAULT_MAX_OUTPUT_SIZE,\n): HookSpec {\n return defineHook({\n name: 'output-truncation',\n timing: 'tool.execute.after',\n priority: 10,\n handle(_input, output) {\n const { content } = output.result\n let totalSize = 0\n\n for (const part of content) {\n if (part.type === 'text') {\n totalSize += part.text.length\n }\n }\n\n if (totalSize <= maxOutputSize) {\n return\n }\n\n let remaining = maxOutputSize\n const truncatedContent = content\n .map((part) => {\n if (part.type !== 'text') {\n return part\n }\n if (remaining <= 0) {\n return { type: 'text' as const, text: '' }\n }\n if (part.text.length <= remaining) {\n remaining -= part.text.length\n return part\n }\n const truncated = part.text.slice(0, remaining)\n remaining = 0\n return {\n type: 'text' as const,\n text: `${truncated}\\n\\n... [output truncated: ${totalSize} chars → ${maxOutputSize} chars]`,\n }\n })\n .filter((part) => part.type !== 'text' || part.text.length > 0)\n\n output.result = { ...output.result, content: truncatedContent }\n output.metadata.truncated = true\n output.metadata.originalSize = totalSize\n },\n })\n}\n\n","import { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\n\nconst MAX_LABEL_LENGTH = 200\n\nexport function createLabelTruncatorHook(): HookSpec {\n return defineHook({\n name: 'label-truncator',\n timing: 'tool.execute.before',\n priority: 20,\n handle(_input, output) {\n for (const key of ['label', 'description', 'title']) {\n const value = output.args[key]\n if (typeof value === 'string' && value.length > MAX_LABEL_LENGTH) {\n output.args[key] = `${value.slice(0, MAX_LABEL_LENGTH)}...`\n }\n }\n },\n })\n}\n\n","import { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\n\nexport function createThinkingValidatorHook(): HookSpec {\n return defineHook({\n name: 'thinking-validator',\n timing: 'messages.transform',\n priority: 20,\n handle(_input, output) {\n output.messages = output.messages.map((msg) => {\n if (!isAssistantMessage(msg)) {\n return msg\n }\n const content: unknown = msg.content\n if (!Array.isArray(content)) {\n return msg\n }\n\n const filtered = content.filter((part: Record<string, unknown>) => {\n if (\n part.type === 'thinking' &&\n (typeof part.thinking !== 'string' || part.thinking.trim() === '')\n ) {\n return false\n }\n return true\n })\n\n return { ...msg, content: filtered }\n })\n },\n })\n}\n\nfunction isAssistantMessage(msg: unknown): msg is { role: 'assistant'; content: unknown } {\n return typeof msg === 'object' && msg !== null && 'role' in msg && msg.role === 'assistant'\n}\n","import { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\n\nexport function createAnthropicEffortHook(): HookSpec {\n return defineHook({\n name: 'anthropic-effort',\n timing: 'chat.params',\n priority: 10,\n handle(input, output) {\n if (input.provider !== 'anthropic') {\n return\n }\n\n if (!output.thinkingLevel) {\n // if/else if 而非独立 if:模型名理论上可能同时含多个子串(如未来的复合命名),\n // 确保只命中优先级最高的一个而非被后续分支静默覆盖(终局架构review 2026-07-21)。\n if (input.model.includes('opus')) {\n output.thinkingLevel = 'high'\n } else if (input.model.includes('sonnet')) {\n output.thinkingLevel = 'medium'\n } else if (input.model.includes('haiku')) {\n output.thinkingLevel = 'low'\n }\n }\n },\n })\n}\n\n","import { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\n\nexport function createFirstMessageVariantHook(): HookSpec {\n return defineHook({\n name: 'first-message-variant',\n timing: 'chat.message.before',\n priority: 10,\n handle(input, output) {\n if (input.isFirstMessage) {\n output.metadata.isFirstMessage = true\n output.metadata.variant = 'first-message'\n }\n },\n })\n}\n\n","import { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\n\nconst AI_COMMENT_PATTERNS = [\n /\\/\\/\\s*TODO:\\s*implement/i,\n /\\/\\/\\s*add\\s+(your|the)\\s+/i,\n /\\/\\/\\s*placeholder/i,\n /\\/\\/\\s*\\.\\.\\.\\s*(rest|more|other|remaining)/i,\n /\\/\\/\\s*handle\\s+error\\s+here/i,\n /\\/\\*\\s*\\*\\//,\n]\n\nexport function createCommentCheckerHook(): HookSpec {\n return defineHook({\n name: 'comment-checker',\n timing: 'tool.execute.after',\n priority: 20,\n handle(input, output) {\n // 只读工具(read/grep/find/lsp…)不产出待写代码——按注册表声明的 readonly 跳过,\n // 取代硬编码 write/edit 工具名集;非只读工具若不含代码内容由下方 extractCodeContent 兜底 no-op。\n if (input.readonly) {\n return\n }\n\n const code = extractCodeContent(input.args)\n if (!code) {\n return\n }\n\n const violations = detectAiComments(code)\n if (violations.length > 0) {\n const warning = `\\n\\n⚠️ AI-style comments detected:\\n${violations.map((v) => ` - Line ${v.line}: \"${v.text}\"`).join('\\n')}\\nPlease replace with meaningful implementation comments.`\n\n output.result = {\n ...output.result,\n content: [...output.result.content, { type: 'text', text: warning }],\n }\n output.metadata.aiCommentsDetected = violations.length\n }\n },\n })\n}\n\ninterface CommentViolation {\n line: number\n text: string\n}\n\nfunction detectAiComments(code: string): CommentViolation[] {\n const violations: CommentViolation[] = []\n const lines = code.split('\\n')\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i] ?? ''\n for (const pattern of AI_COMMENT_PATTERNS) {\n if (pattern.test(line)) {\n violations.push({ line: i + 1, text: line.trim() })\n break\n }\n }\n }\n return violations\n}\n\nfunction extractCodeContent(args: Record<string, unknown>): string | null {\n if (typeof args.content === 'string') {\n return args.content\n }\n if (typeof args.newString === 'string') {\n return args.newString\n }\n if (typeof args.new_string === 'string') {\n return args.new_string\n }\n return null\n}\n\n","import { createLogger } from '@x-otto/shared'\nimport { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\nimport type { HookState } from '../hook-state'\n\nconst log = createLogger('@x-otto/hooks:stream-metrics')\n\nexport function createStreamMetricsHook(state: HookState): HookSpec {\n return defineHook({\n name: 'stream-metrics',\n timing: 'stream.start',\n priority: 10,\n handle(input) {\n const metrics = state.streamMetrics\n metrics.set(input.sessionId, {\n startTime: Date.now(),\n model: input.model,\n requestCount: (metrics.get(input.sessionId)?.requestCount ?? 0) + 1,\n lastStopReason: '',\n totalDurationMs: metrics.get(input.sessionId)?.totalDurationMs ?? 0,\n })\n log.debug('Stream started: session=%s model=%s', input.sessionId, input.model)\n },\n })\n}\n\nexport function createStreamEndMetricsHook(state: HookState): HookSpec {\n return defineHook({\n name: 'stream-end-metrics',\n timing: 'stream.end',\n priority: 10,\n handle(input) {\n const metrics = state.streamMetrics.get(input.sessionId)\n if (metrics) {\n const duration = Date.now() - metrics.startTime\n metrics.totalDurationMs += duration\n metrics.lastStopReason = input.stopReason\n log.debug(\n 'Stream ended: session=%s model=%s duration=%dms stopReason=%s total=%dms',\n input.sessionId,\n input.model,\n duration,\n input.stopReason,\n metrics.totalDurationMs,\n )\n }\n },\n })\n}\n\n","import { createLogger } from '@x-otto/shared'\nimport { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\nimport type { HookState } from '../hook-state'\n\nconst logger = createLogger('@x-otto/hooks:compaction')\n\nexport function createCompactionLoggerHook(state: HookState): HookSpec {\n return defineHook({\n name: 'compaction-logger',\n timing: 'compaction.before',\n priority: 10,\n handle(input) {\n const stats = state.compactionStats.get(input.sessionId) ?? {\n compactionCount: 0,\n totalCompacted: 0,\n lastCompactionTime: 0,\n }\n\n stats.compactionCount++\n stats.lastCompactionTime = Date.now()\n state.compactionStats.set(input.sessionId, stats)\n\n logger.info(\n 'Compaction started: session=%s messageCount=%d compaction#%d',\n input.sessionId,\n input.messageCount,\n stats.compactionCount,\n )\n },\n })\n}\n\nexport function createCompactionAfterHook(state: HookState): HookSpec {\n return defineHook({\n name: 'compaction-after-logger',\n timing: 'compaction.after',\n priority: 10,\n handle(input) {\n const stats = state.compactionStats.get(input.sessionId)\n if (stats) {\n const duration = Date.now() - stats.lastCompactionTime\n stats.totalCompacted += input.retainedCount\n logger.info(\n 'Compaction finished: session=%s retained=%d duration=%dms totalCompactions=%d',\n input.sessionId,\n input.retainedCount,\n duration,\n stats.compactionCount,\n )\n }\n },\n })\n}\n\n","import { createLogger } from '@x-otto/shared'\nimport { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\nimport type { HookState, ToolErrorRecord } from '../hook-state'\n\nconst log = createLogger('@x-otto/hooks:tool-error-tracker')\n\nexport interface ToolErrorTrackerConfig {\n /** 默认 5。 */\n circuitBreakerThreshold?: number\n /** 默认 120_000 ms。 */\n decayWindowMs?: number\n}\n\nexport function createToolErrorTrackerHook(\n state: HookState,\n config?: ToolErrorTrackerConfig,\n): HookSpec {\n const threshold = config?.circuitBreakerThreshold ?? 5\n const decayMs = config?.decayWindowMs ?? 120_000\n\n return defineHook({\n name: 'tool-error-tracker',\n timing: 'tool.execute.after',\n priority: 15,\n handle(input, output) {\n const toolMap = state.toolErrors.get(input.sessionId) ?? new Map<string, ToolErrorRecord>()\n state.toolErrors.set(input.sessionId, toolMap)\n\n let record = toolMap.get(input.toolName)\n\n if (record && Date.now() - record.lastErrorTime > decayMs) {\n record = undefined\n }\n\n if (input.result.isError) {\n if (!record) {\n record = {\n toolName: input.toolName,\n errorCount: 0,\n lastErrorTime: 0,\n consecutiveErrors: 0,\n }\n }\n record.errorCount++\n record.consecutiveErrors++\n record.lastErrorTime = Date.now()\n toolMap.set(input.toolName, record)\n\n if (record.consecutiveErrors >= threshold) {\n const msg = `Tool \"${input.toolName}\" hit circuit-breaker: ${record.consecutiveErrors} consecutive errors`\n log.warn(msg)\n output.metadata.toolCircuitBreaker = {\n toolName: input.toolName,\n consecutiveErrors: record.consecutiveErrors,\n tripped: true,\n }\n }\n } else if (record) {\n record.consecutiveErrors = 0\n toolMap.set(input.toolName, record)\n }\n },\n })\n}\n\n","import { createLogger } from '@x-otto/shared'\nimport { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\n\nconst log = createLogger('@x-otto/hooks:token-budget')\n\nexport interface TokenBudgetConfig {\n /** 默认 16384。 */\n maxOutputTokens?: number\n}\n\nexport function createTokenBudgetHook(config?: TokenBudgetConfig): HookSpec {\n const maxOutput = config?.maxOutputTokens ?? 16384\n\n return defineHook({\n name: 'token-budget',\n timing: 'chat.params',\n priority: 20,\n handle(input, output) {\n if (!output.maxTokens || output.maxTokens > maxOutput) {\n output.maxTokens = maxOutput\n log.debug('Capped maxTokens to %d for model=%s', maxOutput, input.model)\n }\n },\n })\n}\n\n","export interface StreamMetrics {\n startTime: number\n model: string\n requestCount: number\n lastStopReason: string\n totalDurationMs: number\n}\n\nexport interface CompactionStats {\n compactionCount: number\n totalCompacted: number\n lastCompactionTime: number\n}\n\nexport interface ToolErrorRecord {\n toolName: string\n errorCount: number\n lastErrorTime: number\n consecutiveErrors: number\n}\n\nexport class HookState {\n readonly streamMetrics = new Map<string, StreamMetrics>()\n readonly compactionStats = new Map<string, CompactionStats>()\n readonly toolErrors = new Map<string, Map<string, ToolErrorRecord>>()\n\n /** session.deleted 统一驱逐:registry 生命周期调用,杜绝 per-session 状态泄漏。 */\n clearSession(sessionId: string): void {\n this.streamMetrics.delete(sessionId)\n this.compactionStats.delete(sessionId)\n this.toolErrors.delete(sessionId)\n }\n}\n\n","import { createLogger } from '@x-otto/shared'\nimport { normalizeEnv } from '@x-otto/env'\nimport { createOutputTruncationHook } from './core/tool-guard/output-truncation'\nimport { createLabelTruncatorHook } from './core/tool-guard/label-truncator'\nimport { createThinkingValidatorHook } from './core/transform/thinking-validator'\nimport { createAnthropicEffortHook } from './core/transform/anthropic-effort'\nimport { createFirstMessageVariantHook } from './core/session/first-message-variant'\nimport { createCommentCheckerHook } from './core/quality/comment-checker'\nimport { createStreamMetricsHook, createStreamEndMetricsHook } from './core/stream/stream-metrics'\nimport {\n createCompactionLoggerHook,\n createCompactionAfterHook,\n} from './core/compaction/compaction-logger'\nimport { createToolErrorTrackerHook } from './core/tool-guard/tool-error-tracker'\nimport { createTokenBudgetHook } from './core/transform/token-budget'\nimport { HookState } from './core/hook-state'\n\nimport type {\n HookHandle,\n HookInput,\n HookOutput,\n HookTiming,\n InterceptorTiming,\n ObserverTiming,\n} from './types'\nimport {\n type HookSpec,\n type ObserverRuntimeHook,\n type InterceptorRuntimeHook,\n defineHook,\n} from './hook-spec'\nimport { HOOK_TIMINGS, createHookBuckets } from './hook-nodes'\n\nconst logger = createLogger('@x-otto/hooks')\n\nconst FAIL_CLOSED_TIMINGS = new Set<string>(['tool.execute.before'])\n\n/**\n * RFC-153 D1:interceptor 与 observer 两条 hook 执行路径共享同一个超时阈值——避免两处各自\n *维护一份可能漂移的数字。默认 5000ms 承袭 RFC-143 M0 为 observer 路径选定的值(远超正常 hook\n * 应有的毫秒级/快速 I/O 耗时,又不会误杀慢但终将完成的合法 hook)。硬编码审计 P1:插件生态\n * 下慢 hook 是用户真实调参点,经 `OTTO_HOOK_TIMEOUT_MS` 覆盖(保持单源,两条路径一并生效)。\n */\nconst HOOK_TIMEOUT_MS = normalizeEnv(process.env['OTTO_HOOK_TIMEOUT_MS'], 5000)\n\n// RFC-088 D3:sticky-veto 白名单——从 HOOK_NODES `kind:'interceptor'` 派生子集。\n// 仅包含 HookPayloadMap 中 output 确含 `cancelled`/`decision` 字段的 timing。\n// 其余 interceptor timing(如 chat.params / messages.transform / system.prompt.transform\n// / tool.execute.after)的 output 无此二字段,不做盲探测。\n// 编译期由 _driftGuard(hook-nodes.ts)保证 InterceptorTiming 联合与 HOOK_NODES 表一致。\nconst STICKY_CANCELLED_TIMINGS: ReadonlySet<InterceptorTiming> = new Set([\n 'chat.message.before',\n 'chat.message.after',\n 'tool.execute.before',\n 'memory.prune.before',\n])\nconst STICKY_DECISION_TIMINGS: ReadonlySet<InterceptorTiming> = new Set([\n 'tool.execute.before',\n])\n\nexport { HOOK_TIMINGS }\n\nexport interface RegisteredHookInfo {\n name: string\n timing: HookTiming\n priority: number\n enabled: boolean\n}\n\nexport type HookPreset = 'default' | 'strict' | 'minimal' | 'none'\n\nexport interface HookRegistryOptions {\n preset?: HookPreset\n /** RFC-153 D1:interceptor/observer hook 超时时的旁路通知(可选,宿主层注入)。\n * HookRegistry 本身不认识\"插件\"概念,只把 name/timing/timeoutMs 透传给调用方决定如何处理\n * (如写入 PluginHealthTracker)——保持 packages/hooks 对插件生命周期零认知(规则 7)。 */\n onHookTimeout?: (info: { hookName: string; timing: string; timeoutMs: number }) => void\n}\n\nexport class HookRegistry {\n private readonly hooks = createHookBuckets<HookSpec>()\n private readonly disabled = new Set<string>()\n private readonly onHookTimeout?: HookRegistryOptions['onHookTimeout']\n readonly state = new HookState()\n\n constructor(options?: HookRegistryOptions) {\n this.onHookTimeout = options?.onHookTimeout\n this.register(\n defineHook({\n name: 'hook-state-cleanup',\n timing: 'session.deleted',\n priority: 90,\n handle: (input) => {\n this.state.clearSession(input.sessionId)\n },\n }),\n )\n\n if (options?.preset && options.preset !== 'none') {\n this.applyPreset(options.preset)\n }\n }\n\n register(spec: HookSpec): void {\n this.hooks[spec.timing].push(spec)\n\n logger.debug(\n { hookName: spec.name, timing: spec.timing, priority: spec.priority },\n 'Hook registered',\n )\n }\n\n registerAll(specs: HookSpec[]): void {\n for (const spec of specs) {\n this.register(spec)\n }\n }\n\n on<T extends HookTiming>(timing: T, name: string, handle: HookHandle<T>, priority = 50): this {\n this.register(defineHook({ name, timing, handle, priority }))\n\n return this\n }\n\n has(name: string): boolean {\n return this.getRegistered().some((h) => h.name === name)\n }\n\n unregister(name: string): boolean {\n let removed = false\n for (const timing of HOOK_TIMINGS) {\n const list = this.hooks[timing]\n const filtered = list.filter((hook) => hook.name !== name)\n\n if (filtered.length < list.length) {\n this.hooks[timing] = filtered\n removed = true\n\n logger.debug({ hookName: name, timing }, 'Hook unregistered')\n }\n }\n return removed\n }\n\n disable(name: string): void {\n this.disabled.add(name)\n logger.debug({ hookName: name }, 'Hook disabled')\n }\n\n enable(name: string): void {\n this.disabled.delete(name)\n logger.debug({ hookName: name }, 'Hook enabled')\n }\n\n getRegistered(timing?: HookTiming): RegisteredHookInfo[] {\n if (timing) {\n return this.hooks[timing].map(toHookInfo)\n }\n\n const all: RegisteredHookInfo[] = []\n for (const key of HOOK_TIMINGS) {\n all.push(...this.hooks[key].map(toHookInfo))\n }\n\n return all\n }\n\n async execute<T extends InterceptorTiming>(\n timing: T,\n input: HookInput<T>,\n output: HookOutput<T>,\n ): Promise<void> {\n const hooks = this.getSortedInterceptorHooks(timing)\n if (hooks.length === 0) {\n return\n }\n\n // RFC-088 D3:sticky-veto 白名单——只有这些 interceptor timing 的 output 有 cancelled/decision\n // 字段(由 HookPayloadMap 编译期担保),其余 timing 不做盲探测。\n\n let stickyCancelled = false\n let stickyDeny = false\n for (const hook of hooks) {\n const result = await this.runInterceptorWithTimeout(hook, timing, input, output)\n if (!result.ok) {\n if (FAIL_CLOSED_TIMINGS.has(timing)) {\n const out = output as { cancelled?: boolean; cancelReason?: string }\n if (typeof out.cancelled === 'boolean') {\n out.cancelled = true\n out.cancelReason = result.timedOut\n ? `hook \"${hook.name}\" timed out after ${HOOK_TIMEOUT_MS}ms`\n : `hook \"${hook.name}\" failed: ${result.message}`\n }\n }\n // 非 fail-closed timing:与既有\"hook 抛同步异常\"行为完全同构——跳过该 hook,\n // 用当前累积的 output 值继续走后续 hook(RFC-153 D1 skip 语义)。\n }\n\n // sticky-veto:仅当本 timing 的 HookPayloadMap output 确实含对应字段时探测。\n if (STICKY_CANCELLED_TIMINGS.has(timing)) {\n const out = output as { cancelled?: boolean }\n if (out.cancelled === true) stickyCancelled = true\n if (stickyCancelled) out.cancelled = true\n }\n if (STICKY_DECISION_TIMINGS.has(timing)) {\n const out = output as { decision?: string }\n if (out.decision === 'deny') stickyDeny = true\n if (stickyDeny) out.decision = 'deny'\n }\n }\n }\n\n /**\n * RFC-153 D1:interceptor hooks 挂起兜底——`execute()` 循环内此前是裸 `await hook.handle(...)`,\n * 挂起(无限循环/无限 fetch/死锁)的 hook 会永久阻塞整个 agent turn。复用 observer 路径\n * (`runObserverHookWithTimeout`)已验证的 `Promise.race` + `unref()` 模式,阈值改用两路径共享的\n * `HOOK_TIMEOUT_MS` 常量(规则 1:超时不等于中止——不承诺真正终止 hook 内部执行)。\n * 超时命中时通过 `onHookTimeout` 回调向上冒泡(规则 7:只携带领域无关字段,`packages/hooks`\n * 不认识\"插件\"概念),调用方(`packages/coding`)负责解析 hookName 里的 pluginId 前缀。\n */\n private async runInterceptorWithTimeout<T extends InterceptorTiming>(\n hook: InterceptorRuntimeHook,\n timing: T,\n input: HookInput<T>,\n output: HookOutput<T>,\n ): Promise<{ ok: true } | { ok: false; timedOut: boolean; message: string }> {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n await Promise.race([\n hook.handle(input, output),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`hook \"${hook.name}\" timed out after ${HOOK_TIMEOUT_MS}ms`))\n }, HOOK_TIMEOUT_MS)\n timer.unref?.()\n }),\n ])\n return { ok: true }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n const timedOut = message.includes('timed out after')\n logger.error({ name: hook.name, timing, err: message, timedOut }, 'Hook failed or timed out')\n\n if (timedOut) {\n this.onHookTimeout?.({ hookName: hook.name, timing, timeoutMs: HOOK_TIMEOUT_MS })\n }\n\n return { ok: false, timedOut, message }\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n }\n\n /**\n * RFC-143 M0:observer hook 挂起兜底。`emit()` 此前的 try/catch 只吞同步抛错和 rejected\n * promise,不吞「handler 挂起」(无限循环/无限 fetch/死锁)——`await hook.handle(input)` 会\n * 无限期阻塞调用方(如用户显式 `removeSession`、task 生命周期判定、流式启动)。\n * 超时阈值使用与 interceptor 路径(`runInterceptorWithTimeout`)共享的模块级常量\n * `HOOK_TIMEOUT_MS`(RFC-153 D1:避免两条路径各自维护一份可能漂移的数字,此前本方法\n * 独立持有的 `EMIT_TIMEOUT_MS` 已废弃合并)。`unref()` 避免这个定时器阻塞进程退出。\n */\n private async runObserverHookWithTimeout<T extends ObserverTiming>(\n hook: ObserverRuntimeHook,\n timing: T,\n input: HookInput<T>,\n ): Promise<void> {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n await Promise.race([\n hook.handle(input),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new Error(`hook \"${hook.name}\" timed out after ${HOOK_TIMEOUT_MS}ms`))\n }, HOOK_TIMEOUT_MS)\n timer.unref?.()\n }),\n ])\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n logger.error({ name: hook.name, timing, err: message }, 'Hook failed or timed out')\n } finally {\n if (timer !== undefined) clearTimeout(timer)\n }\n }\n\n /**\n * RFC-143 R2a:`session.error` timing 的 input 契约上携带原始 `Error` 对象\n * (`HookPayloadMap['session.error'].input.error: Error`,见 hook-contracts/timings.ts)。\n * SDK 层(`module-wiring.ts`)已对 definePlugin 消费路径做 `error.message` 转换,但那只是\n * 单一消费方的自觉行为——emit() 是所有 observer hook(含非 SDK 注册路径,如未来的引擎内部\n * 钩子/测试工具)的唯一入口,必须在这里做结构式 sanitize,防止任何消费方读到含栈追踪/敏感\n * 路径信息的原始 Error 对象。sanitize 后的 input 只对 `session.error` 生效,其余 timing 原样传递。\n */\n private sanitizeEmitInput<T extends ObserverTiming>(timing: T, input: HookInput<T>): HookInput<T> {\n if (timing !== 'session.error') return input\n\n const raw = input as unknown as { sessionId: string; metadata: Record<string, unknown>; error: Error }\n const sanitized = {\n sessionId: raw.sessionId,\n metadata: raw.metadata,\n error: raw.error instanceof Error ? raw.error.message : String(raw.error),\n }\n return sanitized as unknown as HookInput<T>\n }\n\n async emit<T extends ObserverTiming>(timing: T, input: HookInput<T>): Promise<void> {\n const hooks = this.getSortedObserverHooks(timing)\n if (hooks.length === 0) {\n return\n }\n\n const sanitizedInput = this.sanitizeEmitInput(timing, input)\n\n for (const hook of hooks) {\n await this.runObserverHookWithTimeout(hook, timing, sanitizedInput)\n }\n }\n\n clear(): void {\n for (const timing of HOOK_TIMINGS) {\n this.hooks[timing] = []\n }\n this.disabled.clear()\n logger.debug(`All hooks cleared`)\n }\n\n private getSortedInterceptorHooks(timing: InterceptorTiming): InterceptorRuntimeHook[] {\n const bucket = this.hooks[timing]\n if (!bucket) {\n logger.warn(\n { timing },\n 'Unknown hook timing (no bucket) — emit name mismatch with HOOK_NODES?',\n )\n return []\n }\n return bucket\n .filter(\n (hook): hook is InterceptorRuntimeHook =>\n hook.kind === 'interceptor' && hook.enabled && !this.disabled.has(hook.name),\n )\n .sort((a, b) => a.priority - b.priority)\n }\n\n private getSortedObserverHooks(timing: ObserverTiming): ObserverRuntimeHook[] {\n const bucket = this.hooks[timing]\n if (!bucket) {\n logger.warn(\n { timing },\n 'Unknown hook timing (no bucket) — emit name mismatch with HOOK_NODES?',\n )\n return []\n }\n return bucket\n .filter(\n (hook): hook is ObserverRuntimeHook =>\n hook.kind === 'observer' && hook.enabled && !this.disabled.has(hook.name),\n )\n .sort((a, b) => a.priority - b.priority)\n }\n\n private applyPreset(preset: HookPreset): void {\n const hooks = getPresetHooks(preset, this.state)\n for (const hook of hooks) {\n this.register(hook)\n }\n }\n}\n\nfunction toHookInfo(hook: HookSpec): RegisteredHookInfo {\n return {\n name: hook.name,\n timing: hook.timing,\n priority: hook.priority,\n enabled: hook.enabled,\n }\n}\n\nfunction getPresetHooks(preset: HookPreset, state: HookState): HookSpec[] {\n switch (preset) {\n case 'default':\n return getDefaultPresetHooks(state)\n case 'strict':\n return getStrictPresetHooks(state)\n case 'minimal':\n return getMinimalPresetHooks()\n default:\n return []\n }\n}\n\nfunction getDefaultPresetHooks(state: HookState): HookSpec[] {\n // RFC-077 M3-B:删 standalone createFileGuardHook——protected-path 写保护已由权限策略引擎单源\n // (createFileGuardPolicy + permission-guard hook,含 defense-in-depth 路径解析)接管。bare-runtime\n // 经 provisionBaselineSecurity 获引擎、coding 经 PermissionController,覆盖不缩水(D6)。\n return [\n createOutputTruncationHook(),\n createLabelTruncatorHook(),\n createThinkingValidatorHook(),\n createAnthropicEffortHook(),\n createFirstMessageVariantHook(),\n createStreamMetricsHook(state),\n createStreamEndMetricsHook(state),\n createCompactionLoggerHook(state),\n createCompactionAfterHook(state),\n createToolErrorTrackerHook(state),\n createTokenBudgetHook(),\n ]\n}\n\nfunction getStrictPresetHooks(state: HookState): HookSpec[] {\n return [...getDefaultPresetHooks(state), createCommentCheckerHook()]\n}\n\nfunction getMinimalPresetHooks(): HookSpec[] {\n return [createOutputTruncationHook()]\n}\n\nexport function createHookRegistry(options?: HookRegistryOptions): HookRegistry {\n return new HookRegistry(options)\n}\n","import { defineHook } from '../hook-spec'\nimport type { HookSpec } from '../hook-spec'\nimport type { ToolExecuteAfterInput } from '@x-otto/hook-contracts'\n\n/** 编辑文件记录的内部类型(hooks 层使用,不耦合 tui 的 EditedFileEntry)。 */\nexport interface EditFileInfo {\n path: string\n operation: 'edit' | 'write' | 'delete'\n oldContent?: string\n newContent?: string\n addedLines: number\n removedLines: number\n timestamp: number\n toolCallId: string\n}\n\n/** 文件修改回调(CLI 层负责适配到 TuiHandle.addEditedFile) */\nexport type EditFileCallback = (info: EditFileInfo) => void\n\nfunction countLines(s: string): number {\n if (!s) return 0\n const stripped = s.endsWith('\\n') ? s.slice(0, -1) : s\n return stripped.split('\\n').length\n}\n\n// 工具身份按 args 形状派生(edit=oldContent+newContent / write=content),不再硬编码工具名集;\n// readonly 工具已在 handle 入口被 readonly gate 挡掉,此处只面对会改文件的工具。\nfunction extractFileInfo(input: ToolExecuteAfterInput): EditFileInfo | null {\n const { toolCallId, args } = input\n const path = typeof args.path === 'string' ? args.path : undefined\n if (!path) return null\n\n const oldContent = typeof args.oldContent === 'string' ? args.oldContent : undefined\n const newContent = typeof args.newContent === 'string' ? args.newContent : undefined\n if (oldContent !== undefined && newContent !== undefined) {\n const oldLines = countLines(oldContent)\n const newLines = countLines(newContent)\n\n return {\n path,\n operation: 'edit',\n oldContent,\n newContent,\n addedLines: Math.max(0, newLines - oldLines),\n removedLines: Math.max(0, oldLines - newLines),\n timestamp: Date.now(),\n toolCallId,\n }\n }\n\n const content = typeof args.content === 'string' ? args.content : undefined\n if (content !== undefined) {\n const newLines = countLines(content)\n\n return {\n path,\n operation: 'write',\n newContent: content,\n addedLines: newLines,\n removedLines: 0,\n timestamp: Date.now(),\n toolCallId,\n }\n }\n\n return null\n}\n\nexport interface EditFilesPanelHookOptions {\n onFileEdited?: EditFileCallback\n /** 延迟获取回调——用于注册时 TUI 尚未就绪的场景(取值时机=emit 时而非 register 时)。优先级高于 onFileEdited。 */\n getOnFileEdited?: () => EditFileCallback | undefined\n dedupCache?: Set<string> | null\n}\n\n/**\n * 创建 hook spec,注册后在每次 edit/write 工具执行完成后累积回写 TUI。\n *\n * 用法:\n * const hook = createEditFilesPanelHook({\n * onFileEdited: (info) => tuiHandle.addEditedFile({\n * ...info,\n * addedLines: info.addedLines,\n * removedLines: info.removedLines,\n * turnId: currentTurnId,\n * }),\n * })\n * hookRegistry.register(hook)\n */\nexport function createEditFilesPanelHook(options: EditFilesPanelHookOptions = {}): HookSpec {\n const { onFileEdited, getOnFileEdited, dedupCache } = options\n\n return defineHook({\n name: 'edit-files-panel',\n timing: 'tool.execute.after',\n priority: 90,\n handle(input) {\n const callback = getOnFileEdited?.() ?? onFileEdited\n if (!callback) return\n\n // 只读工具按注册表声明跳过——会改文件的工具才进入形状派生。\n if (input.readonly) return\n\n const info = extractFileInfo(input)\n if (!info) return\n\n const dedupKey = info.toolCallId\n if (dedupCache) {\n if (dedupCache.has(dedupKey)) return\n dedupCache.add(dedupKey)\n }\n\n callback(info)\n },\n })\n}\n","export const PROTECTED_PATH_PATTERNS: RegExp[] = [\n /^\\/etc\\//,\n /^\\/usr\\//,\n /^\\/sys\\//,\n /^\\/proc\\//,\n /node_modules\\//,\n /\\.git\\//,\n /\\.env$/,\n /\\.env\\.local$/,\n\n /\\/\\.ssh\\//,\n /\\/\\.aws\\//,\n /\\/\\.(bash|zsh)rc$/,\n /\\/\\.(bash_profile|zprofile|profile)$/,\n]\n","const MAX_DEPTH = 12\n\n/** 顶层按 shell 运算符拆分(&& || ; | & 换行),尊重单/双引号。 */\nexport function splitTopLevel(command: string): string[] {\n const parts: string[] = []\n let buf = ''\n let sq = false\n let dq = false\n for (let i = 0; i < command.length; i++) {\n const ch = command[i]!\n const next = command[i + 1]\n if (sq) {\n if (ch === \"'\") sq = false\n buf += ch\n continue\n }\n if (dq) {\n if (ch === '\"') dq = false\n buf += ch\n continue\n }\n if (ch === \"'\") {\n sq = true\n buf += ch\n continue\n }\n if (ch === '\"') {\n dq = true\n buf += ch\n continue\n }\n if ((ch === '&' && next === '&') || (ch === '|' && next === '|')) {\n parts.push(buf)\n buf = ''\n i++\n continue\n }\n if (ch === ';' || ch === '|' || ch === '\\n' || ch === '&') {\n parts.push(buf)\n buf = ''\n continue\n }\n buf += ch\n }\n parts.push(buf)\n return parts.map((p) => p.trim()).filter(Boolean)\n}\n\nfunction stripOuterQuotes(s: string): string {\n const t = s.trim()\n if (\n t.length >= 2 &&\n ((t[0] === '\"' && t[t.length - 1] === '\"') || (t[0] === \"'\" && t[t.length - 1] === \"'\"))\n ) {\n return t.slice(1, -1)\n }\n return t\n}\n\n/** 解包解释器:bash/sh/zsh -c \"script\"、python/node/perl/ruby -e|-c \"code\" → 内层脚本。 */\nexport function unwrapInterpreter(command: string): string | null {\n const m = command.match(\n /^(?:\\S*\\/)?(?:bash|sh|zsh|dash|ksh|ash|python[0-9.]*|node|nodejs|perl|ruby|php|deno|bun)\\b[\\s\\S]*?\\s-[a-z]*[cer]\\b\\s*([\\s\\S]+)$/i,\n )\n return m ? stripOuterQuotes(m[1]!) : null\n}\n\n/** 剥离前缀包装:env VAR=val、sudo、nohup、nice、timeout N、stdbuf、ionice、setsid。 */\nexport function stripPrefixes(command: string): string {\n let c = command.trim()\n for (let guard = 0; guard < 10; guard++) {\n const before = c\n c = c.replace(/^[A-Za-z_][A-Za-z0-9_]*=(?:\"[^\"]*\"|'[^']*'|\\S*)\\s+/, '')\n c = c.replace(\n /^(?:sudo|env|nohup|setsid|ionice)\\b(?:\\s+-\\S+|\\s+[A-Za-z_][A-Za-z0-9_]*=\\S+)*\\s+/i,\n '',\n )\n c = c.replace(/^(?:nice|stdbuf)\\b(?:\\s+-\\S+)*\\s+/i, '')\n c = c.replace(/^timeout\\b(?:\\s+-\\S+)*\\s+\\S+\\s+/i, '')\n if (c === before) break\n }\n return c\n}\n\n/** 抽取命令替换内容:$( ... )(平衡括号)与 backtick。 */\nexport function extractSubstitutions(command: string): string[] {\n const out: string[] = []\n for (let i = 0; i < command.length; i++) {\n if (command[i] === '$' && command[i + 1] === '(') {\n let depth = 1\n let j = i + 2\n const start = j\n while (j < command.length && depth > 0) {\n if (command[j] === '(') depth++\n else if (command[j] === ')') depth--\n j++\n }\n if (depth === 0) out.push(command.slice(start, j - 1))\n }\n }\n const bt = command.match(/`([^`]+)`/g)\n if (bt) for (const b of bt) out.push(b.slice(1, -1))\n return out\n}\n\nconst SIMPLE_ANSI_C_ESCAPES: Record<string, string> = {\n n: '\\n',\n t: '\\t',\n r: '\\r',\n a: '\\x07',\n b: '\\b',\n f: '\\f',\n v: '\\v',\n e: '\\x1b',\n E: '\\x1b',\n '\\\\': '\\\\',\n \"'\": \"'\",\n '\"': '\"',\n '?': '?',\n}\n\nfunction isHex(ch: string): boolean {\n return /[0-9a-fA-F]/.test(ch)\n}\n\nfunction decodeAnsiCBody(body: string): string {\n let out = ''\n let i = 0\n while (i < body.length) {\n if (body[i] !== '\\\\') {\n out += body[i]\n i++\n continue\n }\n const c = body[i + 1]\n if (c === undefined) {\n out += '\\\\'\n break\n }\n if (c === 'x') {\n let hex = ''\n while (hex.length < 2 && isHex(body[i + 2 + hex.length] ?? ''))\n hex += body[i + 2 + hex.length]\n if (hex) {\n out += String.fromCharCode(parseInt(hex, 16))\n i += 2 + hex.length\n continue\n }\n }\n if (c === 'u' || c === 'U') {\n const max = c === 'u' ? 4 : 8\n let hex = ''\n while (hex.length < max && isHex(body[i + 2 + hex.length] ?? ''))\n hex += body[i + 2 + hex.length]\n if (hex) {\n out += String.fromCodePoint(parseInt(hex, 16))\n i += 2 + hex.length\n continue\n }\n }\n if (c >= '0' && c <= '7') {\n let oct = ''\n while (oct.length < 3) {\n const d = body[i + 1 + oct.length] ?? ''\n if (d < '0' || d > '7') break\n oct += d\n }\n out += String.fromCharCode(parseInt(oct, 8) & 0xff)\n i += 1 + oct.length\n continue\n }\n out += SIMPLE_ANSI_C_ESCAPES[c] ?? c\n i += 2\n }\n return out\n}\n\n/** 解码 ANSI-C 引用 `$'...'`(\\xHH、\\NNN 八进制、\\uHHHH/\\UHHHHHHHH、常见转义),消除十六进制/八进制混淆。 */\nexport function decodeAnsiCQuoting(command: string): string {\n return command.replace(/\\$'((?:\\\\.|[^'\\\\])*)'/g, (_m, body: string) => decodeAnsiCBody(body))\n}\n\n/** 归一 `${IFS}`/`$IFS`(含 `${IFS%??}`、`$IFS$9` 等分词技巧)为空格,消除无空格分词绕过。 */\nexport function normalizeIfs(command: string): string {\n return command.replace(/\\$\\{IFS[^}]*\\}/g, ' ').replace(/\\$IFS\\b(?:\\$[0-9@*])?/g, ' ')\n}\n\nfunction deobfuscate(command: string): string {\n return normalizeIfs(decodeAnsiCQuoting(command))\n}\n\n/**\n * 综合:返回一条命令的所有可执行片段(含原始串、去混淆形、拆分子命令、解包内层、替换内容、\n * here-string payload),去重。destructive/suspicious/interpreter 模式对每个片段独立检查。\n */\nexport function extractShellCommands(command: string): string[] {\n const seen = new Set<string>()\n const out: string[] = []\n const visit = (cmd: string, depth: number): void => {\n const t = cmd.trim()\n if (!t || depth > MAX_DEPTH || seen.has(t)) return\n seen.add(t)\n out.push(t)\n\n const deobf = deobfuscate(t)\n if (deobf !== t) visit(deobf, depth + 1)\n\n const hsPayload = t.match(/<<<\\s*('[^']*'|\"[^\"]*\"|[^\\s|;&]+)/)?.[1]\n if (hsPayload) visit(stripOuterQuotes(hsPayload), depth + 1)\n\n for (const sub of extractSubstitutions(t)) visit(sub, depth + 1)\n\n const parts = splitTopLevel(t)\n if (parts.length > 1) {\n for (const p of parts) visit(p, depth + 1)\n return\n }\n\n const inner = unwrapInterpreter(t)\n if (inner) visit(inner, depth + 1)\n const stripped = stripPrefixes(t)\n if (stripped !== t) visit(stripped, depth + 1)\n }\n visit(command, 0)\n return out\n}\n\nfunction unquote(token: string): string {\n if (\n token.length >= 2 &&\n ((token[0] === '\"' && token.endsWith('\"')) || (token[0] === \"'\" && token.endsWith(\"'\")))\n ) {\n return token.slice(1, -1)\n }\n return token\n}\n\nexport function tokenize(fragment: string): string[] {\n const tokens: string[] = []\n let buf = ''\n let sq = false\n let dq = false\n for (let i = 0; i < fragment.length; i++) {\n const ch = fragment[i]!\n if (sq) {\n if (ch === \"'\") sq = false\n else buf += ch\n continue\n }\n if (dq) {\n if (ch === '\"') dq = false\n else buf += ch\n continue\n }\n if (ch === \"'\") {\n sq = true\n continue\n }\n if (ch === '\"') {\n dq = true\n continue\n }\n if (ch === ' ' || ch === '\\t') {\n if (buf) {\n tokens.push(buf)\n buf = ''\n }\n continue\n }\n buf += ch\n }\n if (buf) tokens.push(buf)\n return tokens\n}\n\nconst WRITE_REDIRECT = /(?:^|[^0-9&>])(?:[0-9]*&?>>?|>\\|)\\s*('[^']*'|\"[^\"]*\"|[^\\s;|&<>]+)/g\n\n/**\n * 从一条 bash 命令启发式提取**写入目标路径**(M4 Phase 2 前置 / C2 加固)。\n *\n * bash 无 pathParams,沙箱 assess 看不到 shell 写目标 → 默认开会让越界 bash 写硬失败。\n * 本函数解析每个子片段的写向量,喂给 assess 以便升级为 ask 而非硬失败:\n * - 重定向 `> f` `>> f` `2> f` `&> f` `>| f`\n * - `tee [-a] f...`、`cp/mv/install/ln ... dst`、`dd of=f`、`touch/rm/rmdir/mkdir f...`\n * 启发式(非 tree-sitter):覆盖常见形态;复杂引用/变量间接漏网者仍由沙箱兜底(硬失败,\n * 不弱于现状)。返回去重路径(相对/绝对原样,由 assess 解析)。\n */\nexport function extractWriteTargets(command: string): string[] {\n const targets = new Set<string>()\n const add = (p: string | undefined): void => {\n if (!p) return\n const v = unquote(p.trim())\n if (\n v &&\n !v.startsWith('-') &&\n !v.includes('$') &&\n v !== '/dev/null' &&\n v !== '/dev/stdout' &&\n v !== '/dev/stderr'\n ) {\n targets.add(v)\n }\n }\n\n for (const frag of extractShellCommands(command)) {\n let m: RegExpExecArray | null\n WRITE_REDIRECT.lastIndex = 0\n while ((m = WRITE_REDIRECT.exec(frag)) !== null) {\n add(m[1])\n }\n\n const tokens = tokenize(frag)\n if (tokens.length === 0) continue\n const cmd = tokens[0]!.replace(/^.*\\//, '')\n const rest = tokens.slice(1)\n const nonFlag = rest.filter((t) => !t.startsWith('-'))\n\n if (cmd === 'tee') {\n for (const t of nonFlag) add(t)\n } else if (cmd === 'cp' || cmd === 'mv' || cmd === 'install' || cmd === 'ln') {\n if (nonFlag.length >= 1) add(nonFlag[nonFlag.length - 1])\n } else if (cmd === 'touch' || cmd === 'rm' || cmd === 'rmdir' || cmd === 'mkdir') {\n for (const t of nonFlag) add(t)\n } else if (cmd === 'dd') {\n for (const t of rest) {\n if (t.startsWith('of=')) add(t.slice(3))\n }\n }\n }\n return [...targets]\n}\n","import type { PermissionPolicy, PermissionMode } from './types'\nimport { PROTECTED_PATH_PATTERNS } from '../protected-paths'\nimport { extractShellCommands, tokenize } from './command-canonicalize'\n\nexport interface PermissionToolDescriptor {\n name: string\n readonly?: boolean\n metadata?: {\n category?: string\n }\n}\n\nexport interface PermissionToolSets {\n readonlyTools: Set<string>\n writeTools: Set<string>\n fileWriteTools: Set<string>\n}\n\nexport interface PermissionToolSetsInput {\n readonlyTools?: Iterable<string>\n writeTools?: Iterable<string>\n fileWriteTools?: Iterable<string>\n}\n\nfunction resolvePermissionToolSets(toolSets?: PermissionToolSetsInput): PermissionToolSets {\n return {\n readonlyTools: new Set(toolSets?.readonlyTools ?? []),\n writeTools: new Set(toolSets?.writeTools ?? []),\n fileWriteTools: new Set(toolSets?.fileWriteTools ?? []),\n }\n}\n\nexport function createPermissionToolSetsFromRegistry(\n tools: Iterable<PermissionToolDescriptor>,\n): PermissionToolSets {\n const readonlyTools = new Set<string>()\n const writeTools = new Set<string>()\n const fileWriteTools = new Set<string>()\n\n for (const tool of tools) {\n if (!tool.name) {\n continue\n }\n\n if (tool.readonly === true) {\n readonlyTools.add(tool.name)\n continue\n }\n\n if (tool.name === 'bash') {\n writeTools.add(tool.name)\n continue\n }\n\n if (tool.metadata?.category === 'fs') {\n writeTools.add(tool.name)\n fileWriteTools.add(tool.name)\n continue\n }\n\n if (tool.name.startsWith('mcp__')) {\n writeTools.add(tool.name)\n }\n }\n\n return {\n readonlyTools,\n writeTools,\n fileWriteTools,\n }\n}\n\nexport function createFileGuardPolicy(toolSets?: PermissionToolSetsInput): PermissionPolicy {\n const { fileWriteTools } = resolvePermissionToolSets(toolSets)\n\n return {\n name: 'builtin::file-guard',\n priority: 10,\n enabled: true,\n safety: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'protect-system-paths',\n effect: 'deny',\n match: {\n tools: [...fileWriteTools],\n paths: PROTECTED_PATH_PATTERNS,\n },\n denyReason: 'Write to protected system path is not allowed',\n },\n ],\n }\n}\n\n// 注:`rm` 的破坏性判定**不在**本正则——位置式 `rm\\s+-[a-z]*[rf]` 会漏 `rm --recursive --force`、\n// `rm -v -rf x`(中间夹无关 flag)、`rm important -rf`(操作数在前,GNU rm 会重排参数)。改由\n// rmIsDestructive() 按 argv token 逐个扫 -r/-f/--recursive/--force(复用 tokenize 去引号,与去混淆同源)。\nconst DESTRUCTIVE_PATTERN =\n /\\b(rmdir\\b|drop\\s+table|git\\s+push\\s+(?:\\S+\\s+)*(?:--force\\b|-f\\b)|git\\s+reset\\s+--hard|git\\s+clean\\s+-[a-z]*f|truncate\\s+table|dd\\s+if=|mkfs\\b|>\\s*\\/dev\\/(?:sd|nvme|disk|hd))/i\n\n/**\n * B1:`rm` 是否带递归/强制标志——按 argv token 扫,不依赖标志紧跟 `rm `。\n * 覆盖 `rm -rf`、`rm -Rf`、`rm --recursive --force`、`rm -v -rf x`、`rm important -rf`(操作数在前)、\n * `/bin/rm -rf`(带路径)。短旗用 `[rf]`(i 标志同时匹配大写 R/F);长旗匹配 --recursive/--force。\n * 与原正则同口径:仅 `r`/`f` 类视为破坏性(裸 `rm file` 不触发,保持既有 UX)。\n */\nfunction rmIsDestructive(fragment: string): boolean {\n const tokens = tokenize(fragment)\n if (tokens.length === 0) return false\n const cmd = tokens[0]!.replace(/^.*\\//, '')\n if (cmd !== 'rm') return false\n return tokens.slice(1).some((t) => {\n if (t === '--recursive' || t === '--force') return true\n return /^-[a-z]*[rf]/i.test(t)\n })\n}\nconst SUSPICIOUS_PATTERN =\n /(\\beval\\s|(?:curl|wget)\\b[^|;&]*\\|\\s*(?:sudo\\s+)?(?:ba|z|d)?sh\\b|:\\s*\\(\\s*\\)\\s*\\{[^}]*:\\s*\\|\\s*:|base64\\s+(?:-d|--decode)\\b[^|]*\\|)/i\n\nconst INTERPRETER_INLINE_EXEC =\n /\\b(?:python[0-9.]*|pythonw|pypy[0-9]*|node|nodejs|deno|bun|perl|ruby|php|osascript|Rscript|pwsh|powershell(?:\\.exe)?)\\b[^|;&\\n]*?(?:\\s-{1,2}(?:c|e|r|E|p|eval|Command|EncodedCommand)\\b|\\s*<<)/i\n\nfunction commandTriggersGuard(command: string): boolean {\n if (!command) return false\n const fragments = extractShellCommands(command)\n return fragments.some(\n (f) =>\n DESTRUCTIVE_PATTERN.test(f) ||\n rmIsDestructive(f) ||\n SUSPICIOUS_PATTERN.test(f) ||\n INTERPRETER_INLINE_EXEC.test(f),\n )\n}\n\nfunction mcpAnyArgTriggersGuard(toolName: string, args: Record<string, unknown>): boolean {\n if (!toolName.startsWith('mcp__')) return false\n return Object.values(args).some((v) => typeof v === 'string' && commandTriggersGuard(v))\n}\n\nexport const DESTRUCTIVE_COMMAND_POLICY: PermissionPolicy = {\n name: 'builtin::destructive-guard',\n priority: 15,\n enabled: true,\n safety: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'ask-destructive-bash',\n effect: 'ask',\n match: {\n condition: (ctx) => commandTriggersGuard(String(ctx.args.command ?? '')),\n },\n askPrompt:\n 'This command may be destructive or runs code that cannot be statically checked. Continue?',\n },\n {\n name: 'ask-destructive-mcp-args',\n effect: 'ask',\n match: {\n condition: (ctx) => mcpAnyArgTriggersGuard(ctx.toolName, ctx.args),\n },\n askPrompt: 'This MCP tool argument may run a destructive or unverifiable command. Continue?',\n },\n ],\n}\n\nconst CREDENTIAL_DENY_PATH_PATTERNS: RegExp[] = [\n /\\/\\.ssh(\\/|$)/,\n /\\/\\.aws(\\/|$)/,\n /\\/\\.gnupg(\\/|$)/,\n /\\/otto\\/auth\\.json$/,\n]\nconst CREDENTIAL_ASK_PATH_PATTERNS: RegExp[] = [/\\/\\.env(\\.[\\w.-]+)?$/]\nconst CREDENTIAL_DENY_COMMAND_PATTERNS: RegExp[] = [\n /\\.ssh(\\/|\\s|$)/,\n /\\.aws(\\/|\\s|$)/,\n /\\.gnupg(\\/|\\s|$)/,\n /otto\\/auth\\.json/,\n]\nconst CREDENTIAL_ASK_COMMAND_PATTERNS: RegExp[] = [/\\.env(\\b|\\.[\\w-]+)/]\n\n// B2:凭证守卫必须看**去混淆后**的命令片段,与 commandTriggersGuard 同源(extractShellCommands)。\n// 否则 `cat $'\\x2e\\x73\\x73\\x68/id_rsa'`(ANSI-C 编码 ~/.ssh)能绕过硬 deny。extractShellCommands\n// 首元素即原始串,故按片段匹配是原「匹配原始串」的严格超集——只增覆盖、不丢覆盖。\nfunction commandMatchesAny(command: string, patterns: RegExp[]): boolean {\n if (!command) return false\n return extractShellCommands(command).some((frag) => patterns.some((p) => p.test(frag)))\n}\n\nfunction mcpArgsMatchAny(\n toolName: string,\n args: Record<string, unknown>,\n patterns: RegExp[],\n): boolean {\n if (!toolName.startsWith('mcp__')) return false\n for (const v of Object.values(args)) {\n if (typeof v === 'string' && commandMatchesAny(v, patterns)) return true\n }\n return false\n}\n\nexport const CREDENTIAL_READ_POLICY: PermissionPolicy = {\n name: 'builtin::credential-guard',\n priority: 16,\n enabled: true,\n safety: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'deny-credential-paths',\n effect: 'deny',\n match: { paths: CREDENTIAL_DENY_PATH_PATTERNS },\n denyReason:\n 'Reading credential/secret files (~/.ssh, ~/.aws, ~/.gnupg, auth store) is not allowed',\n },\n {\n name: 'deny-credential-command',\n effect: 'deny',\n match: {\n condition: (ctx) =>\n commandMatchesAny(String(ctx.args.command ?? ''), CREDENTIAL_DENY_COMMAND_PATTERNS),\n },\n denyReason:\n 'Command references credential/secret paths (~/.ssh, ~/.aws, ~/.gnupg, auth store)',\n },\n {\n name: 'ask-dotenv-paths',\n effect: 'ask',\n match: { paths: CREDENTIAL_ASK_PATH_PATTERNS },\n askPrompt: 'This reads a .env file which may contain secrets. Continue?',\n },\n {\n name: 'ask-dotenv-command',\n effect: 'ask',\n match: {\n condition: (ctx) =>\n commandMatchesAny(String(ctx.args.command ?? ''), CREDENTIAL_ASK_COMMAND_PATTERNS),\n },\n askPrompt: 'This command references a .env file which may contain secrets. Continue?',\n },\n {\n name: 'deny-mcp-credential-args',\n effect: 'deny',\n match: {\n condition: (ctx) =>\n mcpArgsMatchAny(ctx.toolName, ctx.args, CREDENTIAL_DENY_COMMAND_PATTERNS),\n },\n denyReason:\n 'MCP tool argument references credential/secret paths (~/.ssh, ~/.aws, ~/.gnupg, auth store)',\n },\n {\n name: 'ask-mcp-dotenv-args',\n effect: 'ask',\n match: {\n condition: (ctx) =>\n mcpArgsMatchAny(ctx.toolName, ctx.args, CREDENTIAL_ASK_COMMAND_PATTERNS),\n },\n askPrompt: 'This MCP tool reads a .env file which may contain secrets. Continue?',\n },\n ],\n}\n\n// ---- RFC-228: `.otto` 状态域写保护 ----\n// A 类:信任/权限自提升——插件信任表、路由配置、调度、插件实现目录。改写可让模型自己批准\n// 插件信任、篡改路由、植入持久化自主执行代码。\nconst OTTO_STATE_DENY_PATH_PATTERNS_A: RegExp[] = [\n /\\.otto\\/plugin-trust\\.json$/,\n /\\.otto\\/claude-trust\\.json$/,\n /\\.otto\\/config\\.json$/,\n /\\.otto\\/schedules\\//,\n /\\.otto\\/plugins\\//,\n /\\.otto\\/timers\\//,\n]\n\n// C 类:运行时完整性——会话存储、写租约锁。模型直接改写会破坏 RFC-159\n// 的写租约/单调序等不变式。\nconst OTTO_STATE_DENY_PATH_PATTERNS_C: RegExp[] = [\n /\\.otto\\/sessions\\//,\n /\\.otto\\/.*\\.lock$/,\n\n]\n\n// B 类:跨会话记忆域——deny 经通用工具直写,引导走 memory_record(RFC-228 D2/D3)。\nconst OTTO_STATE_DENY_PATH_PATTERNS_B: RegExp[] = [\n /\\.otto\\/AGENTS\\.md$/,\n /\\.otto\\/MEMORY\\.md$/,\n /\\.otto\\/memory\\//,\n /\\.otto\\/local\\/MEMORY\\.md$/,\n /\\.otto\\/local\\/memory\\//,\n /\\.otto\\/lessons\\.json$/,\n /\\.otto\\/local\\/lessons\\.json$/,\n]\n\n/**\n * command-level deny(RFC-228 D1 评审修正,P0):write/edit 的 `pathParams` 解析对 bash\n * 天然不生效(bash 未声明 pathParams,`input.paths` 恒空)。本组正则对 bash 命令文本做\n * `commandMatchesAny` 字符串/正则匹配,覆盖 `sqlite3 .otto/sessions/x.db`、\n * `node -e \"fs.writeFileSync('.otto/plugins/x','')\"` 等语法级写目标抽取抓不到的绕过路径。\n * 含 A(信任/执行)+ C(运行时)+ B(记忆)三类路径——`echo x >> .otto/AGENTS.md` 这类最直接\n * 的\"绕过 memory_record 用 bash 写记忆\"场景同样必须被 command 级规则拦住,不能只指望\n * path-level 规则(那只对 write/edit 生效)。\n * **已知局限**(不追求语义级证明,与 checkShellCommand 同级):二次编码(`base64 -d | bash`)、\n * 变量拼接路径等混淆仍可能绕过——本策略目标是大幅提高绕过门槛、覆盖常见/无意路径,不是\n * 消除一切理论绕过。真正的强隔离属沙箱层职责(非本 RFC 范围)。\n */\nconst OTTO_STATE_DENY_COMMAND_PATTERNS: RegExp[] = [\n /\\.otto\\/(plugin-trust|claude-trust|config)\\.json/,\n /\\.otto\\/(schedules|plugins|timers|sessions)\\//,\n /\\.otto\\/.*\\.lock\\b/,\n\n /\\.otto\\/AGENTS\\.md/,\n // B 类含 local/ 变体(`.otto/local/MEMORY.md`、`.otto/local/memory/`、`.otto/local/lessons.json`)\n // ——里程碑完成审核发现的缺口:`(local\\/)?` 可选段同时覆盖 workspace 与 local 两态,不需要\n // 拆成两条重复正则。\n /\\.otto\\/(local\\/)?MEMORY\\.md/,\n /\\.otto\\/(local\\/)?memory\\//,\n /\\.otto\\/(local\\/)?lessons\\.json/,\n]\n\nexport function createOttoStateDomainPolicy(toolSets?: PermissionToolSetsInput): PermissionPolicy {\n const { fileWriteTools } = resolvePermissionToolSets(toolSets)\n const tools = [...fileWriteTools]\n\n return {\n name: 'builtin::otto-state-domain-guard',\n priority: 17,\n enabled: true,\n safety: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'deny-otto-trust-and-execution-state',\n effect: 'deny',\n match: { tools, paths: OTTO_STATE_DENY_PATH_PATTERNS_A },\n denyReason:\n 'Writing to otto trust/config/schedule/plugin state is not allowed via file tools — use the dedicated command (/extension, schedule_create, settings) instead',\n },\n {\n name: 'deny-otto-runtime-state',\n effect: 'deny',\n match: { tools, paths: OTTO_STATE_DENY_PATH_PATTERNS_C },\n denyReason: 'otto runtime state (sessions/locks) must not be modified directly',\n },\n {\n name: 'deny-otto-memory-state',\n effect: 'deny',\n match: { tools, paths: OTTO_STATE_DENY_PATH_PATTERNS_B },\n denyReason:\n 'Cross-session memory must be written via the memory_record tool (not edit/write) — use scope: \"workspace\" | \"local\" | \"project-shared\"',\n },\n {\n name: 'deny-otto-state-command-reference',\n effect: 'deny',\n match: {\n condition: (ctx) =>\n commandMatchesAny(String(ctx.args.command ?? ''), OTTO_STATE_DENY_COMMAND_PATTERNS),\n },\n denyReason:\n 'Command references otto trust/config/schedule/plugin/session state — not allowed via bash',\n },\n ],\n }\n}\n\nfunction extractUrlHost(url: string): string | null {\n try {\n return new URL(url).hostname.toLowerCase()\n } catch {\n return null\n }\n}\n\nfunction hostInAllowlist(host: string, allow: readonly string[]): boolean {\n return allow.some((entry) => {\n const e = entry\n .trim()\n .toLowerCase()\n .replace(/^\\*?\\./, '')\n return e.length > 0 && (host === e || host.endsWith(`.${e}`))\n })\n}\n\nexport function createEgressAllowlistPolicy(allowedHosts: readonly string[]): PermissionPolicy {\n const allow = allowedHosts.map((h) => h.trim().toLowerCase()).filter(Boolean)\n return {\n name: 'setting::egress-allowlist',\n priority: 8,\n enabled: true,\n safety: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'ask-egress-offlist',\n effect: 'ask',\n match: {\n condition: (ctx) => {\n const url = ctx.args.url\n if (typeof url !== 'string') return false\n const host = extractUrlHost(url)\n if (!host) return false\n return !hostInAllowlist(host, allow)\n },\n },\n askPrompt: 'This requests a host not in the egress allowlist. Continue?',\n },\n ],\n }\n}\n\nexport function createDisabledToolsPolicy(disabledTools: string[]): PermissionPolicy {\n return {\n name: 'setting::disabled-tools',\n priority: 2,\n enabled: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'deny-disabled',\n effect: 'deny',\n match: { tools: disabledTools },\n denyReason: 'Tool disabled by configuration',\n },\n ],\n }\n}\n\nexport function createPermissionModePolicy(\n mode: PermissionMode,\n toolSets?: PermissionToolSetsInput,\n): PermissionPolicy {\n const { readonlyTools, writeTools } = resolvePermissionToolSets(toolSets)\n\n switch (mode) {\n case 'bypass':\n return {\n name: 'mode::bypass',\n priority: 1,\n enabled: true,\n scope: { agents: ['*'] },\n rules: [{ name: 'allow-all', effect: 'allow', match: {} }],\n }\n\n case 'readonly':\n return {\n name: 'mode::readonly',\n priority: 1,\n enabled: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'allow-reads',\n effect: 'allow',\n match: { condition: (ctx) => readonlyTools.has(ctx.toolName) },\n },\n {\n name: 'deny-writes',\n effect: 'deny',\n match: {},\n fallthrough: true,\n denyReason: 'Read-only mode: write operations are not allowed',\n },\n ],\n }\n\n case 'confirm':\n return {\n name: 'mode::confirm',\n priority: 1,\n enabled: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'allow-reads',\n effect: 'allow',\n match: { condition: (ctx) => readonlyTools.has(ctx.toolName) },\n },\n {\n name: 'ask-writes',\n effect: 'ask',\n match: { tools: [...writeTools] },\n askPrompt: 'Confirm write operation?',\n },\n ],\n }\n\n case 'strict':\n return {\n name: 'mode::strict',\n priority: 9999,\n enabled: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'deny-unmatched',\n effect: 'deny',\n match: {},\n fallthrough: true,\n denyReason: 'Strict mode: no matching permission policy',\n },\n ],\n }\n\n case 'auto':\n default:\n return {\n name: 'mode::auto',\n priority: 1,\n enabled: true,\n scope: { agents: ['*'] },\n rules: [\n {\n name: 'allow-reads',\n effect: 'allow',\n match: { condition: (ctx) => readonlyTools.has(ctx.toolName) },\n },\n {\n name: 'auto-writes',\n effect: 'allow',\n match: { tools: [...writeTools] },\n },\n ],\n }\n }\n}\n","import {\n createFileGuardPolicy,\n createPermissionModePolicy,\n createOttoStateDomainPolicy,\n DESTRUCTIVE_COMMAND_POLICY,\n CREDENTIAL_READ_POLICY,\n} from './builtin-policies'\nimport type { PermissionPolicyRegistry } from './policy-registry'\nimport type { PermissionToolSetsInput } from './builtin-policies'\n\nexport interface RegisterBuiltinPoliciesOptions {\n toolSets?: PermissionToolSetsInput\n}\n\nexport function registerBuiltinPolicies(\n registry: PermissionPolicyRegistry,\n options: RegisterBuiltinPoliciesOptions = {},\n): void {\n registry.register(createPermissionModePolicy('auto', options.toolSets))\n registry.register(createFileGuardPolicy(options.toolSets))\n registry.register(DESTRUCTIVE_COMMAND_POLICY)\n registry.register(CREDENTIAL_READ_POLICY)\n // RFC-228:`.otto` 状态域写保护(信任/执行/记忆/运行时完整性四类 path+command 双轨 deny)。\n registry.register(createOttoStateDomainPolicy(options.toolSets))\n}\n","import { extractShellCommands } from './command-canonicalize'\nimport type { PermissionContext } from './types'\n\n/**\n * always-allow 条目:`tool` 或 `tool(content)`。content 仅对 bash 解释为命令前缀。\n */\nexport interface AllowEntry {\n toolName: string\n content?: string\n}\n\n/** 解析一条 always-allow 字符串。`bash` → 整工具(content=undefined);`bash(git status)` → 前缀条目。 */\nexport function parseAllowEntry(raw: string): AllowEntry {\n const s = raw.trim()\n const m = /^([^()\\s]+)\\((.*)\\)$/.exec(s)\n if (m) return { toolName: m[1] ?? s, content: (m[2] ?? '').trim() }\n return { toolName: s }\n}\n\n/**\n * 条目是否格式良好——供 `/permissions add` 校验,拒绝静默持久化危险/无意义条目。\n * 合法:`tool`(整工具)或 `tool(非空 content)`。\n * 非法:空串、空括号 `tool()`(读法似\"无参\"实为整工具,塌缩陷阱,RFC-073 P3)、\n * 括号不平衡/含元字符的垃圾(`bash(((`、`a b`)。\n */\nexport function isWellFormedAllowEntry(raw: string): boolean {\n const s = raw.trim()\n if (!s) return false\n const m = /^([^()\\s]+)\\((.*)\\)$/.exec(s)\n if (m) {\n const name = (m[1] ?? '').trim()\n const content = (m[2] ?? '').trim()\n return name.length > 0 && content.length > 0 // 空括号非法\n }\n return /^[^()\\s]+$/.test(s) // 无括号整工具:单 token、无括号/空格\n}\n\n/** 片段是否以某前缀开头(词边界:前缀须整词,避免 `git` 匹到 `github`)。 */\nfunction fragmentHasPrefix(fragment: string, prefix: string): boolean {\n const f = fragment.trim()\n const p = prefix.trim()\n if (!p) return false\n return f === p || f.startsWith(p + ' ')\n}\n\n/**\n * bash 命令是否被前缀集合完全覆盖。\n *\n * 复用 extractShellCommands 的反混淆面(递归展开 bash -c/$()/反引号/here-string + 解码 +\n * 拆复合命令),要求**每个**展开片段都被某前缀覆盖。这天然防复合命令绕过:危险叶子命令\n * 总在展开集合里,未被覆盖即整体不放行(复合父串自匹前缀也无害——危险叶子仍会落空)。\n */\n/** 纯导航片段:`cd <单参数>`(无 shell 元字符——extractShellCommands 已拆出所有运算符/替换叶子)。 */\nconst NAV_CD_ONLY = /^cd\\s+\\S+$/\n\nexport function bashCommandCovered(command: string, prefixes: readonly string[]): boolean {\n const usable = prefixes.filter(Boolean)\n if (usable.length === 0) return false\n const fragments = extractShellCommands(command)\n if (fragments.length === 0) return false\n return fragments.every((frag) => {\n // RFC-073 P4:纯 cd 导航片段豁免(只换目录,不执行危险动作)。otto 自身大量命令是\n // `cd <ws> && <cmd>`,派生侧 bashAlwaysAllowPrefix 已剥 cd,匹配侧须对称——否则招牌用例失效。\n // 安全:`cd $(rm)`/`cd /x;rm` 的危险叶子已被 extractShellCommands 拆出、不匹 NAV_CD → 仍需覆盖。\n if (NAV_CD_ONLY.test(frag)) return true\n const core = frag.replace(/^cd\\s+\\S+\\s*&&\\s*/, '').trim()\n return usable.some((p) => fragmentHasPrefix(core, p))\n })\n}\n\n/**\n * 某工具调用是否被 always-allow 条目覆盖(合并项目级+会话级条目)。\n * - 工具名条目(无 content)→ 整工具放行(保留旧语义)。\n * - bash(content) → 命令所有片段须被前缀覆盖。\n * - 其它工具的 content 条目暂不解释(保守:不匹配 → 仍 ask)。\n *\n * 注意:本函数只决定「是否升级非安全 ask→allow」,安全护栏(safety=true)的 ask/deny\n * 由调用方单独保留,绝不被此覆盖。\n */\nexport function toolCallCoveredByAllow(\n context: PermissionContext,\n entries: Iterable<string>,\n): boolean {\n const parsed: AllowEntry[] = []\n for (const e of entries) {\n const p = parseAllowEntry(e)\n if (p.toolName === context.toolName) parsed.push(p)\n }\n if (parsed.length === 0) return false\n // 整工具放行只认「无括号」条目(content===undefined);空括号 `tool()`(content==='')\n // 不再塌缩为整工具(RFC-073 P3 陷阱)——它对 bash 经 bashCommandCovered 过滤空前缀后天然失活。\n if (parsed.some((e) => e.content === undefined)) return true\n\n if (context.toolName === 'bash') {\n const cmd = typeof context.args?.command === 'string' ? (context.args.command as string) : ''\n if (!cmd) return false\n return bashCommandCovered(\n cmd,\n parsed.map((e) => e.content as string),\n )\n }\n return false\n}\n\n/**\n * 为「始终允许」按钮派生一个 bash 命令前缀(程序名 + 子命令两段),供 UI 显式呈现 +\n * 持久化为 `bash(<prefix>)`。复合/混淆/env 赋值命令返回 null → 调用方回退整工具放行\n * (此时安全护栏仍 bypass-immune,不会因整工具放行而漏过危险叶子)。\n *\n * 例:`git status -s`→`git status`、`npm run build`→`npm run`、`ls -la`→`ls`、\n * `cd /x && git status`→`git status`、`a && b`/`$()`/`FOO=1 cmd`→null。\n */\nexport function bashAlwaysAllowPrefix(command: string): string | null {\n const stripped = command\n .trim()\n .replace(/^cd\\s+\\S+\\s*&&\\s*/, '')\n .trim()\n // 复合/命令替换/here-doc/eval → 前缀不可靠,回退整工具\n if (/[;&|]|`|\\$\\(|<<|\\beval\\b/.test(stripped)) return null\n const tokens = stripped.split(/\\s+/).filter(Boolean)\n const prog = tokens[0]\n if (!prog || prog.includes('=')) return null // env 赋值前缀(FOO=bar cmd)跳过\n const sub = tokens[1] && !tokens[1].startsWith('-') ? tokens[1] : null\n return sub ? `${prog} ${sub}` : prog\n}\n","import { createLogger } from '@x-otto/shared'\nimport { registerBuiltinPolicies } from './register-builtin'\nimport { toolCallCoveredByAllow } from './always-allow'\nimport type { RegisterBuiltinPoliciesOptions } from './register-builtin'\nimport type { PermissionPolicySetting } from './types'\nimport type {\n PermissionPolicy,\n PermissionContext,\n PermissionDecision,\n PolicyScope,\n RuleMatch,\n PermissionRule,\n} from './types'\n\nconst logger = createLogger('@x-otto/hooks:permission')\n\nfunction matchesScopeAgent(scope: PolicyScope, agentName: string): boolean {\n if (!scope.agents || scope.agents.length === 0) {\n return true\n }\n return scope.agents.includes('*') || scope.agents.includes(agentName)\n}\n\nfunction matchesScopeSession(scope: PolicyScope, sessionId: string): boolean {\n if (!scope.sessions || scope.sessions.length === 0) {\n return true\n }\n return scope.sessions.includes('*') || scope.sessions.includes(sessionId)\n}\n\nfunction matchesScope(scope: PolicyScope, ctx: PermissionContext): boolean {\n return matchesScopeAgent(scope, ctx.agentName) && matchesScopeSession(scope, ctx.sessionId)\n}\n\nfunction matchesRule(match: RuleMatch, ctx: PermissionContext): boolean {\n if (match.tools && match.tools.length > 0) {\n if (!match.tools.includes(ctx.toolName)) {\n return false\n }\n }\n if (match.paths && match.paths.length > 0) {\n if (!ctx.filePath) {\n return false\n }\n const pathMatched = match.paths.some((pattern) => pattern.test(ctx.filePath as string))\n if (!pathMatched) {\n return false\n }\n }\n if (match.condition) {\n if (!match.condition(ctx)) {\n return false\n }\n }\n return true\n}\n\nconst DEFAULT_ALLOW: PermissionDecision = {\n effect: 'allow',\n policyName: '__default__',\n ruleName: '__fallthrough__',\n timestamp: 0,\n evaluatedPolicies: 0,\n}\n\nexport class PermissionPolicyRegistry {\n private policies: PermissionPolicy[] = []\n\n /**\n * 项目级「始终允许」的工具名(R8-B base)。语义:把该工具被评估出的 `ask` 升级为 `allow`,\n * 但绝不把 `deny` 改为 allow——安全 deny(受保护路径/破坏性命令)始终优先。\n * 来源 .otto/config.json,对工作区所有会话生效(手动配置的常驻放行基线)。\n */\n private alwaysAllow = new Set<string>()\n\n /**\n * 会话级「始终允许」(弹窗里点「始终允许」的运行时授权):按 sessionId 隔离,\n * 跟随会话持久化(落盘进 session 记录,--continue 时回灌),互不串味——\n * 一次性的信任决策不再泄漏到项目其它会话/永久污染 config(修旧 R8-B 全局泄漏)。\n */\n private sessionAlwaysAllow = new Map<string, Set<string>>()\n\n setAlwaysAllow(tools: readonly string[]): void {\n this.alwaysAllow = new Set(tools)\n }\n\n addAlwaysAllow(tool: string): void {\n this.alwaysAllow.add(tool)\n }\n\n getAlwaysAllow(): string[] {\n return [...this.alwaysAllow]\n }\n\n /** 整体替换某会话的 always-allow 集合(会话恢复/回灌用)。空集合=移除该会话条目。 */\n setSessionAlwaysAllow(sessionId: string, tools: readonly string[]): void {\n if (!sessionId) return\n if (tools.length === 0) {\n this.sessionAlwaysAllow.delete(sessionId)\n return\n }\n this.sessionAlwaysAllow.set(sessionId, new Set(tools))\n }\n\n addSessionAlwaysAllow(sessionId: string, tool: string): void {\n if (!sessionId || !tool) return\n const set = this.sessionAlwaysAllow.get(sessionId) ?? new Set<string>()\n set.add(tool)\n this.sessionAlwaysAllow.set(sessionId, set)\n }\n\n /** 不含项目级基线。 */\n getSessionAlwaysAllow(sessionId: string): string[] {\n return [...(this.sessionAlwaysAllow.get(sessionId) ?? [])]\n }\n\n clearSessionAlwaysAllow(sessionId: string): void {\n this.sessionAlwaysAllow.delete(sessionId)\n }\n\n register(policy: PermissionPolicy): void {\n this.unregister(policy.name)\n this.policies.push(policy)\n this.policies.sort((a, b) => a.priority - b.priority)\n logger.debug(\n { name: policy.name, priority: policy.priority, rules: policy.rules.length },\n 'Permission policy registered',\n )\n }\n\n registerAll(policies: PermissionPolicy[]): void {\n for (const p of policies) {\n this.register(p)\n }\n }\n\n unregister(name: string): boolean {\n const idx = this.policies.findIndex((p) => p.name === name)\n if (idx >= 0) {\n this.policies.splice(idx, 1)\n logger.debug({ name }, 'Permission policy unregistered')\n return true\n }\n return false\n }\n\n has(name: string): boolean {\n return this.policies.some((p) => p.name === name)\n }\n\n /**\n * collect-all / deny-wins(C1/RFC-016,收集全部命中规则后按 effect 优先级裁决):\n * 收集所有命中规则,决策优先级 deny > ask > allow,**与命中/优先级顺序无关**——\n * priority 仅用于在同 effect 内择 reason。这样 `bypass` 的 allow-all 不再能盖过\n * 安全护栏的 deny/ask(first-match 旧语义的倒置缺口)。\n *\n * alwaysAllow(R8-B):仅升级**非安全护栏**的 ask→allow;安全护栏(file-guard/\n * destructive-guard,policy.safety=true)的 ask/deny 绝不被升级或盖过。\n */\n evaluate(context: PermissionContext): PermissionDecision {\n let evaluated = 0\n type Match = {\n effect: PermissionRule['effect']\n policyName: string\n ruleName: string\n priority: number\n reason?: string\n safety: boolean\n fallthrough: boolean\n }\n const matches: Match[] = []\n\n for (const policy of this.policies) {\n if (!policy.enabled) {\n continue\n }\n if (!matchesScope(policy.scope, context)) {\n continue\n }\n evaluated++\n for (const rule of policy.rules) {\n if (matchesRule(rule.match, context)) {\n matches.push({\n effect: rule.effect,\n policyName: policy.name,\n ruleName: rule.name,\n priority: policy.priority,\n reason: rule.effect === 'deny' ? rule.denyReason : rule.askPrompt,\n safety: policy.safety === true,\n fallthrough: rule.fallthrough === true,\n })\n }\n }\n }\n\n const hasOverrideMode = matches.some(\n (m) => m.policyName === 'mode::auto' || m.policyName === 'mode::bypass',\n )\n\n const pick = (effect: PermissionRule['effect'], pool: Match[]): PermissionDecision => {\n const top = pool.reduce((a, b) => (b.priority < a.priority ? b : a))\n return {\n effect,\n policyName: top.policyName,\n ruleName: top.ruleName,\n reason: top.reason,\n timestamp: Date.now(),\n evaluatedPolicies: evaluated,\n safety: top.safety,\n }\n }\n\n const decide = (pool: Match[]): PermissionDecision | null => {\n if (pool.length === 0) return null\n const denies = pool.filter((m) => m.effect === 'deny')\n if (denies.length > 0) return pick('deny', denies)\n const asks = pool.filter((m) => m.effect === 'ask')\n if (asks.length > 0) {\n if (hasOverrideMode) {\n return {\n effect: 'allow',\n policyName: 'mode::auto-override',\n ruleName: asks[0]!.ruleName,\n timestamp: Date.now(),\n evaluatedPolicies: evaluated,\n }\n }\n const toolAllowed = toolCallCoveredByAllow(context, [\n ...this.alwaysAllow,\n ...(this.sessionAlwaysAllow.get(context.sessionId) ?? []),\n ])\n const blocking = asks.filter((m) => m.safety || !toolAllowed)\n if (blocking.length === 0) {\n return {\n effect: 'allow',\n policyName: 'user::always-allow',\n ruleName: context.toolName,\n timestamp: Date.now(),\n evaluatedPolicies: evaluated,\n }\n }\n return pick('ask', blocking)\n }\n const allows = pool.filter((m) => m.effect === 'allow')\n if (allows.length > 0) return pick('allow', allows)\n return null\n }\n\n const normal = matches.filter((m) => !m.fallthrough)\n const fallback = matches.filter((m) => m.fallthrough)\n const n = decide(normal)\n const f = decide(fallback)\n if (n?.effect === 'deny' || n?.effect === 'allow') return n\n if (f?.effect === 'deny') return f\n const decision = n ?? f\n if (decision) return decision\n\n return {\n ...DEFAULT_ALLOW,\n timestamp: Date.now(),\n evaluatedPolicies: evaluated,\n }\n }\n\n getEffective(agentName: string): PermissionPolicy[] {\n return this.policies.filter((p) => p.enabled && matchesScopeAgent(p.scope, agentName))\n }\n\n getAll(): PermissionPolicy[] {\n return [...this.policies]\n }\n\n clear(): void {\n this.policies = []\n logger.debug('All permission policies cleared')\n }\n}\n\nexport function createPermissionRegistry(\n options: RegisterBuiltinPoliciesOptions = {},\n): PermissionPolicyRegistry {\n const registry = new PermissionPolicyRegistry()\n registerBuiltinPolicies(registry, options)\n return registry\n}\n\nconst MAX_PATTERN_LENGTH = 1024\n\nconst NESTED_QUANTIFIER = /\\([^)]*(?:[*+]|\\{\\d+,\\d*\\})[^)]*\\)(?:[*+]|\\{\\d+,\\d*\\})/\n\nfunction compileGuardedRegex(pattern: string, ruleName: string): RegExp | null {\n if (pattern.length > MAX_PATTERN_LENGTH) {\n logger.warn(\n { rule: ruleName, length: pattern.length, max: MAX_PATTERN_LENGTH },\n 'Permission path pattern exceeds max length — rule skipped (ReDoS guard)',\n )\n return null\n }\n if (NESTED_QUANTIFIER.test(pattern)) {\n logger.warn(\n { rule: ruleName, pattern },\n 'Permission path pattern has nested quantifiers (catastrophic backtracking risk) — rule skipped (ReDoS guard)',\n )\n return null\n }\n try {\n return new RegExp(pattern)\n } catch (err) {\n logger.warn(\n { rule: ruleName, pattern, error: err instanceof Error ? err.message : String(err) },\n 'Permission path pattern failed to compile — rule skipped',\n )\n return null\n }\n}\n\nexport function compilePolicyFromSetting(setting: PermissionPolicySetting): PermissionPolicy {\n const rules: PermissionRule[] = []\n for (const rc of setting.rules) {\n let paths: RegExp[] | undefined\n if (rc.paths && rc.paths.length > 0) {\n const compiled: RegExp[] = []\n let skipRule = false\n for (const p of rc.paths) {\n const re = compileGuardedRegex(p, rc.name)\n if (re === null) {\n skipRule = true\n break\n }\n compiled.push(re)\n }\n if (skipRule) {\n continue\n }\n paths = compiled\n }\n rules.push({\n name: rc.name,\n effect: rc.effect,\n match: {\n tools: rc.tools,\n paths,\n },\n denyReason: rc.deny_reason,\n askPrompt: rc.ask_prompt,\n })\n }\n\n return {\n name: setting.name,\n priority: setting.priority ?? 50,\n enabled: setting.enabled ?? true,\n scope: {\n agents: setting.scope?.agents,\n sessions: setting.scope?.sessions,\n },\n rules,\n }\n}\n","import type {\n PermissionContext,\n PermissionDecision,\n PermissionAuditEntry,\n RuleEffect,\n} from './types'\n\nexport class PermissionAuditLog {\n private entries: PermissionAuditEntry[] = []\n private readonly maxEntries: number\n\n constructor(maxEntries = 1000) {\n this.maxEntries = maxEntries\n }\n\n record(context: PermissionContext, decision: PermissionDecision): void {\n this.entries.push({\n timestamp: decision.timestamp,\n sessionId: context.sessionId,\n agentName: context.agentName,\n toolName: context.toolName,\n filePath: context.filePath,\n decision,\n })\n\n if (this.entries.length > this.maxEntries) {\n this.entries.splice(0, this.entries.length - this.maxEntries)\n }\n }\n\n getEntries(filter?: { sessionId?: string; effect?: RuleEffect }): PermissionAuditEntry[] {\n let result = this.entries\n if (filter?.sessionId) {\n result = result.filter((e) => e.sessionId === filter.sessionId)\n }\n if (filter?.effect) {\n result = result.filter((e) => e.decision.effect === filter.effect)\n }\n // 无 filter 时 result 仍是内部数组活引用——返回防御性副本,杜绝调用方篡改审计日志。\n return result === this.entries ? this.entries.slice() : result\n }\n\n getDenyCount(sessionId?: string): number {\n return this.getEntries({ sessionId, effect: 'deny' }).length\n }\n\n getAskCount(sessionId?: string): number {\n return this.getEntries({ sessionId, effect: 'ask' }).length\n }\n\n clear(sessionId?: string): void {\n if (sessionId) {\n this.entries = this.entries.filter((e) => e.sessionId !== sessionId)\n } else {\n this.entries = []\n }\n }\n\n get size(): number {\n return this.entries.length\n }\n}\n","import type { ApprovalRisk } from '@x-otto/hook-contracts'\nimport type { PermissionDecision } from './types'\n\n/**\n * 从权限裁决的结构化安全位(decision.safety,透传自命中策略 PermissionPolicy.safety)派生 HITL\n * 显示风险等级。RFC-077 M3:取代原先靠正则匹配 policyName 子串\n * (destructive|credential|file-guard) 反推风险的字符串考古——那会把名字不含三魔词的自定义安全\n * 策略误判为 medium,且 builtin 策略一改名就静默退化。\n *\n * 仅作 UI 显示提示——不参与 allow/deny/ask 裁决。安全护栏(safety:true)→ high,其余 ask\n * (confirm 写 / egress 越界 / 自定义非安全策略)→ medium。\n *\n * 注意:always-allow 折叠后的非安全 ask 变成 effect==='allow',不会走到这里;到达 HITL 的 ask\n * 要么是安全护栏(safety:true→high),要么是未折叠的 mode/自定义 ask(safety 缺省→medium)。\n */\nexport function riskFromDecision(decision: PermissionDecision): ApprovalRisk {\n return decision.safety ? 'high' : 'medium'\n}\n","import { resolve } from 'node:path'\n\nimport { defineHook } from '../../hook-spec'\nimport type { HookSpec } from '../../hook-spec'\nimport type { ToolExecuteBeforeInput, ToolExecuteBeforeOutput } from '../../types'\nimport type { PermissionPolicyRegistry } from './policy-registry'\nimport type { PermissionAuditLog } from './audit-log'\nimport type { PermissionContext } from './types'\nimport { riskFromDecision } from './risk'\n\n/**\n * RFC-077 M3-B:路径评估时**额外**纳入 `resolve(p)` 变体——防 `../` traversal 逃逸\n * (`../../../etc/passwd` 解析后落 `/etc/passwd`)绕过锚定的 protected/credential 路径模式。\n * defense-in-depth:生产 file-write 工具(write/edit)的 `resolvePath` 已把路径解析为绝对,但本\n * 守卫不依赖每个工具都配 resolvePath——与被取代的 standalone file-guard hook 的内部 `resolve()`\n * 同语义,确保把策略引擎设为单源后**覆盖不缩水**(含原始 + 解析两形,deny-wins)。\n */\nfunction withResolvedVariants(paths: readonly (string | undefined)[]): (string | undefined)[] {\n const out: (string | undefined)[] = []\n for (const p of paths) {\n out.push(p)\n if (typeof p === 'string') {\n const resolved = resolve(p)\n if (resolved !== p) out.push(resolved)\n }\n }\n return out\n}\n\nexport class PermissionGuardHook {\n readonly name = 'permission-guard'\n readonly timing = 'tool.execute.before' as const\n readonly priority = 5\n public enabled = true\n\n private readonly registry: PermissionPolicyRegistry\n private readonly auditLog?: PermissionAuditLog\n\n constructor(registry: PermissionPolicyRegistry, auditLog?: PermissionAuditLog) {\n this.registry = registry\n this.auditLog = auditLog\n }\n\n handle = (input: ToolExecuteBeforeInput, output: ToolExecuteBeforeOutput): void => {\n const base = {\n timing: 'tool.execute.before' as const,\n toolName: input.toolName,\n args: input.args,\n agentName: input.agentName,\n sessionId: input.sessionId,\n metadata: {},\n }\n\n const paths = withResolvedVariants(\n input.paths && input.paths.length > 0 ? input.paths : [undefined],\n )\n let decision = this.registry.evaluate({ ...base, filePath: paths[0] })\n let decisionPath = paths[0]\n for (let i = 1; i < paths.length; i++) {\n const d = this.registry.evaluate({ ...base, filePath: paths[i] })\n\n if (d.effect === 'deny' || (d.effect === 'ask' && decision.effect === 'allow')) {\n decision = d\n decisionPath = paths[i]\n }\n }\n // 审计用产生最终 decision 的那条路径(而非固定 paths[0]),避免多路径时来源失真。\n const context: PermissionContext = { ...base, filePath: decisionPath }\n this.auditLog?.record(context, decision)\n\n if (decision.effect === 'deny') {\n output.cancelled = true\n output.decision = 'deny'\n output.decisionReason = decision.reason ?? decision.ruleName\n output.cancelReason = `Permission denied: ${output.decisionReason}`\n return\n }\n\n if (decision.effect === 'ask') {\n output.decision = 'ask'\n output.decisionReason = decision.reason ?? 'Requires confirmation'\n output.risk = riskFromDecision(decision)\n } else if (output.decision !== 'ask' && output.decision !== 'deny') {\n output.decision = 'allow'\n }\n }\n}\n\nexport function createPermissionGuardHook(\n registry: PermissionPolicyRegistry,\n auditLog?: PermissionAuditLog,\n): HookSpec {\n const hook = new PermissionGuardHook(registry, auditLog)\n return defineHook({\n name: hook.name,\n timing: hook.timing,\n priority: hook.priority,\n enabled: hook.enabled,\n handle: hook.handle,\n })\n}\n","import type { HookRegistry } from '../../hook-registry'\nimport type { PermissionToolSetsInput } from './builtin-policies'\nimport { createPermissionGuardHook } from './permission-guard'\nimport { createPermissionRegistry, type PermissionPolicyRegistry } from './policy-registry'\nimport { PermissionAuditLog } from './audit-log'\n\n/**\n * RFC-077 M3-B:bare-runtime(无工具注册表)的基线 `fileWriteTools`——`createFileGuardPolicy`\n * 的 protect-system-paths 规则按 `tools: [...fileWriteTools]` 过滤,无注册表派生集时须给默认,\n * 否则空集 → 该规则匹配不到任何工具 → protected-path 写不被拦。\n *\n * 与 standalone `createFileGuardHook` 的 `WRITE_TOOLS` 同源(canonical otto 文件写工具);bash 非\n * path-based 故不入 fileWriteTools(其命令面由 destructive/credential 命令策略覆盖)。**宿主有工具\n * 注册表时经 `createPermissionToolSetsFromRegistry` 派生集覆盖本默认**(coding 走该路径)。\n */\nexport const BASELINE_PERMISSION_TOOL_SETS: PermissionToolSetsInput = {\n fileWriteTools: ['write', 'edit', 'edit-diff'],\n}\n\n/**\n * RFC-077 M3-B(D6 §4.11「安全护栏先 provision 再删」):把权限策略引擎 + 基线安全策略\n * (file-guard / credential / destructive,经 `createPermissionRegistry` 注册)下沉为 bare-runtime\n * 的**默认提供**——注册 permission-guard hook(`tool.execute.before`),让无宿主 PermissionController\n * 的裸运行时也 safe-by-default(安全是引擎核心能力,非宿主可选项)。\n *\n * **幂等**:宿主(coding)已注册 permission-guard 时直接返回,绝不双注册——bare 分支与宿主\n * provision 二选一(createAgentRuntime 仅在自建 hookRegistry、未被宿主注入时调用本函数)。\n *\n * @returns 新建的基线 registry(宿主已 provision 时返回 undefined)。\n */\nexport function provisionBaselineSecurity(\n hookRegistry: HookRegistry,\n): PermissionPolicyRegistry | undefined {\n if (hookRegistry.has('permission-guard')) {\n return undefined\n }\n const registry = createPermissionRegistry({ toolSets: BASELINE_PERMISSION_TOOL_SETS })\n hookRegistry.register(createPermissionGuardHook(registry, new PermissionAuditLog()))\n return registry\n}\n","/**\n * sandbox-safety.ts — 沙箱安全评估核心(M-C 从 @x-otto/coding 下沉至 @x-otto/hooks)。\n *\n * 纯函数,仅依赖 PermissionMode(本包自身类型)+ node:path/node:fs(realpath 归一化,零副作用\n * 读操作),实现 patch 安全评估的三态矩阵(safe / require-approval / reject)。\n */\nimport { realpathSync } from 'node:fs'\nimport { resolve } from 'node:path'\n\nimport type { PermissionMode } from './core/permission/types'\n\nexport type SandboxVerdict = 'auto-allow' | 'ask' | 'deny'\n\nexport interface SandboxAssessment {\n verdict: SandboxVerdict\n reason?: string\n}\n\nexport interface SandboxOp {\n writePaths?: readonly string[]\n network?: boolean\n}\n\nexport interface SandboxAssessContext {\n mode: PermissionMode\n projectRoot: string\n writableRoots: readonly string[]\n deniedRoots: readonly string[]\n networkAllowed: boolean\n /**\n * bash 沙箱越界自动放行开关(opt-in,默认 false,见 SandboxConfig.autoAllowBashIfSandboxed)。\n * 本字段只是透传给 sandbox-guard hook 消费——assessSandboxSafety 本身不读取它(保持纯函数\n * 决策矩阵不变),由 sandbox-guard 在拿到 'ask' verdict 后结合此字段 + 权限层已有裁决\n * (output.decision)判断是否降级为自动放行。\n */\n autoAllowBashIfSandboxed?: boolean\n}\n\n/**\n * 尝试解析路径的 realpath(symlink 归一化)。路径不存在(如尚未创建的写目标)或\n * 解析失败时静默回退 undefined——调用方仍用 resolve() 形式兜底比较。\n */\nfunction tryRealpath(p: string): string | undefined {\n try {\n return realpathSync(p)\n } catch {\n return undefined\n }\n}\n\n/**\n * 给一组根路径补充 realpath 变体(macOS `/tmp` → `/private/tmp`、`/var` → `/private/var` 等\n * symlink 归一化落差)。与 macos-seatbelt.ts buildProfile() 的 writableDirs 构造同口径——\n * 沙箱执行层已对可写根做 realpath 双写入,评估层(本文件)此前未做,导致同一物理路径的\n * 不同书写形式(如 os.tmpdir() 的 /var/folders/... 与其 /private/var/folders/... 真实路径)\n * 在评估层和执行层可能给出相反结论。\n */\nfunction withRealpathVariants(paths: readonly string[]): string[] {\n const out = new Set<string>()\n for (const p of paths) {\n const resolved = resolve(p)\n out.add(resolved)\n const real = tryRealpath(resolved)\n if (real && real !== resolved) {\n out.add(real)\n }\n }\n return [...out]\n}\n\nfunction isUnder(p: string, root: string): boolean {\n return p === root || p.startsWith(root.endsWith('/') ? root : `${root}/`)\n}\n\n/**\n * 路径落在根集合内的判定:根集合与待判定路径都各自展开 resolve()/realpath() 两种形式后\n * 做笛卡尔比较,避免书写形式不一致(symlink 别名)导致误判越界。\n */\nfunction underAny(path: string, roots: readonly string[]): boolean {\n const rootVariants = withRealpathVariants(roots)\n const pathVariants = withRealpathVariants([path])\n return pathVariants.some((p) => rootVariants.some((r) => isUnder(p, r)))\n}\n\nexport function assessSandboxSafety(\n op: SandboxOp,\n ctx: SandboxAssessContext,\n): SandboxAssessment {\n const writes = op.writePaths ?? []\n const needsNetwork = op.network === true\n\n for (const w of writes) {\n if (underAny(w, ctx.deniedRoots)) {\n return {\n verdict: 'deny',\n reason: `write to protected path \"${w}\" is never allowed (escape guard)`,\n }\n }\n }\n\n if (ctx.mode === 'bypass') {\n return { verdict: 'auto-allow' }\n }\n\n if (writes.length === 0 && !needsNetwork) {\n return { verdict: 'auto-allow' }\n }\n\n const writesInRoots = writes.every((w) => underAny(w, ctx.writableRoots))\n const networkOk = !needsNetwork || ctx.networkAllowed\n if (writesInRoots && networkOk) {\n return { verdict: 'auto-allow' }\n }\n\n if (ctx.mode === 'strict' || ctx.mode === 'readonly') {\n const why = !writesInRoots\n ? 'write outside the sandbox writable roots'\n : 'network access'\n return { verdict: 'deny', reason: `${why} is not permitted in ${ctx.mode} mode` }\n }\n\n const why = !writesInRoots\n ? 'write outside the sandbox writable roots'\n : 'network access'\n return {\n verdict: 'ask',\n reason: `${why} requires approval (will run with sandbox bypassed for this operation)`,\n }\n}\n\nexport function defaultDeniedRoots(workspaceDir: string, homeDir: string): string[] {\n return [resolve(workspaceDir, '.otto'), resolve(homeDir, '.otto')]\n}\n\n/**\n * 默认可写根:projectRoot + os.tmpdir() + 字面 `/tmp`。\n *\n * 参照 codex(openai/codex codex-rs/protocol/src/permissions.rs\n * legacy_runtime_file_system_policy_for_cwd):SlashTmp 与 Tmpdir 是两个独立的默认可写\n * special path,同时生效(`exclude_slash_tmp`/`exclude_tmpdir_env_var` 默认均 false)。\n * macOS 上 `os.tmpdir()` 落在 `/var/folders/...`(进程级临时目录),与用户/shell 习惯手写的\n * 字面 `/tmp`(→ 真实路径 `/private/tmp`,全系统共享临时目录)是两棵不同的目录树——只信\n * os.tmpdir() 会让 `mkdir -p /tmp/xxx` 这类零风险操作被误判越界触发审批(见 sandbox-guard\n * 越界升级事故复盘)。underAny() 的 realpath 归一化解决的是\"同一路径不同书写形式\",本条解决\n * 的是\"两个本就不同的物理临时目录都该被信任\"。\n */\nexport function computeSandboxRoots(\n mode: PermissionMode,\n projectRoot: string,\n tmpDir: string,\n extra: readonly string[] = [],\n): string[] {\n const roots = [resolve(projectRoot), resolve(tmpDir), '/tmp']\n if (mode !== 'strict' && mode !== 'readonly') {\n for (const e of extra) {\n roots.push(resolve(e))\n }\n }\n return [...new Set(roots)]\n}\n","/** Failure signals: collect/analyze tool/stream errors for pattern detection. */\n\nimport { defineHook } from '..'\nimport type { HookSpec } from '..'\n\nexport type FailureSignalKind = 'tool-error' | 'permission-denied'\n\nexport interface FailureSignal {\n kind: FailureSignalKind\n /** 聚合签名(如 toolName)——同签名复发即累加。 */\n signature: string\n count: number\n firstSeen: number\n lastSeen: number\n /** 最近一次的简短细节(如错误首行,截断)。 */\n detail?: string\n}\n\n/** 跨会话失败信号存储:按 (kind,signature) 聚合计数。诊断据此只认硬信号。 */\nexport class FailureSignalStore {\n private readonly signals = new Map<string, FailureSignal>()\n private readonly clock: () => number\n\n constructor(clock: () => number = () => Date.now()) {\n this.clock = clock\n }\n\n record(kind: FailureSignalKind, signature: string, detail?: string): void {\n const key = `${kind}:${signature}`\n const now = this.clock()\n const cur = this.signals.get(key)\n if (cur) {\n cur.count++\n cur.lastSeen = now\n if (detail) cur.detail = detail\n } else {\n this.signals.set(key, { kind, signature, count: 1, firstSeen: now, lastSeen: now, detail })\n }\n }\n\n /** 复发达阈值(默认 ≥3)的信号,按计数降序——诊断只在这些硬信号上提案。 */\n recurring(minCount = 3): FailureSignal[] {\n return [...this.signals.values()]\n .filter((s) => s.count >= minCount)\n .sort((a, b) => b.count - a.count)\n }\n\n all(): FailureSignal[] {\n return [...this.signals.values()].sort((a, b) => b.count - a.count)\n }\n\n clear(): void {\n this.signals.clear()\n }\n}\n\n/** 取 tool 结果的首行文本(截断)作 detail。 */\nfunction firstLine(result: { content?: unknown }): string | undefined {\n const content = result.content\n if (!Array.isArray(content)) return undefined\n const text = content\n .map((c) =>\n c && typeof c === 'object' && 'text' in c ? String((c as { text?: unknown }).text ?? '') : '',\n )\n .join(' ')\n .trim()\n return text ? text.split('\\n')[0]!.slice(0, 120) : undefined\n}\n\n/** RFC-026 阶段3:tool.execute.after 把工具 isError 记成结构化失败信号。 */\nexport function createFailureSignalHook(store: FailureSignalStore): HookSpec {\n return defineHook({\n name: 'failure-signal-recorder',\n timing: 'tool.execute.after',\n priority: 14,\n handle(input) {\n if (input.result?.isError) {\n store.record('tool-error', input.toolName, firstLine(input.result))\n }\n },\n })\n}\n\n/**\n * RFC-067 M121-04a:把**权限拒绝**记成 `permission-denied` 失败信号(补 FailureSignalKind 的另一半,\n * 此前从无 caller 记录此 kind)。\n *\n * 为何在 tool.execute.before:denied 工具在 tool-executor 早返(errorKind:'permission'),**绕过\n * tool.execute.after**,故 after-hook(createFailureSignalHook)看不到它。权限/沙箱守卫在 before 层\n * 设 `output.decision='deny'`(如 sandbox-guard pri 6),所有 before-hook 跑完才判 deny 裁决——故本\n * recorder 用**晚 priority(90)**确保跑在守卫之后、读到累积的 decision。\n */\nexport function createPermissionDenyHook(store: FailureSignalStore): HookSpec {\n return defineHook({\n name: 'failure-signal-permission-deny',\n timing: 'tool.execute.before',\n priority: 90,\n handle(input, output) {\n if (output?.decision === 'deny') {\n store.record('permission-denied', input.toolName, output.decisionReason)\n }\n },\n })\n}\n","/** Diagnoser: pattern-matching failure signals → user-facing improvement proposals. */\n\nimport type { FailureSignal, FailureSignalStore } from './failure-signals'\n\nexport interface Proposal {\n /** 稳定 id(= kind:signature,便于去重)。 */\n id: string\n signature: string\n kind: 'investigate-fix'\n /** 改进指令(L1:用户审后可 /job 执行;诊断器**不**自动跑)。 */\n directive: string\n /** 来源证据(provenance):哪个信号、复发几次、细节。 */\n evidence: { signalKind: FailureSignal['kind']; count: number; detail?: string }\n createdAt: number\n}\n\nexport interface DiagnoserConfig {\n /** 复发阈值(信号 count ≥ 此才提案;默认 3)。 */\n minCount?: number\n clock?: () => number\n}\n\nexport class Diagnoser {\n private readonly proposed = new Set<string>()\n private readonly minCount: number\n private readonly clock: () => number\n\n constructor(\n private readonly signals: FailureSignalStore,\n config: DiagnoserConfig = {},\n ) {\n this.minCount = config.minCount ?? 3\n this.clock = config.clock ?? (() => Date.now())\n }\n\n /** 扫硬信号(recurring ≥ minCount)→ **新**提案(已提过的签名跳过,防风暴)。 */\n propose(): Proposal[] {\n const out: Proposal[] = []\n for (const s of this.signals.recurring(this.minCount)) {\n const id = `${s.kind}:${s.signature}`\n if (this.proposed.has(id)) continue\n this.proposed.add(id)\n out.push({\n id,\n signature: s.signature,\n kind: 'investigate-fix',\n directive: directiveFor(s),\n evidence: { signalKind: s.kind, count: s.count, detail: s.detail },\n createdAt: this.clock(),\n })\n }\n return out\n }\n\n /** 重置去重(用户处理完想重新评估时)。 */\n reset(): void {\n this.proposed.clear()\n }\n}\n\nfunction directiveFor(s: FailureSignal): string {\n if (s.kind === 'tool-error') {\n return `工具「${s.signature}」反复失败(${s.count} 次${s.detail ? `,如「${s.detail}」` : ''})。调查根因并修复:定位失败路径、补健壮性或修 bug,跑测试验证。`\n }\n return `反复出现「${s.kind}: ${s.signature}」(${s.count} 次)。调查并消除。`\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@x-otto/hooks",
3
+ "version": "0.0.1-alpha.0",
4
+ "files": [
5
+ "dist"
6
+ ],
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org",
19
+ "tag": "alpha"
20
+ },
21
+ "dependencies": {
22
+ "@x-otto/env": "0.1.0-alpha.1",
23
+ "@x-otto/hook-contracts": "0.0.1-alpha.0",
24
+ "@x-otto/shared": "0.1.0-alpha.1"
25
+ },
26
+ "private": false,
27
+ "scripts": {
28
+ "build": "tsdown",
29
+ "typecheck:project": "tsc -p tsconfig.json --noEmit",
30
+ "typecheck:file": "tsc src/index.ts --noEmit --target ES2024 --module ESNext --moduleResolution bundler --esModuleInterop --skipLibCheck",
31
+ "typecheck": "tsc --noEmit",
32
+ "clean": "rm -rf dist"
33
+ }
34
+ }