From 5fde48024e2a663f519051a279e1a95e4c538822 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:26:26 -0400 Subject: [PATCH 01/11] The platform Catalogue lists every module and solution, and says which facts this host cannot state (Refs #1020) A super-admin page, /admin/platform/catalogue, backed by one new read on the existing PlatformAdminService: ListPlatformCatalogue. It is a projection over records the host already keeps - the solution registry, the installations table and the composition's module principals - and adds no store. Each entry shows its identity, registration status, publisher, and every active installation with its organization, agent release and the teams a grant at its authority root reaches. Declared release, build digest, generation, running incarnation, installation revision and build size have no record on this host yet; each is a oneof of its value and a CatalogueGap with the reason, so the page renders "Not recorded" / "Not observed" and never an empty cell, a zero or a false match. Build size slots into its oneof when the presence document carries it (codefly-dev/core#708, codefly-dev/cli#901); the host counts nothing itself. The registration it returns withholds the manifest, upstream and service alias, as every browser-facing projection of the registry does. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../api/accounts/authority/contract.binpb | Bin 940470 -> 951088 bytes .../saas/accounts/v1/platform_admin.proto | 175 ++ .../api/accounts/connect/contract.binpb | Bin 940470 -> 951088 bytes .../saas/accounts/v1/platform_admin.proto | 175 ++ module/contracts/api/catalog.codefly.json | 6 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 37 + module/services/accounts/AUTHZ_MATRIX.md | 5 +- .../platform_admin_authorization_test.go | 6 +- .../pkg/adapters/platform_catalogue_rpcs.go | 66 + .../adapters/platform_catalogue_rpcs_test.go | 93 + .../adapters/rest_registration_catalog_gen.go | 1 + .../code/pkg/business/installations.go | 4 + .../code/pkg/business/introspection.go | 1 + .../code/pkg/business/platform_catalogue.go | 210 ++ .../pkg/business/platform_catalogue_test.go | 191 ++ .../code/pkg/business/service_catalog_test.go | 2 +- .../pkg/cataloggen/frontend_plugins_test.go | 3 +- .../platform_admin.connect.go | 33 + .../gen/saas/accounts/v1/platform_admin.pb.go | 1696 ++++++++++++++--- .../saas/accounts/v1/platform_admin.pb.gw.go | 74 + .../accounts/v1/platform_admin_grpc.pb.go | 42 + .../postgres_catalogue_installations_test.go | 100 + .../code/pkg/infra/postgres_installations.go | 77 + .../code/pkg/infra/postgres_layered_access.go | 63 +- .../testdata/control_plane_call_sites.txt | 1 + .../accounts/generated/authz-methods.json | 37 + .../accounts/generated/gateway-routes.json | 48 + .../generated/openapi-raw/api.swagger.json | 318 ++++ .../accounts/generated/rest-surface.json | 16 + .../accounts/generated/service-catalog.json | 52 + .../accounts/openapi/api.swagger.json | 318 ++++ .../saas/accounts/v1/platform_admin.proto | 175 ++ .../auth-gateway/code/authz_catalog_gen.go | 2 + .../auth-gateway/code/routing_catalog_gen.go | 2 + .../code/routing_rest_catalog_gen.go | 1 + module/services/frontend/AGENTS.md | 7 + .../generated/contract/catalog.codefly.json | 3 +- .../generated/contract/contract.binpb | Bin 940470 -> 951088 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 498 ++++- .../src/app/admin/platform/catalogue/page.tsx | 24 + .../code/src/app/admin/platform/page.tsx | 8 + .../src/features/platform-catalogue/index.ts | 2 + .../platform-catalogue/model/facts.test.ts | 195 ++ .../platform-catalogue/model/facts.ts | 231 +++ .../platform-catalogue/service/queries.ts | 12 + ...platform-catalogue-page.container.test.tsx | 189 ++ .../ui/platform-catalogue-page.tsx | 336 ++++ .../gen/saas/accounts/v1/platform_admin_pb.ts | 498 ++++- .../gen/saas/frontend/v1/plugin_catalog.ts | 2 + .../services/frontend/code/ui-inventory.json | 33 + .../frontend/frontend.bindings.codefly.yaml | 12 + .../frontend/generated/plugin-catalog.json | 21 + 54 files changed, 5775 insertions(+), 332 deletions(-) create mode 100644 module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go create mode 100644 module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs_test.go create mode 100644 module/services/accounts/code/pkg/business/platform_catalogue.go create mode 100644 module/services/accounts/code/pkg/business/platform_catalogue_test.go create mode 100644 module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go create mode 100644 module/services/frontend/code/src/app/admin/platform/catalogue/page.tsx create mode 100644 module/services/frontend/code/src/features/platform-catalogue/index.ts create mode 100644 module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts create mode 100644 module/services/frontend/code/src/features/platform-catalogue/model/facts.ts create mode 100644 module/services/frontend/code/src/features/platform-catalogue/service/queries.ts create mode 100644 module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx create mode 100644 module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index d7db149dd1f5da7900a177c0b6a8b4d3e65eabe0..e1cd68f066d2eb1eee8e8b3f660cbc6817ce6b1a 100644 GIT binary patch delta 23468 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 Q)8VgXcOU+0zcV-dA9Dm)pa1{> delta 13547 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

Kg?PZYG+-197@zLd6BwTf7%?SU zPUKf`y`#9C;-j*xco`zA<Z-xUvg61ozTK@HeB`~g_ z;Xia36JUIfXpP5+#(2%yi`Qfn#Y5CT1&U&!@k&oI<=-dJ6&P0qjAAjy=L1IZfboR@ zLo8r?F)T*0K=IFkqFA8#Qm>AnxElQZZ3a`KwPCx(dImGLvhCJd$Z9QbxAxYJe}Y+0 z`t6J&{~OGDe%bA=+JM&Uscxv3ptzonJk3#Tpd^i6Cfbr=Y_J$#rg%$+Q9Oj*5HN~` z(64xmIV~0_zDm(S?n9_pvZ-z){2zr~q1Hs-O|q%lFQCN)+t&j%vB36?z*a1<-Q24c z*uELqN-MDaE3Z&{cwnB}yop5}!EVVyNrv z4vPP}*+KdcB7^JF2$c@ zw8aDM_X2IP5P4^yEf#3Mzt0XT9xvJt!lD%qVE+;Lhy}182C!lQ>_-8tSOB}LS4V*T zIK29p5v89JjdMF_qg%9}Qhb7gYAs~-DRq9wEn57e&2G}&dB215zuN4kNPa@cyWWJ> zyQ$xaXwAR4HJK#8n@*Xe9L6SK+(UFl-d?rv1~*xbdnmpl@9$dTA@1jaqgaUjMc^nF zIPMMazF3U$%YaclVEii35DOUpi6h?sswf^fejPZ91&&?$?2;xHI8MrE?>?qPlk@aF zhq1|FoSctObQrCLtS09>zxyB2`4cBxIN|Wp@`0s}o+;bQgFAYDR9}95=#+VMx10x=HcK`qY diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index 9a4c2a755..8c79d0188 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -6,7 +6,10 @@ import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; +import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; +import "saas/accounts/v1/installations.proto"; +import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; import "saas/policy/v1/options.proto"; @@ -236,6 +239,161 @@ message UpsertFeatureFlagResponse { string name = 1; } +// ============================================================================ +// Platform Catalogue (issue #1020) +// +// The operator's read of what is deployed: every composed module and every +// solution, beside what it declares, what is running, and which organizations +// have it installed. It is a projection over records this host already keeps — +// the durable solution registry, the installations table, and the composition's +// module principal registry — and it adds no store of its own. +// +// Several facts the Catalogue is specified to show are carried by records this +// host does not hold yet: the declared release, build digest and generation +// arrive with applied presence documents, the running incarnation needs observed +// cluster state, and build size is a signed section of the presence document +// (codefly-dev/core#708, computed by the CLI at build). Each such fact is a +// oneof of its value and a CatalogueGap, so an absent fact is reported with its +// reason and is never an empty value a reader could take for zero, unset, or a +// match. A fact gains its value case when the record that carries it exists. +// ============================================================================ + +// CatalogueEntryKind distinguishes a composed module from a solution. +enum CatalogueEntryKind { + CATALOGUE_ENTRY_KIND_UNSPECIFIED = 0; + // A module composed into this deployment, known by its module principal. + CATALOGUE_ENTRY_KIND_MODULE = 1; + // A solution, known by its registration, its installations, or both. + CATALOGUE_ENTRY_KIND_SOLUTION = 2; +} + +// CatalogueGapReason says why a Catalogue fact has no value. +enum CatalogueGapReason { + CATALOGUE_GAP_REASON_UNSPECIFIED = 0; + // This host keeps no record that carries the fact. + CATALOGUE_GAP_REASON_NOT_RECORDED = 1; + // The fact is a section of the producer's document, and the document this + // host holds carries no such section (an older producer). + CATALOGUE_GAP_REASON_NOT_REPORTED = 2; + // The fact needs observed cluster state, and this host has none to read — + // a local run with no cluster, or an observation that is missing or stale. + CATALOGUE_GAP_REASON_NOT_OBSERVED = 3; +} + +// CatalogueGap is a Catalogue fact this host cannot state, with the reason. +message CatalogueGap { + CatalogueGapReason reason = 1; + // Operator-facing: names the record or producer that would supply the fact. + string detail = 2; +} + +// CatalogueRelease is a declared release, publisher/name@version. +message CatalogueRelease { + string publisher = 1; + string name = 2; + string version = 3; +} + +// CatalogueExecution is what runs: an OCI image manifest digest and the build +// incarnation the host assigned to it, the same pair a Work Context seals. +message CatalogueExecution { + string image_digest = 1; + uint64 build_incarnation = 2; +} + +// CatalogueRunningVerdict compares the observed execution with the declared +// one. It exists only when both are known; anything less is a CatalogueGap. +enum CatalogueRunningVerdict { + CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; + // The observed image digest and incarnation are the declared ones. + CATALOGUE_RUNNING_VERDICT_MATCHES = 1; + // Something other than the declared execution is running — what cluster + // admission refuses. + CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; +} + +// CatalogueRunning is the running incarnation beside the declared one. +message CatalogueRunning { + CatalogueExecution declared = 1; + CatalogueExecution observed = 2; + CatalogueRunningVerdict verdict = 3; +} + +// CatalogueInstallation is one organization's installation of a solution. +message CatalogueInstallation { + Installation installation = 1; + string org_name = 2; + // The release the installation's agent principal was created for, read from + // its canonical publisher/name:version identifier. + oneof agent_release_value { + CatalogueRelease agent_release = 3; + CatalogueGap agent_release_gap = 4; + } + // The installation revision the installed generation is pinned at. + oneof revision_value { + CatalogueGap revision_gap = 5; + } + // Active team grants at the installation's authority root or above it, so + // every team whose members the grant reaches inside this installation. + repeated CollectionReadGrant exposed_teams = 6; +} + +// CatalogueEntry is one module or solution. +message CatalogueEntry { + CatalogueEntryKind kind = 1; + // A module's principal prefix, or a solution's registry id — the identifier + // its installations name. + string name = 2; + // The owner of record: a solution registration's publisher. + oneof publisher_value { + string publisher = 3; + CatalogueGap publisher_gap = 4; + } + oneof declared_release_value { + CatalogueRelease declared_release = 5; + CatalogueGap declared_release_gap = 6; + } + // The OCI image manifest digest the entry was published at. + oneof build_digest_value { + string build_digest = 7; + CatalogueGap build_digest_gap = 8; + } + // The presence generation this host applied. + oneof generation_value { + int64 generation = 9; + CatalogueGap generation_gap = 10; + } + oneof running_value { + CatalogueRunning running = 11; + CatalogueGap running_gap = 12; + } + // Lines of code, backend and frontend, vendored paths excluded. The value + // case is the presence document's build_size section (codefly-dev/core#708) + // as delivered; this host counts nothing itself. + oneof build_size_value { + CatalogueGap build_size_gap = 13; + } + // The solution's registration, with deployment topology withheld: the + // frontend manifest, backend upstream and service alias are always empty + // here, as on every browser-facing projection of the registry. Absent for a + // module, which does not self-register, and for a solution known only from + // its installations. + optional SolutionRegistration registration = 14; + repeated CatalogueInstallation installations = 15; +} + +message ListPlatformCatalogueRequest { + // Include deregistered solutions. Their installations are listed either way. + bool include_tombstoned = 1; +} + +message ListPlatformCatalogueResponse { + // Modules first, then solutions, each ordered by name. + repeated CatalogueEntry entries = 1; + // The registry revision the solution entries were read at. + int64 registry_revision = 2; +} + // PlatformAdminService — cross-tenant operations for platform operators. // All RPCs require a platform_role in the JWT claims. service PlatformAdminService { @@ -643,4 +801,21 @@ service PlatformAdminService { }; option (google.api.http) = {get: "/v1/platform/events/subscriptions"}; } + + // ListPlatformCatalogue lists every composed module and solution with what + // it declares, what runs, and which organizations have it installed. + rpc ListPlatformCatalogue(ListPlatformCatalogueRequest) returns (ListPlatformCatalogueResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_SENSITIVE + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN + }; + option (google.api.http) = {get: "/v1/platform/catalogue"}; + } } diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index d7db149dd1f5da7900a177c0b6a8b4d3e65eabe0..e1cd68f066d2eb1eee8e8b3f660cbc6817ce6b1a 100644 GIT binary patch delta 23468 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 Q)8VgXcOU+0zcV-dA9Dm)pa1{> delta 13547 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

Kg?PZYG+-197@zLd6BwTf7%?SU zPUKf`y`#9C;-j*xco`zA<Z-xUvg61ozTK@HeB`~g_ z;Xia36JUIfXpP5+#(2%yi`Qfn#Y5CT1&U&!@k&oI<=-dJ6&P0qjAAjy=L1IZfboR@ zLo8r?F)T*0K=IFkqFA8#Qm>AnxElQZZ3a`KwPCx(dImGLvhCJd$Z9QbxAxYJe}Y+0 z`t6J&{~OGDe%bA=+JM&Uscxv3ptzonJk3#Tpd^i6Cfbr=Y_J$#rg%$+Q9Oj*5HN~` z(64xmIV~0_zDm(S?n9_pvZ-z){2zr~q1Hs-O|q%lFQCN)+t&j%vB36?z*a1<-Q24c z*uELqN-MDaE3Z&{cwnB}yop5}!EVVyNrv z4vPP}*+KdcB7^JF2$c@ zw8aDM_X2IP5P4^yEf#3Mzt0XT9xvJt!lD%qVE+;Lhy}182C!lQ>_-8tSOB}LS4V*T zIK29p5v89JjdMF_qg%9}Qhb7gYAs~-DRq9wEn57e&2G}&dB215zuN4kNPa@cyWWJ> zyQ$xaXwAR4HJK#8n@*Xe9L6SK+(UFl-d?rv1~*xbdnmpl@9$dTA@1jaqgaUjMc^nF zIPMMazF3U$%YaclVEii35DOUpi6h?sswf^fejPZ91&&?$?2;xHI8MrE?>?qPlk@aF zhq1|FoSctObQrCLtS09>zxyB2`4cBxIN|Wp@`0s}o+;bQgFAYDR9}95=#+VMx10x=HcK`qY diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index 9a4c2a755..8c79d0188 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -6,7 +6,10 @@ import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; +import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; +import "saas/accounts/v1/installations.proto"; +import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; import "saas/policy/v1/options.proto"; @@ -236,6 +239,161 @@ message UpsertFeatureFlagResponse { string name = 1; } +// ============================================================================ +// Platform Catalogue (issue #1020) +// +// The operator's read of what is deployed: every composed module and every +// solution, beside what it declares, what is running, and which organizations +// have it installed. It is a projection over records this host already keeps — +// the durable solution registry, the installations table, and the composition's +// module principal registry — and it adds no store of its own. +// +// Several facts the Catalogue is specified to show are carried by records this +// host does not hold yet: the declared release, build digest and generation +// arrive with applied presence documents, the running incarnation needs observed +// cluster state, and build size is a signed section of the presence document +// (codefly-dev/core#708, computed by the CLI at build). Each such fact is a +// oneof of its value and a CatalogueGap, so an absent fact is reported with its +// reason and is never an empty value a reader could take for zero, unset, or a +// match. A fact gains its value case when the record that carries it exists. +// ============================================================================ + +// CatalogueEntryKind distinguishes a composed module from a solution. +enum CatalogueEntryKind { + CATALOGUE_ENTRY_KIND_UNSPECIFIED = 0; + // A module composed into this deployment, known by its module principal. + CATALOGUE_ENTRY_KIND_MODULE = 1; + // A solution, known by its registration, its installations, or both. + CATALOGUE_ENTRY_KIND_SOLUTION = 2; +} + +// CatalogueGapReason says why a Catalogue fact has no value. +enum CatalogueGapReason { + CATALOGUE_GAP_REASON_UNSPECIFIED = 0; + // This host keeps no record that carries the fact. + CATALOGUE_GAP_REASON_NOT_RECORDED = 1; + // The fact is a section of the producer's document, and the document this + // host holds carries no such section (an older producer). + CATALOGUE_GAP_REASON_NOT_REPORTED = 2; + // The fact needs observed cluster state, and this host has none to read — + // a local run with no cluster, or an observation that is missing or stale. + CATALOGUE_GAP_REASON_NOT_OBSERVED = 3; +} + +// CatalogueGap is a Catalogue fact this host cannot state, with the reason. +message CatalogueGap { + CatalogueGapReason reason = 1; + // Operator-facing: names the record or producer that would supply the fact. + string detail = 2; +} + +// CatalogueRelease is a declared release, publisher/name@version. +message CatalogueRelease { + string publisher = 1; + string name = 2; + string version = 3; +} + +// CatalogueExecution is what runs: an OCI image manifest digest and the build +// incarnation the host assigned to it, the same pair a Work Context seals. +message CatalogueExecution { + string image_digest = 1; + uint64 build_incarnation = 2; +} + +// CatalogueRunningVerdict compares the observed execution with the declared +// one. It exists only when both are known; anything less is a CatalogueGap. +enum CatalogueRunningVerdict { + CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; + // The observed image digest and incarnation are the declared ones. + CATALOGUE_RUNNING_VERDICT_MATCHES = 1; + // Something other than the declared execution is running — what cluster + // admission refuses. + CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; +} + +// CatalogueRunning is the running incarnation beside the declared one. +message CatalogueRunning { + CatalogueExecution declared = 1; + CatalogueExecution observed = 2; + CatalogueRunningVerdict verdict = 3; +} + +// CatalogueInstallation is one organization's installation of a solution. +message CatalogueInstallation { + Installation installation = 1; + string org_name = 2; + // The release the installation's agent principal was created for, read from + // its canonical publisher/name:version identifier. + oneof agent_release_value { + CatalogueRelease agent_release = 3; + CatalogueGap agent_release_gap = 4; + } + // The installation revision the installed generation is pinned at. + oneof revision_value { + CatalogueGap revision_gap = 5; + } + // Active team grants at the installation's authority root or above it, so + // every team whose members the grant reaches inside this installation. + repeated CollectionReadGrant exposed_teams = 6; +} + +// CatalogueEntry is one module or solution. +message CatalogueEntry { + CatalogueEntryKind kind = 1; + // A module's principal prefix, or a solution's registry id — the identifier + // its installations name. + string name = 2; + // The owner of record: a solution registration's publisher. + oneof publisher_value { + string publisher = 3; + CatalogueGap publisher_gap = 4; + } + oneof declared_release_value { + CatalogueRelease declared_release = 5; + CatalogueGap declared_release_gap = 6; + } + // The OCI image manifest digest the entry was published at. + oneof build_digest_value { + string build_digest = 7; + CatalogueGap build_digest_gap = 8; + } + // The presence generation this host applied. + oneof generation_value { + int64 generation = 9; + CatalogueGap generation_gap = 10; + } + oneof running_value { + CatalogueRunning running = 11; + CatalogueGap running_gap = 12; + } + // Lines of code, backend and frontend, vendored paths excluded. The value + // case is the presence document's build_size section (codefly-dev/core#708) + // as delivered; this host counts nothing itself. + oneof build_size_value { + CatalogueGap build_size_gap = 13; + } + // The solution's registration, with deployment topology withheld: the + // frontend manifest, backend upstream and service alias are always empty + // here, as on every browser-facing projection of the registry. Absent for a + // module, which does not self-register, and for a solution known only from + // its installations. + optional SolutionRegistration registration = 14; + repeated CatalogueInstallation installations = 15; +} + +message ListPlatformCatalogueRequest { + // Include deregistered solutions. Their installations are listed either way. + bool include_tombstoned = 1; +} + +message ListPlatformCatalogueResponse { + // Modules first, then solutions, each ordered by name. + repeated CatalogueEntry entries = 1; + // The registry revision the solution entries were read at. + int64 registry_revision = 2; +} + // PlatformAdminService — cross-tenant operations for platform operators. // All RPCs require a platform_role in the JWT claims. service PlatformAdminService { @@ -643,4 +801,21 @@ service PlatformAdminService { }; option (google.api.http) = {get: "/v1/platform/events/subscriptions"}; } + + // ListPlatformCatalogue lists every composed module and solution with what + // it declares, what runs, and which organizations have it installed. + rpc ListPlatformCatalogue(ListPlatformCatalogueRequest) returns (ListPlatformCatalogueResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_SENSITIVE + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN + }; + option (google.api.http) = {get: "/v1/platform/catalogue"}; + } } diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 81c8ba5c1..a9d5c8360 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:4b71d34bfccf54f2d734ad31023c08c7a00154c3067f69954924cbbb07337565", + "digest": "sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048", "services": [ { "name": "APIKeyService", @@ -329,6 +329,7 @@ "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags", "/saas.accounts.v1.PlatformAdminService/ListJobs", "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", + "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/ReplayJob", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", @@ -476,7 +477,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:4b71d34bfccf54f2d734ad31023c08c7a00154c3067f69954924cbbb07337565", + "digest": "sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048", "services": [ { "name": "APIKeyService", @@ -795,6 +796,7 @@ "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags", "/saas.accounts.v1.PlatformAdminService/ListJobs", "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", + "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/ReplayJob", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 87da3ec77..6ede72a63 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:4b71d34bfccf54f2d734ad31023c08c7a00154c3067f69954924cbbb07337565 + digest: sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:4b71d34bfccf54f2d734ad31023c08c7a00154c3067f69954924cbbb07337565 + digest: sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index c63715dc5..799adeb02 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -138,6 +138,43 @@ not to be a solution's. Otherwise mints are unchanged: a request with no verified solution must name its own `task_id`, exactly as the schema used to require. +## The platform Catalogue is a projection, not a registry + +`PlatformAdminService.ListPlatformCatalogue` (super administrators, page +`/admin/platform/catalogue`) lists every composed module and every solution +with what it declares, what runs and where it is installed. It **adds no store**: +modules come from the `MODULE_PRINCIPALS` registry (`Service.modulePrincipals`), +solutions from `solution_registrations` (read with tombstones, so an +installation of a deregistered solution sits beside its tombstone rather than +reading as a solution nobody registered), and installations from +`InstallationStore.ListCatalogueInstallations` — every organization's active +installations under the control plane, with the organization's name, the +`publisher/name:version` of the agent principal each was installed as, and the +team grants at its authority root or above (the same labelled-grant query +`ListCollectionAccess` reads). An installation names its solution by its free-text +`solution_identifier`, so that is the join; an identifier no registration carries +becomes a solution entry known only by its installations. + +**Absent facts are gaps, never values.** The declared release, build digest, +generation, running incarnation, installation revision and build size each have +a `oneof` of their value and a `CatalogueGap{reason, detail}`. Today no record on +this host carries the declared release, build digest, generation or installation +revision (they arrive with applied presence documents), nothing observes what +runs, and build size is the presence document's `build_size` section +(codefly-dev/core#708, computed by the CLI at build, codefly-dev/cli#901): the host +counts no lines itself. So every entry reports those as `NOT_RECORDED`, and +running as `NOT_OBSERVED`. A running verdict (`MATCHES` / `DIFFERS`) exists only +when both a declared and an observed `{image_digest, build_incarnation}` are +known (`setCatalogueRunning`); an unobserved deployment is never a match. When a +record starts carrying a fact, `newCatalogueEntry` reads it and the value case of +that `oneof` replaces the gap — build size gains its value field in the same +`oneof`. + +**The registration it returns withholds topology**: `catalogueRegistrationProto` +blanks the frontend manifest, backend upstream and service alias, as every other +browser-facing projection of the registry does, and the runtime-boundary seed is +withheld as on every response. + ## The composed-module service principal A module consuming the module-facing capability surface diff --git a/module/services/accounts/AUTHZ_MATRIX.md b/module/services/accounts/AUTHZ_MATRIX.md index 8bd89b02a..a2b19d4de 100644 --- a/module/services/accounts/AUTHZ_MATRIX.md +++ b/module/services/accounts/AUTHZ_MATRIX.md @@ -2,7 +2,7 @@ > Generated from protobuf service descriptors and `saas.policy.v1.method_policy`; only the prose description is joined from `pkg/business/introspection.go`. Do not edit by hand. Run `go generate ./pkg/business` from `module/services/accounts/code`. -Inventory: **245 RPCs** across **35 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. +Inventory: **246 RPCs** across **35 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. The compact tier is retained for compatibility. Guards, resource bindings, audit events, limiter class, and data sensitivity are descriptor-authoritative. Domain handlers may enforce stronger state-dependent rules but may not weaken this floor. @@ -183,6 +183,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/saas.accounts.v1.PlatformAdminService/ListFeatureFlags` | unary | `GET /v1/platform/feature-flags` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN | — | — | — | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | List the legacy feature-flag migration inventory. | | `/saas.accounts.v1.PlatformAdminService/ListJobs` | unary | `GET /v1/platform/jobs` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN | — | — | — | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Seek-paginated payload-free job operations view. | | `/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins` | unary | `GET /v1/platform/admins` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN | — | — | — | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | List the platform administrators and their granted roles. | +| `/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue` | unary | `GET /v1/platform/catalogue` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN | — | — | — | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | List every composed module and solution with its declared, running and installed state. | | `/saas.accounts.v1.PlatformAdminService/OverrideEntitlement` | unary | `POST /v1/platform/organizations/{org_id}/entitlements` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=ANY; impersonation=FORBIDDEN | — | — | SUCCESS: saas.entitlement.override | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Override one entitlement limit for a single organization. | | `/saas.accounts.v1.PlatformAdminService/ReplayJob` | unary | `POST /v1/platform/jobs/{source_job_id}:replay` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN; mfa=RECENT_STEP_UP | — | — | SUCCESS: saas.job.replayed | REQUIRED / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Idempotently copy dead-lettered work for another attempt. | | `/saas.accounts.v1.PlatformAdminService/RevokePlatformRole` | unary | `DELETE /v1/platform/admins/{user_id}` | `platform_admin` | exposure=AUTHENTICATED; tenant=NONE; platform=SUPER_ADMIN; mfa=IF_ENROLLED_RECENT_STEP_UP; impersonation=FORBIDDEN | — | — | SUCCESS: saas.platform.role_revoked | FORBIDDEN / SENSITIVE | CONFIDENTIAL → CONFIDENTIAL | Revoke a platform role. | @@ -261,5 +262,5 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit - `mfa`: 4 - `org_admin`: 56 - `org_member`: 49 -- `platform_admin`: 26 +- `platform_admin`: 27 - `public`: 18 diff --git a/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go b/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go index 7f8e556e7..eba43bc18 100644 --- a/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go +++ b/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go @@ -46,7 +46,7 @@ func (f *platformAdminAuthzStore) HasVerifiedMFA(context.Context, string) (bool, return true, nil } -// platformAdminHandler names one of the 14 PlatformAdminService methods and the +// platformAdminHandler names one PlatformAdminService method and the // minimum platform role its business layer enforces, so the handler-layer gate // can be checked against the same bar. type platformAdminHandler struct { @@ -113,6 +113,10 @@ func platformAdminHandlers() []platformAdminHandler { _, err := srv.ReplayJob(ctx, &jobsv1.ReplayJobRequest{SourceJobId: platformTargetD, IdempotencyKey: "replay-key"}) return err }}, + {"ListPlatformCatalogue", "super_admin", func(ctx context.Context, srv *PlatformAdminServer) error { + _, err := srv.ListPlatformCatalogue(ctx, &gen.ListPlatformCatalogueRequest{}) + return err + }}, } } diff --git a/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go b/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go new file mode 100644 index 000000000..3421eb998 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go @@ -0,0 +1,66 @@ +package adapters + +import ( + "context" + "errors" + + "connectrpc.com/connect" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// ListPlatformCatalogue is the platform Catalogue: every composed module and +// solution, what it declares, what runs, and where it is installed. +func (s *PlatformAdminServer) ListPlatformCatalogue(ctx context.Context, req *gen.ListPlatformCatalogueRequest) (*gen.ListPlatformCatalogueResponse, error) { + if err := Validate(req); err != nil { + return nil, err + } + actorID, err := requireAuth(ctx) + if err != nil { + return nil, err + } + if err := requirePlatformRole(ctx, actorID, "super_admin"); err != nil { + return nil, err + } + catalogue, err := service.ListPlatformCatalogue(ctx, actorID, req.GetIncludeTombstoned()) + if err != nil { + var storeErr *business.StoreError + if errors.As(err, &storeErr) && storeErr.StoreErrorType == business.ErrTypePermission { + return nil, status.Error(codes.PermissionDenied, "requires platform super_admin role") + } + return nil, status.Error(codes.Internal, "cannot read the platform catalogue") + } + response := &gen.ListPlatformCatalogueResponse{RegistryRevision: catalogue.RegistryRevision} + for _, row := range catalogue.Entries { + entry := row.Entry + if row.Registration != nil { + entry.Registration = catalogueRegistrationProto(row.Registration) + } + response.Entries = append(response.Entries, entry) + } + return response, nil +} + +// catalogueRegistrationProto is the registry's own projection with deployment +// topology withheld. The frontend manifest, backend upstream and service alias +// are served only on the cluster-internal projection, never to a browser, and a +// super administrator's browser is still a browser. Revisions, contract +// versions and leases stay: they are what the derived status is read from. +func catalogueRegistrationProto(record *business.SolutionRegistration) *gen.SolutionRegistration { + out := solutionRegistrationProto(record) + if out.Frontend != nil { + out.Frontend.Manifest = "" + } + if out.Backend != nil { + out.Backend.Upstream = "" + out.Backend.ServiceAlias = "" + } + return out +} + +func (h *platformAdminConnectHandler) ListPlatformCatalogue(ctx context.Context, req *connect.Request[gen.ListPlatformCatalogueRequest]) (*connect.Response[gen.ListPlatformCatalogueResponse], error) { + return unary(ctx, req, h.inner.ListPlatformCatalogue) +} diff --git a/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs_test.go b/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs_test.go new file mode 100644 index 000000000..cba1d8a5f --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs_test.go @@ -0,0 +1,93 @@ +package adapters + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "accounts/pkg/auth" + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// catalogueStore holds a registry record that DOES carry deployment topology +// and a runtime-boundary seed, so the assertions below are about what the +// Catalogue chooses to send. Every other method is an embedded nil interface +// and panics if reached. +type catalogueStore struct { + business.Store + business.InstallationStore + role string + registrations []*business.SolutionRegistration + installations []*business.CatalogueInstallationRecord +} + +func (s *catalogueStore) GetPlatformRole(context.Context, string) (string, error) { + return s.role, nil +} + +func (s *catalogueStore) WithControlPlane(ctx context.Context, fn func(context.Context) error) error { + return fn(ctx) +} + +func (s *catalogueStore) ListSolutionRegistrations(context.Context, bool) ([]*business.SolutionRegistration, int64, error) { + return s.registrations, 12, nil +} + +func (s *catalogueStore) ListCatalogueInstallations(context.Context) ([]*business.CatalogueInstallationRecord, error) { + return s.installations, nil +} + +// The Catalogue is a browser-facing projection of the registry, so it withholds +// the topology every other browser projection withholds — and the runtime +// boundary every response withholds — while keeping what the derived status is +// read from. +func TestListPlatformCatalogueWithholdsRegistryTopology(t *testing.T) { + lease := time.Now().UTC().Add(time.Minute) + store := &catalogueStore{ + role: "super_admin", + registrations: []*business.SolutionRegistration{{ + SolutionID: "example-solution", + Publisher: "solution:example-solution", + Revision: 12, + RuntimeBoundary: boundaryStoredSeed, + Frontend: &business.SolutionFrontendHalf{ + Revision: 11, Manifest: `{"id":"example-solution"}`, ContractVersion: "v1", LeaseExpiresAt: lease, + }, + Backend: &business.SolutionBackendHalf{ + Revision: 12, Upstream: "http://upstream.example:8080", ServiceAlias: "example-solution", + ContractVersion: "v1", LeaseExpiresAt: lease, + }, + UpdatedAt: time.Now().UTC(), + }}, + installations: []*business.CatalogueInstallationRecord{{ + Installation: &gen.Installation{Id: platformTargetD, SolutionIdentifier: "example-solution"}, + OrgName: "Acme", + }}, + } + installLayeredAuthzService(t, store) + + ctx := stampVerifiedIdentity(context.Background(), platformActorID, "", auth.Assurance{}) + out, err := (&PlatformAdminServer{}).ListPlatformCatalogue(ctx, &gen.ListPlatformCatalogueRequest{}) + require.NoError(t, err) + require.Equal(t, int64(12), out.GetRegistryRevision()) + require.Len(t, out.GetEntries(), 1) + + entry := out.GetEntries()[0] + require.Equal(t, gen.CatalogueEntryKind_CATALOGUE_ENTRY_KIND_SOLUTION, entry.GetKind()) + require.Equal(t, "solution:example-solution", entry.GetPublisher()) + require.Len(t, entry.GetInstallations(), 1) + require.Equal(t, "Acme", entry.GetInstallations()[0].GetOrgName()) + + registration := entry.GetRegistration() + require.Equal(t, gen.SolutionRegistrationStatus_SOLUTION_REGISTRATION_STATUS_ACTIVE, registration.GetStatus()) + require.Empty(t, registration.GetRuntimeBoundary()) + require.Empty(t, registration.GetFrontend().GetManifest()) + require.Empty(t, registration.GetBackend().GetUpstream()) + require.Empty(t, registration.GetBackend().GetServiceAlias()) + require.Equal(t, int64(11), registration.GetFrontend().GetRevision()) + require.Equal(t, "v1", registration.GetBackend().GetContractVersion()) + require.NotNil(t, registration.GetBackend().GetLeaseExpiresAt()) +} diff --git a/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go b/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go index 3f10a80c8..9fecbdf4a 100644 --- a/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go +++ b/module/services/accounts/code/pkg/adapters/rest_registration_catalog_gen.go @@ -131,6 +131,7 @@ var catalogRESTExactRoutes = map[string]struct{}{ "GET /v1/notifications/unread-count": {}, "GET /v1/organizations": {}, "GET /v1/platform/admins": {}, + "GET /v1/platform/catalogue": {}, "GET /v1/platform/events/operations": {}, "GET /v1/platform/events/subscriptions": {}, "GET /v1/platform/feature-flags": {}, diff --git a/module/services/accounts/code/pkg/business/installations.go b/module/services/accounts/code/pkg/business/installations.go index 6f6a69ab0..980c4e20e 100644 --- a/module/services/accounts/code/pkg/business/installations.go +++ b/module/services/accounts/code/pkg/business/installations.go @@ -54,6 +54,10 @@ type InstallationStore interface { // The bool reports whether this call actually flipped an active installation // to revoked, so the caller emits the audit event exactly once. UninstallSolution(ctx context.Context, orgID, installationID string) (*gen.Installation, bool, error) + // ListCatalogueInstallations reads every organization's active + // installations for the platform Catalogue (platform_catalogue.go). Unlike + // the methods above it runs under the control plane, not an org transaction. + ListCatalogueInstallations(ctx context.Context) ([]*CatalogueInstallationRecord, error) } func (s *Service) installationStore() InstallationStore { diff --git a/module/services/accounts/code/pkg/business/introspection.go b/module/services/accounts/code/pkg/business/introspection.go index 7e926d8e0..a5d826481 100644 --- a/module/services/accounts/code/pkg/business/introspection.go +++ b/module/services/accounts/code/pkg/business/introspection.go @@ -253,6 +253,7 @@ var rpcDescriptions = withWorkContextConsumerDescriptions(map[string]string{ "PlatformAdminService/SearchUsers": "Search across all users.", "PlatformAdminService/ListAllOrganizations": "List every organization on the platform, with member counts.", "PlatformAdminService/GetOrganizationRoster": "List any organization's members for the platform view.", + "PlatformAdminService/ListPlatformCatalogue": "List every composed module and solution with its declared, running and installed state.", "PlatformAdminService/StopImpersonation": "End the caller's own impersonation session.", "PlatformAdminService/SuspendUser": "Suspend a user account.", "PlatformAdminService/UnsuspendUser": "Restore a suspended user.", diff --git a/module/services/accounts/code/pkg/business/platform_catalogue.go b/module/services/accounts/code/pkg/business/platform_catalogue.go new file mode 100644 index 000000000..7a8793d66 --- /dev/null +++ b/module/services/accounts/code/pkg/business/platform_catalogue.go @@ -0,0 +1,210 @@ +package business + +import ( + "context" + "slices" + "strings" + + "github.com/codefly-dev/core/wool" + + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// The platform Catalogue (issue #1020) is a projection over records this host +// already keeps — the durable solution registry, the installations table and the +// composition's module principal registry. It adds no store of its own. +// +// Some facts the Catalogue is specified to show have no record on this host +// yet. Each is reported as a CatalogueGap naming why, never as an empty value: +// an unrecorded generation is not generation 0, an unreported build size is not +// zero lines, and an unobserved deployment is not one that matches. + +// CatalogueInstallationRecord is one active installation as the Catalogue reads +// it: the installation, its organization's name, the identifier of the agent +// principal it was installed as, and the team grants that reach inside it. +type CatalogueInstallationRecord struct { + Installation *gen.Installation + OrgName string + AgentIdentifier string + ExposedTeams []*gen.CollectionReadGrant +} + +// PlatformCatalogueEntry is one Catalogue row. Registration is kept as the +// business record so the transport projects it through the same mapping every +// other registry response uses; Entry carries everything else. +type PlatformCatalogueEntry struct { + Entry *gen.CatalogueEntry + Registration *SolutionRegistration +} + +// PlatformCatalogue is the whole Catalogue at one registry revision. +type PlatformCatalogue struct { + Entries []*PlatformCatalogueEntry + RegistryRevision int64 +} + +const ( + catalogueNoPresenceDetail = "This host holds no applied presence document for this entry, which is what records it." + catalogueNotObserved = "This host reads no observed cluster state, so what runs is unknown — never assumed to match." + catalogueNoBuildSize = "Build size is the presence document's build_size section (codefly-dev/core#708), computed at build by the CLI; this host holds no presence document for this entry." + catalogueModulePublisher = "A composed module is known by its principal prefix alone; the composition records no publisher." + catalogueUnregistered = "No registration exists under this identifier; it is named only by its installations." + catalogueNoRevision = "Installations are not pinned to a presence revision on this host." + catalogueNoAgentRelease = "The installation's agent principal carries no publisher/name:version identifier." +) + +// ListPlatformCatalogue reads the Catalogue for a super administrator. +func (s *Service) ListPlatformCatalogue(ctx context.Context, actorID string, includeTombstoned bool) (*PlatformCatalogue, error) { + w := wool.Get(ctx).In("ListPlatformCatalogue") + if err := s.requirePlatformRole(ctx, actorID, "super_admin"); err != nil { + return nil, NewStoreError(err, ErrTypePermission) + } + var ( + registrations []*SolutionRegistration + revision int64 + installations []*CatalogueInstallationRecord + ) + if err := s.store.WithControlPlane(ctx, func(ctx context.Context) error { + var err error + // Tombstones are always read: an installation of a deregistered solution + // is shown beside the tombstone, not as a solution nobody registered. + if registrations, revision, err = s.store.ListSolutionRegistrations(ctx, true); err != nil { + return err + } + installations, err = s.installationStore().ListCatalogueInstallations(ctx) + return err + }); err != nil { + return nil, w.Wrapf(err, "cannot read the platform catalogue") + } + return &PlatformCatalogue{ + Entries: projectPlatformCatalogue(s.modulePrincipals, registrations, installations, includeTombstoned), + RegistryRevision: revision, + }, nil +} + +// projectPlatformCatalogue builds the Catalogue's rows: one per composed module, +// then one per solution identifier that a registration or an active +// installation names, each ordered by name. A tombstoned registration is listed +// only when asked for, or when an installation still names it. +func projectPlatformCatalogue( + modules ModulePrincipalRegistry, + registrations []*SolutionRegistration, + installations []*CatalogueInstallationRecord, + includeTombstoned bool, +) []*PlatformCatalogueEntry { + var moduleEntries []*PlatformCatalogueEntry + for _, grant := range modules { + entry := newCatalogueEntry(gen.CatalogueEntryKind_CATALOGUE_ENTRY_KIND_MODULE, grant.Prefix) + entry.PublisherValue = &gen.CatalogueEntry_PublisherGap{PublisherGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueModulePublisher)} + moduleEntries = append(moduleEntries, &PlatformCatalogueEntry{Entry: entry}) + } + + solutions := make(map[string]*PlatformCatalogueEntry) + solution := func(name string) *PlatformCatalogueEntry { + if existing, ok := solutions[name]; ok { + return existing + } + entry := newCatalogueEntry(gen.CatalogueEntryKind_CATALOGUE_ENTRY_KIND_SOLUTION, name) + entry.PublisherValue = &gen.CatalogueEntry_PublisherGap{PublisherGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueUnregistered)} + solutions[name] = &PlatformCatalogueEntry{Entry: entry} + return solutions[name] + } + for _, installation := range installations { + row := solution(installation.Installation.GetSolutionIdentifier()) + row.Entry.Installations = append(row.Entry.Installations, catalogueInstallation(installation)) + } + for _, registration := range registrations { + _, installed := solutions[registration.SolutionID] + if registration.TombstonedAt != nil && !includeTombstoned && !installed { + continue + } + row := solution(registration.SolutionID) + row.Registration = registration + row.Entry.PublisherValue = &gen.CatalogueEntry_Publisher{Publisher: registration.Publisher} + } + + byName := func(a, b *PlatformCatalogueEntry) int { return strings.Compare(a.Entry.Name, b.Entry.Name) } + slices.SortFunc(moduleEntries, byName) + solutionEntries := make([]*PlatformCatalogueEntry, 0, len(solutions)) + for _, row := range solutions { + solutionEntries = append(solutionEntries, row) + } + slices.SortFunc(solutionEntries, byName) + return append(moduleEntries, solutionEntries...) +} + +// newCatalogueEntry is an entry whose presence-borne facts are all gaps: no +// record on this host carries a declared release, build digest, generation, +// observed execution or build size yet. When one does, the entry reads it here +// and the gap gives way to the value. +func newCatalogueEntry(kind gen.CatalogueEntryKind, name string) *gen.CatalogueEntry { + notRecorded := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED + entry := &gen.CatalogueEntry{ + Kind: kind, + Name: name, + DeclaredReleaseValue: &gen.CatalogueEntry_DeclaredReleaseGap{DeclaredReleaseGap: catalogueGap(notRecorded, catalogueNoPresenceDetail)}, + BuildDigestValue: &gen.CatalogueEntry_BuildDigestGap{BuildDigestGap: catalogueGap(notRecorded, catalogueNoPresenceDetail)}, + GenerationValue: &gen.CatalogueEntry_GenerationGap{GenerationGap: catalogueGap(notRecorded, catalogueNoPresenceDetail)}, + BuildSizeValue: &gen.CatalogueEntry_BuildSizeGap{BuildSizeGap: catalogueGap(notRecorded, catalogueNoBuildSize)}, + } + setCatalogueRunning(entry, nil, nil) + return entry +} + +// setCatalogueRunning states what runs beside what was declared. A verdict is +// given only when both are known: without an observation the answer is +// NOT_OBSERVED, and without a declaration there is nothing to compare an +// observation with. Neither is ever reported as a match. +func setCatalogueRunning(entry *gen.CatalogueEntry, declared, observed *gen.CatalogueExecution) { + switch { + case observed == nil: + entry.RunningValue = &gen.CatalogueEntry_RunningGap{RunningGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, catalogueNotObserved)} + case declared == nil: + entry.RunningValue = &gen.CatalogueEntry_RunningGap{RunningGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoPresenceDetail)} + default: + verdict := gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS + if declared.GetImageDigest() != "" && declared.GetImageDigest() == observed.GetImageDigest() && + declared.GetBuildIncarnation() == observed.GetBuildIncarnation() { + verdict = gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_MATCHES + } + entry.RunningValue = &gen.CatalogueEntry_Running{Running: &gen.CatalogueRunning{Declared: declared, Observed: observed, Verdict: verdict}} + } +} + +func catalogueInstallation(record *CatalogueInstallationRecord) *gen.CatalogueInstallation { + out := &gen.CatalogueInstallation{ + Installation: record.Installation, + OrgName: record.OrgName, + RevisionValue: &gen.CatalogueInstallation_RevisionGap{ + RevisionGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoRevision), + }, + ExposedTeams: record.ExposedTeams, + } + if release, ok := parseAgentRelease(record.AgentIdentifier); ok { + out.AgentReleaseValue = &gen.CatalogueInstallation_AgentRelease{AgentRelease: release} + } else { + out.AgentReleaseValue = &gen.CatalogueInstallation_AgentReleaseGap{ + AgentReleaseGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoAgentRelease), + } + } + return out +} + +// parseAgentRelease splits a canonical "publisher/name:version" agent +// identifier at the same separators looksLikeAgentIdentifier admits it by. +func parseAgentRelease(identifier string) (*gen.CatalogueRelease, bool) { + if !looksLikeAgentIdentifier(identifier) { + return nil, false + } + slash := strings.IndexByte(identifier, '/') + colon := strings.IndexByte(identifier, ':') + return &gen.CatalogueRelease{ + Publisher: identifier[:slash], + Name: identifier[slash+1 : colon], + Version: identifier[colon+1:], + }, true +} + +func catalogueGap(reason gen.CatalogueGapReason, detail string) *gen.CatalogueGap { + return &gen.CatalogueGap{Reason: reason, Detail: detail} +} diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_test.go new file mode 100644 index 000000000..247b85cbe --- /dev/null +++ b/module/services/accounts/code/pkg/business/platform_catalogue_test.go @@ -0,0 +1,191 @@ +package business + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + gen "accounts/pkg/gen/saas/accounts/v1" +) + +// The running column answers "is what runs what was declared?" Only two known +// executions get a verdict; an unobserved one is never reported as a match. +func TestCatalogueRunningVerdict(t *testing.T) { + declared := &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3} + cases := []struct { + name string + declared *gen.CatalogueExecution + observed *gen.CatalogueExecution + verdict gen.CatalogueRunningVerdict + gap gen.CatalogueGapReason + }{ + {name: "nothing observed", declared: declared, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, + {name: "nothing declared or observed", gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, + { + name: "observed without a declaration", + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, + gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, + }, + { + name: "same digest and incarnation", + declared: declared, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, + verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_MATCHES, + }, + { + name: "another image", + declared: declared, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:bbb", BuildIncarnation: 3}, + verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS, + }, + { + // The incarnation moves when command, configuration or identity + // change under the same image, so the image alone does not match. + name: "same image, another incarnation", + declared: declared, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 2}, + verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS, + }, + { + name: "two empty digests are not a match", + declared: &gen.CatalogueExecution{}, + observed: &gen.CatalogueExecution{}, + verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + entry := &gen.CatalogueEntry{} + setCatalogueRunning(entry, tc.declared, tc.observed) + if tc.gap != gen.CatalogueGapReason_CATALOGUE_GAP_REASON_UNSPECIFIED { + require.Nil(t, entry.GetRunning()) + require.Equal(t, tc.gap, entry.GetRunningGap().GetReason()) + require.NotEmpty(t, entry.GetRunningGap().GetDetail()) + return + } + require.Nil(t, entry.GetRunningGap()) + require.Equal(t, tc.verdict, entry.GetRunning().GetVerdict()) + require.Same(t, tc.declared, entry.GetRunning().GetDeclared()) + require.Same(t, tc.observed, entry.GetRunning().GetObserved()) + }) + } +} + +func catalogueTestInstallation(id, solution, org, agent string) *CatalogueInstallationRecord { + return &CatalogueInstallationRecord{ + Installation: &gen.Installation{ + Id: id, OrgId: org + "-id", SolutionIdentifier: solution, + Status: gen.InstallationStatus_INSTALLATION_STATUS_ACTIVE, + }, + OrgName: org, + AgentIdentifier: agent, + ExposedTeams: []*gen.CollectionReadGrant{{ + Grant: &gen.ScopeGrant{SubjectId: "team-" + id, SubjectKind: gen.SubjectKind_SUBJECT_KIND_TEAM}, + SubjectLabel: "Team " + id, + }}, + } +} + +func catalogueEntryNames(entries []*PlatformCatalogueEntry) []string { + names := make([]string, 0, len(entries)) + for _, entry := range entries { + names = append(names, entry.Entry.GetKind().String()+":"+entry.Entry.GetName()) + } + return names +} + +func TestProjectPlatformCatalogue(t *testing.T) { + now := time.Now() + modules := ModulePrincipalRegistry{ + ModulePrincipalID("zeta"): {Prefix: "zeta"}, + ModulePrincipalID("alpha"): {Prefix: "alpha"}, + } + registrations := []*SolutionRegistration{ + {SolutionID: "registered", Publisher: "solution:registered", Revision: 4}, + {SolutionID: "installed", Publisher: "solution:installed", Revision: 7}, + {SolutionID: "retired", Publisher: "solution:retired", Revision: 9, TombstonedAt: &now}, + {SolutionID: "retired-but-installed", Publisher: "solution:retired-but-installed", Revision: 11, TombstonedAt: &now}, + } + installations := []*CatalogueInstallationRecord{ + catalogueTestInstallation("i1", "installed", "Acme", "example/installed:1.2.0"), + catalogueTestInstallation("i2", "installed", "ExampleCorp", "example/installed:1.3.0"), + catalogueTestInstallation("i3", "unregistered", "Acme", "not-an-identifier"), + catalogueTestInstallation("i4", "retired-but-installed", "Acme", "example/retired:0.9.0"), + } + + entries := projectPlatformCatalogue(modules, registrations, installations, false) + require.Equal(t, []string{ + "CATALOGUE_ENTRY_KIND_MODULE:alpha", + "CATALOGUE_ENTRY_KIND_MODULE:zeta", + "CATALOGUE_ENTRY_KIND_SOLUTION:installed", + "CATALOGUE_ENTRY_KIND_SOLUTION:registered", + "CATALOGUE_ENTRY_KIND_SOLUTION:retired-but-installed", + "CATALOGUE_ENTRY_KIND_SOLUTION:unregistered", + }, catalogueEntryNames(entries), "modules first, then solutions, each by name; an uninstalled tombstone is omitted") + + byName := make(map[string]*PlatformCatalogueEntry) + for _, entry := range entries { + byName[entry.Entry.GetName()] = entry + } + + // Every entry reports the presence-borne facts as gaps, never as values. + for _, entry := range entries { + e := entry.Entry + notRecorded := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED + require.Equal(t, notRecorded, e.GetDeclaredReleaseGap().GetReason(), e.GetName()) + require.Equal(t, notRecorded, e.GetBuildDigestGap().GetReason(), e.GetName()) + require.Equal(t, notRecorded, e.GetGenerationGap().GetReason(), e.GetName()) + require.Equal(t, notRecorded, e.GetBuildSizeGap().GetReason(), e.GetName()) + require.Contains(t, e.GetBuildSizeGap().GetDetail(), "codefly-dev/core#708") + require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, e.GetRunningGap().GetReason(), e.GetName()) + require.Nil(t, e.GetRunning(), "%s: an unobserved entry has no verdict", e.GetName()) + } + + module := byName["alpha"] + require.Nil(t, module.Registration) + require.Empty(t, module.Entry.GetInstallations()) + require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, module.Entry.GetPublisherGap().GetReason()) + + registered := byName["registered"] + require.Same(t, registrations[0], registered.Registration) + require.Equal(t, "solution:registered", registered.Entry.GetPublisher()) + require.Empty(t, registered.Entry.GetInstallations()) + + installed := byName["installed"] + require.Same(t, registrations[1], installed.Registration) + require.Len(t, installed.Entry.GetInstallations(), 2) + first := installed.Entry.GetInstallations()[0] + require.Equal(t, "i1", first.GetInstallation().GetId()) + require.Equal(t, "Acme", first.GetOrgName()) + require.Equal(t, &gen.CatalogueRelease{Publisher: "example", Name: "installed", Version: "1.2.0"}, first.GetAgentRelease()) + require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, first.GetRevisionGap().GetReason()) + require.Equal(t, "Team i1", first.GetExposedTeams()[0].GetSubjectLabel()) + require.Equal(t, "1.3.0", installed.Entry.GetInstallations()[1].GetAgentRelease().GetVersion()) + + unregistered := byName["unregistered"] + require.Nil(t, unregistered.Registration) + require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, unregistered.Entry.GetPublisherGap().GetReason()) + require.Contains(t, unregistered.Entry.GetPublisherGap().GetDetail(), "No registration") + require.Nil(t, unregistered.Entry.GetInstallations()[0].GetAgentRelease()) + require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, + unregistered.Entry.GetInstallations()[0].GetAgentReleaseGap().GetReason()) + + retired := byName["retired-but-installed"] + require.Same(t, registrations[3], retired.Registration, "an installation of a deregistered solution is shown beside its tombstone") + require.Len(t, retired.Entry.GetInstallations(), 1) + + withTombstones := projectPlatformCatalogue(modules, registrations, installations, true) + require.Contains(t, catalogueEntryNames(withTombstones), "CATALOGUE_ENTRY_KIND_SOLUTION:retired") +} + +func TestParseAgentRelease(t *testing.T) { + release, ok := parseAgentRelease("example.io/billing:2.0.1") + require.True(t, ok) + require.Equal(t, &gen.CatalogueRelease{Publisher: "example.io", Name: "billing", Version: "2.0.1"}, release) + + for _, identifier := range []string{"", "billing", "example/billing", "example/billing:", "/billing:1", "example:1/billing"} { + _, ok := parseAgentRelease(identifier) + require.False(t, ok, identifier) + } +} diff --git a/module/services/accounts/code/pkg/business/service_catalog_test.go b/module/services/accounts/code/pkg/business/service_catalog_test.go index a829b6aaf..5b286372f 100644 --- a/module/services/accounts/code/pkg/business/service_catalog_test.go +++ b/module/services/accounts/code/pkg/business/service_catalog_test.go @@ -23,7 +23,7 @@ func TestServiceCatalogCompilation(t *testing.T) { require.Equal(t, "saas.accounts.v1", catalog.GetApiPackage()) require.Equal(t, business.ServiceVersion, catalog.GetApiVersion()) require.Len(t, catalog.GetServices(), 35) - require.Len(t, catalog.GetMethods(), 245) + require.Len(t, catalog.GetMethods(), 246) require.Len(t, catalog.GetPermissions(), 24) require.Len(t, catalog.GetEntitlements(), 5) require.Equal(t, "*:*", catalog.GetPermissions()[0].GetPermission()) diff --git a/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go b/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go index 9c1e16b09..071369b8f 100644 --- a/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go +++ b/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go @@ -40,7 +40,7 @@ func TestFrontendPluginCatalogIsDeterministicAndCurrent(t *testing.T) { func TestFrontendPageDiscoveryPinsAccessAndMatch(t *testing.T) { routes, err := cataloggen.DiscoverNextPageRoutes(filepath.Clean("../../../../frontend/code")) require.NoError(t, err) - require.Len(t, routes, 55) + require.Len(t, routes, 56) byPath := make(map[string]*catalogv1.FrontendRoute, len(routes)) for _, route := range routes { byPath[route.GetPath()] = route @@ -59,6 +59,7 @@ func TestFrontendPageDiscoveryPinsAccessAndMatch(t *testing.T) { require.Equal(t, catalogv1.FrontendRouteAccess_FRONTEND_ROUTE_ACCESS_SUPER_ADMIN, byPath["/admin/platform"].GetAccess()) require.Equal(t, catalogv1.FrontendRouteAccess_FRONTEND_ROUTE_ACCESS_SUPER_ADMIN, byPath["/admin/platform/jobs"].GetAccess()) require.Equal(t, catalogv1.FrontendRouteAccess_FRONTEND_ROUTE_ACCESS_SUPER_ADMIN, byPath["/admin/platform/events"].GetAccess()) + require.Equal(t, catalogv1.FrontendRouteAccess_FRONTEND_ROUTE_ACCESS_SUPER_ADMIN, byPath["/admin/platform/catalogue"].GetAccess()) require.Equal(t, catalogv1.FrontendRouteAccess_FRONTEND_ROUTE_ACCESS_SUPER_ADMIN, byPath["/admin/platform/organizations"].GetAccess()) require.Equal(t, catalogv1.FrontendRouteMatch_FRONTEND_ROUTE_MATCH_CATCH_ALL, byPath["/admin/{*slug}"].GetMatch()) diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go index 8f84d39a5..cc3b38a05 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/accountsv1connect/platform_admin.connect.go @@ -103,6 +103,9 @@ const ( // PlatformAdminServiceListEventSubscriptionsProcedure is the fully-qualified name of the // PlatformAdminService's ListEventSubscriptions RPC. PlatformAdminServiceListEventSubscriptionsProcedure = "/saas.accounts.v1.PlatformAdminService/ListEventSubscriptions" + // PlatformAdminServiceListPlatformCatalogueProcedure is the fully-qualified name of the + // PlatformAdminService's ListPlatformCatalogue RPC. + PlatformAdminServiceListPlatformCatalogueProcedure = "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue" ) // PlatformAdminServiceClient is a client for the saas.accounts.v1.PlatformAdminService service. @@ -155,6 +158,9 @@ type PlatformAdminServiceClient interface { // control-plane subscription metadata do. GetEventOperations(context.Context, *connect.Request[v12.GetEventOperationsRequest]) (*connect.Response[v12.GetEventOperationsResponse], error) ListEventSubscriptions(context.Context, *connect.Request[v12.ListEventSubscriptionsRequest]) (*connect.Response[v12.ListEventSubscriptionsResponse], error) + // ListPlatformCatalogue lists every composed module and solution with what + // it declares, what runs, and which organizations have it installed. + ListPlatformCatalogue(context.Context, *connect.Request[v1.ListPlatformCatalogueRequest]) (*connect.Response[v1.ListPlatformCatalogueResponse], error) } // NewPlatformAdminServiceClient constructs a client for the saas.accounts.v1.PlatformAdminService @@ -300,6 +306,12 @@ func NewPlatformAdminServiceClient(httpClient connect.HTTPClient, baseURL string connect.WithSchema(platformAdminServiceMethods.ByName("ListEventSubscriptions")), connect.WithClientOptions(opts...), ), + listPlatformCatalogue: connect.NewClient[v1.ListPlatformCatalogueRequest, v1.ListPlatformCatalogueResponse]( + httpClient, + baseURL+PlatformAdminServiceListPlatformCatalogueProcedure, + connect.WithSchema(platformAdminServiceMethods.ByName("ListPlatformCatalogue")), + connect.WithClientOptions(opts...), + ), } } @@ -327,6 +339,7 @@ type platformAdminServiceClient struct { replayJob *connect.Client[v11.ReplayJobRequest, v11.ReplayJobResponse] getEventOperations *connect.Client[v12.GetEventOperationsRequest, v12.GetEventOperationsResponse] listEventSubscriptions *connect.Client[v12.ListEventSubscriptionsRequest, v12.ListEventSubscriptionsResponse] + listPlatformCatalogue *connect.Client[v1.ListPlatformCatalogueRequest, v1.ListPlatformCatalogueResponse] } // SearchUsers calls saas.accounts.v1.PlatformAdminService.SearchUsers. @@ -441,6 +454,11 @@ func (c *platformAdminServiceClient) ListEventSubscriptions(ctx context.Context, return c.listEventSubscriptions.CallUnary(ctx, req) } +// ListPlatformCatalogue calls saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue. +func (c *platformAdminServiceClient) ListPlatformCatalogue(ctx context.Context, req *connect.Request[v1.ListPlatformCatalogueRequest]) (*connect.Response[v1.ListPlatformCatalogueResponse], error) { + return c.listPlatformCatalogue.CallUnary(ctx, req) +} + // PlatformAdminServiceHandler is an implementation of the saas.accounts.v1.PlatformAdminService // service. type PlatformAdminServiceHandler interface { @@ -492,6 +510,9 @@ type PlatformAdminServiceHandler interface { // control-plane subscription metadata do. GetEventOperations(context.Context, *connect.Request[v12.GetEventOperationsRequest]) (*connect.Response[v12.GetEventOperationsResponse], error) ListEventSubscriptions(context.Context, *connect.Request[v12.ListEventSubscriptionsRequest]) (*connect.Response[v12.ListEventSubscriptionsResponse], error) + // ListPlatformCatalogue lists every composed module and solution with what + // it declares, what runs, and which organizations have it installed. + ListPlatformCatalogue(context.Context, *connect.Request[v1.ListPlatformCatalogueRequest]) (*connect.Response[v1.ListPlatformCatalogueResponse], error) } // NewPlatformAdminServiceHandler builds an HTTP handler from the service implementation. It returns @@ -633,6 +654,12 @@ func NewPlatformAdminServiceHandler(svc PlatformAdminServiceHandler, opts ...con connect.WithSchema(platformAdminServiceMethods.ByName("ListEventSubscriptions")), connect.WithHandlerOptions(opts...), ) + platformAdminServiceListPlatformCatalogueHandler := connect.NewUnaryHandler( + PlatformAdminServiceListPlatformCatalogueProcedure, + svc.ListPlatformCatalogue, + connect.WithSchema(platformAdminServiceMethods.ByName("ListPlatformCatalogue")), + connect.WithHandlerOptions(opts...), + ) return "/saas.accounts.v1.PlatformAdminService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case PlatformAdminServiceSearchUsersProcedure: @@ -679,6 +706,8 @@ func NewPlatformAdminServiceHandler(svc PlatformAdminServiceHandler, opts ...con platformAdminServiceGetEventOperationsHandler.ServeHTTP(w, r) case PlatformAdminServiceListEventSubscriptionsProcedure: platformAdminServiceListEventSubscriptionsHandler.ServeHTTP(w, r) + case PlatformAdminServiceListPlatformCatalogueProcedure: + platformAdminServiceListPlatformCatalogueHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -775,3 +804,7 @@ func (UnimplementedPlatformAdminServiceHandler) GetEventOperations(context.Conte func (UnimplementedPlatformAdminServiceHandler) ListEventSubscriptions(context.Context, *connect.Request[v12.ListEventSubscriptionsRequest]) (*connect.Response[v12.ListEventSubscriptionsResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.PlatformAdminService.ListEventSubscriptions is not implemented")) } + +func (UnimplementedPlatformAdminServiceHandler) ListPlatformCatalogue(context.Context, *connect.Request[v1.ListPlatformCatalogueRequest]) (*connect.Response[v1.ListPlatformCatalogueResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue is not implemented")) +} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index 9b56f42e1..36ae61af0 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -29,6 +29,170 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// CatalogueEntryKind distinguishes a composed module from a solution. +type CatalogueEntryKind int32 + +const ( + CatalogueEntryKind_CATALOGUE_ENTRY_KIND_UNSPECIFIED CatalogueEntryKind = 0 + // A module composed into this deployment, known by its module principal. + CatalogueEntryKind_CATALOGUE_ENTRY_KIND_MODULE CatalogueEntryKind = 1 + // A solution, known by its registration, its installations, or both. + CatalogueEntryKind_CATALOGUE_ENTRY_KIND_SOLUTION CatalogueEntryKind = 2 +) + +// Enum value maps for CatalogueEntryKind. +var ( + CatalogueEntryKind_name = map[int32]string{ + 0: "CATALOGUE_ENTRY_KIND_UNSPECIFIED", + 1: "CATALOGUE_ENTRY_KIND_MODULE", + 2: "CATALOGUE_ENTRY_KIND_SOLUTION", + } + CatalogueEntryKind_value = map[string]int32{ + "CATALOGUE_ENTRY_KIND_UNSPECIFIED": 0, + "CATALOGUE_ENTRY_KIND_MODULE": 1, + "CATALOGUE_ENTRY_KIND_SOLUTION": 2, + } +) + +func (x CatalogueEntryKind) Enum() *CatalogueEntryKind { + p := new(CatalogueEntryKind) + *p = x + return p +} + +func (x CatalogueEntryKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CatalogueEntryKind) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_platform_admin_proto_enumTypes[0].Descriptor() +} + +func (CatalogueEntryKind) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_platform_admin_proto_enumTypes[0] +} + +func (x CatalogueEntryKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CatalogueEntryKind.Descriptor instead. +func (CatalogueEntryKind) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{0} +} + +// CatalogueGapReason says why a Catalogue fact has no value. +type CatalogueGapReason int32 + +const ( + CatalogueGapReason_CATALOGUE_GAP_REASON_UNSPECIFIED CatalogueGapReason = 0 + // This host keeps no record that carries the fact. + CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED CatalogueGapReason = 1 + // The fact is a section of the producer's document, and the document this + // host holds carries no such section (an older producer). + CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_REPORTED CatalogueGapReason = 2 + // The fact needs observed cluster state, and this host has none to read — + // a local run with no cluster, or an observation that is missing or stale. + CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED CatalogueGapReason = 3 +) + +// Enum value maps for CatalogueGapReason. +var ( + CatalogueGapReason_name = map[int32]string{ + 0: "CATALOGUE_GAP_REASON_UNSPECIFIED", + 1: "CATALOGUE_GAP_REASON_NOT_RECORDED", + 2: "CATALOGUE_GAP_REASON_NOT_REPORTED", + 3: "CATALOGUE_GAP_REASON_NOT_OBSERVED", + } + CatalogueGapReason_value = map[string]int32{ + "CATALOGUE_GAP_REASON_UNSPECIFIED": 0, + "CATALOGUE_GAP_REASON_NOT_RECORDED": 1, + "CATALOGUE_GAP_REASON_NOT_REPORTED": 2, + "CATALOGUE_GAP_REASON_NOT_OBSERVED": 3, + } +) + +func (x CatalogueGapReason) Enum() *CatalogueGapReason { + p := new(CatalogueGapReason) + *p = x + return p +} + +func (x CatalogueGapReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CatalogueGapReason) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_platform_admin_proto_enumTypes[1].Descriptor() +} + +func (CatalogueGapReason) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_platform_admin_proto_enumTypes[1] +} + +func (x CatalogueGapReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CatalogueGapReason.Descriptor instead. +func (CatalogueGapReason) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{1} +} + +// CatalogueRunningVerdict compares the observed execution with the declared +// one. It exists only when both are known; anything less is a CatalogueGap. +type CatalogueRunningVerdict int32 + +const ( + CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_UNSPECIFIED CatalogueRunningVerdict = 0 + // The observed image digest and incarnation are the declared ones. + CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_MATCHES CatalogueRunningVerdict = 1 + // Something other than the declared execution is running — what cluster + // admission refuses. + CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS CatalogueRunningVerdict = 2 +) + +// Enum value maps for CatalogueRunningVerdict. +var ( + CatalogueRunningVerdict_name = map[int32]string{ + 0: "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", + 1: "CATALOGUE_RUNNING_VERDICT_MATCHES", + 2: "CATALOGUE_RUNNING_VERDICT_DIFFERS", + } + CatalogueRunningVerdict_value = map[string]int32{ + "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED": 0, + "CATALOGUE_RUNNING_VERDICT_MATCHES": 1, + "CATALOGUE_RUNNING_VERDICT_DIFFERS": 2, + } +) + +func (x CatalogueRunningVerdict) Enum() *CatalogueRunningVerdict { + p := new(CatalogueRunningVerdict) + *p = x + return p +} + +func (x CatalogueRunningVerdict) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CatalogueRunningVerdict) Descriptor() protoreflect.EnumDescriptor { + return file_saas_accounts_v1_platform_admin_proto_enumTypes[2].Descriptor() +} + +func (CatalogueRunningVerdict) Type() protoreflect.EnumType { + return &file_saas_accounts_v1_platform_admin_proto_enumTypes[2] +} + +func (x CatalogueRunningVerdict) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CatalogueRunningVerdict.Descriptor instead. +func (CatalogueRunningVerdict) EnumDescriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{2} +} + type SearchUsersRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` @@ -1805,320 +1969,1268 @@ func (x *UpsertFeatureFlagResponse) GetName() string { return "" } -var File_saas_accounts_v1_platform_admin_proto protoreflect.FileDescriptor - -const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + - "\n" + - "%saas/accounts/v1/platform_admin.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1dsaas/accounts/v1/common.proto\x1a\x1fsaas/events/v1/operations.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"q\n" + - "\x12SearchUsersRequest\x12\x14\n" + - "\x05query\x18\x01 \x01(\tR\x05query\x12&\n" + - "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + - "\n" + - "page_token\x18\x03 \x01(\tR\tpageToken\"\x8c\x01\n" + - "\x13SearchUsersResponse\x12,\n" + - "\x05users\x18\x01 \x03(\v2\x16.saas.accounts.v1.UserR\x05users\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1f\n" + - "\vtotal_count\x18\x03 \x01(\x05R\n" + - "totalCount\"\xa5\x01\n" + - "\x1bListAllOrganizationsRequest\x12\x14\n" + - "\x05query\x18\x01 \x01(\tR\x05query\x12&\n" + - "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + - "\n" + - "page_token\x18\x03 \x01(\tR\tpageToken\x12)\n" + - "\x10include_archived\x18\x04 \x01(\bR\x0fincludeArchived\"}\n" + - "\x14PlatformOrganization\x12B\n" + - "\forganization\x18\x01 \x01(\v2\x1e.saas.accounts.v1.OrganizationR\forganization\x12!\n" + - "\fmember_count\x18\x02 \x01(\x05R\vmemberCount\"\x94\x01\n" + - "\x1cListAllOrganizationsResponse\x12L\n" + - "\rorganizations\x18\x01 \x03(\v2&.saas.accounts.v1.PlatformOrganizationR\rorganizations\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"?\n" + - "\x1cGetOrganizationRosterRequest\x12\x1f\n" + - "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\"Z\n" + - "\x1dGetOrganizationRosterResponse\x129\n" + - "\amembers\x18\x01 \x03(\v2\x1f.saas.accounts.v1.OrgMembershipR\amembers\"O\n" + - "\x12SuspendUserRequest\x12!\n" + - "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\x12\x16\n" + - "\x06reason\x18\x02 \x01(\tR\x06reason\"9\n" + - "\x14UnsuspendUserRequest\x12!\n" + - "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\"_\n" + - "\x16ImpersonateUserRequest\x12!\n" + - "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\x12\"\n" + - "\x06reason\x18\x02 \x01(\tB\n" + - "\xbaH\ar\x05\x10\n" + - "\x18\xf4\x03R\x06reason\"[\n" + - "\x17ImpersonateUserResponse\x12!\n" + - "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12\x1d\n" + - "\n" + - "expires_in\x18\x02 \x01(\x03R\texpiresIn\"\x1a\n" + - "\x18StopImpersonationRequest\"\x9f\x01\n" + - "\x19StopImpersonationResponse\x12)\n" + - "\x10duration_seconds\x18\x01 \x01(\x03R\x0fdurationSeconds\x120\n" + - "\x14access_token_revoked\x18\x02 \x01(\bR\x12accessTokenRevoked\x12%\n" + - "\x0ealready_closed\x18\x03 \x01(\bR\ralreadyClosed\"{\n" + - "\x19ListActiveSessionsRequest\x12\x17\n" + - "\auser_id\x18\x01 \x01(\tR\x06userId\x12&\n" + - "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + - "\n" + - "page_token\x18\x03 \x01(\tR\tpageToken\"\x8b\x04\n" + - "\vSessionInfo\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + - "\auser_id\x18\x02 \x01(\tR\x06userId\x12\x1d\n" + - "\n" + - "ip_address\x18\x03 \x01(\tR\tipAddress\x12N\n" + - "\vdevice_info\x18\x04 \x03(\v2-.saas.accounts.v1.SessionInfo.DeviceInfoEntryR\n" + - "deviceInfo\x129\n" + - "\n" + - "created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12@\n" + - "\x0elast_active_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\flastActiveAt\x129\n" + - "\n" + - "expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12B\n" + - "\x0fidle_expires_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\ridleExpiresAt\x12)\n" + - "\x11acting_as_user_id\x18\t \x01(\tR\x0eactingAsUserId\x1a=\n" + - "\x0fDeviceInfoEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x7f\n" + - "\x1aListActiveSessionsResponse\x129\n" + - "\bsessions\x18\x01 \x03(\v2\x1d.saas.accounts.v1.SessionInfoR\bsessions\x12&\n" + - "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"V\n" + - "\x14RevokeSessionRequest\x12&\n" + - "\n" + - "session_id\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tsessionId\x12\x16\n" + - "\x06reason\x18\x02 \x01(\tR\x06reason\"<\n" + - "\x19GetOrgEntitlementsRequest\x12\x1f\n" + - "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\"\x80\x01\n" + - "\x1aGetOrgEntitlementsResponse\x12\x1b\n" + - "\tplan_name\x18\x01 \x01(\tR\bplanName\x12E\n" + - "\fentitlements\x18\x02 \x03(\v2!.saas.accounts.v1.EntitlementInfoR\fentitlements\"x\n" + - "\x0fEntitlementInfo\x12\x18\n" + - "\afeature\x18\x01 \x01(\tR\afeature\x12\x14\n" + - "\x05limit\x18\x02 \x01(\x03R\x05limit\x12\x12\n" + - "\x04used\x18\x03 \x01(\x03R\x04used\x12!\n" + - "\fhas_override\x18\x04 \x01(\bR\vhasOverride\"\x99\x01\n" + - "\x1aOverrideEntitlementRequest\x12\x1f\n" + - "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12!\n" + - "\afeature\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\afeature\x12\x1f\n" + - "\vlimit_value\x18\x03 \x01(\x03R\n" + - "limitValue\x12\x16\n" + - "\x06reason\x18\x04 \x01(\tR\x06reason\"-\n" + - "\x1bOverrideEntitlementResponse\x12\x0e\n" + - "\x02id\x18\x01 \x01(\tR\x02id\"\x88\x01\n" + - "\x18GrantPlatformRoleRequest\x12!\n" + - "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\x12I\n" + - "\rplatform_role\x18\x02 \x01(\tB$\xbaH!r\x1fR\vsuper_adminR\asupportR\abillingR\fplatformRole\">\n" + - "\x19RevokePlatformRoleRequest\x12!\n" + - "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\"\x1b\n" + - "\x19ListPlatformAdminsRequest\"\xac\x01\n" + - "\x12PlatformAdminEntry\x12\x17\n" + - "\auser_id\x18\x01 \x01(\tR\x06userId\x12#\n" + - "\rplatform_role\x18\x02 \x01(\tR\fplatformRole\x12\x1d\n" + - "\n" + - "granted_by\x18\x03 \x01(\tR\tgrantedBy\x129\n" + - "\n" + - "granted_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tgrantedAt\"Z\n" + - "\x1aListPlatformAdminsResponse\x12<\n" + - "\x06admins\x18\x01 \x03(\v2$.saas.accounts.v1.PlatformAdminEntryR\x06admins\"\x19\n" + - "\x17ListFeatureFlagsRequest\"\xb1\x01\n" + - "\x10FeatureFlagEntry\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x18\n" + - "\aenabled\x18\x03 \x01(\bR\aenabled\x12'\n" + - "\x0frollout_percent\x18\x04 \x01(\x05R\x0erolloutPercent\x12$\n" + - "\x0etarget_org_ids\x18\x05 \x03(\tR\ftargetOrgIds\"T\n" + - "\x18ListFeatureFlagsResponse\x128\n" + - "\x05flags\x18\x01 \x03(\v2\".saas.accounts.v1.FeatureFlagEntryR\x05flags\"\xc6\x01\n" + - "\x18UpsertFeatureFlagRequest\x12\x1b\n" + - "\x04name\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x04name\x12 \n" + - "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x18\n" + - "\aenabled\x18\x03 \x01(\bR\aenabled\x12'\n" + - "\x0frollout_percent\x18\x04 \x01(\x05R\x0erolloutPercent\x12$\n" + - "\x0etarget_org_ids\x18\x05 \x03(\tR\ftargetOrgIds:\x02\x18\x01\"3\n" + - "\x19UpsertFeatureFlagResponse\x12\x12\n" + - "\x04name\x18\x01 \x01(\tR\x04name:\x02\x18\x012\xbc\x1e\n" + - "\x14PlatformAdminService\x12\x8e\x01\n" + - "\vSearchUsers\x12$.saas.accounts.v1.SearchUsersRequest\x1a%.saas.accounts.v1.SearchUsersResponse\"2\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/platform/users\x12\xab\x01\n" + - "\vSuspendUser\x12$.saas.accounts.v1.SuspendUserRequest\x1a\x16.google.protobuf.Empty\"^\xc2\xf3\x18+\b\x02\x10\x010\x01:\x17\n" + - "\x13saas.user.suspended\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02):\x01*\"$/v1/platform/users/{user_id}:suspend\x12\xb3\x01\n" + - "\rUnsuspendUser\x12&.saas.accounts.v1.UnsuspendUserRequest\x1a\x16.google.protobuf.Empty\"b\xc2\xf3\x18-\b\x02\x10\x010\x01:\x19\n" + - "\x15saas.user.unsuspended\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02+:\x01*\"&/v1/platform/users/{user_id}:unsuspend\x12\xd6\x01\n" + - "\x0fImpersonateUser\x12(.saas.accounts.v1.ImpersonateUserRequest\x1a).saas.accounts.v1.ImpersonateUserResponse\"n\xc2\xf3\x187\b\x02\x10\x010\x04:#\n" + - "\x1fsaas.platform.user_impersonated\x10\x02@\x01H\x05P\x03X\x04`\x03x\x02\x82\xd3\xe4\x93\x02-:\x01*\"(/v1/platform/users/{user_id}:impersonate\x12\xd8\x01\n" + - "\x11StopImpersonation\x12*.saas.accounts.v1.StopImpersonationRequest\x1a+.saas.accounts.v1.StopImpersonationResponse\"j\xc2\xf3\x18<\b\x02\x10\x010\x01:*\n" + - "&saas.platform.user_impersonation_ended\x10\x02@\x01H\x05P\x03X\x03`\x01\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/v1/platform/impersonation:stop\x12\xa6\x01\n" + - "\x12ListActiveSessions\x12+.saas.accounts.v1.ListActiveSessionsRequest\x1a,.saas.accounts.v1.ListActiveSessionsResponse\"5\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02\x17\x12\x15/v1/platform/sessions\x12\xa9\x01\n" + - "\rRevokeSession\x12&.saas.accounts.v1.RevokeSessionRequest\x1a\x16.google.protobuf.Empty\"X\xc2\xf3\x18*\b\x02\x10\x010\x01:\x18\n" + - "\x14saas.session.revoked\x10\x02@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02$*\"/v1/platform/sessions/{session_id}\x12\xcf\x01\n" + - "\x12GetOrgEntitlements\x12+.saas.accounts.v1.GetOrgEntitlementsRequest\x1a,.saas.accounts.v1.GetOrgEntitlementsResponse\"^\xc2\xf3\x18\"\b\x02\x10\x03*\f\n" + - "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x022\x120/v1/platform/organizations/{org_id}/entitlements\x12\xe4\x01\n" + - "\x13OverrideEntitlement\x12,.saas.accounts.v1.OverrideEntitlementRequest\x1a-.saas.accounts.v1.OverrideEntitlementResponse\"p\xc2\xf3\x181\b\x02\x10\x010\x01:\x1d\n" + - "\x19saas.entitlement.override\x10\x02@\x01H\x05P\x03X\x03`\x02x\x02\x82\xd3\xe4\x93\x025:\x01*\"0/v1/platform/organizations/{org_id}/entitlements\x12\xad\x01\n" + - "\x11GrantPlatformRole\x12*.saas.accounts.v1.GrantPlatformRoleRequest\x1a\x16.google.protobuf.Empty\"T\xc2\xf3\x182\b\x02\x10\x010\x04:\x1e\n" + - "\x1asaas.platform.role_granted\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/v1/platform/admins\x12\xb6\x01\n" + - "\x12RevokePlatformRole\x12+.saas.accounts.v1.RevokePlatformRoleRequest\x1a\x16.google.protobuf.Empty\"[\xc2\xf3\x182\b\x02\x10\x010\x04:\x1e\n" + - "\x1asaas.platform.role_revoked\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02\x1f*\x1d/v1/platform/admins/{user_id}\x12\xa4\x01\n" + - "\x12ListPlatformAdmins\x12+.saas.accounts.v1.ListPlatformAdminsRequest\x1a,.saas.accounts.v1.ListPlatformAdminsResponse\"3\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x15\x12\x13/v1/platform/admins\x12\x8f\x01\n" + - "\x14ListAllOrganizations\x12-.saas.accounts.v1.ListAllOrganizationsRequest\x1a..saas.accounts.v1.ListAllOrganizationsResponse\"\x18\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x12\x92\x01\n" + - "\x15GetOrganizationRoster\x12..saas.accounts.v1.GetOrganizationRosterRequest\x1a/.saas.accounts.v1.GetOrganizationRosterResponse\"\x18\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x12\xa5\x01\n" + - "\x10ListFeatureFlags\x12).saas.accounts.v1.ListFeatureFlagsRequest\x1a*.saas.accounts.v1.ListFeatureFlagsResponse\":\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1c\x12\x1a/v1/platform/feature-flags\x12\xd2\x01\n" + - "\x11UpsertFeatureFlag\x12*.saas.accounts.v1.UpsertFeatureFlagRequest\x1a+.saas.accounts.v1.UpsertFeatureFlagResponse\"d\xc2\xf3\x181\b\x02\x10\x010\x01:\x1d\n" + - "\x19saas.feature_flag.updated\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02&:\x01*\x1a!/v1/platform/feature-flags/{name}\x88\x02\x01\x12\x9f\x01\n" + - "\x10GetJobOperations\x12%.saas.jobs.v1.GetJobOperationsRequest\x1a&.saas.jobs.v1.GetJobOperationsResponse\"<\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/platform/jobs/operations\x12|\n" + - "\bListJobs\x12\x1d.saas.jobs.v1.ListJobsRequest\x1a\x1e.saas.jobs.v1.ListJobsResponse\"1\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/platform/jobs\x12\x7f\n" + - "\x06GetJob\x12\x1b.saas.jobs.v1.GetJobRequest\x1a\x1c.saas.jobs.v1.GetJobResponse\":\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1c\x12\x1a/v1/platform/jobs/{job_id}\x12\xac\x01\n" + - "\tReplayJob\x12\x1e.saas.jobs.v1.ReplayJobRequest\x1a\x1f.saas.jobs.v1.ReplayJobResponse\"^\xc2\xf3\x18'\b\x02\x10\x010\x03:\x15\n" + - "\x11saas.job.replayed\x10\x02@\x03H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02-:\x01*\"(/v1/platform/jobs/{source_job_id}:replay\x12\xab\x01\n" + - "\x12GetEventOperations\x12).saas.events.v1.GetEventOperationsRequest\x1a*.saas.events.v1.GetEventOperationsResponse\">\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02 \x12\x1e/v1/platform/events/operations\x12\xba\x01\n" + - "\x16ListEventSubscriptions\x12-.saas.events.v1.ListEventSubscriptionsRequest\x1a..saas.events.v1.ListEventSubscriptionsResponse\"A\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02#\x12!/v1/platform/events/subscriptionsB\xba\x01\n" + - "\x14com.saas.accounts.v1B\x12PlatformAdminProtoP\x01Z,accounts/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" +// CatalogueGap is a Catalogue fact this host cannot state, with the reason. +type CatalogueGap struct { + state protoimpl.MessageState `protogen:"open.v1"` + Reason CatalogueGapReason `protobuf:"varint,1,opt,name=reason,proto3,enum=saas.accounts.v1.CatalogueGapReason" json:"reason,omitempty"` + // Operator-facing: names the record or producer that would supply the fact. + Detail string `protobuf:"bytes,2,opt,name=detail,proto3" json:"detail,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} -var ( - file_saas_accounts_v1_platform_admin_proto_rawDescOnce sync.Once - file_saas_accounts_v1_platform_admin_proto_rawDescData []byte -) +func (x *CatalogueGap) Reset() { + *x = CatalogueGap{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} -func file_saas_accounts_v1_platform_admin_proto_rawDescGZIP() []byte { - file_saas_accounts_v1_platform_admin_proto_rawDescOnce.Do(func() { - file_saas_accounts_v1_platform_admin_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_platform_admin_proto_rawDesc), len(file_saas_accounts_v1_platform_admin_proto_rawDesc))) - }) - return file_saas_accounts_v1_platform_admin_proto_rawDescData +func (x *CatalogueGap) String() string { + return protoimpl.X.MessageStringOf(x) } -var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 33) -var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ - (*SearchUsersRequest)(nil), // 0: saas.accounts.v1.SearchUsersRequest - (*SearchUsersResponse)(nil), // 1: saas.accounts.v1.SearchUsersResponse - (*ListAllOrganizationsRequest)(nil), // 2: saas.accounts.v1.ListAllOrganizationsRequest - (*PlatformOrganization)(nil), // 3: saas.accounts.v1.PlatformOrganization - (*ListAllOrganizationsResponse)(nil), // 4: saas.accounts.v1.ListAllOrganizationsResponse - (*GetOrganizationRosterRequest)(nil), // 5: saas.accounts.v1.GetOrganizationRosterRequest - (*GetOrganizationRosterResponse)(nil), // 6: saas.accounts.v1.GetOrganizationRosterResponse - (*SuspendUserRequest)(nil), // 7: saas.accounts.v1.SuspendUserRequest - (*UnsuspendUserRequest)(nil), // 8: saas.accounts.v1.UnsuspendUserRequest - (*ImpersonateUserRequest)(nil), // 9: saas.accounts.v1.ImpersonateUserRequest - (*ImpersonateUserResponse)(nil), // 10: saas.accounts.v1.ImpersonateUserResponse - (*StopImpersonationRequest)(nil), // 11: saas.accounts.v1.StopImpersonationRequest - (*StopImpersonationResponse)(nil), // 12: saas.accounts.v1.StopImpersonationResponse - (*ListActiveSessionsRequest)(nil), // 13: saas.accounts.v1.ListActiveSessionsRequest - (*SessionInfo)(nil), // 14: saas.accounts.v1.SessionInfo - (*ListActiveSessionsResponse)(nil), // 15: saas.accounts.v1.ListActiveSessionsResponse - (*RevokeSessionRequest)(nil), // 16: saas.accounts.v1.RevokeSessionRequest - (*GetOrgEntitlementsRequest)(nil), // 17: saas.accounts.v1.GetOrgEntitlementsRequest - (*GetOrgEntitlementsResponse)(nil), // 18: saas.accounts.v1.GetOrgEntitlementsResponse - (*EntitlementInfo)(nil), // 19: saas.accounts.v1.EntitlementInfo - (*OverrideEntitlementRequest)(nil), // 20: saas.accounts.v1.OverrideEntitlementRequest - (*OverrideEntitlementResponse)(nil), // 21: saas.accounts.v1.OverrideEntitlementResponse - (*GrantPlatformRoleRequest)(nil), // 22: saas.accounts.v1.GrantPlatformRoleRequest - (*RevokePlatformRoleRequest)(nil), // 23: saas.accounts.v1.RevokePlatformRoleRequest - (*ListPlatformAdminsRequest)(nil), // 24: saas.accounts.v1.ListPlatformAdminsRequest - (*PlatformAdminEntry)(nil), // 25: saas.accounts.v1.PlatformAdminEntry - (*ListPlatformAdminsResponse)(nil), // 26: saas.accounts.v1.ListPlatformAdminsResponse - (*ListFeatureFlagsRequest)(nil), // 27: saas.accounts.v1.ListFeatureFlagsRequest - (*FeatureFlagEntry)(nil), // 28: saas.accounts.v1.FeatureFlagEntry - (*ListFeatureFlagsResponse)(nil), // 29: saas.accounts.v1.ListFeatureFlagsResponse - (*UpsertFeatureFlagRequest)(nil), // 30: saas.accounts.v1.UpsertFeatureFlagRequest - (*UpsertFeatureFlagResponse)(nil), // 31: saas.accounts.v1.UpsertFeatureFlagResponse - nil, // 32: saas.accounts.v1.SessionInfo.DeviceInfoEntry - (*User)(nil), // 33: saas.accounts.v1.User - (*Organization)(nil), // 34: saas.accounts.v1.Organization - (*OrgMembership)(nil), // 35: saas.accounts.v1.OrgMembership - (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp - (*v1.GetJobOperationsRequest)(nil), // 37: saas.jobs.v1.GetJobOperationsRequest - (*v1.ListJobsRequest)(nil), // 38: saas.jobs.v1.ListJobsRequest - (*v1.GetJobRequest)(nil), // 39: saas.jobs.v1.GetJobRequest - (*v1.ReplayJobRequest)(nil), // 40: saas.jobs.v1.ReplayJobRequest - (*v11.GetEventOperationsRequest)(nil), // 41: saas.events.v1.GetEventOperationsRequest - (*v11.ListEventSubscriptionsRequest)(nil), // 42: saas.events.v1.ListEventSubscriptionsRequest - (*emptypb.Empty)(nil), // 43: google.protobuf.Empty - (*v1.GetJobOperationsResponse)(nil), // 44: saas.jobs.v1.GetJobOperationsResponse - (*v1.ListJobsResponse)(nil), // 45: saas.jobs.v1.ListJobsResponse - (*v1.GetJobResponse)(nil), // 46: saas.jobs.v1.GetJobResponse - (*v1.ReplayJobResponse)(nil), // 47: saas.jobs.v1.ReplayJobResponse - (*v11.GetEventOperationsResponse)(nil), // 48: saas.events.v1.GetEventOperationsResponse - (*v11.ListEventSubscriptionsResponse)(nil), // 49: saas.events.v1.ListEventSubscriptionsResponse +func (*CatalogueGap) ProtoMessage() {} + +func (x *CatalogueGap) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ - 33, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User - 34, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization - 3, // 2: saas.accounts.v1.ListAllOrganizationsResponse.organizations:type_name -> saas.accounts.v1.PlatformOrganization - 35, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership - 32, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry - 36, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp - 36, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp - 36, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp - 36, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp - 14, // 9: saas.accounts.v1.ListActiveSessionsResponse.sessions:type_name -> saas.accounts.v1.SessionInfo - 19, // 10: saas.accounts.v1.GetOrgEntitlementsResponse.entitlements:type_name -> saas.accounts.v1.EntitlementInfo - 36, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp - 25, // 12: saas.accounts.v1.ListPlatformAdminsResponse.admins:type_name -> saas.accounts.v1.PlatformAdminEntry - 28, // 13: saas.accounts.v1.ListFeatureFlagsResponse.flags:type_name -> saas.accounts.v1.FeatureFlagEntry - 0, // 14: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest - 7, // 15: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest - 8, // 16: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest - 9, // 17: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest - 11, // 18: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest - 13, // 19: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest - 16, // 20: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest - 17, // 21: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest - 20, // 22: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest - 22, // 23: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest - 23, // 24: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest - 24, // 25: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest - 2, // 26: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest - 5, // 27: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest - 27, // 28: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest - 30, // 29: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest - 37, // 30: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest - 38, // 31: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest - 39, // 32: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest - 40, // 33: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest - 41, // 34: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest - 42, // 35: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest - 1, // 36: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse - 43, // 37: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty - 43, // 38: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty - 10, // 39: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse - 12, // 40: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse - 15, // 41: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse - 43, // 42: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty - 18, // 43: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse - 21, // 44: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse - 43, // 45: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty - 43, // 46: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty - 26, // 47: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse - 4, // 48: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse - 6, // 49: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse - 29, // 50: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse - 31, // 51: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse - 44, // 52: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse - 45, // 53: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse - 46, // 54: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse - 47, // 55: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse - 48, // 56: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse - 49, // 57: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse - 36, // [36:58] is the sub-list for method output_type - 14, // [14:36] is the sub-list for method input_type - 14, // [14:14] is the sub-list for extension type_name - 14, // [14:14] is the sub-list for extension extendee - 0, // [0:14] is the sub-list for field type_name + +// Deprecated: Use CatalogueGap.ProtoReflect.Descriptor instead. +func (*CatalogueGap) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{32} } -func init() { file_saas_accounts_v1_platform_admin_proto_init() } +func (x *CatalogueGap) GetReason() CatalogueGapReason { + if x != nil { + return x.Reason + } + return CatalogueGapReason_CATALOGUE_GAP_REASON_UNSPECIFIED +} + +func (x *CatalogueGap) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +// CatalogueRelease is a declared release, publisher/name@version. +type CatalogueRelease struct { + state protoimpl.MessageState `protogen:"open.v1"` + Publisher string `protobuf:"bytes,1,opt,name=publisher,proto3" json:"publisher,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueRelease) Reset() { + *x = CatalogueRelease{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueRelease) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueRelease) ProtoMessage() {} + +func (x *CatalogueRelease) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueRelease.ProtoReflect.Descriptor instead. +func (*CatalogueRelease) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{33} +} + +func (x *CatalogueRelease) GetPublisher() string { + if x != nil { + return x.Publisher + } + return "" +} + +func (x *CatalogueRelease) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CatalogueRelease) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +// CatalogueExecution is what runs: an OCI image manifest digest and the build +// incarnation the host assigned to it, the same pair a Work Context seals. +type CatalogueExecution struct { + state protoimpl.MessageState `protogen:"open.v1"` + ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` + BuildIncarnation uint64 `protobuf:"varint,2,opt,name=build_incarnation,json=buildIncarnation,proto3" json:"build_incarnation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueExecution) Reset() { + *x = CatalogueExecution{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueExecution) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueExecution) ProtoMessage() {} + +func (x *CatalogueExecution) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueExecution.ProtoReflect.Descriptor instead. +func (*CatalogueExecution) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{34} +} + +func (x *CatalogueExecution) GetImageDigest() string { + if x != nil { + return x.ImageDigest + } + return "" +} + +func (x *CatalogueExecution) GetBuildIncarnation() uint64 { + if x != nil { + return x.BuildIncarnation + } + return 0 +} + +// CatalogueRunning is the running incarnation beside the declared one. +type CatalogueRunning struct { + state protoimpl.MessageState `protogen:"open.v1"` + Declared *CatalogueExecution `protobuf:"bytes,1,opt,name=declared,proto3" json:"declared,omitempty"` + Observed *CatalogueExecution `protobuf:"bytes,2,opt,name=observed,proto3" json:"observed,omitempty"` + Verdict CatalogueRunningVerdict `protobuf:"varint,3,opt,name=verdict,proto3,enum=saas.accounts.v1.CatalogueRunningVerdict" json:"verdict,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueRunning) Reset() { + *x = CatalogueRunning{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueRunning) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueRunning) ProtoMessage() {} + +func (x *CatalogueRunning) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueRunning.ProtoReflect.Descriptor instead. +func (*CatalogueRunning) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{35} +} + +func (x *CatalogueRunning) GetDeclared() *CatalogueExecution { + if x != nil { + return x.Declared + } + return nil +} + +func (x *CatalogueRunning) GetObserved() *CatalogueExecution { + if x != nil { + return x.Observed + } + return nil +} + +func (x *CatalogueRunning) GetVerdict() CatalogueRunningVerdict { + if x != nil { + return x.Verdict + } + return CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_UNSPECIFIED +} + +// CatalogueInstallation is one organization's installation of a solution. +type CatalogueInstallation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + OrgName string `protobuf:"bytes,2,opt,name=org_name,json=orgName,proto3" json:"org_name,omitempty"` + // The release the installation's agent principal was created for, read from + // its canonical publisher/name:version identifier. + // + // Types that are valid to be assigned to AgentReleaseValue: + // + // *CatalogueInstallation_AgentRelease + // *CatalogueInstallation_AgentReleaseGap + AgentReleaseValue isCatalogueInstallation_AgentReleaseValue `protobuf_oneof:"agent_release_value"` + // The installation revision the installed generation is pinned at. + // + // Types that are valid to be assigned to RevisionValue: + // + // *CatalogueInstallation_RevisionGap + RevisionValue isCatalogueInstallation_RevisionValue `protobuf_oneof:"revision_value"` + // Active team grants at the installation's authority root or above it, so + // every team whose members the grant reaches inside this installation. + ExposedTeams []*CollectionReadGrant `protobuf:"bytes,6,rep,name=exposed_teams,json=exposedTeams,proto3" json:"exposed_teams,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueInstallation) Reset() { + *x = CatalogueInstallation{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueInstallation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueInstallation) ProtoMessage() {} + +func (x *CatalogueInstallation) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueInstallation.ProtoReflect.Descriptor instead. +func (*CatalogueInstallation) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{36} +} + +func (x *CatalogueInstallation) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *CatalogueInstallation) GetOrgName() string { + if x != nil { + return x.OrgName + } + return "" +} + +func (x *CatalogueInstallation) GetAgentReleaseValue() isCatalogueInstallation_AgentReleaseValue { + if x != nil { + return x.AgentReleaseValue + } + return nil +} + +func (x *CatalogueInstallation) GetAgentRelease() *CatalogueRelease { + if x != nil { + if x, ok := x.AgentReleaseValue.(*CatalogueInstallation_AgentRelease); ok { + return x.AgentRelease + } + } + return nil +} + +func (x *CatalogueInstallation) GetAgentReleaseGap() *CatalogueGap { + if x != nil { + if x, ok := x.AgentReleaseValue.(*CatalogueInstallation_AgentReleaseGap); ok { + return x.AgentReleaseGap + } + } + return nil +} + +func (x *CatalogueInstallation) GetRevisionValue() isCatalogueInstallation_RevisionValue { + if x != nil { + return x.RevisionValue + } + return nil +} + +func (x *CatalogueInstallation) GetRevisionGap() *CatalogueGap { + if x != nil { + if x, ok := x.RevisionValue.(*CatalogueInstallation_RevisionGap); ok { + return x.RevisionGap + } + } + return nil +} + +func (x *CatalogueInstallation) GetExposedTeams() []*CollectionReadGrant { + if x != nil { + return x.ExposedTeams + } + return nil +} + +type isCatalogueInstallation_AgentReleaseValue interface { + isCatalogueInstallation_AgentReleaseValue() +} + +type CatalogueInstallation_AgentRelease struct { + AgentRelease *CatalogueRelease `protobuf:"bytes,3,opt,name=agent_release,json=agentRelease,proto3,oneof"` +} + +type CatalogueInstallation_AgentReleaseGap struct { + AgentReleaseGap *CatalogueGap `protobuf:"bytes,4,opt,name=agent_release_gap,json=agentReleaseGap,proto3,oneof"` +} + +func (*CatalogueInstallation_AgentRelease) isCatalogueInstallation_AgentReleaseValue() {} + +func (*CatalogueInstallation_AgentReleaseGap) isCatalogueInstallation_AgentReleaseValue() {} + +type isCatalogueInstallation_RevisionValue interface { + isCatalogueInstallation_RevisionValue() +} + +type CatalogueInstallation_RevisionGap struct { + RevisionGap *CatalogueGap `protobuf:"bytes,5,opt,name=revision_gap,json=revisionGap,proto3,oneof"` +} + +func (*CatalogueInstallation_RevisionGap) isCatalogueInstallation_RevisionValue() {} + +// CatalogueEntry is one module or solution. +type CatalogueEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind CatalogueEntryKind `protobuf:"varint,1,opt,name=kind,proto3,enum=saas.accounts.v1.CatalogueEntryKind" json:"kind,omitempty"` + // A module's principal prefix, or a solution's registry id — the identifier + // its installations name. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // The owner of record: a solution registration's publisher. + // + // Types that are valid to be assigned to PublisherValue: + // + // *CatalogueEntry_Publisher + // *CatalogueEntry_PublisherGap + PublisherValue isCatalogueEntry_PublisherValue `protobuf_oneof:"publisher_value"` + // Types that are valid to be assigned to DeclaredReleaseValue: + // + // *CatalogueEntry_DeclaredRelease + // *CatalogueEntry_DeclaredReleaseGap + DeclaredReleaseValue isCatalogueEntry_DeclaredReleaseValue `protobuf_oneof:"declared_release_value"` + // The OCI image manifest digest the entry was published at. + // + // Types that are valid to be assigned to BuildDigestValue: + // + // *CatalogueEntry_BuildDigest + // *CatalogueEntry_BuildDigestGap + BuildDigestValue isCatalogueEntry_BuildDigestValue `protobuf_oneof:"build_digest_value"` + // The presence generation this host applied. + // + // Types that are valid to be assigned to GenerationValue: + // + // *CatalogueEntry_Generation + // *CatalogueEntry_GenerationGap + GenerationValue isCatalogueEntry_GenerationValue `protobuf_oneof:"generation_value"` + // Types that are valid to be assigned to RunningValue: + // + // *CatalogueEntry_Running + // *CatalogueEntry_RunningGap + RunningValue isCatalogueEntry_RunningValue `protobuf_oneof:"running_value"` + // Lines of code, backend and frontend, vendored paths excluded. The value + // case is the presence document's build_size section (codefly-dev/core#708) + // as delivered; this host counts nothing itself. + // + // Types that are valid to be assigned to BuildSizeValue: + // + // *CatalogueEntry_BuildSizeGap + BuildSizeValue isCatalogueEntry_BuildSizeValue `protobuf_oneof:"build_size_value"` + // The solution's registration, with deployment topology withheld: the + // frontend manifest, backend upstream and service alias are always empty + // here, as on every browser-facing projection of the registry. Absent for a + // module, which does not self-register, and for a solution known only from + // its installations. + Registration *SolutionRegistration `protobuf:"bytes,14,opt,name=registration,proto3,oneof" json:"registration,omitempty"` + Installations []*CatalogueInstallation `protobuf:"bytes,15,rep,name=installations,proto3" json:"installations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueEntry) Reset() { + *x = CatalogueEntry{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueEntry) ProtoMessage() {} + +func (x *CatalogueEntry) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueEntry.ProtoReflect.Descriptor instead. +func (*CatalogueEntry) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{37} +} + +func (x *CatalogueEntry) GetKind() CatalogueEntryKind { + if x != nil { + return x.Kind + } + return CatalogueEntryKind_CATALOGUE_ENTRY_KIND_UNSPECIFIED +} + +func (x *CatalogueEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CatalogueEntry) GetPublisherValue() isCatalogueEntry_PublisherValue { + if x != nil { + return x.PublisherValue + } + return nil +} + +func (x *CatalogueEntry) GetPublisher() string { + if x != nil { + if x, ok := x.PublisherValue.(*CatalogueEntry_Publisher); ok { + return x.Publisher + } + } + return "" +} + +func (x *CatalogueEntry) GetPublisherGap() *CatalogueGap { + if x != nil { + if x, ok := x.PublisherValue.(*CatalogueEntry_PublisherGap); ok { + return x.PublisherGap + } + } + return nil +} + +func (x *CatalogueEntry) GetDeclaredReleaseValue() isCatalogueEntry_DeclaredReleaseValue { + if x != nil { + return x.DeclaredReleaseValue + } + return nil +} + +func (x *CatalogueEntry) GetDeclaredRelease() *CatalogueRelease { + if x != nil { + if x, ok := x.DeclaredReleaseValue.(*CatalogueEntry_DeclaredRelease); ok { + return x.DeclaredRelease + } + } + return nil +} + +func (x *CatalogueEntry) GetDeclaredReleaseGap() *CatalogueGap { + if x != nil { + if x, ok := x.DeclaredReleaseValue.(*CatalogueEntry_DeclaredReleaseGap); ok { + return x.DeclaredReleaseGap + } + } + return nil +} + +func (x *CatalogueEntry) GetBuildDigestValue() isCatalogueEntry_BuildDigestValue { + if x != nil { + return x.BuildDigestValue + } + return nil +} + +func (x *CatalogueEntry) GetBuildDigest() string { + if x != nil { + if x, ok := x.BuildDigestValue.(*CatalogueEntry_BuildDigest); ok { + return x.BuildDigest + } + } + return "" +} + +func (x *CatalogueEntry) GetBuildDigestGap() *CatalogueGap { + if x != nil { + if x, ok := x.BuildDigestValue.(*CatalogueEntry_BuildDigestGap); ok { + return x.BuildDigestGap + } + } + return nil +} + +func (x *CatalogueEntry) GetGenerationValue() isCatalogueEntry_GenerationValue { + if x != nil { + return x.GenerationValue + } + return nil +} + +func (x *CatalogueEntry) GetGeneration() int64 { + if x != nil { + if x, ok := x.GenerationValue.(*CatalogueEntry_Generation); ok { + return x.Generation + } + } + return 0 +} + +func (x *CatalogueEntry) GetGenerationGap() *CatalogueGap { + if x != nil { + if x, ok := x.GenerationValue.(*CatalogueEntry_GenerationGap); ok { + return x.GenerationGap + } + } + return nil +} + +func (x *CatalogueEntry) GetRunningValue() isCatalogueEntry_RunningValue { + if x != nil { + return x.RunningValue + } + return nil +} + +func (x *CatalogueEntry) GetRunning() *CatalogueRunning { + if x != nil { + if x, ok := x.RunningValue.(*CatalogueEntry_Running); ok { + return x.Running + } + } + return nil +} + +func (x *CatalogueEntry) GetRunningGap() *CatalogueGap { + if x != nil { + if x, ok := x.RunningValue.(*CatalogueEntry_RunningGap); ok { + return x.RunningGap + } + } + return nil +} + +func (x *CatalogueEntry) GetBuildSizeValue() isCatalogueEntry_BuildSizeValue { + if x != nil { + return x.BuildSizeValue + } + return nil +} + +func (x *CatalogueEntry) GetBuildSizeGap() *CatalogueGap { + if x != nil { + if x, ok := x.BuildSizeValue.(*CatalogueEntry_BuildSizeGap); ok { + return x.BuildSizeGap + } + } + return nil +} + +func (x *CatalogueEntry) GetRegistration() *SolutionRegistration { + if x != nil { + return x.Registration + } + return nil +} + +func (x *CatalogueEntry) GetInstallations() []*CatalogueInstallation { + if x != nil { + return x.Installations + } + return nil +} + +type isCatalogueEntry_PublisherValue interface { + isCatalogueEntry_PublisherValue() +} + +type CatalogueEntry_Publisher struct { + Publisher string `protobuf:"bytes,3,opt,name=publisher,proto3,oneof"` +} + +type CatalogueEntry_PublisherGap struct { + PublisherGap *CatalogueGap `protobuf:"bytes,4,opt,name=publisher_gap,json=publisherGap,proto3,oneof"` +} + +func (*CatalogueEntry_Publisher) isCatalogueEntry_PublisherValue() {} + +func (*CatalogueEntry_PublisherGap) isCatalogueEntry_PublisherValue() {} + +type isCatalogueEntry_DeclaredReleaseValue interface { + isCatalogueEntry_DeclaredReleaseValue() +} + +type CatalogueEntry_DeclaredRelease struct { + DeclaredRelease *CatalogueRelease `protobuf:"bytes,5,opt,name=declared_release,json=declaredRelease,proto3,oneof"` +} + +type CatalogueEntry_DeclaredReleaseGap struct { + DeclaredReleaseGap *CatalogueGap `protobuf:"bytes,6,opt,name=declared_release_gap,json=declaredReleaseGap,proto3,oneof"` +} + +func (*CatalogueEntry_DeclaredRelease) isCatalogueEntry_DeclaredReleaseValue() {} + +func (*CatalogueEntry_DeclaredReleaseGap) isCatalogueEntry_DeclaredReleaseValue() {} + +type isCatalogueEntry_BuildDigestValue interface { + isCatalogueEntry_BuildDigestValue() +} + +type CatalogueEntry_BuildDigest struct { + BuildDigest string `protobuf:"bytes,7,opt,name=build_digest,json=buildDigest,proto3,oneof"` +} + +type CatalogueEntry_BuildDigestGap struct { + BuildDigestGap *CatalogueGap `protobuf:"bytes,8,opt,name=build_digest_gap,json=buildDigestGap,proto3,oneof"` +} + +func (*CatalogueEntry_BuildDigest) isCatalogueEntry_BuildDigestValue() {} + +func (*CatalogueEntry_BuildDigestGap) isCatalogueEntry_BuildDigestValue() {} + +type isCatalogueEntry_GenerationValue interface { + isCatalogueEntry_GenerationValue() +} + +type CatalogueEntry_Generation struct { + Generation int64 `protobuf:"varint,9,opt,name=generation,proto3,oneof"` +} + +type CatalogueEntry_GenerationGap struct { + GenerationGap *CatalogueGap `protobuf:"bytes,10,opt,name=generation_gap,json=generationGap,proto3,oneof"` +} + +func (*CatalogueEntry_Generation) isCatalogueEntry_GenerationValue() {} + +func (*CatalogueEntry_GenerationGap) isCatalogueEntry_GenerationValue() {} + +type isCatalogueEntry_RunningValue interface { + isCatalogueEntry_RunningValue() +} + +type CatalogueEntry_Running struct { + Running *CatalogueRunning `protobuf:"bytes,11,opt,name=running,proto3,oneof"` +} + +type CatalogueEntry_RunningGap struct { + RunningGap *CatalogueGap `protobuf:"bytes,12,opt,name=running_gap,json=runningGap,proto3,oneof"` +} + +func (*CatalogueEntry_Running) isCatalogueEntry_RunningValue() {} + +func (*CatalogueEntry_RunningGap) isCatalogueEntry_RunningValue() {} + +type isCatalogueEntry_BuildSizeValue interface { + isCatalogueEntry_BuildSizeValue() +} + +type CatalogueEntry_BuildSizeGap struct { + BuildSizeGap *CatalogueGap `protobuf:"bytes,13,opt,name=build_size_gap,json=buildSizeGap,proto3,oneof"` +} + +func (*CatalogueEntry_BuildSizeGap) isCatalogueEntry_BuildSizeValue() {} + +type ListPlatformCatalogueRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Include deregistered solutions. Their installations are listed either way. + IncludeTombstoned bool `protobuf:"varint,1,opt,name=include_tombstoned,json=includeTombstoned,proto3" json:"include_tombstoned,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPlatformCatalogueRequest) Reset() { + *x = ListPlatformCatalogueRequest{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPlatformCatalogueRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPlatformCatalogueRequest) ProtoMessage() {} + +func (x *ListPlatformCatalogueRequest) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPlatformCatalogueRequest.ProtoReflect.Descriptor instead. +func (*ListPlatformCatalogueRequest) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{38} +} + +func (x *ListPlatformCatalogueRequest) GetIncludeTombstoned() bool { + if x != nil { + return x.IncludeTombstoned + } + return false +} + +type ListPlatformCatalogueResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Modules first, then solutions, each ordered by name. + Entries []*CatalogueEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + // The registry revision the solution entries were read at. + RegistryRevision int64 `protobuf:"varint,2,opt,name=registry_revision,json=registryRevision,proto3" json:"registry_revision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPlatformCatalogueResponse) Reset() { + *x = ListPlatformCatalogueResponse{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPlatformCatalogueResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPlatformCatalogueResponse) ProtoMessage() {} + +func (x *ListPlatformCatalogueResponse) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPlatformCatalogueResponse.ProtoReflect.Descriptor instead. +func (*ListPlatformCatalogueResponse) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{39} +} + +func (x *ListPlatformCatalogueResponse) GetEntries() []*CatalogueEntry { + if x != nil { + return x.Entries + } + return nil +} + +func (x *ListPlatformCatalogueResponse) GetRegistryRevision() int64 { + if x != nil { + return x.RegistryRevision + } + return 0 +} + +var File_saas_accounts_v1_platform_admin_proto protoreflect.FileDescriptor + +const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + + "\n" + + "%saas/accounts/v1/platform_admin.proto\x12\x10saas.accounts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$saas/accounts/v1/authorization.proto\x1a\x1dsaas/accounts/v1/common.proto\x1a$saas/accounts/v1/installations.proto\x1a(saas/accounts/v1/solution_registry.proto\x1a\x1fsaas/events/v1/operations.proto\x1a\x17saas/jobs/v1/jobs.proto\x1a\x1csaas/policy/v1/options.proto\"q\n" + + "\x12SearchUsersRequest\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"\x8c\x01\n" + + "\x13SearchUsersResponse\x12,\n" + + "\x05users\x18\x01 \x03(\v2\x16.saas.accounts.v1.UserR\x05users\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1f\n" + + "\vtotal_count\x18\x03 \x01(\x05R\n" + + "totalCount\"\xa5\x01\n" + + "\x1bListAllOrganizationsRequest\x12\x14\n" + + "\x05query\x18\x01 \x01(\tR\x05query\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\x12)\n" + + "\x10include_archived\x18\x04 \x01(\bR\x0fincludeArchived\"}\n" + + "\x14PlatformOrganization\x12B\n" + + "\forganization\x18\x01 \x01(\v2\x1e.saas.accounts.v1.OrganizationR\forganization\x12!\n" + + "\fmember_count\x18\x02 \x01(\x05R\vmemberCount\"\x94\x01\n" + + "\x1cListAllOrganizationsResponse\x12L\n" + + "\rorganizations\x18\x01 \x03(\v2&.saas.accounts.v1.PlatformOrganizationR\rorganizations\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"?\n" + + "\x1cGetOrganizationRosterRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\"Z\n" + + "\x1dGetOrganizationRosterResponse\x129\n" + + "\amembers\x18\x01 \x03(\v2\x1f.saas.accounts.v1.OrgMembershipR\amembers\"O\n" + + "\x12SuspendUserRequest\x12!\n" + + "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"9\n" + + "\x14UnsuspendUserRequest\x12!\n" + + "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\"_\n" + + "\x16ImpersonateUserRequest\x12!\n" + + "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\x12\"\n" + + "\x06reason\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\n" + + "\x18\xf4\x03R\x06reason\"[\n" + + "\x17ImpersonateUserResponse\x12!\n" + + "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12\x1d\n" + + "\n" + + "expires_in\x18\x02 \x01(\x03R\texpiresIn\"\x1a\n" + + "\x18StopImpersonationRequest\"\x9f\x01\n" + + "\x19StopImpersonationResponse\x12)\n" + + "\x10duration_seconds\x18\x01 \x01(\x03R\x0fdurationSeconds\x120\n" + + "\x14access_token_revoked\x18\x02 \x01(\bR\x12accessTokenRevoked\x12%\n" + + "\x0ealready_closed\x18\x03 \x01(\bR\ralreadyClosed\"{\n" + + "\x19ListActiveSessionsRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12&\n" + + "\tpage_size\x18\x02 \x01(\x05B\t\xbaH\x06\x1a\x04\x18d \x00R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x03 \x01(\tR\tpageToken\"\x8b\x04\n" + + "\vSessionInfo\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + + "\auser_id\x18\x02 \x01(\tR\x06userId\x12\x1d\n" + + "\n" + + "ip_address\x18\x03 \x01(\tR\tipAddress\x12N\n" + + "\vdevice_info\x18\x04 \x03(\v2-.saas.accounts.v1.SessionInfo.DeviceInfoEntryR\n" + + "deviceInfo\x129\n" + + "\n" + + "created_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12@\n" + + "\x0elast_active_at\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\flastActiveAt\x129\n" + + "\n" + + "expires_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\x12B\n" + + "\x0fidle_expires_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\ridleExpiresAt\x12)\n" + + "\x11acting_as_user_id\x18\t \x01(\tR\x0eactingAsUserId\x1a=\n" + + "\x0fDeviceInfoEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x7f\n" + + "\x1aListActiveSessionsResponse\x129\n" + + "\bsessions\x18\x01 \x03(\v2\x1d.saas.accounts.v1.SessionInfoR\bsessions\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"V\n" + + "\x14RevokeSessionRequest\x12&\n" + + "\n" + + "session_id\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\tsessionId\x12\x16\n" + + "\x06reason\x18\x02 \x01(\tR\x06reason\"<\n" + + "\x19GetOrgEntitlementsRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\"\x80\x01\n" + + "\x1aGetOrgEntitlementsResponse\x12\x1b\n" + + "\tplan_name\x18\x01 \x01(\tR\bplanName\x12E\n" + + "\fentitlements\x18\x02 \x03(\v2!.saas.accounts.v1.EntitlementInfoR\fentitlements\"x\n" + + "\x0fEntitlementInfo\x12\x18\n" + + "\afeature\x18\x01 \x01(\tR\afeature\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x03R\x05limit\x12\x12\n" + + "\x04used\x18\x03 \x01(\x03R\x04used\x12!\n" + + "\fhas_override\x18\x04 \x01(\bR\vhasOverride\"\x99\x01\n" + + "\x1aOverrideEntitlementRequest\x12\x1f\n" + + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x12!\n" + + "\afeature\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\afeature\x12\x1f\n" + + "\vlimit_value\x18\x03 \x01(\x03R\n" + + "limitValue\x12\x16\n" + + "\x06reason\x18\x04 \x01(\tR\x06reason\"-\n" + + "\x1bOverrideEntitlementResponse\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"\x88\x01\n" + + "\x18GrantPlatformRoleRequest\x12!\n" + + "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\x12I\n" + + "\rplatform_role\x18\x02 \x01(\tB$\xbaH!r\x1fR\vsuper_adminR\asupportR\abillingR\fplatformRole\">\n" + + "\x19RevokePlatformRoleRequest\x12!\n" + + "\auser_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\"\x1b\n" + + "\x19ListPlatformAdminsRequest\"\xac\x01\n" + + "\x12PlatformAdminEntry\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12#\n" + + "\rplatform_role\x18\x02 \x01(\tR\fplatformRole\x12\x1d\n" + + "\n" + + "granted_by\x18\x03 \x01(\tR\tgrantedBy\x129\n" + + "\n" + + "granted_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tgrantedAt\"Z\n" + + "\x1aListPlatformAdminsResponse\x12<\n" + + "\x06admins\x18\x01 \x03(\v2$.saas.accounts.v1.PlatformAdminEntryR\x06admins\"\x19\n" + + "\x17ListFeatureFlagsRequest\"\xb1\x01\n" + + "\x10FeatureFlagEntry\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x18\n" + + "\aenabled\x18\x03 \x01(\bR\aenabled\x12'\n" + + "\x0frollout_percent\x18\x04 \x01(\x05R\x0erolloutPercent\x12$\n" + + "\x0etarget_org_ids\x18\x05 \x03(\tR\ftargetOrgIds\"T\n" + + "\x18ListFeatureFlagsResponse\x128\n" + + "\x05flags\x18\x01 \x03(\v2\".saas.accounts.v1.FeatureFlagEntryR\x05flags\"\xc6\x01\n" + + "\x18UpsertFeatureFlagRequest\x12\x1b\n" + + "\x04name\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x04name\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x18\n" + + "\aenabled\x18\x03 \x01(\bR\aenabled\x12'\n" + + "\x0frollout_percent\x18\x04 \x01(\x05R\x0erolloutPercent\x12$\n" + + "\x0etarget_org_ids\x18\x05 \x03(\tR\ftargetOrgIds:\x02\x18\x01\"3\n" + + "\x19UpsertFeatureFlagResponse\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name:\x02\x18\x01\"d\n" + + "\fCatalogueGap\x12<\n" + + "\x06reason\x18\x01 \x01(\x0e2$.saas.accounts.v1.CatalogueGapReasonR\x06reason\x12\x16\n" + + "\x06detail\x18\x02 \x01(\tR\x06detail\"^\n" + + "\x10CatalogueRelease\x12\x1c\n" + + "\tpublisher\x18\x01 \x01(\tR\tpublisher\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + + "\aversion\x18\x03 \x01(\tR\aversion\"d\n" + + "\x12CatalogueExecution\x12!\n" + + "\fimage_digest\x18\x01 \x01(\tR\vimageDigest\x12+\n" + + "\x11build_incarnation\x18\x02 \x01(\x04R\x10buildIncarnation\"\xdb\x01\n" + + "\x10CatalogueRunning\x12@\n" + + "\bdeclared\x18\x01 \x01(\v2$.saas.accounts.v1.CatalogueExecutionR\bdeclared\x12@\n" + + "\bobserved\x18\x02 \x01(\v2$.saas.accounts.v1.CatalogueExecutionR\bobserved\x12C\n" + + "\averdict\x18\x03 \x01(\x0e2).saas.accounts.v1.CatalogueRunningVerdictR\averdict\"\xc9\x03\n" + + "\x15CatalogueInstallation\x12B\n" + + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12\x19\n" + + "\borg_name\x18\x02 \x01(\tR\aorgName\x12I\n" + + "\ragent_release\x18\x03 \x01(\v2\".saas.accounts.v1.CatalogueReleaseH\x00R\fagentRelease\x12L\n" + + "\x11agent_release_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x0fagentReleaseGap\x12C\n" + + "\frevision_gap\x18\x05 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\vrevisionGap\x12J\n" + + "\rexposed_teams\x18\x06 \x03(\v2%.saas.accounts.v1.CollectionReadGrantR\fexposedTeamsB\x15\n" + + "\x13agent_release_valueB\x10\n" + + "\x0erevision_value\"\xbe\b\n" + + "\x0eCatalogueEntry\x128\n" + + "\x04kind\x18\x01 \x01(\x0e2$.saas.accounts.v1.CatalogueEntryKindR\x04kind\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1e\n" + + "\tpublisher\x18\x03 \x01(\tH\x00R\tpublisher\x12E\n" + + "\rpublisher_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\fpublisherGap\x12O\n" + + "\x10declared_release\x18\x05 \x01(\v2\".saas.accounts.v1.CatalogueReleaseH\x01R\x0fdeclaredRelease\x12R\n" + + "\x14declared_release_gap\x18\x06 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x12declaredReleaseGap\x12#\n" + + "\fbuild_digest\x18\a \x01(\tH\x02R\vbuildDigest\x12J\n" + + "\x10build_digest_gap\x18\b \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x02R\x0ebuildDigestGap\x12 \n" + + "\n" + + "generation\x18\t \x01(\x03H\x03R\n" + + "generation\x12G\n" + + "\x0egeneration_gap\x18\n" + + " \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x03R\rgenerationGap\x12>\n" + + "\arunning\x18\v \x01(\v2\".saas.accounts.v1.CatalogueRunningH\x04R\arunning\x12A\n" + + "\vrunning_gap\x18\f \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x04R\n" + + "runningGap\x12F\n" + + "\x0ebuild_size_gap\x18\r \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x05R\fbuildSizeGap\x12O\n" + + "\fregistration\x18\x0e \x01(\v2&.saas.accounts.v1.SolutionRegistrationH\x06R\fregistration\x88\x01\x01\x12M\n" + + "\rinstallations\x18\x0f \x03(\v2'.saas.accounts.v1.CatalogueInstallationR\rinstallationsB\x11\n" + + "\x0fpublisher_valueB\x18\n" + + "\x16declared_release_valueB\x14\n" + + "\x12build_digest_valueB\x12\n" + + "\x10generation_valueB\x0f\n" + + "\rrunning_valueB\x12\n" + + "\x10build_size_valueB\x0f\n" + + "\r_registration\"M\n" + + "\x1cListPlatformCatalogueRequest\x12-\n" + + "\x12include_tombstoned\x18\x01 \x01(\bR\x11includeTombstoned\"\x88\x01\n" + + "\x1dListPlatformCatalogueResponse\x12:\n" + + "\aentries\x18\x01 \x03(\v2 .saas.accounts.v1.CatalogueEntryR\aentries\x12+\n" + + "\x11registry_revision\x18\x02 \x01(\x03R\x10registryRevision*~\n" + + "\x12CatalogueEntryKind\x12$\n" + + " CATALOGUE_ENTRY_KIND_UNSPECIFIED\x10\x00\x12\x1f\n" + + "\x1bCATALOGUE_ENTRY_KIND_MODULE\x10\x01\x12!\n" + + "\x1dCATALOGUE_ENTRY_KIND_SOLUTION\x10\x02*\xaf\x01\n" + + "\x12CatalogueGapReason\x12$\n" + + " CATALOGUE_GAP_REASON_UNSPECIFIED\x10\x00\x12%\n" + + "!CATALOGUE_GAP_REASON_NOT_RECORDED\x10\x01\x12%\n" + + "!CATALOGUE_GAP_REASON_NOT_REPORTED\x10\x02\x12%\n" + + "!CATALOGUE_GAP_REASON_NOT_OBSERVED\x10\x03*\x92\x01\n" + + "\x17CatalogueRunningVerdict\x12)\n" + + "%CATALOGUE_RUNNING_VERDICT_UNSPECIFIED\x10\x00\x12%\n" + + "!CATALOGUE_RUNNING_VERDICT_MATCHES\x10\x01\x12%\n" + + "!CATALOGUE_RUNNING_VERDICT_DIFFERS\x10\x022\xef\x1f\n" + + "\x14PlatformAdminService\x12\x8e\x01\n" + + "\vSearchUsers\x12$.saas.accounts.v1.SearchUsersRequest\x1a%.saas.accounts.v1.SearchUsersResponse\"2\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/platform/users\x12\xab\x01\n" + + "\vSuspendUser\x12$.saas.accounts.v1.SuspendUserRequest\x1a\x16.google.protobuf.Empty\"^\xc2\xf3\x18+\b\x02\x10\x010\x01:\x17\n" + + "\x13saas.user.suspended\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02):\x01*\"$/v1/platform/users/{user_id}:suspend\x12\xb3\x01\n" + + "\rUnsuspendUser\x12&.saas.accounts.v1.UnsuspendUserRequest\x1a\x16.google.protobuf.Empty\"b\xc2\xf3\x18-\b\x02\x10\x010\x01:\x19\n" + + "\x15saas.user.unsuspended\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02+:\x01*\"&/v1/platform/users/{user_id}:unsuspend\x12\xd6\x01\n" + + "\x0fImpersonateUser\x12(.saas.accounts.v1.ImpersonateUserRequest\x1a).saas.accounts.v1.ImpersonateUserResponse\"n\xc2\xf3\x187\b\x02\x10\x010\x04:#\n" + + "\x1fsaas.platform.user_impersonated\x10\x02@\x01H\x05P\x03X\x04`\x03x\x02\x82\xd3\xe4\x93\x02-:\x01*\"(/v1/platform/users/{user_id}:impersonate\x12\xd8\x01\n" + + "\x11StopImpersonation\x12*.saas.accounts.v1.StopImpersonationRequest\x1a+.saas.accounts.v1.StopImpersonationResponse\"j\xc2\xf3\x18<\b\x02\x10\x010\x01:*\n" + + "&saas.platform.user_impersonation_ended\x10\x02@\x01H\x05P\x03X\x03`\x01\x82\xd3\xe4\x93\x02$:\x01*\"\x1f/v1/platform/impersonation:stop\x12\xa6\x01\n" + + "\x12ListActiveSessions\x12+.saas.accounts.v1.ListActiveSessionsRequest\x1a,.saas.accounts.v1.ListActiveSessionsResponse\"5\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02\x17\x12\x15/v1/platform/sessions\x12\xa9\x01\n" + + "\rRevokeSession\x12&.saas.accounts.v1.RevokeSessionRequest\x1a\x16.google.protobuf.Empty\"X\xc2\xf3\x18*\b\x02\x10\x010\x01:\x18\n" + + "\x14saas.session.revoked\x10\x02@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02$*\"/v1/platform/sessions/{session_id}\x12\xcf\x01\n" + + "\x12GetOrgEntitlements\x12+.saas.accounts.v1.GetOrgEntitlementsRequest\x1a,.saas.accounts.v1.GetOrgEntitlementsResponse\"^\xc2\xf3\x18\"\b\x02\x10\x03*\f\n" + + "\x06org_id\x10\x02\x18\x010\x01:\x02\x10\x01@\x01H\x03P\x03X\x03`\x01\x82\xd3\xe4\x93\x022\x120/v1/platform/organizations/{org_id}/entitlements\x12\xe4\x01\n" + + "\x13OverrideEntitlement\x12,.saas.accounts.v1.OverrideEntitlementRequest\x1a-.saas.accounts.v1.OverrideEntitlementResponse\"p\xc2\xf3\x181\b\x02\x10\x010\x01:\x1d\n" + + "\x19saas.entitlement.override\x10\x02@\x01H\x05P\x03X\x03`\x02x\x02\x82\xd3\xe4\x93\x025:\x01*\"0/v1/platform/organizations/{org_id}/entitlements\x12\xad\x01\n" + + "\x11GrantPlatformRole\x12*.saas.accounts.v1.GrantPlatformRoleRequest\x1a\x16.google.protobuf.Empty\"T\xc2\xf3\x182\b\x02\x10\x010\x04:\x1e\n" + + "\x1asaas.platform.role_granted\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02\x18:\x01*\"\x13/v1/platform/admins\x12\xb6\x01\n" + + "\x12RevokePlatformRole\x12+.saas.accounts.v1.RevokePlatformRoleRequest\x1a\x16.google.protobuf.Empty\"[\xc2\xf3\x182\b\x02\x10\x010\x04:\x1e\n" + + "\x1asaas.platform.role_revoked\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02\x1f*\x1d/v1/platform/admins/{user_id}\x12\xa4\x01\n" + + "\x12ListPlatformAdmins\x12+.saas.accounts.v1.ListPlatformAdminsRequest\x1a,.saas.accounts.v1.ListPlatformAdminsResponse\"3\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x15\x12\x13/v1/platform/admins\x12\x8f\x01\n" + + "\x14ListAllOrganizations\x12-.saas.accounts.v1.ListAllOrganizationsRequest\x1a..saas.accounts.v1.ListAllOrganizationsResponse\"\x18\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x12\x92\x01\n" + + "\x15GetOrganizationRoster\x12..saas.accounts.v1.GetOrganizationRosterRequest\x1a/.saas.accounts.v1.GetOrganizationRosterResponse\"\x18\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x12\xa5\x01\n" + + "\x10ListFeatureFlags\x12).saas.accounts.v1.ListFeatureFlagsRequest\x1a*.saas.accounts.v1.ListFeatureFlagsResponse\":\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1c\x12\x1a/v1/platform/feature-flags\x12\xd2\x01\n" + + "\x11UpsertFeatureFlag\x12*.saas.accounts.v1.UpsertFeatureFlagRequest\x1a+.saas.accounts.v1.UpsertFeatureFlagResponse\"d\xc2\xf3\x181\b\x02\x10\x010\x01:\x1d\n" + + "\x19saas.feature_flag.updated\x10\x02@\x01H\x05P\x03X\x03`\x05x\x02\x82\xd3\xe4\x93\x02&:\x01*\x1a!/v1/platform/feature-flags/{name}\x88\x02\x01\x12\x9f\x01\n" + + "\x10GetJobOperations\x12%.saas.jobs.v1.GetJobOperationsRequest\x1a&.saas.jobs.v1.GetJobOperationsResponse\"<\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/platform/jobs/operations\x12|\n" + + "\bListJobs\x12\x1d.saas.jobs.v1.ListJobsRequest\x1a\x1e.saas.jobs.v1.ListJobsResponse\"1\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x13\x12\x11/v1/platform/jobs\x12\x7f\n" + + "\x06GetJob\x12\x1b.saas.jobs.v1.GetJobRequest\x1a\x1c.saas.jobs.v1.GetJobResponse\":\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x1c\x12\x1a/v1/platform/jobs/{job_id}\x12\xac\x01\n" + + "\tReplayJob\x12\x1e.saas.jobs.v1.ReplayJobRequest\x1a\x1f.saas.jobs.v1.ReplayJobResponse\"^\xc2\xf3\x18'\b\x02\x10\x010\x03:\x15\n" + + "\x11saas.job.replayed\x10\x02@\x03H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02-:\x01*\"(/v1/platform/jobs/{source_job_id}:replay\x12\xab\x01\n" + + "\x12GetEventOperations\x12).saas.events.v1.GetEventOperationsRequest\x1a*.saas.events.v1.GetEventOperationsResponse\">\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02 \x12\x1e/v1/platform/events/operations\x12\xba\x01\n" + + "\x16ListEventSubscriptions\x12-.saas.events.v1.ListEventSubscriptionsRequest\x1a..saas.events.v1.ListEventSubscriptionsResponse\"A\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02#\x12!/v1/platform/events/subscriptions\x12\xb0\x01\n" + + "\x15ListPlatformCatalogue\x12..saas.accounts.v1.ListPlatformCatalogueRequest\x1a/.saas.accounts.v1.ListPlatformCatalogueResponse\"6\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x05\x82\xd3\xe4\x93\x02\x18\x12\x16/v1/platform/catalogueB\xba\x01\n" + + "\x14com.saas.accounts.v1B\x12PlatformAdminProtoP\x01Z,accounts/pkg/gen/saas/accounts/v1;accountsv1\xa2\x02\x03SAX\xaa\x02\x10Saas.Accounts.V1\xca\x02\x10Saas\\Accounts\\V1\xe2\x02\x1cSaas\\Accounts\\V1\\GPBMetadata\xea\x02\x12Saas::Accounts::V1b\x06proto3" + +var ( + file_saas_accounts_v1_platform_admin_proto_rawDescOnce sync.Once + file_saas_accounts_v1_platform_admin_proto_rawDescData []byte +) + +func file_saas_accounts_v1_platform_admin_proto_rawDescGZIP() []byte { + file_saas_accounts_v1_platform_admin_proto_rawDescOnce.Do(func() { + file_saas_accounts_v1_platform_admin_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_platform_admin_proto_rawDesc), len(file_saas_accounts_v1_platform_admin_proto_rawDesc))) + }) + return file_saas_accounts_v1_platform_admin_proto_rawDescData +} + +var file_saas_accounts_v1_platform_admin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ + (CatalogueEntryKind)(0), // 0: saas.accounts.v1.CatalogueEntryKind + (CatalogueGapReason)(0), // 1: saas.accounts.v1.CatalogueGapReason + (CatalogueRunningVerdict)(0), // 2: saas.accounts.v1.CatalogueRunningVerdict + (*SearchUsersRequest)(nil), // 3: saas.accounts.v1.SearchUsersRequest + (*SearchUsersResponse)(nil), // 4: saas.accounts.v1.SearchUsersResponse + (*ListAllOrganizationsRequest)(nil), // 5: saas.accounts.v1.ListAllOrganizationsRequest + (*PlatformOrganization)(nil), // 6: saas.accounts.v1.PlatformOrganization + (*ListAllOrganizationsResponse)(nil), // 7: saas.accounts.v1.ListAllOrganizationsResponse + (*GetOrganizationRosterRequest)(nil), // 8: saas.accounts.v1.GetOrganizationRosterRequest + (*GetOrganizationRosterResponse)(nil), // 9: saas.accounts.v1.GetOrganizationRosterResponse + (*SuspendUserRequest)(nil), // 10: saas.accounts.v1.SuspendUserRequest + (*UnsuspendUserRequest)(nil), // 11: saas.accounts.v1.UnsuspendUserRequest + (*ImpersonateUserRequest)(nil), // 12: saas.accounts.v1.ImpersonateUserRequest + (*ImpersonateUserResponse)(nil), // 13: saas.accounts.v1.ImpersonateUserResponse + (*StopImpersonationRequest)(nil), // 14: saas.accounts.v1.StopImpersonationRequest + (*StopImpersonationResponse)(nil), // 15: saas.accounts.v1.StopImpersonationResponse + (*ListActiveSessionsRequest)(nil), // 16: saas.accounts.v1.ListActiveSessionsRequest + (*SessionInfo)(nil), // 17: saas.accounts.v1.SessionInfo + (*ListActiveSessionsResponse)(nil), // 18: saas.accounts.v1.ListActiveSessionsResponse + (*RevokeSessionRequest)(nil), // 19: saas.accounts.v1.RevokeSessionRequest + (*GetOrgEntitlementsRequest)(nil), // 20: saas.accounts.v1.GetOrgEntitlementsRequest + (*GetOrgEntitlementsResponse)(nil), // 21: saas.accounts.v1.GetOrgEntitlementsResponse + (*EntitlementInfo)(nil), // 22: saas.accounts.v1.EntitlementInfo + (*OverrideEntitlementRequest)(nil), // 23: saas.accounts.v1.OverrideEntitlementRequest + (*OverrideEntitlementResponse)(nil), // 24: saas.accounts.v1.OverrideEntitlementResponse + (*GrantPlatformRoleRequest)(nil), // 25: saas.accounts.v1.GrantPlatformRoleRequest + (*RevokePlatformRoleRequest)(nil), // 26: saas.accounts.v1.RevokePlatformRoleRequest + (*ListPlatformAdminsRequest)(nil), // 27: saas.accounts.v1.ListPlatformAdminsRequest + (*PlatformAdminEntry)(nil), // 28: saas.accounts.v1.PlatformAdminEntry + (*ListPlatformAdminsResponse)(nil), // 29: saas.accounts.v1.ListPlatformAdminsResponse + (*ListFeatureFlagsRequest)(nil), // 30: saas.accounts.v1.ListFeatureFlagsRequest + (*FeatureFlagEntry)(nil), // 31: saas.accounts.v1.FeatureFlagEntry + (*ListFeatureFlagsResponse)(nil), // 32: saas.accounts.v1.ListFeatureFlagsResponse + (*UpsertFeatureFlagRequest)(nil), // 33: saas.accounts.v1.UpsertFeatureFlagRequest + (*UpsertFeatureFlagResponse)(nil), // 34: saas.accounts.v1.UpsertFeatureFlagResponse + (*CatalogueGap)(nil), // 35: saas.accounts.v1.CatalogueGap + (*CatalogueRelease)(nil), // 36: saas.accounts.v1.CatalogueRelease + (*CatalogueExecution)(nil), // 37: saas.accounts.v1.CatalogueExecution + (*CatalogueRunning)(nil), // 38: saas.accounts.v1.CatalogueRunning + (*CatalogueInstallation)(nil), // 39: saas.accounts.v1.CatalogueInstallation + (*CatalogueEntry)(nil), // 40: saas.accounts.v1.CatalogueEntry + (*ListPlatformCatalogueRequest)(nil), // 41: saas.accounts.v1.ListPlatformCatalogueRequest + (*ListPlatformCatalogueResponse)(nil), // 42: saas.accounts.v1.ListPlatformCatalogueResponse + nil, // 43: saas.accounts.v1.SessionInfo.DeviceInfoEntry + (*User)(nil), // 44: saas.accounts.v1.User + (*Organization)(nil), // 45: saas.accounts.v1.Organization + (*OrgMembership)(nil), // 46: saas.accounts.v1.OrgMembership + (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp + (*Installation)(nil), // 48: saas.accounts.v1.Installation + (*CollectionReadGrant)(nil), // 49: saas.accounts.v1.CollectionReadGrant + (*SolutionRegistration)(nil), // 50: saas.accounts.v1.SolutionRegistration + (*v1.GetJobOperationsRequest)(nil), // 51: saas.jobs.v1.GetJobOperationsRequest + (*v1.ListJobsRequest)(nil), // 52: saas.jobs.v1.ListJobsRequest + (*v1.GetJobRequest)(nil), // 53: saas.jobs.v1.GetJobRequest + (*v1.ReplayJobRequest)(nil), // 54: saas.jobs.v1.ReplayJobRequest + (*v11.GetEventOperationsRequest)(nil), // 55: saas.events.v1.GetEventOperationsRequest + (*v11.ListEventSubscriptionsRequest)(nil), // 56: saas.events.v1.ListEventSubscriptionsRequest + (*emptypb.Empty)(nil), // 57: google.protobuf.Empty + (*v1.GetJobOperationsResponse)(nil), // 58: saas.jobs.v1.GetJobOperationsResponse + (*v1.ListJobsResponse)(nil), // 59: saas.jobs.v1.ListJobsResponse + (*v1.GetJobResponse)(nil), // 60: saas.jobs.v1.GetJobResponse + (*v1.ReplayJobResponse)(nil), // 61: saas.jobs.v1.ReplayJobResponse + (*v11.GetEventOperationsResponse)(nil), // 62: saas.events.v1.GetEventOperationsResponse + (*v11.ListEventSubscriptionsResponse)(nil), // 63: saas.events.v1.ListEventSubscriptionsResponse +} +var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ + 44, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User + 45, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization + 6, // 2: saas.accounts.v1.ListAllOrganizationsResponse.organizations:type_name -> saas.accounts.v1.PlatformOrganization + 46, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership + 43, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry + 47, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp + 47, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp + 47, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp + 47, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp + 17, // 9: saas.accounts.v1.ListActiveSessionsResponse.sessions:type_name -> saas.accounts.v1.SessionInfo + 22, // 10: saas.accounts.v1.GetOrgEntitlementsResponse.entitlements:type_name -> saas.accounts.v1.EntitlementInfo + 47, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp + 28, // 12: saas.accounts.v1.ListPlatformAdminsResponse.admins:type_name -> saas.accounts.v1.PlatformAdminEntry + 31, // 13: saas.accounts.v1.ListFeatureFlagsResponse.flags:type_name -> saas.accounts.v1.FeatureFlagEntry + 1, // 14: saas.accounts.v1.CatalogueGap.reason:type_name -> saas.accounts.v1.CatalogueGapReason + 37, // 15: saas.accounts.v1.CatalogueRunning.declared:type_name -> saas.accounts.v1.CatalogueExecution + 37, // 16: saas.accounts.v1.CatalogueRunning.observed:type_name -> saas.accounts.v1.CatalogueExecution + 2, // 17: saas.accounts.v1.CatalogueRunning.verdict:type_name -> saas.accounts.v1.CatalogueRunningVerdict + 48, // 18: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation + 36, // 19: saas.accounts.v1.CatalogueInstallation.agent_release:type_name -> saas.accounts.v1.CatalogueRelease + 35, // 20: saas.accounts.v1.CatalogueInstallation.agent_release_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 21: saas.accounts.v1.CatalogueInstallation.revision_gap:type_name -> saas.accounts.v1.CatalogueGap + 49, // 22: saas.accounts.v1.CatalogueInstallation.exposed_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 0, // 23: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind + 35, // 24: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap + 36, // 25: saas.accounts.v1.CatalogueEntry.declared_release:type_name -> saas.accounts.v1.CatalogueRelease + 35, // 26: saas.accounts.v1.CatalogueEntry.declared_release_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 27: saas.accounts.v1.CatalogueEntry.build_digest_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 28: saas.accounts.v1.CatalogueEntry.generation_gap:type_name -> saas.accounts.v1.CatalogueGap + 38, // 29: saas.accounts.v1.CatalogueEntry.running:type_name -> saas.accounts.v1.CatalogueRunning + 35, // 30: saas.accounts.v1.CatalogueEntry.running_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 31: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap + 50, // 32: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration + 39, // 33: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation + 40, // 34: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry + 3, // 35: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest + 10, // 36: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest + 11, // 37: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest + 12, // 38: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest + 14, // 39: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest + 16, // 40: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest + 19, // 41: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest + 20, // 42: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest + 23, // 43: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest + 25, // 44: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest + 26, // 45: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest + 27, // 46: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest + 5, // 47: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest + 8, // 48: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest + 30, // 49: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest + 33, // 50: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest + 51, // 51: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest + 52, // 52: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest + 53, // 53: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest + 54, // 54: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest + 55, // 55: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest + 56, // 56: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest + 41, // 57: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest + 4, // 58: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse + 57, // 59: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty + 57, // 60: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty + 13, // 61: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse + 15, // 62: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse + 18, // 63: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse + 57, // 64: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty + 21, // 65: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse + 24, // 66: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse + 57, // 67: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty + 57, // 68: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty + 29, // 69: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse + 7, // 70: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse + 9, // 71: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse + 32, // 72: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse + 34, // 73: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse + 58, // 74: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse + 59, // 75: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse + 60, // 76: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse + 61, // 77: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse + 62, // 78: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse + 63, // 79: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse + 42, // 80: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse + 58, // [58:81] is the sub-list for method output_type + 35, // [35:58] is the sub-list for method input_type + 35, // [35:35] is the sub-list for extension type_name + 35, // [35:35] is the sub-list for extension extendee + 0, // [0:35] is the sub-list for field type_name +} + +func init() { file_saas_accounts_v1_platform_admin_proto_init() } func file_saas_accounts_v1_platform_admin_proto_init() { if File_saas_accounts_v1_platform_admin_proto != nil { return } + file_saas_accounts_v1_authorization_proto_init() file_saas_accounts_v1_common_proto_init() + file_saas_accounts_v1_installations_proto_init() + file_saas_accounts_v1_solution_registry_proto_init() + file_saas_accounts_v1_platform_admin_proto_msgTypes[36].OneofWrappers = []any{ + (*CatalogueInstallation_AgentRelease)(nil), + (*CatalogueInstallation_AgentReleaseGap)(nil), + (*CatalogueInstallation_RevisionGap)(nil), + } + file_saas_accounts_v1_platform_admin_proto_msgTypes[37].OneofWrappers = []any{ + (*CatalogueEntry_Publisher)(nil), + (*CatalogueEntry_PublisherGap)(nil), + (*CatalogueEntry_DeclaredRelease)(nil), + (*CatalogueEntry_DeclaredReleaseGap)(nil), + (*CatalogueEntry_BuildDigest)(nil), + (*CatalogueEntry_BuildDigestGap)(nil), + (*CatalogueEntry_Generation)(nil), + (*CatalogueEntry_GenerationGap)(nil), + (*CatalogueEntry_Running)(nil), + (*CatalogueEntry_RunningGap)(nil), + (*CatalogueEntry_BuildSizeGap)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_platform_admin_proto_rawDesc), len(file_saas_accounts_v1_platform_admin_proto_rawDesc)), - NumEnums: 0, - NumMessages: 33, + NumEnums: 3, + NumMessages: 41, NumExtensions: 0, NumServices: 1, }, GoTypes: file_saas_accounts_v1_platform_admin_proto_goTypes, DependencyIndexes: file_saas_accounts_v1_platform_admin_proto_depIdxs, + EnumInfos: file_saas_accounts_v1_platform_admin_proto_enumTypes, MessageInfos: file_saas_accounts_v1_platform_admin_proto_msgTypes, }.Build() File_saas_accounts_v1_platform_admin_proto = out.File diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go index 0a4aba37b..837d638dd 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.gw.go @@ -755,6 +755,41 @@ func local_request_PlatformAdminService_ListEventSubscriptions_0(ctx context.Con return msg, metadata, err } +var filter_PlatformAdminService_ListPlatformCatalogue_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_PlatformAdminService_ListPlatformCatalogue_0(ctx context.Context, marshaler runtime.Marshaler, client PlatformAdminServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListPlatformCatalogueRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_PlatformAdminService_ListPlatformCatalogue_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListPlatformCatalogue(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_PlatformAdminService_ListPlatformCatalogue_0(ctx context.Context, marshaler runtime.Marshaler, server PlatformAdminServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListPlatformCatalogueRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_PlatformAdminService_ListPlatformCatalogue_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListPlatformCatalogue(ctx, &protoReq) + return msg, metadata, err +} + // RegisterPlatformAdminServiceHandlerServer registers the http handlers for service PlatformAdminService to "mux". // UnaryRPC :call PlatformAdminServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -1161,6 +1196,26 @@ func RegisterPlatformAdminServiceHandlerServer(ctx context.Context, mux *runtime } forward_PlatformAdminService_ListEventSubscriptions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_PlatformAdminService_ListPlatformCatalogue_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", runtime.WithHTTPPathPattern("/v1/platform/catalogue")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_PlatformAdminService_ListPlatformCatalogue_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PlatformAdminService_ListPlatformCatalogue_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -1541,6 +1596,23 @@ func RegisterPlatformAdminServiceHandlerClient(ctx context.Context, mux *runtime } forward_PlatformAdminService_ListEventSubscriptions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_PlatformAdminService_ListPlatformCatalogue_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", runtime.WithHTTPPathPattern("/v1/platform/catalogue")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_PlatformAdminService_ListPlatformCatalogue_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_PlatformAdminService_ListPlatformCatalogue_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) return nil } @@ -1565,6 +1637,7 @@ var ( pattern_PlatformAdminService_ReplayJob_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"v1", "platform", "jobs", "source_job_id"}, "replay")) pattern_PlatformAdminService_GetEventOperations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "events", "operations"}, "")) pattern_PlatformAdminService_ListEventSubscriptions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "platform", "events", "subscriptions"}, "")) + pattern_PlatformAdminService_ListPlatformCatalogue_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "platform", "catalogue"}, "")) ) var ( @@ -1588,4 +1661,5 @@ var ( forward_PlatformAdminService_ReplayJob_0 = runtime.ForwardResponseMessage forward_PlatformAdminService_GetEventOperations_0 = runtime.ForwardResponseMessage forward_PlatformAdminService_ListEventSubscriptions_0 = runtime.ForwardResponseMessage + forward_PlatformAdminService_ListPlatformCatalogue_0 = runtime.ForwardResponseMessage ) diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go index 786e6186f..825a108cf 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin_grpc.pb.go @@ -45,6 +45,7 @@ const ( PlatformAdminService_ReplayJob_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ReplayJob" PlatformAdminService_GetEventOperations_FullMethodName = "/saas.accounts.v1.PlatformAdminService/GetEventOperations" PlatformAdminService_ListEventSubscriptions_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListEventSubscriptions" + PlatformAdminService_ListPlatformCatalogue_FullMethodName = "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue" ) // PlatformAdminServiceClient is the client API for PlatformAdminService service. @@ -101,6 +102,9 @@ type PlatformAdminServiceClient interface { // control-plane subscription metadata do. GetEventOperations(ctx context.Context, in *v11.GetEventOperationsRequest, opts ...grpc.CallOption) (*v11.GetEventOperationsResponse, error) ListEventSubscriptions(ctx context.Context, in *v11.ListEventSubscriptionsRequest, opts ...grpc.CallOption) (*v11.ListEventSubscriptionsResponse, error) + // ListPlatformCatalogue lists every composed module and solution with what + // it declares, what runs, and which organizations have it installed. + ListPlatformCatalogue(ctx context.Context, in *ListPlatformCatalogueRequest, opts ...grpc.CallOption) (*ListPlatformCatalogueResponse, error) } type platformAdminServiceClient struct { @@ -332,6 +336,16 @@ func (c *platformAdminServiceClient) ListEventSubscriptions(ctx context.Context, return out, nil } +func (c *platformAdminServiceClient) ListPlatformCatalogue(ctx context.Context, in *ListPlatformCatalogueRequest, opts ...grpc.CallOption) (*ListPlatformCatalogueResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListPlatformCatalogueResponse) + err := c.cc.Invoke(ctx, PlatformAdminService_ListPlatformCatalogue_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // PlatformAdminServiceServer is the server API for PlatformAdminService service. // All implementations must embed UnimplementedPlatformAdminServiceServer // for forward compatibility. @@ -386,6 +400,9 @@ type PlatformAdminServiceServer interface { // control-plane subscription metadata do. GetEventOperations(context.Context, *v11.GetEventOperationsRequest) (*v11.GetEventOperationsResponse, error) ListEventSubscriptions(context.Context, *v11.ListEventSubscriptionsRequest) (*v11.ListEventSubscriptionsResponse, error) + // ListPlatformCatalogue lists every composed module and solution with what + // it declares, what runs, and which organizations have it installed. + ListPlatformCatalogue(context.Context, *ListPlatformCatalogueRequest) (*ListPlatformCatalogueResponse, error) mustEmbedUnimplementedPlatformAdminServiceServer() } @@ -462,6 +479,9 @@ func (UnimplementedPlatformAdminServiceServer) GetEventOperations(context.Contex func (UnimplementedPlatformAdminServiceServer) ListEventSubscriptions(context.Context, *v11.ListEventSubscriptionsRequest) (*v11.ListEventSubscriptionsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListEventSubscriptions not implemented") } +func (UnimplementedPlatformAdminServiceServer) ListPlatformCatalogue(context.Context, *ListPlatformCatalogueRequest) (*ListPlatformCatalogueResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListPlatformCatalogue not implemented") +} func (UnimplementedPlatformAdminServiceServer) mustEmbedUnimplementedPlatformAdminServiceServer() {} func (UnimplementedPlatformAdminServiceServer) testEmbeddedByValue() {} @@ -879,6 +899,24 @@ func _PlatformAdminService_ListEventSubscriptions_Handler(srv interface{}, ctx c return interceptor(ctx, in, info, handler) } +func _PlatformAdminService_ListPlatformCatalogue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListPlatformCatalogueRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PlatformAdminServiceServer).ListPlatformCatalogue(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PlatformAdminService_ListPlatformCatalogue_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PlatformAdminServiceServer).ListPlatformCatalogue(ctx, req.(*ListPlatformCatalogueRequest)) + } + return interceptor(ctx, in, info, handler) +} + // PlatformAdminService_ServiceDesc is the grpc.ServiceDesc for PlatformAdminService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -974,6 +1012,10 @@ var PlatformAdminService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListEventSubscriptions", Handler: _PlatformAdminService_ListEventSubscriptions_Handler, }, + { + MethodName: "ListPlatformCatalogue", + Handler: _PlatformAdminService_ListPlatformCatalogue_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "saas/accounts/v1/platform_admin.proto", diff --git a/module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go b/module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go new file mode 100644 index 000000000..40d273923 --- /dev/null +++ b/module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go @@ -0,0 +1,100 @@ +//go:build !pure + +package infra_test + +import ( + "context" + "testing" + "time" + + "accounts/pkg/business" + gen "accounts/pkg/gen/saas/accounts/v1" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// catalogueInstallations reads the platform Catalogue's installation listing the +// way the Service does — under the control plane — keyed by installation id, so +// installations other tests left in the shared database do not matter. +func catalogueInstallations(t *testing.T) map[string]*business.CatalogueInstallationRecord { + t.Helper() + var records []*business.CatalogueInstallationRecord + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + var err error + records, err = testStore.ListCatalogueInstallations(ctx) + return err + })) + byID := make(map[string]*business.CatalogueInstallationRecord, len(records)) + for _, record := range records { + byID[record.Installation.GetId()] = record + } + return byID +} + +func seedCatalogueTeam(t *testing.T, orgID, name string) string { + t.Helper() + teamID := business.NewIDString() + require.NoError(t, testStore.WithOrgTx(testCtx, orgID, func(ctx context.Context) error { + return testStore.CreateTeam(ctx, &gen.Team{ + Id: teamID, OrgId: orgID, Name: name, Slug: "team-" + teamID, Path: "team-" + teamID, + }) + })) + return teamID +} + +func grantCatalogueTeam(t *testing.T, orgID, teamID, roleID, path string, expiresAt *timestamppb.Timestamp) { + t.Helper() + require.NoError(t, testStore.WithOrgTx(testCtx, orgID, func(ctx context.Context) error { + return testStore.GrantScope(ctx, &gen.ScopeGrant{ + Id: business.NewIDString(), OrgId: orgID, SubjectId: teamID, + SubjectKind: gen.SubjectKind_SUBJECT_KIND_TEAM, ScopePath: path, RoleId: roleID, ExpiresAt: expiresAt, + }) + })) +} + +// The Catalogue lists an active installation with its organization's name and +// the release its agent was created for, and exposes exactly the teams an active +// grant at the installation's authority root reaches: not the agent's own +// standing grant, not a team granted elsewhere, and not an expired grant. +func TestListCatalogueInstallationsReadsTeamsReachingTheRoot(t *testing.T) { + orgID, _, roleID, installation, rootPath := installFixture(t, "doc", "read") + + exposed := seedCatalogueTeam(t, orgID, "Exposed Team") + grantCatalogueTeam(t, orgID, exposed, roleID, rootPath, nil) + + elsewhere := seedCatalogueTeam(t, orgID, "Elsewhere Team") + registerNode(t, orgID, "elsewhere", "space", "", "") + grantCatalogueTeam(t, orgID, elsewhere, roleID, "elsewhere", nil) + + expired := seedCatalogueTeam(t, orgID, "Expired Team") + grantCatalogueTeam(t, orgID, expired, roleID, rootPath, timestamppb.New(time.Now().Add(-time.Hour))) + + record := catalogueInstallations(t)[installation.Id] + require.NotNil(t, record, "an active installation is listed") + require.Equal(t, orgID, record.Installation.GetOrgId()) + require.Equal(t, "acme.example/solution", record.Installation.GetSolutionIdentifier()) + require.Equal(t, "Test Org", record.OrgName) + require.Equal(t, "acme.example/solution:1.0.0", record.AgentIdentifier) + + require.Len(t, record.ExposedTeams, 1, "only the active team grant reaching the root exposes a team") + team := record.ExposedTeams[0] + require.Equal(t, exposed, team.GetGrant().GetSubjectId()) + require.Equal(t, gen.SubjectKind_SUBJECT_KIND_TEAM, team.GetGrant().GetSubjectKind()) + require.Equal(t, orgID, team.GetGrant().GetOrgId()) + require.Equal(t, rootPath, team.GetGrant().GetScopePath()) + require.Equal(t, "Exposed Team", team.GetSubjectLabel()) + require.NotEmpty(t, team.GetRoleName()) +} + +// An uninstalled solution is no longer installed anywhere the Catalogue says. +func TestListCatalogueInstallationsOmitsRevoked(t *testing.T) { + orgID, _, _, installation, _ := installFixture(t, "doc", "read") + require.Contains(t, catalogueInstallations(t), installation.Id) + + require.NoError(t, testStore.WithOrgTx(testCtx, orgID, func(ctx context.Context) error { + _, _, err := testStore.UninstallSolution(ctx, orgID, installation.Id) + return err + })) + require.NotContains(t, catalogueInstallations(t), installation.Id) +} diff --git a/module/services/accounts/code/pkg/infra/postgres_installations.go b/module/services/accounts/code/pkg/infra/postgres_installations.go index 950b95f48..3b5fee889 100644 --- a/module/services/accounts/code/pkg/infra/postgres_installations.go +++ b/module/services/accounts/code/pkg/infra/postgres_installations.go @@ -656,4 +656,81 @@ func installationScopeAllowed(ctx context.Context, executor QueryExecutor, orgID return allowed, err } +// ListCatalogueInstallations lists every active installation in every +// organization for the platform Catalogue, with the organization's name, the +// identifier of the agent principal it was installed as, and the active team +// grants at its authority root or above it. It must run under the control plane: +// it reads across tenants. +func (s *PostgresStore) ListCatalogueInstallations(ctx context.Context) ([]*business.CatalogueInstallationRecord, error) { + executor := s.getQueryExecutor(ctx) + rows, err := executor.Query(ctx, `SELECT `+installationColumns+`, org_name, agent_identifier + FROM (SELECT i.*, o.name AS org_name, COALESCE(p.agent_identifier, '') AS agent_identifier + FROM installations i + JOIN organizations o ON o.id = i.org_id + LEFT JOIN principals p ON p.id = i.agent_principal_id + WHERE i.status = 'active') installations + ORDER BY solution_identifier, org_name, id`) + if err != nil { + return nil, fmt.Errorf("list catalogue installations: %w", err) + } + var records []*business.CatalogueInstallationRecord + byID := make(map[string]*business.CatalogueInstallationRecord) + for rows.Next() { + record := &business.CatalogueInstallationRecord{} + installation, err := scanInstallation(trailingColumns{rows, []any{&record.OrgName, &record.AgentIdentifier}}) + if err != nil { + rows.Close() + return nil, fmt.Errorf("scan catalogue installation: %w", err) + } + record.Installation = installation + records = append(records, record) + byID[installation.Id] = record + } + err = rows.Err() + rows.Close() + if err != nil { + return nil, fmt.Errorf("list catalogue installations: %w", err) + } + + grants, err := executor.Query(ctx, `SELECT i.id::text, i.org_id::text, `+labelledGrantColumns+` + FROM installations i + JOIN scope_nodes root ON root.id = i.root_scope_node_id + JOIN scope_grants g ON g.org_id = i.org_id AND g.subject_kind = 'team' + AND g.scope_path @> root.scope_path + AND (g.expires_at IS NULL OR g.expires_at > NOW()) + `+labelledGrantJoins+` + WHERE i.status = 'active' + ORDER BY i.id, g.created_at, g.id`) + if err != nil { + return nil, fmt.Errorf("list catalogue installation teams: %w", err) + } + defer grants.Close() + for grants.Next() { + var installationID, orgID string + view, err := scanLabelledGrant(grants, "", &installationID, &orgID) + if err != nil { + return nil, fmt.Errorf("scan catalogue installation team: %w", err) + } + view.Grant.OrgId = orgID + if record, ok := byID[installationID]; ok { + record.ExposedTeams = append(record.ExposedTeams, view) + } + } + if err := grants.Err(); err != nil { + return nil, fmt.Errorf("list catalogue installation teams: %w", err) + } + return records, nil +} + +// trailingColumns scans columns a query selects after a canonical column list +// into extra destinations, so the canonical scanner stays the only reader of it. +type trailingColumns struct { + rowScanner + extra []any +} + +func (t trailingColumns) Scan(dest ...any) error { + return t.rowScanner.Scan(append(dest, t.extra...)...) +} + var _ business.InstallationStore = (*PostgresStore)(nil) diff --git a/module/services/accounts/code/pkg/infra/postgres_layered_access.go b/module/services/accounts/code/pkg/infra/postgres_layered_access.go index 824fb9516..d94e2f3a0 100644 --- a/module/services/accounts/code/pkg/infra/postgres_layered_access.go +++ b/module/services/accounts/code/pkg/infra/postgres_layered_access.go @@ -735,6 +735,42 @@ func (s *PostgresStore) ListShares(ctx context.Context, orgID, resourceType, res return out, nil } +// labelledGrantColumns and labelledGrantJoins select one scope grant, aliased g, +// with the labels an administrator reads it by: the subject's team or display +// name, the role's name, and who granted it. scanLabelledGrant reads them, after +// any columns a caller selects ahead of them. +const labelledGrantColumns = `g.id, g.subject_id, g.subject_kind, g.scope_path::text, g.role_id, + COALESCE(g.granted_by::text, ''), g.expires_at, g.created_at, + COALESCE(t.name, p.display_name, g.subject_id::text), r.name, + COALESCE(a.display_name, g.granted_by::text, 'Unknown actor')` + +const labelledGrantJoins = `JOIN roles r ON r.id = g.role_id + LEFT JOIN principals p ON g.subject_kind = 'principal' AND p.id = g.subject_id + LEFT JOIN teams t ON g.subject_kind = 'team' AND t.id = g.subject_id + LEFT JOIN principals a ON a.id = g.granted_by` + +func scanLabelledGrant(row rowScanner, orgID string, leading ...any) (*gen.CollectionReadGrant, error) { + view := &gen.CollectionReadGrant{Grant: &gen.ScopeGrant{OrgId: orgID}} + g := view.Grant + var kind string + var expires *time.Time + var created time.Time + dest := append(leading, &g.Id, &g.SubjectId, &kind, &g.ScopePath, &g.RoleId, &g.GrantedBy, + &expires, &created, &view.SubjectLabel, &view.RoleName, &view.ActorLabel) + if err := row.Scan(dest...); err != nil { + return nil, err + } + g.SubjectKind = gen.SubjectKind_SUBJECT_KIND_PRINCIPAL + if kind == "team" { + g.SubjectKind = gen.SubjectKind_SUBJECT_KIND_TEAM + } + g.CreatedAt = timestamppb.New(created) + if expires != nil { + g.ExpiresAt = timestamppb.New(*expires) + } + return view, nil +} + // ListCollectionAccess lists collection boundaries with the grants that confer // read on their content. readResources names the permission resource types that // content is governed by; it comes from the composition's declared module @@ -769,14 +805,8 @@ func (s *PostgresStore) ListCollectionAccess(ctx context.Context, orgID, afterPa return nil, err } for _, collection := range collections { - grants, err := executor.Query(ctx, `SELECT g.id, g.subject_id, g.subject_kind, g.scope_path::text, g.role_id, - COALESCE(g.granted_by::text, ''), g.expires_at, g.created_at, - COALESCE(t.name, p.display_name, g.subject_id::text), r.name, - COALESCE(a.display_name, g.granted_by::text, 'Unknown actor') - FROM scope_grants g JOIN roles r ON r.id = g.role_id - LEFT JOIN principals p ON g.subject_kind = 'principal' AND p.id = g.subject_id - LEFT JOIN teams t ON g.subject_kind = 'team' AND t.id = g.subject_id - LEFT JOIN principals a ON a.id = g.granted_by + grants, err := executor.Query(ctx, `SELECT `+labelledGrantColumns+` + FROM scope_grants g `+labelledGrantJoins+` WHERE g.org_id = $1 AND g.scope_path @> $2::ltree AND (g.expires_at IS NULL OR g.expires_at > NOW()) AND EXISTS (SELECT 1 FROM role_permissions rp WHERE rp.role_id = g.role_id @@ -786,24 +816,11 @@ func (s *PostgresStore) ListCollectionAccess(ctx context.Context, orgID, afterPa return nil, err } for grants.Next() { - view := &gen.CollectionReadGrant{Grant: &gen.ScopeGrant{OrgId: orgID}} - g := view.Grant - var kind string - var expires *time.Time - var created time.Time - if err := grants.Scan(&g.Id, &g.SubjectId, &kind, &g.ScopePath, &g.RoleId, &g.GrantedBy, - &expires, &created, &view.SubjectLabel, &view.RoleName, &view.ActorLabel); err != nil { + view, err := scanLabelledGrant(grants, orgID) + if err != nil { grants.Close() return nil, err } - g.SubjectKind = gen.SubjectKind_SUBJECT_KIND_PRINCIPAL - if kind == "team" { - g.SubjectKind = gen.SubjectKind_SUBJECT_KIND_TEAM - } - g.CreatedAt = timestamppb.New(created) - if expires != nil { - g.ExpiresAt = timestamppb.New(*expires) - } collection.ReadGrants = append(collection.ReadGrants, view) } err = grants.Err() diff --git a/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt b/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt index 0462ee955..7e65a6fa7 100644 --- a/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt +++ b/module/services/accounts/code/pkg/infra/testdata/control_plane_call_sites.txt @@ -97,6 +97,7 @@ pkg/business/platform_admin.go: (*Service).SearchUsers: WithControlPlane pkg/business/platform_admin.go: (*Service).StopImpersonation: WithControlPlane pkg/business/platform_admin.go: (*Service).SuspendUser: WithControlPlane pkg/business/platform_admin.go: (*Service).UnsuspendUser: WithControlPlane +pkg/business/platform_catalogue.go: (*Service).ListPlatformCatalogue: WithControlPlane pkg/business/principals.go: (*Service).GetPrincipal: As(System()) pkg/business/principals.go: (*Service).withPrincipalScope: As(System()) pkg/business/principals.go: (*Service).withPrincipalScope: As(System()) diff --git a/module/services/accounts/generated/authz-methods.json b/module/services/accounts/generated/authz-methods.json index 337480ac9..58179b92a 100644 --- a/module/services/accounts/generated/authz-methods.json +++ b/module/services/accounts/generated/authz-methods.json @@ -7185,6 +7185,43 @@ "policy_sha256": "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "full_service": "saas.accounts.v1.PlatformAdminService", + "method": "ListPlatformCatalogue", + "input_type": "saas.accounts.v1.ListPlatformCatalogueRequest", + "output_type": "saas.accounts.v1.ListPlatformCatalogueResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_SENSITIVE", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "platform_admin", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "saas/accounts/v1/platform_admin.proto", + "policy_sha256": "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "full_service": "saas.accounts.v1.PlatformAdminService", diff --git a/module/services/accounts/generated/gateway-routes.json b/module/services/accounts/generated/gateway-routes.json index 8c0d37b8c..542fd52da 100644 --- a/module/services/accounts/generated/gateway-routes.json +++ b/module/services/accounts/generated/gateway-routes.json @@ -2209,6 +2209,22 @@ "rewrite_path": "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", "remove_after": "2026-10-11" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/customers.PlatformAdminService/ListPlatformCatalogue", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_COMPATIBILITY_ALIAS", + "rewrite_path": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "remove_after": "2026-10-11" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", @@ -5345,6 +5361,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", @@ -7073,6 +7105,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_REST", + "method": "GET", + "path": "/v1/platform/catalogue", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "rest", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_REST", "method": "GET", diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index de400512b..81b089d58 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -3211,6 +3211,38 @@ ] } }, + "/v1/platform/catalogue": { + "get": { + "summary": "ListPlatformCatalogue lists every composed module and solution with what\nit declares, what runs, and which organizations have it installed.", + "operationId": "PlatformAdminService_ListPlatformCatalogue", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListPlatformCatalogueResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "includeTombstoned", + "description": "Include deregistered solutions. Their installations are listed either way.", + "in": "query", + "required": false, + "type": "boolean" + } + ], + "tags": [ + "PlatformAdminService" + ] + } + }, "/v1/platform/events/operations": { "get": { "summary": "Domain-event platform operations (#494 P3). Payload bytes never leave the\ndurable event boundary through these methods — only counts, timings, and\ncontrol-plane subscription metadata do.", @@ -6813,6 +6845,180 @@ } } }, + "v1CatalogueEntry": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/definitions/v1CatalogueEntryKind" + }, + "name": { + "type": "string", + "description": "A module's principal prefix, or a solution's registry id — the identifier\nits installations name." + }, + "publisher": { + "type": "string" + }, + "publisherGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "declaredRelease": { + "$ref": "#/definitions/v1CatalogueRelease" + }, + "declaredReleaseGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "buildDigest": { + "type": "string" + }, + "buildDigestGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "generation": { + "type": "string", + "format": "int64" + }, + "generationGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "running": { + "$ref": "#/definitions/v1CatalogueRunning" + }, + "runningGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "buildSizeGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "registration": { + "$ref": "#/definitions/v1SolutionRegistration", + "description": "The solution's registration, with deployment topology withheld: the\nfrontend manifest, backend upstream and service alias are always empty\nhere, as on every browser-facing projection of the registry. Absent for a\nmodule, which does not self-register, and for a solution known only from\nits installations." + }, + "installations": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1CatalogueInstallation" + } + } + }, + "description": "CatalogueEntry is one module or solution." + }, + "v1CatalogueEntryKind": { + "type": "string", + "enum": [ + "CATALOGUE_ENTRY_KIND_UNSPECIFIED", + "CATALOGUE_ENTRY_KIND_MODULE", + "CATALOGUE_ENTRY_KIND_SOLUTION" + ], + "default": "CATALOGUE_ENTRY_KIND_UNSPECIFIED", + "description": "CatalogueEntryKind distinguishes a composed module from a solution.\n\n - CATALOGUE_ENTRY_KIND_MODULE: A module composed into this deployment, known by its module principal.\n - CATALOGUE_ENTRY_KIND_SOLUTION: A solution, known by its registration, its installations, or both." + }, + "v1CatalogueExecution": { + "type": "object", + "properties": { + "imageDigest": { + "type": "string" + }, + "buildIncarnation": { + "type": "string", + "format": "uint64" + } + }, + "description": "CatalogueExecution is what runs: an OCI image manifest digest and the build\nincarnation the host assigned to it, the same pair a Work Context seals." + }, + "v1CatalogueGap": { + "type": "object", + "properties": { + "reason": { + "$ref": "#/definitions/v1CatalogueGapReason" + }, + "detail": { + "type": "string", + "description": "Operator-facing: names the record or producer that would supply the fact." + } + }, + "description": "CatalogueGap is a Catalogue fact this host cannot state, with the reason." + }, + "v1CatalogueGapReason": { + "type": "string", + "enum": [ + "CATALOGUE_GAP_REASON_UNSPECIFIED", + "CATALOGUE_GAP_REASON_NOT_RECORDED", + "CATALOGUE_GAP_REASON_NOT_REPORTED", + "CATALOGUE_GAP_REASON_NOT_OBSERVED" + ], + "default": "CATALOGUE_GAP_REASON_UNSPECIFIED", + "description": "CatalogueGapReason says why a Catalogue fact has no value.\n\n - CATALOGUE_GAP_REASON_NOT_RECORDED: This host keeps no record that carries the fact.\n - CATALOGUE_GAP_REASON_NOT_REPORTED: The fact is a section of the producer's document, and the document this\nhost holds carries no such section (an older producer).\n - CATALOGUE_GAP_REASON_NOT_OBSERVED: The fact needs observed cluster state, and this host has none to read —\na local run with no cluster, or an observation that is missing or stale." + }, + "v1CatalogueInstallation": { + "type": "object", + "properties": { + "installation": { + "$ref": "#/definitions/v1Installation" + }, + "orgName": { + "type": "string" + }, + "agentRelease": { + "$ref": "#/definitions/v1CatalogueRelease" + }, + "agentReleaseGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "revisionGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "exposedTeams": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1CollectionReadGrant" + }, + "description": "Active team grants at the installation's authority root or above it, so\nevery team whose members the grant reaches inside this installation." + } + }, + "description": "CatalogueInstallation is one organization's installation of a solution." + }, + "v1CatalogueRelease": { + "type": "object", + "properties": { + "publisher": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "description": "CatalogueRelease is a declared release, publisher/name@version." + }, + "v1CatalogueRunning": { + "type": "object", + "properties": { + "declared": { + "$ref": "#/definitions/v1CatalogueExecution" + }, + "observed": { + "$ref": "#/definitions/v1CatalogueExecution" + }, + "verdict": { + "$ref": "#/definitions/v1CatalogueRunningVerdict" + } + }, + "description": "CatalogueRunning is the running incarnation beside the declared one." + }, + "v1CatalogueRunningVerdict": { + "type": "string", + "enum": [ + "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", + "CATALOGUE_RUNNING_VERDICT_MATCHES", + "CATALOGUE_RUNNING_VERDICT_DIFFERS" + ], + "default": "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", + "description": "CatalogueRunningVerdict compares the observed execution with the declared\none. It exists only when both are known; anything less is a CatalogueGap.\n\n - CATALOGUE_RUNNING_VERDICT_MATCHES: The observed image digest and incarnation are the declared ones.\n - CATALOGUE_RUNNING_VERDICT_DIFFERS: Something other than the declared execution is running — what cluster\nadmission refuses." + }, "v1ClientAuthorizationRequest": { "type": "object", "properties": { @@ -8806,6 +9012,24 @@ } } }, + "v1ListPlatformCatalogueResponse": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1CatalogueEntry" + }, + "description": "Modules first, then solutions, each ordered by name." + }, + "registryRevision": { + "type": "string", + "format": "int64", + "description": "The registry revision the solution entries were read at." + } + } + }, "v1ListPrincipalsResponse": { "type": "object", "properties": { @@ -10229,6 +10453,100 @@ } } }, + "v1SolutionBackendBinding": { + "type": "object", + "properties": { + "revision": { + "type": "string", + "format": "int64" + }, + "upstream": { + "type": "string" + }, + "serviceAlias": { + "type": "string" + }, + "contractVersion": { + "type": "string" + }, + "leaseExpiresAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "SolutionBackendBinding is the stored backend half: the upstream the gateway\nproxies to and the logical service alias it is addressed by." + }, + "v1SolutionFrontendBinding": { + "type": "object", + "properties": { + "revision": { + "type": "string", + "format": "int64" + }, + "manifest": { + "type": "string" + }, + "contractVersion": { + "type": "string" + }, + "leaseExpiresAt": { + "type": "string", + "format": "date-time" + } + }, + "description": "SolutionFrontendBinding is the stored frontend half. manifest is the document\nthe frontend validated before handing it over; this service persists it\nverbatim and never reinterprets it, so the host keeps sole ownership of what\na manifest may contain." + }, + "v1SolutionRegistration": { + "type": "object", + "properties": { + "solutionId": { + "type": "string" + }, + "publisher": { + "type": "string", + "description": "Owner of record. First claim binds it; a registration naming a different\npublisher is refused rather than overwriting." + }, + "revision": { + "type": "string", + "format": "int64" + }, + "status": { + "$ref": "#/definitions/v1SolutionRegistrationStatus" + }, + "frontend": { + "$ref": "#/definitions/v1SolutionFrontendBinding" + }, + "backend": { + "$ref": "#/definitions/v1SolutionBackendBinding" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "tombstonedAt": { + "type": "string", + "format": "date-time" + }, + "runtimeBoundary": { + "type": "string", + "description": "The seed a solution's runtime boundary is derived from (issue #1015).\n\nALWAYS EMPTY on the wire, on every response, deliberately. The seed is\nassigned by this service when the record is created and never again, and it\nis the only input to the per-organization boundary every Work Context\nminted for that solution is sealed under — so it is what stands between one\nsolution's runs and another's. No registrant needs it: accounts derives and\nseals the boundary from the credential the solution already presents.\n\nThe field is kept rather than removed so this promise is testable: a change\nthat starts populating it fails a test instead of shipping." + } + }, + "description": "SolutionRegistration is the canonical record. revision is drawn from one\nregistry-wide sequence, so revisions are comparable across records and a\nconsumer can order two snapshots without a wall clock." + }, + "v1SolutionRegistrationStatus": { + "type": "string", + "enum": [ + "SOLUTION_REGISTRATION_STATUS_UNSPECIFIED", + "SOLUTION_REGISTRATION_STATUS_ACTIVE", + "SOLUTION_REGISTRATION_STATUS_PENDING", + "SOLUTION_REGISTRATION_STATUS_EXPIRED", + "SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE", + "SOLUTION_REGISTRATION_STATUS_TOMBSTONED" + ], + "default": "SOLUTION_REGISTRATION_STATUS_UNSPECIFIED", + "description": "SolutionRegistrationStatus is derived at read time from the stored record; it\nis never a stored column. It is what lets an operator tell a registration\nthat was never completed from one whose publisher stopped renewing, and both\nfrom one that was deliberately removed.\n\n - SOLUTION_REGISTRATION_STATUS_ACTIVE: Both halves are present, their leases are live, and their declared contract\nversions agree. This is the only status either surface serves.\n - SOLUTION_REGISTRATION_STATUS_PENDING: A half has never registered. The record is durable but incomplete, so no\npage advertises it and the gateway has nothing to route to.\n - SOLUTION_REGISTRATION_STATUS_EXPIRED: Both halves registered, but at least one lease lapsed: the deployment that\nowns it stopped renewing. The intent survives; the endpoint is not live.\n - SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE: The two halves declare different contract versions, so activating them\ntogether would serve a page against a backend it was not built for.\n - SOLUTION_REGISTRATION_STATUS_TOMBSTONED: The registration was deregistered. The row survives as a tombstone so a\ndelayed heartbeat from a retiring deployment cannot recreate it." + }, "v1StartChildSessionWorkContextRequest": { "type": "object", "properties": { diff --git a/module/services/accounts/generated/rest-surface.json b/module/services/accounts/generated/rest-surface.json index eebb0c648..1e16000cd 100644 --- a/module/services/accounts/generated/rest-surface.json +++ b/module/services/accounts/generated/rest-surface.json @@ -805,6 +805,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_REST", + "method": "GET", + "path": "/v1/platform/catalogue", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "rest", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_REST", "method": "GET", diff --git a/module/services/accounts/generated/service-catalog.json b/module/services/accounts/generated/service-catalog.json index fe186fe3a..bacfcb956 100644 --- a/module/services/accounts/generated/service-catalog.json +++ b/module/services/accounts/generated/service-catalog.json @@ -324,6 +324,7 @@ "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags", "/saas.accounts.v1.PlatformAdminService/ListJobs", "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", + "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/ReplayJob", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", @@ -9502,6 +9503,57 @@ "service": "accounts" } }, + { + "procedure": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", + "service": "PlatformAdminService", + "full_service": "saas.accounts.v1.PlatformAdminService", + "method": "ListPlatformCatalogue", + "input_type": "saas.accounts.v1.ListPlatformCatalogueRequest", + "output_type": "saas.accounts.v1.ListPlatformCatalogueResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "saas/accounts/v1/platform_admin.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT", + "PROTOCOL_REST" + ], + "http_bindings": [ + { + "method": "GET", + "path": "/v1/platform/catalogue", + "body": "", + "response_body": "" + } + ], + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_FORBIDDEN", + "rate_limit": "RATE_LIMIT_CLASS_SENSITIVE", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_UNSPECIFIED" + }, + "policy_tier": "platform_admin", + "description": "List every composed module and solution with its declared, running and installed state.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "service": "PlatformAdminService", diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 6787b030e..b8bef02ad 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -834,6 +834,180 @@ }, "type": "object" }, + "v1CatalogueEntry": { + "description": "CatalogueEntry is one module or solution.", + "properties": { + "buildDigest": { + "type": "string" + }, + "buildDigestGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "buildSizeGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "declaredRelease": { + "$ref": "#/definitions/v1CatalogueRelease" + }, + "declaredReleaseGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "generation": { + "format": "int64", + "type": "string" + }, + "generationGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "installations": { + "items": { + "$ref": "#/definitions/v1CatalogueInstallation", + "type": "object" + }, + "type": "array" + }, + "kind": { + "$ref": "#/definitions/v1CatalogueEntryKind" + }, + "name": { + "description": "A module's principal prefix, or a solution's registry id — the identifier\nits installations name.", + "type": "string" + }, + "publisher": { + "type": "string" + }, + "publisherGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "registration": { + "$ref": "#/definitions/v1SolutionRegistration", + "description": "The solution's registration, with deployment topology withheld: the\nfrontend manifest, backend upstream and service alias are always empty\nhere, as on every browser-facing projection of the registry. Absent for a\nmodule, which does not self-register, and for a solution known only from\nits installations." + }, + "running": { + "$ref": "#/definitions/v1CatalogueRunning" + }, + "runningGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "type": "object" + }, + "v1CatalogueEntryKind": { + "default": "CATALOGUE_ENTRY_KIND_UNSPECIFIED", + "description": "CatalogueEntryKind distinguishes a composed module from a solution.\n\n - CATALOGUE_ENTRY_KIND_MODULE: A module composed into this deployment, known by its module principal.\n - CATALOGUE_ENTRY_KIND_SOLUTION: A solution, known by its registration, its installations, or both.", + "enum": [ + "CATALOGUE_ENTRY_KIND_UNSPECIFIED", + "CATALOGUE_ENTRY_KIND_MODULE", + "CATALOGUE_ENTRY_KIND_SOLUTION" + ], + "type": "string" + }, + "v1CatalogueExecution": { + "description": "CatalogueExecution is what runs: an OCI image manifest digest and the build\nincarnation the host assigned to it, the same pair a Work Context seals.", + "properties": { + "buildIncarnation": { + "format": "uint64", + "type": "string" + }, + "imageDigest": { + "type": "string" + } + }, + "type": "object" + }, + "v1CatalogueGap": { + "description": "CatalogueGap is a Catalogue fact this host cannot state, with the reason.", + "properties": { + "detail": { + "description": "Operator-facing: names the record or producer that would supply the fact.", + "type": "string" + }, + "reason": { + "$ref": "#/definitions/v1CatalogueGapReason" + } + }, + "type": "object" + }, + "v1CatalogueGapReason": { + "default": "CATALOGUE_GAP_REASON_UNSPECIFIED", + "description": "CatalogueGapReason says why a Catalogue fact has no value.\n\n - CATALOGUE_GAP_REASON_NOT_RECORDED: This host keeps no record that carries the fact.\n - CATALOGUE_GAP_REASON_NOT_REPORTED: The fact is a section of the producer's document, and the document this\nhost holds carries no such section (an older producer).\n - CATALOGUE_GAP_REASON_NOT_OBSERVED: The fact needs observed cluster state, and this host has none to read —\na local run with no cluster, or an observation that is missing or stale.", + "enum": [ + "CATALOGUE_GAP_REASON_UNSPECIFIED", + "CATALOGUE_GAP_REASON_NOT_RECORDED", + "CATALOGUE_GAP_REASON_NOT_REPORTED", + "CATALOGUE_GAP_REASON_NOT_OBSERVED" + ], + "type": "string" + }, + "v1CatalogueInstallation": { + "description": "CatalogueInstallation is one organization's installation of a solution.", + "properties": { + "agentRelease": { + "$ref": "#/definitions/v1CatalogueRelease" + }, + "agentReleaseGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "exposedTeams": { + "description": "Active team grants at the installation's authority root or above it, so\nevery team whose members the grant reaches inside this installation.", + "items": { + "$ref": "#/definitions/v1CollectionReadGrant", + "type": "object" + }, + "type": "array" + }, + "installation": { + "$ref": "#/definitions/v1Installation" + }, + "orgName": { + "type": "string" + }, + "revisionGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "type": "object" + }, + "v1CatalogueRelease": { + "description": "CatalogueRelease is a declared release, publisher/name@version.", + "properties": { + "name": { + "type": "string" + }, + "publisher": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "type": "object" + }, + "v1CatalogueRunning": { + "description": "CatalogueRunning is the running incarnation beside the declared one.", + "properties": { + "declared": { + "$ref": "#/definitions/v1CatalogueExecution" + }, + "observed": { + "$ref": "#/definitions/v1CatalogueExecution" + }, + "verdict": { + "$ref": "#/definitions/v1CatalogueRunningVerdict" + } + }, + "type": "object" + }, + "v1CatalogueRunningVerdict": { + "default": "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", + "description": "CatalogueRunningVerdict compares the observed execution with the declared\none. It exists only when both are known; anything less is a CatalogueGap.\n\n - CATALOGUE_RUNNING_VERDICT_MATCHES: The observed image digest and incarnation are the declared ones.\n - CATALOGUE_RUNNING_VERDICT_DIFFERS: Something other than the declared execution is running — what cluster\nadmission refuses.", + "enum": [ + "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", + "CATALOGUE_RUNNING_VERDICT_MATCHES", + "CATALOGUE_RUNNING_VERDICT_DIFFERS" + ], + "type": "string" + }, "v1ClientAuthorizationRequest": { "description": "ClientAuthorizationRequest is the authorization request a client encodes into\nthe host's login-page URL. code_challenge is the S256 hash of a verifier the\nclient keeps, so an intercepted code cannot be redeemed by anyone else.", "properties": { @@ -2827,6 +3001,24 @@ }, "type": "object" }, + "v1ListPlatformCatalogueResponse": { + "properties": { + "entries": { + "description": "Modules first, then solutions, each ordered by name.", + "items": { + "$ref": "#/definitions/v1CatalogueEntry", + "type": "object" + }, + "type": "array" + }, + "registryRevision": { + "description": "The registry revision the solution entries were read at.", + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, "v1ListPrincipalsResponse": { "properties": { "nextPageToken": { @@ -4250,6 +4442,100 @@ }, "type": "object" }, + "v1SolutionBackendBinding": { + "description": "SolutionBackendBinding is the stored backend half: the upstream the gateway\nproxies to and the logical service alias it is addressed by.", + "properties": { + "contractVersion": { + "type": "string" + }, + "leaseExpiresAt": { + "format": "date-time", + "type": "string" + }, + "revision": { + "format": "int64", + "type": "string" + }, + "serviceAlias": { + "type": "string" + }, + "upstream": { + "type": "string" + } + }, + "type": "object" + }, + "v1SolutionFrontendBinding": { + "description": "SolutionFrontendBinding is the stored frontend half. manifest is the document\nthe frontend validated before handing it over; this service persists it\nverbatim and never reinterprets it, so the host keeps sole ownership of what\na manifest may contain.", + "properties": { + "contractVersion": { + "type": "string" + }, + "leaseExpiresAt": { + "format": "date-time", + "type": "string" + }, + "manifest": { + "type": "string" + }, + "revision": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "v1SolutionRegistration": { + "description": "SolutionRegistration is the canonical record. revision is drawn from one\nregistry-wide sequence, so revisions are comparable across records and a\nconsumer can order two snapshots without a wall clock.", + "properties": { + "backend": { + "$ref": "#/definitions/v1SolutionBackendBinding" + }, + "frontend": { + "$ref": "#/definitions/v1SolutionFrontendBinding" + }, + "publisher": { + "description": "Owner of record. First claim binds it; a registration naming a different\npublisher is refused rather than overwriting.", + "type": "string" + }, + "revision": { + "format": "int64", + "type": "string" + }, + "runtimeBoundary": { + "description": "The seed a solution's runtime boundary is derived from (issue #1015).\n\nALWAYS EMPTY on the wire, on every response, deliberately. The seed is\nassigned by this service when the record is created and never again, and it\nis the only input to the per-organization boundary every Work Context\nminted for that solution is sealed under — so it is what stands between one\nsolution's runs and another's. No registrant needs it: accounts derives and\nseals the boundary from the credential the solution already presents.\n\nThe field is kept rather than removed so this promise is testable: a change\nthat starts populating it fails a test instead of shipping.", + "type": "string" + }, + "solutionId": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/v1SolutionRegistrationStatus" + }, + "tombstonedAt": { + "format": "date-time", + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, + "v1SolutionRegistrationStatus": { + "default": "SOLUTION_REGISTRATION_STATUS_UNSPECIFIED", + "description": "SolutionRegistrationStatus is derived at read time from the stored record; it\nis never a stored column. It is what lets an operator tell a registration\nthat was never completed from one whose publisher stopped renewing, and both\nfrom one that was deliberately removed.\n\n - SOLUTION_REGISTRATION_STATUS_ACTIVE: Both halves are present, their leases are live, and their declared contract\nversions agree. This is the only status either surface serves.\n - SOLUTION_REGISTRATION_STATUS_PENDING: A half has never registered. The record is durable but incomplete, so no\npage advertises it and the gateway has nothing to route to.\n - SOLUTION_REGISTRATION_STATUS_EXPIRED: Both halves registered, but at least one lease lapsed: the deployment that\nowns it stopped renewing. The intent survives; the endpoint is not live.\n - SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE: The two halves declare different contract versions, so activating them\ntogether would serve a page against a backend it was not built for.\n - SOLUTION_REGISTRATION_STATUS_TOMBSTONED: The registration was deregistered. The row survives as a tombstone so a\ndelayed heartbeat from a retiring deployment cannot recreate it.", + "enum": [ + "SOLUTION_REGISTRATION_STATUS_UNSPECIFIED", + "SOLUTION_REGISTRATION_STATUS_ACTIVE", + "SOLUTION_REGISTRATION_STATUS_PENDING", + "SOLUTION_REGISTRATION_STATUS_EXPIRED", + "SOLUTION_REGISTRATION_STATUS_INCOMPATIBLE", + "SOLUTION_REGISTRATION_STATUS_TOMBSTONED" + ], + "type": "string" + }, "v1StartChildSessionWorkContextRequest": { "properties": { "actorPrincipalId": { @@ -8225,6 +8511,38 @@ ] } }, + "/v1/platform/catalogue": { + "get": { + "operationId": "PlatformAdminService_ListPlatformCatalogue", + "parameters": [ + { + "description": "Include deregistered solutions. Their installations are listed either way.", + "in": "query", + "name": "includeTombstoned", + "required": false, + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ListPlatformCatalogueResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "summary": "ListPlatformCatalogue lists every composed module and solution with what\nit declares, what runs, and which organizations have it installed.", + "tags": [ + "PlatformAdminService" + ] + } + }, "/v1/platform/events/operations": { "get": { "operationId": "PlatformAdminService_GetEventOperations", diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index 9a4c2a755..8c79d0188 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -6,7 +6,10 @@ import "buf/validate/validate.proto"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; +import "saas/accounts/v1/authorization.proto"; import "saas/accounts/v1/common.proto"; +import "saas/accounts/v1/installations.proto"; +import "saas/accounts/v1/solution_registry.proto"; import "saas/events/v1/operations.proto"; import "saas/jobs/v1/jobs.proto"; import "saas/policy/v1/options.proto"; @@ -236,6 +239,161 @@ message UpsertFeatureFlagResponse { string name = 1; } +// ============================================================================ +// Platform Catalogue (issue #1020) +// +// The operator's read of what is deployed: every composed module and every +// solution, beside what it declares, what is running, and which organizations +// have it installed. It is a projection over records this host already keeps — +// the durable solution registry, the installations table, and the composition's +// module principal registry — and it adds no store of its own. +// +// Several facts the Catalogue is specified to show are carried by records this +// host does not hold yet: the declared release, build digest and generation +// arrive with applied presence documents, the running incarnation needs observed +// cluster state, and build size is a signed section of the presence document +// (codefly-dev/core#708, computed by the CLI at build). Each such fact is a +// oneof of its value and a CatalogueGap, so an absent fact is reported with its +// reason and is never an empty value a reader could take for zero, unset, or a +// match. A fact gains its value case when the record that carries it exists. +// ============================================================================ + +// CatalogueEntryKind distinguishes a composed module from a solution. +enum CatalogueEntryKind { + CATALOGUE_ENTRY_KIND_UNSPECIFIED = 0; + // A module composed into this deployment, known by its module principal. + CATALOGUE_ENTRY_KIND_MODULE = 1; + // A solution, known by its registration, its installations, or both. + CATALOGUE_ENTRY_KIND_SOLUTION = 2; +} + +// CatalogueGapReason says why a Catalogue fact has no value. +enum CatalogueGapReason { + CATALOGUE_GAP_REASON_UNSPECIFIED = 0; + // This host keeps no record that carries the fact. + CATALOGUE_GAP_REASON_NOT_RECORDED = 1; + // The fact is a section of the producer's document, and the document this + // host holds carries no such section (an older producer). + CATALOGUE_GAP_REASON_NOT_REPORTED = 2; + // The fact needs observed cluster state, and this host has none to read — + // a local run with no cluster, or an observation that is missing or stale. + CATALOGUE_GAP_REASON_NOT_OBSERVED = 3; +} + +// CatalogueGap is a Catalogue fact this host cannot state, with the reason. +message CatalogueGap { + CatalogueGapReason reason = 1; + // Operator-facing: names the record or producer that would supply the fact. + string detail = 2; +} + +// CatalogueRelease is a declared release, publisher/name@version. +message CatalogueRelease { + string publisher = 1; + string name = 2; + string version = 3; +} + +// CatalogueExecution is what runs: an OCI image manifest digest and the build +// incarnation the host assigned to it, the same pair a Work Context seals. +message CatalogueExecution { + string image_digest = 1; + uint64 build_incarnation = 2; +} + +// CatalogueRunningVerdict compares the observed execution with the declared +// one. It exists only when both are known; anything less is a CatalogueGap. +enum CatalogueRunningVerdict { + CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; + // The observed image digest and incarnation are the declared ones. + CATALOGUE_RUNNING_VERDICT_MATCHES = 1; + // Something other than the declared execution is running — what cluster + // admission refuses. + CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; +} + +// CatalogueRunning is the running incarnation beside the declared one. +message CatalogueRunning { + CatalogueExecution declared = 1; + CatalogueExecution observed = 2; + CatalogueRunningVerdict verdict = 3; +} + +// CatalogueInstallation is one organization's installation of a solution. +message CatalogueInstallation { + Installation installation = 1; + string org_name = 2; + // The release the installation's agent principal was created for, read from + // its canonical publisher/name:version identifier. + oneof agent_release_value { + CatalogueRelease agent_release = 3; + CatalogueGap agent_release_gap = 4; + } + // The installation revision the installed generation is pinned at. + oneof revision_value { + CatalogueGap revision_gap = 5; + } + // Active team grants at the installation's authority root or above it, so + // every team whose members the grant reaches inside this installation. + repeated CollectionReadGrant exposed_teams = 6; +} + +// CatalogueEntry is one module or solution. +message CatalogueEntry { + CatalogueEntryKind kind = 1; + // A module's principal prefix, or a solution's registry id — the identifier + // its installations name. + string name = 2; + // The owner of record: a solution registration's publisher. + oneof publisher_value { + string publisher = 3; + CatalogueGap publisher_gap = 4; + } + oneof declared_release_value { + CatalogueRelease declared_release = 5; + CatalogueGap declared_release_gap = 6; + } + // The OCI image manifest digest the entry was published at. + oneof build_digest_value { + string build_digest = 7; + CatalogueGap build_digest_gap = 8; + } + // The presence generation this host applied. + oneof generation_value { + int64 generation = 9; + CatalogueGap generation_gap = 10; + } + oneof running_value { + CatalogueRunning running = 11; + CatalogueGap running_gap = 12; + } + // Lines of code, backend and frontend, vendored paths excluded. The value + // case is the presence document's build_size section (codefly-dev/core#708) + // as delivered; this host counts nothing itself. + oneof build_size_value { + CatalogueGap build_size_gap = 13; + } + // The solution's registration, with deployment topology withheld: the + // frontend manifest, backend upstream and service alias are always empty + // here, as on every browser-facing projection of the registry. Absent for a + // module, which does not self-register, and for a solution known only from + // its installations. + optional SolutionRegistration registration = 14; + repeated CatalogueInstallation installations = 15; +} + +message ListPlatformCatalogueRequest { + // Include deregistered solutions. Their installations are listed either way. + bool include_tombstoned = 1; +} + +message ListPlatformCatalogueResponse { + // Modules first, then solutions, each ordered by name. + repeated CatalogueEntry entries = 1; + // The registry revision the solution entries were read at. + int64 registry_revision = 2; +} + // PlatformAdminService — cross-tenant operations for platform operators. // All RPCs require a platform_role in the JWT claims. service PlatformAdminService { @@ -643,4 +801,21 @@ service PlatformAdminService { }; option (google.api.http) = {get: "/v1/platform/events/subscriptions"}; } + + // ListPlatformCatalogue lists every composed module and solution with what + // it declares, what runs, and which organizations have it installed. + rpc ListPlatformCatalogue(ListPlatformCatalogueRequest) returns (ListPlatformCatalogueResponse) { + option (saas.policy.v1.method_policy) = { + exposure: EXPOSURE_AUTHENTICATED + tenant: TENANT_REQUIREMENT_NONE + mfa: MFA_REQUIREMENT_NONE + audit: {emission: AUDIT_EMISSION_NONE} + idempotency: IDEMPOTENCY_REQUIREMENT_FORBIDDEN + rate_limit: RATE_LIMIT_CLASS_SENSITIVE + request_sensitivity: SENSITIVITY_CONFIDENTIAL + response_sensitivity: SENSITIVITY_CONFIDENTIAL + platform_role: PLATFORM_ROLE_REQUIREMENT_SUPER_ADMIN + }; + option (google.api.http) = {get: "/v1/platform/catalogue"}; + } } diff --git a/module/services/auth-gateway/code/authz_catalog_gen.go b/module/services/auth-gateway/code/authz_catalog_gen.go index d2e020e2d..0496fef26 100644 --- a/module/services/auth-gateway/code/authz_catalog_gen.go +++ b/module/services/auth-gateway/code/authz_catalog_gen.go @@ -186,6 +186,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3"}, "/saas.accounts.v1.PlatformAdminService/ListJobs": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3"}, "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3"}, + "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "e3ae6ee3f1b70dbabb131e3a486c403859c0f3540c61dca8132317bd1be9d7b3"}, "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "3665682c29d02878db8cbfd5fee73da95fb61edf4b3a9329fcb6c13c5decaf18"}, "/saas.accounts.v1.PlatformAdminService/ReplayJob": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "ca332de30ec2ccedf0bc703de994eb50cb7f2055c2574c330b0cb421134caf25"}, "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassSensitive, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "5d13d1bec715a08e7cac379958c4e6ad88776a9cac020912bad2c3724e58f514"}, @@ -360,6 +361,7 @@ var generatedRESTProcedureByRoute = map[string]string{ "GET /v1/platform/feature-flags": "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags", "GET /v1/platform/jobs": "/saas.accounts.v1.PlatformAdminService/ListJobs", "GET /v1/platform/admins": "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", + "GET /v1/platform/catalogue": "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", "POST /v1/platform/organizations/{org_id}/entitlements": "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "POST /v1/platform/jobs/{source_job_id}:replay": "/saas.accounts.v1.PlatformAdminService/ReplayJob", "DELETE /v1/platform/admins/{user_id}": "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", diff --git a/module/services/auth-gateway/code/routing_catalog_gen.go b/module/services/auth-gateway/code/routing_catalog_gen.go index 4dbcd2c0b..8e8a3dba0 100644 --- a/module/services/auth-gateway/code/routing_catalog_gen.go +++ b/module/services/auth-gateway/code/routing_catalog_gen.go @@ -144,6 +144,7 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/ListFeatureFlags", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags", Procedure: "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/ListJobs", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/ListJobs", Procedure: "/saas.accounts.v1.PlatformAdminService/ListJobs"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/ListPlatformAdmins", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", Procedure: "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins"}, + {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/ListPlatformCatalogue", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", Procedure: "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/OverrideEntitlement", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", Procedure: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/ReplayJob", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/ReplayJob", Procedure: "/saas.accounts.v1.PlatformAdminService/ReplayJob"}, {Service: "accounts_connect", Method: "POST", Path: "/customers.PlatformAdminService/RevokePlatformRole", UpstreamPath: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", Procedure: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole"}, @@ -340,6 +341,7 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/ListJobs", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/ListJobs"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins"}, + {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/ReplayJob", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/ReplayJob"}, {Service: "accounts_connect", Method: "POST", Path: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", UpstreamPath: "", Procedure: "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole"}, diff --git a/module/services/auth-gateway/code/routing_rest_catalog_gen.go b/module/services/auth-gateway/code/routing_rest_catalog_gen.go index 79aca6bcf..8bf5e2084 100644 --- a/module/services/auth-gateway/code/routing_rest_catalog_gen.go +++ b/module/services/auth-gateway/code/routing_rest_catalog_gen.go @@ -54,6 +54,7 @@ func generatedCatalogRESTRoutes() []*RouteEntry { {Service: "accounts", Method: "GET", Path: "/v1/organizations/{organization_id}/usage/{meter}", Procedure: "/saas.accounts.v1.UsageService/GetUsage"}, {Service: "accounts", Method: "GET", Path: "/v1/organizations/{organization_id}/usage/{meter}/history", Procedure: "/saas.accounts.v1.UsageService/GetUsageHistory"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/admins", Procedure: "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins"}, + {Service: "accounts", Method: "GET", Path: "/v1/platform/catalogue", Procedure: "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/events/operations", Procedure: "/saas.accounts.v1.PlatformAdminService/GetEventOperations"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/events/subscriptions", Procedure: "/saas.accounts.v1.PlatformAdminService/ListEventSubscriptions"}, {Service: "accounts", Method: "GET", Path: "/v1/platform/feature-flags", Procedure: "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags"}, diff --git a/module/services/frontend/AGENTS.md b/module/services/frontend/AGENTS.md index ad8dede4a..cfebe5267 100644 --- a/module/services/frontend/AGENTS.md +++ b/module/services/frontend/AGENTS.md @@ -75,6 +75,13 @@ activating a remote. token. - The dashboard graph is on none of them: the solution page reads it in-process through `findSolution`. +- The platform Catalogue (`/admin/platform/catalogue`) is not one of them either: + it reads accounts' `PlatformAdminService.ListPlatformCatalogue`, super + administrators only, and that response carries each registration's status, + revisions, contract versions and leases with the manifest, upstream and service + alias withheld (`../accounts/AGENTS.md`). A fact the host has no record of is + rendered as its gap reason — "Not recorded", "Not observed" — never as an empty + cell, a zero or a match. ## Loading a remote, and its CSP diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index c821156a8..7f593c86d 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:4b71d34bfccf54f2d734ad31023c08c7a00154c3067f69954924cbbb07337565", + "digest": "sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048", "services": [ { "name": "APIKeyService", @@ -329,6 +329,7 @@ "/saas.accounts.v1.PlatformAdminService/ListFeatureFlags", "/saas.accounts.v1.PlatformAdminService/ListJobs", "/saas.accounts.v1.PlatformAdminService/ListPlatformAdmins", + "/saas.accounts.v1.PlatformAdminService/ListPlatformCatalogue", "/saas.accounts.v1.PlatformAdminService/OverrideEntitlement", "/saas.accounts.v1.PlatformAdminService/ReplayJob", "/saas.accounts.v1.PlatformAdminService/RevokePlatformRole", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index d7db149dd1f5da7900a177c0b6a8b4d3e65eabe0..e1cd68f066d2eb1eee8e8b3f660cbc6817ce6b1a 100644 GIT binary patch delta 23468 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 Q)8VgXcOU+0zcV-dA9Dm)pa1{> delta 13547 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

Kg?PZYG+-197@zLd6BwTf7%?SU zPUKf`y`#9C;-j*xco`zA<Z-xUvg61ozTK@HeB`~g_ z;Xia36JUIfXpP5+#(2%yi`Qfn#Y5CT1&U&!@k&oI<=-dJ6&P0qjAAjy=L1IZfboR@ zLo8r?F)T*0K=IFkqFA8#Qm>AnxElQZZ3a`KwPCx(dImGLvhCJd$Z9QbxAxYJe}Y+0 z`t6J&{~OGDe%bA=+JM&Uscxv3ptzonJk3#Tpd^i6Cfbr=Y_J$#rg%$+Q9Oj*5HN~` z(64xmIV~0_zDm(S?n9_pvZ-z){2zr~q1Hs-O|q%lFQCN)+t&j%vB36?z*a1<-Q24c z*uELqN-MDaE3Z&{cwnB}yop5}!EVVyNrv z4vPP}*+KdcB7^JF2$c@ zw8aDM_X2IP5P4^yEf#3Mzt0XT9xvJt!lD%qVE+;Lhy}182C!lQ>_-8tSOB}LS4V*T zIK29p5v89JjdMF_qg%9}Qhb7gYAs~-DRq9wEn57e&2G}&dB215zuN4kNPa@cyWWJ> zyQ$xaXwAR4HJK#8n@*Xe9L6SK+(UFl-d?rv1~*xbdnmpl@9$dTA@1jaqgaUjMc^nF zIPMMazF3U$%YaclVEii35DOUpi6h?sswf^fejPZ91&&?$?2;xHI8MrE?>?qPlk@aF zhq1|FoSctObQrCLtS09>zxyB2`4cBxIN|Wp@`0s}o+;bQgFAYDR9}95=#+VMx10x=HcK`qY diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index 472a553bf..be13e1f17 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:4b71d34bfccf54f2d734ad31023c08c7a00154c3067f69954924cbbb07337565 + contract-digest: sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index ea99a984a..ea62b0fad 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -2,14 +2,20 @@ // @generated from file saas/accounts/v1/platform_admin.proto (package saas.accounts.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; -import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { CollectionReadGrant } from "./authorization_pb"; +import { file_saas_accounts_v1_authorization } from "./authorization_pb"; import type { Organization, OrgMembership, User } from "./common_pb"; import { file_saas_accounts_v1_common } from "./common_pb"; +import type { Installation } from "./installations_pb"; +import { file_saas_accounts_v1_installations } from "./installations_pb"; +import type { SolutionRegistration } from "./solution_registry_pb"; +import { file_saas_accounts_v1_solution_registry } from "./solution_registry_pb"; import type { GetEventOperationsRequestSchema, GetEventOperationsResponseSchema, ListEventSubscriptionsRequestSchema, ListEventSubscriptionsResponseSchema } from "../../events/v1/operations_pb"; import { file_saas_events_v1_operations } from "../../events/v1/operations_pb"; import type { GetJobOperationsRequestSchema, GetJobOperationsResponseSchema, GetJobRequestSchema, GetJobResponseSchema, ListJobsRequestSchema, ListJobsResponseSchema, ReplayJobRequestSchema, ReplayJobResponseSchema } from "../../jobs/v1/jobs_pb"; @@ -21,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEyvB4KFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnNiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_common, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCK+AQoQQ2F0YWxvZ3VlUnVubmluZxI2CghkZWNsYXJlZBgBIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRXhlY3V0aW9uEjYKCG9ic2VydmVkGAIgASgLMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFeGVjdXRpb24SOgoHdmVyZGljdBgDIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3Qi+AIKFUNhdGFsb2d1ZUluc3RhbGxhdGlvbhI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhIQCghvcmdfbmFtZRgCIAEoCRI7Cg1hZ2VudF9yZWxlYXNlGAMgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAASOwoRYWdlbnRfcmVsZWFzZV9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjYKDHJldmlzaW9uX2dhcBgFIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESPAoNZXhwb3NlZF90ZWFtcxgGIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIvwGCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI+ChBkZWNsYXJlZF9yZWxlYXNlGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAESPgoUZGVjbGFyZWRfcmVsZWFzZV9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEhYKDGJ1aWxkX2RpZ2VzdBgHIAEoCUgCEjoKEGJ1aWxkX2RpZ2VzdF9nYXAYCCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEhQKCmdlbmVyYXRpb24YCSABKANIAxI4Cg5nZW5lcmF0aW9uX2dhcBgKIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAMSNQoHcnVubmluZxgLIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ0gEEjUKC3J1bm5pbmdfZ2FwGAwgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIBBI4Cg5idWlsZF9zaXplX2dhcBgNIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAUSQQoMcmVnaXN0cmF0aW9uGA4gASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgGiAEBEj4KDWluc3RhbGxhdGlvbnMYDyADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZUIUChJidWlsZF9kaWdlc3RfdmFsdWVCEgoQZ2VuZXJhdGlvbl92YWx1ZUIPCg1ydW5uaW5nX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCJtCh1MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZRIxCgdlbnRyaWVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeRIZChFyZWdpc3RyeV9yZXZpc2lvbhgCIAEoAyp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyqSAQoXQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3QSKQolQ0FUQUxPR1VFX1JVTk5JTkdfVkVSRElDVF9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfTUFUQ0hFUxABEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfRElGRkVSUxACMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -825,6 +831,481 @@ export type UpsertFeatureFlagResponse = Message<"saas.accounts.v1.UpsertFeatureF export const UpsertFeatureFlagResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 31); +/** + * CatalogueGap is a Catalogue fact this host cannot state, with the reason. + * + * @generated from message saas.accounts.v1.CatalogueGap + */ +export type CatalogueGap = Message<"saas.accounts.v1.CatalogueGap"> & { + /** + * @generated from field: saas.accounts.v1.CatalogueGapReason reason = 1; + */ + reason: CatalogueGapReason; + + /** + * Operator-facing: names the record or producer that would supply the fact. + * + * @generated from field: string detail = 2; + */ + detail: string; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueGap. + * Use `create(CatalogueGapSchema)` to create a new message. + */ +export const CatalogueGapSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 32); + +/** + * CatalogueRelease is a declared release, publisher/name@version. + * + * @generated from message saas.accounts.v1.CatalogueRelease + */ +export type CatalogueRelease = Message<"saas.accounts.v1.CatalogueRelease"> & { + /** + * @generated from field: string publisher = 1; + */ + publisher: string; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * @generated from field: string version = 3; + */ + version: string; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueRelease. + * Use `create(CatalogueReleaseSchema)` to create a new message. + */ +export const CatalogueReleaseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 33); + +/** + * CatalogueExecution is what runs: an OCI image manifest digest and the build + * incarnation the host assigned to it, the same pair a Work Context seals. + * + * @generated from message saas.accounts.v1.CatalogueExecution + */ +export type CatalogueExecution = Message<"saas.accounts.v1.CatalogueExecution"> & { + /** + * @generated from field: string image_digest = 1; + */ + imageDigest: string; + + /** + * @generated from field: uint64 build_incarnation = 2; + */ + buildIncarnation: bigint; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueExecution. + * Use `create(CatalogueExecutionSchema)` to create a new message. + */ +export const CatalogueExecutionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 34); + +/** + * CatalogueRunning is the running incarnation beside the declared one. + * + * @generated from message saas.accounts.v1.CatalogueRunning + */ +export type CatalogueRunning = Message<"saas.accounts.v1.CatalogueRunning"> & { + /** + * @generated from field: saas.accounts.v1.CatalogueExecution declared = 1; + */ + declared?: CatalogueExecution; + + /** + * @generated from field: saas.accounts.v1.CatalogueExecution observed = 2; + */ + observed?: CatalogueExecution; + + /** + * @generated from field: saas.accounts.v1.CatalogueRunningVerdict verdict = 3; + */ + verdict: CatalogueRunningVerdict; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueRunning. + * Use `create(CatalogueRunningSchema)` to create a new message. + */ +export const CatalogueRunningSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 35); + +/** + * CatalogueInstallation is one organization's installation of a solution. + * + * @generated from message saas.accounts.v1.CatalogueInstallation + */ +export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallation"> & { + /** + * @generated from field: saas.accounts.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * @generated from field: string org_name = 2; + */ + orgName: string; + + /** + * The release the installation's agent principal was created for, read from + * its canonical publisher/name:version identifier. + * + * @generated from oneof saas.accounts.v1.CatalogueInstallation.agent_release_value + */ + agentReleaseValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRelease agent_release = 3; + */ + value: CatalogueRelease; + case: "agentRelease"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap agent_release_gap = 4; + */ + value: CatalogueGap; + case: "agentReleaseGap"; + } | { case: undefined; value?: undefined }; + + /** + * The installation revision the installed generation is pinned at. + * + * @generated from oneof saas.accounts.v1.CatalogueInstallation.revision_value + */ + revisionValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap revision_gap = 5; + */ + value: CatalogueGap; + case: "revisionGap"; + } | { case: undefined; value?: undefined }; + + /** + * Active team grants at the installation's authority root or above it, so + * every team whose members the grant reaches inside this installation. + * + * @generated from field: repeated saas.accounts.v1.CollectionReadGrant exposed_teams = 6; + */ + exposedTeams: CollectionReadGrant[]; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueInstallation. + * Use `create(CatalogueInstallationSchema)` to create a new message. + */ +export const CatalogueInstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 36); + +/** + * CatalogueEntry is one module or solution. + * + * @generated from message saas.accounts.v1.CatalogueEntry + */ +export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { + /** + * @generated from field: saas.accounts.v1.CatalogueEntryKind kind = 1; + */ + kind: CatalogueEntryKind; + + /** + * A module's principal prefix, or a solution's registry id — the identifier + * its installations name. + * + * @generated from field: string name = 2; + */ + name: string; + + /** + * The owner of record: a solution registration's publisher. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.publisher_value + */ + publisherValue: { + /** + * @generated from field: string publisher = 3; + */ + value: string; + case: "publisher"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap publisher_gap = 4; + */ + value: CatalogueGap; + case: "publisherGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueEntry.declared_release_value + */ + declaredReleaseValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRelease declared_release = 5; + */ + value: CatalogueRelease; + case: "declaredRelease"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap declared_release_gap = 6; + */ + value: CatalogueGap; + case: "declaredReleaseGap"; + } | { case: undefined; value?: undefined }; + + /** + * The OCI image manifest digest the entry was published at. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.build_digest_value + */ + buildDigestValue: { + /** + * @generated from field: string build_digest = 7; + */ + value: string; + case: "buildDigest"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap build_digest_gap = 8; + */ + value: CatalogueGap; + case: "buildDigestGap"; + } | { case: undefined; value?: undefined }; + + /** + * The presence generation this host applied. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.generation_value + */ + generationValue: { + /** + * @generated from field: int64 generation = 9; + */ + value: bigint; + case: "generation"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap generation_gap = 10; + */ + value: CatalogueGap; + case: "generationGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueEntry.running_value + */ + runningValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRunning running = 11; + */ + value: CatalogueRunning; + case: "running"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap running_gap = 12; + */ + value: CatalogueGap; + case: "runningGap"; + } | { case: undefined; value?: undefined }; + + /** + * Lines of code, backend and frontend, vendored paths excluded. The value + * case is the presence document's build_size section (codefly-dev/core#708) + * as delivered; this host counts nothing itself. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.build_size_value + */ + buildSizeValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap build_size_gap = 13; + */ + value: CatalogueGap; + case: "buildSizeGap"; + } | { case: undefined; value?: undefined }; + + /** + * The solution's registration, with deployment topology withheld: the + * frontend manifest, backend upstream and service alias are always empty + * here, as on every browser-facing projection of the registry. Absent for a + * module, which does not self-register, and for a solution known only from + * its installations. + * + * @generated from field: optional saas.accounts.v1.SolutionRegistration registration = 14; + */ + registration?: SolutionRegistration; + + /** + * @generated from field: repeated saas.accounts.v1.CatalogueInstallation installations = 15; + */ + installations: CatalogueInstallation[]; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueEntry. + * Use `create(CatalogueEntrySchema)` to create a new message. + */ +export const CatalogueEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 37); + +/** + * @generated from message saas.accounts.v1.ListPlatformCatalogueRequest + */ +export type ListPlatformCatalogueRequest = Message<"saas.accounts.v1.ListPlatformCatalogueRequest"> & { + /** + * Include deregistered solutions. Their installations are listed either way. + * + * @generated from field: bool include_tombstoned = 1; + */ + includeTombstoned: boolean; +}; + +/** + * Describes the message saas.accounts.v1.ListPlatformCatalogueRequest. + * Use `create(ListPlatformCatalogueRequestSchema)` to create a new message. + */ +export const ListPlatformCatalogueRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 38); + +/** + * @generated from message saas.accounts.v1.ListPlatformCatalogueResponse + */ +export type ListPlatformCatalogueResponse = Message<"saas.accounts.v1.ListPlatformCatalogueResponse"> & { + /** + * Modules first, then solutions, each ordered by name. + * + * @generated from field: repeated saas.accounts.v1.CatalogueEntry entries = 1; + */ + entries: CatalogueEntry[]; + + /** + * The registry revision the solution entries were read at. + * + * @generated from field: int64 registry_revision = 2; + */ + registryRevision: bigint; +}; + +/** + * Describes the message saas.accounts.v1.ListPlatformCatalogueResponse. + * Use `create(ListPlatformCatalogueResponseSchema)` to create a new message. + */ +export const ListPlatformCatalogueResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 39); + +/** + * CatalogueEntryKind distinguishes a composed module from a solution. + * + * @generated from enum saas.accounts.v1.CatalogueEntryKind + */ +export enum CatalogueEntryKind { + /** + * @generated from enum value: CATALOGUE_ENTRY_KIND_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * A module composed into this deployment, known by its module principal. + * + * @generated from enum value: CATALOGUE_ENTRY_KIND_MODULE = 1; + */ + MODULE = 1, + + /** + * A solution, known by its registration, its installations, or both. + * + * @generated from enum value: CATALOGUE_ENTRY_KIND_SOLUTION = 2; + */ + SOLUTION = 2, +} + +/** + * Describes the enum saas.accounts.v1.CatalogueEntryKind. + */ +export const CatalogueEntryKindSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_platform_admin, 0); + +/** + * CatalogueGapReason says why a Catalogue fact has no value. + * + * @generated from enum saas.accounts.v1.CatalogueGapReason + */ +export enum CatalogueGapReason { + /** + * @generated from enum value: CATALOGUE_GAP_REASON_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * This host keeps no record that carries the fact. + * + * @generated from enum value: CATALOGUE_GAP_REASON_NOT_RECORDED = 1; + */ + NOT_RECORDED = 1, + + /** + * The fact is a section of the producer's document, and the document this + * host holds carries no such section (an older producer). + * + * @generated from enum value: CATALOGUE_GAP_REASON_NOT_REPORTED = 2; + */ + NOT_REPORTED = 2, + + /** + * The fact needs observed cluster state, and this host has none to read — + * a local run with no cluster, or an observation that is missing or stale. + * + * @generated from enum value: CATALOGUE_GAP_REASON_NOT_OBSERVED = 3; + */ + NOT_OBSERVED = 3, +} + +/** + * Describes the enum saas.accounts.v1.CatalogueGapReason. + */ +export const CatalogueGapReasonSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_platform_admin, 1); + +/** + * CatalogueRunningVerdict compares the observed execution with the declared + * one. It exists only when both are known; anything less is a CatalogueGap. + * + * @generated from enum saas.accounts.v1.CatalogueRunningVerdict + */ +export enum CatalogueRunningVerdict { + /** + * @generated from enum value: CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * The observed image digest and incarnation are the declared ones. + * + * @generated from enum value: CATALOGUE_RUNNING_VERDICT_MATCHES = 1; + */ + MATCHES = 1, + + /** + * Something other than the declared execution is running — what cluster + * admission refuses. + * + * @generated from enum value: CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; + */ + DIFFERS = 2, +} + +/** + * Describes the enum saas.accounts.v1.CatalogueRunningVerdict. + */ +export const CatalogueRunningVerdictSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_platform_admin, 2); + /** * PlatformAdminService — cross-tenant operations for platform operators. * All RPCs require a platform_role in the JWT claims. @@ -1044,5 +1525,16 @@ export const PlatformAdminService: GenService<{ input: typeof ListEventSubscriptionsRequestSchema; output: typeof ListEventSubscriptionsResponseSchema; }, + /** + * ListPlatformCatalogue lists every composed module and solution with what + * it declares, what runs, and which organizations have it installed. + * + * @generated from rpc saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue + */ + listPlatformCatalogue: { + methodKind: "unary"; + input: typeof ListPlatformCatalogueRequestSchema; + output: typeof ListPlatformCatalogueResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_saas_accounts_v1_platform_admin, 0); diff --git a/module/services/frontend/code/src/app/admin/platform/catalogue/page.tsx b/module/services/frontend/code/src/app/admin/platform/catalogue/page.tsx new file mode 100644 index 000000000..16254c19e --- /dev/null +++ b/module/services/frontend/code/src/app/admin/platform/catalogue/page.tsx @@ -0,0 +1,24 @@ +import { RoleGate } from "@/components/auth/role-gate"; +import { PlatformCataloguePage } from "@/features/platform-catalogue"; + +function AccessDenied() { + return ( +

+

+ Super administrator required +

+

+ The Catalogue spans every organization's installations and is + restricted to platform super administrators. +

+
+ ); +} + +export default function Page() { + return ( + }> + + + ); +} diff --git a/module/services/frontend/code/src/app/admin/platform/page.tsx b/module/services/frontend/code/src/app/admin/platform/page.tsx index 82700292d..cfcd57682 100644 --- a/module/services/frontend/code/src/app/admin/platform/page.tsx +++ b/module/services/frontend/code/src/app/admin/platform/page.tsx @@ -3,6 +3,7 @@ import { Building2, CreditCard, Flag, + Layers, ListChecks, ShieldCheck, Workflow, @@ -13,6 +14,13 @@ import { readPublicRuntimeConfig } from "@/lib/read-public-runtime-config"; import { Card, CardDescription, CardHeader, CardTitle } from "@/shared/ui"; const sections = [ + { + title: "Catalogue", + description: + "Every deployed module and solution: what it declares, what runs, where it is installed.", + href: "/admin/platform/catalogue", + icon: Layers, + }, { title: "Organizations", description: diff --git a/module/services/frontend/code/src/features/platform-catalogue/index.ts b/module/services/frontend/code/src/features/platform-catalogue/index.ts new file mode 100644 index 000000000..9c8c1b949 --- /dev/null +++ b/module/services/frontend/code/src/features/platform-catalogue/index.ts @@ -0,0 +1,2 @@ +export { usePlatformCatalogue } from "./service/queries"; +export { PlatformCataloguePage } from "./ui/platform-catalogue-page"; diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts new file mode 100644 index 000000000..4f24d7dd4 --- /dev/null +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts @@ -0,0 +1,195 @@ +import { create } from "@bufbuild/protobuf"; +import { describe, expect, it } from "vitest"; +import { + CatalogueEntryKind, + CatalogueEntrySchema, + CatalogueGapReason, + CatalogueRunningVerdict, +} from "@/gen/saas/accounts/v1/platform_admin_pb"; +import { + SolutionRegistrationSchema, + SolutionRegistrationStatus, +} from "@/gen/saas/accounts/v1/solution_registry_pb"; +import { + buildSizeView, + catalogueGaps, + declaredReleaseView, + gapLabel, + generationView, + registrationView, + runningView, +} from "./facts"; + +const notObserved = { + case: "runningGap" as const, + value: { + reason: CatalogueGapReason.NOT_OBSERVED, + detail: "No cluster state.", + }, +}; + +function entry( + init: Parameters>[1] = {}, +) { + return create(CatalogueEntrySchema, { + kind: CatalogueEntryKind.SOLUTION, + name: "example", + runningValue: notObserved, + ...init, + }); +} + +describe("runningView", () => { + const declared = { imageDigest: "sha256:aaa", buildIncarnation: BigInt(3) }; + + it("states a match only when the server judged both executions equal", () => { + const view = runningView( + entry({ + runningValue: { + case: "running", + value: { + declared, + observed: declared, + verdict: CatalogueRunningVerdict.MATCHES, + }, + }, + }), + ); + expect(view).toMatchObject({ + kind: "value", + text: "Matches declared", + tone: "success", + }); + }); + + it("flags a mismatch with both executions, since admission would refuse it", () => { + const view = runningView( + entry({ + runningValue: { + case: "running", + value: { + declared, + observed: { + imageDigest: "sha256:bbb", + buildIncarnation: BigInt(3), + }, + verdict: CatalogueRunningVerdict.DIFFERS, + }, + }, + }), + ); + expect(view).toMatchObject({ + kind: "value", + text: "Differs from declared", + tone: "danger", + }); + expect(view.kind === "value" && view.detail).toContain("sha256:aaa"); + expect(view.kind === "value" && view.detail).toContain("sha256:bbb"); + }); + + it("says an unobserved deployment is not observed — never a match", () => { + expect(runningView(entry())).toEqual({ + kind: "gap", + label: "Not observed", + detail: "No cluster state.", + }); + }); + + it("does not read an unset verdict or an unset oneof as a value", () => { + expect( + runningView( + entry({ + runningValue: { + case: "running", + value: { declared, observed: declared }, + }, + }), + ).kind, + ).toBe("gap"); + expect( + runningView(entry({ runningValue: { case: undefined } })), + ).toMatchObject({ + kind: "gap", + label: "Unknown", + }); + }); +}); + +describe("presence-borne facts", () => { + it("render the server's gap reason, never an empty value or zero", () => { + const gap = { + reason: CatalogueGapReason.NOT_RECORDED, + detail: "No presence document.", + }; + const e = entry({ + generationValue: { case: "generationGap", value: gap }, + buildSizeValue: { case: "buildSizeGap", value: gap }, + declaredReleaseValue: { case: "declaredReleaseGap", value: gap }, + }); + for (const view of [ + generationView(e), + buildSizeView(e), + declaredReleaseView(e), + ]) { + expect(view).toEqual({ + kind: "gap", + label: "Not recorded", + detail: "No presence document.", + }); + } + expect(gapLabel(CatalogueGapReason.NOT_REPORTED)).toBe("Not reported"); + }); + + it("render a value once the server sends one", () => { + const e = entry({ + generationValue: { case: "generation", value: BigInt(7) }, + declaredReleaseValue: { + case: "declaredRelease", + value: { publisher: "example", name: "billing", version: "1.2.0" }, + }, + }); + expect(generationView(e)).toEqual({ kind: "value", text: "7" }); + expect(declaredReleaseView(e)).toEqual({ + kind: "value", + text: "example/billing@1.2.0", + }); + }); +}); + +describe("registrationView", () => { + it("tells a composed module, a solution known only by installations, and each status apart", () => { + expect( + registrationView(entry({ kind: CatalogueEntryKind.MODULE })), + ).toMatchObject({ text: "Composed" }); + expect(registrationView(entry())).toMatchObject({ + kind: "gap", + label: "Not registered", + }); + expect( + registrationView( + entry({ + registration: create(SolutionRegistrationSchema, { + status: SolutionRegistrationStatus.INCOMPATIBLE, + }), + }), + ), + ).toMatchObject({ text: "Incompatible", tone: "danger" }); + }); +}); + +describe("catalogueGaps", () => { + it("lists each unavailable fact once, however many entries share it", () => { + const gap = { reason: CatalogueGapReason.NOT_RECORDED, detail: "core#708" }; + const shared = { + buildSizeValue: { case: "buildSizeGap" as const, value: gap }, + }; + const gaps = catalogueGaps([ + entry(shared), + entry({ ...shared, name: "other" }), + ]); + expect(gaps.filter((g) => g.fact === "Build size")).toEqual([ + { fact: "Build size", label: "Not recorded", detail: "core#708" }, + ]); + expect(gaps.filter((g) => g.fact === "Running")).toHaveLength(1); + }); +}); diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts new file mode 100644 index 000000000..321aafba2 --- /dev/null +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts @@ -0,0 +1,231 @@ +import type { ComponentProps } from "react"; +import { + type CatalogueEntry, + CatalogueEntryKind, + type CatalogueExecution, + type CatalogueGap, + CatalogueGapReason, + type CatalogueInstallation, + type CatalogueRelease, + CatalogueRunningVerdict, +} from "@/gen/saas/accounts/v1/platform_admin_pb"; +import { SolutionRegistrationStatus } from "@/gen/saas/accounts/v1/solution_registry_pb"; +import type { Badge } from "@/shared/ui"; + +type StatusTone = NonNullable["tone"]>; + +/** + * One Catalogue cell. A fact the host holds is a `value`; a fact it does not is + * a `gap` with the server's reason and detail — never an empty cell, a zero, or + * a match, because each of those is a claim the host cannot make. + */ +export type FactView = + | { kind: "value"; text: string; tone?: StatusTone; detail?: string } + | { kind: "gap"; label: string; detail: string }; + +export function gapLabel(reason: CatalogueGapReason): string { + switch (reason) { + case CatalogueGapReason.NOT_RECORDED: + return "Not recorded"; + case CatalogueGapReason.NOT_REPORTED: + return "Not reported"; + case CatalogueGapReason.NOT_OBSERVED: + return "Not observed"; + default: + return "Unknown"; + } +} + +function gapView(gap: CatalogueGap): FactView { + return { kind: "gap", label: gapLabel(gap.reason), detail: gap.detail }; +} + +// A oneof the server left unset is not a value either: an older or newer +// server, or a defect, and the cell says it does not know. +const UNSET: FactView = { + kind: "gap", + label: "Unknown", + detail: "The server sent neither a value nor a reason for this fact.", +}; + +export function releaseText(release: CatalogueRelease): string { + return `${release.publisher}/${release.name}@${release.version}`; +} + +export function executionText( + execution: CatalogueExecution | undefined, +): string { + if (!execution) return "unknown"; + return `${execution.imageDigest || "no digest"} (incarnation ${execution.buildIncarnation})`; +} + +export function kindLabel(kind: CatalogueEntryKind): string { + switch (kind) { + case CatalogueEntryKind.MODULE: + return "Module"; + case CatalogueEntryKind.SOLUTION: + return "Solution"; + default: + return "Unknown"; + } +} + +export function publisherView(entry: CatalogueEntry): FactView { + const v = entry.publisherValue; + if (v.case === "publisher") return { kind: "value", text: v.value }; + if (v.case === "publisherGap") return gapView(v.value); + return UNSET; +} + +export function declaredReleaseView(entry: CatalogueEntry): FactView { + const v = entry.declaredReleaseValue; + if (v.case === "declaredRelease") + return { kind: "value", text: releaseText(v.value) }; + if (v.case === "declaredReleaseGap") return gapView(v.value); + return UNSET; +} + +export function buildDigestView(entry: CatalogueEntry): FactView { + const v = entry.buildDigestValue; + if (v.case === "buildDigest") return { kind: "value", text: v.value }; + if (v.case === "buildDigestGap") return gapView(v.value); + return UNSET; +} + +export function generationView(entry: CatalogueEntry): FactView { + const v = entry.generationValue; + if (v.case === "generation") + return { kind: "value", text: v.value.toString() }; + if (v.case === "generationGap") return gapView(v.value); + return UNSET; +} + +/** + * What runs beside what was declared. A mismatch is the row an operator is + * looking for — it is what cluster admission would refuse — so it carries the + * danger tone and both executions. + */ +export function runningView(entry: CatalogueEntry): FactView { + const v = entry.runningValue; + if (v.case === "runningGap") return gapView(v.value); + if (v.case !== "running") return UNSET; + const detail = `Declared ${executionText(v.value.declared)}; observed ${executionText(v.value.observed)}.`; + switch (v.value.verdict) { + case CatalogueRunningVerdict.MATCHES: + return { + kind: "value", + text: "Matches declared", + tone: "success", + detail, + }; + case CatalogueRunningVerdict.DIFFERS: + return { + kind: "value", + text: "Differs from declared", + tone: "danger", + detail, + }; + default: + return { ...UNSET, detail }; + } +} + +export function buildSizeView(entry: CatalogueEntry): FactView { + const v = entry.buildSizeValue; + if (v.case === "buildSizeGap") return gapView(v.value); + return UNSET; +} + +export function agentReleaseView( + installation: CatalogueInstallation, +): FactView { + const v = installation.agentReleaseValue; + if (v.case === "agentRelease") + return { kind: "value", text: releaseText(v.value) }; + if (v.case === "agentReleaseGap") return gapView(v.value); + return UNSET; +} + +export function installationRevisionView( + installation: CatalogueInstallation, +): FactView { + const v = installation.revisionValue; + if (v.case === "revisionGap") return gapView(v.value); + return UNSET; +} + +export function registrationView(entry: CatalogueEntry): FactView { + if (entry.kind === CatalogueEntryKind.MODULE) { + return { kind: "value", text: "Composed", tone: "neutral" }; + } + const registration = entry.registration; + if (!registration) { + return { + kind: "gap", + label: "Not registered", + detail: + "Named only by its installations; no registration exists under this identifier.", + }; + } + switch (registration.status) { + case SolutionRegistrationStatus.ACTIVE: + return { kind: "value", text: "Active", tone: "success" }; + case SolutionRegistrationStatus.PENDING: + return { + kind: "value", + text: "Pending", + tone: "warning", + detail: "A half has never registered.", + }; + case SolutionRegistrationStatus.EXPIRED: + return { + kind: "value", + text: "Expired", + tone: "warning", + detail: "A half's lease lapsed: its deployment stopped renewing.", + }; + case SolutionRegistrationStatus.INCOMPATIBLE: + return { + kind: "value", + text: "Incompatible", + tone: "danger", + detail: "The two halves declare different contract versions.", + }; + case SolutionRegistrationStatus.TOMBSTONED: + return { kind: "value", text: "Deregistered", tone: "neutral" }; + default: + return UNSET; + } +} + +/** + * The facts this host cannot state, deduplicated, so the page can say once — + * under the table — what each "Not recorded" means and what would supply it. + */ +export function catalogueGaps( + entries: CatalogueEntry[], +): { fact: string; label: string; detail: string }[] { + const seen = new Map< + string, + { fact: string; label: string; detail: string } + >(); + const note = (fact: string, view: FactView) => { + if (view.kind !== "gap") return; + const key = `${fact}\u0000${view.label}\u0000${view.detail}`; + if (!seen.has(key)) + seen.set(key, { fact, label: view.label, detail: view.detail }); + }; + for (const entry of entries) { + note("Publisher", publisherView(entry)); + note("Declared release", declaredReleaseView(entry)); + note("Build", buildDigestView(entry)); + note("Generation", generationView(entry)); + note("Running", runningView(entry)); + note("Build size", buildSizeView(entry)); + for (const installation of entry.installations) { + note("Agent release", agentReleaseView(installation)); + note("Installation revision", installationRevisionView(installation)); + } + } + return [...seen.values()]; +} diff --git a/module/services/frontend/code/src/features/platform-catalogue/service/queries.ts b/module/services/frontend/code/src/features/platform-catalogue/service/queries.ts new file mode 100644 index 000000000..43359ec29 --- /dev/null +++ b/module/services/frontend/code/src/features/platform-catalogue/service/queries.ts @@ -0,0 +1,12 @@ +import { useQuery } from "@tanstack/react-query"; +import { usePlatformAdminService } from "@/lib/hooks/use-api-client"; + +// The Catalogue is one snapshot of the registry, the installations and the +// composed modules; it holds tens of entries, so it is read whole, not paged. +export function usePlatformCatalogue(includeTombstoned: boolean) { + const service = usePlatformAdminService(); + return useQuery({ + queryKey: ["platform-catalogue", includeTombstoned], + queryFn: () => service.listPlatformCatalogue({ includeTombstoned }), + }); +} diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx new file mode 100644 index 000000000..5ab88deab --- /dev/null +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx @@ -0,0 +1,189 @@ +import { cleanup, fireEvent, screen, within } from "@testing-library/react"; +import { HttpResponse, http } from "msw"; +import { afterEach, describe, expect, it } from "vitest"; +import { renderInApp, rpc } from "@/test/container"; +import { server } from "@/test/setup"; +import { PlatformCataloguePage } from "./platform-catalogue-page"; + +afterEach(cleanup); + +const notRecorded = (detail: string) => ({ + reason: "CATALOGUE_GAP_REASON_NOT_RECORDED", + detail, +}); +const presenceGaps = { + declaredReleaseGap: notRecorded("No applied presence document."), + buildDigestGap: notRecorded("No applied presence document."), + generationGap: notRecorded("No applied presence document."), + buildSizeGap: notRecorded("Build size is codefly-dev/core#708."), +}; +const declared = { imageDigest: "sha256:aaa", buildIncarnation: "3" }; + +const catalogue = { + registryRevision: "12", + entries: [ + { + kind: "CATALOGUE_ENTRY_KIND_MODULE", + name: "billing-module", + publisherGap: notRecorded( + "A composed module is known by its prefix alone.", + ), + ...presenceGaps, + runningGap: { + reason: "CATALOGUE_GAP_REASON_NOT_OBSERVED", + detail: "No cluster state.", + }, + }, + { + kind: "CATALOGUE_ENTRY_KIND_SOLUTION", + name: "drifted-solution", + publisher: "solution:drifted-solution", + ...presenceGaps, + running: { + declared, + observed: { imageDigest: "sha256:bbb", buildIncarnation: "3" }, + verdict: "CATALOGUE_RUNNING_VERDICT_DIFFERS", + }, + registration: { + solutionId: "drifted-solution", + status: "SOLUTION_REGISTRATION_STATUS_ACTIVE", + }, + installations: [ + { + installation: { + id: "11111111-1111-1111-1111-111111111111", + solutionIdentifier: "drifted-solution", + createdAt: "2026-10-01T00:00:00Z", + }, + orgName: "Acme", + agentRelease: { + publisher: "example", + name: "drifted", + version: "1.2.0", + }, + revisionGap: notRecorded("Not pinned to a presence revision."), + exposedTeams: [ + { + grant: { id: "g1", scopePath: "root" }, + subjectLabel: "Finance Team", + roleName: "Viewer", + }, + ], + }, + ], + }, + { + kind: "CATALOGUE_ENTRY_KIND_SOLUTION", + name: "steady-solution", + publisher: "solution:steady-solution", + ...presenceGaps, + running: { + declared, + observed: declared, + verdict: "CATALOGUE_RUNNING_VERDICT_MATCHES", + }, + registration: { + solutionId: "steady-solution", + status: "SOLUTION_REGISTRATION_STATUS_ACTIVE", + }, + }, + ], +}; + +function rowOf(name: string): HTMLElement { + const row = screen.getByText(name).closest("tr"); + if (!row) throw new Error(`no row for ${name}`); + return row; +} + +describe("PlatformCataloguePage admin container", () => { + it("shows a match, a mismatch and an unobserved entry for what they are", async () => { + server.use( + http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => + HttpResponse.json(catalogue), + ), + ); + renderInApp(); + await screen.findByText("drifted-solution"); + + expect( + within(rowOf("drifted-solution")).getByText("Differs from declared"), + ).toBeTruthy(); + expect( + within(rowOf("steady-solution")).getByText("Matches declared"), + ).toBeTruthy(); + const moduleRow = rowOf("billing-module"); + expect(within(moduleRow).getByText("Not observed")).toBeTruthy(); + expect(within(moduleRow).queryByText("Matches declared")).toBeNull(); + expect(within(moduleRow).getByText("Module")).toBeTruthy(); + + // Build size is unavailable on every row until the presence document + // carries it, and the page says what would supply it. + for (const name of [ + "billing-module", + "drifted-solution", + "steady-solution", + ]) { + const sizeCell = within(rowOf(name)) + .getAllByText("Not recorded") + .find((cell) => cell.getAttribute("title")?.includes("core#708")); + expect(sizeCell).toBeTruthy(); + } + expect(screen.getByText("Not known to this host")).toBeTruthy(); + expect( + screen.getByText("Build size is codefly-dev/core#708."), + ).toBeTruthy(); + }); + + it("opens an entry's installations: organization, agent release, exposed teams", async () => { + server.use( + http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => + HttpResponse.json(catalogue), + ), + ); + renderInApp(); + fireEvent.click( + await screen.findByRole("button", { + name: "Show installations of drifted-solution", + }), + ); + const installations = screen.getByRole("table", { + name: "Installations of drifted-solution", + }); + expect(within(installations).getByText("Acme")).toBeTruthy(); + expect( + within(installations).getByText("example/drifted@1.2.0"), + ).toBeTruthy(); + expect(within(installations).getByText("Finance Team")).toBeTruthy(); + expect(within(installations).getByText("Not recorded")).toBeTruthy(); + }); + + it("says a refused read is refused, not that nothing is deployed", async () => { + server.use( + http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => + HttpResponse.json( + { + code: "permission_denied", + message: "requires platform super_admin role", + }, + { status: 403 }, + ), + ), + ); + renderInApp(); + expect( + await screen.findByText(/You don't have permission to see the catalogue/), + ).toBeTruthy(); + expect(screen.queryByText(/Nothing is deployed/)).toBeNull(); + }); + + it("says an empty platform is empty", async () => { + server.use( + http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => + HttpResponse.json({ entries: [], registryRevision: "0" }), + ), + ); + renderInApp(); + expect(await screen.findByText(/Nothing is deployed/)).toBeTruthy(); + }); +}); diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx new file mode 100644 index 000000000..4a10a4608 --- /dev/null +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx @@ -0,0 +1,336 @@ +"use client"; + +import { timestampDate } from "@bufbuild/protobuf/wkt"; +import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react"; +import { Fragment, useState } from "react"; +import type { + CatalogueEntry, + CatalogueInstallation, +} from "@/gen/saas/accounts/v1/platform_admin_pb"; +import { + mayKeepRetainedRows, + readOutcome, + readOutcomeMessage, + staleReadNotice, +} from "@/shared/lib/read-outcome"; +import { formatDate } from "@/shared/lib/utils"; +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Label, + Skeleton, + Switch, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui"; +import { + agentReleaseView, + buildDigestView, + buildSizeView, + catalogueGaps, + declaredReleaseView, + type FactView, + generationView, + installationRevisionView, + kindLabel, + publisherView, + registrationView, + runningView, +} from "../model/facts"; +import { usePlatformCatalogue } from "../service/queries"; + +const COLUMNS = 10; + +/** + * One cell. A gap reads as its reason in muted italics with the detail on + * hover, so a reader never mistakes "this host does not know" for a value. + */ +export function Fact({ view }: { view: FactView }) { + if (view.kind === "gap") { + return ( + + {view.label} + + ); + } + if (view.tone) { + return ( + + {view.text} + + ); + } + return ( + + {view.text} + + ); +} + +function entryKey(entry: CatalogueEntry): string { + return `${entry.kind}:${entry.name}`; +} + +/** + * The platform Catalogue: every composed module and solution, what it declares, + * what runs, and which organizations have it installed. Facts this host holds + * no record of yet are shown as such, with what would supply them. + */ +export function PlatformCataloguePage() { + const [includeTombstoned, setIncludeTombstoned] = useState(false); + const [expanded, setExpanded] = useState>(new Set()); + const catalogue = usePlatformCatalogue(includeTombstoned); + + const outcome = readOutcome(catalogue.isError, catalogue.error); + const withheld = !mayKeepRetainedRows(outcome); + const stale = staleReadNotice(outcome, "the catalogue"); + const entries = withheld ? [] : (catalogue.data?.entries ?? []); + const gaps = catalogueGaps(entries); + + const toggle = (key: string) => + setExpanded((current) => { + const next = new Set(current); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + + return ( +
+
+
+

+ Catalogue +

+

+ Every module and solution deployed on this platform: what it + declares, what runs, and which organizations have it installed. +

+
+ +
+ +
+ + +
+ + {stale && ( +

+ {stale} +

+ )} + + + + {catalogue.isLoading ? ( + + ) : entries.length === 0 ? ( +

+ {readOutcomeMessage( + outcome, + "the catalogue", + "Nothing is deployed: no module is composed and no solution is registered or installed.", + )} +

+ ) : ( + + + + + Name + Registration + Publisher + Declared release + Build + Generation + Running + Installations + Build size + + + + {entries.map((entry) => { + const key = entryKey(entry); + const open = expanded.has(key); + return ( + + + + {entry.installations.length > 0 && ( + + )} + + +
+ {entry.name} + + {kindLabel(entry.kind)} + +
+
+ + + + + + + + + + + + + + + + + + + + {entry.installations.length} + + + + +
+ {open && ( + + + + + + + )} +
+ ); + })} +
+
+ )} +
+
+ + {gaps.length > 0 && ( + + + Not known to this host + + What each unavailable cell above means, and what would supply it. + + + +
+ {gaps.map((gap) => ( +
+
+ {gap.fact} — {gap.label} +
+
{gap.detail}
+
+ ))} +
+
+
+ )} +
+ ); +} + +function InstallationsTable({ + name, + installations, +}: { + name: string; + installations: CatalogueInstallation[]; +}) { + return ( + + + + Organization + Agent release + Revision + Teams exposed + Installed + + + + {installations.map((item) => ( + + {item.orgName} + + + + + + + + {item.exposedTeams.length === 0 ? ( + No team + ) : ( +
+ {item.exposedTeams.map((team) => ( + + {team.subjectLabel} + + ))} +
+ )} +
+ + {formatDate( + item.installation?.createdAt + ? timestampDate(item.installation.createdAt).toISOString() + : undefined, + )} + +
+ ))} +
+
+ ); +} diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index 22df44d45..3fc32b031 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -2,14 +2,20 @@ // @generated from file saas/accounts/v1/platform_admin.proto (package saas.accounts.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; -import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../../buf/validate/validate_pb"; import { file_google_api_annotations } from "../../../google/api/annotations_pb"; import type { EmptySchema, Timestamp } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_empty, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; +import type { CollectionReadGrant } from "./authorization_pb"; +import { file_saas_accounts_v1_authorization } from "./authorization_pb"; import type { Organization, OrgMembership, User } from "./common_pb"; import { file_saas_accounts_v1_common } from "./common_pb"; +import type { Installation } from "./installations_pb"; +import { file_saas_accounts_v1_installations } from "./installations_pb"; +import type { SolutionRegistration } from "./solution_registry_pb"; +import { file_saas_accounts_v1_solution_registry } from "./solution_registry_pb"; import type { GetEventOperationsRequestSchema, GetEventOperationsResponseSchema, ListEventSubscriptionsRequestSchema, ListEventSubscriptionsResponseSchema } from "../../events/v1/operations_pb"; import { file_saas_events_v1_operations } from "../../events/v1/operations_pb"; import type { GetJobOperationsRequestSchema, GetJobOperationsResponseSchema, GetJobRequestSchema, GetJobResponseSchema, ListJobsRequestSchema, ListJobsResponseSchema, ReplayJobRequestSchema, ReplayJobResponseSchema } from "../../jobs/v1/jobs_pb"; @@ -21,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEyvB4KFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnNCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJQbGF0Zm9ybUFkbWluUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_common, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCK+AQoQQ2F0YWxvZ3VlUnVubmluZxI2CghkZWNsYXJlZBgBIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRXhlY3V0aW9uEjYKCG9ic2VydmVkGAIgASgLMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFeGVjdXRpb24SOgoHdmVyZGljdBgDIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3Qi+AIKFUNhdGFsb2d1ZUluc3RhbGxhdGlvbhI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhIQCghvcmdfbmFtZRgCIAEoCRI7Cg1hZ2VudF9yZWxlYXNlGAMgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAASOwoRYWdlbnRfcmVsZWFzZV9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjYKDHJldmlzaW9uX2dhcBgFIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESPAoNZXhwb3NlZF90ZWFtcxgGIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIvwGCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI+ChBkZWNsYXJlZF9yZWxlYXNlGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAESPgoUZGVjbGFyZWRfcmVsZWFzZV9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEhYKDGJ1aWxkX2RpZ2VzdBgHIAEoCUgCEjoKEGJ1aWxkX2RpZ2VzdF9nYXAYCCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEhQKCmdlbmVyYXRpb24YCSABKANIAxI4Cg5nZW5lcmF0aW9uX2dhcBgKIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAMSNQoHcnVubmluZxgLIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ0gEEjUKC3J1bm5pbmdfZ2FwGAwgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIBBI4Cg5idWlsZF9zaXplX2dhcBgNIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAUSQQoMcmVnaXN0cmF0aW9uGA4gASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgGiAEBEj4KDWluc3RhbGxhdGlvbnMYDyADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZUIUChJidWlsZF9kaWdlc3RfdmFsdWVCEgoQZ2VuZXJhdGlvbl92YWx1ZUIPCg1ydW5uaW5nX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCJtCh1MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZRIxCgdlbnRyaWVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeRIZChFyZWdpc3RyeV9yZXZpc2lvbhgCIAEoAyp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyqSAQoXQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3QSKQolQ0FUQUxPR1VFX1JVTk5JTkdfVkVSRElDVF9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfTUFUQ0hFUxABEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfRElGRkVSUxACMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWVCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJQbGF0Zm9ybUFkbWluUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -825,6 +831,481 @@ export type UpsertFeatureFlagResponse = Message<"saas.accounts.v1.UpsertFeatureF export const UpsertFeatureFlagResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 31); +/** + * CatalogueGap is a Catalogue fact this host cannot state, with the reason. + * + * @generated from message saas.accounts.v1.CatalogueGap + */ +export type CatalogueGap = Message<"saas.accounts.v1.CatalogueGap"> & { + /** + * @generated from field: saas.accounts.v1.CatalogueGapReason reason = 1; + */ + reason: CatalogueGapReason; + + /** + * Operator-facing: names the record or producer that would supply the fact. + * + * @generated from field: string detail = 2; + */ + detail: string; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueGap. + * Use `create(CatalogueGapSchema)` to create a new message. + */ +export const CatalogueGapSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 32); + +/** + * CatalogueRelease is a declared release, publisher/name@version. + * + * @generated from message saas.accounts.v1.CatalogueRelease + */ +export type CatalogueRelease = Message<"saas.accounts.v1.CatalogueRelease"> & { + /** + * @generated from field: string publisher = 1; + */ + publisher: string; + + /** + * @generated from field: string name = 2; + */ + name: string; + + /** + * @generated from field: string version = 3; + */ + version: string; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueRelease. + * Use `create(CatalogueReleaseSchema)` to create a new message. + */ +export const CatalogueReleaseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 33); + +/** + * CatalogueExecution is what runs: an OCI image manifest digest and the build + * incarnation the host assigned to it, the same pair a Work Context seals. + * + * @generated from message saas.accounts.v1.CatalogueExecution + */ +export type CatalogueExecution = Message<"saas.accounts.v1.CatalogueExecution"> & { + /** + * @generated from field: string image_digest = 1; + */ + imageDigest: string; + + /** + * @generated from field: uint64 build_incarnation = 2; + */ + buildIncarnation: bigint; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueExecution. + * Use `create(CatalogueExecutionSchema)` to create a new message. + */ +export const CatalogueExecutionSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 34); + +/** + * CatalogueRunning is the running incarnation beside the declared one. + * + * @generated from message saas.accounts.v1.CatalogueRunning + */ +export type CatalogueRunning = Message<"saas.accounts.v1.CatalogueRunning"> & { + /** + * @generated from field: saas.accounts.v1.CatalogueExecution declared = 1; + */ + declared?: CatalogueExecution; + + /** + * @generated from field: saas.accounts.v1.CatalogueExecution observed = 2; + */ + observed?: CatalogueExecution; + + /** + * @generated from field: saas.accounts.v1.CatalogueRunningVerdict verdict = 3; + */ + verdict: CatalogueRunningVerdict; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueRunning. + * Use `create(CatalogueRunningSchema)` to create a new message. + */ +export const CatalogueRunningSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 35); + +/** + * CatalogueInstallation is one organization's installation of a solution. + * + * @generated from message saas.accounts.v1.CatalogueInstallation + */ +export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallation"> & { + /** + * @generated from field: saas.accounts.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * @generated from field: string org_name = 2; + */ + orgName: string; + + /** + * The release the installation's agent principal was created for, read from + * its canonical publisher/name:version identifier. + * + * @generated from oneof saas.accounts.v1.CatalogueInstallation.agent_release_value + */ + agentReleaseValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRelease agent_release = 3; + */ + value: CatalogueRelease; + case: "agentRelease"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap agent_release_gap = 4; + */ + value: CatalogueGap; + case: "agentReleaseGap"; + } | { case: undefined; value?: undefined }; + + /** + * The installation revision the installed generation is pinned at. + * + * @generated from oneof saas.accounts.v1.CatalogueInstallation.revision_value + */ + revisionValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap revision_gap = 5; + */ + value: CatalogueGap; + case: "revisionGap"; + } | { case: undefined; value?: undefined }; + + /** + * Active team grants at the installation's authority root or above it, so + * every team whose members the grant reaches inside this installation. + * + * @generated from field: repeated saas.accounts.v1.CollectionReadGrant exposed_teams = 6; + */ + exposedTeams: CollectionReadGrant[]; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueInstallation. + * Use `create(CatalogueInstallationSchema)` to create a new message. + */ +export const CatalogueInstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 36); + +/** + * CatalogueEntry is one module or solution. + * + * @generated from message saas.accounts.v1.CatalogueEntry + */ +export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { + /** + * @generated from field: saas.accounts.v1.CatalogueEntryKind kind = 1; + */ + kind: CatalogueEntryKind; + + /** + * A module's principal prefix, or a solution's registry id — the identifier + * its installations name. + * + * @generated from field: string name = 2; + */ + name: string; + + /** + * The owner of record: a solution registration's publisher. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.publisher_value + */ + publisherValue: { + /** + * @generated from field: string publisher = 3; + */ + value: string; + case: "publisher"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap publisher_gap = 4; + */ + value: CatalogueGap; + case: "publisherGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueEntry.declared_release_value + */ + declaredReleaseValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRelease declared_release = 5; + */ + value: CatalogueRelease; + case: "declaredRelease"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap declared_release_gap = 6; + */ + value: CatalogueGap; + case: "declaredReleaseGap"; + } | { case: undefined; value?: undefined }; + + /** + * The OCI image manifest digest the entry was published at. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.build_digest_value + */ + buildDigestValue: { + /** + * @generated from field: string build_digest = 7; + */ + value: string; + case: "buildDigest"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap build_digest_gap = 8; + */ + value: CatalogueGap; + case: "buildDigestGap"; + } | { case: undefined; value?: undefined }; + + /** + * The presence generation this host applied. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.generation_value + */ + generationValue: { + /** + * @generated from field: int64 generation = 9; + */ + value: bigint; + case: "generation"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap generation_gap = 10; + */ + value: CatalogueGap; + case: "generationGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueEntry.running_value + */ + runningValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRunning running = 11; + */ + value: CatalogueRunning; + case: "running"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap running_gap = 12; + */ + value: CatalogueGap; + case: "runningGap"; + } | { case: undefined; value?: undefined }; + + /** + * Lines of code, backend and frontend, vendored paths excluded. The value + * case is the presence document's build_size section (codefly-dev/core#708) + * as delivered; this host counts nothing itself. + * + * @generated from oneof saas.accounts.v1.CatalogueEntry.build_size_value + */ + buildSizeValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap build_size_gap = 13; + */ + value: CatalogueGap; + case: "buildSizeGap"; + } | { case: undefined; value?: undefined }; + + /** + * The solution's registration, with deployment topology withheld: the + * frontend manifest, backend upstream and service alias are always empty + * here, as on every browser-facing projection of the registry. Absent for a + * module, which does not self-register, and for a solution known only from + * its installations. + * + * @generated from field: optional saas.accounts.v1.SolutionRegistration registration = 14; + */ + registration?: SolutionRegistration; + + /** + * @generated from field: repeated saas.accounts.v1.CatalogueInstallation installations = 15; + */ + installations: CatalogueInstallation[]; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueEntry. + * Use `create(CatalogueEntrySchema)` to create a new message. + */ +export const CatalogueEntrySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 37); + +/** + * @generated from message saas.accounts.v1.ListPlatformCatalogueRequest + */ +export type ListPlatformCatalogueRequest = Message<"saas.accounts.v1.ListPlatformCatalogueRequest"> & { + /** + * Include deregistered solutions. Their installations are listed either way. + * + * @generated from field: bool include_tombstoned = 1; + */ + includeTombstoned: boolean; +}; + +/** + * Describes the message saas.accounts.v1.ListPlatformCatalogueRequest. + * Use `create(ListPlatformCatalogueRequestSchema)` to create a new message. + */ +export const ListPlatformCatalogueRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 38); + +/** + * @generated from message saas.accounts.v1.ListPlatformCatalogueResponse + */ +export type ListPlatformCatalogueResponse = Message<"saas.accounts.v1.ListPlatformCatalogueResponse"> & { + /** + * Modules first, then solutions, each ordered by name. + * + * @generated from field: repeated saas.accounts.v1.CatalogueEntry entries = 1; + */ + entries: CatalogueEntry[]; + + /** + * The registry revision the solution entries were read at. + * + * @generated from field: int64 registry_revision = 2; + */ + registryRevision: bigint; +}; + +/** + * Describes the message saas.accounts.v1.ListPlatformCatalogueResponse. + * Use `create(ListPlatformCatalogueResponseSchema)` to create a new message. + */ +export const ListPlatformCatalogueResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 39); + +/** + * CatalogueEntryKind distinguishes a composed module from a solution. + * + * @generated from enum saas.accounts.v1.CatalogueEntryKind + */ +export enum CatalogueEntryKind { + /** + * @generated from enum value: CATALOGUE_ENTRY_KIND_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * A module composed into this deployment, known by its module principal. + * + * @generated from enum value: CATALOGUE_ENTRY_KIND_MODULE = 1; + */ + MODULE = 1, + + /** + * A solution, known by its registration, its installations, or both. + * + * @generated from enum value: CATALOGUE_ENTRY_KIND_SOLUTION = 2; + */ + SOLUTION = 2, +} + +/** + * Describes the enum saas.accounts.v1.CatalogueEntryKind. + */ +export const CatalogueEntryKindSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_platform_admin, 0); + +/** + * CatalogueGapReason says why a Catalogue fact has no value. + * + * @generated from enum saas.accounts.v1.CatalogueGapReason + */ +export enum CatalogueGapReason { + /** + * @generated from enum value: CATALOGUE_GAP_REASON_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * This host keeps no record that carries the fact. + * + * @generated from enum value: CATALOGUE_GAP_REASON_NOT_RECORDED = 1; + */ + NOT_RECORDED = 1, + + /** + * The fact is a section of the producer's document, and the document this + * host holds carries no such section (an older producer). + * + * @generated from enum value: CATALOGUE_GAP_REASON_NOT_REPORTED = 2; + */ + NOT_REPORTED = 2, + + /** + * The fact needs observed cluster state, and this host has none to read — + * a local run with no cluster, or an observation that is missing or stale. + * + * @generated from enum value: CATALOGUE_GAP_REASON_NOT_OBSERVED = 3; + */ + NOT_OBSERVED = 3, +} + +/** + * Describes the enum saas.accounts.v1.CatalogueGapReason. + */ +export const CatalogueGapReasonSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_platform_admin, 1); + +/** + * CatalogueRunningVerdict compares the observed execution with the declared + * one. It exists only when both are known; anything less is a CatalogueGap. + * + * @generated from enum saas.accounts.v1.CatalogueRunningVerdict + */ +export enum CatalogueRunningVerdict { + /** + * @generated from enum value: CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * The observed image digest and incarnation are the declared ones. + * + * @generated from enum value: CATALOGUE_RUNNING_VERDICT_MATCHES = 1; + */ + MATCHES = 1, + + /** + * Something other than the declared execution is running — what cluster + * admission refuses. + * + * @generated from enum value: CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; + */ + DIFFERS = 2, +} + +/** + * Describes the enum saas.accounts.v1.CatalogueRunningVerdict. + */ +export const CatalogueRunningVerdictSchema: GenEnum = /*@__PURE__*/ + enumDesc(file_saas_accounts_v1_platform_admin, 2); + /** * PlatformAdminService — cross-tenant operations for platform operators. * All RPCs require a platform_role in the JWT claims. @@ -1044,6 +1525,17 @@ export const PlatformAdminService: GenService<{ input: typeof ListEventSubscriptionsRequestSchema; output: typeof ListEventSubscriptionsResponseSchema; }, + /** + * ListPlatformCatalogue lists every composed module and solution with what + * it declares, what runs, and which organizations have it installed. + * + * @generated from rpc saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue + */ + listPlatformCatalogue: { + methodKind: "unary"; + input: typeof ListPlatformCatalogueRequestSchema; + output: typeof ListPlatformCatalogueResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_saas_accounts_v1_platform_admin, 0); diff --git a/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts b/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts index 739644568..708bdc047 100644 --- a/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts +++ b/module/services/frontend/code/src/gen/saas/frontend/v1/plugin_catalog.ts @@ -55,6 +55,7 @@ export const FRONTEND_ROUTES = [ { path: "/admin/permissions", sourcePath: "src/app/admin/permissions/page.tsx", match: "exact", access: "admin" }, { path: "/admin/platform", sourcePath: "src/app/admin/platform/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/admins", sourcePath: "src/app/admin/platform/admins/page.tsx", match: "exact", access: "super_admin" }, + { path: "/admin/platform/catalogue", sourcePath: "src/app/admin/platform/catalogue/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/events", sourcePath: "src/app/admin/platform/events/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/feature-flags", sourcePath: "src/app/admin/platform/feature-flags/page.tsx", match: "exact", access: "super_admin" }, { path: "/admin/platform/jobs", sourcePath: "src/app/admin/platform/jobs/page.tsx", match: "exact", access: "super_admin" }, @@ -141,6 +142,7 @@ export const FRONTEND_NAVIGATION: readonly FrontendNavigationItem[] = [ { id: "admin-sessions", plugin: "platform-admin", label: "Sessions", href: "/admin/sessions", icon: "Monitor", group: "Platform", access: "super_admin", requiredRole: "super_admin", surfaces: ["command_palette", "plugin_registry", "sidebar"], order: 220 }, { id: "admin-entitlements", plugin: "platform-admin", label: "Entitlements", href: "/admin/entitlements", icon: "ShieldCheck", group: "Platform", access: "admin", requiredRole: "admin", requiredPermission: "entitlements:read", surfaces: ["command_palette", "plugin_registry", "sidebar"], order: 230 }, { id: "admin-job-operations", plugin: "platform-admin", label: "Job Operations", href: "/admin/platform/jobs", icon: "ListChecks", group: "Platform", access: "super_admin", requiredRole: "super_admin", surfaces: ["command_palette", "plugin_registry", "sidebar"], order: 235 }, + { id: "admin-catalogue", plugin: "platform-admin", label: "Catalogue", href: "/admin/platform/catalogue", icon: "Layers", group: "Platform", access: "super_admin", requiredRole: "super_admin", surfaces: ["command_palette", "plugin_registry", "sidebar"], order: 236 }, { id: "admin-audit-log", plugin: "audit", label: "Audit Log", href: "/admin/audit-log", icon: "Activity", group: "Platform", access: "admin", requiredRole: "admin", requiredPermission: "audit:read", surfaces: ["command_palette", "plugin_registry", "sidebar"], order: 240 }, { id: "admin-billing", plugin: "core-users", label: "Subscription", href: "/admin/billing", icon: "CreditCard", group: "Billing", access: "admin", requiredRole: "admin", requiredPermission: "billing:read", surfaces: ["sidebar"], order: 300 }, { id: "admin-webhooks", plugin: "core-users", label: "Webhooks", href: "/admin/webhooks", icon: "Globe", group: "Integrations", access: "admin", requiredRole: "admin", requiredPermission: "webhooks:read", surfaces: ["command_palette", "plugin_registry", "sidebar"], order: 400 }, diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index ea4d9708c..401a89741 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -2241,6 +2241,19 @@ "coverage": "missing", "remaining": "Owner stories or a reviewed nonvisual exclusion still required." }, + { + "source": "src/app/admin/platform/catalogue/page.tsx", + "imports": [ + "@/components/auth/role-gate", + "@/features/platform-catalogue" + ], + "owner": "SaaS application", + "disposition": "keep feature-local", + "reason": "Next.js route or layout entry; owns host routing and server/provider composition. Full-page preview coverage is still pending.", + "stories": [], + "coverage": "missing", + "remaining": "Owner stories or a reviewed nonvisual exclusion still required." + }, { "source": "src/app/admin/platform/events/page.tsx", "imports": [ @@ -3682,6 +3695,26 @@ "coverage": "missing", "remaining": "Owner stories or a reviewed nonvisual exclusion still required." }, + { + "source": "src/features/platform-catalogue/ui/platform-catalogue-page.tsx", + "imports": [ + "@bufbuild/protobuf/wkt", + "lucide-react", + "react", + "@/gen/saas/accounts/v1/platform_admin_pb", + "@/shared/lib/read-outcome", + "@/shared/lib/utils", + "@/shared/ui", + "../model/facts", + "../service/queries" + ], + "owner": "SaaS application", + "disposition": "keep feature-local", + "reason": "Owns the platform Catalogue workflow, data binding or route composition. Lower-level presentation consumes the shared kit.", + "stories": [], + "coverage": "missing", + "remaining": "Owner stories or a reviewed nonvisual exclusion still required." + }, { "source": "src/features/event-operations/ui/event-operations-page.tsx", "imports": [ diff --git a/module/services/frontend/frontend.bindings.codefly.yaml b/module/services/frontend/frontend.bindings.codefly.yaml index 2990d1bab..f2a6883ef 100644 --- a/module/services/frontend/frontend.bindings.codefly.yaml +++ b/module/services/frontend/frontend.bindings.codefly.yaml @@ -217,6 +217,18 @@ navigation: - plugin_registry - sidebar order: 235 + - id: admin-catalogue + plugin: platform-admin + label: Catalogue + path: /admin/platform/catalogue + icon: Layers + group: Platform + access: super_admin + surfaces: + - command_palette + - plugin_registry + - sidebar + order: 236 - id: admin-audit-log plugin: audit label: Audit Log diff --git a/module/services/frontend/generated/plugin-catalog.json b/module/services/frontend/generated/plugin-catalog.json index e962a4143..517d92732 100644 --- a/module/services/frontend/generated/plugin-catalog.json +++ b/module/services/frontend/generated/plugin-catalog.json @@ -101,6 +101,12 @@ "match": "FRONTEND_ROUTE_MATCH_EXACT", "access": "FRONTEND_ROUTE_ACCESS_SUPER_ADMIN" }, + { + "path": "/admin/platform/catalogue", + "source_path": "src/app/admin/platform/catalogue/page.tsx", + "match": "FRONTEND_ROUTE_MATCH_EXACT", + "access": "FRONTEND_ROUTE_ACCESS_SUPER_ADMIN" + }, { "path": "/admin/platform/events", "source_path": "src/app/admin/platform/events/page.tsx", @@ -607,6 +613,21 @@ ], "order": 235 }, + { + "id": "admin-catalogue", + "plugin": "platform-admin", + "label": "Catalogue", + "path": "/admin/platform/catalogue", + "icon": "Layers", + "group": "Platform", + "access": "FRONTEND_ROUTE_ACCESS_SUPER_ADMIN", + "surfaces": [ + "FRONTEND_NAVIGATION_SURFACE_COMMAND_PALETTE", + "FRONTEND_NAVIGATION_SURFACE_PLUGIN_REGISTRY", + "FRONTEND_NAVIGATION_SURFACE_SIDEBAR" + ], + "order": 236 + }, { "id": "admin-audit-log", "plugin": "audit", From 7fdfb42ecdfb6f17982bb681aef6a9bd2417b614 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:13:33 -0400 Subject: [PATCH 02/11] The Catalogue places each entry in the registry's six states (Refs #1020) An entry is no longer "declared vs running". It shows the registry's state model - desired, authorized, applied, observed, withdrawing, physically retired - one message per state on the wire and one column each on the page, carrying the public status fields the model names: declared_revision, authorized_revision, applied_revision, observed_revision, observation_freshness, withdrawal_state, credential_revocation_state and retirement_state. Self-registration status moves under Desired, the only record of intent this host holds today. Authorization has three cases: an approval (revision plus the approved image digest and incarnation), a known absence of one, and a gap where the host cannot tell, so an unrecorded approval never reads as a refusal. The observed verdict now judges what runs against the authorization, never the declaration: running authorized, differs from authorized, or running with no current authorization. Without both an observation and a known authorization there is no verdict, and nothing is ever reported as a match by default. Withdrawal, credential revocation and retirement have gap-only oneofs: their value vocabularies belong to the deployment contract that records them, and join those oneofs when it exists. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../api/accounts/authority/contract.binpb | Bin 951088 -> 956280 bytes .../saas/accounts/v1/platform_admin.proto | 174 +- .../api/accounts/connect/contract.binpb | Bin 951088 -> 956280 bytes .../saas/accounts/v1/platform_admin.proto | 174 +- module/contracts/api/catalog.codefly.json | 4 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 49 +- .../code/pkg/business/platform_catalogue.go | 101 +- .../pkg/business/platform_catalogue_test.go | 123 +- .../gen/saas/accounts/v1/platform_admin.pb.go | 1608 ++++++++++++----- .../generated/openapi-raw/api.swagger.json | 183 +- .../accounts/openapi/api.swagger.json | 165 +- .../saas/accounts/v1/platform_admin.proto | 174 +- module/services/frontend/AGENTS.md | 8 +- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 951088 -> 956280 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 452 ++++- .../platform-catalogue/model/facts.test.ts | 300 +-- .../platform-catalogue/model/facts.ts | 298 ++- ...platform-catalogue-page.container.test.tsx | 220 ++- .../ui/platform-catalogue-page.tsx | 71 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 452 ++++- 23 files changed, 3301 insertions(+), 1263 deletions(-) diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index e1cd68f066d2eb1eee8e8b3f660cbc6817ce6b1a..9b89d9def1b008d9a75973c66811bfb0c179c984 100644 GIT binary patch delta 17440 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�pAc6TXXBS-HJ9g|&9_ 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

F9$%wn<>k6;%1Z)D{f_GIZif;s5^Pa0r9B$Epy zNr*f}CKpJO0Q+HSE|8?Dn*EDRE|8>w>tAJZfg}lBr^;mKlLW5QWO9Kd30$YQas<~I zGAUWC2tRO1aYHhBU9*o$=lUq!zySf{h=CLy#k|W z$qR>IwE9C_1^293a_If32xA2>&X!_fgb^FKLUWugorMWU(hxT^97#g##|%f3z;TXD zIw(mB#sI=LLo%Y2Y~Da3l#Fn_78-;{wAGOA=2=abtwBBHH>-NM}fd zQA?QB6Ve}SFVJP!#*>RBXRBm*_G@>3uvjLy_BI%~SSGjjHYmi!GP$+40l{aaSdkQB z8x8Oo>8wZqk_Ld!8h|7r@^c0tNdUMcrw~a~1eY3uq=De`h670;_<{jQ5&$kU07(MC u<*gh6u-O2_ibRiJbzG diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index 8c79d0188..00f45cfff 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -248,14 +248,20 @@ message UpsertFeatureFlagResponse { // the durable solution registry, the installations table, and the composition's // module principal registry — and it adds no store of its own. // -// Several facts the Catalogue is specified to show are carried by records this -// host does not hold yet: the declared release, build digest and generation -// arrive with applied presence documents, the running incarnation needs observed -// cluster state, and build size is a signed section of the presence document -// (codefly-dev/core#708, computed by the CLI at build). Each such fact is a -// oneof of its value and a CatalogueGap, so an absent fact is reported with its -// reason and is never an empty value a reader could take for zero, unset, or a -// match. A fact gains its value case when the record that carries it exists. +// Each entry is placed in the registry's state model — desired, authorized, +// applied, observed, withdrawing, physically retired — with the public status +// fields that model names (declared_revision, authorized_revision, +// applied_revision, observed_revision, observation_freshness, withdrawal_state, +// retirement_state, credential_revocation_state), never one "installed" flag. +// +// Most of those facts are carried by records this host does not hold yet: +// declarations and applied generations arrive with presence documents, approval +// and withdrawal with the deployment contract's signed approval, observations +// and retirement with cluster observation, and build size is a signed section of +// the presence document (codefly-dev/core#708, computed by the CLI at build). +// Each such fact is a oneof of its value and a CatalogueGap, so an absent fact is +// reported with its reason and is never an empty value a reader could take for +// zero, unset, or a match. A fact gains its value case when its record exists. // ============================================================================ // CatalogueEntryKind distinguishes a composed module from a solution. @@ -287,7 +293,7 @@ message CatalogueGap { string detail = 2; } -// CatalogueRelease is a declared release, publisher/name@version. +// CatalogueRelease is a release, publisher/name@version. message CatalogueRelease { string publisher = 1; string name = 2; @@ -301,22 +307,106 @@ message CatalogueExecution { uint64 build_incarnation = 2; } -// CatalogueRunningVerdict compares the observed execution with the declared -// one. It exists only when both are known; anything less is a CatalogueGap. -enum CatalogueRunningVerdict { - CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; - // The observed image digest and incarnation are the declared ones. - CATALOGUE_RUNNING_VERDICT_MATCHES = 1; - // Something other than the declared execution is running — what cluster +// CatalogueDesired is what the composition declares. +message CatalogueDesired { + oneof declared_revision_value { + int64 declared_revision = 1; + CatalogueGap declared_revision_gap = 2; + } + oneof declared_release_value { + CatalogueRelease declared_release = 3; + CatalogueGap declared_release_gap = 4; + } +} + +// CatalogueAuthorization is a current platform approval: the authorized +// revision and the execution it approved. +message CatalogueAuthorization { + int64 authorized_revision = 1; + CatalogueExecution approved_execution = 2; +} + +// CatalogueNotAuthorized is a known absence of current authorization — +// never approved, or approval withdrawn. It is a statement the approval record +// makes, distinct from a CatalogueGap, where the host cannot tell. +message CatalogueNotAuthorized { + string detail = 1; +} + +// CatalogueAuthorized is what the platform has approved to execute. +message CatalogueAuthorized { + oneof authorization_value { + CatalogueAuthorization authorization = 1; + CatalogueNotAuthorized not_authorized = 2; + CatalogueGap authorization_gap = 3; + } +} + +// CatalogueApplied is what delivery actually wrote. +message CatalogueApplied { + oneof applied_revision_value { + int64 applied_revision = 1; + CatalogueGap applied_revision_gap = 2; + } +} + +// CatalogueObservedVerdict judges what runs against what is authorized — +// never against what is declared, because only authorization admits execution. +enum CatalogueObservedVerdict { + CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; + // The observed image digest and incarnation are the approved execution. + CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; + // Something other than the approved execution is running: what cluster // admission refuses. - CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; + CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS = 2; + // Something is running with no current authorization at all: what the + // retirement sweep exists to stop. + CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED = 3; } -// CatalogueRunning is the running incarnation beside the declared one. -message CatalogueRunning { - CatalogueExecution declared = 1; - CatalogueExecution observed = 2; - CatalogueRunningVerdict verdict = 3; +// CatalogueObserved is what is actually running. +message CatalogueObserved { + oneof observed_revision_value { + int64 observed_revision = 1; + CatalogueGap observed_revision_gap = 2; + } + // When the observation was made; a stale observation is never current. + oneof observation_freshness_value { + google.protobuf.Timestamp observed_at = 3; + CatalogueGap observation_freshness_gap = 4; + } + oneof observed_execution_value { + CatalogueExecution observed_execution = 5; + CatalogueGap observed_execution_gap = 6; + } + // A verdict exists only when both an observation and a known authorization + // do; anything less is a gap, and an unobserved entry is never a match. + oneof verdict_value { + CatalogueObservedVerdict verdict = 7; + CatalogueGap verdict_gap = 8; + } +} + +// CatalogueWithdrawing is approval withdrawn and retirement not yet complete, +// acknowledged separately from retirement itself. The value cases are the +// deployment contract's withdrawal and revocation records; until they exist +// each field holds only its gap. +message CatalogueWithdrawing { + oneof withdrawal_state_value { + CatalogueGap withdrawal_state_gap = 1; + } + oneof credential_revocation_state_value { + CatalogueGap credential_revocation_state_gap = 2; + } +} + +// CatalogueRetired is whether the workload is physically stopped. Retired +// requires stop or fence evidence; until a retirement controller reports to +// this host, the field holds only its gap and nothing reads as retired. +message CatalogueRetired { + oneof retirement_state_value { + CatalogueGap retirement_state_gap = 1; + } } // CatalogueInstallation is one organization's installation of a solution. @@ -329,7 +419,7 @@ message CatalogueInstallation { CatalogueRelease agent_release = 3; CatalogueGap agent_release_gap = 4; } - // The installation revision the installed generation is pinned at. + // The installation's own revision, independent of the deployment's. oneof revision_value { CatalogueGap revision_gap = 5; } @@ -338,7 +428,11 @@ message CatalogueInstallation { repeated CollectionReadGrant exposed_teams = 6; } -// CatalogueEntry is one module or solution. +// CatalogueEntry is one module or solution, placed in the registry's state +// model: desired, authorized, applied, observed, withdrawing and physically +// retired are distinct, and a row shows each separately, so an entry desired +// but not authorized, or observed but no longer authorized, reads as exactly +// that. message CatalogueEntry { CatalogueEntryKind kind = 1; // A module's principal prefix, or a solution's registry id — the identifier @@ -349,37 +443,25 @@ message CatalogueEntry { string publisher = 3; CatalogueGap publisher_gap = 4; } - oneof declared_release_value { - CatalogueRelease declared_release = 5; - CatalogueGap declared_release_gap = 6; - } - // The OCI image manifest digest the entry was published at. - oneof build_digest_value { - string build_digest = 7; - CatalogueGap build_digest_gap = 8; - } - // The presence generation this host applied. - oneof generation_value { - int64 generation = 9; - CatalogueGap generation_gap = 10; - } - oneof running_value { - CatalogueRunning running = 11; - CatalogueGap running_gap = 12; - } + CatalogueDesired desired = 5; + CatalogueAuthorized authorized = 6; + CatalogueApplied applied = 7; + CatalogueObserved observed = 8; + CatalogueWithdrawing withdrawing = 9; + CatalogueRetired retired = 10; // Lines of code, backend and frontend, vendored paths excluded. The value // case is the presence document's build_size section (codefly-dev/core#708) // as delivered; this host counts nothing itself. oneof build_size_value { - CatalogueGap build_size_gap = 13; + CatalogueGap build_size_gap = 11; } - // The solution's registration, with deployment topology withheld: the + // The solution's self-registration, with deployment topology withheld: the // frontend manifest, backend upstream and service alias are always empty // here, as on every browser-facing projection of the registry. Absent for a // module, which does not self-register, and for a solution known only from // its installations. - optional SolutionRegistration registration = 14; - repeated CatalogueInstallation installations = 15; + optional SolutionRegistration registration = 12; + repeated CatalogueInstallation installations = 13; } message ListPlatformCatalogueRequest { diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index e1cd68f066d2eb1eee8e8b3f660cbc6817ce6b1a..9b89d9def1b008d9a75973c66811bfb0c179c984 100644 GIT binary patch delta 17440 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�pAc6TXXBS-HJ9g|&9_ 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

F9$%wn<>k6;%1Z)D{f_GIZif;s5^Pa0r9B$Epy zNr*f}CKpJO0Q+HSE|8?Dn*EDRE|8>w>tAJZfg}lBr^;mKlLW5QWO9Kd30$YQas<~I zGAUWC2tRO1aYHhBU9*o$=lUq!zySf{h=CLy#k|W z$qR>IwE9C_1^293a_If32xA2>&X!_fgb^FKLUWugorMWU(hxT^97#g##|%f3z;TXD zIw(mB#sI=LLo%Y2Y~Da3l#Fn_78-;{wAGOA=2=abtwBBHH>-NM}fd zQA?QB6Ve}SFVJP!#*>RBXRBm*_G@>3uvjLy_BI%~SSGjjHYmi!GP$+40l{aaSdkQB z8x8Oo>8wZqk_Ld!8h|7r@^c0tNdUMcrw~a~1eY3uq=De`h670;_<{jQ5&$kU07(MC u<*gh6u-O2_ibRiJbzG diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index 8c79d0188..00f45cfff 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -248,14 +248,20 @@ message UpsertFeatureFlagResponse { // the durable solution registry, the installations table, and the composition's // module principal registry — and it adds no store of its own. // -// Several facts the Catalogue is specified to show are carried by records this -// host does not hold yet: the declared release, build digest and generation -// arrive with applied presence documents, the running incarnation needs observed -// cluster state, and build size is a signed section of the presence document -// (codefly-dev/core#708, computed by the CLI at build). Each such fact is a -// oneof of its value and a CatalogueGap, so an absent fact is reported with its -// reason and is never an empty value a reader could take for zero, unset, or a -// match. A fact gains its value case when the record that carries it exists. +// Each entry is placed in the registry's state model — desired, authorized, +// applied, observed, withdrawing, physically retired — with the public status +// fields that model names (declared_revision, authorized_revision, +// applied_revision, observed_revision, observation_freshness, withdrawal_state, +// retirement_state, credential_revocation_state), never one "installed" flag. +// +// Most of those facts are carried by records this host does not hold yet: +// declarations and applied generations arrive with presence documents, approval +// and withdrawal with the deployment contract's signed approval, observations +// and retirement with cluster observation, and build size is a signed section of +// the presence document (codefly-dev/core#708, computed by the CLI at build). +// Each such fact is a oneof of its value and a CatalogueGap, so an absent fact is +// reported with its reason and is never an empty value a reader could take for +// zero, unset, or a match. A fact gains its value case when its record exists. // ============================================================================ // CatalogueEntryKind distinguishes a composed module from a solution. @@ -287,7 +293,7 @@ message CatalogueGap { string detail = 2; } -// CatalogueRelease is a declared release, publisher/name@version. +// CatalogueRelease is a release, publisher/name@version. message CatalogueRelease { string publisher = 1; string name = 2; @@ -301,22 +307,106 @@ message CatalogueExecution { uint64 build_incarnation = 2; } -// CatalogueRunningVerdict compares the observed execution with the declared -// one. It exists only when both are known; anything less is a CatalogueGap. -enum CatalogueRunningVerdict { - CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; - // The observed image digest and incarnation are the declared ones. - CATALOGUE_RUNNING_VERDICT_MATCHES = 1; - // Something other than the declared execution is running — what cluster +// CatalogueDesired is what the composition declares. +message CatalogueDesired { + oneof declared_revision_value { + int64 declared_revision = 1; + CatalogueGap declared_revision_gap = 2; + } + oneof declared_release_value { + CatalogueRelease declared_release = 3; + CatalogueGap declared_release_gap = 4; + } +} + +// CatalogueAuthorization is a current platform approval: the authorized +// revision and the execution it approved. +message CatalogueAuthorization { + int64 authorized_revision = 1; + CatalogueExecution approved_execution = 2; +} + +// CatalogueNotAuthorized is a known absence of current authorization — +// never approved, or approval withdrawn. It is a statement the approval record +// makes, distinct from a CatalogueGap, where the host cannot tell. +message CatalogueNotAuthorized { + string detail = 1; +} + +// CatalogueAuthorized is what the platform has approved to execute. +message CatalogueAuthorized { + oneof authorization_value { + CatalogueAuthorization authorization = 1; + CatalogueNotAuthorized not_authorized = 2; + CatalogueGap authorization_gap = 3; + } +} + +// CatalogueApplied is what delivery actually wrote. +message CatalogueApplied { + oneof applied_revision_value { + int64 applied_revision = 1; + CatalogueGap applied_revision_gap = 2; + } +} + +// CatalogueObservedVerdict judges what runs against what is authorized — +// never against what is declared, because only authorization admits execution. +enum CatalogueObservedVerdict { + CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; + // The observed image digest and incarnation are the approved execution. + CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; + // Something other than the approved execution is running: what cluster // admission refuses. - CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; + CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS = 2; + // Something is running with no current authorization at all: what the + // retirement sweep exists to stop. + CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED = 3; } -// CatalogueRunning is the running incarnation beside the declared one. -message CatalogueRunning { - CatalogueExecution declared = 1; - CatalogueExecution observed = 2; - CatalogueRunningVerdict verdict = 3; +// CatalogueObserved is what is actually running. +message CatalogueObserved { + oneof observed_revision_value { + int64 observed_revision = 1; + CatalogueGap observed_revision_gap = 2; + } + // When the observation was made; a stale observation is never current. + oneof observation_freshness_value { + google.protobuf.Timestamp observed_at = 3; + CatalogueGap observation_freshness_gap = 4; + } + oneof observed_execution_value { + CatalogueExecution observed_execution = 5; + CatalogueGap observed_execution_gap = 6; + } + // A verdict exists only when both an observation and a known authorization + // do; anything less is a gap, and an unobserved entry is never a match. + oneof verdict_value { + CatalogueObservedVerdict verdict = 7; + CatalogueGap verdict_gap = 8; + } +} + +// CatalogueWithdrawing is approval withdrawn and retirement not yet complete, +// acknowledged separately from retirement itself. The value cases are the +// deployment contract's withdrawal and revocation records; until they exist +// each field holds only its gap. +message CatalogueWithdrawing { + oneof withdrawal_state_value { + CatalogueGap withdrawal_state_gap = 1; + } + oneof credential_revocation_state_value { + CatalogueGap credential_revocation_state_gap = 2; + } +} + +// CatalogueRetired is whether the workload is physically stopped. Retired +// requires stop or fence evidence; until a retirement controller reports to +// this host, the field holds only its gap and nothing reads as retired. +message CatalogueRetired { + oneof retirement_state_value { + CatalogueGap retirement_state_gap = 1; + } } // CatalogueInstallation is one organization's installation of a solution. @@ -329,7 +419,7 @@ message CatalogueInstallation { CatalogueRelease agent_release = 3; CatalogueGap agent_release_gap = 4; } - // The installation revision the installed generation is pinned at. + // The installation's own revision, independent of the deployment's. oneof revision_value { CatalogueGap revision_gap = 5; } @@ -338,7 +428,11 @@ message CatalogueInstallation { repeated CollectionReadGrant exposed_teams = 6; } -// CatalogueEntry is one module or solution. +// CatalogueEntry is one module or solution, placed in the registry's state +// model: desired, authorized, applied, observed, withdrawing and physically +// retired are distinct, and a row shows each separately, so an entry desired +// but not authorized, or observed but no longer authorized, reads as exactly +// that. message CatalogueEntry { CatalogueEntryKind kind = 1; // A module's principal prefix, or a solution's registry id — the identifier @@ -349,37 +443,25 @@ message CatalogueEntry { string publisher = 3; CatalogueGap publisher_gap = 4; } - oneof declared_release_value { - CatalogueRelease declared_release = 5; - CatalogueGap declared_release_gap = 6; - } - // The OCI image manifest digest the entry was published at. - oneof build_digest_value { - string build_digest = 7; - CatalogueGap build_digest_gap = 8; - } - // The presence generation this host applied. - oneof generation_value { - int64 generation = 9; - CatalogueGap generation_gap = 10; - } - oneof running_value { - CatalogueRunning running = 11; - CatalogueGap running_gap = 12; - } + CatalogueDesired desired = 5; + CatalogueAuthorized authorized = 6; + CatalogueApplied applied = 7; + CatalogueObserved observed = 8; + CatalogueWithdrawing withdrawing = 9; + CatalogueRetired retired = 10; // Lines of code, backend and frontend, vendored paths excluded. The value // case is the presence document's build_size section (codefly-dev/core#708) // as delivered; this host counts nothing itself. oneof build_size_value { - CatalogueGap build_size_gap = 13; + CatalogueGap build_size_gap = 11; } - // The solution's registration, with deployment topology withheld: the + // The solution's self-registration, with deployment topology withheld: the // frontend manifest, backend upstream and service alias are always empty // here, as on every browser-facing projection of the registry. Absent for a // module, which does not self-register, and for a solution known only from // its installations. - optional SolutionRegistration registration = 14; - repeated CatalogueInstallation installations = 15; + optional SolutionRegistration registration = 12; + repeated CatalogueInstallation installations = 13; } message ListPlatformCatalogueRequest { diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index a9d5c8360..2d8511250 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048", + "digest": "sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18", "services": [ { "name": "APIKeyService", @@ -477,7 +477,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048", + "digest": "sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 6ede72a63..a6063f37c 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048 + digest: sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048 + digest: sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 799adeb02..05f083e54 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -155,20 +155,41 @@ team grants at its authority root or above (the same labelled-grant query `solution_identifier`, so that is the join; an identifier no registration carries becomes a solution entry known only by its installations. -**Absent facts are gaps, never values.** The declared release, build digest, -generation, running incarnation, installation revision and build size each have -a `oneof` of their value and a `CatalogueGap{reason, detail}`. Today no record on -this host carries the declared release, build digest, generation or installation -revision (they arrive with applied presence documents), nothing observes what -runs, and build size is the presence document's `build_size` section -(codefly-dev/core#708, computed by the CLI at build, codefly-dev/cli#901): the host -counts no lines itself. So every entry reports those as `NOT_RECORDED`, and -running as `NOT_OBSERVED`. A running verdict (`MATCHES` / `DIFFERS`) exists only -when both a declared and an observed `{image_digest, build_incarnation}` are -known (`setCatalogueRunning`); an unobserved deployment is never a match. When a -record starts carrying a fact, `newCatalogueEntry` reads it and the value case of -that `oneof` replaces the gap — build size gains its value field in the same -`oneof`. +**Each entry is placed in the registry's state model** — desired, authorized, +applied, observed, withdrawing, physically retired — one message per state, +carrying the public status fields that model names: `declared_revision`, +`authorized_revision`, `applied_revision`, `observed_revision`, +`observation_freshness`, `withdrawal_state`, `credential_revocation_state`, +`retirement_state`. Never one "installed" flag: an entry desired but not +authorized, or observed but no longer authorized, must read as exactly that. +The solution's self-registration status is shown under Desired, as the only +record of intent this host holds today. + +**Absent facts are gaps, never values.** Every fact is a `oneof` of its value and +a `CatalogueGap{reason, detail}`. Today no record on this host carries a +declaration or an applied generation (they arrive with applied presence +documents), an approval or a withdrawal (the deployment contract's signed +approval, not built yet), an observation or a retirement (nothing observes the +cluster or reports stop/fence evidence), an installation revision, or a build +size (the presence document's `build_size` section, codefly-dev/core#708, +computed by the CLI at build, codefly-dev/cli#901): the host counts no lines +itself. So each reads `NOT_RECORDED` or `NOT_OBSERVED`. + +**Authorization has three cases, not two**: an approval (`authorized_revision` + +the approved `{image_digest, build_incarnation}`), `not_authorized` — a known +absence the approval record states — and a gap, where the host cannot tell. An +unrecorded approval is never shown as a refusal. + +**The observed verdict judges against the authorization, never the +declaration** (`judgeCatalogueObserved`): `RUNNING_AUTHORIZED` only for the +approved digest and incarnation, `RUNNING_DIFFERS` for anything else, +`RUNNING_UNAUTHORIZED` for an observation with no current authorization — the +row the retirement sweep exists to stop. Without an observation it is +`NOT_OBSERVED`, and with an observation but no known authorization it is +`NOT_RECORDED`; neither is ever a match. Withdrawal, credential revocation, +retirement and build size have gap-only `oneof`s: their value vocabularies are +the deployment contract's and core#708's to define, and each value case joins +its `oneof` when that record exists, read in `newCatalogueEntry`. **The registration it returns withholds topology**: `catalogueRegistrationProto` blanks the frontend manifest, backend upstream and service alias, as every other diff --git a/module/services/accounts/code/pkg/business/platform_catalogue.go b/module/services/accounts/code/pkg/business/platform_catalogue.go index 7a8793d66..854ad109e 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue.go @@ -14,10 +14,13 @@ import ( // already keeps — the durable solution registry, the installations table and the // composition's module principal registry. It adds no store of its own. // -// Some facts the Catalogue is specified to show have no record on this host -// yet. Each is reported as a CatalogueGap naming why, never as an empty value: -// an unrecorded generation is not generation 0, an unreported build size is not -// zero lines, and an unobserved deployment is not one that matches. +// Each entry is placed in the registry's state model — desired, authorized, +// applied, observed, withdrawing, physically retired — each state reported +// separately. Most of those facts have no record on this host yet. Each is +// reported as a CatalogueGap naming why, never as an empty value: an unrecorded +// revision is not revision 0, an unreported build size is not zero lines, an +// unobserved deployment is not one that matches, and an unrecorded approval is +// not a refusal. // CatalogueInstallationRecord is one active installation as the Catalogue reads // it: the installation, its organization's name, the identifier of the agent @@ -44,12 +47,18 @@ type PlatformCatalogue struct { } const ( - catalogueNoPresenceDetail = "This host holds no applied presence document for this entry, which is what records it." + catalogueNoDeclaration = "This host holds no applied presence document for this entry, which is what records what the composition declares." + catalogueNoApplication = "This host has applied no presence generation for this entry." + catalogueNoApproval = "This host holds no approval record: signed platform approval of the execution inventory, bound to target and ownership scope, has not been built yet." catalogueNotObserved = "This host reads no observed cluster state, so what runs is unknown — never assumed to match." + catalogueNoVerdict = "There is no approval record to judge the observed execution against." + catalogueNoWithdrawal = "This host holds no withdrawal record; withdrawal is a change to the approval record, which does not exist yet." + catalogueNoRevocation = "This host holds no credential-revocation record for this entry." + catalogueNoRetirement = "No retirement controller reports to this host; without stop or fence evidence nothing is reported as retired." catalogueNoBuildSize = "Build size is the presence document's build_size section (codefly-dev/core#708), computed at build by the CLI; this host holds no presence document for this entry." catalogueModulePublisher = "A composed module is known by its principal prefix alone; the composition records no publisher." catalogueUnregistered = "No registration exists under this identifier; it is named only by its installations." - catalogueNoRevision = "Installations are not pinned to a presence revision on this host." + catalogueNoRevision = "Installations carry no revision of their own on this host." catalogueNoAgentRelease = "The installation's agent principal carries no publisher/name:version identifier." ) @@ -133,41 +142,71 @@ func projectPlatformCatalogue( return append(moduleEntries, solutionEntries...) } -// newCatalogueEntry is an entry whose presence-borne facts are all gaps: no -// record on this host carries a declared release, build digest, generation, -// observed execution or build size yet. When one does, the entry reads it here -// and the gap gives way to the value. +// newCatalogueEntry is an entry whose state facts are all gaps: no record on +// this host carries a declaration, an approval, an applied generation, an +// observation, a withdrawal, a retirement or a build size yet. When one does, +// the entry reads it here and the gap gives way to the value. func newCatalogueEntry(kind gen.CatalogueEntryKind, name string) *gen.CatalogueEntry { notRecorded := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED + notObserved := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED entry := &gen.CatalogueEntry{ - Kind: kind, - Name: name, - DeclaredReleaseValue: &gen.CatalogueEntry_DeclaredReleaseGap{DeclaredReleaseGap: catalogueGap(notRecorded, catalogueNoPresenceDetail)}, - BuildDigestValue: &gen.CatalogueEntry_BuildDigestGap{BuildDigestGap: catalogueGap(notRecorded, catalogueNoPresenceDetail)}, - GenerationValue: &gen.CatalogueEntry_GenerationGap{GenerationGap: catalogueGap(notRecorded, catalogueNoPresenceDetail)}, - BuildSizeValue: &gen.CatalogueEntry_BuildSizeGap{BuildSizeGap: catalogueGap(notRecorded, catalogueNoBuildSize)}, + Kind: kind, + Name: name, + Desired: &gen.CatalogueDesired{ + DeclaredRevisionValue: &gen.CatalogueDesired_DeclaredRevisionGap{DeclaredRevisionGap: catalogueGap(notRecorded, catalogueNoDeclaration)}, + DeclaredReleaseValue: &gen.CatalogueDesired_DeclaredReleaseGap{DeclaredReleaseGap: catalogueGap(notRecorded, catalogueNoDeclaration)}, + }, + Authorized: &gen.CatalogueAuthorized{ + AuthorizationValue: &gen.CatalogueAuthorized_AuthorizationGap{AuthorizationGap: catalogueGap(notRecorded, catalogueNoApproval)}, + }, + Applied: &gen.CatalogueApplied{ + AppliedRevisionValue: &gen.CatalogueApplied_AppliedRevisionGap{AppliedRevisionGap: catalogueGap(notRecorded, catalogueNoApplication)}, + }, + Observed: &gen.CatalogueObserved{ + ObservedRevisionValue: &gen.CatalogueObserved_ObservedRevisionGap{ObservedRevisionGap: catalogueGap(notObserved, catalogueNotObserved)}, + ObservationFreshnessValue: &gen.CatalogueObserved_ObservationFreshnessGap{ObservationFreshnessGap: catalogueGap(notObserved, catalogueNotObserved)}, + ObservedExecutionValue: &gen.CatalogueObserved_ObservedExecutionGap{ObservedExecutionGap: catalogueGap(notObserved, catalogueNotObserved)}, + }, + Withdrawing: &gen.CatalogueWithdrawing{ + WithdrawalStateValue: &gen.CatalogueWithdrawing_WithdrawalStateGap{WithdrawalStateGap: catalogueGap(notRecorded, catalogueNoWithdrawal)}, + CredentialRevocationStateValue: &gen.CatalogueWithdrawing_CredentialRevocationStateGap{CredentialRevocationStateGap: catalogueGap(notRecorded, catalogueNoRevocation)}, + }, + Retired: &gen.CatalogueRetired{ + RetirementStateValue: &gen.CatalogueRetired_RetirementStateGap{RetirementStateGap: catalogueGap(notObserved, catalogueNoRetirement)}, + }, + BuildSizeValue: &gen.CatalogueEntry_BuildSizeGap{BuildSizeGap: catalogueGap(notRecorded, catalogueNoBuildSize)}, } - setCatalogueRunning(entry, nil, nil) + judgeCatalogueObserved(entry.Authorized, entry.Observed) return entry } -// setCatalogueRunning states what runs beside what was declared. A verdict is -// given only when both are known: without an observation the answer is -// NOT_OBSERVED, and without a declaration there is nothing to compare an -// observation with. Neither is ever reported as a match. -func setCatalogueRunning(entry *gen.CatalogueEntry, declared, observed *gen.CatalogueExecution) { +// judgeCatalogueObserved sets the observed verdict from the authorized and +// observed states the entry already carries. It judges what runs against what +// is authorized, never against what is declared, and gives a verdict only when +// both are known: +// +// - no observation: NOT_OBSERVED, never a match; +// - an observation, but the host cannot tell what is authorized: NOT_RECORDED; +// - an observation with no current authorization: RUNNING_UNAUTHORIZED; +// - an observation of the approved digest and incarnation: RUNNING_AUTHORIZED; +// - anything else: RUNNING_DIFFERS. +func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.CatalogueObserved) { + running := observed.GetObservedExecution() switch { - case observed == nil: - entry.RunningValue = &gen.CatalogueEntry_RunningGap{RunningGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, catalogueNotObserved)} - case declared == nil: - entry.RunningValue = &gen.CatalogueEntry_RunningGap{RunningGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoPresenceDetail)} + case running == nil: + observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, catalogueNotObserved)} + case authorized.GetNotAuthorized() != nil: + observed.VerdictValue = &gen.CatalogueObserved_Verdict{Verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED} + case authorized.GetAuthorization() == nil: + observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoVerdict)} default: - verdict := gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS - if declared.GetImageDigest() != "" && declared.GetImageDigest() == observed.GetImageDigest() && - declared.GetBuildIncarnation() == observed.GetBuildIncarnation() { - verdict = gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_MATCHES + approved := authorized.GetAuthorization().GetApprovedExecution() + verdict := gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS + if approved.GetImageDigest() != "" && approved.GetImageDigest() == running.GetImageDigest() && + approved.GetBuildIncarnation() == running.GetBuildIncarnation() { + verdict = gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED } - entry.RunningValue = &gen.CatalogueEntry_Running{Running: &gen.CatalogueRunning{Declared: declared, Observed: observed, Verdict: verdict}} + observed.VerdictValue = &gen.CatalogueObserved_Verdict{Verdict: verdict} } } diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_test.go index 247b85cbe..c755a3281 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue_test.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue_test.go @@ -9,65 +9,88 @@ import ( gen "accounts/pkg/gen/saas/accounts/v1" ) -// The running column answers "is what runs what was declared?" Only two known -// executions get a verdict; an unobserved one is never reported as a match. -func TestCatalogueRunningVerdict(t *testing.T) { - declared := &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3} +// The observed state's verdict answers "is what runs what is authorized?" It is +// judged against the approval, never the declaration, and given only when both +// an observation and a known authorization exist: an unobserved entry is never a +// match, and an entry the host cannot judge is never "unauthorized". +func TestCatalogueObservedVerdict(t *testing.T) { + approved := &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3} + authorized := &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ + Authorization: &gen.CatalogueAuthorization{AuthorizedRevision: 4, ApprovedExecution: approved}, + }} + notAuthorized := &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_NotAuthorized{ + NotAuthorized: &gen.CatalogueNotAuthorized{Detail: "approval withdrawn"}, + }} + unknown := &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_AuthorizationGap{ + AuthorizationGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, "no approval record"), + }} cases := []struct { - name string - declared *gen.CatalogueExecution - observed *gen.CatalogueExecution - verdict gen.CatalogueRunningVerdict - gap gen.CatalogueGapReason + name string + authorized *gen.CatalogueAuthorized + observed *gen.CatalogueExecution + verdict gen.CatalogueObservedVerdict + gap gen.CatalogueGapReason }{ - {name: "nothing observed", declared: declared, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, - {name: "nothing declared or observed", gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, + {name: "authorized, nothing observed", authorized: authorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, + {name: "not authorized, nothing observed", authorized: notAuthorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, { - name: "observed without a declaration", - observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, - gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, + name: "observed, authorization unknown to this host", + authorized: unknown, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, + gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, }, { - name: "same digest and incarnation", - declared: declared, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, - verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_MATCHES, + name: "the approved digest and incarnation", + authorized: authorized, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED, }, { - name: "another image", - declared: declared, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:bbb", BuildIncarnation: 3}, - verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS, + name: "another image", + authorized: authorized, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:bbb", BuildIncarnation: 3}, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { // The incarnation moves when command, configuration or identity // change under the same image, so the image alone does not match. - name: "same image, another incarnation", - declared: declared, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 2}, - verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS, + name: "same image, another incarnation", + authorized: authorized, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 2}, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { - name: "two empty digests are not a match", - declared: &gen.CatalogueExecution{}, + name: "two empty digests are not a match", + authorized: &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ + Authorization: &gen.CatalogueAuthorization{ApprovedExecution: &gen.CatalogueExecution{}}, + }}, observed: &gen.CatalogueExecution{}, - verdict: gen.CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + }, + { + // The dangerous row: something runs that nothing currently + // authorizes, whatever its image. + name: "observed with no current authorization", + authorized: notAuthorized, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - entry := &gen.CatalogueEntry{} - setCatalogueRunning(entry, tc.declared, tc.observed) + observed := &gen.CatalogueObserved{} + if tc.observed != nil { + observed.ObservedExecutionValue = &gen.CatalogueObserved_ObservedExecution{ObservedExecution: tc.observed} + } + judgeCatalogueObserved(tc.authorized, observed) if tc.gap != gen.CatalogueGapReason_CATALOGUE_GAP_REASON_UNSPECIFIED { - require.Nil(t, entry.GetRunning()) - require.Equal(t, tc.gap, entry.GetRunningGap().GetReason()) - require.NotEmpty(t, entry.GetRunningGap().GetDetail()) + require.Equal(t, gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED, observed.GetVerdict()) + require.Equal(t, tc.gap, observed.GetVerdictGap().GetReason()) + require.NotEmpty(t, observed.GetVerdictGap().GetDetail()) return } - require.Nil(t, entry.GetRunningGap()) - require.Equal(t, tc.verdict, entry.GetRunning().GetVerdict()) - require.Same(t, tc.declared, entry.GetRunning().GetDeclared()) - require.Same(t, tc.observed, entry.GetRunning().GetObserved()) + require.Nil(t, observed.GetVerdictGap()) + require.Equal(t, tc.verdict, observed.GetVerdict()) }) } } @@ -129,17 +152,29 @@ func TestProjectPlatformCatalogue(t *testing.T) { byName[entry.Entry.GetName()] = entry } - // Every entry reports the presence-borne facts as gaps, never as values. + // Every entry reports each state's facts as gaps, never as values: no + // record on this host carries them yet, so nothing reads as authorized, + // applied, running, withdrawn or retired. for _, entry := range entries { e := entry.Entry notRecorded := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED - require.Equal(t, notRecorded, e.GetDeclaredReleaseGap().GetReason(), e.GetName()) - require.Equal(t, notRecorded, e.GetBuildDigestGap().GetReason(), e.GetName()) - require.Equal(t, notRecorded, e.GetGenerationGap().GetReason(), e.GetName()) + notObserved := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED + require.Equal(t, notRecorded, e.GetDesired().GetDeclaredRevisionGap().GetReason(), e.GetName()) + require.Equal(t, notRecorded, e.GetDesired().GetDeclaredReleaseGap().GetReason(), e.GetName()) + require.Equal(t, notRecorded, e.GetAuthorized().GetAuthorizationGap().GetReason(), e.GetName()) + require.Nil(t, e.GetAuthorized().GetNotAuthorized(), "%s: an unrecorded approval is not a refusal", e.GetName()) + require.Equal(t, notRecorded, e.GetApplied().GetAppliedRevisionGap().GetReason(), e.GetName()) + require.Equal(t, notObserved, e.GetObserved().GetObservedRevisionGap().GetReason(), e.GetName()) + require.Equal(t, notObserved, e.GetObserved().GetObservationFreshnessGap().GetReason(), e.GetName()) + require.Equal(t, notObserved, e.GetObserved().GetObservedExecutionGap().GetReason(), e.GetName()) + require.Equal(t, notObserved, e.GetObserved().GetVerdictGap().GetReason(), e.GetName()) + require.Equal(t, gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED, e.GetObserved().GetVerdict(), + "%s: an unobserved entry has no verdict", e.GetName()) + require.Equal(t, notRecorded, e.GetWithdrawing().GetWithdrawalStateGap().GetReason(), e.GetName()) + require.Equal(t, notRecorded, e.GetWithdrawing().GetCredentialRevocationStateGap().GetReason(), e.GetName()) + require.Equal(t, notObserved, e.GetRetired().GetRetirementStateGap().GetReason(), e.GetName()) require.Equal(t, notRecorded, e.GetBuildSizeGap().GetReason(), e.GetName()) require.Contains(t, e.GetBuildSizeGap().GetDetail(), "codefly-dev/core#708") - require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, e.GetRunningGap().GetReason(), e.GetName()) - require.Nil(t, e.GetRunning(), "%s: an unobserved entry has no verdict", e.GetName()) } module := byName["alpha"] diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index 36ae61af0..0f013401a 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -139,57 +139,62 @@ func (CatalogueGapReason) EnumDescriptor() ([]byte, []int) { return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{1} } -// CatalogueRunningVerdict compares the observed execution with the declared -// one. It exists only when both are known; anything less is a CatalogueGap. -type CatalogueRunningVerdict int32 +// CatalogueObservedVerdict judges what runs against what is authorized — +// never against what is declared, because only authorization admits execution. +type CatalogueObservedVerdict int32 const ( - CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_UNSPECIFIED CatalogueRunningVerdict = 0 - // The observed image digest and incarnation are the declared ones. - CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_MATCHES CatalogueRunningVerdict = 1 - // Something other than the declared execution is running — what cluster + CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED CatalogueObservedVerdict = 0 + // The observed image digest and incarnation are the approved execution. + CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED CatalogueObservedVerdict = 1 + // Something other than the approved execution is running: what cluster // admission refuses. - CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_DIFFERS CatalogueRunningVerdict = 2 + CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS CatalogueObservedVerdict = 2 + // Something is running with no current authorization at all: what the + // retirement sweep exists to stop. + CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED CatalogueObservedVerdict = 3 ) -// Enum value maps for CatalogueRunningVerdict. +// Enum value maps for CatalogueObservedVerdict. var ( - CatalogueRunningVerdict_name = map[int32]string{ - 0: "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", - 1: "CATALOGUE_RUNNING_VERDICT_MATCHES", - 2: "CATALOGUE_RUNNING_VERDICT_DIFFERS", - } - CatalogueRunningVerdict_value = map[string]int32{ - "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED": 0, - "CATALOGUE_RUNNING_VERDICT_MATCHES": 1, - "CATALOGUE_RUNNING_VERDICT_DIFFERS": 2, + CatalogueObservedVerdict_name = map[int32]string{ + 0: "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", + 1: "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", + 2: "CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS", + 3: "CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED", + } + CatalogueObservedVerdict_value = map[string]int32{ + "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED": 0, + "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED": 1, + "CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS": 2, + "CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED": 3, } ) -func (x CatalogueRunningVerdict) Enum() *CatalogueRunningVerdict { - p := new(CatalogueRunningVerdict) +func (x CatalogueObservedVerdict) Enum() *CatalogueObservedVerdict { + p := new(CatalogueObservedVerdict) *p = x return p } -func (x CatalogueRunningVerdict) String() string { +func (x CatalogueObservedVerdict) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } -func (CatalogueRunningVerdict) Descriptor() protoreflect.EnumDescriptor { +func (CatalogueObservedVerdict) Descriptor() protoreflect.EnumDescriptor { return file_saas_accounts_v1_platform_admin_proto_enumTypes[2].Descriptor() } -func (CatalogueRunningVerdict) Type() protoreflect.EnumType { +func (CatalogueObservedVerdict) Type() protoreflect.EnumType { return &file_saas_accounts_v1_platform_admin_proto_enumTypes[2] } -func (x CatalogueRunningVerdict) Number() protoreflect.EnumNumber { +func (x CatalogueObservedVerdict) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } -// Deprecated: Use CatalogueRunningVerdict.Descriptor instead. -func (CatalogueRunningVerdict) EnumDescriptor() ([]byte, []int) { +// Deprecated: Use CatalogueObservedVerdict.Descriptor instead. +func (CatalogueObservedVerdict) EnumDescriptor() ([]byte, []int) { return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{2} } @@ -2023,7 +2028,7 @@ func (x *CatalogueGap) GetDetail() string { return "" } -// CatalogueRelease is a declared release, publisher/name@version. +// CatalogueRelease is a release, publisher/name@version. type CatalogueRelease struct { state protoimpl.MessageState `protogen:"open.v1"` Publisher string `protobuf:"bytes,1,opt,name=publisher,proto3" json:"publisher,omitempty"` @@ -2138,30 +2143,37 @@ func (x *CatalogueExecution) GetBuildIncarnation() uint64 { return 0 } -// CatalogueRunning is the running incarnation beside the declared one. -type CatalogueRunning struct { - state protoimpl.MessageState `protogen:"open.v1"` - Declared *CatalogueExecution `protobuf:"bytes,1,opt,name=declared,proto3" json:"declared,omitempty"` - Observed *CatalogueExecution `protobuf:"bytes,2,opt,name=observed,proto3" json:"observed,omitempty"` - Verdict CatalogueRunningVerdict `protobuf:"varint,3,opt,name=verdict,proto3,enum=saas.accounts.v1.CatalogueRunningVerdict" json:"verdict,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +// CatalogueDesired is what the composition declares. +type CatalogueDesired struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to DeclaredRevisionValue: + // + // *CatalogueDesired_DeclaredRevision + // *CatalogueDesired_DeclaredRevisionGap + DeclaredRevisionValue isCatalogueDesired_DeclaredRevisionValue `protobuf_oneof:"declared_revision_value"` + // Types that are valid to be assigned to DeclaredReleaseValue: + // + // *CatalogueDesired_DeclaredRelease + // *CatalogueDesired_DeclaredReleaseGap + DeclaredReleaseValue isCatalogueDesired_DeclaredReleaseValue `protobuf_oneof:"declared_release_value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *CatalogueRunning) Reset() { - *x = CatalogueRunning{} +func (x *CatalogueDesired) Reset() { + *x = CatalogueDesired{} mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CatalogueRunning) String() string { +func (x *CatalogueDesired) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CatalogueRunning) ProtoMessage() {} +func (*CatalogueDesired) ProtoMessage() {} -func (x *CatalogueRunning) ProtoReflect() protoreflect.Message { +func (x *CatalogueDesired) ProtoReflect() protoreflect.Message { mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2173,72 +2185,117 @@ func (x *CatalogueRunning) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CatalogueRunning.ProtoReflect.Descriptor instead. -func (*CatalogueRunning) Descriptor() ([]byte, []int) { +// Deprecated: Use CatalogueDesired.ProtoReflect.Descriptor instead. +func (*CatalogueDesired) Descriptor() ([]byte, []int) { return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{35} } -func (x *CatalogueRunning) GetDeclared() *CatalogueExecution { +func (x *CatalogueDesired) GetDeclaredRevisionValue() isCatalogueDesired_DeclaredRevisionValue { if x != nil { - return x.Declared + return x.DeclaredRevisionValue } return nil } -func (x *CatalogueRunning) GetObserved() *CatalogueExecution { +func (x *CatalogueDesired) GetDeclaredRevision() int64 { if x != nil { - return x.Observed + if x, ok := x.DeclaredRevisionValue.(*CatalogueDesired_DeclaredRevision); ok { + return x.DeclaredRevision + } + } + return 0 +} + +func (x *CatalogueDesired) GetDeclaredRevisionGap() *CatalogueGap { + if x != nil { + if x, ok := x.DeclaredRevisionValue.(*CatalogueDesired_DeclaredRevisionGap); ok { + return x.DeclaredRevisionGap + } } return nil } -func (x *CatalogueRunning) GetVerdict() CatalogueRunningVerdict { +func (x *CatalogueDesired) GetDeclaredReleaseValue() isCatalogueDesired_DeclaredReleaseValue { if x != nil { - return x.Verdict + return x.DeclaredReleaseValue } - return CatalogueRunningVerdict_CATALOGUE_RUNNING_VERDICT_UNSPECIFIED + return nil } -// CatalogueInstallation is one organization's installation of a solution. -type CatalogueInstallation struct { - state protoimpl.MessageState `protogen:"open.v1"` - Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` - OrgName string `protobuf:"bytes,2,opt,name=org_name,json=orgName,proto3" json:"org_name,omitempty"` - // The release the installation's agent principal was created for, read from - // its canonical publisher/name:version identifier. - // - // Types that are valid to be assigned to AgentReleaseValue: - // - // *CatalogueInstallation_AgentRelease - // *CatalogueInstallation_AgentReleaseGap - AgentReleaseValue isCatalogueInstallation_AgentReleaseValue `protobuf_oneof:"agent_release_value"` - // The installation revision the installed generation is pinned at. - // - // Types that are valid to be assigned to RevisionValue: - // - // *CatalogueInstallation_RevisionGap - RevisionValue isCatalogueInstallation_RevisionValue `protobuf_oneof:"revision_value"` - // Active team grants at the installation's authority root or above it, so - // every team whose members the grant reaches inside this installation. - ExposedTeams []*CollectionReadGrant `protobuf:"bytes,6,rep,name=exposed_teams,json=exposedTeams,proto3" json:"exposed_teams,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *CatalogueDesired) GetDeclaredRelease() *CatalogueRelease { + if x != nil { + if x, ok := x.DeclaredReleaseValue.(*CatalogueDesired_DeclaredRelease); ok { + return x.DeclaredRelease + } + } + return nil } -func (x *CatalogueInstallation) Reset() { - *x = CatalogueInstallation{} +func (x *CatalogueDesired) GetDeclaredReleaseGap() *CatalogueGap { + if x != nil { + if x, ok := x.DeclaredReleaseValue.(*CatalogueDesired_DeclaredReleaseGap); ok { + return x.DeclaredReleaseGap + } + } + return nil +} + +type isCatalogueDesired_DeclaredRevisionValue interface { + isCatalogueDesired_DeclaredRevisionValue() +} + +type CatalogueDesired_DeclaredRevision struct { + DeclaredRevision int64 `protobuf:"varint,1,opt,name=declared_revision,json=declaredRevision,proto3,oneof"` +} + +type CatalogueDesired_DeclaredRevisionGap struct { + DeclaredRevisionGap *CatalogueGap `protobuf:"bytes,2,opt,name=declared_revision_gap,json=declaredRevisionGap,proto3,oneof"` +} + +func (*CatalogueDesired_DeclaredRevision) isCatalogueDesired_DeclaredRevisionValue() {} + +func (*CatalogueDesired_DeclaredRevisionGap) isCatalogueDesired_DeclaredRevisionValue() {} + +type isCatalogueDesired_DeclaredReleaseValue interface { + isCatalogueDesired_DeclaredReleaseValue() +} + +type CatalogueDesired_DeclaredRelease struct { + DeclaredRelease *CatalogueRelease `protobuf:"bytes,3,opt,name=declared_release,json=declaredRelease,proto3,oneof"` +} + +type CatalogueDesired_DeclaredReleaseGap struct { + DeclaredReleaseGap *CatalogueGap `protobuf:"bytes,4,opt,name=declared_release_gap,json=declaredReleaseGap,proto3,oneof"` +} + +func (*CatalogueDesired_DeclaredRelease) isCatalogueDesired_DeclaredReleaseValue() {} + +func (*CatalogueDesired_DeclaredReleaseGap) isCatalogueDesired_DeclaredReleaseValue() {} + +// CatalogueAuthorization is a current platform approval: the authorized +// revision and the execution it approved. +type CatalogueAuthorization struct { + state protoimpl.MessageState `protogen:"open.v1"` + AuthorizedRevision int64 `protobuf:"varint,1,opt,name=authorized_revision,json=authorizedRevision,proto3" json:"authorized_revision,omitempty"` + ApprovedExecution *CatalogueExecution `protobuf:"bytes,2,opt,name=approved_execution,json=approvedExecution,proto3" json:"approved_execution,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueAuthorization) Reset() { + *x = CatalogueAuthorization{} mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CatalogueInstallation) String() string { +func (x *CatalogueAuthorization) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CatalogueInstallation) ProtoMessage() {} +func (*CatalogueAuthorization) ProtoMessage() {} -func (x *CatalogueInstallation) ProtoReflect() protoreflect.Message { +func (x *CatalogueAuthorization) ProtoReflect() protoreflect.Message { mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -2250,171 +2307,198 @@ func (x *CatalogueInstallation) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CatalogueInstallation.ProtoReflect.Descriptor instead. -func (*CatalogueInstallation) Descriptor() ([]byte, []int) { +// Deprecated: Use CatalogueAuthorization.ProtoReflect.Descriptor instead. +func (*CatalogueAuthorization) Descriptor() ([]byte, []int) { return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{36} } -func (x *CatalogueInstallation) GetInstallation() *Installation { +func (x *CatalogueAuthorization) GetAuthorizedRevision() int64 { if x != nil { - return x.Installation + return x.AuthorizedRevision + } + return 0 +} + +func (x *CatalogueAuthorization) GetApprovedExecution() *CatalogueExecution { + if x != nil { + return x.ApprovedExecution } return nil } -func (x *CatalogueInstallation) GetOrgName() string { +// CatalogueNotAuthorized is a known absence of current authorization — +// never approved, or approval withdrawn. It is a statement the approval record +// makes, distinct from a CatalogueGap, where the host cannot tell. +type CatalogueNotAuthorized struct { + state protoimpl.MessageState `protogen:"open.v1"` + Detail string `protobuf:"bytes,1,opt,name=detail,proto3" json:"detail,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueNotAuthorized) Reset() { + *x = CatalogueNotAuthorized{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueNotAuthorized) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueNotAuthorized) ProtoMessage() {} + +func (x *CatalogueNotAuthorized) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] if x != nil { - return x.OrgName + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms } - return "" + return mi.MessageOf(x) } -func (x *CatalogueInstallation) GetAgentReleaseValue() isCatalogueInstallation_AgentReleaseValue { +// Deprecated: Use CatalogueNotAuthorized.ProtoReflect.Descriptor instead. +func (*CatalogueNotAuthorized) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{37} +} + +func (x *CatalogueNotAuthorized) GetDetail() string { if x != nil { - return x.AgentReleaseValue + return x.Detail } - return nil + return "" } -func (x *CatalogueInstallation) GetAgentRelease() *CatalogueRelease { +// CatalogueAuthorized is what the platform has approved to execute. +type CatalogueAuthorized struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to AuthorizationValue: + // + // *CatalogueAuthorized_Authorization + // *CatalogueAuthorized_NotAuthorized + // *CatalogueAuthorized_AuthorizationGap + AuthorizationValue isCatalogueAuthorized_AuthorizationValue `protobuf_oneof:"authorization_value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueAuthorized) Reset() { + *x = CatalogueAuthorized{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueAuthorized) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueAuthorized) ProtoMessage() {} + +func (x *CatalogueAuthorized) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] if x != nil { - if x, ok := x.AgentReleaseValue.(*CatalogueInstallation_AgentRelease); ok { - return x.AgentRelease + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) } + return ms } - return nil + return mi.MessageOf(x) } -func (x *CatalogueInstallation) GetAgentReleaseGap() *CatalogueGap { +// Deprecated: Use CatalogueAuthorized.ProtoReflect.Descriptor instead. +func (*CatalogueAuthorized) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{38} +} + +func (x *CatalogueAuthorized) GetAuthorizationValue() isCatalogueAuthorized_AuthorizationValue { if x != nil { - if x, ok := x.AgentReleaseValue.(*CatalogueInstallation_AgentReleaseGap); ok { - return x.AgentReleaseGap - } + return x.AuthorizationValue } return nil } -func (x *CatalogueInstallation) GetRevisionValue() isCatalogueInstallation_RevisionValue { +func (x *CatalogueAuthorized) GetAuthorization() *CatalogueAuthorization { if x != nil { - return x.RevisionValue + if x, ok := x.AuthorizationValue.(*CatalogueAuthorized_Authorization); ok { + return x.Authorization + } } return nil } -func (x *CatalogueInstallation) GetRevisionGap() *CatalogueGap { +func (x *CatalogueAuthorized) GetNotAuthorized() *CatalogueNotAuthorized { if x != nil { - if x, ok := x.RevisionValue.(*CatalogueInstallation_RevisionGap); ok { - return x.RevisionGap + if x, ok := x.AuthorizationValue.(*CatalogueAuthorized_NotAuthorized); ok { + return x.NotAuthorized } } return nil } -func (x *CatalogueInstallation) GetExposedTeams() []*CollectionReadGrant { +func (x *CatalogueAuthorized) GetAuthorizationGap() *CatalogueGap { if x != nil { - return x.ExposedTeams + if x, ok := x.AuthorizationValue.(*CatalogueAuthorized_AuthorizationGap); ok { + return x.AuthorizationGap + } } return nil } -type isCatalogueInstallation_AgentReleaseValue interface { - isCatalogueInstallation_AgentReleaseValue() +type isCatalogueAuthorized_AuthorizationValue interface { + isCatalogueAuthorized_AuthorizationValue() } -type CatalogueInstallation_AgentRelease struct { - AgentRelease *CatalogueRelease `protobuf:"bytes,3,opt,name=agent_release,json=agentRelease,proto3,oneof"` +type CatalogueAuthorized_Authorization struct { + Authorization *CatalogueAuthorization `protobuf:"bytes,1,opt,name=authorization,proto3,oneof"` } -type CatalogueInstallation_AgentReleaseGap struct { - AgentReleaseGap *CatalogueGap `protobuf:"bytes,4,opt,name=agent_release_gap,json=agentReleaseGap,proto3,oneof"` +type CatalogueAuthorized_NotAuthorized struct { + NotAuthorized *CatalogueNotAuthorized `protobuf:"bytes,2,opt,name=not_authorized,json=notAuthorized,proto3,oneof"` } -func (*CatalogueInstallation_AgentRelease) isCatalogueInstallation_AgentReleaseValue() {} - -func (*CatalogueInstallation_AgentReleaseGap) isCatalogueInstallation_AgentReleaseValue() {} - -type isCatalogueInstallation_RevisionValue interface { - isCatalogueInstallation_RevisionValue() +type CatalogueAuthorized_AuthorizationGap struct { + AuthorizationGap *CatalogueGap `protobuf:"bytes,3,opt,name=authorization_gap,json=authorizationGap,proto3,oneof"` } -type CatalogueInstallation_RevisionGap struct { - RevisionGap *CatalogueGap `protobuf:"bytes,5,opt,name=revision_gap,json=revisionGap,proto3,oneof"` -} +func (*CatalogueAuthorized_Authorization) isCatalogueAuthorized_AuthorizationValue() {} -func (*CatalogueInstallation_RevisionGap) isCatalogueInstallation_RevisionValue() {} +func (*CatalogueAuthorized_NotAuthorized) isCatalogueAuthorized_AuthorizationValue() {} -// CatalogueEntry is one module or solution. -type CatalogueEntry struct { +func (*CatalogueAuthorized_AuthorizationGap) isCatalogueAuthorized_AuthorizationValue() {} + +// CatalogueApplied is what delivery actually wrote. +type CatalogueApplied struct { state protoimpl.MessageState `protogen:"open.v1"` - Kind CatalogueEntryKind `protobuf:"varint,1,opt,name=kind,proto3,enum=saas.accounts.v1.CatalogueEntryKind" json:"kind,omitempty"` - // A module's principal prefix, or a solution's registry id — the identifier - // its installations name. - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - // The owner of record: a solution registration's publisher. - // - // Types that are valid to be assigned to PublisherValue: - // - // *CatalogueEntry_Publisher - // *CatalogueEntry_PublisherGap - PublisherValue isCatalogueEntry_PublisherValue `protobuf_oneof:"publisher_value"` - // Types that are valid to be assigned to DeclaredReleaseValue: - // - // *CatalogueEntry_DeclaredRelease - // *CatalogueEntry_DeclaredReleaseGap - DeclaredReleaseValue isCatalogueEntry_DeclaredReleaseValue `protobuf_oneof:"declared_release_value"` - // The OCI image manifest digest the entry was published at. - // - // Types that are valid to be assigned to BuildDigestValue: - // - // *CatalogueEntry_BuildDigest - // *CatalogueEntry_BuildDigestGap - BuildDigestValue isCatalogueEntry_BuildDigestValue `protobuf_oneof:"build_digest_value"` - // The presence generation this host applied. - // - // Types that are valid to be assigned to GenerationValue: - // - // *CatalogueEntry_Generation - // *CatalogueEntry_GenerationGap - GenerationValue isCatalogueEntry_GenerationValue `protobuf_oneof:"generation_value"` - // Types that are valid to be assigned to RunningValue: - // - // *CatalogueEntry_Running - // *CatalogueEntry_RunningGap - RunningValue isCatalogueEntry_RunningValue `protobuf_oneof:"running_value"` - // Lines of code, backend and frontend, vendored paths excluded. The value - // case is the presence document's build_size section (codefly-dev/core#708) - // as delivered; this host counts nothing itself. - // - // Types that are valid to be assigned to BuildSizeValue: + // Types that are valid to be assigned to AppliedRevisionValue: // - // *CatalogueEntry_BuildSizeGap - BuildSizeValue isCatalogueEntry_BuildSizeValue `protobuf_oneof:"build_size_value"` - // The solution's registration, with deployment topology withheld: the - // frontend manifest, backend upstream and service alias are always empty - // here, as on every browser-facing projection of the registry. Absent for a - // module, which does not self-register, and for a solution known only from - // its installations. - Registration *SolutionRegistration `protobuf:"bytes,14,opt,name=registration,proto3,oneof" json:"registration,omitempty"` - Installations []*CatalogueInstallation `protobuf:"bytes,15,rep,name=installations,proto3" json:"installations,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // *CatalogueApplied_AppliedRevision + // *CatalogueApplied_AppliedRevisionGap + AppliedRevisionValue isCatalogueApplied_AppliedRevisionValue `protobuf_oneof:"applied_revision_value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *CatalogueEntry) Reset() { - *x = CatalogueEntry{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] +func (x *CatalogueApplied) Reset() { + *x = CatalogueApplied{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *CatalogueEntry) String() string { +func (x *CatalogueApplied) String() string { return protoimpl.X.MessageStringOf(x) } -func (*CatalogueEntry) ProtoMessage() {} +func (*CatalogueApplied) ProtoMessage() {} -func (x *CatalogueEntry) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] +func (x *CatalogueApplied) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2425,266 +2509,798 @@ func (x *CatalogueEntry) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use CatalogueEntry.ProtoReflect.Descriptor instead. -func (*CatalogueEntry) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{37} -} - -func (x *CatalogueEntry) GetKind() CatalogueEntryKind { - if x != nil { - return x.Kind - } - return CatalogueEntryKind_CATALOGUE_ENTRY_KIND_UNSPECIFIED -} - -func (x *CatalogueEntry) GetName() string { - if x != nil { - return x.Name - } - return "" +// Deprecated: Use CatalogueApplied.ProtoReflect.Descriptor instead. +func (*CatalogueApplied) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{39} } -func (x *CatalogueEntry) GetPublisherValue() isCatalogueEntry_PublisherValue { +func (x *CatalogueApplied) GetAppliedRevisionValue() isCatalogueApplied_AppliedRevisionValue { if x != nil { - return x.PublisherValue + return x.AppliedRevisionValue } return nil } -func (x *CatalogueEntry) GetPublisher() string { +func (x *CatalogueApplied) GetAppliedRevision() int64 { if x != nil { - if x, ok := x.PublisherValue.(*CatalogueEntry_Publisher); ok { - return x.Publisher + if x, ok := x.AppliedRevisionValue.(*CatalogueApplied_AppliedRevision); ok { + return x.AppliedRevision } } - return "" + return 0 } -func (x *CatalogueEntry) GetPublisherGap() *CatalogueGap { +func (x *CatalogueApplied) GetAppliedRevisionGap() *CatalogueGap { if x != nil { - if x, ok := x.PublisherValue.(*CatalogueEntry_PublisherGap); ok { - return x.PublisherGap + if x, ok := x.AppliedRevisionValue.(*CatalogueApplied_AppliedRevisionGap); ok { + return x.AppliedRevisionGap } } return nil } -func (x *CatalogueEntry) GetDeclaredReleaseValue() isCatalogueEntry_DeclaredReleaseValue { - if x != nil { - return x.DeclaredReleaseValue - } - return nil +type isCatalogueApplied_AppliedRevisionValue interface { + isCatalogueApplied_AppliedRevisionValue() } -func (x *CatalogueEntry) GetDeclaredRelease() *CatalogueRelease { - if x != nil { - if x, ok := x.DeclaredReleaseValue.(*CatalogueEntry_DeclaredRelease); ok { - return x.DeclaredRelease - } - } - return nil +type CatalogueApplied_AppliedRevision struct { + AppliedRevision int64 `protobuf:"varint,1,opt,name=applied_revision,json=appliedRevision,proto3,oneof"` } -func (x *CatalogueEntry) GetDeclaredReleaseGap() *CatalogueGap { - if x != nil { - if x, ok := x.DeclaredReleaseValue.(*CatalogueEntry_DeclaredReleaseGap); ok { - return x.DeclaredReleaseGap - } - } - return nil +type CatalogueApplied_AppliedRevisionGap struct { + AppliedRevisionGap *CatalogueGap `protobuf:"bytes,2,opt,name=applied_revision_gap,json=appliedRevisionGap,proto3,oneof"` } -func (x *CatalogueEntry) GetBuildDigestValue() isCatalogueEntry_BuildDigestValue { +func (*CatalogueApplied_AppliedRevision) isCatalogueApplied_AppliedRevisionValue() {} + +func (*CatalogueApplied_AppliedRevisionGap) isCatalogueApplied_AppliedRevisionValue() {} + +// CatalogueObserved is what is actually running. +type CatalogueObserved struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to ObservedRevisionValue: + // + // *CatalogueObserved_ObservedRevision + // *CatalogueObserved_ObservedRevisionGap + ObservedRevisionValue isCatalogueObserved_ObservedRevisionValue `protobuf_oneof:"observed_revision_value"` + // When the observation was made; a stale observation is never current. + // + // Types that are valid to be assigned to ObservationFreshnessValue: + // + // *CatalogueObserved_ObservedAt + // *CatalogueObserved_ObservationFreshnessGap + ObservationFreshnessValue isCatalogueObserved_ObservationFreshnessValue `protobuf_oneof:"observation_freshness_value"` + // Types that are valid to be assigned to ObservedExecutionValue: + // + // *CatalogueObserved_ObservedExecution + // *CatalogueObserved_ObservedExecutionGap + ObservedExecutionValue isCatalogueObserved_ObservedExecutionValue `protobuf_oneof:"observed_execution_value"` + // A verdict exists only when both an observation and a known authorization + // do; anything less is a gap, and an unobserved entry is never a match. + // + // Types that are valid to be assigned to VerdictValue: + // + // *CatalogueObserved_Verdict + // *CatalogueObserved_VerdictGap + VerdictValue isCatalogueObserved_VerdictValue `protobuf_oneof:"verdict_value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueObserved) Reset() { + *x = CatalogueObserved{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueObserved) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueObserved) ProtoMessage() {} + +func (x *CatalogueObserved) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueObserved.ProtoReflect.Descriptor instead. +func (*CatalogueObserved) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{40} +} + +func (x *CatalogueObserved) GetObservedRevisionValue() isCatalogueObserved_ObservedRevisionValue { if x != nil { - return x.BuildDigestValue + return x.ObservedRevisionValue } return nil } -func (x *CatalogueEntry) GetBuildDigest() string { +func (x *CatalogueObserved) GetObservedRevision() int64 { if x != nil { - if x, ok := x.BuildDigestValue.(*CatalogueEntry_BuildDigest); ok { - return x.BuildDigest + if x, ok := x.ObservedRevisionValue.(*CatalogueObserved_ObservedRevision); ok { + return x.ObservedRevision } } - return "" + return 0 } -func (x *CatalogueEntry) GetBuildDigestGap() *CatalogueGap { +func (x *CatalogueObserved) GetObservedRevisionGap() *CatalogueGap { if x != nil { - if x, ok := x.BuildDigestValue.(*CatalogueEntry_BuildDigestGap); ok { - return x.BuildDigestGap + if x, ok := x.ObservedRevisionValue.(*CatalogueObserved_ObservedRevisionGap); ok { + return x.ObservedRevisionGap } } return nil } -func (x *CatalogueEntry) GetGenerationValue() isCatalogueEntry_GenerationValue { +func (x *CatalogueObserved) GetObservationFreshnessValue() isCatalogueObserved_ObservationFreshnessValue { if x != nil { - return x.GenerationValue + return x.ObservationFreshnessValue } return nil } -func (x *CatalogueEntry) GetGeneration() int64 { +func (x *CatalogueObserved) GetObservedAt() *timestamppb.Timestamp { if x != nil { - if x, ok := x.GenerationValue.(*CatalogueEntry_Generation); ok { - return x.Generation + if x, ok := x.ObservationFreshnessValue.(*CatalogueObserved_ObservedAt); ok { + return x.ObservedAt } } - return 0 + return nil } -func (x *CatalogueEntry) GetGenerationGap() *CatalogueGap { +func (x *CatalogueObserved) GetObservationFreshnessGap() *CatalogueGap { if x != nil { - if x, ok := x.GenerationValue.(*CatalogueEntry_GenerationGap); ok { - return x.GenerationGap + if x, ok := x.ObservationFreshnessValue.(*CatalogueObserved_ObservationFreshnessGap); ok { + return x.ObservationFreshnessGap } } return nil } -func (x *CatalogueEntry) GetRunningValue() isCatalogueEntry_RunningValue { +func (x *CatalogueObserved) GetObservedExecutionValue() isCatalogueObserved_ObservedExecutionValue { if x != nil { - return x.RunningValue + return x.ObservedExecutionValue } return nil } -func (x *CatalogueEntry) GetRunning() *CatalogueRunning { +func (x *CatalogueObserved) GetObservedExecution() *CatalogueExecution { if x != nil { - if x, ok := x.RunningValue.(*CatalogueEntry_Running); ok { - return x.Running + if x, ok := x.ObservedExecutionValue.(*CatalogueObserved_ObservedExecution); ok { + return x.ObservedExecution } } return nil } -func (x *CatalogueEntry) GetRunningGap() *CatalogueGap { +func (x *CatalogueObserved) GetObservedExecutionGap() *CatalogueGap { if x != nil { - if x, ok := x.RunningValue.(*CatalogueEntry_RunningGap); ok { - return x.RunningGap + if x, ok := x.ObservedExecutionValue.(*CatalogueObserved_ObservedExecutionGap); ok { + return x.ObservedExecutionGap } } return nil } -func (x *CatalogueEntry) GetBuildSizeValue() isCatalogueEntry_BuildSizeValue { +func (x *CatalogueObserved) GetVerdictValue() isCatalogueObserved_VerdictValue { if x != nil { - return x.BuildSizeValue + return x.VerdictValue } return nil } -func (x *CatalogueEntry) GetBuildSizeGap() *CatalogueGap { +func (x *CatalogueObserved) GetVerdict() CatalogueObservedVerdict { if x != nil { - if x, ok := x.BuildSizeValue.(*CatalogueEntry_BuildSizeGap); ok { - return x.BuildSizeGap + if x, ok := x.VerdictValue.(*CatalogueObserved_Verdict); ok { + return x.Verdict + } + } + return CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED +} + +func (x *CatalogueObserved) GetVerdictGap() *CatalogueGap { + if x != nil { + if x, ok := x.VerdictValue.(*CatalogueObserved_VerdictGap); ok { + return x.VerdictGap } } return nil } -func (x *CatalogueEntry) GetRegistration() *SolutionRegistration { +type isCatalogueObserved_ObservedRevisionValue interface { + isCatalogueObserved_ObservedRevisionValue() +} + +type CatalogueObserved_ObservedRevision struct { + ObservedRevision int64 `protobuf:"varint,1,opt,name=observed_revision,json=observedRevision,proto3,oneof"` +} + +type CatalogueObserved_ObservedRevisionGap struct { + ObservedRevisionGap *CatalogueGap `protobuf:"bytes,2,opt,name=observed_revision_gap,json=observedRevisionGap,proto3,oneof"` +} + +func (*CatalogueObserved_ObservedRevision) isCatalogueObserved_ObservedRevisionValue() {} + +func (*CatalogueObserved_ObservedRevisionGap) isCatalogueObserved_ObservedRevisionValue() {} + +type isCatalogueObserved_ObservationFreshnessValue interface { + isCatalogueObserved_ObservationFreshnessValue() +} + +type CatalogueObserved_ObservedAt struct { + ObservedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=observed_at,json=observedAt,proto3,oneof"` +} + +type CatalogueObserved_ObservationFreshnessGap struct { + ObservationFreshnessGap *CatalogueGap `protobuf:"bytes,4,opt,name=observation_freshness_gap,json=observationFreshnessGap,proto3,oneof"` +} + +func (*CatalogueObserved_ObservedAt) isCatalogueObserved_ObservationFreshnessValue() {} + +func (*CatalogueObserved_ObservationFreshnessGap) isCatalogueObserved_ObservationFreshnessValue() {} + +type isCatalogueObserved_ObservedExecutionValue interface { + isCatalogueObserved_ObservedExecutionValue() +} + +type CatalogueObserved_ObservedExecution struct { + ObservedExecution *CatalogueExecution `protobuf:"bytes,5,opt,name=observed_execution,json=observedExecution,proto3,oneof"` +} + +type CatalogueObserved_ObservedExecutionGap struct { + ObservedExecutionGap *CatalogueGap `protobuf:"bytes,6,opt,name=observed_execution_gap,json=observedExecutionGap,proto3,oneof"` +} + +func (*CatalogueObserved_ObservedExecution) isCatalogueObserved_ObservedExecutionValue() {} + +func (*CatalogueObserved_ObservedExecutionGap) isCatalogueObserved_ObservedExecutionValue() {} + +type isCatalogueObserved_VerdictValue interface { + isCatalogueObserved_VerdictValue() +} + +type CatalogueObserved_Verdict struct { + Verdict CatalogueObservedVerdict `protobuf:"varint,7,opt,name=verdict,proto3,enum=saas.accounts.v1.CatalogueObservedVerdict,oneof"` +} + +type CatalogueObserved_VerdictGap struct { + VerdictGap *CatalogueGap `protobuf:"bytes,8,opt,name=verdict_gap,json=verdictGap,proto3,oneof"` +} + +func (*CatalogueObserved_Verdict) isCatalogueObserved_VerdictValue() {} + +func (*CatalogueObserved_VerdictGap) isCatalogueObserved_VerdictValue() {} + +// CatalogueWithdrawing is approval withdrawn and retirement not yet complete, +// acknowledged separately from retirement itself. The value cases are the +// deployment contract's withdrawal and revocation records; until they exist +// each field holds only its gap. +type CatalogueWithdrawing struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to WithdrawalStateValue: + // + // *CatalogueWithdrawing_WithdrawalStateGap + WithdrawalStateValue isCatalogueWithdrawing_WithdrawalStateValue `protobuf_oneof:"withdrawal_state_value"` + // Types that are valid to be assigned to CredentialRevocationStateValue: + // + // *CatalogueWithdrawing_CredentialRevocationStateGap + CredentialRevocationStateValue isCatalogueWithdrawing_CredentialRevocationStateValue `protobuf_oneof:"credential_revocation_state_value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueWithdrawing) Reset() { + *x = CatalogueWithdrawing{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueWithdrawing) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueWithdrawing) ProtoMessage() {} + +func (x *CatalogueWithdrawing) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[41] if x != nil { - return x.Registration + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueWithdrawing.ProtoReflect.Descriptor instead. +func (*CatalogueWithdrawing) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{41} +} + +func (x *CatalogueWithdrawing) GetWithdrawalStateValue() isCatalogueWithdrawing_WithdrawalStateValue { + if x != nil { + return x.WithdrawalStateValue } return nil } -func (x *CatalogueEntry) GetInstallations() []*CatalogueInstallation { +func (x *CatalogueWithdrawing) GetWithdrawalStateGap() *CatalogueGap { if x != nil { - return x.Installations + if x, ok := x.WithdrawalStateValue.(*CatalogueWithdrawing_WithdrawalStateGap); ok { + return x.WithdrawalStateGap + } } return nil } -type isCatalogueEntry_PublisherValue interface { - isCatalogueEntry_PublisherValue() +func (x *CatalogueWithdrawing) GetCredentialRevocationStateValue() isCatalogueWithdrawing_CredentialRevocationStateValue { + if x != nil { + return x.CredentialRevocationStateValue + } + return nil } -type CatalogueEntry_Publisher struct { - Publisher string `protobuf:"bytes,3,opt,name=publisher,proto3,oneof"` +func (x *CatalogueWithdrawing) GetCredentialRevocationStateGap() *CatalogueGap { + if x != nil { + if x, ok := x.CredentialRevocationStateValue.(*CatalogueWithdrawing_CredentialRevocationStateGap); ok { + return x.CredentialRevocationStateGap + } + } + return nil } -type CatalogueEntry_PublisherGap struct { - PublisherGap *CatalogueGap `protobuf:"bytes,4,opt,name=publisher_gap,json=publisherGap,proto3,oneof"` +type isCatalogueWithdrawing_WithdrawalStateValue interface { + isCatalogueWithdrawing_WithdrawalStateValue() } -func (*CatalogueEntry_Publisher) isCatalogueEntry_PublisherValue() {} +type CatalogueWithdrawing_WithdrawalStateGap struct { + WithdrawalStateGap *CatalogueGap `protobuf:"bytes,1,opt,name=withdrawal_state_gap,json=withdrawalStateGap,proto3,oneof"` +} -func (*CatalogueEntry_PublisherGap) isCatalogueEntry_PublisherValue() {} +func (*CatalogueWithdrawing_WithdrawalStateGap) isCatalogueWithdrawing_WithdrawalStateValue() {} -type isCatalogueEntry_DeclaredReleaseValue interface { - isCatalogueEntry_DeclaredReleaseValue() +type isCatalogueWithdrawing_CredentialRevocationStateValue interface { + isCatalogueWithdrawing_CredentialRevocationStateValue() } -type CatalogueEntry_DeclaredRelease struct { - DeclaredRelease *CatalogueRelease `protobuf:"bytes,5,opt,name=declared_release,json=declaredRelease,proto3,oneof"` +type CatalogueWithdrawing_CredentialRevocationStateGap struct { + CredentialRevocationStateGap *CatalogueGap `protobuf:"bytes,2,opt,name=credential_revocation_state_gap,json=credentialRevocationStateGap,proto3,oneof"` } -type CatalogueEntry_DeclaredReleaseGap struct { - DeclaredReleaseGap *CatalogueGap `protobuf:"bytes,6,opt,name=declared_release_gap,json=declaredReleaseGap,proto3,oneof"` +func (*CatalogueWithdrawing_CredentialRevocationStateGap) isCatalogueWithdrawing_CredentialRevocationStateValue() { } -func (*CatalogueEntry_DeclaredRelease) isCatalogueEntry_DeclaredReleaseValue() {} +// CatalogueRetired is whether the workload is physically stopped. Retired +// requires stop or fence evidence; until a retirement controller reports to +// this host, the field holds only its gap and nothing reads as retired. +type CatalogueRetired struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to RetirementStateValue: + // + // *CatalogueRetired_RetirementStateGap + RetirementStateValue isCatalogueRetired_RetirementStateValue `protobuf_oneof:"retirement_state_value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} -func (*CatalogueEntry_DeclaredReleaseGap) isCatalogueEntry_DeclaredReleaseValue() {} +func (x *CatalogueRetired) Reset() { + *x = CatalogueRetired{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} -type isCatalogueEntry_BuildDigestValue interface { - isCatalogueEntry_BuildDigestValue() +func (x *CatalogueRetired) String() string { + return protoimpl.X.MessageStringOf(x) } -type CatalogueEntry_BuildDigest struct { - BuildDigest string `protobuf:"bytes,7,opt,name=build_digest,json=buildDigest,proto3,oneof"` +func (*CatalogueRetired) ProtoMessage() {} + +func (x *CatalogueRetired) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) } -type CatalogueEntry_BuildDigestGap struct { - BuildDigestGap *CatalogueGap `protobuf:"bytes,8,opt,name=build_digest_gap,json=buildDigestGap,proto3,oneof"` +// Deprecated: Use CatalogueRetired.ProtoReflect.Descriptor instead. +func (*CatalogueRetired) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{42} } -func (*CatalogueEntry_BuildDigest) isCatalogueEntry_BuildDigestValue() {} +func (x *CatalogueRetired) GetRetirementStateValue() isCatalogueRetired_RetirementStateValue { + if x != nil { + return x.RetirementStateValue + } + return nil +} + +func (x *CatalogueRetired) GetRetirementStateGap() *CatalogueGap { + if x != nil { + if x, ok := x.RetirementStateValue.(*CatalogueRetired_RetirementStateGap); ok { + return x.RetirementStateGap + } + } + return nil +} -func (*CatalogueEntry_BuildDigestGap) isCatalogueEntry_BuildDigestValue() {} +type isCatalogueRetired_RetirementStateValue interface { + isCatalogueRetired_RetirementStateValue() +} + +type CatalogueRetired_RetirementStateGap struct { + RetirementStateGap *CatalogueGap `protobuf:"bytes,1,opt,name=retirement_state_gap,json=retirementStateGap,proto3,oneof"` +} + +func (*CatalogueRetired_RetirementStateGap) isCatalogueRetired_RetirementStateValue() {} -type isCatalogueEntry_GenerationValue interface { - isCatalogueEntry_GenerationValue() +// CatalogueInstallation is one organization's installation of a solution. +type CatalogueInstallation struct { + state protoimpl.MessageState `protogen:"open.v1"` + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + OrgName string `protobuf:"bytes,2,opt,name=org_name,json=orgName,proto3" json:"org_name,omitempty"` + // The release the installation's agent principal was created for, read from + // its canonical publisher/name:version identifier. + // + // Types that are valid to be assigned to AgentReleaseValue: + // + // *CatalogueInstallation_AgentRelease + // *CatalogueInstallation_AgentReleaseGap + AgentReleaseValue isCatalogueInstallation_AgentReleaseValue `protobuf_oneof:"agent_release_value"` + // The installation's own revision, independent of the deployment's. + // + // Types that are valid to be assigned to RevisionValue: + // + // *CatalogueInstallation_RevisionGap + RevisionValue isCatalogueInstallation_RevisionValue `protobuf_oneof:"revision_value"` + // Active team grants at the installation's authority root or above it, so + // every team whose members the grant reaches inside this installation. + ExposedTeams []*CollectionReadGrant `protobuf:"bytes,6,rep,name=exposed_teams,json=exposedTeams,proto3" json:"exposed_teams,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -type CatalogueEntry_Generation struct { - Generation int64 `protobuf:"varint,9,opt,name=generation,proto3,oneof"` +func (x *CatalogueInstallation) Reset() { + *x = CatalogueInstallation{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } -type CatalogueEntry_GenerationGap struct { - GenerationGap *CatalogueGap `protobuf:"bytes,10,opt,name=generation_gap,json=generationGap,proto3,oneof"` +func (x *CatalogueInstallation) String() string { + return protoimpl.X.MessageStringOf(x) } -func (*CatalogueEntry_Generation) isCatalogueEntry_GenerationValue() {} +func (*CatalogueInstallation) ProtoMessage() {} -func (*CatalogueEntry_GenerationGap) isCatalogueEntry_GenerationValue() {} +func (x *CatalogueInstallation) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} -type isCatalogueEntry_RunningValue interface { - isCatalogueEntry_RunningValue() +// Deprecated: Use CatalogueInstallation.ProtoReflect.Descriptor instead. +func (*CatalogueInstallation) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{43} } -type CatalogueEntry_Running struct { - Running *CatalogueRunning `protobuf:"bytes,11,opt,name=running,proto3,oneof"` +func (x *CatalogueInstallation) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil } -type CatalogueEntry_RunningGap struct { - RunningGap *CatalogueGap `protobuf:"bytes,12,opt,name=running_gap,json=runningGap,proto3,oneof"` +func (x *CatalogueInstallation) GetOrgName() string { + if x != nil { + return x.OrgName + } + return "" } -func (*CatalogueEntry_Running) isCatalogueEntry_RunningValue() {} +func (x *CatalogueInstallation) GetAgentReleaseValue() isCatalogueInstallation_AgentReleaseValue { + if x != nil { + return x.AgentReleaseValue + } + return nil +} -func (*CatalogueEntry_RunningGap) isCatalogueEntry_RunningValue() {} +func (x *CatalogueInstallation) GetAgentRelease() *CatalogueRelease { + if x != nil { + if x, ok := x.AgentReleaseValue.(*CatalogueInstallation_AgentRelease); ok { + return x.AgentRelease + } + } + return nil +} + +func (x *CatalogueInstallation) GetAgentReleaseGap() *CatalogueGap { + if x != nil { + if x, ok := x.AgentReleaseValue.(*CatalogueInstallation_AgentReleaseGap); ok { + return x.AgentReleaseGap + } + } + return nil +} + +func (x *CatalogueInstallation) GetRevisionValue() isCatalogueInstallation_RevisionValue { + if x != nil { + return x.RevisionValue + } + return nil +} + +func (x *CatalogueInstallation) GetRevisionGap() *CatalogueGap { + if x != nil { + if x, ok := x.RevisionValue.(*CatalogueInstallation_RevisionGap); ok { + return x.RevisionGap + } + } + return nil +} + +func (x *CatalogueInstallation) GetExposedTeams() []*CollectionReadGrant { + if x != nil { + return x.ExposedTeams + } + return nil +} + +type isCatalogueInstallation_AgentReleaseValue interface { + isCatalogueInstallation_AgentReleaseValue() +} + +type CatalogueInstallation_AgentRelease struct { + AgentRelease *CatalogueRelease `protobuf:"bytes,3,opt,name=agent_release,json=agentRelease,proto3,oneof"` +} + +type CatalogueInstallation_AgentReleaseGap struct { + AgentReleaseGap *CatalogueGap `protobuf:"bytes,4,opt,name=agent_release_gap,json=agentReleaseGap,proto3,oneof"` +} + +func (*CatalogueInstallation_AgentRelease) isCatalogueInstallation_AgentReleaseValue() {} + +func (*CatalogueInstallation_AgentReleaseGap) isCatalogueInstallation_AgentReleaseValue() {} + +type isCatalogueInstallation_RevisionValue interface { + isCatalogueInstallation_RevisionValue() +} + +type CatalogueInstallation_RevisionGap struct { + RevisionGap *CatalogueGap `protobuf:"bytes,5,opt,name=revision_gap,json=revisionGap,proto3,oneof"` +} + +func (*CatalogueInstallation_RevisionGap) isCatalogueInstallation_RevisionValue() {} + +// CatalogueEntry is one module or solution, placed in the registry's state +// model: desired, authorized, applied, observed, withdrawing and physically +// retired are distinct, and a row shows each separately, so an entry desired +// but not authorized, or observed but no longer authorized, reads as exactly +// that. +type CatalogueEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind CatalogueEntryKind `protobuf:"varint,1,opt,name=kind,proto3,enum=saas.accounts.v1.CatalogueEntryKind" json:"kind,omitempty"` + // A module's principal prefix, or a solution's registry id — the identifier + // its installations name. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // The owner of record: a solution registration's publisher. + // + // Types that are valid to be assigned to PublisherValue: + // + // *CatalogueEntry_Publisher + // *CatalogueEntry_PublisherGap + PublisherValue isCatalogueEntry_PublisherValue `protobuf_oneof:"publisher_value"` + Desired *CatalogueDesired `protobuf:"bytes,5,opt,name=desired,proto3" json:"desired,omitempty"` + Authorized *CatalogueAuthorized `protobuf:"bytes,6,opt,name=authorized,proto3" json:"authorized,omitempty"` + Applied *CatalogueApplied `protobuf:"bytes,7,opt,name=applied,proto3" json:"applied,omitempty"` + Observed *CatalogueObserved `protobuf:"bytes,8,opt,name=observed,proto3" json:"observed,omitempty"` + Withdrawing *CatalogueWithdrawing `protobuf:"bytes,9,opt,name=withdrawing,proto3" json:"withdrawing,omitempty"` + Retired *CatalogueRetired `protobuf:"bytes,10,opt,name=retired,proto3" json:"retired,omitempty"` + // Lines of code, backend and frontend, vendored paths excluded. The value + // case is the presence document's build_size section (codefly-dev/core#708) + // as delivered; this host counts nothing itself. + // + // Types that are valid to be assigned to BuildSizeValue: + // + // *CatalogueEntry_BuildSizeGap + BuildSizeValue isCatalogueEntry_BuildSizeValue `protobuf_oneof:"build_size_value"` + // The solution's self-registration, with deployment topology withheld: the + // frontend manifest, backend upstream and service alias are always empty + // here, as on every browser-facing projection of the registry. Absent for a + // module, which does not self-register, and for a solution known only from + // its installations. + Registration *SolutionRegistration `protobuf:"bytes,12,opt,name=registration,proto3,oneof" json:"registration,omitempty"` + Installations []*CatalogueInstallation `protobuf:"bytes,13,rep,name=installations,proto3" json:"installations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueEntry) Reset() { + *x = CatalogueEntry{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueEntry) ProtoMessage() {} + +func (x *CatalogueEntry) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueEntry.ProtoReflect.Descriptor instead. +func (*CatalogueEntry) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{44} +} + +func (x *CatalogueEntry) GetKind() CatalogueEntryKind { + if x != nil { + return x.Kind + } + return CatalogueEntryKind_CATALOGUE_ENTRY_KIND_UNSPECIFIED +} + +func (x *CatalogueEntry) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CatalogueEntry) GetPublisherValue() isCatalogueEntry_PublisherValue { + if x != nil { + return x.PublisherValue + } + return nil +} + +func (x *CatalogueEntry) GetPublisher() string { + if x != nil { + if x, ok := x.PublisherValue.(*CatalogueEntry_Publisher); ok { + return x.Publisher + } + } + return "" +} + +func (x *CatalogueEntry) GetPublisherGap() *CatalogueGap { + if x != nil { + if x, ok := x.PublisherValue.(*CatalogueEntry_PublisherGap); ok { + return x.PublisherGap + } + } + return nil +} + +func (x *CatalogueEntry) GetDesired() *CatalogueDesired { + if x != nil { + return x.Desired + } + return nil +} + +func (x *CatalogueEntry) GetAuthorized() *CatalogueAuthorized { + if x != nil { + return x.Authorized + } + return nil +} + +func (x *CatalogueEntry) GetApplied() *CatalogueApplied { + if x != nil { + return x.Applied + } + return nil +} + +func (x *CatalogueEntry) GetObserved() *CatalogueObserved { + if x != nil { + return x.Observed + } + return nil +} + +func (x *CatalogueEntry) GetWithdrawing() *CatalogueWithdrawing { + if x != nil { + return x.Withdrawing + } + return nil +} + +func (x *CatalogueEntry) GetRetired() *CatalogueRetired { + if x != nil { + return x.Retired + } + return nil +} + +func (x *CatalogueEntry) GetBuildSizeValue() isCatalogueEntry_BuildSizeValue { + if x != nil { + return x.BuildSizeValue + } + return nil +} + +func (x *CatalogueEntry) GetBuildSizeGap() *CatalogueGap { + if x != nil { + if x, ok := x.BuildSizeValue.(*CatalogueEntry_BuildSizeGap); ok { + return x.BuildSizeGap + } + } + return nil +} + +func (x *CatalogueEntry) GetRegistration() *SolutionRegistration { + if x != nil { + return x.Registration + } + return nil +} + +func (x *CatalogueEntry) GetInstallations() []*CatalogueInstallation { + if x != nil { + return x.Installations + } + return nil +} + +type isCatalogueEntry_PublisherValue interface { + isCatalogueEntry_PublisherValue() +} + +type CatalogueEntry_Publisher struct { + Publisher string `protobuf:"bytes,3,opt,name=publisher,proto3,oneof"` +} + +type CatalogueEntry_PublisherGap struct { + PublisherGap *CatalogueGap `protobuf:"bytes,4,opt,name=publisher_gap,json=publisherGap,proto3,oneof"` +} + +func (*CatalogueEntry_Publisher) isCatalogueEntry_PublisherValue() {} + +func (*CatalogueEntry_PublisherGap) isCatalogueEntry_PublisherValue() {} type isCatalogueEntry_BuildSizeValue interface { isCatalogueEntry_BuildSizeValue() } type CatalogueEntry_BuildSizeGap struct { - BuildSizeGap *CatalogueGap `protobuf:"bytes,13,opt,name=build_size_gap,json=buildSizeGap,proto3,oneof"` + BuildSizeGap *CatalogueGap `protobuf:"bytes,11,opt,name=build_size_gap,json=buildSizeGap,proto3,oneof"` } func (*CatalogueEntry_BuildSizeGap) isCatalogueEntry_BuildSizeValue() {} @@ -2699,7 +3315,7 @@ type ListPlatformCatalogueRequest struct { func (x *ListPlatformCatalogueRequest) Reset() { *x = ListPlatformCatalogueRequest{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2711,7 +3327,7 @@ func (x *ListPlatformCatalogueRequest) String() string { func (*ListPlatformCatalogueRequest) ProtoMessage() {} func (x *ListPlatformCatalogueRequest) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2724,7 +3340,7 @@ func (x *ListPlatformCatalogueRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPlatformCatalogueRequest.ProtoReflect.Descriptor instead. func (*ListPlatformCatalogueRequest) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{38} + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{45} } func (x *ListPlatformCatalogueRequest) GetIncludeTombstoned() bool { @@ -2746,7 +3362,7 @@ type ListPlatformCatalogueResponse struct { func (x *ListPlatformCatalogueResponse) Reset() { *x = ListPlatformCatalogueResponse{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2758,7 +3374,7 @@ func (x *ListPlatformCatalogueResponse) String() string { func (*ListPlatformCatalogueResponse) ProtoMessage() {} func (x *ListPlatformCatalogueResponse) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2771,7 +3387,7 @@ func (x *ListPlatformCatalogueResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListPlatformCatalogueResponse.ProtoReflect.Descriptor instead. func (*ListPlatformCatalogueResponse) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{39} + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{46} } func (x *ListPlatformCatalogueResponse) GetEntries() []*CatalogueEntry { @@ -2926,11 +3542,51 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\aversion\x18\x03 \x01(\tR\aversion\"d\n" + "\x12CatalogueExecution\x12!\n" + "\fimage_digest\x18\x01 \x01(\tR\vimageDigest\x12+\n" + - "\x11build_incarnation\x18\x02 \x01(\x04R\x10buildIncarnation\"\xdb\x01\n" + - "\x10CatalogueRunning\x12@\n" + - "\bdeclared\x18\x01 \x01(\v2$.saas.accounts.v1.CatalogueExecutionR\bdeclared\x12@\n" + - "\bobserved\x18\x02 \x01(\v2$.saas.accounts.v1.CatalogueExecutionR\bobserved\x12C\n" + - "\averdict\x18\x03 \x01(\x0e2).saas.accounts.v1.CatalogueRunningVerdictR\averdict\"\xc9\x03\n" + + "\x11build_incarnation\x18\x02 \x01(\x04R\x10buildIncarnation\"\xf1\x02\n" + + "\x10CatalogueDesired\x12-\n" + + "\x11declared_revision\x18\x01 \x01(\x03H\x00R\x10declaredRevision\x12T\n" + + "\x15declared_revision_gap\x18\x02 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x13declaredRevisionGap\x12O\n" + + "\x10declared_release\x18\x03 \x01(\v2\".saas.accounts.v1.CatalogueReleaseH\x01R\x0fdeclaredRelease\x12R\n" + + "\x14declared_release_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x12declaredReleaseGapB\x19\n" + + "\x17declared_revision_valueB\x18\n" + + "\x16declared_release_value\"\x9e\x01\n" + + "\x16CatalogueAuthorization\x12/\n" + + "\x13authorized_revision\x18\x01 \x01(\x03R\x12authorizedRevision\x12S\n" + + "\x12approved_execution\x18\x02 \x01(\v2$.saas.accounts.v1.CatalogueExecutionR\x11approvedExecution\"0\n" + + "\x16CatalogueNotAuthorized\x12\x16\n" + + "\x06detail\x18\x01 \x01(\tR\x06detail\"\xa0\x02\n" + + "\x13CatalogueAuthorized\x12P\n" + + "\rauthorization\x18\x01 \x01(\v2(.saas.accounts.v1.CatalogueAuthorizationH\x00R\rauthorization\x12Q\n" + + "\x0enot_authorized\x18\x02 \x01(\v2(.saas.accounts.v1.CatalogueNotAuthorizedH\x00R\rnotAuthorized\x12M\n" + + "\x11authorization_gap\x18\x03 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x10authorizationGapB\x15\n" + + "\x13authorization_value\"\xad\x01\n" + + "\x10CatalogueApplied\x12+\n" + + "\x10applied_revision\x18\x01 \x01(\x03H\x00R\x0fappliedRevision\x12R\n" + + "\x14applied_revision_gap\x18\x02 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x12appliedRevisionGapB\x18\n" + + "\x16applied_revision_value\"\xd6\x05\n" + + "\x11CatalogueObserved\x12-\n" + + "\x11observed_revision\x18\x01 \x01(\x03H\x00R\x10observedRevision\x12T\n" + + "\x15observed_revision_gap\x18\x02 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x13observedRevisionGap\x12=\n" + + "\vobserved_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\n" + + "observedAt\x12\\\n" + + "\x19observation_freshness_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x17observationFreshnessGap\x12U\n" + + "\x12observed_execution\x18\x05 \x01(\v2$.saas.accounts.v1.CatalogueExecutionH\x02R\x11observedExecution\x12V\n" + + "\x16observed_execution_gap\x18\x06 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x02R\x14observedExecutionGap\x12F\n" + + "\averdict\x18\a \x01(\x0e2*.saas.accounts.v1.CatalogueObservedVerdictH\x03R\averdict\x12A\n" + + "\vverdict_gap\x18\b \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x03R\n" + + "verdictGapB\x19\n" + + "\x17observed_revision_valueB\x1d\n" + + "\x1bobservation_freshness_valueB\x1a\n" + + "\x18observed_execution_valueB\x0f\n" + + "\rverdict_value\"\x92\x02\n" + + "\x14CatalogueWithdrawing\x12R\n" + + "\x14withdrawal_state_gap\x18\x01 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x12withdrawalStateGap\x12g\n" + + "\x1fcredential_revocation_state_gap\x18\x02 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x1ccredentialRevocationStateGapB\x18\n" + + "\x16withdrawal_state_valueB#\n" + + "!credential_revocation_state_value\"\x80\x01\n" + + "\x10CatalogueRetired\x12R\n" + + "\x14retirement_state_gap\x18\x01 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x12retirementStateGapB\x18\n" + + "\x16retirement_state_value\"\xc9\x03\n" + "\x15CatalogueInstallation\x12B\n" + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12\x19\n" + "\borg_name\x18\x02 \x01(\tR\aorgName\x12I\n" + @@ -2939,32 +3595,25 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\frevision_gap\x18\x05 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\vrevisionGap\x12J\n" + "\rexposed_teams\x18\x06 \x03(\v2%.saas.accounts.v1.CollectionReadGrantR\fexposedTeamsB\x15\n" + "\x13agent_release_valueB\x10\n" + - "\x0erevision_value\"\xbe\b\n" + + "\x0erevision_value\"\xf1\x06\n" + "\x0eCatalogueEntry\x128\n" + "\x04kind\x18\x01 \x01(\x0e2$.saas.accounts.v1.CatalogueEntryKindR\x04kind\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1e\n" + "\tpublisher\x18\x03 \x01(\tH\x00R\tpublisher\x12E\n" + - "\rpublisher_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\fpublisherGap\x12O\n" + - "\x10declared_release\x18\x05 \x01(\v2\".saas.accounts.v1.CatalogueReleaseH\x01R\x0fdeclaredRelease\x12R\n" + - "\x14declared_release_gap\x18\x06 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x12declaredReleaseGap\x12#\n" + - "\fbuild_digest\x18\a \x01(\tH\x02R\vbuildDigest\x12J\n" + - "\x10build_digest_gap\x18\b \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x02R\x0ebuildDigestGap\x12 \n" + + "\rpublisher_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\fpublisherGap\x12<\n" + + "\adesired\x18\x05 \x01(\v2\".saas.accounts.v1.CatalogueDesiredR\adesired\x12E\n" + "\n" + - "generation\x18\t \x01(\x03H\x03R\n" + - "generation\x12G\n" + - "\x0egeneration_gap\x18\n" + - " \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x03R\rgenerationGap\x12>\n" + - "\arunning\x18\v \x01(\v2\".saas.accounts.v1.CatalogueRunningH\x04R\arunning\x12A\n" + - "\vrunning_gap\x18\f \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x04R\n" + - "runningGap\x12F\n" + - "\x0ebuild_size_gap\x18\r \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x05R\fbuildSizeGap\x12O\n" + - "\fregistration\x18\x0e \x01(\v2&.saas.accounts.v1.SolutionRegistrationH\x06R\fregistration\x88\x01\x01\x12M\n" + - "\rinstallations\x18\x0f \x03(\v2'.saas.accounts.v1.CatalogueInstallationR\rinstallationsB\x11\n" + - "\x0fpublisher_valueB\x18\n" + - "\x16declared_release_valueB\x14\n" + - "\x12build_digest_valueB\x12\n" + - "\x10generation_valueB\x0f\n" + - "\rrunning_valueB\x12\n" + + "authorized\x18\x06 \x01(\v2%.saas.accounts.v1.CatalogueAuthorizedR\n" + + "authorized\x12<\n" + + "\aapplied\x18\a \x01(\v2\".saas.accounts.v1.CatalogueAppliedR\aapplied\x12?\n" + + "\bobserved\x18\b \x01(\v2#.saas.accounts.v1.CatalogueObservedR\bobserved\x12H\n" + + "\vwithdrawing\x18\t \x01(\v2&.saas.accounts.v1.CatalogueWithdrawingR\vwithdrawing\x12<\n" + + "\aretired\x18\n" + + " \x01(\v2\".saas.accounts.v1.CatalogueRetiredR\aretired\x12F\n" + + "\x0ebuild_size_gap\x18\v \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\fbuildSizeGap\x12O\n" + + "\fregistration\x18\f \x01(\v2&.saas.accounts.v1.SolutionRegistrationH\x02R\fregistration\x88\x01\x01\x12M\n" + + "\rinstallations\x18\r \x03(\v2'.saas.accounts.v1.CatalogueInstallationR\rinstallationsB\x11\n" + + "\x0fpublisher_valueB\x12\n" + "\x10build_size_valueB\x0f\n" + "\r_registration\"M\n" + "\x1cListPlatformCatalogueRequest\x12-\n" + @@ -2980,11 +3629,12 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + " CATALOGUE_GAP_REASON_UNSPECIFIED\x10\x00\x12%\n" + "!CATALOGUE_GAP_REASON_NOT_RECORDED\x10\x01\x12%\n" + "!CATALOGUE_GAP_REASON_NOT_REPORTED\x10\x02\x12%\n" + - "!CATALOGUE_GAP_REASON_NOT_OBSERVED\x10\x03*\x92\x01\n" + - "\x17CatalogueRunningVerdict\x12)\n" + - "%CATALOGUE_RUNNING_VERDICT_UNSPECIFIED\x10\x00\x12%\n" + - "!CATALOGUE_RUNNING_VERDICT_MATCHES\x10\x01\x12%\n" + - "!CATALOGUE_RUNNING_VERDICT_DIFFERS\x10\x022\xef\x1f\n" + + "!CATALOGUE_GAP_REASON_NOT_OBSERVED\x10\x03*\xde\x01\n" + + "\x18CatalogueObservedVerdict\x12*\n" + + "&CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED\x10\x00\x121\n" + + "-CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED\x10\x01\x12.\n" + + "*CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS\x10\x02\x123\n" + + "/CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED\x10\x032\xef\x1f\n" + "\x14PlatformAdminService\x12\x8e\x01\n" + "\vSearchUsers\x12$.saas.accounts.v1.SearchUsersRequest\x1a%.saas.accounts.v1.SearchUsersResponse\"2\xc2\xf3\x18\x14\b\x02\x10\x010\x01:\x02\x10\x01@\x01H\x05P\x03X\x03`\x03\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/platform/users\x12\xab\x01\n" + "\vSuspendUser\x12$.saas.accounts.v1.SuspendUserRequest\x1a\x16.google.protobuf.Empty\"^\xc2\xf3\x18+\b\x02\x10\x010\x01:\x17\n" + @@ -3035,11 +3685,11 @@ func file_saas_accounts_v1_platform_admin_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_platform_admin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 48) var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ (CatalogueEntryKind)(0), // 0: saas.accounts.v1.CatalogueEntryKind (CatalogueGapReason)(0), // 1: saas.accounts.v1.CatalogueGapReason - (CatalogueRunningVerdict)(0), // 2: saas.accounts.v1.CatalogueRunningVerdict + (CatalogueObservedVerdict)(0), // 2: saas.accounts.v1.CatalogueObservedVerdict (*SearchUsersRequest)(nil), // 3: saas.accounts.v1.SearchUsersRequest (*SearchUsersResponse)(nil), // 4: saas.accounts.v1.SearchUsersResponse (*ListAllOrganizationsRequest)(nil), // 5: saas.accounts.v1.ListAllOrganizationsRequest @@ -3075,120 +3725,142 @@ var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ (*CatalogueGap)(nil), // 35: saas.accounts.v1.CatalogueGap (*CatalogueRelease)(nil), // 36: saas.accounts.v1.CatalogueRelease (*CatalogueExecution)(nil), // 37: saas.accounts.v1.CatalogueExecution - (*CatalogueRunning)(nil), // 38: saas.accounts.v1.CatalogueRunning - (*CatalogueInstallation)(nil), // 39: saas.accounts.v1.CatalogueInstallation - (*CatalogueEntry)(nil), // 40: saas.accounts.v1.CatalogueEntry - (*ListPlatformCatalogueRequest)(nil), // 41: saas.accounts.v1.ListPlatformCatalogueRequest - (*ListPlatformCatalogueResponse)(nil), // 42: saas.accounts.v1.ListPlatformCatalogueResponse - nil, // 43: saas.accounts.v1.SessionInfo.DeviceInfoEntry - (*User)(nil), // 44: saas.accounts.v1.User - (*Organization)(nil), // 45: saas.accounts.v1.Organization - (*OrgMembership)(nil), // 46: saas.accounts.v1.OrgMembership - (*timestamppb.Timestamp)(nil), // 47: google.protobuf.Timestamp - (*Installation)(nil), // 48: saas.accounts.v1.Installation - (*CollectionReadGrant)(nil), // 49: saas.accounts.v1.CollectionReadGrant - (*SolutionRegistration)(nil), // 50: saas.accounts.v1.SolutionRegistration - (*v1.GetJobOperationsRequest)(nil), // 51: saas.jobs.v1.GetJobOperationsRequest - (*v1.ListJobsRequest)(nil), // 52: saas.jobs.v1.ListJobsRequest - (*v1.GetJobRequest)(nil), // 53: saas.jobs.v1.GetJobRequest - (*v1.ReplayJobRequest)(nil), // 54: saas.jobs.v1.ReplayJobRequest - (*v11.GetEventOperationsRequest)(nil), // 55: saas.events.v1.GetEventOperationsRequest - (*v11.ListEventSubscriptionsRequest)(nil), // 56: saas.events.v1.ListEventSubscriptionsRequest - (*emptypb.Empty)(nil), // 57: google.protobuf.Empty - (*v1.GetJobOperationsResponse)(nil), // 58: saas.jobs.v1.GetJobOperationsResponse - (*v1.ListJobsResponse)(nil), // 59: saas.jobs.v1.ListJobsResponse - (*v1.GetJobResponse)(nil), // 60: saas.jobs.v1.GetJobResponse - (*v1.ReplayJobResponse)(nil), // 61: saas.jobs.v1.ReplayJobResponse - (*v11.GetEventOperationsResponse)(nil), // 62: saas.events.v1.GetEventOperationsResponse - (*v11.ListEventSubscriptionsResponse)(nil), // 63: saas.events.v1.ListEventSubscriptionsResponse + (*CatalogueDesired)(nil), // 38: saas.accounts.v1.CatalogueDesired + (*CatalogueAuthorization)(nil), // 39: saas.accounts.v1.CatalogueAuthorization + (*CatalogueNotAuthorized)(nil), // 40: saas.accounts.v1.CatalogueNotAuthorized + (*CatalogueAuthorized)(nil), // 41: saas.accounts.v1.CatalogueAuthorized + (*CatalogueApplied)(nil), // 42: saas.accounts.v1.CatalogueApplied + (*CatalogueObserved)(nil), // 43: saas.accounts.v1.CatalogueObserved + (*CatalogueWithdrawing)(nil), // 44: saas.accounts.v1.CatalogueWithdrawing + (*CatalogueRetired)(nil), // 45: saas.accounts.v1.CatalogueRetired + (*CatalogueInstallation)(nil), // 46: saas.accounts.v1.CatalogueInstallation + (*CatalogueEntry)(nil), // 47: saas.accounts.v1.CatalogueEntry + (*ListPlatformCatalogueRequest)(nil), // 48: saas.accounts.v1.ListPlatformCatalogueRequest + (*ListPlatformCatalogueResponse)(nil), // 49: saas.accounts.v1.ListPlatformCatalogueResponse + nil, // 50: saas.accounts.v1.SessionInfo.DeviceInfoEntry + (*User)(nil), // 51: saas.accounts.v1.User + (*Organization)(nil), // 52: saas.accounts.v1.Organization + (*OrgMembership)(nil), // 53: saas.accounts.v1.OrgMembership + (*timestamppb.Timestamp)(nil), // 54: google.protobuf.Timestamp + (*Installation)(nil), // 55: saas.accounts.v1.Installation + (*CollectionReadGrant)(nil), // 56: saas.accounts.v1.CollectionReadGrant + (*SolutionRegistration)(nil), // 57: saas.accounts.v1.SolutionRegistration + (*v1.GetJobOperationsRequest)(nil), // 58: saas.jobs.v1.GetJobOperationsRequest + (*v1.ListJobsRequest)(nil), // 59: saas.jobs.v1.ListJobsRequest + (*v1.GetJobRequest)(nil), // 60: saas.jobs.v1.GetJobRequest + (*v1.ReplayJobRequest)(nil), // 61: saas.jobs.v1.ReplayJobRequest + (*v11.GetEventOperationsRequest)(nil), // 62: saas.events.v1.GetEventOperationsRequest + (*v11.ListEventSubscriptionsRequest)(nil), // 63: saas.events.v1.ListEventSubscriptionsRequest + (*emptypb.Empty)(nil), // 64: google.protobuf.Empty + (*v1.GetJobOperationsResponse)(nil), // 65: saas.jobs.v1.GetJobOperationsResponse + (*v1.ListJobsResponse)(nil), // 66: saas.jobs.v1.ListJobsResponse + (*v1.GetJobResponse)(nil), // 67: saas.jobs.v1.GetJobResponse + (*v1.ReplayJobResponse)(nil), // 68: saas.jobs.v1.ReplayJobResponse + (*v11.GetEventOperationsResponse)(nil), // 69: saas.events.v1.GetEventOperationsResponse + (*v11.ListEventSubscriptionsResponse)(nil), // 70: saas.events.v1.ListEventSubscriptionsResponse } var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ - 44, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User - 45, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization + 51, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User + 52, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization 6, // 2: saas.accounts.v1.ListAllOrganizationsResponse.organizations:type_name -> saas.accounts.v1.PlatformOrganization - 46, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership - 43, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry - 47, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp - 47, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp - 47, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp - 47, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp + 53, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership + 50, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry + 54, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp + 54, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp + 54, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp + 54, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp 17, // 9: saas.accounts.v1.ListActiveSessionsResponse.sessions:type_name -> saas.accounts.v1.SessionInfo 22, // 10: saas.accounts.v1.GetOrgEntitlementsResponse.entitlements:type_name -> saas.accounts.v1.EntitlementInfo - 47, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp + 54, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp 28, // 12: saas.accounts.v1.ListPlatformAdminsResponse.admins:type_name -> saas.accounts.v1.PlatformAdminEntry 31, // 13: saas.accounts.v1.ListFeatureFlagsResponse.flags:type_name -> saas.accounts.v1.FeatureFlagEntry 1, // 14: saas.accounts.v1.CatalogueGap.reason:type_name -> saas.accounts.v1.CatalogueGapReason - 37, // 15: saas.accounts.v1.CatalogueRunning.declared:type_name -> saas.accounts.v1.CatalogueExecution - 37, // 16: saas.accounts.v1.CatalogueRunning.observed:type_name -> saas.accounts.v1.CatalogueExecution - 2, // 17: saas.accounts.v1.CatalogueRunning.verdict:type_name -> saas.accounts.v1.CatalogueRunningVerdict - 48, // 18: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation - 36, // 19: saas.accounts.v1.CatalogueInstallation.agent_release:type_name -> saas.accounts.v1.CatalogueRelease - 35, // 20: saas.accounts.v1.CatalogueInstallation.agent_release_gap:type_name -> saas.accounts.v1.CatalogueGap - 35, // 21: saas.accounts.v1.CatalogueInstallation.revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 49, // 22: saas.accounts.v1.CatalogueInstallation.exposed_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 0, // 23: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind - 35, // 24: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap - 36, // 25: saas.accounts.v1.CatalogueEntry.declared_release:type_name -> saas.accounts.v1.CatalogueRelease - 35, // 26: saas.accounts.v1.CatalogueEntry.declared_release_gap:type_name -> saas.accounts.v1.CatalogueGap - 35, // 27: saas.accounts.v1.CatalogueEntry.build_digest_gap:type_name -> saas.accounts.v1.CatalogueGap - 35, // 28: saas.accounts.v1.CatalogueEntry.generation_gap:type_name -> saas.accounts.v1.CatalogueGap - 38, // 29: saas.accounts.v1.CatalogueEntry.running:type_name -> saas.accounts.v1.CatalogueRunning - 35, // 30: saas.accounts.v1.CatalogueEntry.running_gap:type_name -> saas.accounts.v1.CatalogueGap - 35, // 31: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap - 50, // 32: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration - 39, // 33: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation - 40, // 34: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry - 3, // 35: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest - 10, // 36: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest - 11, // 37: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest - 12, // 38: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest - 14, // 39: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest - 16, // 40: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest - 19, // 41: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest - 20, // 42: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest - 23, // 43: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest - 25, // 44: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest - 26, // 45: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest - 27, // 46: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest - 5, // 47: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest - 8, // 48: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest - 30, // 49: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest - 33, // 50: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest - 51, // 51: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest - 52, // 52: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest - 53, // 53: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest - 54, // 54: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest - 55, // 55: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest - 56, // 56: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest - 41, // 57: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest - 4, // 58: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse - 57, // 59: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty - 57, // 60: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty - 13, // 61: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse - 15, // 62: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse - 18, // 63: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse - 57, // 64: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty - 21, // 65: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse - 24, // 66: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse - 57, // 67: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty - 57, // 68: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty - 29, // 69: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse - 7, // 70: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse - 9, // 71: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse - 32, // 72: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse - 34, // 73: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse - 58, // 74: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse - 59, // 75: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse - 60, // 76: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse - 61, // 77: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse - 62, // 78: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse - 63, // 79: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse - 42, // 80: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse - 58, // [58:81] is the sub-list for method output_type - 35, // [35:58] is the sub-list for method input_type - 35, // [35:35] is the sub-list for extension type_name - 35, // [35:35] is the sub-list for extension extendee - 0, // [0:35] is the sub-list for field type_name + 35, // 15: saas.accounts.v1.CatalogueDesired.declared_revision_gap:type_name -> saas.accounts.v1.CatalogueGap + 36, // 16: saas.accounts.v1.CatalogueDesired.declared_release:type_name -> saas.accounts.v1.CatalogueRelease + 35, // 17: saas.accounts.v1.CatalogueDesired.declared_release_gap:type_name -> saas.accounts.v1.CatalogueGap + 37, // 18: saas.accounts.v1.CatalogueAuthorization.approved_execution:type_name -> saas.accounts.v1.CatalogueExecution + 39, // 19: saas.accounts.v1.CatalogueAuthorized.authorization:type_name -> saas.accounts.v1.CatalogueAuthorization + 40, // 20: saas.accounts.v1.CatalogueAuthorized.not_authorized:type_name -> saas.accounts.v1.CatalogueNotAuthorized + 35, // 21: saas.accounts.v1.CatalogueAuthorized.authorization_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 22: saas.accounts.v1.CatalogueApplied.applied_revision_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 23: saas.accounts.v1.CatalogueObserved.observed_revision_gap:type_name -> saas.accounts.v1.CatalogueGap + 54, // 24: saas.accounts.v1.CatalogueObserved.observed_at:type_name -> google.protobuf.Timestamp + 35, // 25: saas.accounts.v1.CatalogueObserved.observation_freshness_gap:type_name -> saas.accounts.v1.CatalogueGap + 37, // 26: saas.accounts.v1.CatalogueObserved.observed_execution:type_name -> saas.accounts.v1.CatalogueExecution + 35, // 27: saas.accounts.v1.CatalogueObserved.observed_execution_gap:type_name -> saas.accounts.v1.CatalogueGap + 2, // 28: saas.accounts.v1.CatalogueObserved.verdict:type_name -> saas.accounts.v1.CatalogueObservedVerdict + 35, // 29: saas.accounts.v1.CatalogueObserved.verdict_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 30: saas.accounts.v1.CatalogueWithdrawing.withdrawal_state_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 31: saas.accounts.v1.CatalogueWithdrawing.credential_revocation_state_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 32: saas.accounts.v1.CatalogueRetired.retirement_state_gap:type_name -> saas.accounts.v1.CatalogueGap + 55, // 33: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation + 36, // 34: saas.accounts.v1.CatalogueInstallation.agent_release:type_name -> saas.accounts.v1.CatalogueRelease + 35, // 35: saas.accounts.v1.CatalogueInstallation.agent_release_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 36: saas.accounts.v1.CatalogueInstallation.revision_gap:type_name -> saas.accounts.v1.CatalogueGap + 56, // 37: saas.accounts.v1.CatalogueInstallation.exposed_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 0, // 38: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind + 35, // 39: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap + 38, // 40: saas.accounts.v1.CatalogueEntry.desired:type_name -> saas.accounts.v1.CatalogueDesired + 41, // 41: saas.accounts.v1.CatalogueEntry.authorized:type_name -> saas.accounts.v1.CatalogueAuthorized + 42, // 42: saas.accounts.v1.CatalogueEntry.applied:type_name -> saas.accounts.v1.CatalogueApplied + 43, // 43: saas.accounts.v1.CatalogueEntry.observed:type_name -> saas.accounts.v1.CatalogueObserved + 44, // 44: saas.accounts.v1.CatalogueEntry.withdrawing:type_name -> saas.accounts.v1.CatalogueWithdrawing + 45, // 45: saas.accounts.v1.CatalogueEntry.retired:type_name -> saas.accounts.v1.CatalogueRetired + 35, // 46: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap + 57, // 47: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration + 46, // 48: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation + 47, // 49: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry + 3, // 50: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest + 10, // 51: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest + 11, // 52: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest + 12, // 53: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest + 14, // 54: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest + 16, // 55: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest + 19, // 56: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest + 20, // 57: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest + 23, // 58: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest + 25, // 59: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest + 26, // 60: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest + 27, // 61: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest + 5, // 62: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest + 8, // 63: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest + 30, // 64: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest + 33, // 65: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest + 58, // 66: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest + 59, // 67: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest + 60, // 68: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest + 61, // 69: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest + 62, // 70: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest + 63, // 71: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest + 48, // 72: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest + 4, // 73: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse + 64, // 74: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty + 64, // 75: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty + 13, // 76: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse + 15, // 77: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse + 18, // 78: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse + 64, // 79: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty + 21, // 80: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse + 24, // 81: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse + 64, // 82: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty + 64, // 83: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty + 29, // 84: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse + 7, // 85: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse + 9, // 86: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse + 32, // 87: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse + 34, // 88: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse + 65, // 89: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse + 66, // 90: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse + 67, // 91: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse + 68, // 92: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse + 69, // 93: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse + 70, // 94: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse + 49, // 95: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse + 73, // [73:96] is the sub-list for method output_type + 50, // [50:73] is the sub-list for method input_type + 50, // [50:50] is the sub-list for extension type_name + 50, // [50:50] is the sub-list for extension extendee + 0, // [0:50] is the sub-list for field type_name } func init() { file_saas_accounts_v1_platform_admin_proto_init() } @@ -3200,22 +3872,46 @@ func file_saas_accounts_v1_platform_admin_proto_init() { file_saas_accounts_v1_common_proto_init() file_saas_accounts_v1_installations_proto_init() file_saas_accounts_v1_solution_registry_proto_init() - file_saas_accounts_v1_platform_admin_proto_msgTypes[36].OneofWrappers = []any{ + file_saas_accounts_v1_platform_admin_proto_msgTypes[35].OneofWrappers = []any{ + (*CatalogueDesired_DeclaredRevision)(nil), + (*CatalogueDesired_DeclaredRevisionGap)(nil), + (*CatalogueDesired_DeclaredRelease)(nil), + (*CatalogueDesired_DeclaredReleaseGap)(nil), + } + file_saas_accounts_v1_platform_admin_proto_msgTypes[38].OneofWrappers = []any{ + (*CatalogueAuthorized_Authorization)(nil), + (*CatalogueAuthorized_NotAuthorized)(nil), + (*CatalogueAuthorized_AuthorizationGap)(nil), + } + file_saas_accounts_v1_platform_admin_proto_msgTypes[39].OneofWrappers = []any{ + (*CatalogueApplied_AppliedRevision)(nil), + (*CatalogueApplied_AppliedRevisionGap)(nil), + } + file_saas_accounts_v1_platform_admin_proto_msgTypes[40].OneofWrappers = []any{ + (*CatalogueObserved_ObservedRevision)(nil), + (*CatalogueObserved_ObservedRevisionGap)(nil), + (*CatalogueObserved_ObservedAt)(nil), + (*CatalogueObserved_ObservationFreshnessGap)(nil), + (*CatalogueObserved_ObservedExecution)(nil), + (*CatalogueObserved_ObservedExecutionGap)(nil), + (*CatalogueObserved_Verdict)(nil), + (*CatalogueObserved_VerdictGap)(nil), + } + file_saas_accounts_v1_platform_admin_proto_msgTypes[41].OneofWrappers = []any{ + (*CatalogueWithdrawing_WithdrawalStateGap)(nil), + (*CatalogueWithdrawing_CredentialRevocationStateGap)(nil), + } + file_saas_accounts_v1_platform_admin_proto_msgTypes[42].OneofWrappers = []any{ + (*CatalogueRetired_RetirementStateGap)(nil), + } + file_saas_accounts_v1_platform_admin_proto_msgTypes[43].OneofWrappers = []any{ (*CatalogueInstallation_AgentRelease)(nil), (*CatalogueInstallation_AgentReleaseGap)(nil), (*CatalogueInstallation_RevisionGap)(nil), } - file_saas_accounts_v1_platform_admin_proto_msgTypes[37].OneofWrappers = []any{ + file_saas_accounts_v1_platform_admin_proto_msgTypes[44].OneofWrappers = []any{ (*CatalogueEntry_Publisher)(nil), (*CatalogueEntry_PublisherGap)(nil), - (*CatalogueEntry_DeclaredRelease)(nil), - (*CatalogueEntry_DeclaredReleaseGap)(nil), - (*CatalogueEntry_BuildDigest)(nil), - (*CatalogueEntry_BuildDigestGap)(nil), - (*CatalogueEntry_Generation)(nil), - (*CatalogueEntry_GenerationGap)(nil), - (*CatalogueEntry_Running)(nil), - (*CatalogueEntry_RunningGap)(nil), (*CatalogueEntry_BuildSizeGap)(nil), } type x struct{} @@ -3224,7 +3920,7 @@ func file_saas_accounts_v1_platform_admin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_platform_admin_proto_rawDesc), len(file_saas_accounts_v1_platform_admin_proto_rawDesc)), NumEnums: 3, - NumMessages: 41, + NumMessages: 48, NumExtensions: 0, NumServices: 1, }, diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 81b089d58..7aa6c77c0 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -6845,20 +6845,55 @@ } } }, - "v1CatalogueEntry": { + "v1CatalogueApplied": { "type": "object", "properties": { - "kind": { - "$ref": "#/definitions/v1CatalogueEntryKind" + "appliedRevision": { + "type": "string", + "format": "int64" }, - "name": { + "appliedRevisionGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "description": "CatalogueApplied is what delivery actually wrote." + }, + "v1CatalogueAuthorization": { + "type": "object", + "properties": { + "authorizedRevision": { "type": "string", - "description": "A module's principal prefix, or a solution's registry id — the identifier\nits installations name." + "format": "int64" }, - "publisher": { - "type": "string" + "approvedExecution": { + "$ref": "#/definitions/v1CatalogueExecution" + } + }, + "description": "CatalogueAuthorization is a current platform approval: the authorized\nrevision and the execution it approved." + }, + "v1CatalogueAuthorized": { + "type": "object", + "properties": { + "authorization": { + "$ref": "#/definitions/v1CatalogueAuthorization" }, - "publisherGap": { + "notAuthorized": { + "$ref": "#/definitions/v1CatalogueNotAuthorized" + }, + "authorizationGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "description": "CatalogueAuthorized is what the platform has approved to execute." + }, + "v1CatalogueDesired": { + "type": "object", + "properties": { + "declaredRevision": { + "type": "string", + "format": "int64" + }, + "declaredRevisionGap": { "$ref": "#/definitions/v1CatalogueGap" }, "declaredRelease": { @@ -6866,32 +6901,50 @@ }, "declaredReleaseGap": { "$ref": "#/definitions/v1CatalogueGap" + } + }, + "description": "CatalogueDesired is what the composition declares." + }, + "v1CatalogueEntry": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/definitions/v1CatalogueEntryKind" }, - "buildDigest": { + "name": { + "type": "string", + "description": "A module's principal prefix, or a solution's registry id — the identifier\nits installations name." + }, + "publisher": { "type": "string" }, - "buildDigestGap": { + "publisherGap": { "$ref": "#/definitions/v1CatalogueGap" }, - "generation": { - "type": "string", - "format": "int64" + "desired": { + "$ref": "#/definitions/v1CatalogueDesired" }, - "generationGap": { - "$ref": "#/definitions/v1CatalogueGap" + "authorized": { + "$ref": "#/definitions/v1CatalogueAuthorized" }, - "running": { - "$ref": "#/definitions/v1CatalogueRunning" + "applied": { + "$ref": "#/definitions/v1CatalogueApplied" }, - "runningGap": { - "$ref": "#/definitions/v1CatalogueGap" + "observed": { + "$ref": "#/definitions/v1CatalogueObserved" + }, + "withdrawing": { + "$ref": "#/definitions/v1CatalogueWithdrawing" + }, + "retired": { + "$ref": "#/definitions/v1CatalogueRetired" }, "buildSizeGap": { "$ref": "#/definitions/v1CatalogueGap" }, "registration": { "$ref": "#/definitions/v1SolutionRegistration", - "description": "The solution's registration, with deployment topology withheld: the\nfrontend manifest, backend upstream and service alias are always empty\nhere, as on every browser-facing projection of the registry. Absent for a\nmodule, which does not self-register, and for a solution known only from\nits installations." + "description": "The solution's self-registration, with deployment topology withheld: the\nfrontend manifest, backend upstream and service alias are always empty\nhere, as on every browser-facing projection of the registry. Absent for a\nmodule, which does not self-register, and for a solution known only from\nits installations." }, "installations": { "type": "array", @@ -6901,7 +6954,7 @@ } } }, - "description": "CatalogueEntry is one module or solution." + "description": "CatalogueEntry is one module or solution, placed in the registry's state\nmodel: desired, authorized, applied, observed, withdrawing and physically\nretired are distinct, and a row shows each separately, so an entry desired\nbut not authorized, or observed but no longer authorized, reads as exactly\nthat." }, "v1CatalogueEntryKind": { "type": "string", @@ -6979,6 +7032,58 @@ }, "description": "CatalogueInstallation is one organization's installation of a solution." }, + "v1CatalogueNotAuthorized": { + "type": "object", + "properties": { + "detail": { + "type": "string" + } + }, + "description": "CatalogueNotAuthorized is a known absence of current authorization —\nnever approved, or approval withdrawn. It is a statement the approval record\nmakes, distinct from a CatalogueGap, where the host cannot tell." + }, + "v1CatalogueObserved": { + "type": "object", + "properties": { + "observedRevision": { + "type": "string", + "format": "int64" + }, + "observedRevisionGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "observedAt": { + "type": "string", + "format": "date-time" + }, + "observationFreshnessGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "observedExecution": { + "$ref": "#/definitions/v1CatalogueExecution" + }, + "observedExecutionGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "verdict": { + "$ref": "#/definitions/v1CatalogueObservedVerdict" + }, + "verdictGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "description": "CatalogueObserved is what is actually running." + }, + "v1CatalogueObservedVerdict": { + "type": "string", + "enum": [ + "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", + "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", + "CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS", + "CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED" + ], + "default": "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", + "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: The observed image digest and incarnation are the approved execution.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop." + }, "v1CatalogueRelease": { "type": "object", "properties": { @@ -6992,32 +7097,28 @@ "type": "string" } }, - "description": "CatalogueRelease is a declared release, publisher/name@version." + "description": "CatalogueRelease is a release, publisher/name@version." }, - "v1CatalogueRunning": { + "v1CatalogueRetired": { "type": "object", "properties": { - "declared": { - "$ref": "#/definitions/v1CatalogueExecution" - }, - "observed": { - "$ref": "#/definitions/v1CatalogueExecution" - }, - "verdict": { - "$ref": "#/definitions/v1CatalogueRunningVerdict" + "retirementStateGap": { + "$ref": "#/definitions/v1CatalogueGap" } }, - "description": "CatalogueRunning is the running incarnation beside the declared one." + "description": "CatalogueRetired is whether the workload is physically stopped. Retired\nrequires stop or fence evidence; until a retirement controller reports to\nthis host, the field holds only its gap and nothing reads as retired." }, - "v1CatalogueRunningVerdict": { - "type": "string", - "enum": [ - "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", - "CATALOGUE_RUNNING_VERDICT_MATCHES", - "CATALOGUE_RUNNING_VERDICT_DIFFERS" - ], - "default": "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", - "description": "CatalogueRunningVerdict compares the observed execution with the declared\none. It exists only when both are known; anything less is a CatalogueGap.\n\n - CATALOGUE_RUNNING_VERDICT_MATCHES: The observed image digest and incarnation are the declared ones.\n - CATALOGUE_RUNNING_VERDICT_DIFFERS: Something other than the declared execution is running — what cluster\nadmission refuses." + "v1CatalogueWithdrawing": { + "type": "object", + "properties": { + "withdrawalStateGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "credentialRevocationStateGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "description": "CatalogueWithdrawing is approval withdrawn and retirement not yet complete,\nacknowledged separately from retirement itself. The value cases are the\ndeployment contract's withdrawal and revocation records; until they exist\neach field holds only its gap." }, "v1ClientAuthorizationRequest": { "type": "object", diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index b8bef02ad..4298323f4 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -834,31 +834,81 @@ }, "type": "object" }, - "v1CatalogueEntry": { - "description": "CatalogueEntry is one module or solution.", + "v1CatalogueApplied": { + "description": "CatalogueApplied is what delivery actually wrote.", "properties": { - "buildDigest": { + "appliedRevision": { + "format": "int64", "type": "string" }, - "buildDigestGap": { + "appliedRevisionGap": { "$ref": "#/definitions/v1CatalogueGap" + } + }, + "type": "object" + }, + "v1CatalogueAuthorization": { + "description": "CatalogueAuthorization is a current platform approval: the authorized\nrevision and the execution it approved.", + "properties": { + "approvedExecution": { + "$ref": "#/definitions/v1CatalogueExecution" }, - "buildSizeGap": { + "authorizedRevision": { + "format": "int64", + "type": "string" + } + }, + "type": "object" + }, + "v1CatalogueAuthorized": { + "description": "CatalogueAuthorized is what the platform has approved to execute.", + "properties": { + "authorization": { + "$ref": "#/definitions/v1CatalogueAuthorization" + }, + "authorizationGap": { "$ref": "#/definitions/v1CatalogueGap" }, + "notAuthorized": { + "$ref": "#/definitions/v1CatalogueNotAuthorized" + } + }, + "type": "object" + }, + "v1CatalogueDesired": { + "description": "CatalogueDesired is what the composition declares.", + "properties": { "declaredRelease": { "$ref": "#/definitions/v1CatalogueRelease" }, "declaredReleaseGap": { "$ref": "#/definitions/v1CatalogueGap" }, - "generation": { + "declaredRevision": { "format": "int64", "type": "string" }, - "generationGap": { + "declaredRevisionGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "type": "object" + }, + "v1CatalogueEntry": { + "description": "CatalogueEntry is one module or solution, placed in the registry's state\nmodel: desired, authorized, applied, observed, withdrawing and physically\nretired are distinct, and a row shows each separately, so an entry desired\nbut not authorized, or observed but no longer authorized, reads as exactly\nthat.", + "properties": { + "applied": { + "$ref": "#/definitions/v1CatalogueApplied" + }, + "authorized": { + "$ref": "#/definitions/v1CatalogueAuthorized" + }, + "buildSizeGap": { "$ref": "#/definitions/v1CatalogueGap" }, + "desired": { + "$ref": "#/definitions/v1CatalogueDesired" + }, "installations": { "items": { "$ref": "#/definitions/v1CatalogueInstallation", @@ -873,6 +923,9 @@ "description": "A module's principal prefix, or a solution's registry id — the identifier\nits installations name.", "type": "string" }, + "observed": { + "$ref": "#/definitions/v1CatalogueObserved" + }, "publisher": { "type": "string" }, @@ -881,13 +934,13 @@ }, "registration": { "$ref": "#/definitions/v1SolutionRegistration", - "description": "The solution's registration, with deployment topology withheld: the\nfrontend manifest, backend upstream and service alias are always empty\nhere, as on every browser-facing projection of the registry. Absent for a\nmodule, which does not self-register, and for a solution known only from\nits installations." + "description": "The solution's self-registration, with deployment topology withheld: the\nfrontend manifest, backend upstream and service alias are always empty\nhere, as on every browser-facing projection of the registry. Absent for a\nmodule, which does not self-register, and for a solution known only from\nits installations." }, - "running": { - "$ref": "#/definitions/v1CatalogueRunning" + "retired": { + "$ref": "#/definitions/v1CatalogueRetired" }, - "runningGap": { - "$ref": "#/definitions/v1CatalogueGap" + "withdrawing": { + "$ref": "#/definitions/v1CatalogueWithdrawing" } }, "type": "object" @@ -968,8 +1021,60 @@ }, "type": "object" }, + "v1CatalogueNotAuthorized": { + "description": "CatalogueNotAuthorized is a known absence of current authorization —\nnever approved, or approval withdrawn. It is a statement the approval record\nmakes, distinct from a CatalogueGap, where the host cannot tell.", + "properties": { + "detail": { + "type": "string" + } + }, + "type": "object" + }, + "v1CatalogueObserved": { + "description": "CatalogueObserved is what is actually running.", + "properties": { + "observationFreshnessGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "observedAt": { + "format": "date-time", + "type": "string" + }, + "observedExecution": { + "$ref": "#/definitions/v1CatalogueExecution" + }, + "observedExecutionGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "observedRevision": { + "format": "int64", + "type": "string" + }, + "observedRevisionGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "verdict": { + "$ref": "#/definitions/v1CatalogueObservedVerdict" + }, + "verdictGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "type": "object" + }, + "v1CatalogueObservedVerdict": { + "default": "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", + "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: The observed image digest and incarnation are the approved execution.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop.", + "enum": [ + "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", + "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", + "CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS", + "CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED" + ], + "type": "string" + }, "v1CatalogueRelease": { - "description": "CatalogueRelease is a declared release, publisher/name@version.", + "description": "CatalogueRelease is a release, publisher/name@version.", "properties": { "name": { "type": "string" @@ -983,30 +1088,26 @@ }, "type": "object" }, - "v1CatalogueRunning": { - "description": "CatalogueRunning is the running incarnation beside the declared one.", + "v1CatalogueRetired": { + "description": "CatalogueRetired is whether the workload is physically stopped. Retired\nrequires stop or fence evidence; until a retirement controller reports to\nthis host, the field holds only its gap and nothing reads as retired.", "properties": { - "declared": { - "$ref": "#/definitions/v1CatalogueExecution" - }, - "observed": { - "$ref": "#/definitions/v1CatalogueExecution" - }, - "verdict": { - "$ref": "#/definitions/v1CatalogueRunningVerdict" + "retirementStateGap": { + "$ref": "#/definitions/v1CatalogueGap" } }, "type": "object" }, - "v1CatalogueRunningVerdict": { - "default": "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", - "description": "CatalogueRunningVerdict compares the observed execution with the declared\none. It exists only when both are known; anything less is a CatalogueGap.\n\n - CATALOGUE_RUNNING_VERDICT_MATCHES: The observed image digest and incarnation are the declared ones.\n - CATALOGUE_RUNNING_VERDICT_DIFFERS: Something other than the declared execution is running — what cluster\nadmission refuses.", - "enum": [ - "CATALOGUE_RUNNING_VERDICT_UNSPECIFIED", - "CATALOGUE_RUNNING_VERDICT_MATCHES", - "CATALOGUE_RUNNING_VERDICT_DIFFERS" - ], - "type": "string" + "v1CatalogueWithdrawing": { + "description": "CatalogueWithdrawing is approval withdrawn and retirement not yet complete,\nacknowledged separately from retirement itself. The value cases are the\ndeployment contract's withdrawal and revocation records; until they exist\neach field holds only its gap.", + "properties": { + "credentialRevocationStateGap": { + "$ref": "#/definitions/v1CatalogueGap" + }, + "withdrawalStateGap": { + "$ref": "#/definitions/v1CatalogueGap" + } + }, + "type": "object" }, "v1ClientAuthorizationRequest": { "description": "ClientAuthorizationRequest is the authorization request a client encodes into\nthe host's login-page URL. code_challenge is the S256 hash of a verifier the\nclient keeps, so an intercepted code cannot be redeemed by anyone else.", diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index 8c79d0188..00f45cfff 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -248,14 +248,20 @@ message UpsertFeatureFlagResponse { // the durable solution registry, the installations table, and the composition's // module principal registry — and it adds no store of its own. // -// Several facts the Catalogue is specified to show are carried by records this -// host does not hold yet: the declared release, build digest and generation -// arrive with applied presence documents, the running incarnation needs observed -// cluster state, and build size is a signed section of the presence document -// (codefly-dev/core#708, computed by the CLI at build). Each such fact is a -// oneof of its value and a CatalogueGap, so an absent fact is reported with its -// reason and is never an empty value a reader could take for zero, unset, or a -// match. A fact gains its value case when the record that carries it exists. +// Each entry is placed in the registry's state model — desired, authorized, +// applied, observed, withdrawing, physically retired — with the public status +// fields that model names (declared_revision, authorized_revision, +// applied_revision, observed_revision, observation_freshness, withdrawal_state, +// retirement_state, credential_revocation_state), never one "installed" flag. +// +// Most of those facts are carried by records this host does not hold yet: +// declarations and applied generations arrive with presence documents, approval +// and withdrawal with the deployment contract's signed approval, observations +// and retirement with cluster observation, and build size is a signed section of +// the presence document (codefly-dev/core#708, computed by the CLI at build). +// Each such fact is a oneof of its value and a CatalogueGap, so an absent fact is +// reported with its reason and is never an empty value a reader could take for +// zero, unset, or a match. A fact gains its value case when its record exists. // ============================================================================ // CatalogueEntryKind distinguishes a composed module from a solution. @@ -287,7 +293,7 @@ message CatalogueGap { string detail = 2; } -// CatalogueRelease is a declared release, publisher/name@version. +// CatalogueRelease is a release, publisher/name@version. message CatalogueRelease { string publisher = 1; string name = 2; @@ -301,22 +307,106 @@ message CatalogueExecution { uint64 build_incarnation = 2; } -// CatalogueRunningVerdict compares the observed execution with the declared -// one. It exists only when both are known; anything less is a CatalogueGap. -enum CatalogueRunningVerdict { - CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; - // The observed image digest and incarnation are the declared ones. - CATALOGUE_RUNNING_VERDICT_MATCHES = 1; - // Something other than the declared execution is running — what cluster +// CatalogueDesired is what the composition declares. +message CatalogueDesired { + oneof declared_revision_value { + int64 declared_revision = 1; + CatalogueGap declared_revision_gap = 2; + } + oneof declared_release_value { + CatalogueRelease declared_release = 3; + CatalogueGap declared_release_gap = 4; + } +} + +// CatalogueAuthorization is a current platform approval: the authorized +// revision and the execution it approved. +message CatalogueAuthorization { + int64 authorized_revision = 1; + CatalogueExecution approved_execution = 2; +} + +// CatalogueNotAuthorized is a known absence of current authorization — +// never approved, or approval withdrawn. It is a statement the approval record +// makes, distinct from a CatalogueGap, where the host cannot tell. +message CatalogueNotAuthorized { + string detail = 1; +} + +// CatalogueAuthorized is what the platform has approved to execute. +message CatalogueAuthorized { + oneof authorization_value { + CatalogueAuthorization authorization = 1; + CatalogueNotAuthorized not_authorized = 2; + CatalogueGap authorization_gap = 3; + } +} + +// CatalogueApplied is what delivery actually wrote. +message CatalogueApplied { + oneof applied_revision_value { + int64 applied_revision = 1; + CatalogueGap applied_revision_gap = 2; + } +} + +// CatalogueObservedVerdict judges what runs against what is authorized — +// never against what is declared, because only authorization admits execution. +enum CatalogueObservedVerdict { + CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; + // The observed image digest and incarnation are the approved execution. + CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; + // Something other than the approved execution is running: what cluster // admission refuses. - CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; + CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS = 2; + // Something is running with no current authorization at all: what the + // retirement sweep exists to stop. + CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED = 3; } -// CatalogueRunning is the running incarnation beside the declared one. -message CatalogueRunning { - CatalogueExecution declared = 1; - CatalogueExecution observed = 2; - CatalogueRunningVerdict verdict = 3; +// CatalogueObserved is what is actually running. +message CatalogueObserved { + oneof observed_revision_value { + int64 observed_revision = 1; + CatalogueGap observed_revision_gap = 2; + } + // When the observation was made; a stale observation is never current. + oneof observation_freshness_value { + google.protobuf.Timestamp observed_at = 3; + CatalogueGap observation_freshness_gap = 4; + } + oneof observed_execution_value { + CatalogueExecution observed_execution = 5; + CatalogueGap observed_execution_gap = 6; + } + // A verdict exists only when both an observation and a known authorization + // do; anything less is a gap, and an unobserved entry is never a match. + oneof verdict_value { + CatalogueObservedVerdict verdict = 7; + CatalogueGap verdict_gap = 8; + } +} + +// CatalogueWithdrawing is approval withdrawn and retirement not yet complete, +// acknowledged separately from retirement itself. The value cases are the +// deployment contract's withdrawal and revocation records; until they exist +// each field holds only its gap. +message CatalogueWithdrawing { + oneof withdrawal_state_value { + CatalogueGap withdrawal_state_gap = 1; + } + oneof credential_revocation_state_value { + CatalogueGap credential_revocation_state_gap = 2; + } +} + +// CatalogueRetired is whether the workload is physically stopped. Retired +// requires stop or fence evidence; until a retirement controller reports to +// this host, the field holds only its gap and nothing reads as retired. +message CatalogueRetired { + oneof retirement_state_value { + CatalogueGap retirement_state_gap = 1; + } } // CatalogueInstallation is one organization's installation of a solution. @@ -329,7 +419,7 @@ message CatalogueInstallation { CatalogueRelease agent_release = 3; CatalogueGap agent_release_gap = 4; } - // The installation revision the installed generation is pinned at. + // The installation's own revision, independent of the deployment's. oneof revision_value { CatalogueGap revision_gap = 5; } @@ -338,7 +428,11 @@ message CatalogueInstallation { repeated CollectionReadGrant exposed_teams = 6; } -// CatalogueEntry is one module or solution. +// CatalogueEntry is one module or solution, placed in the registry's state +// model: desired, authorized, applied, observed, withdrawing and physically +// retired are distinct, and a row shows each separately, so an entry desired +// but not authorized, or observed but no longer authorized, reads as exactly +// that. message CatalogueEntry { CatalogueEntryKind kind = 1; // A module's principal prefix, or a solution's registry id — the identifier @@ -349,37 +443,25 @@ message CatalogueEntry { string publisher = 3; CatalogueGap publisher_gap = 4; } - oneof declared_release_value { - CatalogueRelease declared_release = 5; - CatalogueGap declared_release_gap = 6; - } - // The OCI image manifest digest the entry was published at. - oneof build_digest_value { - string build_digest = 7; - CatalogueGap build_digest_gap = 8; - } - // The presence generation this host applied. - oneof generation_value { - int64 generation = 9; - CatalogueGap generation_gap = 10; - } - oneof running_value { - CatalogueRunning running = 11; - CatalogueGap running_gap = 12; - } + CatalogueDesired desired = 5; + CatalogueAuthorized authorized = 6; + CatalogueApplied applied = 7; + CatalogueObserved observed = 8; + CatalogueWithdrawing withdrawing = 9; + CatalogueRetired retired = 10; // Lines of code, backend and frontend, vendored paths excluded. The value // case is the presence document's build_size section (codefly-dev/core#708) // as delivered; this host counts nothing itself. oneof build_size_value { - CatalogueGap build_size_gap = 13; + CatalogueGap build_size_gap = 11; } - // The solution's registration, with deployment topology withheld: the + // The solution's self-registration, with deployment topology withheld: the // frontend manifest, backend upstream and service alias are always empty // here, as on every browser-facing projection of the registry. Absent for a // module, which does not self-register, and for a solution known only from // its installations. - optional SolutionRegistration registration = 14; - repeated CatalogueInstallation installations = 15; + optional SolutionRegistration registration = 12; + repeated CatalogueInstallation installations = 13; } message ListPlatformCatalogueRequest { diff --git a/module/services/frontend/AGENTS.md b/module/services/frontend/AGENTS.md index cfebe5267..603764115 100644 --- a/module/services/frontend/AGENTS.md +++ b/module/services/frontend/AGENTS.md @@ -79,9 +79,11 @@ activating a remote. it reads accounts' `PlatformAdminService.ListPlatformCatalogue`, super administrators only, and that response carries each registration's status, revisions, contract versions and leases with the manifest, upstream and service - alias withheld (`../accounts/AGENTS.md`). A fact the host has no record of is - rendered as its gap reason — "Not recorded", "Not observed" — never as an empty - cell, a zero or a match. + alias withheld (`../accounts/AGENTS.md`). Each entry shows the registry's six + states — desired, authorized, applied, observed, withdrawing, retired — one + column each, and a fact the host has no record of is rendered as its gap + reason ("Not recorded", "Not observed"), never as an empty cell, a zero or a + match. ## Loading a remote, and its CSP diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 7f593c86d..a22161acb 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048", + "digest": "sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index e1cd68f066d2eb1eee8e8b3f660cbc6817ce6b1a..9b89d9def1b008d9a75973c66811bfb0c179c984 100644 GIT binary patch delta 17440 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�pAc6TXXBS-HJ9g|&9_ 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

F9$%wn<>k6;%1Z)D{f_GIZif;s5^Pa0r9B$Epy zNr*f}CKpJO0Q+HSE|8?Dn*EDRE|8>w>tAJZfg}lBr^;mKlLW5QWO9Kd30$YQas<~I zGAUWC2tRO1aYHhBU9*o$=lUq!zySf{h=CLy#k|W z$qR>IwE9C_1^293a_If32xA2>&X!_fgb^FKLUWugorMWU(hxT^97#g##|%f3z;TXD zIw(mB#sI=LLo%Y2Y~Da3l#Fn_78-;{wAGOA=2=abtwBBHH>-NM}fd zQA?QB6Ve}SFVJP!#*>RBXRBm*_G@>3uvjLy_BI%~SSGjjHYmi!GP$+40l{aaSdkQB z8x8Oo>8wZqk_Ld!8h|7r@^c0tNdUMcrw~a~1eY3uq=De`h670;_<{jQ5&$kU07(MC u<*gh6u-O2_ibRiJbzG diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index be13e1f17..701ebc84f 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:dae674b62ba21fc79d40887c50c4647428567f2eb906fc2b287966d335b36048 + contract-digest: sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index ea62b0fad..ea551beb8 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCK+AQoQQ2F0YWxvZ3VlUnVubmluZxI2CghkZWNsYXJlZBgBIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRXhlY3V0aW9uEjYKCG9ic2VydmVkGAIgASgLMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFeGVjdXRpb24SOgoHdmVyZGljdBgDIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3Qi+AIKFUNhdGFsb2d1ZUluc3RhbGxhdGlvbhI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhIQCghvcmdfbmFtZRgCIAEoCRI7Cg1hZ2VudF9yZWxlYXNlGAMgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAASOwoRYWdlbnRfcmVsZWFzZV9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjYKDHJldmlzaW9uX2dhcBgFIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESPAoNZXhwb3NlZF90ZWFtcxgGIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIvwGCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI+ChBkZWNsYXJlZF9yZWxlYXNlGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAESPgoUZGVjbGFyZWRfcmVsZWFzZV9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEhYKDGJ1aWxkX2RpZ2VzdBgHIAEoCUgCEjoKEGJ1aWxkX2RpZ2VzdF9nYXAYCCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEhQKCmdlbmVyYXRpb24YCSABKANIAxI4Cg5nZW5lcmF0aW9uX2dhcBgKIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAMSNQoHcnVubmluZxgLIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ0gEEjUKC3J1bm5pbmdfZ2FwGAwgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIBBI4Cg5idWlsZF9zaXplX2dhcBgNIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAUSQQoMcmVnaXN0cmF0aW9uGA4gASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgGiAEBEj4KDWluc3RhbGxhdGlvbnMYDyADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZUIUChJidWlsZF9kaWdlc3RfdmFsdWVCEgoQZ2VuZXJhdGlvbl92YWx1ZUIPCg1ydW5uaW5nX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCJtCh1MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZRIxCgdlbnRyaWVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeRIZChFyZWdpc3RyeV9yZXZpc2lvbhgCIAEoAyp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyqSAQoXQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3QSKQolQ0FUQUxPR1VFX1JVTk5JTkdfVkVSRElDVF9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfTUFUQ0hFUxABEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfRElGRkVSUxACMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIvgCChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWV4cG9zZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnRCFQoTYWdlbnRfcmVsZWFzZV92YWx1ZUIQCg5yZXZpc2lvbl92YWx1ZSLjBQoOQ2F0YWxvZ3VlRW50cnkSMgoEa2luZBgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnlLaW5kEgwKBG5hbWUYAiABKAkSEwoJcHVibGlzaGVyGAMgASgJSAASNwoNcHVibGlzaGVyX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASMwoHZGVzaXJlZBgFIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRGVzaXJlZBI5CgphdXRob3JpemVkGAYgASgLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemVkEjMKB2FwcGxpZWQYByABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUFwcGxpZWQSNQoIb2JzZXJ2ZWQYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkEjsKC3dpdGhkcmF3aW5nGAkgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVXaXRoZHJhd2luZxIzCgdyZXRpcmVkGAogASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZXRpcmVkEjgKDmJ1aWxkX3NpemVfZ2FwGAsgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJBCgxyZWdpc3RyYXRpb24YDCABKAsyJi5zYWFzLmFjY291bnRzLnYxLlNvbHV0aW9uUmVnaXN0cmF0aW9uSAKIAQESPgoNaW5zdGFsbGF0aW9ucxgNIAMoCzInLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uQhEKD3B1Ymxpc2hlcl92YWx1ZUISChBidWlsZF9zaXplX3ZhbHVlQg8KDV9yZWdpc3RyYXRpb24iOgocTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBIaChJpbmNsdWRlX3RvbWJzdG9uZWQYASABKAgibQodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMqfgoSQ2F0YWxvZ3VlRW50cnlLaW5kEiQKIENBVEFMT0dVRV9FTlRSWV9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQ0FUQUxPR1VFX0VOVFJZX0tJTkRfTU9EVUxFEAESIQodQ0FUQUxPR1VFX0VOVFJZX0tJTkRfU09MVVRJT04QAiqvAQoSQ2F0YWxvZ3VlR2FwUmVhc29uEiQKIENBVEFMT0dVRV9HQVBfUkVBU09OX1VOU1BFQ0lGSUVEEAASJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFQ09SREVEEAESJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFUE9SVEVEEAISJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX09CU0VSVkVEEAMq3gEKGENhdGFsb2d1ZU9ic2VydmVkVmVyZGljdBIqCiZDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9VTlNQRUNJRklFRBAAEjEKLUNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfQVVUSE9SSVpFRBABEi4KKkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfRElGRkVSUxACEjMKL0NBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfVU5BVVRIT1JJWkVEEAMy7x8KFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnMSsAEKFUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZRIuLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2UiNsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCGBIWL3YxL3BsYXRmb3JtL2NhdGFsb2d1ZWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -858,7 +858,7 @@ export const CatalogueGapSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 32); /** - * CatalogueRelease is a declared release, publisher/name@version. + * CatalogueRelease is a release, publisher/name@version. * * @generated from message saas.accounts.v1.CatalogueRelease */ @@ -912,33 +912,319 @@ export const CatalogueExecutionSchema: GenMessage = /*@__PUR messageDesc(file_saas_accounts_v1_platform_admin, 34); /** - * CatalogueRunning is the running incarnation beside the declared one. + * CatalogueDesired is what the composition declares. * - * @generated from message saas.accounts.v1.CatalogueRunning + * @generated from message saas.accounts.v1.CatalogueDesired */ -export type CatalogueRunning = Message<"saas.accounts.v1.CatalogueRunning"> & { +export type CatalogueDesired = Message<"saas.accounts.v1.CatalogueDesired"> & { /** - * @generated from field: saas.accounts.v1.CatalogueExecution declared = 1; + * @generated from oneof saas.accounts.v1.CatalogueDesired.declared_revision_value */ - declared?: CatalogueExecution; + declaredRevisionValue: { + /** + * @generated from field: int64 declared_revision = 1; + */ + value: bigint; + case: "declaredRevision"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap declared_revision_gap = 2; + */ + value: CatalogueGap; + case: "declaredRevisionGap"; + } | { case: undefined; value?: undefined }; /** - * @generated from field: saas.accounts.v1.CatalogueExecution observed = 2; + * @generated from oneof saas.accounts.v1.CatalogueDesired.declared_release_value */ - observed?: CatalogueExecution; + declaredReleaseValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRelease declared_release = 3; + */ + value: CatalogueRelease; + case: "declaredRelease"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap declared_release_gap = 4; + */ + value: CatalogueGap; + case: "declaredReleaseGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueDesired. + * Use `create(CatalogueDesiredSchema)` to create a new message. + */ +export const CatalogueDesiredSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 35); +/** + * CatalogueAuthorization is a current platform approval: the authorized + * revision and the execution it approved. + * + * @generated from message saas.accounts.v1.CatalogueAuthorization + */ +export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthorization"> & { /** - * @generated from field: saas.accounts.v1.CatalogueRunningVerdict verdict = 3; + * @generated from field: int64 authorized_revision = 1; */ - verdict: CatalogueRunningVerdict; + authorizedRevision: bigint; + + /** + * @generated from field: saas.accounts.v1.CatalogueExecution approved_execution = 2; + */ + approvedExecution?: CatalogueExecution; }; /** - * Describes the message saas.accounts.v1.CatalogueRunning. - * Use `create(CatalogueRunningSchema)` to create a new message. + * Describes the message saas.accounts.v1.CatalogueAuthorization. + * Use `create(CatalogueAuthorizationSchema)` to create a new message. */ -export const CatalogueRunningSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 35); +export const CatalogueAuthorizationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 36); + +/** + * CatalogueNotAuthorized is a known absence of current authorization — + * never approved, or approval withdrawn. It is a statement the approval record + * makes, distinct from a CatalogueGap, where the host cannot tell. + * + * @generated from message saas.accounts.v1.CatalogueNotAuthorized + */ +export type CatalogueNotAuthorized = Message<"saas.accounts.v1.CatalogueNotAuthorized"> & { + /** + * @generated from field: string detail = 1; + */ + detail: string; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueNotAuthorized. + * Use `create(CatalogueNotAuthorizedSchema)` to create a new message. + */ +export const CatalogueNotAuthorizedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 37); + +/** + * CatalogueAuthorized is what the platform has approved to execute. + * + * @generated from message saas.accounts.v1.CatalogueAuthorized + */ +export type CatalogueAuthorized = Message<"saas.accounts.v1.CatalogueAuthorized"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueAuthorized.authorization_value + */ + authorizationValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueAuthorization authorization = 1; + */ + value: CatalogueAuthorization; + case: "authorization"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueNotAuthorized not_authorized = 2; + */ + value: CatalogueNotAuthorized; + case: "notAuthorized"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap authorization_gap = 3; + */ + value: CatalogueGap; + case: "authorizationGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueAuthorized. + * Use `create(CatalogueAuthorizedSchema)` to create a new message. + */ +export const CatalogueAuthorizedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 38); + +/** + * CatalogueApplied is what delivery actually wrote. + * + * @generated from message saas.accounts.v1.CatalogueApplied + */ +export type CatalogueApplied = Message<"saas.accounts.v1.CatalogueApplied"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueApplied.applied_revision_value + */ + appliedRevisionValue: { + /** + * @generated from field: int64 applied_revision = 1; + */ + value: bigint; + case: "appliedRevision"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap applied_revision_gap = 2; + */ + value: CatalogueGap; + case: "appliedRevisionGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueApplied. + * Use `create(CatalogueAppliedSchema)` to create a new message. + */ +export const CatalogueAppliedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 39); + +/** + * CatalogueObserved is what is actually running. + * + * @generated from message saas.accounts.v1.CatalogueObserved + */ +export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueObserved.observed_revision_value + */ + observedRevisionValue: { + /** + * @generated from field: int64 observed_revision = 1; + */ + value: bigint; + case: "observedRevision"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap observed_revision_gap = 2; + */ + value: CatalogueGap; + case: "observedRevisionGap"; + } | { case: undefined; value?: undefined }; + + /** + * When the observation was made; a stale observation is never current. + * + * @generated from oneof saas.accounts.v1.CatalogueObserved.observation_freshness_value + */ + observationFreshnessValue: { + /** + * @generated from field: google.protobuf.Timestamp observed_at = 3; + */ + value: Timestamp; + case: "observedAt"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap observation_freshness_gap = 4; + */ + value: CatalogueGap; + case: "observationFreshnessGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueObserved.observed_execution_value + */ + observedExecutionValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueExecution observed_execution = 5; + */ + value: CatalogueExecution; + case: "observedExecution"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap observed_execution_gap = 6; + */ + value: CatalogueGap; + case: "observedExecutionGap"; + } | { case: undefined; value?: undefined }; + + /** + * A verdict exists only when both an observation and a known authorization + * do; anything less is a gap, and an unobserved entry is never a match. + * + * @generated from oneof saas.accounts.v1.CatalogueObserved.verdict_value + */ + verdictValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueObservedVerdict verdict = 7; + */ + value: CatalogueObservedVerdict; + case: "verdict"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap verdict_gap = 8; + */ + value: CatalogueGap; + case: "verdictGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueObserved. + * Use `create(CatalogueObservedSchema)` to create a new message. + */ +export const CatalogueObservedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 40); + +/** + * CatalogueWithdrawing is approval withdrawn and retirement not yet complete, + * acknowledged separately from retirement itself. The value cases are the + * deployment contract's withdrawal and revocation records; until they exist + * each field holds only its gap. + * + * @generated from message saas.accounts.v1.CatalogueWithdrawing + */ +export type CatalogueWithdrawing = Message<"saas.accounts.v1.CatalogueWithdrawing"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueWithdrawing.withdrawal_state_value + */ + withdrawalStateValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap withdrawal_state_gap = 1; + */ + value: CatalogueGap; + case: "withdrawalStateGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueWithdrawing.credential_revocation_state_value + */ + credentialRevocationStateValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap credential_revocation_state_gap = 2; + */ + value: CatalogueGap; + case: "credentialRevocationStateGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueWithdrawing. + * Use `create(CatalogueWithdrawingSchema)` to create a new message. + */ +export const CatalogueWithdrawingSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 41); + +/** + * CatalogueRetired is whether the workload is physically stopped. Retired + * requires stop or fence evidence; until a retirement controller reports to + * this host, the field holds only its gap and nothing reads as retired. + * + * @generated from message saas.accounts.v1.CatalogueRetired + */ +export type CatalogueRetired = Message<"saas.accounts.v1.CatalogueRetired"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueRetired.retirement_state_value + */ + retirementStateValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap retirement_state_gap = 1; + */ + value: CatalogueGap; + case: "retirementStateGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueRetired. + * Use `create(CatalogueRetiredSchema)` to create a new message. + */ +export const CatalogueRetiredSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 42); /** * CatalogueInstallation is one organization's installation of a solution. @@ -977,7 +1263,7 @@ export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallat } | { case: undefined; value?: undefined }; /** - * The installation revision the installed generation is pinned at. + * The installation's own revision, independent of the deployment's. * * @generated from oneof saas.accounts.v1.CatalogueInstallation.revision_value */ @@ -1003,10 +1289,14 @@ export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallat * Use `create(CatalogueInstallationSchema)` to create a new message. */ export const CatalogueInstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 36); + messageDesc(file_saas_accounts_v1_platform_admin, 43); /** - * CatalogueEntry is one module or solution. + * CatalogueEntry is one module or solution, placed in the registry's state + * model: desired, authorized, applied, observed, withdrawing and physically + * retired are distinct, and a row shows each separately, so an entry desired + * but not authorized, or observed but no longer authorized, reads as exactly + * that. * * @generated from message saas.accounts.v1.CatalogueEntry */ @@ -1044,76 +1334,34 @@ export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { } | { case: undefined; value?: undefined }; /** - * @generated from oneof saas.accounts.v1.CatalogueEntry.declared_release_value + * @generated from field: saas.accounts.v1.CatalogueDesired desired = 5; */ - declaredReleaseValue: { - /** - * @generated from field: saas.accounts.v1.CatalogueRelease declared_release = 5; - */ - value: CatalogueRelease; - case: "declaredRelease"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap declared_release_gap = 6; - */ - value: CatalogueGap; - case: "declaredReleaseGap"; - } | { case: undefined; value?: undefined }; + desired?: CatalogueDesired; /** - * The OCI image manifest digest the entry was published at. - * - * @generated from oneof saas.accounts.v1.CatalogueEntry.build_digest_value + * @generated from field: saas.accounts.v1.CatalogueAuthorized authorized = 6; */ - buildDigestValue: { - /** - * @generated from field: string build_digest = 7; - */ - value: string; - case: "buildDigest"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap build_digest_gap = 8; - */ - value: CatalogueGap; - case: "buildDigestGap"; - } | { case: undefined; value?: undefined }; + authorized?: CatalogueAuthorized; /** - * The presence generation this host applied. - * - * @generated from oneof saas.accounts.v1.CatalogueEntry.generation_value + * @generated from field: saas.accounts.v1.CatalogueApplied applied = 7; */ - generationValue: { - /** - * @generated from field: int64 generation = 9; - */ - value: bigint; - case: "generation"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap generation_gap = 10; - */ - value: CatalogueGap; - case: "generationGap"; - } | { case: undefined; value?: undefined }; + applied?: CatalogueApplied; /** - * @generated from oneof saas.accounts.v1.CatalogueEntry.running_value + * @generated from field: saas.accounts.v1.CatalogueObserved observed = 8; */ - runningValue: { - /** - * @generated from field: saas.accounts.v1.CatalogueRunning running = 11; - */ - value: CatalogueRunning; - case: "running"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap running_gap = 12; - */ - value: CatalogueGap; - case: "runningGap"; - } | { case: undefined; value?: undefined }; + observed?: CatalogueObserved; + + /** + * @generated from field: saas.accounts.v1.CatalogueWithdrawing withdrawing = 9; + */ + withdrawing?: CatalogueWithdrawing; + + /** + * @generated from field: saas.accounts.v1.CatalogueRetired retired = 10; + */ + retired?: CatalogueRetired; /** * Lines of code, backend and frontend, vendored paths excluded. The value @@ -1124,25 +1372,25 @@ export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { */ buildSizeValue: { /** - * @generated from field: saas.accounts.v1.CatalogueGap build_size_gap = 13; + * @generated from field: saas.accounts.v1.CatalogueGap build_size_gap = 11; */ value: CatalogueGap; case: "buildSizeGap"; } | { case: undefined; value?: undefined }; /** - * The solution's registration, with deployment topology withheld: the + * The solution's self-registration, with deployment topology withheld: the * frontend manifest, backend upstream and service alias are always empty * here, as on every browser-facing projection of the registry. Absent for a * module, which does not self-register, and for a solution known only from * its installations. * - * @generated from field: optional saas.accounts.v1.SolutionRegistration registration = 14; + * @generated from field: optional saas.accounts.v1.SolutionRegistration registration = 12; */ registration?: SolutionRegistration; /** - * @generated from field: repeated saas.accounts.v1.CatalogueInstallation installations = 15; + * @generated from field: repeated saas.accounts.v1.CatalogueInstallation installations = 13; */ installations: CatalogueInstallation[]; }; @@ -1152,7 +1400,7 @@ export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { * Use `create(CatalogueEntrySchema)` to create a new message. */ export const CatalogueEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 37); + messageDesc(file_saas_accounts_v1_platform_admin, 44); /** * @generated from message saas.accounts.v1.ListPlatformCatalogueRequest @@ -1171,7 +1419,7 @@ export type ListPlatformCatalogueRequest = Message<"saas.accounts.v1.ListPlatfor * Use `create(ListPlatformCatalogueRequestSchema)` to create a new message. */ export const ListPlatformCatalogueRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 38); + messageDesc(file_saas_accounts_v1_platform_admin, 45); /** * @generated from message saas.accounts.v1.ListPlatformCatalogueResponse @@ -1197,7 +1445,7 @@ export type ListPlatformCatalogueResponse = Message<"saas.accounts.v1.ListPlatfo * Use `create(ListPlatformCatalogueResponseSchema)` to create a new message. */ export const ListPlatformCatalogueResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 39); + messageDesc(file_saas_accounts_v1_platform_admin, 46); /** * CatalogueEntryKind distinguishes a composed module from a solution. @@ -1273,37 +1521,45 @@ export const CatalogueGapReasonSchema: GenEnum = /*@__PURE__ enumDesc(file_saas_accounts_v1_platform_admin, 1); /** - * CatalogueRunningVerdict compares the observed execution with the declared - * one. It exists only when both are known; anything less is a CatalogueGap. + * CatalogueObservedVerdict judges what runs against what is authorized — + * never against what is declared, because only authorization admits execution. * - * @generated from enum saas.accounts.v1.CatalogueRunningVerdict + * @generated from enum saas.accounts.v1.CatalogueObservedVerdict */ -export enum CatalogueRunningVerdict { +export enum CatalogueObservedVerdict { /** - * @generated from enum value: CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; */ UNSPECIFIED = 0, /** - * The observed image digest and incarnation are the declared ones. + * The observed image digest and incarnation are the approved execution. * - * @generated from enum value: CATALOGUE_RUNNING_VERDICT_MATCHES = 1; + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; */ - MATCHES = 1, + RUNNING_AUTHORIZED = 1, /** - * Something other than the declared execution is running — what cluster + * Something other than the approved execution is running: what cluster * admission refuses. * - * @generated from enum value: CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS = 2; + */ + RUNNING_DIFFERS = 2, + + /** + * Something is running with no current authorization at all: what the + * retirement sweep exists to stop. + * + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED = 3; */ - DIFFERS = 2, + RUNNING_UNAUTHORIZED = 3, } /** - * Describes the enum saas.accounts.v1.CatalogueRunningVerdict. + * Describes the enum saas.accounts.v1.CatalogueObservedVerdict. */ -export const CatalogueRunningVerdictSchema: GenEnum = /*@__PURE__*/ +export const CatalogueObservedVerdictSchema: GenEnum = /*@__PURE__*/ enumDesc(file_saas_accounts_v1_platform_admin, 2); /** diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts index 4f24d7dd4..d13b817ee 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts @@ -4,28 +4,30 @@ import { CatalogueEntryKind, CatalogueEntrySchema, CatalogueGapReason, - CatalogueRunningVerdict, + CatalogueObservedVerdict, } from "@/gen/saas/accounts/v1/platform_admin_pb"; import { SolutionRegistrationSchema, SolutionRegistrationStatus, } from "@/gen/saas/accounts/v1/solution_registry_pb"; import { + authorizationView, buildSizeView, + CATALOGUE_STATES, catalogueGaps, - declaredReleaseView, + catalogueStates, gapLabel, - generationView, + observedVerdictView, registrationView, - runningView, } from "./facts"; +const notRecorded = (detail: string) => ({ + reason: CatalogueGapReason.NOT_RECORDED, + detail, +}); const notObserved = { - case: "runningGap" as const, - value: { - reason: CatalogueGapReason.NOT_OBSERVED, - detail: "No cluster state.", - }, + reason: CatalogueGapReason.NOT_OBSERVED, + detail: "No cluster state.", }; function entry( @@ -34,125 +36,207 @@ function entry( return create(CatalogueEntrySchema, { kind: CatalogueEntryKind.SOLUTION, name: "example", - runningValue: notObserved, ...init, }); } -describe("runningView", () => { - const declared = { imageDigest: "sha256:aaa", buildIncarnation: BigInt(3) }; +function observedWith(verdict: CatalogueObservedVerdict) { + return { + observedExecutionValue: { + case: "observedExecution" as const, + value: { imageDigest: "sha256:bbb", buildIncarnation: BigInt(3) }, + }, + verdictValue: { case: "verdict" as const, value: verdict }, + }; +} - it("states a match only when the server judged both executions equal", () => { - const view = runningView( - entry({ - runningValue: { - case: "running", - value: { - declared, - observed: declared, - verdict: CatalogueRunningVerdict.MATCHES, - }, - }, - }), - ); - expect(view).toMatchObject({ - kind: "value", - text: "Matches declared", - tone: "success", - }); +describe("the state model", () => { + it("is the registry's six states, in order", () => { + expect([...CATALOGUE_STATES]).toEqual([ + "Desired", + "Authorized", + "Applied", + "Observed", + "Withdrawing", + "Retired", + ]); }); - it("flags a mismatch with both executions, since admission would refuse it", () => { - const view = runningView( - entry({ - runningValue: { - case: "running", - value: { - declared, - observed: { - imageDigest: "sha256:bbb", - buildIncarnation: BigInt(3), - }, - verdict: CatalogueRunningVerdict.DIFFERS, - }, + it("reads every state of an entry the host holds nothing for as gaps, never as values", () => { + const e = entry({ + authorized: { + authorizationValue: { + case: "authorizationGap", + value: notRecorded("No approval record."), }, - }), - ); - expect(view).toMatchObject({ - kind: "value", - text: "Differs from declared", - tone: "danger", + }, + applied: { + appliedRevisionValue: { + case: "appliedRevisionGap", + value: notRecorded("No applied generation."), + }, + }, + observed: { verdictValue: { case: "verdictGap", value: notObserved } }, + withdrawing: { + withdrawalStateValue: { + case: "withdrawalStateGap", + value: notRecorded("No withdrawal record."), + }, + }, + retired: { + retirementStateValue: { + case: "retirementStateGap", + value: notObserved, + }, + }, }); - expect(view.kind === "value" && view.detail).toContain("sha256:aaa"); - expect(view.kind === "value" && view.detail).toContain("sha256:bbb"); - }); - - it("says an unobserved deployment is not observed — never a match", () => { - expect(runningView(entry())).toEqual({ + const states = catalogueStates(e); + for (const state of [ + "Authorized", + "Applied", + "Withdrawing", + "Retired", + ] as const) { + for (const item of states[state]) + expect(item.view.kind, `${state} ${item.label}`).toBe("gap"); + } + expect(observedVerdictView(e)).toEqual({ kind: "gap", label: "Not observed", detail: "No cluster state.", }); + expect(gapLabel(CatalogueGapReason.NOT_REPORTED)).toBe("Not reported"); + }); + + it("does not read an unset oneof as a value", () => { + const states = catalogueStates(entry()); + for (const state of CATALOGUE_STATES) { + for (const item of states[state]) { + if (item.label === "Registration") continue; + expect(item.view, `${state} ${item.label}`).toMatchObject({ + kind: "gap", + label: "Unknown", + }); + } + } }); - it("does not read an unset verdict or an unset oneof as a value", () => { + it("renders a revision once the server sends one", () => { + const e = entry({ + applied: { + appliedRevisionValue: { case: "appliedRevision", value: BigInt(7) }, + }, + desired: { + declaredReleaseValue: { + case: "declaredRelease", + value: { publisher: "example", name: "billing", version: "1.2.0" }, + }, + }, + }); + const states = catalogueStates(e); + expect(states.Applied[0].view).toEqual({ kind: "value", text: "7" }); + expect(states.Desired[1].view).toEqual({ + kind: "value", + text: "example/billing@1.2.0", + }); + }); +}); + +describe("authorization", () => { + it("tells an approval, a known absence of one, and the host not knowing apart", () => { expect( - runningView( + authorizationView( entry({ - runningValue: { - case: "running", - value: { declared, observed: declared }, + authorized: { + authorizationValue: { + case: "authorization", + value: { + authorizedRevision: BigInt(4), + approvedExecution: { + imageDigest: "sha256:aaa", + buildIncarnation: BigInt(3), + }, + }, + }, }, }), - ).kind, - ).toBe("gap"); + ), + ).toMatchObject({ kind: "value", text: "Revision 4", tone: "success" }); expect( - runningView(entry({ runningValue: { case: undefined } })), - ).toMatchObject({ + authorizationView( + entry({ + authorized: { + authorizationValue: { + case: "notAuthorized", + value: { detail: "approval withdrawn" }, + }, + }, + }), + ), + ).toMatchObject({ kind: "value", text: "Not authorized" }); + expect( + authorizationView( + entry({ + authorized: { + authorizationValue: { + case: "authorizationGap", + value: notRecorded("No approval record."), + }, + }, + }), + ), + ).toEqual({ kind: "gap", - label: "Unknown", + label: "Not recorded", + detail: "No approval record.", }); }); }); -describe("presence-borne facts", () => { - it("render the server's gap reason, never an empty value or zero", () => { - const gap = { - reason: CatalogueGapReason.NOT_RECORDED, - detail: "No presence document.", - }; - const e = entry({ - generationValue: { case: "generationGap", value: gap }, - buildSizeValue: { case: "buildSizeGap", value: gap }, - declaredReleaseValue: { case: "declaredReleaseGap", value: gap }, +describe("observedVerdictView", () => { + it("states running authorized only when the server judged it so", () => { + expect( + observedVerdictView( + entry({ + observed: observedWith(CatalogueObservedVerdict.RUNNING_AUTHORIZED), + }), + ), + ).toMatchObject({ + kind: "value", + text: "Running authorized", + tone: "success", }); - for (const view of [ - generationView(e), - buildSizeView(e), - declaredReleaseView(e), - ]) { - expect(view).toEqual({ - kind: "gap", - label: "Not recorded", - detail: "No presence document.", - }); - } - expect(gapLabel(CatalogueGapReason.NOT_REPORTED)).toBe("Not reported"); }); - it("render a value once the server sends one", () => { - const e = entry({ - generationValue: { case: "generation", value: BigInt(7) }, - declaredReleaseValue: { - case: "declaredRelease", - value: { publisher: "example", name: "billing", version: "1.2.0" }, - }, - }); - expect(generationView(e)).toEqual({ kind: "value", text: "7" }); - expect(declaredReleaseView(e)).toEqual({ - kind: "value", - text: "example/billing@1.2.0", + it("flags an execution that differs from the approved one", () => { + const view = observedVerdictView( + entry({ + observed: observedWith(CatalogueObservedVerdict.RUNNING_DIFFERS), + }), + ); + expect(view).toMatchObject({ + text: "Differs from authorized", + tone: "danger", }); + expect(view.kind === "value" && view.detail).toContain("sha256:bbb"); + }); + + it("flags an execution nothing currently authorizes — the dangerous row", () => { + expect( + observedVerdictView( + entry({ + observed: observedWith(CatalogueObservedVerdict.RUNNING_UNAUTHORIZED), + }), + ), + ).toMatchObject({ text: "Running without authorization", tone: "danger" }); + }); + + it("does not read an unspecified verdict as a value", () => { + expect( + observedVerdictView( + entry({ observed: observedWith(CatalogueObservedVerdict.UNSPECIFIED) }), + ).kind, + ).toBe("gap"); }); }); @@ -160,7 +244,9 @@ describe("registrationView", () => { it("tells a composed module, a solution known only by installations, and each status apart", () => { expect( registrationView(entry({ kind: CatalogueEntryKind.MODULE })), - ).toMatchObject({ text: "Composed" }); + ).toMatchObject({ + text: "Composed", + }); expect(registrationView(entry())).toMatchObject({ kind: "gap", label: "Not registered", @@ -178,10 +264,15 @@ describe("registrationView", () => { }); describe("catalogueGaps", () => { - it("lists each unavailable fact once, however many entries share it", () => { - const gap = { reason: CatalogueGapReason.NOT_RECORDED, detail: "core#708" }; + it("lists each unavailable fact once, named by its state, however many entries share it", () => { const shared = { - buildSizeValue: { case: "buildSizeGap" as const, value: gap }, + buildSizeValue: { + case: "buildSizeGap" as const, + value: notRecorded("core#708"), + }, + observed: { + verdictValue: { case: "verdictGap" as const, value: notObserved }, + }, }; const gaps = catalogueGaps([ entry(shared), @@ -190,6 +281,7 @@ describe("catalogueGaps", () => { expect(gaps.filter((g) => g.fact === "Build size")).toEqual([ { fact: "Build size", label: "Not recorded", detail: "core#708" }, ]); - expect(gaps.filter((g) => g.fact === "Running")).toHaveLength(1); + expect(gaps.filter((g) => g.fact === "Observed · Running")).toHaveLength(1); + expect(buildSizeView(entry(shared)).kind).toBe("gap"); }); }); diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts index 321aafba2..5eb0ebd1c 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts @@ -1,3 +1,5 @@ +import type { Timestamp } from "@bufbuild/protobuf/wkt"; +import { timestampDate } from "@bufbuild/protobuf/wkt"; import type { ComponentProps } from "react"; import { type CatalogueEntry, @@ -6,8 +8,8 @@ import { type CatalogueGap, CatalogueGapReason, type CatalogueInstallation, + CatalogueObservedVerdict, type CatalogueRelease, - CatalogueRunningVerdict, } from "@/gen/saas/accounts/v1/platform_admin_pb"; import { SolutionRegistrationStatus } from "@/gen/saas/accounts/v1/solution_registry_pb"; import type { Badge } from "@/shared/ui"; @@ -15,7 +17,7 @@ import type { Badge } from "@/shared/ui"; type StatusTone = NonNullable["tone"]>; /** - * One Catalogue cell. A fact the host holds is a `value`; a fact it does not is + * One Catalogue fact. A fact the host holds is a `value`; a fact it does not is * a `gap` with the server's reason and detail — never an empty cell, a zero, or * a match, because each of those is a claim the host cannot make. */ @@ -23,6 +25,27 @@ export type FactView = | { kind: "value"; text: string; tone?: StatusTone; detail?: string } | { kind: "gap"; label: string; detail: string }; +/** A labelled fact inside one state of the registry's state model. */ +export interface StateFact { + label: string; + view: FactView; +} + +/** + * The registry's state model, in order. An entry shows each state separately, + * so desired-but-not-authorized and observed-but-no-longer-authorized read as + * exactly that rather than collapsing into "declared" and "running". + */ +export const CATALOGUE_STATES = [ + "Desired", + "Authorized", + "Applied", + "Observed", + "Withdrawing", + "Retired", +] as const; +export type CatalogueState = (typeof CATALOGUE_STATES)[number]; + export function gapLabel(reason: CatalogueGapReason): string { switch (reason) { case CatalogueGapReason.NOT_RECORDED: @@ -48,6 +71,19 @@ const UNSET: FactView = { detail: "The server sent neither a value nor a reason for this fact.", }; +type Oneof = { case: string | undefined; value?: unknown }; + +// fact reads a value-or-gap oneof: the case ending in "Gap" is the gap, any +// other set case is a value, rendered by format. +function fact( + oneof: V | undefined, + format: (value: Exclude) => FactView, +): FactView { + if (!oneof || oneof.case === undefined) return UNSET; + if (oneof.case.endsWith("Gap")) return gapView(oneof.value as CatalogueGap); + return format(oneof as Exclude); +} + export function releaseText(release: CatalogueRelease): string { return `${release.publisher}/${release.name}@${release.version}`; } @@ -71,89 +107,17 @@ export function kindLabel(kind: CatalogueEntryKind): string { } export function publisherView(entry: CatalogueEntry): FactView { - const v = entry.publisherValue; - if (v.case === "publisher") return { kind: "value", text: v.value }; - if (v.case === "publisherGap") return gapView(v.value); - return UNSET; -} - -export function declaredReleaseView(entry: CatalogueEntry): FactView { - const v = entry.declaredReleaseValue; - if (v.case === "declaredRelease") - return { kind: "value", text: releaseText(v.value) }; - if (v.case === "declaredReleaseGap") return gapView(v.value); - return UNSET; -} - -export function buildDigestView(entry: CatalogueEntry): FactView { - const v = entry.buildDigestValue; - if (v.case === "buildDigest") return { kind: "value", text: v.value }; - if (v.case === "buildDigestGap") return gapView(v.value); - return UNSET; -} - -export function generationView(entry: CatalogueEntry): FactView { - const v = entry.generationValue; - if (v.case === "generation") - return { kind: "value", text: v.value.toString() }; - if (v.case === "generationGap") return gapView(v.value); - return UNSET; + return fact(entry.publisherValue, (v) => ({ + kind: "value", + text: v.value as string, + })); } /** - * What runs beside what was declared. A mismatch is the row an operator is - * looking for — it is what cluster admission would refuse — so it carries the - * danger tone and both executions. + * The self-registration a solution's runtime heartbeats — the only record of a + * solution's intent this host holds today, so it sits under Desired. A solution + * named only by installations is desired by nobody this host can see. */ -export function runningView(entry: CatalogueEntry): FactView { - const v = entry.runningValue; - if (v.case === "runningGap") return gapView(v.value); - if (v.case !== "running") return UNSET; - const detail = `Declared ${executionText(v.value.declared)}; observed ${executionText(v.value.observed)}.`; - switch (v.value.verdict) { - case CatalogueRunningVerdict.MATCHES: - return { - kind: "value", - text: "Matches declared", - tone: "success", - detail, - }; - case CatalogueRunningVerdict.DIFFERS: - return { - kind: "value", - text: "Differs from declared", - tone: "danger", - detail, - }; - default: - return { ...UNSET, detail }; - } -} - -export function buildSizeView(entry: CatalogueEntry): FactView { - const v = entry.buildSizeValue; - if (v.case === "buildSizeGap") return gapView(v.value); - return UNSET; -} - -export function agentReleaseView( - installation: CatalogueInstallation, -): FactView { - const v = installation.agentReleaseValue; - if (v.case === "agentRelease") - return { kind: "value", text: releaseText(v.value) }; - if (v.case === "agentReleaseGap") return gapView(v.value); - return UNSET; -} - -export function installationRevisionView( - installation: CatalogueInstallation, -): FactView { - const v = installation.revisionValue; - if (v.case === "revisionGap") return gapView(v.value); - return UNSET; -} - export function registrationView(entry: CatalogueEntry): FactView { if (entry.kind === CatalogueEntryKind.MODULE) { return { kind: "value", text: "Composed", tone: "neutral" }; @@ -198,9 +162,163 @@ export function registrationView(entry: CatalogueEntry): FactView { } } +/** + * What the platform has approved. "Not authorized" is a statement the approval + * record makes; a gap is the host being unable to tell — never the same thing. + */ +export function authorizationView(entry: CatalogueEntry): FactView { + const v = entry.authorized?.authorizationValue; + if (v?.case === "authorization") { + return { + kind: "value", + text: `Revision ${v.value.authorizedRevision}`, + tone: "success", + detail: `Approved ${executionText(v.value.approvedExecution)}.`, + }; + } + if (v?.case === "notAuthorized") { + return { + kind: "value", + text: "Not authorized", + tone: "warning", + detail: v.value.detail, + }; + } + return fact(v, () => UNSET); +} + +/** + * What runs, judged against what is authorized. Both failures carry the danger + * tone: a different execution is what admission refuses, and an execution with + * no current authorization is what the retirement sweep exists to stop. + */ +export function observedVerdictView(entry: CatalogueEntry): FactView { + const observed = entry.observed; + const v = observed?.verdictValue; + if (v?.case !== "verdict") return fact(v, () => UNSET); + const running = + observed?.observedExecutionValue.case === "observedExecution" + ? observed.observedExecutionValue.value + : undefined; + const detail = `Observed ${executionText(running)}.`; + switch (v.value) { + case CatalogueObservedVerdict.RUNNING_AUTHORIZED: + return { + kind: "value", + text: "Running authorized", + tone: "success", + detail, + }; + case CatalogueObservedVerdict.RUNNING_DIFFERS: + return { + kind: "value", + text: "Differs from authorized", + tone: "danger", + detail, + }; + case CatalogueObservedVerdict.RUNNING_UNAUTHORIZED: + return { + kind: "value", + text: "Running without authorization", + tone: "danger", + detail, + }; + default: + return { ...UNSET, detail }; + } +} + +function revisionView(oneof: Oneof | undefined): FactView { + return fact(oneof, (v) => ({ + kind: "value", + text: (v.value as bigint).toString(), + })); +} + +/** Each state's facts, labelled as the registry's state model names them. */ +export function catalogueStates( + entry: CatalogueEntry, +): Record { + return { + Desired: [ + { label: "Registration", view: registrationView(entry) }, + { + label: "Declared release", + view: fact(entry.desired?.declaredReleaseValue, (v) => ({ + kind: "value", + text: releaseText(v.value as CatalogueRelease), + })), + }, + { + label: "Declared revision", + view: revisionView(entry.desired?.declaredRevisionValue), + }, + ], + Authorized: [{ label: "Approval", view: authorizationView(entry) }], + Applied: [ + { + label: "Applied revision", + view: revisionView(entry.applied?.appliedRevisionValue), + }, + ], + Observed: [ + { label: "Running", view: observedVerdictView(entry) }, + { + label: "Observed revision", + view: revisionView(entry.observed?.observedRevisionValue), + }, + { + label: "Observed at", + view: fact(entry.observed?.observationFreshnessValue, (v) => ({ + kind: "value", + text: timestampDate(v.value as Timestamp).toISOString(), + })), + }, + ], + Withdrawing: [ + { + label: "Withdrawal", + view: fact(entry.withdrawing?.withdrawalStateValue, () => UNSET), + }, + { + label: "Credential revocation", + view: fact( + entry.withdrawing?.credentialRevocationStateValue, + () => UNSET, + ), + }, + ], + Retired: [ + { + label: "Retirement", + view: fact(entry.retired?.retirementStateValue, () => UNSET), + }, + ], + }; +} + +export function buildSizeView(entry: CatalogueEntry): FactView { + return fact(entry.buildSizeValue, () => UNSET); +} + +export function agentReleaseView( + installation: CatalogueInstallation, +): FactView { + return fact(installation.agentReleaseValue, (v) => ({ + kind: "value", + text: releaseText(v.value as CatalogueRelease), + })); +} + +export function installationRevisionView( + installation: CatalogueInstallation, +): FactView { + return fact(installation.revisionValue, () => UNSET); +} + /** * The facts this host cannot state, deduplicated, so the page can say once — - * under the table — what each "Not recorded" means and what would supply it. + * under the table — what each gap means and what would supply it. */ export function catalogueGaps( entries: CatalogueEntry[], @@ -209,18 +327,20 @@ export function catalogueGaps( string, { fact: string; label: string; detail: string } >(); - const note = (fact: string, view: FactView) => { + const note = (factName: string, view: FactView) => { if (view.kind !== "gap") return; - const key = `${fact}\u0000${view.label}\u0000${view.detail}`; + const key = `${factName}\u0000${view.label}\u0000${view.detail}`; if (!seen.has(key)) - seen.set(key, { fact, label: view.label, detail: view.detail }); + seen.set(key, { fact: factName, label: view.label, detail: view.detail }); }; for (const entry of entries) { note("Publisher", publisherView(entry)); - note("Declared release", declaredReleaseView(entry)); - note("Build", buildDigestView(entry)); - note("Generation", generationView(entry)); - note("Running", runningView(entry)); + const states = catalogueStates(entry); + for (const state of CATALOGUE_STATES) { + for (const item of states[state]) { + note(`${state} · ${item.label}`, item.view); + } + } note("Build size", buildSizeView(entry)); for (const installation of entry.installations) { note("Agent release", agentReleaseView(installation)); diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx index 5ab88deab..c46a77229 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx @@ -1,5 +1,5 @@ import { cleanup, fireEvent, screen, within } from "@testing-library/react"; -import { HttpResponse, http } from "msw"; +import { HttpResponse, http, type JsonBodyType } from "msw"; import { afterEach, describe, expect, it } from "vitest"; import { renderInApp, rpc } from "@/test/container"; import { server } from "@/test/setup"; @@ -11,13 +11,56 @@ const notRecorded = (detail: string) => ({ reason: "CATALOGUE_GAP_REASON_NOT_RECORDED", detail, }); -const presenceGaps = { - declaredReleaseGap: notRecorded("No applied presence document."), - buildDigestGap: notRecorded("No applied presence document."), - generationGap: notRecorded("No applied presence document."), +const notObserved = (detail: string) => ({ + reason: "CATALOGUE_GAP_REASON_NOT_OBSERVED", + detail, +}); +const approved = { imageDigest: "sha256:aaa", buildIncarnation: "3" }; + +// What this host has today for every entry: each state's facts as gaps. +const unknownStates = { + desired: { + declaredRevisionGap: notRecorded("No applied presence document."), + declaredReleaseGap: notRecorded("No applied presence document."), + }, + authorized: { authorizationGap: notRecorded("No approval record.") }, + applied: { appliedRevisionGap: notRecorded("No applied generation.") }, + observed: { + observedRevisionGap: notObserved("No cluster state."), + observationFreshnessGap: notObserved("No cluster state."), + observedExecutionGap: notObserved("No cluster state."), + verdictGap: notObserved("No cluster state."), + }, + withdrawing: { + withdrawalStateGap: notRecorded("No withdrawal record."), + credentialRevocationStateGap: notRecorded("No revocation record."), + }, + retired: { retirementStateGap: notObserved("No retirement controller.") }, buildSizeGap: notRecorded("Build size is codefly-dev/core#708."), }; -const declared = { imageDigest: "sha256:aaa", buildIncarnation: "3" }; + +function observedRunning(verdict: string, imageDigest: string) { + return { + observedRevisionGap: notObserved("No cluster state."), + observationFreshnessGap: notObserved("No cluster state."), + observedExecution: { imageDigest, buildIncarnation: "3" }, + verdict, + }; +} + +function solution(name: string, extra: Record) { + return { + kind: "CATALOGUE_ENTRY_KIND_SOLUTION", + name, + publisher: `solution:${name}`, + ...unknownStates, + registration: { + solutionId: name, + status: "SOLUTION_REGISTRATION_STATUS_ACTIVE", + }, + ...extra, + }; +} const catalogue = { registryRevision: "12", @@ -28,26 +71,16 @@ const catalogue = { publisherGap: notRecorded( "A composed module is known by its prefix alone.", ), - ...presenceGaps, - runningGap: { - reason: "CATALOGUE_GAP_REASON_NOT_OBSERVED", - detail: "No cluster state.", - }, + ...unknownStates, }, - { - kind: "CATALOGUE_ENTRY_KIND_SOLUTION", - name: "drifted-solution", - publisher: "solution:drifted-solution", - ...presenceGaps, - running: { - declared, - observed: { imageDigest: "sha256:bbb", buildIncarnation: "3" }, - verdict: "CATALOGUE_RUNNING_VERDICT_DIFFERS", - }, - registration: { - solutionId: "drifted-solution", - status: "SOLUTION_REGISTRATION_STATUS_ACTIVE", + solution("drifted-solution", { + authorized: { + authorization: { authorizedRevision: "4", approvedExecution: approved }, }, + observed: observedRunning( + "CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS", + "sha256:bbb", + ), installations: [ { installation: { @@ -61,7 +94,7 @@ const catalogue = { name: "drifted", version: "1.2.0", }, - revisionGap: notRecorded("Not pinned to a presence revision."), + revisionGap: notRecorded("No installation revision."), exposedTeams: [ { grant: { id: "g1", scopePath: "root" }, @@ -71,76 +104,111 @@ const catalogue = { ], }, ], - }, - { - kind: "CATALOGUE_ENTRY_KIND_SOLUTION", - name: "steady-solution", - publisher: "solution:steady-solution", - ...presenceGaps, - running: { - declared, - observed: declared, - verdict: "CATALOGUE_RUNNING_VERDICT_MATCHES", - }, - registration: { - solutionId: "steady-solution", - status: "SOLUTION_REGISTRATION_STATUS_ACTIVE", + }), + solution("rogue-solution", { + authorized: { notAuthorized: { detail: "Approval withdrawn." } }, + observed: observedRunning( + "CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED", + "sha256:aaa", + ), + }), + solution("steady-solution", { + authorized: { + authorization: { authorizedRevision: "4", approvedExecution: approved }, }, - }, + observed: observedRunning( + "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", + "sha256:aaa", + ), + }), ], }; -function rowOf(name: string): HTMLElement { +function stateOf(name: string, state: string): HTMLElement { const row = screen.getByText(name).closest("tr"); - if (!row) throw new Error(`no row for ${name}`); - return row; + const cell = row?.querySelector(`[aria-label="${state}"]`); + if (!cell) throw new Error(`no ${state} cell for ${name}`); + return cell; +} + +function serve(body: JsonBodyType) { + server.use( + http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => + HttpResponse.json(body), + ), + ); } describe("PlatformCataloguePage admin container", () => { - it("shows a match, a mismatch and an unobserved entry for what they are", async () => { - server.use( - http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => - HttpResponse.json(catalogue), - ), - ); + it("shows each entry in the six states, one column each", async () => { + serve(catalogue); + renderInApp(); + await screen.findByText("drifted-solution"); + for (const state of [ + "Desired", + "Authorized", + "Applied", + "Observed", + "Withdrawing", + "Retired", + ]) { + expect(screen.getByRole("columnheader", { name: state })).toBeTruthy(); + } + }); + + it("tells running authorized, differing, running without authorization and unobserved apart", async () => { + serve(catalogue); renderInApp(); await screen.findByText("drifted-solution"); expect( - within(rowOf("drifted-solution")).getByText("Differs from declared"), + within(stateOf("steady-solution", "Observed")).getByText( + "Running authorized", + ), ).toBeTruthy(); expect( - within(rowOf("steady-solution")).getByText("Matches declared"), + within(stateOf("drifted-solution", "Observed")).getByText( + "Differs from authorized", + ), ).toBeTruthy(); - const moduleRow = rowOf("billing-module"); - expect(within(moduleRow).getByText("Not observed")).toBeTruthy(); - expect(within(moduleRow).queryByText("Matches declared")).toBeNull(); - expect(within(moduleRow).getByText("Module")).toBeTruthy(); - - // Build size is unavailable on every row until the presence document - // carries it, and the page says what would supply it. - for (const name of [ - "billing-module", - "drifted-solution", - "steady-solution", - ]) { - const sizeCell = within(rowOf(name)) - .getAllByText("Not recorded") - .find((cell) => cell.getAttribute("title")?.includes("core#708")); - expect(sizeCell).toBeTruthy(); - } + expect( + within(stateOf("rogue-solution", "Observed")).getByText( + "Running without authorization", + ), + ).toBeTruthy(); + expect( + within(stateOf("rogue-solution", "Authorized")).getByText( + "Not authorized", + ), + ).toBeTruthy(); + + const moduleObserved = stateOf("billing-module", "Observed"); + expect( + within(moduleObserved).getAllByText("Not observed").length, + ).toBeGreaterThan(0); + expect(within(moduleObserved).queryByText("Running authorized")).toBeNull(); + expect( + within(stateOf("billing-module", "Authorized")).getByText("Not recorded"), + ).toBeTruthy(); + expect( + within(stateOf("billing-module", "Retired")).getByText("Not observed"), + ).toBeTruthy(); + }); + + it("says what the host cannot state, and what would supply it", async () => { + serve(catalogue); + renderInApp(); + await screen.findByText("drifted-solution"); expect(screen.getByText("Not known to this host")).toBeTruthy(); expect( screen.getByText("Build size is codefly-dev/core#708."), ).toBeTruthy(); + expect(screen.getByText("No approval record.")).toBeTruthy(); + expect(screen.getByText("No withdrawal record.")).toBeTruthy(); }); it("opens an entry's installations: organization, agent release, exposed teams", async () => { - server.use( - http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => - HttpResponse.json(catalogue), - ), - ); + serve(catalogue); renderInApp(); fireEvent.click( await screen.findByRole("button", { @@ -178,11 +246,7 @@ describe("PlatformCataloguePage admin container", () => { }); it("says an empty platform is empty", async () => { - server.use( - http.post(rpc("PlatformAdminService", "ListPlatformCatalogue"), () => - HttpResponse.json({ entries: [], registryRevision: "0" }), - ), - ); + serve({ entries: [], registryRevision: "0" }); renderInApp(); expect(await screen.findByText(/Nothing is deployed/)).toBeTruthy(); }); diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx index 4a10a4608..b0adc3143 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx @@ -34,21 +34,20 @@ import { } from "@/shared/ui"; import { agentReleaseView, - buildDigestView, buildSizeView, + CATALOGUE_STATES, catalogueGaps, - declaredReleaseView, + catalogueStates, type FactView, - generationView, installationRevisionView, kindLabel, publisherView, - registrationView, - runningView, + type StateFact, } from "../model/facts"; import { usePlatformCatalogue } from "../service/queries"; -const COLUMNS = 10; +// The expander, the entry, one column per state, installations and build size. +const COLUMNS = 2 + CATALOGUE_STATES.length + 2; /** * One cell. A gap reads as its reason in muted italics with the detail on @@ -80,6 +79,26 @@ export function Fact({ view }: { view: FactView }) { ); } +/** + * One state's facts, stacked and labelled. A state the host holds nothing for + * reads as its gaps, so a row shows where an entry stands in the progression + * and where this host cannot tell. + */ +function StateFacts({ state, facts }: { state: string; facts: StateFact[] }) { + return ( +

+ {facts.map((item) => ( +
+
{item.label}
+
+ +
+
+ ))} +
+ ); +} + function entryKey(entry: CatalogueEntry): string { return `${entry.kind}:${entry.name}`; } @@ -162,13 +181,10 @@ export function PlatformCataloguePage() { - Name - Registration - Publisher - Declared release - Build - Generation - Running + Entry + {CATALOGUE_STATES.map((state) => ( + {state} + ))} Installations Build size @@ -204,25 +220,18 @@ export function PlatformCataloguePage() { {kindLabel(entry.kind)} +
+ +
- - - - - - - - - - - - - - - - - - + {CATALOGUE_STATES.map((state) => ( + + + + ))} {entry.installations.length} diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index 3fc32b031..c9840d7a5 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCK+AQoQQ2F0YWxvZ3VlUnVubmluZxI2CghkZWNsYXJlZBgBIAEoCzIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRXhlY3V0aW9uEjYKCG9ic2VydmVkGAIgASgLMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFeGVjdXRpb24SOgoHdmVyZGljdBgDIAEoDjIpLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3Qi+AIKFUNhdGFsb2d1ZUluc3RhbGxhdGlvbhI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhIQCghvcmdfbmFtZRgCIAEoCRI7Cg1hZ2VudF9yZWxlYXNlGAMgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAASOwoRYWdlbnRfcmVsZWFzZV9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjYKDHJldmlzaW9uX2dhcBgFIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESPAoNZXhwb3NlZF90ZWFtcxgGIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIvwGCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI+ChBkZWNsYXJlZF9yZWxlYXNlGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZWxlYXNlSAESPgoUZGVjbGFyZWRfcmVsZWFzZV9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEhYKDGJ1aWxkX2RpZ2VzdBgHIAEoCUgCEjoKEGJ1aWxkX2RpZ2VzdF9nYXAYCCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEhQKCmdlbmVyYXRpb24YCSABKANIAxI4Cg5nZW5lcmF0aW9uX2dhcBgKIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAMSNQoHcnVubmluZxgLIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUnVubmluZ0gEEjUKC3J1bm5pbmdfZ2FwGAwgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIBBI4Cg5idWlsZF9zaXplX2dhcBgNIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAUSQQoMcmVnaXN0cmF0aW9uGA4gASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgGiAEBEj4KDWluc3RhbGxhdGlvbnMYDyADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZUIUChJidWlsZF9kaWdlc3RfdmFsdWVCEgoQZ2VuZXJhdGlvbl92YWx1ZUIPCg1ydW5uaW5nX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCJtCh1MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZRIxCgdlbnRyaWVzGAEgAygLMiAuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeRIZChFyZWdpc3RyeV9yZXZpc2lvbhgCIAEoAyp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyqSAQoXQ2F0YWxvZ3VlUnVubmluZ1ZlcmRpY3QSKQolQ0FUQUxPR1VFX1JVTk5JTkdfVkVSRElDVF9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfTUFUQ0hFUxABEiUKIUNBVEFMT0dVRV9SVU5OSU5HX1ZFUkRJQ1RfRElGRkVSUxACMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWVCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJQbGF0Zm9ybUFkbWluUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIvgCChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWV4cG9zZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnRCFQoTYWdlbnRfcmVsZWFzZV92YWx1ZUIQCg5yZXZpc2lvbl92YWx1ZSLjBQoOQ2F0YWxvZ3VlRW50cnkSMgoEa2luZBgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnlLaW5kEgwKBG5hbWUYAiABKAkSEwoJcHVibGlzaGVyGAMgASgJSAASNwoNcHVibGlzaGVyX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASMwoHZGVzaXJlZBgFIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRGVzaXJlZBI5CgphdXRob3JpemVkGAYgASgLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemVkEjMKB2FwcGxpZWQYByABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUFwcGxpZWQSNQoIb2JzZXJ2ZWQYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkEjsKC3dpdGhkcmF3aW5nGAkgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVXaXRoZHJhd2luZxIzCgdyZXRpcmVkGAogASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZXRpcmVkEjgKDmJ1aWxkX3NpemVfZ2FwGAsgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJBCgxyZWdpc3RyYXRpb24YDCABKAsyJi5zYWFzLmFjY291bnRzLnYxLlNvbHV0aW9uUmVnaXN0cmF0aW9uSAKIAQESPgoNaW5zdGFsbGF0aW9ucxgNIAMoCzInLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uQhEKD3B1Ymxpc2hlcl92YWx1ZUISChBidWlsZF9zaXplX3ZhbHVlQg8KDV9yZWdpc3RyYXRpb24iOgocTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBIaChJpbmNsdWRlX3RvbWJzdG9uZWQYASABKAgibQodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMqfgoSQ2F0YWxvZ3VlRW50cnlLaW5kEiQKIENBVEFMT0dVRV9FTlRSWV9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQ0FUQUxPR1VFX0VOVFJZX0tJTkRfTU9EVUxFEAESIQodQ0FUQUxPR1VFX0VOVFJZX0tJTkRfU09MVVRJT04QAiqvAQoSQ2F0YWxvZ3VlR2FwUmVhc29uEiQKIENBVEFMT0dVRV9HQVBfUkVBU09OX1VOU1BFQ0lGSUVEEAASJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFQ09SREVEEAESJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFUE9SVEVEEAISJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX09CU0VSVkVEEAMq3gEKGENhdGFsb2d1ZU9ic2VydmVkVmVyZGljdBIqCiZDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9VTlNQRUNJRklFRBAAEjEKLUNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfQVVUSE9SSVpFRBABEi4KKkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfRElGRkVSUxACEjMKL0NBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfVU5BVVRIT1JJWkVEEAMy7x8KFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnMSsAEKFUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZRIuLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2UiNsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCGBIWL3YxL3BsYXRmb3JtL2NhdGFsb2d1ZUK6AQoUY29tLnNhYXMuYWNjb3VudHMudjFCElBsYXRmb3JtQWRtaW5Qcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -858,7 +858,7 @@ export const CatalogueGapSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 32); /** - * CatalogueRelease is a declared release, publisher/name@version. + * CatalogueRelease is a release, publisher/name@version. * * @generated from message saas.accounts.v1.CatalogueRelease */ @@ -912,33 +912,319 @@ export const CatalogueExecutionSchema: GenMessage = /*@__PUR messageDesc(file_saas_accounts_v1_platform_admin, 34); /** - * CatalogueRunning is the running incarnation beside the declared one. + * CatalogueDesired is what the composition declares. * - * @generated from message saas.accounts.v1.CatalogueRunning + * @generated from message saas.accounts.v1.CatalogueDesired */ -export type CatalogueRunning = Message<"saas.accounts.v1.CatalogueRunning"> & { +export type CatalogueDesired = Message<"saas.accounts.v1.CatalogueDesired"> & { /** - * @generated from field: saas.accounts.v1.CatalogueExecution declared = 1; + * @generated from oneof saas.accounts.v1.CatalogueDesired.declared_revision_value */ - declared?: CatalogueExecution; + declaredRevisionValue: { + /** + * @generated from field: int64 declared_revision = 1; + */ + value: bigint; + case: "declaredRevision"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap declared_revision_gap = 2; + */ + value: CatalogueGap; + case: "declaredRevisionGap"; + } | { case: undefined; value?: undefined }; /** - * @generated from field: saas.accounts.v1.CatalogueExecution observed = 2; + * @generated from oneof saas.accounts.v1.CatalogueDesired.declared_release_value */ - observed?: CatalogueExecution; + declaredReleaseValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueRelease declared_release = 3; + */ + value: CatalogueRelease; + case: "declaredRelease"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap declared_release_gap = 4; + */ + value: CatalogueGap; + case: "declaredReleaseGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueDesired. + * Use `create(CatalogueDesiredSchema)` to create a new message. + */ +export const CatalogueDesiredSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 35); +/** + * CatalogueAuthorization is a current platform approval: the authorized + * revision and the execution it approved. + * + * @generated from message saas.accounts.v1.CatalogueAuthorization + */ +export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthorization"> & { /** - * @generated from field: saas.accounts.v1.CatalogueRunningVerdict verdict = 3; + * @generated from field: int64 authorized_revision = 1; */ - verdict: CatalogueRunningVerdict; + authorizedRevision: bigint; + + /** + * @generated from field: saas.accounts.v1.CatalogueExecution approved_execution = 2; + */ + approvedExecution?: CatalogueExecution; }; /** - * Describes the message saas.accounts.v1.CatalogueRunning. - * Use `create(CatalogueRunningSchema)` to create a new message. + * Describes the message saas.accounts.v1.CatalogueAuthorization. + * Use `create(CatalogueAuthorizationSchema)` to create a new message. */ -export const CatalogueRunningSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 35); +export const CatalogueAuthorizationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 36); + +/** + * CatalogueNotAuthorized is a known absence of current authorization — + * never approved, or approval withdrawn. It is a statement the approval record + * makes, distinct from a CatalogueGap, where the host cannot tell. + * + * @generated from message saas.accounts.v1.CatalogueNotAuthorized + */ +export type CatalogueNotAuthorized = Message<"saas.accounts.v1.CatalogueNotAuthorized"> & { + /** + * @generated from field: string detail = 1; + */ + detail: string; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueNotAuthorized. + * Use `create(CatalogueNotAuthorizedSchema)` to create a new message. + */ +export const CatalogueNotAuthorizedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 37); + +/** + * CatalogueAuthorized is what the platform has approved to execute. + * + * @generated from message saas.accounts.v1.CatalogueAuthorized + */ +export type CatalogueAuthorized = Message<"saas.accounts.v1.CatalogueAuthorized"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueAuthorized.authorization_value + */ + authorizationValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueAuthorization authorization = 1; + */ + value: CatalogueAuthorization; + case: "authorization"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueNotAuthorized not_authorized = 2; + */ + value: CatalogueNotAuthorized; + case: "notAuthorized"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap authorization_gap = 3; + */ + value: CatalogueGap; + case: "authorizationGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueAuthorized. + * Use `create(CatalogueAuthorizedSchema)` to create a new message. + */ +export const CatalogueAuthorizedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 38); + +/** + * CatalogueApplied is what delivery actually wrote. + * + * @generated from message saas.accounts.v1.CatalogueApplied + */ +export type CatalogueApplied = Message<"saas.accounts.v1.CatalogueApplied"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueApplied.applied_revision_value + */ + appliedRevisionValue: { + /** + * @generated from field: int64 applied_revision = 1; + */ + value: bigint; + case: "appliedRevision"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap applied_revision_gap = 2; + */ + value: CatalogueGap; + case: "appliedRevisionGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueApplied. + * Use `create(CatalogueAppliedSchema)` to create a new message. + */ +export const CatalogueAppliedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 39); + +/** + * CatalogueObserved is what is actually running. + * + * @generated from message saas.accounts.v1.CatalogueObserved + */ +export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueObserved.observed_revision_value + */ + observedRevisionValue: { + /** + * @generated from field: int64 observed_revision = 1; + */ + value: bigint; + case: "observedRevision"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap observed_revision_gap = 2; + */ + value: CatalogueGap; + case: "observedRevisionGap"; + } | { case: undefined; value?: undefined }; + + /** + * When the observation was made; a stale observation is never current. + * + * @generated from oneof saas.accounts.v1.CatalogueObserved.observation_freshness_value + */ + observationFreshnessValue: { + /** + * @generated from field: google.protobuf.Timestamp observed_at = 3; + */ + value: Timestamp; + case: "observedAt"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap observation_freshness_gap = 4; + */ + value: CatalogueGap; + case: "observationFreshnessGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueObserved.observed_execution_value + */ + observedExecutionValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueExecution observed_execution = 5; + */ + value: CatalogueExecution; + case: "observedExecution"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap observed_execution_gap = 6; + */ + value: CatalogueGap; + case: "observedExecutionGap"; + } | { case: undefined; value?: undefined }; + + /** + * A verdict exists only when both an observation and a known authorization + * do; anything less is a gap, and an unobserved entry is never a match. + * + * @generated from oneof saas.accounts.v1.CatalogueObserved.verdict_value + */ + verdictValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueObservedVerdict verdict = 7; + */ + value: CatalogueObservedVerdict; + case: "verdict"; + } | { + /** + * @generated from field: saas.accounts.v1.CatalogueGap verdict_gap = 8; + */ + value: CatalogueGap; + case: "verdictGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueObserved. + * Use `create(CatalogueObservedSchema)` to create a new message. + */ +export const CatalogueObservedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 40); + +/** + * CatalogueWithdrawing is approval withdrawn and retirement not yet complete, + * acknowledged separately from retirement itself. The value cases are the + * deployment contract's withdrawal and revocation records; until they exist + * each field holds only its gap. + * + * @generated from message saas.accounts.v1.CatalogueWithdrawing + */ +export type CatalogueWithdrawing = Message<"saas.accounts.v1.CatalogueWithdrawing"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueWithdrawing.withdrawal_state_value + */ + withdrawalStateValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap withdrawal_state_gap = 1; + */ + value: CatalogueGap; + case: "withdrawalStateGap"; + } | { case: undefined; value?: undefined }; + + /** + * @generated from oneof saas.accounts.v1.CatalogueWithdrawing.credential_revocation_state_value + */ + credentialRevocationStateValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap credential_revocation_state_gap = 2; + */ + value: CatalogueGap; + case: "credentialRevocationStateGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueWithdrawing. + * Use `create(CatalogueWithdrawingSchema)` to create a new message. + */ +export const CatalogueWithdrawingSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 41); + +/** + * CatalogueRetired is whether the workload is physically stopped. Retired + * requires stop or fence evidence; until a retirement controller reports to + * this host, the field holds only its gap and nothing reads as retired. + * + * @generated from message saas.accounts.v1.CatalogueRetired + */ +export type CatalogueRetired = Message<"saas.accounts.v1.CatalogueRetired"> & { + /** + * @generated from oneof saas.accounts.v1.CatalogueRetired.retirement_state_value + */ + retirementStateValue: { + /** + * @generated from field: saas.accounts.v1.CatalogueGap retirement_state_gap = 1; + */ + value: CatalogueGap; + case: "retirementStateGap"; + } | { case: undefined; value?: undefined }; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueRetired. + * Use `create(CatalogueRetiredSchema)` to create a new message. + */ +export const CatalogueRetiredSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 42); /** * CatalogueInstallation is one organization's installation of a solution. @@ -977,7 +1263,7 @@ export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallat } | { case: undefined; value?: undefined }; /** - * The installation revision the installed generation is pinned at. + * The installation's own revision, independent of the deployment's. * * @generated from oneof saas.accounts.v1.CatalogueInstallation.revision_value */ @@ -1003,10 +1289,14 @@ export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallat * Use `create(CatalogueInstallationSchema)` to create a new message. */ export const CatalogueInstallationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 36); + messageDesc(file_saas_accounts_v1_platform_admin, 43); /** - * CatalogueEntry is one module or solution. + * CatalogueEntry is one module or solution, placed in the registry's state + * model: desired, authorized, applied, observed, withdrawing and physically + * retired are distinct, and a row shows each separately, so an entry desired + * but not authorized, or observed but no longer authorized, reads as exactly + * that. * * @generated from message saas.accounts.v1.CatalogueEntry */ @@ -1044,76 +1334,34 @@ export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { } | { case: undefined; value?: undefined }; /** - * @generated from oneof saas.accounts.v1.CatalogueEntry.declared_release_value + * @generated from field: saas.accounts.v1.CatalogueDesired desired = 5; */ - declaredReleaseValue: { - /** - * @generated from field: saas.accounts.v1.CatalogueRelease declared_release = 5; - */ - value: CatalogueRelease; - case: "declaredRelease"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap declared_release_gap = 6; - */ - value: CatalogueGap; - case: "declaredReleaseGap"; - } | { case: undefined; value?: undefined }; + desired?: CatalogueDesired; /** - * The OCI image manifest digest the entry was published at. - * - * @generated from oneof saas.accounts.v1.CatalogueEntry.build_digest_value + * @generated from field: saas.accounts.v1.CatalogueAuthorized authorized = 6; */ - buildDigestValue: { - /** - * @generated from field: string build_digest = 7; - */ - value: string; - case: "buildDigest"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap build_digest_gap = 8; - */ - value: CatalogueGap; - case: "buildDigestGap"; - } | { case: undefined; value?: undefined }; + authorized?: CatalogueAuthorized; /** - * The presence generation this host applied. - * - * @generated from oneof saas.accounts.v1.CatalogueEntry.generation_value + * @generated from field: saas.accounts.v1.CatalogueApplied applied = 7; */ - generationValue: { - /** - * @generated from field: int64 generation = 9; - */ - value: bigint; - case: "generation"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap generation_gap = 10; - */ - value: CatalogueGap; - case: "generationGap"; - } | { case: undefined; value?: undefined }; + applied?: CatalogueApplied; /** - * @generated from oneof saas.accounts.v1.CatalogueEntry.running_value + * @generated from field: saas.accounts.v1.CatalogueObserved observed = 8; */ - runningValue: { - /** - * @generated from field: saas.accounts.v1.CatalogueRunning running = 11; - */ - value: CatalogueRunning; - case: "running"; - } | { - /** - * @generated from field: saas.accounts.v1.CatalogueGap running_gap = 12; - */ - value: CatalogueGap; - case: "runningGap"; - } | { case: undefined; value?: undefined }; + observed?: CatalogueObserved; + + /** + * @generated from field: saas.accounts.v1.CatalogueWithdrawing withdrawing = 9; + */ + withdrawing?: CatalogueWithdrawing; + + /** + * @generated from field: saas.accounts.v1.CatalogueRetired retired = 10; + */ + retired?: CatalogueRetired; /** * Lines of code, backend and frontend, vendored paths excluded. The value @@ -1124,25 +1372,25 @@ export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { */ buildSizeValue: { /** - * @generated from field: saas.accounts.v1.CatalogueGap build_size_gap = 13; + * @generated from field: saas.accounts.v1.CatalogueGap build_size_gap = 11; */ value: CatalogueGap; case: "buildSizeGap"; } | { case: undefined; value?: undefined }; /** - * The solution's registration, with deployment topology withheld: the + * The solution's self-registration, with deployment topology withheld: the * frontend manifest, backend upstream and service alias are always empty * here, as on every browser-facing projection of the registry. Absent for a * module, which does not self-register, and for a solution known only from * its installations. * - * @generated from field: optional saas.accounts.v1.SolutionRegistration registration = 14; + * @generated from field: optional saas.accounts.v1.SolutionRegistration registration = 12; */ registration?: SolutionRegistration; /** - * @generated from field: repeated saas.accounts.v1.CatalogueInstallation installations = 15; + * @generated from field: repeated saas.accounts.v1.CatalogueInstallation installations = 13; */ installations: CatalogueInstallation[]; }; @@ -1152,7 +1400,7 @@ export type CatalogueEntry = Message<"saas.accounts.v1.CatalogueEntry"> & { * Use `create(CatalogueEntrySchema)` to create a new message. */ export const CatalogueEntrySchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 37); + messageDesc(file_saas_accounts_v1_platform_admin, 44); /** * @generated from message saas.accounts.v1.ListPlatformCatalogueRequest @@ -1171,7 +1419,7 @@ export type ListPlatformCatalogueRequest = Message<"saas.accounts.v1.ListPlatfor * Use `create(ListPlatformCatalogueRequestSchema)` to create a new message. */ export const ListPlatformCatalogueRequestSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 38); + messageDesc(file_saas_accounts_v1_platform_admin, 45); /** * @generated from message saas.accounts.v1.ListPlatformCatalogueResponse @@ -1197,7 +1445,7 @@ export type ListPlatformCatalogueResponse = Message<"saas.accounts.v1.ListPlatfo * Use `create(ListPlatformCatalogueResponseSchema)` to create a new message. */ export const ListPlatformCatalogueResponseSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 39); + messageDesc(file_saas_accounts_v1_platform_admin, 46); /** * CatalogueEntryKind distinguishes a composed module from a solution. @@ -1273,37 +1521,45 @@ export const CatalogueGapReasonSchema: GenEnum = /*@__PURE__ enumDesc(file_saas_accounts_v1_platform_admin, 1); /** - * CatalogueRunningVerdict compares the observed execution with the declared - * one. It exists only when both are known; anything less is a CatalogueGap. + * CatalogueObservedVerdict judges what runs against what is authorized — + * never against what is declared, because only authorization admits execution. * - * @generated from enum saas.accounts.v1.CatalogueRunningVerdict + * @generated from enum saas.accounts.v1.CatalogueObservedVerdict */ -export enum CatalogueRunningVerdict { +export enum CatalogueObservedVerdict { /** - * @generated from enum value: CATALOGUE_RUNNING_VERDICT_UNSPECIFIED = 0; + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; */ UNSPECIFIED = 0, /** - * The observed image digest and incarnation are the declared ones. + * The observed image digest and incarnation are the approved execution. * - * @generated from enum value: CATALOGUE_RUNNING_VERDICT_MATCHES = 1; + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; */ - MATCHES = 1, + RUNNING_AUTHORIZED = 1, /** - * Something other than the declared execution is running — what cluster + * Something other than the approved execution is running: what cluster * admission refuses. * - * @generated from enum value: CATALOGUE_RUNNING_VERDICT_DIFFERS = 2; + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS = 2; + */ + RUNNING_DIFFERS = 2, + + /** + * Something is running with no current authorization at all: what the + * retirement sweep exists to stop. + * + * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED = 3; */ - DIFFERS = 2, + RUNNING_UNAUTHORIZED = 3, } /** - * Describes the enum saas.accounts.v1.CatalogueRunningVerdict. + * Describes the enum saas.accounts.v1.CatalogueObservedVerdict. */ -export const CatalogueRunningVerdictSchema: GenEnum = /*@__PURE__*/ +export const CatalogueObservedVerdictSchema: GenEnum = /*@__PURE__*/ enumDesc(file_saas_accounts_v1_platform_admin, 2); /** From 56a82634f028dcd69509395aa8f046a11482d3cb Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:21:00 -0400 Subject: [PATCH 03/11] The Catalogue labels each exposed team as granted here or inherited from above (Refs #1020) An installation's teams are listed in two kinds the server keeps apart: granted at the installation's authority root, and inherited from a grant above it. Both reach inside the installation; only the first was granted to it, so inherited reach is never shown as if it had been. No write path produces an inherited grant today - an installation root is a single-label path and every grant needs a registered, non-empty node - so the inherited list is empty in practice; the test seeds one directly to hold the classification for when scope trees deepen. Also formats platform_catalogue.go, which the previous head left unformatted. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../api/accounts/authority/contract.binpb | Bin 956280 -> 956621 bytes .../saas/accounts/v1/platform_admin.proto | 10 +- .../api/accounts/connect/contract.binpb | Bin 956280 -> 956621 bytes .../saas/accounts/v1/platform_admin.proto | 10 +- module/contracts/api/catalog.codefly.json | 4 +- module/module.package.codefly.yaml | 4 +- .../code/pkg/business/platform_catalogue.go | 35 ++-- .../pkg/business/platform_catalogue_test.go | 9 +- .../gen/saas/accounts/v1/platform_admin.pb.go | 159 ++++++++++-------- .../postgres_catalogue_installations_test.go | 40 ++++- .../code/pkg/infra/postgres_installations.go | 18 +- .../generated/openapi-raw/api.swagger.json | 11 +- .../accounts/openapi/api.swagger.json | 11 +- .../saas/accounts/v1/platform_admin.proto | 10 +- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 956280 -> 956621 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 18 +- ...platform-catalogue-page.container.test.tsx | 22 ++- .../ui/platform-catalogue-page.tsx | 61 +++++-- .../gen/saas/accounts/v1/platform_admin_pb.ts | 18 +- .../services/frontend/code/ui-inventory.json | 1 + 22 files changed, 297 insertions(+), 148 deletions(-) diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 9b89d9def1b008d9a75973c66811bfb0c179c984..6a532d3e0d7914c9388b701a207a035cd97272b7 100644 GIT binary patch delta 9419 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 delta 9218 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 H7EJ#?)_h;- diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index 00f45cfff..e6830ddb1 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -423,9 +423,13 @@ message CatalogueInstallation { oneof revision_value { CatalogueGap revision_gap = 5; } - // Active team grants at the installation's authority root or above it, so - // every team whose members the grant reaches inside this installation. - repeated CollectionReadGrant exposed_teams = 6; + // Every team an active grant reaches inside this installation, in two kinds + // that are never merged: granted at this installation's authority root, and + // inherited from a grant above it. Both reach; only the first is exposure + // granted to this installation, so a reader is never shown inherited reach + // as if it had been granted here. + repeated CollectionReadGrant granted_teams = 6; + repeated CollectionReadGrant inherited_teams = 7; } // CatalogueEntry is one module or solution, placed in the registry's state diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 9b89d9def1b008d9a75973c66811bfb0c179c984..6a532d3e0d7914c9388b701a207a035cd97272b7 100644 GIT binary patch delta 9419 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 delta 9218 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 H7EJ#?)_h;- diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index 00f45cfff..e6830ddb1 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -423,9 +423,13 @@ message CatalogueInstallation { oneof revision_value { CatalogueGap revision_gap = 5; } - // Active team grants at the installation's authority root or above it, so - // every team whose members the grant reaches inside this installation. - repeated CollectionReadGrant exposed_teams = 6; + // Every team an active grant reaches inside this installation, in two kinds + // that are never merged: granted at this installation's authority root, and + // inherited from a grant above it. Both reach; only the first is exposure + // granted to this installation, so a reader is never shown inherited reach + // as if it had been granted here. + repeated CollectionReadGrant granted_teams = 6; + repeated CollectionReadGrant inherited_teams = 7; } // CatalogueEntry is one module or solution, placed in the registry's state diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 2d8511250..bf66d74ad 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18", + "digest": "sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275", "services": [ { "name": "APIKeyService", @@ -477,7 +477,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18", + "digest": "sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index a6063f37c..b9035bfc9 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18 + digest: sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275 - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18 + digest: sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/code/pkg/business/platform_catalogue.go b/module/services/accounts/code/pkg/business/platform_catalogue.go index 854ad109e..3407ccb28 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue.go @@ -24,12 +24,14 @@ import ( // CatalogueInstallationRecord is one active installation as the Catalogue reads // it: the installation, its organization's name, the identifier of the agent -// principal it was installed as, and the team grants that reach inside it. +// principal it was installed as, and the team grants that reach inside it — +// those made at its authority root apart from those inherited from above it. type CatalogueInstallationRecord struct { Installation *gen.Installation OrgName string AgentIdentifier string - ExposedTeams []*gen.CollectionReadGrant + GrantedTeams []*gen.CollectionReadGrant + InheritedTeams []*gen.CollectionReadGrant } // PlatformCatalogueEntry is one Catalogue row. Registration is kept as the @@ -47,19 +49,19 @@ type PlatformCatalogue struct { } const ( - catalogueNoDeclaration = "This host holds no applied presence document for this entry, which is what records what the composition declares." - catalogueNoApplication = "This host has applied no presence generation for this entry." - catalogueNoApproval = "This host holds no approval record: signed platform approval of the execution inventory, bound to target and ownership scope, has not been built yet." - catalogueNotObserved = "This host reads no observed cluster state, so what runs is unknown — never assumed to match." - catalogueNoVerdict = "There is no approval record to judge the observed execution against." - catalogueNoWithdrawal = "This host holds no withdrawal record; withdrawal is a change to the approval record, which does not exist yet." - catalogueNoRevocation = "This host holds no credential-revocation record for this entry." - catalogueNoRetirement = "No retirement controller reports to this host; without stop or fence evidence nothing is reported as retired." - catalogueNoBuildSize = "Build size is the presence document's build_size section (codefly-dev/core#708), computed at build by the CLI; this host holds no presence document for this entry." - catalogueModulePublisher = "A composed module is known by its principal prefix alone; the composition records no publisher." - catalogueUnregistered = "No registration exists under this identifier; it is named only by its installations." - catalogueNoRevision = "Installations carry no revision of their own on this host." - catalogueNoAgentRelease = "The installation's agent principal carries no publisher/name:version identifier." + catalogueNoDeclaration = "This host holds no applied presence document for this entry, which is what records what the composition declares." + catalogueNoApplication = "This host has applied no presence generation for this entry." + catalogueNoApproval = "This host holds no approval record: signed platform approval of the execution inventory, bound to target and ownership scope, has not been built yet." + catalogueNotObserved = "This host reads no observed cluster state, so what runs is unknown — never assumed to match." + catalogueNoVerdict = "There is no approval record to judge the observed execution against." + catalogueNoWithdrawal = "This host holds no withdrawal record; withdrawal is a change to the approval record, which does not exist yet." + catalogueNoRevocation = "This host holds no credential-revocation record for this entry." + catalogueNoRetirement = "No retirement controller reports to this host; without stop or fence evidence nothing is reported as retired." + catalogueNoBuildSize = "Build size is the presence document's build_size section (codefly-dev/core#708), computed at build by the CLI; this host holds no presence document for this entry." + catalogueModulePublisher = "A composed module is known by its principal prefix alone; the composition records no publisher." + catalogueUnregistered = "No registration exists under this identifier; it is named only by its installations." + catalogueNoRevision = "Installations carry no revision of their own on this host." + catalogueNoAgentRelease = "The installation's agent principal carries no publisher/name:version identifier." ) // ListPlatformCatalogue reads the Catalogue for a super administrator. @@ -217,7 +219,8 @@ func catalogueInstallation(record *CatalogueInstallationRecord) *gen.CatalogueIn RevisionValue: &gen.CatalogueInstallation_RevisionGap{ RevisionGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoRevision), }, - ExposedTeams: record.ExposedTeams, + GrantedTeams: record.GrantedTeams, + InheritedTeams: record.InheritedTeams, } if release, ok := parseAgentRelease(record.AgentIdentifier); ok { out.AgentReleaseValue = &gen.CatalogueInstallation_AgentRelease{AgentRelease: release} diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_test.go index c755a3281..ed664d174 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue_test.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue_test.go @@ -103,10 +103,14 @@ func catalogueTestInstallation(id, solution, org, agent string) *CatalogueInstal }, OrgName: org, AgentIdentifier: agent, - ExposedTeams: []*gen.CollectionReadGrant{{ + GrantedTeams: []*gen.CollectionReadGrant{{ Grant: &gen.ScopeGrant{SubjectId: "team-" + id, SubjectKind: gen.SubjectKind_SUBJECT_KIND_TEAM}, SubjectLabel: "Team " + id, }}, + InheritedTeams: []*gen.CollectionReadGrant{{ + Grant: &gen.ScopeGrant{SubjectId: "org-team-" + id, SubjectKind: gen.SubjectKind_SUBJECT_KIND_TEAM}, + SubjectLabel: "Org team " + id, + }}, } } @@ -195,7 +199,8 @@ func TestProjectPlatformCatalogue(t *testing.T) { require.Equal(t, "Acme", first.GetOrgName()) require.Equal(t, &gen.CatalogueRelease{Publisher: "example", Name: "installed", Version: "1.2.0"}, first.GetAgentRelease()) require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, first.GetRevisionGap().GetReason()) - require.Equal(t, "Team i1", first.GetExposedTeams()[0].GetSubjectLabel()) + require.Equal(t, "Team i1", first.GetGrantedTeams()[0].GetSubjectLabel()) + require.Equal(t, "Org team i1", first.GetInheritedTeams()[0].GetSubjectLabel(), "inherited reach is carried apart from what was granted here") require.Equal(t, "1.3.0", installed.Entry.GetInstallations()[1].GetAgentRelease().GetVersion()) unregistered := byName["unregistered"] diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index 0f013401a..a73c866af 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -2970,11 +2970,15 @@ type CatalogueInstallation struct { // // *CatalogueInstallation_RevisionGap RevisionValue isCatalogueInstallation_RevisionValue `protobuf_oneof:"revision_value"` - // Active team grants at the installation's authority root or above it, so - // every team whose members the grant reaches inside this installation. - ExposedTeams []*CollectionReadGrant `protobuf:"bytes,6,rep,name=exposed_teams,json=exposedTeams,proto3" json:"exposed_teams,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Every team an active grant reaches inside this installation, in two kinds + // that are never merged: granted at this installation's authority root, and + // inherited from a grant above it. Both reach; only the first is exposure + // granted to this installation, so a reader is never shown inherited reach + // as if it had been granted here. + GrantedTeams []*CollectionReadGrant `protobuf:"bytes,6,rep,name=granted_teams,json=grantedTeams,proto3" json:"granted_teams,omitempty"` + InheritedTeams []*CollectionReadGrant `protobuf:"bytes,7,rep,name=inherited_teams,json=inheritedTeams,proto3" json:"inherited_teams,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CatalogueInstallation) Reset() { @@ -3062,9 +3066,16 @@ func (x *CatalogueInstallation) GetRevisionGap() *CatalogueGap { return nil } -func (x *CatalogueInstallation) GetExposedTeams() []*CollectionReadGrant { +func (x *CatalogueInstallation) GetGrantedTeams() []*CollectionReadGrant { + if x != nil { + return x.GrantedTeams + } + return nil +} + +func (x *CatalogueInstallation) GetInheritedTeams() []*CollectionReadGrant { if x != nil { - return x.ExposedTeams + return x.InheritedTeams } return nil } @@ -3586,14 +3597,15 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "!credential_revocation_state_value\"\x80\x01\n" + "\x10CatalogueRetired\x12R\n" + "\x14retirement_state_gap\x18\x01 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x12retirementStateGapB\x18\n" + - "\x16retirement_state_value\"\xc9\x03\n" + + "\x16retirement_state_value\"\x99\x04\n" + "\x15CatalogueInstallation\x12B\n" + "\finstallation\x18\x01 \x01(\v2\x1e.saas.accounts.v1.InstallationR\finstallation\x12\x19\n" + "\borg_name\x18\x02 \x01(\tR\aorgName\x12I\n" + "\ragent_release\x18\x03 \x01(\v2\".saas.accounts.v1.CatalogueReleaseH\x00R\fagentRelease\x12L\n" + "\x11agent_release_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x0fagentReleaseGap\x12C\n" + "\frevision_gap\x18\x05 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\vrevisionGap\x12J\n" + - "\rexposed_teams\x18\x06 \x03(\v2%.saas.accounts.v1.CollectionReadGrantR\fexposedTeamsB\x15\n" + + "\rgranted_teams\x18\x06 \x03(\v2%.saas.accounts.v1.CollectionReadGrantR\fgrantedTeams\x12N\n" + + "\x0finherited_teams\x18\a \x03(\v2%.saas.accounts.v1.CollectionReadGrantR\x0einheritedTeamsB\x15\n" + "\x13agent_release_valueB\x10\n" + "\x0erevision_value\"\xf1\x06\n" + "\x0eCatalogueEntry\x128\n" + @@ -3797,70 +3809,71 @@ var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ 36, // 34: saas.accounts.v1.CatalogueInstallation.agent_release:type_name -> saas.accounts.v1.CatalogueRelease 35, // 35: saas.accounts.v1.CatalogueInstallation.agent_release_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 36: saas.accounts.v1.CatalogueInstallation.revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 56, // 37: saas.accounts.v1.CatalogueInstallation.exposed_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 0, // 38: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind - 35, // 39: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap - 38, // 40: saas.accounts.v1.CatalogueEntry.desired:type_name -> saas.accounts.v1.CatalogueDesired - 41, // 41: saas.accounts.v1.CatalogueEntry.authorized:type_name -> saas.accounts.v1.CatalogueAuthorized - 42, // 42: saas.accounts.v1.CatalogueEntry.applied:type_name -> saas.accounts.v1.CatalogueApplied - 43, // 43: saas.accounts.v1.CatalogueEntry.observed:type_name -> saas.accounts.v1.CatalogueObserved - 44, // 44: saas.accounts.v1.CatalogueEntry.withdrawing:type_name -> saas.accounts.v1.CatalogueWithdrawing - 45, // 45: saas.accounts.v1.CatalogueEntry.retired:type_name -> saas.accounts.v1.CatalogueRetired - 35, // 46: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap - 57, // 47: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration - 46, // 48: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation - 47, // 49: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry - 3, // 50: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest - 10, // 51: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest - 11, // 52: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest - 12, // 53: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest - 14, // 54: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest - 16, // 55: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest - 19, // 56: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest - 20, // 57: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest - 23, // 58: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest - 25, // 59: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest - 26, // 60: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest - 27, // 61: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest - 5, // 62: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest - 8, // 63: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest - 30, // 64: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest - 33, // 65: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest - 58, // 66: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest - 59, // 67: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest - 60, // 68: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest - 61, // 69: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest - 62, // 70: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest - 63, // 71: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest - 48, // 72: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest - 4, // 73: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse - 64, // 74: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty - 64, // 75: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty - 13, // 76: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse - 15, // 77: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse - 18, // 78: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse - 64, // 79: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty - 21, // 80: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse - 24, // 81: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse - 64, // 82: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty - 64, // 83: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty - 29, // 84: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse - 7, // 85: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse - 9, // 86: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse - 32, // 87: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse - 34, // 88: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse - 65, // 89: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse - 66, // 90: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse - 67, // 91: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse - 68, // 92: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse - 69, // 93: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse - 70, // 94: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse - 49, // 95: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse - 73, // [73:96] is the sub-list for method output_type - 50, // [50:73] is the sub-list for method input_type - 50, // [50:50] is the sub-list for extension type_name - 50, // [50:50] is the sub-list for extension extendee - 0, // [0:50] is the sub-list for field type_name + 56, // 37: saas.accounts.v1.CatalogueInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 56, // 38: saas.accounts.v1.CatalogueInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 0, // 39: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind + 35, // 40: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap + 38, // 41: saas.accounts.v1.CatalogueEntry.desired:type_name -> saas.accounts.v1.CatalogueDesired + 41, // 42: saas.accounts.v1.CatalogueEntry.authorized:type_name -> saas.accounts.v1.CatalogueAuthorized + 42, // 43: saas.accounts.v1.CatalogueEntry.applied:type_name -> saas.accounts.v1.CatalogueApplied + 43, // 44: saas.accounts.v1.CatalogueEntry.observed:type_name -> saas.accounts.v1.CatalogueObserved + 44, // 45: saas.accounts.v1.CatalogueEntry.withdrawing:type_name -> saas.accounts.v1.CatalogueWithdrawing + 45, // 46: saas.accounts.v1.CatalogueEntry.retired:type_name -> saas.accounts.v1.CatalogueRetired + 35, // 47: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap + 57, // 48: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration + 46, // 49: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation + 47, // 50: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry + 3, // 51: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest + 10, // 52: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest + 11, // 53: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest + 12, // 54: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest + 14, // 55: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest + 16, // 56: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest + 19, // 57: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest + 20, // 58: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest + 23, // 59: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest + 25, // 60: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest + 26, // 61: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest + 27, // 62: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest + 5, // 63: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest + 8, // 64: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest + 30, // 65: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest + 33, // 66: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest + 58, // 67: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest + 59, // 68: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest + 60, // 69: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest + 61, // 70: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest + 62, // 71: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest + 63, // 72: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest + 48, // 73: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest + 4, // 74: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse + 64, // 75: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty + 64, // 76: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty + 13, // 77: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse + 15, // 78: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse + 18, // 79: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse + 64, // 80: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty + 21, // 81: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse + 24, // 82: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse + 64, // 83: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty + 64, // 84: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty + 29, // 85: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse + 7, // 86: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse + 9, // 87: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse + 32, // 88: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse + 34, // 89: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse + 65, // 90: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse + 66, // 91: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse + 67, // 92: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse + 68, // 93: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse + 69, // 94: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse + 70, // 95: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse + 49, // 96: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse + 74, // [74:97] is the sub-list for method output_type + 51, // [51:74] is the sub-list for method input_type + 51, // [51:51] is the sub-list for extension type_name + 51, // [51:51] is the sub-list for extension extendee + 0, // [0:51] is the sub-list for field type_name } func init() { file_saas_accounts_v1_platform_admin_proto_init() } diff --git a/module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go b/module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go index 40d273923..6b4b94caa 100644 --- a/module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go +++ b/module/services/accounts/code/pkg/infra/postgres_catalogue_installations_test.go @@ -9,6 +9,7 @@ import ( "accounts/pkg/business" gen "accounts/pkg/gen/saas/accounts/v1" + "accounts/pkg/infra/storetx" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" @@ -54,9 +55,10 @@ func grantCatalogueTeam(t *testing.T, orgID, teamID, roleID, path string, expire } // The Catalogue lists an active installation with its organization's name and -// the release its agent was created for, and exposes exactly the teams an active -// grant at the installation's authority root reaches: not the agent's own -// standing grant, not a team granted elsewhere, and not an expired grant. +// the release its agent was created for, and lists the teams an active grant +// reaches inside it in two kinds: granted at the installation's authority root, +// and inherited from a grant above it. Not the agent's own standing grant, not a +// team granted elsewhere, and not an expired grant. func TestListCatalogueInstallationsReadsTeamsReachingTheRoot(t *testing.T) { orgID, _, roleID, installation, rootPath := installFixture(t, "doc", "read") @@ -77,8 +79,9 @@ func TestListCatalogueInstallationsReadsTeamsReachingTheRoot(t *testing.T) { require.Equal(t, "Test Org", record.OrgName) require.Equal(t, "acme.example/solution:1.0.0", record.AgentIdentifier) - require.Len(t, record.ExposedTeams, 1, "only the active team grant reaching the root exposes a team") - team := record.ExposedTeams[0] + require.Empty(t, record.InheritedTeams, "nothing above the root grants anything yet") + require.Len(t, record.GrantedTeams, 1, "only the active team grant at the root is granted here") + team := record.GrantedTeams[0] require.Equal(t, exposed, team.GetGrant().GetSubjectId()) require.Equal(t, gen.SubjectKind_SUBJECT_KIND_TEAM, team.GetGrant().GetSubjectKind()) require.Equal(t, orgID, team.GetGrant().GetOrgId()) @@ -98,3 +101,30 @@ func TestListCatalogueInstallationsOmitsRevoked(t *testing.T) { })) require.NotContains(t, catalogueInstallations(t), installation.Id) } + +// A grant above the root reaches inside the installation too, and is listed as +// inherited, never as granted here. An installation root is a single-label path +// and every write path requires a registered, non-empty node, so the only path +// above a root is the empty one and no write path produces it today: this seeds +// it directly, to hold the classification for the day scope trees deepen. +func TestListCatalogueInstallationsSeparatesInheritedTeams(t *testing.T) { + orgID, _, roleID, installation, rootPath := installFixture(t, "doc", "read") + granted := seedCatalogueTeam(t, orgID, "Granted Team") + grantCatalogueTeam(t, orgID, granted, roleID, rootPath, nil) + inherited := seedCatalogueTeam(t, orgID, "Org-wide Team") + require.NoError(t, testStore.WithControlPlane(testCtx, func(ctx context.Context) error { + _, err := storetx.Tx(ctx).Exec(ctx, + `INSERT INTO scope_grants (id, org_id, subject_id, subject_kind, scope_path, role_id, created_at) + VALUES ($1, $2, $3, 'team', ''::ltree, $4, now())`, + business.NewIDString(), orgID, inherited, roleID) + return err + })) + + record := catalogueInstallations(t)[installation.Id] + require.NotNil(t, record) + require.Len(t, record.GrantedTeams, 1) + require.Equal(t, granted, record.GrantedTeams[0].GetGrant().GetSubjectId()) + require.Len(t, record.InheritedTeams, 1) + require.Equal(t, inherited, record.InheritedTeams[0].GetGrant().GetSubjectId()) + require.Equal(t, "Org-wide Team", record.InheritedTeams[0].GetSubjectLabel()) +} diff --git a/module/services/accounts/code/pkg/infra/postgres_installations.go b/module/services/accounts/code/pkg/infra/postgres_installations.go index 3b5fee889..b5f370e4f 100644 --- a/module/services/accounts/code/pkg/infra/postgres_installations.go +++ b/module/services/accounts/code/pkg/infra/postgres_installations.go @@ -659,8 +659,8 @@ func installationScopeAllowed(ctx context.Context, executor QueryExecutor, orgID // ListCatalogueInstallations lists every active installation in every // organization for the platform Catalogue, with the organization's name, the // identifier of the agent principal it was installed as, and the active team -// grants at its authority root or above it. It must run under the control plane: -// it reads across tenants. +// grants at its authority root (granted there) or above it (inherited). It must +// run under the control plane: it reads across tenants. func (s *PostgresStore) ListCatalogueInstallations(ctx context.Context) ([]*business.CatalogueInstallationRecord, error) { executor := s.getQueryExecutor(ctx) rows, err := executor.Query(ctx, `SELECT `+installationColumns+`, org_name, agent_identifier @@ -692,7 +692,7 @@ func (s *PostgresStore) ListCatalogueInstallations(ctx context.Context) ([]*busi return nil, fmt.Errorf("list catalogue installations: %w", err) } - grants, err := executor.Query(ctx, `SELECT i.id::text, i.org_id::text, `+labelledGrantColumns+` + grants, err := executor.Query(ctx, `SELECT i.id::text, i.org_id::text, g.scope_path = root.scope_path, `+labelledGrantColumns+` FROM installations i JOIN scope_nodes root ON root.id = i.root_scope_node_id JOIN scope_grants g ON g.org_id = i.org_id AND g.subject_kind = 'team' @@ -707,13 +707,19 @@ func (s *PostgresStore) ListCatalogueInstallations(ctx context.Context) ([]*busi defer grants.Close() for grants.Next() { var installationID, orgID string - view, err := scanLabelledGrant(grants, "", &installationID, &orgID) + var atRoot bool + view, err := scanLabelledGrant(grants, "", &installationID, &orgID, &atRoot) if err != nil { return nil, fmt.Errorf("scan catalogue installation team: %w", err) } view.Grant.OrgId = orgID - if record, ok := byID[installationID]; ok { - record.ExposedTeams = append(record.ExposedTeams, view) + record, ok := byID[installationID] + switch { + case !ok: + case atRoot: + record.GrantedTeams = append(record.GrantedTeams, view) + default: + record.InheritedTeams = append(record.InheritedTeams, view) } } if err := grants.Err(); err != nil { diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 7aa6c77c0..814f17bbc 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -7021,13 +7021,20 @@ "revisionGap": { "$ref": "#/definitions/v1CatalogueGap" }, - "exposedTeams": { + "grantedTeams": { "type": "array", "items": { "type": "object", "$ref": "#/definitions/v1CollectionReadGrant" }, - "description": "Active team grants at the installation's authority root or above it, so\nevery team whose members the grant reaches inside this installation." + "description": "Every team an active grant reaches inside this installation, in two kinds\nthat are never merged: granted at this installation's authority root, and\ninherited from a grant above it. Both reach; only the first is exposure\ngranted to this installation, so a reader is never shown inherited reach\nas if it had been granted here." + }, + "inheritedTeams": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1CollectionReadGrant" + } } }, "description": "CatalogueInstallation is one organization's installation of a solution." diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 4298323f4..1a002507a 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -1001,8 +1001,15 @@ "agentReleaseGap": { "$ref": "#/definitions/v1CatalogueGap" }, - "exposedTeams": { - "description": "Active team grants at the installation's authority root or above it, so\nevery team whose members the grant reaches inside this installation.", + "grantedTeams": { + "description": "Every team an active grant reaches inside this installation, in two kinds\nthat are never merged: granted at this installation's authority root, and\ninherited from a grant above it. Both reach; only the first is exposure\ngranted to this installation, so a reader is never shown inherited reach\nas if it had been granted here.", + "items": { + "$ref": "#/definitions/v1CollectionReadGrant", + "type": "object" + }, + "type": "array" + }, + "inheritedTeams": { "items": { "$ref": "#/definitions/v1CollectionReadGrant", "type": "object" diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index 00f45cfff..e6830ddb1 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -423,9 +423,13 @@ message CatalogueInstallation { oneof revision_value { CatalogueGap revision_gap = 5; } - // Active team grants at the installation's authority root or above it, so - // every team whose members the grant reaches inside this installation. - repeated CollectionReadGrant exposed_teams = 6; + // Every team an active grant reaches inside this installation, in two kinds + // that are never merged: granted at this installation's authority root, and + // inherited from a grant above it. Both reach; only the first is exposure + // granted to this installation, so a reader is never shown inherited reach + // as if it had been granted here. + repeated CollectionReadGrant granted_teams = 6; + repeated CollectionReadGrant inherited_teams = 7; } // CatalogueEntry is one module or solution, placed in the registry's state diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index a22161acb..6908e20dc 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18", + "digest": "sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 9b89d9def1b008d9a75973c66811bfb0c179c984..6a532d3e0d7914c9388b701a207a035cd97272b7 100644 GIT binary patch delta 9419 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 delta 9218 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 H7EJ#?)_h;- diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index 701ebc84f..c92e85560 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:b8330991086432fdf4721704e86c30446676768857597ce072d6b468c71ead18 + contract-digest: sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275 services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index ea551beb8..907e9a8c5 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIvgCChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWV4cG9zZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnRCFQoTYWdlbnRfcmVsZWFzZV92YWx1ZUIQCg5yZXZpc2lvbl92YWx1ZSLjBQoOQ2F0YWxvZ3VlRW50cnkSMgoEa2luZBgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnlLaW5kEgwKBG5hbWUYAiABKAkSEwoJcHVibGlzaGVyGAMgASgJSAASNwoNcHVibGlzaGVyX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASMwoHZGVzaXJlZBgFIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRGVzaXJlZBI5CgphdXRob3JpemVkGAYgASgLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemVkEjMKB2FwcGxpZWQYByABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUFwcGxpZWQSNQoIb2JzZXJ2ZWQYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkEjsKC3dpdGhkcmF3aW5nGAkgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVXaXRoZHJhd2luZxIzCgdyZXRpcmVkGAogASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZXRpcmVkEjgKDmJ1aWxkX3NpemVfZ2FwGAsgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJBCgxyZWdpc3RyYXRpb24YDCABKAsyJi5zYWFzLmFjY291bnRzLnYxLlNvbHV0aW9uUmVnaXN0cmF0aW9uSAKIAQESPgoNaW5zdGFsbGF0aW9ucxgNIAMoCzInLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uQhEKD3B1Ymxpc2hlcl92YWx1ZUISChBidWlsZF9zaXplX3ZhbHVlQg8KDV9yZWdpc3RyYXRpb24iOgocTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBIaChJpbmNsdWRlX3RvbWJzdG9uZWQYASABKAgibQodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMqfgoSQ2F0YWxvZ3VlRW50cnlLaW5kEiQKIENBVEFMT0dVRV9FTlRSWV9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQ0FUQUxPR1VFX0VOVFJZX0tJTkRfTU9EVUxFEAESIQodQ0FUQUxPR1VFX0VOVFJZX0tJTkRfU09MVVRJT04QAiqvAQoSQ2F0YWxvZ3VlR2FwUmVhc29uEiQKIENBVEFMT0dVRV9HQVBfUkVBU09OX1VOU1BFQ0lGSUVEEAASJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFQ09SREVEEAESJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFUE9SVEVEEAISJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX09CU0VSVkVEEAMq3gEKGENhdGFsb2d1ZU9ic2VydmVkVmVyZGljdBIqCiZDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9VTlNQRUNJRklFRBAAEjEKLUNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfQVVUSE9SSVpFRBABEi4KKkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfRElGRkVSUxACEjMKL0NBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfVU5BVVRIT1JJWkVEEAMy7x8KFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnMSsAEKFUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZRIuLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2UiNsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCGBIWL3YxL3BsYXRmb3JtL2NhdGFsb2d1ZWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIrgDChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWdyYW50ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAcgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50QhUKE2FnZW50X3JlbGVhc2VfdmFsdWVCEAoOcmV2aXNpb25fdmFsdWUi4wUKDkNhdGFsb2d1ZUVudHJ5EjIKBGtpbmQYASABKA4yJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5S2luZBIMCgRuYW1lGAIgASgJEhMKCXB1Ymxpc2hlchgDIAEoCUgAEjcKDXB1Ymxpc2hlcl9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjMKB2Rlc2lyZWQYBSABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZURlc2lyZWQSOQoKYXV0aG9yaXplZBgGIAEoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXplZBIzCgdhcHBsaWVkGAcgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBcHBsaWVkEjUKCG9ic2VydmVkGAggASgLMiMuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZBI7Cgt3aXRoZHJhd2luZxgJIAEoCzImLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlV2l0aGRyYXdpbmcSMwoHcmV0aXJlZBgKIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmV0aXJlZBI4Cg5idWlsZF9zaXplX2dhcBgLIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESQQoMcmVnaXN0cmF0aW9uGAwgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgCiAEBEj4KDWluc3RhbGxhdGlvbnMYDSADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCEgoQYnVpbGRfc2l6ZV92YWx1ZUIPCg1fcmVnaXN0cmF0aW9uIjoKHExpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QSGgoSaW5jbHVkZV90b21ic3RvbmVkGAEgASgIIm0KHUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlEjEKB2VudHJpZXMYASADKAsyIC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5EhkKEXJlZ2lzdHJ5X3JldmlzaW9uGAIgASgDKn4KEkNhdGFsb2d1ZUVudHJ5S2luZBIkCiBDQVRBTE9HVUVfRU5UUllfS0lORF9VTlNQRUNJRklFRBAAEh8KG0NBVEFMT0dVRV9FTlRSWV9LSU5EX01PRFVMRRABEiEKHUNBVEFMT0dVRV9FTlRSWV9LSU5EX1NPTFVUSU9OEAIqrwEKEkNhdGFsb2d1ZUdhcFJlYXNvbhIkCiBDQVRBTE9HVUVfR0FQX1JFQVNPTl9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRUNPUkRFRBABEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRVBPUlRFRBACEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9PQlNFUlZFRBADKt4BChhDYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3QSKgomQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfVU5TUEVDSUZJRUQQABIxCi1DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0FVVEhPUklaRUQQARIuCipDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0RJRkZFUlMQAhIzCi9DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX1VOQVVUSE9SSVpFRBADMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -1276,12 +1276,20 @@ export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallat } | { case: undefined; value?: undefined }; /** - * Active team grants at the installation's authority root or above it, so - * every team whose members the grant reaches inside this installation. + * Every team an active grant reaches inside this installation, in two kinds + * that are never merged: granted at this installation's authority root, and + * inherited from a grant above it. Both reach; only the first is exposure + * granted to this installation, so a reader is never shown inherited reach + * as if it had been granted here. * - * @generated from field: repeated saas.accounts.v1.CollectionReadGrant exposed_teams = 6; + * @generated from field: repeated saas.accounts.v1.CollectionReadGrant granted_teams = 6; */ - exposedTeams: CollectionReadGrant[]; + grantedTeams: CollectionReadGrant[]; + + /** + * @generated from field: repeated saas.accounts.v1.CollectionReadGrant inherited_teams = 7; + */ + inheritedTeams: CollectionReadGrant[]; }; /** diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx index c46a77229..7bc09d62a 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx @@ -95,13 +95,20 @@ const catalogue = { version: "1.2.0", }, revisionGap: notRecorded("No installation revision."), - exposedTeams: [ + grantedTeams: [ { grant: { id: "g1", scopePath: "root" }, subjectLabel: "Finance Team", roleName: "Viewer", }, ], + inheritedTeams: [ + { + grant: { id: "g2", scopePath: "" }, + subjectLabel: "Everyone Team", + roleName: "Viewer", + }, + ], }, ], }), @@ -222,7 +229,18 @@ describe("PlatformCataloguePage admin container", () => { expect( within(installations).getByText("example/drifted@1.2.0"), ).toBeTruthy(); - expect(within(installations).getByText("Finance Team")).toBeTruthy(); + // Granted here and inherited from above are listed apart, each named. + const granted = within(installations) + .getByText("Granted at this installation") + .closest("div"); + const inherited = within(installations) + .getByText("Inherited from above") + .closest("div"); + if (!granted || !inherited) throw new Error("team groups missing"); + expect(within(granted).getByText("Finance Team")).toBeTruthy(); + expect(within(granted).queryByText("Everyone Team")).toBeNull(); + expect(within(inherited).getByText("Everyone Team")).toBeTruthy(); + expect(within(inherited).queryByText("Finance Team")).toBeNull(); expect(within(installations).getByText("Not recorded")).toBeTruthy(); }); diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx index b0adc3143..065933e56 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.tsx @@ -3,6 +3,7 @@ import { timestampDate } from "@bufbuild/protobuf/wkt"; import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react"; import { Fragment, useState } from "react"; +import type { CollectionReadGrant } from "@/gen/saas/accounts/v1/authorization_pb"; import type { CatalogueEntry, CatalogueInstallation, @@ -99,6 +100,47 @@ function StateFacts({ state, facts }: { state: string; facts: StateFact[] }) { ); } +/** + * The teams a grant reaches inside an installation, in the two kinds the server + * keeps apart: granted at this installation, and inherited from a grant above + * it. Inherited reach is real reach, but it was never granted here, so it is + * never shown as if it had been. + */ +export function TeamGrants({ + granted, + inherited, +}: { + granted: CollectionReadGrant[]; + inherited: CollectionReadGrant[]; +}) { + if (granted.length === 0 && inherited.length === 0) { + return No team; + } + const group = (label: string, teams: CollectionReadGrant[]) => + teams.length > 0 && ( +
+

{label}

+
+ {teams.map((team) => ( + + {team.subjectLabel} + + ))} +
+
+ ); + return ( +
+ {group("Granted at this installation", granted)} + {group("Inherited from above", inherited)} +
+ ); +} + function entryKey(entry: CatalogueEntry): string { return `${entry.kind}:${entry.name}`; } @@ -314,21 +356,10 @@ function InstallationsTable({ - {item.exposedTeams.length === 0 ? ( - No team - ) : ( -
- {item.exposedTeams.map((team) => ( - - {team.subjectLabel} - - ))} -
- )} +
{formatDate( diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index c9840d7a5..e6f576bf8 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIvgCChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWV4cG9zZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnRCFQoTYWdlbnRfcmVsZWFzZV92YWx1ZUIQCg5yZXZpc2lvbl92YWx1ZSLjBQoOQ2F0YWxvZ3VlRW50cnkSMgoEa2luZBgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnlLaW5kEgwKBG5hbWUYAiABKAkSEwoJcHVibGlzaGVyGAMgASgJSAASNwoNcHVibGlzaGVyX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASMwoHZGVzaXJlZBgFIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRGVzaXJlZBI5CgphdXRob3JpemVkGAYgASgLMiUuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemVkEjMKB2FwcGxpZWQYByABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUFwcGxpZWQSNQoIb2JzZXJ2ZWQYCCABKAsyIy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkEjsKC3dpdGhkcmF3aW5nGAkgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVXaXRoZHJhd2luZxIzCgdyZXRpcmVkGAogASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVSZXRpcmVkEjgKDmJ1aWxkX3NpemVfZ2FwGAsgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJBCgxyZWdpc3RyYXRpb24YDCABKAsyJi5zYWFzLmFjY291bnRzLnYxLlNvbHV0aW9uUmVnaXN0cmF0aW9uSAKIAQESPgoNaW5zdGFsbGF0aW9ucxgNIAMoCzInLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uQhEKD3B1Ymxpc2hlcl92YWx1ZUISChBidWlsZF9zaXplX3ZhbHVlQg8KDV9yZWdpc3RyYXRpb24iOgocTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBIaChJpbmNsdWRlX3RvbWJzdG9uZWQYASABKAgibQodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMqfgoSQ2F0YWxvZ3VlRW50cnlLaW5kEiQKIENBVEFMT0dVRV9FTlRSWV9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQ0FUQUxPR1VFX0VOVFJZX0tJTkRfTU9EVUxFEAESIQodQ0FUQUxPR1VFX0VOVFJZX0tJTkRfU09MVVRJT04QAiqvAQoSQ2F0YWxvZ3VlR2FwUmVhc29uEiQKIENBVEFMT0dVRV9HQVBfUkVBU09OX1VOU1BFQ0lGSUVEEAASJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFQ09SREVEEAESJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX1JFUE9SVEVEEAISJQohQ0FUQUxPR1VFX0dBUF9SRUFTT05fTk9UX09CU0VSVkVEEAMq3gEKGENhdGFsb2d1ZU9ic2VydmVkVmVyZGljdBIqCiZDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9VTlNQRUNJRklFRBAAEjEKLUNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfQVVUSE9SSVpFRBABEi4KKkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfRElGRkVSUxACEjMKL0NBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1JVTk5JTkdfVU5BVVRIT1JJWkVEEAMy7x8KFFBsYXRmb3JtQWRtaW5TZXJ2aWNlEo4BCgtTZWFyY2hVc2VycxIkLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXF1ZXN0GiUuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1Jlc3BvbnNlIjLC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhQSEi92MS9wbGF0Zm9ybS91c2VycxKrAQoLU3VzcGVuZFVzZXISJC5zYWFzLmFjY291bnRzLnYxLlN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJewvMYKwgCEAEwAToXChNzYWFzLnVzZXIuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAik6ASoiJC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06c3VzcGVuZBKzAQoNVW5zdXNwZW5kVXNlchImLnNhYXMuYWNjb3VudHMudjEuVW5zdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiYsLzGC0IAhABMAE6GQoVc2Fhcy51c2VyLnVuc3VzcGVuZGVkEAJAAUgFUANYA2AFeAKC0+STAis6ASoiJi92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06dW5zdXNwZW5kEtYBCg9JbXBlcnNvbmF0ZVVzZXISKC5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlcXVlc3QaKS5zYWFzLmFjY291bnRzLnYxLkltcGVyc29uYXRlVXNlclJlc3BvbnNlIm7C8xg3CAIQATAEOiMKH3NhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0ZWQQAkABSAVQA1gEYAN4AoLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTppbXBlcnNvbmF0ZRLYAQoRU3RvcEltcGVyc29uYXRpb24SKi5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXNwb25zZSJqwvMYPAgCEAEwAToqCiZzYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGlvbl9lbmRlZBACQAFIBVADWANgAYLT5JMCJDoBKiIfL3YxL3BsYXRmb3JtL2ltcGVyc29uYXRpb246c3RvcBKmAQoSTGlzdEFjdGl2ZVNlc3Npb25zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0QWN0aXZlU2Vzc2lvbnNSZXNwb25zZSI1wvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIXEhUvdjEvcGxhdGZvcm0vc2Vzc2lvbnMSqQEKDVJldm9rZVNlc3Npb24SJi5zYWFzLmFjY291bnRzLnYxLlJldm9rZVNlc3Npb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IljC8xgqCAIQATABOhgKFHNhYXMuc2Vzc2lvbi5yZXZva2VkEAJAAUgFUANYA2ADgtPkkwIkKiIvdjEvcGxhdGZvcm0vc2Vzc2lvbnMve3Nlc3Npb25faWR9Es8BChJHZXRPcmdFbnRpdGxlbWVudHMSKy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkdldE9yZ0VudGl0bGVtZW50c1Jlc3BvbnNlIl7C8xgiCAIQAyoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCMhIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEuQBChNPdmVycmlkZUVudGl0bGVtZW50Eiwuc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVxdWVzdBotLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlInDC8xgxCAIQATABOh0KGXNhYXMuZW50aXRsZW1lbnQub3ZlcnJpZGUQAkABSAVQA1gDYAJ4AoLT5JMCNToBKiIwL3YxL3BsYXRmb3JtL29yZ2FuaXphdGlvbnMve29yZ19pZH0vZW50aXRsZW1lbnRzEq0BChFHcmFudFBsYXRmb3JtUm9sZRIqLnNhYXMuYWNjb3VudHMudjEuR3JhbnRQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlTC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9ncmFudGVkEAJAAUgFUANYA2AFeAKC0+STAhg6ASoiEy92MS9wbGF0Zm9ybS9hZG1pbnMStgEKElJldm9rZVBsYXRmb3JtUm9sZRIrLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJbwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfcmV2b2tlZBACQAFIBVADWANgBXgCgtPkkwIfKh0vdjEvcGxhdGZvcm0vYWRtaW5zL3t1c2VyX2lkfRKkAQoSTGlzdFBsYXRmb3JtQWRtaW5zEisuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1BZG1pbnNSZXNwb25zZSIzwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIVEhMvdjEvcGxhdGZvcm0vYWRtaW5zEo8BChRMaXN0QWxsT3JnYW5pemF0aW9ucxItLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1Jlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSkgEKFUdldE9yZ2FuaXphdGlvblJvc3RlchIuLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnYW5pemF0aW9uUm9zdGVyUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKlAQoQTGlzdEZlYXR1cmVGbGFncxIpLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QaKi5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncxLSAQoRVXBzZXJ0RmVhdHVyZUZsYWcSKi5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXNwb25zZSJkiAIBwvMYMQgCEAEwATodChlzYWFzLmZlYXR1cmVfZmxhZy51cGRhdGVkEAJAAUgFUANYA2AFeAKC0+STAiY6ASoaIS92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzL3tuYW1lfRKfAQoQR2V0Sm9iT3BlcmF0aW9ucxIlLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVxdWVzdBomLnNhYXMuam9icy52MS5HZXRKb2JPcGVyYXRpb25zUmVzcG9uc2UiPMLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHhIcL3YxL3BsYXRmb3JtL2pvYnMvb3BlcmF0aW9ucxJ8CghMaXN0Sm9icxIdLnNhYXMuam9icy52MS5MaXN0Sm9ic1JlcXVlc3QaHi5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXNwb25zZSIxwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwITEhEvdjEvcGxhdGZvcm0vam9icxJ/CgZHZXRKb2ISGy5zYWFzLmpvYnMudjEuR2V0Sm9iUmVxdWVzdBocLnNhYXMuam9icy52MS5HZXRKb2JSZXNwb25zZSI6wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIcEhovdjEvcGxhdGZvcm0vam9icy97am9iX2lkfRKsAQoJUmVwbGF5Sm9iEh4uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlcXVlc3QaHy5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVzcG9uc2UiXsLzGCcIAhABMAM6FQoRc2Fhcy5qb2IucmVwbGF5ZWQQAkADSAVQA1gDYAWC0+STAi06ASoiKC92MS9wbGF0Zm9ybS9qb2JzL3tzb3VyY2Vfam9iX2lkfTpyZXBsYXkSqwEKEkdldEV2ZW50T3BlcmF0aW9ucxIpLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1JlcXVlc3QaKi5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXNwb25zZSI+wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIgEh4vdjEvcGxhdGZvcm0vZXZlbnRzL29wZXJhdGlvbnMSugEKFkxpc3RFdmVudFN1YnNjcmlwdGlvbnMSLS5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVxdWVzdBouLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXNwb25zZSJBwvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIjEiEvdjEvcGxhdGZvcm0vZXZlbnRzL3N1YnNjcmlwdGlvbnMSsAEKFUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZRIuLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVxdWVzdBovLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2UiNsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCGBIWL3YxL3BsYXRmb3JtL2NhdGFsb2d1ZUK6AQoUY29tLnNhYXMuYWNjb3VudHMudjFCElBsYXRmb3JtQWRtaW5Qcm90b1ABWixhY2NvdW50cy9wa2cvZ2VuL3NhYXMvYWNjb3VudHMvdjE7YWNjb3VudHN2MaICA1NBWKoCEFNhYXMuQWNjb3VudHMuVjHKAhBTYWFzXEFjY291bnRzXFYx4gIcU2Fhc1xBY2NvdW50c1xWMVxHUEJNZXRhZGF0YeoCElNhYXM6OkFjY291bnRzOjpWMWIGcHJvdG8z", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIrgDChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWdyYW50ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAcgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50QhUKE2FnZW50X3JlbGVhc2VfdmFsdWVCEAoOcmV2aXNpb25fdmFsdWUi4wUKDkNhdGFsb2d1ZUVudHJ5EjIKBGtpbmQYASABKA4yJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5S2luZBIMCgRuYW1lGAIgASgJEhMKCXB1Ymxpc2hlchgDIAEoCUgAEjcKDXB1Ymxpc2hlcl9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjMKB2Rlc2lyZWQYBSABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZURlc2lyZWQSOQoKYXV0aG9yaXplZBgGIAEoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXplZBIzCgdhcHBsaWVkGAcgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBcHBsaWVkEjUKCG9ic2VydmVkGAggASgLMiMuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZBI7Cgt3aXRoZHJhd2luZxgJIAEoCzImLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlV2l0aGRyYXdpbmcSMwoHcmV0aXJlZBgKIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmV0aXJlZBI4Cg5idWlsZF9zaXplX2dhcBgLIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESQQoMcmVnaXN0cmF0aW9uGAwgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgCiAEBEj4KDWluc3RhbGxhdGlvbnMYDSADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCEgoQYnVpbGRfc2l6ZV92YWx1ZUIPCg1fcmVnaXN0cmF0aW9uIjoKHExpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QSGgoSaW5jbHVkZV90b21ic3RvbmVkGAEgASgIIm0KHUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlEjEKB2VudHJpZXMYASADKAsyIC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5EhkKEXJlZ2lzdHJ5X3JldmlzaW9uGAIgASgDKn4KEkNhdGFsb2d1ZUVudHJ5S2luZBIkCiBDQVRBTE9HVUVfRU5UUllfS0lORF9VTlNQRUNJRklFRBAAEh8KG0NBVEFMT0dVRV9FTlRSWV9LSU5EX01PRFVMRRABEiEKHUNBVEFMT0dVRV9FTlRSWV9LSU5EX1NPTFVUSU9OEAIqrwEKEkNhdGFsb2d1ZUdhcFJlYXNvbhIkCiBDQVRBTE9HVUVfR0FQX1JFQVNPTl9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRUNPUkRFRBABEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRVBPUlRFRBACEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9PQlNFUlZFRBADKt4BChhDYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3QSKgomQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfVU5TUEVDSUZJRUQQABIxCi1DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0FVVEhPUklaRUQQARIuCipDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0RJRkZFUlMQAhIzCi9DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX1VOQVVUSE9SSVpFRBADMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWVCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJQbGF0Zm9ybUFkbWluUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -1276,12 +1276,20 @@ export type CatalogueInstallation = Message<"saas.accounts.v1.CatalogueInstallat } | { case: undefined; value?: undefined }; /** - * Active team grants at the installation's authority root or above it, so - * every team whose members the grant reaches inside this installation. + * Every team an active grant reaches inside this installation, in two kinds + * that are never merged: granted at this installation's authority root, and + * inherited from a grant above it. Both reach; only the first is exposure + * granted to this installation, so a reader is never shown inherited reach + * as if it had been granted here. * - * @generated from field: repeated saas.accounts.v1.CollectionReadGrant exposed_teams = 6; + * @generated from field: repeated saas.accounts.v1.CollectionReadGrant granted_teams = 6; */ - exposedTeams: CollectionReadGrant[]; + grantedTeams: CollectionReadGrant[]; + + /** + * @generated from field: repeated saas.accounts.v1.CollectionReadGrant inherited_teams = 7; + */ + inheritedTeams: CollectionReadGrant[]; }; /** diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index 401a89741..ffc5bd3d8 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -3701,6 +3701,7 @@ "@bufbuild/protobuf/wkt", "lucide-react", "react", + "@/gen/saas/accounts/v1/authorization_pb", "@/gen/saas/accounts/v1/platform_admin_pb", "@/shared/lib/read-outcome", "@/shared/lib/utils", From 6927ad9e12bc0221340a5fc6604a6959521bcaa6 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:21:00 -0400 Subject: [PATCH 04/11] chore(scout): pin the frontend route set, not its size (Refs #1020) Rebasing this branch onto #1007 showed the failure: main pinned 55 routes with /oauth2/consent, this branch pinned 55 with /admin/platform/catalogue, and both edits changed the same line to the same number, so git merged them silently at 55 while the set held 56. A count that agrees while its contents differ answers about a number, not about the routes. The discovery test now asserts the exact set of route paths. Two branches that each add a page add different lines, and both survive the merge. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../pkg/cataloggen/frontend_plugins_test.go | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go b/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go index 071369b8f..c8294e77b 100644 --- a/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go +++ b/module/services/accounts/code/pkg/cataloggen/frontend_plugins_test.go @@ -40,11 +40,73 @@ func TestFrontendPluginCatalogIsDeterministicAndCurrent(t *testing.T) { func TestFrontendPageDiscoveryPinsAccessAndMatch(t *testing.T) { routes, err := cataloggen.DiscoverNextPageRoutes(filepath.Clean("../../../../frontend/code")) require.NoError(t, err) - require.Len(t, routes, 56) + // The route SET, not its size: two branches that each add a different page + // change one shared count to the same number and merge silently with the + // wrong value, while each adds its own line to this list and merges both. byPath := make(map[string]*catalogv1.FrontendRoute, len(routes)) + paths := make([]string, 0, len(routes)) for _, route := range routes { byPath[route.GetPath()] = route + paths = append(paths, route.GetPath()) } + require.ElementsMatch(t, []string{ + "/", + "/admin", + "/admin/api-keys", + "/admin/audit-log", + "/admin/billing", + "/admin/billing/success", + "/admin/datasources", + "/admin/entitlements", + "/admin/invitations", + "/admin/organizations", + "/admin/organizations/settings", + "/admin/permissions", + "/admin/platform", + "/admin/platform/admins", + "/admin/platform/catalogue", + "/admin/platform/events", + "/admin/platform/feature-flags", + "/admin/platform/jobs", + "/admin/platform/organizations", + "/admin/platform/waitlist", + "/admin/roles", + "/admin/roles/{roleId}", + "/admin/sessions", + "/admin/sso", + "/admin/teams", + "/admin/teams/{teamId}", + "/admin/users", + "/admin/users/{userId}", + "/admin/webhooks", + "/admin/{*slug}", + "/auth/callback", + "/auth/login", + "/auth/logout", + "/auth/magic-link", + "/auth/mfa", + "/dashboards", + "/docs", + "/docs/compliance", + "/docs/sdks", + "/insights", + "/invitations/accept", + "/legal/privacy", + "/legal/terms", + "/notifications", + "/oauth2/consent", + "/onboarding", + "/s/{solutionId}", + "/security", + "/settings", + "/settings/data", + "/settings/mfa", + "/settings/notifications", + "/settings/privacy", + "/status", + "/waitlist", + "/waitlist/verify", + }, paths) require.Equal(t, catalogv1.FrontendRouteAccess_FRONTEND_ROUTE_ACCESS_PUBLIC, byPath["/auth/login"].GetAccess()) // Public so a user stuck behind a blocking surface can always end the session. require.Equal(t, catalogv1.FrontendRouteAccess_FRONTEND_ROUTE_ACCESS_PUBLIC, byPath["/auth/logout"].GetAccess()) From a1f7bc186653e8ecd1bfc96ee7fff15baaf3b6ba Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:43:27 -0400 Subject: [PATCH 05/11] The Catalogue's authorized state is the deployment contract's inventory, not a host copy of it (Refs #1020) What an approval approves is an execution inventory, codefly/execution-inventory/v1, whose one definition is the Go module github.com/codefly-dev/cli/contracts/deployment. The host no longer describes an approved execution in its own proto: an authorization names the approved inventory by the digest approval signs, the member of that delivery aggregate the entry is, and the host's own build_incarnation, which is not an inventory field. The inventory's canonical bytes travel once per response in approved_inventories. The observed verdict reads the approved member's containers only through the contract: Check accepts the inventory's intrinsic form, its digest must be the approved one, and the containers are the contract's own seven-key projection (Rows) - no traversal of workload templates is written here. It judges an observation keyed by the inventory's own workload and container names. Init containers count; an observation missing an approved container is incomplete and never a match; an inventory that is not held, does not hash to its digest, is refused by the contract or lacks the member leaves the verdict a named gap. The test reads the contract's own base inventory through the contract, so a copy the contract no longer accepts fails instead of drifting. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../api/accounts/authority/contract.binpb | Bin 956621 -> 958992 bytes .../saas/accounts/v1/platform_admin.proto | 57 ++- .../api/accounts/connect/contract.binpb | Bin 956621 -> 958992 bytes .../saas/accounts/v1/platform_admin.proto | 57 ++- module/contracts/api/catalog.codefly.json | 4 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 51 +- module/services/accounts/code/go.mod | 1 + module/services/accounts/code/go.sum | 2 + .../pkg/adapters/platform_catalogue_rpcs.go | 5 +- .../code/pkg/business/platform_catalogue.go | 113 ++++- .../pkg/business/platform_catalogue_test.go | 135 +++-- .../testdata/execution-inventory-base.json | 269 ++++++++++ .../gen/saas/accounts/v1/platform_admin.pb.go | 462 ++++++++++-------- .../generated/openapi-raw/api.swagger.json | 57 ++- .../accounts/openapi/api.swagger.json | 59 ++- .../saas/accounts/v1/platform_admin.proto | 57 ++- .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 956621 -> 958992 bytes .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 123 +++-- .../platform-catalogue/model/facts.test.ts | 19 +- .../platform-catalogue/model/facts.ts | 28 +- ...platform-catalogue-page.container.test.tsx | 16 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 123 +++-- 25 files changed, 1195 insertions(+), 451 deletions(-) create mode 100644 module/services/accounts/code/pkg/business/testdata/execution-inventory-base.json diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 6a532d3e0d7914c9388b701a207a035cd97272b7..78a3928696ef00a6995d088a65d2e89a6a7bcde0 100644 GIT binary patch delta 14599 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 delta 12230 zcmZXad3;vIxySR)%$dA7=Rn9w0vJVJWeX<U*7Y*Ig^!)Z(qZ_tU1DYFvQJqHXWz za}P*lRkW>mq!+L*6;}kb24xcnpe&VL5Tm%TtAK1ODEBwdY&7@J=XYkFGtc*V=9zg< z@^Ev%)wBAIXv{UTM&2kIgGSj{+?a1HX$%{qSL5dfu!GB*UKkMfFFXI%FuA;LSlwNN zZW~^IBw#I z@LXTKA{d{uRG24G{9Conc^<{P-y|U@-JK~V# zHZ|RFcl?7~?ibmFf$^!?fXQJwO>0^?F#a$v#^%CK+0VwumDz&vaXwplZ`{!I_3vYu zMDst&g~drwR5ZRgTYFZU>zaNsA@1KheI^$&ab_ZCvZ7a5t}4qF{(q!}hM|RfZ?79x z7&3g|(0+xwfrX!5er z69KEB$1*i`4hv7UCe}NLoo=V*WFlT?qMezFv9M}VGdM?fS$AtzqEZuqd<2MuZXV?9b{Nc}wxLsh-l zQgYh_wNkKq!qi7sym5(*tSj+uW~`VZgP90enl~(rZDx7R8zxrQe4Pc}l_}s$8Pil? zl2uD_!y>b?hcz*GDeF}e?o`{STOwDoqSe_O3*(P}+6^OD+7)%96|J&m2Qv|{D##5i zj9tY_O05a6t5{MV{v1xbCvpud+LY~mGwz>u$G|me>8k3wtEyYe*hZ6I*m%6wM%JAb zA(C}WR1pQ3Wc8+(z{1#d?9^&&;+^YQjgGTBL0IpnVl1pa^=7c5u%4Z&ivfc%cRj1M zo>f_Gu%*$Rp{!tOVz4lE11mezcw(5kfpzO1UW!nABytNY`XD=XaePtQ0|U3%4c0@2 z`W|B~CREsX{GP?zgYa&(8w_S5VCgLc3uCvkQ15D(c;{BNsZ}rOL7~2HmkToyuzG0_ zFv&Kh3=?zTSKDAZ5+1Nbe$0wKW7*)P@!!*rSdxf6UM3)d1WR77V%LQhy6j2K_2iCPH>AEWEc>_Rab*qmgI+=o zZ`sbyIoFsfu3e1PhPTsO@Niewb$NV8=UuGtS!RsdwtE>nI~-195b9o*y|g^Or`=w5 z-uJ>wkfQ2D?qfxxvUA@;58TJ{r%)wUtMu$=?4U^xY`EHQ`(HJs=YUNQ%tXM_`w$?9Vt!siiox~^_KgwG?kx~rLLd`JCy@DNsCGgT2h%F1*f z#8`UTyAMH1tMjOruNgb;D5+?^W_b%j!1Xswv^T2~WZ6{-jQxgnu&WYq{f%E$fT_p) zs&K}^(luPo0!%%|x|ZoZ2pe;csjl^8jxg~=o?z@g&a3{EO)>U*a_V1ewcXW1diRW+HX%)UJr8i zkn^mH@j;$Ps8^xNdQ$05^sey~*7s=nct~a$?L9RXt|#(y&kOgclcZN7C-I`m*$b=V z-FHml=9H4@jr*JoPbALk_2M)|OHUGZCNH+a89s=D3`LEK^9jzL&RX7yA4s3z=B%!vefuOgHx`(QfThbC7MwoG zEA`O=6HcGxI9k+}uc3YWlw|`m5wPr3wQrx|=2XH%dWxwxAkQT=D$&ny_H1@wO?>^$ z&+xnrLv7gU-X}IJEc<@7VW;yli&Aaa>AawexrPN8JcF|^+`{k<*f5)>u8n_{OK0#% zpG{EVV+QZ6H!@84n89_w#vBPBFDoDF77UXtYWT_`(|J<+?q%LtcN08?rJG8>P|{X%|* z&SF~2RE`&MmJno^Wa*m)EX-ZR%k|9yCgv{U=4MgLR8wBeS*4kZ^~2JaVm!pu#k{@V zOfWHZG4G=LVl8EF31_K&Ej+=ruT{xh;$N$hxx~L#HPL3y%FS!hL}BT=t7WQ*HuDPI zL}6lTvudJhlhm?2rY_}7pFl8EGW9NjiMdO82aN_M<}S4xtd^Bv?lKRmge>d01XGv! z6_;S@GTy0kxKW)zwQ7T{;A|C6pyphmf5X7~p@l)eE8H?D+x>35p`{h4>^8KT|*u) z_QULy_v0UR`;g-Ts1nnMHt~mQJ!s)fVh?}H@r5B*l@_wH|BfrN11<5;@=w*Nd{!uA z5tbo$aCVSq_kIxfNq2BbXMuWvjlF{>#6p=^$`0Q5vamD^lqnLqlZyjfhDn$wVa`O{ zUv~2701v{lu*7OuEFookxJ*04TWDt%W87Zew&vt<`9yri<#{Jd zDDcm@tUXymIex+AxhG4gvVE;4qH6YY`F#aWM@$nYhOCsGnPvlCY{T zdEo+e_rNslm_wY66?CgnHsU?xDXQE5Ax}}Y*h9SU6^3FC6c2MTN*Ic5SWXbe!#o-# zvH@?%3)Av2Qf`%AoI}xkqbf>M(jwk)jOVM(vPlD_Z+);Nfzo%L5=o$R!c!s%ltu`L zS4k-crjbJSJK29}BJLCN{F9<+6k-gbD5t#V1fi3SA{rwCw^$`&58N;0Isc-eF^(3p z)BwfZe4JoYEzp#%6XS#|H+fPA&Nxv)H&uX9hsZc_?lopDI2Yu2As(|t8Ph<=i|8>d zMb{;1$kPPj>XIaQen7aoBuTaO2eY+vWkvds$oDYorbntg9~KTgX#oFc&yOU4|BGKmb1T;8n1bg_z>NGXs9Ih^9F3S_JsNsu$?W(Hg-2 zuLqCaJ58|J7CfED(}bj769Ap!ND zo-}~}n+H!4JU`*VlZ1xwq>$f3MXBc1S}SUi4{Dq0bOvmP-RSX_z6RSuPD)L>gXJ z-gFwYhBVAnX*kBIqrlr-!Tu?D)gE5BVc4L-*WNw2e#p?KKYSYR$)$6Jq>C7!2t+69PR`Iaus-KpJw;>~la8a52oM_^KqD{;fyCkp+;YAVg*L>?6E{_Kx?;vVTi zUz(~g4*JqmKOPSH(kuqlLqd!S?A@$bGd(1tQGq=JNdwcvo+(N2d_>5au!8PkBn7x5 z9u=uPCW_7aNe1fU3bk%!B&l_M>kF1NQ2oxcAqh}V2zl1Y&H!~p;EqHZ1*jtfcNZz9 z^AA+-Q^Rz{B2fI9Q2f$4qH71F=-=I{l!R3^28EkU!R8Y=I$(bd?2S$Rni?HQx@Ot2jt{P5;5)y*dlxMKn_$-&KQxMPB7l7mYc+>Q<01%o8G9v8R^21%ee zK5zv~5*6$NLB7TWp~V!F9}L`egG8`>$Ri>NY#$EXb%P|Z{gY=)64?H^)kL8EOMsi1 zKIUi?j=4tyd(9hwZ+rOa6^O|QUElNartLl&M3WJ_E(9$iGg%-n`4HE^d|3F4d@xKmC2LY*31_%qXXOR$D%fq2p3FVXm?1<{KRKWV`K zxW`WtJpaw(CkgnU2xPgvXOKqxpUN)%A}&v#_QdHKh(F`Okp$w?J#msi{8>+&BoKeD z)kF|~K5!Rn8U^AnIO42C{h)jaP1g}GF~naAqM44k7D0SQmfMFlzpNnZUngNAX9sMF zt9A9ue0JcDxDZ5U2kwYdKh$Rj7hi8!hhY6mAQm~+p=SL`5G``7NrTr{9qUf`S&tqL z*Z=UaNdoNGvYGqhigZqpuQjznPgLW0-J>QAsONfOBmwn2kD4T)p6^kU1k?*!O$6$N z9(8E%(r+r(x|bkWu{aRdn)(k-hh7{+*SLkMUZOr81%;~(-zbsI0bA?%s;?H!o^OP( znmu3j5u-W46~goqd~2C$&v)Jt2|kfKz4Pji6Hw=MOIT(-*(mn;vz#aG8ESY(S>eR zpqRaRAQm0g1ciR#AE_C{iQE{lj{;tGM@jWL%{_1D5Pe|AHNdxOAJu#BN`YF$v zB(Q$kqb3Qc|K4gMP(Pzk>xx4}B7IhhD@}d3GxhzfjIJ;MwS-kYD+`yK!?P@r&r8-M z?M1u{@jfqQTjRA1qo0?h^e+a0T?UHJ%kSS{D3*cZ3sU^UQcPKy#`uDa{^2l^2DdMI zj3mMJOCBRhz&Jydu}(NiisH+jB59yF)59PM6lclQl`Lt%INM_+2^e2#H4_+L^%xP7 zcuflXrM1jZd`(6dxK){-Gq1_Q`G#VW$T^ZNO&!Jr@y?MlHW(8GF-Nwcza0T%0*rIy zIX^ZS6JUH@iX|4K8mzwKzb>OCjv{IB`i7@S5(dw;6jSy00!#(Qc^)H43gdi_ku+dj z;9-yij0=4+k_3uxdWs~0;-Xd)L2)to+iFBeqS-fF$Y;YLMHOA#K+Tbvf2Dfi{ zj3mMJ+ZH2S8;UEW>}ft@kVHLomBhc!=nB;$=B}omn$nkd62bNzPmLt7UE|r31h#8i zjRM{k_%&z_}ekW~_?uSVQ;%bqnTx#f70C$V*Kin*%0?YV7id~L&g{J+1jCMKN zq=EK_o;FGFyv@@l2{rrCNgb3lRkR=bq9qMrKk
0@zPISdsvCy9Y}W!0u=@5ny-v zt&fQC?MaHOO$V(oMY~%@R~e{U!m4)5!j-0ID-*d_vaejxRwCZLQqmulQKgj_y;pX+ z7NgZ)xGH%P?3Fi;%o&W8!1$RIM+`>n;7ZN$GZ`Im97%)U&pk(y;QI^DktA^3=l4EI z3gdo{ku+dD;AxNqj9=n}w|~SV4IB@8jwFHOp;j}&@v!HJki=0beq=CK8jMF}bgjXt zC9LYGEL;;luIsRUB9F^F8&}M$CgyN`SO2d)BiCkBdR)5edwa||F5UILJxcPpbl3Ox zVEL^SV~Xvqs=Y?}t&GML+glZBK>3|VNfJDt@F+P$x~X99irRb<)7{zG8QQ zA_*)<6+4t90cB&cyFZZxl=ruq2$Z9XT^mJ2qIztx_@QCh-mn~79NEu0TEeQvj=71q MX}XEGeSO;h0e|!&%>V!Z diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index e6830ddb1..96b2a0010 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -300,13 +300,6 @@ message CatalogueRelease { string version = 3; } -// CatalogueExecution is what runs: an OCI image manifest digest and the build -// incarnation the host assigned to it, the same pair a Work Context seals. -message CatalogueExecution { - string image_digest = 1; - uint64 build_incarnation = 2; -} - // CatalogueDesired is what the composition declares. message CatalogueDesired { oneof declared_revision_value { @@ -319,11 +312,28 @@ message CatalogueDesired { } } -// CatalogueAuthorization is a current platform approval: the authorized -// revision and the execution it approved. +// CatalogueAuthorization is a current platform approval. What it approves is an +// execution inventory, `codefly/execution-inventory/v1`, whose one definition is +// the deployment contract module (github.com/codefly-dev/cli/contracts/deployment). +// The host does not re-describe that document: it names the approved inventory +// by the digest approval signs, carries the inventory's canonical bytes once per +// response (ListPlatformCatalogueResponse.approved_inventories), and reads them +// only through the contract. message CatalogueAuthorization { int64 authorized_revision = 1; - CatalogueExecution approved_execution = 2; + // The approved inventory's digest, as the contract computes it over its + // canonical bytes. + string inventory_digest = 2; + // Which member of that inventory's delivery aggregate this entry is. + string member_binding = 3; + // The build incarnation this host assigned to the approved execution. Not an + // inventory field: this host, and only this host, assigns it — advancing it + // whenever the approved execution changes — and a Work Context seals it beside + // the image digest. The verdict compares it with an observation's + // incarnation, which is the same host-assigned value as presented by the + // running workload; an incarnation from any other assigner would make that + // comparison meaningless, not merely wrong. + uint64 build_incarnation = 4; } // CatalogueNotAuthorized is a known absence of current authorization — @@ -350,11 +360,24 @@ message CatalogueApplied { } } +// CatalogueObservedExecution is what an observation found running for an entry: +// each container's image manifest digest, keyed "/" in the approved inventory's own names, and the build incarnation the +// running workload presents — the incarnation this host assigned, sealed into +// its Work Context, never one the workload or another party chose. +// Observation is the observing consumer's record, not the inventory's, so its +// shape is the host's. +message CatalogueObservedExecution { + map container_image_digests = 1; + uint64 build_incarnation = 2; +} + // CatalogueObservedVerdict judges what runs against what is authorized — // never against what is declared, because only authorization admits execution. enum CatalogueObservedVerdict { CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; - // The observed image digest and incarnation are the approved execution. + // Every container of the approved inventory member runs its approved image + // digest, no other container runs, and the incarnation is the approved one. CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; // Something other than the approved execution is running: what cluster // admission refuses. @@ -376,11 +399,12 @@ message CatalogueObserved { CatalogueGap observation_freshness_gap = 4; } oneof observed_execution_value { - CatalogueExecution observed_execution = 5; + CatalogueObservedExecution observed_execution = 5; CatalogueGap observed_execution_gap = 6; } - // A verdict exists only when both an observation and a known authorization - // do; anything less is a gap, and an unobserved entry is never a match. + // A verdict exists only when both a complete observation and a readable + // authorization do; anything less is a gap, and an unobserved or + // incompletely observed entry is never a match. oneof verdict_value { CatalogueObservedVerdict verdict = 7; CatalogueGap verdict_gap = 8; @@ -478,6 +502,11 @@ message ListPlatformCatalogueResponse { repeated CatalogueEntry entries = 1; // The registry revision the solution entries were read at. int64 registry_revision = 2; + // Every approved execution inventory an entry's authorization names, keyed by + // its digest, in the deployment contract's canonical bytes (codefly-json-v1). + // Carried once per response because one delivery aggregate is approved as a + // whole and several entries can be members of it. + map approved_inventories = 3; } // PlatformAdminService — cross-tenant operations for platform operators. diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 6a532d3e0d7914c9388b701a207a035cd97272b7..78a3928696ef00a6995d088a65d2e89a6a7bcde0 100644 GIT binary patch delta 14599 zcmZ{qd3+T`w#Rd;s_WkLZ3yWkh*6*gfnY*#8y7_9bAyub1}zpgr6 z^{rEY#Y3jQJmjXfL>p`4ZKBO-lWobiR9kVI+m>G9J$f};wx#_aS9_P0w*P6A z*P0NoCfx3YMa#YJg->$csh1CQyXBh3W}8~`&Ex)DpBtHNY3;#+L{;$>C9!@1$$me# zYh$)?c((b^!*fmbxu%glI5q5|)n90_FnE;Ly-eVVxT4$eF}a5NOPcC3%}ts1#_K(o zoj;4YmBTWvnTGtxG1=b^Z^<@~&DLM|mu%gb)?B{H58Y$x@=dLoToc}tYs`$yno+g% zNCdnped;d@FOKq#D?HokrOt`F)eejLT%x+@0k>S()Y?4mii+4@$7gDogL+h6jKTbe zu7&FkcR4Mwqx~-p-kFKSp9+f_@yZi+I^~7wH+U&}dHdc*@BNfm$K0NU#1yY*;iIu$ zF7W@?EzgV|-JHjy{u}|!Wm|ws4gTW7oU-EXg*R@7lP*)d(hF+OH;pebi-5Lz*kzHM z4o@mCa(n!H2Q}FKo_B>aHq$UBJA|E;XuteMPx|iWFA{FjcZ!N0Y0pgY?jDr>h08=A zUoK=tPZv&p+B-Mwlkmktmg?blb%p4YD)Qwb#-3u_?czGh#^XgS)mxja$nnKuCW>88 zwPr7B2VB2y26DMrB%qTx(!ft|iE|1eqo`$VSl}L=iEr zCb+IHU!NV(Fm7=CbWr?s@YqwXs|ju#m2b%g`KD}uhPkGk+8=P~2Cbu%G`b-_t`QqI zQ1RWI3F}&e#(e#lhHOwZGS@n4%<$T}eB)sAmO=H|v4iUxaY8)Ivv?zBR-dmMqh2^f&9N!dh<0iuX0{{i?d;rO_&7aE z${eOirXgs_jcjVE36Kw(5$Mz;p7ekg=uP>iTpbz?AJ>ZK`KG#TFgn}pVlXYE^G(1} zMVl?Gjdn-JO*XIwxa-k2Q%}AdU3h3vBWcc$899nx7tO!cb<-{veX+(bMzPeFBK#%B zk`zlYeE~~HI4rb%iKSG8hmL@afOshi@xCm9-Laanlsgi;114F$Dt}LCqv2Xcn66H@ z7_=r44Gjz0IC_klKR6?iG~+K;y(63uPcZ|vOYtUpRq?TriD9i~$M-ZQ!?4zGP z!uEi5eB}j*)?D4FK>5^lO!P{OU=6F(sh}_87{Piqg0DFZ0Z(D}uH4HB4V@8qF&z>V`mvd&Ivi`j z?PBv~o3e_1hY+ui-x@Xrb)zy(I4uL}im1y3*W{aT2!>&QW&hF|v}7|42S;_Bm=Pi9SGHg7@t}_%tei_1)Gs3 zH7Eu~XMu{W(eR2D14F$hv6sd)HK}Rdi19Y$Gxg`Fmv!(SjA+g`LK_$4;k+46Xc3yx zVN64P(3rU)`$xPs14??>xfUd*#%xA;ZOFD@gRA_}m~GBt^WE5-ZyFiLs>-SBS#hO_ z6qQrgv!1=&y$R&h6Mean6}?kfxWe1@+Y`}pW3)3*q@B4*1(7;?VEO`9Ik|y_wwqXS zNo2z7Cgzv9m&0j4Uv6bZ?-$-*>0K7~L&L3V3#;?4pE~chG4`H0SzzPwHoe0g(T^43 zblA>BCA||SS%c^$u+VlpJFY4+@y_k6y2hC5ynEMnMO#?4+2|?R$LV9JA4{O`yR0Vi ztPZpt(V^6jVaLK6V7jWUwu6=SiA;3e!TR-g&&O2z`*Jrc`l!(DS#N0A9}Ra$M|*!Y z)jf>uHB*I+$9p2Y{R!^}(a{dm7qIl<4GV2QK-os`go$^4pcYM~pZ-iu^}}emFv&Ve zAMG&7Hisxo^!-pBz#nl8cz`dzWJO=$5Z%Iq|Gy|yVSb~^PCwlg!HE1AmF`VAkH{lb zn^bmc9yi2AhR9%psGLf*B=VWweT@jy5vX$G?0Z9Y95zRvq0x2-a`op89vXLa>8Lxe z4|d2;b3vSL;5vU}lRgo3ropxega4G*kO!jS#;-FQRA&;kn>%jBe@}%qlv*(29BOuC zh1QmzBp&QNN?W0-8JH92aPpOtsnbDX+OGqzQiFSZGC)8Ox0HN?DOX<)ZK)dSzV->W! zVEO{qsT5SO(Dnf9uD1kCgv|kV@+s!2I%*F_$zXsAIaoScVUgL}K%4R=5nHdSPaJAL2^e!C z#J&&)I{B($V;0}DQ%_SAVX{Q2FMedK#vMhoz|$WKe|X-zzW0x8@X4l;O4#E$JH;JC zZNPFoFYI~V8{2I>KmBL!dDz8OzMRO5?k!yZ0&Wx&dFoiou2m}KOycZTu5S~t;c5~$ zw}~oBIg`1$O~CX8EM05CLfgsQ+$Lb+y_31QO;l0JnZnI&0;VruRjUyX6fns)w+Wc& zJB8ymaW!&&RSLI>shr)x3%`BQyDFT@Q++9@s+dZ<)41qK@d%Tw+UOyCPUF3F#>PYV zoW@PYu43vMal0K49>N-Ix+;RV^HP1QKwEk`sFomwRVf7Ibk6RKDXG~^=cx#Ufa@7t z^fIFoWSwDL3$&fVd+Tjc#ROc>P^+R6a}{&Yb*3E^_6#h2K;a>}&g3VS>T?A)`p)Dh zRl92t(!j)hS>Ws*&MTJ{R=?!+J&n%#hAfJ}4s{6f~OockZj zCWUURyuqb;l=?hpo z=3&9<3SOZPB$#lzf}@sF+ozf~^~#71Okco?x~fgRlAAIN59ukoUWs)ssa6a99A__J z_Y4cJD!kCuttj+*%{%kj=Xffbk=oeLTld)5u%e-;jr}|?ji{=P{X7qJTvsy(jbGu+ zb+<6Q4mQm8g|B&kNQAHOw64pb!pAGTx8CqD;o}vq^E&!S_*kQSsCpVES#&Zei_F-Q z+Jb9%Z=DPA6rR@bQ%-ePVIPSaU#{mxpYlZI5*D;idEFbD`2G55nl%&OC-h z0ZR`N7979DEA-73CLF)T&CRuj0%C&-h*s)~wi^n))_G%3*uaas&?2D?x@_RxO5ICU z8x;l{Ion)Vy3YGmxRIyyjkJa-M4PzKvxP}k^jZ}Ln|NAZCh!n0H}PY7m^Y~0{!X+` zHR>TOeP+Ny*LQenG#?cP@9-1!Duy*ot??Gld^33+c*oO*J2Pbqt~j%+-6^^7IT|@ zt;(m{IV&?GMm~k5N>wF3mUAL=zs`g6_OQGw#YRBr8PK9LZ4TGoX`z}95 zqk)OO??!vAh83gl4hyQ7tjKXOy6&(eE=JcKyk~EBqdKW-)Lz@g*&gh*mlMHd_->4H zd<0Ib!guSvRoCuPxlj+##Y=YEm2}C9kh*xuZeAKua`BSgyibHQjiuYCcFS6|<7!wM zW_ypfy~`5eKAw&`rQu^A5A=>hBly_I2kXU*RmzyJ zV8ce$%2{pYUzw?{Gb(t~oN85knGWYlM- zJU-Z}Dyl_Y+cK9n&;?d~9K}cie0tR%=qRRht>!xlm;mJWyh0aAFaiC0RgdC4WD;5M zW>NG=;jVvqQ_F4^C~Qu4g)GAI$q9l@7lqR|dbQyMA?Y|(4}f@r@F|9r32zg`;9s~U zuA@xh%Qhiy7t;0J6bVn?j{2}oq;D6FTj~}^8U}TXko`MK@H|n-ejO!*`XnL4-tgu| zJ(FlRS#+)L*e->-UC7frN=OX13R%-p!ZyB5$kRGX*qu{5b;NF)CM4C(dPX9=Lx^+Y zUW?W~)5jNgi1d(nR7%3A?hwJ*s(eE?WSSX*%@=f`Q8wl~!%|e&s~MJ}I+|vP!50{c z2~eCV#6uCqkR=3RoGH=|#TZG0+dC~rlHhul#Yhq`-X&yjx6Dneu_$NB1a@daVEbyc zT14NwMXJiol{BCY@3DwT1KWEoTav)`KFgLQu)V)iN3eZB$RQp5m-ymAA$bJrrcx2qTEwa^;4KW({V%dZq{eaxL|U?J%i2hbtT zZ?RHzr{fPc)x_e=-x%;5V|hl1wNd;tuDL%W(raVxq=EahmODxC{G8=Z61YDvz5|&}LT?Ue{46oQ_APLK`M#x$d>m*?rUR9|j z)QONZEW>Md8PwEY7vjx$8Ms-7*G2lxcp0>aWq3n*)61YWEW5dupm{XLv-2^Gx(R8mc%V(&rj{TEeJ~h+wEm*V30q1)JtXr>44q9JT3M zqV-XmuGM|ys7=?nrph0LxXm%yI>g15PR$=g`ZgyjK1hSxA1y|b;QA&fEc%?VDbznO#<(YMWsPfgN*Hk|0h6$xozJIRSF5|Y4nvJ+P%B!TUeP94GaRwu4V z)OCESBYtDhN`rQ)lm2x)DixU(D5s}7!DVh0g=P{T4;}V!;mn=hfN;7Kho%aQ=}sJ) z>ZUf`i9<69s52b#Pz04FHPabR`k|O9X<$0ji3>WC;Q3BRR=ed?ypiOfu$<+D@@^+- z#!oU(&sL~)DkDja>mDaA^GE~Ldo3H10QEjcp4`zHpx$qxQY%1xphHJG|3LLY)l8QH zn5dZJh~LMK==6h>{;zmcO2Vk-IKee0U{k)F>#+7XVAa)it|RHTf_;uJwlFK{=B7-r zo$LJkS~t|mCk43k9PwlfH>KgubJ9=7a7lyP`4%ooaJ|3=EJ>jFhz(ehC}1CTQq^V> zS`5+o?>1sd1lz|fB9g%NaT~BCf$bBPElFUzuv15%UF4uJ)5jdO!ZEkliORgI@lDm2 zPdQ>4rmpL}lu5f!Iq7AXyAA{`Vr7;%^1PpDNV`iNq}}rO_(=l()fPWV!2e3ejv;a$_^)x|GMk!# z|En>7rl!8u5$j_7#RmUcC%rDluSMX0-H|866ELUV8w$U^?6~-@=dkznE(j6m>I=zw zn_^tdcfCz9>VwI8=iDpIGPoGaTaMTfFN3R>;Vmb_2Dg@w$=`E=UmDzLU+#3+{y6EVZ%I32+96A0 zUOO#r^=)aV&FJ_(C3iXE(-?PJWkZFju@&bp3c$+<7ZC#9D`9y z7}aM^FvR^IN*bOo4>;@xhgV)76#jd>S6+DRBk#KKfFtQUE5g`AhXc-W(We0QY3+b> z{+~?T;v=0r=!l~+NKb=2=%kOvAV~wruPjKC;Q5dRNfJPQjr0*6f}|;shb>6b0P-8l zgCv0b)`BDnAdgs(Bmv}iojL;K_o}j(i0`AAa2H=X<0ZRY7E1PcZQ*z+=?56~1Opi_ zkB%(Zx&XxlDW=KjqTEHJm>|>BWK?;P1{7^Ft`bOs=UZf4d65JZ6QxB# znnE#2#`OScKrvayMHWdwF-68j7D+&HtBi{*l7Qm2P91?_ssxIwh(uSEJzTN}3bQ`( zt_){LNx#JaL{|)BhCGHow<;4LX2`QIHz2wK#7rsfi$QeNAZE(+eK82q0OC#yf+TpJ zWkHYx5O;L|L7D>bR||qPfGF7ckOUBSTM#4x#B2+KB!IZ5Q%8WfSH_zQGZI7YmtvU7 zQC-dEx?kd7g!L({C5-BR8JzF_RfkfEFXu?MtkC;2@2+r;jF03Jw3{R2Be?|p=15#L z^;Ryy+a8wqVZEy>LAOMlU~FA5;jx@6#o`!miN-rurWeO}Ndw+_G2WhVgsg-&&$oO@ z0^bFN-|hFx!@tS6MxrN*@}n{?>PZ9Te^^)~f%0RPGD)EPxTQ=IC_mAuBPcIaly!+z zGD-~)hr=Qn)kqk#FQ1fRDJH8+q!M#RKPl5oFR_)uBN4!?;0t)8f>L#y+)SMKb9bH zOF?msJmX43u@n?vmExZf#gLV1jIYY{KVyuf!R>1nBS~<*)?y?H7+;t1eTgJR@eNCn zG*Db;VUPrhZ^|%EkfZ_QdW(@HV0^1nPhfo8V#Jih1}UNdE;ST4$n@EUqLwhK4Kg^( zQ1tPmt7N;v7^9E*Zj{n97=1L~D7(-vfxzek<3@SvuMI{Y7&l4reuNQ?v1^o;em|y2 z8oa(^DUyW7nLab(-`;2^p`P4(%|+3i;*O_{xHG_*M{O=DF>R*t0Ymq z{jtP9OzDiIMfBZADOK0VB!caymKsT5`tV)Cz1rw`{2u*nXiRR38?Yr#v9V zAxu-}&oYxg56GeELzrkZG=&_?a8RB89W^A-uN2g`bpkE-~@~<1Z-X znAZ_`?D>X%IdC75myI#QD913qlj4?SR2!9R+TY3aEy<`hA`P^^x3o!u=cAT3Nr>4W zI%H7NRM7rtgO)Uay(t;jMkE33&B=J{lLWBilW}cC62MOA)Dd9Yl5xypM)0Qp$XawUrtW4yOL1`t-yRICnf!h0HU-4ttTgYUV_%@r!~CB?4{#tMURRx*8w!KfvS zYF0A1*nLpPVRv87PBMnN||V{cSjt~PR2EQcL2O6Ddxp-*j@9x zCz+lX^CJ!X?zQ|#g6I1zKa#-j{-mwZNmBqHumDK|zy~b`k^t}_%a0`Rn`8Nr1bz>9 r>Ii;wEkDdiRLxI{pBsSP4Z!)ybo8-EOBmJsN##ep_VOcLmOt=+n1dR_ delta 12230 zcmZXad3;vIxySR)%$dA7=Rn9w0vJVJWeX<U*7Y*Ig^!)Z(qZ_tU1DYFvQJqHXWz za}P*lRkW>mq!+L*6;}kb24xcnpe&VL5Tm%TtAK1ODEBwdY&7@J=XYkFGtc*V=9zg< z@^Ev%)wBAIXv{UTM&2kIgGSj{+?a1HX$%{qSL5dfu!GB*UKkMfFFXI%FuA;LSlwNN zZW~^IBw#I z@LXTKA{d{uRG24G{9Conc^<{P-y|U@-JK~V# zHZ|RFcl?7~?ibmFf$^!?fXQJwO>0^?F#a$v#^%CK+0VwumDz&vaXwplZ`{!I_3vYu zMDst&g~drwR5ZRgTYFZU>zaNsA@1KheI^$&ab_ZCvZ7a5t}4qF{(q!}hM|RfZ?79x z7&3g|(0+xwfrX!5er z69KEB$1*i`4hv7UCe}NLoo=V*WFlT?qMezFv9M}VGdM?fS$AtzqEZuqd<2MuZXV?9b{Nc}wxLsh-l zQgYh_wNkKq!qi7sym5(*tSj+uW~`VZgP90enl~(rZDx7R8zxrQe4Pc}l_}s$8Pil? zl2uD_!y>b?hcz*GDeF}e?o`{STOwDoqSe_O3*(P}+6^OD+7)%96|J&m2Qv|{D##5i zj9tY_O05a6t5{MV{v1xbCvpud+LY~mGwz>u$G|me>8k3wtEyYe*hZ6I*m%6wM%JAb zA(C}WR1pQ3Wc8+(z{1#d?9^&&;+^YQjgGTBL0IpnVl1pa^=7c5u%4Z&ivfc%cRj1M zo>f_Gu%*$Rp{!tOVz4lE11mezcw(5kfpzO1UW!nABytNY`XD=XaePtQ0|U3%4c0@2 z`W|B~CREsX{GP?zgYa&(8w_S5VCgLc3uCvkQ15D(c;{BNsZ}rOL7~2HmkToyuzG0_ zFv&Kh3=?zTSKDAZ5+1Nbe$0wKW7*)P@!!*rSdxf6UM3)d1WR77V%LQhy6j2K_2iCPH>AEWEc>_Rab*qmgI+=o zZ`sbyIoFsfu3e1PhPTsO@Niewb$NV8=UuGtS!RsdwtE>nI~-195b9o*y|g^Or`=w5 z-uJ>wkfQ2D?qfxxvUA@;58TJ{r%)wUtMu$=?4U^xY`EHQ`(HJs=YUNQ%tXM_`w$?9Vt!siiox~^_KgwG?kx~rLLd`JCy@DNsCGgT2h%F1*f z#8`UTyAMH1tMjOruNgb;D5+?^W_b%j!1Xswv^T2~WZ6{-jQxgnu&WYq{f%E$fT_p) zs&K}^(luPo0!%%|x|ZoZ2pe;csjl^8jxg~=o?z@g&a3{EO)>U*a_V1ewcXW1diRW+HX%)UJr8i zkn^mH@j;$Ps8^xNdQ$05^sey~*7s=nct~a$?L9RXt|#(y&kOgclcZN7C-I`m*$b=V z-FHml=9H4@jr*JoPbALk_2M)|OHUGZCNH+a89s=D3`LEK^9jzL&RX7yA4s3z=B%!vefuOgHx`(QfThbC7MwoG zEA`O=6HcGxI9k+}uc3YWlw|`m5wPr3wQrx|=2XH%dWxwxAkQT=D$&ny_H1@wO?>^$ z&+xnrLv7gU-X}IJEc<@7VW;yli&Aaa>AawexrPN8JcF|^+`{k<*f5)>u8n_{OK0#% zpG{EVV+QZ6H!@84n89_w#vBPBFDoDF77UXtYWT_`(|J<+?q%LtcN08?rJG8>P|{X%|* z&SF~2RE`&MmJno^Wa*m)EX-ZR%k|9yCgv{U=4MgLR8wBeS*4kZ^~2JaVm!pu#k{@V zOfWHZG4G=LVl8EF31_K&Ej+=ruT{xh;$N$hxx~L#HPL3y%FS!hL}BT=t7WQ*HuDPI zL}6lTvudJhlhm?2rY_}7pFl8EGW9NjiMdO82aN_M<}S4xtd^Bv?lKRmge>d01XGv! z6_;S@GTy0kxKW)zwQ7T{;A|C6pyphmf5X7~p@l)eE8H?D+x>35p`{h4>^8KT|*u) z_QULy_v0UR`;g-Ts1nnMHt~mQJ!s)fVh?}H@r5B*l@_wH|BfrN11<5;@=w*Nd{!uA z5tbo$aCVSq_kIxfNq2BbXMuWvjlF{>#6p=^$`0Q5vamD^lqnLqlZyjfhDn$wVa`O{ zUv~2701v{lu*7OuEFookxJ*04TWDt%W87Zew&vt<`9yri<#{Jd zDDcm@tUXymIex+AxhG4gvVE;4qH6YY`F#aWM@$nYhOCsGnPvlCY{T zdEo+e_rNslm_wY66?CgnHsU?xDXQE5Ax}}Y*h9SU6^3FC6c2MTN*Ic5SWXbe!#o-# zvH@?%3)Av2Qf`%AoI}xkqbf>M(jwk)jOVM(vPlD_Z+);Nfzo%L5=o$R!c!s%ltu`L zS4k-crjbJSJK29}BJLCN{F9<+6k-gbD5t#V1fi3SA{rwCw^$`&58N;0Isc-eF^(3p z)BwfZe4JoYEzp#%6XS#|H+fPA&Nxv)H&uX9hsZc_?lopDI2Yu2As(|t8Ph<=i|8>d zMb{;1$kPPj>XIaQen7aoBuTaO2eY+vWkvds$oDYorbntg9~KTgX#oFc&yOU4|BGKmb1T;8n1bg_z>NGXs9Ih^9F3S_JsNsu$?W(Hg-2 zuLqCaJ58|J7CfED(}bj769Ap!ND zo-}~}n+H!4JU`*VlZ1xwq>$f3MXBc1S}SUi4{Dq0bOvmP-RSX_z6RSuPD)L>gXJ z-gFwYhBVAnX*kBIqrlr-!Tu?D)gE5BVc4L-*WNw2e#p?KKYSYR$)$6Jq>C7!2t+69PR`Iaus-KpJw;>~la8a52oM_^KqD{;fyCkp+;YAVg*L>?6E{_Kx?;vVTi zUz(~g4*JqmKOPSH(kuqlLqd!S?A@$bGd(1tQGq=JNdwcvo+(N2d_>5au!8PkBn7x5 z9u=uPCW_7aNe1fU3bk%!B&l_M>kF1NQ2oxcAqh}V2zl1Y&H!~p;EqHZ1*jtfcNZz9 z^AA+-Q^Rz{B2fI9Q2f$4qH71F=-=I{l!R3^28EkU!R8Y=I$(bd?2S$Rni?HQx@Ot2jt{P5;5)y*dlxMKn_$-&KQxMPB7l7mYc+>Q<01%o8G9v8R^21%ee zK5zv~5*6$NLB7TWp~V!F9}L`egG8`>$Ri>NY#$EXb%P|Z{gY=)64?H^)kL8EOMsi1 zKIUi?j=4tyd(9hwZ+rOa6^O|QUElNartLl&M3WJ_E(9$iGg%-n`4HE^d|3F4d@xKmC2LY*31_%qXXOR$D%fq2p3FVXm?1<{KRKWV`K zxW`WtJpaw(CkgnU2xPgvXOKqxpUN)%A}&v#_QdHKh(F`Okp$w?J#msi{8>+&BoKeD z)kF|~K5!Rn8U^AnIO42C{h)jaP1g}GF~naAqM44k7D0SQmfMFlzpNnZUngNAX9sMF zt9A9ue0JcDxDZ5U2kwYdKh$Rj7hi8!hhY6mAQm~+p=SL`5G``7NrTr{9qUf`S&tqL z*Z=UaNdoNGvYGqhigZqpuQjznPgLW0-J>QAsONfOBmwn2kD4T)p6^kU1k?*!O$6$N z9(8E%(r+r(x|bkWu{aRdn)(k-hh7{+*SLkMUZOr81%;~(-zbsI0bA?%s;?H!o^OP( znmu3j5u-W46~goqd~2C$&v)Jt2|kfKz4Pji6Hw=MOIT(-*(mn;vz#aG8ESY(S>eR zpqRaRAQm0g1ciR#AE_C{iQE{lj{;tGM@jWL%{_1D5Pe|AHNdxOAJu#BN`YF$v zB(Q$kqb3Qc|K4gMP(Pzk>xx4}B7IhhD@}d3GxhzfjIJ;MwS-kYD+`yK!?P@r&r8-M z?M1u{@jfqQTjRA1qo0?h^e+a0T?UHJ%kSS{D3*cZ3sU^UQcPKy#`uDa{^2l^2DdMI zj3mMJOCBRhz&Jydu}(NiisH+jB59yF)59PM6lclQl`Lt%INM_+2^e2#H4_+L^%xP7 zcuflXrM1jZd`(6dxK){-Gq1_Q`G#VW$T^ZNO&!Jr@y?MlHW(8GF-Nwcza0T%0*rIy zIX^ZS6JUH@iX|4K8mzwKzb>OCjv{IB`i7@S5(dw;6jSy00!#(Qc^)H43gdi_ku+dj z;9-yij0=4+k_3uxdWs~0;-Xd)L2)to+iFBeqS-fF$Y;YLMHOA#K+Tbvf2Dfi{ zj3mMJ+ZH2S8;UEW>}ft@kVHLomBhc!=nB;$=B}omn$nkd62bNzPmLt7UE|r31h#8i zjRM{k_%&z_}ekW~_?uSVQ;%bqnTx#f70C$V*Kin*%0?YV7id~L&g{J+1jCMKN zq=EK_o;FGFyv@@l2{rrCNgb3lRkR=bq9qMrKk0@zPISdsvCy9Y}W!0u=@5ny-v zt&fQC?MaHOO$V(oMY~%@R~e{U!m4)5!j-0ID-*d_vaejxRwCZLQqmulQKgj_y;pX+ z7NgZ)xGH%P?3Fi;%o&W8!1$RIM+`>n;7ZN$GZ`Im97%)U&pk(y;QI^DktA^3=l4EI z3gdo{ku+dD;AxNqj9=n}w|~SV4IB@8jwFHOp;j}&@v!HJki=0beq=CK8jMF}bgjXt zC9LYGEL;;luIsRUB9F^F8&}M$CgyN`SO2d)BiCkBdR)5edwa||F5UILJxcPpbl3Ox zVEL^SV~Xvqs=Y?}t&GML+glZBK>3|VNfJDt@F+P$x~X99irRb<)7{zG8QQ zA_*)<6+4t90cB&cyFZZxl=ruq2$Z9XT^mJ2qIztx_@QCh-mn~79NEu0TEeQvj=71q MX}XEGeSO;h0e|!&%>V!Z diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index e6830ddb1..96b2a0010 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -300,13 +300,6 @@ message CatalogueRelease { string version = 3; } -// CatalogueExecution is what runs: an OCI image manifest digest and the build -// incarnation the host assigned to it, the same pair a Work Context seals. -message CatalogueExecution { - string image_digest = 1; - uint64 build_incarnation = 2; -} - // CatalogueDesired is what the composition declares. message CatalogueDesired { oneof declared_revision_value { @@ -319,11 +312,28 @@ message CatalogueDesired { } } -// CatalogueAuthorization is a current platform approval: the authorized -// revision and the execution it approved. +// CatalogueAuthorization is a current platform approval. What it approves is an +// execution inventory, `codefly/execution-inventory/v1`, whose one definition is +// the deployment contract module (github.com/codefly-dev/cli/contracts/deployment). +// The host does not re-describe that document: it names the approved inventory +// by the digest approval signs, carries the inventory's canonical bytes once per +// response (ListPlatformCatalogueResponse.approved_inventories), and reads them +// only through the contract. message CatalogueAuthorization { int64 authorized_revision = 1; - CatalogueExecution approved_execution = 2; + // The approved inventory's digest, as the contract computes it over its + // canonical bytes. + string inventory_digest = 2; + // Which member of that inventory's delivery aggregate this entry is. + string member_binding = 3; + // The build incarnation this host assigned to the approved execution. Not an + // inventory field: this host, and only this host, assigns it — advancing it + // whenever the approved execution changes — and a Work Context seals it beside + // the image digest. The verdict compares it with an observation's + // incarnation, which is the same host-assigned value as presented by the + // running workload; an incarnation from any other assigner would make that + // comparison meaningless, not merely wrong. + uint64 build_incarnation = 4; } // CatalogueNotAuthorized is a known absence of current authorization — @@ -350,11 +360,24 @@ message CatalogueApplied { } } +// CatalogueObservedExecution is what an observation found running for an entry: +// each container's image manifest digest, keyed "/" in the approved inventory's own names, and the build incarnation the +// running workload presents — the incarnation this host assigned, sealed into +// its Work Context, never one the workload or another party chose. +// Observation is the observing consumer's record, not the inventory's, so its +// shape is the host's. +message CatalogueObservedExecution { + map container_image_digests = 1; + uint64 build_incarnation = 2; +} + // CatalogueObservedVerdict judges what runs against what is authorized — // never against what is declared, because only authorization admits execution. enum CatalogueObservedVerdict { CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; - // The observed image digest and incarnation are the approved execution. + // Every container of the approved inventory member runs its approved image + // digest, no other container runs, and the incarnation is the approved one. CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; // Something other than the approved execution is running: what cluster // admission refuses. @@ -376,11 +399,12 @@ message CatalogueObserved { CatalogueGap observation_freshness_gap = 4; } oneof observed_execution_value { - CatalogueExecution observed_execution = 5; + CatalogueObservedExecution observed_execution = 5; CatalogueGap observed_execution_gap = 6; } - // A verdict exists only when both an observation and a known authorization - // do; anything less is a gap, and an unobserved entry is never a match. + // A verdict exists only when both a complete observation and a readable + // authorization do; anything less is a gap, and an unobserved or + // incompletely observed entry is never a match. oneof verdict_value { CatalogueObservedVerdict verdict = 7; CatalogueGap verdict_gap = 8; @@ -478,6 +502,11 @@ message ListPlatformCatalogueResponse { repeated CatalogueEntry entries = 1; // The registry revision the solution entries were read at. int64 registry_revision = 2; + // Every approved execution inventory an entry's authorization names, keyed by + // its digest, in the deployment contract's canonical bytes (codefly-json-v1). + // Carried once per response because one delivery aggregate is approved as a + // whole and several entries can be members of it. + map approved_inventories = 3; } // PlatformAdminService — cross-tenant operations for platform operators. diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index bf66d74ad..bbfe7e115 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275", + "digest": "sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b", "services": [ { "name": "APIKeyService", @@ -477,7 +477,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275", + "digest": "sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index b9035bfc9..880d3576b 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275 + digest: sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275 + digest: sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index 05f083e54..f0fc12259 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -175,21 +175,46 @@ size (the presence document's `build_size` section, codefly-dev/core#708, computed by the CLI at build, codefly-dev/cli#901): the host counts no lines itself. So each reads `NOT_RECORDED` or `NOT_OBSERVED`. -**Authorization has three cases, not two**: an approval (`authorized_revision` + -the approved `{image_digest, build_incarnation}`), `not_authorized` — a known -absence the approval record states — and a gap, where the host cannot tell. An -unrecorded approval is never shown as a refusal. +**Authorization has three cases, not two**: an approval, `not_authorized` — a +known absence the approval record states — and a gap, where the host cannot +tell. An unrecorded approval is never shown as a refusal. + +**What an approval approves is the deployment contract's document, not a host +description of it.** The execution inventory (`codefly/execution-inventory/v1`) +has one definition, the Go module `github.com/codefly-dev/cli/contracts/deployment` +(stdlib-only, enforced by its own boundary test). An approval in the Catalogue +names the approved inventory by the digest approval signs, plus which member of +that delivery aggregate the entry is, plus the host's own `build_incarnation`, +which is not an inventory field. The inventory's canonical bytes travel once per +response in `approved_inventories`, and the host reads them only through the +contract: `Check` accepts the inventory's intrinsic form, its digest must be the +approved one, and the containers are the contract's own seven-key projection +(`Rows`) — no traversal of workload templates is written here. No proto here re-describes an inventory +field; a second description would be the first thing to disagree with what is +deployed. **The observed verdict judges against the authorization, never the -declaration** (`judgeCatalogueObserved`): `RUNNING_AUTHORIZED` only for the -approved digest and incarnation, `RUNNING_DIFFERS` for anything else, -`RUNNING_UNAUTHORIZED` for an observation with no current authorization — the -row the retirement sweep exists to stop. Without an observation it is -`NOT_OBSERVED`, and with an observation but no known authorization it is -`NOT_RECORDED`; neither is ever a match. Withdrawal, credential revocation, -retirement and build size have gap-only `oneof`s: their value vocabularies are -the deployment contract's and core#708's to define, and each value case joins -its `oneof` when that record exists, read in `newCatalogueEntry`. +declaration** (`judgeCatalogueObserved`). An observation is each container's image +digest, keyed `/` in the approved inventory's names, +plus the presented incarnation: + +- `RUNNING_AUTHORIZED` only when every container of the approved member (init + containers included) runs its approved digest, nothing else runs, and the + incarnation is the approved one; +- `RUNNING_DIFFERS` for any other digest, an extra container, or another + incarnation; +- `RUNNING_UNAUTHORIZED` for an observation with no current authorization — the + row the retirement sweep exists to stop; +- `NOT_OBSERVED` with no observation, or one missing an approved container (an + incomplete observation is never counted as approved); +- `NOT_RECORDED` when there is no authorization to read, or the approved + inventory is not held, does not hash to its digest, is refused by the + contract, or has no such member. + +Withdrawal, credential revocation, retirement and build size have gap-only +`oneof`s: their value vocabularies are later spec items' and core#708's to define, +and each value case joins its `oneof` when that record exists, read in +`newCatalogueEntry`. **The registration it returns withholds topology**: `catalogueRegistrationProto` blanks the frontend manifest, backend upstream and service alias, as every other diff --git a/module/services/accounts/code/go.mod b/module/services/accounts/code/go.mod index 63b667578..cd79e9f4e 100644 --- a/module/services/accounts/code/go.mod +++ b/module/services/accounts/code/go.mod @@ -7,6 +7,7 @@ require ( buf.build/go/protovalidate v1.4.0 connectrpc.com/connect v1.21.0 connectrpc.com/otelconnect v0.9.0 + github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006144637-aacb2284b983 github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683 github.com/codefly-dev/interface-cache/go/cache v0.2.2-0.20260926205419-327a6e1d9aba github.com/codefly-dev/sdk-go v0.2.1-0.20260926212413-b91ce91eb23e diff --git a/module/services/accounts/code/go.sum b/module/services/accounts/code/go.sum index dc7e3b233..5a5c0ed4c 100644 --- a/module/services/accounts/code/go.sum +++ b/module/services/accounts/code/go.sum @@ -58,6 +58,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006144637-aacb2284b983 h1:kcnaCpxyprLrlQSeXyyEEMeVbhTtYoknAv19T5lJXQw= +github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006144637-aacb2284b983/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683 h1:/vzymsu5B9NWwdhWRtlU+68VcVEVDmeZNChRCXAUwlE= github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683/go.mod h1:SVF+t040X+9pAXAqBnNE/JkXwmw9peVuKnHKFSOzyzk= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= diff --git a/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go b/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go index 3421eb998..de1255941 100644 --- a/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go +++ b/module/services/accounts/code/pkg/adapters/platform_catalogue_rpcs.go @@ -33,7 +33,10 @@ func (s *PlatformAdminServer) ListPlatformCatalogue(ctx context.Context, req *ge } return nil, status.Error(codes.Internal, "cannot read the platform catalogue") } - response := &gen.ListPlatformCatalogueResponse{RegistryRevision: catalogue.RegistryRevision} + response := &gen.ListPlatformCatalogueResponse{ + RegistryRevision: catalogue.RegistryRevision, + ApprovedInventories: catalogue.ApprovedInventories, + } for _, row := range catalogue.Entries { entry := row.Entry if row.Registration != nil { diff --git a/module/services/accounts/code/pkg/business/platform_catalogue.go b/module/services/accounts/code/pkg/business/platform_catalogue.go index 3407ccb28..1ee0d7c00 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue.go @@ -2,9 +2,12 @@ package business import ( "context" + "fmt" + "maps" "slices" "strings" + deployment "github.com/codefly-dev/cli/contracts/deployment" "github.com/codefly-dev/core/wool" gen "accounts/pkg/gen/saas/accounts/v1" @@ -43,9 +46,13 @@ type PlatformCatalogueEntry struct { } // PlatformCatalogue is the whole Catalogue at one registry revision. +// ApprovedInventories holds, by digest, the canonical bytes of every approved +// execution inventory an entry's authorization names; no record carries an +// approval yet, so it is empty. type PlatformCatalogue struct { - Entries []*PlatformCatalogueEntry - RegistryRevision int64 + Entries []*PlatformCatalogueEntry + RegistryRevision int64 + ApprovedInventories map[string][]byte } const ( @@ -178,7 +185,7 @@ func newCatalogueEntry(kind gen.CatalogueEntryKind, name string) *gen.CatalogueE }, BuildSizeValue: &gen.CatalogueEntry_BuildSizeGap{BuildSizeGap: catalogueGap(notRecorded, catalogueNoBuildSize)}, } - judgeCatalogueObserved(entry.Authorized, entry.Observed) + judgeCatalogueObserved(entry.Authorized, entry.Observed, nil) return entry } @@ -188,28 +195,102 @@ func newCatalogueEntry(kind gen.CatalogueEntryKind, name string) *gen.CatalogueE // both are known: // // - no observation: NOT_OBSERVED, never a match; -// - an observation, but the host cannot tell what is authorized: NOT_RECORDED; // - an observation with no current authorization: RUNNING_UNAUTHORIZED; -// - an observation of the approved digest and incarnation: RUNNING_AUTHORIZED; +// - an observation, but no authorization this host can read: NOT_RECORDED; +// - an observation missing an approved container: NOT_OBSERVED, because an +// incomplete observation is never counted as approved execution; +// - every approved container on its approved digest, nothing else running, +// and the approved incarnation: RUNNING_AUTHORIZED; // - anything else: RUNNING_DIFFERS. -func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.CatalogueObserved) { +// +// The approved execution is read from the approved inventory through the +// deployment contract, keyed by the inventory's digest in inventories. +func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.CatalogueObserved, inventories map[string][]byte) { + gap := func(reason gen.CatalogueGapReason, detail string) { + observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(reason, detail)} + } + verdict := func(v gen.CatalogueObservedVerdict) { + observed.VerdictValue = &gen.CatalogueObserved_Verdict{Verdict: v} + } running := observed.GetObservedExecution() switch { case running == nil: - observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, catalogueNotObserved)} + gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, catalogueNotObserved) + return case authorized.GetNotAuthorized() != nil: - observed.VerdictValue = &gen.CatalogueObserved_Verdict{Verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED} + verdict(gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED) + return case authorized.GetAuthorization() == nil: - observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoVerdict)} - default: - approved := authorized.GetAuthorization().GetApprovedExecution() - verdict := gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS - if approved.GetImageDigest() != "" && approved.GetImageDigest() == running.GetImageDigest() && - approved.GetBuildIncarnation() == running.GetBuildIncarnation() { - verdict = gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED + gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoVerdict) + return + } + approval := authorized.GetAuthorization() + approved, err := approvedContainerImageDigests(inventories[approval.GetInventoryDigest()], approval.GetInventoryDigest(), approval.GetMemberBinding()) + if err != nil { + gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, "The approved inventory cannot be read: "+err.Error()) + return + } + observedDigests := running.GetContainerImageDigests() + for key := range approved { + if _, ok := observedDigests[key]; !ok { + gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, "The observation is incomplete: approved container "+key+" was not observed.") + return + } + } + if maps.Equal(approved, observedDigests) && running.GetBuildIncarnation() == approval.GetBuildIncarnation() { + verdict(gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED) + return + } + verdict(gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS) +} + +// approvedContainerImageDigests reads one member's approved containers out of +// an approved inventory, as "/" → image manifest +// digest. Everything comes from the deployment contract: Check accepts the +// inventory's intrinsic form, its digest must be the one approval signed, and +// the containers are the contract's own seven-key projection (Rows), never a +// traversal of the workload templates written here. +func approvedContainerImageDigests(canonical []byte, digest, member string) (map[string]string, error) { + if len(canonical) == 0 { + return nil, fmt.Errorf("no inventory is held for %s", digest) + } + checked, err := deployment.Check(canonical) + if err != nil { + return nil, fmt.Errorf("the deployment contract refuses it: %w", err) + } + if got := checked.Digest(); got != digest { + return nil, fmt.Errorf("its canonical bytes hash to %s, not the approved %s", got, digest) + } + inventory := checked.Inventory() + var workloads []string + for _, candidate := range inventory.Members { + if candidate.Binding == member { + workloads = candidate.Workloads + } + } + if workloads == nil { + return nil, fmt.Errorf("it has no member %q", member) + } + // The projection is one row per workload, in the inventory's workload + // order; a row does not name its workload, so the pairing is by position. + rows := checked.Rows() + if len(rows) != len(inventory.Workloads) { + return nil, fmt.Errorf("the contract projected %d rows for %d workloads", len(rows), len(inventory.Workloads)) + } + out := make(map[string]string) + for i, workload := range inventory.Workloads { + if !slices.Contains(workloads, workload.ID) { + continue + } + for name, reference := range rows[i].Images { + _, imageDigest, ok := strings.Cut(reference, "@") + if !ok { + return nil, fmt.Errorf("container %s/%s has no image digest", workload.ID, name) + } + out[workload.ID+"/"+name] = imageDigest } - observed.VerdictValue = &gen.CatalogueObserved_Verdict{Verdict: verdict} } + return out, nil } func catalogueInstallation(record *CatalogueInstallationRecord) *gen.CatalogueInstallation { diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_test.go index ed664d174..d5b91201d 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue_test.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue_test.go @@ -1,80 +1,129 @@ package business import ( + "os" + "strings" "testing" "time" + deployment "github.com/codefly-dev/cli/contracts/deployment" + "github.com/stretchr/testify/require" gen "accounts/pkg/gen/saas/accounts/v1" ) +// executionInventory is the deployment contract's own base inventory +// (contracts/deployment/testdata/base.json in codefly-dev/cli), copied as test +// data. Every test reads it through the contract, so a copy the contract no +// longer accepts fails here rather than drifting: one member, "member", whose +// workload "worker" runs app containers helper and worker and init containers +// prepare and seed, all on one image digest. +func executionInventory(t *testing.T) (canonical []byte, digest string) { + t.Helper() + raw, err := os.ReadFile("testdata/execution-inventory-base.json") + require.NoError(t, err) + require.NoError(t, deployment.CheckSchema(raw), "refresh testdata from the deployment contract") + canonical, err = deployment.CanonicalJSON(raw) + require.NoError(t, err) + return canonical, deployment.Digest(canonical) +} + +const inventoryImage = "sha256:227abccb20fc21ca22053afa8770ffeaeee9f48848f5dd55fdd8cd3d185e8e77" + +func observedAll(incarnation uint64, edit func(map[string]string)) *gen.CatalogueObservedExecution { + digests := map[string]string{ + "worker/helper": inventoryImage, "worker/worker": inventoryImage, + "worker/prepare": inventoryImage, "worker/seed": inventoryImage, + } + if edit != nil { + edit(digests) + } + return &gen.CatalogueObservedExecution{ContainerImageDigests: digests, BuildIncarnation: incarnation} +} + // The observed state's verdict answers "is what runs what is authorized?" It is -// judged against the approval, never the declaration, and given only when both -// an observation and a known authorization exist: an unobserved entry is never a -// match, and an entry the host cannot judge is never "unauthorized". +// judged against the approved inventory, read through the deployment contract, +// never against the declaration, and given only when a complete observation and +// a readable authorization both exist: an unobserved or partly observed entry is +// never a match, and an entry the host cannot judge is never "unauthorized". func TestCatalogueObservedVerdict(t *testing.T) { - approved := &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3} - authorized := &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ - Authorization: &gen.CatalogueAuthorization{AuthorizedRevision: 4, ApprovedExecution: approved}, - }} + canonical, digest := executionInventory(t) + inventories := map[string][]byte{digest: canonical} + authorizedAs := func(inventoryDigest, member string) *gen.CatalogueAuthorized { + return &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ + Authorization: &gen.CatalogueAuthorization{ + AuthorizedRevision: 4, InventoryDigest: inventoryDigest, MemberBinding: member, BuildIncarnation: 3, + }, + }} + } + authorized := authorizedAs(digest, "member") notAuthorized := &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_NotAuthorized{ NotAuthorized: &gen.CatalogueNotAuthorized{Detail: "approval withdrawn"}, }} unknown := &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_AuthorizationGap{ AuthorizationGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, "no approval record"), }} + // A valid inventory the contract accepts, held under a digest it does not + // hash to: what a record that swapped the approved bytes would present. + wrongDigest := "sha256:" + strings.Repeat("0", 64) + otherSchema := []byte(strings.Replace(string(canonical), `"codefly/execution-inventory/v1"`, `"codefly/execution-inventory/v0"`, 1)) + require.NotEqual(t, canonical, otherSchema) + otherSchemaDigest := deployment.Digest(otherSchema) + cases := []struct { - name string - authorized *gen.CatalogueAuthorized - observed *gen.CatalogueExecution - verdict gen.CatalogueObservedVerdict - gap gen.CatalogueGapReason + name string + authorized *gen.CatalogueAuthorized + observed *gen.CatalogueObservedExecution + inventories map[string][]byte + verdict gen.CatalogueObservedVerdict + gap gen.CatalogueGapReason }{ {name: "authorized, nothing observed", authorized: authorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, {name: "not authorized, nothing observed", authorized: notAuthorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, + {name: "observed, authorization unknown to this host", authorized: unknown, observed: observedAll(3, nil), gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, { - name: "observed, authorization unknown to this host", - authorized: unknown, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, - gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, + name: "every approved container on its approved digest, at the approved incarnation", authorized: authorized, + observed: observedAll(3, nil), verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED, }, { - name: "the approved digest and incarnation", - authorized: authorized, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, - verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED, + name: "one container on another image", authorized: authorized, + observed: observedAll(3, func(m map[string]string) { m["worker/helper"] = "sha256:" + strings.Repeat("b", 64) }), + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { - name: "another image", - authorized: authorized, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:bbb", BuildIncarnation: 3}, - verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + name: "a container the inventory does not approve", authorized: authorized, + observed: observedAll(3, func(m map[string]string) { m["worker/sidecar"] = inventoryImage }), + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { // The incarnation moves when command, configuration or identity - // change under the same image, so the image alone does not match. - name: "same image, another incarnation", - authorized: authorized, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 2}, - verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + // change under the same image, so the images alone do not match. + name: "every image approved, another incarnation", authorized: authorized, + observed: observedAll(2, nil), verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { - name: "two empty digests are not a match", - authorized: &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ - Authorization: &gen.CatalogueAuthorization{ApprovedExecution: &gen.CatalogueExecution{}}, - }}, - observed: &gen.CatalogueExecution{}, - verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + // A finished init container is still part of what ran. + name: "an approved init container missing from the observation", authorized: authorized, + observed: observedAll(3, func(m map[string]string) { delete(m, "worker/seed") }), + gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, }, { // The dangerous row: something runs that nothing currently // authorizes, whatever its image. - name: "observed with no current authorization", - authorized: notAuthorized, - observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, - verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED, + name: "observed with no current authorization", authorized: notAuthorized, + observed: observedAll(3, nil), verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED, + }, + {name: "approved inventory not held", authorized: authorized, observed: observedAll(3, nil), inventories: map[string][]byte{}, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, + {name: "inventory bytes that do not hash to the approved digest", authorized: authorizedAs(wrongDigest, "member"), observed: observedAll(3, nil), inventories: map[string][]byte{wrongDigest: canonical}, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, + { + // Its digest is right and its member and images are all there: only the + // contract can say this is not an inventory it defines. + name: "an inventory the contract refuses, held under its own digest", authorized: authorizedAs(otherSchemaDigest, "member"), + observed: observedAll(3, nil), inventories: map[string][]byte{otherSchemaDigest: otherSchema}, + gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, }, + {name: "a member the inventory does not have", authorized: authorizedAs(digest, "someone-else"), observed: observedAll(3, nil), gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -82,14 +131,18 @@ func TestCatalogueObservedVerdict(t *testing.T) { if tc.observed != nil { observed.ObservedExecutionValue = &gen.CatalogueObserved_ObservedExecution{ObservedExecution: tc.observed} } - judgeCatalogueObserved(tc.authorized, observed) + held := inventories + if tc.inventories != nil { + held = tc.inventories + } + judgeCatalogueObserved(tc.authorized, observed, held) if tc.gap != gen.CatalogueGapReason_CATALOGUE_GAP_REASON_UNSPECIFIED { require.Equal(t, gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED, observed.GetVerdict()) require.Equal(t, tc.gap, observed.GetVerdictGap().GetReason()) require.NotEmpty(t, observed.GetVerdictGap().GetDetail()) return } - require.Nil(t, observed.GetVerdictGap()) + require.Nil(t, observed.GetVerdictGap(), observed.GetVerdictGap().GetDetail()) require.Equal(t, tc.verdict, observed.GetVerdict()) }) } diff --git a/module/services/accounts/code/pkg/business/testdata/execution-inventory-base.json b/module/services/accounts/code/pkg/business/testdata/execution-inventory-base.json new file mode 100644 index 000000000..23b412920 --- /dev/null +++ b/module/services/accounts/code/pkg/business/testdata/execution-inventory-base.json @@ -0,0 +1,269 @@ +{ + "artifacts": [ + { + "digest": "sha256:8b87e12b2e81f186e2d2e38d5b0eca16e19dabad14ecd8b9d325c607184ca9f0", + "id": "rendered", + "objects": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "worker", + "namespace": "workloads" + } + ], + "path": "worker.json", + "source": "rendered" + } + ], + "complete": true, + "delivery": "delivery", + "dependencies": [], + "egress": [], + "envelope_revision": "1", + "generation": 1, + "ingress": [], + "members": [ + { + "artifacts": [ + "rendered" + ], + "binding": "member", + "declaration_kind": "module", + "digest": "sha256:112747ee2d71526450b6d522353b55cb3d245e890ae69270ddf100647a0e1e88", + "presence_generation": 1, + "removed": false, + "workloads": [ + "worker" + ] + } + ], + "namespaces": [ + { + "member_binding": "member", + "name": "workloads" + } + ], + "ownership_domain": "team", + "platform_refs": { + "compiler": { + "digest": "sha256:5f348f0829d9bf970a068ce6b8f6c68b5f675d729a5e1a92826c58a79ba52f7c", + "id": "compiler", + "revision": "1" + }, + "execution": { + "digest": "sha256:20ad183d2e0d34a2dbf65c4a284845d0aa9d1f76c38425ce9208e6438d16fa68", + "id": "execution", + "revision": "1" + }, + "identity": { + "digest": "sha256:919917359a0566095385e8772667ffa849afecda0a1ed41632222f30e6a1048a", + "id": "identity", + "revision": "1" + }, + "trust": { + "digest": "sha256:e7712c94496595e654a83c5163e76ff14c907410c37cb823674a0512afe9d800", + "id": "trust", + "revision": "1" + } + }, + "previous": null, + "removed": false, + "resource_bindings": [], + "schema": "codefly/execution-inventory/v1", + "target": { + "cluster_instance": "cluster-instance-1", + "coordinate": "local", + "host_component": "host" + }, + "workloads": [ + { + "artifact": { + "artifact": "rendered", + "carrier_profile": null + }, + "authenticating_container": null, + "configuration_refs": [], + "controller": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "worker", + "namespace": "workloads" + }, + "credential_kind": "none", + "id": "worker", + "identity": null, + "member_binding": "member", + "platform_role_ref": null, + "selector": { + "app": "worker" + }, + "template": { + "metadata": { + "annotations": {}, + "labels": { + "app": "worker" + } + }, + "spec": { + "automountServiceAccountToken": false, + "containers": [ + { + "args": [], + "command": [ + "/helper" + ], + "env": [], + "image": "registry.example/team/worker@sha256:227abccb20fc21ca22053afa8770ffeaeee9f48848f5dd55fdd8cd3d185e8e77", + "imagePullPolicy": "IfNotPresent", + "name": "helper", + "ports": [], + "restartPolicy": null, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [], + "drop": [ + "ALL" + ] + }, + "privileged": false, + "readOnlyRootFilesystem": true, + "runAsGroup": 1000, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [], + "workingDir": "/" + }, + { + "args": [], + "command": [ + "/app" + ], + "env": [], + "image": "registry.example/team/worker@sha256:227abccb20fc21ca22053afa8770ffeaeee9f48848f5dd55fdd8cd3d185e8e77", + "imagePullPolicy": "IfNotPresent", + "name": "worker", + "ports": [ + { + "containerPort": 8080, + "name": "http", + "protocol": "TCP" + } + ], + "restartPolicy": null, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [], + "drop": [ + "ALL" + ] + }, + "privileged": false, + "readOnlyRootFilesystem": true, + "runAsGroup": 1000, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [], + "workingDir": "/" + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": false, + "initContainers": [ + { + "args": [], + "command": [ + "/prepare" + ], + "env": [], + "image": "registry.example/team/worker@sha256:227abccb20fc21ca22053afa8770ffeaeee9f48848f5dd55fdd8cd3d185e8e77", + "imagePullPolicy": "IfNotPresent", + "name": "prepare", + "ports": [], + "restartPolicy": "Always", + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [], + "drop": [ + "ALL" + ] + }, + "privileged": false, + "readOnlyRootFilesystem": true, + "runAsGroup": 1000, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [], + "workingDir": "/" + }, + { + "args": [], + "command": [ + "/seed" + ], + "env": [], + "image": "registry.example/team/worker@sha256:227abccb20fc21ca22053afa8770ffeaeee9f48848f5dd55fdd8cd3d185e8e77", + "imagePullPolicy": "IfNotPresent", + "name": "seed", + "ports": [], + "restartPolicy": null, + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "add": [], + "drop": [ + "ALL" + ] + }, + "privileged": false, + "readOnlyRootFilesystem": true, + "runAsGroup": 1000, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [], + "workingDir": "/" + } + ], + "nodeSelector": { + "kubernetes.io/arch": "amd64", + "kubernetes.io/os": "linux" + }, + "os": { + "name": "linux" + }, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "serviceAccountName": "worker", + "terminationGracePeriodSeconds": 30, + "volumes": [] + } + } + } + ] +} diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index a73c866af..9d9c30263 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -145,7 +145,8 @@ type CatalogueObservedVerdict int32 const ( CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED CatalogueObservedVerdict = 0 - // The observed image digest and incarnation are the approved execution. + // Every container of the approved inventory member runs its approved image + // digest, no other container runs, and the incarnation is the approved one. CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED CatalogueObservedVerdict = 1 // Something other than the approved execution is running: what cluster // admission refuses. @@ -2089,60 +2090,6 @@ func (x *CatalogueRelease) GetVersion() string { return "" } -// CatalogueExecution is what runs: an OCI image manifest digest and the build -// incarnation the host assigned to it, the same pair a Work Context seals. -type CatalogueExecution struct { - state protoimpl.MessageState `protogen:"open.v1"` - ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` - BuildIncarnation uint64 `protobuf:"varint,2,opt,name=build_incarnation,json=buildIncarnation,proto3" json:"build_incarnation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *CatalogueExecution) Reset() { - *x = CatalogueExecution{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *CatalogueExecution) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CatalogueExecution) ProtoMessage() {} - -func (x *CatalogueExecution) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[34] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CatalogueExecution.ProtoReflect.Descriptor instead. -func (*CatalogueExecution) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{34} -} - -func (x *CatalogueExecution) GetImageDigest() string { - if x != nil { - return x.ImageDigest - } - return "" -} - -func (x *CatalogueExecution) GetBuildIncarnation() uint64 { - if x != nil { - return x.BuildIncarnation - } - return 0 -} - // CatalogueDesired is what the composition declares. type CatalogueDesired struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2162,7 +2109,7 @@ type CatalogueDesired struct { func (x *CatalogueDesired) Reset() { *x = CatalogueDesired{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2174,7 +2121,7 @@ func (x *CatalogueDesired) String() string { func (*CatalogueDesired) ProtoMessage() {} func (x *CatalogueDesired) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2187,7 +2134,7 @@ func (x *CatalogueDesired) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogueDesired.ProtoReflect.Descriptor instead. func (*CatalogueDesired) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{35} + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{34} } func (x *CatalogueDesired) GetDeclaredRevisionValue() isCatalogueDesired_DeclaredRevisionValue { @@ -2272,19 +2219,36 @@ func (*CatalogueDesired_DeclaredRelease) isCatalogueDesired_DeclaredReleaseValue func (*CatalogueDesired_DeclaredReleaseGap) isCatalogueDesired_DeclaredReleaseValue() {} -// CatalogueAuthorization is a current platform approval: the authorized -// revision and the execution it approved. +// CatalogueAuthorization is a current platform approval. What it approves is an +// execution inventory, `codefly/execution-inventory/v1`, whose one definition is +// the deployment contract module (github.com/codefly-dev/cli/contracts/deployment). +// The host does not re-describe that document: it names the approved inventory +// by the digest approval signs, carries the inventory's canonical bytes once per +// response (ListPlatformCatalogueResponse.approved_inventories), and reads them +// only through the contract. type CatalogueAuthorization struct { state protoimpl.MessageState `protogen:"open.v1"` AuthorizedRevision int64 `protobuf:"varint,1,opt,name=authorized_revision,json=authorizedRevision,proto3" json:"authorized_revision,omitempty"` - ApprovedExecution *CatalogueExecution `protobuf:"bytes,2,opt,name=approved_execution,json=approvedExecution,proto3" json:"approved_execution,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The approved inventory's digest, as the contract computes it over its + // canonical bytes. + InventoryDigest string `protobuf:"bytes,2,opt,name=inventory_digest,json=inventoryDigest,proto3" json:"inventory_digest,omitempty"` + // Which member of that inventory's delivery aggregate this entry is. + MemberBinding string `protobuf:"bytes,3,opt,name=member_binding,json=memberBinding,proto3" json:"member_binding,omitempty"` + // The build incarnation this host assigned to the approved execution. Not an + // inventory field: this host, and only this host, assigns it — advancing it + // whenever the approved execution changes — and a Work Context seals it beside + // the image digest. The verdict compares it with an observation's + // incarnation, which is the same host-assigned value as presented by the + // running workload; an incarnation from any other assigner would make that + // comparison meaningless, not merely wrong. + BuildIncarnation uint64 `protobuf:"varint,4,opt,name=build_incarnation,json=buildIncarnation,proto3" json:"build_incarnation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CatalogueAuthorization) Reset() { *x = CatalogueAuthorization{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2296,7 +2260,7 @@ func (x *CatalogueAuthorization) String() string { func (*CatalogueAuthorization) ProtoMessage() {} func (x *CatalogueAuthorization) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2309,7 +2273,7 @@ func (x *CatalogueAuthorization) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogueAuthorization.ProtoReflect.Descriptor instead. func (*CatalogueAuthorization) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{36} + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{35} } func (x *CatalogueAuthorization) GetAuthorizedRevision() int64 { @@ -2319,11 +2283,25 @@ func (x *CatalogueAuthorization) GetAuthorizedRevision() int64 { return 0 } -func (x *CatalogueAuthorization) GetApprovedExecution() *CatalogueExecution { +func (x *CatalogueAuthorization) GetInventoryDigest() string { if x != nil { - return x.ApprovedExecution + return x.InventoryDigest } - return nil + return "" +} + +func (x *CatalogueAuthorization) GetMemberBinding() string { + if x != nil { + return x.MemberBinding + } + return "" +} + +func (x *CatalogueAuthorization) GetBuildIncarnation() uint64 { + if x != nil { + return x.BuildIncarnation + } + return 0 } // CatalogueNotAuthorized is a known absence of current authorization — @@ -2338,7 +2316,7 @@ type CatalogueNotAuthorized struct { func (x *CatalogueNotAuthorized) Reset() { *x = CatalogueNotAuthorized{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2350,7 +2328,7 @@ func (x *CatalogueNotAuthorized) String() string { func (*CatalogueNotAuthorized) ProtoMessage() {} func (x *CatalogueNotAuthorized) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2363,7 +2341,7 @@ func (x *CatalogueNotAuthorized) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogueNotAuthorized.ProtoReflect.Descriptor instead. func (*CatalogueNotAuthorized) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{37} + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{36} } func (x *CatalogueNotAuthorized) GetDetail() string { @@ -2388,7 +2366,7 @@ type CatalogueAuthorized struct { func (x *CatalogueAuthorized) Reset() { *x = CatalogueAuthorized{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2400,7 +2378,7 @@ func (x *CatalogueAuthorized) String() string { func (*CatalogueAuthorized) ProtoMessage() {} func (x *CatalogueAuthorized) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2413,7 +2391,7 @@ func (x *CatalogueAuthorized) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogueAuthorized.ProtoReflect.Descriptor instead. func (*CatalogueAuthorized) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{38} + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{37} } func (x *CatalogueAuthorized) GetAuthorizationValue() isCatalogueAuthorized_AuthorizationValue { @@ -2486,7 +2464,7 @@ type CatalogueApplied struct { func (x *CatalogueApplied) Reset() { *x = CatalogueApplied{} - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2498,7 +2476,7 @@ func (x *CatalogueApplied) String() string { func (*CatalogueApplied) ProtoMessage() {} func (x *CatalogueApplied) ProtoReflect() protoreflect.Message { - mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2511,7 +2489,7 @@ func (x *CatalogueApplied) ProtoReflect() protoreflect.Message { // Deprecated: Use CatalogueApplied.ProtoReflect.Descriptor instead. func (*CatalogueApplied) Descriptor() ([]byte, []int) { - return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{39} + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{38} } func (x *CatalogueApplied) GetAppliedRevisionValue() isCatalogueApplied_AppliedRevisionValue { @@ -2555,6 +2533,65 @@ func (*CatalogueApplied_AppliedRevision) isCatalogueApplied_AppliedRevisionValue func (*CatalogueApplied_AppliedRevisionGap) isCatalogueApplied_AppliedRevisionValue() {} +// CatalogueObservedExecution is what an observation found running for an entry: +// each container's image manifest digest, keyed "/" in the approved inventory's own names, and the build incarnation the +// running workload presents — the incarnation this host assigned, sealed into +// its Work Context, never one the workload or another party chose. +// Observation is the observing consumer's record, not the inventory's, so its +// shape is the host's. +type CatalogueObservedExecution struct { + state protoimpl.MessageState `protogen:"open.v1"` + ContainerImageDigests map[string]string `protobuf:"bytes,1,rep,name=container_image_digests,json=containerImageDigests,proto3" json:"container_image_digests,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + BuildIncarnation uint64 `protobuf:"varint,2,opt,name=build_incarnation,json=buildIncarnation,proto3" json:"build_incarnation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CatalogueObservedExecution) Reset() { + *x = CatalogueObservedExecution{} + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CatalogueObservedExecution) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CatalogueObservedExecution) ProtoMessage() {} + +func (x *CatalogueObservedExecution) ProtoReflect() protoreflect.Message { + mi := &file_saas_accounts_v1_platform_admin_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CatalogueObservedExecution.ProtoReflect.Descriptor instead. +func (*CatalogueObservedExecution) Descriptor() ([]byte, []int) { + return file_saas_accounts_v1_platform_admin_proto_rawDescGZIP(), []int{39} +} + +func (x *CatalogueObservedExecution) GetContainerImageDigests() map[string]string { + if x != nil { + return x.ContainerImageDigests + } + return nil +} + +func (x *CatalogueObservedExecution) GetBuildIncarnation() uint64 { + if x != nil { + return x.BuildIncarnation + } + return 0 +} + // CatalogueObserved is what is actually running. type CatalogueObserved struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -2575,8 +2612,9 @@ type CatalogueObserved struct { // *CatalogueObserved_ObservedExecution // *CatalogueObserved_ObservedExecutionGap ObservedExecutionValue isCatalogueObserved_ObservedExecutionValue `protobuf_oneof:"observed_execution_value"` - // A verdict exists only when both an observation and a known authorization - // do; anything less is a gap, and an unobserved entry is never a match. + // A verdict exists only when both a complete observation and a readable + // authorization do; anything less is a gap, and an unobserved or + // incompletely observed entry is never a match. // // Types that are valid to be assigned to VerdictValue: // @@ -2674,7 +2712,7 @@ func (x *CatalogueObserved) GetObservedExecutionValue() isCatalogueObserved_Obse return nil } -func (x *CatalogueObserved) GetObservedExecution() *CatalogueExecution { +func (x *CatalogueObserved) GetObservedExecution() *CatalogueObservedExecution { if x != nil { if x, ok := x.ObservedExecutionValue.(*CatalogueObserved_ObservedExecution); ok { return x.ObservedExecution @@ -2754,7 +2792,7 @@ type isCatalogueObserved_ObservedExecutionValue interface { } type CatalogueObserved_ObservedExecution struct { - ObservedExecution *CatalogueExecution `protobuf:"bytes,5,opt,name=observed_execution,json=observedExecution,proto3,oneof"` + ObservedExecution *CatalogueObservedExecution `protobuf:"bytes,5,opt,name=observed_execution,json=observedExecution,proto3,oneof"` } type CatalogueObserved_ObservedExecutionGap struct { @@ -3367,8 +3405,13 @@ type ListPlatformCatalogueResponse struct { Entries []*CatalogueEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` // The registry revision the solution entries were read at. RegistryRevision int64 `protobuf:"varint,2,opt,name=registry_revision,json=registryRevision,proto3" json:"registry_revision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Every approved execution inventory an entry's authorization names, keyed by + // its digest, in the deployment contract's canonical bytes (codefly-json-v1). + // Carried once per response because one delivery aggregate is approved as a + // whole and several entries can be members of it. + ApprovedInventories map[string][]byte `protobuf:"bytes,3,rep,name=approved_inventories,json=approvedInventories,proto3" json:"approved_inventories,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ListPlatformCatalogueResponse) Reset() { @@ -3415,6 +3458,13 @@ func (x *ListPlatformCatalogueResponse) GetRegistryRevision() int64 { return 0 } +func (x *ListPlatformCatalogueResponse) GetApprovedInventories() map[string][]byte { + if x != nil { + return x.ApprovedInventories + } + return nil +} + var File_saas_accounts_v1_platform_admin_proto protoreflect.FileDescriptor const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + @@ -3550,20 +3600,19 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\x10CatalogueRelease\x12\x1c\n" + "\tpublisher\x18\x01 \x01(\tR\tpublisher\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x18\n" + - "\aversion\x18\x03 \x01(\tR\aversion\"d\n" + - "\x12CatalogueExecution\x12!\n" + - "\fimage_digest\x18\x01 \x01(\tR\vimageDigest\x12+\n" + - "\x11build_incarnation\x18\x02 \x01(\x04R\x10buildIncarnation\"\xf1\x02\n" + + "\aversion\x18\x03 \x01(\tR\aversion\"\xf1\x02\n" + "\x10CatalogueDesired\x12-\n" + "\x11declared_revision\x18\x01 \x01(\x03H\x00R\x10declaredRevision\x12T\n" + "\x15declared_revision_gap\x18\x02 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x13declaredRevisionGap\x12O\n" + "\x10declared_release\x18\x03 \x01(\v2\".saas.accounts.v1.CatalogueReleaseH\x01R\x0fdeclaredRelease\x12R\n" + "\x14declared_release_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x12declaredReleaseGapB\x19\n" + "\x17declared_revision_valueB\x18\n" + - "\x16declared_release_value\"\x9e\x01\n" + + "\x16declared_release_value\"\xc8\x01\n" + "\x16CatalogueAuthorization\x12/\n" + - "\x13authorized_revision\x18\x01 \x01(\x03R\x12authorizedRevision\x12S\n" + - "\x12approved_execution\x18\x02 \x01(\v2$.saas.accounts.v1.CatalogueExecutionR\x11approvedExecution\"0\n" + + "\x13authorized_revision\x18\x01 \x01(\x03R\x12authorizedRevision\x12)\n" + + "\x10inventory_digest\x18\x02 \x01(\tR\x0finventoryDigest\x12%\n" + + "\x0emember_binding\x18\x03 \x01(\tR\rmemberBinding\x12+\n" + + "\x11build_incarnation\x18\x04 \x01(\x04R\x10buildIncarnation\"0\n" + "\x16CatalogueNotAuthorized\x12\x16\n" + "\x06detail\x18\x01 \x01(\tR\x06detail\"\xa0\x02\n" + "\x13CatalogueAuthorized\x12P\n" + @@ -3574,14 +3623,20 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\x10CatalogueApplied\x12+\n" + "\x10applied_revision\x18\x01 \x01(\x03H\x00R\x0fappliedRevision\x12R\n" + "\x14applied_revision_gap\x18\x02 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x12appliedRevisionGapB\x18\n" + - "\x16applied_revision_value\"\xd6\x05\n" + + "\x16applied_revision_value\"\x94\x02\n" + + "\x1aCatalogueObservedExecution\x12\x7f\n" + + "\x17container_image_digests\x18\x01 \x03(\v2G.saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntryR\x15containerImageDigests\x12+\n" + + "\x11build_incarnation\x18\x02 \x01(\x04R\x10buildIncarnation\x1aH\n" + + "\x1aContainerImageDigestsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xde\x05\n" + "\x11CatalogueObserved\x12-\n" + "\x11observed_revision\x18\x01 \x01(\x03H\x00R\x10observedRevision\x12T\n" + "\x15observed_revision_gap\x18\x02 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x00R\x13observedRevisionGap\x12=\n" + "\vobserved_at\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampH\x01R\n" + "observedAt\x12\\\n" + - "\x19observation_freshness_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x17observationFreshnessGap\x12U\n" + - "\x12observed_execution\x18\x05 \x01(\v2$.saas.accounts.v1.CatalogueExecutionH\x02R\x11observedExecution\x12V\n" + + "\x19observation_freshness_gap\x18\x04 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x01R\x17observationFreshnessGap\x12]\n" + + "\x12observed_execution\x18\x05 \x01(\v2,.saas.accounts.v1.CatalogueObservedExecutionH\x02R\x11observedExecution\x12V\n" + "\x16observed_execution_gap\x18\x06 \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x02R\x14observedExecutionGap\x12F\n" + "\averdict\x18\a \x01(\x0e2*.saas.accounts.v1.CatalogueObservedVerdictH\x03R\averdict\x12A\n" + "\vverdict_gap\x18\b \x01(\v2\x1e.saas.accounts.v1.CatalogueGapH\x03R\n" + @@ -3629,10 +3684,14 @@ const file_saas_accounts_v1_platform_admin_proto_rawDesc = "" + "\x10build_size_valueB\x0f\n" + "\r_registration\"M\n" + "\x1cListPlatformCatalogueRequest\x12-\n" + - "\x12include_tombstoned\x18\x01 \x01(\bR\x11includeTombstoned\"\x88\x01\n" + + "\x12include_tombstoned\x18\x01 \x01(\bR\x11includeTombstoned\"\xcd\x02\n" + "\x1dListPlatformCatalogueResponse\x12:\n" + "\aentries\x18\x01 \x03(\v2 .saas.accounts.v1.CatalogueEntryR\aentries\x12+\n" + - "\x11registry_revision\x18\x02 \x01(\x03R\x10registryRevision*~\n" + + "\x11registry_revision\x18\x02 \x01(\x03R\x10registryRevision\x12{\n" + + "\x14approved_inventories\x18\x03 \x03(\v2H.saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntryR\x13approvedInventories\x1aF\n" + + "\x18ApprovedInventoriesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01*~\n" + "\x12CatalogueEntryKind\x12$\n" + " CATALOGUE_ENTRY_KIND_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bCATALOGUE_ENTRY_KIND_MODULE\x10\x01\x12!\n" + @@ -3697,7 +3756,7 @@ func file_saas_accounts_v1_platform_admin_proto_rawDescGZIP() []byte { } var file_saas_accounts_v1_platform_admin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 48) +var file_saas_accounts_v1_platform_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 50) var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ (CatalogueEntryKind)(0), // 0: saas.accounts.v1.CatalogueEntryKind (CatalogueGapReason)(0), // 1: saas.accounts.v1.CatalogueGapReason @@ -3736,12 +3795,12 @@ var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ (*UpsertFeatureFlagResponse)(nil), // 34: saas.accounts.v1.UpsertFeatureFlagResponse (*CatalogueGap)(nil), // 35: saas.accounts.v1.CatalogueGap (*CatalogueRelease)(nil), // 36: saas.accounts.v1.CatalogueRelease - (*CatalogueExecution)(nil), // 37: saas.accounts.v1.CatalogueExecution - (*CatalogueDesired)(nil), // 38: saas.accounts.v1.CatalogueDesired - (*CatalogueAuthorization)(nil), // 39: saas.accounts.v1.CatalogueAuthorization - (*CatalogueNotAuthorized)(nil), // 40: saas.accounts.v1.CatalogueNotAuthorized - (*CatalogueAuthorized)(nil), // 41: saas.accounts.v1.CatalogueAuthorized - (*CatalogueApplied)(nil), // 42: saas.accounts.v1.CatalogueApplied + (*CatalogueDesired)(nil), // 37: saas.accounts.v1.CatalogueDesired + (*CatalogueAuthorization)(nil), // 38: saas.accounts.v1.CatalogueAuthorization + (*CatalogueNotAuthorized)(nil), // 39: saas.accounts.v1.CatalogueNotAuthorized + (*CatalogueAuthorized)(nil), // 40: saas.accounts.v1.CatalogueAuthorized + (*CatalogueApplied)(nil), // 41: saas.accounts.v1.CatalogueApplied + (*CatalogueObservedExecution)(nil), // 42: saas.accounts.v1.CatalogueObservedExecution (*CatalogueObserved)(nil), // 43: saas.accounts.v1.CatalogueObserved (*CatalogueWithdrawing)(nil), // 44: saas.accounts.v1.CatalogueWithdrawing (*CatalogueRetired)(nil), // 45: saas.accounts.v1.CatalogueRetired @@ -3750,130 +3809,133 @@ var file_saas_accounts_v1_platform_admin_proto_goTypes = []any{ (*ListPlatformCatalogueRequest)(nil), // 48: saas.accounts.v1.ListPlatformCatalogueRequest (*ListPlatformCatalogueResponse)(nil), // 49: saas.accounts.v1.ListPlatformCatalogueResponse nil, // 50: saas.accounts.v1.SessionInfo.DeviceInfoEntry - (*User)(nil), // 51: saas.accounts.v1.User - (*Organization)(nil), // 52: saas.accounts.v1.Organization - (*OrgMembership)(nil), // 53: saas.accounts.v1.OrgMembership - (*timestamppb.Timestamp)(nil), // 54: google.protobuf.Timestamp - (*Installation)(nil), // 55: saas.accounts.v1.Installation - (*CollectionReadGrant)(nil), // 56: saas.accounts.v1.CollectionReadGrant - (*SolutionRegistration)(nil), // 57: saas.accounts.v1.SolutionRegistration - (*v1.GetJobOperationsRequest)(nil), // 58: saas.jobs.v1.GetJobOperationsRequest - (*v1.ListJobsRequest)(nil), // 59: saas.jobs.v1.ListJobsRequest - (*v1.GetJobRequest)(nil), // 60: saas.jobs.v1.GetJobRequest - (*v1.ReplayJobRequest)(nil), // 61: saas.jobs.v1.ReplayJobRequest - (*v11.GetEventOperationsRequest)(nil), // 62: saas.events.v1.GetEventOperationsRequest - (*v11.ListEventSubscriptionsRequest)(nil), // 63: saas.events.v1.ListEventSubscriptionsRequest - (*emptypb.Empty)(nil), // 64: google.protobuf.Empty - (*v1.GetJobOperationsResponse)(nil), // 65: saas.jobs.v1.GetJobOperationsResponse - (*v1.ListJobsResponse)(nil), // 66: saas.jobs.v1.ListJobsResponse - (*v1.GetJobResponse)(nil), // 67: saas.jobs.v1.GetJobResponse - (*v1.ReplayJobResponse)(nil), // 68: saas.jobs.v1.ReplayJobResponse - (*v11.GetEventOperationsResponse)(nil), // 69: saas.events.v1.GetEventOperationsResponse - (*v11.ListEventSubscriptionsResponse)(nil), // 70: saas.events.v1.ListEventSubscriptionsResponse + nil, // 51: saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry + nil, // 52: saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry + (*User)(nil), // 53: saas.accounts.v1.User + (*Organization)(nil), // 54: saas.accounts.v1.Organization + (*OrgMembership)(nil), // 55: saas.accounts.v1.OrgMembership + (*timestamppb.Timestamp)(nil), // 56: google.protobuf.Timestamp + (*Installation)(nil), // 57: saas.accounts.v1.Installation + (*CollectionReadGrant)(nil), // 58: saas.accounts.v1.CollectionReadGrant + (*SolutionRegistration)(nil), // 59: saas.accounts.v1.SolutionRegistration + (*v1.GetJobOperationsRequest)(nil), // 60: saas.jobs.v1.GetJobOperationsRequest + (*v1.ListJobsRequest)(nil), // 61: saas.jobs.v1.ListJobsRequest + (*v1.GetJobRequest)(nil), // 62: saas.jobs.v1.GetJobRequest + (*v1.ReplayJobRequest)(nil), // 63: saas.jobs.v1.ReplayJobRequest + (*v11.GetEventOperationsRequest)(nil), // 64: saas.events.v1.GetEventOperationsRequest + (*v11.ListEventSubscriptionsRequest)(nil), // 65: saas.events.v1.ListEventSubscriptionsRequest + (*emptypb.Empty)(nil), // 66: google.protobuf.Empty + (*v1.GetJobOperationsResponse)(nil), // 67: saas.jobs.v1.GetJobOperationsResponse + (*v1.ListJobsResponse)(nil), // 68: saas.jobs.v1.ListJobsResponse + (*v1.GetJobResponse)(nil), // 69: saas.jobs.v1.GetJobResponse + (*v1.ReplayJobResponse)(nil), // 70: saas.jobs.v1.ReplayJobResponse + (*v11.GetEventOperationsResponse)(nil), // 71: saas.events.v1.GetEventOperationsResponse + (*v11.ListEventSubscriptionsResponse)(nil), // 72: saas.events.v1.ListEventSubscriptionsResponse } var file_saas_accounts_v1_platform_admin_proto_depIdxs = []int32{ - 51, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User - 52, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization + 53, // 0: saas.accounts.v1.SearchUsersResponse.users:type_name -> saas.accounts.v1.User + 54, // 1: saas.accounts.v1.PlatformOrganization.organization:type_name -> saas.accounts.v1.Organization 6, // 2: saas.accounts.v1.ListAllOrganizationsResponse.organizations:type_name -> saas.accounts.v1.PlatformOrganization - 53, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership + 55, // 3: saas.accounts.v1.GetOrganizationRosterResponse.members:type_name -> saas.accounts.v1.OrgMembership 50, // 4: saas.accounts.v1.SessionInfo.device_info:type_name -> saas.accounts.v1.SessionInfo.DeviceInfoEntry - 54, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp - 54, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp - 54, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp - 54, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp + 56, // 5: saas.accounts.v1.SessionInfo.created_at:type_name -> google.protobuf.Timestamp + 56, // 6: saas.accounts.v1.SessionInfo.last_active_at:type_name -> google.protobuf.Timestamp + 56, // 7: saas.accounts.v1.SessionInfo.expires_at:type_name -> google.protobuf.Timestamp + 56, // 8: saas.accounts.v1.SessionInfo.idle_expires_at:type_name -> google.protobuf.Timestamp 17, // 9: saas.accounts.v1.ListActiveSessionsResponse.sessions:type_name -> saas.accounts.v1.SessionInfo 22, // 10: saas.accounts.v1.GetOrgEntitlementsResponse.entitlements:type_name -> saas.accounts.v1.EntitlementInfo - 54, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp + 56, // 11: saas.accounts.v1.PlatformAdminEntry.granted_at:type_name -> google.protobuf.Timestamp 28, // 12: saas.accounts.v1.ListPlatformAdminsResponse.admins:type_name -> saas.accounts.v1.PlatformAdminEntry 31, // 13: saas.accounts.v1.ListFeatureFlagsResponse.flags:type_name -> saas.accounts.v1.FeatureFlagEntry 1, // 14: saas.accounts.v1.CatalogueGap.reason:type_name -> saas.accounts.v1.CatalogueGapReason 35, // 15: saas.accounts.v1.CatalogueDesired.declared_revision_gap:type_name -> saas.accounts.v1.CatalogueGap 36, // 16: saas.accounts.v1.CatalogueDesired.declared_release:type_name -> saas.accounts.v1.CatalogueRelease 35, // 17: saas.accounts.v1.CatalogueDesired.declared_release_gap:type_name -> saas.accounts.v1.CatalogueGap - 37, // 18: saas.accounts.v1.CatalogueAuthorization.approved_execution:type_name -> saas.accounts.v1.CatalogueExecution - 39, // 19: saas.accounts.v1.CatalogueAuthorized.authorization:type_name -> saas.accounts.v1.CatalogueAuthorization - 40, // 20: saas.accounts.v1.CatalogueAuthorized.not_authorized:type_name -> saas.accounts.v1.CatalogueNotAuthorized - 35, // 21: saas.accounts.v1.CatalogueAuthorized.authorization_gap:type_name -> saas.accounts.v1.CatalogueGap - 35, // 22: saas.accounts.v1.CatalogueApplied.applied_revision_gap:type_name -> saas.accounts.v1.CatalogueGap + 38, // 18: saas.accounts.v1.CatalogueAuthorized.authorization:type_name -> saas.accounts.v1.CatalogueAuthorization + 39, // 19: saas.accounts.v1.CatalogueAuthorized.not_authorized:type_name -> saas.accounts.v1.CatalogueNotAuthorized + 35, // 20: saas.accounts.v1.CatalogueAuthorized.authorization_gap:type_name -> saas.accounts.v1.CatalogueGap + 35, // 21: saas.accounts.v1.CatalogueApplied.applied_revision_gap:type_name -> saas.accounts.v1.CatalogueGap + 51, // 22: saas.accounts.v1.CatalogueObservedExecution.container_image_digests:type_name -> saas.accounts.v1.CatalogueObservedExecution.ContainerImageDigestsEntry 35, // 23: saas.accounts.v1.CatalogueObserved.observed_revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 54, // 24: saas.accounts.v1.CatalogueObserved.observed_at:type_name -> google.protobuf.Timestamp + 56, // 24: saas.accounts.v1.CatalogueObserved.observed_at:type_name -> google.protobuf.Timestamp 35, // 25: saas.accounts.v1.CatalogueObserved.observation_freshness_gap:type_name -> saas.accounts.v1.CatalogueGap - 37, // 26: saas.accounts.v1.CatalogueObserved.observed_execution:type_name -> saas.accounts.v1.CatalogueExecution + 42, // 26: saas.accounts.v1.CatalogueObserved.observed_execution:type_name -> saas.accounts.v1.CatalogueObservedExecution 35, // 27: saas.accounts.v1.CatalogueObserved.observed_execution_gap:type_name -> saas.accounts.v1.CatalogueGap 2, // 28: saas.accounts.v1.CatalogueObserved.verdict:type_name -> saas.accounts.v1.CatalogueObservedVerdict 35, // 29: saas.accounts.v1.CatalogueObserved.verdict_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 30: saas.accounts.v1.CatalogueWithdrawing.withdrawal_state_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 31: saas.accounts.v1.CatalogueWithdrawing.credential_revocation_state_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 32: saas.accounts.v1.CatalogueRetired.retirement_state_gap:type_name -> saas.accounts.v1.CatalogueGap - 55, // 33: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation + 57, // 33: saas.accounts.v1.CatalogueInstallation.installation:type_name -> saas.accounts.v1.Installation 36, // 34: saas.accounts.v1.CatalogueInstallation.agent_release:type_name -> saas.accounts.v1.CatalogueRelease 35, // 35: saas.accounts.v1.CatalogueInstallation.agent_release_gap:type_name -> saas.accounts.v1.CatalogueGap 35, // 36: saas.accounts.v1.CatalogueInstallation.revision_gap:type_name -> saas.accounts.v1.CatalogueGap - 56, // 37: saas.accounts.v1.CatalogueInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant - 56, // 38: saas.accounts.v1.CatalogueInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 58, // 37: saas.accounts.v1.CatalogueInstallation.granted_teams:type_name -> saas.accounts.v1.CollectionReadGrant + 58, // 38: saas.accounts.v1.CatalogueInstallation.inherited_teams:type_name -> saas.accounts.v1.CollectionReadGrant 0, // 39: saas.accounts.v1.CatalogueEntry.kind:type_name -> saas.accounts.v1.CatalogueEntryKind 35, // 40: saas.accounts.v1.CatalogueEntry.publisher_gap:type_name -> saas.accounts.v1.CatalogueGap - 38, // 41: saas.accounts.v1.CatalogueEntry.desired:type_name -> saas.accounts.v1.CatalogueDesired - 41, // 42: saas.accounts.v1.CatalogueEntry.authorized:type_name -> saas.accounts.v1.CatalogueAuthorized - 42, // 43: saas.accounts.v1.CatalogueEntry.applied:type_name -> saas.accounts.v1.CatalogueApplied + 37, // 41: saas.accounts.v1.CatalogueEntry.desired:type_name -> saas.accounts.v1.CatalogueDesired + 40, // 42: saas.accounts.v1.CatalogueEntry.authorized:type_name -> saas.accounts.v1.CatalogueAuthorized + 41, // 43: saas.accounts.v1.CatalogueEntry.applied:type_name -> saas.accounts.v1.CatalogueApplied 43, // 44: saas.accounts.v1.CatalogueEntry.observed:type_name -> saas.accounts.v1.CatalogueObserved 44, // 45: saas.accounts.v1.CatalogueEntry.withdrawing:type_name -> saas.accounts.v1.CatalogueWithdrawing 45, // 46: saas.accounts.v1.CatalogueEntry.retired:type_name -> saas.accounts.v1.CatalogueRetired 35, // 47: saas.accounts.v1.CatalogueEntry.build_size_gap:type_name -> saas.accounts.v1.CatalogueGap - 57, // 48: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration + 59, // 48: saas.accounts.v1.CatalogueEntry.registration:type_name -> saas.accounts.v1.SolutionRegistration 46, // 49: saas.accounts.v1.CatalogueEntry.installations:type_name -> saas.accounts.v1.CatalogueInstallation 47, // 50: saas.accounts.v1.ListPlatformCatalogueResponse.entries:type_name -> saas.accounts.v1.CatalogueEntry - 3, // 51: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest - 10, // 52: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest - 11, // 53: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest - 12, // 54: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest - 14, // 55: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest - 16, // 56: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest - 19, // 57: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest - 20, // 58: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest - 23, // 59: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest - 25, // 60: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest - 26, // 61: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest - 27, // 62: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest - 5, // 63: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest - 8, // 64: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest - 30, // 65: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest - 33, // 66: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest - 58, // 67: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest - 59, // 68: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest - 60, // 69: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest - 61, // 70: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest - 62, // 71: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest - 63, // 72: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest - 48, // 73: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest - 4, // 74: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse - 64, // 75: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty - 64, // 76: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty - 13, // 77: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse - 15, // 78: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse - 18, // 79: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse - 64, // 80: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty - 21, // 81: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse - 24, // 82: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse - 64, // 83: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty - 64, // 84: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty - 29, // 85: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse - 7, // 86: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse - 9, // 87: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse - 32, // 88: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse - 34, // 89: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse - 65, // 90: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse - 66, // 91: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse - 67, // 92: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse - 68, // 93: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse - 69, // 94: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse - 70, // 95: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse - 49, // 96: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse - 74, // [74:97] is the sub-list for method output_type - 51, // [51:74] is the sub-list for method input_type - 51, // [51:51] is the sub-list for extension type_name - 51, // [51:51] is the sub-list for extension extendee - 0, // [0:51] is the sub-list for field type_name + 52, // 51: saas.accounts.v1.ListPlatformCatalogueResponse.approved_inventories:type_name -> saas.accounts.v1.ListPlatformCatalogueResponse.ApprovedInventoriesEntry + 3, // 52: saas.accounts.v1.PlatformAdminService.SearchUsers:input_type -> saas.accounts.v1.SearchUsersRequest + 10, // 53: saas.accounts.v1.PlatformAdminService.SuspendUser:input_type -> saas.accounts.v1.SuspendUserRequest + 11, // 54: saas.accounts.v1.PlatformAdminService.UnsuspendUser:input_type -> saas.accounts.v1.UnsuspendUserRequest + 12, // 55: saas.accounts.v1.PlatformAdminService.ImpersonateUser:input_type -> saas.accounts.v1.ImpersonateUserRequest + 14, // 56: saas.accounts.v1.PlatformAdminService.StopImpersonation:input_type -> saas.accounts.v1.StopImpersonationRequest + 16, // 57: saas.accounts.v1.PlatformAdminService.ListActiveSessions:input_type -> saas.accounts.v1.ListActiveSessionsRequest + 19, // 58: saas.accounts.v1.PlatformAdminService.RevokeSession:input_type -> saas.accounts.v1.RevokeSessionRequest + 20, // 59: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:input_type -> saas.accounts.v1.GetOrgEntitlementsRequest + 23, // 60: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:input_type -> saas.accounts.v1.OverrideEntitlementRequest + 25, // 61: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:input_type -> saas.accounts.v1.GrantPlatformRoleRequest + 26, // 62: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:input_type -> saas.accounts.v1.RevokePlatformRoleRequest + 27, // 63: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:input_type -> saas.accounts.v1.ListPlatformAdminsRequest + 5, // 64: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:input_type -> saas.accounts.v1.ListAllOrganizationsRequest + 8, // 65: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:input_type -> saas.accounts.v1.GetOrganizationRosterRequest + 30, // 66: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:input_type -> saas.accounts.v1.ListFeatureFlagsRequest + 33, // 67: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:input_type -> saas.accounts.v1.UpsertFeatureFlagRequest + 60, // 68: saas.accounts.v1.PlatformAdminService.GetJobOperations:input_type -> saas.jobs.v1.GetJobOperationsRequest + 61, // 69: saas.accounts.v1.PlatformAdminService.ListJobs:input_type -> saas.jobs.v1.ListJobsRequest + 62, // 70: saas.accounts.v1.PlatformAdminService.GetJob:input_type -> saas.jobs.v1.GetJobRequest + 63, // 71: saas.accounts.v1.PlatformAdminService.ReplayJob:input_type -> saas.jobs.v1.ReplayJobRequest + 64, // 72: saas.accounts.v1.PlatformAdminService.GetEventOperations:input_type -> saas.events.v1.GetEventOperationsRequest + 65, // 73: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:input_type -> saas.events.v1.ListEventSubscriptionsRequest + 48, // 74: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:input_type -> saas.accounts.v1.ListPlatformCatalogueRequest + 4, // 75: saas.accounts.v1.PlatformAdminService.SearchUsers:output_type -> saas.accounts.v1.SearchUsersResponse + 66, // 76: saas.accounts.v1.PlatformAdminService.SuspendUser:output_type -> google.protobuf.Empty + 66, // 77: saas.accounts.v1.PlatformAdminService.UnsuspendUser:output_type -> google.protobuf.Empty + 13, // 78: saas.accounts.v1.PlatformAdminService.ImpersonateUser:output_type -> saas.accounts.v1.ImpersonateUserResponse + 15, // 79: saas.accounts.v1.PlatformAdminService.StopImpersonation:output_type -> saas.accounts.v1.StopImpersonationResponse + 18, // 80: saas.accounts.v1.PlatformAdminService.ListActiveSessions:output_type -> saas.accounts.v1.ListActiveSessionsResponse + 66, // 81: saas.accounts.v1.PlatformAdminService.RevokeSession:output_type -> google.protobuf.Empty + 21, // 82: saas.accounts.v1.PlatformAdminService.GetOrgEntitlements:output_type -> saas.accounts.v1.GetOrgEntitlementsResponse + 24, // 83: saas.accounts.v1.PlatformAdminService.OverrideEntitlement:output_type -> saas.accounts.v1.OverrideEntitlementResponse + 66, // 84: saas.accounts.v1.PlatformAdminService.GrantPlatformRole:output_type -> google.protobuf.Empty + 66, // 85: saas.accounts.v1.PlatformAdminService.RevokePlatformRole:output_type -> google.protobuf.Empty + 29, // 86: saas.accounts.v1.PlatformAdminService.ListPlatformAdmins:output_type -> saas.accounts.v1.ListPlatformAdminsResponse + 7, // 87: saas.accounts.v1.PlatformAdminService.ListAllOrganizations:output_type -> saas.accounts.v1.ListAllOrganizationsResponse + 9, // 88: saas.accounts.v1.PlatformAdminService.GetOrganizationRoster:output_type -> saas.accounts.v1.GetOrganizationRosterResponse + 32, // 89: saas.accounts.v1.PlatformAdminService.ListFeatureFlags:output_type -> saas.accounts.v1.ListFeatureFlagsResponse + 34, // 90: saas.accounts.v1.PlatformAdminService.UpsertFeatureFlag:output_type -> saas.accounts.v1.UpsertFeatureFlagResponse + 67, // 91: saas.accounts.v1.PlatformAdminService.GetJobOperations:output_type -> saas.jobs.v1.GetJobOperationsResponse + 68, // 92: saas.accounts.v1.PlatformAdminService.ListJobs:output_type -> saas.jobs.v1.ListJobsResponse + 69, // 93: saas.accounts.v1.PlatformAdminService.GetJob:output_type -> saas.jobs.v1.GetJobResponse + 70, // 94: saas.accounts.v1.PlatformAdminService.ReplayJob:output_type -> saas.jobs.v1.ReplayJobResponse + 71, // 95: saas.accounts.v1.PlatformAdminService.GetEventOperations:output_type -> saas.events.v1.GetEventOperationsResponse + 72, // 96: saas.accounts.v1.PlatformAdminService.ListEventSubscriptions:output_type -> saas.events.v1.ListEventSubscriptionsResponse + 49, // 97: saas.accounts.v1.PlatformAdminService.ListPlatformCatalogue:output_type -> saas.accounts.v1.ListPlatformCatalogueResponse + 75, // [75:98] is the sub-list for method output_type + 52, // [52:75] is the sub-list for method input_type + 52, // [52:52] is the sub-list for extension type_name + 52, // [52:52] is the sub-list for extension extendee + 0, // [0:52] is the sub-list for field type_name } func init() { file_saas_accounts_v1_platform_admin_proto_init() } @@ -3885,18 +3947,18 @@ func file_saas_accounts_v1_platform_admin_proto_init() { file_saas_accounts_v1_common_proto_init() file_saas_accounts_v1_installations_proto_init() file_saas_accounts_v1_solution_registry_proto_init() - file_saas_accounts_v1_platform_admin_proto_msgTypes[35].OneofWrappers = []any{ + file_saas_accounts_v1_platform_admin_proto_msgTypes[34].OneofWrappers = []any{ (*CatalogueDesired_DeclaredRevision)(nil), (*CatalogueDesired_DeclaredRevisionGap)(nil), (*CatalogueDesired_DeclaredRelease)(nil), (*CatalogueDesired_DeclaredReleaseGap)(nil), } - file_saas_accounts_v1_platform_admin_proto_msgTypes[38].OneofWrappers = []any{ + file_saas_accounts_v1_platform_admin_proto_msgTypes[37].OneofWrappers = []any{ (*CatalogueAuthorized_Authorization)(nil), (*CatalogueAuthorized_NotAuthorized)(nil), (*CatalogueAuthorized_AuthorizationGap)(nil), } - file_saas_accounts_v1_platform_admin_proto_msgTypes[39].OneofWrappers = []any{ + file_saas_accounts_v1_platform_admin_proto_msgTypes[38].OneofWrappers = []any{ (*CatalogueApplied_AppliedRevision)(nil), (*CatalogueApplied_AppliedRevisionGap)(nil), } @@ -3933,7 +3995,7 @@ func file_saas_accounts_v1_platform_admin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_saas_accounts_v1_platform_admin_proto_rawDesc), len(file_saas_accounts_v1_platform_admin_proto_rawDesc)), NumEnums: 3, - NumMessages: 48, + NumMessages: 50, NumExtensions: 0, NumServices: 1, }, diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 814f17bbc..47d7ab54e 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -6865,11 +6865,21 @@ "type": "string", "format": "int64" }, - "approvedExecution": { - "$ref": "#/definitions/v1CatalogueExecution" + "inventoryDigest": { + "type": "string", + "description": "The approved inventory's digest, as the contract computes it over its\ncanonical bytes." + }, + "memberBinding": { + "type": "string", + "description": "Which member of that inventory's delivery aggregate this entry is." + }, + "buildIncarnation": { + "type": "string", + "format": "uint64", + "description": "The build incarnation this host assigned to the approved execution. Not an\ninventory field: this host, and only this host, assigns it — advancing it\nwhenever the approved execution changes — and a Work Context seals it beside\nthe image digest. The verdict compares it with an observation's\nincarnation, which is the same host-assigned value as presented by the\nrunning workload; an incarnation from any other assigner would make that\ncomparison meaningless, not merely wrong." } }, - "description": "CatalogueAuthorization is a current platform approval: the authorized\nrevision and the execution it approved." + "description": "CatalogueAuthorization is a current platform approval. What it approves is an\nexecution inventory, `codefly/execution-inventory/v1`, whose one definition is\nthe deployment contract module (github.com/codefly-dev/cli/contracts/deployment).\nThe host does not re-describe that document: it names the approved inventory\nby the digest approval signs, carries the inventory's canonical bytes once per\nresponse (ListPlatformCatalogueResponse.approved_inventories), and reads them\nonly through the contract." }, "v1CatalogueAuthorized": { "type": "object", @@ -6966,19 +6976,6 @@ "default": "CATALOGUE_ENTRY_KIND_UNSPECIFIED", "description": "CatalogueEntryKind distinguishes a composed module from a solution.\n\n - CATALOGUE_ENTRY_KIND_MODULE: A module composed into this deployment, known by its module principal.\n - CATALOGUE_ENTRY_KIND_SOLUTION: A solution, known by its registration, its installations, or both." }, - "v1CatalogueExecution": { - "type": "object", - "properties": { - "imageDigest": { - "type": "string" - }, - "buildIncarnation": { - "type": "string", - "format": "uint64" - } - }, - "description": "CatalogueExecution is what runs: an OCI image manifest digest and the build\nincarnation the host assigned to it, the same pair a Work Context seals." - }, "v1CatalogueGap": { "type": "object", "properties": { @@ -7066,7 +7063,7 @@ "$ref": "#/definitions/v1CatalogueGap" }, "observedExecution": { - "$ref": "#/definitions/v1CatalogueExecution" + "$ref": "#/definitions/v1CatalogueObservedExecution" }, "observedExecutionGap": { "$ref": "#/definitions/v1CatalogueGap" @@ -7080,6 +7077,22 @@ }, "description": "CatalogueObserved is what is actually running." }, + "v1CatalogueObservedExecution": { + "type": "object", + "properties": { + "containerImageDigests": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "buildIncarnation": { + "type": "string", + "format": "uint64" + } + }, + "description": "CatalogueObservedExecution is what an observation found running for an entry:\neach container's image manifest digest, keyed \"\u003cworkload id\u003e/\u003ccontainer\nname\u003e\" in the approved inventory's own names, and the build incarnation the\nrunning workload presents — the incarnation this host assigned, sealed into\nits Work Context, never one the workload or another party chose.\nObservation is the observing consumer's record, not the inventory's, so its\nshape is the host's." + }, "v1CatalogueObservedVerdict": { "type": "string", "enum": [ @@ -7089,7 +7102,7 @@ "CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED" ], "default": "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", - "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: The observed image digest and incarnation are the approved execution.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop." + "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: Every container of the approved inventory member runs its approved image\ndigest, no other container runs, and the incarnation is the approved one.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop." }, "v1CatalogueRelease": { "type": "object", @@ -9135,6 +9148,14 @@ "type": "string", "format": "int64", "description": "The registry revision the solution entries were read at." + }, + "approvedInventories": { + "type": "object", + "additionalProperties": { + "type": "string", + "format": "byte" + }, + "description": "Every approved execution inventory an entry's authorization names, keyed by\nits digest, in the deployment contract's canonical bytes (codefly-json-v1).\nCarried once per response because one delivery aggregate is approved as a\nwhole and several entries can be members of it." } } }, diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 1a002507a..7ddbb33bb 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -848,14 +848,24 @@ "type": "object" }, "v1CatalogueAuthorization": { - "description": "CatalogueAuthorization is a current platform approval: the authorized\nrevision and the execution it approved.", + "description": "CatalogueAuthorization is a current platform approval. What it approves is an\nexecution inventory, `codefly/execution-inventory/v1`, whose one definition is\nthe deployment contract module (github.com/codefly-dev/cli/contracts/deployment).\nThe host does not re-describe that document: it names the approved inventory\nby the digest approval signs, carries the inventory's canonical bytes once per\nresponse (ListPlatformCatalogueResponse.approved_inventories), and reads them\nonly through the contract.", "properties": { - "approvedExecution": { - "$ref": "#/definitions/v1CatalogueExecution" - }, "authorizedRevision": { "format": "int64", "type": "string" + }, + "buildIncarnation": { + "description": "The build incarnation this host assigned to the approved execution. Not an\ninventory field: this host, and only this host, assigns it — advancing it\nwhenever the approved execution changes — and a Work Context seals it beside\nthe image digest. The verdict compares it with an observation's\nincarnation, which is the same host-assigned value as presented by the\nrunning workload; an incarnation from any other assigner would make that\ncomparison meaningless, not merely wrong.", + "format": "uint64", + "type": "string" + }, + "inventoryDigest": { + "description": "The approved inventory's digest, as the contract computes it over its\ncanonical bytes.", + "type": "string" + }, + "memberBinding": { + "description": "Which member of that inventory's delivery aggregate this entry is.", + "type": "string" } }, "type": "object" @@ -955,19 +965,6 @@ ], "type": "string" }, - "v1CatalogueExecution": { - "description": "CatalogueExecution is what runs: an OCI image manifest digest and the build\nincarnation the host assigned to it, the same pair a Work Context seals.", - "properties": { - "buildIncarnation": { - "format": "uint64", - "type": "string" - }, - "imageDigest": { - "type": "string" - } - }, - "type": "object" - }, "v1CatalogueGap": { "description": "CatalogueGap is a Catalogue fact this host cannot state, with the reason.", "properties": { @@ -1048,7 +1045,7 @@ "type": "string" }, "observedExecution": { - "$ref": "#/definitions/v1CatalogueExecution" + "$ref": "#/definitions/v1CatalogueObservedExecution" }, "observedExecutionGap": { "$ref": "#/definitions/v1CatalogueGap" @@ -1069,9 +1066,25 @@ }, "type": "object" }, + "v1CatalogueObservedExecution": { + "description": "CatalogueObservedExecution is what an observation found running for an entry:\neach container's image manifest digest, keyed \"/\" in the approved inventory's own names, and the build incarnation the\nrunning workload presents — the incarnation this host assigned, sealed into\nits Work Context, never one the workload or another party chose.\nObservation is the observing consumer's record, not the inventory's, so its\nshape is the host's.", + "properties": { + "buildIncarnation": { + "format": "uint64", + "type": "string" + }, + "containerImageDigests": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "v1CatalogueObservedVerdict": { "default": "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", - "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: The observed image digest and incarnation are the approved execution.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop.", + "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: Every container of the approved inventory member runs its approved image\ndigest, no other container runs, and the incarnation is the approved one.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop.", "enum": [ "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", @@ -3111,6 +3124,14 @@ }, "v1ListPlatformCatalogueResponse": { "properties": { + "approvedInventories": { + "additionalProperties": { + "format": "byte", + "type": "string" + }, + "description": "Every approved execution inventory an entry's authorization names, keyed by\nits digest, in the deployment contract's canonical bytes (codefly-json-v1).\nCarried once per response because one delivery aggregate is approved as a\nwhole and several entries can be members of it.", + "type": "object" + }, "entries": { "description": "Modules first, then solutions, each ordered by name.", "items": { diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index e6830ddb1..96b2a0010 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -300,13 +300,6 @@ message CatalogueRelease { string version = 3; } -// CatalogueExecution is what runs: an OCI image manifest digest and the build -// incarnation the host assigned to it, the same pair a Work Context seals. -message CatalogueExecution { - string image_digest = 1; - uint64 build_incarnation = 2; -} - // CatalogueDesired is what the composition declares. message CatalogueDesired { oneof declared_revision_value { @@ -319,11 +312,28 @@ message CatalogueDesired { } } -// CatalogueAuthorization is a current platform approval: the authorized -// revision and the execution it approved. +// CatalogueAuthorization is a current platform approval. What it approves is an +// execution inventory, `codefly/execution-inventory/v1`, whose one definition is +// the deployment contract module (github.com/codefly-dev/cli/contracts/deployment). +// The host does not re-describe that document: it names the approved inventory +// by the digest approval signs, carries the inventory's canonical bytes once per +// response (ListPlatformCatalogueResponse.approved_inventories), and reads them +// only through the contract. message CatalogueAuthorization { int64 authorized_revision = 1; - CatalogueExecution approved_execution = 2; + // The approved inventory's digest, as the contract computes it over its + // canonical bytes. + string inventory_digest = 2; + // Which member of that inventory's delivery aggregate this entry is. + string member_binding = 3; + // The build incarnation this host assigned to the approved execution. Not an + // inventory field: this host, and only this host, assigns it — advancing it + // whenever the approved execution changes — and a Work Context seals it beside + // the image digest. The verdict compares it with an observation's + // incarnation, which is the same host-assigned value as presented by the + // running workload; an incarnation from any other assigner would make that + // comparison meaningless, not merely wrong. + uint64 build_incarnation = 4; } // CatalogueNotAuthorized is a known absence of current authorization — @@ -350,11 +360,24 @@ message CatalogueApplied { } } +// CatalogueObservedExecution is what an observation found running for an entry: +// each container's image manifest digest, keyed "/" in the approved inventory's own names, and the build incarnation the +// running workload presents — the incarnation this host assigned, sealed into +// its Work Context, never one the workload or another party chose. +// Observation is the observing consumer's record, not the inventory's, so its +// shape is the host's. +message CatalogueObservedExecution { + map container_image_digests = 1; + uint64 build_incarnation = 2; +} + // CatalogueObservedVerdict judges what runs against what is authorized — // never against what is declared, because only authorization admits execution. enum CatalogueObservedVerdict { CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; - // The observed image digest and incarnation are the approved execution. + // Every container of the approved inventory member runs its approved image + // digest, no other container runs, and the incarnation is the approved one. CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; // Something other than the approved execution is running: what cluster // admission refuses. @@ -376,11 +399,12 @@ message CatalogueObserved { CatalogueGap observation_freshness_gap = 4; } oneof observed_execution_value { - CatalogueExecution observed_execution = 5; + CatalogueObservedExecution observed_execution = 5; CatalogueGap observed_execution_gap = 6; } - // A verdict exists only when both an observation and a known authorization - // do; anything less is a gap, and an unobserved entry is never a match. + // A verdict exists only when both a complete observation and a readable + // authorization do; anything less is a gap, and an unobserved or + // incompletely observed entry is never a match. oneof verdict_value { CatalogueObservedVerdict verdict = 7; CatalogueGap verdict_gap = 8; @@ -478,6 +502,11 @@ message ListPlatformCatalogueResponse { repeated CatalogueEntry entries = 1; // The registry revision the solution entries were read at. int64 registry_revision = 2; + // Every approved execution inventory an entry's authorization names, keyed by + // its digest, in the deployment contract's canonical bytes (codefly-json-v1). + // Carried once per response because one delivery aggregate is approved as a + // whole and several entries can be members of it. + map approved_inventories = 3; } // PlatformAdminService — cross-tenant operations for platform operators. diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 6908e20dc..729e8e3da 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275", + "digest": "sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 6a532d3e0d7914c9388b701a207a035cd97272b7..78a3928696ef00a6995d088a65d2e89a6a7bcde0 100644 GIT binary patch delta 14599 zcmZ{qd3+T`w#Rd;s_WkLZ3yWkh*6*gfnY*#8y7_9bAyub1}zpgr6 z^{rEY#Y3jQJmjXfL>p`4ZKBO-lWobiR9kVI+m>G9J$f};wx#_aS9_P0w*P6A z*P0NoCfx3YMa#YJg->$csh1CQyXBh3W}8~`&Ex)DpBtHNY3;#+L{;$>C9!@1$$me# zYh$)?c((b^!*fmbxu%glI5q5|)n90_FnE;Ly-eVVxT4$eF}a5NOPcC3%}ts1#_K(o zoj;4YmBTWvnTGtxG1=b^Z^<@~&DLM|mu%gb)?B{H58Y$x@=dLoToc}tYs`$yno+g% zNCdnped;d@FOKq#D?HokrOt`F)eejLT%x+@0k>S()Y?4mii+4@$7gDogL+h6jKTbe zu7&FkcR4Mwqx~-p-kFKSp9+f_@yZi+I^~7wH+U&}dHdc*@BNfm$K0NU#1yY*;iIu$ zF7W@?EzgV|-JHjy{u}|!Wm|ws4gTW7oU-EXg*R@7lP*)d(hF+OH;pebi-5Lz*kzHM z4o@mCa(n!H2Q}FKo_B>aHq$UBJA|E;XuteMPx|iWFA{FjcZ!N0Y0pgY?jDr>h08=A zUoK=tPZv&p+B-Mwlkmktmg?blb%p4YD)Qwb#-3u_?czGh#^XgS)mxja$nnKuCW>88 zwPr7B2VB2y26DMrB%qTx(!ft|iE|1eqo`$VSl}L=iEr zCb+IHU!NV(Fm7=CbWr?s@YqwXs|ju#m2b%g`KD}uhPkGk+8=P~2Cbu%G`b-_t`QqI zQ1RWI3F}&e#(e#lhHOwZGS@n4%<$T}eB)sAmO=H|v4iUxaY8)Ivv?zBR-dmMqh2^f&9N!dh<0iuX0{{i?d;rO_&7aE z${eOirXgs_jcjVE36Kw(5$Mz;p7ekg=uP>iTpbz?AJ>ZK`KG#TFgn}pVlXYE^G(1} zMVl?Gjdn-JO*XIwxa-k2Q%}AdU3h3vBWcc$899nx7tO!cb<-{veX+(bMzPeFBK#%B zk`zlYeE~~HI4rb%iKSG8hmL@afOshi@xCm9-Laanlsgi;114F$Dt}LCqv2Xcn66H@ z7_=r44Gjz0IC_klKR6?iG~+K;y(63uPcZ|vOYtUpRq?TriD9i~$M-ZQ!?4zGP z!uEi5eB}j*)?D4FK>5^lO!P{OU=6F(sh}_87{Piqg0DFZ0Z(D}uH4HB4V@8qF&z>V`mvd&Ivi`j z?PBv~o3e_1hY+ui-x@Xrb)zy(I4uL}im1y3*W{aT2!>&QW&hF|v}7|42S;_Bm=Pi9SGHg7@t}_%tei_1)Gs3 zH7Eu~XMu{W(eR2D14F$hv6sd)HK}Rdi19Y$Gxg`Fmv!(SjA+g`LK_$4;k+46Xc3yx zVN64P(3rU)`$xPs14??>xfUd*#%xA;ZOFD@gRA_}m~GBt^WE5-ZyFiLs>-SBS#hO_ z6qQrgv!1=&y$R&h6Mean6}?kfxWe1@+Y`}pW3)3*q@B4*1(7;?VEO`9Ik|y_wwqXS zNo2z7Cgzv9m&0j4Uv6bZ?-$-*>0K7~L&L3V3#;?4pE~chG4`H0SzzPwHoe0g(T^43 zblA>BCA||SS%c^$u+VlpJFY4+@y_k6y2hC5ynEMnMO#?4+2|?R$LV9JA4{O`yR0Vi ztPZpt(V^6jVaLK6V7jWUwu6=SiA;3e!TR-g&&O2z`*Jrc`l!(DS#N0A9}Ra$M|*!Y z)jf>uHB*I+$9p2Y{R!^}(a{dm7qIl<4GV2QK-os`go$^4pcYM~pZ-iu^}}emFv&Ve zAMG&7Hisxo^!-pBz#nl8cz`dzWJO=$5Z%Iq|Gy|yVSb~^PCwlg!HE1AmF`VAkH{lb zn^bmc9yi2AhR9%psGLf*B=VWweT@jy5vX$G?0Z9Y95zRvq0x2-a`op89vXLa>8Lxe z4|d2;b3vSL;5vU}lRgo3ropxega4G*kO!jS#;-FQRA&;kn>%jBe@}%qlv*(29BOuC zh1QmzBp&QNN?W0-8JH92aPpOtsnbDX+OGqzQiFSZGC)8Ox0HN?DOX<)ZK)dSzV->W! zVEO{qsT5SO(Dnf9uD1kCgv|kV@+s!2I%*F_$zXsAIaoScVUgL}K%4R=5nHdSPaJAL2^e!C z#J&&)I{B($V;0}DQ%_SAVX{Q2FMedK#vMhoz|$WKe|X-zzW0x8@X4l;O4#E$JH;JC zZNPFoFYI~V8{2I>KmBL!dDz8OzMRO5?k!yZ0&Wx&dFoiou2m}KOycZTu5S~t;c5~$ zw}~oBIg`1$O~CX8EM05CLfgsQ+$Lb+y_31QO;l0JnZnI&0;VruRjUyX6fns)w+Wc& zJB8ymaW!&&RSLI>shr)x3%`BQyDFT@Q++9@s+dZ<)41qK@d%Tw+UOyCPUF3F#>PYV zoW@PYu43vMal0K49>N-Ix+;RV^HP1QKwEk`sFomwRVf7Ibk6RKDXG~^=cx#Ufa@7t z^fIFoWSwDL3$&fVd+Tjc#ROc>P^+R6a}{&Yb*3E^_6#h2K;a>}&g3VS>T?A)`p)Dh zRl92t(!j)hS>Ws*&MTJ{R=?!+J&n%#hAfJ}4s{6f~OockZj zCWUURyuqb;l=?hpo z=3&9<3SOZPB$#lzf}@sF+ozf~^~#71Okco?x~fgRlAAIN59ukoUWs)ssa6a99A__J z_Y4cJD!kCuttj+*%{%kj=Xffbk=oeLTld)5u%e-;jr}|?ji{=P{X7qJTvsy(jbGu+ zb+<6Q4mQm8g|B&kNQAHOw64pb!pAGTx8CqD;o}vq^E&!S_*kQSsCpVES#&Zei_F-Q z+Jb9%Z=DPA6rR@bQ%-ePVIPSaU#{mxpYlZI5*D;idEFbD`2G55nl%&OC-h z0ZR`N7979DEA-73CLF)T&CRuj0%C&-h*s)~wi^n))_G%3*uaas&?2D?x@_RxO5ICU z8x;l{Ion)Vy3YGmxRIyyjkJa-M4PzKvxP}k^jZ}Ln|NAZCh!n0H}PY7m^Y~0{!X+` zHR>TOeP+Ny*LQenG#?cP@9-1!Duy*ot??Gld^33+c*oO*J2Pbqt~j%+-6^^7IT|@ zt;(m{IV&?GMm~k5N>wF3mUAL=zs`g6_OQGw#YRBr8PK9LZ4TGoX`z}95 zqk)OO??!vAh83gl4hyQ7tjKXOy6&(eE=JcKyk~EBqdKW-)Lz@g*&gh*mlMHd_->4H zd<0Ib!guSvRoCuPxlj+##Y=YEm2}C9kh*xuZeAKua`BSgyibHQjiuYCcFS6|<7!wM zW_ypfy~`5eKAw&`rQu^A5A=>hBly_I2kXU*RmzyJ zV8ce$%2{pYUzw?{Gb(t~oN85knGWYlM- zJU-Z}Dyl_Y+cK9n&;?d~9K}cie0tR%=qRRht>!xlm;mJWyh0aAFaiC0RgdC4WD;5M zW>NG=;jVvqQ_F4^C~Qu4g)GAI$q9l@7lqR|dbQyMA?Y|(4}f@r@F|9r32zg`;9s~U zuA@xh%Qhiy7t;0J6bVn?j{2}oq;D6FTj~}^8U}TXko`MK@H|n-ejO!*`XnL4-tgu| zJ(FlRS#+)L*e->-UC7frN=OX13R%-p!ZyB5$kRGX*qu{5b;NF)CM4C(dPX9=Lx^+Y zUW?W~)5jNgi1d(nR7%3A?hwJ*s(eE?WSSX*%@=f`Q8wl~!%|e&s~MJ}I+|vP!50{c z2~eCV#6uCqkR=3RoGH=|#TZG0+dC~rlHhul#Yhq`-X&yjx6Dneu_$NB1a@daVEbyc zT14NwMXJiol{BCY@3DwT1KWEoTav)`KFgLQu)V)iN3eZB$RQp5m-ymAA$bJrrcx2qTEwa^;4KW({V%dZq{eaxL|U?J%i2hbtT zZ?RHzr{fPc)x_e=-x%;5V|hl1wNd;tuDL%W(raVxq=EahmODxC{G8=Z61YDvz5|&}LT?Ue{46oQ_APLK`M#x$d>m*?rUR9|j z)QONZEW>Md8PwEY7vjx$8Ms-7*G2lxcp0>aWq3n*)61YWEW5dupm{XLv-2^Gx(R8mc%V(&rj{TEeJ~h+wEm*V30q1)JtXr>44q9JT3M zqV-XmuGM|ys7=?nrph0LxXm%yI>g15PR$=g`ZgyjK1hSxA1y|b;QA&fEc%?VDbznO#<(YMWsPfgN*Hk|0h6$xozJIRSF5|Y4nvJ+P%B!TUeP94GaRwu4V z)OCESBYtDhN`rQ)lm2x)DixU(D5s}7!DVh0g=P{T4;}V!;mn=hfN;7Kho%aQ=}sJ) z>ZUf`i9<69s52b#Pz04FHPabR`k|O9X<$0ji3>WC;Q3BRR=ed?ypiOfu$<+D@@^+- z#!oU(&sL~)DkDja>mDaA^GE~Ldo3H10QEjcp4`zHpx$qxQY%1xphHJG|3LLY)l8QH zn5dZJh~LMK==6h>{;zmcO2Vk-IKee0U{k)F>#+7XVAa)it|RHTf_;uJwlFK{=B7-r zo$LJkS~t|mCk43k9PwlfH>KgubJ9=7a7lyP`4%ooaJ|3=EJ>jFhz(ehC}1CTQq^V> zS`5+o?>1sd1lz|fB9g%NaT~BCf$bBPElFUzuv15%UF4uJ)5jdO!ZEkliORgI@lDm2 zPdQ>4rmpL}lu5f!Iq7AXyAA{`Vr7;%^1PpDNV`iNq}}rO_(=l()fPWV!2e3ejv;a$_^)x|GMk!# z|En>7rl!8u5$j_7#RmUcC%rDluSMX0-H|866ELUV8w$U^?6~-@=dkznE(j6m>I=zw zn_^tdcfCz9>VwI8=iDpIGPoGaTaMTfFN3R>;Vmb_2Dg@w$=`E=UmDzLU+#3+{y6EVZ%I32+96A0 zUOO#r^=)aV&FJ_(C3iXE(-?PJWkZFju@&bp3c$+<7ZC#9D`9y z7}aM^FvR^IN*bOo4>;@xhgV)76#jd>S6+DRBk#KKfFtQUE5g`AhXc-W(We0QY3+b> z{+~?T;v=0r=!l~+NKb=2=%kOvAV~wruPjKC;Q5dRNfJPQjr0*6f}|;shb>6b0P-8l zgCv0b)`BDnAdgs(Bmv}iojL;K_o}j(i0`AAa2H=X<0ZRY7E1PcZQ*z+=?56~1Opi_ zkB%(Zx&XxlDW=KjqTEHJm>|>BWK?;P1{7^Ft`bOs=UZf4d65JZ6QxB# znnE#2#`OScKrvayMHWdwF-68j7D+&HtBi{*l7Qm2P91?_ssxIwh(uSEJzTN}3bQ`( zt_){LNx#JaL{|)BhCGHow<;4LX2`QIHz2wK#7rsfi$QeNAZE(+eK82q0OC#yf+TpJ zWkHYx5O;L|L7D>bR||qPfGF7ckOUBSTM#4x#B2+KB!IZ5Q%8WfSH_zQGZI7YmtvU7 zQC-dEx?kd7g!L({C5-BR8JzF_RfkfEFXu?MtkC;2@2+r;jF03Jw3{R2Be?|p=15#L z^;Ryy+a8wqVZEy>LAOMlU~FA5;jx@6#o`!miN-rurWeO}Ndw+_G2WhVgsg-&&$oO@ z0^bFN-|hFx!@tS6MxrN*@}n{?>PZ9Te^^)~f%0RPGD)EPxTQ=IC_mAuBPcIaly!+z zGD-~)hr=Qn)kqk#FQ1fRDJH8+q!M#RKPl5oFR_)uBN4!?;0t)8f>L#y+)SMKb9bH zOF?msJmX43u@n?vmExZf#gLV1jIYY{KVyuf!R>1nBS~<*)?y?H7+;t1eTgJR@eNCn zG*Db;VUPrhZ^|%EkfZ_QdW(@HV0^1nPhfo8V#Jih1}UNdE;ST4$n@EUqLwhK4Kg^( zQ1tPmt7N;v7^9E*Zj{n97=1L~D7(-vfxzek<3@SvuMI{Y7&l4reuNQ?v1^o;em|y2 z8oa(^DUyW7nLab(-`;2^p`P4(%|+3i;*O_{xHG_*M{O=DF>R*t0Ymq z{jtP9OzDiIMfBZADOK0VB!caymKsT5`tV)Cz1rw`{2u*nXiRR38?Yr#v9V zAxu-}&oYxg56GeELzrkZG=&_?a8RB89W^A-uN2g`bpkE-~@~<1Z-X znAZ_`?D>X%IdC75myI#QD913qlj4?SR2!9R+TY3aEy<`hA`P^^x3o!u=cAT3Nr>4W zI%H7NRM7rtgO)Uay(t;jMkE33&B=J{lLWBilW}cC62MOA)Dd9Yl5xypM)0Qp$XawUrtW4yOL1`t-yRICnf!h0HU-4ttTgYUV_%@r!~CB?4{#tMURRx*8w!KfvS zYF0A1*nLpPVRv87PBMnN||V{cSjt~PR2EQcL2O6Ddxp-*j@9x zCz+lX^CJ!X?zQ|#g6I1zKa#-j{-mwZNmBqHumDK|zy~b`k^t}_%a0`Rn`8Nr1bz>9 r>Ii;wEkDdiRLxI{pBsSP4Z!)ybo8-EOBmJsN##ep_VOcLmOt=+n1dR_ delta 12230 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 MX}XEGeSO;h0e|!&%>V!Z diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index c92e85560..24badf9f0 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:b03955a649f5aa5fe76b56c05c12ac1905c61d27aaf1334d0b9b993b34b5d275 + contract-digest: sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b services: - APIKeyService - AccessibleScopeService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index 907e9a8c5..1a317518c 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIrgDChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWdyYW50ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAcgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50QhUKE2FnZW50X3JlbGVhc2VfdmFsdWVCEAoOcmV2aXNpb25fdmFsdWUi4wUKDkNhdGFsb2d1ZUVudHJ5EjIKBGtpbmQYASABKA4yJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5S2luZBIMCgRuYW1lGAIgASgJEhMKCXB1Ymxpc2hlchgDIAEoCUgAEjcKDXB1Ymxpc2hlcl9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjMKB2Rlc2lyZWQYBSABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZURlc2lyZWQSOQoKYXV0aG9yaXplZBgGIAEoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXplZBIzCgdhcHBsaWVkGAcgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBcHBsaWVkEjUKCG9ic2VydmVkGAggASgLMiMuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZBI7Cgt3aXRoZHJhd2luZxgJIAEoCzImLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlV2l0aGRyYXdpbmcSMwoHcmV0aXJlZBgKIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmV0aXJlZBI4Cg5idWlsZF9zaXplX2dhcBgLIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESQQoMcmVnaXN0cmF0aW9uGAwgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgCiAEBEj4KDWluc3RhbGxhdGlvbnMYDSADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCEgoQYnVpbGRfc2l6ZV92YWx1ZUIPCg1fcmVnaXN0cmF0aW9uIjoKHExpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QSGgoSaW5jbHVkZV90b21ic3RvbmVkGAEgASgIIm0KHUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlEjEKB2VudHJpZXMYASADKAsyIC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5EhkKEXJlZ2lzdHJ5X3JldmlzaW9uGAIgASgDKn4KEkNhdGFsb2d1ZUVudHJ5S2luZBIkCiBDQVRBTE9HVUVfRU5UUllfS0lORF9VTlNQRUNJRklFRBAAEh8KG0NBVEFMT0dVRV9FTlRSWV9LSU5EX01PRFVMRRABEiEKHUNBVEFMT0dVRV9FTlRSWV9LSU5EX1NPTFVUSU9OEAIqrwEKEkNhdGFsb2d1ZUdhcFJlYXNvbhIkCiBDQVRBTE9HVUVfR0FQX1JFQVNPTl9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRUNPUkRFRBABEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRVBPUlRFRBACEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9PQlNFUlZFRBADKt4BChhDYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3QSKgomQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfVU5TUEVDSUZJRUQQABIxCi1DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0FVVEhPUklaRUQQARIuCipDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0RJRkZFUlMQAhIzCi9DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX1VOQVVUSE9SSVpFRBADMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWViBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzLvHwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -886,31 +886,6 @@ export type CatalogueRelease = Message<"saas.accounts.v1.CatalogueRelease"> & { export const CatalogueReleaseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 33); -/** - * CatalogueExecution is what runs: an OCI image manifest digest and the build - * incarnation the host assigned to it, the same pair a Work Context seals. - * - * @generated from message saas.accounts.v1.CatalogueExecution - */ -export type CatalogueExecution = Message<"saas.accounts.v1.CatalogueExecution"> & { - /** - * @generated from field: string image_digest = 1; - */ - imageDigest: string; - - /** - * @generated from field: uint64 build_incarnation = 2; - */ - buildIncarnation: bigint; -}; - -/** - * Describes the message saas.accounts.v1.CatalogueExecution. - * Use `create(CatalogueExecutionSchema)` to create a new message. - */ -export const CatalogueExecutionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 34); - /** * CatalogueDesired is what the composition declares. * @@ -957,11 +932,16 @@ export type CatalogueDesired = Message<"saas.accounts.v1.CatalogueDesired"> & { * Use `create(CatalogueDesiredSchema)` to create a new message. */ export const CatalogueDesiredSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 35); + messageDesc(file_saas_accounts_v1_platform_admin, 34); /** - * CatalogueAuthorization is a current platform approval: the authorized - * revision and the execution it approved. + * CatalogueAuthorization is a current platform approval. What it approves is an + * execution inventory, `codefly/execution-inventory/v1`, whose one definition is + * the deployment contract module (github.com/codefly-dev/cli/contracts/deployment). + * The host does not re-describe that document: it names the approved inventory + * by the digest approval signs, carries the inventory's canonical bytes once per + * response (ListPlatformCatalogueResponse.approved_inventories), and reads them + * only through the contract. * * @generated from message saas.accounts.v1.CatalogueAuthorization */ @@ -972,9 +952,32 @@ export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthoriz authorizedRevision: bigint; /** - * @generated from field: saas.accounts.v1.CatalogueExecution approved_execution = 2; + * The approved inventory's digest, as the contract computes it over its + * canonical bytes. + * + * @generated from field: string inventory_digest = 2; + */ + inventoryDigest: string; + + /** + * Which member of that inventory's delivery aggregate this entry is. + * + * @generated from field: string member_binding = 3; + */ + memberBinding: string; + + /** + * The build incarnation this host assigned to the approved execution. Not an + * inventory field: this host, and only this host, assigns it — advancing it + * whenever the approved execution changes — and a Work Context seals it beside + * the image digest. The verdict compares it with an observation's + * incarnation, which is the same host-assigned value as presented by the + * running workload; an incarnation from any other assigner would make that + * comparison meaningless, not merely wrong. + * + * @generated from field: uint64 build_incarnation = 4; */ - approvedExecution?: CatalogueExecution; + buildIncarnation: bigint; }; /** @@ -982,7 +985,7 @@ export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthoriz * Use `create(CatalogueAuthorizationSchema)` to create a new message. */ export const CatalogueAuthorizationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 36); + messageDesc(file_saas_accounts_v1_platform_admin, 35); /** * CatalogueNotAuthorized is a known absence of current authorization — @@ -1003,7 +1006,7 @@ export type CatalogueNotAuthorized = Message<"saas.accounts.v1.CatalogueNotAutho * Use `create(CatalogueNotAuthorizedSchema)` to create a new message. */ export const CatalogueNotAuthorizedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 37); + messageDesc(file_saas_accounts_v1_platform_admin, 36); /** * CatalogueAuthorized is what the platform has approved to execute. @@ -1040,7 +1043,7 @@ export type CatalogueAuthorized = Message<"saas.accounts.v1.CatalogueAuthorized" * Use `create(CatalogueAuthorizedSchema)` to create a new message. */ export const CatalogueAuthorizedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 38); + messageDesc(file_saas_accounts_v1_platform_admin, 37); /** * CatalogueApplied is what delivery actually wrote. @@ -1071,6 +1074,36 @@ export type CatalogueApplied = Message<"saas.accounts.v1.CatalogueApplied"> & { * Use `create(CatalogueAppliedSchema)` to create a new message. */ export const CatalogueAppliedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 38); + +/** + * CatalogueObservedExecution is what an observation found running for an entry: + * each container's image manifest digest, keyed "/" in the approved inventory's own names, and the build incarnation the + * running workload presents — the incarnation this host assigned, sealed into + * its Work Context, never one the workload or another party chose. + * Observation is the observing consumer's record, not the inventory's, so its + * shape is the host's. + * + * @generated from message saas.accounts.v1.CatalogueObservedExecution + */ +export type CatalogueObservedExecution = Message<"saas.accounts.v1.CatalogueObservedExecution"> & { + /** + * @generated from field: map container_image_digests = 1; + */ + containerImageDigests: { [key: string]: string }; + + /** + * @generated from field: uint64 build_incarnation = 2; + */ + buildIncarnation: bigint; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueObservedExecution. + * Use `create(CatalogueObservedExecutionSchema)` to create a new message. + */ +export const CatalogueObservedExecutionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 39); /** @@ -1120,9 +1153,9 @@ export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & */ observedExecutionValue: { /** - * @generated from field: saas.accounts.v1.CatalogueExecution observed_execution = 5; + * @generated from field: saas.accounts.v1.CatalogueObservedExecution observed_execution = 5; */ - value: CatalogueExecution; + value: CatalogueObservedExecution; case: "observedExecution"; } | { /** @@ -1133,8 +1166,9 @@ export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & } | { case: undefined; value?: undefined }; /** - * A verdict exists only when both an observation and a known authorization - * do; anything less is a gap, and an unobserved entry is never a match. + * A verdict exists only when both a complete observation and a readable + * authorization do; anything less is a gap, and an unobserved or + * incompletely observed entry is never a match. * * @generated from oneof saas.accounts.v1.CatalogueObserved.verdict_value */ @@ -1446,6 +1480,16 @@ export type ListPlatformCatalogueResponse = Message<"saas.accounts.v1.ListPlatfo * @generated from field: int64 registry_revision = 2; */ registryRevision: bigint; + + /** + * Every approved execution inventory an entry's authorization names, keyed by + * its digest, in the deployment contract's canonical bytes (codefly-json-v1). + * Carried once per response because one delivery aggregate is approved as a + * whole and several entries can be members of it. + * + * @generated from field: map approved_inventories = 3; + */ + approvedInventories: { [key: string]: Uint8Array }; }; /** @@ -1541,7 +1585,8 @@ export enum CatalogueObservedVerdict { UNSPECIFIED = 0, /** - * The observed image digest and incarnation are the approved execution. + * Every container of the approved inventory member runs its approved image + * digest, no other container runs, and the incarnation is the approved one. * * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; */ diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts index d13b817ee..d23fad40f 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts @@ -44,7 +44,10 @@ function observedWith(verdict: CatalogueObservedVerdict) { return { observedExecutionValue: { case: "observedExecution" as const, - value: { imageDigest: "sha256:bbb", buildIncarnation: BigInt(3) }, + value: { + containerImageDigests: { "worker/worker": "sha256:bbb" }, + buildIncarnation: BigInt(3), + }, }, verdictValue: { case: "verdict" as const, value: verdict }, }; @@ -152,16 +155,20 @@ describe("authorization", () => { case: "authorization", value: { authorizedRevision: BigInt(4), - approvedExecution: { - imageDigest: "sha256:aaa", - buildIncarnation: BigInt(3), - }, + inventoryDigest: "sha256:aaa", + memberBinding: "member", + buildIncarnation: BigInt(3), }, }, }, }), ), - ).toMatchObject({ kind: "value", text: "Revision 4", tone: "success" }); + ).toMatchObject({ + kind: "value", + text: "Revision 4", + tone: "success", + detail: "Approved inventory sha256:aaa, member member, incarnation 3.", + }); expect( authorizationView( entry({ diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts index 5eb0ebd1c..23d238b8c 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts @@ -2,12 +2,13 @@ import type { Timestamp } from "@bufbuild/protobuf/wkt"; import { timestampDate } from "@bufbuild/protobuf/wkt"; import type { ComponentProps } from "react"; import { + type CatalogueAuthorization, type CatalogueEntry, CatalogueEntryKind, - type CatalogueExecution, type CatalogueGap, CatalogueGapReason, type CatalogueInstallation, + type CatalogueObservedExecution, CatalogueObservedVerdict, type CatalogueRelease, } from "@/gen/saas/accounts/v1/platform_admin_pb"; @@ -88,11 +89,24 @@ export function releaseText(release: CatalogueRelease): string { return `${release.publisher}/${release.name}@${release.version}`; } -export function executionText( - execution: CatalogueExecution | undefined, +/** + * An approval names the execution inventory it approved by the digest approval + * signs, and the member of that inventory this entry is. The inventory itself is + * the deployment contract's document; the page names it, and does not re-read + * its fields. + */ +export function approvalText(approval: CatalogueAuthorization): string { + return `Approved inventory ${approval.inventoryDigest}, member ${approval.memberBinding}, incarnation ${approval.buildIncarnation}.`; +} + +export function observationText( + execution: CatalogueObservedExecution | undefined, ): string { - if (!execution) return "unknown"; - return `${execution.imageDigest || "no digest"} (incarnation ${execution.buildIncarnation})`; + if (!execution) return "Nothing observed."; + const containers = Object.entries(execution.containerImageDigests) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([container, digest]) => `${container} ${digest}`); + return `Observed ${containers.join(", ") || "no containers"} at incarnation ${execution.buildIncarnation}.`; } export function kindLabel(kind: CatalogueEntryKind): string { @@ -173,7 +187,7 @@ export function authorizationView(entry: CatalogueEntry): FactView { kind: "value", text: `Revision ${v.value.authorizedRevision}`, tone: "success", - detail: `Approved ${executionText(v.value.approvedExecution)}.`, + detail: approvalText(v.value), }; } if (v?.case === "notAuthorized") { @@ -200,7 +214,7 @@ export function observedVerdictView(entry: CatalogueEntry): FactView { observed?.observedExecutionValue.case === "observedExecution" ? observed.observedExecutionValue.value : undefined; - const detail = `Observed ${executionText(running)}.`; + const detail = observationText(running); switch (v.value) { case CatalogueObservedVerdict.RUNNING_AUTHORIZED: return { diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx index 7bc09d62a..0138dcfbb 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx @@ -15,7 +15,12 @@ const notObserved = (detail: string) => ({ reason: "CATALOGUE_GAP_REASON_NOT_OBSERVED", detail, }); -const approved = { imageDigest: "sha256:aaa", buildIncarnation: "3" }; +const approved = { + authorizedRevision: "4", + inventoryDigest: "sha256:aaa", + memberBinding: "member", + buildIncarnation: "3", +}; // What this host has today for every entry: each state's facts as gaps. const unknownStates = { @@ -43,7 +48,10 @@ function observedRunning(verdict: string, imageDigest: string) { return { observedRevisionGap: notObserved("No cluster state."), observationFreshnessGap: notObserved("No cluster state."), - observedExecution: { imageDigest, buildIncarnation: "3" }, + observedExecution: { + containerImageDigests: { "worker/worker": imageDigest }, + buildIncarnation: "3", + }, verdict, }; } @@ -75,7 +83,7 @@ const catalogue = { }, solution("drifted-solution", { authorized: { - authorization: { authorizedRevision: "4", approvedExecution: approved }, + authorization: approved, }, observed: observedRunning( "CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS", @@ -121,7 +129,7 @@ const catalogue = { }), solution("steady-solution", { authorized: { - authorization: { authorizedRevision: "4", approvedExecution: approved }, + authorization: approved, }, observed: observedRunning( "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index e6f576bf8..ebe6a298c 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -27,7 +27,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/platform_admin.proto. */ export const file_saas_accounts_v1_platform_admin: GenFile = /*@__PURE__*/ - fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkiRQoSQ2F0YWxvZ3VlRXhlY3V0aW9uEhQKDGltYWdlX2RpZ2VzdBgBIAEoCRIZChFidWlsZF9pbmNhcm5hdGlvbhgCIAEoBCKlAgoQQ2F0YWxvZ3VlRGVzaXJlZBIbChFkZWNsYXJlZF9yZXZpc2lvbhgBIAEoA0gAEj8KFWRlY2xhcmVkX3JldmlzaW9uX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASPgoQZGVjbGFyZWRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgBEj4KFGRlY2xhcmVkX3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIZChdkZWNsYXJlZF9yZXZpc2lvbl92YWx1ZUIYChZkZWNsYXJlZF9yZWxlYXNlX3ZhbHVlIncKFkNhdGFsb2d1ZUF1dGhvcml6YXRpb24SGwoTYXV0aG9yaXplZF9yZXZpc2lvbhgBIAEoAxJAChJhcHByb3ZlZF9leGVjdXRpb24YAiABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbiIoChZDYXRhbG9ndWVOb3RBdXRob3JpemVkEg4KBmRldGFpbBgBIAEoCSLwAQoTQ2F0YWxvZ3VlQXV0aG9yaXplZBJBCg1hdXRob3JpemF0aW9uGAEgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBdXRob3JpemF0aW9uSAASQgoObm90X2F1dGhvcml6ZWQYAiABKAsyKC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU5vdEF1dGhvcml6ZWRIABI7ChFhdXRob3JpemF0aW9uX2dhcBgDIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCFQoTYXV0aG9yaXphdGlvbl92YWx1ZSKIAQoQQ2F0YWxvZ3VlQXBwbGllZBIaChBhcHBsaWVkX3JldmlzaW9uGAEgASgDSAASPgoUYXBwbGllZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhgKFmFwcGxpZWRfcmV2aXNpb25fdmFsdWUizAQKEUNhdGFsb2d1ZU9ic2VydmVkEhsKEW9ic2VydmVkX3JldmlzaW9uGAEgASgDSAASPwoVb2JzZXJ2ZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIxCgtvYnNlcnZlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIARJDChlvYnNlcnZhdGlvbl9mcmVzaG5lc3NfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARJCChJvYnNlcnZlZF9leGVjdXRpb24YBSABKAsyJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUV4ZWN1dGlvbkgCEkAKFm9ic2VydmVkX2V4ZWN1dGlvbl9nYXAYBiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgCEj0KB3ZlcmRpY3QYByABKA4yKi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZU9ic2VydmVkVmVyZGljdEgDEjUKC3ZlcmRpY3RfZ2FwGAggASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIA0IZChdvYnNlcnZlZF9yZXZpc2lvbl92YWx1ZUIdChtvYnNlcnZhdGlvbl9mcmVzaG5lc3NfdmFsdWVCGgoYb2JzZXJ2ZWRfZXhlY3V0aW9uX3ZhbHVlQg8KDXZlcmRpY3RfdmFsdWUi4AEKFENhdGFsb2d1ZVdpdGhkcmF3aW5nEj4KFHdpdGhkcmF3YWxfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABJJCh9jcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAUIYChZ3aXRoZHJhd2FsX3N0YXRlX3ZhbHVlQiMKIWNyZWRlbnRpYWxfcmV2b2NhdGlvbl9zdGF0ZV92YWx1ZSJsChBDYXRhbG9ndWVSZXRpcmVkEj4KFHJldGlyZW1lbnRfc3RhdGVfZ2FwGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZyZXRpcmVtZW50X3N0YXRlX3ZhbHVlIrgDChVDYXRhbG9ndWVJbnN0YWxsYXRpb24SNAoMaW5zdGFsbGF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24SEAoIb3JnX25hbWUYAiABKAkSOwoNYWdlbnRfcmVsZWFzZRgDIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmVsZWFzZUgAEjsKEWFnZW50X3JlbGVhc2VfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABI2CgxyZXZpc2lvbl9nYXAYBSABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEjwKDWdyYW50ZWRfdGVhbXMYBiADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAcgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50QhUKE2FnZW50X3JlbGVhc2VfdmFsdWVCEAoOcmV2aXNpb25fdmFsdWUi4wUKDkNhdGFsb2d1ZUVudHJ5EjIKBGtpbmQYASABKA4yJC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5S2luZBIMCgRuYW1lGAIgASgJEhMKCXB1Ymxpc2hlchgDIAEoCUgAEjcKDXB1Ymxpc2hlcl9nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjMKB2Rlc2lyZWQYBSABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZURlc2lyZWQSOQoKYXV0aG9yaXplZBgGIAEoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXplZBIzCgdhcHBsaWVkGAcgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVBcHBsaWVkEjUKCG9ic2VydmVkGAggASgLMiMuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZBI7Cgt3aXRoZHJhd2luZxgJIAEoCzImLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlV2l0aGRyYXdpbmcSMwoHcmV0aXJlZBgKIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlUmV0aXJlZBI4Cg5idWlsZF9zaXplX2dhcBgLIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAESQQoMcmVnaXN0cmF0aW9uGAwgASgLMiYuc2Fhcy5hY2NvdW50cy52MS5Tb2x1dGlvblJlZ2lzdHJhdGlvbkgCiAEBEj4KDWluc3RhbGxhdGlvbnMYDSADKAsyJy5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUluc3RhbGxhdGlvbkIRCg9wdWJsaXNoZXJfdmFsdWVCEgoQYnVpbGRfc2l6ZV92YWx1ZUIPCg1fcmVnaXN0cmF0aW9uIjoKHExpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QSGgoSaW5jbHVkZV90b21ic3RvbmVkGAEgASgIIm0KHUxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlEjEKB2VudHJpZXMYASADKAsyIC5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUVudHJ5EhkKEXJlZ2lzdHJ5X3JldmlzaW9uGAIgASgDKn4KEkNhdGFsb2d1ZUVudHJ5S2luZBIkCiBDQVRBTE9HVUVfRU5UUllfS0lORF9VTlNQRUNJRklFRBAAEh8KG0NBVEFMT0dVRV9FTlRSWV9LSU5EX01PRFVMRRABEiEKHUNBVEFMT0dVRV9FTlRSWV9LSU5EX1NPTFVUSU9OEAIqrwEKEkNhdGFsb2d1ZUdhcFJlYXNvbhIkCiBDQVRBTE9HVUVfR0FQX1JFQVNPTl9VTlNQRUNJRklFRBAAEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRUNPUkRFRBABEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9SRVBPUlRFRBACEiUKIUNBVEFMT0dVRV9HQVBfUkVBU09OX05PVF9PQlNFUlZFRBADKt4BChhDYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3QSKgomQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfVU5TUEVDSUZJRUQQABIxCi1DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0FVVEhPUklaRUQQARIuCipDQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX0RJRkZFUlMQAhIzCi9DQVRBTE9HVUVfT0JTRVJWRURfVkVSRElDVF9SVU5OSU5HX1VOQVVUSE9SSVpFRBADMu8fChRQbGF0Zm9ybUFkbWluU2VydmljZRKOAQoLU2VhcmNoVXNlcnMSJC5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVxdWVzdBolLnNhYXMuYWNjb3VudHMudjEuU2VhcmNoVXNlcnNSZXNwb25zZSIywvMYFAgCEAEwAToCEAFAAUgFUANYA2ADgtPkkwIUEhIvdjEvcGxhdGZvcm0vdXNlcnMSqwEKC1N1c3BlbmRVc2VyEiQuc2Fhcy5hY2NvdW50cy52MS5TdXNwZW5kVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiXsLzGCsIAhABMAE6FwoTc2Fhcy51c2VyLnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIpOgEqIiQvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnN1c3BlbmQSswEKDVVuc3VzcGVuZFVzZXISJi5zYWFzLmFjY291bnRzLnYxLlVuc3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5ImLC8xgtCAIQATABOhkKFXNhYXMudXNlci51bnN1c3BlbmRlZBACQAFIBVADWANgBXgCgtPkkwIrOgEqIiYvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OnVuc3VzcGVuZBLWAQoPSW1wZXJzb25hdGVVc2VyEiguc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXF1ZXN0Gikuc2Fhcy5hY2NvdW50cy52MS5JbXBlcnNvbmF0ZVVzZXJSZXNwb25zZSJuwvMYNwgCEAEwBDojCh9zYWFzLnBsYXRmb3JtLnVzZXJfaW1wZXJzb25hdGVkEAJAAUgFUANYBGADeAKC0+STAi06ASoiKC92MS9wbGF0Zm9ybS91c2Vycy97dXNlcl9pZH06aW1wZXJzb25hdGUS2AEKEVN0b3BJbXBlcnNvbmF0aW9uEiouc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2UiasLzGDwIAhABMAE6Kgomc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRpb25fZW5kZWQQAkABSAVQA1gDYAGC0+STAiQ6ASoiHy92MS9wbGF0Zm9ybS9pbXBlcnNvbmF0aW9uOnN0b3ASpgEKEkxpc3RBY3RpdmVTZXNzaW9ucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2UiNcLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFxIVL3YxL3BsYXRmb3JtL3Nlc3Npb25zEqkBCg1SZXZva2VTZXNzaW9uEiYuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VTZXNzaW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJYwvMYKggCEAEwAToYChRzYWFzLnNlc3Npb24ucmV2b2tlZBACQAFIBVADWANgA4LT5JMCJCoiL3YxL3BsYXRmb3JtL3Nlc3Npb25zL3tzZXNzaW9uX2lkfRLPAQoSR2V0T3JnRW50aXRsZW1lbnRzEisuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0Giwuc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdFbnRpdGxlbWVudHNSZXNwb25zZSJewvMYIggCEAMqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAjISMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxLkAQoTT3ZlcnJpZGVFbnRpdGxlbWVudBIsLnNhYXMuYWNjb3VudHMudjEuT3ZlcnJpZGVFbnRpdGxlbWVudFJlcXVlc3QaLS5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXNwb25zZSJwwvMYMQgCEAEwATodChlzYWFzLmVudGl0bGVtZW50Lm92ZXJyaWRlEAJAAUgFUANYA2ACeAKC0+STAjU6ASoiMC92MS9wbGF0Zm9ybS9vcmdhbml6YXRpb25zL3tvcmdfaWR9L2VudGl0bGVtZW50cxKtAQoRR3JhbnRQbGF0Zm9ybVJvbGUSKi5zYWFzLmFjY291bnRzLnYxLkdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJUwvMYMggCEAEwBDoeChpzYWFzLnBsYXRmb3JtLnJvbGVfZ3JhbnRlZBACQAFIBVADWANgBXgCgtPkkwIYOgEqIhMvdjEvcGxhdGZvcm0vYWRtaW5zErYBChJSZXZva2VQbGF0Zm9ybVJvbGUSKy5zYWFzLmFjY291bnRzLnYxLlJldm9rZVBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiW8LzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX3Jldm9rZWQQAkABSAVQA1gDYAV4AoLT5JMCHyodL3YxL3BsYXRmb3JtL2FkbWlucy97dXNlcl9pZH0SpAEKEkxpc3RQbGF0Zm9ybUFkbWlucxIrLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2UiM8LzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCFRITL3YxL3BsYXRmb3JtL2FkbWlucxKPAQoUTGlzdEFsbE9yZ2FuaXphdGlvbnMSLS5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBouLnNhYXMuYWNjb3VudHMudjEuTGlzdEFsbE9yZ2FuaXphdGlvbnNSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEpIBChVHZXRPcmdhbml6YXRpb25Sb3N0ZXISLi5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkdldE9yZ2FuaXphdGlvblJvc3RlclJlc3BvbnNlIhjC8xgUCAIQATABOgIQAUABSAVQA1gDYAMSpQEKEExpc3RGZWF0dXJlRmxhZ3MSKS5zYWFzLmFjY291bnRzLnYxLkxpc3RGZWF0dXJlRmxhZ3NSZXF1ZXN0Giouc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3MS0gEKEVVwc2VydEZlYXR1cmVGbGFnEiouc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1JlcXVlc3QaKy5zYWFzLmFjY291bnRzLnYxLlVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2UiZIgCAcLzGDEIAhABMAE6HQoZc2Fhcy5mZWF0dXJlX2ZsYWcudXBkYXRlZBACQAFIBVADWANgBXgCgtPkkwImOgEqGiEvdjEvcGxhdGZvcm0vZmVhdHVyZS1mbGFncy97bmFtZX0SnwEKEEdldEpvYk9wZXJhdGlvbnMSJS5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1JlcXVlc3QaJi5zYWFzLmpvYnMudjEuR2V0Sm9iT3BlcmF0aW9uc1Jlc3BvbnNlIjzC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAh4SHC92MS9wbGF0Zm9ybS9qb2JzL29wZXJhdGlvbnMSfAoITGlzdEpvYnMSHS5zYWFzLmpvYnMudjEuTGlzdEpvYnNSZXF1ZXN0Gh4uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVzcG9uc2UiMcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCExIRL3YxL3BsYXRmb3JtL2pvYnMSfwoGR2V0Sm9iEhsuc2Fhcy5qb2JzLnYxLkdldEpvYlJlcXVlc3QaHC5zYWFzLmpvYnMudjEuR2V0Sm9iUmVzcG9uc2UiOsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCHBIaL3YxL3BsYXRmb3JtL2pvYnMve2pvYl9pZH0SrAEKCVJlcGxheUpvYhIeLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXF1ZXN0Gh8uc2Fhcy5qb2JzLnYxLlJlcGxheUpvYlJlc3BvbnNlIl7C8xgnCAIQATADOhUKEXNhYXMuam9iLnJlcGxheWVkEAJAA0gFUANYA2AFgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vam9icy97c291cmNlX2pvYl9pZH06cmVwbGF5EqsBChJHZXRFdmVudE9wZXJhdGlvbnMSKS5zYWFzLmV2ZW50cy52MS5HZXRFdmVudE9wZXJhdGlvbnNSZXF1ZXN0Giouc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVzcG9uc2UiPsLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIBIeL3YxL3BsYXRmb3JtL2V2ZW50cy9vcGVyYXRpb25zEroBChZMaXN0RXZlbnRTdWJzY3JpcHRpb25zEi0uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1JlcXVlc3QaLi5zYWFzLmV2ZW50cy52MS5MaXN0RXZlbnRTdWJzY3JpcHRpb25zUmVzcG9uc2UiQcLzGBQIAhABMAE6AhABQAFIBVADWANgBYLT5JMCIxIhL3YxL3BsYXRmb3JtL2V2ZW50cy9zdWJzY3JpcHRpb25zErABChVMaXN0UGxhdGZvcm1DYXRhbG9ndWUSLi5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlcXVlc3QaLy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlIjbC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhgSFi92MS9wbGF0Zm9ybS9jYXRhbG9ndWVCugEKFGNvbS5zYWFzLmFjY291bnRzLnYxQhJQbGF0Zm9ybUFkbWluUHJvdG9QAVosYWNjb3VudHMvcGtnL2dlbi9zYWFzL2FjY291bnRzL3YxO2FjY291bnRzdjGiAgNTQViqAhBTYWFzLkFjY291bnRzLlYxygIQU2Fhc1xBY2NvdW50c1xWMeICHFNhYXNcQWNjb3VudHNcVjFcR1BCTWV0YWRhdGHqAhJTYWFzOjpBY2NvdW50czo6VjFiBnByb3RvMw", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); + fileDesc("CiVzYWFzL2FjY291bnRzL3YxL3BsYXRmb3JtX2FkbWluLnByb3RvEhBzYWFzLmFjY291bnRzLnYxIlUKElNlYXJjaFVzZXJzUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJImoKE1NlYXJjaFVzZXJzUmVzcG9uc2USJQoFdXNlcnMYASADKAsyFi5zYWFzLmFjY291bnRzLnYxLlVzZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgFIngKG0xpc3RBbGxPcmdhbml6YXRpb25zUmVxdWVzdBINCgVxdWVyeRgBIAEoCRIcCglwYWdlX3NpemUYAiABKAVCCbpIBhoEGGQgABISCgpwYWdlX3Rva2VuGAMgASgJEhgKEGluY2x1ZGVfYXJjaGl2ZWQYBCABKAgiYgoUUGxhdGZvcm1Pcmdhbml6YXRpb24SNAoMb3JnYW5pemF0aW9uGAEgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb24SFAoMbWVtYmVyX2NvdW50GAIgASgFInYKHExpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2USPQoNb3JnYW5pemF0aW9ucxgBIAMoCzImLnNhYXMuYWNjb3VudHMudjEuUGxhdGZvcm1Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIjgKHEdldE9yZ2FuaXphdGlvblJvc3RlclJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABASJRCh1HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZRIwCgdtZW1iZXJzGAEgAygLMh8uc2Fhcy5hY2NvdW50cy52MS5PcmdNZW1iZXJzaGlwIj8KElN1c3BlbmRVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIOCgZyZWFzb24YAiABKAkiMQoUVW5zdXNwZW5kVXNlclJlcXVlc3QSGQoHdXNlcl9pZBgBIAEoCUIIukgFcgOwAQEiTwoWSW1wZXJzb25hdGVVc2VyUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARIaCgZyZWFzb24YAiABKAlCCrpIB3IFEAoY9AMiQwoXSW1wZXJzb25hdGVVc2VyUmVzcG9uc2USFAoMYWNjZXNzX3Rva2VuGAEgASgJEhIKCmV4cGlyZXNfaW4YAiABKAMiGgoYU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0ImsKGVN0b3BJbXBlcnNvbmF0aW9uUmVzcG9uc2USGAoQZHVyYXRpb25fc2Vjb25kcxgBIAEoAxIcChRhY2Nlc3NfdG9rZW5fcmV2b2tlZBgCIAEoCBIWCg5hbHJlYWR5X2Nsb3NlZBgDIAEoCCJeChlMaXN0QWN0aXZlU2Vzc2lvbnNSZXF1ZXN0Eg8KB3VzZXJfaWQYASABKAkSHAoJcGFnZV9zaXplGAIgASgFQgm6SAYaBBhkIAASEgoKcGFnZV90b2tlbhgDIAEoCSKZAwoLU2Vzc2lvbkluZm8SCgoCaWQYASABKAkSDwoHdXNlcl9pZBgCIAEoCRISCgppcF9hZGRyZXNzGAMgASgJEkIKC2RldmljZV9pbmZvGAQgAygLMi0uc2Fhcy5hY2NvdW50cy52MS5TZXNzaW9uSW5mby5EZXZpY2VJbmZvRW50cnkSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMgoObGFzdF9hY3RpdmVfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCmV4cGlyZXNfYXQYByABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKD2lkbGVfZXhwaXJlc19hdBgIIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASGQoRYWN0aW5nX2FzX3VzZXJfaWQYCSABKAkaMQoPRGV2aWNlSW5mb0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiZgoaTGlzdEFjdGl2ZVNlc3Npb25zUmVzcG9uc2USLwoIc2Vzc2lvbnMYASADKAsyHS5zYWFzLmFjY291bnRzLnYxLlNlc3Npb25JbmZvEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCSJDChRSZXZva2VTZXNzaW9uUmVxdWVzdBIbCgpzZXNzaW9uX2lkGAEgASgJQge6SARyAhABEg4KBnJlYXNvbhgCIAEoCSI1ChlHZXRPcmdFbnRpdGxlbWVudHNSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQEiaAoaR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2USEQoJcGxhbl9uYW1lGAEgASgJEjcKDGVudGl0bGVtZW50cxgCIAMoCzIhLnNhYXMuYWNjb3VudHMudjEuRW50aXRsZW1lbnRJbmZvIlUKD0VudGl0bGVtZW50SW5mbxIPCgdmZWF0dXJlGAEgASgJEg0KBWxpbWl0GAIgASgDEgwKBHVzZWQYAyABKAMSFAoMaGFzX292ZXJyaWRlGAQgASgIInUKGk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESGAoHZmVhdHVyZRgCIAEoCUIHukgEcgIQARITCgtsaW1pdF92YWx1ZRgDIAEoAxIOCgZyZWFzb24YBCABKAkiKQobT3ZlcnJpZGVFbnRpdGxlbWVudFJlc3BvbnNlEgoKAmlkGAEgASgJInIKGEdyYW50UGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABARI7Cg1wbGF0Zm9ybV9yb2xlGAIgASgJQiS6SCFyH1ILc3VwZXJfYWRtaW5SB3N1cHBvcnRSB2JpbGxpbmciNgoZUmV2b2tlUGxhdGZvcm1Sb2xlUmVxdWVzdBIZCgd1c2VyX2lkGAEgASgJQgi6SAVyA7ABASIbChlMaXN0UGxhdGZvcm1BZG1pbnNSZXF1ZXN0IoABChJQbGF0Zm9ybUFkbWluRW50cnkSDwoHdXNlcl9pZBgBIAEoCRIVCg1wbGF0Zm9ybV9yb2xlGAIgASgJEhIKCmdyYW50ZWRfYnkYAyABKAkSLgoKZ3JhbnRlZF9hdBgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiUgoaTGlzdFBsYXRmb3JtQWRtaW5zUmVzcG9uc2USNAoGYWRtaW5zGAEgAygLMiQuc2Fhcy5hY2NvdW50cy52MS5QbGF0Zm9ybUFkbWluRW50cnkiGQoXTGlzdEZlYXR1cmVGbGFnc1JlcXVlc3QidwoQRmVhdHVyZUZsYWdFbnRyeRIMCgRuYW1lGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAIgASgJEg8KB2VuYWJsZWQYAyABKAgSFwoPcm9sbG91dF9wZXJjZW50GAQgASgFEhYKDnRhcmdldF9vcmdfaWRzGAUgAygJIk0KGExpc3RGZWF0dXJlRmxhZ3NSZXNwb25zZRIxCgVmbGFncxgBIAMoCzIiLnNhYXMuYWNjb3VudHMudjEuRmVhdHVyZUZsYWdFbnRyeSKMAQoYVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0EhUKBG5hbWUYASABKAlCB7pIBHICEAESEwoLZGVzY3JpcHRpb24YAiABKAkSDwoHZW5hYmxlZBgDIAEoCBIXCg9yb2xsb3V0X3BlcmNlbnQYBCABKAUSFgoOdGFyZ2V0X29yZ19pZHMYBSADKAk6AhgBIi0KGVVwc2VydEZlYXR1cmVGbGFnUmVzcG9uc2USDAoEbmFtZRgBIAEoCToCGAEiVAoMQ2F0YWxvZ3VlR2FwEjQKBnJlYXNvbhgBIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwUmVhc29uEg4KBmRldGFpbBgCIAEoCSJEChBDYXRhbG9ndWVSZWxlYXNlEhEKCXB1Ymxpc2hlchgBIAEoCRIMCgRuYW1lGAIgASgJEg8KB3ZlcnNpb24YAyABKAkipQIKEENhdGFsb2d1ZURlc2lyZWQSGwoRZGVjbGFyZWRfcmV2aXNpb24YASABKANIABI/ChVkZWNsYXJlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEj4KEGRlY2xhcmVkX3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIARI+ChRkZWNsYXJlZF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGQoXZGVjbGFyZWRfcmV2aXNpb25fdmFsdWVCGAoWZGVjbGFyZWRfcmVsZWFzZV92YWx1ZSKCAQoWQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbhIbChNhdXRob3JpemVkX3JldmlzaW9uGAEgASgDEhgKEGludmVudG9yeV9kaWdlc3QYAiABKAkSFgoObWVtYmVyX2JpbmRpbmcYAyABKAkSGQoRYnVpbGRfaW5jYXJuYXRpb24YBCABKAQiKAoWQ2F0YWxvZ3VlTm90QXV0aG9yaXplZBIOCgZkZXRhaWwYASABKAki8AEKE0NhdGFsb2d1ZUF1dGhvcml6ZWQSQQoNYXV0aG9yaXphdGlvbhgBIAEoCzIoLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXV0aG9yaXphdGlvbkgAEkIKDm5vdF9hdXRob3JpemVkGAIgASgLMiguc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVOb3RBdXRob3JpemVkSAASOwoRYXV0aG9yaXphdGlvbl9nYXAYAyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAQhUKE2F1dGhvcml6YXRpb25fdmFsdWUiiAEKEENhdGFsb2d1ZUFwcGxpZWQSGgoQYXBwbGllZF9yZXZpc2lvbhgBIAEoA0gAEj4KFGFwcGxpZWRfcmV2aXNpb25fZ2FwGAIgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAEIYChZhcHBsaWVkX3JldmlzaW9uX3ZhbHVlIt8BChpDYXRhbG9ndWVPYnNlcnZlZEV4ZWN1dGlvbhJoChdjb250YWluZXJfaW1hZ2VfZGlnZXN0cxgBIAMoCzJHLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb24uQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSGQoRYnVpbGRfaW5jYXJuYXRpb24YAiABKAQaPAoaQ29udGFpbmVySW1hZ2VEaWdlc3RzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASLUBAoRQ2F0YWxvZ3VlT2JzZXJ2ZWQSGwoRb2JzZXJ2ZWRfcmV2aXNpb24YASABKANIABI/ChVvYnNlcnZlZF9yZXZpc2lvbl9nYXAYAiABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgAEjEKC29ic2VydmVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBEkMKGW9ic2VydmF0aW9uX2ZyZXNobmVzc19nYXAYBCABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkoKEm9ic2VydmVkX2V4ZWN1dGlvbhgFIAEoCzIsLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWRFeGVjdXRpb25IAhJAChZvYnNlcnZlZF9leGVjdXRpb25fZ2FwGAYgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIAhI9Cgd2ZXJkaWN0GAcgASgOMiouc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVPYnNlcnZlZFZlcmRpY3RIAxI1Cgt2ZXJkaWN0X2dhcBgIIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSANCGQoXb2JzZXJ2ZWRfcmV2aXNpb25fdmFsdWVCHQobb2JzZXJ2YXRpb25fZnJlc2huZXNzX3ZhbHVlQhoKGG9ic2VydmVkX2V4ZWN1dGlvbl92YWx1ZUIPCg12ZXJkaWN0X3ZhbHVlIuABChRDYXRhbG9ndWVXaXRoZHJhd2luZxI+ChR3aXRoZHJhd2FsX3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASSQofY3JlZGVudGlhbF9yZXZvY2F0aW9uX3N0YXRlX2dhcBgCIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAFCGAoWd2l0aGRyYXdhbF9zdGF0ZV92YWx1ZUIjCiFjcmVkZW50aWFsX3Jldm9jYXRpb25fc3RhdGVfdmFsdWUibAoQQ2F0YWxvZ3VlUmV0aXJlZBI+ChRyZXRpcmVtZW50X3N0YXRlX2dhcBgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSABCGAoWcmV0aXJlbWVudF9zdGF0ZV92YWx1ZSK4AwoVQ2F0YWxvZ3VlSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEhAKCG9yZ19uYW1lGAIgASgJEjsKDWFnZW50X3JlbGVhc2UYAyABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJlbGVhc2VIABI7ChFhZ2VudF9yZWxlYXNlX2dhcBgEIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlR2FwSAASNgoMcmV2aXNpb25fZ2FwGAUgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIARI8Cg1ncmFudGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50Ej4KD2luaGVyaXRlZF90ZWFtcxgHIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ29sbGVjdGlvblJlYWRHcmFudEIVChNhZ2VudF9yZWxlYXNlX3ZhbHVlQhAKDnJldmlzaW9uX3ZhbHVlIuMFCg5DYXRhbG9ndWVFbnRyeRIyCgRraW5kGAEgASgOMiQuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVFbnRyeUtpbmQSDAoEbmFtZRgCIAEoCRITCglwdWJsaXNoZXIYAyABKAlIABI3Cg1wdWJsaXNoZXJfZ2FwGAQgASgLMh4uc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVHYXBIABIzCgdkZXNpcmVkGAUgASgLMiIuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVEZXNpcmVkEjkKCmF1dGhvcml6ZWQYBiABKAsyJS5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUF1dGhvcml6ZWQSMwoHYXBwbGllZBgHIAEoCzIiLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlQXBwbGllZBI1CghvYnNlcnZlZBgIIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlT2JzZXJ2ZWQSOwoLd2l0aGRyYXdpbmcYCSABKAsyJi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVdpdGhkcmF3aW5nEjMKB3JldGlyZWQYCiABKAsyIi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZVJldGlyZWQSOAoOYnVpbGRfc2l6ZV9nYXAYCyABKAsyHi5zYWFzLmFjY291bnRzLnYxLkNhdGFsb2d1ZUdhcEgBEkEKDHJlZ2lzdHJhdGlvbhgMIAEoCzImLnNhYXMuYWNjb3VudHMudjEuU29sdXRpb25SZWdpc3RyYXRpb25IAogBARI+Cg1pbnN0YWxsYXRpb25zGA0gAygLMicuc2Fhcy5hY2NvdW50cy52MS5DYXRhbG9ndWVJbnN0YWxsYXRpb25CEQoPcHVibGlzaGVyX3ZhbHVlQhIKEGJ1aWxkX3NpemVfdmFsdWVCDwoNX3JlZ2lzdHJhdGlvbiI6ChxMaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0EhoKEmluY2x1ZGVfdG9tYnN0b25lZBgBIAEoCCKRAgodTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlUmVzcG9uc2USMQoHZW50cmllcxgBIAMoCzIgLnNhYXMuYWNjb3VudHMudjEuQ2F0YWxvZ3VlRW50cnkSGQoRcmVnaXN0cnlfcmV2aXNpb24YAiABKAMSZgoUYXBwcm92ZWRfaW52ZW50b3JpZXMYAyADKAsySC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUNhdGFsb2d1ZVJlc3BvbnNlLkFwcHJvdmVkSW52ZW50b3JpZXNFbnRyeRo6ChhBcHByb3ZlZEludmVudG9yaWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ASp+ChJDYXRhbG9ndWVFbnRyeUtpbmQSJAogQ0FUQUxPR1VFX0VOVFJZX0tJTkRfVU5TUEVDSUZJRUQQABIfChtDQVRBTE9HVUVfRU5UUllfS0lORF9NT0RVTEUQARIhCh1DQVRBTE9HVUVfRU5UUllfS0lORF9TT0xVVElPThACKq8BChJDYXRhbG9ndWVHYXBSZWFzb24SJAogQ0FUQUxPR1VFX0dBUF9SRUFTT05fVU5TUEVDSUZJRUQQABIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVDT1JERUQQARIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfUkVQT1JURUQQAhIlCiFDQVRBTE9HVUVfR0FQX1JFQVNPTl9OT1RfT0JTRVJWRUQQAyreAQoYQ2F0YWxvZ3VlT2JzZXJ2ZWRWZXJkaWN0EioKJkNBVEFMT0dVRV9PQlNFUlZFRF9WRVJESUNUX1VOU1BFQ0lGSUVEEAASMQotQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19BVVRIT1JJWkVEEAESLgoqQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19ESUZGRVJTEAISMwovQ0FUQUxPR1VFX09CU0VSVkVEX1ZFUkRJQ1RfUlVOTklOR19VTkFVVEhPUklaRUQQAzLvHwoUUGxhdGZvcm1BZG1pblNlcnZpY2USjgEKC1NlYXJjaFVzZXJzEiQuc2Fhcy5hY2NvdW50cy52MS5TZWFyY2hVc2Vyc1JlcXVlc3QaJS5zYWFzLmFjY291bnRzLnYxLlNlYXJjaFVzZXJzUmVzcG9uc2UiMsLzGBQIAhABMAE6AhABQAFIBVADWANgA4LT5JMCFBISL3YxL3BsYXRmb3JtL3VzZXJzEqsBCgtTdXNwZW5kVXNlchIkLnNhYXMuYWNjb3VudHMudjEuU3VzcGVuZFVzZXJSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Il7C8xgrCAIQATABOhcKE3NhYXMudXNlci5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKToBKiIkL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTpzdXNwZW5kErMBCg1VbnN1c3BlbmRVc2VyEiYuc2Fhcy5hY2NvdW50cy52MS5VbnN1c3BlbmRVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSJiwvMYLQgCEAEwAToZChVzYWFzLnVzZXIudW5zdXNwZW5kZWQQAkABSAVQA1gDYAV4AoLT5JMCKzoBKiImL3YxL3BsYXRmb3JtL3VzZXJzL3t1c2VyX2lkfTp1bnN1c3BlbmQS1gEKD0ltcGVyc29uYXRlVXNlchIoLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuSW1wZXJzb25hdGVVc2VyUmVzcG9uc2UibsLzGDcIAhABMAQ6Iwofc2Fhcy5wbGF0Zm9ybS51c2VyX2ltcGVyc29uYXRlZBACQAFIBVADWARgA3gCgtPkkwItOgEqIigvdjEvcGxhdGZvcm0vdXNlcnMve3VzZXJfaWR9OmltcGVyc29uYXRlEtgBChFTdG9wSW1wZXJzb25hdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuU3RvcEltcGVyc29uYXRpb25SZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5TdG9wSW1wZXJzb25hdGlvblJlc3BvbnNlImrC8xg8CAIQATABOioKJnNhYXMucGxhdGZvcm0udXNlcl9pbXBlcnNvbmF0aW9uX2VuZGVkEAJAAUgFUANYA2ABgtPkkwIkOgEqIh8vdjEvcGxhdGZvcm0vaW1wZXJzb25hdGlvbjpzdG9wEqYBChJMaXN0QWN0aXZlU2Vzc2lvbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RBY3RpdmVTZXNzaW9uc1Jlc3BvbnNlIjXC8xgUCAIQATABOgIQAUABSAVQA1gDYAOC0+STAhcSFS92MS9wbGF0Zm9ybS9zZXNzaW9ucxKpAQoNUmV2b2tlU2Vzc2lvbhImLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlU2Vzc2lvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiWMLzGCoIAhABMAE6GAoUc2Fhcy5zZXNzaW9uLnJldm9rZWQQAkABSAVQA1gDYAOC0+STAiQqIi92MS9wbGF0Zm9ybS9zZXNzaW9ucy97c2Vzc2lvbl9pZH0SzwEKEkdldE9yZ0VudGl0bGVtZW50cxIrLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVxdWVzdBosLnNhYXMuYWNjb3VudHMudjEuR2V0T3JnRW50aXRsZW1lbnRzUmVzcG9uc2UiXsLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIyEjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMS5AEKE092ZXJyaWRlRW50aXRsZW1lbnQSLC5zYWFzLmFjY291bnRzLnYxLk92ZXJyaWRlRW50aXRsZW1lbnRSZXF1ZXN0Gi0uc2Fhcy5hY2NvdW50cy52MS5PdmVycmlkZUVudGl0bGVtZW50UmVzcG9uc2UicMLzGDEIAhABMAE6HQoZc2Fhcy5lbnRpdGxlbWVudC5vdmVycmlkZRACQAFIBVADWANgAngCgtPkkwI1OgEqIjAvdjEvcGxhdGZvcm0vb3JnYW5pemF0aW9ucy97b3JnX2lkfS9lbnRpdGxlbWVudHMSrQEKEUdyYW50UGxhdGZvcm1Sb2xlEiouc2Fhcy5hY2NvdW50cy52MS5HcmFudFBsYXRmb3JtUm9sZVJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiVMLzGDIIAhABMAQ6Hgoac2Fhcy5wbGF0Zm9ybS5yb2xlX2dyYW50ZWQQAkABSAVQA1gDYAV4AoLT5JMCGDoBKiITL3YxL3BsYXRmb3JtL2FkbWlucxK2AQoSUmV2b2tlUGxhdGZvcm1Sb2xlEisuc2Fhcy5hY2NvdW50cy52MS5SZXZva2VQbGF0Zm9ybVJvbGVSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IlvC8xgyCAIQATAEOh4KGnNhYXMucGxhdGZvcm0ucm9sZV9yZXZva2VkEAJAAUgFUANYA2AFeAKC0+STAh8qHS92MS9wbGF0Zm9ybS9hZG1pbnMve3VzZXJfaWR9EqQBChJMaXN0UGxhdGZvcm1BZG1pbnMSKy5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1JlcXVlc3QaLC5zYWFzLmFjY291bnRzLnYxLkxpc3RQbGF0Zm9ybUFkbWluc1Jlc3BvbnNlIjPC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhUSEy92MS9wbGF0Zm9ybS9hZG1pbnMSjwEKFExpc3RBbGxPcmdhbml6YXRpb25zEi0uc2Fhcy5hY2NvdW50cy52MS5MaXN0QWxsT3JnYW5pemF0aW9uc1JlcXVlc3QaLi5zYWFzLmFjY291bnRzLnYxLkxpc3RBbGxPcmdhbml6YXRpb25zUmVzcG9uc2UiGMLzGBQIAhABMAE6AhABQAFIBVADWANgAxKSAQoVR2V0T3JnYW5pemF0aW9uUm9zdGVyEi4uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5HZXRPcmdhbml6YXRpb25Sb3N0ZXJSZXNwb25zZSIYwvMYFAgCEAEwAToCEAFAAUgFUANYA2ADEqUBChBMaXN0RmVhdHVyZUZsYWdzEikuc2Fhcy5hY2NvdW50cy52MS5MaXN0RmVhdHVyZUZsYWdzUmVxdWVzdBoqLnNhYXMuYWNjb3VudHMudjEuTGlzdEZlYXR1cmVGbGFnc1Jlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9mZWF0dXJlLWZsYWdzEtIBChFVcHNlcnRGZWF0dXJlRmxhZxIqLnNhYXMuYWNjb3VudHMudjEuVXBzZXJ0RmVhdHVyZUZsYWdSZXF1ZXN0Gisuc2Fhcy5hY2NvdW50cy52MS5VcHNlcnRGZWF0dXJlRmxhZ1Jlc3BvbnNlImSIAgHC8xgxCAIQATABOh0KGXNhYXMuZmVhdHVyZV9mbGFnLnVwZGF0ZWQQAkABSAVQA1gDYAV4AoLT5JMCJjoBKhohL3YxL3BsYXRmb3JtL2ZlYXR1cmUtZmxhZ3Mve25hbWV9Ep8BChBHZXRKb2JPcGVyYXRpb25zEiUuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXF1ZXN0GiYuc2Fhcy5qb2JzLnYxLkdldEpvYk9wZXJhdGlvbnNSZXNwb25zZSI8wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIeEhwvdjEvcGxhdGZvcm0vam9icy9vcGVyYXRpb25zEnwKCExpc3RKb2JzEh0uc2Fhcy5qb2JzLnYxLkxpc3RKb2JzUmVxdWVzdBoeLnNhYXMuam9icy52MS5MaXN0Sm9ic1Jlc3BvbnNlIjHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhMSES92MS9wbGF0Zm9ybS9qb2JzEn8KBkdldEpvYhIbLnNhYXMuam9icy52MS5HZXRKb2JSZXF1ZXN0Ghwuc2Fhcy5qb2JzLnYxLkdldEpvYlJlc3BvbnNlIjrC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAhwSGi92MS9wbGF0Zm9ybS9qb2JzL3tqb2JfaWR9EqwBCglSZXBsYXlKb2ISHi5zYWFzLmpvYnMudjEuUmVwbGF5Sm9iUmVxdWVzdBofLnNhYXMuam9icy52MS5SZXBsYXlKb2JSZXNwb25zZSJewvMYJwgCEAEwAzoVChFzYWFzLmpvYi5yZXBsYXllZBACQANIBVADWANgBYLT5JMCLToBKiIoL3YxL3BsYXRmb3JtL2pvYnMve3NvdXJjZV9qb2JfaWR9OnJlcGxheRKrAQoSR2V0RXZlbnRPcGVyYXRpb25zEikuc2Fhcy5ldmVudHMudjEuR2V0RXZlbnRPcGVyYXRpb25zUmVxdWVzdBoqLnNhYXMuZXZlbnRzLnYxLkdldEV2ZW50T3BlcmF0aW9uc1Jlc3BvbnNlIj7C8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiASHi92MS9wbGF0Zm9ybS9ldmVudHMvb3BlcmF0aW9ucxK6AQoWTGlzdEV2ZW50U3Vic2NyaXB0aW9ucxItLnNhYXMuZXZlbnRzLnYxLkxpc3RFdmVudFN1YnNjcmlwdGlvbnNSZXF1ZXN0Gi4uc2Fhcy5ldmVudHMudjEuTGlzdEV2ZW50U3Vic2NyaXB0aW9uc1Jlc3BvbnNlIkHC8xgUCAIQATABOgIQAUABSAVQA1gDYAWC0+STAiMSIS92MS9wbGF0Zm9ybS9ldmVudHMvc3Vic2NyaXB0aW9ucxKwAQoVTGlzdFBsYXRmb3JtQ2F0YWxvZ3VlEi4uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXF1ZXN0Gi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0UGxhdGZvcm1DYXRhbG9ndWVSZXNwb25zZSI2wvMYFAgCEAEwAToCEAFAAUgFUANYA2AFgtPkkwIYEhYvdjEvcGxhdGZvcm0vY2F0YWxvZ3VlQroBChRjb20uc2Fhcy5hY2NvdW50cy52MUISUGxhdGZvcm1BZG1pblByb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_accounts_v1_common, file_saas_accounts_v1_installations, file_saas_accounts_v1_solution_registry, file_saas_events_v1_operations, file_saas_jobs_v1_jobs, file_saas_policy_v1_options]); /** * @generated from message saas.accounts.v1.SearchUsersRequest @@ -886,31 +886,6 @@ export type CatalogueRelease = Message<"saas.accounts.v1.CatalogueRelease"> & { export const CatalogueReleaseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 33); -/** - * CatalogueExecution is what runs: an OCI image manifest digest and the build - * incarnation the host assigned to it, the same pair a Work Context seals. - * - * @generated from message saas.accounts.v1.CatalogueExecution - */ -export type CatalogueExecution = Message<"saas.accounts.v1.CatalogueExecution"> & { - /** - * @generated from field: string image_digest = 1; - */ - imageDigest: string; - - /** - * @generated from field: uint64 build_incarnation = 2; - */ - buildIncarnation: bigint; -}; - -/** - * Describes the message saas.accounts.v1.CatalogueExecution. - * Use `create(CatalogueExecutionSchema)` to create a new message. - */ -export const CatalogueExecutionSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 34); - /** * CatalogueDesired is what the composition declares. * @@ -957,11 +932,16 @@ export type CatalogueDesired = Message<"saas.accounts.v1.CatalogueDesired"> & { * Use `create(CatalogueDesiredSchema)` to create a new message. */ export const CatalogueDesiredSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 35); + messageDesc(file_saas_accounts_v1_platform_admin, 34); /** - * CatalogueAuthorization is a current platform approval: the authorized - * revision and the execution it approved. + * CatalogueAuthorization is a current platform approval. What it approves is an + * execution inventory, `codefly/execution-inventory/v1`, whose one definition is + * the deployment contract module (github.com/codefly-dev/cli/contracts/deployment). + * The host does not re-describe that document: it names the approved inventory + * by the digest approval signs, carries the inventory's canonical bytes once per + * response (ListPlatformCatalogueResponse.approved_inventories), and reads them + * only through the contract. * * @generated from message saas.accounts.v1.CatalogueAuthorization */ @@ -972,9 +952,32 @@ export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthoriz authorizedRevision: bigint; /** - * @generated from field: saas.accounts.v1.CatalogueExecution approved_execution = 2; + * The approved inventory's digest, as the contract computes it over its + * canonical bytes. + * + * @generated from field: string inventory_digest = 2; + */ + inventoryDigest: string; + + /** + * Which member of that inventory's delivery aggregate this entry is. + * + * @generated from field: string member_binding = 3; + */ + memberBinding: string; + + /** + * The build incarnation this host assigned to the approved execution. Not an + * inventory field: this host, and only this host, assigns it — advancing it + * whenever the approved execution changes — and a Work Context seals it beside + * the image digest. The verdict compares it with an observation's + * incarnation, which is the same host-assigned value as presented by the + * running workload; an incarnation from any other assigner would make that + * comparison meaningless, not merely wrong. + * + * @generated from field: uint64 build_incarnation = 4; */ - approvedExecution?: CatalogueExecution; + buildIncarnation: bigint; }; /** @@ -982,7 +985,7 @@ export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthoriz * Use `create(CatalogueAuthorizationSchema)` to create a new message. */ export const CatalogueAuthorizationSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 36); + messageDesc(file_saas_accounts_v1_platform_admin, 35); /** * CatalogueNotAuthorized is a known absence of current authorization — @@ -1003,7 +1006,7 @@ export type CatalogueNotAuthorized = Message<"saas.accounts.v1.CatalogueNotAutho * Use `create(CatalogueNotAuthorizedSchema)` to create a new message. */ export const CatalogueNotAuthorizedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 37); + messageDesc(file_saas_accounts_v1_platform_admin, 36); /** * CatalogueAuthorized is what the platform has approved to execute. @@ -1040,7 +1043,7 @@ export type CatalogueAuthorized = Message<"saas.accounts.v1.CatalogueAuthorized" * Use `create(CatalogueAuthorizedSchema)` to create a new message. */ export const CatalogueAuthorizedSchema: GenMessage = /*@__PURE__*/ - messageDesc(file_saas_accounts_v1_platform_admin, 38); + messageDesc(file_saas_accounts_v1_platform_admin, 37); /** * CatalogueApplied is what delivery actually wrote. @@ -1071,6 +1074,36 @@ export type CatalogueApplied = Message<"saas.accounts.v1.CatalogueApplied"> & { * Use `create(CatalogueAppliedSchema)` to create a new message. */ export const CatalogueAppliedSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_saas_accounts_v1_platform_admin, 38); + +/** + * CatalogueObservedExecution is what an observation found running for an entry: + * each container's image manifest digest, keyed "/" in the approved inventory's own names, and the build incarnation the + * running workload presents — the incarnation this host assigned, sealed into + * its Work Context, never one the workload or another party chose. + * Observation is the observing consumer's record, not the inventory's, so its + * shape is the host's. + * + * @generated from message saas.accounts.v1.CatalogueObservedExecution + */ +export type CatalogueObservedExecution = Message<"saas.accounts.v1.CatalogueObservedExecution"> & { + /** + * @generated from field: map container_image_digests = 1; + */ + containerImageDigests: { [key: string]: string }; + + /** + * @generated from field: uint64 build_incarnation = 2; + */ + buildIncarnation: bigint; +}; + +/** + * Describes the message saas.accounts.v1.CatalogueObservedExecution. + * Use `create(CatalogueObservedExecutionSchema)` to create a new message. + */ +export const CatalogueObservedExecutionSchema: GenMessage = /*@__PURE__*/ messageDesc(file_saas_accounts_v1_platform_admin, 39); /** @@ -1120,9 +1153,9 @@ export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & */ observedExecutionValue: { /** - * @generated from field: saas.accounts.v1.CatalogueExecution observed_execution = 5; + * @generated from field: saas.accounts.v1.CatalogueObservedExecution observed_execution = 5; */ - value: CatalogueExecution; + value: CatalogueObservedExecution; case: "observedExecution"; } | { /** @@ -1133,8 +1166,9 @@ export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & } | { case: undefined; value?: undefined }; /** - * A verdict exists only when both an observation and a known authorization - * do; anything less is a gap, and an unobserved entry is never a match. + * A verdict exists only when both a complete observation and a readable + * authorization do; anything less is a gap, and an unobserved or + * incompletely observed entry is never a match. * * @generated from oneof saas.accounts.v1.CatalogueObserved.verdict_value */ @@ -1446,6 +1480,16 @@ export type ListPlatformCatalogueResponse = Message<"saas.accounts.v1.ListPlatfo * @generated from field: int64 registry_revision = 2; */ registryRevision: bigint; + + /** + * Every approved execution inventory an entry's authorization names, keyed by + * its digest, in the deployment contract's canonical bytes (codefly-json-v1). + * Carried once per response because one delivery aggregate is approved as a + * whole and several entries can be members of it. + * + * @generated from field: map approved_inventories = 3; + */ + approvedInventories: { [key: string]: Uint8Array }; }; /** @@ -1541,7 +1585,8 @@ export enum CatalogueObservedVerdict { UNSPECIFIED = 0, /** - * The observed image digest and incarnation are the approved execution. + * Every container of the approved inventory member runs its approved image + * digest, no other container runs, and the incarnation is the approved one. * * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; */ From 746a9066c81d32195a31f83f6843fb98ec5badf5 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:03:29 -0400 Subject: [PATCH 06/11] fix(catalogue): an affirmative verdict is constructible only from fresh, complete evidence The review at 6927ad9e found the one defect this page could least afford: the renderer painted "Running authorized" green beside "Not observed". The business function was right about every case it considered -- two empty digests DIFFER, an authorization gap is NOT_RECORDED, not_authorized with an observation is RUNNING_UNAUTHORIZED -- and then never consulted the observation's freshness, and compared incarnations with == on two zero-defaulted ints, so "both omitted" matched. The renderer then trusted the verdict without checking the evidence it rested on. The reviewer executed the renderer in memory to confirm both. An affirmative verdict is a claim about NOW. So, at the read boundary: - an observation with no observed_at cannot be current: NOT_OBSERVED, never a match. An execution nobody can date is not evidence that anything runs; - a zero incarnation on either side is an ABSENT incarnation, not a matching one -- completeExecution() requires a digest and a positive incarnation before a match can be declared. Two defaults comparing equal is exactly how a page says "authorized" about nothing; - RUNNING_UNAUTHORIZED needs no freshness: something ran without approval, and that fact does not expire. Both of the reviewer's constructions are now pinned as test cases, plus the stale-but-unauthorized case so the condemning verdict is not weakened by the freshness rule. The renderer is a SECOND line, never the only one: if the host ever emits RUNNING_AUTHORIZED beside an absent observation, an undated one, or no authorization, the row says "Inconsistent" and treats itself as unknown rather than painting green. An operator reads the green and stops. The existing frontend test asserting "Running authorized" renders green was feeding a fixture with no freshness and no authorization -- the contradiction itself. Its name was right ("only when the server judged it so") and its fixture never carried the judgement's evidence. The fixture now does, and a new test pins all three contradictions. Go: build, vet, gofmt clean; pkg/business and pkg/adapters suites ok. Frontend: facts.test.ts 12/12. The container test and tsc fail in this checkout on workspace-package resolution (@codefly-dev/ui/dashboard) unrelated to this change; the PR's CI resolves them and passed at 6927ad9e. Co-Authored-By: Claude Fable 5.1 --- .../code/pkg/business/platform_catalogue.go | 58 +++++++++++++------ .../pkg/business/platform_catalogue_test.go | 43 +++++++++++++- .../platform-catalogue/model/facts.test.ts | 45 +++++++++++++- .../platform-catalogue/model/facts.ts | 17 ++++++ 4 files changed, 141 insertions(+), 22 deletions(-) diff --git a/module/services/accounts/code/pkg/business/platform_catalogue.go b/module/services/accounts/code/pkg/business/platform_catalogue.go index 3407ccb28..640237a75 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue.go @@ -49,19 +49,20 @@ type PlatformCatalogue struct { } const ( - catalogueNoDeclaration = "This host holds no applied presence document for this entry, which is what records what the composition declares." - catalogueNoApplication = "This host has applied no presence generation for this entry." - catalogueNoApproval = "This host holds no approval record: signed platform approval of the execution inventory, bound to target and ownership scope, has not been built yet." - catalogueNotObserved = "This host reads no observed cluster state, so what runs is unknown — never assumed to match." - catalogueNoVerdict = "There is no approval record to judge the observed execution against." - catalogueNoWithdrawal = "This host holds no withdrawal record; withdrawal is a change to the approval record, which does not exist yet." - catalogueNoRevocation = "This host holds no credential-revocation record for this entry." - catalogueNoRetirement = "No retirement controller reports to this host; without stop or fence evidence nothing is reported as retired." - catalogueNoBuildSize = "Build size is the presence document's build_size section (codefly-dev/core#708), computed at build by the CLI; this host holds no presence document for this entry." - catalogueModulePublisher = "A composed module is known by its principal prefix alone; the composition records no publisher." - catalogueUnregistered = "No registration exists under this identifier; it is named only by its installations." - catalogueNoRevision = "Installations carry no revision of their own on this host." - catalogueNoAgentRelease = "The installation's agent principal carries no publisher/name:version identifier." + catalogueNoDeclaration = "This host holds no applied presence document for this entry, which is what records what the composition declares." + catalogueNoApplication = "This host has applied no presence generation for this entry." + catalogueNoApproval = "This host holds no approval record: signed platform approval of the execution inventory, bound to target and ownership scope, has not been built yet." + catalogueStaleObservation = "an execution was reported without a time it was observed at, so it cannot be judged current" + catalogueNotObserved = "This host reads no observed cluster state, so what runs is unknown — never assumed to match." + catalogueNoVerdict = "There is no approval record to judge the observed execution against." + catalogueNoWithdrawal = "This host holds no withdrawal record; withdrawal is a change to the approval record, which does not exist yet." + catalogueNoRevocation = "This host holds no credential-revocation record for this entry." + catalogueNoRetirement = "No retirement controller reports to this host; without stop or fence evidence nothing is reported as retired." + catalogueNoBuildSize = "Build size is the presence document's build_size section (codefly-dev/core#708), computed at build by the CLI; this host holds no presence document for this entry." + catalogueModulePublisher = "A composed module is known by its principal prefix alone; the composition records no publisher." + catalogueUnregistered = "No registration exists under this identifier; it is named only by its installations." + catalogueNoRevision = "Installations carry no revision of their own on this host." + catalogueNoAgentRelease = "The installation's agent principal carries no publisher/name:version identifier." ) // ListPlatformCatalogue reads the Catalogue for a super administrator. @@ -184,13 +185,20 @@ func newCatalogueEntry(kind gen.CatalogueEntryKind, name string) *gen.CatalogueE // judgeCatalogueObserved sets the observed verdict from the authorized and // observed states the entry already carries. It judges what runs against what -// is authorized, never against what is declared, and gives a verdict only when -// both are known: +// is authorized, never against what is declared, and an AFFIRMATIVE verdict is +// constructible only from fresh, complete evidence: // -// - no observation: NOT_OBSERVED, never a match; +// - no observation, or an observation whose freshness is unknown or stale: +// NOT_OBSERVED, never a match. An observation nobody can date is not +// evidence that anything runs now; // - an observation, but the host cannot tell what is authorized: NOT_RECORDED; -// - an observation with no current authorization: RUNNING_UNAUTHORIZED; -// - an observation of the approved digest and incarnation: RUNNING_AUTHORIZED; +// - an observation with no current authorization: RUNNING_UNAUTHORIZED -- +// this one needs no freshness, because something ran without approval and +// that fact does not expire; +// - an observation of the approved digest and a POSITIVE matching +// incarnation: RUNNING_AUTHORIZED. An incarnation of zero on either side is +// an absent incarnation, not a matching one -- two defaults comparing equal +// is exactly how a page says "authorized" about nothing; // - anything else: RUNNING_DIFFERS. func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.CatalogueObserved) { running := observed.GetObservedExecution() @@ -201,10 +209,15 @@ func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.C observed.VerdictValue = &gen.CatalogueObserved_Verdict{Verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED} case authorized.GetAuthorization() == nil: observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueNoVerdict)} + case observed.GetObservedAt() == nil: + // An execution was reported but nobody can say when. Without a date it + // cannot be CURRENT, and an affirmative verdict is a claim about now. + observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, catalogueStaleObservation)} default: approved := authorized.GetAuthorization().GetApprovedExecution() verdict := gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS - if approved.GetImageDigest() != "" && approved.GetImageDigest() == running.GetImageDigest() && + if completeExecution(approved) && completeExecution(running) && + approved.GetImageDigest() == running.GetImageDigest() && approved.GetBuildIncarnation() == running.GetBuildIncarnation() { verdict = gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED } @@ -212,6 +225,13 @@ func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.C } } +// completeExecution reports whether an execution carries both facts a match +// needs. A missing digest or a zero incarnation is absent evidence; comparing +// absent against absent and calling it equal is the defect this guards. +func completeExecution(e *gen.CatalogueExecution) bool { + return e.GetImageDigest() != "" && e.GetBuildIncarnation() > 0 +} + func catalogueInstallation(record *CatalogueInstallationRecord) *gen.CatalogueInstallation { out := &gen.CatalogueInstallation{ Installation: record.Installation, diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_test.go index ed664d174..0e85c1ce0 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue_test.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue_test.go @@ -1,6 +1,7 @@ package business import ( + "google.golang.org/protobuf/types/known/timestamppb" "testing" "time" @@ -28,9 +29,44 @@ func TestCatalogueObservedVerdict(t *testing.T) { name string authorized *gen.CatalogueAuthorized observed *gen.CatalogueExecution - verdict gen.CatalogueObservedVerdict - gap gen.CatalogueGapReason + // stale leaves observation_freshness unset: an execution reported + // with no time it was seen at. + stale bool + verdict gen.CatalogueObservedVerdict + gap gen.CatalogueGapReason }{ + { + // The reviewer's construction: the approved digest and incarnation, + // identical on both sides, but the observation carries no freshness. + // The renderer once showed "Running authorized" beside "Not + // observed" for exactly this; an affirmative verdict is a claim + // about NOW and an undated observation cannot make it. + name: "the approved execution, observed at no known time", + authorized: authorized, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, + stale: true, + gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, + }, + { + // The reviewer's second construction: same digest, both + // incarnations omitted. Two zero defaults compare equal, which is + // how a page says "authorized" about nothing. + name: "same digest, neither side carries an incarnation", + authorized: &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ + Authorization: &gen.CatalogueAuthorization{AuthorizedRevision: 4, ApprovedExecution: &gen.CatalogueExecution{ImageDigest: "sha256:aaa"}}, + }}, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa"}, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + }, + { + // A stale observation of an UNAUTHORIZED execution still condemns: + // something ran without approval, and that fact does not expire. + name: "observed with no current authorization, at no known time", + authorized: notAuthorized, + observed: &gen.CatalogueExecution{ImageDigest: "sha256:aaa", BuildIncarnation: 3}, + stale: true, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED, + }, {name: "authorized, nothing observed", authorized: authorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, {name: "not authorized, nothing observed", authorized: notAuthorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, { @@ -81,6 +117,9 @@ func TestCatalogueObservedVerdict(t *testing.T) { observed := &gen.CatalogueObserved{} if tc.observed != nil { observed.ObservedExecutionValue = &gen.CatalogueObserved_ObservedExecution{ObservedExecution: tc.observed} + if !tc.stale { + observed.ObservationFreshnessValue = &gen.CatalogueObserved_ObservedAt{ObservedAt: timestamppb.Now()} + } } judgeCatalogueObserved(tc.authorized, observed) if tc.gap != gen.CatalogueGapReason_CATALOGUE_GAP_REASON_UNSPECIFIED { diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts index d13b817ee..ea53c12ae 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts @@ -40,16 +40,36 @@ function entry( }); } +// An observation that carries the evidence a verdict rests on: a dated +// execution. An affirmative verdict is a claim about NOW, and the renderer +// refuses to paint one green beside an undated or absent observation — so a +// fixture that omits the date is not "an observation", it is the contradiction. function observedWith(verdict: CatalogueObservedVerdict) { return { observedExecutionValue: { case: "observedExecution" as const, value: { imageDigest: "sha256:bbb", buildIncarnation: BigInt(3) }, }, + observationFreshnessValue: { + case: "observedAt" as const, + value: { seconds: BigInt(1_700_000_000), nanos: 0 }, + }, verdictValue: { case: "verdict" as const, value: verdict }, }; } +// A current approval, so an affirmative verdict has something to be judged +// against. +const currentAuthorization = { + authorizationValue: { + case: "authorization" as const, + value: { + authorizedRevision: BigInt(4), + approvedExecution: { imageDigest: "sha256:bbb", buildIncarnation: BigInt(3) }, + }, + }, +}; + describe("the state model", () => { it("is the registry's six states, in order", () => { expect([...CATALOGUE_STATES]).toEqual([ @@ -194,10 +214,11 @@ describe("authorization", () => { }); describe("observedVerdictView", () => { - it("states running authorized only when the server judged it so", () => { + it("states running authorized only when the server judged it so, with the evidence beside it", () => { expect( observedVerdictView( entry({ + authorized: currentAuthorization, observed: observedWith(CatalogueObservedVerdict.RUNNING_AUTHORIZED), }), ), @@ -208,6 +229,28 @@ describe("observedVerdictView", () => { }); }); + // The reviewer's finding: the renderer once painted "Running authorized" + // green beside "Not observed". The host enforces consistency at its read + // boundary; this is the second line, and it must hold on its own. An + // operator reads the green and stops, so a contradiction is shown as + // unknown, never as success. + it("refuses an authorized verdict whose evidence is missing", () => { + const authorizedVerdict = observedWith(CatalogueObservedVerdict.RUNNING_AUTHORIZED); + + const undated = { ...authorizedVerdict, observationFreshnessValue: { case: undefined } }; + expect( + observedVerdictView(entry({ authorized: currentAuthorization, observed: undated })), + ).toMatchObject({ kind: "gap", label: "Inconsistent" }); + + const noAuthorization = entry({ observed: authorizedVerdict }); + expect(observedVerdictView(noAuthorization)).toMatchObject({ kind: "gap", label: "Inconsistent" }); + + const noExecution = { ...authorizedVerdict, observedExecutionValue: { case: undefined } }; + expect( + observedVerdictView(entry({ authorized: currentAuthorization, observed: noExecution })), + ).toMatchObject({ kind: "gap", label: "Inconsistent" }); + }); + it("flags an execution that differs from the approved one", () => { const view = observedVerdictView( entry({ diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts index 5eb0ebd1c..b12d14ecc 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts @@ -201,6 +201,23 @@ export function observedVerdictView(entry: CatalogueEntry): FactView { ? observed.observedExecutionValue.value : undefined; const detail = `Observed ${executionText(running)}.`; + // The host enforces this at its read boundary; this is the second line, + // not the only one. An affirmative verdict rendered beside an absent + // observation, an undated one, or an absent authorization is a contradiction + // the server must never emit — and if it ever does, the page says so + // rather than painting the row green. An operator reads the green and stops. + if (v.value === CatalogueObservedVerdict.RUNNING_AUTHORIZED) { + const dated = observed?.observationFreshnessValue.case === "observedAt"; + const authorized = entry.authorized?.authorizationValue.case === "authorization"; + if (!running || !dated || !authorized) { + return { + kind: "gap", + label: "Inconsistent", + detail: + "The host reported an authorized execution without the evidence that supports it; treat this row as unknown.", + }; + } + } switch (v.value) { case CatalogueObservedVerdict.RUNNING_AUTHORIZED: return { From 85f09c07705a82d23902a6d32badb3de45f43873 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:11:39 -0400 Subject: [PATCH 07/11] chore(catalogue): goimports ordering in the verdict test The quality lint phase refused platform_catalogue_test.go: the timestamppb import was inserted at the top of the block rather than in its group. goimports only; no behaviour change. Co-Authored-By: Claude Fable 5.1 --- .../accounts/code/pkg/business/platform_catalogue_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_test.go index 0e85c1ce0..626bf9208 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue_test.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue_test.go @@ -1,10 +1,11 @@ package business import ( - "google.golang.org/protobuf/types/known/timestamppb" "testing" "time" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/stretchr/testify/require" gen "accounts/pkg/gen/saas/accounts/v1" From 050522124a69c86410a887d8698b23c712fe3b32 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:20:59 -0400 Subject: [PATCH 08/11] An affirmative Catalogue verdict needs complete, current evidence (Refs #1020) Two things, both in the observed state. The approved containers now come from the deployment contract's own member-and-workload projection (Projections, codefly-dev/cli#902) instead of Rows paired against the inventory's workload order by position. A row does not name its workload, so the pairing was the host inferring an association the contract can state; a contract that reorders workloads or projects a different number of rows would have silently attributed one member's containers to another. Selection is by member_binding, and each container is keyed by the projection's own workload name. The second is the review's blocker. RUNNING_AUTHORIZED did not read the observation's freshness, so the approved execution matched beside observation_freshness_gap = NOT_OBSERVED, and an ancient observed_at read the same as a current one. The renderer then showed that green without looking at the evidence under it: "Running authorized" beside "Not observed" is worse than saying nothing, because a reader sees the green and stops. A second construction gave both sides an incarnation of zero, where both getters return the same value and so matched: this host assigns incarnations from one, so zero is an approval carrying none, not one approving incarnation zero. An affirmative verdict is now constructible only from an approval complete enough to judge against - a non-zero build_incarnation, an inventory held at the digest approval signed and accepted by the contract, with at least one approved container - and an observation that is complete, carries observed_at, and is inside the validity this host treats as current. Each missing piece leaves a named gap, and a defect in the evidence is never reported as DIFFERS either: that would be a statement about what runs. The asymmetry is deliberate and tested. RUNNING_DIFFERS and RUNNING_UNAUTHORIZED carry none of that burden, because withholding an alarming verdict for want of freshness would hide the risk the page exists to surface; an hour-old observation of something running unauthorized still reports it. The validity window is this host's own, named once in catalogueObservationValidity, because the observing producer declares no validity of its own yet. The rule is enforced where the verdict is made and again at the read boundary, which replaces an unsupported affirmative verdict with a gap and logs it: nothing should reach that check, but the state messages are to be filled from records that do not exist yet (#953's applied presence, the signed approval, cluster observation), and a filler that set a value case without re-judging would otherwise publish a green cell. platform_admin.proto states the rule on the verdict oneof so a consumer reads it from the contract; Go enforces it, since nothing validates a response on the wire. The browser checks that each piece of evidence is present before showing the affirmative cell, and deliberately does not reproduce the window. Tests. The verdict table now states every case's observation age, including the ones that match in every other respect and must not come back green. Four mutations killed: dropping the freshness check, dropping the incarnation guard, making the window look only backwards, and removing the read boundary's freshness check. The emptiness guard cannot be reached through an inventory the contract accepts, and a test pins why - the contract refuses a workload with no container - so it fails there rather than the page turning green here. The container fixture no longer asserts an affirmative verdict beside a freshness gap; it has an entry that sends that contradiction and expects a gap. Also, honestly: the handler-layer role tests cannot isolate the handler gate, because both gates read the same stored role and return the same code, so removing either leaves them green. Their comment said otherwise. The business gate is now proved on its own, in pkg/business with no handler in the call, and the comment says what the handler test does and does not establish. Co-Authored-By: Claude Opus 5 (1M context) --- .../api/accounts/authority/contract.binpb | Bin 958992 -> 960217 bytes .../saas/accounts/v1/platform_admin.proto | 23 +- .../api/accounts/connect/contract.binpb | Bin 958992 -> 960217 bytes .../saas/accounts/v1/platform_admin.proto | 23 +- module/contracts/api/catalog.codefly.json | 4 +- module/module.package.codefly.yaml | 4 +- module/services/accounts/AGENTS.md | 56 ++++- module/services/accounts/code/go.mod | 2 +- module/services/accounts/code/go.sum | 4 +- .../platform_admin_authorization_test.go | 11 +- .../code/pkg/business/platform_catalogue.go | 177 ++++++++++++--- .../platform_catalogue_access_test.go | 56 +++++ .../pkg/business/platform_catalogue_test.go | 213 ++++++++++++++++-- .../gen/saas/accounts/v1/platform_admin.pb.go | 23 +- .../generated/openapi-raw/api.swagger.json | 4 +- .../accounts/openapi/api.swagger.json | 4 +- .../saas/accounts/v1/platform_admin.proto | 23 +- module/services/frontend/AGENTS.md | 10 + .../generated/contract/catalog.codefly.json | 2 +- .../generated/contract/contract.binpb | Bin 958992 -> 960217 bytes .../saas-sdk/generated/library.codefly.yaml | 4 +- .../gen/saas/accounts/v1/platform_admin_pb.ts | 23 +- .../platform-catalogue/model/facts.test.ts | 109 ++++++++- .../platform-catalogue/model/facts.ts | 48 +++- ...platform-catalogue-page.container.test.tsx | 35 ++- .../gen/saas/accounts/v1/platform_admin_pb.ts | 23 +- 26 files changed, 782 insertions(+), 99 deletions(-) create mode 100644 module/services/accounts/code/pkg/business/platform_catalogue_access_test.go diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 78a3928696ef00a6995d088a65d2e89a6a7bcde0..140c47177d1433f1a53dab9c83befce06532bd23 100644 GIT binary patch delta 12722 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 delta 11440 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
V!48S$AzwwObIB&5sCv^+u+~lDLV6L>CL({6 z7=DL^F3@qa5c?f;VIqIC2=}|lCk;EW#qR(~@VwRU07=+^Z9=6hma90~Aqtn6@A2VI5}>zv=p+I9-VPlBeV=Y+?j&(1mUcjhgATEee;Ne6_#(u8{uV2MgXu(Kj<{5rZcIV==aN&t#4OyaP(h(2U#6MPSgtM0b5 zz}trUR*2J%uQYtW72#>emo)G_;`x#U&qqC9lEC*^yPJ_T&Gopy8A$`z?>ruo!1aXZ zN)ot!@41o$t|vQm1lLpkX4LOb$r&N8w!tk8RdGgyS6O~W!mQ4Snk#LBR+?Q8>%#iH&(C<@Qa=dsgtYZVf;TX_8GjJr6EZ6>NQ2uSJw}q?`Vr|0 z43dEHQRxyiNt7r?$wDu?2qU6zgLKy3L0;u9PDURG;;1NCMR7q#D@X8KBPaP^lH5&TQ9_E;&$rUNUH2Qdri7ZPOtS1 zDVAWZrjjezMEiyemte7G=Z%Q1SR~ce|Dqw$E|y5NS5q(-$EroLPtxJX@MXJ2x_hh` z>uizk9@8(fEpqs+HqRDg4$Gui?RXa(-pgdT+VLh0yx;P?NrLCMJ#Ui0d%09eR!WdY z8RA`8sJF8>B09h4@skGpD?CM#fd74upCsV_z~d(g_*b^?n1D6-w@Oz@Q#0USvKwg^{(|f zMZf#6l~?`7ZbN{%tdnAg+lIhw!#Wx6aN9r{yngDpfh2hT%x{C1I_d96>!qvcf@hB7 z1W;og_8UES(!hO_$43&lZ}!|t0{1PRJ4xWawL?c>-{!GXD`4La?1@b)SgY76#Rz*c z0-LON%J4?Z*GS0KJ7vu;E#EL!yCpm9(u)2LvfJYgv8>%5ul_8u+ox=NT2XrvDZX}m zL&JBE48L}KNrT%q&zB^)-s}021ibrvawbWG-S3k#X#jh`@ktnIAp4~!OA^R_<;ju+ zvIkvH(-CA3X|g6c>#s%MNO7g57+Q+o$Z)u&Xe7+)8(A|f_*)R^dN+#IG0A?A>8$Ug z|4WWZMPCB-12jA)FV5cM_1BbMl=9lyx%J1)cXjvr~@_nqfQ5kA zZ5Bi+KulKRIR~QDfS9bp=Nt&q0Ah*Bmu-! z4}v6snC3x{1Q63ZbOeZJl{;Kmkr*~ZiRP2FKNI#*Wn!uN4N$t@;uL%B=DWDToF$aUC}S351zw!gBO*n+vyoF|HIQF z37B8v!1zWw=zLUBu}I+}9&0wC6Ml+QY-M`)`DjeC)CX}VpjfId7;dR|Lhq&O7k{=v z+zFIhlz87!?qn#psPKJ9nKV#d<|s3g;Q1{NnIu4dyIl~Irh|C74`R|l_Z<(3B+z}= z(upu4)M6_~>sD1p|G_@Rzfv+X2liXVB3q=Dkc9tKIE_=)l#p-2P9wH_l$z__kMPhkAiW5klg zdL^=$Ew>cctMCdpEBh9-Ue#P~DaQCsP_aFU!x&?|8rQ#=R;$>M)W9xBEOs zlHhuOh7qnU#RE$9v7bpvqQv=?!hhzN2sI-59;7Uq&=-0V!S-uUjU=%B#l?qlh8M~2G}KQ` z0~kJAN$`B$!zKwK`$M|~N}7(@AAQV{2Ck3fT{%P&xIUV9+K_i4ixr8=(RuMpn?NgV%#Kcv3j8!QI$v{x4b!Svjm@*C^I6WU!V<^k75!$7 zxU9mo$L70?Kx_S@N)?ag*!&%j<}A4?kQ)VUYw_lRPYv0IRV>N5GowVPO&Csl51= zWmQG2`e-d2VS~+RnD0~hnj3@XO|VtRYFeJnPk*mUW+v0}uJ*1*yJ>k>dsidarsZAj zT@7T@^J1<;R&9_?&xdmzGSYzT*$i0`BMF|L^TbF3u^D+^dy}SvZKmf(8u&f$F^~j) zvpheNz^}>kBMJOwcjyRyb38w+NYu{Di)$>vY8!0x@?rMX$4HpfyfHO}lIEI1>C&11 E57I1I=>Px# diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto index 96b2a0010..f6eac2488 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/platform_admin.proto @@ -333,6 +333,11 @@ message CatalogueAuthorization { // incarnation, which is the same host-assigned value as presented by the // running workload; an incarnation from any other assigner would make that // comparison meaningless, not merely wrong. + // + // Assigned from one, so zero is an approval that carries no incarnation + // rather than one approving incarnation zero. An approval at zero cannot + // produce an affirmative verdict: left uncompared, it would match any + // observation that also carries none. uint64 build_incarnation = 4; } @@ -377,7 +382,10 @@ message CatalogueObservedExecution { enum CatalogueObservedVerdict { CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; // Every container of the approved inventory member runs its approved image - // digest, no other container runs, and the incarnation is the approved one. + // digest, no other container runs, the incarnation is the approved one, and + // the observation that says so is dated and current. This is the only verdict + // a reader acts on by doing nothing, so it is the only one that requires + // complete, current evidence: see the verdict field below. CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; // Something other than the approved execution is running: what cluster // admission refuses. @@ -405,6 +413,19 @@ message CatalogueObserved { // A verdict exists only when both a complete observation and a readable // authorization do; anything less is a gap, and an unobserved or // incompletely observed entry is never a match. + // + // RUNNING_AUTHORIZED is constructible only from an approval complete enough + // to judge against (an inventory this host holds at the digest approval + // signed, and a non-zero build_incarnation) and an observation that is + // complete, carries observed_at, and is within the validity this host treats + // as current. Any of those missing leaves a gap. The two verdicts that report + // trouble carry no such burden: a partly evidenced report of something wrong + // is still worth showing, while "Running authorized" beside "Not observed" is + // worse than nothing, because a reader sees it and stops looking. + // + // The rule is enforced where the verdict is made and again at the read + // boundary that serves it, so a reader of this response never has to re-derive + // it from the fields beside it. oneof verdict_value { CatalogueObservedVerdict verdict = 7; CatalogueGap verdict_gap = 8; diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 78a3928696ef00a6995d088a65d2e89a6a7bcde0..140c47177d1433f1a53dab9c83befce06532bd23 100644 GIT binary patch delta 12722 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 delta 11440 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
V!48S$AzwwObIB&5sCv^+u+~lDLV6L>CL({6 z7=DL^F3@qa5c?f;VIqIC2=}|lCk;EW#qR(~@VwRU07=+^Z9=6hma90~Aqtn6@A2VI5}>zv=p+I9-VPlBeV=Y+?j&(1mUcjhgATEee;Ne6_#(u8{uV2MgXu(Kj<{5rZcIV==aN&t#4OyaP(h(2U#6MPSgtM0b5 zz}trUR*2J%uQYtW72#>emo)G_;`x#U&qqC9lEC*^yPJ_T&Gopy8A$`z?>ruo!1aXZ zN)ot!@41o$t|vQm1lLpkX4LOb$r&N8w!tk8RdGgyS6O~W!mQ4Snk#LBR+?Q8>%#iH&(C<@Qa=dsgtYZVf;TX_8GjJr6EZ6>NQ2uSJw}q?`Vr|0 z43dEHQRxyiNt7r?$wDu?2qU6zgLKy3L0;u9PDURG;;1NCMR7q#D@X8KBPaP^lH5&TQ9_E;&$rUNUH2Qdri7ZPOtS1 zDVAWZrjjezMEiyemte7G=Z%Q1SR~ce|Dqw$E|y5NS5q(-$EroLPtxJX@MXJ2x_hh` z>uizk9@8(fEpqs+HqRDg4$Gui?RXa(-pgdT+VLh0yx;P?NrLCMJ#Ui0d%09eR!WdY z8RA`8sJF8>B09h4@skGpD?CM#fd74upCsV_z~d(g_*b^?n1D6-w@Oz@Q#0USvKwg^{(|f zMZf#6l~?`7ZbN{%tdnAg+lIhw!#Wx6aN9r{yngDpfh2hT%x{C1I_d96>!qvcf@hB7 z1W;og_8UES(!hO_$43&lZ}!|t0{1PRJ4xWawL?c>-{!GXD`4La?1@b)SgY76#Rz*c z0-LON%J4?Z*GS0KJ7vu;E#EL!yCpm9(u)2LvfJYgv8>%5ul_8u+ox=NT2XrvDZX}m zL&JBE48L}KNrT%q&zB^)-s}021ibrvawbWG-S3k#X#jh`@ktnIAp4~!OA^R_<;ju+ zvIkvH(-CA3X|g6c>#s%MNO7g57+Q+o$Z)u&Xe7+)8(A|f_*)R^dN+#IG0A?A>8$Ug z|4WWZMPCB-12jA)FV5cM_1BbMl=9lyx%J1)cXjvr~@_nqfQ5kA zZ5Bi+KulKRIR~QDfS9bp=Nt&q0Ah*Bmu-! z4}v6snC3x{1Q63ZbOeZJl{;Kmkr*~ZiRP2FKNI#*Wn!uN4N$t@;uL%B=DWDToF$aUC}S351zw!gBO*n+vyoF|HIQF z37B8v!1zWw=zLUBu}I+}9&0wC6Ml+QY-M`)`DjeC)CX}VpjfId7;dR|Lhq&O7k{=v z+zFIhlz87!?qn#psPKJ9nKV#d<|s3g;Q1{NnIu4dyIl~Irh|C74`R|l_Z<(3B+z}= z(upu4)M6_~>sD1p|G_@Rzfv+X2liXVB3q=Dkc9tKIE_=)l#p-2P9wH_l$z__kMPhkAiW5klg zdL^=$Ew>cctMCdpEBh9-Ue#P~DaQCsP_aFU!x&?|8rQ#=R;$>M)W9xBEOs zlHhuOh7qnU#RE$9v7bpvqQv=?!hhzN2sI-59;7Uq&=-0V!S-uUjU=%B#l?qlh8M~2G}KQ` z0~kJAN$`B$!zKwK`$M|~N}7(@AAQV{2Ck3fT{%P&xIUV9+K_i4ixr8=(RuMpn?NgV%#Kcv3j8!QI$v{x4b!Svjm@*C^I6WU!V<^k75!$7 zxU9mo$L70?Kx_S@N)?ag*!&%j<}A4?kQ)VUYw_lRPYv0IRV>N5GowVPO&Csl51= zWmQG2`e-d2VS~+RnD0~hnj3@XO|VtRYFeJnPk*mUW+v0}uJ*1*yJ>k>dsidarsZAj zT@7T@^J1<;R&9_?&xdmzGSYzT*$i0`BMF|L^TbF3u^D+^dy}SvZKmf(8u&f$F^~j) zvpheNz^}>kBMJOwcjyRyb38w+NYu{Di)$>vY8!0x@?rMX$4HpfyfHO}lIEI1>C&11 E57I1I=>Px# diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto index 96b2a0010..f6eac2488 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/platform_admin.proto @@ -333,6 +333,11 @@ message CatalogueAuthorization { // incarnation, which is the same host-assigned value as presented by the // running workload; an incarnation from any other assigner would make that // comparison meaningless, not merely wrong. + // + // Assigned from one, so zero is an approval that carries no incarnation + // rather than one approving incarnation zero. An approval at zero cannot + // produce an affirmative verdict: left uncompared, it would match any + // observation that also carries none. uint64 build_incarnation = 4; } @@ -377,7 +382,10 @@ message CatalogueObservedExecution { enum CatalogueObservedVerdict { CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; // Every container of the approved inventory member runs its approved image - // digest, no other container runs, and the incarnation is the approved one. + // digest, no other container runs, the incarnation is the approved one, and + // the observation that says so is dated and current. This is the only verdict + // a reader acts on by doing nothing, so it is the only one that requires + // complete, current evidence: see the verdict field below. CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; // Something other than the approved execution is running: what cluster // admission refuses. @@ -405,6 +413,19 @@ message CatalogueObserved { // A verdict exists only when both a complete observation and a readable // authorization do; anything less is a gap, and an unobserved or // incompletely observed entry is never a match. + // + // RUNNING_AUTHORIZED is constructible only from an approval complete enough + // to judge against (an inventory this host holds at the digest approval + // signed, and a non-zero build_incarnation) and an observation that is + // complete, carries observed_at, and is within the validity this host treats + // as current. Any of those missing leaves a gap. The two verdicts that report + // trouble carry no such burden: a partly evidenced report of something wrong + // is still worth showing, while "Running authorized" beside "Not observed" is + // worse than nothing, because a reader sees it and stops looking. + // + // The rule is enforced where the verdict is made and again at the read + // boundary that serves it, so a reader of this response never has to re-derive + // it from the fields beside it. oneof verdict_value { CatalogueObservedVerdict verdict = 7; CatalogueGap verdict_gap = 8; diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index bbfe7e115..96565e7e1 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b", + "digest": "sha256:a80e92b319dc442c93eb9604129192c86aaf491e635cb549cd7bcca5681b45ee", "services": [ { "name": "APIKeyService", @@ -477,7 +477,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b", + "digest": "sha256:a80e92b319dc442c93eb9604129192c86aaf491e635cb549cd7bcca5681b45ee", "services": [ { "name": "APIKeyService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 880d3576b..9f1fb55a7 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b + digest: sha256:a80e92b319dc442c93eb9604129192c86aaf491e635cb549cd7bcca5681b45ee - endpoint: connect kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/connect/contract.binpb - digest: sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b + digest: sha256:a80e92b319dc442c93eb9604129192c86aaf491e635cb549cd7bcca5681b45ee - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AGENTS.md b/module/services/accounts/AGENTS.md index f0fc12259..da538ebef 100644 --- a/module/services/accounts/AGENTS.md +++ b/module/services/accounts/AGENTS.md @@ -188,10 +188,11 @@ that delivery aggregate the entry is, plus the host's own `build_incarnation`, which is not an inventory field. The inventory's canonical bytes travel once per response in `approved_inventories`, and the host reads them only through the contract: `Check` accepts the inventory's intrinsic form, its digest must be the -approved one, and the containers are the contract's own seven-key projection -(`Rows`) — no traversal of workload templates is written here. No proto here re-describes an inventory -field; a second description would be the first thing to disagree with what is -deployed. +approved one, and the containers are the contract's own projection, selected by +member binding and workload name (`Projections`, codefly-dev/cli#902) — never +paired by position, and no traversal of workload templates is written here. No +proto here re-describes an inventory field; a second description would be the +first thing to disagree with what is deployed. **The observed verdict judges against the authorization, never the declaration** (`judgeCatalogueObserved`). An observation is each container's image @@ -199,17 +200,50 @@ digest, keyed `/` in the approved inventory's names plus the presented incarnation: - `RUNNING_AUTHORIZED` only when every container of the approved member (init - containers included) runs its approved digest, nothing else runs, and the - incarnation is the approved one; + containers included) runs its approved digest, nothing else runs, the + incarnation is the approved one, **and the observation that says so is dated + and current**; - `RUNNING_DIFFERS` for any other digest, an extra container, or another incarnation; - `RUNNING_UNAUTHORIZED` for an observation with no current authorization — the row the retirement sweep exists to stop; -- `NOT_OBSERVED` with no observation, or one missing an approved container (an - incomplete observation is never counted as approved); -- `NOT_RECORDED` when there is no authorization to read, or the approved - inventory is not held, does not hash to its digest, is refused by the - contract, or has no such member. +- `NOT_OBSERVED` with no observation, one missing an approved container (an + incomplete observation is never counted as approved), or a match the host + cannot date: no `observed_at`, a stamp older than + `catalogueObservationValidity`, or one further ahead of this host's clock than + `catalogueObservationSkew`; +- `NOT_RECORDED` when there is no authorization to read, the approval carries no + `build_incarnation`, or the approved inventory is not held, does not hash to + its digest, is refused by the contract, or has no such member. + +**Only the affirmative verdict carries that burden, and deliberately so.** It is +the one cell a reader acts on by doing nothing, so it needs an approval complete +enough to judge against — a non-zero `build_incarnation`, since this host assigns +incarnations from one and zero therefore means *none*, which would otherwise match +an observation that also carries none — and an observation that is complete and +current. The two verdicts that report trouble need neither: a stale report of +something running unauthorized is still worth showing, while "Running authorized" +beside "Not observed" is worse than nothing. Withholding the alarming verdict for +want of freshness would hide the risk the page exists to surface. + +The window is this host's own, named once in `catalogueObservationValidity` +(`pkg/business/platform_catalogue.go`), because the observing producer declares no +validity of its own yet; when its record carries one, that bound replaces the +constant and this host stops choosing. + +**The invariant is enforced twice and asserted in the contract.** +`judgeCatalogueObserved` is the only author of a verdict, and +`enforceCatalogueVerdictEvidence` re-checks every affirmative verdict at the read +boundary before the response leaves, replacing one its own evidence does not +support with a gap and logging it. Nothing should reach the second check — but the +state messages are to be filled from records that do not exist yet (#953's applied +presence, the signed approval, cluster observation), and a filler that set a value +case without re-judging would otherwise publish a green cell. `platform_admin.proto` +states the rule on the `verdict` oneof so a consumer reads it from the contract; it +is Go, not protovalidate, that enforces it, since nothing validates a response on +the wire. The browser does not re-derive the judgement either: `facts.ts` shows the +affirmative cell only when the evidence is present beside it, and never reproduces +the validity window, which would drift from the host's. Withdrawal, credential revocation, retirement and build size have gap-only `oneof`s: their value vocabularies are later spec items' and core#708's to define, diff --git a/module/services/accounts/code/go.mod b/module/services/accounts/code/go.mod index cd79e9f4e..6a8b71cb9 100644 --- a/module/services/accounts/code/go.mod +++ b/module/services/accounts/code/go.mod @@ -7,7 +7,7 @@ require ( buf.build/go/protovalidate v1.4.0 connectrpc.com/connect v1.21.0 connectrpc.com/otelconnect v0.9.0 - github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006144637-aacb2284b983 + github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006145225-c65184679fda github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683 github.com/codefly-dev/interface-cache/go/cache v0.2.2-0.20260926205419-327a6e1d9aba github.com/codefly-dev/sdk-go v0.2.1-0.20260926212413-b91ce91eb23e diff --git a/module/services/accounts/code/go.sum b/module/services/accounts/code/go.sum index 5a5c0ed4c..38f83ae62 100644 --- a/module/services/accounts/code/go.sum +++ b/module/services/accounts/code/go.sum @@ -58,8 +58,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006144637-aacb2284b983 h1:kcnaCpxyprLrlQSeXyyEEMeVbhTtYoknAv19T5lJXQw= -github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006144637-aacb2284b983/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= +github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006145225-c65184679fda h1:UI416Gg1LEPXZuzl04wVZWxU6Uo2pJsoqjECM/3WSmE= +github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006145225-c65184679fda/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683 h1:/vzymsu5B9NWwdhWRtlU+68VcVEVDmeZNChRCXAUwlE= github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683/go.mod h1:SVF+t040X+9pAXAqBnNE/JkXwmw9peVuKnHKFSOzyzk= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= diff --git a/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go b/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go index eba43bc18..acc7ce94f 100644 --- a/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go +++ b/module/services/accounts/code/pkg/adapters/platform_admin_authorization_test.go @@ -137,8 +137,15 @@ func TestPlatformAdminHandlersRejectNonAdmin(t *testing.T) { } // The handler gate enforces the same minimum role as its business method, so a -// lower-privileged platform admin is denied at the handler for super_admin-only -// endpoints even if the business-layer check were removed. +// lower-privileged platform admin is denied for a super_admin-only endpoint. +// +// What this does not establish: which of the two gates did the denying. Both read +// the same stored role and return the same code, so removing either one on its own +// leaves this test green. The business half is proved on its own where it can be — +// in pkg/business, with no handler in the call (TestListPlatformCatalogueRequires +// SuperAdminInBusiness for the Catalogue). The handler half cannot be isolated the +// same way, and claiming otherwise here would be a test that reads as proof of +// defence in depth while proving one layer twice. func TestPlatformAdminHandlersEnforceMinimumRole(t *testing.T) { for _, h := range platformAdminHandlers() { if h.minRole != "super_admin" { diff --git a/module/services/accounts/code/pkg/business/platform_catalogue.go b/module/services/accounts/code/pkg/business/platform_catalogue.go index 1ee0d7c00..43e42ab67 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue.go @@ -6,6 +6,7 @@ import ( "maps" "slices" "strings" + "time" deployment "github.com/codefly-dev/cli/contracts/deployment" "github.com/codefly-dev/core/wool" @@ -71,6 +72,20 @@ const ( catalogueNoAgentRelease = "The installation's agent principal carries no publisher/name:version identifier." ) +// catalogueObservationValidity is how long an observation may be used to say +// what runs *now*, and catalogueObservationSkew how far ahead of this host's +// clock an observer's stamp may be before it stops being evidence at all. +// +// Only the affirmative verdict needs either: a reassuring answer read off an +// observation that no longer describes the present is the one failure this page +// cannot afford, while an answer that reports trouble is worth showing whatever +// its age. The observing producer declares no validity of its own yet; when its +// record carries one, that bound replaces these and this host stops choosing. +const ( + catalogueObservationValidity = 5 * time.Minute + catalogueObservationSkew = 30 * time.Second +) + // ListPlatformCatalogue reads the Catalogue for a super administrator. func (s *Service) ListPlatformCatalogue(ctx context.Context, actorID string, includeTombstoned bool) (*PlatformCatalogue, error) { w := wool.Get(ctx).In("ListPlatformCatalogue") @@ -94,12 +109,54 @@ func (s *Service) ListPlatformCatalogue(ctx context.Context, actorID string, inc }); err != nil { return nil, w.Wrapf(err, "cannot read the platform catalogue") } + now := time.Now() + entries := projectPlatformCatalogue(s.modulePrincipals, registrations, installations, includeTombstoned, now) + enforceCatalogueVerdictEvidence(ctx, entries, now) return &PlatformCatalogue{ - Entries: projectPlatformCatalogue(s.modulePrincipals, registrations, installations, includeTombstoned), + Entries: entries, RegistryRevision: revision, }, nil } +// enforceCatalogueVerdictEvidence is the read boundary refusing to serve an +// affirmative verdict that the evidence beside it does not support. Nothing +// should reach it: judgeCatalogueObserved is the only author of a verdict, and +// it applies the same rule. But the states are filled from records this host +// does not hold yet — applied presence (#953), the deployment contract's signed +// approval, cluster observation — and any of those fillers could set a value +// case without re-judging. An operator reads the green and stops looking, so the +// boundary fails closed: the verdict becomes a gap naming what was missing, and +// the inconsistency is logged as the defect it is rather than rendered. +func enforceCatalogueVerdictEvidence(ctx context.Context, entries []*PlatformCatalogueEntry, now time.Time) { + for _, row := range entries { + observed := row.Entry.GetObserved() + if observed.GetVerdict() != gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED { + continue + } + var reason string + switch approval := row.Entry.GetAuthorized().GetAuthorization(); { + case approval == nil: + reason = "It carries no approval for an observation to be authorized against." + case approval.GetBuildIncarnation() == 0: + reason = "Its approval carries no build incarnation." + case observed.GetObservedExecution() == nil: + reason = "It carries no observed execution." + default: + reason = catalogueObservationNotCurrent(observed, now) + } + if reason == "" { + continue + } + wool.Get(ctx).In("ListPlatformCatalogue").Error( + "withholding an affirmative catalogue verdict the entry's own evidence does not support", + wool.Field("entry", row.Entry.GetName()), + wool.Field("reason", reason)) + observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap( + gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, + "This host withheld an affirmative verdict that its own evidence does not support. "+reason)} + } +} + // projectPlatformCatalogue builds the Catalogue's rows: one per composed module, // then one per solution identifier that a registration or an active // installation names, each ordered by name. A tombstoned registration is listed @@ -109,10 +166,11 @@ func projectPlatformCatalogue( registrations []*SolutionRegistration, installations []*CatalogueInstallationRecord, includeTombstoned bool, + now time.Time, ) []*PlatformCatalogueEntry { var moduleEntries []*PlatformCatalogueEntry for _, grant := range modules { - entry := newCatalogueEntry(gen.CatalogueEntryKind_CATALOGUE_ENTRY_KIND_MODULE, grant.Prefix) + entry := newCatalogueEntry(gen.CatalogueEntryKind_CATALOGUE_ENTRY_KIND_MODULE, grant.Prefix, now) entry.PublisherValue = &gen.CatalogueEntry_PublisherGap{PublisherGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueModulePublisher)} moduleEntries = append(moduleEntries, &PlatformCatalogueEntry{Entry: entry}) } @@ -122,7 +180,7 @@ func projectPlatformCatalogue( if existing, ok := solutions[name]; ok { return existing } - entry := newCatalogueEntry(gen.CatalogueEntryKind_CATALOGUE_ENTRY_KIND_SOLUTION, name) + entry := newCatalogueEntry(gen.CatalogueEntryKind_CATALOGUE_ENTRY_KIND_SOLUTION, name, now) entry.PublisherValue = &gen.CatalogueEntry_PublisherGap{PublisherGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, catalogueUnregistered)} solutions[name] = &PlatformCatalogueEntry{Entry: entry} return solutions[name] @@ -155,7 +213,7 @@ func projectPlatformCatalogue( // this host carries a declaration, an approval, an applied generation, an // observation, a withdrawal, a retirement or a build size yet. When one does, // the entry reads it here and the gap gives way to the value. -func newCatalogueEntry(kind gen.CatalogueEntryKind, name string) *gen.CatalogueEntry { +func newCatalogueEntry(kind gen.CatalogueEntryKind, name string, now time.Time) *gen.CatalogueEntry { notRecorded := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED notObserved := gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED entry := &gen.CatalogueEntry{ @@ -185,27 +243,38 @@ func newCatalogueEntry(kind gen.CatalogueEntryKind, name string) *gen.CatalogueE }, BuildSizeValue: &gen.CatalogueEntry_BuildSizeGap{BuildSizeGap: catalogueGap(notRecorded, catalogueNoBuildSize)}, } - judgeCatalogueObserved(entry.Authorized, entry.Observed, nil) + judgeCatalogueObserved(entry.Authorized, entry.Observed, nil, now) return entry } // judgeCatalogueObserved sets the observed verdict from the authorized and // observed states the entry already carries. It judges what runs against what -// is authorized, never against what is declared, and gives a verdict only when -// both are known: +// is authorized, never against what is declared. +// +// The asymmetry here is deliberate. An affirmative verdict is constructible only +// from a complete, readable approval and a complete observation whose age is +// known and current, because an operator reads the green and stops looking. A +// verdict that reports trouble is never withheld for want of freshness: a stale +// report of something running unauthorized is still worth seeing. // // - no observation: NOT_OBSERVED, never a match; -// - an observation with no current authorization: RUNNING_UNAUTHORIZED; +// - an observation with a known absence of authorization: RUNNING_UNAUTHORIZED; // - an observation, but no authorization this host can read: NOT_RECORDED; +// - an approval there is nothing to judge against — no build incarnation, an +// inventory this host does not hold or the contract refuses, or a member the +// inventory approves no container for: NOT_RECORDED. A defect in the +// evidence is not a statement about what runs, so it is never DIFFERS; // - an observation missing an approved container: NOT_OBSERVED, because an // incomplete observation is never counted as approved execution; -// - every approved container on its approved digest, nothing else running, -// and the approved incarnation: RUNNING_AUTHORIZED; +// - the approved execution exactly, but an observation whose age is unknown or +// past catalogueObservationValidity: NOT_OBSERVED. A match this host cannot +// date is not a match now; +// - the approved execution exactly, currently observed: RUNNING_AUTHORIZED; // - anything else: RUNNING_DIFFERS. // // The approved execution is read from the approved inventory through the // deployment contract, keyed by the inventory's digest in inventories. -func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.CatalogueObserved, inventories map[string][]byte) { +func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.CatalogueObserved, inventories map[string][]byte, now time.Time) { gap := func(reason gen.CatalogueGapReason, detail string) { observed.VerdictValue = &gen.CatalogueObserved_VerdictGap{VerdictGap: catalogueGap(reason, detail)} } @@ -225,30 +294,75 @@ func judgeCatalogueObserved(authorized *gen.CatalogueAuthorized, observed *gen.C return } approval := authorized.GetAuthorization() + // This host assigns the incarnation, starting at one, so zero is an approval + // that carries none rather than one approving incarnation zero. Comparing an + // observation against it would make any observation that also carries none + // into a match, which is the whole class of false green this guard closes. + if approval.GetBuildIncarnation() == 0 { + gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, + "The approval carries no build incarnation, so there is nothing for an observed incarnation to match.") + return + } approved, err := approvedContainerImageDigests(inventories[approval.GetInventoryDigest()], approval.GetInventoryDigest(), approval.GetMemberBinding()) if err != nil { gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, "The approved inventory cannot be read: "+err.Error()) return } + // Unreachable through an inventory the contract accepts: it refuses a + // workload with no application container (CONTAINER_REQUIRED), which + // TestExecutionInventoryContractRequiresAContainer pins. The guard is here + // because the day that changes, an approved member with no container would + // match an observation with none — the empty-equals-empty green this whole + // function exists to refuse. + if len(approved) == 0 { + gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, + "The approved inventory approves no container for member "+approval.GetMemberBinding()+", so nothing observed can match it.") + return + } observedDigests := running.GetContainerImageDigests() - for key := range approved { + for _, key := range slices.Sorted(maps.Keys(approved)) { if _, ok := observedDigests[key]; !ok { gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, "The observation is incomplete: approved container "+key+" was not observed.") return } } - if maps.Equal(approved, observedDigests) && running.GetBuildIncarnation() == approval.GetBuildIncarnation() { - verdict(gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED) + if !maps.Equal(approved, observedDigests) || running.GetBuildIncarnation() != approval.GetBuildIncarnation() { + verdict(gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS) + return + } + if reason := catalogueObservationNotCurrent(observed, now); reason != "" { + gap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, reason) return } - verdict(gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS) + verdict(gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED) +} + +// catalogueObservationNotCurrent says why an observation cannot speak for what +// runs now, or "" when it can. It is the one definition of observation currency: +// the verdict's author and the read boundary that enforces the invariant both +// read it here, so neither can drift from the other. +func catalogueObservationNotCurrent(observed *gen.CatalogueObserved, now time.Time) string { + at := observed.GetObservedAt() + if at == nil { + return "The observation carries no time, so this host cannot tell whether it describes what runs now." + } + switch age := now.Sub(at.AsTime()); { + case age > catalogueObservationValidity: + return fmt.Sprintf("The observation is %s old, past the %s this host treats as current.", + age.Round(time.Second), catalogueObservationValidity) + case age < -catalogueObservationSkew: + return fmt.Sprintf("The observation is stamped %s ahead of this host's clock, past the %s of skew it tolerates.", + (-age).Round(time.Second), catalogueObservationSkew) + } + return "" } // approvedContainerImageDigests reads one member's approved containers out of // an approved inventory, as "/" → image manifest // digest. Everything comes from the deployment contract: Check accepts the // inventory's intrinsic form, its digest must be the one approval signed, and -// the containers are the contract's own seven-key projection (Rows), never a +// the containers are the contract's own projection, selected by member and +// workload name (Projections) — never paired by position, and never a // traversal of the workload templates written here. func approvedContainerImageDigests(canonical []byte, digest, member string) (map[string]string, error) { if len(canonical) == 0 { @@ -261,35 +375,24 @@ func approvedContainerImageDigests(canonical []byte, digest, member string) (map if got := checked.Digest(); got != digest { return nil, fmt.Errorf("its canonical bytes hash to %s, not the approved %s", got, digest) } - inventory := checked.Inventory() - var workloads []string - for _, candidate := range inventory.Members { - if candidate.Binding == member { - workloads = candidate.Workloads - } - } - if workloads == nil { - return nil, fmt.Errorf("it has no member %q", member) - } - // The projection is one row per workload, in the inventory's workload - // order; a row does not name its workload, so the pairing is by position. - rows := checked.Rows() - if len(rows) != len(inventory.Workloads) { - return nil, fmt.Errorf("the contract projected %d rows for %d workloads", len(rows), len(inventory.Workloads)) - } out := make(map[string]string) - for i, workload := range inventory.Workloads { - if !slices.Contains(workloads, workload.ID) { + found := false + for _, projection := range checked.Projections() { + if projection.MemberBinding != member { continue } - for name, reference := range rows[i].Images { + found = true + for name, reference := range projection.Row.Images { _, imageDigest, ok := strings.Cut(reference, "@") if !ok { - return nil, fmt.Errorf("container %s/%s has no image digest", workload.ID, name) + return nil, fmt.Errorf("container %s/%s has no image digest", projection.Workload, name) } - out[workload.ID+"/"+name] = imageDigest + out[projection.Workload+"/"+name] = imageDigest } } + if !found { + return nil, fmt.Errorf("it has no member %q", member) + } return out, nil } diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_access_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_access_test.go new file mode 100644 index 000000000..0a1fd9f6d --- /dev/null +++ b/module/services/accounts/code/pkg/business/platform_catalogue_access_test.go @@ -0,0 +1,56 @@ +package business_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "accounts/pkg/business" +) + +// catalogueAccessStore answers the role lookup the gate makes and records +// whether anything went on to read the control plane. Every other method is the +// embedded nil interface, so a path that got past the gate panics rather than +// passing quietly. +type catalogueAccessStore struct { + business.Store + role string + planeRead bool +} + +func (s *catalogueAccessStore) GetPlatformRole(context.Context, string) (string, error) { + return s.role, nil +} + +func (s *catalogueAccessStore) WithControlPlane(ctx context.Context, fn func(context.Context) error) error { + s.planeRead = true + return fn(ctx) +} + +// The Catalogue is gated in the business method as well as in the handler, and +// this proves the business half on its own: the handler is not in the call at +// all. The handler half is covered by TestPlatformAdminHandlersRejectNonAdmin and +// TestPlatformAdminHandlersEnforceMinimumRole in pkg/adapters — which, as those +// tests now say, cannot isolate the handler gate, because both gates read the +// same stored role and return the same code. This test is the layer that can be +// isolated, and it is the one that has to hold if a transport is ever added. +func TestListPlatformCatalogueRequiresSuperAdminInBusiness(t *testing.T) { + const actor = "019f6bf7-5b1c-730d-9687-fe6d4aff31ee" + for _, role := range []string{"", "support", "billing"} { + t.Run("role "+role, func(t *testing.T) { + store := &catalogueAccessStore{role: role} + service, err := business.NewService(store) + require.NoError(t, err) + + catalogue, err := service.ListPlatformCatalogue(context.Background(), actor, false) + + require.Error(t, err) + require.Nil(t, catalogue) + var storeErr *business.StoreError + require.ErrorAs(t, err, &storeErr) + require.Equal(t, business.ErrTypePermission, storeErr.StoreErrorType) + require.False(t, store.planeRead, "the role is refused before any record is read") + }) + } +} diff --git a/module/services/accounts/code/pkg/business/platform_catalogue_test.go b/module/services/accounts/code/pkg/business/platform_catalogue_test.go index d5b91201d..d0a61e88c 100644 --- a/module/services/accounts/code/pkg/business/platform_catalogue_test.go +++ b/module/services/accounts/code/pkg/business/platform_catalogue_test.go @@ -1,6 +1,8 @@ package business import ( + "context" + "encoding/json" "os" "strings" "testing" @@ -9,6 +11,7 @@ import ( deployment "github.com/codefly-dev/cli/contracts/deployment" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" gen "accounts/pkg/gen/saas/accounts/v1" ) @@ -47,17 +50,29 @@ func observedAll(incarnation uint64, edit func(map[string]string)) *gen.Catalogu // never against the declaration, and given only when a complete observation and // a readable authorization both exist: an unobserved or partly observed entry is // never a match, and an entry the host cannot judge is never "unauthorized". +// +// The affirmative verdict carries the extra burden, because it is the one a +// reader acts on by doing nothing: it needs an approval complete enough to judge +// against and an observation whose age is known and current. Every case below +// states the observation's age, including the ones that are a match in every +// other respect and still must not come back green. func TestCatalogueObservedVerdict(t *testing.T) { canonical, digest := executionInventory(t) + now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) + // fresh and stale are ages, not instants: a verdict reads the observation's + // age against the host's validity window, never a fixed date. + fresh := timestamppb.New(now.Add(-time.Minute)) + stale := timestamppb.New(now.Add(-time.Hour)) + ahead := timestamppb.New(now.Add(time.Hour)) inventories := map[string][]byte{digest: canonical} - authorizedAs := func(inventoryDigest, member string) *gen.CatalogueAuthorized { + authorizedAs := func(inventoryDigest, member string, incarnation uint64) *gen.CatalogueAuthorized { return &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ Authorization: &gen.CatalogueAuthorization{ - AuthorizedRevision: 4, InventoryDigest: inventoryDigest, MemberBinding: member, BuildIncarnation: 3, + AuthorizedRevision: 4, InventoryDigest: inventoryDigest, MemberBinding: member, BuildIncarnation: incarnation, }, }} } - authorized := authorizedAs(digest, "member") + authorized := authorizedAs(digest, "member", 3) notAuthorized := &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_NotAuthorized{ NotAuthorized: &gen.CatalogueNotAuthorized{Detail: "approval withdrawn"}, }} @@ -75,55 +90,98 @@ func TestCatalogueObservedVerdict(t *testing.T) { name string authorized *gen.CatalogueAuthorized observed *gen.CatalogueObservedExecution + observedAt *timestamppb.Timestamp inventories map[string][]byte verdict gen.CatalogueObservedVerdict gap gen.CatalogueGapReason + detail string }{ {name: "authorized, nothing observed", authorized: authorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, {name: "not authorized, nothing observed", authorized: notAuthorized, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED}, - {name: "observed, authorization unknown to this host", authorized: unknown, observed: observedAll(3, nil), gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, + {name: "observed, authorization unknown to this host", authorized: unknown, observed: observedAll(3, nil), observedAt: fresh, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, { - name: "every approved container on its approved digest, at the approved incarnation", authorized: authorized, - observed: observedAll(3, nil), verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED, + name: "every approved container on its approved digest, at the approved incarnation, currently observed", authorized: authorized, + observed: observedAll(3, nil), observedAt: fresh, verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED, + }, + { + // The construction that matters: everything matches, and the host + // cannot say when. "Running authorized" beside "Not observed" is a + // green an operator stops at, so there is no verdict at all. + name: "the approved execution exactly, observed at an unknown time", authorized: authorized, + observed: observedAll(3, nil), gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, + detail: "carries no time", + }, + { + name: "the approved execution exactly, observed an hour ago", authorized: authorized, + observed: observedAll(3, nil), observedAt: stale, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, + detail: "past the 5m0s this host treats as current", + }, + { + // A stamp from the future is not evidence about the present either, + // and a window that only looked backwards would accept it. + name: "the approved execution exactly, observed an hour ahead of this host", authorized: authorized, + observed: observedAll(3, nil), observedAt: ahead, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, + detail: "ahead of this host's clock", + }, + { + // This host assigns the incarnation from one, so an approval at zero + // carries none. Left uncompared, an observation that also carries + // none would match it. + name: "an approval carrying no build incarnation", authorized: authorizedAs(digest, "member", 0), + observed: observedAll(0, nil), observedAt: fresh, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, + detail: "no build incarnation", }, { name: "one container on another image", authorized: authorized, - observed: observedAll(3, func(m map[string]string) { m["worker/helper"] = "sha256:" + strings.Repeat("b", 64) }), - verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + observed: observedAll(3, func(m map[string]string) { m["worker/helper"] = "sha256:" + strings.Repeat("b", 64) }), observedAt: fresh, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { name: "a container the inventory does not approve", authorized: authorized, - observed: observedAll(3, func(m map[string]string) { m["worker/sidecar"] = inventoryImage }), - verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + observed: observedAll(3, func(m map[string]string) { m["worker/sidecar"] = inventoryImage }), observedAt: fresh, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { // The incarnation moves when command, configuration or identity // change under the same image, so the images alone do not match. name: "every image approved, another incarnation", authorized: authorized, - observed: observedAll(2, nil), verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + observed: observedAll(2, nil), observedAt: fresh, verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, }, { // A finished init container is still part of what ran. name: "an approved init container missing from the observation", authorized: authorized, - observed: observedAll(3, func(m map[string]string) { delete(m, "worker/seed") }), - gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, + observed: observedAll(3, func(m map[string]string) { delete(m, "worker/seed") }), observedAt: fresh, + gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, }, { // The dangerous row: something runs that nothing currently // authorizes, whatever its image. name: "observed with no current authorization", authorized: notAuthorized, - observed: observedAll(3, nil), verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED, + observed: observedAll(3, nil), observedAt: fresh, verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED, + }, + { + // The asymmetry, stated as a test: an age that withholds the green + // verdict does not withhold the alarming ones. An old report of + // something wrong is still a report of something wrong. + name: "an hour-old observation of another incarnation", authorized: authorized, + observed: observedAll(2, nil), observedAt: stale, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS, + }, + { + name: "an hour-old observation of something nothing authorizes", authorized: notAuthorized, + observed: observedAll(3, nil), observedAt: stale, + verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED, }, - {name: "approved inventory not held", authorized: authorized, observed: observedAll(3, nil), inventories: map[string][]byte{}, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, - {name: "inventory bytes that do not hash to the approved digest", authorized: authorizedAs(wrongDigest, "member"), observed: observedAll(3, nil), inventories: map[string][]byte{wrongDigest: canonical}, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, + {name: "approved inventory not held", authorized: authorized, observed: observedAll(3, nil), observedAt: fresh, inventories: map[string][]byte{}, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, + {name: "inventory bytes that do not hash to the approved digest", authorized: authorizedAs(wrongDigest, "member", 3), observed: observedAll(3, nil), observedAt: fresh, inventories: map[string][]byte{wrongDigest: canonical}, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, { // Its digest is right and its member and images are all there: only the // contract can say this is not an inventory it defines. - name: "an inventory the contract refuses, held under its own digest", authorized: authorizedAs(otherSchemaDigest, "member"), - observed: observedAll(3, nil), inventories: map[string][]byte{otherSchemaDigest: otherSchema}, + name: "an inventory the contract refuses, held under its own digest", authorized: authorizedAs(otherSchemaDigest, "member", 3), + observed: observedAll(3, nil), observedAt: fresh, inventories: map[string][]byte{otherSchemaDigest: otherSchema}, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED, }, - {name: "a member the inventory does not have", authorized: authorizedAs(digest, "someone-else"), observed: observedAll(3, nil), gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, + {name: "a member the inventory does not have", authorized: authorizedAs(digest, "someone-else", 3), observed: observedAll(3, nil), observedAt: fresh, gap: gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_RECORDED}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -131,15 +189,26 @@ func TestCatalogueObservedVerdict(t *testing.T) { if tc.observed != nil { observed.ObservedExecutionValue = &gen.CatalogueObserved_ObservedExecution{ObservedExecution: tc.observed} } + if tc.observedAt != nil { + observed.ObservationFreshnessValue = &gen.CatalogueObserved_ObservedAt{ObservedAt: tc.observedAt} + } else { + observed.ObservationFreshnessValue = &gen.CatalogueObserved_ObservationFreshnessGap{ + ObservationFreshnessGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, "no cluster state"), + } + } held := inventories if tc.inventories != nil { held = tc.inventories } - judgeCatalogueObserved(tc.authorized, observed, held) + judgeCatalogueObserved(tc.authorized, observed, held, now) if tc.gap != gen.CatalogueGapReason_CATALOGUE_GAP_REASON_UNSPECIFIED { require.Equal(t, gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED, observed.GetVerdict()) require.Equal(t, tc.gap, observed.GetVerdictGap().GetReason()) require.NotEmpty(t, observed.GetVerdictGap().GetDetail()) + if tc.detail != "" { + require.Contains(t, observed.GetVerdictGap().GetDetail(), tc.detail, + "the gap must say which evidence was missing, not only that one was") + } return } require.Nil(t, observed.GetVerdictGap(), observed.GetVerdictGap().GetDetail()) @@ -148,6 +217,106 @@ func TestCatalogueObservedVerdict(t *testing.T) { } } +// The emptiness guard in judgeCatalogueObserved cannot be reached through an +// inventory the contract accepts, and this is why: the contract refuses a +// workload with no application container. If that ever relaxes, an approved +// member with no container would equal an observation with none, so this test +// fails here rather than the page turning green there. +func TestExecutionInventoryContractRequiresAContainer(t *testing.T) { + raw, err := os.ReadFile("testdata/execution-inventory-base.json") + require.NoError(t, err) + var doc map[string]any + require.NoError(t, json.Unmarshal(raw, &doc)) + spec := doc["workloads"].([]any)[0].(map[string]any)["template"].(map[string]any)["spec"].(map[string]any) + spec["containers"] = []any{} + spec["initContainers"] = []any{} + empty, err := json.Marshal(doc) + require.NoError(t, err) + + err = deployment.CheckSchema(empty) + require.Error(t, err, "an inventory whose member approves no container must not be a valid inventory") + require.Contains(t, err.Error(), "CONTAINER_REQUIRED") +} + +// The read boundary withholds an affirmative verdict whose evidence is not +// beside it. judgeCatalogueObserved is the only author of a verdict and applies +// the same rule, so nothing should reach this check — but the states are filled +// from records that do not exist yet (applied presence, the signed approval, +// cluster observation), and a filler that set a value case without re-judging +// would otherwise publish a green cell. The boundary fails closed instead. +func TestCatalogueReadBoundaryWithholdsUnsupportedAffirmativeVerdict(t *testing.T) { + now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) + authorized := func(incarnation uint64) *gen.CatalogueAuthorized { + return &gen.CatalogueAuthorized{AuthorizationValue: &gen.CatalogueAuthorized_Authorization{ + Authorization: &gen.CatalogueAuthorization{AuthorizedRevision: 4, InventoryDigest: "sha256:x", MemberBinding: "member", BuildIncarnation: incarnation}, + }} + } + stampFreshness := func(observed *gen.CatalogueObserved, at *timestamppb.Timestamp) { + if at == nil { + observed.ObservationFreshnessValue = &gen.CatalogueObserved_ObservationFreshnessGap{ + ObservationFreshnessGap: catalogueGap(gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, "no cluster state"), + } + return + } + observed.ObservationFreshnessValue = &gen.CatalogueObserved_ObservedAt{ObservedAt: at} + } + for _, tc := range []struct { + name string + authorized *gen.CatalogueAuthorized + execution *gen.CatalogueObservedExecution + observedAt *timestamppb.Timestamp + withheld string + }{ + { + name: "fresh, complete and approved", authorized: authorized(3), execution: observedAll(3, nil), + observedAt: timestamppb.New(now.Add(-time.Minute)), + }, + { + name: "no approval beside it", execution: observedAll(3, nil), + observedAt: timestamppb.New(now.Add(-time.Minute)), withheld: "no approval", + }, + { + name: "an approval with no build incarnation", authorized: authorized(0), execution: observedAll(3, nil), + observedAt: timestamppb.New(now.Add(-time.Minute)), withheld: "no build incarnation", + }, + { + name: "no observed execution beside it", authorized: authorized(3), + observedAt: timestamppb.New(now.Add(-time.Minute)), withheld: "no observed execution", + }, + { + name: "an observation whose age is unknown", authorized: authorized(3), execution: observedAll(3, nil), + withheld: "carries no time", + }, + { + name: "an hour-old observation", authorized: authorized(3), execution: observedAll(3, nil), + observedAt: timestamppb.New(now.Add(-time.Hour)), withheld: "treats as current", + }, + } { + t.Run(tc.name, func(t *testing.T) { + observed := &gen.CatalogueObserved{ + VerdictValue: &gen.CatalogueObserved_Verdict{Verdict: gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED}, + } + stampFreshness(observed, tc.observedAt) + if tc.execution != nil { + observed.ObservedExecutionValue = &gen.CatalogueObserved_ObservedExecution{ObservedExecution: tc.execution} + } + entries := []*PlatformCatalogueEntry{{Entry: &gen.CatalogueEntry{Name: "entry", Authorized: tc.authorized, Observed: observed}}} + + enforceCatalogueVerdictEvidence(context.Background(), entries, now) + + if tc.withheld == "" { + require.Equal(t, gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED, observed.GetVerdict(), + "a verdict its own evidence supports is served as it stands") + require.Nil(t, observed.GetVerdictGap()) + return + } + require.Equal(t, gen.CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED, observed.GetVerdict()) + require.Equal(t, gen.CatalogueGapReason_CATALOGUE_GAP_REASON_NOT_OBSERVED, observed.GetVerdictGap().GetReason()) + require.Contains(t, observed.GetVerdictGap().GetDetail(), tc.withheld) + }) + } +} + func catalogueTestInstallation(id, solution, org, agent string) *CatalogueInstallationRecord { return &CatalogueInstallationRecord{ Installation: &gen.Installation{ @@ -194,7 +363,7 @@ func TestProjectPlatformCatalogue(t *testing.T) { catalogueTestInstallation("i4", "retired-but-installed", "Acme", "example/retired:0.9.0"), } - entries := projectPlatformCatalogue(modules, registrations, installations, false) + entries := projectPlatformCatalogue(modules, registrations, installations, false, now) require.Equal(t, []string{ "CATALOGUE_ENTRY_KIND_MODULE:alpha", "CATALOGUE_ENTRY_KIND_MODULE:zeta", @@ -268,7 +437,7 @@ func TestProjectPlatformCatalogue(t *testing.T) { require.Same(t, registrations[3], retired.Registration, "an installation of a deregistered solution is shown beside its tombstone") require.Len(t, retired.Entry.GetInstallations(), 1) - withTombstones := projectPlatformCatalogue(modules, registrations, installations, true) + withTombstones := projectPlatformCatalogue(modules, registrations, installations, true, now) require.Contains(t, catalogueEntryNames(withTombstones), "CATALOGUE_ENTRY_KIND_SOLUTION:retired") } diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go index 9d9c30263..cafc0fc58 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/platform_admin.pb.go @@ -146,7 +146,10 @@ type CatalogueObservedVerdict int32 const ( CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED CatalogueObservedVerdict = 0 // Every container of the approved inventory member runs its approved image - // digest, no other container runs, and the incarnation is the approved one. + // digest, no other container runs, the incarnation is the approved one, and + // the observation that says so is dated and current. This is the only verdict + // a reader acts on by doing nothing, so it is the only one that requires + // complete, current evidence: see the verdict field below. CatalogueObservedVerdict_CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED CatalogueObservedVerdict = 1 // Something other than the approved execution is running: what cluster // admission refuses. @@ -2241,6 +2244,11 @@ type CatalogueAuthorization struct { // incarnation, which is the same host-assigned value as presented by the // running workload; an incarnation from any other assigner would make that // comparison meaningless, not merely wrong. + // + // Assigned from one, so zero is an approval that carries no incarnation + // rather than one approving incarnation zero. An approval at zero cannot + // produce an affirmative verdict: left uncompared, it would match any + // observation that also carries none. BuildIncarnation uint64 `protobuf:"varint,4,opt,name=build_incarnation,json=buildIncarnation,proto3" json:"build_incarnation,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -2616,6 +2624,19 @@ type CatalogueObserved struct { // authorization do; anything less is a gap, and an unobserved or // incompletely observed entry is never a match. // + // RUNNING_AUTHORIZED is constructible only from an approval complete enough + // to judge against (an inventory this host holds at the digest approval + // signed, and a non-zero build_incarnation) and an observation that is + // complete, carries observed_at, and is within the validity this host treats + // as current. Any of those missing leaves a gap. The two verdicts that report + // trouble carry no such burden: a partly evidenced report of something wrong + // is still worth showing, while "Running authorized" beside "Not observed" is + // worse than nothing, because a reader sees it and stops looking. + // + // The rule is enforced where the verdict is made and again at the read + // boundary that serves it, so a reader of this response never has to re-derive + // it from the fields beside it. + // // Types that are valid to be assigned to VerdictValue: // // *CatalogueObserved_Verdict diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 47d7ab54e..50810c25a 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -6876,7 +6876,7 @@ "buildIncarnation": { "type": "string", "format": "uint64", - "description": "The build incarnation this host assigned to the approved execution. Not an\ninventory field: this host, and only this host, assigns it — advancing it\nwhenever the approved execution changes — and a Work Context seals it beside\nthe image digest. The verdict compares it with an observation's\nincarnation, which is the same host-assigned value as presented by the\nrunning workload; an incarnation from any other assigner would make that\ncomparison meaningless, not merely wrong." + "description": "The build incarnation this host assigned to the approved execution. Not an\ninventory field: this host, and only this host, assigns it — advancing it\nwhenever the approved execution changes — and a Work Context seals it beside\nthe image digest. The verdict compares it with an observation's\nincarnation, which is the same host-assigned value as presented by the\nrunning workload; an incarnation from any other assigner would make that\ncomparison meaningless, not merely wrong.\n\nAssigned from one, so zero is an approval that carries no incarnation\nrather than one approving incarnation zero. An approval at zero cannot\nproduce an affirmative verdict: left uncompared, it would match any\nobservation that also carries none." } }, "description": "CatalogueAuthorization is a current platform approval. What it approves is an\nexecution inventory, `codefly/execution-inventory/v1`, whose one definition is\nthe deployment contract module (github.com/codefly-dev/cli/contracts/deployment).\nThe host does not re-describe that document: it names the approved inventory\nby the digest approval signs, carries the inventory's canonical bytes once per\nresponse (ListPlatformCatalogueResponse.approved_inventories), and reads them\nonly through the contract." @@ -7102,7 +7102,7 @@ "CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED" ], "default": "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", - "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: Every container of the approved inventory member runs its approved image\ndigest, no other container runs, and the incarnation is the approved one.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop." + "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: Every container of the approved inventory member runs its approved image\ndigest, no other container runs, the incarnation is the approved one, and\nthe observation that says so is dated and current. This is the only verdict\na reader acts on by doing nothing, so it is the only one that requires\ncomplete, current evidence: see the verdict field below.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop." }, "v1CatalogueRelease": { "type": "object", diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 7ddbb33bb..a020ff62e 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -855,7 +855,7 @@ "type": "string" }, "buildIncarnation": { - "description": "The build incarnation this host assigned to the approved execution. Not an\ninventory field: this host, and only this host, assigns it — advancing it\nwhenever the approved execution changes — and a Work Context seals it beside\nthe image digest. The verdict compares it with an observation's\nincarnation, which is the same host-assigned value as presented by the\nrunning workload; an incarnation from any other assigner would make that\ncomparison meaningless, not merely wrong.", + "description": "The build incarnation this host assigned to the approved execution. Not an\ninventory field: this host, and only this host, assigns it — advancing it\nwhenever the approved execution changes — and a Work Context seals it beside\nthe image digest. The verdict compares it with an observation's\nincarnation, which is the same host-assigned value as presented by the\nrunning workload; an incarnation from any other assigner would make that\ncomparison meaningless, not merely wrong.\n\nAssigned from one, so zero is an approval that carries no incarnation\nrather than one approving incarnation zero. An approval at zero cannot\nproduce an affirmative verdict: left uncompared, it would match any\nobservation that also carries none.", "format": "uint64", "type": "string" }, @@ -1084,7 +1084,7 @@ }, "v1CatalogueObservedVerdict": { "default": "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", - "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: Every container of the approved inventory member runs its approved image\ndigest, no other container runs, and the incarnation is the approved one.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop.", + "description": "CatalogueObservedVerdict judges what runs against what is authorized —\nnever against what is declared, because only authorization admits execution.\n\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED: Every container of the approved inventory member runs its approved image\ndigest, no other container runs, the incarnation is the approved one, and\nthe observation that says so is dated and current. This is the only verdict\na reader acts on by doing nothing, so it is the only one that requires\ncomplete, current evidence: see the verdict field below.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_DIFFERS: Something other than the approved execution is running: what cluster\nadmission refuses.\n - CATALOGUE_OBSERVED_VERDICT_RUNNING_UNAUTHORIZED: Something is running with no current authorization at all: what the\nretirement sweep exists to stop.", "enum": [ "CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED", "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", diff --git a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto index 96b2a0010..f6eac2488 100644 --- a/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto +++ b/module/services/accounts/proto/saas/accounts/v1/platform_admin.proto @@ -333,6 +333,11 @@ message CatalogueAuthorization { // incarnation, which is the same host-assigned value as presented by the // running workload; an incarnation from any other assigner would make that // comparison meaningless, not merely wrong. + // + // Assigned from one, so zero is an approval that carries no incarnation + // rather than one approving incarnation zero. An approval at zero cannot + // produce an affirmative verdict: left uncompared, it would match any + // observation that also carries none. uint64 build_incarnation = 4; } @@ -377,7 +382,10 @@ message CatalogueObservedExecution { enum CatalogueObservedVerdict { CATALOGUE_OBSERVED_VERDICT_UNSPECIFIED = 0; // Every container of the approved inventory member runs its approved image - // digest, no other container runs, and the incarnation is the approved one. + // digest, no other container runs, the incarnation is the approved one, and + // the observation that says so is dated and current. This is the only verdict + // a reader acts on by doing nothing, so it is the only one that requires + // complete, current evidence: see the verdict field below. CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; // Something other than the approved execution is running: what cluster // admission refuses. @@ -405,6 +413,19 @@ message CatalogueObserved { // A verdict exists only when both a complete observation and a readable // authorization do; anything less is a gap, and an unobserved or // incompletely observed entry is never a match. + // + // RUNNING_AUTHORIZED is constructible only from an approval complete enough + // to judge against (an inventory this host holds at the digest approval + // signed, and a non-zero build_incarnation) and an observation that is + // complete, carries observed_at, and is within the validity this host treats + // as current. Any of those missing leaves a gap. The two verdicts that report + // trouble carry no such burden: a partly evidenced report of something wrong + // is still worth showing, while "Running authorized" beside "Not observed" is + // worse than nothing, because a reader sees it and stops looking. + // + // The rule is enforced where the verdict is made and again at the read + // boundary that serves it, so a reader of this response never has to re-derive + // it from the fields beside it. oneof verdict_value { CatalogueObservedVerdict verdict = 7; CatalogueGap verdict_gap = 8; diff --git a/module/services/frontend/AGENTS.md b/module/services/frontend/AGENTS.md index 603764115..a43f09db1 100644 --- a/module/services/frontend/AGENTS.md +++ b/module/services/frontend/AGENTS.md @@ -84,6 +84,16 @@ activating a remote. column each, and a fact the host has no record of is rendered as its gap reason ("Not recorded", "Not observed"), never as an empty cell, a zero or a match. +- **"Running authorized" is shown only beside the evidence it rests on.** The + server judges the verdict and re-checks it at its read boundary, so a response + that still arrives claiming the approved execution runs with no approval, no + build incarnation, no observed execution or no time of observation is a defect — + and `features/platform-catalogue/model/facts.ts` renders it as a fact this host + cannot state rather than a green cell. A reader sees green and stops looking, + which is why that one cell is checked and the two that report trouble are not. + The browser checks only that each piece of evidence is *present*: how long an + observation speaks for the present is the host's to decide, and a copy of that + window here would drift from it. ## Loading a remote, and its CSP diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json index 729e8e3da..08239314f 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json +++ b/module/services/frontend/code/packages/saas-sdk/generated/contract/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contract.binpb", - "digest": "sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b", + "digest": "sha256:a80e92b319dc442c93eb9604129192c86aaf491e635cb549cd7bcca5681b45ee", "services": [ { "name": "APIKeyService", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb b/module/services/frontend/code/packages/saas-sdk/generated/contract/contract.binpb index 78a3928696ef00a6995d088a65d2e89a6a7bcde0..140c47177d1433f1a53dab9c83befce06532bd23 100644 GIT binary patch delta 12722 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 delta 11440 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
V!48S$AzwwObIB&5sCv^+u+~lDLV6L>CL({6 z7=DL^F3@qa5c?f;VIqIC2=}|lCk;EW#qR(~@VwRU07=+^Z9=6hma90~Aqtn6@A2VI5}>zv=p+I9-VPlBeV=Y+?j&(1mUcjhgATEee;Ne6_#(u8{uV2MgXu(Kj<{5rZcIV==aN&t#4OyaP(h(2U#6MPSgtM0b5 zz}trUR*2J%uQYtW72#>emo)G_;`x#U&qqC9lEC*^yPJ_T&Gopy8A$`z?>ruo!1aXZ zN)ot!@41o$t|vQm1lLpkX4LOb$r&N8w!tk8RdGgyS6O~W!mQ4Snk#LBR+?Q8>%#iH&(C<@Qa=dsgtYZVf;TX_8GjJr6EZ6>NQ2uSJw}q?`Vr|0 z43dEHQRxyiNt7r?$wDu?2qU6zgLKy3L0;u9PDURG;;1NCMR7q#D@X8KBPaP^lH5&TQ9_E;&$rUNUH2Qdri7ZPOtS1 zDVAWZrjjezMEiyemte7G=Z%Q1SR~ce|Dqw$E|y5NS5q(-$EroLPtxJX@MXJ2x_hh` z>uizk9@8(fEpqs+HqRDg4$Gui?RXa(-pgdT+VLh0yx;P?NrLCMJ#Ui0d%09eR!WdY z8RA`8sJF8>B09h4@skGpD?CM#fd74upCsV_z~d(g_*b^?n1D6-w@Oz@Q#0USvKwg^{(|f zMZf#6l~?`7ZbN{%tdnAg+lIhw!#Wx6aN9r{yngDpfh2hT%x{C1I_d96>!qvcf@hB7 z1W;og_8UES(!hO_$43&lZ}!|t0{1PRJ4xWawL?c>-{!GXD`4La?1@b)SgY76#Rz*c z0-LON%J4?Z*GS0KJ7vu;E#EL!yCpm9(u)2LvfJYgv8>%5ul_8u+ox=NT2XrvDZX}m zL&JBE48L}KNrT%q&zB^)-s}021ibrvawbWG-S3k#X#jh`@ktnIAp4~!OA^R_<;ju+ zvIkvH(-CA3X|g6c>#s%MNO7g57+Q+o$Z)u&Xe7+)8(A|f_*)R^dN+#IG0A?A>8$Ug z|4WWZMPCB-12jA)FV5cM_1BbMl=9lyx%J1)cXjvr~@_nqfQ5kA zZ5Bi+KulKRIR~QDfS9bp=Nt&q0Ah*Bmu-! z4}v6snC3x{1Q63ZbOeZJl{;Kmkr*~ZiRP2FKNI#*Wn!uN4N$t@;uL%B=DWDToF$aUC}S351zw!gBO*n+vyoF|HIQF z37B8v!1zWw=zLUBu}I+}9&0wC6Ml+QY-M`)`DjeC)CX}VpjfId7;dR|Lhq&O7k{=v z+zFIhlz87!?qn#psPKJ9nKV#d<|s3g;Q1{NnIu4dyIl~Irh|C74`R|l_Z<(3B+z}= z(upu4)M6_~>sD1p|G_@Rzfv+X2liXVB3q=Dkc9tKIE_=)l#p-2P9wH_l$z__kMPhkAiW5klg zdL^=$Ew>cctMCdpEBh9-Ue#P~DaQCsP_aFU!x&?|8rQ#=R;$>M)W9xBEOs zlHhuOh7qnU#RE$9v7bpvqQv=?!hhzN2sI-59;7Uq&=-0V!S-uUjU=%B#l?qlh8M~2G}KQ` z0~kJAN$`B$!zKwK`$M|~N}7(@AAQV{2Ck3fT{%P&xIUV9+K_i4ixr8=(RuMpn?NgV%#Kcv3j8!QI$v{x4b!Svjm@*C^I6WU!V<^k75!$7 zxU9mo$L70?Kx_S@N)?ag*!&%j<}A4?kQ)VUYw_lRPYv0IRV>N5GowVPO&Csl51= zWmQG2`e-d2VS~+RnD0~hnj3@XO|VtRYFeJnPk*mUW+v0}uJ*1*yJ>k>dsidarsZAj zT@7T@^J1<;R&9_?&xdmzGSYzT*$i0`BMF|L^TbF3u^D+^dy}SvZKmf(8u&f$F^~j) zvpheNz^}>kBMJOwcjyRyb38w+NYu{Di)$>vY8!0x@?rMX$4HpfyfHO}lIEI1>C&11 E57I1I=>Px# diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index 24badf9f0..55f1a5535 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:48fb8502bc03f5aa0f6512106b49f6b37ac06d935389e630b25241874df4d86b + contract-digest: sha256:a80e92b319dc442c93eb9604129192c86aaf491e635cb549cd7bcca5681b45ee services: - APIKeyService - AccessibleScopeService @@ -51,6 +51,6 @@ sources: - WorkContextService as: accounts generated-by: - codefly: 0.1.171 + codefly: 0.1.176 companion: ghcr.io/codefly-dev/proto:0.0.16 facade: true diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts index 1a317518c..6d86e9a4b 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -975,6 +975,11 @@ export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthoriz * running workload; an incarnation from any other assigner would make that * comparison meaningless, not merely wrong. * + * Assigned from one, so zero is an approval that carries no incarnation + * rather than one approving incarnation zero. An approval at zero cannot + * produce an affirmative verdict: left uncompared, it would match any + * observation that also carries none. + * * @generated from field: uint64 build_incarnation = 4; */ buildIncarnation: bigint; @@ -1170,6 +1175,19 @@ export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & * authorization do; anything less is a gap, and an unobserved or * incompletely observed entry is never a match. * + * RUNNING_AUTHORIZED is constructible only from an approval complete enough + * to judge against (an inventory this host holds at the digest approval + * signed, and a non-zero build_incarnation) and an observation that is + * complete, carries observed_at, and is within the validity this host treats + * as current. Any of those missing leaves a gap. The two verdicts that report + * trouble carry no such burden: a partly evidenced report of something wrong + * is still worth showing, while "Running authorized" beside "Not observed" is + * worse than nothing, because a reader sees it and stops looking. + * + * The rule is enforced where the verdict is made and again at the read + * boundary that serves it, so a reader of this response never has to re-derive + * it from the fields beside it. + * * @generated from oneof saas.accounts.v1.CatalogueObserved.verdict_value */ verdictValue: { @@ -1586,7 +1604,10 @@ export enum CatalogueObservedVerdict { /** * Every container of the approved inventory member runs its approved image - * digest, no other container runs, and the incarnation is the approved one. + * digest, no other container runs, the incarnation is the approved one, and + * the observation that says so is dated and current. This is the only verdict + * a reader acts on by doing nothing, so it is the only one that requires + * complete, current evidence: see the verdict field below. * * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; */ diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts index d23fad40f..fca51d4b1 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.test.ts @@ -1,4 +1,5 @@ import { create } from "@bufbuild/protobuf"; +import { timestampFromDate } from "@bufbuild/protobuf/wkt"; import { describe, expect, it } from "vitest"; import { CatalogueEntryKind, @@ -40,7 +41,22 @@ function entry( }); } -function observedWith(verdict: CatalogueObservedVerdict) { +// The evidence an affirmative verdict rests on. A verdict reporting trouble +// needs none of it, which is why each piece is a separate argument here rather +// than baked into one fixture. +const approvedAt = (buildIncarnation = BigInt(3)) => ({ + authorizationValue: { + case: "authorization" as const, + value: { + authorizedRevision: BigInt(4), + inventoryDigest: "sha256:aaa", + memberBinding: "member", + buildIncarnation, + }, + }, +}); + +function observedWith(verdict: CatalogueObservedVerdict, observedAt?: Date) { return { observedExecutionValue: { case: "observedExecution" as const, @@ -49,6 +65,14 @@ function observedWith(verdict: CatalogueObservedVerdict) { buildIncarnation: BigInt(3), }, }, + ...(observedAt + ? { + observationFreshnessValue: { + case: "observedAt" as const, + value: timestampFromDate(observedAt), + }, + } + : {}), verdictValue: { case: "verdict" as const, value: verdict }, }; } @@ -201,11 +225,15 @@ describe("authorization", () => { }); describe("observedVerdictView", () => { - it("states running authorized only when the server judged it so", () => { + it("states running authorized when the server judged it so beside the evidence", () => { expect( observedVerdictView( entry({ - observed: observedWith(CatalogueObservedVerdict.RUNNING_AUTHORIZED), + authorized: approvedAt(), + observed: observedWith( + CatalogueObservedVerdict.RUNNING_AUTHORIZED, + new Date("2026-10-06T12:00:00Z"), + ), }), ), ).toMatchObject({ @@ -215,6 +243,81 @@ describe("observedVerdictView", () => { }); }); + // A page that says "Running authorized" beside "Not observed" is worse than + // one that says nothing, because a reader sees the green and stops. The server + // will not send that combination; if one arrives, it is a defect and must read + // as a fact this host cannot state. + it.each([ + [ + "no approval to be authorized against", + { + observed: observedWith( + CatalogueObservedVerdict.RUNNING_AUTHORIZED, + new Date("2026-10-06T12:00:00Z"), + ), + }, + "no approval", + ], + [ + "an approval carrying no build incarnation", + { + authorized: approvedAt(BigInt(0)), + observed: observedWith( + CatalogueObservedVerdict.RUNNING_AUTHORIZED, + new Date("2026-10-06T12:00:00Z"), + ), + }, + "no build incarnation", + ], + [ + "no time the observation was made", + { + authorized: approvedAt(), + observed: observedWith(CatalogueObservedVerdict.RUNNING_AUTHORIZED), + }, + "without the time the observation was made", + ], + [ + "no observed execution", + { + authorized: approvedAt(), + observed: { + observationFreshnessValue: { + case: "observedAt" as const, + value: timestampFromDate(new Date("2026-10-06T12:00:00Z")), + }, + verdictValue: { + case: "verdict" as const, + value: CatalogueObservedVerdict.RUNNING_AUTHORIZED, + }, + }, + }, + "no observed execution", + ], + ])( + "refuses to show an affirmative verdict with %s", + (_name, init, missing) => { + const view = observedVerdictView(entry(init)); + expect(view.kind).toBe("gap"); + expect(view.kind === "gap" && view.label).toBe("Not observed"); + expect(view.kind === "gap" && view.detail).toContain(missing); + }, + ); + + // The asymmetry: the evidence an affirmative verdict needs is not demanded of + // one that reports trouble, which is still worth showing. + it.each([ + ["Differs from authorized", CatalogueObservedVerdict.RUNNING_DIFFERS], + [ + "Running without authorization", + CatalogueObservedVerdict.RUNNING_UNAUTHORIZED, + ], + ])("still reports %s without that evidence", (text, verdict) => { + expect( + observedVerdictView(entry({ observed: observedWith(verdict) })), + ).toMatchObject({ kind: "value", text, tone: "danger" }); + }); + it("flags an execution that differs from the approved one", () => { const view = observedVerdictView( entry({ diff --git a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts index 23d238b8c..105b4f0e6 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts +++ b/module/services/frontend/code/src/features/platform-catalogue/model/facts.ts @@ -201,10 +201,46 @@ export function authorizationView(entry: CatalogueEntry): FactView { return fact(v, () => UNSET); } +/** + * Why an affirmative verdict may not be shown, or undefined when it may. + * + * "Running authorized" is the one cell a reader acts on by doing nothing, so the + * page shows it only beside the evidence it rests on: an approval complete enough + * to judge against, an observed execution, and the time that observation was + * made. The server applies the same rule where the verdict is made and again at + * its read boundary, so a response that arrives contradicting itself is a defect + * — and a defect must read as a fact this host cannot state, never as green. + * + * It is the presence of each piece of evidence that is checked here, never its + * age: how long an observation speaks for the present is the host's to decide, + * and a second copy of that window in a browser would drift from it. The + * observation's own time is shown beside the verdict, so an operator reads the + * age for themselves. + */ +function affirmativeVerdictUnsupported( + entry: CatalogueEntry, +): string | undefined { + const unsupported = (missing: string) => + `The server reported the approved execution running, ${missing}. Without that evidence this host cannot say what runs, so it is not shown as a match.`; + const authorization = entry.authorized?.authorizationValue; + if (authorization?.case !== "authorization") + return unsupported("with no approval beside it to be authorized against"); + if (authorization.value.buildIncarnation === BigInt(0)) + return unsupported("against an approval that carries no build incarnation"); + const observed = entry.observed; + if (observed?.observedExecutionValue.case !== "observedExecution") + return unsupported("with no observed execution beside it"); + if (observed.observationFreshnessValue.case !== "observedAt") + return unsupported("without the time the observation was made"); + return undefined; +} + /** * What runs, judged against what is authorized. Both failures carry the danger * tone: a different execution is what admission refuses, and an execution with - * no current authorization is what the retirement sweep exists to stop. + * no current authorization is what the retirement sweep exists to stop. Neither + * is withheld for want of the evidence an affirmative verdict needs: a partly + * evidenced report of trouble is still a report of trouble. */ export function observedVerdictView(entry: CatalogueEntry): FactView { const observed = entry.observed; @@ -216,13 +252,21 @@ export function observedVerdictView(entry: CatalogueEntry): FactView { : undefined; const detail = observationText(running); switch (v.value) { - case CatalogueObservedVerdict.RUNNING_AUTHORIZED: + case CatalogueObservedVerdict.RUNNING_AUTHORIZED: { + const unsupported = affirmativeVerdictUnsupported(entry); + if (unsupported) + return { + kind: "gap", + label: gapLabel(CatalogueGapReason.NOT_OBSERVED), + detail: unsupported, + }; return { kind: "value", text: "Running authorized", tone: "success", detail, }; + } case CatalogueObservedVerdict.RUNNING_DIFFERS: return { kind: "value", diff --git a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx index 0138dcfbb..25c556b40 100644 --- a/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx +++ b/module/services/frontend/code/src/features/platform-catalogue/ui/platform-catalogue-page.container.test.tsx @@ -44,10 +44,19 @@ const unknownStates = { buildSizeGap: notRecorded("Build size is codefly-dev/core#708."), }; -function observedRunning(verdict: string, imageDigest: string) { +// observedAt is what dates an observation. A verdict that reports trouble is +// shown without it; the affirmative one is not, so each caller below states +// whether its observation carries a time. +function observedRunning( + verdict: string, + imageDigest: string, + observedAt?: string, +) { return { observedRevisionGap: notObserved("No cluster state."), - observationFreshnessGap: notObserved("No cluster state."), + ...(observedAt + ? { observedAt } + : { observationFreshnessGap: notObserved("No cluster state.") }), observedExecution: { containerImageDigests: { "worker/worker": imageDigest }, buildIncarnation: "3", @@ -128,6 +137,20 @@ const catalogue = { ), }), solution("steady-solution", { + authorized: { + authorization: approved, + }, + observed: observedRunning( + "CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED", + "sha256:aaa", + "2026-10-06T11:59:00Z", + ), + }), + // A response that contradicts itself: the approved execution reported + // running, and no time the observation was made. The server will not send + // this — it withholds the verdict at its read boundary — so if one arrives + // it is a defect, and the page must not turn it into green. + solution("undated-solution", { authorized: { authorization: approved, }, @@ -197,6 +220,14 @@ describe("PlatformCataloguePage admin container", () => { ), ).toBeTruthy(); + // "Running authorized" beside an undated observation is the one cell an + // operator reads and stops at, so it is never shown. + const undated = stateOf("undated-solution", "Observed"); + expect(within(undated).queryByText("Running authorized")).toBeNull(); + expect(within(undated).getAllByText("Not observed").length).toBeGreaterThan( + 0, + ); + const moduleObserved = stateOf("billing-module", "Observed"); expect( within(moduleObserved).getAllByText("Not observed").length, diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts index ebe6a298c..04e4bc7e8 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/platform_admin_pb.ts @@ -975,6 +975,11 @@ export type CatalogueAuthorization = Message<"saas.accounts.v1.CatalogueAuthoriz * running workload; an incarnation from any other assigner would make that * comparison meaningless, not merely wrong. * + * Assigned from one, so zero is an approval that carries no incarnation + * rather than one approving incarnation zero. An approval at zero cannot + * produce an affirmative verdict: left uncompared, it would match any + * observation that also carries none. + * * @generated from field: uint64 build_incarnation = 4; */ buildIncarnation: bigint; @@ -1170,6 +1175,19 @@ export type CatalogueObserved = Message<"saas.accounts.v1.CatalogueObserved"> & * authorization do; anything less is a gap, and an unobserved or * incompletely observed entry is never a match. * + * RUNNING_AUTHORIZED is constructible only from an approval complete enough + * to judge against (an inventory this host holds at the digest approval + * signed, and a non-zero build_incarnation) and an observation that is + * complete, carries observed_at, and is within the validity this host treats + * as current. Any of those missing leaves a gap. The two verdicts that report + * trouble carry no such burden: a partly evidenced report of something wrong + * is still worth showing, while "Running authorized" beside "Not observed" is + * worse than nothing, because a reader sees it and stops looking. + * + * The rule is enforced where the verdict is made and again at the read + * boundary that serves it, so a reader of this response never has to re-derive + * it from the fields beside it. + * * @generated from oneof saas.accounts.v1.CatalogueObserved.verdict_value */ verdictValue: { @@ -1586,7 +1604,10 @@ export enum CatalogueObservedVerdict { /** * Every container of the approved inventory member runs its approved image - * digest, no other container runs, and the incarnation is the approved one. + * digest, no other container runs, the incarnation is the approved one, and + * the observation that says so is dated and current. This is the only verdict + * a reader acts on by doing nothing, so it is the only one that requires + * complete, current evidence: see the verdict field below. * * @generated from enum value: CATALOGUE_OBSERVED_VERDICT_RUNNING_AUTHORIZED = 1; */ From ded97b38b46f5be9e314729f7e79ce67342e5adc Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:25:56 -0400 Subject: [PATCH 09/11] chore(scout): sharp override 0.35.4 -> 0.35.5 in frontend and marketing (CVE-2026-96889) The first-party vulnerability gate fails on main, not on this PR: the PR touches no package.json or lockfile, and origin/main's own trees fail the same `npm audit --omit=dev --audit-level=high` with two HIGH findings per tree -- sharp <0.35.5 (librsvg, CVE-2026-96889, GHSA-wq5f-xc86-pv6w) and next, which is only in range through sharp. sharp is an `overrides` entry, placed by #526 for exactly this kind of advisory, so this continues that pin's intent rather than removing it: 0.35.4 -> 0.35.5, lockfiles regenerated. Both trees audit CLEAN at high after the bump. Left alone, and said so rather than hidden: the frontend tree still carries 4 MODERATE findings (sprintf-js via argparse via remarkable via swagger-ui-react), below the gate's threshold and fixable only by npm's `--force` downgrade of swagger-ui-react to 3.23.3, which is a breaking change this PR has no business making. No ignore entry, no allowlist, no threshold change. Co-Authored-By: Claude Fable 5.1 --- .../services/frontend/code/package-lock.json | 236 +++++++++--------- module/services/frontend/code/package.json | 2 +- .../services/marketing/code/package-lock.json | 236 +++++++++--------- module/services/marketing/code/package.json | 2 +- 4 files changed, 238 insertions(+), 238 deletions(-) diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index 1ac7c323a..30b842a3e 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -1789,9 +1789,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -1807,13 +1807,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -1829,20 +1829,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -1852,9 +1852,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -1868,9 +1868,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -1884,9 +1884,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -1903,9 +1903,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -1922,9 +1922,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -1941,9 +1941,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -1960,9 +1960,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -1979,9 +1979,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -1998,9 +1998,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -2017,9 +2017,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -2036,9 +2036,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -2057,13 +2057,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -2082,13 +2082,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -2107,13 +2107,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -2132,13 +2132,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -2157,13 +2157,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -2182,13 +2182,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -2207,13 +2207,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -2232,13 +2232,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -2252,16 +2252,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -2271,9 +2271,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -2290,9 +2290,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -2309,9 +2309,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -13102,9 +13102,9 @@ } }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -13119,31 +13119,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { diff --git a/module/services/frontend/code/package.json b/module/services/frontend/code/package.json index 20129fa23..49a29606f 100644 --- a/module/services/frontend/code/package.json +++ b/module/services/frontend/code/package.json @@ -103,6 +103,6 @@ "js-yaml": "$js-yaml", "nanoid@>=3.0.0 <3.3.18": "3.3.18", "postcss": "8.5.26", - "sharp": "0.35.4" + "sharp": "0.35.5" } } diff --git a/module/services/marketing/code/package-lock.json b/module/services/marketing/code/package-lock.json index bde1e3e98..2bc7701a4 100644 --- a/module/services/marketing/code/package-lock.json +++ b/module/services/marketing/code/package-lock.json @@ -485,9 +485,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", - "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -503,13 +503,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.3" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", - "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -525,20 +525,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.3" + "@img/sharp-libvips-darwin-x64": "1.3.4" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", - "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -548,9 +548,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", - "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -564,9 +564,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", - "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -580,9 +580,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", - "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -599,9 +599,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", - "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -618,9 +618,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", - "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -637,9 +637,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", - "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -656,9 +656,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", - "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -675,9 +675,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", - "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -694,9 +694,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", - "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -713,9 +713,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", - "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -732,9 +732,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", - "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -753,13 +753,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.3" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", - "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -778,13 +778,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.3" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", - "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -803,13 +803,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.3" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", - "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -828,13 +828,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.3" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", - "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -853,13 +853,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.3" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", - "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -878,13 +878,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.3" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", - "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -903,13 +903,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", - "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -928,13 +928,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", - "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -948,16 +948,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", - "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.4" + "@img/sharp-wasm32": "0.35.5" }, "engines": { "node": ">=20.9.0" @@ -967,9 +967,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", - "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -986,9 +986,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", - "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1005,9 +1005,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", - "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1466,9 +1466,9 @@ } }, "node_modules/sharp": { - "version": "0.35.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", - "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1483,31 +1483,31 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.4", - "@img/sharp-darwin-x64": "0.35.4", - "@img/sharp-freebsd-wasm32": "0.35.4", - "@img/sharp-libvips-darwin-arm64": "1.3.3", - "@img/sharp-libvips-darwin-x64": "1.3.3", - "@img/sharp-libvips-linux-arm": "1.3.3", - "@img/sharp-libvips-linux-arm64": "1.3.3", - "@img/sharp-libvips-linux-ppc64": "1.3.3", - "@img/sharp-libvips-linux-riscv64": "1.3.3", - "@img/sharp-libvips-linux-s390x": "1.3.3", - "@img/sharp-libvips-linux-x64": "1.3.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", - "@img/sharp-libvips-linuxmusl-x64": "1.3.3", - "@img/sharp-linux-arm": "0.35.4", - "@img/sharp-linux-arm64": "0.35.4", - "@img/sharp-linux-ppc64": "0.35.4", - "@img/sharp-linux-riscv64": "0.35.4", - "@img/sharp-linux-s390x": "0.35.4", - "@img/sharp-linux-x64": "0.35.4", - "@img/sharp-linuxmusl-arm64": "0.35.4", - "@img/sharp-linuxmusl-x64": "0.35.4", - "@img/sharp-webcontainers-wasm32": "0.35.4", - "@img/sharp-win32-arm64": "0.35.4", - "@img/sharp-win32-ia32": "0.35.4", - "@img/sharp-win32-x64": "0.35.4" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" }, "peerDependenciesMeta": { "@types/node": { diff --git a/module/services/marketing/code/package.json b/module/services/marketing/code/package.json index 430993777..c15dbbc38 100644 --- a/module/services/marketing/code/package.json +++ b/module/services/marketing/code/package.json @@ -36,6 +36,6 @@ "fast-uri@>=3.0.0 <3.1.8": "3.1.8", "nanoid@>=3.0.0 <3.3.18": "3.3.18", "postcss": "8.5.26", - "sharp": "0.35.4" + "sharp": "0.35.5" } } From f6a0a54795d8b0237c733a1d44d9803d0f3317c5 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 11:50:54 -0400 Subject: [PATCH 10/11] Re-pin the deployment contract to cli#902's merge commit (Refs #1020) c65184679fda was the pull request's head. codefly-dev/cli#902 merged by squash as 5824cf5342fc, so that SHA is on no branch of cli: it resolved here only because it was still in this machine's module cache, and CI would have failed to download it. The pin moves to the merge commit, v0.0.0-20261006151622-5824cf5342fc. The two module trees are byte-identical (diff -r over both extracted versions is empty), so the only change is the version string and its go.sum lines; the h1 differs because the version path is part of the dirhash input, not because any file did. Nothing regenerates, and the Projections read is unchanged. Re-verified on the new pin: build, vet, gofmt, 44 accounts packages under -tags pure, and the five guards each killed by its own mutation - the freshness check in judgeCatalogueObserved, the approval-incarnation guard, the forward half of the validity window, the read boundary's freshness check, and the business role gate. codefly audit service accounts is CLEAN. Co-Authored-By: Claude Opus 5 (1M context) --- module/services/accounts/code/go.mod | 2 +- module/services/accounts/code/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/module/services/accounts/code/go.mod b/module/services/accounts/code/go.mod index 6a8b71cb9..1baf6311e 100644 --- a/module/services/accounts/code/go.mod +++ b/module/services/accounts/code/go.mod @@ -7,7 +7,7 @@ require ( buf.build/go/protovalidate v1.4.0 connectrpc.com/connect v1.21.0 connectrpc.com/otelconnect v0.9.0 - github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006145225-c65184679fda + github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683 github.com/codefly-dev/interface-cache/go/cache v0.2.2-0.20260926205419-327a6e1d9aba github.com/codefly-dev/sdk-go v0.2.1-0.20260926212413-b91ce91eb23e diff --git a/module/services/accounts/code/go.sum b/module/services/accounts/code/go.sum index 38f83ae62..8addb8a9b 100644 --- a/module/services/accounts/code/go.sum +++ b/module/services/accounts/code/go.sum @@ -58,8 +58,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006145225-c65184679fda h1:UI416Gg1LEPXZuzl04wVZWxU6Uo2pJsoqjECM/3WSmE= -github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006145225-c65184679fda/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= +github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc h1:olDnVJ1A1Retn0z9A59YMwDRmDLgkV2y1lbl+HzNrj8= +github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683 h1:/vzymsu5B9NWwdhWRtlU+68VcVEVDmeZNChRCXAUwlE= github.com/codefly-dev/core v0.5.11-0.20260926231217-d481a2d49683/go.mod h1:SVF+t040X+9pAXAqBnNE/JkXwmw9peVuKnHKFSOzyzk= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= From c83ec88c9c15f31711781f5b74921a777ebea8ee Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:01:18 -0400 Subject: [PATCH 11/11] chore(scout): the event-transport conformance lease outlives the test's own tail CI at f6a0a547 failed 2 of 3669 accounts tests on TestPostgresEventTransportConformance/LeaseExpiry with "events: lease lost" at the final Ack (eventstest/suite.go:184). The PR touches nothing in the event transport or its suite, main passes it, and the same commit passed on rerun -- a timing flake, which the scout rule says to fix rather than re-roll. The race is the harness's own: LeaseExpiry sleeps 1.5 x lease + 100ms to prove expiry, reclaims the row under a FRESH lease, then Acks -- and with a 300ms lease the claim-then-ack tail outlived the lease it had just taken on a loaded runner, so the Ack was refused against the test's own fresh lease. The expiry sleep is proportional, so a longer lease still proves expiry; it only stops proving it against the test's tail. 300ms is this harness's value alone -- every production path leases for a minute (postgres_job_operations_test.go, postgres_jobs_execution_test.go, story_jobs_test.go). Lease 300ms -> 2s. Verified locally against a codefly-provisioned Postgres: -count=3 of the full conformance suite, ok (13.8s). Co-Authored-By: Claude Fable 5.1 --- .../accounts/code/pkg/infra/postgres_events_test.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/module/services/accounts/code/pkg/infra/postgres_events_test.go b/module/services/accounts/code/pkg/infra/postgres_events_test.go index b9f5ac841..3cdd46cc8 100644 --- a/module/services/accounts/code/pkg/infra/postgres_events_test.go +++ b/module/services/accounts/code/pkg/infra/postgres_events_test.go @@ -45,7 +45,15 @@ func seedSubscriptions(t *testing.T, subscriptions []events.Subscription) { } func TestPostgresEventTransportConformance(t *testing.T) { - const lease = 300 * time.Millisecond + // 300ms was enough to prove expiry and not enough to survive the steps + // after it: LeaseExpiry sleeps past expiry, reclaims the row under a FRESH + // lease, then Acks it -- and on a loaded runner the claim-then-ack tail + // outlived 300ms, so the Ack hit "events: lease lost" against a lease the + // test itself had just taken. The expiry sleep is proportional + // (1.5 x lease + 100ms), so a longer lease still proves expiry; it only + // stops proving it against the test's own tail. Production leases are a + // minute (see the job tests); this value is this harness's alone. + const lease = 2 * time.Second pool, err := infra.NewJobWorkerPool(testCtx) require.NoError(t, err) t.Cleanup(pool.Close)