Skip to content

HT-3: clean-room protocol doc - #1

Closed
zaridan wants to merge 4 commits into
mainfrom
docs/ht-3-clean-room-protocol
Closed

HT-3: clean-room protocol doc#1
zaridan wants to merge 4 commits into
mainfrom
docs/ht-3-clean-room-protocol

Conversation

@zaridan

@zaridan zaridan commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Operationalizes CHARTER.md §3 into a followable procedure: session-side declaration (spec/implementation/foundation), reference hierarchy, the never-read list, session checklist, literal templates for provenance/sessions.md entries and per-module manifests, spec hygiene good/bad examples, contamination handling ("if in doubt, it was contaminated"), quarantine registry, and the human-authorship bar.

Also fixes a real contradiction the proof pass caught: the "nothing in this repo's history may trace to a source-visible session" phrasing (in the draft's §8, CLAUDE.md, and the charter's Phase 0 bullet) would have outlawed committing specs — the entire point of spec sessions. All three now scope the rule to engine code with an explicit spec-side carve-out. Charter §3 additionally gains a parenthetical explaining why FreeScout alone gets the categorical no-code declaration (the maintainer's review question).

Includes this session's provenance/sessions.md entry per the protocol's own §4c.

Jira: https://resonantiq.atlassian.net/browse/HT-3

🤖 Generated with Claude Code

https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b

zaridan and others added 4 commits July 9, 2026 19:52
Helpthread — open-source serverless helpdesk. CHARTER.md is the
constitution: vision (FreeScout-inspired, Help Scout-caliber),
AGPL-3.0 core + CLA + commercial first-party modules, clean-room
provenance policy with reference hierarchy, architecture direction,
staged roadmap. CLAUDE.md operationalizes it; provenance/ starts the
mandated session log. No code precedes this document.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
Principle #1 anointed FreeScout as the sole behavioral oracle; the
earned knowledge we draw on is plural (FreeScout, Chatwoot, RFCs,
mail libraries). FreeScout stays as a named example — fixtures still
come from black-box observation of the live instance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
docs/clean-room-protocol.md operationalizes charter §3: session sides,
reference hierarchy, session procedure, record templates, spec hygiene
examples, contamination handling, quarantine registry, human-authorship
bar.

Consistency fixes from the proof pass: the 'nothing traceable to
source-visible sessions' rule (protocol §8, CLAUDE.md, charter Phase 0)
would have outlawed committing specs — now scoped to engine code with
an explicit spec-side carve-out. Charter §3 gains a note on why
FreeScout alone gets the categorical no-code statement. Example paths
moved off lib/mail to avoid colliding with the quarantine's path names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
Help Scout is the experience North Star (founder ran support on it for
years — now stated in the mission); FreeScout is demoted to its three
real roles: proof of the self-hosted market, observable mail-semantics
reference, and the legal-care object of the clean-room policy. Identity
sections (intro, mission, architecture plugin-folder line, roadmap map
line) no longer lead with FreeScout; the clean-room and licensing
mentions stay — those are doing legal work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
@zaridan zaridan closed this Jul 10, 2026
@zaridan
zaridan deleted the docs/ht-3-clean-room-protocol branch July 10, 2026 03:29
zaridan added a commit that referenced this pull request Jul 12, 2026
…T-30)

Store: deleteConversation() — a single UPDATE to status 'deleted',
scoped to exclude already-deleted rows so a second delete reports a
miss. No updated_at bump (never surfaced again; the row stays an exact
record of its last live activity). Soft, permanently: rows and threads
survive in storage (charter invariant #1) — every public path already
treats 'deleted' as nonexistent, so only the flag flips.

API: DELETE joins GET/PATCH on the item route (Allow header updated);
204 with an empty body via a new noContent() helper (still no-store);
missing, already-deleted, and non-UUID ids are one identical 404 (§5
no-existence-leak).

Per specs/api/agent-inbox-v1.md §4d (v1.1, HT-25). 349/349 tests,
including post-delete invisibility across every endpoint, the §4a
replay-vs-delete rule (a keyed replay of a successful send 404s after
the delete), and proof the mail itself stays in storage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 13, 2026
…sh (HT-34)

All 8 findings incorporated.

gmail-push: require email_verified=true on the Pub/Sub OIDC JWT; bind to the push envelope's exact subscription field; resolve emailAddress->mailbox (reject on mismatch) before reconciling; add a bounded daily history.list reconciliation sweep so dropped/delayed pushes cannot leave a mailbox stale.

inbound-ingestion: attachment extraction is the pipeline's job (post-parse), not the transport's; make the ledger claim atomic (unique-key get-or-insert) and commit the store write + 'stored' outcome in one transaction (closes the concurrent-delivery and partial-failure double-create windows); loop suppression now requires a verifiable correlation (own Message-ID / valid own token) — sender identity alone never drops mail (invariant #1); 'Agent' capitalization.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 14, 2026
A daily maintenance pass over active Gmail mailboxes (a plain injected-deps
function like the HT-16 delivery worker; the SchedulerProvider/Vercel-cron
schedule and the queue-consumer wiring are composition-root config, deferred
to HT-43). Per active mailbox, failure-isolated:

- Re-arm users.watch() (Gmail push silently expires every ~7 days) and store
  the fresh expiration via GmailWatchStateStore.setWatchExpiration, which
  updates watch_expiration ONLY and never the cursor (a renewal's fresh
  historyId is ahead of the stored cursor; overwriting it would skip
  un-reconciled mail — charter invariant #1).
- Enqueue a bounded reconciliation sweep (one reconcile job per active mailbox
  onto HT-41's GMAIL_RECONCILE_TOPIC) — the safety net for dropped best-effort
  pushes. No dedupeKey; idempotent ingest makes the redundant work safe.

The access token is acquired once per mailbox and reused for the single
watch() call. The OAuth token layer (HT-38) owns needs_reconnect via
invalid_grant; a valid-token watch() failure is transient. Re-arm and sweep
are independent.

New: MailboxStore.listActiveMailboxes, GmailWatchStateStore.setWatchExpiration,
src/mail/gmail-watch-maintenance.ts. No migration. The reconciliation lease
from gmail-push.md §6 is deferred to HT-48.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 17, 2026
…apshot (HT-50)

CodeRabbit round 3: Gmail's HistoryLabelAdded schema carries the added
labels in the record's TOP-LEVEL labelIds field, beside `message` — the
embedded message's own labelIds is not guaranteed to be populated in
history records. The folding logic was reading message.labelIds and
OVERWRITING the tracked entry, so an omitted embedded snapshot would
clobber a real ['SENT'] down to [] (losing the label the self-echo
filter keys on and defeating the very INBOX-visibility hardening the
fold exists for).

Now merges (set union) the schema-guaranteed top-level delta into the
tracked entry and never reads the embedded snapshot: merging can only
ADD labels, so a later INBOX becomes visible without ever losing the
earlier SENT. Still fails open — an absent delta leaves the snapshot
intact, biasing toward ingesting, never toward dropping (invariant #1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 17, 2026
…ening (HT-50)

Review round 2 findings, all addressed:

- Skip DRAFT-labeled history entries too (src/mail/gmail-reconcile.ts's
  isSelfEchoMessage), not just SENT-without-INBOX. Gmail autosaves an
  Agent's in-progress Gmail-UI reply as a new DRAFT-labeled message id on
  every pause, each surfacing in history.list before anything is sent; the
  prior filter left these to ingest as ghost/half-written "customer"
  messages. Safe in the drop direction with no ambiguity: genuine inbound
  mail can never carry the system DRAFT label.

- Harden src/providers/adapters/gmail/history.ts's listAddedMessageIds
  against the (unverified-live, but plausible) case where Gmail records a
  self-addressed send's SENT and INBOX labels as two SEPARATE history
  records instead of one messagesAdded snapshot carrying both. Now also
  requests the labelAdded history type and folds a later labelsAdded
  record's label snapshot into an id already tracked via messagesAdded in
  the same listed window, so a split SENT-then-INBOX ordering can no longer
  cause a silent permanent drop (invariant #1). Live verification against
  the dogfood mailbox remains open and is called out in the module doc as
  such — this hardening does not depend on nor confirm the assumption, it
  removes the failure mode either way.

- specs/mail/gmail-push.md: document the DRAFT skip; amend §4's cursor rule
  to scope "the batch" to messages actually handed to the ingest pipeline,
  closing the contradiction with §3's self-echo filter (which was already
  described as cursor-neutral but never reconciled against §4's literal
  wording).

- Full `npx vitest run` on this branch: 44 files, 868 tests, all passing
  (exit 0) — the previously-unattributed 18 failures from review round 1 do
  not reproduce; no comparison against origin/main was needed since this run
  itself is clean evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 17, 2026
…apshot (HT-50)

CodeRabbit round 3: Gmail's HistoryLabelAdded schema carries the added
labels in the record's TOP-LEVEL labelIds field, beside `message` — the
embedded message's own labelIds is not guaranteed to be populated in
history records. The folding logic was reading message.labelIds and
OVERWRITING the tracked entry, so an omitted embedded snapshot would
clobber a real ['SENT'] down to [] (losing the label the self-echo
filter keys on and defeating the very INBOX-visibility hardening the
fold exists for).

Now merges (set union) the schema-guaranteed top-level delta into the
tracked entry and never reads the embedded snapshot: merging can only
ADD labels, so a later INBOX becomes visible without ever losing the
earlier SENT. Still fails open — an absent delta leaves the snapshot
intact, biasing toward ingesting, never toward dropping (invariant #1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan added a commit that referenced this pull request Jul 20, 2026
…e install path (HT-79) (#98)

* docs(modules): managed hosting becomes the mainline install path (HT-79)

TJ walked the v1 manual self-host install as customer #1 (HT-82 dogfood)
and rejected its friction: the designed loop is browse → buy → install →
enable, not five hand-assembled seams. This revises marketplace-v1 to v1.1
with managed hosting as the default, the tarball flow demoted to a
preserved self-host escape hatch.

- specs/modules/marketplace-v1.md → v1.1: new hosting control plane (§3d,
  the sole trust domain holding both a license key and a per-desk
  provisioning grant, entirely outside the AGPL core); the buy → install →
  enable managed handoff via a one-time claim token redeemed by the control
  plane, never the desk (§3e); rewritten §5 (managed install/update/
  uninstall mainline + self-host escape hatch §5.4); one-click in-place
  update for hosted instances, resolving the admin-ia "in-place update"
  conflict; hosted-instance lifecycle (lapsed/frozen keep running, refunded
  decommissions after a config-export grace, revoked immediately); §6/§7/§8/
  §9/§10 updated. Entities, commerce/download/update-check APIs, and the
  artifact pipeline are unchanged.
- CHARTER.md: amended by direct edit (§4 + §7 appendix, the PR #86
  mechanism) — restates the credential/no-runtime-check/lapse invariants for
  managed hosting; leaves the §2 data-residency reconciliation to TJ.
- specs/modules/catalog.md: §5 reconciling note (hosting machinery lives
  outside the core, so "no special machinery in the product" still holds).
- SPEC-CHANGES.md: chosen-defaults and OPEN-needs-TJ lists for the lead.

Invariants held verbatim: license = distribution credential only, zero
runtime license checks in any module, lapse never stops running software
(even when RIQ is the host), AGPL core never holds a marketplace credential
and never calls the marketplace. Self-host escape hatch preserved and not
weakened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(modules): collect module runtime config browser-direct to the control plane (lead review fix)

Lead-tier review finding on the managed-hosting revision: §5.1 promised
'no hand-entered env vars' but never said where operator-supplied module
settings — most prominently the operator's own model API key, which
charter §2 requires — come from in the managed flow. Added §3e 'Module
runtime configuration': an install-time in-app form submitted
browser-direct to the control plane (same never-transits-the-core rule
as the claim token), stored in the control-plane vault, re-injected on
update rolls; §5.1 reworded honestly; §10.13 extended — operator secrets
in the vault are the credential face of the same data-residency call.

Also drops SPEC-CHANGES.md from the branch: it is adjudication material
for the PR description, not a permanent repo artifact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <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