@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
@@ -0,0 +1,121 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Inject, Injectable } from "@nestjs/common";
11
+ import { audit } from "../identity/audit.js";
12
+ import { KernelDb } from "../infra/database/kernel-db.js";
13
+ import { KERNEL_LOGGER, REDIS } from "../infra/tokens.js";
14
+ import { GitTickets } from "./tickets.js";
15
+ /**
16
+ * What a run's end does for the git gateway (sandbox git credentials spec §9): revokes the run's
17
+ * tickets and writes the `git.fetch` summaries. `RunFinish` calls it in `afterCommit` of the
18
+ * transaction that set the terminal status (set first, then revoke: `GitTickets.resolve` already
19
+ * refuses a run that is not active, so revocation is cleanup and defense in depth).
20
+ */
21
+ /** The Redis hash of a run's fetch counters: fields `<host>/<repo>|n` and `<host>/<repo>|bytes`. */
22
+ export const gitFetchKey = (runId) => `git-fetch:${runId}`;
23
+ /**
24
+ * Counters of a run that never ends cleanly (a lost end hook) expire after this. A fetch still in
25
+ * flight after the run ended writes a counter nobody summarises: it expires by this TTL too.
26
+ */
27
+ export const FETCH_COUNTER_TTL_SECONDS = 24 * 60 * 60;
28
+ /** Adds one fetch request and its bytes to the run's counters (atomic, with a TTL). */
29
+ export async function recordGitFetch(redis, runId, host, repo, bytes, countRequest) {
30
+ const key = gitFetchKey(runId);
31
+ const field = `${host}/${repo}`;
32
+ const m = redis.multi();
33
+ if (countRequest)
34
+ m.hincrby(key, `${field}|n`, 1);
35
+ m.hincrby(key, `${field}|bytes`, Math.max(0, Math.round(bytes)));
36
+ await m.expire(key, FETCH_COUNTER_TTL_SECONDS).exec();
37
+ }
38
+ /** Reads and deletes a hash in one step, so a summary is written once. */
39
+ const TAKE_HASH = `
40
+ local all = redis.call('HGETALL', KEYS[1])
41
+ redis.call('DEL', KEYS[1])
42
+ return all
43
+ `;
44
+ let GitRunEnd = class GitRunEnd {
45
+ tickets;
46
+ db;
47
+ redis;
48
+ log;
49
+ constructor(tickets, db, redis, log) {
50
+ this.tickets = tickets;
51
+ this.db = db;
52
+ this.redis = redis;
53
+ this.log = log;
54
+ }
55
+ /** Run end: revokes the run's tickets and writes one `git.fetch` audit row per repository. */
56
+ async onRunFinished(runId) {
57
+ // both steps always run: a failing revocation must not lose the summaries, nor the reverse
58
+ const results = await Promise.allSettled([
59
+ this.tickets.revokeRun(runId),
60
+ this.writeFetchSummaries(runId),
61
+ ]);
62
+ for (const r of results) {
63
+ if (r.status === "rejected") {
64
+ this.log.warn("git run end failed", {
65
+ runId,
66
+ error: r.reason instanceof Error ? r.reason.name : "unknown",
67
+ });
68
+ }
69
+ }
70
+ }
71
+ async writeFetchSummaries(runId) {
72
+ const flat = (await this.redis.eval(TAKE_HASH, 1, gitFetchKey(runId)));
73
+ if (flat.length === 0)
74
+ return;
75
+ const perRepo = new Map();
76
+ for (let i = 0; i + 1 < flat.length; i += 2) {
77
+ const field = String(flat[i]);
78
+ const cut = field.lastIndexOf("|");
79
+ const target = field.slice(0, cut);
80
+ const entry = perRepo.get(target) ?? { count: 0, bytes: 0 };
81
+ if (field.slice(cut + 1) === "n")
82
+ entry.count = Number(flat[i + 1]);
83
+ else
84
+ entry.bytes = Number(flat[i + 1]);
85
+ perRepo.set(target, entry);
86
+ }
87
+ const run = await this.db.db
88
+ .selectFrom("runs")
89
+ .innerJoin("conversations", "conversations.id", "runs.conversation_id")
90
+ .select(["runs.conversation_id", "runs.trigger_account_id", "conversations.org_id"])
91
+ .where("runs.id", "=", runId)
92
+ .executeTakeFirst();
93
+ if (run === undefined)
94
+ return;
95
+ for (const [target, { count, bytes }] of perRepo) {
96
+ const slash = target.indexOf("/");
97
+ await audit(this.db.db, {
98
+ actorAccountId: run.trigger_account_id,
99
+ orgId: run.org_id,
100
+ action: "git.fetch",
101
+ target,
102
+ detail: {
103
+ runId,
104
+ conversationId: run.conversation_id,
105
+ host: target.slice(0, slash),
106
+ repo: target.slice(slash + 1),
107
+ count,
108
+ bytes,
109
+ },
110
+ });
111
+ }
112
+ }
113
+ };
114
+ GitRunEnd = __decorate([
115
+ Injectable(),
116
+ __param(0, Inject(GitTickets)),
117
+ __param(1, Inject(KernelDb)),
118
+ __param(2, Inject(REDIS)),
119
+ __param(3, Inject(KERNEL_LOGGER))
120
+ ], GitRunEnd);
121
+ export { GitRunEnd };
@@ -0,0 +1,42 @@
1
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
2
+ import { KernelDb } from "../infra/database/kernel-db.js";
3
+ import { Credentials } from "../tools/credentials.js";
4
+ /**
5
+ * Git sources (sandbox git credentials spec §6): which connector's credential the git gateway
6
+ * presents to a git host for one actor. Everything is read live per call (no process memory).
7
+ */
8
+ /** The source of one git host for one actor. */
9
+ export interface GitSourceFor {
10
+ /** The git host (from the connector's `git_hosts`), e.g. `github.com`. */
11
+ host: string;
12
+ connectorId: string;
13
+ /** The basic username the host expects with a token password (GitHub: `x-access-token`). */
14
+ username: string;
15
+ /** An org connector an admin turned on for git: pushes are the bot's, not the person's. */
16
+ bot: boolean;
17
+ }
18
+ export declare class GitSources {
19
+ private readonly db;
20
+ private readonly credentials;
21
+ /** Read per call: `tools.git.enabled` is hot. */
22
+ private readonly config;
23
+ constructor(db: KernelDb, credentials: Pick<Credentials, "status">,
24
+ /** Read per call: `tools.git.enabled` is hot. */
25
+ config: Pick<KernelConfigService, "get">);
26
+ /**
27
+ * The git sources of `accountId` in `orgId`, one per host (spec §6.2):
28
+ *
29
+ * - nothing while the gateway is off (`tools.git.enabled`, the org's `gitGateway.enabled`), for
30
+ * an account that is not an active member of the live org, or for an anonymous channel guest;
31
+ * - one connector per host for the whole org ("never mixed"): the org's
32
+ * `gitGateway.hostSources[host]` pick; without one, among the org's live, active connectors
33
+ * declaring the host (personal-ownership ones, and org-credential ones only with
34
+ * `use_for_git`): a personal one before a bot (a bot is the default only when no personal
35
+ * connector declares the host), the authorization-code (`oauth_code`) one first, else the
36
+ * oldest;
37
+ * - the account gets that connector only when it can use it: its own credential for a
38
+ * personal one (never another account's), the org credential for a bot (`bot: true`);
39
+ * otherwise no source for the host, never another connector.
40
+ */
41
+ sourcesFor(orgId: string, accountId: string): Promise<GitSourceFor[]>;
42
+ }
@@ -0,0 +1,134 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { Inject, Injectable } from "@nestjs/common";
11
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
12
+ import { KernelDb } from "../infra/database/kernel-db.js";
13
+ import { isAnonymousGuest } from "../orgs/members.service.js";
14
+ import { readOrgSettings } from "../orgs/settings.service.js";
15
+ import { Credentials, isOAuth } from "../tools/credentials.js";
16
+ /**
17
+ * The org-wide default among a host's candidates (no pick): personal-ownership connectors before
18
+ * `use_for_git` bots, then the authorization-code one, then the oldest, then the lowest id.
19
+ */
20
+ function byDefault(a, b) {
21
+ return (Number(a.bot) - Number(b.bot) ||
22
+ Number(b.oauthCode) - Number(a.oauthCode) ||
23
+ a.createdAt.getTime() - b.createdAt.getTime() ||
24
+ a.connectorId.localeCompare(b.connectorId));
25
+ }
26
+ /** Auth types whose credential can be presented as a basic password (not `none` / `headers`). */
27
+ const gitCapable = (auth) => auth.type === "bearer" || auth.type === "api_key" || auth.type === "basic" || isOAuth(auth);
28
+ let GitSources = class GitSources {
29
+ db;
30
+ credentials;
31
+ config;
32
+ constructor(db, credentials,
33
+ /** Read per call: `tools.git.enabled` is hot. */
34
+ config) {
35
+ this.db = db;
36
+ this.credentials = credentials;
37
+ this.config = config;
38
+ }
39
+ /**
40
+ * The git sources of `accountId` in `orgId`, one per host (spec §6.2):
41
+ *
42
+ * - nothing while the gateway is off (`tools.git.enabled`, the org's `gitGateway.enabled`), for
43
+ * an account that is not an active member of the live org, or for an anonymous channel guest;
44
+ * - one connector per host for the whole org ("never mixed"): the org's
45
+ * `gitGateway.hostSources[host]` pick; without one, among the org's live, active connectors
46
+ * declaring the host (personal-ownership ones, and org-credential ones only with
47
+ * `use_for_git`): a personal one before a bot (a bot is the default only when no personal
48
+ * connector declares the host), the authorization-code (`oauth_code`) one first, else the
49
+ * oldest;
50
+ * - the account gets that connector only when it can use it: its own credential for a
51
+ * personal one (never another account's), the org credential for a bot (`bot: true`);
52
+ * otherwise no source for the host, never another connector.
53
+ */
54
+ async sourcesFor(orgId, accountId) {
55
+ if (!this.config.get().tools.git.enabled)
56
+ return [];
57
+ const db = this.db.db;
58
+ const member = await db
59
+ .selectFrom("memberships as m")
60
+ .innerJoin("accounts as a", "a.id", "m.account_id")
61
+ .innerJoin("orgs as o", "o.id", "m.org_id")
62
+ .select("m.account_id")
63
+ .where("m.org_id", "=", orgId)
64
+ .where("m.account_id", "=", accountId)
65
+ .where("a.status", "=", "active")
66
+ .where("o.deleted_at", "is", null)
67
+ .executeTakeFirst();
68
+ if (member === undefined)
69
+ return [];
70
+ // anonymous channel guests never get a credential (spec §6.2)
71
+ if (await isAnonymousGuest(db, accountId))
72
+ return [];
73
+ const settings = await readOrgSettings(db, orgId);
74
+ if (settings.gitGateway?.enabled === false)
75
+ return [];
76
+ const rows = await db
77
+ .selectFrom("connectors")
78
+ .select(["id", "auth", "git_hosts", "use_for_git", "created_at"])
79
+ .where("org_id", "=", orgId)
80
+ .where("deleted_at", "is", null)
81
+ .where("status", "=", "active")
82
+ .where("git_hosts", "is not", null)
83
+ .execute();
84
+ const byHost = new Map();
85
+ for (const r of rows) {
86
+ const auth = r.auth;
87
+ if (!gitCapable(auth))
88
+ continue;
89
+ const bot = auth.ownership === "org";
90
+ if (bot && !r.use_for_git)
91
+ continue;
92
+ for (const h of (r.git_hosts ?? [])) {
93
+ const list = byHost.get(h.host) ?? [];
94
+ list.push({
95
+ host: h.host,
96
+ connectorId: r.id,
97
+ username: h.username,
98
+ bot,
99
+ auth,
100
+ oauthCode: auth.type === "oauth_code",
101
+ createdAt: new Date(r.created_at),
102
+ });
103
+ byHost.set(h.host, list);
104
+ }
105
+ }
106
+ const picks = settings.gitGateway?.hostSources ?? {};
107
+ const out = [];
108
+ for (const [host, list] of [...byHost].sort(([a], [b]) => a.localeCompare(b))) {
109
+ const chosen = Object.hasOwn(picks, host)
110
+ ? list.find((c) => c.connectorId === picks[host])
111
+ : [...list].sort(byDefault)[0];
112
+ if (chosen === undefined)
113
+ continue;
114
+ // personal: this account's own credential; bot: the org's (status never decrypts)
115
+ const usable = await this.credentials.status(orgId, chosen.connectorId, chosen.auth, accountId);
116
+ if (usable !== "connected")
117
+ continue;
118
+ out.push({
119
+ host,
120
+ connectorId: chosen.connectorId,
121
+ username: chosen.username,
122
+ bot: chosen.bot,
123
+ });
124
+ }
125
+ return out;
126
+ }
127
+ };
128
+ GitSources = __decorate([
129
+ Injectable(),
130
+ __param(0, Inject(KernelDb)),
131
+ __param(1, Inject(Credentials)),
132
+ __param(2, Inject(KernelConfigService))
133
+ ], GitSources);
134
+ export { GitSources };
@@ -0,0 +1,69 @@
1
+ import type { Redis } from "ioredis";
2
+ import type { KernelConfig } from "../config/schema.js";
3
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
4
+ import { KernelDb } from "../infra/database/kernel-db.js";
5
+ import type { KernelLogger } from "../log.js";
6
+ /**
7
+ * Run tickets of the git gateway (sandbox git credentials spec §4, §5.1, §9): an opaque bearer
8
+ * value one exec gets in its environment, bound to one run and one person. Only the SHA-256 of
9
+ * the ticket is stored (Redis `git-ticket:<hash>` → the JSON binding, TTL
10
+ * `tools.git.ticketTtlSeconds`); the run's set `git-run-tickets:<runId>` lists the hashes for
11
+ * revocation. The raw ticket is never stored or logged: logs carry the first 8 hex characters of
12
+ * its hash. Every resolve re-checks the run, the membership and the switches live, so nothing is
13
+ * cached in process memory (any replica resolves what another issued).
14
+ */
15
+ /** What a ticket is issued for. */
16
+ export interface GitTicketRequest {
17
+ orgId: string;
18
+ /** The actor: the account the exec runs for (the run's trigger account). */
19
+ accountId: string;
20
+ runId: string;
21
+ execId: string;
22
+ /** The git hosts the ticket may reach (from the org's git sources), e.g. `github.com`. */
23
+ hosts: string[];
24
+ /** Optional repository allowlist (`owner/name`); absent = every repository on the hosts. */
25
+ repos?: string[];
26
+ }
27
+ /** A ticket's stored binding. */
28
+ export interface GitTicketBinding extends GitTicketRequest {
29
+ /** When the ticket was issued (epoch ms). */
30
+ issuedAt: number;
31
+ }
32
+ /** The Redis key of a ticket's binding. */
33
+ export declare const gitTicketKey: (hash: string) => string;
34
+ /** The Redis set of a run's ticket hashes. */
35
+ export declare const gitRunTicketsKey: (runId: string) => string;
36
+ /** The SHA-256 (hex) of a raw ticket: the only form the kernel stores. */
37
+ export declare const hashGitTicket: (raw: string) => string;
38
+ /**
39
+ * The origin sandboxes reach the git gateway at: `tools.git.gatewayUrl`, else `http.publicUrl`;
40
+ * null when neither is set (the gateway cannot be offered).
41
+ */
42
+ export declare function gitGatewayOrigin(config: Pick<KernelConfig, "tools" | "http">): string | null;
43
+ export declare class GitTickets {
44
+ private readonly redis;
45
+ private readonly db;
46
+ /** Read per call: `tools.git.enabled` is hot. */
47
+ private readonly config;
48
+ private readonly log;
49
+ constructor(redis: Redis, db: KernelDb,
50
+ /** Read per call: `tools.git.enabled` is hot. */
51
+ config: Pick<KernelConfigService, "get">, log: Pick<KernelLogger, "debug">);
52
+ /**
53
+ * Issues a ticket for one exec and returns the raw value (it goes only into that exec's
54
+ * environment). Stores the binding under the ticket's hash and adds the hash to the run's set,
55
+ * the ticket with the ticket TTL, the set with the longest TTL any of its tickets got.
56
+ */
57
+ issue(req: GitTicketRequest): Promise<string>;
58
+ /**
59
+ * The binding of a raw ticket, or null: unknown or expired, or no longer usable because the run
60
+ * is not active (or belongs to another org), the account is not an active member of the org, or
61
+ * the gateway is off (`tools.git.enabled`, the org's `gitGateway.enabled`). Every check is live.
62
+ */
63
+ resolve(raw: string): Promise<GitTicketBinding | null>;
64
+ /**
65
+ * Deletes every ticket of the run (run end, abort, offboarding) in one Lua script, so a ticket
66
+ * added concurrently is either deleted with the rest or left in a set that still lists it.
67
+ */
68
+ revokeRun(runId: string): Promise<void>;
69
+ }
@@ -0,0 +1,194 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
8
+ return function (target, key) { decorator(target, key, paramIndex); }
9
+ };
10
+ import { createHash, randomBytes } from "node:crypto";
11
+ import { Inject, Injectable } from "@nestjs/common";
12
+ import { z } from "zod";
13
+ import { KernelConfigService } from "../infra/config/kernel-config.service.js";
14
+ import { KernelDb } from "../infra/database/kernel-db.js";
15
+ import { KERNEL_LOGGER, REDIS } from "../infra/tokens.js";
16
+ import { readOrgSettings } from "../orgs/settings.service.js";
17
+ const BindingSchema = z.object({
18
+ orgId: z.string().min(1),
19
+ accountId: z.string().min(1),
20
+ runId: z.string().min(1),
21
+ execId: z.string().min(1),
22
+ hosts: z.array(z.string()),
23
+ repos: z.array(z.string()).optional(),
24
+ issuedAt: z.number(),
25
+ });
26
+ /** Run statuses during which the run's tickets stay valid. */
27
+ const ACTIVE_RUN_STATUSES = ["queued", "running", "waiting"];
28
+ /** A raw ticket: 32 random bytes in base64url (43 characters, no padding). */
29
+ const TICKET_SHAPE = /^[A-Za-z0-9_-]{43}$/;
30
+ /** The Redis key of a ticket's binding. */
31
+ export const gitTicketKey = (hash) => `git-ticket:${hash}`;
32
+ /** The Redis set of a run's ticket hashes. */
33
+ export const gitRunTicketsKey = (runId) => `git-run-tickets:${runId}`;
34
+ /** The SHA-256 (hex) of a raw ticket: the only form the kernel stores. */
35
+ export const hashGitTicket = (raw) => createHash("sha256").update(raw, "utf8").digest("hex");
36
+ /**
37
+ * The origin sandboxes reach the git gateway at: `tools.git.gatewayUrl`, else `http.publicUrl`;
38
+ * null when neither is set (the gateway cannot be offered).
39
+ */
40
+ export function gitGatewayOrigin(config) {
41
+ const url = config.tools.git.gatewayUrl ?? config.http.publicUrl;
42
+ if (url === undefined || !URL.canParse(url))
43
+ return null;
44
+ return new URL(url).origin;
45
+ }
46
+ let GitTickets = class GitTickets {
47
+ redis;
48
+ db;
49
+ config;
50
+ log;
51
+ constructor(redis, db,
52
+ /** Read per call: `tools.git.enabled` is hot. */
53
+ config, log) {
54
+ this.redis = redis;
55
+ this.db = db;
56
+ this.config = config;
57
+ this.log = log;
58
+ }
59
+ /**
60
+ * Issues a ticket for one exec and returns the raw value (it goes only into that exec's
61
+ * environment). Stores the binding under the ticket's hash and adds the hash to the run's set,
62
+ * the ticket with the ticket TTL, the set with the longest TTL any of its tickets got.
63
+ */
64
+ async issue(req) {
65
+ const raw = randomBytes(32).toString("base64url");
66
+ const hash = hashGitTicket(raw);
67
+ const ttl = this.config.get().tools.git.ticketTtlSeconds;
68
+ const binding = {
69
+ orgId: req.orgId,
70
+ accountId: req.accountId,
71
+ runId: req.runId,
72
+ execId: req.execId,
73
+ hosts: [...req.hosts],
74
+ ...(req.repos !== undefined && { repos: [...req.repos] }),
75
+ issuedAt: Date.now(),
76
+ };
77
+ try {
78
+ await this.redis.eval(ISSUE, 2, gitTicketKey(hash), gitRunTicketsKey(req.runId), JSON.stringify(binding), String(ttl), hash);
79
+ }
80
+ catch {
81
+ // a ticket whose binding or revocation entry was not written must not be handed out
82
+ // (the script lists the ticket first, so a failure leaves no binding behind)
83
+ throw new Error("git ticket: storing the binding in Redis failed");
84
+ }
85
+ this.log.debug("git ticket issued", {
86
+ ticket: hash.slice(0, 8),
87
+ runId: req.runId,
88
+ execId: req.execId,
89
+ });
90
+ return raw;
91
+ }
92
+ /**
93
+ * The binding of a raw ticket, or null: unknown or expired, or no longer usable because the run
94
+ * is not active (or belongs to another org), the account is not an active member of the org, or
95
+ * the gateway is off (`tools.git.enabled`, the org's `gitGateway.enabled`). Every check is live.
96
+ */
97
+ async resolve(raw) {
98
+ if (!TICKET_SHAPE.test(raw))
99
+ return null;
100
+ const hash = hashGitTicket(raw);
101
+ const refuse = (reason) => {
102
+ this.log.debug("git ticket refused", { ticket: hash.slice(0, 8), reason });
103
+ return null;
104
+ };
105
+ if (!this.config.get().tools.git.enabled)
106
+ return refuse("gateway disabled");
107
+ const stored = await this.redis.get(gitTicketKey(hash));
108
+ if (stored === null)
109
+ return refuse("unknown or expired");
110
+ const binding = parseBinding(stored);
111
+ if (binding === null)
112
+ return refuse("invalid binding");
113
+ const run = await this.db
114
+ .selectFrom("runs")
115
+ .innerJoin("conversations", "conversations.id", "runs.conversation_id")
116
+ .innerJoin("orgs", "orgs.id", "conversations.org_id")
117
+ .select(["runs.status", "conversations.org_id"])
118
+ .where("runs.id", "=", binding.runId)
119
+ .where("orgs.deleted_at", "is", null)
120
+ .executeTakeFirst();
121
+ if (run === undefined || run.org_id !== binding.orgId)
122
+ return refuse("run not in org");
123
+ if (!ACTIVE_RUN_STATUSES.includes(run.status)) {
124
+ return refuse("run not active");
125
+ }
126
+ const member = await this.db
127
+ .selectFrom("memberships")
128
+ .innerJoin("accounts", "accounts.id", "memberships.account_id")
129
+ .select("memberships.account_id")
130
+ .where("memberships.org_id", "=", binding.orgId)
131
+ .where("memberships.account_id", "=", binding.accountId)
132
+ .where("accounts.status", "=", "active")
133
+ .executeTakeFirst();
134
+ if (member === undefined)
135
+ return refuse("not an active member");
136
+ const settings = await readOrgSettings(this.db.db, binding.orgId);
137
+ if (settings.gitGateway?.enabled === false)
138
+ return refuse("gateway off for the org");
139
+ return binding;
140
+ }
141
+ /**
142
+ * Deletes every ticket of the run (run end, abort, offboarding) in one Lua script, so a ticket
143
+ * added concurrently is either deleted with the rest or left in a set that still lists it.
144
+ */
145
+ async revokeRun(runId) {
146
+ const count = await this.redis.eval(REVOKE_RUN, 1, gitRunTicketsKey(runId), TICKET_KEY_PREFIX);
147
+ if (typeof count === "number" && count > 0) {
148
+ this.log.debug("git tickets revoked", { runId, count });
149
+ }
150
+ }
151
+ };
152
+ GitTickets = __decorate([
153
+ Injectable(),
154
+ __param(0, Inject(REDIS)),
155
+ __param(1, Inject(KernelDb)),
156
+ __param(2, Inject(KernelConfigService)),
157
+ __param(3, Inject(KERNEL_LOGGER))
158
+ ], GitTickets);
159
+ export { GitTickets };
160
+ /**
161
+ * Both scripts below build ticket keys from {@link TICKET_KEY_PREFIX} instead of declaring them in
162
+ * KEYS, so they assume a non-Cluster Redis / Valkey (one node, or Sentinel).
163
+ */
164
+ const TICKET_KEY_PREFIX = "git-ticket:";
165
+ /**
166
+ * Lists the ticket in the run's set, stores its binding with the TTL and gives the set a TTL that
167
+ * only grows (KEYS: ticket, run set; ARGV: binding, ttl, hash): lowering `ticketTtlSeconds` must
168
+ * not cut the set below a live ticket.
169
+ */
170
+ const ISSUE = `
171
+ redis.call('SADD', KEYS[2], ARGV[3])
172
+ redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])
173
+ if redis.call('TTL', KEYS[2]) < tonumber(ARGV[2]) then redis.call('EXPIRE', KEYS[2], ARGV[2]) end
174
+ return 1
175
+ `;
176
+ /** Reads the run's set, deletes every listed ticket and the set (KEYS[1]; ARGV[1] the ticket key prefix). */
177
+ const REVOKE_RUN = `
178
+ local hashes = redis.call('SMEMBERS', KEYS[1])
179
+ for _, h in ipairs(hashes) do redis.call('DEL', ARGV[1] .. h) end
180
+ redis.call('DEL', KEYS[1])
181
+ return #hashes
182
+ `;
183
+ function parseBinding(stored) {
184
+ try {
185
+ const r = BindingSchema.safeParse(JSON.parse(stored));
186
+ if (!r.success)
187
+ return null;
188
+ const { repos, ...rest } = r.data;
189
+ return { ...rest, ...(repos !== undefined && { repos }) };
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ }
@@ -49,6 +49,7 @@ import { ExtensionsModule } from "../extensions/extensions.module.js";
49
49
  import { ExtensionPointDefaultsController } from "../extensions/point-defaults.controller.js";
50
50
  import { FilesModule } from "../files/files.module.js";
51
51
  import { FilePreviewersController } from "../files/previewers.controller.js";
52
+ import { GitModule } from "../git/git.module.js";
52
53
  import { AuthController } from "../identity/auth.controller.js";
53
54
  import { AuthGuard } from "../identity/auth.guard.js";
54
55
  import { IdentityModule } from "../identity/identity.module.js";
@@ -211,6 +212,8 @@ ApiModule = __decorate([
211
212
  SetupModule,
212
213
  // scheduled tasks (the controller comes with the HTTP routes)
213
214
  SchedulesModule,
215
+ // the git gateway (run tickets; the gateway route comes with it)
216
+ GitModule,
214
217
  ],
215
218
  controllers: CONTROLLERS,
216
219
  providers: [{ provide: APP_GUARD, useClass: AuthGuard }],
@@ -3,6 +3,7 @@ import type { Logger } from "@capybara-hq/plugin-sdk";
3
3
  import type { NestFastifyApplication } from "@nestjs/platform-fastify";
4
4
  import type { InjectOptions, LightMyRequestResponse } from "fastify";
5
5
  import type { Route } from "../ctx/create-ctx.js";
6
+ import type { GitGateway } from "../git/gateway.js";
6
7
  import { type GatewayRoutes } from "./gateway-routes.js";
7
8
  import { type PluginRouteTable } from "./plugin-dispatch.js";
8
9
  /** 内核 HTTP 的只读视图:进程内注入请求与列出路由(测试与元测试用)。 */
@@ -41,6 +42,11 @@ export interface CreateHttpAppOptions {
41
42
  * when `roles` includes `gateway` (`serveGateway`).
42
43
  */
43
44
  gateway?: GatewayRoutes;
45
+ /**
46
+ * The git gateway (`/git/v1/*`, a raw route outside Nest), registered only when given: the
47
+ * kernel passes it when `roles` includes `api`.
48
+ */
49
+ git?: Pick<GitGateway, "handle">;
44
50
  /** 副本标识(`http.replicaId`):设置时每个响应带 `x-capybara-replica` 头。 */
45
51
  replicaId?: string | undefined;
46
52
  /**
package/dist/http/app.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import "reflect-metadata";
2
2
  import fastifyCookie from "@fastify/cookie";
3
3
  import { ApiError, errorBody } from "../errors.js";
4
+ import { registerGitGatewayRoute } from "../git/gateway.route.js";
4
5
  import { AccountsService } from "../identity/accounts.js";
5
6
  import { ApiTokenService } from "../identity/api-tokens.js";
6
7
  import { authenticateOrgUploader, authorizeSuperAdmin, } from "../identity/auth.guard.js";
@@ -171,6 +172,8 @@ export async function createHttpApp(opts, app) {
171
172
  : undefined;
172
173
  if (opts.gateway)
173
174
  await registerGatewayRoutes(fastify, opts.gateway, opts.log);
175
+ if (opts.git)
176
+ await registerGitGatewayRoute(fastify, opts.git, opts.log);
174
177
  return {
175
178
  inject: (o) => fastify.inject(o),
176
179
  routes: () => [...routes],
@@ -28,6 +28,8 @@ const HOT_APPLIERS = {
28
28
  // Login methods and trusted identity sources are read on every request.
29
29
  "auth.loginSources": () => { },
30
30
  "auth.trustedIdentitySources": () => { },
31
+ // The git gateway reads tools.git.enabled through the config service on every request.
32
+ "tools.git.enabled": () => { },
31
33
  // The hook registry reads the time limit on every run.
32
34
  "hooks.timeoutMs": () => { },
33
35
  // The provider registry reads the list time limit at every listModels call.