Skip to content

Commit 4a25192

Browse files
feat(sharing): a share or sharing rule on a public object reaches an external member of the record's organization (#25591)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao9sUfiwLBMYyhPAkzpzFo9P)_ Closes #25549 Clause-②: yes (widening: the grant door and the materialiser admit one recipient population on effective-public objects) Metadata-protocol: semantic Letter A of the ruling on #25549 (record 6377844010, re-recorded 6378238137 after Tesla's item-by-item approval of 2026-10-24T01:28Z). An external member (`sys_member.role` exactly `external`) meets an owner-only floor on every object since #25541, so on a public object a share is how it reaches one record. The D7 pre-flight still refused that share as one "no gate would consult". This PR makes that verdict read the recipient's population. ## What changes | Item | Where | What | |---|---|---| | The verdict | `plugin-sharing` `sharing-service.ts` `inertGrantReason(object, recipient?)` | On an effective-`public` object with a real `owner_id` (not a federated phantom anchor), a `user` recipient whose `sys_member.role` in the record's organization is exactly `external` is admitted. Every other recipient keeps the 422 `SHARING_NOT_ENABLED`, now worded "consulted only for an external member of the record's organization, and the recipient is not one". `controlled_by_parent` (ADR-0111 D7.2), owner-less, phantom-anchor and bypass objects are refused for every recipient, as before. | | Where it is asked | `grant()` | The object half stays first and caller-free. The recipient half runs on both paths after the organization is resolved (the rule's threaded organization, else the record's; the record's, else the session's for a direct grant). On the direct path it runs after the D1 management check, so a caller who cannot manage the record learns nothing about the recipient. No organization resolved is refused with its own named reason. | | The population test | `isExternalMemberIn` | The stage-1 doors' test (`isExternalMembershipRole` in plugin-auth): a `sys_member` row in that organization whose role list is exactly `external`. Mirrored locally, since plugin-sharing does not depend on plugin-auth. | | Rule materialisation | `sharing-rule-service.ts` | The per-recipient refusal is marked (`isInternalRecipientRefusal`), and all three passes (`reconcile`, `reconcileForRecord`, `reconcileRuleForUser`) skip that recipient instead of aborting. A pass that skipped any carries one `note` line on its result and logs the count once (`[sharing-rule] internal recipients skipped on a public object`). Every other D7 refusal still aborts the rule's pass, as before. | | Lint | `validate-sharing-rule-enforceability.ts`, `rule-explanations.ts` | `sharing-rule-object-not-shareable` on a public anchor drops from `error` to `warning`: "so the rule is effective for external recipients only". The `controlled_by_parent` arm stays an `error`. The `os explain` entry says when to keep the rule and when to change the object's `sharingModel`. | | Docs | `content/docs/permissions/sharing-rules.mdx` | One paragraph under "External principals". `permissions/index.mdx` and the app-crm comment said the rule "fails the build"; both now say what it does. | | Changeset | `.changeset/25549-external-share-public-object.md` | `@objectstack/plugin-sharing` and `@objectstack/lint` `minor`, with both declaration lines. | ### How the rule's report surfaces (measured) `POST /api/v1/sharing/rules/:idOrName/evaluate` answers `evaluateRule`'s return value unfiltered (`sharing-routes.ts`, `res.json(result)`), so the `note` reaches the admin who evaluates a rule. The boot backfill, the write hooks and the re-grant queues read only the counts, so for them the log line is the report. `note` is declared on plugin-sharing's own `SharingRuleReconcilePassResult`, not on the spec's `SharingRuleEvaluationResult`. ## Verification (head `a1260e57`) - `plugin-sharing`, `system-caller-inert-grant.test.ts`, new block (9 cases): an external recipient is admitted, its row carries the record's organization and `buildReadFilter` for that external returns exactly that record; an internal recipient gets `SHARING_NOT_ENABLED:` and no row; a user external in org B and a member of org A is refused on an org A record, and the reverse user is admitted; a role list `external,member` is refused; a system caller gets the same two answers; a caller without management rights gets `PERMISSION_DENIED` first; a team rule expanding to one external and one staff member on two gold records writes two rows for the external, `grantsRefused: 0`, the note, and one log line with `skipped: 2`; a rule to externals only has no note; an owner-less public object still refuses an external. - The #8207 pins in the same file: the rule-pass refusal block now covers the four object classes that still abort, and a new case pins that a rule on the public object with an internal recipient mints nothing and carries the note. - `pnpm --filter @objectstack/plugin-sharing typecheck` exit 0; the suite: 55/55 files, 1233 tests passed. - `pnpm --filter @objectstack/lint typecheck` exit 0; the suite: 158/158 files, 7237 tests passed. - Dogfood, `external-member-directory.dogfood.test.ts` on a showcase boot, two new cases: the owner POSTs a share of one `showcase_invoice` (`public_read_write`) to an external over `/data/showcase_invoice/ID/shares` and gets 201 with the organization stamped; the external read zero invoices before and exactly that one after. The same share to an ordinary member answers 422 with `error.code` `SHARING_NOT_ENABLED`, and no row is written. The file: 13/13. `tsc --noEmit` on the dogfood package exit 0. - Ablation (committed fix first, `scripts/ablation-replace.mjs`): `if (recipient === 'external') return null;` replaced so the external arm returns a reason. Anchor 1 to 0, blob c1f616f to 5d5c8c31; the file went red, 8 failed of 33; restored, blob equals HEAD, `git diff HEAD` empty. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `a1260e57` derived 104 families; each ran with its exit code captured before any pipe. First sweep: 101 exit 0, 3 exit 3 (PREREQUISITE NOT MET: the pinned control commit absent from the shallow clone for `check-plugin-teardown-shape --self-test`; no `client-react` dist for `check:skill-examples`; workspace dists for `check:dual-build-cjs-loads`). After fetching that commit and a locked workspace build, all three re-ran exit 0. `--ran` reconciliation: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN. ## Acceptance notes - **The two confidence gaps.** The REST door and the rule evaluator both have the organization at hand: `grant()` resolves it on both paths before writing (commit 3f64fe6), and the recipient half reads it from there. When neither path resolves one, the grant is refused with a named reason, not judged against the session's organization. - **Error order on a public object.** For a direct grant on a public object the 422 now comes after the D1 management pre-flight, so a caller who may not manage the record gets 403/404 where it used to get 422. That is deliberate (the population is only read for an authorized caller). On every other object the order is unchanged. - **Existing inert rows.** A `source: 'rule'` row for an internal recipient on a public object, minted by an older build, is still left standing by a pass when its level is unchanged; it is never consulted for that recipient. - **Spec contract docblock.** `ISharingService.grant`'s docblock in `packages/spec/src/contracts/sharing-service.ts` still says a share on a public object "enforces nothing". It is outside this claim's file surface and is left for the seat to route with the ADR-0111 line drafted in the os-dev-report. - **ADR-0111 D7.** The dated line is Tier H and is not in this PR; its draft is in the os-dev-report on the card. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AjwMndyviznGagEUcX7QVx --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ba6912c commit 4a25192

12 files changed

Lines changed: 531 additions & 82 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-sharing': minor
3+
'@objectstack/lint': minor
4+
---
5+
6+
feat: a share or sharing rule on a public object reaches an external member of the record's organization
7+
8+
Clause-②: yes (widening: the grant door and the materialiser admit one recipient population on effective-public objects)
9+
Metadata-protocol: semantic
10+
11+
An external member (`sys_member.role` exactly `external`) reads only the rows it owns and the rows shared to it, on every object. On a `public_read_write` object, or a system object with no `sharingModel`, a share to it used to be refused as one that would never take effect. For that member it is the only thing that does.
12+
13+
- **Manual shares.** `SharingService.grant` now admits a `user` recipient on such an object when the recipient is an `external` member of the record's organization. Every other recipient keeps the 422 `SHARING_NOT_ENABLED`, with a message that says the share is consulted for external members only. A user who is external in one organization and a member of another is judged in the record's organization. `controlled_by_parent`, owner-less, federated phantom-anchor and bypass objects are refused for every recipient, as before.
14+
- **Sharing rules.** A rule's pass writes rows for the external members it expands to and skips the internal ones without failing the pass. When it skips any, the evaluate result carries a one-line `note`, and the pass logs the count once.
15+
- **Lint.** `sharing-rule-object-not-shareable` on a public object is now a warning: the rule is effective for external recipients only. `os explain` says when to keep it and when to change the object's `sharingModel`.

‎content/docs/permissions/index.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ export const KeyAccountQualifiedContactRule = defineSharingRule({
4343
Note the object it is anchored on. `showcase_contact` is OWD `private`, which is
4444
what makes this grant one a read gate actually consults — sharing only ever
4545
WIDENS a baseline, so a rule on an object that is already `public_read_write`
46-
grants nothing and is refused at boot. That is an authoring-time build error
47-
(`sharing-rule-object-not-shareable`), not a runtime surprise.
46+
grants nothing to staff, and reaches only the external (portal) members it names.
47+
The authoring-time advisory `sharing-rule-object-not-shareable` says so before
48+
anything boots.
4849

4950
Because AI agents act through the same permission-aware surface, these rules bound
5051
agent access exactly as they bound users ([Actions as Tools](/docs/ai/actions-as-tools)).

‎content/docs/permissions/sharing-rules.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ the rows it owns and the rows granted to it:
137137
the object (for example `supplier in current_user.<key>`). Such a policy
138138
lifts the owner-only floor for that object, and the policy bounds the rows.
139139

140+
On a `public_read_write` object, or a system object with no `sharingModel`, a
141+
share or sharing rule takes effect for external recipients only: the grant is
142+
accepted for a recipient who is an `external` member of the record's
143+
organization, every other recipient is refused with `SHARING_NOT_ENABLED`
144+
(422), and a rule's evaluation writes rows for its external recipients alone.
145+
140146
`externalSharingModel`, the external dial ADR-0090 D11 declared, was removed
141147
in `@objectstack/spec` 18 (ADR-0049 enforce-or-remove): while it existed no
142148
request resolved an external principal, so the value changed no answer. The schema

‎examples/app-crm/objectstack.config.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,11 @@ export default defineStack({
109109
// were removed under ADR-0049 enforce-or-remove (#9698), the same call
110110
// #9237 made for app-showcase's two.
111111
//
112-
// ⛔ Do not re-add one on a public object — `sharing-rule-object-not-shareable`
113-
// now fails the build, and its message states the two honest fixes. Giving
114-
// this app a LIVE sharing demonstration means giving it a `private` object
115-
// first; that is an access-matrix change (ADR-0090 D6 review), not a rider.
112+
// ⛔ Do not re-add one for staff on a public object — such a rule reaches
113+
// external (portal) members only, and `sharing-rule-object-not-shareable`
114+
// says so. Giving this app a LIVE sharing demonstration for staff means
115+
// giving it a `private` object first; that is an access-matrix change
116+
// (ADR-0090 D6 review), not a rider.
116117

117118
// Seed data
118119
data: CrmSeedData,

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,12 +2222,15 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
22222222
// does not lower means the rule is SKIPPED by enforcement.
22232223
// - `object` decides whether the grant is refused outright: reconcile hands
22242224
// each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
2225-
// `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
2226-
// `public` or it is a `controlled_by_parent` detail. Both are decidable
2227-
// from authored metadata; the other arms of that verdict (`owner_id`, the
2225+
// `SHARING_NOT_ENABLED` when the anchor is a `controlled_by_parent`
2226+
// detail, and, when its effective sharing model is `public`, for every
2227+
// recipient who is not an `external` member of the record's organization
2228+
// (#25549: that arm is advice, a `warning`). Both are decidable from
2229+
// authored metadata; the other arms of that verdict (`owner_id`, the
22282230
// bypass set, federated anchors) are not, and are excluded by name.
22292231
//
2230-
// Either way the grant is declared and does not exist. The lint calls the
2232+
// Either way the grant is declared and does not exist (for staff, on the
2233+
// public arm). The lint calls the
22312234
// same compiler and mirrors the same verdict function, from the same inputs
22322235
// — the verdict cannot drift from the consumers'. Gating for the ADR-0078
22332236
// reason `SharingRuleSchema`'s own docblock states: the whole authorable

‎packages/lint/src/rule-explanations.ts‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,22 +1670,31 @@ const SHARING_RULE_RUNTIME_VARIABLE_CONDITION_EXPLANATION: RuleExplanation = {
16701670

16711671
const SHARING_RULE_OBJECT_NOT_SHAREABLE_EXPLANATION: RuleExplanation = {
16721672
rule: 'sharing-rule-object-not-shareable',
1673-
covers: 'why a rule on a public object grants nothing',
1674-
paragraphs: [
1675-
'Sharing only ever WIDENS an organization-wide default (OWD) baseline, and on the widest baseline there is ' +
1676-
'nothing left to widen. An object\'s effective sharing model is `public` when it declares ' +
1677-
'`sharingModel: \'public_read_write\'`, or when it declares none and is a system object (`isSystem: true` ' +
1678-
'or a `sys_` name), which ADR-0090 D1 resolves to public. A custom object with no `sharingModel` resolves ' +
1679-
'to private and is not reported.',
1680-
SHARING_INERT_GRANT,
1681-
'Here the refusal reads "\'OBJECT\' is not under record-sharing enforcement". Measured: `buildReadFilter` ' +
1682-
'returns `null` for such an object — NO record-level filter at all — so every principal already reads ' +
1683-
'every row, and the rule advertises a restriction that does not exist.',
1684-
'To fix it, decide which half is wrong. If the ACCESS is right — everyone should read and write these ' +
1685-
'records — the rule is dead metadata: delete it (ADR-0049 enforce-or-remove). If the RULE is right — ' +
1686-
'only the named audience should reach these records — the object\'s OWD is the defect: set ' +
1687-
'`sharingModel: \'private\'` (the owner plus explicit shares) or `\'public_read\'`, and the rule starts ' +
1688-
'enforcing. Do not re-home the rule onto another public object; that moves the inertness instead of ' +
1673+
covers: 'why a rule on a public object reaches externals only',
1674+
paragraphs: [
1675+
'Sharing only ever WIDENS an organization-wide default (OWD) baseline, and for the people who work in the ' +
1676+
'organization a public baseline has nothing left to widen. An object\'s effective sharing model is ' +
1677+
'`public` when it declares `sharingModel: \'public_read_write\'`, or when it declares none and is a ' +
1678+
'system object (`isSystem: true` or a `sys_` name), which ADR-0090 D1 resolves to public. A custom object ' +
1679+
'with no `sharingModel` resolves to private and is not reported.',
1680+
'An EXTERNAL member of the organization (`sys_member.role` exactly `external`: a supplier\'s or a ' +
1681+
'customer\'s portal user) is different. Its baseline on every object is the rows it owns, so a share is ' +
1682+
'how it reaches a record of a public object. `SharingService.grant` therefore admits a grant on such an ' +
1683+
'object for a recipient who is an external member of the record\'s organization, and its ' +
1684+
'`assertNotInertGrant` pre-flight (ADR-0111 D7) refuses everyone else with SHARING_NOT_ENABLED: "\'OBJECT\' ' +
1685+
'has a public sharing model, so a share row on it is consulted only for an external member". The rule\'s ' +
1686+
'pass writes rows for the external recipients it expands to, skips the internal ones, and says so in one ' +
1687+
'line (`note` on the evaluate result, and one log line per pass).',
1688+
'Measured for an internal reader: `buildReadFilter` returns `null` on such an object — NO record-level ' +
1689+
'filter at all — so every member already reads every row, and for them the rule advertises a restriction ' +
1690+
'that does not exist. That is why this is advice and not an error: it is right when the rule\'s audience ' +
1691+
'is external, and only half of what it says when the audience includes staff.',
1692+
'To act on it, decide which half is wrong. If the audience is external — the rule hands a supplier the ' +
1693+
'records it should see — keep it. If the ACCESS is right for staff and the rule names staff, the staff ' +
1694+
'half is dead metadata: narrow the recipients (ADR-0049 enforce-or-remove). If only the named audience ' +
1695+
'should reach these records, staff included, the object\'s OWD is the defect: set ' +
1696+
'`sharingModel: \'private\'` (the owner plus explicit shares) or `\'public_read\'`, and the rule enforces ' +
1697+
'for everyone. Do not re-home the rule onto another public object; that moves the inertness instead of ' +
16891698
'removing it.',
16901699
],
16911700
};

‎packages/lint/src/validate-sharing-rule-enforceability.test.ts‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,13 @@ const anchoredOn = (owd: unknown, extra: Record<string, unknown> = {}) => ({
319319
});
320320

321321
describe('validateSharingRuleEnforceability — an anchor no gate would consult goes RED', () => {
322-
it('flags `public_read_write` — nothing left to widen', () => {
322+
it('advises on `public_read_write` — effective for external recipients only (#25549)', () => {
323323
const findings = validateSharingRuleEnforceability(anchoredOn('public_read_write'));
324324
expect(findings).toHaveLength(1);
325325
expect(findings[0]).toMatchObject({
326-
severity: 'error',
326+
// Advice, not a refusal: the runtime admits the grant for a recipient who
327+
// is an `external` member of the record's organization.
328+
severity: 'warning',
327329
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
328330
// The defect is on `object`, not on `condition` — a different field with
329331
// a different fix, so it gets its own path.
@@ -333,12 +335,12 @@ describe('validateSharingRuleEnforceability — an anchor no gate would consult
333335
// An author must be able to act on this: it has to name the object, the
334336
// rule (the `where` above), and WHY the rule cannot take effect.
335337
expect(findings[0].message).toBe(
336-
'anchor object "crm_opportunity" declares sharingModel \'public_read_write\', so the grant is refused ' +
337-
'(SHARING_NOT_ENABLED) and the rule restricts nothing',
338+
'anchor object "crm_opportunity" declares sharingModel \'public_read_write\', so the rule is effective for ' +
339+
'external recipients only',
338340
);
339341
expect(explanationOf(SHARING_RULE_OBJECT_NOT_SHAREABLE)).toMatch(/`buildReadFilter` returns `null`/);
340-
// …and both honest fixes, because which one is right is the author's call.
341-
expect(findings[0].hint).toMatch(/delete it/);
342+
// …and both honest choices, because which one is right is the author's call.
343+
expect(findings[0].hint).toMatch(/Keep it if its audience is external/);
342344
expect(findings[0].hint).toMatch(/sharingModel: 'private'/);
343345
});
344346

@@ -654,7 +656,7 @@ describe('[#22161] one-line verdicts — the four ids', () => {
654656
const MOVED: Record<string, readonly string[]> = {
655657
[SHARING_RULE_UNLOWERABLE_CONDITION]: ['`createDeclaredSharingRuleSource`', 'one WARN line in the log', 'ADR-0049', 'SharingRuleService', 'INVALID_FILTER'],
656658
[SHARING_RULE_RUNTIME_VARIABLE_CONDITION]: ['MATERIALISED', 'no "current user"', 'the rule is SKIPPED'],
657-
[SHARING_RULE_OBJECT_NOT_SHAREABLE]: ['WIDENS', 'ADR-0090 D1', '`assertNotInertGrant`', 'not under record-sharing enforcement', 'restriction that does not exist'],
659+
[SHARING_RULE_OBJECT_NOT_SHAREABLE]: ['WIDENS', 'ADR-0090 D1', '`assertNotInertGrant`', 'consulted only for an external member', 'restriction that does not exist'],
658660
[SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT]: ['ADR-0055', 'no shares to widen', '`assertNotInertGrant`', 'whatever the MASTER grants them'],
659661
};
660662

‎packages/lint/src/validate-sharing-rule-enforceability.ts‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@
6161
* a warning. It fails closed (the recipient under-sees rather than over-sees),
6262
* which is why it was survivable, not why it is acceptable.
6363
*
64+
* [#25549] One arm is no longer an `error`: on a `public` anchor the runtime
65+
* admits the grant for a recipient who is an `external` member of the record's
66+
* organization (the external floor consults a share row there) and refuses the
67+
* rest, so that rule IS live for its external half. Whether a rule's audience
68+
* is external is runtime data, not metadata, so the arm reports a `warning`
69+
* saying what the rule does — "effective for external recipients only".
70+
*
6471
* Measured before shipping: every sharing-rule condition and RLS predicate
6572
* declared anywhere in this repo (examples, platform permission sets) lowers
6673
* cleanly, so the gate turns nothing red that works today.
@@ -439,10 +446,13 @@ function masterOf(obj: AnyRec): string | undefined {
439446
* `buildReadFilter`, all three postures), the arms differ in the direction
440447
* that decides the wording:
441448
*
442-
* - **`public` OWD** → grant refused, `buildReadFilter` returns `null`, zero
443-
* share rows. Nothing is filtered, so the intended audience already reads
444-
* every row — and so does everyone else. Nobody UNDER-sees; the harm is
445-
* that the declaration advertises a restriction that does not exist.
449+
* - **`public` OWD** → grant refused for an internal recipient,
450+
* `buildReadFilter` returns `null` for an internal reader, zero share rows
451+
* for staff. Nothing is filtered for them, so the intended staff audience
452+
* already reads every row — and so does everyone else. Nobody UNDER-sees;
453+
* the harm is that the declaration advertises a restriction that does not
454+
* exist. An `external` recipient is admitted and its row is consulted
455+
* (#25549), which is why this arm is a `warning`.
446456
* - **`controlled_by_parent`** → grant refused with a DIFFERENT reason
447457
* ("share the master record instead"), and the detail's visibility comes
448458
* from its master's path (ADR-0055), not from this rule. Here the author
@@ -524,21 +534,26 @@ function anchorFindings(
524534
? `declares sharingModel 'public_read_write'`
525535
: 'declares no sharingModel and is a system object, which resolves to public';
526536

537+
// [#25549] Advice, not a refusal: the runtime admits this rule's grants for
538+
// the recipients who are `external` members of the record's organization
539+
// (their owner_only floor consults a share row on a public object) and skips
540+
// the rest. Whether the rule's audience is external is runtime data
541+
// (`sys_member.role`), not metadata, so this door can say what the rule does
542+
// and cannot say it is wrong.
527543
return [{
528-
severity: 'error',
544+
severity: 'warning',
529545
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
530546
where,
531547
path,
532548
// [#22161] One verdict sentence; why a public baseline has nothing to
533-
// widen, and the measured read filter, is the `os explain` entry's.
549+
// widen for staff, and the measured read filter, is the `os explain` entry's.
534550
message:
535-
`anchor object "${object}" ${declared}, so the grant is refused (SHARING_NOT_ENABLED) and the rule ` +
536-
'restricts nothing',
537-
// [#22161] One remedy line naming both honest fixes; how to tell which
551+
`anchor object "${object}" ${declared}, so the rule is effective for external recipients only`,
552+
// [#22161] One remedy line naming the honest choices; how to tell which
538553
// half is wrong, and why re-homing the rule fixes nothing, are the
539554
// `os explain` entry's.
540555
hint:
541-
`If everyone should reach these records, delete it; if only its audience should, set "${object}"'s ` +
556+
`Keep it if its audience is external; if staff should be narrowed too, set "${object}"'s ` +
542557
`sharingModel: 'private' or 'public_read'`,
543558
}];
544559
}

0 commit comments

Comments
 (0)