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.
- steerable_agent_harness/__init__.py +37 -0
- steerable_agent_harness/budget.py +38 -0
- steerable_agent_harness/completion.py +13 -0
- steerable_agent_harness/generated.py +250 -0
- steerable_agent_harness/policy.py +35 -0
- steerable_agent_harness/retry.py +21 -0
- steerable_agent_harness/safety.py +170 -0
- steerable_agent_harness/tracing.py +138 -0
- steerable_agent_harness-0.6.0.dist-info/METADATA +12 -0
- steerable_agent_harness-0.6.0.dist-info/RECORD +12 -0
- steerable_agent_harness-0.6.0.dist-info/WHEEL +5 -0
- steerable_agent_harness-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -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 @@
|
|
|
1
|
+
steerable_agent_harness
|