sneakoscope 8.1.2 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -58
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/fast-inline.js +5 -51
- package/dist/cli/command-manifest-lite.js +2 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/cli/global-mode-router.js +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +112 -914
- package/dist/cli/install-helpers-codex-lb-shared.js +3 -44
- package/dist/cli/install-helpers-install-support.js +4 -20
- package/dist/cli/install-helpers.js +36 -2716
- package/dist/cli/router.js +9 -9
- package/dist/commands/bridge-command-output.js +111 -0
- package/dist/commands/bridge.js +320 -0
- package/dist/commands/codex-app.js +39 -123
- package/dist/commands/doctor-helpers.js +380 -0
- package/dist/commands/doctor-profile.js +3 -0
- package/dist/commands/doctor.js +114 -873
- package/dist/commands/mad-sks.js +0 -4
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-effort-policy.js +11 -21
- package/dist/core/agents/agent-recursion-guard.js +26 -0
- package/dist/core/agents/agent-runner-codex-exec.js +7 -9
- package/dist/core/architecture-hardening/contracts/contracts.js +225 -161
- package/dist/core/architecture-hardening/state/state-service.js +49 -13
- package/dist/core/auto-review.js +15 -9
- package/dist/core/codex/codex-config-guard.js +8 -0
- package/dist/core/codex-adapter.js +8 -10
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -29
- package/dist/core/codex-app/codex-app-launcher.js +10 -12
- package/dist/core/codex-app/codex-app-restart.js +10 -12
- package/dist/core/codex-app/menubar/constants.js +2 -5
- package/dist/core/codex-app/menubar/global-install.js +3 -2
- package/dist/core/codex-app/menubar/installer/result.js +23 -0
- package/dist/core/codex-app/menubar/installer/runtime.js +28 -0
- package/dist/core/codex-app/menubar/installer.js +5 -96
- package/dist/core/codex-app/menubar/migration.js +1 -42
- package/dist/core/codex-app/openrouter-provider.js +0 -78
- package/dist/core/codex-app.js +97 -360
- package/dist/core/codex-control/codex-app-server-runtime-env.js +8 -28
- package/dist/core/codex-control/codex-current-image-path-real-probe.js +2 -3
- package/dist/core/codex-control/codex-current-web-search-probe.js +2 -3
- package/dist/core/codex-control/codex-sdk-adapter.js +5 -48
- package/dist/core/codex-control/codex-task-runner.js +16 -60
- package/dist/core/codex-control/desktop-bridge-launch-guard.js +227 -0
- package/dist/core/codex-exec-output-schema.js +11 -13
- package/dist/core/codex-lb/bridge-contracts.js +2 -0
- package/dist/core/codex-lb/bridge-runtime-validation/capability.js +104 -0
- package/dist/core/codex-lb/bridge-runtime-validation/catalog.js +52 -0
- package/dist/core/codex-lb/bridge-runtime-validation/shared.js +109 -0
- package/dist/core/codex-lb/bridge-runtime-validation/status.js +250 -0
- package/dist/core/codex-lb/bridge-runtime-validation.js +106 -0
- package/dist/core/codex-lb/capability-runner.js +505 -201
- package/dist/core/codex-lb/codex-lb-env.js +179 -12
- package/dist/core/codex-lb/combined-catalog/builder.js +86 -0
- package/dist/core/codex-lb/combined-catalog/contracts.js +5 -0
- package/dist/core/codex-lb/combined-catalog/normalize.js +170 -0
- package/dist/core/codex-lb/combined-catalog/persistence.js +207 -0
- package/dist/core/codex-lb/combined-catalog/shared.js +12 -0
- package/dist/core/codex-lb/combined-catalog/storage.js +171 -0
- package/dist/core/codex-lb/combined-catalog.js +12 -0
- package/dist/core/codex-lb/desktop-bridge/client.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +48 -63
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +115 -83
- package/dist/core/codex-lb/desktop-bridge/launchd.js +20 -13
- package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/security.js +381 -267
- package/dist/core/codex-lb/desktop-bridge/server.js +148 -19
- package/dist/core/codex-lb/desktop-bridge/state.js +56 -70
- package/dist/core/codex-lb/desktop-bridge/types.js +6 -1
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +281 -106
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +245 -0
- package/dist/core/codex-lb/desktop-bridge-migration/historical-toml.js +56 -0
- package/dist/core/codex-lb/desktop-bridge-migration/metadata.js +73 -0
- package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +52 -0
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +106 -0
- package/dist/core/codex-lb/desktop-bridge-migration/types.js +1 -0
- package/dist/core/codex-lb/desktop-bridge-migration.js +238 -0
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +257 -0
- package/dist/core/codex-lb/desktop-controller-v3/diagnostics.js +62 -0
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +203 -0
- package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +293 -0
- package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +144 -0
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +182 -0
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +279 -0
- package/dist/core/codex-lb/desktop-controller-v3/status.js +269 -0
- package/dist/core/codex-lb/desktop-controller-v3/types.js +1 -0
- package/dist/core/codex-lb/desktop-controller-v3/verification.js +89 -0
- package/dist/core/codex-lb/desktop-controller-v3.js +67 -0
- package/dist/core/codex-lb/desktop-controller.js +2 -1730
- package/dist/core/codex-lb/desktop-service.js +462 -627
- package/dist/core/codex-lb/migration-receipt.js +560 -118
- package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +37 -53
- package/dist/core/codex-lb/probes/bridge-probe.js +125 -64
- package/dist/core/codex-lb/probes/catalog-probe.js +36 -136
- package/dist/core/codex-lb/probes/computer-use-probe.js +63 -39
- package/dist/core/codex-lb/probes/image-generation-probe.js +98 -41
- package/dist/core/codex-lb/probes/probe-evidence.js +32 -33
- package/dist/core/codex-lb/probes/voice-realtime-probe.js +69 -43
- package/dist/core/codex-lb/provider-credentials/configuration.js +127 -0
- package/dist/core/codex-lb/provider-credentials/locks.js +11 -0
- package/dist/core/codex-lb/provider-credentials/private-snapshot.js +28 -0
- package/dist/core/codex-lb/provider-credentials/removal.js +33 -0
- package/dist/core/codex-lb/provider-credentials/resolution.js +59 -0
- package/dist/core/codex-lb/provider-credentials/runtime.js +55 -0
- package/dist/core/codex-lb/provider-credentials/types.js +1 -0
- package/dist/core/codex-lb/provider-credentials/validation-store.js +137 -0
- package/dist/core/codex-lb/provider-credentials.js +5 -0
- package/dist/core/codex-lb/provider-registry.js +323 -0
- package/dist/core/codex-lb/provider-route-policy.js +118 -0
- package/dist/core/codex-lb/request-route-resolver.js +172 -0
- package/dist/core/codex-lb/route-index.js +148 -0
- package/dist/core/codex-lb/trusted-deep-evidence.js +158 -286
- package/dist/core/commands/basic-cli.js +1 -1
- package/dist/core/commands/check-command.js +3 -10
- package/dist/core/commands/gate-result-contract.js +10 -14
- package/dist/core/commands/intent-normalization/normalizer.js +1 -1
- package/dist/core/commands/mad-sks-command.js +2 -6
- package/dist/core/commands/release-command.js +11 -1
- package/dist/core/commands/telegram-command.js +46 -6
- package/dist/core/doctor/doctor-readiness-matrix.js +9 -7
- package/dist/core/doctor/skill-legacy-surface.js +5 -5
- package/dist/core/evidence/v2/evidence-key.js +30 -3
- package/dist/core/feature-fixtures.js +8 -1
- package/dist/core/feature-registry.js +3 -2
- package/dist/core/hooks-runtime/stop-finalization.js +11 -1
- package/dist/core/hooks-runtime/subagent-context.js +15 -1
- package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +1 -1
- package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
- package/dist/core/hooks-runtime.js +73 -87
- package/dist/core/image-ux-review/imagegen-adapter.js +108 -94
- package/dist/core/image-ux-review/real-callout-extractor.js +144 -14
- package/dist/core/imagegen/desktop-bridge-imagegen-target.js +77 -0
- package/dist/core/imagegen/imagegen-capability.js +43 -151
- package/dist/core/init/skills/filesystem.js +89 -0
- package/dist/core/init/skills/inventory.js +97 -0
- package/dist/core/init/skills/manifest.js +151 -0
- package/dist/core/init/skills.js +5 -325
- package/dist/core/init.js +0 -63
- package/dist/core/install/installed-package-smoke.js +1 -1
- package/dist/core/managed-assets/managed-assets-manifest.js +0 -1
- package/dist/core/policy/latest-version-guidance.js +15 -2
- package/dist/core/preflight/parallel-preflight-engine.js +27 -51
- package/dist/core/provider/provider-badge.js +9 -4
- package/dist/core/provider/provider-context.js +57 -107
- package/dist/core/providers/openrouter/openrouter-account.js +3 -1
- package/dist/core/providers/openrouter/openrouter-secret-store.js +91 -22
- package/dist/core/release/desktop-bridge-release-evidence.js +457 -0
- package/dist/core/release/gate-manifest.js +0 -1
- package/dist/core/release/main-push-guard.js +13 -0
- package/dist/core/release/main-push-receipt.js +11 -0
- package/dist/core/release/physical-release-gates.js +664 -61
- package/dist/core/release/publish-preflight.js +3 -3
- package/dist/core/release/release-gate-batch-runner.js +19 -6
- package/dist/core/release/release-gate-dag.js +33 -6
- package/dist/core/release/release-gate-node.js +1 -3
- package/dist/core/release/release-gate-output-contract.js +55 -0
- package/dist/core/release/release-pack-contract.js +1 -1
- package/dist/core/release/release-pack-receipt.js +209 -8
- package/dist/core/release/release-real-contract.js +1 -1
- package/dist/core/release/stage-publish.js +106 -28
- package/dist/core/retention/temp-lease.js +204 -0
- package/dist/core/retention.js +18 -195
- package/dist/core/routes.js +1 -2
- package/dist/core/safety/intent-contract/intent-contract.js +3 -3
- package/dist/core/subagents/naruto-host-credentials.js +6 -1
- package/dist/core/subagents/official-subagent-config.js +27 -9
- package/dist/core/subagents/official-subagent-runner.js +14 -31
- package/dist/core/subagents/wave-lifecycle.js +36 -8
- package/dist/core/subagents/wave-parent-guidance.js +2 -1
- package/dist/core/telegram/doctor.js +2 -2
- package/dist/core/telegram/liveness.js +10 -4
- package/dist/core/triwiki/context-graph/compiler/index.js +18 -18
- package/dist/core/update/update-migration-state/fast-mode-config.js +172 -0
- package/dist/core/update/update-migration-state/session-state-split.js +247 -0
- package/dist/core/update/update-migration-state/simple-stages.js +46 -0
- package/dist/core/update/update-migration-state.js +4 -481
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +28 -4
- package/dist/native/sks-menubar/Sources/OperationModels.swift +112 -2
- package/dist/native/sks-menubar/Sources/ProviderRouteExplanation.swift +21 -0
- package/dist/native/sks-menubar/Sources/ProvidersBridgeCatalog.swift +140 -0
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +107 -470
- package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +27 -197
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +335 -269
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +233 -437
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +16 -7
- package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +39 -9
- package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
- package/dist/scripts/canonical-test-runner.js +7 -1
- package/dist/scripts/check-runtime-schemas.js +7 -2
- package/dist/scripts/codex-sdk-real-smoke-check.js +3 -1
- package/dist/scripts/desktop-bridge-capabilities-check.js +123 -0
- package/dist/scripts/{codex-lb-desktop-bridge-latency-check.js → desktop-bridge-latency-check.js} +61 -16
- package/dist/scripts/desktop-bridge-real-evidence-check.js +64 -0
- package/dist/scripts/{codex-lb-desktop-bridge-check.js → desktop-bridge-transport-check.js} +92 -155
- package/dist/scripts/desktop-bridge-transport-client.js +116 -0
- package/dist/scripts/desktop-bridge-unification-check.js +276 -0
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -8
- package/dist/scripts/gate-policy-audit-check.js +52 -2
- package/dist/scripts/provider-badge-context-check.js +55 -12
- package/dist/scripts/publish-preflight.js +1 -2
- package/dist/scripts/release-affected-selector-check.js +1 -2
- package/dist/scripts/release-aggressive-resource-governor-check.js +1 -1
- package/dist/scripts/release-cache-glob-hashing-check.js +1 -0
- package/dist/scripts/release-cache-version-neutral-fixture-check.js +1 -0
- package/dist/scripts/release-full-parallelism-blackbox.js +1 -1
- package/dist/scripts/release-gate-batch-runner-check.js +9 -0
- package/dist/scripts/release-gate-dag-runner-check.js +6 -0
- package/dist/scripts/release-gate-dag-runner.js +4 -2
- package/dist/scripts/release-metadata-check.js +16 -3
- package/dist/scripts/release-pack-receipt.js +29 -6
- package/dist/scripts/release-physical-gates-check.js +39 -0
- package/dist/scripts/release-real-check.js +7 -5
- package/package.json +6 -8
- package/release-gates.v2.json +156 -157
- package/runtime-required-scripts.json +11 -11
- package/schemas/codex/desktop-bridge-status-v3.schema.json +286 -0
- package/schemas/codex/desktop-capabilities-v3.schema.json +198 -0
- package/schemas/release/release-gate-node.schema.json +2 -3
- package/dist/cli/codex-lb-setup-warning-output.js +0 -17
- package/dist/cli/install-helpers-codex-lb-chain.js +0 -529
- package/dist/commands/codex-lb.js +0 -1232
- package/dist/core/architecture-hardening/migration/migration.js +0 -111
- package/dist/core/codex-app/auxiliary-oauth-policy.js +0 -36
- package/dist/core/codex-app/catalog-compat/catalog-service.js +0 -104
- package/dist/core/codex-app/child-policy/child-policy.js +0 -35
- package/dist/core/codex-app/desktop-routing-snapshot.js +0 -233
- package/dist/core/codex-app/glm-model-profile.js +0 -1
- package/dist/core/codex-app/glm-profile-installer.js +0 -191
- package/dist/core/codex-app/glm-profile-schema.js +0 -42
- package/dist/core/codex-app/multi-provider-router.js +0 -391
- package/dist/core/codex-app/openrouter-activate.js +0 -405
- package/dist/core/codex-app/openrouter-model-catalog.js +0 -217
- package/dist/core/codex-app/provider-child-policy.js +0 -64
- package/dist/core/codex-app/provider-mode.js +0 -91
- package/dist/core/codex-app/provider-session-policy.js +0 -85
- package/dist/core/codex-app/session-policy/session-pinning.js +0 -68
- package/dist/core/codex-control/codex-lb-launch-recovery.js +0 -319
- package/dist/core/codex-lb/codex-lb-setup.js +0 -293
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +0 -456
- package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +0 -290
- package/dist/core/codex-lb/desktop-center-credentials.js +0 -1083
- package/dist/core/codex-lb/desktop-mode.js +0 -32
- package/dist/core/codex-lb/desktop-real-evidence.js +0 -168
- package/dist/core/codex-lb/legacy-migration.js +0 -576
- package/dist/core/codex-lb/native-openai-transport/transport.js +0 -68
- package/dist/core/codex-lb/provider-routing/provider-router.js +0 -58
- package/dist/core/codex-lb/routing-truth.js +0 -327
- package/dist/core/imagegen/codex-lb-imagegen-target.js +0 -92
- package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +0 -118
- package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +0 -17
- package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +0 -331
- package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +0 -260
- package/dist/scripts/codex-lb-catalog-passthrough-check.js +0 -120
- package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +0 -169
- package/dist/scripts/codex-lb-desktop-capabilities-check.js +0 -287
- package/dist/scripts/codex-lb-desktop-real-evidence-check.js +0 -123
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +0 -114
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +0 -152
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +0 -231
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { writeJsonAtomic } from '../../fsx.js';
|
|
5
|
+
import { providerCode, unique } from './runtime.js';
|
|
6
|
+
import { resolveAllProviderCredentials, resolveProviderCredential } from './resolution.js';
|
|
7
|
+
import { withFileLock } from '../../locks/file-lock.js';
|
|
8
|
+
import { withProviderCredentialLock } from './locks.js';
|
|
9
|
+
export function providerCredentialValidationPath(home = process.env.HOME || os.homedir()) {
|
|
10
|
+
return path.join(path.resolve(home), '.codex', 'sks', 'sks-bridge-provider-validation.json');
|
|
11
|
+
}
|
|
12
|
+
export async function resolveAllProviderCredentialsWithValidation(options = {}) {
|
|
13
|
+
const home = options.home || process.env.HOME || os.homedir();
|
|
14
|
+
const raw = await resolveAllProviderCredentials({
|
|
15
|
+
codexLb: { home, ...(options.codexLb || {}) },
|
|
16
|
+
openrouter: { home, ...(options.openrouter || {}) }
|
|
17
|
+
});
|
|
18
|
+
const store = await readProviderCredentialValidationStore(options.validationPath || providerCredentialValidationPath(home));
|
|
19
|
+
const validationFor = (providerId) => {
|
|
20
|
+
const credential = raw[providerId];
|
|
21
|
+
const record = store.providers[providerId];
|
|
22
|
+
if (!record
|
|
23
|
+
|| !credential.fingerprint
|
|
24
|
+
|| record.credential_fingerprint !== credential.fingerprint
|
|
25
|
+
|| record.endpoint_url !== String(credential.endpoint_url || '')) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
state: record.state,
|
|
30
|
+
checked_at: record.checked_at,
|
|
31
|
+
blockers: record.blockers,
|
|
32
|
+
warnings: record.warnings
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const [codexLb, openrouter] = await Promise.all([
|
|
36
|
+
resolveProviderCredential('codex-lb', {
|
|
37
|
+
home,
|
|
38
|
+
...(options.codexLb || {}),
|
|
39
|
+
validation: validationFor('codex-lb')
|
|
40
|
+
}),
|
|
41
|
+
resolveProviderCredential('openrouter', {
|
|
42
|
+
home,
|
|
43
|
+
...(options.openrouter || {}),
|
|
44
|
+
validation: validationFor('openrouter')
|
|
45
|
+
})
|
|
46
|
+
]);
|
|
47
|
+
return { 'codex-lb': codexLb, openrouter };
|
|
48
|
+
}
|
|
49
|
+
export async function recordProviderCredentialValidation(input) {
|
|
50
|
+
if (!input.credential.fingerprint || !input.credential.endpoint_url) {
|
|
51
|
+
throw new Error(`${providerCode(input.provider_id)}_credential_validation_binding_missing`);
|
|
52
|
+
}
|
|
53
|
+
const home = input.home || process.env.HOME || os.homedir();
|
|
54
|
+
const file = input.validationPath || providerCredentialValidationPath(home);
|
|
55
|
+
return withProviderCredentialLock(home, input.provider_id, () => withFileLock({
|
|
56
|
+
lockPath: `${path.resolve(file)}.lock`, timeoutMs: 10_000, staleMs: 60_000,
|
|
57
|
+
}, async () => {
|
|
58
|
+
if (input.resolveCurrentCredential) {
|
|
59
|
+
const currentCredential = await input.resolveCurrentCredential();
|
|
60
|
+
if (currentCredential.fingerprint !== input.credential.fingerprint
|
|
61
|
+
|| currentCredential.endpoint_url !== input.credential.endpoint_url) {
|
|
62
|
+
throw new Error(`${providerCode(input.provider_id)}_credential_validation_conflict`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const current = await readProviderCredentialValidationStore(file);
|
|
66
|
+
const next = {
|
|
67
|
+
schema: 'sks.provider-credential-validation-store.v1',
|
|
68
|
+
providers: {
|
|
69
|
+
...current.providers,
|
|
70
|
+
[input.provider_id]: {
|
|
71
|
+
credential_fingerprint: input.credential.fingerprint,
|
|
72
|
+
endpoint_url: input.credential.endpoint_url,
|
|
73
|
+
state: input.state,
|
|
74
|
+
checked_at: input.checked_at || new Date().toISOString(),
|
|
75
|
+
blockers: unique(input.blockers || []),
|
|
76
|
+
warnings: unique(input.warnings || [])
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
await writeJsonAtomic(file, next, { mode: 0o600 });
|
|
81
|
+
await fs.chmod(file, 0o600);
|
|
82
|
+
return next;
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
export async function readProviderCredentialValidationStore(file) {
|
|
86
|
+
const empty = {
|
|
87
|
+
schema: 'sks.provider-credential-validation-store.v1',
|
|
88
|
+
providers: {}
|
|
89
|
+
};
|
|
90
|
+
const stat = await fs.lstat(file).catch(() => null);
|
|
91
|
+
if (!stat)
|
|
92
|
+
return empty;
|
|
93
|
+
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
94
|
+
if (!stat.isFile()
|
|
95
|
+
|| stat.isSymbolicLink()
|
|
96
|
+
|| (expectedUid !== null && stat.uid !== expectedUid)
|
|
97
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600)) {
|
|
98
|
+
throw new Error('provider_credential_validation_store_insecure');
|
|
99
|
+
}
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error('provider_credential_validation_store_invalid');
|
|
106
|
+
}
|
|
107
|
+
if (!isValidationStore(parsed))
|
|
108
|
+
throw new Error('provider_credential_validation_store_invalid');
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
function isValidationStore(value) {
|
|
112
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
113
|
+
return false;
|
|
114
|
+
const row = value;
|
|
115
|
+
if (row.schema !== 'sks.provider-credential-validation-store.v1'
|
|
116
|
+
|| !row.providers
|
|
117
|
+
|| typeof row.providers !== 'object'
|
|
118
|
+
|| Array.isArray(row.providers))
|
|
119
|
+
return false;
|
|
120
|
+
return Object.entries(row.providers).every(([providerId, entry]) => {
|
|
121
|
+
if (providerId !== 'codex-lb' && providerId !== 'openrouter')
|
|
122
|
+
return false;
|
|
123
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
124
|
+
return false;
|
|
125
|
+
const record = entry;
|
|
126
|
+
return typeof record.credential_fingerprint === 'string'
|
|
127
|
+
&& /^[a-f0-9]{16}$/.test(record.credential_fingerprint)
|
|
128
|
+
&& typeof record.endpoint_url === 'string'
|
|
129
|
+
&& ['ready', 'rejected', 'unavailable', 'stale'].includes(String(record.state || ''))
|
|
130
|
+
&& typeof record.checked_at === 'string'
|
|
131
|
+
&& Number.isFinite(Date.parse(record.checked_at))
|
|
132
|
+
&& Array.isArray(record.blockers)
|
|
133
|
+
&& record.blockers.every((item) => typeof item === 'string')
|
|
134
|
+
&& Array.isArray(record.warnings)
|
|
135
|
+
&& record.warnings.every((item) => typeof item === 'string');
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { configureProviderCredential } from './provider-credentials/configuration.js';
|
|
2
|
+
export { removeProviderCredential } from './provider-credentials/removal.js';
|
|
3
|
+
export { resolveAllProviderCredentials, resolveProviderCredential } from './provider-credentials/resolution.js';
|
|
4
|
+
export { providerCredentialStatus } from './provider-credentials/runtime.js';
|
|
5
|
+
export { providerCredentialValidationPath, readProviderCredentialValidationStore, recordProviderCredentialValidation, resolveAllProviderCredentialsWithValidation } from './provider-credentials/validation-store.js';
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { providerCredentialStatus, resolveAllProviderCredentials } from './provider-credentials.js';
|
|
6
|
+
import { codexLbBaseUrlSecurityBlocker } from './codex-lb-env.js';
|
|
7
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
8
|
+
import { withFileLock } from '../locks/file-lock.js';
|
|
9
|
+
export const BRIDGE_PROVIDER_REGISTRY_SCHEMA = 'sks.bridge-provider-registry.v1';
|
|
10
|
+
export const BRIDGE_PROVIDER_REGISTRY_FILENAME = 'sks-bridge-provider-registry.json';
|
|
11
|
+
export const OPENROUTER_ALLOWED_ORIGIN = 'https://openrouter.ai';
|
|
12
|
+
export function bridgeProviderRegistryPath(home = process.env.HOME || os.homedir()) {
|
|
13
|
+
return path.join(path.resolve(home), '.codex', 'sks', BRIDGE_PROVIDER_REGISTRY_FILENAME);
|
|
14
|
+
}
|
|
15
|
+
export async function resolveBridgeProviderRegistry(options = {}) {
|
|
16
|
+
const credentials = options.credentials || await resolveAllProviderCredentials(options.credentialOptions || {});
|
|
17
|
+
const file = options.registryPath || bridgeProviderRegistryPath(options.home);
|
|
18
|
+
const read = options.storedRegistry
|
|
19
|
+
? { registry: options.storedRegistry, blockers: [] }
|
|
20
|
+
: await readStoredBridgeProviderRegistry(file);
|
|
21
|
+
const stored = read.registry || defaultStoredRegistry(credentials);
|
|
22
|
+
const profiles = {
|
|
23
|
+
'codex-lb': resolveProfile('codex-lb', stored.profiles['codex-lb'], credentials['codex-lb']),
|
|
24
|
+
openrouter: resolveProfile('openrouter', stored.profiles.openrouter, credentials.openrouter)
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
schema: BRIDGE_PROVIDER_REGISTRY_SCHEMA,
|
|
28
|
+
generation: digest({
|
|
29
|
+
profiles: Object.fromEntries(Object.entries(profiles).map(([provider, profile]) => [provider, {
|
|
30
|
+
enabled: profile.enabled,
|
|
31
|
+
profile_generation: profile.profile_generation
|
|
32
|
+
}]))
|
|
33
|
+
}),
|
|
34
|
+
profiles,
|
|
35
|
+
blockers: unique([
|
|
36
|
+
...read.blockers,
|
|
37
|
+
...profiles['codex-lb'].blockers,
|
|
38
|
+
...profiles.openrouter.blockers
|
|
39
|
+
]),
|
|
40
|
+
warnings: unique([
|
|
41
|
+
...profiles['codex-lb'].warnings,
|
|
42
|
+
...profiles.openrouter.warnings
|
|
43
|
+
])
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export async function loadStoredBridgeProviderRegistry(options = {}) {
|
|
47
|
+
const file = options.registryPath || bridgeProviderRegistryPath(options.home);
|
|
48
|
+
const read = await readStoredBridgeProviderRegistry(file);
|
|
49
|
+
return { path: file, registry: read.registry, blockers: read.blockers };
|
|
50
|
+
}
|
|
51
|
+
export function buildStoredBridgeProviderRegistry(input) {
|
|
52
|
+
const base = defaultStoredRegistry(input.credentials);
|
|
53
|
+
const profiles = { ...base.profiles };
|
|
54
|
+
for (const providerId of ['codex-lb', 'openrouter']) {
|
|
55
|
+
const override = input.overrides?.[providerId];
|
|
56
|
+
if (!override?.present)
|
|
57
|
+
continue;
|
|
58
|
+
const endpoint = normalizeEndpoint(override.endpoint_url || base.profiles[providerId].endpoint_url || '');
|
|
59
|
+
const origin = endpointOrigin(endpoint);
|
|
60
|
+
const authTransport = override.auth_transport || defaultAuthTransport(providerId);
|
|
61
|
+
const blocker = providerEndpointSecurityBlocker(providerId, endpoint, origin ? [origin] : []);
|
|
62
|
+
if (blocker)
|
|
63
|
+
throw new Error(blocker);
|
|
64
|
+
profiles[providerId] = {
|
|
65
|
+
enabled: override.enabled,
|
|
66
|
+
endpoint_url: endpoint,
|
|
67
|
+
allowed_origins: origin ? [origin] : [],
|
|
68
|
+
auth_transport: authTransport
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { schema: BRIDGE_PROVIDER_REGISTRY_SCHEMA, profiles };
|
|
72
|
+
}
|
|
73
|
+
export function serializeStoredBridgeProviderRegistry(registry) {
|
|
74
|
+
if (!isStoredRegistry(registry))
|
|
75
|
+
throw new Error('provider_registry_schema_invalid');
|
|
76
|
+
return `${JSON.stringify(registry, null, 2)}\n`;
|
|
77
|
+
}
|
|
78
|
+
export async function setBridgeProviderEnabled(input) {
|
|
79
|
+
const credentials = input.credentials || await resolveAllProviderCredentials(input.credentialOptions || {});
|
|
80
|
+
const file = input.registryPath || bridgeProviderRegistryPath(input.home);
|
|
81
|
+
return withFileLock(registryLockOptions(file), async () => {
|
|
82
|
+
const read = await readStoredBridgeProviderRegistry(file);
|
|
83
|
+
if (read.blockers.length > 0)
|
|
84
|
+
throw new Error(read.blockers[0]);
|
|
85
|
+
const current = read.registry || defaultStoredRegistry(credentials);
|
|
86
|
+
const next = {
|
|
87
|
+
schema: BRIDGE_PROVIDER_REGISTRY_SCHEMA,
|
|
88
|
+
profiles: {
|
|
89
|
+
...current.profiles,
|
|
90
|
+
[input.provider_id]: {
|
|
91
|
+
...current.profiles[input.provider_id],
|
|
92
|
+
enabled: input.enabled
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
await writeJsonAtomic(file, next, { mode: 0o600 });
|
|
97
|
+
return resolveBridgeProviderRegistry({
|
|
98
|
+
...(input.home ? { home: input.home } : {}),
|
|
99
|
+
registryPath: file,
|
|
100
|
+
credentials
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export async function configureBridgeProviderProfile(input) {
|
|
105
|
+
const credentials = input.credentials || await resolveAllProviderCredentials();
|
|
106
|
+
const endpoint = normalizeEndpoint(input.endpoint_url);
|
|
107
|
+
const origin = endpointOrigin(endpoint) || '';
|
|
108
|
+
const blocker = providerEndpointSecurityBlocker(input.provider_id, endpoint, input.allowed_origins || [origin]);
|
|
109
|
+
if (blocker)
|
|
110
|
+
throw new Error(blocker);
|
|
111
|
+
const file = input.registryPath || bridgeProviderRegistryPath(input.home);
|
|
112
|
+
return withFileLock(registryLockOptions(file), async () => {
|
|
113
|
+
const read = await readStoredBridgeProviderRegistry(file);
|
|
114
|
+
if (read.blockers.length > 0)
|
|
115
|
+
throw new Error(read.blockers[0]);
|
|
116
|
+
const current = read.registry || defaultStoredRegistry(credentials);
|
|
117
|
+
const nextProfile = {
|
|
118
|
+
enabled: input.enabled ?? current.profiles[input.provider_id].enabled,
|
|
119
|
+
endpoint_url: endpoint,
|
|
120
|
+
allowed_origins: normalizeOrigins(input.allowed_origins || [origin]),
|
|
121
|
+
auth_transport: input.auth_transport || defaultAuthTransport(input.provider_id)
|
|
122
|
+
};
|
|
123
|
+
const next = {
|
|
124
|
+
schema: BRIDGE_PROVIDER_REGISTRY_SCHEMA,
|
|
125
|
+
profiles: { ...current.profiles, [input.provider_id]: nextProfile }
|
|
126
|
+
};
|
|
127
|
+
await writeJsonAtomic(file, next, { mode: 0o600 });
|
|
128
|
+
return resolveBridgeProviderRegistry({
|
|
129
|
+
...(input.home ? { home: input.home } : {}),
|
|
130
|
+
registryPath: file,
|
|
131
|
+
credentials
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function registryLockOptions(file) {
|
|
136
|
+
return { lockPath: `${path.resolve(file)}.lock`, timeoutMs: 10_000, staleMs: 60_000 };
|
|
137
|
+
}
|
|
138
|
+
export function providerEndpointSecurityBlocker(providerId, endpointUrl, allowedOrigins) {
|
|
139
|
+
if (!endpointUrl)
|
|
140
|
+
return `${providerCode(providerId)}_endpoint_missing`;
|
|
141
|
+
try {
|
|
142
|
+
const url = new URL(endpointUrl);
|
|
143
|
+
if (url.username || url.password || url.search || url.hash)
|
|
144
|
+
return 'provider_endpoint_components_forbidden';
|
|
145
|
+
const normalizedAllowed = normalizeOrigins(allowedOrigins);
|
|
146
|
+
if (!normalizedAllowed.includes(url.origin))
|
|
147
|
+
return 'provider_endpoint_origin_not_allowlisted';
|
|
148
|
+
if (providerId === 'openrouter') {
|
|
149
|
+
if (url.protocol !== 'https:' || url.origin !== OPENROUTER_ALLOWED_ORIGIN) {
|
|
150
|
+
return 'openrouter_endpoint_origin_not_allowlisted';
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return codexLbBaseUrlSecurityBlocker(endpointUrl);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return 'provider_endpoint_invalid';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function readStoredBridgeProviderRegistry(file) {
|
|
161
|
+
const stat = await fs.lstat(file).catch(() => null);
|
|
162
|
+
if (!stat)
|
|
163
|
+
return { registry: null, blockers: [] };
|
|
164
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
165
|
+
return { registry: null, blockers: ['provider_registry_not_regular_file'] };
|
|
166
|
+
}
|
|
167
|
+
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
168
|
+
if (expectedUid !== null && stat.uid !== expectedUid) {
|
|
169
|
+
return { registry: null, blockers: ['provider_registry_owner_mismatch'] };
|
|
170
|
+
}
|
|
171
|
+
if (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600) {
|
|
172
|
+
return { registry: null, blockers: ['provider_registry_mode_insecure'] };
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const value = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
176
|
+
if (!isStoredRegistry(value))
|
|
177
|
+
return { registry: null, blockers: ['provider_registry_schema_invalid'] };
|
|
178
|
+
return { registry: value, blockers: [] };
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return { registry: null, blockers: ['provider_registry_json_invalid'] };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function defaultStoredRegistry(credentials) {
|
|
185
|
+
const codexEndpoint = credentials['codex-lb'].endpoint_url;
|
|
186
|
+
const openRouterEndpoint = credentials.openrouter.endpoint_url || 'https://openrouter.ai/api/v1';
|
|
187
|
+
return {
|
|
188
|
+
schema: BRIDGE_PROVIDER_REGISTRY_SCHEMA,
|
|
189
|
+
profiles: {
|
|
190
|
+
'codex-lb': defaultProfile('codex-lb', codexEndpoint, credentials['codex-lb'].state !== 'not_configured'),
|
|
191
|
+
openrouter: defaultProfile('openrouter', openRouterEndpoint, credentials.openrouter.state !== 'not_configured')
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function defaultProfile(providerId, endpointUrl, enabled) {
|
|
196
|
+
const endpoint = normalizeEndpoint(endpointUrl || '');
|
|
197
|
+
const origin = endpointOrigin(endpoint);
|
|
198
|
+
return {
|
|
199
|
+
enabled,
|
|
200
|
+
endpoint_url: endpoint,
|
|
201
|
+
allowed_origins: origin ? [origin] : [],
|
|
202
|
+
auth_transport: defaultAuthTransport(providerId)
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function resolveProfile(providerId, stored, credential) {
|
|
206
|
+
const endpoint = normalizeEndpoint(stored.endpoint_url || credential.endpoint_url || '');
|
|
207
|
+
const endpointBlocker = providerEndpointSecurityBlocker(providerId, endpoint, stored.allowed_origins);
|
|
208
|
+
const credentialStatus = providerCredentialStatus(credential);
|
|
209
|
+
const blockers = unique([
|
|
210
|
+
...(endpointBlocker ? [endpointBlocker] : []),
|
|
211
|
+
...credentialStatus.blockers
|
|
212
|
+
]);
|
|
213
|
+
const state = !stored.enabled
|
|
214
|
+
? 'disabled'
|
|
215
|
+
: blockers.length > 0 && credentialStatus.state !== 'not_configured'
|
|
216
|
+
? 'blocked'
|
|
217
|
+
: credentialStatus.state === 'ready'
|
|
218
|
+
? 'ready'
|
|
219
|
+
: credentialStatus.state === 'not_configured'
|
|
220
|
+
? 'not_configured'
|
|
221
|
+
: 'configured_unverified';
|
|
222
|
+
const origin = endpointOrigin(endpoint);
|
|
223
|
+
const profileGeneration = digest({
|
|
224
|
+
provider_id: providerId,
|
|
225
|
+
enabled: stored.enabled,
|
|
226
|
+
credential_fingerprint: credentialStatus.fingerprint,
|
|
227
|
+
credential_state: credentialStatus.state,
|
|
228
|
+
endpoint,
|
|
229
|
+
allowed_origins: [...stored.allowed_origins].sort(),
|
|
230
|
+
auth_transport: stored.auth_transport
|
|
231
|
+
});
|
|
232
|
+
const endpointStatus = {
|
|
233
|
+
configured: Boolean(endpoint) && !endpointBlocker,
|
|
234
|
+
origin,
|
|
235
|
+
origin_redacted: origin,
|
|
236
|
+
allowed_origins: [...stored.allowed_origins].sort(),
|
|
237
|
+
auth_transport: stored.auth_transport,
|
|
238
|
+
blockers: endpointBlocker ? [endpointBlocker] : []
|
|
239
|
+
};
|
|
240
|
+
Object.defineProperty(endpointStatus, 'url', {
|
|
241
|
+
value: endpoint,
|
|
242
|
+
enumerable: false,
|
|
243
|
+
configurable: false,
|
|
244
|
+
writable: false
|
|
245
|
+
});
|
|
246
|
+
return {
|
|
247
|
+
provider_id: providerId,
|
|
248
|
+
enabled: stored.enabled,
|
|
249
|
+
profile_generation: profileGeneration,
|
|
250
|
+
credential: credentialStatus,
|
|
251
|
+
endpoint: endpointStatus,
|
|
252
|
+
state,
|
|
253
|
+
blockers,
|
|
254
|
+
warnings: [...credentialStatus.warnings]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function isStoredRegistry(value) {
|
|
258
|
+
if (value.schema !== BRIDGE_PROVIDER_REGISTRY_SCHEMA || !value.profiles)
|
|
259
|
+
return false;
|
|
260
|
+
return ['codex-lb', 'openrouter'].every((providerId) => {
|
|
261
|
+
const profile = value.profiles?.[providerId];
|
|
262
|
+
return Boolean(profile
|
|
263
|
+
&& typeof profile.enabled === 'boolean'
|
|
264
|
+
&& (profile.endpoint_url === null || typeof profile.endpoint_url === 'string')
|
|
265
|
+
&& Array.isArray(profile.allowed_origins)
|
|
266
|
+
&& profile.allowed_origins.every((origin) => typeof origin === 'string')
|
|
267
|
+
&& ['authorization-bearer', 'x-codex-lb-api-key', 'openrouter-bearer'].includes(profile.auth_transport));
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
function defaultAuthTransport(providerId) {
|
|
271
|
+
return providerId === 'openrouter' ? 'openrouter-bearer' : 'authorization-bearer';
|
|
272
|
+
}
|
|
273
|
+
function normalizeEndpoint(value) {
|
|
274
|
+
const text = String(value || '').trim().replace(/\/+$/, '');
|
|
275
|
+
if (!text)
|
|
276
|
+
return null;
|
|
277
|
+
try {
|
|
278
|
+
return new URL(text).toString().replace(/\/+$/, '');
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return text;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function endpointOrigin(endpoint) {
|
|
285
|
+
if (!endpoint)
|
|
286
|
+
return null;
|
|
287
|
+
try {
|
|
288
|
+
return new URL(endpoint).origin;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function normalizeOrigins(values) {
|
|
295
|
+
return unique(values.map((value) => {
|
|
296
|
+
try {
|
|
297
|
+
return new URL(value).origin;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return '';
|
|
301
|
+
}
|
|
302
|
+
})).sort();
|
|
303
|
+
}
|
|
304
|
+
function providerCode(providerId) {
|
|
305
|
+
return providerId === 'codex-lb' ? 'codex_lb' : 'openrouter';
|
|
306
|
+
}
|
|
307
|
+
function digest(value) {
|
|
308
|
+
return crypto.createHash('sha256').update(stableStringify(value)).digest('hex');
|
|
309
|
+
}
|
|
310
|
+
function stableStringify(value) {
|
|
311
|
+
if (Array.isArray(value))
|
|
312
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
313
|
+
if (value && typeof value === 'object') {
|
|
314
|
+
return `{${Object.entries(value)
|
|
315
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
316
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`)
|
|
317
|
+
.join(',')}}`;
|
|
318
|
+
}
|
|
319
|
+
return JSON.stringify(value) ?? 'null';
|
|
320
|
+
}
|
|
321
|
+
function unique(values) {
|
|
322
|
+
return [...new Set(values.map((value) => String(value || '').trim()).filter(Boolean))];
|
|
323
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable } from './route-index.js';
|
|
5
|
+
import { writeJsonAtomic } from '../fsx.js';
|
|
6
|
+
import { withFileLock } from '../locks/file-lock.js';
|
|
7
|
+
export const BRIDGE_ROUTE_POLICY_FILENAME = 'sks-bridge-route-policy.json';
|
|
8
|
+
export function bridgeRoutePolicyPath(codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')) {
|
|
9
|
+
return path.join(path.resolve(codexHome), 'sks', BRIDGE_ROUTE_POLICY_FILENAME);
|
|
10
|
+
}
|
|
11
|
+
export function buildBridgeRoutingPolicy(input) {
|
|
12
|
+
const routes = canonicalRoutes(input.route_index.routes);
|
|
13
|
+
const semantic = {
|
|
14
|
+
default_provider_id: input.default_provider_id ?? null,
|
|
15
|
+
fallback: 'none',
|
|
16
|
+
model_routes: routes,
|
|
17
|
+
catalog_generation: input.catalog_generation
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
schema: 'sks.bridge-routing-policy.v1',
|
|
21
|
+
...semantic,
|
|
22
|
+
policy_generation: sha256Stable(semantic),
|
|
23
|
+
changed_at: input.changed_at || new Date().toISOString()
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function setBridgeRoutingDefault(policy, providerId, changedAt) {
|
|
27
|
+
const semantic = {
|
|
28
|
+
default_provider_id: providerId,
|
|
29
|
+
fallback: 'none',
|
|
30
|
+
model_routes: canonicalRoutes(policy.model_routes),
|
|
31
|
+
catalog_generation: policy.catalog_generation
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
schema: 'sks.bridge-routing-policy.v1',
|
|
35
|
+
...semantic,
|
|
36
|
+
policy_generation: sha256Stable(semantic),
|
|
37
|
+
changed_at: changedAt || new Date().toISOString()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function validateBridgeRoutingPolicy(policy, routeIndex) {
|
|
41
|
+
const blockers = [];
|
|
42
|
+
if (policy.schema !== 'sks.bridge-routing-policy.v1')
|
|
43
|
+
blockers.push('bridge_route_policy_schema_invalid');
|
|
44
|
+
if (policy.fallback !== 'none')
|
|
45
|
+
blockers.push('bridge_route_policy_fallback_forbidden');
|
|
46
|
+
if (!policy.catalog_generation)
|
|
47
|
+
blockers.push('bridge_route_policy_catalog_generation_missing');
|
|
48
|
+
const canonical = canonicalRoutes(policy.model_routes);
|
|
49
|
+
const semantic = {
|
|
50
|
+
default_provider_id: policy.default_provider_id,
|
|
51
|
+
fallback: 'none',
|
|
52
|
+
model_routes: canonical,
|
|
53
|
+
catalog_generation: policy.catalog_generation
|
|
54
|
+
};
|
|
55
|
+
if (sha256Stable(semantic) !== policy.policy_generation)
|
|
56
|
+
blockers.push('bridge_route_policy_generation_invalid');
|
|
57
|
+
if (Object.keys(canonical).length !== Object.keys(policy.model_routes).length)
|
|
58
|
+
blockers.push('bridge_route_policy_model_invalid');
|
|
59
|
+
if (routeIndex) {
|
|
60
|
+
for (const [model, target] of Object.entries(canonical)) {
|
|
61
|
+
const indexed = routeIndex.routes[model];
|
|
62
|
+
if (!indexed || !sameTarget(indexed, target))
|
|
63
|
+
blockers.push('bridge_route_policy_route_index_mismatch');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return unique(blockers);
|
|
67
|
+
}
|
|
68
|
+
export async function writeBridgeRoutingPolicy(file, policy, routeIndex) {
|
|
69
|
+
const blockers = validateBridgeRoutingPolicy(policy, routeIndex);
|
|
70
|
+
if (blockers.length > 0)
|
|
71
|
+
throw new Error(blockers[0]);
|
|
72
|
+
await withFileLock({
|
|
73
|
+
lockPath: `${path.resolve(file)}.lock`,
|
|
74
|
+
timeoutMs: 10_000,
|
|
75
|
+
staleMs: 60_000
|
|
76
|
+
}, () => writeJsonAtomic(file, policy, { mode: 0o600 }));
|
|
77
|
+
}
|
|
78
|
+
export async function readBridgeRoutingPolicy(file) {
|
|
79
|
+
const stat = await fs.lstat(file).catch(() => null);
|
|
80
|
+
if (!stat)
|
|
81
|
+
return { policy: null, blockers: ['bridge_route_policy_missing'] };
|
|
82
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
83
|
+
return { policy: null, blockers: ['bridge_route_policy_not_regular_file'] };
|
|
84
|
+
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
85
|
+
if (expectedUid !== null && stat.uid !== expectedUid)
|
|
86
|
+
return { policy: null, blockers: ['bridge_route_policy_owner_mismatch'] };
|
|
87
|
+
if (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600) {
|
|
88
|
+
return { policy: null, blockers: ['bridge_route_policy_mode_insecure'] };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const policy = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
92
|
+
const blockers = validateBridgeRoutingPolicy(policy);
|
|
93
|
+
return blockers.length > 0 ? { policy: null, blockers } : { policy, blockers: [] };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { policy: null, blockers: ['bridge_route_policy_json_invalid'] };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function canonicalRoutes(routes) {
|
|
100
|
+
const rows = [];
|
|
101
|
+
for (const [model, target] of Object.entries(routes)) {
|
|
102
|
+
const canonicalModel = canonicalizeBridgeModelId(model);
|
|
103
|
+
const upstreamModel = normalizeBridgeUpstreamModelId(target.upstream_model);
|
|
104
|
+
if (!canonicalModel || !upstreamModel || !isProviderId(target.provider_id))
|
|
105
|
+
continue;
|
|
106
|
+
rows.push([canonicalModel, { provider_id: target.provider_id, upstream_model: upstreamModel }]);
|
|
107
|
+
}
|
|
108
|
+
return Object.fromEntries(rows.sort(([left], [right]) => left.localeCompare(right)));
|
|
109
|
+
}
|
|
110
|
+
function isProviderId(value) {
|
|
111
|
+
return value === 'codex-lb' || value === 'openrouter';
|
|
112
|
+
}
|
|
113
|
+
function sameTarget(left, right) {
|
|
114
|
+
return left.provider_id === right.provider_id && left.upstream_model === right.upstream_model;
|
|
115
|
+
}
|
|
116
|
+
function unique(values) {
|
|
117
|
+
return [...new Set(values)];
|
|
118
|
+
}
|