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.
Files changed (2) hide show
  1. package/dist/index.js +69 -28
  2. 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.stagedHash !== null || f.status === "deleted");
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.state = "rejected";
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.state = "rejected";
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.state = "rejected";
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.state = "rejected";
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.state = "rejected";
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
- const channelName = daemonConfig.channel;
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.`);
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
- const channelConfig = daemonConfig[channelName];
7526
- this._channel = createChannelFn(channelConfig);
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulguard",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Identity protection for AI agents",
5
5
  "homepage": "https://soulguard.ai",
6
6
  "license": "MIT",