@the-ai-company/cbio-node-runtime 1.58.0 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -24
- package/dist/clients/agent/client.d.ts +9 -1
- package/dist/clients/agent/client.js +41 -11
- package/dist/clients/agent/client.js.map +1 -1
- package/dist/clients/agent/contracts.d.ts +5 -2
- package/dist/clients/owner/client.d.ts +15 -11
- package/dist/clients/owner/client.js +83 -30
- package/dist/clients/owner/client.js.map +1 -1
- package/dist/clients/owner/contracts.d.ts +32 -22
- package/dist/clients/owner/index.d.ts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/vault-core/contracts.d.ts +204 -26
- package/dist/vault-core/contracts.js +6 -2
- package/dist/vault-core/contracts.js.map +1 -1
- package/dist/vault-core/core.d.ts +23 -4
- package/dist/vault-core/core.js +480 -121
- package/dist/vault-core/core.js.map +1 -1
- package/dist/vault-core/defaults.d.ts +9 -2
- package/dist/vault-core/defaults.js +41 -14
- package/dist/vault-core/defaults.js.map +1 -1
- package/dist/vault-core/index.d.ts +1 -1
- package/dist/vault-core/index.js.map +1 -1
- package/dist/vault-core/persistence.d.ts +12 -2
- package/dist/vault-core/persistence.js +43 -4
- package/dist/vault-core/persistence.js.map +1 -1
- package/dist/vault-core/ports.d.ts +7 -1
- package/dist/vault-core/tool-metadata.js +27 -10
- package/dist/vault-core/tool-metadata.js.map +1 -1
- package/dist/vault-ingress/defaults.d.ts +2 -0
- package/dist/vault-ingress/defaults.js +6 -0
- package/dist/vault-ingress/defaults.js.map +1 -1
- package/dist/vault-ingress/index.d.ts +54 -9
- package/dist/vault-ingress/index.js +160 -46
- package/dist/vault-ingress/index.js.map +1 -1
- package/dist/vault-ingress/remote-transport.d.ts +2 -0
- package/dist/vault-ingress/remote-transport.js +33 -4
- package/dist/vault-ingress/remote-transport.js.map +1 -1
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/REFERENCE.md +43 -30
- package/docs/WORKS_WITH_CUSTOM_FETCH.md +2 -2
- package/docs/api/README.md +5 -5
- package/docs/api/classes/IdentityError.md +1 -1
- package/docs/api/classes/OwnerClientError.md +1 -1
- package/docs/api/classes/VaultCore.md +176 -16
- package/docs/api/classes/VaultCoreError.md +1 -1
- package/docs/api/enumerations/IdentityErrorCode.md +1 -1
- package/docs/api/enumerations/OwnerClientErrorCode.md +1 -1
- package/docs/api/functions/createAgentClient.md +1 -1
- package/docs/api/functions/createIdentity.md +1 -1
- package/docs/api/functions/createOwnerHttpFlowBoundary.md +1 -1
- package/docs/api/functions/createOwnerSession.md +1 -1
- package/docs/api/functions/createPersistentVaultCoreDependencies.md +1 -1
- package/docs/api/functions/createStandardAcquireBoundary.md +1 -1
- package/docs/api/functions/createStandardDispatchBoundary.md +1 -1
- package/docs/api/functions/createVault.md +1 -1
- package/docs/api/functions/createVaultClient.md +1 -1
- package/docs/api/functions/createVaultCore.md +1 -1
- package/docs/api/functions/createVaultCoreDependencies.md +1 -1
- package/docs/api/functions/createVaultService.md +1 -1
- package/docs/api/functions/createWorkspaceStorage.md +1 -1
- package/docs/api/functions/deriveIdentityId.md +1 -1
- package/docs/api/functions/deriveVaultWorkingKeyFromPassword.md +1 -1
- package/docs/api/functions/getDefaultWorkspaceDir.md +1 -1
- package/docs/api/functions/handleVaultAgentControlHttp.md +1 -1
- package/docs/api/functions/handleVaultHttpDispatch.md +1 -1
- package/docs/api/functions/initializeVaultCustody.md +1 -1
- package/docs/api/functions/listVaults.md +1 -1
- package/docs/api/functions/readVaultProfile.md +1 -1
- package/docs/api/functions/recoverVault.md +1 -1
- package/docs/api/functions/recoverVaultWorkingKey.md +1 -1
- package/docs/api/functions/restoreIdentity.md +1 -1
- package/docs/api/functions/updateVaultMetadata.md +1 -1
- package/docs/api/functions/wrapVaultCoreAsVaultService.md +1 -1
- package/docs/api/functions/writeVaultProfile.md +1 -1
- package/docs/api/interfaces/AgentClient.md +33 -1
- package/docs/api/interfaces/AgentDispatchIntent.md +1 -1
- package/docs/api/interfaces/AgentDispatchTransport.md +33 -1
- package/docs/api/interfaces/AgentIdentity.md +1 -1
- package/docs/api/interfaces/AgentSigner.md +1 -1
- package/docs/api/interfaces/AgentSubmitCapabilityRequestInput.md +9 -9
- package/docs/api/interfaces/CbioRuntime.md +1 -1
- package/docs/api/interfaces/CreateAgentClientOptions.md +1 -1
- package/docs/api/interfaces/CreateIdentityOptions.md +1 -1
- package/docs/api/interfaces/CreateOwnerSessionOptions.md +1 -1
- package/docs/api/interfaces/CreatePersistentVaultCoreDependenciesOptions.md +1 -1
- package/docs/api/interfaces/CreateVaultClientOptions.md +1 -1
- package/docs/api/interfaces/CreateVaultOptions.md +1 -1
- package/docs/api/interfaces/CreatedVault.md +1 -1
- package/docs/api/interfaces/DefaultPolicyEngineOptions.md +1 -1
- package/docs/api/interfaces/IStorageProvider.md +1 -1
- package/docs/api/interfaces/InitializeVaultCustodyOptions.md +1 -1
- package/docs/api/interfaces/InitializedVaultCustody.md +1 -1
- package/docs/api/interfaces/OwnerAgentProvisionResult.md +1 -1
- package/docs/api/interfaces/{OwnerStoreSecretInput.md → OwnerCreateSecretInput.md} +2 -2
- package/docs/api/interfaces/{VaultDeleteSecretInput.md → OwnerRemoveSecretInput.md} +2 -2
- package/docs/api/interfaces/OwnerSensitiveActionConfirmation.md +1 -1
- package/docs/api/interfaces/OwnerSensitiveActionContext.md +1 -1
- package/docs/api/interfaces/OwnerSession.md +1 -1
- package/docs/api/interfaces/{OwnerWriteSecretInput.md → OwnerUpdateSecretInput.md} +2 -2
- package/docs/api/interfaces/RecoverVaultOptions.md +1 -1
- package/docs/api/interfaces/RecoveredVault.md +1 -1
- package/docs/api/interfaces/RestoreIdentityOptions.md +1 -1
- package/docs/api/interfaces/Signer.md +1 -1
- package/docs/api/interfaces/VaultApproveCapabilityRequestInput.md +1 -1
- package/docs/api/interfaces/VaultApproveDispatchInput.md +1 -1
- package/docs/api/interfaces/VaultAuditQueryInput.md +1 -1
- package/docs/api/interfaces/VaultClient.md +112 -48
- package/docs/api/interfaces/VaultCoreDependenciesOptions.md +1 -1
- package/docs/api/interfaces/VaultCreateAgentInput.md +1 -1
- package/docs/api/interfaces/VaultExportSecretInput.md +1 -1
- package/docs/api/interfaces/VaultGrantCapabilityInput.md +9 -21
- package/docs/api/interfaces/VaultGrantCapabilityRequest.md +1 -1
- package/docs/api/interfaces/VaultIdentity.md +1 -1
- package/docs/api/interfaces/VaultImportAgentInput.md +1 -1
- package/docs/api/interfaces/VaultIssueSessionTokenInput.md +1 -1
- package/docs/api/interfaces/VaultListAgentsInput.md +1 -1
- package/docs/api/interfaces/VaultListCapabilitiesInput.md +1 -1
- package/docs/api/interfaces/VaultListSecretsInput.md +1 -1
- package/docs/api/interfaces/VaultMetadata.md +1 -1
- package/docs/api/interfaces/VaultObject.md +1 -1
- package/docs/api/interfaces/VaultProfile.md +1 -1
- package/docs/api/interfaces/VaultReadAgentPrivateKeyInput.md +1 -1
- package/docs/api/interfaces/VaultReadSecretPlaintextInput.md +1 -1
- package/docs/api/interfaces/VaultRegisterFlowInput.md +1 -1
- package/docs/api/interfaces/VaultRevokeCapabilityInput.md +1 -1
- package/docs/api/interfaces/VaultRevokeSessionTokenInput.md +1 -1
- package/docs/api/interfaces/VaultSigner.md +1 -1
- package/docs/api/interfaces/VaultSubmitCapabilityRequestInput.md +11 -17
- package/docs/api/interfaces/VaultUpdateAgentInput.md +1 -1
- package/docs/api/type-aliases/AgentCapabilityEnvelope.md +1 -1
- package/docs/api/type-aliases/AgentVisibleSecretRecord.md +1 -1
- package/docs/api/type-aliases/CbioRuntimeModule.md +1 -1
- package/docs/api/type-aliases/OwnerGrantCapabilityInput.md +1 -1
- package/docs/api/variables/DEFAULT_VAULT_KEY_CUSTODY_BLOB_KEY.md +1 -1
- package/docs/zh/README.md +27 -9
- package/examples/process-isolation.ts +6 -4
- package/package.json +1 -1
package/dist/vault-core/core.js
CHANGED
|
@@ -2,6 +2,7 @@ import { AuditAction, AuditOutcome, DispatchStatus, } from "./contracts.js";
|
|
|
2
2
|
import { VaultCoreError } from "./errors.js";
|
|
3
3
|
import { verifySignature } from "../protocol/crypto.js";
|
|
4
4
|
import { getAgentToolbox } from "./tool-metadata.js";
|
|
5
|
+
import { InMemoryRequestRecordRegistry } from "./defaults.js";
|
|
5
6
|
const VAULT_MASTER_ID = "vault-master";
|
|
6
7
|
function toAuditEntry(deps, actor, action, outcome, detail, options) {
|
|
7
8
|
return {
|
|
@@ -21,22 +22,32 @@ function toAuditEntry(deps, actor, action, outcome, detail, options) {
|
|
|
21
22
|
agentId: options?.agentId,
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
|
-
function buildSecretRecord(deps, command) {
|
|
25
|
+
function buildSecretRecord(deps, command, previousRecord) {
|
|
25
26
|
const now = deps.clock.nowIso();
|
|
26
27
|
const source = command.source?.kind === "request" && command.source.requestId
|
|
27
28
|
? { kind: "request", requestId: command.source.requestId }
|
|
28
29
|
: { kind: "manual" };
|
|
30
|
+
const previousVersion = previousRecord ? Number.parseInt(previousRecord.version.value, 10) : 0;
|
|
31
|
+
const nextVersion = Number.isFinite(previousVersion) ? previousVersion + 1 : 1;
|
|
29
32
|
return {
|
|
30
33
|
vaultId: deps.vaultId,
|
|
31
34
|
secretId: deps.ids.newSecretId(),
|
|
32
35
|
alias: { value: command.alias },
|
|
33
|
-
version:
|
|
36
|
+
version: { value: String(nextVersion) },
|
|
37
|
+
lifecycleStatus: "ACTIVE",
|
|
38
|
+
previousSecretId: previousRecord?.secretId,
|
|
34
39
|
issuerId: command.kind === "issuer.write_secret" ? command.issuerSiteId : null,
|
|
35
40
|
source,
|
|
36
41
|
createdAt: now,
|
|
37
42
|
updatedAt: now,
|
|
38
43
|
};
|
|
39
44
|
}
|
|
45
|
+
function isSecretActive(record) {
|
|
46
|
+
if (record.lifecycleStatus) {
|
|
47
|
+
return record.lifecycleStatus === "ACTIVE";
|
|
48
|
+
}
|
|
49
|
+
return !record.retiredAt;
|
|
50
|
+
}
|
|
40
51
|
function normalizeScopeTarget(targetUrl) {
|
|
41
52
|
try {
|
|
42
53
|
const parsed = new URL(targetUrl);
|
|
@@ -98,12 +109,21 @@ export class VaultCore {
|
|
|
98
109
|
vaultId: state.vaultId,
|
|
99
110
|
capabilityId: state.capabilityId ?? "",
|
|
100
111
|
agentId: state.agentId,
|
|
101
|
-
secretIds: state.secretIds ? [...state.secretIds] : undefined,
|
|
102
|
-
secretAliases: state.secretAliases ? [...state.secretAliases] : undefined,
|
|
103
112
|
operation: state.operation,
|
|
104
113
|
customFlowId: state.customFlowId,
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
write: {
|
|
115
|
+
secretIds: state.write.secretIds ? [...state.write.secretIds] : undefined,
|
|
116
|
+
scope: state.write.scope,
|
|
117
|
+
methods: [...state.write.methods],
|
|
118
|
+
},
|
|
119
|
+
read: state.actions.read.status === "APPROVED"
|
|
120
|
+
? {
|
|
121
|
+
mode: state.read.mode,
|
|
122
|
+
paths: state.read.paths ? [...state.read.paths] : undefined,
|
|
123
|
+
}
|
|
124
|
+
: {
|
|
125
|
+
mode: "none",
|
|
126
|
+
},
|
|
107
127
|
issuedAt: state.issuedAt ?? state.requestedAt,
|
|
108
128
|
expiresAt: state.expiresAt,
|
|
109
129
|
rateLimit: state.rateLimit,
|
|
@@ -112,49 +132,68 @@ export class VaultCore {
|
|
|
112
132
|
}
|
|
113
133
|
async _buildAgentCapabilityStates(agentId) {
|
|
114
134
|
return (await this._deps.capabilityStates.list(this._deps.vaultId, agentId)).map((state) => ({
|
|
115
|
-
status: state.status,
|
|
116
135
|
source: state.source,
|
|
117
136
|
agentId: state.agentId,
|
|
118
137
|
requestId: state.requestId,
|
|
119
138
|
capabilityId: state.capabilityId,
|
|
120
139
|
operation: state.operation,
|
|
121
|
-
secretIds: state.secretIds ? [...state.secretIds] : undefined,
|
|
122
|
-
secretAliases: state.secretAliases ? [...state.secretAliases] : undefined,
|
|
123
140
|
customFlowId: state.customFlowId,
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
write: {
|
|
142
|
+
secretIds: state.write.secretIds ? [...state.write.secretIds] : undefined,
|
|
143
|
+
scope: state.write.scope,
|
|
144
|
+
methods: [...state.write.methods],
|
|
145
|
+
},
|
|
146
|
+
read: {
|
|
147
|
+
mode: state.read.mode,
|
|
148
|
+
paths: state.read.paths ? [...state.read.paths] : undefined,
|
|
149
|
+
},
|
|
126
150
|
issuedAt: state.issuedAt,
|
|
127
151
|
requestedAt: state.requestedAt,
|
|
128
152
|
expiresAt: state.expiresAt,
|
|
129
153
|
rateLimit: state.rateLimit,
|
|
130
154
|
skipAudit: state.skipAudit,
|
|
131
155
|
justification: state.justification,
|
|
132
|
-
|
|
156
|
+
secretId: state.secretId,
|
|
133
157
|
targetUrl: state.targetUrl,
|
|
158
|
+
actions: {
|
|
159
|
+
write: { ...state.actions.write },
|
|
160
|
+
read: { ...state.actions.read },
|
|
161
|
+
},
|
|
134
162
|
}));
|
|
135
163
|
}
|
|
136
164
|
_isExecutablePendingState(state) {
|
|
137
|
-
return !!(state.requestId && state.targetUrl && state.
|
|
165
|
+
return !!(state.requestId && state.targetUrl && state.proof);
|
|
138
166
|
}
|
|
139
167
|
async _executePendingCapabilityState(command, mode) {
|
|
140
168
|
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
141
169
|
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
142
170
|
}
|
|
143
171
|
const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
|
|
144
|
-
if (!pending
|
|
145
|
-
throw new VaultCoreError("
|
|
172
|
+
if (!pending) {
|
|
173
|
+
throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
174
|
+
}
|
|
175
|
+
if (pending.actions.write.status !== "APPROVED") {
|
|
176
|
+
throw new VaultCoreError("write approval required before execution", "VAULT_WRITE_DENIED");
|
|
177
|
+
}
|
|
178
|
+
if (mode === "once" && pending.source !== "dispatch_discovery") {
|
|
179
|
+
throw new VaultCoreError("one-time execution is only available for dispatch discovery requests", "VAULT_WRITE_DENIED");
|
|
146
180
|
}
|
|
147
181
|
const issuedAt = this._deps.clock.nowIso();
|
|
148
182
|
const capability = {
|
|
149
183
|
vaultId: this._deps.vaultId,
|
|
150
184
|
agentId: pending.agentId,
|
|
151
185
|
capabilityId: pending.capabilityId ?? this._deps.ids.newCapabilityId(),
|
|
152
|
-
secretIds: pending.secretIds ? [...pending.secretIds] : undefined,
|
|
153
|
-
secretAliases: pending.secretAliases ? [...pending.secretAliases] : (pending.secretAlias ? [pending.secretAlias] : []),
|
|
154
186
|
operation: pending.operation,
|
|
155
187
|
customFlowId: pending.customFlowId,
|
|
156
|
-
|
|
157
|
-
|
|
188
|
+
write: {
|
|
189
|
+
secretIds: pending.write.secretIds ? [...pending.write.secretIds] : undefined,
|
|
190
|
+
scope: pending.targetUrl ?? pending.write.scope,
|
|
191
|
+
methods: [...pending.write.methods],
|
|
192
|
+
},
|
|
193
|
+
read: {
|
|
194
|
+
mode: pending.read.mode,
|
|
195
|
+
paths: pending.read.paths ? [...pending.read.paths] : undefined,
|
|
196
|
+
},
|
|
158
197
|
issuedAt,
|
|
159
198
|
expiresAt: pending.expiresAt,
|
|
160
199
|
rateLimit: pending.rateLimit,
|
|
@@ -166,9 +205,9 @@ export class VaultCore {
|
|
|
166
205
|
vaultId: this._deps.vaultId,
|
|
167
206
|
agent: { kind: "agent", id: pending.agentId },
|
|
168
207
|
capability,
|
|
169
|
-
|
|
208
|
+
secretId: pending.secretId,
|
|
170
209
|
targetUrl: pending.targetUrl,
|
|
171
|
-
method: pending.methods[0] ?? "POST",
|
|
210
|
+
method: pending.write.methods[0] ?? "POST",
|
|
172
211
|
headers: pending.headers,
|
|
173
212
|
body: pending.body,
|
|
174
213
|
proof: pending.proof,
|
|
@@ -181,8 +220,8 @@ export class VaultCore {
|
|
|
181
220
|
vaultId: this._deps.vaultId,
|
|
182
221
|
requestId: pending.requestId ?? command.requestId,
|
|
183
222
|
status: DispatchStatus.SUCCEEDED,
|
|
184
|
-
targetUrl: pending.scope,
|
|
185
|
-
method: pending.methods[0] ?? "POST",
|
|
223
|
+
targetUrl: pending.write.scope,
|
|
224
|
+
method: pending.write.methods[0] ?? "POST",
|
|
186
225
|
};
|
|
187
226
|
}
|
|
188
227
|
else {
|
|
@@ -192,26 +231,13 @@ export class VaultCore {
|
|
|
192
231
|
await this._deps.capabilityStates.upsert({
|
|
193
232
|
...pending,
|
|
194
233
|
capabilityId: capability.capabilityId,
|
|
195
|
-
status: "GRANTED",
|
|
196
234
|
source: "owner_grant",
|
|
197
235
|
issuedAt,
|
|
198
236
|
decidedAt: issuedAt,
|
|
199
237
|
});
|
|
200
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.APPROVE_CAPABILITY_REQUEST, AuditOutcome.SUCCEEDED, `executed and granted capability state ${command.requestId}`, {
|
|
201
|
-
requestId: command.requestId,
|
|
202
|
-
agentId: pending.agentId,
|
|
203
|
-
capabilityId: capability.capabilityId,
|
|
204
|
-
operation: capability.operation,
|
|
205
|
-
}));
|
|
206
238
|
}
|
|
207
239
|
else {
|
|
208
240
|
await this._deps.capabilityStates.deleteByRequestId(command.vaultId, command.requestId);
|
|
209
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.APPROVE_CAPABILITY_REQUEST, AuditOutcome.SUCCEEDED, `executed once and deleted capability state ${command.requestId}`, {
|
|
210
|
-
requestId: command.requestId,
|
|
211
|
-
agentId: pending.agentId,
|
|
212
|
-
capabilityId: capability.capabilityId,
|
|
213
|
-
operation: capability.operation,
|
|
214
|
-
}));
|
|
215
241
|
}
|
|
216
242
|
return result;
|
|
217
243
|
}
|
|
@@ -233,8 +259,8 @@ export class VaultCore {
|
|
|
233
259
|
capabilityId: request.capability?.capabilityId,
|
|
234
260
|
operation: request.capability?.operation ?? AuditAction.AUTHORIZE_DISPATCH,
|
|
235
261
|
targetUrl: request.targetUrl,
|
|
236
|
-
secretAlias: options?.secretAlias
|
|
237
|
-
secretId: options?.secretId,
|
|
262
|
+
secretAlias: options?.secretAlias,
|
|
263
|
+
secretId: options?.secretId ?? request.secretId,
|
|
238
264
|
}));
|
|
239
265
|
}
|
|
240
266
|
async _verifyAgentControlProof(request, action, payload = {}) {
|
|
@@ -265,27 +291,36 @@ export class VaultCore {
|
|
|
265
291
|
}
|
|
266
292
|
async _listVisibleSecretsForAgent(agentId) {
|
|
267
293
|
const capabilities = (await this._deps.capabilityStates.list(this._deps.vaultId, agentId))
|
|
268
|
-
.filter((state) => state.status === "
|
|
294
|
+
.filter((state) => !!state.capabilityId && !!state.issuedAt && state.actions.write.status === "APPROVED")
|
|
269
295
|
.map((state) => this._stateToGrantedCapability(state));
|
|
270
296
|
const capabilityMap = new Map();
|
|
271
297
|
for (const capability of capabilities) {
|
|
272
|
-
for (const
|
|
273
|
-
const existing = capabilityMap.get(
|
|
298
|
+
for (const secretId of capability.write.secretIds ?? []) {
|
|
299
|
+
const existing = capabilityMap.get(secretId) ?? [];
|
|
274
300
|
existing.push({
|
|
275
301
|
capabilityId: capability.capabilityId,
|
|
276
|
-
|
|
277
|
-
|
|
302
|
+
write: {
|
|
303
|
+
secretIds: capability.write.secretIds ? [...capability.write.secretIds] : undefined,
|
|
304
|
+
scope: capability.write.scope,
|
|
305
|
+
methods: [...capability.write.methods],
|
|
306
|
+
},
|
|
307
|
+
read: {
|
|
308
|
+
mode: capability.read.mode,
|
|
309
|
+
paths: capability.read.paths ? [...capability.read.paths] : undefined,
|
|
310
|
+
},
|
|
278
311
|
});
|
|
279
|
-
capabilityMap.set(
|
|
312
|
+
capabilityMap.set(secretId, existing);
|
|
280
313
|
}
|
|
281
314
|
}
|
|
282
315
|
const records = await this._deps.secrets.list(this._deps.vaultId);
|
|
283
316
|
return records.map((record) => {
|
|
284
|
-
const authorizedCapabilities = capabilityMap.get(record.
|
|
317
|
+
const authorizedCapabilities = capabilityMap.get(record.secretId.value) ?? [];
|
|
285
318
|
return {
|
|
286
319
|
vaultId: record.vaultId,
|
|
287
320
|
secretId: record.secretId,
|
|
288
321
|
alias: record.alias,
|
|
322
|
+
version: record.version,
|
|
323
|
+
lifecycleStatus: record.lifecycleStatus ?? "ACTIVE",
|
|
289
324
|
issuerId: record.issuerId,
|
|
290
325
|
source: record.source,
|
|
291
326
|
createdAt: record.createdAt,
|
|
@@ -295,6 +330,94 @@ export class VaultCore {
|
|
|
295
330
|
};
|
|
296
331
|
});
|
|
297
332
|
}
|
|
333
|
+
async _recordRequestExecution(request, capability, result) {
|
|
334
|
+
await this._deps.requests.save({
|
|
335
|
+
vaultId: this._deps.vaultId,
|
|
336
|
+
requestId: request.requestId,
|
|
337
|
+
agentId: request.agent.id,
|
|
338
|
+
capabilityId: capability?.capabilityId,
|
|
339
|
+
operation: capability?.operation ?? "dispatch_http",
|
|
340
|
+
createdAt: this._deps.clock.nowIso(),
|
|
341
|
+
request: {
|
|
342
|
+
targetUrl: request.targetUrl,
|
|
343
|
+
method: request.method,
|
|
344
|
+
headers: request.headers,
|
|
345
|
+
body: request.body,
|
|
346
|
+
secretId: request.secretId,
|
|
347
|
+
},
|
|
348
|
+
response: {
|
|
349
|
+
status: result.responseStatus,
|
|
350
|
+
body: result.responseBody,
|
|
351
|
+
error: result.error,
|
|
352
|
+
},
|
|
353
|
+
execution: {
|
|
354
|
+
status: result.status,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
toVisibleRequestRecord(record, state) {
|
|
359
|
+
const readStatus = state?.actions.read.status ?? "PENDING";
|
|
360
|
+
return {
|
|
361
|
+
requestId: record.requestId,
|
|
362
|
+
createdAt: record.createdAt,
|
|
363
|
+
capabilityId: record.capabilityId,
|
|
364
|
+
operation: record.operation,
|
|
365
|
+
targetUrl: record.request.targetUrl,
|
|
366
|
+
method: record.request.method,
|
|
367
|
+
executionStatus: record.execution.status,
|
|
368
|
+
responseStatus: record.response?.status,
|
|
369
|
+
error: record.response?.error,
|
|
370
|
+
readStatus,
|
|
371
|
+
hasResponseBody: typeof record.response?.body === "string" && record.response.body.length > 0,
|
|
372
|
+
resultVisible: readStatus === "APPROVED",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
toOwnerVisibleRequestRecord(record, state) {
|
|
376
|
+
return {
|
|
377
|
+
requestId: record.requestId,
|
|
378
|
+
createdAt: record.createdAt,
|
|
379
|
+
agentId: record.agentId,
|
|
380
|
+
capabilityId: record.capabilityId,
|
|
381
|
+
operation: record.operation,
|
|
382
|
+
targetUrl: record.request.targetUrl,
|
|
383
|
+
method: record.request.method,
|
|
384
|
+
executionStatus: record.execution.status,
|
|
385
|
+
responseStatus: record.response?.status,
|
|
386
|
+
error: record.response?.error,
|
|
387
|
+
writeStatus: state?.actions.write.status ?? "PENDING",
|
|
388
|
+
readStatus: state?.actions.read.status ?? "PENDING",
|
|
389
|
+
hasResponseBody: typeof record.response?.body === "string" && record.response.body.length > 0,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
toOwnerRequestRecord(record, state) {
|
|
393
|
+
return {
|
|
394
|
+
requestId: record.requestId,
|
|
395
|
+
createdAt: record.createdAt,
|
|
396
|
+
agentId: record.agentId,
|
|
397
|
+
capabilityId: record.capabilityId,
|
|
398
|
+
operation: record.operation,
|
|
399
|
+
request: {
|
|
400
|
+
targetUrl: record.request.targetUrl,
|
|
401
|
+
method: record.request.method,
|
|
402
|
+
headers: record.request.headers ? { ...record.request.headers } : undefined,
|
|
403
|
+
body: record.request.body,
|
|
404
|
+
secretId: record.request.secretId,
|
|
405
|
+
},
|
|
406
|
+
response: record.response
|
|
407
|
+
? {
|
|
408
|
+
status: record.response.status,
|
|
409
|
+
headers: record.response.headers ? { ...record.response.headers } : undefined,
|
|
410
|
+
body: record.response.body,
|
|
411
|
+
error: record.response.error,
|
|
412
|
+
}
|
|
413
|
+
: undefined,
|
|
414
|
+
actions: {
|
|
415
|
+
write: state?.actions.write ?? { action: "write", status: "PENDING" },
|
|
416
|
+
read: state?.actions.read ?? { action: "read", status: "PENDING" },
|
|
417
|
+
},
|
|
418
|
+
executionStatus: record.execution.status,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
298
421
|
ownerOnCapabilityState(callback) {
|
|
299
422
|
this._capabilityStateObservers.add(callback);
|
|
300
423
|
return () => {
|
|
@@ -365,10 +488,13 @@ export class VaultCore {
|
|
|
365
488
|
try {
|
|
366
489
|
await this._deps.capabilityStates.upsert({
|
|
367
490
|
...command.capability,
|
|
368
|
-
status: "GRANTED",
|
|
369
491
|
source: "owner_grant",
|
|
370
492
|
requestId: undefined,
|
|
371
493
|
requestedAt: command.capability.issuedAt,
|
|
494
|
+
actions: {
|
|
495
|
+
write: { action: "write", status: "APPROVED", decidedAt: command.capability.issuedAt },
|
|
496
|
+
read: { action: "read", status: "APPROVED", decidedAt: command.capability.issuedAt },
|
|
497
|
+
},
|
|
372
498
|
});
|
|
373
499
|
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CAPABILITY, AuditOutcome.SUCCEEDED, `capability registered: ${command.capability.capabilityId}`, {
|
|
374
500
|
capabilityId: command.capability.capabilityId,
|
|
@@ -391,27 +517,36 @@ export class VaultCore {
|
|
|
391
517
|
if (!command.agentId.trim()) {
|
|
392
518
|
throw new VaultCoreError("capability request agent id required", "VAULT_IDENTITY_DENIED");
|
|
393
519
|
}
|
|
394
|
-
if (!command.
|
|
520
|
+
if (!command.capability.write.scope.trim()) {
|
|
395
521
|
throw new VaultCoreError("capability request scope required", "VAULT_IDENTITY_DENIED");
|
|
396
522
|
}
|
|
397
|
-
if (command.
|
|
523
|
+
if (command.capability.write.methods.length === 0) {
|
|
398
524
|
throw new VaultCoreError("capability request method required", "VAULT_IDENTITY_DENIED");
|
|
399
525
|
}
|
|
400
526
|
const pendingRecord = {
|
|
401
527
|
vaultId: this._deps.vaultId,
|
|
402
|
-
status: "PENDING",
|
|
403
528
|
source: "explicit_request",
|
|
404
529
|
requestId: command.requestId,
|
|
405
530
|
agentId: command.agentId,
|
|
406
|
-
operation: command.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
531
|
+
operation: command.capability.operation,
|
|
532
|
+
write: {
|
|
533
|
+
secretIds: command.capability.write.secretIds ? [...command.capability.write.secretIds] : undefined,
|
|
534
|
+
scope: command.capability.write.scope,
|
|
535
|
+
methods: [...command.capability.write.methods],
|
|
536
|
+
},
|
|
537
|
+
read: {
|
|
538
|
+
mode: command.capability.read.mode,
|
|
539
|
+
paths: command.capability.read.paths ? [...command.capability.read.paths] : undefined,
|
|
540
|
+
},
|
|
541
|
+
rateLimit: command.capability.rateLimit,
|
|
542
|
+
skipAudit: command.capability.skipAudit,
|
|
543
|
+
expiresAt: command.capability.expiresAt,
|
|
413
544
|
justification: command.justification,
|
|
414
545
|
requestedAt: command.requestedAt,
|
|
546
|
+
actions: {
|
|
547
|
+
write: { action: "write", status: "PENDING" },
|
|
548
|
+
read: { action: "read", status: "PENDING" },
|
|
549
|
+
},
|
|
415
550
|
};
|
|
416
551
|
await this._deps.capabilityStates.upsert(pendingRecord);
|
|
417
552
|
for (const observer of this._capabilityStateObservers) {
|
|
@@ -425,7 +560,7 @@ export class VaultCore {
|
|
|
425
560
|
await this._appendAudit(toAuditEntry(this._deps, command.requester, AuditAction.SUBMIT_CAPABILITY_REQUEST, AuditOutcome.PENDING, `capability request submitted for agent: ${command.agentId}`, {
|
|
426
561
|
requestId: command.requestId,
|
|
427
562
|
agentId: command.agentId,
|
|
428
|
-
operation: command.
|
|
563
|
+
operation: command.capability.operation,
|
|
429
564
|
}));
|
|
430
565
|
return pendingRecord;
|
|
431
566
|
}
|
|
@@ -434,17 +569,19 @@ export class VaultCore {
|
|
|
434
569
|
throw new VaultCoreError("capability lookup vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
435
570
|
}
|
|
436
571
|
const state = await this._deps.capabilityStates.getByCapabilityId(vaultId, agentId, capabilityId);
|
|
437
|
-
return state && state.
|
|
572
|
+
return state && state.capabilityId && state.issuedAt && state.actions.write.status === "APPROVED"
|
|
573
|
+
? this._stateToGrantedCapability(state)
|
|
574
|
+
: null;
|
|
438
575
|
}
|
|
439
576
|
async ownerRegisterCustomFlow(command) {
|
|
440
577
|
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
441
|
-
throw new VaultCoreError("
|
|
578
|
+
throw new VaultCoreError("request template vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
442
579
|
}
|
|
443
580
|
if (!command.flow.flowId.trim()) {
|
|
444
|
-
throw new VaultCoreError("
|
|
581
|
+
throw new VaultCoreError("request template id required", "VAULT_IDENTITY_DENIED");
|
|
445
582
|
}
|
|
446
583
|
if (command.flow.mode !== "send_secret" && !command.flow.responseSecret) {
|
|
447
|
-
throw new VaultCoreError("
|
|
584
|
+
throw new VaultCoreError("request template response secret rule required", "VAULT_IDENTITY_DENIED");
|
|
448
585
|
}
|
|
449
586
|
try {
|
|
450
587
|
await this._deps.customFlows.register({
|
|
@@ -458,7 +595,7 @@ export class VaultCore {
|
|
|
458
595
|
responseSecret: command.flow.responseSecret,
|
|
459
596
|
createdAt: this._deps.clock.nowIso(),
|
|
460
597
|
});
|
|
461
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CUSTOM_FLOW, AuditOutcome.SUCCEEDED, `
|
|
598
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CUSTOM_FLOW, AuditOutcome.SUCCEEDED, `request template registered: ${command.flow.flowId}`));
|
|
462
599
|
}
|
|
463
600
|
catch (error) {
|
|
464
601
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -478,7 +615,7 @@ export class VaultCore {
|
|
|
478
615
|
throw new VaultCoreError("alias already bound to existing secret", "VAULT_WRITE_DENIED");
|
|
479
616
|
}
|
|
480
617
|
const record = buildSecretRecord(this._deps, {
|
|
481
|
-
kind: "owner.
|
|
618
|
+
kind: "owner.create_secret",
|
|
482
619
|
vaultId: this._deps.vaultId,
|
|
483
620
|
requestId,
|
|
484
621
|
owner: actor,
|
|
@@ -493,7 +630,7 @@ export class VaultCore {
|
|
|
493
630
|
try {
|
|
494
631
|
await this._deps.custody.store(record.secretId, plaintext);
|
|
495
632
|
await this._deps.secrets.save(record);
|
|
496
|
-
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `
|
|
633
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `request template stored secret: ${alias}`, {
|
|
497
634
|
secretAlias: record.alias.value,
|
|
498
635
|
secretId: record.secretId.value,
|
|
499
636
|
}));
|
|
@@ -507,7 +644,36 @@ export class VaultCore {
|
|
|
507
644
|
}
|
|
508
645
|
return record;
|
|
509
646
|
}
|
|
510
|
-
async
|
|
647
|
+
async _getActiveSecretByAlias(alias) {
|
|
648
|
+
const matches = (await this._deps.secrets.list(this._deps.vaultId))
|
|
649
|
+
.filter((record) => record.alias.value === alias && isSecretActive(record));
|
|
650
|
+
if (matches.length > 1) {
|
|
651
|
+
throw new VaultCoreError(`multiple active secrets found for alias: ${alias}`, "VAULT_WRITE_DENIED");
|
|
652
|
+
}
|
|
653
|
+
return matches[0] ?? null;
|
|
654
|
+
}
|
|
655
|
+
async _persistNewSecretRecord(record, plaintext, actor, successDetail) {
|
|
656
|
+
try {
|
|
657
|
+
await this._deps.custody.store(record.secretId, plaintext);
|
|
658
|
+
await this._deps.secrets.save(record);
|
|
659
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, successDetail, {
|
|
660
|
+
secretAlias: record.alias.value,
|
|
661
|
+
secretId: record.secretId.value,
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
await Promise.allSettled([
|
|
666
|
+
this._deps.secrets.delete(record.secretId),
|
|
667
|
+
this._deps.custody.delete(record.secretId),
|
|
668
|
+
]);
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
return record;
|
|
672
|
+
}
|
|
673
|
+
async ownerCreateSecret(command) {
|
|
674
|
+
return this.ownerWriteSecret(command);
|
|
675
|
+
}
|
|
676
|
+
async ownerUpdateSecret(command) {
|
|
511
677
|
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
512
678
|
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
513
679
|
}
|
|
@@ -516,47 +682,89 @@ export class VaultCore {
|
|
|
516
682
|
}
|
|
517
683
|
catch (error) {
|
|
518
684
|
const detail = error instanceof Error ? error.message : String(error);
|
|
519
|
-
await this._appendAudit(toAuditEntry(this._deps, command.
|
|
685
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.WRITE_SECRET, AuditOutcome.DENIED, detail, {
|
|
520
686
|
secretAlias: command.alias,
|
|
521
687
|
}));
|
|
522
688
|
throw error;
|
|
523
689
|
}
|
|
524
|
-
const existing = await this.
|
|
525
|
-
if (existing) {
|
|
526
|
-
|
|
527
|
-
secretAlias: existing.alias.value,
|
|
528
|
-
secretId: existing.secretId.value,
|
|
529
|
-
}));
|
|
530
|
-
throw new VaultCoreError("alias already bound to existing secret", "VAULT_WRITE_DENIED");
|
|
690
|
+
const existing = await this._getActiveSecretByAlias(command.alias);
|
|
691
|
+
if (!existing) {
|
|
692
|
+
throw new VaultCoreError(`secret not found: ${command.alias}`, "VAULT_SECRET_NOT_FOUND");
|
|
531
693
|
}
|
|
532
|
-
const record = buildSecretRecord(this._deps, command);
|
|
694
|
+
const record = buildSecretRecord(this._deps, command, existing);
|
|
695
|
+
const supersededAt = this._deps.clock.nowIso();
|
|
696
|
+
const superseded = {
|
|
697
|
+
...existing,
|
|
698
|
+
lifecycleStatus: "SUPERSEDED",
|
|
699
|
+
supersededBySecretId: record.secretId,
|
|
700
|
+
supersededAt,
|
|
701
|
+
retiredAt: supersededAt,
|
|
702
|
+
updatedAt: supersededAt,
|
|
703
|
+
};
|
|
704
|
+
let custodyStored = false;
|
|
705
|
+
let previousSuperseded = false;
|
|
706
|
+
let newRecordSaved = false;
|
|
533
707
|
try {
|
|
534
708
|
await this._deps.custody.store(record.secretId, command.plaintext);
|
|
709
|
+
custodyStored = true;
|
|
710
|
+
await this._deps.secrets.save(superseded);
|
|
711
|
+
previousSuperseded = true;
|
|
535
712
|
await this._deps.secrets.save(record);
|
|
536
|
-
|
|
713
|
+
newRecordSaved = true;
|
|
714
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, "secret updated", {
|
|
537
715
|
secretAlias: record.alias.value,
|
|
538
716
|
secretId: record.secretId.value,
|
|
539
717
|
}));
|
|
718
|
+
return record;
|
|
540
719
|
}
|
|
541
720
|
catch (error) {
|
|
721
|
+
if (previousSuperseded) {
|
|
722
|
+
await Promise.allSettled([this._deps.secrets.save(existing)]);
|
|
723
|
+
}
|
|
542
724
|
await Promise.allSettled([
|
|
543
|
-
this._deps.secrets.delete(record.secretId),
|
|
544
|
-
this._deps.custody.delete(record.secretId),
|
|
725
|
+
newRecordSaved ? this._deps.secrets.delete(record.secretId) : Promise.resolve(),
|
|
726
|
+
custodyStored ? this._deps.custody.delete(record.secretId) : Promise.resolve(),
|
|
545
727
|
]);
|
|
546
728
|
throw error;
|
|
547
729
|
}
|
|
548
|
-
return record;
|
|
549
730
|
}
|
|
550
|
-
async
|
|
551
|
-
|
|
731
|
+
async ownerWriteSecret(command) {
|
|
732
|
+
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
733
|
+
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
734
|
+
}
|
|
735
|
+
try {
|
|
736
|
+
await this._deps.policy.authorizeWrite(command);
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
740
|
+
await this._appendAudit(toAuditEntry(this._deps, command.kind === "issuer.write_secret" ? command.issuer : command.owner, AuditAction.WRITE_SECRET, AuditOutcome.DENIED, detail, {
|
|
741
|
+
secretAlias: command.alias,
|
|
742
|
+
}));
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
const existing = await this._getActiveSecretByAlias(command.alias);
|
|
746
|
+
if (existing) {
|
|
747
|
+
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", {
|
|
748
|
+
secretAlias: existing.alias.value,
|
|
749
|
+
secretId: existing.secretId.value,
|
|
750
|
+
}));
|
|
751
|
+
throw new VaultCoreError("alias already bound to existing secret", "VAULT_WRITE_DENIED");
|
|
752
|
+
}
|
|
753
|
+
const record = buildSecretRecord(this._deps, command);
|
|
754
|
+
return this._persistNewSecretRecord(record, command.plaintext, command.kind === "issuer.write_secret" ? command.issuer : command.owner, "secret created");
|
|
755
|
+
}
|
|
756
|
+
async ownerRemoveSecret(command) {
|
|
757
|
+
const record = await this._getActiveSecretByAlias(command.alias);
|
|
552
758
|
if (!record) {
|
|
553
759
|
throw new VaultCoreError(`secret not found: ${command.alias}`, "VAULT_SECRET_NOT_FOUND");
|
|
554
760
|
}
|
|
555
|
-
const
|
|
761
|
+
const removedAt = this._deps.clock.nowIso();
|
|
556
762
|
await this._deps.secrets.save({
|
|
557
763
|
...record,
|
|
558
|
-
|
|
559
|
-
|
|
764
|
+
lifecycleStatus: "REMOVED",
|
|
765
|
+
updatedAt: removedAt,
|
|
766
|
+
removedAt,
|
|
767
|
+
retiredAt: removedAt,
|
|
560
768
|
});
|
|
561
769
|
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.DELETE_SECRET, AuditOutcome.SUCCEEDED, `retired secret ${command.alias}`, {
|
|
562
770
|
requestId: command.requestId,
|
|
@@ -564,14 +772,17 @@ export class VaultCore {
|
|
|
564
772
|
secretId: record.secretId.value,
|
|
565
773
|
}));
|
|
566
774
|
}
|
|
775
|
+
async ownerDeleteSecret(command) {
|
|
776
|
+
return this.ownerRemoveSecret(command);
|
|
777
|
+
}
|
|
567
778
|
async agentAuthorizeDispatch(request) {
|
|
568
779
|
if (request.vaultId.value !== this._deps.vaultId.value) {
|
|
569
780
|
throw new VaultCoreError("request vault mismatch", "VAULT_DISPATCH_DENIED");
|
|
570
781
|
}
|
|
571
|
-
const record = request.
|
|
572
|
-
? await this._deps.secrets.
|
|
782
|
+
const record = request.secretId
|
|
783
|
+
? await this._deps.secrets.getById({ value: request.secretId })
|
|
573
784
|
: null;
|
|
574
|
-
if (request.
|
|
785
|
+
if (request.secretId && !record) {
|
|
575
786
|
await this._appendDecisionAudit(request, AuditOutcome.DENIED, "secret not found");
|
|
576
787
|
return {
|
|
577
788
|
vaultId: this._deps.vaultId,
|
|
@@ -588,7 +799,7 @@ export class VaultCore {
|
|
|
588
799
|
catch (error) {
|
|
589
800
|
const detail = error instanceof Error ? error.message : String(error);
|
|
590
801
|
await this._appendDecisionAudit(request, AuditOutcome.DENIED, detail, {
|
|
591
|
-
secretAlias: record?.alias.value
|
|
802
|
+
secretAlias: record?.alias.value,
|
|
592
803
|
secretId: record?.secretId.value,
|
|
593
804
|
});
|
|
594
805
|
throw error;
|
|
@@ -599,7 +810,7 @@ export class VaultCore {
|
|
|
599
810
|
return { vaultId: this._deps.vaultId, decision: "deny", reason: "agent not found", secretId: null };
|
|
600
811
|
}
|
|
601
812
|
const capabilities = (await this._deps.capabilityStates.list(this._deps.vaultId, request.agent.id))
|
|
602
|
-
.filter((state) => state.status === "
|
|
813
|
+
.filter((state) => !!state.capabilityId && !!state.issuedAt && state.actions.write.status === "APPROVED")
|
|
603
814
|
.map((state) => this._stateToGrantedCapability(state));
|
|
604
815
|
const requestedCapabilityId = request.capability?.capabilityId;
|
|
605
816
|
const candidateCapabilities = requestedCapabilityId
|
|
@@ -610,21 +821,29 @@ export class VaultCore {
|
|
|
610
821
|
// It's a discovery case if the agent and secret exist but no capability matches
|
|
611
822
|
const pendingRecord = {
|
|
612
823
|
vaultId: this._deps.vaultId,
|
|
613
|
-
status: "PENDING",
|
|
614
824
|
source: "dispatch_discovery",
|
|
615
825
|
requestId: request.requestId,
|
|
616
826
|
agentId: request.agent.id,
|
|
617
827
|
capabilityId: undefined,
|
|
618
828
|
operation: "dispatch_http",
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
829
|
+
write: {
|
|
830
|
+
secretIds: request.secretId ? [request.secretId] : undefined,
|
|
831
|
+
scope: request.targetUrl,
|
|
832
|
+
methods: [request.method],
|
|
833
|
+
},
|
|
834
|
+
read: {
|
|
835
|
+
mode: "none",
|
|
836
|
+
},
|
|
622
837
|
requestedAt: request.requestedAt,
|
|
623
|
-
|
|
838
|
+
secretId: request.secretId,
|
|
624
839
|
targetUrl: request.targetUrl,
|
|
625
840
|
headers: request.headers,
|
|
626
841
|
body: request.body,
|
|
627
842
|
proof: request.proof,
|
|
843
|
+
actions: {
|
|
844
|
+
write: { action: "write", status: "PENDING" },
|
|
845
|
+
read: { action: "read", status: "PENDING" },
|
|
846
|
+
},
|
|
628
847
|
};
|
|
629
848
|
await this._deps.capabilityStates.upsert(pendingRecord);
|
|
630
849
|
// Notify observers
|
|
@@ -637,7 +856,7 @@ export class VaultCore {
|
|
|
637
856
|
}
|
|
638
857
|
}
|
|
639
858
|
await this._appendDecisionAudit(request, AuditOutcome.PENDING, "dispatch stalled for manual discovery approval", {
|
|
640
|
-
secretAlias: record?.alias.value
|
|
859
|
+
secretAlias: record?.alias.value,
|
|
641
860
|
secretId: record?.secretId.value,
|
|
642
861
|
});
|
|
643
862
|
return {
|
|
@@ -656,7 +875,7 @@ export class VaultCore {
|
|
|
656
875
|
catch (error) {
|
|
657
876
|
const detail = error instanceof Error ? error.message : String(error);
|
|
658
877
|
await this._appendDecisionAudit(request, AuditOutcome.DENIED, detail, {
|
|
659
|
-
secretAlias: record?.alias.value
|
|
878
|
+
secretAlias: record?.alias.value,
|
|
660
879
|
secretId: record?.secretId.value,
|
|
661
880
|
});
|
|
662
881
|
return {
|
|
@@ -669,7 +888,7 @@ export class VaultCore {
|
|
|
669
888
|
// Capability found, proceed
|
|
670
889
|
if (!capability.skipAudit) {
|
|
671
890
|
await this._appendDecisionAudit(request, AuditOutcome.ALLOWED, "dispatch authorized", {
|
|
672
|
-
secretAlias: record?.alias.value
|
|
891
|
+
secretAlias: record?.alias.value,
|
|
673
892
|
secretId: record?.secretId.value,
|
|
674
893
|
});
|
|
675
894
|
}
|
|
@@ -720,9 +939,11 @@ export class VaultCore {
|
|
|
720
939
|
secretAlias: record.alias.value,
|
|
721
940
|
secretId: record.secretId.value,
|
|
722
941
|
}));
|
|
942
|
+
await this._recordRequestExecution(request, authorization.capability, result);
|
|
723
943
|
return {
|
|
724
944
|
...result,
|
|
725
945
|
vaultId: this._deps.vaultId,
|
|
946
|
+
responseBody: undefined,
|
|
726
947
|
};
|
|
727
948
|
}
|
|
728
949
|
async ownerReadAudit(actor, query, request) {
|
|
@@ -766,18 +987,16 @@ export class VaultCore {
|
|
|
766
987
|
}
|
|
767
988
|
}
|
|
768
989
|
isCapabilityMatch(capability, request, secretId) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const idMatched = secretId ? (capability.secretIds?.includes(secretId) ?? false) : false;
|
|
773
|
-
if (!aliasMatched && !idMatched) {
|
|
990
|
+
if (request.secretId) {
|
|
991
|
+
const idMatched = secretId ? (capability.write.secretIds?.includes(secretId) ?? false) : false;
|
|
992
|
+
if (!idMatched) {
|
|
774
993
|
return false;
|
|
775
994
|
}
|
|
776
995
|
}
|
|
777
|
-
if (request.method && capability.methods?.length > 0 && !capability.methods.includes(request.method)) {
|
|
996
|
+
if (request.method && capability.write.methods?.length > 0 && !capability.write.methods.includes(request.method)) {
|
|
778
997
|
return false;
|
|
779
998
|
}
|
|
780
|
-
if (capability.scope && !isScopeMatch(capability.scope, request.targetUrl)) {
|
|
999
|
+
if (capability.write.scope && !isScopeMatch(capability.write.scope, request.targetUrl)) {
|
|
781
1000
|
return false;
|
|
782
1001
|
}
|
|
783
1002
|
return true;
|
|
@@ -791,7 +1010,7 @@ export class VaultCore {
|
|
|
791
1010
|
}
|
|
792
1011
|
async ownerListCapabilities(actor, agentId, request) {
|
|
793
1012
|
const capabilities = (await this._deps.capabilityStates.list(this._deps.vaultId, agentId))
|
|
794
|
-
.filter((state) => state.status === "
|
|
1013
|
+
.filter((state) => !!state.capabilityId && !!state.issuedAt && state.actions.write.status === "APPROVED")
|
|
795
1014
|
.map((state) => this._stateToGrantedCapability(state));
|
|
796
1015
|
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_CAPABILITIES, AuditOutcome.ALLOWED, "capabilities listed", {
|
|
797
1016
|
requestId: request?.requestId,
|
|
@@ -799,6 +1018,28 @@ export class VaultCore {
|
|
|
799
1018
|
}));
|
|
800
1019
|
return capabilities;
|
|
801
1020
|
}
|
|
1021
|
+
async ownerListRequests(actor, agentId, request) {
|
|
1022
|
+
const records = await this._deps.requests.list(this._deps.vaultId, agentId);
|
|
1023
|
+
const states = await this._deps.capabilityStates.list(this._deps.vaultId, agentId);
|
|
1024
|
+
const stateByRequestId = new Map(states.filter((state) => state.requestId).map((state) => [state.requestId, state]));
|
|
1025
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_REQUESTS, AuditOutcome.ALLOWED, "request records listed", {
|
|
1026
|
+
requestId: request?.requestId,
|
|
1027
|
+
agentId,
|
|
1028
|
+
}));
|
|
1029
|
+
return records.map((record) => this.toOwnerVisibleRequestRecord(record, stateByRequestId.get(record.requestId) ?? null));
|
|
1030
|
+
}
|
|
1031
|
+
async ownerGetRequest(actor, targetRequestId, request) {
|
|
1032
|
+
const record = await this._deps.requests.get(this._deps.vaultId, targetRequestId);
|
|
1033
|
+
if (!record) {
|
|
1034
|
+
throw new VaultCoreError("request record not found", "VAULT_READ_DENIED");
|
|
1035
|
+
}
|
|
1036
|
+
const state = await this._deps.capabilityStates.getByRequestId(this._deps.vaultId, targetRequestId);
|
|
1037
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_REQUEST, AuditOutcome.ALLOWED, "request record read", {
|
|
1038
|
+
requestId: request?.requestId,
|
|
1039
|
+
agentId: record.agentId,
|
|
1040
|
+
}));
|
|
1041
|
+
return this.toOwnerRequestRecord(record, state);
|
|
1042
|
+
}
|
|
802
1043
|
async ownerListSecrets(actor, request) {
|
|
803
1044
|
const records = await this._deps.secrets.list(this._deps.vaultId);
|
|
804
1045
|
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_AUDIT, AuditOutcome.ALLOWED, "secret metadata listed", {
|
|
@@ -808,6 +1049,8 @@ export class VaultCore {
|
|
|
808
1049
|
vaultId: record.vaultId,
|
|
809
1050
|
secretId: record.secretId,
|
|
810
1051
|
alias: record.alias,
|
|
1052
|
+
version: record.version,
|
|
1053
|
+
lifecycleStatus: record.lifecycleStatus ?? "ACTIVE",
|
|
811
1054
|
issuerId: record.issuerId,
|
|
812
1055
|
source: record.source,
|
|
813
1056
|
createdAt: record.createdAt,
|
|
@@ -828,6 +1071,35 @@ export class VaultCore {
|
|
|
828
1071
|
await this._verifyAgentControlProof(request, "list_secrets");
|
|
829
1072
|
return this._listVisibleSecretsForAgent(request.agent.id);
|
|
830
1073
|
}
|
|
1074
|
+
async agentListRequests(request) {
|
|
1075
|
+
if (request.vaultId.value !== this._deps.vaultId.value) {
|
|
1076
|
+
throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
|
|
1077
|
+
}
|
|
1078
|
+
await this._verifyAgentControlProof(request, "list_requests");
|
|
1079
|
+
const records = await this._deps.requests.list(this._deps.vaultId, request.agent.id);
|
|
1080
|
+
const states = await this._deps.capabilityStates.list(this._deps.vaultId, request.agent.id);
|
|
1081
|
+
const stateByRequestId = new Map(states.filter((state) => state.requestId).map((state) => [state.requestId, state]));
|
|
1082
|
+
return records.map((record) => this.toVisibleRequestRecord(record, stateByRequestId.get(record.requestId) ?? null));
|
|
1083
|
+
}
|
|
1084
|
+
async agentGetRequest(request) {
|
|
1085
|
+
if (request.vaultId.value !== this._deps.vaultId.value) {
|
|
1086
|
+
throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
|
|
1087
|
+
}
|
|
1088
|
+
await this._verifyAgentControlProof(request, "read_request_result", { targetRequestId: request.targetRequestId });
|
|
1089
|
+
const record = await this._deps.requests.get(this._deps.vaultId, request.targetRequestId);
|
|
1090
|
+
if (!record || record.agentId !== request.agent.id) {
|
|
1091
|
+
throw new VaultCoreError("request record not found", "VAULT_READ_DENIED");
|
|
1092
|
+
}
|
|
1093
|
+
const state = await this._deps.capabilityStates.getByRequestId(this._deps.vaultId, request.targetRequestId);
|
|
1094
|
+
const readApproved = state?.actions.read.status === "APPROVED";
|
|
1095
|
+
return {
|
|
1096
|
+
requestId: record.requestId,
|
|
1097
|
+
executionStatus: record.execution.status,
|
|
1098
|
+
responseStatus: record.response?.status,
|
|
1099
|
+
responseBody: readApproved ? record.response?.body : undefined,
|
|
1100
|
+
error: record.response?.error,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
831
1103
|
async agentGetRuntimeManifest(command) {
|
|
832
1104
|
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
833
1105
|
throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
|
|
@@ -860,10 +1132,9 @@ export class VaultCore {
|
|
|
860
1132
|
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
861
1133
|
}
|
|
862
1134
|
await this._verifyAgentControlProof(command, "submit_capability_request", {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
operation: command.
|
|
866
|
-
secretAliases: command.scope.secretAliases ?? [],
|
|
1135
|
+
write: command.capability.write,
|
|
1136
|
+
read: command.capability.read,
|
|
1137
|
+
operation: command.capability.operation,
|
|
867
1138
|
justification: command.justification ?? null,
|
|
868
1139
|
});
|
|
869
1140
|
return this.ownerSubmitCapabilityRequest({
|
|
@@ -871,7 +1142,7 @@ export class VaultCore {
|
|
|
871
1142
|
requestId: command.requestId,
|
|
872
1143
|
requester: command.agent,
|
|
873
1144
|
agentId: command.agent.id,
|
|
874
|
-
|
|
1145
|
+
capability: command.capability,
|
|
875
1146
|
justification: command.justification,
|
|
876
1147
|
requestedAt: command.requestedAt,
|
|
877
1148
|
});
|
|
@@ -881,10 +1152,14 @@ export class VaultCore {
|
|
|
881
1152
|
if (!existing) {
|
|
882
1153
|
throw new VaultCoreError("capability not found", "VAULT_CAPABILITY_NOT_FOUND");
|
|
883
1154
|
}
|
|
1155
|
+
const decidedAt = this._deps.clock.nowIso();
|
|
884
1156
|
await this._deps.capabilityStates.upsert({
|
|
885
1157
|
...existing,
|
|
886
|
-
|
|
887
|
-
|
|
1158
|
+
decidedAt,
|
|
1159
|
+
actions: {
|
|
1160
|
+
write: { action: "write", status: "REJECTED", decidedAt },
|
|
1161
|
+
read: { action: "read", status: "REJECTED", decidedAt },
|
|
1162
|
+
},
|
|
888
1163
|
});
|
|
889
1164
|
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REVOKE_CAPABILITY, AuditOutcome.SUCCEEDED, "capability revoked", {
|
|
890
1165
|
requestId: command.requestId,
|
|
@@ -936,29 +1211,110 @@ export class VaultCore {
|
|
|
936
1211
|
throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
|
|
937
1212
|
}
|
|
938
1213
|
return (await this._deps.capabilityStates.list(command.vaultId, command.agentId))
|
|
939
|
-
.filter((state) => !command.
|
|
1214
|
+
.filter((state) => !command.writeStatus || state.actions.write.status === command.writeStatus)
|
|
1215
|
+
.filter((state) => !command.readStatus || state.actions.read.status === command.readStatus);
|
|
940
1216
|
}
|
|
941
|
-
async
|
|
1217
|
+
async ownerApproveCapabilityWrite(command) {
|
|
1218
|
+
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
1219
|
+
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
1220
|
+
}
|
|
1221
|
+
const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
|
|
1222
|
+
if (!pending) {
|
|
1223
|
+
throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
1224
|
+
}
|
|
1225
|
+
if (pending.actions.write.status !== "PENDING") {
|
|
1226
|
+
throw new VaultCoreError("write approval not pending", "VAULT_WRITE_DENIED");
|
|
1227
|
+
}
|
|
1228
|
+
const decidedAt = this._deps.clock.nowIso();
|
|
1229
|
+
const next = {
|
|
1230
|
+
...pending,
|
|
1231
|
+
decidedAt,
|
|
1232
|
+
actions: {
|
|
1233
|
+
...pending.actions,
|
|
1234
|
+
write: {
|
|
1235
|
+
action: "write",
|
|
1236
|
+
status: "APPROVED",
|
|
1237
|
+
decidedAt,
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
};
|
|
1241
|
+
await this._deps.capabilityStates.upsert(next);
|
|
1242
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.APPROVE_CAPABILITY_WRITE, AuditOutcome.SUCCEEDED, `approved write policy for capability request ${command.requestId}`, {
|
|
1243
|
+
requestId: command.requestId,
|
|
1244
|
+
agentId: pending.agentId,
|
|
1245
|
+
operation: pending.operation,
|
|
1246
|
+
}));
|
|
1247
|
+
return next;
|
|
1248
|
+
}
|
|
1249
|
+
async ownerApproveCapabilityRead(command) {
|
|
1250
|
+
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
1251
|
+
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
1252
|
+
}
|
|
1253
|
+
const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
|
|
1254
|
+
if (!pending) {
|
|
1255
|
+
throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
1256
|
+
}
|
|
1257
|
+
if (pending.actions.write.status !== "APPROVED") {
|
|
1258
|
+
throw new VaultCoreError("write approval required before read approval", "VAULT_WRITE_DENIED");
|
|
1259
|
+
}
|
|
1260
|
+
if (pending.actions.read.status !== "PENDING") {
|
|
1261
|
+
throw new VaultCoreError("read approval not pending", "VAULT_WRITE_DENIED");
|
|
1262
|
+
}
|
|
1263
|
+
const decidedAt = this._deps.clock.nowIso();
|
|
1264
|
+
const next = {
|
|
1265
|
+
...pending,
|
|
1266
|
+
decidedAt,
|
|
1267
|
+
actions: {
|
|
1268
|
+
...pending.actions,
|
|
1269
|
+
read: {
|
|
1270
|
+
action: "read",
|
|
1271
|
+
status: "APPROVED",
|
|
1272
|
+
decidedAt,
|
|
1273
|
+
},
|
|
1274
|
+
},
|
|
1275
|
+
};
|
|
1276
|
+
await this._deps.capabilityStates.upsert(next);
|
|
1277
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.APPROVE_CAPABILITY_READ, AuditOutcome.SUCCEEDED, `approved read policy for capability request ${command.requestId}`, {
|
|
1278
|
+
requestId: command.requestId,
|
|
1279
|
+
agentId: pending.agentId,
|
|
1280
|
+
operation: pending.operation,
|
|
1281
|
+
}));
|
|
1282
|
+
return next;
|
|
1283
|
+
}
|
|
1284
|
+
async ownerAllowOnce(command) {
|
|
942
1285
|
return this._executePendingCapabilityState(command, "once");
|
|
943
1286
|
}
|
|
944
|
-
async
|
|
1287
|
+
async ownerAllowAlways(command) {
|
|
945
1288
|
return this._executePendingCapabilityState(command, "grant");
|
|
946
1289
|
}
|
|
947
|
-
async
|
|
1290
|
+
async ownerDeny(command) {
|
|
948
1291
|
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
949
1292
|
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
950
1293
|
}
|
|
951
1294
|
const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
|
|
952
|
-
if (!pending
|
|
953
|
-
throw new VaultCoreError("
|
|
1295
|
+
if (!pending) {
|
|
1296
|
+
throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
1297
|
+
}
|
|
1298
|
+
const decidedAt = this._deps.clock.nowIso();
|
|
1299
|
+
const rejectWrite = pending.actions.write.status === "PENDING";
|
|
1300
|
+
const rejectRead = !rejectWrite && pending.actions.read.status === "PENDING";
|
|
1301
|
+
if (!rejectWrite && !rejectRead) {
|
|
1302
|
+
throw new VaultCoreError("no capability action approval is pending", "VAULT_WRITE_DENIED");
|
|
954
1303
|
}
|
|
955
1304
|
const rejectedState = {
|
|
956
1305
|
...pending,
|
|
957
|
-
|
|
958
|
-
|
|
1306
|
+
decidedAt,
|
|
1307
|
+
actions: {
|
|
1308
|
+
write: rejectWrite
|
|
1309
|
+
? { action: "write", status: "REJECTED", decidedAt }
|
|
1310
|
+
: { ...pending.actions.write },
|
|
1311
|
+
read: rejectRead
|
|
1312
|
+
? { action: "read", status: "REJECTED", decidedAt }
|
|
1313
|
+
: { ...pending.actions.read },
|
|
1314
|
+
},
|
|
959
1315
|
};
|
|
960
1316
|
await this._deps.capabilityStates.upsert(rejectedState);
|
|
961
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.
|
|
1317
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, rejectWrite ? AuditAction.REJECT_CAPABILITY_WRITE : AuditAction.REJECT_CAPABILITY_READ, AuditOutcome.SUCCEEDED, `rejected capability request ${command.requestId}`, {
|
|
962
1318
|
requestId: command.requestId,
|
|
963
1319
|
agentId: pending.agentId,
|
|
964
1320
|
operation: pending.operation,
|
|
@@ -967,6 +1323,9 @@ export class VaultCore {
|
|
|
967
1323
|
}
|
|
968
1324
|
}
|
|
969
1325
|
export function createVaultCore(deps) {
|
|
970
|
-
return new VaultCore(
|
|
1326
|
+
return new VaultCore({
|
|
1327
|
+
...deps,
|
|
1328
|
+
requests: deps.requests ?? new InMemoryRequestRecordRegistry(),
|
|
1329
|
+
});
|
|
971
1330
|
}
|
|
972
1331
|
//# sourceMappingURL=core.js.map
|