soulguard 0.2.3 → 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 +69 -28
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4949,7 +4949,7 @@ class StateTree {
|
|
|
4949
4949
|
return this.flatFiles().filter((f) => f.status !== "unchanged");
|
|
4950
4950
|
}
|
|
4951
4951
|
stagedFiles() {
|
|
4952
|
-
return this.flatFiles().filter((f) => f.
|
|
4952
|
+
return this.flatFiles().filter((f) => f.status !== "unchanged");
|
|
4953
4953
|
}
|
|
4954
4954
|
driftedEntities() {
|
|
4955
4955
|
return collectDrifts(this.entities, this.protectOwnership);
|
|
@@ -7056,6 +7056,10 @@ class TierCommand {
|
|
|
7056
7056
|
this.out.error(`Failed to write config: ${writeResult.error.message}`);
|
|
7057
7057
|
return 1;
|
|
7058
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
|
+
}
|
|
7059
7063
|
if (action.kind === "set" && action.tier === "protect") {
|
|
7060
7064
|
const expectedProtectOwnership = getProtectOwnership(configResult.value.guardian);
|
|
7061
7065
|
for (const file of changedPaths) {
|
|
@@ -7395,17 +7399,11 @@ class ProposalManager extends EventEmitter {
|
|
|
7395
7399
|
if (!freshTree.ok) {
|
|
7396
7400
|
const error = new Error(`Failed to build fresh state tree: ${freshTree.error.message}`);
|
|
7397
7401
|
this.emit("error", error, "approval:verification");
|
|
7398
|
-
proposal
|
|
7399
|
-
this._activeProposal = null;
|
|
7400
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7401
|
-
this.emit("rejected", proposal);
|
|
7402
|
+
await this._rejectProposal(proposal);
|
|
7402
7403
|
return;
|
|
7403
7404
|
}
|
|
7404
7405
|
if (freshTree.value.approvalHash !== proposal.payload.hash) {
|
|
7405
|
-
proposal
|
|
7406
|
-
this._activeProposal = null;
|
|
7407
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7408
|
-
this.emit("rejected", proposal);
|
|
7406
|
+
await this._rejectProposal(proposal);
|
|
7409
7407
|
return;
|
|
7410
7408
|
}
|
|
7411
7409
|
const applyResult = await apply({
|
|
@@ -7416,10 +7414,7 @@ class ProposalManager extends EventEmitter {
|
|
|
7416
7414
|
if (!applyResult.ok) {
|
|
7417
7415
|
const error = new Error(`Apply failed: ${applyResult.error.kind}`);
|
|
7418
7416
|
this.emit("error", error, "approval:apply");
|
|
7419
|
-
proposal
|
|
7420
|
-
this._activeProposal = null;
|
|
7421
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7422
|
-
this.emit("rejected", proposal);
|
|
7417
|
+
await this._rejectProposal(proposal);
|
|
7423
7418
|
return;
|
|
7424
7419
|
}
|
|
7425
7420
|
proposal.state = "approved";
|
|
@@ -7427,10 +7422,7 @@ class ProposalManager extends EventEmitter {
|
|
|
7427
7422
|
await this._channel.postResult(proposal.externalId, "applied");
|
|
7428
7423
|
this.emit("applied", proposal);
|
|
7429
7424
|
} else {
|
|
7430
|
-
proposal
|
|
7431
|
-
this._activeProposal = null;
|
|
7432
|
-
await this._channel.postResult(proposal.externalId, "rejected");
|
|
7433
|
-
this.emit("rejected", proposal);
|
|
7425
|
+
await this._rejectProposal(proposal);
|
|
7434
7426
|
}
|
|
7435
7427
|
} catch (e) {
|
|
7436
7428
|
if (e instanceof DOMException && e.name === "AbortError")
|
|
@@ -7439,15 +7431,21 @@ class ProposalManager extends EventEmitter {
|
|
|
7439
7431
|
return;
|
|
7440
7432
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
7441
7433
|
this.emit("error", error, "approval:wait");
|
|
7442
|
-
proposal
|
|
7443
|
-
this._activeProposal = null;
|
|
7434
|
+
await this._rejectProposal(proposal);
|
|
7444
7435
|
}
|
|
7445
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
|
+
}
|
|
7446
7445
|
async _supersedePending() {
|
|
7447
7446
|
if (this._activeProposal && this._abortController) {
|
|
7448
7447
|
const oldProposal = this._activeProposal;
|
|
7449
7448
|
const oldController = this._abortController;
|
|
7450
|
-
oldProposal.state = "superseded";
|
|
7451
7449
|
this._activeProposal = null;
|
|
7452
7450
|
this._abortController = null;
|
|
7453
7451
|
oldController.abort();
|
|
@@ -7455,6 +7453,9 @@ class ProposalManager extends EventEmitter {
|
|
|
7455
7453
|
await this._pendingFlow.catch(() => {});
|
|
7456
7454
|
this._pendingFlow = null;
|
|
7457
7455
|
}
|
|
7456
|
+
if (oldProposal.state !== "pending")
|
|
7457
|
+
return;
|
|
7458
|
+
oldProposal.state = "superseded";
|
|
7458
7459
|
await this._channel.postResult(oldProposal.externalId, "superseded");
|
|
7459
7460
|
this.emit("superseded", oldProposal);
|
|
7460
7461
|
}
|
|
@@ -7469,6 +7470,15 @@ class ProposalManager extends EventEmitter {
|
|
|
7469
7470
|
this._activeProposal = null;
|
|
7470
7471
|
this._abortController = null;
|
|
7471
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
|
+
}
|
|
7472
7482
|
}
|
|
7473
7483
|
// ../core/src/daemon/daemon.ts
|
|
7474
7484
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
@@ -7493,15 +7503,20 @@ function getChannel(name) {
|
|
|
7493
7503
|
class SoulguardDaemon extends EventEmitter2 {
|
|
7494
7504
|
_ops;
|
|
7495
7505
|
_config;
|
|
7506
|
+
_channelOverride;
|
|
7507
|
+
_maxProposals;
|
|
7496
7508
|
_channel = null;
|
|
7497
7509
|
_proposalManager = null;
|
|
7498
7510
|
_syncTimer = null;
|
|
7499
7511
|
_syncRunning = false;
|
|
7500
7512
|
_running = false;
|
|
7513
|
+
_doneResolve = null;
|
|
7501
7514
|
constructor(options) {
|
|
7502
7515
|
super();
|
|
7503
7516
|
this._ops = options.ops;
|
|
7504
7517
|
this._config = options.config;
|
|
7518
|
+
this._channelOverride = options.channelOverride ?? null;
|
|
7519
|
+
this._maxProposals = options.maxProposals ?? null;
|
|
7505
7520
|
}
|
|
7506
7521
|
get running() {
|
|
7507
7522
|
return this._running;
|
|
@@ -7516,14 +7531,20 @@ class SoulguardDaemon extends EventEmitter2 {
|
|
|
7516
7531
|
if (!daemonConfig) {
|
|
7517
7532
|
throw new Error("Daemon configuration missing. Add a 'daemon' section to soulguard.json.");
|
|
7518
7533
|
}
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
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);
|
|
7524
7545
|
}
|
|
7525
|
-
|
|
7526
|
-
|
|
7546
|
+
}
|
|
7547
|
+
if (this._channel) {
|
|
7527
7548
|
this._proposalManager = new ProposalManager({
|
|
7528
7549
|
ops: this._ops,
|
|
7529
7550
|
config: this._config,
|
|
@@ -7534,6 +7555,17 @@ class SoulguardDaemon extends EventEmitter2 {
|
|
|
7534
7555
|
this._proposalManager.on("rejected", (...args) => this.emit("rejected", ...args));
|
|
7535
7556
|
this._proposalManager.on("superseded", (...args) => this.emit("superseded", ...args));
|
|
7536
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
|
+
}
|
|
7537
7569
|
this._proposalManager.start();
|
|
7538
7570
|
}
|
|
7539
7571
|
const syncIntervalSecs = daemonConfig.syncIntervalSecs ?? 60;
|
|
@@ -7543,6 +7575,13 @@ class SoulguardDaemon extends EventEmitter2 {
|
|
|
7543
7575
|
}
|
|
7544
7576
|
this._running = true;
|
|
7545
7577
|
}
|
|
7578
|
+
done() {
|
|
7579
|
+
if (!this._running)
|
|
7580
|
+
return Promise.resolve();
|
|
7581
|
+
return new Promise((resolve3) => {
|
|
7582
|
+
this._doneResolve = resolve3;
|
|
7583
|
+
});
|
|
7584
|
+
}
|
|
7546
7585
|
async stop() {
|
|
7547
7586
|
if (!this._running)
|
|
7548
7587
|
return;
|
|
@@ -7555,10 +7594,12 @@ class SoulguardDaemon extends EventEmitter2 {
|
|
|
7555
7594
|
await this._proposalManager.stop();
|
|
7556
7595
|
this._proposalManager = null;
|
|
7557
7596
|
}
|
|
7558
|
-
if (this._channel) {
|
|
7597
|
+
if (this._channel && !this._channelOverride) {
|
|
7559
7598
|
await this._channel.dispose();
|
|
7560
7599
|
this._channel = null;
|
|
7561
7600
|
}
|
|
7601
|
+
this._doneResolve?.();
|
|
7602
|
+
this._doneResolve = null;
|
|
7562
7603
|
}
|
|
7563
7604
|
async _runSync() {
|
|
7564
7605
|
if (this._syncRunning)
|