From 632487d20599a51834819c97284052c29a83475f Mon Sep 17 00:00:00 2001 From: JinWoo Lee Date: Wed, 4 Nov 2026 01:00:02 +0900 Subject: [PATCH 1/3] test(g01): align command fixtures with v3 broker contract --- .../livecanary/ack_command_fixture_test.go | 93 ++++++++++++++++--- .../acquire_loss_command_fixture_test.go | 28 +++++- .../controller_process_broker_fixture_test.go | 23 +++-- .../original_context_command_fixture_test.go | 2 +- .../livecanary/original_context_fixture.go | 13 ++- 5 files changed, 129 insertions(+), 30 deletions(-) diff --git a/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go b/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go index 99a67740..a9b82c35 100644 --- a/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go @@ -298,7 +298,11 @@ func (f *ackCommandFixture) brokerWire(r *http.Request) { } key := r.Method + " " + r.URL.Path f.brokerAttempts = append(f.brokerAttempts, key) // count before body drain or dispatch + v3 := f.base.a.GroupPolicy.Version == 2 primary := []string{"GET " + workflowRunFixturePath(f.transport), "GET /app", "GET /orgs/fixture-org/installation", "GET " + workflowRunFixturePath(f.transport), "POST /app/installations/12/access_tokens", "GET /installation/repositories", "GET /repos/fixture-org/canary", "GET /orgs/fixture-org/actions/runner-groups/3", "GET /orgs/fixture-org/actions/runner-groups/3/repositories"} + if v3 { + primary = []string{"GET " + workflowRunFixturePath(f.transport), "GET /app", "GET /orgs/fixture-org/installation", "POST /app/installations/12/access_tokens", "GET /installation/repositories", "GET /repos/fixture-org/canary", "GET /orgs/fixture-org/actions/runner-groups/3", "GET /orgs/fixture-org/actions/runner-groups/3/repositories", "DELETE /installation/token", "GET " + workflowRunFixturePath(f.transport), "POST /app/installations/12/access_tokens", "GET /installation/repositories", "GET /repos/fixture-org/canary", "GET /orgs/fixture-org/actions/runner-groups/3", "GET /orgs/fixture-org/actions/runner-groups/3/repositories", "GET " + workflowRunFixturePath(f.transport)} + } position := len(f.brokerAttempts) - 1 // shared lock held; already counted before drain fresh := f.transport.attempt == f.attempt+"-fresh" if fresh { @@ -351,13 +355,21 @@ func (f *ackCommandFixture) brokerWire(r *http.Request) { if position == 0 || fresh { token = "synthetic-observation-credential" } + } else if v3 && position >= 4 && position <= 7 { + token = "synthetic-process-read-installation-token" + } else if v3 && r.URL.Path == "/installation/token" { + token = "synthetic-process-read-installation-token" } if auth != "Bearer "+token { t.Error("ACK broker distinct observation/verification/installation authority differs") } } if key == "POST /app/installations/12/access_tokens" { - if string(data) != `{"repository_ids":[42],"permissions":{"metadata":"read","organization_self_hosted_runners":"write"}}` { + want := `{"repository_ids":[42],"permissions":{"metadata":"read","organization_self_hosted_runners":"write"}}` + if v3 && position == 3 { + want = `{"repository_ids":[42],"permissions":{"metadata":"read","organization_self_hosted_runners":"read"}}` + } + if string(data) != want { t.Error("ACK broker exact scoped mint body differs") } } else if len(data) != 0 { @@ -423,7 +435,7 @@ func (f *ackCommandFixture) consumption() *controllerHandoffConsumption { } if event.Kind == "claim" && event.Slot == f.phase && event.Provenance != nil { p := event.Provenance - if found != nil || p.Source != "github-rest-workflow-v1" || len(p.Digest) != 64 || len(p.Nonce) != 32 { + if found != nil || p.Source != originalContextClaimSource(f.base.a) || len(p.Digest) != 64 || len(p.Nonce) != 32 { f.t.Error("ACK original claim provenance differs") } found = &controllerHandoffConsumption{ReceiptNonce: p.Nonce, Phase: f.phase, ApprovalSHA256: handoff.Digest(f.raw), ReceiptSHA256: p.Digest, KeyID: "fixture-ed25519-v1"} @@ -588,11 +600,22 @@ func (f *ackCommandFixture) assertBroker() { f.brokerMu.Lock() defer f.brokerMu.Unlock() want := []string{"GET " + workflowRunFixturePath(f.transport), "GET /app", "GET /orgs/fixture-org/installation", "GET " + workflowRunFixturePath(f.transport), "POST /app/installations/12/access_tokens", "GET /installation/repositories", "GET /repos/fixture-org/canary", "GET /orgs/fixture-org/actions/runner-groups/3", "GET /orgs/fixture-org/actions/runner-groups/3/repositories"} + v3 := f.base.a.GroupPolicy.Version == 2 + if v3 { + want = []string{"GET " + workflowRunFixturePath(f.transport), "GET /app", "GET /orgs/fixture-org/installation", "POST /app/installations/12/access_tokens", "GET /installation/repositories", "GET /repos/fixture-org/canary", "GET /orgs/fixture-org/actions/runner-groups/3", "GET /orgs/fixture-org/actions/runner-groups/3/repositories", "DELETE /installation/token", "GET " + workflowRunFixturePath(f.transport), "POST /app/installations/12/access_tokens", "GET /installation/repositories", "GET /repos/fixture-org/canary", "GET /orgs/fixture-org/actions/runner-groups/3", "GET /orgs/fixture-org/actions/runner-groups/3/repositories", "GET " + workflowRunFixturePath(f.transport)} + } if !reflect.DeepEqual(f.brokerAttempts, f.transport.calls) { f.t.Error("ACK broker server-entry counts differ from dispatched calls") } - if f.transport.mints != 1 || !reflect.DeepEqual(f.transport.calls, want) { - f.t.Errorf("ACK actual broker exact trace differs: calls=%v mints=%d", f.transport.calls, f.transport.mints) + if !reflect.DeepEqual(f.transport.calls, want) { + f.t.Errorf("ACK actual broker exact trace differs: calls=%v want=%v", f.transport.calls, want) + } + if v3 { + if f.transport.mints != 2 || f.transport.readOnlyMints != 1 || f.transport.managementMints != 1 || f.transport.revocations != 1 { + f.t.Errorf("ACK actual broker issuance lifecycle differs: mints=%d read-only=%d management=%d revocations=%d", f.transport.mints, f.transport.readOnlyMints, f.transport.managementMints, f.transport.revocations) + } + } else if f.transport.mints != 1 { + f.t.Errorf("ACK actual broker issuance lifecycle differs: mints=%d", f.transport.mints) } raw := f.read(filepath.Join(f.attempt, "broker.jsonl")) completed := 0 @@ -624,16 +647,46 @@ func (f *ackCommandFixture) assertBroker() { } // Completion preserves the permanent occupied claim and all its provenance. ledger := bytes.Split(bytes.TrimSpace(f.read(filepath.Join(f.admission, "broker-admission.jsonl"))), []byte{'\n'}) - if len(ledger) != 2+wantComplete { + wantLedger := 2 + wantComplete + if v3 { + wantLedger += 2 // read-token lifecycle start and confirmation surround completion. + } + claimIndex, completionIndex := 1, 2 + if v3 { + completionIndex = 4 + } + if len(ledger) != wantLedger { f.t.Error("ACK exact broker claim/completion count differs") - } else if wantComplete == 1 { - var claim, completion map[string]any - if json.Unmarshal(ledger[1], &claim) != nil || json.Unmarshal(ledger[2], &completion) != nil || claim["kind"] != "claim" || completion["kind"] != "complete" { - f.t.Error("ACK durable broker claim/completion unavailable") + } else { + var claimIdentity struct { + Kind string `json:"kind"` + Slot string `json:"slot"` + Attempt json.RawMessage `json:"attempt"` + Journal json.RawMessage `json:"journal"` + } + if json.Unmarshal(ledger[claimIndex], &claimIdentity) != nil || claimIdentity.Kind != "claim" { + f.t.Error("ACK durable broker claim unavailable") + } + if v3 { + var started, confirmed struct { + Kind string `json:"kind"` + Slot string `json:"slot"` + Attempt json.RawMessage `json:"attempt"` + Journal json.RawMessage `json:"journal"` + } + if json.Unmarshal(ledger[2], &started) != nil || json.Unmarshal(ledger[3], &confirmed) != nil || started.Kind != "read_token_lifecycle_started" || confirmed.Kind != "read_token_lifecycle_confirmed" || started.Slot != claimIdentity.Slot || confirmed.Slot != claimIdentity.Slot || !bytes.Equal(started.Attempt, claimIdentity.Attempt) || !bytes.Equal(started.Journal, claimIdentity.Journal) || !bytes.Equal(confirmed.Attempt, claimIdentity.Attempt) || !bytes.Equal(confirmed.Journal, claimIdentity.Journal) { + f.t.Error("ACK read-token lifecycle claim identity differs") + } } - completion["kind"] = "claim" - if !reflect.DeepEqual(claim, completion) { - f.t.Error("ACK completion changed occupied claim/provenance") + if wantComplete == 1 { + var claim, completion map[string]any + if json.Unmarshal(ledger[claimIndex], &claim) != nil || json.Unmarshal(ledger[completionIndex], &completion) != nil || claim["kind"] != "claim" || completion["kind"] != "complete" { + f.t.Error("ACK durable broker claim/completion unavailable") + } + completion["kind"] = "claim" + if !reflect.DeepEqual(claim, completion) { + f.t.Error("ACK completion changed occupied claim/provenance") + } } } snapshot := f.read(filepath.Join(f.attempt, "controller-approval.json")) @@ -652,7 +705,21 @@ func (f *ackCommandFixture) validatePreparation(receipt PreparationReceipt, phas return preparedIdentity(info) } claimPath := f.base.j.claim.file.Name() - want := PreparationReceipt{Version: 1, Status: "controller_journal_prepared", Phase: phase, ApprovalDigest: approvalDigest(f.base.a), State: identity(f.base.j.directory), Journal: identity(filepath.Join(f.base.j.directory, "journal.jsonl")), Claim: identity(claimPath), AdmissionDirectory: identity(f.admission), JournalDigest: handoff.Digest(journal), ClaimDigest: handoff.Digest(f.read(claimPath))} + var rootBinding *handoff.RootBinding + if f.base.a.GroupPolicy.Version == 2 && phase == f.phase { + _, workflow, issuer, e := handoff.DecodePublicPolicy([]byte(experimentalPolicy(f.t, f.base.a, f.raw, "controller", phase, "controller")), "controller", time.Now()) + if e != nil { + f.t.Error("ACK reusable-callee policy decode differs") + } else { + binding, e := handoff.NewRootBinding(workflow, issuer) + if e != nil { + f.t.Error("ACK reusable-callee root binding unavailable") + } else { + rootBinding = &binding + } + } + } + want := PreparationReceipt{Version: 1, Status: "controller_journal_prepared", Phase: phase, ApprovalDigest: approvalDigest(f.base.a), State: identity(f.base.j.directory), Journal: identity(filepath.Join(f.base.j.directory, "journal.jsonl")), Claim: identity(claimPath), AdmissionDirectory: identity(f.admission), JournalDigest: handoff.Digest(journal), ClaimDigest: handoff.Digest(f.read(claimPath)), RootBinding: rootBinding} if !reflect.DeepEqual(receipt, want) { f.t.Error("ACK actual preparation full concrete receipt differs") } diff --git a/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go b/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go index 113a5082..698bcaf1 100644 --- a/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go @@ -160,6 +160,10 @@ func acquireLossPreflight(w http.ResponseWriter, r *http.Request, a Approval) bo body = repo case "/orgs/fixture-org/actions/runner-groups/3": body = map[string]any{"id": 3, "visibility": "selected", "default": false, "allows_public_repositories": false, "inherited": false} + if a.GroupPolicy.Version == 2 { + body.(map[string]any)["restricted_to_workflows"] = true + body.(map[string]any)["selected_workflows"] = []string{a.GroupPolicy.Workflow} + } default: return false } @@ -193,6 +197,11 @@ func newCommandQualificationFixture(t *testing.T, controller, broker, variant, p t.Helper() a := approval() a.WorkflowRef = "refs/heads/main" + // Actual command qualification uses the reusable-callee v3 contract. + // Keep its independent group-policy authority aligned with the policy and + // approval it writes; the legacy zero-value policy is rejected before any + // provider request or issuance. + a.GroupPolicy = handoff.GroupPolicy{Version: 2, OrganizationID: 101, Workflow: a.Organization + "/" + a.Repository + "/.github/workflows/runner.yml@" + strings.Repeat("d", 40)} if phase == "drain" { a.Phases = append(append([]string(nil), a.Phases...), phase) } @@ -326,7 +335,7 @@ func newCommandQualificationFixture(t *testing.T, controller, broker, variant, p if os.WriteFile(f.controller, data, 0500) != nil { t.Fatal("controller TEST helper copy") } - ba := enrollment.BrokerApproval{OwnerNonce: a.OwnerNonce, Mode: "controller", Phase: phase, AppID: a.AppID, AppName: "fixture-app", AppOwner: a.Organization, AppOwnerID: 101, InstallationID: a.InstallationID, Organization: a.Organization, OrganizationID: 101, Repository: a.Repository, RepositoryID: a.RepositoryID, RunnerGroupID: int64(a.RunnerGroupID), RunnerGroupName: "fixture-group", ExpiresAt: a.ExpiresAt, ControllerBinarySHA256: handoff.Digest(data), ControllerApprovalSHA256: handoff.Digest(f.raw), ControllerHarnessSHA: a.HarnessSHA, AllowVerificationAuthority: true} + ba := enrollment.BrokerApproval{GroupPolicy: a.GroupPolicy, ManagementScope: a.ManagementScope, OwnerNonce: a.OwnerNonce, Mode: "controller", Phase: phase, AppID: a.AppID, AppName: "fixture-app", AppOwner: a.Organization, AppOwnerID: 101, InstallationID: a.InstallationID, Organization: a.Organization, OrganizationID: 101, Repository: a.Repository, RepositoryID: a.RepositoryID, RunnerGroupID: int64(a.RunnerGroupID), RunnerGroupName: "fixture-group", ExpiresAt: a.ExpiresAt, ControllerBinarySHA256: handoff.Digest(data), ControllerApprovalSHA256: handoff.Digest(f.raw), ControllerHarnessSHA: a.HarnessSHA, AllowVerificationAuthority: true} raw, _ := json.Marshal(ba) if os.WriteFile(f.brokerApprovalPath, raw, 0600) != nil { t.Fatal("broker approval write") @@ -645,8 +654,9 @@ func (f *acquireLossCommandFixture) guardControlsForPhase(events []Event, phase brokerAfter, _ := os.ReadFile(brokerJournal) f.brokerMu.Lock() replayCalls, replayMints := len(f.transport.calls), f.transport.mints + replayReadOnlyMints, replayManagementMints, replayRevocations := f.transport.readOnlyMints, f.transport.managementMints, f.transport.revocations f.brokerMu.Unlock() - if !bytes.Equal(brokerBefore, brokerAfter) || replayCalls != originalCalls || replayMints != 1 { + if !bytes.Equal(brokerBefore, brokerAfter) || replayCalls != originalCalls || replayMints != 2 || replayReadOnlyMints != 1 || replayManagementMints != 1 || replayRevocations != 1 { t.Error("exact broker-attempt replay changed history or crossed API/mint boundary") } claimPath := filepath.Join(f.admission, "broker-admission.jsonl") @@ -665,8 +675,9 @@ func (f *acquireLossCommandFixture) guardControlsForPhase(events []Event, phase f.run(f.broker, f.brokerArgs(fresh), inputs...) f.brokerMu.Lock() calls, mints := append([]string(nil), f.transport.calls[callCount:]...), f.transport.mints + readOnlyMints, managementMints, revocations := f.transport.readOnlyMints, f.transport.managementMints, f.transport.revocations f.brokerMu.Unlock() - if !reflect.DeepEqual(calls, []string{"GET " + workflowRunFixturePath(f.transport)}) || mints != 1 { + if !reflect.DeepEqual(calls, []string{"GET " + workflowRunFixturePath(f.transport)}) || mints != 2 || readOnlyMints != 1 || managementMints != 1 || revocations != 1 { t.Error("fresh valid broker attempt did not reach observation/admission without remint") } snapshot, e := os.ReadFile(filepath.Join(fresh, "controller-approval.json")) @@ -697,9 +708,16 @@ func TestExperimentalAcquireLossCommand(t *testing.T) { f.run(f.broker, f.brokerArgs(f.attempt), f.management, []byte("synthetic-observation-credential"), f.workflowSeed, f.issuerSeed) f.brokerMu.Lock() mints := f.transport.mints + managementMints := f.transport.managementMints + readOnlyMints := f.transport.readOnlyMints + revocations := f.transport.revocations + calls := append([]string(nil), f.transport.calls...) f.brokerMu.Unlock() - if mints != 1 { - t.Fatal("actual broker issuance did not mint exactly once") + if managementMints != 1 { + t.Fatalf("actual broker management issuance did not mint exactly once: total=%d read-only=%d management=%d revocations=%d calls=%v", mints, readOnlyMints, managementMints, revocations, calls) + } + if mints != 2 || readOnlyMints != 1 || revocations != 1 { + t.Fatalf("actual broker issuance lifecycle differs: total=%d read-only=%d management=%d revocations=%d calls=%v", mints, readOnlyMints, managementMints, revocations, calls) } events := f.assertRetained(variant) if t.Failed() { diff --git a/experiments/g01-scaleset/livecanary/controller_process_broker_fixture_test.go b/experiments/g01-scaleset/livecanary/controller_process_broker_fixture_test.go index 3970fa46..01094359 100644 --- a/experiments/g01-scaleset/livecanary/controller_process_broker_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/controller_process_broker_fixture_test.go @@ -21,14 +21,16 @@ import ( ) type processBrokerHTTP struct { - approval *Approval - t *testing.T - attempt string - calls []string - mints int - revocations int - failPreflight bool - groupFault bool + approval *Approval + t *testing.T + attempt string + calls []string + mints int + readOnlyMints int + managementMints int + revocations int + failPreflight bool + groupFault bool } func (f *processBrokerHTTP) RoundTrip(r *http.Request) (*http.Response, error) { @@ -76,6 +78,11 @@ func (f *processBrokerHTTP) RoundTrip(r *http.Request) (*http.Response, error) { if json.Unmarshal(body, &request) != nil || (request.Permissions["organization_self_hosted_runners"] != "read" && request.Permissions["organization_self_hosted_runners"] != "write") { f.t.Error("mint permission set was invalid") } + if request.Permissions["organization_self_hosted_runners"] == "read" { + f.readOnlyMints++ + } else { + f.managementMints++ + } organizationOnly := f.approval != nil && f.approval.ManagementScope == "organization-only" if !organizationOnly && !reflect.DeepEqual(request.RepositoryIDs, []int64{42}) { f.t.Error("mint not scoped") diff --git a/experiments/g01-scaleset/livecanary/original_context_command_fixture_test.go b/experiments/g01-scaleset/livecanary/original_context_command_fixture_test.go index d358fec5..fcdfc0a7 100644 --- a/experiments/g01-scaleset/livecanary/original_context_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/original_context_command_fixture_test.go @@ -179,7 +179,7 @@ func (p *originalContextParent) coordinate() { f.mu.Lock() f.checkpoint("original-context-target", target.Sequence) raw := f.read(filepath.Join(f.base.j.directory, "journal.jsonl")) - if handoff.Digest(raw) != target.Digest || !originalContextBrokerClaim(f.admission, f.consumption()) || f.counts["DELETE /queue/9"] != 1 || f.counts["POST /tenant/_apis/runtime/runnerscalesets/7/acquirejobs"] != 0 || f.counts["DELETE /tenant/_apis/runtime/runnerscalesets/7/sessions/"+f.sid] != 0 { + if handoff.Digest(raw) != target.Digest || !originalContextBrokerClaim(f.admission, f.consumption(), originalContextClaimSource(f.base.a)) || f.counts["DELETE /queue/9"] != 1 || f.counts["POST /tenant/_apis/runtime/runnerscalesets/7/acquirejobs"] != 0 || f.counts["DELETE /tenant/_apis/runtime/runnerscalesets/7/sessions/"+f.sid] != 0 { f.t.Error("original context target full-prefix/physical prerequisite differs") f.mu.Unlock() return diff --git a/experiments/g01-scaleset/livecanary/original_context_fixture.go b/experiments/g01-scaleset/livecanary/original_context_fixture.go index 5c050fa0..ae8b6601 100644 --- a/experiments/g01-scaleset/livecanary/original_context_fixture.go +++ b/experiments/g01-scaleset/livecanary/original_context_fixture.go @@ -286,7 +286,7 @@ func (f *OriginalContextCommandFixture) sync(file *os.File) error { return ErrJournal } events, e := originalContextEvents(raw, f.approval) - if e != nil || len(events) < 8 || !originalContextBrokerClaim(f.admission, events[7].ControllerHandoff) { + if e != nil || len(events) < 8 || !originalContextBrokerClaim(f.admission, events[7].ControllerHandoff, originalContextClaimSource(f.approval)) { return ErrJournal } n := f.config.frame("", "", "").Sequence @@ -541,7 +541,14 @@ func (o *OriginalContextFixtureOutput) Bytes() []byte { return o.data } -func originalContextBrokerClaim(admission string, c *controllerHandoffConsumption) bool { +func originalContextClaimSource(a Approval) string { + if a.GroupPolicy.Version == 2 { + return handoff.ReusableCalleeSource + } + return "github-rest-workflow-v1" +} + +func originalContextBrokerClaim(admission string, c *controllerHandoffConsumption, source string) bool { if c == nil { return false } @@ -568,7 +575,7 @@ func originalContextBrokerClaim(admission string, c *controllerHandoffConsumptio Nonce string `json:"receipt_nonce"` Source string `json:"source"` } - if originalContextDecode(event["provenance"], &provenance) != nil || provenance.Digest != c.ReceiptSHA256 || provenance.Nonce != c.ReceiptNonce || provenance.Source != "github-rest-workflow-v1" { + if originalContextDecode(event["provenance"], &provenance) != nil || provenance.Digest != c.ReceiptSHA256 || provenance.Nonce != c.ReceiptNonce || provenance.Source != source { return false } count++ From 22189c383e4dc326d5cc39d426ab60a1191eac62 Mon Sep 17 00:00:00 2001 From: JinWoo Lee Date: Wed, 4 Nov 2026 01:16:22 +0900 Subject: [PATCH 2/3] test(g01): account for workflow provenance in broker fixtures --- .../livecanary/ack_command_fixture_test.go | 57 +++++++++++++++---- .../acquire_loss_command_fixture_test.go | 39 +++++++++---- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go b/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go index a9b82c35..609284df 100644 --- a/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go @@ -647,26 +647,62 @@ func (f *ackCommandFixture) assertBroker() { } // Completion preserves the permanent occupied claim and all its provenance. ledger := bytes.Split(bytes.TrimSpace(f.read(filepath.Join(f.admission, "broker-admission.jsonl"))), []byte{'\n'}) - wantLedger := 2 + wantComplete + workflowEvents := 0 + claimIndex := 1 + for claimIndex < len(ledger) { + var event struct { + Kind string `json:"kind"` + } + if json.Unmarshal(ledger[claimIndex], &event) != nil { + f.t.Error("ACK admission event decode differs") + break + } + if event.Kind == "claim" { + break + } + if strings.HasPrefix(event.Kind, "workflow_provenance_get_") { + workflowEvents++ + } + claimIndex++ + } + wantLedger := 2 + workflowEvents + wantComplete + if workflowEvents != 0 && workflowEvents != 2 { + f.t.Errorf("ACK workflow provenance lifecycle events=%d, want none or a start/confirmation pair", workflowEvents) + } if v3 { wantLedger += 2 // read-token lifecycle start and confirmation surround completion. } - claimIndex, completionIndex := 1, 2 + completionIndex := claimIndex + 1 if v3 { - completionIndex = 4 + completionIndex += 2 } - if len(ledger) != wantLedger { - f.t.Error("ACK exact broker claim/completion count differs") + if len(ledger) != wantLedger || claimIndex >= len(ledger) { + f.t.Errorf("ACK exact broker claim/completion count differs: events=%d want=%d claim-index=%d", len(ledger), wantLedger, claimIndex) } else { var claimIdentity struct { - Kind string `json:"kind"` - Slot string `json:"slot"` - Attempt json.RawMessage `json:"attempt"` - Journal json.RawMessage `json:"journal"` + Kind string `json:"kind"` + Slot string `json:"slot"` + Attempt json.RawMessage `json:"attempt"` + Journal json.RawMessage `json:"journal"` + Provenance struct { + Source string `json:"source"` + } `json:"provenance"` } if json.Unmarshal(ledger[claimIndex], &claimIdentity) != nil || claimIdentity.Kind != "claim" { f.t.Error("ACK durable broker claim unavailable") } + if workflowEvents == 2 { + var started, confirmed struct { + Kind string `json:"kind"` + Slot string `json:"slot"` + Attempt json.RawMessage `json:"attempt"` + Journal json.RawMessage `json:"journal"` + Source string `json:"source"` + } + if json.Unmarshal(ledger[1], &started) != nil || json.Unmarshal(ledger[2], &confirmed) != nil || started.Kind != "workflow_provenance_get_started" || confirmed.Kind != "workflow_provenance_get_confirmed" || started.Slot != claimIdentity.Slot || confirmed.Slot != claimIdentity.Slot || !bytes.Equal(started.Attempt, claimIdentity.Attempt) || !bytes.Equal(started.Journal, claimIdentity.Journal) || !bytes.Equal(confirmed.Attempt, claimIdentity.Attempt) || !bytes.Equal(confirmed.Journal, claimIdentity.Journal) || started.Source != claimIdentity.Provenance.Source || confirmed.Source != claimIdentity.Provenance.Source { + f.t.Error("ACK workflow provenance lifecycle claim identity differs") + } + } if v3 { var started, confirmed struct { Kind string `json:"kind"` @@ -674,7 +710,8 @@ func (f *ackCommandFixture) assertBroker() { Attempt json.RawMessage `json:"attempt"` Journal json.RawMessage `json:"journal"` } - if json.Unmarshal(ledger[2], &started) != nil || json.Unmarshal(ledger[3], &confirmed) != nil || started.Kind != "read_token_lifecycle_started" || confirmed.Kind != "read_token_lifecycle_confirmed" || started.Slot != claimIdentity.Slot || confirmed.Slot != claimIdentity.Slot || !bytes.Equal(started.Attempt, claimIdentity.Attempt) || !bytes.Equal(started.Journal, claimIdentity.Journal) || !bytes.Equal(confirmed.Attempt, claimIdentity.Attempt) || !bytes.Equal(confirmed.Journal, claimIdentity.Journal) { + readTokenStartIndex := claimIndex + 1 + if json.Unmarshal(ledger[readTokenStartIndex], &started) != nil || json.Unmarshal(ledger[readTokenStartIndex+1], &confirmed) != nil || started.Kind != "read_token_lifecycle_started" || confirmed.Kind != "read_token_lifecycle_confirmed" || started.Slot != claimIdentity.Slot || confirmed.Slot != claimIdentity.Slot || !bytes.Equal(started.Attempt, claimIdentity.Attempt) || !bytes.Equal(started.Journal, claimIdentity.Journal) || !bytes.Equal(confirmed.Attempt, claimIdentity.Attempt) || !bytes.Equal(confirmed.Journal, claimIdentity.Journal) { f.t.Error("ACK read-token lifecycle claim identity differs") } } diff --git a/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go b/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go index 698bcaf1..052d484c 100644 --- a/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go @@ -664,25 +664,44 @@ func (f *acquireLossCommandFixture) guardControlsForPhase(events []Event, phase if e != nil || !bytes.Contains(claim, []byte(`"slot":"`+phase+`"`)) { t.Fatal("original occupied broker claim missing") } - // Fresh valid inputs may be consumed. Require valid observation plus the - // exact synced approval snapshot and unchanged occupied claim, establishing - // admission-stage refusal rather than a bad seed/payload/setup rejection. + // A confirmed account-ledger workflow GET permanently occupies this phase. + // Older source without account-level GET accounting reaches the existing + // broker admission fence after observation; both paths must avoid reminting. fresh := f.attempt + "-fresh" + workflowGETAccounted := bytes.Contains(claim, []byte(`"kind":"workflow_provenance_get_confirmed"`)) f.brokerMu.Lock() callCount := len(f.transport.calls) + mintsBefore := f.transport.mints + readOnlyMintsBefore := f.transport.readOnlyMints + managementMintsBefore := f.transport.managementMints + revocationsBefore := f.transport.revocations f.transport.attempt = fresh f.brokerMu.Unlock() f.run(f.broker, f.brokerArgs(fresh), inputs...) f.brokerMu.Lock() - calls, mints := append([]string(nil), f.transport.calls[callCount:]...), f.transport.mints - readOnlyMints, managementMints, revocations := f.transport.readOnlyMints, f.transport.managementMints, f.transport.revocations + calls := append([]string(nil), f.transport.calls[callCount:]...) + mintsAfter := f.transport.mints + readOnlyMintsAfter := f.transport.readOnlyMints + managementMintsAfter := f.transport.managementMints + revocationsAfter := f.transport.revocations f.brokerMu.Unlock() - if !reflect.DeepEqual(calls, []string{"GET " + workflowRunFixturePath(f.transport)}) || mints != 2 || readOnlyMints != 1 || managementMints != 1 || revocations != 1 { - t.Error("fresh valid broker attempt did not reach observation/admission without remint") + wantCalls := []string{"GET " + workflowRunFixturePath(f.transport)} + if workflowGETAccounted { + wantCalls = nil } - snapshot, e := os.ReadFile(filepath.Join(fresh, "controller-approval.json")) - if e != nil || !bytes.Equal(snapshot, f.raw) { - t.Error("fresh broker admission exact approval snapshot absent") + if !reflect.DeepEqual(calls, wantCalls) || mintsAfter != mintsBefore || readOnlyMintsAfter != readOnlyMintsBefore || managementMintsAfter != managementMintsBefore || revocationsAfter != revocationsBefore { + t.Error("fresh occupied-phase broker attempt crossed the expected provider or token boundary") + } + snapshotPath := filepath.Join(fresh, "controller-approval.json") + if workflowGETAccounted { + if _, e := os.Lstat(snapshotPath); !os.IsNotExist(e) { + t.Error("fresh account-ledger occupied-phase attempt created an approval snapshot") + } + } else { + snapshot, e := os.ReadFile(snapshotPath) + if e != nil || !bytes.Equal(snapshot, f.raw) { + t.Error("fresh legacy attempt did not reach the existing admission fence") + } } claimAfter, _ := os.ReadFile(claimPath) after, _ := os.ReadFile(journal) From 707164b1b1cab0198f376e18244b11140b3bd044 Mon Sep 17 00:00:00 2001 From: JinWoo Lee Date: Wed, 4 Nov 2026 01:40:25 +0900 Subject: [PATCH 3/3] test(g01): align v3 provenance fixture assertions --- .../livecanary/ack_command_fixture_test.go | 9 +- .../acquire_loss_command_fixture_test.go | 186 +++++++++++++++++- .../livecanary/drain_command_fixture_test.go | 7 +- 3 files changed, 184 insertions(+), 18 deletions(-) diff --git a/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go b/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go index 609284df..9511aefd 100644 --- a/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/ack_command_fixture_test.go @@ -692,14 +692,7 @@ func (f *ackCommandFixture) assertBroker() { f.t.Error("ACK durable broker claim unavailable") } if workflowEvents == 2 { - var started, confirmed struct { - Kind string `json:"kind"` - Slot string `json:"slot"` - Attempt json.RawMessage `json:"attempt"` - Journal json.RawMessage `json:"journal"` - Source string `json:"source"` - } - if json.Unmarshal(ledger[1], &started) != nil || json.Unmarshal(ledger[2], &confirmed) != nil || started.Kind != "workflow_provenance_get_started" || confirmed.Kind != "workflow_provenance_get_confirmed" || started.Slot != claimIdentity.Slot || confirmed.Slot != claimIdentity.Slot || !bytes.Equal(started.Attempt, claimIdentity.Attempt) || !bytes.Equal(started.Journal, claimIdentity.Journal) || !bytes.Equal(confirmed.Attempt, claimIdentity.Attempt) || !bytes.Equal(confirmed.Journal, claimIdentity.Journal) || started.Source != claimIdentity.Provenance.Source || confirmed.Source != claimIdentity.Provenance.Source { + if !workflowProvenancePairMatchesClaim(ledger[1], ledger[2], ledger[claimIndex]) { f.t.Error("ACK workflow provenance lifecycle claim identity differs") } } diff --git a/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go b/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go index 052d484c..f14788f1 100644 --- a/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/acquire_loss_command_fixture_test.go @@ -30,6 +30,130 @@ import ( const acquireLossPhase = "acquire-loss" +func workflowProvenancePairMatchesClaim(startedRaw, confirmedRaw, claimRaw []byte) bool { + type inode struct { + Device uint64 `json:"device"` + Inode uint64 `json:"inode"` + } + type workflowEvent struct { + Kind string `json:"kind"` + Slot string `json:"slot"` + Attempt inode `json:"attempt"` + Journal inode `json:"journal"` + ControllerApprovalSHA256 string `json:"controller_approval_sha256"` + Source string `json:"source"` + WorkflowRunID int64 `json:"workflow_run_id"` + WorkflowRef string `json:"workflow_ref"` + WorkflowSHA string `json:"workflow_sha"` + WorkflowPath string `json:"workflow_path"` + } + type claimEvent struct { + Kind string `json:"kind"` + Slot string `json:"slot"` + Attempt inode `json:"attempt"` + Journal inode `json:"journal"` + SnapshotDigest string `json:"snapshot_digest"` + Provenance struct { + Source string `json:"source"` + } `json:"provenance"` + Authority struct { + Approval struct { + WorkflowRunID int64 `json:"workflow_run_id"` + WorkflowRef string `json:"workflow_ref"` + WorkflowSHA string `json:"workflow_sha"` + WorkflowPath string `json:"workflow_path"` + } `json:"approval"` + } `json:"authority"` + } + decode := func(raw []byte, target any) bool { + var fields map[string]json.RawMessage + return DecodeStrict(raw, &fields) == nil && json.Unmarshal(raw, target) == nil + } + var started, confirmed workflowEvent + var claim claimEvent + if !decode(startedRaw, &started) || !decode(confirmedRaw, &confirmed) || !decode(claimRaw, &claim) || + started.Kind != "workflow_provenance_get_started" || confirmed.Kind != "workflow_provenance_get_confirmed" || claim.Kind != "claim" || + claim.Slot == "" || claim.Attempt.Device == 0 || claim.Attempt.Inode == 0 || claim.Journal.Device == 0 || claim.Journal.Inode == 0 || + claim.SnapshotDigest == "" || claim.Provenance.Source == "" || claim.Authority.Approval.WorkflowRunID <= 0 || + claim.Authority.Approval.WorkflowRef == "" || claim.Authority.Approval.WorkflowSHA == "" || claim.Authority.Approval.WorkflowPath == "" { + return false + } + confirmed.Kind = "" + started.Kind = "" + return started == confirmed && started.Slot == claim.Slot && started.Attempt == claim.Attempt && started.Journal == claim.Journal && + started.ControllerApprovalSHA256 == claim.SnapshotDigest && started.Source == claim.Provenance.Source && + started.WorkflowRunID == claim.Authority.Approval.WorkflowRunID && started.WorkflowRef == claim.Authority.Approval.WorkflowRef && + started.WorkflowSHA == claim.Authority.Approval.WorkflowSHA && started.WorkflowPath == claim.Authority.Approval.WorkflowPath +} + +func TestWorkflowProvenancePairMatchesExactClaim(t *testing.T) { + event := func(kind string) map[string]any { + return map[string]any{ + "kind": kind, "slot": "after-ack", + "attempt": map[string]any{"device": uint64(1), "inode": uint64(2)}, + "journal": map[string]any{"device": uint64(3), "inode": uint64(4)}, + "controller_approval_sha256": strings.Repeat("a", 64), + "source": "signed-fixture-v1", + "workflow_run_id": int64(7), + "workflow_ref": "refs/heads/main", + "workflow_sha": strings.Repeat("b", 40), + "workflow_path": ".github/workflows/canary.yml", + } + } + claim := map[string]any{ + "kind": "claim", "slot": "after-ack", + "attempt": map[string]any{"device": uint64(1), "inode": uint64(2)}, + "journal": map[string]any{"device": uint64(3), "inode": uint64(4)}, + "snapshot_digest": strings.Repeat("a", 64), + "provenance": map[string]any{"source": "signed-fixture-v1"}, + "authority": map[string]any{"approval": map[string]any{ + "workflow_run_id": int64(7), "workflow_ref": "refs/heads/main", + "workflow_sha": strings.Repeat("b", 40), "workflow_path": ".github/workflows/canary.yml", + }}, + } + encode := func(value any) []byte { + t.Helper() + raw, err := json.Marshal(value) + if err != nil { + t.Fatal("fixture JSON encoding failed") + } + return raw + } + if !workflowProvenancePairMatchesClaim(encode(event("workflow_provenance_get_started")), encode(event("workflow_provenance_get_confirmed")), encode(claim)) { + t.Fatal("matching workflow provenance pair rejected") + } + mutations := []struct { + name string + field string + value any + }{ + {"controller approval", "controller_approval_sha256", strings.Repeat("c", 64)}, + {"workflow run", "workflow_run_id", int64(8)}, + {"workflow ref", "workflow_ref", "refs/heads/other"}, + {"workflow SHA", "workflow_sha", strings.Repeat("d", 40)}, + {"workflow path", "workflow_path", ".github/workflows/other.yml"}, + {"attempt", "attempt", map[string]any{"device": uint64(5), "inode": uint64(6)}}, + {"journal", "journal", map[string]any{"device": uint64(7), "inode": uint64(8)}}, + {"source", "source", "other-source"}, + } + for _, mutation := range mutations { + t.Run(mutation.name, func(t *testing.T) { + started, confirmed := event("workflow_provenance_get_started"), event("workflow_provenance_get_confirmed") + started[mutation.field], confirmed[mutation.field] = mutation.value, mutation.value + if workflowProvenancePairMatchesClaim(encode(started), encode(confirmed), encode(claim)) { + t.Error("mismatched workflow provenance identity accepted") + } + }) + } + t.Run("start-and-confirmation-pair", func(t *testing.T) { + started, confirmed := event("workflow_provenance_get_started"), event("workflow_provenance_get_confirmed") + confirmed["workflow_ref"] = "refs/heads/other" + if workflowProvenancePairMatchesClaim(encode(started), encode(confirmed), encode(claim)) { + t.Error("mismatched start and confirmation accepted") + } + }) +} + // Qualification requires both actual, current-source commands. Missing/stale // helpers are setup failures, never optional coverage or an in-process fallback. func acquireLossHelpers(t *testing.T) (string, string) { @@ -610,12 +734,26 @@ func (f *acquireLossCommandFixture) guardControlsForPhase(events []Event, phase for _, n := range []string{nonce, freshNonce} { a := f.base.a now := time.Now().UTC().Truncate(time.Second) - _, request, e := controllerHandoffRequest(f.raw, phase, "github-rest-workflow-v1", now) - if e != nil { - t.Fatal("receiver negative request setup") + policyJSON := []byte(experimentalPolicy(t, a, f.raw, "controller", phase, "controller")) + policy, workflowRoot, issuerRoot, e := handoff.DecodePublicPolicy(policyJSON, "controller", now) + if e != nil || len(policy.Grants) != 1 { + t.Fatal("receiver negative selected policy unavailable") + } + var request handoff.Request + if policy.Version == 3 { + _, request, e = controllerHandoffRequestForExpected(f.raw, phase, policy.Source, &policy.Grants[0].Request, now) + } else { + _, request, e = controllerHandoffRequest(f.raw, phase, policy.Source, now) + } + if e != nil || request != policy.Grants[0].Request { + t.Fatal("receiver negative request does not match selected policy grant") } key := ed25519.NewKeyFromSeed(f.workflowSeed) receipt := signedControllerReceipt(t, request, now, n, key) + if request.Version == 2 { + receipt.Version = 2 + receipt.Callee = request.Callee + } receipt.ExpiresAt = a.ExpiresAt receipt.Signature = base64.RawURLEncoding.EncodeToString(ed25519.Sign(key, receipt.SigningBytes())) receiptRaw, _ := json.Marshal(receipt) @@ -624,9 +762,7 @@ func (f *acquireLossCommandFixture) guardControlsForPhase(events []Event, phase binding := handoff.PhaseBinding{BindingClaims: handoff.BindingClaims{Request: request, Version: 2, Kind: handoff.PhaseBindingKind, Algorithm: "ed25519", Audience: handoff.PhaseAudience("controller", phase), KeyID: "fixture-issuer-v1", IssuerSource: handoff.IssuerSource, WorkflowKeyID: receipt.KeyID, ReceiptSHA256: handoff.Digest(receiptRaw), PayloadSHA256: handoff.Digest(payload), PayloadLength: len(payload), ReceiptNonce: n, IssuedAt: now, ExpiresAt: a.ExpiresAt}, Mode: "controller"} issuerKey := ed25519.NewKeyFromSeed(f.issuerSeed) binding.Signature = base64.RawURLEncoding.EncodeToString(ed25519.Sign(issuerKey, binding.SigningBytes())) - workflow, _ := handoff.NewTrustRoot(receipt.KeyID, key.Public().(ed25519.PublicKey)) - issuer, _ := handoff.NewIssuerRoot(binding.KeyID, issuerKey.Public().(ed25519.PublicKey)) - if workflow.Verify(request, receipt, time.Now()) != nil || issuer.VerifyPhaseBinding(handoff.BindingExpected{Request: request, Receipt: receipt, ReceiptBytes: receiptRaw, ApprovalExpiresAt: a.ExpiresAt, Deadline: a.ExpiresAt}, binding, time.Now()) != nil { + if workflowRoot.Verify(request, receipt, time.Now()) != nil || issuerRoot.VerifyPhaseBinding(handoff.BindingExpected{Request: request, Receipt: receipt, ReceiptBytes: receiptRaw, ApprovalExpiresAt: a.ExpiresAt, Deadline: a.ExpiresAt}, binding, time.Now()) != nil { t.Fatal("receiver negative specimen is not otherwise valid") } clear(key) @@ -661,14 +797,48 @@ func (f *acquireLossCommandFixture) guardControlsForPhase(events []Event, phase } claimPath := filepath.Join(f.admission, "broker-admission.jsonl") claim, e := os.ReadFile(claimPath) - if e != nil || !bytes.Contains(claim, []byte(`"slot":"`+phase+`"`)) { + if e != nil { t.Fatal("original occupied broker claim missing") } + ledger := bytes.Split(bytes.TrimSpace(claim), []byte{'\n'}) + claimIndex := -1 + for i := 1; i < len(ledger); i++ { + var event struct { + Kind string `json:"kind"` + Slot string `json:"slot"` + } + if json.Unmarshal(ledger[i], &event) != nil { + t.Fatal("original broker admission event malformed") + } + if event.Kind == "claim" && event.Slot == phase { + if claimIndex >= 0 { + t.Fatal("duplicate original occupied broker claim") + } + claimIndex = i + } + } + if claimIndex < 0 { + t.Fatal("original occupied broker claim identity missing") + } // A confirmed account-ledger workflow GET permanently occupies this phase. // Older source without account-level GET accounting reaches the existing // broker admission fence after observation; both paths must avoid reminting. fresh := f.attempt + "-fresh" - workflowGETAccounted := bytes.Contains(claim, []byte(`"kind":"workflow_provenance_get_confirmed"`)) + workflowGETAccounted := false + for i := 1; i < claimIndex; i++ { + var event struct { + Kind string `json:"kind"` + } + if json.Unmarshal(ledger[i], &event) != nil { + t.Fatal("original broker workflow provenance event malformed") + } + if event.Kind == "workflow_provenance_get_started" { + if workflowGETAccounted || i+1 >= claimIndex || !workflowProvenancePairMatchesClaim(ledger[i], ledger[i+1], ledger[claimIndex]) { + t.Fatal("original broker workflow provenance pair is not bound to its claim") + } + workflowGETAccounted = true + } + } f.brokerMu.Lock() callCount := len(f.transport.calls) mintsBefore := f.transport.mints diff --git a/experiments/g01-scaleset/livecanary/drain_command_fixture_test.go b/experiments/g01-scaleset/livecanary/drain_command_fixture_test.go index 98fa2b5f..79177e29 100644 --- a/experiments/g01-scaleset/livecanary/drain_command_fixture_test.go +++ b/experiments/g01-scaleset/livecanary/drain_command_fixture_test.go @@ -621,9 +621,12 @@ func TestExperimentalDrainCommand(t *testing.T) { f.freezeHandlers() f.brokerMu.Lock() mints := f.transport.mints + readOnlyMints := f.transport.readOnlyMints + managementMints := f.transport.managementMints + revocations := f.transport.revocations f.brokerMu.Unlock() - if mints != 1 { - t.Error("actual signed broker mint1 missing") + if mints != 2 || readOnlyMints != 1 || managementMints != 1 || revocations != 1 { + t.Errorf("actual signed broker issuance lifecycle differs: total=%d read-only=%d management=%d revocations=%d", mints, readOnlyMints, managementMints, revocations) } events := f.assertDrain() if t.Failed() {