Skip to content

docs(adr): add ADR 0068 for privacy allowlist on poll input drivers#3428

Open
waynesun09 wants to merge 3 commits into
mainfrom
adr-poll-privacy-gate
Open

docs(adr): add ADR 0068 for privacy allowlist on poll input drivers#3428
waynesun09 wants to merge 3 commits into
mainfrom
adr-poll-privacy-gate

Conversation

@waynesun09

Copy link
Copy Markdown
Member

Summary

Adds ADR 0068, proposing a configurable privacy allowlist gate on jira-poll (ADR 0063) applied before NormalizedEvent construction — since jira-poll is the first dispatch input driver where the event source (an internal Jira project) and the dispatch target (potentially a public repo) do not share a trust boundary, and no sanitization exists in the current mapping.

Related Issue

Relevant to the fullsend poll implementation epic (#2263) — this ADR is intended to land before that implementation starts, since no jira-poll code exists yet.

Changes

  • New docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md: default-deny field allowlist, template-projected comment bodies, PII scanning as defense in depth, and a provenance hash for audit — configured via a new privacy_gate block under poll.input_drivers.
  • Updated docs/normative/normalized-event/v1/jira-poll-adapter.md to document the privacy_gate schema extension and reflect the gated mapping as the new default (previously unfiltered).
  • Surgical addition to docs/architecture.md noting the decision under the dispatch section.
  • Cross-referenced ADR 0068 from docs/problems/security-threat-model.md's existing "Indirect information disclosure" section, as the input-side complement to the existing output-side SecretRedactor/ADR 0022 mitigation.

Testing

  • make lint passes (stage changes first, then run)
  • Tests added/updated for new or modified logic — not applicable, no implementation exists yet; this is a design ADR

Checklist

  • PR title follows Conventional Commits (correct type, ! for breaking changes)
  • Commits are signed off (DCO) — human and human-directed agent sessions only
  • I wrote this contribution myself and can explain all changes in it

Assisted-by: Claude

ADR 0063 introduces jira-poll, the first dispatch input driver where
the event source (an internal Jira project) and the dispatch target
(potentially a public repo) do not share a trust boundary. The Jira
poll adapter maps issue fields into NormalizedEvent unfiltered --
comment bodies, labels, and other content flow directly into
agent-visible FULLSEND_WORK_ITEM_* env vars with no sanitization.

Add a configurable privacy allowlist gate applied inside jira-poll,
before NormalizedEvent construction: default-deny field allowlist,
template-projected comment bodies, PII scanning as defense in depth,
and a provenance hash for audit. Update the Jira poll adapter to
reflect the gated mapping as the new default, cross-reference the
threat model's existing output-side mitigations with this input-side
complement, and note the decision in architecture.md.

No jira-poll code exists yet (#2263 tracks implementation), so this
lands as design input before the driver ships rather than a retrofit.

Signed-off-by: Wayne Sun <gsun@redhat.com>
@waynesun09 waynesun09 requested a review from a team as a code owner July 7, 2026 22:09
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 10:10 PM UTC · Ended 10:18 PM UTC
Commit: e8381e3 · View workflow run →

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

docs(adr): add ADR 0068 privacy allowlist gate for poll input drivers

📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add ADR 0068 defining a default-deny privacy allowlist for jira-poll inputs.
• Extend Jira poll adapter spec with a privacy_gate config schema and new defaults.
• Cross-reference the decision in architecture docs and the security threat model.
Diagram

graph TD
  J[Jira project] --> D[jira-poll driver] --> G[privacy gate] --> N[(NormalizedEvent)] --> C[dispatch core] --> A[agent context]
  ADR["ADR 0068"] -. defines .-> G
  ADR -. updates .-> SPEC["jira-poll-adapter spec"]
  ADR -. referenced by .-> TM["security threat model"]
  ADR -. noted in .-> ARCH["architecture.md"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Enforce the allowlist in dispatch core (not input drivers)
  • ➕ Single enforcement point across all input drivers
  • ➕ Less per-driver implementation risk and drift
  • ➖ Sensitive data still traverses shared dispatch internals before being dropped
  • ➖ Harder to express source-specific fields and transformations (e.g., Jira comment templating)
2. Rely on output-side redaction only (SecretRedactor / ADR 0022)
  • ➕ No new config/schema surface for poll drivers
  • ➕ Keeps input mapping simple
  • ➖ Does not prevent indirect disclosure via agent prompts/context before output
  • ➖ Does not address over-collection/minimization for cross-trust-boundary sources
3. Make the gate opt-in (default = current unfiltered mapping)
  • ➕ Avoids breaking expectations for early adopters expecting full Jira pass-through
  • ➕ Less config churn during initial rollout
  • ➖ Unsafe-by-default for the first cross-boundary driver, increasing retrofit risk
  • ➖ Encourages implementations to ship without the gate enabled

Recommendation: The PR’s approach (input-driver privacy gate, default-deny by default) is the best fit for cross-boundary sources because it minimizes sensitive data propagation before shared dispatch/agent context. If anything, ensure the eventual implementation reuses a shared library/util across poll drivers so the privacy_gate semantics stay consistent and don’t diverge per driver.

Files changed (4) +157 / -2

Documentation (4) +157 / -2
0068-privacy-allowlist-for-poll-input-drivers.mdAdd ADR 0068 defining a privacy allowlist gate for poll drivers +129/-0

Add ADR 0068 defining a privacy allowlist gate for poll drivers

• Introduces an Accepted ADR establishing an input-side, default-deny allowlist for 'jira-poll' (and similar drivers) before 'NormalizedEvent' construction. Defines the proposed 'poll.input_drivers[].privacy_gate' config shape, including 'allowed_fields', 'comment_template', PII scanning guidance, and a provenance hash for auditability.

docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md

architecture.mdDocument privacy allowlist decision in dispatch/poll architecture section +4/-0

Document privacy allowlist decision in dispatch/poll architecture section

• Adds a brief architectural note that poll input drivers apply a privacy allowlist prior to constructing 'NormalizedEvent'. Links the note to ADR 0068 for rationale and details.

docs/architecture.md

jira-poll-adapter.mdExtend Jira poll adapter spec with 'privacy_gate' and new defaults +23/-2

Extend Jira poll adapter spec with 'privacy_gate' and new defaults

• Updates the normative mapping to make 'state.labels' explicitly subject to gating and adds 'state.privacy_gate.source_hash' semantics. Introduces a new “Privacy gate” section documenting 'allowed_fields' and 'comment_template', and clarifies that default behavior is now allowlisted (with opt-out via config).

docs/normative/normalized-event/v1/jira-poll-adapter.md

security-threat-model.mdAdd threat-model mitigation referencing input-side allowlisting +1/-0

Add threat-model mitigation referencing input-side allowlisting

• Adds a new bullet under “Indirect information disclosure” calling out input-side allowlisting for cross-trust-boundary sources. Cross-references ADR 0068 as the complementary control to existing output-side mitigations.

docs/problems/security-threat-model.md

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Site preview

Preview: https://8c7daca4-site.fullsend-ai.workers.dev

Commit: b40f6b093a81d3d677ffbef5ed0d416238045246

@qodo-code-review

qodo-code-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 54 rules

Grey Divider


Action required

1. Schema rejects privacy_gate state ✓ Resolved 🐞 Bug ≡ Correctness
Description
jira-poll-adapter.md now specifies emitting state.privacy_gate.source_hash, but
normalized-event.schema.json defines state with additionalProperties: false and does not
include privacy_gate, so schema validation will fail for any event that includes this field.
Code

docs/normative/normalized-event/v1/jira-poll-adapter.md[R83-84]

+- `state.privacy_gate.source_hash` — SHA-256 of the pre-gate event payload,
+  present when the privacy gate applied a transformation. See below.
Relevance

⭐⭐⭐ High

Repo enforces strict schema correctness (additionalProperties/required invariants); similar schema
tightening accepted in PR #2650.

PR-#2650

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The adapter doc requires a new state.privacy_gate.source_hash, but the schema disallows any
unknown keys under state, so a NormalizedEvent containing that field cannot validate against the
current v1 schema.

docs/normative/normalized-event/v1/jira-poll-adapter.md[76-103]
docs/normative/normalized-event/v1/normalized-event.schema.json[289-355]

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/normative/normalized-event/v1/jira-poll-adapter.md` documents a new `state.privacy_gate.source_hash` field, but `docs/normative/normalized-event/v1/normalized-event.schema.json` currently forbids unknown fields under `state` (`additionalProperties: false`). This makes the normative documentation internally inconsistent with the schema and will cause schema-based validation to reject Jira poll events.

## Issue Context
- The adapter doc explicitly defines `state.privacy_gate.source_hash`.
- The JSON schema’s `$defs.state` only allows `labels` and `change_proposal` and disallows additional properties.

## Fix Focus Areas
- docs/normative/normalized-event/v1/normalized-event.schema.json[289-355]
- docs/normative/normalized-event/v1/jira-poll-adapter.md[76-103]

## Implementation notes
- Extend `$defs.state.properties` with an optional `privacy_gate` object.
 - Suggested shape: `{ "type":"object", "additionalProperties":false, "properties": { "source_hash": {"type":"string","pattern":"^[0-9a-fA-F]{64}$"} } }`.
- Update `jira-poll-adapter.md` “Schema extensions” section to include this field (or clearly mark it as an extension not yet present in the schema, but then the schema still needs to be loosened/updated to match the normative spec).

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



Remediation recommended

2. Allowlist can drop labels ✓ Resolved 🐞 Bug ☼ Reliability
Description
ADR 0068 defines privacy_gate.allowed_fields as default-deny (“only listed fields are read into
NormalizedEvent”), but NormalizedEvent v1 requires state.labels; a configuration that omits
labels from allowed_fields would either violate the schema or require undocumented fallback
behavior.
Code

docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md[R81-85]

+- **Allowlist projection, default-deny.** Only fields listed in
+  `allowed_fields` are read into `NormalizedEvent`. Fields not listed
+  (description, comment body, custom fields, reporter identity beyond role
+  mapping) are omitted at construction time, not merely hidden from display.
+- **Template-projected free text.** `comment_template`, when set, replaces
Relevance

⭐⭐⭐ High

Team has accepted schema/contract consistency fixes in NormalizedEvent v1 (tightening required
fields) in PR #2650.

PR-#2650

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The ADR allows allowed_fields to control whether labels are read, but the schema requires
state.labels, so excluding labels yields an invalid event unless the spec defines a required
fallback/enforcement.

docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md[81-102]
docs/normative/normalized-event/v1/normalized-event.schema.json[289-303]
docs/normative/normalized-event/v1/jira-poll-adapter.md[76-103]

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 0068’s privacy gate design implies `labels` can be excluded by configuration (`allowed_fields`), but the current NormalizedEvent v1 schema requires `state.labels`. Without an explicit rule, implementers either produce schema-invalid events or invent an implicit behavior (e.g., emitting `labels: []`) that is not specified.

## Issue Context
- `allowed_fields` is described as controlling what is read into `NormalizedEvent`.
- `state.labels` is `required` in the v1 JSON schema.

## Fix Focus Areas
- docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md[81-102]
- docs/normative/normalized-event/v1/jira-poll-adapter.md[76-103]
- docs/normative/normalized-event/v1/normalized-event.schema.json[289-303]

## Implementation notes
Pick one and document it explicitly (preferably in both ADR 0068 and the Jira poll adapter doc):
1) **Enforce**: `allowed_fields` MUST include `labels` (and the config schema/validation should reject configs that omit it).
2) **Fallback**: `state.labels` is always emitted; if `labels` is not allowlisted, the adapter MUST set `state.labels` to an empty array (so no Jira label content crosses the boundary but the event remains schema-valid).

Ensure the chosen behavior is stated unambiguously in the normative docs so future `jira-poll` implementations don’t diverge.

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



Informational

3. ADR 0068 exceeds 100 lines 📜 Skill insight ⚙ Maintainability
Description
docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md has more than 100 content lines
(excluding frontmatter), which violates the ADR size limit and makes the decision record harder to
review and maintain.
Code

docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md[R14-129]

+
+# 68. Privacy allowlist for poll input drivers
+
+Date: 2026-07-06
+
+## Status
+
+Accepted
+
+<!-- ADRs are point-in-time records, but not fully frozen after acceptance.
+     Minor annotations are welcome: cross-references to related ADRs, short
+     notes linking to newer decisions, or clarifying remarks. However, do not
+     substantially rewrite the Context, Decision, or Consequences sections. If
+     the decision itself needs to change, write a new ADR that supersedes this
+     one. For evolving design narrative, use docs/architecture.md. -->
+
+## Context
+
+[ADR 0063](0063-polling-based-work-discovery.md) defines `fullsend poll`,
+including a `jira-poll` input driver that maps Jira issue fields into
+`NormalizedEvent` per the
+[Jira poll adapter](../normative/normalized-event/v1/jira-poll-adapter.md).
+That mapping is unfiltered: issue title, comment bodies, and label state flow
+directly into `NormalizedEvent` and are projected into agent-visible
+`FULLSEND_WORK_ITEM_*` environment variables and `event_payload.comment`.
+
+This is a gap for installations where the polled Jira project is internal but
+the target repo is public. Jira issues can carry customer names, internal
+priority rationale, or references to other internal tickets — content with no
+equivalent in a GitHub-native dispatch, where the event source and the target
+repo share the same trust boundary. `jira-poll` is the first driver where the
+event source and the dispatch target do not.
+
+The [security threat model](../problems/security-threat-model.md)'s "Indirect
+information disclosure" section already treats forge content flowing through
+an agent as a threat surface and mitigates it on the *output* side
+(`SecretRedactor`, [ADR 0022](0022-harness-level-output-schema-enforcement.md)).
+`jira-poll` introduces a new *input*-side question that ADR 0063 does not
+answer: which Jira fields should reach `NormalizedEvent` at all. ADR 0063
+scopes poll input drivers to discovery, change detection, and coordination
+only ("Poll input drivers MUST NOT perform authorization policy — that is the
+dispatch core's responsibility") and does not address content filtering.
+
+## Decision
+
+`jira-poll` (and future poll input drivers for non-forge-native sources)
+apply a **privacy allowlist** to Jira fields before constructing
+`NormalizedEvent`, inside the input driver, before the event reaches the
+shared dispatch core.
+
+Configuration extends the existing `poll.input_drivers` block:
+
+```yaml
+poll:
+  input_drivers:
+    - type: jira-poll
+      connection: { ... }
+      queries:
+        - project = PROJ AND status != Done
+      privacy_gate:
+        allowed_fields: [summary, issue_type, priority, labels]
+        comment_template: |
+          Type: {issue_type}
+          Priority: {priority}
+          Summary: {summary}
+```
+
+- **Allowlist projection, default-deny.** Only fields listed in
+  `allowed_fields` are read into `NormalizedEvent`. Fields not listed
+  (description, comment body, custom fields, reporter identity beyond role
+  mapping) are omitted at construction time, not merely hidden from display.
+- **Template-projected free text.** `comment_template`, when set, replaces
+  `transition.comment.body`/`event_payload.comment` with a bounded,
+  named-slot projection instead of the verbatim Jira comment.
+- **PII scan on allowlisted free text.** Fields allowed through by name (e.g.
+  `summary`) are still scanned for PII patterns (email, phone, SSN-shaped
+  strings) as defense in depth, consistent with the threat model's existing
+  content-aware redaction guidance.
+- **Provenance hash.** A SHA-256 hash of the pre-gate `NormalizedEvent`
+  payload is attached as `state.privacy_gate.source_hash`, so sanitization
+  can be verified for a given dispatched run without retaining the original
+  Jira content anywhere downstream.
+
+When `privacy_gate` is omitted, the driver defaults to the current allowlist
+(`summary`, `issue_type`, `priority`, `labels`) rather than the unfiltered
+behavior described in [the Jira poll adapter](../normative/normalized-event/v1/jira-poll-adapter.md) —
+that document is updated alongside this ADR to reflect the gated mapping as
+the default, with an explicit opt-out for installations where the Jira
+project and target repo share the same trust boundary.
+
+## Consequences
+
+- Poll-sourced `NormalizedEvent`s carry less Jira content by default than the
+  original adapter mapping; harnesses that need additional fields must
+  request them explicitly via `allowed_fields`.
+- `jira-poll-adapter.md` gains a new schema extension (`privacy_gate` block)
+  that must be implemented before or alongside the `jira-poll` driver in the
+  [#2263](https://github.com/fullsend-ai/fullsend/issues/2263) epic — this
+  ADR should land before that implementation starts, not as a retrofit.
+- Installations that want the pre-gate behavior (single-trust-boundary Jira +
+  target repo) can set a broad `allowed_fields` list or omit `comment_template`
+  to pass comment bodies through unmodified.
+- Future poll input drivers for other internal sources should implement the
+  same `privacy_gate` shape rather than inventing per-driver filtering.
+- Does not change dispatch-core authorization ([ADR 0054](0054-require-authorization-on-all-agent-dispatch-paths.md)),
+  CEL trigger evaluation, or output drivers — the gate applies strictly
+  before `NormalizedEvent` construction.
+
+## References
+
+- [ADR 0063 — Polling-based work discovery via dispatch drivers](0063-polling-based-work-discovery.md)
+- [ADR 0067 — GitLab cron-polling event dispatch](0067-gitlab-cron-polling-event-dispatch.md)
+- [ADR 0054 — Require authorization on all agent dispatch paths](0054-require-authorization-on-all-agent-dispatch-paths.md)
+- [ADR 0022 — Harness-level output schema enforcement](0022-harness-level-output-schema-enforcement.md)
+- [Security threat model — Indirect information disclosure](../problems/security-threat-model.md)
+- [Jira poll adapter (NormalizedEvent extension)](../normative/normalized-event/v1/jira-poll-adapter.md)
Relevance

⭐ Low

Similar “ADR must be ≤100 lines” suggestion was rejected; ADR 0055 merged at 190 lines (PR #2582).

PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062092 requires ADR content (excluding frontmatter) to be ≤100 lines. In ADR 0068,
frontmatter ends at line 13 and the remaining content spans lines 14-129 (116 lines), exceeding the
limit.

docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md[1-129]
Skill: writing-adrs

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 0068 exceeds the maximum allowed length: content (excluding YAML frontmatter) must not exceed 100 lines.

## Issue Context
In `docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md`, the YAML frontmatter ends at line 13; content runs from line 14 through line 129 (116 lines), exceeding the 100-line limit.

## Fix Focus Areas
- docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md[14-129]

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


Grey Divider

Qodo Logo

Comment thread docs/normative/normalized-event/v1/jira-poll-adapter.md Outdated
Comment thread docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md Outdated
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Two issues flagged by automated review:

- normalized-event.schema.json's state object had
  additionalProperties: false with no privacy_gate property defined,
  so any event carrying state.privacy_gate.source_hash (as the
  adapter doc specified) would fail schema validation. Add
  privacy_gate as an optional object under state.
- ADR 0068's default-deny allowed_fields design didn't account for
  state.labels being a required NormalizedEvent field -- a config
  that excludes labels from allowed_fields would produce a
  schema-invalid event. Resolve explicitly: state.labels is always
  emitted, set to an empty array when labels isn't allowlisted,
  documented in both the ADR and the Jira poll adapter.

Signed-off-by: Wayne Sun <gsun@redhat.com>
@waynesun09

Copy link
Copy Markdown
Member Author

Fixed both bugs flagged by review (commit a4c9033):

  1. Added privacy_gate as an optional state property in normalized-event.schema.json (was previously undefined under additionalProperties: false).
  2. Resolved the labels-required-but-allowlist-can-exclude-it conflict: state.labels is always emitted (schema-valid), set to [] when labels isn't in allowed_fields — documented explicitly in both ADR 0068 and the Jira poll adapter.

The third finding (ADR line count) was marked low-relevance/optional by the reviewer itself, citing prior precedent (ADR 0055 merged at 190 lines) — left as-is.

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:19 PM UTC · Completed 10:28 PM UTC
Commit: a4c9033 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review — Approve

Re-review after one new commit (b40f6b0) that addresses substantive design gaps identified by a four-agent review squad. The ADR and supporting documentation are now internally consistent and technically sound.

Changes since prior review (a4c9033b40f6b0):

  • comment_template slots now normatively constrained to allowed_fields (MUST, with config-time rejection)
  • Template value sanitization added as normative MUST (injection defense)
  • allowed_fields clarified as Jira source field names, not NormalizedEvent output fields
  • entity.url/entity.key always-emitted carve-out with explicit rationale and accepted-gap documentation
  • Provenance hash reframed as correlation/tamper-evidence fingerprint (not verification mechanism); RFC 8785 canonicalization added
  • PII scan claims corrected (SecretRedactor is secrets-only; regex limitations acknowledged as residual risk)
  • Schema fix: required: ["source_hash"] added inside privacy_gate (addresses prior finding Add problem areas: Tekton pipeline review, migration path, multi-tenancy #2)
  • source_hash description fixed to "pre-gate Jira source payload" (addresses prior finding docs: Add agent-compatible code problem document #1)

Prior findings disposition:

# Prior finding Status
1 [low · correctness] source_hash wording inconsistency ✅ Fixed — now "pre-gate Jira source payload" throughout
2 [low · correctness] Schema privacy_gate missing required array ✅ Fixed — required: ["source_hash"] added
3 [low · docs-currency] ADR 0063 cross-reference annotation ⏳ Not addressed — carried forward below

Correctness: ✅ All cross-file references verified consistent: default allowed_fields list, comment_template subset constraint, RFC 8785 canonicalization, source_hash description, and fail-closed defaults match across ADR 0068, jira-poll-adapter.md, and normalized-event.schema.json. ADR 0063 quote ("Poll input drivers MUST NOT perform authorization policy") verified against source. Schema addition is valid JSON Schema 2020-12 and backward-compatible (optional property in state).

Security: ✅ No secrets, injection patterns, or permission changes. Fail-closed design confirmed (omitted/empty privacy_gate defaults to restrictive allowlist). Template injection defense explicitly addressed with mandatory sanitization. entity.url/entity.key residual exposure acknowledged with rationale in Consequences section.

ADR conventions: ✅ Status Accepted on new file (correct per AGENTS.md), docs/architecture.md updated, docs/problems/security-threat-model.md updated, annotation comment matches corpus pattern. DCO sign-off verified on all three commits.

Schema backward compatibility: ✅ Adding optional privacy_gate to state is non-breaking per normative versioning policy (README.md: "add optional fields" allowed in v1.x). No runtime validation infrastructure found in Go codebase. Existing valid documents remain valid.

PR title:docs(adr) prefix correct per COMMITS.md. No breaking change marker needed (additive optional schema extension).

Findings

[low · docs-currency] docs/ADRs/0063-polling-based-work-discovery.md — ADR 0063's poll input driver section could benefit from a minor forward-reference annotation to ADR 0068, noting that content filtering is addressed there. This is acceptable under ADR immutability rules as a cross-reference annotation and could be a follow-up PR. (Carried from prior review.)


Labels: PR adds a new ADR and modifies normative dispatch documentation and schema

Previous run

Review — Approve

Well-structured ADR that addresses a real privacy gap in the jira-poll input driver design. The default-deny allowlist, template-projected comments, PII scanning, and provenance hash form a coherent defense-in-depth approach. All cross-cutting documentation updates (architecture.md, security-threat-model.md, jira-poll-adapter.md, normalized-event.schema.json) are present and consistent.

ADR conventions: ✅ Status Accepted on new file (correct per AGENTS.md), architecture.md updated, security-threat-model.md updated, annotation comment matches corpus pattern.

Schema backward compatibility: ✅ Adding optional privacy_gate to the state object is a non-breaking additive change. Existing valid NormalizedEvent documents remain valid.

Security design: ✅ Default-deny with safe fallback when privacy_gate is omitted. SHA-256 provenance hash is one-way and does not leak filtered content. No secrets, injection patterns, or permission changes in the diff.

Findings

[low · correctness] docs/ADRs/0068-privacy-allowlist-for-poll-input-drivers.md — The ADR describes source_hash as "a SHA-256 hash of the pre-gate NormalizedEvent payload" (line 91), but the gate applies before NormalizedEvent construction per the same ADR ("omitted at construction time," line 79). There is no pre-gate NormalizedEvent — only raw Jira API data exists at that point. The jira-poll-adapter.md and schema correctly use "pre-gate event payload" instead. Consider aligning the ADR wording to match.

[low · correctness] docs/normative/normalized-event/v1/normalized-event.schema.json — The privacy_gate object has no required array, so {"privacy_gate": {}} (without source_hash) is schema-valid. The ADR and adapter doc both describe source_hash as present "when the privacy gate applied a transformation," implying it should always accompany a privacy_gate object. Adding "required": ["source_hash"] would enforce this invariant, or alternatively document when an empty privacy_gate without source_hash is valid. This is a future-implementation concern — the schema can be tightened when the driver is built.

[low · docs-currency] docs/ADRs/0063-polling-based-work-discovery.md — ADR 0063's poll input driver section could benefit from a minor forward-reference annotation to ADR 0068, noting that content filtering is addressed there. This is acceptable under ADR immutability rules as a cross-reference annotation and could be a follow-up PR.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label Jul 7, 2026
Four-agent review (2x Claude, Gemini, Codex) surfaced substantive
design gaps beyond the earlier automated pass:

- comment_template slots were not constrained to allowed_fields,
  letting the template mechanism re-introduce excluded content --
  now a normative MUST, with config-time rejection.
- Substituted template values were not sanitized, allowing a crafted
  Jira field to inject content indistinguishable from template
  structure -- now a normative MUST.
- allowed_fields defaults (summary, issue_type, priority) don't
  correspond to any direct NormalizedEvent schema field; only
  state.labels does. Clarified allowed_fields governs Jira content
  read for comment_template interpolation, not NormalizedEvent
  output fields directly, and fixed the Context's incorrect claim
  that issue title already flows into NormalizedEvent today.
- entity.url/entity.key are required-but-excludable under a literal
  default-deny reading, same class as the already-fixed labels bug --
  and they're the highest-signal internal-identity fields the ADR
  exists to protect (Jira hostname, ticket key). Extended the
  always-emitted carve-out and explicitly acknowledged this as an
  accepted gap with rationale (both fields are load-bearing for
  ADR 0063's poll cancel / lock-refresh resolution), rather than
  leaving it unaddressed.
- Provenance hash claimed to let "sanitization be verified... without
  retaining the original content" -- self-contradictory for a
  one-way hash with nothing to recompute against. Reframed as a
  correlation/tamper-evidence fingerprint, and added an RFC 8785
  canonicalization requirement for cross-implementation
  reproducibility.
- PII scan claims corrected: no existing fullsend component does PII
  scanning today (SecretRedactor is secrets-only, verified against
  internal/security/redactor.go); regex scanning cannot catch the
  customer names/rationale/ticket-reference leakage the ADR is
  motivated by, and that residual risk is now stated explicitly.
  Specified the match action (redact in place).
- Schema fix: added required: ["source_hash"] inside privacy_gate so
  an empty {} object can't validate.
- Minor: fixed a stale #2263 reference (closed design issue, not an
  implementation epic), broadened "non-forge-native" scoping to the
  actual trust-boundary condition, added conformance-test guidance
  for the eventual jira-poll implementation, and clarified the
  privacy_gate-present-but-allowed_fields-omitted default.

Mirrored all changes into jira-poll-adapter.md for cross-file
consistency.

Signed-off-by: Wayne Sun <gsun@redhat.com>
@waynesun09

Copy link
Copy Markdown
Member Author

Review Squad Report — #3428

Agents dispatched: 4 (claude-coder, claude-researcher, gemini-code-review, cursor-code-review)
Models used: Claude, Gemini, Codex
Total findings: 13 unique (after dedup, after verification against actual file contents)

Critical (2)

  • Allowlist bypass via comment_template — slots weren't constrained to allowed_fields, letting the template mechanism re-introduce excluded Jira content. [verified]
  • Template injection — substituted values weren't sanitized before interpolation; a crafted field could inject content indistinguishable from template structure. [verified]

High (4)

  • allowed_fields defaults didn't correspond to any real NormalizedEvent fieldsummary/issue_type/priority only ever existed as comment_template slots; the ADR's own Context overstated what currently flows into the schema. [verified against schema + adapter doc]
  • entity.url/entity.key required-but-excludable — same class of bug as the already-fixed labels issue, and the highest-signal internal-identity fields (Jira hostname, ticket key) the ADR exists to protect. [verified: entity.required: ["kind","id","url"], conditional key requirement for source.system: jira]
  • Provenance hash claimed "verification without retention" — self-contradictory. A one-way hash of content that's never retained can't verify anything against itself. [3-way consensus: claude-researcher, gemini, claude-coder]

Medium (3)

  • source_hash not marked required inside privacy_gate{} validated as schema-correct. [3-way consensus: codex, gemini, claude-coder; verified against schema.json]
  • PII-scan claims overstated coverage and misattributed SecretRedactor (secrets-only, no PII patterns) as related prior art. [verified against internal/security/redactor.go]
  • No canonicalization specified for the provenance hash, making it non-reproducible across implementations.

Low (4)

  • PII-scan match action unspecified (redact/drop/reject?).
  • #2263 referenced as an "implementation epic" — it's actually a closed design issue; the real driver work is untracked as a dedicated issue.
  • "Non-forge-native sources" scoping was narrower than the actual trust-boundary condition (a GitHub-native poll crossing an internal→public boundary would've been unscoped).
  • Ambiguous default when privacy_gate is present but allowed_fields itself is omitted.

Info (3)

  • ADR line count (~190) exceeds the writing-adrs skill's 100-line guidance — consistent with repo precedent (ADR 0055 merged at 190 lines); not acted on.
  • Cross-file consistency (field names, defaults, link paths, YAML nesting against ADR 0063's own config block) all verified correct.
  • Minor style nit (-- vs ) — fixed.

False Positives Removed

None — all reported findings were verified as real against the actual file contents before inclusion.

Fixes applied (commit b40f6b0)

  • ADR 0068 Decision/Consequences substantially revised: comment_template now normatively constrained to allowed_fields with load-time rejection; substitution values must be sanitized; allowed_fields semantics clarified (Jira content for template interpolation vs. structural NormalizedEvent fields); entity.url/entity.key gap explicitly acknowledged with rationale (both load-bearing for fullsend poll cancel/lock-refresh); provenance hash reframed as a correlation/tamper-evidence fingerprint with RFC 8785 canonicalization; PII-scan claims corrected and residual risk stated; conformance-test guidance added for the eventual implementation.
  • Schema: added required: ["source_hash"] inside privacy_gate.
  • Mirrored all changes into jira-poll-adapter.md for cross-file consistency.
  • #2263 reference reworded; trust-boundary scoping broadened; ambiguous-default case clarified.

Summary

The first review round (Qodo) caught schema-validity bugs; this round caught design-level security gaps in the mechanism itself — most notably that the privacy gate's own comment_template feature could be used to bypass the allowlist it was built to enforce. All findings were verified against actual file contents (not taken on the reviewers' word) before being acted on.

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 7, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:31 PM UTC · Completed 10:43 PM UTC
Commit: b40f6b0 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added ready-for-merge All reviewers approved — ready to merge component/docs User-facing documentation component/dispatch Workflow dispatch and triggers and removed ready-for-merge All reviewers approved — ready to merge labels Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/dispatch Workflow dispatch and triggers component/docs User-facing documentation ready-for-merge All reviewers approved — ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant