agentdynamics 0.7.0__tar.gz → 0.9.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.
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/PKG-INFO +14 -6
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/README.md +13 -5
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/__init__.py +1 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/__main__.py +87 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/alerts.py +50 -3
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/analysis.py +473 -76
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/api/assess.py +28 -4
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/api/base.py +33 -3
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/api/diagnose.py +52 -1
- agentdynamics-0.9.0/agentdynamics/api/governance.py +327 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/api/monitor.py +7 -2
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/api/ops.py +2 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/autotrace.py +45 -7
- agentdynamics-0.9.0/agentdynamics/calibrate.py +173 -0
- agentdynamics-0.9.0/agentdynamics/checker.py +197 -0
- agentdynamics-0.9.0/agentdynamics/collectors/billing.py +76 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/claude_code.py +6 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/generic.py +5 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/langfuse.py +2 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/langsmith.py +1 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/otlp.py +1 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/spans.py +26 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/config.py +57 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/engine.py +415 -5
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/govern.py +15 -0
- agentdynamics-0.9.0/agentdynamics/incidents.py +123 -0
- agentdynamics-0.9.0/agentdynamics/integrations/aegis.py +766 -0
- agentdynamics-0.9.0/agentdynamics/migrate.py +169 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/pg.py +31 -4
- agentdynamics-0.9.0/agentdynamics/pricing.py +111 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/privacy.py +11 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/server.py +101 -7
- agentdynamics-0.9.0/agentdynamics/sketch.py +73 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/slo.py +6 -2
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/store.py +314 -10
- agentdynamics-0.9.0/agentdynamics/tripwires.py +96 -0
- agentdynamics-0.9.0/agentdynamics/trust.py +147 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/app.js +8 -5
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/charts.js +2 -2
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/assess.js +21 -5
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/diagnose.js +17 -4
- agentdynamics-0.9.0/agentdynamics/web/pages/governance.js +330 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/monitor.js +25 -10
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/PKG-INFO +14 -6
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/SOURCES.txt +21 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/pyproject.toml +1 -1
- agentdynamics-0.9.0/tests/test_aegis_integration.py +843 -0
- agentdynamics-0.9.0/tests/test_allocation.py +84 -0
- agentdynamics-0.9.0/tests/test_apdex.py +169 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_autotrace.py +21 -1
- agentdynamics-0.9.0/tests/test_billing.py +166 -0
- agentdynamics-0.9.0/tests/test_calibrate.py +170 -0
- agentdynamics-0.9.0/tests/test_checker.py +196 -0
- agentdynamics-0.9.0/tests/test_console_interaction.py +422 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_core.py +117 -0
- agentdynamics-0.9.0/tests/test_ground_truth.py +266 -0
- agentdynamics-0.9.0/tests/test_incidents.py +350 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_incremental.py +141 -3
- agentdynamics-0.9.0/tests/test_live_anthropic.py +123 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_policy_coverage.py +66 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_postgres.py +297 -42
- agentdynamics-0.9.0/tests/test_revocations.py +253 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_rollups.py +52 -1
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_scoped_keys.py +14 -1
- agentdynamics-0.9.0/tests/test_sketch.py +49 -0
- agentdynamics-0.9.0/tests/test_tripwires.py +211 -0
- agentdynamics-0.9.0/tests/test_trust.py +222 -0
- agentdynamics-0.7.0/agentdynamics/api/governance.py +0 -160
- agentdynamics-0.7.0/agentdynamics/integrations/aegis.py +0 -365
- agentdynamics-0.7.0/agentdynamics/pricing.py +0 -69
- agentdynamics-0.7.0/agentdynamics/web/pages/governance.js +0 -87
- agentdynamics-0.7.0/tests/test_aegis_integration.py +0 -335
- agentdynamics-0.7.0/tests/test_live_anthropic.py +0 -66
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/LICENSE +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/api/__init__.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/bootstrap/sitecustomize.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/__init__.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/aegis_audit.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/collectors/inbox.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/flows.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/integrations/__init__.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/phases.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/sdk.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/index.html +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/configure.js +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics/web/style.css +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/dependency_links.txt +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/entry_points.txt +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/requires.txt +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/top_level.txt +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/setup.cfg +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_alerts.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_console_ui.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_ecosystem.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_integrations.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_outcomes.py +0 -0
- {agentdynamics-0.7.0 → agentdynamics-0.9.0}/tests/test_token_accounting.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentdynamics
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Application performance monitoring for AI agents: flows, cost, reliability and process quality
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/Aditya31398/agentdynamics
|
|
@@ -70,7 +70,9 @@ def handle_ticket(question):
|
|
|
70
70
|
|
|
71
71
|
Without `outcome()`, success is inferred from errors, interrupts and follow-ups. The console always shows
|
|
72
72
|
which outcomes were stated and which were guessed; grades can also be posted afterwards by a person or an
|
|
73
|
-
eval pipeline (`POST /api/outcomes`)
|
|
73
|
+
eval pipeline (`POST /api/outcomes`), or by your own systems using your own ids: trace with
|
|
74
|
+
`agentdynamics.trace("support", ticket_id="T-123")` and post `{"key": {"ticket_id": "T-123"}, "outcome": "rework"}`
|
|
75
|
+
when the ticket is reopened.
|
|
74
76
|
|
|
75
77
|
### 2. Python, zero code changes
|
|
76
78
|
|
|
@@ -148,10 +150,11 @@ AGENTDYNAMICS_URL=http://127.0.0.1:8790 python examples/governed_agent.py 40
|
|
|
148
150
|
| **Workflows** | The real execution graph mined from your traces: path variants and their success rates, loops, the critical node, handoffs |
|
|
149
151
|
| **Task snapshots** | Span-tree waterfall for each request: context growth, cost against the baseline, what went wrong, what the user said next |
|
|
150
152
|
| **Baselines & Apdex** | "Normal" cost and latency per workflow, learned automatically, plus an agent Apdex score |
|
|
151
|
-
| **Health rules & alerts** |
|
|
152
|
-
| **Tools & Models** | Error rate and p95 per tool; per model TTFT, tokens/s, truncation rate, cache hit rate and spend |
|
|
153
|
-
| **SLOs** | Success rate, Apdex, latency and cost objectives with error budgets and burn rates |
|
|
153
|
+
| **Health rules & alerts** | 29 rules (runaway cost, policy denials, boundary probing, tripwires, revocations, retry loops, node loops, truncation, rate limits, ping-pong handoffs, unverified code changes, …) sent to Slack, PagerDuty or webhooks, routed by project, rule and severity |
|
|
154
|
+
| **Tools & Models** | Error rate and p95 per tool; per model TTFT, tokens/s, truncation rate, cache hit rate and spend, and the estimate beside what Anthropic billed (with an Admin API key) |
|
|
155
|
+
| **SLOs** | Success rate, Apdex, good-task rate, latency and cost objectives with error budgets and burn rates |
|
|
154
156
|
| **Governance** | Aegis policy enforcement per task: denials by rule, budget stops, kill-switch revocations, unused grants, policy export, and what a policy change would refuse |
|
|
157
|
+
| **Incidents & trust** | Tripwires, probing, refused calls and revocations about one agent, grouped into one incident with its evidence; revoke the agent or resolve it as real or a false alarm, and alert once per incident. Each agent's trust score (0-100) weighs that evidence by age and by the verdicts given, and is exported to Prometheus. An optional checker has a model review each incident in shadow mode, and keeps score of how often it agreed with you |
|
|
155
158
|
| **Process Review** | Scores how well the agent works (efficiency, focus, reliability, verification, context, autonomy, compliance) and suggests fixes |
|
|
156
159
|
| **Analytics & Compare** | Any metric by any dimension; compare models, prompts or releases side by side |
|
|
157
160
|
|
|
@@ -169,7 +172,10 @@ governance.instrument(kernel, root, watchdog=governance.Watchdog(max_repeated_de
|
|
|
169
172
|
That call sets up four flows:
|
|
170
173
|
1. **Every Aegis decision appears in its task.** Denials carry the rule id, and the Aegis audit log carries the task's run id so the two logs join.
|
|
171
174
|
2. **Model calls are charged to the Aegis budget before they're sent.** When the budget runs out, the call is refused and never made.
|
|
172
|
-
3. **A watchdog revokes the grant** of an agent that keeps probing a boundary, for example after a prompt injection
|
|
175
|
+
3. **A watchdog revokes the grant** of an agent that keeps probing a boundary, for example after a prompt injection,
|
|
176
|
+
and **tripwires** (decoy tools and planted canary values) stop a run before the call that touches one is made.
|
|
177
|
+
For tools that can't be undone, an opt-in **reviewer** model checks each call against what the user asked
|
|
178
|
+
before Aegis admits it, and refuses what goes beyond it (`review={"tools": ["payments.refund"]}`).
|
|
173
179
|
4. **`agentdynamics policy export` writes a tighter, least-privilege policy** from observed behaviour. `aegis ratify` and `aegis drift` verify its declaration; `agentdynamics policy check` replays the traffic that actually ran through it and reports how much it would refuse, so a policy change can be gated in CI.
|
|
174
180
|
|
|
175
181
|
The console's **Governance** page shows denials, budget stops, revocations, unused grants and budget headroom per policy version. See [docs/GOVERNANCE.md](docs/GOVERNANCE.md).
|
|
@@ -213,6 +219,8 @@ The store is a SQLite file by default. For several instances behind a load balan
|
|
|
213
219
|
and operate like any other, use Postgres: `pip install "agentdynamics[postgres]"` and set
|
|
214
220
|
`AGENTDYNAMICS_DB_URL=postgresql://...` (or `deploy/docker-compose.postgres.yml`). One instance is elected to
|
|
215
221
|
write the analysis; the others serve the console and take ingest, and one takes over if the writer goes away.
|
|
222
|
+
An existing install moves with its history: `agentdynamics store copy --to postgresql://...`, then
|
|
223
|
+
`agentdynamics store verify`.
|
|
216
224
|
|
|
217
225
|
- **Security:** role-based API keys, optionally scoped to projects. A scoped key reads only its projects' tasks,
|
|
218
226
|
runs, steps and events, on every endpoint, and can't write into or over another project's traces.
|
|
@@ -45,7 +45,9 @@ def handle_ticket(question):
|
|
|
45
45
|
|
|
46
46
|
Without `outcome()`, success is inferred from errors, interrupts and follow-ups. The console always shows
|
|
47
47
|
which outcomes were stated and which were guessed; grades can also be posted afterwards by a person or an
|
|
48
|
-
eval pipeline (`POST /api/outcomes`)
|
|
48
|
+
eval pipeline (`POST /api/outcomes`), or by your own systems using your own ids: trace with
|
|
49
|
+
`agentdynamics.trace("support", ticket_id="T-123")` and post `{"key": {"ticket_id": "T-123"}, "outcome": "rework"}`
|
|
50
|
+
when the ticket is reopened.
|
|
49
51
|
|
|
50
52
|
### 2. Python, zero code changes
|
|
51
53
|
|
|
@@ -123,10 +125,11 @@ AGENTDYNAMICS_URL=http://127.0.0.1:8790 python examples/governed_agent.py 40
|
|
|
123
125
|
| **Workflows** | The real execution graph mined from your traces: path variants and their success rates, loops, the critical node, handoffs |
|
|
124
126
|
| **Task snapshots** | Span-tree waterfall for each request: context growth, cost against the baseline, what went wrong, what the user said next |
|
|
125
127
|
| **Baselines & Apdex** | "Normal" cost and latency per workflow, learned automatically, plus an agent Apdex score |
|
|
126
|
-
| **Health rules & alerts** |
|
|
127
|
-
| **Tools & Models** | Error rate and p95 per tool; per model TTFT, tokens/s, truncation rate, cache hit rate and spend |
|
|
128
|
-
| **SLOs** | Success rate, Apdex, latency and cost objectives with error budgets and burn rates |
|
|
128
|
+
| **Health rules & alerts** | 29 rules (runaway cost, policy denials, boundary probing, tripwires, revocations, retry loops, node loops, truncation, rate limits, ping-pong handoffs, unverified code changes, …) sent to Slack, PagerDuty or webhooks, routed by project, rule and severity |
|
|
129
|
+
| **Tools & Models** | Error rate and p95 per tool; per model TTFT, tokens/s, truncation rate, cache hit rate and spend, and the estimate beside what Anthropic billed (with an Admin API key) |
|
|
130
|
+
| **SLOs** | Success rate, Apdex, good-task rate, latency and cost objectives with error budgets and burn rates |
|
|
129
131
|
| **Governance** | Aegis policy enforcement per task: denials by rule, budget stops, kill-switch revocations, unused grants, policy export, and what a policy change would refuse |
|
|
132
|
+
| **Incidents & trust** | Tripwires, probing, refused calls and revocations about one agent, grouped into one incident with its evidence; revoke the agent or resolve it as real or a false alarm, and alert once per incident. Each agent's trust score (0-100) weighs that evidence by age and by the verdicts given, and is exported to Prometheus. An optional checker has a model review each incident in shadow mode, and keeps score of how often it agreed with you |
|
|
130
133
|
| **Process Review** | Scores how well the agent works (efficiency, focus, reliability, verification, context, autonomy, compliance) and suggests fixes |
|
|
131
134
|
| **Analytics & Compare** | Any metric by any dimension; compare models, prompts or releases side by side |
|
|
132
135
|
|
|
@@ -144,7 +147,10 @@ governance.instrument(kernel, root, watchdog=governance.Watchdog(max_repeated_de
|
|
|
144
147
|
That call sets up four flows:
|
|
145
148
|
1. **Every Aegis decision appears in its task.** Denials carry the rule id, and the Aegis audit log carries the task's run id so the two logs join.
|
|
146
149
|
2. **Model calls are charged to the Aegis budget before they're sent.** When the budget runs out, the call is refused and never made.
|
|
147
|
-
3. **A watchdog revokes the grant** of an agent that keeps probing a boundary, for example after a prompt injection
|
|
150
|
+
3. **A watchdog revokes the grant** of an agent that keeps probing a boundary, for example after a prompt injection,
|
|
151
|
+
and **tripwires** (decoy tools and planted canary values) stop a run before the call that touches one is made.
|
|
152
|
+
For tools that can't be undone, an opt-in **reviewer** model checks each call against what the user asked
|
|
153
|
+
before Aegis admits it, and refuses what goes beyond it (`review={"tools": ["payments.refund"]}`).
|
|
148
154
|
4. **`agentdynamics policy export` writes a tighter, least-privilege policy** from observed behaviour. `aegis ratify` and `aegis drift` verify its declaration; `agentdynamics policy check` replays the traffic that actually ran through it and reports how much it would refuse, so a policy change can be gated in CI.
|
|
149
155
|
|
|
150
156
|
The console's **Governance** page shows denials, budget stops, revocations, unused grants and budget headroom per policy version. See [docs/GOVERNANCE.md](docs/GOVERNANCE.md).
|
|
@@ -188,6 +194,8 @@ The store is a SQLite file by default. For several instances behind a load balan
|
|
|
188
194
|
and operate like any other, use Postgres: `pip install "agentdynamics[postgres]"` and set
|
|
189
195
|
`AGENTDYNAMICS_DB_URL=postgresql://...` (or `deploy/docker-compose.postgres.yml`). One instance is elected to
|
|
190
196
|
write the analysis; the others serve the console and take ingest, and one takes over if the writer goes away.
|
|
197
|
+
An existing install moves with its history: `agentdynamics store copy --to postgresql://...`, then
|
|
198
|
+
`agentdynamics store verify`.
|
|
191
199
|
|
|
192
200
|
- **Security:** role-based API keys, optionally scoped to projects. A scoped key reads only its projects' tasks,
|
|
193
201
|
runs, steps and events, on every endpoint, and can't write into or over another project's traces.
|
|
@@ -187,6 +187,74 @@ def cmd_alerts(a, data):
|
|
|
187
187
|
return 1 if failed else 0
|
|
188
188
|
|
|
189
189
|
|
|
190
|
+
def cmd_store(a, data):
|
|
191
|
+
from . import config as cfgmod, migrate
|
|
192
|
+
from .pg import redact_url, schema_name
|
|
193
|
+
cfg = cfgmod.load(data)
|
|
194
|
+
url = a.to or (cfg.get("store") or {}).get("url")
|
|
195
|
+
if not url:
|
|
196
|
+
print("say where to: --to postgresql://... (or set [store] url / AGENTDYNAMICS_DB_URL)")
|
|
197
|
+
return 2
|
|
198
|
+
schema = schema_name(a.schema or (cfg.get("store") or {}).get("schema") or "agentdynamics", data)
|
|
199
|
+
if a.action == "verify":
|
|
200
|
+
ok = True
|
|
201
|
+
for what, (lite, pg) in migrate.verify(data, url, schema).items():
|
|
202
|
+
same = lite == pg
|
|
203
|
+
ok &= same
|
|
204
|
+
print(f" {what:<14} SQLite {lite:>10} Postgres {pg:>10} {'ok' if same else 'DIFFERENT'}")
|
|
205
|
+
return 0 if ok else 1
|
|
206
|
+
print(f"copying {os.path.join(data, 'agentdynamics.db')} -> {redact_url(url)} (schema {schema})")
|
|
207
|
+
try:
|
|
208
|
+
migrate.copy(data, url, schema, force=a.force)
|
|
209
|
+
except migrate.CopyError as ex:
|
|
210
|
+
print(f"not copied: {ex}")
|
|
211
|
+
return 1
|
|
212
|
+
print("done. Point the servers at it ([store] url or AGENTDYNAMICS_DB_URL); their first refresh rebuilds the "
|
|
213
|
+
"analysis from what was copied. API keys and pricing.json are configuration: copy them yourself.")
|
|
214
|
+
return 0
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def cmd_revoke(a, data):
|
|
218
|
+
from . import config as cfgmod, store
|
|
219
|
+
con = store.connect(*store.target(cfgmod.load(data), data))
|
|
220
|
+
now = time.time()
|
|
221
|
+
if a.clear:
|
|
222
|
+
n = store.clear_revocation(con, a.clear, now)
|
|
223
|
+
print("cleared" if n else f"no active directive {a.clear}")
|
|
224
|
+
return 0 if n else 1
|
|
225
|
+
if a.list:
|
|
226
|
+
rows_ = store.revocations(con, now)
|
|
227
|
+
for d in rows_:
|
|
228
|
+
status = "cleared" if d["cleared"] else "expired" if d["expires"] <= now else "active"
|
|
229
|
+
what = "revoke" if d["kind"] == "revoke" else "restrict: " + ", ".join(
|
|
230
|
+
(d["spec"].get("tools") or []) + ([f"budget x{d['spec']['budget']:g}"] if "budget" in d["spec"] else []))
|
|
231
|
+
print(f"{d['id']} {status:<8} {d['project'] or '(all projects)':<18} {d['agent'] or '(all agents)':<18} "
|
|
232
|
+
f"until {time.strftime('%Y-%m-%d %H:%M', time.localtime(d['expires']))} [{d['source']}] {what} {d['reason']}")
|
|
233
|
+
if not rows_:
|
|
234
|
+
print("no directives")
|
|
235
|
+
return 0
|
|
236
|
+
if not a.reason:
|
|
237
|
+
print("--reason is required: it goes into the kernel's audit log")
|
|
238
|
+
return 2
|
|
239
|
+
who = f"{'agent ' + a.agent if a.agent else 'every agent'} in {a.project or 'every project'}"
|
|
240
|
+
if a.tools or a.budget is not None:
|
|
241
|
+
from .engine import restrict_spec
|
|
242
|
+
try:
|
|
243
|
+
spec = restrict_spec((a.tools or "").split(","), a.budget)
|
|
244
|
+
except ValueError as ex:
|
|
245
|
+
print(ex)
|
|
246
|
+
return 2
|
|
247
|
+
rid = store.add_revocation(con, a.project, a.agent, a.reason, "operator", now, now + a.minutes * 60,
|
|
248
|
+
kind="restrict", spec=spec)
|
|
249
|
+
print(f"{rid}: {who} loses {', '.join(spec.get('tools', [])) or 'part of its budget'} for {a.minutes:g} min, "
|
|
250
|
+
"in processes using agentdynamics.integrations.aegis.instrument(..., revocations=True)")
|
|
251
|
+
return 0
|
|
252
|
+
rid = store.add_revocation(con, a.project, a.agent, a.reason, "operator", now, now + a.minutes * 60)
|
|
253
|
+
print(f"{rid}: {who} is revoked "
|
|
254
|
+
f"for {a.minutes:g} min, in processes using agentdynamics.integrations.aegis.instrument(..., revocations=True)")
|
|
255
|
+
return 0
|
|
256
|
+
|
|
257
|
+
|
|
190
258
|
def cmd_keys(a, data):
|
|
191
259
|
from .config import keys_path, load_keys, save_keys
|
|
192
260
|
keys = load_keys(data)
|
|
@@ -373,6 +441,21 @@ def main(argv=None):
|
|
|
373
441
|
al.add_argument("action", choices=["status", "test"],
|
|
374
442
|
help="status: destinations, queue and firing SLO alerts; test: send a test alert to each")
|
|
375
443
|
al.add_argument("--to", metavar="NAME", help="test only this destination (its name, as status shows it)")
|
|
444
|
+
st = sub.add_parser("store", help="copy a SQLite store into Postgres (copy), or compare the two (verify)")
|
|
445
|
+
st.add_argument("action", choices=["copy", "verify"])
|
|
446
|
+
st.add_argument("--to", metavar="URL", help="the Postgres URL (default: [store] url / AGENTDYNAMICS_DB_URL)")
|
|
447
|
+
st.add_argument("--schema", help="the Postgres schema (default: [store] schema, else agentdynamics)")
|
|
448
|
+
st.add_argument("--force", action="store_true", help="merge into a schema that already holds another store's data")
|
|
449
|
+
rv = sub.add_parser("revoke", help="revoke an agent's Aegis grants wherever it runs (applied in-process by "
|
|
450
|
+
"agentdynamics.integrations.aegis), or list/clear directives")
|
|
451
|
+
rv.add_argument("--agent", help="the agent (grant) name; omit to revoke every grant in the project")
|
|
452
|
+
rv.add_argument("--project", help="omit for every project")
|
|
453
|
+
rv.add_argument("--reason", help="why (it goes into the kernel's audit log)")
|
|
454
|
+
rv.add_argument("--minutes", type=float, default=60, help="how long new grants of that agent are refused too")
|
|
455
|
+
rv.add_argument("--tools", help="restrict instead of revoke: take these tools away (comma-separated)")
|
|
456
|
+
rv.add_argument("--budget", type=float, help="restrict: keep only this share (0-1) of what remains of the budget")
|
|
457
|
+
rv.add_argument("--list", action="store_true", help="list directives")
|
|
458
|
+
rv.add_argument("--clear", metavar="ID", help="stop a directive (grants it revoked stay revoked)")
|
|
376
459
|
sub.add_parser("ingest", help="scan sources once and rebuild the database")
|
|
377
460
|
rp = sub.add_parser("report", help="print a text summary")
|
|
378
461
|
rp.add_argument("--project")
|
|
@@ -397,6 +480,10 @@ def main(argv=None):
|
|
|
397
480
|
return cmd_keys(a, a.data)
|
|
398
481
|
if cmd == "alerts":
|
|
399
482
|
return cmd_alerts(a, a.data)
|
|
483
|
+
if cmd == "revoke":
|
|
484
|
+
return cmd_revoke(a, a.data)
|
|
485
|
+
if cmd == "store":
|
|
486
|
+
return cmd_store(a, a.data)
|
|
400
487
|
|
|
401
488
|
from .engine import Engine
|
|
402
489
|
eng = Engine(a.data, a.claude_root or None)
|
|
@@ -10,7 +10,9 @@ Destinations are `[[alerts.webhooks]]` in agentdynamics.toml:
|
|
|
10
10
|
format = "pagerduty" # json (default) | slack | pagerduty
|
|
11
11
|
routing_key_env = "PD_ROUTING_KEY" # PagerDuty Events API v2 integration key (or routing_key = "...")
|
|
12
12
|
min_severity = "critical" # info | warning (default) | critical
|
|
13
|
-
kinds = ["events", "slos"] # default ["events"]; "slos" adds SLO burn-rate alerts
|
|
13
|
+
kinds = ["events", "slos"] # default ["events"]; "slos" adds SLO burn-rate alerts,
|
|
14
|
+
# "revocations" the directives that stop an agent (server-side #8),
|
|
15
|
+
# "incidents" one alert per security incident (incidents.py)
|
|
14
16
|
projects = ["checkout"] # optional: only these projects
|
|
15
17
|
rules = ["run_failed"] # optional: only these health rules
|
|
16
18
|
|
|
@@ -18,6 +20,9 @@ What is sent:
|
|
|
18
20
|
- A health-rule event is a point in time. Slack and JSON get each one (JSON in the original
|
|
19
21
|
`{"source": "agentdynamics", "events": [...]}` shape, a contract). PagerDuty gets one alert per rule,
|
|
20
22
|
project and task type (the dedup key), so a burst of runaway-cost tasks is one incident, not fifty.
|
|
23
|
+
- An incident is several security signals about one agent: a trigger when it opens, another when it
|
|
24
|
+
escalates (warning to critical), a resolve when a person gives it a verdict. A destination that takes
|
|
25
|
+
incidents instead of events hears once about an agent tripping five health rules, not five times.
|
|
21
26
|
- An SLO alert has a start and an end: a trigger when a burn-rate policy starts firing and a resolve when
|
|
22
27
|
it stops (slo.alert_conditions). What is firing is kept in the durable `alert_state` table, so a restart
|
|
23
28
|
neither repeats a trigger nor forgets to resolve.
|
|
@@ -37,7 +42,7 @@ from collections import OrderedDict
|
|
|
37
42
|
from urllib.parse import quote
|
|
38
43
|
|
|
39
44
|
FORMATS = ("json", "slack", "pagerduty")
|
|
40
|
-
KINDS = ("events", "slos")
|
|
45
|
+
KINDS = ("events", "slos", "revocations", "incidents")
|
|
41
46
|
SEV = {"info": 1, "warning": 2, "critical": 3}
|
|
42
47
|
# https://raw.githubusercontent.com/PagerDuty/api-schema/main/reference/events-v2/openapiv3.json
|
|
43
48
|
PAGERDUTY_URL = "https://events.pagerduty.com/v2/enqueue"
|
|
@@ -135,6 +140,23 @@ def from_slo(key, c, action, now):
|
|
|
135
140
|
"project": (c.get("scope") or {}).get("project") or None, "slo": c["slo"], "ts": now, "condition": c}
|
|
136
141
|
|
|
137
142
|
|
|
143
|
+
def from_revocation(d):
|
|
144
|
+
who = f"agent {d['agent']}" if d.get("agent") else "every agent"
|
|
145
|
+
where = f" in project {d['project']}" if d.get("project") else ""
|
|
146
|
+
until = time.strftime("%Y-%m-%d %H:%M", time.localtime(d["expires"]))
|
|
147
|
+
return {"kind": "revocation", "action": "trigger", "key": f"revocation/{d['id']}", "severity": "critical",
|
|
148
|
+
"summary": f"Revoked {who}{where} until {until}: {d['reason']}", "project": d.get("project"),
|
|
149
|
+
"ts": d["created"], "revocation": d}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def from_incident(inc, summary, action="trigger"):
|
|
153
|
+
"""`action`: trigger (opened), escalate (now more severe) or resolve (a person gave a verdict)."""
|
|
154
|
+
return {"kind": "incident", "action": action, "key": f"incident/{inc['id']}", "severity": inc["severity"],
|
|
155
|
+
"summary": summary, "project": inc["project"], "ts": inc["resolved_at"] if action == "resolve" else inc["updated"],
|
|
156
|
+
"incident": {k: inc[k] for k in ("id", "project", "agent", "workflow", "opened", "updated", "status",
|
|
157
|
+
"severity", "signals", "verdict", "note")}}
|
|
158
|
+
|
|
159
|
+
|
|
138
160
|
# ---------------------------------------------------------------- formats
|
|
139
161
|
|
|
140
162
|
def _dedup_key(key):
|
|
@@ -148,6 +170,10 @@ def link(console_url, alert):
|
|
|
148
170
|
base = console_url.rstrip("/")
|
|
149
171
|
if alert["kind"] == "event":
|
|
150
172
|
return f"{base}/#/task/{quote(alert['event']['task_id'], safe='')}"
|
|
173
|
+
if alert["kind"] == "revocation":
|
|
174
|
+
return f"{base}/#/governance"
|
|
175
|
+
if alert["kind"] == "incident":
|
|
176
|
+
return f"{base}/#/incident/{quote(alert['incident']['id'], safe='')}"
|
|
151
177
|
return f"{base}/#/slos"
|
|
152
178
|
|
|
153
179
|
|
|
@@ -172,6 +198,13 @@ def render(dest, alerts, console_url=""):
|
|
|
172
198
|
"link": link(console_url, a), **a["condition"]} for a in alerts if a["kind"] == "slo"]
|
|
173
199
|
if slos:
|
|
174
200
|
bodies.append({"source": "agentdynamics", "slo_alerts": slos})
|
|
201
|
+
revs = [dict(a["revocation"], summary=a["summary"]) for a in alerts if a["kind"] == "revocation"]
|
|
202
|
+
if revs:
|
|
203
|
+
bodies.append({"source": "agentdynamics", "revocations": revs})
|
|
204
|
+
incs = [dict(a["incident"], action=a["action"], summary=a["summary"], link=link(console_url, a))
|
|
205
|
+
for a in alerts if a["kind"] == "incident"]
|
|
206
|
+
if incs:
|
|
207
|
+
bodies.append({"source": "agentdynamics", "incidents": incs})
|
|
175
208
|
return bodies
|
|
176
209
|
# one line (Slack) or one alert (PagerDuty) per dedup key, so a burst of the same violation reads as one
|
|
177
210
|
groups = OrderedDict()
|
|
@@ -185,6 +218,14 @@ def render(dest, alerts, console_url=""):
|
|
|
185
218
|
where = " / ".join(_slack_escape(x) for x in (a.get("project"), a.get("task_type")) if x)
|
|
186
219
|
url = link(console_url, a)
|
|
187
220
|
ref = f" · <{url}|{'latest' if len(g) > 1 else 'open'}>" if url else ""
|
|
221
|
+
if a["kind"] == "revocation":
|
|
222
|
+
lines.append(f"*REVOKED* · {_slack_escape(a['summary'])}{ref}")
|
|
223
|
+
continue
|
|
224
|
+
if a["kind"] == "incident":
|
|
225
|
+
head = {"trigger": "INCIDENT", "escalate": "ESCALATED", "resolve": "RESOLVED"}[a["action"]]
|
|
226
|
+
verdict = f" ({a['incident']['verdict'].replace('_', ' ')})" if a["action"] == "resolve" else ""
|
|
227
|
+
lines.append(f"*{head}*{verdict} · {_slack_escape(a['summary'])}{ref}")
|
|
228
|
+
continue
|
|
188
229
|
if a["kind"] == "event":
|
|
189
230
|
e = a["event"]
|
|
190
231
|
times = f" ×{len(g)}" if len(g) > 1 else ""
|
|
@@ -200,7 +241,13 @@ def render(dest, alerts, console_url=""):
|
|
|
200
241
|
if a["action"] == "resolve":
|
|
201
242
|
bodies.append({"event_action": "resolve", "dedup_key": _dedup_key(key)})
|
|
202
243
|
continue
|
|
203
|
-
if a["kind"] == "
|
|
244
|
+
if a["kind"] == "revocation":
|
|
245
|
+
d = a["revocation"]
|
|
246
|
+
details, group, klass = dict(d), d.get("agent") or "all agents", "revocation"
|
|
247
|
+
elif a["kind"] == "incident":
|
|
248
|
+
i = a["incident"]
|
|
249
|
+
details, group, klass = dict(i), i.get("agent") or i.get("workflow") or "all agents", "incident"
|
|
250
|
+
elif a["kind"] == "event":
|
|
204
251
|
e = a["event"]
|
|
205
252
|
details = {"rule": e["rule"], "rule_id": e["rule_id"], "message": e["message"], "value": e.get("value"),
|
|
206
253
|
"task_id": e["task_id"], "run_id": e.get("run_id"), "project": e["project"],
|