@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.
- package/bin/agent-guardrails +16 -8
- package/bin/aport-cursor-hook.sh +10 -8
- package/bin/aport-resolve-paths.sh +30 -1
- package/bin/frameworks/generic.sh +60 -0
- package/bin/frameworks/next-steps.d/crewai.txt +13 -0
- package/bin/frameworks/next-steps.d/deerflow.txt +14 -0
- package/bin/frameworks/next-steps.d/langchain.txt +12 -0
- package/bin/frameworks/next-steps.d/n8n.txt +9 -0
- package/bin/lib/config.sh +1 -0
- package/docs/frameworks/deerflow.md +168 -0
- package/external/aport-policies/README.md +6 -0
- package/external/aport-policies/deliverable.task.complete.v1/README.md +95 -0
- package/external/aport-policies/deliverable.task.complete.v1/policy.json +169 -0
- package/external/aport-policies/deliverable.task.complete.v1/tests/deliverable-task-complete-policy.test.ts +588 -0
- package/package.json +1 -1
- package/bin/frameworks/crewai.sh +0 -52
- package/bin/frameworks/langchain.sh +0 -51
- package/bin/frameworks/n8n.sh +0 -39
- package/external/aport-spec/index.json +0 -12
- package/external/aport-spec/passport-schema.json +0 -586
package/bin/agent-guardrails
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
165
|
+
echo " Supported: openclaw, langchain, crewai, cursor, claude-code, deerflow, n8n" >&2
|
|
158
166
|
exit 1
|
package/bin/aport-cursor-hook.sh
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
```
|