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
This triple describes discoverable object surfaces, not three independent
identities. name and name.d/ identify and describe the object. name.sock
is an optional transport entry for a live stateful service; it may be an alias
to a runtime-owned socket and must not be treated as the object's lifecycle
record or implementation ABI.
If an object does not support stateful interaction, do not expose name.sock.
A socket that only reports errors is bad ABI.
Agent and tool control directories may contain a hook convention subtree:
name.d/
hooks/
pre.d/
post.d/
pre.d contains hooks that run before the object action; post.d contains
hooks that run after the object action. This is an object-local convention under
agent and tool; it does not create a /ctx/hook root namespace. Model
control directories stay limited to provider/model controls and do not carry
empty hook trees. The stable ABI defines the directory shape only. Implementations may keep
compiled hook state in process memory, but development refresh is still a Git
commit/runtime restart boundary; CortexFS does not define background
watchers, polling, or hot reload.
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-5.6
/ctx/model/openai/gpt-5.6.d/id = openai/gpt-5.6
/ctx/home/1000/model/main -> /ctx/model/openai/gpt-5.6
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-5.6
/ctx/model/openai/gpt-5.6.d/id
/ctx/model/openai/gpt-5.6.d/default
Exec Protocol
Executable agents use the required agent/<name>.d/abi control. Its accepted
value is sdk-envelope-v1; the runtime supplies the typed invocation described
by the Agent Runtime specification.
The executable mode of agent/<name> continues to mean that the agent object
can be invoked. It MUST NOT be overloaded to mean shell permission, because
clearing that bit would also make the agent ABI itself non-executable. The
separate agent/<name>.d/perm marker carries the inspectable rwx capability
ceiling.
model/<provider>/<model> and tool/<name> are executable files that accept
argv or stdin input. Agent executables are host-launched through the SDK
envelope rather than called with user argv:
/ctx/model/openai/gpt-5.6 "hello"
echo "hello" | /ctx/model/openai/gpt-5.6
echo '{"messages":[{"role":"user","content":"hello"}]}' | /ctx/model/openai/gpt-5.6
/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-5.6"}
{"type":"delta","run":"r1","text":"hello"}
{"type":"done","run":"r1","status":"ok"}
Executable objects must emit the canonical JSONL sequence. Raw human-readable stdout is invalid tool output.
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.
For installed executable plugins, the backing source keeps the verified
artifact bytes needed for execve, while the FUSE read projection uses the
object class and .d controls to return canonical inspectable metadata through
object_exec_metadata. Reading /ctx/tool/<name> or /ctx/agent/<name>
therefore does not reveal the plugin binary or source implementation.
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":"/workspace","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-5.6"}
{"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
The durable agent object may use agent/<name>.sock as a stopped placeholder.
When started, the visible agent socket may be an owner-authorized symlink to the
live socket below /run/user/<uid>/cortexfs/agent/, or a direct socket node at
/ctx/agent/<name>.sock in some deployments. Terminal sessions expose
home/<uid>/agent/<name>/session/<session>/terminal/main.sock as a symlink to
the matching runtime terminal socket. A start is not ready until both requested
visible aliases have been created and verified.
Hard socket rules:
max frame size 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.
Executable Object Manifests
The legacy manifest schema remains strict:
cortexfs.object/v1 accepts neither version nor compatibility
version and compatibility are rejected on v1 rather than ignored. A v2
manifest must instead provide both fields:
{
"schema": "cortexfs.object/v2",
"version": "0.1.0",
"compatibility": {
"cortexfs": ">=0.1.7, <0.2.0"
},
"class": "tool",
"name": "example.echo",
"executable": {
"path": "target/release/example-echo-tool",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
},
"controls": {
"description": "Echo text.",
"schema": "{\"type\":\"object\"}",
"cap": "text",
"policy": ""
}
}
version is an object SemVer. compatibility.cortexfs is a Cargo-style
SemVer requirement. Both ctx object check and ctx object install match that
requirement against the CortexFS package version compiled into the current
ctx; a mismatch is invalid input, exits 2, and performs no writes. Unknown
fields remain rejected for both schemas.
A cortexfs.object-install/v2 receipt records object_version and
cortexfs_requirement; a cortexfs.object-install/v1 receipt records neither.
Inspection reports the recorded compatibility facts. They are
installation-admission and audit facts, not authority grants, and do not start
a runtime. If a later CortexFS build no longer satisfies the recorded
requirement, that mismatch must not prevent receipt-managed uninstall.
Installation remains new-object-only. Receipt-managed replacement is a separate lifecycle operation with a mandatory v2 candidate:
ctx object replace --source PATH MANIFEST [--tier user|system] [--yes]
ctx object upgrade --source PATH MANIFEST [--tier user|system] [--yes]
ctx object rollback --source PATH MANIFEST [--tier user|system] [--yes]
replace accepts an existing receipt-managed v1 or v2 object and imposes no
version ordering, so it is the migration path from a legacy v1 receipt to v2.
upgrade requires an existing v2 object and a strictly higher candidate v2
version. rollback requires an existing v2 object and a strictly lower
candidate v2 version. CortexFS keeps no version history: the rollback caller
must provide the older v2 manifest and its exact hash-bound artifact.
All three commands default to a no-write dry-run; --yes applies the
transition. The candidate manifest must name the exact installed class/name,
must pass current CortexFS compatibility validation, and must use
cortexfs.object/v2. Compatibility still grants no authority.
Applied replacement prepares and syncs the complete candidate in a same-filesystem stage. It hides the old executable first, transitions the exact receipt-managed pair, and publishes the new executable last as the visible commit boundary. A pre-commit failure automatically rolls the exact old pair back when its receipts still match. At receipt checkpoints the operation does not intentionally overwrite or delete a foreign inode; a conflict or failed safe restoration may retain audit-visible safety residue.
This protocol does not claim pair atomicity and cannot close Linux's final
pathname syscall race against a hostile writer with the same Unix authority.
Before --yes, the caller must quiesce the matching runtime and other writers.
Replacement itself keeps no version archive, does not stop or start a runtime,
does not grant policy authority, and does not create socket state.
Installed Object Replacement, Inspection, and Removal
The host-side inspection and receipt-managed removal surfaces are:
ctx object inspect --source PATH CLASS NAME [--tier user|system]
ctx object uninstall --source PATH CLASS NAME [--tier user|system] [--yes]
CLASS is tool or agent, and the tier defaults to user. Inspection
validates the installer receipt and its identity/version, the recorded
class/name/tier, the retained control directory's device/inode/type, and the
retained executable's device/inode/regular type, execute bits, and SHA-256. It
also rejects executable length, mode, mtime, or ctime changes observed during
inspection; the receipt does not bind the complete install-time mode. It holds
no-follow descriptors through validation and does not modify the backing tree.
Mutable control-file contents are outside this receipt claim and are not revalidated against their install-time values. An object with a missing or legacy receipt is unmanaged and is reported as unavailable; inspection does not adopt it. For a v2 receipt, inspection also prints the recorded object version and CortexFS requirement. Inspection does not re-run installation compatibility admission against a later CortexFS build.
Uninstall accepts only one exact installer-receipt-managed tool or agent
pair, with tier defaulting to user. It is a no-write receipt validation and
report by default. --yes first quarantines the executable on the same
filesystem to establish the invisible-object boundary, syncs and rechecks its
receipt, then quarantines the control directory, syncs and rechecks both
receipts. Only after the complete exact stage is verified does it reuse bounded
residue cleanup. This is not pair atomicity.
At receipt checkpoints, a failure does not intentionally overwrite or delete a
foreign replacement and may retain audit-visible safety residue. Before
--yes, the caller must quiesce the matching agent runtime and every writer
sharing the same Unix authority. Receipt checkpoints do not close Linux's final
pathname syscall race against such a writer. Uninstall grants no authority,
creates no socket, and does not start or stop a runtime. Uninstall validates
the installed receipt and exact pair without rejecting an object merely because
the current CortexFS build no longer matches its recorded v2 requirement.
Durable Safety Residue
Object installation may leave a hidden .cortexfs-install-* stage in the
install-class directory. Applied cleanup temporarily uses a
.cortexfs-cleanup-* quarantine, and agent creation rollback may leave
.ctx-rollback-*. These names are safety residue, not object names and not a
second submission or orchestration namespace.
The host-side maintenance surface is:
ctx object residue audit --source PATH
ctx object residue cleanup --source PATH --path REL --dev DEV --ino INO [--yes]
Audit is a bounded, no-follow observation of a durable source. It reports
.cortexfs-install-*, .cortexfs-cleanup-*, and .ctx-rollback-* residue.
Its reported path, device, and inode are useful for review but do not grant
authority. Cleanup requires the caller to resubmit an explicit exact receipt
and then builds a complete bounded receipt plan before mutation. Audit fails
instead of silently skipping unreadable, cross-device, or over-limit subtrees.
Only .cortexfs-install-* directories directly below tool, agent,
home/<decimal-uid>/tool, or home/<decimal-uid>/agent are eligible. Cleanup
quarantine and rollback residue are always audit-only. Cleanup is dry-run
unless --yes is present.
Applied cleanup isolates each candidate with same-directory
renameat2(RENAME_NOREPLACE), verifies the moved inode, and only then performs
descriptor-relative, post-order deletion. Symlinks are unlinked as leaves and
are never followed. If a later cleanup step fails, the original
.cortexfs-install-* name is restored only when the quarantined top-level inode
still matches the submitted receipt and no-replace restoration is safe. That
permits a fresh audit and retry. If safe restoration is impossible, the exact
retained .cortexfs-cleanup-* quarantine path is reported for later audit.
Unknown-type, depth/count, added-entry, and sync conflicts also stop cleanup.
Before --yes, the caller must quiesce processes that share write authority to
the backing directories. Linux does not provide receipt-conditioned unlink, so
no userspace design can close the final pathname syscall window against a
hostile writer in the same Unix authority boundary. At every receipt checkpoint
cleanup refuses to intentionally unlink a mismatched inode.
Rollback residue and retained cleanup quarantine are audit-only; only an eligible install residue path can be submitted to cleanup. This command never removes rollback residue. Owned agents are not residue. Install and agent-create operations do not trigger automatic background residue cleanup.