Skip to content

docs(adr): specify Level 3 content capture activation and sandbox env denylist - #5947

Closed
dhshah13 wants to merge 5 commits into
fullsend-ai:mainfrom
dhshah13:docs/l3-activation-adrs
Closed

docs(adr): specify Level 3 content capture activation and sandbox env denylist#5947
dhshah13 wants to merge 5 commits into
fullsend-ai:mainfrom
dhshah13:docs/l3-activation-adrs

Conversation

@dhshah13

@dhshah13 dhshah13 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

ADR 0050 defined Level 3 — prompt/completion content in spans — as an explicit opt-in but left the activation semantics unspecified: which configuration enables it, what happens on partial configuration, and where content may flow. Two ADRs close that gap:

  • ADR 0084 — Level 3 content capture activation contract. Three independently-owned conditions (operator env opt-in per ADR 0081's plumbing model, per-agent harness consent under ADR 0080's placement rule, and an org-level endpoint allow-list in ADR 0082's shape whose entries record governance sign-off). Absent/off conditions leave runs metadata-only; an affirmative capture request that cannot satisfy the contract fails before sandbox creation. Content flows over OTLP only, passes through secret redaction with hits recorded as findings, and deliberately excludes reasoning/thinking text (a stated narrowing of 0050's sketch).
  • ADR 0085 — sandbox denylist for telemetry and runtime control variables. Extends the fix(#5832): strip OIDC credentials from user-controlled environments #5837 denylist surface to OTEL_*/CLAUDE_CODE_*/TRACEPARENT keys and host_files destinations, as pre-flight hard errors, so the activation contract cannot be bypassed from inside the sandbox. Closes a live gap independent of Level 3.

Living documents updated in the same PR per the writing-adrs skill: dated annotations on ADRs 0050 and 0021, the operator guide's Level 3 section rewritten to the contract (no ADR links, per the guide-link policy), an architecture.md Decided entry, and a cross-reference in the operational-observability problem doc.

ADR numbers verified free at commit time against main and in-flight PRs via skills/renumber-adr/scripts/inflight-adr-numbers.sh.

Implementation is tracked separately (issues to follow); every implementation PR merges inert until the contract's preconditions are met. Relates to #2862 and the Level 3 line item in ADR 0050.

Test plan

  • make lint — ADR statuses/numbers/frontmatter, docs link scope, and markdown link linters all pass
  • ADRs conform to the template and writing-adrs skill (one decision each, 69/57 content lines, Consequences as one-sentence bullets)

🤖 Generated with Claude Code

@dhshah13
dhshah13 requested a review from a team as a code owner August 5, 2026 18:53
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

See E2E testing guide for details.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Define Level 3 content capture activation contract and sandbox telemetry denylist

📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Define a fail-closed, three-party activation contract for Level 3 span content capture.
• Add an ADR to prevent in-sandbox self-enablement via telemetry/control env vars and sourced files.
• Update observability docs to align with the contract and cross-reference related decisions.
Diagram

graph TD
  OperatorEnv["Operator env opt-in"] & HarnessConsent["Harness consent"] & OrgAllowlist["Org endpoint allowlist"] --> Gate{"Activation contract"} -->|"pass"| Sandbox["Sandbox run"] --> Assembly["Span content assembly"] --> Redactor["Secret redactor"] --> OTLP["OTLP export"]
  Gate -->|"fail"| Abort["Fail before sandbox"]
  Assembly --> JSONL["JSONL exporter (metadata-only)"]
  subgraph Legend
    direction LR
    _cfg["Config input"] ~~~ _dec{"Decision"} ~~~ _flow["Processing step"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Single operator env flag (status quo-style opt-in)
  • ➕ Simpler activation model and documentation
  • ➕ Less coordination across owners
  • ➖ High risk: a single variable can route sensitive content to arbitrary endpoints
  • ➖ Harder to enforce governance and non-production boundaries
  • ➖ Easier to bypass from within the sandbox without additional controls
2. Centralize all consent in org policy (no harness knob)
  • ➕ One place to audit/approve; simpler per-repo operations
  • ➕ Stronger centralized governance
  • ➖ Removes per-agent/repo sensitivity control and CODEOWNERS-based consent
  • ➖ Encourages overly broad enablement that may not fit all agents
3. Allow-list by full OTLP URL (host+scheme+path) instead of host-only
  • ➕ More precise control; reduces ambiguity when multiple collectors share a host
  • ➕ Can differentiate prod vs non-prod paths on same host
  • ➖ More operational churn if paths/ports change
  • ➖ Harder to express safely across varied collector setups; host-level may be the practical governance unit

Recommendation: Keep the PR’s three-condition, fail-closed activation contract plus sandbox denylist. It balances independent ownership (operator + content-owner + governance), prevents accidental enablement, and explicitly blocks the known in-sandbox bypass surface. Consider tightening the allow-list granularity to full URL only if host-level control proves insufficient in practice.

Files changed (7) +208 / -20

Documentation (7) +208 / -20
0021-jsonl-reasoning-trace-exposure.mdAnnotate secret redaction role for future Level 3 span content +8/-0

Annotate secret redaction role for future Level 3 span content

• Adds a dated annotation referencing ADR 0084. Clarifies that secret redaction is an output-side invariant check and that access control remains the primary boundary.

docs/ADRs/0021-jsonl-reasoning-trace-exposure.md

0050-distributed-tracing-instrumentation.mdRecord where Level 3 activation semantics are specified +8/-0

Record where Level 3 activation semantics are specified

• Adds a dated note pointing Level 3 activation semantics to ADR 0084. Also references ADR 0085 as closing the sandbox-side opt-in path around the Level 3 model.

docs/ADRs/0050-distributed-tracing-instrumentation.md

0084-level-3-content-capture-activation-contract.mdAdd ADR for Level 3 content capture activation contract +92/-0

Add ADR for Level 3 content capture activation contract

• Introduces an accepted ADR defining a three-condition activation gate (operator env opt-in, per-agent harness consent, and org allow-listed OTLP endpoint host). Specifies fail-closed behavior for capture requests, OTLP-only content flow, mandatory redaction with findings, and explicitly excludes reasoning/thinking capture.

docs/ADRs/0084-level-3-content-capture-activation-contract.md

0085-sandbox-environment-variable-denylist.mdAdd ADR extending sandbox denylist to telemetry/control variables +68/-0

Add ADR extending sandbox denylist to telemetry/control variables

• Adds an accepted ADR denying OTEL_*, CLAUDE_CODE_*, and TRACEPARENT injection via harness env and via sourced/consumed host_files destinations. Establishes preflight hard-error semantics to prevent bypassing Level 3 gating from inside the sandbox.

docs/ADRs/0085-sandbox-environment-variable-denylist.md

architecture.mdDocument decided Level 3 activation + sandbox denylist in observability section +1/-0

Document decided Level 3 activation + sandbox denylist in observability section

• Adds a Decided bullet summarizing the dual-consent + allow-list activation contract and the sandbox denylist enforcement. Links to ADRs 0084 and 0085 for the canonical decisions.

docs/architecture.md

distributed-tracing.mdRewrite Level 3 guide section to the activation contract +30/-19

Rewrite Level 3 guide section to the activation contract

• Replaces a single-env-var enablement description with the three-condition activation contract and fail-closed semantics. Clarifies OTLP-only content flow, mandatory secret redaction, metadata-only JSONL behavior, and that reasoning/thinking is excluded from capture.

docs/guides/infrastructure/distributed-tracing.md

operational-observability.mdCross-reference ADR 0084 from trace-granularity open question +1/-1

Cross-reference ADR 0084 from trace-granularity open question

• Extends the trace-granularity open question to reference ADR 0084’s activation decision. Clarifies that metadata-by-default with on-demand content is the chosen activation model, while granularity/retention remain open.

docs/problems/operational-observability.md

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Site preview

Preview: https://a26c2f8a-site.fullsend-ai.workers.dev

Commit: ed9a50d1b63673f26d337e93e0ef2b02724a50c8

@qodo-code-review

qodo-code-review Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (2)

Grey Divider


Remediation recommended

1. Endpoint allow-list underspecified ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
ADR 0084 introduces FULLSEND_CONTENT_CAPTURE_ALLOWED_ENDPOINTS and says it follows ADR 0082’s
allow-list “shape,” but ADR 0082 defines a different allow-list (WORKFLOW_HOST_REPOS) for mint
workflow refs and doesn’t specify endpoint-host parsing/matching rules. This leaves the new endpoint
allow-list gate underspecified (format, normalization, host vs host:port, wildcards, defaults, and
management/status surface).
Code

docs/ADRs/0084-level-3-content-capture-activation-contract.md[R52-55]

+3. **Allowlisted destination:** the resolved OTLP traces endpoint host must
+   appear in the `FULLSEND_CONTENT_CAPTURE_ALLOWED_ENDPOINTS` org variable,
+   following the [ADR 0082](0082-workflow-host-allow-list.md) allow-list
+   shape (default-minimal, CLI-managed, surfaced in status). With no
Relevance

●●● Strong

Team often asks ADRs to spell out exact semantics/limits; underspecified allow-list rules likely get
clarified.

PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
ADR 0084 claims an endpoint-host allow-list follows ADR 0082’s shape, but ADR 0082 is specifically
about mint workflow-host allow-listing via WORKFLOW_HOST_REPOS, not OTLP endpoint hosts; therefore
ADR 0084 lacks concrete endpoint allow-list semantics on its own.

docs/ADRs/0084-level-3-content-capture-activation-contract.md[52-56]
docs/ADRs/0082-workflow-host-allow-list.md[48-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ADR 0084 depends on an org-level endpoint allow-list, but it does not fully define the semantics of that allow-list (value format and matching rules) and instead references ADR 0082, which governs a different allow-list domain.

## Issue Context
Because this allow-list is a security gate for Level 3 content export, ambiguity in parsing/matching/ownership can lead to inconsistent or incorrect implementations.

## Fix Focus Areas
- docs/ADRs/0084-level-3-content-capture-activation-contract.md[52-57]
- docs/ADRs/0082-workflow-host-allow-list.md[48-60]

## Suggested approach
- In ADR 0084, explicitly define:
 - the exact `FULLSEND_CONTENT_CAPTURE_ALLOWED_ENDPOINTS` value format (e.g., CSV hosts, JSON array, newline-separated).
 - the matching algorithm (host only vs host:port, case/IDNA rules, handling of schemes/paths, wildcard policy).
 - defaults and behavior when unset/empty.
 - how it’s managed and surfaced (CLI command names and status output), if that’s part of the contract.
- Keep the ADR 0082 reference only as a *precedent* (if desired), but do not rely on it for endpoint-specific semantics.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Reasoning capture contradiction ✓ Resolved 🐞 Bug ≡ Correctness
Description
The operator guide and ADR 0084 explicitly state Level 3 excludes reasoning/thinking text, but ADR
0050 still states Level 3 “enables LLM-judge evaluation scorers that need to read agent reasoning,”
creating conflicting guidance on what sensitive data Level 3 exports. This can mislead
implementers/operators about the data-scope and privacy implications of enabling Level 3.
Code

docs/guides/infrastructure/distributed-tracing.md[R154-157]

+When active, spans include system prompts, user messages, per-turn assistant
+text, tool arguments, and tool results, all passed through secret redaction
+before export. Reasoning/thinking text is not captured. Content flows only
+to the allow-listed OTLP endpoint; `run-telemetry.jsonl` stays
Relevance

●● Moderate

They prefer annotations over rewriting accepted ADRs; may add clarifying note, but scope/wording is
subjective.

PR-#5244
PR-#2465

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The updated guide and ADR 0084 clearly state reasoning/thinking text is not captured, while ADR 0050
still ties Level 3 to reasoning-dependent scorers, creating a cross-document contradiction about
Level 3’s contents.

docs/guides/infrastructure/distributed-tracing.md[154-158]
docs/ADRs/0084-level-3-content-capture-activation-contract.md[72-75]
docs/ADRs/0050-distributed-tracing-instrumentation.md[82-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Level 3’s scope is inconsistent across documentation: new contract docs say reasoning/thinking is not captured, while ADR 0050 still implies Level 3 supports reasoning-dependent use cases.

## Issue Context
ADR 0084 intentionally narrows ADR 0050’s earlier Level 3 sketch, but ADR 0050’s *Decision* section still contains reasoning-oriented wording that reads like the active contract.

## Fix Focus Areas
- docs/ADRs/0050-distributed-tracing-instrumentation.md[82-88]
- docs/ADRs/0084-level-3-content-capture-activation-contract.md[72-75]
- docs/guides/infrastructure/distributed-tracing.md[154-158]

## Suggested approach
- Update ADR 0050’s Level 3 bullets to match the current contract (or explicitly mark the old “reasoning” statement as superseded/narrowed by ADR 0084).
- Keep ADR 0084/guide as the source of truth; ensure ADR 0050 can’t be read as promising reasoning capture.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Planned callout lacks issue link 📜 Skill insight ≡ Correctness
Description
The > **Planned:** callout for Level 3 content capture does not include a link to the tracking
issue. This violates the requirement that planned (not-yet-implemented) features be explicitly
marked and linked to an issue for traceability.
Code

docs/guides/infrastructure/distributed-tracing.md[R130-131]

> **Planned:** Level 3 content capture is not yet implemented. This section
-> documents the telemetry contract.
+> documents the activation contract.
Relevance

● Weak

Exact precedent: adding an issue link to the Level 3 “Planned” callout was requested and rejected.

PR-#3903

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The guide explicitly marks Level 3 as planned but does not provide an issue link in the `>
**Planned:**` callout. The compliance rule requires the callout format to include a link to the
relevant issue.

docs/guides/infrastructure/distributed-tracing.md[128-132]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The planned-feature callout for Level 3 content capture is missing a required issue link.

## Issue Context
Planned features must use the `> **Planned:**` blockquote format *and* include a link to the relevant issue.

## Fix Focus Areas
- docs/guides/infrastructure/distributed-tracing.md[128-132]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Guide in infrastructure/ directory 📜 Skill insight ⌂ Architecture
Description
docs/guides/infrastructure/distributed-tracing.md is a documentation guide but is not placed under
docs/guides/admin/ or docs/guides/user/ as required. This breaks the guide directory/audience
placement policy and makes the guide harder to classify and maintain.
Code

docs/guides/infrastructure/distributed-tracing.md[R128-131]

## Enabling content capture (Level 3)

> **Planned:** Level 3 content capture is not yet implemented. This section
-> documents the telemetry contract.
+> documents the activation contract.
Relevance

● Weak

Close precedent: requests to move non-admin/user guides out of disallowed subdirs were explicitly
rejected.

PR-#5454
PR-#5502
PR-#5457

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires every guide under docs/guides/ to be placed in either admin/ or
user/. The modified guide remains at docs/guides/infrastructure/distributed-tracing.md, which
violates that directory constraint.

docs/guides/infrastructure/distributed-tracing.md[1-5]
Skill: writing-user-docs

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`docs/guides/infrastructure/distributed-tracing.md` is under `docs/guides/infrastructure/`, but guides must live under either `docs/guides/admin/` or `docs/guides/user/` and target a single audience.

## Issue Context
This PR modifies the guide, so it must be brought into compliance with the guide placement policy.

## Fix Focus Areas
- docs/guides/infrastructure/distributed-tracing.md[1-5]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context
✅ Compliance rules (platform): 54 rules

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/guides/infrastructure/distributed-tracing.md
Comment thread docs/ADRs/0084-level-3-content-capture-activation-contract.md Outdated
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dhshah13

dhshah13 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Both findings addressed in c7d43c3.

Allow-list underspecified: correct — ADR 0082 governs mint workflow refs, so it cannot supply matching semantics for an OTLP endpoint allow-list, and leaning on it left a security gate ambiguous. ADR 0084 now defines them directly: comma-separated host or host:port, matched exactly and case-insensitively, no wildcards and no scheme/path matching, unset or empty means no capture request can succeed. The no-wildcard rule is stated as a decision rather than an implementation detail — a wildcard entry would let an unreviewed subdomain receive content. ADR 0082 remains cited only as precedent for the operational shape (default-minimal, CLI-managed, surfaced in status). The guide mirrors the format.

Reasoning-capture contradiction: correct that a reader of ADR 0050 alone would still expect reasoning text. Since 0050 is accepted and its Decision cannot be rewritten, its annotation now records the narrowing explicitly — Level 3 serves the LLM-judge case with prompts, messages, and tool activity, and extending scope to reasoning requires a new ADR.

On the alternatives raised in the summary: full-URL allow-listing is worth revisiting if host-level granularity proves insufficient, but host-level matches the governance unit — sign-off is about which backend holds the content, not which path on it.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review findings (7 items, all MEDIUM severity) — see inline comments for details.

activation semantics unspecified: which configuration enables it, what happens
on partial configuration, and where content may flow. Content is the
highest-sensitivity telemetry fullsend emits (proprietary source, PII, tool
output), and the sibling agentic-ci harness demonstrated the failure mode this

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Unverified "sibling agentic-ci harness" incident cited as established fact

The Context section states as settled fact: "the sibling agentic-ci harness demonstrated the failure mode this gap invites: content capture enabled unconditionally by default." No other ADR, problem doc, or design doc in this repo documents or references this incident — the only other "agentic-ci" hits in the repo are unrelated UI-rendering comparisons. This is an external, unverified claim about another project's behavior used to justify the strictness of this contract, with no citation a reader could check.

Suggestion: Either link to where this was actually observed/discussed (an issue, incident writeup, or the agentic-ci repo itself), or soften the claim to something reviewable, e.g. "a comparable harness has shipped content capture enabled unconditionally by default, which is the failure mode this contract exists to prevent," with a source link.

rather than degrading silently:

1. **Operator opt-in (env):** `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`
set to `true` (or the equivalent `span_only`); `false` or unset is off,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Unverified span_only value for OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT

This states the operator opt-in env var accepts true "(or the equivalent span_only)" as if that is an already-established literal value per the upstream OTel GenAI semantic conventions v1.37.0 pinned by this ADR. The OTel GenAI instrumentation content-capture design documents a multi-mode enum (span-only/event-only/span-and-event style values), but that doesn't confirm this exact literal string ("span_only", this casing/spelling) is what this specific env var accepts — naming is known to vary by language/instrumentation and the spec is still evolving. ADR 0050 (which this builds on) only ever documented true/unset. The same claim is repeated verbatim in the operator guide, and this ADR states "any other value is a hard error," so an incorrect assumption here directly affects implementation behavior.

Suggestion: Before merging, verify against the actual OTel GenAI semantic-conventions spec/instrumentation source (pinned at v1.37.0) exactly which literal values this env var accepts (case and spelling). If span_only is not correct, drop the parenthetical or fix it; if correct, cite the source so implementers don't have to re-derive it.

sensitivity varies by agent and repository, and the harness file's
CODEOWNERS review is the consent mechanism — which is why this knob lives
on the harness rather than `config.yaml` under
[ADR 0080](0080-config-yaml-vs-agent-env-var-scope.md)'s placement rule.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Citing ADR 0080's placement rule for a harness field shape ADR 0080 never defines

Condition 2 introduces telemetry.content_capture: true as a new structured harness field and justifies its placement by invoking "ADR 0080's placement rule." ADR 0080 draws a binary line between exactly two surfaces: a plain config.yaml field with no {AGENT}_ prefix (pipeline/dispatch policy), or an {AGENT}_-prefixed env var delivered via env.runner/env.sandbox per ADR 0049 (single-agent behavior tuning). telemetry.content_capture: true is neither — it's a structured, non-prefixed, non-env-var harness key, a third shape ADR 0080's decision text doesn't address (there's also no existing Telemetry/ContentCapture field precedent to fall back on). Citing ADR 0080 as though it already sanctions this field's exact placement stretches a rule that only covers two specific mechanisms.

Suggestion: Either scope the claim down ("a new harness field shape not covered by ADR 0080's two surfaces; chosen because...") and justify it directly, or use an {AGENT}_CONTENT_CAPTURE-style env var in env.runner/env.sandbox instead, which would let this condition ride the existing ADR 0080/0049 placement rule without stretching it.

Content flows over OTLP export only. `run-telemetry.jsonl` keeps its
documented metadata-only contract, enforced by an attribute allow-list at the
file exporter. Captured content is assembled post-iteration from the
transcript and harness inputs into OTel GenAI semantic-convention aggregated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Full multi-turn content aggregated into one span attribute may hit backend/collector limits

The Decision commits to assembling "system prompts, user messages, per-turn assistant text, tool arguments, and tool results" (potentially file contents / command output) into a single OTel GenAI semantic-convention aggregated attribute per per-iteration agent span, for a whole run's content. Span attribute values are commonly size-limited by collectors/backends, and aggregating an entire multi-turn transcript's tool output into one attribute is exactly the kind of point where such a limit could silently truncate or drop content, or exceed backend ingestion limits. This isn't addressed anywhere in the ADR, and unlike ADR 0050 there's no "Deferred to implementation" list to park it in.

Suggestion: Note explicitly in the ADR (or add a deferred-to-implementation item) how the design handles per-backend attribute size limits — e.g., truncation policy, splitting across multiple attributes, or falling back to span events/log records if a size ceiling is hit — so implementers don't discover this only after building against the aggregated-attribute shape.


- Content cannot flow by accident: no single variable, file, or workflow
edit enables capture, and every implementation PR merges inert.
- The non-production restriction on content is mechanically enforced — the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Dangling "non-production restriction" reference never established in the Decision

This Consequences bullet states "The non-production restriction on content is mechanically enforced — the allow-list, not prose, decides where content may go..." but no production/non-production distinction is defined anywhere in this ADR's Context or Decision ("production" doesn't appear outside this one bullet). The Decision only describes three conditions (operator env opt-in, harness consent, allow-listed endpoint host) with no production/non-production framing. A reader relying on Consequences would believe there's a policy restricting capture to non-production backends that the Decision never actually establishes.

Suggestion: Either add an explicit statement in the Decision establishing what "non-production" means (e.g., the allow-list is expected to hold only non-production endpoints, enforced via required sign-off) or reword this bullet to drop the "non-production" framing: "Content cannot flow to an arbitrary destination — the allow-list, not prose, decides where content may go, and every entry requires a recorded sign-off."

@@ -128,27 +128,39 @@ export OTEL_EXPORTER_OTLP_TRACES_HEADERS="authorization=Basic%20${CREDS_B64},x-m
## Enabling content capture (Level 3)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] "Guide-link policy" cited to justify omitting ADR links does not exist and contradicts current practice

The PR description says this rewritten Level 3 section has "no ADR links, per the guide-link policy." No such policy is documented anywhere in this repo — not in docs/contributing/adrs.md, not in the writing-adrs skill, not in AGENTS.md. It also contradicts existing practice: docs/guides/infrastructure/layered-config-reference.md, docs/guides/dev/e2e-testing.md, docs/guides/dev/testing-workflows.md, docs/guides/dev/behaviour-testing.md, and docs/guides/user/jira-integration.md all link directly to specific ADRs by number and path. This reads as an invented convention asserted as an established rule.

Suggestion: Either link ADR 0084/0085 from this Level 3 section (matching the convention used elsewhere in docs/guides/), or, if there's an intended distinction between reference guides (which link ADRs) and operator/how-to guides (which don't), document that rule in docs/contributing/ so it can be checked in future reviews instead of asserted per-PR.

## Annotations

**2026-08-05:** [ADR 0084](0084-level-3-content-capture-activation-contract.md)
applies the secret redactor to Level 3 span content as an output-side

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Annotation claims a consistency the ADR 0084 mechanism doesn't clearly establish

This annotation says applying the secret redactor to Level 3 content is "consistent with this ADR's framing: access control remains the security boundary, and a redaction hit is a breach signal, not the defense." But this ADR's actual mechanism (per its Consequences: "Credential detection in JSONL files surfaces isolation breaches... as incidents rather than silently redacting them") is binary and non-transformative — on a hit the entire JSONL is suppressed, content is never sanitized and shipped. ADR 0084's Level 3 mechanism reads differently: content "passes through secret redaction at assembly" and "the export path strips any content attribute lacking the redaction marker" — implying redaction actively gates what leaves over OTLP to an external, allow-listed-but-still-third-party endpoint. That makes the redactor function as a preventive control on the egress path, not merely a detector paired with access-control-as-the-real-boundary — arguably the opposite framing from this ADR. ADR 0084 also never states precisely what happens to content on a hit (drop the flagged attribute vs. mask-and-still-export), so this "consistent framing" claim can't actually be verified against ADR 0084's own text.

Suggestion: Either (a) have ADR 0084 specify precisely what happens to content on a redaction hit (drop just the flagged attribute vs. mask-and-still-export), or (b) soften this annotation to acknowledge that Level 3's export-to-external-backend context genuinely requires redaction to act as a defense, not merely a signal, since there's no post-hoc suppression option once content has left over OTLP the way there is for a stored JSONL file.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activation contract's structure is sound — three gates with distinct owners, fail-closed, single fullsend-owned export pipeline. But verifying ADR 0085 against the current fullsend-ai/agents harnesses shows the denylist as written breaks every existing agent, and two of its guarantees rest on unstated assumptions. Four comments inline; the first is blocking-level, the rest are wording/spec-gap fixes.


- **Keys:** deny `OTEL_*`, `CLAUDE_CODE_*`, and `TRACEPARENT` among
`env.sandbox` keys, including forge-merged harness bases.
- **Destinations:** deny `host_files` destinations that the sandbox sources

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this denylist hard-fails every existing harness in fullsend-ai/agents, and the migration path it assumes is circular:

  • Every agent harness mounts env files into the denied destination: common/env/gcp-vertex.env → /sandbox/workspace/.env.d/gcp-vertex.env, env/<agent>.env → .env.d/<agent>.env. Denying .env.d/* outright is a day-one pre-flight failure for all of them.
  • The natural migration is env.sandbox keys (ADR 0055's delivery) — but gcp-vertex.env exists to set CLAUDE_CODE_USE_VERTEX=1, and the CLAUDE_CODE_* prefix is denied there too. The variable that selects the inference backend is left with no valid configuration surface at all. The Consequences section says harnesses "must migrate to the supported configuration surfaces" but for this key no such surface exists.
  • Prefix-matching also confiscates legitimate use: CLAUDE_CODE_ENABLE_TELEMETRY + metrics export to a user's own collector is a documented runtime feature (cost tracking), unrelated to Level 3 content.

Suggestion: deny exact keys that carry content/export-routing risk (OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, OTEL_EXPORTER_OTLP_*, TRACEPARENT), and move runtime-provider/operational config behind fullsend-owned first-class knobs (e.g. runtime.provider: vertex, a telemetry: block) that the runner translates into env from trusted code — shipped before the deny takes effect. Otherwise this ADR contradicts the platform's own env delivery mechanism the moment it lands.


- Closes a live gap: a harness can no longer enable the runtime's native
content telemetry or redirect its export with no fullsend gate involved.
- ADR 0084's activation contract cannot be bypassed from inside the sandbox.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This consequence is conditional on an assumption the ADR never states: a trusted sandbox image. image: is an unvalidated string in the harness schema — no allowlist, no digest pinning, and allowed_remote_resources covers composition URLs, not container images. A harness pointing at a custom image can bake OTEL_* into the image env, pre-populate .env.d/, or ship a doctored .claude/settings.json — all below this pre-flight's line of sight, making the bypass-from-inside claim false as written.

Either state the assumption as a consequence ("guarantees are conditional on a platform-trusted sandbox image; image trust is tracked separately") or add the missing control (image reference allowlist and/or digest pinning). Without one of those, the denylist's real value — collapsing the attack surface to a few loud, reviewable artifacts (image ref, policy hosts) instead of many quiet env lines — should be what this bullet claims, not impossibility.

3. **Allowlisted destination:** the resolved OTLP traces endpoint's host —
`host` or `host:port`, matched exactly and case-insensitively, no
wildcards and no scheme or path matching — must appear in the
comma-separated `FULLSEND_CONTENT_CAPTURE_ALLOWED_ENDPOINTS` org

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The org-variable gate has no owner in per-repo mode, and the ADR doesn't say what happens there. GitHub org variables don't exist for personal-account repos, and the repo-scoped CLI subcommands (correctly) can't manage org settings. So either Level 3 is silently impossible for per-repo installs (allow-list permanently empty), or an implementation falls back to a repo variable — which collapses gates 2 and 3 into the same owner and quietly deletes the dual-consent property this contract is built on.

Please state the per-repo story explicitly: either "Level 3 requires org-mode installation" as a listed consequence, or name who the second, non-repo owner is in per-repo mode. Also worth requiring allow-list entries to reference their governance sign-off record (rather than leaving the link to convention), since a bare host:port gives the approving admin nothing to evaluate.


Content flows over OTLP export only. `run-telemetry.jsonl` keeps its
documented metadata-only contract, enforced by an attribute allow-list at the
file exporter. Captured content is assembled post-iteration from the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence carries the design's most important invariant, and it's easy to miss mid-paragraph: content is assembled runner-side from the transcript fullsend already extracts — the runtime's native in-sandbox telemetry is never enabled, and ADR 0085 makes it unconstructible. Two careful readers of this PR independently first misread Level 3 as enabling Claude Code's own content capture inside the sandbox.

Suggest promoting it to a headline statement in the Decision (e.g. "The agent runtime's native content telemetry is never enabled; Level 3 re-exports the existing transcript through fullsend's own pipeline") so the single-pipeline property — no second exporter, no redaction bypass, no trace-identity drift — reads as the contract's foundation rather than an implementation detail.

@waynesun09
waynesun09 requested a review from rh-hemartin August 5, 2026 21:14
dhshah13 added a commit to dhshah13/fullsend that referenced this pull request Aug 6, 2026
Nine findings from review, all accepted:

- Cite the agentic-ci content-on-by-default claim to its public source
  instead of asserting it as unreferenced fact.
- State that the capture variable's value set is fullsend-defined — the
  upstream conventions require opt-in but do not standardize values.
- Stop citing ADR 0080's placement rule for a harness-field shape it does
  not cover; justify the surface directly (per-agent review semantics).
- Define what happens on a redaction hit (mask and record a finding;
  encoding evasion drops the part whole) and state per-kind size budgets
  with backend-limit validation as a pilot precondition.
- Drop the dangling non-production framing from Consequences.
- State the per-repo story: the allow-list is an organization variable so
  its owner is distinct from CODEOWNERS; personal-account repositories
  cannot enable Level 3. Entries record a sign-off reference.
- Promote the single-pipeline invariant to a headline Decision statement:
  the runtime's native content telemetry is never enabled.
- ADR 0085: replace prefix denial with an enumerated denied set (content
  flags, OTEL_EXPORTER_*, trace identity) so provider selection and
  metrics-only telemetry via .env.d keep working — the previous text
  hard-failed every existing harness with no migration surface for
  CLAUDE_CODE_USE_VERTEX. .env.d mounting stays supported; its content is
  scanned for denied assignments. State the trusted-image assumption.
- ADR 0021 annotation: acknowledge redaction acts as a preventive egress
  control at Level 3, not only a breach signal.
- Guide: link ADRs 0084/0085 matching other guides' practice.

Reviewed-by findings: waynesun09 on PR fullsend-ai#5947.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
@dhshah13

dhshah13 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

All findings addressed in 4311fa0. Point by point:

agentic-ci claim — cited to its public source: src/agentic_ci/harness.py sets the runtime's content-logging variables unconditionally (merged in opendatahub-io/agentic-ci#84).

span_only value — correct that upstream doesn't standardize it. The ADR now states the value set is fullsend-defined: the conventions require content capture to be opt-in but leave this variable's values to implementations; span_only is fullsend's alias, defined here, not inherited. Guide mirrors the wording.

ADR 0080 stretch — accepted. The claim is scoped down: a structured harness field is a third shape 0080's two surfaces don't cover, chosen because neither an org-wide config.yaml field nor an {AGENT}_ env var carries the per-agent CODEOWNERS review semantics this consent requires. The env-var alternative was considered and rejected for exactly that reason — workflow env has no per-agent review gate.

Attribute size limits — the Decision now states per-kind size budgets with structure-preserving truncation markers, and names backend/collector limit validation as a pilot precondition tracked in #5948.

Dangling non-production reference — reworded per your suggestion; the prod/non-prod sequencing lives in #5948's preconditions, not this ADR.

Guide-link policy — you're right that no such documented policy exists; I inferred it from f058f7d and overstated it. The Level 3 section now links ADRs 0084/0085, matching layered-config-reference and the other guides. (If maintainers do want a link-free rule for operator guides, that belongs in docs/contributing/ as you say — not asserted per-PR.)

0021 annotation — accepted, and 0084 now specifies hit behavior: a hit masks the matched value and records a security finding; encoding evasion drops the affected part whole. The annotation now acknowledges the real difference: on the OTLP egress path there is no post-hoc suppression, so redaction acts as a preventive control there, in addition to 0021's breach-signal role. Access control at the backend remains the primary boundary.

0085 circular migration — your strongest catch; the prefix denial was wrong. The Decision now denies an enumerated set only: the content-capture flags (exact keys), OTEL_EXPORTER_* (export routing), and TRACEPARENT/TRACESTATE. CLAUDE_CODE_USE_VERTEX, metrics-only telemetry, and .env.d/ mounting are explicitly untouched — the file scan gates what mounted env files may set, not whether they may exist. No existing harness in fullsend-ai/agents fails under the revised rule.

Trusted-image assumption — stated as a consequence: guarantees are conditional on a platform-trusted image; the denylist's value is collapsing the attack surface to a few loud, reviewable artifacts; image trust (allowlisting/digest pinning) is a separate control.

Per-repo owner — stated in the Decision: the allow-list is an organization variable precisely so its owner is distinct from the harness's CODEOWNERS; personal-account repositories have no second owner and cannot enable Level 3. Allow-list entries record a sign-off reference alongside the change.

Single-pipeline invariant — promoted to a headline Decision statement: "The agent runtime's native content telemetry is never enabled" — Level 3 re-exports the transcript fullsend already extracts through its own pipeline, and ADR 0085 makes the in-sandbox alternative unconstructible.

ADR 0084 is now 91 content lines — over the 80 target, all of it review-driven substance. If that reads as more than one decision, the pre-agreed fallback is splitting activation/gates from content-shape/redaction into a sibling ADR; the composite is defended first.

… denylist

ADR 0050 defined Level 3 (content in spans, explicit opt-in) but left the
activation semantics unspecified. ADR 0084 records the activation contract:
dual consent (operator env opt-in + per-agent harness consent) plus an
allow-listed OTLP destination whose entries record governance sign-off,
fail-closed on unsatisfiable capture requests, content over OTLP export
only, redaction with findings, no reasoning capture. ADR 0085 extends the
sandbox env denylist to OTEL_/CLAUDE_CODE_/TRACEPARENT and host_files
destinations so the contract cannot be bypassed from inside the sandbox.

Living docs updated per the writing-adrs skill: annotations on ADRs 0050
and 0021, the operator guide's Level 3 section rewritten to the contract,
an architecture.md Decided entry, and a problem-doc cross-reference.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
…cope narrowing

Review feedback on the Level 3 activation contract:

- ADR 0082's allow-list governs mint workflow refs, so it cannot supply
  matching semantics for an OTLP endpoint allow-list. Define them in
  ADR 0084 instead: comma-separated host or host:port, matched exactly and
  case-insensitively, no wildcards — a wildcard entry would let an
  unreviewed subdomain receive content. ADR 0082 stays as precedent for the
  operational shape only.
- ADR 0050's Level 3 section still promises reasoning text to LLM-judge
  scorers. Extend its annotation to record that ADR 0084 narrows the
  captured scope, so a reader of ADR 0050 alone is not misled about what
  content Level 3 exports.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
Nine findings from review, all accepted:

- Cite the agentic-ci content-on-by-default claim to its public source
  instead of asserting it as unreferenced fact.
- State that the capture variable's value set is fullsend-defined — the
  upstream conventions require opt-in but do not standardize values.
- Stop citing ADR 0080's placement rule for a harness-field shape it does
  not cover; justify the surface directly (per-agent review semantics).
- Define what happens on a redaction hit (mask and record a finding;
  encoding evasion drops the part whole) and state per-kind size budgets
  with backend-limit validation as a pilot precondition.
- Drop the dangling non-production framing from Consequences.
- State the per-repo story: the allow-list is an organization variable so
  its owner is distinct from CODEOWNERS; personal-account repositories
  cannot enable Level 3. Entries record a sign-off reference.
- Promote the single-pipeline invariant to a headline Decision statement:
  the runtime's native content telemetry is never enabled.
- ADR 0085: replace prefix denial with an enumerated denied set (content
  flags, OTEL_EXPORTER_*, trace identity) so provider selection and
  metrics-only telemetry via .env.d keep working — the previous text
  hard-failed every existing harness with no migration surface for
  CLAUDE_CODE_USE_VERTEX. .env.d mounting stays supported; its content is
  scanned for denied assignments. State the trusted-image assumption.
- ADR 0021 annotation: acknowledge redaction acts as a preventive egress
  control at Level 3, not only a breach signal.
- Guide: link ADRs 0084/0085 matching other guides' practice.

Reviewed-by findings: waynesun09 on PR fullsend-ai#5947.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
Compression only — every review-driven point survives with identical
meaning. The prior revision wrote each fix with a self-defending rationale
clause; ADR style states the decision with one clause of why.

Signed-off-by: Dharit Shah <dhshah@redhat.com>
@dhshah13

Copy link
Copy Markdown
Contributor Author

@rh-hemartin can you take a look?

@rh-hemartin rh-hemartin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about forcing users to create their own harness to send traces, I get the point of making sure they opt-in very explictly, but couldn't we move that to the config.yaml?

@dhshah13

Copy link
Copy Markdown
Contributor Author

Sending traces never requires a harness: Level 1 is zero-config and Level 2 is
one env var (OTEL_EXPORTER_OTLP_ENDPOINT) — see the level table in the
tracing guide. Condition 2 exists only for Level 3 content — prompts, tool
output — which is unimplemented and the highest-sensitivity data fullsend
would ever export.

A top-level config.yaml field is the wrong shape for that: 0084's premise is
that content sensitivity is per-agent, and once the operator env opt-in is
set, a repo-wide field makes every agent request capture — coupling the whole
fleet to the strictest harness. The workable reading is a per-agent flag on
the agents: entry. Measured against the code, that saves exactly one file:
an enabled agents: entry must carry a source
(internal/config/config.go:411), URL sources must carry a #sha256 pin
(config.go:439), so a stock install — no harness files, no agents:
entries, first-party agents resolved from agents@v0 at runtime — pays
registration plus a pin under either placement. The harness path adds only
the ~4-line base: overlay that carries the flag. And the entry-flag costs
more than it saves: it needs an ADR 0080 amendment (a per-agent config.yaml
field is the shape 0080 calls a misplacement signal), a novel overlay-only
merge rule so a config.base.yaml preset can't import consent (agents: is
keyed-merged across layers), and it detaches consent from content — the entry
is a pointer, so a local-path harness can change while a standing flag keeps
consenting to it.

That last point is the principle: capture consent should bind to the exact
reviewed content definition. In the harness, the flag and the agent
definition travel in one integrity-pinned document. The same reason rules out
a consent-only entry (name + flag, resolution stays on the fallback): one
line, but it consents to whatever v0 becomes. The pin is the consent.

What is genuinely missing is the recipe. Consenting for a first-party agent
is seven lines across two files:

# .fullsend/config.yaml
agents:
  - name: code
    source: harness/code-capture.yaml
# .fullsend/harness/code-capture.yaml
base: https://raw.githubusercontent.com/fullsend-ai/agents/<sha>/harness/code.yaml#sha256=<hash>
telemetry:
  content_capture: true

I'll add that to the operator guide in this PR, reword 0084's "three
configuration steps" consequence to be honest about what a stock install
pays, and state that telemetry.content_capture does not inherit through
base: composition (precedent: allowed_remote_resources,
compose.go:596-600) — an upstream harness must never pre-consent for its
consumers. If you still want it config-side with these trade-offs on the
table, the per-entry flag is the only viable shape and I'd respin 0084 and
amend 0080 together — but the harness is the right place for this one.

Review follow-up to fullsend-ai#5947 (rh-hemartin): sending traces never requires a
harness — only Level 3 content consent does. Make that cost honest and
the path concrete:

- ADR 0084: state that telemetry.content_capture never inherits through
  base: composition (the allowed_remote_resources exemption), so an
  upstream harness cannot pre-consent for repos that compose from it.
- ADR 0084: reword the enablement consequence — a stock install has no
  harness files, so consent costs an agents: registration plus a pinned
  base: overlay, not "three configuration steps".
- Operator guide: show the two-file consent recipe and why the pin is
  the consent object.
@rh-hemartin

Copy link
Copy Markdown
Member

I would ask this on the public forum and debate this on the collaborators meeting. I won't be there but I'm fine with any decision. That being said I think this is too much, users has been complaining about complexity, so having them create a harness to enable finer telemetry looks like the wrong direction.

@dhshah13

dhshah13 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Agreed on the complexity — and it goes further than stated: the pinned overlay is not a one-time cost. Every upstream agent release needs a pin bump, so consenting today also takes the agent off the v0 update channel and its prompt/guardrail fixes.

Proposal, in config.yaml as you suggested:

telemetry:
  content_capture: [code]

One reviewed field, no harness authoring. One rule keeps the failure direction safe: a listed name must resolve to a config-registered agents: entry with a #sha256-pinned source — otherwise the capture request fails before sandbox creation, per the existing contract. Without it, a bare name list fails open: the name is a moving referent (floating v0, source rebinds), so capture would stay on for agent content nobody re-reviewed. With it, consent and the pin it binds to sit in the same reviewed file — and once ADR 0058 Phase 5 removes the fallback and every
agent is a pinned entry anyway, the extra cost over a bare list goes to zero.

Rides along in the respin: the field never resolves from config.base.yaml, so a vendor preset cannot pre-consent adopting repos; on GitLab it joins the kill_switch/roles FETCH_HEAD read; a listed name that doesn't validate against the registered agent set is a hard error, not a silent no-op.

@ascerra ascerra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing this forward. A few thoughts from the eval / “what’s on the trace” side.

What’s landing for me: gating where content can leave feels right. Operator env + harness consent + org allow-list so a single flip can’t route conversations to whatever OTLP host is configured. ADR 0085’s enumerated denylist also looks useful on its own even if Level 3 takes a bit.

Where I’m less sure: mostly for eval measurements (#6036): that work treats the trace as the record of what happened (run-telemetry.jsonl, and the same data over OTLP when configured). Eval scores are computed from that record afterward (using run-telemetry.jsonl to remain vendor-agnostic, OTLP if a user chooses to score traces using a specific vendor). If Level 3 content is OTLP-only and drops reasoning, anything that wants to score content has to couple to a specific backend instead of the portable local contract. I’d rather when L3 is on, the full useful payload (including reasoning) show up on the trace — file and export — still gated by who can see the artifact / job / allow-listed endpoint. ADR 0021 already puts full reasoning in transcript JSONL under that access model; I don’t think we want a thinner second shape on OTEL.

On fail-closed before sandbox: I’m with you on not silently shipping content somewhere bad. Aborting the whole agent feels heavier than how Level 2 treats a dead OTLP endpoint (ADR 0050). Some options that still protect routing:

  1. Degrade to metadata-only (run continues; no content export)
  2. Hard-fail only the content export path, not sandbox creation
  3. Keep fail-closed only for “we’re about to attach content to an export,” not “abort the agent”

Level 3 activates only when three independently-owned conditions all hold.
While any of them is absent or off, runs stay metadata-only with no error.
A run that affirmatively requests capture — conditions 1 and 2 both on —
but cannot satisfy the rest of the contract fails before sandbox creation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m with you on not silently shipping content to a bad destination. I’m less sold that incomplete L3 config should abort the agent. Level 2 already fail-opens on a dead OTLP endpoint (ADR 0050). Could we instead:
(a) degrade to metadata-only
(b) hard-fail only content export
(c) fail-closed only at “about to attach content,” not before sandbox?

@dhshah13 dhshah13 Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dead-endpoint comparison lands on the wrong axis: every abort in 0084 is a static config check — an allowlisted-but-dead endpoint under L3 degrades exactly as L2 does today (retry, drop, stderr; run unaffected). The concern survives on two other axes, though: main warns-and-continues even on static
telemetry misconfig (internal/telemetry/telemetry.go:159-161), and the allowlist gate couples run availability to an org-side owner — removing an allowlist entry (legitimate revocation) aborts every consenting repo's runs until their config changes.

Respin direction: split by cause. Same-repo contradictions — consent on with the secret redactor disabled, invalid opt-in value — stay pre-sandbox hard errors: locally introduced, locally fixable, genuinely contradictory. Org-side allowlist absence or revocation degrades to metadata-only with a mandatory machine-readable marker (span attribute + workflow annotation) — your (b), loud, keeping the no-silent-degrade invariant. (c) buys nothing: all gates are static and pre-sandbox-evaluable, so deferring the check just burns a sandbox.

[ADR 0085](0085-sandbox-environment-variable-denylist.md) makes the
in-sandbox alternative unconstructible.

Content flows over OTLP export only; `run-telemetry.jsonl` keeps its

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main the file and OTLP are two exporters of the same SDK spans (#4510, dev guide). The JSONL is the local/artifact view when you aren’t looking at the backend. Making L3 OTLP-only breaks that “same trace, two views” story and means eval-measure (#6036) can’t see content on the portable primary-fact artifact without coupling to a specific backend. Prefer: when L3 is on, both exporters get the content (still gated by allow-list + who can see artifact/job/endpoint).

@dhshah13 dhshah13 Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed the premise: one provider, two processors (internal/telemetry/telemetry.go:153,166), the file exporter copies attributes verbatim, and today's only asymmetry runs the other way — file OTLP, via the unsampled-parent suppression. 0084 as written inverts that for the first time, and #6036 reads run-telemetry.jsonl as the primary fact, so the coupling concern is real.

One wrinkle in "both exporters get the content (still gated by allow-list)": if file content requires the endpoint allow-list, an OTLP gate controls a file that never leaves the runner — and L1-only installs still can't eval content. Respin direction: two lanes. Content assembled once, redacted at assembly, byte-identical to both sinks, strip-unmarked backstop at both exporters; the file lane activates on conditions 1+2 (+ redactor), the OTLP lane additionally requires the allowlist. File-lane exposure is bounded by the artifact bundle that already carries the full redacted transcript — with conditions 1+2 it is strictly more gated than the transcript is today. The trade-off to state explicitly: per-repo/personal installs get file-lane content but never egress — the dual-owner gate stays exactly where egress creates a new boundary.

activation semantics unspecified: which configuration enables it, what happens
on partial configuration, and where content may flow. Content is the
highest-sensitivity telemetry fullsend emits (proprietary source, PII, tool
output), and a comparable harness ships the failure mode this gap invites:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you adding this statement about agentic-ci here? Are you using it as a cautionary tail showing an example of what not to do?

@dhshah13 dhshah13 Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — cautionary example: content capture enabled unconditionally is the failure mode the contract exists to prevent, and the earlier review round asked that the claim be checkable, hence the link.

issue). Redaction runs at assembly: a hit masks the value and records a
security finding, encoding evasion drops the affected part whole, and export
strips any content attribute lacking the redaction marker.
Reasoning/thinking text is not captured — a deliberate narrowing of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d drop this narrowing. ADR 0021 already stores full reasoning in transcript JSONL; the boundary is who can download the artifact / see the run — not a thinner OTEL shape. For eval / LLM-judge use (ADR 0050 Level 3), reasoning is part of what makes the trace useful. Same access model as the rest of the content when L3 is enabled.

@dhshah13 dhshah13 Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the file lane, agreed — and the exclusion's own justification doesn't hold: ADR 0021 is the only ADR about reasoning and it decided default-exposed with access control as the boundary, and the same artifact bundle already carries full reasoning transcripts. "Treats most conservatively" comes out in the respin.

For OTLP, the 0021 annotation's point stands: once content leaves there is no post-hoc suppression. So rather than silent inclusion, reasoning gets a named consent bit — the consent shape distinguishes messages from reasoning, and the allowlist sign-off records whether reasoning-bearing traces may egress. That covers the eval case (reasoning on the portable artifact) without making external retention of reasoning an unnamed rider.

(dual consent plus an allow-listed destination, fail-closed). The environment
variable named here is honored with the value semantics defined there.
ADR 0084 also narrows the captured scope: reasoning/thinking text is **not**
captured, so the LLM-judge use case named above is served only by prompts,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the reasoning note on 0084 — please don’t bake the exclusion into 0050 via annotation until we’ve settled whether L3 is the full useful record or a prompts-only export. Ties to eval measurements (#6036) needing contentful primary facts.

@dhshah13 dhshah13 Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the reasoning thread on 0084: the exclusion comes out — reasoning rides the file lane with L3, with a named consent bit for OTLP egress. The 0050 annotation gets rewritten accordingly in the respin.

While any setting is absent or off, runs stay metadata-only with no error.
A run that requests capture (settings 1 and 2 on) but cannot satisfy the
rest — endpoint host not allow-listed, secret redactor disabled — fails
before the sandbox is created: content is never silently enabled, and a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ask as on ADR 0084: degrade / fail content-export-only rather than abort the agent. Keep the “never silently enable content” invariant.

@dhshah13 dhshah13 Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same resolution as the 0084 thread: cause-split — hard error on same-repo contradictions, loud metadata-only degrade on org-side allowlist absence or revocation.


When active, spans include system prompts, user messages, per-turn assistant
text, tool arguments, and tool results, all passed through secret redaction
before export. Reasoning/thinking text is not captured. Content flows only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fights the span-hierarchy note just below that “exported spans and the local file are two views of the same trace.” If L3 splits them, that sentence stops being true. Suggest aligning with: L3 on → same content on both views; access = artifact / job / allow-listed endpoint.

@dhshah13 dhshah13 Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two-lane resolution on the 0084 thread keeps that sentence true: content is assembled once, and both views are byte-identical wherever their lane's gates pass.

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reject both ADRs as scoped. Recommend dropping the activation-contract/denylist apparatus entirely and replacing it with a one-line boundary statement in the security threat model doc.

ADR 0084 — reject

The three-gate "independently owned conditions" premise doesn't hold under this repo's own architecture:

  • Gates 1 and 2 collapse to one actor. Operator env opt-in (gate 1, delivered as CI-native infra plumbing) and harness consent (gate 2, CODEOWNERS-gated) are both reachable by whoever has repo-write access to workflow/harness config. There's no separation between them.
  • Gate 3's ownership model is broken. It's specified as "an organization variable" whose "owner is distinct from the harness's CODEOWNERS," with personal-account repos structurally excluded. But ADR 0044 (Accepted) deprecated per-org install mode specifically to drop the admin:org scope requirement — per-repo is the sole supported model, deliberately scoped to repo+workflow only. An "org variable" gate reintroduces exactly the elevated-scope dependency 0044 removed, and for personal-account repos it's not a degraded gate, it's a permanent wall. The ADR also cites ADR 0082 as precedent for this gate's "operational shape," but 0082's actual analog (WORKFLOW_HOST_REPOS) is a mint-operator-owned env var, not a GitHub-org-owned one — different owner, different access model. As written, gate 3 has no mechanism that's both (a) actually distinct from repo-write and (b) consistent with the per-repo-only architecture.
  • The agentic-ci citation is weak support. It's cited as a cautionary precedent, but agentic-ci is converging into fullsend, not an adversarial input — the citation shows a self-inflicted failure mode is possible, not that fullsend faces it from an external actor.

Net: the activation contract asserts a security property — "content cannot flow to an arbitrary destination because a distinctly-owned gate approved it" — that doesn't structurally exist for either of fullsend's two install populations (personal: gate 3 unreachable; org: gate 3 collapses to the same actor as gates 1/2 whenever repo-write and org-admin coincide, which is common).

ADR 0085 — reject, don't respin

Independent of 0084, this doesn't hold up on its own terms either:

  • OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT is not a Claude Code variable. Checked against Claude Code's telemetry docs — it isn't documented anywhere. It reads like the generic upstream OTel GenAI semantic-convention name, not something the runtime actually reads. Denying it blocks nothing.
  • The real denied-set is incomplete, and the gaps are the dangerous ones. Claude Code's actual content-capture vars are OTEL_LOG_USER_PROMPTS, OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_TOOL_CONTENT. The Decision's denied set covers the first and third but omits OTEL_LOG_ASSISTANT_RESPONSES entirely. Worse: it also omits OTEL_LOG_RAW_API_BODIES, which dumps the full Anthropic Messages API request/response — entire conversation history — either inline or, via file:<dir>, as untruncated files written to disk. That's a bigger, uncovered hole than anything the denylist currently blocks.
  • This is a whack-a-mole problem against infrastructure fullsend doesn't own. Claude Code's telemetry surface grows over time (the docs version-tag several of these vars as recent additions). A denylist that has to track an upstream product's evolving env surface starts behind and stays behind — this isn't a one-time gap, it's a standing maintenance liability, and it just missed two vars on the first pass.
  • It defends against an actor who doesn't need this path anyway. The actor who could set a denied var via env.sandbox/.env.d is the same repo-write/harness-owning actor who already has broader capability inside the sandbox — a pre/post script curl, a custom skill that POSTs conversation content, anything executable. Closing five specific env var names doesn't reduce that actor's capability to exfiltrate; it just removes one path among many that were already open to someone at that trust level.

What to add instead

No new ADR. Add a scoping sentence to docs/problems/security-threat-model.md (or wherever the pipeline's guarantees are documented): fullsend's content-handling guarantees apply to its own extraction/redaction pipeline only; Claude Code's native OTel instrumentation is out of scope and left to the harness/operator's own env, same as any other in-sandbox capability.

Note there's nothing to gate today in the first place: telemetry.content_capture doesn't exist anywhere in the current harness schema — internal/harness/harness.go has no Telemetry field, and content_capture has zero hits in the repo. Fullsend's pipeline captures no content today because the capability isn't implemented, not because a settable field defaults to off. If a capture toggle is ever added, its default-off behavior is a one-line implementation fact — it doesn't need this contract to justify it.

@dhshah13

Copy link
Copy Markdown
Contributor Author

Closing — not respinning.

The core read holds: the contract asserts owner separation that doesn't
structurally exist. Gate 1 is a repo Actions variable (write access, no
diff, no CODEOWNERS review), and gate 3 collapses for a reason not yet
named in this thread — a repo-level variable shadows an org-level one of
the same name, so the distinct-owner property fails in org mode and
personal-account repos aren't walled out either.

0085's denied set is wrong on the facts in both directions.
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT is an upstream OTel
GenAI convention read by GenAI instrumentation libraries, not by Claude
Code — zero occurrences in the shipped binary, which also carries 69 other
OTEL_* names. Meanwhile OTEL_LOG_ASSISTANT_RESPONSES,
OTEL_LOG_RAW_API_BODIES, ENABLE_ENHANCED_TELEMETRY_BETA (a documented
alias of a key that is denied), CLAUDE_CODE_ENABLE_TELEMETRY and
OTEL_LOGS_EXPORTER are all live and uncovered — and
OTEL_LOG_RAW_API_BODIES=file:<dir> writes untruncated request/response
bodies to disk, which the OTEL_EXPORTER_* denial doesn't touch. An
enumerated list that missed that much on its first pass is the argument
against enumerated lists.

Level 3 is unimplemented, so nothing regresses by not deciding activation
now. It gets a fresh ADR when there's a pipeline to govern, written
against the surface as it stands then.

@dhshah13 dhshah13 closed this Aug 18, 2026
@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 18, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ❌ Failure · Started 7:05 PM UTC · Completed 7:05 PM UTC

Commit: ed9a50d · View workflow run →

@ascerra

ascerra commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

/fs-retro

@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 18, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ❌ Failure · Started 7:13 PM UTC · Completed 7:13 PM UTC

Commit: ed9a50d · View workflow run →

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.

4 participants