@capybara-hq/kernel 0.1.0-rc.26 → 0.1.0-rc.28

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 (88) hide show
  1. package/dist/app.js +2 -0
  2. package/dist/catalog/catalog-content.d.ts +18 -1
  3. package/dist/catalog/catalog-content.js +5 -2
  4. package/dist/catalog/catalog-scope.service.js +3 -2
  5. package/dist/catalog/marketplaces.service.d.ts +1 -1
  6. package/dist/catalog/org-catalog.service.js +13 -3
  7. package/dist/catalog/plugin-install.d.ts +37 -16
  8. package/dist/catalog/plugin-install.js +108 -34
  9. package/dist/catalog/plugin-upgrade.js +1 -0
  10. package/dist/channels/actions.d.ts +4 -1
  11. package/dist/channels/actions.js +5 -2
  12. package/dist/channels/cards.d.ts +7 -1
  13. package/dist/channels/cards.js +28 -1
  14. package/dist/channels/phrases.d.ts +15 -0
  15. package/dist/channels/phrases.js +9 -0
  16. package/dist/config/schema.d.ts +8 -0
  17. package/dist/config/schema.js +69 -0
  18. package/dist/conversations/events.d.ts +14 -0
  19. package/dist/conversations/events.js +20 -0
  20. package/dist/db/kernel-schema.d.ts +19 -4
  21. package/dist/errors.d.ts +5 -0
  22. package/dist/errors.js +5 -0
  23. package/dist/git/gateway.d.ts +93 -0
  24. package/dist/git/gateway.js +750 -0
  25. package/dist/git/gateway.route.d.ts +16 -0
  26. package/dist/git/gateway.route.js +41 -0
  27. package/dist/git/git.module.d.ts +10 -0
  28. package/dist/git/git.module.js +31 -0
  29. package/dist/git/identity.d.ts +80 -0
  30. package/dist/git/identity.js +209 -0
  31. package/dist/git/pkt-line.d.ts +53 -0
  32. package/dist/git/pkt-line.js +146 -0
  33. package/dist/git/run-end.d.ts +29 -0
  34. package/dist/git/run-end.js +121 -0
  35. package/dist/git/sources.d.ts +42 -0
  36. package/dist/git/sources.js +134 -0
  37. package/dist/git/tickets.d.ts +69 -0
  38. package/dist/git/tickets.js +194 -0
  39. package/dist/http/api.module.js +3 -0
  40. package/dist/http/app.d.ts +6 -0
  41. package/dist/http/app.js +3 -0
  42. package/dist/infra/config/kernel-config.service.js +2 -0
  43. package/dist/orgs/settings.service.d.ts +21 -0
  44. package/dist/orgs/settings.service.js +18 -0
  45. package/dist/overrides.d.ts +6 -0
  46. package/dist/overrides.js +4 -0
  47. package/dist/platform/exclusions.js +3 -0
  48. package/dist/runtime/run-finish.d.ts +5 -0
  49. package/dist/runtime/run-finish.js +12 -3
  50. package/dist/runtime/runtime.module.js +2 -0
  51. package/dist/runtime/waits.js +7 -5
  52. package/dist/schedules/fire.d.ts +3 -3
  53. package/dist/schedules/fire.js +7 -4
  54. package/dist/schedules/grant-rules.d.ts +40 -0
  55. package/dist/schedules/grant-rules.js +83 -0
  56. package/dist/schedules/scheduled-tasks.service.d.ts +9 -7
  57. package/dist/schedules/scheduled-tasks.service.js +50 -15
  58. package/dist/schedules/types.d.ts +2 -0
  59. package/dist/tools/approvals.service.d.ts +100 -19
  60. package/dist/tools/approvals.service.js +288 -21
  61. package/dist/tools/builtin-names.d.ts +4 -2
  62. package/dist/tools/builtin-names.js +6 -1
  63. package/dist/tools/builtins/schedules.d.ts +11 -9
  64. package/dist/tools/builtins/schedules.js +202 -44
  65. package/dist/tools/connector-account-policies.d.ts +8 -0
  66. package/dist/tools/connector-account-policies.js +8 -0
  67. package/dist/tools/connectors.controller.d.ts +2 -0
  68. package/dist/tools/connectors.controller.js +5 -1
  69. package/dist/tools/connectors.service.d.ts +46 -3
  70. package/dist/tools/connectors.service.js +98 -9
  71. package/dist/tools/credentials.d.ts +18 -0
  72. package/dist/tools/credentials.js +60 -0
  73. package/dist/tools/credentials.module.d.ts +7 -0
  74. package/dist/tools/credentials.module.js +22 -0
  75. package/dist/tools/git-asks.d.ts +113 -0
  76. package/dist/tools/git-asks.js +174 -0
  77. package/dist/tools/git-push-policy.d.ts +89 -0
  78. package/dist/tools/git-push-policy.js +175 -0
  79. package/dist/tools/info.d.ts +3 -3
  80. package/dist/tools/info.js +3 -3
  81. package/dist/tools/network-asks.d.ts +5 -0
  82. package/dist/tools/network-asks.js +9 -2
  83. package/dist/tools/tools.module.js +4 -3
  84. package/package.json +3 -3
  85. package/src/migrations/0126_git_gateway.sql +10 -0
  86. package/src/migrations/0127_git_push_policy.sql +12 -0
  87. package/src/migrations/0128_scheduled_task_plugin_disabled.sql +10 -0
  88. package/src/migrations/0129_connector_plugin_disabled.sql +5 -0
package/dist/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import "reflect-metadata";
2
2
  import { NestFactory } from "@nestjs/core";
3
+ import { GitGateway } from "./git/gateway.js";
3
4
  import { createKernelAdapter } from "./http/adapter.js";
4
5
  import { createHttpApp } from "./http/app.js";
5
6
  import { NestLogAdapter } from "./http/nest-logger.js";
@@ -26,6 +27,7 @@ export async function buildKernelApp(config, options = {}) {
26
27
  log: logger,
27
28
  servePlugins: config.roles.includes("api") || config.roles.includes("gateway"),
28
29
  ...(gateway !== undefined && { gateway }),
30
+ ...(config.roles.includes("api") && { git: app.get(GitGateway, { strict: false }) }),
29
31
  replicaId: config.http.replicaId === undefined ? undefined : String(config.http.replicaId),
30
32
  packageMaxBytes: config.packages.maxBytes,
31
33
  }, app));
@@ -77,6 +77,19 @@ export interface OrgCatalogTemplate {
77
77
  * install wizard does not ask for it (an org admin sets it up when switching it on).
78
78
  */
79
79
  defaultOff: boolean;
80
+ /**
81
+ * On for this org if the plugin is installed (or wanted on by its scope): the template's own
82
+ * switch, its scope and `defaultEnabled` evaluated with the plugin installed, so a default-on
83
+ * template of a plugin this org has not installed reads true too (the install creates its
84
+ * connector).
85
+ */
86
+ on: boolean;
87
+ /**
88
+ * The org already has a live connector made from this template (a switch-off kept it because an
89
+ * agent uses it): the install takes that connector back instead of creating one, so it does not
90
+ * ask for the template's input.
91
+ */
92
+ connected: boolean;
80
93
  /**
81
94
  * The template's setup guide in the item's latest version (`GET …/catalog/:itemId/guides/
82
95
  * :guideId`), null without one (spec 2026-11-09-setup-guides-v2-design.md §9.3).
@@ -118,7 +131,11 @@ export interface OrgCatalogDetail {
118
131
  * A connector template as an org sees it (the install wizard and the upgrade preview's inputs):
119
132
  * presets and editable paths, no credentials.
120
133
  */
121
- export declare function orgTemplateView(id: string, file: ConnectorTemplateFile, guide?: OrgCatalogTemplate["guide"]): OrgCatalogTemplate;
134
+ export declare function orgTemplateView(id: string, file: ConnectorTemplateFile, o?: {
135
+ guide?: OrgCatalogTemplate["guide"];
136
+ on?: boolean;
137
+ connected?: boolean;
138
+ }): OrgCatalogTemplate;
122
139
  /** An item's icon (the latest version's plugin icon). */
123
140
  export interface CatalogIcon {
124
141
  data: Uint8Array;
@@ -16,7 +16,7 @@ import { satisfiesRange } from "./versions.js";
16
16
  * A connector template as an org sees it (the install wizard and the upgrade preview's inputs):
17
17
  * presets and editable paths, no credentials.
18
18
  */
19
- export function orgTemplateView(id, file, guide = null) {
19
+ export function orgTemplateView(id, file, o = {}) {
20
20
  const t = file.template;
21
21
  return {
22
22
  id,
@@ -30,7 +30,9 @@ export function orgTemplateView(id, file, guide = null) {
30
30
  ownership: t.ownership ??
31
31
  (typeof t.auth.ownership === "string" ? t.auth.ownership : null),
32
32
  defaultOff: file.defaultEnabled === false,
33
- guide,
33
+ on: o.on ?? true,
34
+ connected: o.connected ?? false,
35
+ guide: o.guide ?? null,
34
36
  };
35
37
  }
36
38
  const iconNotFound = () => new ApiError("catalog.icon_not_found", "This catalog item has no icon");
@@ -265,6 +267,7 @@ export function templateConnectorInput(deps, template, ref, input, defaultName,
265
267
  endpoint,
266
268
  auth,
267
269
  ...(credential !== undefined && { credential }),
270
+ ...(template.git !== undefined && { git: template.git }),
268
271
  template: {
269
272
  ...ref,
270
273
  ...(template.defaultToolPolicies !== undefined && {
@@ -1067,8 +1067,9 @@ let CatalogScopeService = class CatalogScopeService {
1067
1067
  orgId: org.id,
1068
1068
  orgName: org.name,
1069
1069
  agents: p.agents.map((a) => ({ id: a.id, name: a.name })),
1070
- // The sync's uninstall keeps the connectors the install created as the org's own
1071
- connectorsKept: p.components.filter((c) => c.kind === "connector" && c.created).length,
1070
+ // A switch-off keeps every connector recorded on the install but disables it with the
1071
+ // plugin (spec §5 rule 8); reused ones (created: false) are disabled too
1072
+ connectorsKept: p.components.filter((c) => c.kind === "connector").length,
1072
1073
  });
1073
1074
  continue;
1074
1075
  }
@@ -188,7 +188,7 @@ export declare function headsToResolve(entries: readonly MarketplaceEntry[], imp
188
188
  /** The host versions (an entry's `minKernel` is judged against them). */
189
189
  export declare const HOST_VERSIONS: {
190
190
  readonly kernel: string;
191
- readonly sdk: "0.1.0-rc.20";
191
+ readonly sdk: "0.1.0-rc.21";
192
192
  };
193
193
  /** Whether this deployment satisfies the entry's `minKernel`. */
194
194
  export declare const entryCompatible: (e: Pick<MarketplaceEntry, "minKernel">) => boolean;
@@ -273,11 +273,17 @@ let OrgCatalogService = class OrgCatalogService {
273
273
  throw itemNotFound();
274
274
  const m = v.manifest;
275
275
  const templates = await readCatalogTemplates(this.storage, v);
276
- // where each template's page finds its setup guide (spec 2026-11-09-setup-guides-v2 §9.3)
276
+ // Which templates are on in this org now: the install asks for the input of those, a
277
+ // default-off one an admin switched on included
278
+ const on = await connectorTemplatesOn(this.db, orgId, itemId, templates.map((t) => t.id));
279
+ // Where each template's page finds its setup guide (spec 2026-11-09-setup-guides-v2 §9.3)
277
280
  const guideOf = (templateId) => {
278
281
  const g = templateGuideOf(m, templateId);
279
282
  return g ? { itemId, guideId: g.id } : null;
280
283
  };
284
+ // A template whose connector an earlier switch-off kept: the install reuses it, so the wizard
285
+ // does not ask for its input
286
+ const connected = await this.pluginInstaller.connectedTemplates(orgId, itemId);
281
287
  // Providers the install would switch on first (a refusal is the install's to report)
282
288
  const implied = await this.pluginInstaller
283
289
  .impliedProviders(orgId, m, templates, m.dependencies ?? [])
@@ -290,7 +296,11 @@ let OrgCatalogService = class OrgCatalogService {
290
296
  item,
291
297
  hasIcon: m.icon !== undefined,
292
298
  extension: m.extension ? m.name : null,
293
- templates: templates.map((t) => orgTemplateView(t.id, t.file, guideOf(t.id))),
299
+ templates: templates.map((t) => orgTemplateView(t.id, t.file, {
300
+ guide: guideOf(t.id),
301
+ on: on.has(t.id),
302
+ connected: connected.has(t.id),
303
+ })),
294
304
  dependencies: [
295
305
  ...(m.dependencies ?? []).map((d) => ({ name: d.name, range: d.version })),
296
306
  ...implied.map((d) => ({ name: d.name, range: d.range, implied: true })),
@@ -298,7 +308,7 @@ let OrgCatalogService = class OrgCatalogService {
298
308
  skills: (m.skills ?? []).map((s) => packagePath(s.path)),
299
309
  agents: (m.agents ?? []).map((a) => packagePath(a.path)),
300
310
  missingTemplates: item.installed !== null && item.state === "needs_config"
301
- ? ((await this.pluginInstaller.missingConnectors(orgId, itemId))?.templates ?? []).map((t) => orgTemplateView(t.id, t.file, guideOf(t.id)))
311
+ ? ((await this.pluginInstaller.missingConnectors(orgId, itemId))?.templates ?? []).map((t) => orgTemplateView(t.id, t.file, { guide: guideOf(t.id), on: true }))
302
312
  : [],
303
313
  };
304
314
  }
@@ -56,16 +56,18 @@ export interface UninstallReport {
56
56
  /** A dependency install still wanted on in the org by its own scope: it stays, standalone. */
57
57
  | "wanted_by_scope"
58
58
  /**
59
- * A connector the install created, kept by a scope sync uninstall (the plugin disabled or its
60
- * scope narrowed, spec §5 rule 8): it stays as the org's own, with its credentials.
59
+ * A connector recorded on the install (created there, or taken back from an earlier
60
+ * switch-off): kept with its endpoint and credentials but switched off with the plugin
61
+ * (spec §5 rule 8); switching the plugin back on switches it back on.
61
62
  */
62
- | "org_owned";
63
+ | "disabled_with_plugin";
63
64
  }[];
64
65
  }
65
66
  /**
66
67
  * What switching a plugin off would do, read without changing anything: its components, the ones the
67
- * uninstall rules would keep for a reason visible beforehand (a component that pre-existed, or one an
68
- * agent uses), and the agents that use the plugin's skills, connectors or agent templates.
68
+ * uninstall rules would keep for a reason visible beforehand (a component that pre-existed, one an
69
+ * agent uses, a connector kept but switched off with the plugin), and the agents that use the
70
+ * plugin's skills, connectors or agent templates.
69
71
  */
70
72
  export interface UninstallPreview {
71
73
  installId: string;
@@ -211,7 +213,7 @@ export interface UninstallCtx {
211
213
  actorTokenId?: string;
212
214
  auditDetail?: AuditDetail;
213
215
  };
214
- /** Uninstalled by the plugin scope sync: the connectors it created stay as the org's own. */
216
+ /** Uninstalled by the plugin scope sync (a deployment-level disable or a narrowed scope). */
215
217
  via?: "scope";
216
218
  }
217
219
  /**
@@ -434,7 +436,9 @@ export declare class PluginInstaller {
434
436
  * lock, one transaction). Removes only components with created=true:
435
437
  * - a skill is kept when an agent names it (an inline sub-agent template's `skills`;
436
438
  * used_by_agents) or another plugin install in this org uses it too (used_by_other_install);
437
- * - a connector is kept when an agent references it (used_by_agents);
439
+ * - a connector recorded on the install (created there, or taken back from an earlier switch-off)
440
+ * is kept with its endpoint and credentials but switched off with the plugin
441
+ * (disabled_with_plugin, spec §5 rule 8); switching the plugin back on switches it back on;
438
442
  * - an extension is kept when another plugin install uses it too (used_by_other_install), a
439
443
  * connector still uses a connector it provides (used_by_connectors), or another enabled
440
444
  * extension depends on it (used_by_plugins);
@@ -462,7 +466,8 @@ export declare class PluginInstaller {
462
466
  * The uninstall's one transaction; the extensions it disabled are notified once it commits. A
463
467
  * scope sync uninstall (`via: "scope"`) re-checks under the org row lock that the item is still
464
468
  * not wanted in the org and that no other live install depends on it ({@link ScopeStillWanted},
465
- * nothing written), and keeps the connectors it created as the org's own (`org_owned`).
469
+ * nothing written). Connectors recorded on the install are kept but switched off with the plugin
470
+ * (spec §5 rule 8), whichever uninstall path leads here.
466
471
  */
467
472
  private writeUninstall;
468
473
  /**
@@ -487,12 +492,12 @@ export declare class PluginInstaller {
487
492
  */
488
493
  uninstallIn(orgId: string, actor: string | null, installId: string, ctx: UninstallCtx): Promise<"missing" | "kept" | "removed" | "wanted_by_scope">;
489
494
  /**
490
- * The uninstall rules for one component an install created (a skill, connector or extension;
491
- * dependency installs recurse through {@link uninstallIn}), in the caller's transaction: kept
492
- * when an agent uses the skill / connector (`used_by_agents`), when another live install outside
493
- * `removing` has it (`used_by_other_install`, ownership handed over to the oldest one, so the
494
- * uninstall order doesn't change the result), when a connector still uses the extension's
495
- * connector kind (`used_by_connectors`) or another enabled extension depends on it
495
+ * The uninstall rules for one component an install created, in the caller's transaction (see
496
+ * {@link uninstall}); connectors go through the switch-off branch in {@link uninstallIn} instead,
497
+ * except in a rollback. Kept when an agent uses the skill (`used_by_agents`), when another live
498
+ * install outside `removing` has it (`used_by_other_install`, ownership handed over to the oldest
499
+ * one, so the uninstall order doesn't change the result), when a connector still uses the
500
+ * extension's connector kind (`used_by_connectors`) or another enabled extension depends on it
496
501
  * (`used_by_plugins`); otherwise removed (`removed`, or `gone` when it no longer existed). Used
497
502
  * by uninstall and by an upgrade's removals.
498
503
  */
@@ -540,6 +545,20 @@ export declare class PluginInstaller {
540
545
  file: ConnectorTemplateFile;
541
546
  }[];
542
547
  } | undefined>;
548
+ /**
549
+ * The org's live connectors made from one of the item's connector templates, oldest first
550
+ * (`templateId` picks one): an uninstall keeps a connector its agents use, and the plugin's next
551
+ * switch-on reuses it.
552
+ */
553
+ private liveTemplateConnectors;
554
+ /** {@link liveTemplateConnectors} for one template. */
555
+ private liveTemplateConnector;
556
+ /**
557
+ * The org's connector templates it already has a live connector for, by template id: the install
558
+ * takes that connector back instead of creating one, so the org item detail tells the wizard not
559
+ * to ask for their input (the same join as {@link liveTemplateConnector}).
560
+ */
561
+ connectedTemplates(orgId: string, itemId: string): Promise<Set<string>>;
543
562
  /**
544
563
  * Creates the connectors an installed plugin is missing (ruling P2-10, `POST …/catalog/:itemId/
545
564
  * connectors`): `input.connectors` names some of {@link missingConnectors}' templates with the
@@ -590,8 +609,10 @@ export declare class PluginInstaller {
590
609
  * skills and agent templates are platform components (phase 2): the ones on in the org through
591
610
  * the plugin (`follow`) go off with it, so the agents using them (by the platform id or an
592
611
  * unpinned org copy linked to it) are listed, also for the dependency installs it created that
593
- * go with it; ledger rows of earlier installs' org copies are not removed and not listed (an
594
- * uninstall keeps legacy copies, like the platform update keeps copies of removed components).
612
+ * go with it; the connectors recorded on it are listed as kept but disabled with it
613
+ * (`disabled_with_plugin`) and their users as affected. Ledger rows of earlier installs' org
614
+ * copies are not removed and not listed (an uninstall keeps legacy copies, like the platform
615
+ * update keeps copies of removed components).
595
616
  */
596
617
  previewUninstall(orgId: string, installId: string): Promise<UninstallPreview>;
597
618
  /**
@@ -356,8 +356,11 @@ let PluginInstaller = class PluginInstaller {
356
356
  catch (e) {
357
357
  if (!(e instanceof InstallFailure))
358
358
  throw e;
359
- // A dependency another install is still installing: retryable, a 409 as it is
360
- if (e.cause instanceof ApiError && e.cause.code === "plugin.install_in_progress") {
359
+ // Retryable: a dependency another install is still installing, or the connector this install
360
+ // would reuse went away meanwhile: a 409 as it is, with the message saying what to do
361
+ if (e.cause instanceof ApiError &&
362
+ (e.cause.code === "plugin.install_in_progress" ||
363
+ e.cause.code === "catalog.connector_removed")) {
361
364
  throw e.cause;
362
365
  }
363
366
  const dep = e.dependency.length > 0 ? ` in dependency ${e.dependency.join(" > ")}` : "";
@@ -484,6 +487,10 @@ let PluginInstaller = class PluginInstaller {
484
487
  // The kernel installs only what needs no org input (the scope sync's `needs_config` rule)
485
488
  if (actor === null)
486
489
  throw systemNeedsInput(manifest.name);
490
+ // An earlier switch-off kept this template's connector: the install takes it back, so its
491
+ // input is not asked for here (the connector keeps its endpoint and credentials)
492
+ if ((await this.liveTemplateConnector(orgId, itemId, ct.id)) !== undefined)
493
+ continue;
487
494
  connectors.set(ct.id, this.planConnector(actor, { itemId, version, name: manifest.name }, ct, input, at, approvedBy));
488
495
  }
489
496
  }
@@ -743,9 +750,29 @@ let PluginInstaller = class PluginInstaller {
743
750
  }
744
751
  step = "connectors";
745
752
  for (const ct of plan.templates) {
753
+ // An earlier switch-off kept this template's connector (its agents use it): the plugin
754
+ // takes it back instead of creating a second one, whose default slug the first holds
755
+ const existing = await this.liveTemplateConnector(orgId, itemId, ct.id);
756
+ if (existing !== undefined) {
757
+ // Switching the plugin back on switches back on the connectors it switched off; one an
758
+ // admin disabled by hand stays off
759
+ await this.connectors.enableIfPluginDisabledIn(orgId, existing, itemId);
760
+ await record({
761
+ kind: "connector",
762
+ ref: existing,
763
+ created: false,
764
+ skillVersion: null,
765
+ templateId: ct.id,
766
+ });
767
+ continue;
768
+ }
746
769
  const ci = plan.connectors.get(ct.id);
747
- if (!ci)
748
- throw new ApiError("internal", `Connector template ${ct.id} was not planned`);
770
+ if (!ci) {
771
+ // The plan skipped this template's input because a live connector existed, and it went
772
+ // away since (deleted between planning and this step): fail honestly, so the person
773
+ // retries and the new plan asks for the input
774
+ throw new ApiError("catalog.connector_removed", `The connector this install would reuse for connector template ${ct.id} was removed meanwhile; start the install again`);
775
+ }
749
776
  if (actor === null)
750
777
  throw systemNeedsInput(manifest.name);
751
778
  const r = await this.connectors.create(orgId, actor, ci, opts.actorTokenId);
@@ -1069,7 +1096,9 @@ let PluginInstaller = class PluginInstaller {
1069
1096
  * lock, one transaction). Removes only components with created=true:
1070
1097
  * - a skill is kept when an agent names it (an inline sub-agent template's `skills`;
1071
1098
  * used_by_agents) or another plugin install in this org uses it too (used_by_other_install);
1072
- * - a connector is kept when an agent references it (used_by_agents);
1099
+ * - a connector recorded on the install (created there, or taken back from an earlier switch-off)
1100
+ * is kept with its endpoint and credentials but switched off with the plugin
1101
+ * (disabled_with_plugin, spec §5 rule 8); switching the plugin back on switches it back on;
1073
1102
  * - an extension is kept when another plugin install uses it too (used_by_other_install), a
1074
1103
  * connector still uses a connector it provides (used_by_connectors), or another enabled
1075
1104
  * extension depends on it (used_by_plugins);
@@ -1095,7 +1124,8 @@ let PluginInstaller = class PluginInstaller {
1095
1124
  * The uninstall's one transaction; the extensions it disabled are notified once it commits. A
1096
1125
  * scope sync uninstall (`via: "scope"`) re-checks under the org row lock that the item is still
1097
1126
  * not wanted in the org and that no other live install depends on it ({@link ScopeStillWanted},
1098
- * nothing written), and keeps the connectors it created as the org's own (`org_owned`).
1127
+ * nothing written). Connectors recorded on the install are kept but switched off with the plugin
1128
+ * (spec §5 rule 8), whichever uninstall path leads here.
1099
1129
  */
1100
1130
  async writeUninstall(orgId, actor, installId, opts, sw) {
1101
1131
  const { via, ...actorOpts } = opts;
@@ -1296,6 +1326,11 @@ let PluginInstaller = class PluginInstaller {
1296
1326
  // failed (already rolled back) and a stale installing (a replica crashed midway) are cleaned up
1297
1327
  // as a rollback
1298
1328
  const rollbackMode = row.status !== "installed" && row.status !== "upgrading";
1329
+ // A dependency install removed by a failed install's (or a crashed upgrade's) rollback removes
1330
+ // what it created, like the rolled-back install itself: it is a rollback, not a switch-off
1331
+ const connectorRollback = rollbackMode ||
1332
+ ctx.reason === "plugin_install_rollback" ||
1333
+ ctx.reason === "plugin_upgrade_cleanup";
1299
1334
  const comps = await t
1300
1335
  .selectFrom("plugin_install_components")
1301
1336
  .select(["kind", "ref", "created", "skill_version"])
@@ -1338,6 +1373,15 @@ let PluginInstaller = class PluginInstaller {
1338
1373
  created = now.created;
1339
1374
  }
1340
1375
  const comp = { kind: c.kind, ref: c.ref, created, skillVersion: c.skill_version };
1376
+ // A connector of a switch-off is kept with its endpoint and credentials but switched off
1377
+ // with the plugin (spec §5 rule 8), whether this install created it or took it back from an
1378
+ // earlier switch-off; switching the plugin back on re-enables it. A rollback keeps removing
1379
+ // what the install created (below).
1380
+ if (c.kind === "connector" && !connectorRollback) {
1381
+ await this.connectors.disableWithPluginIn(orgId, c.ref, row.item_id);
1382
+ keep("disabled_with_plugin");
1383
+ continue;
1384
+ }
1341
1385
  // Skills and agent templates of earlier installs (org copies, now linked to the platform
1342
1386
  // components, phase 2) stay: the plugin's components go off in the org with the install
1343
1387
  if ((c.kind === "skill" || c.kind === "agent_template") && !rollbackMode)
@@ -1365,12 +1409,8 @@ let PluginInstaller = class PluginInstaller {
1365
1409
  keep("wanted_by_scope");
1366
1410
  continue;
1367
1411
  }
1368
- // A scope sync uninstall leaves the connectors to the org (spec §5 rule 8): the component row
1369
- // goes with the install row, so nothing removes them later
1370
- if (c.kind === "connector" && ctx.via === "scope" && !rollbackMode) {
1371
- keep("org_owned");
1372
- continue;
1373
- }
1412
+ // A scope sync uninstall is a switch-off too: its connectors are disabled above with the
1413
+ // plugin, like an org admin's switch-off.
1374
1414
  const reason = rollbackMode ? "plugin_install_cleanup" : (ctx.reason ?? "plugin_uninstall");
1375
1415
  const outcome = await this.decideRemovalIn(orgId, actor, comp, removing, agentRefs, reason);
1376
1416
  if (outcome === "removed")
@@ -1400,12 +1440,12 @@ let PluginInstaller = class PluginInstaller {
1400
1440
  return "removed";
1401
1441
  }
1402
1442
  /**
1403
- * The uninstall rules for one component an install created (a skill, connector or extension;
1404
- * dependency installs recurse through {@link uninstallIn}), in the caller's transaction: kept
1405
- * when an agent uses the skill / connector (`used_by_agents`), when another live install outside
1406
- * `removing` has it (`used_by_other_install`, ownership handed over to the oldest one, so the
1407
- * uninstall order doesn't change the result), when a connector still uses the extension's
1408
- * connector kind (`used_by_connectors`) or another enabled extension depends on it
1443
+ * The uninstall rules for one component an install created, in the caller's transaction (see
1444
+ * {@link uninstall}); connectors go through the switch-off branch in {@link uninstallIn} instead,
1445
+ * except in a rollback. Kept when an agent uses the skill (`used_by_agents`), when another live
1446
+ * install outside `removing` has it (`used_by_other_install`, ownership handed over to the oldest
1447
+ * one, so the uninstall order doesn't change the result), when a connector still uses the
1448
+ * extension's connector kind (`used_by_connectors`) or another enabled extension depends on it
1409
1449
  * (`used_by_plugins`); otherwise removed (`removed`, or `gone` when it no longer existed). Used
1410
1450
  * by uninstall and by an upgrade's removals.
1411
1451
  */
@@ -1657,6 +1697,38 @@ let PluginInstaller = class PluginInstaller {
1657
1697
  templates: all.filter((t) => on.has(t.id) && !made.has(t.id)),
1658
1698
  };
1659
1699
  }
1700
+ /**
1701
+ * The org's live connectors made from one of the item's connector templates, oldest first
1702
+ * (`templateId` picks one): an uninstall keeps a connector its agents use, and the plugin's next
1703
+ * switch-on reuses it.
1704
+ */
1705
+ async liveTemplateConnectors(orgId, itemId, templateId) {
1706
+ let q = this.db
1707
+ .selectFrom("connectors as c")
1708
+ .innerJoin("platform_connector_templates as p", "p.id", "c.platform_template_id")
1709
+ .select(["p.template_id", "c.id"])
1710
+ .where("c.org_id", "=", orgId)
1711
+ .where("c.deleted_at", "is", null)
1712
+ .where("p.catalog_item_id", "=", itemId);
1713
+ if (templateId !== undefined)
1714
+ q = q.where("p.template_id", "=", templateId);
1715
+ const rows = await q.orderBy("c.created_at").orderBy("c.id").execute();
1716
+ return rows.map((r) => ({ templateId: r.template_id, connectorId: r.id }));
1717
+ }
1718
+ /** {@link liveTemplateConnectors} for one template. */
1719
+ async liveTemplateConnector(orgId, itemId, templateId) {
1720
+ const [row] = await this.liveTemplateConnectors(orgId, itemId, templateId);
1721
+ return row?.connectorId;
1722
+ }
1723
+ /**
1724
+ * The org's connector templates it already has a live connector for, by template id: the install
1725
+ * takes that connector back instead of creating one, so the org item detail tells the wizard not
1726
+ * to ask for their input (the same join as {@link liveTemplateConnector}).
1727
+ */
1728
+ async connectedTemplates(orgId, itemId) {
1729
+ const rows = await this.liveTemplateConnectors(orgId, itemId);
1730
+ return new Set(rows.map((r) => r.templateId));
1731
+ }
1660
1732
  /**
1661
1733
  * Creates the connectors an installed plugin is missing (ruling P2-10, `POST …/catalog/:itemId/
1662
1734
  * connectors`): `input.connectors` names some of {@link missingConnectors}' templates with the
@@ -1851,8 +1923,10 @@ let PluginInstaller = class PluginInstaller {
1851
1923
  * skills and agent templates are platform components (phase 2): the ones on in the org through
1852
1924
  * the plugin (`follow`) go off with it, so the agents using them (by the platform id or an
1853
1925
  * unpinned org copy linked to it) are listed, also for the dependency installs it created that
1854
- * go with it; ledger rows of earlier installs' org copies are not removed and not listed (an
1855
- * uninstall keeps legacy copies, like the platform update keeps copies of removed components).
1926
+ * go with it; the connectors recorded on it are listed as kept but disabled with it
1927
+ * (`disabled_with_plugin`) and their users as affected. Ledger rows of earlier installs' org
1928
+ * copies are not removed and not listed (an uninstall keeps legacy copies, like the platform
1929
+ * update keeps copies of removed components).
1856
1930
  */
1857
1931
  async previewUninstall(orgId, installId) {
1858
1932
  if (!UUID_RE.test(installId))
@@ -1885,13 +1959,23 @@ let PluginInstaller = class PluginInstaller {
1885
1959
  };
1886
1960
  const kept = [];
1887
1961
  /**
1888
- * The agents using what goes with an install: connectors it created (they stay, kept for the
1889
- * agents) and the plugin's platform skills and agent templates on in the org through it
1890
- * (`follow`; by the platform id or an unpinned org copy linked to it). Dependency installs it
1891
- * created go with it unless their own scope wants them (Task 7: their components count too).
1962
+ * The agents using what goes with an install: the connectors recorded on it (they are kept
1963
+ * but disabled with the plugin, so their tools stop) and the plugin's platform skills and
1964
+ * agent templates on in the org through it (`follow`; by the platform id or an unpinned org
1965
+ * copy linked to it). Dependency installs it created go with it unless their own scope wants
1966
+ * them (Task 7: their components count too).
1892
1967
  */
1893
1968
  const visit = async (itemId, rows, top, seen) => {
1894
1969
  for (const c of rows) {
1970
+ // Every connector of the install, created here or taken back, is kept and disabled with
1971
+ // the plugin (a rollback's preview is not shown): it is not removed, and its users lose it
1972
+ if (c.kind === "connector") {
1973
+ if (top)
1974
+ kept.push({ kind: c.kind, ref: c.ref, reason: "disabled_with_plugin" });
1975
+ for (const a of agents.filter((x) => x.connectors.has(c.ref)))
1976
+ use(a, c.kind, c.ref);
1977
+ continue;
1978
+ }
1895
1979
  if (!c.created) {
1896
1980
  if (top)
1897
1981
  kept.push({ kind: c.kind, ref: c.ref, reason: "pre_existing" });
@@ -1916,17 +2000,7 @@ let PluginInstaller = class PluginInstaller {
1916
2000
  seen.add(c.ref);
1917
2001
  const sub = (await ledger(c.ref)).filter((x) => x.kind !== "skill" && x.kind !== "agent_template");
1918
2002
  await visit(dep.item_id, sub, false, seen);
1919
- continue;
1920
2003
  }
1921
- if (c.kind !== "connector")
1922
- continue;
1923
- const users = agents.filter((a) => a.connectors.has(c.ref));
1924
- if (users.length === 0)
1925
- continue;
1926
- if (top)
1927
- kept.push({ kind: c.kind, ref: c.ref, reason: "used_by_agents" });
1928
- for (const a of users)
1929
- use(a, c.kind, c.ref);
1930
2004
  }
1931
2005
  for (const kind of ["skill", "agent_template"]) {
1932
2006
  const platform = await loadComponents(t, kind, { itemId });
@@ -689,6 +689,7 @@ let PlatformPluginUpdater = class PlatformPluginUpdater {
689
689
  const input = {
690
690
  endpoint,
691
691
  auth: template.auth,
692
+ ...(template.git !== undefined && { git: template.git }),
692
693
  template: {
693
694
  itemId,
694
695
  version,
@@ -35,7 +35,10 @@ export declare class ChannelCardActions {
35
35
  private readonly deps;
36
36
  constructor(deps: ChannelCardActionsDeps);
37
37
  handle(connectionId: string, e: CardActionEvent): Promise<void>;
38
- /** Whether the approval `id` of the org is a network ask. */
38
+ /**
39
+ * Whether the approval `id` of the org is a network or git push ask (each the asked person's
40
+ * own, decided by them in person).
41
+ */
39
42
  private isNetworkAsk;
40
43
  }
41
44
  export {};
@@ -130,7 +130,10 @@ export class ChannelCardActions {
130
130
  // row is the child's, which has no delivery of its own
131
131
  await this.deps.outbound.enqueue(card.conversation_id);
132
132
  }
133
- /** Whether the approval `id` of the org is a network ask. */
133
+ /**
134
+ * Whether the approval `id` of the org is a network or git push ask (each the asked person's
135
+ * own, decided by them in person).
136
+ */
134
137
  async isNetworkAsk(orgId, id) {
135
138
  const r = await this.deps.db
136
139
  .selectFrom("approvals")
@@ -138,6 +141,6 @@ export class ChannelCardActions {
138
141
  .where("id", "=", id)
139
142
  .where("org_id", "=", orgId)
140
143
  .executeTakeFirst();
141
- return r?.kind === "network";
144
+ return r?.kind === "network" || r?.kind === "git";
142
145
  }
143
146
  }
@@ -1,6 +1,6 @@
1
1
  import { type ChannelCard, type FormAnswer, type InputForm, type OutboundMessage } from "@capybara-hq/plugin-sdk";
2
2
  import type { KernelSchemaDb } from "../db/kernel-schema.js";
3
- import type { ApprovalsService, ApprovalView, NetworkAskView } from "../tools/approvals.service.js";
3
+ import type { ApprovalsService, ApprovalView, GitAskView, NetworkAskView } from "../tools/approvals.service.js";
4
4
  import { type ToolInfo } from "../tools/info.js";
5
5
  import type { ToolRegistry } from "../tools/registry.js";
6
6
  import { type ChannelLocale } from "./phrases.js";
@@ -62,6 +62,12 @@ export interface NetworkCardText {
62
62
  * are redacted by `redact` (the connection's secret values); the description stays on the web.
63
63
  */
64
64
  export declare function networkCardText(detail: Pick<NetworkAskView, "hosts" | "command">, locale: ChannelLocale, redact?: (s: string) => string): NetworkCardText;
65
+ /**
66
+ * A git push ask's card text (sandbox git credentials spec §7): the title names the repository;
67
+ * the preview is `<host>/<repo>`, the refs (`refs/heads/` dropped) and the command clipped to
68
+ * {@link CARD_SUMMARY_MAX}. Model-controlled values are redacted by `redact`.
69
+ */
70
+ export declare function gitCardText(detail: Pick<GitAskView, "host" | "repo" | "refs" | "command">, locale: ChannelLocale, redact?: (s: string) => string): NetworkCardText;
65
71
  /**
66
72
  * The approval card in the approval's current state; undefined when the approval does not exist.
67
73
  * A role escalation offers only "once". A network ask names its hosts and command and labels its
@@ -194,6 +194,28 @@ export function networkCardText(detail, locale, redact = (s) => s) {
194
194
  },
195
195
  };
196
196
  }
197
+ /**
198
+ * A git push ask's card text (sandbox git credentials spec §7): the title names the repository;
199
+ * the preview is `<host>/<repo>`, the refs (`refs/heads/` dropped) and the command clipped to
200
+ * {@link CARD_SUMMARY_MAX}. Model-controlled values are redacted by `redact`.
201
+ */
202
+ export function gitCardText(detail, locale, redact = (s) => s) {
203
+ const repo = redact(detail.repo);
204
+ const refs = detail.refs.map((r) => redact(r.replace(/^refs\/heads\//, "")));
205
+ const command = cut(redact((detail.command ?? "").trim()), CARD_SUMMARY_MAX);
206
+ return {
207
+ title: phrase("gitTitle", locale).replace("{repo}", repo),
208
+ inputPreview: [
209
+ `${redact(detail.host)}/${repo}`,
210
+ ...(refs.length > 0 ? [refs.join(", ")] : []),
211
+ ...(command !== "" ? ["", command] : []),
212
+ ].join("\n"),
213
+ scopeLabels: {
214
+ once: phrase("gitAllowOnce", locale),
215
+ always: phrase("gitAllowAlways", locale),
216
+ },
217
+ };
218
+ }
197
219
  /**
198
220
  * The approval card in the approval's current state; undefined when the approval does not exist.
199
221
  * A role escalation offers only "once". A network ask names its hosts and command and labels its
@@ -204,7 +226,12 @@ export async function approvalCard(d, approvalId, base, locale, redact) {
204
226
  if (!v)
205
227
  return undefined;
206
228
  const decidedBy = await displayName(d.db, v.decidedBy);
207
- const network = v.kind === "network" && v.detail !== null ? networkCardText(v.detail, locale, redact) : null;
229
+ // a network or git ask names what it asks about instead of a tool call (same card shape)
230
+ const network = v.kind === "network" && v.detail !== null
231
+ ? networkCardText(v.detail, locale, redact)
232
+ : v.kind === "git" && v.git != null
233
+ ? gitCardText(v.git, locale, redact)
234
+ : null;
208
235
  const display = v.summary === null && network === null
209
236
  ? (await toolInfo({ db: d.db, registry: d.registry }, v.orgId, [v.toolKey]))[0]
210
237
  : undefined;
@@ -116,6 +116,21 @@ export declare const CHANNEL_PHRASES: {
116
116
  readonly zh: "这台设备一直允许";
117
117
  readonly en: "Always allow on this device";
118
118
  };
119
+ /** A git push ask's card title (`{repo}`; sandbox git credentials spec §7). */
120
+ readonly gitTitle: {
121
+ readonly zh: "智能体想推送到 {repo},允许吗?";
122
+ readonly en: "The agent wants to push to {repo}. Allow?";
123
+ };
124
+ /** A git push ask's 「once」 button. */
125
+ readonly gitAllowOnce: {
126
+ readonly zh: "这次允许";
127
+ readonly en: "Allow this time";
128
+ };
129
+ /** A git push ask's 「always」 button: the person's standing allow for the repository. */
130
+ readonly gitAllowAlways: {
131
+ readonly zh: "这个仓库一直允许";
132
+ readonly en: "Always allow this repository";
133
+ };
119
134
  /** 提问卡片的随附文本。 */
120
135
  readonly questionTitle: {
121
136
  readonly zh: "需要你的回答";