@capybara-hq/kernel 0.1.0-rc.20 → 0.1.0-rc.22
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/bootstrap.json +10 -1
- package/dist/agents/admin-agents.d.ts +16 -3
- package/dist/agents/admin-agents.js +65 -7
- package/dist/agents/agents.schemas.d.ts +24 -6
- package/dist/agents/agents.service.d.ts +4 -3
- package/dist/agents/agents.service.js +39 -26
- package/dist/agents/definition.d.ts +44 -6
- package/dist/agents/definition.js +86 -18
- package/dist/agents/from-template.d.ts +6 -3
- package/dist/agents/from-template.js +57 -44
- package/dist/agents/org-templates.js +97 -41
- package/dist/agents/templates.d.ts +6 -0
- package/dist/agents/templates.js +15 -1
- package/dist/catalog/admin-catalog.controller.d.ts +2 -4
- package/dist/catalog/admin-catalog.controller.js +2 -6
- package/dist/catalog/catalog.module.js +3 -0
- package/dist/catalog/claude/report.d.ts +9 -2
- package/dist/catalog/claude/report.js +1 -0
- package/dist/catalog/claude/translate.js +1 -1
- package/dist/catalog/component-promotion.js +1 -2
- package/dist/catalog/component-state.d.ts +13 -1
- package/dist/catalog/component-state.js +14 -5
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/marketplaces.service.js +2 -1
- package/dist/catalog/org-catalog.controller.d.ts +13 -3
- package/dist/catalog/org-catalog.controller.js +26 -4
- package/dist/catalog/org-catalog.schemas.d.ts +13 -0
- package/dist/catalog/org-catalog.schemas.js +34 -0
- package/dist/catalog/org-plugin-uploads.controller.js +0 -1
- package/dist/catalog/org-plugin-uploads.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.js +0 -3
- package/dist/catalog/org-plugin-uploads.schemas.d.ts +0 -1
- package/dist/catalog/org-plugin-uploads.schemas.js +0 -2
- package/dist/catalog/platform-components.d.ts +0 -1
- package/dist/catalog/platform-components.js +3 -8
- package/dist/catalog/plugin-import.d.ts +0 -8
- package/dist/catalog/plugin-import.js +4 -35
- package/dist/catalog/plugin-install.d.ts +2 -2
- package/dist/catalog/plugin-install.js +2 -2
- package/dist/catalog/plugin-package.d.ts +7 -0
- package/dist/catalog/plugin-package.js +27 -1
- package/dist/catalog/plugin-upgrade.d.ts +5 -6
- package/dist/catalog/plugin-upgrade.js +5 -23
- package/dist/catalog/requests.service.d.ts +66 -10
- package/dist/catalog/requests.service.js +196 -15
- package/dist/channels/actions.d.ts +2 -0
- package/dist/channels/actions.js +19 -4
- package/dist/channels/cards.d.ts +16 -2
- package/dist/channels/cards.js +40 -7
- package/dist/channels/channels.module.js +3 -0
- package/dist/channels/outbound.d.ts +22 -1
- package/dist/channels/outbound.js +116 -7
- package/dist/channels/phrases.d.ts +25 -0
- package/dist/channels/phrases.js +19 -0
- package/dist/channels/threads.d.ts +12 -0
- package/dist/channels/threads.js +34 -0
- package/dist/config/schema.d.ts +23 -1
- package/dist/config/schema.js +105 -5
- package/dist/conversations/conversations.service.d.ts +42 -9
- package/dist/conversations/conversations.service.js +57 -25
- package/dist/conversations/events.d.ts +106 -11
- package/dist/conversations/events.js +121 -18
- package/dist/conversations/message-context.d.ts +6 -3
- package/dist/conversations/message-context.js +14 -10
- package/dist/conversations/queue.d.ts +15 -2
- package/dist/conversations/queue.js +22 -3
- package/dist/conversations/queue.service.d.ts +12 -6
- package/dist/conversations/queue.service.js +36 -13
- package/dist/db/kernel-schema.d.ts +105 -3
- package/dist/devices/conversation-devices.d.ts +8 -1
- package/dist/devices/conversation-devices.js +8 -0
- package/dist/devices/devices.controller.d.ts +21 -4
- package/dist/devices/devices.controller.js +46 -8
- package/dist/devices/devices.schemas.d.ts +9 -1
- package/dist/devices/devices.schemas.js +11 -3
- package/dist/devices/devices.service.d.ts +176 -4
- package/dist/devices/devices.service.js +341 -6
- package/dist/devices/dto.d.ts +56 -3
- package/dist/devices/dto.js +51 -1
- package/dist/devices/public-suffix.d.ts +6 -0
- package/dist/devices/public-suffix.js +79 -0
- package/dist/errors.d.ts +46 -5
- package/dist/errors.js +46 -5
- package/dist/evals/reconstruct.d.ts +18 -13
- package/dist/evals/reconstruct.js +50 -36
- package/dist/evals/recorded-tools.d.ts +7 -7
- package/dist/evals/recorded-tools.js +4 -3
- package/dist/evals/trajectory.d.ts +6 -1
- package/dist/evals/trajectory.js +7 -0
- package/dist/extensions/bootstrap.d.ts +49 -10
- package/dist/extensions/bootstrap.js +104 -27
- package/dist/extensions/package.js +2 -2
- package/dist/http/api.module.js +5 -0
- package/dist/inbox/inbox.module.js +2 -1
- package/dist/inbox/inbox.service.d.ts +19 -4
- package/dist/inbox/inbox.service.js +14 -3
- package/dist/infra/config/kernel-config.service.js +11 -0
- package/dist/infra/events/kernel-events.d.ts +2 -1
- package/dist/lifecycle/kernel-lifecycle.d.ts +5 -1
- package/dist/lifecycle/kernel-lifecycle.js +16 -4
- package/dist/local-devices/device-policy.service.d.ts +65 -0
- package/dist/local-devices/device-policy.service.js +217 -0
- package/dist/local-devices/local-device-gateway.js +4 -1
- package/dist/local-devices/local-device-provider.d.ts +40 -3
- package/dist/local-devices/local-device-provider.js +109 -7
- package/dist/local-devices/local-devices.module.d.ts +2 -1
- package/dist/local-devices/local-devices.module.js +19 -3
- package/dist/local-devices/machines.d.ts +14 -3
- package/dist/local-devices/machines.js +41 -3
- package/dist/local-devices/network-grants.d.ts +104 -0
- package/dist/local-devices/network-grants.js +288 -0
- package/dist/local-devices/policy-summary.d.ts +20 -0
- package/dist/local-devices/policy-summary.js +63 -0
- package/dist/orgs/orgs.module.js +4 -10
- package/dist/orgs/roles.d.ts +1 -1
- package/dist/orgs/roles.js +4 -0
- package/dist/orgs/settings.controller.js +2 -1
- package/dist/orgs/settings.service.d.ts +164 -12
- package/dist/orgs/settings.service.js +212 -24
- package/dist/platform/exclusions.js +11 -1
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/admin-items.ops.js +1 -1
- package/dist/platform/ops/agent-skills.ops.js +2 -2
- package/dist/platform/ops/agent-templates.ops.js +2 -2
- package/dist/platform/ops/agents.ops.js +11 -15
- package/dist/platform/ops/devices.ops.js +3 -1
- package/dist/platform/ops/inbox.ops.js +19 -0
- package/dist/platform/ops/org-settings.ops.js +14 -5
- package/dist/platform/ops/platform.ops.d.ts +9 -1
- package/dist/platform/ops/platform.ops.js +34 -12
- package/dist/platform/ops/requests.ops.js +3 -2
- package/dist/platform/ops/schedules.ops.d.ts +21 -0
- package/dist/platform/ops/schedules.ops.js +470 -0
- package/dist/platform/ops/skills.ops.js +3 -3
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +3 -0
- package/dist/roles/worker.module.js +3 -0
- package/dist/runtime/context/build.js +13 -4
- package/dist/runtime/context/compaction.js +6 -3
- package/dist/runtime/context/device-paths.d.ts +43 -21
- package/dist/runtime/context/device-paths.js +104 -21
- package/dist/runtime/context/run-context.d.ts +9 -0
- package/dist/runtime/context/run-context.js +8 -0
- package/dist/runtime/context/skills.d.ts +14 -1
- package/dist/runtime/context/skills.js +36 -5
- package/dist/runtime/maintenance.d.ts +4 -2
- package/dist/runtime/maintenance.js +16 -5
- package/dist/runtime/run-finish.d.ts +6 -1
- package/dist/runtime/run-finish.js +16 -2
- package/dist/runtime/run-queue.js +21 -2
- package/dist/runtime/run-worker.js +14 -33
- package/dist/runtime/runtime.module.js +3 -0
- package/dist/runtime/subagents/definition.d.ts +32 -14
- package/dist/runtime/subagents/definition.js +106 -109
- package/dist/runtime/subagents/dispatch.js +4 -3
- package/dist/runtime/subagents/preamble.d.ts +2 -1
- package/dist/runtime/subagents/preamble.js +2 -1
- package/dist/runtime/waits.js +14 -4
- package/dist/sandbox/sandbox.service.d.ts +120 -10
- package/dist/sandbox/sandbox.service.js +335 -28
- package/dist/sandbox/tiers.d.ts +86 -0
- package/dist/sandbox/tiers.js +96 -0
- package/dist/schedules/effective-settings.d.ts +16 -0
- package/dist/schedules/effective-settings.js +17 -0
- package/dist/schedules/errors.d.ts +8 -0
- package/dist/schedules/errors.js +8 -0
- package/dist/schedules/fire.d.ts +118 -0
- package/dist/schedules/fire.js +625 -0
- package/dist/schedules/grant-rules.d.ts +94 -0
- package/dist/schedules/grant-rules.js +126 -0
- package/dist/schedules/job-ids.d.ts +32 -0
- package/dist/schedules/job-ids.js +27 -0
- package/dist/schedules/pause.d.ts +9 -0
- package/dist/schedules/pause.js +33 -0
- package/dist/schedules/run-tracker.d.ts +81 -0
- package/dist/schedules/run-tracker.js +333 -0
- package/dist/schedules/scan.job.d.ts +64 -0
- package/dist/schedules/scan.job.js +303 -0
- package/dist/schedules/schedule-math.d.ts +31 -0
- package/dist/schedules/schedule-math.js +223 -0
- package/dist/schedules/scheduled-tasks.service.d.ts +351 -0
- package/dist/schedules/scheduled-tasks.service.js +1193 -0
- package/dist/schedules/schedules.controller.d.ts +68 -0
- package/dist/schedules/schedules.controller.js +242 -0
- package/dist/schedules/schedules.module.d.ts +16 -0
- package/dist/schedules/schedules.module.js +65 -0
- package/dist/schedules/schedules.schemas.d.ts +316 -0
- package/dist/schedules/schedules.schemas.js +186 -0
- package/dist/schedules/store.d.ts +81 -0
- package/dist/schedules/store.js +390 -0
- package/dist/schedules/store.module.d.ts +6 -0
- package/dist/schedules/store.module.js +21 -0
- package/dist/schedules/types.d.ts +108 -0
- package/dist/schedules/types.js +1 -0
- package/dist/schedules/unattended.d.ts +117 -0
- package/dist/schedules/unattended.js +228 -0
- package/dist/setup/oauth-token-probe.js +8 -4
- package/dist/skills/agent-skills.service.d.ts +4 -8
- package/dist/skills/agent-skills.service.js +18 -53
- package/dist/skills/catalog.d.ts +65 -62
- package/dist/skills/catalog.js +109 -206
- package/dist/skills/conversation-catalog.d.ts +3 -3
- package/dist/skills/conversation-catalog.js +5 -12
- package/dist/skills/invoke.d.ts +2 -2
- package/dist/skills/skills.controller.d.ts +3 -1
- package/dist/skills/skills.service.d.ts +38 -37
- package/dist/skills/skills.service.js +168 -107
- package/dist/skills/usable.d.ts +57 -8
- package/dist/skills/usable.js +172 -38
- package/dist/tools/approvals.controller.d.ts +4 -2
- package/dist/tools/approvals.controller.js +17 -2
- package/dist/tools/approvals.schemas.d.ts +1 -0
- package/dist/tools/approvals.schemas.js +3 -1
- package/dist/tools/approvals.service.d.ts +182 -7
- package/dist/tools/approvals.service.js +597 -13
- package/dist/tools/builtin-names.d.ts +25 -4
- package/dist/tools/builtin-names.js +45 -4
- package/dist/tools/builtins/builtin-tools.d.ts +8 -0
- package/dist/tools/builtins/builtin-tools.js +23 -2
- package/dist/tools/builtins/sandbox/bash.js +16 -7
- package/dist/tools/builtins/sandbox/common.d.ts +1 -1
- package/dist/tools/builtins/sandbox/common.js +33 -10
- package/dist/tools/builtins/sandbox/edit.js +7 -3
- package/dist/tools/builtins/sandbox/file-state.d.ts +31 -1
- package/dist/tools/builtins/sandbox/file-state.js +33 -3
- package/dist/tools/builtins/sandbox/write.js +8 -4
- package/dist/tools/builtins/schedules.d.ts +107 -0
- package/dist/tools/builtins/schedules.js +789 -0
- package/dist/tools/elicitation.d.ts +2 -0
- package/dist/tools/elicitation.js +1 -1
- package/dist/tools/fixed-ask.d.ts +7 -0
- package/dist/tools/fixed-ask.js +9 -0
- package/dist/tools/network-asks.d.ts +109 -0
- package/dist/tools/network-asks.js +221 -0
- package/dist/tools/pipeline.d.ts +53 -6
- package/dist/tools/pipeline.js +202 -13
- package/dist/tools/policy.d.ts +21 -8
- package/dist/tools/policy.js +63 -9
- package/dist/tools/registry.d.ts +10 -1
- package/dist/tools/tools.module.js +12 -0
- package/package.json +2 -2
- package/src/migrations/0113_scheduled_tasks.sql +108 -0
- package/src/migrations/0114_network_asks.sql +23 -0
- package/src/migrations/0115_network_requests.sql +28 -0
- package/src/migrations/0116_skills_default_on.sql +37 -0
- package/src/migrations/0117_sandbox_tiers.sql +11 -0
|
@@ -8,10 +8,11 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { connectorToolKey, parseToolKey } from "@capybara-hq/plugin-sdk";
|
|
11
|
+
import { connectorToolKey, parseToolKey, validateAllowHost, } from "@capybara-hq/plugin-sdk";
|
|
12
12
|
import { Inject, Injectable } from "@nestjs/common";
|
|
13
13
|
import { sql } from "kysely";
|
|
14
14
|
import { EventLog } from "../conversations/event-log.js";
|
|
15
|
+
import { NETWORK_ASK_MAX_HOSTS, NETWORK_HOST_MAX_CHARS, } from "../conversations/events.js";
|
|
15
16
|
import { ApiError } from "../errors.js";
|
|
16
17
|
import { audit } from "../identity/audit.js";
|
|
17
18
|
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
@@ -19,12 +20,38 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
19
20
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
20
21
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
21
22
|
import { KernelEvents } from "../infra/events/kernel-events.js";
|
|
23
|
+
import { LockService } from "../infra/lock/lock.service.js";
|
|
24
|
+
import { WithLock } from "../infra/lock/with-lock.decorator.js";
|
|
25
|
+
import { DevicePolicyService } from "../local-devices/device-policy.service.js";
|
|
22
26
|
import { ORG_ROLES, roleAtLeast } from "../orgs/roles.js";
|
|
23
27
|
import { rootConversationOf } from "../runtime/subagents/child.js";
|
|
28
|
+
import { SandboxService } from "../sandbox/sandbox.service.js";
|
|
29
|
+
import { readScheduleSettings } from "../schedules/effective-settings.js";
|
|
30
|
+
import { grantRefusalOf, NETWORK_GRANT_PREFIX, NETWORK_GRANT_SPEC_HASH, networkGrantRefusal, } from "../schedules/grant-rules.js";
|
|
31
|
+
import { ScheduledTaskStore } from "../schedules/store.js";
|
|
32
|
+
import { unattendedTaskRunOf } from "../schedules/unattended.js";
|
|
24
33
|
import { pluginDisplayNames } from "./display-names.js";
|
|
34
|
+
import { normalizeStoredDefinitionIn } from "./legacy-data.js";
|
|
35
|
+
import { ToolRegistry } from "./registry.js";
|
|
25
36
|
export { APPROVAL_DENY_REASON_MAX_CHARS } from "../conversations/events.js";
|
|
26
37
|
/** `list` 一次最多返回的审批数(收件箱据此判断这个来源是否取满)。 */
|
|
27
38
|
export const APPROVALS_LIST_MAX = 100;
|
|
39
|
+
/**
|
|
40
|
+
* The model-visible note written after a network ask's kept result when its run is stopped before
|
|
41
|
+
* the device owner answered (spec §6.7).
|
|
42
|
+
*/
|
|
43
|
+
export const NETWORK_ASK_CANCELLED_NOTE = "[network] The run was stopped before the user answered; access stays blocked.";
|
|
44
|
+
/** The record without a network ask's kept command result (what routes and cards may show). */
|
|
45
|
+
export function publicRecord(r) {
|
|
46
|
+
if (r.detail === null)
|
|
47
|
+
return { ...r, detail: null };
|
|
48
|
+
const { result: _result, ...detail } = r.detail;
|
|
49
|
+
return { ...r, detail };
|
|
50
|
+
}
|
|
51
|
+
/** The tool key a network ask is recorded under (the command ran in `Bash`). */
|
|
52
|
+
export const NETWORK_ASK_TOOL_KEY = "Bash";
|
|
53
|
+
/** The spec hash of a network ask (no tool definition is approved). */
|
|
54
|
+
const NETWORK_ASK_SPEC_HASH = "network";
|
|
28
55
|
const notFound = () => new ApiError("approval.not_found", "Approval not found");
|
|
29
56
|
const iso = (v) => new Date(v).toISOString();
|
|
30
57
|
const isRole = (v) => v !== null && ORG_ROLES.includes(v);
|
|
@@ -38,6 +65,8 @@ function toRecord(r) {
|
|
|
38
65
|
runId: r.run_id,
|
|
39
66
|
callId: r.call_id,
|
|
40
67
|
attempt: r.attempt,
|
|
68
|
+
kind: r.kind,
|
|
69
|
+
detail: r.kind === "network" ? (r.detail ?? null) : null,
|
|
41
70
|
toolKey: r.tool_key,
|
|
42
71
|
specHash: r.spec_hash,
|
|
43
72
|
connectorId: r.connector_id,
|
|
@@ -84,12 +113,30 @@ let ApprovalsService = class ApprovalsService {
|
|
|
84
113
|
eventLog;
|
|
85
114
|
events;
|
|
86
115
|
txHost;
|
|
87
|
-
|
|
116
|
+
schedules;
|
|
117
|
+
registry;
|
|
118
|
+
policies;
|
|
119
|
+
lockService;
|
|
120
|
+
sandbox;
|
|
121
|
+
constructor(db, config, eventLog, events, txHost, schedules,
|
|
122
|
+
/** A trial approval is kept only for a tool the task may hold as a grant (its agent's tools). */
|
|
123
|
+
registry,
|
|
124
|
+
/** 「这台设备一直允许」 sends the `policy` op to the device before the decision is recorded. */
|
|
125
|
+
policies,
|
|
126
|
+
/** One decision of a network ask at a time, across replicas (the op is sent before the transaction). */
|
|
127
|
+
lockService,
|
|
128
|
+
/** A trial run's 「这次允许」 becomes a task grant only within the org's egress. */
|
|
129
|
+
sandbox) {
|
|
88
130
|
this.db = db;
|
|
89
131
|
this.config = config;
|
|
90
132
|
this.eventLog = eventLog;
|
|
91
133
|
this.events = events;
|
|
92
134
|
this.txHost = txHost;
|
|
135
|
+
this.schedules = schedules;
|
|
136
|
+
this.registry = registry;
|
|
137
|
+
this.policies = policies;
|
|
138
|
+
this.lockService = lockService;
|
|
139
|
+
this.sandbox = sandbox;
|
|
93
140
|
}
|
|
94
141
|
/**
|
|
95
142
|
* 新建 pending 审批(`expires_at` = 现在 + `tools.approvalTimeoutHours`)并写 `approval_requested`,同一事务。
|
|
@@ -165,6 +212,97 @@ let ApprovalsService = class ApprovalsService {
|
|
|
165
212
|
]);
|
|
166
213
|
return { record: rec, toSeq: Math.max(...written.map((e) => e.seq)) };
|
|
167
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* A network ask (network ask-to-allow spec §6.2): a pending approval of kind `network` for the
|
|
217
|
+
* device owner, expiring like a tool approval (`tools.approvalTimeoutHours`), and its
|
|
218
|
+
* `approval_requested` (tool key `Bash`, the display-only `network.hosts`), in one transaction.
|
|
219
|
+
* It takes the attempt after the call's other approvals (P-D4: the command ran, so no tool
|
|
220
|
+
* approval of the call follows it). One per call: a second request returns the first.
|
|
221
|
+
*/
|
|
222
|
+
async requestNetwork(input) {
|
|
223
|
+
const hosts = input.detail.hosts;
|
|
224
|
+
if (hosts.length === 0 || hosts.length > NETWORK_ASK_MAX_HOSTS) {
|
|
225
|
+
throw new Error(`a network ask lists 1 to ${NETWORK_ASK_MAX_HOSTS} hosts`);
|
|
226
|
+
}
|
|
227
|
+
if (hosts.some((h) => h.host.length === 0 || h.host.length > NETWORK_HOST_MAX_CHARS)) {
|
|
228
|
+
throw new Error("a network ask's host is 1 to 255 characters");
|
|
229
|
+
}
|
|
230
|
+
if (input.scopes.length === 0 || !input.scopes.includes("once")) {
|
|
231
|
+
throw new Error('a network ask offers at least "once"');
|
|
232
|
+
}
|
|
233
|
+
const { record, toSeq } = await this.requestNetworkIn(input);
|
|
234
|
+
if (toSeq !== null)
|
|
235
|
+
await this.eventLog.notify(record.conversationId, toSeq);
|
|
236
|
+
return record;
|
|
237
|
+
}
|
|
238
|
+
async requestNetworkIn(input) {
|
|
239
|
+
const hours = this.config.get().tools.approvalTimeoutHours;
|
|
240
|
+
const t = this.db.db;
|
|
241
|
+
// the conversation row first (as `requestIn`): it also serializes two asks of one call
|
|
242
|
+
await t
|
|
243
|
+
.selectFrom("conversations")
|
|
244
|
+
.select("id")
|
|
245
|
+
.where("id", "=", input.conversationId)
|
|
246
|
+
.forUpdate()
|
|
247
|
+
.execute();
|
|
248
|
+
// only the device's owner is ever asked: never another account the caller names
|
|
249
|
+
const owner = await this.deviceOwnerOf(input.orgId, input.detail.deviceId);
|
|
250
|
+
if (owner === null || owner !== input.requestedFor) {
|
|
251
|
+
throw new Error(`a network ask is for the owner of a live device of the org (device ${input.detail.deviceId})`);
|
|
252
|
+
}
|
|
253
|
+
const existing = await this.networkForCall(input.runId, input.callId);
|
|
254
|
+
if (existing !== null)
|
|
255
|
+
return { record: existing, toSeq: null };
|
|
256
|
+
const last = await t
|
|
257
|
+
.selectFrom("approvals")
|
|
258
|
+
.select(sql `max(attempt)`.as("attempt"))
|
|
259
|
+
.where("run_id", "=", input.runId)
|
|
260
|
+
.where("call_id", "=", input.callId)
|
|
261
|
+
.executeTakeFirst();
|
|
262
|
+
const row = await t
|
|
263
|
+
.insertInto("approvals")
|
|
264
|
+
.values({
|
|
265
|
+
id: randomUUID(),
|
|
266
|
+
org_id: input.orgId,
|
|
267
|
+
conversation_id: input.conversationId,
|
|
268
|
+
run_id: input.runId,
|
|
269
|
+
call_id: input.callId,
|
|
270
|
+
attempt: Number(last?.attempt ?? 0) + 1,
|
|
271
|
+
kind: "network",
|
|
272
|
+
detail: JSON.stringify(input.detail),
|
|
273
|
+
tool_key: NETWORK_ASK_TOOL_KEY,
|
|
274
|
+
spec_hash: NETWORK_ASK_SPEC_HASH,
|
|
275
|
+
connector_id: null,
|
|
276
|
+
input: JSON.stringify({}),
|
|
277
|
+
requested_for: input.requestedFor,
|
|
278
|
+
approver_role: null,
|
|
279
|
+
scopes: [...new Set(input.scopes)],
|
|
280
|
+
scope: null,
|
|
281
|
+
decided_by: null,
|
|
282
|
+
expires_at: sql `now() + ${hours}::integer * interval '1 hour'`,
|
|
283
|
+
})
|
|
284
|
+
.returningAll()
|
|
285
|
+
.executeTakeFirstOrThrow();
|
|
286
|
+
const rec = toRecord(row);
|
|
287
|
+
const written = await this.eventLog.appendIn(rec.conversationId, [
|
|
288
|
+
{
|
|
289
|
+
type: "approval_requested",
|
|
290
|
+
runId: rec.runId,
|
|
291
|
+
idemKey: `approval_requested:${rec.id}`,
|
|
292
|
+
payload: {
|
|
293
|
+
step: input.step,
|
|
294
|
+
callId: rec.callId,
|
|
295
|
+
approvalId: rec.id,
|
|
296
|
+
toolKey: rec.toolKey,
|
|
297
|
+
expiresAt: rec.expiresAt,
|
|
298
|
+
network: { hosts: input.detail.hosts.map((h) => h.host) },
|
|
299
|
+
requestedFor: rec.requestedFor,
|
|
300
|
+
approverRole: null,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
return { record: rec, toSeq: Math.max(...written.map((e) => e.seq)) };
|
|
305
|
+
}
|
|
168
306
|
/** 当前用户能处理的审批(新的在前,最多 {@link APPROVALS_LIST_MAX} 条);`status` 缺省为全部。 */
|
|
169
307
|
async list(orgId, accountId, status) {
|
|
170
308
|
const role = await this.roleOf(orgId, accountId);
|
|
@@ -220,8 +358,196 @@ let ApprovalsService = class ApprovalsService {
|
|
|
220
358
|
* `always` → 发起人 × agent × 工具)、`approval_decided` 事件(幂等键 `approval_decided:<id>`)与审计;
|
|
221
359
|
* 提交后发布事件并调用 `onDecided`。
|
|
222
360
|
*/
|
|
223
|
-
async decide(orgId, id, accountId, body, actorTokenId) {
|
|
361
|
+
async decide(orgId, id, accountId, body, actorTokenId, via = "web") {
|
|
362
|
+
const kind = await this.db
|
|
363
|
+
.selectFrom("approvals")
|
|
364
|
+
.select("kind")
|
|
365
|
+
.where("id", "=", id)
|
|
366
|
+
.where("org_id", "=", orgId)
|
|
367
|
+
.executeTakeFirst();
|
|
368
|
+
if (!kind)
|
|
369
|
+
throw notFound();
|
|
370
|
+
if (kind.kind === "network") {
|
|
371
|
+
return this.decideNetwork(orgId, id, accountId, body, actorTokenId, via);
|
|
372
|
+
}
|
|
373
|
+
if (body.hosts !== undefined) {
|
|
374
|
+
throw new ApiError("validation_failed", "hosts are only for network asks", [
|
|
375
|
+
{ path: "hosts", message: "Only a network ask takes hosts" },
|
|
376
|
+
]);
|
|
377
|
+
}
|
|
224
378
|
const { record, toSeq } = await this.decideIn(orgId, id, accountId, body, actorTokenId);
|
|
379
|
+
await this.announceDecided(record, toSeq);
|
|
380
|
+
return record;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* A network ask's decision (network ask-to-allow spec §5.2), one at a time per ask across
|
|
384
|
+
* replicas: only the device owner (`requested_for`) decides, in a signed-in session (a personal
|
|
385
|
+
* access token is refused; the route already refuses service and embed tokens); the allowed
|
|
386
|
+
* hosts are a subset of those asked. 「这台设备一直允许」 sends the `policy` op first, outside any
|
|
387
|
+
* transaction; when the device cannot be reached or lacks the feature the decision is recorded
|
|
388
|
+
* as a run grant with the reason, and when the device refuses (its owner takes no remote grants)
|
|
389
|
+
* as `always` with outcome `refused` and nothing granted. Then {@link decideIn} records what took
|
|
390
|
+
* effect and the run resumes after the commit; a change the device applied after the ask closed
|
|
391
|
+
* is audited (`late`) and answered with a 409 that says so.
|
|
392
|
+
*/
|
|
393
|
+
async decideNetwork(orgId, id, accountId, body, actorTokenId, via) {
|
|
394
|
+
const row = await this.db
|
|
395
|
+
.selectFrom("approvals")
|
|
396
|
+
.selectAll()
|
|
397
|
+
.select(sql `expires_at <= now()`.as("expired"))
|
|
398
|
+
.where("id", "=", id)
|
|
399
|
+
.where("org_id", "=", orgId)
|
|
400
|
+
.executeTakeFirst();
|
|
401
|
+
if (!row)
|
|
402
|
+
throw notFound();
|
|
403
|
+
const ask = toRecord(row);
|
|
404
|
+
const detail = ask.detail;
|
|
405
|
+
if (detail === null)
|
|
406
|
+
throw new Error(`network ask ${id} has no detail`);
|
|
407
|
+
// the device's owner now (an ownership change since the ask is honored), never anyone else
|
|
408
|
+
const owner = await this.deviceOwnerOf(orgId, detail.deviceId);
|
|
409
|
+
if (ask.requestedFor !== accountId || owner !== accountId) {
|
|
410
|
+
// named only to the people the ask is shown to (the run's starter, the asked owner); any
|
|
411
|
+
// other member learns nothing about it, as `get` (approval.not_found)
|
|
412
|
+
const trigger = await this.db
|
|
413
|
+
.selectFrom("runs")
|
|
414
|
+
.select("trigger_account_id")
|
|
415
|
+
.where("id", "=", ask.runId)
|
|
416
|
+
.executeTakeFirst();
|
|
417
|
+
if (accountId !== ask.requestedFor && accountId !== trigger?.trigger_account_id) {
|
|
418
|
+
throw notFound();
|
|
419
|
+
}
|
|
420
|
+
const name = owner === null ? null : await this.displayNameOf(owner);
|
|
421
|
+
throw new ApiError("approval.not_yours", `Only ${name ?? "the device's owner"} can allow network access on this device.`);
|
|
422
|
+
}
|
|
423
|
+
if (actorTokenId !== undefined) {
|
|
424
|
+
throw new ApiError("auth.session_required", "Network access is decided in a signed-in session, not with a personal access token.");
|
|
425
|
+
}
|
|
426
|
+
if (ask.status !== "pending") {
|
|
427
|
+
throw new ApiError("approval.already_decided", "Approval has already been decided");
|
|
428
|
+
}
|
|
429
|
+
if (row.expired)
|
|
430
|
+
throw new ApiError("approval.already_decided", "Approval has expired");
|
|
431
|
+
const asked = [...new Set(detail.hosts.map((h) => h.host))];
|
|
432
|
+
let network;
|
|
433
|
+
if (body.decision === "approve") {
|
|
434
|
+
const scope = body.scope ?? "once";
|
|
435
|
+
if (scope === "session" || !ask.scopes.includes(scope)) {
|
|
436
|
+
throw new ApiError("approval.scope_not_allowed", `Scope "${scope}" is not allowed for this approval; allowed: ${ask.scopes.join(", ")}`);
|
|
437
|
+
}
|
|
438
|
+
const unknown = (body.hosts ?? []).filter((h) => !asked.includes(h));
|
|
439
|
+
if (unknown.length > 0) {
|
|
440
|
+
throw new ApiError("validation_failed", "hosts must be among the hosts asked about", [
|
|
441
|
+
{ path: "hosts", message: `Not asked about: ${unknown.join(", ")}` },
|
|
442
|
+
]);
|
|
443
|
+
}
|
|
444
|
+
// exact hosts only: the device and the run grants never take anything else
|
|
445
|
+
const granted = [...new Set(body.hosts ?? asked)].filter((h) => !h.startsWith("*.") && validateAllowHost(h).length === 0);
|
|
446
|
+
if (granted.length === 0) {
|
|
447
|
+
throw new ApiError("validation_failed", "No host to allow", [
|
|
448
|
+
{ path: "hosts", message: "Choose at least one host" },
|
|
449
|
+
]);
|
|
450
|
+
}
|
|
451
|
+
network = { wanted: granted, granted, outcome: "once", via };
|
|
452
|
+
if (scope === "always") {
|
|
453
|
+
const applied = await this.applyAlways(orgId, detail.deviceId, granted, {
|
|
454
|
+
approvalId: id,
|
|
455
|
+
accountId,
|
|
456
|
+
via,
|
|
457
|
+
});
|
|
458
|
+
network =
|
|
459
|
+
applied === null
|
|
460
|
+
? { wanted: granted, granted, outcome: "always", via }
|
|
461
|
+
: applied === "refused"
|
|
462
|
+
? // the device's owner takes no remote grants: nothing is allowed, not even this run
|
|
463
|
+
{ wanted: granted, granted: [], outcome: "refused", fallback: applied, via }
|
|
464
|
+
: { wanted: granted, granted, outcome: "once_fallback", fallback: applied, via };
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
if (body.hosts !== undefined) {
|
|
469
|
+
throw new ApiError("validation_failed", "hosts are only given when allowing", [
|
|
470
|
+
{ path: "hosts", message: "Only an approval takes hosts" },
|
|
471
|
+
]);
|
|
472
|
+
}
|
|
473
|
+
network = { wanted: [], granted: [], outcome: null, via };
|
|
474
|
+
}
|
|
475
|
+
let decided;
|
|
476
|
+
try {
|
|
477
|
+
decided = await this.decideIn(orgId, id, accountId, body, actorTokenId, network);
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
// the device took the hosts, but a reply, the expiry or a stop closed the ask meanwhile:
|
|
481
|
+
// the change stands (the owner chose it), so it is audited and said
|
|
482
|
+
if (network.outcome === "always" &&
|
|
483
|
+
e instanceof ApiError &&
|
|
484
|
+
e.code === "approval.already_decided") {
|
|
485
|
+
await this.auditLateAllow(orgId, ask, network);
|
|
486
|
+
throw new ApiError("approval.already_decided", `The device now allows ${network.granted.join(", ")}; the request had already closed.`);
|
|
487
|
+
}
|
|
488
|
+
throw e;
|
|
489
|
+
}
|
|
490
|
+
await this.announceDecided(decided.record, decided.toSeq);
|
|
491
|
+
return decided.record;
|
|
492
|
+
}
|
|
493
|
+
/** `device.network_allowed` of a policy change that reached the device after the ask closed. */
|
|
494
|
+
async auditLateAllow(orgId, ask, network) {
|
|
495
|
+
if (ask.detail === null)
|
|
496
|
+
return;
|
|
497
|
+
await audit(this.db.db, {
|
|
498
|
+
actorAccountId: ask.requestedFor,
|
|
499
|
+
orgId,
|
|
500
|
+
action: "device.network_allowed",
|
|
501
|
+
target: ask.detail.deviceId,
|
|
502
|
+
detail: {
|
|
503
|
+
approvalId: ask.id,
|
|
504
|
+
deviceId: ask.detail.deviceId,
|
|
505
|
+
hosts: network.granted,
|
|
506
|
+
scope: "always",
|
|
507
|
+
outcome: "always",
|
|
508
|
+
late: true,
|
|
509
|
+
by: ask.requestedFor,
|
|
510
|
+
via: network.via,
|
|
511
|
+
conversationId: ask.conversationId,
|
|
512
|
+
runId: ask.runId,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/** The owner of a live device of the org (null: none, deleted, or of another org). */
|
|
517
|
+
async deviceOwnerOf(orgId, deviceId) {
|
|
518
|
+
const d = await this.db
|
|
519
|
+
.selectFrom("devices")
|
|
520
|
+
.select("owner_account_id")
|
|
521
|
+
.where("id", "=", deviceId)
|
|
522
|
+
.where("org_id", "=", orgId)
|
|
523
|
+
.where("deleted_at", "is", null)
|
|
524
|
+
.executeTakeFirst();
|
|
525
|
+
return d?.owner_account_id ?? null;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Adds the hosts to the device's local policy (the `policy` op; never inside a transaction).
|
|
529
|
+
* Null when it was applied, else why not: the decision falls back to a run grant.
|
|
530
|
+
*/
|
|
531
|
+
async applyAlways(orgId, deviceId, hosts, source) {
|
|
532
|
+
const accountName = (await this.displayNameOf(source.accountId)) ?? source.accountId;
|
|
533
|
+
try {
|
|
534
|
+
const out = await this.policies.apply({
|
|
535
|
+
orgId,
|
|
536
|
+
deviceId,
|
|
537
|
+
change: { add: hosts },
|
|
538
|
+
source: { ...source, accountName },
|
|
539
|
+
});
|
|
540
|
+
return out.applied ? null : out.reason;
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
// the device was deleted meanwhile: nothing to write to, the run cannot go on there either
|
|
544
|
+
if (e instanceof ApiError && e.code === "device.not_found")
|
|
545
|
+
return "offline";
|
|
546
|
+
throw e;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/** After the commit: the decision's events reach the page, the run's waits and IM delivery. */
|
|
550
|
+
async announceDecided(record, toSeq) {
|
|
225
551
|
await this.eventLog.notify(record.conversationId, toSeq);
|
|
226
552
|
// waits resume the run and its IM delivery is enqueued (handler failures are logged)
|
|
227
553
|
await this.events.emit("approval.decided", {
|
|
@@ -231,11 +557,19 @@ let ApprovalsService = class ApprovalsService {
|
|
|
231
557
|
runId: record.runId,
|
|
232
558
|
status: record.status === "approved" ? "approved" : "denied",
|
|
233
559
|
});
|
|
234
|
-
|
|
560
|
+
}
|
|
561
|
+
/** An account's display name (null when unknown). */
|
|
562
|
+
async displayNameOf(accountId) {
|
|
563
|
+
const a = await this.db
|
|
564
|
+
.selectFrom("accounts")
|
|
565
|
+
.select("display_name")
|
|
566
|
+
.where("id", "=", accountId)
|
|
567
|
+
.executeTakeFirst();
|
|
568
|
+
return a?.display_name ?? null;
|
|
235
569
|
}
|
|
236
570
|
// its own transaction (as the legacy one was): `approval.decided` is emitted only after the
|
|
237
571
|
// decision committed, even when a caller has a transaction open
|
|
238
|
-
async decideIn(orgId, id, accountId, body, actorTokenId) {
|
|
572
|
+
async decideIn(orgId, id, accountId, body, actorTokenId, network) {
|
|
239
573
|
const t = this.db.db;
|
|
240
574
|
const locked = await t
|
|
241
575
|
.selectFrom("approvals")
|
|
@@ -267,9 +601,19 @@ let ApprovalsService = class ApprovalsService {
|
|
|
267
601
|
if (locked.expired) {
|
|
268
602
|
throw new ApiError("approval.already_decided", "Approval has expired");
|
|
269
603
|
}
|
|
604
|
+
// a network ask is decided only through decideNetwork (its policy op went first)
|
|
605
|
+
if ((before.kind === "network") !== (network !== undefined)) {
|
|
606
|
+
throw new Error(`approval ${id} is of kind ${before.kind}`);
|
|
607
|
+
}
|
|
270
608
|
const approved = body.decision === "approve";
|
|
271
609
|
const reason = approved ? undefined : body.reason?.trim() || undefined;
|
|
272
|
-
const scope = approved
|
|
610
|
+
const scope = !approved
|
|
611
|
+
? null
|
|
612
|
+
: network === undefined
|
|
613
|
+
? (body.scope ?? "once")
|
|
614
|
+
: network.outcome === "always" || network.outcome === "refused"
|
|
615
|
+
? "always"
|
|
616
|
+
: "once";
|
|
273
617
|
// 按角色升级的批准只对这一次有效:授权记在发起人名下会让其以后绕过角色要求
|
|
274
618
|
if (scope !== null && scope !== "once" && before.requestedFor === null) {
|
|
275
619
|
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' }]);
|
|
@@ -287,6 +631,17 @@ let ApprovalsService = class ApprovalsService {
|
|
|
287
631
|
decided_by: accountId,
|
|
288
632
|
decided_at: sql `now()`,
|
|
289
633
|
deny_reason: reason ?? null,
|
|
634
|
+
...(network?.outcome != null &&
|
|
635
|
+
before.detail !== null && {
|
|
636
|
+
// what took effect; run grants read grantedHosts (an always grant's hosts are on
|
|
637
|
+
// the device's policy already, so it carries them harmlessly)
|
|
638
|
+
detail: JSON.stringify({
|
|
639
|
+
...before.detail,
|
|
640
|
+
grantedHosts: network.granted,
|
|
641
|
+
outcome: network.outcome,
|
|
642
|
+
...(network.fallback !== undefined && { fallback: network.fallback }),
|
|
643
|
+
}),
|
|
644
|
+
}),
|
|
290
645
|
})
|
|
291
646
|
.where("id", "=", id)
|
|
292
647
|
.returningAll()
|
|
@@ -294,7 +649,10 @@ let ApprovalsService = class ApprovalsService {
|
|
|
294
649
|
// 授权绑定所批准的定义指纹与来源连接器(同 slug 重新安装的连接器 id 不同,不继承)
|
|
295
650
|
const hash = before.specHash;
|
|
296
651
|
const connectorId = before.connectorId;
|
|
297
|
-
if (
|
|
652
|
+
if (network !== undefined) {
|
|
653
|
+
// a network ask grants nothing of the tool kind: its run grant is the row itself
|
|
654
|
+
}
|
|
655
|
+
else if (scope === "session") {
|
|
298
656
|
// a sub-agent's grant is the root conversation's (agent map spec §7): the root and every
|
|
299
657
|
// agent of its tree, at any depth, are not asked again for the same call
|
|
300
658
|
await t
|
|
@@ -330,6 +688,14 @@ let ApprovalsService = class ApprovalsService {
|
|
|
330
688
|
}))
|
|
331
689
|
.execute();
|
|
332
690
|
}
|
|
691
|
+
if (approved && scope !== null && network === undefined) {
|
|
692
|
+
await this.keepTrialGrant(before, accountId);
|
|
693
|
+
}
|
|
694
|
+
// 「这次允许」 in a trial run (also an 「一直允许」 the device could not take): the task's
|
|
695
|
+
// grants; one the device took needs none (network ask-to-allow spec §8.1)
|
|
696
|
+
if (network?.outcome === "once" || network?.outcome === "once_fallback") {
|
|
697
|
+
await this.keepTrialNetworkGrant(before, accountId, network.granted);
|
|
698
|
+
}
|
|
333
699
|
const written = await this.eventLog.appendIn(before.conversationId, [
|
|
334
700
|
{
|
|
335
701
|
type: "approval_decided",
|
|
@@ -345,6 +711,31 @@ let ApprovalsService = class ApprovalsService {
|
|
|
345
711
|
},
|
|
346
712
|
},
|
|
347
713
|
]);
|
|
714
|
+
if (network !== undefined && before.detail !== null) {
|
|
715
|
+
const asked = [...new Set(before.detail.hosts.map((h) => h.host))];
|
|
716
|
+
const refused = asked.filter((h) => !network.wanted.includes(h));
|
|
717
|
+
await audit(t, {
|
|
718
|
+
actorAccountId: accountId,
|
|
719
|
+
orgId,
|
|
720
|
+
action: approved ? "device.network_allowed" : "device.network_denied",
|
|
721
|
+
target: before.detail.deviceId,
|
|
722
|
+
detail: {
|
|
723
|
+
approvalId: id,
|
|
724
|
+
deviceId: before.detail.deviceId,
|
|
725
|
+
hosts: approved ? network.wanted : asked,
|
|
726
|
+
...(approved && refused.length > 0 && { refused }),
|
|
727
|
+
scope,
|
|
728
|
+
outcome: approved ? network.outcome : "denied",
|
|
729
|
+
...(network.fallback !== undefined && { fallback: network.fallback }),
|
|
730
|
+
by: accountId,
|
|
731
|
+
via: network.via,
|
|
732
|
+
conversationId: before.conversationId,
|
|
733
|
+
runId: before.runId,
|
|
734
|
+
...(reason !== undefined && { reason }),
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
return { record: toRecord(updated), toSeq: Math.max(...written.map((e) => e.seq)) };
|
|
738
|
+
}
|
|
348
739
|
await audit(t, {
|
|
349
740
|
actorAccountId: accountId,
|
|
350
741
|
...(actorTokenId !== undefined && { actorTokenId }),
|
|
@@ -365,6 +756,103 @@ let ApprovalsService = class ApprovalsService {
|
|
|
365
756
|
});
|
|
366
757
|
return { record: toRecord(updated), toSeq: Math.max(...written.map((e) => e.seq)) };
|
|
367
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* A trial run of a scheduled task (scheduled tasks spec §7): an approval its creator granted, in
|
|
761
|
+
* any scope, is also added to the task's grants with the approved definition's hash, so its
|
|
762
|
+
* unattended runs do not ask again. Kept only by the rule `addGrants` applies
|
|
763
|
+
* (`grantRefusalOf`, against the task agent's tools now): never a role-escalated approval
|
|
764
|
+
* (`requestedFor` null, it stays `once`), one another account decided, one whose scopes never
|
|
765
|
+
* allowed remembering (an org / platform operation: "once" only), a per-call key
|
|
766
|
+
* (`Platform:<operation>`, a skill change among them), a fixed ask, a tool the org forbids (its key or the
|
|
767
|
+
* call's), or a tool the task's agent does not have. In the decision's transaction.
|
|
768
|
+
*/
|
|
769
|
+
async keepTrialGrant(a, decidedBy) {
|
|
770
|
+
if (a.requestedFor === null || a.requestedFor !== decidedBy)
|
|
771
|
+
return;
|
|
772
|
+
if (!a.scopes.includes("always"))
|
|
773
|
+
return;
|
|
774
|
+
const t = this.db.db;
|
|
775
|
+
const served = await unattendedTaskRunOf(t, a.runId);
|
|
776
|
+
if (served === null || !served.trial)
|
|
777
|
+
return;
|
|
778
|
+
const task = await t
|
|
779
|
+
.selectFrom("scheduled_tasks as s")
|
|
780
|
+
.innerJoin("agents as ag", "ag.id", "s.agent_id")
|
|
781
|
+
.leftJoin("agent_versions as v", (j) => j.onRef("v.agent_id", "=", "ag.id").onRef("v.version", "=", "ag.current_version"))
|
|
782
|
+
.select(["s.account_id", "v.definition"])
|
|
783
|
+
.where("s.id", "=", served.taskId)
|
|
784
|
+
.where("s.deleted_at", "is", null)
|
|
785
|
+
.executeTakeFirst();
|
|
786
|
+
if (task?.account_id !== decidedBy)
|
|
787
|
+
return;
|
|
788
|
+
const member = await t
|
|
789
|
+
.selectFrom("memberships")
|
|
790
|
+
.select("role")
|
|
791
|
+
.where("org_id", "=", a.orgId)
|
|
792
|
+
.where("account_id", "=", decidedBy)
|
|
793
|
+
.executeTakeFirst();
|
|
794
|
+
if (!member)
|
|
795
|
+
return;
|
|
796
|
+
const def = task.definition === null
|
|
797
|
+
? undefined
|
|
798
|
+
: await normalizeStoredDefinitionIn(t, a.orgId, task.definition);
|
|
799
|
+
const { tools } = await this.registry.forAgent(a.orgId, {
|
|
800
|
+
disabledTools: def?.disabledTools ?? [],
|
|
801
|
+
subagentTools: def?.subagentTools,
|
|
802
|
+
subagents: def?.subagents,
|
|
803
|
+
});
|
|
804
|
+
const tool = tools.find((x) => x.key === a.toolKey);
|
|
805
|
+
const { forbiddenTools } = await readScheduleSettings(t, this.config, a.orgId);
|
|
806
|
+
const why = grantRefusalOf(a.toolKey, tool, {
|
|
807
|
+
role: member.role,
|
|
808
|
+
forbiddenTools,
|
|
809
|
+
});
|
|
810
|
+
if (why !== null)
|
|
811
|
+
return;
|
|
812
|
+
await this.schedules.addGrant(t, served.taskId, {
|
|
813
|
+
toolKey: a.toolKey,
|
|
814
|
+
specHash: a.specHash,
|
|
815
|
+
connectorId: a.connectorId,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* A trial run of a scheduled task (network ask-to-allow spec §8.1): the hosts its creator allowed
|
|
820
|
+
* for this run (「这次允许」) on a network ask about one of their devices become the task's
|
|
821
|
+
* `network:<host>` grants, each only when `networkGrantRefusal` allows it (exact host, within the
|
|
822
|
+
* org's effective egress, the device takes remote grants). Never for an ask another account
|
|
823
|
+
* decided or one asked of someone else. In the decision's transaction.
|
|
824
|
+
*/
|
|
825
|
+
async keepTrialNetworkGrant(a, decidedBy, hosts) {
|
|
826
|
+
if (hosts.length === 0 || a.detail === null)
|
|
827
|
+
return;
|
|
828
|
+
if (a.requestedFor === null || a.requestedFor !== decidedBy)
|
|
829
|
+
return;
|
|
830
|
+
const t = this.db.db;
|
|
831
|
+
const served = await unattendedTaskRunOf(t, a.runId);
|
|
832
|
+
if (served === null || !served.trial)
|
|
833
|
+
return;
|
|
834
|
+
const task = await t
|
|
835
|
+
.selectFrom("scheduled_tasks")
|
|
836
|
+
.select("account_id")
|
|
837
|
+
.where("id", "=", served.taskId)
|
|
838
|
+
.where("org_id", "=", a.orgId)
|
|
839
|
+
.where("deleted_at", "is", null)
|
|
840
|
+
.executeTakeFirst();
|
|
841
|
+
if (task?.account_id !== decidedBy)
|
|
842
|
+
return;
|
|
843
|
+
const limits = await this.sandbox.ceilingLimits(a.orgId);
|
|
844
|
+
const summary = await this.policies.summary(a.detail.deviceId);
|
|
845
|
+
for (const host of hosts) {
|
|
846
|
+
if (networkGrantRefusal(host, { limits, deviceSummary: summary?.policy ?? null }) !== null) {
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
await this.schedules.addGrant(t, served.taskId, {
|
|
850
|
+
toolKey: `${NETWORK_GRANT_PREFIX}${host}`,
|
|
851
|
+
specHash: NETWORK_GRANT_SPEC_HASH,
|
|
852
|
+
connectorId: null,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
368
856
|
/**
|
|
369
857
|
* 详情,不做处理人校验(内核内部用:渠道出站投递组装 IM 审批卡片);不存在返回 null。
|
|
370
858
|
* 不得直接回给请求方——HTTP 接口用 {@link get}。
|
|
@@ -380,13 +868,27 @@ let ApprovalsService = class ApprovalsService {
|
|
|
380
868
|
const [view] = await this.views([toRecord(row)]);
|
|
381
869
|
return view ?? null;
|
|
382
870
|
}
|
|
383
|
-
/**
|
|
871
|
+
/** 某个工具调用最近一次的工具审批(attempt 最大者,网络询问除外);没有则返回 null。 */
|
|
384
872
|
async forCall(runId, callId) {
|
|
385
873
|
const row = await this.db
|
|
386
874
|
.selectFrom("approvals")
|
|
387
875
|
.selectAll()
|
|
388
876
|
.where("run_id", "=", runId)
|
|
389
877
|
.where("call_id", "=", callId)
|
|
878
|
+
.where("kind", "=", "tool")
|
|
879
|
+
.orderBy("attempt", "desc")
|
|
880
|
+
.limit(1)
|
|
881
|
+
.executeTakeFirst();
|
|
882
|
+
return row ? toRecord(row) : null;
|
|
883
|
+
}
|
|
884
|
+
/** The network ask of a tool call (at most one per call); null when it has none. */
|
|
885
|
+
async networkForCall(runId, callId) {
|
|
886
|
+
const row = await this.db
|
|
887
|
+
.selectFrom("approvals")
|
|
888
|
+
.selectAll()
|
|
889
|
+
.where("run_id", "=", runId)
|
|
890
|
+
.where("call_id", "=", callId)
|
|
891
|
+
.where("kind", "=", "network")
|
|
390
892
|
.orderBy("attempt", "desc")
|
|
391
893
|
.limit(1)
|
|
392
894
|
.executeTakeFirst();
|
|
@@ -487,14 +989,73 @@ let ApprovalsService = class ApprovalsService {
|
|
|
487
989
|
}
|
|
488
990
|
return { records, seqs };
|
|
489
991
|
}
|
|
490
|
-
/**
|
|
491
|
-
|
|
992
|
+
/**
|
|
993
|
+
* In the caller's transaction (`mandatory`; it wrote `authorAccountId`'s message in
|
|
994
|
+
* `conversationId` and holds that conversation's row lock): the message skips the pending
|
|
995
|
+
* network asks it closes (network ask-to-allow spec §6.7), as it skips questions: the asks of
|
|
996
|
+
* active runs `authorAccountId` started, in the conversation and in its sub-agents'
|
|
997
|
+
* conversations at any depth (their asks are shown in the root). Each becomes `skipped` with
|
|
998
|
+
* `approval_decided{skipped}` by the author; access stays blocked. Returns the skipped records and
|
|
999
|
+
* the highest seq written per conversation; the caller notifies and emits
|
|
1000
|
+
* `approval.decided{skipped}` after the commit, so the runs resume.
|
|
1001
|
+
*/
|
|
1002
|
+
async skipNetworkIn(conversationId, authorAccountId) {
|
|
1003
|
+
const t = this.db.db;
|
|
1004
|
+
const tree = await this.subtreeOf(conversationId);
|
|
1005
|
+
const rows = await t
|
|
1006
|
+
.selectFrom("approvals as a")
|
|
1007
|
+
.innerJoin("runs as r", "r.id", "a.run_id")
|
|
1008
|
+
.selectAll("a")
|
|
1009
|
+
.where("a.conversation_id", "in", tree)
|
|
1010
|
+
.where("a.kind", "=", "network")
|
|
1011
|
+
.where("a.status", "=", "pending")
|
|
1012
|
+
.where(sql `a.expires_at > now()`)
|
|
1013
|
+
.where("r.status", "in", ["running", "waiting"])
|
|
1014
|
+
.where("r.trigger_account_id", "=", authorAccountId)
|
|
1015
|
+
.orderBy("a.created_at")
|
|
1016
|
+
.orderBy("a.id")
|
|
1017
|
+
.forUpdate("a")
|
|
1018
|
+
.execute();
|
|
1019
|
+
const records = [];
|
|
1020
|
+
const seqs = new Map();
|
|
1021
|
+
for (const row of rows) {
|
|
1022
|
+
const updated = await t
|
|
1023
|
+
.updateTable("approvals")
|
|
1024
|
+
.set({ status: "skipped", decided_by: authorAccountId, decided_at: sql `now()` })
|
|
1025
|
+
.where("id", "=", row.id)
|
|
1026
|
+
.returningAll()
|
|
1027
|
+
.executeTakeFirstOrThrow();
|
|
1028
|
+
const seq = await this.decidedEvent(toRecord(row), "skipped", authorAccountId);
|
|
1029
|
+
seqs.set(row.conversation_id, Math.max(seqs.get(row.conversation_id) ?? 0, seq));
|
|
1030
|
+
records.push(toRecord(updated));
|
|
1031
|
+
}
|
|
1032
|
+
return { records, seqs };
|
|
1033
|
+
}
|
|
1034
|
+
/** `conversationId` and its sub-agents' conversations at any depth (at most 16 levels). */
|
|
1035
|
+
async subtreeOf(conversationId) {
|
|
1036
|
+
const all = [conversationId];
|
|
1037
|
+
let level = [conversationId];
|
|
1038
|
+
for (let depth = 0; depth < 16 && level.length > 0; depth++) {
|
|
1039
|
+
const rows = await this.db
|
|
1040
|
+
.selectFrom("conversations")
|
|
1041
|
+
.select("id")
|
|
1042
|
+
.where("parent_conversation_id", "in", level)
|
|
1043
|
+
.execute();
|
|
1044
|
+
level = rows.map((r) => r.id).filter((id) => !all.includes(id));
|
|
1045
|
+
all.push(...level);
|
|
1046
|
+
}
|
|
1047
|
+
return all;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* 系统写的 `approval_decided`(超时 / 取消,处理人为 null;跳过,处理人为消息作者),在当前事务内;返回事件 seq。
|
|
1051
|
+
*/
|
|
1052
|
+
async decidedEvent(a, decision, byAccountId = null) {
|
|
492
1053
|
const written = await this.eventLog.appendIn(a.conversationId, [
|
|
493
1054
|
{
|
|
494
1055
|
type: "approval_decided",
|
|
495
1056
|
runId: a.runId,
|
|
496
1057
|
idemKey: `approval_decided:${a.id}`,
|
|
497
|
-
payload: { approvalId: a.id, callId: a.callId, decision, byAccountId
|
|
1058
|
+
payload: { approvalId: a.id, callId: a.callId, decision, byAccountId },
|
|
498
1059
|
},
|
|
499
1060
|
]);
|
|
500
1061
|
return Math.max(...written.map((e) => e.seq));
|
|
@@ -620,7 +1181,7 @@ let ApprovalsService = class ApprovalsService {
|
|
|
620
1181
|
const prefix = app ? connectorToolKey(app.slug, "") : undefined;
|
|
621
1182
|
return [
|
|
622
1183
|
{
|
|
623
|
-
...r,
|
|
1184
|
+
...publicRecord(r),
|
|
624
1185
|
toolName: prefix !== undefined && r.toolKey.startsWith(prefix)
|
|
625
1186
|
? r.toolKey.slice(prefix.length)
|
|
626
1187
|
: r.toolKey,
|
|
@@ -647,9 +1208,24 @@ let ApprovalsService = class ApprovalsService {
|
|
|
647
1208
|
__decorate([
|
|
648
1209
|
Transactional()
|
|
649
1210
|
], ApprovalsService.prototype, "requestIn", null);
|
|
1211
|
+
__decorate([
|
|
1212
|
+
Transactional()
|
|
1213
|
+
], ApprovalsService.prototype, "requestNetworkIn", null);
|
|
1214
|
+
__decorate([
|
|
1215
|
+
WithLock({ key: "network-ask-#{1}", ttl: 60_000, waitTimeout: 45_000 })
|
|
1216
|
+
], ApprovalsService.prototype, "decideNetwork", null);
|
|
1217
|
+
__decorate([
|
|
1218
|
+
Transactional()
|
|
1219
|
+
], ApprovalsService.prototype, "auditLateAllow", null);
|
|
650
1220
|
__decorate([
|
|
651
1221
|
Transactional({ propagation: "requires_new" })
|
|
652
1222
|
], ApprovalsService.prototype, "decideIn", null);
|
|
1223
|
+
__decorate([
|
|
1224
|
+
Transactional({ propagation: "mandatory" })
|
|
1225
|
+
], ApprovalsService.prototype, "keepTrialGrant", null);
|
|
1226
|
+
__decorate([
|
|
1227
|
+
Transactional({ propagation: "mandatory" })
|
|
1228
|
+
], ApprovalsService.prototype, "keepTrialNetworkGrant", null);
|
|
653
1229
|
__decorate([
|
|
654
1230
|
Transactional()
|
|
655
1231
|
], ApprovalsService.prototype, "expireIn", null);
|
|
@@ -659,6 +1235,9 @@ __decorate([
|
|
|
659
1235
|
__decorate([
|
|
660
1236
|
Transactional({ propagation: "mandatory" })
|
|
661
1237
|
], ApprovalsService.prototype, "cancelPendingFor", null);
|
|
1238
|
+
__decorate([
|
|
1239
|
+
Transactional({ propagation: "mandatory" })
|
|
1240
|
+
], ApprovalsService.prototype, "skipNetworkIn", null);
|
|
662
1241
|
__decorate([
|
|
663
1242
|
Transactional()
|
|
664
1243
|
], ApprovalsService.prototype, "revokeGrantIn", null);
|
|
@@ -668,6 +1247,11 @@ ApprovalsService = __decorate([
|
|
|
668
1247
|
__param(1, Inject(KernelConfigService)),
|
|
669
1248
|
__param(2, Inject(EventLog)),
|
|
670
1249
|
__param(3, Inject(KernelEvents)),
|
|
671
|
-
__param(4, Inject(TransactionHost))
|
|
1250
|
+
__param(4, Inject(TransactionHost)),
|
|
1251
|
+
__param(5, Inject(ScheduledTaskStore)),
|
|
1252
|
+
__param(6, Inject(ToolRegistry)),
|
|
1253
|
+
__param(7, Inject(DevicePolicyService)),
|
|
1254
|
+
__param(8, Inject(LockService)),
|
|
1255
|
+
__param(9, Inject(SandboxService))
|
|
672
1256
|
], ApprovalsService);
|
|
673
1257
|
export { ApprovalsService };
|