@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
@@ -1,5 +1,6 @@
1
1
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
2
2
  import { EvalBudget } from "../evals/budget.js";
3
+ import type { WriteActor } from "../identity/write-actor.js";
3
4
  import { KernelDb } from "../infra/database/kernel-db.js";
4
5
  import { TransactionHost } from "../infra/database/transaction-host.js";
5
6
  import { OrgLock } from "../orgs/org-lock.js";
@@ -76,7 +77,10 @@ export declare class AgentsService {
76
77
  private readonly db;
77
78
  private readonly lookup;
78
79
  private readonly tools;
79
- /** 删除 agent 时取消它待处理的技能改动(T15)。 */
80
+ /**
81
+ * Cancels the agent's pending skill requests: all of them when it is deleted, share requests
82
+ * when its owner changes.
83
+ */
80
84
  private readonly skillChanges;
81
85
  /** The org row lock (orgs/org-lock.ts): joins the caller's transaction. */
82
86
  private readonly orgLock;
@@ -88,10 +92,13 @@ export declare class AgentsService {
88
92
  /** Settles the agent's connector setup request when its pending connectors change (P4-7). */
89
93
  private readonly setupRequests;
90
94
  constructor(db: KernelDb, lookup: AgentLookup, tools: Pick<ToolRegistry, "checkDisabled">,
91
- /** 删除 agent 时取消它待处理的技能改动(T15)。 */
95
+ /**
96
+ * Cancels the agent's pending skill requests: all of them when it is deleted, share requests
97
+ * when its owner changes.
98
+ */
92
99
  skillChanges: Pick<SkillChangesService, "cancelPendingForAgent" | "finishCancel">,
93
100
  /** The org row lock (orgs/org-lock.ts): joins the caller's transaction. */
94
- orgLock: Pick<OrgLock, "lockAndAuthorize">,
101
+ orgLock: Pick<OrgLock, "lockAndAuthorize" | "tryLock">,
95
102
  /** Resolves the agent's models (and their parameter declarations) when it is saved. */
96
103
  providers: Pick<ProviderRegistry, "resolve">, txHost: TransactionHost,
97
104
  /** Cancels the deleted agent's running eval runs (in the delete's transaction). */
@@ -162,6 +169,17 @@ export declare class AgentsService {
162
169
  visibility: AgentVisibility;
163
170
  current_version: number;
164
171
  }>;
172
+ /**
173
+ * Locks the live agent row of `orgId` (the caller holds the org row lock); missing or deleted →
174
+ * `agent.not_found`.
175
+ */
176
+ private lockAgentRow;
177
+ /**
178
+ * The platform path's lock (a super admin who need not be a member of the org): finds the live
179
+ * agent's org, locks the org row, then the agent row. A missing or deleted agent, or one of a
180
+ * deleted org → `agent.not_found`. The super admin check is the caller's (`@SuperAdmin()`).
181
+ */
182
+ private lockForPlatform;
165
183
  /**
166
184
  * 修改 agent:`definition` 变化时写新版本(`current_version + 1`),只改 visibility / memberIds 不产生新版本。
167
185
  * 只有负责人或组织管理员以上可改,在组织行锁内复核。
@@ -183,12 +201,32 @@ export declare class AgentsService {
183
201
  /** 软删除:权限同 update。待处理的技能改动与该 agent 评测集在执行的评测运行一并取消,提交后删除草稿(T15)。 */
184
202
  softDelete(orgId: string, agentId: string, actorAccountId: string, actorTokenId?: string, auditDetail?: Record<string, unknown>): Promise<void>;
185
203
  private softDeleteIn;
204
+ /**
205
+ * The platform's delete (a super admin who need not be a member of the agent's org): the same
206
+ * effects as {@link softDelete}, under the org row lock, audited `agent.deleted` with
207
+ * `detail.via = "platform"`. A missing or deleted agent, or one of a deleted org →
208
+ * `agent.not_found`. `auditDetail` (a Platform tool write's `viaAgent`) is merged into the
209
+ * detail; `via` stays `"platform"`.
210
+ */
211
+ softDeleteAsPlatform(agentId: string, actor: WriteActor, auditDetail?: Record<string, unknown>): Promise<void>;
212
+ private softDeleteAsPlatformIn;
213
+ /** The delete itself; the caller holds the org row and agent row locks. */
214
+ private softDeleteLocked;
186
215
  /**
187
216
  * 重新指派负责人(`agents.manage`,admin):在组织行锁内授权;agent 不存在 / 已删除 → `agent.not_found`;
188
217
  * 新负责人不是该组织成员 → `member.not_found`;是匿名访客 → `member.anonymous_guest`。
189
218
  * 清除 `needs_reassignment`,审计 `agent.owner_changed`。
190
219
  */
191
220
  reassignOwner(orgId: string, agentId: string, newOwnerId: string, actorAccountId: string, actorTokenId?: string): Promise<AgentView>;
221
+ /**
222
+ * The platform's owner transfer (a super admin who need not be a member of the agent's org):
223
+ * the same checks and effects as {@link reassignOwner} (the new owner is a member of the agent's
224
+ * org and not its anonymous guest), audited with `detail.via = "platform"`. A missing or deleted
225
+ * agent, or one of a deleted org → `agent.not_found`.
226
+ */
227
+ reassignOwnerAsPlatform(agentId: string, newOwnerId: string, actor: WriteActor): Promise<AgentView>;
228
+ /** The owner transfer itself; the caller holds the org row and agent row locks. */
229
+ private reassignOwnerLocked;
192
230
  /**
193
231
  * `views` with `createdFrom.available` set (ruling P4-8): whether each recorded template is
194
232
  * still usable in the org. One template read for all of them; only the agents API calls it.
@@ -26,7 +26,7 @@ import { assertOrgSkills, assertPinnedSkills } from "../skills/skills.service.js
26
26
  import { legacyKeySaveText } from "../tools/legacy-keys.js";
27
27
  import { ToolRegistry } from "../tools/registry.js";
28
28
  import { AgentLookup, isAgentVisibleTo, visibleTo, } from "./agent-lookup.js";
29
- import { canonicalDisabledTools, normalizeStoredDefinition, paramIssues, SUBAGENT_NAME_RE, SUBAGENT_ROSTER_MAX, } from "./definition.js";
29
+ import { canonicalDisabledTools, normalizeStoredDefinition, paramIssues, SUBAGENT_NAME_RE, SUBAGENT_ROSTER_MAX, withoutRemovedBuiltinPolicies, } from "./definition.js";
30
30
  import { deriveInstructions } from "./instruction-files.js";
31
31
  import { PendingConnectorRequests } from "./pending-connector-requests.js";
32
32
  import { randomPreset } from "./profile.js";
@@ -275,7 +275,10 @@ let AgentsService = class AgentsService {
275
275
  evalBudget;
276
276
  setupRequests;
277
277
  constructor(db, lookup, tools,
278
- /** 删除 agent 时取消它待处理的技能改动(T15)。 */
278
+ /**
279
+ * Cancels the agent's pending skill requests: all of them when it is deleted, share requests
280
+ * when its owner changes.
281
+ */
279
282
  skillChanges,
280
283
  /** The org row lock (orgs/org-lock.ts): joins the caller's transaction. */
281
284
  orgLock,
@@ -417,11 +420,13 @@ let AgentsService = class AgentsService {
417
420
  return this.createChecked(orgId, actorAccountId, request, template, actorTokenId, auditDetail);
418
421
  }
419
422
  async createChecked(orgId, actorAccountId, request, template, actorTokenId, auditDetail) {
423
+ // a policy key of a removed builtin (e.g. copied from a template) is dropped, never refused
424
+ const given = withoutRemovedBuiltinPolicies(request.definition);
420
425
  const input = {
421
426
  ...request,
422
427
  // run context spec §3.5: version 1 carries the description (the profile's when the
423
428
  // definition gives none)
424
- definition: withCanonicalTools(withDescription(deriveInstructions(request.definition, null), request.definition.description ?? request.profile?.description, undefined)),
429
+ definition: withCanonicalTools(withDescription(deriveInstructions(given, null), given.description ?? request.profile?.description, undefined)),
425
430
  };
426
431
  // 先解析模型(可能访问供应商),不在持有组织行锁时做
427
432
  const warnings = await this.checkModels(orgId, input.definition);
@@ -503,11 +508,29 @@ let AgentsService = class AgentsService {
503
508
  async lockForWrite(orgId, agentId, actorAccountId) {
504
509
  const t = this.db.db;
505
510
  const { role } = await this.orgLock.lockAndAuthorize(orgId, actorAccountId, "agents.create");
511
+ const agent = await this.lockAgentRow(orgId, agentId);
512
+ if (agent.owner_account_id === actorAccountId || roleAtLeast(role, "admin"))
513
+ return agent;
514
+ const visible = await t
515
+ .selectFrom("agents")
516
+ .select("id")
517
+ .where("id", "=", agentId)
518
+ .where(visibleTo(actorAccountId))
519
+ .executeTakeFirst();
520
+ if (!visible)
521
+ throw notFound();
522
+ throw new ApiError("forbidden", "Only the agent owner or an organization admin can modify it");
523
+ }
524
+ /**
525
+ * Locks the live agent row of `orgId` (the caller holds the org row lock); missing or deleted →
526
+ * `agent.not_found`.
527
+ */
528
+ async lockAgentRow(orgId, agentId) {
506
529
  // NO KEY UPDATE: a soft delete then appends revert snapshots to conversations; FOR UPDATE would
507
530
  // block the agent foreign key check of an append already holding one of them (40P01)
508
- const agent = await t
531
+ const agent = await this.db.db
509
532
  .selectFrom("agents")
510
- .select(["owner_account_id", "visibility", "current_version"])
533
+ .select(["owner_account_id", "needs_reassignment", "visibility", "current_version"])
511
534
  .where("id", "=", agentId)
512
535
  .where("org_id", "=", orgId)
513
536
  .where("deleted_at", "is", null)
@@ -515,24 +538,36 @@ let AgentsService = class AgentsService {
515
538
  .executeTakeFirst();
516
539
  if (!agent)
517
540
  throw notFound();
518
- if (agent.owner_account_id === actorAccountId || roleAtLeast(role, "admin"))
519
- return agent;
520
- const visible = await t
541
+ return agent;
542
+ }
543
+ /**
544
+ * The platform path's lock (a super admin who need not be a member of the org): finds the live
545
+ * agent's org, locks the org row, then the agent row. A missing or deleted agent, or one of a
546
+ * deleted org → `agent.not_found`. The super admin check is the caller's (`@SuperAdmin()`).
547
+ */
548
+ async lockForPlatform(agentId) {
549
+ const found = await this.db.db
521
550
  .selectFrom("agents")
522
- .select("id")
551
+ .select("org_id")
523
552
  .where("id", "=", agentId)
524
- .where(visibleTo(actorAccountId))
553
+ .where("deleted_at", "is", null)
525
554
  .executeTakeFirst();
526
- if (!visible)
555
+ if (!found)
527
556
  throw notFound();
528
- throw new ApiError("forbidden", "Only the agent owner or an organization admin can modify it");
557
+ const orgId = found.org_id;
558
+ if (!(await this.orgLock.tryLock(orgId)))
559
+ throw notFound();
560
+ return { orgId, agent: await this.lockAgentRow(orgId, agentId) };
529
561
  }
530
562
  /**
531
563
  * 修改 agent:`definition` 变化时写新版本(`current_version + 1`),只改 visibility / memberIds 不产生新版本。
532
564
  * 只有负责人或组织管理员以上可改,在组织行锁内复核。
533
565
  */
534
566
  async update(orgId, agentId, actorAccountId, request, actorTokenId, auditDetail) {
535
- const patch = request;
567
+ // a policy key of a removed builtin (stored before it went) is dropped, never refused
568
+ const patch = request.definition === undefined
569
+ ? request
570
+ : { ...request, definition: withoutRemovedBuiltinPolicies(request.definition) };
536
571
  const warnings = patch.definition ? await this.checkModels(orgId, patch.definition) : [];
537
572
  const toolKeys = patch.definition
538
573
  ? await this.checkTools(orgId, patch.definition, await this.keptDisabled(orgId, agentId))
@@ -641,8 +676,32 @@ let AgentsService = class AgentsService {
641
676
  await this.skillChanges.finishCancel(orgId, cancelled);
642
677
  }
643
678
  async softDeleteIn(orgId, agentId, actorAccountId, actorTokenId, auditDetail) {
644
- const t = this.db.db;
645
679
  await this.lockForWrite(orgId, agentId, actorAccountId);
680
+ return this.softDeleteLocked(orgId, agentId, { accountId: actorAccountId, ...(actorTokenId !== undefined && { tokenId: actorTokenId }) }, auditDetail);
681
+ }
682
+ /**
683
+ * The platform's delete (a super admin who need not be a member of the agent's org): the same
684
+ * effects as {@link softDelete}, under the org row lock, audited `agent.deleted` with
685
+ * `detail.via = "platform"`. A missing or deleted agent, or one of a deleted org →
686
+ * `agent.not_found`. `auditDetail` (a Platform tool write's `viaAgent`) is merged into the
687
+ * detail; `via` stays `"platform"`.
688
+ */
689
+ async softDeleteAsPlatform(agentId, actor, auditDetail) {
690
+ const { orgId, cancelled } = await this.softDeleteAsPlatformIn(agentId, actor, auditDetail);
691
+ await this.skillChanges.finishCancel(orgId, cancelled);
692
+ }
693
+ async softDeleteAsPlatformIn(agentId, actor, auditDetail) {
694
+ const { orgId } = await this.lockForPlatform(agentId);
695
+ const cancelled = await this.softDeleteLocked(orgId, agentId, actor, {
696
+ ...auditDetail,
697
+ via: "platform",
698
+ });
699
+ return { orgId, cancelled };
700
+ }
701
+ /** The delete itself; the caller holds the org row and agent row locks. */
702
+ async softDeleteLocked(orgId, agentId, actor, auditDetail) {
703
+ const t = this.db.db;
704
+ const { accountId: actorAccountId, tokenId: actorTokenId } = actor;
646
705
  await t
647
706
  .updateTable("agents")
648
707
  .set({ deleted_at: sql `now()`, updated_at: sql `now()` })
@@ -683,18 +742,23 @@ let AgentsService = class AgentsService {
683
742
  * 清除 `needs_reassignment`,审计 `agent.owner_changed`。
684
743
  */
685
744
  async reassignOwner(orgId, agentId, newOwnerId, actorAccountId, actorTokenId) {
686
- const t = this.db.db;
687
745
  await this.orgLock.lockAndAuthorize(orgId, actorAccountId, "agents.manage");
688
- const agent = await t
689
- .selectFrom("agents")
690
- .select(["owner_account_id", "needs_reassignment"])
691
- .where("id", "=", agentId)
692
- .where("org_id", "=", orgId)
693
- .where("deleted_at", "is", null)
694
- .forUpdate()
695
- .executeTakeFirst();
696
- if (!agent)
697
- throw notFound();
746
+ const agent = await this.lockAgentRow(orgId, agentId);
747
+ return this.reassignOwnerLocked(orgId, agentId, agent, newOwnerId, { accountId: actorAccountId, ...(actorTokenId !== undefined && { tokenId: actorTokenId }) }, undefined);
748
+ }
749
+ /**
750
+ * The platform's owner transfer (a super admin who need not be a member of the agent's org):
751
+ * the same checks and effects as {@link reassignOwner} (the new owner is a member of the agent's
752
+ * org and not its anonymous guest), audited with `detail.via = "platform"`. A missing or deleted
753
+ * agent, or one of a deleted org → `agent.not_found`.
754
+ */
755
+ async reassignOwnerAsPlatform(agentId, newOwnerId, actor) {
756
+ const { orgId, agent } = await this.lockForPlatform(agentId);
757
+ return this.reassignOwnerLocked(orgId, agentId, agent, newOwnerId, actor, { via: "platform" });
758
+ }
759
+ /** The owner transfer itself; the caller holds the org row and agent row locks. */
760
+ async reassignOwnerLocked(orgId, agentId, agent, newOwnerId, actor, auditDetail) {
761
+ const t = this.db.db;
698
762
  const member = await t
699
763
  .selectFrom("memberships")
700
764
  .select("role")
@@ -711,9 +775,12 @@ let AgentsService = class AgentsService {
711
775
  .execute();
712
776
  // the new owner hears about the agent's connector setup request (P4-T6-4)
713
777
  await this.setupRequests.retargetIn([agentId], newOwnerId);
778
+ // the previous owner's asks to share the agent's skills lapse (the new owner did not ask);
779
+ // a share request has no draft or trial, so nothing is left to clean up after the commit
780
+ await this.skillChanges.cancelPendingForAgent(orgId, agentId, actor.accountId, "owner_changed", actor.tokenId, ["share"]);
714
781
  await audit(t, {
715
- actorAccountId,
716
- ...(actorTokenId !== undefined && { actorTokenId }),
782
+ actorAccountId: actor.accountId,
783
+ ...(actor.tokenId !== undefined && { actorTokenId: actor.tokenId }),
717
784
  orgId,
718
785
  action: "agent.owner_changed",
719
786
  target: agentId,
@@ -721,6 +788,7 @@ let AgentsService = class AgentsService {
721
788
  from: agent.owner_account_id,
722
789
  to: newOwnerId,
723
790
  neededReassignment: agent.needs_reassignment,
791
+ ...auditDetail,
724
792
  },
725
793
  });
726
794
  return this.lookup.view(agentId);
@@ -789,15 +857,33 @@ __decorate([
789
857
  __decorate([
790
858
  Transactional({ propagation: "mandatory" })
791
859
  ], AgentsService.prototype, "lockForWrite", null);
860
+ __decorate([
861
+ Transactional({ propagation: "mandatory" })
862
+ ], AgentsService.prototype, "lockAgentRow", null);
863
+ __decorate([
864
+ Transactional({ propagation: "mandatory" })
865
+ ], AgentsService.prototype, "lockForPlatform", null);
792
866
  __decorate([
793
867
  Transactional()
794
868
  ], AgentsService.prototype, "updateIn", null);
795
869
  __decorate([
796
870
  Transactional()
797
871
  ], AgentsService.prototype, "softDeleteIn", null);
872
+ __decorate([
873
+ Transactional()
874
+ ], AgentsService.prototype, "softDeleteAsPlatformIn", null);
875
+ __decorate([
876
+ Transactional({ propagation: "mandatory" })
877
+ ], AgentsService.prototype, "softDeleteLocked", null);
798
878
  __decorate([
799
879
  Transactional()
800
880
  ], AgentsService.prototype, "reassignOwner", null);
881
+ __decorate([
882
+ Transactional()
883
+ ], AgentsService.prototype, "reassignOwnerAsPlatform", null);
884
+ __decorate([
885
+ Transactional({ propagation: "mandatory" })
886
+ ], AgentsService.prototype, "reassignOwnerLocked", null);
801
887
  AgentsService = __decorate([
802
888
  Injectable(),
803
889
  __param(0, Inject(KernelDb)),
@@ -287,6 +287,22 @@ interface OwnRefs {
287
287
  * goes through this: a new definition with an old key is refused (`AgentsService`).
288
288
  */
289
289
  export declare function normalizeToolKeys(definition: unknown, scope?: LegacyScope, inherited?: OwnRefs): unknown;
290
+ /** The fields {@link withoutRemovedBuiltinPolicies} reads. */
291
+ interface AgentDefinitionLike {
292
+ toolPolicies?: Record<string, string> | undefined;
293
+ subagents?: {
294
+ inline?: readonly {
295
+ toolPolicies?: Record<string, string> | undefined;
296
+ }[] | undefined;
297
+ } | undefined;
298
+ }
299
+ /**
300
+ * `definition` without the `toolPolicies` keys (its own and its inline sub-agents') of builtins
301
+ * that no longer exist ({@link isRemovedBuiltinKey}): applied to a definition being saved, so a
302
+ * key left over from a removed builtin never blocks the save. Returns the same object when
303
+ * nothing is dropped.
304
+ */
305
+ export declare function withoutRemovedBuiltinPolicies<T extends AgentDefinitionLike>(definition: T): T;
290
306
  /**
291
307
  * Does a stored definition or template hold an rc `<x>__<tool>` key, which only a
292
308
  * {@link LegacyScope} can map? (Callers load the scope only then.)
@@ -1,5 +1,6 @@
1
1
  import { validateParams } from "@capybara-hq/plugin-sdk";
2
2
  import { z } from "zod";
3
+ import { isRemovedBuiltinKey } from "../tools/builtin-names.js";
3
4
  import { isLegacyBuiltinKey, isLegacyPrefixedKey, legacyToolKey, } from "../tools/legacy-keys.js";
4
5
  import { InstructionFilesSchema } from "./instruction-files.js";
5
6
  /** 模型引用:供应商插件名 + 模型 id。 */
@@ -219,8 +220,9 @@ export const AgentDefinitionSchema = z.object({
219
220
  * 技能(M5 T13):`orgSkills` 是引用的组织技能 id(保存时校验属于本组织且未删除,不可重复)。
220
221
  * agent 自己的技能(`scope=agent`)自动对它生效,不进定义。可省略(较早保存的定义也没有),按空处理;
221
222
  * 不给默认值:没用技能的定义保存前后不变,不会因此多出一个版本。
222
- * `writePolicy`(M6 T27,D10):私有 agent 的 `SkillWrite` 是否固定确认,缺省按 `allow`(不写进定义);
223
- * 共享 agent 不论取值都走 R3 + R4。
223
+ * `writePolicy`: whether registering a skill on a private agent (`agents.register-skill`) needs
224
+ * the person's confirmation; absent means `allow` (not stored in the definition). On a shared
225
+ * agent a registration is always confirmed at the org level.
224
226
  */
225
227
  skills: z
226
228
  .object({
@@ -412,6 +414,34 @@ export function normalizeToolKeys(definition, scope, inherited) {
412
414
  }
413
415
  return changed ? next : definition;
414
416
  }
417
+ /**
418
+ * `definition` without the `toolPolicies` keys (its own and its inline sub-agents') of builtins
419
+ * that no longer exist ({@link isRemovedBuiltinKey}): applied to a definition being saved, so a
420
+ * key left over from a removed builtin never blocks the save. Returns the same object when
421
+ * nothing is dropped.
422
+ */
423
+ export function withoutRemovedBuiltinPolicies(definition) {
424
+ const strip = (p) => {
425
+ if (p === undefined || !Object.keys(p).some(isRemovedBuiltinKey))
426
+ return p;
427
+ return Object.fromEntries(Object.entries(p).filter(([k]) => !isRemovedBuiltinKey(k)));
428
+ };
429
+ let next = definition;
430
+ const policies = strip(definition.toolPolicies);
431
+ if (policies !== definition.toolPolicies)
432
+ next = { ...next, toolPolicies: policies };
433
+ const inline = definition.subagents?.inline;
434
+ if (inline !== undefined) {
435
+ const stripped = inline.map((t) => {
436
+ const p = strip(t.toolPolicies);
437
+ return p === t.toolPolicies ? t : { ...t, toolPolicies: p };
438
+ });
439
+ if (stripped.some((t, i) => t !== inline[i])) {
440
+ next = { ...next, subagents: { ...definition.subagents, inline: stripped } };
441
+ }
442
+ }
443
+ return next;
444
+ }
415
445
  /**
416
446
  * Does a stored definition or template hold an rc `<x>__<tool>` key, which only a
417
447
  * {@link LegacyScope} can map? (Callers load the scope only then.)
@@ -23,6 +23,7 @@ import { TransactionHost } from "../infra/database/transaction-host.js";
23
23
  import { Transactional } from "../infra/database/transactional.decorator.js";
24
24
  import { OrgLock } from "../orgs/org-lock.js";
25
25
  import { skillVersionsById } from "../skills/usable.js";
26
+ import { isRemovedBuiltinKey } from "../tools/builtin-names.js";
26
27
  import { canonicalJson } from "../tools/canonical-json.js";
27
28
  import { ToolRegistry } from "../tools/registry.js";
28
29
  import { AgentLookup } from "./agent-lookup.js";
@@ -204,6 +205,9 @@ extensionTools = []) {
204
205
  // is not carried into it
205
206
  if (v === "allow")
206
207
  continue;
208
+ // a removed builtin's policy left in a stored definition is not carried
209
+ if (isRemovedBuiltinKey(k))
210
+ continue;
207
211
  const parsed = parseToolKey(k);
208
212
  if (parsed !== null && parsed.kind !== "connector" && TOOL_KEY_RE.test(k)) {
209
213
  toolPolicies[k] = v;
@@ -33,7 +33,7 @@ export declare const TASK_ID_RE: RegExp;
33
33
  * hand) to `.capybara/tasks/<id>.sh` and starts it with bash (the shell a foreground `Bash` call
34
34
  * runs its command in) in a session of its own under `timeout -s KILL <seconds>`, with its exit
35
35
  * status written to `<id>.exit` and its output to `<id>.out`; writes the session id (`$!`, the pid
36
- * `setsid` started) to `<id>.sid` and prints it. A sandbox without one of the needed programs fails with a readable error on
36
+ * `setsid` started) to `<id>.sid` and prints it. An environment without one of the needed programs fails with a readable error on
37
37
  * stderr and exit code 127, before anything starts. `id` must be a task id and `seconds` a
38
38
  * positive integer (both go into the script unquoted).
39
39
  */
@@ -34,7 +34,7 @@ export function newTaskId() {
34
34
  out += BASE32[b & 31];
35
35
  return out;
36
36
  }
37
- /** The programs the launch and the later polls and stops need inside the sandbox. */
37
+ /** The programs the launch and the later polls and stops need in the environment. */
38
38
  const REQUIRED = ["bash", "setsid", "nohup", "timeout", "pgrep", "pkill", "base64"];
39
39
  /** A task id ({@link newTaskId}). */
40
40
  export const TASK_ID_RE = /^bt_[a-z2-7]{6}$/;
@@ -43,7 +43,7 @@ export const TASK_ID_RE = /^bt_[a-z2-7]{6}$/;
43
43
  * hand) to `.capybara/tasks/<id>.sh` and starts it with bash (the shell a foreground `Bash` call
44
44
  * runs its command in) in a session of its own under `timeout -s KILL <seconds>`, with its exit
45
45
  * status written to `<id>.exit` and its output to `<id>.out`; writes the session id (`$!`, the pid
46
- * `setsid` started) to `<id>.sid` and prints it. A sandbox without one of the needed programs fails with a readable error on
46
+ * `setsid` started) to `<id>.sid` and prints it. An environment without one of the needed programs fails with a readable error on
47
47
  * stderr and exit code 127, before anything starts. `id` must be a task id and `seconds` a
48
48
  * positive integer (both go into the script unquoted).
49
49
  */
@@ -57,7 +57,7 @@ export function launchScript(id, command, seconds) {
57
57
  const t = `${TASKS_DIR}/${id}`;
58
58
  return [
59
59
  "set -e",
60
- `for c in ${REQUIRED.join(" ")}; do command -v "$c" >/dev/null 2>&1 || { echo "Background tasks need ${REQUIRED.join(", ")} in the sandbox; $c is missing." >&2; exit 127; }; done`,
60
+ `for c in ${REQUIRED.join(" ")}; do command -v "$c" >/dev/null 2>&1 || { echo "Background tasks need ${REQUIRED.join(", ")} in this environment; $c is missing." >&2; exit 127; }; done`,
61
61
  `mkdir -p ${TASKS_DIR}`,
62
62
  `printf '%s' '${b64}' | base64 -d > ${t}.sh`,
63
63
  `setsid nohup sh -c 'timeout -s KILL ${seconds} bash ${t}.sh; echo $? > ${t}.exit' > ${t}.out 2>&1 < /dev/null &`,
@@ -24,12 +24,29 @@ export interface BackgroundTaskView {
24
24
  /** True while a stop has claimed the task and it is still running. */
25
25
  stopping: boolean;
26
26
  }
27
+ /**
28
+ * Why a task's output cannot be read live: `starting` (the launch has not recorded the task's
29
+ * session yet), `not_started` (the launch failed: the task never ran, its `lostReason` says why),
30
+ * `device_offline` (the local device's machine is not connected), `suspended` (the instance is
31
+ * suspended; reading never resumes it), `rebuilt` (the instance the task ran in is gone, and its
32
+ * files with it), `output_missing` (the instance runs but the output file is gone), `no_access`
33
+ * (the conversation's owner may no longer use the device), `unavailable` (the device's provider
34
+ * is not loaded), `read_failed` (the read command failed).
35
+ */
36
+ export type OutputUnavailableReason = "starting" | "not_started" | "device_offline" | "suspended" | "rebuilt" | "output_missing" | "no_access" | "unavailable" | "read_failed";
37
+ /** Why the live read failed, and where the task ran: a `local` device or a `cloud` sandbox. */
38
+ export interface OutputUnavailable {
39
+ reason: OutputUnavailableReason;
40
+ environment: "local" | "cloud";
41
+ }
27
42
  export interface BackgroundTaskOutput {
28
43
  text: string;
29
44
  /** True when `text` is only the end of the output (or, for `event`, the last lines). */
30
45
  truncated: boolean;
31
46
  /** `live`: read from the running sandbox; `event`: the last lines the finished event kept. */
32
47
  source: "live" | "event";
48
+ /** With `event`: why the output was not read live. */
49
+ unavailable?: OutputUnavailable;
33
50
  }
34
51
  /**
35
52
  * The HTTP side of background tasks (spec 2026-11-05-background-bash-design §5): the tasks of a
@@ -69,9 +86,24 @@ export declare class BackgroundTasks {
69
86
  private deviceUsable;
70
87
  /**
71
88
  * The end of the task's output, at most `tail` bytes. Read live when the task's device
72
- * instance runs (never resuming a suspended one); a finished task whose sandbox does not run answers
73
- * with the last lines its finished event kept. Anything else is
74
- * `background_task.output_unavailable` (409).
89
+ * instance runs (never resuming a suspended one); a finished task that cannot be read live
90
+ * answers with the last lines its finished event kept and why (`unavailable`). Anything else is
91
+ * `background_task.output_unavailable` (409) with `details: [{ reason, environment }]`.
75
92
  */
76
93
  output(orgId: string, conversationId: string, accountId: string, taskId: string, tail: number): Promise<BackgroundTaskOutput>;
94
+ /** Whether the task ran on a local device (its machine) or in a cloud sandbox. */
95
+ private environmentOf;
96
+ /** Reads the output from the task's running instance, or says why it cannot. */
97
+ private readLive;
77
98
  }
99
+ /**
100
+ * Prints `size <bytes>` and `out <base64 of the last `tail` bytes>`, or `missing` when the output
101
+ * file is not there. Relative to the workspace root; POSIX tools only (`wc -c`, `tail -c`,
102
+ * `base64`, `tr`), so it reads the same on Linux and macOS.
103
+ */
104
+ export declare function tailScript(id: string, tail: number): string;
105
+ /**
106
+ * Decodes {@link tailScript}'s output (`missing`: no output file; null: unreadable); a cut that
107
+ * splits a UTF-8 character drops its tail bytes.
108
+ */
109
+ export declare function parseTail(stdout: string, tail: number): Omit<BackgroundTaskOutput, "source"> | "missing" | null;