primust 1.0.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.
- primust-1.0.0/.gitignore +22 -0
- primust-1.0.0/PKG-INFO +23 -0
- primust-1.0.0/README.md +183 -0
- primust-1.0.0/pyproject.toml +45 -0
- primust-1.0.0/src/primust/__init__.py +74 -0
- primust-1.0.0/src/primust/adapters/__init__.py +1 -0
- primust-1.0.0/src/primust/adapters/crewai.py +147 -0
- primust-1.0.0/src/primust/adapters/pydantic_ai.py +236 -0
- primust-1.0.0/src/primust/cli.py +82 -0
- primust-1.0.0/src/primust/discovery/__init__.py +4 -0
- primust-1.0.0/src/primust/discovery/analyzer.py +242 -0
- primust-1.0.0/src/primust/discovery/patterns.py +178 -0
- primust-1.0.0/src/primust/models.py +156 -0
- primust-1.0.0/src/primust/pipeline.py +526 -0
- primust-1.0.0/src/primust/queue.py +133 -0
- primust-1.0.0/src/primust/run.py +447 -0
- primust-1.0.0/src/primust/transport.py +176 -0
- primust-1.0.0/tests/__init__.py +0 -0
- primust-1.0.0/tests/discovery_sample/approvals/batch.py +11 -0
- primust-1.0.0/tests/discovery_sample/llm/generate.py +19 -0
- primust-1.0.0/tests/discovery_sample/models/classifier.py +6 -0
- primust-1.0.0/tests/discovery_sample/tools/langgraph_agent.py +9 -0
- primust-1.0.0/tests/discovery_sample/tools/search.py +9 -0
- primust-1.0.0/tests/discovery_sample/validators/pii.py +8 -0
- primust-1.0.0/tests/test_crewai_adapter.py +224 -0
- primust-1.0.0/tests/test_discovery.py +116 -0
- primust-1.0.0/tests/test_logger.py +189 -0
- primust-1.0.0/tests/test_pydantic_ai_adapter.py +269 -0
- primust-1.0.0/tests/test_run_api.py +366 -0
- primust-1.0.0/tests/test_sdk.py +296 -0
primust-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.next/
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.venv/
|
|
7
|
+
.env
|
|
8
|
+
.env.local
|
|
9
|
+
.env.*.local
|
|
10
|
+
*.pyc
|
|
11
|
+
.turbo/
|
|
12
|
+
*.pem
|
|
13
|
+
*.key
|
|
14
|
+
.DS_Store
|
|
15
|
+
.claude/
|
|
16
|
+
.claude.json
|
|
17
|
+
.claude.json.backup
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
*.db
|
|
20
|
+
*.db-shm
|
|
21
|
+
*.db-wal
|
|
22
|
+
.vercel
|
primust-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: primust
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Prove your governance checks ran. Portable, offline-verifiable cryptographic credentials.
|
|
5
|
+
Project-URL: Homepage, https://primust.com
|
|
6
|
+
Project-URL: Documentation, https://docs.primust.com
|
|
7
|
+
Project-URL: Verify, https://verify.primust.com
|
|
8
|
+
Author-email: "Primust, Inc." <eng@primust.com>
|
|
9
|
+
License-Expression: LicenseRef-Proprietary
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Security :: Cryptography
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: cryptography>=41.0
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Requires-Dist: primust-artifact-core>=1.0.0
|
|
21
|
+
Requires-Dist: pydantic>=2.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
primust-1.0.0/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Primust Python SDK
|
|
2
|
+
|
|
3
|
+
Prove governance ran. Disclose nothing.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install primust
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What this is
|
|
10
|
+
|
|
11
|
+
Primust issues **Verifiable Process Execution Credentials (VPECs)** — portable, offline-verifiable proofs that a defined governance process executed correctly on specific data, without disclosing the data.
|
|
12
|
+
|
|
13
|
+
A VPEC answers: *"Did your AML screening actually run on this entity?"* — without your watchlist matching criteria, velocity thresholds, or customer data leaving your environment.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import primust
|
|
19
|
+
|
|
20
|
+
p = primust.Pipeline(
|
|
21
|
+
api_key="pk_live_...",
|
|
22
|
+
workflow_id="aml-screening-v2"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
run = p.open()
|
|
26
|
+
|
|
27
|
+
result = run.record(
|
|
28
|
+
check="aml_entity_screen",
|
|
29
|
+
manifest_id="sha256:abc123...", # from p.register_check()
|
|
30
|
+
input=entity_data, # committed locally — never sent to Primust
|
|
31
|
+
check_result="pass",
|
|
32
|
+
visibility="opaque",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Write result.commitment_hash to your own logs
|
|
36
|
+
# This is your log linkage anchor — connects your logs to the VPEC
|
|
37
|
+
print(result.commitment_hash) # sha256:...
|
|
38
|
+
|
|
39
|
+
vpec = run.close()
|
|
40
|
+
# vpec is the signed, portable credential
|
|
41
|
+
# Provide to your regulator. They verify at verify.primust.com
|
|
42
|
+
# without receiving your data.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Privacy guarantee
|
|
46
|
+
|
|
47
|
+
Raw input values are committed locally via SHA-256 (Poseidon2 when the native extension is available) before any network call. Only the commitment hash and bounded normalized metadata transit to `api.primust.com`.
|
|
48
|
+
|
|
49
|
+
**Your data never leaves your environment.**
|
|
50
|
+
|
|
51
|
+
This is enforced in the SDK — not advisory. The transport layer never receives raw values. Tests verify this by intercepting every outbound HTTP request and asserting sensitive input strings are absent.
|
|
52
|
+
|
|
53
|
+
## Proof levels
|
|
54
|
+
|
|
55
|
+
| Level | When | Verifier confidence |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `mathematical` | Deterministic rule, arithmetic verifiable | Cryptographic — can replay |
|
|
58
|
+
| `execution` | In-process instrumentation | Strong — execution binding |
|
|
59
|
+
| `witnessed` | Human review with RFC 3161 timing | Regulatory — signed review |
|
|
60
|
+
| `attestation` | API-level observation | Audit — process ran |
|
|
61
|
+
|
|
62
|
+
The VPEC applies weakest-link: the overall credential level is the lowest level across all checks in the run.
|
|
63
|
+
|
|
64
|
+
## API reference
|
|
65
|
+
|
|
66
|
+
### `Pipeline(api_key, workflow_id, ...)`
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
p = primust.Pipeline(
|
|
70
|
+
api_key="pk_live_...", # or set PRIMUST_API_KEY env var
|
|
71
|
+
workflow_id="my-workflow", # identifies the governed process
|
|
72
|
+
surface_id=None, # optional: instrumentation surface
|
|
73
|
+
environment="production", # inferred from key prefix
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `p.open(policy_pack_id?) → Run`
|
|
78
|
+
|
|
79
|
+
Opens a governed process run. Returns a `Run`. All `.record()` calls belong to this run. Close with `.close()` to issue the VPEC.
|
|
80
|
+
|
|
81
|
+
### `run.record(check, manifest_id, check_result, input, ...) → RecordResult`
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
result = run.record(
|
|
85
|
+
check="pii_scan",
|
|
86
|
+
manifest_id="sha256:...",
|
|
87
|
+
check_result="pass", # pass | fail | error | skipped | degraded | override
|
|
88
|
+
input=content, # committed locally — never sent
|
|
89
|
+
details={"score": 0.04}, # bounded metadata — will transit, must not be sensitive
|
|
90
|
+
output=None, # optional output commitment
|
|
91
|
+
visibility="opaque", # transparent | selective | opaque
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
result.commitment_hash # sha256:... — write this to your logs
|
|
95
|
+
result.record_id # rec_...
|
|
96
|
+
result.proof_level # attestation | execution | witnessed | mathematical
|
|
97
|
+
result.queued # True if API was unreachable — will flush on reconnect
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `run.open_check(check, manifest_id) → CheckSession`
|
|
101
|
+
|
|
102
|
+
Opens a timed check session. Returns an RFC 3161 timestamp at open time. Pass to `run.record(check_session=...)`. Sub-100ms ML inference emits `check_timing_suspect` gap.
|
|
103
|
+
|
|
104
|
+
### `run.open_review(check, manifest_id, reviewer_key_id, ...) → ReviewSession`
|
|
105
|
+
|
|
106
|
+
Opens a Witnessed level human review session. Pass to `run.record(check_session=..., reviewer_signature=..., rationale=...)`.
|
|
107
|
+
|
|
108
|
+
### `run.close() → VPEC`
|
|
109
|
+
|
|
110
|
+
Closes the run and issues the VPEC. After close, no further records can be added.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
vpec = run.close()
|
|
114
|
+
|
|
115
|
+
vpec.vpec_id # vpec_...
|
|
116
|
+
vpec.proof_level # weakest-link across all checks
|
|
117
|
+
vpec.chain_intact # True if commitment chain is unbroken
|
|
118
|
+
vpec.governance_gaps # list of GovernanceGap — missing checks, timing anomalies
|
|
119
|
+
vpec.is_clean() # True if chain intact and zero gaps
|
|
120
|
+
vpec.to_dict() # full JSON for offline verification
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `p.register_check(manifest) → ManifestRegistration`
|
|
124
|
+
|
|
125
|
+
Register a check manifest. Call once per manifest version. Returns `manifest_id` — content-addressed SHA-256, idempotent.
|
|
126
|
+
|
|
127
|
+
## Offline durability
|
|
128
|
+
|
|
129
|
+
If `api.primust.com` is unreachable, records queue locally in SQLite. The SDK never throws to the caller due to API unavailability. When connectivity recovers, the queue flushes automatically on the next successful call.
|
|
130
|
+
|
|
131
|
+
If the queue is permanently lost, a `system_unavailable` gap is recorded in the VPEC — the SDK never silently drops governance evidence.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
p.pending_queue_count() # items waiting in local queue
|
|
135
|
+
p.flush_queue() # manually trigger flush attempt
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Governance gaps
|
|
139
|
+
|
|
140
|
+
The VPEC records gaps automatically:
|
|
141
|
+
|
|
142
|
+
| Gap type | Meaning |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `check_missing` | Expected check in manifest not executed |
|
|
145
|
+
| `check_failed` | Check ran and returned fail |
|
|
146
|
+
| `check_timing_suspect` | Execution time implausibly fast (< 100ms for ML check) |
|
|
147
|
+
| `sequence_gap` | Record sequence broken — possible tampering |
|
|
148
|
+
| `system_unavailable` | Primust API unreachable during run |
|
|
149
|
+
| `policy_config_drift` | Policy changed between run open and close |
|
|
150
|
+
|
|
151
|
+
## Log linkage
|
|
152
|
+
|
|
153
|
+
Every `RecordResult` contains a `commitment_hash`. Write this to your operational logs alongside the transaction or decision ID it corresponds to. This creates a verifiable link between your logs and the VPEC — a verifier can confirm your log entry corresponds to a specific record in the credential.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
result = run.record(...)
|
|
157
|
+
logger.info("aml_screen completed",
|
|
158
|
+
commitment_hash=result.commitment_hash,
|
|
159
|
+
transaction_id=txn_id,
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Verify a VPEC
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pip install primust-verify
|
|
167
|
+
primust-verify vpec.json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Or online at [verify.primust.com](https://verify.primust.com) — no account required.
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
- Python 3.11+
|
|
175
|
+
- `httpx>=0.27.0`
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
Proprietary — see LICENSE file.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
[Docs](https://docs.primust.com) · [Verify](https://verify.primust.com) · [Connectors](https://github.com/primust-dev/connectors)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "primust"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Prove your governance checks ran. Portable, offline-verifiable cryptographic credentials."
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "LicenseRef-Proprietary"
|
|
11
|
+
authors = [{ name = "Primust, Inc.", email = "eng@primust.com" }]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 5 - Production/Stable",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Security :: Cryptography",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.27.0",
|
|
23
|
+
"pydantic>=2.0",
|
|
24
|
+
"cryptography>=41.0",
|
|
25
|
+
"primust-artifact-core>=1.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
primust = "primust.cli:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://primust.com"
|
|
38
|
+
Documentation = "https://docs.primust.com"
|
|
39
|
+
Verify = "https://verify.primust.com"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.wheel]
|
|
42
|
+
packages = ["src/primust"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Primust SDK
|
|
3
|
+
===========
|
|
4
|
+
Verifiable Process Execution Credentials for regulated workflows.
|
|
5
|
+
|
|
6
|
+
Quickstart:
|
|
7
|
+
import primust
|
|
8
|
+
|
|
9
|
+
p = primust.Pipeline(api_key="pk_live_...", workflow_id="my-workflow")
|
|
10
|
+
|
|
11
|
+
run = p.open()
|
|
12
|
+
result = run.record(
|
|
13
|
+
check="aml_screen",
|
|
14
|
+
manifest_id="sha256:abc123...",
|
|
15
|
+
input=entity_data, # committed locally — never sent to Primust
|
|
16
|
+
check_result="pass",
|
|
17
|
+
)
|
|
18
|
+
# Write result.commitment_hash to your own logs (log linkage anchor)
|
|
19
|
+
vpec = run.close()
|
|
20
|
+
|
|
21
|
+
Privacy guarantee:
|
|
22
|
+
Raw input values are committed locally via Poseidon2 (or SHA-256 when
|
|
23
|
+
the native extension is unavailable). Only commitment hashes and bounded
|
|
24
|
+
normalized metadata transit to api.primust.com. Your data never leaves
|
|
25
|
+
your environment.
|
|
26
|
+
|
|
27
|
+
Docs: https://docs.primust.com
|
|
28
|
+
Verify: https://verify.primust.com
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from primust.pipeline import (
|
|
32
|
+
Pipeline,
|
|
33
|
+
CheckSession,
|
|
34
|
+
ReviewSession,
|
|
35
|
+
ResumedContext,
|
|
36
|
+
ZK_IS_BLOCKING,
|
|
37
|
+
)
|
|
38
|
+
from primust.models import (
|
|
39
|
+
CheckResult,
|
|
40
|
+
GovernanceGap,
|
|
41
|
+
LoggerOptions,
|
|
42
|
+
ManifestRegistration,
|
|
43
|
+
PrimustLogEvent,
|
|
44
|
+
ProofLevel,
|
|
45
|
+
ProofLevelBreakdown,
|
|
46
|
+
RecordResult,
|
|
47
|
+
VPEC,
|
|
48
|
+
VisibilityMode,
|
|
49
|
+
)
|
|
50
|
+
from primust.models import (
|
|
51
|
+
CheckSession as ModelCheckSession,
|
|
52
|
+
ReviewSession as ModelReviewSession,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__version__ = "1.0.0"
|
|
56
|
+
__all__ = [
|
|
57
|
+
"Pipeline",
|
|
58
|
+
"CheckResult",
|
|
59
|
+
"CheckSession",
|
|
60
|
+
"GovernanceGap",
|
|
61
|
+
"LoggerOptions",
|
|
62
|
+
"ManifestRegistration",
|
|
63
|
+
"ModelCheckSession",
|
|
64
|
+
"ModelReviewSession",
|
|
65
|
+
"PrimustLogEvent",
|
|
66
|
+
"ProofLevel",
|
|
67
|
+
"ProofLevelBreakdown",
|
|
68
|
+
"RecordResult",
|
|
69
|
+
"ResumedContext",
|
|
70
|
+
"ReviewSession",
|
|
71
|
+
"VPEC",
|
|
72
|
+
"VisibilityMode",
|
|
73
|
+
"ZK_IS_BLOCKING",
|
|
74
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Primust framework adapters."""
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Primust CrewAI adapter — step_callback observer.
|
|
3
|
+
|
|
4
|
+
Privacy invariant: raw agent output NEVER leaves the customer environment.
|
|
5
|
+
Only commitment hashes (poseidon2) transit to the Primust API.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from primust.adapters.crewai import PrimustCrewAICallback
|
|
9
|
+
|
|
10
|
+
callback = PrimustCrewAICallback(
|
|
11
|
+
pipeline=p,
|
|
12
|
+
manifest_map={
|
|
13
|
+
"Research Analyst": "manifest_research_v1",
|
|
14
|
+
"Content Writer": "manifest_output_policy_v2",
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
crew = Crew(
|
|
19
|
+
agents=[research_analyst, content_writer],
|
|
20
|
+
tasks=[research_task, write_task],
|
|
21
|
+
step_callback=callback.on_step
|
|
22
|
+
)
|
|
23
|
+
result = crew.kickoff()
|
|
24
|
+
vpec = p.close()
|
|
25
|
+
|
|
26
|
+
Surface declaration:
|
|
27
|
+
surface_type: in_process_adapter
|
|
28
|
+
surface_name: crewai_step_callback
|
|
29
|
+
observation_mode: post_action_realtime
|
|
30
|
+
scope_type: full_workflow
|
|
31
|
+
proof_ceiling: execution
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from primust import Pipeline
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger("primust.crewai")
|
|
42
|
+
|
|
43
|
+
SURFACE_DECLARATION = {
|
|
44
|
+
"surface_type": "in_process_adapter",
|
|
45
|
+
"surface_name": "crewai_step_callback",
|
|
46
|
+
"observation_mode": "post_action_realtime",
|
|
47
|
+
"scope_type": "full_workflow",
|
|
48
|
+
"proof_ceiling": "execution",
|
|
49
|
+
"surface_coverage_statement": (
|
|
50
|
+
"All CrewAI agent steps observed via step_callback. "
|
|
51
|
+
"Actions taken by agents outside the CrewAI step lifecycle are not observed."
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PrimustCrewAICallback:
|
|
57
|
+
"""
|
|
58
|
+
CrewAI step_callback observer. Attaches as crew.step_callback.
|
|
59
|
+
|
|
60
|
+
Callback NEVER raises into CrewAI — all exceptions caught, logged, continue.
|
|
61
|
+
Raw agent output committed locally — only commitment_hash transits.
|
|
62
|
+
CrewAI step_callback has no return value — Primust is purely observational.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
pipeline: Pipeline,
|
|
68
|
+
manifest_map: dict[str, str] | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
self.pipeline = pipeline
|
|
71
|
+
self.manifest_map = manifest_map or {}
|
|
72
|
+
|
|
73
|
+
def on_step(self, agent_output: Any) -> None:
|
|
74
|
+
"""
|
|
75
|
+
CrewAI step_callback handler.
|
|
76
|
+
|
|
77
|
+
agent_output can be:
|
|
78
|
+
- AgentAction: tool call in progress
|
|
79
|
+
- AgentFinish: agent output ready
|
|
80
|
+
- dict or other: fallback handling
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
self._handle_step(agent_output)
|
|
84
|
+
except Exception:
|
|
85
|
+
# CRITICAL: never raise into CrewAI
|
|
86
|
+
logger.exception("Primust callback error (swallowed)")
|
|
87
|
+
|
|
88
|
+
def _handle_step(self, agent_output: Any) -> None:
|
|
89
|
+
output_type = type(agent_output).__name__
|
|
90
|
+
|
|
91
|
+
# Extract agent role for manifest lookup
|
|
92
|
+
agent_role = getattr(agent_output, "agent", None)
|
|
93
|
+
if agent_role and hasattr(agent_role, "role"):
|
|
94
|
+
agent_role = agent_role.role
|
|
95
|
+
elif isinstance(agent_output, dict):
|
|
96
|
+
agent_role = agent_output.get("agent_role", "unknown")
|
|
97
|
+
else:
|
|
98
|
+
agent_role = getattr(agent_output, "role", "unknown")
|
|
99
|
+
|
|
100
|
+
manifest_id = self.manifest_map.get(str(agent_role), f"auto:{agent_role}")
|
|
101
|
+
|
|
102
|
+
# Determine if this is an action (tool call) or finish (output)
|
|
103
|
+
is_action = output_type == "AgentAction" or (
|
|
104
|
+
isinstance(agent_output, dict) and agent_output.get("type") == "action"
|
|
105
|
+
)
|
|
106
|
+
is_finish = output_type == "AgentFinish" or (
|
|
107
|
+
isinstance(agent_output, dict) and agent_output.get("type") == "finish"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Extract input and output
|
|
111
|
+
if isinstance(agent_output, dict):
|
|
112
|
+
input_data = agent_output.get("input", agent_output.get("tool_input", str(agent_output)))
|
|
113
|
+
output_data = agent_output.get("output", agent_output.get("result"))
|
|
114
|
+
else:
|
|
115
|
+
input_data = getattr(agent_output, "tool_input", getattr(agent_output, "text", str(agent_output)))
|
|
116
|
+
output_data = getattr(agent_output, "output", getattr(agent_output, "return_values", None))
|
|
117
|
+
|
|
118
|
+
check_name = f"crewai_{output_type.lower()}"
|
|
119
|
+
if is_action:
|
|
120
|
+
tool = getattr(agent_output, "tool", None) or (
|
|
121
|
+
agent_output.get("tool") if isinstance(agent_output, dict) else None
|
|
122
|
+
)
|
|
123
|
+
if tool:
|
|
124
|
+
check_name = f"crewai_action:{tool}"
|
|
125
|
+
|
|
126
|
+
# Check if agent role is mapped
|
|
127
|
+
unverified = str(agent_role) not in self.manifest_map
|
|
128
|
+
|
|
129
|
+
session = self.pipeline.open_check(check_name, manifest_id)
|
|
130
|
+
|
|
131
|
+
record_kwargs: dict[str, Any] = {}
|
|
132
|
+
if output_data is not None:
|
|
133
|
+
record_kwargs["output"] = output_data
|
|
134
|
+
|
|
135
|
+
check_result = "pass"
|
|
136
|
+
if unverified:
|
|
137
|
+
check_result = "pass" # still pass, but marked unverified via metadata
|
|
138
|
+
|
|
139
|
+
self.pipeline.record(
|
|
140
|
+
session,
|
|
141
|
+
input=input_data,
|
|
142
|
+
check_result=check_result,
|
|
143
|
+
**record_kwargs,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def get_surface_declaration(self) -> dict[str, str]:
|
|
147
|
+
return dict(SURFACE_DECLARATION)
|