# Defense-in-Depth Score: AgentField

**Repo:** https://github.com/agent-field/agentfield · **Commit:** `68cadab38de019896b1cbc710a852562ac3f5b8c` (sdk/go/v0.1.142-rc.8) · **Reviewed:** 2026-10-04
**What it is:** Open-source control plane and Python/Go/TypeScript SDKs for running AI agents as REST microservices, with memory, async execution, coding-agent harnesses and an embedded MCP server.
**Category:** Agent Frameworks
**Scored configuration:** `af server` from the install script with no config file or environment overrides (local SQLite storage, port 8080), plus Python SDK Agent and app.ai defaults.
**Agent surface (default):** code execution opt-in · filesystem write yes · network egress yes · external credentials yes · persistent memory yes · untrusted input yes · third party extensions yes · sub agents yes · external communication opt-in

## Score: 2.2 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L1 | L1 | L1 | L0 | 0.20 | C1-SELFESC | **0.20** (alt) | High |
| C2 | Approval gates | L2 | L0 | L0 | L0 | 0.15 | C2-SELFAPPROVE | **0.15** | High |
| C3 | Tool & action scoping | L2 | L1 | L1 | L1 | 0.33 | G1 | **0.33** (alt) | High |
| C4 | Code-execution isolation | L0 | L0 | L0 | L0 | 0.00 | — | **0.00** | High |
| C5 | Untrusted input blast radius | L0 | L0 | L0 | L0 | 0.00 | C5-WORSTCASE | **0.00** | High |
| C6 | Memory, context & configuration integrity | L1 | L0 | L1 | L1 | 0.17 | — | **0.17** | Medium |
| C7 | Third-party extensions | L1 | L1 | L0 | L0 | 0.15 | C7-RCELOAD | **0.15** | High |
| C8 | Secrets & sensitive-data protection | L2 | L1 | L1 | L0 | 0.28 | — | **0.28** | High |
| C9 | Audit & traceability | L2 | L2 | L1 | L3 | 0.50 | — | **0.50** | High |
| C10 | Limits & kill switch | L2 | L2 | L2 | L1 | 0.45 | — | **0.45** | High |


As shipped, access control on the AgentField control plane is not locked down in the default configuration. Agents and coding-agent harnesses run unsandboxed on the host with the operator's full environment, and installed agent packages auto-update from upstream every six hours by default. Strong pieces exist (DID identities, access policies with argument constraints, fail-closed execution records, an encrypted secret store), but most are opt-in and the policy layer does not cover every route.

## Critical gaps
- Agents and harness subprocesses inherit the operator's full environment, so a hijack holds the operator's account. (ASI03, T3; C1) — [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290)
- With authorization enabled, agents self-declare tags that are auto-approved by default, which lets them choose their own access-policy authority. (ASI03, T3; C1) — [control-plane/internal/services/tag_approval_service.go:59](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/tag_approval_service.go#L59)
- Approval resolution is protected only by the shared API key agents hold, so approvals can be self-satisfied. (ASI09, T10; C2) — [control-plane/internal/server/routes_core.go:162](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/routes_core.go#L162)
- Harness CLIs and agent nodes run unsandboxed on the host with the full parent environment. (ASI05, T11; C4) — [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290); [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151)
- A hijacked tool loop can call any reasoner, including unoffered ones, with no human gate while agents hold credentials and egress. (ASI01, LLM01; C5) — [sdk/python/agentfield/tool_calling.py:492](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L492); [sdk/python/agentfield/tool_calling.py:524](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L524); [sdk/python/agentfield/tool_calling.py:532](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L532)
- Installed agent packages auto-update from upstream HEAD every 6 hours by default and run with the operator's full environment. (ASI04, T17; C7) — [control-plane/internal/packages/installer.go:215](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/installer.go#L215); [control-plane/internal/services/packagemaint/service.go:500-526](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/packagemaint/service.go#L500-L526); [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151)

## Criterion details

### C1 Identity & least privilege — 0.20 (high)

Authentication on the control plane is not locked down in its default configuration. When a key is set it is one shared secret that every agent node also receives, and agent nodes and harness subprocesses inherit the operator's full environment (cloud and LLM credentials). A per-agent DID identity with tag-based access policies exists, but it is off by default, tags are self-declared and auto-approved, and enforcement of the policy check does not cover every route.

- **default configuration** (default; raw 0.00 → 0.00)
  - **S L0:** Default deployment has no scoped caller identity, and agents run with the operator's ambient environment. — [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151) (verified)
    - *To reach the next level:* Give each agent and caller a distinct, scoped identity and require authentication by default.
  - **C L0:** Only package-install and secret endpoints get a loopback check; execution, memory, approvals and MCP run with no authorization check. — [control-plane/internal/server/middleware/privileged.go:55](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/middleware/privileged.go#L55) (verified)
    - *To reach the next level:* Route every execution path through an authorization check on the requesting principal.
  - **D L0:** Fresh install has authorization disabled; least privilege needs manual hardening. — [control-plane/internal/config/config.go:486](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L486) (verified)
    - *To reach the next level:* Ship with authentication and authorization on by default.
  - **B L0:** A hijacked agent can invoke every agent on the plane, and every agent node and harness process holds the operator's full environment. — [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151); [control-plane/internal/packages/runner.go:160](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L160); [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290) (verified)
    - *To reach the next level:* Scrub subprocess environments and scope each agent's credentials to its own job.
- **opt-in DID authorization with tag access policies** (alt; raw 0.20, cap C1-SELFESC → 0.20) ← counted
  - **S L1:** Agents get DID identities and tag-scoped policies, but tags are proposed by the agent itself and auto-approved by default, which defeats scoping. — [control-plane/internal/config/config.go:486](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L486); [control-plane/internal/services/tag_approval_service.go:59](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/tag_approval_service.go#L59) (verified)
    - *To reach the next level:* Require operator approval for tags (default_mode manual) so agents cannot pick their own authority.
  - **C L1:** Execute, session and MCP paths are gated, but the permission middleware does not cover every route. — [control-plane/internal/server/middleware/memory_permission.go:58](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/middleware/memory_permission.go#L58) (verified)
    - *To reach the next level:* Make the permission middleware cover every execution route, and gate memory's global scope.
  - **D L1:** Even when enabled, default_deny is false so unmatched calls are allowed, and tag approval defaults to auto. — [control-plane/internal/config/config.go:511](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L511); [control-plane/internal/services/tag_approval_service.go:59](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/tag_approval_service.go#L59) (verified)
    - *To reach the next level:* Default to deny on no matching policy.
  - **B L0:** Agents still inherit the operator's full environment in this mode. — [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151); [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290) (verified)
    - *To reach the next level:* Scrub agent and harness environments.
- **Cap:** C1-SELFESC — With authorization enabled, an agent chooses its own tags at registration and the default tag approval mode auto-approves them, so it sets its own access-policy authority.

### C2 Approval gates — 0.15 (high)

AgentField offers a human-approval primitive: agent code calls app.pause(), the run goes into a waiting state, and an external approval service or operator approves, rejects or requests changes. Nothing is gated by default, though. No reasoner, tool-loop call or harness action needs approval unless the developer writes the pause call. The approval-response endpoint is protected only by the global API key, which agent nodes also hold, so an agent can approve its own request.

- **S L2:** Per-call approval exists with approve/reject/request_changes outcomes, but what the approver sees is whatever the agent code put in its approval request. — [sdk/python/agentfield/agent.py:5104](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/agent.py#L5104); [control-plane/internal/server/routes_core.go:162](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/routes_core.go#L162) (verified)
  - *To reach the next level:* Have the control plane render the exact pending call and arguments to the approver.
- **C L0:** No consequential path crosses a gate by default; the tool-calling loop dispatches agent.call directly. — [sdk/python/agentfield/tool_calling.py:524](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L524); searched `rg -n -i 'requires_approval|require_approval|needs_approval'` in `sdk/python/agentfield/agent.py sdk/python/agentfield/decorators.py sdk/python/agentfield/tool_calling.py` → 0 hits (no per-reasoner approval flag; approval is only an explicit app.pause() call in user code) (verified)
  - *To reach the next level:* Gate consequential reasoners in the control plane rather than only where developers call pause().
- **D L0:** Approval is opt-in per reasoner via an explicit app.pause() call. — [sdk/python/agentfield/agent.py:5104](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/agent.py#L5104); searched `rg -n -i 'requires_approval|require_approval|needs_approval'` in `sdk/python/agentfield/agent.py sdk/python/agentfield/decorators.py sdk/python/agentfield/tool_calling.py` → 0 hits (no per-reasoner approval flag; approval is only an explicit app.pause() call in user code) (verified)
  - *To reach the next level:* Provide on-by-default approval for reasoners tagged as consequential.
- **B L0:** Reasoners are arbitrary user code; a bypassed approval leaves irreversible actions with no checkpoint or undo in the framework. — [sdk/python/agentfield/tool_calling.py:524](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L524) (verified)
  - *To reach the next level:* Add dry-run/rollback hooks and rate limits on consequential calls.
- **Cap:** C2-SELFAPPROVE — POST /executions/:id/approval-response accepts the shared API key that agent nodes receive, so the agent can approve.

### C3 Tool & action scoping — 0.33 (high)

Agent functions are typed Python functions, and the SDK coerces inputs to the declared types or Pydantic models. There are no allowlists on paths, hosts or amounts. The built-in tool loop (tools="discover") hands the model every function registered on the control plane, and it dispatches whatever function name the model returns without checking that name against the offered set. The opt-in access-policy layer can allow or deny functions by glob and put numeric limits on arguments, failing closed when a constrained parameter is missing. But it is off by default, falls through to allow when no policy matches, and its enforcement does not cover every route.

- **default configuration** (default; raw 0.30 → 0.30)
  - **S L2:** Inputs are coerced to declared types and Pydantic models; there is no allowlist or bounds validation. — [sdk/python/agentfield/agent.py:1267-1295](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/agent.py#L1267-L1295) (verified)
    - *To reach the next level:* Validate arguments against allowlists and bounds in a central layer.
  - **C L2:** Type coercion applies to Python SDK reasoners; harness tools and raw reasoner internals are unvalidated. — [sdk/python/agentfield/agent.py:1267-1295](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/agent.py#L1267-L1295); [sdk/python/agentfield/harness/providers/opencode.py:69](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/providers/opencode.py#L69) (verified)
    - *To reach the next level:* Apply a shared validation layer to every registered tool, including harness-driven actions.
  - **D L0:** tools='discover' exposes every reasoner on the plane, and model-chosen names are dispatched without membership checks. — [sdk/python/agentfield/tool_calling.py:352](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L352); [sdk/python/agentfield/tool_calling.py:492](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L492); [sdk/python/agentfield/tool_calling.py:524](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L524) (verified)
    - *To reach the next level:* Default to an explicit per-call tool allowlist and reject tool names not offered.
  - **B L0:** Reasoners and harness agents are general-purpose code with the operator's environment. — [sdk/python/agentfield/harness/providers/opencode.py:69](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/providers/opencode.py#L69); [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290) (verified)
    - *To reach the next level:* Bound reasoner reach (quantities, recipients, scopes) in the framework.
- **opt-in access policies (function allow/deny + argument constraints)** (alt; raw 0.33, cap G1 → 0.33) ← counted
  - **S L2:** Policies allow/deny functions by glob pattern and compare top-level arguments against numeric or equality constraints, failing closed when a constrained parameter is missing; there is no path containment, host/URL allowlisting or nested-argument validation. — [control-plane/internal/services/access_policy_service.go:329-334](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/access_policy_service.go#L329-L334); [control-plane/internal/services/access_policy_service.go:346-381](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/access_policy_service.go#L346-L381) (verified)
    - *To reach the next level:* Add allowlist validation for paths, URLs/hosts and nested arguments, not only numeric and equality comparisons on top-level fields.
  - **C L1:** Enforcement of the policy layer does not cover every route. (verified)
    - *To reach the next level:* Cover every execution route in the policy layer.
  - **D L1:** default_deny is false so unmatched calls pass, and tags that select policies are auto-approved. — [control-plane/internal/config/config.go:511](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L511); [control-plane/internal/services/tag_approval_service.go:59](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/tag_approval_service.go#L59) (verified)
    - *To reach the next level:* Default deny and manual tag approval.
  - **B L1:** Policies bound some quantities, but allowed reasoners are still general code. — [control-plane/internal/services/access_policy_service.go:329-334](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/access_policy_service.go#L329-L334) (verified)
    - *To reach the next level:* Scope allowed reasoners to bounded, reversible operations.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.

### C4 Code-execution isolation — 0.00 (high)

AgentField runs model-driven code in two ways: the harness, which launches coding CLIs such as aforge, Claude Code, Codex and OpenCode, and agent-node packages that the control plane installs and starts. Both run as ordinary host subprocesses under the operator's user, with the full parent environment. The framework adds no sandbox of its own, the OpenCode baseline allows every tool, and permission_mode='auto' maps to Claude Code's bypassPermissions. Any isolation comes from the third-party CLI's own defaults.

- **S L0:** Harness CLIs and agent nodes are same-user host subprocesses; the framework has no isolation primitive. — [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290); [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151); searched `rg -n -i 'seccomp|landlock|bwrap|firejail|gvisor|firecracker|nsjail'` in `sdk/python/agentfield control-plane/internal/packages` → 0 hits (no isolation primitive anywhere in the SDK harness or the package runner) (verified)
  - *To reach the next level:* Run harness and node processes in a container or OS sandbox profile.
- **C L0:** No execution path is sandboxed by the framework. — searched `rg -n -i 'seccomp|landlock|bwrap|firejail|gvisor|firecracker|nsjail'` in `sdk/python/agentfield control-plane/internal/packages` → 0 hits (no isolation primitive anywhere in the SDK harness or the package runner); [sdk/python/agentfield/harness/providers/opencode.py:69](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/providers/opencode.py#L69) (verified)
  - *To reach the next level:* Sandbox every harness and node launch path, failing closed.
- **D L0:** No sandbox exists to be on; OpenCode harness ships a wildcard allow and auto maps to bypassPermissions. — [sdk/python/agentfield/harness/providers/opencode.py:69](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/providers/opencode.py#L69); [sdk/python/agentfield/harness/providers/claude.py:39](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/providers/claude.py#L39) (verified)
  - *To reach the next level:* Make sandboxed execution the default harness mode.
- **B L0:** Subprocesses get the operator's full environment (including AGENTFIELD_API_KEY and LLM keys) and full host filesystem/network. — [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290); [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151); [control-plane/internal/packages/runner.go:160](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L160) (verified)
  - *To reach the next level:* Scrub env, mount only the workspace, and restrict egress.
- **Cap:** none

### C5 Untrusted input blast radius — 0.00 (high)

Tool results go back to the model as ordinary tool messages, with nothing marking where they came from. The model can then call any agent on the control plane, including ones it was not offered. Nothing in the framework separates sessions that read untrusted content (webhook triggers, other agents' output, fetched data) from sessions that hold secrets or can act. If a hijack succeeds, it can both leak data and take actions with no human involved.

- **S L0:** No structural limit on a hijacked tool loop; untrusted results feed straight back into tool selection. — [sdk/python/agentfield/tool_calling.py:532](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L532); searched `rg -n -i 'untrusted|taint|provenance'` in `sdk/python/agentfield/tool_calling.py sdk/python/agentfield/agent_ai.py` → 0 hits (tool results carry no provenance or taint marking) (verified)
  - *To reach the next level:* Disable or gate egress/state-changing tools once untrusted content enters a session.
- **C L0:** No source is distinguished: tool results, other agents' outputs and trigger payloads all enter context the same way. — [sdk/python/agentfield/tool_calling.py:532](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L532); searched `rg -n -i 'untrusted|taint|provenance'` in `sdk/python/agentfield/tool_calling.py sdk/python/agentfield/agent_ai.py` → 0 hits (tool results carry no provenance or taint marking) (verified)
  - *To reach the next level:* Tag every untrusted source and treat it as data.
- **D L0:** There is no control to be on. — searched `rg -n -i 'untrusted|taint|provenance'` in `sdk/python/agentfield/tool_calling.py sdk/python/agentfield/agent_ai.py` → 0 hits (tool results carry no provenance or taint marking) (verified)
  - *To reach the next level:* Ship a default Rule-of-Two policy for the tool loop.
- **B L0:** A hijacked loop can call any reasoner (including unoffered ones) while agents hold the operator's credentials and full egress, unattended. — [sdk/python/agentfield/tool_calling.py:492](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L492); [sdk/python/agentfield/tool_calling.py:524](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L524); [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290) (verified)
  - *To reach the next level:* Remove either unattended egress or unattended state change from sessions reading untrusted content.
- **Cap:** C5-WORSTCASE — Worst case (B L0): a hijacked agent can leak data and take irreversible actions unattended.

### C6 Memory, context & configuration integrity — 0.17 (medium)

The control plane keeps persistent key-value, vector and knowledge memory with workflow, session, actor and global scopes. The scope is chosen from headers the caller supplies, the global scope is open to everyone by design, and the permission middleware is off by default. Writes are recorded as memory events but not validated, and entries carry no provenance. The SDK does not yet inject memory into prompts automatically, so poisoned memory reaches the model only through developer code. Separately, `af server` loads agentfield.yaml from ./config or the current directory when no user-level config exists.

- **S L1:** Memory writes are evented but unvalidated and unprovenanced; server config can come from the working directory. — [control-plane/internal/handlers/memory_access_control.go:21-26](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/memory_access_control.go#L21-L26); [control-plane/cmd/af/main.go:316-317](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/cmd/af/main.go#L316-L317) (verified)
  - *To reach the next level:* Tag memory entries with provenance and load security config only from user scope.
- **C L0:** No memory store or config path is controlled by default. — [control-plane/internal/server/routes_memory.go:17](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/routes_memory.go#L17); [control-plane/internal/handlers/memory_access_control.go:21-26](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/memory_access_control.go#L21-L26) (verified)
  - *To reach the next level:* Enforce scope ownership on all memory stores by default.
- **D L1:** Namespaces exist but are selected by caller headers, and enforcement is an optional middleware; global scope is open. — [control-plane/internal/handlers/memory.go:407](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/memory.go#L407); [control-plane/internal/server/middleware/memory_permission.go:58](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/middleware/memory_permission.go#L58); [control-plane/internal/server/routes_memory.go:17](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/routes_memory.go#L17) (verified)
  - *To reach the next level:* Bind memory scope to an authenticated caller identity by default.
- **B L1:** Poisoned memory persists across sessions and is shared via global scope, but is not auto-injected into prompts. — [sdk/python/agentfield/agent_ai.py:624](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/agent_ai.py#L624) (inferred)
  - *To reach the next level:* Make memory session-scoped or human-reviewed by default, with purge/rollback.
- **Cap:** none

### C7 Third-party extensions — 0.15 (high)

Agent nodes are installed from git repositories the user chooses. They are cloned, their dependencies are installed with pip or npm, and they run as host processes with the operator's full environment and the control-plane API key. Unpinned installs track the remote HEAD, and automatic updates are on by default: every six hours the control plane pulls and reinstalls new upstream code without asking again. The bundled aforge harness binary is the exception. Its version is pinned and its download is checked against a SHA-256 checksum.

- **S L1:** User-chosen git sources, unpinned by default (HEAD tracked); only the first-party aforge binary is pinned and checksummed. — [control-plane/internal/packages/updatecheck/checker.go:141-144](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/updatecheck/checker.go#L141-L144); [control-plane/internal/packages/updatecheck/checker.go:165-167](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/updatecheck/checker.go#L165-L167); [control-plane/internal/aforge/ensure.go:138-142](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/aforge/ensure.go#L138-L142); [control-plane/internal/aforge/ensure.go:23](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/aforge/ensure.go#L23) (verified)
  - *To reach the next level:* Pin installs to a commit and verify integrity, re-approving on change.
- **C L1:** Only the aforge binary is integrity-checked; agent-node packages and their pip/npm deps are not. — [control-plane/internal/aforge/ensure.go:138-142](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/aforge/ensure.go#L138-L142); [control-plane/internal/packages/updatecheck/checker.go:141-144](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/updatecheck/checker.go#L141-L144) (verified)
  - *To reach the next level:* Verify every extension type.
- **D L0:** Auto-update is on by default and reinstalls new upstream code without consent. — [control-plane/internal/packages/installer.go:215](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/installer.go#L215); [control-plane/internal/services/packagemaint/service.go:500-526](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/packagemaint/service.go#L500-L526); [control-plane/internal/services/packagemaint/service.go:25](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/packagemaint/service.go#L25) (verified)
  - *To reach the next level:* Make auto-update opt-in and show the diff/commands before enabling new code.
- **B L0:** Installed nodes run as the operator with full os.Environ plus the control-plane API key. — [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151); [control-plane/internal/packages/runner.go:160](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L160) (verified)
  - *To reach the next level:* Run each node with a scrubbed env and its own scoped credentials, sandboxed.
- **Cap:** C7-RCELOAD — Auto-update defaults on for unpinned packages and reinstalls and runs new upstream code every 6 hours without user consent.

### C8 Secrets & sensitive-data protection — 0.28 (high)

Secrets that agent nodes declare are kept in a local store encrypted with AES-256-GCM, under a key file with 0600 permissions. Execution payloads are left out of structured logs by default. Those are the only protected paths. Agent nodes and harness subprocesses receive the operator's entire environment (LLM keys, cloud credentials, the control-plane API key), nothing is redacted from what is sent to the model, and execution inputs and outputs are stored unencrypted in the local database. Anonymous, content-free telemetry is on by default. Secret handling in the shipped configuration is not fully locked down either.

- **S L2:** Encrypted secret store with a 0600 local keyfile; payload redaction in logs. — [control-plane/internal/packages/secrets.go:22-29](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/secrets.go#L22-L29); [control-plane/internal/config/config.go:43](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L43) (verified)
  - *To reach the next level:* Use an OS keychain/secret manager and redact before model-bound messages.
- **C L1:** Only the log path is protected; subprocess env, model-bound messages and stored execution payloads are not. — [control-plane/internal/config/config.go:43](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L43); [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290); [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151) (verified)
  - *To reach the next level:* Scrub subprocess environments and redact stored/model-bound payloads.
- **D L1:** Anonymous telemetry is on by default (content-free); redaction is on by default but disableable. — [control-plane/internal/config/config.go:60-62](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L60-L62); [control-plane/internal/config/config.go:43](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L43) (verified)
  - *To reach the next level:* Make telemetry opt-in.
- **B L0:** Long-lived operator keys are reachable by every agent node and harness subprocess. — [sdk/python/agentfield/harness/_cli.py:290](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L290); [control-plane/internal/packages/runner.go:151](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/packages/runner.go#L151) (verified)
  - *To reach the next level:* Hand subprocesses only scoped, short-lived credentials.
- **Cap:** none

### C9 Audit & traceability — 0.50 (high)

Every call routed through the control plane, including calls from the SDK tool loop and the MCP endpoint, becomes a structured execution record with target, input, status, timestamps, run ID and parent-execution ID. The record is written before the call is dispatched, and the call is aborted if the write fails. The parent and actor fields come from headers the caller supplies, so attribution is self-asserted. Actions taken inside a reasoner or a harness CLI are not recorded. The records live in the local database and can be deleted through the API. Signed verifiable credentials per execution exist, but the shipped `af` binary leaves them off unless they are configured.

- **S L2:** Structured per-call records with run and parent correlation, but actor/parent come from unauthenticated headers. — [control-plane/internal/handlers/execute_prepare.go:247-248](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/execute_prepare.go#L247-L248); [control-plane/internal/handlers/execute_prepare.go:210](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/execute_prepare.go#L210); [control-plane/internal/handlers/execute_helpers.go:34-36](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/execute_helpers.go#L34-L36) (verified)
  - *To reach the next level:* Attribute records to authenticated principals and approvers.
- **C L2:** All control-plane-routed calls and approval transitions are recorded; harness-internal and in-reasoner actions are not. — [control-plane/internal/handlers/execute_prepare.go:247-248](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/execute_prepare.go#L247-L248); [sdk/python/agentfield/tool_calling.py:524](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L524) (verified)
  - *To reach the next level:* Record harness and in-process actions too.
- **D L1:** Recording is on by default, but an agent-reachable API route deletes workflow records. — [control-plane/internal/server/routes_ui.go:262](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/routes_ui.go#L262) (verified)
  - *To reach the next level:* Make records non-deletable from agent-reachable APIs.
- **B L3:** The execution record is persisted before dispatch and dispatch aborts if the write fails. — [control-plane/internal/handlers/execute_prepare.go:247-248](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/execute_prepare.go#L247-L248) (verified)
  - *To reach the next level:* Extend fail-closed recording to completion records and in-process actions.
- **Cap:** none

### C10 Limits & kill switch — 0.45 (high)

The SDK tool loop stops at 10 turns and 25 tool calls, calls between agents time out after 90 seconds by default, and harness runs are killed with their whole process group after 30 minutes. There is no cost cap. Rate limiting is off by default, and per-agent concurrency is unlimited. Fan-out through app.call starts a fresh budget at every level, with no depth limit beyond an advisory environment variable. Cancelling a workflow tree cancels the agents' asyncio tasks, but the cancel is delivered best-effort, so in-flight work may finish anyway.

- **S L2:** Iteration caps plus per-call and harness timeouts enforced in code; no cost cap. — [sdk/python/agentfield/tool_calling.py:47-48](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L47-L48); [control-plane/internal/config/config.go:325](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L325); [sdk/python/agentfield/harness/_cli.py:335](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/harness/_cli.py#L335); searched `rg -n -i 'max_cost|cost_limit|max_spend|budget_usd'` in `sdk/python/agentfield/agent_ai.py sdk/python/agentfield/tool_calling.py` → 0 hits (no spend cap on app.ai or its tool loop) (verified)
  - *To reach the next level:* Add a token/cost cap and rate limits on side-effecting calls.
- **C L2:** Limits apply per app.ai loop and per call; delegated calls get fresh budgets. — [sdk/python/agentfield/tool_calling.py:47-48](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L47-L48); [sdk/python/agentfield/tool_calling.py:524](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/tool_calling.py#L524) (verified)
  - *To reach the next level:* Make sub-agent calls count against the parent run's budget.
- **D L2:** Sensible defaults that operators can configure, but delegation resets limits. — [control-plane/internal/config/config.go:325](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/config/config.go#L325); [control-plane/internal/server/routes_middleware.go:49](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/routes_middleware.go#L49) (verified)
  - *To reach the next level:* Prevent budget reset by delegation.
- **B L1:** No depth or concurrency ceiling; rate limiting off; cancel is best-effort. — [control-plane/internal/server/server.go:1136](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/server/server.go#L1136); [control-plane/internal/handlers/agent_concurrency.go:45](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/handlers/agent_concurrency.go#L45); [control-plane/internal/skillkit/skill_data/agentfield/SKILL.md:10](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/skillkit/skill_data/agentfield/SKILL.md#L10); [control-plane/internal/services/cancel_dispatcher.go:35](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/control-plane/internal/services/cancel_dispatcher.go#L35); [sdk/python/agentfield/cancel.py:78](https://github.com/agent-field/agentfield/blob/68cadab38de019896b1cbc710a852562ac3f5b8c/sdk/python/agentfield/cancel.py#L78) (verified)
  - *To reach the next level:* Cap depth and concurrency by default and make cancel reliable.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: tool results and other agents' outputs re-enter the model as plain tool messages (sdk/python/agentfield/tool_calling.py:532) · [B] sensitive data/systems: agent nodes and harness subprocesses inherit the operator's full environment (control-plane/internal/packages/runner.go:151, sdk/python/agentfield/harness/_cli.py:290) · [C] state change / egress: model-chosen targets dispatched via agent.call with no gate (sdk/python/agentfield/tool_calling.py:524) · Same default session? Yes

## Highest-impact improvements
1. Require authentication on the control plane by default (generate a credential at install). — C1 D L0→L3, +0.150 before caps (Playbook 4)
2. Make package auto-update opt-in and pin installs to the installed commit, re-approving on change. — C7 D L0→L3, +0.150 before caps (Playbook 3)
3. Scrub os.environ for harness subprocesses and agent nodes, passing only declared variables. — C4 B L0→L2, +0.100 before caps (Playbook 3)
4. Reject tool names in the tool-call loop that were not in the offered tool list. — C3 D L0→L2, +0.100 before caps (Playbook 3)
5. Restrict approval-response to an operator-only credential distinct from the agent API key. — C2 S L2→L3, +0.075 before caps (Playbook 5)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- The aforge harness binary and the third-party coding CLIs (Claude Code, Codex, Gemini, OpenCode) are separate projects whose own sandboxes were not examined; the framework's own isolation was scored.
- Docker/Helm deployments, the Electron desktop app, and the Go/TypeScript SDKs were reviewed only spot-wise; the Python SDK and `af` binary defaults were scored.
- The `agentfield-server` binary (used by the Docker image) defaults DID/VC on, unlike the `af` binary scored here.
- No text aimed at AI reviewers was found in README.md, AGENTS.md or CLAUDE.md.
