@capybara-hq/kernel 0.1.0-rc.11 → 0.1.0-rc.12

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 (37) hide show
  1. package/dist/agents/agents.schemas.d.ts +3 -0
  2. package/dist/agents/agents.service.js +11 -1
  3. package/dist/agents/definition.d.ts +1 -0
  4. package/dist/agents/definition.js +7 -3
  5. package/dist/agents/org-templates.js +4 -0
  6. package/dist/agents/pending-connectors.service.js +1 -1
  7. package/dist/catalog/component-scope.controller.d.ts +18 -6
  8. package/dist/catalog/component-scope.controller.js +36 -13
  9. package/dist/catalog/component-scope.service.d.ts +11 -0
  10. package/dist/catalog/component-scope.service.js +4 -0
  11. package/dist/catalog/component-state.d.ts +8 -0
  12. package/dist/catalog/component-state.js +19 -11
  13. package/dist/catalog/plugin-import.js +4 -1
  14. package/dist/conversations/conversations.service.d.ts +9 -0
  15. package/dist/conversations/conversations.service.js +9 -2
  16. package/dist/conversations/events.d.ts +1 -0
  17. package/dist/db/kernel-schema.d.ts +13 -0
  18. package/dist/errors.d.ts +2 -0
  19. package/dist/errors.js +2 -0
  20. package/dist/platform/exclusions.js +10 -0
  21. package/dist/platform/ops/agents.ops.js +19 -6
  22. package/dist/runtime/subagents/definition.d.ts +3 -2
  23. package/dist/runtime/subagents/definition.js +2 -1
  24. package/dist/tools/connector-account-policies.d.ts +51 -0
  25. package/dist/tools/connector-account-policies.js +165 -0
  26. package/dist/tools/connectors.controller.d.ts +35 -4
  27. package/dist/tools/connectors.controller.js +66 -7
  28. package/dist/tools/connectors.schemas.d.ts +11 -0
  29. package/dist/tools/connectors.schemas.js +11 -0
  30. package/dist/tools/connectors.service.d.ts +2 -0
  31. package/dist/tools/connectors.service.js +1 -0
  32. package/dist/tools/pipeline.d.ts +1 -1
  33. package/dist/tools/policy.d.ts +23 -1
  34. package/dist/tools/policy.js +99 -24
  35. package/dist/tools/tools.module.js +3 -0
  36. package/package.json +1 -1
  37. package/src/migrations/0104_connector_tool_account_policies.sql +16 -0
@@ -33,6 +33,7 @@ export declare const AgentCreateDefinition: z.ZodObject<{
33
33
  tools: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
34
34
  }, z.core.$strict>], "kind">>>;
35
35
  toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
36
+ allow: "allow";
36
37
  ask: "ask";
37
38
  deny: "deny";
38
39
  }>>>;
@@ -116,6 +117,7 @@ export declare const AgentCreateBody: z.ZodObject<{
116
117
  tools: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
117
118
  }, z.core.$strict>], "kind">>>;
118
119
  toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
120
+ allow: "allow";
119
121
  ask: "ask";
120
122
  deny: "deny";
121
123
  }>>>;
@@ -231,6 +233,7 @@ export declare const AgentUpdateBody: z.ZodObject<{
231
233
  tools: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodArray<z.ZodString>]>;
232
234
  }, z.core.$strict>], "kind">>>;
233
235
  toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
236
+ allow: "allow";
234
237
  ask: "ask";
235
238
  deny: "deny";
236
239
  }>>>;
@@ -8,6 +8,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
10
  import { randomUUID } from "node:crypto";
11
+ import { parseToolKey } from "@capybara-hq/plugin-sdk";
11
12
  import { Inject, Injectable } from "@nestjs/common";
12
13
  import { sql } from "kysely";
13
14
  import { ApiError } from "../errors.js";
@@ -310,7 +311,16 @@ let AgentsService = class AgentsService {
310
311
  message: "subagentTools is set only on a sub-agent's definition at dispatch",
311
312
  });
312
313
  }
313
- for (const key of Object.keys(def.toolPolicies)) {
314
+ for (const [key, policy] of Object.entries(def.toolPolicies)) {
315
+ // `allow` counts only for a connector tool (decision 2026-11-09): elsewhere a policy only
316
+ // tightens, so an `allow` there is refused rather than kept as a no-op
317
+ if (policy === "allow" && parseToolKey(key)?.kind !== "connector") {
318
+ details.push({
319
+ path: `toolPolicies.${key}`,
320
+ message: `"allow" applies only to a connector's tools; "${key}" can only be tightened (ask or deny)`,
321
+ });
322
+ continue;
323
+ }
314
324
  if (!isToolPolicyKey(key, keys)) {
315
325
  // an rc key (tool naming spec §8.3): saving writes current keys only, so name them
316
326
  const renamed = legacyKeySaveText(key, keys);
@@ -196,6 +196,7 @@ export declare const AgentDefinitionSchema: z.ZodObject<{
196
196
  deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
197
197
  }, z.core.$strict>>;
198
198
  toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
199
+ allow: "allow";
199
200
  ask: "ask";
200
201
  deny: "deny";
201
202
  }>>>;
@@ -207,10 +207,14 @@ export const AgentDefinitionSchema = z.object({
207
207
  */
208
208
  subagentTools: SubagentToolsSchema.optional(),
209
209
  /**
210
- * Tightened policies by tool key (`ask` / `deny` only, never `allow`); keys are tools the agent
211
- * could have (on or turned off). Older definitions without it read as empty.
210
+ * Policies by tool key; keys are tools the agent could have (on or turned off). `ask` / `deny`
211
+ * tighten any tool. `allow` is accepted only on a connector tool key and counts only for a
212
+ * personal-credential connector's tool (decision 2026-11-09): it overrides the org default and
213
+ * the member's own 询问, never the org's or the member's 禁止 (`tools/policy.ts`
214
+ * `connectorPolicy`); on any other tool it changes nothing. Older definitions without it read as
215
+ * empty.
212
216
  */
213
- toolPolicies: z.record(z.string(), z.enum(["ask", "deny"])).default({}),
217
+ toolPolicies: z.record(z.string(), z.enum(["allow", "ask", "deny"])).default({}),
214
218
  /**
215
219
  * 技能(M5 T13):`orgSkills` 是引用的组织技能 id(保存时校验属于本组织且未删除,不可重复)。
216
220
  * agent 自己的技能(`scope=agent`)自动对它生效,不进定义。可省略(较早保存的定义也没有),按空处理;
@@ -200,6 +200,10 @@ extensionTools = []) {
200
200
  const toolPolicies = {};
201
201
  const connectorPolicies = new Map();
202
202
  for (const [k, v] of Object.entries(def.toolPolicies ?? {})) {
203
+ // a template only tightens: an agent's `allow` (a connector tool's, decision 2026-11-09)
204
+ // is not carried into it
205
+ if (v === "allow")
206
+ continue;
203
207
  const parsed = parseToolKey(k);
204
208
  if (parsed !== null && parsed.kind !== "connector" && TOOL_KEY_RE.test(k)) {
205
209
  toolPolicies[k] = v;
@@ -134,7 +134,7 @@ let PendingConnectorsService = class PendingConnectorsService {
134
134
  }
135
135
  const k = connectorToolKey(c.slug, n);
136
136
  const prev = toolPolicies[k];
137
- if (prev === undefined || RANK[p] > RANK[prev])
137
+ if (prev === undefined || prev === "allow" || RANK[p] > RANK[prev])
138
138
  toolPolicies[k] = p;
139
139
  }
140
140
  // tools are on by default (tools default-on spec §3): binding an entry that names some tools
@@ -1,4 +1,5 @@
1
1
  import type { z } from "zod";
2
+ import { PluginIcons } from "../extensions/plugin-icons.js";
2
3
  import { type Principal } from "../identity/principal.js";
3
4
  import { type OrgContext } from "../orgs/org.guard.js";
4
5
  import { ComponentParams, ComponentDisabledSchema as DisabledBody, ComponentIdSchema as IdParams, ComponentListQuery as ListQuery, OrgComponentListQuery, ComponentScopePreviewSchema as PreviewBody, ComponentScopeSchema as ScopeBody, ComponentShownSchema as ShownBody, ComponentSwitchBody as SwitchBody, ComponentTemplateVersionSchema as TemplateVersionBody } from "./component-scope.schemas.js";
@@ -13,7 +14,8 @@ import { PlatformComponents, type UploadedComponentVersion } from "./platform-co
13
14
  export declare class AdminComponentsController {
14
15
  private readonly scope;
15
16
  private readonly components;
16
- constructor(scope: ComponentScopeService, components: PlatformComponents);
17
+ private readonly icons;
18
+ constructor(scope: ComponentScopeService, components: PlatformComponents, icons: PluginIcons);
17
19
  /**
18
20
  * Updates a platform skill on its own (spec §7.2): the skill package (zip) as the raw body
19
21
  * (`application/octet-stream`, at most `packages.maxBytes`). The package keeps the skill's name
@@ -26,9 +28,14 @@ export declare class AdminComponentsController {
26
28
  }): Promise<UploadedComponentVersion>;
27
29
  /** Updates a platform agent template on its own: `{ content }` is the template markdown file. */
28
30
  uploadAgentTemplateVersion(principal: Principal, p: z.infer<typeof IdParams>, body: z.infer<typeof TemplateVersionBody>): Promise<UploadedComponentVersion>;
29
- /** `GET ?kind=`: every platform component of the kind (plugin, owner, scope, version, orgs on). */
31
+ /**
32
+ * `GET ?kind=`: every platform component of the kind (plugin, owner, scope, version, orgs on),
33
+ * each with its display name and its plugin's icon (`icon`, the admin route; null without one).
34
+ */
30
35
  list(q: z.infer<typeof ListQuery>): Promise<{
31
- components: ComponentView[];
36
+ components: (ComponentView & {
37
+ icon: string | null;
38
+ })[];
32
39
  }>;
33
40
  /**
34
41
  * A component's versions, newest first: `{ versions: [{ version, catalogVersion, updatedOnOwn,
@@ -65,14 +72,19 @@ export declare class AdminComponentsController {
65
72
  */
66
73
  export declare class OrgComponentsController {
67
74
  private readonly scope;
68
- constructor(scope: ComponentScopeService);
75
+ private readonly icons;
76
+ constructor(scope: ComponentScopeService, icons: PluginIcons);
69
77
  /**
70
78
  * The platform components offered to the org (`?kind=`, `?itemId=`: only those; Task 10): each
71
79
  * with its state in the org (`on`, `locked` for `all`, the org's `switch`) and whether an org
72
- * admin may switch it. Members read it (the workspace pages are read-only for them).
80
+ * admin may switch it. Members read it (the workspace pages are read-only for them). Each has
81
+ * its display name and `icon`, its plugin's icon on the org's catalog route (null without one,
82
+ * or when the org's catalog does not show the plugin to the reader: the share gate holds).
73
83
  */
74
84
  list(org: OrgContext, q: z.infer<typeof OrgComponentListQuery>): Promise<{
75
- components: OrgOfferedComponentView[];
85
+ components: (OrgOfferedComponentView & {
86
+ icon: string | null;
87
+ })[];
76
88
  }>;
77
89
  setSwitch(principal: Principal, org: OrgContext, p: z.infer<typeof ComponentParams>, body: z.infer<typeof SwitchBody>): Promise<{
78
90
  component: OrgComponentView;
@@ -9,6 +9,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
9
9
  };
10
10
  import { Controller, Get, HttpCode, Inject, Post, Put, Req } from "@nestjs/common";
11
11
  import { ApiError } from "../errors.js";
12
+ import { orgIconScope, PluginIcons } from "../extensions/plugin-icons.js";
12
13
  import { CurrentPrincipal, patTokenId, requireUser, SuperAdmin, } from "../identity/principal.js";
13
14
  import { createZodDto } from "../infra/validation/create-zod-dto.js";
14
15
  import { ZBody, ZParam, ZQuery } from "../infra/validation/zod-params.js";
@@ -36,6 +37,16 @@ class ComponentShownBody extends createZodDto(ShownBody, "ComponentShownBody") {
36
37
  }
37
38
  class ComponentSwitchBody extends createZodDto(SwitchBody, "ComponentSwitchBody") {
38
39
  }
40
+ /**
41
+ * Each component with `icon`: its plugin's icon URL on the reader's route (null without one, for
42
+ * a component without a plugin, or when the reader may not see the plugin; maintainer decision
43
+ * 2026-11-09: a connector template without an icon of its own shows its plugin's).
44
+ */
45
+ async function withIcons(icons, rows, scope) {
46
+ const ids = rows.flatMap((r) => (r.itemId === null ? [] : [r.itemId]));
47
+ const urls = await icons.ofItems(ids, scope);
48
+ return rows.map((r) => ({ ...r, icon: r.itemId === null ? null : (urls.get(r.itemId) ?? null) }));
49
+ }
39
50
  const actorOpts = (actor) => {
40
51
  const tokenId = patTokenId(actor);
41
52
  return tokenId !== undefined ? { actorTokenId: tokenId } : {};
@@ -49,9 +60,11 @@ const actorOpts = (actor) => {
49
60
  let AdminComponentsController = class AdminComponentsController {
50
61
  scope;
51
62
  components;
52
- constructor(scope, components) {
63
+ icons;
64
+ constructor(scope, components, icons) {
53
65
  this.scope = scope;
54
66
  this.components = components;
67
+ this.icons = icons;
55
68
  }
56
69
  /**
57
70
  * Updates a platform skill on its own (spec §7.2): the skill package (zip) as the raw body
@@ -73,9 +86,14 @@ let AdminComponentsController = class AdminComponentsController {
73
86
  const actor = requireUser(principal);
74
87
  return this.components.uploadAgentTemplateVersion({ accountId: actor.account.id, ...actorOpts(actor) }, p.id, body.content);
75
88
  }
76
- /** `GET ?kind=`: every platform component of the kind (plugin, owner, scope, version, orgs on). */
89
+ /**
90
+ * `GET ?kind=`: every platform component of the kind (plugin, owner, scope, version, orgs on),
91
+ * each with its display name and its plugin's icon (`icon`, the admin route; null without one).
92
+ */
77
93
  async list(q) {
78
- return { components: await this.scope.list(q.kind) };
94
+ return {
95
+ components: await withIcons(this.icons, await this.scope.list(q.kind), { kind: "admin" }),
96
+ };
79
97
  }
80
98
  /**
81
99
  * A component's versions, newest first: `{ versions: [{ version, catalogVersion, updatedOnOwn,
@@ -178,7 +196,8 @@ AdminComponentsController = __decorate([
178
196
  Controller("api/v1/admin/components"),
179
197
  SuperAdmin(),
180
198
  __param(0, Inject(ComponentScopeService)),
181
- __param(1, Inject(PlatformComponents))
199
+ __param(1, Inject(PlatformComponents)),
200
+ __param(2, Inject(PluginIcons))
182
201
  ], AdminComponentsController);
183
202
  export { AdminComponentsController };
184
203
  /**
@@ -189,21 +208,24 @@ export { AdminComponentsController };
189
208
  */
190
209
  let OrgComponentsController = class OrgComponentsController {
191
210
  scope;
192
- constructor(scope) {
211
+ icons;
212
+ constructor(scope, icons) {
193
213
  this.scope = scope;
214
+ this.icons = icons;
194
215
  }
195
216
  /**
196
217
  * The platform components offered to the org (`?kind=`, `?itemId=`: only those; Task 10): each
197
218
  * with its state in the org (`on`, `locked` for `all`, the org's `switch`) and whether an org
198
- * admin may switch it. Members read it (the workspace pages are read-only for them).
219
+ * admin may switch it. Members read it (the workspace pages are read-only for them). Each has
220
+ * its display name and `icon`, its plugin's icon on the org's catalog route (null without one,
221
+ * or when the org's catalog does not show the plugin to the reader: the share gate holds).
199
222
  */
200
223
  async list(org, q) {
201
- return {
202
- components: await this.scope.orgList(org.orgId, {
203
- ...(q.kind !== undefined && { kind: q.kind }),
204
- ...(q.itemId !== undefined && { itemId: q.itemId }),
205
- }),
206
- };
224
+ const rows = await this.scope.orgList(org.orgId, {
225
+ ...(q.kind !== undefined && { kind: q.kind }),
226
+ ...(q.itemId !== undefined && { itemId: q.itemId }),
227
+ });
228
+ return { components: await withIcons(this.icons, rows, orgIconScope(org)) };
207
229
  }
208
230
  async setSwitch(principal, org, p, body) {
209
231
  const actor = requireUser(principal);
@@ -229,6 +251,7 @@ __decorate([
229
251
  ], OrgComponentsController.prototype, "setSwitch", null);
230
252
  OrgComponentsController = __decorate([
231
253
  Controller("api/v1/orgs/:orgId/components"),
232
- __param(0, Inject(ComponentScopeService))
254
+ __param(0, Inject(ComponentScopeService)),
255
+ __param(1, Inject(PluginIcons))
233
256
  ], OrgComponentsController);
234
257
  export { OrgComponentsController };
@@ -68,7 +68,13 @@ export type ComponentTarget = {
68
68
  export interface ComponentView {
69
69
  kind: ComponentKind;
70
70
  id: string;
71
+ /** Skill or template name; a connector template's id (the file's base name). */
71
72
  name: string;
73
+ /** A connector template's localized display name (null: show `name`). */
74
+ displayName: {
75
+ zh: string;
76
+ en: string;
77
+ } | null;
72
78
  description: string | null;
73
79
  /** The plugin it came from (null: none). */
74
80
  plugin: string | null;
@@ -138,6 +144,11 @@ export interface OrgComponentView {
138
144
  * show with their switches. Never a component the org cannot see.
139
145
  */
140
146
  export interface OrgOfferedComponentView extends OrgComponentView {
147
+ /** A connector template's localized display name (null: show `name`). */
148
+ displayName: {
149
+ zh: string;
150
+ en: string;
151
+ } | null;
141
152
  description: string | null;
142
153
  itemId: string | null;
143
154
  version: string;
@@ -204,6 +204,7 @@ let ComponentScopeService = class ComponentScopeService {
204
204
  kind,
205
205
  id: c.id,
206
206
  name: c.name,
207
+ displayName: c.displayName,
207
208
  description: c.description,
208
209
  plugin: c.plugin,
209
210
  itemId: c.itemId,
@@ -258,6 +259,7 @@ let ComponentScopeService = class ComponentScopeService {
258
259
  kind,
259
260
  id: c.id,
260
261
  name: c.name,
262
+ displayName: c.displayName,
261
263
  description: c.description,
262
264
  plugin: c.plugin,
263
265
  itemId: c.itemId,
@@ -360,6 +362,7 @@ let ComponentScopeService = class ComponentScopeService {
360
362
  kind,
361
363
  id: r.id,
362
364
  name: r.name,
365
+ displayName: null,
363
366
  description: r.description,
364
367
  plugin: r.plugin,
365
368
  itemId: r.catalog_item_id,
@@ -1084,6 +1087,7 @@ const virtualOf = (o) => ({
1084
1087
  plugin: o.plugin,
1085
1088
  version: String(o.version),
1086
1089
  description: null,
1090
+ displayName: null,
1087
1091
  itemId: null,
1088
1092
  scopeMode: "own",
1089
1093
  scope: "named",
@@ -68,6 +68,14 @@ export interface ComponentRow extends ComponentScopeFacts {
68
68
  /** The current version (a connector template: the catalog version that last changed it). */
69
69
  version: string;
70
70
  description: string | null;
71
+ /**
72
+ * A connector template's localized display name (its file's `displayName`); null for skills and
73
+ * agent templates, whose name is what they are shown by.
74
+ */
75
+ displayName: {
76
+ zh: string;
77
+ en: string;
78
+ } | null;
71
79
  shownToOthers: boolean;
72
80
  /** The owning org (null: the platform; ruling P3-1). */
73
81
  ownerOrgId: string | null;
@@ -63,6 +63,11 @@ export function componentOffered(c, p, listed) {
63
63
  export function componentLocked(_c, p) {
64
64
  return p?.builtin === true;
65
65
  }
66
+ /** A `{ zh, en }` display name from stored JSON (a template file's), else null. */
67
+ function localizedName(v) {
68
+ const n = v;
69
+ return typeof n?.zh === "string" && typeof n.en === "string" ? { zh: n.zh, en: n.en } : null;
70
+ }
66
71
  /** The live platform components of `kind` (`ids` / `itemId`: only those). */
67
72
  export async function loadComponents(db, kind, filter = {}) {
68
73
  if (filter.ids !== undefined && filter.ids.length === 0)
@@ -105,17 +110,19 @@ export async function loadComponents(db, kind, filter = {}) {
105
110
  q = q.where("p.template_id", "=", filter.name);
106
111
  if (filter.forUpdate)
107
112
  q = q.forUpdate("p");
108
- return (await q.orderBy("p.id").execute()).map((r) => ({
109
- ...common(r),
110
- ...(r.file?.defaultEnabled === false && {
111
- defaultOff: true,
112
- }),
113
- name: r.template_id,
114
- plugin: r.item_name,
115
- version: r.current_version,
116
- description: null,
117
- originOrgId: null,
118
- }));
113
+ return (await q.orderBy("p.id").execute()).map((r) => {
114
+ const file = r.file;
115
+ return {
116
+ ...common(r),
117
+ ...(file?.defaultEnabled === false && { defaultOff: true }),
118
+ name: r.template_id,
119
+ plugin: r.item_name,
120
+ version: r.current_version,
121
+ description: typeof file?.description === "string" ? file.description : null,
122
+ displayName: localizedName(file?.displayName),
123
+ originOrgId: null,
124
+ };
125
+ });
119
126
  }
120
127
  const table = kind === "skill"
121
128
  ? "platform_skills"
@@ -154,6 +161,7 @@ export async function loadComponents(db, kind, filter = {}) {
154
161
  plugin: r.plugin_name,
155
162
  version: String(r.current_version),
156
163
  description: r.description,
164
+ displayName: null,
157
165
  originOrgId: r.origin_org_id,
158
166
  }));
159
167
  }
@@ -1714,7 +1714,10 @@ function actionOf(s, sha256, version) {
1714
1714
  export function pluginReviewHash(r, files) {
1715
1715
  const e = r.extension;
1716
1716
  const lic = r.licenseText;
1717
- const canonical = JSON.stringify([
1717
+ // canonicalJson, not JSON.stringify: the review is stored as jsonb, which reorders object keys,
1718
+ // so a template's auth / endpoint object would hash differently at confirm (plugin.review_stale
1719
+ // on every import of a plugin whose connector template has a non-trivial auth block).
1720
+ const canonical = canonicalJson([
1718
1721
  r.name,
1719
1722
  r.version,
1720
1723
  r.contentDigest,
@@ -1,5 +1,6 @@
1
1
  import { AgentLookup } from "../agents/agent-lookup.js";
2
2
  import type { ModelRef } from "../agents/definition.js";
3
+ import type { AgentAvatar } from "../agents/profile.js";
3
4
  import { BackgroundTaskStopper } from "../background/stop.js";
4
5
  import type { TitleSource } from "../db/kernel-schema.js";
5
6
  import { ConversationEnvironments, type NewConversationEnvironment } from "../environments/conversation-environments.js";
@@ -53,9 +54,17 @@ export interface ConversationView {
53
54
  export declare function escapeLike(s: string): string;
54
55
  /** One agent's group in the sidebar list: the caller's newest conversations with that agent. */
55
56
  export interface ConversationGroup {
57
+ /**
58
+ * The agent's name and the avatar half of its profile, in the agents API's shape (the sidebar
59
+ * draws the group title from it without reading the agents list).
60
+ */
56
61
  agent: {
57
62
  id: string;
58
63
  name: string;
64
+ profile: {
65
+ avatar: AgentAvatar;
66
+ updatedAt: string;
67
+ };
59
68
  };
60
69
  /** max(updated_at) of the group's conversations. */
61
70
  latestAt: Date;
@@ -482,7 +482,7 @@ let ConversationsService = class ConversationsService {
482
482
  LIMIT ${opts.limit + 1}
483
483
  )
484
484
  SELECT page.agent_id, page.latest_at, v.definition ->> 'name' AS agent_name,
485
- cs.id, cs.org_id, cs.account_id, cs.title, cs.title_source, cs.selection, cs.last_seq,
485
+ a.profile_avatar, a.profile_updated_at, cs.id, cs.org_id, cs.account_id, cs.title, cs.title_source, cs.selection, cs.last_seq,
486
486
  cs.created_at, cs.updated_at, cs.parent_conversation_id, cs.subagent_type,
487
487
  cs.archived_at, cs.environment_id, cs.activity
488
488
  FROM page
@@ -506,7 +506,14 @@ let ConversationsService = class ConversationsService {
506
506
  for (const r of rows) {
507
507
  if (current?.agent.id !== r.agent_id) {
508
508
  current = {
509
- agent: { id: r.agent_id, name: r.agent_name ?? "" },
509
+ agent: {
510
+ id: r.agent_id,
511
+ name: r.agent_name ?? "",
512
+ profile: {
513
+ avatar: r.profile_avatar,
514
+ updatedAt: r.profile_updated_at.toISOString(),
515
+ },
516
+ },
510
517
  latestAt: r.latest_at,
511
518
  conversations: [],
512
519
  more: false,
@@ -809,6 +809,7 @@ export declare const ConversationEventSchema: z.ZodDiscriminatedUnion<[z.ZodObje
809
809
  deny: z.ZodOptional<z.ZodArray<z.ZodString>>;
810
810
  }, z.core.$strict>>;
811
811
  toolPolicies: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<{
812
+ allow: "allow";
812
813
  ask: "ask";
813
814
  deny: "deny";
814
815
  }>>>;
@@ -420,6 +420,18 @@ export interface ConnectorsTable {
420
420
  /** 0081: the platform connector template this connector was made from (null = none). */
421
421
  platform_template_id: ColumnType<string | null, string | null | undefined, string | null>;
422
422
  }
423
+ /**
424
+ * 0104: a member's own policy for one tool of a personal-credential connector, for calls made with
425
+ * their own connection (maintainer decision 2026-11-09).
426
+ */
427
+ export interface ConnectorToolAccountPoliciesTable {
428
+ org_id: string;
429
+ connector_id: string;
430
+ account_id: string;
431
+ tool_name: string;
432
+ policy: "allow" | "ask" | "deny";
433
+ updated_at: Ts;
434
+ }
423
435
  /** 0010_tools.sql(0033 由 `app_tools` 改名):连接器暴露的工具。 */
424
436
  export interface ConnectorToolsTable {
425
437
  connector_id: string;
@@ -1142,6 +1154,7 @@ export interface KernelDatabase {
1142
1154
  lg_checkpoint_writes: LgCheckpointWritesTable;
1143
1155
  connectors: ConnectorsTable;
1144
1156
  connector_tools: ConnectorToolsTable;
1157
+ connector_tool_account_policies: ConnectorToolAccountPoliciesTable;
1145
1158
  tool_grants: ToolGrantsTable;
1146
1159
  conversation_tool_grants: ConversationToolGrantsTable;
1147
1160
  approvals: ApprovalsTable;
package/dist/errors.d.ts CHANGED
@@ -251,6 +251,8 @@ export declare const ERROR_CODES: {
251
251
  readonly "connector.tool_not_found": 404;
252
252
  readonly "connector.tool_not_pending": 409;
253
253
  readonly "connector.resources_disabled": 409;
254
+ /** Per-member tool policies on a connector with one org credential (decision 2026-11-09). */
255
+ readonly "connector.not_personal": 409;
254
256
  readonly "approval.not_found": 404;
255
257
  readonly "approval.already_decided": 409;
256
258
  readonly "approval.scope_not_allowed": 409;
package/dist/errors.js CHANGED
@@ -251,6 +251,8 @@ export const ERROR_CODES = {
251
251
  "connector.tool_not_found": 404,
252
252
  "connector.tool_not_pending": 409,
253
253
  "connector.resources_disabled": 409,
254
+ /** Per-member tool policies on a connector with one org credential (decision 2026-11-09). */
255
+ "connector.not_personal": 409,
254
256
  "approval.not_found": 404,
255
257
  "approval.already_decided": 409,
256
258
  "approval.scope_not_allowed": 409,
@@ -608,6 +608,16 @@ export const PLATFORM_EXCLUSIONS = [
608
608
  category: "secret",
609
609
  reason: "Writes the caller's connector credentials",
610
610
  },
611
+ {
612
+ route: "GET /api/v1/orgs/:orgId/connectors/:connectorId/tool-policies/me",
613
+ category: "internal",
614
+ reason: "Display list of the connector card's 权限 sheet; connectors.get gives an agent the tools and their org defaults",
615
+ },
616
+ {
617
+ route: "PUT /api/v1/orgs/:orgId/connectors/:connectorId/tool-policies/me",
618
+ category: "human-only",
619
+ reason: "A member's own tool policies decide what agents may call with their connection without asking; an agent must never loosen them for itself",
620
+ },
611
621
  {
612
622
  route: "DELETE /api/v1/orgs/:orgId/connectors/:connectorId/credentials/me",
613
623
  category: "secret",
@@ -40,6 +40,15 @@ const RouteSkills = Def.skills.unwrap();
40
40
  const Skills = RouteSkills.pick({ pinned: true })
41
41
  .extend({ orgSkills: RouteSkills.shape.orgSkills.unwrap() })
42
42
  .strict();
43
+ /**
44
+ * Whether `next` sets an `allow` that `current` does not have (decision 2026-11-09: `allow` on a
45
+ * connector tool lets an agent call it without asking). Such a change is confirmed every time and
46
+ * never remembered, so an agent can never loosen its own policies on a grant the person remembered
47
+ * for ordinary edits.
48
+ */
49
+ function addsAllow(next, current) {
50
+ return Object.entries(next ?? {}).some(([k, v]) => v === "allow" && current?.[k] !== "allow");
51
+ }
43
52
  /** The definition fields an agent can set through the tool; the rest keep the web's defaults. */
44
53
  const Fields = {
45
54
  name: Def.name,
@@ -156,8 +165,10 @@ let AgentsOperations = class AgentsOperations {
156
165
  * `agents.update`'s class (spec §13): `personal` (may be remembered) only on a private agent the
157
166
  * actor owns; any other agent the actor may edit is shared, so each change is confirmed.
158
167
  */
159
- async updateClass(agentId, ctx) {
168
+ async updateClass(agentId, toolPolicies, ctx) {
160
169
  const a = await this.agents.get(ctx.orgId, agentId, ctx.actorAccountId);
170
+ if (addsAllow(toolPolicies, a.definition.toolPolicies))
171
+ return "org";
161
172
  return a.visibility === "private" && a.ownerAccountId === ctx.actorAccountId
162
173
  ? "personal"
163
174
  : "org";
@@ -221,6 +232,8 @@ let AgentsOperations = class AgentsOperations {
221
232
  omits: ["memberIds", "profile"],
222
233
  loosens: { visibility: "optional: the operation creates private agents only" },
223
234
  confirm: "personal",
235
+ // an `allow` loosens: never on a remembered grant
236
+ confirmFor: async (i) => addsAllow(i.definition.toolPolicies, undefined) ? "org" : "personal",
224
237
  summary: (i) => ({
225
238
  zh: `新建私有智能体「${i.definition.name}」`,
226
239
  en: `Create private agent "${i.definition.name}"`,
@@ -246,7 +259,7 @@ let AgentsOperations = class AgentsOperations {
246
259
  }),
247
260
  defineOperation({
248
261
  id: "agents.update",
249
- description: 'Change an agent you may edit: your own, private or shared, and as an org admin any agent of the org. Send the version you read from agents.get and only the fields to change; the others keep their value. The description (one short paragraph, "" removes it) is saved with the definition; the avatar and quick actions are changed with agents.update-profile. disabledTools replaces the whole list of turned-off tools (send [] to turn every tool on). A version that is no longer current is refused, so read the agent again first. The visibility cannot be changed here (that is done on the agents page of the web app). A change to your own private agent may be remembered by the person; a change to a shared agent is confirmed every time.',
262
+ description: 'Change an agent you may edit: your own, private or shared, and as an org admin any agent of the org. Send the version you read from agents.get and only the fields to change; the others keep their value. The description (one short paragraph, "" removes it) is saved with the definition; the avatar and quick actions are changed with agents.update-profile. disabledTools replaces the whole list of turned-off tools (send [] to turn every tool on). A version that is no longer current is refused, so read the agent again first. The visibility cannot be changed here (that is done on the agents page of the web app). A change to your own private agent may be remembered by the person; a change to a shared agent, or one that sets a tool policy to allow, is confirmed every time.',
250
263
  input: z
251
264
  .object({
252
265
  agentId: AgentId,
@@ -289,7 +302,7 @@ let AgentsOperations = class AgentsOperations {
289
302
  next: "The agent changed since you read it: read it again with agents.get, then send its current version.",
290
303
  },
291
304
  ],
292
- confirmFor: (i, ctx) => this.updateClass(i.agentId, ctx),
305
+ confirmFor: (i, ctx) => this.updateClass(i.agentId, i.toolPolicies, ctx),
293
306
  summary: async (i, ctx) => {
294
307
  const a = await this.agents.get(ctx.orgId, i.agentId, ctx.actorAccountId);
295
308
  const name = named(a.definition.name);
@@ -353,7 +366,7 @@ let AgentsOperations = class AgentsOperations {
353
366
  // needs no declaration: an input that accepts less than the route passes the parity test.
354
367
  omits: ["expectedUpdatedAt"],
355
368
  confirm: "personal",
356
- confirmFor: (i, ctx) => this.updateClass(i.agentId, ctx),
369
+ confirmFor: (i, ctx) => this.updateClass(i.agentId, undefined, ctx),
357
370
  errors: [
358
371
  {
359
372
  code: "agent.not_found",
@@ -493,7 +506,7 @@ let AgentsOperations = class AgentsOperations {
493
506
  confirm: "personal",
494
507
  // destructive: asked every time and never remembered, a shared agent as an org write
495
508
  rememberable: false,
496
- confirmFor: (i, ctx) => this.updateClass(i.agentId, ctx),
509
+ confirmFor: (i, ctx) => this.updateClass(i.agentId, undefined, ctx),
497
510
  errors: [
498
511
  {
499
512
  code: "agent.not_found",
@@ -578,7 +591,7 @@ let AgentsOperations = class AgentsOperations {
578
591
  authz: { orgAction: "agents.create" },
579
592
  mirrors: ["POST /api/v1/orgs/:orgId/agents/:agentId/pending-connectors/:key/bind"],
580
593
  confirm: "personal",
581
- confirmFor: (i, ctx) => this.updateClass(i.agentId, ctx),
594
+ confirmFor: (i, ctx) => this.updateClass(i.agentId, undefined, ctx),
582
595
  errors: [
583
596
  {
584
597
  code: "agent.pending_connector_not_found",
@@ -34,9 +34,10 @@ export interface EffectiveDefinitionDeps {
34
34
  type Policy = "ask" | "deny";
35
35
  /**
36
36
  * `base` with `over` applied on top, the stricter value winning per key (`deny` > `ask`). Values
37
- * other than `ask` / `deny` (stored template data is not re-validated) are skipped on both sides.
37
+ * other than `ask` / `deny` (stored template data is not re-validated; an agent's connector-tool
38
+ * `allow`, which the policy check reads from the agent's current version) are skipped on both sides.
38
39
  */
39
- export declare function stricterPolicies(base: Record<string, Policy>, over: Record<string, Policy>): Record<string, Policy>;
40
+ export declare function stricterPolicies(base: Record<string, string>, over: Record<string, string>): Record<string, Policy>;
40
41
  /** An org agent's version and definition as checked by its gates. */
41
42
  export interface CheckedOrgAgent {
42
43
  version: number;
@@ -23,7 +23,8 @@ const RANK = { ask: 1, deny: 2 };
23
23
  const isPolicy = (p) => p === "ask" || p === "deny";
24
24
  /**
25
25
  * `base` with `over` applied on top, the stricter value winning per key (`deny` > `ask`). Values
26
- * other than `ask` / `deny` (stored template data is not re-validated) are skipped on both sides.
26
+ * other than `ask` / `deny` (stored template data is not re-validated; an agent's connector-tool
27
+ * `allow`, which the policy check reads from the agent's current version) are skipped on both sides.
27
28
  */
28
29
  export function stricterPolicies(base, over) {
29
30
  const out = {};