@the-ai-company/cbio-node-runtime 1.63.3 → 1.63.6

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 (233) hide show
  1. package/README.md +48 -209
  2. package/dist/clients/agent/client.d.ts +18 -40
  3. package/dist/clients/agent/client.js +22 -109
  4. package/dist/clients/agent/client.js.map +1 -1
  5. package/dist/clients/agent/contracts.d.ts +1 -8
  6. package/dist/clients/agent/index.d.ts +1 -1
  7. package/dist/clients/owner/client.d.ts +2 -102
  8. package/dist/clients/owner/client.js +111 -266
  9. package/dist/clients/owner/client.js.map +1 -1
  10. package/dist/clients/owner/contracts.d.ts +37 -75
  11. package/dist/clients/owner/index.d.ts +2 -4
  12. package/dist/clients/owner/index.js +1 -2
  13. package/dist/clients/owner/index.js.map +1 -1
  14. package/dist/internal/id-factory.d.ts +0 -2
  15. package/dist/internal/id-factory.js +0 -6
  16. package/dist/internal/id-factory.js.map +1 -1
  17. package/dist/protocol/identity.d.ts +1 -1
  18. package/dist/protocol/identity.js +3 -3
  19. package/dist/protocol/identity.js.map +1 -1
  20. package/dist/public-types.d.ts +5 -14
  21. package/dist/public-types.js +1 -8
  22. package/dist/public-types.js.map +1 -1
  23. package/dist/runtime/bootstrap.d.ts +1 -3
  24. package/dist/runtime/bootstrap.js.map +1 -1
  25. package/dist/runtime/identity.d.ts +2 -2
  26. package/dist/runtime/identity.js +3 -5
  27. package/dist/runtime/identity.js.map +1 -1
  28. package/dist/runtime/index.d.ts +10 -12
  29. package/dist/runtime/index.js +7 -8
  30. package/dist/runtime/index.js.map +1 -1
  31. package/dist/runtime/owner-session.d.ts +7 -6
  32. package/dist/runtime/owner-session.js +5 -6
  33. package/dist/runtime/owner-session.js.map +1 -1
  34. package/dist/storage/fs.d.ts +3 -2
  35. package/dist/storage/fs.js +8 -5
  36. package/dist/storage/fs.js.map +1 -1
  37. package/dist/storage/prefix.d.ts +1 -0
  38. package/dist/storage/prefix.js +7 -0
  39. package/dist/storage/prefix.js.map +1 -1
  40. package/dist/storage/provider.d.ts +2 -0
  41. package/dist/vault-core/contracts.d.ts +95 -210
  42. package/dist/vault-core/contracts.js +8 -11
  43. package/dist/vault-core/contracts.js.map +1 -1
  44. package/dist/vault-core/core.d.ts +119 -62
  45. package/dist/vault-core/core.js +518 -1180
  46. package/dist/vault-core/core.js.map +1 -1
  47. package/dist/vault-core/defaults.d.ts +22 -44
  48. package/dist/vault-core/defaults.js +65 -234
  49. package/dist/vault-core/defaults.js.map +1 -1
  50. package/dist/vault-core/errors.d.ts +3 -2
  51. package/dist/vault-core/errors.js.map +1 -1
  52. package/dist/vault-core/index.d.ts +5 -5
  53. package/dist/vault-core/index.js +2 -2
  54. package/dist/vault-core/index.js.map +1 -1
  55. package/dist/vault-core/persistence.d.ts +72 -119
  56. package/dist/vault-core/persistence.js +310 -427
  57. package/dist/vault-core/persistence.js.map +1 -1
  58. package/dist/vault-core/ports.d.ts +19 -30
  59. package/dist/vault-core/read-policy.d.ts +3 -2
  60. package/dist/vault-core/read-policy.js.map +1 -1
  61. package/dist/vault-core/tool-metadata.js +2 -2
  62. package/dist/vault-core/tool-metadata.js.map +1 -1
  63. package/dist/vault-ingress/defaults.d.ts +4 -2
  64. package/dist/vault-ingress/defaults.js +14 -8
  65. package/dist/vault-ingress/defaults.js.map +1 -1
  66. package/dist/vault-ingress/index.d.ts +39 -119
  67. package/dist/vault-ingress/index.js +98 -456
  68. package/dist/vault-ingress/index.js.map +1 -1
  69. package/dist/vault-ingress/remote-transport.d.ts +5 -3
  70. package/dist/vault-ingress/remote-transport.js +8 -28
  71. package/dist/vault-ingress/remote-transport.js.map +1 -1
  72. package/docs/ARCHITECTURE.md +39 -22
  73. package/docs/CUSTODY_MODEL.md +1 -1
  74. package/docs/IDENTITY_MODEL.md +5 -5
  75. package/docs/MIGRATION-1.51.md +19 -19
  76. package/docs/MIGRATION-1.65.md +87 -0
  77. package/docs/PROCESS_ISOLATION.md +2 -2
  78. package/docs/REFERENCE.md +42 -224
  79. package/docs/api/README.md +48 -30
  80. package/docs/api/classes/IdentityError.md +1 -1
  81. package/docs/api/classes/OwnerClientError.md +1 -1
  82. package/docs/api/classes/PersistentVaultAgentIdentityRegistry.md +89 -0
  83. package/docs/api/classes/PersistentVaultAgentSecretGrantRegistry.md +125 -0
  84. package/docs/api/classes/PersistentVaultAuditLog.md +65 -0
  85. package/docs/api/classes/PersistentVaultSecretCustody.md +93 -0
  86. package/docs/api/classes/PersistentVaultSecretDestinationGrantRegistry.md +125 -0
  87. package/docs/api/classes/PersistentVaultSecretRepository.md +127 -0
  88. package/docs/api/classes/VaultCore.md +264 -237
  89. package/docs/api/classes/VaultCoreError.md +3 -3
  90. package/docs/api/enumerations/AuditAction.md +143 -0
  91. package/docs/api/enumerations/AuditOutcome.md +35 -0
  92. package/docs/api/enumerations/DispatchStatus.md +35 -0
  93. package/docs/api/enumerations/IdentityErrorCode.md +1 -1
  94. package/docs/api/enumerations/OwnerClientErrorCode.md +1 -1
  95. package/docs/api/functions/createAgentClient.md +1 -15
  96. package/docs/api/functions/createIdentity.md +2 -2
  97. package/docs/api/functions/createOwnerClient.md +17 -0
  98. package/docs/api/functions/createOwnerSession.md +1 -1
  99. package/docs/api/functions/createPersistentVaultCoreDependencies.md +4 -4
  100. package/docs/api/functions/createVault.md +1 -1
  101. package/docs/api/functions/createVaultCore.md +1 -1
  102. package/docs/api/functions/createVaultCoreDependencies.md +1 -1
  103. package/docs/api/functions/createVaultService.md +5 -13
  104. package/docs/api/functions/createWorkspaceStorage.md +1 -1
  105. package/docs/api/functions/deriveRootAgentId.md +17 -0
  106. package/docs/api/functions/deriveVaultWorkingKeyFromPassword.md +1 -1
  107. package/docs/api/functions/getDefaultWorkspaceDir.md +1 -1
  108. package/docs/api/functions/handleVaultAgentControlHttp.md +2 -2
  109. package/docs/api/functions/handleVaultHttpDispatch.md +2 -2
  110. package/docs/api/functions/initializeVaultCustody.md +7 -3
  111. package/docs/api/functions/listVaults.md +1 -1
  112. package/docs/api/functions/readVaultProfile.md +1 -1
  113. package/docs/api/functions/recoverVault.md +1 -1
  114. package/docs/api/functions/recoverVaultWorkingKey.md +4 -8
  115. package/docs/api/functions/restoreIdentity.md +1 -1
  116. package/docs/api/functions/updateVaultMetadata.md +1 -1
  117. package/docs/api/functions/writeVaultProfile.md +1 -1
  118. package/docs/api/interfaces/AgentClient.md +20 -59
  119. package/docs/api/interfaces/AgentDispatchIntent.md +1 -1
  120. package/docs/api/interfaces/AgentDispatchTransport.md +12 -44
  121. package/docs/api/interfaces/AgentIdentity.md +3 -3
  122. package/docs/api/interfaces/AgentIdentityRecord.md +47 -0
  123. package/docs/api/interfaces/AgentRequestResult.md +35 -0
  124. package/docs/api/interfaces/AgentRuntimeManifest.md +55 -0
  125. package/docs/api/interfaces/AgentSecretGrant.md +41 -0
  126. package/docs/api/interfaces/AgentSigner.md +1 -1
  127. package/docs/api/interfaces/AgentVisibleRequestRecord.md +53 -0
  128. package/docs/api/interfaces/AgentVisibleSecretRecord.md +65 -0
  129. package/docs/api/interfaces/AuditEntry.md +83 -0
  130. package/docs/api/interfaces/CbioRuntime.md +13 -154
  131. package/docs/api/interfaces/CreateAgentClientOptions.md +4 -10
  132. package/docs/api/interfaces/CreateIdentityOptions.md +1 -1
  133. package/docs/api/interfaces/{CreateVaultClientOptions.md → CreateOwnerClientOptions.md} +9 -11
  134. package/docs/api/interfaces/CreateOwnerSessionOptions.md +3 -121
  135. package/docs/api/interfaces/CreatePersistentVaultCoreDependenciesOptions.md +3 -131
  136. package/docs/api/interfaces/CreateVaultOptions.md +1 -125
  137. package/docs/api/interfaces/CreatedVault.md +2 -2
  138. package/docs/api/interfaces/DefaultPolicyEngineOptions.md +1 -13
  139. package/docs/api/interfaces/DispatchAuthorization.md +43 -0
  140. package/docs/api/interfaces/DispatchInstruction.md +47 -0
  141. package/docs/api/interfaces/DispatchRequest.md +83 -0
  142. package/docs/api/interfaces/DispatchResult.md +53 -0
  143. package/docs/api/interfaces/IStorageProvider.md +13 -1
  144. package/docs/api/interfaces/InitializeVaultCustodyOptions.md +31 -11
  145. package/docs/api/interfaces/InitializedVaultCustody.md +1 -7
  146. package/docs/api/interfaces/OwnerAgentProvisionResult.md +2 -2
  147. package/docs/api/interfaces/OwnerClient.md +401 -0
  148. package/docs/api/interfaces/OwnerCreateSecretInput.md +1 -1
  149. package/docs/api/interfaces/OwnerRemoveSecretInput.md +1 -1
  150. package/docs/api/interfaces/OwnerRequestRecord.md +97 -0
  151. package/docs/api/interfaces/OwnerSensitiveActionConfirmation.md +1 -1
  152. package/docs/api/interfaces/OwnerSensitiveActionContext.md +1 -1
  153. package/docs/api/interfaces/OwnerSession.md +3 -3
  154. package/docs/api/interfaces/OwnerUpdateSecretInput.md +1 -1
  155. package/docs/api/interfaces/OwnerVisibleRequestRecord.md +73 -0
  156. package/docs/api/interfaces/RecoverVaultOptions.md +1 -125
  157. package/docs/api/interfaces/RecoveredVault.md +2 -2
  158. package/docs/api/interfaces/RequestRecord.md +107 -0
  159. package/docs/api/interfaces/RestoreIdentityOptions.md +1 -1
  160. package/docs/api/interfaces/SecretAlias.md +11 -0
  161. package/docs/api/interfaces/SecretDestinationGrant.md +41 -0
  162. package/docs/api/interfaces/SecretId.md +11 -0
  163. package/docs/api/interfaces/SecretRecord.md +89 -0
  164. package/docs/api/interfaces/Signer.md +1 -1
  165. package/docs/api/interfaces/VaultApproveDispatchInput.md +3 -9
  166. package/docs/api/interfaces/VaultAuditQueryInput.md +1 -1
  167. package/docs/api/interfaces/VaultCoreDependenciesOptions.md +1 -5
  168. package/docs/api/interfaces/VaultCreateAgentInput.md +1 -1
  169. package/docs/api/interfaces/VaultExportSecretInput.md +1 -1
  170. package/docs/api/interfaces/VaultGetRequestInput.md +17 -0
  171. package/docs/api/interfaces/VaultGrantAgentSecretInput.md +23 -0
  172. package/docs/api/interfaces/VaultGrantSecretDestinationInput.md +23 -0
  173. package/docs/api/interfaces/VaultId.md +11 -0
  174. package/docs/api/interfaces/VaultImportAgentInput.md +1 -1
  175. package/docs/api/interfaces/VaultIssueSessionTokenInput.md +5 -5
  176. package/docs/api/interfaces/VaultListAgentsInput.md +1 -1
  177. package/docs/api/interfaces/VaultListGrantsInput.md +23 -0
  178. package/docs/api/interfaces/VaultListRequestsInput.md +17 -0
  179. package/docs/api/interfaces/VaultListSecretsInput.md +1 -1
  180. package/docs/api/interfaces/VaultMetadata.md +1 -1
  181. package/docs/api/interfaces/VaultObject.md +2 -2
  182. package/docs/api/interfaces/VaultPrincipal.md +17 -0
  183. package/docs/api/interfaces/VaultProfile.md +1 -1
  184. package/docs/api/interfaces/VaultReadAgentPrivateKeyInput.md +7 -7
  185. package/docs/api/interfaces/VaultReadSecretPlaintextInput.md +1 -1
  186. package/docs/api/interfaces/VaultRevokeAgentSecretInput.md +23 -0
  187. package/docs/api/interfaces/VaultRevokeSecretDestinationInput.md +23 -0
  188. package/docs/api/interfaces/VaultRevokeSessionTokenInput.md +1 -1
  189. package/docs/api/interfaces/VaultService.md +511 -0
  190. package/docs/api/interfaces/VaultUpdateAgentInput.md +7 -7
  191. package/docs/api/type-aliases/AgentId.md +7 -0
  192. package/docs/api/type-aliases/CbioRuntimeModule.md +1 -1
  193. package/docs/api/type-aliases/DispatchApprovalDecision.md +7 -0
  194. package/docs/api/type-aliases/GrantStatus.md +7 -0
  195. package/docs/api/type-aliases/SecretLifecycleStatus.md +7 -0
  196. package/docs/api/type-aliases/VaultPrincipalKind.md +7 -0
  197. package/docs/api/variables/DEFAULT_VAULT_KEY_CUSTODY_BLOB_KEY.md +2 -2
  198. package/docs/es/README.md +3 -3
  199. package/docs/fr/README.md +3 -3
  200. package/docs/ja/README.md +5 -5
  201. package/docs/ko/README.md +5 -5
  202. package/docs/pt/README.md +3 -3
  203. package/docs/zh/PROCESS_ISOLATION.md +2 -2
  204. package/docs/zh/README.md +47 -63
  205. package/examples/process-isolation.ts +26 -35
  206. package/package.json +1 -1
  207. package/docs/api/functions/createOwnerHttpFlowBoundary.md +0 -17
  208. package/docs/api/functions/createStandardAcquireBoundary.md +0 -31
  209. package/docs/api/functions/createStandardDispatchBoundary.md +0 -23
  210. package/docs/api/functions/createVaultClient.md +0 -32
  211. package/docs/api/functions/deriveIdentityId.md +0 -17
  212. package/docs/api/functions/wrapVaultCoreAsVaultService.md +0 -31
  213. package/docs/api/interfaces/AgentSubmitCapabilityRequestInput.md +0 -41
  214. package/docs/api/interfaces/VaultApproveCapabilityRequestInput.md +0 -23
  215. package/docs/api/interfaces/VaultClient.md +0 -473
  216. package/docs/api/interfaces/VaultGrantCapabilityInput.md +0 -79
  217. package/docs/api/interfaces/VaultGrantCapabilityRequest.md +0 -23
  218. package/docs/api/interfaces/VaultIdentity.md +0 -11
  219. package/docs/api/interfaces/VaultListCapabilitiesInput.md +0 -17
  220. package/docs/api/interfaces/VaultRegisterFlowInput.md +0 -77
  221. package/docs/api/interfaces/VaultRevokeCapabilityInput.md +0 -23
  222. package/docs/api/interfaces/VaultSigner.md +0 -21
  223. package/docs/api/interfaces/VaultSubmitCapabilityRequestInput.md +0 -73
  224. package/docs/api/type-aliases/AgentCapabilityEnvelope.md +0 -7
  225. package/docs/api/type-aliases/AgentVisibleSecretRecord.md +0 -7
  226. package/docs/api/type-aliases/CreateOwnerClientOptions.md +0 -7
  227. package/docs/api/type-aliases/OwnerAgentView.md +0 -7
  228. package/docs/api/type-aliases/OwnerClient.md +0 -13
  229. package/docs/api/type-aliases/OwnerGrantCapabilityInput.md +0 -7
  230. package/docs/api/type-aliases/OwnerPendingApprovalView.md +0 -7
  231. package/docs/api/type-aliases/OwnerRequestDetailView.md +0 -7
  232. package/docs/api/type-aliases/OwnerRequestSummaryView.md +0 -7
  233. package/docs/api/type-aliases/OwnerSecretView.md +0 -7
@@ -1,1067 +1,358 @@
1
1
  import { AuditAction, AuditOutcome, DispatchStatus, } from "./contracts.js";
2
2
  import { VaultCoreError } from "./errors.js";
3
- import { verifySignature } from "../protocol/crypto.js";
3
+ import { applyResponseReadPolicy } from "./read-policy.js";
4
4
  import { getAgentToolbox } from "./tool-metadata.js";
5
5
  import { InMemoryRequestRecordRegistry } from "./defaults.js";
6
- import { applyResponseReadPolicy } from "./read-policy.js";
7
- const VAULT_MASTER_ID = "vault-master";
8
- function toAuditEntry(deps, actor, action, outcome, detail, options) {
6
+ function isScopeMatch(scope, url) {
7
+ if (scope === "*")
8
+ return true;
9
+ const regex = new RegExp("^" + scope.replace(/\*/g, ".*") + "$");
10
+ return regex.test(url);
11
+ }
12
+ function extractDomain(url) {
13
+ try {
14
+ const parsed = new URL(url);
15
+ return parsed.hostname;
16
+ }
17
+ catch {
18
+ return url;
19
+ }
20
+ }
21
+ function toAuditEntry(deps, actor, action, outcome, detail, extra = {}) {
9
22
  return {
10
23
  entryId: deps.ids.newAuditEntryId(),
11
24
  occurredAt: deps.clock.nowIso(),
12
- vaultId: deps.vaultId.value,
25
+ vaultId: deps.vaultId,
13
26
  actor,
14
27
  action,
15
28
  outcome,
16
29
  detail,
17
- requestId: options?.requestId,
18
- capabilityId: options?.capabilityId,
19
- operation: options?.operation ?? action,
20
- targetUrl: options?.targetUrl,
21
- secretAlias: options?.secretAlias,
22
- secretId: options?.secretId,
23
- agentId: options?.agentId,
24
- };
25
- }
26
- function buildSecretRecord(deps, command, previousRecord) {
27
- const now = deps.clock.nowIso();
28
- const source = command.source?.kind === "request" && command.source.requestId
29
- ? { kind: "request", requestId: command.source.requestId }
30
- : { kind: "manual" };
31
- const previousVersion = previousRecord ? Number.parseInt(previousRecord.version.value, 10) : 0;
32
- const nextVersion = Number.isFinite(previousVersion) ? previousVersion + 1 : 1;
33
- return {
34
- vaultId: deps.vaultId,
35
- secretId: deps.ids.newSecretId(),
36
- alias: { value: command.alias },
37
- version: { value: String(nextVersion) },
38
- lifecycleStatus: "ACTIVE",
39
- previousSecretId: previousRecord?.secretId,
40
- issuerId: command.kind === "issuer.write_secret" ? command.issuerSiteId : null,
41
- source,
42
- createdAt: now,
43
- updatedAt: now,
30
+ ...extra,
44
31
  };
45
32
  }
46
- function isSecretActive(record) {
47
- if (record.lifecycleStatus) {
48
- return record.lifecycleStatus === "ACTIVE";
49
- }
50
- return !record.retiredAt;
51
- }
52
- function normalizeScopeTarget(targetUrl) {
53
- try {
54
- const parsed = new URL(targetUrl);
55
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
56
- return null;
57
- }
58
- parsed.protocol = parsed.protocol.toLowerCase();
59
- parsed.hostname = parsed.hostname.toLowerCase();
60
- parsed.hash = "";
61
- parsed.search = "";
62
- if ((parsed.protocol === "https:" && parsed.port === "443") || (parsed.protocol === "http:" && parsed.port === "80")) {
63
- parsed.port = "";
64
- }
65
- parsed.pathname = parsed.pathname || "/";
66
- return parsed.toString();
67
- }
68
- catch {
69
- return null;
70
- }
71
- }
72
- function isScopeMatch(scope, targetUrl) {
73
- const normalizedTarget = normalizeScopeTarget(targetUrl);
74
- if (!normalizedTarget) {
75
- return false;
76
- }
77
- if (scope.endsWith("*")) {
78
- const normalizedPrefix = normalizeScopeTarget(scope.slice(0, -1));
79
- return normalizedPrefix ? normalizedTarget.startsWith(normalizedPrefix) : false;
80
- }
81
- const normalizedScope = normalizeScopeTarget(scope);
82
- return normalizedScope === normalizedTarget;
83
- }
84
- function createAgentControlBinding(requestId, requestedAt, agentId, action, payload = {}) {
85
- return JSON.stringify({
86
- requestId,
87
- requestedAt,
88
- agentId,
89
- action,
90
- ...payload,
91
- });
92
- }
93
- /**
94
- * The Sovereign Vault Core.
95
- * This is the primary implementation of the Vault logic.
96
- */
97
33
  export class VaultCore {
98
34
  _deps;
99
- _capabilityStateObservers = new Set();
100
- constructor(_deps) {
101
- this._deps = _deps;
102
- }
103
- _assertOwnerPrincipal(actor, code = "VAULT_AUDIT_DENIED") {
104
- if (actor.kind !== "owner" || actor.id !== VAULT_MASTER_ID) {
105
- throw new VaultCoreError("owner access denied", code);
106
- }
107
- }
108
- _stateToGrantedCapability(state) {
109
- return {
110
- vaultId: state.vaultId,
111
- capabilityId: state.capabilityId ?? "",
112
- agentId: state.agentId,
113
- operation: state.operation,
114
- customFlowId: state.customFlowId,
115
- write: {
116
- secretIds: state.write.secretIds ? [...state.write.secretIds] : undefined,
117
- scope: state.write.scope,
118
- methods: [...state.write.methods],
119
- },
120
- read: { paths: [...(state.readGrant ?? [])] },
121
- issuedAt: state.issuedAt ?? state.requestedAt,
122
- expiresAt: state.expiresAt,
123
- rateLimit: state.rateLimit,
124
- skipAudit: state.skipAudit,
125
- };
126
- }
127
- async _buildAgentCapabilityStates(agentId) {
128
- return (await this._deps.capabilityStates.list(this._deps.vaultId, agentId)).map((state) => ({
129
- source: state.source,
130
- agentId: state.agentId,
131
- requestId: state.requestId,
132
- capabilityId: state.capabilityId,
133
- operation: state.operation,
134
- customFlowId: state.customFlowId,
135
- write: {
136
- secretIds: state.write.secretIds ? [...state.write.secretIds] : undefined,
137
- scope: state.write.scope,
138
- methods: [...state.write.methods],
139
- },
140
- read: {
141
- paths: [...state.read.paths],
142
- },
143
- issuedAt: state.issuedAt,
144
- requestedAt: state.requestedAt,
145
- expiresAt: state.expiresAt,
146
- rateLimit: state.rateLimit,
147
- skipAudit: state.skipAudit,
148
- writeGrant: state.writeGrant,
149
- writeGrantedAt: state.writeGrantedAt,
150
- readGrant: state.readGrant ? [...state.readGrant] : null,
151
- readGrantedAt: state.readGrantedAt,
152
- reason: state.reason,
153
- secretId: state.secretId,
154
- targetUrl: state.targetUrl,
155
- }));
156
- }
157
- _isExecutablePendingState(state) {
158
- return !!(state.requestId && state.targetUrl && state.proof);
159
- }
160
- async _resolveRequestState(record) {
161
- const byRequestId = await this._deps.capabilityStates.getByRequestId(this._deps.vaultId, record.requestId);
162
- if (byRequestId) {
163
- return byRequestId;
164
- }
165
- if (record.capabilityId) {
166
- return this._deps.capabilityStates.getByCapabilityId(this._deps.vaultId, record.agentId, record.capabilityId);
167
- }
168
- return null;
169
- }
170
- async _executePendingCapabilityState(command, mode) {
171
- if (command.vaultId.value !== this._deps.vaultId.value) {
172
- throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
173
- }
174
- const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
175
- if (!pending) {
176
- throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
177
- }
178
- if (pending.writeGrant !== "once" && pending.writeGrant !== "always") {
179
- throw new VaultCoreError("write grant required before execution", "VAULT_WRITE_DENIED");
180
- }
181
- if (mode === "once" && pending.source !== "dispatch_discovery") {
182
- throw new VaultCoreError("one-time execution is only available for dispatch discovery requests", "VAULT_WRITE_DENIED");
183
- }
184
- const issuedAt = this._deps.clock.nowIso();
185
- const capability = {
186
- vaultId: this._deps.vaultId,
187
- agentId: pending.agentId,
188
- capabilityId: pending.capabilityId ?? this._deps.ids.newCapabilityId(),
189
- operation: pending.operation,
190
- customFlowId: pending.customFlowId,
191
- write: {
192
- secretIds: pending.write.secretIds ? [...pending.write.secretIds] : undefined,
193
- scope: pending.targetUrl ?? pending.write.scope,
194
- methods: [...pending.write.methods],
195
- },
196
- read: { paths: [...(pending.readGrant ?? [])] },
197
- issuedAt,
198
- expiresAt: pending.expiresAt,
199
- rateLimit: pending.rateLimit,
200
- skipAudit: pending.skipAudit,
201
- };
202
- let result;
203
- if (this._isExecutablePendingState(pending)) {
204
- result = await this.agentDispatchSecret({
205
- vaultId: this._deps.vaultId,
206
- agent: { kind: "agent", id: pending.agentId },
207
- capability,
208
- secretId: pending.secretId,
209
- targetUrl: pending.targetUrl,
210
- method: pending.write.methods[0] ?? "POST",
211
- headers: pending.headers,
212
- body: pending.body,
213
- proof: pending.proof,
214
- requestId: pending.requestId,
215
- requestedAt: pending.requestedAt,
216
- reason: pending.reason ?? "Approved previously requested dispatch.",
217
- skipReplayGuard: true,
218
- });
219
- }
220
- else if (mode === "grant") {
221
- result = {
222
- vaultId: this._deps.vaultId,
223
- requestId: pending.requestId ?? command.requestId,
224
- status: DispatchStatus.SUCCEEDED,
225
- targetUrl: pending.write.scope,
226
- method: pending.write.methods[0] ?? "POST",
227
- };
228
- }
229
- else {
230
- throw new VaultCoreError("pending capability state is not executable", "VAULT_WRITE_DENIED");
231
- }
232
- if (mode === "grant") {
233
- await this._deps.capabilityStates.upsert({
234
- ...pending,
235
- capabilityId: capability.capabilityId,
236
- source: "owner_grant",
237
- issuedAt,
238
- decidedAt: issuedAt,
239
- writeGrant: "always",
240
- writeGrantedAt: pending.writeGrantedAt ?? issuedAt,
241
- });
242
- }
243
- else {
244
- await this._deps.capabilityStates.deleteByRequestId(command.vaultId, command.requestId);
245
- }
246
- return result;
35
+ constructor(deps) {
36
+ this._deps = deps;
247
37
  }
248
38
  get vaultId() {
249
39
  return this._deps.vaultId;
250
40
  }
251
- async _appendAudit(entry) {
252
- try {
253
- await this._deps.audit.append(entry);
254
- }
255
- catch (error) {
256
- const message = error instanceof Error ? error.message : String(error);
257
- throw new VaultCoreError(`audit append failed: ${message}`, "VAULT_AUDIT_FAILED");
258
- }
259
- }
260
- async _appendDecisionAudit(request, outcome, detail, options) {
261
- await this._appendAudit(toAuditEntry(this._deps, request.agent, AuditAction.AUTHORIZE_DISPATCH, outcome, detail, {
262
- requestId: request.requestId,
263
- capabilityId: request.capability?.capabilityId,
264
- operation: request.capability?.operation ?? AuditAction.AUTHORIZE_DISPATCH,
265
- targetUrl: request.targetUrl,
266
- secretAlias: options?.secretAlias,
267
- secretId: options?.secretId ?? request.secretId,
268
- }));
269
- }
270
- async _verifyAgentControlProof(request, action, payload = {}) {
271
- if (request.proof.agentId !== request.agent.id) {
272
- throw new VaultCoreError("agent identity mismatch", "VAULT_DISPATCH_DENIED");
273
- }
274
- if (request.proof.token) {
275
- const valid = await this._deps.sessionTokens.verify(request.proof.token, request.agent.id);
276
- if (!valid) {
277
- throw new VaultCoreError("invalid or expired session token", "VAULT_DISPATCH_DENIED");
278
- }
279
- return;
280
- }
281
- if (!request.proof.signature) {
282
- throw new VaultCoreError("missing agent proof (signature or token required)", "VAULT_DISPATCH_DENIED");
283
- }
284
- if (request.proof.requestId !== request.requestId || request.proof.requestedAt !== request.requestedAt) {
285
- throw new VaultCoreError("proof binding mismatch", "VAULT_DISPATCH_DENIED");
286
- }
287
- const identity = await this._deps.agentIdentities.get(request.vaultId, request.agent.id);
288
- if (!identity) {
289
- throw new VaultCoreError("agent identity not registered", "VAULT_DISPATCH_DENIED");
290
- }
291
- const binding = createAgentControlBinding(request.requestId, request.requestedAt, request.agent.id, action, payload);
292
- if (!verifySignature(identity.publicKey, request.proof.signature, binding)) {
293
- throw new VaultCoreError("invalid proof signature", "VAULT_DISPATCH_DENIED");
41
+ _assertOwnerPrincipal(actor, errorCode = "VAULT_ACCESS_DENIED") {
42
+ if (actor.kind !== "owner") {
43
+ throw new VaultCoreError("owner principal required", errorCode);
294
44
  }
295
45
  }
296
- async _listVisibleSecretsForAgent(agentId) {
297
- const capabilities = (await this._deps.capabilityStates.list(this._deps.vaultId, agentId))
298
- .filter((state) => !!state.capabilityId && !!state.issuedAt && state.writeGrant === "always")
299
- .map((state) => this._stateToGrantedCapability(state));
300
- const capabilityMap = new Map();
301
- for (const capability of capabilities) {
302
- for (const secretId of capability.write.secretIds ?? []) {
303
- const existing = capabilityMap.get(secretId) ?? [];
304
- existing.push({
305
- capabilityId: capability.capabilityId,
306
- write: {
307
- secretIds: capability.write.secretIds ? [...capability.write.secretIds] : undefined,
308
- scope: capability.write.scope,
309
- methods: [...capability.write.methods],
310
- },
311
- read: {
312
- paths: [...capability.read.paths],
313
- },
314
- });
315
- capabilityMap.set(secretId, existing);
316
- }
317
- }
318
- const records = await this._deps.secrets.list(this._deps.vaultId);
319
- return records.map((record) => {
320
- const authorizedCapabilities = capabilityMap.get(record.secretId.value) ?? [];
321
- return {
322
- vaultId: record.vaultId,
323
- secretId: record.secretId,
324
- alias: record.alias,
325
- version: record.version,
326
- lifecycleStatus: record.lifecycleStatus ?? "ACTIVE",
327
- issuerId: record.issuerId,
328
- source: record.source,
329
- createdAt: record.createdAt,
330
- updatedAt: record.updatedAt,
331
- isAuthorizedForAgent: authorizedCapabilities.length > 0,
332
- authorizedCapabilities,
333
- };
334
- });
335
- }
336
- async _recordRequestExecution(request, capability, result) {
337
- await this._deps.requests.save({
338
- vaultId: this._deps.vaultId,
339
- requestId: request.requestId,
340
- agentId: request.agent.id,
341
- reason: request.reason,
342
- capabilityId: capability?.capabilityId,
343
- operation: capability?.operation ?? "dispatch_http",
344
- createdAt: this._deps.clock.nowIso(),
345
- request: {
346
- targetUrl: request.targetUrl,
347
- method: request.method,
348
- headers: request.headers,
349
- body: request.body,
350
- secretId: request.secretId,
351
- },
352
- response: {
353
- status: result.responseStatus,
354
- body: result.responseBody,
355
- error: result.error,
356
- },
357
- execution: {
358
- status: result.status,
359
- },
360
- });
361
- }
362
- toVisibleRequestRecord(record, state) {
363
- const readGrant = state?.readGrant ?? null;
364
- return {
365
- requestId: record.requestId,
366
- createdAt: record.createdAt,
367
- reason: record.reason,
368
- capabilityId: record.capabilityId,
369
- operation: record.operation,
370
- targetUrl: record.request.targetUrl,
371
- method: record.request.method,
372
- executionStatus: record.execution.status,
373
- responseStatus: record.response?.status,
374
- error: record.response?.error,
375
- readGrant,
376
- hasResponseBody: typeof record.response?.body === "string" && record.response.body.length > 0,
377
- resultVisible: typeof record.response?.body === "string" && record.response.body.length > 0,
378
- };
379
- }
380
- toOwnerVisibleRequestRecord(record, state) {
381
- return {
382
- requestId: record.requestId,
383
- createdAt: record.createdAt,
384
- agentId: record.agentId,
385
- reason: record.reason,
386
- capabilityId: record.capabilityId,
387
- operation: record.operation,
388
- targetUrl: record.request.targetUrl,
389
- method: record.request.method,
390
- executionStatus: record.execution.status,
391
- responseStatus: record.response?.status,
392
- error: record.response?.error,
393
- writeGrant: state?.writeGrant ?? null,
394
- readGrant: state?.readGrant ?? null,
395
- hasResponseBody: typeof record.response?.body === "string" && record.response.body.length > 0,
396
- };
397
- }
398
- toOwnerRequestRecord(record, state) {
399
- return {
400
- requestId: record.requestId,
401
- createdAt: record.createdAt,
402
- agentId: record.agentId,
403
- reason: record.reason,
404
- capabilityId: record.capabilityId,
405
- operation: record.operation,
406
- request: {
407
- targetUrl: record.request.targetUrl,
408
- method: record.request.method,
409
- headers: record.request.headers ? { ...record.request.headers } : undefined,
410
- body: record.request.body,
411
- secretId: record.request.secretId,
412
- },
413
- response: record.response
414
- ? {
415
- status: record.response.status,
416
- headers: record.response.headers ? { ...record.response.headers } : undefined,
417
- body: record.response.body,
418
- error: record.response.error,
419
- }
420
- : undefined,
421
- writeGrant: state?.writeGrant ?? null,
422
- writeGrantedAt: state?.writeGrantedAt,
423
- readGrant: state?.readGrant ?? null,
424
- readGrantedAt: state?.readGrantedAt,
425
- executionStatus: record.execution.status,
426
- };
427
- }
428
- ownerOnCapabilityState(callback) {
429
- this._capabilityStateObservers.add(callback);
430
- return () => {
431
- this._capabilityStateObservers.delete(callback);
432
- };
433
- }
434
- async ownerRegisterAgentIdentity(command) {
435
- if (command.vaultId.value !== this._deps.vaultId.value) {
436
- throw new VaultCoreError("identity registration vault mismatch", "VAULT_IDENTITY_DENIED");
437
- }
438
- if (command.agentIdentity.vaultId.value !== this._deps.vaultId.value) {
439
- throw new VaultCoreError("agent identity vault mismatch", "VAULT_IDENTITY_DENIED");
440
- }
441
- try {
442
- // Sovereign Vault: Owner has full privileges. No signature required for unlocked vault.
443
- await this._deps.agentIdentities.register(command.agentIdentity);
444
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_AGENT_IDENTITY, AuditOutcome.SUCCEEDED, `agent identity registered: ${command.agentIdentity.agentId}`));
445
- }
446
- catch (error) {
447
- const detail = error instanceof Error ? error.message : String(error);
448
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_AGENT_IDENTITY, AuditOutcome.DENIED, detail));
449
- throw error;
450
- }
46
+ async _appendAudit(entry) {
47
+ await this._deps.audit.append(entry);
451
48
  }
452
- async ownerUpdateAgentIdentity(command) {
453
- if (command.vaultId.value !== this._deps.vaultId.value) {
454
- throw new VaultCoreError("identity update vault mismatch", "VAULT_IDENTITY_DENIED");
455
- }
456
- const existing = await this._deps.agentIdentities.get(this._deps.vaultId, command.agentId);
457
- if (!existing) {
458
- throw new VaultCoreError("agent identity not found", "VAULT_IDENTITY_DENIED");
459
- }
460
- const updated = {
461
- ...existing,
462
- nickname: command.nickname,
463
- metadata: command.metadata,
464
- };
49
+ async _verifyAgentControlProof(command, actionName, extraAudit = {}) {
465
50
  try {
466
- await this._deps.agentIdentities.register(updated);
467
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.UPDATE_AGENT_IDENTITY, AuditOutcome.SUCCEEDED, `agent identity updated: ${command.agentId}`, {
468
- requestId: command.requestId,
469
- agentId: command.agentId,
470
- }));
471
- return updated;
51
+ await this._deps.agentProofVerifier.verify(command);
52
+ await this._deps.replayGuard.assertNotReplayed(command);
472
53
  }
473
54
  catch (error) {
474
55
  const detail = error instanceof Error ? error.message : String(error);
475
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.UPDATE_AGENT_IDENTITY, AuditOutcome.DENIED, detail, {
56
+ await this._appendAudit(toAuditEntry(this._deps, command.agent, AuditAction.EVALUATE_DISPATCH_POLICY, AuditOutcome.DENIED, `proof verification failed: ${detail}`, {
476
57
  requestId: command.requestId,
477
- agentId: command.agentId,
478
- }));
479
- throw error;
480
- }
481
- }
482
- async ownerRegisterCapability(command) {
483
- if (command.vaultId.value !== this._deps.vaultId.value) {
484
- throw new VaultCoreError("capability registration vault mismatch", "VAULT_IDENTITY_DENIED");
485
- }
486
- if (command.capability.vaultId.value !== this._deps.vaultId.value) {
487
- throw new VaultCoreError("capability vault mismatch", "VAULT_IDENTITY_DENIED");
488
- }
489
- if (command.capability.agentId !== command.capability.agentId.trim() || !command.capability.agentId.trim()) {
490
- throw new VaultCoreError("capability agent id required", "VAULT_IDENTITY_DENIED");
491
- }
492
- if (!command.capability.capabilityId.trim()) {
493
- throw new VaultCoreError("capability id required", "VAULT_IDENTITY_DENIED");
494
- }
495
- try {
496
- await this._deps.capabilityStates.upsert({
497
- ...command.capability,
498
- source: "owner_grant",
499
- requestId: undefined,
500
- requestedAt: command.capability.issuedAt,
501
- writeGrant: "always",
502
- writeGrantedAt: command.capability.issuedAt,
503
- readGrant: [...command.capability.read.paths],
504
- readGrantedAt: command.capability.issuedAt,
505
- });
506
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CAPABILITY, AuditOutcome.SUCCEEDED, `capability registered: ${command.capability.capabilityId}`, {
507
- capabilityId: command.capability.capabilityId,
508
- operation: command.capability.operation,
509
- }));
510
- }
511
- catch (error) {
512
- const detail = error instanceof Error ? error.message : String(error);
513
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CAPABILITY, AuditOutcome.DENIED, detail, {
514
- capabilityId: command.capability.capabilityId,
515
- operation: command.capability.operation,
58
+ ...extraAudit,
516
59
  }));
517
60
  throw error;
518
61
  }
519
62
  }
520
- async ownerSubmitCapabilityRequest(command) {
521
- if (command.vaultId.value !== this._deps.vaultId.value) {
522
- throw new VaultCoreError("capability request vault mismatch", "VAULT_IDENTITY_DENIED");
523
- }
524
- if (!command.agentId.trim()) {
525
- throw new VaultCoreError("capability request agent id required", "VAULT_IDENTITY_DENIED");
526
- }
527
- if (!command.capability.write.scope.trim()) {
528
- throw new VaultCoreError("capability request scope required", "VAULT_IDENTITY_DENIED");
529
- }
530
- if (command.capability.write.methods.length === 0) {
531
- throw new VaultCoreError("capability request method required", "VAULT_IDENTITY_DENIED");
532
- }
533
- const pendingRecord = {
63
+ // ─── Grant Management ─────────────────────────────────────────────────────────
64
+ async ownerGrantAgentSecret(actor, rootAgentId, secretAlias, request) {
65
+ this._assertOwnerPrincipal(actor);
66
+ const now = this._deps.clock.nowIso();
67
+ const grant = {
534
68
  vaultId: this._deps.vaultId,
535
- source: "explicit_request",
536
- requestId: command.requestId,
537
- agentId: command.agentId,
538
- operation: command.capability.operation,
539
- write: {
540
- secretIds: command.capability.write.secretIds ? [...command.capability.write.secretIds] : undefined,
541
- scope: command.capability.write.scope,
542
- methods: [...command.capability.write.methods],
543
- },
544
- read: {
545
- paths: [...command.capability.read.paths],
546
- },
547
- rateLimit: command.capability.rateLimit,
548
- skipAudit: command.capability.skipAudit,
549
- expiresAt: command.capability.expiresAt,
550
- reason: command.reason,
551
- requestedAt: command.requestedAt,
552
- writeGrant: null,
553
- readGrant: null,
69
+ rootAgentId,
70
+ secretAlias,
71
+ status: "approved",
72
+ requestedAt: now,
73
+ grantedAt: now,
554
74
  };
555
- await this._deps.capabilityStates.upsert(pendingRecord);
556
- for (const observer of this._capabilityStateObservers) {
557
- try {
558
- observer(pendingRecord);
559
- }
560
- catch (error) {
561
- console.error("VaultCore: error in capability state observer:", error);
562
- }
563
- }
564
- await this._appendAudit(toAuditEntry(this._deps, command.requester, AuditAction.SUBMIT_CAPABILITY_REQUEST, AuditOutcome.PENDING, `capability request submitted for agent: ${command.agentId}`, {
565
- requestId: command.requestId,
566
- agentId: command.agentId,
567
- operation: command.capability.operation,
75
+ await this._deps.agentSecretGrants.upsert(grant);
76
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.GRANT_AGENT_SECRET, AuditOutcome.SUCCEEDED, `granted secret "${secretAlias}" to agent "${rootAgentId}"`, {
77
+ requestId: request?.requestId,
78
+ rootAgentId,
79
+ secretAlias,
568
80
  }));
569
- return pendingRecord;
570
- }
571
- async _getCapability(vaultId, agentId, capabilityId) {
572
- if (vaultId.value !== this._deps.vaultId.value) {
573
- throw new VaultCoreError("capability lookup vault mismatch", "VAULT_IDENTITY_DENIED");
574
- }
575
- const state = await this._deps.capabilityStates.getByCapabilityId(vaultId, agentId, capabilityId);
576
- return state && state.capabilityId && state.issuedAt && state.writeGrant === "always"
577
- ? this._stateToGrantedCapability(state)
578
- : null;
579
- }
580
- async ownerRegisterCustomFlow(command) {
581
- if (command.vaultId.value !== this._deps.vaultId.value) {
582
- throw new VaultCoreError("request template vault mismatch", "VAULT_IDENTITY_DENIED");
583
- }
584
- if (!command.flow.flowId.trim()) {
585
- throw new VaultCoreError("request template id required", "VAULT_IDENTITY_DENIED");
586
- }
587
- if (command.flow.mode !== "send_secret" && !command.flow.responseSecret) {
588
- throw new VaultCoreError("request template response secret rule required", "VAULT_IDENTITY_DENIED");
589
- }
590
- try {
591
- await this._deps.customFlows.register({
592
- vaultId: this._deps.vaultId,
593
- flowId: command.flow.flowId,
594
- ownerId: command.owner.id,
595
- mode: command.flow.mode,
596
- targetUrl: command.flow.targetUrl,
597
- method: command.flow.method,
598
- responseVisibility: command.flow.responseVisibility,
599
- responseSecret: command.flow.responseSecret,
600
- createdAt: this._deps.clock.nowIso(),
601
- });
602
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CUSTOM_FLOW, AuditOutcome.SUCCEEDED, `request template registered: ${command.flow.flowId}`));
603
- }
604
- catch (error) {
605
- const detail = error instanceof Error ? error.message : String(error);
606
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CUSTOM_FLOW, AuditOutcome.DENIED, detail));
607
- throw error;
608
- }
81
+ return grant;
609
82
  }
610
- async _storeCustomFlowSecret(flow, alias, plaintext) {
611
- const actor = { kind: "owner", id: flow.ownerId };
612
- const requestId = this._deps.ids.newRequestId("custom_flow_store");
613
- const existing = await this._deps.secrets.getByAlias({ value: alias });
614
- if (existing) {
615
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.REASSIGN_ALIAS, AuditOutcome.DENIED, "alias already bound to existing secret; explicit alias lifecycle required", {
616
- secretAlias: existing.alias.value,
617
- secretId: existing.secretId.value,
618
- }));
619
- throw new VaultCoreError("alias already bound to existing secret", "VAULT_WRITE_DENIED");
620
- }
621
- const record = buildSecretRecord(this._deps, {
622
- kind: "owner.create_secret",
83
+ async ownerGrantSecretDestination(actor, secretAlias, siteId, request) {
84
+ this._assertOwnerPrincipal(actor);
85
+ const now = this._deps.clock.nowIso();
86
+ const grant = {
623
87
  vaultId: this._deps.vaultId,
624
- requestId,
625
- owner: actor,
626
- alias,
627
- plaintext,
628
- source: {
629
- kind: "request",
630
- requestId,
631
- },
632
- requestedAt: this._deps.clock.nowIso(),
633
- });
634
- try {
635
- await this._deps.custody.store(record.secretId, plaintext);
636
- await this._deps.secrets.save(record);
637
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `request template stored secret: ${alias}`, {
638
- secretAlias: record.alias.value,
639
- secretId: record.secretId.value,
640
- }));
641
- }
642
- catch (error) {
643
- await Promise.allSettled([
644
- this._deps.secrets.delete(record.secretId),
645
- this._deps.custody.delete(record.secretId),
646
- ]);
647
- throw error;
648
- }
649
- return record;
650
- }
651
- async _getActiveSecretByAlias(alias) {
652
- const matches = (await this._deps.secrets.list(this._deps.vaultId))
653
- .filter((record) => record.alias.value === alias && isSecretActive(record));
654
- if (matches.length > 1) {
655
- throw new VaultCoreError(`multiple active secrets found for alias: ${alias}`, "VAULT_WRITE_DENIED");
656
- }
657
- return matches[0] ?? null;
658
- }
659
- async _persistNewSecretRecord(record, plaintext, actor, successDetail) {
660
- try {
661
- await this._deps.custody.store(record.secretId, plaintext);
662
- await this._deps.secrets.save(record);
663
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, successDetail, {
664
- secretAlias: record.alias.value,
665
- secretId: record.secretId.value,
666
- }));
667
- }
668
- catch (error) {
669
- await Promise.allSettled([
670
- this._deps.secrets.delete(record.secretId),
671
- this._deps.custody.delete(record.secretId),
672
- ]);
673
- throw error;
674
- }
675
- return record;
676
- }
677
- async ownerCreateSecret(command) {
678
- return this.ownerWriteSecret(command);
679
- }
680
- async ownerUpdateSecret(command) {
681
- if (command.vaultId.value !== this._deps.vaultId.value) {
682
- throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
683
- }
684
- try {
685
- await this._deps.policy.authorizeWrite(command);
686
- }
687
- catch (error) {
688
- const detail = error instanceof Error ? error.message : String(error);
689
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.WRITE_SECRET, AuditOutcome.DENIED, detail, {
690
- secretAlias: command.alias,
691
- }));
692
- throw error;
693
- }
694
- const existing = await this._getActiveSecretByAlias(command.alias);
695
- if (!existing) {
696
- throw new VaultCoreError(`secret not found: ${command.alias}`, "VAULT_SECRET_NOT_FOUND");
697
- }
698
- const record = buildSecretRecord(this._deps, command, existing);
699
- const supersededAt = this._deps.clock.nowIso();
700
- const superseded = {
701
- ...existing,
702
- lifecycleStatus: "SUPERSEDED",
703
- supersededBySecretId: record.secretId,
704
- supersededAt,
705
- retiredAt: supersededAt,
706
- updatedAt: supersededAt,
88
+ secretAlias,
89
+ siteId,
90
+ status: "approved",
91
+ requestedAt: now,
92
+ grantedAt: now,
707
93
  };
708
- let custodyStored = false;
709
- let previousSuperseded = false;
710
- let newRecordSaved = false;
711
- try {
712
- await this._deps.custody.store(record.secretId, command.plaintext);
713
- custodyStored = true;
714
- await this._deps.secrets.save(superseded);
715
- previousSuperseded = true;
716
- await this._deps.secrets.save(record);
717
- newRecordSaved = true;
718
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, "secret updated", {
719
- secretAlias: record.alias.value,
720
- secretId: record.secretId.value,
721
- }));
722
- return record;
723
- }
724
- catch (error) {
725
- if (previousSuperseded) {
726
- await Promise.allSettled([this._deps.secrets.save(existing)]);
727
- }
728
- await Promise.allSettled([
729
- newRecordSaved ? this._deps.secrets.delete(record.secretId) : Promise.resolve(),
730
- custodyStored ? this._deps.custody.delete(record.secretId) : Promise.resolve(),
731
- ]);
732
- throw error;
733
- }
94
+ await this._deps.secretDestinationGrants.upsert(grant);
95
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.GRANT_SECRET_DESTINATION, AuditOutcome.SUCCEEDED, `granted destination "${siteId}" for secret "${secretAlias}"`, {
96
+ requestId: request?.requestId,
97
+ secretAlias,
98
+ siteId,
99
+ }));
100
+ return grant;
734
101
  }
735
- async ownerWriteSecret(command) {
736
- if (command.vaultId.value !== this._deps.vaultId.value) {
737
- throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
738
- }
739
- try {
740
- await this._deps.policy.authorizeWrite(command);
741
- }
742
- catch (error) {
743
- const detail = error instanceof Error ? error.message : String(error);
744
- await this._appendAudit(toAuditEntry(this._deps, command.kind === "issuer.write_secret" ? command.issuer : command.owner, AuditAction.WRITE_SECRET, AuditOutcome.DENIED, detail, {
745
- secretAlias: command.alias,
746
- }));
747
- throw error;
748
- }
749
- const existing = await this._getActiveSecretByAlias(command.alias);
750
- if (existing) {
751
- await this._appendAudit(toAuditEntry(this._deps, command.kind === "issuer.write_secret" ? command.issuer : command.owner, AuditAction.REASSIGN_ALIAS, AuditOutcome.DENIED, "alias already bound to existing secret; explicit alias lifecycle required", {
752
- secretAlias: existing.alias.value,
753
- secretId: existing.secretId.value,
754
- }));
755
- throw new VaultCoreError("alias already bound to existing secret", "VAULT_WRITE_DENIED");
756
- }
757
- const record = buildSecretRecord(this._deps, command);
758
- return this._persistNewSecretRecord(record, command.plaintext, command.kind === "issuer.write_secret" ? command.issuer : command.owner, "secret created");
102
+ async ownerRevokeAgentSecret(actor, rootAgentId, secretAlias, request) {
103
+ this._assertOwnerPrincipal(actor);
104
+ await this._deps.agentSecretGrants.delete(this._deps.vaultId, rootAgentId, secretAlias);
105
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.REVOKE_AGENT_SECRET, AuditOutcome.SUCCEEDED, `revoked secret "${secretAlias}" from agent "${rootAgentId}"`, {
106
+ requestId: request?.requestId,
107
+ rootAgentId,
108
+ secretAlias,
109
+ }));
759
110
  }
760
- async ownerRemoveSecret(command) {
761
- const record = await this._getActiveSecretByAlias(command.alias);
762
- if (!record) {
763
- throw new VaultCoreError(`secret not found: ${command.alias}`, "VAULT_SECRET_NOT_FOUND");
764
- }
765
- const removedAt = this._deps.clock.nowIso();
766
- await this._deps.secrets.save({
767
- ...record,
768
- lifecycleStatus: "REMOVED",
769
- updatedAt: removedAt,
770
- removedAt,
771
- retiredAt: removedAt,
772
- });
773
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.DELETE_SECRET, AuditOutcome.SUCCEEDED, `retired secret ${command.alias}`, {
774
- requestId: command.requestId,
775
- secretAlias: command.alias,
776
- secretId: record.secretId.value,
111
+ async ownerRevokeSecretDestination(actor, secretAlias, siteId, request) {
112
+ this._assertOwnerPrincipal(actor);
113
+ await this._deps.secretDestinationGrants.delete(this._deps.vaultId, secretAlias, siteId);
114
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.REVOKE_SECRET_DESTINATION, AuditOutcome.SUCCEEDED, `revoked destination "${siteId}" from secret "${secretAlias}"`, {
115
+ requestId: request?.requestId,
116
+ secretAlias,
117
+ siteId,
777
118
  }));
778
119
  }
779
- async ownerDeleteSecret(command) {
780
- return this.ownerRemoveSecret(command);
120
+ async ownerListGrants(actor, rootAgentId, secretAlias) {
121
+ this._assertOwnerPrincipal(actor);
122
+ const [agentSecrets, secretDestinations] = await Promise.all([
123
+ this._deps.agentSecretGrants.list(this._deps.vaultId, rootAgentId),
124
+ this._deps.secretDestinationGrants.list(this._deps.vaultId, secretAlias),
125
+ ]);
126
+ return { agentSecrets, secretDestinations };
781
127
  }
128
+ // ─── Dispatch Authorization ───────────────────────────────────────────────────
782
129
  async agentAuthorizeDispatch(request) {
783
- if (request.vaultId.value !== this._deps.vaultId.value) {
784
- throw new VaultCoreError("request vault mismatch", "VAULT_DISPATCH_DENIED");
785
- }
786
- if (!request.reason?.trim()) {
787
- throw new VaultCoreError("dispatch reason is required", "VAULT_DISPATCH_DENIED");
788
- }
789
- const record = request.secretId
790
- ? await this._deps.secrets.getById({ value: request.secretId })
791
- : null;
792
- if (request.secretId && !record) {
793
- await this._appendDecisionAudit(request, AuditOutcome.DENIED, "secret not found");
794
- return {
795
- vaultId: this._deps.vaultId,
796
- decision: "deny",
797
- reason: "secret not found",
798
- secretId: null,
799
- };
800
- }
801
- try {
802
- if (!request.skipReplayGuard) {
803
- await this._deps.replayGuard.assertNotReplayed(request);
804
- }
805
- await this._deps.agentProofVerifier.verify(request);
806
- // Removed direct policy.authorizeDispatch here to handle discovery
807
- }
808
- catch (error) {
809
- const detail = error instanceof Error ? error.message : String(error);
810
- await this._appendDecisionAudit(request, AuditOutcome.DENIED, detail, {
811
- secretAlias: record?.alias.value,
812
- secretId: record?.secretId.value,
813
- });
814
- throw error;
815
- }
816
- // DISCOVERY LOGIC: Find best matching capability
817
- const agentRecord = await this._deps.agentIdentities.get(this._deps.vaultId, request.agent.id);
818
- if (!agentRecord) {
819
- return { vaultId: this._deps.vaultId, decision: "deny", reason: "agent not found", secretId: null };
820
- }
821
- const capabilities = (await this._deps.capabilityStates.list(this._deps.vaultId, request.agent.id))
822
- .filter((state) => !!state.capabilityId && !!state.issuedAt && state.writeGrant === "always")
823
- .map((state) => this._stateToGrantedCapability(state));
824
- const requestedCapabilityId = request.capability?.capabilityId;
825
- const candidateCapabilities = requestedCapabilityId
826
- ? capabilities.filter((cap) => cap.capabilityId === requestedCapabilityId)
827
- : capabilities;
828
- const capability = candidateCapabilities.find((cap) => this.isCapabilityMatch(cap, request, record?.secretId.value));
829
- if (!capability) {
830
- // It's a discovery case if the agent and secret exist but no capability matches
831
- const pendingRecord = {
832
- vaultId: this._deps.vaultId,
833
- source: "dispatch_discovery",
834
- requestId: request.requestId,
835
- agentId: request.agent.id,
836
- capabilityId: undefined,
837
- operation: "dispatch_http",
838
- write: {
839
- secretIds: request.secretId ? [request.secretId] : undefined,
840
- scope: request.targetUrl,
841
- methods: [request.method],
842
- },
843
- read: {
844
- paths: [],
845
- },
846
- requestedAt: request.requestedAt,
847
- reason: request.reason,
848
- secretId: request.secretId,
849
- targetUrl: request.targetUrl,
850
- headers: request.headers,
851
- body: request.body,
852
- proof: request.proof,
853
- writeGrant: null,
854
- readGrant: null,
855
- };
856
- await this._deps.capabilityStates.upsert(pendingRecord);
857
- // Notify observers
858
- for (const observer of this._capabilityStateObservers) {
859
- try {
860
- observer(pendingRecord);
861
- }
862
- catch (error) {
863
- console.error("VaultCore: error in capability state observer:", error);
864
- }
865
- }
866
- await this._appendDecisionAudit(request, AuditOutcome.PENDING, "dispatch stalled for manual discovery approval", {
867
- secretAlias: record?.alias.value,
868
- secretId: record?.secretId.value,
869
- });
870
- return {
871
- vaultId: this._deps.vaultId,
872
- decision: "pending",
873
- reason: "no matching capability found (discovery needed)",
874
- secretId: record?.secretId ?? null,
875
- };
876
- }
877
- try {
878
- await this._deps.policy.authorizeDispatch({
879
- ...request,
880
- capability,
881
- }, record);
882
- }
883
- catch (error) {
884
- const detail = error instanceof Error ? error.message : String(error);
885
- await this._appendDecisionAudit(request, AuditOutcome.DENIED, detail, {
886
- secretAlias: record?.alias.value,
887
- secretId: record?.secretId.value,
888
- });
889
- return {
890
- vaultId: this._deps.vaultId,
891
- decision: "deny",
892
- reason: detail,
893
- secretId: record?.secretId ?? null,
894
- };
895
- }
896
- // Capability found, proceed
897
- if (!capability.skipAudit) {
898
- await this._appendDecisionAudit(request, AuditOutcome.ALLOWED, "dispatch authorized", {
899
- secretAlias: record?.alias.value,
900
- secretId: record?.secretId.value,
901
- });
902
- }
130
+ const { agent, secretAlias, targetUrl } = request;
131
+ if (!secretAlias) {
132
+ return { vaultId: this._deps.vaultId, decision: "deny", reason: "secretAlias required", secretId: null };
133
+ }
134
+ const secret = await this._deps.secrets.getByAlias({ value: secretAlias });
135
+ if (!secret) {
136
+ return { vaultId: this._deps.vaultId, decision: "deny", reason: `secret not found: ${secretAlias}`, secretId: null };
137
+ }
138
+ // 1. Check Agent-Secret Grant
139
+ const agentSecretGrant = await this._deps.agentSecretGrants.get(this._deps.vaultId, agent.id, secretAlias);
140
+ const agentSecretApproved = agentSecretGrant?.status === "approved";
141
+ // 2. Check Secret-Destination Grant
142
+ const siteId = extractDomain(targetUrl);
143
+ const destGrant = await this._deps.secretDestinationGrants.get(this._deps.vaultId, secretAlias, siteId);
144
+ const destApproved = destGrant?.status === "approved";
145
+ if (agentSecretApproved && destApproved) {
146
+ return { vaultId: this._deps.vaultId, decision: "allow", reason: "granted", secretId: secret.secretId };
147
+ }
148
+ const missingGrants = {
149
+ agentSecret: !agentSecretApproved,
150
+ secretDestination: !destApproved,
151
+ };
903
152
  return {
904
153
  vaultId: this._deps.vaultId,
905
- decision: "allow",
906
- reason: null,
907
- secretId: record?.secretId ?? null,
908
- capability, // Expose the found capability for subsequent steps
154
+ decision: "pending",
155
+ reason: "pending approval",
156
+ secretId: secret.secretId,
157
+ missingGrants,
909
158
  };
910
159
  }
911
160
  async agentDispatchSecret(request) {
161
+ await this._verifyAgentControlProof(request, "dispatch");
912
162
  const authorization = await this.agentAuthorizeDispatch(request);
913
- if (authorization.decision === "deny" || !authorization.secretId) {
914
- throw new VaultCoreError("dispatch denied", "VAULT_DISPATCH_DENIED");
163
+ if (authorization.decision === "deny") {
164
+ const result = {
165
+ vaultId: this._deps.vaultId,
166
+ requestId: request.requestId,
167
+ status: DispatchStatus.DENIED,
168
+ targetUrl: request.targetUrl,
169
+ method: request.method,
170
+ error: authorization.reason ?? "denied",
171
+ };
172
+ await this._appendAudit(toAuditEntry(this._deps, request.agent, AuditAction.EVALUATE_DISPATCH_POLICY, AuditOutcome.DENIED, authorization.reason ?? "denied", {
173
+ requestId: request.requestId,
174
+ targetUrl: request.targetUrl,
175
+ secretAlias: request.secretAlias,
176
+ }));
177
+ await this._recordRequestInternal(request, result);
178
+ return result;
915
179
  }
916
180
  if (authorization.decision === "pending") {
917
- return {
181
+ const result = {
918
182
  vaultId: this._deps.vaultId,
919
183
  requestId: request.requestId,
920
184
  status: DispatchStatus.PENDING,
921
185
  targetUrl: request.targetUrl,
922
186
  method: request.method,
923
187
  };
188
+ await this._appendAudit(toAuditEntry(this._deps, request.agent, AuditAction.PENDING_DISPATCH_APPROVAL, AuditOutcome.ALLOWED, "request held for human approval", {
189
+ requestId: request.requestId,
190
+ targetUrl: request.targetUrl,
191
+ secretAlias: request.secretAlias,
192
+ }));
193
+ await this._recordRequestInternal(request, result, authorization.missingGrants);
194
+ return result;
924
195
  }
925
- const record = await this._deps.secrets.getById(authorization.secretId);
926
- if (!record) {
927
- throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
196
+ // Proceed with dispatch
197
+ const secretId = authorization.secretId;
198
+ const secretRecord = await this._deps.secrets.getById(secretId);
199
+ if (!secretRecord) {
200
+ throw new VaultCoreError("secret record not found after authorization", "VAULT_INTERNAL_ERROR");
928
201
  }
929
- const plaintext = await this._deps.custody.load(record.secretId);
202
+ const plaintext = await this._deps.custody.load(secretId);
930
203
  if (plaintext === null) {
931
204
  throw new VaultCoreError("secret material not found", "VAULT_SECRET_NOT_FOUND");
932
205
  }
933
206
  const result = await this._deps.executor.dispatch({
934
207
  vaultId: this._deps.vaultId,
935
208
  requestId: request.requestId,
936
- secretId: record.secretId,
209
+ secretId: secretId,
937
210
  targetUrl: request.targetUrl,
938
211
  method: request.method,
939
212
  headers: request.headers,
940
213
  body: request.body,
941
- }, { record, plaintext });
214
+ }, { record: secretRecord, plaintext });
942
215
  await this._appendAudit(toAuditEntry(this._deps, request.agent, AuditAction.DISPATCH_SECRET, result.status === DispatchStatus.SUCCEEDED ? AuditOutcome.SUCCEEDED : AuditOutcome.FAILED, result.status === DispatchStatus.SUCCEEDED ? "dispatch completed" : (result.error ?? "dispatch failed"), {
943
216
  requestId: request.requestId,
944
- capabilityId: authorization.capability?.capabilityId,
945
- operation: authorization.capability?.operation,
946
217
  targetUrl: request.targetUrl,
947
- secretAlias: record.alias.value,
948
- secretId: record.secretId.value,
218
+ secretAlias: request.secretAlias,
219
+ secretId: secretId.value,
949
220
  }));
950
- await this._recordRequestExecution(request, authorization.capability, result);
221
+ await this._recordRequestInternal(request, result);
951
222
  return {
952
223
  ...result,
953
224
  vaultId: this._deps.vaultId,
954
- responseBody: undefined,
225
+ responseBody: undefined, // Hide body in direct return
955
226
  };
956
227
  }
957
- async ownerReadAudit(actor, query, request) {
958
- this._assertOwnerPrincipal(actor, "VAULT_AUDIT_DENIED");
959
- const entries = await this._deps.audit.query(query);
960
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_AUDIT, AuditOutcome.ALLOWED, "audit queried"));
961
- return entries;
962
- }
963
- async ownerExportSecret(actor, alias, request) {
964
- this._assertOwnerPrincipal(actor, "VAULT_AUDIT_DENIED");
965
- try {
966
- const record = await this._deps.secrets.getByAlias({ value: alias });
967
- if (!record) {
968
- throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
969
- }
970
- const plaintext = await this._deps.custody.load(record.secretId);
971
- if (plaintext === null) {
972
- throw new VaultCoreError("secret material not found", "VAULT_SECRET_NOT_FOUND");
973
- }
974
- const exportedAt = this._deps.clock.nowIso();
975
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.EXPORT_SECRET, AuditOutcome.SUCCEEDED, "secret exported", {
976
- requestId: request?.requestId,
977
- secretAlias: record.alias.value,
978
- secretId: record.secretId.value,
979
- }));
980
- return {
981
- vaultId: this._deps.vaultId,
982
- secretId: record.secretId,
983
- alias: record.alias,
984
- plaintext,
985
- exportedAt,
986
- };
987
- }
988
- catch (error) {
989
- const detail = error instanceof Error ? error.message : String(error);
990
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.EXPORT_SECRET, AuditOutcome.DENIED, detail, {
991
- requestId: request?.requestId,
992
- secretAlias: alias,
993
- }));
994
- throw error;
228
+ // ─── Pending Approval ─────────────────────────────────────────────────────────
229
+ async ownerApproveDispatch(actor, requestId, decision) {
230
+ this._assertOwnerPrincipal(actor);
231
+ const record = await this._deps.requests.get(this._deps.vaultId, requestId);
232
+ if (!record) {
233
+ throw new VaultCoreError("request record not found", "VAULT_REQUEST_NOT_FOUND");
995
234
  }
996
- }
997
- isCapabilityMatch(capability, request, secretId) {
998
- if (request.secretId) {
999
- const idMatched = secretId ? (capability.write.secretIds?.includes(secretId) ?? false) : false;
1000
- if (!idMatched) {
1001
- return false;
1002
- }
235
+ if (record.execution.status !== DispatchStatus.PENDING) {
236
+ throw new VaultCoreError("request is not pending", "VAULT_REQUEST_NOT_PENDING");
1003
237
  }
1004
- if (request.method && capability.write.methods?.length > 0 && !capability.write.methods.includes(request.method)) {
1005
- return false;
238
+ if (decision === "deny") {
239
+ const updated = {
240
+ ...record,
241
+ execution: { status: DispatchStatus.DENIED },
242
+ };
243
+ await this._deps.requests.save(updated);
244
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.REJECT_DISPATCH, AuditOutcome.SUCCEEDED, "dispatch rejected by owner", {
245
+ requestId,
246
+ rootAgentId: record.rootAgentId,
247
+ }));
248
+ return null;
1006
249
  }
1007
- if (capability.write.scope && !isScopeMatch(capability.write.scope, request.targetUrl)) {
1008
- return false;
250
+ const secretAlias = record.request.secretAlias;
251
+ if (!secretAlias) {
252
+ throw new VaultCoreError("record missing secretAlias", "VAULT_INTERNAL_ERROR");
253
+ }
254
+ const secret = await this._deps.secrets.getByAlias({ value: secretAlias });
255
+ if (!secret) {
256
+ throw new VaultCoreError("secret not found during approval", "VAULT_SECRET_NOT_FOUND");
257
+ }
258
+ // Auto-grant if requested
259
+ if (decision === "allow_and_grant") {
260
+ const now = this._deps.clock.nowIso();
261
+ const siteId = extractDomain(record.request.targetUrl);
262
+ await Promise.all([
263
+ this._deps.agentSecretGrants.upsert({
264
+ vaultId: this._deps.vaultId,
265
+ rootAgentId: record.rootAgentId,
266
+ secretAlias,
267
+ status: "approved",
268
+ requestedAt: now,
269
+ grantedAt: now,
270
+ }),
271
+ this._deps.secretDestinationGrants.upsert({
272
+ vaultId: this._deps.vaultId,
273
+ secretAlias,
274
+ siteId,
275
+ status: "approved",
276
+ requestedAt: now,
277
+ grantedAt: now,
278
+ }),
279
+ ]);
280
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.GRANT_AGENT_SECRET, AuditOutcome.SUCCEEDED, "granted during dispatch approval", {
281
+ rootAgentId: record.rootAgentId,
282
+ secretAlias,
283
+ }));
284
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.GRANT_SECRET_DESTINATION, AuditOutcome.SUCCEEDED, "granted during dispatch approval", {
285
+ secretAlias,
286
+ siteId,
287
+ }));
1009
288
  }
1010
- return true;
1011
- }
1012
- async ownerListAgents(actor, request) {
1013
- const identities = await this._deps.agentIdentities.list(this._deps.vaultId);
1014
- const sessionTokens = await this._deps.sessionTokens.list();
1015
- const sessionTokensByAgentId = new Map();
1016
- for (const token of sessionTokens) {
1017
- const existing = sessionTokensByAgentId.get(token.agentId) ?? [];
1018
- existing.push(token);
1019
- sessionTokensByAgentId.set(token.agentId, existing);
289
+ // Execute
290
+ const plaintext = await this._deps.custody.load(secret.secretId);
291
+ if (plaintext === null) {
292
+ throw new VaultCoreError("secret material not found", "VAULT_SECRET_NOT_FOUND");
1020
293
  }
1021
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_AGENTS, AuditOutcome.ALLOWED, "agent identities listed", {
1022
- requestId: request?.requestId,
1023
- }));
1024
- return identities.map((identity) => ({
1025
- ...identity,
1026
- sessionTokens: sessionTokensByAgentId.get(identity.agentId) ?? [],
1027
- }));
1028
- }
1029
- async ownerListCapabilities(actor, agentId, request) {
1030
- const capabilities = (await this._deps.capabilityStates.list(this._deps.vaultId, agentId))
1031
- .filter((state) => !!state.capabilityId && !!state.issuedAt && state.writeGrant === "always")
1032
- .map((state) => this._stateToGrantedCapability(state));
1033
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_CAPABILITIES, AuditOutcome.ALLOWED, "capabilities listed", {
1034
- requestId: request?.requestId,
1035
- agentId,
1036
- }));
1037
- return capabilities;
1038
- }
1039
- async ownerListRequests(actor, agentId, request) {
1040
- const records = await this._deps.requests.list(this._deps.vaultId, agentId);
1041
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_REQUESTS, AuditOutcome.ALLOWED, "request records listed", {
1042
- requestId: request?.requestId,
1043
- agentId,
294
+ const result = await this._deps.executor.dispatch({
295
+ vaultId: this._deps.vaultId,
296
+ requestId,
297
+ secretId: secret.secretId,
298
+ targetUrl: record.request.targetUrl,
299
+ method: record.request.method,
300
+ headers: record.request.headers,
301
+ body: record.request.body,
302
+ }, { record: secret, plaintext });
303
+ const finalRecord = {
304
+ ...record,
305
+ response: {
306
+ status: result.responseStatus,
307
+ body: result.responseBody,
308
+ error: result.error,
309
+ },
310
+ execution: { status: result.status },
311
+ };
312
+ await this._deps.requests.save(finalRecord);
313
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.APPROVE_DISPATCH, AuditOutcome.SUCCEEDED, `dispatch approved (${decision})`, {
314
+ requestId,
315
+ rootAgentId: record.rootAgentId,
1044
316
  }));
1045
- return Promise.all(records.map(async (record) => this.toOwnerVisibleRequestRecord(record, await this._resolveRequestState(record))));
317
+ return result;
1046
318
  }
1047
- async ownerGetRequest(actor, targetRequestId, request) {
1048
- const record = await this._deps.requests.get(this._deps.vaultId, targetRequestId);
1049
- if (!record) {
1050
- throw new VaultCoreError("request record not found", "VAULT_READ_DENIED");
1051
- }
1052
- const state = await this._resolveRequestState(record);
1053
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_REQUEST, AuditOutcome.ALLOWED, "request record read", {
1054
- requestId: request?.requestId,
1055
- agentId: record.agentId,
1056
- }));
1057
- return this.toOwnerRequestRecord(record, state);
319
+ // ─── Agent Control APIs ───────────────────────────────────────────────────────
320
+ async agentGetRuntimeManifest(command) {
321
+ await this._verifyAgentControlProof(command, "get_manifest");
322
+ const agentRecord = await this._deps.agentRecords.get(this._deps.vaultId, command.agent.id);
323
+ if (!agentRecord) {
324
+ throw new VaultCoreError("agent.identity not registered", "VAULT_DISPATCH_DENIED");
325
+ }
326
+ const [agentSecrets, secretDestinations] = await Promise.all([
327
+ this._deps.agentSecretGrants.list(this._deps.vaultId, command.agent.id),
328
+ this._deps.secretDestinationGrants.list(this._deps.vaultId), // All destination grants for these secrets? Or just a subset?
329
+ // For simplicity, return all destinations that mention a secret the agent has a grant for.
330
+ ]);
331
+ const secretAliases = new Set(agentSecrets.map(g => g.secretAlias));
332
+ const relevantDestinations = secretDestinations.filter(d => secretAliases.has(d.secretAlias));
333
+ return {
334
+ rootAgentId: command.agent.id,
335
+ vaultId: this._deps.vaultId.value,
336
+ issuedAt: this._deps.clock.nowIso(),
337
+ agent: {
338
+ rootAgentId: agentRecord.rootAgentId,
339
+ publicKey: agentRecord.publicKey,
340
+ nickname: agentRecord.nickname,
341
+ metadata: agentRecord.metadata,
342
+ },
343
+ grants: {
344
+ agentSecrets: agentSecrets.filter(g => g.status === "approved"),
345
+ secretDestinations: relevantDestinations.filter(d => d.status === "approved"),
346
+ },
347
+ tools: getAgentToolbox(),
348
+ };
1058
349
  }
1059
- async ownerListSecrets(actor, request) {
350
+ async agentListSecrets(command) {
351
+ await this._verifyAgentControlProof(command, "list_secrets");
1060
352
  const records = await this._deps.secrets.list(this._deps.vaultId);
1061
- await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_AUDIT, AuditOutcome.ALLOWED, "secret metadata listed", {
1062
- requestId: request?.requestId,
1063
- }));
1064
- return records.map((record) => ({
353
+ const grants = await this._deps.agentSecretGrants.list(this._deps.vaultId, command.agent.id);
354
+ const approvedAliases = new Set(grants.filter(g => g.status === "approved").map(g => g.secretAlias));
355
+ return records.map(record => ({
1065
356
  vaultId: record.vaultId,
1066
357
  secretId: record.secretId,
1067
358
  alias: record.alias,
@@ -1071,242 +362,289 @@ export class VaultCore {
1071
362
  source: record.source,
1072
363
  createdAt: record.createdAt,
1073
364
  updatedAt: record.updatedAt,
365
+ granted: approvedAliases.has(record.alias.value),
1074
366
  }));
1075
367
  }
1076
- async agentListCapabilities(request) {
1077
- if (request.vaultId.value !== this._deps.vaultId.value) {
1078
- throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
1079
- }
1080
- await this._verifyAgentControlProof(request, "list_capabilities");
1081
- return this._buildAgentCapabilityStates(request.agent.id);
1082
- }
1083
- async agentListSecrets(request) {
1084
- if (request.vaultId.value !== this._deps.vaultId.value) {
1085
- throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
1086
- }
1087
- await this._verifyAgentControlProof(request, "list_secrets");
1088
- return this._listVisibleSecretsForAgent(request.agent.id);
368
+ async agentListRequests(command) {
369
+ await this._verifyAgentControlProof(command, "list_requests");
370
+ const records = await this._deps.requests.list(this._deps.vaultId, command.agent.id);
371
+ return records.map(r => this.toAgentVisibleRequestRecord(r));
1089
372
  }
1090
- async agentListRequests(request) {
1091
- if (request.vaultId.value !== this._deps.vaultId.value) {
1092
- throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
1093
- }
1094
- await this._verifyAgentControlProof(request, "list_requests");
1095
- const records = await this._deps.requests.list(this._deps.vaultId, request.agent.id);
1096
- return Promise.all(records.map(async (record) => this.toVisibleRequestRecord(record, await this._resolveRequestState(record))));
1097
- }
1098
- async agentGetRequest(request) {
1099
- if (request.vaultId.value !== this._deps.vaultId.value) {
1100
- throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
1101
- }
1102
- await this._verifyAgentControlProof(request, "read_request_result", { targetRequestId: request.targetRequestId });
1103
- const record = await this._deps.requests.get(this._deps.vaultId, request.targetRequestId);
1104
- if (!record || record.agentId !== request.agent.id) {
373
+ async agentGetRequest(command) {
374
+ await this._verifyAgentControlProof(command, "read_request");
375
+ const record = await this._deps.requests.get(this._deps.vaultId, command.targetRequestId);
376
+ if (!record || record.rootAgentId !== command.agent.id) {
1105
377
  throw new VaultCoreError("request record not found", "VAULT_READ_DENIED");
1106
378
  }
1107
- const state = await this._resolveRequestState(record);
1108
- const responseBody = applyResponseReadPolicy(record.response?.body, { paths: [...(state?.readGrant ?? [])] });
379
+ // By default, no read-policy is granted anymore in this simplified model.
380
+ // However, if we wanted to support some response visibility, we'd need another grant table.
381
+ // For now, let's assume agent can see their own requested status but not necessarily the body
382
+ // unless they have a specific grant (omitted for now to focus on dispatch).
383
+ const parsedResponseBody = applyResponseReadPolicy(record.response?.body, { paths: [] });
1109
384
  return {
1110
385
  requestId: record.requestId,
1111
386
  executionStatus: record.execution.status,
1112
387
  responseStatus: record.response?.status,
1113
- responseBody,
388
+ responseBody: parsedResponseBody,
1114
389
  error: record.response?.error,
1115
390
  };
1116
391
  }
1117
- async agentGetRuntimeManifest(command) {
1118
- if (command.vaultId.value !== this._deps.vaultId.value) {
1119
- throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
1120
- }
1121
- await this._verifyAgentControlProof(command, "get_manifest");
1122
- const agentRecord = await this._deps.agentIdentities.get(this._deps.vaultId, command.agent.id);
1123
- if (!agentRecord) {
1124
- throw new VaultCoreError("agent identity not registered", "VAULT_DISPATCH_DENIED");
1125
- }
1126
- const capabilities = await this._buildAgentCapabilityStates(command.agent.id);
1127
- const vaultNickname = "CBIO Vault"; // TODO: Pull from profile if available
1128
- return {
1129
- agentId: command.agent.id,
1130
- vaultId: this._deps.vaultId.value,
1131
- vaultNickname,
1132
- issuedAt: this._deps.clock.nowIso(),
1133
- agent: {
1134
- agentId: agentRecord.agentId,
1135
- identityId: agentRecord.identityId,
1136
- publicKey: agentRecord.publicKey,
1137
- nickname: agentRecord.nickname,
1138
- metadata: agentRecord.metadata,
1139
- },
1140
- capabilities,
1141
- tools: getAgentToolbox(),
1142
- };
392
+ // ─── Owner Management APIs ────────────────────────────────────────────────────
393
+ async ownerRegisterAgentIdentity(command) {
394
+ this._assertOwnerPrincipal(command.owner);
395
+ await this._deps.agentRecords.register(command.agentRecord);
396
+ await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_AGENT_IDENTITY, AuditOutcome.SUCCEEDED, `agent identity registered: "${command.agentRecord.rootAgentId}"`, { rootAgentId: command.agentRecord.rootAgentId }));
1143
397
  }
1144
- async agentSubmitCapabilityRequest(command) {
1145
- if (command.vaultId.value !== this._deps.vaultId.value) {
1146
- throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
1147
- }
1148
- if (!command.reason?.trim()) {
1149
- throw new VaultCoreError("capability request reason is required", "VAULT_WRITE_DENIED");
1150
- }
1151
- await this._verifyAgentControlProof(command, "submit_capability_request", {
1152
- write: command.capability.write,
1153
- read: command.capability.read,
1154
- operation: command.capability.operation,
1155
- reason: command.reason,
1156
- });
1157
- return this.ownerSubmitCapabilityRequest({
398
+ async ownerUpdateAgentIdentity(command) {
399
+ this._assertOwnerPrincipal(command.owner);
400
+ const existing = await this._deps.agentRecords.get(command.vaultId, command.rootAgentId);
401
+ if (!existing)
402
+ throw new VaultCoreError("agent identity not found", "VAULT_IDENTITY_NOT_FOUND");
403
+ const updated = { ...existing, nickname: command.nickname ?? existing.nickname, metadata: command.metadata ?? existing.metadata };
404
+ await this._deps.agentRecords.register(updated);
405
+ await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.UPDATE_AGENT_IDENTITY, AuditOutcome.SUCCEEDED, `agent identity updated: "${command.rootAgentId}"`, { rootAgentId: command.rootAgentId }));
406
+ return updated;
407
+ }
408
+ async ownerCreateSecret(command) {
409
+ this._assertOwnerPrincipal(command.owner);
410
+ await this._deps.policy.authorizeWrite(command);
411
+ const secretId = this._deps.ids.newSecretId();
412
+ const now = this._deps.clock.nowIso();
413
+ const record = {
1158
414
  vaultId: command.vaultId,
1159
- requestId: command.requestId,
1160
- requester: command.agent,
1161
- agentId: command.agent.id,
1162
- capability: command.capability,
1163
- reason: command.reason,
1164
- requestedAt: command.requestedAt,
1165
- });
415
+ secretId,
416
+ alias: { value: command.alias },
417
+ version: this._deps.ids.newVersion(),
418
+ lifecycleStatus: "ACTIVE",
419
+ issuerId: null,
420
+ source: command.source ? (command.source.kind === "request" ? { kind: "request", requestId: command.source.requestId } : { kind: "manual" }) : { kind: "manual" },
421
+ createdAt: now,
422
+ updatedAt: now,
423
+ };
424
+ await this._deps.secrets.save(record);
425
+ await this._deps.custody.store(secretId, command.plaintext);
426
+ await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `secret created: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
427
+ return record;
428
+ }
429
+ async ownerUpdateSecret(command) {
430
+ this._assertOwnerPrincipal(command.owner);
431
+ await this._deps.policy.authorizeWrite(command);
432
+ const existing = await this._deps.secrets.getByAlias({ value: command.alias });
433
+ if (!existing)
434
+ throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
435
+ const secretId = existing.secretId;
436
+ const now = this._deps.clock.nowIso();
437
+ const record = {
438
+ ...existing,
439
+ version: this._deps.ids.newVersion(),
440
+ updatedAt: now,
441
+ };
442
+ await this._deps.secrets.save(record);
443
+ await this._deps.custody.store(secretId, command.plaintext);
444
+ await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `secret updated: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
445
+ return record;
446
+ }
447
+ async ownerRemoveSecret(command) {
448
+ this._assertOwnerPrincipal(command.owner);
449
+ const record = await this._deps.secrets.getByAlias({ value: command.alias });
450
+ if (!record)
451
+ throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
452
+ await this._deps.secrets.delete(record.secretId);
453
+ await this._deps.custody.delete(record.secretId);
454
+ await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.DELETE_SECRET, AuditOutcome.SUCCEEDED, `secret deleted: "${command.alias}"`, { secretAlias: command.alias, secretId: record.secretId.value }));
1166
455
  }
1167
- async ownerRevokeCapability(command) {
1168
- const existing = await this._deps.capabilityStates.getByCapabilityId(command.vaultId, command.agentId, command.capabilityId);
456
+ async ownerWriteSecret(command) {
457
+ this._assertOwnerPrincipal(command.owner ?? command.issuer);
458
+ await this._deps.policy.authorizeWrite(command);
459
+ const existing = await this._deps.secrets.getByAlias({ value: command.alias });
460
+ const secretId = existing ? existing.secretId : this._deps.ids.newSecretId();
461
+ const now = this._deps.clock.nowIso();
462
+ const record = {
463
+ vaultId: command.vaultId,
464
+ secretId,
465
+ alias: { value: command.alias },
466
+ version: this._deps.ids.newVersion(),
467
+ lifecycleStatus: "ACTIVE",
468
+ issuerId: command.issuer?.id ?? null,
469
+ source: command.source,
470
+ createdAt: existing ? existing.createdAt : now,
471
+ updatedAt: now,
472
+ };
473
+ await this._deps.secrets.save(record);
474
+ await this._deps.custody.store(secretId, command.plaintext);
475
+ // Generic write doesn't have a specific audit message here, assuming it's called by Create/Update which do their own audit.
476
+ // However, if called directly:
1169
477
  if (!existing) {
1170
- throw new VaultCoreError("capability not found", "VAULT_CAPABILITY_NOT_FOUND");
478
+ await this._appendAudit(toAuditEntry(this._deps, command.owner ?? command.issuer, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `secret created via generic write: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
1171
479
  }
1172
- const decidedAt = this._deps.clock.nowIso();
1173
- await this._deps.capabilityStates.upsert({
1174
- ...existing,
1175
- decidedAt,
1176
- writeGrant: "none",
1177
- writeGrantedAt: decidedAt,
1178
- readGrant: [],
1179
- readGrantedAt: decidedAt,
1180
- });
1181
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REVOKE_CAPABILITY, AuditOutcome.SUCCEEDED, "capability revoked", {
1182
- requestId: command.requestId,
1183
- agentId: command.agentId,
1184
- capabilityId: command.capabilityId,
480
+ else {
481
+ await this._appendAudit(toAuditEntry(this._deps, command.owner ?? command.issuer, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `secret updated via generic write: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
482
+ }
483
+ return record;
484
+ }
485
+ async ownerReadAudit(actor, query) {
486
+ this._assertOwnerPrincipal(actor);
487
+ const entries = await this._deps.audit.query(query);
488
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_AUDIT, AuditOutcome.SUCCEEDED, "audit log accessed", { detail: JSON.stringify(query) }));
489
+ return entries;
490
+ }
491
+ async ownerExportSecret(actor, alias) {
492
+ this._assertOwnerPrincipal(actor);
493
+ const record = await this._deps.secrets.getByAlias({ value: alias });
494
+ if (!record)
495
+ throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
496
+ const plaintext = await this._deps.custody.load(record.secretId);
497
+ if (plaintext === null)
498
+ throw new VaultCoreError("secret material not found", "VAULT_SECRET_NOT_FOUND");
499
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.EXPORT_SECRET, AuditOutcome.SUCCEEDED, `secret exported as plaintext: "${alias}"`, { secretAlias: alias, secretId: record.secretId.value }));
500
+ return { vaultId: this._deps.vaultId, secretId: record.secretId, alias: record.alias, plaintext, exportedAt: this._deps.clock.nowIso() };
501
+ }
502
+ async ownerListAgents(actor) {
503
+ this._assertOwnerPrincipal(actor);
504
+ const identities = await this._deps.agentRecords.list(this._deps.vaultId);
505
+ const sessionTokens = await this._deps.sessionTokens.list();
506
+ const tokensByAgentId = new Map();
507
+ for (const st of sessionTokens) {
508
+ const list = tokensByAgentId.get(st.rootAgentId) ?? [];
509
+ list.push(st);
510
+ tokensByAgentId.set(st.rootAgentId, list);
511
+ }
512
+ const result = identities.map(id => ({ ...id, sessionTokens: tokensByAgentId.get(id.rootAgentId) ?? [] }));
513
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_AGENTS, AuditOutcome.SUCCEEDED, "agent identity list accessed"));
514
+ return result;
515
+ }
516
+ async ownerListRequests(actor, rootAgentId) {
517
+ this._assertOwnerPrincipal(actor);
518
+ const records = await this._deps.requests.list(this._deps.vaultId, rootAgentId);
519
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_REQUESTS, AuditOutcome.SUCCEEDED, "request list accessed"));
520
+ return records.map(r => this.toOwnerVisibleRequestRecord(r));
521
+ }
522
+ async ownerGetRequest(actor, requestId) {
523
+ this._assertOwnerPrincipal(actor);
524
+ const record = await this._deps.requests.get(this._deps.vaultId, requestId);
525
+ if (!record)
526
+ throw new VaultCoreError("request record not found", "VAULT_REQUEST_NOT_FOUND");
527
+ const result = this.toOwnerRequestRecord(record);
528
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_REQUEST, AuditOutcome.SUCCEEDED, `dispatch request detailed: "${requestId}"`, { requestId }));
529
+ return result;
530
+ }
531
+ async ownerListSecrets(actor) {
532
+ this._assertOwnerPrincipal(actor);
533
+ const records = await this._deps.secrets.list(this._deps.vaultId);
534
+ await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_SECRETS, AuditOutcome.SUCCEEDED, "secret list accessed"));
535
+ return records.map(r => ({
536
+ vaultId: r.vaultId,
537
+ secretId: r.secretId,
538
+ alias: r.alias,
539
+ version: r.version,
540
+ lifecycleStatus: r.lifecycleStatus ?? "ACTIVE",
541
+ issuerId: r.issuerId,
542
+ source: r.source,
543
+ createdAt: r.createdAt,
544
+ updatedAt: r.updatedAt,
545
+ granted: true,
1185
546
  }));
1186
547
  }
1187
548
  async ownerIssueSessionToken(request) {
1188
- if (request.vaultId.value !== this._deps.vaultId.value) {
1189
- throw new VaultCoreError("session token vault mismatch", "VAULT_IDENTITY_DENIED");
1190
- }
1191
- const agent = await this._deps.agentIdentities.get(this._deps.vaultId, request.agentId);
1192
- if (!agent) {
1193
- throw new VaultCoreError("agent identity not found", "VAULT_IDENTITY_DENIED");
1194
- }
1195
- const token = await this._deps.sessionTokens.issue(request.agentId);
1196
- const issuedAt = this._deps.clock.nowIso();
1197
- await this._appendAudit(toAuditEntry(this._deps, request.actor, AuditAction.ISSUE_SESSION_TOKEN, AuditOutcome.SUCCEEDED, `session token issued for agent: ${request.agentId}`));
1198
- return {
1199
- token,
1200
- agentId: request.agentId,
1201
- issuedAt,
1202
- };
549
+ this._assertOwnerPrincipal(request.actor);
550
+ const token = await this._deps.sessionTokens.issue(request.rootAgentId);
551
+ await this._appendAudit(toAuditEntry(this._deps, request.actor, AuditAction.ISSUE_SESSION_TOKEN, AuditOutcome.SUCCEEDED, `session token issued for agent: "${request.rootAgentId}"`, { rootAgentId: request.rootAgentId }));
552
+ return { token, rootAgentId: request.rootAgentId, issuedAt: this._deps.clock.nowIso() };
1203
553
  }
1204
554
  async ownerIssueAllAgentSessionTokens(actor) {
1205
- const agents = await this._deps.agentIdentities.list(this._deps.vaultId);
1206
- const results = [];
1207
- const requestedAt = this._deps.clock.nowIso();
1208
- for (const agent of agents) {
1209
- results.push(await this.ownerIssueSessionToken({
1210
- vaultId: this._deps.vaultId,
1211
- requestId: this._deps.ids.newRequestId("warmup_session_token"),
1212
- actor,
1213
- agentId: agent.agentId,
1214
- requestedAt,
1215
- }));
1216
- }
1217
- return results;
555
+ this._assertOwnerPrincipal(actor);
556
+ const agents = await this.ownerListAgents(actor);
557
+ return Promise.all(agents.map(a => this.ownerIssueSessionToken({ vaultId: this._deps.vaultId, actor, rootAgentId: a.rootAgentId })));
1218
558
  }
1219
559
  async ownerRevokeSessionToken(request) {
1220
- if (request.vaultId.value !== this._deps.vaultId.value) {
1221
- throw new VaultCoreError("session token vault mismatch", "VAULT_IDENTITY_DENIED");
1222
- }
560
+ this._assertOwnerPrincipal(request.actor);
1223
561
  await this._deps.sessionTokens.revoke(request.token);
1224
562
  await this._appendAudit(toAuditEntry(this._deps, request.actor, AuditAction.REVOKE_SESSION_TOKEN, AuditOutcome.SUCCEEDED, "session token revoked"));
1225
563
  }
1226
- async ownerListCapabilityStates(command) {
1227
- if (command.vaultId.value !== this._deps.vaultId.value) {
1228
- throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
1229
- }
1230
- return (await this._deps.capabilityStates.list(command.vaultId, command.agentId))
1231
- .filter((state) => command.writeGranted === undefined || (command.writeGranted ? state.writeGrant != null : state.writeGrant == null))
1232
- .filter((state) => command.readGranted === undefined || (command.readGranted ? state.readGrant != null : state.readGrant == null));
1233
- }
1234
- async ownerApproveCapabilityRead(command) {
1235
- if (command.vaultId.value !== this._deps.vaultId.value) {
1236
- throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
1237
- }
1238
- const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
1239
- if (!pending) {
1240
- throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
1241
- }
1242
- if (pending.writeGrant == null) {
1243
- throw new VaultCoreError("write decision required before read grant", "VAULT_WRITE_DENIED");
1244
- }
1245
- if (pending.readGrant != null) {
1246
- throw new VaultCoreError("read grant already decided", "VAULT_WRITE_DENIED");
1247
- }
1248
- const decidedAt = this._deps.clock.nowIso();
1249
- const next = {
1250
- ...pending,
1251
- readGrant: [...(command.read?.paths ?? [])],
1252
- readGrantedAt: decidedAt,
1253
- decidedAt,
564
+ // ─── Event Observers ──────────────────────────────────────────────────────────
565
+ _requestObservers = [];
566
+ ownerOnPendingDispatch(callback) {
567
+ this._requestObservers.push(callback);
568
+ return () => {
569
+ const idx = this._requestObservers.indexOf(callback);
570
+ if (idx >= 0)
571
+ this._requestObservers.splice(idx, 1);
1254
572
  };
1255
- await this._deps.capabilityStates.upsert(next);
1256
- await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.APPROVE_CAPABILITY_READ, AuditOutcome.SUCCEEDED, `approved read policy for capability request ${command.requestId}`, {
1257
- requestId: command.requestId,
1258
- agentId: pending.agentId,
1259
- operation: pending.operation,
1260
- }));
1261
- return next;
1262
573
  }
1263
- async ownerAllowOnce(command) {
1264
- const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
1265
- if (!pending) {
1266
- throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
1267
- }
1268
- const decidedAt = this._deps.clock.nowIso();
1269
- await this._deps.capabilityStates.upsert({ ...pending, writeGrant: "once", writeGrantedAt: decidedAt, decidedAt });
1270
- return this._executePendingCapabilityState(command, "once");
574
+ ownerOnGrantState(callback) {
575
+ return this.ownerOnPendingDispatch(callback);
1271
576
  }
1272
- async ownerAllowAlways(command) {
1273
- const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
1274
- if (!pending) {
1275
- throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
577
+ // ─── Internal Helpers ──────────────────────────────────────────────────────────
578
+ async _recordRequestInternal(request, result, missingGrants) {
579
+ const record = {
580
+ vaultId: this._deps.vaultId,
581
+ requestId: request.requestId,
582
+ rootAgentId: request.agent.id,
583
+ reason: request.reason,
584
+ createdAt: this._deps.clock.nowIso(),
585
+ request: {
586
+ targetUrl: request.targetUrl,
587
+ method: request.method,
588
+ headers: request.headers,
589
+ body: request.body,
590
+ secretAlias: request.secretAlias,
591
+ },
592
+ response: {
593
+ status: result.responseStatus,
594
+ body: result.responseBody,
595
+ error: result.error,
596
+ },
597
+ execution: { status: result.status },
598
+ missingGrants,
599
+ };
600
+ await this._deps.requests.save(record);
601
+ if (result.status === DispatchStatus.PENDING) {
602
+ this._requestObservers.forEach(obs => obs(record));
1276
603
  }
1277
- const decidedAt = this._deps.clock.nowIso();
1278
- await this._deps.capabilityStates.upsert({ ...pending, writeGrant: "always", writeGrantedAt: decidedAt, decidedAt });
1279
- return this._executePendingCapabilityState(command, "grant");
1280
604
  }
1281
- async ownerDeny(command) {
1282
- if (command.vaultId.value !== this._deps.vaultId.value) {
1283
- throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
1284
- }
1285
- const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
1286
- if (!pending) {
1287
- throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
1288
- }
1289
- const decidedAt = this._deps.clock.nowIso();
1290
- const rejectWrite = pending.writeGrant == null;
1291
- const rejectRead = !rejectWrite && pending.readGrant == null;
1292
- if (!rejectWrite && !rejectRead) {
1293
- throw new VaultCoreError("no capability action approval is pending", "VAULT_WRITE_DENIED");
1294
- }
1295
- const rejectedState = {
1296
- ...pending,
1297
- decidedAt,
1298
- writeGrant: rejectWrite ? "none" : pending.writeGrant,
1299
- writeGrantedAt: rejectWrite ? decidedAt : pending.writeGrantedAt,
1300
- readGrant: rejectRead ? [] : pending.readGrant,
1301
- readGrantedAt: rejectRead ? decidedAt : pending.readGrantedAt,
605
+ toAgentVisibleRequestRecord(record) {
606
+ return {
607
+ requestId: record.requestId,
608
+ createdAt: record.createdAt,
609
+ reason: record.reason,
610
+ targetUrl: record.request.targetUrl,
611
+ executionStatus: record.execution.status,
612
+ responseStatus: record.response?.status,
613
+ error: record.response?.error,
614
+ hasResponseBody: !!record.response?.body,
615
+ };
616
+ }
617
+ toOwnerVisibleRequestRecord(record) {
618
+ return {
619
+ requestId: record.requestId,
620
+ createdAt: record.createdAt,
621
+ rootAgentId: record.rootAgentId,
622
+ reason: record.reason,
623
+ targetUrl: record.request.targetUrl,
624
+ executionStatus: record.execution.status,
625
+ responseStatus: record.response?.status,
626
+ error: record.response?.error,
627
+ hasResponseBody: !!record.response?.body,
628
+ missingGrants: record.missingGrants,
629
+ };
630
+ }
631
+ toOwnerRequestRecord(record) {
632
+ return {
633
+ requestId: record.requestId,
634
+ createdAt: record.createdAt,
635
+ rootAgentId: record.rootAgentId,
636
+ reason: record.reason,
637
+ request: {
638
+ targetUrl: record.request.targetUrl,
639
+ method: record.request.method,
640
+ headers: record.request.headers,
641
+ body: record.request.body,
642
+ secretAlias: record.request.secretAlias,
643
+ },
644
+ response: record.response,
645
+ executionStatus: record.execution.status,
646
+ missingGrants: record.missingGrants,
1302
647
  };
1303
- await this._deps.capabilityStates.upsert(rejectedState);
1304
- await this._appendAudit(toAuditEntry(this._deps, command.owner, rejectWrite ? AuditAction.REJECT_CAPABILITY_WRITE : AuditAction.REJECT_CAPABILITY_READ, AuditOutcome.SUCCEEDED, `rejected capability request ${command.requestId}`, {
1305
- requestId: command.requestId,
1306
- agentId: pending.agentId,
1307
- operation: pending.operation,
1308
- }));
1309
- return rejectedState;
1310
648
  }
1311
649
  }
1312
650
  export function createVaultCore(deps) {