@capybara-hq/kernel 0.1.0-rc.19 → 0.1.0-rc.20

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 (141) hide show
  1. package/bootstrap.json +25 -25
  2. package/dist/admin/admin-agents.controller.d.ts +28 -0
  3. package/dist/admin/admin-agents.controller.js +79 -0
  4. package/dist/admin/admin-connectors.controller.d.ts +29 -0
  5. package/dist/admin/admin-connectors.controller.js +79 -0
  6. package/dist/admin/admin-items.schemas.d.ts +42 -0
  7. package/dist/admin/admin-items.schemas.js +48 -0
  8. package/dist/admin/admin.controller.d.ts +11 -20
  9. package/dist/admin/admin.controller.js +28 -34
  10. package/dist/admin/admin.module.d.ts +4 -1
  11. package/dist/admin/admin.module.js +50 -4
  12. package/dist/admin/admin.schemas.d.ts +4 -0
  13. package/dist/admin/admin.schemas.js +4 -1
  14. package/dist/admin/audit.controller.d.ts +14 -0
  15. package/dist/admin/audit.controller.js +42 -0
  16. package/dist/admin/audit.schemas.d.ts +8 -0
  17. package/dist/admin/audit.schemas.js +17 -0
  18. package/dist/admin/health.service.d.ts +104 -0
  19. package/dist/admin/health.service.js +246 -0
  20. package/dist/admin/overview.controller.d.ts +31 -0
  21. package/dist/admin/overview.controller.js +78 -0
  22. package/dist/admin/overview.schemas.d.ts +8 -0
  23. package/dist/admin/overview.schemas.js +13 -0
  24. package/dist/admin/overview.service.d.ts +112 -0
  25. package/dist/admin/overview.service.js +223 -0
  26. package/dist/admin/platform-audit.service.d.ts +41 -0
  27. package/dist/admin/platform-audit.service.js +70 -0
  28. package/dist/admin/usage.service.d.ts +149 -0
  29. package/dist/admin/usage.service.js +321 -0
  30. package/dist/agents/admin-agents.d.ts +92 -0
  31. package/dist/agents/admin-agents.js +170 -0
  32. package/dist/agents/agents.module.d.ts +4 -3
  33. package/dist/agents/agents.module.js +7 -3
  34. package/dist/agents/agents.service.d.ts +41 -3
  35. package/dist/agents/agents.service.js +113 -27
  36. package/dist/agents/definition.d.ts +16 -0
  37. package/dist/agents/definition.js +32 -2
  38. package/dist/agents/org-templates.js +4 -0
  39. package/dist/background/launch.d.ts +1 -1
  40. package/dist/background/launch.js +3 -3
  41. package/dist/background/tasks.service.d.ts +35 -3
  42. package/dist/background/tasks.service.js +91 -36
  43. package/dist/catalog/catalog.module.js +8 -2
  44. package/dist/channels/channels.module.js +11 -2
  45. package/dist/channels/supervisor.d.ts +2 -0
  46. package/dist/channels/supervisor.js +4 -0
  47. package/dist/conversations/events.js +1 -1
  48. package/dist/conversations/titles.js +4 -8
  49. package/dist/db/kernel-schema.d.ts +8 -1
  50. package/dist/devices/devices.module.js +5 -2
  51. package/dist/errors.d.ts +22 -2
  52. package/dist/errors.js +22 -2
  53. package/dist/evals/evals.module.js +11 -2
  54. package/dist/extensions/permission-view.js +3 -3
  55. package/dist/http/api.module.js +10 -1
  56. package/dist/identity/settings.js +3 -2
  57. package/dist/identity/write-actor.d.ts +11 -0
  58. package/dist/identity/write-actor.js +7 -0
  59. package/dist/inbox/inbox.service.d.ts +2 -1
  60. package/dist/inbox/platform-inbox.service.d.ts +7 -1
  61. package/dist/inbox/platform-inbox.service.js +20 -2
  62. package/dist/infra/presence/presence.module.d.ts +6 -0
  63. package/dist/infra/presence/presence.module.js +22 -0
  64. package/dist/infra/presence/replica-presence.d.ts +87 -0
  65. package/dist/infra/presence/replica-presence.js +189 -0
  66. package/dist/kernel.module.js +3 -0
  67. package/dist/lifecycle/kernel-lifecycle.d.ts +3 -1
  68. package/dist/lifecycle/kernel-lifecycle.js +17 -3
  69. package/dist/local-devices/local-device-gateway.d.ts +6 -0
  70. package/dist/local-devices/local-device-gateway.js +12 -0
  71. package/dist/overrides.d.ts +7 -1
  72. package/dist/overrides.js +4 -0
  73. package/dist/platform/exclusions.js +15 -0
  74. package/dist/platform/operation.d.ts +1 -1
  75. package/dist/platform/operation.js +1 -1
  76. package/dist/platform/ops/admin-items.ops.d.ts +37 -0
  77. package/dist/platform/ops/admin-items.ops.js +278 -0
  78. package/dist/platform/ops/admin.ops.d.ts +10 -10
  79. package/dist/platform/ops/admin.ops.js +111 -42
  80. package/dist/platform/ops/agent-skills.ops.d.ts +29 -3
  81. package/dist/platform/ops/agent-skills.ops.js +290 -7
  82. package/dist/platform/ops/inbox.ops.js +7 -4
  83. package/dist/platform/ops/platform.ops.js +1 -1
  84. package/dist/platform/ops/skills.ops.js +1 -57
  85. package/dist/platform/platform-ops.module.d.ts +2 -1
  86. package/dist/platform/platform-ops.module.js +5 -0
  87. package/dist/platform/route-schemas.js +8 -0
  88. package/dist/roles/gateway-role.d.ts +4 -0
  89. package/dist/roles/gateway-role.js +8 -0
  90. package/dist/runtime/run-jobs.js +4 -2
  91. package/dist/sandbox/sandbox-file-reader.d.ts +35 -0
  92. package/dist/sandbox/sandbox-file-reader.js +98 -0
  93. package/dist/skills/agent-skills.controller.d.ts +48 -0
  94. package/dist/skills/agent-skills.controller.js +131 -0
  95. package/dist/skills/agent-skills.service.d.ts +262 -0
  96. package/dist/skills/agent-skills.service.js +996 -0
  97. package/dist/skills/catalog.d.ts +8 -6
  98. package/dist/skills/catalog.js +8 -4
  99. package/dist/skills/changes.controller.d.ts +5 -22
  100. package/dist/skills/changes.controller.js +7 -55
  101. package/dist/skills/changes.service.d.ts +69 -115
  102. package/dist/skills/changes.service.js +249 -599
  103. package/dist/skills/conversation-catalog.d.ts +53 -0
  104. package/dist/skills/conversation-catalog.js +149 -0
  105. package/dist/skills/invocation-keys.d.ts +1 -1
  106. package/dist/skills/invocation-keys.js +1 -1
  107. package/dist/skills/package.d.ts +0 -5
  108. package/dist/skills/package.js +0 -14
  109. package/dist/skills/skills.controller.js +1 -10
  110. package/dist/skills/skills.module.js +15 -2
  111. package/dist/skills/skills.schemas.d.ts +11 -15
  112. package/dist/skills/skills.schemas.js +27 -20
  113. package/dist/skills/skills.service.d.ts +37 -30
  114. package/dist/skills/skills.service.js +107 -59
  115. package/dist/skills/usable.d.ts +4 -3
  116. package/dist/skills/usable.js +13 -4
  117. package/dist/tools/admin-connectors.d.ts +69 -0
  118. package/dist/tools/admin-connectors.js +135 -0
  119. package/dist/tools/builtin-names.d.ts +9 -14
  120. package/dist/tools/builtin-names.js +13 -22
  121. package/dist/tools/builtins/builtin-tools.d.ts +2 -3
  122. package/dist/tools/builtins/builtin-tools.js +21 -26
  123. package/dist/tools/builtins/sandbox/bash.js +2 -2
  124. package/dist/tools/builtins/skill-load.js +1 -0
  125. package/dist/tools/connectors.controller.d.ts +5 -1
  126. package/dist/tools/connectors.controller.js +17 -3
  127. package/dist/tools/connectors.service.d.ts +20 -1
  128. package/dist/tools/connectors.service.js +61 -0
  129. package/dist/tools/legacy-keys.js +0 -1
  130. package/dist/tools/org-tool-switches.d.ts +3 -2
  131. package/dist/tools/org-tool-switches.js +4 -4
  132. package/dist/tools/policy.d.ts +7 -9
  133. package/dist/tools/policy.js +11 -22
  134. package/dist/tools/tools.module.js +3 -0
  135. package/package.json +1 -1
  136. package/src/migrations/0109_audit_platform_idx.sql +5 -0
  137. package/src/migrations/0110_usage_time_idx.sql +8 -0
  138. package/src/migrations/0111_agent_skill_sharing.sql +22 -0
  139. package/src/migrations/0112_skill_share_requests.sql +4 -0
  140. package/dist/tools/builtins/skill-write.d.ts +0 -59
  141. package/dist/tools/builtins/skill-write.js +0 -244
@@ -28,7 +28,6 @@ import { conversationTimeZone, RUN_CONTEXT_CLOCK } from "../../runtime/run-conte
28
28
  import { StepLog } from "../../runtime/step-log.js";
29
29
  import { UsageReporter } from "../../runtime/usage-report.js";
30
30
  import { SandboxService } from "../../sandbox/sandbox.service.js";
31
- import { SkillChangesService } from "../../skills/changes.service.js";
32
31
  import { StorageService } from "../../storage/storage.service.js";
33
32
  import { ToolOutputs } from "../outputs.js";
34
33
  import { agentTool } from "./agent.js";
@@ -40,18 +39,17 @@ import { readToolOutputTool } from "./read-tool-output.js";
40
39
  import { sandboxTools } from "./sandbox/index.js";
41
40
  import { sendMessageTool } from "./send-message.js";
42
41
  import { skillLoadTool } from "./skill-load.js";
43
- import { skillWriteTool } from "./skill-write.js";
44
42
  import { toolSearchTool } from "./tool-search.js";
45
43
  import { extractAnswer, WebPages, webTools } from "./web/index.js";
46
44
  /**
47
45
  * The kernel's builtin tools, built once from the injected services (their specs: names,
48
46
  * descriptions and schemas, are the factories' own and unchanged). `ToolSearch` and `Skill`
49
47
  * are added to a run by the registry (`forAgent`: organization setting and threshold, a skills
50
- * catalog snapshot), not by agent definitions; `SkillWrite` only when a definition names it.
48
+ * catalog snapshot), not by agent definitions.
51
49
  */
52
50
  let BuiltinTools = class BuiltinTools {
53
51
  tools;
54
- constructor(db, config, outputs, storage, skillChanges, attachments, eventLog, sandbox, devices, points, webPages,
52
+ constructor(db, config, outputs, storage, attachments, eventLog, sandbox, devices, points, webPages,
55
53
  /** The WebFetch extraction call: the `web_fetch` purpose, its hooks, usage and denials. */
56
54
  purposes, host, usageReport, stepLog, log, background, finisher, stopper, platformOps, platformActor, agentDocs,
57
55
  /** The clock `CurrentTime` reads (the run context's clock seam; `Date.now` by default). */
@@ -91,8 +89,6 @@ let BuiltinTools = class BuiltinTools {
91
89
  return db.db;
92
90
  },
93
91
  }),
94
- // 自写技能:agent 定义显式引用才可用
95
- skillWriteTool({ changes: skillChanges }),
96
92
  // web search / fetch through the org's default provider (web tools spec §3); WebFetch answers
97
93
  // from the page with one call on the `web_fetch` purpose (§5), never returning the page
98
94
  ...webTools({
@@ -127,25 +123,24 @@ BuiltinTools = __decorate([
127
123
  __param(1, Inject(KernelConfigService)),
128
124
  __param(2, Inject(ToolOutputs)),
129
125
  __param(3, Inject(StorageService)),
130
- __param(4, Inject(SkillChangesService)),
131
- __param(5, Inject(AttachmentsService)),
132
- __param(6, Inject(EventLog)),
133
- __param(7, Inject(SandboxService)),
134
- __param(8, Inject(ConversationDevices)),
135
- __param(9, Inject(ExtensionPointDefaults)),
136
- __param(10, Inject(WebPages)),
137
- __param(11, Inject(ModelPurposesService)),
138
- __param(12, Inject(ExtensionHost)),
139
- __param(13, Inject(UsageReporter)),
140
- __param(14, Inject(StepLog)),
141
- __param(15, Inject(KERNEL_LOGGER)),
142
- __param(16, Inject(BackgroundTaskStore)),
143
- __param(17, Inject(BackgroundTaskFinisher)),
144
- __param(18, Inject(BackgroundTaskStopper)),
145
- __param(19, Inject(PlatformOperations)),
146
- __param(20, Inject(PlatformActor)),
147
- __param(21, Inject(AgentDocs)),
148
- __param(22, Optional()),
149
- __param(22, Inject(RUN_CONTEXT_CLOCK))
126
+ __param(4, Inject(AttachmentsService)),
127
+ __param(5, Inject(EventLog)),
128
+ __param(6, Inject(SandboxService)),
129
+ __param(7, Inject(ConversationDevices)),
130
+ __param(8, Inject(ExtensionPointDefaults)),
131
+ __param(9, Inject(WebPages)),
132
+ __param(10, Inject(ModelPurposesService)),
133
+ __param(11, Inject(ExtensionHost)),
134
+ __param(12, Inject(UsageReporter)),
135
+ __param(13, Inject(StepLog)),
136
+ __param(14, Inject(KERNEL_LOGGER)),
137
+ __param(15, Inject(BackgroundTaskStore)),
138
+ __param(16, Inject(BackgroundTaskFinisher)),
139
+ __param(17, Inject(BackgroundTaskStopper)),
140
+ __param(18, Inject(PlatformOperations)),
141
+ __param(19, Inject(PlatformActor)),
142
+ __param(20, Inject(AgentDocs)),
143
+ __param(21, Optional()),
144
+ __param(21, Inject(RUN_CONTEXT_CLOCK))
150
145
  ], BuiltinTools);
151
146
  export { BuiltinTools };
@@ -282,13 +282,13 @@ async function runInBackground(deps, ctx, input) {
282
282
  await announce();
283
283
  if (!isLarge || e instanceof ToolError)
284
284
  throw e;
285
- throw new ToolError(`The background task was not started: ${clip(why, 500).replace(/\.$/, "")}. Check the sandbox, or call Bash without run_in_background.${largeText()}`);
285
+ throw new ToolError(`The background task was not started: ${clip(why, 500).replace(/\.$/, "")}. Check the environment, or call Bash without run_in_background.${largeText()}`);
286
286
  }
287
287
  }
288
288
  if (launch.kind === "not_started") {
289
289
  await background.failLaunch(id, launch.why);
290
290
  await announce();
291
- throw new ToolError(`The background task was not started: ${launch.why}. Check the sandbox, or call Bash without run_in_background.${largeText()}`);
291
+ throw new ToolError(`The background task was not started: ${launch.why}. Check the environment, or call Bash without run_in_background.${largeText()}`);
292
292
  }
293
293
  if (launch.kind === "stopped") {
294
294
  await announce();
@@ -220,6 +220,7 @@ export async function findSkillVersion(db, ctx, entry) {
220
220
  .where("org_id", "=", ctx.orgId)
221
221
  .where("requested_in_conversation", "=", ctx.conversationId)
222
222
  .where("status", "=", "pending")
223
+ .where("kind", "=", "version")
223
224
  .where("object_prefix", "=", entry.source)
224
225
  .where(sql `proposed_version ->> 'skillId'`, "=", entry.id)
225
226
  .executeTakeFirst();
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import type { ConnectionGuideState } from "../extensions/plugin-guides.js";
3
3
  import { PluginIcons } from "../extensions/plugin-icons.js";
4
4
  import { type Principal } from "../identity/principal.js";
5
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
5
6
  import { type OrgContext } from "../orgs/org.guard.js";
6
7
  import { ConnectorAccountPolicies, type MemberToolPolicyView } from "./connector-account-policies.js";
7
8
  import { ConnectorGuideAckSchema, CredentialValue, MemberToolPoliciesBody } from "./connectors.schemas.js";
@@ -177,6 +178,8 @@ declare const UpdateBody: z.ZodObject<{
177
178
  */
178
179
  export type ConnectorPage<T extends ConnectorSummary = ConnectorDetail> = T & ConnectionGuideState & {
179
180
  icon: string | null;
181
+ /** The one-connector read of an `oauth_code` connector, for managers: its redirect URI. */
182
+ oauthCallbackUrl?: string | null;
180
183
  };
181
184
  /**
182
185
  * 连接器(`/api/v1/orgs/:orgId/connectors`):读与个人凭据用 `connectors.use`(访客起),安装 / 修改 / 删除 / 刷新 /
@@ -186,7 +189,8 @@ export declare class ConnectorsController {
186
189
  private readonly connectors;
187
190
  private readonly icons;
188
191
  private readonly memberPolicies;
189
- constructor(connectors: ConnectorsService, icons: PluginIcons, memberPolicies: ConnectorAccountPolicies);
192
+ private readonly config;
193
+ constructor(connectors: ConnectorsService, icons: PluginIcons, memberPolicies: ConnectorAccountPolicies, config: Pick<KernelConfigService, "get">);
190
194
  /**
191
195
  * Each connector with `icon`: the icon of the plugin its template came from, on the org's catalog
192
196
  * route (null for a custom connector, a plugin without an icon, or one the reader's catalog does
@@ -12,6 +12,7 @@ import { Controller, Delete, Get, HttpCode, Inject, Param, Patch, Post, Put } fr
12
12
  import { z } from "zod";
13
13
  import { orgIconScope, PluginIcons } from "../extensions/plugin-icons.js";
14
14
  import { CurrentPrincipal, patTokenId, requireUser, } from "../identity/principal.js";
15
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
15
16
  import { createZodDto } from "../infra/validation/create-zod-dto.js";
16
17
  import { ZBody } from "../infra/validation/zod-params.js";
17
18
  import { CurrentOrg, OrgAction } from "../orgs/org.guard.js";
@@ -20,6 +21,7 @@ import { ConnectorAccountPolicies, } from "./connector-account-policies.js";
20
21
  import { ConnectorGuideAckSchema, CredentialValue, MemberToolPoliciesBody, } from "./connectors.schemas.js";
21
22
  import { ConnectorsService, } from "./connectors.service.js";
22
23
  import { publicEndpoint } from "./endpoint.js";
24
+ import { callbackUrl } from "./oauth/flow-paths.js";
23
25
  /**
24
26
  * 没有 `connectors.manage` 的读者只看到端点的公开部分(URL 不带 query、只有头名,见 endpoint.ts),
25
27
  * 也看不到工具的连接器数据(可能含服务器地址)。
@@ -98,10 +100,12 @@ let ConnectorsController = class ConnectorsController {
98
100
  connectors;
99
101
  icons;
100
102
  memberPolicies;
101
- constructor(connectors, icons, memberPolicies) {
103
+ config;
104
+ constructor(connectors, icons, memberPolicies, config) {
102
105
  this.connectors = connectors;
103
106
  this.icons = icons;
104
107
  this.memberPolicies = memberPolicies;
108
+ this.config = config;
105
109
  }
106
110
  /**
107
111
  * Each connector with `icon`: the icon of the plugin its template came from, on the org's catalog
@@ -157,7 +161,16 @@ let ConnectorsController = class ConnectorsController {
157
161
  async get(principal, org, connectorId) {
158
162
  const p = requireUser(principal);
159
163
  const view = forViewer(org, await this.connectors.get(org.orgId, connectorId, p.account.id));
160
- return { connector: await this.withGuide(org, view) };
164
+ const page = await this.withGuide(org, view);
165
+ // The redirect URI to register at the authorization server, for those who manage connectors
166
+ // (the credentials tab shows it next to the OAuth client); null without `http.publicUrl`.
167
+ if (page.auth.type !== "oauth_code" || !can(org.role, "connectors.manage")) {
168
+ return { connector: page };
169
+ }
170
+ const publicUrl = this.config.get().http.publicUrl;
171
+ return {
172
+ connector: { ...page, oauthCallbackUrl: publicUrl ? callbackUrl(publicUrl) : null },
173
+ };
161
174
  }
162
175
  async update(principal, org, connectorId, body) {
163
176
  const p = requireUser(principal);
@@ -343,6 +356,7 @@ ConnectorsController = __decorate([
343
356
  Controller("api/v1/orgs/:orgId/connectors"),
344
357
  __param(0, Inject(ConnectorsService)),
345
358
  __param(1, Inject(PluginIcons)),
346
- __param(2, Inject(ConnectorAccountPolicies))
359
+ __param(2, Inject(ConnectorAccountPolicies)),
360
+ __param(3, Inject(KernelConfigService))
347
361
  ], ConnectorsController);
348
362
  export { ConnectorsController };
@@ -1,6 +1,7 @@
1
1
  import { type AuthHint, type ConnectorOperation, type ToolAnnotations } from "@capybara-hq/plugin-sdk";
2
2
  import { type ConnectionGuideState, PluginGuides } from "../extensions/plugin-guides.js";
3
3
  import { ExtensionRegistry } from "../extensions/registry.js";
4
+ import type { WriteActor } from "../identity/write-actor.js";
4
5
  import { KernelConfigService } from "../infra/config/kernel-config.service.js";
5
6
  import { KernelDb } from "../infra/database/kernel-db.js";
6
7
  import { TransactionHost } from "../infra/database/transaction-host.js";
@@ -220,7 +221,7 @@ export declare class ConnectorsService {
220
221
  /** Extension manifests (a slug may not take an extension's name). */
221
222
  extensions: Pick<ExtensionRegistry, "manifests">, credentials: Credentials, config: Pick<KernelConfigService, "get">, drift: ToolDrift,
222
223
  /** The org row lock (orgs/org-lock.ts): joins the caller's transaction. */
223
- orgLock: Pick<OrgLock, "lockAndAuthorize">, txHost: TransactionHost,
224
+ orgLock: Pick<OrgLock, "lock" | "lockAndAuthorize">, txHost: TransactionHost,
224
225
  /** Setup guides of connector templates (spec 2026-11-09-setup-guides-v2-design.md §11). */
225
226
  guides: Pick<PluginGuides, "templateAck" | "connectorStates">);
226
227
  /** The kernel-schema handle of the current context (its transaction, else the pool). */
@@ -338,6 +339,24 @@ export declare class ConnectorsService {
338
339
  get(orgId: string, connectorId: string, viewerAccountId: string): Promise<ConnectorDetail>;
339
340
  /** 改名、启停连接器、工具启停与默认策略、批量把无注解工具改为 allow(`connectors.manage`)。 */
340
341
  update(orgId: string, connectorId: string, actorAccountId: string, patch: UpdateConnectorInput, actorTokenId?: string): Promise<ConnectorDetail>;
342
+ /**
343
+ * The org id of a live connector of a live org (the platform paths address a connector by its
344
+ * global id); else `connector.not_found`.
345
+ */
346
+ private liveOrgOf;
347
+ /**
348
+ * Disables or enables a connector on the platform's behalf (a super admin who need not be a
349
+ * member of the org; the caller checks the super admin): org row lock, the status update, audit
350
+ * `connector.updated` with `via: "platform"`. `auditDetail` (a Platform tool write's
351
+ * `viaAgent`) is merged into the detail; `via` stays `"platform"`.
352
+ */
353
+ setStatusAsPlatform(connectorId: string, status: "active" | "disabled", actor: WriteActor, auditDetail?: Record<string, unknown>): Promise<void>;
354
+ /**
355
+ * Deletes a connector on the platform's behalf (see {@link setStatusAsPlatform}): org row lock,
356
+ * then {@link removeIn} with `via: "platform"` in the audit detail (merged over `auditDetail`,
357
+ * a Platform tool write's `viaAgent`).
358
+ */
359
+ removeAsPlatform(connectorId: string, actor: WriteActor, auditDetail?: Record<string, unknown>): Promise<void>;
341
360
  /** 软删除,并删除该连接器的全部凭据(`connectors.manage`)。 */
342
361
  remove(orgId: string, connectorId: string, actorAccountId: string, actorTokenId?: string, auditDetail?: Record<string, unknown>): Promise<void>;
343
362
  /**
@@ -1051,6 +1051,61 @@ let ConnectorsService = class ConnectorsService {
1051
1051
  });
1052
1052
  return this.detail(t, orgId, connectorId, actorAccountId);
1053
1053
  }
1054
+ /**
1055
+ * The org id of a live connector of a live org (the platform paths address a connector by its
1056
+ * global id); else `connector.not_found`.
1057
+ */
1058
+ async liveOrgOf(connectorId) {
1059
+ const r = await this.db
1060
+ .selectFrom("connectors as c")
1061
+ .innerJoin("orgs as o", "o.id", "c.org_id")
1062
+ .select("c.org_id")
1063
+ .where("c.id", "=", connectorId)
1064
+ .where("c.deleted_at", "is", null)
1065
+ .where("o.deleted_at", "is", null)
1066
+ .executeTakeFirst();
1067
+ if (!r)
1068
+ throw notFound();
1069
+ return r.org_id;
1070
+ }
1071
+ /**
1072
+ * Disables or enables a connector on the platform's behalf (a super admin who need not be a
1073
+ * member of the org; the caller checks the super admin): org row lock, the status update, audit
1074
+ * `connector.updated` with `via: "platform"`. `auditDetail` (a Platform tool write's
1075
+ * `viaAgent`) is merged into the detail; `via` stays `"platform"`.
1076
+ */
1077
+ async setStatusAsPlatform(connectorId, status, actor, auditDetail) {
1078
+ const t = this.db;
1079
+ const orgId = await this.liveOrgOf(connectorId);
1080
+ await this.orgLock.lock(orgId);
1081
+ const app = await this.row(t, orgId, connectorId, true);
1082
+ await t
1083
+ .updateTable("connectors")
1084
+ .set({ status, updated_at: sql `now()` })
1085
+ .where("id", "=", connectorId)
1086
+ .execute();
1087
+ await audit(t, {
1088
+ actorAccountId: actor.accountId,
1089
+ ...(actor.tokenId !== undefined && { actorTokenId: actor.tokenId }),
1090
+ orgId,
1091
+ action: "connector.updated",
1092
+ target: connectorId,
1093
+ detail: { ...auditDetail, status: { from: app.status, to: status }, via: "platform" },
1094
+ });
1095
+ }
1096
+ /**
1097
+ * Deletes a connector on the platform's behalf (see {@link setStatusAsPlatform}): org row lock,
1098
+ * then {@link removeIn} with `via: "platform"` in the audit detail (merged over `auditDetail`,
1099
+ * a Platform tool write's `viaAgent`).
1100
+ */
1101
+ async removeAsPlatform(connectorId, actor, auditDetail) {
1102
+ const orgId = await this.liveOrgOf(connectorId);
1103
+ await this.orgLock.lock(orgId);
1104
+ await this.removeIn(orgId, connectorId, actor.accountId, actor.tokenId, undefined, {
1105
+ ...auditDetail,
1106
+ via: "platform",
1107
+ });
1108
+ }
1054
1109
  /** 软删除,并删除该连接器的全部凭据(`connectors.manage`)。 */
1055
1110
  async remove(orgId, connectorId, actorAccountId, actorTokenId, auditDetail) {
1056
1111
  await this.orgLock.lockAndAuthorize(orgId, actorAccountId, "connectors.manage");
@@ -1333,6 +1388,12 @@ __decorate([
1333
1388
  __decorate([
1334
1389
  Transactional()
1335
1390
  ], ConnectorsService.prototype, "update", null);
1391
+ __decorate([
1392
+ Transactional()
1393
+ ], ConnectorsService.prototype, "setStatusAsPlatform", null);
1394
+ __decorate([
1395
+ Transactional()
1396
+ ], ConnectorsService.prototype, "removeAsPlatform", null);
1336
1397
  __decorate([
1337
1398
  Transactional()
1338
1399
  ], ConnectorsService.prototype, "remove", null);
@@ -20,7 +20,6 @@ export const LEGACY_BUILTINS = Object.freeze({
20
20
  sandbox_grep: { name: BUILTIN.Grep, params: { ignoreCase: "-i" } },
21
21
  sandbox_deliver: { name: BUILTIN.Deliver, params: { path: "file_path" } },
22
22
  skill_load: { name: BUILTIN.Skill, params: { name: "skill" } },
23
- skill_write: { name: BUILTIN.SkillWrite, params: {} },
24
23
  ask_user: { name: BUILTIN.AskUserQuestion, params: {} },
25
24
  read_tool_output: { name: BUILTIN.ReadToolOutput, params: {} },
26
25
  tool_search: { name: BUILTIN.ToolSearch, params: { limit: "max_results" } },
@@ -39,7 +39,7 @@ export declare class OrgToolSwitches {
39
39
  private readonly host;
40
40
  /** The org row lock: re-checks `org.extensions.manage` under the lock. */
41
41
  private readonly orgLock;
42
- constructor(kdb: KernelDb, txHost: TransactionHost, host: Pick<ExtensionHost, "tools">,
42
+ constructor(kdb: KernelDb, txHost: TransactionHost, host: Pick<ExtensionHost, "tools" | "current">,
43
43
  /** The org row lock: re-checks `org.extensions.manage` under the lock. */
44
44
  orgLock: Pick<OrgLock, "lockAndAuthorize">);
45
45
  private get db();
@@ -47,7 +47,8 @@ export declare class OrgToolSwitches {
47
47
  private registered;
48
48
  /**
49
49
  * The tools of an extension the org sees: running here and visible under plugin scope, else 404
50
- * `extension.not_found` (an invisible one is answered like an unknown one).
50
+ * `extension.not_found` (an invisible one is answered like an unknown one). An extension that
51
+ * runs but registers no tools (a channel, a model provider) answers an empty list, not 404.
51
52
  */
52
53
  private toolsFor;
53
54
  /** The stored switches of the org (all extensions, or one), `false` ones only matter. */
@@ -63,14 +63,14 @@ let OrgToolSwitches = class OrgToolSwitches {
63
63
  }
64
64
  /**
65
65
  * The tools of an extension the org sees: running here and visible under plugin scope, else 404
66
- * `extension.not_found` (an invisible one is answered like an unknown one).
66
+ * `extension.not_found` (an invisible one is answered like an unknown one). An extension that
67
+ * runs but registers no tools (a channel, a model provider) answers an empty list, not 404.
67
68
  */
68
69
  async toolsFor(orgId, plugin) {
69
- const tools = this.registered(plugin);
70
- if (tools.length === 0 || !(await extensionVisibleTo(this.db, orgId, plugin))) {
70
+ if (!this.host.current(plugin) || !(await extensionVisibleTo(this.db, orgId, plugin))) {
71
71
  throw new ApiError("extension.not_found", `Extension ${plugin} is not installed`);
72
72
  }
73
- return tools;
73
+ return this.registered(plugin);
74
74
  }
75
75
  /** The stored switches of the org (all extensions, or one), `false` ones only matter. */
76
76
  async offRows(orgId, plugin) {
@@ -1,7 +1,6 @@
1
1
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
2
2
  import type { DecidePolicy } from "./pipeline.js";
3
3
  import type { ResolvedTool, ToolPolicy } from "./registry.js";
4
- export { R3_FIXED_ASK_TOOL_KEYS, R3_SHARED_AGENT_ONLY_TOOL_KEYS } from "./builtin-names.js";
5
4
  /** 取更严的策略(`allow < ask < deny`)。 */
6
5
  export declare function stricter(a: ToolPolicy, b: ToolPolicy | undefined): ToolPolicy;
7
6
  export interface PolicyInput {
@@ -9,22 +8,21 @@ export interface PolicyInput {
9
8
  connectorDefault: ToolPolicy;
10
9
  /** agent 负责人的收紧(`definition.toolPolicies[key]`)。 */
11
10
  agentOverride?: "ask" | "deny";
12
- /** R3 清单或 `requiresUserInteraction`。 */
11
+ /** `requiresUserInteraction`。 */
13
12
  fixedAsk: boolean;
14
13
  /** 已记住且 specHash 与当前一致的授权。 */
15
14
  grant: "session" | "always" | null;
16
15
  }
17
16
  /**
18
- * 策略判定(纯函数):取组织默认、agent 收紧、R3 固定询问三者中最严者;
19
- * 记住的授权只能把 `ask` 放行为 `allow`,不能越过 `deny`,对 R3 / `requiresUserInteraction` 的工具无效。
17
+ * 策略判定(纯函数):取组织默认、agent 收紧、固定询问三者中最严者;
18
+ * 记住的授权只能把 `ask` 放行为 `allow`,不能越过 `deny`,对 `requiresUserInteraction` 的工具无效。
20
19
  */
21
20
  export declare function decide(input: PolicyInput): ToolPolicy;
22
21
  /**
23
- * 是否属于 R3 固定询问(清单或工具声明需要用户交互)。`visibility` 为 agent 当前的可见性:
24
- * {@link R3_SHARED_AGENT_ONLY_TOOL_KEYS} 里的内置工具在私有 agent 上不算固定询问,
25
- * 除非 agent 当前版本的 `skills.writePolicy` 为 `ask`(`skillWritePolicy`);不知道可见性时按共享处理(更严)。
22
+ * Whether the tool is a fixed ask: its spec declares `requiresUserInteraction`, so its policy is
23
+ * at least `ask` and remembered grants never let it through.
26
24
  */
27
- export declare function isFixedAsk(tool: ResolvedTool, visibility?: "private" | "org" | "members" | undefined, skillWritePolicy?: "allow" | "ask" | undefined): boolean;
25
+ export declare function isFixedAsk(tool: ResolvedTool): boolean;
28
26
  /** Which level decided a connector tool's policy ({@link connectorPolicy}). */
29
27
  export type ConnectorPolicySource = "org" | "member" | "agent";
30
28
  /**
@@ -37,7 +35,7 @@ export type ConnectorPolicySource = "org" | "member" | "agent";
37
35
  * 4. the member's own `allow` / `ask`;
38
36
  * 5. the org's default (`connector_tools.default_policy`, the annotation default unless an org
39
37
  * admin changed it: `readOnlyHint: true` allows, anything else asks).
40
- * Fixed asks (R3, `requiresUserInteraction`), role escalation and grants apply after this.
38
+ * Fixed asks (`requiresUserInteraction`), role escalation and grants apply after this.
41
39
  */
42
40
  export declare function connectorPolicy(input: {
43
41
  orgDefault: ToolPolicy;
@@ -1,18 +1,17 @@
1
1
  import { sql } from "kysely";
2
2
  import { roleAtLeast } from "../orgs/roles.js";
3
3
  import { readOrgSettings } from "../orgs/settings.service.js";
4
- import { R3_FIXED_ASK_TOOL_KEYS, R3_SHARED_AGENT_ONLY_TOOL_KEYS, SANDBOX_ASK_TOOL_NAMES, } from "./builtin-names.js";
4
+ import { SANDBOX_ASK_TOOL_NAMES } from "./builtin-names.js";
5
5
  import { personalAuth } from "./connector-account-policies.js";
6
6
  import { normalizeStoredDefinitionIn } from "./legacy-data.js";
7
- export { R3_FIXED_ASK_TOOL_KEYS, R3_SHARED_AGENT_ONLY_TOOL_KEYS } from "./builtin-names.js";
8
7
  const RANK = { allow: 0, ask: 1, deny: 2 };
9
8
  /** 取更严的策略(`allow < ask < deny`)。 */
10
9
  export function stricter(a, b) {
11
10
  return b !== undefined && RANK[b] > RANK[a] ? b : a;
12
11
  }
13
12
  /**
14
- * 策略判定(纯函数):取组织默认、agent 收紧、R3 固定询问三者中最严者;
15
- * 记住的授权只能把 `ask` 放行为 `allow`,不能越过 `deny`,对 R3 / `requiresUserInteraction` 的工具无效。
13
+ * 策略判定(纯函数):取组织默认、agent 收紧、固定询问三者中最严者;
14
+ * 记住的授权只能把 `ask` 放行为 `allow`,不能越过 `deny`,对 `requiresUserInteraction` 的工具无效。
16
15
  */
17
16
  export function decide(input) {
18
17
  let p = stricter(input.connectorDefault, input.agentOverride);
@@ -23,18 +22,11 @@ export function decide(input) {
23
22
  return p;
24
23
  }
25
24
  /**
26
- * 是否属于 R3 固定询问(清单或工具声明需要用户交互)。`visibility` 为 agent 当前的可见性:
27
- * {@link R3_SHARED_AGENT_ONLY_TOOL_KEYS} 里的内置工具在私有 agent 上不算固定询问,
28
- * 除非 agent 当前版本的 `skills.writePolicy` 为 `ask`(`skillWritePolicy`);不知道可见性时按共享处理(更严)。
25
+ * Whether the tool is a fixed ask: its spec declares `requiresUserInteraction`, so its policy is
26
+ * at least `ask` and remembered grants never let it through.
29
27
  */
30
- export function isFixedAsk(tool, visibility, skillWritePolicy) {
31
- if (tool.spec.annotations?.requiresUserInteraction === true)
32
- return true;
33
- if (!R3_FIXED_ASK_TOOL_KEYS.has(tool.key))
34
- return false;
35
- const builtinOnly = tool.source.kind === "builtin" && R3_SHARED_AGENT_ONLY_TOOL_KEYS.has(tool.key);
36
- // 私有 agent:缺省 / allow 不问(R4),负责人设为 ask(D10)时与共享 agent 一样固定询问
37
- return !(builtinOnly && visibility === "private" && skillWritePolicy !== "ask");
28
+ export function isFixedAsk(tool) {
29
+ return tool.spec.annotations?.requiresUserInteraction === true;
38
30
  }
39
31
  const asSetting = (v) => v === "allow" || v === "ask" || v === "deny" ? v : undefined;
40
32
  /** The stricter of the settings that are set (none set: undefined). */
@@ -69,7 +61,7 @@ const tighteningOf = (s) => s === "ask" || s === "deny" ? s : undefined;
69
61
  * 4. the member's own `allow` / `ask`;
70
62
  * 5. the org's default (`connector_tools.default_policy`, the annotation default unless an org
71
63
  * admin changed it: `readOnlyHint: true` allows, anything else asks).
72
- * Fixed asks (R3, `requiresUserInteraction`), role escalation and grants apply after this.
64
+ * Fixed asks (`requiresUserInteraction`), role escalation and grants apply after this.
73
65
  */
74
66
  export function connectorPolicy(input) {
75
67
  if (input.orgDefault === "deny")
@@ -174,7 +166,7 @@ export function createDecidePolicy(db) {
174
166
  .innerJoin("agent_versions", (j) => j
175
167
  .onRef("agent_versions.agent_id", "=", "agents.id")
176
168
  .onRef("agent_versions.version", "=", "agents.current_version"))
177
- .select(["agent_versions.definition", "agents.visibility"])
169
+ .select(["agent_versions.definition"])
178
170
  .where("agents.id", "=", ctx.agentId)
179
171
  .executeTakeFirst();
180
172
  // stored raw: an rc version's keys read as current keys, so its deny keeps applying (§8.3)
@@ -222,12 +214,9 @@ export function createDecidePolicy(db) {
222
214
  if (!roleAtLeast(m.role, tool.requiredRole))
223
215
  escalateTo = tool.requiredRole;
224
216
  }
225
- // 可见性实时读(R5):共享 agent 的 SkillWrite 固定 ask,私有 agent 不问(见 R3_SHARED_AGENT_ONLY_TOOL_KEYS)
226
- // 当前版本的 skills.writePolicy 同样实时读(R5):运行中改为 ask,下一次调用即生效;固定 ask 时授权不放行
227
- const writePolicy = currentDef?.skills?.writePolicy;
228
217
  // `remember: false` on a call that is not a plain allow (an org / platform operation, or a read
229
218
  // an agent tightened to ask): every call asks, grants never apply. A read (`allow`) never asks.
230
- const fixedAsk = isFixedAsk(tool, current?.visibility, writePolicy) ||
219
+ const fixedAsk = isFixedAsk(tool) ||
231
220
  (call !== null && !call.remember && (call.policy !== "allow" || agentOverride !== undefined));
232
221
  const input = {
233
222
  connectorDefault,
@@ -253,7 +242,7 @@ export function createDecidePolicy(db) {
253
242
  requestedFor: escalateTo === null ? ctx.accountId : null,
254
243
  approverRole: escalateTo,
255
244
  // the most specific rule that asked: role escalation, the agent's tightening, the connector,
256
- // a fixed ask (R3, requiresUserInteraction), the org's sandboxToolPolicy, else the tool's own
245
+ // a fixed ask (requiresUserInteraction), the org's sandboxToolPolicy, else the tool's own
257
246
  // default
258
247
  policy: escalateTo !== null
259
248
  ? "role"
@@ -22,6 +22,7 @@ import { UsageReportModule } from "../runtime/usage-report.js";
22
22
  import { SandboxModule } from "../sandbox/sandbox.module.js";
23
23
  import { SkillsModule } from "../skills/skills.module.js";
24
24
  import { StorageModule } from "../storage/storage.module.js";
25
+ import { AdminConnectors } from "./admin-connectors.js";
25
26
  import { ApprovalsService } from "./approvals.service.js";
26
27
  import { Breaker } from "./breaker.js";
27
28
  import { BuiltinTools } from "./builtins/builtin-tools.js";
@@ -87,6 +88,7 @@ ToolsModule = __decorate([
87
88
  ConnectorContent,
88
89
  ToolPipeline,
89
90
  ConnectorsService,
91
+ AdminConnectors,
90
92
  ConnectorAccountPolicies,
91
93
  OAuthFlow,
92
94
  ConversationToolInfo,
@@ -105,6 +107,7 @@ ToolsModule = __decorate([
105
107
  ConnectorContent,
106
108
  ToolPipeline,
107
109
  ConnectorsService,
110
+ AdminConnectors,
108
111
  ConnectorAccountPolicies,
109
112
  OAuthFlow,
110
113
  ConversationToolInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capybara-hq/kernel",
3
- "version": "0.1.0-rc.19",
3
+ "version": "0.1.0-rc.20",
4
4
  "description": "Capybara kernel: config, database, plugin host and HTTP API of the Capybara agent platform",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,5 @@
1
+ -- Platform-level audit events (org_id null), newest first: the admin overview's recent platform
2
+ -- operations and /admin/audit page by keyset on id (admin overview spec §4.4, §5.4). A plain
3
+ -- CREATE INDEX: the migrator runs each file in a transaction.
4
+ CREATE INDEX IF NOT EXISTS audit_events_platform_idx
5
+ ON audit_events (id DESC) WHERE org_id IS NULL;
@@ -0,0 +1,8 @@
1
+ -- The admin overview's seven-day usage aggregate (admin overview spec §4.3, §5.4) reads runs and
2
+ -- embedding usage of every org by time: runs by created_at (only (conversation_id, created_at)
3
+ -- existed) and model_usage by occurred_at (only (org_id, occurred_at) and a partial index on the
4
+ -- unreported rows existed). Chat usage events already have conversation_events_usage_idx (0040).
5
+ -- A plain CREATE INDEX: the migrator runs each file in a transaction, so not CONCURRENTLY; it
6
+ -- locks writes to the table while it builds (pre-0.1.0: no large deployments).
7
+ CREATE INDEX IF NOT EXISTS runs_created_idx ON runs (created_at);
8
+ CREATE INDEX IF NOT EXISTS model_usage_time_idx ON model_usage (occurred_at);
@@ -0,0 +1,22 @@
1
+ -- Skill authoring via the Platform tool: an agent's owner shares an agent skill with the org. The
2
+ -- shared skill is an org skill (scope 'org') that remembers the agent it came from
3
+ -- (origin_agent_id, cleared when that agent is deleted; the skill stays) and the account that
4
+ -- maintains it (maintainer_account_id).
5
+ ALTER TABLE skills
6
+ ADD COLUMN IF NOT EXISTS origin_agent_id text REFERENCES agents(id) ON DELETE SET NULL;
7
+ ALTER TABLE skills
8
+ ADD COLUMN IF NOT EXISTS maintainer_account_id text REFERENCES accounts(id);
9
+ -- The skills an agent shared, for its skill list and for sharing again.
10
+ CREATE INDEX IF NOT EXISTS skills_origin_agent_idx
11
+ ON skills (origin_agent_id)
12
+ WHERE origin_agent_id IS NOT NULL AND deleted_at IS NULL;
13
+
14
+ -- A request is either a share of an agent skill with the org or a new version of a shared skill.
15
+ ALTER TABLE skill_change_requests
16
+ ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT 'version'
17
+ CHECK (kind IN ('share', 'version'));
18
+
19
+ -- The R4 owner-approved change flow ends. The draft objects of the cancelled rows are left to the
20
+ -- org data cleanup.
21
+ -- capybara:allow-destructive R4 owner-approved changes are removed; pending ones cannot be decided any more
22
+ UPDATE skill_change_requests SET status = 'cancelled', decided_at = now() WHERE status = 'pending';
@@ -0,0 +1,4 @@
1
+ -- A share request of an agent skill comes from the agent page or the Platform operation and has
2
+ -- no trial, so it records no conversation; a version proposal registered on the web has none
3
+ -- either. requested_in_conversation becomes optional.
4
+ ALTER TABLE skill_change_requests ALTER COLUMN requested_in_conversation DROP NOT NULL;
@@ -1,59 +0,0 @@
1
- import type { SkillManifest } from "../../db/kernel-schema.js";
2
- import type { SkillChangesService } from "../../skills/changes.service.js";
3
- import type { BuiltinTool } from "../registry.js";
4
- /**
5
- * 内置工具 `SkillWrite`(M5 T15,R4):agent 新建或更新它自己的技能(`scope = agent`)。
6
- * - 输入 `{ name, description, files: [{ path, content }] }`,只收文本文件,整个技能 ≤ {@link SKILL_WRITE_MAX_BYTES};
7
- * - the kernel generates SKILL.md ({@link buildSkillWrite}): its frontmatter is `name` / `description`
8
- * plus the invocation keys (`disable-model-invocation`, `context`, ... phase D) of the frontmatter
9
- * of the `SKILL.md` entry in `files`, whose other keys are dropped; its body is that entry's body
10
- * (empty without the entry);
11
- * - 每次调用写一个完整的新版本(上一版有、这次没给的文件不再属于新版本);
12
- * - 私有 agent 直接生成新版本并立即生效;共享 agent 由触发对话的人先确认这次调用(R3 固定 ask,见 tools/policy.ts),
13
- * 再生成变更申请等负责人批准,期间只有本对话试用新版本(见 skills/changes.service.ts);
14
- * - 成功后立即在日志追加技能目录快照(`skills_catalog`,Ruling M17),同一运行的下一步就能 `Skill`。
15
- */
16
- /** 一个技能(含合成的 SKILL.md)的总大小上限(UTF-8 字节)。 */
17
- export declare const SKILL_WRITE_MAX_BYTES: number;
18
- /** 一次最多的文件数(含 SKILL.md)。 */
19
- export declare const SKILL_WRITE_MAX_FILES = 200;
20
- /** `SkillWrite` 的输入(已按 inputSchema 校验)。 */
21
- export interface SkillWriteInput {
22
- name: string;
23
- description: string;
24
- files: {
25
- path: string;
26
- content: string;
27
- }[];
28
- }
29
- /** 一个待写入的文件。 */
30
- export interface BuiltSkillFile {
31
- path: string;
32
- data: Uint8Array;
33
- size: number;
34
- sha256: string;
35
- }
36
- /** {@link buildSkillWrite} 成功的结果。 */
37
- export interface BuiltSkill {
38
- name: string;
39
- description: string;
40
- files: BuiltSkillFile[];
41
- manifest: SkillManifest;
42
- }
43
- /**
44
- * 由工具输入合成技能文件(纯函数):
45
- * - SKILL.md = `---\n<yaml name/description>---\n\n<正文>\n`,正文为 `files` 里 `SKILL.md` 的内容去掉 frontmatter、
46
- * 去掉首尾空行;合成结果再用 `parseSkillMd` 校验(名字规则、描述长度、不含 XML 标签);
47
- * - 其余文件按 `validateSkillPackage` 校验路径(不能越出 `/skills/<name>/`、不能重复、大小写冲突等);
48
- * - 只收文本:内容含 NUL 视为二进制,拒绝;全部文件(含 SKILL.md)的 UTF-8 字节数之和 ≤ {@link SKILL_WRITE_MAX_BYTES}。
49
- * 返回合成结果或全部问题(英文,直接给模型)。
50
- */
51
- export declare function buildSkillWrite(input: SkillWriteInput): BuiltSkill | {
52
- errors: string[];
53
- };
54
- /** 依赖:技能变更服务。 */
55
- export interface SkillWriteDeps {
56
- changes: Pick<SkillChangesService, "write">;
57
- }
58
- /** 内置工具 `SkillWrite`(见文件头)。 */
59
- export declare function skillWriteTool(deps: SkillWriteDeps): BuiltinTool;