@the-ai-company/cbio-node-runtime 1.63.3 → 1.63.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +111 -266
- package/dist/clients/owner/client.js.map +1 -1
- package/dist/clients/owner/contracts.d.ts +37 -75
- 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 -14
- package/dist/public-types.js +1 -8
- package/dist/public-types.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts +1 -3
- 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 -12
- 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 +95 -210
- package/dist/vault-core/contracts.js +8 -11
- package/dist/vault-core/contracts.js.map +1 -1
- package/dist/vault-core/core.d.ts +119 -62
- package/dist/vault-core/core.js +518 -1180
- package/dist/vault-core/core.js.map +1 -1
- package/dist/vault-core/defaults.d.ts +22 -44
- package/dist/vault-core/defaults.js +65 -234
- 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 +72 -119
- package/dist/vault-core/persistence.js +310 -427
- package/dist/vault-core/persistence.js.map +1 -1
- package/dist/vault-core/ports.d.ts +19 -30
- 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 +39 -119
- package/dist/vault-ingress/index.js +98 -456
- 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 +87 -0
- package/docs/PROCESS_ISOLATION.md +2 -2
- package/docs/REFERENCE.md +42 -224
- package/docs/api/README.md +48 -30
- 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/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 +264 -237
- 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 -13
- 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 -154
- 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 -121
- package/docs/api/interfaces/CreatePersistentVaultCoreDependenciesOptions.md +3 -131
- package/docs/api/interfaces/CreateVaultOptions.md +1 -125
- package/docs/api/interfaces/CreatedVault.md +2 -2
- 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 +401 -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 -125
- 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/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 +511 -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 +47 -63
- package/examples/process-isolation.ts +26 -35
- package/package.json +1 -1
- 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/VaultRegisterFlowInput.md +0 -77
- 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/CreateOwnerClientOptions.md +0 -7
- package/docs/api/type-aliases/OwnerAgentView.md +0 -7
- package/docs/api/type-aliases/OwnerClient.md +0 -13
- package/docs/api/type-aliases/OwnerGrantCapabilityInput.md +0 -7
- package/docs/api/type-aliases/OwnerPendingApprovalView.md +0 -7
- package/docs/api/type-aliases/OwnerRequestDetailView.md +0 -7
- package/docs/api/type-aliases/OwnerRequestSummaryView.md +0 -7
- package/docs/api/type-aliases/OwnerSecretView.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.EVALUATE_DISPATCH_POLICY, AuditOutcome.DENIED, `proof verification failed: ${detail}`, {
|
|
476
57
|
requestId: command.requestId,
|
|
477
|
-
|
|
478
|
-
}));
|
|
479
|
-
throw error;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
async ownerRegisterCapability(command) {
|
|
483
|
-
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
484
|
-
throw new VaultCoreError("capability registration vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
485
|
-
}
|
|
486
|
-
if (command.capability.vaultId.value !== this._deps.vaultId.value) {
|
|
487
|
-
throw new VaultCoreError("capability vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
488
|
-
}
|
|
489
|
-
if (command.capability.agentId !== command.capability.agentId.trim() || !command.capability.agentId.trim()) {
|
|
490
|
-
throw new VaultCoreError("capability agent id required", "VAULT_IDENTITY_DENIED");
|
|
491
|
-
}
|
|
492
|
-
if (!command.capability.capabilityId.trim()) {
|
|
493
|
-
throw new VaultCoreError("capability id required", "VAULT_IDENTITY_DENIED");
|
|
494
|
-
}
|
|
495
|
-
try {
|
|
496
|
-
await this._deps.capabilityStates.upsert({
|
|
497
|
-
...command.capability,
|
|
498
|
-
source: "owner_grant",
|
|
499
|
-
requestId: undefined,
|
|
500
|
-
requestedAt: command.capability.issuedAt,
|
|
501
|
-
writeGrant: "always",
|
|
502
|
-
writeGrantedAt: command.capability.issuedAt,
|
|
503
|
-
readGrant: [...command.capability.read.paths],
|
|
504
|
-
readGrantedAt: command.capability.issuedAt,
|
|
505
|
-
});
|
|
506
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CAPABILITY, AuditOutcome.SUCCEEDED, `capability registered: ${command.capability.capabilityId}`, {
|
|
507
|
-
capabilityId: command.capability.capabilityId,
|
|
508
|
-
operation: command.capability.operation,
|
|
509
|
-
}));
|
|
510
|
-
}
|
|
511
|
-
catch (error) {
|
|
512
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
513
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CAPABILITY, AuditOutcome.DENIED, detail, {
|
|
514
|
-
capabilityId: command.capability.capabilityId,
|
|
515
|
-
operation: command.capability.operation,
|
|
58
|
+
...extraAudit,
|
|
516
59
|
}));
|
|
517
60
|
throw error;
|
|
518
61
|
}
|
|
519
62
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
570
|
-
}
|
|
571
|
-
async _getCapability(vaultId, agentId, capabilityId) {
|
|
572
|
-
if (vaultId.value !== this._deps.vaultId.value) {
|
|
573
|
-
throw new VaultCoreError("capability lookup vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
574
|
-
}
|
|
575
|
-
const state = await this._deps.capabilityStates.getByCapabilityId(vaultId, agentId, capabilityId);
|
|
576
|
-
return state && state.capabilityId && state.issuedAt && state.writeGrant === "always"
|
|
577
|
-
? this._stateToGrantedCapability(state)
|
|
578
|
-
: null;
|
|
579
|
-
}
|
|
580
|
-
async ownerRegisterCustomFlow(command) {
|
|
581
|
-
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
582
|
-
throw new VaultCoreError("request template vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
583
|
-
}
|
|
584
|
-
if (!command.flow.flowId.trim()) {
|
|
585
|
-
throw new VaultCoreError("request template id required", "VAULT_IDENTITY_DENIED");
|
|
586
|
-
}
|
|
587
|
-
if (command.flow.mode !== "send_secret" && !command.flow.responseSecret) {
|
|
588
|
-
throw new VaultCoreError("request template response secret rule required", "VAULT_IDENTITY_DENIED");
|
|
589
|
-
}
|
|
590
|
-
try {
|
|
591
|
-
await this._deps.customFlows.register({
|
|
592
|
-
vaultId: this._deps.vaultId,
|
|
593
|
-
flowId: command.flow.flowId,
|
|
594
|
-
ownerId: command.owner.id,
|
|
595
|
-
mode: command.flow.mode,
|
|
596
|
-
targetUrl: command.flow.targetUrl,
|
|
597
|
-
method: command.flow.method,
|
|
598
|
-
responseVisibility: command.flow.responseVisibility,
|
|
599
|
-
responseSecret: command.flow.responseSecret,
|
|
600
|
-
createdAt: this._deps.clock.nowIso(),
|
|
601
|
-
});
|
|
602
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CUSTOM_FLOW, AuditOutcome.SUCCEEDED, `request template registered: ${command.flow.flowId}`));
|
|
603
|
-
}
|
|
604
|
-
catch (error) {
|
|
605
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
606
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.REGISTER_CUSTOM_FLOW, AuditOutcome.DENIED, detail));
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
81
|
+
return grant;
|
|
609
82
|
}
|
|
610
|
-
async
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
if (existing) {
|
|
615
|
-
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.REASSIGN_ALIAS, AuditOutcome.DENIED, "alias already bound to existing secret; explicit alias lifecycle required", {
|
|
616
|
-
secretAlias: existing.alias.value,
|
|
617
|
-
secretId: existing.secretId.value,
|
|
618
|
-
}));
|
|
619
|
-
throw new VaultCoreError("alias already bound to existing secret", "VAULT_WRITE_DENIED");
|
|
620
|
-
}
|
|
621
|
-
const record = buildSecretRecord(this._deps, {
|
|
622
|
-
kind: "owner.create_secret",
|
|
83
|
+
async ownerGrantSecretDestination(actor, secretAlias, siteId, request) {
|
|
84
|
+
this._assertOwnerPrincipal(actor);
|
|
85
|
+
const now = this._deps.clock.nowIso();
|
|
86
|
+
const grant = {
|
|
623
87
|
vaultId: this._deps.vaultId,
|
|
624
|
-
|
|
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
|
+
siteId,
|
|
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 "${siteId}" for secret "${secretAlias}"`, {
|
|
96
|
+
requestId: request?.requestId,
|
|
97
|
+
secretAlias,
|
|
98
|
+
siteId,
|
|
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, siteId, request) {
|
|
112
|
+
this._assertOwnerPrincipal(actor);
|
|
113
|
+
await this._deps.secretDestinationGrants.delete(this._deps.vaultId, secretAlias, siteId);
|
|
114
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.REVOKE_SECRET_DESTINATION, AuditOutcome.SUCCEEDED, `revoked destination "${siteId}" from secret "${secretAlias}"`, {
|
|
115
|
+
requestId: request?.requestId,
|
|
116
|
+
secretAlias,
|
|
117
|
+
siteId,
|
|
777
118
|
}));
|
|
778
119
|
}
|
|
779
|
-
async
|
|
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 siteId = extractDomain(targetUrl);
|
|
143
|
+
const destGrant = await this._deps.secretDestinationGrants.get(this._deps.vaultId, secretAlias, siteId);
|
|
144
|
+
const destApproved = destGrant?.status === "approved";
|
|
145
|
+
if (agentSecretApproved && destApproved) {
|
|
146
|
+
return { vaultId: this._deps.vaultId, decision: "allow", reason: "granted", secretId: secret.secretId };
|
|
147
|
+
}
|
|
148
|
+
const missingGrants = {
|
|
149
|
+
agentSecret: !agentSecretApproved,
|
|
150
|
+
secretDestination: !destApproved,
|
|
151
|
+
};
|
|
903
152
|
return {
|
|
904
153
|
vaultId: this._deps.vaultId,
|
|
905
|
-
decision: "
|
|
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.EVALUATE_DISPATCH_POLICY, AuditOutcome.DENIED, authorization.reason ?? "denied", {
|
|
173
|
+
requestId: request.requestId,
|
|
174
|
+
targetUrl: request.targetUrl,
|
|
175
|
+
secretAlias: request.secretAlias,
|
|
176
|
+
}));
|
|
177
|
+
await this._recordRequestInternal(request, result);
|
|
178
|
+
return result;
|
|
915
179
|
}
|
|
916
180
|
if (authorization.decision === "pending") {
|
|
917
|
-
|
|
181
|
+
const result = {
|
|
918
182
|
vaultId: this._deps.vaultId,
|
|
919
183
|
requestId: request.requestId,
|
|
920
184
|
status: DispatchStatus.PENDING,
|
|
921
185
|
targetUrl: request.targetUrl,
|
|
922
186
|
method: request.method,
|
|
923
187
|
};
|
|
188
|
+
await this._appendAudit(toAuditEntry(this._deps, request.agent, AuditAction.PENDING_DISPATCH_APPROVAL, AuditOutcome.ALLOWED, "request held for human approval", {
|
|
189
|
+
requestId: request.requestId,
|
|
190
|
+
targetUrl: request.targetUrl,
|
|
191
|
+
secretAlias: request.secretAlias,
|
|
192
|
+
}));
|
|
193
|
+
await this._recordRequestInternal(request, result, authorization.missingGrants);
|
|
194
|
+
return result;
|
|
924
195
|
}
|
|
925
|
-
|
|
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 siteId = extractDomain(record.request.targetUrl);
|
|
262
|
+
await Promise.all([
|
|
263
|
+
this._deps.agentSecretGrants.upsert({
|
|
264
|
+
vaultId: this._deps.vaultId,
|
|
265
|
+
rootAgentId: record.rootAgentId,
|
|
266
|
+
secretAlias,
|
|
267
|
+
status: "approved",
|
|
268
|
+
requestedAt: now,
|
|
269
|
+
grantedAt: now,
|
|
270
|
+
}),
|
|
271
|
+
this._deps.secretDestinationGrants.upsert({
|
|
272
|
+
vaultId: this._deps.vaultId,
|
|
273
|
+
secretAlias,
|
|
274
|
+
siteId,
|
|
275
|
+
status: "approved",
|
|
276
|
+
requestedAt: now,
|
|
277
|
+
grantedAt: now,
|
|
278
|
+
}),
|
|
279
|
+
]);
|
|
280
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.GRANT_AGENT_SECRET, AuditOutcome.SUCCEEDED, "granted during dispatch approval", {
|
|
281
|
+
rootAgentId: record.rootAgentId,
|
|
282
|
+
secretAlias,
|
|
283
|
+
}));
|
|
284
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.GRANT_SECRET_DESTINATION, AuditOutcome.SUCCEEDED, "granted during dispatch approval", {
|
|
285
|
+
secretAlias,
|
|
286
|
+
siteId,
|
|
287
|
+
}));
|
|
1009
288
|
}
|
|
1010
|
-
|
|
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,289 @@ export class VaultCore {
|
|
|
1071
362
|
source: record.source,
|
|
1072
363
|
createdAt: record.createdAt,
|
|
1073
364
|
updatedAt: record.updatedAt,
|
|
365
|
+
granted: approvedAliases.has(record.alias.value),
|
|
1074
366
|
}));
|
|
1075
367
|
}
|
|
1076
|
-
async
|
|
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);
|
|
368
|
+
async agentListRequests(command) {
|
|
369
|
+
await this._verifyAgentControlProof(command, "list_requests");
|
|
370
|
+
const records = await this._deps.requests.list(this._deps.vaultId, command.agent.id);
|
|
371
|
+
return records.map(r => this.toAgentVisibleRequestRecord(r));
|
|
1089
372
|
}
|
|
1090
|
-
async
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
await this._verifyAgentControlProof(request, "list_requests");
|
|
1095
|
-
const records = await this._deps.requests.list(this._deps.vaultId, request.agent.id);
|
|
1096
|
-
return Promise.all(records.map(async (record) => this.toVisibleRequestRecord(record, await this._resolveRequestState(record))));
|
|
1097
|
-
}
|
|
1098
|
-
async agentGetRequest(request) {
|
|
1099
|
-
if (request.vaultId.value !== this._deps.vaultId.value) {
|
|
1100
|
-
throw new VaultCoreError("read vault mismatch", "VAULT_READ_DENIED");
|
|
1101
|
-
}
|
|
1102
|
-
await this._verifyAgentControlProof(request, "read_request_result", { targetRequestId: request.targetRequestId });
|
|
1103
|
-
const record = await this._deps.requests.get(this._deps.vaultId, request.targetRequestId);
|
|
1104
|
-
if (!record || record.agentId !== request.agent.id) {
|
|
373
|
+
async agentGetRequest(command) {
|
|
374
|
+
await this._verifyAgentControlProof(command, "read_request");
|
|
375
|
+
const record = await this._deps.requests.get(this._deps.vaultId, command.targetRequestId);
|
|
376
|
+
if (!record || record.rootAgentId !== command.agent.id) {
|
|
1105
377
|
throw new VaultCoreError("request record not found", "VAULT_READ_DENIED");
|
|
1106
378
|
}
|
|
1107
|
-
|
|
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}"`, { 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: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
|
|
427
|
+
return record;
|
|
428
|
+
}
|
|
429
|
+
async ownerUpdateSecret(command) {
|
|
430
|
+
this._assertOwnerPrincipal(command.owner);
|
|
431
|
+
await this._deps.policy.authorizeWrite(command);
|
|
432
|
+
const existing = await this._deps.secrets.getByAlias({ value: command.alias });
|
|
433
|
+
if (!existing)
|
|
434
|
+
throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
|
|
435
|
+
const secretId = existing.secretId;
|
|
436
|
+
const now = this._deps.clock.nowIso();
|
|
437
|
+
const record = {
|
|
438
|
+
...existing,
|
|
439
|
+
version: this._deps.ids.newVersion(),
|
|
440
|
+
updatedAt: now,
|
|
441
|
+
};
|
|
442
|
+
await this._deps.secrets.save(record);
|
|
443
|
+
await this._deps.custody.store(secretId, command.plaintext);
|
|
444
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `secret updated: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
|
|
445
|
+
return record;
|
|
446
|
+
}
|
|
447
|
+
async ownerRemoveSecret(command) {
|
|
448
|
+
this._assertOwnerPrincipal(command.owner);
|
|
449
|
+
const record = await this._deps.secrets.getByAlias({ value: command.alias });
|
|
450
|
+
if (!record)
|
|
451
|
+
throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
|
|
452
|
+
await this._deps.secrets.delete(record.secretId);
|
|
453
|
+
await this._deps.custody.delete(record.secretId);
|
|
454
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.DELETE_SECRET, AuditOutcome.SUCCEEDED, `secret deleted: "${command.alias}"`, { secretAlias: command.alias, secretId: record.secretId.value }));
|
|
1166
455
|
}
|
|
1167
|
-
async
|
|
1168
|
-
|
|
456
|
+
async ownerWriteSecret(command) {
|
|
457
|
+
this._assertOwnerPrincipal(command.owner ?? command.issuer);
|
|
458
|
+
await this._deps.policy.authorizeWrite(command);
|
|
459
|
+
const existing = await this._deps.secrets.getByAlias({ value: command.alias });
|
|
460
|
+
const secretId = existing ? existing.secretId : this._deps.ids.newSecretId();
|
|
461
|
+
const now = this._deps.clock.nowIso();
|
|
462
|
+
const record = {
|
|
463
|
+
vaultId: command.vaultId,
|
|
464
|
+
secretId,
|
|
465
|
+
alias: { value: command.alias },
|
|
466
|
+
version: this._deps.ids.newVersion(),
|
|
467
|
+
lifecycleStatus: "ACTIVE",
|
|
468
|
+
issuerId: command.issuer?.id ?? null,
|
|
469
|
+
source: command.source,
|
|
470
|
+
createdAt: existing ? existing.createdAt : now,
|
|
471
|
+
updatedAt: now,
|
|
472
|
+
};
|
|
473
|
+
await this._deps.secrets.save(record);
|
|
474
|
+
await this._deps.custody.store(secretId, command.plaintext);
|
|
475
|
+
// Generic write doesn't have a specific audit message here, assuming it's called by Create/Update which do their own audit.
|
|
476
|
+
// However, if called directly:
|
|
1169
477
|
if (!existing) {
|
|
1170
|
-
|
|
478
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner ?? command.issuer, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `secret created via generic write: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
|
|
1171
479
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
480
|
+
else {
|
|
481
|
+
await this._appendAudit(toAuditEntry(this._deps, command.owner ?? command.issuer, AuditAction.WRITE_SECRET, AuditOutcome.SUCCEEDED, `secret updated via generic write: "${command.alias}"`, { secretAlias: command.alias, secretId: secretId.value }));
|
|
482
|
+
}
|
|
483
|
+
return record;
|
|
484
|
+
}
|
|
485
|
+
async ownerReadAudit(actor, query) {
|
|
486
|
+
this._assertOwnerPrincipal(actor);
|
|
487
|
+
const entries = await this._deps.audit.query(query);
|
|
488
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_AUDIT, AuditOutcome.SUCCEEDED, "audit log accessed", { detail: JSON.stringify(query) }));
|
|
489
|
+
return entries;
|
|
490
|
+
}
|
|
491
|
+
async ownerExportSecret(actor, alias) {
|
|
492
|
+
this._assertOwnerPrincipal(actor);
|
|
493
|
+
const record = await this._deps.secrets.getByAlias({ value: alias });
|
|
494
|
+
if (!record)
|
|
495
|
+
throw new VaultCoreError("secret not found", "VAULT_SECRET_NOT_FOUND");
|
|
496
|
+
const plaintext = await this._deps.custody.load(record.secretId);
|
|
497
|
+
if (plaintext === null)
|
|
498
|
+
throw new VaultCoreError("secret material not found", "VAULT_SECRET_NOT_FOUND");
|
|
499
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.EXPORT_SECRET, AuditOutcome.SUCCEEDED, `secret exported as plaintext: "${alias}"`, { secretAlias: alias, secretId: record.secretId.value }));
|
|
500
|
+
return { vaultId: this._deps.vaultId, secretId: record.secretId, alias: record.alias, plaintext, exportedAt: this._deps.clock.nowIso() };
|
|
501
|
+
}
|
|
502
|
+
async ownerListAgents(actor) {
|
|
503
|
+
this._assertOwnerPrincipal(actor);
|
|
504
|
+
const identities = await this._deps.agentRecords.list(this._deps.vaultId);
|
|
505
|
+
const sessionTokens = await this._deps.sessionTokens.list();
|
|
506
|
+
const tokensByAgentId = new Map();
|
|
507
|
+
for (const st of sessionTokens) {
|
|
508
|
+
const list = tokensByAgentId.get(st.rootAgentId) ?? [];
|
|
509
|
+
list.push(st);
|
|
510
|
+
tokensByAgentId.set(st.rootAgentId, list);
|
|
511
|
+
}
|
|
512
|
+
const result = identities.map(id => ({ ...id, sessionTokens: tokensByAgentId.get(id.rootAgentId) ?? [] }));
|
|
513
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_AGENTS, AuditOutcome.SUCCEEDED, "agent identity list accessed"));
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
async ownerListRequests(actor, rootAgentId) {
|
|
517
|
+
this._assertOwnerPrincipal(actor);
|
|
518
|
+
const records = await this._deps.requests.list(this._deps.vaultId, rootAgentId);
|
|
519
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_REQUESTS, AuditOutcome.SUCCEEDED, "request list accessed"));
|
|
520
|
+
return records.map(r => this.toOwnerVisibleRequestRecord(r));
|
|
521
|
+
}
|
|
522
|
+
async ownerGetRequest(actor, requestId) {
|
|
523
|
+
this._assertOwnerPrincipal(actor);
|
|
524
|
+
const record = await this._deps.requests.get(this._deps.vaultId, requestId);
|
|
525
|
+
if (!record)
|
|
526
|
+
throw new VaultCoreError("request record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
527
|
+
const result = this.toOwnerRequestRecord(record);
|
|
528
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.READ_REQUEST, AuditOutcome.SUCCEEDED, `dispatch request detailed: "${requestId}"`, { requestId }));
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
async ownerListSecrets(actor) {
|
|
532
|
+
this._assertOwnerPrincipal(actor);
|
|
533
|
+
const records = await this._deps.secrets.list(this._deps.vaultId);
|
|
534
|
+
await this._appendAudit(toAuditEntry(this._deps, actor, AuditAction.LIST_SECRETS, AuditOutcome.SUCCEEDED, "secret list accessed"));
|
|
535
|
+
return records.map(r => ({
|
|
536
|
+
vaultId: r.vaultId,
|
|
537
|
+
secretId: r.secretId,
|
|
538
|
+
alias: r.alias,
|
|
539
|
+
version: r.version,
|
|
540
|
+
lifecycleStatus: r.lifecycleStatus ?? "ACTIVE",
|
|
541
|
+
issuerId: r.issuerId,
|
|
542
|
+
source: r.source,
|
|
543
|
+
createdAt: r.createdAt,
|
|
544
|
+
updatedAt: r.updatedAt,
|
|
545
|
+
granted: true,
|
|
1185
546
|
}));
|
|
1186
547
|
}
|
|
1187
548
|
async ownerIssueSessionToken(request) {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
if (!agent) {
|
|
1193
|
-
throw new VaultCoreError("agent identity not found", "VAULT_IDENTITY_DENIED");
|
|
1194
|
-
}
|
|
1195
|
-
const token = await this._deps.sessionTokens.issue(request.agentId);
|
|
1196
|
-
const issuedAt = this._deps.clock.nowIso();
|
|
1197
|
-
await this._appendAudit(toAuditEntry(this._deps, request.actor, AuditAction.ISSUE_SESSION_TOKEN, AuditOutcome.SUCCEEDED, `session token issued for agent: ${request.agentId}`));
|
|
1198
|
-
return {
|
|
1199
|
-
token,
|
|
1200
|
-
agentId: request.agentId,
|
|
1201
|
-
issuedAt,
|
|
1202
|
-
};
|
|
549
|
+
this._assertOwnerPrincipal(request.actor);
|
|
550
|
+
const token = await this._deps.sessionTokens.issue(request.rootAgentId);
|
|
551
|
+
await this._appendAudit(toAuditEntry(this._deps, request.actor, AuditAction.ISSUE_SESSION_TOKEN, AuditOutcome.SUCCEEDED, `session token issued for agent: "${request.rootAgentId}"`, { rootAgentId: request.rootAgentId }));
|
|
552
|
+
return { token, rootAgentId: request.rootAgentId, issuedAt: this._deps.clock.nowIso() };
|
|
1203
553
|
}
|
|
1204
554
|
async ownerIssueAllAgentSessionTokens(actor) {
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
for (const agent of agents) {
|
|
1209
|
-
results.push(await this.ownerIssueSessionToken({
|
|
1210
|
-
vaultId: this._deps.vaultId,
|
|
1211
|
-
requestId: this._deps.ids.newRequestId("warmup_session_token"),
|
|
1212
|
-
actor,
|
|
1213
|
-
agentId: agent.agentId,
|
|
1214
|
-
requestedAt,
|
|
1215
|
-
}));
|
|
1216
|
-
}
|
|
1217
|
-
return results;
|
|
555
|
+
this._assertOwnerPrincipal(actor);
|
|
556
|
+
const agents = await this.ownerListAgents(actor);
|
|
557
|
+
return Promise.all(agents.map(a => this.ownerIssueSessionToken({ vaultId: this._deps.vaultId, actor, rootAgentId: a.rootAgentId })));
|
|
1218
558
|
}
|
|
1219
559
|
async ownerRevokeSessionToken(request) {
|
|
1220
|
-
|
|
1221
|
-
throw new VaultCoreError("session token vault mismatch", "VAULT_IDENTITY_DENIED");
|
|
1222
|
-
}
|
|
560
|
+
this._assertOwnerPrincipal(request.actor);
|
|
1223
561
|
await this._deps.sessionTokens.revoke(request.token);
|
|
1224
562
|
await this._appendAudit(toAuditEntry(this._deps, request.actor, AuditAction.REVOKE_SESSION_TOKEN, AuditOutcome.SUCCEEDED, "session token revoked"));
|
|
1225
563
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
return (
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
async ownerApproveCapabilityRead(command) {
|
|
1235
|
-
if (command.vaultId.value !== this._deps.vaultId.value) {
|
|
1236
|
-
throw new VaultCoreError("write vault mismatch", "VAULT_WRITE_DENIED");
|
|
1237
|
-
}
|
|
1238
|
-
const pending = await this._deps.capabilityStates.getByRequestId(command.vaultId, command.requestId);
|
|
1239
|
-
if (!pending) {
|
|
1240
|
-
throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
1241
|
-
}
|
|
1242
|
-
if (pending.writeGrant == null) {
|
|
1243
|
-
throw new VaultCoreError("write decision required before read grant", "VAULT_WRITE_DENIED");
|
|
1244
|
-
}
|
|
1245
|
-
if (pending.readGrant != null) {
|
|
1246
|
-
throw new VaultCoreError("read grant already decided", "VAULT_WRITE_DENIED");
|
|
1247
|
-
}
|
|
1248
|
-
const decidedAt = this._deps.clock.nowIso();
|
|
1249
|
-
const next = {
|
|
1250
|
-
...pending,
|
|
1251
|
-
readGrant: [...(command.read?.paths ?? [])],
|
|
1252
|
-
readGrantedAt: decidedAt,
|
|
1253
|
-
decidedAt,
|
|
564
|
+
// ─── Event Observers ──────────────────────────────────────────────────────────
|
|
565
|
+
_requestObservers = [];
|
|
566
|
+
ownerOnPendingDispatch(callback) {
|
|
567
|
+
this._requestObservers.push(callback);
|
|
568
|
+
return () => {
|
|
569
|
+
const idx = this._requestObservers.indexOf(callback);
|
|
570
|
+
if (idx >= 0)
|
|
571
|
+
this._requestObservers.splice(idx, 1);
|
|
1254
572
|
};
|
|
1255
|
-
await this._deps.capabilityStates.upsert(next);
|
|
1256
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, AuditAction.APPROVE_CAPABILITY_READ, AuditOutcome.SUCCEEDED, `approved read policy for capability request ${command.requestId}`, {
|
|
1257
|
-
requestId: command.requestId,
|
|
1258
|
-
agentId: pending.agentId,
|
|
1259
|
-
operation: pending.operation,
|
|
1260
|
-
}));
|
|
1261
|
-
return next;
|
|
1262
573
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
if (!pending) {
|
|
1266
|
-
throw new VaultCoreError("capability action record not found", "VAULT_REQUEST_NOT_FOUND");
|
|
1267
|
-
}
|
|
1268
|
-
const decidedAt = this._deps.clock.nowIso();
|
|
1269
|
-
await this._deps.capabilityStates.upsert({ ...pending, writeGrant: "once", writeGrantedAt: decidedAt, decidedAt });
|
|
1270
|
-
return this._executePendingCapabilityState(command, "once");
|
|
574
|
+
ownerOnGrantState(callback) {
|
|
575
|
+
return this.ownerOnPendingDispatch(callback);
|
|
1271
576
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
577
|
+
// ─── Internal Helpers ──────────────────────────────────────────────────────────
|
|
578
|
+
async _recordRequestInternal(request, result, missingGrants) {
|
|
579
|
+
const record = {
|
|
580
|
+
vaultId: this._deps.vaultId,
|
|
581
|
+
requestId: request.requestId,
|
|
582
|
+
rootAgentId: request.agent.id,
|
|
583
|
+
reason: request.reason,
|
|
584
|
+
createdAt: this._deps.clock.nowIso(),
|
|
585
|
+
request: {
|
|
586
|
+
targetUrl: request.targetUrl,
|
|
587
|
+
method: request.method,
|
|
588
|
+
headers: request.headers,
|
|
589
|
+
body: request.body,
|
|
590
|
+
secretAlias: request.secretAlias,
|
|
591
|
+
},
|
|
592
|
+
response: {
|
|
593
|
+
status: result.responseStatus,
|
|
594
|
+
body: result.responseBody,
|
|
595
|
+
error: result.error,
|
|
596
|
+
},
|
|
597
|
+
execution: { status: result.status },
|
|
598
|
+
missingGrants,
|
|
599
|
+
};
|
|
600
|
+
await this._deps.requests.save(record);
|
|
601
|
+
if (result.status === DispatchStatus.PENDING) {
|
|
602
|
+
this._requestObservers.forEach(obs => obs(record));
|
|
1276
603
|
}
|
|
1277
|
-
const decidedAt = this._deps.clock.nowIso();
|
|
1278
|
-
await this._deps.capabilityStates.upsert({ ...pending, writeGrant: "always", writeGrantedAt: decidedAt, decidedAt });
|
|
1279
|
-
return this._executePendingCapabilityState(command, "grant");
|
|
1280
604
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
605
|
+
toAgentVisibleRequestRecord(record) {
|
|
606
|
+
return {
|
|
607
|
+
requestId: record.requestId,
|
|
608
|
+
createdAt: record.createdAt,
|
|
609
|
+
reason: record.reason,
|
|
610
|
+
targetUrl: record.request.targetUrl,
|
|
611
|
+
executionStatus: record.execution.status,
|
|
612
|
+
responseStatus: record.response?.status,
|
|
613
|
+
error: record.response?.error,
|
|
614
|
+
hasResponseBody: !!record.response?.body,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
toOwnerVisibleRequestRecord(record) {
|
|
618
|
+
return {
|
|
619
|
+
requestId: record.requestId,
|
|
620
|
+
createdAt: record.createdAt,
|
|
621
|
+
rootAgentId: record.rootAgentId,
|
|
622
|
+
reason: record.reason,
|
|
623
|
+
targetUrl: record.request.targetUrl,
|
|
624
|
+
executionStatus: record.execution.status,
|
|
625
|
+
responseStatus: record.response?.status,
|
|
626
|
+
error: record.response?.error,
|
|
627
|
+
hasResponseBody: !!record.response?.body,
|
|
628
|
+
missingGrants: record.missingGrants,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
toOwnerRequestRecord(record) {
|
|
632
|
+
return {
|
|
633
|
+
requestId: record.requestId,
|
|
634
|
+
createdAt: record.createdAt,
|
|
635
|
+
rootAgentId: record.rootAgentId,
|
|
636
|
+
reason: record.reason,
|
|
637
|
+
request: {
|
|
638
|
+
targetUrl: record.request.targetUrl,
|
|
639
|
+
method: record.request.method,
|
|
640
|
+
headers: record.request.headers,
|
|
641
|
+
body: record.request.body,
|
|
642
|
+
secretAlias: record.request.secretAlias,
|
|
643
|
+
},
|
|
644
|
+
response: record.response,
|
|
645
|
+
executionStatus: record.execution.status,
|
|
646
|
+
missingGrants: record.missingGrants,
|
|
1302
647
|
};
|
|
1303
|
-
await this._deps.capabilityStates.upsert(rejectedState);
|
|
1304
|
-
await this._appendAudit(toAuditEntry(this._deps, command.owner, rejectWrite ? AuditAction.REJECT_CAPABILITY_WRITE : AuditAction.REJECT_CAPABILITY_READ, AuditOutcome.SUCCEEDED, `rejected capability request ${command.requestId}`, {
|
|
1305
|
-
requestId: command.requestId,
|
|
1306
|
-
agentId: pending.agentId,
|
|
1307
|
-
operation: pending.operation,
|
|
1308
|
-
}));
|
|
1309
|
-
return rejectedState;
|
|
1310
648
|
}
|
|
1311
649
|
}
|
|
1312
650
|
export function createVaultCore(deps) {
|