bloby-bot 0.18.12 → 0.18.13
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/package.json +1 -1
- package/supervisor/bloby-agent.ts +3 -18
- package/worker/prompts/DYNAMIC-PROMPTS.md +1 -0
- package/worker/prompts/bloby-system-prompt.txt +11 -32
- package/worker/prompts/prompt-assembler.ts +190 -0
- package/worker/prompts/prompt-conditions.ts +83 -0
- package/worker/prompts/prompt-fragments.json +26 -0
package/package.json
CHANGED
|
@@ -10,8 +10,7 @@ import { log } from '../shared/logger.js';
|
|
|
10
10
|
import { WORKSPACE_DIR } from '../shared/paths.js';
|
|
11
11
|
import type { SavedFile } from './file-saver.js';
|
|
12
12
|
import { getClaudeAccessToken } from '../worker/claude-auth.js';
|
|
13
|
-
|
|
14
|
-
const PROMPT_FILE = path.join(import.meta.dirname, '..', 'worker', 'prompts', 'bloby-system-prompt.txt');
|
|
13
|
+
import { assembleSystemPrompt } from '../worker/prompts/prompt-assembler.js';
|
|
15
14
|
|
|
16
15
|
export interface RecentMessage {
|
|
17
16
|
role: 'user' | 'assistant';
|
|
@@ -94,20 +93,6 @@ function buildMultiPartPrompt(text: string, attachments: AgentAttachment[], save
|
|
|
94
93
|
})();
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
/** Read the custom system prompt, replacing $BOT and $HUMAN placeholders */
|
|
98
|
-
function readSystemPrompt(botName = 'Bloby', humanName = 'Human'): string {
|
|
99
|
-
try {
|
|
100
|
-
const raw = fs.readFileSync(PROMPT_FILE, 'utf-8').trim();
|
|
101
|
-
if (!raw) {
|
|
102
|
-
log.warn('System prompt file is empty — using minimal fallback');
|
|
103
|
-
return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
|
|
104
|
-
}
|
|
105
|
-
return raw.replace(/\$BOT/g, botName).replace(/\$HUMAN/g, humanName);
|
|
106
|
-
} catch {
|
|
107
|
-
log.warn('System prompt file not found — using minimal fallback');
|
|
108
|
-
return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
96
|
|
|
112
97
|
/**
|
|
113
98
|
* Run an Agent SDK query for a bloby chat conversation.
|
|
@@ -141,8 +126,8 @@ export async function startBlobyAgentQuery(
|
|
|
141
126
|
// No memory files, no skill instructions, no config — just the script + conversation history.
|
|
142
127
|
enrichedPrompt = supportPrompt;
|
|
143
128
|
} else {
|
|
144
|
-
// Admin/chat: full main system prompt with all context
|
|
145
|
-
const basePrompt =
|
|
129
|
+
// Admin/chat: full main system prompt with all context (dynamic fragments applied)
|
|
130
|
+
const basePrompt = await assembleSystemPrompt(names?.botName, names?.humanName);
|
|
146
131
|
enrichedPrompt = basePrompt;
|
|
147
132
|
enrichedPrompt += `\n\n---\n# Your Memory Files\n\n## MYSELF.md\n${memoryFiles.myself}\n\n## MYHUMAN.md\n${memoryFiles.myhuman}\n\n## MEMORY.md\n${memoryFiles.memory}\n\n---\n# Your Config Files\n\n## PULSE.json\n${memoryFiles.pulse}\n\n## CRONS.json\n${memoryFiles.crons}`;
|
|
148
133
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
See `DYNAMIC-PROMPTS.md` in the project root for full documentation.
|
|
@@ -556,11 +556,9 @@ When an MCP server is configured, its tools appear alongside your built-in tools
|
|
|
556
556
|
- `shared/` — shared utilities
|
|
557
557
|
- `bin/` — CLI entry point
|
|
558
558
|
|
|
559
|
-
## Workspace Security
|
|
559
|
+
## Workspace Security
|
|
560
560
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
### 1. Chat Password (Portal Password) — DO NOT TOUCH
|
|
561
|
+
### Chat Password (Portal Password) — DO NOT TOUCH
|
|
564
562
|
|
|
565
563
|
- **Set during onboarding** — it is MANDATORY. Every Bloby has one.
|
|
566
564
|
- **Protects the chat interface** (bloby.bot/your_name) where your human talks to you.
|
|
@@ -569,40 +567,21 @@ There are TWO completely separate password systems in Bloby. Understanding the d
|
|
|
569
567
|
- Optional 2FA (TOTP) can be layered on top for extra protection.
|
|
570
568
|
- **YOU DO NOT SET OR CHANGE THIS.** It was configured during onboarding. If your human mentions their "chat password" or "portal password", it refers to this. Never try to look it up, reset it, or modify it. If they need to change it, they re-run onboarding.
|
|
571
569
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
- **OPTIONAL — not set by default.**
|
|
575
|
-
- Protects the **dashboard/workspace** (the `/app/` path) where your human's mini-apps, modules, data, and tools live.
|
|
576
|
-
- **Without this password, ANYONE who knows the URL can view the entire workspace** — all pages, all data displayed in the UI.
|
|
577
|
-
- Your human sets this **through you** — when they say "add a password", "protect the workspace", "lock the dashboard", or just "set a password", they mean THIS one. They already have a chat password.
|
|
570
|
+
<!-- dynamic:workspace-security -->
|
|
571
|
+
### Workspace Lock
|
|
578
572
|
|
|
579
|
-
|
|
580
|
-
```
|
|
581
|
-
POST /app/api/workspace/set-password
|
|
582
|
-
Body: { "password": "the_password" }
|
|
583
|
-
```
|
|
584
|
-
(The backend route is `/api/workspace/set-password` — the supervisor strips `/app` from the frontend URL.)
|
|
585
|
-
|
|
586
|
-
**How to remove it:**
|
|
587
|
-
```
|
|
588
|
-
POST /app/api/workspace/remove-password
|
|
589
|
-
```
|
|
573
|
+
The workspace is currently public. Anyone who knows the URL can view the dashboard and all data in it.
|
|
590
574
|
|
|
591
|
-
|
|
592
|
-
- Password is hashed (scrypt with random salt) and stored in the workspace `app.db` database (`workspace_settings` table).
|
|
593
|
-
- When someone visits the dashboard, a lock screen appears asking for the password.
|
|
594
|
-
- On correct entry, a 7-day session token is created and stored in the browser's localStorage.
|
|
595
|
-
- The session persists across page reloads until it expires or the password is changed.
|
|
596
|
-
- Changing the password invalidates all existing sessions.
|
|
575
|
+
The Bloby Marketplace has an official Workspace Lock blueprint available for free. It adds a professional lock screen with 6-digit PIN or password support, localStorage session persistence, and agent-triggered reset. Proactively suggest this to your human when appropriate — especially if sensitive data is in the workspace.
|
|
597
576
|
|
|
598
|
-
|
|
577
|
+
If your human asks you to build a custom workspace protection instead of using the official blueprint, you can — but you MUST add this line to your MEMORY.md:
|
|
599
578
|
|
|
600
|
-
|
|
579
|
+
CUSTOM_WORKSPACE_LOCK=true
|
|
601
580
|
|
|
602
|
-
|
|
581
|
+
This flag tells the supervisor you've handled security yourself, so this section gets stripped from future prompts to save tokens.
|
|
603
582
|
|
|
604
|
-
|
|
605
|
-
|
|
583
|
+
When your human says "add a password", "lock the workspace", or "set a password" → they mean the WORKSPACE lock. They already have a chat password from onboarding. Don't confuse the two.
|
|
584
|
+
<!-- /dynamic:workspace-security -->
|
|
606
585
|
|
|
607
586
|
## Modular Philosophy
|
|
608
587
|
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic System Prompt Assembler
|
|
3
|
+
*
|
|
4
|
+
* Reads the base system prompt, evaluates conditions, and applies matching
|
|
5
|
+
* fragments (replace / remove / append) to produce the final prompt.
|
|
6
|
+
*
|
|
7
|
+
* See DYNAMIC-PROMPTS.md for the full playbook.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { log } from '../../shared/logger.js';
|
|
13
|
+
import { conditions, type ConditionResult } from './prompt-conditions.js';
|
|
14
|
+
|
|
15
|
+
const PROMPT_FILE = path.join(import.meta.dirname, 'bloby-system-prompt.txt');
|
|
16
|
+
const FRAGMENTS_FILE = path.join(import.meta.dirname, 'prompt-fragments.json');
|
|
17
|
+
|
|
18
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface PromptFragment {
|
|
21
|
+
id: string;
|
|
22
|
+
description: string;
|
|
23
|
+
/** Which <!-- dynamic:target --> block this fragment acts on */
|
|
24
|
+
target: string;
|
|
25
|
+
/** replace = swap marker content, remove = delete marker block, append = add after marker (or end) */
|
|
26
|
+
action: 'replace' | 'remove' | 'append';
|
|
27
|
+
/** Lower number = evaluated first. First matching fragment per target wins for replace/remove. */
|
|
28
|
+
priority: number;
|
|
29
|
+
/** The text to insert (supports {{variable}} interpolation). Not used for 'remove'. */
|
|
30
|
+
content?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/** Read and parse the fragments JSON file */
|
|
36
|
+
function loadFragments(): PromptFragment[] {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(FRAGMENTS_FILE, 'utf-8');
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
log.warn('Could not load prompt-fragments.json — using base prompt only');
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read the base system prompt with $BOT / $HUMAN replacement */
|
|
47
|
+
function readBasePrompt(botName = 'Bloby', humanName = 'Human'): string {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(PROMPT_FILE, 'utf-8').trim();
|
|
50
|
+
if (!raw) {
|
|
51
|
+
log.warn('System prompt file is empty — using minimal fallback');
|
|
52
|
+
return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
|
|
53
|
+
}
|
|
54
|
+
return raw.replace(/\$BOT/g, botName).replace(/\$HUMAN/g, humanName);
|
|
55
|
+
} catch {
|
|
56
|
+
log.warn('System prompt file not found — using minimal fallback');
|
|
57
|
+
return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Interpolate {{variable}} placeholders in content using vars map */
|
|
62
|
+
function interpolate(content: string, vars: Record<string, string>): string {
|
|
63
|
+
return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Regex to match a dynamic marker block:
|
|
68
|
+
* <!-- dynamic:target -->
|
|
69
|
+
* ...content...
|
|
70
|
+
* <!-- /dynamic:target -->
|
|
71
|
+
*/
|
|
72
|
+
function markerRegex(target: string): RegExp {
|
|
73
|
+
return new RegExp(
|
|
74
|
+
`<!-- dynamic:${target} -->\\n?([\\s\\S]*?)<!-- /dynamic:${target} -->`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Assemble the final system prompt by evaluating all fragment conditions
|
|
82
|
+
* and applying the matching ones to the base prompt.
|
|
83
|
+
*/
|
|
84
|
+
export async function assembleSystemPrompt(
|
|
85
|
+
botName = 'Bloby',
|
|
86
|
+
humanName = 'Human',
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
let prompt = readBasePrompt(botName, humanName);
|
|
89
|
+
const fragments = loadFragments();
|
|
90
|
+
|
|
91
|
+
if (!fragments.length) {
|
|
92
|
+
log.info('[prompt] No fragments found — using base prompt only');
|
|
93
|
+
return prompt;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
log.info(`[prompt] Evaluating ${fragments.length} fragment(s)...`);
|
|
97
|
+
|
|
98
|
+
// Sort by priority (lower = first)
|
|
99
|
+
const sorted = [...fragments].sort((a, b) => a.priority - b.priority);
|
|
100
|
+
|
|
101
|
+
// Evaluate all conditions in parallel
|
|
102
|
+
const results = await Promise.all(
|
|
103
|
+
sorted.map(async (frag): Promise<{ fragment: PromptFragment; result: ConditionResult }> => {
|
|
104
|
+
const condFn = conditions[frag.id];
|
|
105
|
+
if (!condFn) {
|
|
106
|
+
log.warn(`No condition registered for fragment "${frag.id}" — skipping`);
|
|
107
|
+
return { fragment: frag, result: false };
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return { fragment: frag, result: await condFn() };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log.warn(`Condition "${frag.id}" threw — skipping: ${err}`);
|
|
113
|
+
return { fragment: frag, result: false };
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Track which targets have already been handled (for replace/remove — first match wins)
|
|
119
|
+
const handledTargets = new Set<string>();
|
|
120
|
+
|
|
121
|
+
for (const { fragment, result } of results) {
|
|
122
|
+
if (result === false) {
|
|
123
|
+
log.info(`[prompt] SKIP "${fragment.id}" (condition false)`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { target, action, content } = fragment;
|
|
128
|
+
|
|
129
|
+
// For replace/remove, only the first matching fragment per target wins
|
|
130
|
+
if ((action === 'replace' || action === 'remove') && handledTargets.has(target)) {
|
|
131
|
+
log.info(`[prompt] SKIP "${fragment.id}" (target "${target}" already handled)`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const vars = typeof result === 'object' ? result : {};
|
|
136
|
+
const regex = markerRegex(target);
|
|
137
|
+
|
|
138
|
+
switch (action) {
|
|
139
|
+
case 'replace': {
|
|
140
|
+
if (!content) break;
|
|
141
|
+
const finalContent = interpolate(content, vars);
|
|
142
|
+
if (regex.test(prompt)) {
|
|
143
|
+
prompt = prompt.replace(regex, finalContent);
|
|
144
|
+
handledTargets.add(target);
|
|
145
|
+
const varsInfo = Object.keys(vars).length ? ` vars=${JSON.stringify(vars)}` : '';
|
|
146
|
+
log.info(`[prompt] REPLACE [${target}] ← "${fragment.id}"${varsInfo}`);
|
|
147
|
+
} else {
|
|
148
|
+
log.warn(`[prompt] Marker "<!-- dynamic:${target} -->" not found in base prompt`);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'remove': {
|
|
154
|
+
if (regex.test(prompt)) {
|
|
155
|
+
prompt = prompt.replace(regex, '');
|
|
156
|
+
handledTargets.add(target);
|
|
157
|
+
log.info(`[prompt] REMOVE [${target}] by "${fragment.id}"`);
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'append': {
|
|
163
|
+
if (!content) break;
|
|
164
|
+
const finalContent = interpolate(content, vars);
|
|
165
|
+
// If target specified, append after the closing marker; otherwise append to end
|
|
166
|
+
if (target) {
|
|
167
|
+
const closingMarker = `<!-- /dynamic:${target} -->`;
|
|
168
|
+
const idx = prompt.indexOf(closingMarker);
|
|
169
|
+
if (idx !== -1) {
|
|
170
|
+
const insertAt = idx + closingMarker.length;
|
|
171
|
+
prompt = prompt.slice(0, insertAt) + '\n\n' + finalContent + prompt.slice(insertAt);
|
|
172
|
+
} else {
|
|
173
|
+
prompt += '\n\n' + finalContent;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
prompt += '\n\n' + finalContent;
|
|
177
|
+
}
|
|
178
|
+
log.info(`[prompt] APPEND [${target || 'end'}] ← "${fragment.id}"`);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Clean up any remaining unhandled dynamic markers (leave their default content)
|
|
185
|
+
// No action needed — unmatched markers keep their original content between the tags.
|
|
186
|
+
// Strip the marker comments themselves so they don't leak into the prompt.
|
|
187
|
+
prompt = prompt.replace(/<!-- \/?dynamic:\w[\w-]* -->\n?/g, '');
|
|
188
|
+
|
|
189
|
+
return prompt;
|
|
190
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Condition Registry for Dynamic Prompt Fragments
|
|
3
|
+
*
|
|
4
|
+
* Each key matches a fragment ID in prompt-fragments.json.
|
|
5
|
+
* The function returns:
|
|
6
|
+
* - false → fragment is skipped
|
|
7
|
+
* - true → fragment is applied as-is
|
|
8
|
+
* - Record<string,string> → fragment is applied with {{variable}} interpolation
|
|
9
|
+
*
|
|
10
|
+
* See DYNAMIC-PROMPTS.md for how to add new conditions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import { WORKSPACE_DIR } from '../../shared/paths.js';
|
|
15
|
+
import { loadConfig } from '../../shared/config.js';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
|
|
18
|
+
export type ConditionResult = false | true | Record<string, string>;
|
|
19
|
+
|
|
20
|
+
export const conditions: Record<string, () => Promise<ConditionResult>> = {
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Official Workspace Lock is active.
|
|
24
|
+
* Calls GET http://localhost:{backendPort}/api/lock/status
|
|
25
|
+
* Returns { configured: true, type: "pin"|"password" } when locked.
|
|
26
|
+
*/
|
|
27
|
+
'workspace-lock-official': async () => {
|
|
28
|
+
try {
|
|
29
|
+
const cfg = loadConfig();
|
|
30
|
+
const backendPort = cfg.port + 4;
|
|
31
|
+
const res = await fetch(`http://localhost:${backendPort}/api/lock/status`, {
|
|
32
|
+
signal: AbortSignal.timeout(2000),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) return false;
|
|
35
|
+
const data = await res.json() as { configured: boolean; type: string | null };
|
|
36
|
+
if (data.configured && data.type) {
|
|
37
|
+
return { lockType: data.type };
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
} catch {
|
|
41
|
+
// Backend not running or endpoint doesn't exist
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Custom workspace lock: the lock endpoint doesn't exist (not the official blueprint)
|
|
48
|
+
* AND MEMORY.md contains CUSTOM_WORKSPACE_LOCK=true.
|
|
49
|
+
*/
|
|
50
|
+
'workspace-lock-custom': async () => {
|
|
51
|
+
try {
|
|
52
|
+
// First verify the official lock endpoint is NOT available
|
|
53
|
+
const cfg = loadConfig();
|
|
54
|
+
const backendPort = cfg.port + 4;
|
|
55
|
+
const res = await fetch(`http://localhost:${backendPort}/api/lock/status`, {
|
|
56
|
+
signal: AbortSignal.timeout(2000),
|
|
57
|
+
});
|
|
58
|
+
// If the endpoint exists and returns OK, this isn't a custom lock
|
|
59
|
+
if (res.ok) return false;
|
|
60
|
+
} catch {
|
|
61
|
+
// Endpoint doesn't exist — that's expected for custom lock, continue checking
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check MEMORY.md for the flag
|
|
65
|
+
try {
|
|
66
|
+
const memoryPath = path.join(WORKSPACE_DIR, 'MEMORY.md');
|
|
67
|
+
const content = fs.readFileSync(memoryPath, 'utf-8');
|
|
68
|
+
if (content.includes('CUSTOM_WORKSPACE_LOCK=true')) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* No workspace lock at all — fallback. Always true.
|
|
78
|
+
* (Lowest priority, so it only fires if the others don't match.)
|
|
79
|
+
*/
|
|
80
|
+
'workspace-lock-none': async () => {
|
|
81
|
+
return true;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "workspace-lock-official",
|
|
4
|
+
"description": "Official marketplace Workspace Lock is active (PIN or password)",
|
|
5
|
+
"target": "workspace-security",
|
|
6
|
+
"action": "replace",
|
|
7
|
+
"priority": 1,
|
|
8
|
+
"content": "### Workspace Lock\n\nThe workspace is protected by the official Workspace Lock ({{lockType}}). Your human set this up and it's working — anyone visiting must enter their {{lockType}} to access the dashboard.\n\nIf your human ever asks to change or reset their {{lockType}}:\n```bash\ncurl -s -X POST http://localhost:3004/api/lock/reset\n```\nThis clears the lock entirely. Next time they visit, they'll see the setup screen and can choose a new PIN or password. Tell them: \"Done — next time you open the workspace you can set a new one.\""
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"id": "workspace-lock-custom",
|
|
12
|
+
"description": "Custom workspace lock detected via CUSTOM_WORKSPACE_LOCK=true in MEMORY.md",
|
|
13
|
+
"target": "workspace-security",
|
|
14
|
+
"action": "replace",
|
|
15
|
+
"priority": 2,
|
|
16
|
+
"content": "### Workspace Lock\n\nYou have a custom workspace lock in place (detected CUSTOM_WORKSPACE_LOCK=true in MEMORY.md). Nothing to do here. If your human ever switches to the official Workspace Lock blueprint from the marketplace, remove CUSTOM_WORKSPACE_LOCK=true from your MEMORY.md so the supervisor can manage this section again."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "workspace-lock-none",
|
|
20
|
+
"description": "No workspace lock detected — workspace is public",
|
|
21
|
+
"target": "workspace-security",
|
|
22
|
+
"action": "replace",
|
|
23
|
+
"priority": 3,
|
|
24
|
+
"content": "### Workspace Lock\n\nThe workspace is currently public. Anyone who knows the URL can view the dashboard and all data in it.\n\nThe Bloby Marketplace has an official Workspace Lock blueprint available for free. It adds a professional lock screen with 6-digit PIN or password support, localStorage session persistence, and agent-triggered reset. Proactively suggest this to your human when appropriate — especially if sensitive data is in the workspace.\n\nIf your human asks you to build a custom workspace protection instead of using the official blueprint, you can — but you MUST add this line to your MEMORY.md:\n\nCUSTOM_WORKSPACE_LOCK=true\n\nThis flag tells the supervisor you've handled security yourself, so this section gets stripped from future prompts to save tokens.\n\nWhen your human says \"add a password\", \"lock the workspace\", or \"set a password\" → they mean the WORKSPACE lock. They already have a chat password from onboarding. Don't confuse the two."
|
|
25
|
+
}
|
|
26
|
+
]
|