steerable-agent-harness 0.6.0__py3-none-any.whl

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.
@@ -0,0 +1,37 @@
1
+ from .budget import BudgetLimit, BudgetState, consume_budget
2
+ from .completion import is_terminal_result
3
+ from .policy import PolicyDecision, ToolMode, decide_tool_mode
4
+ from .retry import RetryPolicy, next_retry_delay_ms
5
+ from .safety import (
6
+ BUILTIN_PATTERNS,
7
+ SAFETY_CATEGORIES,
8
+ CommandSafetyConfig,
9
+ SafetyPatternDef,
10
+ ShellCommandClassification,
11
+ classify_shell_command,
12
+ get_patterns_by_category,
13
+ )
14
+ from .tracing import TraceSpan
15
+
16
+ __version__ = "0.2.0"
17
+
18
+ __all__ = [
19
+ "BUILTIN_PATTERNS",
20
+ "SAFETY_CATEGORIES",
21
+ "BudgetLimit",
22
+ "BudgetState",
23
+ "CommandSafetyConfig",
24
+ "PolicyDecision",
25
+ "RetryPolicy",
26
+ "SafetyPatternDef",
27
+ "ShellCommandClassification",
28
+ "ToolMode",
29
+ "TraceSpan",
30
+ "__version__",
31
+ "classify_shell_command",
32
+ "consume_budget",
33
+ "decide_tool_mode",
34
+ "get_patterns_by_category",
35
+ "is_terminal_result",
36
+ "next_retry_delay_ms",
37
+ ]
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class BudgetLimit:
8
+ max_tokens: int
9
+ max_steps: int
10
+ max_tool_calls: int
11
+
12
+
13
+ @dataclass(slots=True)
14
+ class BudgetState:
15
+ tokens_used: int = 0
16
+ steps_used: int = 0
17
+ tool_calls_used: int = 0
18
+
19
+
20
+ def consume_budget(
21
+ state: BudgetState,
22
+ limits: BudgetLimit,
23
+ *,
24
+ tokens: int = 0,
25
+ step: bool = False,
26
+ tool_call: bool = False,
27
+ ) -> tuple[BudgetState, bool]:
28
+ next_state = BudgetState(
29
+ tokens_used=state.tokens_used + max(tokens, 0),
30
+ steps_used=state.steps_used + (1 if step else 0),
31
+ tool_calls_used=state.tool_calls_used + (1 if tool_call else 0),
32
+ )
33
+ exhausted = (
34
+ next_state.tokens_used > limits.max_tokens
35
+ or next_state.steps_used > limits.max_steps
36
+ or next_state.tool_calls_used > limits.max_tool_calls
37
+ )
38
+ return next_state, exhausted
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ def is_terminal_result(result: dict[str, Any] | None) -> bool:
7
+ if not result:
8
+ return False
9
+ if result.get("terminal") is True:
10
+ return True
11
+ if result.get("success") is False and result.get("needsFollowup") is not True:
12
+ return True
13
+ return False
@@ -0,0 +1,250 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from pydantic import BaseModel
5
+
6
+ class ActionSegmentPayload(BaseModel):
7
+ segments: list[Any]
8
+
9
+ class AnalysisDocumentPayload(BaseModel):
10
+ title: Any | None = None
11
+ body: str
12
+ createdAt: Any | None = None
13
+ modelId: Any | None = None
14
+
15
+ class AskUserQuestionsPayload(BaseModel):
16
+ intro: str
17
+ outro: Any | None = None
18
+ answers: Any | None = None
19
+ questions: list[Any]
20
+
21
+ class CoverageReportPayload(BaseModel):
22
+ reportId: str
23
+ title: str
24
+ overallCoverage: float
25
+ overallMastery: float
26
+ summary: Any | None = None
27
+ sections: list[Any]
28
+ weakPoints: list[Any]
29
+ actions: dict[str, Any]
30
+
31
+ class OrchestrationPlanPayload(BaseModel):
32
+ rationale: str | None = None
33
+ mode: str | None = None
34
+ tasks: list[Any]
35
+ coordinator: dict[str, Any] | None = None
36
+
37
+ class PlanSelectorPayload(BaseModel):
38
+ comparison: str
39
+ selectedPlan: Any | None = None
40
+ goalAttribution: dict[str, Any]
41
+ plans: list[Any]
42
+
43
+ class PlanStepsPayload(BaseModel):
44
+ steps: list[Any]
45
+
46
+ class QuizPayload(BaseModel):
47
+ quizId: str
48
+ title: str
49
+ description: Any | None = None
50
+ submitActionLabel: str
51
+ submittedAnswers: Any | None = None
52
+ questions: list[Any]
53
+
54
+ class ResearchPlanPayload(BaseModel):
55
+ topic: str
56
+ round: int
57
+ final: bool
58
+ subQuestions: list[Any]
59
+ decision: dict[str, Any]
60
+
61
+ class SearchSourcesPayload(BaseModel):
62
+ sources: list[Any]
63
+
64
+ class SuggestedRepliesPayload(BaseModel):
65
+ suggestions: list[Any]
66
+
67
+ class SummaryMessagePayload(BaseModel):
68
+ body: str
69
+ summarizedCount: Any | None = None
70
+ status: Any | None = None
71
+ type: Any | None = None
72
+
73
+ class ThinkingProcessPayload(BaseModel):
74
+ body: str
75
+ defaultExpanded: bool | None = None
76
+
77
+ class ToolExecutionPayload(BaseModel):
78
+ id: str
79
+ name: str
80
+ status: str
81
+ summary: Any | None = None
82
+ args: Any | None = None
83
+ output: Any | None = None
84
+ error: Any | None = None
85
+ durationMs: Any | None = None
86
+ icon: Any | None = None
87
+ expandable: bool | None = None
88
+
89
+ class ChatAgent(BaseModel):
90
+ id: str
91
+ slug: str | None = None
92
+ name: str
93
+ icon: str | None = None
94
+ color: str | None = None
95
+ description: str | None = None
96
+ rolePrompt: str | None = None
97
+ forbiddenPrompt: str | None = None
98
+ skillIds: list[Any] | None = None
99
+ allowExternalSkills: bool | None = None
100
+ isBuiltin: bool | None = None
101
+ isArchived: bool | None = None
102
+ sortOrder: int | None = None
103
+ createdAt: str
104
+ updatedAt: str
105
+
106
+ class ChatMessage(BaseModel):
107
+ id: str
108
+ chatId: str | None = None
109
+ role: str
110
+ content: str
111
+ parts: list[Any] | None = None
112
+ agentId: str | None = None
113
+ toolCalls: list[Any] | None = None
114
+ toolResult: Any | None = None
115
+ createdAt: str
116
+ updatedAt: str | None = None
117
+
118
+ class ContentPart(BaseModel):
119
+ type: str
120
+ text: str | None = None
121
+ url: str | None = None
122
+ data: str | None = None
123
+ mediaType: str | None = None
124
+
125
+ class SSEEvent(BaseModel):
126
+ type: str
127
+ event: str | None = None
128
+ content: str | None = None
129
+ hint: str | None = None
130
+ message: str | None = None
131
+ code: str | None = None
132
+ orchestrationGroupId: str | None = None
133
+ taskId: str | None = None
134
+ messageId: str | None = None
135
+ payload: dict[str, Any] | None = None
136
+
137
+ class AgentSession(BaseModel):
138
+ id: str | None = None
139
+ sessionId: str
140
+ userId: str
141
+ projectId: Any | None = None
142
+ chatId: str
143
+ currentStage: str
144
+ nextStage: Any | None = None
145
+ scenario: str | None = None
146
+ stageData: Any | None = None
147
+ isActive: bool
148
+ createdAt: str
149
+ updatedAt: str
150
+
151
+ class HarnessTrace(BaseModel):
152
+ traceId: str
153
+ userId: Any | None = None
154
+ chatId: Any | None = None
155
+ sessionId: Any | None = None
156
+ assistantMessageId: Any | None = None
157
+ status: str
158
+ durationMs: Any | None = None
159
+ hadError: bool
160
+ errorMessage: Any | None = None
161
+ eventCount: int
162
+ spanCount: int
163
+ totalTokens: Any | None = None
164
+ modelId: Any | None = None
165
+ startedAtMs: Any | None = None
166
+ createdAt: str
167
+ updatedAt: str
168
+
169
+ class TraceEvent(BaseModel):
170
+ id: str | None = None
171
+ traceId: str
172
+ kind: str
173
+ name: str
174
+ sequence: int
175
+ timestampMs: int
176
+ durationMs: Any | None = None
177
+ status: Any | None = None
178
+ payload: Any | None = None
179
+ createdAt: str | None = None
180
+
181
+ class TraceSpan(BaseModel):
182
+ spanId: str
183
+ traceId: Any | None = None
184
+ parentSpanId: Any | None = None
185
+ name: str
186
+ kind: str | None = None
187
+ startMs: int
188
+ endMs: Any | None = None
189
+ durationMs: Any | None = None
190
+ status: str
191
+ attrs: dict[str, Any] | None = None
192
+
193
+ class CommandSafetyPattern(BaseModel):
194
+ id: str
195
+ label: str
196
+ description: str
197
+ pattern: str
198
+ category: str
199
+ severity: str
200
+ platform: str
201
+
202
+ class SidecarError(BaseModel):
203
+ code: int
204
+ message: str
205
+ data: Any | None = None
206
+ kind: str | None = None
207
+
208
+ class SidecarHealth(BaseModel):
209
+ status: str
210
+ version: str
211
+ protocolVersion: str | None = None
212
+ uptimeMs: int
213
+ pid: int | None = None
214
+ pythonVersion: str | None = None
215
+ platform: str | None = None
216
+ loadedProviders: list[Any] | None = None
217
+ loadedTools: int | None = None
218
+ activeTraces: int | None = None
219
+ checks: dict[str, Any] | None = None
220
+
221
+ class SidecarNotification(BaseModel):
222
+ jsonrpc: str
223
+ method: str
224
+ params: Any | None = None
225
+
226
+ class SidecarRequest(BaseModel):
227
+ jsonrpc: str
228
+ id: Any
229
+ method: str
230
+ params: Any | None = None
231
+
232
+ class SidecarResponse(BaseModel):
233
+ jsonrpc: str
234
+ id: Any
235
+ result: Any | None = None
236
+ error: Any | None = None
237
+
238
+ class ToolCall(BaseModel):
239
+ id: str
240
+ name: str
241
+ arguments: dict[str, Any]
242
+
243
+ class ToolResult(BaseModel):
244
+ success: bool
245
+ terminal: bool | None = None
246
+ needsFollowup: bool | None = None
247
+ nextAction: str | None = None
248
+ message: str | None = None
249
+ error: str | None = None
250
+ data: dict[str, Any] | None = None
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+
7
+ ToolMode = Literal["read", "safe_write", "destructive", "other"]
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class PolicyDecision:
12
+ allowed: bool
13
+ tool_mode: ToolMode
14
+ reason: str
15
+
16
+
17
+ #: Side-effect-free network reads the prefix rules cannot reach. Exact
18
+ #: names, not a ``web_`` prefix: a future ``web_deploy``-style tool must not
19
+ #: inherit the read posture. The approval algebra reads this table
20
+ #: (``ApprovalRequest.mode``), so the classification decides both the
21
+ #: prompt's risk label and the headless AutoApprover's default verdict.
22
+ _READ_EXACT = frozenset({"web_search", "web_fetch"})
23
+
24
+
25
+ def decide_tool_mode(tool_name: str) -> ToolMode:
26
+ normalized = tool_name.lower()
27
+ if normalized in _READ_EXACT:
28
+ return "read"
29
+ if normalized.startswith(("get_", "list_", "read_")):
30
+ return "read"
31
+ if normalized.startswith(("create_", "update_", "set_", "write_", "apply_")):
32
+ return "safe_write"
33
+ if normalized.startswith(("delete_", "drop_", "remove_", "destroy_")):
34
+ return "destructive"
35
+ return "other"
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import random
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class RetryPolicy:
9
+ max_attempts: int = 3
10
+ base_delay_ms: int = 200
11
+ max_delay_ms: int = 5000
12
+ jitter: bool = True
13
+
14
+
15
+ def next_retry_delay_ms(policy: RetryPolicy, attempt: int) -> int:
16
+ if attempt < 1:
17
+ attempt = 1
18
+ delay = min(policy.base_delay_ms * (2 ** (attempt - 1)), policy.max_delay_ms)
19
+ if policy.jitter:
20
+ delay = int(delay * random.uniform(0.8, 1.2))
21
+ return max(delay, 0)
@@ -0,0 +1,170 @@
1
+ """Shell command safety patterns — classify a command before it runs.
2
+
3
+ Ported from deeppath-agent's `src/harness/safety-patterns.ts` (61 built-in
4
+ rules) and kept in lockstep with the TS twin
5
+ ``packages/agent-harness/ts/src/safety-patterns.ts`` via the conformance case
6
+ ``tests/conformance/cases/safety/classify_shell_command.yaml``.
7
+
8
+ The classifier is a pure function: it never executes anything. Products decide
9
+ what to do with a ``critical`` / ``warning`` verdict (block, require consent,
10
+ or just annotate). Rule order matters: ``matched_rules`` follows
11
+ ``BUILTIN_PATTERNS`` order so both languages return identical lists.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass, field
18
+ from typing import Literal
19
+
20
+ PatternSeverity = Literal["critical", "warning"]
21
+ PatternPlatform = Literal["all", "unix", "windows"]
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class SafetyPatternDef:
26
+ id: str
27
+ label: str
28
+ description: str
29
+ pattern: str
30
+ category: str
31
+ severity: PatternSeverity
32
+ platform: PatternPlatform
33
+
34
+
35
+ SAFETY_CATEGORIES: dict[str, str] = {
36
+ "file_ops": "文件操作",
37
+ "system": "系统管理",
38
+ "process": "进程管理",
39
+ "network": "网络安全",
40
+ "package": "包管理",
41
+ "vcs": "版本控制",
42
+ "container": "容器管理",
43
+ "file_write": "文件写入",
44
+ "windows": "Windows / PowerShell",
45
+ }
46
+
47
+ # Merged from deeppath-agent local-actions.ts (web, severity=warning) and
48
+ # local-executor.ts (desktop, severity=critical). Keep aligned with the TS twin.
49
+ BUILTIN_PATTERNS: list[SafetyPatternDef] = [
50
+ # ── file_ops ──
51
+ SafetyPatternDef("rm", "rm 删除", "匹配 rm 命令", r"\brm\s", "file_ops", "warning", "unix"),
52
+ SafetyPatternDef("rm_end", "rm(行尾)", "匹配行尾的 rm", r"\brm$", "file_ops", "warning", "unix"),
53
+ SafetyPatternDef("rm_rf_root", "rm -rf /", "递归删除根目录", r"rm\s+-rf\s+\/(?:\s|$)", "file_ops", "critical", "unix"),
54
+ SafetyPatternDef("rmdir", "rmdir 删除目录", "匹配 rmdir 命令", r"\brmdir\s", "file_ops", "warning", "unix"),
55
+ SafetyPatternDef("mv", "mv 移动/重命名", "匹配 mv 命令", r"\bmv\s.*\/", "file_ops", "warning", "unix"),
56
+ SafetyPatternDef("shred", "shred 安全擦除", "不可恢复地擦除文件", r"\bshred\b", "file_ops", "warning", "unix"),
57
+ SafetyPatternDef("truncate", "truncate 截断文件", "截断文件内容", r"\btruncate\b", "file_ops", "warning", "unix"),
58
+ # ── system ──
59
+ SafetyPatternDef("sudo", "sudo 提权", "以超级用户权限执行", r"\bsudo\s", "system", "critical", "unix"),
60
+ SafetyPatternDef("mkfs", "mkfs 格式化磁盘", "创建文件系统(格式化)", r"\bmkfs\b", "system", "critical", "unix"),
61
+ SafetyPatternDef("dd", "dd 磁盘写入", "底层磁盘数据复制", r"\bdd\s", "system", "warning", "unix"),
62
+ SafetyPatternDef("dd_if", "dd if= 磁盘镜像", "使用 dd if= 读写磁盘", r"\bdd\s+if=", "system", "critical", "unix"),
63
+ SafetyPatternDef("format", "format 格式化", "格式化磁盘", r"\bformat\b", "system", "warning", "all"),
64
+ SafetyPatternDef("shutdown", "shutdown 关机", "关闭系统", r"\bshutdown\b", "system", "warning", "all"),
65
+ SafetyPatternDef("reboot", "reboot 重启", "重启系统", r"\breboot\b", "system", "warning", "all"),
66
+ SafetyPatternDef("chmod", "chmod 修改权限", "修改文件权限", r"\bchmod\s", "system", "warning", "unix"),
67
+ SafetyPatternDef("chmod_777_root", "chmod -R 777 /", "递归赋予根目录所有权限", r"chmod\s+-R\s+777\s+\/(?:\s|$)", "system", "critical", "unix"),
68
+ SafetyPatternDef("chown", "chown 修改所有者", "修改文件所有者", r"\bchown\s", "system", "warning", "unix"),
69
+ SafetyPatternDef("fork_bomb", "Fork Bomb", ":(){ :|:& };: fork 炸弹", r":\(\)\s*\{\s*:\|:&\s*\};:", "system", "critical", "unix"),
70
+ # ── process ──
71
+ SafetyPatternDef("kill", "kill 终止进程", "向进程发送信号", r"\bkill\s", "process", "warning", "unix"),
72
+ SafetyPatternDef("killall", "killall 终止所有", "按名称终止进程", r"\bkillall\s", "process", "warning", "unix"),
73
+ # ── network ──
74
+ SafetyPatternDef("curl_pipe_sh", "curl | sh", "从网络下载并直接执行脚本", r"\bcurl\s.*\|\s*(sh|bash|zsh)", "network", "warning", "unix"),
75
+ SafetyPatternDef("wget_pipe_sh", "wget | sh", "从网络下载并直接执行脚本", r"\bwget\s.*\|\s*(sh|bash|zsh)", "network", "warning", "unix"),
76
+ SafetyPatternDef("redirect_dev", "重定向到 /dev/", "向设备文件写入数据", r">\s*\/dev\/", "network", "warning", "unix"),
77
+ # ── package ──
78
+ SafetyPatternDef("npm_publish", "npm publish", "发布/取消发布 npm 包", r"\bnpm\s+(publish|unpublish)", "package", "warning", "all"),
79
+ SafetyPatternDef("pip_install", "pip install", "安装 Python 包", r"\bpip\s+install\b", "package", "warning", "all"),
80
+ SafetyPatternDef("npm_install", "npm install", "安装 npm 包", r"\bnpm\s+install\b", "package", "warning", "all"),
81
+ SafetyPatternDef("yarn_add", "yarn add", "添加 yarn 依赖", r"\byarn\s+add\b", "package", "warning", "all"),
82
+ SafetyPatternDef("pnpm_add", "pnpm add", "添加 pnpm 依赖", r"\bpnpm\s+add\b", "package", "warning", "all"),
83
+ SafetyPatternDef("uv_add", "uv add", "添加 uv 依赖", r"\buv\s+add\b", "package", "warning", "all"),
84
+ SafetyPatternDef("apt_install", "apt install/remove", "系统包管理器操作", r"\bapt(-get)?\s+(install|remove|purge)", "package", "warning", "unix"),
85
+ SafetyPatternDef("brew_install", "brew install/uninstall", "Homebrew 包管理器操作", r"\bbrew\s+(install|uninstall|remove)", "package", "warning", "unix"),
86
+ # ── vcs ──
87
+ SafetyPatternDef("git_push", "git push / reset --hard", "Git 远程推送或硬重置", r"\bgit\s+(push|reset\s+--hard|clean\s+-fd)", "vcs", "warning", "all"),
88
+ # ── container ──
89
+ SafetyPatternDef("docker_rm", "docker rm/rmi/prune", "删除容器/镜像或清理系统", r"\bdocker\s+(rm|rmi|system\s+prune)", "container", "warning", "all"),
90
+ # ── file_write ──
91
+ SafetyPatternDef("redirect_overwrite", "> / >> 重定向写入", "文件重定向覆盖或追加", r"\b(>\s|>>)\s*[^|]", "file_write", "warning", "all"),
92
+ SafetyPatternDef("tee", "tee 写入文件", "将输出写入文件", r"\btee\s", "file_write", "warning", "unix"),
93
+ SafetyPatternDef("sed_inplace", "sed -i 原地修改", "直接修改文件内容", r"\bsed\s+-i", "file_write", "warning", "unix"),
94
+ # ── windows ──
95
+ SafetyPatternDef("win_del", "del 删除", "Windows 删除命令", r"\bdel\s", "windows", "warning", "windows"),
96
+ SafetyPatternDef("win_rd", "rd 删除目录", "Windows 删除目录", r"\brd\s", "windows", "warning", "windows"),
97
+ SafetyPatternDef("win_rd_end", "rd(行尾)", "匹配行尾的 rd", r"\brd$", "windows", "warning", "windows"),
98
+ SafetyPatternDef("win_rdel", "rdel", "Windows rdel 命令", r"\brdel\b", "windows", "warning", "windows"),
99
+ SafetyPatternDef("win_del_force", "del /f /s /q 强制删除", "强制递归删除整个驱动器", r"\bdel\s+\/f\s+\/s\s+\/q\s+[a-z]:\\", "windows", "critical", "windows"),
100
+ SafetyPatternDef("win_rd_force", "rd /s /q 强制删除目录", "强制递归删除整个驱动器目录", r"\brd\s+\/s\s+\/q\s+[a-z]:\\", "windows", "critical", "windows"),
101
+ SafetyPatternDef("win_remove_item", "Remove-Item", "PowerShell 删除项", r"\bRemove-Item\b", "windows", "warning", "windows"),
102
+ SafetyPatternDef("win_stop_process", "Stop-Process", "PowerShell 终止进程", r"\bStop-Process\b", "windows", "warning", "windows"),
103
+ SafetyPatternDef("win_stop_computer", "Stop-Computer", "PowerShell 关机", r"\bStop-Computer\b", "windows", "warning", "windows"),
104
+ SafetyPatternDef("win_restart_computer", "Restart-Computer", "PowerShell 重启", r"\bRestart-Computer\b", "windows", "warning", "windows"),
105
+ SafetyPatternDef("win_set_execution_policy", "Set-ExecutionPolicy", "修改脚本执行策略", r"\bSet-ExecutionPolicy\b", "windows", "warning", "windows"),
106
+ SafetyPatternDef("win_format_volume", "Format-Volume", "PowerShell 格式化卷", r"\bFormat-Volume\b", "windows", "warning", "windows"),
107
+ SafetyPatternDef("win_clear_disk", "Clear-Disk", "PowerShell 清除磁盘", r"\bClear-Disk\b", "windows", "warning", "windows"),
108
+ SafetyPatternDef("win_wsl", "wsl 子系统", "调用 WSL 子系统", r"\bwsl\s", "windows", "warning", "windows"),
109
+ SafetyPatternDef("win_powershell_cmd", "powershell -Command", "通过 PowerShell 执行命令", r"\bpowershell\s.*-[Cc]ommand", "windows", "warning", "windows"),
110
+ SafetyPatternDef("win_pwsh", "pwsh", "PowerShell Core", r"\bpwsh\s", "windows", "warning", "windows"),
111
+ SafetyPatternDef("win_cmd_c", "cmd /c", "CMD 执行命令", r"\bcmd\s*\/c\b", "windows", "warning", "windows"),
112
+ SafetyPatternDef("win_reg", "reg delete/add", "注册表操作", r"\breg\s+(delete|add)\b", "windows", "warning", "windows"),
113
+ SafetyPatternDef("win_net", "net user/stop/start", "网络和用户管理", r"\bnet\s+(user|stop|start)\b", "windows", "warning", "windows"),
114
+ SafetyPatternDef("win_sc", "sc delete/stop/config", "服务管理", r"\bsc\s+(delete|stop|config)\b", "windows", "warning", "windows"),
115
+ SafetyPatternDef("win_diskpart", "diskpart", "磁盘分区工具", r"\bdiskpart\b", "windows", "warning", "windows"),
116
+ SafetyPatternDef("win_bcdedit", "bcdedit", "启动配置编辑", r"\bbcdedit\b", "windows", "warning", "windows"),
117
+ SafetyPatternDef("win_sfc", "sfc", "系统文件检查器", r"\bsfc\b", "windows", "warning", "windows"),
118
+ SafetyPatternDef("win_dism", "dism", "部署映像服务和管理", r"\bdism\b", "windows", "warning", "windows"),
119
+ # Only matches "format <drive>:" style disk formatting (incl. format.com),
120
+ # not PowerShell's Format-List / Format-Table output cmdlets (Format-Volume
121
+ # is covered separately above).
122
+ SafetyPatternDef("win_format_cmd", "format(Windows)", "Windows 格式化磁盘命令", r"\bformat(\.com)?\s+[a-z]:", "windows", "critical", "windows"),
123
+ ]
124
+
125
+
126
+ @dataclass(slots=True)
127
+ class CommandSafetyConfig:
128
+ disabled_pattern_ids: list[str] = field(default_factory=list)
129
+ custom_patterns: list[dict] = field(default_factory=list)
130
+
131
+
132
+ @dataclass(frozen=True, slots=True)
133
+ class ShellCommandClassification:
134
+ severity: Literal["safe", "critical", "warning"]
135
+ matched_rules: list[str]
136
+
137
+
138
+ def _compile(rule: SafetyPatternDef) -> re.Pattern[str]:
139
+ flags = re.IGNORECASE if rule.platform == "windows" else 0
140
+ return re.compile(rule.pattern, flags)
141
+
142
+
143
+ def classify_shell_command(
144
+ command: str, config: CommandSafetyConfig | None = None
145
+ ) -> ShellCommandClassification:
146
+ normalized = command.strip()
147
+ if not normalized:
148
+ return ShellCommandClassification(severity="safe", matched_rules=[])
149
+
150
+ disabled = set(config.disabled_pattern_ids if config else [])
151
+ matched = [
152
+ rule
153
+ for rule in BUILTIN_PATTERNS
154
+ if rule.id not in disabled and _compile(rule).search(normalized)
155
+ ]
156
+ if not matched:
157
+ return ShellCommandClassification(severity="safe", matched_rules=[])
158
+ severity: Literal["critical", "warning"] = (
159
+ "critical" if any(r.severity == "critical" for r in matched) else "warning"
160
+ )
161
+ return ShellCommandClassification(
162
+ severity=severity, matched_rules=[r.id for r in matched]
163
+ )
164
+
165
+
166
+ def get_patterns_by_category() -> dict[str, list[SafetyPatternDef]]:
167
+ grouped: dict[str, list[SafetyPatternDef]] = {}
168
+ for p in BUILTIN_PATTERNS:
169
+ grouped.setdefault(p.category, []).append(p)
170
+ return grouped
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class TraceSpan:
11
+ span_id: str
12
+ name: str
13
+ start_at: str = field(
14
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
15
+ )
16
+ end_at: str | None = None
17
+ attrs: dict[str, Any] = field(default_factory=dict)
18
+
19
+ def finish(self) -> None:
20
+ if self.end_at is None:
21
+ self.end_at = datetime.now(timezone.utc).isoformat()
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Secret redaction (spec/runtime/README.md "Secret redaction").
26
+ #
27
+ # Every `payload` / `attrs` / `stageData` field persisted to a trace must be
28
+ # secret-redacted first. `sanitize_for_trace` is the canonical scrubber: it
29
+ # walks a JSON-ish value and redacts (a) any dict value whose key names a
30
+ # credential, and (b) any string that *looks* like a live credential even
31
+ # under a benign key (defense in depth — a tool result echoing an
32
+ # `Authorization: Bearer …` header or an `sk-…` key must not land in a trace).
33
+ # ---------------------------------------------------------------------------
34
+
35
+ #: Replacement text for anything redacted.
36
+ REDACTED = "***"
37
+
38
+ #: Secret key names, compared after normalizing to lowercase alphanumerics
39
+ #: (so `api_key`, `apiKey`, `api-key`, `ApiKey` all match `apikey`). Exact
40
+ #: match only — substring matching would false-positive on `tokenize`,
41
+ #: `monkey`, `authority`, etc.
42
+ _SECRET_KEY_NAMES = frozenset(
43
+ {
44
+ "password",
45
+ "passwd",
46
+ "pwd",
47
+ "secret",
48
+ "token",
49
+ "accesstoken",
50
+ "refreshtoken",
51
+ "idtoken",
52
+ "apikey",
53
+ "authorization",
54
+ "auth",
55
+ "credential",
56
+ "credentials",
57
+ "clientsecret",
58
+ "privatekey",
59
+ "sessionkey",
60
+ "sessionid",
61
+ "cookie",
62
+ "setcookie",
63
+ "xapikey",
64
+ }
65
+ )
66
+
67
+ _KEY_NORMALIZE = re.compile(r"[^a-z0-9]")
68
+
69
+ #: High-confidence live-credential value patterns. Kept narrow on purpose —
70
+ #: each is a well-known secret prefix/format, so matches are almost never
71
+ #: benign. Long opaque strings under a non-secret key are left alone (a hash
72
+ #: or id is not a credential).
73
+ _SECRET_VALUE_PATTERNS = (
74
+ # PEM private keys (any algorithm).
75
+ re.compile(
76
+ r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?-----END [A-Z0-9 ]*PRIVATE KEY-----",
77
+ re.DOTALL,
78
+ ),
79
+ # Authorization: Bearer <token>.
80
+ re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]{8,}", re.IGNORECASE),
81
+ # Common API-key prefixes: OpenAI/DeepSeek `sk-…`, GitHub
82
+ # (`ghp_`/`gho_`/`ghu_`/`ghs_`/`ghr_`/`github_pat_…`), GitLab `glpat-…`,
83
+ # Slack `xox…-…`, AWS `AKIA`/`ASIA…`.
84
+ re.compile(
85
+ r"\b(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{16,}"
86
+ r"|glpat-[A-Za-z0-9_-]{12,}|xox[baprs]-[A-Za-z0-9-]{8,}|(?:AKIA|ASIA)[A-Z0-9]{16})\b"
87
+ ),
88
+ )
89
+
90
+
91
+ def _is_secret_key(key: Any) -> bool:
92
+ if not isinstance(key, str):
93
+ return False
94
+ return _KEY_NORMALIZE.sub("", key.lower()) in _SECRET_KEY_NAMES
95
+
96
+
97
+ def _scrub_string(text: str) -> str:
98
+ for pattern in _SECRET_VALUE_PATTERNS:
99
+ text = pattern.sub(REDACTED, text)
100
+ return text
101
+
102
+
103
+ def sanitize_for_trace(value: Any, *, extra_keys: frozenset[str] | None = None) -> Any:
104
+ """Return ``value`` with credentials redacted, safe to persist in a trace.
105
+
106
+ Recursively walks dicts / lists / tuples / strings:
107
+
108
+ - a dict value whose key names a credential (see ``_SECRET_KEY_NAMES``,
109
+ plus ``extra_keys``) is replaced with ``REDACTED`` regardless of content;
110
+ - a string is scanned for live-credential patterns (Bearer tokens, ``sk-…``
111
+ keys, PEM private keys, …) and each match replaced with ``REDACTED``;
112
+ - all other values pass through unchanged.
113
+
114
+ The input is never mutated; a new structure is returned. Non-JSON scalar
115
+ types (numbers, booleans, ``None``) are returned as-is.
116
+ """
117
+ secret_names = (
118
+ _SECRET_KEY_NAMES | {_KEY_NORMALIZE.sub("", k.lower()) for k in extra_keys}
119
+ if extra_keys
120
+ else _SECRET_KEY_NAMES
121
+ )
122
+
123
+ def walk(node: Any) -> Any:
124
+ if isinstance(node, dict):
125
+ out: dict[Any, Any] = {}
126
+ for k, v in node.items():
127
+ if isinstance(k, str) and _KEY_NORMALIZE.sub("", k.lower()) in secret_names:
128
+ out[k] = REDACTED
129
+ else:
130
+ out[k] = walk(v)
131
+ return out
132
+ if isinstance(node, (list, tuple)):
133
+ return [walk(item) for item in node]
134
+ if isinstance(node, str):
135
+ return _scrub_string(node)
136
+ return node
137
+
138
+ return walk(value)
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: steerable-agent-harness
3
+ Version: 0.6.0
4
+ Summary: Steerable harness helpers for Python
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pydantic>=2.10.0
8
+ Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
9
+
10
+ # steerable-agent-harness
11
+
12
+ Python harness primitives and policy helpers for Steerable.
@@ -0,0 +1,12 @@
1
+ steerable_agent_harness/__init__.py,sha256=NPyjrYCQQBvS7aJUu-UknVI6RHa-H-1wa3UCaxXGYl4,920
2
+ steerable_agent_harness/budget.py,sha256=Cx38m3ifbbjVtG-FJPKpY4TrBsgS5AOTOaVGybkbUvk,927
3
+ steerable_agent_harness/completion.py,sha256=FMqUyfyUssFmZXwA7ohwWMqj93gsXYsyd1VdfkZxCl0,343
4
+ steerable_agent_harness/generated.py,sha256=jeynKncOP0HWEpXcFs7A7y3z4zloR0njCar2gMcBfbo,6005
5
+ steerable_agent_harness/policy.py,sha256=nCv8F1U6Mmke0q1i6ZnGC6WaIK6BIWbXzj8XwxHYcIE,1137
6
+ steerable_agent_harness/retry.py,sha256=UKDAfpKCc92oWABQb2rPepeCg3RYt45X3lHL18OGMQo,528
7
+ steerable_agent_harness/safety.py,sha256=WwnI5LRNw9UuGb_a5t5u97_YgmmYVD7sYsQKPVlbD8c,11637
8
+ steerable_agent_harness/tracing.py,sha256=PpcqRCrC4NIiESRSaUAPZi0FeZQn4gRxCUlqVSngKXg,4762
9
+ steerable_agent_harness-0.6.0.dist-info/METADATA,sha256=Pd79fzji_JDunEtpCvjYJ_wXbgdfb69dyUgY1aE3egA,351
10
+ steerable_agent_harness-0.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ steerable_agent_harness-0.6.0.dist-info/top_level.txt,sha256=L99GRcOHaeL_gakhZZnmpEbK5yV4fOeIxXzWKW_2lDA,24
12
+ steerable_agent_harness-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ steerable_agent_harness