computer-agent-py 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/PKG-INFO +4 -4
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/README.md +3 -3
- computer_agent_py-0.3.0/pyproject.toml +177 -0
- computer_agent_py-0.2.2/pyproject.toml → computer_agent_py-0.3.0/pyproject.toml.orig +1 -1
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/__init__.py +3 -1
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/_proxy/query.py +2 -2
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/engines/claude_agent.py +85 -1
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/__init__.py +27 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/coordinator.py +76 -0
- computer_agent_py-0.3.0/src/computeragent/harness/fork.py +409 -0
- computer_agent_py-0.3.0/src/computeragent/identity/gap.py +411 -0
- computer_agent_py-0.3.0/src/computeragent/identity/gap_hooks.py +169 -0
- computer_agent_py-0.3.0/src/computeragent/identity/gap_subagents.py +137 -0
- computer_agent_py-0.3.0/src/computeragent/identity/gap_tools.py +243 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/policy/cedar.py +2 -2
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/policy/srs.py +11 -2
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/protocol_types/__init__.py +7 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/event.py +7 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/sinks/agentos_http.py +5 -0
- computer_agent_py-0.2.2/src/computeragent/identity/gap.py +0 -246
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/_proxy/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/_proxy/client.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/agentos.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/engines/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/engines/git_agent.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/agent_resolve_client.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/cas_client.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/errors.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/git_credential_client.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/protocol.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/remote.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/usage.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/identity/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/identity/passthrough.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/policy/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/policy/authorizer.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/policy/opa.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/policy/types.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/py.typed +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/session_stores/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/session_stores/memory.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/substrates/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/substrates/local.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/config.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/middleware/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/middleware/guardrails.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/middleware/pii.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/pipeline.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/sinks/__init__.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/sinks/content.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/telemetry/sinks/otel.py +0 -0
- {computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: computer-agent-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks.
|
|
5
5
|
Keywords: computeragent,claude-agent-sdk,claude,agent,telemetry,otel,opentelemetry,agentos,pii
|
|
6
6
|
Author: Abhi Bhat
|
|
@@ -509,7 +509,7 @@ configure(
|
|
|
509
509
|
PiiRedactor(strategy="hash", extra_patterns=[r"BADGE-\d{6}"]),
|
|
510
510
|
GuardrailFilter(
|
|
511
511
|
max_attribute_length=4096,
|
|
512
|
-
tool_name_allowlist={"Read", "Glob", "Grep", "
|
|
512
|
+
tool_name_allowlist={"Read", "Glob", "Grep", "mcp__acme-assist-tools__*"},
|
|
513
513
|
cost_ceiling_usd=1.50,
|
|
514
514
|
),
|
|
515
515
|
],
|
|
@@ -551,7 +551,7 @@ opa = OpaPolicyEngine(
|
|
|
551
551
|
authorizer = PolicyToolAuthorizer(
|
|
552
552
|
engine=opa,
|
|
553
553
|
principal_resolver=lambda ctx: PolicyPrincipal(id="alice", groups=["engineer"]),
|
|
554
|
-
resource_resolver=lambda ctx: PolicyResource(agent_name="
|
|
554
|
+
resource_resolver=lambda ctx: PolicyResource(agent_name="acme-assist", model="claude-sonnet-4-5"),
|
|
555
555
|
context_resolver=lambda ctx: {"env": "prod"},
|
|
556
556
|
)
|
|
557
557
|
|
|
@@ -650,7 +650,7 @@ The proxy is a pure tap — messages are never modified or reordered. Sinks run
|
|
|
650
650
|
|
|
651
651
|
| File | Demonstrates |
|
|
652
652
|
|---|---|
|
|
653
|
-
| [`examples/
|
|
653
|
+
| [`examples/drop_in.py`](examples/drop_in.py) | The minimum drop-in change |
|
|
654
654
|
| [`examples/with_otel.py`](examples/with_otel.py) | OTel pointed at a local collector |
|
|
655
655
|
| [`examples/with_new_relic.py`](examples/with_new_relic.py) | OTel pointed at New Relic (just env vars) |
|
|
656
656
|
| [`examples/with_datadog.py`](examples/with_datadog.py) | OTel pointed at Datadog |
|
|
@@ -452,7 +452,7 @@ configure(
|
|
|
452
452
|
PiiRedactor(strategy="hash", extra_patterns=[r"BADGE-\d{6}"]),
|
|
453
453
|
GuardrailFilter(
|
|
454
454
|
max_attribute_length=4096,
|
|
455
|
-
tool_name_allowlist={"Read", "Glob", "Grep", "
|
|
455
|
+
tool_name_allowlist={"Read", "Glob", "Grep", "mcp__acme-assist-tools__*"},
|
|
456
456
|
cost_ceiling_usd=1.50,
|
|
457
457
|
),
|
|
458
458
|
],
|
|
@@ -494,7 +494,7 @@ opa = OpaPolicyEngine(
|
|
|
494
494
|
authorizer = PolicyToolAuthorizer(
|
|
495
495
|
engine=opa,
|
|
496
496
|
principal_resolver=lambda ctx: PolicyPrincipal(id="alice", groups=["engineer"]),
|
|
497
|
-
resource_resolver=lambda ctx: PolicyResource(agent_name="
|
|
497
|
+
resource_resolver=lambda ctx: PolicyResource(agent_name="acme-assist", model="claude-sonnet-4-5"),
|
|
498
498
|
context_resolver=lambda ctx: {"env": "prod"},
|
|
499
499
|
)
|
|
500
500
|
|
|
@@ -593,7 +593,7 @@ The proxy is a pure tap — messages are never modified or reordered. Sinks run
|
|
|
593
593
|
|
|
594
594
|
| File | Demonstrates |
|
|
595
595
|
|---|---|
|
|
596
|
-
| [`examples/
|
|
596
|
+
| [`examples/drop_in.py`](examples/drop_in.py) | The minimum drop-in change |
|
|
597
597
|
| [`examples/with_otel.py`](examples/with_otel.py) | OTel pointed at a local collector |
|
|
598
598
|
| [`examples/with_new_relic.py`](examples/with_new_relic.py) | OTel pointed at New Relic (just env vars) |
|
|
599
599
|
| [`examples/with_datadog.py`](examples/with_datadog.py) | OTel pointed at Datadog |
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "computer-agent-py"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
keywords = [
|
|
8
|
+
"computeragent",
|
|
9
|
+
"claude-agent-sdk",
|
|
10
|
+
"claude",
|
|
11
|
+
"agent",
|
|
12
|
+
"telemetry",
|
|
13
|
+
"otel",
|
|
14
|
+
"opentelemetry",
|
|
15
|
+
"agentos",
|
|
16
|
+
"pii",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"claude-agent-sdk>=0.2,<0.3",
|
|
33
|
+
"typing-extensions>=4.12",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[[project.authors]]
|
|
37
|
+
name = "Abhi Bhat"
|
|
38
|
+
email = "abhishek.bhat@lyzr.ai"
|
|
39
|
+
|
|
40
|
+
[project.license]
|
|
41
|
+
text = "MIT"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
otel = [
|
|
45
|
+
"opentelemetry-api>=1.27",
|
|
46
|
+
"opentelemetry-sdk>=1.27",
|
|
47
|
+
"opentelemetry-exporter-otlp>=1.27",
|
|
48
|
+
"opentelemetry-semantic-conventions>=0.48b0",
|
|
49
|
+
]
|
|
50
|
+
agentos = ["httpx>=0.27"]
|
|
51
|
+
remote = ["httpx>=0.27"]
|
|
52
|
+
cedar = ["cedarpy>=4,<5"]
|
|
53
|
+
gitagent = [
|
|
54
|
+
"openai>=1,<2",
|
|
55
|
+
"anthropic>=0.40,<1",
|
|
56
|
+
"pyyaml>=6,<7",
|
|
57
|
+
]
|
|
58
|
+
gap = ["pyyaml>=6,<7"]
|
|
59
|
+
sqlite = ["aiosqlite>=0.20,<1"]
|
|
60
|
+
all = [
|
|
61
|
+
"opentelemetry-api>=1.27",
|
|
62
|
+
"opentelemetry-sdk>=1.27",
|
|
63
|
+
"opentelemetry-exporter-otlp>=1.27",
|
|
64
|
+
"opentelemetry-semantic-conventions>=0.48b0",
|
|
65
|
+
"httpx>=0.27",
|
|
66
|
+
"cedarpy>=4,<5",
|
|
67
|
+
"openai>=1,<2",
|
|
68
|
+
"anthropic>=0.40,<1",
|
|
69
|
+
"pyyaml>=6,<7",
|
|
70
|
+
"aiosqlite>=0.20,<1",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[project.urls]
|
|
74
|
+
Homepage = "https://github.com/open-gitagent/computer-agent-py"
|
|
75
|
+
Issues = "https://github.com/open-gitagent/computer-agent-py/issues"
|
|
76
|
+
Changelog = "https://github.com/open-gitagent/computer-agent-py/blob/main/CHANGELOG.md"
|
|
77
|
+
|
|
78
|
+
[build-system]
|
|
79
|
+
requires = ["uv_build>=0.9.24,<0.10.0"]
|
|
80
|
+
build-backend = "uv_build"
|
|
81
|
+
|
|
82
|
+
[tool.uv.build-backend]
|
|
83
|
+
module-name = "computeragent"
|
|
84
|
+
|
|
85
|
+
[tool.ruff]
|
|
86
|
+
line-length = 100
|
|
87
|
+
target-version = "py310"
|
|
88
|
+
src = [
|
|
89
|
+
"src",
|
|
90
|
+
"tests",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint]
|
|
94
|
+
select = [
|
|
95
|
+
"E",
|
|
96
|
+
"F",
|
|
97
|
+
"W",
|
|
98
|
+
"I",
|
|
99
|
+
"UP",
|
|
100
|
+
"B",
|
|
101
|
+
"ASYNC",
|
|
102
|
+
"RET",
|
|
103
|
+
"SIM",
|
|
104
|
+
"TCH",
|
|
105
|
+
]
|
|
106
|
+
ignore = ["E501"]
|
|
107
|
+
|
|
108
|
+
[tool.ruff.lint.per-file-ignores]
|
|
109
|
+
"tests/**" = [
|
|
110
|
+
"E501",
|
|
111
|
+
"ASYNC",
|
|
112
|
+
"TC001",
|
|
113
|
+
"TC003",
|
|
114
|
+
]
|
|
115
|
+
"examples/**" = ["E501"]
|
|
116
|
+
"src/computeragent/__init__.py" = [
|
|
117
|
+
"F401",
|
|
118
|
+
"F403",
|
|
119
|
+
]
|
|
120
|
+
"src/computeragent/types.py" = [
|
|
121
|
+
"F401",
|
|
122
|
+
"F403",
|
|
123
|
+
]
|
|
124
|
+
"src/computeragent/harness/**" = [
|
|
125
|
+
"TC001",
|
|
126
|
+
"TC003",
|
|
127
|
+
"SIM101",
|
|
128
|
+
]
|
|
129
|
+
"src/computeragent/engines/**" = [
|
|
130
|
+
"TC001",
|
|
131
|
+
"TC003",
|
|
132
|
+
]
|
|
133
|
+
"src/computeragent/substrates/**" = ["TC003"]
|
|
134
|
+
"src/computeragent/identity/**" = ["TC003"]
|
|
135
|
+
"src/computeragent/session_stores/**" = ["TC001"]
|
|
136
|
+
"src/computeragent/protocol_types/**" = ["TC003"]
|
|
137
|
+
|
|
138
|
+
[tool.mypy]
|
|
139
|
+
strict = true
|
|
140
|
+
python_version = "3.10"
|
|
141
|
+
files = ["src"]
|
|
142
|
+
ignore_missing_imports = true
|
|
143
|
+
|
|
144
|
+
[[tool.mypy.overrides]]
|
|
145
|
+
module = "tests.*"
|
|
146
|
+
disallow_untyped_defs = false
|
|
147
|
+
disallow_incomplete_defs = false
|
|
148
|
+
|
|
149
|
+
[tool.pytest.ini_options]
|
|
150
|
+
asyncio_mode = "auto"
|
|
151
|
+
testpaths = ["tests"]
|
|
152
|
+
addopts = [
|
|
153
|
+
"-ra",
|
|
154
|
+
"--strict-markers",
|
|
155
|
+
"--strict-config",
|
|
156
|
+
]
|
|
157
|
+
markers = ["integration: requires real Claude CLI and credentials"]
|
|
158
|
+
|
|
159
|
+
[dependency-groups]
|
|
160
|
+
dev = [
|
|
161
|
+
"pytest>=8",
|
|
162
|
+
"pytest-asyncio>=0.23",
|
|
163
|
+
"pytest-cov>=5",
|
|
164
|
+
"respx>=0.21",
|
|
165
|
+
"ruff>=0.6",
|
|
166
|
+
"mypy>=1.11",
|
|
167
|
+
"httpx>=0.27",
|
|
168
|
+
"opentelemetry-api>=1.27",
|
|
169
|
+
"opentelemetry-sdk>=1.27",
|
|
170
|
+
"opentelemetry-exporter-otlp>=1.27",
|
|
171
|
+
"opentelemetry-semantic-conventions>=0.48b0",
|
|
172
|
+
"cedarpy>=4,<5",
|
|
173
|
+
"openai>=1,<2",
|
|
174
|
+
"anthropic>=0.40,<1",
|
|
175
|
+
"pyyaml>=6,<7",
|
|
176
|
+
"aiosqlite>=0.20,<1",
|
|
177
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "computer-agent-py"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "Drop-in replacement for claude-agent-sdk that adds a proxied telemetry pipeline (PII redaction + guardrails) with OpenTelemetry and AgentOS sinks."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Abhi Bhat", email = "abhishek.bhat@lyzr.ai" },
|
|
@@ -42,7 +42,7 @@ _upstream_all = [n for n in getattr(_cas, "__all__", []) if n != "__version__"]
|
|
|
42
42
|
for _name in _upstream_all:
|
|
43
43
|
globals()[_name] = getattr(_cas, _name)
|
|
44
44
|
|
|
45
|
-
__version__ = "0.
|
|
45
|
+
__version__ = "0.3.0"
|
|
46
46
|
upstream_version = getattr(_cas, "__version__", "unknown")
|
|
47
47
|
|
|
48
48
|
# Override the two entry points with the proxied versions. Same signatures,
|
|
@@ -60,6 +60,7 @@ from .harness import ( # noqa: E402
|
|
|
60
60
|
ChatInput,
|
|
61
61
|
ChatResult,
|
|
62
62
|
ComputerAgent,
|
|
63
|
+
ForkConfig,
|
|
63
64
|
HarnessError,
|
|
64
65
|
HarnessProtocolError,
|
|
65
66
|
UnknownEngineError,
|
|
@@ -127,6 +128,7 @@ __all__ = sorted({*_upstream_all, "__version__"}) + [
|
|
|
127
128
|
"PolicyResource",
|
|
128
129
|
"PolicyResult",
|
|
129
130
|
"PolicyToolAuthorizer",
|
|
131
|
+
"ForkConfig",
|
|
130
132
|
"TelemetryEvent",
|
|
131
133
|
"TelemetryMiddleware",
|
|
132
134
|
"TelemetrySink",
|
|
@@ -169,8 +169,8 @@ def _derive_agent_name(options: Any) -> str | None:
|
|
|
169
169
|
Strategy:
|
|
170
170
|
1. Explicit ``agent_name`` attribute (not in upstream today but cheap to
|
|
171
171
|
honour if a fork adds it).
|
|
172
|
-
2. First non-empty line of ``system_prompt`` (
|
|
173
|
-
|
|
172
|
+
2. First non-empty line of ``system_prompt`` (prod prompts typically
|
|
173
|
+
start with "You are <Name>..." — that line is the natural label).
|
|
174
174
|
3. ``None`` — the sinks fall back to an anonymous hash.
|
|
175
175
|
"""
|
|
176
176
|
if options is None:
|
{computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/engines/claude_agent.py
RENAMED
|
@@ -105,7 +105,29 @@ def _build_options(ctx: EngineContext) -> Any:
|
|
|
105
105
|
# carry one) so the resulting policy_decision telemetry correlates with
|
|
106
106
|
# this session's open OTel spans instead of landing on "unknown".
|
|
107
107
|
if ctx.policy_decider is not None:
|
|
108
|
-
|
|
108
|
+
bound = _bind_session_id(ctx.policy_decider, ctx.session_id)
|
|
109
|
+
payload["can_use_tool"] = bound
|
|
110
|
+
# Policy enforcement that survives prompt-skipping permission modes.
|
|
111
|
+
# ``can_use_tool`` only fires when the CLI's permission rules evaluate
|
|
112
|
+
# to "ask" — under bypassPermissions / dontAsk / auto (and for edits
|
|
113
|
+
# under acceptEdits) it is skipped entirely, which would leave a
|
|
114
|
+
# console-bound policy unenforced. A PreToolUse hook fires for EVERY
|
|
115
|
+
# tool call regardless of permission mode (SDK-documented), so route
|
|
116
|
+
# the same bound decider through one and translate a deny into the
|
|
117
|
+
# hook's deny decision — mirroring the TS engine. Registered only for
|
|
118
|
+
# the skipping modes so default-mode behavior (and telemetry volume)
|
|
119
|
+
# is unchanged. MERGED with GAP/caller-supplied hooks, never replacing.
|
|
120
|
+
if payload.get("permission_mode") in (
|
|
121
|
+
"bypassPermissions",
|
|
122
|
+
"dontAsk",
|
|
123
|
+
"auto",
|
|
124
|
+
"acceptEdits",
|
|
125
|
+
):
|
|
126
|
+
hooks = dict(payload.get("hooks") or {})
|
|
127
|
+
pre = list(hooks.get("PreToolUse") or [])
|
|
128
|
+
pre.append(_policy_pretooluse_matcher(bound))
|
|
129
|
+
hooks["PreToolUse"] = pre
|
|
130
|
+
payload["hooks"] = hooks
|
|
109
131
|
|
|
110
132
|
options = ClaudeAgentOptions(**payload)
|
|
111
133
|
# Surface harness-supplied agent identity to the proxy's
|
|
@@ -119,6 +141,68 @@ def _build_options(ctx: EngineContext) -> Any:
|
|
|
119
141
|
return options
|
|
120
142
|
|
|
121
143
|
|
|
144
|
+
def _policy_pretooluse_matcher(decider: Any) -> Any:
|
|
145
|
+
"""Wrap the bound ``can_use_tool`` decider as a PreToolUse ``HookMatcher``.
|
|
146
|
+
|
|
147
|
+
The hook receives the SDK's PreToolUse input (``tool_name`` /
|
|
148
|
+
``tool_input`` / ``tool_use_id``), runs the SAME decider that backs
|
|
149
|
+
``can_use_tool``, and maps a deny onto the hook's
|
|
150
|
+
``permissionDecision: "deny"`` — the only gate that fires under
|
|
151
|
+
prompt-skipping permission modes. An allow returns ``{}`` (no opinion).
|
|
152
|
+
An unexpected decider exception fails CLOSED (the authorizer's own
|
|
153
|
+
fail-open handling lives inside it; anything escaping is a bug).
|
|
154
|
+
"""
|
|
155
|
+
from claude_agent_sdk.types import HookMatcher
|
|
156
|
+
|
|
157
|
+
async def _hook(hook_input: Any, _tool_use_id: str | None, _context: Any) -> dict[str, Any]:
|
|
158
|
+
data = hook_input if isinstance(hook_input, dict) else {}
|
|
159
|
+
tool_name = str(data.get("tool_name") or "")
|
|
160
|
+
tool_input = data.get("tool_input") or {}
|
|
161
|
+
try:
|
|
162
|
+
result = await decider(tool_name, tool_input, _permission_context())
|
|
163
|
+
except Exception as exc: # noqa: BLE001 — fail closed on decider bugs
|
|
164
|
+
logger.exception("policy PreToolUse hook: decider raised")
|
|
165
|
+
return _hook_deny(f"policy evaluation failed: {exc}")
|
|
166
|
+
behavior = getattr(result, "behavior", None)
|
|
167
|
+
if behavior is None and isinstance(result, dict):
|
|
168
|
+
behavior = result.get("behavior")
|
|
169
|
+
if behavior == "deny":
|
|
170
|
+
message = getattr(result, "message", None)
|
|
171
|
+
if message is None and isinstance(result, dict):
|
|
172
|
+
message = result.get("message")
|
|
173
|
+
return _hook_deny(str(message or "blocked by policy"))
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
return HookMatcher(hooks=[_hook])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _permission_context() -> Any:
|
|
180
|
+
"""A best-effort ``ToolPermissionContext`` for hook-originated decisions.
|
|
181
|
+
|
|
182
|
+
The hook path has no real permission context (the CLI never built one —
|
|
183
|
+
the call skipped the prompt), so hand the decider an empty one; the
|
|
184
|
+
``_bind_session_id`` wrapper still stamps the session id onto it.
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
from claude_agent_sdk.types import ToolPermissionContext
|
|
188
|
+
|
|
189
|
+
return ToolPermissionContext()
|
|
190
|
+
except Exception: # noqa: BLE001 — required fields / import drift
|
|
191
|
+
from types import SimpleNamespace
|
|
192
|
+
|
|
193
|
+
return SimpleNamespace()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _hook_deny(reason: str) -> dict[str, Any]:
|
|
197
|
+
return {
|
|
198
|
+
"hookSpecificOutput": {
|
|
199
|
+
"hookEventName": "PreToolUse",
|
|
200
|
+
"permissionDecision": "deny",
|
|
201
|
+
"permissionDecisionReason": reason,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
122
206
|
def _bind_session_id(decider: Any, session_id: str) -> Any:
|
|
123
207
|
"""Wrap a ``can_use_tool`` decider so the permission context carries the
|
|
124
208
|
harness session id. ``ToolPermissionContext`` is a plain dataclass, so the
|
|
@@ -204,6 +204,18 @@ class ComputerAgent:
|
|
|
204
204
|
# id from the UI and paste it here. When omitted, the policy bound to
|
|
205
205
|
# `agent_name` in AgentOS (the UI's Policy tab) is fetched and used.
|
|
206
206
|
policy_id: str | None = None,
|
|
207
|
+
# ── process-fork (opt-in; OFF by default) ───────────────────────────
|
|
208
|
+
# ``fork_mode="manual"`` exposes the ``mcp__fork__spawn`` tool so the
|
|
209
|
+
# agent can spawn SEPARATE child ComputerAgent processes (each its own
|
|
210
|
+
# session/agent id). ``"auto"`` additionally disables the native
|
|
211
|
+
# in-process Task sub-agent and steers all delegation to the fork tool.
|
|
212
|
+
# ``"off"`` (default) = today's behavior, untouched.
|
|
213
|
+
fork_mode: str = "off",
|
|
214
|
+
fork_config: Any = None,
|
|
215
|
+
# Internal — set by the fork tool when THIS agent is itself a child.
|
|
216
|
+
parent_session_id: str | None = None,
|
|
217
|
+
parent_agent_id: str | None = None,
|
|
218
|
+
_fork_depth: int = 0,
|
|
207
219
|
) -> None:
|
|
208
220
|
from .. import agentos as _agentos
|
|
209
221
|
|
|
@@ -366,6 +378,12 @@ class ComputerAgent:
|
|
|
366
378
|
|
|
367
379
|
policy_decider = srs_authorizer_from_env(agent_name or "")
|
|
368
380
|
|
|
381
|
+
# Fork config: default one in when a mode is on but none supplied.
|
|
382
|
+
if fork_mode != "off" and fork_config is None:
|
|
383
|
+
from .fork import ForkConfig
|
|
384
|
+
|
|
385
|
+
fork_config = ForkConfig()
|
|
386
|
+
|
|
369
387
|
self._coordinator = HarnessCoordinator(
|
|
370
388
|
engine=resolved_engine,
|
|
371
389
|
substrate=resolved_substrate,
|
|
@@ -386,6 +404,12 @@ class ComputerAgent:
|
|
|
386
404
|
# layers these on top of whatever the identity loader resolves
|
|
387
405
|
# so explicit caller kwargs always win.
|
|
388
406
|
options_overlay=merged_options,
|
|
407
|
+
# Process-fork wiring (opt-in).
|
|
408
|
+
parent_session_id=parent_session_id,
|
|
409
|
+
parent_agent_id=parent_agent_id,
|
|
410
|
+
fork_mode=fork_mode,
|
|
411
|
+
fork_config=fork_config,
|
|
412
|
+
fork_depth=_fork_depth,
|
|
389
413
|
)
|
|
390
414
|
|
|
391
415
|
async def __aenter__(self) -> ComputerAgent:
|
|
@@ -517,11 +541,14 @@ async def run_task(
|
|
|
517
541
|
return await agent.chat(message)
|
|
518
542
|
|
|
519
543
|
|
|
544
|
+
from .fork import ForkConfig # noqa: E402 (re-exported for callers)
|
|
545
|
+
|
|
520
546
|
__all__ = [
|
|
521
547
|
"ChatHandle",
|
|
522
548
|
"ChatInput",
|
|
523
549
|
"ChatResult",
|
|
524
550
|
"ComputerAgent",
|
|
551
|
+
"ForkConfig",
|
|
525
552
|
"HarnessError",
|
|
526
553
|
"HarnessProtocolError",
|
|
527
554
|
"UnknownEngineError",
|
{computer_agent_py-0.2.2 → computer_agent_py-0.3.0}/src/computeragent/harness/coordinator.py
RENAMED
|
@@ -92,6 +92,12 @@ class HarnessCoordinator:
|
|
|
92
92
|
telemetry_pipeline: Pipeline | None = None,
|
|
93
93
|
project_key: str = "default",
|
|
94
94
|
options_overlay: dict[str, Any] | None = None,
|
|
95
|
+
# ── process-fork (opt-in; see harness/fork.py) ──────────────────────
|
|
96
|
+
parent_session_id: str | None = None,
|
|
97
|
+
parent_agent_id: str | None = None,
|
|
98
|
+
fork_mode: str = "off",
|
|
99
|
+
fork_config: Any = None,
|
|
100
|
+
fork_depth: int = 0,
|
|
95
101
|
) -> None:
|
|
96
102
|
self._engine = engine
|
|
97
103
|
self._substrate = substrate
|
|
@@ -105,6 +111,16 @@ class HarnessCoordinator:
|
|
|
105
111
|
# Stable agent identifier (telemetry/registry only — never affects
|
|
106
112
|
# execution). Stamped onto every emitted event via ``_emit``.
|
|
107
113
|
self._agent_id = agent_id
|
|
114
|
+
# Process-fork linkage + config. ``parent_*`` are stamped onto every
|
|
115
|
+
# emitted event (like ``agent_id``) so a forked child links to its
|
|
116
|
+
# parent. The fork server (mcp__fork__spawn) is injected into the run
|
|
117
|
+
# options in ``_ensure_booted`` when ``fork_mode != "off"``.
|
|
118
|
+
self._parent_session_id = parent_session_id
|
|
119
|
+
self._parent_agent_id = parent_agent_id
|
|
120
|
+
self._fork_mode = fork_mode
|
|
121
|
+
self._fork_config = fork_config
|
|
122
|
+
self._fork_depth = fork_depth
|
|
123
|
+
self._fork_context: Any = None
|
|
108
124
|
self._policy_decider = policy_decider
|
|
109
125
|
self._on_permission_request_user = on_permission_request
|
|
110
126
|
self._budget_max_usd = budget_max_usd
|
|
@@ -174,6 +190,54 @@ class HarnessCoordinator:
|
|
|
174
190
|
from dataclasses import replace
|
|
175
191
|
|
|
176
192
|
loaded = replace(loaded, options=merged_options)
|
|
193
|
+
# Loader-declared compliance hardening (e.g. GAP human-in-the-loop →
|
|
194
|
+
# permission mode) runs AFTER the caller overlay, so strictest-wins
|
|
195
|
+
# constraints hold even against caller-supplied options.
|
|
196
|
+
harden = getattr(loaded, "harden", None)
|
|
197
|
+
if callable(harden):
|
|
198
|
+
from dataclasses import replace
|
|
199
|
+
|
|
200
|
+
with suppress(Exception):
|
|
201
|
+
hardened = harden(dict(loaded.options or {}))
|
|
202
|
+
if isinstance(hardened, dict):
|
|
203
|
+
loaded = replace(loaded, options=hardened)
|
|
204
|
+
# ── process-fork tool injection (opt-in) ─────────────────────────────
|
|
205
|
+
# Injected HERE (loader-agnostic, after overlay+harden) rather than via
|
|
206
|
+
# the caller overlay — the overlay is a shallow ``dict.update`` and would
|
|
207
|
+
# CLOBBER the loader's ``mcp_servers`` (e.g. gap_tools). We deep-merge so
|
|
208
|
+
# the fork server coexists with gap_tools + agents. A child only gets the
|
|
209
|
+
# tool while under the depth cap.
|
|
210
|
+
if (
|
|
211
|
+
self._fork_mode != "off"
|
|
212
|
+
and self._fork_config is not None
|
|
213
|
+
and self._fork_depth < getattr(self._fork_config, "max_depth", 0)
|
|
214
|
+
):
|
|
215
|
+
from dataclasses import replace
|
|
216
|
+
|
|
217
|
+
from .fork import ForkContext, append_steer, build_fork_mcp_server
|
|
218
|
+
|
|
219
|
+
self._fork_context = ForkContext(
|
|
220
|
+
self, self._fork_config, self._fork_depth, self._fork_mode
|
|
221
|
+
)
|
|
222
|
+
server, tool_names = build_fork_mcp_server(self._fork_context)
|
|
223
|
+
opts = dict(loaded.options or {})
|
|
224
|
+
mcp = dict(opts.get("mcp_servers") or {})
|
|
225
|
+
mcp["fork"] = server
|
|
226
|
+
opts["mcp_servers"] = mcp
|
|
227
|
+
# Only extend an EXISTING allowlist — introducing one where there was
|
|
228
|
+
# none would restrict the agent to just the fork tool. When no
|
|
229
|
+
# allowlist is set, all tools (incl. the fork MCP tool) are available.
|
|
230
|
+
if opts.get("allowed_tools"):
|
|
231
|
+
opts["allowed_tools"] = list(opts["allowed_tools"]) + list(tool_names)
|
|
232
|
+
if self._fork_mode == "auto":
|
|
233
|
+
# Disable native in-process sub-agents; steer delegation to fork.
|
|
234
|
+
opts["agents"] = {}
|
|
235
|
+
dis = list(opts.get("disallowed_tools") or [])
|
|
236
|
+
if "Task" not in dis:
|
|
237
|
+
dis.append("Task")
|
|
238
|
+
opts["disallowed_tools"] = dis
|
|
239
|
+
opts["system_prompt"] = append_steer(opts.get("system_prompt"))
|
|
240
|
+
loaded = replace(loaded, options=opts)
|
|
177
241
|
self._manifest = loaded
|
|
178
242
|
|
|
179
243
|
async def dispose(self) -> None:
|
|
@@ -186,6 +250,12 @@ class HarnessCoordinator:
|
|
|
186
250
|
if self._lifecycle_started and self._lifecycle_session_id:
|
|
187
251
|
with suppress(Exception):
|
|
188
252
|
await self._emit_lifecycle_session_ended()
|
|
253
|
+
# Tear down any still-running forked children first — the SDK's atexit
|
|
254
|
+
# net kills orphaned CLI processes but NOT the children's temp
|
|
255
|
+
# workspaces, so an explicit dispose here prevents leaks.
|
|
256
|
+
if self._fork_context is not None:
|
|
257
|
+
with suppress(Exception):
|
|
258
|
+
await self._fork_context.teardown()
|
|
189
259
|
if self._substrate_handle is not None:
|
|
190
260
|
with suppress(Exception):
|
|
191
261
|
await self._substrate_handle.close()
|
|
@@ -521,6 +591,12 @@ class HarnessCoordinator:
|
|
|
521
591
|
hands off to the pipeline. ``TelemetryEvent`` is mutable by design."""
|
|
522
592
|
if self._agent_id is not None:
|
|
523
593
|
ev.agent_id = self._agent_id
|
|
594
|
+
# Fork linkage — stamp the parent ids so a child's events nest under
|
|
595
|
+
# the parent (None for a top-level agent → field stays None).
|
|
596
|
+
if self._parent_session_id is not None:
|
|
597
|
+
ev.parent_session_id = self._parent_session_id
|
|
598
|
+
if self._parent_agent_id is not None:
|
|
599
|
+
ev.parent_agent_id = self._parent_agent_id
|
|
524
600
|
await self._pipeline.emit(ev)
|
|
525
601
|
|
|
526
602
|
def _build_permission_resolver(
|