feat(runtime): caller-aware access hook that ownership does not pre-empt - #8662
feat(runtime): caller-aware access hook that ownership does not pre-empt#8662MusabMahmoodh wants to merge 18 commits into
Conversation
__jac_access__ is invoked with no arguments and the owner fast-path returns WRITE before it runs, so an archetype policy can say what every non-owner gets but never what this caller gets, and a role such as viewer-of-this-team was only expressible as an endpoint check. Both facts block any group design: bind members to a shared root and every member is an owner, so policy never runs. Archetypes may now declare __jac_access_for__(caller), receiving the calling root's UUID. It is consulted before the owner fast-path, so ownership admits and the role decides; a returned level is final for that caller, None falls through to ownership and stored grants. The system root keeps its bypass. Archetypes that declare only the zero-arg hook, or neither, resolve exactly as before; when both are declared the caller-aware hook wins. Documented in the jac-sv-multi-user skill. Fixes jaseci-labs#8459
jaseci-labs#8745 made Anchor.id optional and added ensure_id() -> UUID, so passing jroot.id straight into a UUID parameter no longer checks. The override probe also read __jac_access_for__ off the result of type(), which the checker sees as bare type. Both go through the declared surfaces now. The test helper took a mutable list default, which the mutable-default rule rejects.
jac fmt --lintfix rewrites getattr(obj, 'attr', None) to the null-safe form, so write it that way directly.
|
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.
aa713db to
3a8c178
Compare
`filename` was renamed to `target` on main, so the test file no longer compiled and test-runtime errored out on it.
Summary
Fixes #8459.
__jac_access__is invoked with zero arguments, so an archetype policy can saywhat every non-owner gets but never what this caller gets; and the owner
fast-path returns
WRITEthree lines before the hook is consulted, so foranything the caller's root owns the hook never runs at all. Together those made
a role such as "viewer of this team's doc" expressible only as an endpoint
check - the very thing an ACL-in-traversal design exists to make unnecessary.
The ordering is also a blocker for any group design: bind members to a shared
root and every member is an owner, so policy never runs (the section-6 point on
#8332).
Archetypes may now declare
__jac_access_for__(caller: UUID), consultedbefore the owner fast-path: ownership admits, the role decides.
READ, which is what makes org-style roles expressible at all.Nonefalls through to the normal resolution (ownership, thenstored grants), so conditional policies still compose.
jroot == to), so the platformcannot be locked out of its own graph.
today; the arity is resolved by comparing against the base implementation,
not by inspecting signatures per access check. When both are declared the
caller-aware one wins.
jac-sv-multi-userbeside the existing__jac_access__section.
Test (real CLI runner, three real roots, no endpoint checks anywhere)
tests/runtimelib/test_access_hook_caller.jac:TeamDocgrants WRITE to listed editor rootsand READ to everyone else. The editor's cross-user write persists; the
viewer's write is dropped at the seam (no handler check exists in the
fixture) while both can still read.
editors, so theowner's own write is refused.
owner writes, stranger cannot resolve.
Pre-fix: 2 of 3 fail (
owner bypassed the caller-aware policy, and thehook not being consulted at all leaves the doc unreadable to non-owners).
Post-fix: 3 passing.
Neighbors re-run on the fix:
test_permission_diagnostics6/6,test_jaseci18/18, andtest_acl_pushdown4/4 - the last one mattersbecause this PR reorders
check_access_level, which the #8598 pushdown workmirrors in SQL.
Could this have been less code?
check_access_level; no newPrincipalobject, no context change. Thecaller's root id is already in hand at that point.
Principalstruct (the RFC's 4.1 shape): it needsExecutionContextto carry an org binding, which is the open question onRFC: Organizations and multi-user collaboration from first principles: the tenancy primitive jac-scale SaaS apps are hand-rolling #8332 and not settled. A
UUIDis the part that exists today, and a richerprincipal can be added later without breaking this signature's callers.
__jac_access__by signature inspection: an arityprobe per access check is hot-path cost and ambiguous for inherited hooks; a
separate name resolved by an identity comparison against the base is cheaper
and unambiguous.
effect for archetypes that declare the new hook, so it is already opt-in by
construction.
Deleted
correct for type-wide policy. No test asserted the old ordering (the owner
fast-path had no coverage that a caller-aware hook could contradict), and no
config key or fallback became dead.
What else could this break
Grepped
__jac_access__,check_access_level,no_custom,access_level_cast:order (still after the owner fast-path). Covered by test 3 and by
test_permission_diagnostics.is declared. For every archetype in the tree today (none declare it), the
emitted decision is identical.
no_custom=Truecallers: skip both hooks exactly as before.form, the same as one with
__jac_access__today - those keep theobject-space filter.
test_acl_pushdownpasses unchanged; the two mustcontinue to agree, which is why that suite is in the evidence above.
check on non-owned anchors; no signature inspection, no allocation.
into the gate, bounded by the recursion limit (loud) - the same shape as an
existing
__jac_access__that does so.count: the change is pure in-process policy resolution; no storage,
transport, or deployment surface is touched.