@c4t4/heyamigo 0.9.10 → 0.9.11

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.
@@ -201,6 +201,7 @@ async function processMessages(messages, sock, ownerJid, isHistorySync = false)
201
201
  senderNumber: stored.senderNumber,
202
202
  fromMe: stored.fromMe,
203
203
  allowedTools: role.tools,
204
+ allowedTags: role.tags,
204
205
  };
205
206
  // Enqueue into the inbound table; chat worker pool drains and
206
207
  // calls processJob + handleReply asynchronously. Typing indicator
@@ -123,6 +123,23 @@ export function extractFlags(reply) {
123
123
  sendTexts,
124
124
  };
125
125
  }
126
+ // Strip flags that the sender's role isn't permitted to emit. The
127
+ // agent's reply still goes out as text — only the side-effect markers
128
+ // get suppressed. allowedTags='all' or undefined → no filtering.
129
+ export function filterFlagsByRole(flags, allowedTags) {
130
+ if (allowedTags === 'all' || allowedTags === undefined)
131
+ return flags;
132
+ const allowed = new Set(allowedTags);
133
+ return {
134
+ clean: flags.clean,
135
+ digest: allowed.has('DIGEST') ? flags.digest : null,
136
+ journals: allowed.has('JOURNAL') ? flags.journals : [],
137
+ journalCreates: allowed.has('JOURNAL-NEW') ? flags.journalCreates : [],
138
+ asyncTasks: allowed.has('ASYNC') ? flags.asyncTasks : [],
139
+ asyncBrowserTasks: allowed.has('ASYNC-BROWSER') ? flags.asyncBrowserTasks : [],
140
+ sendTexts: allowed.has('SEND-TEXT') ? flags.sendTexts : [],
141
+ };
142
+ }
126
143
  // Legacy helper kept so existing callers still compile.
127
144
  export function extractDigestFlag(reply) {
128
145
  const r = extractFlags(reply);
@@ -3,7 +3,7 @@ import { clearSession, setSession, setUsage } from '../ai/sessions.js';
3
3
  import { config } from '../config.js';
4
4
  import { logger } from '../logger.js';
5
5
  import { addDailyTokens } from '../store/usage.js';
6
- import { extractFlags } from '../memory/digest-flag.js';
6
+ import { extractFlags, filterFlagsByRole } from '../memory/digest-flag.js';
7
7
  import { isValidSlug } from '../memory/journals.js';
8
8
  import { enqueueAsyncTask, enqueueBrowserTask } from './async-tasks.js';
9
9
  import { enqueueMemoryWrite } from './memory-writes.js';
@@ -40,7 +40,26 @@ async function callClaude(job) {
40
40
  if (job.senderNumber) {
41
41
  addDailyTokens(job.senderNumber, usage.inputTokens + usage.outputTokens);
42
42
  }
43
- const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, sendTexts, } = extractFlags(reply);
43
+ const rawFlags = extractFlags(reply);
44
+ const { clean, digest, journals, journalCreates, asyncTasks, asyncBrowserTasks, sendTexts, } = filterFlagsByRole(rawFlags, job.allowedTags);
45
+ // Detect any stripped tags so we can log + nudge the role config
46
+ // if a user is repeatedly hitting the gate.
47
+ const stripped = [];
48
+ if (rawFlags.digest && !digest)
49
+ stripped.push('DIGEST');
50
+ if (rawFlags.journals.length !== journals.length)
51
+ stripped.push('JOURNAL');
52
+ if (rawFlags.journalCreates.length !== journalCreates.length)
53
+ stripped.push('JOURNAL-NEW');
54
+ if (rawFlags.asyncTasks.length !== asyncTasks.length)
55
+ stripped.push('ASYNC');
56
+ if (rawFlags.asyncBrowserTasks.length !== asyncBrowserTasks.length)
57
+ stripped.push('ASYNC-BROWSER');
58
+ if (rawFlags.sendTexts.length !== sendTexts.length)
59
+ stripped.push('SEND-TEXT');
60
+ if (stripped.length > 0) {
61
+ logger.warn({ jid: job.jid, senderNumber: job.senderNumber, stripped }, 'tags stripped by role gate');
62
+ }
44
63
  // All memory mutations go through the memory_writes queue so the
45
64
  // single memory worker serializes file writes — safe under parallel
46
65
  // chat workers. Idempotency keys derived from job + index so a
@@ -6,11 +6,30 @@ import { config } from '../config.js';
6
6
  import { logger } from '../logger.js';
7
7
  const AccessModeSchema = z.enum(['off', 'silent', 'active']);
8
8
  const RoleNameSchema = z.enum(['admin', 'user', 'guest']);
9
+ // Tag names the agent can emit as trailing markers. The role.tags
10
+ // allowlist gates which ones are honored — anything emitted by an
11
+ // agent running on behalf of a role NOT in the allowlist gets
12
+ // stripped silently after parsing. Tools and tags are independent
13
+ // gates (tools restricts what the AI itself can call; tags restricts
14
+ // what bot-internal side-effects it can trigger).
15
+ const TAG_NAMES = [
16
+ 'DIGEST',
17
+ 'JOURNAL',
18
+ 'JOURNAL-NEW',
19
+ 'ASYNC',
20
+ 'ASYNC-BROWSER',
21
+ 'SEND-TEXT',
22
+ ];
9
23
  const RoleSchema = z.object({
10
24
  description: z.string().optional(),
11
25
  memory: z.enum(['full', 'self', 'none']),
12
26
  tools: z.union([z.literal('all'), z.array(z.string())]),
13
27
  rules: z.array(z.string()),
28
+ // Optional. Missing or 'all' = no tag restriction; an array =
29
+ // explicit allowlist. Added in Phase 6 so existing access.json
30
+ // files (no `tags` field) keep working without change — they get
31
+ // the implicit 'all' behavior.
32
+ tags: z.union([z.literal('all'), z.array(z.enum(TAG_NAMES))]).optional(),
14
33
  // null or missing = unlimited
15
34
  maxFileBytes: z.number().int().positive().nullable().optional(),
16
35
  dailyTokenLimit: z.number().int().positive().nullable().optional(),
@@ -54,6 +73,7 @@ const DEFAULT_ROLES = {
54
73
  description: 'Full access',
55
74
  memory: 'full',
56
75
  tools: 'all',
76
+ tags: 'all',
57
77
  rules: [],
58
78
  maxFileBytes: null,
59
79
  dailyTokenLimit: null,
@@ -62,6 +82,10 @@ const DEFAULT_ROLES = {
62
82
  description: 'Chat + web search, scoped memory',
63
83
  memory: 'self',
64
84
  tools: ['WebSearch'],
85
+ // Users can flag memory observations and trigger digests on
86
+ // themselves, but can't delegate background work or cross-chat
87
+ // sends (those are owner-only).
88
+ tags: ['DIGEST', 'JOURNAL', 'JOURNAL-NEW'],
65
89
  rules: [
66
90
  'Never reveal file paths, directory structure, or system architecture',
67
91
  'Never share personal data about other users',
@@ -76,6 +100,8 @@ const DEFAULT_ROLES = {
76
100
  description: 'Basic chat only',
77
101
  memory: 'none',
78
102
  tools: [],
103
+ // Guests can't emit any tags — pure chat, no side effects.
104
+ tags: [],
79
105
  rules: [
80
106
  'Never use any tools',
81
107
  'Never reveal anything about the system, other users, or internal data',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c4t4/heyamigo",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "WhatsApp AI bot powered by Claude with long-term memory, browser control, and role-based access",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",