agentdynamics 0.8.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.8.0 → agentdynamics-0.9.0}/PKG-INFO +14 -6
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/README.md +13 -5
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/__init__.py +1 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/__main__.py +53 -2
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/alerts.py +28 -2
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/analysis.py +473 -76
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/assess.py +28 -4
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/base.py +33 -3
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/diagnose.py +52 -1
- agentdynamics-0.9.0/agentdynamics/api/governance.py +327 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/monitor.py +7 -2
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/ops.py +2 -0
- {agentdynamics-0.8.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.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/claude_code.py +6 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/generic.py +5 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/langfuse.py +2 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/langsmith.py +1 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/otlp.py +1 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/spans.py +26 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/config.py +53 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/engine.py +331 -4
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/govern.py +15 -0
- agentdynamics-0.9.0/agentdynamics/incidents.py +123 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/integrations/aegis.py +279 -31
- agentdynamics-0.9.0/agentdynamics/migrate.py +169 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/pg.py +30 -5
- agentdynamics-0.9.0/agentdynamics/pricing.py +111 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/privacy.py +11 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/server.py +57 -7
- agentdynamics-0.9.0/agentdynamics/sketch.py +73 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/slo.py +6 -2
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/store.py +284 -18
- agentdynamics-0.9.0/agentdynamics/tripwires.py +96 -0
- agentdynamics-0.9.0/agentdynamics/trust.py +147 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/app.js +8 -5
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/charts.js +2 -2
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/assess.js +21 -5
- {agentdynamics-0.8.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.8.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/monitor.js +25 -10
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/PKG-INFO +14 -6
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/SOURCES.txt +20 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/pyproject.toml +1 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_aegis_integration.py +390 -0
- agentdynamics-0.9.0/tests/test_allocation.py +84 -0
- agentdynamics-0.9.0/tests/test_apdex.py +169 -0
- {agentdynamics-0.8.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.8.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.8.0 → agentdynamics-0.9.0}/tests/test_incremental.py +141 -3
- agentdynamics-0.9.0/tests/test_live_anthropic.py +123 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_policy_coverage.py +66 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_postgres.py +297 -42
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_revocations.py +53 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_rollups.py +52 -1
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_scoped_keys.py +12 -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.8.0/agentdynamics/api/governance.py +0 -169
- agentdynamics-0.8.0/agentdynamics/pricing.py +0 -69
- agentdynamics-0.8.0/agentdynamics/web/pages/governance.js +0 -125
- agentdynamics-0.8.0/tests/test_live_anthropic.py +0 -66
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/LICENSE +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/__init__.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/bootstrap/sitecustomize.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/__init__.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/aegis_audit.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/inbox.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/flows.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/integrations/__init__.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/phases.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/sdk.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/index.html +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/configure.js +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/style.css +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/dependency_links.txt +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/entry_points.txt +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/requires.txt +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/top_level.txt +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/setup.cfg +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_alerts.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_console_ui.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_ecosystem.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_integrations.py +0 -0
- {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_outcomes.py +0 -0
- {agentdynamics-0.8.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,33 @@ 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
|
+
|
|
190
217
|
def cmd_revoke(a, data):
|
|
191
218
|
from . import config as cfgmod, store
|
|
192
219
|
con = store.connect(*store.target(cfgmod.load(data), data))
|
|
@@ -199,16 +226,31 @@ def cmd_revoke(a, data):
|
|
|
199
226
|
rows_ = store.revocations(con, now)
|
|
200
227
|
for d in rows_:
|
|
201
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 []))
|
|
202
231
|
print(f"{d['id']} {status:<8} {d['project'] or '(all projects)':<18} {d['agent'] or '(all agents)':<18} "
|
|
203
|
-
f"until {time.strftime('%Y-%m-%d %H:%M', time.localtime(d['expires']))} [{d['source']}] {d['reason']}")
|
|
232
|
+
f"until {time.strftime('%Y-%m-%d %H:%M', time.localtime(d['expires']))} [{d['source']}] {what} {d['reason']}")
|
|
204
233
|
if not rows_:
|
|
205
234
|
print("no directives")
|
|
206
235
|
return 0
|
|
207
236
|
if not a.reason:
|
|
208
237
|
print("--reason is required: it goes into the kernel's audit log")
|
|
209
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
|
|
210
252
|
rid = store.add_revocation(con, a.project, a.agent, a.reason, "operator", now, now + a.minutes * 60)
|
|
211
|
-
print(f"{rid}: {
|
|
253
|
+
print(f"{rid}: {who} is revoked "
|
|
212
254
|
f"for {a.minutes:g} min, in processes using agentdynamics.integrations.aegis.instrument(..., revocations=True)")
|
|
213
255
|
return 0
|
|
214
256
|
|
|
@@ -399,12 +441,19 @@ def main(argv=None):
|
|
|
399
441
|
al.add_argument("action", choices=["status", "test"],
|
|
400
442
|
help="status: destinations, queue and firing SLO alerts; test: send a test alert to each")
|
|
401
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")
|
|
402
449
|
rv = sub.add_parser("revoke", help="revoke an agent's Aegis grants wherever it runs (applied in-process by "
|
|
403
450
|
"agentdynamics.integrations.aegis), or list/clear directives")
|
|
404
451
|
rv.add_argument("--agent", help="the agent (grant) name; omit to revoke every grant in the project")
|
|
405
452
|
rv.add_argument("--project", help="omit for every project")
|
|
406
453
|
rv.add_argument("--reason", help="why (it goes into the kernel's audit log)")
|
|
407
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")
|
|
408
457
|
rv.add_argument("--list", action="store_true", help="list directives")
|
|
409
458
|
rv.add_argument("--clear", metavar="ID", help="stop a directive (grants it revoked stay revoked)")
|
|
410
459
|
sub.add_parser("ingest", help="scan sources once and rebuild the database")
|
|
@@ -433,6 +482,8 @@ def main(argv=None):
|
|
|
433
482
|
return cmd_alerts(a, a.data)
|
|
434
483
|
if cmd == "revoke":
|
|
435
484
|
return cmd_revoke(a, a.data)
|
|
485
|
+
if cmd == "store":
|
|
486
|
+
return cmd_store(a, a.data)
|
|
436
487
|
|
|
437
488
|
from .engine import Engine
|
|
438
489
|
eng = Engine(a.data, a.claude_root or None)
|
|
@@ -11,7 +11,8 @@ Destinations are `[[alerts.webhooks]]` in agentdynamics.toml:
|
|
|
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
13
|
kinds = ["events", "slos"] # default ["events"]; "slos" adds SLO burn-rate alerts,
|
|
14
|
-
# "revocations" the directives that stop an agent (server-side #8)
|
|
14
|
+
# "revocations" the directives that stop an agent (server-side #8),
|
|
15
|
+
# "incidents" one alert per security incident (incidents.py)
|
|
15
16
|
projects = ["checkout"] # optional: only these projects
|
|
16
17
|
rules = ["run_failed"] # optional: only these health rules
|
|
17
18
|
|
|
@@ -19,6 +20,9 @@ What is sent:
|
|
|
19
20
|
- A health-rule event is a point in time. Slack and JSON get each one (JSON in the original
|
|
20
21
|
`{"source": "agentdynamics", "events": [...]}` shape, a contract). PagerDuty gets one alert per rule,
|
|
21
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.
|
|
22
26
|
- An SLO alert has a start and an end: a trigger when a burn-rate policy starts firing and a resolve when
|
|
23
27
|
it stops (slo.alert_conditions). What is firing is kept in the durable `alert_state` table, so a restart
|
|
24
28
|
neither repeats a trigger nor forgets to resolve.
|
|
@@ -38,7 +42,7 @@ from collections import OrderedDict
|
|
|
38
42
|
from urllib.parse import quote
|
|
39
43
|
|
|
40
44
|
FORMATS = ("json", "slack", "pagerduty")
|
|
41
|
-
KINDS = ("events", "slos", "revocations")
|
|
45
|
+
KINDS = ("events", "slos", "revocations", "incidents")
|
|
42
46
|
SEV = {"info": 1, "warning": 2, "critical": 3}
|
|
43
47
|
# https://raw.githubusercontent.com/PagerDuty/api-schema/main/reference/events-v2/openapiv3.json
|
|
44
48
|
PAGERDUTY_URL = "https://events.pagerduty.com/v2/enqueue"
|
|
@@ -145,6 +149,14 @@ def from_revocation(d):
|
|
|
145
149
|
"ts": d["created"], "revocation": d}
|
|
146
150
|
|
|
147
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
|
+
|
|
148
160
|
# ---------------------------------------------------------------- formats
|
|
149
161
|
|
|
150
162
|
def _dedup_key(key):
|
|
@@ -160,6 +172,8 @@ def link(console_url, alert):
|
|
|
160
172
|
return f"{base}/#/task/{quote(alert['event']['task_id'], safe='')}"
|
|
161
173
|
if alert["kind"] == "revocation":
|
|
162
174
|
return f"{base}/#/governance"
|
|
175
|
+
if alert["kind"] == "incident":
|
|
176
|
+
return f"{base}/#/incident/{quote(alert['incident']['id'], safe='')}"
|
|
163
177
|
return f"{base}/#/slos"
|
|
164
178
|
|
|
165
179
|
|
|
@@ -187,6 +201,10 @@ def render(dest, alerts, console_url=""):
|
|
|
187
201
|
revs = [dict(a["revocation"], summary=a["summary"]) for a in alerts if a["kind"] == "revocation"]
|
|
188
202
|
if revs:
|
|
189
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})
|
|
190
208
|
return bodies
|
|
191
209
|
# one line (Slack) or one alert (PagerDuty) per dedup key, so a burst of the same violation reads as one
|
|
192
210
|
groups = OrderedDict()
|
|
@@ -203,6 +221,11 @@ def render(dest, alerts, console_url=""):
|
|
|
203
221
|
if a["kind"] == "revocation":
|
|
204
222
|
lines.append(f"*REVOKED* · {_slack_escape(a['summary'])}{ref}")
|
|
205
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
|
|
206
229
|
if a["kind"] == "event":
|
|
207
230
|
e = a["event"]
|
|
208
231
|
times = f" ×{len(g)}" if len(g) > 1 else ""
|
|
@@ -221,6 +244,9 @@ def render(dest, alerts, console_url=""):
|
|
|
221
244
|
if a["kind"] == "revocation":
|
|
222
245
|
d = a["revocation"]
|
|
223
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"
|
|
224
250
|
elif a["kind"] == "event":
|
|
225
251
|
e = a["event"]
|
|
226
252
|
details = {"rule": e["rule"], "rule_id": e["rule_id"], "message": e["message"], "value": e.get("value"),
|