pi-vault-mind 0.16.28 → 0.16.29
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/CHANGELOG.md +12 -0
- package/README.md +1 -0
- package/dist/src/commands.js +81 -0
- package/dist/src/identities-config.js +20 -4
- package/dist/src/identity-injector.d.ts +13 -2
- package/dist/src/identity-injector.js +42 -28
- package/dist/src/scaffold.js +29 -29
- package/dist/src/server.js +21 -12
- package/dist/src/types.d.ts +8 -0
- package/dist/src/utils.js +15 -0
- package/dist/test/commands.test.js +78 -0
- package/dist/test/identity-injector.test.js +86 -148
- package/dist/test/rest-vm.test.js +24 -0
- package/dist/test/utils.test.js +13 -0
- package/package.json +1 -1
- package/skills/.agents/broadcaster.agent.md +0 -31
- package/skills/.agents/heavy-lifter.agent.md +0 -31
- package/skills/.agents/manager.agent.md +0 -30
- package/skills/.agents/miner.agent.md +0 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.29 — 2026-08-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Main-terminal identity isolation.** Ambient bundled specialist skills no longer make the interactive Pi session inherit the first matching specialist boundary. Identity injection now defaults to the unrestricted main agent and only activates from an explicit vault or environment selection.
|
|
8
|
+
- **Explicit persona controls.** Added `/vm identity status | set <role> | on | off`, `VAULT_MIND_AGENT_ROLE`, and `VAULT_MIND_IDENTITY_INJECTION`, with canonical persistence under `vaultMind.identities`.
|
|
9
|
+
- **Identity configuration migration and write safety.** Legacy top-level identity settings migrate without losing role allowlists, new scaffolds use the canonical path, and malformed JSON is reported without overwriting the existing vault configuration or leaking an unhandled HTTP rejection.
|
|
10
|
+
|
|
11
|
+
### Verification
|
|
12
|
+
|
|
13
|
+
- Root and Obsidian builds passed; full test suite: 899/899 passing.
|
|
14
|
+
- Real-vault startup probe confirmed ambient Broadcaster resources keep the unrestricted main identity while an explicit Broadcaster override activates the boundary.
|
|
3
15
|
|
|
4
16
|
## 0.16.28 — 2026-08-09
|
|
5
17
|
|
package/README.md
CHANGED
|
@@ -356,6 +356,7 @@ Edit `<vault>/.vault-mind/vault-mind.config.json` to match your domain:
|
|
|
356
356
|
| `/vm injector create` | Interactive wizard to create a new injector |
|
|
357
357
|
| `/vm context enable \| disable \| status` | Manage pi-context integration |
|
|
358
358
|
| `/vm embedding status \| use \| model \| models \| pull` | Manage embedding provider |
|
|
359
|
+
| `/vm identity status \| set <role> \| on \| off` | Inspect or explicitly control the interactive agent persona boundary |
|
|
359
360
|
| `/vm remote status \| config \| sync \| jobs \| migrate` | Manage remote embedding + vector sync |
|
|
360
361
|
| `/vm watcher start \| stop \| status` | Manage the passive file watcher |
|
|
361
362
|
| `/vm server status` | Show HTTP server status, port, and uptime |
|
package/dist/src/commands.js
CHANGED
|
@@ -8,6 +8,8 @@ import { revokeToken, rotateToken } from "./auth.js";
|
|
|
8
8
|
import { handleDiscoverSchema } from "./discover-schema.js";
|
|
9
9
|
import { writeEnvEntry } from "./embedding-secrets.js";
|
|
10
10
|
import { queryGraph } from "./graph.js";
|
|
11
|
+
import { writeIdentitiesLayer } from "./identities-config.js";
|
|
12
|
+
import { IDENTITY_BOUNDARY_ROLES, resolveIdentityBoundaryRole } from "./identity-injector.js";
|
|
11
13
|
import { runAskIntake } from "./intake.js";
|
|
12
14
|
import { connect, searchFts, searchHybrid, searchHybridRanked } from "./lance.js";
|
|
13
15
|
import { createModalClient, isModalConfigured, MODAL_TOKEN_ENV, modalTokenEnvPath, resolveModalToken, } from "./modal-config.js";
|
|
@@ -39,6 +41,9 @@ const VM_USAGE = [
|
|
|
39
41
|
" /vm index [--all] [--reembed] [--remote] Index collections (alias: reindex)",
|
|
40
42
|
" /vm discover-schema Infer schema from a JSONL file",
|
|
41
43
|
" /vm injector create Create a new injector (wizard)",
|
|
44
|
+
" /vm identity status Show configured and effective persona",
|
|
45
|
+
" /vm identity set <role> Set main|manager|miner|broadcaster|heavy-lifter",
|
|
46
|
+
" /vm identity on|off Enable or disable persona boundary injection",
|
|
42
47
|
" /vm context enable|disable Enable/disable pi-context integration",
|
|
43
48
|
" /vm embedding status Show embedding config",
|
|
44
49
|
" /vm remote status Show remote provider config + health + collections",
|
|
@@ -1616,6 +1621,74 @@ const handleToken = (args, ctx) => {
|
|
|
1616
1621
|
ctx.ui.notify(`Unknown: ${sub}. Try: show, rotate, revoke`, "error");
|
|
1617
1622
|
}
|
|
1618
1623
|
};
|
|
1624
|
+
const handleIdentity = (args, ctx) => {
|
|
1625
|
+
const parts = args.trim().split(/\s+/g).filter(Boolean);
|
|
1626
|
+
const subcommand = parts[0]?.toLowerCase() || "status";
|
|
1627
|
+
const identities = loadConfig(ctx.cwd).vaultMind.identities;
|
|
1628
|
+
const configuredRole = identities?.activeRole?.trim().toLowerCase() || "main";
|
|
1629
|
+
const updateIdentity = (patch) => {
|
|
1630
|
+
try {
|
|
1631
|
+
writeIdentitiesLayer(ctx.cwd, patch);
|
|
1632
|
+
return true;
|
|
1633
|
+
}
|
|
1634
|
+
catch (error) {
|
|
1635
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1636
|
+
ctx.ui.notify(`Failed to update identity config: ${message}`, "error");
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
switch (subcommand) {
|
|
1641
|
+
case "status": {
|
|
1642
|
+
const effectiveRole = resolveIdentityBoundaryRole(ctx.cwd);
|
|
1643
|
+
const injectionEnabled = identities?.injectBoundaries !== false;
|
|
1644
|
+
const envRole = process.env.VAULT_MIND_AGENT_ROLE?.trim();
|
|
1645
|
+
const envInjection = process.env.VAULT_MIND_IDENTITY_INJECTION?.trim();
|
|
1646
|
+
ctx.ui.notify([
|
|
1647
|
+
"**Identity Boundary**",
|
|
1648
|
+
`Persona: ${configuredRole}`,
|
|
1649
|
+
`Injection: ${injectionEnabled ? "enabled" : "disabled"}`,
|
|
1650
|
+
`Effective boundary: ${effectiveRole ?? "none (main agent)"}`,
|
|
1651
|
+
...(envRole ? [`Environment role override: ${envRole}`] : []),
|
|
1652
|
+
...(envInjection ? [`Environment injection override: ${envInjection}`] : []),
|
|
1653
|
+
].join("\n"), "info");
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
case "set": {
|
|
1657
|
+
const role = parts[1]?.toLowerCase();
|
|
1658
|
+
const validRole = role === "main" ||
|
|
1659
|
+
(role !== undefined &&
|
|
1660
|
+
IDENTITY_BOUNDARY_ROLES.includes(role));
|
|
1661
|
+
if (!role || !validRole) {
|
|
1662
|
+
ctx.ui.notify(`Unknown persona: ${role ?? "(missing)"}. Use main, ${IDENTITY_BOUNDARY_ROLES.join(", ")}.`, "error");
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (!updateIdentity({
|
|
1666
|
+
activeRole: role,
|
|
1667
|
+
injectBoundaries: true,
|
|
1668
|
+
})) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
ctx.ui.notify(`Persona set to ${role}. The boundary applies on the next agent turn.`, "info");
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
case "off":
|
|
1675
|
+
case "disable": {
|
|
1676
|
+
if (!updateIdentity({ injectBoundaries: false }))
|
|
1677
|
+
return;
|
|
1678
|
+
ctx.ui.notify("Identity injection disabled. The main agent remains unrestricted.", "info");
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
case "on":
|
|
1682
|
+
case "enable": {
|
|
1683
|
+
if (!updateIdentity({ injectBoundaries: true }))
|
|
1684
|
+
return;
|
|
1685
|
+
ctx.ui.notify(`Identity injection enabled for ${configuredRole}. The setting applies on the next agent turn.`, "info");
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
default:
|
|
1689
|
+
ctx.ui.notify("Usage: /vm identity status | set <role> | on | off", "error");
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1619
1692
|
// ── Main /vm command ───────────────────────────────────────────────────────
|
|
1620
1693
|
export const registerCommands = (pi) => {
|
|
1621
1694
|
pi.registerCommand("vm", {
|
|
@@ -1633,6 +1706,7 @@ export const registerCommands = (pi) => {
|
|
|
1633
1706
|
"discover-schema",
|
|
1634
1707
|
"doctor",
|
|
1635
1708
|
"injector",
|
|
1709
|
+
"identity",
|
|
1636
1710
|
"context",
|
|
1637
1711
|
"embedding",
|
|
1638
1712
|
"remote",
|
|
@@ -1662,6 +1736,11 @@ export const registerCommands = (pi) => {
|
|
|
1662
1736
|
.filter((c) => c.startsWith(prefix))
|
|
1663
1737
|
.map((c) => ({ label: c, value: c, description: `context ${c}` }));
|
|
1664
1738
|
}
|
|
1739
|
+
if (subcommand === "identity") {
|
|
1740
|
+
return ["status", "set", "on", "off", "main", ...IDENTITY_BOUNDARY_ROLES]
|
|
1741
|
+
.filter((c) => c.startsWith(prefix))
|
|
1742
|
+
.map((c) => ({ label: c, value: c, description: `identity ${c}` }));
|
|
1743
|
+
}
|
|
1665
1744
|
if (subcommand === "discover-schema") {
|
|
1666
1745
|
return ["--name", "--sample"]
|
|
1667
1746
|
.filter((c) => c.startsWith(prefix))
|
|
@@ -1736,6 +1815,8 @@ export const registerCommands = (pi) => {
|
|
|
1736
1815
|
return handleCollection(rest, ctx, pi);
|
|
1737
1816
|
case "injector":
|
|
1738
1817
|
return handleInjector(rest, ctx);
|
|
1818
|
+
case "identity":
|
|
1819
|
+
return handleIdentity(rest, ctx);
|
|
1739
1820
|
case "context":
|
|
1740
1821
|
return handleContext(rest, ctx, pi);
|
|
1741
1822
|
case "embedding":
|
|
@@ -26,11 +26,27 @@ function deepMerge(base, overlay) {
|
|
|
26
26
|
*/
|
|
27
27
|
export function writeIdentitiesLayer(cwd, identities) {
|
|
28
28
|
const configPath = getConfigPath(cwd);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
let existing = {};
|
|
30
|
+
if (fs.existsSync(configPath)) {
|
|
31
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
35
|
+
throw new TypeError("the root value must be a JSON object");
|
|
36
|
+
}
|
|
37
|
+
existing = parsed;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Invalid JSON in ${configPath}; the file was not changed.`, {
|
|
41
|
+
cause: error,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
32
45
|
const vaultMind = existing.vaultMind || {};
|
|
33
|
-
|
|
46
|
+
const legacyIdentities = existing.identities || {};
|
|
47
|
+
const canonicalIdentities = vaultMind.identities || {};
|
|
48
|
+
vaultMind.identities = deepMerge(deepMerge(legacyIdentities, canonicalIdentities), identities);
|
|
49
|
+
delete existing.identities;
|
|
34
50
|
existing.vaultMind = vaultMind;
|
|
35
51
|
const dir = path.dirname(configPath);
|
|
36
52
|
if (!fs.existsSync(dir))
|
|
@@ -6,10 +6,21 @@
|
|
|
6
6
|
* from pi-guideline-loader (kylebrodeur/pi-guideline-loader, MIT).
|
|
7
7
|
*/
|
|
8
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
export declare const IDENTITY_BOUNDARY_ROLES: readonly ["heavy-lifter", "miner", "broadcaster", "manager"];
|
|
10
|
+
export type IdentityBoundaryRole = (typeof IDENTITY_BOUNDARY_ROLES)[number];
|
|
11
|
+
/**
|
|
12
|
+
* Resolve an explicitly selected specialist persona.
|
|
13
|
+
*
|
|
14
|
+
* Specialist skills are ambient resources in every pi session, so their
|
|
15
|
+
* presence must never select a persona. The environment can override the
|
|
16
|
+
* vault-local config for launcher/fork use. Unset, "main", "none", and invalid
|
|
17
|
+
* roles all keep the unrestricted interactive main agent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveIdentityBoundaryRole(cwd: string, env?: NodeJS.ProcessEnv): IdentityBoundaryRole | undefined;
|
|
9
20
|
/**
|
|
10
21
|
* Register the identity injector on a pi ExtensionAPI.
|
|
11
22
|
*
|
|
12
|
-
* On every before_agent_start event,
|
|
13
|
-
*
|
|
23
|
+
* On every before_agent_start event, injects a boundary only when a specialist
|
|
24
|
+
* persona was selected explicitly through environment or vault config.
|
|
14
25
|
*/
|
|
15
26
|
export declare function registerIdentityInjector(pi: ExtensionAPI): void;
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* per role stating capability boundaries as a contract. Pattern adapted
|
|
6
6
|
* from pi-guideline-loader (kylebrodeur/pi-guideline-loader, MIT).
|
|
7
7
|
*/
|
|
8
|
+
import { loadConfig } from "./utils.js";
|
|
8
9
|
// ── Per-role boundary prompts ────────────────────────────────────────────────
|
|
10
|
+
export const IDENTITY_BOUNDARY_ROLES = ["heavy-lifter", "miner", "broadcaster", "manager"];
|
|
9
11
|
const BOUNDARY_PROMPTS = {
|
|
10
12
|
"heavy-lifter": [
|
|
11
13
|
"--- IDENTITY BOUNDARY: Heavy-Lifter ---",
|
|
@@ -50,46 +52,58 @@ const BOUNDARY_PROMPTS = {
|
|
|
50
52
|
"------------------------------------------",
|
|
51
53
|
].join("\n"),
|
|
52
54
|
};
|
|
53
|
-
// ── Role
|
|
55
|
+
// ── Role resolution ──────────────────────────────────────────────────────────
|
|
56
|
+
const DISABLED_SWITCH_VALUES = {
|
|
57
|
+
"0": true,
|
|
58
|
+
false: true,
|
|
59
|
+
no: true,
|
|
60
|
+
off: true,
|
|
61
|
+
disabled: true,
|
|
62
|
+
};
|
|
63
|
+
const ENABLED_SWITCH_VALUES = {
|
|
64
|
+
"1": true,
|
|
65
|
+
true: true,
|
|
66
|
+
yes: true,
|
|
67
|
+
on: true,
|
|
68
|
+
enabled: true,
|
|
69
|
+
};
|
|
54
70
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* PRIMARY signal — event.systemPromptOptions.skills:
|
|
58
|
-
* pi loads `--agent vault-mind-{role}` as a named Skill (Skill.name == "vault-mind-{role}").
|
|
59
|
-
* Exact name match is structural and immune to prose false-positives.
|
|
60
|
-
*
|
|
61
|
-
* FALLBACK signal — system prompt XML:
|
|
62
|
-
* Only when no matching skill is present. Matches only the machine-generated
|
|
63
|
-
* `<name>vault-mind-{role}</name>` element emitted by formatSkillsForPrompt()
|
|
64
|
-
* inside <available_skills> — not any free-text prose mention of the role name.
|
|
71
|
+
* Resolve an explicitly selected specialist persona.
|
|
65
72
|
*
|
|
66
|
-
*
|
|
73
|
+
* Specialist skills are ambient resources in every pi session, so their
|
|
74
|
+
* presence must never select a persona. The environment can override the
|
|
75
|
+
* vault-local config for launcher/fork use. Unset, "main", "none", and invalid
|
|
76
|
+
* roles all keep the unrestricted interactive main agent.
|
|
67
77
|
*/
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
78
|
+
export function resolveIdentityBoundaryRole(cwd, env = process.env) {
|
|
79
|
+
const identities = loadConfig(cwd).vaultMind.identities;
|
|
80
|
+
const injectionOverride = env.VAULT_MIND_IDENTITY_INJECTION?.trim().toLowerCase();
|
|
81
|
+
if (injectionOverride && DISABLED_SWITCH_VALUES[injectionOverride])
|
|
82
|
+
return;
|
|
83
|
+
if ((!injectionOverride || !ENABLED_SWITCH_VALUES[injectionOverride]) &&
|
|
84
|
+
identities?.injectBoundaries === false) {
|
|
85
|
+
return;
|
|
77
86
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
const rawRole = Object.hasOwn(env, "VAULT_MIND_AGENT_ROLE")
|
|
88
|
+
? env.VAULT_MIND_AGENT_ROLE
|
|
89
|
+
: identities?.activeRole;
|
|
90
|
+
const role = rawRole?.trim().toLowerCase();
|
|
91
|
+
if (!role || role === "main" || role === "none")
|
|
92
|
+
return;
|
|
93
|
+
if (!Object.hasOwn(BOUNDARY_PROMPTS, role))
|
|
94
|
+
return;
|
|
95
|
+
return role;
|
|
82
96
|
}
|
|
83
97
|
// ── Injector ──────────────────────────────────────────────────────────────────
|
|
84
98
|
/**
|
|
85
99
|
* Register the identity injector on a pi ExtensionAPI.
|
|
86
100
|
*
|
|
87
|
-
* On every before_agent_start event,
|
|
88
|
-
*
|
|
101
|
+
* On every before_agent_start event, injects a boundary only when a specialist
|
|
102
|
+
* persona was selected explicitly through environment or vault config.
|
|
89
103
|
*/
|
|
90
104
|
export function registerIdentityInjector(pi) {
|
|
91
105
|
pi.on("before_agent_start", async (event) => {
|
|
92
|
-
const role =
|
|
106
|
+
const role = resolveIdentityBoundaryRole(event.systemPromptOptions.cwd);
|
|
93
107
|
if (!role)
|
|
94
108
|
return;
|
|
95
109
|
const boundaryPrompt = BOUNDARY_PROMPTS[role];
|
package/dist/src/scaffold.js
CHANGED
|
@@ -135,37 +135,37 @@ export const scaffoldVaultConfig = (vaultPath, collectionOverride) => {
|
|
|
135
135
|
version: 2,
|
|
136
136
|
collections: defaultCollections,
|
|
137
137
|
injectors: defaultInjectors,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
138
|
+
vaultMind: {
|
|
139
|
+
identities: {
|
|
140
|
+
roles: {
|
|
141
|
+
main: {
|
|
142
|
+
allowedTools: [
|
|
143
|
+
"read",
|
|
144
|
+
"write",
|
|
145
|
+
"edit",
|
|
146
|
+
"bash",
|
|
147
|
+
"grep",
|
|
148
|
+
"find",
|
|
149
|
+
"ls",
|
|
150
|
+
"vm_search",
|
|
151
|
+
"vm_query",
|
|
152
|
+
"vm_append",
|
|
153
|
+
"vm_stats",
|
|
154
|
+
"vm_status",
|
|
155
|
+
"vm_describe",
|
|
156
|
+
"vm_configure",
|
|
157
|
+
"vault_read",
|
|
158
|
+
"vault_search",
|
|
159
|
+
"vault_list",
|
|
160
|
+
"vault_tags",
|
|
161
|
+
"vm_backlinks",
|
|
162
|
+
"vm_related",
|
|
163
|
+
"vm_broken_links",
|
|
164
|
+
"vm_codegraph_impact",
|
|
165
|
+
],
|
|
166
|
+
},
|
|
165
167
|
},
|
|
166
168
|
},
|
|
167
|
-
},
|
|
168
|
-
vaultMind: {
|
|
169
169
|
dataDir: ".vault-mind/.lancedb",
|
|
170
170
|
ftsEnabled: true,
|
|
171
171
|
graph: { enabled: true, canvasSync: false },
|
package/dist/src/server.js
CHANGED
|
@@ -832,19 +832,28 @@ export function startServer(pi, serverState, watcherState, readinessCallback) {
|
|
|
832
832
|
if (parts.length === 5) {
|
|
833
833
|
const role = parts[3];
|
|
834
834
|
withAuth(req, res, async () => {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
835
|
+
try {
|
|
836
|
+
const body = (await readJsonBody(req));
|
|
837
|
+
if (!body.allowedTools || !Array.isArray(body.allowedTools)) {
|
|
838
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
839
|
+
res.end(JSON.stringify({ error: "Missing allowedTools array in request body" }));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
// writeIdentitiesLayer deep-merges; pass a partial role config
|
|
843
|
+
writeIdentitiesLayer(process.cwd(), {
|
|
844
|
+
roles: { [role]: { allowedTools: body.allowedTools } },
|
|
845
|
+
});
|
|
846
|
+
refreshAgentIdentities([role]);
|
|
847
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
848
|
+
res.end(JSON.stringify({ ok: true, role, allowedTools: body.allowedTools }));
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
852
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
853
|
+
res.end(JSON.stringify({
|
|
854
|
+
error: `Failed to update identity config: ${message}`,
|
|
855
|
+
}));
|
|
840
856
|
}
|
|
841
|
-
// writeIdentitiesLayer deep-merges; pass a partial role config
|
|
842
|
-
writeIdentitiesLayer(process.cwd(), {
|
|
843
|
-
roles: { [role]: { allowedTools: body.allowedTools } },
|
|
844
|
-
});
|
|
845
|
-
refreshAgentIdentities([role]);
|
|
846
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
847
|
-
res.end(JSON.stringify({ ok: true, role, allowedTools: body.allowedTools }));
|
|
848
857
|
}, { requireWrite: true });
|
|
849
858
|
}
|
|
850
859
|
else {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -203,6 +203,10 @@ export interface VaultMindConfig {
|
|
|
203
203
|
defaultProfile?: IdentityConfig;
|
|
204
204
|
/** Per-role overrides. */
|
|
205
205
|
roles?: Record<string, IdentityConfig>;
|
|
206
|
+
/** Master switch for system-prompt identity-boundary injection. */
|
|
207
|
+
injectBoundaries?: boolean;
|
|
208
|
+
/** Explicit persona to inject; unset, "main", or "none" keeps the main agent. */
|
|
209
|
+
activeRole?: string;
|
|
206
210
|
};
|
|
207
211
|
/**
|
|
208
212
|
* Vault folder layout overrides. Each path is vault-relative and joined to
|
|
@@ -235,6 +239,10 @@ export interface IdentitiesConfig {
|
|
|
235
239
|
defaultProfile?: IdentityConfig;
|
|
236
240
|
/** Per-role overrides. */
|
|
237
241
|
roles?: Record<string, IdentityConfig>;
|
|
242
|
+
/** Master switch for system-prompt identity-boundary injection. */
|
|
243
|
+
injectBoundaries?: boolean;
|
|
244
|
+
/** Explicit persona to inject; unset, "main", or "none" keeps the main agent. */
|
|
245
|
+
activeRole?: string;
|
|
238
246
|
}
|
|
239
247
|
export interface PiContextDef {
|
|
240
248
|
enabled?: boolean;
|
package/dist/src/utils.js
CHANGED
|
@@ -89,6 +89,20 @@ const mergeInjectors = (base, layer) => {
|
|
|
89
89
|
};
|
|
90
90
|
const mergeConfigLayer = (base, layer, cwd) => {
|
|
91
91
|
const rawLayer = layer;
|
|
92
|
+
const legacyIdentities = rawLayer.identities;
|
|
93
|
+
const canonicalIdentities = layer.vaultMind?.identities;
|
|
94
|
+
const mergedIdentities = base.vaultMind.identities || legacyIdentities || canonicalIdentities
|
|
95
|
+
? {
|
|
96
|
+
...base.vaultMind.identities,
|
|
97
|
+
...legacyIdentities,
|
|
98
|
+
...canonicalIdentities,
|
|
99
|
+
roles: {
|
|
100
|
+
...base.vaultMind.identities?.roles,
|
|
101
|
+
...legacyIdentities?.roles,
|
|
102
|
+
...canonicalIdentities?.roles,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
: undefined;
|
|
92
106
|
const merged = {
|
|
93
107
|
version: layer.version ?? base.version,
|
|
94
108
|
collections: { ...base.collections },
|
|
@@ -99,6 +113,7 @@ const mergeConfigLayer = (base, layer, cwd) => {
|
|
|
99
113
|
embedding: { ...base.vaultMind.embedding, ...(layer.vaultMind?.embedding || {}) },
|
|
100
114
|
graph: { ...base.vaultMind.graph, ...(layer.vaultMind?.graph || {}) },
|
|
101
115
|
vaults: { ...base.vaultMind.vaults, ...(layer.vaultMind?.vaults || {}) },
|
|
116
|
+
identities: mergedIdentities,
|
|
102
117
|
},
|
|
103
118
|
extensionCompatibility: {
|
|
104
119
|
...base.extensionCompatibility,
|
|
@@ -157,6 +157,13 @@ describe("CLI command handlers", () => {
|
|
|
157
157
|
process.chdir(origCwd);
|
|
158
158
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
159
159
|
});
|
|
160
|
+
describe("scaffoldVaultConfig", () => {
|
|
161
|
+
it("writes identities only at the canonical vaultMind path", () => {
|
|
162
|
+
const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
|
|
163
|
+
assert.equal(raw.identities, undefined);
|
|
164
|
+
assert.ok(raw.vaultMind.identities?.roles?.main);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
160
167
|
describe("handleSearch", () => {
|
|
161
168
|
it("returns results in hybrid mode (default)", async () => {
|
|
162
169
|
const vm = getVmHandler();
|
|
@@ -288,6 +295,77 @@ describe("CLI command handlers", () => {
|
|
|
288
295
|
assert.match(last.message, /Unknown: bogus/);
|
|
289
296
|
});
|
|
290
297
|
});
|
|
298
|
+
describe("handleIdentity", () => {
|
|
299
|
+
it("reports main as the safe default persona", async () => {
|
|
300
|
+
const vm = getVmHandler();
|
|
301
|
+
await vm("identity status", makeCtx(dir, notifies));
|
|
302
|
+
const last = notifies.at(-1);
|
|
303
|
+
assert.ok(last, "notify was called");
|
|
304
|
+
assert.equal(last.level, "info");
|
|
305
|
+
assert.match(last.message, /Persona: main/);
|
|
306
|
+
assert.match(last.message, /Injection: enabled/);
|
|
307
|
+
});
|
|
308
|
+
it("sets an explicit persona and enables injection", async () => {
|
|
309
|
+
const vm = getVmHandler();
|
|
310
|
+
await vm("identity set broadcaster", makeCtx(dir, notifies));
|
|
311
|
+
const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
|
|
312
|
+
assert.equal(raw.vaultMind.identities.activeRole, "broadcaster");
|
|
313
|
+
assert.equal(raw.vaultMind.identities.injectBoundaries, true);
|
|
314
|
+
assert.match(notifies.at(-1)?.message ?? "", /Persona set to broadcaster/);
|
|
315
|
+
});
|
|
316
|
+
it("migrates legacy top-level identities when setting a persona", async () => {
|
|
317
|
+
const configPath = getConfigPath(dir);
|
|
318
|
+
const legacy = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
319
|
+
legacy.identities = legacy.vaultMind.identities;
|
|
320
|
+
delete legacy.vaultMind.identities;
|
|
321
|
+
fs.writeFileSync(configPath, `${JSON.stringify(legacy, null, 2)}\n`, "utf-8");
|
|
322
|
+
const vm = getVmHandler();
|
|
323
|
+
await vm("identity set miner", makeCtx(dir, notifies));
|
|
324
|
+
const migrated = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
325
|
+
assert.equal(migrated.identities, undefined);
|
|
326
|
+
assert.ok(migrated.vaultMind.identities.roles.main);
|
|
327
|
+
assert.equal(migrated.vaultMind.identities.activeRole, "miner");
|
|
328
|
+
});
|
|
329
|
+
it("turns injection off without discarding the selected persona", async () => {
|
|
330
|
+
const vm = getVmHandler();
|
|
331
|
+
await vm("identity set miner", makeCtx(dir, notifies));
|
|
332
|
+
await vm("identity off", makeCtx(dir, notifies));
|
|
333
|
+
const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
|
|
334
|
+
assert.equal(raw.vaultMind.identities.activeRole, "miner");
|
|
335
|
+
assert.equal(raw.vaultMind.identities.injectBoundaries, false);
|
|
336
|
+
assert.match(notifies.at(-1)?.message ?? "", /Identity injection disabled/);
|
|
337
|
+
});
|
|
338
|
+
it("turns injection back on for the configured persona", async () => {
|
|
339
|
+
const vm = getVmHandler();
|
|
340
|
+
await vm("identity set manager", makeCtx(dir, notifies));
|
|
341
|
+
await vm("identity off", makeCtx(dir, notifies));
|
|
342
|
+
await vm("identity on", makeCtx(dir, notifies));
|
|
343
|
+
const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
|
|
344
|
+
assert.equal(raw.vaultMind.identities.activeRole, "manager");
|
|
345
|
+
assert.equal(raw.vaultMind.identities.injectBoundaries, true);
|
|
346
|
+
assert.match(notifies.at(-1)?.message ?? "", /Identity injection enabled/);
|
|
347
|
+
});
|
|
348
|
+
it("rejects an unknown persona without changing identity config", async () => {
|
|
349
|
+
const configPath = getConfigPath(dir);
|
|
350
|
+
const before = JSON.parse(fs.readFileSync(configPath, "utf-8")).vaultMind.identities;
|
|
351
|
+
const vm = getVmHandler();
|
|
352
|
+
await vm("identity set reviewer", makeCtx(dir, notifies));
|
|
353
|
+
const after = JSON.parse(fs.readFileSync(configPath, "utf-8")).vaultMind.identities;
|
|
354
|
+
assert.deepEqual(after, before);
|
|
355
|
+
assert.equal(notifies.at(-1)?.level, "error");
|
|
356
|
+
assert.match(notifies.at(-1)?.message ?? "", /Unknown persona/);
|
|
357
|
+
});
|
|
358
|
+
it("reports malformed config without overwriting it", async () => {
|
|
359
|
+
const configPath = getConfigPath(dir);
|
|
360
|
+
const malformed = "{ invalid json";
|
|
361
|
+
fs.writeFileSync(configPath, malformed, "utf-8");
|
|
362
|
+
const vm = getVmHandler();
|
|
363
|
+
await assert.doesNotReject(() => vm("identity set miner", makeCtx(dir, notifies)));
|
|
364
|
+
assert.equal(fs.readFileSync(configPath, "utf-8"), malformed);
|
|
365
|
+
assert.equal(notifies.at(-1)?.level, "error");
|
|
366
|
+
assert.match(notifies.at(-1)?.message ?? "", /Failed to update identity config/);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
291
369
|
describe("handleWatcher", () => {
|
|
292
370
|
it("status shows watcher stopped by default", async () => {
|
|
293
371
|
const vm = getVmHandler();
|
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
|
-
import
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { afterEach, describe, it } from "node:test";
|
|
3
6
|
import { registerIdentityInjector } from "../src/identity-injector.js";
|
|
4
|
-
|
|
7
|
+
const ENV_KEYS = ["VAULT_MIND_AGENT_ROLE", "VAULT_MIND_IDENTITY_INJECTION"];
|
|
8
|
+
const originalEnv = {
|
|
9
|
+
VAULT_MIND_AGENT_ROLE: process.env.VAULT_MIND_AGENT_ROLE,
|
|
10
|
+
VAULT_MIND_IDENTITY_INJECTION: process.env.VAULT_MIND_IDENTITY_INJECTION,
|
|
11
|
+
};
|
|
12
|
+
const temporaryDirectories = [];
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
for (const key of ENV_KEYS) {
|
|
15
|
+
const value = originalEnv[key];
|
|
16
|
+
if (value === undefined)
|
|
17
|
+
delete process.env[key];
|
|
18
|
+
else
|
|
19
|
+
process.env[key] = value;
|
|
20
|
+
}
|
|
21
|
+
while (temporaryDirectories.length > 0) {
|
|
22
|
+
fs.rmSync(temporaryDirectories.pop(), { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
5
25
|
function mockPi() {
|
|
6
26
|
const captured = { current: null };
|
|
7
27
|
const pi = {
|
|
8
28
|
on(event, handler) {
|
|
9
|
-
if (event === "before_agent_start")
|
|
29
|
+
if (event === "before_agent_start")
|
|
10
30
|
captured.current = handler;
|
|
11
|
-
}
|
|
12
31
|
},
|
|
13
32
|
};
|
|
14
33
|
return {
|
|
@@ -18,19 +37,27 @@ function mockPi() {
|
|
|
18
37
|
},
|
|
19
38
|
};
|
|
20
39
|
}
|
|
21
|
-
|
|
40
|
+
function temporaryVault(identities) {
|
|
41
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "pvm-identity-"));
|
|
42
|
+
temporaryDirectories.push(cwd);
|
|
43
|
+
if (identities) {
|
|
44
|
+
const configDirectory = path.join(cwd, ".vault-mind");
|
|
45
|
+
fs.mkdirSync(configDirectory, { recursive: true });
|
|
46
|
+
fs.writeFileSync(path.join(configDirectory, "vault-mind.config.json"), `${JSON.stringify({ version: 2, vaultMind: { identities } }, null, 2)}\n`, "utf-8");
|
|
47
|
+
}
|
|
48
|
+
return cwd;
|
|
49
|
+
}
|
|
22
50
|
function makeEvent(opts) {
|
|
23
51
|
return {
|
|
24
52
|
type: "before_agent_start",
|
|
25
53
|
prompt: "test prompt",
|
|
26
54
|
systemPrompt: opts.systemPrompt ?? "base system prompt",
|
|
27
55
|
systemPromptOptions: {
|
|
28
|
-
cwd:
|
|
56
|
+
cwd: opts.cwd,
|
|
29
57
|
skills: opts.skills,
|
|
30
58
|
},
|
|
31
59
|
};
|
|
32
60
|
}
|
|
33
|
-
/** Build a Skill with a given name. */
|
|
34
61
|
function skill(name) {
|
|
35
62
|
return {
|
|
36
63
|
name,
|
|
@@ -41,157 +68,68 @@ function skill(name) {
|
|
|
41
68
|
disableModelInvocation: false,
|
|
42
69
|
};
|
|
43
70
|
}
|
|
44
|
-
|
|
71
|
+
async function invoke(event) {
|
|
72
|
+
const mock = mockPi();
|
|
73
|
+
registerIdentityInjector(mock.pi);
|
|
74
|
+
assert.ok(mock.handler, "before_agent_start handler should be registered");
|
|
75
|
+
return mock.handler(event);
|
|
76
|
+
}
|
|
45
77
|
describe("registerIdentityInjector", () => {
|
|
46
|
-
it("
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
const result = await mock.handler(event);
|
|
78
|
+
it("does not infer a persona from ambient specialist skills", async () => {
|
|
79
|
+
const cwd = temporaryVault();
|
|
80
|
+
const result = await invoke(makeEvent({
|
|
81
|
+
cwd,
|
|
82
|
+
skills: [
|
|
83
|
+
skill("vault-mind-broadcaster"),
|
|
84
|
+
skill("vault-mind-manager"),
|
|
85
|
+
skill("vault-mind-miner"),
|
|
86
|
+
],
|
|
87
|
+
systemPrompt: "base prompt\n<available_skills><name>vault-mind-broadcaster</name></available_skills>",
|
|
88
|
+
}));
|
|
59
89
|
assert.strictEqual(result, undefined);
|
|
60
90
|
});
|
|
61
|
-
it("the
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
assert.ok(
|
|
65
|
-
|
|
66
|
-
const result = await mock.handler(event);
|
|
67
|
-
assert.strictEqual(result, undefined);
|
|
91
|
+
it("injects the persona selected in vault config", async () => {
|
|
92
|
+
const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
|
|
93
|
+
const result = await invoke(makeEvent({ cwd, systemPrompt: "base prompt" }));
|
|
94
|
+
assert.ok(result?.systemPrompt?.includes("base prompt"));
|
|
95
|
+
assert.ok(result?.systemPrompt?.includes("IDENTITY BOUNDARY: Broadcaster"));
|
|
68
96
|
});
|
|
69
|
-
it("
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
assert.strictEqual(result, undefined);
|
|
97
|
+
it("treats main and none as the full-tool main persona", async () => {
|
|
98
|
+
for (const activeRole of ["main", "none"]) {
|
|
99
|
+
const cwd = temporaryVault({ injectBoundaries: true, activeRole });
|
|
100
|
+
const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
|
|
101
|
+
assert.strictEqual(result, undefined, activeRole);
|
|
102
|
+
}
|
|
76
103
|
});
|
|
77
|
-
it("
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
assert.
|
|
81
|
-
const event = makeEvent({
|
|
82
|
-
skills: [skill("vault-mind-heavy-lifter")],
|
|
83
|
-
systemPrompt: "base prompt",
|
|
84
|
-
});
|
|
85
|
-
const result = await mock.handler(event);
|
|
86
|
-
assert.ok(result);
|
|
87
|
-
assert.ok("systemPrompt" in result);
|
|
88
|
-
assert.ok(result.systemPrompt.includes("base prompt"));
|
|
89
|
-
assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Heavy-Lifter"));
|
|
90
|
-
assert.ok(result.systemPrompt.includes("durable knowledge store"));
|
|
91
|
-
});
|
|
92
|
-
it("the hook injects boundary prompt for miner role", async () => {
|
|
93
|
-
const mock = mockPi();
|
|
94
|
-
registerIdentityInjector(mock.pi);
|
|
95
|
-
assert.ok(mock.handler);
|
|
96
|
-
const event = makeEvent({
|
|
97
|
-
skills: [skill("vault-mind-miner")],
|
|
98
|
-
systemPrompt: "base prompt",
|
|
99
|
-
});
|
|
100
|
-
const result = await mock.handler(event);
|
|
101
|
-
assert.ok(result);
|
|
102
|
-
assert.ok("systemPrompt" in result);
|
|
103
|
-
assert.ok(result.systemPrompt.includes("base prompt"));
|
|
104
|
-
assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Miner"));
|
|
105
|
-
assert.ok(result.systemPrompt.includes("'main' and 'research'"));
|
|
106
|
-
});
|
|
107
|
-
it("the hook injects boundary prompt for broadcaster role", async () => {
|
|
108
|
-
const mock = mockPi();
|
|
109
|
-
registerIdentityInjector(mock.pi);
|
|
110
|
-
assert.ok(mock.handler);
|
|
111
|
-
const event = makeEvent({
|
|
112
|
-
skills: [skill("vault-mind-broadcaster")],
|
|
113
|
-
systemPrompt: "base prompt",
|
|
114
|
-
});
|
|
115
|
-
const result = await mock.handler(event);
|
|
116
|
-
assert.ok(result);
|
|
117
|
-
assert.ok("systemPrompt" in result);
|
|
118
|
-
assert.ok(result.systemPrompt.includes("base prompt"));
|
|
119
|
-
assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Broadcaster"));
|
|
120
|
-
assert.ok(result.systemPrompt.includes("Agent/Presentations"));
|
|
121
|
-
});
|
|
122
|
-
it("the hook injects boundary prompt for manager role", async () => {
|
|
123
|
-
const mock = mockPi();
|
|
124
|
-
registerIdentityInjector(mock.pi);
|
|
125
|
-
assert.ok(mock.handler);
|
|
126
|
-
const event = makeEvent({
|
|
127
|
-
skills: [skill("vault-mind-manager")],
|
|
128
|
-
systemPrompt: "base prompt",
|
|
129
|
-
});
|
|
130
|
-
const result = await mock.handler(event);
|
|
131
|
-
assert.ok(result);
|
|
132
|
-
assert.ok("systemPrompt" in result);
|
|
133
|
-
assert.ok(result.systemPrompt.includes("base prompt"));
|
|
134
|
-
assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Manager"));
|
|
135
|
-
assert.ok(result.systemPrompt.includes("publish to the human-facing vault"));
|
|
136
|
-
});
|
|
137
|
-
// ── Fallback: XML-based role detection ────────────────────────────────────
|
|
138
|
-
it("detects role via XML fallback when no skills match", async () => {
|
|
139
|
-
const mock = mockPi();
|
|
140
|
-
registerIdentityInjector(mock.pi);
|
|
141
|
-
assert.ok(mock.handler);
|
|
142
|
-
const event = makeEvent({
|
|
143
|
-
skills: [skill("unrelated-skill")],
|
|
144
|
-
systemPrompt: "base prompt\n<available_skills>\n<name>vault-mind-miner</name>\n</available_skills>",
|
|
145
|
-
});
|
|
146
|
-
const result = await mock.handler(event);
|
|
147
|
-
assert.ok(result);
|
|
148
|
-
assert.ok("systemPrompt" in result);
|
|
149
|
-
assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Miner"));
|
|
104
|
+
it("config hard-off suppresses a configured specialist persona", async () => {
|
|
105
|
+
const cwd = temporaryVault({ injectBoundaries: false, activeRole: "broadcaster" });
|
|
106
|
+
const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
|
|
107
|
+
assert.strictEqual(result, undefined);
|
|
150
108
|
});
|
|
151
|
-
it("
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
const result = await mock.handler(event);
|
|
159
|
-
assert.ok(result);
|
|
160
|
-
assert.ok("systemPrompt" in result);
|
|
161
|
-
assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Broadcaster"));
|
|
109
|
+
it("environment role overrides the configured persona", async () => {
|
|
110
|
+
process.env.VAULT_MIND_AGENT_ROLE = "miner";
|
|
111
|
+
const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
|
|
112
|
+
const result = await invoke(makeEvent({ cwd }));
|
|
113
|
+
assert.ok(result?.systemPrompt?.includes("IDENTITY BOUNDARY: Miner"));
|
|
114
|
+
assert.ok(!result?.systemPrompt?.includes("IDENTITY BOUNDARY: Broadcaster"));
|
|
162
115
|
});
|
|
163
|
-
it("
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
systemPrompt: "base prompt\n<available_skills>\n<name>some-other-skill</name>\n</available_skills>",
|
|
169
|
-
});
|
|
170
|
-
const result = await mock.handler(event);
|
|
116
|
+
it("environment hard-off overrides both environment role and config", async () => {
|
|
117
|
+
process.env.VAULT_MIND_AGENT_ROLE = "miner";
|
|
118
|
+
process.env.VAULT_MIND_IDENTITY_INJECTION = "off";
|
|
119
|
+
const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
|
|
120
|
+
const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
|
|
171
121
|
assert.strictEqual(result, undefined);
|
|
172
122
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
assert.ok(
|
|
178
|
-
const event = makeEvent({
|
|
179
|
-
skills: [skill("vault-mind-HEAVY-LIFTER")],
|
|
180
|
-
});
|
|
181
|
-
const result = await mock.handler(event);
|
|
182
|
-
assert.ok(result);
|
|
183
|
-
assert.ok("systemPrompt" in result);
|
|
184
|
-
assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Heavy-Lifter"));
|
|
123
|
+
it("normalizes case and whitespace for explicit roles", async () => {
|
|
124
|
+
process.env.VAULT_MIND_AGENT_ROLE = " HEAVY-LIFTER ";
|
|
125
|
+
const cwd = temporaryVault();
|
|
126
|
+
const result = await invoke(makeEvent({ cwd }));
|
|
127
|
+
assert.ok(result?.systemPrompt?.includes("IDENTITY BOUNDARY: Heavy-Lifter"));
|
|
185
128
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
assert.ok(mock.handler);
|
|
191
|
-
const event = makeEvent({
|
|
192
|
-
skills: [skill("vault-mind-unknown-role")],
|
|
193
|
-
});
|
|
194
|
-
const result = await mock.handler(event);
|
|
129
|
+
it("ignores an unknown explicit persona rather than injecting the wrong one", async () => {
|
|
130
|
+
process.env.VAULT_MIND_AGENT_ROLE = "unknown-role";
|
|
131
|
+
const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
|
|
132
|
+
const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
|
|
195
133
|
assert.strictEqual(result, undefined);
|
|
196
134
|
});
|
|
197
135
|
});
|
|
@@ -461,4 +461,28 @@ describe("REST vm routes", () => {
|
|
|
461
461
|
assert.ok(Array.isArray(returned), "GET must return a config with an injectors array");
|
|
462
462
|
assert.ok(!returned.some((ij) => ij.name === "draft-context"), "GET must not resurrect the default injector after an explicit empty patch");
|
|
463
463
|
});
|
|
464
|
+
it("returns JSON 500 without overwriting a malformed identity config", async () => {
|
|
465
|
+
const configPath = path.join(vaultPath, ".vault-mind", "vault-mind.config.json");
|
|
466
|
+
const validConfig = fs.readFileSync(configPath, "utf-8");
|
|
467
|
+
const malformed = "{ invalid json";
|
|
468
|
+
fs.writeFileSync(configPath, malformed, "utf-8");
|
|
469
|
+
try {
|
|
470
|
+
const response = await fetch(`http://127.0.0.1:${port}/vault-mind/identities/main/allowedTools`, {
|
|
471
|
+
method: "PUT",
|
|
472
|
+
headers: {
|
|
473
|
+
Authorization: `Bearer ${TEST_TOKEN}`,
|
|
474
|
+
"Content-Type": "application/json",
|
|
475
|
+
},
|
|
476
|
+
body: JSON.stringify({ allowedTools: ["read", "write"] }),
|
|
477
|
+
signal: AbortSignal.timeout(1000),
|
|
478
|
+
});
|
|
479
|
+
const body = (await response.json());
|
|
480
|
+
assert.equal(response.status, 500);
|
|
481
|
+
assert.match(String(body.error), /Failed to update identity config/);
|
|
482
|
+
assert.equal(fs.readFileSync(configPath, "utf-8"), malformed);
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
fs.writeFileSync(configPath, validConfig, "utf-8");
|
|
486
|
+
}
|
|
487
|
+
});
|
|
464
488
|
});
|
package/dist/test/utils.test.js
CHANGED
|
@@ -392,6 +392,19 @@ describe("loadConfig", () => {
|
|
|
392
392
|
// Default embedding.localUrl should still be present from merge
|
|
393
393
|
assert.equal(cfg.vaultMind.embedding.localUrl, "http://127.0.0.1:11434");
|
|
394
394
|
});
|
|
395
|
+
it("loads legacy top-level identities into the canonical vaultMind identities path", () => {
|
|
396
|
+
writeConfig(testDir, {
|
|
397
|
+
identities: {
|
|
398
|
+
roles: {
|
|
399
|
+
main: {
|
|
400
|
+
allowedTools: ["read", "bash"],
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
const cfg = loadConfig(testDir);
|
|
406
|
+
assert.deepEqual(cfg.vaultMind.identities?.roles?.main.allowedTools, ["read", "bash"]);
|
|
407
|
+
});
|
|
395
408
|
it("resolves collection paths relative to cwd", () => {
|
|
396
409
|
writeConfig(testDir, {
|
|
397
410
|
collections: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vault-mind",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.29",
|
|
4
4
|
"description": "Passive Obsidian vault extension for pi. Watches @agent markers, dispatches forked subagents (Miner, Broadcaster, Heavy-Lifter), stores in LanceDB with vector + FTS + graph. Multi-agent 'Drop & Forget' workflow for the pi agent ecosystem.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: BroadcasterAgent
|
|
3
|
-
role: broadcaster
|
|
4
|
-
capabilities: [read, write, edit]
|
|
5
|
-
allowed_tools: [read, write, edit, vm_search, vm_fts_search]
|
|
6
|
-
write_collections: []
|
|
7
|
-
can_publish: false
|
|
8
|
-
llm_provider: none
|
|
9
|
-
llm_model: none
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Broadcaster Agent
|
|
13
|
-
|
|
14
|
-
Content distribution agent for pi-vault-mind. Creates presentations, summaries,
|
|
15
|
-
and external-facing content from vault knowledge.
|
|
16
|
-
|
|
17
|
-
## Capability Boundary
|
|
18
|
-
|
|
19
|
-
- **MAY**: read files, write files, edit files
|
|
20
|
-
- **MAY**: search collections via vm_search, vm_fts_search
|
|
21
|
-
- **MUST NOT**: run bash commands, grep, or find
|
|
22
|
-
- **MUST NOT**: write to the durable knowledge store
|
|
23
|
-
- **MUST NOT**: publish or spawn sub-agents
|
|
24
|
-
- Write output only under Agent/Presentations/
|
|
25
|
-
|
|
26
|
-
## Tool Calling Convention
|
|
27
|
-
|
|
28
|
-
When calling pi-vault-mind tools, always include your role:
|
|
29
|
-
```
|
|
30
|
-
vm_search({ query: "...", role: "broadcaster" })
|
|
31
|
-
```
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: HeavyLifterAgent
|
|
3
|
-
role: heavy-lifter
|
|
4
|
-
capabilities: [read, write, edit, grep, find, ls, bash]
|
|
5
|
-
allowed_tools: [read, write, edit, grep, find, ls, bash, vm_search, vm_fts_search]
|
|
6
|
-
write_collections: []
|
|
7
|
-
can_publish: false
|
|
8
|
-
llm_provider: none
|
|
9
|
-
llm_model: none
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Heavy-Lifter Agent
|
|
13
|
-
|
|
14
|
-
External CLI execution agent for pi-vault-mind. Runs bash commands in an isolated
|
|
15
|
-
worktree. Cannot write to the durable knowledge store — all findings must be
|
|
16
|
-
routed to the Miner for storage.
|
|
17
|
-
|
|
18
|
-
## Capability Boundary
|
|
19
|
-
|
|
20
|
-
- **MAY**: read files, write files, edit files, run bash commands, search with grep/find/ls
|
|
21
|
-
- **MUST NOT**: write to the durable store (vm_append, vm_sync disabled)
|
|
22
|
-
- **MUST NOT**: spawn sub-agents
|
|
23
|
-
- **MUST NOT**: publish to the human-facing vault
|
|
24
|
-
- All bash commands run in an isolated worktree. Do not modify files outside it.
|
|
25
|
-
|
|
26
|
-
## Tool Calling Convention
|
|
27
|
-
|
|
28
|
-
When calling pi-vault-mind tools, always include your role:
|
|
29
|
-
```
|
|
30
|
-
vm_search({ query: "...", role: "heavy-lifter" })
|
|
31
|
-
```
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: ManagerAgent
|
|
3
|
-
role: manager
|
|
4
|
-
capabilities: [read, write, edit, grep, find, ls]
|
|
5
|
-
allowed_tools: [read, write, edit, grep, find, ls, vm_search, vm_fts_search, vm_append, vm_sync, vm_promote, vm_query, vm_configure, vm_describe, vm_stats, vm_export, vm_ingest, vm_status, vm_graph_query]
|
|
6
|
-
write_collections: [main, research, presentations]
|
|
7
|
-
can_publish: true
|
|
8
|
-
llm_provider: none
|
|
9
|
-
llm_model: none
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Manager Agent
|
|
13
|
-
|
|
14
|
-
Final gate and publishing agent for pi-vault-mind. The only role authorized to
|
|
15
|
-
publish to the human-facing vault and dispatch sub-agents.
|
|
16
|
-
|
|
17
|
-
## Capability Boundary
|
|
18
|
-
|
|
19
|
-
- **MAY**: all vm_* tools, read, write, edit, grep, find, ls
|
|
20
|
-
- **MAY**: write to any collection
|
|
21
|
-
- **MAY**: publish to the human-facing vault via vm_sync
|
|
22
|
-
- **MAY**: dispatch sub-agents
|
|
23
|
-
- **MUST NOT**: run bash commands
|
|
24
|
-
|
|
25
|
-
## Tool Calling Convention
|
|
26
|
-
|
|
27
|
-
When calling pi-vault-mind tools, always include your role:
|
|
28
|
-
```
|
|
29
|
-
vm_sync({ collection: "main", role: "manager" })
|
|
30
|
-
```
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: MinerAgent
|
|
3
|
-
role: miner
|
|
4
|
-
capabilities: [read, write, edit, grep, find, ls]
|
|
5
|
-
allowed_tools: [read, write, edit, grep, find, ls, vm_search, vm_fts_search, vm_append]
|
|
6
|
-
write_collections: [main, research]
|
|
7
|
-
can_publish: false
|
|
8
|
-
llm_provider: none
|
|
9
|
-
llm_model: none
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Miner Agent
|
|
13
|
-
|
|
14
|
-
Knowledge extraction and storage agent for pi-vault-mind. Extracts entities and
|
|
15
|
-
relationships from vault content and stores them in the durable knowledge store.
|
|
16
|
-
|
|
17
|
-
## Capability Boundary
|
|
18
|
-
|
|
19
|
-
- **MAY**: read files, write files, edit files, search with grep/find/ls
|
|
20
|
-
- **MAY**: append to collections 'main' and 'research' via vm_append
|
|
21
|
-
- **MUST NOT**: run bash commands
|
|
22
|
-
- **MUST NOT**: publish to the human-facing vault (vm_sync disabled)
|
|
23
|
-
- **MUST NOT**: spawn sub-agents
|
|
24
|
-
|
|
25
|
-
## Tool Calling Convention
|
|
26
|
-
|
|
27
|
-
When calling pi-vault-mind tools, always include your role:
|
|
28
|
-
```
|
|
29
|
-
vm_append({ collection: "main", entry: {...}, role: "miner" })
|
|
30
|
-
```
|