@opengeni/core 3.0.1 → 3.1.2
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 +4 -4
- package/dist/access/index.d.ts +131 -7
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +1 -1
- package/dist/application/connect-operation.d.ts +6 -0
- package/dist/application/external-workspace-members.d.ts +13 -1
- package/dist/application/modal-native-live-declaration-join.d.ts +37 -0
- package/dist/application/modal-native-original-configuration.d.ts +79 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +29 -2
- package/dist/application/organization-privacy.d.ts +57 -0
- package/dist/application/prepared-mcp-connection.d.ts +31 -0
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +3 -0
- package/dist/application/session-tenancy.d.ts +13 -4
- package/dist/billing/agent-run-admission.d.ts +21 -0
- package/dist/billing/limits.d.ts +27 -1
- package/dist/billing/model-call-admission.d.ts +34 -0
- package/dist/billing/model-usage-settlement.d.ts +112 -0
- package/dist/canonical-human-identities.js +1 -1
- package/dist/{chunk-VCZFFEPB.js → chunk-7T2SZ7BI.js} +16 -2
- package/dist/chunk-7T2SZ7BI.js.map +1 -0
- package/dist/{chunk-ZYCNLYGH.js → chunk-OMJFIIWK.js} +178 -12
- package/dist/chunk-OMJFIIWK.js.map +1 -0
- package/dist/{chunk-H7X52RI2.js → chunk-QI2M2B2K.js} +83 -8
- package/dist/chunk-QI2M2B2K.js.map +1 -0
- package/dist/{chunk-OF65T3PM.js → chunk-TTWTXQB2.js} +94 -6
- package/dist/chunk-TTWTXQB2.js.map +1 -0
- package/dist/codex-model-availability.d.ts +55 -0
- package/dist/default-session-model.d.ts +192 -0
- package/dist/dependencies.d.ts +41 -7
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +8 -1
- package/dist/domain/capabilities.d.ts +112 -8
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +62 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +3 -1
- package/dist/domain/editable-artifacts/types.d.ts +1 -1
- package/dist/domain/encryption-key.d.ts +12 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +21 -3
- package/dist/domain/knowledge-files.d.ts +1 -1
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -76
- package/dist/domain/mcp-account-bindings.d.ts +17 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/personal-connection-delegations.d.ts +73 -12
- package/dist/domain/pr-review.d.ts +121 -6
- package/dist/domain/realtime-voice-billing.d.ts +103 -0
- package/dist/domain/resources.d.ts +17 -1
- package/dist/domain/scheduled-task-access.d.ts +265 -0
- package/dist/domain/scheduled-task-account-audit.d.ts +27 -0
- package/dist/domain/scheduled-task-audience.d.ts +70 -0
- package/dist/domain/scheduled-tasks.d.ts +61 -18
- package/dist/domain/session-connection-accounts.d.ts +53 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +102 -4
- package/dist/domain/sessions.d.ts +179 -15
- package/dist/domain/site-audience.d.ts +52 -0
- package/dist/domain/skills.d.ts +29 -1
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/slack-publication-secret-safety.d.ts +1 -1
- package/dist/domain/vaults/index.d.ts +7 -0
- package/dist/domain/vaults/mapping.d.ts +25 -0
- package/dist/domain/vaults/provider-registry.d.ts +72 -0
- package/dist/domain/vaults/provider-sync.d.ts +176 -0
- package/dist/domain/vaults/provider-writes.d.ts +57 -0
- package/dist/domain/vaults/secret-resolver.d.ts +34 -0
- package/dist/domain/vaults/types.d.ts +24 -0
- package/dist/domain/vaults/vault-service.d.ts +35 -0
- package/dist/domain/voice-input-billing.d.ts +77 -0
- package/dist/domain/web-search-billing.d.ts +51 -0
- package/dist/domain/workspace-credits.d.ts +7 -0
- package/dist/editable-artifact-live/application.d.ts +2 -1
- package/dist/editable-artifact-live/server.d.ts +5 -1
- package/dist/editable-artifact-live/ticket.d.ts +2 -1
- package/dist/editable-artifact-live/types.d.ts +9 -0
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -2
- package/dist/goal-admission.d.ts +627 -0
- package/dist/host-session-usage-limit.d.ts +15 -0
- package/dist/index.d.ts +43 -8
- package/dist/index.js +22682 -11743
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +110 -7
- package/dist/model-call.d.ts +58 -0
- package/dist/model-catalog.d.ts +29 -3
- package/dist/model-provider-credentials.d.ts +43 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +1 -13
- package/dist/sandbox/fleet.d.ts +58 -4
- package/dist/sandbox/routing.d.ts +42 -2
- package/dist/sandbox/runtime-settings.d.ts +27 -17
- package/dist/session-authorization.d.ts +10 -0
- package/dist/session-usage-limit-metrics.d.ts +60 -0
- package/dist/session-usage-limits.d.ts +89 -0
- package/dist/site-agent-clamp.d.ts +71 -0
- package/dist/site-session-origin.d.ts +39 -2
- package/dist/transcription.d.ts +120 -3
- package/dist/turn-surface.d.ts +20 -0
- package/dist/user-presence.d.ts +32 -0
- package/dist/workflow-wake-contract.d.ts +13 -1
- package/package.json +15 -22
- package/src/access/index.ts +961 -38
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +9 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +3 -2
- package/src/application/connect-authority.ts +3 -3
- package/src/application/connect-operation.ts +37 -3
- package/src/application/external-continuation.ts +20 -3
- package/src/application/external-link-work-admission.ts +3 -0
- package/src/application/external-workspace-members.ts +51 -3
- package/src/application/modal-native-live-declaration-join.ts +196 -0
- package/src/application/modal-native-original-configuration.ts +261 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +172 -55
- package/src/application/organization-privacy.ts +161 -0
- package/src/application/prepared-mcp-connection.ts +133 -0
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +73 -1
- package/src/application/session-mcp-credential-rotation.ts +61 -18
- package/src/application/session-tenancy.ts +262 -44
- package/src/application/user-resource-grants.ts +1 -29
- package/src/billing/agent-run-admission.ts +117 -0
- package/src/billing/limits.ts +138 -14
- package/src/billing/model-call-admission.ts +118 -0
- package/src/billing/model-usage-settlement.ts +472 -0
- package/src/codex-model-availability.ts +264 -0
- package/src/default-session-model.ts +692 -0
- package/src/dependencies.ts +48 -6
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +76 -2
- package/src/domain/capabilities.ts +830 -181
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +572 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +44 -6
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +20 -0
- package/src/domain/editable-artifacts/types.ts +1 -1
- package/src/domain/encryption-key.ts +26 -0
- package/src/domain/file-owner.ts +16 -1
- package/src/domain/github-action-policies.ts +1 -1
- package/src/domain/github-skill-source.ts +3 -1
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +167 -43
- package/src/domain/knowledge-files.ts +10 -3
- package/src/domain/knowledge-messages.ts +1 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +617 -5
- package/src/domain/knowledge-search.ts +240 -23
- package/src/domain/mcp-account-bindings.ts +169 -0
- package/src/domain/mcp-account-routes.ts +152 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/personal-connection-delegations.ts +399 -257
- package/src/domain/pr-review.ts +24 -8
- package/src/domain/realtime-voice-billing.ts +425 -0
- package/src/domain/resources.ts +50 -4
- package/src/domain/scheduled-task-access.ts +1089 -0
- package/src/domain/scheduled-task-account-audit.ts +90 -0
- package/src/domain/scheduled-task-audience.ts +249 -0
- package/src/domain/scheduled-tasks.ts +1313 -374
- package/src/domain/session-connection-accounts.ts +239 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +60 -0
- package/src/domain/session-tool-policy.ts +489 -14
- package/src/domain/sessions.ts +2254 -717
- package/src/domain/site-audience.ts +130 -0
- package/src/domain/skills.ts +9 -0
- package/src/domain/slack-bot.ts +141 -4
- package/src/domain/slack-publication-secret-safety.ts +1 -1
- package/src/domain/vaults/index.ts +19 -0
- package/src/domain/vaults/mapping.ts +190 -0
- package/src/domain/vaults/provider-registry.ts +201 -0
- package/src/domain/vaults/provider-sync.ts +531 -0
- package/src/domain/vaults/provider-writes.ts +288 -0
- package/src/domain/vaults/secret-resolver.ts +464 -0
- package/src/domain/vaults/types.ts +26 -0
- package/src/domain/vaults/vault-service.ts +1316 -0
- package/src/domain/voice-input-billing.ts +330 -0
- package/src/domain/web-search-billing.ts +169 -0
- package/src/domain/workspace-credits.ts +13 -0
- package/src/editable-artifact-live/application.ts +8 -1
- package/src/editable-artifact-live/server.ts +154 -6
- package/src/editable-artifact-live/ticket.ts +86 -1
- package/src/editable-artifact-live/types.ts +10 -0
- package/src/goal-admission.ts +220 -0
- package/src/host-session-usage-limit.ts +25 -0
- package/src/index.ts +75 -11
- package/src/managed-session.ts +137 -6
- package/src/model-call.ts +79 -0
- package/src/model-catalog.ts +345 -91
- package/src/model-provider-credentials.ts +394 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +23 -111
- package/src/sandbox/fleet.ts +253 -28
- package/src/sandbox/routing.ts +204 -43
- package/src/sandbox/runtime-settings.ts +159 -85
- package/src/session-authorization.ts +172 -23
- package/src/session-usage-limit-metrics.ts +95 -0
- package/src/session-usage-limits.ts +415 -0
- package/src/site-agent-clamp.ts +226 -0
- package/src/site-session-origin.ts +84 -5
- package/src/transcription.ts +123 -4
- package/src/turn-surface.ts +47 -0
- package/src/user-presence.ts +134 -0
- package/src/workflow-wake-contract.ts +14 -1
- package/dist/application/host-mcp-owner.d.ts +0 -6
- package/dist/application/host-mcp-resolvers.d.ts +0 -13
- package/dist/chunk-5WZYFL45.js +0 -266
- package/dist/chunk-5WZYFL45.js.map +0 -1
- package/dist/chunk-H7X52RI2.js.map +0 -1
- package/dist/chunk-OF65T3PM.js.map +0 -1
- package/dist/chunk-VCZFFEPB.js.map +0 -1
- package/dist/chunk-ZYCNLYGH.js.map +0 -1
- package/dist/domain/connection-authority.d.ts +0 -97
- package/dist/domain/connection-authority.js +0 -169
- package/dist/domain/connection-authority.js.map +0 -1
- package/dist/domain/environments.d.ts +0 -33
- package/dist/domain/host-mcp-authority-source-admission.d.ts +0 -10
- package/dist/domain/host-mcp-task-admission.d.ts +0 -18
- package/dist/domain/packs.d.ts +0 -59
- package/dist/domain/product-integration-pack.d.ts +0 -44
- package/dist/domain/product-integration-skill.gen.d.ts +0 -5
- package/dist/remote-mcp-credentials.d.ts +0 -23
- package/dist/remote-mcp-credentials.js +0 -11
- package/dist/remote-mcp-credentials.js.map +0 -1
- package/src/application/host-mcp-owner.ts +0 -41
- package/src/application/host-mcp-resolvers.ts +0 -84
- package/src/domain/connection-authority.ts +0 -329
- package/src/domain/environments.ts +0 -120
- package/src/domain/host-mcp-authority-source-admission.ts +0 -25
- package/src/domain/host-mcp-task-admission.ts +0 -121
- package/src/domain/packs.ts +0 -699
- package/src/domain/product-integration-pack.ts +0 -42
- package/src/domain/product-integration-skill.gen.ts +0 -52
- package/src/remote-mcp-credentials.ts +0 -361
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hosted secret resolver (§10B.2). One database roundtrip returns the
|
|
3
|
+
* principal's facts and every candidate for every reference; candidates are
|
|
4
|
+
* chosen here (archived dropped, lowest tier, names over aliases, otherwise
|
|
5
|
+
* ambiguous), provider values are fetched, hosted values decrypted, and the
|
|
6
|
+
* batch is recorded. A call resolves all of its references or none (I2).
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
buildMissingSecretAskCard,
|
|
10
|
+
formatSecretReference,
|
|
11
|
+
isValidTotpInput,
|
|
12
|
+
parseTotpInput,
|
|
13
|
+
referenceKey,
|
|
14
|
+
SecretReferenceError,
|
|
15
|
+
VAULT_LIMITS,
|
|
16
|
+
type ResolvedSecretValue,
|
|
17
|
+
type SecretMaterial,
|
|
18
|
+
type SecretPrincipal,
|
|
19
|
+
type SecretReference,
|
|
20
|
+
type SecretReferenceFailure,
|
|
21
|
+
type SecretResolver,
|
|
22
|
+
type SecretUseContext,
|
|
23
|
+
} from "@opengeni/contracts";
|
|
24
|
+
import {
|
|
25
|
+
decryptEnvironmentValue,
|
|
26
|
+
listVaultsForSubject,
|
|
27
|
+
recordVaultSecretUse,
|
|
28
|
+
resolveSecretReferenceCandidates,
|
|
29
|
+
type SecretReferenceCandidate,
|
|
30
|
+
type SecretReferenceLookup,
|
|
31
|
+
type SecretResolutionMeta,
|
|
32
|
+
} from "@opengeni/db";
|
|
33
|
+
import {
|
|
34
|
+
createProviderCallBudget,
|
|
35
|
+
EMPTY_VAULT_PROVIDER_REGISTRY,
|
|
36
|
+
isVaultProviderError,
|
|
37
|
+
type ProviderCallBudget,
|
|
38
|
+
type VaultProviderErrorCode,
|
|
39
|
+
type VaultProviderRegistry,
|
|
40
|
+
} from "@opengeni/vaults";
|
|
41
|
+
import { LINKED_VAULT_MISS_MAX_AGE_MS, refreshStaleLinkedVaults } from "./provider-sync";
|
|
42
|
+
import type { VaultServiceDeps } from "./types";
|
|
43
|
+
|
|
44
|
+
export type SecretResolverOptions = {
|
|
45
|
+
/** `agent.capabilities.secrets`; when false the resolver refuses every reference. */
|
|
46
|
+
capabilityEnabled: boolean;
|
|
47
|
+
/** Provider calls allowed for this attempt (Q-18); a fresh 50-call budget by default. */
|
|
48
|
+
attemptBudget?: ProviderCallBudget;
|
|
49
|
+
/**
|
|
50
|
+
* Connected vaults for callers that only learn the acting human from the
|
|
51
|
+
* lookup itself (browser and computer routes, `run_on`, the tool gateway):
|
|
52
|
+
* 1Password connection tokens load for that subject. `deps.providers` wins.
|
|
53
|
+
*/
|
|
54
|
+
providersForSubject?: (subjectId: string | null) => VaultProviderRegistry;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type Chosen = { reference: SecretReference; candidate: SecretReferenceCandidate };
|
|
58
|
+
|
|
59
|
+
function lookupFor(reference: SecretReference, index: number): SecretReferenceLookup {
|
|
60
|
+
return reference.target.form === "id"
|
|
61
|
+
? {
|
|
62
|
+
index,
|
|
63
|
+
form: "id",
|
|
64
|
+
itemId: reference.target.itemId,
|
|
65
|
+
field: reference.target.field,
|
|
66
|
+
otp: reference.otp,
|
|
67
|
+
previous: reference.previous,
|
|
68
|
+
}
|
|
69
|
+
: {
|
|
70
|
+
index,
|
|
71
|
+
form: "path",
|
|
72
|
+
segments: [...reference.target.segments],
|
|
73
|
+
otp: reference.otp,
|
|
74
|
+
previous: reference.previous,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readyReference(reference: SecretReference, candidate: SecretReferenceCandidate): string {
|
|
79
|
+
const tail = {
|
|
80
|
+
...(candidate.fieldName !== null ? { field: candidate.fieldName } : {}),
|
|
81
|
+
otp: reference.otp,
|
|
82
|
+
previous: reference.previous,
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
return formatSecretReference({
|
|
86
|
+
kind: reference.kind,
|
|
87
|
+
vault: candidate.vaultName,
|
|
88
|
+
item: candidate.itemName,
|
|
89
|
+
...tail,
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
return formatSecretReference({ kind: reference.kind, itemId: candidate.itemId, ...tail });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Why a reference without candidates fails: restricted reach is `denied`, else `not_found`. */
|
|
97
|
+
function missingCode(meta: SecretResolutionMeta): "denied" | "not_found" {
|
|
98
|
+
if (meta.kind === "attempt" && !meta.agentAllowed) return "denied";
|
|
99
|
+
if (meta.humanExpected && !meta.humanValid) return "denied";
|
|
100
|
+
if (!meta.hasSecretsUse) return "denied";
|
|
101
|
+
return "not_found";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function deniedMessage(meta: SecretResolutionMeta): string {
|
|
105
|
+
if (meta.kind === "attempt" && !meta.agentAllowed) {
|
|
106
|
+
return "this agent is not allowed to use secrets (secrets:use)";
|
|
107
|
+
}
|
|
108
|
+
if (meta.humanExpected && !meta.humanValid) {
|
|
109
|
+
return "the person this run acts for no longer has access to this workspace; only the task vault is available";
|
|
110
|
+
}
|
|
111
|
+
return "the person this run acts for cannot use secrets here; only the task vault is available";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Choose one candidate per reference (§10B.2): drop archived items, keep the
|
|
116
|
+
* lowest tier, prefer names over aliases; more than one remaining target is
|
|
117
|
+
* ambiguous and returns ready references to choose from.
|
|
118
|
+
*/
|
|
119
|
+
export function chooseSecretCandidate(
|
|
120
|
+
reference: SecretReference,
|
|
121
|
+
candidates: readonly SecretReferenceCandidate[],
|
|
122
|
+
meta: SecretResolutionMeta,
|
|
123
|
+
): { candidate: SecretReferenceCandidate } | { failure: SecretReferenceFailure } {
|
|
124
|
+
const live = candidates.filter((candidate) => !candidate.archived);
|
|
125
|
+
if (live.length === 0) {
|
|
126
|
+
const code = missingCode(meta);
|
|
127
|
+
return {
|
|
128
|
+
failure: {
|
|
129
|
+
raw: reference.raw,
|
|
130
|
+
code,
|
|
131
|
+
message: code === "denied" ? deniedMessage(meta) : "no reachable vault item matches",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const lowestTier = Math.min(...live.map((candidate) => candidate.tier));
|
|
136
|
+
let remaining = live.filter((candidate) => candidate.tier === lowestTier);
|
|
137
|
+
if (remaining.some((candidate) => candidate.matchedBy !== "alias")) {
|
|
138
|
+
remaining = remaining.filter((candidate) => candidate.matchedBy !== "alias");
|
|
139
|
+
}
|
|
140
|
+
const targets = new Map<string, SecretReferenceCandidate>();
|
|
141
|
+
for (const candidate of remaining) {
|
|
142
|
+
targets.set(`${candidate.itemId}|${candidate.fieldId ?? ""}`, candidate);
|
|
143
|
+
}
|
|
144
|
+
if (targets.size > 1) {
|
|
145
|
+
const choices = [...targets.values()].map((candidate) => readyReference(reference, candidate));
|
|
146
|
+
return {
|
|
147
|
+
failure: {
|
|
148
|
+
raw: reference.raw,
|
|
149
|
+
code: "ambiguous",
|
|
150
|
+
message: `${targets.size} vault items match; use one of the candidate references`,
|
|
151
|
+
candidates: [...new Set(choices)].sort(),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const candidate = [...targets.values()][0]!;
|
|
156
|
+
if (candidate.missing === "primary_field") {
|
|
157
|
+
return {
|
|
158
|
+
failure: {
|
|
159
|
+
raw: reference.raw,
|
|
160
|
+
code: "no_primary_field",
|
|
161
|
+
message: `${candidate.itemName} has no primary field; name the field ({{secret:${candidate.itemName}/field}})`,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (candidate.missing !== null) {
|
|
166
|
+
const messages = {
|
|
167
|
+
field: "the item has no field with that name",
|
|
168
|
+
totp_field: "the item has no one-time-password (TOTP) field",
|
|
169
|
+
previous_version: "the field has no previous version",
|
|
170
|
+
value: "the field has no value",
|
|
171
|
+
} as const;
|
|
172
|
+
return {
|
|
173
|
+
failure: { raw: reference.raw, code: "not_found", message: messages[candidate.missing] },
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (reference.previous && candidate.source !== "opengeni") {
|
|
177
|
+
return {
|
|
178
|
+
failure: {
|
|
179
|
+
raw: reference.raw,
|
|
180
|
+
code: "invalid_reference",
|
|
181
|
+
message: "#previous is not available for connected vaults",
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (reference.otp && candidate.fieldKind !== "totp") {
|
|
186
|
+
return {
|
|
187
|
+
failure: {
|
|
188
|
+
raw: reference.raw,
|
|
189
|
+
code: "invalid_reference",
|
|
190
|
+
message: `/otp needs a one-time-password field; ${candidate.fieldName ?? "the field"} is not one`,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { candidate };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function providerFailureCode(code: VaultProviderErrorCode): SecretReferenceFailure["code"] {
|
|
198
|
+
if (code === "provider_rate_limited") return "provider_rate_limited";
|
|
199
|
+
if (code === "provider_auth_expired" || code === "provider_auth_invalid") {
|
|
200
|
+
return "provider_auth_expired";
|
|
201
|
+
}
|
|
202
|
+
if (code === "provider_not_found") return "not_found";
|
|
203
|
+
if (code === "provider_permission_denied") return "denied";
|
|
204
|
+
return "provider_unavailable";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function decodeBase64(value: string): Uint8Array {
|
|
208
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function materialFromPlaintext(
|
|
212
|
+
reference: SecretReference,
|
|
213
|
+
kind: SecretReferenceCandidate["fieldKind"],
|
|
214
|
+
plaintext: string,
|
|
215
|
+
): SecretMaterial | SecretReferenceFailure {
|
|
216
|
+
if (reference.otp) {
|
|
217
|
+
if (!isValidTotpInput(plaintext)) {
|
|
218
|
+
return {
|
|
219
|
+
raw: reference.raw,
|
|
220
|
+
code: "invalid_reference",
|
|
221
|
+
message: "the one-time-password field does not hold a valid setup key",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { type: "totp", params: parseTotpInput(plaintext) };
|
|
225
|
+
}
|
|
226
|
+
if (kind === "file") return { type: "bytes", value: decodeBase64(plaintext) };
|
|
227
|
+
return { type: "text", value: plaintext };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isFailure(
|
|
231
|
+
value: SecretMaterial | SecretReferenceFailure,
|
|
232
|
+
): value is SecretReferenceFailure {
|
|
233
|
+
return "code" in value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* One provider-read budget per agent attempt in this process. The API builds a
|
|
238
|
+
* resolver per browser action, gateway call and `run_on`; sharing the budget
|
|
239
|
+
* keeps a loop of calls inside one attempt at the per-attempt read cap instead
|
|
240
|
+
* of a fresh cap per call.
|
|
241
|
+
*/
|
|
242
|
+
const attemptBudgets = new Map<string, ProviderCallBudget>();
|
|
243
|
+
const ATTEMPT_BUDGET_CACHE_LIMIT = 4096;
|
|
244
|
+
|
|
245
|
+
function sharedAttemptBudget(principal: SecretPrincipal): ProviderCallBudget {
|
|
246
|
+
if (principal.kind !== "attempt") {
|
|
247
|
+
return createProviderCallBudget(VAULT_LIMITS.providerReadsPerAttempt);
|
|
248
|
+
}
|
|
249
|
+
const key = `${principal.attemptId}:${principal.executionGeneration}`;
|
|
250
|
+
let budget = attemptBudgets.get(key);
|
|
251
|
+
if (!budget) {
|
|
252
|
+
budget = createProviderCallBudget(VAULT_LIMITS.providerReadsPerAttempt);
|
|
253
|
+
attemptBudgets.set(key, budget);
|
|
254
|
+
while (attemptBudgets.size > ATTEMPT_BUDGET_CACHE_LIMIT) {
|
|
255
|
+
attemptBudgets.delete(attemptBudgets.keys().next().value!);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return budget;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function createSecretResolver(
|
|
262
|
+
deps: VaultServiceDeps,
|
|
263
|
+
principal: SecretPrincipal,
|
|
264
|
+
opts: SecretResolverOptions,
|
|
265
|
+
): SecretResolver {
|
|
266
|
+
const budget = opts.attemptBudget ?? sharedAttemptBudget(principal);
|
|
267
|
+
const context = { kind: "principal" as const, principal };
|
|
268
|
+
|
|
269
|
+
async function resolve(
|
|
270
|
+
refs: readonly SecretReference[],
|
|
271
|
+
use: SecretUseContext,
|
|
272
|
+
): Promise<Map<string, ResolvedSecretValue>> {
|
|
273
|
+
const unique = new Map<string, SecretReference>();
|
|
274
|
+
for (const reference of refs) {
|
|
275
|
+
if (!unique.has(referenceKey(reference))) unique.set(referenceKey(reference), reference);
|
|
276
|
+
}
|
|
277
|
+
const resolved = new Map<string, ResolvedSecretValue>();
|
|
278
|
+
if (unique.size === 0) return resolved;
|
|
279
|
+
const references = [...unique.values()];
|
|
280
|
+
if (!opts.capabilityEnabled) {
|
|
281
|
+
throw new SecretReferenceError(
|
|
282
|
+
references.map((reference) => ({
|
|
283
|
+
raw: reference.raw,
|
|
284
|
+
code: "denied",
|
|
285
|
+
message: "secrets are turned off for this agent",
|
|
286
|
+
})),
|
|
287
|
+
null,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const choose = async () => {
|
|
292
|
+
const { meta, candidates } = await resolveSecretReferenceCandidates(deps.db, context, {
|
|
293
|
+
refs: references.map((reference, index) => lookupFor(reference, index)),
|
|
294
|
+
});
|
|
295
|
+
const failures: SecretReferenceFailure[] = [];
|
|
296
|
+
const chosen: Chosen[] = [];
|
|
297
|
+
references.forEach((reference, index) => {
|
|
298
|
+
const outcome = chooseSecretCandidate(
|
|
299
|
+
reference,
|
|
300
|
+
candidates.filter((candidate) => candidate.refIndex === index),
|
|
301
|
+
meta,
|
|
302
|
+
);
|
|
303
|
+
if ("failure" in outcome) failures.push(outcome.failure);
|
|
304
|
+
else chosen.push({ reference, candidate: outcome.candidate });
|
|
305
|
+
});
|
|
306
|
+
return { meta, failures, chosen };
|
|
307
|
+
};
|
|
308
|
+
let { meta, failures, chosen } = await choose();
|
|
309
|
+
// A reference to an item added straight in a connected provider (1Password)
|
|
310
|
+
// finds nothing until the vault's mirror catches up: refresh stale linked
|
|
311
|
+
// vaults once and look again before reporting it missing.
|
|
312
|
+
if (failures.some((failure) => failure.code === "not_found")) {
|
|
313
|
+
const registry = deps.providers ?? opts.providersForSubject?.(meta.subjectId);
|
|
314
|
+
if (registry) {
|
|
315
|
+
const reachable = await listVaultsForSubject(deps.db, context, {
|
|
316
|
+
workspaceId: principal.workspaceId,
|
|
317
|
+
});
|
|
318
|
+
const changed = await refreshStaleLinkedVaults({
|
|
319
|
+
db: deps.db,
|
|
320
|
+
context,
|
|
321
|
+
vaults: reachable,
|
|
322
|
+
providers: registry,
|
|
323
|
+
maxAgeMs: LINKED_VAULT_MISS_MAX_AGE_MS,
|
|
324
|
+
});
|
|
325
|
+
if (changed.length > 0) ({ meta, failures, chosen } = await choose());
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const hasActingHuman = meta.humanExpected && meta.humanValid;
|
|
329
|
+
if (failures.length > 0) {
|
|
330
|
+
throw new SecretReferenceError(
|
|
331
|
+
failures,
|
|
332
|
+
buildMissingSecretAskCard(failures, { hasActingHuman }),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Provider values: one readFields call per provider item.
|
|
337
|
+
const providerValues = new Map<string, SecretMaterial>();
|
|
338
|
+
const providerGroups = new Map<string, Chosen[]>();
|
|
339
|
+
for (const entry of chosen) {
|
|
340
|
+
if (entry.candidate.source === "opengeni") continue;
|
|
341
|
+
const groupKey = `${entry.candidate.vaultId}|${entry.candidate.itemId}`;
|
|
342
|
+
providerGroups.set(groupKey, [...(providerGroups.get(groupKey) ?? []), entry]);
|
|
343
|
+
}
|
|
344
|
+
for (const group of providerGroups.values()) {
|
|
345
|
+
const first = group[0]!.candidate;
|
|
346
|
+
try {
|
|
347
|
+
const registry =
|
|
348
|
+
deps.providers ??
|
|
349
|
+
opts.providersForSubject?.(meta.subjectId) ??
|
|
350
|
+
EMPTY_VAULT_PROVIDER_REGISTRY;
|
|
351
|
+
const provider = await registry.forVault({
|
|
352
|
+
source: first.source,
|
|
353
|
+
providerConnectionId: first.providerConnectionId,
|
|
354
|
+
});
|
|
355
|
+
// The provider charges `budget` per call (one readFields per item).
|
|
356
|
+
const values = await provider.readFields(
|
|
357
|
+
{ connectionId: first.providerConnectionId, budget },
|
|
358
|
+
first.providerVaultRef ?? "",
|
|
359
|
+
first.providerItemRef ?? "",
|
|
360
|
+
[...new Set(group.map((entry) => entry.candidate.providerFieldRef ?? ""))],
|
|
361
|
+
);
|
|
362
|
+
for (const value of values) {
|
|
363
|
+
providerValues.set(`${first.itemId}|${value.externalFieldId}`, value.material);
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const code = isVaultProviderError(error)
|
|
367
|
+
? providerFailureCode(error.code)
|
|
368
|
+
: "provider_unavailable";
|
|
369
|
+
for (const entry of group) {
|
|
370
|
+
failures.push({
|
|
371
|
+
raw: entry.reference.raw,
|
|
372
|
+
code,
|
|
373
|
+
message:
|
|
374
|
+
code === "provider_rate_limited"
|
|
375
|
+
? "the connected vault is rate limited; try again later"
|
|
376
|
+
: code === "provider_auth_expired"
|
|
377
|
+
? "the connected vault needs to be reconnected"
|
|
378
|
+
: "the connected vault is unavailable",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const { reference, candidate } of chosen) {
|
|
385
|
+
let material: SecretMaterial | SecretReferenceFailure;
|
|
386
|
+
if (candidate.source === "opengeni") {
|
|
387
|
+
if (deps.encryptionKey === null || candidate.valueEncrypted === null) {
|
|
388
|
+
material = {
|
|
389
|
+
raw: reference.raw,
|
|
390
|
+
code: "provider_unavailable",
|
|
391
|
+
message: "vault encryption is not configured on this server",
|
|
392
|
+
};
|
|
393
|
+
} else {
|
|
394
|
+
let plaintext: string | null = null;
|
|
395
|
+
try {
|
|
396
|
+
plaintext = decryptEnvironmentValue(deps.encryptionKey, candidate.valueEncrypted);
|
|
397
|
+
} catch {
|
|
398
|
+
plaintext = null;
|
|
399
|
+
}
|
|
400
|
+
material =
|
|
401
|
+
plaintext === null
|
|
402
|
+
? {
|
|
403
|
+
raw: reference.raw,
|
|
404
|
+
code: "provider_unavailable",
|
|
405
|
+
message: "the value cannot be decrypted",
|
|
406
|
+
}
|
|
407
|
+
: materialFromPlaintext(reference, candidate.fieldKind, plaintext);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
const value = providerValues.get(`${candidate.itemId}|${candidate.providerFieldRef ?? ""}`);
|
|
411
|
+
if (value === undefined) {
|
|
412
|
+
if (failures.some((failure) => failure.raw === reference.raw)) continue;
|
|
413
|
+
material = {
|
|
414
|
+
raw: reference.raw,
|
|
415
|
+
code: "not_found",
|
|
416
|
+
message: "the connected vault no longer has this field",
|
|
417
|
+
};
|
|
418
|
+
} else if (reference.otp && value.type === "text") {
|
|
419
|
+
material = materialFromPlaintext(reference, "totp", value.value);
|
|
420
|
+
} else {
|
|
421
|
+
material = value;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (isFailure(material)) {
|
|
425
|
+
failures.push(material);
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
resolved.set(referenceKey(reference), {
|
|
429
|
+
key: referenceKey(reference),
|
|
430
|
+
reference,
|
|
431
|
+
source: candidate.source,
|
|
432
|
+
vaultId: candidate.vaultId,
|
|
433
|
+
vaultName: candidate.vaultName,
|
|
434
|
+
itemId: candidate.itemId,
|
|
435
|
+
itemName: candidate.itemName,
|
|
436
|
+
fieldId: candidate.fieldId,
|
|
437
|
+
fieldName: candidate.fieldName ?? "",
|
|
438
|
+
version: candidate.version,
|
|
439
|
+
material,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (failures.length > 0) {
|
|
443
|
+
resolved.clear();
|
|
444
|
+
throw new SecretReferenceError(
|
|
445
|
+
failures,
|
|
446
|
+
buildMissingSecretAskCard(failures, { hasActingHuman }),
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
await recordVaultSecretUse(deps.db, context, {
|
|
451
|
+
use,
|
|
452
|
+
items: [...resolved.values()].map((value) => ({
|
|
453
|
+
itemId: value.itemId,
|
|
454
|
+
vaultId: value.vaultId,
|
|
455
|
+
fieldId: value.fieldId,
|
|
456
|
+
fieldName: value.fieldName,
|
|
457
|
+
reference: value.reference.raw,
|
|
458
|
+
})),
|
|
459
|
+
});
|
|
460
|
+
return resolved;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return { principal, enabled: opts.capabilityEnabled, resolve };
|
|
464
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AccessGrant, SecretClock } from "@opengeni/contracts";
|
|
2
|
+
import type { Database } from "@opengeni/db";
|
|
3
|
+
import type { VaultProviderRegistry } from "@opengeni/vaults";
|
|
4
|
+
|
|
5
|
+
export interface VaultServiceDeps {
|
|
6
|
+
db: Database;
|
|
7
|
+
/**
|
|
8
|
+
* The variable-set envelope key (`OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY`);
|
|
9
|
+
* null when unconfigured, which fails value reads and writes with
|
|
10
|
+
* `encryption_key_missing`.
|
|
11
|
+
*/
|
|
12
|
+
encryptionKey: Uint8Array | null;
|
|
13
|
+
/** Provider vaults (1Password, host); none configured by default. */
|
|
14
|
+
providers?: VaultProviderRegistry;
|
|
15
|
+
clock?: SecretClock;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A person or API key calling the vault API. Routes check the permission each
|
|
20
|
+
* operation needs; the actor carries what the service needs on top of that.
|
|
21
|
+
*/
|
|
22
|
+
export interface VaultRequestActor {
|
|
23
|
+
grant: AccessGrant;
|
|
24
|
+
/** The caller holds `account:admin` on the organization (organization vaults). */
|
|
25
|
+
accountAdmin: boolean;
|
|
26
|
+
}
|