@rubytech/taskmaster 1.0.35 → 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.
@@ -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
- log.info(`system.uninstall: scopes=${scopes.join(",")} purge=${purge}`);
133
- // Respond before running uninstall — the gateway will exit afterward
134
- respond(true, { ok: true, scopes, purge }, undefined);
135
- // Small delay to let the response reach the client
136
- await new Promise((r) => setTimeout(r, 500));
137
- // skipService: we cannot stop our own service — systemd would SIGTERM
138
- // this process before workspace/state removal runs. The service files
139
- // are uninstalled separately below (without stopping).
140
- try {
141
- await uninstallCommand(defaultRuntime, {
142
- state: scopes.includes("state"),
143
- workspace: scopes.includes("workspace"),
144
- app: scopes.includes("app"),
145
- purge,
146
- skipService: true,
147
- yes: true,
148
- nonInteractive: true,
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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.35",
3
+ "version": "1.0.38",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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