soulguard 0.2.2 → 0.3.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/dist/index.js +448 -227
- 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),
|
|
@@ -4948,7 +4949,7 @@ class StateTree {
|
|
|
4948
4949
|
return this.flatFiles().filter((f) => f.status !== "unchanged");
|
|
4949
4950
|
}
|
|
4950
4951
|
stagedFiles() {
|
|
4951
|
-
return this.flatFiles().filter((f) => f.
|
|
4952
|
+
return this.flatFiles().filter((f) => f.status !== "unchanged");
|
|
4952
4953
|
}
|
|
4953
4954
|
driftedEntities() {
|
|
4954
4955
|
return collectDrifts(this.entities, this.protectOwnership);
|
|
@@ -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;
|
|
@@ -6972,6 +7056,10 @@ class TierCommand {
|
|
|
6972
7056
|
this.out.error(`Failed to write config: ${writeResult.error.message}`);
|
|
6973
7057
|
return 1;
|
|
6974
7058
|
}
|
|
7059
|
+
const sgCopyResult = await createStagingCopy(ops, "soulguard.json", configResult.value.defaultOwnership ?? undefined);
|
|
7060
|
+
if (!sgCopyResult.ok) {
|
|
7061
|
+
this.out.warn(` Warning: staging copy failed for soulguard.json: ${sgCopyResult.error}`);
|
|
7062
|
+
}
|
|
6975
7063
|
if (action.kind === "set" && action.tier === "protect") {
|
|
6976
7064
|
const expectedProtectOwnership = getProtectOwnership(configResult.value.guardian);
|
|
6977
7065
|
for (const file of changedPaths) {
|
|
@@ -6981,6 +7069,34 @@ class TierCommand {
|
|
|
6981
7069
|
return 1;
|
|
6982
7070
|
}
|
|
6983
7071
|
}
|
|
7072
|
+
const defaultOwnership = configResult.value.defaultOwnership;
|
|
7073
|
+
await ops.mkdir(".soulguard-staging");
|
|
7074
|
+
if (defaultOwnership) {
|
|
7075
|
+
await ops.chown(".soulguard-staging", {
|
|
7076
|
+
user: defaultOwnership.user,
|
|
7077
|
+
group: defaultOwnership.group
|
|
7078
|
+
});
|
|
7079
|
+
await ops.chmod(".soulguard-staging", "755");
|
|
7080
|
+
}
|
|
7081
|
+
for (const file of changedPaths) {
|
|
7082
|
+
const isDir = await isDirectory(ops, file);
|
|
7083
|
+
if (isDir) {
|
|
7084
|
+
const listResult = await ops.listDir(file);
|
|
7085
|
+
if (listResult.ok) {
|
|
7086
|
+
for (const childPath of listResult.value) {
|
|
7087
|
+
const copyResult = await createStagingCopy(ops, childPath, defaultOwnership ?? undefined);
|
|
7088
|
+
if (!copyResult.ok) {
|
|
7089
|
+
this.out.warn(` Warning: staging copy failed for ${childPath}: ${copyResult.error}`);
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
} else {
|
|
7094
|
+
const copyResult = await createStagingCopy(ops, file, defaultOwnership ?? undefined);
|
|
7095
|
+
if (!copyResult.ok) {
|
|
7096
|
+
this.out.warn(` Warning: staging copy failed for ${file}: ${copyResult.error}`);
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
6984
7100
|
} else if (action.kind === "set" && action.tier === "watch") {
|
|
6985
7101
|
const defaultOwnership = configResult.value.defaultOwnership;
|
|
6986
7102
|
for (const file of changedPaths) {
|
|
@@ -7018,153 +7134,114 @@ class TierCommand {
|
|
|
7018
7134
|
return 0;
|
|
7019
7135
|
}
|
|
7020
7136
|
}
|
|
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;
|
|
7137
|
+
// ../core/src/sdk/create.ts
|
|
7138
|
+
async function create(options) {
|
|
7139
|
+
const { ops, config, path } = options;
|
|
7140
|
+
if (!isInProtectTier(path, protectPatterns(config))) {
|
|
7141
|
+
return err({ kind: "not_in_protect_tier", path });
|
|
7030
7142
|
}
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
const parentDir = dirname3(path);
|
|
7035
|
-
if (parentDir === "." || parentDir === "/") {
|
|
7036
|
-
return ok(undefined);
|
|
7143
|
+
const exists = await ops.exists(path);
|
|
7144
|
+
if (exists.ok && exists.value) {
|
|
7145
|
+
return ok({ path, alreadyExisted: true });
|
|
7037
7146
|
}
|
|
7038
|
-
const
|
|
7039
|
-
|
|
7040
|
-
|
|
7147
|
+
const stagePath = stagingPath(path);
|
|
7148
|
+
const parentResult = await ensureStagingParentDir(ops, path);
|
|
7149
|
+
if (!parentResult.ok) {
|
|
7150
|
+
return err({ kind: "create_failed", path, message: parentResult.error });
|
|
7041
7151
|
}
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
async function stage(options) {
|
|
7045
|
-
const { ops, config, path, delete: isDelete } = options;
|
|
7046
|
-
const protectFiles = protectPatterns(config);
|
|
7047
|
-
if (!isInProtectTier(path, protectFiles)) {
|
|
7048
|
-
return err({ kind: "not_in_protect_tier", path });
|
|
7049
|
-
}
|
|
7050
|
-
const stagedFiles = [];
|
|
7051
|
-
const pathStat = await ops.stat(path);
|
|
7052
|
-
if (!pathStat.ok && pathStat.error.kind !== "not_found") {
|
|
7152
|
+
const writeResult = await ops.writeFile(stagePath, "");
|
|
7153
|
+
if (!writeResult.ok) {
|
|
7053
7154
|
return err({
|
|
7054
|
-
kind: "
|
|
7155
|
+
kind: "create_failed",
|
|
7055
7156
|
path,
|
|
7056
|
-
message: `Cannot
|
|
7157
|
+
message: `Cannot create staging file: ${writeResult.error.kind}`
|
|
7057
7158
|
});
|
|
7058
7159
|
}
|
|
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
|
-
});
|
|
7160
|
+
return ok({ path, alreadyExisted: false });
|
|
7161
|
+
}
|
|
7162
|
+
|
|
7163
|
+
// ../core/src/cli/create-command.ts
|
|
7164
|
+
class CreateCommand {
|
|
7165
|
+
opts;
|
|
7166
|
+
out;
|
|
7167
|
+
constructor(opts, out) {
|
|
7168
|
+
this.opts = opts;
|
|
7169
|
+
this.out = out;
|
|
7170
|
+
}
|
|
7171
|
+
async execute() {
|
|
7172
|
+
const { ops, config, paths } = this.opts;
|
|
7173
|
+
if (paths.length === 0) {
|
|
7174
|
+
this.out.error("No paths specified.");
|
|
7175
|
+
return 1;
|
|
7090
7176
|
}
|
|
7091
|
-
|
|
7092
|
-
|
|
7177
|
+
let created = 0;
|
|
7178
|
+
for (const path of paths) {
|
|
7179
|
+
const result = await create({ ops, config, path });
|
|
7093
7180
|
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: [] });
|
|
7181
|
+
switch (result.error.kind) {
|
|
7182
|
+
case "not_in_protect_tier":
|
|
7183
|
+
this.out.error(`${result.error.path} is not in the protect tier.`);
|
|
7184
|
+
return 1;
|
|
7185
|
+
case "create_failed":
|
|
7186
|
+
this.out.error(`Failed to create ${result.error.path}: ${result.error.message}`);
|
|
7187
|
+
return 1;
|
|
7188
|
+
}
|
|
7105
7189
|
}
|
|
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: [] });
|
|
7190
|
+
if (result.value.alreadyExisted) {
|
|
7191
|
+
this.out.warn(` · ${path} (already exists — staging copy was created automatically)`);
|
|
7192
|
+
continue;
|
|
7127
7193
|
}
|
|
7194
|
+
this.out.success(` + ${path} → ${stagingPath(path)}`);
|
|
7195
|
+
created++;
|
|
7128
7196
|
}
|
|
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
|
-
}
|
|
7197
|
+
if (created === 0) {
|
|
7198
|
+
this.out.info("Nothing to create.");
|
|
7151
7199
|
} else {
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
return err({
|
|
7155
|
-
kind: "stage_failed",
|
|
7156
|
-
path,
|
|
7157
|
-
message: `Cannot create empty staging file: ${writeResult.error.kind}`
|
|
7158
|
-
});
|
|
7159
|
-
}
|
|
7200
|
+
this.out.write("");
|
|
7201
|
+
this.out.success(`Created ${created} staging ${created === 1 ? "entry" : "entries"}.`);
|
|
7160
7202
|
}
|
|
7161
|
-
|
|
7203
|
+
return 0;
|
|
7162
7204
|
}
|
|
7163
|
-
|
|
7205
|
+
}
|
|
7206
|
+
// ../core/src/sdk/delete.ts
|
|
7207
|
+
async function deleteStagingEntry(options) {
|
|
7208
|
+
const { ops, config, path } = options;
|
|
7209
|
+
if (!isInProtectTier(path, protectPatterns(config))) {
|
|
7210
|
+
return err({ kind: "not_in_protect_tier", path });
|
|
7211
|
+
}
|
|
7212
|
+
const diskExists = await ops.exists(path);
|
|
7213
|
+
const stagePath = stagingPath(path);
|
|
7214
|
+
const stagingExists = await ops.exists(stagePath);
|
|
7215
|
+
if ((!diskExists.ok || !diskExists.value) && (!stagingExists.ok || !stagingExists.value)) {
|
|
7216
|
+
return err({ kind: "not_found", path });
|
|
7217
|
+
}
|
|
7218
|
+
if (stagingExists.ok && stagingExists.value) {
|
|
7219
|
+
const readResult = await ops.readFile(stagePath);
|
|
7220
|
+
if (readResult.ok && isDeleteSentinel(readResult.value)) {
|
|
7221
|
+
return err({ kind: "already_deleted", path });
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
const stageExists = await ops.stat(stagePath);
|
|
7225
|
+
if (stageExists.ok && stageExists.value.isDirectory) {
|
|
7226
|
+
await ops.exec("rm", ["-rf", stagePath]);
|
|
7227
|
+
}
|
|
7228
|
+
const parentResult = await ensureStagingParentDir(ops, path);
|
|
7229
|
+
if (!parentResult.ok) {
|
|
7230
|
+
return err({ kind: "delete_failed", path, message: parentResult.error });
|
|
7231
|
+
}
|
|
7232
|
+
const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
|
|
7233
|
+
if (!writeResult.ok) {
|
|
7234
|
+
return err({
|
|
7235
|
+
kind: "delete_failed",
|
|
7236
|
+
path,
|
|
7237
|
+
message: `Cannot write delete sentinel: ${writeResult.error.kind}`
|
|
7238
|
+
});
|
|
7239
|
+
}
|
|
7240
|
+
return ok({ path });
|
|
7164
7241
|
}
|
|
7165
7242
|
|
|
7166
|
-
// ../core/src/cli/
|
|
7167
|
-
class
|
|
7243
|
+
// ../core/src/cli/delete-command.ts
|
|
7244
|
+
class DeleteCommand {
|
|
7168
7245
|
opts;
|
|
7169
7246
|
out;
|
|
7170
7247
|
constructor(opts, out) {
|
|
@@ -7172,47 +7249,38 @@ class StageCommand {
|
|
|
7172
7249
|
this.out = out;
|
|
7173
7250
|
}
|
|
7174
7251
|
async execute() {
|
|
7175
|
-
const { ops, config, paths
|
|
7252
|
+
const { ops, config, paths } = this.opts;
|
|
7176
7253
|
if (paths.length === 0) {
|
|
7177
7254
|
this.out.error("No paths specified.");
|
|
7178
7255
|
return 1;
|
|
7179
7256
|
}
|
|
7180
|
-
|
|
7181
|
-
const alreadyStaged = [];
|
|
7257
|
+
let deleted = 0;
|
|
7182
7258
|
for (const path of paths) {
|
|
7183
|
-
const result = await
|
|
7259
|
+
const result = await deleteStagingEntry({ ops, config, path });
|
|
7184
7260
|
if (!result.ok) {
|
|
7185
7261
|
switch (result.error.kind) {
|
|
7186
7262
|
case "not_in_protect_tier":
|
|
7187
7263
|
this.out.error(`${result.error.path} is not in the protect tier.`);
|
|
7188
7264
|
return 1;
|
|
7189
|
-
case "
|
|
7190
|
-
this.out.error(
|
|
7265
|
+
case "not_found":
|
|
7266
|
+
this.out.error(`${result.error.path} does not exist.`);
|
|
7267
|
+
return 1;
|
|
7268
|
+
case "already_deleted":
|
|
7269
|
+
this.out.info(` · ${result.error.path} (already staged for deletion)`);
|
|
7270
|
+
continue;
|
|
7271
|
+
case "delete_failed":
|
|
7272
|
+
this.out.error(`Failed to delete ${result.error.path}: ${result.error.message}`);
|
|
7191
7273
|
return 1;
|
|
7192
7274
|
}
|
|
7193
7275
|
}
|
|
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
|
-
const staged = stagingPath(path);
|
|
7205
|
-
if (action === "delete") {
|
|
7206
|
-
this.out.success(` \uD83D\uDDD1️ ${path} (staged for deletion)`);
|
|
7207
|
-
} else {
|
|
7208
|
-
this.out.success(` \uD83D\uDCDD ${path} → ${staged}`);
|
|
7209
|
-
}
|
|
7276
|
+
this.out.success(` \uD83D\uDDD1️ ${path} (staged for deletion)`);
|
|
7277
|
+
deleted++;
|
|
7210
7278
|
}
|
|
7211
|
-
if (
|
|
7212
|
-
this.out.info("Nothing to
|
|
7279
|
+
if (deleted === 0) {
|
|
7280
|
+
this.out.info("Nothing to do.");
|
|
7213
7281
|
} else {
|
|
7214
7282
|
this.out.write("");
|
|
7215
|
-
this.out.success(`Staged ${
|
|
7283
|
+
this.out.success(`Staged ${deleted} ${deleted === 1 ? "path" : "paths"} for deletion.`);
|
|
7216
7284
|
}
|
|
7217
7285
|
return 0;
|
|
7218
7286
|
}
|
|
@@ -7331,17 +7399,11 @@ class ProposalManager extends EventEmitter {
|
|
|
7331
7399
|
if (!freshTree.ok) {
|
|
7332
7400
|
const error = new Error(`Failed to build fresh state tree: ${freshTree.error.message}`);
|
|
7333
7401
|
this.emit("error", error, "approval:verification");
|
|
7334
|
-
proposal
|
|
7335
|
-
this._activeProposal = null;
|
|
7336
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7337
|
-
this.emit("rejected", proposal);
|
|
7402
|
+
await this._rejectProposal(proposal);
|
|
7338
7403
|
return;
|
|
7339
7404
|
}
|
|
7340
7405
|
if (freshTree.value.approvalHash !== proposal.payload.hash) {
|
|
7341
|
-
proposal
|
|
7342
|
-
this._activeProposal = null;
|
|
7343
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7344
|
-
this.emit("rejected", proposal);
|
|
7406
|
+
await this._rejectProposal(proposal);
|
|
7345
7407
|
return;
|
|
7346
7408
|
}
|
|
7347
7409
|
const applyResult = await apply({
|
|
@@ -7352,10 +7414,7 @@ class ProposalManager extends EventEmitter {
|
|
|
7352
7414
|
if (!applyResult.ok) {
|
|
7353
7415
|
const error = new Error(`Apply failed: ${applyResult.error.kind}`);
|
|
7354
7416
|
this.emit("error", error, "approval:apply");
|
|
7355
|
-
proposal
|
|
7356
|
-
this._activeProposal = null;
|
|
7357
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7358
|
-
this.emit("rejected", proposal);
|
|
7417
|
+
await this._rejectProposal(proposal);
|
|
7359
7418
|
return;
|
|
7360
7419
|
}
|
|
7361
7420
|
proposal.state = "approved";
|
|
@@ -7363,10 +7422,7 @@ class ProposalManager extends EventEmitter {
|
|
|
7363
7422
|
await this._channel.postResult(proposal.externalId, "applied");
|
|
7364
7423
|
this.emit("applied", proposal);
|
|
7365
7424
|
} else {
|
|
7366
|
-
proposal
|
|
7367
|
-
this._activeProposal = null;
|
|
7368
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7369
|
-
this.emit("rejected", proposal);
|
|
7425
|
+
await this._rejectProposal(proposal);
|
|
7370
7426
|
}
|
|
7371
7427
|
} catch (e) {
|
|
7372
7428
|
if (e instanceof DOMException && e.name === "AbortError")
|
|
@@ -7375,15 +7431,21 @@ class ProposalManager extends EventEmitter {
|
|
|
7375
7431
|
return;
|
|
7376
7432
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
7377
7433
|
this.emit("error", error, "approval:wait");
|
|
7378
|
-
proposal
|
|
7379
|
-
this._activeProposal = null;
|
|
7434
|
+
await this._rejectProposal(proposal);
|
|
7380
7435
|
}
|
|
7381
7436
|
}
|
|
7437
|
+
async _rejectProposal(proposal) {
|
|
7438
|
+
proposal.state = "rejected";
|
|
7439
|
+
await this._resetStaging(proposal);
|
|
7440
|
+
this._pendingProposalHash = null;
|
|
7441
|
+
this._activeProposal = null;
|
|
7442
|
+
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7443
|
+
this.emit("rejected", proposal);
|
|
7444
|
+
}
|
|
7382
7445
|
async _supersedePending() {
|
|
7383
7446
|
if (this._activeProposal && this._abortController) {
|
|
7384
7447
|
const oldProposal = this._activeProposal;
|
|
7385
7448
|
const oldController = this._abortController;
|
|
7386
|
-
oldProposal.state = "superseded";
|
|
7387
7449
|
this._activeProposal = null;
|
|
7388
7450
|
this._abortController = null;
|
|
7389
7451
|
oldController.abort();
|
|
@@ -7391,6 +7453,9 @@ class ProposalManager extends EventEmitter {
|
|
|
7391
7453
|
await this._pendingFlow.catch(() => {});
|
|
7392
7454
|
this._pendingFlow = null;
|
|
7393
7455
|
}
|
|
7456
|
+
if (oldProposal.state !== "pending")
|
|
7457
|
+
return;
|
|
7458
|
+
oldProposal.state = "superseded";
|
|
7394
7459
|
await this._channel.postResult(oldProposal.externalId, "superseded");
|
|
7395
7460
|
this.emit("superseded", oldProposal);
|
|
7396
7461
|
}
|
|
@@ -7405,7 +7470,19 @@ class ProposalManager extends EventEmitter {
|
|
|
7405
7470
|
this._activeProposal = null;
|
|
7406
7471
|
this._abortController = null;
|
|
7407
7472
|
}
|
|
7473
|
+
async _resetStaging(proposal) {
|
|
7474
|
+
const defaultOwnership = this._config.defaultOwnership;
|
|
7475
|
+
for (const file of proposal.payload.files) {
|
|
7476
|
+
const result = file.status === "created" ? await this._ops.deleteFile(stagingPath(file.path)) : await createStagingCopy(this._ops, file.path, defaultOwnership);
|
|
7477
|
+
if (!result.ok) {
|
|
7478
|
+
this.emit("error", new Error(`Reset staging failed for ${file.path}: ${String(result.error)}`), `resetStaging:${file.path}`);
|
|
7479
|
+
}
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7408
7482
|
}
|
|
7483
|
+
// ../core/src/daemon/daemon.ts
|
|
7484
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
7485
|
+
|
|
7409
7486
|
// ../core/src/daemon/channel-registry.ts
|
|
7410
7487
|
var REGISTRY_KEY = "__soulguard_channel_registry__";
|
|
7411
7488
|
function getRegistry() {
|
|
@@ -7423,15 +7500,23 @@ function getChannel(name) {
|
|
|
7423
7500
|
}
|
|
7424
7501
|
|
|
7425
7502
|
// ../core/src/daemon/daemon.ts
|
|
7426
|
-
class SoulguardDaemon {
|
|
7503
|
+
class SoulguardDaemon extends EventEmitter2 {
|
|
7427
7504
|
_ops;
|
|
7428
7505
|
_config;
|
|
7506
|
+
_channelOverride;
|
|
7507
|
+
_maxProposals;
|
|
7429
7508
|
_channel = null;
|
|
7430
7509
|
_proposalManager = null;
|
|
7510
|
+
_syncTimer = null;
|
|
7511
|
+
_syncRunning = false;
|
|
7431
7512
|
_running = false;
|
|
7513
|
+
_doneResolve = null;
|
|
7432
7514
|
constructor(options) {
|
|
7515
|
+
super();
|
|
7433
7516
|
this._ops = options.ops;
|
|
7434
7517
|
this._config = options.config;
|
|
7518
|
+
this._channelOverride = options.channelOverride ?? null;
|
|
7519
|
+
this._maxProposals = options.maxProposals ?? null;
|
|
7435
7520
|
}
|
|
7436
7521
|
get running() {
|
|
7437
7522
|
return this._running;
|
|
@@ -7446,37 +7531,104 @@ class SoulguardDaemon {
|
|
|
7446
7531
|
if (!daemonConfig) {
|
|
7447
7532
|
throw new Error("Daemon configuration missing. Add a 'daemon' section to soulguard.json.");
|
|
7448
7533
|
}
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7534
|
+
if (this._channelOverride) {
|
|
7535
|
+
this._channel = this._channelOverride;
|
|
7536
|
+
} else {
|
|
7537
|
+
const channelName = daemonConfig.channel;
|
|
7538
|
+
if (channelName) {
|
|
7539
|
+
const createChannelFn = getChannel(channelName);
|
|
7540
|
+
if (!createChannelFn) {
|
|
7541
|
+
throw new Error(`No channel registered for "${channelName}". Register it with registerChannel() before starting the daemon.`);
|
|
7542
|
+
}
|
|
7543
|
+
const channelConfig = daemonConfig[channelName];
|
|
7544
|
+
this._channel = createChannelFn(channelConfig);
|
|
7545
|
+
}
|
|
7546
|
+
}
|
|
7547
|
+
if (this._channel) {
|
|
7548
|
+
this._proposalManager = new ProposalManager({
|
|
7549
|
+
ops: this._ops,
|
|
7550
|
+
config: this._config,
|
|
7551
|
+
channel: this._channel
|
|
7552
|
+
});
|
|
7553
|
+
this._proposalManager.on("proposed", (...args) => this.emit("proposed", ...args));
|
|
7554
|
+
this._proposalManager.on("applied", (...args) => this.emit("applied", ...args));
|
|
7555
|
+
this._proposalManager.on("rejected", (...args) => this.emit("rejected", ...args));
|
|
7556
|
+
this._proposalManager.on("superseded", (...args) => this.emit("superseded", ...args));
|
|
7557
|
+
this._proposalManager.on("error", (...args) => this.emit("proposal:error", ...args));
|
|
7558
|
+
if (this._maxProposals != null) {
|
|
7559
|
+
let count = 0;
|
|
7560
|
+
const checkDone = () => {
|
|
7561
|
+
count++;
|
|
7562
|
+
if (count >= this._maxProposals) {
|
|
7563
|
+
this.stop();
|
|
7564
|
+
}
|
|
7565
|
+
};
|
|
7566
|
+
this._proposalManager.on("applied", checkDone);
|
|
7567
|
+
this._proposalManager.on("rejected", checkDone);
|
|
7568
|
+
}
|
|
7569
|
+
this._proposalManager.start();
|
|
7570
|
+
}
|
|
7571
|
+
const syncIntervalSecs = daemonConfig.syncIntervalSecs ?? 60;
|
|
7572
|
+
if (syncIntervalSecs > 0) {
|
|
7573
|
+
this._runSync();
|
|
7574
|
+
this._syncTimer = setInterval(() => this._runSync(), syncIntervalSecs * 1000);
|
|
7575
|
+
}
|
|
7466
7576
|
this._running = true;
|
|
7467
7577
|
}
|
|
7578
|
+
done() {
|
|
7579
|
+
if (!this._running)
|
|
7580
|
+
return Promise.resolve();
|
|
7581
|
+
return new Promise((resolve3) => {
|
|
7582
|
+
this._doneResolve = resolve3;
|
|
7583
|
+
});
|
|
7584
|
+
}
|
|
7468
7585
|
async stop() {
|
|
7469
7586
|
if (!this._running)
|
|
7470
7587
|
return;
|
|
7471
7588
|
this._running = false;
|
|
7589
|
+
if (this._syncTimer) {
|
|
7590
|
+
clearInterval(this._syncTimer);
|
|
7591
|
+
this._syncTimer = null;
|
|
7592
|
+
}
|
|
7472
7593
|
if (this._proposalManager) {
|
|
7473
7594
|
await this._proposalManager.stop();
|
|
7474
7595
|
this._proposalManager = null;
|
|
7475
7596
|
}
|
|
7476
|
-
if (this._channel) {
|
|
7597
|
+
if (this._channel && !this._channelOverride) {
|
|
7477
7598
|
await this._channel.dispose();
|
|
7478
7599
|
this._channel = null;
|
|
7479
7600
|
}
|
|
7601
|
+
this._doneResolve?.();
|
|
7602
|
+
this._doneResolve = null;
|
|
7603
|
+
}
|
|
7604
|
+
async _runSync() {
|
|
7605
|
+
if (this._syncRunning)
|
|
7606
|
+
return;
|
|
7607
|
+
this._syncRunning = true;
|
|
7608
|
+
try {
|
|
7609
|
+
const treeResult = await StateTree.build({
|
|
7610
|
+
ops: this._ops,
|
|
7611
|
+
config: this._config
|
|
7612
|
+
});
|
|
7613
|
+
if (!treeResult.ok) {
|
|
7614
|
+
this.emit("sync:error", new Error(`StateTree.build failed: ${treeResult.error.message}`));
|
|
7615
|
+
return;
|
|
7616
|
+
}
|
|
7617
|
+
const syncResult = await sync({
|
|
7618
|
+
tree: treeResult.value,
|
|
7619
|
+
ops: this._ops,
|
|
7620
|
+
config: this._config
|
|
7621
|
+
});
|
|
7622
|
+
if (!syncResult.ok) {
|
|
7623
|
+
this.emit("sync:error", new Error(`sync failed: ${syncResult.error.message}`));
|
|
7624
|
+
return;
|
|
7625
|
+
}
|
|
7626
|
+
this.emit("synced", syncResult.value);
|
|
7627
|
+
} catch (e) {
|
|
7628
|
+
this.emit("sync:error", e instanceof Error ? e : new Error(String(e)));
|
|
7629
|
+
} finally {
|
|
7630
|
+
this._syncRunning = false;
|
|
7631
|
+
}
|
|
7480
7632
|
}
|
|
7481
7633
|
}
|
|
7482
7634
|
// ../core/src/daemon/service.ts
|
|
@@ -7629,8 +7781,9 @@ var templates = {
|
|
|
7629
7781
|
};
|
|
7630
7782
|
// ../openclaw/src/plugin.ts
|
|
7631
7783
|
import { readFileSync } from "node:fs";
|
|
7784
|
+
import { access as access2, chmod, copyFile, mkdir } from "node:fs/promises";
|
|
7632
7785
|
import os from "node:os";
|
|
7633
|
-
import { join as join2 } from "node:path";
|
|
7786
|
+
import { dirname as dirname5, join as join2 } from "node:path";
|
|
7634
7787
|
|
|
7635
7788
|
// ../openclaw/src/guard.ts
|
|
7636
7789
|
import path from "node:path";
|
|
@@ -7638,40 +7791,57 @@ var WRITE_TOOLS = new Set(["write", "edit"]);
|
|
|
7638
7791
|
var PATH_KEYS = ["file_path", "path", "file"];
|
|
7639
7792
|
function guardToolCall(toolName, params, options) {
|
|
7640
7793
|
if (!WRITE_TOOLS.has(toolName.toLowerCase())) {
|
|
7641
|
-
return {
|
|
7794
|
+
return { action: "allow" };
|
|
7642
7795
|
}
|
|
7643
|
-
let
|
|
7796
|
+
let pathKey;
|
|
7797
|
+
let rawPath;
|
|
7644
7798
|
for (const key of PATH_KEYS) {
|
|
7645
7799
|
const v = params[key];
|
|
7646
7800
|
if (typeof v === "string" && v.length > 0) {
|
|
7647
|
-
|
|
7801
|
+
pathKey = key;
|
|
7802
|
+
rawPath = v;
|
|
7648
7803
|
break;
|
|
7649
7804
|
}
|
|
7650
7805
|
}
|
|
7651
|
-
if (
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7806
|
+
if (!pathKey || !rawPath)
|
|
7807
|
+
return { action: "allow" };
|
|
7808
|
+
const isAbsolute = path.isAbsolute(rawPath);
|
|
7809
|
+
const relativePath = isAbsolute ? path.relative(options.stateDir, rawPath) : rawPath;
|
|
7810
|
+
if (isStagingPath(relativePath))
|
|
7811
|
+
return { action: "allow" };
|
|
7812
|
+
if (!isProtectedFile(options.protectFiles, relativePath))
|
|
7813
|
+
return { action: "allow" };
|
|
7814
|
+
const relativeStaging = stagingPath(relativePath);
|
|
7815
|
+
const redirectedPath = isAbsolute ? path.join(options.stateDir, relativeStaging) : relativeStaging;
|
|
7660
7816
|
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(" ")
|
|
7817
|
+
action: "redirect",
|
|
7818
|
+
pathKey,
|
|
7819
|
+
originalPath: relativePath,
|
|
7820
|
+
redirectedPath
|
|
7669
7821
|
};
|
|
7670
7822
|
}
|
|
7671
7823
|
|
|
7672
7824
|
// ../openclaw/src/plugin.ts
|
|
7673
7825
|
var PKG_VERSION = typeof SOULGUARD_VERSION !== "undefined" ? SOULGUARD_VERSION : "0.0.0-dev";
|
|
7674
7826
|
var PLUGIN_DESCRIPTION = "Identity protection for AI agents";
|
|
7827
|
+
var MAX_TRACKED_REDIRECTS = 1024;
|
|
7828
|
+
var redirectMap = new Map;
|
|
7829
|
+
async function ensureStagingCopy(originalAbsPath, stagingAbsPath) {
|
|
7830
|
+
try {
|
|
7831
|
+
await access2(stagingAbsPath);
|
|
7832
|
+
return;
|
|
7833
|
+
} catch {}
|
|
7834
|
+
await mkdir(dirname5(stagingAbsPath), { recursive: true });
|
|
7835
|
+
try {
|
|
7836
|
+
await copyFile(originalAbsPath, stagingAbsPath);
|
|
7837
|
+
await chmod(stagingAbsPath, 420);
|
|
7838
|
+
} catch {}
|
|
7839
|
+
}
|
|
7840
|
+
function buildRedirectWarning(info) {
|
|
7841
|
+
return `
|
|
7842
|
+
|
|
7843
|
+
[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.";
|
|
7844
|
+
}
|
|
7675
7845
|
function createSoulguardPlugin(options) {
|
|
7676
7846
|
return {
|
|
7677
7847
|
id: "soulguard",
|
|
@@ -7692,7 +7862,7 @@ function createSoulguardPlugin(options) {
|
|
|
7692
7862
|
}
|
|
7693
7863
|
if (protectFiles.length === 0)
|
|
7694
7864
|
return;
|
|
7695
|
-
api.on("before_tool_call", (...args) => {
|
|
7865
|
+
api.on("before_tool_call", async (...args) => {
|
|
7696
7866
|
const event = args[0];
|
|
7697
7867
|
if (!event || typeof event !== "object" || !("toolName" in event)) {
|
|
7698
7868
|
return;
|
|
@@ -7702,11 +7872,57 @@ function createSoulguardPlugin(options) {
|
|
|
7702
7872
|
protectFiles,
|
|
7703
7873
|
stateDir
|
|
7704
7874
|
});
|
|
7705
|
-
if (result.
|
|
7875
|
+
if (result.action === "block") {
|
|
7706
7876
|
return { block: true, blockReason: result.reason };
|
|
7707
7877
|
}
|
|
7878
|
+
if (result.action === "redirect") {
|
|
7879
|
+
const originalAbsPath = join2(stateDir, result.originalPath);
|
|
7880
|
+
const stagingAbsPath = result.redirectedPath.startsWith("/") ? result.redirectedPath : join2(stateDir, result.redirectedPath);
|
|
7881
|
+
await ensureStagingCopy(originalAbsPath, stagingAbsPath);
|
|
7882
|
+
const toolCallId = e.toolCallId;
|
|
7883
|
+
if (toolCallId) {
|
|
7884
|
+
if (redirectMap.size >= MAX_TRACKED_REDIRECTS) {
|
|
7885
|
+
const oldest = redirectMap.keys().next().value;
|
|
7886
|
+
if (oldest)
|
|
7887
|
+
redirectMap.delete(oldest);
|
|
7888
|
+
}
|
|
7889
|
+
redirectMap.set(toolCallId, {
|
|
7890
|
+
originalPath: result.originalPath,
|
|
7891
|
+
redirectedPath: result.redirectedPath
|
|
7892
|
+
});
|
|
7893
|
+
}
|
|
7894
|
+
return {
|
|
7895
|
+
params: { [result.pathKey]: result.redirectedPath }
|
|
7896
|
+
};
|
|
7897
|
+
}
|
|
7708
7898
|
return;
|
|
7709
7899
|
});
|
|
7900
|
+
api.on("tool_result_persist", (...args) => {
|
|
7901
|
+
const event = args[0];
|
|
7902
|
+
if (!event?.toolCallId || !event.message)
|
|
7903
|
+
return;
|
|
7904
|
+
const info = redirectMap.get(event.toolCallId);
|
|
7905
|
+
if (!info)
|
|
7906
|
+
return;
|
|
7907
|
+
redirectMap.delete(event.toolCallId);
|
|
7908
|
+
const warning = buildRedirectWarning(info);
|
|
7909
|
+
const msg = event.message;
|
|
7910
|
+
const content = Array.isArray(msg.content) ? [...msg.content] : [];
|
|
7911
|
+
let lastText;
|
|
7912
|
+
for (let i = content.length - 1;i >= 0; i--) {
|
|
7913
|
+
const block = content[i];
|
|
7914
|
+
if (block && block.type === "text") {
|
|
7915
|
+
lastText = block;
|
|
7916
|
+
break;
|
|
7917
|
+
}
|
|
7918
|
+
}
|
|
7919
|
+
if (lastText && lastText.text != null) {
|
|
7920
|
+
lastText.text += warning;
|
|
7921
|
+
} else {
|
|
7922
|
+
content.push({ type: "text", text: warning.trimStart() });
|
|
7923
|
+
}
|
|
7924
|
+
return { message: { ...msg, content } };
|
|
7925
|
+
});
|
|
7710
7926
|
}
|
|
7711
7927
|
};
|
|
7712
7928
|
}
|
|
@@ -7752,6 +7968,7 @@ export {
|
|
|
7752
7968
|
makeDefaultConfig,
|
|
7753
7969
|
isStagingPath,
|
|
7754
7970
|
isProtectedFile,
|
|
7971
|
+
isInProtectTier,
|
|
7755
7972
|
isDeleteSentinel,
|
|
7756
7973
|
guardianName,
|
|
7757
7974
|
getProtectOwnership,
|
|
@@ -7762,13 +7979,15 @@ export {
|
|
|
7762
7979
|
evaluatePolicies,
|
|
7763
7980
|
err,
|
|
7764
7981
|
diff,
|
|
7982
|
+
deleteStagingEntry,
|
|
7983
|
+
createStagingCopy,
|
|
7984
|
+
create,
|
|
7765
7985
|
apply,
|
|
7766
7986
|
TierCommand,
|
|
7767
7987
|
SyncCommand,
|
|
7768
7988
|
StatusCommand,
|
|
7769
7989
|
StateTreeBuildError,
|
|
7770
7990
|
StateTree,
|
|
7771
|
-
StageCommand,
|
|
7772
7991
|
SoulguardDaemon,
|
|
7773
7992
|
STAGING_DIR,
|
|
7774
7993
|
SOULGUARD_GROUP,
|
|
@@ -7778,6 +7997,8 @@ export {
|
|
|
7778
7997
|
MockSystemOps,
|
|
7779
7998
|
LiveConsoleOutput,
|
|
7780
7999
|
DiffCommand,
|
|
8000
|
+
DeleteCommand,
|
|
7781
8001
|
DELETE_SENTINEL,
|
|
8002
|
+
CreateCommand,
|
|
7782
8003
|
ApplyCommand
|
|
7783
8004
|
};
|