soulguard 0.2.1 → 0.2.3
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/dist/index.js +386 -206
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4096,7 +4096,8 @@ var ownershipSchema = exports_external.object({
|
|
|
4096
4096
|
mode: exports_external.string()
|
|
4097
4097
|
});
|
|
4098
4098
|
var daemonConfigSchema = exports_external.object({
|
|
4099
|
-
channel: exports_external.string()
|
|
4099
|
+
channel: exports_external.string().optional(),
|
|
4100
|
+
syncIntervalSecs: exports_external.number().int().nonnegative().optional()
|
|
4100
4101
|
}).passthrough();
|
|
4101
4102
|
var soulguardConfigSchema = exports_external.object({
|
|
4102
4103
|
version: exports_external.literal(1),
|
|
@@ -6153,6 +6154,7 @@ ${approvalMessage}`;
|
|
|
6153
6154
|
}
|
|
6154
6155
|
|
|
6155
6156
|
// ../core/src/sdk/sync.ts
|
|
6157
|
+
import { dirname as dirname2 } from "node:path";
|
|
6156
6158
|
async function sync(options) {
|
|
6157
6159
|
const { tree, ops, config } = options;
|
|
6158
6160
|
const drifts = tree.driftedEntities();
|
|
@@ -6182,8 +6184,38 @@ async function sync(options) {
|
|
|
6182
6184
|
}
|
|
6183
6185
|
}
|
|
6184
6186
|
}
|
|
6187
|
+
let stagingCopiesCreated = 0;
|
|
6188
|
+
const protectFiles = tree.flatFiles().filter((f) => f.configTier === "protect");
|
|
6189
|
+
for (const file of protectFiles) {
|
|
6190
|
+
if (file.canonicalHash === null)
|
|
6191
|
+
continue;
|
|
6192
|
+
if (file.stagedHash !== null || file.status === "deleted")
|
|
6193
|
+
continue;
|
|
6194
|
+
const stagePath = stagingPath(file.path);
|
|
6195
|
+
const parentDir = dirname2(stagePath);
|
|
6196
|
+
if (parentDir !== "." && parentDir !== "/" && parentDir !== STAGING_DIR) {
|
|
6197
|
+
await ops.mkdir(parentDir);
|
|
6198
|
+
const defaultOwnership = config.defaultOwnership;
|
|
6199
|
+
if (defaultOwnership) {
|
|
6200
|
+
await ops.chown(parentDir, { user: defaultOwnership.user, group: defaultOwnership.group });
|
|
6201
|
+
await ops.chmod(parentDir, "755");
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
const readResult = await ops.readFile(file.path);
|
|
6205
|
+
if (!readResult.ok)
|
|
6206
|
+
continue;
|
|
6207
|
+
const writeResult = await ops.writeFile(stagePath, readResult.value);
|
|
6208
|
+
if (writeResult.ok) {
|
|
6209
|
+
const defaultOwnership = config.defaultOwnership;
|
|
6210
|
+
if (defaultOwnership) {
|
|
6211
|
+
await ops.chown(stagePath, { user: defaultOwnership.user, group: defaultOwnership.group });
|
|
6212
|
+
await ops.chmod(stagePath, defaultOwnership.mode);
|
|
6213
|
+
}
|
|
6214
|
+
stagingCopiesCreated++;
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6185
6217
|
if (errors2.length > 0) {
|
|
6186
|
-
return ok({ drifts, errors: errors2, git: undefined });
|
|
6218
|
+
return ok({ drifts, errors: errors2, git: undefined, stagingCopiesCreated });
|
|
6187
6219
|
}
|
|
6188
6220
|
let git;
|
|
6189
6221
|
if (await isGitEnabled(ops, config)) {
|
|
@@ -6195,7 +6227,7 @@ async function sync(options) {
|
|
|
6195
6227
|
}
|
|
6196
6228
|
}
|
|
6197
6229
|
}
|
|
6198
|
-
return ok({ drifts, errors: errors2, git });
|
|
6230
|
+
return ok({ drifts, errors: errors2, git, stagingCopiesCreated });
|
|
6199
6231
|
}
|
|
6200
6232
|
// ../core/src/util/console-live.ts
|
|
6201
6233
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
@@ -6280,11 +6312,11 @@ function validateSelfProtection(pendingContents, deletedFiles = []) {
|
|
|
6280
6312
|
return ok(undefined);
|
|
6281
6313
|
}
|
|
6282
6314
|
// ../core/src/sdk/apply.ts
|
|
6283
|
-
import { dirname as
|
|
6315
|
+
import { dirname as dirname3 } from "node:path";
|
|
6284
6316
|
function collectParentDirs(prefix, paths) {
|
|
6285
6317
|
const dirs = new Set;
|
|
6286
6318
|
for (const p of paths) {
|
|
6287
|
-
const parent =
|
|
6319
|
+
const parent = dirname3(`${prefix}/${p}`);
|
|
6288
6320
|
if (parent !== prefix) {
|
|
6289
6321
|
dirs.add(parent);
|
|
6290
6322
|
}
|
|
@@ -6384,7 +6416,7 @@ async function apply(options) {
|
|
|
6384
6416
|
}
|
|
6385
6417
|
} else {
|
|
6386
6418
|
if (file.status === "created") {
|
|
6387
|
-
const parentDir =
|
|
6419
|
+
const parentDir = dirname3(file.path);
|
|
6388
6420
|
if (parentDir !== ".") {
|
|
6389
6421
|
await ops.mkdir(parentDir);
|
|
6390
6422
|
}
|
|
@@ -6432,7 +6464,7 @@ async function apply(options) {
|
|
|
6432
6464
|
const defaultOwnership = tree.config.defaultOwnership;
|
|
6433
6465
|
for (const file of nonDeletedFiles) {
|
|
6434
6466
|
const stagePath = stagingPath(file.path);
|
|
6435
|
-
const stageParent =
|
|
6467
|
+
const stageParent = dirname3(stagePath);
|
|
6436
6468
|
if (stageParent !== STAGING_DIR) {
|
|
6437
6469
|
await ops.mkdir(stageParent);
|
|
6438
6470
|
}
|
|
@@ -6613,10 +6645,10 @@ class SyncCommand {
|
|
|
6613
6645
|
const result = await sync(this.opts);
|
|
6614
6646
|
if (!result.ok)
|
|
6615
6647
|
return 1;
|
|
6616
|
-
const { drifts, errors: errors2, git } = result.value;
|
|
6648
|
+
const { drifts, errors: errors2, git, stagingCopiesCreated } = result.value;
|
|
6617
6649
|
this.out.heading(`Soulguard Sync — ${this.opts.ops.workspace}`);
|
|
6618
6650
|
this.out.write("");
|
|
6619
|
-
if (drifts.length === 0 && errors2.length === 0) {
|
|
6651
|
+
if (drifts.length === 0 && errors2.length === 0 && stagingCopiesCreated === 0) {
|
|
6620
6652
|
this.out.success("Nothing to fix — all files ok.");
|
|
6621
6653
|
this.reportGit(git);
|
|
6622
6654
|
return 0;
|
|
@@ -6639,6 +6671,10 @@ class SyncCommand {
|
|
|
6639
6671
|
this.out.write("");
|
|
6640
6672
|
return 1;
|
|
6641
6673
|
}
|
|
6674
|
+
if (stagingCopiesCreated > 0) {
|
|
6675
|
+
this.out.success(` Refreshed ${stagingCopiesCreated} staging ${stagingCopiesCreated === 1 ? "copy" : "copies"}.`);
|
|
6676
|
+
this.out.write("");
|
|
6677
|
+
}
|
|
6642
6678
|
this.out.success("All files now ok.");
|
|
6643
6679
|
this.reportGit(git);
|
|
6644
6680
|
return 0;
|
|
@@ -6815,6 +6851,54 @@ class ResetCommand {
|
|
|
6815
6851
|
return 0;
|
|
6816
6852
|
}
|
|
6817
6853
|
}
|
|
6854
|
+
// ../core/src/sdk/staging-ops.ts
|
|
6855
|
+
import { dirname as dirname4 } from "node:path";
|
|
6856
|
+
function isInProtectTier(path, protectPatterns2) {
|
|
6857
|
+
for (const pattern of protectPatterns2) {
|
|
6858
|
+
const normalized = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
6859
|
+
if (path === normalized || path === pattern)
|
|
6860
|
+
return true;
|
|
6861
|
+
if (path.startsWith(normalized + "/"))
|
|
6862
|
+
return true;
|
|
6863
|
+
}
|
|
6864
|
+
return false;
|
|
6865
|
+
}
|
|
6866
|
+
async function ensureStagingParentDir(ops, canonicalPath, ownership) {
|
|
6867
|
+
const stagePath = stagingPath(canonicalPath);
|
|
6868
|
+
const parentDir = dirname4(stagePath);
|
|
6869
|
+
if (parentDir === "." || parentDir === "/" || parentDir === STAGING_DIR) {
|
|
6870
|
+
return ok(undefined);
|
|
6871
|
+
}
|
|
6872
|
+
const mkdirResult = await ops.mkdir(parentDir);
|
|
6873
|
+
if (!mkdirResult.ok) {
|
|
6874
|
+
return err(`Cannot create parent directory ${parentDir}: ${mkdirResult.error.kind}`);
|
|
6875
|
+
}
|
|
6876
|
+
if (ownership) {
|
|
6877
|
+
await ops.chown(parentDir, { user: ownership.user, group: ownership.group });
|
|
6878
|
+
await ops.chmod(parentDir, "755");
|
|
6879
|
+
}
|
|
6880
|
+
return ok(undefined);
|
|
6881
|
+
}
|
|
6882
|
+
async function createStagingCopy(ops, path, ownership) {
|
|
6883
|
+
const stagePath = stagingPath(path);
|
|
6884
|
+
const parentResult = await ensureStagingParentDir(ops, path, ownership);
|
|
6885
|
+
if (!parentResult.ok)
|
|
6886
|
+
return parentResult;
|
|
6887
|
+
const readResult = await ops.readFile(path);
|
|
6888
|
+
if (!readResult.ok) {
|
|
6889
|
+
return err(`Cannot read ${path}: ${readResult.error.kind}`);
|
|
6890
|
+
}
|
|
6891
|
+
const writeResult = await ops.writeFile(stagePath, readResult.value);
|
|
6892
|
+
if (!writeResult.ok) {
|
|
6893
|
+
return err(`Cannot write staging copy ${stagePath}: ${writeResult.error.kind}`);
|
|
6894
|
+
}
|
|
6895
|
+
if (ownership) {
|
|
6896
|
+
await ops.chown(stagePath, { user: ownership.user, group: ownership.group });
|
|
6897
|
+
await ops.chmod(stagePath, ownership.mode);
|
|
6898
|
+
}
|
|
6899
|
+
return ok(undefined);
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6818
6902
|
// ../core/src/cli/tier-command.ts
|
|
6819
6903
|
function formatSummary(paths) {
|
|
6820
6904
|
const dirs = paths.filter((p) => p.endsWith("/")).length;
|
|
@@ -6981,6 +7065,34 @@ class TierCommand {
|
|
|
6981
7065
|
return 1;
|
|
6982
7066
|
}
|
|
6983
7067
|
}
|
|
7068
|
+
const defaultOwnership = configResult.value.defaultOwnership;
|
|
7069
|
+
await ops.mkdir(".soulguard-staging");
|
|
7070
|
+
if (defaultOwnership) {
|
|
7071
|
+
await ops.chown(".soulguard-staging", {
|
|
7072
|
+
user: defaultOwnership.user,
|
|
7073
|
+
group: defaultOwnership.group
|
|
7074
|
+
});
|
|
7075
|
+
await ops.chmod(".soulguard-staging", "755");
|
|
7076
|
+
}
|
|
7077
|
+
for (const file of changedPaths) {
|
|
7078
|
+
const isDir = await isDirectory(ops, file);
|
|
7079
|
+
if (isDir) {
|
|
7080
|
+
const listResult = await ops.listDir(file);
|
|
7081
|
+
if (listResult.ok) {
|
|
7082
|
+
for (const childPath of listResult.value) {
|
|
7083
|
+
const copyResult = await createStagingCopy(ops, childPath, defaultOwnership ?? undefined);
|
|
7084
|
+
if (!copyResult.ok) {
|
|
7085
|
+
this.out.warn(` Warning: staging copy failed for ${childPath}: ${copyResult.error}`);
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
} else {
|
|
7090
|
+
const copyResult = await createStagingCopy(ops, file, defaultOwnership ?? undefined);
|
|
7091
|
+
if (!copyResult.ok) {
|
|
7092
|
+
this.out.warn(` Warning: staging copy failed for ${file}: ${copyResult.error}`);
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
6984
7096
|
} else if (action.kind === "set" && action.tier === "watch") {
|
|
6985
7097
|
const defaultOwnership = configResult.value.defaultOwnership;
|
|
6986
7098
|
for (const file of changedPaths) {
|
|
@@ -7018,153 +7130,114 @@ class TierCommand {
|
|
|
7018
7130
|
return 0;
|
|
7019
7131
|
}
|
|
7020
7132
|
}
|
|
7021
|
-
// ../core/src/sdk/
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
if (path === normalized || path === pattern)
|
|
7027
|
-
return true;
|
|
7028
|
-
if (path.startsWith(normalized + "/"))
|
|
7029
|
-
return true;
|
|
7030
|
-
}
|
|
7031
|
-
return false;
|
|
7032
|
-
}
|
|
7033
|
-
async function ensureParentDir(ops, path) {
|
|
7034
|
-
const parentDir = dirname3(path);
|
|
7035
|
-
if (parentDir === "." || parentDir === "/") {
|
|
7036
|
-
return ok(undefined);
|
|
7133
|
+
// ../core/src/sdk/create.ts
|
|
7134
|
+
async function create(options) {
|
|
7135
|
+
const { ops, config, path } = options;
|
|
7136
|
+
if (!isInProtectTier(path, protectPatterns(config))) {
|
|
7137
|
+
return err({ kind: "not_in_protect_tier", path });
|
|
7037
7138
|
}
|
|
7038
|
-
const
|
|
7039
|
-
if (
|
|
7040
|
-
return
|
|
7139
|
+
const exists = await ops.exists(path);
|
|
7140
|
+
if (exists.ok && exists.value) {
|
|
7141
|
+
return ok({ path, alreadyExisted: true });
|
|
7041
7142
|
}
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
const protectFiles = protectPatterns(config);
|
|
7047
|
-
if (!isInProtectTier(path, protectFiles)) {
|
|
7048
|
-
return err({ kind: "not_in_protect_tier", path });
|
|
7143
|
+
const stagePath = stagingPath(path);
|
|
7144
|
+
const parentResult = await ensureStagingParentDir(ops, path);
|
|
7145
|
+
if (!parentResult.ok) {
|
|
7146
|
+
return err({ kind: "create_failed", path, message: parentResult.error });
|
|
7049
7147
|
}
|
|
7050
|
-
const
|
|
7051
|
-
|
|
7052
|
-
if (!pathStat.ok && pathStat.error.kind !== "not_found") {
|
|
7148
|
+
const writeResult = await ops.writeFile(stagePath, "");
|
|
7149
|
+
if (!writeResult.ok) {
|
|
7053
7150
|
return err({
|
|
7054
|
-
kind: "
|
|
7151
|
+
kind: "create_failed",
|
|
7055
7152
|
path,
|
|
7056
|
-
message: `Cannot
|
|
7153
|
+
message: `Cannot create staging file: ${writeResult.error.kind}`
|
|
7057
7154
|
});
|
|
7058
7155
|
}
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
return err({
|
|
7076
|
-
kind: "stage_failed",
|
|
7077
|
-
path,
|
|
7078
|
-
message: `Cannot write delete sentinel: ${writeResult.error.kind}`
|
|
7079
|
-
});
|
|
7080
|
-
}
|
|
7081
|
-
stagedFiles.push({ path, action: "delete" });
|
|
7082
|
-
} else if (isDirectory2 && !isDelete) {
|
|
7083
|
-
const listResult = await ops.listDir(path);
|
|
7084
|
-
if (!listResult.ok) {
|
|
7085
|
-
return err({
|
|
7086
|
-
kind: "stage_failed",
|
|
7087
|
-
path,
|
|
7088
|
-
message: `Cannot list directory: ${listResult.error.kind}`
|
|
7089
|
-
});
|
|
7156
|
+
return ok({ path, alreadyExisted: false });
|
|
7157
|
+
}
|
|
7158
|
+
|
|
7159
|
+
// ../core/src/cli/create-command.ts
|
|
7160
|
+
class CreateCommand {
|
|
7161
|
+
opts;
|
|
7162
|
+
out;
|
|
7163
|
+
constructor(opts, out) {
|
|
7164
|
+
this.opts = opts;
|
|
7165
|
+
this.out = out;
|
|
7166
|
+
}
|
|
7167
|
+
async execute() {
|
|
7168
|
+
const { ops, config, paths } = this.opts;
|
|
7169
|
+
if (paths.length === 0) {
|
|
7170
|
+
this.out.error("No paths specified.");
|
|
7171
|
+
return 1;
|
|
7090
7172
|
}
|
|
7091
|
-
|
|
7092
|
-
|
|
7173
|
+
let created = 0;
|
|
7174
|
+
for (const path of paths) {
|
|
7175
|
+
const result = await create({ ops, config, path });
|
|
7093
7176
|
if (!result.ok) {
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
const readResult = await ops.readFile(stagePath);
|
|
7103
|
-
if (readResult.ok && isDeleteSentinel(readResult.value)) {
|
|
7104
|
-
return ok({ stagedFiles: [] });
|
|
7177
|
+
switch (result.error.kind) {
|
|
7178
|
+
case "not_in_protect_tier":
|
|
7179
|
+
this.out.error(`${result.error.path} is not in the protect tier.`);
|
|
7180
|
+
return 1;
|
|
7181
|
+
case "create_failed":
|
|
7182
|
+
this.out.error(`Failed to create ${result.error.path}: ${result.error.message}`);
|
|
7183
|
+
return 1;
|
|
7184
|
+
}
|
|
7105
7185
|
}
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
return err({ kind: "stage_failed", path, message: parentResult.error });
|
|
7110
|
-
}
|
|
7111
|
-
const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
|
|
7112
|
-
if (!writeResult.ok) {
|
|
7113
|
-
return err({
|
|
7114
|
-
kind: "stage_failed",
|
|
7115
|
-
path,
|
|
7116
|
-
message: `Cannot write delete sentinel: ${writeResult.error.kind}`
|
|
7117
|
-
});
|
|
7118
|
-
}
|
|
7119
|
-
stagedFiles.push({ path, action: "delete" });
|
|
7120
|
-
} else if (!isDirectory2 && !isDelete) {
|
|
7121
|
-
const stagePath = stagingPath(path);
|
|
7122
|
-
const existsResult = await ops.exists(stagePath);
|
|
7123
|
-
if (existsResult.ok && existsResult.value) {
|
|
7124
|
-
const readResult = await ops.readFile(stagePath);
|
|
7125
|
-
if (readResult.ok && isDeleteSentinel(readResult.value)) {} else {
|
|
7126
|
-
return ok({ stagedFiles: [] });
|
|
7186
|
+
if (result.value.alreadyExisted) {
|
|
7187
|
+
this.out.warn(` · ${path} (already exists — staging copy was created automatically)`);
|
|
7188
|
+
continue;
|
|
7127
7189
|
}
|
|
7190
|
+
this.out.success(` + ${path} → ${stagingPath(path)}`);
|
|
7191
|
+
created++;
|
|
7128
7192
|
}
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
return err({ kind: "stage_failed", path, message: parentResult.error });
|
|
7132
|
-
}
|
|
7133
|
-
const sourceExists = await ops.exists(path);
|
|
7134
|
-
if (sourceExists.ok && sourceExists.value) {
|
|
7135
|
-
const readResult = await ops.readFile(path);
|
|
7136
|
-
if (!readResult.ok) {
|
|
7137
|
-
return err({
|
|
7138
|
-
kind: "stage_failed",
|
|
7139
|
-
path,
|
|
7140
|
-
message: `Cannot read file: ${readResult.error.kind}`
|
|
7141
|
-
});
|
|
7142
|
-
}
|
|
7143
|
-
const writeResult = await ops.writeFile(stagePath, readResult.value);
|
|
7144
|
-
if (!writeResult.ok) {
|
|
7145
|
-
return err({
|
|
7146
|
-
kind: "stage_failed",
|
|
7147
|
-
path,
|
|
7148
|
-
message: `Cannot write staging copy: ${writeResult.error.kind}`
|
|
7149
|
-
});
|
|
7150
|
-
}
|
|
7193
|
+
if (created === 0) {
|
|
7194
|
+
this.out.info("Nothing to create.");
|
|
7151
7195
|
} else {
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7196
|
+
this.out.write("");
|
|
7197
|
+
this.out.success(`Created ${created} staging ${created === 1 ? "entry" : "entries"}.`);
|
|
7198
|
+
}
|
|
7199
|
+
return 0;
|
|
7200
|
+
}
|
|
7201
|
+
}
|
|
7202
|
+
// ../core/src/sdk/delete.ts
|
|
7203
|
+
async function deleteStagingEntry(options) {
|
|
7204
|
+
const { ops, config, path } = options;
|
|
7205
|
+
if (!isInProtectTier(path, protectPatterns(config))) {
|
|
7206
|
+
return err({ kind: "not_in_protect_tier", path });
|
|
7207
|
+
}
|
|
7208
|
+
const diskExists = await ops.exists(path);
|
|
7209
|
+
const stagePath = stagingPath(path);
|
|
7210
|
+
const stagingExists = await ops.exists(stagePath);
|
|
7211
|
+
if ((!diskExists.ok || !diskExists.value) && (!stagingExists.ok || !stagingExists.value)) {
|
|
7212
|
+
return err({ kind: "not_found", path });
|
|
7213
|
+
}
|
|
7214
|
+
if (stagingExists.ok && stagingExists.value) {
|
|
7215
|
+
const readResult = await ops.readFile(stagePath);
|
|
7216
|
+
if (readResult.ok && isDeleteSentinel(readResult.value)) {
|
|
7217
|
+
return err({ kind: "already_deleted", path });
|
|
7160
7218
|
}
|
|
7161
|
-
stagedFiles.push({ path, action: "edit" });
|
|
7162
7219
|
}
|
|
7163
|
-
|
|
7220
|
+
const stageExists = await ops.stat(stagePath);
|
|
7221
|
+
if (stageExists.ok && stageExists.value.isDirectory) {
|
|
7222
|
+
await ops.exec("rm", ["-rf", stagePath]);
|
|
7223
|
+
}
|
|
7224
|
+
const parentResult = await ensureStagingParentDir(ops, path);
|
|
7225
|
+
if (!parentResult.ok) {
|
|
7226
|
+
return err({ kind: "delete_failed", path, message: parentResult.error });
|
|
7227
|
+
}
|
|
7228
|
+
const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
|
|
7229
|
+
if (!writeResult.ok) {
|
|
7230
|
+
return err({
|
|
7231
|
+
kind: "delete_failed",
|
|
7232
|
+
path,
|
|
7233
|
+
message: `Cannot write delete sentinel: ${writeResult.error.kind}`
|
|
7234
|
+
});
|
|
7235
|
+
}
|
|
7236
|
+
return ok({ path });
|
|
7164
7237
|
}
|
|
7165
7238
|
|
|
7166
|
-
// ../core/src/cli/
|
|
7167
|
-
class
|
|
7239
|
+
// ../core/src/cli/delete-command.ts
|
|
7240
|
+
class DeleteCommand {
|
|
7168
7241
|
opts;
|
|
7169
7242
|
out;
|
|
7170
7243
|
constructor(opts, out) {
|
|
@@ -7172,46 +7245,38 @@ class StageCommand {
|
|
|
7172
7245
|
this.out = out;
|
|
7173
7246
|
}
|
|
7174
7247
|
async execute() {
|
|
7175
|
-
const { ops, config, paths
|
|
7248
|
+
const { ops, config, paths } = this.opts;
|
|
7176
7249
|
if (paths.length === 0) {
|
|
7177
7250
|
this.out.error("No paths specified.");
|
|
7178
7251
|
return 1;
|
|
7179
7252
|
}
|
|
7180
|
-
|
|
7181
|
-
const alreadyStaged = [];
|
|
7253
|
+
let deleted = 0;
|
|
7182
7254
|
for (const path of paths) {
|
|
7183
|
-
const result = await
|
|
7255
|
+
const result = await deleteStagingEntry({ ops, config, path });
|
|
7184
7256
|
if (!result.ok) {
|
|
7185
7257
|
switch (result.error.kind) {
|
|
7186
7258
|
case "not_in_protect_tier":
|
|
7187
7259
|
this.out.error(`${result.error.path} is not in the protect tier.`);
|
|
7188
7260
|
return 1;
|
|
7189
|
-
case "
|
|
7190
|
-
this.out.error(
|
|
7261
|
+
case "not_found":
|
|
7262
|
+
this.out.error(`${result.error.path} does not exist.`);
|
|
7263
|
+
return 1;
|
|
7264
|
+
case "already_deleted":
|
|
7265
|
+
this.out.info(` · ${result.error.path} (already staged for deletion)`);
|
|
7266
|
+
continue;
|
|
7267
|
+
case "delete_failed":
|
|
7268
|
+
this.out.error(`Failed to delete ${result.error.path}: ${result.error.message}`);
|
|
7191
7269
|
return 1;
|
|
7192
7270
|
}
|
|
7193
7271
|
}
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
} else {
|
|
7197
|
-
allStagedFiles.push(...result.value.stagedFiles);
|
|
7198
|
-
}
|
|
7199
|
-
}
|
|
7200
|
-
for (const file of alreadyStaged) {
|
|
7201
|
-
this.out.info(` · ${file} (already staged)`);
|
|
7202
|
-
}
|
|
7203
|
-
for (const { path, action } of allStagedFiles) {
|
|
7204
|
-
if (action === "delete") {
|
|
7205
|
-
this.out.success(` \uD83D\uDDD1️ ${path} (staged for deletion)`);
|
|
7206
|
-
} else {
|
|
7207
|
-
this.out.success(` \uD83D\uDCDD ${path} (staged for editing)`);
|
|
7208
|
-
}
|
|
7272
|
+
this.out.success(` \uD83D\uDDD1️ ${path} (staged for deletion)`);
|
|
7273
|
+
deleted++;
|
|
7209
7274
|
}
|
|
7210
|
-
if (
|
|
7211
|
-
this.out.info("Nothing to
|
|
7275
|
+
if (deleted === 0) {
|
|
7276
|
+
this.out.info("Nothing to do.");
|
|
7212
7277
|
} else {
|
|
7213
7278
|
this.out.write("");
|
|
7214
|
-
this.out.success(`Staged ${
|
|
7279
|
+
this.out.success(`Staged ${deleted} ${deleted === 1 ? "path" : "paths"} for deletion.`);
|
|
7215
7280
|
}
|
|
7216
7281
|
return 0;
|
|
7217
7282
|
}
|
|
@@ -7405,6 +7470,9 @@ class ProposalManager extends EventEmitter {
|
|
|
7405
7470
|
this._abortController = null;
|
|
7406
7471
|
}
|
|
7407
7472
|
}
|
|
7473
|
+
// ../core/src/daemon/daemon.ts
|
|
7474
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
7475
|
+
|
|
7408
7476
|
// ../core/src/daemon/channel-registry.ts
|
|
7409
7477
|
var REGISTRY_KEY = "__soulguard_channel_registry__";
|
|
7410
7478
|
function getRegistry() {
|
|
@@ -7422,13 +7490,16 @@ function getChannel(name) {
|
|
|
7422
7490
|
}
|
|
7423
7491
|
|
|
7424
7492
|
// ../core/src/daemon/daemon.ts
|
|
7425
|
-
class SoulguardDaemon {
|
|
7493
|
+
class SoulguardDaemon extends EventEmitter2 {
|
|
7426
7494
|
_ops;
|
|
7427
7495
|
_config;
|
|
7428
7496
|
_channel = null;
|
|
7429
7497
|
_proposalManager = null;
|
|
7498
|
+
_syncTimer = null;
|
|
7499
|
+
_syncRunning = false;
|
|
7430
7500
|
_running = false;
|
|
7431
7501
|
constructor(options) {
|
|
7502
|
+
super();
|
|
7432
7503
|
this._ops = options.ops;
|
|
7433
7504
|
this._config = options.config;
|
|
7434
7505
|
}
|
|
@@ -7446,28 +7517,40 @@ class SoulguardDaemon {
|
|
|
7446
7517
|
throw new Error("Daemon configuration missing. Add a 'daemon' section to soulguard.json.");
|
|
7447
7518
|
}
|
|
7448
7519
|
const channelName = daemonConfig.channel;
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7520
|
+
if (channelName) {
|
|
7521
|
+
const createChannelFn = getChannel(channelName);
|
|
7522
|
+
if (!createChannelFn) {
|
|
7523
|
+
throw new Error(`No channel registered for "${channelName}". Register it with registerChannel() before starting the daemon.`);
|
|
7524
|
+
}
|
|
7525
|
+
const channelConfig = daemonConfig[channelName];
|
|
7526
|
+
this._channel = createChannelFn(channelConfig);
|
|
7527
|
+
this._proposalManager = new ProposalManager({
|
|
7528
|
+
ops: this._ops,
|
|
7529
|
+
config: this._config,
|
|
7530
|
+
channel: this._channel
|
|
7531
|
+
});
|
|
7532
|
+
this._proposalManager.on("proposed", (...args) => this.emit("proposed", ...args));
|
|
7533
|
+
this._proposalManager.on("applied", (...args) => this.emit("applied", ...args));
|
|
7534
|
+
this._proposalManager.on("rejected", (...args) => this.emit("rejected", ...args));
|
|
7535
|
+
this._proposalManager.on("superseded", (...args) => this.emit("superseded", ...args));
|
|
7536
|
+
this._proposalManager.on("error", (...args) => this.emit("proposal:error", ...args));
|
|
7537
|
+
this._proposalManager.start();
|
|
7538
|
+
}
|
|
7539
|
+
const syncIntervalSecs = daemonConfig.syncIntervalSecs ?? 60;
|
|
7540
|
+
if (syncIntervalSecs > 0) {
|
|
7541
|
+
this._runSync();
|
|
7542
|
+
this._syncTimer = setInterval(() => this._runSync(), syncIntervalSecs * 1000);
|
|
7543
|
+
}
|
|
7465
7544
|
this._running = true;
|
|
7466
7545
|
}
|
|
7467
7546
|
async stop() {
|
|
7468
7547
|
if (!this._running)
|
|
7469
7548
|
return;
|
|
7470
7549
|
this._running = false;
|
|
7550
|
+
if (this._syncTimer) {
|
|
7551
|
+
clearInterval(this._syncTimer);
|
|
7552
|
+
this._syncTimer = null;
|
|
7553
|
+
}
|
|
7471
7554
|
if (this._proposalManager) {
|
|
7472
7555
|
await this._proposalManager.stop();
|
|
7473
7556
|
this._proposalManager = null;
|
|
@@ -7477,6 +7560,35 @@ class SoulguardDaemon {
|
|
|
7477
7560
|
this._channel = null;
|
|
7478
7561
|
}
|
|
7479
7562
|
}
|
|
7563
|
+
async _runSync() {
|
|
7564
|
+
if (this._syncRunning)
|
|
7565
|
+
return;
|
|
7566
|
+
this._syncRunning = true;
|
|
7567
|
+
try {
|
|
7568
|
+
const treeResult = await StateTree.build({
|
|
7569
|
+
ops: this._ops,
|
|
7570
|
+
config: this._config
|
|
7571
|
+
});
|
|
7572
|
+
if (!treeResult.ok) {
|
|
7573
|
+
this.emit("sync:error", new Error(`StateTree.build failed: ${treeResult.error.message}`));
|
|
7574
|
+
return;
|
|
7575
|
+
}
|
|
7576
|
+
const syncResult = await sync({
|
|
7577
|
+
tree: treeResult.value,
|
|
7578
|
+
ops: this._ops,
|
|
7579
|
+
config: this._config
|
|
7580
|
+
});
|
|
7581
|
+
if (!syncResult.ok) {
|
|
7582
|
+
this.emit("sync:error", new Error(`sync failed: ${syncResult.error.message}`));
|
|
7583
|
+
return;
|
|
7584
|
+
}
|
|
7585
|
+
this.emit("synced", syncResult.value);
|
|
7586
|
+
} catch (e) {
|
|
7587
|
+
this.emit("sync:error", e instanceof Error ? e : new Error(String(e)));
|
|
7588
|
+
} finally {
|
|
7589
|
+
this._syncRunning = false;
|
|
7590
|
+
}
|
|
7591
|
+
}
|
|
7480
7592
|
}
|
|
7481
7593
|
// ../core/src/daemon/service.ts
|
|
7482
7594
|
function generateServiceFile(options) {
|
|
@@ -7578,10 +7690,9 @@ var templates = {
|
|
|
7578
7690
|
"workspace/HEARTBEAT.md",
|
|
7579
7691
|
"workspace/BOOTSTRAP.md",
|
|
7580
7692
|
"openclaw.json",
|
|
7581
|
-
"cron/",
|
|
7582
7693
|
"extensions/"
|
|
7583
7694
|
],
|
|
7584
|
-
watch: ["workspace/MEMORY.md", "workspace/memory/", "workspace/skills/"],
|
|
7695
|
+
watch: ["workspace/MEMORY.md", "workspace/memory/", "workspace/skills/", "cron/"],
|
|
7585
7696
|
release: ["workspace/sessions/"]
|
|
7586
7697
|
},
|
|
7587
7698
|
paranoid: {
|
|
@@ -7629,8 +7740,9 @@ var templates = {
|
|
|
7629
7740
|
};
|
|
7630
7741
|
// ../openclaw/src/plugin.ts
|
|
7631
7742
|
import { readFileSync } from "node:fs";
|
|
7743
|
+
import { access as access2, chmod, copyFile, mkdir } from "node:fs/promises";
|
|
7632
7744
|
import os from "node:os";
|
|
7633
|
-
import { join as join2 } from "node:path";
|
|
7745
|
+
import { dirname as dirname5, join as join2 } from "node:path";
|
|
7634
7746
|
|
|
7635
7747
|
// ../openclaw/src/guard.ts
|
|
7636
7748
|
import path from "node:path";
|
|
@@ -7638,40 +7750,57 @@ var WRITE_TOOLS = new Set(["write", "edit"]);
|
|
|
7638
7750
|
var PATH_KEYS = ["file_path", "path", "file"];
|
|
7639
7751
|
function guardToolCall(toolName, params, options) {
|
|
7640
7752
|
if (!WRITE_TOOLS.has(toolName.toLowerCase())) {
|
|
7641
|
-
return {
|
|
7753
|
+
return { action: "allow" };
|
|
7642
7754
|
}
|
|
7643
|
-
let
|
|
7755
|
+
let pathKey;
|
|
7756
|
+
let rawPath;
|
|
7644
7757
|
for (const key of PATH_KEYS) {
|
|
7645
7758
|
const v = params[key];
|
|
7646
7759
|
if (typeof v === "string" && v.length > 0) {
|
|
7647
|
-
|
|
7760
|
+
pathKey = key;
|
|
7761
|
+
rawPath = v;
|
|
7648
7762
|
break;
|
|
7649
7763
|
}
|
|
7650
7764
|
}
|
|
7651
|
-
if (
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7765
|
+
if (!pathKey || !rawPath)
|
|
7766
|
+
return { action: "allow" };
|
|
7767
|
+
const isAbsolute = path.isAbsolute(rawPath);
|
|
7768
|
+
const relativePath = isAbsolute ? path.relative(options.stateDir, rawPath) : rawPath;
|
|
7769
|
+
if (isStagingPath(relativePath))
|
|
7770
|
+
return { action: "allow" };
|
|
7771
|
+
if (!isProtectedFile(options.protectFiles, relativePath))
|
|
7772
|
+
return { action: "allow" };
|
|
7773
|
+
const relativeStaging = stagingPath(relativePath);
|
|
7774
|
+
const redirectedPath = isAbsolute ? path.join(options.stateDir, relativeStaging) : relativeStaging;
|
|
7660
7775
|
return {
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
`then edit the staged file at ${stagingPath(targetPath)}.`,
|
|
7666
|
-
`Run \`soulguard diff\` to review your changes.`,
|
|
7667
|
-
`Your owner will review and apply the changes.`
|
|
7668
|
-
].join(" ")
|
|
7776
|
+
action: "redirect",
|
|
7777
|
+
pathKey,
|
|
7778
|
+
originalPath: relativePath,
|
|
7779
|
+
redirectedPath
|
|
7669
7780
|
};
|
|
7670
7781
|
}
|
|
7671
7782
|
|
|
7672
7783
|
// ../openclaw/src/plugin.ts
|
|
7673
7784
|
var PKG_VERSION = typeof SOULGUARD_VERSION !== "undefined" ? SOULGUARD_VERSION : "0.0.0-dev";
|
|
7674
7785
|
var PLUGIN_DESCRIPTION = "Identity protection for AI agents";
|
|
7786
|
+
var MAX_TRACKED_REDIRECTS = 1024;
|
|
7787
|
+
var redirectMap = new Map;
|
|
7788
|
+
async function ensureStagingCopy(originalAbsPath, stagingAbsPath) {
|
|
7789
|
+
try {
|
|
7790
|
+
await access2(stagingAbsPath);
|
|
7791
|
+
return;
|
|
7792
|
+
} catch {}
|
|
7793
|
+
await mkdir(dirname5(stagingAbsPath), { recursive: true });
|
|
7794
|
+
try {
|
|
7795
|
+
await copyFile(originalAbsPath, stagingAbsPath);
|
|
7796
|
+
await chmod(stagingAbsPath, 420);
|
|
7797
|
+
} catch {}
|
|
7798
|
+
}
|
|
7799
|
+
function buildRedirectWarning(info) {
|
|
7800
|
+
return `
|
|
7801
|
+
|
|
7802
|
+
[Soulguard] This edit was redirected to the staging copy at ${info.redirectedPath}. ` + `The original file ${info.originalPath} is protected. ` + "Run `soulguard diff` to review your changes. " + "Your owner will review and apply them.";
|
|
7803
|
+
}
|
|
7675
7804
|
function createSoulguardPlugin(options) {
|
|
7676
7805
|
return {
|
|
7677
7806
|
id: "soulguard",
|
|
@@ -7692,7 +7821,7 @@ function createSoulguardPlugin(options) {
|
|
|
7692
7821
|
}
|
|
7693
7822
|
if (protectFiles.length === 0)
|
|
7694
7823
|
return;
|
|
7695
|
-
api.on("before_tool_call", (...args) => {
|
|
7824
|
+
api.on("before_tool_call", async (...args) => {
|
|
7696
7825
|
const event = args[0];
|
|
7697
7826
|
if (!event || typeof event !== "object" || !("toolName" in event)) {
|
|
7698
7827
|
return;
|
|
@@ -7702,11 +7831,57 @@ function createSoulguardPlugin(options) {
|
|
|
7702
7831
|
protectFiles,
|
|
7703
7832
|
stateDir
|
|
7704
7833
|
});
|
|
7705
|
-
if (result.
|
|
7834
|
+
if (result.action === "block") {
|
|
7706
7835
|
return { block: true, blockReason: result.reason };
|
|
7707
7836
|
}
|
|
7837
|
+
if (result.action === "redirect") {
|
|
7838
|
+
const originalAbsPath = join2(stateDir, result.originalPath);
|
|
7839
|
+
const stagingAbsPath = result.redirectedPath.startsWith("/") ? result.redirectedPath : join2(stateDir, result.redirectedPath);
|
|
7840
|
+
await ensureStagingCopy(originalAbsPath, stagingAbsPath);
|
|
7841
|
+
const toolCallId = e.toolCallId;
|
|
7842
|
+
if (toolCallId) {
|
|
7843
|
+
if (redirectMap.size >= MAX_TRACKED_REDIRECTS) {
|
|
7844
|
+
const oldest = redirectMap.keys().next().value;
|
|
7845
|
+
if (oldest)
|
|
7846
|
+
redirectMap.delete(oldest);
|
|
7847
|
+
}
|
|
7848
|
+
redirectMap.set(toolCallId, {
|
|
7849
|
+
originalPath: result.originalPath,
|
|
7850
|
+
redirectedPath: result.redirectedPath
|
|
7851
|
+
});
|
|
7852
|
+
}
|
|
7853
|
+
return {
|
|
7854
|
+
params: { [result.pathKey]: result.redirectedPath }
|
|
7855
|
+
};
|
|
7856
|
+
}
|
|
7708
7857
|
return;
|
|
7709
7858
|
});
|
|
7859
|
+
api.on("tool_result_persist", (...args) => {
|
|
7860
|
+
const event = args[0];
|
|
7861
|
+
if (!event?.toolCallId || !event.message)
|
|
7862
|
+
return;
|
|
7863
|
+
const info = redirectMap.get(event.toolCallId);
|
|
7864
|
+
if (!info)
|
|
7865
|
+
return;
|
|
7866
|
+
redirectMap.delete(event.toolCallId);
|
|
7867
|
+
const warning = buildRedirectWarning(info);
|
|
7868
|
+
const msg = event.message;
|
|
7869
|
+
const content = Array.isArray(msg.content) ? [...msg.content] : [];
|
|
7870
|
+
let lastText;
|
|
7871
|
+
for (let i = content.length - 1;i >= 0; i--) {
|
|
7872
|
+
const block = content[i];
|
|
7873
|
+
if (block && block.type === "text") {
|
|
7874
|
+
lastText = block;
|
|
7875
|
+
break;
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
if (lastText && lastText.text != null) {
|
|
7879
|
+
lastText.text += warning;
|
|
7880
|
+
} else {
|
|
7881
|
+
content.push({ type: "text", text: warning.trimStart() });
|
|
7882
|
+
}
|
|
7883
|
+
return { message: { ...msg, content } };
|
|
7884
|
+
});
|
|
7710
7885
|
}
|
|
7711
7886
|
};
|
|
7712
7887
|
}
|
|
@@ -7752,6 +7927,7 @@ export {
|
|
|
7752
7927
|
makeDefaultConfig,
|
|
7753
7928
|
isStagingPath,
|
|
7754
7929
|
isProtectedFile,
|
|
7930
|
+
isInProtectTier,
|
|
7755
7931
|
isDeleteSentinel,
|
|
7756
7932
|
guardianName,
|
|
7757
7933
|
getProtectOwnership,
|
|
@@ -7762,13 +7938,15 @@ export {
|
|
|
7762
7938
|
evaluatePolicies,
|
|
7763
7939
|
err,
|
|
7764
7940
|
diff,
|
|
7941
|
+
deleteStagingEntry,
|
|
7942
|
+
createStagingCopy,
|
|
7943
|
+
create,
|
|
7765
7944
|
apply,
|
|
7766
7945
|
TierCommand,
|
|
7767
7946
|
SyncCommand,
|
|
7768
7947
|
StatusCommand,
|
|
7769
7948
|
StateTreeBuildError,
|
|
7770
7949
|
StateTree,
|
|
7771
|
-
StageCommand,
|
|
7772
7950
|
SoulguardDaemon,
|
|
7773
7951
|
STAGING_DIR,
|
|
7774
7952
|
SOULGUARD_GROUP,
|
|
@@ -7778,6 +7956,8 @@ export {
|
|
|
7778
7956
|
MockSystemOps,
|
|
7779
7957
|
LiveConsoleOutput,
|
|
7780
7958
|
DiffCommand,
|
|
7959
|
+
DeleteCommand,
|
|
7781
7960
|
DELETE_SENTINEL,
|
|
7961
|
+
CreateCommand,
|
|
7782
7962
|
ApplyCommand
|
|
7783
7963
|
};
|