@rubytech/taskmaster 1.20.2 → 1.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/skills/agent-scope.js +14 -0
- package/dist/agents/tools/skill-pack-install-tool.js +54 -36
- package/dist/agents/tools/skill-pack-sign-tool.js +139 -37
- package/dist/agents/tools/system-status-tool.js +1 -1
- package/dist/build-info.json +3 -3
- package/dist/config/agent-tools-reconcile.js +66 -0
- package/dist/control-ui/assets/{index-DbeiXb7c.js → index-fP8Y5Vwq.js} +163 -158
- package/dist/control-ui/assets/index-fP8Y5Vwq.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/server.impl.js +15 -1
- package/dist/license/skill-pack.js +63 -26
- package/package.json +1 -1
- package/skills/skill-pack-distribution/SKILL.md +10 -5
- package/dist/control-ui/assets/index-DbeiXb7c.js.map +0 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PRELOADED_SKILL_AGENTS } from "./types.js";
|
|
2
|
+
const DEFAULT_AGENTS = ["admin", "public"];
|
|
3
|
+
/**
|
|
4
|
+
* Resolve which agents can access a skill.
|
|
5
|
+
* Preloaded skills use the hardcoded map (not overridable).
|
|
6
|
+
* User/licensed skills use config, defaulting to all agents.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveSkillAgents(skillName, isPreloaded, config) {
|
|
9
|
+
if (isPreloaded) {
|
|
10
|
+
return PRELOADED_SKILL_AGENTS[skillName] ?? DEFAULT_AGENTS;
|
|
11
|
+
}
|
|
12
|
+
const entry = config?.skills?.entries?.[skillName];
|
|
13
|
+
return entry?.agents ?? DEFAULT_AGENTS;
|
|
14
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import fsp from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import { jsonResult, readStringParam } from "./common.js";
|
|
5
5
|
import { verifySkillPack, hasMarker, readMarker, writeMarker, } from "../../license/skill-pack.js";
|
|
@@ -16,6 +16,7 @@ export function createSkillPackInstallTool(opts) {
|
|
|
16
16
|
name: "skill_pack_install",
|
|
17
17
|
description: "Install a signed skill pack (.skillpack.json) onto this device. " +
|
|
18
18
|
"Verifies the cryptographic signature and device binding before installing. " +
|
|
19
|
+
"Supports single skills and bundles (multiple skills in one pack). " +
|
|
19
20
|
"Use this when a customer forwards a skill pack file to you.",
|
|
20
21
|
parameters: SkillPackInstallSchema,
|
|
21
22
|
execute: async (_toolCallId, args) => {
|
|
@@ -43,48 +44,65 @@ export function createSkillPackInstallTool(opts) {
|
|
|
43
44
|
throw new Error("This skill pack is licensed for a different device. " +
|
|
44
45
|
"It cannot be installed on this device.");
|
|
45
46
|
}
|
|
46
|
-
// Check
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
// Check if all skills in this pack version are already installed
|
|
48
|
+
const allAlreadyInstalled = await allSkillsInstalled(opts.workspaceDir, payload.content.skills.map((s) => s.id), payload.pack.id, payload.pack.version);
|
|
49
|
+
if (allAlreadyInstalled) {
|
|
50
|
+
return jsonResult({
|
|
51
|
+
ok: true,
|
|
52
|
+
alreadyInstalled: true,
|
|
53
|
+
packId: payload.pack.id,
|
|
54
|
+
version: payload.pack.version,
|
|
55
|
+
message: `${payload.pack.name} v${payload.pack.version} is already installed.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Install each skill via skills.create gateway RPC
|
|
59
|
+
const installed = [];
|
|
60
|
+
for (const entry of payload.content.skills) {
|
|
61
|
+
const references = entry.references.map((r) => ({
|
|
62
|
+
name: r.name,
|
|
63
|
+
content: r.content,
|
|
64
|
+
}));
|
|
65
|
+
await callGatewayTool("skills.create", {}, {
|
|
66
|
+
name: entry.id,
|
|
67
|
+
skillContent: entry.skill,
|
|
68
|
+
...(references.length > 0 ? { references } : {}),
|
|
69
|
+
});
|
|
70
|
+
// Write marker file for each skill
|
|
71
|
+
const skillDir = path.join(opts.workspaceDir, "skills", entry.id);
|
|
72
|
+
const marker = {
|
|
73
|
+
packId: payload.pack.id,
|
|
74
|
+
version: payload.pack.version,
|
|
75
|
+
author: payload.pack.author,
|
|
76
|
+
installedAt: new Date().toISOString(),
|
|
77
|
+
deviceId: payload.device.did,
|
|
78
|
+
customerId: payload.device.cid,
|
|
79
|
+
};
|
|
80
|
+
await writeMarker(skillDir, marker);
|
|
81
|
+
installed.push(entry.id);
|
|
59
82
|
}
|
|
60
|
-
// Install via skills.create gateway RPC
|
|
61
|
-
const references = payload.content.references.map((r) => ({
|
|
62
|
-
name: r.name,
|
|
63
|
-
content: r.content,
|
|
64
|
-
}));
|
|
65
|
-
await callGatewayTool("skills.create", {}, {
|
|
66
|
-
name: payload.pack.id,
|
|
67
|
-
skillContent: payload.content.skill,
|
|
68
|
-
...(references.length > 0 ? { references } : {}),
|
|
69
|
-
});
|
|
70
|
-
// Write marker file
|
|
71
|
-
const marker = {
|
|
72
|
-
packId: payload.pack.id,
|
|
73
|
-
version: payload.pack.version,
|
|
74
|
-
author: payload.pack.author,
|
|
75
|
-
installedAt: new Date().toISOString(),
|
|
76
|
-
deviceId: payload.device.did,
|
|
77
|
-
customerId: payload.device.cid,
|
|
78
|
-
};
|
|
79
|
-
await writeMarker(skillDir, marker);
|
|
80
83
|
return jsonResult({
|
|
81
84
|
ok: true,
|
|
82
85
|
packId: payload.pack.id,
|
|
83
86
|
version: payload.pack.version,
|
|
84
87
|
name: payload.pack.name,
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
skillCount: installed.length,
|
|
89
|
+
skills: installed,
|
|
90
|
+
message: `Successfully installed ${payload.pack.name} v${payload.pack.version} ` +
|
|
91
|
+
`with ${installed.length} skill(s): ${installed.join(", ")}. ` +
|
|
92
|
+
`${installed.length === 1 ? "This skill is" : "These skills are"} now available for use.`,
|
|
87
93
|
});
|
|
88
94
|
},
|
|
89
95
|
};
|
|
90
96
|
}
|
|
97
|
+
/** Check whether all skills from a pack are already installed at the given version. */
|
|
98
|
+
async function allSkillsInstalled(workspaceDir, skillIds, packId, version) {
|
|
99
|
+
for (const id of skillIds) {
|
|
100
|
+
const skillDir = path.join(workspaceDir, "skills", id);
|
|
101
|
+
if (!hasMarker(skillDir))
|
|
102
|
+
return false;
|
|
103
|
+
const existing = await readMarker(skillDir);
|
|
104
|
+
if (!existing || existing.packId !== packId || existing.version !== version)
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
@@ -2,79 +2,178 @@ import fsp from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
4
|
import { jsonResult, readStringParam } from "./common.js";
|
|
5
|
-
import { signSkillPack } from "../../license/skill-pack.js";
|
|
5
|
+
import { signSkillPack, } from "../../license/skill-pack.js";
|
|
6
6
|
import { parseFrontmatter } from "../skills/frontmatter.js";
|
|
7
7
|
const SkillPackSignSchema = Type.Object({
|
|
8
|
-
packId: Type.String({
|
|
9
|
-
description: "Skill pack ID — must match a directory name in the skill-packs/ folder
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
packId: Type.Optional(Type.String({
|
|
9
|
+
description: "Skill pack ID — must match a directory name in the skill-packs/ folder. " +
|
|
10
|
+
"Omit to list available packs.",
|
|
11
|
+
})),
|
|
12
|
+
deviceId: Type.Optional(Type.String({
|
|
12
13
|
description: "Target device ID (tm_dev_...) from the customer's contact record.",
|
|
13
|
-
}),
|
|
14
|
-
customerId: Type.String({
|
|
14
|
+
})),
|
|
15
|
+
customerId: Type.Optional(Type.String({
|
|
15
16
|
description: "Customer identifier from the contact record.",
|
|
16
|
-
}),
|
|
17
|
+
})),
|
|
17
18
|
version: Type.Optional(Type.String({ description: 'Skill pack version (e.g. "1.0.0"). Defaults to "1.0.0".' })),
|
|
18
19
|
});
|
|
20
|
+
/** Read a single skill entry from a directory containing SKILL.md and optional references/. */
|
|
21
|
+
async function readSkillEntry(dir, id) {
|
|
22
|
+
const skillPath = path.join(dir, "SKILL.md");
|
|
23
|
+
const skillContent = await fsp.readFile(skillPath, "utf-8");
|
|
24
|
+
const references = [];
|
|
25
|
+
try {
|
|
26
|
+
const refsDir = path.join(dir, "references");
|
|
27
|
+
const refFiles = await fsp.readdir(refsDir);
|
|
28
|
+
for (const file of refFiles.sort()) {
|
|
29
|
+
if (file.startsWith("."))
|
|
30
|
+
continue;
|
|
31
|
+
const content = await fsp.readFile(path.join(refsDir, file), "utf-8");
|
|
32
|
+
references.push({ name: file, content });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// No references directory — that's fine
|
|
37
|
+
}
|
|
38
|
+
return { id, skill: skillContent, references };
|
|
39
|
+
}
|
|
40
|
+
/** Check whether a directory is a bundle (subdirectories with SKILL.md) or a single skill. */
|
|
41
|
+
async function detectPackLayout(packDir) {
|
|
42
|
+
try {
|
|
43
|
+
await fsp.access(path.join(packDir, "SKILL.md"));
|
|
44
|
+
return "single";
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// No SKILL.md at top level — check for subdirectories
|
|
48
|
+
}
|
|
49
|
+
const entries = await fsp.readdir(packDir, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
await fsp.access(path.join(packDir, entry.name, "SKILL.md"));
|
|
55
|
+
return "bundle";
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Pack "${path.basename(packDir)}" has no SKILL.md at root and no subdirectories with SKILL.md.`);
|
|
62
|
+
}
|
|
19
63
|
export function createSkillPackSignTool(opts) {
|
|
20
64
|
return {
|
|
21
65
|
label: "Skill Pack Sign",
|
|
22
66
|
name: "skill_pack_sign",
|
|
23
|
-
description: "Sign a skill pack for a specific customer device.
|
|
24
|
-
"skill-packs/<
|
|
25
|
-
"
|
|
67
|
+
description: "Sign a skill pack for a specific customer device. Supports single skills " +
|
|
68
|
+
"(skill-packs/<id>/SKILL.md) and bundles (skill-packs/<id>/<skill>/SKILL.md). " +
|
|
69
|
+
"Call without packId to list available packs.",
|
|
26
70
|
parameters: SkillPackSignSchema,
|
|
27
71
|
execute: async (_toolCallId, args) => {
|
|
28
72
|
const params = args;
|
|
29
|
-
const packId = readStringParam(params, "packId"
|
|
30
|
-
const deviceId = readStringParam(params, "deviceId"
|
|
31
|
-
const customerId = readStringParam(params, "customerId"
|
|
73
|
+
const packId = readStringParam(params, "packId");
|
|
74
|
+
const deviceId = readStringParam(params, "deviceId");
|
|
75
|
+
const customerId = readStringParam(params, "customerId");
|
|
32
76
|
const version = readStringParam(params, "version") ?? "1.0.0";
|
|
77
|
+
// ── List mode: no packId → return available packs ──
|
|
78
|
+
const packsDir = path.join(opts.workspaceDir, "skill-packs");
|
|
79
|
+
if (!packId) {
|
|
80
|
+
const packs = [];
|
|
81
|
+
try {
|
|
82
|
+
const entries = await fsp.readdir(packsDir, { withFileTypes: true });
|
|
83
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
84
|
+
if (!entry.isDirectory() || entry.name === "signed" || entry.name.startsWith("."))
|
|
85
|
+
continue;
|
|
86
|
+
const dir = path.join(packsDir, entry.name);
|
|
87
|
+
try {
|
|
88
|
+
const layout = await detectPackLayout(dir);
|
|
89
|
+
if (layout === "bundle") {
|
|
90
|
+
const subs = await fsp.readdir(dir, { withFileTypes: true });
|
|
91
|
+
const skillIds = subs
|
|
92
|
+
.filter((s) => s.isDirectory() && !s.name.startsWith("."))
|
|
93
|
+
.map((s) => s.name)
|
|
94
|
+
.sort();
|
|
95
|
+
packs.push({ id: entry.name, type: "bundle", skills: skillIds });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
packs.push({ id: entry.name, type: "single" });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Skip invalid pack directories
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// directory doesn't exist
|
|
108
|
+
}
|
|
109
|
+
return jsonResult({
|
|
110
|
+
ok: true,
|
|
111
|
+
packsDir,
|
|
112
|
+
packs,
|
|
113
|
+
message: packs.length
|
|
114
|
+
? `Found ${packs.length} pack(s)`
|
|
115
|
+
: `No skill packs found. Create pack templates in ${packsDir}/<packId>/SKILL.md`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (!deviceId || !customerId) {
|
|
119
|
+
throw new Error("deviceId and customerId are required when signing a pack.");
|
|
120
|
+
}
|
|
33
121
|
if (!deviceId.startsWith("tm_dev_")) {
|
|
34
122
|
throw new Error(`Invalid device ID format: "${deviceId}". Device IDs start with "tm_dev_".`);
|
|
35
123
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const skillPath = path.join(packDir, "SKILL.md");
|
|
39
|
-
let skillContent;
|
|
124
|
+
const packDir = path.join(packsDir, packId);
|
|
125
|
+
let layout;
|
|
40
126
|
try {
|
|
41
|
-
|
|
127
|
+
layout = await detectPackLayout(packDir);
|
|
42
128
|
}
|
|
43
129
|
catch {
|
|
44
|
-
throw new Error(`Skill pack
|
|
130
|
+
throw new Error(`Skill pack not found: ${packId}. Expected at ${packDir}`);
|
|
45
131
|
}
|
|
46
|
-
// Read
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
132
|
+
// ── Read skill entries ──
|
|
133
|
+
const skillEntries = [];
|
|
134
|
+
if (layout === "single") {
|
|
135
|
+
skillEntries.push(await readSkillEntry(packDir, packId));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const subs = await fsp.readdir(packDir, { withFileTypes: true });
|
|
139
|
+
for (const sub of subs.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
140
|
+
if (!sub.isDirectory() || sub.name.startsWith("."))
|
|
141
|
+
continue;
|
|
142
|
+
const subDir = path.join(packDir, sub.name);
|
|
143
|
+
try {
|
|
144
|
+
await fsp.access(path.join(subDir, "SKILL.md"));
|
|
145
|
+
skillEntries.push(await readSkillEntry(subDir, sub.name));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// subdirectory without SKILL.md — skip
|
|
149
|
+
}
|
|
54
150
|
}
|
|
55
151
|
}
|
|
56
|
-
|
|
57
|
-
|
|
152
|
+
if (skillEntries.length === 0) {
|
|
153
|
+
throw new Error(`No skills found in pack "${packId}".`);
|
|
58
154
|
}
|
|
59
|
-
// Parse frontmatter for metadata
|
|
60
|
-
const
|
|
61
|
-
const name =
|
|
62
|
-
const description =
|
|
155
|
+
// Parse frontmatter from first skill for pack-level metadata
|
|
156
|
+
const firstFm = parseFrontmatter(skillEntries[0].skill);
|
|
157
|
+
const name = layout === "single" ? (firstFm?.name ?? packId) : packId;
|
|
158
|
+
const description = layout === "single"
|
|
159
|
+
? (firstFm?.description ?? "")
|
|
160
|
+
: `Bundle of ${skillEntries.length} skills`;
|
|
63
161
|
// Build and sign payload
|
|
64
162
|
const payload = {
|
|
65
163
|
format: "skillpack-v1",
|
|
66
164
|
pack: { id: packId, version, name, description, author: "Rubytech LLC" },
|
|
67
165
|
device: { did: deviceId, cid: customerId },
|
|
68
|
-
content: {
|
|
166
|
+
content: { skills: skillEntries },
|
|
69
167
|
signedAt: new Date().toISOString(),
|
|
70
168
|
};
|
|
71
169
|
const signed = signSkillPack(payload);
|
|
72
170
|
// Write signed file
|
|
73
171
|
const shortDid = deviceId.slice(0, 20);
|
|
74
|
-
const signedDir = path.join(
|
|
172
|
+
const signedDir = path.join(packsDir, "signed");
|
|
75
173
|
await fsp.mkdir(signedDir, { recursive: true });
|
|
76
174
|
const outputPath = path.join(signedDir, `${packId}-${shortDid}.skillpack.json`);
|
|
77
175
|
await fsp.writeFile(outputPath, JSON.stringify(signed, null, 2), "utf-8");
|
|
176
|
+
const skillNames = skillEntries.map((s) => s.id).join(", ");
|
|
78
177
|
return jsonResult({
|
|
79
178
|
ok: true,
|
|
80
179
|
filePath: outputPath,
|
|
@@ -82,7 +181,10 @@ export function createSkillPackSignTool(opts) {
|
|
|
82
181
|
version,
|
|
83
182
|
deviceId,
|
|
84
183
|
customerId,
|
|
85
|
-
|
|
184
|
+
skillCount: skillEntries.length,
|
|
185
|
+
skills: skillEntries.map((s) => s.id),
|
|
186
|
+
message: `Signed skill pack "${name}" v${version} with ${skillEntries.length} skill(s) ` +
|
|
187
|
+
`(${skillNames}) for device ${deviceId}. ` +
|
|
86
188
|
`File saved to ${outputPath}. Send this file to the customer with instructions ` +
|
|
87
189
|
`to forward it to their admin agent and ask it to install.`,
|
|
88
190
|
});
|
|
@@ -32,7 +32,7 @@ export function createSystemStatusTool() {
|
|
|
32
32
|
description: "Check the status of the Taskmaster system. " +
|
|
33
33
|
'"overview" returns a combined snapshot (health, auth, license, channels, version, update availability). ' +
|
|
34
34
|
"Use specific actions for detail: " +
|
|
35
|
-
'"health" (gateway health), "auth" (Claude API auth), "license" (license info), ' +
|
|
35
|
+
'"health" (gateway health), "auth" (Claude API auth), "license" (license info + device ID), ' +
|
|
36
36
|
'"channels" (WhatsApp/iMessage connections), "models" (available LLM models), ' +
|
|
37
37
|
'"update" (software version and update availability), ' +
|
|
38
38
|
'"tailscale" (Tailscale connection + public URL), "network" (hostname + port).',
|
package/dist/build-info.json
CHANGED
|
@@ -294,3 +294,69 @@ export function reconcileSkillReadTool(params) {
|
|
|
294
294
|
}
|
|
295
295
|
return { config, changes };
|
|
296
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Individual skill tool names that should be replaced by `group:skills`.
|
|
299
|
+
* Matches the members of TOOL_GROUPS["group:skills"] in tool-policy.ts.
|
|
300
|
+
*/
|
|
301
|
+
const INDIVIDUAL_SKILL_TOOLS = ["skill_read", "skill_draft_save", "skill_pack_install"];
|
|
302
|
+
/**
|
|
303
|
+
* Replace individual skill tool entries with `group:skills` on admin agents.
|
|
304
|
+
*
|
|
305
|
+
* When `group:skills` gains new tools (e.g. `skill_pack_install`), agents with
|
|
306
|
+
* explicit allow lists don't see them. This reconciliation upgrades individual
|
|
307
|
+
* entries to the group reference so future additions are automatic.
|
|
308
|
+
*
|
|
309
|
+
* Runs unconditionally on gateway startup. Idempotent — skips agents that
|
|
310
|
+
* already have `group:skills`.
|
|
311
|
+
*/
|
|
312
|
+
export function reconcileSkillsGroup(params) {
|
|
313
|
+
const config = structuredClone(params.config);
|
|
314
|
+
const changes = [];
|
|
315
|
+
const agents = config.agents?.list;
|
|
316
|
+
if (!Array.isArray(agents))
|
|
317
|
+
return { config, changes };
|
|
318
|
+
for (const agent of agents) {
|
|
319
|
+
if (!agent || !isAdminAgent(agent))
|
|
320
|
+
continue;
|
|
321
|
+
const allow = agent.tools?.allow;
|
|
322
|
+
if (!Array.isArray(allow))
|
|
323
|
+
continue;
|
|
324
|
+
// Already using the group — just clean up any redundant individual entries
|
|
325
|
+
if (allow.includes("group:skills")) {
|
|
326
|
+
const removed = [];
|
|
327
|
+
for (const tool of INDIVIDUAL_SKILL_TOOLS) {
|
|
328
|
+
const idx = allow.indexOf(tool);
|
|
329
|
+
if (idx !== -1) {
|
|
330
|
+
allow.splice(idx, 1);
|
|
331
|
+
removed.push(tool);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (removed.length > 0) {
|
|
335
|
+
changes.push(`Removed redundant ${removed.join(", ")} from agent "${agent.id}" tools.allow (group:skills covers them).`);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
// Check if any individual skill tools are present
|
|
340
|
+
const hasAny = INDIVIDUAL_SKILL_TOOLS.some((t) => allow.includes(t));
|
|
341
|
+
if (!hasAny) {
|
|
342
|
+
// No skill tools at all — only add group if this looks like a full admin agent
|
|
343
|
+
if (!allow.includes("message"))
|
|
344
|
+
continue;
|
|
345
|
+
allow.push("group:skills");
|
|
346
|
+
changes.push(`Added group:skills to agent "${agent.id ?? "<unnamed>"}" tools.allow.`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Replace individual entries with group
|
|
350
|
+
const removed = [];
|
|
351
|
+
for (const tool of INDIVIDUAL_SKILL_TOOLS) {
|
|
352
|
+
const idx = allow.indexOf(tool);
|
|
353
|
+
if (idx !== -1) {
|
|
354
|
+
allow.splice(idx, 1);
|
|
355
|
+
removed.push(tool);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
allow.push("group:skills");
|
|
359
|
+
changes.push(`Replaced ${removed.join(", ")} with group:skills in agent "${agent.id}" tools.allow.`);
|
|
360
|
+
}
|
|
361
|
+
return { config, changes };
|
|
362
|
+
}
|