@skaile/forge-settings 0.0.0-stage → 1.0.1
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/dist/module.d.mts +17 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +23 -0
- package/dist/runtime/ai-sdk/index.d.ts +46 -0
- package/dist/runtime/ai-sdk/index.js +31 -0
- package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +21 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue +435 -0
- package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +21 -0
- package/dist/runtime/editor/index.d.ts +58 -0
- package/dist/runtime/editor/index.js +72 -0
- package/dist/runtime/model/errors.d.ts +32 -0
- package/dist/runtime/model/errors.js +19 -0
- package/dist/runtime/server/credential-policy.d.ts +61 -0
- package/dist/runtime/server/credential-policy.js +65 -0
- package/dist/runtime/server/credential-store.d.ts +184 -0
- package/dist/runtime/server/credential-store.js +234 -0
- package/dist/runtime/server/env-layers.d.ts +62 -0
- package/dist/runtime/server/env-layers.js +183 -0
- package/dist/runtime/server/env.d.ts +30 -0
- package/dist/runtime/server/env.js +15 -0
- package/dist/runtime/server/harness-settings.d.ts +150 -0
- package/dist/runtime/server/harness-settings.js +166 -0
- package/dist/runtime/server/index.d.ts +23 -0
- package/dist/runtime/server/index.js +14 -0
- package/dist/runtime/server/provider-discovery.d.ts +73 -0
- package/dist/runtime/server/provider-discovery.js +77 -0
- package/dist/runtime/server/provider-layers.d.ts +27 -0
- package/dist/runtime/server/provider-layers.js +35 -0
- package/dist/runtime/server/provider-service.d.ts +126 -0
- package/dist/runtime/server/provider-service.js +87 -0
- package/dist/runtime/server/provider-settings.d.ts +78 -0
- package/dist/runtime/server/provider-settings.js +81 -0
- package/dist/runtime/server/provider-write.d.ts +154 -0
- package/dist/runtime/server/provider-write.js +221 -0
- package/dist/runtime/server/settings-defaults.d.ts +26 -0
- package/dist/runtime/server/settings-defaults.js +46 -0
- package/dist/runtime/shared/env-vars.d.ts +55 -0
- package/dist/runtime/shared/env-vars.js +22 -0
- package/dist/runtime/shared/guards.d.ts +2 -0
- package/dist/runtime/shared/guards.js +3 -0
- package/dist/runtime/shared/harness-providers.d.ts +62 -0
- package/dist/runtime/shared/harness-providers.js +38 -0
- package/dist/runtime/shared/harnesses.d.ts +19 -0
- package/dist/runtime/shared/harnesses.js +15 -0
- package/dist/runtime/shared/provider-registry.d.ts +255 -0
- package/dist/runtime/shared/provider-registry.js +112 -0
- package/dist/runtime/shared/settings-types.d.ts +84 -0
- package/dist/runtime/shared/settings-types.js +0 -0
- package/dist/runtime/shims-vue.d.ts +8 -0
- package/dist/runtime/vue/index.d.ts +14 -0
- package/dist/runtime/vue/index.js +6 -0
- package/dist/runtime/vue/providers-panel-model.d.ts +147 -0
- package/dist/runtime/vue/providers-panel-model.js +37 -0
- package/dist/runtime/vue/settings-modal-scope.d.ts +62 -0
- package/dist/runtime/vue/settings-modal-scope.js +2 -0
- package/dist/runtime/vue/useProvidersPanel.d.ts +115 -0
- package/dist/runtime/vue/useProvidersPanel.js +633 -0
- package/dist/types.d.mts +9 -0
- package/package.json +117 -4
- package/src/runtime/ai-sdk/index.ts +88 -0
- package/src/runtime/components/ForgeProvidersPanel.vue +449 -0
- package/src/runtime/editor/index.ts +146 -0
- package/src/runtime/model/errors.ts +52 -0
- package/src/runtime/server/credential-policy.ts +139 -0
- package/src/runtime/server/credential-store.ts +473 -0
- package/src/runtime/server/env-layers.ts +287 -0
- package/src/runtime/server/env.ts +46 -0
- package/src/runtime/server/harness-settings.ts +373 -0
- package/src/runtime/server/index.ts +24 -0
- package/src/runtime/server/provider-discovery.ts +164 -0
- package/src/runtime/server/provider-layers.ts +67 -0
- package/src/runtime/server/provider-service.ts +244 -0
- package/src/runtime/server/provider-settings.ts +192 -0
- package/src/runtime/server/provider-write.ts +433 -0
- package/src/runtime/server/settings-defaults.ts +87 -0
- package/src/runtime/shared/env-vars.ts +82 -0
- package/src/runtime/shared/guards.ts +4 -0
- package/src/runtime/shared/harness-providers.ts +118 -0
- package/src/runtime/shared/harnesses.ts +34 -0
- package/src/runtime/shared/provider-registry.ts +391 -0
- package/src/runtime/shared/settings-types.ts +87 -0
- package/src/runtime/shims-vue.d.ts +8 -0
- package/src/runtime/vue/index.ts +15 -0
- package/src/runtime/vue/providers-panel-model.ts +203 -0
- package/src/runtime/vue/settings-modal-scope.ts +66 -0
- package/src/runtime/vue/useProvidersPanel.ts +930 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@skaile/forge-settings/editor` — the editor's `models` port over a provider
|
|
3
|
+
* registry.
|
|
4
|
+
*
|
|
5
|
+
* `@skaile/forge-editor`'s AI routes ask the host's `ModelProvider`
|
|
6
|
+
* (`listModels` / `languageModel`) before their own runtime-config presets.
|
|
7
|
+
* This adapter answers from the host's registry: only `llm` entries on the
|
|
8
|
+
* `openai-completions` surface are offered, because the editor's transport is a
|
|
9
|
+
* chat-completions client (`@ai-sdk/openai-compatible`). A profile's id is the
|
|
10
|
+
* registry selector `<providerId>/<modelId>`, which `languageModel` receives
|
|
11
|
+
* back as `{ preset: id }`.
|
|
12
|
+
*
|
|
13
|
+
* The registry and the credential environment are supplied by the host on every
|
|
14
|
+
* call, so provider edits apply without a restart and nothing reads the ambient
|
|
15
|
+
* process. Failures are typed ({@link ModelResolutionError}); the host maps
|
|
16
|
+
* them to HTTP in `onFailure`.
|
|
17
|
+
*
|
|
18
|
+
* `@skaile/forge-editor` is an optional peer of this entry only, for its
|
|
19
|
+
* `ModelProvider` contract (types). `@ai-sdk/openai-compatible` builds the model.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
23
|
+
import type { ModelProfile, ModelRef } from "@skaile/forge-editor/ai";
|
|
24
|
+
import type { ModelProvider } from "@skaile/forge-editor/server";
|
|
25
|
+
import { ModelResolutionError, assertEntryAllowed } from "../model/errors";
|
|
26
|
+
import { type CredentialPolicy, entryPolicyRefusal } from "../server/credential-policy";
|
|
27
|
+
import { resolveEntryCredential } from "../server/credential-store";
|
|
28
|
+
import { listAIProviderModels } from "../server/provider-service";
|
|
29
|
+
import {
|
|
30
|
+
type AIProviderLlmEntry,
|
|
31
|
+
type AIProviderRegistryView,
|
|
32
|
+
entriesOfKind,
|
|
33
|
+
} from "../shared/provider-registry";
|
|
34
|
+
|
|
35
|
+
export { ModelResolutionError } from "../model/errors";
|
|
36
|
+
export type { ModelResolutionCode } from "../model/errors";
|
|
37
|
+
|
|
38
|
+
/** The editor can only speak chat completions. */
|
|
39
|
+
const EDITOR_SURFACE = "openai-completions";
|
|
40
|
+
|
|
41
|
+
/** Policy for an editor model call; see `CredentialPolicy`. */
|
|
42
|
+
export interface EditorModelPolicyOptions {
|
|
43
|
+
/** Which credential references may be resolved; refused entries are not offered. */
|
|
44
|
+
policy?: CredentialPolicy;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The chat-completions entries (with an endpoint) of `registry`, by id. */
|
|
48
|
+
function editorEntries(registry: AIProviderRegistryView): Map<string, AIProviderLlmEntry> {
|
|
49
|
+
const entries = new Map<string, AIProviderLlmEntry>();
|
|
50
|
+
for (const [id, entry] of entriesOfKind(registry, "llm")) {
|
|
51
|
+
if (entry.surface === EDITOR_SURFACE && entry.base_url) entries.set(id, entry);
|
|
52
|
+
}
|
|
53
|
+
return entries;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The registry's chat-completions models as editor model profiles, in registry
|
|
58
|
+
* order. With a policy, entries it refuses are left out.
|
|
59
|
+
*/
|
|
60
|
+
export function editorModelProfiles(
|
|
61
|
+
registry: AIProviderRegistryView,
|
|
62
|
+
options: EditorModelPolicyOptions = {},
|
|
63
|
+
): ModelProfile[] {
|
|
64
|
+
const entries = editorEntries(registry);
|
|
65
|
+
const allowed = (providerId: string) => {
|
|
66
|
+
const entry = entries.get(providerId);
|
|
67
|
+
return entry !== undefined && !entryPolicyRefusal(entry, options.policy);
|
|
68
|
+
};
|
|
69
|
+
return listAIProviderModels(registry)
|
|
70
|
+
.filter((model) => allowed(model.providerId))
|
|
71
|
+
.map((model) => ({
|
|
72
|
+
id: model.id,
|
|
73
|
+
label: `${model.providerId} · ${model.modelId}`,
|
|
74
|
+
endpoint: entries.get(model.providerId)!.base_url!,
|
|
75
|
+
model: model.providerModelId,
|
|
76
|
+
source: "preset" as const,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The language model of selector `ref.preset`, or `null` when the registry does
|
|
82
|
+
* not serve it (a browser-defined `{ endpoint, model, apiKey }` reference is the
|
|
83
|
+
* editor's own to resolve).
|
|
84
|
+
*
|
|
85
|
+
* @throws {ModelResolutionError} `policy_refused` when the policy refuses the
|
|
86
|
+
* entry (checked before its credential is read); `unresolved` when its
|
|
87
|
+
* credential reference does not resolve. Neither message carries a secret.
|
|
88
|
+
*/
|
|
89
|
+
export function editorLanguageModel(
|
|
90
|
+
registry: AIProviderRegistryView,
|
|
91
|
+
env: Record<string, string | undefined>,
|
|
92
|
+
ref: ModelRef,
|
|
93
|
+
options: EditorModelPolicyOptions = {},
|
|
94
|
+
) {
|
|
95
|
+
if (!("preset" in ref)) return null;
|
|
96
|
+
const model = listAIProviderModels(registry).find((candidate) => candidate.id === ref.preset);
|
|
97
|
+
const entry = model && editorEntries(registry).get(model.providerId);
|
|
98
|
+
if (!model || !entry) return null;
|
|
99
|
+
assertEntryAllowed(entry, model.providerId, options.policy);
|
|
100
|
+
const credential = resolveEntryCredential(entry, {
|
|
101
|
+
env,
|
|
102
|
+
...(options.policy ? { policy: options.policy } : {}),
|
|
103
|
+
});
|
|
104
|
+
if (credential.error && !credential.value) {
|
|
105
|
+
throw new ModelResolutionError(
|
|
106
|
+
"unresolved",
|
|
107
|
+
`provider '${model.providerId}': ${credential.error}`,
|
|
108
|
+
{ providerId: model.providerId },
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return createOpenAICompatible({
|
|
112
|
+
name: model.providerId,
|
|
113
|
+
baseURL: entry.base_url!,
|
|
114
|
+
...(credential.value ? { apiKey: credential.value } : {}),
|
|
115
|
+
...(entry.headers ? { headers: entry.headers } : {}),
|
|
116
|
+
}).chatModel(model.providerModelId);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Options for {@link createEditorModelProvider}. */
|
|
120
|
+
export interface EditorModelProviderOptions extends EditorModelPolicyOptions {
|
|
121
|
+
/** The registry to serve, read on every call. */
|
|
122
|
+
registry: () => AIProviderRegistryView | Promise<AIProviderRegistryView>;
|
|
123
|
+
/** The environment `env:` references resolve against, read on every call. */
|
|
124
|
+
env: () => Record<string, string | undefined> | Promise<Record<string, string | undefined>>;
|
|
125
|
+
/** Turn a typed failure into the host's own error (e.g. its HTTP error). Default: rethrow as is. */
|
|
126
|
+
onFailure?: (error: ModelResolutionError) => Error;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The editor's `models` port over the host's registry. */
|
|
130
|
+
export function createEditorModelProvider(options: EditorModelProviderOptions): ModelProvider {
|
|
131
|
+
return {
|
|
132
|
+
async languageModel(ref) {
|
|
133
|
+
try {
|
|
134
|
+
return editorLanguageModel(await options.registry(), await options.env(), ref, options);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof ModelResolutionError && options.onFailure)
|
|
137
|
+
throw options.onFailure(error);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
async listModels() {
|
|
142
|
+
const profiles = editorModelProfiles(await options.registry(), options);
|
|
143
|
+
return profiles.length > 0 ? profiles : null;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type CredentialPolicy, entryPolicyRefusal } from "../server/credential-policy";
|
|
2
|
+
import type { AIProviderEntry } from "../shared/provider-registry";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A model could not be resolved. Framework-free: the host decides which HTTP
|
|
6
|
+
* status (or tool error) each `code` becomes.
|
|
7
|
+
*
|
|
8
|
+
* `message` never carries a secret. `providerId` names the registry entry the
|
|
9
|
+
* selector routed to, when it got that far.
|
|
10
|
+
*/
|
|
11
|
+
export type ModelResolutionCode =
|
|
12
|
+
/** The registry has error diagnostics, so no selector can be trusted. */
|
|
13
|
+
| "registry_invalid"
|
|
14
|
+
/** No registry entry serves the selector. */
|
|
15
|
+
| "unknown_selector"
|
|
16
|
+
/** The host's credential policy refuses the entry; nothing was resolved. */
|
|
17
|
+
| "policy_refused"
|
|
18
|
+
/** The entry's credential, endpoint or provider SDK could not be resolved. */
|
|
19
|
+
| "unresolved";
|
|
20
|
+
|
|
21
|
+
export class ModelResolutionError extends Error {
|
|
22
|
+
readonly code: ModelResolutionCode;
|
|
23
|
+
readonly providerId?: string;
|
|
24
|
+
constructor(
|
|
25
|
+
code: ModelResolutionCode,
|
|
26
|
+
message: string,
|
|
27
|
+
options: { providerId?: string; cause?: unknown } = {},
|
|
28
|
+
) {
|
|
29
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
30
|
+
this.name = "ModelResolutionError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
if (options.providerId !== undefined) this.providerId = options.providerId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Throw `policy_refused` when `policy` refuses `entry`; no policy refuses
|
|
38
|
+
* nothing. The one place a model adapter turns a policy refusal into an error,
|
|
39
|
+
* so every adapter words it the same way. Nothing is resolved.
|
|
40
|
+
*/
|
|
41
|
+
export function assertEntryAllowed(
|
|
42
|
+
entry: AIProviderEntry,
|
|
43
|
+
providerId: string,
|
|
44
|
+
policy?: CredentialPolicy,
|
|
45
|
+
): void {
|
|
46
|
+
const refusal = entryPolicyRefusal(entry, policy);
|
|
47
|
+
if (refusal) {
|
|
48
|
+
throw new ModelResolutionError("policy_refused", `provider '${providerId}': ${refusal}`, {
|
|
49
|
+
providerId,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential policy — which references a host lets the credential layer touch.
|
|
3
|
+
*
|
|
4
|
+
* A reference names where a secret lives (`env:NAME`, `keyring:s/a`, `cmd:…`,
|
|
5
|
+
* `platform:…`). A host that stores provider entries an administrator typed,
|
|
6
|
+
* and runs next to its own secrets (a Directus token, an encryption key), must
|
|
7
|
+
* decide up front which references may be resolved at all. The decision is
|
|
8
|
+
* made *before* a reference is read: a refused reference is never resolved,
|
|
9
|
+
* never looked up in an environment and never passed on to a child process.
|
|
10
|
+
*
|
|
11
|
+
* The policy covers every place an entry names an environment variable:
|
|
12
|
+
*
|
|
13
|
+
* - `credential.ref` — by form, and for `env:` by variable name;
|
|
14
|
+
* - `${NAME}` interpolation tokens in `base_url`, `headers` and `query_params`,
|
|
15
|
+
* which a spawned runtime expands from its environment. Only bare variable
|
|
16
|
+
* names the `envNames` allow-list accepts pass; any other token is refused.
|
|
17
|
+
*
|
|
18
|
+
* No policy means no restriction: existing hosts keep their current reference
|
|
19
|
+
* handling. A policy is opt-in per host and states the host's rule explicitly.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { AIProviderEntry, AIProviderRegistryView } from "../shared/provider-registry";
|
|
23
|
+
import { parseCredentialRef, registryEntries } from "../shared/provider-registry";
|
|
24
|
+
|
|
25
|
+
/** The four credential reference forms. */
|
|
26
|
+
export type CredentialRefForm = "env" | "cmd" | "platform" | "keyring";
|
|
27
|
+
|
|
28
|
+
/** A host's rule for which credential references may be resolved. */
|
|
29
|
+
export interface CredentialPolicy {
|
|
30
|
+
/** Reference forms that may be resolved. Absent: every form. */
|
|
31
|
+
forms?: readonly CredentialRefForm[];
|
|
32
|
+
/**
|
|
33
|
+
* Environment variable names a reference or interpolation token may name:
|
|
34
|
+
* exact strings, or expressions that must match the whole name. Absent:
|
|
35
|
+
* any name.
|
|
36
|
+
*/
|
|
37
|
+
envNames?: ReadonlyArray<string | RegExp>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** An entry the policy refused, and why. The reason is secret-free. */
|
|
41
|
+
export interface PolicyRefusal {
|
|
42
|
+
providerId: string;
|
|
43
|
+
error: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const BARE_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
47
|
+
const INTERPOLATION_TOKEN = /\$\{([^}]*)\}/g;
|
|
48
|
+
|
|
49
|
+
/** Whether `name` passes the policy's `envNames` allow-list. */
|
|
50
|
+
export function isEnvNameAllowed(name: string, policy: CredentialPolicy): boolean {
|
|
51
|
+
if (!policy.envNames) return true;
|
|
52
|
+
return policy.envNames.some((rule) => {
|
|
53
|
+
if (typeof rule === "string") return rule === name;
|
|
54
|
+
const match = new RegExp(rule.source, rule.flags.replace(/[gy]/g, "")).exec(name);
|
|
55
|
+
return match !== null && match.index === 0 && match[0] === name;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The reason a credential reference is refused, or `undefined` when the policy
|
|
61
|
+
* lets it through. A string that is not a reference is not this function's
|
|
62
|
+
* concern: it resolves to nothing anyway.
|
|
63
|
+
*/
|
|
64
|
+
export function credentialRefRefusal(ref: string, policy: CredentialPolicy): string | undefined {
|
|
65
|
+
const parsed = parseCredentialRef(ref);
|
|
66
|
+
if (!parsed) return undefined;
|
|
67
|
+
if (policy.forms && !policy.forms.includes(parsed.form)) {
|
|
68
|
+
return `credential references of the form '${parsed.form}:' are not allowed here`;
|
|
69
|
+
}
|
|
70
|
+
if (parsed.form === "env" && !isEnvNameAllowed(parsed.name, policy)) {
|
|
71
|
+
return `environment variable ${parsed.name} is not allowed here`;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function interpolationRefusal(
|
|
77
|
+
field: string,
|
|
78
|
+
value: string,
|
|
79
|
+
policy: CredentialPolicy,
|
|
80
|
+
): string | undefined {
|
|
81
|
+
for (const match of value.matchAll(INTERPOLATION_TOKEN)) {
|
|
82
|
+
const name = match[1]!.trim();
|
|
83
|
+
if (!BARE_ENV_NAME.test(name) || !isEnvNameAllowed(name, policy)) {
|
|
84
|
+
return `${field} interpolates '\${${name}}', which is not allowed here`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The reason an entry is refused, or `undefined`: its credential reference and
|
|
92
|
+
* every environment name it interpolates are checked, nothing is resolved.
|
|
93
|
+
* Without a policy nothing is refused.
|
|
94
|
+
*/
|
|
95
|
+
export function entryPolicyRefusal(
|
|
96
|
+
entry: AIProviderEntry,
|
|
97
|
+
policy?: CredentialPolicy,
|
|
98
|
+
): string | undefined {
|
|
99
|
+
if (!policy) return undefined;
|
|
100
|
+
const ref = entry.credential?.ref;
|
|
101
|
+
if (ref !== undefined) {
|
|
102
|
+
const refusal = credentialRefRefusal(ref, policy);
|
|
103
|
+
if (refusal) return refusal;
|
|
104
|
+
}
|
|
105
|
+
const fields: Array<[string, unknown]> = [
|
|
106
|
+
["base_url", entry.base_url],
|
|
107
|
+
...Object.entries(entry.headers ?? {}).map(([name, value]): [string, unknown] => [
|
|
108
|
+
`headers.${name}`,
|
|
109
|
+
value,
|
|
110
|
+
]),
|
|
111
|
+
...Object.entries((entry as { query_params?: Record<string, string> }).query_params ?? {}).map(
|
|
112
|
+
([name, value]): [string, unknown] => [`query_params.${name}`, value],
|
|
113
|
+
),
|
|
114
|
+
];
|
|
115
|
+
for (const [field, value] of fields) {
|
|
116
|
+
if (typeof value !== "string") continue;
|
|
117
|
+
const refusal = interpolationRefusal(field, value, policy);
|
|
118
|
+
if (refusal) return refusal;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The registry without the entries the policy refuses, as a plain entry map in
|
|
125
|
+
* registry order, plus what was refused. Nothing is resolved.
|
|
126
|
+
*/
|
|
127
|
+
export function applyCredentialPolicy(
|
|
128
|
+
registry: AIProviderRegistryView,
|
|
129
|
+
policy: CredentialPolicy,
|
|
130
|
+
): { registry: Record<string, AIProviderEntry>; refused: PolicyRefusal[] } {
|
|
131
|
+
const allowed: Record<string, AIProviderEntry> = {};
|
|
132
|
+
const refused: PolicyRefusal[] = [];
|
|
133
|
+
for (const [providerId, entry] of registryEntries(registry)) {
|
|
134
|
+
const error = entryPolicyRefusal(entry, policy);
|
|
135
|
+
if (error) refused.push({ providerId, error });
|
|
136
|
+
else allowed[providerId] = entry;
|
|
137
|
+
}
|
|
138
|
+
return { registry: allowed, refused };
|
|
139
|
+
}
|