@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.
Files changed (63) hide show
  1. package/dist/admin/admin.controller.d.ts +13 -2
  2. package/dist/admin/admin.controller.js +29 -4
  3. package/dist/agents/agents.service.d.ts +3 -1
  4. package/dist/agents/agents.service.js +8 -6
  5. package/dist/agents/from-template.service.js +3 -3
  6. package/dist/agents/models.controller.d.ts +34 -5
  7. package/dist/agents/models.controller.js +50 -6
  8. package/dist/agents/readiness.controller.d.ts +5 -5
  9. package/dist/agents/readiness.controller.js +6 -6
  10. package/dist/catalog/marketplaces.service.d.ts +1 -1
  11. package/dist/config/schema.d.ts +4 -0
  12. package/dist/config/schema.js +24 -0
  13. package/dist/conversations/conversations.service.d.ts +4 -3
  14. package/dist/conversations/conversations.service.js +9 -8
  15. package/dist/ctx/create-ctx.d.ts +15 -1
  16. package/dist/ctx/create-ctx.js +23 -2
  17. package/dist/db/kernel-schema.d.ts +24 -0
  18. package/dist/errors.d.ts +3 -0
  19. package/dist/errors.js +3 -0
  20. package/dist/evals/run-replay.js +4 -2
  21. package/dist/evals/runs.service.d.ts +2 -1
  22. package/dist/evals/runs.service.js +13 -12
  23. package/dist/evals/step-replay.d.ts +2 -1
  24. package/dist/evals/step-replay.js +3 -2
  25. package/dist/extensions/config-hash.d.ts +10 -0
  26. package/dist/extensions/config-hash.js +38 -0
  27. package/dist/extensions/extensions.module.js +6 -12
  28. package/dist/extensions/host.d.ts +6 -10
  29. package/dist/extensions/host.js +22 -50
  30. package/dist/extensions/state.module.d.ts +9 -0
  31. package/dist/extensions/state.module.js +38 -0
  32. package/dist/http/api.module.js +2 -0
  33. package/dist/http/plugin-dispatch.d.ts +4 -1
  34. package/dist/http/plugin-dispatch.js +14 -2
  35. package/dist/infra/config/kernel-config.service.js +3 -0
  36. package/dist/infra/events/kernel-events.d.ts +8 -0
  37. package/dist/models/embeddings.d.ts +82 -0
  38. package/dist/models/embeddings.js +326 -0
  39. package/dist/models/enablement.controller.d.ts +40 -0
  40. package/dist/models/enablement.controller.js +74 -0
  41. package/dist/models/enablement.d.ts +99 -0
  42. package/dist/models/enablement.js +263 -0
  43. package/dist/models/models.module.d.ts +5 -1
  44. package/dist/models/models.module.js +16 -4
  45. package/dist/models/purposes.d.ts +18 -4
  46. package/dist/models/purposes.js +46 -9
  47. package/dist/platform/exclusions.js +18 -0
  48. package/dist/runtime/hook-calls.d.ts +8 -1
  49. package/dist/runtime/hook-calls.js +40 -9
  50. package/dist/runtime/model-call.d.ts +8 -3
  51. package/dist/runtime/model-call.js +28 -5
  52. package/dist/runtime/providers.d.ts +146 -32
  53. package/dist/runtime/providers.js +321 -67
  54. package/dist/runtime/run-worker.d.ts +1 -1
  55. package/dist/runtime/run-worker.js +4 -4
  56. package/dist/runtime/subagents/definition.js +3 -3
  57. package/dist/runtime/usage-report-jobs.d.ts +88 -0
  58. package/dist/runtime/usage-report-jobs.js +158 -0
  59. package/dist/runtime/usage-report.d.ts +33 -38
  60. package/dist/runtime/usage-report.js +152 -84
  61. package/dist/tools/pipeline.js +6 -0
  62. package/package.json +3 -3
  63. 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 层的错误过滤器转成错误信封。 */
@@ -435,8 +435,10 @@ export async function replayRun(d, s, candidate, evalRunId, signal) {
435
435
  return metered;
436
436
  };
437
437
  const registry = {
438
- resolve: async (ref) => {
439
- const r = await d.registry.resolve(ref);
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`). */
@@ -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, getPath, secretValues, setPath, unreachableSecrets, } from "../config/fields.js";
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
- /** 键排序后的 JSON(对象键顺序不影响结果);undefined 字段与 JSON.stringify 一样省略。 */
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 };
@@ -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. */