@aporthq/aport-agent-guardrails 1.0.25 → 1.0.26
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/README.md +20 -4
- package/bin/agent-guardrails +36 -0
- package/bin/aport-reset-framework.sh +263 -0
- package/docs/RELEASE.md +1 -1
- package/docs/frameworks/claude-code.md +22 -0
- package/extensions/openclaw-aport/CHANGELOG.md +2 -0
- package/extensions/openclaw-aport/openclaw.plugin.json +1 -1
- package/extensions/openclaw-aport/package-lock.json +2 -2
- package/extensions/openclaw-aport/package.json +1 -1
- package/external/aport-spec/oap/CHANGELOG.md +18 -0
- package/external/aport-spec/oap/delegation.md +455 -0
- package/external/aport-spec/oap/oap-spec.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ npx @aporthq/aport-agent-guardrails
|
|
|
55
55
|
- Choose your framework: `openclaw`, `cursor`, `claude-code`, `langchain`, `crewai`, `deerflow`, `n8n`
|
|
56
56
|
- OpenClaw direct: `npx @aporthq/aport-agent-guardrails openclaw`
|
|
57
57
|
- Hosted passport: `npx @aporthq/aport-agent-guardrails openclaw <agent_id>`
|
|
58
|
+
- Reset a framework to a clean APort state: `npx @aporthq/aport-agent-guardrails reset claude-code --yes`
|
|
58
59
|
|
|
59
60
|
### Why Developers and teams trust APort
|
|
60
61
|
|
|
@@ -129,6 +130,22 @@ npx @aporthq/aport-agent-guardrails
|
|
|
129
130
|
# --mode=local
|
|
130
131
|
```
|
|
131
132
|
|
|
133
|
+
**Reset / uninstall APort-owned wiring**
|
|
134
|
+
|
|
135
|
+
Use the same dispatcher for cleanup:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx @aporthq/aport-agent-guardrails reset claude-code --yes
|
|
139
|
+
# or
|
|
140
|
+
npx @aporthq/aport-agent-guardrails claude-code reset --yes
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Supported reset targets match the CLI-supported frameworks:
|
|
144
|
+
`openclaw`, `cursor`, `claude-code`, `langchain`, `crewai`, `deerflow`, `n8n`.
|
|
145
|
+
|
|
146
|
+
Reset removes APort-owned config and integration wiring for the selected framework.
|
|
147
|
+
When possible, unrelated user hooks are preserved.
|
|
148
|
+
|
|
132
149
|
**Python (LangChain, CrewAI, or DeerFlow):** Use the Python CLI directly via `uvx` or an installed package:
|
|
133
150
|
```bash
|
|
134
151
|
uvx --from aport-agent-guardrails aport setup --framework=langchain
|
|
@@ -183,7 +200,7 @@ Your framework doc (Cursor, OpenClaw, LangChain, CrewAI) describes where the con
|
|
|
183
200
|
| **Bypass risk** | None | High |
|
|
184
201
|
| **Recommended** | **Yes** | Only if plugin unavailable |
|
|
185
202
|
|
|
186
|
-
**Plugin (recommended):** Platform runs the guardrail before every tool; the model cannot skip it. This repo implements the **plugin (before_tool_call)** integration
|
|
203
|
+
**Plugin (recommended):** Platform runs the guardrail before every tool; the model cannot skip it. This repo implements the public **plugin (before_tool_call)** integration for OpenClaw.
|
|
187
204
|
**AGENTS.md:** Agent is *instructed* to call the guardrail; best-effort only.
|
|
188
205
|
|
|
189
206
|
---
|
|
@@ -405,6 +422,7 @@ See [Verification methods](docs/VERIFICATION_METHODS.md) for a detailed comparis
|
|
|
405
422
|
| Command | Purpose |
|
|
406
423
|
|--------|---------|
|
|
407
424
|
| `agent-guardrails` | Main entry — prompt for framework or pass one: `agent-guardrails openclaw \| cursor \| claude-code \| langchain \| crewai \| deerflow \| n8n`. Args after the framework are passed through (e.g. `agent-guardrails openclaw <agent_id>`). |
|
|
425
|
+
| `agent-guardrails reset <framework> [--yes]` | Remove APort-owned config and hook/plugin wiring for one framework. Positional form also works: `agent-guardrails <framework> reset --yes`. |
|
|
408
426
|
| `aport` | OpenClaw one-command setup (passport + plugin + wrappers). Optional: `aport <agent_id>` for hosted passport. |
|
|
409
427
|
| `aport-guardrail` | Run guardrail check from the CLI (e.g. `aport-guardrail system.command.execute '{"command":"ls"}'`). Uses passport from your framework config dir. |
|
|
410
428
|
|
|
@@ -517,7 +535,7 @@ Contributions welcome: policy packs, framework adapters, docs. See [CONTRIBUTING
|
|
|
517
535
|
|
|
518
536
|
Apache 2.0 — see [LICENSE](LICENSE).
|
|
519
537
|
|
|
520
|
-
**Open-core:** Local evaluation and CLI in this repo are open source (Apache 2.0). [api.aport.io](https://api.aport.io) is a separate product for cloud features
|
|
538
|
+
**Open-core:** Local evaluation and CLI in this repo are open source (Apache 2.0). [api.aport.io](https://api.aport.io) is a separate product for cloud features such as signed receipts, global kill switch, and team sync.
|
|
521
539
|
|
|
522
540
|
---
|
|
523
541
|
|
|
@@ -527,5 +545,3 @@ Apache 2.0 — see [LICENSE](LICENSE).
|
|
|
527
545
|
- [GitHub Issues](https://github.com/aporthq/aport-agent-guardrails/issues) · [Discussions](https://github.com/aporthq/aport-agent-guardrails/discussions)
|
|
528
546
|
|
|
529
547
|
---
|
|
530
|
-
|
|
531
|
-
<p align="center">Made with ❤️ by [Uchi](https://github.com/uchibeke/) </p>
|
package/bin/agent-guardrails
CHANGED
|
@@ -29,6 +29,7 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
29
29
|
FRAMEWORKS_DIR="$SCRIPT_DIR/frameworks"
|
|
30
30
|
LIB_DIR="$SCRIPT_DIR/lib"
|
|
31
31
|
SUPPORTED_FRAMEWORKS=(openclaw langchain crewai cursor claude-code deerflow n8n)
|
|
32
|
+
reset_requested=""
|
|
32
33
|
|
|
33
34
|
framework_supported() {
|
|
34
35
|
local candidate="${1:-}"
|
|
@@ -44,6 +45,7 @@ framework_supported() {
|
|
|
44
45
|
# Parse --framework= and -f (skip detection when set)
|
|
45
46
|
framework=""
|
|
46
47
|
integration_mode=""
|
|
48
|
+
noninteractive_requested=""
|
|
47
49
|
REST=()
|
|
48
50
|
while [[ $# -gt 0 ]]; do
|
|
49
51
|
case "$1" in
|
|
@@ -73,6 +75,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
73
75
|
exit 1
|
|
74
76
|
fi
|
|
75
77
|
;;
|
|
78
|
+
--non-interactive|--noninteractive)
|
|
79
|
+
noninteractive_requested="1"
|
|
80
|
+
shift
|
|
81
|
+
;;
|
|
76
82
|
*)
|
|
77
83
|
REST+=("$1")
|
|
78
84
|
shift
|
|
@@ -80,7 +86,23 @@ while [[ $# -gt 0 ]]; do
|
|
|
80
86
|
esac
|
|
81
87
|
done
|
|
82
88
|
|
|
89
|
+
if [[ -n "$noninteractive_requested" ]]; then
|
|
90
|
+
export APORT_NONINTERACTIVE=1
|
|
91
|
+
fi
|
|
92
|
+
|
|
83
93
|
# If no framework from args, check if first REST argument is a framework name
|
|
94
|
+
if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
|
|
95
|
+
first_arg="${REST[0]}"
|
|
96
|
+
if [[ "$first_arg" == "reset" ]] && [[ ${#REST[@]} -gt 1 ]]; then
|
|
97
|
+
second_arg="${REST[1]}"
|
|
98
|
+
if framework_supported "$second_arg"; then
|
|
99
|
+
framework="$second_arg"
|
|
100
|
+
reset_requested="1"
|
|
101
|
+
REST=("${REST[@]:2}")
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
|
|
84
106
|
if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
|
|
85
107
|
first_arg="${REST[0]}"
|
|
86
108
|
# Check if first arg looks like a framework name (lowercase alphanumeric + hyphen)
|
|
@@ -93,6 +115,11 @@ if [[ -z "$framework" ]] && [[ ${#REST[@]} -gt 0 ]]; then
|
|
|
93
115
|
fi
|
|
94
116
|
fi
|
|
95
117
|
|
|
118
|
+
if [[ -n "$framework" ]] && [[ ${#REST[@]} -gt 0 ]] && [[ "${REST[0]}" == "reset" ]]; then
|
|
119
|
+
reset_requested="1"
|
|
120
|
+
REST=("${REST[@]:1}")
|
|
121
|
+
fi
|
|
122
|
+
|
|
96
123
|
# If still no framework from args, try APORT_FRAMEWORK (non-interactive) or detection
|
|
97
124
|
if [[ -z "$framework" ]]; then
|
|
98
125
|
if [[ -n "${APORT_FRAMEWORK:-}" ]]; then
|
|
@@ -165,6 +192,15 @@ fi
|
|
|
165
192
|
|
|
166
193
|
echo "[aport] Selected framework: $framework" >&2
|
|
167
194
|
|
|
195
|
+
if [[ -n "$reset_requested" ]]; then
|
|
196
|
+
reset_script="$SCRIPT_DIR/aport-reset-framework.sh"
|
|
197
|
+
if [[ ! -x "$reset_script" ]]; then
|
|
198
|
+
echo "[aport] ERROR: Reset helper not found: $reset_script" >&2
|
|
199
|
+
exit 1
|
|
200
|
+
fi
|
|
201
|
+
exec "$reset_script" "$framework" ${REST+"${REST[@]}"}
|
|
202
|
+
fi
|
|
203
|
+
|
|
168
204
|
if [[ -n "$integration_mode" ]]; then
|
|
169
205
|
case "$integration_mode" in
|
|
170
206
|
compat|native) ;;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")/lib" && pwd)"
|
|
6
|
+
# shellcheck source=./lib/common.sh
|
|
7
|
+
source "$LIB/common.sh"
|
|
8
|
+
# shellcheck source=./lib/config.sh
|
|
9
|
+
source "$LIB/config.sh"
|
|
10
|
+
|
|
11
|
+
framework="${1:-}"
|
|
12
|
+
shift || true
|
|
13
|
+
|
|
14
|
+
if [[ -z "$framework" ]]; then
|
|
15
|
+
log_error "Usage: agent-guardrails reset <framework> [--yes]"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
yes_mode="${APORT_NONINTERACTIVE:-${CI:-}}"
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--yes | -y)
|
|
23
|
+
yes_mode=1
|
|
24
|
+
;;
|
|
25
|
+
*)
|
|
26
|
+
log_error "Unknown reset option: $1"
|
|
27
|
+
exit 1
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
shift
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
framework="$(echo "$framework" | tr '[:upper:]' '[:lower:]')"
|
|
34
|
+
config_dir="$(get_config_dir "$framework")"
|
|
35
|
+
config_dir="${config_dir/#\~/$HOME}"
|
|
36
|
+
|
|
37
|
+
confirm_reset() {
|
|
38
|
+
if [[ -n "$yes_mode" ]]; then
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo ""
|
|
43
|
+
echo " Reset APort for $framework"
|
|
44
|
+
echo " ──────────────────────────"
|
|
45
|
+
echo " This removes APort-owned local config and hook/plugin wiring for this framework."
|
|
46
|
+
echo " Other framework settings are preserved when possible."
|
|
47
|
+
echo ""
|
|
48
|
+
|
|
49
|
+
local answer
|
|
50
|
+
read -r -p " Continue? [y/N]: " answer
|
|
51
|
+
case "$answer" in
|
|
52
|
+
y | Y | yes | YES) ;;
|
|
53
|
+
*)
|
|
54
|
+
echo " Reset cancelled."
|
|
55
|
+
exit 0
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
backup_file() {
|
|
61
|
+
local file="$1"
|
|
62
|
+
if [[ -f "$file" ]]; then
|
|
63
|
+
cp "$file" "${file}.bak"
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
remove_dir_if_exists() {
|
|
68
|
+
local dir="$1"
|
|
69
|
+
if [[ -d "$dir" ]]; then
|
|
70
|
+
rm -rf "$dir"
|
|
71
|
+
echo " ✅ Removed $dir"
|
|
72
|
+
fi
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
remove_file_if_exists() {
|
|
76
|
+
local file="$1"
|
|
77
|
+
if [[ -f "$file" ]]; then
|
|
78
|
+
rm -f "$file"
|
|
79
|
+
echo " ✅ Removed $file"
|
|
80
|
+
fi
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cleanup_claude_settings() {
|
|
84
|
+
local settings_file="$1"
|
|
85
|
+
if [[ ! -f "$settings_file" ]]; then
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
if ! command -v jq &> /dev/null; then
|
|
89
|
+
log_warn "jq not found; leaving Claude settings intact at $settings_file"
|
|
90
|
+
return 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
local tmpfile
|
|
94
|
+
tmpfile="$(mktemp "${settings_file}.XXXXXX")"
|
|
95
|
+
if jq '
|
|
96
|
+
if (.hooks.PreToolUse? // null) == null then
|
|
97
|
+
.
|
|
98
|
+
else
|
|
99
|
+
.hooks.PreToolUse = (
|
|
100
|
+
(.hooks.PreToolUse // [])
|
|
101
|
+
| map(
|
|
102
|
+
if (.hooks? // null) == null then
|
|
103
|
+
.
|
|
104
|
+
else
|
|
105
|
+
.hooks = (
|
|
106
|
+
(.hooks // [])
|
|
107
|
+
| map(select((((.command // "") | test("aport-(cursor-hook|claude-code-hook)\\.sh$")) | not)))
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
)
|
|
111
|
+
| map(select(((.hooks? // []) | length) > 0))
|
|
112
|
+
)
|
|
113
|
+
| if ((.hooks.PreToolUse // []) | length) == 0 then del(.hooks.PreToolUse) else . end
|
|
114
|
+
| if ((.hooks // {}) | keys | length) == 0 then del(.hooks) else . end
|
|
115
|
+
end
|
|
116
|
+
' "$settings_file" > "$tmpfile"; then
|
|
117
|
+
backup_file "$settings_file"
|
|
118
|
+
mv "$tmpfile" "$settings_file"
|
|
119
|
+
echo " ✅ Removed APort Claude Code hook entries from $settings_file"
|
|
120
|
+
else
|
|
121
|
+
rm -f "$tmpfile"
|
|
122
|
+
log_warn "Failed to clean Claude settings at $settings_file"
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
cleanup_cursor_hooks() {
|
|
127
|
+
local hooks_file="$1"
|
|
128
|
+
if [[ ! -f "$hooks_file" ]]; then
|
|
129
|
+
return 0
|
|
130
|
+
fi
|
|
131
|
+
if ! command -v jq &> /dev/null; then
|
|
132
|
+
log_warn "jq not found; leaving Cursor hooks intact at $hooks_file"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
local tmpfile
|
|
137
|
+
tmpfile="$(mktemp "${hooks_file}.XXXXXX")"
|
|
138
|
+
if jq '
|
|
139
|
+
def strip_aport_hooks:
|
|
140
|
+
(. // []) | map(select((((.command // "") | test("aport-(cursor-hook|claude-code-hook)\\.sh$")) | not)));
|
|
141
|
+
.hooks.beforeShellExecution = ((.hooks.beforeShellExecution // []) | strip_aport_hooks) |
|
|
142
|
+
.hooks.preToolUse = ((.hooks.preToolUse // []) | strip_aport_hooks) |
|
|
143
|
+
.hooks.beforeMCPExecution = ((.hooks.beforeMCPExecution // []) | strip_aport_hooks) |
|
|
144
|
+
.hooks.subagentStart = ((.hooks.subagentStart // []) | strip_aport_hooks) |
|
|
145
|
+
if ((.hooks.beforeShellExecution // []) | length) == 0 then del(.hooks.beforeShellExecution) else . end |
|
|
146
|
+
if ((.hooks.preToolUse // []) | length) == 0 then del(.hooks.preToolUse) else . end |
|
|
147
|
+
if ((.hooks.beforeMCPExecution // []) | length) == 0 then del(.hooks.beforeMCPExecution) else . end |
|
|
148
|
+
if ((.hooks.subagentStart // []) | length) == 0 then del(.hooks.subagentStart) else . end |
|
|
149
|
+
if ((.hooks // {}) | keys | length) == 0 then del(.hooks) else . end
|
|
150
|
+
' "$hooks_file" > "$tmpfile"; then
|
|
151
|
+
backup_file "$hooks_file"
|
|
152
|
+
mv "$tmpfile" "$hooks_file"
|
|
153
|
+
echo " ✅ Removed APort Cursor hook entries from $hooks_file"
|
|
154
|
+
else
|
|
155
|
+
rm -f "$tmpfile"
|
|
156
|
+
log_warn "Failed to clean Cursor hooks at $hooks_file"
|
|
157
|
+
fi
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cleanup_openclaw_json() {
|
|
161
|
+
local openclaw_json="$1"
|
|
162
|
+
if [[ ! -f "$openclaw_json" ]]; then
|
|
163
|
+
return 0
|
|
164
|
+
fi
|
|
165
|
+
if ! command -v jq &> /dev/null; then
|
|
166
|
+
log_warn "jq not found; leaving OpenClaw JSON config intact at $openclaw_json"
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
local tmpfile
|
|
171
|
+
tmpfile="$(mktemp "${openclaw_json}.XXXXXX")"
|
|
172
|
+
if jq '
|
|
173
|
+
.plugins = (.plugins // {}) |
|
|
174
|
+
.plugins.entries = ((.plugins.entries // {}) | del(.["openclaw-aport"])) |
|
|
175
|
+
.plugins.installs = ((.plugins.installs // {}) | del(.["openclaw-aport"])) |
|
|
176
|
+
.plugins.load = (.plugins.load // {}) |
|
|
177
|
+
.plugins.load.paths = ((.plugins.load.paths // []) | map(select((type == "string" and contains("/openclaw-aport")) | not))) |
|
|
178
|
+
if ((.plugins.entries // {}) | keys | length) == 0 then del(.plugins.entries) else . end |
|
|
179
|
+
if ((.plugins.installs // {}) | keys | length) == 0 then del(.plugins.installs) else . end |
|
|
180
|
+
if ((.plugins.load.paths // []) | length) == 0 then del(.plugins.load.paths) else . end |
|
|
181
|
+
if ((.plugins.load // {}) | keys | length) == 0 then del(.plugins.load) else . end |
|
|
182
|
+
if ((.plugins // {}) | keys | length) == 0 then del(.plugins) else . end
|
|
183
|
+
' "$openclaw_json" > "$tmpfile"; then
|
|
184
|
+
backup_file "$openclaw_json"
|
|
185
|
+
mv "$tmpfile" "$openclaw_json"
|
|
186
|
+
echo " ✅ Removed APort OpenClaw entries from $openclaw_json"
|
|
187
|
+
else
|
|
188
|
+
rm -f "$tmpfile"
|
|
189
|
+
log_warn "Failed to clean OpenClaw JSON config at $openclaw_json"
|
|
190
|
+
fi
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
cleanup_python_framework() {
|
|
194
|
+
local config_dir="$1"
|
|
195
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
196
|
+
remove_file_if_exists "$config_dir/config.yaml"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
cleanup_n8n() {
|
|
200
|
+
local config_dir="$1"
|
|
201
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cleanup_claude() {
|
|
205
|
+
local settings_file="$config_dir/settings.json"
|
|
206
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
207
|
+
cleanup_claude_settings "$settings_file"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
cleanup_cursor() {
|
|
211
|
+
local hooks_file="$config_dir/hooks.json"
|
|
212
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
213
|
+
cleanup_cursor_hooks "$hooks_file"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
cleanup_openclaw() {
|
|
217
|
+
local openclaw_json="$config_dir/openclaw.json"
|
|
218
|
+
remove_dir_if_exists "$config_dir/aport"
|
|
219
|
+
remove_dir_if_exists "$config_dir/extensions/openclaw-aport"
|
|
220
|
+
remove_dir_if_exists "$config_dir/skills/aport-guardrail"
|
|
221
|
+
remove_file_if_exists "$config_dir/.aport-repo"
|
|
222
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail.sh"
|
|
223
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail-bash.sh"
|
|
224
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail-api.sh"
|
|
225
|
+
remove_file_if_exists "$config_dir/.skills/aport-guardrail-v2.sh"
|
|
226
|
+
remove_file_if_exists "$config_dir/.skills/aport-create-passport.sh"
|
|
227
|
+
remove_file_if_exists "$config_dir/.skills/aport-status.sh"
|
|
228
|
+
cleanup_openclaw_json "$openclaw_json"
|
|
229
|
+
if [[ -f "$config_dir/config.yaml" ]]; then
|
|
230
|
+
log_warn "OpenClaw config.yaml may still contain APort plugin config. Review $config_dir/config.yaml if you need a completely pristine OpenClaw config."
|
|
231
|
+
fi
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
confirm_reset
|
|
235
|
+
|
|
236
|
+
echo "[aport] Resetting framework: $framework" >&2
|
|
237
|
+
|
|
238
|
+
case "$framework" in
|
|
239
|
+
claude-code)
|
|
240
|
+
cleanup_claude
|
|
241
|
+
;;
|
|
242
|
+
cursor)
|
|
243
|
+
cleanup_cursor
|
|
244
|
+
;;
|
|
245
|
+
openclaw)
|
|
246
|
+
cleanup_openclaw
|
|
247
|
+
;;
|
|
248
|
+
langchain | crewai | deerflow)
|
|
249
|
+
cleanup_python_framework "$config_dir"
|
|
250
|
+
;;
|
|
251
|
+
n8n)
|
|
252
|
+
cleanup_n8n "$config_dir"
|
|
253
|
+
;;
|
|
254
|
+
*)
|
|
255
|
+
log_error "Unsupported framework reset: $framework"
|
|
256
|
+
exit 1
|
|
257
|
+
;;
|
|
258
|
+
esac
|
|
259
|
+
|
|
260
|
+
echo ""
|
|
261
|
+
echo " Reset complete for $framework."
|
|
262
|
+
echo " Re-run the setup command when you want a fresh install."
|
|
263
|
+
echo ""
|
package/docs/RELEASE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release process and version policy
|
|
2
2
|
|
|
3
|
-
**Current release:** 1.0.
|
|
3
|
+
**Current release:** 1.0.26 (see [CHANGELOG.md](../CHANGELOG.md)).
|
|
4
4
|
|
|
5
5
|
We keep **one version number** across all published packages (Node core, Python core, and every framework adapter). That avoids “core is 1.2 but CLI is 0.9” and keeps the story simple for users and support.
|
|
6
6
|
|
|
@@ -24,6 +24,28 @@ npx @aporthq/aport-agent-guardrails --framework=claude-code
|
|
|
24
24
|
|
|
25
25
|
This runs the **passport wizard** and writes **`~/.claude/settings.json`** with the APort hook registered for **all tools** via `"matcher": "*"`. Default passport path: **`~/.claude/aport/passport.json`**. Restart Claude Code after setup so the PreToolUse hook is picked up.
|
|
26
26
|
|
|
27
|
+
If you already have a hosted passport and API key, the intended hosted install path is:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export APORT_API_KEY="apk_..."
|
|
31
|
+
export APORT_AGENT_ID="ap_..."
|
|
32
|
+
npx @aporthq/aport-agent-guardrails claude-code "ap_..." --non-interactive
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That setup writes `~/.claude/aport/guardrail-mode.env`, and the Claude hook loads those values before every tool call. Hosted mode is fail-closed: if the API evaluator is unreachable, the tool call is denied rather than silently downgraded to local mode.
|
|
36
|
+
|
|
37
|
+
## Reset / uninstall
|
|
38
|
+
|
|
39
|
+
To remove APort-owned Claude hook wiring and local config:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @aporthq/aport-agent-guardrails reset claude-code --yes
|
|
43
|
+
# or
|
|
44
|
+
npx @aporthq/aport-agent-guardrails claude-code reset --yes
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This removes `~/.claude/aport/` and strips APort hook entries from `~/.claude/settings.json` while preserving unrelated Claude hooks where possible.
|
|
48
|
+
|
|
27
49
|
### Marketplace install (Claude plugins)
|
|
28
50
|
|
|
29
51
|
APort now includes a Claude plugin marketplace catalog at `.claude-plugin/marketplace.json`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "openclaw-aport",
|
|
3
3
|
"name": "APort Guardrails",
|
|
4
4
|
"description": "Deterministic pre-action authorization via APort policy enforcement. Registers before_tool_call to block disallowed tools.",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.26",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aporthq/openclaw-aport",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@aporthq/openclaw-aport",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.26",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@types/node": "^18.0.0",
|
|
@@ -5,6 +5,24 @@ All notable changes to the Open Agent Passport (OAP) specification will be docum
|
|
|
5
5
|
The format is based on [Keep a Change Log](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `oap/delegation.md` — OAP Delegation Chains specification (D-004)
|
|
13
|
+
- Delegation token object with Ed25519 signing and JCS canonicalization
|
|
14
|
+
- Mandatory scope-narrowing: `granted_capabilities ⊆ delegator.effective_capabilities`
|
|
15
|
+
- Configurable depth cap (`depth_cap` 1–8, default 3) with `depth_remaining` counter
|
|
16
|
+
- Chain verification algorithm with 10 error codes (`OAP-D-001` through `OAP-D-010`)
|
|
17
|
+
- Revocation endpoint protocol with 60-second cache
|
|
18
|
+
- Audit trail specification: every DT-governed action traceable to `chain_root_passport_id`
|
|
19
|
+
- Integration guide for OAP policy packs (`delegation_chain` context)
|
|
20
|
+
- Security considerations: short-lived tokens, minimum scope principle, key compromise handling
|
|
21
|
+
- Conformance requirements (7 MUST requirements)
|
|
22
|
+
- Informed by: aeoess Agent Passport System delegation chains (aport-spec issue #21)
|
|
23
|
+
- StaffEngineer Pass 1 fixes: depth_cap validation, formal `limitsWithinParent()` algorithm, clock skew tolerance (CLOCK_SKEW_TOLERANCE = 30s)
|
|
24
|
+
- StaffEngineer Pass 2 polish: `not_before` assertion in verification algorithm, null/empty edge case handling in `limitsWithinParent()`
|
|
25
|
+
|
|
8
26
|
## [1.0.0] - 2025-01-16
|
|
9
27
|
|
|
10
28
|
### Added
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# OAP Delegation Chains — Specification
|
|
2
|
+
|
|
3
|
+
**Status:** Working Draft
|
|
4
|
+
**Version:** 1.0.0
|
|
5
|
+
**Spec version:** oap/1.0
|
|
6
|
+
**Last updated:** 2026-03-15
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Abstract
|
|
11
|
+
|
|
12
|
+
This document specifies **OAP Delegation Chains** — the mechanism by which an AI agent holding a valid OAP passport may grant a sub-agent or downstream agent the authority to act on its behalf, within a strictly narrowed scope, up to a bounded delegation depth.
|
|
13
|
+
|
|
14
|
+
Delegation is a first-class concern in multi-agent architectures. When an orchestrator agent spawns workers, tool-calling cascades through potentially many agent boundaries. Without a formal delegation mechanism, two failure modes emerge:
|
|
15
|
+
|
|
16
|
+
1. **Scope escalation** — a sub-agent acquires capabilities that were never intended (the "confused deputy" problem)
|
|
17
|
+
2. **Chain opacity** — the authorizing system cannot trace which root principal authorized a downstream action, making auditability impossible
|
|
18
|
+
|
|
19
|
+
OAP Delegation Chains address both failure modes through cryptographically signed delegation tokens, mandatory scope narrowing, and a configurable depth cap.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Core Concepts
|
|
24
|
+
|
|
25
|
+
### 1.1 Delegation Token
|
|
26
|
+
|
|
27
|
+
A **Delegation Token** (DT) is a signed, time-bounded object that grants a **delegate** (recipient agent) a strict subset of the **delegator**'s active capabilities for a specified purpose.
|
|
28
|
+
|
|
29
|
+
Key properties:
|
|
30
|
+
- **Signed** by the delegator using its OAP-registered Ed25519 key
|
|
31
|
+
- **Scope-narrowing** — the delegate's effective capability set is the *intersection* of its own passport capabilities and the explicitly granted scope in the DT
|
|
32
|
+
- **Depth-limited** — each DT carries a `depth_remaining` counter that decrements with each re-delegation; when it reaches 0, re-delegation is prohibited
|
|
33
|
+
- **Time-bounded** — every DT has a mandatory `expires_at`; there is no `never_expires` flag for delegation tokens
|
|
34
|
+
- **Single-purpose** — a DT specifies a `purpose` string that documents the intended use; enforcement adapters MAY use this for logging and policy-pack filtering
|
|
35
|
+
|
|
36
|
+
### 1.2 Delegation Chain
|
|
37
|
+
|
|
38
|
+
A **Delegation Chain** is the ordered sequence of delegation tokens from a root OAP passport holder down to the currently-acting agent. Each link in the chain is a DT signed by the previous holder.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Root Passport (Org/User)
|
|
42
|
+
└─ DT-1: delegated to AgentA (depth_cap=3, scope=[finance.read])
|
|
43
|
+
└─ DT-2: AgentA re-delegates to AgentB (depth_remaining=2, scope=[finance.read])
|
|
44
|
+
└─ DT-3: AgentB re-delegates to AgentC (depth_remaining=1, scope=[finance.read])
|
|
45
|
+
(AgentC CANNOT re-delegate — depth_remaining=0)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 1.3 Scope Narrowing Rule (MUST)
|
|
49
|
+
|
|
50
|
+
When creating a DT, the delegator MUST ensure:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
granted_capabilities ⊆ delegator.effective_capabilities
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Where `delegator.effective_capabilities` is:
|
|
57
|
+
- The delegator's OAP passport capabilities, if the delegator is the root
|
|
58
|
+
- The capabilities in the delegator's own received DT, if the delegator is itself a delegate
|
|
59
|
+
|
|
60
|
+
Violation of this rule MUST cause the enforcement adapter to reject the DT with error code `OAP-D-001: SCOPE_EXCEEDS_DELEGATOR`.
|
|
61
|
+
|
|
62
|
+
### 1.4 Depth Cap
|
|
63
|
+
|
|
64
|
+
- The root delegator sets `depth_cap` (integer, 1–8) when issuing the first DT.
|
|
65
|
+
- Each re-delegation MUST set `depth_remaining = parent_dt.depth_remaining - 1`.
|
|
66
|
+
- A DT with `depth_remaining = 0` MUST NOT be re-delegated. Attempting to do so MUST fail with `OAP-D-003: DEPTH_EXHAUSTED`.
|
|
67
|
+
- **Default recommended cap:** `depth_cap = 3`. Values above 8 are implementation-defined and SHOULD require L4 assurance.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2. Delegation Token Object
|
|
72
|
+
|
|
73
|
+
### 2.1 Required Fields
|
|
74
|
+
|
|
75
|
+
| Field | Type | Description |
|
|
76
|
+
|-------|------|-------------|
|
|
77
|
+
| `delegation_id` | UUID v4 | Unique identifier for this DT |
|
|
78
|
+
| `spec_version` | string | MUST be `"oap/1.0"` |
|
|
79
|
+
| `delegator_passport_id` | UUID v4 | Passport ID of the issuing agent |
|
|
80
|
+
| `delegator_agent_id` | UUID v4 | Agent ID of the issuing agent |
|
|
81
|
+
| `delegate_passport_id` | UUID v4 | Passport ID of the receiving agent |
|
|
82
|
+
| `delegate_agent_id` | UUID v4 | Agent ID of the receiving agent |
|
|
83
|
+
| `granted_capabilities` | array of CapabilityGrant | Capabilities granted; each is a subset of delegator's active capabilities |
|
|
84
|
+
| `granted_limits` | object | Per-capability limits; MUST be ≤ delegator's own limits for each capability |
|
|
85
|
+
| `purpose` | string (max 256 chars) | Human-readable description of the delegation's intended use |
|
|
86
|
+
| `depth_cap` | integer (1–8) | Maximum delegation depth from root; set only by root delegator, propagated read-only |
|
|
87
|
+
| `depth_remaining` | integer (0–8) | Decrements each re-delegation; 0 = cannot re-delegate |
|
|
88
|
+
| `created_at` | ISO 8601 | When this DT was issued |
|
|
89
|
+
| `expires_at` | ISO 8601 | When this DT expires; REQUIRED; no never-expires flag |
|
|
90
|
+
| `parent_delegation_id` | UUID v4 \| null | `null` for root delegation; parent DT's `delegation_id` for re-delegations |
|
|
91
|
+
| `chain_root_passport_id` | UUID v4 | Passport ID of the root principal; propagated unchanged through entire chain |
|
|
92
|
+
| `delegator_signature` | string | Base64url-encoded Ed25519 signature over the JCS-canonicalized DT payload (excluding `delegator_signature` field) |
|
|
93
|
+
| `delegator_key_id` | string | Key ID (`kid`) used to sign; resolves via delegator's `/.well-known/oap/keys.json` |
|
|
94
|
+
|
|
95
|
+
### 2.2 Optional Fields
|
|
96
|
+
|
|
97
|
+
| Field | Type | Description |
|
|
98
|
+
|-------|------|-------------|
|
|
99
|
+
| `not_before` | ISO 8601 | If present, DT is not valid before this time; enforcement adapters MUST reject if `now() < not_before - CLOCK_SKEW_TOLERANCE` with error code `OAP-D-011: DELEGATION_NOT_YET_VALID` |
|
|
100
|
+
| `regions` | array of string | If present, restricts delegate to a subset of delegator's authorized regions |
|
|
101
|
+
| `policy_packs` | array of string | If present, restricts which OAP policy packs the delegate may use |
|
|
102
|
+
| `revocation_endpoint` | string (URI) | URL at which this DT's revocation status may be queried |
|
|
103
|
+
| `metadata` | object | Arbitrary key-value annotations; **NOT included in signed payload**; MUST NOT affect authorization decisions; advisory only |
|
|
104
|
+
|
|
105
|
+
### 2.3 Example Delegation Token
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"delegation_id": "7f3c8a1b-1e2d-4b5a-9c0e-123456789abc",
|
|
110
|
+
"spec_version": "oap/1.0",
|
|
111
|
+
"delegator_passport_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
112
|
+
"delegator_agent_id": "agt_orchestrator_001",
|
|
113
|
+
"delegate_passport_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
|
|
114
|
+
"delegate_agent_id": "agt_worker_finance_01",
|
|
115
|
+
"granted_capabilities": [
|
|
116
|
+
{
|
|
117
|
+
"id": "finance.payment.refund",
|
|
118
|
+
"params": {
|
|
119
|
+
"max_amount": 500,
|
|
120
|
+
"currency": "USD"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"granted_limits": {
|
|
125
|
+
"finance.payment.refund": {
|
|
126
|
+
"currency_limits": {
|
|
127
|
+
"USD": {
|
|
128
|
+
"max_per_tx": 500,
|
|
129
|
+
"daily_cap": 2000
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"reason_codes": ["customer_request"],
|
|
133
|
+
"idempotency_required": true
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"purpose": "Process refunds for open support tickets assigned in this batch run",
|
|
137
|
+
"depth_cap": 3,
|
|
138
|
+
"depth_remaining": 2,
|
|
139
|
+
"created_at": "2026-03-15T03:00:00Z",
|
|
140
|
+
"expires_at": "2026-03-15T05:00:00Z",
|
|
141
|
+
"parent_delegation_id": null,
|
|
142
|
+
"chain_root_passport_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
143
|
+
"delegator_signature": "base64url_encoded_ed25519_signature_here",
|
|
144
|
+
"delegator_key_id": "oap:owner:api.example.com:key-2026-01",
|
|
145
|
+
"revocation_endpoint": "https://api.aport.io/v1/delegations/7f3c8a1b-1e2d-4b5a-9c0e-123456789abc/status"
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 3. Signature and Verification
|
|
152
|
+
|
|
153
|
+
### 3.1 Signing
|
|
154
|
+
|
|
155
|
+
The delegator MUST sign the DT using its OAP-registered Ed25519 private key.
|
|
156
|
+
|
|
157
|
+
**Signed payload** (all fields EXCEPT `delegator_signature` and `metadata`), JCS-canonicalized per RFC 8785:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
payload = JCS({
|
|
161
|
+
delegation_id, spec_version, delegator_passport_id, delegator_agent_id,
|
|
162
|
+
delegate_passport_id, delegate_agent_id, granted_capabilities, granted_limits,
|
|
163
|
+
purpose, depth_cap, depth_remaining, created_at, expires_at,
|
|
164
|
+
parent_delegation_id, chain_root_passport_id, delegator_key_id,
|
|
165
|
+
regions?, policy_packs?, revocation_endpoint?
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
delegator_signature = base64url(Ed25519.sign(private_key, payload))
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 3.2 Verification Algorithm
|
|
172
|
+
|
|
173
|
+
An enforcement adapter receiving a DT (or chain of DTs) MUST execute the following. Implementations MUST allow a **clock skew tolerance of ±30 seconds** when evaluating `expires_at` (i.e., ASSERT `now() < expires_at + 30s`); this tolerance is a MUST to support distributed multi-agent deployments.
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
CONSTANT CLOCK_SKEW_TOLERANCE = 30 // seconds; implementations MAY use a stricter value
|
|
177
|
+
|
|
178
|
+
function verifyDelegationChain(chain: DT[], action: ToolCall, agent_passport: Passport):
|
|
179
|
+
1. ASSERT chain is ordered root-to-leaf (parent_delegation_id links form a valid chain)
|
|
180
|
+
2. ASSERT chain[0].parent_delegation_id == null
|
|
181
|
+
3. ASSERT chain[0].chain_root_passport_id == chain[0].delegator_passport_id
|
|
182
|
+
// root delegator IS the root principal
|
|
183
|
+
4. FOR each DT at index i in chain:
|
|
184
|
+
a. ASSERT DT.spec_version == "oap/1.0"
|
|
185
|
+
b. ASSERT now() < DT.expires_at + CLOCK_SKEW_TOLERANCE
|
|
186
|
+
→ else OAP-D-004: DELEGATION_EXPIRED
|
|
187
|
+
b2. IF DT.not_before is present:
|
|
188
|
+
ASSERT now() + CLOCK_SKEW_TOLERANCE >= DT.not_before
|
|
189
|
+
→ else OAP-D-011: DELEGATION_NOT_YET_VALID
|
|
190
|
+
c. ASSERT 0 <= DT.depth_remaining <= DT.depth_cap
|
|
191
|
+
→ else OAP-D-007: DEPTH_INCONSISTENT
|
|
192
|
+
d. RESOLVE DT.delegator_key_id → public_key
|
|
193
|
+
e. ASSERT Ed25519.verify(public_key, payload(DT), DT.delegator_signature)
|
|
194
|
+
→ else OAP-D-005: INVALID_SIGNATURE
|
|
195
|
+
f. IF i > 0:
|
|
196
|
+
ASSERT DT.parent_delegation_id == chain[i-1].delegation_id
|
|
197
|
+
→ else OAP-D-006: BROKEN_CHAIN
|
|
198
|
+
ASSERT DT.depth_cap == chain[0].depth_cap
|
|
199
|
+
// depth_cap is immutable: propagated from root
|
|
200
|
+
ASSERT DT.depth_remaining == chain[i-1].depth_remaining - 1
|
|
201
|
+
→ else OAP-D-007: DEPTH_INCONSISTENT
|
|
202
|
+
ASSERT DT.chain_root_passport_id == chain[0].chain_root_passport_id
|
|
203
|
+
→ else OAP-D-006: BROKEN_CHAIN
|
|
204
|
+
ASSERT DT.granted_capabilities ⊆ chain[i-1].granted_capabilities
|
|
205
|
+
→ else OAP-D-001: SCOPE_EXCEEDS_DELEGATOR
|
|
206
|
+
ASSERT limitsWithinParent(DT.granted_limits, chain[i-1].granted_limits)
|
|
207
|
+
→ else OAP-D-002: LIMITS_EXCEED_DELEGATOR
|
|
208
|
+
5. ASSERT action.capability ∈ chain[last].granted_capabilities
|
|
209
|
+
→ else OAP-D-008: ACTION_NOT_IN_SCOPE
|
|
210
|
+
6. FOR each DT in chain WHERE DT.revocation_endpoint is present:
|
|
211
|
+
ASSERT fetchRevocationStatus(DT, cache_ttl=60s) != "revoked"
|
|
212
|
+
→ else OAP-D-009: DELEGATION_REVOKED
|
|
213
|
+
// Note: cascade revocation is enforced here — checking ALL tokens in chain,
|
|
214
|
+
// not just the leaf. Revoking a parent revokes the sub-chain via this check.
|
|
215
|
+
7. RETURN ALLOW
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Note on cascade revocation:** Step 6 checks all tokens in the chain for revocation, not just the leaf. This ensures that revoking a parent DT (by marking it revoked at its `revocation_endpoint`) automatically blocks all sub-chain actions, even though child signatures remain cryptographically valid. This is a policy-layer mechanism, not a cryptographic one.
|
|
219
|
+
|
|
220
|
+
**Note on metadata:** `metadata` fields are unsigned and MUST NOT affect authorization decisions. Enforcement adapters MUST ignore metadata when evaluating ALLOW/DENY.
|
|
221
|
+
|
|
222
|
+
### 3.3 Limit Comparison — `limitsWithinParent(child, parent)`
|
|
223
|
+
|
|
224
|
+
The `limitsWithinParent` function MUST implement the following recursive deep-comparison algorithm:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
function limitsWithinParent(child_limits: object, parent_limits: object) -> boolean:
|
|
228
|
+
// Edge case: if child has no limits, always valid (empty grant cannot exceed parent)
|
|
229
|
+
IF child_limits == null OR keys(child_limits).length == 0:
|
|
230
|
+
RETURN true
|
|
231
|
+
// Edge case: parent has no limits but child claims some → reject
|
|
232
|
+
IF parent_limits == null OR keys(parent_limits).length == 0:
|
|
233
|
+
RETURN false
|
|
234
|
+
FOR each capability_id in keys(child_limits):
|
|
235
|
+
IF capability_id NOT IN parent_limits:
|
|
236
|
+
RETURN false // child claims a limit key the parent doesn't have → reject
|
|
237
|
+
child_cap = child_limits[capability_id]
|
|
238
|
+
parent_cap = parent_limits[capability_id]
|
|
239
|
+
IF NOT capabilityLimitsLE(child_cap, parent_cap):
|
|
240
|
+
RETURN false
|
|
241
|
+
RETURN true
|
|
242
|
+
|
|
243
|
+
function capabilityLimitsLE(child: object, parent: object) -> boolean:
|
|
244
|
+
FOR each field in keys(child):
|
|
245
|
+
child_val = child[field]
|
|
246
|
+
parent_val = parent[field] // if missing in parent, treat as unbounded
|
|
247
|
+
SWITCH typeof(child_val):
|
|
248
|
+
CASE number:
|
|
249
|
+
IF child_val > parent_val: RETURN false
|
|
250
|
+
CASE array: // e.g., reason_codes
|
|
251
|
+
IF NOT (child_val ⊆ parent_val): RETURN false
|
|
252
|
+
CASE boolean:
|
|
253
|
+
// Security-hardening flags: MUST NOT be relaxed (true → false is prohibited)
|
|
254
|
+
// Example: idempotency_required = true in parent MUST remain true in child
|
|
255
|
+
IF parent_val == true AND child_val == false: RETURN false
|
|
256
|
+
CASE object:
|
|
257
|
+
IF NOT capabilityLimitsLE(child_val, parent_val): RETURN false // recurse
|
|
258
|
+
RETURN true
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
When `parent_val` is absent for a given field, the child's value is unconstrained by that field; no rejection occurs. Implementations MAY add additional domain-specific comparison rules in extension fields prefixed with `x-`.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 4. Re-delegation
|
|
266
|
+
|
|
267
|
+
### 4.1 When Re-delegation is Permitted
|
|
268
|
+
|
|
269
|
+
An agent holding a valid DT MAY issue a new DT (a "child DT") to a sub-agent IF:
|
|
270
|
+
|
|
271
|
+
1. `depth_remaining > 0`
|
|
272
|
+
2. The child DT's `granted_capabilities` are a strict subset of the parent DT's `granted_capabilities`
|
|
273
|
+
3. The child DT's `expires_at` ≤ the parent DT's `expires_at`
|
|
274
|
+
4. The child DT's `depth_remaining` **MUST equal** `parent_dt.depth_remaining - 1` (constraint, not assignment)
|
|
275
|
+
5. The child DT's `depth_cap` **MUST equal** `parent_dt.depth_cap` (`depth_cap` is read-only once set by root)
|
|
276
|
+
6. The child DT's `chain_root_passport_id` **MUST equal** `parent_dt.chain_root_passport_id`
|
|
277
|
+
|
|
278
|
+
### 4.2 Prohibited Re-delegation
|
|
279
|
+
|
|
280
|
+
Re-delegation MUST be rejected by enforcement adapters when:
|
|
281
|
+
- `depth_remaining == 0` → `OAP-D-003: DEPTH_EXHAUSTED`
|
|
282
|
+
- Child `expires_at` > parent `expires_at` → `OAP-D-010: EXPIRY_EXCEEDS_PARENT`
|
|
283
|
+
- Child grants capabilities not in parent → `OAP-D-001: SCOPE_EXCEEDS_DELEGATOR`
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 5. Integration with Policy Packs
|
|
288
|
+
|
|
289
|
+
Policy packs that perform per-action enforcement SHOULD accept a `delegation_chain` context object alongside the standard passport context:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
interface PolicyEvalContext {
|
|
293
|
+
passport: OAPPassport;
|
|
294
|
+
action: ToolCall;
|
|
295
|
+
delegation_chain?: DelegationToken[]; // Present when agent is a delegate
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
When `delegation_chain` is present:
|
|
300
|
+
- The **effective capability set** for policy evaluation is the **intersection** of the agent's passport capabilities and the final DT's `granted_capabilities`
|
|
301
|
+
- Limits from the DT override passport limits where the DT is more restrictive
|
|
302
|
+
- The `chain_root_passport_id` SHOULD be logged as the authorizing principal in the audit trail
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 6. Audit Trail
|
|
307
|
+
|
|
308
|
+
Every DT-governed action MUST produce an audit record that includes:
|
|
309
|
+
|
|
310
|
+
| Field | Description |
|
|
311
|
+
|-------|-------------|
|
|
312
|
+
| `delegation_chain_ids` | Ordered array of `delegation_id`s from root to leaf |
|
|
313
|
+
| `chain_root_passport_id` | Root principal who ultimately authorized the action |
|
|
314
|
+
| `acting_agent_id` | Agent that executed the action |
|
|
315
|
+
| `delegation_depth` | Number of DTs in the chain |
|
|
316
|
+
| `effective_capability` | The capability evaluated (from final DT's granted scope) |
|
|
317
|
+
| `decision` | `ALLOW` or `DENY` with reason codes |
|
|
318
|
+
|
|
319
|
+
This audit trail enables forensic reconstruction: given any logged action, a reviewer can identify the root principal and every agent in the delegation chain.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 7. Revocation
|
|
324
|
+
|
|
325
|
+
### 7.1 Revocation Modes
|
|
326
|
+
|
|
327
|
+
| Mode | Description | Latency |
|
|
328
|
+
|------|-------------|---------|
|
|
329
|
+
| **Immediate** | DT marked revoked at `revocation_endpoint`; all adapters must check before each action | ~0 |
|
|
330
|
+
| **Expiry-based** | No explicit revocation; DT becomes invalid at `expires_at` | Up to TTL |
|
|
331
|
+
| **Cascade** | Revoking a parent DT implicitly revokes all child DTs in the chain | Depends on mode |
|
|
332
|
+
|
|
333
|
+
### 7.2 Revocation Endpoint Protocol
|
|
334
|
+
|
|
335
|
+
If `revocation_endpoint` is present, a GET request MUST return:
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"delegation_id": "7f3c8a1b-1e2d-4b5a-9c0e-123456789abc",
|
|
340
|
+
"status": "active" | "revoked" | "expired",
|
|
341
|
+
"revoked_at": "2026-03-15T04:12:00Z", // present if status == "revoked"
|
|
342
|
+
"revocation_reason": "task_complete" // optional
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Enforcement adapters MAY cache revocation responses for up to 60 seconds.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 8. Error Codes
|
|
351
|
+
|
|
352
|
+
| Code | Name | Description |
|
|
353
|
+
|------|------|-------------|
|
|
354
|
+
| `OAP-D-001` | `SCOPE_EXCEEDS_DELEGATOR` | DT grants capabilities not held by delegator; the set `granted_capabilities ⊄ delegator.effective_capabilities` |
|
|
355
|
+
| `OAP-D-002` | `LIMITS_EXCEED_DELEGATOR` | DT grants limits that are less restrictive than the delegator's own limits (numeric value is higher, array set is larger, or a security-hardening boolean is relaxed) |
|
|
356
|
+
| `OAP-D-003` | `DEPTH_EXHAUSTED` | Re-delegation attempted with `depth_remaining = 0` |
|
|
357
|
+
| `OAP-D-004` | `DELEGATION_EXPIRED` | DT `expires_at` is in the past (accounting for clock skew tolerance) |
|
|
358
|
+
| `OAP-D-005` | `INVALID_SIGNATURE` | Ed25519 signature verification failed |
|
|
359
|
+
| `OAP-D-006` | `BROKEN_CHAIN` | `parent_delegation_id` does not match expected parent, or `chain_root_passport_id` is inconsistent across the chain |
|
|
360
|
+
| `OAP-D-007` | `DEPTH_INCONSISTENT` | `depth_remaining` does not equal `parent.depth_remaining - 1`, or `depth_remaining` is outside `[0, depth_cap]` |
|
|
361
|
+
| `OAP-D-008` | `ACTION_NOT_IN_SCOPE` | Action capability not found in final DT's granted scope |
|
|
362
|
+
| `OAP-D-009` | `DELEGATION_REVOKED` | DT has been explicitly revoked at its `revocation_endpoint` |
|
|
363
|
+
| `OAP-D-010` | `EXPIRY_EXCEEDS_PARENT` | Child DT `expires_at` is later than parent DT `expires_at` |
|
|
364
|
+
| `OAP-D-011` | `DELEGATION_NOT_YET_VALID` | Current time is before DT's `not_before` timestamp (accounting for clock skew tolerance) |
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 9. Relationship to aeoess Agent Passport System
|
|
369
|
+
|
|
370
|
+
The OAP delegation model was informed by the aeoess Agent Passport System (aport-spec issue #21), which demonstrated depth-limit and scope-narrowing delegation chains with Ed25519 identity. Key alignments and divergences:
|
|
371
|
+
|
|
372
|
+
| Property | OAP Delegation | aeoess APS |
|
|
373
|
+
|----------|----------------|------------|
|
|
374
|
+
| Signature algorithm | Ed25519 | Ed25519 |
|
|
375
|
+
| Scope narrowing | ✅ Required (MUST) | ✅ Required |
|
|
376
|
+
| Depth limits | ✅ `depth_cap` + `depth_remaining` | ✅ depth-limit |
|
|
377
|
+
| Policy packs | ✅ 15+ named packs | ❌ Not specified |
|
|
378
|
+
| Framework adapters | ✅ 4 (Claude Code, Cursor, Express, FastAPI) | In development |
|
|
379
|
+
| Audit trail | ✅ Mandatory, chain-root attributed | Partial |
|
|
380
|
+
| Revocation | ✅ Endpoint-based + expiry | Expiry-based |
|
|
381
|
+
| Production adapters | ✅ v1.0.15+ | In development |
|
|
382
|
+
|
|
383
|
+
OAP delegation is designed to be cross-compatible with APS identity in a delegation chain — a passport issued by an APS-compatible system MAY appear as a `delegator_passport_id` if the signing key format is compatible and the chain verification algorithm can resolve the `kid`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 10. Security Considerations
|
|
388
|
+
|
|
389
|
+
### 10.1 Short-lived Tokens
|
|
390
|
+
Delegation tokens SHOULD have `expires_at` set to the minimum necessary duration. Recommended maximums by use case:
|
|
391
|
+
|
|
392
|
+
| Use case | Max TTL |
|
|
393
|
+
|----------|---------|
|
|
394
|
+
| Single batch job | 2 hours |
|
|
395
|
+
| Scheduled daily task | 24 hours |
|
|
396
|
+
| Long-running background agent | 7 days (requires L3+ assurance) |
|
|
397
|
+
|
|
398
|
+
### 10.2 Minimum Scope Principle
|
|
399
|
+
Delegators SHOULD grant only the capabilities the sub-agent strictly requires for the specified `purpose`. Capability enumeration in `granted_capabilities` MUST be explicit; wildcard grants are not supported.
|
|
400
|
+
|
|
401
|
+
### 10.3 Key Compromise
|
|
402
|
+
If a delegator's signing key is compromised, all DTs signed by that key MUST be revoked. Since child DTs depend on the parent chain's signature validity, a compromised delegator key invalidates the entire sub-chain below it.
|
|
403
|
+
|
|
404
|
+
### 10.4 Depth Cap Selection
|
|
405
|
+
- `depth_cap = 1`: Permits direct sub-agent delegation only. Recommended for financial capabilities.
|
|
406
|
+
- `depth_cap = 3`: Permits 3-level chains (orchestrator → worker → tool-calling sub-agent). Recommended default.
|
|
407
|
+
- `depth_cap > 5`: SHOULD require L4 assurance for root passport; high-depth chains are difficult to audit.
|
|
408
|
+
|
|
409
|
+
### 10.5 Delegation vs. Instance Passports
|
|
410
|
+
For long-running sub-agents with stable, predictable capabilities, prefer issuing an **OAP passport instance** (via the registry) rather than a delegation token. Delegation chains are optimized for ephemeral, task-scoped authority grants. Persistent sub-agents with a fixed role SHOULD hold their own passport.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## 11. Conformance
|
|
415
|
+
|
|
416
|
+
A system claiming OAP Delegation conformance MUST:
|
|
417
|
+
|
|
418
|
+
1. Implement the delegation token schema (Section 2) fully
|
|
419
|
+
2. Enforce scope narrowing on creation (Section 1.3) — reject violating tokens at issuance time
|
|
420
|
+
3. Enforce scope narrowing on verification (Section 3.2 step 3f) — reject violating tokens at enforcement time
|
|
421
|
+
4. Enforce depth limits (Sections 1.4, 4)
|
|
422
|
+
5. Verify Ed25519 signatures before acting on any DT (Section 3.2 step 3e)
|
|
423
|
+
6. Reject expired tokens (Section 3.2 step 3b)
|
|
424
|
+
7. Produce audit records per Section 6 for every DT-governed action
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Appendix A: Delegation Token JSON Schema
|
|
429
|
+
|
|
430
|
+
The normative JSON Schema for the delegation token object is published at:
|
|
431
|
+
|
|
432
|
+
```
|
|
433
|
+
https://github.com/aporthq/aport-spec/oap/delegation-schema.json
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
*(Schema file to be added in subsequent PR — tracked in aport-spec issue.)*
|
|
437
|
+
|
|
438
|
+
---
|
|
439
|
+
|
|
440
|
+
## Appendix B: Example — Three-Level Finance Delegation Chain
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
Root: Org passport (L4FIN, finance.payment.refund up to $50,000/day)
|
|
444
|
+
DT-1 → OrchestratorAgent (depth_cap=3, depth_remaining=2, max_per_tx=$5000, daily_cap=$25000, expires in 4h)
|
|
445
|
+
DT-2 → WorkerAgent (depth_remaining=1, max_per_tx=$1000, daily_cap=$5000, expires in 2h)
|
|
446
|
+
DT-3 → ToolAgent (depth_remaining=0, max_per_tx=$250, daily_cap=$1000, expires in 30min)
|
|
447
|
+
CANNOT re-delegate. Can execute finance.payment.refund ≤ $250/tx, $1000/day, for next 30min.
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Each level narrows scope. ToolAgent cannot exceed its own grant even if it tries to claim the root passport's permissions. The root's `chain_root_passport_id` is propagated to every audit record, making the authorizing principal traceable.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
*Specification authored by EngineerBot (LiftRails Inc.) · March 15, 2026*
|
|
455
|
+
*Informed by: aeoess Agent Passport System (aport-spec issue #21), OAP core spec v1.0, production adapter requirements*
|
|
@@ -20,6 +20,7 @@ This document is a working draft of the Open Agent Passport specification v1.0.
|
|
|
20
20
|
8. [Versioning](#versioning)
|
|
21
21
|
9. [Security](#security)
|
|
22
22
|
10. [Conformance](#conformance)
|
|
23
|
+
11. [Delegation Chains](./delegation.md) — multi-agent delegation specification
|
|
23
24
|
|
|
24
25
|
## Introduction
|
|
25
26
|
|