Skip to content

fix(plugin-sharing): the record owner and an explicit Modify-All holder may mint a share link without visibility (ADR-0111 D8 rule 1, ruling A′) - #21447

Merged
os-zhuang merged 10 commits into
mainfrom
claude/issue-21329-share-link-mint-authority
Oct 2, 2026
Merged

os-zhuang merged 10 commits into
mainfrom
claude/issue-21329-share-link-mint-authority

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21329
Clause-②: yes (widening)

Ruling 5950188467 (A′): createLink admits visibility OR the record owner OR an explicit Modify-All bypass, behind the publicSharing opt-in and before eligibility. A hierarchy manager still needs visibility to mint. ADR-0111 D8 rule 1 is restated to match. This PR is governed (Tier H, docs/adr/**) and stays draft for the maintainer's own approval.

It carries two deliberate narrowings of the ruled letter, each answered by the seat in claim revision 1 on #21329 and each a named part of the approval: the organization wall and a required capability. Where either applies, visibility alone admits, as before this PR. Neither is ever wider than main plus the two ruled alternatives. Both are described below.

Measured on main (bdd3654f2), before the change

Driven in-process through the runtime dispatcher's /share-links domain. The stack is the real SecurityPlugin and the real SharingServicePlugin (registerShareLinkRoutes: false), single posture. The object is an analogue of the conversation object: access.default: 'private' (no wildcard grant covers it), publicSharing on, an owner_id the owner holds.

caller POST /share-links
the owner (member baseline only) 403 PERMISSION_DENIED (the CRUD gate refuses the owner's own read)
a non-owner member 403 PERMISSION_DENIED
a Modify-All holder 201
a hierarchy manager: write depth unit covering the owner, no read grant (canManageShares answers true for them) 403 PERMISSION_DENIED
anonymous resolve of a minted link (the Modify-All holder's) 200, record served

PR #21429 had not landed at bdd3654f2, so the plugin route door was not measured there. It has landed since, this branch merges it, and the pins below read both doors.

What changed

  • SharingService (sharing-service.ts). canManageShares' owner and Modify-All branches move, unchanged, into one private helper ownerOrBypass. It returns admit, refuse or undecided, and undecided carries the owner value. canManageShares reads it, then runs its DEPTH branch exactly as before. The new public canMintWithoutVisibility(object, recordId, context) reads the same helper. No second notion of ownership is written.
  • How the hierarchy branch is kept out of minting. canMintWithoutVisibility never calls resolveWriteScope or the hierarchy resolver. It is a different method over the shared helper, not canManageShares with a branch that happens to say no. Pinned at both levels: the manager is a share-manager (canManageShares is true and their revoke succeeds), their mint is refused, and the write-scope probe's call count does not move during the mint.
  • ShareLinkService.createLink (share-link-service.ts). The order is: the publicSharing opt-in, then the request-shape checks (permission, audience, email allowlist), then authority, then eligibility, then expiry. Authority means visibility first. Only when that read refuses does the service ask the late-bound canMintWithoutVisibility option. When it admits, the row is read under the system context, so eligibility judges the row an anonymous holder would be served. A refusal the visibility read throws is kept and re-thrown unless the probe admits, so every caller refused before this PR, and every caller a narrowing stops, gets the same envelope it got on main. A throwing probe is a refusal. A host that builds ShareLinkService without the option keeps visibility alone.
  • SharingServicePlugin (sharing-plugin.ts, one hunk). It wires canMintWithoutVisibility into the link service beside canManageShares.
  • packages/spec, TSDoc only. IShareLinkService.createLink now states who may mint. It used to say "you may only link-share a record you can yourself see". ISharingService.canManageShares now describes its hierarchy-manager branch, which is implemented, and says that branch is not mint authority. No schema, key, type or export changes.

Refusal envelopes, in order:

step refusal
opt-in 422 SHARING_NOT_ENABLED
request shape 422 PERMISSION_NOT_ALLOWED / AUDIENCE_NOT_ALLOWED, 400 VALIDATION_FAILED
authority, the read threw (CRUD grant, or the capability gate) the gate's own refusal re-thrown: PERMISSION_DENIED, 403 at both doors
authority, the read was empty 403 FORBIDDEN; a system caller on a missing record gets 404 RECORD_NOT_FOUND
eligibility 422 RECORD_NOT_ELIGIBLE / ELIGIBILITY_UNEVALUABLE

Narrowing 1: the organization wall

Under the group and isolated postures (ADR-0105 D1), canMintWithoutVisibility answers false and visibility alone admits, as on main. The visibility read is what applies Layer 0, and neither alternative knows the record's organization. The owner column outlives a membership: a member who left an organization still owns the rows they created there. The Modify-All probe (hasWriteBypass) is object-wide.

Without this narrowing, the owner alternative carries a mint across the wall. Measured through the real SharingServicePlugin wiring and the real computeTenantLayer0Filter: a member of plant B who owns a record in plant A gets 201, and a link lands on plant A's record. That is ablation A4 below, under both group and isolated. The posture is the one organizationScopeRequired() already reads, and it fails closed: an unresolvable posture counts as walled. Cloud runs one database per environment (ADR-0095), so single, and the card's own case is served in full. Revoke authority is untouched by the wall.

Narrowing 2: a required capability

When the visibility read was refused because the caller lacks a capability the object requires (requiredPermissions, ADR-0066 D3), that refusal is a hard stop. Neither alternative applies past it, and the caller gets the capability refusal itself. The owner exception is about the record row, not about a capability an administrator withheld for the whole object.

The premise, measured before building. The premise was that this refusal is distinguishable from the owner-private CRUD refusal by a signal that already exists and is declared. It is, through ISecurityService.explain:

  • explain is a declared, non-optional contract method, reached through the same security service handle the sharing service already probes.
  • Its report is declared in spec (ExplainDecisionSchema) with a required_permissions layer and a denies verdict.
  • The explain engine computes that layer with the read gate's own capability fold: the same requiredPermissions normalisation, the same held-capability union and the same ADR-0090 D10 delegator intersection.
  • On an owner-private object that requires no capability, the same report gives required_permissions: not_applicable and object_crud: denies.

The refusal's thrown shape is not usable: PermissionDeniedError.details is typed as an open record (string keys, unknown values), the spec error envelope types details as z.unknown(), and missingPermissions is declared nowhere. No new contract method is added. The sharing side's own structural slice, SharingSecurityProbe, gains an optional explain for the existing method.

Fail-closed. Admitting needs positive evidence: the layer present with neutral (capabilities held) or not_applicable (none required). A security service without explain, a throw, a report missing the layer, or any other verdict is a stop. The one exception is a deployment with no security service at all, where nothing enforces a capability gate. explain runs only for a principal an alternative already admitted, so a refused stranger never pays for it.

File surface

This follows claim revision 1:

  • share-link-service.ts and sharing-service.ts;
  • sharing-plugin.ts (one hunk, accepted);
  • the TSDoc-only hunks in packages/spec/src/contracts/share-link-service.ts and sharing-service.ts (cross-lane domain:spec);
  • the pins, the changeset, ADR-0111 (D8, its status line, and the Consequences line on hierarchy managers) and the share-link docs paragraph.

Not touched: permission sets, and any spec shape, key or export.

Pins

  • Beside the service (plugin-sharing/src/share-link-service.test.ts, 26 cases in the A′ block). A real SharingService is wired as the plugin wires it, with probe doubles. The doubles give the security probe an explain whose required_permissions layer is computed from the same held-capability table the read double refuses with.
    • The four ruled cases: the owner mints and the link resolves; the hierarchy manager is refused, with a control that canManageShares is true and the write scope is never asked; a non-owner member is refused; Modify-All mints with its visibility read refused.
    • Ordering and envelopes: visibility admits without asking the probe; an empty read stays 403 FORBIDDEN; the opt-in comes before the probe; eligibility comes after the owner is admitted; a caller with no authority is refused before eligibility.
    • The wall (isolated, group, an unresolvable posture), a deployment without the probe, a throwing probe, and a system caller's 404.
    • The capability stop. An owner lacking the capability is refused with the capability refusal itself (details.missingPermissions on the rejection), nothing lands, and canManageShares is still true for them. A Modify-All holder lacking it is refused the same way. Two controls mint: an owner who holds the capability and is refused only by the CRUD grant, and the owner of an object that requires no capability. A refused stranger never calls explain. Four fail-closed probe shapes stop the owner. With no security service at all, the alternative stands.
  • At both doors (runtime/src/domains/share-links-enforcement-context.test.ts, the [#21329] block, 8 cases). The real SecurityPlugin and the real SharingServicePlugin (registerShareLinkRoutes: false) compose the service. The dispatcher's handleShareLinksRequest and the plugin's registerShareLinkRoutes both drive it.
    • The owner gets 201 at both doors, and the anonymous resolve answers 200.
    • The stranger gets 403 PERMISSION_DENIED at both doors, and nothing lands.
    • Modify-All gets 201 at both doors.
    • The manager gets 403 at both doors, nothing lands, the write scope is not asked, and their revoke of the owner's link answers 200.
    • On a capability-gated, owner-private object. A persona control: the owner's read is refused at the capability gate (missingPermissions: ['view_vault']), and the capable owner's only at the CRUD grant. The owner lacking the capability gets 403 PERMISSION_DENIED at both doors, with the refusal's own message, and nothing lands. The control: the capable owner mints at both doors. The wire carries the refusal's user-facing sentence only, so which gate refused is pinned beside the service.
  • At the organization wall (plugin-security/src/share-link-tenant-wall.test.ts, 3 cases). A cross-organization owner is refused 403 FORBIDDEN under group and isolated, with the store read back empty. The control: the same owner mints in their own organization.

Ablations (sharing-service.ts, each from a committed head through scripts/ablation-replace.mjs)

mutation service suite (src) both doors (dist) wall suite (dist)
A1 owner branch never admits (at 6bf7f10f6) 5 red: the owner pins, plus the wall cases' canManageShares(owner) control 2 red: [owner], and [manager], whose revoke control mints as the owner green
A2 canMintWithoutVisibility asks canManageShares in place of ownerOrBypass (the hierarchy branch let in; at 6bf7f10f6 and again at 103113347 on the rewritten line) 1 red both times: the manager mints 1 red both times: [manager], 201 where 403 was expected green
A3 bypass branch never admits (at 6bf7f10f6) 1 red: Modify-All with visibility refused green: the Modify-All holder reads the private object green
A4 wall guard removed (at 6bf7f10f6) 3 red: isolated, group, an unresolvable posture green 2 red: the cross-organization owner gets 201
A5 capability stop removed (at 103113347) 6 red: the owner and the Modify-All holder lacking the capability, and the four fail-closed shapes 1 red: [capability], where the owner gets 201 on vault_owner green
  • The dist legs rebuilt plugin-sharing. scripts/ablation-dist-preflight.mjs found each marker present before the run.
  • Restore: git diff HEAD was empty on every leg. A rebuild, then the preflight with --absent, exited 0 for every marker, with the tree clean, and the suites were green again.
  • The first A1 attempt did not run. Its marker was stripped by the bundler, so the reading was void, and it was redone with a marker that survives the build.
  • In A5 the build's DTS pass failed (TS6133: the stop's helper becomes unused). The JS pass the suites consume emitted, and the preflight found the marker in both built files.
  • The round-2 commits rewrote the line A2 mutates, so A2 was rerun on the new line at 103113347, with the same direction. The lines A1, A3 and A4 mutate are byte-identical at 103113347.

Tests (head 103113347)

  • plugin-sharing: typecheck OK (the test layer holds 2 files / 3 errors / 3 signatures, held). test: 38 files, 954 passed.
  • plugin-security: typecheck OK. test: 162 files, 3519 passed, 45 skipped.
  • runtime: typecheck OK (the test layer holds 27 files / 190 errors / 68 signatures, held). share-links-enforcement-context and share-links-internal-hash-probe: 2 files, 29 passed.
  • spec contracts/sharing-service.test.ts (which pins canManageShares' doc naming modifyAllRecords) and contracts/share-link-service.test.ts: 2 files, 17 passed.
  • Gates: dispatch-gates --commands derives 118 commands, including the spec families for the two contract files (check:api-surface, check:docs, check:export-origins, check:skill-refs and others). Every one exits 0, and --ran reconciles 118 derived, 118 run, 0 NOT-MEASURED. check:dual-build-cjs-loads first exited 3 on this fresh worktree's unbuilt packages; after a full build it exited 0.
  • Lint, narrowed: eslint --no-inline-config --format json over the 8 changed TS files reports 8 files, 0 errors, 0 warnings. The config resolves for each file (5 to 6 rules). eslint.config.mjs enables no type-aware linting (its own note, lines 327-328), so this diff cannot move a verdict on an untouched file. The full pnpm lint is CI's.
  • Dogfood share-links-self-list, showcase-client-liaison-fixtures and audit-log-internal-fields passed at 41f8cf30c (3 files, 19 passed). Not rerun at this head; CI's Dogfood Regression Gate runs them.

Changeset

.changeset/21329-share-link-owner-mint.md grades @objectstack/plugin-sharing minor (the widening) and @objectstack/spec patch. The spec grade follows two rules:

  • The TSDoc ships: measured, the new sentences are in packages/spec/dist/contracts/index.d.ts. So it is a published change, and AGENTS.md (Post-Task Checklist, step 3) gives a fix-class change in a released package patch.
  • check-changeset-no-major's level axis makes the Clause-②: yes minimum PR-scoped ("at least one" moved package at minor+), and plugin-sharing carries it.

Docs

  • content/docs/protocol/objectql/security.mdx, "Who may mint and revoke": the three alternatives, the hierarchy-manager exclusion, both narrowings and the order. Its revoke parenthetical now names the hierarchy manager, as canManageShares has admitted since the DEPTH extension.
  • skills/** holds no sentence about mint authority.
  • ADR-0111:
    • D8 rule 1 is restated as ruled, with the ruling id. Both narrowings are recorded as named parts of the approval.
    • The D-future sentence now says the tightening stays recorded and untaken.
    • The status line names both narrowings.
    • The Consequences line on hierarchy managers now says DEPTH landed, depends on the enterprise resolver, and is not mint authority.

Acceptance notes

  • Both refusal kinds look the same on the wire: 403 PERMISSION_DENIED, the same user-facing sentence, no capability names. That is by design, since details carries the developer half. So which gate refused is pinned at the service, on the rejection itself.
  • The merge commit 6bf7f10f6 holds the conflict resolution in the runtime test. Both describe blocks are kept, and the landed plugin-door helper mintOnPluginDoor takes an optional body whose default is the one it always sent.

维护者速读(草稿)

改了什么:私有对象上的记录,主人现在可以自己生成分享链接;拥有“全部修改”权限的管理员也可以。仍然必须先在对象上开启公开分享,资格条件也照旧检查。部门上级虽然能撤销下属记录上的链接,但如果看不到这条记录,仍然不能生成新链接。规范里对应的两段接口说明也一起改了,只改说明文字,不改任何字段。

为什么改:AI 会话这类“只有主人能看”的对象,数据接口连主人自己也挡在外面,所以普通成员一直分享不了自己的会话(403)。您 10 月 2 日的裁决(A′)定下这个口径,ADR-0111 D8 第 1 条随之改写。

风险与代价(含回滚):比裁决字面多收紧了两处,都是有意为之,请一并确认。

  • 多组织共库部署(group / isolated)下,两条新通道关闭,只看可见性。否则已离开组织的成员能把旧组织里自己的记录做成公开链接,已实测会泄露。
  • 对象要求某项能力而调用者没有时,两条新通道同样不放行。管理员收回了能力,主人就不能绕过去。

两处之外的行为与改动前一致;云上一个环境一个库,主人分享会话不受影响。回滚即还原本 PR,行为退回只看可见性。

席位意见:

你要做的:确认上面两处收紧,然后批准本 PR。


Generated by Claude Code

claude added 6 commits October 2, 2026 15:53
…ct at the dispatcher door

The ruled matrix (ADR-0111 D8 rule 1, ruling A'): the owner mints on an
owner-private object, a hierarchy manager without visibility is refused, a
non-owner member is refused, Modify-All mints, and an anonymous holder
resolves the owner's link. Booted on the real SecurityPlugin and the real
SharingServicePlugin (registerShareLinkRoutes: false).

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…er may mint a share link without visibility (ADR-0111 D8 rule 1, ruling A')

createLink admits visibility OR owner OR Modify-All bypass, behind the
publicSharing opt-in and before eligibility. The owner and bypass halves are
canManageShares' own first two branches (ownerOrBypass), exposed as
SharingService.canMintWithoutVisibility without the hierarchy-depth branch,
and withheld where an organization wall is in force. The plugin wires the
probe into ShareLinkService beside canManageShares.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…e the service and the owner alternative at the organization wall

Beside the service: the owner mints on an owner-private object, a hierarchy
manager without visibility is refused (and its write scope is never asked),
a non-owner member is refused, Modify-All mints with visibility refused; the
opt-in comes first and eligibility last. Through the real plugin wiring and
the real Layer 0: a member who owns a record in an organization they are not
in is refused under group and isolated.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…ure sentence; who may mint in the share-link docs; changeset

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…are-link-mint-authority

# Conflicts:
#	packages/runtime/src/domains/share-links-enforcement-context.test.ts
…sSystem reads

A system caller's read runs under the system context, and the probe admits
only on a row it re-reads the same way, so the two added checks bought
nothing and moved the system-context census. Pinned: a system caller keeps
its 404 for a missing record with the probe wired.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 2, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-sharing, @objectstack/spec, touching 15 documentable anchor(s).

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/index.mdx (via ISharingService (symbol, a top-level interface))
  • content/docs/kernel/index.mdx (via canManageShares (symbol, a method of class SharingService))
  • content/docs/kernel/runtime-services/sharing-service.mdx (via ISharingService (symbol, a top-level interface), canManageShares (symbol, a method of class SharingService))
  • content/docs/permissions/authorization.mdx (via not_applicable (literal, a string literal in capabilityGateRefusesRead), required_permissions (literal, a string literal in capabilityGateRefusesRead))
  • content/docs/permissions/explain.mdx (via not_applicable (literal, a string literal in capabilityGateRefusesRead), required_permissions (literal, a string literal in capabilityGateRefusesRead))
  • content/docs/permissions/rls.mdx (via not_applicable (literal, a string literal in capabilityGateRefusesRead))
  • content/docs/permissions/system-context.mdx (via canManageShares (symbol, a method of class SharingService), createLink (symbol, a method of class ShareLinkService))
  • content/docs/protocol/objectql/security.mdx (via canManageShares (symbol, a method of class SharingService))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via required_permissions (literal, a string literal in capabilityGateRefusesRead))
  • content/docs/releases/v14.mdx (via createLink (symbol, a method of class ShareLinkService))
  • content/docs/releases/v17/17-3.mdx (via SharingService (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 53fd35e3e3a0b18b790ab79bd2c65f353e11ab65 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5be3cb898d8d15b3d61a7b308a544b914e731fb6 — the merge of head 1031133472b9544fed48d15ee6ee3be419c00809 into base 53fd35e3e3a0b18b790ab79bd2c65f353e11ab65, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5be3cb898d8d15b3d61a7b308a544b914e731fb6 && git checkout 5be3cb898d8d15b3d61a7b308a544b914e731fb6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53fd35e3e3a0b18b790ab79bd2c65f353e11ab65 1031133472b9544fed48d15ee6ee3be419c00809 && git checkout -B drift-repro 53fd35e3e3a0b18b790ab79bd2c65f353e11ab65 && git merge --no-ff 1031133472b9544fed48d15ee6ee3be419c00809

node scripts/docs-audit/affected-docs.mjs --json 53fd35e3e3a0b18b790ab79bd2c65f353e11ab65

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 53fd35e3e3a0b18b790ab79bd2c65f353e11ab65 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… owner and Modify-All mint alternatives do not pass

canMintWithoutVisibility answers false when the ADR-0066 D3 capability
AND-gate refuses the caller a read, read from the declared
required_permissions layer of ISecurityService.explain; createLink then
re-throws the capability refusal as it came. Positive evidence is needed to
admit; a probe without explain, a throw or a report without the layer stop.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Oct 2, 2026
claude added 3 commits October 2, 2026 18:12
…h the capable-owner control

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
….createLink and ISharingService.canManageShares TSDoc; the capability hard stop in D8 rule 1; the Consequences line on hierarchy managers; spec patch in the changeset

TSDoc only in packages/spec: no schema, key, type or export changes.

Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

domain:services 席位 2 · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-02T19:33Z · 对照 103113347 的 diff 校正

改了什么:私有对象上的记录,主人现在可以自己生成分享链接;持有"全部修改"(Modify-All)权限的人也可以。

  • 照旧要求两件事:对象先开启公开分享;资格条件仍然检查。
  • 部门上级能撤销下属记录上的链接,但看不到这条记录时,仍然不能生成新链接。
  • 规范(spec)里两段接口说明、ADR-0111 D8 第 1 条和后果段、权限文档一起改了。只改说明文字,不改任何字段或导出。

为什么改:AI 会话这类只有主人能看的对象,数据接口连主人自己都挡在外面,普通成员分享自己的会话一直是 403。10 月 2 日总监席的裁决 A′(您回复「同意」)定了这个口径。

风险与代价(含回滚):本 PR 比裁决字面多收紧了两处,都是席位有意定的,请一并确认。

  • 多组织共库部署(group / isolated):两条新通道关闭,只看可见性。 否则已离开某组织的成员,能把旧组织里自己名下的记录做成公开链接,已实测会泄露。
  • 对象要求某项能力而调用者没有:两条新通道同样不放行。 管理员收回了能力,主人不能借"我是主人"绕过去。判断用的是已声明的 explain 接口,没有新增接口。
  • 除这两处外,行为与改动前一致。云上一个环境一个库,主人分享会话不受影响。
  • 回滚:还原本 PR,行为退回只看可见性。

席位意见:建议批准。

  • 实现只复用 canManageShares 原有的"主人 / 全部修改"两条分支,只有一处判断。上级那条分支被消融实验证明进不了生成链接的路径。
  • 两处收紧都是失败即关闭,比裁决字面更窄,不会比 main 加 A′ 更宽。
  • CI 全绿,契约档复审正在进行。

你要做的:确认上面两处收紧,然后批准本 PR(Approve)。批准后席位负责落地,不需要您再操作。


Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

3 participants