Skip to content

[RefImpl] Harden the trace exporter's file and endpoint handling, and the spans it vouches for #283

Open

Description

@rocklambros

Target runtime or language

AGT reference Guardian: packages/guardian/src/otel-trace-exporter.ts, server.ts

What this proves

This is part of issue #274, the second of three slices.

The exporter's file and network handling must not leak trace content or write through a link. Spans must not carry a request pair that the Guardian cannot vouch for.

Scope, with no blocker:

  1. File opens.
    • On every append, open with O_WRONLY|O_APPEND|O_CREAT|O_NOFOLLOW|O_NONBLOCK, then fstat the descriptor.
    • Refuse when the path is not a regular file, or when the effective user doesn't own it. Otherwise set mode 0600 with fchmod.
    • Do not refuse a file only because its mode is wider. PR Batch Guardian log writes and drain them on shutdown #112 keeps existing file modes, so the rule here should tighten the mode, not refuse.
    • fs.promises.appendFile with O_NOFOLLOW still follows a symlink in Bun 1.3.14, so use open plus write.
    • Refuse a parent directory that the user doesn't own, or that others can write to.
    • On Windows, O_NOFOLLOW does not exist. Write the Windows behavior as a documented branch, and give it a test.
  2. Non-loopback http:// endpoint. Build no endpoint output unless an explicit opt-in variable is set. If OTEL_EXPORTER_OTLP_*HEADERS is set, refuse even with the opt-in, because the headers can carry credentials. Decide what counts as loopback from the parsed address (127.0.0.0/8, ::1, ::ffff:127.0.0.1), not with string checks. Document TLS and headers in the Guardian README.
  3. Steps open at shutdown. If a step is still open and older than the timeout, give it class timeout, not other.
  4. elapsed_ms. A late decision must never show elapsed_ms equal to timeout_ms. Fix the rounding.
  5. Error codes. Clamp oversized error codes so they never overflow int64.

Scope that waits for a decision:

  1. Spans the Guardian cannot vouch for. Today, re-posting a real pair puts three events on that pair's span: an allow, a guardian_error, and a deny. The schema-invalid re-post comes back as a deny with envelope_invalid, not as an error, so a rule that looks only at error responses misses it. The proposed rule is that the Guardian puts a pair on a span only when it can vouch for it. @bar-capsule, please decide how such a span is represented: is it dropped, or written under names outside step_to_span? After PR fix(agt): implement ACS-Core wire safeguards #229 lands, "vouch" means the signature verified.
  2. The signal path (main.ts, process.once, the second SIGTERM). This waits for PR Batch Guardian log writes and drain them on shutdown #112, which rewrites the same lines.

Acceptance tests:

  • File opens. A symlink at the final path is refused. A symlink in the parent directory is refused. A hardlink is refused. An existing 0644 file ends up 0600.
  • Endpoint. http://10.0.0.1:4318 gives no endpoint output and a report. With the opt-in, it gives output plus a warning. With the opt-in and a headers variable set, it is refused.
  • Re-post. Replaying the four re-posts from the issue Trace exporter follow-ups from the PR #244 review #274 premortem leaves exactly one span carrying the real pair.

Do you intend to implement this yourself

Yes. @sabasiddique1 is implementing it after the first slice is in review. The reviewer is @rocklambros.

Current Priority Scope

Feeds the runnable Guardian reference implementation

Activity

  1. added
    type:refimplReference implementation or adapter work
    scope:in-focusFeeds the ninety-day committed outcome. Maintainers only
    status:acceptedIn the backlog. Maintainers only
    on Oct 15, 2026
  2. moved this from Needs triage to Accepted in ACS Project Trackeron Oct 15, 2026
  3. sabasiddique1 commented on Oct 15, 2026

    @sabasiddique1
    Contributor

    I’ll pick this up once #282 is in review.

  4. bar-capsule commented on Oct 17, 2026

    @bar-capsule
    Collaborator

    @rocklambros @sabasiddique1 keep them under separate names. A forged or replayed request is worth seeing, so dropping it would hide the attack, but it should never be recorded under the genuine request's IDs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:P2Maintainers onlyscope:in-focusFeeds the ninety-day committed outcome. Maintainers onlystatus:acceptedIn the backlog. Maintainers onlytype:refimplReference implementation or adapter workworkstream:refimplOwning workstream. Maintainers only

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions