@capybara-hq/kernel 0.1.0-rc.19 → 0.1.0-rc.20
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 +25 -25
- package/dist/admin/admin-agents.controller.d.ts +28 -0
- package/dist/admin/admin-agents.controller.js +79 -0
- package/dist/admin/admin-connectors.controller.d.ts +29 -0
- package/dist/admin/admin-connectors.controller.js +79 -0
- package/dist/admin/admin-items.schemas.d.ts +42 -0
- package/dist/admin/admin-items.schemas.js +48 -0
- package/dist/admin/admin.controller.d.ts +11 -20
- package/dist/admin/admin.controller.js +28 -34
- package/dist/admin/admin.module.d.ts +4 -1
- package/dist/admin/admin.module.js +50 -4
- package/dist/admin/admin.schemas.d.ts +4 -0
- package/dist/admin/admin.schemas.js +4 -1
- package/dist/admin/audit.controller.d.ts +14 -0
- package/dist/admin/audit.controller.js +42 -0
- package/dist/admin/audit.schemas.d.ts +8 -0
- package/dist/admin/audit.schemas.js +17 -0
- package/dist/admin/health.service.d.ts +104 -0
- package/dist/admin/health.service.js +246 -0
- package/dist/admin/overview.controller.d.ts +31 -0
- package/dist/admin/overview.controller.js +78 -0
- package/dist/admin/overview.schemas.d.ts +8 -0
- package/dist/admin/overview.schemas.js +13 -0
- package/dist/admin/overview.service.d.ts +112 -0
- package/dist/admin/overview.service.js +223 -0
- package/dist/admin/platform-audit.service.d.ts +41 -0
- package/dist/admin/platform-audit.service.js +70 -0
- package/dist/admin/usage.service.d.ts +149 -0
- package/dist/admin/usage.service.js +321 -0
- package/dist/agents/admin-agents.d.ts +92 -0
- package/dist/agents/admin-agents.js +170 -0
- package/dist/agents/agents.module.d.ts +4 -3
- package/dist/agents/agents.module.js +7 -3
- package/dist/agents/agents.service.d.ts +41 -3
- package/dist/agents/agents.service.js +113 -27
- package/dist/agents/definition.d.ts +16 -0
- package/dist/agents/definition.js +32 -2
- package/dist/agents/org-templates.js +4 -0
- package/dist/background/launch.d.ts +1 -1
- package/dist/background/launch.js +3 -3
- package/dist/background/tasks.service.d.ts +35 -3
- package/dist/background/tasks.service.js +91 -36
- package/dist/catalog/catalog.module.js +8 -2
- package/dist/channels/channels.module.js +11 -2
- package/dist/channels/supervisor.d.ts +2 -0
- package/dist/channels/supervisor.js +4 -0
- package/dist/conversations/events.js +1 -1
- package/dist/conversations/titles.js +4 -8
- package/dist/db/kernel-schema.d.ts +8 -1
- package/dist/devices/devices.module.js +5 -2
- package/dist/errors.d.ts +22 -2
- package/dist/errors.js +22 -2
- package/dist/evals/evals.module.js +11 -2
- package/dist/extensions/permission-view.js +3 -3
- package/dist/http/api.module.js +10 -1
- package/dist/identity/settings.js +3 -2
- package/dist/identity/write-actor.d.ts +11 -0
- package/dist/identity/write-actor.js +7 -0
- package/dist/inbox/inbox.service.d.ts +2 -1
- package/dist/inbox/platform-inbox.service.d.ts +7 -1
- package/dist/inbox/platform-inbox.service.js +20 -2
- package/dist/infra/presence/presence.module.d.ts +6 -0
- package/dist/infra/presence/presence.module.js +22 -0
- package/dist/infra/presence/replica-presence.d.ts +87 -0
- package/dist/infra/presence/replica-presence.js +189 -0
- package/dist/kernel.module.js +3 -0
- package/dist/lifecycle/kernel-lifecycle.d.ts +3 -1
- package/dist/lifecycle/kernel-lifecycle.js +17 -3
- package/dist/local-devices/local-device-gateway.d.ts +6 -0
- package/dist/local-devices/local-device-gateway.js +12 -0
- package/dist/overrides.d.ts +7 -1
- package/dist/overrides.js +4 -0
- package/dist/platform/exclusions.js +15 -0
- package/dist/platform/operation.d.ts +1 -1
- package/dist/platform/operation.js +1 -1
- package/dist/platform/ops/admin-items.ops.d.ts +37 -0
- package/dist/platform/ops/admin-items.ops.js +278 -0
- package/dist/platform/ops/admin.ops.d.ts +10 -10
- package/dist/platform/ops/admin.ops.js +111 -42
- package/dist/platform/ops/agent-skills.ops.d.ts +29 -3
- package/dist/platform/ops/agent-skills.ops.js +290 -7
- package/dist/platform/ops/inbox.ops.js +7 -4
- package/dist/platform/ops/platform.ops.js +1 -1
- package/dist/platform/ops/skills.ops.js +1 -57
- 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 +8 -0
- package/dist/roles/gateway-role.d.ts +4 -0
- package/dist/roles/gateway-role.js +8 -0
- package/dist/runtime/run-jobs.js +4 -2
- package/dist/sandbox/sandbox-file-reader.d.ts +35 -0
- package/dist/sandbox/sandbox-file-reader.js +98 -0
- package/dist/skills/agent-skills.controller.d.ts +48 -0
- package/dist/skills/agent-skills.controller.js +131 -0
- package/dist/skills/agent-skills.service.d.ts +262 -0
- package/dist/skills/agent-skills.service.js +996 -0
- package/dist/skills/catalog.d.ts +8 -6
- package/dist/skills/catalog.js +8 -4
- package/dist/skills/changes.controller.d.ts +5 -22
- package/dist/skills/changes.controller.js +7 -55
- package/dist/skills/changes.service.d.ts +69 -115
- package/dist/skills/changes.service.js +249 -599
- package/dist/skills/conversation-catalog.d.ts +53 -0
- package/dist/skills/conversation-catalog.js +149 -0
- package/dist/skills/invocation-keys.d.ts +1 -1
- package/dist/skills/invocation-keys.js +1 -1
- package/dist/skills/package.d.ts +0 -5
- package/dist/skills/package.js +0 -14
- package/dist/skills/skills.controller.js +1 -10
- package/dist/skills/skills.module.js +15 -2
- package/dist/skills/skills.schemas.d.ts +11 -15
- package/dist/skills/skills.schemas.js +27 -20
- package/dist/skills/skills.service.d.ts +37 -30
- package/dist/skills/skills.service.js +107 -59
- package/dist/skills/usable.d.ts +4 -3
- package/dist/skills/usable.js +13 -4
- package/dist/tools/admin-connectors.d.ts +69 -0
- package/dist/tools/admin-connectors.js +135 -0
- package/dist/tools/builtin-names.d.ts +9 -14
- package/dist/tools/builtin-names.js +13 -22
- package/dist/tools/builtins/builtin-tools.d.ts +2 -3
- package/dist/tools/builtins/builtin-tools.js +21 -26
- package/dist/tools/builtins/sandbox/bash.js +2 -2
- package/dist/tools/builtins/skill-load.js +1 -0
- package/dist/tools/connectors.controller.d.ts +5 -1
- package/dist/tools/connectors.controller.js +17 -3
- package/dist/tools/connectors.service.d.ts +20 -1
- package/dist/tools/connectors.service.js +61 -0
- package/dist/tools/legacy-keys.js +0 -1
- package/dist/tools/org-tool-switches.d.ts +3 -2
- package/dist/tools/org-tool-switches.js +4 -4
- package/dist/tools/policy.d.ts +7 -9
- package/dist/tools/policy.js +11 -22
- package/dist/tools/tools.module.js +3 -0
- package/package.json +1 -1
- package/src/migrations/0109_audit_platform_idx.sql +5 -0
- package/src/migrations/0110_usage_time_idx.sql +8 -0
- package/src/migrations/0111_agent_skill_sharing.sql +22 -0
- package/src/migrations/0112_skill_share_requests.sql +4 -0
- package/dist/tools/builtins/skill-write.d.ts +0 -59
- package/dist/tools/builtins/skill-write.js +0 -244
|
@@ -7,65 +7,45 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
8
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
9
|
};
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { parseSkillMd, SKILL_ENTRY_FILE, ToolError } from "@capybara-hq/plugin-sdk";
|
|
12
10
|
import { Inject, Injectable } from "@nestjs/common";
|
|
13
11
|
import { sql } from "kysely";
|
|
14
|
-
import { visibleTo } from "../agents/agent-lookup.js";
|
|
15
|
-
import { skillFilesPrefix } from "../conversations/attachments.service.js";
|
|
16
12
|
import { EventLog } from "../conversations/event-log.js";
|
|
17
13
|
import { ApiError } from "../errors.js";
|
|
18
14
|
import { audit } from "../identity/audit.js";
|
|
15
|
+
import { assertCustomAllowed } from "../identity/settings.js";
|
|
19
16
|
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
20
17
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
21
18
|
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
22
19
|
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
23
|
-
import {
|
|
20
|
+
import { OrgLock } from "../orgs/org-lock.js";
|
|
24
21
|
import { StorageService } from "../storage/storage.service.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
22
|
+
import { assertShareableName, isOrgAdmin, shareSkillIn } from "./agent-skills.service.js";
|
|
23
|
+
import { cancelRequestIn, revertTrialCatalog, } from "./conversation-catalog.js";
|
|
24
|
+
import { SkillsService } from "./skills.service.js";
|
|
27
25
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* The cap on file contents read for one detail (a package may hold up to 50 MB; the inbox shows
|
|
27
|
+
* text for review, so files past the cap read as no content).
|
|
30
28
|
*/
|
|
31
|
-
export const AGENT_SKILLS_MAX = 50;
|
|
32
|
-
/** 写技能文件时的并发数。 */
|
|
33
|
-
const PUT_CONCURRENCY = 4;
|
|
34
|
-
/** 详情里读取文件内容的上限(草稿 ≤ 1MB,留余量)。 */
|
|
35
29
|
const DETAIL_MAX_BYTES = 2 * 1024 * 1024;
|
|
36
|
-
/**
|
|
30
|
+
/** The most skill changes one inbox read returns. */
|
|
37
31
|
export const SKILL_CHANGES_LIST_MAX = 100;
|
|
38
|
-
// ---- 模型可见文案(英文) ----
|
|
39
|
-
const STORAGE_UNAVAILABLE_TEXT = "File storage is temporarily unavailable, so the skill could not be saved; retry later.";
|
|
40
|
-
const pendingElsewhereText = (name) => `Skill "${name}" already has a change awaiting the owner's approval (requested in another conversation), so it cannot be changed now; ask the user to have the owner decide that change first, or use a different skill name.`;
|
|
41
|
-
const capText = () => `This agent already has ${AGENT_SKILLS_MAX} skills (including changes awaiting approval), the maximum; update an existing skill instead, or ask the agent's owner to delete one of this agent's skills.`;
|
|
42
|
-
const AGENT_GONE_TEXT = "This agent no longer exists, so the skill cannot be saved; ask the user to start a conversation with another agent.";
|
|
43
32
|
const iso = (v) => new Date(v).toISOString();
|
|
44
|
-
const byName = (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
45
33
|
const notFound = () => new ApiError("skill_change.not_found", "Skill change not found");
|
|
46
|
-
|
|
47
|
-
function writtenBody(pkg) {
|
|
48
|
-
const md = pkg.files.find((f) => f.path === SKILL_ENTRY_FILE);
|
|
49
|
-
if (!md)
|
|
50
|
-
return null;
|
|
51
|
-
try {
|
|
52
|
-
const parsed = parseSkillMd(new TextDecoder("utf-8", { fatal: true }).decode(md.data));
|
|
53
|
-
return "errors" in parsed ? null : parsed.body;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
34
|
+
const forbidden = () => new ApiError("forbidden", "Insufficient organization role");
|
|
59
35
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* - `decide
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
36
|
+
* Skill change requests decided by org admins (spec §5.4, §5.5): an agent's owner asks to share
|
|
37
|
+
* one of its skills with the org (`kind = 'share'`, created by `AgentSkillsService.share`), and
|
|
38
|
+
* the maintainer of a shared skill proposes a new version of it (`kind = 'version'`, created by
|
|
39
|
+
* `AgentSkillsService.register`).
|
|
40
|
+
* - `decide` (HTTP, human only; org admins, role read live under the org row lock): approving a
|
|
41
|
+
* share makes the skill an org skill with the requester as maintainer; approving a version
|
|
42
|
+
* writes it as the org skill's next version (`SkillsService.createVersionFromFiles`, the draft
|
|
43
|
+
* prefix becomes the version's) so every agent using it gets it at its next run start;
|
|
44
|
+
* rejecting a version deletes its draft and reverts the proposing conversation's trial; a share
|
|
45
|
+
* keeps its files whatever happens. A request that no longer applies (its agent deleted or
|
|
46
|
+
* transferred, its skill deleted or already moved) is cancelled instead.
|
|
47
|
+
* State is in PG (the org row lock serializes decisions), files in object storage
|
|
48
|
+
* `kernel/<orgId>/skills/<id>/`; no in-process state.
|
|
69
49
|
*/
|
|
70
50
|
let SkillChangesService = class SkillChangesService {
|
|
71
51
|
db;
|
|
@@ -73,300 +53,209 @@ let SkillChangesService = class SkillChangesService {
|
|
|
73
53
|
eventLog;
|
|
74
54
|
log;
|
|
75
55
|
txHost;
|
|
76
|
-
|
|
56
|
+
orgLock;
|
|
57
|
+
skills;
|
|
58
|
+
catalogDeps;
|
|
59
|
+
constructor(db, storage, eventLog, log, txHost, orgLock,
|
|
60
|
+
/** An approved version is an org skill version. */
|
|
61
|
+
skills) {
|
|
77
62
|
this.db = db;
|
|
78
63
|
this.storage = storage;
|
|
79
64
|
this.eventLog = eventLog;
|
|
80
65
|
this.log = log;
|
|
81
66
|
this.txHost = txHost;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let store;
|
|
86
|
-
try {
|
|
87
|
-
this.storage.assertAvailable();
|
|
88
|
-
store = this.storage.kernel(ctx.orgId);
|
|
89
|
-
}
|
|
90
|
-
catch (e) {
|
|
91
|
-
throw storageError(e);
|
|
92
|
-
}
|
|
93
|
-
const id = randomUUID();
|
|
94
|
-
const prefix = skillFilesPrefix(id);
|
|
95
|
-
try {
|
|
96
|
-
await putAll(store, prefix, pkg.files);
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
99
|
-
await this.deletePrefix(ctx.orgId, prefix);
|
|
100
|
-
throw storageError(e);
|
|
101
|
-
}
|
|
102
|
-
let committed;
|
|
103
|
-
try {
|
|
104
|
-
committed = await this.writeIn(ctx, pkg, id, prefix);
|
|
105
|
-
}
|
|
106
|
-
catch (e) {
|
|
107
|
-
await this.deletePrefix(ctx.orgId, prefix);
|
|
108
|
-
throw e;
|
|
109
|
-
}
|
|
110
|
-
if (committed.toSeq !== null) {
|
|
111
|
-
await this.eventLog.notify(ctx.conversationId, committed.toSeq);
|
|
112
|
-
}
|
|
113
|
-
for (const p of committed.drop)
|
|
114
|
-
await this.deletePrefix(ctx.orgId, p);
|
|
115
|
-
return committed.outcome;
|
|
67
|
+
this.orgLock = orgLock;
|
|
68
|
+
this.skills = skills;
|
|
69
|
+
this.catalogDeps = { db, log, eventLog };
|
|
116
70
|
}
|
|
117
71
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
72
|
+
* An org admin decides a request (see the class comment): not an org admin -> 403 `forbidden`;
|
|
73
|
+
* already decided -> 409 `skill_change.already_decided`; not in this org -> 404. Approving
|
|
74
|
+
* re-checks `customSkills` (403 `catalog.custom_disabled`) and, for a share, the name (409
|
|
75
|
+
* `skill.name_exists`; the request stays pending). A share whose skill has a newer version than
|
|
76
|
+
* the one reviewed is cancelled, never shared. Audit `skill.change_decided` (and
|
|
77
|
+
* `skill.shared`). Of concurrent decisions one wins, the others get `already_decided`.
|
|
122
78
|
*/
|
|
123
79
|
async decide(orgId, id, accountId, body, actorTokenId) {
|
|
124
80
|
const head = await this.db
|
|
125
81
|
.selectFrom("skill_change_requests")
|
|
126
|
-
.
|
|
82
|
+
.selectAll()
|
|
127
83
|
.where("id", "=", id)
|
|
128
84
|
.where("org_id", "=", orgId)
|
|
129
85
|
.executeTakeFirst();
|
|
130
86
|
if (!head)
|
|
131
87
|
throw notFound();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
88
|
+
// Refused before any file is read; re-checked under the org row lock
|
|
89
|
+
if (!(await isOrgAdmin(this.db.db, orgId, accountId)))
|
|
90
|
+
throw forbidden();
|
|
91
|
+
// An approved version needs its files as a package (read before the transaction) and, for a
|
|
92
|
+
// skill the org promoted, written as the platform row's next version: no storage writes run
|
|
93
|
+
// inside the transaction
|
|
94
|
+
let pkg;
|
|
95
|
+
let owner;
|
|
96
|
+
if (body.decision === "approve" && head.kind === "version" && head.status === "pending") {
|
|
97
|
+
await assertCustomAllowed(this.db.db, "customSkills");
|
|
98
|
+
pkg = await this.readDraft(orgId, head);
|
|
99
|
+
owner = await this.skills.prepareStoredVersion(orgId, pkg);
|
|
136
100
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
101
|
+
let done;
|
|
102
|
+
try {
|
|
103
|
+
done = await this.decideIn(orgId, id, accountId, body, actorTokenId, head, pkg, owner);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
if (owner !== undefined)
|
|
107
|
+
await this.skills.discardStoredVersion(owner);
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
if (owner !== undefined && !done.usedOwner)
|
|
111
|
+
await this.skills.discardStoredVersion(owner);
|
|
112
|
+
for (const [conv, seq] of done.drafts.seqs)
|
|
113
|
+
await this.eventLog.notify(conv, seq);
|
|
114
|
+
for (const p of done.drafts.prefixes)
|
|
115
|
+
await this.deletePrefix(orgId, p);
|
|
116
|
+
const [view] = await this.views([done.updated]);
|
|
140
117
|
if (!view)
|
|
141
118
|
throw notFound();
|
|
142
119
|
return view;
|
|
143
120
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
/** The transaction of {@link write}. */
|
|
154
|
-
async writeIn(ctx, pkg, id, prefix) {
|
|
155
|
-
await this.keyShareOrg(ctx.orgId);
|
|
156
|
-
// NO KEY UPDATE, not UPDATE (here, in decide and in deleteAgentSkill): it still serializes
|
|
157
|
-
// the writes and decisions of one agent, but not the foreign key check (FOR KEY SHARE) of a
|
|
158
|
-
// transaction that already holds the conversation row this one locks last, e.g. an event
|
|
159
|
-
// log append updating that row a second time (the title job): FOR UPDATE made that 40P01
|
|
160
|
-
const agent = await this.db
|
|
161
|
-
.selectFrom("agents")
|
|
162
|
-
.select(["visibility", "deleted_at"])
|
|
163
|
-
.where("id", "=", ctx.agentId)
|
|
164
|
-
.where("org_id", "=", ctx.orgId)
|
|
165
|
-
.forNoKeyUpdate()
|
|
166
|
-
.executeTakeFirst();
|
|
167
|
-
if (!agent || agent.deleted_at !== null)
|
|
168
|
-
throw new ToolError(AGENT_GONE_TEXT);
|
|
169
|
-
// Then the conversation row (appendCatalog's lock, still after the agent row like decide),
|
|
170
|
-
// before the change request insert: its foreign key check takes FOR KEY SHARE on that row,
|
|
171
|
-
// and upgrading to FOR UPDATE afterwards deadlocked with another writer doing the same
|
|
172
|
-
await this.db
|
|
173
|
-
.selectFrom("conversations")
|
|
174
|
-
.select("id")
|
|
175
|
-
.where("id", "=", ctx.conversationId)
|
|
176
|
-
.forUpdate()
|
|
177
|
-
.execute();
|
|
178
|
-
const existing = await this.db
|
|
179
|
-
.selectFrom("skills")
|
|
180
|
-
.select(["id", "current_version"])
|
|
181
|
-
.where("org_id", "=", ctx.orgId)
|
|
182
|
-
.where("scope", "=", "agent")
|
|
183
|
-
.where("agent_id", "=", ctx.agentId)
|
|
184
|
-
.where("name", "=", pkg.name)
|
|
185
|
-
.where("deleted_at", "is", null)
|
|
186
|
-
.forUpdate()
|
|
187
|
-
.executeTakeFirst();
|
|
188
|
-
const pend = await this.db
|
|
189
|
-
.selectFrom("skill_change_requests")
|
|
190
|
-
.selectAll()
|
|
191
|
-
.where("agent_id", "=", ctx.agentId)
|
|
192
|
-
.where("status", "=", "pending")
|
|
193
|
-
.where(sql `proposed_version -> 'manifest' ->> 'name'`, "=", pkg.name)
|
|
194
|
-
.forUpdate()
|
|
195
|
-
.executeTakeFirst();
|
|
196
|
-
const drop = [];
|
|
197
|
-
if (pend) {
|
|
198
|
-
if (pend.requested_in_conversation !== ctx.conversationId) {
|
|
199
|
-
throw new ToolError(pendingElsewhereText(pkg.name));
|
|
121
|
+
/** A version draft's files as a package (as registered), or 503 when one cannot be read. */
|
|
122
|
+
async readDraft(orgId, req) {
|
|
123
|
+
const store = this.storage.kernel(orgId);
|
|
124
|
+
const m = req.proposed_version.manifest;
|
|
125
|
+
const files = [];
|
|
126
|
+
for (const f of m.files) {
|
|
127
|
+
const got = await store.get(`${req.object_prefix}${f.path}`);
|
|
128
|
+
if (!got) {
|
|
129
|
+
throw new ApiError("storage.unavailable", "The proposed version's files cannot be read; try again later");
|
|
200
130
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
.updateTable("skill_change_requests")
|
|
204
|
-
.set({ status: "cancelled", decided_at: sql `now()` })
|
|
205
|
-
.where("id", "=", pend.id)
|
|
206
|
-
.execute();
|
|
207
|
-
drop.push(pend.object_prefix);
|
|
208
|
-
await audit(this.db.db, {
|
|
209
|
-
actorAccountId: ctx.accountId,
|
|
210
|
-
orgId: ctx.orgId,
|
|
211
|
-
action: "skill.change_decided",
|
|
212
|
-
target: pend.id,
|
|
213
|
-
detail: {
|
|
214
|
-
decision: "cancelled",
|
|
215
|
-
reason: "superseded",
|
|
216
|
-
supersededBy: id,
|
|
217
|
-
agentId: ctx.agentId,
|
|
218
|
-
name: pkg.name,
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
if (!existing)
|
|
223
|
-
await this.assertBelowCap(ctx.agentId, pend?.id);
|
|
224
|
-
const version = (existing?.current_version ?? 0) + 1;
|
|
225
|
-
const skillId = existing?.id ?? pend?.proposed_version.skillId ?? randomUUID();
|
|
226
|
-
let outcome;
|
|
227
|
-
if (agent.visibility === "private") {
|
|
228
|
-
await this.createVersion({
|
|
229
|
-
orgId: ctx.orgId,
|
|
230
|
-
agentId: ctx.agentId,
|
|
231
|
-
skillId,
|
|
232
|
-
exists: existing !== undefined,
|
|
233
|
-
version,
|
|
234
|
-
manifest: pkg.manifest,
|
|
235
|
-
prefix,
|
|
236
|
-
createdBy: ctx.accountId,
|
|
237
|
-
});
|
|
238
|
-
await audit(this.db.db, {
|
|
239
|
-
actorAccountId: ctx.accountId,
|
|
240
|
-
orgId: ctx.orgId,
|
|
241
|
-
action: existing ? "skill.version_created" : "skill.created",
|
|
242
|
-
target: skillId,
|
|
243
|
-
detail: {
|
|
244
|
-
name: pkg.name,
|
|
245
|
-
version,
|
|
246
|
-
files: pkg.manifest.files.length,
|
|
247
|
-
agentId: ctx.agentId,
|
|
248
|
-
conversationId: ctx.conversationId,
|
|
249
|
-
via: BUILTIN.SkillWrite,
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
outcome = { kind: "version", skillId, version, created: !existing };
|
|
131
|
+
const data = new Uint8Array(await new Response(got.body).arrayBuffer());
|
|
132
|
+
files.push({ path: f.path, size: f.size, sha256: f.sha256, data });
|
|
253
133
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
org_id: ctx.orgId,
|
|
261
|
-
agent_id: ctx.agentId,
|
|
262
|
-
skill_id: existing?.id ?? null,
|
|
263
|
-
proposed_version: JSON.stringify(proposed),
|
|
264
|
-
object_prefix: prefix,
|
|
265
|
-
requested_by_account: ctx.accountId,
|
|
266
|
-
requested_in_conversation: ctx.conversationId,
|
|
267
|
-
decided_by: null,
|
|
268
|
-
})
|
|
269
|
-
.execute();
|
|
270
|
-
await audit(this.db.db, {
|
|
271
|
-
actorAccountId: ctx.accountId,
|
|
272
|
-
orgId: ctx.orgId,
|
|
273
|
-
action: "skill.change_requested",
|
|
274
|
-
target: id,
|
|
275
|
-
detail: {
|
|
276
|
-
agentId: ctx.agentId,
|
|
277
|
-
skillId: existing?.id ?? null,
|
|
278
|
-
name: pkg.name,
|
|
279
|
-
version,
|
|
280
|
-
files: pkg.manifest.files.length,
|
|
281
|
-
conversationId: ctx.conversationId,
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
outcome = { kind: "requested", requestId: id, skillId, version };
|
|
285
|
-
}
|
|
286
|
-
// 本对话立即看到新版本:在上一份快照上替换 / 加入这个技能(确定、来自日志;来源 = 本次的对象前缀)
|
|
287
|
-
// A pinned entry of this skill gets the version, source and body just written (phase B)
|
|
288
|
-
// A manual-only version (phase D) is never in the model's catalog: it removes the skill from
|
|
289
|
-
// the snapshot instead, and appendCatalog drops its pinned entry with it (no longer live)
|
|
290
|
-
const forModel = invocationOf(pkg.manifest).model;
|
|
291
|
-
const pinnedBody = writtenBody(pkg);
|
|
292
|
-
const toSeq = await this.appendCatalog(ctx.conversationId, (base) => ({
|
|
293
|
-
...base,
|
|
294
|
-
skills: [
|
|
295
|
-
...base.skills.filter((s) => s.name !== pkg.name),
|
|
296
|
-
...(forModel
|
|
297
|
-
? [
|
|
298
|
-
{
|
|
299
|
-
id: skillId,
|
|
300
|
-
name: pkg.name,
|
|
301
|
-
description: pkg.description,
|
|
302
|
-
version,
|
|
303
|
-
source: prefix,
|
|
304
|
-
},
|
|
305
|
-
]
|
|
306
|
-
: []),
|
|
307
|
-
],
|
|
308
|
-
...(base.pinned && {
|
|
309
|
-
pinned: base.pinned.map((p) => p.id === skillId && pinnedBody !== null
|
|
310
|
-
? { id: skillId, version, name: pkg.name, source: prefix, body: pinnedBody }
|
|
311
|
-
: p),
|
|
312
|
-
}),
|
|
313
|
-
}), `skills_catalog:write:${id}`, ctx.runId);
|
|
314
|
-
return { outcome, drop, toSeq };
|
|
134
|
+
return {
|
|
135
|
+
name: m.name,
|
|
136
|
+
description: m.description,
|
|
137
|
+
files,
|
|
138
|
+
...(m.invocation !== undefined && { invocation: m.invocation }),
|
|
139
|
+
};
|
|
315
140
|
}
|
|
316
|
-
/**
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
141
|
+
/**
|
|
142
|
+
* The transaction of {@link decide}. Lock order: org row (FOR NO KEY UPDATE, role re-checked) -> agent
|
|
143
|
+
* row -> the proposing conversation (a version's revert appends there) -> skill row -> request
|
|
144
|
+
* row, the order registration and sharing use.
|
|
145
|
+
*/
|
|
146
|
+
async decideIn(orgId, id, accountId, body, actorTokenId, head, pkg, owner) {
|
|
147
|
+
await this.orgLock.lockAndAuthorize(orgId, accountId, "skills.manage");
|
|
320
148
|
const agent = await this.db
|
|
321
149
|
.selectFrom("agents")
|
|
322
150
|
.select(["owner_account_id", "deleted_at"])
|
|
323
|
-
.where("id", "=",
|
|
151
|
+
.where("id", "=", head.agent_id)
|
|
324
152
|
.forNoKeyUpdate()
|
|
325
153
|
.executeTakeFirst();
|
|
326
|
-
if (
|
|
327
|
-
|
|
154
|
+
if (head.kind === "version" && head.requested_in_conversation !== null) {
|
|
155
|
+
await this.db
|
|
156
|
+
.selectFrom("conversations")
|
|
157
|
+
.select("id")
|
|
158
|
+
.where("id", "=", head.requested_in_conversation)
|
|
159
|
+
.forUpdate()
|
|
160
|
+
.executeTakeFirst();
|
|
161
|
+
}
|
|
162
|
+
const skill = head.skill_id === null
|
|
163
|
+
? undefined
|
|
164
|
+
: await this.db
|
|
165
|
+
.selectFrom("skills")
|
|
166
|
+
.select(["id", "scope", "agent_id", "name", "deleted_at", "current_version"])
|
|
167
|
+
.where("id", "=", head.skill_id)
|
|
168
|
+
.forUpdate()
|
|
169
|
+
.executeTakeFirst();
|
|
328
170
|
const req = await this.db
|
|
329
171
|
.selectFrom("skill_change_requests")
|
|
330
172
|
.selectAll()
|
|
331
173
|
.where("id", "=", id)
|
|
332
174
|
.forUpdate()
|
|
333
175
|
.executeTakeFirst();
|
|
334
|
-
//
|
|
176
|
+
// the row went between the first read and the lock (agent / org hard-deleted) -> 404, not 500
|
|
335
177
|
if (!req)
|
|
336
178
|
throw notFound();
|
|
337
|
-
if (agent.owner_account_id !== accountId) {
|
|
338
|
-
throw new ApiError("skill_change.not_owner", "Only the agent's owner can decide on its skill changes");
|
|
339
|
-
}
|
|
340
179
|
if (req.status !== "pending") {
|
|
341
180
|
throw new ApiError("skill_change.already_decided", "This skill change has already been decided");
|
|
342
181
|
}
|
|
182
|
+
const drafts = { prefixes: [], seqs: [] };
|
|
183
|
+
// A share applies only to the version the admin reviewed; a version proposal only to the skill
|
|
184
|
+
// of its name
|
|
185
|
+
const applies = agent !== undefined &&
|
|
186
|
+
agent.deleted_at === null &&
|
|
187
|
+
skill !== undefined &&
|
|
188
|
+
skill.deleted_at === null &&
|
|
189
|
+
(req.kind === "share"
|
|
190
|
+
? skill.scope === "agent" &&
|
|
191
|
+
skill.agent_id === req.agent_id &&
|
|
192
|
+
skill.current_version === req.proposed_version.version &&
|
|
193
|
+
agent.owner_account_id === req.requested_by_account
|
|
194
|
+
: skill.scope === "org" && skill.name === req.proposed_version.manifest.name);
|
|
195
|
+
if (!applies) {
|
|
196
|
+
await cancelRequestIn(this.catalogDeps, req, drafts, {
|
|
197
|
+
actorAccountId: accountId,
|
|
198
|
+
...(actorTokenId !== undefined && { actorTokenId }),
|
|
199
|
+
reason: "stale",
|
|
200
|
+
});
|
|
201
|
+
return { updated: await this.reread(id), drafts, usedOwner: false };
|
|
202
|
+
}
|
|
343
203
|
const p = req.proposed_version;
|
|
344
204
|
const approved = body.decision === "approve";
|
|
345
|
-
let skillId = req.skill_id;
|
|
346
205
|
let version = p.version;
|
|
347
|
-
if (approved) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
.
|
|
352
|
-
.where("scope", "=", "agent")
|
|
353
|
-
.where("agent_id", "=", req.agent_id)
|
|
354
|
-
.where("name", "=", p.manifest.name)
|
|
355
|
-
.where("deleted_at", "is", null)
|
|
356
|
-
.forUpdate()
|
|
357
|
-
.executeTakeFirst();
|
|
358
|
-
skillId = existing?.id ?? p.skillId;
|
|
359
|
-
version = (existing?.current_version ?? 0) + 1;
|
|
360
|
-
await this.createVersion({
|
|
361
|
-
orgId,
|
|
206
|
+
if (approved && req.kind === "share") {
|
|
207
|
+
await assertCustomAllowed(this.db.db, "customSkills");
|
|
208
|
+
await assertShareableName(this.db, orgId, skill.name);
|
|
209
|
+
await shareSkillIn(this.db, {
|
|
210
|
+
skillId: skill.id,
|
|
362
211
|
agentId: req.agent_id,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
212
|
+
maintainerAccountId: req.requested_by_account,
|
|
213
|
+
});
|
|
214
|
+
await audit(this.db.db, {
|
|
215
|
+
actorAccountId: accountId,
|
|
216
|
+
...(actorTokenId !== undefined && { actorTokenId }),
|
|
217
|
+
orgId,
|
|
218
|
+
action: "skill.shared",
|
|
219
|
+
target: skill.id,
|
|
220
|
+
detail: {
|
|
221
|
+
name: skill.name,
|
|
222
|
+
agentId: req.agent_id,
|
|
223
|
+
maintainer: req.requested_by_account,
|
|
224
|
+
requestId: id,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else if (approved) {
|
|
229
|
+
if (pkg === undefined)
|
|
230
|
+
throw new Error("an approved version needs its files");
|
|
231
|
+
await assertCustomAllowed(this.db.db, "customSkills");
|
|
232
|
+
// Joins this transaction without storage I/O: the version is the org skill's next one, the
|
|
233
|
+
// draft prefix its files; a skill the org promoted gets the platform version prepared before
|
|
234
|
+
const out = await this.skills.createVersionFromFiles(orgId, accountId, pkg, {
|
|
367
235
|
prefix: req.object_prefix,
|
|
368
|
-
|
|
236
|
+
stored: owner !== undefined ? { owner } : {},
|
|
237
|
+
...(actorTokenId !== undefined && { actorTokenId }),
|
|
238
|
+
auditDetail: {
|
|
239
|
+
requestId: id,
|
|
240
|
+
agentId: req.agent_id,
|
|
241
|
+
requestedBy: req.requested_by_account,
|
|
242
|
+
},
|
|
369
243
|
});
|
|
244
|
+
version = out.version;
|
|
245
|
+
const own = await this.db
|
|
246
|
+
.selectFrom("skill_versions")
|
|
247
|
+
.select("object_prefix")
|
|
248
|
+
.where("skill_id", "=", skill.id)
|
|
249
|
+
.where("version", "=", out.version)
|
|
250
|
+
.executeTakeFirst();
|
|
251
|
+
if (own?.object_prefix !== req.object_prefix) {
|
|
252
|
+
// Served from elsewhere (the platform row): the draft is not a version's files
|
|
253
|
+
const seq = await revertTrialCatalog(this.catalogDeps, req);
|
|
254
|
+
if (seq !== null && req.requested_in_conversation !== null) {
|
|
255
|
+
drafts.seqs.push([req.requested_in_conversation, seq]);
|
|
256
|
+
}
|
|
257
|
+
drafts.prefixes.push(req.object_prefix);
|
|
258
|
+
}
|
|
370
259
|
}
|
|
371
260
|
const updated = await this.db
|
|
372
261
|
.updateTable("skill_change_requests")
|
|
@@ -374,7 +263,6 @@ let SkillChangesService = class SkillChangesService {
|
|
|
374
263
|
status: approved ? "approved" : "rejected",
|
|
375
264
|
decided_by: accountId,
|
|
376
265
|
decided_at: sql `now()`,
|
|
377
|
-
...(approved && { skill_id: skillId }),
|
|
378
266
|
})
|
|
379
267
|
.where("id", "=", id)
|
|
380
268
|
.returningAll()
|
|
@@ -387,136 +275,83 @@ let SkillChangesService = class SkillChangesService {
|
|
|
387
275
|
target: id,
|
|
388
276
|
detail: {
|
|
389
277
|
decision: approved ? "approved" : "rejected",
|
|
278
|
+
kind: req.kind,
|
|
390
279
|
agentId: req.agent_id,
|
|
391
|
-
skillId,
|
|
280
|
+
skillId: skill.id,
|
|
392
281
|
name: p.manifest.name,
|
|
393
|
-
version
|
|
282
|
+
version,
|
|
394
283
|
requestedBy: req.requested_by_account,
|
|
395
|
-
|
|
284
|
+
...(req.requested_in_conversation !== null && {
|
|
285
|
+
conversationId: req.requested_in_conversation,
|
|
286
|
+
}),
|
|
396
287
|
...(body.note !== undefined && { note: body.note }),
|
|
397
288
|
},
|
|
398
289
|
});
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.selectFrom("agents")
|
|
408
|
-
.select(["owner_account_id", "deleted_at"])
|
|
409
|
-
.where("id", "=", agentId)
|
|
410
|
-
.where("org_id", "=", orgId)
|
|
411
|
-
.forNoKeyUpdate()
|
|
412
|
-
.executeTakeFirst();
|
|
413
|
-
const agentNotFound = () => new ApiError("agent.not_found", "Agent not found");
|
|
414
|
-
if (!agent || agent.deleted_at !== null)
|
|
415
|
-
throw agentNotFound();
|
|
416
|
-
if (agent.owner_account_id !== accountId) {
|
|
417
|
-
// 与 AgentsService 一致:看不到的 agent → 404,看得到但不是负责人 → 403
|
|
418
|
-
const visible = await this.db
|
|
419
|
-
.selectFrom("agents")
|
|
420
|
-
.select("id")
|
|
421
|
-
.where("id", "=", agentId)
|
|
422
|
-
.where(visibleTo(accountId))
|
|
423
|
-
.executeTakeFirst();
|
|
424
|
-
if (!visible)
|
|
425
|
-
throw agentNotFound();
|
|
426
|
-
throw new ApiError("forbidden", "Only the agent's owner can delete its skills");
|
|
290
|
+
// A rejected version: the proposing conversation goes back to the current version at once
|
|
291
|
+
// and the draft is deleted; a rejected share changes nothing
|
|
292
|
+
if (!approved && req.kind === "version") {
|
|
293
|
+
const seq = await revertTrialCatalog(this.catalogDeps, req);
|
|
294
|
+
if (seq !== null && req.requested_in_conversation !== null) {
|
|
295
|
+
drafts.seqs.push([req.requested_in_conversation, seq]);
|
|
296
|
+
}
|
|
297
|
+
drafts.prefixes.push(req.object_prefix);
|
|
427
298
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
.returning(["name"])
|
|
437
|
-
.executeTakeFirst();
|
|
438
|
-
if (!r)
|
|
439
|
-
throw new ApiError("skill.not_found", "Skill not found");
|
|
440
|
-
const out = { prefixes: [], seqs: [] };
|
|
441
|
-
const pend = await this.db
|
|
299
|
+
return {
|
|
300
|
+
updated,
|
|
301
|
+
drafts,
|
|
302
|
+
usedOwner: approved && req.kind === "version" && owner !== undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
reread(id) {
|
|
306
|
+
return this.db
|
|
442
307
|
.selectFrom("skill_change_requests")
|
|
443
308
|
.selectAll()
|
|
444
|
-
.where("
|
|
445
|
-
.
|
|
446
|
-
.where(sql `proposed_version -> 'manifest' ->> 'name'`, "=", r.name)
|
|
447
|
-
.forUpdate()
|
|
448
|
-
.executeTakeFirst();
|
|
449
|
-
if (pend) {
|
|
450
|
-
await this.db
|
|
451
|
-
.updateTable("skill_change_requests")
|
|
452
|
-
.set({ status: "cancelled", decided_at: sql `now()` })
|
|
453
|
-
.where("id", "=", pend.id)
|
|
454
|
-
.execute();
|
|
455
|
-
await audit(this.db.db, {
|
|
456
|
-
actorAccountId: accountId,
|
|
457
|
-
...(actorTokenId !== undefined && { actorTokenId }),
|
|
458
|
-
orgId,
|
|
459
|
-
action: "skill.change_decided",
|
|
460
|
-
target: pend.id,
|
|
461
|
-
detail: { decision: "cancelled", reason: "skill_deleted", agentId, name: r.name },
|
|
462
|
-
});
|
|
463
|
-
const seq = await this.revertTrial(pend);
|
|
464
|
-
if (seq !== null)
|
|
465
|
-
out.seqs.push([pend.requested_in_conversation, seq]);
|
|
466
|
-
out.prefixes.push(pend.object_prefix);
|
|
467
|
-
}
|
|
468
|
-
await audit(this.db.db, {
|
|
469
|
-
actorAccountId: accountId,
|
|
470
|
-
...(actorTokenId !== undefined && { actorTokenId }),
|
|
471
|
-
orgId,
|
|
472
|
-
action: "skill.deleted",
|
|
473
|
-
target: skillId,
|
|
474
|
-
detail: { name: r.name, agentId, ...auditDetail },
|
|
475
|
-
});
|
|
476
|
-
return out;
|
|
309
|
+
.where("id", "=", id)
|
|
310
|
+
.executeTakeFirstOrThrow();
|
|
477
311
|
}
|
|
478
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* The org's pending requests when the account is an org admin (read live; anyone else gets
|
|
314
|
+
* none), newest first, at most {@link SKILL_CHANGES_LIST_MAX}.
|
|
315
|
+
*/
|
|
479
316
|
async listPending(orgId, accountId) {
|
|
317
|
+
if (!(await isOrgAdmin(this.db.db, orgId, accountId)))
|
|
318
|
+
return [];
|
|
480
319
|
const rows = await this.db
|
|
481
320
|
.selectFrom("skill_change_requests")
|
|
482
|
-
.
|
|
483
|
-
.
|
|
484
|
-
.where("
|
|
485
|
-
.
|
|
486
|
-
.where("agents.owner_account_id", "=", accountId)
|
|
487
|
-
.where("agents.deleted_at", "is", null)
|
|
488
|
-
.orderBy("skill_change_requests.created_at", "desc")
|
|
321
|
+
.selectAll()
|
|
322
|
+
.where("org_id", "=", orgId)
|
|
323
|
+
.where("status", "=", "pending")
|
|
324
|
+
.orderBy("created_at", "desc")
|
|
489
325
|
.limit(SKILL_CHANGES_LIST_MAX)
|
|
490
326
|
.execute();
|
|
491
327
|
return this.views(rows);
|
|
492
328
|
}
|
|
493
329
|
/**
|
|
494
|
-
*
|
|
495
|
-
*
|
|
330
|
+
* The detail (with file texts): only org admins and the requester see it, anyone else 404 (the
|
|
331
|
+
* request's existence is not disclosed). A version draft's contents are null once it is deleted
|
|
332
|
+
* (rejected / cancelled); a share's files are the skill version's own and stay readable.
|
|
496
333
|
*/
|
|
497
334
|
async get(orgId, id, accountId) {
|
|
498
335
|
const row = await this.db
|
|
499
336
|
.selectFrom("skill_change_requests")
|
|
500
|
-
.
|
|
501
|
-
.
|
|
502
|
-
.
|
|
503
|
-
.where("skill_change_requests.id", "=", id)
|
|
504
|
-
.where("skill_change_requests.org_id", "=", orgId)
|
|
505
|
-
.where("agents.deleted_at", "is", null)
|
|
337
|
+
.selectAll()
|
|
338
|
+
.where("id", "=", id)
|
|
339
|
+
.where("org_id", "=", orgId)
|
|
506
340
|
.executeTakeFirst();
|
|
507
341
|
if (!row)
|
|
508
342
|
throw notFound();
|
|
509
|
-
if (row.
|
|
343
|
+
if (row.requested_by_account !== accountId &&
|
|
344
|
+
!(await isOrgAdmin(this.db.db, orgId, accountId))) {
|
|
510
345
|
throw notFound();
|
|
511
346
|
}
|
|
512
347
|
const [view] = await this.views([row]);
|
|
513
348
|
if (!view)
|
|
514
349
|
throw notFound();
|
|
515
|
-
const live = row.status === "pending" || row.status === "approved";
|
|
350
|
+
const live = row.kind === "share" || row.status === "pending" || row.status === "approved";
|
|
516
351
|
const store = this.storage.kernel(orgId);
|
|
517
352
|
const budget = { left: DETAIL_MAX_BYTES };
|
|
518
353
|
const files = await readTexts(live ? store : undefined, row.object_prefix, row.proposed_version.manifest.files, budget);
|
|
519
|
-
//
|
|
354
|
+
// A change of an existing skill: the version in effect, for the difference
|
|
520
355
|
let current = null;
|
|
521
356
|
if (row.skill_id !== null) {
|
|
522
357
|
const cur = await this.db
|
|
@@ -532,7 +367,7 @@ let SkillChangesService = class SkillChangesService {
|
|
|
532
367
|
.where("skills.id", "=", row.skill_id)
|
|
533
368
|
.where("skills.deleted_at", "is", null)
|
|
534
369
|
.executeTakeFirst();
|
|
535
|
-
//
|
|
370
|
+
// the request is the current version itself (a share, an approved version): no "before"
|
|
536
371
|
if (cur && cur.object_prefix !== row.object_prefix) {
|
|
537
372
|
current = {
|
|
538
373
|
version: cur.version,
|
|
@@ -543,209 +378,42 @@ let SkillChangesService = class SkillChangesService {
|
|
|
543
378
|
return { ...view, files, current };
|
|
544
379
|
}
|
|
545
380
|
/**
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*/
|
|
553
|
-
async appendCatalog(conversationId, derive, idemKey, runId) {
|
|
554
|
-
const base = (await lockLatestCatalog(this.db.db, conversationId)) ?? { skills: [] };
|
|
555
|
-
const derived = derive(base);
|
|
556
|
-
if (derived === null)
|
|
557
|
-
return null;
|
|
558
|
-
const skills = [...derived.skills].sort(byName);
|
|
559
|
-
const live = (p) => skills.some((s) => s.id === p.id && s.version === p.version && s.source === p.source);
|
|
560
|
-
const pinned = fitPinned((derived.pinned ?? []).filter(live), this.log, {
|
|
561
|
-
conversationId,
|
|
562
|
-
});
|
|
563
|
-
const next = {
|
|
564
|
-
skills,
|
|
565
|
-
...(pinned.length > 0 && { pinned }),
|
|
566
|
-
...(derived.toolMap !== undefined && { toolMap: derived.toolMap }),
|
|
567
|
-
};
|
|
568
|
-
if (sameSnapshot(base, next))
|
|
569
|
-
return null;
|
|
570
|
-
const written = await this.eventLog.appendIn(conversationId, [
|
|
571
|
-
{
|
|
572
|
-
type: "skills_catalog",
|
|
573
|
-
...(runId !== undefined && { runId }),
|
|
574
|
-
idemKey,
|
|
575
|
-
payload: next,
|
|
576
|
-
},
|
|
577
|
-
]);
|
|
578
|
-
return Math.max(...written.map((e) => e.seq));
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* 草稿不再生效(拒绝 / 取消)时,在事务 `t` 里给发起对话追加一份回退快照:由该对话最新一份快照推出——
|
|
582
|
-
* 来源是这份草稿的条目换成这个 agent 技能当前的正式版本(`t` 里已是决定之后的状态),没有正式版本就移除。
|
|
583
|
-
* 快照里已没有这份草稿 → 不写。对话正在运行时,提醒落在当前位置(构建时排在工具结果之后),下一步即生效。
|
|
584
|
-
* 移除时不推测同名的组织技能(快照里已被替换掉),下一次运行开始的解析会把它补回来。
|
|
381
|
+
* In the caller's transaction (`mandatory`: `AgentsService` holds the org and agent row locks),
|
|
382
|
+
* cancels the agent's pending requests of the given kinds (default both): marks them
|
|
383
|
+
* cancelled, audits, and for version drafts appends a reverting snapshot to each proposing
|
|
384
|
+
* conversation. The agent's delete cancels both kinds; an ownership transfer only shares (the
|
|
385
|
+
* new owner did not ask). After the commit the caller runs {@link finishCancel} to delete the
|
|
386
|
+
* drafts and publish the events.
|
|
585
387
|
*/
|
|
586
|
-
async
|
|
587
|
-
const name = req.proposed_version.manifest.name;
|
|
588
|
-
const real = await this.db
|
|
589
|
-
.selectFrom("skills")
|
|
590
|
-
.innerJoin("skill_versions", (j) => j
|
|
591
|
-
.onRef("skill_versions.skill_id", "=", "skills.id")
|
|
592
|
-
.onRef("skill_versions.version", "=", "skills.current_version"))
|
|
593
|
-
.select([
|
|
594
|
-
"skills.id",
|
|
595
|
-
"skills.current_version",
|
|
596
|
-
"skill_versions.manifest",
|
|
597
|
-
"skill_versions.object_prefix",
|
|
598
|
-
])
|
|
599
|
-
.where("skills.org_id", "=", req.org_id)
|
|
600
|
-
.where("skills.scope", "=", "agent")
|
|
601
|
-
.where("skills.agent_id", "=", req.agent_id)
|
|
602
|
-
.where("skills.name", "=", name)
|
|
603
|
-
.where("skills.deleted_at", "is", null)
|
|
604
|
-
.executeTakeFirst();
|
|
605
|
-
return this.appendCatalog(req.requested_in_conversation, (base) => {
|
|
606
|
-
if (!base.skills.some((s) => s.source === req.object_prefix))
|
|
607
|
-
return null;
|
|
608
|
-
const rest = base.skills.filter((s) => s.source !== req.object_prefix);
|
|
609
|
-
// The draft's pinned entry goes (Decision 8): the next run start pins the real version
|
|
610
|
-
return {
|
|
611
|
-
...base,
|
|
612
|
-
// A manual-only current version (phase D) stays out of the model's catalog
|
|
613
|
-
skills: real && invocationOf(real.manifest).model
|
|
614
|
-
? [
|
|
615
|
-
...rest,
|
|
616
|
-
{
|
|
617
|
-
id: real.id,
|
|
618
|
-
name,
|
|
619
|
-
description: real.manifest.description,
|
|
620
|
-
version: real.current_version,
|
|
621
|
-
source: real.object_prefix,
|
|
622
|
-
},
|
|
623
|
-
]
|
|
624
|
-
: rest,
|
|
625
|
-
...(base.pinned && {
|
|
626
|
-
pinned: base.pinned.filter((p) => p.source !== req.object_prefix),
|
|
627
|
-
}),
|
|
628
|
-
};
|
|
629
|
-
}, `skills_catalog:revert:${req.id}`);
|
|
630
|
-
}
|
|
631
|
-
/**
|
|
632
|
-
* In the caller's transaction (`mandatory`: `AgentsService.softDelete` holds the org and agent
|
|
633
|
-
* row locks), cancels every pending skill change of the agent: marks them cancelled, audits, and
|
|
634
|
-
* appends a reverting snapshot to each requesting conversation. After the commit the caller runs
|
|
635
|
-
* {@link finishCancel} to delete the drafts and publish the events.
|
|
636
|
-
*/
|
|
637
|
-
async cancelPendingForAgent(orgId, agentId, actorAccountId, reason, actorTokenId) {
|
|
388
|
+
async cancelPendingForAgent(orgId, agentId, actorAccountId, reason, actorTokenId, kinds = ["share", "version"]) {
|
|
638
389
|
const rows = await this.db
|
|
639
390
|
.selectFrom("skill_change_requests")
|
|
640
391
|
.selectAll()
|
|
641
392
|
.where("org_id", "=", orgId)
|
|
642
393
|
.where("agent_id", "=", agentId)
|
|
643
394
|
.where("status", "=", "pending")
|
|
395
|
+
.where("kind", "in", [...kinds])
|
|
644
396
|
.orderBy("created_at")
|
|
645
397
|
.forUpdate()
|
|
646
398
|
.execute();
|
|
647
399
|
const out = { prefixes: [], seqs: [] };
|
|
648
400
|
for (const r of rows) {
|
|
649
|
-
await this.
|
|
650
|
-
.updateTable("skill_change_requests")
|
|
651
|
-
.set({ status: "cancelled", decided_at: sql `now()` })
|
|
652
|
-
.where("id", "=", r.id)
|
|
653
|
-
.execute();
|
|
654
|
-
await audit(this.db.db, {
|
|
401
|
+
await cancelRequestIn(this.catalogDeps, r, out, {
|
|
655
402
|
actorAccountId,
|
|
656
403
|
...(actorTokenId !== undefined && { actorTokenId }),
|
|
657
|
-
|
|
658
|
-
action: "skill.change_decided",
|
|
659
|
-
target: r.id,
|
|
660
|
-
detail: { decision: "cancelled", reason, agentId, name: r.proposed_version.manifest.name },
|
|
404
|
+
reason,
|
|
661
405
|
});
|
|
662
|
-
const seq = await this.revertTrial(r);
|
|
663
|
-
if (seq !== null)
|
|
664
|
-
out.seqs.push([r.requested_in_conversation, seq]);
|
|
665
|
-
out.prefixes.push(r.object_prefix);
|
|
666
406
|
}
|
|
667
407
|
return out;
|
|
668
408
|
}
|
|
669
|
-
/** {@link cancelPendingForAgent}
|
|
409
|
+
/** After {@link cancelPendingForAgent} commits: publishes the events, deletes the drafts. */
|
|
670
410
|
async finishCancel(orgId, c) {
|
|
671
411
|
for (const [conv, seq] of c.seqs)
|
|
672
412
|
await this.eventLog.notify(conv, seq);
|
|
673
413
|
for (const p of c.prefixes)
|
|
674
414
|
await this.deletePrefix(orgId, p);
|
|
675
415
|
}
|
|
676
|
-
/**
|
|
677
|
-
async assertBelowCap(agentId, excluding) {
|
|
678
|
-
const skills = await this.db
|
|
679
|
-
.selectFrom("skills")
|
|
680
|
-
.select((eb) => eb.fn.countAll().as("n"))
|
|
681
|
-
.where("scope", "=", "agent")
|
|
682
|
-
.where("agent_id", "=", agentId)
|
|
683
|
-
.where("deleted_at", "is", null)
|
|
684
|
-
.executeTakeFirstOrThrow();
|
|
685
|
-
let q = this.db
|
|
686
|
-
.selectFrom("skill_change_requests")
|
|
687
|
-
.select((eb) => eb.fn.countAll().as("n"))
|
|
688
|
-
.where("agent_id", "=", agentId)
|
|
689
|
-
.where("status", "=", "pending")
|
|
690
|
-
.where("skill_id", "is", null);
|
|
691
|
-
if (excluding !== undefined)
|
|
692
|
-
q = q.where("id", "!=", excluding);
|
|
693
|
-
const drafts = await q.executeTakeFirstOrThrow();
|
|
694
|
-
if (Number(skills.n) + Number(drafts.n) >= AGENT_SKILLS_MAX)
|
|
695
|
-
throw new ToolError(capText());
|
|
696
|
-
}
|
|
697
|
-
/** 写一个版本并让 `current_version` 指向它(技能不存在则新建)。 */
|
|
698
|
-
async createVersion(v) {
|
|
699
|
-
if (v.exists) {
|
|
700
|
-
await this.db
|
|
701
|
-
.updateTable("skills")
|
|
702
|
-
.set({
|
|
703
|
-
current_version: v.version,
|
|
704
|
-
description: v.manifest.description,
|
|
705
|
-
updated_at: sql `now()`,
|
|
706
|
-
})
|
|
707
|
-
.where("id", "=", v.skillId)
|
|
708
|
-
.execute();
|
|
709
|
-
}
|
|
710
|
-
else {
|
|
711
|
-
await this.db
|
|
712
|
-
.insertInto("skills")
|
|
713
|
-
.values({
|
|
714
|
-
id: v.skillId,
|
|
715
|
-
org_id: v.orgId,
|
|
716
|
-
scope: "agent",
|
|
717
|
-
agent_id: v.agentId,
|
|
718
|
-
name: v.manifest.name,
|
|
719
|
-
description: v.manifest.description,
|
|
720
|
-
current_version: v.version,
|
|
721
|
-
created_by: v.createdBy,
|
|
722
|
-
})
|
|
723
|
-
.execute();
|
|
724
|
-
}
|
|
725
|
-
await this.db
|
|
726
|
-
.insertInto("skill_versions")
|
|
727
|
-
.values({
|
|
728
|
-
skill_id: v.skillId,
|
|
729
|
-
version: v.version,
|
|
730
|
-
manifest: JSON.stringify(v.manifest),
|
|
731
|
-
object_prefix: v.prefix,
|
|
732
|
-
created_by: v.createdBy,
|
|
733
|
-
})
|
|
734
|
-
.execute();
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* 先给组织行加 KEY SHARE 锁,再锁 agent 行:与 `AgentsService`(组织行 FOR UPDATE → agent 行)同一顺序。
|
|
738
|
-
* 不先锁的话,本事务持有 agent 行后插入引用组织的行(外键取 KEY SHARE)会与之互相等待(40P01)。
|
|
739
|
-
*/
|
|
740
|
-
async keyShareOrg(orgId) {
|
|
741
|
-
await this.db
|
|
742
|
-
.selectFrom("orgs")
|
|
743
|
-
.select("id")
|
|
744
|
-
.where("id", "=", orgId)
|
|
745
|
-
.forKeyShare()
|
|
746
|
-
.executeTakeFirst();
|
|
747
|
-
}
|
|
748
|
-
/** 尽力删除草稿对象(失败只记日志:孤儿对象随组织数据清理删除)。 */
|
|
416
|
+
/** Deletes draft objects best effort (a failure is only logged: org data cleanup removes them). */
|
|
749
417
|
async deletePrefix(orgId, prefix) {
|
|
750
418
|
try {
|
|
751
419
|
await this.storage.kernel(orgId).deletePrefix(prefix);
|
|
@@ -754,7 +422,7 @@ let SkillChangesService = class SkillChangesService {
|
|
|
754
422
|
this.log.warn("skill draft cleanup failed", { orgId, prefix, err });
|
|
755
423
|
}
|
|
756
424
|
}
|
|
757
|
-
/**
|
|
425
|
+
/** Adds the agent names and requester names. */
|
|
758
426
|
async views(rows) {
|
|
759
427
|
if (rows.length === 0)
|
|
760
428
|
return [];
|
|
@@ -780,6 +448,7 @@ let SkillChangesService = class SkillChangesService {
|
|
|
780
448
|
return rows.map((r) => ({
|
|
781
449
|
id: r.id,
|
|
782
450
|
orgId: r.org_id,
|
|
451
|
+
kind: r.kind,
|
|
783
452
|
agent: { id: r.agent_id, name: names.get(r.agent_id) ?? "" },
|
|
784
453
|
skillId: r.skill_id,
|
|
785
454
|
name: r.proposed_version.manifest.name,
|
|
@@ -790,22 +459,16 @@ let SkillChangesService = class SkillChangesService {
|
|
|
790
459
|
requestedBy: r.requested_by_account,
|
|
791
460
|
requestedByName: requesterNames.get(r.requested_by_account) ?? "",
|
|
792
461
|
requestedInConversation: r.requested_in_conversation,
|
|
793
|
-
conversationPath: `/chat/${r.requested_in_conversation}`,
|
|
462
|
+
conversationPath: r.requested_in_conversation === null ? null : `/chat/${r.requested_in_conversation}`,
|
|
794
463
|
decidedBy: r.decided_by,
|
|
795
464
|
decidedAt: r.decided_at === null ? null : iso(r.decided_at),
|
|
796
465
|
createdAt: iso(r.created_at),
|
|
797
466
|
}));
|
|
798
467
|
}
|
|
799
468
|
};
|
|
800
|
-
__decorate([
|
|
801
|
-
Transactional()
|
|
802
|
-
], SkillChangesService.prototype, "writeIn", null);
|
|
803
469
|
__decorate([
|
|
804
470
|
Transactional()
|
|
805
471
|
], SkillChangesService.prototype, "decideIn", null);
|
|
806
|
-
__decorate([
|
|
807
|
-
Transactional()
|
|
808
|
-
], SkillChangesService.prototype, "deleteAgentSkillIn", null);
|
|
809
472
|
__decorate([
|
|
810
473
|
Transactional({ propagation: "mandatory" })
|
|
811
474
|
], SkillChangesService.prototype, "cancelPendingForAgent", null);
|
|
@@ -815,12 +478,16 @@ SkillChangesService = __decorate([
|
|
|
815
478
|
__param(1, Inject(StorageService)),
|
|
816
479
|
__param(2, Inject(EventLog)),
|
|
817
480
|
__param(3, Inject(KERNEL_LOGGER)),
|
|
818
|
-
__param(4, Inject(TransactionHost))
|
|
481
|
+
__param(4, Inject(TransactionHost)),
|
|
482
|
+
__param(5, Inject(OrgLock)),
|
|
483
|
+
__param(6, Inject(SkillsService))
|
|
819
484
|
], SkillChangesService);
|
|
820
485
|
export { SkillChangesService };
|
|
821
486
|
/**
|
|
822
|
-
*
|
|
823
|
-
*
|
|
487
|
+
* Reads the files' text in manifest order, sharing the read budget `budget`. A file's content is
|
|
488
|
+
* null past the budget, when its object is missing, on a storage error, and for a binary file
|
|
489
|
+
* (not valid UTF-8, or holding a NUL byte), so the inbox never shows garbage. `store` undefined
|
|
490
|
+
* means nothing is read (e.g. the draft is deleted).
|
|
824
491
|
*/
|
|
825
492
|
async function readTexts(store, prefix, manifest, budget) {
|
|
826
493
|
const out = [];
|
|
@@ -830,8 +497,10 @@ async function readTexts(store, prefix, manifest, budget) {
|
|
|
830
497
|
try {
|
|
831
498
|
const got = await store.get(`${prefix}${f.path}`);
|
|
832
499
|
if (got) {
|
|
833
|
-
|
|
500
|
+
const bytes = await new Response(got.body).arrayBuffer();
|
|
834
501
|
budget.left -= f.size;
|
|
502
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
503
|
+
content = text.includes("\u0000") ? null : text;
|
|
835
504
|
}
|
|
836
505
|
}
|
|
837
506
|
catch {
|
|
@@ -842,22 +511,3 @@ async function readTexts(store, prefix, manifest, budget) {
|
|
|
842
511
|
}
|
|
843
512
|
return out;
|
|
844
513
|
}
|
|
845
|
-
/** 并发写入全部文件。 */
|
|
846
|
-
async function putAll(store, prefix, files) {
|
|
847
|
-
const queue = [...files];
|
|
848
|
-
await Promise.all(Array.from({ length: Math.min(PUT_CONCURRENCY, queue.length) }, async () => {
|
|
849
|
-
for (let f = queue.shift(); f; f = queue.shift()) {
|
|
850
|
-
await store.put(`${prefix}${f.path}`, f.data, {
|
|
851
|
-
contentType: "application/octet-stream",
|
|
852
|
-
size: f.size,
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
}));
|
|
856
|
-
}
|
|
857
|
-
/** 存储错误 → 给模型的说明(存储不可用可重试);其余原样抛出。 */
|
|
858
|
-
function storageError(e) {
|
|
859
|
-
if (e instanceof ApiError && e.code === "storage.unavailable") {
|
|
860
|
-
return new ToolError(STORAGE_UNAVAILABLE_TEXT);
|
|
861
|
-
}
|
|
862
|
-
return e;
|
|
863
|
-
}
|