@soulguard/core 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.
Files changed (94) hide show
  1. package/README.md +127 -0
  2. package/dist/cli.js +8838 -0
  3. package/dist/index.js +6555 -0
  4. package/package.json +35 -0
  5. package/src/approve.test.ts +386 -0
  6. package/src/approve.ts +352 -0
  7. package/src/cli/approve-command.ts +107 -0
  8. package/src/cli/diff-command.test.ts +61 -0
  9. package/src/cli/diff-command.ts +81 -0
  10. package/src/cli/init-command.ts +83 -0
  11. package/src/cli/reset-command.ts +36 -0
  12. package/src/cli/status-command.test.ts +90 -0
  13. package/src/cli/status-command.ts +78 -0
  14. package/src/cli/sync-command.test.ts +67 -0
  15. package/src/cli/sync-command.ts +105 -0
  16. package/src/cli.ts +224 -0
  17. package/src/console-live.ts +32 -0
  18. package/src/console-mock.ts +48 -0
  19. package/src/console.ts +12 -0
  20. package/src/constants.ts +21 -0
  21. package/src/diff.test.ts +189 -0
  22. package/src/diff.ts +212 -0
  23. package/src/git.test.ts +180 -0
  24. package/src/git.ts +131 -0
  25. package/src/glob.test.ts +74 -0
  26. package/src/glob.ts +84 -0
  27. package/src/index.ts +100 -0
  28. package/src/init.test.ts +234 -0
  29. package/src/init.ts +317 -0
  30. package/src/policy.test.ts +123 -0
  31. package/src/policy.ts +100 -0
  32. package/src/reset.test.ts +68 -0
  33. package/src/reset.ts +63 -0
  34. package/src/result.ts +14 -0
  35. package/src/schema.test.ts +27 -0
  36. package/src/schema.ts +22 -0
  37. package/src/self-protection.test.ts +139 -0
  38. package/src/self-protection.ts +63 -0
  39. package/src/status.test.ts +241 -0
  40. package/src/status.ts +114 -0
  41. package/src/sync.test.ts +172 -0
  42. package/src/sync.ts +101 -0
  43. package/src/system-ops-mock.ts +243 -0
  44. package/src/system-ops-node.test.ts +183 -0
  45. package/src/system-ops-node.ts +499 -0
  46. package/src/system-ops.ts +109 -0
  47. package/src/types.ts +91 -0
  48. package/src/vault-check.test.ts +41 -0
  49. package/src/vault-check.ts +24 -0
  50. package/test-e2e/Dockerfile +29 -0
  51. package/test-e2e/cases/approve-with-hash/expected.txt +16 -0
  52. package/test-e2e/cases/approve-with-hash/test.sh +23 -0
  53. package/test-e2e/cases/diff-no-changes/expected.txt +5 -0
  54. package/test-e2e/cases/diff-no-changes/test.sh +7 -0
  55. package/test-e2e/cases/diff-no-staging/expected.txt +1 -0
  56. package/test-e2e/cases/diff-no-staging/test.sh +6 -0
  57. package/test-e2e/cases/diff-shows-changes/expected.txt +13 -0
  58. package/test-e2e/cases/diff-shows-changes/test.sh +12 -0
  59. package/test-e2e/cases/diff-vault-missing/expected.txt +6 -0
  60. package/test-e2e/cases/diff-vault-missing/test.sh +10 -0
  61. package/test-e2e/cases/glob-ledger-files/expected.txt +52 -0
  62. package/test-e2e/cases/glob-ledger-files/test.sh +28 -0
  63. package/test-e2e/cases/glob-vault-files/expected.txt +41 -0
  64. package/test-e2e/cases/glob-vault-files/test.sh +30 -0
  65. package/test-e2e/cases/init-blocked-by-agent/expected.txt +11 -0
  66. package/test-e2e/cases/init-blocked-by-agent/test.sh +15 -0
  67. package/test-e2e/cases/init-happy/expected.txt +18 -0
  68. package/test-e2e/cases/init-happy/test.sh +20 -0
  69. package/test-e2e/cases/init-idempotent/expected.txt +13 -0
  70. package/test-e2e/cases/init-idempotent/test.sh +14 -0
  71. package/test-e2e/cases/reset-staging/expected.txt +16 -0
  72. package/test-e2e/cases/reset-staging/test.sh +23 -0
  73. package/test-e2e/cases/self-protection-blocks-invalid/expected.txt +12 -0
  74. package/test-e2e/cases/self-protection-blocks-invalid/test.sh +25 -0
  75. package/test-e2e/cases/status-clean/expected.txt +9 -0
  76. package/test-e2e/cases/status-clean/test.sh +8 -0
  77. package/test-e2e/cases/status-drifted/expected.txt +9 -0
  78. package/test-e2e/cases/status-drifted/test.sh +11 -0
  79. package/test-e2e/cases/status-no-config/expected.txt +1 -0
  80. package/test-e2e/cases/status-no-config/test.sh +2 -0
  81. package/test-e2e/cases/sync-fix-drift/expected.txt +15 -0
  82. package/test-e2e/cases/sync-fix-drift/test.sh +14 -0
  83. package/test-e2e/cases/sync-no-sudo-clean/expected.txt +11 -0
  84. package/test-e2e/cases/sync-no-sudo-clean/test.sh +9 -0
  85. package/test-e2e/cases/sync-no-sudo-drift/expected.txt +7 -0
  86. package/test-e2e/cases/sync-no-sudo-drift/test.sh +10 -0
  87. package/test-e2e/cases/vault-remove-file/expected.txt +26 -0
  88. package/test-e2e/cases/vault-remove-file/test.sh +31 -0
  89. package/test-e2e/cases/vault-write-blocked/expected.txt +8 -0
  90. package/test-e2e/cases/vault-write-blocked/test.sh +14 -0
  91. package/test-e2e/run-tests.sh +76 -0
  92. package/test-integration/Dockerfile +17 -0
  93. package/test-integration/system-ops-node.integration.test.ts +170 -0
  94. package/tsconfig.json +8 -0
@@ -0,0 +1,41 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { isVaultedFile, normalizePath } from "./vault-check.js";
3
+
4
+ describe("isVaultedFile", () => {
5
+ test("exact match", () => {
6
+ expect(isVaultedFile(["SOUL.md"], "SOUL.md")).toBe(true);
7
+ expect(isVaultedFile(["SOUL.md"], "AGENTS.md")).toBe(false);
8
+ });
9
+
10
+ test("normalizes leading ./ and /", () => {
11
+ expect(isVaultedFile(["SOUL.md"], "./SOUL.md")).toBe(true);
12
+ expect(isVaultedFile(["./SOUL.md"], "SOUL.md")).toBe(true);
13
+ });
14
+
15
+ test("glob * matches single segment", () => {
16
+ expect(isVaultedFile(["memory/*.md"], "memory/day1.md")).toBe(true);
17
+ expect(isVaultedFile(["memory/*.md"], "memory/archive/old.md")).toBe(false);
18
+ expect(isVaultedFile(["*.md"], "SOUL.md")).toBe(true);
19
+ });
20
+
21
+ test("glob ** matches nested paths", () => {
22
+ expect(isVaultedFile(["memory/**"], "memory/day1.md")).toBe(true);
23
+ expect(isVaultedFile(["memory/**"], "memory/archive/old.md")).toBe(true);
24
+ expect(isVaultedFile(["skills/**"], "memory/day1.md")).toBe(false);
25
+ });
26
+ });
27
+
28
+ describe("normalizePath", () => {
29
+ test("strips leading ./", () => {
30
+ expect(normalizePath("./SOUL.md")).toBe("SOUL.md");
31
+ });
32
+
33
+ test("strips leading /", () => {
34
+ expect(normalizePath("/SOUL.md")).toBe("SOUL.md");
35
+ });
36
+
37
+ test("passes through normal paths", () => {
38
+ expect(normalizePath("SOUL.md")).toBe("SOUL.md");
39
+ expect(normalizePath("memory/day1.md")).toBe("memory/day1.md");
40
+ });
41
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Check if a file path matches a vault entry.
3
+ * Supports exact matches and glob patterns (*, **).
4
+ */
5
+
6
+ import { isGlob, matchGlob } from "./glob.js";
7
+
8
+ export function isVaultedFile(vaultFiles: string[], filePath: string): boolean {
9
+ const norm = normalizePath(filePath);
10
+ return vaultFiles.some((pattern) => {
11
+ const normPattern = normalizePath(pattern);
12
+ if (isGlob(normPattern)) {
13
+ return matchGlob(normPattern, norm);
14
+ }
15
+ return norm === normPattern;
16
+ });
17
+ }
18
+
19
+ export function normalizePath(p: string): string {
20
+ let s = p;
21
+ if (s.startsWith("./")) s = s.slice(2);
22
+ if (s.startsWith("/")) s = s.slice(1);
23
+ return s;
24
+ }
@@ -0,0 +1,29 @@
1
+ FROM oven/bun:latest
2
+
3
+ # Create agent user only — soulguardian/soulguard are created by `soulguard init`
4
+ RUN useradd -m -s /bin/bash agent
5
+
6
+ # Install sudo + git (git needed for e2e tests with git integration)
7
+ RUN apt-get update && apt-get install -y sudo git && rm -rf /var/lib/apt/lists/*
8
+
9
+ # No broad sudoers for agent — init generates scoped rules.
10
+ # Agent gets sudo only after `soulguard init` writes /etc/sudoers.d/soulguard.
11
+
12
+ WORKDIR /app
13
+
14
+ # Copy full monorepo
15
+ COPY . .
16
+
17
+ # Install deps
18
+ RUN bun install --frozen-lockfile
19
+
20
+ # Make CLI executable
21
+ RUN chmod +x packages/core/src/cli.ts
22
+
23
+ # Create soulguard command
24
+ RUN printf '#!/bin/bash\nexec bun /app/packages/core/src/cli.ts "$@"\n' > /usr/local/bin/soulguard && \
25
+ chmod +x /usr/local/bin/soulguard
26
+
27
+ # Default to root (simulates human/owner running setup)
28
+ # Tests switch to agent via `su - agent -c "..."` for agent actions
29
+ USER root
@@ -0,0 +1,16 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 2 vault file(s)
7
+
8
+ Done.
9
+ HASH: 0e94615ddc559fbd6541b0b002014ab8ee99df58d6ad360ba45e8510623a8990
10
+
11
+ Approved 1 file(s):
12
+ ✅ SOUL.md
13
+
14
+ Vault updated. Staging synced.
15
+ VAULT:
16
+ # My Updated Soul
@@ -0,0 +1,23 @@
1
+ # Approve with --hash (non-interactive, implicit proposal)
2
+
3
+ echo '# My Soul' > SOUL.md
4
+ cat > soulguard.json <<'EOF'
5
+ {"vault":["SOUL.md","soulguard.json"],"ledger":[]}
6
+ EOF
7
+
8
+ # Owner runs init
9
+ soulguard init . --agent-user agent
10
+
11
+ # Agent modifies staging
12
+ su - agent -c "echo '# My Updated Soul' > $(pwd)/.soulguard/staging/SOUL.md"
13
+
14
+ # Get approval hash from diff output
15
+ HASH=$(NO_COLOR=1 soulguard diff . 2>&1 | grep 'Approval hash:' | awk '{print $NF}')
16
+ echo "HASH: $HASH"
17
+
18
+ # Owner approves with hash
19
+ soulguard approve . --hash "$HASH"
20
+
21
+ # Verify vault has new content
22
+ echo "VAULT:"
23
+ cat SOUL.md
@@ -0,0 +1,5 @@
1
+ Soulguard Diff — /workspace
2
+
3
+ ✅ SOUL.md (no changes)
4
+
5
+ No changes
@@ -0,0 +1,7 @@
1
+ # Setup: create config, vault file, init — but don't modify staging
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+ soulguard init . --agent-user agent > /dev/null 2>&1
5
+
6
+ # Run diff — should show no changes
7
+ soulguard diff .
@@ -0,0 +1 @@
1
+ No staging directory found. Run `soulguard init` first.
@@ -0,0 +1,6 @@
1
+ # Setup: create config but do NOT init (no .soulguard/staging/)
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+
5
+ # Run diff — should error about missing staging
6
+ soulguard diff .
@@ -0,0 +1,13 @@
1
+ Soulguard Diff — /workspace
2
+
3
+ 📝 SOUL.md
4
+ ===================================================================
5
+ --- a/SOUL.md
6
+ +++ b/SOUL.md
7
+ @@ -1,1 +1,1 @@
8
+ -# My Soul
9
+ +# My Modified Soul
10
+
11
+
12
+ 1 file(s) changed
13
+ Approval hash: 3ef797046758a06b4f4cae5b20fdca383f4baff6ec653abe6b42597618a0b577
@@ -0,0 +1,12 @@
1
+ # Setup: create config, vault file, init + create staging with modification
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+ soulguard init . --agent-user agent > /dev/null 2>&1
5
+
6
+ # Create staging with modified copy
7
+ mkdir -p .soulguard/staging
8
+ cp SOUL.md .soulguard/staging/SOUL.md
9
+ echo '# My Modified Soul' > .soulguard/staging/SOUL.md
10
+
11
+ # Run diff
12
+ soulguard diff .
@@ -0,0 +1,6 @@
1
+ Soulguard Diff — /workspace
2
+
3
+ ⚠️ SOUL.md (vault file missing — new file)
4
+
5
+ 1 file(s) changed
6
+ Approval hash: ddc1ac8615d0e23d031c518f50b17bacc02fd15e5e5cd1a6e2993978f50221a0
@@ -0,0 +1,10 @@
1
+ # Setup: create config, vault file, init — then delete the vault file
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+ soulguard init . --agent-user agent > /dev/null 2>&1
5
+
6
+ # Delete the vault file so staging exists but vault doesn't
7
+ rm SOUL.md
8
+
9
+ # Run diff — should show vault_missing status
10
+ soulguard diff .
@@ -0,0 +1,52 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 3 vault file(s)
7
+
8
+ Done.
9
+ STATUS:
10
+ Soulguard Status — /workspace
11
+
12
+ Vault
13
+ ✅ soulguard.json
14
+
15
+ Ledger
16
+ ✅ memory/2026-01-01.md
17
+ ✅ memory/2026-01-02.md
18
+
19
+ 3 files ok, 0 drifted, 0 missing
20
+ STATUS AFTER DRIFT:
21
+ Soulguard Status — /workspace
22
+
23
+ Vault
24
+ ✅ soulguard.json
25
+
26
+ Ledger
27
+ ⚠️ memory/2026-01-01.md
28
+ owner is root, expected agent
29
+ group is root, expected soulguard
30
+ ✅ memory/2026-01-02.md
31
+
32
+ 2 files ok, 1 drifted, 0 missing
33
+ SYNC:
34
+ Soulguard Sync — /workspace
35
+
36
+ Fixed:
37
+ 🔧 memory/2026-01-01.md
38
+ owner is root, expected agent
39
+ group is root, expected soulguard
40
+
41
+ All files now ok.
42
+ STATUS AFTER SYNC:
43
+ Soulguard Status — /workspace
44
+
45
+ Vault
46
+ ✅ soulguard.json
47
+
48
+ Ledger
49
+ ✅ memory/2026-01-01.md
50
+ ✅ memory/2026-01-02.md
51
+
52
+ 3 files ok, 0 drifted, 0 missing
@@ -0,0 +1,28 @@
1
+ # Glob ledger files: ledger protects "memory/*.md" pattern.
2
+ # Tests that glob patterns resolve to actual files in status and sync.
3
+
4
+ # Setup: config with glob ledger pattern
5
+ cat > soulguard.json <<'EOF'
6
+ {"vault":["soulguard.json"],"ledger":["memory/*.md"]}
7
+ EOF
8
+ mkdir -p memory
9
+ echo '# Day 1' > memory/2026-01-01.md
10
+ echo '# Day 2' > memory/2026-01-02.md
11
+
12
+ # Owner runs init
13
+ soulguard init . --agent-user agent
14
+
15
+ echo "STATUS:"
16
+ NO_COLOR=1 soulguard status . 2>&1
17
+
18
+ # Simulate drift: wrong ownership on a ledger file
19
+ chown root:root memory/2026-01-01.md
20
+
21
+ echo "STATUS AFTER DRIFT:"
22
+ NO_COLOR=1 soulguard status . 2>&1
23
+
24
+ echo "SYNC:"
25
+ soulguard sync . 2>&1
26
+
27
+ echo "STATUS AFTER SYNC:"
28
+ NO_COLOR=1 soulguard status . 2>&1
@@ -0,0 +1,41 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 3 vault file(s)
7
+
8
+ Done.
9
+ STATUS:
10
+ Soulguard Status — /workspace
11
+
12
+ Vault
13
+ ✅ skills/python.md
14
+ ✅ skills/typescript.md
15
+ ✅ soulguard.json
16
+
17
+ 3 files ok, 0 drifted, 0 missing
18
+ DIFF:
19
+ Soulguard Diff — /workspace
20
+
21
+ 📝 skills/python.md
22
+ ===================================================================
23
+ --- a/skills/python.md
24
+ +++ b/skills/python.md
25
+ @@ -1,1 +1,1 @@
26
+ -# Python
27
+ +# Python v2
28
+
29
+ ✅ skills/typescript.md (no changes)
30
+ ✅ soulguard.json (no changes)
31
+
32
+ 1 file(s) changed
33
+ Approval hash: dddd99b50cc6db47e3ab9cdcd8730fc93bb3eb4850bd5f2e01ad9c8a5b4a4dbb
34
+ APPROVE:
35
+
36
+ Approved 1 file(s):
37
+ ✅ skills/python.md
38
+
39
+ Vault updated. Staging synced.
40
+ VAULT CONTENT:
41
+ # Python v2
@@ -0,0 +1,30 @@
1
+ # Glob vault files: vault protects "skills/*.md" pattern.
2
+ # Tests that glob patterns resolve to actual files in status, diff, and approve.
3
+
4
+ # Setup: vault with glob + a couple matching files
5
+ cat > soulguard.json <<'EOF'
6
+ {"vault":["soulguard.json","skills/*.md"],"ledger":[]}
7
+ EOF
8
+ mkdir -p skills
9
+ echo '# Python' > skills/python.md
10
+ echo '# TypeScript' > skills/typescript.md
11
+
12
+ # Owner runs init
13
+ soulguard init . --agent-user agent
14
+
15
+ echo "STATUS:"
16
+ NO_COLOR=1 soulguard status . 2>&1
17
+
18
+ # Agent modifies a skill
19
+ su - agent -c "echo '# Python v2' > $(pwd)/.soulguard/staging/skills/python.md"
20
+
21
+ echo "DIFF:"
22
+ NO_COLOR=1 soulguard diff . 2>&1
23
+
24
+ # Approve the change
25
+ HASH=$(NO_COLOR=1 soulguard diff . 2>&1 | grep 'Approval hash:' | awk '{print $NF}')
26
+ echo "APPROVE:"
27
+ soulguard approve . --hash "$HASH"
28
+
29
+ echo "VAULT CONTENT:"
30
+ cat skills/python.md
@@ -0,0 +1,11 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 2 vault file(s)
7
+
8
+ Done.
9
+ --- AGENT TRIES INIT ---
10
+ sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
11
+ sudo: a password is required
@@ -0,0 +1,15 @@
1
+ # After owner runs init, agent can't run init again.
2
+ # Init writes scoped sudoers that excludes init and approve.
3
+
4
+ echo '# My Soul' > SOUL.md
5
+ cat > soulguard.json <<'EOF'
6
+ {"vault":["SOUL.md","soulguard.json"],"ledger":[]}
7
+ EOF
8
+
9
+ # Owner runs init (as root)
10
+ soulguard init . --agent-user agent
11
+
12
+ echo "--- AGENT TRIES INIT ---"
13
+
14
+ # Agent tries init — should be denied by scoped sudoers
15
+ su - agent -c "sudo soulguard init $(pwd) --agent-user agent" 2>&1
@@ -0,0 +1,18 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 2 vault file(s)
7
+
8
+ Done.
9
+ Soulguard Status — /workspace
10
+
11
+ Vault
12
+ ✅ SOUL.md
13
+ ✅ soulguard.json
14
+
15
+ 2 files ok, 0 drifted, 0 missing
16
+ .soulguard/staging/SOUL.md
17
+ STAGING: OK
18
+ -bash: line 1: /workspace/SOUL.md: Permission denied
@@ -0,0 +1,20 @@
1
+ # Init test: owner sets up workspace, verify protection works for agent
2
+ # Runs as root (owner), switches to agent for verification
3
+
4
+ # Create a minimal config and soul file
5
+ echo '# My Soul' > SOUL.md
6
+ cat > soulguard.json <<'EOF'
7
+ {"vault":["SOUL.md","soulguard.json"],"ledger":[]}
8
+ EOF
9
+
10
+ # Owner runs init
11
+ soulguard init . --agent-user agent
12
+
13
+ # Owner verifies status is clean
14
+ soulguard status .
15
+
16
+ # Verify staging copy exists
17
+ ls .soulguard/staging/SOUL.md && echo "STAGING: OK" || echo "STAGING: MISSING"
18
+
19
+ # Agent can't write to vault file
20
+ su - agent -c "(echo hacked > $(pwd)/SOUL.md) 2>&1"
@@ -0,0 +1,13 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 2 vault file(s)
7
+
8
+ Done.
9
+ --- SECOND RUN ---
10
+ Soulguard Init — /workspace
11
+ Created .soulguard/staging/
12
+
13
+ Done.
@@ -0,0 +1,14 @@
1
+ # Init twice: second run should report nothing to do
2
+
3
+ echo '# My Soul' > SOUL.md
4
+ cat > soulguard.json <<'EOF'
5
+ {"vault":["SOUL.md","soulguard.json"],"ledger":[]}
6
+ EOF
7
+
8
+ # First init (as owner/root)
9
+ soulguard init . --agent-user agent
10
+
11
+ echo "--- SECOND RUN ---"
12
+
13
+ # Second init — should be idempotent
14
+ soulguard init . --agent-user agent
@@ -0,0 +1,16 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 2 vault file(s)
7
+
8
+ Done.
9
+ Soulguard Reset — /workspace
10
+
11
+ Reset 1 staging file(s):
12
+ ↩️ SOUL.md
13
+ VAULT:
14
+ # My Soul
15
+ STAGING:
16
+ # My Soul
@@ -0,0 +1,23 @@
1
+ # Reset staging (implicit proposal)
2
+
3
+ echo '# My Soul' > SOUL.md
4
+ cat > soulguard.json <<'EOF'
5
+ {"vault":["SOUL.md","soulguard.json"],"ledger":[]}
6
+ EOF
7
+
8
+ # Owner runs init
9
+ soulguard init . --agent-user agent
10
+
11
+ # Agent modifies staging
12
+ su - agent -c "echo '# Hacked Soul' > $(pwd)/.soulguard/staging/SOUL.md"
13
+
14
+ # Owner resets staging
15
+ soulguard reset .
16
+
17
+ # Verify vault unchanged
18
+ echo "VAULT:"
19
+ cat SOUL.md
20
+
21
+ # Verify staging reset
22
+ echo "STAGING:"
23
+ cat .soulguard/staging/SOUL.md
@@ -0,0 +1,12 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 2 vault file(s)
7
+
8
+ Done.
9
+ APPROVE:
10
+ Self-protection: soulguard.json would be invalid after this change: ledger: Required
11
+ CONFIG:
12
+ {"vault":["SOUL.md","soulguard.json"],"ledger":[]}
@@ -0,0 +1,25 @@
1
+ # Self-protection: approve blocks invalid soulguard.json changes.
2
+ # Even if the owner provides the correct hash, soulguard refuses to
3
+ # brick itself by writing an invalid config.
4
+
5
+ cat > soulguard.json <<'EOF'
6
+ {"vault":["SOUL.md","soulguard.json"],"ledger":[]}
7
+ EOF
8
+ echo '# My Soul' > SOUL.md
9
+
10
+ # Owner runs init
11
+ soulguard init . --agent-user agent
12
+
13
+ # Agent writes invalid config to staging (missing "ledger" field)
14
+ su - agent -c "echo '{\"vault\":[\"SOUL.md\"]}' > $(pwd)/.soulguard/staging/soulguard.json"
15
+
16
+ # Get hash — diff will show the change
17
+ HASH=$(NO_COLOR=1 soulguard diff . 2>&1 | grep 'Approval hash:' | awk '{print $NF}')
18
+
19
+ # Try to approve — should be blocked by self-protection
20
+ echo "APPROVE:"
21
+ soulguard approve . --hash "$HASH" 2>&1 || true
22
+
23
+ # Verify soulguard.json is unchanged
24
+ echo "CONFIG:"
25
+ cat soulguard.json
@@ -0,0 +1,9 @@
1
+ Soulguard Sync — /workspace
2
+
3
+ Nothing to fix — all files ok.
4
+ Soulguard Status — /workspace
5
+
6
+ Vault
7
+ ✅ SOUL.md
8
+
9
+ 1 files ok, 0 drifted, 0 missing
@@ -0,0 +1,8 @@
1
+ # Setup: create config and vault file, init + sync (as owner)
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+ soulguard init . --agent-user agent > /dev/null 2>&1
5
+ soulguard sync .
6
+
7
+ # Status check
8
+ soulguard status .
@@ -0,0 +1,9 @@
1
+ Soulguard Status — /workspace
2
+
3
+ Vault
4
+ ⚠️ SOUL.md
5
+ owner is root, expected soulguardian
6
+ group is root, expected soulguard
7
+ mode is 644, expected 444
8
+
9
+ 0 files ok, 1 drifted, 0 missing
@@ -0,0 +1,11 @@
1
+ # Setup: create config and vault file, init for user/group but don't sync
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+ soulguard init . --agent-user agent > /dev/null 2>&1
5
+
6
+ # Simulate drift: reset ownership to root
7
+ chown root:root SOUL.md
8
+ chmod 644 SOUL.md
9
+
10
+ # Status doesn't need sudo — it's read-only
11
+ soulguard status .
@@ -0,0 +1 @@
1
+ No soulguard.json found in .
@@ -0,0 +1,2 @@
1
+ # No soulguard.json → error
2
+ soulguard status .
@@ -0,0 +1,15 @@
1
+ Soulguard Sync — /workspace
2
+
3
+ Fixed:
4
+ 🔧 SOUL.md
5
+ owner is root, expected soulguardian
6
+ group is root, expected soulguard
7
+ mode is 644, expected 444
8
+
9
+ All files now ok.
10
+ Soulguard Status — /workspace
11
+
12
+ Vault
13
+ ✅ SOUL.md
14
+
15
+ 1 files ok, 0 drifted, 0 missing
@@ -0,0 +1,14 @@
1
+ # Setup: create config and vault file, init for user/group
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+ soulguard init . --agent-user agent > /dev/null 2>&1
5
+
6
+ # Simulate drift: reset ownership to root
7
+ chown root:root SOUL.md
8
+ chmod 644 SOUL.md
9
+
10
+ # Owner syncs to fix drift
11
+ soulguard sync .
12
+
13
+ # Verify status is clean after sync
14
+ soulguard status .
@@ -0,0 +1,11 @@
1
+ Soulguard Init — /workspace
2
+ Created group: soulguard
3
+ Created user: soulguardian
4
+ Wrote /etc/sudoers.d/soulguard
5
+ Created .soulguard/staging/
6
+ Synced 1 vault file(s)
7
+
8
+ Done.
9
+ Soulguard Sync — /workspace
10
+
11
+ Nothing to fix — all files ok.
@@ -0,0 +1,9 @@
1
+ # After init, agent can sudo soulguard sync on a clean workspace
2
+ echo '{"vault": ["SOUL.md"], "ledger": []}' > soulguard.json
3
+ echo '# My Soul' > SOUL.md
4
+
5
+ # Owner runs init (creates sudoers for agent)
6
+ soulguard init . --agent-user agent
7
+
8
+ # Agent syncs via scoped sudoers — workspace is already clean
9
+ su - agent -c "sudo soulguard sync $(pwd)"
@@ -0,0 +1,7 @@
1
+ Soulguard Sync — /workspace
2
+
3
+ Errors:
4
+ ❌ SOUL.md: chown failed (io_error)
5
+
6
+ 1 issue(s) remaining after sync:
7
+ ⚠️ SOUL.md