open-managed-agents 0.1.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/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import lockfile from "proper-lockfile";
|
|
6
|
+
const DEFAULT_AUTH_JSON = "{}";
|
|
7
|
+
const FILE_MODE = 0o600;
|
|
8
|
+
const DIR_MODE = 0o700;
|
|
9
|
+
const DEFAULT_STALE_MS = 30_000;
|
|
10
|
+
export class OmaAuthStorageBackend {
|
|
11
|
+
authPath;
|
|
12
|
+
staleMs;
|
|
13
|
+
syncRetryAttempts;
|
|
14
|
+
syncRetryDelayMs;
|
|
15
|
+
asyncRetries;
|
|
16
|
+
beforeRename;
|
|
17
|
+
constructor(authPath, options = {}) {
|
|
18
|
+
this.authPath = resolve(authPath);
|
|
19
|
+
this.staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
20
|
+
this.syncRetryAttempts = options.syncRetryAttempts ?? 50;
|
|
21
|
+
this.syncRetryDelayMs = options.syncRetryDelayMs ?? 20;
|
|
22
|
+
this.asyncRetries = options.asyncRetries ?? 10;
|
|
23
|
+
this.beforeRename = options.beforeRename;
|
|
24
|
+
}
|
|
25
|
+
withLock(fn) {
|
|
26
|
+
this.prepareStorage();
|
|
27
|
+
let release;
|
|
28
|
+
try {
|
|
29
|
+
release = this.acquireLockSync();
|
|
30
|
+
this.assertSafeAuthFile();
|
|
31
|
+
const current = readFileSync(this.authPath, "utf8");
|
|
32
|
+
const { result, next } = fn(current);
|
|
33
|
+
if (next !== undefined) {
|
|
34
|
+
this.atomicWrite(next);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
release?.();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async withLockAsync(fn) {
|
|
43
|
+
this.prepareStorage();
|
|
44
|
+
let release;
|
|
45
|
+
let compromised;
|
|
46
|
+
const throwIfCompromised = () => {
|
|
47
|
+
if (compromised) {
|
|
48
|
+
throw compromised;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
release = await lockfile.lock(this.authPath, {
|
|
53
|
+
realpath: false,
|
|
54
|
+
stale: this.staleMs,
|
|
55
|
+
retries: {
|
|
56
|
+
retries: this.asyncRetries,
|
|
57
|
+
factor: 2,
|
|
58
|
+
minTimeout: 25,
|
|
59
|
+
maxTimeout: 250,
|
|
60
|
+
randomize: true,
|
|
61
|
+
},
|
|
62
|
+
onCompromised: (error) => {
|
|
63
|
+
compromised = error;
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
throwIfCompromised();
|
|
67
|
+
this.assertSafeAuthFile();
|
|
68
|
+
const current = readFileSync(this.authPath, "utf8");
|
|
69
|
+
const { result, next } = await fn(current);
|
|
70
|
+
throwIfCompromised();
|
|
71
|
+
if (next !== undefined) {
|
|
72
|
+
this.atomicWrite(next);
|
|
73
|
+
}
|
|
74
|
+
throwIfCompromised();
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (release) {
|
|
79
|
+
try {
|
|
80
|
+
await release();
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
if (!compromised) {
|
|
84
|
+
throw new Error("Failed to release auth storage lock");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
prepareStorage() {
|
|
91
|
+
const dir = dirname(this.authPath);
|
|
92
|
+
mkdirSync(dir, { recursive: true, mode: DIR_MODE });
|
|
93
|
+
this.assertSafeDirectory(dir);
|
|
94
|
+
if (!existsSync(this.authPath)) {
|
|
95
|
+
this.createInitialFile();
|
|
96
|
+
}
|
|
97
|
+
this.assertSafeAuthFile();
|
|
98
|
+
}
|
|
99
|
+
createInitialFile() {
|
|
100
|
+
try {
|
|
101
|
+
const fd = openSync(this.authPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollowFlag(), FILE_MODE);
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(fd, DEFAULT_AUTH_JSON, "utf8");
|
|
104
|
+
fsyncSync(fd);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
chmodSync(this.authPath, FILE_MODE);
|
|
110
|
+
this.fsyncParent();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (isNodeError(error) && error.code === "EEXIST") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
acquireLockSync() {
|
|
120
|
+
let lastError;
|
|
121
|
+
for (let attempt = 1; attempt <= this.syncRetryAttempts; attempt += 1) {
|
|
122
|
+
try {
|
|
123
|
+
return lockfile.lockSync(this.authPath, {
|
|
124
|
+
realpath: false,
|
|
125
|
+
stale: this.staleMs,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (!isNodeError(error) || error.code !== "ELOCKED" || attempt === this.syncRetryAttempts) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
lastError = error;
|
|
133
|
+
sleepSync(this.syncRetryDelayMs);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw lastError ?? new Error("Failed to acquire auth storage lock");
|
|
137
|
+
}
|
|
138
|
+
atomicWrite(content) {
|
|
139
|
+
const dir = dirname(this.authPath);
|
|
140
|
+
const temp = resolve(dir, `.${basenameForTemp(this.authPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
141
|
+
let fd;
|
|
142
|
+
try {
|
|
143
|
+
fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollowFlag(), FILE_MODE);
|
|
144
|
+
writeFileSync(fd, content, "utf8");
|
|
145
|
+
fsyncSync(fd);
|
|
146
|
+
closeSync(fd);
|
|
147
|
+
fd = undefined;
|
|
148
|
+
chmodSync(temp, FILE_MODE);
|
|
149
|
+
this.beforeRename?.();
|
|
150
|
+
renameSync(temp, this.authPath);
|
|
151
|
+
chmodSync(this.authPath, FILE_MODE);
|
|
152
|
+
this.assertSafeAuthFile();
|
|
153
|
+
this.fsyncParent();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (fd !== undefined) {
|
|
157
|
+
closeSync(fd);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(temp);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Best effort cleanup; failing here would hide the original error.
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
assertSafeDirectory(path) {
|
|
169
|
+
const lst = lstatSync(path);
|
|
170
|
+
if (lst.isSymbolicLink()) {
|
|
171
|
+
throw new Error(`Refusing to use symlinked auth directory: ${path}`);
|
|
172
|
+
}
|
|
173
|
+
const st = statSync(path);
|
|
174
|
+
if (!st.isDirectory()) {
|
|
175
|
+
throw new Error(`Auth storage parent is not a directory: ${path}`);
|
|
176
|
+
}
|
|
177
|
+
assertOwned(path, st.uid, "auth directory");
|
|
178
|
+
const mode = st.mode & 0o777;
|
|
179
|
+
if (mode !== DIR_MODE) {
|
|
180
|
+
throw new Error(`Refusing unsafe auth directory permissions ${formatMode(mode)} for ${path}; expected 0700`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
assertSafeAuthFile() {
|
|
184
|
+
const lst = lstatSync(this.authPath);
|
|
185
|
+
if (lst.isSymbolicLink()) {
|
|
186
|
+
throw new Error(`Refusing to use symlinked auth file: ${this.authPath}`);
|
|
187
|
+
}
|
|
188
|
+
const st = statSync(this.authPath);
|
|
189
|
+
if (!st.isFile()) {
|
|
190
|
+
throw new Error(`Auth storage path is not a regular file: ${this.authPath}`);
|
|
191
|
+
}
|
|
192
|
+
assertOwned(this.authPath, st.uid, "auth file");
|
|
193
|
+
const mode = st.mode & 0o777;
|
|
194
|
+
if (mode !== FILE_MODE) {
|
|
195
|
+
throw new Error(`Refusing unsafe auth file permissions ${formatMode(mode)} for ${this.authPath}; expected 0600`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
fsyncParent() {
|
|
199
|
+
const fd = openSync(dirname(this.authPath), constants.O_RDONLY);
|
|
200
|
+
try {
|
|
201
|
+
fsyncSync(fd);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
closeSync(fd);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export function createOmaAuthStorageBackend(authPath, options) {
|
|
209
|
+
return new OmaAuthStorageBackend(authPath, options);
|
|
210
|
+
}
|
|
211
|
+
function noFollowFlag() {
|
|
212
|
+
return typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
|
|
213
|
+
}
|
|
214
|
+
function sleepSync(delayMs) {
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
while (Date.now() - start < delayMs) {
|
|
217
|
+
// Busy wait preserves Pi's synchronous backend contract.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function basenameForTemp(path) {
|
|
221
|
+
const index = path.lastIndexOf("/");
|
|
222
|
+
return index >= 0 ? path.slice(index + 1) : path;
|
|
223
|
+
}
|
|
224
|
+
function assertOwned(path, uid, label) {
|
|
225
|
+
if (typeof process.getuid !== "function") {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const currentUid = process.getuid();
|
|
229
|
+
if (uid !== currentUid) {
|
|
230
|
+
throw new Error(`Refusing ${label} not owned by current user: ${path}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function formatMode(mode) {
|
|
234
|
+
return `0${mode.toString(8).padStart(3, "0")}`;
|
|
235
|
+
}
|
|
236
|
+
function isNodeError(error) {
|
|
237
|
+
return error instanceof Error && "code" in error;
|
|
238
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AuthStorage, ModelRegistry, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { scanModelConfigSecurity } from "./config-security.js";
|
|
3
|
+
export const PINNED_PI_MODEL_RUNTIME_VERSION = "0.80.6";
|
|
4
|
+
export function createPiModelCatalog(config) {
|
|
5
|
+
return createCatalog(config, AuthStorage.fromStorage(config.authBackend));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Read-only catalog construction for diagnostics. The caller supplies an
|
|
9
|
+
* already-read credential snapshot, so Pi never opens, locks, or creates an
|
|
10
|
+
* auth file. Runtime code must continue to use createPiModelCatalog().
|
|
11
|
+
*/
|
|
12
|
+
export function createReadOnlyPiModelCatalog(config, authData = {}) {
|
|
13
|
+
return createCatalog(config, AuthStorage.inMemory(authData));
|
|
14
|
+
}
|
|
15
|
+
function createCatalog(config, authStorage) {
|
|
16
|
+
const allowedProviders = new Set(config.allowedProviders);
|
|
17
|
+
if (allowedProviders.size !== config.allowedProviders.length) {
|
|
18
|
+
throw new Error("OMA_MODEL_PROVIDERS must not contain duplicate providers");
|
|
19
|
+
}
|
|
20
|
+
if (!allowedProviders.has(config.defaultModel.provider)) {
|
|
21
|
+
throw new Error(`Default model provider ${config.defaultModel.provider} is not enabled on this deployment`);
|
|
22
|
+
}
|
|
23
|
+
const securityReport = scanModelConfigSecurity({
|
|
24
|
+
modelsPath: config.modelsPath,
|
|
25
|
+
authPath: config.authPath,
|
|
26
|
+
allowedProviders,
|
|
27
|
+
allowCommands: config.allowModelAuthCommands,
|
|
28
|
+
});
|
|
29
|
+
const authErrors = authStorage.drainErrors();
|
|
30
|
+
if (authErrors.length > 0) {
|
|
31
|
+
throw new Error(`Failed to load model auth storage: ${authErrors.map((error) => error.message).join("; ")}`);
|
|
32
|
+
}
|
|
33
|
+
const modelRegistry = ModelRegistry.create(authStorage, config.modelsPath);
|
|
34
|
+
const registryError = modelRegistry.getError();
|
|
35
|
+
if (registryError !== undefined) {
|
|
36
|
+
throw new Error(registryError);
|
|
37
|
+
}
|
|
38
|
+
for (const provider of allowedProviders) {
|
|
39
|
+
if (!modelRegistry.getAll().some((model) => model.provider === provider)) {
|
|
40
|
+
throw new Error(`Model provider ${provider} is not available in the Pi model registry`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!modelRegistry.find(config.defaultModel.provider, config.defaultModel.id)) {
|
|
44
|
+
throw new Error(`Default model ${config.defaultModel.provider}/${config.defaultModel.id} is not available on this deployment`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
defaultModel: { ...config.defaultModel },
|
|
48
|
+
allowedProviders,
|
|
49
|
+
authStorage,
|
|
50
|
+
modelRegistry,
|
|
51
|
+
securityReport,
|
|
52
|
+
resolve(ref) {
|
|
53
|
+
if (!allowedProviders.has(ref.provider))
|
|
54
|
+
return undefined;
|
|
55
|
+
return modelRegistry.find(ref.provider, ref.id);
|
|
56
|
+
},
|
|
57
|
+
list(options = {}) {
|
|
58
|
+
const models = options.availableOnly ? modelRegistry.getAvailable() : modelRegistry.getAll();
|
|
59
|
+
return models.filter((model) => {
|
|
60
|
+
if (!allowedProviders.has(model.provider))
|
|
61
|
+
return false;
|
|
62
|
+
return options.provider === undefined || model.provider === options.provider;
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
hasConfiguredAuth(model) {
|
|
66
|
+
// Deliberately model-scoped: provider status can report env credentials as
|
|
67
|
+
// configured:false in Pi 0.80.6 while the resolved model is usable.
|
|
68
|
+
return modelRegistry.hasConfiguredAuth(model);
|
|
69
|
+
},
|
|
70
|
+
providerAuthMetadata(provider) {
|
|
71
|
+
const status = modelRegistry.getProviderAuthStatus(provider);
|
|
72
|
+
return {
|
|
73
|
+
...(status.source === undefined ? {} : { source: status.source }),
|
|
74
|
+
...(status.label === undefined ? {} : { label: status.label }),
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, statSync, } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
export const SUPPORTED_PI_MODEL_APIS = [
|
|
5
|
+
"anthropic-messages",
|
|
6
|
+
"openai-completions",
|
|
7
|
+
"openai-responses",
|
|
8
|
+
"azure-openai-responses",
|
|
9
|
+
"openai-codex-responses",
|
|
10
|
+
"mistral-conversations",
|
|
11
|
+
"google-generative-ai",
|
|
12
|
+
"google-vertex",
|
|
13
|
+
"bedrock-converse-stream",
|
|
14
|
+
];
|
|
15
|
+
export function scanModelConfigSecurity(options) {
|
|
16
|
+
const warnings = [];
|
|
17
|
+
if (options.modelsPath) {
|
|
18
|
+
scanPathSafety(options.modelsPath, "models.json");
|
|
19
|
+
if (existsSync(options.modelsPath)) {
|
|
20
|
+
scanModelsJson(options.modelsPath, options, warnings);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (options.authPath) {
|
|
24
|
+
scanPathSafety(options.authPath, "auth.json");
|
|
25
|
+
if (existsSync(options.authPath)) {
|
|
26
|
+
scanAuthJson(options.authPath, options.allowCommands === true);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { warnings };
|
|
30
|
+
}
|
|
31
|
+
export function parseJsoncObject(text, label) {
|
|
32
|
+
const parsed = JSON.parse(stripJsonComments(text));
|
|
33
|
+
if (!isPlainObject(parsed)) {
|
|
34
|
+
throw new Error(`${label} must contain a JSON object`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
function scanModelsJson(path, options, warnings) {
|
|
39
|
+
const root = parseJsoncObject(readFileSync(path, "utf8"), "models.json");
|
|
40
|
+
assertOnlyKeys(root, ["providers"], "models.json");
|
|
41
|
+
const providers = requiredObject(root.providers, "models.json.providers");
|
|
42
|
+
for (const [provider, rawProviderConfig] of Object.entries(providers)) {
|
|
43
|
+
if (!options.allowedProviders.has(provider)) {
|
|
44
|
+
throw new Error(`Model provider ${provider} is not enabled on this deployment`);
|
|
45
|
+
}
|
|
46
|
+
const providerConfig = requiredObject(rawProviderConfig, `models.json.providers.${provider}`);
|
|
47
|
+
assertOnlyKeys(providerConfig, ["name", "baseUrl", "apiKey", "api", "headers", "compat", "authHeader", "models", "modelOverrides"], `models.json.providers.${provider}`);
|
|
48
|
+
const providerBaseUrl = scanOptionalUrl(providerConfig.baseUrl, `models.json.providers.${provider}.baseUrl`);
|
|
49
|
+
scanOptionalApi(providerConfig.api, `models.json.providers.${provider}.api`);
|
|
50
|
+
scanOptionalConfigValue(providerConfig.apiKey, `models.json.providers.${provider}.apiKey`, options.allowCommands === true);
|
|
51
|
+
maybeWarnLiteralCredential(warnings, providerConfig.apiKey, `models.json.providers.${provider}.apiKey`, providerBaseUrl);
|
|
52
|
+
scanHeaders(providerConfig.headers, `models.json.providers.${provider}.headers`, options.allowCommands === true, warnings);
|
|
53
|
+
scanCompat(providerConfig.compat, `models.json.providers.${provider}.compat`);
|
|
54
|
+
if (providerConfig.models !== undefined) {
|
|
55
|
+
if (!Array.isArray(providerConfig.models)) {
|
|
56
|
+
throw new Error(`models.json.providers.${provider}.models must be an array`);
|
|
57
|
+
}
|
|
58
|
+
for (const [index, rawModel] of providerConfig.models.entries()) {
|
|
59
|
+
const modelPath = `models.json.providers.${provider}.models[${index}]`;
|
|
60
|
+
const model = requiredObject(rawModel, modelPath);
|
|
61
|
+
assertOnlyKeys(model, ["id", "name", "api", "baseUrl", "reasoning", "thinkingLevelMap", "input", "cost", "contextWindow", "maxTokens", "headers", "compat"], modelPath);
|
|
62
|
+
scanOptionalApi(model.api, `${modelPath}.api`);
|
|
63
|
+
scanOptionalUrl(model.baseUrl, `${modelPath}.baseUrl`);
|
|
64
|
+
scanHeaders(model.headers, `${modelPath}.headers`, options.allowCommands === true, warnings);
|
|
65
|
+
scanThinkingLevelMap(model.thinkingLevelMap, `${modelPath}.thinkingLevelMap`);
|
|
66
|
+
scanCost(model.cost, `${modelPath}.cost`, false);
|
|
67
|
+
scanCompat(model.compat, `${modelPath}.compat`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (providerConfig.modelOverrides !== undefined) {
|
|
71
|
+
const overrides = requiredObject(providerConfig.modelOverrides, `models.json.providers.${provider}.modelOverrides`);
|
|
72
|
+
for (const [modelId, rawOverride] of Object.entries(overrides)) {
|
|
73
|
+
const overridePath = `models.json.providers.${provider}.modelOverrides.${modelId}`;
|
|
74
|
+
const override = requiredObject(rawOverride, overridePath);
|
|
75
|
+
assertOnlyKeys(override, ["name", "reasoning", "thinkingLevelMap", "input", "cost", "contextWindow", "maxTokens", "headers", "compat"], overridePath);
|
|
76
|
+
scanHeaders(override.headers, `${overridePath}.headers`, options.allowCommands === true, warnings);
|
|
77
|
+
scanThinkingLevelMap(override.thinkingLevelMap, `${overridePath}.thinkingLevelMap`);
|
|
78
|
+
scanCost(override.cost, `${overridePath}.cost`, true);
|
|
79
|
+
scanCompat(override.compat, `${overridePath}.compat`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function scanAuthJson(path, allowCommands) {
|
|
85
|
+
const auth = parseJsoncObject(readFileSync(path, "utf8"), "auth.json");
|
|
86
|
+
for (const [provider, rawCredential] of Object.entries(auth)) {
|
|
87
|
+
const credential = requiredObject(rawCredential, `auth.json.${provider}`);
|
|
88
|
+
if (credential.type === "api_key") {
|
|
89
|
+
scanOptionalConfigValue(credential.key, `auth.json.${provider}.key`, allowCommands);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function scanPathSafety(path, label) {
|
|
94
|
+
const resolved = resolve(path);
|
|
95
|
+
const parent = dirname(resolved);
|
|
96
|
+
if (existsSync(parent)) {
|
|
97
|
+
const parentStat = lstatSync(parent);
|
|
98
|
+
if (parentStat.isSymbolicLink()) {
|
|
99
|
+
throw new Error(`${label} parent directory must not be a symlink: ${parent}`);
|
|
100
|
+
}
|
|
101
|
+
if ((parentStat.mode & 0o022) !== 0) {
|
|
102
|
+
throw new Error(`${label} parent directory must not be group/world writable: ${parent}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!existsSync(resolved))
|
|
106
|
+
return;
|
|
107
|
+
const lst = lstatSync(resolved);
|
|
108
|
+
if (lst.isSymbolicLink()) {
|
|
109
|
+
throw new Error(`${label} must not be a symlink: ${resolved}`);
|
|
110
|
+
}
|
|
111
|
+
const st = statSync(resolved);
|
|
112
|
+
if (!st.isFile()) {
|
|
113
|
+
throw new Error(`${label} must be a regular file: ${resolved}`);
|
|
114
|
+
}
|
|
115
|
+
if ((st.mode & 0o022) !== 0) {
|
|
116
|
+
throw new Error(`${label} must not be group/world writable: ${resolved}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function stripJsonComments(text) {
|
|
120
|
+
let out = "";
|
|
121
|
+
let inString = false;
|
|
122
|
+
let escaped = false;
|
|
123
|
+
for (let i = 0; i < text.length; i++) {
|
|
124
|
+
const ch = text[i];
|
|
125
|
+
const next = text[i + 1];
|
|
126
|
+
if (inString) {
|
|
127
|
+
out += ch;
|
|
128
|
+
if (escaped) {
|
|
129
|
+
escaped = false;
|
|
130
|
+
}
|
|
131
|
+
else if (ch === "\\") {
|
|
132
|
+
escaped = true;
|
|
133
|
+
}
|
|
134
|
+
else if (ch === "\"") {
|
|
135
|
+
inString = false;
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (ch === "\"") {
|
|
140
|
+
inString = true;
|
|
141
|
+
out += ch;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (ch === "/" && next === "/") {
|
|
145
|
+
out += " ";
|
|
146
|
+
i += 1;
|
|
147
|
+
while (i + 1 < text.length && text[i + 1] !== "\n" && text[i + 1] !== "\r") {
|
|
148
|
+
out += " ";
|
|
149
|
+
i += 1;
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (ch === "/" && next === "*") {
|
|
154
|
+
out += " ";
|
|
155
|
+
i += 1;
|
|
156
|
+
while (i + 1 < text.length && !(text[i + 1] === "*" && text[i + 2] === "/")) {
|
|
157
|
+
out += text[i + 1] === "\n" || text[i + 1] === "\r" ? text[i + 1] : " ";
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
if (i + 2 < text.length) {
|
|
161
|
+
out += " ";
|
|
162
|
+
i += 2;
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
out += ch;
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
function scanOptionalApi(value, path) {
|
|
171
|
+
if (value === undefined)
|
|
172
|
+
return;
|
|
173
|
+
if (typeof value !== "string")
|
|
174
|
+
throw new Error(`${path} must be a string`);
|
|
175
|
+
if (!SUPPORTED_PI_MODEL_APIS.includes(value)) {
|
|
176
|
+
throw new Error(`${path} uses unsupported Pi model adapter ${JSON.stringify(value)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function scanOptionalUrl(value, path) {
|
|
180
|
+
if (value === undefined)
|
|
181
|
+
return undefined;
|
|
182
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
183
|
+
throw new Error(`${path} must be a non-empty string`);
|
|
184
|
+
let url;
|
|
185
|
+
try {
|
|
186
|
+
url = new URL(value);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
throw new Error(`${path} must be a valid URL`);
|
|
190
|
+
}
|
|
191
|
+
if (url.username || url.password) {
|
|
192
|
+
throw new Error(`${path} must not contain username/password userinfo`);
|
|
193
|
+
}
|
|
194
|
+
if (url.protocol === "https:")
|
|
195
|
+
return url;
|
|
196
|
+
if (url.protocol === "http:" && isLoopbackHost(url.hostname))
|
|
197
|
+
return url;
|
|
198
|
+
throw new Error(`${path} must be https, or http only for syntactic loopback hosts`);
|
|
199
|
+
}
|
|
200
|
+
function isLoopbackHost(hostname) {
|
|
201
|
+
const normalized = hostname.toLowerCase();
|
|
202
|
+
if (normalized === "localhost" || normalized === "[::1]" || normalized === "::1")
|
|
203
|
+
return true;
|
|
204
|
+
if (isIP(normalized) === 4) {
|
|
205
|
+
const [first] = normalized.split(".");
|
|
206
|
+
return first === "127";
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function scanOptionalConfigValue(value, path, allowCommands) {
|
|
211
|
+
if (value === undefined)
|
|
212
|
+
return;
|
|
213
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
214
|
+
throw new Error(`${path} must be a non-empty string`);
|
|
215
|
+
if (value.startsWith("!") && !allowCommands) {
|
|
216
|
+
throw new Error(`${path} uses command-backed auth; set OMA_ALLOW_MODEL_AUTH_COMMANDS=true to allow it`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function maybeWarnLiteralCredential(warnings, value, path, baseUrl) {
|
|
220
|
+
if (typeof value !== "string")
|
|
221
|
+
return;
|
|
222
|
+
if (value.startsWith("$") || value.startsWith("!") || value === "")
|
|
223
|
+
return;
|
|
224
|
+
if (value === "oma-local-keyless" && baseUrl?.protocol === "http:" && isLoopbackHost(baseUrl.hostname))
|
|
225
|
+
return;
|
|
226
|
+
warnings.push(`${path} contains a literal credential; prefer auth.json or environment interpolation`);
|
|
227
|
+
}
|
|
228
|
+
function scanHeaders(value, path, allowCommands, warnings) {
|
|
229
|
+
if (value === undefined)
|
|
230
|
+
return;
|
|
231
|
+
const headers = requiredObject(value, path);
|
|
232
|
+
for (const [name, headerValue] of Object.entries(headers)) {
|
|
233
|
+
scanOptionalConfigValue(headerValue, `${path}.${name}`, allowCommands);
|
|
234
|
+
if (typeof headerValue === "string" &&
|
|
235
|
+
!headerValue.startsWith("$") &&
|
|
236
|
+
!headerValue.startsWith("!") &&
|
|
237
|
+
/authorization|api[-_]?key|token|secret/i.test(name)) {
|
|
238
|
+
warnings.push(`${path}.${name} contains a literal credential-like header; prefer auth.json or environment interpolation`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function scanThinkingLevelMap(value, path) {
|
|
243
|
+
if (value === undefined)
|
|
244
|
+
return;
|
|
245
|
+
const map = requiredObject(value, path);
|
|
246
|
+
assertOnlyKeys(map, ["off", "minimal", "low", "medium", "high", "xhigh", "max"], path);
|
|
247
|
+
for (const [key, item] of Object.entries(map)) {
|
|
248
|
+
if (item !== null && typeof item !== "string")
|
|
249
|
+
throw new Error(`${path}.${key} must be a string or null`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function scanCost(value, path, partial) {
|
|
253
|
+
if (value === undefined)
|
|
254
|
+
return;
|
|
255
|
+
const cost = requiredObject(value, path);
|
|
256
|
+
assertOnlyKeys(cost, ["input", "output", "cacheRead", "cacheWrite", "tiers"], path);
|
|
257
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
|
|
258
|
+
if (cost[key] !== undefined && typeof cost[key] !== "number")
|
|
259
|
+
throw new Error(`${path}.${key} must be a number`);
|
|
260
|
+
if (!partial && cost[key] === undefined && cost.tiers === undefined) {
|
|
261
|
+
// Pi still owns full semantic validation; this only prevents odd non-numeric values.
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (cost.tiers !== undefined) {
|
|
265
|
+
if (!Array.isArray(cost.tiers))
|
|
266
|
+
throw new Error(`${path}.tiers must be an array`);
|
|
267
|
+
for (const [index, rawTier] of cost.tiers.entries()) {
|
|
268
|
+
const tierPath = `${path}.tiers[${index}]`;
|
|
269
|
+
const tier = requiredObject(rawTier, tierPath);
|
|
270
|
+
assertOnlyKeys(tier, ["inputTokensAbove", "input", "output", "cacheRead", "cacheWrite"], tierPath);
|
|
271
|
+
for (const key of ["inputTokensAbove", "input", "output", "cacheRead", "cacheWrite"]) {
|
|
272
|
+
if (typeof tier[key] !== "number")
|
|
273
|
+
throw new Error(`${tierPath}.${key} must be a number`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function scanCompat(value, path) {
|
|
279
|
+
if (value === undefined)
|
|
280
|
+
return;
|
|
281
|
+
const compat = requiredObject(value, path);
|
|
282
|
+
assertOnlyKeys(compat, [
|
|
283
|
+
"supportsStore",
|
|
284
|
+
"supportsDeveloperRole",
|
|
285
|
+
"supportsReasoningEffort",
|
|
286
|
+
"supportsUsageInStreaming",
|
|
287
|
+
"maxTokensField",
|
|
288
|
+
"requiresToolResultName",
|
|
289
|
+
"requiresAssistantAfterToolResult",
|
|
290
|
+
"requiresThinkingAsText",
|
|
291
|
+
"requiresReasoningContentOnAssistantMessages",
|
|
292
|
+
"thinkingFormat",
|
|
293
|
+
"chatTemplateKwargs",
|
|
294
|
+
"cacheControlFormat",
|
|
295
|
+
"openRouterRouting",
|
|
296
|
+
"vercelGatewayRouting",
|
|
297
|
+
"supportsStrictMode",
|
|
298
|
+
"supportsLongCacheRetention",
|
|
299
|
+
"sendSessionIdHeader",
|
|
300
|
+
"supportsEagerToolInputStreaming",
|
|
301
|
+
"sendSessionAffinityHeaders",
|
|
302
|
+
"supportsCacheControlOnTools",
|
|
303
|
+
"forceAdaptiveThinking",
|
|
304
|
+
], path);
|
|
305
|
+
if (compat.openRouterRouting !== undefined)
|
|
306
|
+
scanOpenRouterRouting(compat.openRouterRouting, `${path}.openRouterRouting`);
|
|
307
|
+
if (compat.vercelGatewayRouting !== undefined)
|
|
308
|
+
scanVercelGatewayRouting(compat.vercelGatewayRouting, `${path}.vercelGatewayRouting`);
|
|
309
|
+
if (compat.chatTemplateKwargs !== undefined)
|
|
310
|
+
scanChatTemplateKwargs(compat.chatTemplateKwargs, `${path}.chatTemplateKwargs`);
|
|
311
|
+
}
|
|
312
|
+
function scanOpenRouterRouting(value, path) {
|
|
313
|
+
const routing = requiredObject(value, path);
|
|
314
|
+
assertOnlyKeys(routing, [
|
|
315
|
+
"allow_fallbacks",
|
|
316
|
+
"require_parameters",
|
|
317
|
+
"data_collection",
|
|
318
|
+
"zdr",
|
|
319
|
+
"enforce_distillable_text",
|
|
320
|
+
"order",
|
|
321
|
+
"only",
|
|
322
|
+
"ignore",
|
|
323
|
+
"quantizations",
|
|
324
|
+
"sort",
|
|
325
|
+
"max_price",
|
|
326
|
+
"preferred_min_throughput",
|
|
327
|
+
"preferred_max_latency",
|
|
328
|
+
], path);
|
|
329
|
+
if (routing.sort !== undefined && isPlainObject(routing.sort)) {
|
|
330
|
+
assertOnlyKeys(routing.sort, ["by", "partition"], `${path}.sort`);
|
|
331
|
+
}
|
|
332
|
+
if (routing.max_price !== undefined) {
|
|
333
|
+
assertOnlyKeys(requiredObject(routing.max_price, `${path}.max_price`), ["prompt", "completion", "image", "audio", "request"], `${path}.max_price`);
|
|
334
|
+
}
|
|
335
|
+
for (const key of ["preferred_min_throughput", "preferred_max_latency"]) {
|
|
336
|
+
const item = routing[key];
|
|
337
|
+
if (isPlainObject(item))
|
|
338
|
+
assertOnlyKeys(item, ["p50", "p75", "p90", "p99"], `${path}.${key}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function scanVercelGatewayRouting(value, path) {
|
|
342
|
+
assertOnlyKeys(requiredObject(value, path), ["only", "order"], path);
|
|
343
|
+
}
|
|
344
|
+
function scanChatTemplateKwargs(value, path) {
|
|
345
|
+
const kwargs = requiredObject(value, path);
|
|
346
|
+
for (const [key, rawValue] of Object.entries(kwargs)) {
|
|
347
|
+
if (isPlainObject(rawValue)) {
|
|
348
|
+
assertOnlyKeys(rawValue, ["$var", "omitWhenOff"], `${path}.${key}`);
|
|
349
|
+
if (rawValue.$var !== "thinking.enabled" && rawValue.$var !== "thinking.effort") {
|
|
350
|
+
throw new Error(`${path}.${key}.$var must reference a supported Pi variable`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (rawValue !== null &&
|
|
354
|
+
typeof rawValue !== "string" &&
|
|
355
|
+
typeof rawValue !== "number" &&
|
|
356
|
+
typeof rawValue !== "boolean") {
|
|
357
|
+
throw new Error(`${path}.${key} must be a scalar or variable object`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function assertOnlyKeys(value, allowed, path) {
|
|
362
|
+
const allowedSet = new Set(allowed);
|
|
363
|
+
for (const key of Object.keys(value)) {
|
|
364
|
+
if (!allowedSet.has(key)) {
|
|
365
|
+
throw new Error(`${path}.${key} is not supported by OMA's Pi model security profile`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function requiredObject(value, path) {
|
|
370
|
+
if (!isPlainObject(value))
|
|
371
|
+
throw new Error(`${path} must be an object`);
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
function isPlainObject(value) {
|
|
375
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
376
|
+
}
|