From 0fcb5bf7b8a76a0f14133929f911304f81c25d33 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:13:27 -0400 Subject: [PATCH 1/4] fix(frontend): a module page with roles installs through the review, like a solution Admin -> Modules & solutions sent every module target to the agent-role form, so a module page that declares installer/editor/viewer roles and installation_role none asked for an organisation role and was refused 403 "the delivered module declares no standing machine authority". No wire path could install it: InstallSolution's role_id was a required uuid, so leaving the role out was refused 400 instead. The Install form now follows the authority's answer, not the target's kind (useRolesDeclared, the signal the Setup tab already uses). A module page whose offer names roles opens a review read from that offer (its machine access and what each role permits), is installed through InstallSolution with no agent role, and lands on its Setup page, where each role takes a team. A module the authority refuses (no roles) keeps the agent-role form. The shared authority core installs only solutions (Install refuses a module; its ReviewOffer for a module page carries no consent tuple), so a module page stays this host's installation rather than going through InstallReviewedSolution. accounts: InstallSolutionRequest.role_id may be empty; the host's prepareInstallation already selects a declared installation_role or none, and now refuses an empty role for a target that declares neither. Regenerated with codefly generate proto (CLI 0.1.221) and go generate. The solution and module page reviews share one DeclaredAccess rendering. A long role label no longer widens the agent-role form past the dialog: the form and trigger may shrink and the label truncates, with its full text as the trigger's title. Co-Authored-By: Claude Fable 5.1 --- .../install_solution_validation_test.go | 22 +++ .../pkg/business/installation_dynamic_test.go | 2 +- .../pkg/business/installation_registry.go | 5 + .../module_installation_selection_test.go | 1 + .../gen/saas/accounts/v1/installations.pb.go | 11 +- .../generated/openapi-raw/api.swagger.json | 2 +- .../accounts/openapi/api.swagger.json | 2 +- .../saas/accounts/v1/installations.proto | 10 +- module/services/frontend/AGENTS.md | 10 +- .../ui/declared-access.tsx | 71 ++++++++ .../ui/install-module-dialog.tsx | 22 ++- .../ui/install-module-page-dialog.tsx | 134 +++++++++++++++ .../ui/install-solution-dialog.tsx | 56 +------ ...tion-installations-page.container.test.tsx | 155 +++++++++++++++++- .../ui/solution-installations-page.tsx | 122 ++++++++++---- .../gen/saas/accounts/v1/installations_pb.ts | 7 +- .../services/frontend/code/ui-inventory.json | 34 ++++ 17 files changed, 568 insertions(+), 98 deletions(-) create mode 100644 module/services/accounts/code/pkg/adapters/install_solution_validation_test.go create mode 100644 module/services/frontend/code/src/features/solution-installations/ui/declared-access.tsx create mode 100644 module/services/frontend/code/src/features/solution-installations/ui/install-module-page-dialog.tsx diff --git a/module/services/accounts/code/pkg/adapters/install_solution_validation_test.go b/module/services/accounts/code/pkg/adapters/install_solution_validation_test.go new file mode 100644 index 000000000..4bf679456 --- /dev/null +++ b/module/services/accounts/code/pkg/adapters/install_solution_validation_test.go @@ -0,0 +1,22 @@ +package adapters + +import ( + "testing" + + gen "accounts/pkg/gen/saas/accounts/v1" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +// A module that declares its own installation role is installed with no +// caller role: the host selects a named one, and "none" grants none. A role +// that is given must still be a role id. +func TestInstallSolutionRequestRoleIsOptionalButWellFormed(t *testing.T) { + request := func(role string) *gen.InstallSolutionRequest { + return &gen.InstallSolutionRequest{OrgId: uuid.NewString(), AgentIdentifier: "publisher/name:1.0.0", TargetId: uuid.NewString(), RoleId: role} + } + require.NoError(t, Validate(request("")), "a declared installation role leaves the caller's role out") + require.NoError(t, Validate(request(uuid.NewString()))) + require.Error(t, Validate(request("viewer")), "a given role is a role id") +} diff --git a/module/services/accounts/code/pkg/business/installation_dynamic_test.go b/module/services/accounts/code/pkg/business/installation_dynamic_test.go index dd464e80b..4ef2cd036 100644 --- a/module/services/accounts/code/pkg/business/installation_dynamic_test.go +++ b/module/services/accounts/code/pkg/business/installation_dynamic_test.go @@ -208,7 +208,7 @@ func TestDynamicInstallationTenancyFiltersBeforePaginationAndInstall(t *testing. require.Empty(t, next) _, err = s.prepareInstallation(context.Background(), &InstallSolutionParams{OrgID: "org-a", TargetID: "1"}) require.Equal(t, codes.PermissionDenied, status.Code(err)) - _, err = s.prepareInstallation(context.Background(), &InstallSolutionParams{OrgID: "org-b", TargetID: "1"}) + _, err = s.prepareInstallation(context.Background(), &InstallSolutionParams{OrgID: "org-b", TargetID: "1", RoleID: "role-id"}) require.NoError(t, err) } diff --git a/module/services/accounts/code/pkg/business/installation_registry.go b/module/services/accounts/code/pkg/business/installation_registry.go index 9cb0e7cd4..58d548fb7 100644 --- a/module/services/accounts/code/pkg/business/installation_registry.go +++ b/module/services/accounts/code/pkg/business/installation_registry.go @@ -93,6 +93,11 @@ func (s *Service) prepareInstallation(ctx context.Context, params *InstallSoluti } params.RoleID = role } + // Only the delivered declaration may leave the caller's role out: a named + // installation_role is selected above, and "none" grants no standing role. + if !params.NoStandingAuthority && params.RoleID == "" { + return nil, NewStoreError(fmt.Errorf("role_id is required: %s declares no installation role for this host to select", target.RouteAlias), ErrTypeValidation) + } if params.Kind == "module" && target.Installed && params.OwnerPrincipalID == "" { existing, err := s.heldTargetInstallation(ctx, params.OrgID, params.TargetID) if err != nil { diff --git a/module/services/accounts/code/pkg/business/module_installation_selection_test.go b/module/services/accounts/code/pkg/business/module_installation_selection_test.go index 067fa5a76..9943918e7 100644 --- a/module/services/accounts/code/pkg/business/module_installation_selection_test.go +++ b/module/services/accounts/code/pkg/business/module_installation_selection_test.go @@ -24,6 +24,7 @@ func TestModuleNoStandingSelectionRequiresCurrentPairedDeclaration(t *testing.T) {"caller literal none refused", "none", "none", false, true, false}, {"old generation", "none", "", true, true, false}, {"legacy stays explicit", "", "role-id", false, false, false}, + {"legacy without a role refused", "", "", false, true, false}, {"named module cannot become catalogue role", "reader", "catalogue-role-id", false, true, false}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go index b3216be77..747a170f8 100644 --- a/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go +++ b/module/services/accounts/code/pkg/gen/saas/accounts/v1/installations.pb.go @@ -566,7 +566,10 @@ type InstallSolutionRequest struct { RootScopeLabel string `protobuf:"bytes,6,opt,name=root_scope_label,json=rootScopeLabel,proto3" json:"root_scope_label,omitempty"` // The least-privilege role granted to the agent at the root node. This is // deliberately NOT the full ":catalog" role; widening is a separate, - // audited admin action. + // audited admin action. Empty when the delivered module declares its own + // installation role: the host selects a named one, and "none" grants the + // agent no standing role at all. A role given for "none" is refused, and an + // empty one is refused for a target that declares no installation role. RoleId string `protobuf:"bytes,7,opt,name=role_id,json=roleId,proto3" json:"role_id,omitempty"` // The accountable human of record. Must be a current org admin. Empty defaults // to the calling admin. @@ -2650,14 +2653,14 @@ const file_saas_accounts_v1_installations_proto_rawDesc = "" + "\vsetup_state\x18\x0f \x01(\tR\n" + "setupState\x12#\n" + "\rsetup_message\x18\x10 \x01(\tR\fsetupMessageB\r\n" + - "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xb9\x05\n" + + "\v_revoked_atJ\x04\b\x04\x10\x05R\x13solution_identifier\"\xbc\x05\n" + "\x16InstallSolutionRequest\x12\x1f\n" + "\x06org_id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x05orgId\x125\n" + "\x10agent_identifier\x18\x02 \x01(\tB\n" + "\xbaH\ar\x05\x10\x01\x18\x80\x04R\x0fagentIdentifier\x12+\n" + "\fdisplay_name\x18\x04 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\vdisplayName\x122\n" + - "\x10root_scope_label\x18\x06 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\x0erootScopeLabel\x12!\n" + - "\arole_id\x18\a \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06roleId\x129\n" + + "\x10root_scope_label\x18\x06 \x01(\tB\b\xbaH\x05r\x03\x18\x80\x04R\x0erootScopeLabel\x12$\n" + + "\arole_id\x18\a \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x06roleId\x129\n" + "\x12owner_principal_id\x18\b \x01(\tB\v\xbaH\b\xd8\x01\x01r\x03\xb0\x01\x01R\x10ownerPrincipalId\x12D\n" + "\x16co_owner_principal_ids\x18\t \x03(\tB\x0f\xbaH\f\x92\x01\t\x10 \"\x05r\x03\xb0\x01\x01R\x13coOwnerPrincipalIds\x12<\n" + "\x11allowed_audiences\x18\n" + diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 0e8987987..115c508ec 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -9341,7 +9341,7 @@ }, "roleId": { "type": "string", - "description": "The least-privilege role granted to the agent at the root node. This is\ndeliberately NOT the full \"\u003cnamespace\u003e:catalog\" role; widening is a separate,\naudited admin action." + "description": "The least-privilege role granted to the agent at the root node. This is\ndeliberately NOT the full \"\u003cnamespace\u003e:catalog\" role; widening is a separate,\naudited admin action. Empty when the delivered module declares its own\ninstallation role: the host selects a named one, and \"none\" grants the\nagent no standing role at all. A role given for \"none\" is refused, and an\nempty one is refused for a target that declares no installation role." }, "ownerPrincipalId": { "type": "string", diff --git a/module/services/accounts/openapi/api.swagger.json b/module/services/accounts/openapi/api.swagger.json index 404a52a3e..7450d6bd1 100644 --- a/module/services/accounts/openapi/api.swagger.json +++ b/module/services/accounts/openapi/api.swagger.json @@ -2552,7 +2552,7 @@ "type": "string" }, "roleId": { - "description": "The least-privilege role granted to the agent at the root node. This is\ndeliberately NOT the full \":catalog\" role; widening is a separate,\naudited admin action.", + "description": "The least-privilege role granted to the agent at the root node. This is\ndeliberately NOT the full \":catalog\" role; widening is a separate,\naudited admin action. Empty when the delivered module declares its own\ninstallation role: the host selects a named one, and \"none\" grants the\nagent no standing role at all. A role given for \"none\" is refused, and an\nempty one is refused for a target that declares no installation role.", "type": "string" }, "rootScopeLabel": { diff --git a/module/services/accounts/proto/saas/accounts/v1/installations.proto b/module/services/accounts/proto/saas/accounts/v1/installations.proto index 61af98331..44d793e14 100644 --- a/module/services/accounts/proto/saas/accounts/v1/installations.proto +++ b/module/services/accounts/proto/saas/accounts/v1/installations.proto @@ -133,8 +133,14 @@ message InstallSolutionRequest { string root_scope_label = 6 [(buf.validate.field).string.max_len = 512]; // The least-privilege role granted to the agent at the root node. This is // deliberately NOT the full ":catalog" role; widening is a separate, - // audited admin action. - string role_id = 7 [(buf.validate.field).string.uuid = true]; + // audited admin action. Empty when the delivered module declares its own + // installation role: the host selects a named one, and "none" grants the + // agent no standing role at all. A role given for "none" is refused, and an + // empty one is refused for a target that declares no installation role. + string role_id = 7 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; // The accountable human of record. Must be a current org admin. Empty defaults // to the calling admin. string owner_principal_id = 8 [ diff --git a/module/services/frontend/AGENTS.md b/module/services/frontend/AGENTS.md index c05928533..9e772afd5 100644 --- a/module/services/frontend/AGENTS.md +++ b/module/services/frontend/AGENTS.md @@ -463,7 +463,15 @@ not replace the cached operation; retries retain it, and Review again explicitly requests new consent. Pending installation rows offer current-person continuation. Supporting modules remain visible on `/admin/modules` and in installation detail. That administrator path remains available, but is not a step in normal -solution installation. +solution installation. There, which form Install opens is the authority's +answer, not the target's kind (`useRolesDeclared`): a module page whose offer +names roles is reviewed from that offer (its machine access and what each role +permits), installed through `InstallSolution` with no agent role, because its +signed `installation_role` selects one (or none), and then opened at its Setup +page, where each role takes a team. The shared core installs only solutions, so +a module page stays this host's installation and carries no consent tuple. A +module whose offer the authority refuses (it declares no roles) keeps the +agent-role form. The only installer configuration is a team for each role the solution's offer declares: an existing organization team, or one created inline through diff --git a/module/services/frontend/code/src/features/solution-installations/ui/declared-access.tsx b/module/services/frontend/code/src/features/solution-installations/ui/declared-access.tsx new file mode 100644 index 000000000..7c470efde --- /dev/null +++ b/module/services/frontend/code/src/features/solution-installations/ui/declared-access.tsx @@ -0,0 +1,71 @@ +import type { ReviewedOffer } from "@/gen/host/installation/v1/installation_pb"; +import { offerScopeText } from "../model/install-review"; + +/** + * The machine access an installation receives, as the authority's offer + * declares it: its signed installation role, and the most each declared call + * may ask. One rendering for a solution's review and a module page's, so the + * two reviews cannot describe the same declaration differently. + */ +export function DeclaredAccess({ + subject, + installationRole, + offer, +}: { + subject: "solution" | "module"; + installationRole: string; + offer: Pick; +}) { + const machineRole = offer.roles.find( + (role) => role.name === installationRole, + ); + const heading = subject === "solution" ? "Solution access" : "Module access"; + return ( +
+

{heading}

+ {installationRole === "none" ? ( +

+ This {subject} receives no standing machine access. + {subject === "solution" && + " Setup receives only temporary permission to complete this installation."} +

+ ) : !installationRole ? ( +

+ This {subject} declares no installation access. The host refuses to + install it until its owner declares one. +

+ ) : ( + <> +

+ The {subject} declares its installation access as “ + {machineRole?.name ?? installationRole}”. +

+
    + {machineRole?.scopes.map(offerScopeText).map((text) => ( +
  • {text}
  • + ))} +
+ {machineRole?.scopes.length === 0 && ( +

This declared role grants no data access.

+ )} + + )} + {offer.ceiling.length > 0 && ( + <> +

+ Declared operations, still subject to the caller’s assigned access: +

+
    + {offer.ceiling.map((binding) => ( +
  • + {binding.module}:{" "} + {binding.scopes.map(offerScopeText).join("; ") || + "no data access"} +
  • + ))} +
+ + )} +
+ ); +} diff --git a/module/services/frontend/code/src/features/solution-installations/ui/install-module-dialog.tsx b/module/services/frontend/code/src/features/solution-installations/ui/install-module-dialog.tsx index 787e06bf5..f656c92bb 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/install-module-dialog.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/install-module-dialog.tsx @@ -100,7 +100,9 @@ export function InstallModuleDialog({ owner of record. -
+ {/* min-w-0: the dialog is a grid, and a long role label must not + widen its track past the dialog and push Install off-screen. */} + - - {!withheld && field.value - ? roleLabel.get(field.value) || "Role unavailable" - : undefined} + + {!withheld && field.value ? ( + + {roleLabel.get(field.value) || "Role unavailable"} + + ) : undefined} diff --git a/module/services/frontend/code/src/features/solution-installations/ui/install-module-page-dialog.tsx b/module/services/frontend/code/src/features/solution-installations/ui/install-module-page-dialog.tsx new file mode 100644 index 000000000..f7be9e3a4 --- /dev/null +++ b/module/services/frontend/code/src/features/solution-installations/ui/install-module-page-dialog.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { createClient } from "@connectrpc/connect"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { OfferService } from "@/gen/host/installation/v1/installation_pb"; +import type { AvailableSolution } from "@/gen/saas/accounts/v1/installations_pb"; +import { apiTransport } from "@/lib/connect/transport"; +import { + Button, + DelayedLoading, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui"; +import { offerScopeText } from "../model/install-review"; +import { hostRefusal, releaseText } from "../model/installations"; +import { DeclaredAccess } from "./declared-access"; + +const offers = createClient(OfferService, apiTransport); + +/** + * The review of a module page that declares roles: what it receives and what + * each of its roles permits, read from the authority's offer as a solution's + * review is. A module stays installed by this host (the authority installs + * only solutions), so Install carries no agent role: the module's signed + * installation role decides. Teams take its roles in Set up, as a solution's do. + */ +export function InstallModulePageDialog({ + orgId, + solution, + onSubmit, + isPending, + error, + onClose, +}: { + orgId: string; + solution: AvailableSolution; + onSubmit: () => void; + isPending: boolean; + error?: string; + onClose: () => void; +}) { + const [approved, setApproved] = useState(false); + const review = useQuery({ + queryKey: ["module-page-install-review", orgId, solution.targetId], + queryFn: () => + offers.reviewOffer({ + organizationId: orgId, + target: solution.routeAlias, + }), + staleTime: Infinity, + retry: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }); + const offer = !review.isError && !review.isFetching ? review.data : undefined; + return ( + !open && onClose()}> + + + Install {solution.routeAlias} + + {releaseText(solution)}. Review the module page and its roles. + + + + {review.isError &&

{hostRefusal(review.error)}

} + {offer && ( +
+ +
+

Roles

+
    + {offer.roles.map((role) => ( +
  • + {role.name}:{" "} + {role.scopes.map(offerScopeText).join("; ") || + "No data access declared."} +
  • + ))} +
+

+ Choose a team for each role in Set up once the module is + installed. Nobody holds a role until it is granted there. +

+
+ +
+ )} + {error &&

The host refused the install: {error}

} + + + + + +
+
+ ); +} diff --git a/module/services/frontend/code/src/features/solution-installations/ui/install-solution-dialog.tsx b/module/services/frontend/code/src/features/solution-installations/ui/install-solution-dialog.tsx index 46a77c58c..494501546 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/install-solution-dialog.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/install-solution-dialog.tsx @@ -24,13 +24,13 @@ import { import { assertMatchingOffer, moduleConsentText, - offerScopeText, } from "../model/install-review"; import { hostRefusal, releaseText } from "../model/installations"; import { choicesMatch, useInstallationTeamChoices, } from "../service/team-choices"; +import { DeclaredAccess } from "./declared-access"; import { InstallationTeamChoices } from "./installation-team-choices"; const offers = createClient(OfferService, apiTransport); @@ -81,9 +81,6 @@ export function InstallSolutionDialog({ refetchOnReconnect: false, }); const data = !review.isError && !review.isFetching ? review.data : undefined; - const machineRole = data?.offer.roles.find( - (role) => role.name === data.consent.installationRole, - ); return ( !open && onClose()}> @@ -101,48 +98,11 @@ export function InstallSolutionDialog({ {review.isError &&

{hostRefusal(review.error)}

} {data && (
-
-

Solution access

- {data.consent.installationRole === "none" ? ( -

- This solution receives no standing machine access. Setup - receives only temporary permission to complete this - installation. -

- ) : ( - <> -

- The solution declares its installation access as “ - {machineRole?.name}”. -

-
    - {machineRole?.scopes.map((scope, index) => ( -
  • {offerScopeText(scope)}
  • - ))} -
- {machineRole?.scopes.length === 0 && ( -

This declared role grants no data access.

- )} - - )} - {data.offer.ceiling.length > 0 && ( - <> -

- Declared operations, still subject to the caller’s assigned - access: -

-
    - {data.offer.ceiling.map((binding) => ( -
  • - {binding.module}:{" "} - {binding.scopes.map(offerScopeText).join("; ") || - "no data access"} -
  • - ))} -
- - )} -
+

Supporting modules

{data.modules.length === 0 && ( @@ -171,8 +131,8 @@ export function InstallSolutionDialog({

No standing machine access is granted.

)}
    - {access.map((line, index) => ( -
  • {line}
  • + {access.map((line) => ( +
  • {line}
  • ))}
diff --git a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx index d1a34a254..732b26d6f 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx @@ -28,7 +28,8 @@ const session = vi.hoisted(() => ({ platformRole: undefined as PlatformRole | undefined, switchOrganization: async () => {}, })); -vi.mock("next/navigation", () => ({ useRouter: () => ({ push: vi.fn() }) })); +const push = vi.hoisted(() => vi.fn()); +vi.mock("next/navigation", () => ({ useRouter: () => ({ push }) })); vi.mock("@/lib/auth", () => ({ useAuth: () => session })); vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); @@ -55,6 +56,8 @@ afterEach(() => { expect(actual).toEqual(expected); }); +const REVIEW_OFFER = + "http://localhost:3000/host.installation.v1.OfferService/ReviewOffer"; const EXPENSES_TARGET = "aaaaaaaa-0000-4000-8000-000000000001"; const LEDGER_TARGET = "aaaaaaaa-0000-4000-8000-000000000002"; // The consent the page sends: the reviewed target and generation, a zero @@ -221,6 +224,17 @@ function serve({ reads.teams++; return HttpResponse.json(teamsBody); }), + // A module that declares no roles: the authority refuses its offer. + http.post(REVIEW_OFFER, () => + HttpResponse.json( + { + code: "not_found", + message: + "not a composed solution or a module page declaring roles; a module is installed by composition", + }, + { status: 404 }, + ), + ), ); return reads; } @@ -1554,6 +1568,145 @@ describe("accessible validation and host text", () => { }); }); +describe("installing a module: the form follows its declared roles, not its kind", () => { + it("opens the agent-role form for a module whose offer the authority refuses", async () => { + let asked: unknown; + serve(); + server.use( + http.post(REVIEW_OFFER, async ({ request }) => { + asked = await request.json(); + return HttpResponse.json( + { code: "not_found", message: "declares no roles" }, + { status: 404 }, + ); + }), + ); + renderInApp(); + fireEvent.click( + await screen.findByRole("button", { name: "Install ledger" }), + ); + const dialog = await screen.findByRole("dialog"); + expect( + within(dialog).getByRole("combobox", { name: "Agent role" }), + ).toBeTruthy(); + expect(within(dialog).queryByText("Module access")).toBeNull(); + expect(asked).toEqual({ organizationId: "org-1", target: "ledger" }); + }); + + it("reviews a module page that declares roles, installs it with no agent role, and opens its Set up", async () => { + let installed: unknown; + serve(); + server.use( + http.post(REVIEW_OFFER, () => + HttpResponse.json({ + target: "ledger", + installationRole: "none", + roles: [ + { + name: "installer", + scopes: [{ resourceKind: "entry", actions: ["write"] }], + }, + { name: "viewer", scopes: [] }, + ], + }), + ), + http.post( + rpc("InstallationService", "InstallSolution"), + async ({ request }) => { + installed = await request.json(); + return HttpResponse.json({ + id: "bbbbbbbb-0000-4000-8000-000000000002", + orgId: "org-1", + targetId: LEDGER_TARGET, + status: "INSTALLATION_STATUS_ACTIVE", + }); + }, + ), + ); + renderInApp(); + fireEvent.click( + await screen.findByRole("button", { name: "Install ledger" }), + ); + const dialog = await screen.findByRole("dialog"); + await within(dialog).findByText("Module access"); + // The module's own declaration decides its standing access; nothing + // asks for an organisation role. + expect( + within(dialog).getByText( + "This module receives no standing machine access.", + ), + ).toBeTruthy(); + expect( + within(dialog).queryByRole("combobox", { name: "Agent role" }), + ).toBeNull(); + const roles = within(dialog).getByRole("region", { name: "Roles" }); + expect(within(roles).getByText(/installer:/)).toBeTruthy(); + expect( + within(roles).getByText("viewer: No data access declared."), + ).toBeTruthy(); + + const install = within(dialog).getByRole("button", { + name: "Install", + }) as HTMLButtonElement; + expect(install.disabled).toBe(true); + fireEvent.click(within(dialog).getByRole("checkbox")); + expect(install.disabled).toBe(false); + fireEvent.click(install); + + await waitFor(() => expect(installed).toBeDefined()); + expect(installed).toEqual({ + orgId: "org-1", + targetId: LEDGER_TARGET, + approval: LEDGER_APPROVAL, + agentIdentifier: "acme/ledger:0.9.1", + }); + await waitFor(() => + expect(push).toHaveBeenCalledWith( + "/admin/solutions/bbbbbbbb-0000-4000-8000-000000000002", + ), + ); + }); + + it("keeps a long role label inside the form so Install stays in view", async () => { + const long = { + id: "cccccccc-0000-4000-8000-000000000003", + name: "Module viewer", + orgId: "org-1", + permissions: Array.from({ length: 12 }, (_, index) => ({ + resource: `resource-with-a-long-name-${index}`, + action: "read", + })), + }; + const label = `Module viewer — ${long.permissions + .map((p) => `${p.resource}:${p.action}`) + .join(", ")}`; + serve({ rolesBody: { roles: [...roles.roles, long] } }); + renderInApp(); + fireEvent.click( + await screen.findByRole("button", { name: "Install ledger" }), + ); + const dialog = await screen.findByRole("dialog"); + await choose(dialog, "Agent role", label); + const trigger = within(dialog).getByRole("combobox", { + name: "Agent role", + }); + // happy-dom computes no layout, so this holds the CSS that bounds it: + // the form may shrink inside the dialog's grid, the trigger may shrink + // inside the form, and the label truncates with its full text on hover. + expect(trigger.closest("form")?.classList.contains("min-w-0")).toBe(true); + expect(trigger.classList.contains("min-w-0")).toBe(true); + expect(trigger.getAttribute("title")).toBe(label); + const shown = within(trigger).getByText(label); + expect(shown.classList.contains("truncate")).toBe(true); + expect(shown.closest('[data-slot="select-value"]')?.classList).toContain( + "min-w-0", + ); + expect( + within(dialog).getByRole("button", { name: "Install" }).closest("form"), + ).toBe(trigger.closest("form")); + }); +}); + // HOST-INST-002, HOST-INST-004, HOST-INST-005: no grants while unacknowledged; // a refusal remains visible and Retry reuses the same installation identity. it("shows pending and failed installations and retries the failed step", async () => { diff --git a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx index 94b7b9939..3f41e9084 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx @@ -9,6 +9,7 @@ import { EmptyState } from "@/components/empty-state"; import { OrgSelector } from "@/components/org-selector"; import { type AvailableSolution, + type Installation, InstallationStatus, } from "@/gen/saas/accounts/v1/installations_pb"; import { useAuth } from "@/lib/auth"; @@ -37,6 +38,7 @@ import { CardDescription, CardHeader, CardTitle, + DelayedLoading, Spinner, Table, TableBody, @@ -47,6 +49,7 @@ import { useLoadingPhase, } from "@/shared/ui"; import { + agentIdentifierFor, availableRows, type FactView, hostRefusal, @@ -55,6 +58,7 @@ import { type TeamGrantRow, } from "../model/installations"; import { readinessSummary } from "../model/readiness"; +import type { InstallSolutionValues } from "../model/schemas"; import { useGrantInstallationToTeam, useInstallModule, @@ -66,7 +70,9 @@ import { import { useAvailableSolutions, useInstallations } from "../service/queries"; import { GrantTeamDialog } from "./grant-team-dialog"; import { InstallModuleDialog } from "./install-module-dialog"; +import { InstallModulePageDialog } from "./install-module-page-dialog"; import { InstallSolutionDialog } from "./install-solution-dialog"; +import { useRolesDeclared } from "./when-roles-declared"; /** * One fact. A gap reads as its label in muted italics with what it means on @@ -127,6 +133,20 @@ export function SolutionInstallationsPage({ ); } +/** A module page's install: its release's agent and no agent role, which its declaration selects. */ +function moduleWithoutAgentRole( + target: AvailableSolution, +): InstallSolutionValues { + return { + agentIdentifier: agentIdentifierFor(target), + displayName: "", + rootScopeLabel: "", + roleId: "", + allowedAudiences: "", + allowedScopes: "", + }; +} + function SolutionInstallationsForOrganization({ orgId, showModules, @@ -141,6 +161,38 @@ function SolutionInstallationsForOrganization({ // This owner survives dismissal of the form and reports every host result. const install = useInstallModule(orgId); const combinedInstall = useInstallReviewedSolution(orgId); + // A target that declares roles installs through a review of them, as a + // solution does; a module that declares none keeps its agent-role form. + const rolesDeclared = useRolesDeclared( + installing?.routeAlias ?? "", + installing?.kind, + ); + const installModule = ( + target: AvailableSolution, + values: InstallSolutionValues, + { onSuccess }: { onSuccess: (installation: Installation) => void }, + ) => + install.mutate( + { + targetId: target.targetId, + generation: target.appliedGeneration, + expectedRevision: (installed.data ?? []).reduce( + (revision, row) => + row.installation?.targetId === target.targetId && + row.installation.revision > revision + ? row.installation.revision + : revision, + BigInt(0), + ), + operationId: crypto.randomUUID(), + values, + }, + { + onSuccess, + onError: (error) => + toast.error(`The host refused the install: ${hostRefusal(error)}`), + }, + ); return (
@@ -197,6 +249,7 @@ function SolutionInstallationsForOrganization({ showModules={showModules} query={available} installing={ + (Boolean(installing) && rolesDeclared === undefined) || install.isPending || combinedInstall.isPending || !installed.isSuccess || @@ -216,7 +269,13 @@ function SolutionInstallationsForOrganization({ )} - {installing && installing.kind !== "module" && ( + {/* A module's roles are read before its form opens: which form is + the authority's answer, not the target's kind. */} + + {installing?.kind === "solution" && ( setInstalling(null)} /> )} - {installing?.kind === "module" && ( - { - install.mutate( - { - targetId: installing.targetId, - generation: installing.appliedGeneration, - expectedRevision: (installed.data ?? []).reduce( - (revision, row) => - row.installation?.targetId === installing.targetId && - row.installation.revision > revision - ? row.installation.revision - : revision, - BigInt(0), - ), - operationId: crypto.randomUUID(), - values, + onSubmit={() => + // The module's signed installation role decides its standing + // access, so no agent role is sent; its roles are granted to + // teams in Set up, where the installation now lands. + installModule(installing, moduleWithoutAgentRole(installing), { + onSuccess: (installation) => { + toast.success( + `${installing.routeAlias} installation ${installation.status === InstallationStatus.ACTIVE ? "active" : "pending"}. Choose a team for each role in Set up.`, + ); + setInstalling(null); + router.push(`/admin/solutions/${installation.id}`); }, - { - onSuccess: () => { - toast.success( - `${installing.routeAlias} installation pending`, - ); - setInstalling(null); - }, - onError: (error) => - toast.error( - `The host refused the install: ${hostRefusal(error)}`, - ), + }) + } + onClose={() => setInstalling(null)} + /> + )} + {installing?.kind === "module" && rolesDeclared === false && ( + + installModule(installing, values, { + onSuccess: () => { + toast.success(`${installing.routeAlias} installation pending`); + setInstalling(null); }, - ); - }} + }) + } onClose={() => setInstalling(null)} /> )} diff --git a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts index c52d5e13f..2f3663905 100644 --- a/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/src/gen/saas/accounts/v1/installations_pb.ts @@ -19,7 +19,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEitwQKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEEhAKCHJldmlzaW9uGA4gASgEEhMKC3NldHVwX3N0YXRlGA8gASgJEhUKDXNldHVwX21lc3NhZ2UYECABKAlCDQoLX3Jldm9rZWRfYXRKBAgEEAVSE3NvbHV0aW9uX2lkZW50aWZpZXIimQQKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSGQoHcm9sZV9pZBgHIAEoCUIIukgFcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQESNwoIYXBwcm92YWwYDiABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWxCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIq0CChdSZXZpZXdlZFNvbHV0aW9uSW5zdGFsbBIOCgZvcmdfaWQYASABKAkSEAoIYWN0b3JfaWQYAiABKAkSEQoJdGFyZ2V0X2lkGAMgASgJEg4KBnRhcmdldBgEIAEoCRISCgpiaW5kaW5nX2lkGAUgASgJEg8KB3JlbGVhc2UYBiABKAkSFwoPZGVsaXZlcnlfZGlnZXN0GAcgASgJEjcKCGFwcHJvdmFsGAggASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhkKEWluc3RhbGxhdGlvbl9yb2xlGAkgASgJEjsKB21vZHVsZXMYCiADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJldmlld2VkU3VwcG9ydGluZ01vZHVsZSKiAgoYUmV2aWV3ZWRTdXBwb3J0aW5nTW9kdWxlEhEKCXByaW5jaXBhbBgBIAEoCRIRCgl0YXJnZXRfaWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhIKCmJpbmRpbmdfaWQYBCABKAkSDwoHcmVsZWFzZRgFIAEoCRIXCg9kZWxpdmVyeV9kaWdlc3QYBiABKAkSGQoRaW5zdGFsbGF0aW9uX3JvbGUYByABKAkSHQoVcmV1c2VfaW5zdGFsbGF0aW9uX2lkGAggASgJEg0KBXNjb3BlGAkgASgJEjcKCGFwcHJvdmFsGAogASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhAKCHJlcXVpcmVkGAsgASgIIlUKHFJldmlld1NvbHV0aW9uSW5zdGFsbFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCgl0YXJnZXRfaWQYAiABKAlCCLpIBXIDsAEBIn0KHkluc3RhbGxSZXZpZXdlZFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEkEKBnJldmlldxgCIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGxCBrpIA8gBASJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyrhUKE0luc3RhbGxhdGlvblNlcnZpY2USxgEKFVJldmlld1NvbHV0aW9uSW5zdGFsbBIuLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3U29sdXRpb25JbnN0YWxsUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGwiUsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwImOgEqIiEvdjEvc29sdXRpb24taW5zdGFsbGF0aW9ucy9yZXZpZXcSuAEKF0luc3RhbGxSZXZpZXdlZFNvbHV0aW9uEjAuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsUmV2aWV3ZWRTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJLwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSARQA1gDYAGC0+STAh86ASoiGi92MS9zb2x1dGlvbi1pbnN0YWxsYXRpb25zEroBCg9JbnN0YWxsU29sdXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJdwvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLmNyZWF0ZWQQAkABSARQA1gDYAGC0+STAhY6ASoiES92MS9pbnN0YWxsYXRpb25zEtIBChFVbmluc3RhbGxTb2x1dGlvbhIqLnNhYXMuYWNjb3VudHMudjEuVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InnC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV2b2tlZBACQAFIBFADWANgAYLT5JMCMjoBKiItL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dW5pbnN0YWxsEtIBChJOYXJyb3dJbnN0YWxsYXRpb24SKy5zYWFzLmFjY291bnRzLnYxLk5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkid8LzGD4IAhAEKgwKBm9yZ19pZBACGAEwAToeChpzYWFzLmluc3RhbGxhdGlvbi5uYXJyb3dlZBACQAFIBFADWANgAYLT5JMCLzoBKiIqL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06bmFycm93EtYBChFSZXRyeUluc3RhbGxhdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuUmV0cnlJbnN0YWxsYXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24idcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXRyaWVkEAJAAUgEUANYA2ABgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpyZXRyeRKJAgodVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXASNi5zYWFzLmFjY291bnRzLnYxLlRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIo8BwvMYSwgCEAQqDAoGb3JnX2lkEAIYATABOisKJ3NhYXMuaW5zdGFsbGF0aW9uLm93bmVyc2hpcF90cmFuc2ZlcnJlZBACQAFIBFADWANgAYLT5JMCOjoBKiI1L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dHJhbnNmZXJPd25lcnNoaXASuQEKD0dldEluc3RhbGxhdGlvbhIoLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVzcG9uc2UiUcLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIlEiMvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfRKGAQoRTGlzdEluc3RhbGxhdGlvbnMSKi5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBChZMaXN0QXZhaWxhYmxlU29sdXRpb25zEi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1Jlc3BvbnNlIk3C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCIRIfL3YxL2luc3RhbGxhdGlvbnM6bGlzdEF2YWlsYWJsZRLRAQodTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnMSNi5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBo3LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZSI/wvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhMSES92MS9pbnN0YWxsYXRpb25zEvABChdHcmFudEluc3RhbGxhdGlvblRvVGVhbRIwLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlc3BvbnNlInDC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5ncmFudGVkEAJAAUgEUANYA2ABeAKC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zEu0BChpSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbRIzLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IoEBwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUucmV2b2tlZBACQAFIBFADWANgAXgCgtPkkwI/OgEqIjovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcy97dGVhbV9pZH06cmV2b2tlQroBChRjb20uc2Fhcy5hY2NvdW50cy52MUISSW5zdGFsbGF0aW9uc1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_host_installation_v1_installation, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEitwQKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEEhAKCHJldmlzaW9uGA4gASgEEhMKC3NldHVwX3N0YXRlGA8gASgJEhUKDXNldHVwX21lc3NhZ2UYECABKAlCDQoLX3Jldm9rZWRfYXRKBAgEEAVSE3NvbHV0aW9uX2lkZW50aWZpZXIinAQKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSHAoHcm9sZV9pZBgHIAEoCUILukgI2AEBcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQESNwoIYXBwcm92YWwYDiABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWxCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIq0CChdSZXZpZXdlZFNvbHV0aW9uSW5zdGFsbBIOCgZvcmdfaWQYASABKAkSEAoIYWN0b3JfaWQYAiABKAkSEQoJdGFyZ2V0X2lkGAMgASgJEg4KBnRhcmdldBgEIAEoCRISCgpiaW5kaW5nX2lkGAUgASgJEg8KB3JlbGVhc2UYBiABKAkSFwoPZGVsaXZlcnlfZGlnZXN0GAcgASgJEjcKCGFwcHJvdmFsGAggASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhkKEWluc3RhbGxhdGlvbl9yb2xlGAkgASgJEjsKB21vZHVsZXMYCiADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJldmlld2VkU3VwcG9ydGluZ01vZHVsZSKiAgoYUmV2aWV3ZWRTdXBwb3J0aW5nTW9kdWxlEhEKCXByaW5jaXBhbBgBIAEoCRIRCgl0YXJnZXRfaWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhIKCmJpbmRpbmdfaWQYBCABKAkSDwoHcmVsZWFzZRgFIAEoCRIXCg9kZWxpdmVyeV9kaWdlc3QYBiABKAkSGQoRaW5zdGFsbGF0aW9uX3JvbGUYByABKAkSHQoVcmV1c2VfaW5zdGFsbGF0aW9uX2lkGAggASgJEg0KBXNjb3BlGAkgASgJEjcKCGFwcHJvdmFsGAogASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhAKCHJlcXVpcmVkGAsgASgIIlUKHFJldmlld1NvbHV0aW9uSW5zdGFsbFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCgl0YXJnZXRfaWQYAiABKAlCCLpIBXIDsAEBIn0KHkluc3RhbGxSZXZpZXdlZFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEkEKBnJldmlldxgCIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGxCBrpIA8gBASJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyrhUKE0luc3RhbGxhdGlvblNlcnZpY2USxgEKFVJldmlld1NvbHV0aW9uSW5zdGFsbBIuLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3U29sdXRpb25JbnN0YWxsUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGwiUsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwImOgEqIiEvdjEvc29sdXRpb24taW5zdGFsbGF0aW9ucy9yZXZpZXcSuAEKF0luc3RhbGxSZXZpZXdlZFNvbHV0aW9uEjAuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsUmV2aWV3ZWRTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJLwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSARQA1gDYAGC0+STAh86ASoiGi92MS9zb2x1dGlvbi1pbnN0YWxsYXRpb25zEroBCg9JbnN0YWxsU29sdXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJdwvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLmNyZWF0ZWQQAkABSARQA1gDYAGC0+STAhY6ASoiES92MS9pbnN0YWxsYXRpb25zEtIBChFVbmluc3RhbGxTb2x1dGlvbhIqLnNhYXMuYWNjb3VudHMudjEuVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InnC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV2b2tlZBACQAFIBFADWANgAYLT5JMCMjoBKiItL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dW5pbnN0YWxsEtIBChJOYXJyb3dJbnN0YWxsYXRpb24SKy5zYWFzLmFjY291bnRzLnYxLk5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkid8LzGD4IAhAEKgwKBm9yZ19pZBACGAEwAToeChpzYWFzLmluc3RhbGxhdGlvbi5uYXJyb3dlZBACQAFIBFADWANgAYLT5JMCLzoBKiIqL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06bmFycm93EtYBChFSZXRyeUluc3RhbGxhdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuUmV0cnlJbnN0YWxsYXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24idcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXRyaWVkEAJAAUgEUANYA2ABgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpyZXRyeRKJAgodVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXASNi5zYWFzLmFjY291bnRzLnYxLlRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIo8BwvMYSwgCEAQqDAoGb3JnX2lkEAIYATABOisKJ3NhYXMuaW5zdGFsbGF0aW9uLm93bmVyc2hpcF90cmFuc2ZlcnJlZBACQAFIBFADWANgAYLT5JMCOjoBKiI1L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dHJhbnNmZXJPd25lcnNoaXASuQEKD0dldEluc3RhbGxhdGlvbhIoLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVzcG9uc2UiUcLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIlEiMvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfRKGAQoRTGlzdEluc3RhbGxhdGlvbnMSKi5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBChZMaXN0QXZhaWxhYmxlU29sdXRpb25zEi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1Jlc3BvbnNlIk3C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCIRIfL3YxL2luc3RhbGxhdGlvbnM6bGlzdEF2YWlsYWJsZRLRAQodTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnMSNi5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBo3LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZSI/wvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhMSES92MS9pbnN0YWxsYXRpb25zEvABChdHcmFudEluc3RhbGxhdGlvblRvVGVhbRIwLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlc3BvbnNlInDC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5ncmFudGVkEAJAAUgEUANYA2ABeAKC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zEu0BChpSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbRIzLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IoEBwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUucmV2b2tlZBACQAFIBFADWANgAXgCgtPkkwI/OgEqIjovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcy97dGVhbV9pZH06cmV2b2tlQroBChRjb20uc2Fhcy5hY2NvdW50cy52MUISSW5zdGFsbGF0aW9uc1Byb3RvUAFaLGFjY291bnRzL3BrZy9nZW4vc2Fhcy9hY2NvdW50cy92MTthY2NvdW50c3YxogIDU0FYqgIQU2Fhcy5BY2NvdW50cy5WMcoCEFNhYXNcQWNjb3VudHNcVjHiAhxTYWFzXEFjY291bnRzXFYxXEdQQk1ldGFkYXRh6gISU2Fhczo6QWNjb3VudHM6OlYxYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_host_installation_v1_installation, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -177,7 +177,10 @@ export type InstallSolutionRequest = Message<"saas.accounts.v1.InstallSolutionRe /** * The least-privilege role granted to the agent at the root node. This is * deliberately NOT the full ":catalog" role; widening is a separate, - * audited admin action. + * audited admin action. Empty when the delivered module declares its own + * installation role: the host selects a named one, and "none" grants the + * agent no standing role at all. A role given for "none" is refused, and an + * empty one is refused for a target that declares no installation role. * * @generated from field: string role_id = 7; */ diff --git a/module/services/frontend/code/ui-inventory.json b/module/services/frontend/code/ui-inventory.json index 0502a36aa..7de9cc9cd 100644 --- a/module/services/frontend/code/ui-inventory.json +++ b/module/services/frontend/code/ui-inventory.json @@ -5131,6 +5131,40 @@ "coverage": "missing", "remaining": "Owner stories or a reviewed nonvisual exclusion still required." }, + { + "source": "src/features/solution-installations/ui/declared-access.tsx", + "imports": [ + "@/gen/host/installation/v1/installation_pb", + "../model/install-review" + ], + "owner": "SaaS application", + "disposition": "keep feature-local", + "reason": "States an installation's declared machine access and call ceiling from the authority's offer, once, for both the solution review and the module page review.", + "stories": [], + "coverage": "missing", + "remaining": "Owner stories or a reviewed nonvisual exclusion still required." + }, + { + "source": "src/features/solution-installations/ui/install-module-page-dialog.tsx", + "imports": [ + "@connectrpc/connect", + "@tanstack/react-query", + "react", + "@/gen/host/installation/v1/installation_pb", + "@/gen/saas/accounts/v1/installations_pb", + "@/lib/connect/transport", + "@/shared/ui", + "../model/install-review", + "../model/installations", + "./declared-access" + ], + "owner": "SaaS application", + "disposition": "keep feature-local", + "reason": "Reviews a module page that declares roles from the authority's offer and installs it with no agent role; its roles take teams in Setup. Lower-level presentation consumes the shared kit.", + "stories": [], + "coverage": "missing", + "remaining": "Owner stories or a reviewed nonvisual exclusion still required." + }, { "source": "src/features/solution-installations/ui/install-module-dialog.tsx", "imports": [ From c48994974d672afd60394b46dbf918615e82fc53 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:22:33 -0400 Subject: [PATCH 2/4] chore(contracts): regenerate the exported contracts and libraries for the optional role_id Output of `codefly generate contracts saas-starter` and `scripts/ci/check-generated-libraries.sh` (CLI 0.1.221, the CI pin) after InstallSolutionRequest.role_id became optional. The source-read SDK's client step needed `--force` once, because its recorded contract digest moved. No hand edits. Co-Authored-By: Claude Fable 5.1 --- .../source-read-sdk/library.codefly.yaml | 2 +- .../api/accounts/authority/contract.binpb | Bin 1311587 -> 1311879 bytes .../saas/accounts/v1/installations.proto | 10 ++++++++-- .../api/accounts/connect/contract.binpb | Bin 1311587 -> 1311879 bytes .../saas/accounts/v1/installations.proto | 10 ++++++++-- module/contracts/api/catalog.codefly.json | 4 ++-- module/module.package.codefly.yaml | 4 ++-- .../saas-sdk/generated/library.codefly.yaml | 2 +- .../gen/saas/accounts/v1/installations_pb.ts | 7 +++++-- 9 files changed, 27 insertions(+), 12 deletions(-) diff --git a/libraries/source-read-sdk/library.codefly.yaml b/libraries/source-read-sdk/library.codefly.yaml index ce0c08889..161cdad77 100644 --- a/libraries/source-read-sdk/library.codefly.yaml +++ b/libraries/source-read-sdk/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: authority - contract-digest: sha256:d6a4564228c0a8ee8957d48d0413c642500f03373d14a2ead4d64d0536bbcbe7 + contract-digest: sha256:606e52170c2e55688ce163eb1d54c347f0d19ac40915d7fb6252f2034b787c0e services: - ModuleCapabilitiesService generated-by: diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 70a52aba86601fcff06e183974853708df00837f..257438d435624ec885abbb37d9bf4a00b58aba6b 100644 GIT binary patch delta 15118 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 delta 14714 zcmZ8od7Kr+wZ2_lU455nV0!j3z^Fh(WL%@ZxDidnXcXd-L`~vzA&D>f#U$$Qg_oDt zEF*|43WCj!>xckoy=(8--;tHJI-r%l^@OK#Q6QoXt%j+<|l z&%&|?%V&jhkRvZQZIOi98tlkU#)v`?LmiRNH|UsDr~rpMvi7mMJ05@V!5)u3`tT!v ztb4S_W8bg4|BpT6Km9fMeKgc5lv;hnI@Afzvk*QFbvj*Od{XdfsMC2keA3Y>co2$E z4s+y1`LcWgZn8*{ThBW3`^JtBwq zDX35`aYTpw&)E}M0E{2*9o${F#QE-pwi23N;#_iBe!7zvQiSSNC$lnszjrV#dezCE z&9#Kc!18k9s|2P18C`1&5zKNY*U~~*T<(Nr79yDCPWd(LCsYk(%bg0H5<)mIz9Kaa zYd~G|chks6DdSwVG3UVg3g;|M6-r@!h12>*W8E>+yf~ z4Sp59?&Mn7IxzaWQ>r^%2mzz7J7u>s{{)%;*PZg8nr2u+mYhp%TrA?IuI&z%gmgo- z+R41(IQc@}l`f)K?PPTxyToTC*VveWLqeKRAXHvs2?d0AYb>FF@NSJIl%Sq~)>uMa zCbT&<4r@SNIaU-h%vDDg!+NtN6veRKYzZZ(tv3^)6qXFtIw!NyghkWxIvbYcG+A#O z1`H*TjtU5M*W0M1h$yV}HY^mQ;q^8wg1X`LHY~|uZAh)d9FPtRg$zxa0K)hN8xF;q^n^WsB2c*M7Awv@uAgph;VFAMWW*ZhkZGE%Neos*LZzbbD zLiPhPx~=w#x;x`yzo5(QTb*266QzLdttJDqGBc1)qaqEPO#lmau-9R`HTyKIL6!k=BX!vGQXF56)Q zb=bRX8Y=Fu-KlX{1JeD4LWa3WPglUOzT0*fAgu4U9Y#=F-)++nYx4U}X1@uGmf-iD ztnMIRWH6uab3_ZCgaH}p83hP+_c<-~M#evv!`eqX8Jb0X;XwC8N0jGpLSF#_#T^F( zH+T5ZsW3Y1>xZPWOn_nZ%ONLo(s63%in@5y&w}dWivxnTPISnQ_bd;L!61@9^V5&AKw@;EjnA$ENv z=#PB>WL3vani?4f*S}DUhlmlu)EAB{VcN68MU4|g(62ZI8ZjrFc*FAG7adRtFHSf) zy-)%|^@LMdm0v^j=R(y>Wcn8WJvivP$2b2jw zXk(Z#dlx|XGfbGhOM!>_v#BcT0#zDSBJ5d_(@BK7P<>V)iO6tN;5;0j>SE>v(i=V$ zLU*_@yG%gn4i^>HGG)z(R279l)?><=5vj+NH6v1wB~+giBC^qCE}HiH16JJDuP2~Sy*QdJZJ=>$R{nw?}52#`xn0+k3#zR9c_ zVW-N`nC0HHhCW>ATsEi5jIFCyB3$-y- zLDDtGyP*;_3iCmMI#6j8=7XYy(`lwnallXleb3mV13C8*uGIq{7}ghvb2Z<9u)aXFzlA?3 z3HHy`kfg)S6qU+^swgtkJYHWFIa;I@|JQk`GKfMg7GiZgaumK=7YoHZ0g_`_FBadq zl7c6Q78i>f?x9@_L42t#5ppF`2fG*+EE%S!f&D+EcKcUYGN@r}ycL;H;SUN7a z#))%+MW*Layl@iS&Sl&%U zmvcU|2t=zrqFjA|PX#>~SPZH^jQ5TY%A3mk{G0iR^i&*9PMsr^InJFn%y) zoJC;#P|7%q!1&=a6p`1TP%S-tMLhbB+{Nwe&=Slpm^>;}%ltykBu4W|A%+#3Cj{Sf zqLV`L?f}%eaP_39=4~}WFnLm3)!mrJSBUyj$ic3i8C=cem!dG(wKD_Dz~omclPrSe zuTv&jL`NjI4ySOg{qxJmA^2uwcZ zCQ}59z~sO)6v5;ms->B9??SvoTsai+mhr6TGMOYS3`M|tlG8$@`O~ht>RU3f{)`LO z2jGJeY{J@eu9y?|oD@76J?ARk8<8AQJ?GY3VkRcuT)WrYZoHs_v)Gl53F%Uk9=nCc zgmjjH^pS2d9kU3QN4ZJ*vWQ3@?M7;RLOP2<`UDpfGVTG$C2GTWFr`d%ahjzA)k2t@ zly(8j&;?UcCRv0on3{G0i_itrk}imJ6X=8KXK11iW~6;U(a&_{3rQD9(*-l#!V5_k zXd${_maDEzyrpRcT`-%vpiy@LPV1Un@rLWh|2G8()lF`a`5xkGa+A!bonn)FRaesq z9^9Dk%9Tkcc)AnjyM>iWC$J2iupsRO7QymMX(zCVI$@z(p_U|_z+!a5Qa4`SAt;Mp zPE((YDD{`6yt53vznZegBJjRE<();~eMQ2%U=e74?F>a)98=moO8r%?Y)*Lh4DYMl zLUY2q7J~QHuDZhTu4UkT4e>rmPdGSr+vJLmiW8;<1t;3%D*ix1qA7&6$!&L&$$i>n zZ*uRr&(MwwG4-Y^_a(F|P5YZ}VP8T!%Ru{EDeWwR<;^MWEF#*sfcCc&+F1SjJWa7X1p&UlqH1~% zNH?3OT=^5TGW(irhzhqPUJXch|bN)ri6FH9g21qfY_`s~-A`uOc$t03R#Q`#z zMCfB~fV}EXLo04D)l*Uqmv;HgYFeL?h2hdJpDY8d1Eo6KxWyt^9wb%!(?#HKuvC#H zkgE}aLu5iA%Ru1MGFc&61OlIt=p?;=VG#%%Dw7qGMIdn48H(WVS*o=bSHT#*IBd*K zm13!MYnQ0_#lHtt#d&jrdN-OX6(7@39k?`Aw&vSpf*@t8{LzDk6r850X;QwFkdo7+ zOp}F|5>i+OQl`sfd1DbQ&ydOT#v;nKMp>?2kakbOVsJGl{&N#1*10J=Tmp9Hr9rX? z?7S$G^kNa%X-e5)5!ji3h9cNmK(%ys(LvD?DQ_|?=Qz7MlC03xq?eW~twN-N7i>-Ll#7J=5cQ(9RBTHi@& zWf5q7_cTR2YTrwFrJM4d68AYed-G=0?vw>Pb!!p4+9~U<%+J!figVtBQZy9*v?w^= zjSkA>JFS3<2W9e|RsgLBW!K*rS_`1{kd)sfv=%h2hh*WKgjSY;*25{SEP~}vQd(I= zv>pMipC`1k7_=TETJ`>mMZEv|LgMH}&yZROlgDMU1!ftzK9N$!B5-|DCR<<@f$J|5 zt|P%Rkp0yen#kd=lWMwFOH6+f8!NpnCPI37vae@%#RaoQ_3{dRJ-aK`LiAK`FWDMs z8MX#}yizPu&3HnA2?6JzgFG>{c;3sw8Ydd$DLx6I0)#rqJNtSwoJ!CygS=ZGFm9B< zjlrIL9&YGkiW1!~gT2D@@IvPX%g`@Fykz2F5iCFLB@+jWs9&D(%GGesP8=*ozl@0I zdHM1v_L83963Y6KUeXgRLr;wMlBt$O=!r32(i1E~PmJ}Fsg_0Ph;e5qq9ewiRx6_L z&=V89q$h}iiJqL2^hAm2iHTleO41Wrh@P0_shdnHX&HKAvWJeKL%)`xn&D+$)CWnj zB{-YmnS1P(d^#32dgdww7>AUeIUWN-wf=ld(=Jf$m4%}s6HO|x3G15@$#TuG|MwLaHx#N zW_jiY4iHQ8EU$8or*Gg$z&CKSys9~#e)nW#I31mhT9hZ$CRKx}7jSY)=>`#9fX}0l z=>`svQFU#%x^D3nT*I}U=b7seltFWzhturGKxC^>z2s#!#kZ^o9=-b|&%E4d#rIhY zJ#+mA7)l_0=m!J}7kcLU4G;zwdgl7A6`vn2@^B1Kcd&qXtP@wg7TjFF$TQb&5JF{9 zsv?lL%REt*KaLGJbm0E7;?u7MJ)QbxUb(*6fD}s0JaemAxS-R3Db(+b;wm7?(in-h6on(tAh4Uw8}HLcc=xQS9#|44iGk0dFJ-6 z6~0OE1wl6GA_tI>zN!I2B&$qkitLTRnnuD!8AyTFaq{2m=PhQ-GCjWxmh7jEGgFyB2-?=1+$&f4JiHg9|S+6qFFzUztl{M}p!2HuVTyf(PE?Yo8o)S;V} zU7qNa--2ie1I7o}2HV>1^6L5V7KGFvdp&V!{s~;tDZI46Py10wRDb2+mhl%{mr>k#b8vU` zl~>0}f=a~o*PiHT48j5sryUhCyr8qOPn@6f#CfJJCjX%_KB>faIcETY;_h36d!r}S`8pNptsFj7lxT14!2pm>OcXLauOs^} zIlrD#;#^Y~u>$e1Kx1@%_>}6Pd6yjPz|<~c1u}NK@wwMPl^CXU8>9^3l4b!QO*jw? z4^oD3$yql-8B*HIgl<9NmPV0eA3%mp`u z(213~;4bB5WHjmUoC=H`Lfr5+4xdIVvt&XDozcoHnWe%-JYy8*d+j0$f%JtC3eoBq zRiWSK0CK5$d{s*Co8(v}N{uQQ2eRSNs2{6BJ*BlnW$2DoW-2e`o7C~-Dn0uI#Qq~* z{|;V#j#uWv0fbN)ukhgDH5VL*c!aAmvx+yq8{pyObd}XDg}h;CR5)?J2&MqB1&BNB z2rh{lm9H0A2w|#GwbU=OLqQh>GnH8c02%4&76@U@R0V6Ea(bp}qi@+!3QIF7>!v9< zRfbzHm3d7$wXv$Zy~q6z{;tPEw?6XFX0giZo6s`$bSVY>LwE{^ z6S&yh6Wmz8RGAkV5W@aaWnO5M@qXnMQhA5c02udHanC)$d!1iVW|@Q#IC^LmD57kPQX;%8-OgY+Esj@t!$Z~!-v&wP> zh}Bey%Q;Jdz||^co*)21b(LzTA1DB#kyYw4eTQ5w$|$GTkYnB8SUJVACjOuIgP%rg zY%CB$XAQ+dj#Y$et;%dMj#WTmt*|R8bUO-&t`Qcle?c({+m@0@BaAfkAj}{`)wki4pjDA zQY)FV1Eey61_Cgx{ZkGE6FVPJW^)W7bPg!<)T5G#I7m8ovI+_h#(()Z_*wlyO9SeV z)Y4GNTg)Sr?Mrntm?)0KUwj?*@d`&uR9f^`J_W)0}mJJljgWT1826qIlz> z;OEY@CzRRzLP}!QmUkai2}VvRz3&6WEGkct4cnQ=2Yn8gK!#>fcX0M&5#5_-9ElqhX7%!1cm56Nm~q`^1qE+DKo_-5S&g!KmBj2(j7dV_CXngih+py!aKea*oH(3*^L4Kw&zU#A~vCSYq zTf>P{!+kP@n!Xh+1Jd^8J4b_V-G}&QrG*e}4e?E`0ivxTzIon4P{%XG$MY7-#v1O5 zVPte4#RH7f+j!~G;KTZ1z8Q@W!s;;JjK&%s!6Qf~);eH#M7-hiU{vQ3zFC)02|6Qu ztV_BbdSn=r&b8bO3gdYAF+AOlee-0lhDUJhn-9wxe&02cR44}lad6@by&(gFqa*#C zUXTG{aHL;tsiNmyqf%9k^gf`5pLdP&O@g%thVCevVKw}|Yb@!G)6vo1DE{lef(Z|g zO{Jv|Fk4?|ZG#dcp$felp zi{Lt^ev9+vv=WYsW)u$}4}R}NGyGg&IHyDN8Gb804?zj0 zXZU6MFC+w!3|yo9YMu~kaSGyR7MKa476i}ov-)$lmXmL`jSvtAA$CFlLfzSVLg?m6 zV3?h4X9PerJljqP1a%h9wi7}vXW?8QhdF-%2Y{ghYVy6>2jZcB3p>W$P6j`W=KAIU z2Nlr3T;Ckv0O9~=u3y>Y>jNAT@Bz+TzpBaC2RKFs-sYheAK(}fOuguv1DsmU?fGe( zNNpfa;s}n>@R)DM1t4_i)3~7d^ekM?`uNYGo=?w0&4s?%;+(~MucB}EsepKK<>PCc z#s&~ltmqf?3spd<7X8+G<_AP0MZe?u{Oyd|wD($MJp^Q=a~p`E$!$RBF6!U>Wq@A` U{95AI3cphP!eaB6Wy`Yv58kGP=>Px# diff --git a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto index 61af98331..44d793e14 100644 --- a/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/authority/proto/saas/accounts/v1/installations.proto @@ -133,8 +133,14 @@ message InstallSolutionRequest { string root_scope_label = 6 [(buf.validate.field).string.max_len = 512]; // The least-privilege role granted to the agent at the root node. This is // deliberately NOT the full ":catalog" role; widening is a separate, - // audited admin action. - string role_id = 7 [(buf.validate.field).string.uuid = true]; + // audited admin action. Empty when the delivered module declares its own + // installation role: the host selects a named one, and "none" grants the + // agent no standing role at all. A role given for "none" is refused, and an + // empty one is refused for a target that declares no installation role. + string role_id = 7 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; // The accountable human of record. Must be a current org admin. Empty defaults // to the calling admin. string owner_principal_id = 8 [ diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 70a52aba86601fcff06e183974853708df00837f..257438d435624ec885abbb37d9bf4a00b58aba6b 100644 GIT binary patch delta 15118 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 delta 14714 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 U{95AI3cphP!eaB6Wy`Yv58kGP=>Px# diff --git a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto index 61af98331..44d793e14 100644 --- a/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto +++ b/module/contracts/api/accounts/connect/proto/saas/accounts/v1/installations.proto @@ -133,8 +133,14 @@ message InstallSolutionRequest { string root_scope_label = 6 [(buf.validate.field).string.max_len = 512]; // The least-privilege role granted to the agent at the root node. This is // deliberately NOT the full ":catalog" role; widening is a separate, - // audited admin action. - string role_id = 7 [(buf.validate.field).string.uuid = true]; + // audited admin action. Empty when the delivered module declares its own + // installation role: the host selects a named one, and "none" grants the + // agent no standing role at all. A role given for "none" is refused, and an + // empty one is refused for a target that declares no installation role. + string role_id = 7 [ + (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE, + (buf.validate.field).string.uuid = true + ]; // The accountable human of record. Must be a current org admin. Empty defaults // to the calling admin. string owner_principal_id = 8 [ diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 1202431b6..e65f99eec 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:d6a4564228c0a8ee8957d48d0413c642500f03373d14a2ead4d64d0536bbcbe7", + "digest": "sha256:606e52170c2e55688ce163eb1d54c347f0d19ac40915d7fb6252f2034b787c0e", "services": [ { "name": "ModuleCapabilitiesService", @@ -66,7 +66,7 @@ "kind": "protobuf", "package": "", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:d6a4564228c0a8ee8957d48d0413c642500f03373d14a2ead4d64d0536bbcbe7", + "digest": "sha256:606e52170c2e55688ce163eb1d54c347f0d19ac40915d7fb6252f2034b787c0e", "services": [ { "name": "ArtifactService", diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index e115c475e..75742ac6a 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:d6a4564228c0a8ee8957d48d0413c642500f03373d14a2ead4d64d0536bbcbe7 + digest: sha256:606e52170c2e55688ce163eb1d54c347f0d19ac40915d7fb6252f2034b787c0e - endpoint: connect kind: protobuf package: "" path: contracts/api/accounts/connect/contract.binpb - digest: sha256:d6a4564228c0a8ee8957d48d0413c642500f03373d14a2ead4d64d0536bbcbe7 + digest: sha256:606e52170c2e55688ce163eb1d54c347f0d19ac40915d7fb6252f2034b787c0e - name: auth-gateway endpoints: [grpc, rest] api-contracts: 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 7841419b6..c71813471 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:d6a4564228c0a8ee8957d48d0413c642500f03373d14a2ead4d64d0536bbcbe7 + contract-digest: sha256:606e52170c2e55688ce163eb1d54c347f0d19ac40915d7fb6252f2034b787c0e services: - AccessibleScopeService - AuditService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts index 6552c6c36..abe997559 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/saas/accounts/v1/installations_pb.ts @@ -19,7 +19,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file saas/accounts/v1/installations.proto. */ export const file_saas_accounts_v1_installations: GenFile = /*@__PURE__*/ - fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEitwQKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEEhAKCHJldmlzaW9uGA4gASgEEhMKC3NldHVwX3N0YXRlGA8gASgJEhUKDXNldHVwX21lc3NhZ2UYECABKAlCDQoLX3Jldm9rZWRfYXRKBAgEEAVSE3NvbHV0aW9uX2lkZW50aWZpZXIimQQKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSGQoHcm9sZV9pZBgHIAEoCUIIukgFcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQESNwoIYXBwcm92YWwYDiABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWxCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIq0CChdSZXZpZXdlZFNvbHV0aW9uSW5zdGFsbBIOCgZvcmdfaWQYASABKAkSEAoIYWN0b3JfaWQYAiABKAkSEQoJdGFyZ2V0X2lkGAMgASgJEg4KBnRhcmdldBgEIAEoCRISCgpiaW5kaW5nX2lkGAUgASgJEg8KB3JlbGVhc2UYBiABKAkSFwoPZGVsaXZlcnlfZGlnZXN0GAcgASgJEjcKCGFwcHJvdmFsGAggASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhkKEWluc3RhbGxhdGlvbl9yb2xlGAkgASgJEjsKB21vZHVsZXMYCiADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJldmlld2VkU3VwcG9ydGluZ01vZHVsZSKiAgoYUmV2aWV3ZWRTdXBwb3J0aW5nTW9kdWxlEhEKCXByaW5jaXBhbBgBIAEoCRIRCgl0YXJnZXRfaWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhIKCmJpbmRpbmdfaWQYBCABKAkSDwoHcmVsZWFzZRgFIAEoCRIXCg9kZWxpdmVyeV9kaWdlc3QYBiABKAkSGQoRaW5zdGFsbGF0aW9uX3JvbGUYByABKAkSHQoVcmV1c2VfaW5zdGFsbGF0aW9uX2lkGAggASgJEg0KBXNjb3BlGAkgASgJEjcKCGFwcHJvdmFsGAogASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhAKCHJlcXVpcmVkGAsgASgIIlUKHFJldmlld1NvbHV0aW9uSW5zdGFsbFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCgl0YXJnZXRfaWQYAiABKAlCCLpIBXIDsAEBIn0KHkluc3RhbGxSZXZpZXdlZFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEkEKBnJldmlldxgCIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGxCBrpIA8gBASJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyrhUKE0luc3RhbGxhdGlvblNlcnZpY2USxgEKFVJldmlld1NvbHV0aW9uSW5zdGFsbBIuLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3U29sdXRpb25JbnN0YWxsUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGwiUsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwImOgEqIiEvdjEvc29sdXRpb24taW5zdGFsbGF0aW9ucy9yZXZpZXcSuAEKF0luc3RhbGxSZXZpZXdlZFNvbHV0aW9uEjAuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsUmV2aWV3ZWRTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJLwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSARQA1gDYAGC0+STAh86ASoiGi92MS9zb2x1dGlvbi1pbnN0YWxsYXRpb25zEroBCg9JbnN0YWxsU29sdXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJdwvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLmNyZWF0ZWQQAkABSARQA1gDYAGC0+STAhY6ASoiES92MS9pbnN0YWxsYXRpb25zEtIBChFVbmluc3RhbGxTb2x1dGlvbhIqLnNhYXMuYWNjb3VudHMudjEuVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InnC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV2b2tlZBACQAFIBFADWANgAYLT5JMCMjoBKiItL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dW5pbnN0YWxsEtIBChJOYXJyb3dJbnN0YWxsYXRpb24SKy5zYWFzLmFjY291bnRzLnYxLk5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkid8LzGD4IAhAEKgwKBm9yZ19pZBACGAEwAToeChpzYWFzLmluc3RhbGxhdGlvbi5uYXJyb3dlZBACQAFIBFADWANgAYLT5JMCLzoBKiIqL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06bmFycm93EtYBChFSZXRyeUluc3RhbGxhdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuUmV0cnlJbnN0YWxsYXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24idcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXRyaWVkEAJAAUgEUANYA2ABgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpyZXRyeRKJAgodVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXASNi5zYWFzLmFjY291bnRzLnYxLlRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIo8BwvMYSwgCEAQqDAoGb3JnX2lkEAIYATABOisKJ3NhYXMuaW5zdGFsbGF0aW9uLm93bmVyc2hpcF90cmFuc2ZlcnJlZBACQAFIBFADWANgAYLT5JMCOjoBKiI1L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dHJhbnNmZXJPd25lcnNoaXASuQEKD0dldEluc3RhbGxhdGlvbhIoLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVzcG9uc2UiUcLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIlEiMvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfRKGAQoRTGlzdEluc3RhbGxhdGlvbnMSKi5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBChZMaXN0QXZhaWxhYmxlU29sdXRpb25zEi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1Jlc3BvbnNlIk3C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCIRIfL3YxL2luc3RhbGxhdGlvbnM6bGlzdEF2YWlsYWJsZRLRAQodTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnMSNi5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBo3LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZSI/wvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhMSES92MS9pbnN0YWxsYXRpb25zEvABChdHcmFudEluc3RhbGxhdGlvblRvVGVhbRIwLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlc3BvbnNlInDC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5ncmFudGVkEAJAAUgEUANYA2ABeAKC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zEu0BChpSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbRIzLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IoEBwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUucmV2b2tlZBACQAFIBFADWANgAXgCgtPkkwI/OgEqIjovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcy97dGVhbV9pZH06cmV2b2tlYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_host_installation_v1_installation, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); + fileDesc("CiRzYWFzL2FjY291bnRzL3YxL2luc3RhbGxhdGlvbnMucHJvdG8SEHNhYXMuYWNjb3VudHMudjEitwQKDEluc3RhbGxhdGlvbhIUCgJpZBgBIAEoCUIIukgFcgOwAQESGAoGb3JnX2lkGAIgASgJQgi6SAVyA7ABARIkChJhZ2VudF9wcmluY2lwYWxfaWQYAyABKAlCCLpIBXIDsAEBEiQKEm93bmVyX3ByaW5jaXBhbF9pZBgFIAEoCUIIukgFcgOwAQESHgoWY29fb3duZXJfcHJpbmNpcGFsX2lkcxgGIAMoCRIkChJyb290X3Njb3BlX25vZGVfaWQYByABKAlCCLpIBXIDsAEBEjQKBnN0YXR1cxgIIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uU3RhdHVzEi4KCmNyZWF0ZWRfYXQYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjMKCnJldm9rZWRfYXQYCiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSACIAQESHgoJdGFyZ2V0X2lkGAsgASgJQgu6SAjYAQFyA7ABARIgCg5yZXZva2VkX3JlYXNvbhgMIAEoCUIIukgFcgMYgAQSIAoOZmFpbHVyZV9yZWFzb24YDSABKAlCCLpIBXIDGIAEEhAKCHJldmlzaW9uGA4gASgEEhMKC3NldHVwX3N0YXRlGA8gASgJEhUKDXNldHVwX21lc3NhZ2UYECABKAlCDQoLX3Jldm9rZWRfYXRKBAgEEAVSE3NvbHV0aW9uX2lkZW50aWZpZXIinAQKFkluc3RhbGxTb2x1dGlvblJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIkChBhZ2VudF9pZGVudGlmaWVyGAIgASgJQgq6SAdyBRABGIAEEh4KDGRpc3BsYXlfbmFtZRgEIAEoCUIIukgFcgMYgAQSIgoQcm9vdF9zY29wZV9sYWJlbBgGIAEoCUIIukgFcgMYgAQSHAoHcm9sZV9pZBgHIAEoCUILukgI2AEBcgOwAQESJwoSb3duZXJfcHJpbmNpcGFsX2lkGAggASgJQgu6SAjYAQFyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAkgAygJQg+6SAySAQkQICIFcgOwAQESKgoRYWxsb3dlZF9hdWRpZW5jZXMYCiADKAlCD7pIDJIBCRBAIgVyAxiABBInCg5hbGxvd2VkX3Njb3BlcxgLIAMoCUIPukgMkgEJEEAiBXIDGIABEhsKCXRhcmdldF9pZBgMIAEoCUIIukgFcgOwAQESHwoFc2NvcGUYDSABKAlCC7pICHIGEAIogIBASACIAQESNwoIYXBwcm92YWwYDiABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWxCCAoGX3Njb3BlSgQIAxAESgQIBRAGUhNzb2x1dGlvbl9pZGVudGlmaWVyUg9yb290X3Njb3BlX3BhdGgidAoZTmFycm93SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQESGgoFc2NvcGUYAyABKAlCC7pICHIGEAIogIBAIlcKGFJldHJ5SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEiVwoYVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABASK+AQokVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXBSZXF1ZXN0EhgKBm9yZ19pZBgBIAEoCUIIukgFcgOwAQESIQoPaW5zdGFsbGF0aW9uX2lkGAIgASgJQgi6SAVyA7ABARIoChZuZXdfb3duZXJfcHJpbmNpcGFsX2lkGAMgASgJQgi6SAVyA7ABARIvChZjb19vd25lcl9wcmluY2lwYWxfaWRzGAQgAygJQg+6SAySAQkQICIFcgOwAQEiVQoWR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEiEKD2luc3RhbGxhdGlvbl9pZBgCIAEoCUIIukgFcgOwAQEihQEKF0dldEluc3RhbGxhdGlvblJlc3BvbnNlEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoIp0BChhMaXN0SW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARI0CgZzdGF0dXMYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN0YXR1cxIdCglwYWdlX3NpemUYAyABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgEIAEoCSKBAQoTSW5zdGFsbGF0aW9uU3VtbWFyeRI0CgxpbnN0YWxsYXRpb24YASABKAsyHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbhI0CgZoZWFsdGgYAiABKA4yJC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbkhlYWx0aCJyChlMaXN0SW5zdGFsbGF0aW9uc1Jlc3BvbnNlEjwKDWluc3RhbGxhdGlvbnMYASADKAsyJS5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvblN1bW1hcnkSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIq0CChdSZXZpZXdlZFNvbHV0aW9uSW5zdGFsbBIOCgZvcmdfaWQYASABKAkSEAoIYWN0b3JfaWQYAiABKAkSEQoJdGFyZ2V0X2lkGAMgASgJEg4KBnRhcmdldBgEIAEoCRISCgpiaW5kaW5nX2lkGAUgASgJEg8KB3JlbGVhc2UYBiABKAkSFwoPZGVsaXZlcnlfZGlnZXN0GAcgASgJEjcKCGFwcHJvdmFsGAggASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhkKEWluc3RhbGxhdGlvbl9yb2xlGAkgASgJEjsKB21vZHVsZXMYCiADKAsyKi5zYWFzLmFjY291bnRzLnYxLlJldmlld2VkU3VwcG9ydGluZ01vZHVsZSKiAgoYUmV2aWV3ZWRTdXBwb3J0aW5nTW9kdWxlEhEKCXByaW5jaXBhbBgBIAEoCRIRCgl0YXJnZXRfaWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhIKCmJpbmRpbmdfaWQYBCABKAkSDwoHcmVsZWFzZRgFIAEoCRIXCg9kZWxpdmVyeV9kaWdlc3QYBiABKAkSGQoRaW5zdGFsbGF0aW9uX3JvbGUYByABKAkSHQoVcmV1c2VfaW5zdGFsbGF0aW9uX2lkGAggASgJEg0KBXNjb3BlGAkgASgJEjcKCGFwcHJvdmFsGAogASgLMiUuaG9zdC5pbnN0YWxsYXRpb24udjEuQ29uc2VudEFwcHJvdmFsEhAKCHJlcXVpcmVkGAsgASgIIlUKHFJldmlld1NvbHV0aW9uSW5zdGFsbFJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIbCgl0YXJnZXRfaWQYAiABKAlCCLpIBXIDsAEBIn0KHkluc3RhbGxSZXZpZXdlZFNvbHV0aW9uUmVxdWVzdBIYCgZvcmdfaWQYASABKAlCCLpIBXIDsAEBEkEKBnJldmlldxgCIAEoCzIpLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGxCBrpIA8gBASJzCiRMaXN0T3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSLIAwoYT3JnYW5pemF0aW9uSW5zdGFsbGF0aW9uEjQKDGluc3RhbGxhdGlvbhgBIAEoCzIeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uEjQKBmhlYWx0aBgCIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uSGVhbHRoEjMKBnRhcmdldBgDIAEoCzIjLnNhYXMuYWNjb3VudHMudjEuQXZhaWxhYmxlU29sdXRpb24SFwoPcm9vdF9zY29wZV9wYXRoGAQgASgJEjwKDWdyYW50ZWRfdGVhbXMYBSADKAsyJS5zYWFzLmFjY291bnRzLnYxLkNvbGxlY3Rpb25SZWFkR3JhbnQSPgoPaW5oZXJpdGVkX3RlYW1zGAYgAygLMiUuc2Fhcy5hY2NvdW50cy52MS5Db2xsZWN0aW9uUmVhZEdyYW50EjoKCXJlYWRpbmVzcxgHIAEoCzInLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uUmVhZGluZXNzEjgKD3JlYWNoZWRfbW9kdWxlcxgIIAMoCzIfLnNhYXMuYWNjb3VudHMudjEuUmVhY2hlZE1vZHVsZSLtAQoSUmVhZGluZXNzQ29uZGl0aW9uEjYKBGtpbmQYASABKA4yKC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSDwoHc3ViamVjdBgCIAEoCRIOCgZkZXRhaWwYAyABKAkSOAoKY2xlYXJlZF9ieRgEIAEoDjIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ2xlYXJlZEJ5EjEKBmFjdGlvbhgFIAEoDjIhLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQWN0aW9uEhEKCXNvdXJjZV9pZBgGIAEoCSJbChVJbnN0YWxsYXRpb25SZWFkaW5lc3MSDQoFcmVhZHkYASABKAgSMwoFdW5tZXQYAiADKAsyJC5zYWFzLmFjY291bnRzLnYxLlJlYWRpbmVzc0NvbmRpdGlvbiJPCg1SZWFjaGVkTW9kdWxlEhAKCGF1ZGllbmNlGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRITCgtyb3V0ZV9hbGlhcxgDIAEoCSKDAQolTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZRJBCg1pbnN0YWxsYXRpb25zGAEgAygLMiouc2Fhcy5hY2NvdW50cy52MS5Pcmdhbml6YXRpb25JbnN0YWxsYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpMBCh5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBInUKE0NhcnJpZWRNb2R1bGVBY2Nlc3MSEAoIYXVkaWVuY2UYASABKAkSFwoPaW5zdGFsbGF0aW9uX2lkGAIgASgJEhIKCnNjb3BlX3BhdGgYAyABKAkSDwoHcm9sZV9pZBgEIAEoCRIOCgZzY29wZXMYBSADKAkiuwEKH0dyYW50SW5zdGFsbGF0aW9uVG9UZWFtUmVzcG9uc2USKwoFZ3JhbnQYASABKAsyHC5zYWFzLmFjY291bnRzLnYxLlNjb3BlR3JhbnQSNgoHY2FycmllZBgCIAMoCzIlLnNhYXMuYWNjb3VudHMudjEuQ2FycmllZE1vZHVsZUFjY2VzcxIzCgV1bm1ldBgDIAMoCzIkLnNhYXMuYWNjb3VudHMudjEuUmVhZGluZXNzQ29uZGl0aW9uIpYBCiFSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbVJlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIhCg9pbnN0YWxsYXRpb25faWQYAiABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBEhkKB3JvbGVfaWQYBCABKAlCCLpIBXIDsAEBIoQCChFBdmFpbGFibGVTb2x1dGlvbhIbCgl0YXJnZXRfaWQYASABKAlCCLpIBXIDsAEBEhIKCmJpbmRpbmdfaWQYAiABKAkSEwoLcm91dGVfYWxpYXMYAyABKAkSGQoRcmVsZWFzZV9wdWJsaXNoZXIYBCABKAkSFAoMcmVsZWFzZV9uYW1lGAUgASgJEhcKD3JlbGVhc2VfdmVyc2lvbhgGIAEoCRIZChFvcGVuZWRfZ2VuZXJhdGlvbhgHIAEoBBIaChJhcHBsaWVkX2dlbmVyYXRpb24YCCABKAQSEQoJaW5zdGFsbGVkGAkgASgIEhUKBGtpbmQYCiABKAlCB7pIBHICGCAibAodTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1JlcXVlc3QSGAoGb3JnX2lkGAEgASgJQgi6SAVyA7ABARIdCglwYWdlX3NpemUYAiABKAVCCrpIBxoFGPQDKAASEgoKcGFnZV90b2tlbhgDIAEoCSJxCh5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVzcG9uc2USNgoJc29sdXRpb25zGAEgAygLMiMuc2Fhcy5hY2NvdW50cy52MS5BdmFpbGFibGVTb2x1dGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkquwEKEkluc3RhbGxhdGlvblN0YXR1cxIjCh9JTlNUQUxMQVRJT05fU1RBVFVTX1VOU1BFQ0lGSUVEEAASHgoaSU5TVEFMTEFUSU9OX1NUQVRVU19BQ1RJVkUQARIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1JFVk9LRUQQAhIfChtJTlNUQUxMQVRJT05fU1RBVFVTX1BFTkRJTkcQAxIeChpJTlNUQUxMQVRJT05fU1RBVFVTX0ZBSUxFRBAEKoQCChJJbnN0YWxsYXRpb25IZWFsdGgSIwofSU5TVEFMTEFUSU9OX0hFQUxUSF9VTlNQRUNJRklFRBAAEh8KG0lOU1RBTExBVElPTl9IRUFMVEhfSEVBTFRIWRABEikKJUlOU1RBTExBVElPTl9IRUFMVEhfTk9fRUxJR0lCTEVfT1dORVIQAhIlCiFJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX1JFVk9LRUQQAxImCiJJTlNUQUxMQVRJT05fSEVBTFRIX0FHRU5UX0RJU0FCTEVEEAQSLgoqSU5TVEFMTEFUSU9OX0hFQUxUSF9TVEFORElOR19HUkFOVF9NSVNTSU5HEAUq8wMKFlJlYWRpbmVzc0NvbmRpdGlvbktpbmQSKAokUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASMQotUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9QRU5ESU5HEAESMAosUkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0lOU1RBTExfU1RFUF9GQUlMRUQQAhItCilSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfUkVDT1JEX1VOSEVBTFRIWRADEjUKMVJFQURJTkVTU19DT05ESVRJT05fS0lORF9NT0RVTEVfUFJJTkNJUEFMX09NSVRURUQQBBI2CjJSRUFESU5FU1NfQ09ORElUSU9OX0tJTkRfT1BFUkFUSU9OX0JJTkRJTkdfT01JVFRFRBAFEjEKLVJFQURJTkVTU19DT05ESVRJT05fS0lORF9TT1VSQ0VfTk9UX0RFTEVHQVRFRBAGEjkKNVJFQURJTkVTU19DT05ESVRJT05fS0lORF9SRUFDSEVEX01PRFVMRV9OT1RfSU5TVEFMTEVEEAcSPgo6UkVBRElORVNTX0NPTkRJVElPTl9LSU5EX0NBUlJJRURfTU9EVUxFX0FDQ0VTU19OT1RfREVSSVZFRBAIKtUBChJSZWFkaW5lc3NDbGVhcmVkQnkSJAogUkVBRElORVNTX0NMRUFSRURfQllfVU5TUEVDSUZJRUQQABIfChtSRUFESU5FU1NfQ0xFQVJFRF9CWV9WSUVXRVIQARIrCidSRUFESU5FU1NfQ0xFQVJFRF9CWV9PUkdBTklaQVRJT05fQURNSU4QAhIqCiZSRUFESU5FU1NfQ0xFQVJFRF9CWV9QTEFURk9STV9PUEVSQVRPUhADEh8KG1JFQURJTkVTU19DTEVBUkVEX0JZX05PQk9EWRAEKp8CCg9SZWFkaW5lc3NBY3Rpb24SIAocUkVBRElORVNTX0FDVElPTl9VTlNQRUNJRklFRBAAEikKJVJFQURJTkVTU19BQ1RJT05fTk9ORV9BVF9USElTX1NVUkZBQ0UQARInCiNSRUFESU5FU1NfQUNUSU9OX1JFVFJZX0lOU1RBTExfU1RFUBACEiUKIVJFQURJTkVTU19BQ1RJT05fUkVDT05ORUNUX1NPVVJDRRADEicKI1JFQURJTkVTU19BQ1RJT05fVFJBTlNGRVJfT1dORVJTSElQEAQSKwonUkVBRElORVNTX0FDVElPTl9JTlNUQUxMX1JFQUNIRURfTU9EVUxFEAUSGQoVUkVBRElORVNTX0FDVElPTl9XQUlUEAYyrhUKE0luc3RhbGxhdGlvblNlcnZpY2USxgEKFVJldmlld1NvbHV0aW9uSW5zdGFsbBIuLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3U29sdXRpb25JbnN0YWxsUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuUmV2aWV3ZWRTb2x1dGlvbkluc3RhbGwiUsLzGCIIAhAEKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwImOgEqIiEvdjEvc29sdXRpb24taW5zdGFsbGF0aW9ucy9yZXZpZXcSuAEKF0luc3RhbGxSZXZpZXdlZFNvbHV0aW9uEjAuc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsUmV2aWV3ZWRTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJLwvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSARQA1gDYAGC0+STAh86ASoiGi92MS9zb2x1dGlvbi1pbnN0YWxsYXRpb25zEroBCg9JbnN0YWxsU29sdXRpb24SKC5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxTb2x1dGlvblJlcXVlc3QaHi5zYWFzLmFjY291bnRzLnYxLkluc3RhbGxhdGlvbiJdwvMYPQgCEAQqDAoGb3JnX2lkEAIYATABOh0KGXNhYXMuaW5zdGFsbGF0aW9uLmNyZWF0ZWQQAkABSARQA1gDYAGC0+STAhY6ASoiES92MS9pbnN0YWxsYXRpb25zEtIBChFVbmluc3RhbGxTb2x1dGlvbhIqLnNhYXMuYWNjb3VudHMudjEuVW5pbnN0YWxsU29sdXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5InnC8xg9CAIQBCoMCgZvcmdfaWQQAhgBMAE6HQoZc2Fhcy5pbnN0YWxsYXRpb24ucmV2b2tlZBACQAFIBFADWANgAYLT5JMCMjoBKiItL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dW5pbnN0YWxsEtIBChJOYXJyb3dJbnN0YWxsYXRpb24SKy5zYWFzLmFjY291bnRzLnYxLk5hcnJvd0luc3RhbGxhdGlvblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkid8LzGD4IAhAEKgwKBm9yZ19pZBACGAEwAToeChpzYWFzLmluc3RhbGxhdGlvbi5uYXJyb3dlZBACQAFIBFADWANgAYLT5JMCLzoBKiIqL3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06bmFycm93EtYBChFSZXRyeUluc3RhbGxhdGlvbhIqLnNhYXMuYWNjb3VudHMudjEuUmV0cnlJbnN0YWxsYXRpb25SZXF1ZXN0Gh4uc2Fhcy5hY2NvdW50cy52MS5JbnN0YWxsYXRpb24idcLzGD0IAhAEKgwKBm9yZ19pZBACGAEwATodChlzYWFzLmluc3RhbGxhdGlvbi5yZXRyaWVkEAJAAUgEUANYA2ABgtPkkwIuOgEqIikvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfTpyZXRyeRKJAgodVHJhbnNmZXJJbnN0YWxsYXRpb25Pd25lcnNoaXASNi5zYWFzLmFjY291bnRzLnYxLlRyYW5zZmVySW5zdGFsbGF0aW9uT3duZXJzaGlwUmVxdWVzdBoeLnNhYXMuYWNjb3VudHMudjEuSW5zdGFsbGF0aW9uIo8BwvMYSwgCEAQqDAoGb3JnX2lkEAIYATABOisKJ3NhYXMuaW5zdGFsbGF0aW9uLm93bmVyc2hpcF90cmFuc2ZlcnJlZBACQAFIBFADWANgAYLT5JMCOjoBKiI1L3YxL2luc3RhbGxhdGlvbnMve2luc3RhbGxhdGlvbl9pZH06dHJhbnNmZXJPd25lcnNoaXASuQEKD0dldEluc3RhbGxhdGlvbhIoLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVxdWVzdBopLnNhYXMuYWNjb3VudHMudjEuR2V0SW5zdGFsbGF0aW9uUmVzcG9uc2UiUcLzGCIIAhADKgwKBm9yZ19pZBACGAEwAToCEAFAAUgDUANYA2ABgtPkkwIlEiMvdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfRKGAQoRTGlzdEluc3RhbGxhdGlvbnMSKi5zYWFzLmFjY291bnRzLnYxLkxpc3RJbnN0YWxsYXRpb25zUmVxdWVzdBorLnNhYXMuYWNjb3VudHMudjEuTGlzdEluc3RhbGxhdGlvbnNSZXNwb25zZSIYwvMYFAgDEAEwAToCEAFAAUgHUANYA2ABEsoBChZMaXN0QXZhaWxhYmxlU29sdXRpb25zEi8uc2Fhcy5hY2NvdW50cy52MS5MaXN0QXZhaWxhYmxlU29sdXRpb25zUmVxdWVzdBowLnNhYXMuYWNjb3VudHMudjEuTGlzdEF2YWlsYWJsZVNvbHV0aW9uc1Jlc3BvbnNlIk3C8xgiCAIQBCoMCgZvcmdfaWQQAhgBMAE6AhABQAFIA1ADWANgAYLT5JMCIRIfL3YxL2luc3RhbGxhdGlvbnM6bGlzdEF2YWlsYWJsZRLRAQodTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnMSNi5zYWFzLmFjY291bnRzLnYxLkxpc3RPcmdhbml6YXRpb25JbnN0YWxsYXRpb25zUmVxdWVzdBo3LnNhYXMuYWNjb3VudHMudjEuTGlzdE9yZ2FuaXphdGlvbkluc3RhbGxhdGlvbnNSZXNwb25zZSI/wvMYIggCEAQqDAoGb3JnX2lkEAIYATABOgIQAUABSANQA1gDYAGC0+STAhMSES92MS9pbnN0YWxsYXRpb25zEvABChdHcmFudEluc3RhbGxhdGlvblRvVGVhbRIwLnNhYXMuYWNjb3VudHMudjEuR3JhbnRJbnN0YWxsYXRpb25Ub1RlYW1SZXF1ZXN0GjEuc2Fhcy5hY2NvdW50cy52MS5HcmFudEluc3RhbGxhdGlvblRvVGVhbVJlc3BvbnNlInDC8xg4CAIQBCoMCgZvcmdfaWQQAhgBMAE6FgoSc2Fhcy5zY29wZS5ncmFudGVkEAJAAUgEUANYA2ABeAKC0+STAi46ASoiKS92MS9pbnN0YWxsYXRpb25zL3tpbnN0YWxsYXRpb25faWR9L3RlYW1zEu0BChpSZXZva2VJbnN0YWxsYXRpb25Gcm9tVGVhbRIzLnNhYXMuYWNjb3VudHMudjEuUmV2b2tlSW5zdGFsbGF0aW9uRnJvbVRlYW1SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IoEBwvMYOAgCEAQqDAoGb3JnX2lkEAIYATABOhYKEnNhYXMuc2NvcGUucmV2b2tlZBACQAFIBFADWANgAXgCgtPkkwI/OgEqIjovdjEvaW5zdGFsbGF0aW9ucy97aW5zdGFsbGF0aW9uX2lkfS90ZWFtcy97dGVhbV9pZH06cmV2b2tlYgZwcm90bzM", [file_buf_validate_validate, file_google_api_annotations, file_google_protobuf_empty, file_host_installation_v1_installation, file_google_protobuf_timestamp, file_saas_accounts_v1_authorization, file_saas_policy_v1_options]); /** * Installation is one solution's bound identity in an org. Server-assigned ids; @@ -177,7 +177,10 @@ export type InstallSolutionRequest = Message<"saas.accounts.v1.InstallSolutionRe /** * The least-privilege role granted to the agent at the root node. This is * deliberately NOT the full ":catalog" role; widening is a separate, - * audited admin action. + * audited admin action. Empty when the delivered module declares its own + * installation role: the host selects a named one, and "none" grants the + * agent no standing role at all. A role given for "none" is refused, and an + * empty one is refused for a target that declares no installation role. * * @generated from field: string role_id = 7; */ From fd4c3747af33caddee85b5bcf8c63f9ac340958f Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:34:24 -0400 Subject: [PATCH 3/4] fix(frontend): only NotFound means a module declares no roles; module pages grant teams in Setup Review of #1284. - useRolesDeclaredReview: only the offer review's NotFound means a module declares no roles. Any other failure leaves the answer unknown, and the install page shows it with Try again instead of opening the agent-role form for what may be a module page. useRolesDeclared keeps its contract. - Setup offers a module page no staged team-per-role picker. The picker binds to a solution's consent digest, and a module page's offer has none, so it could only stay disabled. Its roles take teams through Setup's own role grant form. The setup test that gave a module offer a digest now uses the shape the authority sends. - The host conformance suite now installs a module page whose installation_role is a named role with an empty role_id, as the review sends it, and asserts that the agent's one standing grant is that declared role. - AGENTS.md states the NotFound rule, the browser-side approval and the Setup path. Co-Authored-By: Claude Fable 5.1 --- .../host_interface_conformance_test.go | 27 +++- module/services/frontend/AGENTS.md | 8 +- ...installation-setup-page.container.test.tsx | 133 +++++++++++++----- .../ui/installation-setup-page.tsx | 11 +- ...tion-installations-page.container.test.tsx | 31 ++++ .../ui/solution-installations-page.tsx | 27 +++- .../ui/when-roles-declared.test.tsx | 42 +++++- .../ui/when-roles-declared.tsx | 41 ++++-- 8 files changed, 260 insertions(+), 60 deletions(-) diff --git a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go index b1bad18d5..3689fd0c4 100644 --- a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go +++ b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go @@ -1164,9 +1164,32 @@ func runHostInterfaceFixture(t *testing.T) { AgentIdentifier: target.ReleasePublisher + "/" + target.ReleaseName + ":" + target.ReleaseVersion, DisplayName: selected, RootScopeLabel: selected, RoleID: catalogue, OwnerPrincipalID: conformanceAlex}) require.ErrorContains(t, err, "cannot override "+selected+"'s installation_role declaration") - installation, _, err := svc.InstallFixtureTarget(ctx, conformanceAlex, fixtureOrganization, selected, "", conformanceAlex) - require.NoError(t, err, "the declared role installs it") + // The administrator's install with no role, as the module page review + // sends it: the signed installation_role selects the standing role. + installation, err := svc.InstallSolution(ctx, conformanceAlex, &business.InstallSolutionParams{OrgID: fixtureOrganization, TargetID: target.TargetID, + AgentIdentifier: target.ReleasePublisher + "/" + target.ReleaseName + ":" + target.ReleaseVersion, DisplayName: selected, RootScopeLabel: selected, + OwnerPrincipalID: conformanceAlex}) + require.NoError(t, err, "an empty role installs a module page as its declared installation_role") require.NoError(t, completeInstallStep(ctx, store, installation)) + var standing []string + require.NoError(t, store.WithControlPlane(ctx, func(ctx context.Context) error { + rows, err := storetx.Tx(ctx).Query(ctx, `SELECT r.derived_from FROM public.scope_grants g JOIN public.roles r ON r.id = g.role_id + WHERE g.org_id = $1 AND g.subject_kind = 'principal' AND g.subject_id = $2`, fixtureOrganization, installation.GetAgentPrincipalId()) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var from string + if err := rows.Scan(&from); err != nil { + return err + } + standing = append(standing, from) + } + return rows.Err() + })) + require.Equal(t, []string{"org=" + fixtureOrganization + ";solution=" + target.TargetID + ";role=viewer"}, standing, + "the agent's one standing grant is the declared viewer role") }) // Last, because the envelope it writes becomes every module's ceiling: a diff --git a/module/services/frontend/AGENTS.md b/module/services/frontend/AGENTS.md index 9e772afd5..be92979f2 100644 --- a/module/services/frontend/AGENTS.md +++ b/module/services/frontend/AGENTS.md @@ -470,8 +470,12 @@ permits), installed through `InstallSolution` with no agent role, because its signed `installation_role` selects one (or none), and then opened at its Setup page, where each role takes a team. The shared core installs only solutions, so a module page stays this host's installation and carries no consent tuple. A -module whose offer the authority refuses (it declares no roles) keeps the -agent-role form. +module whose offer the authority refuses NotFound (it declares no roles) keeps +the agent-role form; any other failure of that read is shown with a retry, never +taken as "no roles". The review's approval is the browser's: a module page's +offer carries no consent digest for the host to check it against. On Setup, a +module page's roles take teams through the shared role grant form; the staged +team-per-role picker binds to a solution's consent digest and is not offered. The only installer configuration is a team for each role the solution's offer declares: an existing organization team, or one created inline through diff --git a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.container.test.tsx b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.container.test.tsx index ce52967e8..c2160077e 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.container.test.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.container.test.tsx @@ -86,9 +86,20 @@ const docsInstallation = { }; it("uses shared solution roles without offering a legacy host grant", async () => { - const grants = serve({ installations: [installation({ rootScopePath: "", readiness: { ready: true, unmet: [] } })] }); + const grants = serve({ + installations: [ + installation({ + rootScopePath: "", + readiness: { ready: true, unmet: [] }, + }), + ], + }); render(); - expect(await screen.findByText(/Assign people and teams using the solution roles above/)).toBeTruthy(); + expect( + await screen.findByText( + /Assign people and teams using the solution roles above/, + ), + ).toBeTruthy(); expect(screen.queryByText("Team access on this host")).toBeNull(); expect(screen.queryByRole("button", { name: "Grant a team" })).toBeNull(); expect(grants).toHaveLength(0); @@ -119,8 +130,13 @@ function serve(installationsBody: JsonBodyType) { ), http.post(rpc("TeamService", "ListTeams"), () => HttpResponse.json(teams)), // The shared Setup UI's read; a test about it answers it itself. - http.post("http://localhost:3000/host.installation.v1.OfferService/ReviewOffer", () => - HttpResponse.json({ code: "not_found", message: '"suite" is not delivered here' }, { status: 404 }), + http.post( + "http://localhost:3000/host.installation.v1.OfferService/ReviewOffer", + () => + HttpResponse.json( + { code: "not_found", message: '"suite" is not delivered here' }, + { status: 404 }, + ), ), http.post( rpc("InstallationService", "GrantInstallationToTeam"), @@ -317,45 +333,78 @@ it("grants once, and reports what that grant carried", async () => { // authority's answer through the host installation interface, at the // organization the session is at, beside this host's own team access. it("shows the solution's roles, what they grant and who holds them, from the authority", async () => { - serve({ installations: [installation({ readiness: { ready: true, unmet: [] } })] }); + serve({ + installations: [installation({ readiness: { ready: true, unmet: [] } })], + }); const asked: { procedure: string; body: unknown }[] = []; const host = (procedure: string, answer: JsonBodyType) => - http.post(`http://localhost:3000/host.installation.v1.${procedure}`, async ({ request }) => { - asked.push({ procedure, body: await request.json() }); - return HttpResponse.json(answer); - }); + http.post( + `http://localhost:3000/host.installation.v1.${procedure}`, + async ({ request }) => { + asked.push({ procedure, body: await request.json() }); + return HttpResponse.json(answer); + }, + ); server.use( host("OfferService/ReviewOffer", { target: "suite", approval: { targetId: "suite", generation: "3", digest: "sha256:suite" }, - installation: { id: "core-suite", organizationId: "org-1", target: "suite", status: "STATUS_ACTIVE", revision: "2", role: "viewer" }, - ceiling: [{ binding: "documents", module: "documents", scopes: [{ resourceKind: "example-records", actions: ["read"] }] }], + installation: { + id: "core-suite", + organizationId: "org-1", + target: "suite", + status: "STATUS_ACTIVE", + revision: "2", + role: "viewer", + }, + ceiling: [ + { + binding: "documents", + module: "documents", + scopes: [{ resourceKind: "example-records", actions: ["read"] }], + }, + ], roles: [ { name: "viewer", scopes: [{ resourceKind: "suite.page", actions: ["read"] }], - capabilities: [{ module: "suite", operation: "GET /pages/{id}", requires: [{ resourceKind: "suite.page", actions: ["read"] }] }], + capabilities: [ + { + module: "suite", + operation: "GET /pages/{id}", + requires: [{ resourceKind: "suite.page", actions: ["read"] }], + }, + ], }, ], }), - host("InstallationService/ListRoleGrants", { grants: [{ role: "viewer", teamId: SUPPORT_TEAM }] }), + host("InstallationService/ListRoleGrants", { + grants: [{ role: "viewer", teamId: SUPPORT_TEAM }], + }), ); render(); - expect(await screen.findByRole("heading", { name: "Role: viewer" })).toBeTruthy(); + expect( + await screen.findByRole("heading", { name: "Role: viewer" }), + ).toBeTruthy(); expect(await screen.findByText("GET /pages/{id}")).toBeTruthy(); expect(await screen.findByText("viewer · Team Support")).toBeTruthy(); expect(screen.getByText("Team access on this host")).toBeTruthy(); - expect(asked[0]).toEqual({ procedure: "OfferService/ReviewOffer", body: { organizationId: "org-1", target: "suite" } }); + expect(asked[0]).toEqual({ + procedure: "OfferService/ReviewOffer", + body: { organizationId: "org-1", target: "suite" }, + }); expect(asked[1]).toEqual({ procedure: "InstallationService/ListRoleGrants", body: { organizationId: "org-1", installationId: "core-suite" }, }); }); -// A module page that declares roles is set up as a solution is: the authority -// reviews its offer, and a team is offered for each role it declares. -it("offers a module page that declares roles a team per role, as a solution", async () => { +// A module page that declares roles is set up through its roles, as a +// solution is. Its offer carries no consent digest (the authority installs no +// module), so it has no staged team picker: Setup's role grant form, the +// authority's GrantRole, is how each role takes a team. +it("sets up a module page that declares roles through Setup's role grant form", async () => { serve({ installations: [ installation({ @@ -365,26 +414,44 @@ it("offers a module page that declares roles a team per role, as a solution", as ], }); server.use( - http.post("http://localhost:3000/host.installation.v1.OfferService/ReviewOffer", () => - HttpResponse.json({ - target: "suite", - approval: { targetId: "suite", generation: "3", digest: "sha256:suite" }, - installation: { id: "core-suite", organizationId: "org-1", target: "suite", status: "STATUS_ACTIVE", revision: "2" }, - roles: [ - { name: "installer", scopes: [{ resourceKind: "suite.page", actions: ["read", "write"] }] }, - { name: "viewer", scopes: [{ resourceKind: "suite.page", actions: ["read"] }] }, - ], - }), + http.post( + "http://localhost:3000/host.installation.v1.OfferService/ReviewOffer", + () => + HttpResponse.json({ + target: "suite", + installationRole: "none", + installation: { + id: "core-suite", + organizationId: "org-1", + target: "suite", + status: "STATUS_ACTIVE", + revision: "2", + }, + roles: [ + { + name: "installer", + scopes: [ + { resourceKind: "suite.page", actions: ["read", "write"] }, + ], + }, + { + name: "viewer", + scopes: [{ resourceKind: "suite.page", actions: ["read"] }], + }, + ], + }), ), - http.post("http://localhost:3000/host.installation.v1.InstallationService/ListRoleGrants", () => - HttpResponse.json({ grants: [] }), + http.post( + "http://localhost:3000/host.installation.v1.InstallationService/ListRoleGrants", + () => HttpResponse.json({ grants: [] }), ), ); render(); - const access = await screen.findByRole("region", { name: "Team access" }); - expect(within(access).getByText(/For each role this solution declares/)).toBeTruthy(); - expect(within(access).getByText(/viewer:/)).toBeTruthy(); + expect( + await screen.findByRole("button", { name: "Grant role" }), + ).toBeTruthy(); + expect(screen.queryByRole("region", { name: "Team access" })).toBeNull(); // Its roles are the one grant form: no host team access card beside them. expect(screen.queryByText("Team access on this host")).toBeNull(); expect(screen.queryByRole("button", { name: "Grant a team" })).toBeNull(); diff --git a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx index 9289e8515..0e346d978 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/installation-setup-page.tsx @@ -201,10 +201,13 @@ function InstallationSetup({ diff --git a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx index 732b26d6f..57799688f 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.container.test.tsx @@ -1593,6 +1593,37 @@ describe("installing a module: the form follows its declared roles, not its kind expect(asked).toEqual({ organizationId: "org-1", target: "ledger" }); }); + it("shows a failed read of a module's roles instead of guessing its form, and reads again", async () => { + let reads = 0; + serve(); + server.use( + http.post(REVIEW_OFFER, () => { + reads++; + return reads === 1 + ? HttpResponse.json( + { code: "unavailable", message: "authority restarting" }, + { status: 503 }, + ) + : HttpResponse.json( + { code: "not_found", message: "declares no roles" }, + { status: 404 }, + ); + }), + ); + renderInApp(); + fireEvent.click( + await screen.findByRole("button", { name: "Install ledger" }), + ); + expect(await screen.findByText(/authority restarting/)).toBeTruthy(); + expect(screen.queryByRole("dialog")).toBeNull(); + fireEvent.click(screen.getByRole("button", { name: "Try again" })); + const dialog = await screen.findByRole("dialog"); + expect( + within(dialog).getByRole("combobox", { name: "Agent role" }), + ).toBeTruthy(); + expect(reads).toBe(2); + }); + it("reviews a module page that declares roles, installs it with no agent role, and opens its Set up", async () => { let installed: unknown; serve(); diff --git a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx index 3f41e9084..ca706efce 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/solution-installations-page.tsx @@ -72,7 +72,7 @@ import { GrantTeamDialog } from "./grant-team-dialog"; import { InstallModuleDialog } from "./install-module-dialog"; import { InstallModulePageDialog } from "./install-module-page-dialog"; import { InstallSolutionDialog } from "./install-solution-dialog"; -import { useRolesDeclared } from "./when-roles-declared"; +import { useRolesDeclaredReview } from "./when-roles-declared"; /** * One fact. A gap reads as its label in muted italics with what it means on @@ -163,10 +163,11 @@ function SolutionInstallationsForOrganization({ const combinedInstall = useInstallReviewedSolution(orgId); // A target that declares roles installs through a review of them, as a // solution does; a module that declares none keeps its agent-role form. - const rolesDeclared = useRolesDeclared( - installing?.routeAlias ?? "", - installing?.kind, - ); + const { + declared: rolesDeclared, + error: rolesUnread, + retry: readRolesAgain, + } = useRolesDeclaredReview(installing?.routeAlias ?? "", installing?.kind); const installModule = ( target: AvailableSolution, values: InstallSolutionValues, @@ -272,9 +273,23 @@ function SolutionInstallationsForOrganization({ {/* A module's roles are read before its form opens: which form is the authority's answer, not the target's kind. */} + {installing && rolesUnread !== undefined && ( + setInstalling(null)} + > + {hostRefusal(rolesUnread)} Which install form applies depends on them.{" "} + + + )} {installing?.kind === "solution" && ( ({ useAuth: () => ({ organizationId: "example-org" }), })); -import { WhenRolesDeclared } from "./when-roles-declared"; +import { + useRolesDeclaredReview, + WhenRolesDeclared, +} from "./when-roles-declared"; afterEach(() => { cleanup(); @@ -64,10 +68,44 @@ describe("WhenRolesDeclared", () => { // The authority refuses the offer of a module that declares no roles. it("shows nothing for a module whose offer the authority refuses", async () => { reviewOffer.mockRejectedValue( - new Error("not a composed solution or a module page declaring roles"), + new ConnectError( + "not a composed solution or a module page declaring roles", + Code.NotFound, + ), ); renderGate("module"); await vi.waitFor(() => expect(reviewOffer).toHaveBeenCalled()); expect(screen.queryByText("Setup")).toBeNull(); }); + + // Only NotFound means "declares no roles"; any other failure is no answer. + it.each([ + ["NotFound", Code.NotFound, "declared: false"], + ["Unavailable", Code.Unavailable, "declared: unknown, error: try later"], + ])( + "reads %s as an answer only when it is NotFound", + async (_name, code, expected) => { + reviewOffer.mockRejectedValue(new ConnectError("try later", code)); + function Probe() { + const { declared, error } = useRolesDeclaredReview( + "example-target", + "module", + ); + if (declared === undefined && !error) return reading; + return ( + + {declared === undefined + ? `declared: unknown, error: ${ConnectError.from(error).rawMessage}` + : `declared: ${declared}`} + + ); + } + render( + + + , + ); + expect(await screen.findByText(expected)).toBeTruthy(); + }, + ); }); diff --git a/module/services/frontend/code/src/features/solution-installations/ui/when-roles-declared.tsx b/module/services/frontend/code/src/features/solution-installations/ui/when-roles-declared.tsx index 48a56a938..677a8a6bd 100644 --- a/module/services/frontend/code/src/features/solution-installations/ui/when-roles-declared.tsx +++ b/module/services/frontend/code/src/features/solution-installations/ui/when-roles-declared.tsx @@ -1,6 +1,6 @@ "use client"; -import { createClient } from "@connectrpc/connect"; +import { Code, ConnectError, createClient } from "@connectrpc/connect"; import { useQuery } from "@tanstack/react-query"; import type { ReactNode } from "react"; import { OfferService } from "@/gen/host/installation/v1/installation_pb"; @@ -10,16 +10,18 @@ import { apiTransport } from "@/lib/connect/transport"; const offers = createClient(OfferService, apiTransport); /** - * Whether a target declares installation roles. A solution always does: the - * shared authority core composes none without an installer role. A module - * does only when it declares a page and roles; the authority's offer review - * is the answer, and a module it refuses (one that declares no roles) does - * not. Undefined while a module's answer is still being read. + * Whether a target declares installation roles, with the read's own failure. + * A solution always does: the shared authority core composes none without an + * installer role. A module does only when it declares a page and roles; the + * authority's offer review is the answer, and only its NotFound (a module + * that declares no roles) means it does not. Any other failure is not an + * answer: `declared` stays undefined and `error` says why, so a transient + * refusal never passes a module page off as one without roles. */ -export function useRolesDeclared( +export function useRolesDeclaredReview( target: string, kind?: string, -): boolean | undefined { +): { declared: boolean | undefined; error: unknown; retry: () => void } { const { organizationId = "" } = useAuth(); const review = useQuery({ queryKey: ["solution-declares-roles", organizationId, target], @@ -29,9 +31,26 @@ export function useRolesDeclared( retry: false, refetchOnWindowFocus: false, }); - if (kind !== "module") return kind === "solution"; - if (review.isError) return false; - return review.data; + const retry = () => void review.refetch(); + if (kind !== "module") + return { declared: kind === "solution", error: undefined, retry }; + if (review.isError) { + const declaresNone = ConnectError.from(review.error).code === Code.NotFound; + return { + declared: declaresNone ? false : undefined, + error: declaresNone ? undefined : review.error, + retry, + }; + } + return { declared: review.data, error: undefined, retry }; +} + +/** Whether a target declares installation roles; undefined while unknown. */ +export function useRolesDeclared( + target: string, + kind?: string, +): boolean | undefined { + return useRolesDeclaredReview(target, kind).declared; } /** From 2b195d76c251b88801e416b8fcbe9042713c908e Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:37:07 -0400 Subject: [PATCH 4/4] chore(saas-sdk): 0.3.42 for the regenerated optional role_id binding The SDK's generated InstallSolutionRequest binding changed with the contract regeneration, so the published package moves to a new version, as the kit-version gate requires. Co-Authored-By: Claude Fable 5.1 --- module/services/frontend/code/package-lock.json | 4 ++-- module/services/frontend/code/package.json | 2 +- module/services/frontend/code/packages/saas-sdk/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index b92cd77ff..f31666c3b 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -15,7 +15,7 @@ "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.4.4", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.41", + "@codefly-dev/saas-sdk": "0.3.42", "@codefly-dev/saas-ui": "0.13.0", "@codefly-dev/ui": "0.13.0", "@codefly/saas-plugin-manifest": "0.1.2", @@ -15742,7 +15742,7 @@ }, "packages/saas-sdk": { "name": "@codefly-dev/saas-sdk", - "version": "0.3.41", + "version": "0.3.42", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.11.0", diff --git a/module/services/frontend/code/package.json b/module/services/frontend/code/package.json index 5c4fce115..9c722c0c9 100644 --- a/module/services/frontend/code/package.json +++ b/module/services/frontend/code/package.json @@ -39,7 +39,7 @@ "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.4.4", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.41", + "@codefly-dev/saas-sdk": "0.3.42", "@codefly-dev/saas-ui": "0.13.0", "@codefly-dev/ui": "0.13.0", "@codefly/saas-plugin-manifest": "0.1.2", diff --git a/module/services/frontend/code/packages/saas-sdk/package.json b/module/services/frontend/code/packages/saas-sdk/package.json index 15472f8a5..b7791c0a9 100644 --- a/module/services/frontend/code/packages/saas-sdk/package.json +++ b/module/services/frontend/code/packages/saas-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-sdk", - "version": "0.3.41", + "version": "0.3.42", "description": "Typed Connect client, gateway-bound facades, and audit data-graph tooling for the saas-starter public API.", "repository": { "type": "git",