@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
@@ -13,7 +13,7 @@ import { connectorToolKey, parseToolKey, validateAllowHost, } from "@capybara-hq
13
13
  import { Inject, Injectable } from "@nestjs/common";
14
14
  import { sql } from "kysely";
15
15
  import { EventLog } from "../conversations/event-log.js";
16
- import { NETWORK_ASK_MAX_HOSTS, NETWORK_HOST_MAX_CHARS, } from "../conversations/events.js";
16
+ import { GIT_ASK_MAX_REFS, GIT_REF_MAX_CHARS, NETWORK_ASK_MAX_HOSTS, NETWORK_HOST_MAX_CHARS, } from "../conversations/events.js";
17
17
  import { conversationSourceOf } from "../conversations/source.js";
18
18
  import { ApiError } from "../errors.js";
19
19
  import { audit } from "../identity/audit.js";
@@ -32,9 +32,10 @@ import { RunResume } from "../runtime/run-jobs.js";
32
32
  import { rootConversationOf } from "../runtime/subagents/child.js";
33
33
  import { deviceNotFound, SandboxService } from "../sandbox/sandbox.service.js";
34
34
  import { readScheduleSettings } from "../schedules/effective-settings.js";
35
- import { grantRefusalOf, NETWORK_GRANT_PREFIX, NETWORK_GRANT_SPEC_HASH, networkGrantRefusal, } from "../schedules/grant-rules.js";
35
+ import { GIT_GRANT_SPEC_HASH, gitGrantRefusal, gitPushGrantKey, grantRefusalOf, NETWORK_GRANT_PREFIX, NETWORK_GRANT_SPEC_HASH, networkGrantRefusal, normalizeGitRepo, } from "../schedules/grant-rules.js";
36
36
  import { ScheduledTaskStore } from "../schedules/store.js";
37
37
  import { unattendedTaskRunOf } from "../schedules/unattended.js";
38
+ import { gitPushPolicyName, personalAuth } from "./connector-account-policies.js";
38
39
  import { pluginDisplayNames } from "./display-names.js";
39
40
  import { normalizeStoredDefinitionIn } from "./legacy-data.js";
40
41
  import { ToolRegistry } from "./registry.js";
@@ -46,17 +47,26 @@ export const APPROVALS_LIST_MAX = 100;
46
47
  * the device owner answered (spec §6.7).
47
48
  */
48
49
  export const NETWORK_ASK_CANCELLED_NOTE = "[network] The run was stopped before the user answered; access stays blocked.";
49
- /** The record without a network ask's kept command result (what routes and cards may show). */
50
+ /** The model-visible note after a git ask's kept result when its run is stopped before the answer. */
51
+ export const GIT_ASK_CANCELLED_NOTE = "[git] The run was stopped before the user answered; the push was not made.";
52
+ /** The record without an ask's kept command result (what routes and cards may show). */
50
53
  export function publicRecord(r) {
51
- if (r.detail === null)
52
- return { ...r, detail: null };
53
- const { result: _result, ...detail } = r.detail;
54
- return { ...r, detail };
54
+ const strip = (d) => {
55
+ if (d === null)
56
+ return null;
57
+ const { result: _result, ...rest } = d;
58
+ return rest;
59
+ };
60
+ return { ...r, detail: strip(r.detail), git: strip(r.git) };
55
61
  }
56
62
  /** The tool key a network ask is recorded under (the command ran in `Bash`). */
57
63
  export const NETWORK_ASK_TOOL_KEY = "Bash";
58
64
  /** The spec hash of a network ask (no tool definition is approved). */
59
65
  const NETWORK_ASK_SPEC_HASH = "network";
66
+ /** The tool key a git ask is recorded under (the push ran in a `Bash` command). */
67
+ export const GIT_ASK_TOOL_KEY = "Bash";
68
+ /** The spec hash of a git ask (no tool definition is approved). */
69
+ const GIT_ASK_SPEC_HASH = "git";
60
70
  const notFound = () => new ApiError("approval.not_found", "Approval not found");
61
71
  /**
62
72
  * A decision refused before it wrote anything (the ask closed, expired or is not the account's to
@@ -84,6 +94,7 @@ function toRecord(r) {
84
94
  attempt: r.attempt,
85
95
  kind: r.kind,
86
96
  detail: r.kind === "network" ? (r.detail ?? null) : null,
97
+ git: r.kind === "git" ? (r.detail ?? null) : null,
87
98
  toolKey: r.tool_key,
88
99
  specHash: r.spec_hash,
89
100
  connectorId: r.connector_id,
@@ -329,6 +340,109 @@ let ApprovalsService = class ApprovalsService {
329
340
  ]);
330
341
  return { record: rec, toSeq: Math.max(...written.map((e) => e.seq)) };
331
342
  }
343
+ /**
344
+ * A git push ask (sandbox git credentials spec §7): a pending approval of kind `git` for the
345
+ * actor whose credential the push used, expiring like a tool approval, and its
346
+ * `approval_requested` (tool key `Bash`, the display-only `git` field), in one transaction. The
347
+ * scopes are `once` (a run grant) and, for a personal connector (the push used the actor's own
348
+ * connection), `always` (the actor's standing allow for the repository); an org connector's push
349
+ * acts as the org, so no member allow applies to it. Takes the attempt after the call's other
350
+ * approvals; one per call: a second request returns the first.
351
+ */
352
+ async requestGit(input) {
353
+ const d = input.detail;
354
+ const repo = normalizeGitRepo(d.repo);
355
+ if (repo === null || repo !== d.repo) {
356
+ throw new Error(`a git ask names a normalized <owner>/<repo> (got ${d.repo})`);
357
+ }
358
+ if (d.host.length === 0 || d.host.length > NETWORK_HOST_MAX_CHARS) {
359
+ throw new Error("a git ask's host is 1 to 255 characters");
360
+ }
361
+ if (d.refs.length > GIT_ASK_MAX_REFS ||
362
+ d.refs.some((r) => r.length === 0 || r.length > GIT_REF_MAX_CHARS)) {
363
+ throw new Error(`a git ask lists at most ${GIT_ASK_MAX_REFS} refs of 1 to 255 characters`);
364
+ }
365
+ const { record, toSeq } = await this.requestGitIn(input);
366
+ if (toSeq !== null)
367
+ await this.eventLog.notify(record.conversationId, toSeq);
368
+ return record;
369
+ }
370
+ async requestGitIn(input) {
371
+ const hours = this.config.get().tools.approvalTimeoutHours;
372
+ const t = this.db.db;
373
+ // the conversation row first (as `requestIn`): it also serializes two asks of one call
374
+ await t
375
+ .selectFrom("conversations")
376
+ .select("id")
377
+ .where("id", "=", input.conversationId)
378
+ .forUpdate()
379
+ .execute();
380
+ // only a member of the org is asked, and only about a live connector of the org
381
+ const member = await this.roleOf(input.orgId, input.requestedFor);
382
+ const connector = await t
383
+ .selectFrom("connectors")
384
+ .select("auth")
385
+ .where("id", "=", input.connectorId)
386
+ .where("org_id", "=", input.orgId)
387
+ .where("deleted_at", "is", null)
388
+ .executeTakeFirst();
389
+ if (member === null || connector === undefined) {
390
+ throw new Error(`a git ask is for a member of the org about one of its connectors (connector ${input.connectorId})`);
391
+ }
392
+ const existing = await this.gitForCall(input.runId, input.callId);
393
+ if (existing !== null)
394
+ return { record: existing, toSeq: null };
395
+ const last = await t
396
+ .selectFrom("approvals")
397
+ .select(sql `max(attempt)`.as("attempt"))
398
+ .where("run_id", "=", input.runId)
399
+ .where("call_id", "=", input.callId)
400
+ .executeTakeFirst();
401
+ const scopes = personalAuth(connector.auth) ? ["once", "always"] : ["once"];
402
+ const row = await t
403
+ .insertInto("approvals")
404
+ .values({
405
+ id: randomUUID(),
406
+ org_id: input.orgId,
407
+ conversation_id: input.conversationId,
408
+ run_id: input.runId,
409
+ call_id: input.callId,
410
+ attempt: Number(last?.attempt ?? 0) + 1,
411
+ kind: "git",
412
+ detail: JSON.stringify(input.detail),
413
+ tool_key: GIT_ASK_TOOL_KEY,
414
+ spec_hash: GIT_ASK_SPEC_HASH,
415
+ connector_id: input.connectorId,
416
+ input: JSON.stringify({}),
417
+ requested_for: input.requestedFor,
418
+ approver_role: null,
419
+ scopes,
420
+ scope: null,
421
+ decided_by: null,
422
+ expires_at: sql `now() + ${hours}::integer * interval '1 hour'`,
423
+ })
424
+ .returningAll()
425
+ .executeTakeFirstOrThrow();
426
+ const rec = toRecord(row);
427
+ const written = await this.eventLog.appendIn(rec.conversationId, [
428
+ {
429
+ type: "approval_requested",
430
+ runId: rec.runId,
431
+ idemKey: `approval_requested:${rec.id}`,
432
+ payload: {
433
+ step: input.step,
434
+ callId: rec.callId,
435
+ approvalId: rec.id,
436
+ toolKey: rec.toolKey,
437
+ expiresAt: rec.expiresAt,
438
+ git: { host: input.detail.host, repo: input.detail.repo, refs: input.detail.refs },
439
+ requestedFor: rec.requestedFor,
440
+ approverRole: null,
441
+ },
442
+ },
443
+ ]);
444
+ return { record: rec, toSeq: Math.max(...written.map((e) => e.seq)) };
445
+ }
332
446
  /** 当前用户能处理的审批(新的在前,最多 {@link APPROVALS_LIST_MAX} 条);`status` 缺省为全部。 */
333
447
  async list(orgId, accountId, status) {
334
448
  const role = await this.roleOf(orgId, accountId);
@@ -401,6 +515,9 @@ let ApprovalsService = class ApprovalsService {
401
515
  { path: "hosts", message: "Only a network ask takes hosts" },
402
516
  ]);
403
517
  }
518
+ if (kind.kind === "git") {
519
+ return this.decideGit(orgId, id, accountId, body, actorTokenId, via);
520
+ }
404
521
  const { record, toSeq } = await this.decideIn(orgId, id, accountId, body, actorTokenId);
405
522
  await this.announceDecided(record, toSeq);
406
523
  return record;
@@ -496,6 +613,52 @@ let ApprovalsService = class ApprovalsService {
496
613
  await this.announceDecided(decided.record, decided.toSeq);
497
614
  return decided.record;
498
615
  }
616
+ /**
617
+ * A git push ask's decision (sandbox git credentials spec §7): only the asked actor
618
+ * (`requested_for`) decides, in a signed-in session (a personal access token is refused, as for
619
+ * network asks); `once` is a run grant (the row itself), `always` also writes the actor's
620
+ * standing allow for the repository on that host (`connector_tool_account_policies`,
621
+ * `git_push:<host>/<owner>/<repo>`) in the decision's transaction. The run resumes after the
622
+ * commit.
623
+ */
624
+ async decideGit(orgId, id, accountId, body, actorTokenId, via) {
625
+ const row = await this.db
626
+ .selectFrom("approvals")
627
+ .selectAll()
628
+ .where("id", "=", id)
629
+ .where("org_id", "=", orgId)
630
+ .executeTakeFirst();
631
+ if (!row)
632
+ throw notFound();
633
+ const ask = toRecord(row);
634
+ if (ask.requestedFor !== accountId) {
635
+ // named only to the people the ask is shown to (the run's starter); any other member
636
+ // learns nothing about it, as `get` (approval.not_found)
637
+ const trigger = await this.db
638
+ .selectFrom("runs")
639
+ .select("trigger_account_id")
640
+ .where("id", "=", ask.runId)
641
+ .executeTakeFirst();
642
+ if (accountId !== trigger?.trigger_account_id)
643
+ throw notFound();
644
+ const name = ask.requestedFor === null ? null : await this.displayNameOf(ask.requestedFor);
645
+ throw new ApiError("approval.not_yours", `Only ${name ?? "the person whose connection the push used"} can allow this push.`);
646
+ }
647
+ if (actorTokenId !== undefined) {
648
+ throw new ApiError("auth.session_required", "Git pushes are decided in a signed-in session, not with a personal access token.");
649
+ }
650
+ let git = { outcome: null, via };
651
+ if (body.decision === "approve") {
652
+ const scope = body.scope ?? "once";
653
+ if (scope === "session" || !ask.scopes.includes(scope)) {
654
+ throw new ApiError("approval.scope_not_allowed", `Scope "${scope}" is not allowed for this approval; allowed: ${ask.scopes.join(", ")}`);
655
+ }
656
+ git = { outcome: scope, via };
657
+ }
658
+ const decided = await this.decideIn(orgId, id, accountId, body, actorTokenId, undefined, git);
659
+ await this.announceDecided(decided.record, decided.toSeq);
660
+ return decided.record;
661
+ }
499
662
  /**
500
663
  * 「这台设备一直允许」 (transactional outbox spec §7): the intent of the device change (naming the
501
664
  * ask) is recorded, the `policy` op sent (no transaction open), then {@link settleNetworkOp}
@@ -788,14 +951,14 @@ let ApprovalsService = class ApprovalsService {
788
951
  }
789
952
  // its own transaction (as the legacy one was): `approval.decided` is emitted only after the
790
953
  // decision committed, even when a caller has a transaction open
791
- decideIn(orgId, id, accountId, body, actorTokenId, network) {
792
- return this.decideTx(orgId, id, accountId, body, actorTokenId, network);
954
+ decideIn(orgId, id, accountId, body, actorTokenId, network, git) {
955
+ return this.decideTx(orgId, id, accountId, body, actorTokenId, network, git);
793
956
  }
794
957
  /**
795
958
  * A decision in the caller's transaction: lock order conversation → approval → run (the resume).
796
959
  * Refuses (`approval.already_decided`, `approval.not_found`, …) before any write.
797
960
  */
798
- async decideTx(orgId, id, accountId, body, actorTokenId, network) {
961
+ async decideTx(orgId, id, accountId, body, actorTokenId, network, git) {
799
962
  const t = this.db.db;
800
963
  // lock order: the conversation, the approval, then the run (as a stop of the waiting run)
801
964
  if (!(await this.lockConversationOf(id, orgId)))
@@ -830,19 +993,23 @@ let ApprovalsService = class ApprovalsService {
830
993
  if (locked.expired) {
831
994
  throw new ApiError("approval.already_decided", "Approval has expired");
832
995
  }
833
- // a network ask is decided only with what took effect (decideNetwork, the op's resolver)
834
- if ((before.kind === "network") !== (network !== undefined)) {
996
+ // a network ask is decided only through decideNetwork (its policy op went first), a git ask
997
+ // only through decideGit
998
+ if ((before.kind === "network") !== (network !== undefined) ||
999
+ (before.kind === "git") !== (git !== undefined)) {
835
1000
  throw new Error(`approval ${id} is of kind ${before.kind}`);
836
1001
  }
837
1002
  const approved = body.decision === "approve";
838
1003
  const reason = approved ? undefined : body.reason?.trim() || undefined;
839
1004
  const scope = !approved
840
1005
  ? null
841
- : network === undefined
842
- ? (body.scope ?? "once")
843
- : network.outcome === "always" || network.outcome === "refused"
844
- ? "always"
845
- : "once";
1006
+ : git !== undefined
1007
+ ? (git.outcome ?? "once")
1008
+ : network === undefined
1009
+ ? (body.scope ?? "once")
1010
+ : network.outcome === "always" || network.outcome === "refused"
1011
+ ? "always"
1012
+ : "once";
846
1013
  // 按角色升级的批准只对这一次有效:授权记在发起人名下会让其以后绕过角色要求
847
1014
  if (scope !== null && scope !== "once" && before.requestedFor === null) {
848
1015
  throw new ApiError("validation_failed", 'Only scope "once" is allowed for role-escalated approvals', [{ path: "scope", message: 'Only "once" is allowed for role-escalated approvals' }]);
@@ -871,6 +1038,10 @@ let ApprovalsService = class ApprovalsService {
871
1038
  ...(network.fallback !== undefined && { fallback: network.fallback }),
872
1039
  }),
873
1040
  }),
1041
+ ...(git?.outcome != null &&
1042
+ before.git !== null && {
1043
+ detail: JSON.stringify({ ...before.git, outcome: git.outcome }),
1044
+ }),
874
1045
  })
875
1046
  .where("id", "=", id)
876
1047
  .returningAll()
@@ -881,6 +1052,13 @@ let ApprovalsService = class ApprovalsService {
881
1052
  if (network !== undefined) {
882
1053
  // a network ask grants nothing of the tool kind: its run grant is the row itself
883
1054
  }
1055
+ else if (git !== undefined) {
1056
+ // a git ask's run grant is the row itself; `always` is the actor's standing allow for the
1057
+ // repository on the connector (a row the connector's deletion removes with it)
1058
+ if (git.outcome === "always" && before.git !== null && connectorId !== null) {
1059
+ await this.allowRepoIn(orgId, connectorId, accountId, before.git.host, before.git.repo);
1060
+ }
1061
+ }
884
1062
  else if (scope === "session") {
885
1063
  // a sub-agent's grant is the root conversation's (agent map spec §7): the root and every
886
1064
  // agent of its tree, at any depth, are not asked again for the same call
@@ -917,9 +1095,13 @@ let ApprovalsService = class ApprovalsService {
917
1095
  }))
918
1096
  .execute();
919
1097
  }
920
- if (approved && scope !== null && network === undefined) {
1098
+ if (approved && scope !== null && network === undefined && git === undefined) {
921
1099
  await this.keepTrialGrant(before, accountId);
922
1100
  }
1101
+ // 「这次允许」 of a push in a trial run: the task's grant for that repository; a standing
1102
+ // allow needs none (the member's policy already allows it)
1103
+ if (git?.outcome === "once")
1104
+ await this.keepTrialGitGrant(before, accountId);
923
1105
  // 「这次允许」 in a trial run (also an 「一直允许」 the device could not take): the task's
924
1106
  // grants; one the device took needs none (network ask-to-allow spec §8.1)
925
1107
  if (network?.outcome === "once" || network?.outcome === "once_fallback") {
@@ -973,6 +1155,14 @@ let ApprovalsService = class ApprovalsService {
973
1155
  action: "approval.decided",
974
1156
  target: id,
975
1157
  detail: {
1158
+ ...(git !== undefined &&
1159
+ before.git !== null && {
1160
+ kind: "git",
1161
+ host: before.git.host,
1162
+ repo: before.git.repo,
1163
+ connectorId,
1164
+ via: git.via,
1165
+ }),
976
1166
  toolKey: before.toolKey,
977
1167
  conversationId: before.conversationId,
978
1168
  runId: before.runId,
@@ -1088,6 +1278,60 @@ let ApprovalsService = class ApprovalsService {
1088
1278
  });
1089
1279
  }
1090
1280
  }
1281
+ /**
1282
+ * The actor's standing allow for pushes to `repo` through the connector (sandbox git
1283
+ * credentials spec §7, 「一直允许」): a `connector_tool_account_policies` row named
1284
+ * `git_push:<host>/<owner>/<repo>` with policy `allow`, in the decision's transaction.
1285
+ */
1286
+ async allowRepoIn(orgId, connectorId, accountId, host, repo) {
1287
+ await this.db.db
1288
+ .insertInto("connector_tool_account_policies")
1289
+ .values({
1290
+ org_id: orgId,
1291
+ connector_id: connectorId,
1292
+ account_id: accountId,
1293
+ tool_name: gitPushPolicyName(host, repo),
1294
+ policy: "allow",
1295
+ })
1296
+ .onConflict((oc) => oc
1297
+ .columns(["connector_id", "account_id", "tool_name"])
1298
+ .doUpdateSet({ policy: "allow", updated_at: sql `now()` }))
1299
+ .execute();
1300
+ }
1301
+ /**
1302
+ * A trial run of a scheduled task (sandbox git credentials spec §7): a push its creator allowed
1303
+ * for this run (「这次允许」) becomes the task's `git:push:<owner>/<repo>` grant, when
1304
+ * `gitGrantRefusal` allows it (the org does not forbid it). Never for an ask another account
1305
+ * decided or one asked of someone else. In the decision's transaction.
1306
+ */
1307
+ async keepTrialGitGrant(a, decidedBy) {
1308
+ if (a.git === null)
1309
+ return;
1310
+ if (a.requestedFor === null || a.requestedFor !== decidedBy)
1311
+ return;
1312
+ const t = this.db.db;
1313
+ const served = await unattendedTaskRunOf(t, a.runId);
1314
+ if (served === null || !served.trial)
1315
+ return;
1316
+ const task = await t
1317
+ .selectFrom("scheduled_tasks")
1318
+ .select("account_id")
1319
+ .where("id", "=", served.taskId)
1320
+ .where("org_id", "=", a.orgId)
1321
+ .where("deleted_at", "is", null)
1322
+ .executeTakeFirst();
1323
+ if (task?.account_id !== decidedBy)
1324
+ return;
1325
+ const key = gitPushGrantKey(a.git.repo);
1326
+ const { forbiddenTools } = await readScheduleSettings(t, this.config, a.orgId);
1327
+ if (gitGrantRefusal(key, { forbiddenTools }) !== null)
1328
+ return;
1329
+ await this.schedules.addGrant(t, served.taskId, {
1330
+ toolKey: key,
1331
+ specHash: GIT_GRANT_SPEC_HASH,
1332
+ connectorId: null,
1333
+ });
1334
+ }
1091
1335
  /**
1092
1336
  * 详情,不做处理人校验(内核内部用:渠道出站投递组装 IM 审批卡片);不存在返回 null。
1093
1337
  * 不得直接回给请求方——HTTP 接口用 {@link get}。
@@ -1129,6 +1373,19 @@ let ApprovalsService = class ApprovalsService {
1129
1373
  .executeTakeFirst();
1130
1374
  return row ? toRecord(row) : null;
1131
1375
  }
1376
+ /** The git push ask of a tool call (at most one per call); null when it has none. */
1377
+ async gitForCall(runId, callId) {
1378
+ const row = await this.db
1379
+ .selectFrom("approvals")
1380
+ .selectAll()
1381
+ .where("run_id", "=", runId)
1382
+ .where("call_id", "=", callId)
1383
+ .where("kind", "=", "git")
1384
+ .orderBy("attempt", "desc")
1385
+ .limit(1)
1386
+ .executeTakeFirst();
1387
+ return row ? toRecord(row) : null;
1388
+ }
1132
1389
  /**
1133
1390
  * 超时(等待机制的超时任务调用):仍 pending 且已到期 → 置 expired、写 `approval_decided{expired}`(处理人为 null)。
1134
1391
  * 与人工决定都锁同一行,恰好一个生效。返回 `expired`(本次生效)/ `not_due`(尚未到期)/
@@ -1254,7 +1511,8 @@ let ApprovalsService = class ApprovalsService {
1254
1511
  /**
1255
1512
  * In the caller's transaction (`mandatory`; it wrote `authorAccountId`'s message in
1256
1513
  * `conversationId` and holds that conversation's row lock): the message skips the pending
1257
- * network asks it closes (network ask-to-allow spec §6.7), as it skips questions: the asks of
1514
+ * network and git asks it closes (network ask-to-allow spec §6.7; a git push ask follows the
1515
+ * same flow, sandbox git credentials spec §7), as it skips questions: the asks of
1258
1516
  * active runs `authorAccountId` started, in the conversation and in its sub-agents'
1259
1517
  * conversations at any depth (their asks are shown in the root). Each becomes `skipped` with
1260
1518
  * `approval_decided{skipped}` by the author; access stays blocked. Returns the skipped records and
@@ -1272,7 +1530,7 @@ let ApprovalsService = class ApprovalsService {
1272
1530
  .selectFrom("approvals")
1273
1531
  .select("conversation_id")
1274
1532
  .where("conversation_id", "in", tree)
1275
- .where("kind", "=", "network")
1533
+ .where("kind", "in", ["network", "git"])
1276
1534
  .where("status", "=", "pending"));
1277
1535
  if (convs.length === 0)
1278
1536
  return { records: [], seqs: new Map() };
@@ -1281,7 +1539,7 @@ let ApprovalsService = class ApprovalsService {
1281
1539
  .innerJoin("runs as r", "r.id", "a.run_id")
1282
1540
  .selectAll("a")
1283
1541
  .where("a.conversation_id", "in", convs)
1284
- .where("a.kind", "=", "network")
1542
+ .where("a.kind", "in", ["network", "git"])
1285
1543
  .where("a.status", "=", "pending")
1286
1544
  .where(sql `a.expires_at > now()`)
1287
1545
  .where("r.status", "in", ["running", "waiting"])
@@ -1522,6 +1780,9 @@ __decorate([
1522
1780
  __decorate([
1523
1781
  Transactional()
1524
1782
  ], ApprovalsService.prototype, "requestNetworkIn", null);
1783
+ __decorate([
1784
+ Transactional()
1785
+ ], ApprovalsService.prototype, "requestGitIn", null);
1525
1786
  __decorate([
1526
1787
  WithLock({ key: "network-ask-#{1}", ttl: 60_000, waitTimeout: 45_000 })
1527
1788
  ], ApprovalsService.prototype, "decideNetwork", null);
@@ -1552,6 +1813,12 @@ __decorate([
1552
1813
  __decorate([
1553
1814
  Transactional({ propagation: "mandatory" })
1554
1815
  ], ApprovalsService.prototype, "keepTrialNetworkGrant", null);
1816
+ __decorate([
1817
+ Transactional({ propagation: "mandatory" })
1818
+ ], ApprovalsService.prototype, "allowRepoIn", null);
1819
+ __decorate([
1820
+ Transactional({ propagation: "mandatory" })
1821
+ ], ApprovalsService.prototype, "keepTrialGitGrant", null);
1555
1822
  __decorate([
1556
1823
  Transactional()
1557
1824
  ], ApprovalsService.prototype, "expireIn", null);
@@ -31,6 +31,7 @@ export declare const BUILTIN: {
31
31
  readonly ScheduleCreate: "ScheduleCreate";
32
32
  readonly ScheduleUpdate: "ScheduleUpdate";
33
33
  readonly ScheduleList: "ScheduleList";
34
+ readonly ScheduleTargets: "ScheduleTargets";
34
35
  readonly ScheduleDelete: "ScheduleDelete";
35
36
  };
36
37
  export type BuiltinName = (typeof BUILTIN)[keyof typeof BUILTIN];
@@ -55,7 +56,8 @@ export declare const BASH_COMPANION_TOOLS: readonly BuiltinName[];
55
56
  export declare const SANDBOX_ASK_TOOL_NAMES: ReadonlySet<string>;
56
57
  /**
57
58
  * The builtins that manage the person's scheduled tasks (scheduled tasks spec §11): create and
58
- * update are fixed asks ({@link R3_FIXED_ASK_TOOL_KEYS}), list and delete only read or narrow.
59
+ * update are fixed asks ({@link R3_FIXED_ASK_TOOL_KEYS}), list, targets and delete only read or
60
+ * narrow.
59
61
  */
60
62
  export declare const SCHEDULE_TOOL_NAMES: readonly BuiltinName[];
61
63
  /**
@@ -108,7 +110,7 @@ export declare const R3_FIXED_ASK_TOOL_KEYS: ReadonlySet<string>;
108
110
  * person manages tasks from the conversation they see, never from a child). `Agent` is removed
109
111
  * only from a child at `subagents.maxDepth` (agent map spec §4.1, `effectiveDefinition`).
110
112
  */
111
- export declare const CHILD_REMOVED_TOOLS: readonly ["AskUserQuestion", "Deliver", "ScheduleCreate", "ScheduleUpdate", "ScheduleList", "ScheduleDelete"];
113
+ export declare const CHILD_REMOVED_TOOLS: readonly ["AskUserQuestion", "Deliver", "ScheduleCreate", "ScheduleUpdate", "ScheduleList", "ScheduleTargets", "ScheduleDelete"];
112
114
  /**
113
115
  * The builtins every agent has unless it turns them off (tools default-on spec §4), in this
114
116
  * order: {@link BUILTIN_TOOL_NAMES} without the ones the registry manages by rule (`ToolSearch`,
@@ -31,6 +31,7 @@ export const BUILTIN = {
31
31
  ScheduleCreate: "ScheduleCreate",
32
32
  ScheduleUpdate: "ScheduleUpdate",
33
33
  ScheduleList: "ScheduleList",
34
+ ScheduleTargets: "ScheduleTargets",
34
35
  ScheduleDelete: "ScheduleDelete",
35
36
  };
36
37
  /**
@@ -74,12 +75,14 @@ export const SANDBOX_ASK_TOOL_NAMES = new Set([
74
75
  ]);
75
76
  /**
76
77
  * The builtins that manage the person's scheduled tasks (scheduled tasks spec §11): create and
77
- * update are fixed asks ({@link R3_FIXED_ASK_TOOL_KEYS}), list and delete only read or narrow.
78
+ * update are fixed asks ({@link R3_FIXED_ASK_TOOL_KEYS}), list, targets and delete only read or
79
+ * narrow.
78
80
  */
79
81
  export const SCHEDULE_TOOL_NAMES = [
80
82
  BUILTIN.ScheduleCreate,
81
83
  BUILTIN.ScheduleUpdate,
82
84
  BUILTIN.ScheduleList,
85
+ BUILTIN.ScheduleTargets,
83
86
  BUILTIN.ScheduleDelete,
84
87
  ];
85
88
  /**
@@ -160,6 +163,7 @@ export const CHILD_REMOVED_TOOLS = [
160
163
  BUILTIN.ScheduleCreate,
161
164
  BUILTIN.ScheduleUpdate,
162
165
  BUILTIN.ScheduleList,
166
+ BUILTIN.ScheduleTargets,
163
167
  BUILTIN.ScheduleDelete,
164
168
  ];
165
169
  /**
@@ -198,6 +202,7 @@ export const BUILTIN_TOOL_KINDS = {
198
202
  [BUILTIN.ScheduleCreate]: "tool",
199
203
  [BUILTIN.ScheduleUpdate]: "tool",
200
204
  [BUILTIN.ScheduleList]: "tool",
205
+ [BUILTIN.ScheduleTargets]: "tool",
201
206
  [BUILTIN.ScheduleDelete]: "tool",
202
207
  };
203
208
  /**
@@ -9,9 +9,9 @@ import type { ScheduleChat, ScheduledTask, ScheduleTrigger } from "../../schedul
9
9
  import type { BuiltinTool } from "../registry.js";
10
10
  /**
11
11
  * The schedule builtins (scheduled tasks spec §11): `ScheduleCreate`, `ScheduleUpdate`,
12
- * `ScheduleList` and `ScheduleDelete` manage the scheduled tasks of the run's trigger account
13
- * through the same service as the HTTP routes (`ScheduledTasksService`), so the org's limits and
14
- * the grant rules apply unchanged.
12
+ * `ScheduleList`, `ScheduleTargets` and `ScheduleDelete` manage the scheduled tasks of the run's
13
+ * trigger account through the same service as the HTTP routes (`ScheduledTasksService`), so the
14
+ * org's limits and the grant rules apply unchanged.
15
15
  *
16
16
  * - `ScheduleCreate` / `ScheduleUpdate` are fixed asks (R3, `R3_FIXED_ASK_TOOL_KEYS`) with a
17
17
  * per-call policy that never remembers: every call asks, no grant (conversation, always or task)
@@ -21,15 +21,17 @@ import type { BuiltinTool } from "../registry.js";
21
21
  * - The approved tools of a task are exactly the `approvedTools` of the approved input: create
22
22
  * grants that list, and an update that passes it replaces the task's grants with it (the card
23
23
  * lists the full new set), so nothing beyond what the person saw is ever granted.
24
- * - `ScheduleList` (allow) lists the person's own tasks; `ScheduleDelete` (allow; deleting only
25
- * narrows) deletes only the person's own task, even when they are an org admin.
24
+ * - `ScheduleList` (allow) lists the person's own tasks; `ScheduleTargets` (allow) lists the IM
25
+ * chats they can deliver to (the service's `chatTargets`, the same data as the route);
26
+ * `ScheduleDelete` (allow; deleting only narrows) deletes only the person's own task, even when
27
+ * they are an org admin.
26
28
  * - `mode: "here"`: a `chat` task for this conversation's IM direct chat when the conversation is
27
29
  * delivered to one (`channel_deliveries`) and is the live DM thread of the person
28
- * (`channel_threads`), else a `conversation` task for this conversation. The zone is the run's
29
- * (`run_context`, else the org's, else UTC).
30
+ * (`channel_threads`), else a `conversation` task for this conversation. `mode: "chat"` posts into
31
+ * the IM chat named by `chat`. The zone is the run's (`run_context`, else the org's, else UTC).
30
32
  */
31
33
  /** The task service as the tools use it (bound at start; null when not loaded here). */
32
- export type ScheduleService = Pick<ScheduledTasksService, "create" | "update" | "list" | "remove" | "addGrants" | "removeGrant">;
34
+ export type ScheduleService = Pick<ScheduledTasksService, "create" | "update" | "list" | "remove" | "addGrants" | "removeGrant" | "chatTargets">;
33
35
  /**
34
36
  * Where the schedule tools find the task service: a registration port. `SchedulesModule` imports
35
37
  * `ToolsModule` (its grants resolve the agent's tools through `ToolRegistry`, which lists these
@@ -102,6 +104,6 @@ export declare class ScheduleUpdates {
102
104
  constructor(txHost: TransactionHost, db: KernelDb, store: ScheduledTaskStore);
103
105
  apply(svc: Pick<ScheduleService, "update" | "addGrants" | "removeGrant">, orgId: string, accountId: string, task: ScheduledTask, patch: UpdateTaskInput, approvedTools: readonly string[] | undefined): Promise<ScheduledTask>;
104
106
  }
105
- /** The four schedule builtins. */
107
+ /** The five schedule builtins. */
106
108
  export declare function scheduleTools(deps: ScheduleToolsDeps): BuiltinTool[];
107
109
  export {};