@rubytech/taskmaster 1.9.4 → 1.9.5
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/pi-embedded-runner/run/attempt.js +40 -0
- package/dist/agents/taskmaster-tools.js +3 -0
- package/dist/agents/tool-policy.js +10 -1
- package/dist/agents/tools/apikeys-tool.js +2 -2
- package/dist/agents/tools/file-delete-tool.js +20 -15
- package/dist/agents/tools/file-list-tool.js +9 -2
- package/dist/agents/tools/verify-contact-tool.js +197 -0
- package/dist/agents/workspace-migrations.js +163 -0
- package/dist/build-info.json +3 -3
- package/dist/config/defaults.js +4 -0
- package/dist/config/legacy.migrations.part-3.js +24 -0
- package/dist/config/zod-schema.js +21 -0
- package/dist/control-ui/assets/index-DpyzE2YD.js +4532 -0
- package/dist/control-ui/assets/index-DpyzE2YD.js.map +1 -0
- package/dist/control-ui/assets/index-ouo9dqKk.css +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/control-ui.js +6 -1
- package/dist/gateway/public-chat/deliver-email.js +39 -0
- package/dist/gateway/public-chat/deliver-otp.js +59 -6
- package/dist/gateway/public-chat/deliver-sms.js +44 -0
- package/dist/gateway/public-chat/otp.js +14 -12
- package/dist/gateway/public-chat-api.js +100 -24
- package/dist/gateway/server-chat.js +5 -0
- package/dist/gateway/server-methods/access.js +11 -1
- package/dist/gateway/server-methods/apikeys.js +8 -4
- package/dist/gateway/server-methods/chat.js +14 -0
- package/dist/gateway/server-methods/public-chat.js +94 -22
- package/dist/gateway/server-methods/tailscale.js +65 -12
- package/dist/gateway/server.impl.js +5 -0
- package/dist/memory/manager.js +6 -2
- package/dist/records/records-manager.js +25 -1
- package/package.json +1 -1
- package/skills/twilio/SKILL.md +29 -0
- package/skills/twilio/references/browser-setup.md +95 -0
- package/templates/beagle/agents/admin/AGENTS.md +24 -0
- package/templates/beagle/agents/public/AGENTS.md +6 -0
- package/templates/customer/agents/admin/AGENTS.md +24 -0
- package/templates/customer/agents/public/AGENTS.md +6 -0
- package/templates/education-hero/agents/admin/AGENTS.md +184 -0
- package/templates/education-hero/agents/admin/BOOTSTRAP.md +114 -0
- package/templates/education-hero/agents/admin/HEARTBEAT.md +10 -0
- package/templates/education-hero/agents/admin/IDENTITY.md +13 -0
- package/templates/education-hero/agents/admin/SOUL.md +34 -0
- package/templates/education-hero/agents/admin/TOOLS.md +36 -0
- package/templates/education-hero/agents/admin/USER.md +13 -0
- package/templates/education-hero/agents/public/AGENTS.md +173 -0
- package/templates/education-hero/agents/public/IDENTITY.md +10 -0
- package/templates/education-hero/agents/public/SOUL.md +84 -0
- package/templates/education-hero/skills/education-hero/SKILL.md +43 -0
- package/templates/education-hero/skills/education-hero/references/admin-process.md +28 -0
- package/templates/education-hero/skills/education-hero/references/brand-voice.md +51 -0
- package/templates/education-hero/skills/education-hero/references/deregistration.md +34 -0
- package/templates/education-hero/skills/education-hero/references/educational-approach.md +28 -0
- package/templates/education-hero/skills/education-hero/references/intent-classification.md +39 -0
- package/templates/education-hero/skills/education-hero/references/la-email-analysis.md +42 -0
- package/templates/education-hero/skills/education-hero/references/legal-rights.md +37 -0
- package/templates/education-hero/skills/education-hero/references/report-writing.md +30 -0
- package/templates/education-hero/skills/interactive-tutor/SKILL.md +60 -0
- package/templates/education-hero/skills/interactive-tutor/references/assessment.md +70 -0
- package/templates/education-hero/skills/interactive-tutor/references/classroom-conduct.md +43 -0
- package/templates/education-hero/skills/interactive-tutor/references/teaching-modes.md +83 -0
- package/templates/education-hero/skills/lesson-planner/SKILL.md +49 -0
- package/templates/education-hero/skills/lesson-planner/references/context-gathering.md +41 -0
- package/templates/education-hero/skills/lesson-planner/references/plan-structure.md +94 -0
- package/templates/education-hero/skills/study-pack-builder/SKILL.md +53 -0
- package/templates/education-hero/skills/study-pack-builder/references/disaggregation.md +49 -0
- package/templates/education-hero/skills/study-pack-builder/references/materials.md +116 -0
- package/templates/maxy/agents/admin/AGENTS.md +20 -0
- package/templates/maxy/agents/public/AGENTS.md +4 -0
- package/templates/taskmaster/agents/admin/AGENTS.md +24 -0
- package/templates/taskmaster/agents/public/AGENTS.md +6 -0
- package/templates/tradesupport/agents/admin/AGENTS.md +24 -0
- package/templates/tradesupport/agents/public/AGENTS.md +6 -0
- package/dist/control-ui/assets/index-CHIqq3Nn.css +0 -1
- package/dist/control-ui/assets/index-zUaHKRVM.js +0 -4227
- package/dist/control-ui/assets/index-zUaHKRVM.js.map +0 -1
|
@@ -486,6 +486,46 @@ export async function runEmbeddedAttempt(params) {
|
|
|
486
486
|
? validateAnthropicTurns(validatedGemini)
|
|
487
487
|
: validatedGemini;
|
|
488
488
|
const limited = limitHistoryTurns(validated, getEffectiveHistoryLimit(params.sessionKey, params.config));
|
|
489
|
+
// On the first turn of a public-chat session, inject the configured
|
|
490
|
+
// greeting as a synthetic assistant message so the agent knows what
|
|
491
|
+
// the visitor is responding to and treats it as its own opening message.
|
|
492
|
+
// Greetings are keyed by accountId (workspace), not agentId, so we
|
|
493
|
+
// resolve which accountId maps to this session's agent.
|
|
494
|
+
if (limited.length === 0 && params.config?.publicChat?.greetings && params.sessionKey) {
|
|
495
|
+
const greetings = params.config.publicChat.greetings;
|
|
496
|
+
const { sessionAgentId } = resolveSessionAgentIds({
|
|
497
|
+
sessionKey: params.sessionKey,
|
|
498
|
+
config: params.config,
|
|
499
|
+
});
|
|
500
|
+
// Find greeting: try agentId first, then find the accountId whose
|
|
501
|
+
// public agent matches this session's agent.
|
|
502
|
+
let greetingText = greetings[sessionAgentId];
|
|
503
|
+
if (!greetingText) {
|
|
504
|
+
for (const [accountId, text] of Object.entries(greetings)) {
|
|
505
|
+
if (!text)
|
|
506
|
+
continue;
|
|
507
|
+
try {
|
|
508
|
+
const { resolvePublicAgentId } = await import("../../../gateway/public-chat/session.js");
|
|
509
|
+
const mapped = resolvePublicAgentId(params.config, accountId);
|
|
510
|
+
if (mapped === sessionAgentId) {
|
|
511
|
+
greetingText = text;
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (greetingText) {
|
|
521
|
+
log.info(`Injecting greeting as assistant message: agentId=${sessionAgentId} greeting="${greetingText.slice(0, 60)}"`);
|
|
522
|
+
limited.unshift({
|
|
523
|
+
role: "assistant",
|
|
524
|
+
content: [{ type: "text", text: greetingText }],
|
|
525
|
+
timestamp: 0,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
489
529
|
cacheTrace?.recordStage("session:limited", { messages: limited });
|
|
490
530
|
if (limited.length > 0) {
|
|
491
531
|
activeSession.agent.replaceMessages(limited);
|
|
@@ -34,6 +34,7 @@ import { createSkillReadTool } from "./tools/skill-read-tool.js";
|
|
|
34
34
|
import { createApiKeysTool } from "./tools/apikeys-tool.js";
|
|
35
35
|
import { createImageGenerateTool } from "./tools/image-generate-tool.js";
|
|
36
36
|
import { createSoftwareUpdateTool } from "./tools/software-update-tool.js";
|
|
37
|
+
import { createVerifyContactTool, createVerifyContactCodeTool, } from "./tools/verify-contact-tool.js";
|
|
37
38
|
export function createTaskmasterTools(options) {
|
|
38
39
|
const imageTool = options?.agentDir?.trim()
|
|
39
40
|
? createImageTool({
|
|
@@ -147,6 +148,8 @@ export function createTaskmasterTools(options) {
|
|
|
147
148
|
createContactDeleteTool(),
|
|
148
149
|
createContactLookupTool(),
|
|
149
150
|
createContactUpdateTool(),
|
|
151
|
+
createVerifyContactTool({ agentSessionKey: options?.agentSessionKey }),
|
|
152
|
+
createVerifyContactCodeTool({ agentSessionKey: options?.agentSessionKey }),
|
|
150
153
|
createRelayMessageTool(),
|
|
151
154
|
createApiKeysTool(),
|
|
152
155
|
createFileDeleteTool({
|
|
@@ -36,7 +36,14 @@ export const TOOL_GROUPS = {
|
|
|
36
36
|
// Skill management
|
|
37
37
|
"group:skills": ["skill_read", "skill_draft_save"],
|
|
38
38
|
// Contact record management
|
|
39
|
-
"group:contacts": [
|
|
39
|
+
"group:contacts": [
|
|
40
|
+
"contact_create",
|
|
41
|
+
"contact_delete",
|
|
42
|
+
"contact_lookup",
|
|
43
|
+
"contact_update",
|
|
44
|
+
"verify_contact",
|
|
45
|
+
"verify_contact_code",
|
|
46
|
+
],
|
|
40
47
|
// Admin management tools
|
|
41
48
|
"group:admin": ["authorize_admin", "revoke_admin", "list_admins"],
|
|
42
49
|
// All Taskmaster native tools (excludes provider plugins).
|
|
@@ -71,6 +78,8 @@ export const TOOL_GROUPS = {
|
|
|
71
78
|
"file_delete",
|
|
72
79
|
"file_list",
|
|
73
80
|
"skill_draft_save",
|
|
81
|
+
"verify_contact",
|
|
82
|
+
"verify_contact_code",
|
|
74
83
|
],
|
|
75
84
|
};
|
|
76
85
|
// Tools that are never granted by profiles — must be explicitly added to the
|
|
@@ -13,7 +13,7 @@ const ApiKeysSchema = Type.Object({
|
|
|
13
13
|
description: 'Action to perform: "set" stores a key, "list" shows which providers have keys configured, "remove" deletes a stored key, "disable" toggles a key on/off without deleting it.',
|
|
14
14
|
}),
|
|
15
15
|
provider: Type.Optional(Type.String({
|
|
16
|
-
description: "Provider ID (required for set/remove/disable). Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs.",
|
|
16
|
+
description: "Provider ID (required for set/remove/disable). Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs, resend.",
|
|
17
17
|
})),
|
|
18
18
|
apiKey: Type.Optional(Type.String({
|
|
19
19
|
description: "The API key value (required for set).",
|
|
@@ -26,7 +26,7 @@ export function createApiKeysTool() {
|
|
|
26
26
|
return {
|
|
27
27
|
label: "API Keys",
|
|
28
28
|
name: "api_keys",
|
|
29
|
-
description: "Manage API keys for external providers. Use action 'set' to store a key (provider + apiKey required), 'list' to check which providers have keys configured, 'remove' to delete a stored key, or 'disable' to toggle a key on/off without deleting it (provider + disabled required). Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs. Keys are applied immediately without restart.",
|
|
29
|
+
description: "Manage API keys for external providers. Use action 'set' to store a key (provider + apiKey required), 'list' to check which providers have keys configured, 'remove' to delete a stored key, or 'disable' to toggle a key on/off without deleting it (provider + disabled required). Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs, resend. Keys are applied immediately without restart.",
|
|
30
30
|
parameters: ApiKeysSchema,
|
|
31
31
|
execute: async (_toolCallId, args) => {
|
|
32
32
|
const params = args;
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { realpath } from "node:fs/promises";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
-
import { resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../agent-scope.js";
|
|
5
|
+
import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveSessionAgentId, } from "../agent-scope.js";
|
|
6
6
|
import { jsonResult, readStringParam } from "./common.js";
|
|
7
7
|
const FileDeleteSchema = Type.Object({
|
|
8
8
|
path: Type.String({
|
|
@@ -18,13 +18,7 @@ const FileDeleteSchema = Type.Object({
|
|
|
18
18
|
* Protected top-level entries that cannot be deleted.
|
|
19
19
|
* These are structural directories/files whose removal would break the workspace.
|
|
20
20
|
*/
|
|
21
|
-
const PROTECTED_TOP_LEVEL = new Set([
|
|
22
|
-
"agents",
|
|
23
|
-
"IDENTITY.md",
|
|
24
|
-
"SOUL.md",
|
|
25
|
-
"AGENTS.md",
|
|
26
|
-
"TOOLS.md",
|
|
27
|
-
]);
|
|
21
|
+
const PROTECTED_TOP_LEVEL = new Set(["agents", "IDENTITY.md", "SOUL.md", "AGENTS.md", "TOOLS.md"]);
|
|
28
22
|
/**
|
|
29
23
|
* Validate that `target` is strictly inside `root` (not equal to root).
|
|
30
24
|
* Both paths must be real (symlinks resolved) before calling.
|
|
@@ -56,12 +50,17 @@ export function createFileDeleteTool(options) {
|
|
|
56
50
|
config: cfg,
|
|
57
51
|
});
|
|
58
52
|
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
|
|
53
|
+
const agentWorkspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
59
54
|
// ── Normalise and resolve target ────────────────────────────
|
|
60
55
|
// Reject obviously malicious patterns before touching the filesystem.
|
|
61
56
|
if (rawPath.includes("\0")) {
|
|
62
57
|
return jsonResult({ error: "Invalid path." });
|
|
63
58
|
}
|
|
64
|
-
|
|
59
|
+
// Memory paths resolve relative to the agent's workspace dir, which
|
|
60
|
+
// contains shared-scope symlinks alongside agent-scoped subdirectories.
|
|
61
|
+
const isMemoryPath = rawPath === "memory" || rawPath.startsWith("memory/");
|
|
62
|
+
const base = isMemoryPath ? agentWorkspaceDir : workspaceRoot;
|
|
63
|
+
const resolved = path.resolve(base, rawPath);
|
|
65
64
|
// ── Containment check (pre-realpath) ────────────────────────
|
|
66
65
|
if (!isInsideRoot(resolved, workspaceRoot)) {
|
|
67
66
|
return jsonResult({ error: "Path is outside the workspace." });
|
|
@@ -94,12 +93,18 @@ export function createFileDeleteTool(options) {
|
|
|
94
93
|
return jsonResult({ error: "Path resolves outside the workspace." });
|
|
95
94
|
}
|
|
96
95
|
// ── Protected path check ────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
// Memory paths physically reside under agents/<id>/memory/ but the
|
|
97
|
+
// user addresses them as memory/… — they are never structural entries.
|
|
98
|
+
// Only apply the structural protection for non-memory paths resolved
|
|
99
|
+
// against the workspace root.
|
|
100
|
+
if (!isMemoryPath) {
|
|
101
|
+
const relative = path.relative(realRoot, realTarget);
|
|
102
|
+
const topSegment = relative.split(path.sep)[0];
|
|
103
|
+
if (topSegment && PROTECTED_TOP_LEVEL.has(topSegment)) {
|
|
104
|
+
return jsonResult({
|
|
105
|
+
error: `Cannot delete protected path: ${topSegment} is a structural workspace entry.`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
103
108
|
}
|
|
104
109
|
// ── Delete ──────────────────────────────────────────────────
|
|
105
110
|
try {
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { realpath } from "node:fs/promises";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
-
import { resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../agent-scope.js";
|
|
5
|
+
import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveSessionAgentId, } from "../agent-scope.js";
|
|
6
6
|
import { jsonResult, readStringParam, readNumberParam } from "./common.js";
|
|
7
7
|
const FileListSchema = Type.Object({
|
|
8
8
|
path: Type.Optional(Type.String({
|
|
@@ -82,11 +82,18 @@ export function createFileListTool(options) {
|
|
|
82
82
|
config: cfg,
|
|
83
83
|
});
|
|
84
84
|
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
|
|
85
|
+
const agentWorkspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
85
86
|
// ── Normalise and resolve target ────────────────────────────
|
|
86
87
|
if (rawPath.includes("\0")) {
|
|
87
88
|
return jsonResult({ error: "Invalid path." });
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
+
// Memory paths resolve relative to the agent's workspace dir, which
|
|
91
|
+
// contains shared-scope symlinks (groups/, users/, …) alongside
|
|
92
|
+
// agent-scoped subdirectories (admin/, notes/, uploads/). The
|
|
93
|
+
// workspace root's memory/ only has the shared scopes.
|
|
94
|
+
const isMemoryPath = rawPath === "memory" || rawPath.startsWith("memory/");
|
|
95
|
+
const base = isMemoryPath ? agentWorkspaceDir : workspaceRoot;
|
|
96
|
+
const resolved = rawPath ? path.resolve(base, rawPath) : workspaceRoot;
|
|
90
97
|
// ── Containment check ───────────────────────────────────────
|
|
91
98
|
let realTarget;
|
|
92
99
|
try {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-authentication tool -- links a second identifier (phone or email) to an
|
|
3
|
+
* existing contact by sending an OTP to the new identifier and verifying it.
|
|
4
|
+
*
|
|
5
|
+
* Two-step flow:
|
|
6
|
+
* 1. verify_contact -- sends OTP to the unverified identifier
|
|
7
|
+
* 2. verify_contact_code -- confirms the OTP and updates the contact record
|
|
8
|
+
*/
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import { loadConfig } from "../../config/config.js";
|
|
11
|
+
import { deliverOtp } from "../../gateway/public-chat/deliver-otp.js";
|
|
12
|
+
import { requestOtp, verifyOtp } from "../../gateway/public-chat/otp.js";
|
|
13
|
+
import { findRecordByEmail, findRecordByPhone, getRecord, setRecord, } from "../../records/records-manager.js";
|
|
14
|
+
import { resolveAgentBoundAccountId } from "../../routing/bindings.js";
|
|
15
|
+
import { jsonResult } from "./common.js";
|
|
16
|
+
/** In-memory pending cross-auth keyed by session key. */
|
|
17
|
+
const pendingCrossAuth = new Map();
|
|
18
|
+
const VerifyContactSchema = Type.Object({
|
|
19
|
+
type: Type.Union([Type.Literal("phone"), Type.Literal("email")], {
|
|
20
|
+
description: 'The type of identifier to verify: "phone" or "email".',
|
|
21
|
+
}),
|
|
22
|
+
value: Type.String({
|
|
23
|
+
description: "The phone number (E.164 format) or email address to send the verification code to.",
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
const VerifyContactCodeSchema = Type.Object({
|
|
27
|
+
code: Type.String({ description: "The 6-digit verification code." }),
|
|
28
|
+
});
|
|
29
|
+
/** Extract the peer identifier from a DM session key (agent:x:dm:identifier). */
|
|
30
|
+
function extractPeerId(sessionKey) {
|
|
31
|
+
const match = sessionKey.match(/^agent:[^:]+:dm:(.+)$/);
|
|
32
|
+
return match?.[1];
|
|
33
|
+
}
|
|
34
|
+
/** Extract the agent ID from a session key (agent:x:...). */
|
|
35
|
+
function extractAgentId(sessionKey) {
|
|
36
|
+
const match = sessionKey.match(/^agent:([^:]+):/);
|
|
37
|
+
return match?.[1];
|
|
38
|
+
}
|
|
39
|
+
/** Resolve the contact record for a given peer identifier. */
|
|
40
|
+
function resolveContactForPeer(peerId) {
|
|
41
|
+
const isEmailPeer = peerId.includes("@");
|
|
42
|
+
if (isEmailPeer)
|
|
43
|
+
return findRecordByEmail(peerId);
|
|
44
|
+
return findRecordByPhone(peerId) ?? getRecord(peerId);
|
|
45
|
+
}
|
|
46
|
+
export function createVerifyContactTool(opts) {
|
|
47
|
+
const sessionKey = opts?.agentSessionKey;
|
|
48
|
+
return {
|
|
49
|
+
label: "Verification",
|
|
50
|
+
name: "verify_contact",
|
|
51
|
+
description: "Send a verification code to a phone number or email address to cross-authenticate the current contact. " +
|
|
52
|
+
"Use this when a contact who authenticated via one method (e.g. email) provides their other identifier " +
|
|
53
|
+
"(e.g. phone number) and you want to verify and link it to their contact record.",
|
|
54
|
+
parameters: VerifyContactSchema,
|
|
55
|
+
execute: async (_toolCallId, args) => {
|
|
56
|
+
const params = args;
|
|
57
|
+
if (!sessionKey) {
|
|
58
|
+
return jsonResult({ success: false, error: "no active session" });
|
|
59
|
+
}
|
|
60
|
+
const peerId = extractPeerId(sessionKey);
|
|
61
|
+
if (!peerId || peerId.startsWith("anon-")) {
|
|
62
|
+
return jsonResult({
|
|
63
|
+
success: false,
|
|
64
|
+
error: "contact must be authenticated (not anonymous) to cross-verify",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const contact = resolveContactForPeer(peerId);
|
|
68
|
+
if (!contact) {
|
|
69
|
+
return jsonResult({
|
|
70
|
+
success: false,
|
|
71
|
+
error: `no contact record found for ${peerId}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Check they don't already have this identifier
|
|
75
|
+
if (params.type === "email" && contact.email) {
|
|
76
|
+
return jsonResult({
|
|
77
|
+
success: false,
|
|
78
|
+
error: `contact already has email: ${contact.email}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (params.type === "phone" && contact.phone) {
|
|
82
|
+
return jsonResult({
|
|
83
|
+
success: false,
|
|
84
|
+
error: `contact already has phone: ${contact.phone}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Check for conflicts -- another contact already using this identifier
|
|
88
|
+
const existing = params.type === "email" ? findRecordByEmail(params.value) : findRecordByPhone(params.value);
|
|
89
|
+
if (existing && existing.id !== contact.id) {
|
|
90
|
+
return jsonResult({
|
|
91
|
+
success: false,
|
|
92
|
+
error: `another contact (${existing.name}) already uses this ${params.type}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Request and deliver OTP
|
|
96
|
+
const normalized = params.type === "email" ? params.value.toLowerCase() : params.value;
|
|
97
|
+
const otpResult = requestOtp(normalized);
|
|
98
|
+
if (!otpResult.ok) {
|
|
99
|
+
return jsonResult({
|
|
100
|
+
success: false,
|
|
101
|
+
error: "rate limited -- try again shortly",
|
|
102
|
+
retryAfterMs: otpResult.retryAfterMs,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const cfg = loadConfig();
|
|
106
|
+
const agentId = extractAgentId(sessionKey);
|
|
107
|
+
const whatsappAccountId = agentId
|
|
108
|
+
? (resolveAgentBoundAccountId(cfg, agentId, "whatsapp") ?? undefined)
|
|
109
|
+
: undefined;
|
|
110
|
+
try {
|
|
111
|
+
const delivery = await deliverOtp(normalized, otpResult.code, whatsappAccountId);
|
|
112
|
+
pendingCrossAuth.set(sessionKey, {
|
|
113
|
+
type: params.type,
|
|
114
|
+
value: normalized,
|
|
115
|
+
contactId: contact.id,
|
|
116
|
+
});
|
|
117
|
+
return jsonResult({
|
|
118
|
+
success: true,
|
|
119
|
+
message: `Verification code sent via ${delivery.channel}. Ask the contact for the code.`,
|
|
120
|
+
channel: delivery.channel,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return jsonResult({
|
|
125
|
+
success: false,
|
|
126
|
+
error: err instanceof Error ? err.message : "failed to send verification code",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function createVerifyContactCodeTool(opts) {
|
|
133
|
+
const sessionKey = opts?.agentSessionKey;
|
|
134
|
+
return {
|
|
135
|
+
label: "Verification",
|
|
136
|
+
name: "verify_contact_code",
|
|
137
|
+
description: "Verify a cross-authentication code. Call this after verify_contact when the contact provides the 6-digit code.",
|
|
138
|
+
parameters: VerifyContactCodeSchema,
|
|
139
|
+
execute: async (_toolCallId, args) => {
|
|
140
|
+
const params = args;
|
|
141
|
+
if (!sessionKey) {
|
|
142
|
+
return jsonResult({ success: false, error: "no active session" });
|
|
143
|
+
}
|
|
144
|
+
const pending = pendingCrossAuth.get(sessionKey);
|
|
145
|
+
if (!pending) {
|
|
146
|
+
return jsonResult({
|
|
147
|
+
success: false,
|
|
148
|
+
error: "no pending cross-verification -- call verify_contact first",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const result = verifyOtp(pending.value, params.code);
|
|
152
|
+
if (!result.ok) {
|
|
153
|
+
const messages = {
|
|
154
|
+
not_found: "no pending verification -- code may have expired",
|
|
155
|
+
expired: "verification code expired -- request a new one",
|
|
156
|
+
max_attempts: "too many attempts -- request a new code",
|
|
157
|
+
invalid: "incorrect code",
|
|
158
|
+
};
|
|
159
|
+
// Don't clear pending on invalid code -- let them retry
|
|
160
|
+
if (result.error !== "invalid") {
|
|
161
|
+
pendingCrossAuth.delete(sessionKey);
|
|
162
|
+
}
|
|
163
|
+
return jsonResult({
|
|
164
|
+
success: false,
|
|
165
|
+
error: messages[result.error] ?? "verification failed",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Update contact record with the linked identifier
|
|
169
|
+
const contact = getRecord(pending.contactId);
|
|
170
|
+
if (!contact) {
|
|
171
|
+
pendingCrossAuth.delete(sessionKey);
|
|
172
|
+
return jsonResult({ success: false, error: "contact record not found" });
|
|
173
|
+
}
|
|
174
|
+
const updateInput = {
|
|
175
|
+
name: contact.name,
|
|
176
|
+
fields: contact.fields,
|
|
177
|
+
workspace: contact.workspace,
|
|
178
|
+
};
|
|
179
|
+
if (pending.type === "email")
|
|
180
|
+
updateInput.email = pending.value;
|
|
181
|
+
else
|
|
182
|
+
updateInput.phone = pending.value;
|
|
183
|
+
const updated = setRecord(contact.id, updateInput);
|
|
184
|
+
pendingCrossAuth.delete(sessionKey);
|
|
185
|
+
return jsonResult({
|
|
186
|
+
success: true,
|
|
187
|
+
message: `${pending.type} verified and linked to contact ${updated.name}.`,
|
|
188
|
+
contact: {
|
|
189
|
+
id: updated.id,
|
|
190
|
+
name: updated.name,
|
|
191
|
+
email: updated.email,
|
|
192
|
+
phone: updated.phone,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace migrations — patches applied to deployed workspace files on gateway startup.
|
|
3
|
+
*
|
|
4
|
+
* Unlike bundled skills (which are whole-directory copies), workspace files like
|
|
5
|
+
* AGENTS.md are customised per-deployment (owner names, phone numbers, business
|
|
6
|
+
* details). Migrations patch these files in-place — checking for existing content
|
|
7
|
+
* first so they're idempotent.
|
|
8
|
+
*
|
|
9
|
+
* Each migration runs on every startup. They must be safe to re-run (no-op when
|
|
10
|
+
* the change is already present).
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
15
|
+
import { listAgentIds, resolveAgentWorkspaceDir } from "./agent-scope.js";
|
|
16
|
+
const log = createSubsystemLogger("workspace-migrations");
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/** Patterns that indicate a public-facing (non-admin) agent. */
|
|
21
|
+
const PUBLIC_AGENT_INDICATORS = [
|
|
22
|
+
"customer-facing",
|
|
23
|
+
"Public Agent",
|
|
24
|
+
"public agent",
|
|
25
|
+
"parent and student-facing",
|
|
26
|
+
"booking agent",
|
|
27
|
+
"promotional instance",
|
|
28
|
+
];
|
|
29
|
+
/** Formatting section headers commonly found at the end of AGENTS files. */
|
|
30
|
+
const FORMATTING_HEADERS = ["## WhatsApp Formatting", "## Message Formatting", "## Formatting"];
|
|
31
|
+
/** Section headers that typically precede "## Capabilities" in admin agents. */
|
|
32
|
+
const CAPABILITIES_HEADERS = ["## Capabilities", "## Stripe CLI Operations", "## User Management"];
|
|
33
|
+
function isPublicAgent(content) {
|
|
34
|
+
return PUBLIC_AGENT_INDICATORS.some((p) => content.includes(p));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find the index of a formatting-style section to insert before.
|
|
38
|
+
* Returns -1 if no known section is found.
|
|
39
|
+
*/
|
|
40
|
+
function findFormattingInsertPoint(content) {
|
|
41
|
+
for (const header of FORMATTING_HEADERS) {
|
|
42
|
+
const idx = content.lastIndexOf(header);
|
|
43
|
+
if (idx !== -1)
|
|
44
|
+
return idx;
|
|
45
|
+
}
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Find the index of a "## Capabilities" or similar section to insert before.
|
|
50
|
+
* Returns -1 if no known section is found.
|
|
51
|
+
*/
|
|
52
|
+
function findCapabilitiesInsertPoint(content) {
|
|
53
|
+
for (const header of CAPABILITIES_HEADERS) {
|
|
54
|
+
const idx = content.indexOf(header);
|
|
55
|
+
if (idx !== -1)
|
|
56
|
+
return idx;
|
|
57
|
+
}
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Insert a section into AGENTS.md content at the best position.
|
|
62
|
+
* Uses the provided finder to locate an insertion point; falls back to appending.
|
|
63
|
+
*/
|
|
64
|
+
function insertSection(content, section, findInsertPoint) {
|
|
65
|
+
const insertIdx = findInsertPoint(content);
|
|
66
|
+
if (insertIdx !== -1) {
|
|
67
|
+
return content.slice(0, insertIdx) + section + "\n\n---\n\n" + content.slice(insertIdx);
|
|
68
|
+
}
|
|
69
|
+
return content.trimEnd() + "\n\n---\n\n" + section + "\n";
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Migration: Skill Recommendations (v1.9)
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
const ADMIN_SKILL_SECTION = `## Skill Recommendations
|
|
75
|
+
|
|
76
|
+
When you notice a task that follows a repeatable pattern — something that has come up before or is likely to recur — proactively suggest creating a skill for it. Skills encode processes so they're followed consistently every time, without relying on memory or re-explanation.
|
|
77
|
+
|
|
78
|
+
**Signs a task should be a skill:**
|
|
79
|
+
- You've handled the same type of request more than once
|
|
80
|
+
- The task has clear steps that shouldn't vary between occurrences
|
|
81
|
+
- Getting it wrong has consequences (compliance, accuracy, customer experience)
|
|
82
|
+
- The process involves domain knowledge that shouldn't be guessed at
|
|
83
|
+
|
|
84
|
+
Don't wait to be asked. If you spot the pattern, recommend it.`;
|
|
85
|
+
const PUBLIC_SKILL_SECTION = `## Spotting Repeatable Patterns
|
|
86
|
+
|
|
87
|
+
When you find yourself repeatedly handling the same type of request — following the same steps, giving the same guidance, or applying the same rules — note the pattern in memory for review. Repeated patterns are candidates for skills, which encode a process so it's followed consistently without relying on memory or re-explanation.`;
|
|
88
|
+
function hasSkillSection(content) {
|
|
89
|
+
return (content.includes("## Skill Recommendations") ||
|
|
90
|
+
content.includes("## Spotting Repeatable Patterns"));
|
|
91
|
+
}
|
|
92
|
+
async function patchSkillRecommendations(agentsPath, content) {
|
|
93
|
+
if (hasSkillSection(content))
|
|
94
|
+
return null;
|
|
95
|
+
const section = isPublicAgent(content) ? PUBLIC_SKILL_SECTION : ADMIN_SKILL_SECTION;
|
|
96
|
+
return insertSection(content, section, findFormattingInsertPoint);
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Migration: Owner Learning (v1.9)
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
const OWNER_LEARNING_SECTION = `## Learning About Your Operator
|
|
102
|
+
|
|
103
|
+
You talk to the same person every day. An assistant that adapts to how someone thinks and works is more useful than one that starts fresh every session.
|
|
104
|
+
|
|
105
|
+
Pay attention to how they communicate, what they care about, how they make decisions, and what works well between you. When you notice something worth remembering — a preference, a pattern, a lesson about working with them — update \`USER.md\` or store it in \`memory/admin/\`.
|
|
106
|
+
|
|
107
|
+
This isn't a one-time setup. USER.md should grow as your understanding of them deepens: their communication style, decision-making patterns, working rhythms, what they delegate vs handle personally, and what kind of updates they actually find valuable.`;
|
|
108
|
+
function hasOwnerLearningSection(content) {
|
|
109
|
+
return content.includes("## Learning About") || content.includes("## Learning about");
|
|
110
|
+
}
|
|
111
|
+
async function patchOwnerLearning(agentsPath, content) {
|
|
112
|
+
// Only admin agents — public agents don't have USER.md or admin memory.
|
|
113
|
+
if (isPublicAgent(content))
|
|
114
|
+
return null;
|
|
115
|
+
if (hasOwnerLearningSection(content))
|
|
116
|
+
return null;
|
|
117
|
+
return insertSection(content, OWNER_LEARNING_SECTION, findCapabilitiesInsertPoint);
|
|
118
|
+
}
|
|
119
|
+
const MIGRATIONS = [
|
|
120
|
+
{ name: "skill-recommendations", apply: patchSkillRecommendations },
|
|
121
|
+
{ name: "owner-learning", apply: patchOwnerLearning },
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* Run all workspace migrations for every configured agent.
|
|
125
|
+
* Called once at gateway startup, after bundled skill sync.
|
|
126
|
+
*/
|
|
127
|
+
export async function runWorkspaceMigrations(cfg) {
|
|
128
|
+
const agentIds = listAgentIds(cfg);
|
|
129
|
+
for (const agentId of agentIds) {
|
|
130
|
+
const wsDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
131
|
+
const agentsPath = path.join(wsDir, "AGENTS.md");
|
|
132
|
+
let content;
|
|
133
|
+
try {
|
|
134
|
+
content = await fs.readFile(agentsPath, "utf-8");
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
continue; // No AGENTS.md — nothing to patch.
|
|
138
|
+
}
|
|
139
|
+
let current = content;
|
|
140
|
+
const applied = [];
|
|
141
|
+
for (const migration of MIGRATIONS) {
|
|
142
|
+
try {
|
|
143
|
+
const result = await migration.apply(agentsPath, current);
|
|
144
|
+
if (result !== null) {
|
|
145
|
+
current = result;
|
|
146
|
+
applied.push(migration.name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
log.warn(`migration "${migration.name}" failed for agent "${agentId}": ${String(err)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (applied.length > 0) {
|
|
154
|
+
try {
|
|
155
|
+
await fs.writeFile(agentsPath, current, "utf-8");
|
|
156
|
+
log.info(`patched AGENTS.md for agent "${agentId}": ${applied.join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
log.warn(`failed to write AGENTS.md for agent "${agentId}": ${String(err)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
package/dist/build-info.json
CHANGED
package/dist/config/defaults.js
CHANGED
|
@@ -321,4 +321,28 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3 = [
|
|
|
321
321
|
}
|
|
322
322
|
},
|
|
323
323
|
},
|
|
324
|
+
{
|
|
325
|
+
id: "publicChat-greeting-to-greetings-map",
|
|
326
|
+
describe: "Migrate publicChat.greeting (global string) to publicChat.greetings (per-account map)",
|
|
327
|
+
apply: (raw, changes) => {
|
|
328
|
+
const pc = getRecord(raw.publicChat);
|
|
329
|
+
if (!pc)
|
|
330
|
+
return;
|
|
331
|
+
if (typeof pc.greeting !== "string" || !pc.greeting)
|
|
332
|
+
return;
|
|
333
|
+
const text = pc.greeting;
|
|
334
|
+
delete pc.greeting;
|
|
335
|
+
// Associate the old greeting with the default workspace/account
|
|
336
|
+
const defaultId = resolveDefaultAgentIdFromRaw(raw);
|
|
337
|
+
if (defaultId) {
|
|
338
|
+
if (!isRecord(pc.greetings))
|
|
339
|
+
pc.greetings = {};
|
|
340
|
+
pc.greetings[defaultId] = text;
|
|
341
|
+
changes.push(`Migrated publicChat.greeting to publicChat.greetings["${defaultId}"].`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
changes.push("Removed orphaned publicChat.greeting (no default account to associate with).");
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
},
|
|
324
348
|
];
|
|
@@ -571,7 +571,28 @@ export const TaskmasterSchema = z
|
|
|
571
571
|
auth: z
|
|
572
572
|
.union([z.literal("anonymous"), z.literal("verified"), z.literal("choice")])
|
|
573
573
|
.optional(),
|
|
574
|
+
verifyMethods: z
|
|
575
|
+
.array(z.union([z.literal("phone"), z.literal("email")]))
|
|
576
|
+
.min(1)
|
|
577
|
+
.optional(),
|
|
574
578
|
cookieTtlDays: z.number().int().positive().optional(),
|
|
579
|
+
greeting: z.string().max(500).optional(), // deprecated — migrated to greetings map
|
|
580
|
+
greetings: z.record(z.string(), z.string().max(500)).optional(),
|
|
581
|
+
sms: z
|
|
582
|
+
.object({
|
|
583
|
+
accountSid: z.string().optional(),
|
|
584
|
+
authToken: z.string().optional(),
|
|
585
|
+
fromNumber: z.string().optional(),
|
|
586
|
+
})
|
|
587
|
+
.strict()
|
|
588
|
+
.optional(),
|
|
589
|
+
email: z
|
|
590
|
+
.object({
|
|
591
|
+
apiKey: z.string().optional(),
|
|
592
|
+
from: z.string().optional(),
|
|
593
|
+
})
|
|
594
|
+
.strict()
|
|
595
|
+
.optional(),
|
|
575
596
|
})
|
|
576
597
|
.strict()
|
|
577
598
|
.optional(),
|