@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.
- package/dist/app.js +2 -0
- package/dist/catalog/catalog-content.d.ts +18 -1
- package/dist/catalog/catalog-content.js +5 -2
- package/dist/catalog/catalog-scope.service.js +3 -2
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.service.js +13 -3
- package/dist/catalog/plugin-install.d.ts +37 -16
- package/dist/catalog/plugin-install.js +108 -34
- package/dist/catalog/plugin-upgrade.js +1 -0
- package/dist/channels/actions.d.ts +4 -1
- package/dist/channels/actions.js +5 -2
- package/dist/channels/cards.d.ts +7 -1
- package/dist/channels/cards.js +28 -1
- package/dist/channels/phrases.d.ts +15 -0
- package/dist/channels/phrases.js +9 -0
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.js +69 -0
- package/dist/conversations/events.d.ts +14 -0
- package/dist/conversations/events.js +20 -0
- package/dist/db/kernel-schema.d.ts +19 -4
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +5 -0
- package/dist/git/gateway.d.ts +93 -0
- package/dist/git/gateway.js +750 -0
- package/dist/git/gateway.route.d.ts +16 -0
- package/dist/git/gateway.route.js +41 -0
- package/dist/git/git.module.d.ts +10 -0
- package/dist/git/git.module.js +31 -0
- package/dist/git/identity.d.ts +80 -0
- package/dist/git/identity.js +209 -0
- package/dist/git/pkt-line.d.ts +53 -0
- package/dist/git/pkt-line.js +146 -0
- package/dist/git/run-end.d.ts +29 -0
- package/dist/git/run-end.js +121 -0
- package/dist/git/sources.d.ts +42 -0
- package/dist/git/sources.js +134 -0
- package/dist/git/tickets.d.ts +69 -0
- package/dist/git/tickets.js +194 -0
- package/dist/http/api.module.js +3 -0
- package/dist/http/app.d.ts +6 -0
- package/dist/http/app.js +3 -0
- package/dist/infra/config/kernel-config.service.js +2 -0
- package/dist/orgs/settings.service.d.ts +21 -0
- package/dist/orgs/settings.service.js +18 -0
- package/dist/overrides.d.ts +6 -0
- package/dist/overrides.js +4 -0
- package/dist/platform/exclusions.js +3 -0
- package/dist/runtime/run-finish.d.ts +5 -0
- package/dist/runtime/run-finish.js +12 -3
- package/dist/runtime/runtime.module.js +2 -0
- package/dist/runtime/waits.js +7 -5
- package/dist/schedules/fire.d.ts +3 -3
- package/dist/schedules/fire.js +7 -4
- package/dist/schedules/grant-rules.d.ts +40 -0
- package/dist/schedules/grant-rules.js +83 -0
- package/dist/schedules/scheduled-tasks.service.d.ts +9 -7
- package/dist/schedules/scheduled-tasks.service.js +50 -15
- package/dist/schedules/types.d.ts +2 -0
- package/dist/tools/approvals.service.d.ts +100 -19
- package/dist/tools/approvals.service.js +288 -21
- package/dist/tools/builtin-names.d.ts +4 -2
- package/dist/tools/builtin-names.js +6 -1
- package/dist/tools/builtins/schedules.d.ts +11 -9
- package/dist/tools/builtins/schedules.js +202 -44
- package/dist/tools/connector-account-policies.d.ts +8 -0
- package/dist/tools/connector-account-policies.js +8 -0
- package/dist/tools/connectors.controller.d.ts +2 -0
- package/dist/tools/connectors.controller.js +5 -1
- package/dist/tools/connectors.service.d.ts +46 -3
- package/dist/tools/connectors.service.js +98 -9
- package/dist/tools/credentials.d.ts +18 -0
- package/dist/tools/credentials.js +60 -0
- package/dist/tools/credentials.module.d.ts +7 -0
- package/dist/tools/credentials.module.js +22 -0
- package/dist/tools/git-asks.d.ts +113 -0
- package/dist/tools/git-asks.js +174 -0
- package/dist/tools/git-push-policy.d.ts +89 -0
- package/dist/tools/git-push-policy.js +175 -0
- package/dist/tools/info.d.ts +3 -3
- package/dist/tools/info.js +3 -3
- package/dist/tools/network-asks.d.ts +5 -0
- package/dist/tools/network-asks.js +9 -2
- package/dist/tools/tools.module.js +4 -3
- package/package.json +3 -3
- package/src/migrations/0126_git_gateway.sql +10 -0
- package/src/migrations/0127_git_push_policy.sql +12 -0
- package/src/migrations/0128_scheduled_task_plugin_disabled.sql +10 -0
- 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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
834
|
-
|
|
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
|
-
:
|
|
842
|
-
? (
|
|
843
|
-
: network
|
|
844
|
-
? "
|
|
845
|
-
: "
|
|
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
|
|
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", "
|
|
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", "
|
|
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
|
|
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
|
|
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
|
|
13
|
-
* through the same service as the HTTP routes (`ScheduledTasksService`), so the
|
|
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; `
|
|
25
|
-
*
|
|
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.
|
|
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
|
|
107
|
+
/** The five schedule builtins. */
|
|
106
108
|
export declare function scheduleTools(deps: ScheduleToolsDeps): BuiltinTool[];
|
|
107
109
|
export {};
|