@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 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—Option 2 in the [APort × OpenClaw integration proposal](https://github.com/aporthq/agent-passport/tree/main/_plan/execution/openclaw).
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 (signed receipts, global kill switch, team sync). See [APort × OpenClaw proposal](https://github.com/aporthq/agent-passport/tree/main/_plan/execution/openclaw) for free vs. paid tiers.
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>
@@ -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.25 (see [CHANGELOG.md](../CHANGELOG.md)).
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`.
@@ -1,5 +1,7 @@
1
1
  # Changelog - APort OpenClaw Plugin
2
2
 
3
+ ## 1.0.26
4
+
3
5
  ## 1.0.25
4
6
 
5
7
  ## 1.0.24
@@ -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.25",
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.25",
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.25",
9
+ "version": "1.0.26",
10
10
  "license": "Apache-2.0",
11
11
  "devDependencies": {
12
12
  "@types/node": "^18.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aporthq/openclaw-aport",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "OpenClaw plugin for deterministic pre-action authorization via APort guardrails",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aporthq/aport-agent-guardrails",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
4
4
  "description": "Policy enforcement guardrails for OpenClaw-compatible agent frameworks",
5
5
  "workspaces": [
6
6
  "packages/*",