Object ABI
Every executable object follows the same triple:
name executable entry: stateless, one-shot, or task entry
name.sock socket entry: stateful, multi-turn, streaming
name.d/ control directory: config, state, permissions, logs
If an object does not support stateful interaction, do not expose name.sock.
A socket that only reports errors is bad ABI.
Do not expand one object into profile/, runtime/, policy/, control/,
and other layered trees. If a small file can say it, put it in .d/.
Names and Aliases
ABI names under agent and tool are single path components. Model names are
the exception: /ctx/model uses the original provider/model namespace as two
path components.
Agent, tool, provider, and model path-component syntax:
[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}
Forbidden:
/
NUL
empty string
.
..
control characters
newline
suffix .sock
suffix .d
Rules:
agent/tool filename is the stable alias
model stable identity is provider/model using the original model provider
aggregator name, API format, and base URL do not enter stable model names
native ids may live in .d/id, .d/driver, or another control file
short user aliases use symlinks
Example:
/ctx/model/openai/gpt-4o
/ctx/model/openai/gpt-4o.d/id = openai/gpt-4o
/ctx/home/1000/model/main -> /ctx/model/openai/gpt-4o
Alias resolution:
symlink means symlink
readlink decides object identity
there is no alias.d override semantics
one path is not half alias and half real object
If coder needs its own default parameters, create a real object instead of a
symlink overlay:
/ctx/model/openai/gpt-4o-coder
/ctx/model/openai/gpt-4o-coder.d/id
/ctx/model/openai/gpt-4o-coder.d/default
Exec Protocol
model/<provider>/<model>, agent/<name>, and tool/<name> are executable
files. They must accept argv or stdin input:
/ctx/model/openai/gpt-4o "hello"
echo "hello" | /ctx/model/openai/gpt-4o
echo '{"messages":[{"role":"user","content":"hello"}]}' | /ctx/model/openai/gpt-4o
/ctx/agent/coder "fix this project"
echo '{"task":"fix tests"}' | /ctx/agent/coder
/ctx/tool/fs.read '{"path":"README.md"}'
echo '{"path":"README.md"}' | /ctx/tool/fs.read
stdout should be JSONL:
{"type":"start","run":"r1","model":"openai/gpt-4o"}
{"type":"delta","run":"r1","text":"hello"}
{"type":"done","run":"r1","status":"ok"}
Human-readable output can exist as compatibility mode. Machine callers should prefer JSONL.
Reading an executable object returns inspectable metadata, not implementation code. Built-in model and tool executable files use the common CortexFS object runner shebang:
#!/usr/bin/cortexfs-object-runner
tool/<name> must not expose a per-tool shell script as file contents. Tool
implementation dispatch is runtime behavior behind the common runner; name.d/
remains the inspectable control surface.
Exit codes:
0 success
1 generic error
2 bad arguments or bad input format
13 permission denied, maps to EACCES
69 service unavailable, object exists but runtime is unavailable
70 internal error
If stdout has already started emitting JSONL, errors should continue as JSONL error frames. The exit code is only the process-level summary.
TTY rules:
model/<provider>/<model> with no args on a TTY may enter a simple REPL, but is not required to
agent/<name> with no args on a TTY must enter an interactive socket session
tool/<name> with no args on a TTY should print short usage or read stdin, not start a long session
Socket Protocol
model/<provider>/<model>.sock and agent/<name>.sock are Unix domain
sockets. The protocol is JSONL.
Requests:
{"op":"send","id":"msg-1","session":"default","cwd":"/work","input":"hello"}
{"op":"resume","session":"default","after":"event-id"}
{"op":"cancel","id":"run-id"}
{"op":"ping"}
Responses:
{"type":"start","id":"event-id","run":"run-id","model":"openai/gpt-4o"}
{"type":"delta","id":"event-id","run":"run-id","text":"..."}
{"type":"message","id":"event-id","run":"run-id","role":"assistant","content":[{"type":"text","text":"..."}]}
{"type":"error","run":"run-id","code":"EACCES","message":"permission denied"}
{"type":"done","run":"run-id","status":"ok"}
{"type":"pong"}
Socket lifecycle:
missing object does not support stateful mode, or service is not started
ECONNREFUSED object declares a socket, but the process is unavailable
connected requests and responses are JSONL frames
closed private/shared sessions are not deleted
Hard socket rules:
max frame size v1 is 1 MiB; larger frames return EMSGSIZE
unknown fields must be ignored
unknown op returns EINVAL
after disconnect private/shared sessions continue by default; temp sessions may be cancelled
client id retry within one session, retry is idempotent and returns the original run id or final state
delta order strictly increasing send order within one run
backpressure blocking write is backpressure; implementation must not buffer forever in memory
When a client receives SIGINT, it should first send cancel to the socket.
Only the second interrupt, or a broken connection, should exit the client
process.
Error frames use stable errno names such as EACCES, EINVAL, ENOENT,
EMSGSIZE, and EHOSTDOWN. Clients must not parse natural language message.