The Python track
Part I, chapter for chapter, from a language with no kernel in it. Every listing is in
examples/python/ and was run against a served ikigai-cli 0.1.18 as it was written.
pip install rdflib /path/to/ikigai-python # a checkout; not on PyPI yet
ikigai serve /tmp/ikbook-kernel.sock & # a kernel to talk to
Resolution: a name, over the wire
# A name, resolved. `connect` speaks the wire protocol over the kernel's socket; the
# answer is a representation — bytes with a type — exactly what a Rust caller gets.
k = ikigai.connect(path)
rep = k.source("urn:iki:fn:toUpper", **{"in": "resource oriented computing"})
print(rep.text) # RESOURCE ORIENTED COMPUTING
print(rep.media_type) # text/plain;charset=utf-8
print(rep.cache_status.name) # how the kernel's cache answered: MISS, HIT, UNCACHEABLE
connect speaks the wire protocol — a versioned hello each way, then framed requests —
and source is the verb. The answer is a representation: text, a media type, and how
the kernel’s cache answered. Nothing about it says the endpoint was Rust, or in another
process, which is Resolution’s point made from the
other side.
Hello, resource: a decorated function
# The decorator is the description, and the signature is the contract. `who: str`
# becomes a required input of class xsd:string; the `Annotated` text becomes its
# summary; `cacheable=True` is the same claim `.cacheable()` makes in Rust — a pure
# function of its declared inputs — and it is the mounting kernel, not this process,
# that will honor it.
@endpoint("urn:py:hello", summary="Greet someone", cacheable=True)
def hello(who: Annotated[str, "the name to greet"]) -> str:
return f"Hello, {who}!"
# A second one, so `list` has something to show and a pipe has something to feed.
@endpoint("urn:py:shout", summary="Uppercase a string, loudly", cacheable=True)
def shout(text: Annotated[str, "the text to shout"]) -> str:
return text.upper() + "!"
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/py-hello.sock"
print(f"serving urn:py:hello and urn:py:shout on {path}", file=sys.stderr)
serve([hello, shout], path) # blocks; speaks the wire protocol
Compare Hello, resource: a function, a
description, a binding. Here the decorator is the description and the socket path is the
binding — this process is a space, and serve speaks the same protocol a Rust host
speaks to any other peer.
Why an endpoint describes itself: the signature is the contract
In Rust you wrote the ArgSpecs by hand. Here they are derived from the signature:
who: str is a required input of class xsd:string, the Annotated text is its
summary, a default would make it optional, Literal[...] would be one_of. The
description a Rust host sees is exactly the one it would see from a Rust endpoint:
$ ikigai --plain --mount urn:py:=/tmp/py-hello.sock -c 'describe urn:py:hello text/plain'
hello —
Greet someone
verbs: Source, Meta
input who [argument]: the name to greet
outputs: text/plain;charset=utf-8
And from the client side, the same card as data — the JSON face of Meta, parsed:
# Self-description, as data. `describe` is the JSON face of Meta, parsed: the
# ArgSpecs an endpoint declared, which is what an agent's tool definition is made of.
card = k.describe("urn:iki:fn:toUpper")
print(card["id"], [arg["name"] for arg in card["inputs"]]) # toUpper ['in']
# ...and the same card as a graph, which the notebook chapter queries.
turtle = k.meta("urn:iki:fn:toUpper", as_="text/turtle").text
print(turtle.splitlines()[0]) # @prefix ik: <https://ikigai-rs.dev/ns#> .
Binding: a mount is a binding
python3 examples/python/hello.py /tmp/py-hello.sock &
ikigai --plain --mount urn:py:=/tmp/py-hello.sock -c 'source urn:py:hello who=Ada'
Hello, Ada!
[computed]
The Python process bound nothing under urn:py:; the host did, with --mount,
exactly as Binding, and a host of your own said a host
decides the name. list shows where the name is served from:
$ ikigai --plain --mount urn:py:=/tmp/py-hello.sock -c list
urn:py:hello → hello [/tmp/py-hello.sock]
urn:py:shout → shout [/tmp/py-hello.sock]
What resolution buys you: the kernel’s cache and trace, from outside
# Cached once: the probe says not yet; a resolution; the probe says served now.
print(k.is_cached("urn:iki:fn:toUpper", **{"in": "a b"})) # False
k.source("urn:iki:fn:toUpper", **{"in": "a b"})
print(k.is_cached("urn:iki:fn:toUpper", **{"in": "a b"})) # True
# Traced: the kernel records its own events and ships them back over the wire.
rep, events = k.source_traced("urn:iki:fn:toUpper", **{"in": "a b"})
for event in events:
print(event.target, "cache_hit" if event.cache_hit else "computed")
The cache is the kernel’s. cacheable=True on the decorated function only marks the
representation; served straight from Python nothing is cached, and mounted through a Rust
kernel the same function’s answers are — the cache verdict flips to HIT on the second
call. The trace events come back over the wire and are the same TraceEvents a Rust
tracer receives.
Capabilities: a scoped connect
# Capabilities: connect under a narrowed authority and the kernel enforces it — the
# served kernel clamps what you carry to what the channel is entitled to, and a
# write to the file workspace under a read-only scope is refused, typed.
narrow = ikigai.connect(path, capability=Capability.scoped(["urn:cap:kernel:inspect"]))
try:
narrow.sink("urn:file:notes.txt", "nope")
except ikigai.DeniedError as denied:
print("denied:", denied.message)
narrow.close()
denied: capability does not grant `urn:cap:fs:write:*` (declared by `urn:file:notes.txt`)
The client carried a narrowed capability; the served kernel clamped it to what the
channel is entitled to and enforced it. The refusal crossed the wire typed —
DeniedError, not a string to parse — which is what lets a REST face over this client
answer 403 rather than 500 (Who is asking, from a client).
What an L0 peer cannot do
camel-title in What resolution buys you resolved
title from inside its own invocation, and inherited its golden thread. A Python
endpoint cannot: there is no inv.source on this side of the wire, no back-channel from a
served peer into the kernel that is invoking it. A Python endpoint is a leaf — it takes
arguments and returns bytes — and composition happens in the kernel, above it. That is L0,
stated plainly; the ladder’s next rung is the module protocol’s host callback, which
Part II showed and which no polyglot peer speaks yet.