Skip to content

G1: fence the ingest→ACL chain at the pipeline seam, mark G1/G2/G9 shipped - #535

Merged
brentrager merged 3 commits into
mainfrom
g1-connector-acl-contract
Aug 23, 2026
Merged

G1: fence the ingest→ACL chain at the pipeline seam, mark G1/G2/G9 shipped#535
brentrager merged 3 commits into
mainfrom
g1-connector-acl-contract

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

The finding

I was sent to build G1 — the Connector trait, a MockConnector, and an ingest→chunk→embed→store contract test. All of it already exists on main and has since 315841c7: the seam in rust/ingestion/src/connector.rs, the mock, the file / web / github connectors, the chunker (G2), and incremental indexing.

docs/Planning/Feature Gaps.md was never updated to say so. §G1, §G2 and §G9 still read as open gaps while §G3 and §G8 carry ✅ markers — which is how an agent gets dispatched to rebuild shipped code. Most of this PR is correcting that record.

The one real hole underneath the stale doc

The guarantee that a connector's ACL survives ingestionRawDocument::acl → chunk → structured DocAclAclKnowledgeStore side table → AclReader — was asserted end to end in exactly one place: github_connector.rs::custom_group_gates_retrieval_end_to_end.

That is a connector test. Rewrite or delete the GitHub connector and the ingest half of G3 loses its only fence, silently, with ingestion_contract.rs still green.

ingested_acls_gate_retrieval_for_every_connector asserts the same chain at the pipeline seam over a MockConnector, so it binds every connector present and future:

  • a doc ingested for group-eng is readable by a principal carrying that group, and returns nothing for group-fin or for anonymous;
  • symmetrically for a group-fin doc;
  • a doc ingested with no ACL stays org-public — so the gate is an opt-in restriction, not a blanket denial that would make the leak assertions pass for the wrong reason.

Every negative assertion is paired with the entitled-principal positive control on the same query, plus a run-level documents_pulled == 3 / chunks_stored >= 3. "Nothing leaked" must not be satisfiable by a pipeline that stored nothing.

Red before green

Reverted DocAcl::for_groups(acl.clone()).attach_to(document) in pipeline.rs, in place, and re-ran:

running 2 tests
test ingest_chunks_embeds_stores_and_retrieves_then_is_idempotent ... ok
test ingested_acls_gate_retrieval_for_every_connector ... FAILED

thread 'ingested_acls_gate_retrieval_for_every_connector' panicked at
ingestion/tests/ingestion_contract.rs:229:5:
group-fin must not read the group-eng doc, got 1 hits: ["The snarflex incident
postmortem is engineering-only. Snarflex retries were exhausted before the pager
fired."]

test result: FAILED. 1 passed; 1 failed

That is the exact G3 cross-user leak. Note the pre-existing contract test stayed green through the revert — which is precisely what kept this gap invisible.

Restored; full suite green (53 tests: 41 + 6 + 3 + 2 + 1).

What the ✅ markers deliberately do not cover

Recorded in the doc rather than glossed:

  • pull returns Vec<RawDocument>, not the planned Stream<Document>. It materializes a whole source per pull. Fine for file/web/github, wrong for a large Confluence space or Jira project — which is exactly what comes next. Re-shaping it is a breaking trait change, so it belongs before the SaaS connectors, not after.
  • No format extraction (PDF/DOCX/rich HTML → text) ahead of the chunker. The GitHub connector skips binary extensions rather than reading them.
  • .github/workflows/rust.yml has no schedule:, so G9's gated external tier (#[ignore] + SMOOTH_AGENT_E2E) is a convention nothing ever executes. A live-API break in the web or GitHub connector is invisible until a user hits it.
  • The connector long tail — confluence, jira, notion, slack, zendesk, drive, salesforce, sharepoint. Their per-document permissions must map onto RawDocument::acl the way GithubConnectorConfig::acl_groups already does, or ingesting them reopens G3.

Verification

  • cargo test -p smooai-smooth-operator-ingestion — 53 passed, 0 failed, 1 ignored (the network-gated web fetch)
  • cargo clippy -p smooai-smooth-operator-ingestion --all-targets — clean
  • cargo fmt --all -- --check — clean

No production behavior changes: one test, one changeset, one doc.

@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f065025

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@smooai/smooth-operator Patch
@smooai/smooth-operator-web-chat-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…9 shipped

G1's implementation already landed (315841c and follow-ups): the `Connector`
seam, `MockConnector`, the file/web/github connectors, the chunker, incremental
indexing. `docs/Planning/Feature Gaps.md` was never updated to say so, so §G1,
§G2 and §G9 still read as open gaps while G3 and G8 carry ✅ markers. That is how
a second agent gets sent to build what exists.

The one real hole underneath the stale doc: the guarantee that a connector's ACL
survives ingestion was asserted end to end in exactly one place —
`github_connector.rs::custom_group_gates_retrieval_end_to_end`. That is a
connector test. Rewrite or delete the GitHub connector and the ingest half of G3
loses its only fence, with `ingestion_contract.rs` still green.

`ingested_acls_gate_retrieval_for_every_connector` asserts the same chain at the
pipeline seam over a `MockConnector`, so it binds every connector present and
future: a doc ingested for `group-eng` is readable by a principal carrying that
group and returns nothing for `group-fin` or anonymous, while a doc with no ACL
stays org-public. Each negative is paired with the entitled-principal positive
control on the same query — "nothing leaked" must not be satisfiable by a
pipeline that stored nothing, which is a failure mode this repo has shipped.

Red before green: with `DocAcl::for_groups(...).attach_to(document)` reverted in
pipeline.rs the new test fails with `group-fin must not read the group-eng doc,
got 1 hits` — the exact G3 cross-user leak — while the pre-existing contract test
stays green, which is what kept the gap invisible.

The doc now records what shipped, plus what the ✅ does not cover: `pull` returns
`Vec<RawDocument>` rather than the planned `Stream<Document>`, which materializes
a whole source per pull and should be re-shaped before Confluence/Jira rather
than after (it is a breaking trait change); format extraction (PDF/DOCX) does not
exist ahead of the chunker; and rust.yml has no `schedule:`, so G9's gated
`external` tier is a convention nothing ever executes.

No production behavior changes — one test, one changeset, one doc.
…ished work

#534 landed because §G6 claimed "we only helm lint" after the kind
deploy-smoke job had shipped. §5's priority list had the same rot: item 1 still
named G3 as the highest-severity next increment, and item 4 pointed at G2/G9 by
bare letter.

Items 1 and 2 are struck (G3 and G1 shipped). Item 4 now names the specific
remainders rather than the letters — G2's chunker is done and its format
extraction is not; G9's mock and credential-free tier are done and the nightly
that would run the gated external tier does not exist. "G2 is done" and "G2's
chunker is done, format extraction isn't" send whoever picks this up to very
different places.
@brentrager
brentrager force-pushed the g1-connector-acl-contract branch from 6356749 to 3d7f988 Compare August 23, 2026 01:36
brentrager added a commit that referenced this pull request Aug 23, 2026
…-tests trap

Takes sole ownership of §G2 in the gap doc so #535 and #536 don't conflict on
the same section. Folds in #535's correct finding — format extraction (PDF /
DOCX / rich HTML → text) still has to land ahead of the chunker before any
document-store connector works.

Also records why the pre-existing green unit tests were not evidence the
chunker was correct: each asserted its rule on the one input shape that never
triggers the bug (cap checked only with overlap off, spill only on spaced
ASCII, paragraphs only with LF).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
g2chunk's #536 wrote the G2 contract suite and it found four silent defects in
the shipped chunker (overlap pushed emitted chunks past max_chars; unspaced
CJK/URL text never spilled; CRLF destroyed paragraph structure; chunks straddled
markdown sections). My §G2 text called the existing unit tests adequate because
they were green and named every rule — which is exactly the reasoning this
document exists to stop. Each of those tests asserted its rule on the one input
shape that never triggers the bug. §G2 reverts to pristine; #536 owns it.

Also corrects my own §G1 bullet. I wrote that an AclKnowledgeStore records and
enforces the ACL, which reads as though the fence is opt-in wrapping. Traced it:
enforcement is adapter-side in all three backends — Postgres parses
DocAcl::ACL_METADATA_KEY at ingest into knowledge_vectors.acl and filters in SQL,
DynamoDB into an acl attribute post-filtered at read, and InMemoryStorageAdapter
already wraps its own knowledge slice. The live admin indexing path passes
state.storage.knowledge() and is fenced by that, not by the caller remembering to
wrap. The pipeline writing the metadata is therefore the one link no adapter
provides for itself, which is what the contract test guards.
brentrager added a commit that referenced this pull request Aug 23, 2026
A dropped ACL in the chunker fails open, not closed: the pipeline turns
Chunk.acl into the DocAcl the adapters filter on, so a chunk arriving with
acl: None is stored org-public and retrievable by anyone, with nothing logged.
The existing assertion covered the packed path only; this covers the word-spill,
character-spill and heading-break paths added in the previous commit.

Mutation-checked — forcing acl: None in the chunk builder fails it.

Reported by g1conn while tracing adapter-side ACL enforcement for #535.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brentrager
brentrager merged commit 3f25540 into main Aug 23, 2026
3 checks passed
brentrager added a commit that referenced this pull request Aug 23, 2026
A dropped ACL in the chunker fails open, not closed: the pipeline turns
Chunk.acl into the DocAcl the adapters filter on, so a chunk arriving with
acl: None is stored org-public and retrievable by anyone, with nothing logged.
The existing assertion covered the packed path only; this covers the word-spill,
character-spill and heading-break paths added in the previous commit.

Mutation-checked — forcing acl: None in the chunk builder fails it.

Reported by g1conn while tracing adapter-side ACL enforcement for #535.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brentrager added a commit that referenced this pull request Aug 23, 2026
…-tests trap

Takes sole ownership of §G2 in the gap doc so #535 and #536 don't conflict on
the same section. Folds in #535's correct finding — format extraction (PDF /
DOCX / rich HTML → text) still has to land ahead of the chunker before any
document-store connector works.

Also records why the pre-existing green unit tests were not evidence the
chunker was correct: each asserted its rule on the one input shape that never
triggers the bug (cap checked only with overlap off, spill only on spaced
ASCII, paragraphs only with LF).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brentrager added a commit that referenced this pull request Aug 23, 2026
A dropped ACL in the chunker fails open, not closed: the pipeline turns
Chunk.acl into the DocAcl the adapters filter on, so a chunk arriving with
acl: None is stored org-public and retrievable by anyone, with nothing logged.
The existing assertion covered the packed path only; this covers the word-spill,
character-spill and heading-break paths added in the previous commit.

Mutation-checked — forcing acl: None in the chunk builder fails it.

Reported by g1conn while tracing adapter-side ACL enforcement for #535.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brentrager added a commit that referenced this pull request Aug 23, 2026
* G2: chunking contract test + four silent chunker defects it found

Gap G2 was recorded open, but the Chunker the connectors feed shipped with G1.
What was missing was the contract suite — and writing it found the chunker wrong
on four counts, each of which fails silently rather than loudly.

tests/chunking.rs (16 tests) pins chunk count/indices/ids, overlap as a shared
word run, metadata+title+source+acl propagation, oversized spill with no word
lost, UTF-8 integrity at the boundary, and degenerate configs. Fixed against it:

- max_chars is now a hard cap on the emitted chunk, overlap included. Overlap
  was prepended on top of an already-full chunk, so a default 500/64 chunker
  emitted up to 565 chars — past the limit the embedding API truncates at.
- Text without spaces now spills. Word-boundary-only splitting returned a
  Chinese/Japanese/Thai document, a long URL or a minified blob as ONE unbounded
  chunk. The fallback cuts on character boundaries.
- CRLF documents split on paragraphs. "\r\n\r\n" contains no "\n\n", so every
  Windows-authored document arrived as one giant paragraph.
- A markdown heading is a hard boundary, so no chunk straddles two sections.

Characters, never bytes: slicing to a byte offset to hit a character cap cuts an
em-dash or emoji in half. The UTF-8 guard is built on unspaced mixed-width text
because spaced text never reaches the character-split path — the first version,
built from words, passed against a deliberately byte-slicing implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* G2: fold format-extraction remainder into the gap doc, note the green-tests trap

Takes sole ownership of §G2 in the gap doc so #535 and #536 don't conflict on
the same section. Folds in #535's correct finding — format extraction (PDF /
DOCX / rich HTML → text) still has to land ahead of the chunker before any
document-store connector works.

Also records why the pre-existing green unit tests were not evidence the
chunker was correct: each asserted its rule on the one input shape that never
triggers the bug (cap checked only with overlap off, spill only on spaced
ASCII, paragraphs only with LF).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* G2: pin Chunk.acl across every split path

A dropped ACL in the chunker fails open, not closed: the pipeline turns
Chunk.acl into the DocAcl the adapters filter on, so a chunk arriving with
acl: None is stored org-public and retrievable by anyone, with nothing logged.
The existing assertion covered the packed path only; this covers the word-spill,
character-spill and heading-break paths added in the previous commit.

Mutation-checked — forcing acl: None in the chunk builder fails it.

Reported by g1conn while tracing adapter-side ACL enforcement for #535.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* G2: correct the port-scope claim — .NET has its own diverging chunker

My §G2 said the other four languages have no ingestion pipeline to port the
fixes into. That is true of Go, TypeScript and Python, which never split text,
but false of .NET: dotnet/server/src/Chunker.cs is a second chunker carrying all
four defects, plus a fifth unique to it — C# string.Length counts UTF-16 code
units where Rust counts chars(), so the two implementations chunk the same
document to different boundaries for any astral-plane codepoint.

Filed as pearl th-ed78a9 rather than fixed here, to keep this PR to the
reference implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant