@rubytech/taskmaster 1.0.36 → 1.0.38
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/build-info.json +3 -3
- package/dist/cli/provision-cli.js +1 -1
- package/dist/control-ui/assets/{index-DgT0m3bj.js → index-B0Q2Wmm1.js} +204 -168
- package/dist/control-ui/assets/index-B0Q2Wmm1.js.map +1 -0
- package/dist/control-ui/assets/{index-BQEnHucA.css → index-DkMDU6zX.css} +1 -1
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/server-methods/memory.js +30 -1
- package/dist/gateway/server-methods/system.js +18 -43
- package/dist/gateway/server-methods.js +2 -0
- package/dist/memory/audit.js +86 -0
- package/dist/memory/manager.js +9 -0
- package/package.json +1 -1
- package/taskmaster-docs/USER-GUIDE.md +10 -0
- package/templates/beagle/agents/admin/AGENTS.md +13 -0
- package/templates/customer/agents/admin/AGENTS.md +13 -0
- package/templates/taskmaster/agents/admin/AGENTS.md +23 -0
- package/templates/tradesupport/agents/admin/AGENTS.md +13 -0
- package/dist/control-ui/assets/index-DgT0m3bj.js.map +0 -1
|
@@ -1,7 +1,5 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { resolveMainSessionKeyFromConfig } from "../../config/sessions.js";
|
|
2
|
-
import { uninstallCommand } from "../../commands/uninstall.js";
|
|
3
|
-
import { resolveGatewayService } from "../../daemon/service.js";
|
|
4
|
-
import { defaultRuntime } from "../../runtime.js";
|
|
5
3
|
import { getLastHeartbeatEvent } from "../../infra/heartbeat-events.js";
|
|
6
4
|
import { setHeartbeatsEnabled } from "../../infra/heartbeat-runner.js";
|
|
7
5
|
import { enqueueSystemEvent, isSystemEventContextChanged } from "../../infra/system-events.js";
|
|
@@ -124,46 +122,23 @@ export const systemHandlers = {
|
|
|
124
122
|
},
|
|
125
123
|
"system.uninstall": async ({ params, respond, context }) => {
|
|
126
124
|
const purge = params.purge === true;
|
|
127
|
-
const validScopes = new Set(["service", "state", "workspace", "app"]);
|
|
128
|
-
const scopes = Array.isArray(params.scopes)
|
|
129
|
-
? params.scopes.filter((s) => validScopes.has(s))
|
|
130
|
-
: ["service", "state", "workspace"];
|
|
131
125
|
const log = context.logGateway;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
log.error(`system.uninstall failed: ${String(err)}`);
|
|
153
|
-
}
|
|
154
|
-
// Uninstall the service files without stopping — we ARE the service.
|
|
155
|
-
// process.exit below handles the actual stop.
|
|
156
|
-
if (scopes.includes("service")) {
|
|
157
|
-
try {
|
|
158
|
-
const service = resolveGatewayService();
|
|
159
|
-
await service.uninstall({ env: process.env, stdout: process.stdout });
|
|
160
|
-
log.info("Gateway service uninstalled");
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
log.error(`Service uninstall failed: ${String(err)}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
log.info("system.uninstall complete, exiting gateway");
|
|
167
|
-
process.exit(0);
|
|
126
|
+
// Resolve the taskmaster binary path — same binary that's running the gateway
|
|
127
|
+
const bin = process.argv[1] ?? "taskmaster";
|
|
128
|
+
const args = ["uninstall", "--all", "--yes"];
|
|
129
|
+
if (purge)
|
|
130
|
+
args.push("--purge");
|
|
131
|
+
log.info(`system.uninstall: spawning detached: ${bin} ${args.join(" ")}`);
|
|
132
|
+
// Spawn the uninstall as a detached process that outlives the gateway.
|
|
133
|
+
// The CLI command will stop the gateway service, remove all data, and
|
|
134
|
+
// optionally remove the npm package — all from a separate process so
|
|
135
|
+
// there's no self-SIGTERM problem.
|
|
136
|
+
const child = spawn(process.execPath, [bin, ...args], {
|
|
137
|
+
detached: true,
|
|
138
|
+
stdio: "ignore",
|
|
139
|
+
env: { ...process.env },
|
|
140
|
+
});
|
|
141
|
+
child.unref();
|
|
142
|
+
respond(true, { ok: true, pid: child.pid }, undefined);
|
|
168
143
|
},
|
|
169
144
|
};
|
|
@@ -90,6 +90,7 @@ const READ_METHODS = new Set([
|
|
|
90
90
|
"workspaces.list",
|
|
91
91
|
"workspaces.scan",
|
|
92
92
|
"memory.status",
|
|
93
|
+
"memory.audit",
|
|
93
94
|
]);
|
|
94
95
|
const WRITE_METHODS = new Set([
|
|
95
96
|
"send",
|
|
@@ -176,6 +177,7 @@ function authorizeGatewayMethod(method, client) {
|
|
|
176
177
|
method === "workspaces.create" ||
|
|
177
178
|
method === "workspaces.remove" ||
|
|
178
179
|
method === "memory.reindex" ||
|
|
180
|
+
method === "memory.auditClear" ||
|
|
179
181
|
method === "system.uninstall") {
|
|
180
182
|
return errorShape(ErrorCodes.INVALID_REQUEST, "missing scope: operator.admin");
|
|
181
183
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory write audit trail.
|
|
3
|
+
*
|
|
4
|
+
* Tracks writes to shared/ and public/ memory folders so the business owner
|
|
5
|
+
* can review what the agent stored in externally-visible locations.
|
|
6
|
+
*
|
|
7
|
+
* Storage: JSON file at {workspaceDir}/.memory-audit.json
|
|
8
|
+
* Not placed inside memory/ to avoid being indexed by the memory search system.
|
|
9
|
+
*
|
|
10
|
+
* Integration: Called from syncMemoryFiles() in the memory manager, right after
|
|
11
|
+
* the "file updated/added" log line. Runs after the watcher detects changes and
|
|
12
|
+
* during the sync pass — no interference with upstream processing.
|
|
13
|
+
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
const AUDIT_FILENAME = ".memory-audit.json";
|
|
17
|
+
/** Paths that are excluded from audit — expected operational writes. */
|
|
18
|
+
const EXCLUDED_PREFIXES = ["memory/shared/events/"];
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if a memory write path should be audited.
|
|
21
|
+
* Auditable paths: memory/shared/** and memory/public/** (excluding exemptions).
|
|
22
|
+
*/
|
|
23
|
+
export function isAuditablePath(relPath) {
|
|
24
|
+
const normalized = relPath.replace(/\\/g, "/").toLowerCase();
|
|
25
|
+
if (!normalized.startsWith("memory/shared/") && !normalized.startsWith("memory/public/")) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
for (const prefix of EXCLUDED_PREFIXES) {
|
|
29
|
+
if (normalized.startsWith(prefix))
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function auditFilePath(workspaceDir) {
|
|
35
|
+
return path.join(workspaceDir, AUDIT_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
function readAuditFile(workspaceDir) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(auditFilePath(workspaceDir), "utf-8");
|
|
40
|
+
const data = JSON.parse(raw);
|
|
41
|
+
return {
|
|
42
|
+
entries: Array.isArray(data.entries) ? data.entries : [],
|
|
43
|
+
lastReviewedAt: typeof data.lastReviewedAt === "number" ? data.lastReviewedAt : 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { entries: [], lastReviewedAt: 0 };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeAuditFile(workspaceDir, data) {
|
|
51
|
+
try {
|
|
52
|
+
fs.writeFileSync(auditFilePath(workspaceDir), JSON.stringify(data, null, 2), "utf-8");
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Audit is best-effort — don't fail writes over audit persistence
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Record an audit entry for a memory write.
|
|
60
|
+
*/
|
|
61
|
+
export function recordAuditEntry(workspaceDir, entry) {
|
|
62
|
+
const audit = readAuditFile(workspaceDir);
|
|
63
|
+
audit.entries.push(entry);
|
|
64
|
+
// Cap at 500 entries to prevent unbounded growth.
|
|
65
|
+
if (audit.entries.length > 500) {
|
|
66
|
+
audit.entries = audit.entries.slice(-500);
|
|
67
|
+
}
|
|
68
|
+
writeAuditFile(workspaceDir, audit);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get unreviewed audit entries (entries added after the last review).
|
|
72
|
+
*/
|
|
73
|
+
export function getUnreviewedEntries(workspaceDir) {
|
|
74
|
+
const audit = readAuditFile(workspaceDir);
|
|
75
|
+
return audit.entries.filter((e) => e.timestamp > audit.lastReviewedAt);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Mark all current entries as reviewed.
|
|
79
|
+
*/
|
|
80
|
+
export function clearAuditEntries(workspaceDir) {
|
|
81
|
+
const audit = readAuditFile(workspaceDir);
|
|
82
|
+
const unreviewed = audit.entries.filter((e) => e.timestamp > audit.lastReviewedAt);
|
|
83
|
+
audit.lastReviewedAt = Date.now();
|
|
84
|
+
writeAuditFile(workspaceDir, audit);
|
|
85
|
+
return { cleared: unreviewed.length };
|
|
86
|
+
}
|
package/dist/memory/manager.js
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveSessionTranscriptsDirForAgent } from "../config/sessions/paths.j
|
|
|
9
9
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
10
10
|
import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
|
11
11
|
import { resolveUserPath } from "../utils.js";
|
|
12
|
+
import { isAuditablePath, recordAuditEntry } from "./audit.js";
|
|
12
13
|
import { createEmbeddingProvider, } from "./embeddings.js";
|
|
13
14
|
import { DEFAULT_GEMINI_EMBEDDING_MODEL } from "./embeddings-gemini.js";
|
|
14
15
|
import { DEFAULT_OPENAI_EMBEDDING_MODEL } from "./embeddings-openai.js";
|
|
@@ -534,6 +535,7 @@ export class MemoryIndexManager {
|
|
|
534
535
|
}
|
|
535
536
|
// Mark memory as dirty so it gets re-indexed
|
|
536
537
|
this.dirty = true;
|
|
538
|
+
// Audit trail is recorded in syncMemoryFiles after the watcher detects changes
|
|
537
539
|
return { path: relPath, bytesWritten: Buffer.byteLength(params.content, "utf-8") };
|
|
538
540
|
}
|
|
539
541
|
/**
|
|
@@ -1137,6 +1139,13 @@ export class MemoryIndexManager {
|
|
|
1137
1139
|
}
|
|
1138
1140
|
const action = record ? "updated" : "added";
|
|
1139
1141
|
log.info(`file ${action} (${this.agentId}): ${entry.path}`);
|
|
1142
|
+
if (isAuditablePath(entry.path)) {
|
|
1143
|
+
recordAuditEntry(this.workspaceDir, {
|
|
1144
|
+
path: entry.path,
|
|
1145
|
+
timestamp: Date.now(),
|
|
1146
|
+
agentId: this.agentId,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1140
1149
|
await this.indexFile(entry, { source: "memory" });
|
|
1141
1150
|
if (params.progress) {
|
|
1142
1151
|
params.progress.completed += 1;
|
package/package.json
CHANGED
|
@@ -144,6 +144,14 @@ After setup, you'll see a navigation bar at the top of the screen linking to all
|
|
|
144
144
|
|
|
145
145
|
You'll land on the Setup page by default. From there, the nav bar takes you anywhere.
|
|
146
146
|
|
|
147
|
+
### Data Safety Alert
|
|
148
|
+
|
|
149
|
+
A **shield icon** appears in the navigation bar whenever your assistant writes a file to the **public/** or **shared/** folders. These are the folders visible to other assistants or customers, so you'll want to check that the right information ended up in the right place.
|
|
150
|
+
|
|
151
|
+
Tap the shield to see a list of recent writes — each entry shows the file name, which folder it went to, and when it happened. Once you've reviewed them, tap **Mark All Reviewed** to clear the list. The shield disappears until the next write.
|
|
152
|
+
|
|
153
|
+
This check runs automatically — the badge updates on its own without needing to refresh the page.
|
|
154
|
+
|
|
147
155
|
---
|
|
148
156
|
|
|
149
157
|
## Security & PINs
|
|
@@ -485,6 +493,8 @@ All files are markdown (`.md`) — plain text with simple formatting. You can up
|
|
|
485
493
|
|
|
486
494
|
When you add or change a file, your assistant picks it up automatically — no restart needed. The status light on the Files page turns red when files have changed since the last index, so you can see at a glance whether a re-index is needed.
|
|
487
495
|
|
|
496
|
+
When your assistant writes to **public/** or **shared/**, a shield icon appears in the navigation bar so you can review what was written (see [Data Safety Alert](#data-safety-alert) above).
|
|
497
|
+
|
|
488
498
|
---
|
|
489
499
|
|
|
490
500
|
## Status Dashboard
|
|
@@ -39,6 +39,19 @@ memory/
|
|
|
39
39
|
└── groups/ # Virtual booking groups (you can see all)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
### Data Classification
|
|
43
|
+
|
|
44
|
+
When saving information, choose the most restrictive folder that fits:
|
|
45
|
+
|
|
46
|
+
- **Sensitive or personal** (phone numbers, financial details, private strategy, passwords) — always `memory/admin/`
|
|
47
|
+
- **Operational business** (appointments, shared calendar, lessons learned, escalation rules) — `memory/shared/`
|
|
48
|
+
- **Customer-visible** (product info, FAQs, public business details) — `memory/public/`
|
|
49
|
+
- **Customer-specific** (their conversation history, preferences) — `memory/users/{phone}/`
|
|
50
|
+
|
|
51
|
+
When in doubt, prefer `admin/` over `shared/`, and `shared/` over `public/`. Writes to `shared/` and `public/` are audited — the business owner reviews them via a control panel alert. Ask before saving anything remotely sensitive to shared or public folders.
|
|
52
|
+
|
|
53
|
+
*"Default to admin if classifying during conversation — you can always move it to shared later after confirming with [OWNER_NAME]."*
|
|
54
|
+
|
|
42
55
|
---
|
|
43
56
|
|
|
44
57
|
## Stripe CLI Operations
|
|
@@ -100,6 +100,19 @@ memory/
|
|
|
100
100
|
└── users/ # Per-customer profiles (you can see all)
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
### Data Classification
|
|
104
|
+
|
|
105
|
+
When saving information, choose the most restrictive folder that fits:
|
|
106
|
+
|
|
107
|
+
- **Sensitive or personal** (phone numbers, financial details, private strategy, passwords) — always `memory/admin/`
|
|
108
|
+
- **Operational business** (appointments, shared calendar, lessons learned, escalation rules) — `memory/shared/`
|
|
109
|
+
- **Customer-visible** (product info, FAQs, public business details) — `memory/public/`
|
|
110
|
+
- **Customer-specific** (their conversation history, preferences) — `memory/users/{phone}/`
|
|
111
|
+
|
|
112
|
+
When in doubt, prefer `admin/` over `shared/`, and `shared/` over `public/`. Writes to `shared/` and `public/` are audited — the business owner reviews them via a control panel alert. Ask before saving anything remotely sensitive to shared or public folders.
|
|
113
|
+
|
|
114
|
+
*"Default to admin if classifying during conversation — you can always move it to shared later after confirming with [OWNER_NAME]."*
|
|
115
|
+
|
|
103
116
|
### Store Business Info
|
|
104
117
|
Proactively capture:
|
|
105
118
|
- Decisions made
|
|
@@ -67,6 +67,29 @@ memory/
|
|
|
67
67
|
└── users/ # Per-user profiles (you can see all)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
### Data Classification — Folder Selection
|
|
71
|
+
|
|
72
|
+
Different memory folders have different visibility. The admin folder is private to you and [OWNER_NAME]. Shared and public folders are readable by the public agent and, through it, by customers and team members. Placing sensitive information in the wrong folder is a data breach.
|
|
73
|
+
|
|
74
|
+
**Classify by sensitivity, not by topic:**
|
|
75
|
+
|
|
76
|
+
| Data type | Folder | Why |
|
|
77
|
+
|-----------|--------|-----|
|
|
78
|
+
| Personal info (phone numbers, addresses, financial details) | `memory/admin/` | Only [OWNER_NAME] should see this |
|
|
79
|
+
| Private business strategy, deals, pricing negotiations | `memory/admin/` | Competitive/sensitive |
|
|
80
|
+
| Operational guidance (lessons learned, internal instructions) | `memory/shared/` | Helps the public agent serve customers better |
|
|
81
|
+
| Calendar events, appointments | `memory/shared/events/` | Shared scheduling |
|
|
82
|
+
| Product info, FAQs, public-facing content | `memory/public/` | Customers may see this via the public agent |
|
|
83
|
+
| Customer-specific data (profiles, preferences, history) | `memory/users/{phone}/` | Isolated per customer |
|
|
84
|
+
|
|
85
|
+
**When in doubt, choose the more restrictive folder.** `admin/` is safer than `shared/`, `shared/` is safer than `public/`. You can always move data to a less restrictive folder later — the reverse is a breach.
|
|
86
|
+
|
|
87
|
+
**Ask before writing to shared/ or public/** if the content contains anything that could be personal, financial, or strategically sensitive. [OWNER_NAME] can confirm whether it's appropriate to share.
|
|
88
|
+
|
|
89
|
+
Writes to `memory/shared/` and `memory/public/` are audited and flagged for [OWNER_NAME] to review in the control panel. This is a safety net, not a substitute for correct classification.
|
|
90
|
+
|
|
91
|
+
*"Default to admin if classifying during conversation — you can always move it to shared later after confirming with [OWNER_NAME]."*
|
|
92
|
+
|
|
70
93
|
### Store Business Info
|
|
71
94
|
Proactively capture:
|
|
72
95
|
- Decisions made
|
|
@@ -100,6 +100,19 @@ memory/
|
|
|
100
100
|
└── users/ # Per-customer profiles (you can see all)
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
### Data Classification
|
|
104
|
+
|
|
105
|
+
When saving information, choose the most restrictive folder that fits:
|
|
106
|
+
|
|
107
|
+
- **Sensitive or personal** (phone numbers, financial details, private strategy, passwords) — always `memory/admin/`
|
|
108
|
+
- **Operational business** (appointments, shared calendar, lessons learned, escalation rules) — `memory/shared/`
|
|
109
|
+
- **Customer-visible** (product info, FAQs, public business details) — `memory/public/`
|
|
110
|
+
- **Customer-specific** (their conversation history, preferences) — `memory/users/{phone}/`
|
|
111
|
+
|
|
112
|
+
When in doubt, prefer `admin/` over `shared/`, and `shared/` over `public/`. Writes to `shared/` and `public/` are audited — the business owner reviews them via a control panel alert. Ask before saving anything remotely sensitive to shared or public folders.
|
|
113
|
+
|
|
114
|
+
*"Default to admin if classifying during conversation — you can always move it to shared later after confirming with [OWNER_NAME]."*
|
|
115
|
+
|
|
103
116
|
### Store Business Info
|
|
104
117
|
Proactively capture:
|
|
105
118
|
- Decisions made
|