@aporthq/aport-agent-guardrails 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  # agent-guardrails openclaw ap_xxx # openclaw with agent_id (pass-through)
7
7
  # agent-guardrails --framework=langchain
8
8
  # agent-guardrails -f crewai
9
- # Supported: openclaw | langchain | crewai | cursor | claude-code (n8n coming soon)
9
+ # Supported: openclaw | langchain | crewai | cursor | claude-code | deerflow | n8n
10
10
  #
11
11
  # Backward compatibility: All arguments after the framework are passed through to
12
12
  # the framework script. For OpenClaw, bin/openclaw receives them (e.g. agent_id).
@@ -60,7 +60,7 @@ if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
60
60
  # Check if first arg looks like a framework name (lowercase alphanumeric + hyphen)
61
61
  if [[ "$first_arg" =~ ^[a-z0-9-]+$ ]]; then
62
62
  # Valid framework names
63
- valid_frameworks=(openclaw langchain crewai cursor claude-code n8n)
63
+ valid_frameworks=(openclaw langchain crewai cursor claude-code n8n deerflow)
64
64
  for valid_fw in "${valid_frameworks[@]}"; do
65
65
  if [[ "$first_arg" == "$valid_fw" ]]; then
66
66
  framework="$first_arg"
@@ -90,14 +90,14 @@ if [[ -z "$framework" ]]; then
90
90
  noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
91
91
  if [[ -n "$noninteractive" ]]; then
92
92
  echo "[aport] ERROR: Multiple frameworks detected: $detected_list" >&2
93
- echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
93
+ echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code | deerflow | n8n" >&2
94
94
  exit 1
95
95
  fi
96
96
  echo ""
97
97
  echo " APort Agent Guardrails — Framework setup"
98
98
  echo " ─────────────────────────────────────────"
99
99
  echo " Multiple frameworks detected: $detected_list"
100
- echo " Choose one: openclaw | langchain | crewai | cursor | claude-code"
100
+ echo " Choose one: openclaw | langchain | crewai | cursor | claude-code | deerflow | n8n"
101
101
  echo " Example: npx @aporthq/agent-guardrails --framework=openclaw"
102
102
  echo ""
103
103
  read -p " Framework [${framework:-openclaw}]: " choice
@@ -113,14 +113,14 @@ if [[ -z "$framework" ]]; then
113
113
  noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
114
114
  if [[ -n "$noninteractive" ]]; then
115
115
  echo "[aport] ERROR: No framework detected in current directory." >&2
116
- echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
116
+ echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code | deerflow | n8n" >&2
117
117
  exit 1
118
118
  fi
119
119
  echo ""
120
120
  echo " APort Agent Guardrails — Framework setup"
121
121
  echo " ─────────────────────────────────────────"
122
122
  echo " No framework detected in current directory."
123
- echo " Choose: openclaw | langchain | crewai | cursor | claude-code"
123
+ echo " Choose: openclaw | langchain | crewai | cursor | claude-code | deerflow | n8n"
124
124
  echo " Example: npx @aporthq/agent-guardrails openclaw"
125
125
  echo " npx @aporthq/agent-guardrails --framework=langchain"
126
126
  echo ""
@@ -143,7 +143,7 @@ if [[ "$framework" == "n8n" ]]; then
143
143
  echo "[aport] Note: n8n custom node is not yet available. This setup only runs the passport wizard and writes config." >&2
144
144
  fi
145
145
 
146
- # Run framework script if it exists and is executable (safe with empty REST)
146
+ # Run framework-specific script if it exists (cursor, claude-code have custom logic)
147
147
  if [[ -x "$script" ]]; then
148
148
  exec "$script" ${REST+"${REST[@]}"}
149
149
  fi
@@ -153,6 +153,14 @@ if [[ "$framework" == "openclaw" ]]; then
153
153
  exec "$SCRIPT_DIR/openclaw" ${REST+"${REST[@]}"}
154
154
  fi
155
155
 
156
+ # Generic handler: works for any framework with a next-steps.d/<framework>.txt data file
157
+ # (crewai, langchain, deerflow, n8n, and any future framework)
158
+ generic_script="$FRAMEWORKS_DIR/generic.sh"
159
+ if [[ -x "$generic_script" ]]; then
160
+ export APORT_FRAMEWORK="$framework"
161
+ exec "$generic_script" ${REST+"${REST[@]}"}
162
+ fi
163
+
156
164
  echo "[aport] ERROR: Unknown or unsupported framework: $framework" >&2
157
- echo " Supported: openclaw, langchain, crewai, cursor, claude-code (n8n coming soon)" >&2
165
+ echo " Supported: openclaw, langchain, crewai, cursor, claude-code, deerflow, n8n" >&2
158
166
  exit 1
@@ -87,28 +87,30 @@ elif [ "$HOOK_EVENT" = "beforeMCPExecution" ] || { [ -n "$TOOL_NAME" ] && echo "
87
87
 
88
88
  elif [ -n "$TOOL_NAME" ]; then
89
89
  # preToolUse: Cursor tool names — Shell, Read, Write, Grep, Delete, Task, MCP:<name>
90
- case "$TOOL_NAME" in
91
- Shell)
90
+ # Normalize: trim whitespace, lowercase (patterns must match TOOL_NORM)
91
+ TOOL_NORM="$(echo "$TOOL_NAME" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')"
92
+ case "$TOOL_NORM" in
93
+ shell)
92
94
  GUARDRAIL_TOOL="bash"
93
95
  CONTEXT_JSON="$(safe_jq "$INPUT" '{command: (.tool_input.command // "")}')"
94
96
  ;;
95
- Read | Grep)
96
- # Read-family: allow without calling evaluator
97
+ read | grep | glob | semanticsearch)
98
+ # Read-family: allow without calling evaluator (matches Claude Code behavior)
97
99
  exit 0
98
100
  ;;
99
- Write)
101
+ write | strreplace | editnotebook)
100
102
  GUARDRAIL_TOOL="write"
101
103
  CONTEXT_JSON="$(safe_jq "$INPUT" '{file_path: (.tool_input.file_path // .tool_input.path // "")}')"
102
104
  ;;
103
- Delete)
105
+ delete)
104
106
  GUARDRAIL_TOOL="write"
105
107
  CONTEXT_JSON="$(safe_jq "$INPUT" '{file_path: (.tool_input.file_path // .tool_input.path // "")}')"
106
108
  ;;
107
- Task)
109
+ task)
108
110
  GUARDRAIL_TOOL="session.create"
109
111
  CONTEXT_JSON="$(safe_jq "$INPUT" '{description: (.tool_input.description // .tool_input.prompt // "")}')"
110
112
  ;;
111
- MCP:*)
113
+ mcp:*)
112
114
  GUARDRAIL_TOOL="mcp.tool"
113
115
  CONTEXT_JSON="$(safe_jq "$INPUT" '{tool_name: (.tool_name // ""), tool_input: (.tool_input // {})}')"
114
116
  ;;
@@ -24,6 +24,35 @@ resolve_aport_paths() {
24
24
  . "$_resolve_script_dir/../bin/lib/validation.sh" 2> /dev/null || true
25
25
  fi
26
26
 
27
+ # 0) AGENTS.md enforcement block (repo-scoped, highest priority after explicit env)
28
+ # Only checked when no explicit env var is set — OPENCLAW_PASSPORT_FILE always wins.
29
+ if [ -z "${OPENCLAW_PASSPORT_FILE:-}" ]; then
30
+ local _agentsmd_lib="$_resolve_script_dir/lib/agentsmd.sh"
31
+ [ ! -f "$_agentsmd_lib" ] && _agentsmd_lib="$_resolve_script_dir/../bin/lib/agentsmd.sh"
32
+ if [ -f "$_agentsmd_lib" ]; then
33
+ # shellcheck source=lib/agentsmd.sh
34
+ . "$_agentsmd_lib"
35
+ if resolve_agentsmd_enforcement 2> /dev/null; then
36
+ if [ -n "$AGENTSMD_AGENT_ID" ]; then
37
+ export APORT_AGENT_ID="$AGENTSMD_AGENT_ID"
38
+ fi
39
+ if [ -n "$AGENTSMD_PASSPORT" ] && [ -f "$AGENTSMD_PASSPORT" ]; then
40
+ passport_path="$AGENTSMD_PASSPORT"
41
+ data_dir="$(dirname "$AGENTSMD_PASSPORT")"
42
+ export OPENCLAW_PASSPORT_FILE="$passport_path"
43
+ if [ -z "${OPENCLAW_DECISION_FILE:-}" ]; then
44
+ export OPENCLAW_DECISION_FILE="${data_dir}/decision.json"
45
+ fi
46
+ export OPENCLAW_AUDIT_LOG="${data_dir}/audit.log"
47
+ PASSPORT_FILE="$passport_path"
48
+ DECISION_FILE="${OPENCLAW_DECISION_FILE}"
49
+ AUDIT_LOG="${data_dir}/audit.log"
50
+ return 0
51
+ fi
52
+ fi
53
+ fi
54
+ fi
55
+
27
56
  # 1) Explicit path set and file exists → use it (plugin or wrapper)
28
57
  if [ -n "${OPENCLAW_PASSPORT_FILE:-}" ] && [ -f "$OPENCLAW_PASSPORT_FILE" ]; then
29
58
  # Validate env-provided path if validator is available
@@ -47,7 +76,7 @@ resolve_aport_paths() {
47
76
  # 3) No env → probe framework-specific default paths (where each framework stores data), then OpenClaw legacy
48
77
  else
49
78
  config_dir=""
50
- for candidate in "$HOME/.claude" "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.n8n"; do
79
+ for candidate in "$HOME/.claude" "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.aport/deerflow" "$HOME/.n8n"; do
51
80
  if [ -f "${candidate}/aport/passport.json" ]; then
52
81
  config_dir="$candidate"
53
82
  break
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # Generic framework setup. Works for any framework that only needs:
3
+ # 1. Config dir + passport wizard
4
+ # 2. Framework-specific "next steps" text (from next-steps.d/<framework>.txt)
5
+ #
6
+ # Frameworks with custom logic (cursor, claude-code, openclaw) keep their own scripts.
7
+ # To add a new framework: create next-steps.d/<name>.txt and add to valid_frameworks
8
+ # in bin/agent-guardrails. No new shell script needed.
9
+
10
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")/../lib" && pwd)"
11
+ FRAMEWORKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" && pwd)"
12
+ # shellcheck source=../lib/common.sh
13
+ source "$LIB/common.sh"
14
+ # shellcheck source=../lib/passport.sh
15
+ source "$LIB/passport.sh"
16
+ # shellcheck source=../lib/config.sh
17
+ source "$LIB/config.sh"
18
+
19
+ framework="${APORT_FRAMEWORK:?APORT_FRAMEWORK must be set by the dispatcher}"
20
+
21
+ run_setup() {
22
+ log_info "Setting up APort guardrails for $framework..."
23
+ config_dir="$(write_config_template "$framework")"
24
+ mkdir -p "$config_dir/aport"
25
+ chmod 700 "$config_dir/aport"
26
+ export APORT_FRAMEWORK="$framework"
27
+ run_passport_wizard "$@"
28
+ # Harden permissions on passport (contains policy/capabilities)
29
+ [ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
30
+
31
+ # Print framework-specific next steps from data file
32
+ next_steps="$FRAMEWORKS_DIR/next-steps.d/$framework.txt"
33
+ if [ -f "$next_steps" ]; then
34
+ echo ""
35
+ # Substitute $config_dir in the template
36
+ sed "s|\$config_dir|$config_dir|g" "$next_steps"
37
+ echo ""
38
+ else
39
+ echo ""
40
+ echo " Config written to: $config_dir"
41
+ echo " Install: pip install aport-agent-guardrails"
42
+ echo " See: docs/frameworks/$framework.md"
43
+ echo ""
44
+ fi
45
+
46
+ # Check if Python package is installed (skip in CI/tests)
47
+ if [[ -z "${APORT_SKIP_ADAPTER_CHECK:-}" ]]; then
48
+ if command -v pip &> /dev/null || command -v pip3 &> /dev/null; then
49
+ pip_cmd="pip"
50
+ command -v pip3 &> /dev/null && pip_cmd="pip3"
51
+ if ! $pip_cmd show aport-agent-guardrails &> /dev/null; then
52
+ echo "[aport] NOTE: Python package not installed. Run:" >&2
53
+ echo " $pip_cmd install aport-agent-guardrails" >&2
54
+ echo " Or: uv add aport-agent-guardrails" >&2
55
+ fi
56
+ fi
57
+ fi
58
+ }
59
+
60
+ run_setup "$@"
@@ -0,0 +1,13 @@
1
+ Next steps (CrewAI):
2
+ ───────────────────
3
+ 1. Install the Python adapter (required for runtime enforcement):
4
+ pip install aport-agent-guardrails-crewai
5
+ aport-crewai setup
6
+ 2. Config written to: $config_dir
7
+ 3. Register the guardrail before running your crew:
8
+
9
+ from aport_guardrails_crewai import register_aport_guardrail
10
+ register_aport_guardrail()
11
+ crew.kickoff()
12
+
13
+ Or: @with_aport_guardrail on your entry point. See: docs/frameworks/crewai.md
@@ -0,0 +1,14 @@
1
+ Next steps (DeerFlow):
2
+ ──────────────────────
3
+ 1. Install the guardrails package:
4
+ uv add aport-agent-guardrails
5
+
6
+ 2. Add to your DeerFlow config.yaml:
7
+
8
+ guardrails:
9
+ enabled: true
10
+ passport: $config_dir/aport/passport.json
11
+ provider:
12
+ use: aport_guardrails.providers.generic:OAPGuardrailProvider
13
+
14
+ See: docs/frameworks/deerflow.md
@@ -0,0 +1,12 @@
1
+ Next steps (LangChain):
2
+ ───────────────────────
3
+ 1. Install the Python adapter (required for runtime enforcement):
4
+ pip install aport-agent-guardrails-langchain
5
+ aport-langchain setup
6
+ 2. Config written to: $config_dir
7
+ 3. Add to your agent:
8
+
9
+ from aport_guardrails_langchain import APortCallback
10
+ agent = initialize_agent(..., callbacks=[APortCallback()])
11
+
12
+ See: docs/frameworks/langchain.md
@@ -0,0 +1,9 @@
1
+ APort n8n support: coming soon. This setup only writes config; custom node not yet available.
2
+
3
+ Next steps (n8n):
4
+ ────────────────
5
+ 1. Install custom node to ~/.n8n/custom/ (or use HTTP Request node to APort API).
6
+ 2. In your workflow: add APort Guardrail node before action nodes; branch on allow/deny.
7
+ 3. Store agent_id or passport in n8n credentials.
8
+
9
+ See: docs/frameworks/n8n.md
package/bin/lib/config.sh CHANGED
@@ -16,6 +16,7 @@ get_config_dir() {
16
16
  n8n) echo "${APORT_N8N_CONFIG_DIR:-$HOME/.n8n}" ;;
17
17
  cursor) echo "${APORT_CURSOR_CONFIG_DIR:-$HOME/.cursor}" ;;
18
18
  claude-code) echo "${APORT_CLAUDE_CODE_CONFIG_DIR:-$HOME/.claude}" ;;
19
+ deerflow) echo "${APORT_DEERFLOW_CONFIG_DIR:-$HOME/.aport/deerflow}" ;;
19
20
  *) echo "${APORT_CONFIG_DIR:-$HOME/.aport}" ;;
20
21
  esac
21
22
  }
@@ -0,0 +1,168 @@
1
+ # APort Agent Guardrail — DeerFlow
2
+
3
+ DeerFlow is a LangGraph-based AI super agent system by ByteDance with sandbox execution, persistent memory, and subagent delegation. The **APort Agent Guardrail for DeerFlow** plugs into DeerFlow's native **middleware chain** via the `GuardrailMiddleware`: every tool call (including dynamically-loaded MCP tools) is evaluated against your passport and policy before execution.
4
+
5
+ ## How DeerFlow agent guardrails work
6
+
7
+ - **Middleware:** DeerFlow uses an `AgentMiddleware` chain with `wrap_tool_call` / `awrap_tool_call`. The `GuardrailMiddleware` sits between `DanglingToolCallMiddleware` and `ToolErrorHandlingMiddleware` in the chain.
8
+ - **Provider protocol:** DeerFlow defines a generic `GuardrailProvider` protocol. Any class with `evaluate(request)` and `aevaluate(request)` methods works. APort's `OAPGuardrailProvider` implements this protocol.
9
+ - **Deny handling:** Denied tool calls return a `ToolMessage` with `status="error"` and OAP reason codes (e.g. `oap.command_not_allowed`). The agent sees the denial and can adapt.
10
+ - **Fail-closed:** If the provider raises an exception, the tool call is blocked by default.
11
+
12
+ - **Integration:** DeerFlow `GuardrailMiddleware` (in `_build_runtime_middlewares`)
13
+ - **Config:** `config.yaml` guardrails section + `~/.aport/deerflow/config.yaml` for APort settings
14
+ - **Provider class path:** `aport_guardrails.providers.generic:OAPGuardrailProvider`
15
+
16
+ ## Two ways to use APort
17
+
18
+ | Use case | What it is | When to use it |
19
+ |----------|------------|----------------|
20
+ | **Guardrails (CLI/setup)** | One-line installer: runs the **passport wizard**, writes config, prints next steps. Does not run your app. | Getting started: create passport and config so the provider can find them. |
21
+ | **Core (library)** | The **OAPGuardrailProvider** loaded by DeerFlow's `resolve_variable` from your `config.yaml`. Evaluates every tool call. | Integrating into your app: add the guardrails section to config.yaml. |
22
+
23
+ You typically use **both**: run the CLI once to create passport and config, then add the guardrails section to your DeerFlow `config.yaml`.
24
+
25
+ ---
26
+
27
+ ## Setup (Guardrails — create passport and config)
28
+
29
+ **Python (recommended)**
30
+
31
+ ```bash
32
+ pip install aport-agent-guardrails
33
+ aport setup --framework deerflow
34
+ ```
35
+
36
+ **Node/npx (alternative)**
37
+
38
+ ```bash
39
+ npx @aporthq/aport-agent-guardrails deerflow
40
+ ```
41
+
42
+ **Hosted passport (production)**
43
+
44
+ ```bash
45
+ npx @aporthq/aport-agent-guardrails deerflow ap_fa2f6d53bb5b4c98b9af0124285b6e0f
46
+ ```
47
+
48
+ The setup creates:
49
+ - Config directory: `~/.aport/deerflow/`
50
+ - Passport file: `~/.aport/deerflow/aport/passport.json`
51
+ - Config file: `~/.aport/deerflow/config.yaml`
52
+
53
+ ## Using the library (Core) in your DeerFlow app
54
+
55
+ ### Step 1: Install the package
56
+
57
+ ```bash
58
+ uv add aport-agent-guardrails
59
+ # or: pip install aport-agent-guardrails
60
+ ```
61
+
62
+ ### Step 2: Add guardrails to config.yaml
63
+
64
+ Add this section to your DeerFlow `config.yaml`:
65
+
66
+ ```yaml
67
+ guardrails:
68
+ enabled: true
69
+ fail_closed: true
70
+ passport: ~/.aport/deerflow/aport/passport.json
71
+ provider:
72
+ use: aport_guardrails.providers.generic:OAPGuardrailProvider
73
+ ```
74
+
75
+ That's it. Every tool call is now evaluated before execution.
76
+
77
+ ### Step 3 (optional): Use a hosted passport
78
+
79
+ For production, use a hosted passport ID instead of a local file:
80
+
81
+ ```yaml
82
+ guardrails:
83
+ enabled: true
84
+ passport: ap_fa2f6d53bb5b4c98b9af0124285b6e0f
85
+ provider:
86
+ use: aport_guardrails.providers.generic:OAPGuardrailProvider
87
+ ```
88
+
89
+ ---
90
+
91
+ ## How it works under the hood
92
+
93
+ 1. DeerFlow's `_build_runtime_middlewares()` reads `guardrails` from `config.yaml`
94
+ 2. It loads `OAPGuardrailProvider` via `resolve_variable` (same mechanism as models and sandbox providers)
95
+ 3. `GuardrailMiddleware` wraps every tool call:
96
+ - Builds a `GuardrailRequest` with tool name, arguments, and passport reference
97
+ - Calls `provider.evaluate(request)` (sync) or `provider.aevaluate(request)` (async)
98
+ - If denied: returns a `ToolMessage` with error status and OAP reason codes
99
+ - If allowed: calls the original tool handler
100
+ 4. `OAPGuardrailProvider` maps the tool name to an OAP policy pack ID and calls the core `Evaluator`
101
+ 5. The `Evaluator` resolves the passport (local file or hosted API) and evaluates the tool call
102
+
103
+ ## Tool-to-policy mapping
104
+
105
+ | DeerFlow tool | OAP policy pack |
106
+ |---|---|
107
+ | `bash`, `write_file`, `str_replace` | `system.command.execute.v1` |
108
+ | `web_search`, `web_fetch`, `image_search` | `web.fetch.v1` |
109
+ | `read_file`, `ls` | `data.file.read.v1` |
110
+ | `present_file`, `view_image` | `data.export.create.v1` |
111
+ | `ask_clarification`, `task` | `agent.session.create.v1` |
112
+ | MCP tools (dynamic) | `mcp.tool.execute.v1` |
113
+
114
+ ## Evaluation modes
115
+
116
+ | Mode | Config | Network | Use case |
117
+ |---|---|---|---|
118
+ | **Local** | `mode: local` in APort config | None | Dev, CI, air-gapped |
119
+ | **API** | `mode: api` in APort config | API call to aport.io | Full OAP features, signed decisions |
120
+ | **Hosted passport** | `passport: ap_xxx` in DeerFlow config | API call | Production, managed passports |
121
+
122
+ ## Built-in AllowlistProvider (no APort needed)
123
+
124
+ DeerFlow ships with a zero-dependency `AllowlistProvider` for simple deny/allow:
125
+
126
+ ```yaml
127
+ guardrails:
128
+ enabled: true
129
+ provider:
130
+ use: deerflow.guardrails.builtin:AllowlistProvider
131
+ config:
132
+ denied_tools: ["bash", "write_file"]
133
+ ```
134
+
135
+ ## Custom provider (bring your own)
136
+
137
+ Any class with `evaluate` and `aevaluate` methods works:
138
+
139
+ ```python
140
+ class MyGuardrail:
141
+ name = "my-company"
142
+ def evaluate(self, request):
143
+ from deerflow.guardrails.provider import GuardrailDecision, GuardrailReason
144
+ if request.tool_name == "bash":
145
+ return GuardrailDecision(allow=False, reasons=[GuardrailReason(code="oap.denied", message="bash blocked")])
146
+ return GuardrailDecision(allow=True, reasons=[GuardrailReason(code="oap.allowed")])
147
+ async def aevaluate(self, request):
148
+ return self.evaluate(request)
149
+ ```
150
+
151
+ ```yaml
152
+ guardrails:
153
+ enabled: true
154
+ provider:
155
+ use: my_module:MyGuardrail
156
+ ```
157
+
158
+ ## Kill switch
159
+
160
+ - **Local:** Set `"status": "suspended"` in your passport JSON file
161
+ - **Hosted:** Log in to aport.io, click Suspend — all agents using that passport deny within 30 seconds
162
+
163
+ ## References
164
+
165
+ - [DeerFlow GitHub issue #1213](https://github.com/bytedance/deer-flow/issues/1213)
166
+ - [OAP Specification](https://doi.org/10.5281/zenodo.18901595)
167
+ - [Verification methods](../VERIFICATION_METHODS.md)
168
+ - [Hosted passport setup](../HOSTED_PASSPORT_SETUP.md)
@@ -23,6 +23,12 @@ Policy packs are **pre-built, OAP-compliant policy definitions** that provide in
23
23
  | **`agent.session.create.v1`** | `agent.session.create` | L0 | Session limits, duration restrictions, concurrent session controls |
24
24
  | **`agent.tool.register.v1`** | `agent.tool.register` | L0 | Tool naming conventions, capability declarations, registration limits |
25
25
 
26
+ ### ✅ **Task Completion**
27
+
28
+ | Policy Pack | Capability | Min Assurance | Key Features |
29
+ |-------------|------------|---------------|--------------|
30
+ | **`deliverable.task.complete.v1`** | `deliverable.task.complete` | L0 | Summary word count, acceptance criteria attestations, tests passing, different reviewer, blocked pattern scan |
31
+
26
32
  ### 💳 **Finance & Payments**
27
33
 
28
34
  | Policy Pack | Capability | Min Assurance | Key Features |
@@ -0,0 +1,95 @@
1
+ # deliverable.task.complete.v1
2
+
3
+ Task Completion Gate — pre-action governance for an agent marking a task complete.
4
+
5
+ ## Purpose
6
+
7
+ Enforces that the agent has provided required deliverable evidence before "done" is authorized:
8
+
9
+ - Summary (optional, configurable min word count)
10
+ - Acceptance criteria attestations (one per passport-defined criterion, with evidence)
11
+ - Test status (optional, require passing tests)
12
+ - Reviewer identity (optional, require different agent for multi-agent pipelines)
13
+ - Output scan (optional, block patterns like TODO, FIXME)
14
+
15
+ ## Required Context
16
+
17
+ | Field | Type | Required |
18
+ |-------|------|----------|
19
+ | `task_id` | string | Yes |
20
+ | `output_type` | "code" \| "document" \| "analysis" \| "plan" \| "data" \| "other" | Yes |
21
+ | `criteria_attestations` | array of `{ criterion_id, met, evidence }` | Yes |
22
+ | `summary` | string | If `require_summary` |
23
+ | `tests_passing` | boolean | If `require_tests_passing` |
24
+ | `reviewer_agent_id` | string | If `require_different_reviewer` |
25
+ | `author_agent_id` | string | If `require_different_reviewer` |
26
+ | `output_content` | string | If `scan_output` and scanning |
27
+
28
+ ## Passport Limits
29
+
30
+ Configure under `limits["deliverable.task.complete"]`:
31
+
32
+ ```json
33
+ {
34
+ "require_summary": true,
35
+ "min_summary_words": 20,
36
+ "require_tests_passing": false,
37
+ "require_different_reviewer": false,
38
+ "scan_output": false,
39
+ "blocked_patterns": [],
40
+ "acceptance_criteria": [
41
+ { "id": "output_produced", "description": "A concrete output artifact must be produced" },
42
+ { "id": "no_placeholders", "description": "Output must not contain TODO, FIXME, or placeholder text" }
43
+ ]
44
+ }
45
+ ```
46
+
47
+ **Note:** `blocked_patterns` is in passport limits (not API context). At evaluation, only the first 100 patterns are checked; passports with more are truncated silently. Passport issuance APIs may validate this; the validator caps at 100 for DoS protection.
48
+
49
+ ## Security Rationale (Validator Design)
50
+
51
+ - **`met !== true` (strict)** — We use strict equality, not truthy `!met`. PRD v1.2 fix: `met: "true"` or `met: 1` must not pass; only `met: true` is valid.
52
+ - **Missing `author_agent_id` → deny** — When `require_different_reviewer` is true, both `reviewer_agent_id` and `author_agent_id` must be present. Omitting `author_agent_id` would bypass the cross-agent check; we intentionally deny instead of skipping.
53
+ - **Custom validators over expressions** — Safer `undefined` handling and no expression-engine quirks. Expressions can mis-evaluate empty strings or `undefined`; validators give explicit control over deny codes.
54
+
55
+ ## Deny Codes
56
+
57
+ | Code | Meaning |
58
+ |------|---------|
59
+ | `oap.criteria_not_met` | An attestation has `met: false` |
60
+ | `oap.evidence_missing` | An attestation has empty evidence |
61
+ | `oap.criteria_incomplete` | Missing attestation for a passport criterion |
62
+ | `oap.summary_insufficient` | Summary absent or below `min_summary_words` |
63
+ | `oap.tests_not_passing` | `tests_passing` required but false or missing |
64
+ | `oap.self_review_not_allowed` | Same agent for reviewer and author, or either missing |
65
+ | `oap.blocked_pattern_detected` | Output contains a blocked pattern |
66
+
67
+ ## Verification
68
+
69
+ ```bash
70
+ POST /api/verify/policy/deliverable.task.complete.v1
71
+ Content-Type: application/json
72
+
73
+ {
74
+ "context": {
75
+ "agent_id": "ap_xxx",
76
+ "task_id": "task-123",
77
+ "output_type": "code",
78
+ "author_agent_id": "ap_xxx",
79
+ "summary": "Implemented OAuth2 refresh token flow...",
80
+ "tests_passing": true,
81
+ "criteria_attestations": [
82
+ { "criterion_id": "output_produced", "met": true, "evidence": "PR #47" },
83
+ { "criterion_id": "no_placeholders", "met": true, "evidence": "grep -r TODO src/ returned 0" }
84
+ ]
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## Tests
90
+
91
+ Run unit tests:
92
+
93
+ ```bash
94
+ pnpm test:unit policies/deliverable.task.complete.v1/tests/deliverable-task-complete-policy.test.ts
95
+ ```