feat(exporters): built-in HTTPExporter, env auto-attach, and the agentegrity pro quick-connect CLI - #39
Merged
Merged
Conversation
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…ct CLI Connecting an instrumented agent to an agentegrity-pro dashboard required hand-writing a ~20-line exporter class: the SDK shipped the SessionExporter protocol but no implementation, no console script, and no way to attach a sink without editing agent code. The README already promised that setting AGENTEGRITY_URL/AGENTEGRITY_TOKEN would stream 'with no extra package required' — this makes that true. - exporters/http.py: HTTPExporter, a stdlib-only SessionExporter. Delivery runs on a single background daemon thread fed by a FIFO queue, mirroring the sender in core/telemetry.py. That ordering is a correctness requirement, not an optimization: _notify_exporters schedules callbacks with asyncio.ensure_future and never awaits them, so on_session_start and the first on_event are independent tasks that can finish out of order — and the ingest API rejects an event for a session it has not seen (404). An atexit flush gives in-flight requests a bounded window, since close() likewise schedules on_session_end without awaiting it. Fail-open throughout. It also fills profile.framework from adapter_name when unset. AgentProfile leaves framework None unless the caller passes it, so dashboards had no way to label or group an agent by framework; the adapter knows this, so the exporter supplies it (without mutating the caller's dict). - adapters/base.py: _attach_env_exporter() self-attaches an HTTPExporter at construction when AGENTEGRITY_TOKEN and AGENTEGRITY_EXPORTER_URL (alias AGENTEGRITY_URL) are set. This is what lets the CLI stream an agent it did not write. Absent those vars nothing attaches and the SDK stays local. - __main__.py: an 'agentegrity pro' subcommand. Verifies the token against GET /ingest/verify and prints the workspace, then either reports the connection (--push) or execs a trailing '-- <command>' with the exporter env set, so the agent needs no changes. Distinct exit codes for a rejected token, an unreachable host, and a backend without the verify route. - pyproject.toml: [project.scripts] so 'agentegrity' is a real command; until now only 'python -m agentegrity' worked. Verified end-to-end against a live agentegrity-pro backend: a plain instrumented agent with zero exporter code attaches 0 exporters when run directly and 1 when wrapped by the CLI, and its session, events, per-layer results, attestation chain, and signed certificate all land in the dashboard read API. 675 passed, ruff and mypy clean.
requie
force-pushed
the
feature/pro-quick-connect
branch
from
July 28, 2026 19:57
273c93d to
0d71ff0
Compare
_attach_env_exporter() turned on egress silently. Two environment variables
were enough to start streaming, nothing logged it, and get_summary() carried no
exporter field, so an operator printing report() could not tell whether a
session was local or shipping to a remote host.
That matters more here than for telemetry. Telemetry is shape-only; an exporter
sends full event content, including prompts, tool arguments, and tool outputs.
The auto-attach itself is the right design (it is what lets `agentegrity pro …
-- <command>` stream an agent it did not write), so this makes it visible
rather than changing it.
- adapters/base.py: log the destination origin at INFO on attach, naming the
env var that configured it. The token is never logged.
- adapters/base.py: get_summary() gains an "exporters" entry listing each
attached sink as {"type", "target"}. This is the durable surface — a log
line alone is not enough, since Python's default level is WARNING and would
swallow the INFO message in most applications. An empty list is what lets a
run prove it stayed local.
- exporters/http.py, exporters/otel.py: optional describe(). HTTPExporter
reports its origin; OTelSessionExporter reports no target, because the OTLP
destination belongs to the OpenTelemetry SDK's own configuration and naming
one here could contradict where spans actually go.
- describe() is deliberately NOT a member of the SessionExporter Protocol.
Requiring it would break every custom exporter written before it existed;
those are reported by class name instead. Documented in the Protocol
docstring, including that whatever describe() returns is serialized into the
summary and must never carry credentials.
- schemas/exporter/common.json: document SessionSummary.exporters. The object
already allowed additionalProperties, so this documents rather than
constrains; test_schemas.py validates real get_summary() output and passes.
- The eight zero-config report() fallback dicts gain "exporters": [] so the
no-adapter shape matches the real one.
Tests written first, covering the disclosure rather than the plumbing: the
attach logs the host, the token never reaches any log record, a local run
reports [], and an exporter without describe() still appears by class name.
707 passed, ruff and mypy --strict clean.
| with caplog.at_level(logging.INFO, logger="agentegrity.adapters"): | ||
| _adapter() | ||
|
|
||
| assert any("https://dash.test" in r.getMessage() for r in caplog.records) |
agentegrity pro quick-connect CLI
…ring CodeQL flagged the disclosure test with py/incomplete-url-substring-sanitization: it matched the destination with `"https://dash.test" in record.getMessage()`. There is no exploit in a test assertion, but the rule is pointing at a real weakness in the assertion itself. A substring check against a URL proves very little, since a hostile URL can carry a trusted-looking string at an arbitrary position -- which is precisely why the rule exists for production code. Assert on the structured log args by equality instead: record.args == ("https://dash.test", "AGENTEGRITY_EXPORTER_URL") That pins the exact destination and the exact env var that configured it, rather than "the message mentions this somewhere", and it also pins that exactly one attach was logged. Stronger test, and CodeQL clean. 712 passed, ruff and mypy --strict clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes a promise the README already printed actually true. It said setting
AGENTEGRITY_URL/AGENTEGRITY_TOKENwould stream to anagentegrity-prodashboard "with no extra package required" — but the SDK shipped theSessionExporterprotocol with no implementation, no console script, and no way to attach a sink without editing agent code. Connecting required hand-writing a ~20-line exporter class.What's here
exporters/http.py—HTTPExporter, a stdlib-onlySessionExporter. Delivery runs on a single background daemon thread fed by a FIFO queue, mirroring the sender incore/telemetry.py.That ordering is a correctness requirement, not an optimization.
_notify_exportersschedules callbacks withasyncio.ensure_futureand never awaits them, soon_session_startand the firston_eventare independent tasks that can finish out of order — and the ingest API rejects an event for a session it has not seen (404 unknown_session). Anatexitflush gives in-flight requests a bounded window, sinceclose()likewise scheduleson_session_endwithout awaiting it. Fail-open throughout.It also fills
profile.frameworkfromadapter_namewhen unset.AgentProfileleavesframeworkNoneunless the caller passes it, so dashboards had no way to label or group an agent by framework; the adapter knows this, so the exporter supplies it (copying rather than mutating the caller's dict).adapters/base.py—_attach_env_exporter()self-attaches at construction whenAGENTEGRITY_TOKENandAGENTEGRITY_EXPORTER_URL(aliasAGENTEGRITY_URL) are set. This is what lets the CLI stream an agent it did not write. Absent those vars nothing attaches and the SDK stays local.__main__.py— anagentegrity prosubcommand. Verifies the token againstGET /ingest/verifyand prints the workspace, then either reports the connection (--push) or execs a trailing-- <command>with the exporter env set, so the agent needs no changes. Distinct exit codes for a rejected token, an unreachable host, and a backend without the verify route.pyproject.toml—[project.scripts]soagentegrityis a real command; until now onlypython -m agentegrityworked.Attached sinks are disclosed
The second commit closes a gap in the first. Auto-attach turned on egress silently: nothing logged it, and
get_summary()carried no exporter field, so an operator printingreport()could not tell whether a session was local or shipping to a remote host.That matters more here than for telemetry. Telemetry is shape-only; an exporter sends full event content — prompts, tool arguments, tool outputs. Two environment variables were enough to turn that on invisibly.
The auto-attach itself is the right design, so this makes it visible rather than changing it:
get_summary()gained anexportersentry listing each sink as{"type", "target"}.Both surfaces, because one is not enough: Python's default log level is WARNING and would swallow the INFO line in most applications. The summary is the durable surface, and an empty list is what lets a run prove it stayed local.
describe()is deliberately not a member of theSessionExporterProtocol — requiring it would break every custom exporter written against the existing three-method contract. It is a duck-typed convention; exporters without it are reported by class name.OTelSessionExporter.describe()reports notarget, because the OTLP destination belongs to the OpenTelemetry SDK's own configuration and naming one here could contradict where spans actually go.Compatibility
schemas/exporter/common.jsondocumentsSessionSummary.exporters. The object already allowedadditionalProperties, so this documents rather than constrains;tests/test_schemas.pyvalidates realget_summary()output and passes.report()fallback dicts gain"exporters": []so the no-adapter shape matches the real one.mainafter feat(exporters): OpenTelemetry session exporter (traces + metrics) #38 merged. Theexporters/__init__.pyadd/add conflict with the OTel exporter is resolved by exporting both, and the package still imports cleanly without the[otel]extra installed (verified by blocking everyopentelemetryimport at the meta-path level), withOTelSessionExporter()raising the install hint rather than failing at import.Verification
ruffclean,mypy --strictclean across 45 source filesproexit codes confirmed:2no token,2no URL,1unreachable host,2unknown optionexporterslist with no token anywhere in the report; streaming off emits no log line and[]The original work was verified end-to-end against a live
agentegrity-probackend: a plain instrumented agent with zero exporter code attaches 0 exporters when run directly and 1 when wrapped by the CLI, and its session, events, per-layer results, attestation chain, and signed certificate all land in the dashboard read API.