Manifest Reference
Every field in the apiVersion: felix/v1 manifest schema. Source of truth:
packages/harness/src/felix/manifests/schema.py,
plus the compile-time rules in manifests/governance.py and the binders in manifests/builder.py.
Every model is Pydantic v2 with extra="forbid", so an unknown key is a hard parse error — not a
warning. Where a field has a default, the default is what you get if you omit it.
Top-level shape
apiVersion: felix/v1 # default; only this exact value is acceptedkind: Agent # default; only this exact value is acceptedmetadata: { ... } # requiredspec: { ... } # defaults to a minimal react agentValidate one without starting the stack:
uv run felix validate-manifest manifests/quick.yamluv run felix validate-manifest manifests/governed.yaml -e production # applies production rulesmetadata
| Field | Type | Default | Notes |
|---|---|---|---|
name |
string, 1–128 chars, ^[a-zA-Z0-9._-]+$ |
required | The manifest id, the OpenAI model value, the audit manifest_id, and an object-store key segment. No slashes or whitespace, so it cannot escape its key prefix. |
version |
string | "1.0.0" |
Free-form. Distinct from the tenant store’s monotonic version number. |
description |
string | "" |
Surfaced on GET /v1/models and the agent card. |
tags |
string[] | [] |
Free-form labels. |
spec.pattern
spec: pattern: react # defaultBuilt-ins: react, deep, reflect, plan_execute (single-agent) and router, parallel,
groupchat (multi-agent). The registry is open — register_pattern(name, builder) adds more, and
nothing in core enumerates the list. See Patterns.
Multi-agent patterns require sub_agents and reject peers, containers, queues, sandboxes
and browser_tools; a sub-agent’s own leaf manifest declares those instead.
spec.model
spec: model: id: claude-sonnet temperature: 0 max_tokens: 4096 thinking_level: medium fallbacks: [claude-haiku]| Field | Type | Default | Notes |
|---|---|---|---|
id |
string | null | null |
Logical route name. Null falls back to FELIX_DEFAULT_MODEL_ID (claude-sonnet). |
temperature |
float | 0.0 |
|
max_tokens |
int | null | null |
Provider default when unset. |
region |
string | null | null |
|
cache |
bool | false |
Provider prompt caching. |
thinking_budget |
int | null, 128–64000 |
null |
Extended-thinking token budget. |
thinking_level |
off minimal low medium high xhigh max | null |
null |
When set, overrides thinking_budget via the level map. |
fallbacks |
string[] | [] |
Tried in order on a provider error; increments the felix_model_switch counter. |
confidence_escalation |
object | disabled | See below. |
price |
map[string, float] | {} |
USD per 1M tokens, overriding the built-in table for cost attribution. |
Logical ids resolve through DEFAULT_MODEL_ROUTES, overridable with FELIX_MODEL_ROUTES:
claude-opus, claude-sonnet, claude-haiku, claude-fable, gpt-4.1, gpt-4.1-mini,
llama-3-pro, llama-3-fast. The suffixed claude-sonnet-4 and claude-haiku-4 are retained as
legacy aliases onto the current model in the same tier; prefer the unsuffixed ids.
Providers are anthropic, openai, ollama, workers_ai, groq, together, deepseek,
cerebras, fireworks, openrouter, xai, mistral and google, plus any registered through
register_model_provider. Note that limits.max_cost_usd is refused at compile when the resolved
model has no known rates — supply them with spec.model.price. See
Model client.
confidence_escalation retries with a stronger model when the answer looks hedged:
| Field | Type | Default |
|---|---|---|
enabled |
bool | false |
escalate_to |
string | "" |
low_confidence_markers |
string[] | ["i am not sure", "i don't know", "i cannot answer", "unclear", "uncertain", "no information"] |
min_response_chars |
int ≥ 0 | 40 |
A client may override the model mid-session with POST /chat {"model": "..."}, but only against an
allowlist built from spec.model.id, spec.model.fallbacks, FELIX_DEFAULT_MODEL_ID and the route
table. Anything else is 400 model_not_allowlisted:<id>.
spec.system_prompt
spec: system_prompt: inline: | You are a support agent. Cite the knowledge base. soul: false base: ""| Field | Type | Default | Notes |
|---|---|---|---|
inline |
string | "" |
Written in the manifest. |
soul |
bool | false |
Prepend the tenant’s shared “soul” prompt. |
base |
string | "" |
Shared preamble. |
files |
string[] | [] |
Keys loaded after base/inline and appended. |
system_md |
string | null | null |
Key loaded as a full replacement (SYSTEM.md semantics). |
append_system_md |
string | null | null |
Key appended after the composed prompt. |
Parts are joined with "\n\n---\n\n" in the order soul → base → inline, then file-sourced parts.
Empty parts are dropped.
spec.prompts
Named user-message templates, expanded with $1 / $@ / ${1:-default} and invoked by passing
template and template_args on a chat request.
spec: prompts: - name: triage body: "Triage this ticket: $1. Priority hint: ${2:-normal}"| Field | Type | Default | Notes |
|---|---|---|---|
name |
string, 1–64 chars | required | |
body |
string | "" |
|
file |
string | null | null |
Object-store / workspace key; used when body is empty. |
spec.tools
A list of built-in tool names.
spec: tools: [calculator, read_file, write_file, search_files, list_dir]| Tool | Args | Notes |
|---|---|---|
calculator |
{expression} |
Safe expression evaluator. |
list_dir |
{path} |
Max 500 entries. |
read_file |
{path, offset, limit} |
Max 512 KB; flags binary files. |
write_file |
{path, content, append} |
Max 512 KB. |
search_files |
{query, path, regex, max_hits} |
≤ 50 hits; skips files over 256 KB. |
list_skills / activate_skill / deactivate_skill |
— | Bound to the real catalog when spec.skills is set. |
The four workspace tools resolve every path under FELIX_WORKSPACE_ROOT, rejecting absolute paths
and any traversal that escapes the root. With the variable unset they fail with
workspace_root is not configured (set FELIX_WORKSPACE_ROOT).
Patterns contribute their own tools: plan_create / plan_update_step / plan_get (deep), and
remember_procedure when procedural memory is on.
spec.skills
spec: skills: - name: calculator-help| Field | Type | Default |
|---|---|---|
name |
string | required |
version |
string | null | null |
description |
string | null | null |
A skill is a SKILL.md with YAML frontmatter under skills/<name>/. The frontmatter can fold extra
tools, MCP servers and peers into any manifest that lists it; the body is appended to the system
prompt under an ## Active Skills header. Activation is per-tenant and restriction-only — a
tenant overlay can disable declared skills but never enable undeclared ones.
spec.mcp_servers
Each entry becomes one tool per remote tool, namespaced {name}__{tool}.
spec: mcp_servers: - name: notion url: https://mcp.notion.example.com transport: sse auth: secret:NOTION_TOKEN| Field | Type | Default | Notes |
|---|---|---|---|
name |
string | required | Tool namespace. |
url |
string | "" |
Required for http / sse. |
command |
string | "" |
Required for stdio. |
args |
string[] | [] |
|
cwd |
string | "" |
|
env |
map[string,string] | {} |
|
auth |
string | "" |
Literal token or secret:NAME. |
transport |
http | sse | stdio |
sse |
Outbound URLs pass an SSRF guard; internal hostnames are blocked unless allowlisted.
stdio is disabled by default
transport: stdio spawns a subprocess from manifest-supplied argv — arbitrary code execution as the
API process. It is refused unless the operator names the exact command in
FELIX_MCP_STDIO_ALLOWED_COMMANDS (comma-separated; empty, the default, disables stdio entirely).
The check runs on manifest write (400), at compile, and at spawn. The child inherits only
PATH, HOME, LANG, LC_ALL, TZ plus declared keys, and loader variables such as LD_PRELOAD,
PYTHONPATH, NODE_OPTIONS and BASH_ENV are rejected.
spec.peers
Each entry becomes a peer__{name} tool that calls a remote Felix over A2A.
spec: peers: - name: billing url: https://billing.example.com auth: secret:BILLING_TOKENname and url are required; auth accepts a literal or secret:NAME. Hop depth is bounded by
limits.max_peer_hops.
spec.containers
An HTTPS container gateway, one tool per entry.
| Field | Type | Default | Notes |
|---|---|---|---|
name |
string | required | |
description |
string | "" |
|
gateway_url |
string | required | |
image |
string | required | |
container_tool_name |
string | "" |
|
timeout_ms |
int | null | null |
|
auth |
string | "" |
Literal or secret:NAME. |
args_schema |
JSON Schema | null | null |
|
fatal |
bool | false |
When true, a failure aborts the tool batch. |
spec.queues
Asynchronous work handed to a separate consumer over a Valkey/Redis list. The result lands back on the session later, so the run can continue without blocking.
spec: queues: - name: reindex queue_binding: felix-jobs deadline_ms: 600000| Field | Type | Default | Notes |
|---|---|---|---|
name |
string | required | |
description |
string | "" |
|
queue_binding |
string | required | The Redis list key the executor pushes to. |
deadline_ms |
int | null | null |
|
args_schema |
JSON Schema | null | null |
|
fatal |
bool | false |
The consumer writes the result back with POST /internal/sessions/{session_id}/events, which
requires the X-Felix-Consumer-Secret header.
spec.sandboxes
Container-backed code execution (the sandbox extra, Docker).
| Field | Type | Default |
|---|---|---|
name |
string | required |
description |
string | "" |
binding |
string | required |
sandbox_tool_name |
string | "" |
timeout_ms |
int | null | null |
path_prefix |
string | "" |
args_schema |
JSON Schema | null | null |
fatal |
bool | false |
spec.browser_tools
Browser automation through the browser extra (Playwright). Install it explicitly — it is never in
the default image.
| Field | Type | Default |
|---|---|---|
name |
string | required |
description |
string | "" |
binding |
string | required |
op |
content | links | snapshot | screenshot | pdf | json |
content |
timeout_ms |
int | null | null |
path_prefix |
string | "" |
args_schema |
JSON Schema | null | null |
fatal |
bool | false |
spec.client_tools
Tools the browser or CLI client executes, not the server. This is how the web clients run local shell commands and open local files.
spec: client_tools: - name: local_shell description: Run a shell command in the user's local environment. args_schema: type: object properties: command: { type: string } cwd: { type: string } required: [command] additionalProperties: false timeout_seconds: 300| Field | Type | Default | Notes |
|---|---|---|---|
name |
string | required | |
description |
string | "" |
|
args_schema |
JSON Schema | null | null |
|
timeout_seconds |
float, 0 < x ≤ 3600 |
null → 120 s |
|
fatal |
bool | false |
When the model calls one, the harness emits a tool_request frame and blocks until the client
answers with POST /chat/tool_result. A client that never answers stalls the run until the timeout,
which returns [error/timeout] client tool timed out as the tool result. See
the streaming contract.
spec.sub_agents and spec.aggregator_prompt
sub_agents is a list of manifest names, required by the multi-agent patterns and resolved at build
time. aggregator_prompt (string, "") tells parallel and groupchat how to merge their results.
spec.max_turns
spec: max_turns: 4 # default; ceiling 100spec.memory
spec: memory: checkpointer: postgres # default; `none` also built in store: pgvector # default| Field | Type | Default | Notes |
|---|---|---|---|
checkpointer |
postgres | none, or a registered name |
postgres |
Where the session log lives. none keeps no session state, so every turn starts from the messages it was given. Add your own with register_checkpointer. |
store |
pgvector | memory | none (aliases agentcore, vectorize accepted but inert) |
pgvector |
Durable-fact vector store. |
capture |
object | disabled | Extract durable facts from a turn. |
consolidate |
object | disabled | Inert today — do not rely on it. |
capture:
| Field | Type | Default |
|---|---|---|
enabled |
bool | false |
model |
string | "claude-haiku" |
max_facts |
int, 1–20 | 5 |
min_chars |
int ≥ 0 | 80 |
verify |
bool | false |
verify adds a second pass that hands the proposed facts back to model along with the excerpt and
keeps only what the excerpt actually supports. It doubles the extraction calls, which is why it is
off by default — turn it on where stored memory is acted on rather than merely recalled.
The pass can only remove. Its verdict selects from the proposals; it never supplies a memory of
its own, and it cannot alter a kept memory’s kind, topic_key or importance. That matters
because the excerpt it reads is untrusted transcript, and topic_key drives supersession — a
verdict that could set one could delete an unrelated stored fact.
It fails open on anything it cannot read, on the grounds that a broken verifier is not evidence the facts were wrong:
| Verdict | Result |
|---|---|
| A subset of the proposals | Just that subset is stored |
[] — a well-formed empty array |
Nothing is stored. The one unambiguous way to reject everything |
| The call errors | The unverified set is kept |
| Unparseable, or every item unusable | The unverified set is kept |
| Valid items that match no proposal | The unverified set is kept — the verifier rewrote rather than chose |
Content is matched case- and whitespace-insensitively but not punctuation-insensitively, so a verifier that echoes an item back with a trailing full stop counts as having rewritten it.
Extraction runs on model, not on the turn’s model — it is a small, mechanical job on every
completed turn, and billing it to a frontier model roughly doubles the cost of having memory at
all. If that model cannot be built, extraction falls back to the turn’s model rather than dropping
the fact.
Facts land in memory_vectors (pgvector, HNSW) with one of four kinds: fact, event,
instruction, task. The extractor asks the model to classify each fact, and anything outside
that set is silently rewritten to fact rather than rejected — so a prompt that invents a kind
degrades quietly instead of failing. Semantic recall needs the embeddings extra.
Procedural memory is a separate subsystem under spec.procedural_memory, not a kind in this table.
spec.session
spec: session: strategy: compacting reserve_tokens: 16384 keep_recent_tokens: 20000 context_window_tokens: 128000| Field | Type | Default | Notes |
|---|---|---|---|
strategy |
string | "full_replay" |
full_replay, windowed:N, summarizing:N, semantic:N (needs embeddings), compacting. |
compaction_enabled |
bool | true |
|
reserve_tokens |
int ≥ 0 | 16384 |
Headroom kept for the reply. |
keep_recent_tokens |
int ≥ 0 | 20000 |
Never compacted. |
context_window_tokens |
int ≥ 1024 | 128000 |
|
steering_mode |
all | one-at-a-time |
all |
|
follow_up_mode |
all | one-at-a-time |
all |
|
branch_summary |
bool | true |
Summarize an abandoned branch on rewind. |
compact_after_turn |
bool | false |
The session log is append-only and lives outside the context window; the strategy decides what the model actually sees. See Persistence.
spec.auth
spec: auth: inbound: allow_anonymous: false required_scopes: [chat:write] outbound: providers: []| Field | Type | Default | Notes |
|---|---|---|---|
inbound.schemes |
string[] | [] |
Enforced. The scheme the caller authenticated with (api_key, or the JWT verifier scheme) must be listed; jwt is an umbrella for every configured JWT scheme. Mismatch → 403. |
inbound.required_scopes |
string[] | [] |
Enforced. Missing scopes → 403. |
inbound.allow_anonymous |
bool | false |
Enforced. Anonymous callers get 401 unless true. |
outbound.providers |
string[] | [] |
Enforced at compile. The resolved provider for the primary model and every fallback must be listed, or the build fails — not the first model call. |
spec.a2a and spec.observability
spec: observability: trace: true # default| Field | Type | Default | Notes |
|---|---|---|---|
a2a.publish |
bool | true |
Opt-out. false withholds the agent from /.well-known/agent-card.json. The default is true because the field was never read before, so every agent was already advertised; a false default would have 404’d every existing manifest. |
a2a.capabilities[] |
{id, description, input_schema_ref} |
[] |
Merged into the agent card, alongside spec.skills. |
observability.trace |
bool | true |
|
observability.metrics[] |
string[] | [] |
Empty = every counter. Non-empty is an allowlist: unlisted counter names are dropped before the series exists, which also bounds the Prometheus cardinality that tenant-supplied manifest ids and remote MCP tool names would otherwise create. |
spec.execution
spec: execution: mode: durable # default: transient tools: sequential # default| Field | Type | Default | Notes |
|---|---|---|---|
mode |
transient | durable |
transient |
durable runs the invocation as a fiber. |
resume_token_ttl_seconds |
int | null | null → FELIX_HIBERNATE_AFTER_SECONDS (300) |
|
tools |
parallel | sequential |
sequential |
Tool-call execution within a batch. |
A durable POST /chat returns 202 with {status, resume_token, fiber_id, expires_at, thread_id}; poll GET /chat/runs/{resume_token}. Fibers checkpoint in Postgres and are resumed by
the worker’s fiber_scheduler cron. Setting FELIX_DURABILITY=temporal additionally starts a
Temporal workflow on the felix-fibers task queue, falling back to the fiber scheduler if that
fails.
POST /chat/stream ignores execution.mode: durable and always runs inline. Use POST /chat
plus polling when you need durability.
spec.tools_retrieval
Selects a relevant subset of tools per turn instead of sending the whole catalogue.
| Field | Type | Default |
|---|---|---|
enabled |
bool | false |
top_k |
int ≥ 1 | 20 |
model |
string | "bge-base-en-v1.5" |
spec.artifacts
Spills oversized tool output to the object store and shows the model a stub.
| Field | Type | Default |
|---|---|---|
enabled |
bool | false |
threshold_chars |
int ≥ 1 | 8000 |
preview_chars |
int ≥ 1 | 200 |
default_window_chars |
int ≥ 1 | 4000 |
max_window_chars |
int ≥ 1 | 16000 |
Output over the threshold is written to artifacts/{tenant}/{manifest}/{uuid}.txt and replaced with
a preview plus an [artifact:<id> key=<key> …] marker. Keys are tenant-scoped.
spec.reflect
| Field | Type | Default |
|---|---|---|
verifier_model |
string | "" |
threshold |
float 0–1 | 0.7 |
max_iterations |
int 1–5 | 2 |
criteria |
string | "" |
spec.plan_execute
| Field | Type | Default |
|---|---|---|
planner_model |
string | "" |
executor_model |
string | "" |
max_subtasks |
int 1–20 | 8 |
replan_on_failure |
bool | true |
max_replans |
int 0–5 | 2 |
executor_recursion_limit |
int 1–20 | 6 |
planner_few_shots |
int 0–10 | 3 |
spec.procedural_memory
| Field | Type | Default |
|---|---|---|
enabled |
bool | false |
top_k |
int ≥ 1 | 3 |
embedding_model |
string | "bge-base-en-v1.5" |
Adds a remember_procedure tool and recalls stored how-tos as procedural memory rows.
spec.policies
Scope-gated tool access.
spec: policies: - id: notion-writes required_scopes: [notion:write] tools: ["notion__create_page"]| Field | Type | Default |
|---|---|---|
id |
string | required |
description |
string | "" |
required_scopes |
string[] | [] |
tools |
string[] (glob, e.g. github__*) |
[] |
A denial emits a policy_deny audit event with status: denied.
spec.limits
spec: limits: max_tool_calls: 200 max_wall_clock_seconds: 3600| Field | Type | Default | Absolute ceiling |
|---|---|---|---|
max_tool_calls |
int | null | null |
500 |
max_wall_clock_seconds |
float | null | null |
3600 |
max_peer_hops |
int | null | null |
5 |
max_input_tokens |
int | null | null |
1,000,000 |
max_output_tokens |
int | null | null |
100,000 |
max_cost_usd |
float | null | null |
1,000.00 |
precount |
bool | false |
Count input tokens before the call rather than after. |
max_cost_usd is a per-run spend ceiling, accumulated from the model catalog’s prices as tokens are
metered. It fails closed: a model the catalog cannot price is a pricing gap that mis-enforces the
cap, so supply spec.model.price for any model you route to that the built-in table does not
cover.
spec.recursion_limit (int | null, ceiling 50) and spec.max_turns (ceiling 100) are
capped the same way. A breach denies the call like any other governance layer: the tool result
becomes a deny string, felix_tool_calls{status="denied"} increments, and the run records a
policy_deny audit row. There is no distinct limit_exceeded event type.
spec.guardrails
spec: guardrails: providers: [pii] block_on_match: true targets: [input, output, final_response] judges: - name: helpfulness criteria: "Answers the question without inventing facts." threshold: 0.7| Field | Type | Default |
|---|---|---|
providers |
list of pii |
[] |
block_on_match |
bool | false |
targets |
list of input | output | final_response |
["input","output"] |
judges |
JudgeRule[] | [] |
JudgeRule: name (required), criteria (required), threshold (float 0–1, default 0.7),
model (string, ""), target_tools (string[], []), final_response (bool, false).
providers is a closed set, like targets beside it: a typo (PII, pii-redaction) is a
compile error rather than a manifest that applies no wrapper while reporting guardrails enabled.
The pii provider needs the pii extra (Presidio) plus a spaCy English model. Without both —
which is the case on the lean image — it degrades to three regexes (email, US SSN, card-like digit
runs), announced at WARNING with a felix_control_degraded counter, not silently. Guardrail
blocks and below-threshold judge scores deny through the same path as every other layer — a
policy_deny audit row and felix_tool_calls{status="denied"} — not under event types of their
own.
spec.content_screening
Screens tool output for prompt injection before the model sees it.
| Field | Type | Default | Notes |
|---|---|---|---|
enabled |
bool | false |
|
model |
string | "" |
Empty = marker-based only; set an id to add an LLM injection score. |
tools |
string[] | [] |
Empty = all tools. |
on_flag |
quarantine | block |
quarantine |
Applies to output that is flagged and to output that could not be screened. |
Inbound user turns are screened on /chat, /v1 and /a2a as well.
A screener that cannot run has not cleared anything. If the model is unreachable — missing key,
expired credential, 429, provider outage — or returns a score that cannot be parsed, the result is
unavailable, not clean: on_flag: block denies (inbound: 503), quarantine replaces the
content, and a felix_control_unavailable counter is emitted. This matters most on the tool-output
path, which is what screens MCP, A2A, browser, and sandbox content.
spec.command_screening
Pattern rules over shell-style command arguments.
spec: command_screening: enabled: true include_defaults: true rules: - pattern: "rm -rf /" decision: deny reason: "Destructive" - pattern: "git push" decision: require_approval| Field | Type | Default | Notes |
|---|---|---|---|
enabled |
bool | false |
|
include_defaults |
bool | true |
Ship-with floor rules. |
rules |
CommandRule[] | [] |
|
target_tools |
string[] | [] |
|
approval_ttl_seconds |
int, 0 < x ≤ 86400 |
300 |
How long a require_approval rule waits before failing closed. |
CommandRule: pattern (1–256 chars, required), decision (allow | deny | require_approval,
required), reason (string | null).
require_approval creates a real approval request and blocks on it, bounded by
approval_ttl_seconds so a run cannot wait forever on an approver who never comes.
spec.anomaly
Thresholds for the worker’s anomaly scan. Read from the manifest — findings carry the values that produced them.
| Field | Type | Default | Notes |
|---|---|---|---|
enabled |
bool | true |
false disables the scan for this manifest. |
min_volume |
int ≥ 1 | 10 |
Below this call volume the window is not scored. |
baseline_factor |
float ≥ 1 | 3.0 |
Multiple of the baseline that counts as an anomaly. |
min_rate |
float 0–1 | 0.2 |
Still unimplemented — deliberately, since its intended semantics are not recoverable from the code. |
spec.approvals
Human-in-the-loop gates on named tools.
spec: approvals: - id: workspace-write description: Confirm writes to the workspace tools: [write_file] ttl_seconds: 600 one_shot: true bind_principal: true| Field | Type | Default | Notes |
|---|---|---|---|
id |
string | required | |
description |
string | "" |
Shown to the approver. |
tools |
string[] | [] |
|
ttl_seconds |
int > 0 | null | null |
How long the run waits for a decision. |
one_shot |
bool | false |
The grant is consumed once. Enforced with a conditional update, so two concurrent identical calls cannot both spend it. |
bind_principal |
bool | false |
The grant is bound to the approving principal; it will not authorize another user’s identical call. |
allow_unattended |
bool | false |
Permit the call with no human present. |
when_args |
string[] | [] |
Gate only the calls carrying all of these arguments, non-empty. Empty gates every call. |
when_args exists because a tool can be harmless in one shape and a privileged
operation in another. remember is ordinary capture until it carries a topic_key, at
which point it retires whatever else holds that key — the same outcome forget is
gated for. Gating the whole tool would put an approval in front of every memory write:
spec: approvals: - id: memory-retopic description: Confirm replacing the current value of a remembered topic tools: [remember] when_args: [topic_key]A call that does not carry every named argument reaches the tool untouched — no
approval, no approval_required event, no added latency.
A gated call creates a pending row keyed by a SHA-256 signature of its arguments, emits
approval_required, and blocks on the decision. Operators answer with
POST /approvals/{id}/decide (approved / denied, optional note, optional edited_args which
replace the call arguments). It fails closed: no request context, no store, or a store error all
deny.
spec.governance
Opt-in framework mapping. These are compile-time requirements, not a certification.
spec: governance: frameworks: [soc2] forbid_plaintext_secrets: true pin_compile: true retention_days: 365| Field | Type | Default | Notes |
|---|---|---|---|
frameworks |
list of soc2 | eu_ai_act |
[] |
Empty is a no-op, so local manifests stay valid. |
risk_tier |
limited | high |
limited |
EU AI Act deployer hint. |
transparency_notice |
bool | false |
Prepends an “you are talking to an AI agent” notice. |
forbid_plaintext_secrets |
bool | false |
Also forced on when FELIX_ENVIRONMENT=production. |
pin_compile |
bool | false |
Freezes the manifest version for a thread; drift → 409. |
retention_days |
int 1–3650 | null | null |
Setting any framework forces forbid_plaintext_secrets and pin_compile to true, and then:
soc2 requires observability.trace: true, anomaly.enabled: true, at least one of
policies / approvals / limits, at least one of auth.inbound.schemes / required_scopes, and
auth.inbound.allow_anonymous: false outside development.
eu_ai_act requires governance.transparency_notice: true and at least one of
content_screening.enabled or guardrails targeting input. With risk_tier: high it additionally
requires a non-empty approvals list in which no rule sets allow_unattended: true.
Verify in CI with felix validate-manifest <file> -e production. See
deploy/GOVERNANCE.md.
Secret references
Anywhere a credential is accepted (mcp_servers[].auth, peers[].auth, containers[].auth) you may
write a literal, secret:NAME, or {"secret": "NAME"}. Names resolve at compile through
FELIX_SECRETS_BACKEND (env | file | aws | gcp) and are masked out of tool output. An
unresolvable name fails the request with 503. With forbid_plaintext_secrets on, a literal is
a compile error.
Examples
A minimal agent:
apiVersion: felix/v1kind: Agentmetadata: name: quick description: Fast general-purpose assistant.spec: pattern: react model: id: claude-sonnet temperature: 0 system_prompt: inline: You are a concise, accurate assistant. tools: [calculator] max_turns: 8A governed workspace agent with client tools, approvals and durable execution — this is the bundled
cowork manifest the web
clients drive:
apiVersion: felix/v1kind: Agentmetadata: name: cowork version: 1.0.0 description: Felix workspace agent — goals, files, client tools, and approvals. tags: [react, workspace, durable]spec: pattern: react model: temperature: 0 system_prompt: inline: | You are Felix, a workspace agent. Prefer small, reversible changes. Summarize what you did when finished. tools: - calculator - list_dir - read_file - write_file - search_files client_tools: - name: local_shell description: Run a shell command in the user's local environment. args_schema: type: object properties: command: { type: string } cwd: { type: string } required: [command] additionalProperties: false timeout_seconds: 300 skills: - name: calculator-help approvals: - id: workspace-write description: Confirm writes to the workspace tools: [write_file] ttl_seconds: 600 - id: client-shell description: Confirm local shell commands tools: [local_shell] ttl_seconds: 600 limits: max_tool_calls: 200 max_wall_clock_seconds: 3600 max_turns: 40 execution: mode: durable session: strategy: compacting reserve_tokens: 16384 keep_recent_tokens: 20000 context_window_tokens: 128000 memory: checkpointer: postgres store: pgvector auth: inbound: allow_anonymous: true observability: trace: trueFields that parse but do nothing yet
Do not build on these — they validate, but no code path consumes them:
spec.memory.consolidate.* and spec.anomaly.min_rate.
memory.checkpointer used to be on this list. It now selects the session store for
real, and the three names that could never work here — agentcore, do (Durable
Objects, which this stack does not run) and sqlite — are a validation error rather
than a silent alias for postgres.