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.
Files changed (95) hide show
  1. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/PKG-INFO +14 -6
  2. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/README.md +13 -5
  3. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/__init__.py +1 -1
  4. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/__main__.py +53 -2
  5. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/alerts.py +28 -2
  6. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/analysis.py +473 -76
  7. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/assess.py +28 -4
  8. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/base.py +33 -3
  9. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/diagnose.py +52 -1
  10. agentdynamics-0.9.0/agentdynamics/api/governance.py +327 -0
  11. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/monitor.py +7 -2
  12. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/ops.py +2 -0
  13. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/autotrace.py +45 -7
  14. agentdynamics-0.9.0/agentdynamics/calibrate.py +173 -0
  15. agentdynamics-0.9.0/agentdynamics/checker.py +197 -0
  16. agentdynamics-0.9.0/agentdynamics/collectors/billing.py +76 -0
  17. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/claude_code.py +6 -1
  18. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/generic.py +5 -1
  19. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/langfuse.py +2 -1
  20. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/langsmith.py +1 -0
  21. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/otlp.py +1 -1
  22. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/spans.py +26 -1
  23. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/config.py +53 -1
  24. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/engine.py +331 -4
  25. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/govern.py +15 -0
  26. agentdynamics-0.9.0/agentdynamics/incidents.py +123 -0
  27. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/integrations/aegis.py +279 -31
  28. agentdynamics-0.9.0/agentdynamics/migrate.py +169 -0
  29. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/pg.py +30 -5
  30. agentdynamics-0.9.0/agentdynamics/pricing.py +111 -0
  31. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/privacy.py +11 -1
  32. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/server.py +57 -7
  33. agentdynamics-0.9.0/agentdynamics/sketch.py +73 -0
  34. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/slo.py +6 -2
  35. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/store.py +284 -18
  36. agentdynamics-0.9.0/agentdynamics/tripwires.py +96 -0
  37. agentdynamics-0.9.0/agentdynamics/trust.py +147 -0
  38. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/app.js +8 -5
  39. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/charts.js +2 -2
  40. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/assess.js +21 -5
  41. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/diagnose.js +17 -4
  42. agentdynamics-0.9.0/agentdynamics/web/pages/governance.js +330 -0
  43. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/monitor.js +25 -10
  44. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/PKG-INFO +14 -6
  45. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/SOURCES.txt +20 -1
  46. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/pyproject.toml +1 -1
  47. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_aegis_integration.py +390 -0
  48. agentdynamics-0.9.0/tests/test_allocation.py +84 -0
  49. agentdynamics-0.9.0/tests/test_apdex.py +169 -0
  50. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_autotrace.py +21 -1
  51. agentdynamics-0.9.0/tests/test_billing.py +166 -0
  52. agentdynamics-0.9.0/tests/test_calibrate.py +170 -0
  53. agentdynamics-0.9.0/tests/test_checker.py +196 -0
  54. agentdynamics-0.9.0/tests/test_console_interaction.py +422 -0
  55. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_core.py +117 -0
  56. agentdynamics-0.9.0/tests/test_ground_truth.py +266 -0
  57. agentdynamics-0.9.0/tests/test_incidents.py +350 -0
  58. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_incremental.py +141 -3
  59. agentdynamics-0.9.0/tests/test_live_anthropic.py +123 -0
  60. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_policy_coverage.py +66 -0
  61. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_postgres.py +297 -42
  62. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_revocations.py +53 -0
  63. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_rollups.py +52 -1
  64. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_scoped_keys.py +12 -1
  65. agentdynamics-0.9.0/tests/test_sketch.py +49 -0
  66. agentdynamics-0.9.0/tests/test_tripwires.py +211 -0
  67. agentdynamics-0.9.0/tests/test_trust.py +222 -0
  68. agentdynamics-0.8.0/agentdynamics/api/governance.py +0 -169
  69. agentdynamics-0.8.0/agentdynamics/pricing.py +0 -69
  70. agentdynamics-0.8.0/agentdynamics/web/pages/governance.js +0 -125
  71. agentdynamics-0.8.0/tests/test_live_anthropic.py +0 -66
  72. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/LICENSE +0 -0
  73. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/api/__init__.py +0 -0
  74. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/bootstrap/sitecustomize.py +0 -0
  75. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/__init__.py +0 -0
  76. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/aegis_audit.py +0 -0
  77. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/collectors/inbox.py +0 -0
  78. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/flows.py +0 -0
  79. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/integrations/__init__.py +0 -0
  80. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/phases.py +0 -0
  81. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/sdk.py +0 -0
  82. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/index.html +0 -0
  83. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/pages/configure.js +0 -0
  84. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics/web/style.css +0 -0
  85. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/dependency_links.txt +0 -0
  86. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/entry_points.txt +0 -0
  87. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/requires.txt +0 -0
  88. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/agentdynamics.egg-info/top_level.txt +0 -0
  89. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/setup.cfg +0 -0
  90. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_alerts.py +0 -0
  91. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_console_ui.py +0 -0
  92. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_ecosystem.py +0 -0
  93. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_integrations.py +0 -0
  94. {agentdynamics-0.8.0 → agentdynamics-0.9.0}/tests/test_outcomes.py +0 -0
  95. {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.8.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** | 28 rules (runaway cost, policy denials, boundary probing, 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 |
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** | 28 rules (runaway cost, policy denials, boundary probing, 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 |
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.
@@ -5,7 +5,7 @@
5
5
 
6
6
  See agentdynamics.autotrace for @trace / @tool / span.
7
7
  """
8
- __version__ = "0.8.0"
8
+ __version__ = "0.9.0"
9
9
 
10
10
  from .autotrace import flush, init, llm_call, outcome, record_llm, span, tool, trace # noqa: E402,F401
11
11
 
@@ -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}: {'agent ' + a.agent if a.agent else 'every agent'} in {a.project or 'every project'} is revoked "
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"),