Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 typedDeniedError, 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.