Limitations¶
keryx is pre-1.0. The deterministic core — graph, wiki, memory, testing, health, review, tasks and security — runs offline with no model provider, and that is the part the product is built around. This page lists what is not there yet, what each gap actually costs you, and what to use instead.
Summary¶
| Limitation | Impact | Alternative |
|---|---|---|
| Remote approvals need a tool registry | The stock keryx serve registers no tools, so it raises no approvals; with a registry an ask becomes a pending approval answered over /v1/approvals or keryx approvals |
Run tool-using turns locally |
harness exec --allowed-domains is macOS-only |
The interactive/general sandbox's domain-level egress policy, credential masking and TLS termination refuse to run on Linux | Filesystem containment and network on/off work on both; a scheduled agent-task's own network: "allowlist" (flow 301) is a separate mechanism, Linux-only |
| No bundled embedding runtime | No semantic ranking in memory search | Lexical memory search remains fully available |
| No bundled ML security classifiers | Detection is rules plus entropy, not a model | Deterministic detectors run on the full corpus and are evaluated in CI |
| ripgrep is external | keryx ctx rg exits non-zero without rg on PATH |
Install ripgrep, or let the agent read files directly |
| Model commands need a credential | Five commands exit non-zero without one | Every other command is deterministic and offline |
| Windows is unverified | The core CLI is not exercised on Windows in CI | Use macOS or Linux, or WSL |
| No tools in the non-interactive harness | keryx harness run and keryx serve are single text turns |
keryx shell is where tools actually run |
| Replay validates a log, it does not re-execute | A fixture check cannot tell you whether a run would behave the same today | Record and compare fixtures for integrity |
| Session branches never merge | A fork diverges permanently | Re-fork from a shared ancestor |
| No session↔workspace auto-bind | Every SAC call needs an explicit workspaceId |
keryx workspace list, then pass the id |
| SAC MCP is stdio-only | HTTP MCP returns sac_transport_denied |
Use keryx serve-mcp (stdio) or the CLI |
SAC source reads need POSIX openat |
Fail-closed on platforms without O_NOFOLLOW |
Use macOS or Linux |
| Policy experiment is synthetic / off | No production learned policy | Keep the deterministic baseline |
Optional AI features are not bundled¶
Two model-backed features have no runtime shipped: semantic memory search and the ML security detectors. The ONNX stack was removed to keep the package small, so their runtime identifiers are empty strings:
src/memory/config.ts—runtime: ""for the memory embedding seamsrc/security/detect/index.ts—SECURITY_MODEL_RUNTIME = ""
Re-enabling them means setting those constants and installing a transformers.js-API package — it is a code change, not a downloadable asset.
Both features run on their deterministic floor in the meantime, and that floor is the shipped, tested behaviour:
- memory — lexical retrieval with indexing, dedup, bitemporal validity and module/entity/class filters.
- security — deterministic rules plus entropy analysis, measured against a
committed evaluation corpus (
keryx security eval --corpus all).
Commands that require a model credential¶
These five commands add model-generated output on top of deterministic data and exit non-zero without a configured provider credential:
keryx test suggest <file>keryx flow plan <id>keryx memory reflect --narratekeryx health explain <target> --narratekeryx wiki enrich— the exception that degrades rather than failing: it exits0and marks the affected pages skipped.
Nothing else in the CLI needs a provider.
Code search needs ripgrep¶
keryx ctx rg and the agent harness's search_code tool shell out to
ripgrep. Without rg on PATH the
command exits non-zero rather than falling back to a slower scan, so the failure
is visible instead of silently different.
Tree-sitter grammars are optional¶
The symbol and call graph uses tree-sitter grammars that are not bundled.
When a grammar is absent, gdgraph falls back to its deterministic import
resolver — the dependency graph, affected sets, cycles, orphans and repo map all
still work. Grammar-backed symbol extraction is the part that is unavailable.
Platform support¶
| Platform | Status |
|---|---|
| macOS | Full support, including the complete policy sandbox (Seatbelt), domain allowlist, credential masking and TLS termination |
| Linux | Full core support. OS sandbox via bubblewrap (bwrap on PATH): filesystem containment and network on/off. The domain allowlist, credential masking and TLS termination refuse to run rather than degrading to full host network |
| Windows | The core CLI is not exercised in CI on Windows; the OS sandbox is macOS/Linux only |
The macOS-only tier is a fail-closed decision: a domain allowlist that quietly
became "all network" on Linux would be worse than one that says it cannot run.
The refusal is enforced at the spawn point, so KERYX_SANDBOX_ALLOW_UNSANDBOXED
cannot reach it. That variable still does what it was written for — running
uncontained when no launcher is installed, which is a degradation an operator
knowingly accepts — and nothing more.
See the operator guide
for the containment matrix and the
Linux verification runbook
for what has been verified on a real host.
Scheduled agent tasks need the machine on¶
keryx schedule hands a confirmed task to the OS scheduler. keryx itself runs no daemon. The one exception is a digest (keryx schedule add --digest), which has no OS timer and is run by keryx serve, so it does nothing while serve is stopped.
- Missed runs: a machine that is off or asleep misses runs. systemd
(
Persistent=true) and launchd run one catch-up run at the next boot or wake; cron runs none. - Logged out: without linger, a systemd
--usertimer does not run while you are logged out. keryx shows the linger state on the confirmation card and never enables it. - Network:
off,full, orallowlist(flow 301, Linux only — a macOS or non-Linux schedule refusesallowlistwith the reason, never silently falling back toofforfull).allowlistreaches only the domains you name, through a loopback proxy keryx runs outside the sandbox, and governs ONLY the agent's ownshell_execcommands — the model call and every granted tool already run outside the sandbox on your own network, unaffected by this grant. Honest limits ofallowlistitself: - A tool that ignores
HTTP_PROXY/HTTPS_PROXYgets no network at all (the sandbox's netns has no other route out) — this can look likeoffeven though the mode isallowlist. - HTTPS is a blind
CONNECTrelay by default (no TLS termination), so the proxy cannot see the TLS SNI or an in-tunnelHostheader — only the CONNECT authority is checked. A client could in principle address something else once inside an allowed tunnel; termination (already used for credential masking elsewhere) would close this but is not the default here. - DNS resolution happens once, outside the sandbox, and the proxy connects to the exact address it checked — a deliberate defence against DNS rebinding — but this also means a domain that legitimately changes IP between confirmation and a run is resolved fresh each run, not pinned across runs.
- HTTP/2 and WebSockets over the blind relay are not specially handled; most CLI tools (curl, git, npm, pip) negotiate HTTP/1.1 and work as expected.
- Platform: the hardened unattended sandbox is Linux-only, so a macOS schedule
runs in
askmode with granted tools only. - The signing key and an interactive agent: each confirmed schedule is signed
with an HMAC keyed by a per-machine secret. The secret lives outside the project,
in keryx's user-global directory (
schedule-hmac.key, 0600). A committed or forged store therefore never runs, and a scheduled run cannot see the key because its sandbox hides$HOME. But an interactive agent running as the same user, intrustmode with an unrestricted shell, could in principle read the key by spelling its path in a way the text floor does not recognise. The floor turns every command or patch that names the key, the schedule store or the systemd/launchd unit directories into a prompt. That makes reading the key an ask, not an impossibility. - The scheduler-control floor is text analysis. The shell floor parses commands the
way a shell would, so quoting, escapes, wrappers (
env -u,sudo,nohup,bash -c '…') and acdinto a unit directory are all seen. But a same-user shell intrustmode can always spell a command so no text check sees it: a variable, a script file, an interpreter one-liner. The gates behind it: keryx schedule add,resumeandrunrequire an interactive terminal on stdin and stdout,--yesincluded, so a pipe or an MCP/ACP client cannot create, re-enable or fire a schedule;- the terminal requirement alone is not enough, because an agent can make a
terminal:
script -qfc 'keryx shell' /dev/null,tmux,screenorunbuffergive a nested keryx a pseudo-terminal the agent types into, and so reaches the/schedulecard and the/scheduleskeys. Two things cover that path. A keryx started from an agent's shell inheritsKERYX_TOOL_CALL=1, and its/schedule,schedule_create, pause, resume, run-now and delete all refuse. And starting an interactive keryx (keryx,keryx shell) or a terminal driver around one, or typing into a running session (tmux send-keys,screen -X stuff), is in the always-ask family: you are asked every time, and the answer is never remembered; - every stored schedule must carry this machine's signature, and resume checks the signature and every binary pin before it re-enables a timer. The timer runs the keryx recorded (and signed) when you confirmed the card, never the one that pressed resume, and a keryx running from inside the project is refused when the schedule is drafted.
The honest limit stays: an agent running as your user, in trust mode, can unset
KERYX_TOOL_CALL in a way the text floor does not see, or drive a terminal through
a program the floor does not know. The floor makes that an ask, and your answer is
the boundary.
- A script wrapper's own configuration. A pinned #! wrapper (and its interpreter)
cannot change without the schedule being refused, and it runs from an empty directory,
so the project cannot steer it. What the wrapper reads from its own global
configuration (~/.config/..., an account file) is the operator's machine, and outside
what keryx pins.
- Granted binaries between checks. A granted program is hashed once at the start of
a run, and its inode, size and mtime are re-checked before every exec. What is left is
the window between that last stat and the kernel's exec, which is milliseconds.
See schedule.
Remote approvals apply only where tools are registered¶
keryx serve accepts turns over a loopback-bound authenticated HTTP listener. A
turn whose policy decision is ask becomes a durable pending approval that a
person answers over GET /v1/approvals and POST /v1/approvals/{id}, or locally
with keryx approvals (see Answer a remote approval).
Unanswered approvals deny at expiry. But the stock listener registers no tools, so
it raises no approvals today; they become reachable when a tool registry is
injected into the turn. A Telegram or web card for these approvals, session-wide
grants from a remote answer, and approvals for unattended trigger runs are not
built. (A keryx shell session driven from a Telegram topic with /remote-control
has its own Allow/Always/Deny buttons in the topic; that path has been exercised only
against a fake Bot API, not real Telegram. In that path a topic holds at most 500
undelivered lines, and a pure-collision batch after Telegram renumbers its updates,
one whose ids are all ones already seen, cannot be told from a redelivery and is
dropped. A turn from the topic runs in trust by default, so a person with an
allowed Telegram id can run any command the floors do not stop, including network
and outside-project commands, with no run time limit; set permissionMode: "ask"
and a runTimeoutMs in remote/config.json to be stricter. An Always rule is
removed only in the shell or with keryx permissions remove, never from a topic.
The 15-minute approval wait and /remote-policy apply to the Telegram path only; a
running keryx serve hands changed values to shells that register after its own
restart or reload.)
Boundaries that hold:
- The remote policy profile is compared against the local one on every turn, and a weaker remote profile is refused rather than accepted.
- Authentication happens before routing, so an unauthenticated caller cannot distinguish a known path from an unknown one.
Harness gaps¶
Three of them, each stated in full on the harness page:
- No shipped non-interactive path registers a tool. Both production
executors are refusals, so
keryx harness runandkeryx servecomplete a single text turn. Tools run inkeryx shell. - Replay is
validate-log.keryx harness replaychecks that a fixture still describes the run it was built from. It re-executes nothing and contacts nothing, so it is an integrity check, not divergence detection.simulate-recorded-resultsis not implemented. - Branch merge is out of scope.
keryx sessions forkbranches a conversation and records its ancestry; there is no merge back.
Format stability¶
The .metaproject/ layout, artifact formats and CLI surface are still moving
before 1.0. keryx update refreshes managed service files without touching data
artifacts, and the changelog
records what changed in each release, including a standing known-gaps list.