@the-ai-company/cbio-node-runtime 1.63.2 → 1.63.5
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 +48 -209
- package/dist/clients/agent/client.d.ts +18 -40
- package/dist/clients/agent/client.js +22 -109
- package/dist/clients/agent/client.js.map +1 -1
- package/dist/clients/agent/contracts.d.ts +1 -8
- package/dist/clients/agent/index.d.ts +1 -1
- package/dist/clients/owner/client.d.ts +2 -102
- package/dist/clients/owner/client.js +119 -240
- package/dist/clients/owner/client.js.map +1 -1
- package/dist/clients/owner/contracts.d.ts +37 -70
- package/dist/clients/owner/index.d.ts +2 -4
- package/dist/clients/owner/index.js +1 -2
- package/dist/clients/owner/index.js.map +1 -1
- package/dist/internal/id-factory.d.ts +0 -2
- package/dist/internal/id-factory.js +0 -6
- package/dist/internal/id-factory.js.map +1 -1
- package/dist/protocol/identity.d.ts +1 -1
- package/dist/protocol/identity.js +3 -3
- package/dist/protocol/identity.js.map +1 -1
- package/dist/public-types.d.ts +5 -0
- package/dist/public-types.js +2 -0
- package/dist/public-types.js.map +1 -0
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/identity.d.ts +2 -2
- package/dist/runtime/identity.js +3 -5
- package/dist/runtime/identity.js.map +1 -1
- package/dist/runtime/index.d.ts +10 -11
- package/dist/runtime/index.js +7 -8
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/owner-session.d.ts +7 -6
- package/dist/runtime/owner-session.js +5 -6
- package/dist/runtime/owner-session.js.map +1 -1
- package/dist/storage/fs.d.ts +3 -2
- package/dist/storage/fs.js +8 -5
- package/dist/storage/fs.js.map +1 -1
- package/dist/storage/prefix.d.ts +1 -0
- package/dist/storage/prefix.js +7 -0
- package/dist/storage/prefix.js.map +1 -1
- package/dist/storage/provider.d.ts +2 -0
- package/dist/vault-core/contracts.d.ts +112 -193
- package/dist/vault-core/contracts.js +5 -8
- package/dist/vault-core/contracts.js.map +1 -1
- package/dist/vault-core/core.d.ts +127 -62
- package/dist/vault-core/core.js +500 -1182
- package/dist/vault-core/core.js.map +1 -1
- package/dist/vault-core/defaults.d.ts +26 -42
- package/dist/vault-core/defaults.js +73 -229
- package/dist/vault-core/defaults.js.map +1 -1
- package/dist/vault-core/errors.d.ts +3 -2
- package/dist/vault-core/errors.js.map +1 -1
- package/dist/vault-core/index.d.ts +5 -5
- package/dist/vault-core/index.js +2 -2
- package/dist/vault-core/index.js.map +1 -1
- package/dist/vault-core/persistence.d.ts +78 -118
- package/dist/vault-core/persistence.js +329 -421
- package/dist/vault-core/persistence.js.map +1 -1
- package/dist/vault-core/ports.d.ts +19 -24
- package/dist/vault-core/read-policy.d.ts +3 -2
- package/dist/vault-core/read-policy.js.map +1 -1
- package/dist/vault-core/tool-metadata.js +2 -2
- package/dist/vault-core/tool-metadata.js.map +1 -1
- package/dist/vault-ingress/defaults.d.ts +4 -2
- package/dist/vault-ingress/defaults.js +14 -8
- package/dist/vault-ingress/defaults.js.map +1 -1
- package/dist/vault-ingress/index.d.ts +43 -117
- package/dist/vault-ingress/index.js +98 -453
- package/dist/vault-ingress/index.js.map +1 -1
- package/dist/vault-ingress/remote-transport.d.ts +5 -3
- package/dist/vault-ingress/remote-transport.js +8 -28
- package/dist/vault-ingress/remote-transport.js.map +1 -1
- package/docs/ARCHITECTURE.md +39 -22
- package/docs/CUSTODY_MODEL.md +1 -1
- package/docs/IDENTITY_MODEL.md +5 -5
- package/docs/MIGRATION-1.51.md +19 -19
- package/docs/MIGRATION-1.65.md +61 -0
- package/docs/PROCESS_ISOLATION.md +2 -2
- package/docs/REFERENCE.md +42 -200
- package/docs/api/README.md +50 -22
- package/docs/api/classes/IdentityError.md +1 -1
- package/docs/api/classes/OwnerClientError.md +1 -1
- package/docs/api/classes/PersistentVaultAgentIdentityRegistry.md +89 -0
- package/docs/api/classes/PersistentVaultAgentSecretGrantRegistry.md +125 -0
- package/docs/api/classes/PersistentVaultAuditLog.md +65 -0
- package/docs/api/classes/PersistentVaultCustomHttpFlowRegistry.md +69 -0
- package/docs/api/classes/PersistentVaultSecretCustody.md +93 -0
- package/docs/api/classes/PersistentVaultSecretDestinationGrantRegistry.md +125 -0
- package/docs/api/classes/PersistentVaultSecretRepository.md +127 -0
- package/docs/api/classes/VaultCore.md +299 -214
- package/docs/api/classes/VaultCoreError.md +3 -3
- package/docs/api/enumerations/AuditAction.md +143 -0
- package/docs/api/enumerations/AuditOutcome.md +35 -0
- package/docs/api/enumerations/DispatchStatus.md +35 -0
- package/docs/api/enumerations/IdentityErrorCode.md +1 -1
- package/docs/api/enumerations/OwnerClientErrorCode.md +1 -1
- package/docs/api/functions/createAgentClient.md +1 -15
- package/docs/api/functions/createIdentity.md +2 -2
- package/docs/api/functions/createOwnerClient.md +17 -0
- package/docs/api/functions/createOwnerSession.md +1 -1
- package/docs/api/functions/createPersistentVaultCoreDependencies.md +4 -4
- package/docs/api/functions/createVault.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 +5 -9
- package/docs/api/functions/createWorkspaceStorage.md +1 -1
- package/docs/api/functions/deriveRootAgentId.md +17 -0
- package/docs/api/functions/deriveVaultWorkingKeyFromPassword.md +1 -1
- package/docs/api/functions/getDefaultWorkspaceDir.md +1 -1
- package/docs/api/functions/handleVaultAgentControlHttp.md +2 -2
- package/docs/api/functions/handleVaultHttpDispatch.md +2 -2
- package/docs/api/functions/initializeVaultCustody.md +7 -3
- 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 +4 -8
- package/docs/api/functions/restoreIdentity.md +1 -1
- package/docs/api/functions/updateVaultMetadata.md +1 -1
- package/docs/api/functions/writeVaultProfile.md +1 -1
- package/docs/api/interfaces/AgentClient.md +20 -59
- package/docs/api/interfaces/AgentDispatchIntent.md +1 -1
- package/docs/api/interfaces/AgentDispatchTransport.md +12 -44
- package/docs/api/interfaces/AgentIdentity.md +3 -3
- package/docs/api/interfaces/AgentIdentityRecord.md +47 -0
- package/docs/api/interfaces/AgentRequestResult.md +35 -0
- package/docs/api/interfaces/AgentRuntimeManifest.md +55 -0
- package/docs/api/interfaces/AgentSecretGrant.md +41 -0
- package/docs/api/interfaces/AgentSigner.md +1 -1
- package/docs/api/interfaces/AgentVisibleRequestRecord.md +53 -0
- package/docs/api/interfaces/AgentVisibleSecretRecord.md +65 -0
- package/docs/api/interfaces/AuditEntry.md +83 -0
- package/docs/api/interfaces/CbioRuntime.md +13 -150
- package/docs/api/interfaces/CreateAgentClientOptions.md +4 -10
- package/docs/api/interfaces/CreateIdentityOptions.md +1 -1
- package/docs/api/interfaces/{CreateVaultClientOptions.md → CreateOwnerClientOptions.md} +9 -11
- package/docs/api/interfaces/CreateOwnerSessionOptions.md +3 -117
- package/docs/api/interfaces/CreatePersistentVaultCoreDependenciesOptions.md +3 -131
- package/docs/api/interfaces/CreateVaultOptions.md +1 -121
- package/docs/api/interfaces/CreatedVault.md +2 -2
- package/docs/api/interfaces/CustomHttpFlowDefinition.md +71 -0
- package/docs/api/interfaces/DefaultPolicyEngineOptions.md +1 -13
- package/docs/api/interfaces/DispatchAuthorization.md +43 -0
- package/docs/api/interfaces/DispatchInstruction.md +47 -0
- package/docs/api/interfaces/DispatchRequest.md +83 -0
- package/docs/api/interfaces/DispatchResult.md +53 -0
- package/docs/api/interfaces/IStorageProvider.md +13 -1
- package/docs/api/interfaces/InitializeVaultCustodyOptions.md +31 -11
- package/docs/api/interfaces/InitializedVaultCustody.md +1 -7
- package/docs/api/interfaces/OwnerAgentProvisionResult.md +2 -2
- package/docs/api/interfaces/OwnerClient.md +417 -0
- package/docs/api/interfaces/OwnerCreateSecretInput.md +1 -1
- package/docs/api/interfaces/OwnerRemoveSecretInput.md +1 -1
- package/docs/api/interfaces/OwnerRequestRecord.md +97 -0
- package/docs/api/interfaces/OwnerSensitiveActionConfirmation.md +1 -1
- package/docs/api/interfaces/OwnerSensitiveActionContext.md +1 -1
- package/docs/api/interfaces/OwnerSession.md +3 -3
- package/docs/api/interfaces/OwnerUpdateSecretInput.md +1 -1
- package/docs/api/interfaces/OwnerVisibleRequestRecord.md +73 -0
- package/docs/api/interfaces/RecoverVaultOptions.md +1 -121
- package/docs/api/interfaces/RecoveredVault.md +2 -2
- package/docs/api/interfaces/RequestRecord.md +107 -0
- package/docs/api/interfaces/RestoreIdentityOptions.md +1 -1
- package/docs/api/interfaces/SecretAlias.md +11 -0
- package/docs/api/interfaces/SecretDestinationGrant.md +41 -0
- package/docs/api/interfaces/SecretId.md +11 -0
- package/docs/api/interfaces/SecretRecord.md +89 -0
- package/docs/api/interfaces/Signer.md +1 -1
- package/docs/api/interfaces/VaultApproveDispatchInput.md +3 -9
- package/docs/api/interfaces/VaultAuditQueryInput.md +1 -1
- package/docs/api/interfaces/VaultCoreDependenciesOptions.md +1 -5
- package/docs/api/interfaces/VaultCreateAgentInput.md +1 -1
- package/docs/api/interfaces/VaultExportSecretInput.md +1 -1
- package/docs/api/interfaces/VaultGetRequestInput.md +17 -0
- package/docs/api/interfaces/VaultGrantAgentSecretInput.md +23 -0
- package/docs/api/interfaces/VaultGrantSecretDestinationInput.md +23 -0
- package/docs/api/interfaces/VaultId.md +11 -0
- package/docs/api/interfaces/VaultImportAgentInput.md +1 -1
- package/docs/api/interfaces/VaultIssueSessionTokenInput.md +5 -5
- package/docs/api/interfaces/VaultListAgentsInput.md +1 -1
- package/docs/api/interfaces/VaultListGrantsInput.md +23 -0
- package/docs/api/interfaces/VaultListRequestsInput.md +17 -0
- package/docs/api/interfaces/VaultListSecretsInput.md +1 -1
- package/docs/api/interfaces/VaultMetadata.md +1 -1
- package/docs/api/interfaces/VaultObject.md +2 -2
- package/docs/api/interfaces/VaultPrincipal.md +17 -0
- package/docs/api/interfaces/VaultProfile.md +1 -1
- package/docs/api/interfaces/VaultReadAgentPrivateKeyInput.md +7 -7
- package/docs/api/interfaces/VaultReadSecretPlaintextInput.md +1 -1
- package/docs/api/interfaces/VaultRegisterFlowInput.md +1 -1
- package/docs/api/interfaces/VaultRevokeAgentSecretInput.md +23 -0
- package/docs/api/interfaces/VaultRevokeSecretDestinationInput.md +23 -0
- package/docs/api/interfaces/VaultRevokeSessionTokenInput.md +1 -1
- package/docs/api/interfaces/VaultService.md +547 -0
- package/docs/api/interfaces/VaultUpdateAgentInput.md +7 -7
- package/docs/api/type-aliases/AgentId.md +7 -0
- package/docs/api/type-aliases/CbioRuntimeModule.md +1 -1
- package/docs/api/type-aliases/DispatchApprovalDecision.md +7 -0
- package/docs/api/type-aliases/GrantStatus.md +7 -0
- package/docs/api/type-aliases/SecretLifecycleStatus.md +7 -0
- package/docs/api/type-aliases/VaultPrincipalKind.md +7 -0
- package/docs/api/variables/DEFAULT_VAULT_KEY_CUSTODY_BLOB_KEY.md +2 -2
- package/docs/es/README.md +3 -3
- package/docs/fr/README.md +3 -3
- package/docs/ja/README.md +5 -5
- package/docs/ko/README.md +5 -5
- package/docs/pt/README.md +3 -3
- package/docs/zh/PROCESS_ISOLATION.md +2 -2
- package/docs/zh/README.md +24 -24
- package/examples/process-isolation.ts +26 -35
- package/package.json +3 -2
- package/docs/api/functions/createOwnerHttpFlowBoundary.md +0 -17
- package/docs/api/functions/createStandardAcquireBoundary.md +0 -31
- package/docs/api/functions/createStandardDispatchBoundary.md +0 -23
- package/docs/api/functions/createVaultClient.md +0 -32
- package/docs/api/functions/deriveIdentityId.md +0 -17
- package/docs/api/functions/wrapVaultCoreAsVaultService.md +0 -31
- package/docs/api/interfaces/AgentSubmitCapabilityRequestInput.md +0 -41
- package/docs/api/interfaces/VaultApproveCapabilityRequestInput.md +0 -23
- package/docs/api/interfaces/VaultClient.md +0 -473
- package/docs/api/interfaces/VaultGrantCapabilityInput.md +0 -79
- package/docs/api/interfaces/VaultGrantCapabilityRequest.md +0 -23
- package/docs/api/interfaces/VaultIdentity.md +0 -11
- package/docs/api/interfaces/VaultListCapabilitiesInput.md +0 -17
- package/docs/api/interfaces/VaultRevokeCapabilityInput.md +0 -23
- package/docs/api/interfaces/VaultSigner.md +0 -21
- package/docs/api/interfaces/VaultSubmitCapabilityRequestInput.md +0 -73
- package/docs/api/type-aliases/AgentCapabilityEnvelope.md +0 -7
- package/docs/api/type-aliases/AgentVisibleSecretRecord.md +0 -7
- package/docs/api/type-aliases/OwnerGrantCapabilityInput.md +0 -7
package/dist/vault-core/core.js
CHANGED
|
@@ -1,1067 +1,358 @@
|
|
|
1
1
|
import { AuditAction, AuditOutcome, DispatchStatus, } from "./contracts.js";
|
|
2
2
|
import { VaultCoreError } from "./errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { applyResponseReadPolicy } from "./read-policy.js";
|
|
4
4
|
import { getAgentToolbox } from "./tool-metadata.js";
|
|
5
5
|
import { InMemoryRequestRecordRegistry } from "./defaults.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
25
|
+
vaultId: deps.vaultId,
|
|
13
26
|
actor,
|
|
14
27
|
action,
|
|
15
28
|
outcome,
|
|
16
29
|
detail,
|
|
17
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
297
|
-
|
|
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
|
|
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.
|
|
467
|
-
await this.
|
|
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.
|
|
56
|
+
await this._appendAudit(toAuditEntry(this._deps, command.agent, AuditAction.AUTHORIZE_DISPATCH, AuditOutcome.DENIED, `proof verification failed: ${detail}`, {
|
|
476
57
|
requestId: command.requestId,
|
|
477
|
-
|
|
58
|
+
...extraAudit,
|
|
478
59
|
}));
|
|
479
60
|
throw error;
|
|
480
61
|
}
|
|
481
62
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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,
|
|
516
|
-
}));
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
81
|
+
return grant;
|
|
570
82
|
}
|
|
571
|
-
async
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
}
|
|
609
|
-
}
|
|
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, domain, request) {
|
|
84
|
+
this._assertOwnerPrincipal(actor);
|
|
85
|
+
const now = this._deps.clock.nowIso();
|
|
86
|
+
const grant = {
|
|
623
87
|
vaultId: this._deps.vaultId,
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
+
domain,
|
|
90
|
+
status: "approved",
|
|
91
|
+
requestedAt: now,
|
|
92
|
+
grantedAt: now,
|
|
707
93
|
};
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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 ${domain} for secret ${secretAlias}`, {
|
|
96
|
+
requestId: request?.requestId,
|
|
97
|
+
secretAlias,
|
|
98
|
+
domain,
|
|
99
|
+
}));
|
|
100
|
+
return grant;
|
|
734
101
|
}
|
|
735
|
-
async
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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, domain, request) {
|
|
112
|
+
this._assertOwnerPrincipal(actor);
|
|
113
|
+
await this._deps.secretDestinationGrants.delete(this._deps.vaultId, secretAlias, domain);
|
|
114
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.REVOKE_SECRET_DESTINATION, AuditOutcome.SUCCEEDED, `revoked destination ${domain} from secret ${secretAlias}`, {
|
|
115
|
+
requestId: request?.requestId,
|
|
116
|
+
secretAlias,
|
|
117
|
+
domain,
|
|
777
118
|
}));
|
|
778
119
|
}
|
|
779
|
-
async
|
|
780
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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 domain = extractDomain(targetUrl);
|
|
143
|
+
const destGrant = await this._deps.secretDestinationGrants.get(this._deps.vaultId, secretAlias, domain);
|
|
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: "
|
|
906
|
-
reason:
|
|
907
|
-
secretId:
|
|
908
|
-
|
|
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"
|
|
914
|
-
|
|
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.AUTHORIZE_DISPATCH, 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
|
-
|
|
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.STALL_DISPATCH, AuditOutcome.ALLOWED, "request stalled for hitl 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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
948
|
-
secretId:
|
|
218
|
+
secretAlias: request.secretAlias,
|
|
219
|
+
secretId: secretId.value,
|
|
949
220
|
}));
|
|
950
|
-
await this.
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
await this.
|
|
961
|
-
|
|
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
|
-
|
|
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 (
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
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 domain = 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
|
+
domain,
|
|
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
|
+
domain,
|
|
287
|
+
}));
|
|
1009
288
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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.
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
|
317
|
+
return result;
|
|
1046
318
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
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.
|
|
1062
|
-
|
|
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,269 @@ 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
|
|
1077
|
-
|
|
1078
|
-
|
|
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);
|
|
1089
|
-
}
|
|
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))));
|
|
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));
|
|
1097
372
|
}
|
|
1098
|
-
async agentGetRequest(
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
await this.
|
|
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} (${command.agentRecord.rootAgentId})`, { rootAgentId: command.agentRecord.rootAgentId }));
|
|
1143
397
|
}
|
|
1144
|
-
async
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
await this.
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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", { secretAlias: command.alias, secretId: secretId.value }));
|
|
427
|
+
return record;
|
|
1166
428
|
}
|
|
1167
|
-
async
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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 = {
|
|
1174
438
|
...existing,
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
requestId: command.requestId,
|
|
1183
|
-
agentId: command.agentId,
|
|
1184
|
-
capabilityId: command.capabilityId,
|
|
1185
|
-
}));
|
|
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", { secretAlias: command.alias, secretId: secretId.value }));
|
|
445
|
+
return record;
|
|
1186
446
|
}
|
|
1187
|
-
async
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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", { secretAlias: command.alias, secretId: record.secretId.value }));
|
|
455
|
+
}
|
|
456
|
+
async ownerWriteSecret(command) {
|
|
457
|
+
this._assertOwnerPrincipal(command.owner ?? command.issuer); // Trusted issuer check in policy engine
|
|
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,
|
|
1202
472
|
};
|
|
473
|
+
await this._deps.secrets.save(record);
|
|
474
|
+
await this._deps.custody.store(secretId, command.plaintext);
|
|
475
|
+
return record;
|
|
476
|
+
}
|
|
477
|
+
async ownerReadAudit(actor, query) {
|
|
478
|
+
this._assertOwnerPrincipal(actor);
|
|
479
|
+
return await this._deps.audit.query(query);
|
|
480
|
+
}
|
|
481
|
+
async ownerExportSecret(actor, alias) {
|
|
482
|
+
this._assertOwnerPrincipal(actor);
|
|
483
|
+
const record = await this._deps.secrets.getByAlias({ value: alias });
|
|
484
|
+
if (!record)
|
|
485
|
+
throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
|
|
486
|
+
const plaintext = await this._deps.custody.load(record.secretId);
|
|
487
|
+
if (plaintext === null)
|
|
488
|
+
throw new VaultCoreError("secret material not found", "VAULT_SECRET_NOT_FOUND");
|
|
489
|
+
return { vaultId: this._deps.vaultId, secretId: record.secretId, alias: record.alias, plaintext, exportedAt: this._deps.clock.nowIso() };
|
|
490
|
+
}
|
|
491
|
+
async ownerListAgents(actor) {
|
|
492
|
+
this._assertOwnerPrincipal(actor);
|
|
493
|
+
const identities = await this._deps.agentRecords.list(this._deps.vaultId);
|
|
494
|
+
const sessionTokens = await this._deps.sessionTokens.list();
|
|
495
|
+
const tokensByAgentId = new Map();
|
|
496
|
+
for (const st of sessionTokens) {
|
|
497
|
+
const list = tokensByAgentId.get(st.rootAgentId) ?? [];
|
|
498
|
+
list.push(st);
|
|
499
|
+
tokensByAgentId.set(st.rootAgentId, list);
|
|
500
|
+
}
|
|
501
|
+
return identities.map(id => ({ ...id, sessionTokens: tokensByAgentId.get(id.rootAgentId) ?? [] }));
|
|
502
|
+
}
|
|
503
|
+
async ownerListRequests(actor, rootAgentId) {
|
|
504
|
+
this._assertOwnerPrincipal(actor);
|
|
505
|
+
const records = await this._deps.requests.list(this._deps.vaultId, rootAgentId);
|
|
506
|
+
return records.map(r => this.toOwnerVisibleRequestRecord(r));
|
|
507
|
+
}
|
|
508
|
+
async ownerGetRequest(actor, requestId) {
|
|
509
|
+
this._assertOwnerPrincipal(actor);
|
|
510
|
+
const record = await this._deps.requests.get(this._deps.vaultId, requestId);
|
|
511
|
+
if (!record)
|
|
512
|
+
throw new VaultCoreError("request record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
513
|
+
return this.toOwnerRequestRecord(record);
|
|
514
|
+
}
|
|
515
|
+
async ownerListSecrets(actor) {
|
|
516
|
+
this._assertOwnerPrincipal(actor);
|
|
517
|
+
const records = await this._deps.secrets.list(this._deps.vaultId);
|
|
518
|
+
return records.map(r => ({ ...r, granted: true })); // Owner sees all secrets as "granted"
|
|
519
|
+
}
|
|
520
|
+
async ownerIssueSessionToken(request) {
|
|
521
|
+
this._assertOwnerPrincipal(request.actor);
|
|
522
|
+
const token = await this._deps.sessionTokens.issue(request.rootAgentId);
|
|
523
|
+
return { token, rootAgentId: request.rootAgentId, issuedAt: this._deps.clock.nowIso() };
|
|
1203
524
|
}
|
|
1204
525
|
async ownerIssueAllAgentSessionTokens(actor) {
|
|
1205
|
-
const agents = await this.
|
|
1206
|
-
|
|
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;
|
|
526
|
+
const agents = await this.ownerListAgents(actor);
|
|
527
|
+
return Promise.all(agents.map(a => this.ownerIssueSessionToken({ vaultId: this._deps.vaultId, actor, rootAgentId: a.rootAgentId })));
|
|
1218
528
|
}
|
|
1219
529
|
async ownerRevokeSessionToken(request) {
|
|
1220
|
-
|
|
1221
|
-
throw new VaultCoreError("session token vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
1222
|
-
}
|
|
530
|
+
this._assertOwnerPrincipal(request.actor);
|
|
1223
531
|
await this._deps.sessionTokens.revoke(request.token);
|
|
1224
|
-
await this._appendAudit(toAuditEntry(this._deps, request.actor, AuditAction.REVOKE_SESSION_TOKEN, AuditOutcome.SUCCEEDED, "session token revoked"));
|
|
1225
532
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
|
|
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));
|
|
533
|
+
// ─── Custom Flows ─────────────────────────────────────────────────────────────
|
|
534
|
+
async ownerRegisterCustomFlow(command) {
|
|
535
|
+
this._assertOwnerPrincipal(command.owner);
|
|
536
|
+
const flow = { ...command.flow, vaultId: this._deps.vaultId, ownerId: command.owner.id, createdAt: command.requestedAt };
|
|
537
|
+
await this._deps.customFlows.register(flow);
|
|
1233
538
|
}
|
|
1234
|
-
async
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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,
|
|
539
|
+
async _storeCustomFlowSecret(flow, alias, plaintext) {
|
|
540
|
+
const source = { kind: "request", requestId: `flow:${flow.flowId}:${Date.now()}` };
|
|
541
|
+
await this.ownerWriteSecret({ vaultId: flow.vaultId, alias, plaintext, source, issuerSiteId: flow.ownerId, requestedAt: this._deps.clock.nowIso() });
|
|
542
|
+
}
|
|
543
|
+
// ─── Event Observers ──────────────────────────────────────────────────────────
|
|
544
|
+
_requestObservers = [];
|
|
545
|
+
ownerOnPendingDispatch(callback) {
|
|
546
|
+
this._requestObservers.push(callback);
|
|
547
|
+
return () => {
|
|
548
|
+
const idx = this._requestObservers.indexOf(callback);
|
|
549
|
+
if (idx >= 0)
|
|
550
|
+
this._requestObservers.splice(idx, 1);
|
|
1254
551
|
};
|
|
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
552
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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");
|
|
553
|
+
// Legacy name for compatibility or migration
|
|
554
|
+
ownerOnGrantState(callback) {
|
|
555
|
+
return this.ownerOnPendingDispatch(callback);
|
|
1271
556
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
557
|
+
// ─── Internal Helpers ──────────────────────────────────────────────────────────
|
|
558
|
+
async _recordRequestInternal(request, result, missingGrants) {
|
|
559
|
+
const record = {
|
|
560
|
+
vaultId: this._deps.vaultId,
|
|
561
|
+
requestId: request.requestId,
|
|
562
|
+
rootAgentId: request.agent.id,
|
|
563
|
+
reason: request.reason,
|
|
564
|
+
createdAt: this._deps.clock.nowIso(),
|
|
565
|
+
request: {
|
|
566
|
+
targetUrl: request.targetUrl,
|
|
567
|
+
method: request.method,
|
|
568
|
+
headers: request.headers,
|
|
569
|
+
body: request.body,
|
|
570
|
+
secretAlias: request.secretAlias,
|
|
571
|
+
},
|
|
572
|
+
response: {
|
|
573
|
+
status: result.responseStatus,
|
|
574
|
+
body: result.responseBody,
|
|
575
|
+
error: result.error,
|
|
576
|
+
},
|
|
577
|
+
execution: { status: result.status },
|
|
578
|
+
missingGrants,
|
|
579
|
+
};
|
|
580
|
+
await this._deps.requests.save(record);
|
|
581
|
+
if (result.status === DispatchStatus.PENDING) {
|
|
582
|
+
this._requestObservers.forEach(obs => obs(record));
|
|
1276
583
|
}
|
|
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
584
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
585
|
+
toAgentVisibleRequestRecord(record) {
|
|
586
|
+
return {
|
|
587
|
+
requestId: record.requestId,
|
|
588
|
+
createdAt: record.createdAt,
|
|
589
|
+
reason: record.reason,
|
|
590
|
+
targetUrl: record.request.targetUrl,
|
|
591
|
+
executionStatus: record.execution.status,
|
|
592
|
+
responseStatus: record.response?.status,
|
|
593
|
+
error: record.response?.error,
|
|
594
|
+
hasResponseBody: !!record.response?.body,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
toOwnerVisibleRequestRecord(record) {
|
|
598
|
+
return {
|
|
599
|
+
requestId: record.requestId,
|
|
600
|
+
createdAt: record.createdAt,
|
|
601
|
+
rootAgentId: record.rootAgentId,
|
|
602
|
+
reason: record.reason,
|
|
603
|
+
targetUrl: record.request.targetUrl,
|
|
604
|
+
executionStatus: record.execution.status,
|
|
605
|
+
responseStatus: record.response?.status,
|
|
606
|
+
error: record.response?.error,
|
|
607
|
+
hasResponseBody: !!record.response?.body,
|
|
608
|
+
missingGrants: record.missingGrants,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
toOwnerRequestRecord(record) {
|
|
612
|
+
return {
|
|
613
|
+
requestId: record.requestId,
|
|
614
|
+
createdAt: record.createdAt,
|
|
615
|
+
rootAgentId: record.rootAgentId,
|
|
616
|
+
reason: record.reason,
|
|
617
|
+
request: {
|
|
618
|
+
targetUrl: record.request.targetUrl,
|
|
619
|
+
method: record.request.method,
|
|
620
|
+
headers: record.request.headers,
|
|
621
|
+
body: record.request.body,
|
|
622
|
+
secretAlias: record.request.secretAlias,
|
|
623
|
+
},
|
|
624
|
+
response: record.response,
|
|
625
|
+
executionStatus: record.execution.status,
|
|
626
|
+
missingGrants: record.missingGrants,
|
|
1302
627
|
};
|
|
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
628
|
}
|
|
1311
629
|
}
|
|
1312
630
|
export function createVaultCore(deps) {
|