Config Files
Two YAML files. eem.yaml configures the agent: CLI flags win, then environment variables (the primary name, mostly EREBINE_EEM_*, then its legacy EEM_* spelling), then the file, then built-in defaults, flat scalars only. workspaces.yaml declares the workspaces the agent serves, and takes effect without a restart.
Overview
This page documents the on-host config layer for the EEM agent. The Environment Variables reference covers the matching environment overrides; chat templates are project-scoped JSON managed through the router API and live under Author a Chat Template.
Keyboard: press Alt+E to jump to the next example block, Alt+Shift+E for the previous one. The chord ignores text inputs.
eem.yaml
Loaded from the path supplied by --config,
default /etc/erebine/eem.yaml. A missing
file is a legitimate "no config" signal, so flag-only
and environment-only deployments work without an
empty stub.
The file is a flat top-level mapping of scalars
(string, integer, boolean). Nested maps and sequences
are rejected at boot with
invalidConfigFile. Keys are lowercased
before lookup.
# required
router_url: https://router.example.com
registration_name: eem-prod-us-east-01
# first enrollment only
join_key: jk_xxxxxxxxxxxxxxxxxxxxxxxx
# optional (defaults shown)
credentials_dir: /var/lib/erebine/credentials
log_level: info
lease_renewal_interval_ms: 10000
shutdown_grace_seconds: 30
max_concurrent_executions: 20
signing_key_path: /var/lib/erebine/agent-signing-key.hex
curve_key_path: /var/lib/erebine/agent-curve-key.hex
queue_db_path: /var/lib/erebine/state/queue.sqlite
project_external_id: proj_optional_external_id
Keys
Every key has a matching environment override
under its primary name (for example
EREBINE_AGENT_JOIN_KEY,
EREBINE_EEM_QUEUE_DB_PATH); the legacy
EEM_* spelling is read only when the
primary name is unset. The required keys,
router_url and
registration_name, have no default;
the agent refuses to start if either is unset across
CLI, environment, and file. join_key is
needed only when the agent has no saved enrollment to
restore.
| Key | Default | Required | Purpose |
|---|---|---|---|
router_url |
unset | Yes | HTTPS endpoint of the router used for enrollment and the data plane. |
join_key |
unset | First enrollment | One-shot enrollment token issued by the router. The agent redeems it only when there is no saved enrollment to restore: on the first start, when agent-enrollment.json (written beside the signing key) or one of the key files is missing or unreadable, or when the router no longer accepts the saved identity. In those cases it refuses to start without one. Once the first enrollment succeeds, the key can be revoked and dropped from the file. |
registration_name |
unset | Yes | Stable display name the router uses to track this agent across restarts. |
credentials_dir |
/var/lib/erebine/credentials |
No | Directory holding one subdirectory per workspace. A workspace's credentials_ref in workspaces.yaml names a file inside its own subdirectory. See Workspace credentials. |
log_level |
info |
No | Log verbosity. One of trace, debug, info, notice, warning, error. |
lease_renewal_interval_ms |
10000 |
No | Cadence (milliseconds) for lease renewal messages to the router. |
shutdown_grace_seconds |
30 |
No | Time the agent waits for in-flight executions to finish on SIGTERM before forcing exit. |
max_concurrent_executions |
20 |
No | Ceiling on simultaneous tool executions handled by the agent. |
signing_key_path |
/var/lib/erebine/agent-signing-key.hex |
No | Persisted Ed25519 signing-key file used to sign messages to the router. |
curve_key_path |
/var/lib/erebine/agent-curve-key.hex |
No | Persisted Curve25519 key-agreement private-key file. The public half is derived on load. |
queue_db_path |
/var/lib/erebine/state/queue.sqlite |
No | SQLite file backing the outbound dispatch queue. |
project_external_id |
unset | No | External project id (ws_<hex>) attached to callbacks for project-scoped routing. |
Reload Semantics
eem.yaml and the environment are read once, at start.
A change to either takes effect on the next
restart. There is no reload signal: the agent
traps SIGTERM and SIGINT, and both stop it.
Two things are read while the agent runs, so a change to them needs no restart:
workspaces.yaml, on every tool call and every lease renewal. See workspaces.yaml for when the router's catalogue catches up.- A credential file under
credentials_dir, by the call that uses it, so a replaced file is used from the next call. A call already running keeps the credential it read.
The paths to both are settings. Moving
workspaces.yaml or the credentials
directory is a restart.
workspaces.yaml
The agent cannot learn which workspaces it serves.
Enrollment hands it an identity, addresses and a
token, and no workspace list, so the operator
declares them. The name of each
declaration must equal the workspace's name
in your project: that string is the only
thing the router matches a declaration against. A
name that matches nothing binds nothing, and it does
it quietly.
Declarations live in their own file because
eem.yaml accepts flat scalars only, and
a scope block is nested. Default location
/etc/erebine/workspaces.yaml, overridden
by EREBINE_EEM_WORKSPACES_FILE.
workspaces:
- name: prod-k8s # must equal the workspace name in your project
type: kubernetes
description: Production cluster
domains: [kubernetes]
scope:
namespaces: [default, kube-system]
rate_per_minute: 10
limits:
max_concurrent_executions: 4
credentials_ref: prod-k8s
Keys
| Key | Required | Purpose |
|---|---|---|
name |
Yes | The workspace's name in your project. The only axis a declaration binds on. |
type |
Yes | One of the six types below. It selects the scope shape. |
description |
No | Free text carried to the catalogue so a caller can tell two workspaces apart. |
domains |
No | The tool domains this workspace admits. A tool whose domain is outside the list is neither advertised for the workspace nor run in it. Absent or empty, it narrows nothing, and the agent-wide EREBINE_EEM_DOMAINS gate still applies on top. |
scope |
No | The type-specific block. See the scope reference below. |
limits.max_concurrent_executions |
No | Per-workspace ceiling on simultaneous tool calls, under the agent-wide max_concurrent_executions. A call that arrives while the workspace is at its ceiling is refused, not queued. Absent, only the agent-wide ceiling applies. |
credentials_ref |
No | One file name, with no /, in credentials_dir/<name>/. A kubernetes workspace binds it as its kubeconfig and an openstack workspace as its clouds.yaml. See Workspace credentials. The credential material never leaves the host. |
Workspace credentials
A workspace that sets credentials_ref
runs its credentialed tools on that one file and on
nothing else on the host. The file lives at
credentials_dir/<name>/<credentials_ref>,
owned by the agent's account, mode 0600
or 0400, and not a symlink.
| Workspace type | The file is | The tools get |
|---|---|---|
kubernetes |
A kubeconfig. Its current context picks the cluster, namespace default and user. | KUBECONFIG naming the file. kubectl and helm read it and no other kubeconfig; there is no in-cluster fallback. |
openstack |
A clouds.yaml holding exactly one cloud under clouds. That cloud's auth block picks the project. |
OS_CLIENT_CONFIG_FILE naming the file and OS_CLOUD naming its one cloud. No other clouds.yaml or secure.yaml is read. |
The client runs with only those variables, a fixed
PATH, and an empty private home directory
that is also its working directory, so nothing in the
agent account's home or the agent's working directory
is read.
- A file that is missing, empty, a symlink, or
readable by group or other refuses the call with
credential_unavailablebefore anything runs. So does a clouds.yaml that does not hold exactly one cloud. - Any other credentialed tool in a workspace that
sets
credentials_ref-- a cloud CLI, git, a database -- is refused withcredential_binding_unsupportedrather than run on the agent's own identity. So is every credentialed tool in a workspace of another type that sets one. - A workspace without
credentials_refruns its tools on the agent's own identity: whatever its account and working directory reach. Two such workspaces on one agent share it. Run one agent per credential boundary, or bind each workspace.
Generating the file
Write it with erectl rather than by
hand, so the names come from your project and cannot
be mistyped:
erectl workspaces declare --workspace prod-k8s --workspace erebine-repo
erectl workspaces declare --workspace prod-k8s --stdout # preview
erectl workspaces declare --remove old-cluster
A refresh keeps the scope already in the file.
Narrowing is local work and the project's copy is
the wider one, so taking it is opt-in through
--reseed-scope. Host-specific values --
a code workspace's repo_roots -- are
set on the host that runs the work, and a refresh
never removes them.
Writing the file rewrites the document through a YAML parser. Comments, blank lines and key order do not survive. Keep notes somewhere the command does not write.
Reload and the default workspace
An edit takes effect without a restart, on two paths with different guarantees. Enforcement is immediate: the agent rebuilds its manifest from this file on every tool call, so narrowing a scope or removing a workspace binds the very next call. The catalogue converges within about a lease interval (10 seconds by default): the router is told the manifest changed and asks for the new one. The catalogue is what the router offers callers and never what the agent enforces, so a stale catalogue can name a workspace the agent refuses, and can never grant one.
Without this file the agent declares a single
workspace named local with an empty
scope. That workspace can only match a workspace
literally named local, and it delivers
no scope to any tool, so a code workspace will not
work until you declare one here. A file that exists
but declares nothing is not the same thing: it
declares zero workspaces and drops the
local fallback with them.
A file that exists but cannot be understood stops the agent rather than falling back. Silently ignoring a scope declaration is how a workspace ends up running unscoped.
Workspace Scope
Six types, each with its own scope block. Every key listed below is enforced: a tool reads it and refuses work outside it, before it runs anything.
The If absent column is the one to read first, because it says what a key you have not written does. Nothing changes means the key narrows something the tools already do, so leaving it out leaves them as they are. Tools refuse means the key grants something the tools cannot do without it -- a path, a pod, a command, a directory -- so leaving it out turns those tools off until you declare it. Silence never grants anything.
Any key not listed here is carried through unchanged. The scope block is free-form on purpose, so a workspace type this page does not describe still works.
Code
type: code. A checkout on the agent host that the code tools may read, edit and build. Runs on: Linux and macOS.
| Key | Value | Default | If absent | Purpose |
|---|---|---|---|---|
repo_roots (required) | list of absolute paths | - | Tools refuse | Absolute paths the code tools may touch; every other path is refused. |
denied_paths | list of patterns | - | Nothing changes | Patterns carved back out of the roots, matched case insensitively against the basename when the pattern has no slash. |
max_file_bytes | whole number of bytes | 1048576 | Nothing changes | Largest file the read tools will open, in bytes. |
max_write_bytes | whole number of bytes | 262144 | Nothing changes | Largest write the edit tools will make, in bytes. |
allowed_build_commands | name to command line | - | Tools refuse | Named command lines the build tool may run; a caller names one and cannot supply its own. |
allowed_test_commands | name to command line | - | Tools refuse | Named command lines the test tool may run. |
allowed_format_commands | name to command line | - | Tools refuse | Named command lines the format tool may run. |
allowed_lint_commands | name to command line | - | Tools refuse | Named command lines the lint tool may run. |
git_push_allowed | true or false | false | Tools refuse | Whether the git tool may push at all; anything but true refuses. |
protected_branches | list of patterns | - | Nothing changes | Branch patterns a push may never target, matched case sensitively. |
allowed_remotes | list of text | origin | Nothing changes | Remote names a push may target. |
rate_per_minute | whole number of requests a minute | - | Nothing changes | Requests a minute this workspace admits, counted by the router before a call reaches the agent. Absent, the deployment-wide setting applies, and then the per-domain default. |
Kubernetes
type: kubernetes. Clusters the agent drives through the kubectl, helm and kustomize command line. Runs on: Linux and macOS.
| Key | Value | Default | If absent | Purpose |
|---|---|---|---|---|
namespaces | list of text | - | Nothing changes | Namespaces a call may target, matched case insensitively against the namespace the tool is about to pass. Most tools put default on the command line when the call omits the argument, so an omitted namespace is checked as default; declare it, or pass a namespace. k8s_apply_manifest, k8s_apply_from_file and helm_history pass no namespace at all when the call omits one, which would leave the target to the current context of the kubeconfig on the agent host, and helm_list and k8s_top_cluster read every namespace: while this key is set those are refused, because neither answer is inside a list. Absent, it narrows nothing. |
enabled_resource_kinds | list of text | - | Nothing changes | Resource kinds a call may touch. Each tool's kind is known without asking the cluster: a fixed-kind tool carries it, k8s_delete_resource takes it as kind, and k8s_wait and the k8s_rollout_* tools take it as the part of resource before the slash. Spellings are compared after lowercasing and dropping the API group, so Pod, pod, pods, po and pods. are one kind, and deploy and Deployment.apps are deployments. A kind outside the built-in table is matched literally, so a custom resource works when both sides spell it the same way; relating two spellings of one would mean asking the cluster. Absent, it narrows nothing. |
kube_manifest_paths | list of absolute paths | - | Tools refuse | Directories on the agent host that hold appliable manifests. k8s_apply_from_file refuses any path outside them, and refuses every path while this key is absent. Each entry must be absolute, and a path that leaves a directory through a symbolic link is refused rather than followed. |
kustomize_paths | list of absolute paths | - | Tools refuse | Directories that hold kustomize overlays. kustomize_build refuses any directory outside them, and refuses every directory while this key is absent. An omitted directory means the first entry, and a relative one is resolved against it. |
exec_allowed_pods | list of patterns | - | Tools refuse | Pod name patterns k8s_exec may open a shell in, matched case insensitively against the name the call passes. While this key is absent no pod is permitted. |
exec_allowed_commands | list of text | - | Tools refuse | Binaries k8s_exec may run, matched against the first element of the command argv exactly as passed, so /bin/sh and sh are separate entries. While this key is absent no command is permitted. Nothing after the first element is checked, and it does not need to be: kubectl exec runs the argv directly with no shell between, so a separator is an argument rather than a second command. A shell listed here allows everything that shell can run. |
scale_replica_cap | whole number of replicas | - | Nothing changes | Largest replica count k8s_scale_deployment may ask for. Absent, the count is unbounded. |
rate_per_minute | whole number of requests a minute | - | Nothing changes | Requests a minute this workspace admits, counted by the router before a call reaches the agent. Absent, the deployment-wide setting applies, and then the per-domain default. |
user_rate_per_minute | whole number of requests a minute, or null | - | Nothing changes | Per-caller share of the workspace ceiling, counted by the router; null means no separate per-caller limit. Absent, the deployment-wide setting applies. |
Four of these keys refuse when they are
absent. A kubernetes workspace that declares
no exec_allowed_pods and no
exec_allowed_commands cannot run
k8s_exec at all; one that declares no
kube_manifest_paths cannot run
k8s_apply_from_file; one that declares no
kustomize_paths cannot run
kustomize_build. Each of those tools takes
a pod, a command or a filesystem path straight from the
caller, so an undeclared key is a grant rather than a
narrowing, and a grant nobody wrote grants nothing.
Declare the four keys to restore the tools.
namespaces,
enabled_resource_kinds and
scale_replica_cap stay open when absent,
because each narrows something the tools are meant to
do rather than granting it.
Declaring namespaces changes what
an omitted namespace means. It has to: a list
that still admitted "whatever the kubeconfig's current
context says" or "every namespace" would not be a
narrowing. Most calls are unaffected, because most
tools put default on the command line
themselves and that is simply checked. The ones that
change are k8s_apply_manifest,
k8s_apply_from_file and
helm_history, which pass no namespace when
the call omits one, and helm_list and
k8s_top_cluster, which read across every
namespace. Pass a namespace from the list; for
k8s_top_cluster, which takes none, use
k8s_top_pods.
k8s_apply_manifest is the one tool whose
target is in the body rather than in an argument, since
a document carries its own
metadata.namespace. While either narrowing
key is set the body is parsed and every document is
checked: multi-document YAML, JSON and a
List wrapper are all read, a document that
names a namespaced kind with no namespace in it and
none on the call is refused, and a body that is not
valid YAML or JSON is refused rather than passed
through. Kinds Kubernetes puts outside any namespace,
such as Namespace and
ClusterRole, are not bounded by
namespaces, because nothing can bound
them. With neither key set nothing is parsed and the
tool behaves exactly as before.
No key bounds which cluster a call reaches.
No tool in the bundle takes a context or a cluster
argument and none passes --context, so the
cluster is decided entirely by the current context of
the kubeconfig the tool runs on. With
credentials_ref set, that is the
workspace's own kubeconfig and no other; see
Workspace credentials.
Docker / Podman / Container
type: container. Containers on the agent host, through whichever of docker, podman or container is installed. Runs on: Linux and macOS.
| Key | Value | Default | If absent | Purpose |
|---|---|---|---|---|
runtime | one of auto, docker, podman, container | auto | Nothing changes | Which container runtime to drive; auto takes the first one found on the host, trying container, then docker, then podman on macOS, and podman then docker on Linux. Apple's container is the Mac's native runtime, so auto prefers it there; it is considered on macOS only, and a named runtime that is not installed is refused rather than substituted. |
allowed_registries | list of text | - | Nothing changes | Registries container_image_pull may pull from, matched against the registry the reference itself names. While this key is set a reference that names none is refused, because what a bare name resolves to is the runtime's own configuration and not part of the reference: write docker.io/library/nginx rather than nginx. Absent, it narrows nothing. |
allowed_containers | list of patterns | - | Nothing changes | Containers container_start, container_stop, container_restart and container_rm may touch. Matched against the ID or name exactly as the caller passes it; an ID is never resolved to a name, so list the form your callers use. Absent, it narrows nothing. |
allowed_images | list of patterns | - | Nothing changes | Images container_image_pull and container_image_rm may touch, matched against the reference exactly as passed, tag and digest included. Absent, it narrows nothing. |
rate_per_minute | whole number of requests a minute | - | Nothing changes | Requests a minute this workspace admits, counted by the router before a call reaches the agent. Absent, the deployment-wide setting applies, and then the per-domain default. |
user_rate_per_minute | whole number of requests a minute, or null | - | Nothing changes | Per-caller share of the workspace ceiling, counted by the router; null means no separate per-caller limit. Absent, the deployment-wide setting applies. |
Linux host
type: linux. A Linux host the agent inspects and manages through systemd, the journal and the standard utilities. Runs on: Linux only.
| Key | Value | Default | If absent | Purpose |
|---|---|---|---|---|
shell_runas_user | list of text | - | Tools refuse | Accounts a shell command may be run as; without this key every run-as request is refused. Account names are compared exactly, because a Unix account name is case-sensitive. |
allowed_paths | list of absolute paths | - | Tools refuse | Absolute directories the twelve path-taking host tools may work under; every path argument must resolve inside one, a relative argument is refused rather than guessed at, and a symlink below a root is refused rather than followed. Without this key none of those tools runs. |
denied_paths | list of patterns | - | Nothing changes | Patterns carved back out of the allowed paths; a denial beats an allowance. Matched against the whole absolute path, and a pattern with no slash is also matched against the file name alone, so *.pem means pem files anywhere under the roots and /etc/* means that directory. Absent, it narrows nothing. |
allowed_commands | list of text | - | Nothing changes | Binary names, such as systemctl or df, that the host tools may run; each tool is checked against the binary it is built to invoke, so the entries are names and not paths. It does not inspect what a shell command line contains, so it places no limit on shell_exec_irreversible. Absent, it narrows nothing. |
rate_per_minute | whole number of requests a minute | - | Nothing changes | Requests a minute this workspace admits, counted by the router before a call reaches the agent. Absent, the deployment-wide setting applies, and then the per-domain default. |
user_rate_per_minute | whole number of requests a minute, or null | - | Nothing changes | Per-caller share of the workspace ceiling, counted by the router; null means no separate per-caller limit. Absent, the deployment-wide setting applies. |
A linux workspace with no
allowed_paths loses twelve tools.
linux_file_read,
linux_file_stat,
linux_file_hash,
linux_directory_list,
linux_directory_size,
linux_find_files,
linux_chmod, linux_chown,
linux_cp, linux_mv,
linux_tar_create and
linux_tar_extract take an absolute host
path from the caller, and a key that bounds a
caller-supplied path grants nothing when it is absent.
Declare the directories the workspace is for, for
example
allowed_paths: ["/srv/app", "/var/log"].
Every other linux tool is unaffected: nothing else in
the bundle takes a path.
OpenStack
type: openstack. An OpenStack cloud the agent drives through the openstack command line. Runs on: Linux and macOS.
| Key | Value | Default | If absent | Purpose |
|---|---|---|---|---|
enabled_services | list of text | - | Nothing changes | Services these tools may reach: nova, cinder, neutron, glance or heat. No tool takes a service argument, so each one is checked against the service its own command addresses and the refusal happens before a command line is built. The service is the one the call is routed to rather than the resource in the tool's name: openstack_volume_attach runs server add volume, so it needs nova. Absent, it narrows nothing. |
heat_template_paths | list of absolute paths | - | Tools refuse | Absolute directories openstack_stack_create may read its template and environment files from; both must resolve inside one, a relative argument is refused rather than guessed at, and a symlink below a root is refused rather than followed. Without this key no stack is created. |
server_create_flavor_allowlist | list of text | - | Nothing changes | Flavors openstack_server_create and openstack_server_resize may name, compared exactly. Absent, it narrows nothing. |
server_create_image_allowlist | list of text | - | Nothing changes | Images openstack_server_create may boot, compared exactly. Absent, it narrows nothing. |
server_create_network_allowlist | list of text | - | Nothing changes | Networks openstack_server_create may attach to, and pools openstack_floatingip_create may allocate an address from, compared exactly. Absent, it narrows nothing. |
image_source_paths | list of absolute paths | - | Tools refuse | Absolute directories openstack_image_upload may read its file from, under the same containment and symlink rules. Without this key no image is uploaded. |
rate_per_minute | whole number of requests a minute | - | Nothing changes | Requests a minute this workspace admits, counted by the router before a call reaches the agent. Absent, the deployment-wide setting applies, and then the per-domain default. |
user_rate_per_minute | whole number of requests a minute, or null | - | Nothing changes | Per-caller share of the workspace ceiling, counted by the router; null means no separate per-caller limit. Absent, the deployment-wide setting applies. |
An openstack workspace with no
heat_template_paths creates no stacks, and
one with no image_source_paths uploads no
images. template,
environment and file are
absolute host paths straight from the caller, and the
client reads every byte behind them, so each key is a
grant rather than a narrowing, and a grant nobody wrote
grants nothing. Declare the
directories the workspace owns, for example
heat_template_paths: ["/opt/heat-templates"],
to restore those two tools. The other 33 are
unaffected: nothing else in the bundle takes a path.
No key names which project a call acts
in. The project comes from the one cloud in
the clouds.yaml the workspace binds through
credentials_ref, not from any tool
argument; see
Workspace credentials. The
project argument on
openstack_server_list filters a listing
and cannot widen one. Nothing bounds
key_name either, so a caller may inject
any keypair the project holds.
Infrastructure as code
type: iac. Terraform working directories on the agent host. Runs on: Linux and macOS.
| Key | Value | Default | If absent | Purpose |
|---|---|---|---|---|
workspace_dirs | list of absolute paths | - | Tools refuse | Directories the terraform tools may run in. Every call must name a directory at or below one of them, and while this key is absent no directory is permitted, so none of the four tools runs. Each entry must be absolute. Containment compares whole components, so /srv/terraform-scratch is outside /srv/terraform, and a directory that is or sits below a symbolic link is refused rather than followed. |
rate_per_minute | whole number of requests a minute | - | Nothing changes | Requests a minute this workspace admits, counted by the router before a call reaches the agent. Absent, the deployment-wide setting applies, and then the per-domain default. |
user_rate_per_minute | whole number of requests a minute, or null | - | Nothing changes | Per-caller share of the workspace ceiling, counted by the router; null means no separate per-caller limit. Absent, the deployment-wide setting applies. |
An iac workspace that declares no
workspace_dirs runs no terraform at
all. terraform_plan,
terraform_apply,
terraform_output and
terraform_state_list all refuse until the
key is declared. The directory argument is
a filesystem path straight from the caller, and
terraform in a directory is terraform against whatever
real infrastructure that directory's state binds, so the
key is a grant rather than a narrowing, and a grant
nobody wrote grants nothing.
Declare the absolute paths the workspace owns, for
example
workspace_dirs: ["/srv/terraform/prod"],
to restore the tools.
All four tools now require a
directory, and it must be
absolute. It used to be optional, and an
omitted one ran terraform in whichever directory the
agent process was started in. Nothing took that
default's place: a workspace may declare several
directories, and applying, or reading the state of, the
wrong one is somebody else's infrastructure rather than
a wrong answer. A relative path is refused on the same
reasoning, since the only base it could have had is a
guess.
What the key bounds is the directory terraform runs in,
and that is the whole promise. It does not bound what
terraform then reads: a var_file argument
is passed through untouched, and which providers,
modules and remote state a configuration pulls in is
terraform's own resolution. Three keys that read as
wider promises were removed rather than left in place.
allowed_providers would need the
configuration parsed and terraform's provider
resolution reproduced.
plan_before_apply asserts that an earlier
plan saw the same files, which one call cannot know.
targeted_destroy_only named a destroy tool
that does not exist.
terraform_apply always runs
apply -auto-approve -input=false
-no-color. No argument and no scope key
adds a plan gate in front of it; what gates it is the
approval policy for its destructive risk class. The
auto_approve argument it used to advertise
was ignored and has been removed, for the same reason
plan_before_apply was: a brake that is not
connected is worse than no brake.
Capability Manifest
The capability manifest is not an operator-edited
file. The agent constructs it in-process from its
registered tool descriptors and republishes on
enrollment and every lease renewal. The router
persists it server-side in
agents.capability_manifest_json.
The manifest is the allowlist. The router rejects
any tool call not present in the published
manifest. Editing a file named
manifest.json on the EEM host has
no effect on the published manifest.
Wire shape (internal reference)
The fields below are the on-wire JSON shape. Operators do not author these; the agent emits them.
| Field | Purpose |
|---|---|
schema_version | Manifest schema revision. |
agent_id | Stable agent identifier assigned at enrollment. |
display_name | Operator-visible name; mirrors registration_name. |
description | Short free-text description supplied by the tool bundle. |
shared_limits | Object with max_concurrent_executions_total, max_execution_timeout_ms, max_result_size_bytes. |
workspaces[] | Workspaces the agent is bound to. |
tools[] | Flat array; each tool carries name, domain, description, parameters_schema, risk (level, requires_approval, reversible, optional reversal_tool), credential_type, timeout_ms, idempotent, and the workspaces it applies to. |
On-Disk Layout
Default layout on a production EEM host:
/etc/erebine/
eem.yaml
workspaces.yaml
/var/lib/erebine/
agent-signing-key.hex
agent-curve-key.hex
agent-enrollment.json
credentials/
<workspace>/
<credentials_ref>
state/
queue.sqlite
queue.sqlite-wal
queue.sqlite-shm
/var/lib/erebine/must live on a persistent volume. The dispatch queue is not useful if the filesystem is ephemeral.- The Curve25519 public half is derived from the private key on load; there is no separate
.publicfile. - Logs go to stderr by default. Routing to
journald,rsyslog, or a file is the operator's responsibility. See Deploy EEM on a Jumphost for a systemd unit and a compose override.
Further Reading
- EEM Environment Variables - the environment override layer for every key above.
- Deploy EEM on a Jumphost - a systemd unit and a compose override.
- Author a Chat Template - project- and workspace-scoped JSON managed via the router API.