@vess-id/vess 0.2.0-alpha.1

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.
Files changed (280) hide show
  1. package/LICENSE +64 -0
  2. package/README.md +223 -0
  3. package/bin/vess.js +2 -0
  4. package/dist/__mocks__/@napi-rs/keyring.d.ts +9 -0
  5. package/dist/__mocks__/@napi-rs/keyring.d.ts.map +1 -0
  6. package/dist/__mocks__/@napi-rs/keyring.js +33 -0
  7. package/dist/__mocks__/@napi-rs/keyring.js.map +1 -0
  8. package/dist/__mocks__/node-mac-auth.d.ts +8 -0
  9. package/dist/__mocks__/node-mac-auth.d.ts.map +1 -0
  10. package/dist/__mocks__/node-mac-auth.js +29 -0
  11. package/dist/__mocks__/node-mac-auth.js.map +1 -0
  12. package/dist/adapter/mcp/http-transport.d.ts +34 -0
  13. package/dist/adapter/mcp/http-transport.d.ts.map +1 -0
  14. package/dist/adapter/mcp/http-transport.js +158 -0
  15. package/dist/adapter/mcp/http-transport.js.map +1 -0
  16. package/dist/adapter/mcp/mcp-adapter.d.ts +37 -0
  17. package/dist/adapter/mcp/mcp-adapter.d.ts.map +1 -0
  18. package/dist/adapter/mcp/mcp-adapter.js +48 -0
  19. package/dist/adapter/mcp/mcp-adapter.js.map +1 -0
  20. package/dist/adapter/mcp/mcp-server.factory.d.ts +35 -0
  21. package/dist/adapter/mcp/mcp-server.factory.d.ts.map +1 -0
  22. package/dist/adapter/mcp/mcp-server.factory.js +114 -0
  23. package/dist/adapter/mcp/mcp-server.factory.js.map +1 -0
  24. package/dist/adapter/mcp/stdio-transport.d.ts +7 -0
  25. package/dist/adapter/mcp/stdio-transport.d.ts.map +1 -0
  26. package/dist/adapter/mcp/stdio-transport.js +13 -0
  27. package/dist/adapter/mcp/stdio-transport.js.map +1 -0
  28. package/dist/adapter/mcp/transport.d.ts +10 -0
  29. package/dist/adapter/mcp/transport.d.ts.map +1 -0
  30. package/dist/adapter/mcp/transport.js +14 -0
  31. package/dist/adapter/mcp/transport.js.map +1 -0
  32. package/dist/approval/approval-token.d.ts +23 -0
  33. package/dist/approval/approval-token.d.ts.map +1 -0
  34. package/dist/approval/approval-token.js +81 -0
  35. package/dist/approval/approval-token.js.map +1 -0
  36. package/dist/audit/audit-dto-mapper.d.ts +29 -0
  37. package/dist/audit/audit-dto-mapper.d.ts.map +1 -0
  38. package/dist/audit/audit-dto-mapper.js +61 -0
  39. package/dist/audit/audit-dto-mapper.js.map +1 -0
  40. package/dist/audit/audit-logger.d.ts +35 -0
  41. package/dist/audit/audit-logger.d.ts.map +1 -0
  42. package/dist/audit/audit-logger.js +67 -0
  43. package/dist/audit/audit-logger.js.map +1 -0
  44. package/dist/audit/audit-sync.d.ts +12 -0
  45. package/dist/audit/audit-sync.d.ts.map +1 -0
  46. package/dist/audit/audit-sync.js +65 -0
  47. package/dist/audit/audit-sync.js.map +1 -0
  48. package/dist/auth/user-authenticator.d.ts +51 -0
  49. package/dist/auth/user-authenticator.d.ts.map +1 -0
  50. package/dist/auth/user-authenticator.js +155 -0
  51. package/dist/auth/user-authenticator.js.map +1 -0
  52. package/dist/cli/cli-db.d.ts +12 -0
  53. package/dist/cli/cli-db.d.ts.map +1 -0
  54. package/dist/cli/cli-db.js +20 -0
  55. package/dist/cli/cli-db.js.map +1 -0
  56. package/dist/cli/cli-utils.d.ts +14 -0
  57. package/dist/cli/cli-utils.d.ts.map +1 -0
  58. package/dist/cli/cli-utils.js +57 -0
  59. package/dist/cli/cli-utils.js.map +1 -0
  60. package/dist/cli/daemon-utils.d.ts +30 -0
  61. package/dist/cli/daemon-utils.d.ts.map +1 -0
  62. package/dist/cli/daemon-utils.js +131 -0
  63. package/dist/cli/daemon-utils.js.map +1 -0
  64. package/dist/cli/daemon.d.ts +13 -0
  65. package/dist/cli/daemon.d.ts.map +1 -0
  66. package/dist/cli/daemon.js +207 -0
  67. package/dist/cli/daemon.js.map +1 -0
  68. package/dist/cli/doctor.d.ts +2 -0
  69. package/dist/cli/doctor.d.ts.map +1 -0
  70. package/dist/cli/doctor.js +135 -0
  71. package/dist/cli/doctor.js.map +1 -0
  72. package/dist/cli/env-delete.d.ts +6 -0
  73. package/dist/cli/env-delete.d.ts.map +1 -0
  74. package/dist/cli/env-delete.js +80 -0
  75. package/dist/cli/env-delete.js.map +1 -0
  76. package/dist/cli/env-list.d.ts +5 -0
  77. package/dist/cli/env-list.d.ts.map +1 -0
  78. package/dist/cli/env-list.js +42 -0
  79. package/dist/cli/env-list.js.map +1 -0
  80. package/dist/cli/env-post-integration.d.ts +21 -0
  81. package/dist/cli/env-post-integration.d.ts.map +1 -0
  82. package/dist/cli/env-post-integration.js +300 -0
  83. package/dist/cli/env-post-integration.js.map +1 -0
  84. package/dist/cli/env-restore.d.ts +15 -0
  85. package/dist/cli/env-restore.d.ts.map +1 -0
  86. package/dist/cli/env-restore.js +130 -0
  87. package/dist/cli/env-restore.js.map +1 -0
  88. package/dist/cli/env.d.ts +14 -0
  89. package/dist/cli/env.d.ts.map +1 -0
  90. package/dist/cli/env.js +182 -0
  91. package/dist/cli/env.js.map +1 -0
  92. package/dist/cli/error-handlers.d.ts +13 -0
  93. package/dist/cli/error-handlers.d.ts.map +1 -0
  94. package/dist/cli/error-handlers.js +32 -0
  95. package/dist/cli/error-handlers.js.map +1 -0
  96. package/dist/cli/hook-check-env.d.ts +12 -0
  97. package/dist/cli/hook-check-env.d.ts.map +1 -0
  98. package/dist/cli/hook-check-env.js +117 -0
  99. package/dist/cli/hook-check-env.js.map +1 -0
  100. package/dist/cli/index.d.ts +2 -0
  101. package/dist/cli/index.d.ts.map +1 -0
  102. package/dist/cli/index.js +294 -0
  103. package/dist/cli/index.js.map +1 -0
  104. package/dist/cli/init-guard.d.ts +13 -0
  105. package/dist/cli/init-guard.d.ts.map +1 -0
  106. package/dist/cli/init-guard.js +62 -0
  107. package/dist/cli/init-guard.js.map +1 -0
  108. package/dist/cli/init.d.ts +19 -0
  109. package/dist/cli/init.d.ts.map +1 -0
  110. package/dist/cli/init.js +440 -0
  111. package/dist/cli/init.js.map +1 -0
  112. package/dist/cli/install.d.ts +14 -0
  113. package/dist/cli/install.d.ts.map +1 -0
  114. package/dist/cli/install.js +186 -0
  115. package/dist/cli/install.js.map +1 -0
  116. package/dist/cli/login.d.ts +6 -0
  117. package/dist/cli/login.d.ts.map +1 -0
  118. package/dist/cli/login.js +76 -0
  119. package/dist/cli/login.js.map +1 -0
  120. package/dist/cli/logs.d.ts +32 -0
  121. package/dist/cli/logs.d.ts.map +1 -0
  122. package/dist/cli/logs.js +147 -0
  123. package/dist/cli/logs.js.map +1 -0
  124. package/dist/cli/project.d.ts +8 -0
  125. package/dist/cli/project.d.ts.map +1 -0
  126. package/dist/cli/project.js +102 -0
  127. package/dist/cli/project.js.map +1 -0
  128. package/dist/cli/reset.d.ts +8 -0
  129. package/dist/cli/reset.d.ts.map +1 -0
  130. package/dist/cli/reset.js +137 -0
  131. package/dist/cli/reset.js.map +1 -0
  132. package/dist/cli/run.d.ts +22 -0
  133. package/dist/cli/run.d.ts.map +1 -0
  134. package/dist/cli/run.js +103 -0
  135. package/dist/cli/run.js.map +1 -0
  136. package/dist/cli/start.d.ts +2 -0
  137. package/dist/cli/start.d.ts.map +1 -0
  138. package/dist/cli/start.js +29 -0
  139. package/dist/cli/start.js.map +1 -0
  140. package/dist/cli/status.d.ts +12 -0
  141. package/dist/cli/status.d.ts.map +1 -0
  142. package/dist/cli/status.js +131 -0
  143. package/dist/cli/status.js.map +1 -0
  144. package/dist/cli/uninstall.d.ts +8 -0
  145. package/dist/cli/uninstall.d.ts.map +1 -0
  146. package/dist/cli/uninstall.js +111 -0
  147. package/dist/cli/uninstall.js.map +1 -0
  148. package/dist/config/config.d.ts +10 -0
  149. package/dist/config/config.d.ts.map +1 -0
  150. package/dist/config/config.js +64 -0
  151. package/dist/config/config.js.map +1 -0
  152. package/dist/config/constants.d.ts +3 -0
  153. package/dist/config/constants.d.ts.map +1 -0
  154. package/dist/config/constants.js +6 -0
  155. package/dist/config/constants.js.map +1 -0
  156. package/dist/config/paths.d.ts +9 -0
  157. package/dist/config/paths.d.ts.map +1 -0
  158. package/dist/config/paths.js +58 -0
  159. package/dist/config/paths.js.map +1 -0
  160. package/dist/core/execution-engine.d.ts +119 -0
  161. package/dist/core/execution-engine.d.ts.map +1 -0
  162. package/dist/core/execution-engine.js +1291 -0
  163. package/dist/core/execution-engine.js.map +1 -0
  164. package/dist/core/runtime.d.ts +43 -0
  165. package/dist/core/runtime.d.ts.map +1 -0
  166. package/dist/core/runtime.js +143 -0
  167. package/dist/core/runtime.js.map +1 -0
  168. package/dist/core/sync-scheduler.d.ts +42 -0
  169. package/dist/core/sync-scheduler.d.ts.map +1 -0
  170. package/dist/core/sync-scheduler.js +131 -0
  171. package/dist/core/sync-scheduler.js.map +1 -0
  172. package/dist/core/types.d.ts +77 -0
  173. package/dist/core/types.d.ts.map +1 -0
  174. package/dist/core/types.js +7 -0
  175. package/dist/core/types.js.map +1 -0
  176. package/dist/daemon/service-manager.d.ts +68 -0
  177. package/dist/daemon/service-manager.d.ts.map +1 -0
  178. package/dist/daemon/service-manager.js +303 -0
  179. package/dist/daemon/service-manager.js.map +1 -0
  180. package/dist/env/env-classifier.d.ts +14 -0
  181. package/dist/env/env-classifier.d.ts.map +1 -0
  182. package/dist/env/env-classifier.js +94 -0
  183. package/dist/env/env-classifier.js.map +1 -0
  184. package/dist/env/env-parser.d.ts +13 -0
  185. package/dist/env/env-parser.d.ts.map +1 -0
  186. package/dist/env/env-parser.js +33 -0
  187. package/dist/env/env-parser.js.map +1 -0
  188. package/dist/env/env-profile-store.d.ts +15 -0
  189. package/dist/env/env-profile-store.d.ts.map +1 -0
  190. package/dist/env/env-profile-store.js +35 -0
  191. package/dist/env/env-profile-store.js.map +1 -0
  192. package/dist/env/env-reference.d.ts +10 -0
  193. package/dist/env/env-reference.d.ts.map +1 -0
  194. package/dist/env/env-reference.js +33 -0
  195. package/dist/env/env-reference.js.map +1 -0
  196. package/dist/env/env-resolver.d.ts +18 -0
  197. package/dist/env/env-resolver.d.ts.map +1 -0
  198. package/dist/env/env-resolver.js +48 -0
  199. package/dist/env/env-resolver.js.map +1 -0
  200. package/dist/env/fs-utils.d.ts +9 -0
  201. package/dist/env/fs-utils.d.ts.map +1 -0
  202. package/dist/env/fs-utils.js +59 -0
  203. package/dist/env/fs-utils.js.map +1 -0
  204. package/dist/env/secret-backend.d.ts +15 -0
  205. package/dist/env/secret-backend.d.ts.map +1 -0
  206. package/dist/env/secret-backend.js +24 -0
  207. package/dist/env/secret-backend.js.map +1 -0
  208. package/dist/executor/executor-registry.d.ts +22 -0
  209. package/dist/executor/executor-registry.d.ts.map +1 -0
  210. package/dist/executor/executor-registry.js +42 -0
  211. package/dist/executor/executor-registry.js.map +1 -0
  212. package/dist/executor/process-launcher.d.ts +26 -0
  213. package/dist/executor/process-launcher.d.ts.map +1 -0
  214. package/dist/executor/process-launcher.js +98 -0
  215. package/dist/executor/process-launcher.js.map +1 -0
  216. package/dist/executor/secret-file.d.ts +28 -0
  217. package/dist/executor/secret-file.d.ts.map +1 -0
  218. package/dist/executor/secret-file.js +127 -0
  219. package/dist/executor/secret-file.js.map +1 -0
  220. package/dist/gateway/auth.d.ts +26 -0
  221. package/dist/gateway/auth.d.ts.map +1 -0
  222. package/dist/gateway/auth.js +66 -0
  223. package/dist/gateway/auth.js.map +1 -0
  224. package/dist/gateway/gateway-client.d.ts +298 -0
  225. package/dist/gateway/gateway-client.d.ts.map +1 -0
  226. package/dist/gateway/gateway-client.js +501 -0
  227. package/dist/gateway/gateway-client.js.map +1 -0
  228. package/dist/identity/agent-identity.d.ts +29 -0
  229. package/dist/identity/agent-identity.d.ts.map +1 -0
  230. package/dist/identity/agent-identity.js +54 -0
  231. package/dist/identity/agent-identity.js.map +1 -0
  232. package/dist/identity/did-manager.d.ts +17 -0
  233. package/dist/identity/did-manager.d.ts.map +1 -0
  234. package/dist/identity/did-manager.js +29 -0
  235. package/dist/identity/did-manager.js.map +1 -0
  236. package/dist/identity/key-manager.d.ts +18 -0
  237. package/dist/identity/key-manager.d.ts.map +1 -0
  238. package/dist/identity/key-manager.js +101 -0
  239. package/dist/identity/key-manager.js.map +1 -0
  240. package/dist/identity/session-key.d.ts +13 -0
  241. package/dist/identity/session-key.d.ts.map +1 -0
  242. package/dist/identity/session-key.js +17 -0
  243. package/dist/identity/session-key.js.map +1 -0
  244. package/dist/policy/policy-evaluator.d.ts +63 -0
  245. package/dist/policy/policy-evaluator.d.ts.map +1 -0
  246. package/dist/policy/policy-evaluator.js +266 -0
  247. package/dist/policy/policy-evaluator.js.map +1 -0
  248. package/dist/policy/policy-loader.d.ts +10 -0
  249. package/dist/policy/policy-loader.d.ts.map +1 -0
  250. package/dist/policy/policy-loader.js +71 -0
  251. package/dist/policy/policy-loader.js.map +1 -0
  252. package/dist/policy/types.d.ts +21 -0
  253. package/dist/policy/types.d.ts.map +1 -0
  254. package/dist/policy/types.js +3 -0
  255. package/dist/policy/types.js.map +1 -0
  256. package/dist/utils/credential-errors.d.ts +3 -0
  257. package/dist/utils/credential-errors.d.ts.map +1 -0
  258. package/dist/utils/credential-errors.js +23 -0
  259. package/dist/utils/credential-errors.js.map +1 -0
  260. package/dist/utils/resource-canonicalizer.d.ts +19 -0
  261. package/dist/utils/resource-canonicalizer.d.ts.map +1 -0
  262. package/dist/utils/resource-canonicalizer.js +100 -0
  263. package/dist/utils/resource-canonicalizer.js.map +1 -0
  264. package/dist/utils/vc-utils.d.ts +23 -0
  265. package/dist/utils/vc-utils.d.ts.map +1 -0
  266. package/dist/utils/vc-utils.js +53 -0
  267. package/dist/utils/vc-utils.js.map +1 -0
  268. package/dist/wallet/sqlite.d.ts +4 -0
  269. package/dist/wallet/sqlite.d.ts.map +1 -0
  270. package/dist/wallet/sqlite.js +158 -0
  271. package/dist/wallet/sqlite.js.map +1 -0
  272. package/dist/wallet/vp-builder.d.ts +18 -0
  273. package/dist/wallet/vp-builder.d.ts.map +1 -0
  274. package/dist/wallet/vp-builder.js +46 -0
  275. package/dist/wallet/vp-builder.js.map +1 -0
  276. package/dist/wallet/wallet.d.ts +58 -0
  277. package/dist/wallet/wallet.d.ts.map +1 -0
  278. package/dist/wallet/wallet.js +170 -0
  279. package/dist/wallet/wallet.js.map +1 -0
  280. package/package.json +80 -0
@@ -0,0 +1,1291 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ExecutionEngine = void 0;
37
+ /**
38
+ * Execution Control Runtime — the core of agentd.
39
+ *
40
+ * All execution control logic lives here:
41
+ * - Action normalization
42
+ * - Resource canonicalization
43
+ * - Policy evaluation (local + org)
44
+ * - VC acquisition (4-step flow, spec §6.1)
45
+ * - VP creation + Gateway verification (spec §7.4)
46
+ * - Execution routing (local vs gateway)
47
+ * - Offline fallback (spec §7.6)
48
+ * - Audit logging
49
+ *
50
+ * This module is adapter-independent. MCP, IDE plugins, or SDK clients
51
+ * all call ExecutionEngine.execute() through their respective thin adapters.
52
+ */
53
+ const ai_identity_1 = require("@vess-id/ai-identity");
54
+ const os_1 = require("os");
55
+ const path = __importStar(require("path"));
56
+ const fs = __importStar(require("fs"));
57
+ const wallet_1 = require("../wallet/wallet");
58
+ const gateway_client_1 = require("../gateway/gateway-client");
59
+ const resource_canonicalizer_1 = require("../utils/resource-canonicalizer");
60
+ const vc_utils_1 = require("../utils/vc-utils");
61
+ const credential_errors_1 = require("../utils/credential-errors");
62
+ const executor_registry_1 = require("../executor/executor-registry");
63
+ const env_resolver_1 = require("../env/env-resolver");
64
+ /** Policy cache staleness TTL (spec §7.6: 30 minutes) */
65
+ const POLICY_SYNC_TTL_MS = 30 * 60 * 1000;
66
+ /** OOB approval request expiration (seconds) */
67
+ const OOB_APPROVAL_EXPIRES_IN_SECONDS = 300;
68
+ /** Default TTL for OOB-approved credentials (10 minutes) */
69
+ const OOB_CREDENTIAL_DEFAULT_TTL_MS = 10 * 60 * 1000;
70
+ class ExecutionEngine {
71
+ policyEvaluator;
72
+ wallet;
73
+ vpBuilder;
74
+ auditLogger;
75
+ gatewayClient;
76
+ context;
77
+ tokenService;
78
+ secretBackend;
79
+ lastPolicySyncTime = null;
80
+ clientInfo;
81
+ agentRegistered = false;
82
+ recentOOBRequests = new Map();
83
+ static OOB_COOLDOWN_MS = 60_000;
84
+ static OOB_MAP_MAX_SIZE = 1000;
85
+ constructor(policyEvaluator, wallet, vpBuilder, auditLogger, gatewayClient, context, tokenService, secretBackend) {
86
+ this.policyEvaluator = policyEvaluator;
87
+ this.wallet = wallet;
88
+ this.vpBuilder = vpBuilder;
89
+ this.auditLogger = auditLogger;
90
+ this.gatewayClient = gatewayClient;
91
+ this.context = context;
92
+ this.tokenService = tokenService;
93
+ this.secretBackend = secretBackend;
94
+ }
95
+ /**
96
+ * Record a successful policy sync timestamp (called by SyncScheduler).
97
+ */
98
+ setLastPolicySyncTime(time) {
99
+ this.lastPolicySyncTime = time;
100
+ }
101
+ /**
102
+ * Set MCP client info (from oninitialized callback).
103
+ * Used to derive a meaningful agent name for deferred registration.
104
+ */
105
+ setClientInfo(info) {
106
+ this.clientInfo = info;
107
+ }
108
+ /**
109
+ * Ensure the agent is registered in the API.
110
+ * Called lazily on first tool execution (fire-and-forget).
111
+ * Uses clientInfo from MCP initialize handshake if available.
112
+ */
113
+ ensureAgentRegistered() {
114
+ if (this.agentRegistered)
115
+ return;
116
+ this.agentRegistered = true;
117
+ const publicKey = this.context.agentPublicKey;
118
+ if (!publicKey || !this.context.projectId)
119
+ return;
120
+ const agentName = this.clientInfo
121
+ ? `${this.clientInfo.name}-agent`
122
+ : 'vess-agentd';
123
+ this.gatewayClient.registerAgent({
124
+ agentDid: this.context.agentDid,
125
+ name: agentName,
126
+ type: 'assistant',
127
+ publicKey,
128
+ projectId: this.context.projectId,
129
+ deviceInfo: {
130
+ platform: process.platform,
131
+ hostname: (0, os_1.hostname)(),
132
+ runtime: this.clientInfo?.name || 'unknown',
133
+ },
134
+ }).catch((err) => {
135
+ process.stderr.write(`[vess] Agent registration failed (non-fatal): ${err instanceof Error ? err.message : String(err)}\n`);
136
+ });
137
+ }
138
+ /**
139
+ * Execute an action request.
140
+ * This is the single entry point for all adapters (MCP, future IDE, SDK).
141
+ */
142
+ async execute(request) {
143
+ // Deferred agent registration: fire-and-forget on first tool execution
144
+ this.ensureAgentRegistered();
145
+ // Mutual exclusion: pendingRequestId and approval are incompatible
146
+ if (request.pendingRequestId && request.approval) {
147
+ return { success: false, error: 'Cannot provide both pendingRequestId and approval in the same request' };
148
+ }
149
+ const { action, input } = request;
150
+ // 1. Extract provider from action (e.g., 'os' from 'os.secret.read')
151
+ const provider = action.split('.')[0];
152
+ // 2. Resource canonicalization
153
+ const requestedResource = input.file_path || (action === 'os.process.run' ? input.command?.join(' ') : undefined);
154
+ let normalizedResource;
155
+ let resourceFingerprint;
156
+ if ((0, executor_registry_1.isLocalAction)(action) && input.file_path) {
157
+ normalizedResource = (0, resource_canonicalizer_1.canonicalizePath)(input.file_path);
158
+ resourceFingerprint = (0, resource_canonicalizer_1.computeFingerprint)(normalizedResource, this.context.rootDid);
159
+ }
160
+ // 3. Policy evaluation (local + org deny check)
161
+ const policyResult = this.policyEvaluator.evaluate(provider, action.replace(`${provider}.`, ''), {
162
+ type: this.getResourceType(action),
163
+ pattern: normalizedResource || input.file_path,
164
+ });
165
+ if (!policyResult.allowed) {
166
+ this.logAudit({
167
+ action, provider,
168
+ decision: 'deny',
169
+ enforcementType: 'local',
170
+ decisionSource: 'local_policy',
171
+ decisionReason: policyResult.reason,
172
+ requestedResource,
173
+ normalizedResource,
174
+ resourceFingerprint,
175
+ executionType: (0, executor_registry_1.isLocalAction)(action) ? 'local' : 'gateway',
176
+ });
177
+ return { success: false, error: `Action denied by policy: ${policyResult.reason}` };
178
+ }
179
+ // Handle OOB approval polling (pendingRequestId)
180
+ if (request.pendingRequestId) {
181
+ return this.handlePendingApproval(request);
182
+ }
183
+ // Handle 2nd call with approval decision
184
+ if (request.approval) {
185
+ // Security guard: block in-band approval for high/critical risk actions
186
+ const riskLevel = this.getActionRiskLevel(request.action);
187
+ if (riskLevel === 'high' || riskLevel === 'critical') {
188
+ return {
189
+ success: false,
190
+ error: `High-risk action "${request.action}" requires browser approval (Out-of-Band). In-band approval tokens are not accepted.`,
191
+ };
192
+ }
193
+ return this.handleApprovalResponse(request, provider, normalizedResource, resourceFingerprint, requestedResource);
194
+ }
195
+ // 4. Determine execution routing
196
+ const routing = (0, executor_registry_1.determineRouting)(provider, action);
197
+ // 5. Execute based on routing
198
+ if (routing.executor === 'local') {
199
+ return this.executeLocal(action, input, provider, normalizedResource, resourceFingerprint, requestedResource);
200
+ }
201
+ else {
202
+ return this.executeViaGateway(provider, action, input);
203
+ }
204
+ }
205
+ // ===========================================================================
206
+ // 4-step VC acquisition (spec §6.1)
207
+ // ===========================================================================
208
+ /**
209
+ * Ensure a valid VC exists for the requested action.
210
+ * Follows the 4-step acquisition flow from spec §6.1:
211
+ *
212
+ * Step 1: Check wallet for valid VC (verify revocation via Gateway using VC jti)
213
+ * Step 2: Check Gateway for approved requests
214
+ * Step 3: Try auto-issue via Gateway
215
+ * Step 4: Terminal approval prompt -> quick-approve via Gateway
216
+ */
217
+ async ensureVC(action, provider, resourceInfo, input = {}, canonicalResourceId) {
218
+ let approvedRequests = null;
219
+ // Step 1: Check wallet for existing valid VC
220
+ if (canonicalResourceId) {
221
+ // Step 1a: Exact resource match
222
+ const scopedVC = this.wallet.findCredential(this.context.agentDid, action, this.context.projectId, canonicalResourceId);
223
+ if (scopedVC) {
224
+ const verified = await this.verifyWalletVC(scopedVC);
225
+ if (verified)
226
+ return { credential: verified, decisionSource: 'cached_vc' };
227
+ }
228
+ // Step 1b: Legacy VC without resource scope (Gateway will validate)
229
+ const unscopedVC = this.wallet.findUnscopedCredential(this.context.agentDid, action, this.context.projectId);
230
+ if (unscopedVC) {
231
+ const verified = await this.verifyWalletVC(unscopedVC);
232
+ if (verified)
233
+ return { credential: verified, decisionSource: 'cached_vc' };
234
+ }
235
+ // No matching VC — fall through to Step 2-4
236
+ }
237
+ else {
238
+ // No resource context — use original findCredential
239
+ const walletVC = this.wallet.findCredential(this.context.agentDid, action, this.context.projectId);
240
+ if (walletVC) {
241
+ const verified = await this.verifyWalletVC(walletVC);
242
+ if (verified)
243
+ return { credential: verified, decisionSource: 'cached_vc' };
244
+ }
245
+ }
246
+ // Step 2: Check Gateway for approved requests
247
+ try {
248
+ approvedRequests = await this.gatewayClient.findApprovedRequests(this.context.agentDid);
249
+ const matching = approvedRequests.find(r => r.actions?.includes(action) && r.credential);
250
+ if (matching && matching.credential) {
251
+ const stored = this.wallet.storeCredential({
252
+ holderDid: this.context.agentDid,
253
+ projectId: this.context.projectId,
254
+ credentialJwt: matching.credential.jwt,
255
+ actions: matching.actions,
256
+ provider,
257
+ expiresAt: matching.credential.expiresAt,
258
+ normalizedResourceKey: resourceInfo?.normalizedResource,
259
+ resourceFingerprint: resourceInfo?.fingerprint,
260
+ resources: (0, wallet_1.validateResources)(matching.resources),
261
+ });
262
+ return { credential: stored, decisionSource: 'cached_vc' };
263
+ }
264
+ }
265
+ catch {
266
+ // Gateway unreachable for step 2 — continue to step 3
267
+ }
268
+ // Step 3: Try auto-issue via Gateway
269
+ try {
270
+ const autoResult = await this.gatewayClient.autoIssueVC({
271
+ subjectDid: this.context.agentDid,
272
+ projectId: this.context.projectId,
273
+ actions: [action],
274
+ });
275
+ if (autoResult?.autoIssued && autoResult.credential) {
276
+ const stored = this.wallet.storeCredential({
277
+ holderDid: this.context.agentDid,
278
+ projectId: this.context.projectId,
279
+ credentialJwt: autoResult.credential.jwt,
280
+ actions: autoResult.actions || [action],
281
+ provider,
282
+ expiresAt: autoResult.credential.expiresAt,
283
+ normalizedResourceKey: resourceInfo?.normalizedResource,
284
+ resourceFingerprint: resourceInfo?.fingerprint,
285
+ resources: (0, wallet_1.validateResources)(autoResult.resources),
286
+ metadata: autoResult.metadata,
287
+ });
288
+ return { credential: stored, decisionSource: 'cached_vc' };
289
+ }
290
+ }
291
+ catch {
292
+ // Gateway unreachable for step 3 — continue to step 4
293
+ }
294
+ // Step 4: Risk-based approval routing
295
+ const riskLevel = this.getActionRiskLevel(action);
296
+ if (riskLevel === 'high' || riskLevel === 'critical') {
297
+ try {
298
+ return await this.createOOBApprovalRequest(action, provider, riskLevel, resourceInfo, input);
299
+ }
300
+ catch (err) {
301
+ return {
302
+ denied: true,
303
+ reason: `Failed to create out-of-band approval request: ${err instanceof Error ? err.message : String(err)}`,
304
+ };
305
+ }
306
+ }
307
+ else {
308
+ return this.createInBandApprovalResponse(action, provider, riskLevel, approvedRequests, resourceInfo, input);
309
+ }
310
+ }
311
+ // ===========================================================================
312
+ // Local execution with VP verification (spec §7.4)
313
+ // ===========================================================================
314
+ async executeLocal(action, params, provider, normalizedResource, resourceFingerprint, requestedResource) {
315
+ const resourceInfo = normalizedResource && resourceFingerprint
316
+ ? { normalizedResource, fingerprint: resourceFingerprint }
317
+ : undefined;
318
+ // 1. Ensure VC (4-step acquisition)
319
+ const vcResult = await this.ensureVC(action, provider, resourceInfo, params);
320
+ if ('approvalRequired' in vcResult) {
321
+ return { success: false, approvalRequired: vcResult.approvalRequired };
322
+ }
323
+ if ('waitingForApproval' in vcResult) {
324
+ return { success: false, waitingForApproval: vcResult.waitingForApproval };
325
+ }
326
+ if ('denied' in vcResult) {
327
+ this.logAudit({
328
+ action, provider,
329
+ decision: 'deny',
330
+ enforcementType: 'local',
331
+ decisionSource: 'local_policy',
332
+ decisionReason: vcResult.reason,
333
+ requestedResource,
334
+ normalizedResource,
335
+ resourceFingerprint,
336
+ executionType: 'local',
337
+ });
338
+ return { success: false, error: `Permission denied: ${vcResult.reason}` };
339
+ }
340
+ // 2. Execute with the acquired VC
341
+ return this.executeLocalWithVC(action, params, provider, vcResult, normalizedResource, resourceFingerprint, requestedResource);
342
+ }
343
+ /**
344
+ * Execute locally with an already-acquired VC (skips ensureVC).
345
+ * Used by both the normal path (after ensureVC) and handleApprovalResponse (after quickApprove).
346
+ */
347
+ async executeLocalWithVC(action, params, provider, vcResult, normalizedResource, resourceFingerprint, requestedResource, retryCount = 0) {
348
+ const executor = (0, executor_registry_1.getLocalExecutor)(action);
349
+ if (!executor) {
350
+ return { success: false, error: `No local executor for action: ${action}` };
351
+ }
352
+ // Try Gateway VP verification (online path)
353
+ let gatewayReachable = true;
354
+ try {
355
+ const { nonce } = await this.gatewayClient.issueNonce(this.context.agentDid);
356
+ const vpJwt = await this.vpBuilder.buildVP({
357
+ credentialJwt: vcResult.credential.credentialJwt,
358
+ signerDid: this.context.agentDid,
359
+ signerPrivateKeyJwk: this.context.agentPrivateKeyJwk,
360
+ nonce,
361
+ domain: this.context.gatewayUrl,
362
+ });
363
+ const authResult = await this.gatewayClient.verifyAndAuthorize(vpJwt, nonce, this.context.gatewayUrl, action, this.context.agentDid);
364
+ if (!authResult.authorized) {
365
+ // Check if the failure is due to credential expiration — attempt reissuance (max 1 retry)
366
+ if ((0, credential_errors_1.isCredentialInvalidError)(authResult.reason) && retryCount < 1) {
367
+ this.wallet.revokeCredential(vcResult.credential.id);
368
+ const resourceInfo = normalizedResource && resourceFingerprint
369
+ ? { normalizedResource, fingerprint: resourceFingerprint }
370
+ : undefined;
371
+ const newVCResult = await this.ensureVC(action, provider, resourceInfo, params);
372
+ if ('credential' in newVCResult) {
373
+ return this.executeLocalWithVC(action, params, provider, newVCResult, normalizedResource, resourceFingerprint, requestedResource, retryCount + 1);
374
+ }
375
+ // ensureVC returned approval flow — pass through to caller
376
+ if ('waitingForApproval' in newVCResult) {
377
+ return { success: false, waitingForApproval: newVCResult.waitingForApproval };
378
+ }
379
+ if ('approvalRequired' in newVCResult) {
380
+ return { success: false, approvalRequired: newVCResult.approvalRequired };
381
+ }
382
+ // ensureVC denied — return denial reason instead of original auth error
383
+ if ('denied' in newVCResult) {
384
+ return { success: false, error: `VC reissuance denied: ${newVCResult.reason}` };
385
+ }
386
+ }
387
+ this.logAudit({
388
+ action, provider,
389
+ decision: 'deny',
390
+ enforcementType: 'gateway_verified_local',
391
+ decisionSource: vcResult.decisionSource,
392
+ decisionReason: authResult.reason,
393
+ requestedResource,
394
+ normalizedResource,
395
+ resourceFingerprint,
396
+ executionType: 'local',
397
+ grantId: authResult.grantId,
398
+ });
399
+ return { success: false, error: `Authorization denied: ${authResult.reason}` };
400
+ }
401
+ // Consume one-time grant BEFORE execution
402
+ const effectiveGrantId = authResult.grantId || vcResult.grantId;
403
+ if (effectiveGrantId && authResult.approvalMode === 'one_time') {
404
+ const consumeResult = await this.gatewayClient.consumeGrant(effectiveGrantId);
405
+ if (!consumeResult.consumed) {
406
+ this.logAudit({
407
+ action, provider,
408
+ decision: 'deny',
409
+ enforcementType: 'gateway_verified_local',
410
+ decisionSource: vcResult.decisionSource,
411
+ decisionReason: `Grant consumption failed: ${consumeResult.reason}`,
412
+ requestedResource,
413
+ normalizedResource,
414
+ resourceFingerprint,
415
+ executionType: 'local',
416
+ grantId: effectiveGrantId,
417
+ });
418
+ return { success: false, error: `Grant consumption failed: ${consumeResult.reason}` };
419
+ }
420
+ }
421
+ // Execute locally — build context based on action type
422
+ let result;
423
+ if (action === 'os.process.run') {
424
+ const resolvedEnv = await this.resolveProcessEnv(params);
425
+ result = executor(params, { resolvedEnv });
426
+ }
427
+ else {
428
+ result = executor(params, {
429
+ rootDid: this.context.rootDid,
430
+ approvedResource: vcResult.credential.normalizedResourceKey,
431
+ approvedFingerprint: vcResult.credential.resourceFingerprint,
432
+ });
433
+ }
434
+ this.logAudit({
435
+ action, provider,
436
+ decision: result.success ? 'allow' : 'deny',
437
+ enforcementType: 'gateway_verified_local',
438
+ decisionSource: vcResult.decisionSource,
439
+ requestedResource,
440
+ normalizedResource,
441
+ resourceFingerprint: result.resourceFingerprint || resourceFingerprint,
442
+ executionType: 'local',
443
+ grantId: effectiveGrantId || authResult.grantId,
444
+ credentialId: vcResult.credential.id,
445
+ approvalMode: vcResult.approvalMode,
446
+ approvalNonce: vcResult.approvalNonce,
447
+ });
448
+ return { success: result.success, data: result.data, error: result.error };
449
+ }
450
+ catch (err) {
451
+ if (!(err instanceof gateway_client_1.GatewayNetworkError))
452
+ throw err;
453
+ gatewayReachable = false;
454
+ }
455
+ if (!gatewayReachable) {
456
+ return this.executeLocalOffline(action, params, provider, vcResult, normalizedResource, resourceFingerprint, requestedResource);
457
+ }
458
+ return { success: false, error: `Unexpected execution state for ${action}` };
459
+ }
460
+ // ===========================================================================
461
+ // Offline fallback (spec §7.6)
462
+ // ===========================================================================
463
+ /**
464
+ * Execute locally when Gateway is unreachable.
465
+ * All 4 conditions from spec §7.6 must be true:
466
+ * 1. Valid cached VC exists
467
+ * 2. VC not expired
468
+ * 3. Synced org policy not stale (30 min TTL)
469
+ * 4. Local policy does not deny
470
+ */
471
+ async executeLocalOffline(action, params, provider, vcResult, normalizedResource, resourceFingerprint, requestedResource) {
472
+ // Condition 1: Valid cached VC exists (already guaranteed by ensureVC)
473
+ if (!vcResult.credential) {
474
+ return { success: false, error: 'Cannot execute offline: no cached VC' };
475
+ }
476
+ // Condition 2: VC not expired
477
+ if (vcResult.credential.expiresAt && vcResult.credential.expiresAt < Date.now()) {
478
+ this.logAudit({
479
+ action, provider,
480
+ decision: 'deny',
481
+ enforcementType: 'cached_verified_local',
482
+ decisionSource: 'cached_vc',
483
+ decisionReason: 'VC expired during offline execution',
484
+ requestedResource,
485
+ normalizedResource,
486
+ resourceFingerprint,
487
+ executionType: 'local',
488
+ });
489
+ return { success: false, error: 'Cannot execute offline: cached VC expired' };
490
+ }
491
+ // Condition 3: Synced org policy not stale (30 min TTL)
492
+ if (!this.lastPolicySyncTime || (Date.now() - this.lastPolicySyncTime) > POLICY_SYNC_TTL_MS) {
493
+ this.logAudit({
494
+ action, provider,
495
+ decision: 'deny',
496
+ enforcementType: 'cached_verified_local',
497
+ decisionSource: 'org_policy',
498
+ decisionReason: 'Org policy cache is stale or missing for offline execution',
499
+ requestedResource,
500
+ normalizedResource,
501
+ resourceFingerprint,
502
+ executionType: 'local',
503
+ });
504
+ return { success: false, error: 'Cannot execute offline: org policy cache is stale or has never been synced' };
505
+ }
506
+ // Condition 4: Local policy does not deny
507
+ const offlinePolicyCheck = this.policyEvaluator.evaluate(provider, action.replace(`${provider}.`, ''), {
508
+ type: this.getResourceType(action),
509
+ pattern: normalizedResource,
510
+ });
511
+ if (!offlinePolicyCheck.allowed) {
512
+ this.logAudit({
513
+ action, provider,
514
+ decision: 'deny',
515
+ enforcementType: 'cached_verified_local',
516
+ decisionSource: 'local_policy',
517
+ decisionReason: offlinePolicyCheck.reason,
518
+ requestedResource,
519
+ normalizedResource,
520
+ resourceFingerprint,
521
+ executionType: 'local',
522
+ });
523
+ return { success: false, error: `Cannot execute offline: local policy denies: ${offlinePolicyCheck.reason}` };
524
+ }
525
+ // All conditions met — execute with cached_verified_local enforcement
526
+ const executor = (0, executor_registry_1.getLocalExecutor)(action);
527
+ if (!executor) {
528
+ return { success: false, error: `No local executor for action: ${action}` };
529
+ }
530
+ let result;
531
+ if (action === 'os.process.run') {
532
+ const resolvedEnv = await this.resolveProcessEnv(params);
533
+ result = executor(params, { resolvedEnv });
534
+ }
535
+ else {
536
+ result = executor(params, {
537
+ rootDid: this.context.rootDid,
538
+ approvedResource: vcResult.credential.normalizedResourceKey,
539
+ approvedFingerprint: vcResult.credential.resourceFingerprint,
540
+ });
541
+ }
542
+ this.logAudit({
543
+ action, provider,
544
+ decision: result.success ? 'allow' : 'deny',
545
+ enforcementType: 'cached_verified_local',
546
+ decisionSource: 'cached_vc',
547
+ requestedResource,
548
+ normalizedResource,
549
+ resourceFingerprint: result.resourceFingerprint || resourceFingerprint,
550
+ executionType: 'local',
551
+ credentialId: vcResult.credential.id,
552
+ });
553
+ return { success: result.success, data: result.data, error: result.error };
554
+ }
555
+ // ===========================================================================
556
+ // SaaS execution via Gateway (spec §7.4)
557
+ // ===========================================================================
558
+ async executeViaGateway(provider, action, params) {
559
+ // NEW: Resolve resource identifier to canonical ID
560
+ const extractedId = this.extractResourceIdFromInput(action, params);
561
+ let canonicalResourceId = extractedId;
562
+ if (extractedId) {
563
+ try {
564
+ const resolved = await this.gatewayClient.resolveResource({
565
+ provider,
566
+ resourceType: this.getResourceType(action),
567
+ input: extractedId,
568
+ projectId: this.context.projectId,
569
+ });
570
+ if (resolved.resolved) {
571
+ canonicalResourceId = resolved.canonicalId;
572
+ }
573
+ }
574
+ catch {
575
+ // Resolution failed — continue with original input (best-effort)
576
+ }
577
+ }
578
+ // 1. Ensure VC (4-step acquisition) with resource context
579
+ const vcResult = await this.ensureVC(action, provider, undefined, params, canonicalResourceId);
580
+ if ('approvalRequired' in vcResult) {
581
+ return { success: false, approvalRequired: vcResult.approvalRequired };
582
+ }
583
+ if ('waitingForApproval' in vcResult) {
584
+ return { success: false, waitingForApproval: vcResult.waitingForApproval };
585
+ }
586
+ if ('denied' in vcResult) {
587
+ this.logAudit({
588
+ action, provider,
589
+ decision: 'deny',
590
+ enforcementType: 'gateway',
591
+ decisionSource: 'local_policy',
592
+ decisionReason: vcResult.reason,
593
+ executionType: 'gateway',
594
+ });
595
+ return { success: false, error: `Permission denied: ${vcResult.reason}` };
596
+ }
597
+ // 2. Execute with the acquired VC
598
+ return this.executeViaGatewayWithVC(provider, action, params, vcResult, 0, canonicalResourceId);
599
+ }
600
+ async executeViaGatewayWithVC(provider, action, params, vcResult, retryCount = 0, canonicalResourceId) {
601
+ try {
602
+ const { nonce } = await this.gatewayClient.issueNonce(this.context.agentDid);
603
+ const vpJwt = await this.vpBuilder.buildVP({
604
+ credentialJwt: vcResult.credential.credentialJwt,
605
+ signerDid: this.context.agentDid,
606
+ signerPrivateKeyJwk: this.context.agentPrivateKeyJwk,
607
+ nonce,
608
+ domain: this.context.gatewayUrl,
609
+ });
610
+ const result = await this.gatewayClient.invokeTool({
611
+ action,
612
+ parameters: params,
613
+ holderDid: this.context.agentDid,
614
+ vpJwt,
615
+ vpChallenge: nonce,
616
+ vpDomain: this.context.gatewayUrl,
617
+ });
618
+ // Detect resource mismatch from structured error code
619
+ if (!result.success && result.errorCode === 'RESOURCE_MISMATCH') {
620
+ return this.handleResourceMismatch(action, provider, params, vcResult);
621
+ }
622
+ // Detect credential-invalid errors and attempt VC reissuance (max 1 retry)
623
+ if (!result.success && result.errorCode === 'CREDENTIAL_INVALID' && retryCount < 1) {
624
+ this.wallet.revokeCredential(vcResult.credential.id);
625
+ const newVCResult = await this.ensureVC(action, provider, undefined, params, canonicalResourceId);
626
+ if ('credential' in newVCResult) {
627
+ return this.executeViaGatewayWithVC(provider, action, params, newVCResult, retryCount + 1, canonicalResourceId);
628
+ }
629
+ // ensureVC returned approval flow — pass through to caller
630
+ if ('waitingForApproval' in newVCResult) {
631
+ return { success: false, waitingForApproval: newVCResult.waitingForApproval };
632
+ }
633
+ if ('approvalRequired' in newVCResult) {
634
+ return { success: false, approvalRequired: newVCResult.approvalRequired };
635
+ }
636
+ // ensureVC denied — return denial reason
637
+ if ('denied' in newVCResult) {
638
+ return { success: false, error: `VC reissuance denied: ${newVCResult.reason}` };
639
+ }
640
+ // Could not re-acquire VC — return the original error
641
+ return { success: false, error: result.error };
642
+ }
643
+ this.logAudit({
644
+ action, provider,
645
+ decision: result.success ? 'allow' : 'deny',
646
+ enforcementType: 'gateway',
647
+ decisionSource: vcResult.decisionSource,
648
+ decisionReason: result.error,
649
+ executionType: 'gateway',
650
+ credentialId: vcResult.credential.id,
651
+ grantId: vcResult.grantId,
652
+ approvalMode: vcResult.approvalMode,
653
+ approvalNonce: vcResult.approvalNonce,
654
+ });
655
+ return { success: result.success, data: result.data, error: result.error };
656
+ }
657
+ catch (err) {
658
+ this.logAudit({
659
+ action, provider,
660
+ decision: 'deny',
661
+ enforcementType: 'gateway',
662
+ decisionSource: 'gateway_execution',
663
+ decisionReason: `Gateway unreachable: ${err instanceof Error ? err.message : String(err)}`,
664
+ executionType: 'gateway',
665
+ });
666
+ return {
667
+ success: false,
668
+ error: `Gateway required for SaaS operations but unreachable: ${err instanceof Error ? err.message : String(err)}`,
669
+ };
670
+ }
671
+ }
672
+ // ===========================================================================
673
+ // OOB approval methods
674
+ // ===========================================================================
675
+ /**
676
+ * Handle a re-invocation with pendingRequestId (OOB approval polling).
677
+ */
678
+ async handlePendingApproval(request) {
679
+ const { action, pendingRequestId } = request;
680
+ if (!pendingRequestId) {
681
+ return { success: false, error: 'No pendingRequestId provided' };
682
+ }
683
+ let statusResult;
684
+ try {
685
+ statusResult = await this.gatewayClient.getApprovalStatus(pendingRequestId);
686
+ }
687
+ catch (err) {
688
+ return { success: false, error: `Failed to check approval status: ${err instanceof Error ? err.message : String(err)}` };
689
+ }
690
+ // Security: verify that the re-invoked action matches what was originally approved
691
+ if (statusResult.actions && !statusResult.actions.includes(action)) {
692
+ return {
693
+ success: false,
694
+ error: `Action mismatch: approval was for [${statusResult.actions.join(', ')}], but re-invoked with "${action}"`,
695
+ };
696
+ }
697
+ // Security: verify that the re-invoked resource matches what was originally approved
698
+ if (statusResult.resources?.length) {
699
+ const extractedId = this.extractResourceIdFromInput(action, request.input);
700
+ if (extractedId) {
701
+ // For local actions, the approval stores canonicalized paths — canonicalize the input too
702
+ const normalizedId = (0, executor_registry_1.isLocalAction)(action) && request.input.file_path
703
+ ? (0, resource_canonicalizer_1.canonicalizePath)(request.input.file_path)
704
+ : extractedId;
705
+ const approvedResources = statusResult.resources;
706
+ const resourceMatch = approvedResources.some((r) => r.id === extractedId || r.id === normalizedId ||
707
+ r.pattern === extractedId || r.pattern === normalizedId);
708
+ if (!resourceMatch) {
709
+ return {
710
+ success: false,
711
+ error: `Resource mismatch: approval was for [${approvedResources.map((r) => r.id || r.pattern).join(', ')}], but re-invoked with "${extractedId}"`,
712
+ };
713
+ }
714
+ }
715
+ }
716
+ if (statusResult.status === 'denied') {
717
+ const provider = action.split('.')[0];
718
+ this.logAudit({
719
+ action, provider,
720
+ decision: 'deny',
721
+ enforcementType: 'gateway',
722
+ decisionSource: 'org_policy',
723
+ decisionReason: 'User denied the OOB approval request',
724
+ executionType: (0, executor_registry_1.isLocalAction)(action) ? 'local' : 'gateway',
725
+ });
726
+ return { success: false, error: 'Approval request was denied by the user' };
727
+ }
728
+ if (statusResult.status === 'expired') {
729
+ return { success: false, error: 'Approval request has expired. Please try again.' };
730
+ }
731
+ if (statusResult.status === 'pending') {
732
+ return {
733
+ success: false,
734
+ waitingForApproval: {
735
+ status: 'waiting_for_approval',
736
+ approvalUrl: statusResult.approvalUrl || 'Use the URL from your initial request',
737
+ requestId: pendingRequestId,
738
+ action,
739
+ riskLevel: this.getActionRiskLevel(action),
740
+ expiresIn: OOB_APPROVAL_EXPIRES_IN_SECONDS,
741
+ message: 'Still waiting for approval. Please approve in your browser.',
742
+ },
743
+ };
744
+ }
745
+ // approved — store credential and execute directly (no wallet round-trip)
746
+ if (statusResult.status === 'approved' && statusResult.credential) {
747
+ const provider = action.split('.')[0];
748
+ const stored = this.wallet.storeCredential({
749
+ holderDid: this.context.agentDid,
750
+ projectId: this.context.projectId,
751
+ credentialJwt: statusResult.credential,
752
+ actions: [action],
753
+ provider,
754
+ expiresAt: statusResult.expiresAt
755
+ ? new Date(statusResult.expiresAt).getTime()
756
+ : Date.now() + OOB_CREDENTIAL_DEFAULT_TTL_MS,
757
+ resources: (0, wallet_1.validateResources)(statusResult.resources),
758
+ });
759
+ // Build VCAcquired and execute directly (skip re-enter via this.execute)
760
+ const vcAcquired = {
761
+ credential: stored,
762
+ approvalMode: 'one_time',
763
+ decisionSource: 'new_vc_from_quick_approve',
764
+ };
765
+ const routing = (0, executor_registry_1.determineRouting)(provider, action);
766
+ let executeResult;
767
+ const normalizedResource = (0, executor_registry_1.isLocalAction)(action) && request.input.file_path
768
+ ? (0, resource_canonicalizer_1.canonicalizePath)(request.input.file_path)
769
+ : undefined;
770
+ const resourceFingerprint = normalizedResource
771
+ ? (0, resource_canonicalizer_1.computeFingerprint)(normalizedResource, this.context.rootDid)
772
+ : undefined;
773
+ try {
774
+ if (routing.executor === 'local') {
775
+ executeResult = await this.executeLocalWithVC(action, request.input, provider, vcAcquired, normalizedResource, resourceFingerprint, request.input.file_path);
776
+ }
777
+ else {
778
+ executeResult = await this.executeViaGatewayWithVC(provider, action, request.input, vcAcquired);
779
+ }
780
+ }
781
+ finally {
782
+ // Wallet cleanup for OOB one-time credentials
783
+ if (stored?.id) {
784
+ this.wallet.revokeCredential(stored.id);
785
+ }
786
+ }
787
+ return executeResult;
788
+ }
789
+ return { success: false, error: `Unexpected approval status: ${statusResult.status}` };
790
+ }
791
+ /**
792
+ * Create an OOB approval request for high/critical risk actions.
793
+ * Returns a URL for the user to approve in their browser.
794
+ */
795
+ async createOOBApprovalRequest(action, provider, riskLevel, resourceInfo, input) {
796
+ const resourceType = this.getResourceType(action);
797
+ const extractedId = this.extractResourceIdFromInput(action, input);
798
+ const resources = resourceInfo
799
+ ? [{ provider, type: resourceType, pattern: resourceInfo.normalizedResource }]
800
+ : extractedId
801
+ ? [{ provider, type: resourceType, id: extractedId }]
802
+ : [{ provider, type: resourceType }];
803
+ try {
804
+ const result = await this.gatewayClient.createApprovalRequest({
805
+ subjectDid: this.context.agentDid,
806
+ projectId: this.context.projectId,
807
+ actions: [action],
808
+ resources,
809
+ });
810
+ return {
811
+ waitingForApproval: {
812
+ status: 'waiting_for_approval',
813
+ approvalUrl: result.approvalUrl,
814
+ requestId: result.requestId,
815
+ action,
816
+ resource: resourceInfo?.normalizedResource ?? extractedId,
817
+ riskLevel,
818
+ expiresIn: OOB_APPROVAL_EXPIRES_IN_SECONDS,
819
+ message: `This ${riskLevel}-risk action requires browser approval (login required). Open the URL to approve.`,
820
+ },
821
+ };
822
+ }
823
+ catch (err) {
824
+ throw new Error(`Failed to create OOB approval request: ${err instanceof Error ? err.message : String(err)}`);
825
+ }
826
+ }
827
+ /**
828
+ * Create an in-band approval response for low/medium risk actions.
829
+ * Generates an approval token for the MCP client to present back.
830
+ */
831
+ createInBandApprovalResponse(action, _provider, riskLevel, approvedRequests, resourceInfo, input = {}) {
832
+ const defaultTTL = this.getDefaultVCTTL(riskLevel);
833
+ // Determine context using cached result from Step 2
834
+ let approvalContext = 'new_approval';
835
+ if (approvedRequests && approvedRequests.some(r => r.actions?.includes(action))) {
836
+ approvalContext = 'reconfirmation';
837
+ }
838
+ // Narrow resource: use normalized path for local actions, or extract resource ID for SaaS actions
839
+ const extractedResource = this.extractResourceIdFromInput(action, input);
840
+ const tokenResource = resourceInfo?.normalizedResource ?? extractedResource;
841
+ const approvalToken = this.tokenService.generate({
842
+ action,
843
+ scope: action,
844
+ resource: tokenResource,
845
+ context: approvalContext,
846
+ });
847
+ return {
848
+ approvalRequired: {
849
+ status: 'approval_required',
850
+ action,
851
+ description: `Execute ${action}`,
852
+ scope: action,
853
+ resource: tokenResource,
854
+ riskLevel,
855
+ options: ['one_time', 'persistent', 'deny_once', 'deny_persistent'],
856
+ suggestedTTL: defaultTTL,
857
+ maxTTL: { minutes: 1440, label: '24 hours' },
858
+ approvalToken,
859
+ context: approvalContext,
860
+ },
861
+ };
862
+ }
863
+ // ===========================================================================
864
+ // Utility methods
865
+ // ===========================================================================
866
+ /**
867
+ * Pre-request permissions for multiple actions (batch).
868
+ * Triggers VC acquisition for each action without executing.
869
+ * This allows the user to approve multiple actions at once.
870
+ */
871
+ async requestPermissions(actions) {
872
+ const approved = [];
873
+ const denied = [];
874
+ const pending = [];
875
+ for (const action of actions) {
876
+ const provider = action.split('.')[0];
877
+ // Policy check
878
+ const policyResult = this.policyEvaluator.evaluate(provider, action.replace(`${provider}.`, ''), { type: this.getResourceType(action) });
879
+ if (!policyResult.allowed) {
880
+ denied.push(action);
881
+ continue;
882
+ }
883
+ // Try VC acquisition (steps 1-3 only, no terminal prompt for batch)
884
+ try {
885
+ const walletVC = this.wallet.findCredential(this.context.agentDid, action, this.context.projectId);
886
+ if (walletVC) {
887
+ approved.push(action);
888
+ continue;
889
+ }
890
+ // Try auto-issue
891
+ const autoResult = await this.gatewayClient.autoIssueVC({
892
+ subjectDid: this.context.agentDid,
893
+ projectId: this.context.projectId,
894
+ actions: [action],
895
+ });
896
+ if (autoResult?.autoIssued && autoResult.credential) {
897
+ this.wallet.storeCredential({
898
+ holderDid: this.context.agentDid,
899
+ projectId: this.context.projectId,
900
+ credentialJwt: autoResult.credential.jwt,
901
+ actions: autoResult.actions || [action],
902
+ provider,
903
+ expiresAt: autoResult.credential.expiresAt,
904
+ resources: (0, wallet_1.validateResources)(autoResult.resources),
905
+ metadata: autoResult.metadata,
906
+ });
907
+ approved.push(action);
908
+ continue;
909
+ }
910
+ pending.push(action);
911
+ }
912
+ catch {
913
+ pending.push(action);
914
+ }
915
+ }
916
+ const allApproved = denied.length === 0 && pending.length === 0;
917
+ // Create approval URL for pending actions (matches remote-mcp behavior)
918
+ if (pending.length > 0) {
919
+ try {
920
+ const approvalResult = await this.gatewayClient.createApprovalRequest({
921
+ subjectDid: this.context.agentDid,
922
+ projectId: this.context.projectId,
923
+ actions: pending,
924
+ });
925
+ if (!approvalResult.approvalUrl) {
926
+ throw new Error('Gateway returned empty approval URL');
927
+ }
928
+ return {
929
+ success: true,
930
+ data: {
931
+ approved,
932
+ denied,
933
+ pending,
934
+ requestId: approvalResult.requestId,
935
+ approvalUrl: approvalResult.approvalUrl,
936
+ expiresAt: approvalResult.expiresAt,
937
+ status: 'pending',
938
+ message: approved.length > 0
939
+ ? `Approved: [${approved.join(', ')}]. Approval required for: [${pending.join(', ')}]. See approvalUrl field.`
940
+ : `Permission required for: [${pending.join(', ')}]. See approvalUrl field.`,
941
+ },
942
+ };
943
+ }
944
+ catch (err) {
945
+ // Fallback: return pending without URL if approval request creation fails
946
+ return {
947
+ success: true,
948
+ data: {
949
+ approved,
950
+ denied,
951
+ pending,
952
+ status: 'partial',
953
+ message: `Approved: [${approved.join(', ')}]. Pending: [${pending.join(', ')}]. Denied: [${denied.join(', ')}]. Failed to create approval URL: ${err instanceof Error ? err.message : String(err)}. Use aidentity_call_tool for pending actions — you will be prompted to approve.`,
954
+ },
955
+ };
956
+ }
957
+ }
958
+ return {
959
+ success: true,
960
+ data: {
961
+ approved,
962
+ denied,
963
+ pending,
964
+ status: allApproved ? 'approved' : 'partial',
965
+ message: allApproved
966
+ ? 'All requested permissions have been granted. You can now use aidentity_call_tool.'
967
+ : `Approved: [${approved.join(', ')}]. Denied: [${denied.join(', ')}].`,
968
+ },
969
+ };
970
+ }
971
+ async listAvailableTools() {
972
+ const tools = ai_identity_1.ACTION_REGISTRY.actions.map(a => ({
973
+ action: a.action,
974
+ resource_type: a.resource_type,
975
+ risk: a.risk,
976
+ input_schema: a.input_schema,
977
+ }));
978
+ return { success: true, data: { tools, count: tools.length } };
979
+ }
980
+ /**
981
+ * Resolve env vars for os.process.run from .env file.
982
+ * Returns empty record if no env_profile specified or .env not found.
983
+ */
984
+ async resolveProcessEnv(params) {
985
+ if (!this.secretBackend || !params.env_profile) {
986
+ return {};
987
+ }
988
+ const envPath = path.join(params.working_directory || process.cwd(), '.env');
989
+ try {
990
+ const content = fs.readFileSync(envPath, 'utf-8');
991
+ const envResolver = new env_resolver_1.EnvResolver(this.secretBackend);
992
+ const { env, warnings } = await envResolver.resolveEnvContentWithWarnings(content);
993
+ for (const w of warnings) {
994
+ process.stderr.write(`[vess] Warning: ${w}\n`);
995
+ }
996
+ return env;
997
+ }
998
+ catch (err) {
999
+ if (err?.code === 'ENOENT') {
1000
+ process.stderr.write(`[vess] Warning: env_profile "${params.env_profile}" requested but .env not found at ${envPath}\n`);
1001
+ return {};
1002
+ }
1003
+ // Resolution errors (keychain failures, etc.) should propagate
1004
+ throw err;
1005
+ }
1006
+ }
1007
+ getActionRiskLevel(action) {
1008
+ const actionDef = ai_identity_1.ACTION_REGISTRY.actions.find(a => a.action === action);
1009
+ if (!actionDef) {
1010
+ process.stderr.write(`[vess] WARNING: action "${action}" not found in ACTION_REGISTRY, defaulting to high risk\n`);
1011
+ return 'high';
1012
+ }
1013
+ return actionDef.risk || 'high';
1014
+ }
1015
+ getDefaultVCTTL(riskLevel) {
1016
+ const defaults = {
1017
+ low: { minutes: 60, label: '1 hour (low risk default)' },
1018
+ medium: { minutes: 30, label: '30 minutes (medium risk default)' },
1019
+ high: { minutes: 10, label: '10 minutes (high risk default)' },
1020
+ critical: { minutes: 5, label: '5 minutes (critical risk default)' },
1021
+ };
1022
+ return defaults[riskLevel];
1023
+ }
1024
+ async handleApprovalResponse(request, provider, normalizedResource, resourceFingerprint, requestedResource) {
1025
+ const { action, approval } = request;
1026
+ if (!approval)
1027
+ return { success: false, error: 'No approval provided' };
1028
+ // Verify token (with resource binding for both local and SaaS actions)
1029
+ const extractedResource = this.extractResourceIdFromInput(action, request.input);
1030
+ const expectedResource = normalizedResource ?? extractedResource;
1031
+ const tokenResult = this.tokenService.verify(approval.token, action, expectedResource);
1032
+ if (!tokenResult.valid) {
1033
+ return { success: false, error: `Invalid approval token: ${tokenResult.error}` };
1034
+ }
1035
+ // Validate choice
1036
+ const validChoices = ['one_time', 'persistent', 'deny_once', 'deny_persistent'];
1037
+ if (!validChoices.includes(approval.choice)) {
1038
+ return { success: false, error: `Invalid approval choice: ${approval.choice}` };
1039
+ }
1040
+ // Handle deny choices
1041
+ if (approval.choice === 'deny_once') {
1042
+ this.logAudit({
1043
+ action, provider,
1044
+ decision: 'deny',
1045
+ enforcementType: 'local',
1046
+ decisionSource: 'local_policy',
1047
+ decisionReason: 'User denied the action (deny_once)',
1048
+ requestedResource, normalizedResource, resourceFingerprint,
1049
+ executionType: (0, executor_registry_1.isLocalAction)(action) ? 'local' : 'gateway',
1050
+ approvalNonce: tokenResult.nonce,
1051
+ });
1052
+ return { success: false, error: 'User denied the action (this time only)' };
1053
+ }
1054
+ if (approval.choice === 'deny_persistent') {
1055
+ this.policyEvaluator.addDenyRule({
1056
+ actions: [action],
1057
+ effect: 'deny',
1058
+ source: 'user',
1059
+ });
1060
+ this.logAudit({
1061
+ action, provider,
1062
+ decision: 'deny',
1063
+ enforcementType: 'local',
1064
+ decisionSource: 'local_policy',
1065
+ decisionReason: 'User denied the action (deny_persistent)',
1066
+ requestedResource, normalizedResource, resourceFingerprint,
1067
+ executionType: (0, executor_registry_1.isLocalAction)(action) ? 'local' : 'gateway',
1068
+ approvalNonce: tokenResult.nonce,
1069
+ });
1070
+ return { success: false, error: 'User denied the action (persistent deny rule added)' };
1071
+ }
1072
+ // Approved (one_time or persistent) — call quickApprove
1073
+ const approvalMode = approval.choice === 'one_time' ? 'one_time' : 'persistent';
1074
+ const resourceInfo = normalizedResource && resourceFingerprint
1075
+ ? { normalizedResource, fingerprint: resourceFingerprint }
1076
+ : undefined;
1077
+ // Determine VC TTL
1078
+ const riskLevel = this.getActionRiskLevel(action);
1079
+ const defaultTTL = this.getDefaultVCTTL(riskLevel);
1080
+ const vcTTLMinutes = approval.vcTTLMinutes
1081
+ ? Math.max(5, Math.min(1440, approval.vcTTLMinutes))
1082
+ : defaultTTL.minutes;
1083
+ const expiresInHours = vcTTLMinutes / 60;
1084
+ // Build resources: for local actions use pattern, for gateway actions extract resource ID from parameters
1085
+ const resourceType = this.getResourceType(action);
1086
+ let resources;
1087
+ if (resourceInfo) {
1088
+ resources = [{ type: resourceType, pattern: resourceInfo.normalizedResource }];
1089
+ }
1090
+ else {
1091
+ const extractedId = this.extractResourceIdFromInput(action, request.input);
1092
+ if (!extractedId && this.getActionRiskLevel(action) === 'high') {
1093
+ const actionDef = ai_identity_1.ACTION_REGISTRY.actions.find(a => a.action === action);
1094
+ const paramName = actionDef?.target_bindings?.resource_id?.param || 'resource ID';
1095
+ return {
1096
+ success: false,
1097
+ error: `High-risk action "${action}" requires a specific ${paramName}. Cannot approve with wildcard resource scope.`,
1098
+ };
1099
+ }
1100
+ resources = extractedId
1101
+ ? [{ type: resourceType, id: extractedId }]
1102
+ : [{ type: resourceType }];
1103
+ }
1104
+ try {
1105
+ const quickResult = await this.gatewayClient.quickApprove({
1106
+ actions: [action],
1107
+ resources,
1108
+ normalizedResource: resourceInfo?.normalizedResource,
1109
+ resourceFingerprint: resourceInfo?.fingerprint,
1110
+ subjectDid: this.context.agentDid,
1111
+ projectId: this.context.projectId,
1112
+ issueVC: true,
1113
+ approvalMode,
1114
+ approvalNonce: tokenResult.nonce,
1115
+ expiresInHours,
1116
+ });
1117
+ const stored = this.wallet.storeCredential({
1118
+ holderDid: this.context.agentDid,
1119
+ projectId: this.context.projectId,
1120
+ credentialJwt: quickResult.credential,
1121
+ actions: quickResult.grant.actions,
1122
+ provider,
1123
+ expiresAt: new Date(quickResult.expiresAt).getTime(),
1124
+ normalizedResourceKey: resourceInfo?.normalizedResource,
1125
+ resourceFingerprint: resourceInfo?.fingerprint,
1126
+ resources: (0, wallet_1.validateResources)(quickResult.resources),
1127
+ });
1128
+ // Build VCAcquired from quickApprove result
1129
+ const vcAcquired = {
1130
+ credential: stored,
1131
+ grantId: quickResult.grant.id,
1132
+ approvalMode: approvalMode,
1133
+ approvalNonce: tokenResult.nonce,
1134
+ decisionSource: 'new_vc_from_quick_approve',
1135
+ };
1136
+ // Now execute with the freshly acquired VC (skips double ensureVC)
1137
+ const routing = (0, executor_registry_1.determineRouting)(provider, action);
1138
+ let executeResult;
1139
+ try {
1140
+ if (routing.executor === 'local') {
1141
+ executeResult = await this.executeLocalWithVC(action, request.input, provider, vcAcquired, normalizedResource, resourceFingerprint, requestedResource);
1142
+ }
1143
+ else {
1144
+ executeResult = await this.executeViaGatewayWithVC(provider, action, request.input, vcAcquired);
1145
+ }
1146
+ }
1147
+ finally {
1148
+ // Wallet cleanup for one-time grants (regardless of execution success — spec requirement)
1149
+ if (approvalMode === 'one_time' && stored.id) {
1150
+ this.wallet.revokeCredential(stored.id);
1151
+ }
1152
+ }
1153
+ return executeResult;
1154
+ }
1155
+ catch (err) {
1156
+ return {
1157
+ success: false,
1158
+ error: `Quick-approve failed: ${err instanceof Error ? err.message : String(err)}`,
1159
+ };
1160
+ }
1161
+ }
1162
+ getResourceType(action) {
1163
+ const actionDef = ai_identity_1.ACTION_REGISTRY.actions.find(a => a.action === action);
1164
+ if (actionDef) {
1165
+ const parts = actionDef.resource_type.split(':');
1166
+ return parts.length > 1 ? parts[1] : parts[0];
1167
+ }
1168
+ return '*';
1169
+ }
1170
+ /**
1171
+ * Extract specific resource ID from action input using ACTION_REGISTRY target_bindings.
1172
+ * For gateway (SaaS) actions, the resource ID (e.g., channel ID) is in the input parameters.
1173
+ * Returns undefined for actions without param-sourced bindings or when the param is absent
1174
+ * (e.g., target_bindings with required: false).
1175
+ */
1176
+ extractResourceIdFromInput(action, input) {
1177
+ const actionDef = ai_identity_1.ACTION_REGISTRY.actions.find(a => a.action === action);
1178
+ if (!actionDef?.target_bindings?.resource_id)
1179
+ return undefined;
1180
+ const binding = actionDef.target_bindings.resource_id;
1181
+ if (binding.source !== 'param')
1182
+ return undefined;
1183
+ const value = input[binding.param];
1184
+ if (!value || typeof value !== 'string')
1185
+ return undefined;
1186
+ // Defense-in-depth: reject control characters and excessively long values
1187
+ const MAX_RESOURCE_ID_LENGTH = 500;
1188
+ if (value.length > MAX_RESOURCE_ID_LENGTH || /[\x00-\x1f]/.test(value))
1189
+ return undefined;
1190
+ return value;
1191
+ }
1192
+ isRecentOOBRequest(action, resourceId) {
1193
+ const key = `${action}:${resourceId || '*'}`;
1194
+ const lastTime = this.recentOOBRequests.get(key);
1195
+ return !!lastTime && (Date.now() - lastTime) < ExecutionEngine.OOB_COOLDOWN_MS;
1196
+ }
1197
+ trackOOBRequest(action, resourceId) {
1198
+ const key = `${action}:${resourceId || '*'}`;
1199
+ this.recentOOBRequests.set(key, Date.now());
1200
+ if (this.recentOOBRequests.size > ExecutionEngine.OOB_MAP_MAX_SIZE) {
1201
+ const now = Date.now();
1202
+ for (const [k, t] of this.recentOOBRequests) {
1203
+ if (now - t > ExecutionEngine.OOB_COOLDOWN_MS) {
1204
+ this.recentOOBRequests.delete(k);
1205
+ }
1206
+ }
1207
+ }
1208
+ }
1209
+ /** Verify a wallet VC: check revocation via Gateway, handle expiry. Returns the VC if valid, null if invalid. */
1210
+ async verifyWalletVC(vc) {
1211
+ // Check local expiration first — avoids sending expired VCs to the API
1212
+ // Check both wallet expiresAt and JWT exp claim (JWT exp is in seconds, convert to ms)
1213
+ const jwtExp = (0, vc_utils_1.extractExpFromVC)(vc.credentialJwt);
1214
+ const effectiveExpiresAt = vc.expiresAt ?? (jwtExp !== null ? jwtExp * 1000 : null);
1215
+ if (effectiveExpiresAt && effectiveExpiresAt <= Date.now()) {
1216
+ this.wallet.revokeCredential(vc.id);
1217
+ return null;
1218
+ }
1219
+ const jti = (0, vc_utils_1.extractJtiFromVC)(vc.credentialJwt);
1220
+ if (jti) {
1221
+ try {
1222
+ const status = await this.gatewayClient.checkVCStatus(jti);
1223
+ if (!status.valid) {
1224
+ this.wallet.revokeCredential(vc.id);
1225
+ return null;
1226
+ }
1227
+ return vc;
1228
+ }
1229
+ catch {
1230
+ // Gateway unreachable — use cached VC if not expired (already checked above)
1231
+ return vc;
1232
+ }
1233
+ }
1234
+ // No jti extractable — backward compat
1235
+ return vc;
1236
+ }
1237
+ async handleResourceMismatch(action, provider, params, vcResult) {
1238
+ const extractedId = this.extractResourceIdFromInput(action, params);
1239
+ if (this.isRecentOOBRequest(action, extractedId)) {
1240
+ return {
1241
+ success: false,
1242
+ error: 'Resource access denied. An approval request was recently created. Please approve it or wait before retrying.',
1243
+ };
1244
+ }
1245
+ this.logAudit({
1246
+ action, provider,
1247
+ decision: 'deny',
1248
+ enforcementType: 'gateway',
1249
+ decisionSource: 'gateway_execution',
1250
+ decisionReason: 'Resource mismatch — VC scope does not cover target resource',
1251
+ executionType: 'gateway',
1252
+ credentialId: vcResult.credential.id,
1253
+ });
1254
+ try {
1255
+ const riskLevel = this.getActionRiskLevel(action);
1256
+ const oobResult = await this.createOOBApprovalRequest(action, provider, riskLevel, undefined, params);
1257
+ this.trackOOBRequest(action, extractedId);
1258
+ return { success: false, waitingForApproval: oobResult.waitingForApproval };
1259
+ }
1260
+ catch (err) {
1261
+ return {
1262
+ success: false,
1263
+ error: `Resource mismatch detected but failed to create approval request: ${err instanceof Error ? err.message : String(err)}`,
1264
+ };
1265
+ }
1266
+ }
1267
+ logAudit(params) {
1268
+ this.auditLogger?.logEvent({
1269
+ action: params.action,
1270
+ provider: params.provider,
1271
+ executionType: params.executionType,
1272
+ decision: params.decision,
1273
+ enforcementType: params.enforcementType,
1274
+ decisionSource: params.decisionSource,
1275
+ decisionReason: params.decisionReason,
1276
+ rootDid: this.context.rootDid,
1277
+ agentDid: this.context.agentDid,
1278
+ presenterDid: this.context.agentDid,
1279
+ projectId: this.context.projectId,
1280
+ requestedResource: params.requestedResource,
1281
+ normalizedResource: params.normalizedResource,
1282
+ resourceFingerprint: params.resourceFingerprint,
1283
+ grantId: params.grantId,
1284
+ credentialId: params.credentialId,
1285
+ approvalMode: params.approvalMode,
1286
+ approvalNonce: params.approvalNonce,
1287
+ });
1288
+ }
1289
+ }
1290
+ exports.ExecutionEngine = ExecutionEngine;
1291
+ //# sourceMappingURL=execution-engine.js.map