@aporthq/aport-agent-guardrails 1.0.21 → 1.0.22

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.
@@ -0,0 +1,303 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, extname, join } from "node:path";
4
+ import { canonicalize } from "./decision.js";
5
+
6
+ const REQUIRED_CAPABILITIES = {
7
+ "system.command.execute.v1": ["system.command.execute"],
8
+ "code.repository.merge.v1": ["repo.pr.create", "repo.merge"],
9
+ "messaging.message.send.v1": ["messaging.send"],
10
+ "data.file.read.v1": ["data.file.read"],
11
+ "data.file.write.v1": ["data.file.write"],
12
+ "web.fetch.v1": ["web.fetch"],
13
+ "web.browser.v1": ["web.browser"],
14
+ "mcp.tool.execute.v1": ["mcp.tool.execute"],
15
+ "agent.session.create.v1": ["agent.session.create"],
16
+ "agent.tool.register.v1": ["agent.tool.register"],
17
+ "finance.payment.refund.v1": ["finance.payment.refund"],
18
+ "finance.payment.charge.v1": ["payments.charge"],
19
+ "data.export.create.v1": ["data.export"],
20
+ };
21
+
22
+ function validateCommandString(command) {
23
+ if (command.includes("`")) return false;
24
+ if (/\$\([^)]*\b(rm|dd|mkfs|curl|wget|chmod|chown|sudo|kill|nc|netcat)\b/i.test(command)) {
25
+ return false;
26
+ }
27
+ if (/[\u0000-\u0008\u000e-\u001f]/.test(command)) return false;
28
+ return true;
29
+ }
30
+
31
+ function safePatternMatch(string, pattern) {
32
+ if (!pattern) return false;
33
+ if (pattern.includes(" ")) return String(string).toLowerCase().includes(String(pattern).toLowerCase());
34
+ const escaped = String(pattern).replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
35
+ return new RegExp(`(^|[^\\w])${escaped}([^\\w]|$)`, "i").test(String(string));
36
+ }
37
+
38
+ function safePrefixMatch(string, prefix) {
39
+ if (prefix === "*") return true;
40
+ return String(string).startsWith(String(prefix));
41
+ }
42
+
43
+ function getCapabilityIds(passport) {
44
+ const capabilities = Array.isArray(passport.capabilities) ? passport.capabilities : [];
45
+ return capabilities
46
+ .map((entry) => {
47
+ if (typeof entry === "string") return entry;
48
+ if (entry && typeof entry === "object" && typeof entry.id === "string") return entry.id;
49
+ return null;
50
+ })
51
+ .filter(Boolean);
52
+ }
53
+
54
+ function hasRequiredCapabilities(passport, policyId) {
55
+ const required = REQUIRED_CAPABILITIES[policyId] || [];
56
+ if (required.length === 0) return true;
57
+ const granted = new Set(getCapabilityIds(passport));
58
+ return required.every((capability) => {
59
+ if (granted.has(capability)) return true;
60
+ if (capability === "messaging.send" && granted.has("messaging.message.send")) return true;
61
+ return false;
62
+ });
63
+ }
64
+
65
+ function getLimits(passport, policyId) {
66
+ const limits = passport && typeof passport.limits === "object" ? passport.limits : {};
67
+ const policyBase = policyId.replace(/\.v\d+$/, "");
68
+ if (policyId === "messaging.message.send.v1") {
69
+ return limits["messaging.message.send"] || {
70
+ msgs_per_min: limits.msgs_per_min,
71
+ msgs_per_day: limits.msgs_per_day,
72
+ allowed_recipients: limits.allowed_recipients,
73
+ approval_required: limits.approval_required,
74
+ };
75
+ }
76
+ return limits[policyBase] || {};
77
+ }
78
+
79
+ function computePassportDigest(passportRaw) {
80
+ return `sha256:${createHash("sha256").update(canonicalize(JSON.parse(passportRaw)), "utf8").digest("hex")}`;
81
+ }
82
+
83
+ function buildDecision({ allow, policyId, passport, passportRaw, code, message, dataDir }) {
84
+ const now = new Date();
85
+ const issuedAt = now.toISOString();
86
+ const expiresAt = new Date(now.getTime() + 60 * 60 * 1000).toISOString();
87
+ const decisionId = randomUUID();
88
+ const passportId = passport.passport_id || passport.agent_id || "unknown";
89
+ const agentId = passport.agent_id || passport.passport_id || "unknown";
90
+ const ownerId = passport.owner_id || "unknown";
91
+ const reasons = allow
92
+ ? [{ code: "oap.allowed", message: "All policy checks passed" }]
93
+ : [{ code, message }];
94
+
95
+ const decisionsDir = join(dataDir, "decisions");
96
+ const chainStatePath = join(decisionsDir, ".chain-state.json");
97
+ let prevDecisionId = null;
98
+ let prevContentHash = null;
99
+ try {
100
+ if (existsSync(chainStatePath)) {
101
+ const chainState = JSON.parse(readFileSync(chainStatePath, "utf8"));
102
+ prevDecisionId = chainState.last_decision_id || null;
103
+ prevContentHash = chainState.last_content_hash || null;
104
+ }
105
+ } catch {
106
+ prevDecisionId = null;
107
+ prevContentHash = null;
108
+ }
109
+
110
+ const baseDecision = {
111
+ decision_id: decisionId,
112
+ policy_id: policyId,
113
+ passport_id: passportId,
114
+ agent_id: agentId,
115
+ owner_id: ownerId,
116
+ assurance_level: passport.assurance_level || "L0",
117
+ allow,
118
+ reasons,
119
+ issued_at: issuedAt,
120
+ created_at: issuedAt,
121
+ expires_at: expiresAt,
122
+ expires_in: 3600,
123
+ passport_digest: computePassportDigest(passportRaw),
124
+ signature: "local-unsigned",
125
+ kid: "oap:local:dev-key",
126
+ verification_mode: "local",
127
+ prev_decision_id: prevDecisionId,
128
+ prev_content_hash: prevContentHash,
129
+ };
130
+ const contentHash = `sha256:${createHash("sha256").update(canonicalize(baseDecision), "utf8").digest("hex")}`;
131
+ const decision = { ...baseDecision, content_hash: contentHash };
132
+
133
+ try {
134
+ mkdirSync(decisionsDir, { recursive: true });
135
+ writeFileSync(
136
+ chainStatePath,
137
+ JSON.stringify({
138
+ last_decision_id: decisionId,
139
+ last_content_hash: contentHash,
140
+ }),
141
+ "utf8",
142
+ );
143
+ } catch {
144
+ // Best-effort only.
145
+ }
146
+
147
+ return decision;
148
+ }
149
+
150
+ function allowByList(value, list, matcher) {
151
+ if (!Array.isArray(list) || list.length === 0) return true;
152
+ return list.some((entry) => matcher(value, entry));
153
+ }
154
+
155
+ function makeDeny(baseParams, code, message) {
156
+ return buildDecision({ allow: false, code, message, ...baseParams });
157
+ }
158
+
159
+ function makeAllow(baseParams) {
160
+ return buildDecision({
161
+ allow: true,
162
+ code: "oap.allowed",
163
+ message: "All policy checks passed",
164
+ ...baseParams,
165
+ });
166
+ }
167
+
168
+ export function evaluateLocalDecision({ policyName, context, passportFile }) {
169
+ const dataDir = dirname(passportFile || ".");
170
+ const baseParams = {
171
+ policyId: policyName || "unknown",
172
+ passport: {},
173
+ passportRaw: "{}",
174
+ dataDir,
175
+ };
176
+
177
+ if (!passportFile || !existsSync(passportFile)) {
178
+ return makeDeny(baseParams, "oap.passport_not_found", `Passport file not found at ${passportFile}`);
179
+ }
180
+
181
+ let passportRaw = "{}";
182
+ let passport = {};
183
+ try {
184
+ passportRaw = readFileSync(passportFile, "utf8");
185
+ passport = JSON.parse(passportRaw);
186
+ } catch {
187
+ return makeDeny(baseParams, "oap.passport_invalid", "Passport file contains invalid JSON");
188
+ }
189
+
190
+ const params = { ...baseParams, passport, passportRaw };
191
+
192
+ if (passport.status !== "active") {
193
+ return makeDeny(params, "oap.passport_suspended", `Passport status is '${passport.status}', not 'active'. Agent suspended.`);
194
+ }
195
+
196
+ if (passport.spec_version !== "oap/1.0") {
197
+ return makeDeny(params, "oap.passport_version_mismatch", `Passport spec version is '${passport.spec_version}', expected 'oap/1.0'`);
198
+ }
199
+
200
+ if (!hasRequiredCapabilities(passport, policyName)) {
201
+ return makeDeny(params, "oap.unknown_capability", `Passport does not have required capability for policy '${policyName}'`);
202
+ }
203
+
204
+ const limits = getLimits(passport, policyName);
205
+
206
+ if (policyName === "code.repository.merge.v1") {
207
+ const filesChanged = Array.isArray(context.files_changed)
208
+ ? context.files_changed.length
209
+ : Number(context.files_changed ?? context.files ?? 0);
210
+ const maxFiles = Number(limits.max_pr_size_kb ?? 500);
211
+ if (Number.isFinite(filesChanged) && filesChanged > maxFiles) {
212
+ return makeDeny(params, "oap.limit_exceeded", `PR size ${filesChanged} exceeds limit of ${maxFiles} files`);
213
+ }
214
+
215
+ const repo = String(context.repo ?? context.repository ?? "");
216
+ if (!allowByList(repo, limits.allowed_repos, (value, pattern) => value === pattern || value.endsWith(`/${pattern}`) || pattern === "*")) {
217
+ return makeDeny(params, "oap.repo_not_allowed", `Repository '${repo}' is not in allowed list`);
218
+ }
219
+
220
+ const branch = String(context.branch ?? "");
221
+ if (!allowByList(branch, limits.allowed_base_branches, (value, pattern) => value === pattern || pattern === "*")) {
222
+ return makeDeny(params, "oap.branch_not_allowed", `Branch '${branch}' is not in allowed list`);
223
+ }
224
+ }
225
+
226
+ if (policyName === "system.command.execute.v1") {
227
+ const command = String(context.command ?? context.cmd ?? "");
228
+ if (command && !validateCommandString(command)) {
229
+ return makeDeny(params, "oap.command_injection_detected", "Command contains potentially dangerous characters");
230
+ }
231
+
232
+ const allowedCommands = Array.isArray(limits.allowed_commands) ? limits.allowed_commands : [];
233
+ if (!allowByList(command, allowedCommands, safePrefixMatch)) {
234
+ return makeDeny(params, "oap.command_not_allowed", `Command '${command}' is not in allowed list`);
235
+ }
236
+
237
+ if (/rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f[^[:space:]]*[[:space:]]+\/[[:space:]]*$/i.test(command) ||
238
+ /rm[[:space:]]+-[^[:space:]]*r[^[:space:]]*f[^[:space:]]+\/\*/i.test(command)) {
239
+ return makeDeny(params, "oap.dangerous_operation", "Destructive file operation: rm -rf / or rm -rf /*");
240
+ }
241
+ if (/dd[[:space:]]+if=\/dev\//i.test(command)) {
242
+ return makeDeny(params, "oap.dangerous_operation", "Dangerous disk operation: dd if=/dev/");
243
+ }
244
+ if (/mkfs\./i.test(command)) {
245
+ return makeDeny(params, "oap.dangerous_operation", "Filesystem creation: mkfs");
246
+ }
247
+ if (/(curl|wget)[[:space:]][^|]*\|[[:space:]]*(bash|sh|zsh|python|node)/i.test(command)) {
248
+ return makeDeny(params, "oap.dangerous_operation", "Download-and-execute pattern detected");
249
+ }
250
+ if (/:\(\)[[:space:]]*\{[[:space:]]*:[[:space:]]*\|[[:space:]]*:[[:space:]]*&[[:space:]]*\}/.test(command) || /fork\(\)/i.test(command)) {
251
+ return makeDeny(params, "oap.dangerous_operation", "Fork bomb detected");
252
+ }
253
+
254
+ const blockedPatterns = Array.isArray(limits.blocked_patterns) ? limits.blocked_patterns : [];
255
+ const blockedPattern = blockedPatterns.find((pattern) => safePatternMatch(command, pattern));
256
+ if (blockedPattern) {
257
+ return makeDeny(params, "oap.blocked_pattern", `Command contains blocked pattern: ${blockedPattern}`);
258
+ }
259
+ }
260
+
261
+ if (policyName === "messaging.message.send.v1") {
262
+ const recipient = String(context.recipient ?? context.to ?? "");
263
+ if (!allowByList(recipient, limits.allowed_recipients, (value, pattern) => value === pattern || pattern === "*")) {
264
+ return makeDeny(params, "oap.recipient_not_allowed", `Recipient '${recipient}' is not in allowed list`);
265
+ }
266
+ }
267
+
268
+ if (policyName === "data.file.read.v1") {
269
+ const filePath = String(context.file_path ?? context.path ?? "");
270
+ if (!allowByList(filePath, limits.allowed_paths, (value, pattern) => value.startsWith(pattern) || pattern === "*")) {
271
+ return makeDeny(params, "oap.path_not_allowed", `File path '${filePath}' is not in allowed list`);
272
+ }
273
+
274
+ const blockedPattern = (Array.isArray(limits.blocked_patterns) ? limits.blocked_patterns : [])
275
+ .find((pattern) => filePath.includes(pattern) || filePath === pattern);
276
+ if (blockedPattern) {
277
+ return makeDeny(params, "oap.blocked_pattern", `File path matches blocked pattern: ${blockedPattern}`);
278
+ }
279
+ }
280
+
281
+ if (policyName === "data.file.write.v1") {
282
+ const filePath = String(context.file_path ?? context.path ?? "");
283
+ if (!allowByList(filePath, limits.allowed_paths, (value, pattern) => value.startsWith(pattern) || pattern === "*")) {
284
+ return makeDeny(params, "oap.path_not_allowed", `File path '${filePath}' is not in allowed list`);
285
+ }
286
+
287
+ const blockedPath = (Array.isArray(limits.blocked_paths) ? limits.blocked_paths : [])
288
+ .find((pattern) => filePath.startsWith(pattern));
289
+ if (blockedPath) {
290
+ return makeDeny(params, "oap.path_blocked", `Writing to system directory is not allowed: ${blockedPath}`);
291
+ }
292
+
293
+ const allowedExtensions = Array.isArray(limits.allowed_extensions) ? limits.allowed_extensions : [];
294
+ if (allowedExtensions.length > 0) {
295
+ const fileExt = extname(filePath).toLowerCase();
296
+ if (fileExt && !allowedExtensions.includes(fileExt)) {
297
+ return makeDeny(params, "oap.extension_not_allowed", `File extension ${fileExt} is not allowed`);
298
+ }
299
+ }
300
+ }
301
+
302
+ return makeAllow(params);
303
+ }
@@ -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.21",
5
+ "version": "1.0.22",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
@@ -14,7 +14,7 @@
14
14
  "api"
15
15
  ],
16
16
  "default": "local",
17
- "description": "local = guardrail script, api = APort API"
17
+ "description": "local = built-in JS evaluator, api = APort API"
18
18
  },
19
19
  "passportFile": {
20
20
  "type": "string",
@@ -24,7 +24,7 @@
24
24
  "guardrailScript": {
25
25
  "type": "string",
26
26
  "default": "~/.openclaw/.skills/aport-guardrail-bash.sh",
27
- "description": "Path to guardrail script (local mode)"
27
+ "description": "Legacy compatibility field for manual smoke tests and shell tooling. Current plugin versions use the built-in JS local evaluator in local mode."
28
28
  },
29
29
  "apiUrl": {
30
30
  "type": "string",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "apiKey": {
34
34
  "type": "string",
35
- "description": "Optional. Prefer APORT_API_KEY env var; do not put ${APORT_API_KEY} in config (OpenClaw requires the var to exist)."
35
+ "description": "Optional API key for hosted verification mode. Configure it directly in plugin config if needed."
36
36
  },
37
37
  "failClosed": {
38
38
  "type": "boolean",
@@ -51,7 +51,7 @@
51
51
  "alwaysVerifyEachToolCall": {
52
52
  "type": "boolean",
53
53
  "default": true,
54
- "description": "Run fresh APort verify for each tool call"
54
+ "description": "Deprecated compatibility field. Current plugin versions always verify each tool call and ignore this setting."
55
55
  },
56
56
  "mapExecToPolicy": {
57
57
  "type": "boolean",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@aporthq/openclaw-aport",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@aporthq/openclaw-aport",
9
- "version": "1.0.21",
9
+ "version": "1.0.22",
10
10
  "license": "Apache-2.0",
11
11
  "devDependencies": {
12
12
  "@types/node": "^18.0.0",
@@ -1,11 +1,23 @@
1
1
  {
2
2
  "name": "@aporthq/openclaw-aport",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "OpenClaw plugin for deterministic pre-action authorization via APort guardrails",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
+ "files": [
8
+ "index.js",
9
+ "api-client.js",
10
+ "audit.js",
11
+ "decision.js",
12
+ "local-evaluator.js",
13
+ "tool-mapping.js",
14
+ "openclaw.plugin.json",
15
+ "README.md",
16
+ "MIGRATION.md",
17
+ "CHANGELOG.md"
18
+ ],
7
19
  "scripts": {
8
- "test": "node test.js"
20
+ "test": "node ../../tests/extensions/openclaw-aport.test.js"
9
21
  },
10
22
  "keywords": [
11
23
  "openclaw",
@@ -23,19 +35,28 @@
23
35
  "directory": "extensions/openclaw-aport"
24
36
  },
25
37
  "peerDependencies": {
26
- "openclaw": ">=2026.3.0"
38
+ "openclaw": ">=2026.4.11"
27
39
  },
28
40
  "engines": {
29
41
  "node": ">=22.0.0"
30
42
  },
31
43
  "openclaw": {
32
44
  "extensions": [
33
- "./index.ts"
34
- ]
45
+ "./index.js"
46
+ ],
47
+ "install": {
48
+ "minHostVersion": ">=2026.4.11"
49
+ },
50
+ "compat": {
51
+ "pluginApi": ">=2026.4.11"
52
+ },
53
+ "build": {
54
+ "openclawVersion": "2026.4.11"
55
+ }
35
56
  },
36
57
  "devDependencies": {
37
58
  "@types/node": "^22.0.0",
38
- "openclaw": ">=2026.3.0",
59
+ "openclaw": "2026.4.11",
39
60
  "typescript": "^5.0.0"
40
61
  }
41
62
  }
@@ -0,0 +1,89 @@
1
+ export function mapToolToPolicy(toolName) {
2
+ const tool = String(toolName ?? "").toLowerCase();
3
+
4
+ if (tool.match(/git\.(create_pr|merge|push|commit)/)) return "code.repository.merge.v1";
5
+ if (tool.startsWith("git.")) return "code.repository.merge.v1";
6
+
7
+ if (tool === "exec") return "system.command.execute.v1";
8
+ if (tool.match(/exec\.(run|shell)/)) return "system.command.execute.v1";
9
+ if (tool.startsWith("exec.")) return "system.command.execute.v1";
10
+ if (tool.startsWith("system.command.")) return "system.command.execute.v1";
11
+ if (tool === "bash" || tool === "shell" || tool === "command") return "system.command.execute.v1";
12
+
13
+ if (tool.startsWith("message.")) return "messaging.message.send.v1";
14
+ if (tool.startsWith("messaging.")) return "messaging.message.send.v1";
15
+ if (tool.match(/sms|whatsapp|slack|email/)) return "messaging.message.send.v1";
16
+
17
+ if (tool === "read") return "data.file.read.v1";
18
+ if (tool.startsWith("file.read")) return "data.file.read.v1";
19
+ if (tool.startsWith("data.file.read")) return "data.file.read.v1";
20
+ if (tool === "write" || tool === "edit") return "data.file.write.v1";
21
+ if (tool === "multiedit" || tool === "notebookedit") return "data.file.write.v1";
22
+ if (tool === "glob" || tool === "ls" || tool === "grep" || tool === "toolsearch") {
23
+ return "data.file.read.v1";
24
+ }
25
+ if (tool === "todoread") return "data.file.read.v1";
26
+ if (tool === "todowrite") return "data.file.write.v1";
27
+ if (tool === "task" || tool === "taskcreate" || tool === "taskupdate" || tool === "taskstop") {
28
+ return "agent.session.create.v1";
29
+ }
30
+ if (tool === "taskget" || tool === "tasklist" || tool === "taskoutput") return "data.file.read.v1";
31
+ if (tool === "agent" || tool === "skill" || tool === "enterworktree") return "agent.session.create.v1";
32
+ if (tool === "askuserquestion" || tool === "enterplanmode" || tool === "exitplanmode") return null;
33
+ if (tool === "croncreate" || tool === "crondelete") return "agent.session.create.v1";
34
+ if (tool === "cronlist") return "data.file.read.v1";
35
+ if (tool.startsWith("file.write")) return "data.file.write.v1";
36
+ if (tool.startsWith("file.edit")) return "data.file.write.v1";
37
+ if (tool.startsWith("data.file.write")) return "data.file.write.v1";
38
+
39
+ if (tool === "web_fetch" || tool === "webfetch") return "web.fetch.v1";
40
+ if (tool === "web_search" || tool === "websearch") return "web.fetch.v1";
41
+ if (tool.startsWith("web.fetch")) return "web.fetch.v1";
42
+ if (tool.startsWith("web.search")) return "web.fetch.v1";
43
+ if (tool === "browser") return "web.browser.v1";
44
+ if (tool.startsWith("web.browser")) return "web.browser.v1";
45
+ if (tool.startsWith("browser.")) return "web.browser.v1";
46
+
47
+ if (tool.startsWith("mcp.")) return "mcp.tool.execute.v1";
48
+ if (tool.startsWith("mcp__")) return "mcp.tool.execute.v1";
49
+
50
+ if (tool.match(/agent\.session|session\.create/)) return "agent.session.create.v1";
51
+ if (tool === "sessions_spawn" || tool === "sessions_send") return "agent.session.create.v1";
52
+ if (tool.startsWith("session.") || tool.startsWith("sessions.")) return "agent.session.create.v1";
53
+ if (tool === "cron" || tool.startsWith("cron.")) return "agent.session.create.v1";
54
+
55
+ if (tool === "gateway" || tool.startsWith("gateway.")) return "system.command.execute.v1";
56
+ if (tool === "process" || tool.startsWith("process.")) return "system.command.execute.v1";
57
+
58
+ if (tool.match(/agent\.tool|tool\.register/)) return "agent.tool.register.v1";
59
+
60
+ if (tool.match(/payment\.refund|refund/)) return "finance.payment.refund.v1";
61
+ if (tool.match(/payment\.charge|charge/)) return "finance.payment.charge.v1";
62
+ if (tool.startsWith("finance.")) return "finance.payment.refund.v1";
63
+
64
+ if (tool.match(/database\.(write|insert|update|delete)/)) return "data.export.create.v1";
65
+ if (tool.match(/data\.export|export/)) return "data.export.create.v1";
66
+
67
+ return null;
68
+ }
69
+
70
+ export function normalizeExecContext(params, event) {
71
+ const src = event && typeof event === "object" ? { ...event, ...params } : params || {};
72
+ if (!src || typeof src !== "object") return { command: "" };
73
+
74
+ const raw =
75
+ src.command ??
76
+ src.cmd ??
77
+ (src.arguments && typeof src.arguments === "object" ? src.arguments.command : null) ??
78
+ (src.input && typeof src.input === "object" ? src.input.command : null) ??
79
+ (typeof src.input === "string" && src.input.trim().length > 0 ? src.input : null) ??
80
+ (src.args && typeof src.args === "object" ? src.args.command : null) ??
81
+ (src.invocation && typeof src.invocation === "object" ? src.invocation.command : null) ??
82
+ (src.payload && typeof src.payload === "object" ? src.payload.command : null) ??
83
+ (Array.isArray(src.args) && src.args.length > 0 ? src.args.join(" ") : src.args?.[0]);
84
+
85
+ const full = typeof raw === "string" ? raw : raw != null ? String(raw) : "";
86
+ const out = { ...(params || {}), command: full, full_command: full };
87
+ if (params && params.workdir !== undefined && out.cwd === undefined) out.cwd = params.workdir;
88
+ return out;
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aporthq/aport-agent-guardrails",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "Policy enforcement guardrails for OpenClaw-compatible agent frameworks",
5
5
  "workspaces": [
6
6
  "packages/*",