package api import ( "context" "encoding/json" "fmt" "os" "path/filepath" "time" "bat-api/internal/backendrpc" ) // Backend is the subset of daemon RPC used by bat-api. // // Call order for discovery: // 1. daemon.status // 2. daemon.doctor // 3. release.attestation // 4. catalog.status / bound resource.manifest (and resource.state as needed) type Backend interface { DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error) DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error) ResourceState(ctx context.Context) (*backendrpc.ResourceState, error) CatalogStatus(ctx context.Context) (json.RawMessage, error) ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error) ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error) } // ControlBackend is the explicitly allowlisted mutation subset exposed through // the authenticated bat-api admin control surface. // // It intentionally does not include daemon.stop, cleanup, or generic RPC calls. // Restart is forwarded only to Rust's lifecycle RPC; Go never execs bat itself. type ControlBackend interface { DaemonRestart(ctx context.Context) (*backendrpc.Ack, error) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) DaemonRefresh(ctx context.Context, force bool) (*backendrpc.Ack, error) ResourceSync(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) ResourceVerify(ctx context.Context) (*backendrpc.TaskAccepted, error) ResourceRepair(ctx context.Context) (*backendrpc.TaskAccepted, error) CatalogRefresh(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) } // DaemonLogsBackend exposes the Rust daemon log tail for an authenticated // dashboard. It remains read-only and never opens log files from Go. type DaemonLogsBackend interface { DaemonLogs(ctx context.Context, tail int) (*backendrpc.LogsReport, error) } // TaskBackend exposes Rust-owned async task state to the dashboard. Go only // forwards read/cancel requests and does not create generic tasks. type TaskBackend interface { TaskList(ctx context.Context) (*backendrpc.TaskList, error) TaskStatus(ctx context.Context, taskID string) (*backendrpc.TaskRecord, error) TaskLogs(ctx context.Context, taskID string) (*backendrpc.TaskLogs, error) TaskCancel(ctx context.Context, taskID string) (*backendrpc.TaskCancelResult, error) } // ParseBackend exposes existing Rust TextUnit index queries to the dashboard. // It is read-only and does not expand parser coverage. type ParseBackend interface { ParseStatus(ctx context.Context) (json.RawMessage, error) ParseTextUnits(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error) ParseErrors(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error) } // ScheduleBackend exposes the Rust-owned schedule store to an authenticated // dashboard. The JSON result remains Rust's report shape so the API does not // duplicate schedule state or invent a second schema. type ScheduleBackend interface { ScheduleList(ctx context.Context, params backendrpc.ScheduleListParams) (json.RawMessage, error) ScheduleAdd(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) ScheduleUpdate(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) ScheduleRemove(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error) } // TranslationBackend exposes the narrow provider-worker state mutation used // by the dashboard. It does not create arbitrary translation jobs. type TranslationBackend interface { TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) TranslationTasks(ctx context.Context, params backendrpc.TranslationTaskListParams) (json.RawMessage, error) TranslationHandoff(ctx context.Context) (json.RawMessage, error) TranslationWorkerRun(ctx context.Context, params backendrpc.TranslationWorkerRunParams) (*backendrpc.TranslationWorkerRunResult, error) TranslationProofread(ctx context.Context) (json.RawMessage, error) } // TranslationMemoryBackend exposes the Rust-owned Translation Memory query and // explicit confirmation operations. Go forwards typed requests and responses // but never opens or mutates the TM database itself. type TranslationMemoryBackend interface { TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error) TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error) TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error) TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error) } // GlossaryBackend exposes Rust-owned Glossary management and diagnostics. // Go forwards these typed calls and never opens the Glossary database. type GlossaryBackend interface { GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) GlossaryQuery(ctx context.Context, params backendrpc.GlossaryQueryParams) (*backendrpc.GlossaryQueryReport, error) GlossaryDiagnose(ctx context.Context, params backendrpc.GlossaryDiagnoseParams) (*backendrpc.GlossaryDiagnoseReport, error) GlossaryAdd(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) GlossaryUpdate(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) GlossaryApprove(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) GlossaryDeprecate(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) GlossaryDelete(ctx context.Context, params backendrpc.GlossaryDeleteParams) (*backendrpc.GlossaryMutationReport, error) } // LocalizedBackend exposes localized release status and the explicit // publish/rollback controls used by the authenticated dashboard. type LocalizedBackend interface { LocalizedStatus(ctx context.Context) (json.RawMessage, error) LocalizedPublish(ctx context.Context, params backendrpc.LocalizedPublishParams) (json.RawMessage, error) LocalizedRollback(ctx context.Context, params backendrpc.LocalizedRollbackParams) (json.RawMessage, error) } // ReleaseStatusBackend exposes the Rust-owned release health fact used during // discovery. It is kept separate so lightweight test/diagnostic backends do // not have to implement the administrative release surface. type ReleaseStatusBackend interface { ReleaseStatus(ctx context.Context) (*backendrpc.ReleaseStatusReport, error) } // ReleaseBackend exposes Rust-owned dual-release queries, distribution // selection and the explicit cleanup operation. type ReleaseBackend interface { ReleaseStatusBackend ReleaseList(ctx context.Context, params backendrpc.ReleaseListParams) (*backendrpc.ReleaseListReport, error) ReleaseDistribution(ctx context.Context, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) ReleaseCleanup(ctx context.Context, params backendrpc.ReleaseCleanupParams) (*backendrpc.ReleaseCleanupReport, error) } // RPCClient adapts *backendrpc.Client to Backend. type RPCClient struct { Client *backendrpc.Client } func (r RPCClient) DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error) { return r.Client.DaemonStatus(ctx) } func (r RPCClient) DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error) { return r.Client.DaemonDoctor(ctx) } func (r RPCClient) ResourceState(ctx context.Context) (*backendrpc.ResourceState, error) { return r.Client.ResourceState(ctx) } func (r RPCClient) CatalogStatus(ctx context.Context) (json.RawMessage, error) { return r.Client.CatalogStatus(ctx) } func (r RPCClient) ResourceManifest(ctx context.Context, params backendrpc.ResourceManifestParams) (*backendrpc.ResourceManifestPage, error) { return r.Client.ResourceManifest(ctx, params) } func (r RPCClient) ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error) { return r.Client.ReleaseAttestation(ctx) } func (r RPCClient) DaemonRestart(ctx context.Context) (*backendrpc.Ack, error) { return r.Client.DaemonRestart(ctx) } func (r RPCClient) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) { return r.Client.DaemonReload(ctx) } func (r RPCClient) DaemonRefresh(ctx context.Context, force bool) (*backendrpc.Ack, error) { return r.Client.DaemonRefresh(ctx, force) } func (r RPCClient) DaemonLogs(ctx context.Context, tail int) (*backendrpc.LogsReport, error) { return r.Client.DaemonLogs(ctx, tail) } func (r RPCClient) ResourceSync(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) { return r.Client.ResourceSync(ctx, force) } func (r RPCClient) ResourceVerify(ctx context.Context) (*backendrpc.TaskAccepted, error) { return r.Client.ResourceVerify(ctx) } func (r RPCClient) ResourceRepair(ctx context.Context) (*backendrpc.TaskAccepted, error) { return r.Client.ResourceRepair(ctx) } func (r RPCClient) CatalogRefresh(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) { return r.Client.CatalogRefresh(ctx, force) } func (r RPCClient) TaskList(ctx context.Context) (*backendrpc.TaskList, error) { return r.Client.TaskList(ctx) } func (r RPCClient) TaskStatus(ctx context.Context, taskID string) (*backendrpc.TaskRecord, error) { return r.Client.TaskStatus(ctx, taskID) } func (r RPCClient) TaskLogs(ctx context.Context, taskID string) (*backendrpc.TaskLogs, error) { return r.Client.TaskLogs(ctx, taskID) } func (r RPCClient) TaskCancel(ctx context.Context, taskID string) (*backendrpc.TaskCancelResult, error) { return r.Client.TaskCancel(ctx, taskID) } func (r RPCClient) ScheduleList(ctx context.Context, params backendrpc.ScheduleListParams) (json.RawMessage, error) { return r.Client.ScheduleListFiltered(ctx, params) } func (r RPCClient) ScheduleAdd(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) { return r.Client.ScheduleAdd(ctx, params) } func (r RPCClient) ScheduleUpdate(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) { return r.Client.ScheduleUpdate(ctx, params) } func (r RPCClient) ScheduleRemove(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) { return r.Client.ScheduleRemove(ctx, params) } func (r RPCClient) ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error) { return r.Client.ScheduleRun(ctx, params) } func (r RPCClient) TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) { return r.Client.TranslationTaskUpdate(ctx, params) } func (r RPCClient) TranslationTasks(ctx context.Context, params backendrpc.TranslationTaskListParams) (json.RawMessage, error) { return r.Client.TranslationTasks(ctx, params) } func (r RPCClient) TranslationHandoff(ctx context.Context) (json.RawMessage, error) { return r.Client.TranslationHandoff(ctx) } func (r RPCClient) TranslationWorkerRun(ctx context.Context, params backendrpc.TranslationWorkerRunParams) (*backendrpc.TranslationWorkerRunResult, error) { return r.Client.TranslationWorkerRun(ctx, params) } func (r RPCClient) TranslationProofread(ctx context.Context) (json.RawMessage, error) { return r.Client.TranslationProofread(ctx) } func (r RPCClient) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) { return r.Client.TranslationMemorySummary(ctx, params) } func (r RPCClient) TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error) { return r.Client.TranslationMemoryQuery(ctx, params) } func (r RPCClient) TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error) { return r.Client.TranslationMemoryConfirm(ctx, params) } func (r RPCClient) TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error) { return r.Client.TranslationMemoryConflicts(ctx, params) } func (r RPCClient) TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error) { return r.Client.TranslationMemoryResolveConflict(ctx, params) } func (r RPCClient) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) { return r.Client.GlossarySummary(ctx, params) } func (r RPCClient) GlossaryQuery(ctx context.Context, params backendrpc.GlossaryQueryParams) (*backendrpc.GlossaryQueryReport, error) { return r.Client.GlossaryQuery(ctx, params) } func (r RPCClient) GlossaryDiagnose(ctx context.Context, params backendrpc.GlossaryDiagnoseParams) (*backendrpc.GlossaryDiagnoseReport, error) { return r.Client.GlossaryDiagnose(ctx, params) } func (r RPCClient) GlossaryAdd(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) { return r.Client.GlossaryAdd(ctx, params) } func (r RPCClient) GlossaryUpdate(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) { return r.Client.GlossaryUpdate(ctx, params) } func (r RPCClient) GlossaryApprove(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) { return r.Client.GlossaryApprove(ctx, params) } func (r RPCClient) GlossaryDeprecate(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) { return r.Client.GlossaryDeprecate(ctx, params) } func (r RPCClient) GlossaryDelete(ctx context.Context, params backendrpc.GlossaryDeleteParams) (*backendrpc.GlossaryMutationReport, error) { return r.Client.GlossaryDelete(ctx, params) } func (r RPCClient) LocalizedStatus(ctx context.Context) (json.RawMessage, error) { return r.Client.LocalizedStatus(ctx) } func (r RPCClient) LocalizedPublish(ctx context.Context, params backendrpc.LocalizedPublishParams) (json.RawMessage, error) { return r.Client.LocalizedPublish(ctx, params) } func (r RPCClient) LocalizedRollback(ctx context.Context, params backendrpc.LocalizedRollbackParams) (json.RawMessage, error) { return r.Client.LocalizedRollback(ctx, params) } func (r RPCClient) ReleaseStatus(ctx context.Context) (*backendrpc.ReleaseStatusReport, error) { return r.Client.ReleaseStatus(ctx) } func (r RPCClient) ReleaseList(ctx context.Context, params backendrpc.ReleaseListParams) (*backendrpc.ReleaseListReport, error) { return r.Client.ReleaseList(ctx, params) } func (r RPCClient) ReleaseDistribution(ctx context.Context, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) { return r.Client.ReleaseDistribution(ctx, params) } func (r RPCClient) ReleaseCleanup(ctx context.Context, params backendrpc.ReleaseCleanupParams) (*backendrpc.ReleaseCleanupReport, error) { return r.Client.ReleaseCleanup(ctx, params) } func (r RPCClient) ParseStatus(ctx context.Context) (json.RawMessage, error) { return r.Client.ParseStatus(ctx) } func (r RPCClient) ParseTextUnits(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error) { return r.Client.ParseTextUnits(ctx, query) } func (r RPCClient) ParseErrors(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error) { return r.Client.ParseErrors(ctx, query) } func (r RPCClient) UnityFSPatchTextAsset(ctx context.Context, params backendrpc.UnityFSTextAssetPatchParams) (json.RawMessage, error) { return r.Client.UnityFSPatchTextAsset(ctx, params) } func (r RPCClient) UnityFSPatchStringField(ctx context.Context, params backendrpc.UnityFSStringFieldPatchParams) (json.RawMessage, error) { return r.Client.UnityFSPatchStringField(ctx, params) } func (r RPCClient) UnityFSPatchField(ctx context.Context, params backendrpc.UnityFSFieldPatchParams) (json.RawMessage, error) { return r.Client.UnityFSPatchField(ctx, params) } // DiscoverResult is the outcome of talking to the bat daemon. type DiscoverResult struct { RPCAvailable bool DoctorHealthy *bool Status *backendrpc.DaemonStatusReport Doctor *backendrpc.DoctorReport Attestation *backendrpc.DistributionAttestation ReleaseStatus *backendrpc.ReleaseStatusReport Distribution DistributionHealth Snapshot *SnapshotSummary ResourceRoot string Index *ReleaseIndex Warnings []string } // DiscoverAndIndex contacts the daemon (status first, then doctor, then the // lightweight current-release attestation) and builds a release index from // pages bound to that attestation plus on-disk checks. Rust's attestation is // the only release-level integrity authorization used for the production RPC // path; release.status remains an administrative diagnostic. // // If resourceRootOverride is non-empty, it wins over RPC-reported roots after // RPC health probes (still preferred for production to call status/doctor). func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride string) (*DiscoverResult, error) { out := &DiscoverResult{} if backend == nil { if resourceRootOverride == "" { out.Index = &ReleaseIndex{Source: "empty", byRel: map[string]int{}} return out, nil } idx, err := LoadIndexFromResourceRoot(resourceRootOverride) if err != nil { return nil, err } out.ResourceRoot = idx.ResourceRoot out.Distribution = idx.Distribution out.Index = idx return out, nil } // 1) daemon.status first status, err := backend.DaemonStatus(ctx) if err != nil { out.Warnings = append(out.Warnings, fmt.Sprintf("daemon.status: %v", err)) if resourceRootOverride != "" { idx, loadErr := LoadIndexFromResourceRoot(resourceRootOverride) if loadErr != nil { return out, fmt.Errorf("daemon.status failed (%v) and resource-root load failed: %w", err, loadErr) } out.ResourceRoot = idx.ResourceRoot out.Distribution = idx.Distribution out.Index = idx return out, nil } out.Index = &ReleaseIndex{Source: "empty", byRel: map[string]int{}} return out, nil } out.RPCAvailable = true out.Status = status // 2) daemon.doctor second doctor, err := backend.DaemonDoctor(ctx) if err != nil { out.Warnings = append(out.Warnings, fmt.Sprintf("daemon.doctor: %v", err)) } else { out.Doctor = doctor h := doctor.Healthy out.DoctorHealthy = &h } // An explicit root is a fixture/emergency read-only override. Keep it // outside the production RPC release-health contract, while still probing // daemon status and doctor first. if resourceRootOverride != "" { idx, loadErr := LoadIndexFromResourceRoot(resourceRootOverride) if loadErr != nil { return out, fmt.Errorf("resource-root override load failed: %w", loadErr) } idx.Source = "resource_root" idx.RPCAvailable = true idx.DoctorHealthy = out.DoctorHealthy out.ResourceRoot = idx.ResourceRoot out.Snapshot = idx.Snapshot out.Distribution = idx.Distribution out.Index = idx return out, nil } // 3) release.attestation is the Rust-owned current-release distribution gate. attestationBackend, ok := backend.(AttestationBackend) if !ok { out.Warnings = append(out.Warnings, "release.attestation: backend does not expose Rust health proof") return emptyRPCResult(out, nil, "Rust release health is unavailable"), fmt.Errorf("rust release health is unavailable") } attestation, err := attestationBackend.ReleaseAttestation(ctx) if err != nil { out.Warnings = append(out.Warnings, fmt.Sprintf("release.attestation: %v", err)) return emptyRPCResult(out, nil, "Rust release health query failed"), fmt.Errorf("release.attestation failed: %w", err) } if attestation == nil { out.Warnings = append(out.Warnings, "release.attestation: empty response") return emptyRPCResult(out, nil, "Rust release health query returned no response"), fmt.Errorf("release.attestation returned an empty response") } out.Attestation = attestation out.Distribution = rustAttestationHealth(attestation) if !attestation.Available || !attestation.Ready || attestation.Channel != "official" || attestation.IntegrityStatus != "verified" || attestation.VerificationGeneration == 0 || !attestationIsFresh(attestation) { return emptyRPCResult( out, nil, "Rust current official distribution attestation is unavailable or not ready", ), nil } // Catalog / resource discovery var snapshot *SnapshotSummary resourceRoot := attestation.ResourceRoot catalogAvailabilityKnown := false catalogAvailable := false if raw, err := backend.CatalogStatus(ctx); err != nil { out.Warnings = append(out.Warnings, fmt.Sprintf("catalog.status: %v", err)) } else { catalogAvailable, catalogAvailabilityKnown = parseCatalogAvailability(raw) snap, root, ok := parseCatalogStatus(raw) if ok { snapshot = snap resourceRoot = root } } if snapshot != nil && snapshot.VersionID != "" && attestation.ReleaseID != "" && snapshot.VersionID != attestation.ReleaseID { return emptyRPCResult( out, snapshot, fmt.Sprintf( "release.attestation current ID %q does not match catalog current ID %q", attestation.ReleaseID, snapshot.VersionID, ), ), nil } if resourceRoot != "" && attestation.ResourceRoot != "" && resourceRoot != attestation.ResourceRoot { return emptyRPCResult( out, snapshot, fmt.Sprintf( "catalog current root %q does not match attestation root %q", resourceRoot, attestation.ResourceRoot, ), ), nil } if catalogAvailabilityKnown && !catalogAvailable { return emptyRPCResult(out, snapshot, "catalog.status available=false; no published release"), nil } if resourceRoot == "" { if state, err := backend.ResourceState(ctx); err != nil { out.Warnings = append(out.Warnings, fmt.Sprintf("resource.state: %v", err)) } else if state.ResourceOutputRoot != nil && *state.ResourceOutputRoot != "" { // Prefer published current under output root when catalog root missing. candidate := filepath.Join(*state.ResourceOutputRoot, "current") resourceRoot = candidate } } if resourceRootOverride != "" { resourceRoot = resourceRootOverride } if resourceRoot == "" { out.Snapshot = snapshotWithDistributionHealth(snapshot, out.Distribution) out.Index = &ReleaseIndex{ Source: "rpc", RPCAvailable: true, DoctorHealthy: out.DoctorHealthy, Distribution: out.Distribution, Snapshot: snapshotWithDistributionHealth(snapshot, out.Distribution), byRel: map[string]int{}, } out.Warnings = append(out.Warnings, "no resource root from RPC; set --resource-root or publish a version") return out, nil } if resourceRootOverride == "" { info, statErr := os.Stat(resourceRoot) if statErr != nil || !info.IsDir() { warning := fmt.Sprintf("published resource root is unavailable: %s", resourceRoot) if statErr != nil { warning = fmt.Sprintf("%s (%v)", warning, statErr) } else { warning = fmt.Sprintf("%s (not a directory)", warning) } return emptyRPCResult(out, snapshot, warning), nil } } entries, manifestVersion, rootFromManifest, err := fetchAllManifestEntries(ctx, backend, attestation) if err != nil { out.Warnings = append(out.Warnings, fmt.Sprintf("resource.manifest: %v", err)) // Without the RPC manifest there is no evidence that the local // snapshot matches the Rust health fact. Do not pair a fresh health // result with a potentially stale on-disk manifest. return emptyRPCResult( out, snapshot, fmt.Sprintf("published release cannot be indexed: %v", err), ), nil } if rootFromManifest != "" { if attestation.ResourceRoot != "" && rootFromManifest != attestation.ResourceRoot { return emptyRPCResult( out, snapshot, fmt.Sprintf( "resource.manifest resource root %q does not match attestation root %q", rootFromManifest, attestation.ResourceRoot, ), ), nil } resourceRoot = rootFromManifest } if resourceRootOverride != "" { resourceRoot = resourceRootOverride } idx, err := BuildIndexFromManifestEntries( resourceRoot, "rpc", true, out.DoctorHealthy, snapshot, manifestVersion, entries, out.Distribution, ) if err != nil { if resourceRootOverride == "" { return emptyRPCResult( out, snapshot, fmt.Sprintf("published release failed local validation: %v", err), ), nil } return out, err } out.ResourceRoot = idx.ResourceRoot out.Snapshot = idx.Snapshot out.Index = idx return out, nil } // AttestationBackend exposes the lightweight current official health proof. // It is intentionally separate from ReleaseStatusBackend because health // refreshes must not require the historical release scan. type AttestationBackend interface { ReleaseAttestation(ctx context.Context) (*backendrpc.DistributionAttestation, error) } func emptyRPCResult(out *DiscoverResult, snapshot *SnapshotSummary, warning string) *DiscoverResult { distribution := out.Distribution if distribution.Source == "" { distribution = unavailableRustDistributionHealth() out.Distribution = distribution } snapshot = snapshotWithDistributionHealth(snapshot, distribution) out.ResourceRoot = "" out.Snapshot = snapshot out.Index = &ReleaseIndex{ Source: "rpc", RPCAvailable: true, DoctorHealthy: out.DoctorHealthy, Distribution: distribution, Snapshot: snapshot, byRel: map[string]int{}, } if warning != "" { out.Warnings = append(out.Warnings, warning) } return out } func rustAttestationHealth(report *backendrpc.DistributionAttestation) DistributionHealth { health := DistributionHealth{ Source: "rust_release_attestation", Status: "unavailable", StatusCode: "distribution.attestation_unavailable", IntegrityStatus: "unavailable", } if report == nil { return health } health.Available = report.Available health.Ready = report.Ready health.Channel = report.Channel health.ReleaseID = report.ReleaseID health.ResourceRoot = report.ResourceRoot health.PublicationIdentity = report.PublicationIdentity health.MappingIdentity = report.MappingIdentity health.ManifestIdentity = report.ManifestIdentity health.EntryCount = report.EntryCount health.VerificationGeneration = report.VerificationGeneration health.VerifiedAt = report.VerifiedAt health.MaxAgeSeconds = report.MaxAgeSeconds health.Status = report.Status health.StatusCode = report.StatusCode health.IntegrityStatus = report.IntegrityStatus health.Diagnostics = append([]string(nil), report.Diagnostics...) if health.Status == "" { health.Status = "unavailable" } if health.StatusCode == "" { health.StatusCode = "distribution.attestation_unavailable" } if health.IntegrityStatus == "" { health.IntegrityStatus = "unavailable" } return health } func unavailableRustDistributionHealth() DistributionHealth { return DistributionHealth{ Source: "rust_release_attestation", Status: "unavailable", StatusCode: "distribution.attestation_unavailable", IntegrityStatus: "unavailable", } } func attestationIsFresh(attestation *backendrpc.DistributionAttestation) bool { if attestation == nil || attestation.MaxAgeSeconds == 0 || attestation.VerifiedAt == nil { return false } now := uint64(time.Now().Unix()) age := uint64(0) if now > *attestation.VerifiedAt { age = now - *attestation.VerifiedAt } return age <= attestation.MaxAgeSeconds } func snapshotWithDistributionHealth(snapshot *SnapshotSummary, health DistributionHealth) *SnapshotSummary { if snapshot == nil { return nil } updated := *snapshot if health.Status != "" { updated.DistributionStatus = health.Status } if health.StatusCode != "" { updated.DistributionStatusCode = health.StatusCode } return &updated } func parseCatalogStatus(raw json.RawMessage) (*SnapshotSummary, string, bool) { if len(raw) == 0 || string(raw) == "null" { return nil, "", false } var payload struct { Available bool `json:"available"` Status string `json:"status"` StatusCode string `json:"status_code"` DistributionStatus string `json:"distribution_status"` DistributionStatusCode string `json:"distribution_status_code"` AppVersion string `json:"app_version"` BundleVersion string `json:"bundle_version"` ConnectionGroupName string `json:"connection_group_name"` AddressablesRoot string `json:"addressables_root"` LauncherMetadata *LauncherMetadataSummary `json:"launcher_metadata"` GameMainConfigBootstrap *GameMainConfigSummary `json:"game_main_config_bootstrap"` Version *struct { ID string `json:"id"` CompletedUnixSeconds *uint64 `json:"completed_unix_seconds"` ResourceRoot string `json:"resource_root"` } `json:"version"` } if err := json.Unmarshal(raw, &payload); err != nil || !payload.Available { return nil, "", false } snap := &SnapshotSummary{ Status: payload.Status, StatusCode: payload.StatusCode, DistributionStatus: payload.DistributionStatus, DistributionStatusCode: payload.DistributionStatusCode, AppVersion: payload.AppVersion, BundleVersion: payload.BundleVersion, ConnectionGroupName: payload.ConnectionGroupName, AddressablesRoot: payload.AddressablesRoot, LauncherMetadata: payload.LauncherMetadata, GameMainConfig: payload.GameMainConfigBootstrap, } root := "" if payload.Version != nil { snap.VersionID = payload.Version.ID snap.CompletedUnixSeconds = payload.Version.CompletedUnixSeconds root = payload.Version.ResourceRoot } return snap, root, true } func parseCatalogAvailability(raw json.RawMessage) (bool, bool) { if len(raw) == 0 || string(raw) == "null" { return false, false } var payload struct { Available *bool `json:"available"` } if err := json.Unmarshal(raw, &payload); err != nil || payload.Available == nil { return false, false } return *payload.Available, true } func fetchAllManifestEntries( ctx context.Context, backend Backend, attestation *backendrpc.DistributionAttestation, ) ([]manifestEntry, int, string, error) { return fetchAllManifestEntriesWithPageSize(ctx, backend, attestation, 500) } func fetchAllManifestEntriesWithPageSize( ctx context.Context, backend Backend, attestation *backendrpc.DistributionAttestation, pageSize int, ) ([]manifestEntry, int, string, error) { if pageSize <= 0 { return nil, 0, "", fmt.Errorf("manifest page size must be positive") } if attestation == nil { return nil, 0, "", fmt.Errorf("resource.manifest requires a Rust attestation") } if !attestation.Available || !attestation.Ready || attestation.Channel != "official" || attestation.IntegrityStatus != "verified" || attestation.VerificationGeneration == 0 || !attestationIsFresh(attestation) { return nil, 0, "", fmt.Errorf("resource.manifest attestation is not ready") } offset := 0 var all []manifestEntry var version, limit int var channel, root, releaseID, publicationIdentity, mappingIdentity, manifestIdentity string var generation uint64 total := -1 for { page, err := backend.ResourceManifest(ctx, backendrpc.ResourceManifestParams{ ReleaseID: attestation.ReleaseID, ExpectedPublicationIdentity: attestation.PublicationIdentity, ExpectedManifestIdentity: attestation.ManifestIdentity, ExpectedVerificationGeneration: attestation.VerificationGeneration, Offset: offset, Limit: pageSize, }) if err != nil { return nil, 0, "", err } if page == nil { return nil, 0, "", fmt.Errorf("resource.manifest returned nil page") } if !page.Available { return nil, 0, "", fmt.Errorf("resource.manifest available=false") } if root == "" { root = page.ResourceRoot channel = page.Channel releaseID = page.ReleaseID publicationIdentity = page.PublicationIdentity mappingIdentity = page.MappingIdentity manifestIdentity = page.ManifestIdentity generation = page.Generation version = page.ManifestVersion total = page.TotalEntries limit = page.Limit } else if page.ResourceRoot != root || page.Channel != channel || page.ReleaseID != releaseID || page.PublicationIdentity != publicationIdentity || page.MappingIdentity != mappingIdentity || page.ManifestIdentity != manifestIdentity || page.Generation != generation || page.ManifestVersion != version || page.TotalEntries != total || page.Limit != limit { return nil, 0, "", fmt.Errorf("resource.manifest page identity or total changed") } if page.Offset != offset { return nil, 0, "", fmt.Errorf( "resource.manifest page offset mismatch: requested=%d actual=%d", offset, page.Offset, ) } if page.Limit != pageSize { return nil, 0, "", fmt.Errorf("resource.manifest page limit is unreasonable: %d", page.Limit) } if page.TotalEntries < 0 || len(page.Entries) > page.Limit { return nil, 0, "", fmt.Errorf("resource.manifest page entry count is unreasonable") } if total < 0 || offset > total || offset+len(page.Entries) > total { return nil, 0, "", fmt.Errorf("resource.manifest page exceeds declared total") } if channel != attestation.Channel || releaseID != attestation.ReleaseID || root != attestation.ResourceRoot || publicationIdentity != attestation.PublicationIdentity || mappingIdentity != attestation.MappingIdentity || manifestIdentity != attestation.ManifestIdentity || generation != attestation.VerificationGeneration || total != attestation.EntryCount { return nil, 0, "", fmt.Errorf("resource.manifest page does not match attestation") } for _, e := range page.Entries { var bytes uint64 if e.Bytes != nil { bytes = *e.Bytes } all = append(all, manifestEntry{ URL: e.URL, Destination: e.Destination, Bytes: bytes, BLAKE3: e.BLAKE3, }) } offset += len(page.Entries) if offset == total { break } if len(page.Entries) == 0 || len(page.Entries) < page.Limit { return nil, 0, "", fmt.Errorf("resource.manifest page has a gap before total") } } if len(all) != total { return nil, 0, "", fmt.Errorf( "resource.manifest final entry count mismatch: entries=%d total=%d", len(all), total, ) } return all, version, root, nil }