Skip to content

feat(exporters): built-in HTTPExporter, env auto-attach, and the agentegrity pro quick-connect CLI - #39

Merged
requie merged 3 commits into
mainfrom
feature/pro-quick-connect
Jul 28, 2026
Merged

feat(exporters): built-in HTTPExporter, env auto-attach, and the agentegrity pro quick-connect CLI#39
requie merged 3 commits into
mainfrom
feature/pro-quick-connect

Conversation

@requie

@requie requie commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Makes a promise the README already printed actually true. It said setting AGENTEGRITY_URL / AGENTEGRITY_TOKEN would stream to an agentegrity-pro dashboard "with no extra package required" — but the SDK shipped the SessionExporter protocol 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.pyHTTPExporter, 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 unknown_session). 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 (copying rather than mutating the caller's dict).

adapters/base.py_attach_env_exporter() self-attaches 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.

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 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 — 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:

  • The adapter logs the destination origin at INFO on attach, naming the env var that configured it. The token is never logged.
  • get_summary() gained an exporters entry 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 the SessionExporter Protocol — 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 no target, 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.json documents SessionSummary.exporters. The object already allowed additionalProperties, so this documents rather than constrains; tests/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.
  • Rebased onto main after feat(exporters): OpenTelemetry session exporter (traces + metrics) #38 merged. The exporters/__init__.py add/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 every opentelemetry import at the meta-path level), with OTelSessionExporter() raising the install hint rather than failing at import.

Verification

  • 707 passed, 42 skipped
  • ruff clean, mypy --strict clean across 45 source files
  • Schema drift tests pass (5/5)
  • Console script resolves; pro exit codes confirmed: 2 no token, 2 no URL, 1 unreachable host, 2 unknown option
  • Disclosure tested end-to-end both ways: streaming on emits the INFO line and a populated exporters list 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-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.

@cursor

cursor Bot commented Jul 28, 2026

Copy link
Copy Markdown

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
requie force-pushed the feature/pro-quick-connect branch from 273c93d to 0d71ff0 Compare July 28, 2026 19:57
_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.
Comment thread tests/test_exporter_autoattach.py Outdated
with caplog.at_level(logging.INFO, logger="agentegrity.adapters"):
_adapter()

assert any("https://dash.test" in r.getMessage() for r in caplog.records)
@requie requie changed the title Add built-in HTTPExporter, env auto-attach, and the 'pro' quick-conne… feat(exporters): built-in HTTPExporter, env auto-attach, and the agentegrity pro quick-connect CLI Jul 28, 2026
…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.
@requie
requie merged commit cba8eb1 into main Jul 28, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants