@capybara-hq/kernel 0.1.0-rc.27 → 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 +15 -3
  3. package/dist/catalog/catalog-content.js +5 -3
  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 +10 -3
  7. package/dist/catalog/plugin-install.d.ts +34 -18
  8. package/dist/catalog/plugin-install.js +84 -46
  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 -5
  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));
@@ -78,10 +78,18 @@ export interface OrgCatalogTemplate {
78
78
  */
79
79
  defaultOff: boolean;
80
80
  /**
81
- * The template is on in this org right now (a default-off one an admin switched on): the install
82
- * asks for its input too, since the install is what creates its connector.
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).
83
85
  */
84
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;
85
93
  /**
86
94
  * The template's setup guide in the item's latest version (`GET …/catalog/:itemId/guides/
87
95
  * :guideId`), null without one (spec 2026-11-09-setup-guides-v2-design.md §9.3).
@@ -123,7 +131,11 @@ export interface OrgCatalogDetail {
123
131
  * A connector template as an org sees it (the install wizard and the upgrade preview's inputs):
124
132
  * presets and editable paths, no credentials.
125
133
  */
126
- export declare function orgTemplateView(id: string, file: ConnectorTemplateFile, guide?: OrgCatalogTemplate["guide"], on?: boolean): OrgCatalogTemplate;
134
+ export declare function orgTemplateView(id: string, file: ConnectorTemplateFile, o?: {
135
+ guide?: OrgCatalogTemplate["guide"];
136
+ on?: boolean;
137
+ connected?: boolean;
138
+ }): OrgCatalogTemplate;
127
139
  /** An item's icon (the latest version's plugin icon). */
128
140
  export interface CatalogIcon {
129
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, on = true) {
19
+ export function orgTemplateView(id, file, o = {}) {
20
20
  const t = file.template;
21
21
  return {
22
22
  id,
@@ -30,8 +30,9 @@ export function orgTemplateView(id, file, guide = null, on = true) {
30
30
  ownership: t.ownership ??
31
31
  (typeof t.auth.ownership === "string" ? t.auth.ownership : null),
32
32
  defaultOff: file.defaultEnabled === false,
33
- on,
34
- guide,
33
+ on: o.on ?? true,
34
+ connected: o.connected ?? false,
35
+ guide: o.guide ?? null,
35
36
  };
36
37
  }
37
38
  const iconNotFound = () => new ApiError("catalog.icon_not_found", "This catalog item has no icon");
@@ -266,6 +267,7 @@ export function templateConnectorInput(deps, template, ref, input, defaultName,
266
267
  endpoint,
267
268
  auth,
268
269
  ...(credential !== undefined && { credential }),
270
+ ...(template.git !== undefined && { git: template.git }),
269
271
  template: {
270
272
  ...ref,
271
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;
@@ -276,11 +276,14 @@ let OrgCatalogService = class OrgCatalogService {
276
276
  // Which templates are on in this org now: the install asks for the input of those, a
277
277
  // default-off one an admin switched on included
278
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)
279
+ // Where each template's page finds its setup guide (spec 2026-11-09-setup-guides-v2 §9.3)
280
280
  const guideOf = (templateId) => {
281
281
  const g = templateGuideOf(m, templateId);
282
282
  return g ? { itemId, guideId: g.id } : null;
283
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);
284
287
  // Providers the install would switch on first (a refusal is the install's to report)
285
288
  const implied = await this.pluginInstaller
286
289
  .impliedProviders(orgId, m, templates, m.dependencies ?? [])
@@ -293,7 +296,11 @@ let OrgCatalogService = class OrgCatalogService {
293
296
  item,
294
297
  hasIcon: m.icon !== undefined,
295
298
  extension: m.extension ? m.name : null,
296
- templates: templates.map((t) => orgTemplateView(t.id, t.file, guideOf(t.id), on.has(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
+ })),
297
304
  dependencies: [
298
305
  ...(m.dependencies ?? []).map((d) => ({ name: d.name, range: d.version })),
299
306
  ...implied.map((d) => ({ name: d.name, range: d.range, implied: true })),
@@ -301,7 +308,7 @@ let OrgCatalogService = class OrgCatalogService {
301
308
  skills: (m.skills ?? []).map((s) => packagePath(s.path)),
302
309
  agents: (m.agents ?? []).map((a) => packagePath(a.path)),
303
310
  missingTemplates: item.installed !== null && item.state === "needs_config"
304
- ? ((await this.pluginInstaller.missingConnectors(orgId, itemId))?.templates ?? []).map((t) => orgTemplateView(t.id, t.file, guideOf(t.id), true))
311
+ ? ((await this.pluginInstaller.missingConnectors(orgId, itemId))?.templates ?? []).map((t) => orgTemplateView(t.id, t.file, { guide: guideOf(t.id), on: true }))
305
312
  : [],
306
313
  };
307
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
  */
@@ -541,10 +546,19 @@ export declare class PluginInstaller {
541
546
  }[];
542
547
  } | undefined>;
543
548
  /**
544
- * The org's live connector made from one of the plugin's connector templates, if any: an
545
- * uninstall keeps a connector its agents use, and the plugin's next switch-on reuses it.
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.
546
552
  */
553
+ private liveTemplateConnectors;
554
+ /** {@link liveTemplateConnectors} for one template. */
547
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>>;
548
562
  /**
549
563
  * Creates the connectors an installed plugin is missing (ruling P2-10, `POST …/catalog/:itemId/
550
564
  * connectors`): `input.connectors` names some of {@link missingConnectors}' templates with the
@@ -595,8 +609,10 @@ export declare class PluginInstaller {
595
609
  * skills and agent templates are platform components (phase 2): the ones on in the org through
596
610
  * the plugin (`follow`) go off with it, so the agents using them (by the platform id or an
597
611
  * unpinned org copy linked to it) are listed, also for the dependency installs it created that
598
- * go with it; ledger rows of earlier installs' org copies are not removed and not listed (an
599
- * 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).
600
616
  */
601
617
  previewUninstall(orgId: string, installId: string): Promise<UninstallPreview>;
602
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(" > ")}` : "";
@@ -751,6 +754,9 @@ let PluginInstaller = class PluginInstaller {
751
754
  // takes it back instead of creating a second one, whose default slug the first holds
752
755
  const existing = await this.liveTemplateConnector(orgId, itemId, ct.id);
753
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);
754
760
  await record({
755
761
  kind: "connector",
756
762
  ref: existing,
@@ -761,10 +767,12 @@ let PluginInstaller = class PluginInstaller {
761
767
  continue;
762
768
  }
763
769
  const ci = plan.connectors.get(ct.id);
764
- // No input was needed (a live connector was taken back above, or the template was skipped
765
- // at plan time): nothing to create
766
- if (!ci)
767
- continue;
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
+ }
768
776
  if (actor === null)
769
777
  throw systemNeedsInput(manifest.name);
770
778
  const r = await this.connectors.create(orgId, actor, ci, opts.actorTokenId);
@@ -1088,7 +1096,9 @@ let PluginInstaller = class PluginInstaller {
1088
1096
  * lock, one transaction). Removes only components with created=true:
1089
1097
  * - a skill is kept when an agent names it (an inline sub-agent template's `skills`;
1090
1098
  * used_by_agents) or another plugin install in this org uses it too (used_by_other_install);
1091
- * - 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;
1092
1102
  * - an extension is kept when another plugin install uses it too (used_by_other_install), a
1093
1103
  * connector still uses a connector it provides (used_by_connectors), or another enabled
1094
1104
  * extension depends on it (used_by_plugins);
@@ -1114,7 +1124,8 @@ let PluginInstaller = class PluginInstaller {
1114
1124
  * The uninstall's one transaction; the extensions it disabled are notified once it commits. A
1115
1125
  * scope sync uninstall (`via: "scope"`) re-checks under the org row lock that the item is still
1116
1126
  * not wanted in the org and that no other live install depends on it ({@link ScopeStillWanted},
1117
- * 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.
1118
1129
  */
1119
1130
  async writeUninstall(orgId, actor, installId, opts, sw) {
1120
1131
  const { via, ...actorOpts } = opts;
@@ -1315,6 +1326,11 @@ let PluginInstaller = class PluginInstaller {
1315
1326
  // failed (already rolled back) and a stale installing (a replica crashed midway) are cleaned up
1316
1327
  // as a rollback
1317
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";
1318
1334
  const comps = await t
1319
1335
  .selectFrom("plugin_install_components")
1320
1336
  .select(["kind", "ref", "created", "skill_version"])
@@ -1357,6 +1373,15 @@ let PluginInstaller = class PluginInstaller {
1357
1373
  created = now.created;
1358
1374
  }
1359
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
+ }
1360
1385
  // Skills and agent templates of earlier installs (org copies, now linked to the platform
1361
1386
  // components, phase 2) stay: the plugin's components go off in the org with the install
1362
1387
  if ((c.kind === "skill" || c.kind === "agent_template") && !rollbackMode)
@@ -1384,12 +1409,8 @@ let PluginInstaller = class PluginInstaller {
1384
1409
  keep("wanted_by_scope");
1385
1410
  continue;
1386
1411
  }
1387
- // A scope sync uninstall leaves the connectors to the org (spec §5 rule 8): the component row
1388
- // goes with the install row, so nothing removes them later
1389
- if (c.kind === "connector" && ctx.via === "scope" && !rollbackMode) {
1390
- keep("org_owned");
1391
- continue;
1392
- }
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.
1393
1414
  const reason = rollbackMode ? "plugin_install_cleanup" : (ctx.reason ?? "plugin_uninstall");
1394
1415
  const outcome = await this.decideRemovalIn(orgId, actor, comp, removing, agentRefs, reason);
1395
1416
  if (outcome === "removed")
@@ -1419,12 +1440,12 @@ let PluginInstaller = class PluginInstaller {
1419
1440
  return "removed";
1420
1441
  }
1421
1442
  /**
1422
- * The uninstall rules for one component an install created (a skill, connector or extension;
1423
- * dependency installs recurse through {@link uninstallIn}), in the caller's transaction: kept
1424
- * when an agent uses the skill / connector (`used_by_agents`), when another live install outside
1425
- * `removing` has it (`used_by_other_install`, ownership handed over to the oldest one, so the
1426
- * uninstall order doesn't change the result), when a connector still uses the extension's
1427
- * 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
1428
1449
  * (`used_by_plugins`); otherwise removed (`removed`, or `gone` when it no longer existed). Used
1429
1450
  * by uninstall and by an upgrade's removals.
1430
1451
  */
@@ -1677,21 +1698,36 @@ let PluginInstaller = class PluginInstaller {
1677
1698
  };
1678
1699
  }
1679
1700
  /**
1680
- * The org's live connector made from one of the plugin's connector templates, if any: an
1681
- * uninstall keeps a connector its agents use, and the plugin's next switch-on reuses it.
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.
1682
1704
  */
1683
- async liveTemplateConnector(orgId, itemId, templateId) {
1684
- const row = await this.db
1705
+ async liveTemplateConnectors(orgId, itemId, templateId) {
1706
+ let q = this.db
1685
1707
  .selectFrom("connectors as c")
1686
1708
  .innerJoin("platform_connector_templates as p", "p.id", "c.platform_template_id")
1687
- .select("c.id")
1709
+ .select(["p.template_id", "c.id"])
1688
1710
  .where("c.org_id", "=", orgId)
1689
1711
  .where("c.deleted_at", "is", null)
1690
- .where("p.catalog_item_id", "=", itemId)
1691
- .where("p.template_id", "=", templateId)
1692
- .orderBy("c.created_at")
1693
- .executeTakeFirst();
1694
- return row?.id;
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));
1695
1731
  }
1696
1732
  /**
1697
1733
  * Creates the connectors an installed plugin is missing (ruling P2-10, `POST …/catalog/:itemId/
@@ -1887,8 +1923,10 @@ let PluginInstaller = class PluginInstaller {
1887
1923
  * skills and agent templates are platform components (phase 2): the ones on in the org through
1888
1924
  * the plugin (`follow`) go off with it, so the agents using them (by the platform id or an
1889
1925
  * unpinned org copy linked to it) are listed, also for the dependency installs it created that
1890
- * go with it; ledger rows of earlier installs' org copies are not removed and not listed (an
1891
- * 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).
1892
1930
  */
1893
1931
  async previewUninstall(orgId, installId) {
1894
1932
  if (!UUID_RE.test(installId))
@@ -1921,13 +1959,23 @@ let PluginInstaller = class PluginInstaller {
1921
1959
  };
1922
1960
  const kept = [];
1923
1961
  /**
1924
- * The agents using what goes with an install: connectors it created (they stay, kept for the
1925
- * agents) and the plugin's platform skills and agent templates on in the org through it
1926
- * (`follow`; by the platform id or an unpinned org copy linked to it). Dependency installs it
1927
- * 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).
1928
1967
  */
1929
1968
  const visit = async (itemId, rows, top, seen) => {
1930
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
+ }
1931
1979
  if (!c.created) {
1932
1980
  if (top)
1933
1981
  kept.push({ kind: c.kind, ref: c.ref, reason: "pre_existing" });
@@ -1952,17 +2000,7 @@ let PluginInstaller = class PluginInstaller {
1952
2000
  seen.add(c.ref);
1953
2001
  const sub = (await ledger(c.ref)).filter((x) => x.kind !== "skill" && x.kind !== "agent_template");
1954
2002
  await visit(dep.item_id, sub, false, seen);
1955
- continue;
1956
2003
  }
1957
- if (c.kind !== "connector")
1958
- continue;
1959
- const users = agents.filter((a) => a.connectors.has(c.ref));
1960
- if (users.length === 0)
1961
- continue;
1962
- if (top)
1963
- kept.push({ kind: c.kind, ref: c.ref, reason: "used_by_agents" });
1964
- for (const a of users)
1965
- use(a, c.kind, c.ref);
1966
2004
  }
1967
2005
  for (const kind of ["skill", "agent_template"]) {
1968
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: "需要你的回答";
@@ -96,6 +96,15 @@ export const CHANNEL_PHRASES = {
96
96
  networkAllowOnce: { zh: "这次允许", en: "Allow this time" },
97
97
  /** A network ask's 「always」 button. */
98
98
  networkAllowAlways: { zh: "这台设备一直允许", en: "Always allow on this device" },
99
+ /** A git push ask's card title (`{repo}`; sandbox git credentials spec §7). */
100
+ gitTitle: {
101
+ zh: "智能体想推送到 {repo},允许吗?",
102
+ en: "The agent wants to push to {repo}. Allow?",
103
+ },
104
+ /** A git push ask's 「once」 button. */
105
+ gitAllowOnce: { zh: "这次允许", en: "Allow this time" },
106
+ /** A git push ask's 「always」 button: the person's standing allow for the repository. */
107
+ gitAllowAlways: { zh: "这个仓库一直允许", en: "Always allow this repository" },
99
108
  /** 提问卡片的随附文本。 */
100
109
  questionTitle: { zh: "需要你的回答", en: "Your input is needed" },
101
110
  /** 连接卡片的随附文本。 */