@soulguard/openclaw 0.1.0
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/README.md +67 -0
- package/dist/index.js +5901 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +19 -0
- package/src/guard.test.ts +58 -0
- package/src/guard.ts +74 -0
- package/src/index.ts +31 -0
- package/src/openclaw-types.ts +50 -0
- package/src/plugin.ts +162 -0
- package/src/templates.test.ts +36 -0
- package/src/templates.ts +113 -0
- package/tsconfig.json +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soulguard/openclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "bun build src/index.ts --outdir dist --target node",
|
|
9
|
+
"test": "bun test",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@soulguard/core": "workspace:^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/bun": "latest",
|
|
17
|
+
"typescript": "^5.7.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { guardToolCall, type GuardOptions } from "./guard.js";
|
|
3
|
+
|
|
4
|
+
const defaultOpts: GuardOptions = {
|
|
5
|
+
vaultFiles: ["SOUL.md", "IDENTITY.md"],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
describe("guardToolCall", () => {
|
|
9
|
+
it("blocks Write to a vault file", () => {
|
|
10
|
+
const result = guardToolCall("Write", { file_path: "SOUL.md" }, defaultOpts);
|
|
11
|
+
expect(result.blocked).toBe(true);
|
|
12
|
+
expect(result.reason).toContain("vault-protected");
|
|
13
|
+
expect(result.reason).toContain(".soulguard/staging/SOUL.md");
|
|
14
|
+
expect(result.reason).toContain("reviewed and approved by the owner");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("blocks Edit to a vault file", () => {
|
|
18
|
+
const result = guardToolCall("Edit", { path: "IDENTITY.md" }, defaultOpts);
|
|
19
|
+
expect(result.blocked).toBe(true);
|
|
20
|
+
expect(result.reason).toContain("vault-protected");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("allows Write to a non-vault file", () => {
|
|
24
|
+
const result = guardToolCall("Write", { file_path: "README.md" }, defaultOpts);
|
|
25
|
+
expect(result.blocked).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("allows Write to staging copy of a vault file", () => {
|
|
29
|
+
const result = guardToolCall("Write", { file_path: ".soulguard/staging/SOUL.md" }, defaultOpts);
|
|
30
|
+
expect(result.blocked).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("allows non-write tools (e.g. Read)", () => {
|
|
34
|
+
const result = guardToolCall("Read", { file_path: "SOUL.md" }, defaultOpts);
|
|
35
|
+
expect(result.blocked).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// TODO: re-enable when glob matching is delegated to @soulguard/core isVaulted() API
|
|
39
|
+
// For 0.1, only exact matches are supported — globs are not evaluated.
|
|
40
|
+
|
|
41
|
+
it("handles ./prefix in file paths", () => {
|
|
42
|
+
const result = guardToolCall("Write", { path: "./SOUL.md" }, defaultOpts);
|
|
43
|
+
expect(result.blocked).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("allows when no path param is present", () => {
|
|
47
|
+
const result = guardToolCall("Write", { content: "hello" }, defaultOpts);
|
|
48
|
+
expect(result.blocked).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("checks file param key as well", () => {
|
|
52
|
+
const result = guardToolCall("Edit", { file: "SOUL.md" }, defaultOpts);
|
|
53
|
+
expect(result.blocked).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// TODO: glob pattern tests — re-enable when delegated to @soulguard/core isVaulted() API
|
|
57
|
+
// For 0.1, "*.md" in vaultFiles is treated as a literal string, not a glob.
|
|
58
|
+
});
|
package/src/guard.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* before_tool_call guard — blocks writes to vault-protected files and
|
|
3
|
+
* returns a helpful message pointing the agent to the staging workflow.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
import { isVaultedFile, normalizePath } from "@soulguard/core";
|
|
8
|
+
|
|
9
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type GuardOptions = {
|
|
12
|
+
/** Vault file paths/patterns from soulguard.json */
|
|
13
|
+
vaultFiles: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GuardResult = {
|
|
17
|
+
blocked: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** OpenClaw tool names that write files. */
|
|
24
|
+
const WRITE_TOOLS = new Set(["Write", "Edit"]);
|
|
25
|
+
|
|
26
|
+
/** Param keys that carry the target file path. */
|
|
27
|
+
const PATH_KEYS = ["file_path", "path", "file"] as const;
|
|
28
|
+
|
|
29
|
+
/** Staging directory — writes here are always allowed. */
|
|
30
|
+
const STAGING_PREFIX = ".soulguard/staging/";
|
|
31
|
+
|
|
32
|
+
// ── Main guard ─────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate whether a tool call should be blocked.
|
|
36
|
+
*
|
|
37
|
+
* Returns `{ blocked: false }` to allow, or `{ blocked: true, reason }` to block.
|
|
38
|
+
*/
|
|
39
|
+
export function guardToolCall(
|
|
40
|
+
toolName: string,
|
|
41
|
+
params: Record<string, unknown>,
|
|
42
|
+
options: GuardOptions,
|
|
43
|
+
): GuardResult {
|
|
44
|
+
// Only intercept file-writing tools
|
|
45
|
+
if (!WRITE_TOOLS.has(toolName)) return { blocked: false };
|
|
46
|
+
|
|
47
|
+
// Extract target path from params
|
|
48
|
+
let targetPath: string | undefined;
|
|
49
|
+
for (const key of PATH_KEYS) {
|
|
50
|
+
const v = params[key];
|
|
51
|
+
if (typeof v === "string" && v.length > 0) {
|
|
52
|
+
targetPath = v;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!targetPath) return { blocked: false };
|
|
58
|
+
|
|
59
|
+
// Never block writes to staging
|
|
60
|
+
const norm = normalizePath(targetPath);
|
|
61
|
+
if (norm.startsWith(STAGING_PREFIX)) return { blocked: false };
|
|
62
|
+
|
|
63
|
+
// Check against vault using core SDK
|
|
64
|
+
if (!isVaultedFile(options.vaultFiles, targetPath)) return { blocked: false };
|
|
65
|
+
|
|
66
|
+
const fileName = basename(targetPath);
|
|
67
|
+
return {
|
|
68
|
+
blocked: true,
|
|
69
|
+
reason:
|
|
70
|
+
`${fileName} is vault-protected by soulguard. ` +
|
|
71
|
+
`To modify it, edit .soulguard/staging/${norm} instead. ` +
|
|
72
|
+
`Your changes will be reviewed and approved by the owner.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @soulguard/openclaw — OpenClaw framework plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Configuration templates (default, paranoid, relaxed)
|
|
6
|
+
* - before_tool_call hooks to intercept writes to vault files
|
|
7
|
+
* - Cron and extension directory gating
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { templates, defaultTemplate, paranoidTemplate, relaxedTemplate } from "./templates.js";
|
|
11
|
+
export type { TemplateName, Template } from "./templates.js";
|
|
12
|
+
|
|
13
|
+
export { createSoulguardPlugin } from "./plugin.js";
|
|
14
|
+
export type { SoulguardPluginOptions } from "./plugin.js";
|
|
15
|
+
|
|
16
|
+
// Default export for OpenClaw plugin discovery
|
|
17
|
+
// createSoulguardPlugin() returns { id, activate(api) } which matches OpenClaw's plugin shape
|
|
18
|
+
import { createSoulguardPlugin } from "./plugin.js";
|
|
19
|
+
export default createSoulguardPlugin();
|
|
20
|
+
|
|
21
|
+
export { guardToolCall } from "./guard.js";
|
|
22
|
+
export type { GuardOptions, GuardResult } from "./guard.js";
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
OpenClawPluginDefinition,
|
|
26
|
+
OpenClawPluginApi,
|
|
27
|
+
AgentTool,
|
|
28
|
+
AgentToolResult,
|
|
29
|
+
BeforeToolCallEvent,
|
|
30
|
+
BeforeToolCallResult,
|
|
31
|
+
} from "./openclaw-types.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal types from the OpenClaw plugin API.
|
|
3
|
+
*
|
|
4
|
+
* We vendor just the surface soulguard touches so the package has zero
|
|
5
|
+
* runtime dependency on openclaw itself.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type OpenClawPluginDefinition = {
|
|
9
|
+
id?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
register?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
14
|
+
activate?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type OpenClawPluginApi = {
|
|
18
|
+
on: (hookName: string, handler: (...args: unknown[]) => unknown) => void;
|
|
19
|
+
registerHook: (
|
|
20
|
+
events: string | string[],
|
|
21
|
+
handler: (...args: unknown[]) => unknown,
|
|
22
|
+
opts?: { priority?: number },
|
|
23
|
+
) => void;
|
|
24
|
+
registerTool: (tool: AgentTool, opts?: { optional?: boolean }) => void;
|
|
25
|
+
config: Record<string, unknown>;
|
|
26
|
+
runtime: { workspaceDir?: string };
|
|
27
|
+
resolvePath?: (input: string) => string;
|
|
28
|
+
logger?: { warn: (msg: string) => void; error: (msg: string) => void };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type AgentTool = {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
parameters: Record<string, unknown>;
|
|
35
|
+
execute: (id: string, params: Record<string, unknown>) => Promise<AgentToolResult>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type AgentToolResult = {
|
|
39
|
+
content: Array<{ type: "text"; text: string }>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type BeforeToolCallEvent = {
|
|
43
|
+
toolName: string;
|
|
44
|
+
params: Record<string, unknown>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type BeforeToolCallResult = {
|
|
48
|
+
block?: boolean;
|
|
49
|
+
blockReason?: string;
|
|
50
|
+
};
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soulguard OpenClaw plugin — protects vault files from direct writes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { status, diff, parseConfig, NodeSystemOps, type SoulguardConfig } from "@soulguard/core";
|
|
8
|
+
|
|
9
|
+
/** OpenClaw-specific default config — also vaults openclaw.json */
|
|
10
|
+
const OPENCLAW_DEFAULT_CONFIG: SoulguardConfig = {
|
|
11
|
+
vault: ["openclaw.json", "soulguard.json"],
|
|
12
|
+
ledger: [],
|
|
13
|
+
};
|
|
14
|
+
import { guardToolCall } from "./guard.js";
|
|
15
|
+
import type {
|
|
16
|
+
BeforeToolCallEvent,
|
|
17
|
+
BeforeToolCallResult,
|
|
18
|
+
OpenClawPluginDefinition,
|
|
19
|
+
} from "./openclaw-types.js";
|
|
20
|
+
|
|
21
|
+
/** Shared plugin description (plugin.json keeps its own copy). */
|
|
22
|
+
export const PLUGIN_DESCRIPTION = "Identity protection for AI agents";
|
|
23
|
+
|
|
24
|
+
export type SoulguardPluginOptions = {
|
|
25
|
+
/** Path to soulguard.json (relative to workspace or absolute). */
|
|
26
|
+
configPath?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create the soulguard OpenClaw plugin definition.
|
|
31
|
+
*/
|
|
32
|
+
export function createSoulguardPlugin(options?: SoulguardPluginOptions): OpenClawPluginDefinition {
|
|
33
|
+
return {
|
|
34
|
+
id: "soulguard",
|
|
35
|
+
name: "Soulguard",
|
|
36
|
+
description: PLUGIN_DESCRIPTION,
|
|
37
|
+
version: "0.1.0",
|
|
38
|
+
|
|
39
|
+
activate(api) {
|
|
40
|
+
const workspaceDir = api.resolvePath?.(".") ?? api.runtime.workspaceDir ?? ".";
|
|
41
|
+
const configFile = options?.configPath ?? "soulguard.json";
|
|
42
|
+
const configPath = api.resolvePath?.(configFile) ?? join(workspaceDir, configFile);
|
|
43
|
+
|
|
44
|
+
// Load config — fall back to OpenClaw defaults if missing
|
|
45
|
+
let config: SoulguardConfig;
|
|
46
|
+
let vaultFiles: string[];
|
|
47
|
+
try {
|
|
48
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
49
|
+
config = parseConfig(raw);
|
|
50
|
+
vaultFiles = config.vault;
|
|
51
|
+
} catch {
|
|
52
|
+
// No config file — use OpenClaw defaults (includes openclaw.json)
|
|
53
|
+
config = OPENCLAW_DEFAULT_CONFIG;
|
|
54
|
+
vaultFiles = config.vault;
|
|
55
|
+
api.logger?.warn("soulguard: no soulguard.json found — using OpenClaw defaults");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (vaultFiles.length === 0) return;
|
|
59
|
+
|
|
60
|
+
// Helper to create ops for the workspace
|
|
61
|
+
const createOps = () => new NodeSystemOps(workspaceDir);
|
|
62
|
+
|
|
63
|
+
// Status tool
|
|
64
|
+
api.registerTool(
|
|
65
|
+
{
|
|
66
|
+
name: "soulguard_status",
|
|
67
|
+
description: "Check soulguard protection status of vault and ledger files",
|
|
68
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
69
|
+
async execute(_id, _params) {
|
|
70
|
+
const ops = createOps();
|
|
71
|
+
const result = await status({
|
|
72
|
+
config,
|
|
73
|
+
expectedVaultOwnership: { user: "soulguardian", group: "soulguard", mode: "444" },
|
|
74
|
+
// TODO: ledger ownership should come from config, not hardcoded (agent user varies per init)
|
|
75
|
+
expectedLedgerOwnership: { user: "agent", group: "staff", mode: "644" },
|
|
76
|
+
ops,
|
|
77
|
+
});
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
return { content: [{ type: "text" as const, text: "Status check failed" }] };
|
|
80
|
+
}
|
|
81
|
+
const lines: string[] = ["Soulguard Status:", ""];
|
|
82
|
+
for (const f of [...result.value.vault, ...result.value.ledger]) {
|
|
83
|
+
if (f.status === "ok") lines.push(` ✅ ${f.file.path}`);
|
|
84
|
+
else if (f.status === "drifted")
|
|
85
|
+
lines.push(` ⚠️ ${f.file.path} — ${f.issues.map((i) => i.kind).join(", ")}`);
|
|
86
|
+
else if (f.status === "missing") lines.push(` ❌ ${f.path} — missing`);
|
|
87
|
+
else if (f.status === "error") lines.push(` ❌ ${f.path} — error: ${f.error.kind}`);
|
|
88
|
+
}
|
|
89
|
+
if (result.value.issues.length === 0) lines.push("", "All files ok.");
|
|
90
|
+
else lines.push("", `${result.value.issues.length} issue(s) found.`);
|
|
91
|
+
return { content: [{ type: "text" as const, text: lines.join("\n") }] };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{ optional: true },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Diff tool
|
|
98
|
+
api.registerTool(
|
|
99
|
+
{
|
|
100
|
+
name: "soulguard_diff",
|
|
101
|
+
description: "Show differences between vault files and their staging copies",
|
|
102
|
+
parameters: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
files: {
|
|
106
|
+
type: "array",
|
|
107
|
+
items: { type: "string" },
|
|
108
|
+
description: "Specific files to diff (default: all vault files)",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: [],
|
|
112
|
+
},
|
|
113
|
+
async execute(_id, params) {
|
|
114
|
+
const ops = createOps();
|
|
115
|
+
const files = Array.isArray(params.files) ? (params.files as string[]) : undefined;
|
|
116
|
+
const result = await diff({ ops, config, files });
|
|
117
|
+
if (!result.ok) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text" as const, text: `Diff failed: ${result.error.kind}` }],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (!result.value.hasChanges) {
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{ type: "text" as const, text: "No differences — staging matches vault." },
|
|
126
|
+
],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const lines = result.value.files
|
|
130
|
+
.filter((d) => d.status === "modified" && d.diff)
|
|
131
|
+
.map((d) => `--- ${d.path}\n${d.diff}`);
|
|
132
|
+
let text = lines.join("\n\n") || "No modified files.";
|
|
133
|
+
if (result.value.approvalHash) {
|
|
134
|
+
text += `\n\n────────────────────────────────────────\nApproval hash: ${result.value.approvalHash}\nTo approve: soulguard approve --hash ${result.value.approvalHash}`;
|
|
135
|
+
}
|
|
136
|
+
return { content: [{ type: "text" as const, text }] };
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{ optional: true },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Register the guard hook
|
|
143
|
+
// Use registerHook (OpenClaw's actual API) with fallback to on()
|
|
144
|
+
const hookFn = api.registerHook ?? api.on;
|
|
145
|
+
hookFn("before_tool_call", (...args: unknown[]) => {
|
|
146
|
+
const event = args[0];
|
|
147
|
+
// Defense in depth — verify event shape before casting
|
|
148
|
+
if (!event || typeof event !== "object" || !("toolName" in event)) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const e = event as BeforeToolCallEvent;
|
|
152
|
+
const result = guardToolCall(e.toolName, e.params, {
|
|
153
|
+
vaultFiles,
|
|
154
|
+
});
|
|
155
|
+
if (result.blocked) {
|
|
156
|
+
return { block: true, blockReason: result.reason } satisfies BeforeToolCallResult;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ALL_KNOWN_PATHS, templates } from "./templates.js";
|
|
3
|
+
|
|
4
|
+
describe("templates", () => {
|
|
5
|
+
for (const [name, template] of Object.entries(templates)) {
|
|
6
|
+
test(`${name}: accounts for all known paths`, () => {
|
|
7
|
+
const allInTemplate = [...template.vault, ...template.ledger, ...template.unprotected];
|
|
8
|
+
const sorted = (arr: string[]) => [...arr].sort();
|
|
9
|
+
|
|
10
|
+
expect(sorted(allInTemplate)).toEqual(sorted([...ALL_KNOWN_PATHS]));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test(`${name}: no path appears in multiple tiers`, () => {
|
|
14
|
+
const vaultSet = new Set(template.vault);
|
|
15
|
+
const ledgerSet = new Set(template.ledger);
|
|
16
|
+
const unprotectedSet = new Set(template.unprotected);
|
|
17
|
+
|
|
18
|
+
for (const path of template.vault) {
|
|
19
|
+
expect(ledgerSet.has(path)).toBe(false);
|
|
20
|
+
expect(unprotectedSet.has(path)).toBe(false);
|
|
21
|
+
}
|
|
22
|
+
for (const path of template.ledger) {
|
|
23
|
+
expect(vaultSet.has(path)).toBe(false);
|
|
24
|
+
expect(unprotectedSet.has(path)).toBe(false);
|
|
25
|
+
}
|
|
26
|
+
for (const path of template.unprotected) {
|
|
27
|
+
expect(vaultSet.has(path)).toBe(false);
|
|
28
|
+
expect(ledgerSet.has(path)).toBe(false);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test(`${name}: soulguard.json is always in vault`, () => {
|
|
33
|
+
expect(template.vault).toContain("soulguard.json");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw configuration templates for Soulguard.
|
|
3
|
+
*
|
|
4
|
+
* Every known path is explicitly placed in vault, ledger, or unprotected.
|
|
5
|
+
* Tests validate that all paths are accounted for in every template.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SoulguardConfig } from "@soulguard/core";
|
|
9
|
+
|
|
10
|
+
// ── Known path groups ──────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export const SOULGUARD_CONFIG = ["soulguard.json"] as const;
|
|
13
|
+
export const CORE_IDENTITY = ["SOUL.md", "AGENTS.md", "IDENTITY.md", "USER.md"] as const;
|
|
14
|
+
export const CORE_SESSION = ["TOOLS.md", "HEARTBEAT.md", "BOOTSTRAP.md"] as const;
|
|
15
|
+
export const CORE_MEMORY = ["MEMORY.md"] as const;
|
|
16
|
+
export const MEMORY_DIR = ["memory/**"] as const;
|
|
17
|
+
export const SKILLS = ["skills/**"] as const;
|
|
18
|
+
export const OPENCLAW_CONFIG = ["openclaw.json"] as const;
|
|
19
|
+
export const CRON = ["cron/jobs.json"] as const;
|
|
20
|
+
export const EXTENSIONS = ["extensions/**"] as const;
|
|
21
|
+
export const SESSIONS = ["sessions/**"] as const;
|
|
22
|
+
|
|
23
|
+
/** All known paths — every template must account for all of these */
|
|
24
|
+
export const ALL_KNOWN_PATHS = [
|
|
25
|
+
...SOULGUARD_CONFIG,
|
|
26
|
+
...CORE_IDENTITY,
|
|
27
|
+
...CORE_SESSION,
|
|
28
|
+
...CORE_MEMORY,
|
|
29
|
+
...MEMORY_DIR,
|
|
30
|
+
...SKILLS,
|
|
31
|
+
...OPENCLAW_CONFIG,
|
|
32
|
+
...CRON,
|
|
33
|
+
...EXTENSIONS,
|
|
34
|
+
...SESSIONS,
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
// ── Template type ──────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export type TemplateName = "default" | "paranoid" | "relaxed";
|
|
40
|
+
|
|
41
|
+
export type Template = {
|
|
42
|
+
name: TemplateName;
|
|
43
|
+
description: string;
|
|
44
|
+
vault: readonly string[];
|
|
45
|
+
ledger: readonly string[];
|
|
46
|
+
unprotected: readonly string[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Extract just the SoulguardConfig from a template */
|
|
50
|
+
export function templateToConfig(template: Template): SoulguardConfig {
|
|
51
|
+
return {
|
|
52
|
+
vault: [...template.vault],
|
|
53
|
+
ledger: [...template.ledger],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Templates ──────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export const defaultTemplate: Template = {
|
|
60
|
+
name: "default",
|
|
61
|
+
description: "Core identity and config in vault, memory and skills tracked in ledger",
|
|
62
|
+
vault: [
|
|
63
|
+
...SOULGUARD_CONFIG,
|
|
64
|
+
...CORE_IDENTITY,
|
|
65
|
+
...CORE_SESSION,
|
|
66
|
+
...OPENCLAW_CONFIG,
|
|
67
|
+
...CRON,
|
|
68
|
+
...EXTENSIONS,
|
|
69
|
+
],
|
|
70
|
+
ledger: [...CORE_MEMORY, ...MEMORY_DIR, ...SKILLS],
|
|
71
|
+
unprotected: [...SESSIONS],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const paranoidTemplate: Template = {
|
|
75
|
+
name: "paranoid",
|
|
76
|
+
description: "Everything possible in vault, only skills in ledger",
|
|
77
|
+
vault: [
|
|
78
|
+
...SOULGUARD_CONFIG,
|
|
79
|
+
...CORE_IDENTITY,
|
|
80
|
+
...CORE_SESSION,
|
|
81
|
+
...CORE_MEMORY,
|
|
82
|
+
...MEMORY_DIR,
|
|
83
|
+
...SKILLS,
|
|
84
|
+
...OPENCLAW_CONFIG,
|
|
85
|
+
...CRON,
|
|
86
|
+
...EXTENSIONS,
|
|
87
|
+
],
|
|
88
|
+
ledger: [...SESSIONS],
|
|
89
|
+
unprotected: [],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const relaxedTemplate: Template = {
|
|
93
|
+
name: "relaxed",
|
|
94
|
+
description: "Only soulguard config locked, everything else tracked — good for initial setup",
|
|
95
|
+
vault: [...SOULGUARD_CONFIG],
|
|
96
|
+
ledger: [
|
|
97
|
+
...CORE_IDENTITY,
|
|
98
|
+
...CORE_SESSION,
|
|
99
|
+
...CORE_MEMORY,
|
|
100
|
+
...MEMORY_DIR,
|
|
101
|
+
...SKILLS,
|
|
102
|
+
...OPENCLAW_CONFIG,
|
|
103
|
+
...CRON,
|
|
104
|
+
...EXTENSIONS,
|
|
105
|
+
],
|
|
106
|
+
unprotected: [...SESSIONS],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const templates: Record<TemplateName, Template> = {
|
|
110
|
+
default: defaultTemplate,
|
|
111
|
+
paranoid: paranoidTemplate,
|
|
112
|
+
relaxed: relaxedTemplate,
|
|
113
|
+
};
|