@rubytech/taskmaster 1.12.1 → 1.12.2
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/taskmaster-tools.js +5 -3
- package/dist/agents/tool-policy.js +1 -1
- package/dist/agents/tools/authorize-admin-tool.js +58 -17
- package/dist/agents/tools/bootstrap-tool.js +51 -0
- package/dist/agents/tools/cron-tool.js +20 -7
- package/dist/build-info.json +3 -3
- package/dist/commands/agents.config.js +1 -0
- package/dist/control-ui/assets/{index-4h8fLLNN.js → index-CP9IoaZp.js} +81 -70
- package/dist/control-ui/assets/index-CP9IoaZp.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/gateway/protocol/schema/cron.js +8 -0
- package/dist/gateway/server-channels.js +6 -2
- package/dist/gateway/server-methods/cron.js +32 -0
- package/dist/gateway/server-methods/tailscale.js +9 -0
- package/dist/gateway/server-methods/workspaces.js +52 -0
- package/dist/web/auto-reply/monitor.js +3 -0
- package/package.json +1 -1
- package/skills/tailscale/SKILL.md +37 -9
- package/templates/beagle-taxi/agents/admin/AGENTS.md +25 -0
- package/templates/beagle-taxi/agents/admin/IDENTITY.md +9 -0
- package/templates/beagle-taxi/agents/admin/SOUL.md +31 -0
- package/templates/beagle-taxi/agents/public/AGENTS.md +54 -0
- package/templates/beagle-taxi/agents/public/IDENTITY.md +10 -0
- package/templates/beagle-taxi/agents/public/SOUL.md +32 -0
- package/templates/beagle-taxi/memory/public/knowledge-base.md +177 -0
- package/templates/beagle-taxi/skills/beagle-taxi/SKILL.md +44 -0
- package/templates/customer/agents/admin/BOOTSTRAP.md +2 -2
- package/templates/education-hero/agents/admin/BOOTSTRAP.md +2 -2
- package/templates/maxy/agents/admin/BOOTSTRAP.md +2 -3
- package/templates/taskmaster/agents/admin/BOOTSTRAP.md +2 -2
- package/templates/tradesupport/agents/admin/BOOTSTRAP.md +2 -2
- package/dist/control-ui/assets/index-4h8fLLNN.js.map +0 -1
|
@@ -21,6 +21,7 @@ import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
|
|
|
21
21
|
import { createTtsTool } from "./tools/tts-tool.js";
|
|
22
22
|
import { createCurrentTimeTool } from "./tools/current-time.js";
|
|
23
23
|
import { createAuthorizeAdminTool, createListAdminsTool, createRevokeAdminTool, } from "./tools/authorize-admin-tool.js";
|
|
24
|
+
import { createBootstrapCompleteTool } from "./tools/bootstrap-tool.js";
|
|
24
25
|
import { createLicenseGenerateTool } from "./tools/license-tool.js";
|
|
25
26
|
import { createContactCreateTool } from "./tools/contact-create-tool.js";
|
|
26
27
|
import { createContactDeleteTool } from "./tools/contact-delete-tool.js";
|
|
@@ -147,9 +148,10 @@ export function createTaskmasterTools(options) {
|
|
|
147
148
|
sandboxRoot: options?.sandboxRoot,
|
|
148
149
|
}),
|
|
149
150
|
createCurrentTimeTool({ config: options?.config }),
|
|
150
|
-
createAuthorizeAdminTool(),
|
|
151
|
-
createRevokeAdminTool(),
|
|
152
|
-
createListAdminsTool(),
|
|
151
|
+
createAuthorizeAdminTool({ agentAccountId: options?.agentAccountId }),
|
|
152
|
+
createRevokeAdminTool({ agentAccountId: options?.agentAccountId }),
|
|
153
|
+
createListAdminsTool({ agentAccountId: options?.agentAccountId }),
|
|
154
|
+
createBootstrapCompleteTool({ agentSessionKey: options?.agentSessionKey }),
|
|
153
155
|
createLicenseGenerateTool(),
|
|
154
156
|
createContactCreateTool(),
|
|
155
157
|
createContactDeleteTool(),
|
|
@@ -45,7 +45,7 @@ export const TOOL_GROUPS = {
|
|
|
45
45
|
"verify_contact_code",
|
|
46
46
|
],
|
|
47
47
|
// Admin management tools
|
|
48
|
-
"group:admin": ["authorize_admin", "revoke_admin", "list_admins"],
|
|
48
|
+
"group:admin": ["authorize_admin", "revoke_admin", "list_admins", "bootstrap_complete"],
|
|
49
49
|
// Control panel tools — expose setup/dashboard functionality to agents
|
|
50
50
|
"group:control-panel": [
|
|
51
51
|
"system_status",
|
|
@@ -13,9 +13,30 @@ function normalizePhoneNumber(input) {
|
|
|
13
13
|
return `+${digits}`;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Resolve the admin agent ID for a given account.
|
|
17
|
+
* Looks up the existing paired-admin binding for the account, which is the
|
|
18
|
+
* authoritative record of which agent handles admin DMs. Falls back to the
|
|
19
|
+
* naming convention "{accountId}-admin", then to "admin" when no account scope.
|
|
17
20
|
*/
|
|
18
|
-
function
|
|
21
|
+
function resolveAdminAgentId(bindings, accountId) {
|
|
22
|
+
if (accountId) {
|
|
23
|
+
const paired = bindings.find((b) => b.match.channel === "whatsapp" &&
|
|
24
|
+
b.match.accountId === accountId &&
|
|
25
|
+
b.match.peer?.kind === "dm" &&
|
|
26
|
+
b.meta?.paired === true);
|
|
27
|
+
if (paired)
|
|
28
|
+
return paired.agentId;
|
|
29
|
+
return `${accountId}-admin`;
|
|
30
|
+
}
|
|
31
|
+
return "admin";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check if a binding already exists for this peer, optionally scoped to an account.
|
|
35
|
+
* When accountId is provided, bindings for a different specific account are not
|
|
36
|
+
* considered conflicts — only bindings for the same account or cross-account
|
|
37
|
+
* wildcards (no accountId) are.
|
|
38
|
+
*/
|
|
39
|
+
function findExistingBinding(bindings, channel, peerId, accountId) {
|
|
19
40
|
const normalizedPeerId = peerId.toLowerCase();
|
|
20
41
|
return bindings.find((b) => {
|
|
21
42
|
if (b.match.channel !== channel)
|
|
@@ -23,7 +44,16 @@ function findExistingBinding(bindings, channel, peerId) {
|
|
|
23
44
|
if (b.match.peer?.kind !== "dm")
|
|
24
45
|
return false;
|
|
25
46
|
const existingId = b.match.peer.id?.toLowerCase();
|
|
26
|
-
|
|
47
|
+
if (existingId !== normalizedPeerId)
|
|
48
|
+
return false;
|
|
49
|
+
// When an account scope is given, exclude bindings scoped to a different account.
|
|
50
|
+
// A binding with no accountId is a cross-account wildcard and always conflicts.
|
|
51
|
+
if (accountId !== undefined &&
|
|
52
|
+
b.match.accountId !== undefined &&
|
|
53
|
+
b.match.accountId !== accountId) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
27
57
|
});
|
|
28
58
|
}
|
|
29
59
|
const AuthorizeAdminSchema = Type.Object({
|
|
@@ -37,7 +67,7 @@ const RevokeAdminSchema = Type.Object({
|
|
|
37
67
|
}),
|
|
38
68
|
});
|
|
39
69
|
const ListAdminsSchema = Type.Object({});
|
|
40
|
-
export function createAuthorizeAdminTool() {
|
|
70
|
+
export function createAuthorizeAdminTool(options) {
|
|
41
71
|
return {
|
|
42
72
|
label: "Admin",
|
|
43
73
|
name: "authorize_admin",
|
|
@@ -45,7 +75,7 @@ export function createAuthorizeAdminTool() {
|
|
|
45
75
|
parameters: AuthorizeAdminSchema,
|
|
46
76
|
execute: async (_toolCallId, params) => {
|
|
47
77
|
const phoneNumber = normalizePhoneNumber(params.phoneNumber);
|
|
48
|
-
const
|
|
78
|
+
const accountId = options?.agentAccountId;
|
|
49
79
|
// Validate phone number format
|
|
50
80
|
if (!/^\+\d{7,15}$/.test(phoneNumber)) {
|
|
51
81
|
return jsonResult({
|
|
@@ -63,8 +93,10 @@ export function createAuthorizeAdminTool() {
|
|
|
63
93
|
}
|
|
64
94
|
const parsedConfig = structuredClone(snapshot.parsed);
|
|
65
95
|
const existingBindings = (parsedConfig.bindings ?? []);
|
|
66
|
-
//
|
|
67
|
-
const
|
|
96
|
+
// Resolve the admin agent for this account from the existing paired binding
|
|
97
|
+
const agentId = resolveAdminAgentId(existingBindings, accountId);
|
|
98
|
+
// Check if binding already exists within this account scope
|
|
99
|
+
const existing = findExistingBinding(existingBindings, "whatsapp", phoneNumber, accountId);
|
|
68
100
|
if (existing) {
|
|
69
101
|
if (existing.agentId === agentId) {
|
|
70
102
|
return jsonResult({
|
|
@@ -79,16 +111,19 @@ export function createAuthorizeAdminTool() {
|
|
|
79
111
|
error: `${phoneNumber} is already bound to the ${existing.agentId} agent. Remove that binding first.`,
|
|
80
112
|
});
|
|
81
113
|
}
|
|
82
|
-
// Create new binding
|
|
114
|
+
// Create new binding scoped to this agent and account
|
|
83
115
|
const newBinding = {
|
|
84
116
|
agentId,
|
|
85
117
|
match: {
|
|
86
118
|
channel: "whatsapp",
|
|
119
|
+
...(accountId ? { accountId } : {}),
|
|
87
120
|
peer: { kind: "dm", id: phoneNumber },
|
|
88
121
|
},
|
|
89
122
|
};
|
|
90
|
-
// Insert before the catch-all binding (
|
|
91
|
-
const catchAllIndex = existingBindings.findIndex((b) => b.match.channel === "whatsapp" &&
|
|
123
|
+
// Insert before the catch-all binding for this account (channel binding with no peer)
|
|
124
|
+
const catchAllIndex = existingBindings.findIndex((b) => b.match.channel === "whatsapp" &&
|
|
125
|
+
!b.match.peer &&
|
|
126
|
+
(accountId ? b.match.accountId === accountId : b.match.accountId === "*"));
|
|
92
127
|
let updatedBindings;
|
|
93
128
|
if (catchAllIndex >= 0) {
|
|
94
129
|
updatedBindings = [
|
|
@@ -111,7 +146,7 @@ export function createAuthorizeAdminTool() {
|
|
|
111
146
|
});
|
|
112
147
|
}
|
|
113
148
|
await writeConfigFile(validated.config);
|
|
114
|
-
logVerbose(`Authorized admin: ${phoneNumber} -> ${agentId}`);
|
|
149
|
+
logVerbose(`Authorized admin: ${phoneNumber} -> ${agentId}${accountId ? ` (account: ${accountId})` : ""}`);
|
|
115
150
|
return jsonResult({
|
|
116
151
|
success: true,
|
|
117
152
|
message: `Authorized ${phoneNumber} as an admin. They now have full management access.`,
|
|
@@ -120,7 +155,7 @@ export function createAuthorizeAdminTool() {
|
|
|
120
155
|
},
|
|
121
156
|
};
|
|
122
157
|
}
|
|
123
|
-
export function createRevokeAdminTool() {
|
|
158
|
+
export function createRevokeAdminTool(options) {
|
|
124
159
|
return {
|
|
125
160
|
label: "Admin",
|
|
126
161
|
name: "revoke_admin",
|
|
@@ -128,6 +163,7 @@ export function createRevokeAdminTool() {
|
|
|
128
163
|
parameters: RevokeAdminSchema,
|
|
129
164
|
execute: async (_toolCallId, params) => {
|
|
130
165
|
const phoneNumber = normalizePhoneNumber(params.phoneNumber);
|
|
166
|
+
const accountId = options?.agentAccountId;
|
|
131
167
|
const snapshot = await readConfigFileSnapshot();
|
|
132
168
|
if (!snapshot.valid || !snapshot.parsed || typeof snapshot.parsed !== "object") {
|
|
133
169
|
return jsonResult({
|
|
@@ -137,7 +173,7 @@ export function createRevokeAdminTool() {
|
|
|
137
173
|
}
|
|
138
174
|
const parsedConfig = structuredClone(snapshot.parsed);
|
|
139
175
|
const existingBindings = (parsedConfig.bindings ?? []);
|
|
140
|
-
const existing = findExistingBinding(existingBindings, "whatsapp", phoneNumber);
|
|
176
|
+
const existing = findExistingBinding(existingBindings, "whatsapp", phoneNumber, accountId);
|
|
141
177
|
if (!existing) {
|
|
142
178
|
return jsonResult({
|
|
143
179
|
success: false,
|
|
@@ -155,7 +191,7 @@ export function createRevokeAdminTool() {
|
|
|
155
191
|
});
|
|
156
192
|
}
|
|
157
193
|
await writeConfigFile(validated.config);
|
|
158
|
-
logVerbose(`Revoked admin: ${phoneNumber}`);
|
|
194
|
+
logVerbose(`Revoked admin: ${phoneNumber}${accountId ? ` (account: ${accountId})` : ""}`);
|
|
159
195
|
return jsonResult({
|
|
160
196
|
success: true,
|
|
161
197
|
message: `Revoked admin access for ${phoneNumber}.`,
|
|
@@ -164,7 +200,7 @@ export function createRevokeAdminTool() {
|
|
|
164
200
|
},
|
|
165
201
|
};
|
|
166
202
|
}
|
|
167
|
-
export function createListAdminsTool() {
|
|
203
|
+
export function createListAdminsTool(options) {
|
|
168
204
|
return {
|
|
169
205
|
label: "Admin",
|
|
170
206
|
name: "list_admins",
|
|
@@ -173,15 +209,20 @@ export function createListAdminsTool() {
|
|
|
173
209
|
execute: async (_toolCallId) => {
|
|
174
210
|
const cfg = loadConfig();
|
|
175
211
|
const bindings = cfg.bindings ?? [];
|
|
212
|
+
const accountId = options?.agentAccountId;
|
|
213
|
+
// Resolve the admin agent for this account so we only list admins scoped here
|
|
214
|
+
const agentId = resolveAdminAgentId(bindings, accountId);
|
|
176
215
|
const admins = bindings
|
|
177
216
|
.filter((b) => {
|
|
178
|
-
|
|
179
|
-
if (b.agentId !== "admin" && b.agentId !== "management")
|
|
217
|
+
if (b.agentId !== agentId)
|
|
180
218
|
return false;
|
|
181
219
|
if (b.match.channel !== "whatsapp")
|
|
182
220
|
return false;
|
|
183
221
|
if (b.match.peer?.kind !== "dm")
|
|
184
222
|
return false;
|
|
223
|
+
// Exclude bindings scoped to a different account
|
|
224
|
+
if (accountId && b.match.accountId && b.match.accountId !== accountId)
|
|
225
|
+
return false;
|
|
185
226
|
return Boolean(b.match.peer.id);
|
|
186
227
|
})
|
|
187
228
|
.map((b) => b.match.peer?.id)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { resolveAgentWorkspaceDir } from "../agent-scope.js";
|
|
5
|
+
import { loadConfig } from "../../config/config.js";
|
|
6
|
+
import { logVerbose } from "../../globals.js";
|
|
7
|
+
import { DEFAULT_BOOTSTRAP_FILENAME, BOOTSTRAP_DONE_SENTINEL } from "../workspace.js";
|
|
8
|
+
import { resolveSessionAgentId } from "../agent-scope.js";
|
|
9
|
+
import { jsonResult } from "./common.js";
|
|
10
|
+
const BootstrapCompleteSchema = Type.Object({});
|
|
11
|
+
/**
|
|
12
|
+
* Tool that marks agent bootstrap as complete.
|
|
13
|
+
* Writes the .bootstrap-done sentinel (suppresses future BOOTSTRAP.md injection)
|
|
14
|
+
* and removes BOOTSTRAP.md if it exists.
|
|
15
|
+
*
|
|
16
|
+
* This is a privileged action — agents cannot write to the agents/ directory via
|
|
17
|
+
* general file tools, so this dedicated tool provides the safe, auditable path.
|
|
18
|
+
*/
|
|
19
|
+
export function createBootstrapCompleteTool(opts) {
|
|
20
|
+
return {
|
|
21
|
+
label: "Setup",
|
|
22
|
+
name: "bootstrap_complete",
|
|
23
|
+
description: "Mark the initial setup as complete. Writes the .bootstrap-done sentinel so the setup guide is never shown again, then removes BOOTSTRAP.md. Call this after completing all steps in BOOTSTRAP.md.",
|
|
24
|
+
parameters: BootstrapCompleteSchema,
|
|
25
|
+
execute: async (_toolCallId) => {
|
|
26
|
+
const cfg = loadConfig();
|
|
27
|
+
const agentId = opts?.agentSessionKey
|
|
28
|
+
? resolveSessionAgentId({ sessionKey: opts.agentSessionKey, config: cfg })
|
|
29
|
+
: undefined;
|
|
30
|
+
if (!agentId) {
|
|
31
|
+
return jsonResult({ success: false, error: "Cannot resolve agent identity." });
|
|
32
|
+
}
|
|
33
|
+
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
34
|
+
const sentinelPath = path.join(workspaceDir, BOOTSTRAP_DONE_SENTINEL);
|
|
35
|
+
const bootstrapPath = path.join(workspaceDir, DEFAULT_BOOTSTRAP_FILENAME);
|
|
36
|
+
// Write the sentinel — this suppresses BOOTSTRAP.md injection on all future runs.
|
|
37
|
+
await fs.writeFile(sentinelPath, "", "utf-8");
|
|
38
|
+
logVerbose(`bootstrap_complete: wrote sentinel ${sentinelPath}`);
|
|
39
|
+
// Remove BOOTSTRAP.md if present — no longer needed once the sentinel exists.
|
|
40
|
+
const removed = await fs
|
|
41
|
+
.unlink(bootstrapPath)
|
|
42
|
+
.then(() => true)
|
|
43
|
+
.catch(() => false);
|
|
44
|
+
logVerbose(`bootstrap_complete: BOOTSTRAP.md ${removed ? "removed" : "already absent"}`);
|
|
45
|
+
return jsonResult({
|
|
46
|
+
success: true,
|
|
47
|
+
message: "Setup complete. This onboarding guide will no longer appear.",
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -231,31 +231,44 @@ Use jobId as the canonical identifier; id is accepted for compatibility. Use con
|
|
|
231
231
|
throw new Error("patch required");
|
|
232
232
|
}
|
|
233
233
|
const patch = normalizeCronJobPatch(params.patch) ?? params.patch;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
const updatePayload = { id, patch };
|
|
235
|
+
const updateAccountId = opts?.agentAccountId;
|
|
236
|
+
if (updateAccountId)
|
|
237
|
+
updatePayload.accountId = updateAccountId;
|
|
238
|
+
return jsonResult(await callGatewayTool("cron.update", gatewayOpts, updatePayload));
|
|
238
239
|
}
|
|
239
240
|
case "remove": {
|
|
240
241
|
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
|
241
242
|
if (!id) {
|
|
242
243
|
throw new Error("jobId required (id accepted for backward compatibility)");
|
|
243
244
|
}
|
|
244
|
-
|
|
245
|
+
const removePayload = { id };
|
|
246
|
+
const removeAccountId = opts?.agentAccountId;
|
|
247
|
+
if (removeAccountId)
|
|
248
|
+
removePayload.accountId = removeAccountId;
|
|
249
|
+
return jsonResult(await callGatewayTool("cron.remove", gatewayOpts, removePayload));
|
|
245
250
|
}
|
|
246
251
|
case "run": {
|
|
247
252
|
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
|
248
253
|
if (!id) {
|
|
249
254
|
throw new Error("jobId required (id accepted for backward compatibility)");
|
|
250
255
|
}
|
|
251
|
-
|
|
256
|
+
const runPayload = { id };
|
|
257
|
+
const runAccountId = opts?.agentAccountId;
|
|
258
|
+
if (runAccountId)
|
|
259
|
+
runPayload.accountId = runAccountId;
|
|
260
|
+
return jsonResult(await callGatewayTool("cron.run", gatewayOpts, runPayload));
|
|
252
261
|
}
|
|
253
262
|
case "runs": {
|
|
254
263
|
const id = readStringParam(params, "jobId") ?? readStringParam(params, "id");
|
|
255
264
|
if (!id) {
|
|
256
265
|
throw new Error("jobId required (id accepted for backward compatibility)");
|
|
257
266
|
}
|
|
258
|
-
|
|
267
|
+
const runsPayload = { id };
|
|
268
|
+
const runsAccountId = opts?.agentAccountId;
|
|
269
|
+
if (runsAccountId)
|
|
270
|
+
runsPayload.accountId = runsAccountId;
|
|
271
|
+
return jsonResult(await callGatewayTool("cron.runs", gatewayOpts, runsPayload));
|
|
259
272
|
}
|
|
260
273
|
case "wake": {
|
|
261
274
|
const text = readStringParam(params, "text", { required: true });
|
package/dist/build-info.json
CHANGED
|
@@ -90,6 +90,7 @@ export function applyAgentConfig(cfg, params) {
|
|
|
90
90
|
...(params.workspace ? { workspace: params.workspace } : {}),
|
|
91
91
|
...(params.agentDir ? { agentDir: params.agentDir } : {}),
|
|
92
92
|
...(params.model ? { model: params.model } : {}),
|
|
93
|
+
...(params.tools ? { tools: params.tools } : {}),
|
|
93
94
|
};
|
|
94
95
|
const nextList = [...list];
|
|
95
96
|
if (index >= 0) {
|