Skip to content

[CXH-2710] - Sync the users that enterprise role grants name - GitHub Connector - #195

Closed
mateoHernandez123 wants to merge 2 commits into
mainfrom
mateoHernandez123/fix-dangling-enterprise-owner-users
Closed

mateoHernandez123 wants to merge 2 commits into
mainfrom
mateoHernandez123/fix-dangling-enterprise-owner-users

Conversation

@mateoHernandez123

@mateoHernandez123 mateoHernandez123 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Description

  • Bug fix
  • New feature

CXH-2710 reports a grant that points at a user the sync never emits. With --enterprises set, the connector emits an Owner grant for every owner of the enterprise. It emits user resources only for members of the configured organization. An owner outside that organization therefore gets a grant whose principal has no resource row.

The SDK reports ingest invariant I9 on every sync. A local grant against that principal exits 5 with pebble: not found, because the bundle does not contain the principal. QA found this on baton-github-enterprise v0.1.1, which vendors baton-github v0.4.8.

This change makes the user syncer emit the principals that enterprise-role grants name. It adds no configuration and no new credential scope.

Sync:

  • Users (user) — after an organization's member pages finish, the syncer walks the enterprise and emits the principals its grants name. Under GitHub App authentication these are the owners of the enterprise and the enterprise members that hold a pending Owner invitation. Under a personal access token these are the consumed-licenses users that hold an enterprise role. The resource ID is the GitHub database ID the grant already uses. These users get no organization membership.
  • Enterprise roles (enterprise_role) — unchanged surface. The walk runs only when C1 selects this type, because without it no grant names these principals.
  • Organizations (org), Teams (team), Repositories (repository), Organization roles (org_role), Invitations (invitation), GitHub Apps (app), Licenses (license) — unchanged.

Provisioning:

Unchanged. The fix removes the cause of a failure on the existing path: a grant or a revoke against an enterprise owner outside the configured organization failed before, because the SDK could not resolve the principal in the bundle.

Auth:

Unchanged. No new flag and no new scope. The membership check calls GET /orgs/{org}/members/{login}, which needs the same Members read permission that the organization member list already needs.

flowchart TD
  members[organization member pages] --> last{last page?}
  last -->|no| members
  last -->|yes| gate{enterprise_role selected?}
  gate -->|no| done[user list ends]
  gate -->|setup error| skip[skip the phases]
  gate -->|yes| cred{credential?}
  cred -->|GitHub App| owners[Owners, then pending invitees]
  cred -->|token| licenses[licensed role holders]
  owners --> check{already an organization member?}
  licenses --> check
  check -->|yes| keep[keep the member-page user]
  check -->|no| emit[emit the user]
Loading

The walk runs only when C1 selects enterprise_role. A setup error skips the phases, and the role type skips its grants for the same reason, so no grant is left without a user. A principal an organization already emits is left alone, because a second write would drop the SAML identity the member page resolved.

Architecture highlights:

  • The syncer keeps no state between pages. Each page resolves what it needs and passes it down. A hosted deployment can run each page in a different process, so state on the builder would be rebuilt anyway.
  • A principal is skipped only when an organization in this sync already emits it. The syncer asks GET /orgs/{org}/members/{login} per principal. Member pages resolve the user's SAML identity, and the resource store keeps the last write, so a second emission would drop that identity. The cost follows the number of enterprise administrators, not the size of the organization.
  • Which organizations count is decided by the rule the organization syncer applies: the ones the credential administers. A shared administersOrg helper now serves both. Counting an organization this sync leaves out would drop the user and restore the defect.
  • Every configured organization runs the walk at the end of its members. Repeating it emits the same records, so the result does not depend on which organization ran it or on state that survives a resumed sync.
  • Queuing the phases reads the enterprise, so the page can fail on one that is unreadable. NotFound is converted at the boundary. The SDK treats NotFound as a warning and completes the sync, which would drop the members the page already fetched and make C1 delete them.
  • The emission is gated on ConnectorOpts.WillSyncResourceType("enterprise_role"), threaded as a resolved bool. enterprise_role is absent from the published capability set, so C1 leaves it out of a new connector's default selection and an admin enables it by hand.
  • The token path emits every licensed enterprise member, because Member is the role GitHub reports for every license-consuming account and the role sync emits a grant for each one. docs/docs-info.md states the cost and asks the operator to budget for it.

Useful links:

Co-authored-by: Cursor <cursoragent@cursor.com>
@linear-code

linear-code Bot commented Oct 14, 2026

Copy link
Copy Markdown

CXH-2710

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 9e0cf67090d1

Connector PR Review: [CXH-2710] - Sync the users that enterprise role grants name - GitHub Connector

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base c5250139e2d3.
Review mode: full
View review run

Review Summary

The user syncer's page token is now a pagination.Bag. When an org's member pages finish, and only if --enterprises is set and ConnectorOpts.WillSyncResourceType("enterprise_role") is true, the syncer queues per-enterprise phases. The App path walks Owners and pending Owner invitees; the PAT path walks consumed-licenses users that hold a role. It emits users keyed by the same database ID the role grants use, and skips any account GET /orgs/{org}/members/{login} shows an org in this sync already emits. The org-admin check moved into a shared administersOrg helper, and its behavior is unchanged.

I scanned the full PR diff for security and correctness. Every error path in the new phases is returned, not swallowed. NotFound becomes FailedPrecondition through failClosedOnUnreadableEnterprise (enterprise_users.go:219, :347). The only 404 tolerated is in userFromDatabaseID, and there the principal is still emitted rather than dropped, so it is consistent with R7/S1–S6. Pagination is driven one page per call through the bag (F2/R8). The tests cover phase ordering, idempotent re-walks, the PAT path, filtering to administered orgs, and fail-closed on NotFound.

The repo-local criteria loaded and I applied them. Section S (empty-result data loss) and L1/L3 (log levels) both pass: skip and degrade lines are Debug. I found no new endpoint in an existing sync path that needs a new scope (B7/B8): orgs/{org}/members/{login} and users/{id} need the existing Members/read permissions. The new user population only appears once enterprise_role is opted in, and the README and docs/docs-info.md are updated, so BP1–BP3 are satisfied. Sections C (spans), D (JSON types) and F (provisioning) don't apply because no span, response-struct or Grant/Revoke code changed.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (confidence: high), pkg/connector/enterprise_users.go:322 / docs/docs-info.md: syncedOrgs runs on every enterprise-phase page and caches nothing. With --orgs unset, every page re-lists /user/orgs and makes one membership call per org. The doc says the list "is read once per process", which doesn't match the code.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/enterprise_users.go`:
- Around line 322 (and `syncedOrgs` at ~582): `syncedOrgs` is called on every enterprise-principal page. When `u.orgs` is empty, each call re-runs `getOrgs` (it paginates `/user/orgs`) plus one `GetOrgMembership` per candidate org. Fix this one of two ways. Option 1: memoize the administered-org list on `userResourceType`, using a mutex or `sync.Once`, so a process reads it once; Lambda invocations will rebuild it, which is fine. Option 2: change the sentence in `docs/docs-info.md` ("That list is read once per process") so it describes the per-page cost.

Comment thread pkg/connector/enterprise_users.go
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 31ad030e0c48

Connector PR Review: [CXH-2710] - Sync the users that enterprise role grants name - GitHub Connector

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base c5250139e2d3.
Review mode: full
View review run

Review Summary

The user syncer's page token is now a pagination.Bag. When an org's member pages finish, and only if --enterprises is set and ConnectorOpts.WillSyncResourceType("enterprise_role") is true, the syncer queues per-enterprise phases. The App path walks Owners and pending Owner invitees; the PAT path walks consumed-licenses users that hold a role. It emits users keyed by the same database ID the role grants use, and skips any account GET /orgs/{org}/members/{login} shows an org in this sync already emits. The org-admin check moved into a shared administersOrg helper, and its behavior is unchanged.

I scanned the full PR diff for security and correctness. Every error path in the new phases is returned, not swallowed. NotFound becomes FailedPrecondition through failClosedOnUnreadableEnterprise (enterprise_users.go:219, :347). The only 404 tolerated is in userFromDatabaseID, and there the principal is still emitted rather than dropped, so it is consistent with R7/S1–S6. Pagination is driven one page per call through the bag (F2/R8). The tests cover phase ordering, idempotent re-walks, the PAT path, filtering to administered orgs, and fail-closed on NotFound.

The repo-local criteria loaded and I applied them. Section S (empty-result data loss) and L1/L3 (log levels) both pass: skip and degrade lines are Debug. I found no new endpoint in an existing sync path that needs a new scope (B7/B8): orgs/{org}/members/{login} and users/{id} need the existing Members/read permissions. The new user population only appears once enterprise_role is opted in, and the README and docs/docs-info.md are updated, so BP1–BP3 are satisfied. Sections C (spans), D (JSON types) and F (provisioning) don't apply because no span, response-struct or Grant/Revoke code changed.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (confidence: high), pkg/connector/enterprise_users.go:322 / docs/docs-info.md: syncedOrgs runs on every enterprise-phase page and caches nothing. With --orgs unset, every page re-lists /user/orgs and makes one membership call per org. The doc says the list "is read once per process", which doesn't match the code.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/enterprise_users.go`:
- Around line 322 (and `syncedOrgs` at ~582): `syncedOrgs` is called on every enterprise-principal page. When `u.orgs` is empty, each call re-runs `getOrgs` (it paginates `/user/orgs`) plus one `GetOrgMembership` per candidate org. Fix this one of two ways. Option 1: memoize the administered-org list on `userResourceType`, using a mutex or `sync.Once`, so a process reads it once; Lambda invocations will rebuild it, which is fine. Option 2: change the sentence in `docs/docs-info.md` ("That list is read once per process") so it describes the per-page cost.

Reviewed commit: 9e0cf67090d1

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found — see the full review report

…ations

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 31ad030e0c48

Connector PR Review: [CXH-2710] - Sync the users that enterprise role grants name - GitHub Connector

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base c5250139e2d3.
Review mode: incremental since 9e0cf67
View review run

Review Summary

The new commit (31ad030) changes only docs/docs-info.md. It replaces the sentence "That list is read once per process" with the actual cost: without --orgs, each page of enterprise principals walks /user/orgs once and reads membership once per organization. That matches syncedOrgs (pkg/connector/enterprise_users.go:421-448), which caches nothing between pages. No code changed in this push.

I scanned the full PR diff again for security and correctness. It covers the user-syncer enterprise phases, the shared administersOrg helper, the WillSyncResourceType gating and the UserBuilder signature change.

I applied the repo-local criteria, with these results:

  • S1/S5/S6: satisfied.
    • The enterprise-phase loops return every error, including the IsMember, Users.Get and consumed-licenses errors.
    • The only continue skips are on a missing database ID or login and on accounts an org member page already emits.
    • On a 404 from GetByID, the user is still emitted rather than skipped.
  • L-rules: the new log lines are Debug and one-shot.
  • E-rules: errors go through wrapGitHubError.
  • T/J/PR sections: don't apply. No spans, no new response structs, no provisioning code.

The incremental artifact was complete (not partial, no dropped paths).

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • syncedOrgs cost versus the docs (prior suggestion): fixed. The docs now describe the per-page re-resolution (docs/docs-info.md:303), which matches pkg/connector/enterprise_users.go:421-448. The open review thread can be resolved.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Connector PR Review: [CXH-2710] - Sync the users that enterprise role grants name - GitHub Connector

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base c5250139e2d3.
Review mode: incremental since 9e0cf67
View review run

Review Summary

The new commit (31ad030) changes only docs/docs-info.md. It replaces the sentence "That list is read once per process" with the actual cost: without --orgs, each page of enterprise principals walks /user/orgs once and reads membership once per organization. That matches syncedOrgs (pkg/connector/enterprise_users.go:421-448), which caches nothing between pages. No code changed in this push.

I scanned the full PR diff again for security and correctness. It covers the user-syncer enterprise phases, the shared administersOrg helper, the WillSyncResourceType gating and the UserBuilder signature change.

I applied the repo-local criteria, with these results:

  • S1/S5/S6: satisfied.
    • The enterprise-phase loops return every error, including the IsMember, Users.Get and consumed-licenses errors.
    • The only continue skips are on a missing database ID or login and on accounts an org member page already emits.
    • On a 404 from GetByID, the user is still emitted rather than skipped.
  • L-rules: the new log lines are Debug and one-shot.
  • E-rules: errors go through wrapGitHubError.
  • T/J/PR sections: don't apply. No spans, no new response structs, no provisioning code.

The incremental artifact was complete (not partial, no dropped paths).

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • syncedOrgs cost versus the docs (prior suggestion): fixed. The docs now describe the per-page re-resolution (docs/docs-info.md:303), which matches pkg/connector/enterprise_users.go:421-448. The open review thread can be resolved.

Reviewed commit: 31ad030e0c48

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found — see the full review report

@luisina-santos

Copy link
Copy Markdown
Contributor

Thanks for the thorough work on this. We've decided not to fix CXH-2710, so I'm asking to close this PR without merging.

C1 already drops a grant whose principal has no resource row. Uplift builds AppUsers only from user resources in the c1z, and LookupAppUserByConnectorUser returns early when the principal is missing (pkg/mapper/entitlement_user_binding/v2/uplift.go in c1). So the Owner grant never reaches the platform, and the I9 line is only a warning in the SDK log.

Emitting the users would make those Owners visible in C1, but it costs API calls on every sync, and on the PAT path that means the whole licensed enterprise. It also widens the user population beyond the configured organization. We're accepting that an enterprise Owner outside the configured org is not visible in C1. The limitation is already documented in docs/docs-info.md on main.

Full reasoning is on CXH-2710. Could you close this one?

@mateoHernandez123

mateoHernandez123 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

The mechanism you describe is what we measured, and the two calls that produce it are different.

What GitHub returns

  • List organization members — GET /orgs/{org}/members. On main this is the only source of user resources: user.go List calls Organizations.ListMembers for the configured org and returns that page.
  • Organization.enterpriseOwners — GraphQL organization(login:) { enterpriseOwners }. This is every accepted Owner of the enterprise account. On main ownerGrants builds one Owner grant per node and does not create a user. A pending invite is a third call, inviteEnterpriseAdmin on the way in and PendingOwnerInvitations on the way back, and it is emitted on the same entitlement.

On mateo-test those two reads disagree. POST https://api.github.com/graphql with organization(login: "mateo-baton-test") { enterpriseOwners } returns mateoHernandez123 and manuel-ts-14 (database id 282336840). Check organization membership for Manuel is GET https://api.github.com/orgs/mateo-baton-test/members/manuel-ts-14 → 404. Get a user is GET https://api.github.com/user/282336840 → 200. He is an accepted Owner. He is not a member of the configured org, and he is not a pending invite.

flowchart TD
  gql["POST https://api.github.com/graphql<br/>enterpriseOwners"] --> owners[mateoHernandez123 and manuel-ts-14]
  members["GET https://api.github.com/orgs/mateo-baton-test/members"] --> org[org members: me and Felipe]
  owners --> check{GET https://api.github.com/orgs/mateo-baton-test/members/login}
  org --> check
  check -->|204 me and Felipe| row[user row already exists]
  check -->|404 Manuel| gap[Owner grant, no user row]
  row --> shown[C1 shows the Owner]
  gap --> main[main: C1 drops Manuel]
  gap --> pr[this PR: emit Manuel, C1 shows all three]
Loading

What main ships, and what this PR changes

main keeps both behaviors. The bundle has a grant for both Owners and a user row only for org members. C1 builds app users from the user rows (TRAIT_USER). LookupAppUserByConnectorUser returns without a binding when the principal has no row. The UI shows one Owner. The sync exits 0. I9 is the SDK warning for the missing row.

This PR leaves ListMembers where it is (user.go:206) and, after that page ends, walks the same enterpriseOwners connection the grants already walk (listOwnerUsers). For each Owner it calls Organizations.IsMember (enterprise_users.go:395). A 204 means the member page already emitted that user, and we keep that row. A 404 means we emit the user with the database id the grant already uses. The grant then survives uplift.

What we ran

Before this change, C1 on the test tenant showed one Owner, me. Manuel's grant was in the c1z and did not become a binding.

We then invited FeliLucero1 (database id 162376288), who is a member of mateo-baton-test, with the connector's grant CLI. That calls inviteEnterpriseAdmin. GitHub created the Owner invitation at 2026-10-14T20:20:38Z. A second identical run did not send another invite. Felipe has since accepted, so GitHub now counts him as an Owner too.

He would have shown up on main as well, because he is an org member and the pending invite is already emitted as a grant. Manuel is the one main hides. With this PR the entitlement lists all three: me and Manuel as accepted Owners, and Felipe from the moment the invitation existed, since a pending invite and an accepted Owner share the entitlement.

The case I want on the thread is the one where I am not an Owner. enterpriseOwners would still return Manuel, ListMembers would not, and C1 would show an Enterprise Owner entitlement with nobody on it.

App auth on this tenant is two membership checks. The token path is the expensive one, and we have not run it live: List enterprise consumed licenses walks every licensed account, because Member is the role GitHub reports for each of them.

@sergiocorral-conductorone sergiocorral-conductorone left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review

TeamBuilder(gh.client, gh.orgCache, gh.directCollaboratorsOnly),
UserBuilder(gh.client, gh.graphqlClient, gh.orgCache, gh.orgs, gh.customClient, gh.enterprises),
UserBuilder(gh.client, gh.graphqlClient, gh.orgCache, gh.orgs, gh.customClient, gh.enterprises,
gh.newEnterpriseRoleClients, gh.willSyncResourceType(resourceTypeEnterpriseRole.Id)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low-confidence] The enterprise_role gate is resolved once, at construction, from cli.ConnectorOpts.SyncResourceTypeIDs. That field only carries the local --sync-resource-types flag (or the lambda effective config). In self-hosted daemon mode (--client-id), the per-task selection from C1 is applied later, inside the SDK syncer (c1api/full_sync.go: c.task.GetSyncFull().GetSyncResourceTypeIds() -> sdkSync.WithSyncResourceTypes), and never reaches ConnectorOpts. So SyncFilterIsExplicit() is false, WillSyncResourceType("enterprise_role") returns true, and with --enterprises set the user syncer walks the enterprise on every sync even when the admin has left enterprise_role deselected. On the PAT path that walk covers the whole licensed enterprise: one IsMember per org plus one Users.Get per seat, which the new docs put at six figures of REST calls on a large enterprise. It also adds users that no grant references. This contradicts the PR description's claim that the walk runs only when C1 selects the type. Please either derive the decision from something that reflects the actual sync, or make the walk opt-in explicitly (for example a config flag). At minimum, document that daemon-mode deployments always run the walk. Also confirm that hosted lambda deployments actually populate sync-resource-types from the UI selection; otherwise the same applies there.

ctx context.Context,
enterprise string,
) (*customclient.EnterpriseAdminClient, error) {
clients, err := u.newEnterpriseClients(ctx, []string{enterprise})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adminClient calls u.newEnterpriseClients on every Owners and pending-invitee page, and continueWithEnterprisePrincipals calls it again at the end of each org's members. newEnterpriseRoleClients is not memoized. Each call runs GetEnterpriseInstallation, mints a new installation access token, builds a new uhttp client, and runs VerifyOrganization plus ResolveEnterpriseNodeID (two GraphQL queries). The pending phase pages through the whole Enterprise.members list 100 at a time, so a 50k-member enterprise pays roughly 500 token mints and about 1,000 extra GraphQL verification queries per sync on top of the walk itself. Any transient failure in that setup path also fails the user page. The role syncer avoids this with enterpriseRoleResourceType.clients(), which builds once and caches. Please cache the built clients on the user syncer the same way, for example a mutex-guarded map keyed by enterprise, or share the role syncer's cache. A per-process cache stays correct under the multi-process model the PR cites, because a missing entry is simply rebuilt.

@mateoHernandez123

Copy link
Copy Markdown
Contributor Author

Closing this without merging. I agree with keeping main as it is. After checking GitHub's documentation, an enterprise Owner outside the configured organization has no access to that organization, so syncing them as a user does not change what this connector models.

GitHub separates the two roles. Abilities of roles in an enterprise gives the enterprise Owner control of the enterprise: administrators, adding and removing organizations, removing members from all organizations, enterprise settings, policies across organizations, billing and security. The same page says they "do not have access to organization settings or content by default, but they can gain access by joining any organization."

Managing your role in an organization owned by your enterprise states it per organization. With no organization role, an Owner "cannot access organization content or repositories" but can manage the enterprise settings and policies that affect it. Joining as a member gives access to repositories. Joining as an organization owner gives the organization's settings and access management.

That is exactly the test tenant:

  • Organization.enterpriseOwners, through POST https://api.github.com/graphql, is the list of Owners of the enterprise account, not of the organization. It returns mateoHernandez123 and manuel-ts-14 (database id 282336840).
  • Check organization membership, GET https://api.github.com/orgs/mateo-baton-test/members/manuel-ts-14, returns 404.
  • Get a user, GET https://api.github.com/user/282336840, returns 200.

Manuel is a real enterprise Owner with no access to mateo-baton-test. FeliLucero1, who is a member, accepted an Owner invitation (inviteEnterpriseAdmin from the connector's grant CLI), and on main he appears next to me. Every Owner who can act inside the configured organization already reaches C1 through List organization members.

This PR would add a second user phase, a membership check per Owner and, on the token path, a walk of every consumed license in the enterprise. All of that is to show people who cannot see the organization this connector is configured for. #194 already chose to leave the user population as the configured organization, and docs/docs-info.md records it under "Owner grants can reference a user the sync did not emit".

What stays true for QA: an enterprise Owner outside the configured organization does not show up on the Enterprise Owner entitlement in C1, and if they are the only Owner the entitlement is empty. That is expected for an organization-scoped connector, not a defect in how Owners are listed.

flowchart TD
  owner[Enterprise Owner] --> member{member of the configured org?}
  member -->|yes| visible[user row from the org member list, C1 shows the Owner]
  member -->|no| outside[no org content or repositories]
  outside --> policy[still manages enterprise policy and can join the org]
  outside --> c1[C1 drops the grant: expected for an org-scoped sync]
Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants