feat(runtime): save takes an explicit owner root, with an ownership accessor - #8660
feat(runtime): save takes an explicit owner root, with an ownership accessor#8660MusabMahmoodh wants to merge 21 commits into
Conversation
…ccessor Every anchor created during a request took its owner from the ambient user root at first save, with no parameter, context, or accessor to choose otherwise. A group root could be minted but could never own anything, so a grant on it covered nothing and any group-ownership design was unreachable from app code. save(obj, owner=root_id) creates the anchor under the given root when the caller holds WRITE on that root's anchor (the creator passes by ownership, members by an allow_root grant on the root, and the cascade then extends every root-level grant to the new content). The check fails loud with PermissionError, an owner that is not a root or does not exist is a ValueError, and a persistent anchor cannot be re-owned. owner_of(obj) reads which root owns an anchor. Omitting owner keeps today's behavior everywhere. Fixes jaseci-labs#8458
… and cascade-readable
# Conflicts: # jac/jaclang/runtime/impl/runtime.impl.jac
The save declaration fits on one line at 85 chars, so the manual wrap was the formatting delta. _run carried a mutable list default, which the mutable-default rule rejects; it takes None and substitutes an empty list inside instead.
The wrapped assert was the last thing jac fmt wanted to rewrite in this file. Naming the value keeps both lines under the width and matches the cascade assertion a few lines down.
The fence was bare top-level statements, so test_guide could not parse it, and it read the root id off .__jac__, which Root does not expose. It is a declared function now and takes the id through jid(), matching the other fences in this guide. Verified with jac check.
Interface block: kept `save(obj, owner)` and `owner_of`, took 0.37's destroy family (`destroy(objs, strict)`, `_destroy_anchors`, `peek_attr`, `peek_item`) whole - the old single-shape `destroy` this branch carried is superseded by jaseci-labs#8776's one-rule del. The save body merged on its own and now runs through 0.37's `ensure_id()` and `all_edges()`; the owner check sits after identity is minted, which is where it has to be now that `Anchor.id` is None until something persists.
|
CI note: the red lanes on this PR are not from this branch. Recording the
1.
A markdown file cannot cause that. Both #8654 and #8664 went 2. Whole-tree
Same 913 files, same "912 passed, 1 failed", different victim each run, and Other people's PRs look green only because their runs predate 0.37 (newest is Happy to file the |
jac check's fork work pool assigns files to workers differently on every run, and a worker serving a file with an incomplete stub prelude reports stdlib types as Self/<Unknown>. One unrelated file fails per run. No source change here.
`filename` was renamed to `target` on main, so the test file no longer compiled and test-runtime errored out on it.
Summary
Fixes #8458.
saveassignedanchor.rootfrom the ambientuser_rootat first save andexposed no way to choose otherwise, so app code could mint a group
Root()butthat root could never own anything: a member granted on it read nothing,
because all the "group's" content belonged to whichever user created it. This
is the primitive that decides whether any group-ownership design is reachable
from app code at all.
save(obj, owner=root_id)creates the anchor under the given root. Theseam already existed -
savehonours a pre-setanchor.root- it was justunreachable, so this exposes it rather than adding a parallel path.
WRITEon the targetroot's anchor (the existing
check_access_level, which is also how the RFC'smembership grant works).
PermissionErrorotherwise;ValueErrorif theowner is not a root or does not exist; a persistent anchor cannot be
re-owned. It never silently falls back to the caller's root.
Jac.owner_of(obj)answers "which root owns this?", which previouslycould only be measured by attempting a read as someone else.
owneris byte-for-byte today's behavior.One sharp edge found while testing, documented rather than papered over: a
minted
Root()arrives already-persistent with no owner, so nobody - not evenits creator - can reach it afterwards. The mint pattern must grant the creator
WRITEon it in the same function, while the reference is still in hand. Thatis now in the
jac-sv-persistenceskill next to the group-sharing section. Ideliberately did not make
saveauto-grant this: a silent implicit grant at asecurity boundary is the class of behavior these issues are about.
Test (real CLI runner, multiple real roots, no mocks)
tests/runtimelib/test_save_owner.jac, 3 tests / 3 passing, all throughexecution.runwith distinct real roots:owner=orgis owned by the org (owner_ofconfirms),an ungranted user reads nothing (control), and one
allow_rooton theorg root then makes it readable - the cascade, end to end.
READmember and a stranger are both denied(
PermissionError); the same user succeeds once grantedWRITE.ValueError.Test 1 fails pre-change at compile (
savehas noownerparameter) - thecapability did not exist.
Could this have been less code?
rootseam insave(the issue points at it) andreused
check_access_levelfor the authorization, so no new permissionconcept enters the model.
anchor.rootstays the single source of truth and
idx_anchors_rootalready indexes it.with Jac.acting_for(root)context manager (the issue's othersuggested shape): it needs
ExecutionContextto hold more than one root,which is the RFC's open question and not settled; the parameter form gets
the same capability with no context change.
above.
Deleted
owneris additive and itsabsence preserves the existing assignment exactly.
What else could this break
Grepped
JacPersistence.save,Jac.save(,anchor.root,owner_of:savehas exactly one impl and gains an optional trailing parameter, soevery existing call site is unaffected; internal callers (
saverecursinginto edges/endpoints, identity
create_user) pass no owner.identity_storage.create_user) is untouched: aRoot()still arrives persistent-with-no-owner, exactly as today.change is in the in-memory anchor before any store write;
root_idisalready a column in every backend, and no SQL changed.
session like any other read; no new shared state.
root means every member bound to that root would be an owner. That ordering
fix is Jac Scale: the archetype access hook is called with no arguments and the owner fast-path runs before it, so a per-caller role such as "viewer of this org" cannot be expressed at the traversal seam #8459's PR, deliberately separate; this PR does not change
check_access_level.