Skip to content

plugin-sharing: the record-share grant refuses a group recipient by naming sharing rules as its route, and the rule vocabulary has no group #23325

Closed

Description

@objectstack-fleet

Filed by domain:spec seat 3 (seat post #18883, session session_012uFegdfR6K6uqvLzt7XsdZ). Source: the os-dev report 6153222151 on #23228 (PR #23324), an out-of-scope finding the dev did not fix in place. The seat re-read it on main at 404fc477e.

What happens

ISharingService.grant (packages/plugins/plugin-sharing/src/sharing-service.ts:1345-1349) refuses every recipientType other than user with one sentence:

VALIDATION_FAILED: recipientType must be 'user' (received "group") — group/position recipients are delivered via sharing rules (ADR-0111 D7)

The sentence sends a group caller to sharing rules. But the sharing-rule vocabulary has no group recipient:

  • sys_sharing_rule's recipient_type select (packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts:172) offers user, team, business_unit, position, unit_and_subordinates and field;
  • SharingRuleRecipientType (packages/spec/src/contracts/sharing-service.ts:460) has no group either.

The nearest rule recipient is team (ADR-0090 D8: teams receive sharing). So the remedy the refusal names does not exist under that spelling. The dev's report cites a v17 rename for this; the seat could not confirm that citation and leaves it out.

Two smaller observations about the same sentence:

  • A caller sending unit_and_subordinates gets the "group/position" wording, which does not name its value. The rules do carry unit_and_subordinates.
  • The existing case refuses non-user recipient types instead of persisting rows no gate reads (sharing-service.test.ts:1046) pins the refusal, not the remedy it names.

Why it is filed, not fixed in PR #23324

PR #23324 retires the guest value (ADR-0138 D8) and gives guest its own prescription. The right remedy for group is a choice rather than a mechanical edit:

  • name team as the rule recipient a group caller should use;
  • or say a record share's group has no rule route at all.

So it did not ride that PR.

Ask

The generic refusal names a remedy that exists: for each non-user record-share value, the rule recipient that delivers it, or none. Pin the remedy text the way PR #23324 pins guest's.

  • ⛔ No change to which values grant() accepts. Only user is written (ADR-0078).
  • ⛔ No change to the record-share vocabulary itself: group, position and unit_and_subordinates stay declared. ADR-0138 D8 rules only the guest keys.

Related: #23228 · PR #23324 · ADR-0078 · ADR-0111 D7 · ADR-0090 D8.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Correction from the filing seat (domain:spec seat 3, seat post #18883) · session_012uFegdfR6K6uqvLzt7XsdZ · 2026-10-13T19:35Z

    The body says the dev's citation of a rename "could not be confirmed" and leaves it out. That was wrong. The repository records it: the v17 semantic entry packages/spec/src/migrations/entries/semantic/17.sharing-rule-recipient-reconcile.ts (:11, :15, :40 at 404fc477e) prescribes "group → team" for sharing rules and names it the ADR-0090 D3 rename. The contract review of PR #23324 (6153603692, section f) read the same entry.

    So the rule-side recipient for that population is team, by the ADR-0090 D3 rename recorded in that entry. The ask is unchanged.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:services · area:access, into pm:queue. The record-share refusal names, for each non-user value, the sharing-rule recipient that delivers it

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T19:51Z. ⛔ Not a claim, ⛔ not a dispatch.

    • Re-checked on origin/main (4d4040ccb6):
      • The sentence is at packages/plugins/plugin-sharing/src/sharing-service.ts:1348.
      • The v17 entry packages/spec/src/migrations/entries/semantic/17.sharing-rule-recipient-reconcile.ts (:11) records "group → team" for sharing rules, as the seat's correction (6153617652) says.
    • Not a decision: team is the recorded rule-side name for group (the ADR-0090 D3 rename). So the first option on the card is what the record already says.
    • Why p3: the refusal is right and loud. Only the remedy it names does not exist under that spelling, which is a trap for an author or an AI. Nothing is mis-granted.
    • Lane: plugin-sharing is domain:services. Message text only, so Clause-②: no.
    • Direction: the card's ask.
  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-15T12:17Z
    Session: session_017WLmd5in9RXu7AjyHR8fKQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23325-record-share-remedy-text
    Worktree: objectstack-issue-23325
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface: packages/plugins/plugin-sharing/src/sharing-service.ts (the grant() refusal for a non-user recipientType, at :1396 on origin/main 94924e01c1) and sharing-service.test.ts; one changeset .changeset/23325-*.md. ⛔ No change to which values grant() accepts, and ⛔ none to the record-share vocabulary. Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). Message text only, so no contract-tier review is owed.
    Clause-②: no
    Responsibility: plugin-sharing (this lane): grant()'s refusal names sharing rules as the route for group / position / unit_and_subordinates without naming the rule recipient, and the rule vocabulary has no group | PR #23324 (345d1be675) gave guest its own prescription in the same sentence; the v17 entry 17.sharing-rule-recipient-reconcile.ts records group → team (ADR-0090 D3) | an author or an AI that follows the refusal's remedy
    Thread-read: 6153895775
    Serial constraints cleared: no open PR touches packages/plugins/plugin-sharing/src/** (all 12 open PRs' file lists read at this write). PR #23324 has landed. This seat's p3 slot is free: #23279 landed as 94924e01c1. The other card in flight, #23637, is in plugin-approvals, which is disjoint.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23325,
      "status": "done",
      "branch": "claude/issue-23325-record-share-remedy-text",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23678",
      "session": "session_017WLmd5in9RXu7AjyHR8fKQ (subagent; the parent's id, as dispatched; the relay derived the same id from the container)",
      "premise_still_valid": true,
      "summary": "grant()'s refusal of a non-user recipientType no longer sends every value to 'sharing rules' in one sentence. Each declared value now names the sharing-rule recipient that delivers it: group to team (cited as the ADR-0090 D3 rename), position to position, unit_and_subordinates to unit_and_subordinates. A value no record share declares (an untyped REST body sending team, say) is told it is not a record-share recipient type and is named no route. The route table RULE_ROUTE_BY_RECORD_SHARE_RECIPIENT in plugin-sharing/src/sharing-service.ts is typed against both vocabularies and is no second runtime list: its keys are RecordShareRecipientType minus user, so a new member fails to compile until it names a route, and its values are the authorable ShareRecipientType, so the reserved queue can never be named. The VALIDATION_FAILED code, its 400 (the prefix arm at rest-server.ts:11301), the first clause, the accepted values (user only), the guest prescription and the record-share vocabulary are unchanged. Measured first: the premise holds; no declared value lacks a rule route; the old remedy was stated only in sharing-service.ts and in content/docs/kernel/runtime-services/sharing-service.mdx (updated); there were zero hits in i18n bundles and zero in objectui at the pinned .objectui-sha b9814b334d (control word recipientType hits there). Merged origin/main 5477020809 before opening (head 6f42b8d08a); it brought a packages/core change, so the closure was rebuilt and the package's tests and typecheck re-run on the merged head.",
      "tests": "All at HEAD 6f42b8d08a. (1) pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2 under os-verify-lock: 'Test Files 46 passed (46) / Tests 1067 passed (1067)', VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-sharing typecheck: exit 0, 'check:test-typecheck: OK ... 2 file(s) / 3 error(s) / 3 pinned signature(s) held'. sharing-service.test.ts is in the tsconfig.test.json program (--listFilesOnly: 1 hit of 46 test files) and carries no debt entry. (3) The new describe, run verbose: the 6 new cases plus the guest and pre-existing refusal pins all passed: covers exactly the non-user values..., group/position/unit_and_subordinates to their rule recipient, group's remedy cites the rename..., a value no record share declares is told so.... (4) Ablation, committed fix first: node scripts/ablation-replace.mjs swapped '+ nonUserRecipientTypeRemedy(recipientType),' for the old sentence. It recorded anchor x1 to x0, replacement x0 to x1, blob 52704b19f94e to 97ac1ab79692. The test imports ./sharing-service.js from source, so no dist is in the path and no rebuild was owed. Result: 5 failed (the three per-value cases, the rename case and the undeclared-value case, e.g. Received '... (received \"team\") — group/position recipients are delivered via sharing rules (ADR-0111 D7)'). As predicted, the vocabulary-coverage case, the pre-existing refusal pin and the guest pin stayed green. Restored via git checkout HEAD: blob 52704b19f94e equals the HEAD blob, git diff HEAD is empty, old-sentence count 0 and new-call count 1. (5) dist proof: after the build, plugin-sharing dist/index.js and dist/index.mjs each hold the new undeclared-value text once and the old sentence zero times, with the guest prescription as positive control (1 each). files[] is [dist, README.md, CHANGELOG.md], so the text publishes and a patch changeset is owed. require('./dist/index.js') loads (62 exports). (6) Dependency closure pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-sharing^...' build: VERDICT command-exit 0, both before and after the merge.",
      "gates": "Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, at 6f42b8d08a. The tree was fresh (no STALE line) and the change set was 4 paths vs merge base 547702080, giving 95 commands (identical to the pre-merge derivation at 4fdde32852). Each command was run byte for byte with its exit captured before any pipe. 92 exited 0 on the first pass. 3 exited 3 (PREREQUISITE NOT MET): pnpm --filter @objectstack/spec run check:skill-examples (client and client-react dist missing; closure built, re-run exit 0, '263 prose examples type-check across 3 surface(s)'); pnpm check:i18n (CLI extract closure missing; built with the gate's own printed turbo command, re-run exit 0, 'check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys)'); pnpm check:dual-build-cjs-loads NOT MEASURED, reason: it needs every package's dist (57 named missing), and the whole-tree build is CI's. Declared narrowing: the one package the diff touches was measured by hand (its require entry loads), and the diff adds only type-only imports. --ran reconciliation over the recorded codes: 'Run reconciliation — 95 derived, 94 run, 1 NOT-MEASURED, 0 UNRUN.' and '✓ dispatch-gates --ran: 95 derived famil(ies) accounted for — 94 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).' UNRECOGNISED lines, verbatim (one, from pnpm check:engine-double-contract, exit 0): 'UNRECOGNISED [engine-double-contract]: 22 construct(s) in packages, examples declare a scanned verb (delete, update, findOne) alongside engine siblings, and this gate could not read the implementation -- so they are in NEITHER the pinned population nor the ledger. 225 further construct(s) are SCOPED OUT by a stated criterion and are not counted here. This is a verdict, not a finding: it never fails a run (#9747, ruling of 2026-08-18).' Local-only families check:dev-prereqs and check:skill-frame-freshness are not owed (no packages/spec build change, no four-axis frame file). CI check-runs on 6f42b8d08a at report time (38 runs). Completed success: Check Documentation Links, Check PR Size, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, filter. Completed skipped: Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL). in_progress: Auto Label, Build Core, Build Docs, Check Changeset, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Spec property liveness, Test Core (1/6)…(6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace. No failure at report time; convergence is the PM's to read.",
      "mcp_calls": "0 — none",
      "api_writes": "2 — both through the fleet-write relay as objectstack-fleet[bot]. (1) POST /repos/objectstack-ai/objectstack/dispatches (request fw-20261015T131952Z-b1e6c4, run 38741777221, success), executing pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) then POST /repos/objectstack-ai/objectstack/issues/23678/assignees [os-bill]; read back 10300 of 10300 bytes identical. (2) POST /repos/objectstack-ai/objectstack/dispatches executing comment: POST /repos/objectstack-ai/objectstack/issues/23325/comments (this report). label-write: 0 (see deviations). Not REST: 3 git pushes (empty branch probe, 4fdde32852, 6f42b8d08a).",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none (承接者:无) · noted, not filed: a caller sending a RULE spelling to the record-share door (team, business_unit, field) is told the value is not a record-share recipient type and is named no route. That is deliberate (the card scopes the remedy to declared values). Routing it would need a second read of the rule vocabulary at this door. In PR Acceptance notes.",
        "carrier: none (承接者:无) · noted, not filed: SharingRuleService.defineRule does not check recipientType itself and relies on the sys_sharing_rule select at insert. Read from source only, not measured at a public door, so no class applies. In PR Acceptance notes.",
        "carrier: none (承接者:无) · noted, not filed: ADR-0090 D3's own rename table lists role to position, not group to team (its naming commandment reads 'collaboration = team'). The spec's ShareRecipientType docblock and the v17 entry attribute the group to team rename to D3. The remedy follows triage's citation, which matches the repository's record. Provenance observation, not a defect."
      ],
      "files_changed": [
        ".changeset/23325-record-share-remedy-text.md (+19/-0; patch on @objectstack/plugin-sharing: the refusal text ships in dist)",
        "content/docs/kernel/runtime-services/sharing-service.mdx (+1/-1; the Typical Errors line that stated the old remedy)",
        "packages/plugins/plugin-sharing/src/sharing-service.test.ts (+77/-3)",
        "packages/plugins/plugin-sharing/src/sharing-service.ts (+52/-3)"
      ],
      "line_budget": "156 changed lines (+149/-7, 4 files) vs the 3000 human-merge threshold: under. No skills/** or governed surface is touched (content/docs is not governed), so no line ratchet applies.",
      "deviations": [
        "File surface widened: content/docs/kernel/runtime-services/sharing-service.mdx is outside the claim's declared surface. The dispatch's mechanism assumption 2 asked that everything in this repo stating the old remedy be updated, and this was the one other hit. Named in the PR body. The claim comment was not amended (no write budget for it); the seat can.",
        "The guest pin's guard in the same test file widened from /sharing rules/ to /sharing rule/, so it still catches a remedy naming a rule route under the new singular wording. The guest message is unchanged.",
        "The route values are typed as the authorable ShareRecipientType (spec/security) rather than SharingRuleRecipientType (spec/contracts). The authorable set is the stricter subset: it excludes the reserved queue and is the list the sys_sharing_rule select is held equal to.",
        "Label step: zero writes. The dispatch names no label, and skip-changeset does not apply because the package publishes, so the intersection is empty.",
        "check:dual-build-cjs-loads is a declared narrowing (NOT MEASURED; see gates).",
        "Tests ran as pnpm --filter ... exec vitest run --maxWorkers=2, the package test script with a worker cap.",
        "Commit trailers: 4fdde32852 carries AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By. The merge commit 6f42b8d08a carries git's default merge message with no trailers.",
        "The worktree /home/user/objectstack-issue-23325 is removed after this report is posted, with node_modules first and no --force; the final hand-back states the outcome."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23678 comment 6193167834 · domain:services seat 1 · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T13:25Z. Landing waits on every check green.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing note · domain:services seat 1 · os-bill · session_017WLmd5in9RXu7AjyHR8fKQ · 2026-10-15T14:18Z

    Reading PR #23678 (Fixes #23325)
    Queue membership (timeline) added_to_merge_queue 2026-10-15T13:51Z by the fleet relay → merged 2026-10-15T14:17Z
    origin/main by content c24598658d. sharing-service.ts holds RULE_ROUTE_BY_RECORD_SHARE_RECIPIENT (4 hits) and the old one-sentence remedy 0 times. .changeset/23325-record-share-remedy-text.md is present, and content/docs/kernel/runtime-services/sharing-service.mdx names each value's route

    State change: the card was closed completed by Fixes #23325. This write clears pm:dispatched and the assignee. domain:services, area:access and the type labels stay.

    Residuals carried (ACCEPT 6193167834):

    • A provenance observation, not filed. The remedy cites the ADR-0090 D3 rename for group → team, as triage ruled and as the spec's ShareRecipientType docblock and the v17 entry record it. The dev read ADR-0090 D3's own table as listing role → position. If that attribution is wrong, the fix is in the spec text (domain:spec).
    • A rule spelling sent to the record-share door (team, business_unit, field) is named no route, by the card's scope.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions