You want to look for white papers that talk about object-capability systems. It's a fairly old and well-trod area of research. The E programming language[1] was all about that, and it was pretty late in the game on this stuff.
You emphasize natively, but the problem is that's not really well defined. For static capabilities, you're just essentially asking for a suffciently strong module system with parameterized abstract data types. It's literally a subset of the grammar and what it's designed to express. Mark Miller (one of the creators of E) demonstrated that[2].
The knock on effect of that quality is that anything which fulfills that requirement natively supports capabilities. It's part of the grammar. Doesn't even have to be object-oriented. A hackjob demonstration of an SML filesystem library with a brand/mint object capability pattern:
brand.sig:
signature BRAND =
sig
type token
end
mint.sig: signature MINT =
sig
include BRAND
val mint : unit -> token
end
makebrand.fun: functor MakeBrand () =
struct
type token = unit ref
fun mint () = ref ()
end
filesystem.sig: signature FILESYSTEM =
sig
type token
val readFile : token -> string -> string
val writeFile : token -> string -> string -> unit
end
filesystem.fun: functor FileSystem (B : BRAND) :> FILESYSTEM where type token = B.token =
struct
type token = B.token
fun readFile (_ : token) (path : string) : string =
"contents of " ^ path
fun writeFile (_ : token) (path : string) (_ : string) : unit =
()
end
trusted_fs_setup.sml: local
structure FileAuthority :> MINT = MakeBrand ()
in
structure FS :> FILESYSTEM = FileSystem (FileAuthority)
val rootFileToken : FS.token = FileAuthority.mint ()
end
trusted_fs.cm: Library
signature FILESYSTEM
structure FS
val rootFileToken
is
brand.sig
mint.sig
makebrand.fun
filesystem.sig
filesystem.fun
trusted_fs_setup.sml
Now for any given library using the trusted_filsystem library: val doc = FS.readFile rootFileToken "/etc/motd" (* Works fine *)
Delegation is function application: fun helper (t : FS.token) = FS.readFile t "log.txt"
val log = helper rootFileToken
And these all fail: val fake : FS.token = ref () (* Trying to forge a token *)
val t = FileAuthority.mint () (* Trying to bypass the trusted kernel in trusted_fs_setup.sml by calling the mint *)
(* Trying to self-issue authority by making our own brand and mint *)
structure MyCap = MakeBrand ()
val t : FS.token = MyCap.mint () (* type mismatch *)
What's nice about this is... it's just normal modular programming. It's a very natural grain. It's also completely compile-time, no runtime overhead.You can also do a lot of this with phantom types, and it'd be much more terse and easier to handle dynamic capabilities and stuff like a capability algebra, but it ends up way less auditable and is easy to have subtle errors which defeats the point. Also compiler errors will be much more opaque. IMO needing to manually make wrappers for composite capabilities, or to handle dynamic capabilities, is the lesser of two evils. With higher order modules, those problems go away entirely.
[1] - https://en.wikipedia.org/wiki/E_(programming_language)
[2] - https://homepages.ecs.vuw.ac.nz/~kjx/papers/ARND2018.pdf