@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.
- package/README.md +127 -0
- package/dist/cli.js +8838 -0
- package/dist/index.js +6555 -0
- package/package.json +35 -0
- package/src/approve.test.ts +386 -0
- package/src/approve.ts +352 -0
- package/src/cli/approve-command.ts +107 -0
- package/src/cli/diff-command.test.ts +61 -0
- package/src/cli/diff-command.ts +81 -0
- package/src/cli/init-command.ts +83 -0
- package/src/cli/reset-command.ts +36 -0
- package/src/cli/status-command.test.ts +90 -0
- package/src/cli/status-command.ts +78 -0
- package/src/cli/sync-command.test.ts +67 -0
- package/src/cli/sync-command.ts +105 -0
- package/src/cli.ts +224 -0
- package/src/console-live.ts +32 -0
- package/src/console-mock.ts +48 -0
- package/src/console.ts +12 -0
- package/src/constants.ts +21 -0
- package/src/diff.test.ts +189 -0
- package/src/diff.ts +212 -0
- package/src/git.test.ts +180 -0
- package/src/git.ts +131 -0
- package/src/glob.test.ts +74 -0
- package/src/glob.ts +84 -0
- package/src/index.ts +100 -0
- package/src/init.test.ts +234 -0
- package/src/init.ts +317 -0
- package/src/policy.test.ts +123 -0
- package/src/policy.ts +100 -0
- package/src/reset.test.ts +68 -0
- package/src/reset.ts +63 -0
- package/src/result.ts +14 -0
- package/src/schema.test.ts +27 -0
- package/src/schema.ts +22 -0
- package/src/self-protection.test.ts +139 -0
- package/src/self-protection.ts +63 -0
- package/src/status.test.ts +241 -0
- package/src/status.ts +114 -0
- package/src/sync.test.ts +172 -0
- package/src/sync.ts +101 -0
- package/src/system-ops-mock.ts +243 -0
- package/src/system-ops-node.test.ts +183 -0
- package/src/system-ops-node.ts +499 -0
- package/src/system-ops.ts +109 -0
- package/src/types.ts +91 -0
- package/src/vault-check.test.ts +41 -0
- package/src/vault-check.ts +24 -0
- package/test-e2e/Dockerfile +29 -0
- package/test-e2e/cases/approve-with-hash/expected.txt +16 -0
- package/test-e2e/cases/approve-with-hash/test.sh +23 -0
- package/test-e2e/cases/diff-no-changes/expected.txt +5 -0
- package/test-e2e/cases/diff-no-changes/test.sh +7 -0
- package/test-e2e/cases/diff-no-staging/expected.txt +1 -0
- package/test-e2e/cases/diff-no-staging/test.sh +6 -0
- package/test-e2e/cases/diff-shows-changes/expected.txt +13 -0
- package/test-e2e/cases/diff-shows-changes/test.sh +12 -0
- package/test-e2e/cases/diff-vault-missing/expected.txt +6 -0
- package/test-e2e/cases/diff-vault-missing/test.sh +10 -0
- package/test-e2e/cases/glob-ledger-files/expected.txt +52 -0
- package/test-e2e/cases/glob-ledger-files/test.sh +28 -0
- package/test-e2e/cases/glob-vault-files/expected.txt +41 -0
- package/test-e2e/cases/glob-vault-files/test.sh +30 -0
- package/test-e2e/cases/init-blocked-by-agent/expected.txt +11 -0
- package/test-e2e/cases/init-blocked-by-agent/test.sh +15 -0
- package/test-e2e/cases/init-happy/expected.txt +18 -0
- package/test-e2e/cases/init-happy/test.sh +20 -0
- package/test-e2e/cases/init-idempotent/expected.txt +13 -0
- package/test-e2e/cases/init-idempotent/test.sh +14 -0
- package/test-e2e/cases/reset-staging/expected.txt +16 -0
- package/test-e2e/cases/reset-staging/test.sh +23 -0
- package/test-e2e/cases/self-protection-blocks-invalid/expected.txt +12 -0
- package/test-e2e/cases/self-protection-blocks-invalid/test.sh +25 -0
- package/test-e2e/cases/status-clean/expected.txt +9 -0
- package/test-e2e/cases/status-clean/test.sh +8 -0
- package/test-e2e/cases/status-drifted/expected.txt +9 -0
- package/test-e2e/cases/status-drifted/test.sh +11 -0
- package/test-e2e/cases/status-no-config/expected.txt +1 -0
- package/test-e2e/cases/status-no-config/test.sh +2 -0
- package/test-e2e/cases/sync-fix-drift/expected.txt +15 -0
- package/test-e2e/cases/sync-fix-drift/test.sh +14 -0
- package/test-e2e/cases/sync-no-sudo-clean/expected.txt +11 -0
- package/test-e2e/cases/sync-no-sudo-clean/test.sh +9 -0
- package/test-e2e/cases/sync-no-sudo-drift/expected.txt +7 -0
- package/test-e2e/cases/sync-no-sudo-drift/test.sh +10 -0
- package/test-e2e/cases/vault-remove-file/expected.txt +26 -0
- package/test-e2e/cases/vault-remove-file/test.sh +31 -0
- package/test-e2e/cases/vault-write-blocked/expected.txt +8 -0
- package/test-e2e/cases/vault-write-blocked/test.sh +14 -0
- package/test-e2e/run-tests.sh +76 -0
- package/test-integration/Dockerfile +17 -0
- package/test-integration/system-ops-node.integration.test.ts +170 -0
- 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,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,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,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,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,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,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,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)"
|