@capybara-hq/kernel 0.1.0-rc.5 → 0.1.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/admin.controller.d.ts +13 -2
- package/dist/admin/admin.controller.js +29 -4
- package/dist/agents/agents.service.d.ts +3 -1
- package/dist/agents/agents.service.js +8 -6
- package/dist/agents/from-template.service.js +3 -3
- package/dist/agents/models.controller.d.ts +34 -5
- package/dist/agents/models.controller.js +50 -6
- package/dist/agents/readiness.controller.d.ts +5 -5
- package/dist/agents/readiness.controller.js +6 -6
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/config/schema.d.ts +4 -0
- package/dist/config/schema.js +24 -0
- package/dist/conversations/conversations.service.d.ts +4 -3
- package/dist/conversations/conversations.service.js +9 -8
- package/dist/ctx/create-ctx.d.ts +15 -1
- package/dist/ctx/create-ctx.js +23 -2
- package/dist/db/kernel-schema.d.ts +24 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +3 -0
- package/dist/evals/run-replay.js +4 -2
- package/dist/evals/runs.service.d.ts +2 -1
- package/dist/evals/runs.service.js +13 -12
- package/dist/evals/step-replay.d.ts +2 -1
- package/dist/evals/step-replay.js +3 -2
- package/dist/extensions/config-hash.d.ts +10 -0
- package/dist/extensions/config-hash.js +38 -0
- package/dist/extensions/extensions.module.js +6 -12
- package/dist/extensions/host.d.ts +6 -10
- package/dist/extensions/host.js +22 -50
- package/dist/extensions/state.module.d.ts +9 -0
- package/dist/extensions/state.module.js +38 -0
- package/dist/http/api.module.js +2 -0
- package/dist/http/plugin-dispatch.d.ts +4 -1
- package/dist/http/plugin-dispatch.js +14 -2
- package/dist/infra/config/kernel-config.service.js +3 -0
- package/dist/infra/events/kernel-events.d.ts +8 -0
- package/dist/models/embeddings.d.ts +82 -0
- package/dist/models/embeddings.js +326 -0
- package/dist/models/enablement.controller.d.ts +40 -0
- package/dist/models/enablement.controller.js +74 -0
- package/dist/models/enablement.d.ts +99 -0
- package/dist/models/enablement.js +263 -0
- package/dist/models/models.module.d.ts +5 -1
- package/dist/models/models.module.js +16 -4
- package/dist/models/purposes.d.ts +18 -4
- package/dist/models/purposes.js +46 -9
- package/dist/platform/exclusions.js +18 -0
- package/dist/runtime/hook-calls.d.ts +8 -1
- package/dist/runtime/hook-calls.js +40 -9
- package/dist/runtime/model-call.d.ts +8 -3
- package/dist/runtime/model-call.js +28 -5
- package/dist/runtime/providers.d.ts +146 -32
- package/dist/runtime/providers.js +321 -67
- package/dist/runtime/run-worker.d.ts +1 -1
- package/dist/runtime/run-worker.js +4 -4
- package/dist/runtime/subagents/definition.js +3 -3
- package/dist/runtime/usage-report-jobs.d.ts +88 -0
- package/dist/runtime/usage-report-jobs.js +158 -0
- package/dist/runtime/usage-report.d.ts +33 -38
- package/dist/runtime/usage-report.js +152 -84
- package/dist/tools/pipeline.js +6 -0
- package/package.json +3 -3
- package/src/migrations/0089_model_kinds_usage.sql +38 -0
package/dist/errors.js
CHANGED
|
@@ -280,6 +280,9 @@ export const ERROR_CODES = {
|
|
|
280
280
|
"eval.candidate_invalid": 400,
|
|
281
281
|
"model.unconfigured": 409,
|
|
282
282
|
"model.purpose_unknown": 404,
|
|
283
|
+
"model.provider_not_found": 404,
|
|
284
|
+
/** An extension's `ctx.models.embed` call was denied by a `model.request` policy hook. */
|
|
285
|
+
"model.hook_denied": 403,
|
|
283
286
|
"eval.result_not_found": 404,
|
|
284
287
|
};
|
|
285
288
|
/** 业务代码抛出的 API 错误;由 HTTP 层的错误过滤器转成错误信封。 */
|
package/dist/evals/run-replay.js
CHANGED
|
@@ -435,8 +435,10 @@ export async function replayRun(d, s, candidate, evalRunId, signal) {
|
|
|
435
435
|
return metered;
|
|
436
436
|
};
|
|
437
437
|
const registry = {
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
// the case's org (its effective provider config), whatever the replayed step passes
|
|
439
|
+
// (and its disabled lists: a candidate disabled there is refused, spec B §5.3)
|
|
440
|
+
resolve: async (_orgId, ref, opts) => {
|
|
441
|
+
const r = await d.registry.resolve(s.orgId, ref, opts);
|
|
440
442
|
return { ...r, provider: meter(r.provider) };
|
|
441
443
|
},
|
|
442
444
|
};
|
|
@@ -149,7 +149,8 @@ export declare class EvalRunsService {
|
|
|
149
149
|
private liveSet;
|
|
150
150
|
/**
|
|
151
151
|
* 解析模型;不存在 / 不可用 → `eval.candidate_invalid`(供应商列表暂时失败照常抛出)。
|
|
152
|
-
*
|
|
152
|
+
* Resolved for the org (its effective provider config), as the runtime does; per-org model
|
|
153
|
+
* admission must cover eval candidates the same way.
|
|
153
154
|
*/
|
|
154
155
|
private resolveModel;
|
|
155
156
|
/**
|
|
@@ -187,11 +187,12 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
189
|
* 解析模型;不存在 / 不可用 → `eval.candidate_invalid`(供应商列表暂时失败照常抛出)。
|
|
190
|
-
*
|
|
190
|
+
* Resolved for the org (its effective provider config), as the runtime does; per-org model
|
|
191
|
+
* admission must cover eval candidates the same way.
|
|
191
192
|
*/
|
|
192
|
-
async resolveModel(ref) {
|
|
193
|
+
async resolveModel(orgId, ref) {
|
|
193
194
|
try {
|
|
194
|
-
return await this.d.registry.resolve(ref);
|
|
195
|
+
return await this.d.registry.resolve(orgId, ref);
|
|
195
196
|
}
|
|
196
197
|
catch (e) {
|
|
197
198
|
if (e instanceof ProviderListError)
|
|
@@ -208,7 +209,7 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
208
209
|
* 校验候选:版本存在(该集合的 agent)、模型可解析、参数按所用模型的声明严格通过(`paramIssues`)。
|
|
209
210
|
* 所用模型 = 候选模型,否则候选版本的模型,否则 agent 当前版本的模型。返回候选版本的定义。
|
|
210
211
|
*/
|
|
211
|
-
async checkCandidate(agentId, c) {
|
|
212
|
+
async checkCandidate(orgId, agentId, c) {
|
|
212
213
|
const versionDef = await versionDefinition(this.d.db, agentId, c.agentVersion);
|
|
213
214
|
if (c.agentVersion !== undefined && versionDef === undefined) {
|
|
214
215
|
throw candidateInvalid(`Agent version ${c.agentVersion} does not exist`, [
|
|
@@ -227,7 +228,7 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
227
228
|
}
|
|
228
229
|
if (model === undefined)
|
|
229
230
|
return versionDef;
|
|
230
|
-
const { info } = await this.resolveModel(model);
|
|
231
|
+
const { info } = await this.resolveModel(orgId, model);
|
|
231
232
|
if (c.modelParams !== undefined) {
|
|
232
233
|
const issues = paramIssues("candidate.modelParams", info.parameters, c.modelParams);
|
|
233
234
|
if (issues.length > 0)
|
|
@@ -239,7 +240,7 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
239
240
|
* 各用例的回放计划与成本:只读写入时存好的列(`plan_step` / `plan_run`、`base_model`),不读快照。
|
|
240
241
|
* 给了候选参数时按每个用例实际所用的模型严格校验(同一模型在一次调用里只解析一次)。
|
|
241
242
|
*/
|
|
242
|
-
async plan(setId, mode, c, versionDef) {
|
|
243
|
+
async plan(orgId, setId, mode, c, versionDef) {
|
|
243
244
|
const table = await this.d.prices.table();
|
|
244
245
|
const versionOnly = c.agentVersion !== undefined && c.model === undefined;
|
|
245
246
|
const rows = await this.d.db
|
|
@@ -277,7 +278,7 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
277
278
|
const key = priceKey(model.provider, model.model);
|
|
278
279
|
if (!infos.has(key)) {
|
|
279
280
|
try {
|
|
280
|
-
infos.set(key, (await this.d.registry.resolve(model)).info);
|
|
281
|
+
infos.set(key, (await this.d.registry.resolve(orgId, model)).info);
|
|
281
282
|
}
|
|
282
283
|
catch (e) {
|
|
283
284
|
if (e instanceof ProviderListError)
|
|
@@ -329,7 +330,7 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
329
330
|
}
|
|
330
331
|
let maxOutputTokens = configured;
|
|
331
332
|
try {
|
|
332
|
-
const { info } = await this.d.registry.resolve(target.model);
|
|
333
|
+
const { info } = await this.d.registry.resolve(orgId, target.model);
|
|
333
334
|
maxOutputTokens = Math.min(configured, info.maxOutputTokens);
|
|
334
335
|
}
|
|
335
336
|
catch (e) {
|
|
@@ -396,8 +397,8 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
396
397
|
const set = await this.liveSet(orgId, setId);
|
|
397
398
|
await this.assertAgentLive(set.agent_id);
|
|
398
399
|
const caps = this.tokenCaps(input.tokenCap);
|
|
399
|
-
const versionDef = await this.checkCandidate(set.agent_id, input.candidate);
|
|
400
|
-
const plans = await this.plan(setId, input.mode, input.candidate, versionDef);
|
|
400
|
+
const versionDef = await this.checkCandidate(orgId, set.agent_id, input.candidate);
|
|
401
|
+
const plans = await this.plan(orgId, setId, input.mode, input.candidate, versionDef);
|
|
401
402
|
const judge = await this.judgePricing(orgId, set.rubric, input.judge);
|
|
402
403
|
return { estimate: await this.summarize(plans, judge), ...caps };
|
|
403
404
|
}
|
|
@@ -421,11 +422,11 @@ let EvalRunsService = EvalRunsService_1 = class EvalRunsService {
|
|
|
421
422
|
const set = await this.liveSet(orgId, setId);
|
|
422
423
|
await this.assertAgentLive(set.agent_id);
|
|
423
424
|
const tokenCap = this.tokenCaps(input.tokenCap).effectiveTokenCap;
|
|
424
|
-
const versionDef = await this.checkCandidate(set.agent_id, input.candidate);
|
|
425
|
+
const versionDef = await this.checkCandidate(orgId, set.agent_id, input.candidate);
|
|
425
426
|
if (input.judge)
|
|
426
427
|
await this.d.purposes.resolve(orgId, "eval");
|
|
427
428
|
const judge = await this.judgePricing(orgId, set.rubric, input.judge);
|
|
428
|
-
const planned = await this.plan(setId, input.mode, input.candidate, versionDef);
|
|
429
|
+
const planned = await this.plan(orgId, setId, input.mode, input.candidate, versionDef);
|
|
429
430
|
if (planned.length === 0) {
|
|
430
431
|
throw new ApiError("eval.nothing_to_replay", "The eval set has nothing to replay");
|
|
431
432
|
}
|
|
@@ -92,7 +92,8 @@ export declare class ReplayAttemptError extends Error {
|
|
|
92
92
|
* 否则带候选的定义、版本与参数——参数按候选模型的声明宽松处理,与运行时一致),用 `consumeModelStream` 收完。
|
|
93
93
|
* 请求发出之前的错误原样抛出(`agent.model_unavailable`、取附件暂时失败为 `MediaUnavailableError`);请求发出之后的
|
|
94
94
|
* 错误包成 {@link ReplayAttemptError}(带估算用量,调用方计入预算后按 `cause` 分类重试)。
|
|
95
|
-
*
|
|
95
|
+
* The candidate model resolves in the snapshot's org like the runtime (its effective provider
|
|
96
|
+
* config and disabled lists: a model disabled there is `agent.model_unavailable`, spec B §5.3).
|
|
96
97
|
* 不另设单次请求的墙钟时限:与运行时一样只有流空闲看门狗(`streamIdleTimeoutMs` / `runtime.streamIdleMs`)。
|
|
97
98
|
*/
|
|
98
99
|
export declare function replayStep(d: StepReplayDeps, s: CaseSnapshot, call: {
|
|
@@ -126,12 +126,13 @@ function baselineOf(s, call) {
|
|
|
126
126
|
* 否则带候选的定义、版本与参数——参数按候选模型的声明宽松处理,与运行时一致),用 `consumeModelStream` 收完。
|
|
127
127
|
* 请求发出之前的错误原样抛出(`agent.model_unavailable`、取附件暂时失败为 `MediaUnavailableError`);请求发出之后的
|
|
128
128
|
* 错误包成 {@link ReplayAttemptError}(带估算用量,调用方计入预算后按 `cause` 分类重试)。
|
|
129
|
-
*
|
|
129
|
+
* The candidate model resolves in the snapshot's org like the runtime (its effective provider
|
|
130
|
+
* config and disabled lists: a model disabled there is `agent.model_unavailable`, spec B §5.3).
|
|
130
131
|
* 不另设单次请求的墙钟时限:与运行时一样只有流空闲看门狗(`streamIdleTimeoutMs` / `runtime.streamIdleMs`)。
|
|
131
132
|
*/
|
|
132
133
|
export async function replayStep(d, s, call, candidate, signal) {
|
|
133
134
|
const baseline = baselineOf(s, call);
|
|
134
|
-
const { provider, info } = await d.registry.resolve(candidate.model);
|
|
135
|
+
const { provider, info } = await d.registry.resolve(s.orgId, candidate.model);
|
|
135
136
|
let media;
|
|
136
137
|
if (d.media) {
|
|
137
138
|
try {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { core } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* 插件部署配置(校验后)中非密钥部分的摘要:先把 schema 里所有 secret 叶子(`configFields` 可达路径,
|
|
4
|
+
* 加载时 `assertSecretsReachable` 已保证密钥都在这些路径上)换成固定占位值,再取 sha256(键排序 JSON) 前 16 位十六进制。
|
|
5
|
+
* 给需要「配置变了就失效」的缓存键用(如模型列表)。密钥不参与摘要:不加盐的短摘要写进 Redis 键名后,
|
|
6
|
+
* 低熵密钥可被离线猜测、泄露的密钥可被确认;代价是轮换密钥不会让缓存失效(缓存内容本来就不该依赖密钥)。
|
|
7
|
+
* 没有部署配置 schema 时无从得知哪些是密钥,摘要不按配置区分。同一份配置在各副本上得到同一摘要。
|
|
8
|
+
* 函数自己按 schema 遮蔽,调用方不必(也不能忘记)先打码。
|
|
9
|
+
*/
|
|
10
|
+
export declare function deploymentConfigHash(schema: core.$ZodType | undefined, config: unknown): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { configFields, getPath, setPath } from "../config/fields.js";
|
|
3
|
+
/** 键排序后的 JSON(对象键顺序不影响结果);undefined 字段与 JSON.stringify 一样省略。 */
|
|
4
|
+
const canonicalJson = (v) => {
|
|
5
|
+
if (Array.isArray(v))
|
|
6
|
+
return `[${v.map((x) => (x === undefined ? "null" : canonicalJson(x))).join(",")}]`;
|
|
7
|
+
if (v !== null && typeof v === "object") {
|
|
8
|
+
const entries = Object.entries(v)
|
|
9
|
+
.filter(([, x]) => x !== undefined)
|
|
10
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
11
|
+
return `{${entries.map(([k, x]) => `${JSON.stringify(k)}:${canonicalJson(x)}`).join(",")}}`;
|
|
12
|
+
}
|
|
13
|
+
return JSON.stringify(v) ?? "null";
|
|
14
|
+
};
|
|
15
|
+
/** 遮蔽 secret 字段后参与摘要的固定占位值。 */
|
|
16
|
+
const SECRET_PLACEHOLDER = "<secret>";
|
|
17
|
+
/**
|
|
18
|
+
* 插件部署配置(校验后)中非密钥部分的摘要:先把 schema 里所有 secret 叶子(`configFields` 可达路径,
|
|
19
|
+
* 加载时 `assertSecretsReachable` 已保证密钥都在这些路径上)换成固定占位值,再取 sha256(键排序 JSON) 前 16 位十六进制。
|
|
20
|
+
* 给需要「配置变了就失效」的缓存键用(如模型列表)。密钥不参与摘要:不加盐的短摘要写进 Redis 键名后,
|
|
21
|
+
* 低熵密钥可被离线猜测、泄露的密钥可被确认;代价是轮换密钥不会让缓存失效(缓存内容本来就不该依赖密钥)。
|
|
22
|
+
* 没有部署配置 schema 时无从得知哪些是密钥,摘要不按配置区分。同一份配置在各副本上得到同一摘要。
|
|
23
|
+
* 函数自己按 schema 遮蔽,调用方不必(也不能忘记)先打码。
|
|
24
|
+
*/
|
|
25
|
+
export function deploymentConfigHash(schema, config) {
|
|
26
|
+
let redacted = null;
|
|
27
|
+
if (schema) {
|
|
28
|
+
redacted = structuredClone(config);
|
|
29
|
+
if (redacted !== null && typeof redacted === "object") {
|
|
30
|
+
for (const f of configFields(schema)) {
|
|
31
|
+
if (f.meta?.secret && getPath(redacted, f.path) !== undefined) {
|
|
32
|
+
setPath(redacted, f.path, SECRET_PLACEHOLDER);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return createHash("sha256").update(canonicalJson(redacted)).digest("hex").slice(0, 16);
|
|
38
|
+
}
|
|
@@ -6,19 +6,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { Module } from "@nestjs/common";
|
|
8
8
|
import { SuperAdminLockModule } from "../identity/super-admin.js";
|
|
9
|
+
import { ModelsModule } from "../models/models.module.js";
|
|
9
10
|
import { OrgLockModule } from "../orgs/org-lock.js";
|
|
10
11
|
import { StorageModule } from "../storage/storage.module.js";
|
|
11
12
|
import { EmbedTokensModule } from "../ui/embed-token.js";
|
|
12
13
|
import { ExtensionAdminOps } from "./admin-ops.js";
|
|
13
14
|
import { ExtensionAdmin } from "./admin-view.js";
|
|
14
|
-
import { ExtensionApprovals } from "./approvals.js";
|
|
15
|
-
import { ExtensionBlobs } from "./blobs.js";
|
|
16
15
|
import { BootstrapService } from "./bootstrap.js";
|
|
17
16
|
import { ExtensionDeploymentConfigService } from "./deployment-config.js";
|
|
18
17
|
import { ExtensionDisplayNames } from "./display-name.js";
|
|
19
|
-
import { EffectiveConfigs } from "./effective-config.js";
|
|
20
18
|
import { ExtensionHost } from "./host.js";
|
|
21
|
-
import { ExtensionInstalls } from "./installs.js";
|
|
22
19
|
import { ExtensionLifecycle } from "./lifecycle.js";
|
|
23
20
|
import { PluginApproveLock, PluginDeploymentConfigLock, PluginInstallLock } from "./locks.js";
|
|
24
21
|
import { OrgExtensionConfigs } from "./org-config.js";
|
|
@@ -26,6 +23,7 @@ import { OrgOverrides } from "./org-overrides.js";
|
|
|
26
23
|
import { ExtensionPointDefaults } from "./point-defaults.js";
|
|
27
24
|
import { ExtensionRegistryModule } from "./registry.js";
|
|
28
25
|
import { ReplicaStatusStore } from "./sources.js";
|
|
26
|
+
import { ExtensionStateModule } from "./state.module.js";
|
|
29
27
|
/**
|
|
30
28
|
* The extensions domain: the extension host (it builds each `ctx` from the injected kernel
|
|
31
29
|
* services), the package store and install records (the desired state the host converges to, one
|
|
@@ -43,14 +41,14 @@ ExtensionsModule = __decorate([
|
|
|
43
41
|
StorageModule,
|
|
44
42
|
// ctx.ui verifies embed tokens with the one provider the UI routes issue them with
|
|
45
43
|
EmbedTokensModule,
|
|
44
|
+
ExtensionStateModule,
|
|
45
|
+
// ctx.models: the embeddings broker (models domain, below the host through ExtensionStateModule)
|
|
46
|
+
ModelsModule,
|
|
46
47
|
],
|
|
47
48
|
providers: [
|
|
48
49
|
PluginInstallLock,
|
|
49
50
|
PluginApproveLock,
|
|
50
51
|
PluginDeploymentConfigLock,
|
|
51
|
-
ExtensionApprovals,
|
|
52
|
-
ExtensionBlobs,
|
|
53
|
-
ExtensionInstalls,
|
|
54
52
|
ReplicaStatusStore,
|
|
55
53
|
ExtensionHost,
|
|
56
54
|
ExtensionLifecycle,
|
|
@@ -58,7 +56,6 @@ ExtensionsModule = __decorate([
|
|
|
58
56
|
ExtensionAdminOps,
|
|
59
57
|
ExtensionDisplayNames,
|
|
60
58
|
ExtensionDeploymentConfigService,
|
|
61
|
-
EffectiveConfigs,
|
|
62
59
|
OrgExtensionConfigs,
|
|
63
60
|
OrgOverrides,
|
|
64
61
|
ExtensionPointDefaults,
|
|
@@ -66,11 +63,9 @@ ExtensionsModule = __decorate([
|
|
|
66
63
|
],
|
|
67
64
|
exports: [
|
|
68
65
|
ExtensionRegistryModule,
|
|
66
|
+
ExtensionStateModule,
|
|
69
67
|
PluginInstallLock,
|
|
70
68
|
PluginApproveLock,
|
|
71
|
-
ExtensionApprovals,
|
|
72
|
-
ExtensionBlobs,
|
|
73
|
-
ExtensionInstalls,
|
|
74
69
|
ReplicaStatusStore,
|
|
75
70
|
ExtensionHost,
|
|
76
71
|
ExtensionLifecycle,
|
|
@@ -78,7 +73,6 @@ ExtensionsModule = __decorate([
|
|
|
78
73
|
ExtensionAdminOps,
|
|
79
74
|
ExtensionDisplayNames,
|
|
80
75
|
ExtensionDeploymentConfigService,
|
|
81
|
-
EffectiveConfigs,
|
|
82
76
|
OrgExtensionConfigs,
|
|
83
77
|
OrgOverrides,
|
|
84
78
|
ExtensionPointDefaults,
|
|
@@ -10,9 +10,11 @@ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
|
10
10
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
11
11
|
import { LockService } from "../infra/lock/lock.service.js";
|
|
12
12
|
import type { KernelLogger } from "../log.js";
|
|
13
|
+
import { ModelEmbeddings } from "../models/embeddings.js";
|
|
13
14
|
import { EmbedTokens } from "../ui/embed-token.js";
|
|
14
15
|
import type { UiActionHandler, UiRender } from "../ui/registry.js";
|
|
15
16
|
import { ExtensionApprovals, type PendingExtension } from "./approvals.js";
|
|
17
|
+
import { deploymentConfigHash } from "./config-hash.js";
|
|
16
18
|
import { EffectiveConfigs } from "./effective-config.js";
|
|
17
19
|
import { HookRegistry } from "./hooks.js";
|
|
18
20
|
import type { InflightTracker } from "./inflight.js";
|
|
@@ -21,15 +23,7 @@ import { type ExtensionInstance } from "./instance.js";
|
|
|
21
23
|
import { type PermissionSet } from "./permission-set.js";
|
|
22
24
|
import { type ExtensionPointEntry, ExtensionRegistry } from "./registry.js";
|
|
23
25
|
import { type ExtensionSource, ReplicaStatusStore } from "./sources.js";
|
|
24
|
-
|
|
25
|
-
* 插件部署配置(校验后)中非密钥部分的摘要:先把 schema 里所有 secret 叶子(`configFields` 可达路径,
|
|
26
|
-
* 加载时 `assertSecretsReachable` 已保证密钥都在这些路径上)换成固定占位值,再取 sha256(键排序 JSON) 前 16 位十六进制。
|
|
27
|
-
* 给需要「配置变了就失效」的缓存键用(如模型列表)。密钥不参与摘要:不加盐的短摘要写进 Redis 键名后,
|
|
28
|
-
* 低熵密钥可被离线猜测、泄露的密钥可被确认;代价是轮换密钥不会让缓存失效(缓存内容本来就不该依赖密钥)。
|
|
29
|
-
* 没有部署配置 schema 时无从得知哪些是密钥,摘要不按配置区分。同一份配置在各副本上得到同一摘要。
|
|
30
|
-
* 函数自己按 schema 遮蔽,调用方不必(也不能忘记)先打码。
|
|
31
|
-
*/
|
|
32
|
-
export declare function deploymentConfigHash(schema: core.$ZodType | undefined, config: unknown): string;
|
|
26
|
+
export { deploymentConfigHash };
|
|
33
27
|
/** One item of {@link ExtensionHost.uiContributions}: a UI contribution from the manifest with its registered view / action (embeds have none). */
|
|
34
28
|
export interface LoadedUiContribution {
|
|
35
29
|
plugin: string;
|
|
@@ -255,7 +249,9 @@ export declare class ExtensionHost {
|
|
|
255
249
|
* Each hook call's effective config for its organization, and the providers excluded from an
|
|
256
250
|
* org's point resolution (layering spec A §6).
|
|
257
251
|
*/
|
|
258
|
-
effective: Pick<EffectiveConfigs, "get"
|
|
252
|
+
effective: Pick<EffectiveConfigs, "get">,
|
|
253
|
+
/** `ctx.models`: the embeddings broker (spec B §5.4). */
|
|
254
|
+
models: Pick<ModelEmbeddings, "embed" | "describe">);
|
|
259
255
|
/** The startup config's `extensionConfig` (file / Nacos merged with env). */
|
|
260
256
|
private get extensionConfig();
|
|
261
257
|
/** `extensionPermissions.autoApprove` (development and trials only; see `planApprovals`). */
|
package/dist/extensions/host.js
CHANGED
|
@@ -7,14 +7,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
10
|
import { readFile } from "node:fs/promises";
|
|
12
11
|
import { createRequire } from "node:module";
|
|
13
12
|
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
14
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
14
|
import { EXTENSION_POINT_CARDINALITY, parseExtensionPoint, SDK_VERSION, } from "@capybara-hq/plugin-sdk";
|
|
16
15
|
import { Inject, Injectable } from "@nestjs/common";
|
|
17
|
-
import { changedPaths, configFields,
|
|
16
|
+
import { changedPaths, configFields, secretValues, unreachableSecrets } from "../config/fields.js";
|
|
18
17
|
import { pluginSchemaName, queryEnabledPlugins, } from "../ctx/create-ctx.js";
|
|
19
18
|
import { kernelDb } from "../db/kernel-schema.js";
|
|
20
19
|
import { migrate } from "../db/migrator.js";
|
|
@@ -25,10 +24,12 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
25
24
|
import { LockService } from "../infra/lock/lock.service.js";
|
|
26
25
|
import { KERNEL_LOGGER, KEYRING, REDIS, REDIS_SUBSCRIBER } from "../infra/tokens.js";
|
|
27
26
|
import { KERNEL_APP_OPTIONS } from "../lifecycle/tokens.js";
|
|
27
|
+
import { ModelEmbeddings } from "../models/embeddings.js";
|
|
28
28
|
import { packagePath } from "../packages/tgz.js";
|
|
29
29
|
import { EmbedTokens } from "../ui/embed-token.js";
|
|
30
30
|
import { ExtensionApprovals, planApprovals, readSnapshots, withPreApproved, } from "./approvals.js";
|
|
31
31
|
import { ExtensionBlobIntegrityError } from "./blobs.js";
|
|
32
|
+
import { deploymentConfigHash } from "./config-hash.js";
|
|
32
33
|
import { toIssues } from "./config-issues.js";
|
|
33
34
|
import { configFormSchema, describeConfigUiProblems } from "./config-json-schema.js";
|
|
34
35
|
import { EffectiveConfigs } from "./effective-config.js";
|
|
@@ -42,42 +43,7 @@ import { ExtensionRegistry } from "./registry.js";
|
|
|
42
43
|
import { ExtensionResolveError, resolveExtensions } from "./resolve.js";
|
|
43
44
|
import { ReplicaStatusStore, } from "./sources.js";
|
|
44
45
|
import { PackageShaMismatchError, packageRootOf } from "./sync.js";
|
|
45
|
-
|
|
46
|
-
const canonicalJson = (v) => {
|
|
47
|
-
if (Array.isArray(v))
|
|
48
|
-
return `[${v.map((x) => (x === undefined ? "null" : canonicalJson(x))).join(",")}]`;
|
|
49
|
-
if (v !== null && typeof v === "object") {
|
|
50
|
-
const entries = Object.entries(v)
|
|
51
|
-
.filter(([, x]) => x !== undefined)
|
|
52
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
53
|
-
return `{${entries.map(([k, x]) => `${JSON.stringify(k)}:${canonicalJson(x)}`).join(",")}}`;
|
|
54
|
-
}
|
|
55
|
-
return JSON.stringify(v) ?? "null";
|
|
56
|
-
};
|
|
57
|
-
/** 遮蔽 secret 字段后参与摘要的固定占位值。 */
|
|
58
|
-
const SECRET_PLACEHOLDER = "<secret>";
|
|
59
|
-
/**
|
|
60
|
-
* 插件部署配置(校验后)中非密钥部分的摘要:先把 schema 里所有 secret 叶子(`configFields` 可达路径,
|
|
61
|
-
* 加载时 `assertSecretsReachable` 已保证密钥都在这些路径上)换成固定占位值,再取 sha256(键排序 JSON) 前 16 位十六进制。
|
|
62
|
-
* 给需要「配置变了就失效」的缓存键用(如模型列表)。密钥不参与摘要:不加盐的短摘要写进 Redis 键名后,
|
|
63
|
-
* 低熵密钥可被离线猜测、泄露的密钥可被确认;代价是轮换密钥不会让缓存失效(缓存内容本来就不该依赖密钥)。
|
|
64
|
-
* 没有部署配置 schema 时无从得知哪些是密钥,摘要不按配置区分。同一份配置在各副本上得到同一摘要。
|
|
65
|
-
* 函数自己按 schema 遮蔽,调用方不必(也不能忘记)先打码。
|
|
66
|
-
*/
|
|
67
|
-
export function deploymentConfigHash(schema, config) {
|
|
68
|
-
let redacted = null;
|
|
69
|
-
if (schema) {
|
|
70
|
-
redacted = structuredClone(config);
|
|
71
|
-
if (redacted !== null && typeof redacted === "object") {
|
|
72
|
-
for (const f of configFields(schema)) {
|
|
73
|
-
if (f.meta?.secret && getPath(redacted, f.path) !== undefined) {
|
|
74
|
-
setPath(redacted, f.path, SECRET_PLACEHOLDER);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return createHash("sha256").update(canonicalJson(redacted)).digest("hex").slice(0, 16);
|
|
80
|
-
}
|
|
46
|
+
export { deploymentConfigHash };
|
|
81
47
|
const CLOSED_DURING_LOAD = "plugin host closed during load";
|
|
82
48
|
/** 停机时等进行中的 unload / reload 的上限(之后直接关闭 setup 中的 ctx、撤销其余实例)。 */
|
|
83
49
|
const CLOSE_LIFECYCLE_WAIT_MS = 2_000;
|
|
@@ -415,7 +381,9 @@ let ExtensionHost = class ExtensionHost {
|
|
|
415
381
|
* Each hook call's effective config for its organization, and the providers excluded from an
|
|
416
382
|
* org's point resolution (layering spec A §6).
|
|
417
383
|
*/
|
|
418
|
-
effective
|
|
384
|
+
effective,
|
|
385
|
+
/** `ctx.models`: the embeddings broker (spec B §5.4). */
|
|
386
|
+
models) {
|
|
419
387
|
this.registry = registry;
|
|
420
388
|
this.installs = installs;
|
|
421
389
|
this.approvals = approvals;
|
|
@@ -424,6 +392,16 @@ let ExtensionHost = class ExtensionHost {
|
|
|
424
392
|
this.log = log;
|
|
425
393
|
this.effective = effective;
|
|
426
394
|
const c = config.get();
|
|
395
|
+
this.hooks = new HookRegistry({
|
|
396
|
+
instances: () => this.registry.all(),
|
|
397
|
+
current: (plugin) => this.registry.current(plugin),
|
|
398
|
+
expectedPolicy: (mount) => this.expectedPolicy(mount),
|
|
399
|
+
enabledPlugins: (orgId) => this.enabledPlugins(orgId),
|
|
400
|
+
effectiveConfig: (orgId, plugin) => effective.get(orgId, plugin),
|
|
401
|
+
locks,
|
|
402
|
+
config: () => ({ hooks: { timeoutMs: this.config.get().hooks.timeoutMs } }),
|
|
403
|
+
log,
|
|
404
|
+
});
|
|
427
405
|
this.deps = {
|
|
428
406
|
db: kdb.pool,
|
|
429
407
|
dbUrl: c.db.url,
|
|
@@ -433,20 +411,13 @@ let ExtensionHost = class ExtensionHost {
|
|
|
433
411
|
redisUrl: c.redis.url,
|
|
434
412
|
keyring,
|
|
435
413
|
embedTokens,
|
|
414
|
+
models,
|
|
415
|
+
// ctx.models.embed fires model.request through the same registry (spec B §5.4)
|
|
416
|
+
hooks: this.hooks,
|
|
436
417
|
locks,
|
|
437
418
|
runWorkers: c.roles.includes("worker"),
|
|
438
419
|
log,
|
|
439
420
|
};
|
|
440
|
-
this.hooks = new HookRegistry({
|
|
441
|
-
instances: () => this.registry.all(),
|
|
442
|
-
current: (plugin) => this.registry.current(plugin),
|
|
443
|
-
expectedPolicy: (mount) => this.expectedPolicy(mount),
|
|
444
|
-
enabledPlugins: (orgId) => this.enabledPlugins(orgId),
|
|
445
|
-
effectiveConfig: (orgId, plugin) => effective.get(orgId, plugin),
|
|
446
|
-
locks,
|
|
447
|
-
config: () => ({ hooks: { timeoutMs: this.config.get().hooks.timeoutMs } }),
|
|
448
|
-
log,
|
|
449
|
-
});
|
|
450
421
|
this.registry.onChange(() => {
|
|
451
422
|
this.installedCache = undefined;
|
|
452
423
|
});
|
|
@@ -2201,6 +2172,7 @@ ExtensionHost = __decorate([
|
|
|
2201
2172
|
__param(10, Inject(LockService)),
|
|
2202
2173
|
__param(11, Inject(KERNEL_LOGGER)),
|
|
2203
2174
|
__param(12, Inject(KERNEL_APP_OPTIONS)),
|
|
2204
|
-
__param(13, Inject(EffectiveConfigs))
|
|
2175
|
+
__param(13, Inject(EffectiveConfigs)),
|
|
2176
|
+
__param(14, Inject(ModelEmbeddings))
|
|
2205
2177
|
], ExtensionHost);
|
|
2206
2178
|
export { ExtensionHost };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The extension state the model domain reads below the host: the install records with their
|
|
3
|
+
* packages and approvals, and each extension's effective config per org. Apart from
|
|
4
|
+
* `ExtensionsModule` so the model provider registry (and the `ctx.models` broker the host injects)
|
|
5
|
+
* can depend on it without a module cycle (writing-kernel-api: a cycle is cut by moving the shared
|
|
6
|
+
* part into its own provider both sides inject). `ExtensionsModule` re-exports it.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ExtensionStateModule {
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Module } from "@nestjs/common";
|
|
8
|
+
import { SuperAdminLockModule } from "../identity/super-admin.js";
|
|
9
|
+
import { OrgLockModule } from "../orgs/org-lock.js";
|
|
10
|
+
import { StorageModule } from "../storage/storage.module.js";
|
|
11
|
+
import { ExtensionApprovals } from "./approvals.js";
|
|
12
|
+
import { ExtensionBlobs } from "./blobs.js";
|
|
13
|
+
import { EffectiveConfigs } from "./effective-config.js";
|
|
14
|
+
import { ExtensionInstalls } from "./installs.js";
|
|
15
|
+
import { ExtensionRegistryModule } from "./registry.js";
|
|
16
|
+
/**
|
|
17
|
+
* The extension state the model domain reads below the host: the install records with their
|
|
18
|
+
* packages and approvals, and each extension's effective config per org. Apart from
|
|
19
|
+
* `ExtensionsModule` so the model provider registry (and the `ctx.models` broker the host injects)
|
|
20
|
+
* can depend on it without a module cycle (writing-kernel-api: a cycle is cut by moving the shared
|
|
21
|
+
* part into its own provider both sides inject). `ExtensionsModule` re-exports it.
|
|
22
|
+
*/
|
|
23
|
+
let ExtensionStateModule = class ExtensionStateModule {
|
|
24
|
+
};
|
|
25
|
+
ExtensionStateModule = __decorate([
|
|
26
|
+
Module({
|
|
27
|
+
imports: [ExtensionRegistryModule, SuperAdminLockModule, OrgLockModule, StorageModule],
|
|
28
|
+
providers: [ExtensionApprovals, ExtensionBlobs, ExtensionInstalls, EffectiveConfigs],
|
|
29
|
+
exports: [
|
|
30
|
+
ExtensionRegistryModule,
|
|
31
|
+
ExtensionApprovals,
|
|
32
|
+
ExtensionBlobs,
|
|
33
|
+
ExtensionInstalls,
|
|
34
|
+
EffectiveConfigs,
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
], ExtensionStateModule);
|
|
38
|
+
export { ExtensionStateModule };
|
package/dist/http/api.module.js
CHANGED
|
@@ -47,6 +47,7 @@ import { MeController } from "../identity/me.controller.js";
|
|
|
47
47
|
import { BindController, MeIdentitiesController, SsoController, } from "../identity/sso.controller.js";
|
|
48
48
|
import { InboxModule } from "../inbox/inbox.module.js";
|
|
49
49
|
import { PlatformInboxController } from "../inbox/platform-inbox.controller.js";
|
|
50
|
+
import { ModelEnablementController } from "../models/enablement.controller.js";
|
|
50
51
|
import { ModelsModule } from "../models/models.module.js";
|
|
51
52
|
import { ModelPurposesController } from "../models/purposes.controller.js";
|
|
52
53
|
import { DirectorySyncController } from "../orgs/directory-sync.controller.js";
|
|
@@ -92,6 +93,7 @@ const CONTROLLERS = [
|
|
|
92
93
|
AgentSaveAsTemplateController,
|
|
93
94
|
PendingConnectorsController,
|
|
94
95
|
ModelsController,
|
|
96
|
+
ModelEnablementController,
|
|
95
97
|
ReadinessController,
|
|
96
98
|
ConversationsController,
|
|
97
99
|
BackgroundController,
|
|
@@ -45,7 +45,10 @@ export declare function createPluginRouter(routes: Route[]): PluginRouter;
|
|
|
45
45
|
* - `req.signal` aborts only when the client disconnects before the response is written (not on
|
|
46
46
|
* normal completion); long-lived handlers use it to unsubscribe;
|
|
47
47
|
* - a throwing handler is logged (path redacted with `redactPath`) and returns 500 `internal` (the
|
|
48
|
-
* error message is not echoed).
|
|
48
|
+
* error message is not echoed), except a `model.hook_denied` `ApiError` that escaped from
|
|
49
|
+
* `ctx.models.embed` (an organization policy hook denied the call, spec B §5.4): a deliberate
|
|
50
|
+
* outcome, not a failure, so it answers 403 `model.hook_denied` with a generic message (the
|
|
51
|
+
* routes are unauthenticated: the policy extension's name and reason are not echoed).
|
|
49
52
|
* - Fastify's own errors (a body over 1 MiB and so on) are mapped to the error envelope by the
|
|
50
53
|
* scope's error handler: 4xx by the common error code table (413 `payload_too_large`, 415
|
|
51
54
|
* `unsupported_media_type` and so on), 5xx are logged and return `internal`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Readable } from "node:stream";
|
|
2
2
|
import FindMyWay from "find-my-way";
|
|
3
|
-
import { errorBody } from "../errors.js";
|
|
3
|
+
import { ApiError, ERROR_CODES, errorBody } from "../errors.js";
|
|
4
4
|
import { ACCESS_COOKIE, REFRESH_COOKIE } from "../identity/cookies.js";
|
|
5
5
|
import { codeForStatus, redactPath } from "./error.filter.js";
|
|
6
6
|
/** 插件路由都在此前缀下(ctx.http.route 已加 `/plugins/<name>` 前缀)。 */
|
|
@@ -47,7 +47,10 @@ export function createPluginRouter(routes) {
|
|
|
47
47
|
* - `req.signal` aborts only when the client disconnects before the response is written (not on
|
|
48
48
|
* normal completion); long-lived handlers use it to unsubscribe;
|
|
49
49
|
* - a throwing handler is logged (path redacted with `redactPath`) and returns 500 `internal` (the
|
|
50
|
-
* error message is not echoed).
|
|
50
|
+
* error message is not echoed), except a `model.hook_denied` `ApiError` that escaped from
|
|
51
|
+
* `ctx.models.embed` (an organization policy hook denied the call, spec B §5.4): a deliberate
|
|
52
|
+
* outcome, not a failure, so it answers 403 `model.hook_denied` with a generic message (the
|
|
53
|
+
* routes are unauthenticated: the policy extension's name and reason are not echoed).
|
|
51
54
|
* - Fastify's own errors (a body over 1 MiB and so on) are mapped to the error envelope by the
|
|
52
55
|
* scope's error handler: 4xx by the common error code table (413 `payload_too_large`, 415
|
|
53
56
|
* `unsupported_media_type` and so on), 5xx are logged and return `internal`.
|
|
@@ -108,6 +111,15 @@ export async function registerPluginDispatch(fastify, getTable, log) {
|
|
|
108
111
|
res = await pluginHandler(webReq);
|
|
109
112
|
}
|
|
110
113
|
catch (err) {
|
|
114
|
+
if (err instanceof ApiError && err.code === "model.hook_denied") {
|
|
115
|
+
log.warn("plugin handler refused: a policy hook denied its model call", {
|
|
116
|
+
method: req.method,
|
|
117
|
+
path: redactPath(path),
|
|
118
|
+
});
|
|
119
|
+
return reply
|
|
120
|
+
.code(ERROR_CODES["model.hook_denied"])
|
|
121
|
+
.send(errorBody(err.code, "An organization policy denied this model call"));
|
|
122
|
+
}
|
|
111
123
|
log.error("plugin handler failed", { method: req.method, path: redactPath(path), err });
|
|
112
124
|
return reply.code(500).send(errorBody("internal", "Internal error"));
|
|
113
125
|
}
|
|
@@ -28,6 +28,9 @@ const HOT_APPLIERS = {
|
|
|
28
28
|
"auth.trustedIdentitySources": () => { },
|
|
29
29
|
// The hook registry reads the time limit on every run.
|
|
30
30
|
"hooks.timeoutMs": () => { },
|
|
31
|
+
// The provider registry reads the list time limit at every listModels call.
|
|
32
|
+
"models.listTimeoutMs": () => { },
|
|
33
|
+
"models.embedTimeoutMs": () => { },
|
|
31
34
|
// SandboxService reads the shared-lease cap at every acquisition.
|
|
32
35
|
"sandbox.maxConcurrentCommands": () => { },
|
|
33
36
|
// The environments service reads the personal limit at every create.
|
|
@@ -95,6 +95,14 @@ export interface KernelEventMap {
|
|
|
95
95
|
"background_task.finished": {
|
|
96
96
|
conversationId: string;
|
|
97
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* An extension's `ctx.models.embed` call wrote a `model_usage` row (after the commit, spec B
|
|
100
|
+
* §5.4). The usage reporter enqueues its `usage.report` delivery; a lost event is re-derived by
|
|
101
|
+
* the usage sweep (rows whose `reported_at` is still null).
|
|
102
|
+
*/
|
|
103
|
+
"model_usage.recorded": {
|
|
104
|
+
id: string;
|
|
105
|
+
};
|
|
98
106
|
}
|
|
99
107
|
type Handler<K extends keyof KernelEventMap> = (payload: KernelEventMap[K]) => Promise<void> | void;
|
|
100
108
|
/** Typed in-process event emitter; handlers run in order, a failing one is logged and skipped. */
|