@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
@@ -29,6 +29,8 @@ import { OrgLock } from "./org-lock.js";
29
29
  * absent (or an invalid stored value) = follow the platform, personal picks allowed.
30
30
  * - `uiArrangement`: per slot, the extension contributions the org hides and their order (UI slots
31
31
  * spec §5); a slot with no entry follows the platform's; an invalid stored value reads as absent.
32
+ * - `gitGateway`: the org's git gateway switch and host sources (sandbox git credentials spec §9);
33
+ * not in the PATCH yet; an invalid stored value reads as `{ enabled: false }` (fails closed).
32
34
  */
33
35
  /** 组织沙箱出站白名单的主机数上限。 */
34
36
  export declare const ORG_SANDBOX_ALLOW_HOSTS_MAX = 200;
@@ -117,6 +119,21 @@ export declare const ScheduledTasksPatchSchema: z.ZodObject<{
117
119
  pauseAfterFailures: z.ZodOptional<z.ZodNumber>;
118
120
  forbiddenTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
119
121
  }, z.core.$strict>;
122
+ /**
123
+ * The org's git gateway setting (sandbox git credentials spec §9): `enabled: false` switches the
124
+ * gateway off for the org (absent = on); `hostSources` names, per git host, the connector whose
125
+ * credential the gateway uses there. Reads fail closed: an invalid stored value (not an object, or
126
+ * a field of the wrong type) reads as {@link GIT_GATEWAY_INVALID} (off), never wider than stored.
127
+ */
128
+ export declare const GitGatewaySettingSchema: z.ZodObject<{
129
+ enabled: z.ZodOptional<z.ZodBoolean>;
130
+ hostSources: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
131
+ }, z.core.$strip>;
132
+ /** What an invalid stored `gitGateway` reads as: the gateway off for the org. */
133
+ export declare const GIT_GATEWAY_INVALID: {
134
+ readonly enabled: false;
135
+ };
136
+ export type GitGatewaySetting = z.infer<typeof GitGatewaySettingSchema>;
120
137
  export declare const OrgSettingsSchema: z.ZodObject<{
121
138
  toolSearch: z.ZodCatch<z.ZodDefault<z.ZodEnum<{
122
139
  off: "off";
@@ -165,6 +182,10 @@ export declare const OrgSettingsSchema: z.ZodObject<{
165
182
  }>;
166
183
  }, z.core.$strict>>>;
167
184
  uiArrangement: z.ZodCatch<z.ZodOptional<z.ZodType<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown, z.core.$ZodTypeInternals<Partial<Record<"agent.detail.section" | "conversation.composer.frame" | "conversation.empty" | "conversation.panel" | "conversation.properties.section" | "conversation.tool-result.card" | "file.preview" | "org.page" | "org.settings.section" | "platform.page" | "shell.brand" | "shell.overlay", import("../ui/arrangement.js").UiArrangementEntry>>, unknown>>>>;
185
+ gitGateway: z.ZodCatch<z.ZodOptional<z.ZodObject<{
186
+ enabled: z.ZodOptional<z.ZodBoolean>;
187
+ hostSources: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
188
+ }, z.core.$strip>>>;
168
189
  scheduledTasks: z.ZodCatch<z.ZodPrefault<z.ZodObject<{
169
190
  enabled: z.ZodCatch<z.ZodDefault<z.ZodBoolean>>;
170
191
  minIntervalMinutes: z.ZodCatch<z.ZodDefault<z.ZodNumber>>;
@@ -45,6 +45,8 @@ import { OrgLock, OrgLockModule } from "./org-lock.js";
45
45
  * absent (or an invalid stored value) = follow the platform, personal picks allowed.
46
46
  * - `uiArrangement`: per slot, the extension contributions the org hides and their order (UI slots
47
47
  * spec §5); a slot with no entry follows the platform's; an invalid stored value reads as absent.
48
+ * - `gitGateway`: the org's git gateway switch and host sources (sandbox git credentials spec §9);
49
+ * not in the PATCH yet; an invalid stored value reads as `{ enabled: false }` (fails closed).
48
50
  */
49
51
  /** 组织沙箱出站白名单的主机数上限。 */
50
52
  export const ORG_SANDBOX_ALLOW_HOSTS_MAX = 200;
@@ -139,6 +141,18 @@ export const ScheduledTasksPatchSchema = z
139
141
  })
140
142
  .partial()
141
143
  .strict();
144
+ /**
145
+ * The org's git gateway setting (sandbox git credentials spec §9): `enabled: false` switches the
146
+ * gateway off for the org (absent = on); `hostSources` names, per git host, the connector whose
147
+ * credential the gateway uses there. Reads fail closed: an invalid stored value (not an object, or
148
+ * a field of the wrong type) reads as {@link GIT_GATEWAY_INVALID} (off), never wider than stored.
149
+ */
150
+ export const GitGatewaySettingSchema = z.object({
151
+ enabled: z.boolean().optional(),
152
+ hostSources: z.record(z.string(), z.string()).optional(),
153
+ });
154
+ /** What an invalid stored `gitGateway` reads as: the gateway off for the org. */
155
+ export const GIT_GATEWAY_INVALID = { enabled: false };
142
156
  export const OrgSettingsSchema = z.object({
143
157
  toolSearch: z.enum(["on", "off"]).default("on").catch("on"),
144
158
  locale: z.enum(["zh", "en"]).default("en").catch("en"),
@@ -155,6 +169,8 @@ export const OrgSettingsSchema = z.object({
155
169
  theme: OrgThemeSettingSchema.optional().catch(undefined),
156
170
  /** Hidden and ordered extension contributions per slot; absent = follow the platform. */
157
171
  uiArrangement: UiArrangementSchema.optional().catch(undefined),
172
+ /** The org's git gateway switch and host sources; absent = on, sources chosen by default. */
173
+ gitGateway: GitGatewaySettingSchema.optional().catch({ ...GIT_GATEWAY_INVALID }),
158
174
  /** Scheduled tasks (spec §9); the platform bounds clamp it, see `effectiveScheduleSettings`. */
159
175
  scheduledTasks: ScheduledTasksSettingsSchema.prefault({}).catch(ScheduledTasksSettingsSchema.parse({})),
160
176
  });
@@ -434,6 +450,8 @@ let OrgSettingsService = class OrgSettingsService {
434
450
  delete after.theme;
435
451
  if (after.uiArrangement === undefined)
436
452
  delete after.uiArrangement;
453
+ if (after.gitGateway === undefined)
454
+ delete after.gitGateway;
437
455
  if (sameSettings(before, after))
438
456
  return before;
439
457
  // Lock order: the org row (orgLock above), then the theme's catalog item and its row FOR
@@ -8,6 +8,8 @@ import { EVAL_REPLAY_HOOKS } from "./evals/worker.js";
8
8
  import { BOOTSTRAP_LOCK_TTL } from "./extensions/bootstrap.js";
9
9
  import { USAGE_SUBSCRIBERS_CLOCK } from "./extensions/usage-subscribers.js";
10
10
  import { FILE_PREVIEW_TIMEOUT } from "./files/previewers.js";
11
+ import { GIT_UPSTREAM_ORIGIN, type GitUpstreamOrigin } from "./git/gateway.js";
12
+ import { GIT_IDENTITY_API_BASE, type GitIdentityApiBase } from "./git/identity.js";
11
13
  import { OUTBOX_DISPATCH_FAILS } from "./infra/outbox/outbox.dispatcher.js";
12
14
  import { OUTBOX_FAST_PATH_SETTLED } from "./infra/outbox/outbox.module.js";
13
15
  import { OUTBOX_STRICT_CONFLICTS } from "./infra/outbox/outbox.service.js";
@@ -40,6 +42,8 @@ import { EMBED_TOKEN_CLOCK } from "./ui/embed-token.js";
40
42
  * - `EMBED_TOKEN_CLOCK`: the clock embed tokens are issued and checked with;
41
43
  * - `RUN_CONTEXT_CLOCK`: the clock run start reads the run context's date from, and the
42
44
  * `CurrentTime` builtin the time;
45
+ * - `GIT_IDENTITY_API_BASE`: the API base of a git host for the commit identity (a loopback fake);
46
+ * - `GIT_UPSTREAM_ORIGIN`: the origin the git gateway relays a git host to (a loopback fake);
43
47
  * - `OUTBOX_STRICT_CONFLICTS`: a reused outbox job id with different data throws (tests) instead
44
48
  * of logging an error;
45
49
  * - `OUTBOX_DISPATCH_FAILS`: returns true to make the outbox dispatcher's adds to the given queue
@@ -69,6 +73,8 @@ interface KernelSeams {
69
73
  [ADMIN_USAGE_TIMINGS]: Partial<AdminUsageTimings>;
70
74
  [EMBED_TOKEN_CLOCK]: () => number;
71
75
  [RUN_CONTEXT_CLOCK]: () => number;
76
+ [GIT_IDENTITY_API_BASE]: GitIdentityApiBase;
77
+ [GIT_UPSTREAM_ORIGIN]: GitUpstreamOrigin;
72
78
  [OUTBOX_STRICT_CONFLICTS]: boolean;
73
79
  [OUTBOX_DISPATCH_FAILS]: (queue: KernelQueueName) => boolean;
74
80
  [USAGE_SUBSCRIBERS_CLOCK]: () => number;
package/dist/overrides.js CHANGED
@@ -6,6 +6,8 @@ import { EVAL_REPLAY_HOOKS } from "./evals/worker.js";
6
6
  import { BOOTSTRAP_LOCK_TTL } from "./extensions/bootstrap.js";
7
7
  import { USAGE_SUBSCRIBERS_CLOCK } from "./extensions/usage-subscribers.js";
8
8
  import { FILE_PREVIEW_TIMEOUT } from "./files/previewers.js";
9
+ import { GIT_UPSTREAM_ORIGIN } from "./git/gateway.js";
10
+ import { GIT_IDENTITY_API_BASE } from "./git/identity.js";
9
11
  import { OUTBOX_DISPATCH_FAILS } from "./infra/outbox/outbox.dispatcher.js";
10
12
  import { OUTBOX_FAST_PATH_SETTLED } from "./infra/outbox/outbox.module.js";
11
13
  import { OUTBOX_STRICT_CONFLICTS } from "./infra/outbox/outbox.service.js";
@@ -38,6 +40,8 @@ const SEAM_TOKENS = [
38
40
  ADMIN_USAGE_TIMINGS,
39
41
  EMBED_TOKEN_CLOCK,
40
42
  RUN_CONTEXT_CLOCK,
43
+ GIT_IDENTITY_API_BASE,
44
+ GIT_UPSTREAM_ORIGIN,
41
45
  OUTBOX_STRICT_CONFLICTS,
42
46
  OUTBOX_DISPATCH_FAILS,
43
47
  OUTBOX_FAST_PATH_SETTLED,
@@ -282,6 +282,9 @@ export const PLATFORM_EXCLUSIONS = [
282
282
  category: "transport",
283
283
  reason: "Serves sandbox file bytes",
284
284
  },
285
+ // git gateway (sandbox git credentials spec §5): `GET` / `POST /git/v1/*` is git smart-HTTP for
286
+ // sandboxes, authenticated by run tickets, not an API; it is a raw api-role Fastify route
287
+ // (`git/gateway.route.ts`), not a Nest controller, so it is not listed (like the webhook)
285
288
  // devices (personal cloud devices, environments design phase 1)
286
289
  // connected machines (environments design phase 2): device sign-in by the program and on the
287
290
  // confirmation page, and disconnecting a machine. The machine's WebSocket (`GET /device/connect`)
@@ -1,4 +1,5 @@
1
1
  import { type AppendEvent, EventLog } from "../conversations/event-log.js";
2
+ import { GitRunEnd } from "../git/run-end.js";
2
3
  import { KernelDb } from "../infra/database/kernel-db.js";
3
4
  import { TransactionHost } from "../infra/database/transaction-host.js";
4
5
  import { OutboxService } from "../infra/outbox/outbox.service.js";
@@ -30,6 +31,8 @@ export declare class RunFinish {
30
31
  readonly txHost: TransactionHost;
31
32
  /** A scheduled task's run follows its kernel run's end in the same transaction (spec §6). */
32
33
  private readonly taskRuns;
34
+ /** The run's git tickets are revoked and its fetch summaries written after the commit. */
35
+ private readonly gitRunEnd;
33
36
  /** The run's end owes the conversation's ensure-run backstop (transactional outbox spec §6). */
34
37
  private readonly outbox;
35
38
  /** A finished run's pending approvals are cancelled in its terminal transaction. */
@@ -39,6 +42,8 @@ export declare class RunFinish {
39
42
  constructor(kdb: KernelDb, eventLog: EventLog, txHost: TransactionHost,
40
43
  /** A scheduled task's run follows its kernel run's end in the same transaction (spec §6). */
41
44
  taskRuns: Pick<ScheduledTaskRunTracker, "onRunFinished">,
45
+ /** The run's git tickets are revoked and its fetch summaries written after the commit. */
46
+ gitRunEnd: Pick<GitRunEnd, "onRunFinished">,
42
47
  /** The run's end owes the conversation's ensure-run backstop (transactional outbox spec §6). */
43
48
  outbox: Pick<OutboxService, "add">,
44
49
  /** A finished run's pending approvals are cancelled in its terminal transaction. */
@@ -11,6 +11,8 @@ import { Inject, Injectable } from "@nestjs/common";
11
11
  import { sql } from "kysely";
12
12
  import { EventLog } from "../conversations/event-log.js";
13
13
  import { pauseQueueIn } from "../conversations/queue.js";
14
+ import { GitRunEnd } from "../git/run-end.js";
15
+ import { afterCommit } from "../infra/database/after-commit.js";
14
16
  import { KernelDb } from "../infra/database/kernel-db.js";
15
17
  import { TransactionHost } from "../infra/database/transaction-host.js";
16
18
  import { Transactional } from "../infra/database/transactional.decorator.js";
@@ -30,12 +32,15 @@ let RunFinish = class RunFinish {
30
32
  eventLog;
31
33
  txHost;
32
34
  taskRuns;
35
+ gitRunEnd;
33
36
  outbox;
34
37
  approvals;
35
38
  questions;
36
39
  constructor(kdb, eventLog, txHost,
37
40
  /** A scheduled task's run follows its kernel run's end in the same transaction (spec §6). */
38
41
  taskRuns,
42
+ /** The run's git tickets are revoked and its fetch summaries written after the commit. */
43
+ gitRunEnd,
39
44
  /** The run's end owes the conversation's ensure-run backstop (transactional outbox spec §6). */
40
45
  outbox,
41
46
  /** A finished run's pending approvals are cancelled in its terminal transaction. */
@@ -46,6 +51,7 @@ let RunFinish = class RunFinish {
46
51
  this.eventLog = eventLog;
47
52
  this.txHost = txHost;
48
53
  this.taskRuns = taskRuns;
54
+ this.gitRunEnd = gitRunEnd;
49
55
  this.outbox = outbox;
50
56
  this.approvals = approvals;
51
57
  this.questions = questions;
@@ -122,6 +128,8 @@ let RunFinish = class RunFinish {
122
128
  .where("id", "=", a.runId)
123
129
  .execute();
124
130
  await this.taskRuns.onRunFinished(a.runId, a.status, failureMessage(a.event));
131
+ // the terminal status is committed first, then the tickets go (the one place runs end)
132
+ afterCommit(() => this.gitRunEnd.onRunFinished(a.runId));
125
133
  // a failed or stopped run pauses the queued messages until the person resumes (spec §5.7)
126
134
  if (a.status !== "completed")
127
135
  await pauseQueueIn(t, a.conversationId);
@@ -185,9 +193,10 @@ RunFinish = __decorate([
185
193
  __param(1, Inject(EventLog)),
186
194
  __param(2, Inject(TransactionHost)),
187
195
  __param(3, Inject(ScheduledTaskRunTracker)),
188
- __param(4, Inject(OutboxService)),
189
- __param(5, Inject(ApprovalsService)),
190
- __param(6, Inject(QuestionsService))
196
+ __param(4, Inject(GitRunEnd)),
197
+ __param(5, Inject(OutboxService)),
198
+ __param(6, Inject(ApprovalsService)),
199
+ __param(7, Inject(QuestionsService))
191
200
  ], RunFinish);
192
201
  export { RunFinish };
193
202
  /**
@@ -12,6 +12,7 @@ import { AttachmentsModule } from "../conversations/attachments.module.js";
12
12
  import { EventLogModule } from "../conversations/event-log.js";
13
13
  import { TitlesModule } from "../conversations/titles.js";
14
14
  import { ExtensionsModule } from "../extensions/extensions.module.js";
15
+ import { GitModule } from "../git/git.module.js";
15
16
  import { IdentityModule } from "../identity/identity.module.js";
16
17
  import { LockService } from "../infra/lock/lock.service.js";
17
18
  import { QueueModule } from "../infra/queue/queue.module.js";
@@ -58,6 +59,7 @@ RuntimeModule = __decorate([
58
59
  IdentityModule,
59
60
  SkillsModule,
60
61
  ToolsModule,
62
+ GitModule,
61
63
  ProvidersModule,
62
64
  RunJobsModule,
63
65
  SubagentsModule,
@@ -19,7 +19,7 @@ import { Transactional } from "../infra/database/transactional.decorator.js";
19
19
  import { KernelEvents } from "../infra/events/kernel-events.js";
20
20
  import { OutboxService } from "../infra/outbox/outbox.service.js";
21
21
  import { KERNEL_LOGGER } from "../infra/tokens.js";
22
- import { ApprovalsService, NETWORK_ASK_CANCELLED_NOTE, } from "../tools/approvals.service.js";
22
+ import { ApprovalsService, GIT_ASK_CANCELLED_NOTE, NETWORK_ASK_CANCELLED_NOTE, } from "../tools/approvals.service.js";
23
23
  import { APPROVAL_CANCELLED_TEXT, INPUT_CANCELLED_TEXT, RUN_STOPPED_TEXT, ToolPipeline, } from "../tools/pipeline.js";
24
24
  import { QuestionsService } from "../tools/questions.service.js";
25
25
  import { RunAbort } from "./abort.js";
@@ -338,13 +338,15 @@ let Waits = class Waits {
338
338
  .orderBy("attempt")
339
339
  .execute();
340
340
  for (const a of approvals) {
341
- // a network ask's command already ran: its kept result, then why access stays blocked
342
- // (network ask-to-allow spec §6.7)
343
- const kept = a.kind === "network" ? a.detail?.result : null;
341
+ // a network or git ask's command already ran: its kept result, then why access stays
342
+ // blocked or the push was not made (network ask-to-allow spec §6.7)
343
+ const kept = a.kind === "network" || a.kind === "git"
344
+ ? a.detail?.result
345
+ : null;
344
346
  cancelled.set(a.call_id, kept
345
347
  ? {
346
348
  toolKey: a.tool_key,
347
- text: `${kept.content}\n\n${NETWORK_ASK_CANCELLED_NOTE}`,
349
+ text: `${kept.content}\n\n${a.kind === "git" ? GIT_ASK_CANCELLED_NOTE : NETWORK_ASK_CANCELLED_NOTE}`,
348
350
  isError: kept.isError,
349
351
  }
350
352
  : { toolKey: a.tool_key, text: APPROVAL_CANCELLED_TEXT });
@@ -25,9 +25,9 @@ export declare function localTimeOf(at: Date, timeZone: string): string;
25
25
  * count), then the task run and the task rows: the checks in order (task paused since the
26
26
  * scan, unless a person's "run now"; a tick fired more than 10 minutes past the org's catch-up
27
27
  * window, `missed`, unless a "run now"; org disabled, creator inactive or not a member, agent
28
- * gone or not visible, conversation gone, connection inactive,
29
- * overlap, rate limit, device offline, queue paused), each a `skipped` run with its reason, the
30
- * lasting ones also pausing the task; then `fired_at` is written, after the rate limit count,
28
+ * gone or not visible, conversation gone, connection inactive or its extension disabled in the
29
+ * org, overlap, rate limit, device offline, queue paused), each a `skipped` run with its reason,
30
+ * the lasting ones also pausing the task; then `fired_at` is written, after the rate limit count,
31
31
  * with the run's delayed settle check owed (`sched-settle`, {@link settle}).
32
32
  * A run whose claim committed before (a retried job) goes straight to the start.
33
33
  * 2. The start, idempotent: `new` creates the conversation with its first message keyed by the
@@ -72,9 +72,9 @@ const DEVICE_GONE = new Set(["device.not_found", "device.unavailable", "sandbox.
72
72
  * count), then the task run and the task rows: the checks in order (task paused since the
73
73
  * scan, unless a person's "run now"; a tick fired more than 10 minutes past the org's catch-up
74
74
  * window, `missed`, unless a "run now"; org disabled, creator inactive or not a member, agent
75
- * gone or not visible, conversation gone, connection inactive,
76
- * overlap, rate limit, device offline, queue paused), each a `skipped` run with its reason, the
77
- * lasting ones also pausing the task; then `fired_at` is written, after the rate limit count,
75
+ * gone or not visible, conversation gone, connection inactive or its extension disabled in the
76
+ * org, overlap, rate limit, device offline, queue paused), each a `skipped` run with its reason,
77
+ * the lasting ones also pausing the task; then `fired_at` is written, after the rate limit count,
78
78
  * with the run's delayed settle check owed (`sched-settle`, {@link settle}).
79
79
  * A run whose claim committed before (a retried job) goes straight to the start.
80
80
  * 2. The start, idempotent: `new` creates the conversation with its first message keyed by the
@@ -279,10 +279,13 @@ let ScheduledTaskFirer = class ScheduledTaskFirer {
279
279
  !conn ||
280
280
  conn.orgId !== orgId ||
281
281
  conn.status !== "active" ||
282
- !conn.pluginEnabled ||
283
282
  conn.agentId !== task.agentId) {
284
283
  return skip("target_gone");
285
284
  }
285
+ // the org switched the channel's extension off: the task stays active and recovers when it
286
+ // is switched on again (unlike a target that is gone)
287
+ if (!conn.pluginEnabled)
288
+ return skip("plugin_disabled");
286
289
  if (chat.chatType === "group") {
287
290
  // a group has no current conversation: each run starts its own thread at its first post
288
291
  // (spec §8), which needs a channel with threads; the creator must have written there
@@ -71,6 +71,35 @@ export declare function networkGrantRefusal(host: string, ctx: NetworkGrantConte
71
71
  code: GrantRefusalCode;
72
72
  message: string;
73
73
  } | null;
74
+ /**
75
+ * A git push grant (sandbox git credentials spec §7): `git:push` lets the task's unattended runs
76
+ * push to any repository the creator's git source reaches, `git:push:<owner>/<repo>` to that one.
77
+ */
78
+ export declare const GIT_PUSH_GRANT = "git:push";
79
+ /** The prefix of a per-repository git push grant (`git:push:<owner>/<repo>`). */
80
+ export declare const GIT_PUSH_REPO_GRANT_PREFIX = "git:push:";
81
+ /** The `spec_hash` a git push grant is stored with (no tool definition behind it). */
82
+ export declare const GIT_GRANT_SPEC_HASH = "git";
83
+ /**
84
+ * A repository as policies, grants and asks key it: `<owner>/<repo>` lower-cased (hosts compare
85
+ * repositories without case), a trailing `.git` dropped; null when it is not of that shape or a
86
+ * segment is `.` / `..`.
87
+ */
88
+ export declare function normalizeGitRepo(repo: string): string | null;
89
+ /** Whether a grant key is a git push grant (`git:push` or `git:push:<owner>/<repo>`). */
90
+ export declare const isGitGrantKey: (key: string) => boolean;
91
+ /** The grant key of pushes to `repo` (normalized by the caller), or to any repository. */
92
+ export declare const gitPushGrantKey: (repo?: string) => string;
93
+ /**
94
+ * Why a git push grant key cannot be one of a task's grants, or null when it can: the org forbids
95
+ * it (`forbiddenTools` lists the key or `git:push`) or the repository is not `<owner>/<repo>`
96
+ * (`not_grantable`). The key is compared as given (callers normalize it with
97
+ * {@link normalizeGitGrantKey} first).
98
+ */
99
+ export declare function gitGrantRefusal(key: string, ctx: Pick<GrantContext, "forbiddenTools">): {
100
+ code: GrantRefusalCode;
101
+ message: string;
102
+ } | null;
74
103
  /** The prefix of a sandbox tier grant's key (sandbox dev tiers spec §11.4): only `tier:open` exists. */
75
104
  export declare const TIER_GRANT_PREFIX = "tier:";
76
105
  /** Whether a grant key is a sandbox tier grant (`tier:<tier>`). */
@@ -92,3 +121,14 @@ export declare function tierGrantRefusal(key: string, ctx: {
92
121
  code: GrantRefusalCode;
93
122
  message: string;
94
123
  } | null;
124
+ /**
125
+ * Whether the org's `forbiddenTools` forbid the git push grant `key` (normalized): it lists
126
+ * `git:push` or the key, each entry compared normalized (`git:push:Acme/App` forbids
127
+ * `git:push:acme/app`).
128
+ */
129
+ export declare function gitPushForbidden(forbiddenTools: readonly string[], key: string): boolean;
130
+ /**
131
+ * A git push grant key as stored: the repository normalized ({@link normalizeGitRepo}); the key
132
+ * unchanged when its repository is not of the shape (then {@link gitGrantRefusal} refuses it).
133
+ */
134
+ export declare function normalizeGitGrantKey(key: string): string;
@@ -1,4 +1,5 @@
1
1
  import { validateAllowHost, } from "@capybara-hq/plugin-sdk";
2
+ import { GIT_REPO_MAX_CHARS } from "../conversations/events.js";
2
3
  import { orgAllows } from "../local-devices/network-grants.js";
3
4
  import { roleAtLeast } from "../orgs/roles.js";
4
5
  import { isFixedAsk } from "../tools/fixed-ask.js";
@@ -13,6 +14,9 @@ export { TIER_GRANT_SPEC_HASH, TIER_OPEN_GRANT_KEY };
13
14
  * undefined when the agent does not have it now.
14
15
  */
15
16
  export function grantRefusalOf(toolKey, tool, ctx) {
17
+ // a git push grant belongs to no tool (sandbox git credentials spec §7)
18
+ if (isGitGrantKey(toolKey))
19
+ return gitGrantRefusal(toolKey, ctx);
16
20
  if (ctx.forbiddenTools.includes(toolKey) ||
17
21
  (tool !== undefined && ctx.forbiddenTools.includes(tool.key))) {
18
22
  return {
@@ -89,6 +93,61 @@ export function networkGrantRefusal(host, ctx) {
89
93
  }
90
94
  return null;
91
95
  }
96
+ /**
97
+ * A git push grant (sandbox git credentials spec §7): `git:push` lets the task's unattended runs
98
+ * push to any repository the creator's git source reaches, `git:push:<owner>/<repo>` to that one.
99
+ */
100
+ export const GIT_PUSH_GRANT = "git:push";
101
+ /** The prefix of a per-repository git push grant (`git:push:<owner>/<repo>`). */
102
+ export const GIT_PUSH_REPO_GRANT_PREFIX = `${GIT_PUSH_GRANT}:`;
103
+ /** The `spec_hash` a git push grant is stored with (no tool definition behind it). */
104
+ export const GIT_GRANT_SPEC_HASH = "git";
105
+ /** The shape of `<owner>/<repo>` (one owner segment, one repository segment). */
106
+ const GIT_REPO_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
107
+ /**
108
+ * A repository as policies, grants and asks key it: `<owner>/<repo>` lower-cased (hosts compare
109
+ * repositories without case), a trailing `.git` dropped; null when it is not of that shape or a
110
+ * segment is `.` / `..`.
111
+ */
112
+ export function normalizeGitRepo(repo) {
113
+ const r = repo
114
+ .trim()
115
+ .replace(/\.git$/i, "")
116
+ .toLowerCase();
117
+ if (r.length > GIT_REPO_MAX_CHARS || !GIT_REPO_PATTERN.test(r))
118
+ return null;
119
+ if (r.split("/").some((seg) => seg === "." || seg === ".."))
120
+ return null;
121
+ return r;
122
+ }
123
+ /** Whether a grant key is a git push grant (`git:push` or `git:push:<owner>/<repo>`). */
124
+ export const isGitGrantKey = (key) => key === GIT_PUSH_GRANT || key.startsWith(GIT_PUSH_REPO_GRANT_PREFIX);
125
+ /** The grant key of pushes to `repo` (normalized by the caller), or to any repository. */
126
+ export const gitPushGrantKey = (repo) => repo === undefined ? GIT_PUSH_GRANT : `${GIT_PUSH_REPO_GRANT_PREFIX}${repo}`;
127
+ /**
128
+ * Why a git push grant key cannot be one of a task's grants, or null when it can: the org forbids
129
+ * it (`forbiddenTools` lists the key or `git:push`) or the repository is not `<owner>/<repo>`
130
+ * (`not_grantable`). The key is compared as given (callers normalize it with
131
+ * {@link normalizeGitGrantKey} first).
132
+ */
133
+ export function gitGrantRefusal(key, ctx) {
134
+ if (gitPushForbidden(ctx.forbiddenTools, key)) {
135
+ return {
136
+ code: "forbidden",
137
+ message: `The organization does not allow ${key} in scheduled runs`,
138
+ };
139
+ }
140
+ if (key === GIT_PUSH_GRANT)
141
+ return null;
142
+ const repo = key.slice(GIT_PUSH_REPO_GRANT_PREFIX.length);
143
+ if (normalizeGitRepo(repo) !== repo) {
144
+ return {
145
+ code: "not_grantable",
146
+ message: `${key} cannot be approved for a scheduled task: name one repository as git:push:<owner>/<repo>`,
147
+ };
148
+ }
149
+ return null;
150
+ }
92
151
  /** The prefix of a sandbox tier grant's key (sandbox dev tiers spec §11.4): only `tier:open` exists. */
93
152
  export const TIER_GRANT_PREFIX = "tier:";
94
153
  /** Whether a grant key is a sandbox tier grant (`tier:<tier>`). */
@@ -124,3 +183,27 @@ export function tierGrantRefusal(key, ctx) {
124
183
  }
125
184
  return null;
126
185
  }
186
+ /**
187
+ * Whether the org's `forbiddenTools` forbid the git push grant `key` (normalized): it lists
188
+ * `git:push` or the key, each entry compared normalized (`git:push:Acme/App` forbids
189
+ * `git:push:acme/app`).
190
+ */
191
+ export function gitPushForbidden(forbiddenTools, key) {
192
+ return forbiddenTools.some((entry) => {
193
+ const lowered = entry.trim().toLowerCase();
194
+ if (!isGitGrantKey(lowered))
195
+ return false;
196
+ const e = normalizeGitGrantKey(lowered);
197
+ return e === GIT_PUSH_GRANT || e === key;
198
+ });
199
+ }
200
+ /**
201
+ * A git push grant key as stored: the repository normalized ({@link normalizeGitRepo}); the key
202
+ * unchanged when its repository is not of the shape (then {@link gitGrantRefusal} refuses it).
203
+ */
204
+ export function normalizeGitGrantKey(key) {
205
+ if (key === GIT_PUSH_GRANT || !key.startsWith(GIT_PUSH_REPO_GRANT_PREFIX))
206
+ return key;
207
+ const repo = normalizeGitRepo(key.slice(GIT_PUSH_REPO_GRANT_PREFIX.length));
208
+ return repo === null ? key : gitPushGrantKey(repo);
209
+ }
@@ -255,9 +255,9 @@ export declare class ScheduledTasksService {
255
255
  /** Each task with its latest settled non-trial run, in one query. */
256
256
  private withLastRuns;
257
257
  /**
258
- * The chats the caller can point a `chat` task at (spec §8): per active connection of the org,
259
- * their DM threads (archived included) and, on a channel with threads, the groups they have a
260
- * thread in. Members up (`schedules.use`, the route's).
258
+ * The chats the caller can point a `chat` task at (spec §8): per active connection of the org
259
+ * whose extension is enabled in it, their DM threads (archived included) and, on a channel with
260
+ * threads, the groups they have a thread in. Members up (`schedules.use`, the route's).
261
261
  */
262
262
  chatTargets(orgId: string, accountId: string): Promise<ChatTarget[]>;
263
263
  /**
@@ -322,9 +322,9 @@ export declare class ScheduledTasksService {
322
322
  * - the mode and its fields agree (`conversation`: a conversation and no chat; `chat`: a chat
323
323
  * and no conversation; `new`: neither);
324
324
  * - `conversation`: the creator's live chat conversation with this agent, still accessible;
325
- * - `chat`: the connection is active and bound to this agent; a DM needs a thread row (archived
326
- * allowed) of the creator in that chat; a group needs the creator's thread row in the group
327
- * and a channel with threads.
325
+ * - `chat`: the connection is active, its channel extension is enabled in this org, and it is
326
+ * bound to this agent; a DM needs a thread row (archived allowed) of the creator in that chat;
327
+ * a group needs the creator's thread row in the group and a channel with threads.
328
328
  */
329
329
  private checkTarget;
330
330
  /** The channel's declared features; undefined when its extension is not loaded here. */
@@ -347,7 +347,9 @@ export declare class ScheduledTasksService {
347
347
  * `deviceId`, the task's device (null: none can be determined, the device rule is skipped),
348
348
  * stored with `spec_hash` `network` and no connector (plan P-D7), and only when
349
349
  * `source.fromSession` (a person on the task page; never a token, the Platform tool or the
350
- * schedule tools). Duplicates keep the last.
350
+ * schedule tools). Each git push key (`git:push`, `git:push:<owner>/<repo>`, the repository
351
+ * lower-cased) is refused by {@link gitGrantRefusal} and stored with `spec_hash` `git` and no
352
+ * connector, also only when `source.fromSession`. Duplicates keep the last.
351
353
  */
352
354
  private resolveGrants;
353
355
  /**