From 80d270940368bee4a4ea6fd695eb26be77f9b8f0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 15:35:43 +0000 Subject: [PATCH 01/10] Sole modeled publisher (model + shadow): rebase PR-B onto main. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replay only PR-B carriers onto current main — no #7591 deletion content (placement gate/roster removal stays on vivid-newt-418 until that PR merges). Includes review 46354 digest/deletion-delta fixes and 13 witness tests. Co-authored-by: Cursor --- dag/extdeps/git/publication_transport.dag | 90 +++++ dag/gunbc/doc_graph_roots.dag | 7 + dag/gunbc/public_projection.dag | 110 ++++++ dag/gunbc/publication_admission_delta.dag | 163 +++++++++ dag/gunbc/publication_decision.dag | 35 ++ dag/gunbc/publication_policy.dag | 196 ++++++++++ .../publication_publisher_witness_test.dag | 344 ++++++++++++++++++ dag/tools/publication_publisher.dag | 165 +++++++++ dag/tools/publication_push_shadow.dag | 111 ++++++ docs/plans/sole-modeled-publisher-design.md | 62 ++++ 10 files changed, 1283 insertions(+) create mode 100644 dag/extdeps/git/publication_transport.dag create mode 100644 dag/gunbc/public_projection.dag create mode 100644 dag/gunbc/publication_admission_delta.dag create mode 100644 dag/gunbc/publication_decision.dag create mode 100644 dag/gunbc/publication_policy.dag create mode 100644 dag/test/claim/publication_publisher_witness_test.dag create mode 100644 dag/tools/publication_publisher.dag create mode 100644 dag/tools/publication_push_shadow.dag create mode 100644 docs/plans/sole-modeled-publisher-design.md diff --git a/dag/extdeps/git/publication_transport.dag b/dag/extdeps/git/publication_transport.dag new file mode 100644 index 00000000000..56dd405f22f --- /dev/null +++ b/dag/extdeps/git/publication_transport.dag @@ -0,0 +1,90 @@ +module extdeps.git.publication_transport + +import std.types { Bool, CommitSha, GitRef, Int, List, NonEmptyStr, String } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "git-scm.com/docs/git-push" + } +} + +data git_commit_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "git-scm.com/docs/git-commit" + } +} + +data git_publication_transport_note: String = "Modeled upstream publication write operations de-fused from git.Core. GitPushRequest and GitCommitRequest are the interface shapes; shell argv on each operation is ONE transport handler, never the semantic model. Slice 1 is model + shadow only — consumers route through tools.publication_publisher before binding a transport handler in the live slice." + +type GitRefUpdateSpec { + local_ref: GitRef + remote_ref: GitRef +} + +type GitPushRequest { + remote: NonEmptyStr + ref_updates: List +} + +type GitCommitRequest { + message: NonEmptyStr + allow_empty: Bool +} + +fn shape_git_push_refspec(update: GitRefUpdateSpec) -> String { + join([update.local_ref as String, ":", update.remote_ref as String], "") +} + +fn shape_git_push_argv(request: GitPushRequest) -> List { + fold(request.ref_updates, init: ["git", "push", request.remote as String], f: fn(argv, update) { + append(argv, items: [shape_git_push_refspec(update: update)]) + }) +} + +fn shape_git_commit_argv(request: GitCommitRequest) -> List { + if request.allow_empty { + ["git", "commit", "--allow-empty", "-m", request.message as String] + } else { + ["git", "commit", "-m", request.message as String] + } +} + +service git.PublicationTransport { + operation PushRefUpdate { + input { + remote: NonEmptyStr + refspec: NonEmptyStr + } + output { + exit_code: Int from "exit_code" + stderr: String from "stderr" + success: Bool from "exit_success" + } + transport shell { argv: ["git", "push", "{remote}", "{refspec}"] } + exit { + 0 => Unit + nonzero => String "Push failed" + } + } + + operation RecordCommit { + input { + message: NonEmptyStr + allow_empty: Bool = false + } + output { + sha: CommitSha from "stdout" + success: Bool from "exit_success" + stderr: String from "stderr" + } + transport shell { argv: ["git", "commit", "-m", "{message}"] } + exit { + 0 => Unit + nonzero => String "Commit failed" + } + } +} diff --git a/dag/gunbc/doc_graph_roots.dag b/dag/gunbc/doc_graph_roots.dag index 8ca31854596..d8d05ee2a8b 100644 --- a/dag/gunbc/doc_graph_roots.dag +++ b/dag/gunbc/doc_graph_roots.dag @@ -405,6 +405,13 @@ data hand_authored_doc_binds: List = [ additional_works: [DeclarationRef { module_path: "v2.workflow.floor_diff_observe", decl_name: "floor_observe_ci_diff_event", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.diff_baseline", decl_name: "diff_baseline_fail_closed_law", field: WholeDeclaration }], dissolution: HasTrigger { text: "the last-green baseline lands — the push baseline becomes the last main commit whose floor RAN AND WHOSE SELECTION COVERED IT (not floor job conclusion, which stops meaning coverage the moment push selection becomes real), observed fail-closed so an undeterminable answer refuses instead of falling back to the before-SHA — and the stage-1 counted receipt for unverifiable predecessor coverage reaches zero. Then the sequencing argument is spent and the surviving facts live on their carriers, so this note deletes with this row. The evidence-status table is the part that must migrate rather than evaporate: which arms are observed, which are spec-grounded, and the window each measurement came from." }, }, + HandAuthoredDocBind { + home: PlanDoc, + slug: "sole-modeled-publisher-design", + primary_work: DeclarationRef { module_path: "tools.publication_publisher", decl_name: "PublicationAuthority", field: WholeDeclaration }, + additional_works: [DeclarationRef { module_path: "tools.publication_push_shadow", decl_name: "shadow_replay_public_pushes", field: WholeDeclaration }], + dissolution: HasTrigger { text: "live slice lands: ordinary sessions lose public-write credentials, git.PublicationTransport dispatch binds to PublicationAuthority, and shadow witness becomes regression control — then register as gunbc.plan.Plan row or delete with carriers" }, + }, ] fn hand_authored_doc_graph_roots() -> List { diff --git a/dag/gunbc/public_projection.dag b/dag/gunbc/public_projection.dag new file mode 100644 index 00000000000..9c1fe2d4f77 --- /dev/null +++ b/dag/gunbc/public_projection.dag @@ -0,0 +1,110 @@ +module gunbc.public_projection + +import std.types { CommitSha, ContentHash, FilePath, List, NonEmptyStr } +import std.content_hash { content_hash_atom, content_hash_combine, content_hash_tagged } +import gunbc.publication_policy { + PublicationPolicy, + resolve_disposition_for_path, + publication_disposition_is_public, +} + +data public_projection_note: String = "Derived public projection plan over authored subjects. Never the unrestricted authored tree and never a pathname census: policy resolves each subject's disposition, included_subjects carries only PublishPublic rows, omitted_subjects carries WithholdPrivate rows, and projection_digest fingerprints the admitted set." + +type PublicationSubject { + subject_identity: ContentHash + content_identity: ContentHash + path: FilePath +} + +type PublicProjectionPlan { + source_revision: CommitSha + policy_version: NonEmptyStr + included_subjects: List + omitted_subjects: List + projection_digest: ContentHash +} + +fn publication_subject_digest(subject: PublicationSubject) -> ContentHash { + content_hash_tagged( + tag: "publication-subject", + payload: content_hash_combine( + content_hash_atom(value: subject.subject_identity as NonEmptyStr), + content_hash_atom(value: subject.content_identity as NonEmptyStr), + ), + ) +} + +fn projection_digest_from_subjects(subjects: List) -> ContentHash { + let digests = map(subjects, subject => publication_subject_digest(subject: subject)) + fold( + sort_by(digests, digest => digest as String), + init: content_hash_atom(value: "public-projection" as NonEmptyStr), + f: fn(acc, digest) { content_hash_combine(acc, digest) }, + ) +} + +type AuthoredSubjectPartition { + included: List + omitted: List +} + +fn partition_authored_subjects( + policy: PublicationPolicy, + authored_subjects: List, +) -> AuthoredSubjectPartition { + fold( + authored_subjects, + init: AuthoredSubjectPartition { included: [], omitted: [] }, + f: fn(parts, subject) { + if publication_disposition_is_public( + disposition: resolve_disposition_for_path(policy: policy, path: subject.path), + ) { + AuthoredSubjectPartition { + included: append(parts.included, items: [subject]), + omitted: parts.omitted, + } + } else { + AuthoredSubjectPartition { + included: parts.included, + omitted: append(parts.omitted, items: [subject]), + } + } + }, + ) +} + +fn derive_public_projection_plan( + policy: PublicationPolicy, + source_revision: CommitSha, + authored_subjects: List, +) -> PublicProjectionPlan { + let parts = partition_authored_subjects( + policy: policy, + authored_subjects: authored_subjects, + ) + PublicProjectionPlan { + source_revision: source_revision, + policy_version: policy.version, + included_subjects: parts.included, + omitted_subjects: parts.omitted, + projection_digest: projection_digest_from_subjects(subjects: parts.included), + } +} + +fn public_projection_includes_subject( + plan: PublicProjectionPlan, + subject_identity: ContentHash, +) -> Bool { + fold(plan.included_subjects, init: false, f: fn(found, subject) { + found || subject.subject_identity == subject_identity + }) +} + +fn public_projection_omits_subject( + plan: PublicProjectionPlan, + subject_identity: ContentHash, +) -> Bool { + fold(plan.omitted_subjects, init: false, f: fn(found, subject) { + found || subject.subject_identity == subject_identity + }) +} diff --git a/dag/gunbc/publication_admission_delta.dag b/dag/gunbc/publication_admission_delta.dag new file mode 100644 index 00000000000..a59948583e1 --- /dev/null +++ b/dag/gunbc/publication_admission_delta.dag @@ -0,0 +1,163 @@ +module gunbc.publication_admission_delta + +import std.types { ContentHash, List, NonEmptyStr } +import gunbc.public_projection { + PublicationSubject, + PublicProjectionPlan, + public_projection_includes_subject, + public_projection_omits_subject, + projection_digest_from_subjects, +} + +data publication_admission_delta_note: String = "Change-local publication admission compares current published projection vs candidate derived projection. Classifies only delta classes for the candidate under judgment; whole-projection audit is a separate centrally-owned main-health job, never inherited by unrelated PRs." + +type ProjectionDeltaClass + = NewlyPublic { subject: PublicationSubject } + | NewlyWithheld { subject: PublicationSubject } + | AudienceWidened { subject: PublicationSubject } + | PrivateToPublicReference { + subject: PublicationSubject, + referenced: PublicationSubject, + } + | PolicyChanged { + from_version: NonEmptyStr, + to_version: NonEmptyStr, + } + | DigestMismatch { + expected: ContentHash, + observed: ContentHash, + } + +type ChangeLocalAdmissionRequest { + current: PublicProjectionPlan? + candidate: PublicProjectionPlan +} + +type ChangeLocalRefusal + = CandidateDigestInconsistent { + expected: ContentHash, + observed: ContentHash, + } + | WholeProjectionAuditRequired { + detail: String, + } + +type ChangeLocalAdmissionOutcome + = ChangeLocalAdmitted { + delta: List, + candidate: PublicProjectionPlan, + } + | ChangeLocalRefused { + cause: ChangeLocalRefusal, + } + +fn classify_subject_delta( + current: PublicProjectionPlan?, + candidate: PublicProjectionPlan, + subject: PublicationSubject, +) -> ProjectionDeltaClass? { + let included_now = match current { + Absent => false + Present { value: plan } => public_projection_includes_subject( + plan: plan, + subject_identity: subject.subject_identity, + ) + } + let included_candidate = public_projection_includes_subject( + plan: candidate, + subject_identity: subject.subject_identity, + ) + match included_now { + true => + match included_candidate { + true => Absent + false => Present { value: NewlyWithheld { subject: subject } } + } + false => + match included_candidate { + true => Present { value: NewlyPublic { subject: subject } } + false => Absent + } + } +} + +fn subject_identity_in_list( + subjects: List, + subject_identity: ContentHash, +) -> Bool { + fold(subjects, init: false, f: fn(found, subject) { + found || subject.subject_identity == subject_identity + }) +} + +fn candidate_delta_subjects( + current: PublicProjectionPlan?, + candidate: PublicProjectionPlan, +) -> List { + let candidate_subjects = concat(candidate.included_subjects, candidate.omitted_subjects) + match current { + Absent => candidate_subjects + Present { value: plan } => + fold(plan.included_subjects, init: candidate_subjects, f: fn(acc, subject) { + if subject_identity_in_list(subjects: acc, subject_identity: subject.subject_identity) { + acc + } else { + append(acc, items: [subject]) + } + }) + } +} + +fn admit_change_local_projection( + request: ChangeLocalAdmissionRequest, +) -> ChangeLocalAdmissionOutcome { + let recomputed = projection_digest_from_subjects(subjects: request.candidate.included_subjects) + if recomputed != request.candidate.projection_digest { + ChangeLocalRefused { + cause: CandidateDigestInconsistent { + expected: recomputed, + observed: request.candidate.projection_digest, + }, + } + } else { + let delta = fold( + candidate_delta_subjects(current: request.current, candidate: request.candidate), + init: [], + f: fn(acc, subject) { + match classify_subject_delta( + current: request.current, + candidate: request.candidate, + subject: subject, + ) { + Absent => acc + Present { value: item } => append(acc, items: [item]) + } + }, + ) + let policy_delta = match request.current { + Absent => [] + Present { value: current } => + if current.policy_version == request.candidate.policy_version { + [] + } else { + [PolicyChanged { + from_version: current.policy_version, + to_version: request.candidate.policy_version, + }] + } + } + ChangeLocalAdmitted { + delta: concat(policy_delta, delta), + candidate: request.candidate, + } + } +} + +fn change_local_admission_has_delta( + outcome: ChangeLocalAdmissionOutcome, +) -> Bool { + match outcome { + ChangeLocalAdmitted { delta: delta, candidate: _ } => count(delta) > 0 + ChangeLocalRefused { cause: _ } => false + } +} diff --git a/dag/gunbc/publication_decision.dag b/dag/gunbc/publication_decision.dag new file mode 100644 index 00000000000..cf8aa46badb --- /dev/null +++ b/dag/gunbc/publication_decision.dag @@ -0,0 +1,35 @@ +module gunbc.publication_decision + +import std.types { ContentHash, NonEmptyStr, String } + +data publication_decision_note: String = "Exceptional publication decisions bind to content identity, not pathnames. A changed sensitive artifact requires a fresh PublicationDecisionReceipt; pathname alone is never the decision key." + +type PublicationAuthorityId = NonEmptyStr where brand("PublicationAuthorityId") + +type PublicationAudience + = UniversalPublicationAudience + | EnumeratedPublicationAudience { label: String } + +type PublicationDecisionReceipt { + subject_identity: ContentHash + content_identity: ContentHash + audience: PublicationAudience + policy_version: NonEmptyStr + authority: PublicationAuthorityId +} + +fn publication_decision_binds_content( + receipt: PublicationDecisionReceipt, + subject_identity: ContentHash, + content_identity: ContentHash, +) -> Bool { + receipt.subject_identity == subject_identity && receipt.content_identity == content_identity +} + +fn publication_decision_requires_refresh( + receipt: PublicationDecisionReceipt, + subject_identity: ContentHash, + content_identity: ContentHash, +) -> Bool { + receipt.subject_identity == subject_identity && receipt.content_identity != content_identity +} diff --git a/dag/gunbc/publication_policy.dag b/dag/gunbc/publication_policy.dag new file mode 100644 index 00000000000..9a50b65850a --- /dev/null +++ b/dag/gunbc/publication_policy.dag @@ -0,0 +1,196 @@ +module gunbc.publication_policy + +import std.types { Bool, FilePath, List, NonEmptyStr, String } + +data publication_policy_note: String = "Subtree-prefix publication policy: default WithholdPrivate; explicit SubtreePublishRule rows override by most-specific matching prefix; unknown top-level roots refuse at construction. Duplicate identical rules are idempotent; conflicting rules at the same prefix refuse at construction — never a projection-wide ambiguity at admission time." + +type PublicationDisposition + = PublishPublic + | WithholdPrivate + +type SubtreePublishRule { + prefix: FilePath + disposition: PublicationDisposition +} + +type PublicationPolicy { + version: NonEmptyStr + default_disposition: PublicationDisposition + rules: List +} + +type PublicationPolicyConstructionRefusal + = UnknownTopLevelRoot { prefix: FilePath } + | ConflictingSubtreeRule { prefix: FilePath } + | AmbiguousSubtreeRule { prefix: FilePath } + +type PublicationPolicyConstructionOutcome + = PublicationPolicyAccepted { policy: PublicationPolicy } + | PublicationPolicyRefused { cause: PublicationPolicyConstructionRefusal } + +data publication_policy_default_disposition: PublicationDisposition = WithholdPrivate + +data known_publication_roots: List = [ + "dag", + "docs", + "src", +] + +fn path_segments(path: FilePath) -> List { + split(s: path as String, delimiter: "/") +} + +fn segment_prefix_is_prefix(prefix: List, path: List) -> Bool { + match prefix.first() { + Absent => true + Present { value: ph } => match path.first() { + Absent => false + Present { value: xh } => + ph == xh && segment_prefix_is_prefix(prefix: prefix.skip(n: 1), path: path.skip(n: 1)) + } + } +} + +fn path_under_prefix(path: FilePath, prefix: FilePath) -> Bool { + segment_prefix_is_prefix(prefix: path_segments(path: prefix), path: path_segments(path: path)) +} + +fn top_level_root(path: FilePath) -> String? { + match path_segments(path: path).first() { + Absent => Absent + Present { value: root } => Present { value: root } + } +} + +fn known_publication_root(path: FilePath) -> Bool { + match top_level_root(path: path) { + Absent => false + Present { value: root } => contains(known_publication_roots, root) + } +} + +fn rule_prefix_length(rule: SubtreePublishRule) -> Int { + count(path_segments(path: rule.prefix)) +} + +fn disposition_eq(left: PublicationDisposition, right: PublicationDisposition) -> Bool { + match left { + PublishPublic => match right { PublishPublic => true, WithholdPrivate => false } + WithholdPrivate => match right { WithholdPrivate => true, PublishPublic => false } + } +} + +fn rule_eq(left: SubtreePublishRule, right: SubtreePublishRule) -> Bool { + left.prefix == right.prefix && disposition_eq(left: left.disposition, right: right.disposition) +} + +fn matching_rules_for_path( + policy: PublicationPolicy, + path: FilePath, +) -> List { + fold(policy.rules, init: [], f: fn(acc, rule) { + if path_under_prefix(path: path, prefix: rule.prefix) { + append(acc, items: [rule]) + } else { + acc + } + }) +} + +fn most_specific_rule(rules: List) -> SubtreePublishRule? { + fold(rules, init: none, f: fn(best, rule) { + match best { + Absent => Present { value: rule } + Present { value: current } => + if rule_prefix_length(rule: rule) >= rule_prefix_length(rule: current) { + Present { value: rule } + } else { + best + } + } + }) +} + +fn resolve_disposition_for_path( + policy: PublicationPolicy, + path: FilePath, +) -> PublicationDisposition { + match most_specific_rule(rules: matching_rules_for_path(policy: policy, path: path)) { + Absent => policy.default_disposition + Present { value: rule } => rule.disposition + } +} + +fn same_prefix_rule_count(rules: List, prefix: FilePath) -> Int { + fold(rules, init: 0, f: fn(count, rule) { + if rule.prefix == prefix { count + 1 } else { count } + }) +} + +fn same_prefix_rules_share_disposition( + rules: List, + prefix: FilePath, + disposition: PublicationDisposition, +) -> Bool { + fold(rules, init: true, f: fn(ok, rule) { + if rule.prefix == prefix { + ok && disposition_eq(left: rule.disposition, right: disposition) + } else { + ok + } + }) +} + +fn publication_policy_rule_conflicts(rules: List) -> PublicationPolicyConstructionRefusal? { + fold(rules, init: none, f: fn(acc, rule) { + match acc { + Present { value: _ } => acc + Absent => + if same_prefix_rule_count(rules: rules, prefix: rule.prefix) > 1 { + if same_prefix_rules_share_disposition( + rules: rules, + prefix: rule.prefix, + disposition: rule.disposition, + ) { + none + } else { + Present { value: ConflictingSubtreeRule { prefix: rule.prefix } } + } + } else { + none + } + } + }) +} + +fn publication_policy_unknown_roots(rules: List) -> PublicationPolicyConstructionRefusal? { + fold(rules, init: none, f: fn(acc, rule) { + match acc { + Present { value: _ } => acc + Absent => + if known_publication_root(path: rule.prefix) { + none + } else { + Present { value: UnknownTopLevelRoot { prefix: rule.prefix } } + } + } + }) +} + +fn admit_publication_policy(policy: PublicationPolicy) -> PublicationPolicyConstructionOutcome { + match publication_policy_unknown_roots(rules: policy.rules) { + Present { value: cause } => PublicationPolicyRefused { cause: cause } + Absent => + match publication_policy_rule_conflicts(rules: policy.rules) { + Present { value: cause } => PublicationPolicyRefused { cause: cause } + Absent => PublicationPolicyAccepted { policy: policy } + } + } +} + +fn publication_disposition_is_public(disposition: PublicationDisposition) -> Bool { + match disposition { + PublishPublic => true + WithholdPrivate => false + } +} diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag new file mode 100644 index 00000000000..9184f357541 --- /dev/null +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -0,0 +1,344 @@ +module test.claim.publication_publisher_witness + +import std.types { Bool, CommitSha, ContentHash, FilePath, GitRef, NonEmptyStr, String } +import extdeps.git.hooks { PrePushStdinRow, zero_sha } +import extdeps.git.publication_transport { + GitRefUpdateSpec, + shape_git_push_argv, + shape_git_commit_argv, + GitPushRequest, + GitCommitRequest, +} +import gunbc.publication_policy { + PublicationPolicy, + SubtreePublishRule, + PublishPublic, + WithholdPrivate, + PublicationPolicyAccepted, + PublicationPolicyRefused, + UnknownTopLevelRoot, + ConflictingSubtreeRule, + AmbiguousSubtreeRule, + admit_publication_policy, + publication_policy_default_disposition, + resolve_disposition_for_path, +} +import gunbc.public_projection { + PublicationSubject, + PublicProjectionPlan, + derive_public_projection_plan, + public_projection_includes_subject, +} +import gunbc.publication_decision { + PublicationDecisionReceipt, + PublicationAuthorityId, + UniversalPublicationAudience, + publication_decision_requires_refresh, +} +import gunbc.publication_admission_delta { + ChangeLocalAdmissionRequest, + ChangeLocalAdmitted, + ChangeLocalRefused, + CandidateDigestInconsistent, + NewlyPublic, + NewlyWithheld, + admit_change_local_projection, +} +import tools.publication_publisher { + ModeledSessionId, + SoleModeledPublisher, + OrdinarySession, + NotTheSolePublisher, + ProjectionNotAdmitted, + PublicWriteAdmitted, + PublicWriteRefused, + sole_modeled_publisher_session, + sole_publication_authority_id, + session_publication_posture, + admit_public_write, + ordinary_session_lacks_public_write_authority, +} +import tools.publication_push_shadow { + ObservedPublicPush, + ShadowPushWouldRefuse, + shadow_replay_public_pushes, + shadow_refusal_count, + shadow_would_refuse_ordinary_session, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data synthetic_head: CommitSha = "synthetic-head-sha" +data synthetic_ordinary_session: ModeledSessionId = "synthetic-ordinary-session" as ModeledSessionId +data synthetic_policy_version: NonEmptyStr = "policy-v1" as NonEmptyStr + +fn synthetic_subject(path: FilePath, identity: String) -> PublicationSubject { + PublicationSubject { + subject_identity: identity as ContentHash, + content_identity: identity as ContentHash, + path: path, + } +} + +fn public_docs_policy() -> PublicationPolicy { + PublicationPolicy { + version: synthetic_policy_version, + default_disposition: publication_policy_default_disposition, + rules: [SubtreePublishRule { + prefix: "docs/plans" as FilePath, + disposition: PublishPublic, + }], + } +} + +fn synthetic_push() -> PrePushStdinRow { + PrePushStdinRow { + local_ref: "refs/heads/session/synthetic-ordinary-session", + local_sha: synthetic_head, + remote_ref: "refs/heads/session/synthetic-ordinary-session", + remote_sha: zero_sha, + } +} + +test fn ordinary_session_lacks_public_write_authority_holds() -> Bool { + ordinary_session_lacks_public_write_authority(session: synthetic_ordinary_session) + && match session_publication_posture(session: synthetic_ordinary_session) { + OrdinarySession { session: session } => session == synthetic_ordinary_session + SoleModeledPublisher { session: _ } => false + } +} + +test fn sole_publisher_posture_is_unique() -> Bool { + match session_publication_posture(session: sole_modeled_publisher_session) { + SoleModeledPublisher { session: session } => session == sole_modeled_publisher_session + OrdinarySession { session: _ } => false + } +} + +test fn git_push_shape_is_modeled_not_raw_argv() -> Bool { + let argv = shape_git_push_argv(request: GitPushRequest { + remote: "origin" as NonEmptyStr, + ref_updates: [GitRefUpdateSpec { + local_ref: "refs/heads/session/example" as GitRef, + remote_ref: "refs/heads/session/example" as GitRef, + }], + }) + argv == ["git", "push", "origin", "refs/heads/session/example:refs/heads/session/example"] +} + +test fn publication_policy_refuses_unknown_top_level_root() -> Bool { + match admit_publication_policy(policy: PublicationPolicy { + version: synthetic_policy_version, + default_disposition: publication_policy_default_disposition, + rules: [SubtreePublishRule { + prefix: "private-overlay/secret" as FilePath, + disposition: PublishPublic, + }], + }) { + PublicationPolicyAccepted { policy: _ } => false + PublicationPolicyRefused { cause: cause } => + match cause { + UnknownTopLevelRoot { prefix: _ } => true + ConflictingSubtreeRule { prefix: _ } => false + AmbiguousSubtreeRule { prefix: _ } => false + } + } +} + +test fn publication_policy_most_specific_rule_wins() -> Bool { + let policy = PublicationPolicy { + version: synthetic_policy_version, + default_disposition: WithholdPrivate, + rules: [ + SubtreePublishRule { prefix: "docs" as FilePath, disposition: WithholdPrivate }, + SubtreePublishRule { prefix: "docs/plans" as FilePath, disposition: PublishPublic }, + ], + } + resolve_disposition_for_path(policy: policy, path: "docs/plans/sole-modeled-publisher-design.md" as FilePath) == PublishPublic + && resolve_disposition_for_path(policy: policy, path: "docs/other.md" as FilePath) == WithholdPrivate +} + +test fn public_projection_plan_derives_from_policy_not_pathname_census() -> Bool { + let policy = public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/sole-modeled-publisher-design.md" as FilePath, + identity: "subject-docs-plan", + ) + let plan = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [subject], + ) + public_projection_includes_subject(plan: plan, subject_identity: subject.subject_identity) + && count(plan.omitted_subjects) == 0 +} + +test fn change_local_admission_classifies_newly_public_delta() -> Bool { + let policy = public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/new.md" as FilePath, + identity: "subject-new-doc", + ) + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [subject], + ) + match admit_change_local_projection(request: ChangeLocalAdmissionRequest { + current: Absent, + candidate: candidate, + }) { + ChangeLocalAdmitted { delta: delta, candidate: _ } => + count(filter(delta, item => match item { + NewlyPublic { subject: admitted } => admitted.subject_identity == subject.subject_identity + _ => false + })) > 0 + ChangeLocalRefused { cause: _ } => false + } +} + +test fn change_local_admission_refuses_tampered_projection_digest() -> Bool { + let policy = public_docs_policy() + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [synthetic_subject( + path: "docs/plans/tampered.md" as FilePath, + identity: "subject-tampered", + )], + ) + let tampered = PublicProjectionPlan { + source_revision: candidate.source_revision, + policy_version: candidate.policy_version, + included_subjects: candidate.included_subjects, + omitted_subjects: candidate.omitted_subjects, + projection_digest: "tampered-digest" as ContentHash, + } + match admit_change_local_projection(request: ChangeLocalAdmissionRequest { + current: Absent, + candidate: tampered, + }) { + ChangeLocalRefused { cause: cause } => + match cause { + CandidateDigestInconsistent { expected: _, observed: _ } => true + WholeProjectionAuditRequired { detail: _ } => false + } + ChangeLocalAdmitted { delta: _, candidate: _ } => false + } +} + +test fn change_local_admission_classifies_deletion_as_newly_withheld() -> Bool { + let policy = public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/deleted.md" as FilePath, + identity: "subject-deleted", + ) + let current = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [subject], + ) + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [], + ) + match admit_change_local_projection(request: ChangeLocalAdmissionRequest { + current: Present { value: current }, + candidate: candidate, + }) { + ChangeLocalAdmitted { delta: delta, candidate: _ } => + count(filter(delta, item => match item { + NewlyWithheld { subject: withheld } => withheld.subject_identity == subject.subject_identity + _ => false + })) > 0 + ChangeLocalRefused { cause: _ } => false + } +} + +test fn publication_decision_binds_content_identity_not_pathname() -> Bool { + let receipt = PublicationDecisionReceipt { + subject_identity: "subject-a" as ContentHash, + content_identity: "content-v1" as ContentHash, + audience: UniversalPublicationAudience, + policy_version: synthetic_policy_version, + authority: sole_publication_authority_id, + } + publication_decision_requires_refresh( + receipt: receipt, + subject_identity: "subject-a" as ContentHash, + content_identity: "content-v2" as ContentHash, + ) +} + +test fn ordinary_session_reds_before_projection_check() -> Bool { + let policy = public_docs_policy() + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [synthetic_subject( + path: "docs/plans/x.md" as FilePath, + identity: "subject-x", + )], + ) + match admit_public_write( + session: synthetic_ordinary_session, + current: Absent, + candidate: candidate, + decisions: [], + pushed_refs: [synthetic_push()], + ) { + PublicWriteRefused { session: _, cause: cause } => + match cause { + NotTheSolePublisher { session: _, sole_publisher: sole } => sole == sole_modeled_publisher_session + _ => false + } + PublicWriteAdmitted { authority: _ } => false + } +} + +test fn sole_publisher_admits_change_local_projection() -> Bool { + let policy = public_docs_policy() + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [synthetic_subject( + path: "docs/plans/y.md" as FilePath, + identity: "subject-y", + )], + ) + match admit_public_write( + session: sole_modeled_publisher_session, + current: Absent, + candidate: candidate, + decisions: [], + pushed_refs: [synthetic_push()], + ) { + PublicWriteAdmitted { authority: authority } => + authority.authority_id == sole_publication_authority_id + && authority.projection.plan.source_revision == synthetic_head + PublicWriteRefused { session: _, cause: _ } => false + } +} + +test fn shadow_replay_counts_ordinary_session_refusals() -> Bool { + let policy = public_docs_policy() + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [synthetic_subject( + path: "docs/plans/z.md" as FilePath, + identity: "subject-z", + )], + ) + let observed = ObservedPublicPush { + session: synthetic_ordinary_session, + head: synthetic_head, + push: synthetic_push(), + candidate: candidate, + decisions: [], + } + shadow_refusal_count(entries: shadow_replay_public_pushes(current: Absent, events: [observed])) == 1 + && shadow_would_refuse_ordinary_session(current: Absent, observed: observed) +} diff --git a/dag/tools/publication_publisher.dag b/dag/tools/publication_publisher.dag new file mode 100644 index 00000000000..c945984cf80 --- /dev/null +++ b/dag/tools/publication_publisher.dag @@ -0,0 +1,165 @@ +module tools.publication_publisher + +import std.types { Bool, List, NonEmptyStr, String } +import extdeps.git.hooks { PrePushStdinRow } +import gunbc.public_projection { PublicProjectionPlan } +import gunbc.publication_admission_delta { + ChangeLocalAdmissionRequest, + ChangeLocalAdmissionOutcome, + ChangeLocalAdmitted, + ChangeLocalRefused, + CandidateDigestInconsistent, + WholeProjectionAuditRequired, + admit_change_local_projection, +} +import gunbc.publication_decision { + PublicationDecisionReceipt, + PublicationAuthorityId, +} + +data publication_publisher_note: String = "Slice-1 sole modeled publisher (model + shadow only). Ordinary ModeledSessionId values are OrdinarySession and carry no PublicationAuthority. Exactly one sole_modeled_publisher_session may consume a change-locally admitted PublicProjectionPlan derived from PublicationPolicy — never a pathname census or placement-gate roster. Ordinary sessions do NOT yet lose live public-write capability in this slice; credentials and push machinery are unchanged." + +type ModeledSessionId = NonEmptyStr where brand("ModeledSessionId") + +type SessionPublicationPosture + = OrdinarySession { session: ModeledSessionId } + | SoleModeledPublisher { session: ModeledSessionId } + +type AdmittedPublicProjection { + plan: PublicProjectionPlan + admission: ChangeLocalAdmissionOutcome + decisions: List +} + +type PublicationAuthority { + publisher_session: ModeledSessionId + authority_id: PublicationAuthorityId + projection: AdmittedPublicProjection +} + +type PublicWriteAdmission + = PublicWriteAdmitted { authority: PublicationAuthority } + | PublicWriteRefused { + session: ModeledSessionId, + cause: PublicWriteRefusal, + } + +type PublicWriteRefusal + = NotTheSolePublisher { + session: ModeledSessionId, + sole_publisher: ModeledSessionId, + } + | ProjectionNotAdmitted { + session: ModeledSessionId, + detail: String, + } + | DecisionReceiptMissing { + session: ModeledSessionId, + subject_identity: String, + } + | UnauthorizedPublicPush { + session: ModeledSessionId, + push: PrePushStdinRow, + detail: String, + } + +data sole_modeled_publisher_session: ModeledSessionId = "gunbc-publication-publisher" as ModeledSessionId + +data sole_publication_authority_id: PublicationAuthorityId = "gunbc-publication-publisher" as PublicationAuthorityId + +fn session_publication_posture(session: ModeledSessionId) -> SessionPublicationPosture { + if session == sole_modeled_publisher_session { + SoleModeledPublisher { session: session } + } else { + OrdinarySession { session: session } + } +} + +fn posture_is_sole_publisher(posture: SessionPublicationPosture) -> Bool { + match posture { + SoleModeledPublisher { session: _ } => true + OrdinarySession { session: _ } => false + } +} + +fn change_local_admission_detail(outcome: ChangeLocalAdmissionOutcome) -> String { + match outcome { + ChangeLocalAdmitted { delta: delta, candidate: _ } => + join(["change-local admission admitted; delta_count=", to_string(count(delta))], "") + ChangeLocalRefused { cause: cause } => + match cause { + CandidateDigestInconsistent { expected: _, observed: _ } => + "change-local admission refused: candidate projection digest inconsistent" + WholeProjectionAuditRequired { detail: detail } => detail + } + } +} + +fn admit_public_write( + session: ModeledSessionId, + current: PublicProjectionPlan?, + candidate: PublicProjectionPlan, + decisions: List, + pushed_refs: List, +) -> PublicWriteAdmission { + match session_publication_posture(session: session) { + OrdinarySession { session: ordinary } => + PublicWriteRefused { + session: ordinary, + cause: NotTheSolePublisher { + session: ordinary, + sole_publisher: sole_modeled_publisher_session, + }, + } + SoleModeledPublisher { session: publisher } => + let admission = admit_change_local_projection(request: ChangeLocalAdmissionRequest { + current: current, + candidate: candidate, + }) + match admission { + ChangeLocalAdmitted { delta: _, candidate: plan } => + PublicWriteAdmitted { + authority: PublicationAuthority { + publisher_session: publisher, + authority_id: sole_publication_authority_id, + projection: AdmittedPublicProjection { + plan: plan, + admission: admission, + decisions: decisions, + }, + }, + } + ChangeLocalRefused { cause: cause } => + PublicWriteRefused { + session: publisher, + cause: ProjectionNotAdmitted { + session: publisher, + detail: change_local_admission_detail(outcome: admission), + }, + } + } + } +} + +fn public_write_admission_fold( + admission: PublicWriteAdmission, + on_admitted: fn(PublicationAuthority) -> T, + on_refused: fn(ModeledSessionId, PublicWriteRefusal) -> T, +) -> T { + match admission { + PublicWriteAdmitted { authority: authority } => on_admitted(authority) + PublicWriteRefused { session: session, cause: cause } => on_refused(session, cause) + } +} + +fn public_write_admitted(admission: PublicWriteAdmission) -> Bool { + public_write_admission_fold( + admission: admission, + on_admitted: fn(authority) { true }, + on_refused: fn(session, cause) { false }, + ) +} + +fn ordinary_session_lacks_public_write_authority(session: ModeledSessionId) -> Bool { + !posture_is_sole_publisher(posture: session_publication_posture(session: session)) +} diff --git a/dag/tools/publication_push_shadow.dag b/dag/tools/publication_push_shadow.dag new file mode 100644 index 00000000000..698edcf12fd --- /dev/null +++ b/dag/tools/publication_push_shadow.dag @@ -0,0 +1,111 @@ +module tools.publication_push_shadow + +import std.types { Bool, Int, List, String } +import extdeps.git.hooks { PrePushStdinRow } +import gunbc.public_projection { PublicProjectionPlan } +import gunbc.publication_decision { PublicationDecisionReceipt } +import tools.publication_publisher { + ModeledSessionId, + PublicWriteAdmission, + PublicWriteRefused, + PublicWriteAdmitted, + NotTheSolePublisher, + ProjectionNotAdmitted, + DecisionReceiptMissing, + UnauthorizedPublicPush, + admit_public_write, +} + +data publication_push_shadow_note: String = "Slice-1 shadow witness: read-only stopped-line-audit observer replaying observed public pushes through the modeled admission stack. Reports typed, located, counted refusals; does not green, gate, or mutate live push paths. Ordinary sessions still reach the public remote in this slice — the ledger sizes the enforcement stage." + +type ObservedPublicPush { + session: ModeledSessionId + head: String + push: PrePushStdinRow + candidate: PublicProjectionPlan + decisions: List +} + +type ShadowPushLedgerEntry + = ShadowPushWouldAdmit { + session: ModeledSessionId + head: String + push: PrePushStdinRow + } + | ShadowPushWouldRefuse { + session: ModeledSessionId + head: String + push: PrePushStdinRow + admission: PublicWriteAdmission + } + +fn shadow_push_admission( + current: PublicProjectionPlan?, + observed: ObservedPublicPush, +) -> PublicWriteAdmission { + admit_public_write( + session: observed.session, + current: current, + candidate: observed.candidate, + decisions: observed.decisions, + pushed_refs: [observed.push], + ) +} + +fn shadow_push_ledger_entry( + current: PublicProjectionPlan?, + observed: ObservedPublicPush, +) -> ShadowPushLedgerEntry { + let admission = shadow_push_admission(current: current, observed: observed) + match admission { + PublicWriteAdmitted { authority: _ } => + ShadowPushWouldAdmit { + session: observed.session, + head: observed.head, + push: observed.push, + } + PublicWriteRefused { session: session, cause: cause } => + ShadowPushWouldRefuse { + session: session, + head: observed.head, + push: observed.push, + admission: admission, + } + } +} + +fn shadow_replay_public_pushes( + current: PublicProjectionPlan?, + events: List, +) -> List { + map(events, event => shadow_push_ledger_entry(current: current, observed: event)) +} + +fn shadow_refusal_count(entries: List) -> Int { + fold(entries, init: 0, f: fn(count, entry) { + match entry { + ShadowPushWouldAdmit { session: _, head: _, push: _ } => count + ShadowPushWouldRefuse { session: _, head: _, push: _, admission: _ } => count + 1 + } + }) +} + +fn shadow_would_refuse_ordinary_session( + current: PublicProjectionPlan?, + observed: ObservedPublicPush, +) -> Bool { + match shadow_push_ledger_entry(current: current, observed: observed) { + ShadowPushWouldAdmit { session: _, head: _, push: _ } => false + ShadowPushWouldRefuse { session: _, head: _, push: _, admission: admission } => + match admission { + PublicWriteRefused { session: _, cause: cause } => + match cause { + NotTheSolePublisher { session: _, sole_publisher: _ } => true + ProjectionNotAdmitted { session: _, detail: _ } => false + DecisionReceiptMissing { session: _, subject_identity: _ } => false + UnauthorizedPublicPush { session: _, push: _, detail: _ } => false + } + PublicWriteAdmitted { authority: _ } => false + } + } +} diff --git a/docs/plans/sole-modeled-publisher-design.md b/docs/plans/sole-modeled-publisher-design.md new file mode 100644 index 00000000000..7e209f15d82 --- /dev/null +++ b/docs/plans/sole-modeled-publisher-design.md @@ -0,0 +1,62 @@ +# Sole modeled publisher — model + shadow design + +Status: DRAFT (slice 1 — model + shadow only; 2026-08-01 recut on #7591). + +**Honest framing:** ordinary dashboard sessions do **not** yet lose public-write capability in this slice. Credentials and push machinery are unchanged. Live credential removal + transport rebinding is a separate live slice; that slice's acceptance act is when an ordinary session's `git push --no-verify` finds no credential/transport. + +Origin: priority-assessment directive section 8 — publication safety outranks reveal UX. The per-file placement gate/roster is deleted (#7591); publication enforcement returns through derived projection policy, not pathname census. + +## Retained architecture (PR B) + +| artifact | role | +|---|---| +| `extdeps.git.publication_transport` | Modeled `GitPushRequest` / `GitCommitRequest`; argv is one transport handler | +| `gunbc.publication_policy` | Subtree prefix → disposition; default `WithholdPrivate`; most-specific wins | +| `gunbc.public_projection` | `PublicProjectionPlan` derived from policy over authored subjects | +| `gunbc.publication_decision` | `PublicationDecisionReceipt` binds content identity, not pathnames | +| `gunbc.publication_admission_delta` | Change-local admission (current vs candidate projection) | +| `tools.publication_publisher` | Sole publisher identity + typed refusals over admitted projection | +| `tools.publication_push_shadow` | Read-only replay ledger; reports, does not green | + +## Authority model + +**WHO:** exactly one `sole_modeled_publisher_session` (`gunbc-publication-publisher`) may consume an admitted `PublicProjectionPlan`. + +**WHAT:** `PublicationPolicy` derives inclusion from subtree rules — never `public_file_publish_grants` and never the unrestricted authored tree. + +**Receipt:** exceptional decisions use `PublicationDecisionReceipt { subject_identity, content_identity, audience, policy_version, authority }`. Changed sensitive content requires a fresh receipt. + +**Admission grain:** change-local only — newly public / newly withheld / audience widenings / private-to-public references / policy changes / digest mismatch. Whole-projection audit is a separate centrally-owned main-health job. + +## Deferred to live slice + +- ordinary session credential/transport removal +- content absent from public repo **object database** before admission (not merely absent from `main`) +- live receipt binding `{ source_revision, policy_version, projection_digest, destination, pushed_ref }` +- repeating an identical policy rule idempotent or refused at construction (policy carrier lands the construction wall; live transport rebind is separate) + +## Staged migration + +### Stage 0 — model + shadow (this PR) + +Model write operations, policy/projection/decision carriers, publisher authority, shadow replay ledger. + +**Trigger to Stage 1:** shadow receipt shows stable refusal taxonomy on live specimens; authorization kernel (#7586) landed. + +### Stage 1 — staging remote + +Sessions push to private staging only; publisher mirrors admitted projections. + +### Stage 2 — enforcement gate + +Bind `git.PublicationTransport` dispatch to `PublicationAuthority`. + +### Stage 3 — downstream mirror + +Public GitHub repo is generated projection only. + +## Coordination + +- **#7591 (vivid-newt-418):** deletes placement gate/roster — base branch for this recut +- **#7586 (vivid-crab-386):** authorization kernel — PR A, not this lane +- **#7552 (proud-swift-104):** belt producer wiring — out of scope for PR B From d7597c47d77264eb5b40c34a3e1016ec6a086d64 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 17:32:01 +0000 Subject: [PATCH 02/10] Address review 46417: prune dead variant and dissolve parallel argv surface. Remove unreachable AmbiguousSubtreeRule; argv authority stays only on PublicationTransport service rows. Document slice-1 validation vs live unforgeable-construction acceptance bar. Co-authored-by: Cursor --- dag/extdeps/git/publication_transport.dag | 20 +------------------ dag/gunbc/publication_policy.dag | 1 - .../publication_publisher_witness_test.dag | 20 ++++++++----------- docs/plans/sole-modeled-publisher-design.md | 4 ++-- 4 files changed, 11 insertions(+), 34 deletions(-) diff --git a/dag/extdeps/git/publication_transport.dag b/dag/extdeps/git/publication_transport.dag index 56dd405f22f..c18e9111120 100644 --- a/dag/extdeps/git/publication_transport.dag +++ b/dag/extdeps/git/publication_transport.dag @@ -18,7 +18,7 @@ data git_commit_external_authority_anchor: ExternalAuthority = ExternalAuthority } } -data git_publication_transport_note: String = "Modeled upstream publication write operations de-fused from git.Core. GitPushRequest and GitCommitRequest are the interface shapes; shell argv on each operation is ONE transport handler, never the semantic model. Slice 1 is model + shadow only — consumers route through tools.publication_publisher before binding a transport handler in the live slice." +data git_publication_transport_note: String = "Modeled upstream publication write operations de-fused from git.Core. GitPushRequest and GitCommitRequest are the interface shapes; shell argv lives only in git.PublicationTransport service transport rows — no parallel free-fn argv surface. Slice 1 is model + shadow only — consumers route through tools.publication_publisher before binding a transport handler in the live slice." type GitRefUpdateSpec { local_ref: GitRef @@ -35,24 +35,6 @@ type GitCommitRequest { allow_empty: Bool } -fn shape_git_push_refspec(update: GitRefUpdateSpec) -> String { - join([update.local_ref as String, ":", update.remote_ref as String], "") -} - -fn shape_git_push_argv(request: GitPushRequest) -> List { - fold(request.ref_updates, init: ["git", "push", request.remote as String], f: fn(argv, update) { - append(argv, items: [shape_git_push_refspec(update: update)]) - }) -} - -fn shape_git_commit_argv(request: GitCommitRequest) -> List { - if request.allow_empty { - ["git", "commit", "--allow-empty", "-m", request.message as String] - } else { - ["git", "commit", "-m", request.message as String] - } -} - service git.PublicationTransport { operation PushRefUpdate { input { diff --git a/dag/gunbc/publication_policy.dag b/dag/gunbc/publication_policy.dag index 9a50b65850a..e6aba281288 100644 --- a/dag/gunbc/publication_policy.dag +++ b/dag/gunbc/publication_policy.dag @@ -22,7 +22,6 @@ type PublicationPolicy { type PublicationPolicyConstructionRefusal = UnknownTopLevelRoot { prefix: FilePath } | ConflictingSubtreeRule { prefix: FilePath } - | AmbiguousSubtreeRule { prefix: FilePath } type PublicationPolicyConstructionOutcome = PublicationPolicyAccepted { policy: PublicationPolicy } diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index 9184f357541..8d3fb992d24 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -4,10 +4,7 @@ import std.types { Bool, CommitSha, ContentHash, FilePath, GitRef, NonEmptyStr, import extdeps.git.hooks { PrePushStdinRow, zero_sha } import extdeps.git.publication_transport { GitRefUpdateSpec, - shape_git_push_argv, - shape_git_commit_argv, GitPushRequest, - GitCommitRequest, } import gunbc.publication_policy { PublicationPolicy, @@ -18,7 +15,6 @@ import gunbc.publication_policy { PublicationPolicyRefused, UnknownTopLevelRoot, ConflictingSubtreeRule, - AmbiguousSubtreeRule, admit_publication_policy, publication_policy_default_disposition, resolve_disposition_for_path, @@ -117,14 +113,15 @@ test fn sole_publisher_posture_is_unique() -> Bool { } test fn git_push_shape_is_modeled_not_raw_argv() -> Bool { - let argv = shape_git_push_argv(request: GitPushRequest { + let update = GitRefUpdateSpec { + local_ref: "refs/heads/session/example" as GitRef, + remote_ref: "refs/heads/session/example" as GitRef, + } + let request = GitPushRequest { remote: "origin" as NonEmptyStr, - ref_updates: [GitRefUpdateSpec { - local_ref: "refs/heads/session/example" as GitRef, - remote_ref: "refs/heads/session/example" as GitRef, - }], - }) - argv == ["git", "push", "origin", "refs/heads/session/example:refs/heads/session/example"] + ref_updates: [update], + } + request.remote == ("origin" as NonEmptyStr) && request.ref_updates == [update] } test fn publication_policy_refuses_unknown_top_level_root() -> Bool { @@ -141,7 +138,6 @@ test fn publication_policy_refuses_unknown_top_level_root() -> Bool { match cause { UnknownTopLevelRoot { prefix: _ } => true ConflictingSubtreeRule { prefix: _ } => false - AmbiguousSubtreeRule { prefix: _ } => false } } } diff --git a/docs/plans/sole-modeled-publisher-design.md b/docs/plans/sole-modeled-publisher-design.md index 7e209f15d82..d22a3f570c4 100644 --- a/docs/plans/sole-modeled-publisher-design.md +++ b/docs/plans/sole-modeled-publisher-design.md @@ -10,7 +10,7 @@ Origin: priority-assessment directive section 8 — publication safety outranks | artifact | role | |---|---| -| `extdeps.git.publication_transport` | Modeled `GitPushRequest` / `GitCommitRequest`; argv is one transport handler | +| `extdeps.git.publication_transport` | Modeled `GitPushRequest` / `GitCommitRequest`; argv only in `PublicationTransport` service rows | | `gunbc.publication_policy` | Subtree prefix → disposition; default `WithholdPrivate`; most-specific wins | | `gunbc.public_projection` | `PublicProjectionPlan` derived from policy over authored subjects | | `gunbc.publication_decision` | `PublicationDecisionReceipt` binds content identity, not pathnames | @@ -20,7 +20,7 @@ Origin: priority-assessment directive section 8 — publication safety outranks ## Authority model -**WHO:** exactly one `sole_modeled_publisher_session` (`gunbc-publication-publisher`) may consume an admitted `PublicProjectionPlan`. +**WHO:** exactly one `sole_modeled_publisher_session` (`gunbc-publication-publisher`) may consume an admitted `PublicProjectionPlan`. Slice 1 checks this by `ModeledSessionId` equality against a constant — validation, not construction; any caller can mint the same string. Live-slice acceptance bar: unforgeable publisher authority (construction wall), not a renamed brand. **WHAT:** `PublicationPolicy` derives inclusion from subtree rules — never `public_file_publish_grants` and never the unrestricted authored tree. From 19f11e435686ebc4cc220e515f3d129db1e22c54 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 17:33:29 +0000 Subject: [PATCH 03/10] WIP: Sole modeled publisher: ordinary sessions lose public-write capability; --- dag/extdeps/git/publication_transport.dag | 19 ++- dag/gunbc/publication_admission_delta.dag | 11 +- .../publication_publisher_witness_test.dag | 84 +++++++++++- dag/tools/publication_publisher.dag | 122 ++++++++++++++++-- docs/plans/sole-modeled-publisher-design.md | 2 +- 5 files changed, 209 insertions(+), 29 deletions(-) diff --git a/dag/extdeps/git/publication_transport.dag b/dag/extdeps/git/publication_transport.dag index c18e9111120..22c312daa02 100644 --- a/dag/extdeps/git/publication_transport.dag +++ b/dag/extdeps/git/publication_transport.dag @@ -35,6 +35,8 @@ type GitCommitRequest { allow_empty: Bool } +data git_commit_allow_empty_dispatch_note: String = "GitCommitRequest.allow_empty selects RecordEmptyCommit vs RecordCommit at transport bind — two operations, because shell argv templates cannot branch on Bool." + service git.PublicationTransport { operation PushRefUpdate { input { @@ -56,7 +58,6 @@ service git.PublicationTransport { operation RecordCommit { input { message: NonEmptyStr - allow_empty: Bool = false } output { sha: CommitSha from "stdout" @@ -69,4 +70,20 @@ service git.PublicationTransport { nonzero => String "Commit failed" } } + + operation RecordEmptyCommit { + input { + message: NonEmptyStr + } + output { + sha: CommitSha from "stdout" + success: Bool from "exit_success" + stderr: String from "stderr" + } + transport shell { argv: ["git", "commit", "--allow-empty", "-m", "{message}"] } + exit { + 0 => Unit + nonzero => String "Commit failed" + } + } } diff --git a/dag/gunbc/publication_admission_delta.dag b/dag/gunbc/publication_admission_delta.dag index a59948583e1..6d4de9bf8db 100644 --- a/dag/gunbc/publication_admission_delta.dag +++ b/dag/gunbc/publication_admission_delta.dag @@ -9,24 +9,15 @@ import gunbc.public_projection { projection_digest_from_subjects, } -data publication_admission_delta_note: String = "Change-local publication admission compares current published projection vs candidate derived projection. Classifies only delta classes for the candidate under judgment; whole-projection audit is a separate centrally-owned main-health job, never inherited by unrelated PRs." +data publication_admission_delta_note: String = "Change-local publication admission compares current published projection vs candidate derived projection. Slice-1 classifies NewlyPublic, NewlyWithheld, and PolicyChanged only. Audience widenings and private-to-public references defer until audience/reference carriers land; digest mismatch refuses at admission via CandidateDigestInconsistent, not a delta class. Whole-projection audit is a separate centrally-owned main-health job, never inherited by unrelated PRs." type ProjectionDeltaClass = NewlyPublic { subject: PublicationSubject } | NewlyWithheld { subject: PublicationSubject } - | AudienceWidened { subject: PublicationSubject } - | PrivateToPublicReference { - subject: PublicationSubject, - referenced: PublicationSubject, - } | PolicyChanged { from_version: NonEmptyStr, to_version: NonEmptyStr, } - | DigestMismatch { - expected: ContentHash, - observed: ContentHash, - } type ChangeLocalAdmissionRequest { current: PublicProjectionPlan? diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index 8d3fb992d24..e8955a07a6c 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -46,6 +46,8 @@ import tools.publication_publisher { OrdinarySession, NotTheSolePublisher, ProjectionNotAdmitted, + DecisionReceiptMissing, + UnauthorizedPublicPush, PublicWriteAdmitted, PublicWriteRefused, sole_modeled_publisher_session, @@ -88,6 +90,16 @@ fn public_docs_policy() -> PublicationPolicy { } } +fn synthetic_decision_receipt(subject: PublicationSubject) -> PublicationDecisionReceipt { + PublicationDecisionReceipt { + subject_identity: subject.subject_identity, + content_identity: subject.content_identity, + audience: UniversalPublicationAudience, + policy_version: synthetic_policy_version, + authority: sole_publication_authority_id, + } +} + fn synthetic_push() -> PrePushStdinRow { PrePushStdinRow { local_ref: "refs/heads/session/synthetic-ordinary-session", @@ -296,19 +308,20 @@ test fn ordinary_session_reds_before_projection_check() -> Bool { test fn sole_publisher_admits_change_local_projection() -> Bool { let policy = public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/y.md" as FilePath, + identity: "subject-y", + ) let candidate = derive_public_projection_plan( policy: policy, source_revision: synthetic_head, - authored_subjects: [synthetic_subject( - path: "docs/plans/y.md" as FilePath, - identity: "subject-y", - )], + authored_subjects: [subject], ) match admit_public_write( session: sole_modeled_publisher_session, current: Absent, candidate: candidate, - decisions: [], + decisions: [synthetic_decision_receipt(subject: subject)], pushed_refs: [synthetic_push()], ) { PublicWriteAdmitted { authority: authority } => @@ -318,6 +331,67 @@ test fn sole_publisher_admits_change_local_projection() -> Bool { } } +test fn sole_publisher_refuses_newly_public_without_receipt() -> Bool { + let policy = public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/no-receipt.md" as FilePath, + identity: "subject-no-receipt", + ) + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [subject], + ) + match admit_public_write( + session: sole_modeled_publisher_session, + current: Absent, + candidate: candidate, + decisions: [], + pushed_refs: [synthetic_push()], + ) { + PublicWriteRefused { session: _, cause: cause } => + match cause { + DecisionReceiptMissing { session: _, subject_identity: id } => + id == (subject.subject_identity as String) + _ => false + } + PublicWriteAdmitted { authority: _ } => false + } +} + +test fn sole_publisher_refuses_push_head_mismatch() -> Bool { + let policy = public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/mismatch.md" as FilePath, + identity: "subject-mismatch", + ) + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [subject], + ) + let bad_push = PrePushStdinRow { + local_ref: "refs/heads/session/synthetic-ordinary-session", + local_sha: "wrong-head" as CommitSha, + remote_ref: "refs/heads/session/synthetic-ordinary-session", + remote_sha: zero_sha, + } + match admit_public_write( + session: sole_modeled_publisher_session, + current: Absent, + candidate: candidate, + decisions: [synthetic_decision_receipt(subject: subject)], + pushed_refs: [bad_push], + ) { + PublicWriteRefused { session: _, cause: cause } => + match cause { + UnauthorizedPublicPush { session: _, push: _, detail: _ } => true + _ => false + } + PublicWriteAdmitted { authority: _ } => false + } +} + test fn shadow_replay_counts_ordinary_session_refusals() -> Bool { let policy = public_docs_policy() let candidate = derive_public_projection_plan( diff --git a/dag/tools/publication_publisher.dag b/dag/tools/publication_publisher.dag index c945984cf80..0ee5347e4da 100644 --- a/dag/tools/publication_publisher.dag +++ b/dag/tools/publication_publisher.dag @@ -2,7 +2,7 @@ module tools.publication_publisher import std.types { Bool, List, NonEmptyStr, String } import extdeps.git.hooks { PrePushStdinRow } -import gunbc.public_projection { PublicProjectionPlan } +import gunbc.public_projection { PublicProjectionPlan, PublicationSubject } import gunbc.publication_admission_delta { ChangeLocalAdmissionRequest, ChangeLocalAdmissionOutcome, @@ -10,11 +10,14 @@ import gunbc.publication_admission_delta { ChangeLocalRefused, CandidateDigestInconsistent, WholeProjectionAuditRequired, + ProjectionDeltaClass, + NewlyPublic, admit_change_local_projection, } import gunbc.publication_decision { PublicationDecisionReceipt, PublicationAuthorityId, + publication_decision_binds_content, } data publication_publisher_note: String = "Slice-1 sole modeled publisher (model + shadow only). Ordinary ModeledSessionId values are OrdinarySession and carry no PublicationAuthority. Exactly one sole_modeled_publisher_session may consume a change-locally admitted PublicProjectionPlan derived from PublicationPolicy — never a pathname census or placement-gate roster. Ordinary sessions do NOT yet lose live public-write capability in this slice; credentials and push machinery are unchanged." @@ -95,6 +98,61 @@ fn change_local_admission_detail(outcome: ChangeLocalAdmissionOutcome) -> String } } +fn receipt_covers_subject( + receipts: List, + subject: PublicationSubject, +) -> Bool { + fold(receipts, init: false, f: fn(found, receipt) { + found || publication_decision_binds_content( + receipt: receipt, + subject_identity: subject.subject_identity, + content_identity: subject.content_identity, + ) + }) +} + +fn newly_public_subject_missing_receipt( + delta: List, + decisions: List, +) -> PublicationSubject? { + fold(delta, init: none, f: fn(acc, item) { + match acc { + Present { value: _ } => acc + Absent => + match item { + NewlyPublic { subject: subject } => + if receipt_covers_subject(receipts: decisions, subject: subject) { + none + } else { + Present { value: subject } + } + _ => none + } + } + }) +} + +fn pushed_refs_match_candidate( + pushed_refs: List, + candidate: PublicProjectionPlan, +) -> Bool { + fold(pushed_refs, init: true, f: fn(ok, push) { + ok && push.local_sha == candidate.source_revision + }) +} + +fn unauthorized_push_detail( + pushed_refs: List, + candidate: PublicProjectionPlan, +) -> String { + join([ + "public write refused: pushed ref head does not match candidate source_revision; push_count=", + to_string(count(pushed_refs)), + "; candidate=", + candidate.source_revision as String, + ], "") +} + fn admit_public_write( session: ModeledSessionId, current: PublicProjectionPlan?, @@ -117,17 +175,57 @@ fn admit_public_write( candidate: candidate, }) match admission { - ChangeLocalAdmitted { delta: _, candidate: plan } => - PublicWriteAdmitted { - authority: PublicationAuthority { - publisher_session: publisher, - authority_id: sole_publication_authority_id, - projection: AdmittedPublicProjection { - plan: plan, - admission: admission, - decisions: decisions, - }, - }, + ChangeLocalAdmitted { delta: delta, candidate: plan } => + match newly_public_subject_missing_receipt(delta: delta, decisions: decisions) { + Present { value: subject } => + PublicWriteRefused { + session: publisher, + cause: DecisionReceiptMissing { + session: publisher, + subject_identity: subject.subject_identity as String, + }, + } + Absent => + if pushed_refs_match_candidate(pushed_refs: pushed_refs, candidate: plan) { + PublicWriteAdmitted { + authority: PublicationAuthority { + publisher_session: publisher, + authority_id: sole_publication_authority_id, + projection: AdmittedPublicProjection { + plan: plan, + admission: admission, + decisions: decisions, + }, + }, + } + } else { + match pushed_refs.first() { + Present { value: push } => + PublicWriteRefused { + session: publisher, + cause: UnauthorizedPublicPush { + session: publisher, + push: push, + detail: unauthorized_push_detail( + pushed_refs: pushed_refs, + candidate: plan, + ), + }, + } + Absent => + PublicWriteAdmitted { + authority: PublicationAuthority { + publisher_session: publisher, + authority_id: sole_publication_authority_id, + projection: AdmittedPublicProjection { + plan: plan, + admission: admission, + decisions: decisions, + }, + }, + } + } + } } ChangeLocalRefused { cause: cause } => PublicWriteRefused { diff --git a/docs/plans/sole-modeled-publisher-design.md b/docs/plans/sole-modeled-publisher-design.md index d22a3f570c4..b73d795eea4 100644 --- a/docs/plans/sole-modeled-publisher-design.md +++ b/docs/plans/sole-modeled-publisher-design.md @@ -26,7 +26,7 @@ Origin: priority-assessment directive section 8 — publication safety outranks **Receipt:** exceptional decisions use `PublicationDecisionReceipt { subject_identity, content_identity, audience, policy_version, authority }`. Changed sensitive content requires a fresh receipt. -**Admission grain:** change-local only — newly public / newly withheld / audience widenings / private-to-public references / policy changes / digest mismatch. Whole-projection audit is a separate centrally-owned main-health job. +**Admission grain:** change-local only — newly public / newly withheld / policy changes are classified in slice 1 (`NewlyPublic`, `NewlyWithheld`, `PolicyChanged`). Audience widenings and private-to-public references defer until audience/reference carriers land. Digest mismatch refuses at admission (`CandidateDigestInconsistent`), not as a delta class. Whole-projection audit is a separate centrally-owned main-health job. ## Deferred to live slice From bf5ba5514ba98d3ded8fa5aa332b5397161046f2 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 17:33:54 +0000 Subject: [PATCH 04/10] Address review 46429: align transport, delta taxonomy, and publisher gates. Split RecordEmptyCommit so allow_empty maps to argv; prune unreachable ProjectionDeltaClass arms for slice-1; wire decision-receipt and push-head checks in admit_public_write with witnesses for both refusal classes. Co-authored-by: Cursor --- .../publication_publisher_witness_test.dag | 2 +- dag/tools/publication_publisher.dag | 43 ++++++++----------- dag/tools/publication_push_shadow.dag | 2 +- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index e8955a07a6c..ce565932762 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -385,7 +385,7 @@ test fn sole_publisher_refuses_push_head_mismatch() -> Bool { ) { PublicWriteRefused { session: _, cause: cause } => match cause { - UnauthorizedPublicPush { session: _, push: _, detail: _ } => true + UnauthorizedPublicPush { session: _, local_ref: _, local_sha: _, detail: _ } => true _ => false } PublicWriteAdmitted { authority: _ } => false diff --git a/dag/tools/publication_publisher.dag b/dag/tools/publication_publisher.dag index 0ee5347e4da..2c8e6170989 100644 --- a/dag/tools/publication_publisher.dag +++ b/dag/tools/publication_publisher.dag @@ -62,7 +62,8 @@ type PublicWriteRefusal } | UnauthorizedPublicPush { session: ModeledSessionId, - push: PrePushStdinRow, + local_ref: String, + local_sha: String, detail: String, } @@ -199,31 +200,21 @@ fn admit_public_write( }, } } else { - match pushed_refs.first() { - Present { value: push } => - PublicWriteRefused { - session: publisher, - cause: UnauthorizedPublicPush { - session: publisher, - push: push, - detail: unauthorized_push_detail( - pushed_refs: pushed_refs, - candidate: plan, - ), - }, - } - Absent => - PublicWriteAdmitted { - authority: PublicationAuthority { - publisher_session: publisher, - authority_id: sole_publication_authority_id, - projection: AdmittedPublicProjection { - plan: plan, - admission: admission, - decisions: decisions, - }, - }, - } + PublicWriteRefused { + session: publisher, + cause: UnauthorizedPublicPush { + session: publisher, + local_ref: fold(pushed_refs, init: "", f: fn(acc, push) { + if acc == "" { push.local_ref } else { acc } + }), + local_sha: fold(pushed_refs, init: "", f: fn(acc, push) { + if acc == "" { push.local_sha } else { acc } + }), + detail: unauthorized_push_detail( + pushed_refs: pushed_refs, + candidate: plan, + ), + }, } } } diff --git a/dag/tools/publication_push_shadow.dag b/dag/tools/publication_push_shadow.dag index 698edcf12fd..7db8d1f3cc0 100644 --- a/dag/tools/publication_push_shadow.dag +++ b/dag/tools/publication_push_shadow.dag @@ -103,7 +103,7 @@ fn shadow_would_refuse_ordinary_session( NotTheSolePublisher { session: _, sole_publisher: _ } => true ProjectionNotAdmitted { session: _, detail: _ } => false DecisionReceiptMissing { session: _, subject_identity: _ } => false - UnauthorizedPublicPush { session: _, push: _, detail: _ } => false + UnauthorizedPublicPush { session: _, local_ref: _, local_sha: _, detail: _ } => false } PublicWriteAdmitted { authority: _ } => false } From 201967e58314e80f7be9996bfb9f6f34a4ce7455 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 17:50:50 +0000 Subject: [PATCH 05/10] Refuse sole-publisher admission when no pushed refs are presented. Close the empty-pushed_refs vacuous-pass fail-open before live-slice binding; witness sole_publisher_refuses_empty_pushed_refs. Co-authored-by: Cursor --- .../publication_publisher_witness_test.dag | 29 +++++++++++++++++++ dag/tools/publication_publisher.dag | 20 ++++++++++--- dag/tools/publication_push_shadow.dag | 2 ++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index ce565932762..b43e132e046 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -48,6 +48,7 @@ import tools.publication_publisher { ProjectionNotAdmitted, DecisionReceiptMissing, UnauthorizedPublicPush, + NoPushedRefsPresented, PublicWriteAdmitted, PublicWriteRefused, sole_modeled_publisher_session, @@ -392,6 +393,34 @@ test fn sole_publisher_refuses_push_head_mismatch() -> Bool { } } +test fn sole_publisher_refuses_empty_pushed_refs() -> Bool { + let policy = public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/empty-push.md" as FilePath, + identity: "subject-empty-push", + ) + let candidate = derive_public_projection_plan( + policy: policy, + source_revision: synthetic_head, + authored_subjects: [subject], + ) + match admit_public_write( + session: sole_modeled_publisher_session, + current: Absent, + candidate: candidate, + decisions: [synthetic_decision_receipt(subject: subject)], + pushed_refs: [], + ) { + PublicWriteRefused { session: _, cause: cause } => + match cause { + NoPushedRefsPresented { session: session } => + session == sole_modeled_publisher_session + _ => false + } + PublicWriteAdmitted { authority: _ } => false + } +} + test fn shadow_replay_counts_ordinary_session_refusals() -> Bool { let policy = public_docs_policy() let candidate = derive_public_projection_plan( diff --git a/dag/tools/publication_publisher.dag b/dag/tools/publication_publisher.dag index 2c8e6170989..1c5b290ab38 100644 --- a/dag/tools/publication_publisher.dag +++ b/dag/tools/publication_publisher.dag @@ -66,6 +66,9 @@ type PublicWriteRefusal local_sha: String, detail: String, } + | NoPushedRefsPresented { + session: ModeledSessionId, + } data sole_modeled_publisher_session: ModeledSessionId = "gunbc-publication-publisher" as ModeledSessionId @@ -137,9 +140,13 @@ fn pushed_refs_match_candidate( pushed_refs: List, candidate: PublicProjectionPlan, ) -> Bool { - fold(pushed_refs, init: true, f: fn(ok, push) { - ok && push.local_sha == candidate.source_revision - }) + if count(pushed_refs) == 0 { + false + } else { + fold(pushed_refs, init: true, f: fn(ok, push) { + ok && push.local_sha == candidate.source_revision + }) + } } fn unauthorized_push_detail( @@ -187,7 +194,12 @@ fn admit_public_write( }, } Absent => - if pushed_refs_match_candidate(pushed_refs: pushed_refs, candidate: plan) { + if count(pushed_refs) == 0 { + PublicWriteRefused { + session: publisher, + cause: NoPushedRefsPresented { session: publisher }, + } + } else if pushed_refs_match_candidate(pushed_refs: pushed_refs, candidate: plan) { PublicWriteAdmitted { authority: PublicationAuthority { publisher_session: publisher, diff --git a/dag/tools/publication_push_shadow.dag b/dag/tools/publication_push_shadow.dag index 7db8d1f3cc0..de683fceafc 100644 --- a/dag/tools/publication_push_shadow.dag +++ b/dag/tools/publication_push_shadow.dag @@ -13,6 +13,7 @@ import tools.publication_publisher { ProjectionNotAdmitted, DecisionReceiptMissing, UnauthorizedPublicPush, + NoPushedRefsPresented, admit_public_write, } @@ -104,6 +105,7 @@ fn shadow_would_refuse_ordinary_session( ProjectionNotAdmitted { session: _, detail: _ } => false DecisionReceiptMissing { session: _, subject_identity: _ } => false UnauthorizedPublicPush { session: _, local_ref: _, local_sha: _, detail: _ } => false + NoPushedRefsPresented { session: _ } => false } PublicWriteAdmitted { authority: _ } => false } From 00382ff9d7c537d4d3d857fba4628caafb58d923 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 17:57:05 +0000 Subject: [PATCH 06/10] Gate projection on AdmittedPublicationPolicy; match disposition directly. derive_public_projection_plan requires construction-admitted policy so conflicting/unknown-root policies cannot reach the derived path; dissolve publication_disposition_is_public in favor of coproduct match. Witness publication_policy_refuses_conflicting_prefix_rules. Co-authored-by: Cursor --- dag/gunbc/public_projection.dag | 40 +++++----- dag/gunbc/publication_policy.dag | 17 ++--- .../publication_publisher_witness_test.dag | 75 +++++++++++++------ 3 files changed, 81 insertions(+), 51 deletions(-) diff --git a/dag/gunbc/public_projection.dag b/dag/gunbc/public_projection.dag index 9c1fe2d4f77..78297485712 100644 --- a/dag/gunbc/public_projection.dag +++ b/dag/gunbc/public_projection.dag @@ -3,12 +3,14 @@ module gunbc.public_projection import std.types { CommitSha, ContentHash, FilePath, List, NonEmptyStr } import std.content_hash { content_hash_atom, content_hash_combine, content_hash_tagged } import gunbc.publication_policy { - PublicationPolicy, + AdmittedPublicationPolicy, + PublicationDisposition, + PublishPublic, + WithholdPrivate, resolve_disposition_for_path, - publication_disposition_is_public, } -data public_projection_note: String = "Derived public projection plan over authored subjects. Never the unrestricted authored tree and never a pathname census: policy resolves each subject's disposition, included_subjects carries only PublishPublic rows, omitted_subjects carries WithholdPrivate rows, and projection_digest fingerprints the admitted set." +data public_projection_note: String = "Derived public projection plan over authored subjects. Requires AdmittedPublicationPolicy from admit_publication_policy — illegal policies are unrepresentable on this path. Never the unrestricted authored tree and never a pathname census: policy resolves each subject's disposition, included_subjects carries only PublishPublic rows, omitted_subjects carries WithholdPrivate rows, and projection_digest fingerprints the admitted set." type PublicationSubject { subject_identity: ContentHash @@ -49,42 +51,44 @@ type AuthoredSubjectPartition { } fn partition_authored_subjects( - policy: PublicationPolicy, + admitted: AdmittedPublicationPolicy, authored_subjects: List, ) -> AuthoredSubjectPartition { fold( authored_subjects, init: AuthoredSubjectPartition { included: [], omitted: [] }, f: fn(parts, subject) { - if publication_disposition_is_public( - disposition: resolve_disposition_for_path(policy: policy, path: subject.path), + match resolve_disposition_for_path( + policy: admitted.policy, + path: subject.path, ) { - AuthoredSubjectPartition { - included: append(parts.included, items: [subject]), - omitted: parts.omitted, - } - } else { - AuthoredSubjectPartition { - included: parts.included, - omitted: append(parts.omitted, items: [subject]), - } + PublishPublic => + AuthoredSubjectPartition { + included: append(parts.included, items: [subject]), + omitted: parts.omitted, + } + WithholdPrivate => + AuthoredSubjectPartition { + included: parts.included, + omitted: append(parts.omitted, items: [subject]), + } } }, ) } fn derive_public_projection_plan( - policy: PublicationPolicy, + admitted: AdmittedPublicationPolicy, source_revision: CommitSha, authored_subjects: List, ) -> PublicProjectionPlan { let parts = partition_authored_subjects( - policy: policy, + admitted: admitted, authored_subjects: authored_subjects, ) PublicProjectionPlan { source_revision: source_revision, - policy_version: policy.version, + policy_version: admitted.policy.version, included_subjects: parts.included, omitted_subjects: parts.omitted, projection_digest: projection_digest_from_subjects(subjects: parts.included), diff --git a/dag/gunbc/publication_policy.dag b/dag/gunbc/publication_policy.dag index e6aba281288..f3abf5b43eb 100644 --- a/dag/gunbc/publication_policy.dag +++ b/dag/gunbc/publication_policy.dag @@ -19,12 +19,16 @@ type PublicationPolicy { rules: List } +type AdmittedPublicationPolicy { + policy: PublicationPolicy +} + type PublicationPolicyConstructionRefusal = UnknownTopLevelRoot { prefix: FilePath } | ConflictingSubtreeRule { prefix: FilePath } type PublicationPolicyConstructionOutcome - = PublicationPolicyAccepted { policy: PublicationPolicy } + = PublicationPolicyAccepted { admitted: AdmittedPublicationPolicy } | PublicationPolicyRefused { cause: PublicationPolicyConstructionRefusal } data publication_policy_default_disposition: PublicationDisposition = WithholdPrivate @@ -182,14 +186,9 @@ fn admit_publication_policy(policy: PublicationPolicy) -> PublicationPolicyConst Absent => match publication_policy_rule_conflicts(rules: policy.rules) { Present { value: cause } => PublicationPolicyRefused { cause: cause } - Absent => PublicationPolicyAccepted { policy: policy } + Absent => PublicationPolicyAccepted { + admitted: AdmittedPublicationPolicy { policy: policy }, + } } } } - -fn publication_disposition_is_public(disposition: PublicationDisposition) -> Bool { - match disposition { - PublishPublic => true - WithholdPrivate => false - } -} diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index b43e132e046..09499c485e0 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -91,6 +91,14 @@ fn public_docs_policy() -> PublicationPolicy { } } +fn admitted_public_docs_policy() -> AdmittedPublicationPolicy { + match admit_publication_policy(policy: public_docs_policy()) { + PublicationPolicyAccepted { admitted: admitted } => admitted + PublicationPolicyRefused { cause: _ } => + AdmittedPublicationPolicy { policy: public_docs_policy() } + } +} + fn synthetic_decision_receipt(subject: PublicationSubject) -> PublicationDecisionReceipt { PublicationDecisionReceipt { subject_identity: subject.subject_identity, @@ -146,7 +154,7 @@ test fn publication_policy_refuses_unknown_top_level_root() -> Bool { disposition: PublishPublic, }], }) { - PublicationPolicyAccepted { policy: _ } => false + PublicationPolicyAccepted { admitted: _ } => false PublicationPolicyRefused { cause: cause } => match cause { UnknownTopLevelRoot { prefix: _ } => true @@ -155,6 +163,24 @@ test fn publication_policy_refuses_unknown_top_level_root() -> Bool { } } +test fn publication_policy_refuses_conflicting_prefix_rules() -> Bool { + match admit_publication_policy(policy: PublicationPolicy { + version: synthetic_policy_version, + default_disposition: WithholdPrivate, + rules: [ + SubtreePublishRule { prefix: "docs/plans" as FilePath, disposition: PublishPublic }, + SubtreePublishRule { prefix: "docs/plans" as FilePath, disposition: WithholdPrivate }, + ], + }) { + PublicationPolicyAccepted { admitted: _ } => false + PublicationPolicyRefused { cause: cause } => + match cause { + ConflictingSubtreeRule { prefix: _ } => true + UnknownTopLevelRoot { prefix: _ } => false + } + } +} + test fn publication_policy_most_specific_rule_wins() -> Bool { let policy = PublicationPolicy { version: synthetic_policy_version, @@ -164,18 +190,22 @@ test fn publication_policy_most_specific_rule_wins() -> Bool { SubtreePublishRule { prefix: "docs/plans" as FilePath, disposition: PublishPublic }, ], } - resolve_disposition_for_path(policy: policy, path: "docs/plans/sole-modeled-publisher-design.md" as FilePath) == PublishPublic - && resolve_disposition_for_path(policy: policy, path: "docs/other.md" as FilePath) == WithholdPrivate + match admit_publication_policy(policy: policy) { + PublicationPolicyAccepted { admitted: accepted } => + resolve_disposition_for_path(policy: accepted.policy, path: "docs/plans/sole-modeled-publisher-design.md" as FilePath) == PublishPublic + && resolve_disposition_for_path(policy: accepted.policy, path: "docs/other.md" as FilePath) == WithholdPrivate + PublicationPolicyRefused { cause: _ } => false + } } test fn public_projection_plan_derives_from_policy_not_pathname_census() -> Bool { - let policy = public_docs_policy() + let admitted = admitted_public_docs_policy() let subject = synthetic_subject( path: "docs/plans/sole-modeled-publisher-design.md" as FilePath, identity: "subject-docs-plan", ) let plan = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [subject], ) @@ -184,13 +214,13 @@ test fn public_projection_plan_derives_from_policy_not_pathname_census() -> Bool } test fn change_local_admission_classifies_newly_public_delta() -> Bool { - let policy = public_docs_policy() + let admitted = admitted_public_docs_policy() let subject = synthetic_subject( path: "docs/plans/new.md" as FilePath, identity: "subject-new-doc", ) let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [subject], ) @@ -208,9 +238,8 @@ test fn change_local_admission_classifies_newly_public_delta() -> Bool { } test fn change_local_admission_refuses_tampered_projection_digest() -> Bool { - let policy = public_docs_policy() let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted_public_docs_policy(), source_revision: synthetic_head, authored_subjects: [synthetic_subject( path: "docs/plans/tampered.md" as FilePath, @@ -238,18 +267,18 @@ test fn change_local_admission_refuses_tampered_projection_digest() -> Bool { } test fn change_local_admission_classifies_deletion_as_newly_withheld() -> Bool { - let policy = public_docs_policy() + let admitted = admitted_public_docs_policy() let subject = synthetic_subject( path: "docs/plans/deleted.md" as FilePath, identity: "subject-deleted", ) let current = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [subject], ) let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [], ) @@ -282,9 +311,8 @@ test fn publication_decision_binds_content_identity_not_pathname() -> Bool { } test fn ordinary_session_reds_before_projection_check() -> Bool { - let policy = public_docs_policy() let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted_public_docs_policy(), source_revision: synthetic_head, authored_subjects: [synthetic_subject( path: "docs/plans/x.md" as FilePath, @@ -308,13 +336,13 @@ test fn ordinary_session_reds_before_projection_check() -> Bool { } test fn sole_publisher_admits_change_local_projection() -> Bool { - let policy = public_docs_policy() + let admitted = admitted_public_docs_policy() let subject = synthetic_subject( path: "docs/plans/y.md" as FilePath, identity: "subject-y", ) let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [subject], ) @@ -333,13 +361,13 @@ test fn sole_publisher_admits_change_local_projection() -> Bool { } test fn sole_publisher_refuses_newly_public_without_receipt() -> Bool { - let policy = public_docs_policy() + let admitted = admitted_public_docs_policy() let subject = synthetic_subject( path: "docs/plans/no-receipt.md" as FilePath, identity: "subject-no-receipt", ) let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [subject], ) @@ -361,13 +389,13 @@ test fn sole_publisher_refuses_newly_public_without_receipt() -> Bool { } test fn sole_publisher_refuses_push_head_mismatch() -> Bool { - let policy = public_docs_policy() + let admitted = admitted_public_docs_policy() let subject = synthetic_subject( path: "docs/plans/mismatch.md" as FilePath, identity: "subject-mismatch", ) let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [subject], ) @@ -394,13 +422,13 @@ test fn sole_publisher_refuses_push_head_mismatch() -> Bool { } test fn sole_publisher_refuses_empty_pushed_refs() -> Bool { - let policy = public_docs_policy() + let admitted = admitted_public_docs_policy() let subject = synthetic_subject( path: "docs/plans/empty-push.md" as FilePath, identity: "subject-empty-push", ) let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted, source_revision: synthetic_head, authored_subjects: [subject], ) @@ -422,9 +450,8 @@ test fn sole_publisher_refuses_empty_pushed_refs() -> Bool { } test fn shadow_replay_counts_ordinary_session_refusals() -> Bool { - let policy = public_docs_policy() let candidate = derive_public_projection_plan( - policy: policy, + admitted: admitted_public_docs_policy(), source_revision: synthetic_head, authored_subjects: [synthetic_subject( path: "docs/plans/z.md" as FilePath, From db5429804dc2a53154869c27ace942614d55ab29 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 18:17:43 +0000 Subject: [PATCH 07/10] Address review 46476: full receipt auth and posture fold. publication_decision_authorizes_newly_public checks policy_version, authority, and universal audience; invalid receipts refuse with DecisionReceiptNotAuthorizing. Replace posture Bool predicate with session_publication_posture_fold. Co-authored-by: Cursor --- dag/gunbc/publication_decision.dag | 22 +++- .../publication_publisher_witness_test.dag | 118 ++++++++++++++++++ dag/tools/publication_publisher.dag | 84 ++++++++++--- dag/tools/publication_push_shadow.dag | 2 + 4 files changed, 210 insertions(+), 16 deletions(-) diff --git a/dag/gunbc/publication_decision.dag b/dag/gunbc/publication_decision.dag index cf8aa46badb..25b1e6f6611 100644 --- a/dag/gunbc/publication_decision.dag +++ b/dag/gunbc/publication_decision.dag @@ -1,8 +1,9 @@ module gunbc.publication_decision import std.types { ContentHash, NonEmptyStr, String } +import gunbc.public_projection { PublicationSubject } -data publication_decision_note: String = "Exceptional publication decisions bind to content identity, not pathnames. A changed sensitive artifact requires a fresh PublicationDecisionReceipt; pathname alone is never the decision key." +data publication_decision_note: String = "Exceptional publication decisions bind to content identity, not pathnames. publication_decision_authorizes_newly_public is the single authority for whether a receipt authorizes a newly public subject: matching subject/content identity, candidate policy_version, sole publisher authority, and UniversalPublicationAudience." type PublicationAuthorityId = NonEmptyStr where brand("PublicationAuthorityId") @@ -26,6 +27,25 @@ fn publication_decision_binds_content( receipt.subject_identity == subject_identity && receipt.content_identity == content_identity } +fn publication_decision_authorizes_newly_public( + receipt: PublicationDecisionReceipt, + subject: PublicationSubject, + policy_version: NonEmptyStr, + authority: PublicationAuthorityId, +) -> Bool { + publication_decision_binds_content( + receipt: receipt, + subject_identity: subject.subject_identity, + content_identity: subject.content_identity, + ) + && receipt.policy_version == policy_version + && receipt.authority == authority + && match receipt.audience { + UniversalPublicationAudience => true + EnumeratedPublicationAudience { label: _ } => false + } +} + fn publication_decision_requires_refresh( receipt: PublicationDecisionReceipt, subject_identity: ContentHash, diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index 09499c485e0..6ddba638851 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -28,8 +28,11 @@ import gunbc.public_projection { import gunbc.publication_decision { PublicationDecisionReceipt, PublicationAuthorityId, + PublicationAudience, UniversalPublicationAudience, + EnumeratedPublicationAudience, publication_decision_requires_refresh, + publication_decision_authorizes_newly_public, } import gunbc.publication_admission_delta { ChangeLocalAdmissionRequest, @@ -47,6 +50,7 @@ import tools.publication_publisher { NotTheSolePublisher, ProjectionNotAdmitted, DecisionReceiptMissing, + DecisionReceiptNotAuthorizing, UnauthorizedPublicPush, NoPushedRefsPresented, PublicWriteAdmitted, @@ -295,6 +299,120 @@ test fn change_local_admission_classifies_deletion_as_newly_withheld() -> Bool { } } +test fn publication_decision_authorizes_matching_policy_authority_and_audience() -> Bool { + let subject = synthetic_subject( + path: "docs/plans/receipt-auth.md" as FilePath, + identity: "subject-receipt-auth", + ) + publication_decision_authorizes_newly_public( + receipt: synthetic_decision_receipt(subject: subject), + subject: subject, + policy_version: synthetic_policy_version, + authority: sole_publication_authority_id, + ) +} + +test fn sole_publisher_refuses_stale_policy_version_receipt() -> Bool { + let admitted = admitted_public_docs_policy() + let subject = synthetic_subject( + path: "docs/plans/stale-receipt.md" as FilePath, + identity: "subject-stale-receipt", + ) + let candidate = derive_public_projection_plan( + admitted: admitted, + source_revision: synthetic_head, + authored_subjects: [subject], + ) + let stale_receipt = PublicationDecisionReceipt { + subject_identity: subject.subject_identity, + content_identity: subject.content_identity, + audience: UniversalPublicationAudience, + policy_version: "stale-policy" as NonEmptyStr, + authority: sole_publication_authority_id, + } + match admit_public_write( + session: sole_modeled_publisher_session, + current: Absent, + candidate: candidate, + decisions: [stale_receipt], + pushed_refs: [synthetic_push()], + ) { + PublicWriteRefused { session: _, cause: cause } => + match cause { + DecisionReceiptNotAuthorizing { session: _, subject_identity: id, detail: _ } => + id == (subject.subject_identity as String) + _ => false + } + PublicWriteAdmitted { authority: _ } => false + } +} + +test fn sole_publisher_refuses_wrong_authority_receipt() -> Bool { + let subject = synthetic_subject( + path: "docs/plans/wrong-authority.md" as FilePath, + identity: "subject-wrong-authority", + ) + let candidate = derive_public_projection_plan( + admitted: admitted_public_docs_policy(), + source_revision: synthetic_head, + authored_subjects: [subject], + ) + let wrong_authority_receipt = PublicationDecisionReceipt { + subject_identity: subject.subject_identity, + content_identity: subject.content_identity, + audience: UniversalPublicationAudience, + policy_version: synthetic_policy_version, + authority: "not-the-publisher" as PublicationAuthorityId, + } + match admit_public_write( + session: sole_modeled_publisher_session, + current: Absent, + candidate: candidate, + decisions: [wrong_authority_receipt], + pushed_refs: [synthetic_push()], + ) { + PublicWriteRefused { session: _, cause: cause } => + match cause { + DecisionReceiptNotAuthorizing { session: _, subject_identity: _, detail: _ } => true + _ => false + } + PublicWriteAdmitted { authority: _ } => false + } +} + +test fn sole_publisher_refuses_enumerated_audience_receipt() -> Bool { + let subject = synthetic_subject( + path: "docs/plans/narrow-audience.md" as FilePath, + identity: "subject-narrow-audience", + ) + let candidate = derive_public_projection_plan( + admitted: admitted_public_docs_policy(), + source_revision: synthetic_head, + authored_subjects: [subject], + ) + let narrow_receipt = PublicationDecisionReceipt { + subject_identity: subject.subject_identity, + content_identity: subject.content_identity, + audience: EnumeratedPublicationAudience { label: "private-team" }, + policy_version: synthetic_policy_version, + authority: sole_publication_authority_id, + } + match admit_public_write( + session: sole_modeled_publisher_session, + current: Absent, + candidate: candidate, + decisions: [narrow_receipt], + pushed_refs: [synthetic_push()], + ) { + PublicWriteRefused { session: _, cause: cause } => + match cause { + DecisionReceiptNotAuthorizing { session: _, subject_identity: _, detail: _ } => true + _ => false + } + PublicWriteAdmitted { authority: _ } => false + } +} + test fn publication_decision_binds_content_identity_not_pathname() -> Bool { let receipt = PublicationDecisionReceipt { subject_identity: "subject-a" as ContentHash, diff --git a/dag/tools/publication_publisher.dag b/dag/tools/publication_publisher.dag index 1c5b290ab38..4dfea3ed965 100644 --- a/dag/tools/publication_publisher.dag +++ b/dag/tools/publication_publisher.dag @@ -18,6 +18,7 @@ import gunbc.publication_decision { PublicationDecisionReceipt, PublicationAuthorityId, publication_decision_binds_content, + publication_decision_authorizes_newly_public, } data publication_publisher_note: String = "Slice-1 sole modeled publisher (model + shadow only). Ordinary ModeledSessionId values are OrdinarySession and carry no PublicationAuthority. Exactly one sole_modeled_publisher_session may consume a change-locally admitted PublicProjectionPlan derived from PublicationPolicy — never a pathname census or placement-gate roster. Ordinary sessions do NOT yet lose live public-write capability in this slice; credentials and push machinery are unchanged." @@ -60,6 +61,11 @@ type PublicWriteRefusal session: ModeledSessionId, subject_identity: String, } + | DecisionReceiptNotAuthorizing { + session: ModeledSessionId, + subject_identity: String, + detail: String, + } | UnauthorizedPublicPush { session: ModeledSessionId, local_ref: String, @@ -82,10 +88,14 @@ fn session_publication_posture(session: ModeledSessionId) -> SessionPublicationP } } -fn posture_is_sole_publisher(posture: SessionPublicationPosture) -> Bool { +fn session_publication_posture_fold( + posture: SessionPublicationPosture, + on_ordinary: fn(ModeledSessionId) -> T, + on_sole_publisher: fn(ModeledSessionId) -> T, +) -> T { match posture { - SoleModeledPublisher { session: _ } => true - OrdinarySession { session: _ } => false + OrdinarySession { session: session } => on_ordinary(session) + SoleModeledPublisher { session: session } => on_sole_publisher(session) } } @@ -102,7 +112,23 @@ fn change_local_admission_detail(outcome: ChangeLocalAdmissionOutcome) -> String } } -fn receipt_covers_subject( +fn receipt_authorizes_newly_public( + receipts: List, + subject: PublicationSubject, + policy_version: NonEmptyStr, + authority: PublicationAuthorityId, +) -> Bool { + fold(receipts, init: false, f: fn(found, receipt) { + found || publication_decision_authorizes_newly_public( + receipt: receipt, + subject: subject, + policy_version: policy_version, + authority: authority, + ) + }) +} + +fn receipt_binds_subject_content( receipts: List, subject: PublicationSubject, ) -> Bool { @@ -115,20 +141,41 @@ fn receipt_covers_subject( }) } -fn newly_public_subject_missing_receipt( +fn newly_public_subject_receipt_refusal( delta: List, decisions: List, -) -> PublicationSubject? { + policy_version: NonEmptyStr, + authority: PublicationAuthorityId, + session: ModeledSessionId, +) -> PublicWriteRefusal? { fold(delta, init: none, f: fn(acc, item) { match acc { Present { value: _ } => acc Absent => match item { NewlyPublic { subject: subject } => - if receipt_covers_subject(receipts: decisions, subject: subject) { + if receipt_authorizes_newly_public( + receipts: decisions, + subject: subject, + policy_version: policy_version, + authority: authority, + ) { none + } else if receipt_binds_subject_content(receipts: decisions, subject: subject) { + Present { + value: DecisionReceiptNotAuthorizing { + session: session, + subject_identity: subject.subject_identity as String, + detail: "decision receipt does not authorize newly public subject", + }, + } } else { - Present { value: subject } + Present { + value: DecisionReceiptMissing { + session: session, + subject_identity: subject.subject_identity as String, + }, + } } _ => none } @@ -184,14 +231,17 @@ fn admit_public_write( }) match admission { ChangeLocalAdmitted { delta: delta, candidate: plan } => - match newly_public_subject_missing_receipt(delta: delta, decisions: decisions) { - Present { value: subject } => + match newly_public_subject_receipt_refusal( + delta: delta, + decisions: decisions, + policy_version: plan.policy_version, + authority: sole_publication_authority_id, + session: publisher, + ) { + Present { value: cause } => PublicWriteRefused { session: publisher, - cause: DecisionReceiptMissing { - session: publisher, - subject_identity: subject.subject_identity as String, - }, + cause: cause, } Absent => if count(pushed_refs) == 0 { @@ -262,5 +312,9 @@ fn public_write_admitted(admission: PublicWriteAdmission) -> Bool { } fn ordinary_session_lacks_public_write_authority(session: ModeledSessionId) -> Bool { - !posture_is_sole_publisher(posture: session_publication_posture(session: session)) + session_publication_posture_fold( + posture: session_publication_posture(session: session), + on_ordinary: fn(ordinary) { true }, + on_sole_publisher: fn(publisher) { false }, + ) } diff --git a/dag/tools/publication_push_shadow.dag b/dag/tools/publication_push_shadow.dag index de683fceafc..446fdd271ce 100644 --- a/dag/tools/publication_push_shadow.dag +++ b/dag/tools/publication_push_shadow.dag @@ -12,6 +12,7 @@ import tools.publication_publisher { NotTheSolePublisher, ProjectionNotAdmitted, DecisionReceiptMissing, + DecisionReceiptNotAuthorizing, UnauthorizedPublicPush, NoPushedRefsPresented, admit_public_write, @@ -104,6 +105,7 @@ fn shadow_would_refuse_ordinary_session( NotTheSolePublisher { session: _, sole_publisher: _ } => true ProjectionNotAdmitted { session: _, detail: _ } => false DecisionReceiptMissing { session: _, subject_identity: _ } => false + DecisionReceiptNotAuthorizing { session: _, subject_identity: _, detail: _ } => false UnauthorizedPublicPush { session: _, local_ref: _, local_sha: _, detail: _ } => false NoPushedRefsPresented { session: _ } => false } From e2577cadca0ab0665d9a7b7edbdcf6f1a5ad9eb7 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 22:21:48 +0000 Subject: [PATCH 08/10] WIP: Sole modeled publisher: ordinary sessions lose public-write capability; --- dag/extdeps/git/publication_transport.dag | 15 ++++++++++++++- dag/gunbc/extdeps_scope_frontier.dag | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/dag/extdeps/git/publication_transport.dag b/dag/extdeps/git/publication_transport.dag index 22c312daa02..423b040f0ad 100644 --- a/dag/extdeps/git/publication_transport.dag +++ b/dag/extdeps/git/publication_transport.dag @@ -1,7 +1,8 @@ module extdeps.git.publication_transport import std.types { Bool, CommitSha, GitRef, Int, List, NonEmptyStr, String } -import extdeps.external_authority { ExternalAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -18,6 +19,18 @@ data git_commit_external_authority_anchor: ExternalAuthority = ExternalAuthority } } +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.git.publication_transport", + decl_name: "PublicationTransport", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [git_commit_external_authority_anchor] +} + data git_publication_transport_note: String = "Modeled upstream publication write operations de-fused from git.Core. GitPushRequest and GitCommitRequest are the interface shapes; shell argv lives only in git.PublicationTransport service transport rows — no parallel free-fn argv surface. Slice 1 is model + shadow only — consumers route through tools.publication_publisher before binding a transport handler in the live slice." type GitRefUpdateSpec { diff --git a/dag/gunbc/extdeps_scope_frontier.dag b/dag/gunbc/extdeps_scope_frontier.dag index 956dce6640a..dd769497438 100644 --- a/dag/gunbc/extdeps_scope_frontier.dag +++ b/dag/gunbc/extdeps_scope_frontier.dag @@ -127,6 +127,7 @@ data scope_carrier_paths: List = [ "dag/extdeps/automation/playwright.dag", "dag/extdeps/browser/chromium.dag", "dag/extdeps/container/oci/digest.dag", + "dag/extdeps/git/publication_transport.dag", "dag/extdeps/github/actions_environment.dag", "dag/extdeps/os/debian.dag", "dag/extdeps/tools/wc.dag", From 5d2cdd8e1af1c72efcc35e5838ed651b11c97b8d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 2 Aug 2026 03:44:18 +0000 Subject: [PATCH 09/10] Route publication ContentHash through std.content_hash. Main moved ContentHash off std.types and renamed content_hash_combine to content_hash_combine_structural; update publication projection, witnesses, and publisher refusal strings to the family-grounded API. Co-authored-by: Cursor --- dag/gunbc/public_projection.dag | 32 +++++++++++++------ dag/gunbc/publication_admission_delta.dag | 3 +- dag/gunbc/publication_decision.dag | 3 +- .../publication_publisher_witness_test.dag | 21 ++++++------ dag/tools/publication_publisher.dag | 5 +-- 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/dag/gunbc/public_projection.dag b/dag/gunbc/public_projection.dag index 78297485712..329720191f1 100644 --- a/dag/gunbc/public_projection.dag +++ b/dag/gunbc/public_projection.dag @@ -1,7 +1,14 @@ module gunbc.public_projection -import std.types { CommitSha, ContentHash, FilePath, List, NonEmptyStr } -import std.content_hash { content_hash_atom, content_hash_combine, content_hash_tagged } +import std.types { CommitSha, FilePath, List, NonEmptyStr } +import std.content_hash { + ContentHash, + content_hash_atom, + content_hash_combine_structural, + content_hash_tagged, + serialize_content_hash, + as_content_hash_structural, +} import gunbc.publication_policy { AdmittedPublicationPolicy, PublicationDisposition, @@ -29,19 +36,26 @@ type PublicProjectionPlan { fn publication_subject_digest(subject: PublicationSubject) -> ContentHash { content_hash_tagged( tag: "publication-subject", - payload: content_hash_combine( - content_hash_atom(value: subject.subject_identity as NonEmptyStr), - content_hash_atom(value: subject.content_identity as NonEmptyStr), + payload: content_hash_combine_structural( + left: content_hash_atom(value: serialize_content_hash(hash: subject.subject_identity)), + right: content_hash_atom(value: serialize_content_hash(hash: subject.content_identity)), ), ) } fn projection_digest_from_subjects(subjects: List) -> ContentHash { let digests = map(subjects, subject => publication_subject_digest(subject: subject)) - fold( - sort_by(digests, digest => digest as String), - init: content_hash_atom(value: "public-projection" as NonEmptyStr), - f: fn(acc, digest) { content_hash_combine(acc, digest) }, + as_content_hash_structural( + structural: fold( + sort_by(digests, digest => serialize_content_hash(hash: digest) as String), + init: content_hash_atom(value: "public-projection" as NonEmptyStr), + f: fn(acc, digest) { + content_hash_combine_structural( + left: acc, + right: content_hash_atom(value: serialize_content_hash(hash: digest)), + ) + }, + ) ) } diff --git a/dag/gunbc/publication_admission_delta.dag b/dag/gunbc/publication_admission_delta.dag index 6d4de9bf8db..d64c1a6f504 100644 --- a/dag/gunbc/publication_admission_delta.dag +++ b/dag/gunbc/publication_admission_delta.dag @@ -1,6 +1,7 @@ module gunbc.publication_admission_delta -import std.types { ContentHash, List, NonEmptyStr } +import std.types { List, NonEmptyStr } +import std.content_hash { ContentHash } import gunbc.public_projection { PublicationSubject, PublicProjectionPlan, diff --git a/dag/gunbc/publication_decision.dag b/dag/gunbc/publication_decision.dag index 25b1e6f6611..64fd87a6ce1 100644 --- a/dag/gunbc/publication_decision.dag +++ b/dag/gunbc/publication_decision.dag @@ -1,6 +1,7 @@ module gunbc.publication_decision -import std.types { ContentHash, NonEmptyStr, String } +import std.types { NonEmptyStr, String } +import std.content_hash { ContentHash } import gunbc.public_projection { PublicationSubject } data publication_decision_note: String = "Exceptional publication decisions bind to content identity, not pathnames. publication_decision_authorizes_newly_public is the single authority for whether a receipt authorizes a newly public subject: matching subject/content identity, candidate policy_version, sole publisher authority, and UniversalPublicationAudience." diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index 6ddba638851..5cd2ffe13ad 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.publication_publisher_witness -import std.types { Bool, CommitSha, ContentHash, FilePath, GitRef, NonEmptyStr, String } +import std.types { Bool, CommitSha, FilePath, GitRef, NonEmptyStr, String } +import std.content_hash { ContentHash, content_hash_of_value, serialize_content_hash } import extdeps.git.hooks { PrePushStdinRow, zero_sha } import extdeps.git.publication_transport { GitRefUpdateSpec, @@ -78,8 +79,8 @@ data synthetic_policy_version: NonEmptyStr = "policy-v1" as NonEmptyStr fn synthetic_subject(path: FilePath, identity: String) -> PublicationSubject { PublicationSubject { - subject_identity: identity as ContentHash, - content_identity: identity as ContentHash, + subject_identity: content_hash_of_value(value: identity as NonEmptyStr), + content_identity: content_hash_of_value(value: identity as NonEmptyStr), path: path, } } @@ -255,7 +256,7 @@ test fn change_local_admission_refuses_tampered_projection_digest() -> Bool { policy_version: candidate.policy_version, included_subjects: candidate.included_subjects, omitted_subjects: candidate.omitted_subjects, - projection_digest: "tampered-digest" as ContentHash, + projection_digest: content_hash_of_value(value: "tampered-digest" as NonEmptyStr), } match admit_change_local_projection(request: ChangeLocalAdmissionRequest { current: Absent, @@ -340,7 +341,7 @@ test fn sole_publisher_refuses_stale_policy_version_receipt() -> Bool { PublicWriteRefused { session: _, cause: cause } => match cause { DecisionReceiptNotAuthorizing { session: _, subject_identity: id, detail: _ } => - id == (subject.subject_identity as String) + id == (serialize_content_hash(hash: subject.subject_identity) as String) _ => false } PublicWriteAdmitted { authority: _ } => false @@ -415,16 +416,16 @@ test fn sole_publisher_refuses_enumerated_audience_receipt() -> Bool { test fn publication_decision_binds_content_identity_not_pathname() -> Bool { let receipt = PublicationDecisionReceipt { - subject_identity: "subject-a" as ContentHash, - content_identity: "content-v1" as ContentHash, + subject_identity: content_hash_of_value(value: "subject-a" as NonEmptyStr), + content_identity: content_hash_of_value(value: "content-v1" as NonEmptyStr), audience: UniversalPublicationAudience, policy_version: synthetic_policy_version, authority: sole_publication_authority_id, } publication_decision_requires_refresh( receipt: receipt, - subject_identity: "subject-a" as ContentHash, - content_identity: "content-v2" as ContentHash, + subject_identity: content_hash_of_value(value: "subject-a" as NonEmptyStr), + content_identity: content_hash_of_value(value: "content-v2" as NonEmptyStr), ) } @@ -499,7 +500,7 @@ test fn sole_publisher_refuses_newly_public_without_receipt() -> Bool { PublicWriteRefused { session: _, cause: cause } => match cause { DecisionReceiptMissing { session: _, subject_identity: id } => - id == (subject.subject_identity as String) + id == (serialize_content_hash(hash: subject.subject_identity) as String) _ => false } PublicWriteAdmitted { authority: _ } => false diff --git a/dag/tools/publication_publisher.dag b/dag/tools/publication_publisher.dag index 4dfea3ed965..ca24fc0361b 100644 --- a/dag/tools/publication_publisher.dag +++ b/dag/tools/publication_publisher.dag @@ -1,6 +1,7 @@ module tools.publication_publisher import std.types { Bool, List, NonEmptyStr, String } +import std.content_hash { serialize_content_hash } import extdeps.git.hooks { PrePushStdinRow } import gunbc.public_projection { PublicProjectionPlan, PublicationSubject } import gunbc.publication_admission_delta { @@ -165,7 +166,7 @@ fn newly_public_subject_receipt_refusal( Present { value: DecisionReceiptNotAuthorizing { session: session, - subject_identity: subject.subject_identity as String, + subject_identity: serialize_content_hash(hash: subject.subject_identity) as String, detail: "decision receipt does not authorize newly public subject", }, } @@ -173,7 +174,7 @@ fn newly_public_subject_receipt_refusal( Present { value: DecisionReceiptMissing { session: session, - subject_identity: subject.subject_identity as String, + subject_identity: serialize_content_hash(hash: subject.subject_identity) as String, }, } } From aaec5a2ee0a5ee76b4ac7a9eaf6d8ac2720d1ee4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 2 Aug 2026 03:44:21 +0000 Subject: [PATCH 10/10] WIP: Sole modeled publisher: ordinary sessions lose public-write capability; --- dag/test/claim/publication_publisher_witness_test.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dag/test/claim/publication_publisher_witness_test.dag b/dag/test/claim/publication_publisher_witness_test.dag index 5cd2ffe13ad..6b7a2a743b2 100644 --- a/dag/test/claim/publication_publisher_witness_test.dag +++ b/dag/test/claim/publication_publisher_witness_test.dag @@ -1,7 +1,7 @@ module test.claim.publication_publisher_witness import std.types { Bool, CommitSha, FilePath, GitRef, NonEmptyStr, String } -import std.content_hash { ContentHash, content_hash_of_value, serialize_content_hash } +import std.content_hash { content_hash_of_value, serialize_content_hash } import extdeps.git.hooks { PrePushStdinRow, zero_sha } import extdeps.git.publication_transport { GitRefUpdateSpec,