@sanctuary-framework/mcp-server 1.2.4 → 1.2.5

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/cli.js CHANGED
@@ -4450,9 +4450,35 @@ function validatePolicy(raw) {
4450
4450
  };
4451
4451
  delete merged.auto_deny;
4452
4452
  return merged;
4453
- })()
4453
+ })(),
4454
+ approval_redirect: parseApprovalRedirect(raw.approval_redirect)
4454
4455
  };
4455
4456
  }
4457
+ function parseApprovalRedirect(raw) {
4458
+ if (raw === void 0 || raw === null) {
4459
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4460
+ }
4461
+ if (typeof raw !== "object") {
4462
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4463
+ }
4464
+ const obj = raw;
4465
+ const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
4466
+ const modeRaw = obj.mode;
4467
+ let mode = DEFAULT_APPROVAL_REDIRECT.mode;
4468
+ if (modeRaw !== void 0) {
4469
+ if (modeRaw !== "replace" && modeRaw !== "notify") {
4470
+ throw new Error(
4471
+ `approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
4472
+ );
4473
+ }
4474
+ mode = modeRaw;
4475
+ }
4476
+ const result = { enabled, mode };
4477
+ if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
4478
+ result.per_agent = obj.per_agent;
4479
+ }
4480
+ return result;
4481
+ }
4456
4482
  function generateDefaultPolicyYaml() {
4457
4483
  return `# Sanctuary Principal Policy v1
4458
4484
  # This file controls what your agent can do without asking.
@@ -4531,6 +4557,7 @@ tier3_always_allow:
4531
4557
  - handshake_status
4532
4558
  - handshake_exchange
4533
4559
  - handshake_verify_attestation
4560
+ - handshake_abort
4534
4561
  - reputation_query_weighted
4535
4562
  - federation_peers
4536
4563
  - federation_trust_evaluate
@@ -4565,6 +4592,21 @@ tier3_always_allow:
4565
4592
  approval_channel:
4566
4593
  type: stderr
4567
4594
  timeout_seconds: 300
4595
+
4596
+ # \u2500\u2500\u2500 Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4597
+ # Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
4598
+ # resolve via the unified approval inbox at /api/approval-inbox/* instead
4599
+ # of (or in addition to) the configured approval_channel above.
4600
+ #
4601
+ # mode:
4602
+ # replace: bypass the approval_channel entirely; the gate awaits a
4603
+ # decision from the inbox (default once enabled).
4604
+ # notify: fire BOTH the approval_channel and the inbox; first decision
4605
+ # wins. Right shape for harnesses that cannot fully suppress
4606
+ # their local approval prompt (e.g. Mastra-class).
4607
+ approval_redirect:
4608
+ enabled: false
4609
+ mode: replace
4568
4610
  `;
4569
4611
  }
4570
4612
  async function loadPrincipalPolicy(storagePath) {
@@ -4601,7 +4643,7 @@ async function loadPrincipalPolicy(storagePath) {
4601
4643
  );
4602
4644
  }
4603
4645
  }
4604
- var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4646
+ var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_APPROVAL_REDIRECT, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4605
4647
  var init_loader = __esm({
4606
4648
  "src/principal-policy/loader.ts"() {
4607
4649
  DEFAULT_TIER2 = {
@@ -4618,6 +4660,10 @@ var init_loader = __esm({
4618
4660
  // SEC-002: auto_deny is not configurable. Timeout always denies.
4619
4661
  // Field omitted intentionally — all channels hardcode deny on timeout.
4620
4662
  };
4663
+ DEFAULT_APPROVAL_REDIRECT = {
4664
+ enabled: false,
4665
+ mode: "replace"
4666
+ };
4621
4667
  DEFAULT_POLICY = {
4622
4668
  version: 1,
4623
4669
  tier1_always_approve: [
@@ -4691,6 +4737,7 @@ var init_loader = __esm({
4691
4737
  "handshake_status",
4692
4738
  "handshake_exchange",
4693
4739
  "handshake_verify_attestation",
4740
+ "handshake_abort",
4694
4741
  "reputation_query_weighted",
4695
4742
  "federation_peers",
4696
4743
  "federation_trust_evaluate",
@@ -4732,7 +4779,8 @@ var init_loader = __esm({
4732
4779
  "compliance_eu_ai_act_annex_iii_classify"
4733
4780
  // Read-only; rule-based Annex III classifier
4734
4781
  ],
4735
- approval_channel: DEFAULT_CHANNEL
4782
+ approval_channel: DEFAULT_CHANNEL,
4783
+ approval_redirect: DEFAULT_APPROVAL_REDIRECT
4736
4784
  };
4737
4785
  MalformedPrincipalPolicyError = class extends Error {
4738
4786
  constructor(policyPath, reason) {
@@ -20563,6 +20611,174 @@ var init_approval_aggregator = __esm({
20563
20611
  }
20564
20612
  });
20565
20613
 
20614
+ // src/principal-policy/channels/aggregator-backed-channel.ts
20615
+ function auditEntryIdFor(request) {
20616
+ return `${request.timestamp}:${request.operation}`;
20617
+ }
20618
+ function statusToDecision(entry) {
20619
+ switch (entry.status) {
20620
+ case "approved":
20621
+ return {
20622
+ decision: "approve",
20623
+ decided_by: "human"
20624
+ };
20625
+ case "denied":
20626
+ return {
20627
+ decision: "deny",
20628
+ decided_by: "human"
20629
+ };
20630
+ case "timeout":
20631
+ case "expired":
20632
+ return {
20633
+ decision: "deny",
20634
+ decided_by: "timeout"
20635
+ };
20636
+ default:
20637
+ return null;
20638
+ }
20639
+ }
20640
+ function makeRedirectResolverFromPolicySupplier(supplier) {
20641
+ return (_request) => {
20642
+ const cfg = supplier().approval_redirect;
20643
+ if (!cfg || cfg.enabled !== true) {
20644
+ return { enabled: false, mode: "replace" };
20645
+ }
20646
+ return {
20647
+ enabled: true,
20648
+ mode: cfg.mode === "notify" ? "notify" : "replace"
20649
+ };
20650
+ };
20651
+ }
20652
+ var DEFAULT_REPLACE_MODE_TIMEOUT_MS, AggregatorBackedChannel;
20653
+ var init_aggregator_backed_channel = __esm({
20654
+ "src/principal-policy/channels/aggregator-backed-channel.ts"() {
20655
+ DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
20656
+ AggregatorBackedChannel = class {
20657
+ underlying;
20658
+ aggregator;
20659
+ resolveRedirect;
20660
+ replaceModeTimeoutMs;
20661
+ now;
20662
+ constructor(opts) {
20663
+ this.underlying = opts.underlying;
20664
+ this.aggregator = opts.aggregator;
20665
+ this.resolveRedirect = opts.resolveRedirect;
20666
+ this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
20667
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
20668
+ }
20669
+ /** Expose underlying for tests / wire-up reuse. */
20670
+ getUnderlying() {
20671
+ return this.underlying;
20672
+ }
20673
+ async requestApproval(request) {
20674
+ const cfg = this.resolveRedirect(request);
20675
+ if (!cfg.enabled) {
20676
+ return this.underlying.requestApproval(request);
20677
+ }
20678
+ if (cfg.mode === "replace") {
20679
+ return this.awaitAggregatorDecision(request);
20680
+ }
20681
+ return this.notifyMode(request);
20682
+ }
20683
+ /**
20684
+ * `replace` mode. Subscribe to the aggregator's event stream BEFORE
20685
+ * checking already-stored entries (avoids a race where the entry resolves
20686
+ * between list and subscribe). Match incoming events to this request by
20687
+ * audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
20688
+ */
20689
+ async awaitAggregatorDecision(request) {
20690
+ const auditId = auditEntryIdFor(request);
20691
+ return new Promise((resolveOuter) => {
20692
+ let settled = false;
20693
+ let unsubscribe = null;
20694
+ let timeoutHandle = null;
20695
+ const settle = (response) => {
20696
+ if (settled) return;
20697
+ settled = true;
20698
+ if (timeoutHandle) clearTimeout(timeoutHandle);
20699
+ if (unsubscribe) {
20700
+ try {
20701
+ unsubscribe();
20702
+ } catch {
20703
+ }
20704
+ }
20705
+ resolveOuter(response);
20706
+ };
20707
+ const onEvent = (emit) => {
20708
+ if (emit.type !== "resolved") return;
20709
+ if (emit.entry.audit_log_entry_id !== auditId) return;
20710
+ const mapped = statusToDecision(emit.entry);
20711
+ if (!mapped) return;
20712
+ settle({
20713
+ decision: mapped.decision,
20714
+ decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
20715
+ decided_by: mapped.decided_by
20716
+ });
20717
+ };
20718
+ try {
20719
+ unsubscribe = this.aggregator.onEvent(onEvent);
20720
+ } catch (err) {
20721
+ settle({
20722
+ decision: "deny",
20723
+ decided_at: this.now().toISOString(),
20724
+ decided_by: "channel_failure"
20725
+ });
20726
+ throw err instanceof Error ? err : new Error(String(err));
20727
+ }
20728
+ void this.aggregator.list({ limit: 200 }).then((entries) => {
20729
+ for (const entry of entries) {
20730
+ if (entry.audit_log_entry_id !== auditId) continue;
20731
+ const mapped = statusToDecision(entry);
20732
+ if (!mapped) return;
20733
+ settle({
20734
+ decision: mapped.decision,
20735
+ decided_at: entry.resolved_at ?? this.now().toISOString(),
20736
+ decided_by: mapped.decided_by
20737
+ });
20738
+ return;
20739
+ }
20740
+ }).catch(() => {
20741
+ });
20742
+ timeoutHandle = setTimeout(() => {
20743
+ settle({
20744
+ decision: "deny",
20745
+ decided_at: this.now().toISOString(),
20746
+ decided_by: "timeout"
20747
+ });
20748
+ }, this.replaceModeTimeoutMs);
20749
+ });
20750
+ }
20751
+ /**
20752
+ * `notify` mode. Fire the underlying channel and listen on the
20753
+ * aggregator simultaneously; whichever resolves first wins. Both
20754
+ * paths produce identical `ApprovalResponse` shapes; the gate's
20755
+ * downstream audit logging is unchanged.
20756
+ *
20757
+ * On underlying-channel failure, fall through to the aggregator wait
20758
+ * (still bounded by `replaceModeTimeoutMs`). Operator can still
20759
+ * resolve from the inbox even if the dashboard/webhook is down.
20760
+ */
20761
+ async notifyMode(request) {
20762
+ const aggregatorPromise = this.awaitAggregatorDecision(request);
20763
+ let underlyingPromise;
20764
+ try {
20765
+ underlyingPromise = this.underlying.requestApproval(request);
20766
+ } catch (err) {
20767
+ const response = await aggregatorPromise;
20768
+ return response;
20769
+ }
20770
+ return Promise.race([
20771
+ aggregatorPromise,
20772
+ underlyingPromise.catch(
20773
+ () => new Promise(() => {
20774
+ })
20775
+ )
20776
+ ]);
20777
+ }
20778
+ };
20779
+ }
20780
+ });
20781
+
20566
20782
  // src/principal-policy/tools.ts
20567
20783
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
20568
20784
  return [
@@ -21466,6 +21682,76 @@ var init_attestation = __esm({
21466
21682
  }
21467
21683
  });
21468
21684
 
21685
+ // src/handshake/audit.ts
21686
+ function auditHandshakeInitiated(auditLog, ctx) {
21687
+ auditLog.append(
21688
+ "l4",
21689
+ HANDSHAKE_LIFECYCLE_OPS.INITIATED,
21690
+ ctx.identity_id,
21691
+ detailsFromContext(ctx),
21692
+ "success"
21693
+ );
21694
+ }
21695
+ function auditHandshakeCompleted(auditLog, ctx) {
21696
+ const details = detailsFromContext(ctx);
21697
+ if (ctx.trust_tier !== void 0) {
21698
+ details.trust_tier = ctx.trust_tier;
21699
+ }
21700
+ auditLog.append(
21701
+ "l4",
21702
+ HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
21703
+ ctx.identity_id,
21704
+ details,
21705
+ "success"
21706
+ );
21707
+ }
21708
+ function auditHandshakeFailed(auditLog, ctx) {
21709
+ const details = detailsFromContext(ctx);
21710
+ details.reason = ctx.reason;
21711
+ if (ctx.error !== void 0) {
21712
+ details.error = ctx.error;
21713
+ }
21714
+ auditLog.append(
21715
+ "l4",
21716
+ HANDSHAKE_LIFECYCLE_OPS.FAILED,
21717
+ ctx.identity_id,
21718
+ details,
21719
+ "failure"
21720
+ );
21721
+ }
21722
+ function auditHandshakeAborted(auditLog, ctx) {
21723
+ const details = detailsFromContext(ctx);
21724
+ details.reason = ctx.reason;
21725
+ auditLog.append(
21726
+ "l4",
21727
+ HANDSHAKE_LIFECYCLE_OPS.ABORTED,
21728
+ ctx.identity_id,
21729
+ details,
21730
+ "failure"
21731
+ );
21732
+ }
21733
+ function detailsFromContext(ctx) {
21734
+ const details = {
21735
+ session_id: ctx.session_id,
21736
+ role: ctx.role
21737
+ };
21738
+ if (ctx.counterparty_id !== void 0) {
21739
+ details.counterparty_id = ctx.counterparty_id;
21740
+ }
21741
+ return details;
21742
+ }
21743
+ var HANDSHAKE_LIFECYCLE_OPS;
21744
+ var init_audit = __esm({
21745
+ "src/handshake/audit.ts"() {
21746
+ HANDSHAKE_LIFECYCLE_OPS = {
21747
+ INITIATED: "handshake_initiated",
21748
+ COMPLETED: "handshake_completed",
21749
+ FAILED: "handshake_failed",
21750
+ ABORTED: "handshake_aborted"
21751
+ };
21752
+ }
21753
+ });
21754
+
21469
21755
  // src/handshake/tools.ts
21470
21756
  function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
21471
21757
  const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
@@ -21499,6 +21785,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21499
21785
  const { challenge, session } = initiateHandshake(shr);
21500
21786
  sessions.set(session.session_id, session);
21501
21787
  auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
21788
+ auditHandshakeInitiated(auditLog, {
21789
+ session_id: session.session_id,
21790
+ role: "initiator",
21791
+ identity_id: shr.body.instance_id
21792
+ });
21502
21793
  return toolResult({
21503
21794
  session_id: session.session_id,
21504
21795
  challenge,
@@ -21538,10 +21829,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21538
21829
  );
21539
21830
  if ("error" in result) {
21540
21831
  auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
21832
+ auditHandshakeFailed(auditLog, {
21833
+ session_id: "unknown",
21834
+ role: "responder",
21835
+ identity_id: shr.body.instance_id,
21836
+ reason: classifyRespondFailure(result.error),
21837
+ error: result.error
21838
+ });
21541
21839
  return toolResult({ error: result.error });
21542
21840
  }
21543
21841
  sessions.set(result.session.session_id, result.session);
21544
21842
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
21843
+ auditHandshakeInitiated(auditLog, {
21844
+ session_id: result.session.session_id,
21845
+ role: "responder",
21846
+ identity_id: shr.body.instance_id,
21847
+ counterparty_id: challenge.shr.body.instance_id
21848
+ });
21545
21849
  let autoPublishResult;
21546
21850
  if (autoPublishHandshakes) {
21547
21851
  autoPublishResult = { attempted: true };
@@ -21649,9 +21953,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21649
21953
  const response = args.response;
21650
21954
  const session = sessions.get(sessionId);
21651
21955
  if (!session) {
21956
+ auditHandshakeFailed(auditLog, {
21957
+ session_id: sessionId,
21958
+ role: "initiator",
21959
+ identity_id: "unknown",
21960
+ reason: "session_unknown",
21961
+ error: `No handshake session found: ${sessionId}`
21962
+ });
21652
21963
  return toolResult({ error: `No handshake session found: ${sessionId}` });
21653
21964
  }
21654
21965
  if (session.state !== "initiated") {
21966
+ auditHandshakeFailed(auditLog, {
21967
+ session_id: sessionId,
21968
+ role: "initiator",
21969
+ identity_id: session.our_shr.body.instance_id,
21970
+ reason: "session_state_mismatch",
21971
+ error: `Session is in state '${session.state}', expected 'initiated'`
21972
+ });
21655
21973
  return toolResult({
21656
21974
  error: `Session is in state '${session.state}', expected 'initiated'`
21657
21975
  });
@@ -21665,6 +21983,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21665
21983
  if ("error" in result) {
21666
21984
  session.state = "failed";
21667
21985
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
21986
+ auditHandshakeFailed(auditLog, {
21987
+ session_id: sessionId,
21988
+ role: "initiator",
21989
+ identity_id: session.our_shr.body.instance_id,
21990
+ reason: classifyCompleteFailure(result.error),
21991
+ error: result.error
21992
+ });
21668
21993
  return toolResult({ error: result.error });
21669
21994
  }
21670
21995
  session.state = "completed";
@@ -21673,6 +21998,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21673
21998
  session.result = result.result;
21674
21999
  handshakeResults.set(result.result.counterparty_id, result.result);
21675
22000
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
22001
+ auditHandshakeCompleted(auditLog, {
22002
+ session_id: sessionId,
22003
+ role: "initiator",
22004
+ identity_id: session.our_shr.body.instance_id,
22005
+ counterparty_id: result.result.counterparty_id,
22006
+ trust_tier: result.result.trust_tier
22007
+ });
21676
22008
  return toolResult({
21677
22009
  completion: result.completion,
21678
22010
  result: result.result,
@@ -21720,6 +22052,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21720
22052
  void 0,
21721
22053
  result.verified ? "success" : "failure"
21722
22054
  );
22055
+ if (result.verified) {
22056
+ auditHandshakeCompleted(auditLog, {
22057
+ session_id: session.session_id,
22058
+ role: "responder",
22059
+ identity_id: session.our_shr.body.instance_id,
22060
+ counterparty_id: result.counterparty_id,
22061
+ trust_tier: result.trust_tier
22062
+ });
22063
+ } else {
22064
+ auditHandshakeFailed(auditLog, {
22065
+ session_id: session.session_id,
22066
+ role: "responder",
22067
+ identity_id: session.our_shr.body.instance_id,
22068
+ counterparty_id: result.counterparty_id,
22069
+ reason: classifyCompleteFailure(result.errors.join("; ")),
22070
+ error: result.errors.join("; ")
22071
+ });
22072
+ }
21723
22073
  return toolResult({ result });
21724
22074
  }
21725
22075
  return toolResult({
@@ -21827,10 +22177,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21827
22177
  _content_trust: "external"
21828
22178
  });
21829
22179
  }
22180
+ },
22181
+ {
22182
+ name: "handshake_abort",
22183
+ description: "Abort an in-flight handshake session. Drops the session record and appends a session-lifecycle audit entry (handshake_aborted) so the operator can distinguish operator-cancelled, timed-out, and dropped sessions from sessions that simply fell off the protocol path.",
22184
+ inputSchema: {
22185
+ type: "object",
22186
+ properties: {
22187
+ session_id: {
22188
+ type: "string",
22189
+ description: "Session ID returned from handshake_initiate / handshake_respond."
22190
+ },
22191
+ reason: {
22192
+ type: "string",
22193
+ enum: [
22194
+ "operator_cancelled",
22195
+ "session_timeout",
22196
+ "transport_dropped",
22197
+ "shutdown",
22198
+ "other"
22199
+ ],
22200
+ description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
22201
+ }
22202
+ },
22203
+ required: ["session_id"]
22204
+ },
22205
+ handler: async (args) => {
22206
+ const sessionId = args.session_id;
22207
+ const reason = args.reason ?? "operator_cancelled";
22208
+ const session = sessions.get(sessionId);
22209
+ if (!session) {
22210
+ return toolResult({ error: `No handshake session found: ${sessionId}` });
22211
+ }
22212
+ if (session.state === "completed") {
22213
+ return toolResult({
22214
+ error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
22215
+ });
22216
+ }
22217
+ sessions.delete(sessionId);
22218
+ auditHandshakeAborted(auditLog, {
22219
+ session_id: sessionId,
22220
+ role: session.role,
22221
+ identity_id: session.our_shr.body.instance_id,
22222
+ ...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
22223
+ reason
22224
+ });
22225
+ return toolResult({
22226
+ aborted: true,
22227
+ session_id: sessionId,
22228
+ reason
22229
+ });
22230
+ }
21830
22231
  }
21831
22232
  ];
21832
22233
  return { tools, handshakeResults };
21833
22234
  }
22235
+ function classifyRespondFailure(error) {
22236
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
22237
+ if (error.includes("SHR verification failed")) return "shr_invalid";
22238
+ if (error.includes("No identity available")) return "no_signing_identity";
22239
+ return "other";
22240
+ }
22241
+ function classifyCompleteFailure(error) {
22242
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
22243
+ if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
22244
+ if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
22245
+ if (error.includes("No identity available")) return "no_signing_identity";
22246
+ return "other";
22247
+ }
21834
22248
  var init_tools6 = __esm({
21835
22249
  "src/handshake/tools.ts"() {
21836
22250
  init_router();
@@ -21840,6 +22254,7 @@ var init_tools6 = __esm({
21840
22254
  init_encoding();
21841
22255
  init_protocol();
21842
22256
  init_attestation();
22257
+ init_audit();
21843
22258
  init_verifier();
21844
22259
  }
21845
22260
  });
@@ -32975,7 +33390,14 @@ var init_operator_chat_audit_events = __esm({
32975
33390
  * successful thread removal. Body carries thread_id + turn_count of
32976
33391
  * the deleted bundle.
32977
33392
  */
32978
- CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
33393
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
33394
+ /**
33395
+ * Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
33396
+ * the multi-turn coherence fold cannot load the active thread's prior
33397
+ * turns; the concierge degrades to single-turn after emitting. Body
33398
+ * carries thread_id + a stable failure_reason enum.
33399
+ */
33400
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
32979
33401
  };
32980
33402
  }
32981
33403
  });
@@ -32988,13 +33410,20 @@ var init_operator_chat_types = __esm({
32988
33410
  CONCIERGE_THREAD_KEY = "_fortress";
32989
33411
  }
32990
33412
  });
33413
+ function approxTokenLen(text) {
33414
+ return Math.ceil(text.length / 4);
33415
+ }
32991
33416
  function makeEventId(prefix) {
32992
33417
  return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
32993
33418
  }
33419
+ function formatPriorTurnLine(turn) {
33420
+ const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
33421
+ return `${label}: ${turn.content}`;
33422
+ }
32994
33423
  function hashOf(input) {
32995
33424
  return hashToString(sha256(stringToBytes(input)));
32996
33425
  }
32997
- var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
33426
+ var DEFAULT_CONCIERGE_MAX_TOKENS, DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS, DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS, DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET, DEFAULT_CONCIERGE_SESSION_TTL_MS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
32998
33427
  var init_operator_chat_service = __esm({
32999
33428
  "src/chat/operator-chat-service.ts"() {
33000
33429
  init_hashing();
@@ -33002,6 +33431,10 @@ var init_operator_chat_service = __esm({
33002
33431
  init_operator_chat_audit_events();
33003
33432
  init_operator_chat_types();
33004
33433
  DEFAULT_CONCIERGE_MAX_TOKENS = 512;
33434
+ DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
33435
+ DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
33436
+ DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
33437
+ DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
33005
33438
  SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
33006
33439
  1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
33007
33440
  2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
@@ -33038,6 +33471,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33038
33471
  piiFilter;
33039
33472
  conciergeMaxTokens;
33040
33473
  memory;
33474
+ historyWindowTurns;
33475
+ historyFreshnessMs;
33476
+ historyTokenBudget;
33477
+ sessionTtlMs;
33478
+ clock;
33041
33479
  /**
33042
33480
  * In-memory thread_id assigned to the active concierge session.
33043
33481
  * The first sendConcierge call after construction allocates a fresh
@@ -33045,6 +33483,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33045
33483
  * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
33046
33484
  */
33047
33485
  activeMemoryThreadId;
33486
+ /**
33487
+ * Wall-clock ms of the most recent sendConcierge that touched the
33488
+ * active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
33489
+ * check: a fresh sendConcierge after `sessionTtlMs` of quiet
33490
+ * allocates a new thread_id even though the prior one is still
33491
+ * readable from the memory store.
33492
+ */
33493
+ lastInteractionAt;
33048
33494
  constructor(deps) {
33049
33495
  this.store = deps.store;
33050
33496
  this.auditLog = deps.auditLog;
@@ -33056,6 +33502,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33056
33502
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
33057
33503
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
33058
33504
  if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
33505
+ this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
33506
+ this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
33507
+ this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
33508
+ this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
33509
+ this.clock = deps.conciergeClock ?? (() => Date.now());
33059
33510
  }
33060
33511
  // ── Concierge ─────────────────────────────────────────────────────────
33061
33512
  /**
@@ -33074,6 +33525,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33074
33525
  throw new Error("concierge query must not be empty");
33075
33526
  }
33076
33527
  const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
33528
+ const nowMs = this.clock();
33529
+ if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
33530
+ this.activeMemoryThreadId = void 0;
33531
+ }
33077
33532
  const operatorMessage = {
33078
33533
  message_id: randomUUID(),
33079
33534
  surface: "concierge",
@@ -33086,6 +33541,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33086
33541
  CONCIERGE_THREAD_KEY,
33087
33542
  operatorMessage
33088
33543
  );
33544
+ let priorTurns = [];
33545
+ let memoryReadFailureReason = null;
33546
+ let activeThreadIdForRound;
33547
+ if (this.memory) {
33548
+ activeThreadIdForRound = this.ensureActiveMemoryThread();
33549
+ const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
33550
+ if (result.ok) {
33551
+ const cutoff = nowMs - this.historyFreshnessMs;
33552
+ const fresh = result.turns.filter((t) => {
33553
+ const ts = Date.parse(t.created_at);
33554
+ return Number.isFinite(ts) && ts >= cutoff;
33555
+ });
33556
+ const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
33557
+ priorTurns = recent;
33558
+ } else {
33559
+ memoryReadFailureReason = result.reason;
33560
+ this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
33561
+ }
33562
+ }
33089
33563
  if (this.memory) {
33090
33564
  const threadId = this.ensureActiveMemoryThread();
33091
33565
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
@@ -33107,7 +33581,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33107
33581
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
33108
33582
  outcome = "substrate_disabled";
33109
33583
  } else {
33110
- const context = await this.assembleConciergeContext();
33584
+ const context = await this.assembleConciergeContext(priorTurns);
33111
33585
  const response = await this.substrateSelector.invokeSummarize(
33112
33586
  "concierge",
33113
33587
  {
@@ -33146,10 +33620,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33146
33620
  CONCIERGE_THREAD_KEY,
33147
33621
  responseMessage
33148
33622
  );
33623
+ let assistantTurnId;
33149
33624
  if (this.memory) {
33150
33625
  const threadId = this.ensureActiveMemoryThread();
33151
- await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
33152
- });
33626
+ const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
33627
+ if (persisted) assistantTurnId = persisted.turn_id;
33628
+ }
33629
+ if (this.memory && activeThreadIdForRound) {
33630
+ this.lastInteractionAt = nowMs;
33153
33631
  }
33154
33632
  const payload = {
33155
33633
  version: "1.2",
@@ -33162,7 +33640,12 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33162
33640
  response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
33163
33641
  substrate: servedBy,
33164
33642
  latency_ms: latencyMs,
33165
- outcome
33643
+ outcome,
33644
+ ...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
33645
+ ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
33646
+ ...this.memory ? {
33647
+ prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
33648
+ } : {}
33166
33649
  };
33167
33650
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
33168
33651
  return {
@@ -33172,6 +33655,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33172
33655
  outcome
33173
33656
  };
33174
33657
  }
33658
+ /**
33659
+ * Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
33660
+ * out of `sendConcierge` so the read-fold path stays readable. Emits
33661
+ * with `result: "failure"` since the concierge fell back to
33662
+ * single-turn mode for this round-trip.
33663
+ */
33664
+ emitMemoryReadFailed(threadId, reason) {
33665
+ const payload = {
33666
+ version: "1.2",
33667
+ event_id: makeEventId("conc-memfail"),
33668
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33669
+ identity_id: this.identityId,
33670
+ kind: "operator_concierge_memory_read_failed",
33671
+ surface: "concierge",
33672
+ thread_id: threadId,
33673
+ failure_reason: reason
33674
+ };
33675
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
33676
+ }
33175
33677
  /**
33176
33678
  * Read the persisted concierge thread, oldest message first. Returns
33177
33679
  * an empty array when no thread exists yet.
@@ -33294,6 +33796,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33294
33796
  * ## Sanctuary reference
33295
33797
  * <static domain reference block>
33296
33798
  *
33799
+ * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
33800
+ * OPERATOR: ...
33801
+ * CONCIERGE: ...
33802
+ * ---
33803
+ *
33297
33804
  * ## Recent activity
33298
33805
  * <recentActivity output>
33299
33806
  *
@@ -33303,37 +33810,69 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33303
33810
  * ## Open inbox
33304
33811
  * <openInbox output>
33305
33812
  * ```
33306
- */
33307
- async assembleConciergeContext() {
33813
+ *
33814
+ * The substrate selector ships a `context: string` shape (not a
33815
+ * messages array), so multi-turn coherence is folded as a structured
33816
+ * prior-conversation section with explicit OPERATOR / CONCIERGE
33817
+ * boundaries. Coordinator-CTO guidance: prefer messages-array shape
33818
+ * if available; the v1.2 selector does not expose one, so structured
33819
+ * serialization is the canonical path for v1.3.
33820
+ */
33821
+ async assembleConciergeContext(priorTurns = []) {
33308
33822
  const ref = `## Sanctuary reference
33309
33823
  ${SANCTUARY_DOMAIN_REFERENCE}`;
33824
+ const priorSection = this.formatPriorTurnsSection(priorTurns);
33310
33825
  if (!this.contextProviders) {
33311
- return `${ref}
33312
-
33313
- ## Recent activity
33314
- (no providers wired)
33315
-
33316
- ## Wrapped agents
33317
- (no providers wired)
33318
-
33319
- ## Open inbox
33320
- (no providers wired)`;
33826
+ return [
33827
+ ref,
33828
+ ...priorSection ? [priorSection] : [],
33829
+ "## Recent activity\n(no providers wired)",
33830
+ "## Wrapped agents\n(no providers wired)",
33831
+ "## Open inbox\n(no providers wired)"
33832
+ ].join("\n\n");
33321
33833
  }
33322
33834
  const [activity, agents, inbox] = await Promise.all([
33323
33835
  this.contextProviders.recentActivity(),
33324
33836
  this.contextProviders.agentInventory(),
33325
33837
  this.contextProviders.openInbox()
33326
33838
  ]);
33327
- return `${ref}
33328
-
33329
- ## Recent activity
33330
- ${activity}
33331
-
33332
- ## Wrapped agents
33333
- ${agents}
33334
-
33335
- ## Open inbox
33336
- ${inbox}`;
33839
+ return [
33840
+ ref,
33841
+ ...priorSection ? [priorSection] : [],
33842
+ `## Recent activity
33843
+ ${activity}`,
33844
+ `## Wrapped agents
33845
+ ${agents}`,
33846
+ `## Open inbox
33847
+ ${inbox}`
33848
+ ].join("\n\n");
33849
+ }
33850
+ /**
33851
+ * Render the prior-conversation section with token-budget enforcement
33852
+ * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
33853
+ * section exceeds `historyTokenBudget`. Returns an empty string when
33854
+ * the input is empty or when the budget excludes every turn.
33855
+ */
33856
+ formatPriorTurnsSection(turns) {
33857
+ if (turns.length === 0) return "";
33858
+ const HEADER = "## Prior conversation";
33859
+ const lines = turns.map(formatPriorTurnLine);
33860
+ const headerTokens = approxTokenLen(`${HEADER}
33861
+ `);
33862
+ const sepTokens = approxTokenLen("\n");
33863
+ let runningTokens = headerTokens;
33864
+ let runningLines = [];
33865
+ for (let i = lines.length - 1; i >= 0; i--) {
33866
+ const line = lines[i];
33867
+ const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
33868
+ if (runningTokens + tokens > this.historyTokenBudget) break;
33869
+ runningTokens += tokens;
33870
+ runningLines.push(line);
33871
+ }
33872
+ if (runningLines.length === 0) return "";
33873
+ runningLines = runningLines.reverse();
33874
+ return `${HEADER}
33875
+ ${runningLines.join("\n")}`;
33337
33876
  }
33338
33877
  // ── audit helpers ────────────────────────────────────────────────────
33339
33878
  emit(operation, payload, result) {
@@ -33538,6 +34077,65 @@ var init_concierge_memory_store = __esm({
33538
34077
  }
33539
34078
  return turns;
33540
34079
  }
34080
+ /**
34081
+ * Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
34082
+ * `readThread` collapses every failure mode to an empty array, this
34083
+ * variant returns a discriminated result so the multi-turn fold path
34084
+ * can degrade cleanly + emit `operator_concierge_memory_read_failed`
34085
+ * with a concrete cause.
34086
+ *
34087
+ * - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
34088
+ * - Bundle present, decode + decrypt + schema check pass → ok with turns.
34089
+ * - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
34090
+ * - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
34091
+ * - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
34092
+ * - Storage IO error → `io_failed`.
34093
+ */
34094
+ async readThreadStrict(threadId, opts) {
34095
+ const key = bundleKey(threadId);
34096
+ let raw;
34097
+ try {
34098
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
34099
+ } catch {
34100
+ return { ok: false, reason: "io_failed" };
34101
+ }
34102
+ if (!raw) return { ok: true, turns: [] };
34103
+ if (raw.length > MAX_BUNDLE_BYTES2) {
34104
+ return { ok: false, reason: "oversize_bundle" };
34105
+ }
34106
+ let envelope;
34107
+ try {
34108
+ envelope = JSON.parse(bytesToString(raw));
34109
+ } catch {
34110
+ return { ok: false, reason: "schema_mismatch" };
34111
+ }
34112
+ let plaintext;
34113
+ try {
34114
+ const aad = stringToBytes(threadId);
34115
+ plaintext = decrypt(envelope, this.encryptionKey, aad);
34116
+ } catch {
34117
+ return { ok: false, reason: "decrypt_failed" };
34118
+ }
34119
+ let parsed;
34120
+ try {
34121
+ parsed = JSON.parse(bytesToString(plaintext));
34122
+ } catch {
34123
+ return { ok: false, reason: "schema_mismatch" };
34124
+ }
34125
+ if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
34126
+ if (parsed.thread_id !== threadId) {
34127
+ return { ok: false, reason: "schema_mismatch" };
34128
+ }
34129
+ let turns = parsed.turns;
34130
+ if (opts?.sinceTurnId !== void 0) {
34131
+ const cutoff = opts.sinceTurnId;
34132
+ turns = turns.filter((t) => t.turn_id > cutoff);
34133
+ }
34134
+ if (opts?.limit !== void 0) {
34135
+ turns = turns.slice(0, opts.limit);
34136
+ }
34137
+ return { ok: true, turns };
34138
+ }
33541
34139
  /**
33542
34140
  * Enumerate concierge threads in this fortress with summary metadata.
33543
34141
  * Sorted newest-first by last_turn_at.
@@ -36435,7 +37033,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
36435
37033
  }
36436
37034
  return null;
36437
37035
  }
36438
- async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
37036
+ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
36439
37037
  const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
36440
37038
  if (!destinationSigner) {
36441
37039
  return {
@@ -36497,8 +37095,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36497
37095
  }
36498
37096
  }
36499
37097
  }
37098
+ let plaintext;
36500
37099
  try {
36501
- const plaintext = decrypt(
37100
+ plaintext = decrypt(
36502
37101
  item.entry.payload,
36503
37102
  deriveNamespaceKey(sourceMasterKey, item.namespace)
36504
37103
  );
@@ -36507,28 +37106,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36507
37106
  skipped++;
36508
37107
  continue;
36509
37108
  }
36510
- await stateStore.write(
36511
- item.namespace,
36512
- item.key,
36513
- bytesToString(plaintext),
36514
- destinationSigner.identity_id,
36515
- destinationSigner.encrypted_private_key,
36516
- identityEncryptionKey,
36517
- {
36518
- content_type: item.entry.metadata.content_type,
36519
- ttl_seconds: item.entry.metadata.ttl_seconds,
36520
- tags: [
36521
- ...item.entry.metadata.tags ?? [],
36522
- "exit-import",
36523
- `source:${item.entry.kid}`
36524
- ]
36525
- }
36526
- );
36527
- imported++;
36528
37109
  } catch {
36529
37110
  skippedInvalidSig++;
36530
37111
  skipped++;
37112
+ continue;
36531
37113
  }
37114
+ await stateStore.write(
37115
+ item.namespace,
37116
+ item.key,
37117
+ bytesToString(plaintext),
37118
+ destinationSigner.identity_id,
37119
+ destinationSigner.encrypted_private_key,
37120
+ identityEncryptionKey,
37121
+ {
37122
+ content_type: item.entry.metadata.content_type,
37123
+ ttl_seconds: item.entry.metadata.ttl_seconds,
37124
+ tags: [
37125
+ ...item.entry.metadata.tags ?? [],
37126
+ "exit-import",
37127
+ `source:${item.entry.kid}`
37128
+ ]
37129
+ }
37130
+ );
37131
+ imported++;
37132
+ importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
36532
37133
  }
36533
37134
  return {
36534
37135
  status: "rekeyed",
@@ -36539,6 +37140,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36539
37140
  conflicts
36540
37141
  };
36541
37142
  }
37143
+ async function cleanupStagedPaths(storage, staged) {
37144
+ let removed = 0;
37145
+ const failed = [];
37146
+ for (const loc of staged) {
37147
+ try {
37148
+ const ok2 = await storage.delete(loc.namespace, loc.key);
37149
+ if (ok2) {
37150
+ removed++;
37151
+ } else {
37152
+ failed.push(loc);
37153
+ }
37154
+ } catch {
37155
+ failed.push(loc);
37156
+ }
37157
+ }
37158
+ return { removed, failed };
37159
+ }
36542
37160
  async function stageArtifact(storage, namespace, key, value) {
36543
37161
  await storage.write(namespace, key, jsonBytes(value));
36544
37162
  }
@@ -36663,6 +37281,8 @@ async function importExitBundle(opts) {
36663
37281
  }
36664
37282
  const importId = importIdForManifest(manifest);
36665
37283
  const stagedArtifacts = [];
37284
+ const stagedLocations = [];
37285
+ const importedRekeyEntries = [];
36666
37286
  if (identityArtifact) {
36667
37287
  await stageArtifact(
36668
37288
  opts.storage,
@@ -36671,10 +37291,15 @@ async function importExitBundle(opts) {
36671
37291
  identityArtifact.json
36672
37292
  );
36673
37293
  stagedArtifacts.push("public_identity");
37294
+ stagedLocations.push({
37295
+ namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
37296
+ key: identityArtifact.json.bundle.identity_id
37297
+ });
36674
37298
  }
36675
37299
  if (policySet) {
36676
37300
  await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
36677
37301
  stagedArtifacts.push("policy_set");
37302
+ stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
36678
37303
  }
36679
37304
  if (auditReceipts) {
36680
37305
  await stageArtifact(
@@ -36684,10 +37309,12 @@ async function importExitBundle(opts) {
36684
37309
  auditReceipts.json
36685
37310
  );
36686
37311
  stagedArtifacts.push("audit_receipts");
37312
+ stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
36687
37313
  }
36688
37314
  if (commitments) {
36689
37315
  await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
36690
37316
  stagedArtifacts.push("commitments");
37317
+ stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
36691
37318
  }
36692
37319
  if (placeholderMetadata) {
36693
37320
  await stageArtifact(
@@ -36697,12 +37324,17 @@ async function importExitBundle(opts) {
36697
37324
  placeholderMetadata.json
36698
37325
  );
36699
37326
  stagedArtifacts.push("placeholder_vault_metadata");
37327
+ stagedLocations.push({
37328
+ namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
37329
+ key: importId
37330
+ });
36700
37331
  }
36701
37332
  await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
36702
37333
  manifest: manifest.body,
36703
37334
  verified_at: verification.verified_at,
36704
37335
  activated_at: (/* @__PURE__ */ new Date()).toISOString()
36705
37336
  });
37337
+ stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
36706
37338
  const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
36707
37339
  let reputationResult = {
36708
37340
  imported_attestations: 0,
@@ -36727,26 +37359,57 @@ async function importExitBundle(opts) {
36727
37359
  encryptedState?.json ?? null,
36728
37360
  opts
36729
37361
  );
36730
- const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
36731
- encryptedState.json,
36732
- opts,
36733
- sourceMasterKey,
36734
- publicKeys.byIdentityId
36735
- ) : {
36736
- status: "staged_requires_source_key",
36737
- imported_keys: 0,
36738
- skipped_keys: encryptedState.json.entries.length,
36739
- skipped_invalid_sig: 0,
36740
- skipped_unknown_kid: 0,
36741
- conflicts: conflicts.state_conflicts.length
36742
- } : {
36743
- status: "not_requested",
36744
- imported_keys: 0,
36745
- skipped_keys: 0,
36746
- skipped_invalid_sig: 0,
36747
- skipped_unknown_kid: 0,
36748
- conflicts: 0
36749
- };
37362
+ let stateResult;
37363
+ try {
37364
+ stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
37365
+ encryptedState.json,
37366
+ opts,
37367
+ sourceMasterKey,
37368
+ publicKeys.byIdentityId,
37369
+ importedRekeyEntries
37370
+ ) : {
37371
+ status: "staged_requires_source_key",
37372
+ imported_keys: 0,
37373
+ skipped_keys: encryptedState.json.entries.length,
37374
+ skipped_invalid_sig: 0,
37375
+ skipped_unknown_kid: 0,
37376
+ conflicts: conflicts.state_conflicts.length
37377
+ } : {
37378
+ status: "not_requested",
37379
+ imported_keys: 0,
37380
+ skipped_keys: 0,
37381
+ skipped_invalid_sig: 0,
37382
+ skipped_unknown_kid: 0,
37383
+ conflicts: 0
37384
+ };
37385
+ } catch (err) {
37386
+ const toCleanup = [
37387
+ ...importedRekeyEntries,
37388
+ ...stagedLocations
37389
+ ];
37390
+ const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
37391
+ opts.auditLog.append(
37392
+ "l1",
37393
+ "exit_bundle_rekey_failed_cleanup",
37394
+ manifest.body.identity_binding.identity_id,
37395
+ {
37396
+ import_id: importId,
37397
+ manifest_version: manifest.body.manifest_version,
37398
+ rekey_entries_removed: importedRekeyEntries.length,
37399
+ staged_artifacts_removed: stagedLocations.length,
37400
+ removed_total: cleanup.removed,
37401
+ cleanup_failed_count: cleanup.failed.length,
37402
+ original_error: err instanceof Error ? err.message : String(err)
37403
+ },
37404
+ "failure"
37405
+ );
37406
+ await opts.auditLog.flush();
37407
+ const originalMessage = err instanceof Error ? err.message : String(err);
37408
+ throw new ExitBundleImportError(
37409
+ "REKEY_FAILED_AND_CLEANED",
37410
+ `Exit-bundle re-key failed: ${originalMessage}. Cleanup removed ${cleanup.removed} of ${toCleanup.length} staged paths (${importedRekeyEntries.length} re-keyed entries plus ${stagedLocations.length} staged artifacts; ${cleanup.failed.length} cleanup deletes failed).`
37411
+ );
37412
+ }
36750
37413
  opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
36751
37414
  import_id: importId,
36752
37415
  manifest_version: manifest.body.manifest_version,
@@ -37825,7 +38488,6 @@ ${err.message}
37825
38488
  timestamp: alert.timestamp
37826
38489
  });
37827
38490
  } : void 0;
37828
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
37829
38491
  const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37830
38492
  const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
37831
38493
  const approvalAggregator = new ApprovalAggregator({
@@ -37835,6 +38497,20 @@ ${err.message}
37835
38497
  identityId: aggregatorIdentityId,
37836
38498
  fortressId: fortressIdForAggregator
37837
38499
  });
38500
+ const wrappedApprovalChannel = new AggregatorBackedChannel({
38501
+ underlying: approvalChannel,
38502
+ aggregator: approvalAggregator,
38503
+ resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
38504
+ replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
38505
+ });
38506
+ const gate = new ApprovalGate(
38507
+ policy,
38508
+ baseline,
38509
+ wrappedApprovalChannel,
38510
+ auditLog,
38511
+ injectionDetector,
38512
+ onInjectionAlert
38513
+ );
37838
38514
  gate.setApprovalEventCallback((event) => {
37839
38515
  void approvalAggregator.ingest(event);
37840
38516
  });
@@ -38032,6 +38708,7 @@ var init_src = __esm({
38032
38708
  init_webhook();
38033
38709
  init_gate();
38034
38710
  init_approval_aggregator();
38711
+ init_aggregator_backed_channel();
38035
38712
  init_tools4();
38036
38713
  init_router();
38037
38714
  init_router();
@@ -40996,6 +41673,22 @@ var init_broker = __esm({
40996
41673
  auditLog;
40997
41674
  issuer;
40998
41675
  principalIdentityId;
41676
+ /**
41677
+ * Per-secret-name mutex. Hardening wave 6 finding #64: two concurrent
41678
+ * addSecret() / rotateSecret() / deleteSecret() calls on the same name
41679
+ * MUST serialize cleanly. The keychain backend's `find-then-add` and
41680
+ * `find-then-delete-then-add` shapes (KeychainBackend.addSecret /
41681
+ * .rotateSecret) are not atomic against another caller racing the same
41682
+ * service-name; without serialization the second caller can observe a
41683
+ * stale "exists" check and either drop the new value or leave a
41684
+ * duplicate keychain entry.
41685
+ *
41686
+ * Implementation: an in-memory promise chain per name. Subsequent
41687
+ * callers `await` the chain tail and append their own work; failures
41688
+ * propagate to the failing caller without poisoning the chain for
41689
+ * later callers.
41690
+ */
41691
+ nameLocks = /* @__PURE__ */ new Map();
40999
41692
  constructor(opts) {
41000
41693
  this.backend = opts.backend;
41001
41694
  this.auditLog = opts.auditLog;
@@ -41006,6 +41699,40 @@ var init_broker = __esm({
41006
41699
  grants: opts.grants
41007
41700
  });
41008
41701
  }
41702
+ /**
41703
+ * Serialize `op` against any other in-flight write to the same secret
41704
+ * `name`. Per-name fairness only, distinct names run in parallel.
41705
+ * The current chain tail is used as the acceptance gate; we then
41706
+ * publish a new tail that swallows the operation's outcome so a
41707
+ * thrown error does not poison the next caller's wait.
41708
+ */
41709
+ async withNameLock(name, op) {
41710
+ const previous = this.nameLocks.get(name) ?? Promise.resolve();
41711
+ let release = () => {
41712
+ };
41713
+ const next = new Promise((resolve8) => {
41714
+ release = resolve8;
41715
+ });
41716
+ this.nameLocks.set(name, next);
41717
+ try {
41718
+ await previous.catch(() => {
41719
+ });
41720
+ return await op();
41721
+ } finally {
41722
+ release();
41723
+ if (this.nameLocks.get(name) === next) {
41724
+ this.nameLocks.delete(name);
41725
+ }
41726
+ }
41727
+ }
41728
+ /**
41729
+ * Diagnostic-only: visible for tests so they can assert that distinct
41730
+ * names do not contend on a shared lock. Not part of the public broker
41731
+ * contract; do not consume from production code.
41732
+ */
41733
+ __nameLockCountForTests() {
41734
+ return this.nameLocks.size;
41735
+ }
41009
41736
  /** Ensure backend is initialized and unlocked. Audits the unlock. */
41010
41737
  async ensureUnlocked(passphrase) {
41011
41738
  await this.backend.ensureInitialized(passphrase);
@@ -41018,31 +41745,37 @@ var init_broker = __esm({
41018
41745
  );
41019
41746
  }
41020
41747
  async addSecret(name, value) {
41021
- await this.backend.addSecret(name, value);
41022
- this.auditLog.append(
41023
- "l3",
41024
- BROKER_OPS.SECRET_ADDED,
41025
- this.principalIdentityId,
41026
- { secret: name }
41027
- );
41748
+ await this.withNameLock(name, async () => {
41749
+ await this.backend.addSecret(name, value);
41750
+ this.auditLog.append(
41751
+ "l3",
41752
+ BROKER_OPS.SECRET_ADDED,
41753
+ this.principalIdentityId,
41754
+ { secret: name }
41755
+ );
41756
+ });
41028
41757
  }
41029
41758
  async rotateSecret(name, newValue) {
41030
- await this.backend.rotateSecret(name, newValue);
41031
- this.auditLog.append(
41032
- "l3",
41033
- BROKER_OPS.SECRET_ROTATED,
41034
- this.principalIdentityId,
41035
- { secret: name }
41036
- );
41759
+ await this.withNameLock(name, async () => {
41760
+ await this.backend.rotateSecret(name, newValue);
41761
+ this.auditLog.append(
41762
+ "l3",
41763
+ BROKER_OPS.SECRET_ROTATED,
41764
+ this.principalIdentityId,
41765
+ { secret: name }
41766
+ );
41767
+ });
41037
41768
  }
41038
41769
  async deleteSecret(name) {
41039
- await this.backend.deleteSecret(name);
41040
- this.auditLog.append(
41041
- "l3",
41042
- BROKER_OPS.SECRET_DELETED,
41043
- this.principalIdentityId,
41044
- { secret: name }
41045
- );
41770
+ await this.withNameLock(name, async () => {
41771
+ await this.backend.deleteSecret(name);
41772
+ this.auditLog.append(
41773
+ "l3",
41774
+ BROKER_OPS.SECRET_DELETED,
41775
+ this.principalIdentityId,
41776
+ { secret: name }
41777
+ );
41778
+ });
41046
41779
  }
41047
41780
  async listSecretNames() {
41048
41781
  return this.backend.listSecretNames();
@@ -41082,6 +41815,19 @@ var init_broker = __esm({
41082
41815
  liveTokenCount() {
41083
41816
  return this.issuer.liveTokenCount();
41084
41817
  }
41818
+ /**
41819
+ * Drop expired tokens from the in-memory issuer map. Hardening wave 6
41820
+ * finding #86: previously expiry pruning depended on opportunistic
41821
+ * `pruneExpired()` calls; now the cocoon-unlock initialization path
41822
+ * (openBroker -> after backend.ensureInitialized -> after Broker
41823
+ * construction) fires this once so each cocoon-unlock cycle drops
41824
+ * stale bindings before any operator interaction.
41825
+ *
41826
+ * Returns the number of tokens removed. Safe to call repeatedly; idempotent.
41827
+ */
41828
+ pruneExpiredTokens() {
41829
+ return this.issuer.pruneExpired();
41830
+ }
41085
41831
  /**
41086
41832
  * Audit query restricted to broker-scoped operations. Returns entries
41087
41833
  * with their timestamps, op, and result (never the secret value).
@@ -41240,6 +41986,7 @@ async function openBroker(opts = {}) {
41240
41986
  grants,
41241
41987
  principalIdentityId: opts.principalIdentityId ?? "sanctuary-broker"
41242
41988
  });
41989
+ broker.pruneExpiredTokens();
41243
41990
  return {
41244
41991
  broker,
41245
41992
  close: async () => {
@@ -42108,8 +42855,6 @@ var init_health = __esm({
42108
42855
  DEFAULT_TIMEOUT_MS4 = 500;
42109
42856
  }
42110
42857
  });
42111
-
42112
- // src/cli/agents/cli.ts
42113
42858
  function resolveCtx(args) {
42114
42859
  const env = args.env ?? process.env;
42115
42860
  const discoverOpts = {
@@ -42147,6 +42892,8 @@ async function runAgentsCommand(args) {
42147
42892
  return await cmdShow2(rest, ctx);
42148
42893
  case "status":
42149
42894
  return await cmdStatus(rest, ctx);
42895
+ case "config":
42896
+ return await cmdConfig(rest, ctx);
42150
42897
  default:
42151
42898
  ctx.err.write(`Unknown subcommand: ${sub}
42152
42899
  `);
@@ -42162,10 +42909,18 @@ async function runAgentsCommand(args) {
42162
42909
  }
42163
42910
  function printUsage4(s) {
42164
42911
  s.write(`Usage: sanctuary agents <command> [flags]
42912
+ sanctuary agent <command> [flags] (alias)
42165
42913
 
42166
42914
  list [--json] List every tenant visible on this host.
42167
- show <tenant> [--json] Show details for one tenant.
42915
+ show <tenant> [--json] Show details for one tenant (includes
42916
+ approval-redirect state).
42168
42917
  status [--json] One-line-per-tenant running/stopped summary.
42918
+ config <tenant> [opts] Write tenant principal-policy.yaml fields.
42919
+ --approval-redirect=<bool> Toggle cross-harness inbox redirect.
42920
+ --approval-redirect-mode=<replace|notify>
42921
+ Pick replace (bypass underlying channel)
42922
+ or notify (race both paths). Default
42923
+ replace when toggled on.
42169
42924
 
42170
42925
  Options:
42171
42926
  --fortress <path> Scope discovery to a specific storage path
@@ -42267,6 +43022,7 @@ async function cmdShow2(argv, ctx) {
42267
43022
  return 1;
42268
43023
  }
42269
43024
  const probe = await ctx.probe(tenant);
43025
+ const approvalRedirect = await readApprovalRedirectState(tenant);
42270
43026
  const payload = {
42271
43027
  name: tenant.name,
42272
43028
  storage_path: tenant.storage_path,
@@ -42280,7 +43036,8 @@ async function cmdShow2(argv, ctx) {
42280
43036
  running: probe.running,
42281
43037
  status: probe.status,
42282
43038
  reason: probe.reason
42283
- }
43039
+ },
43040
+ approval_redirect: approvalRedirect
42284
43041
  };
42285
43042
  if (hasJsonFlag(argv)) {
42286
43043
  ctx.out.write(JSON.stringify(payload, null, 2) + "\n");
@@ -42324,10 +43081,173 @@ async function cmdShow2(argv, ctx) {
42324
43081
  }
42325
43082
  ctx.out.write(
42326
43083
  `probe: ${probe.running ? "running" : "not-running"}${probe.reason ? ` (${probe.reason})` : ""}
43084
+ `
43085
+ );
43086
+ ctx.out.write(
43087
+ `approval_redirect: ${approvalRedirect.enabled ? `on (${approvalRedirect.mode})` : "off"}
42327
43088
  `
42328
43089
  );
42329
43090
  return 0;
42330
43091
  }
43092
+ async function readApprovalRedirectState(tenant) {
43093
+ const policyPath = join(tenant.storage_path, "principal-policy.yaml");
43094
+ try {
43095
+ const content = await readFile(policyPath, "utf-8");
43096
+ const parsed = parsePolicy(content);
43097
+ const cfg = parsed.approval_redirect;
43098
+ if (!cfg) return { enabled: false, mode: "replace" };
43099
+ return {
43100
+ enabled: !!cfg.enabled,
43101
+ mode: cfg.mode === "notify" ? "notify" : "replace"
43102
+ };
43103
+ } catch {
43104
+ return { enabled: false, mode: "replace" };
43105
+ }
43106
+ }
43107
+ function parseBoolFlag(raw) {
43108
+ if (raw === void 0) return null;
43109
+ const v = raw.toLowerCase();
43110
+ if (v === "true" || v === "yes" || v === "on" || v === "1") return true;
43111
+ if (v === "false" || v === "no" || v === "off" || v === "0") return false;
43112
+ return null;
43113
+ }
43114
+ function findFlagValue(argv, name) {
43115
+ for (let i = 0; i < argv.length; i++) {
43116
+ const a = argv[i];
43117
+ if (a === name) {
43118
+ return argv[i + 1];
43119
+ }
43120
+ const eq = `${name}=`;
43121
+ if (a.startsWith(eq)) {
43122
+ return a.slice(eq.length);
43123
+ }
43124
+ }
43125
+ return void 0;
43126
+ }
43127
+ async function cmdConfig(argv, ctx) {
43128
+ const positional = argv.find((a) => !a.startsWith("--"));
43129
+ if (!positional) {
43130
+ ctx.err.write(
43131
+ "Missing tenant. Usage: sanctuary agents config <tenant> --approval-redirect=<bool>\n"
43132
+ );
43133
+ return 2;
43134
+ }
43135
+ const tenant = await findTenant(positional, ctx.discoverOpts);
43136
+ if (!tenant) {
43137
+ ctx.err.write(`sanctuary agents: unknown tenant "${positional}"
43138
+ `);
43139
+ return 1;
43140
+ }
43141
+ const redirectFlag = parseBoolFlag(
43142
+ findFlagValue(argv, "--approval-redirect")
43143
+ );
43144
+ const modeFlag = findFlagValue(argv, "--approval-redirect-mode");
43145
+ if (redirectFlag === null && modeFlag === void 0) {
43146
+ ctx.err.write(
43147
+ "sanctuary agents config: nothing to do. Pass --approval-redirect=<bool> or --approval-redirect-mode=<replace|notify>.\n"
43148
+ );
43149
+ return 2;
43150
+ }
43151
+ if (modeFlag !== void 0 && modeFlag !== "replace" && modeFlag !== "notify") {
43152
+ ctx.err.write(
43153
+ `sanctuary agents config: --approval-redirect-mode must be "replace" or "notify" (got "${modeFlag}")
43154
+ `
43155
+ );
43156
+ return 2;
43157
+ }
43158
+ const current = await readApprovalRedirectState(tenant);
43159
+ const next = {
43160
+ enabled: redirectFlag !== null ? redirectFlag : current.enabled,
43161
+ mode: modeFlag === "notify" || modeFlag === "replace" ? modeFlag : current.mode
43162
+ };
43163
+ await writeApprovalRedirectToPolicyFile(tenant.storage_path, next);
43164
+ if (hasJsonFlag(argv)) {
43165
+ ctx.out.write(
43166
+ JSON.stringify(
43167
+ {
43168
+ tenant: tenant.name,
43169
+ approval_redirect: next
43170
+ },
43171
+ null,
43172
+ 2
43173
+ ) + "\n"
43174
+ );
43175
+ } else {
43176
+ ctx.out.write(
43177
+ `sanctuary agents config: tenant "${tenant.name}" approval_redirect=${next.enabled ? `on (${next.mode})` : "off"}
43178
+ `
43179
+ );
43180
+ ctx.out.write(
43181
+ ` Takes effect on the next gate request for the running server.
43182
+ `
43183
+ );
43184
+ }
43185
+ return 0;
43186
+ }
43187
+ async function writeApprovalRedirectToPolicyFile(storagePath, state) {
43188
+ const policyPath = join(storagePath, "principal-policy.yaml");
43189
+ let content;
43190
+ try {
43191
+ content = await readFile(policyPath, "utf-8");
43192
+ } catch (err) {
43193
+ const code = err?.code;
43194
+ if (code !== "ENOENT") throw err;
43195
+ content = await defaultPolicyTextForBootstrap();
43196
+ }
43197
+ const block = renderApprovalRedirectBlock(state);
43198
+ const updated = upsertApprovalRedirectBlock(content, block);
43199
+ await writeFile(policyPath, updated, "utf-8");
43200
+ await chmod(policyPath, 384);
43201
+ }
43202
+ function renderApprovalRedirectBlock(state) {
43203
+ return [
43204
+ "# Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2)",
43205
+ "approval_redirect:",
43206
+ ` enabled: ${state.enabled ? "true" : "false"}`,
43207
+ ` mode: ${state.mode}`
43208
+ ].join("\n");
43209
+ }
43210
+ function upsertApprovalRedirectBlock(content, block) {
43211
+ const lines = content.split("\n");
43212
+ const startIdx = lines.findIndex((l) => l.startsWith("approval_redirect:"));
43213
+ if (startIdx === -1) {
43214
+ const trimmed = content.endsWith("\n") ? content : content + "\n";
43215
+ return trimmed + "\n" + block + "\n";
43216
+ }
43217
+ let blockStart = startIdx;
43218
+ if (blockStart > 0 && lines[blockStart - 1] !== void 0 && lines[blockStart - 1].startsWith("# Approval Redirect")) {
43219
+ blockStart = blockStart - 1;
43220
+ }
43221
+ let blockEnd = startIdx + 1;
43222
+ while (blockEnd < lines.length) {
43223
+ const l = lines[blockEnd];
43224
+ if (l === "") {
43225
+ blockEnd++;
43226
+ continue;
43227
+ }
43228
+ if (/^[A-Za-z0-9#]/.test(l)) {
43229
+ break;
43230
+ }
43231
+ blockEnd++;
43232
+ }
43233
+ const before = lines.slice(0, blockStart);
43234
+ const after = lines.slice(blockEnd);
43235
+ const replaced = [...before, ...block.split("\n"), ...after].join("\n");
43236
+ return replaced.endsWith("\n") ? replaced : replaced + "\n";
43237
+ }
43238
+ async function defaultPolicyTextForBootstrap() {
43239
+ return [
43240
+ "version: 1",
43241
+ "tier1_always_approve:",
43242
+ " - state_export",
43243
+ " - state_import",
43244
+ " - state_delete",
43245
+ "approval_channel:",
43246
+ " type: stderr",
43247
+ " timeout_seconds: 300",
43248
+ ""
43249
+ ].join("\n");
43250
+ }
42331
43251
  async function cmdStatus(argv, ctx) {
42332
43252
  const tenants = await discoverTenants(ctx.discoverOpts);
42333
43253
  const probes = await Promise.all(tenants.map((t) => ctx.probe(t)));
@@ -42368,6 +43288,7 @@ var init_cli5 = __esm({
42368
43288
  "src/cli/agents/cli.ts"() {
42369
43289
  init_discovery();
42370
43290
  init_health();
43291
+ init_loader();
42371
43292
  }
42372
43293
  });
42373
43294
 
@@ -43796,7 +44717,7 @@ async function main() {
43796
44717
  const code = await runIdentityCommand2({ argv: args.slice(1) });
43797
44718
  process.exit(code);
43798
44719
  }
43799
- if (args[0] === "agents") {
44720
+ if (args[0] === "agents" || args[0] === "agent") {
43800
44721
  const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
43801
44722
  const code = await runAgentsCommand2({ argv: args.slice(1) });
43802
44723
  process.exit(code);