Skip to content

GV-10: Gate public projections by current visibility - #695

Open
ScottArbeit wants to merge 3 commits into
epic/638-visibility-owned-branchesfrom
agent/649-visibility-gated-projections
Open

GV-10: Gate public projections by current visibility#695
ScottArbeit wants to merge 3 commits into
epic/638-visibility-owned-branchesfrom
agent/649-visibility-gated-projections

Conversation

@ScottArbeit

@ScottArbeit ScottArbeit commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Related to #649.

Summary

  • Adds projection-time visibility rechecks for public fanout so persisted event metadata is not the only privacy gate.
  • Gates public automation/Eventing envelopes, webhook delivery creation, and SignalR/watch reference notifications when the current source is hidden or stale.
  • Preserves reveal as the public publication point: reveal-time public publication remains exactly once, while old private apply history is not replayed.

Surface Preflight Outcomes

  • Webhooks: updated through WebhookDispatch.dispatchCommittedEventAsyncWithCurrentVisibility before delivery creation.
  • Eventing/automation: updated through tryCreateEnvelopeWithCurrentVisibility before public automation envelopes are created.
  • SignalR/watch notifications: updated so reference notifications re-read current reference visibility before sending reference ids to clients.
  • Notifications: updated through the same SignalR automation routing path.
  • Queues: already covered by GV-06: PromotionSet accepted-private workflow and terminal reference secrecy #645 caller-visible queue projection and private metadata stamping on this branch.
  • Review reports/candidates: already covered by GV-06: PromotionSet accepted-private workflow and terminal reference secrecy #645 review metadata inheritance and caller-visible queue projection on this branch.
  • Work items: covered by current fanout recheck when work-item events carry a PromotionSet source, plus GV-06: PromotionSet accepted-private workflow and terminal reference secrecy #645 metadata inheritance.
  • Artifacts: covered by current fanout recheck when artifact events inherit a PromotionSet source id, plus existing private automation suppression.
  • Activity/search: no external activity/search projection route was found in the searched server modules; current activity usage is diagnostics instrumentation.
  • Logs: no public log projection or delivery route was found for GV-10; structured server logs remain internal diagnostics.
  • Docs/event reference: deferred to GV-15 because event names did not change.

Validation

  • dotnet tool run fantomas src/Grace.Server/Eventing.Server.fs src/Grace.Server/WebhookDispatch.Server.fs src/Grace.Server/Notification.Server.fs src/Grace.Server.Unit.Tests/Eventing.Server.Tests.fs src/Grace.Server.Unit.Tests/WebhookDispatch.Server.Tests.fs passed.
  • dotnet tool run fantomas src/Grace.Server/Notification.Server.fs src/Grace.Server.Unit.Tests/Notification.Server.Tests.fs passed.
  • dotnet build --configuration Release src/Grace.Server.Unit.Tests/Grace.Server.Unit.Tests.fsproj passed.
  • dotnet test --no-build --configuration Release src/Grace.Server.Unit.Tests/Grace.Server.Unit.Tests.fsproj --filter "FullyQualifiedName~AutomationEventingTests|FullyQualifiedName~WebhookDispatchUnitTests|FullyQualifiedName~NotificationServerTests" passed: 53 passed.
  • pwsh ./scripts/validate.ps1 -Fast passed after the final diff.
  • git diff --check passed.

Review Status

  • Current head: 5208291f.
  • PR-attached Validate: passed on run 29014521720, job 86106064372.
  • Codex Code Review Bot: third substantive cycle completed on 5208291f with three fresh source-scope findings.
  • Stop notice posted on #649 and PR #695.
  • Local unblock page: C:\Users\scott\AppData\Local\Temp\grace-638-orchestration\grace-649-unblock-current.html.
  • Coding status: stopped. No further worker may be assigned until the maintainer approves the revised issue/ledger or explicitly directs continued patching.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 393ef657e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Grace.Server/Notification.Server.fs Outdated
Comment thread src/Grace.Server/Notification.Server.fs Outdated
Comment thread src/Grace.Server/Notification.Server.fs
@ScottArbeit
ScottArbeit force-pushed the agent/649-visibility-gated-projections branch from 393ef65 to c1c51b5 Compare July 9, 2026 10:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1c51b5c12

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Grace.Server/Notification.Server.fs Outdated
Comment thread src/Grace.Server/WorkItem.Server.fs Outdated
Comment thread src/Grace.Server/Notification.Server.fs Outdated
@ScottArbeit

Copy link
Copy Markdown
Owner Author

Review Stabilization Ledger - Cycle 2

PR: #695
Current head with findings: c1c51b5c120d50a6f2693e7f5c90396280bc6dcb

Review Timeline

  • First substantive Codex cycle on 393ef657: three P1 findings in Notification.Server.fs around deleted reference fanout, quick-scan fanout ordering, and summary artifact PromotionSet scope. The CI run also failed in ServiceBusSubscriberDispatchesMatchingWebhookOnceAndDoesNotBlockOnOutboundFailure while waiting for promotion-set.applied delivery. Fixed by c1c51b5c with local validate -Full and PR-attached Validate passing.
  • Second substantive Codex cycle on c1c51b5c: three fresh P1 findings remain in the same high-risk projection/fanout family.

Missing Invariant Family

Public fanout needs a single structural source-scope invariant:

  • Every public automation, webhook, notification, quick-scan, artifact, and work-item fanout path must carry the effective source scope before the first public projection decision.
  • Missing source scope is not automatically public when the event kind is derived from a branch, reference, PromotionSet, artifact, or work item that can inherit private visibility.
  • A current public snapshot can allow reveal-time publication, but it must not replay older private Created or apply-history events. Reveal remains the first public publication point for private history.
  • Synthetic or integration events that are intentionally public must have an explicit safe fallback path, with regression coverage, rather than relying on None as a blanket allow.

Fresh Findings To Prove

  • discussion_r3551031613: non-create PromotionSet events such as apply/recompute need the PromotionSet id stamped or derived before current visibility rechecks.
  • discussion_r3551031618: add-summary artifact creation events need the same PromotionSet projection scope as the later artifact-link event.
  • discussion_r3551031621: private reference Created events must remain suppressed after reveal; only the reveal event may publish the public path.

Stabilization Proof Obligations

Before requesting another review, the fix must prove the source-scope invariant across the touched fanout paths, not just the three comments:

  • PromotionSet event fanout: create, non-create, apply, recompute, and retry-like events either carry a PromotionSet id/source scope or are explicitly proven public-only.
  • Work-item summary fanout: prompt artifact create, summary artifact create, artifact-link, and PromotionSet-link events carry the same visibility source when the request has a PromotionSet id.
  • Reference fanout: private Created events remain suppressed even if the reference is later revealed; reveal remains the only public publication point for that private history.
  • Derived quick-scan and validation-result fanout cannot drop inherited source scope before public automation/eventing dispatch.
  • Webhook/Eventing/Notification tests include negative public-observer assertions for missing-source, hidden-source, deleted-source, and reveal-after-private-created cases.

Stabilization Decision

Continue #649 with a stabilization fix worker. This does not require a new product decision or a new durable/public contract concept; it is enforcement of the already-approved #649 visibility and reveal-publication contract.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5208291f3a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


return
promotionSetContext
|> Option.map (fun (promotionSet, _) -> allowsPublicProjection promotionSet.Visibility promotionSet.Ownership)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat deleted PromotionSets as hidden

When a public/repository-owned PromotionSet is logically deleted, PromotionSetActor.Exists still returns true and Get returns the DTO with DeletedAt = Some, but this current-source check ignores DeletedAt. Any stale redelivery for that PromotionSet (apply, queue, validation, work-item, etc.) therefore still passes the new visibility gate and can publish automation/webhook data for a resource that is no longer observable; include the deleted state in this decision, with any intentional deletion notification handled explicitly.

Useful? React with 👍 / 👎.

match reviewEvent.Event with
| ReviewEventType.NotesUpserted notes -> notes.PromotionSetId
| ReviewEventType.CheckpointAdded checkpoint -> checkpoint.PromotionSetId
| ReviewEventType.FindingResolved _ -> tryGetPromotionSetIdFromMetadata reviewEvent.Metadata

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stamp finding resolutions with PromotionSet scope

For ReviewEventType.FindingResolved, the event payload has no PromotionSetId, and Review.Server.processCommand only copies inherited visibility/ownership into metadata, not the PromotionSet id. This branch commonly returns None, so currentSourceAllowsPublicProjection treats redelivered finding-resolution events as unscoped and still emits ReviewNotesUpdated after the associated PromotionSet has been logically deleted/hidden; carry the Review actor's PromotionSet id in the event metadata or derive it before allowing fanout.

Useful? React with 👍 / 👎.

| ValidationResultEvent validationResultEvent ->
match validationResultEvent.Event with
| ValidationResultEventType.Recorded result -> result.PromotionSetId
| ArtifactEvent artifactEvent -> tryGetPromotionSetIdFromMetadata artifactEvent.Metadata

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope PromotionSet artifact events by ActorId

When PromotionSetActor creates conflict-report artifacts it calls ArtifactCommand.Create with actor metadata, which sets ActorId to the PromotionSet id but does not add PromotionSetId. This branch only looks for PromotionSetId, so those ArtifactCreated notifications are treated as unscoped; if the public PromotionSet is logically deleted before the artifact event is redelivered, the current-source recheck is skipped and the artifact id can still be published. Fall back to ActorId for PromotionSet-sourced artifact metadata.

Useful? React with 👍 / 👎.

@ScottArbeit

Copy link
Copy Markdown
Owner Author

Third-Cycle Stabilization Stop

Coding is stopped for #649 / PR #695 after the third substantive Codex Code Review Bot cycle on the high-risk public fanout surface.

Review Timeline

  • 393ef657: first substantive Codex cycle. Findings covered deleted reference fanout, quick-scan ordering, and summary artifact PromotionSet scope. The PR-attached Validate run also failed in ServiceBusSubscriberDispatchesMatchingWebhookOnceAndDoesNotBlockOnOutboundFailure while waiting for promotion-set.applied delivery.
  • c1c51b5c: second substantive Codex cycle. Findings stayed in the same source-scope family: non-create PromotionSet events needed scope before current visibility checks, add-summary artifact-create events needed PromotionSet scope, and private Created reference events needed to stay suppressed after reveal. A stabilization ledger was posted before the next worker.
  • 5208291f: third substantive Codex cycle. Current fresh findings are:
    • discussion_r3551253473: deleted PromotionSets must be treated as hidden, or deletion fanout needs an explicit exception.
    • discussion_r3551253479: review finding-resolution events need PromotionSet scope before ReviewNotesUpdated fanout.
    • discussion_r3551253484: PromotionSet artifact events created with actor metadata need a narrow ActorId fallback or explicit PromotionSetId stamping.

Suspected Missing Invariant Family

The existing ledger was directionally right, but still too narrow. The broader invariant is:

  • Every public fanout path must identify the current effective source before publication.
  • Deleted sources are not ordinary public sources unless a specific deletion-publication event says otherwise.
  • Events derived from review, artifact, work-item, queue, reference, and PromotionSet actors must not become public merely because their metadata is missing the most convenient id field.
  • A fallback from actor metadata to source scope is allowed only if the event shape proves which resource family the actor represents.
  • Current public visibility must not replay old private creation/apply history after reveal.

Maintainer Decisions Needed

A local unblock page has been generated at:

C:\Users\scott\AppData\Local\Temp\grace-638-orchestration\grace-649-unblock-current.html

It asks for explicit approval on:

No further coding or fix worker will be assigned until the maintainer approves the revised issue/ledger or explicitly directs continued patching.

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