@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.cjs CHANGED
@@ -4457,9 +4457,35 @@ function validatePolicy(raw) {
4457
4457
  };
4458
4458
  delete merged.auto_deny;
4459
4459
  return merged;
4460
- })()
4460
+ })(),
4461
+ approval_redirect: parseApprovalRedirect(raw.approval_redirect)
4461
4462
  };
4462
4463
  }
4464
+ function parseApprovalRedirect(raw) {
4465
+ if (raw === void 0 || raw === null) {
4466
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4467
+ }
4468
+ if (typeof raw !== "object") {
4469
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4470
+ }
4471
+ const obj = raw;
4472
+ const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
4473
+ const modeRaw = obj.mode;
4474
+ let mode = DEFAULT_APPROVAL_REDIRECT.mode;
4475
+ if (modeRaw !== void 0) {
4476
+ if (modeRaw !== "replace" && modeRaw !== "notify") {
4477
+ throw new Error(
4478
+ `approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
4479
+ );
4480
+ }
4481
+ mode = modeRaw;
4482
+ }
4483
+ const result = { enabled, mode };
4484
+ if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
4485
+ result.per_agent = obj.per_agent;
4486
+ }
4487
+ return result;
4488
+ }
4463
4489
  function generateDefaultPolicyYaml() {
4464
4490
  return `# Sanctuary Principal Policy v1
4465
4491
  # This file controls what your agent can do without asking.
@@ -4538,6 +4564,7 @@ tier3_always_allow:
4538
4564
  - handshake_status
4539
4565
  - handshake_exchange
4540
4566
  - handshake_verify_attestation
4567
+ - handshake_abort
4541
4568
  - reputation_query_weighted
4542
4569
  - federation_peers
4543
4570
  - federation_trust_evaluate
@@ -4572,6 +4599,21 @@ tier3_always_allow:
4572
4599
  approval_channel:
4573
4600
  type: stderr
4574
4601
  timeout_seconds: 300
4602
+
4603
+ # \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
4604
+ # Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
4605
+ # resolve via the unified approval inbox at /api/approval-inbox/* instead
4606
+ # of (or in addition to) the configured approval_channel above.
4607
+ #
4608
+ # mode:
4609
+ # replace: bypass the approval_channel entirely; the gate awaits a
4610
+ # decision from the inbox (default once enabled).
4611
+ # notify: fire BOTH the approval_channel and the inbox; first decision
4612
+ # wins. Right shape for harnesses that cannot fully suppress
4613
+ # their local approval prompt (e.g. Mastra-class).
4614
+ approval_redirect:
4615
+ enabled: false
4616
+ mode: replace
4575
4617
  `;
4576
4618
  }
4577
4619
  async function loadPrincipalPolicy(storagePath) {
@@ -4608,7 +4650,7 @@ async function loadPrincipalPolicy(storagePath) {
4608
4650
  );
4609
4651
  }
4610
4652
  }
4611
- var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4653
+ var DEFAULT_TIER2, DEFAULT_CHANNEL, DEFAULT_APPROVAL_REDIRECT, DEFAULT_POLICY, MalformedPrincipalPolicyError;
4612
4654
  var init_loader = __esm({
4613
4655
  "src/principal-policy/loader.ts"() {
4614
4656
  DEFAULT_TIER2 = {
@@ -4625,6 +4667,10 @@ var init_loader = __esm({
4625
4667
  // SEC-002: auto_deny is not configurable. Timeout always denies.
4626
4668
  // Field omitted intentionally — all channels hardcode deny on timeout.
4627
4669
  };
4670
+ DEFAULT_APPROVAL_REDIRECT = {
4671
+ enabled: false,
4672
+ mode: "replace"
4673
+ };
4628
4674
  DEFAULT_POLICY = {
4629
4675
  version: 1,
4630
4676
  tier1_always_approve: [
@@ -4698,6 +4744,7 @@ var init_loader = __esm({
4698
4744
  "handshake_status",
4699
4745
  "handshake_exchange",
4700
4746
  "handshake_verify_attestation",
4747
+ "handshake_abort",
4701
4748
  "reputation_query_weighted",
4702
4749
  "federation_peers",
4703
4750
  "federation_trust_evaluate",
@@ -4739,7 +4786,8 @@ var init_loader = __esm({
4739
4786
  "compliance_eu_ai_act_annex_iii_classify"
4740
4787
  // Read-only; rule-based Annex III classifier
4741
4788
  ],
4742
- approval_channel: DEFAULT_CHANNEL
4789
+ approval_channel: DEFAULT_CHANNEL,
4790
+ approval_redirect: DEFAULT_APPROVAL_REDIRECT
4743
4791
  };
4744
4792
  MalformedPrincipalPolicyError = class extends Error {
4745
4793
  constructor(policyPath, reason) {
@@ -20570,6 +20618,174 @@ var init_approval_aggregator = __esm({
20570
20618
  }
20571
20619
  });
20572
20620
 
20621
+ // src/principal-policy/channels/aggregator-backed-channel.ts
20622
+ function auditEntryIdFor(request) {
20623
+ return `${request.timestamp}:${request.operation}`;
20624
+ }
20625
+ function statusToDecision(entry) {
20626
+ switch (entry.status) {
20627
+ case "approved":
20628
+ return {
20629
+ decision: "approve",
20630
+ decided_by: "human"
20631
+ };
20632
+ case "denied":
20633
+ return {
20634
+ decision: "deny",
20635
+ decided_by: "human"
20636
+ };
20637
+ case "timeout":
20638
+ case "expired":
20639
+ return {
20640
+ decision: "deny",
20641
+ decided_by: "timeout"
20642
+ };
20643
+ default:
20644
+ return null;
20645
+ }
20646
+ }
20647
+ function makeRedirectResolverFromPolicySupplier(supplier) {
20648
+ return (_request) => {
20649
+ const cfg = supplier().approval_redirect;
20650
+ if (!cfg || cfg.enabled !== true) {
20651
+ return { enabled: false, mode: "replace" };
20652
+ }
20653
+ return {
20654
+ enabled: true,
20655
+ mode: cfg.mode === "notify" ? "notify" : "replace"
20656
+ };
20657
+ };
20658
+ }
20659
+ var DEFAULT_REPLACE_MODE_TIMEOUT_MS, AggregatorBackedChannel;
20660
+ var init_aggregator_backed_channel = __esm({
20661
+ "src/principal-policy/channels/aggregator-backed-channel.ts"() {
20662
+ DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
20663
+ AggregatorBackedChannel = class {
20664
+ underlying;
20665
+ aggregator;
20666
+ resolveRedirect;
20667
+ replaceModeTimeoutMs;
20668
+ now;
20669
+ constructor(opts) {
20670
+ this.underlying = opts.underlying;
20671
+ this.aggregator = opts.aggregator;
20672
+ this.resolveRedirect = opts.resolveRedirect;
20673
+ this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
20674
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
20675
+ }
20676
+ /** Expose underlying for tests / wire-up reuse. */
20677
+ getUnderlying() {
20678
+ return this.underlying;
20679
+ }
20680
+ async requestApproval(request) {
20681
+ const cfg = this.resolveRedirect(request);
20682
+ if (!cfg.enabled) {
20683
+ return this.underlying.requestApproval(request);
20684
+ }
20685
+ if (cfg.mode === "replace") {
20686
+ return this.awaitAggregatorDecision(request);
20687
+ }
20688
+ return this.notifyMode(request);
20689
+ }
20690
+ /**
20691
+ * `replace` mode. Subscribe to the aggregator's event stream BEFORE
20692
+ * checking already-stored entries (avoids a race where the entry resolves
20693
+ * between list and subscribe). Match incoming events to this request by
20694
+ * audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
20695
+ */
20696
+ async awaitAggregatorDecision(request) {
20697
+ const auditId = auditEntryIdFor(request);
20698
+ return new Promise((resolveOuter) => {
20699
+ let settled = false;
20700
+ let unsubscribe = null;
20701
+ let timeoutHandle = null;
20702
+ const settle = (response) => {
20703
+ if (settled) return;
20704
+ settled = true;
20705
+ if (timeoutHandle) clearTimeout(timeoutHandle);
20706
+ if (unsubscribe) {
20707
+ try {
20708
+ unsubscribe();
20709
+ } catch {
20710
+ }
20711
+ }
20712
+ resolveOuter(response);
20713
+ };
20714
+ const onEvent = (emit) => {
20715
+ if (emit.type !== "resolved") return;
20716
+ if (emit.entry.audit_log_entry_id !== auditId) return;
20717
+ const mapped = statusToDecision(emit.entry);
20718
+ if (!mapped) return;
20719
+ settle({
20720
+ decision: mapped.decision,
20721
+ decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
20722
+ decided_by: mapped.decided_by
20723
+ });
20724
+ };
20725
+ try {
20726
+ unsubscribe = this.aggregator.onEvent(onEvent);
20727
+ } catch (err) {
20728
+ settle({
20729
+ decision: "deny",
20730
+ decided_at: this.now().toISOString(),
20731
+ decided_by: "channel_failure"
20732
+ });
20733
+ throw err instanceof Error ? err : new Error(String(err));
20734
+ }
20735
+ void this.aggregator.list({ limit: 200 }).then((entries) => {
20736
+ for (const entry of entries) {
20737
+ if (entry.audit_log_entry_id !== auditId) continue;
20738
+ const mapped = statusToDecision(entry);
20739
+ if (!mapped) return;
20740
+ settle({
20741
+ decision: mapped.decision,
20742
+ decided_at: entry.resolved_at ?? this.now().toISOString(),
20743
+ decided_by: mapped.decided_by
20744
+ });
20745
+ return;
20746
+ }
20747
+ }).catch(() => {
20748
+ });
20749
+ timeoutHandle = setTimeout(() => {
20750
+ settle({
20751
+ decision: "deny",
20752
+ decided_at: this.now().toISOString(),
20753
+ decided_by: "timeout"
20754
+ });
20755
+ }, this.replaceModeTimeoutMs);
20756
+ });
20757
+ }
20758
+ /**
20759
+ * `notify` mode. Fire the underlying channel and listen on the
20760
+ * aggregator simultaneously; whichever resolves first wins. Both
20761
+ * paths produce identical `ApprovalResponse` shapes; the gate's
20762
+ * downstream audit logging is unchanged.
20763
+ *
20764
+ * On underlying-channel failure, fall through to the aggregator wait
20765
+ * (still bounded by `replaceModeTimeoutMs`). Operator can still
20766
+ * resolve from the inbox even if the dashboard/webhook is down.
20767
+ */
20768
+ async notifyMode(request) {
20769
+ const aggregatorPromise = this.awaitAggregatorDecision(request);
20770
+ let underlyingPromise;
20771
+ try {
20772
+ underlyingPromise = this.underlying.requestApproval(request);
20773
+ } catch (err) {
20774
+ const response = await aggregatorPromise;
20775
+ return response;
20776
+ }
20777
+ return Promise.race([
20778
+ aggregatorPromise,
20779
+ underlyingPromise.catch(
20780
+ () => new Promise(() => {
20781
+ })
20782
+ )
20783
+ ]);
20784
+ }
20785
+ };
20786
+ }
20787
+ });
20788
+
20573
20789
  // src/principal-policy/tools.ts
20574
20790
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
20575
20791
  return [
@@ -21473,6 +21689,76 @@ var init_attestation = __esm({
21473
21689
  }
21474
21690
  });
21475
21691
 
21692
+ // src/handshake/audit.ts
21693
+ function auditHandshakeInitiated(auditLog, ctx) {
21694
+ auditLog.append(
21695
+ "l4",
21696
+ HANDSHAKE_LIFECYCLE_OPS.INITIATED,
21697
+ ctx.identity_id,
21698
+ detailsFromContext(ctx),
21699
+ "success"
21700
+ );
21701
+ }
21702
+ function auditHandshakeCompleted(auditLog, ctx) {
21703
+ const details = detailsFromContext(ctx);
21704
+ if (ctx.trust_tier !== void 0) {
21705
+ details.trust_tier = ctx.trust_tier;
21706
+ }
21707
+ auditLog.append(
21708
+ "l4",
21709
+ HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
21710
+ ctx.identity_id,
21711
+ details,
21712
+ "success"
21713
+ );
21714
+ }
21715
+ function auditHandshakeFailed(auditLog, ctx) {
21716
+ const details = detailsFromContext(ctx);
21717
+ details.reason = ctx.reason;
21718
+ if (ctx.error !== void 0) {
21719
+ details.error = ctx.error;
21720
+ }
21721
+ auditLog.append(
21722
+ "l4",
21723
+ HANDSHAKE_LIFECYCLE_OPS.FAILED,
21724
+ ctx.identity_id,
21725
+ details,
21726
+ "failure"
21727
+ );
21728
+ }
21729
+ function auditHandshakeAborted(auditLog, ctx) {
21730
+ const details = detailsFromContext(ctx);
21731
+ details.reason = ctx.reason;
21732
+ auditLog.append(
21733
+ "l4",
21734
+ HANDSHAKE_LIFECYCLE_OPS.ABORTED,
21735
+ ctx.identity_id,
21736
+ details,
21737
+ "failure"
21738
+ );
21739
+ }
21740
+ function detailsFromContext(ctx) {
21741
+ const details = {
21742
+ session_id: ctx.session_id,
21743
+ role: ctx.role
21744
+ };
21745
+ if (ctx.counterparty_id !== void 0) {
21746
+ details.counterparty_id = ctx.counterparty_id;
21747
+ }
21748
+ return details;
21749
+ }
21750
+ var HANDSHAKE_LIFECYCLE_OPS;
21751
+ var init_audit = __esm({
21752
+ "src/handshake/audit.ts"() {
21753
+ HANDSHAKE_LIFECYCLE_OPS = {
21754
+ INITIATED: "handshake_initiated",
21755
+ COMPLETED: "handshake_completed",
21756
+ FAILED: "handshake_failed",
21757
+ ABORTED: "handshake_aborted"
21758
+ };
21759
+ }
21760
+ });
21761
+
21476
21762
  // src/handshake/tools.ts
21477
21763
  function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
21478
21764
  const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
@@ -21506,6 +21792,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21506
21792
  const { challenge, session } = initiateHandshake(shr);
21507
21793
  sessions.set(session.session_id, session);
21508
21794
  auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
21795
+ auditHandshakeInitiated(auditLog, {
21796
+ session_id: session.session_id,
21797
+ role: "initiator",
21798
+ identity_id: shr.body.instance_id
21799
+ });
21509
21800
  return toolResult({
21510
21801
  session_id: session.session_id,
21511
21802
  challenge,
@@ -21545,10 +21836,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21545
21836
  );
21546
21837
  if ("error" in result) {
21547
21838
  auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
21839
+ auditHandshakeFailed(auditLog, {
21840
+ session_id: "unknown",
21841
+ role: "responder",
21842
+ identity_id: shr.body.instance_id,
21843
+ reason: classifyRespondFailure(result.error),
21844
+ error: result.error
21845
+ });
21548
21846
  return toolResult({ error: result.error });
21549
21847
  }
21550
21848
  sessions.set(result.session.session_id, result.session);
21551
21849
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
21850
+ auditHandshakeInitiated(auditLog, {
21851
+ session_id: result.session.session_id,
21852
+ role: "responder",
21853
+ identity_id: shr.body.instance_id,
21854
+ counterparty_id: challenge.shr.body.instance_id
21855
+ });
21552
21856
  let autoPublishResult;
21553
21857
  if (autoPublishHandshakes) {
21554
21858
  autoPublishResult = { attempted: true };
@@ -21656,9 +21960,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21656
21960
  const response = args.response;
21657
21961
  const session = sessions.get(sessionId);
21658
21962
  if (!session) {
21963
+ auditHandshakeFailed(auditLog, {
21964
+ session_id: sessionId,
21965
+ role: "initiator",
21966
+ identity_id: "unknown",
21967
+ reason: "session_unknown",
21968
+ error: `No handshake session found: ${sessionId}`
21969
+ });
21659
21970
  return toolResult({ error: `No handshake session found: ${sessionId}` });
21660
21971
  }
21661
21972
  if (session.state !== "initiated") {
21973
+ auditHandshakeFailed(auditLog, {
21974
+ session_id: sessionId,
21975
+ role: "initiator",
21976
+ identity_id: session.our_shr.body.instance_id,
21977
+ reason: "session_state_mismatch",
21978
+ error: `Session is in state '${session.state}', expected 'initiated'`
21979
+ });
21662
21980
  return toolResult({
21663
21981
  error: `Session is in state '${session.state}', expected 'initiated'`
21664
21982
  });
@@ -21672,6 +21990,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21672
21990
  if ("error" in result) {
21673
21991
  session.state = "failed";
21674
21992
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
21993
+ auditHandshakeFailed(auditLog, {
21994
+ session_id: sessionId,
21995
+ role: "initiator",
21996
+ identity_id: session.our_shr.body.instance_id,
21997
+ reason: classifyCompleteFailure(result.error),
21998
+ error: result.error
21999
+ });
21675
22000
  return toolResult({ error: result.error });
21676
22001
  }
21677
22002
  session.state = "completed";
@@ -21680,6 +22005,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21680
22005
  session.result = result.result;
21681
22006
  handshakeResults.set(result.result.counterparty_id, result.result);
21682
22007
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
22008
+ auditHandshakeCompleted(auditLog, {
22009
+ session_id: sessionId,
22010
+ role: "initiator",
22011
+ identity_id: session.our_shr.body.instance_id,
22012
+ counterparty_id: result.result.counterparty_id,
22013
+ trust_tier: result.result.trust_tier
22014
+ });
21683
22015
  return toolResult({
21684
22016
  completion: result.completion,
21685
22017
  result: result.result,
@@ -21727,6 +22059,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21727
22059
  void 0,
21728
22060
  result.verified ? "success" : "failure"
21729
22061
  );
22062
+ if (result.verified) {
22063
+ auditHandshakeCompleted(auditLog, {
22064
+ session_id: session.session_id,
22065
+ role: "responder",
22066
+ identity_id: session.our_shr.body.instance_id,
22067
+ counterparty_id: result.counterparty_id,
22068
+ trust_tier: result.trust_tier
22069
+ });
22070
+ } else {
22071
+ auditHandshakeFailed(auditLog, {
22072
+ session_id: session.session_id,
22073
+ role: "responder",
22074
+ identity_id: session.our_shr.body.instance_id,
22075
+ counterparty_id: result.counterparty_id,
22076
+ reason: classifyCompleteFailure(result.errors.join("; ")),
22077
+ error: result.errors.join("; ")
22078
+ });
22079
+ }
21730
22080
  return toolResult({ result });
21731
22081
  }
21732
22082
  return toolResult({
@@ -21834,10 +22184,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
21834
22184
  _content_trust: "external"
21835
22185
  });
21836
22186
  }
22187
+ },
22188
+ {
22189
+ name: "handshake_abort",
22190
+ 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.",
22191
+ inputSchema: {
22192
+ type: "object",
22193
+ properties: {
22194
+ session_id: {
22195
+ type: "string",
22196
+ description: "Session ID returned from handshake_initiate / handshake_respond."
22197
+ },
22198
+ reason: {
22199
+ type: "string",
22200
+ enum: [
22201
+ "operator_cancelled",
22202
+ "session_timeout",
22203
+ "transport_dropped",
22204
+ "shutdown",
22205
+ "other"
22206
+ ],
22207
+ description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
22208
+ }
22209
+ },
22210
+ required: ["session_id"]
22211
+ },
22212
+ handler: async (args) => {
22213
+ const sessionId = args.session_id;
22214
+ const reason = args.reason ?? "operator_cancelled";
22215
+ const session = sessions.get(sessionId);
22216
+ if (!session) {
22217
+ return toolResult({ error: `No handshake session found: ${sessionId}` });
22218
+ }
22219
+ if (session.state === "completed") {
22220
+ return toolResult({
22221
+ error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
22222
+ });
22223
+ }
22224
+ sessions.delete(sessionId);
22225
+ auditHandshakeAborted(auditLog, {
22226
+ session_id: sessionId,
22227
+ role: session.role,
22228
+ identity_id: session.our_shr.body.instance_id,
22229
+ ...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
22230
+ reason
22231
+ });
22232
+ return toolResult({
22233
+ aborted: true,
22234
+ session_id: sessionId,
22235
+ reason
22236
+ });
22237
+ }
21837
22238
  }
21838
22239
  ];
21839
22240
  return { tools, handshakeResults };
21840
22241
  }
22242
+ function classifyRespondFailure(error) {
22243
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
22244
+ if (error.includes("SHR verification failed")) return "shr_invalid";
22245
+ if (error.includes("No identity available")) return "no_signing_identity";
22246
+ return "other";
22247
+ }
22248
+ function classifyCompleteFailure(error) {
22249
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
22250
+ if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
22251
+ if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
22252
+ if (error.includes("No identity available")) return "no_signing_identity";
22253
+ return "other";
22254
+ }
21841
22255
  var init_tools6 = __esm({
21842
22256
  "src/handshake/tools.ts"() {
21843
22257
  init_router();
@@ -21847,6 +22261,7 @@ var init_tools6 = __esm({
21847
22261
  init_encoding();
21848
22262
  init_protocol();
21849
22263
  init_attestation();
22264
+ init_audit();
21850
22265
  init_verifier();
21851
22266
  }
21852
22267
  });
@@ -32982,7 +33397,14 @@ var init_operator_chat_audit_events = __esm({
32982
33397
  * successful thread removal. Body carries thread_id + turn_count of
32983
33398
  * the deleted bundle.
32984
33399
  */
32985
- CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
33400
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
33401
+ /**
33402
+ * Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
33403
+ * the multi-turn coherence fold cannot load the active thread's prior
33404
+ * turns; the concierge degrades to single-turn after emitting. Body
33405
+ * carries thread_id + a stable failure_reason enum.
33406
+ */
33407
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
32986
33408
  };
32987
33409
  }
32988
33410
  });
@@ -32995,13 +33417,20 @@ var init_operator_chat_types = __esm({
32995
33417
  CONCIERGE_THREAD_KEY = "_fortress";
32996
33418
  }
32997
33419
  });
33420
+ function approxTokenLen(text) {
33421
+ return Math.ceil(text.length / 4);
33422
+ }
32998
33423
  function makeEventId(prefix) {
32999
33424
  return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
33000
33425
  }
33426
+ function formatPriorTurnLine(turn) {
33427
+ const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
33428
+ return `${label}: ${turn.content}`;
33429
+ }
33001
33430
  function hashOf(input) {
33002
33431
  return hashToString(sha256.sha256(stringToBytes(input)));
33003
33432
  }
33004
- var DEFAULT_CONCIERGE_MAX_TOKENS, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
33433
+ 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;
33005
33434
  var init_operator_chat_service = __esm({
33006
33435
  "src/chat/operator-chat-service.ts"() {
33007
33436
  init_hashing();
@@ -33009,6 +33438,10 @@ var init_operator_chat_service = __esm({
33009
33438
  init_operator_chat_audit_events();
33010
33439
  init_operator_chat_types();
33011
33440
  DEFAULT_CONCIERGE_MAX_TOKENS = 512;
33441
+ DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
33442
+ DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
33443
+ DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
33444
+ DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
33012
33445
  SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
33013
33446
  1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
33014
33447
  2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
@@ -33045,6 +33478,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33045
33478
  piiFilter;
33046
33479
  conciergeMaxTokens;
33047
33480
  memory;
33481
+ historyWindowTurns;
33482
+ historyFreshnessMs;
33483
+ historyTokenBudget;
33484
+ sessionTtlMs;
33485
+ clock;
33048
33486
  /**
33049
33487
  * In-memory thread_id assigned to the active concierge session.
33050
33488
  * The first sendConcierge call after construction allocates a fresh
@@ -33052,6 +33490,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33052
33490
  * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
33053
33491
  */
33054
33492
  activeMemoryThreadId;
33493
+ /**
33494
+ * Wall-clock ms of the most recent sendConcierge that touched the
33495
+ * active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
33496
+ * check: a fresh sendConcierge after `sessionTtlMs` of quiet
33497
+ * allocates a new thread_id even though the prior one is still
33498
+ * readable from the memory store.
33499
+ */
33500
+ lastInteractionAt;
33055
33501
  constructor(deps) {
33056
33502
  this.store = deps.store;
33057
33503
  this.auditLog = deps.auditLog;
@@ -33063,6 +33509,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33063
33509
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
33064
33510
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
33065
33511
  if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
33512
+ this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
33513
+ this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
33514
+ this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
33515
+ this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
33516
+ this.clock = deps.conciergeClock ?? (() => Date.now());
33066
33517
  }
33067
33518
  // ── Concierge ─────────────────────────────────────────────────────────
33068
33519
  /**
@@ -33081,6 +33532,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33081
33532
  throw new Error("concierge query must not be empty");
33082
33533
  }
33083
33534
  const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
33535
+ const nowMs = this.clock();
33536
+ if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
33537
+ this.activeMemoryThreadId = void 0;
33538
+ }
33084
33539
  const operatorMessage = {
33085
33540
  message_id: crypto.randomUUID(),
33086
33541
  surface: "concierge",
@@ -33093,6 +33548,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33093
33548
  CONCIERGE_THREAD_KEY,
33094
33549
  operatorMessage
33095
33550
  );
33551
+ let priorTurns = [];
33552
+ let memoryReadFailureReason = null;
33553
+ let activeThreadIdForRound;
33554
+ if (this.memory) {
33555
+ activeThreadIdForRound = this.ensureActiveMemoryThread();
33556
+ const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
33557
+ if (result.ok) {
33558
+ const cutoff = nowMs - this.historyFreshnessMs;
33559
+ const fresh = result.turns.filter((t) => {
33560
+ const ts = Date.parse(t.created_at);
33561
+ return Number.isFinite(ts) && ts >= cutoff;
33562
+ });
33563
+ const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
33564
+ priorTurns = recent;
33565
+ } else {
33566
+ memoryReadFailureReason = result.reason;
33567
+ this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
33568
+ }
33569
+ }
33096
33570
  if (this.memory) {
33097
33571
  const threadId = this.ensureActiveMemoryThread();
33098
33572
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
@@ -33114,7 +33588,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33114
33588
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
33115
33589
  outcome = "substrate_disabled";
33116
33590
  } else {
33117
- const context = await this.assembleConciergeContext();
33591
+ const context = await this.assembleConciergeContext(priorTurns);
33118
33592
  const response = await this.substrateSelector.invokeSummarize(
33119
33593
  "concierge",
33120
33594
  {
@@ -33153,10 +33627,14 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33153
33627
  CONCIERGE_THREAD_KEY,
33154
33628
  responseMessage
33155
33629
  );
33630
+ let assistantTurnId;
33156
33631
  if (this.memory) {
33157
33632
  const threadId = this.ensureActiveMemoryThread();
33158
- await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
33159
- });
33633
+ const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
33634
+ if (persisted) assistantTurnId = persisted.turn_id;
33635
+ }
33636
+ if (this.memory && activeThreadIdForRound) {
33637
+ this.lastInteractionAt = nowMs;
33160
33638
  }
33161
33639
  const payload = {
33162
33640
  version: "1.2",
@@ -33169,7 +33647,12 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33169
33647
  response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
33170
33648
  substrate: servedBy,
33171
33649
  latency_ms: latencyMs,
33172
- outcome
33650
+ outcome,
33651
+ ...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
33652
+ ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
33653
+ ...this.memory ? {
33654
+ prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
33655
+ } : {}
33173
33656
  };
33174
33657
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
33175
33658
  return {
@@ -33179,6 +33662,25 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33179
33662
  outcome
33180
33663
  };
33181
33664
  }
33665
+ /**
33666
+ * Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
33667
+ * out of `sendConcierge` so the read-fold path stays readable. Emits
33668
+ * with `result: "failure"` since the concierge fell back to
33669
+ * single-turn mode for this round-trip.
33670
+ */
33671
+ emitMemoryReadFailed(threadId, reason) {
33672
+ const payload = {
33673
+ version: "1.2",
33674
+ event_id: makeEventId("conc-memfail"),
33675
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
33676
+ identity_id: this.identityId,
33677
+ kind: "operator_concierge_memory_read_failed",
33678
+ surface: "concierge",
33679
+ thread_id: threadId,
33680
+ failure_reason: reason
33681
+ };
33682
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
33683
+ }
33182
33684
  /**
33183
33685
  * Read the persisted concierge thread, oldest message first. Returns
33184
33686
  * an empty array when no thread exists yet.
@@ -33301,6 +33803,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33301
33803
  * ## Sanctuary reference
33302
33804
  * <static domain reference block>
33303
33805
  *
33806
+ * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
33807
+ * OPERATOR: ...
33808
+ * CONCIERGE: ...
33809
+ * ---
33810
+ *
33304
33811
  * ## Recent activity
33305
33812
  * <recentActivity output>
33306
33813
  *
@@ -33310,37 +33817,69 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
33310
33817
  * ## Open inbox
33311
33818
  * <openInbox output>
33312
33819
  * ```
33313
- */
33314
- async assembleConciergeContext() {
33820
+ *
33821
+ * The substrate selector ships a `context: string` shape (not a
33822
+ * messages array), so multi-turn coherence is folded as a structured
33823
+ * prior-conversation section with explicit OPERATOR / CONCIERGE
33824
+ * boundaries. Coordinator-CTO guidance: prefer messages-array shape
33825
+ * if available; the v1.2 selector does not expose one, so structured
33826
+ * serialization is the canonical path for v1.3.
33827
+ */
33828
+ async assembleConciergeContext(priorTurns = []) {
33315
33829
  const ref = `## Sanctuary reference
33316
33830
  ${SANCTUARY_DOMAIN_REFERENCE}`;
33831
+ const priorSection = this.formatPriorTurnsSection(priorTurns);
33317
33832
  if (!this.contextProviders) {
33318
- return `${ref}
33319
-
33320
- ## Recent activity
33321
- (no providers wired)
33322
-
33323
- ## Wrapped agents
33324
- (no providers wired)
33325
-
33326
- ## Open inbox
33327
- (no providers wired)`;
33833
+ return [
33834
+ ref,
33835
+ ...priorSection ? [priorSection] : [],
33836
+ "## Recent activity\n(no providers wired)",
33837
+ "## Wrapped agents\n(no providers wired)",
33838
+ "## Open inbox\n(no providers wired)"
33839
+ ].join("\n\n");
33328
33840
  }
33329
33841
  const [activity, agents, inbox] = await Promise.all([
33330
33842
  this.contextProviders.recentActivity(),
33331
33843
  this.contextProviders.agentInventory(),
33332
33844
  this.contextProviders.openInbox()
33333
33845
  ]);
33334
- return `${ref}
33335
-
33336
- ## Recent activity
33337
- ${activity}
33338
-
33339
- ## Wrapped agents
33340
- ${agents}
33341
-
33342
- ## Open inbox
33343
- ${inbox}`;
33846
+ return [
33847
+ ref,
33848
+ ...priorSection ? [priorSection] : [],
33849
+ `## Recent activity
33850
+ ${activity}`,
33851
+ `## Wrapped agents
33852
+ ${agents}`,
33853
+ `## Open inbox
33854
+ ${inbox}`
33855
+ ].join("\n\n");
33856
+ }
33857
+ /**
33858
+ * Render the prior-conversation section with token-budget enforcement
33859
+ * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
33860
+ * section exceeds `historyTokenBudget`. Returns an empty string when
33861
+ * the input is empty or when the budget excludes every turn.
33862
+ */
33863
+ formatPriorTurnsSection(turns) {
33864
+ if (turns.length === 0) return "";
33865
+ const HEADER = "## Prior conversation";
33866
+ const lines = turns.map(formatPriorTurnLine);
33867
+ const headerTokens = approxTokenLen(`${HEADER}
33868
+ `);
33869
+ const sepTokens = approxTokenLen("\n");
33870
+ let runningTokens = headerTokens;
33871
+ let runningLines = [];
33872
+ for (let i = lines.length - 1; i >= 0; i--) {
33873
+ const line = lines[i];
33874
+ const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
33875
+ if (runningTokens + tokens > this.historyTokenBudget) break;
33876
+ runningTokens += tokens;
33877
+ runningLines.push(line);
33878
+ }
33879
+ if (runningLines.length === 0) return "";
33880
+ runningLines = runningLines.reverse();
33881
+ return `${HEADER}
33882
+ ${runningLines.join("\n")}`;
33344
33883
  }
33345
33884
  // ── audit helpers ────────────────────────────────────────────────────
33346
33885
  emit(operation, payload, result) {
@@ -33545,6 +34084,65 @@ var init_concierge_memory_store = __esm({
33545
34084
  }
33546
34085
  return turns;
33547
34086
  }
34087
+ /**
34088
+ * Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
34089
+ * `readThread` collapses every failure mode to an empty array, this
34090
+ * variant returns a discriminated result so the multi-turn fold path
34091
+ * can degrade cleanly + emit `operator_concierge_memory_read_failed`
34092
+ * with a concrete cause.
34093
+ *
34094
+ * - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
34095
+ * - Bundle present, decode + decrypt + schema check pass → ok with turns.
34096
+ * - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
34097
+ * - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
34098
+ * - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
34099
+ * - Storage IO error → `io_failed`.
34100
+ */
34101
+ async readThreadStrict(threadId, opts) {
34102
+ const key = bundleKey(threadId);
34103
+ let raw;
34104
+ try {
34105
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
34106
+ } catch {
34107
+ return { ok: false, reason: "io_failed" };
34108
+ }
34109
+ if (!raw) return { ok: true, turns: [] };
34110
+ if (raw.length > MAX_BUNDLE_BYTES2) {
34111
+ return { ok: false, reason: "oversize_bundle" };
34112
+ }
34113
+ let envelope;
34114
+ try {
34115
+ envelope = JSON.parse(bytesToString(raw));
34116
+ } catch {
34117
+ return { ok: false, reason: "schema_mismatch" };
34118
+ }
34119
+ let plaintext;
34120
+ try {
34121
+ const aad = stringToBytes(threadId);
34122
+ plaintext = decrypt(envelope, this.encryptionKey, aad);
34123
+ } catch {
34124
+ return { ok: false, reason: "decrypt_failed" };
34125
+ }
34126
+ let parsed;
34127
+ try {
34128
+ parsed = JSON.parse(bytesToString(plaintext));
34129
+ } catch {
34130
+ return { ok: false, reason: "schema_mismatch" };
34131
+ }
34132
+ if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
34133
+ if (parsed.thread_id !== threadId) {
34134
+ return { ok: false, reason: "schema_mismatch" };
34135
+ }
34136
+ let turns = parsed.turns;
34137
+ if (opts?.sinceTurnId !== void 0) {
34138
+ const cutoff = opts.sinceTurnId;
34139
+ turns = turns.filter((t) => t.turn_id > cutoff);
34140
+ }
34141
+ if (opts?.limit !== void 0) {
34142
+ turns = turns.slice(0, opts.limit);
34143
+ }
34144
+ return { ok: true, turns };
34145
+ }
33548
34146
  /**
33549
34147
  * Enumerate concierge threads in this fortress with summary metadata.
33550
34148
  * Sorted newest-first by last_turn_at.
@@ -36442,7 +37040,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
36442
37040
  }
36443
37041
  return null;
36444
37042
  }
36445
- async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
37043
+ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
36446
37044
  const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
36447
37045
  if (!destinationSigner) {
36448
37046
  return {
@@ -36504,8 +37102,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36504
37102
  }
36505
37103
  }
36506
37104
  }
37105
+ let plaintext;
36507
37106
  try {
36508
- const plaintext = decrypt(
37107
+ plaintext = decrypt(
36509
37108
  item.entry.payload,
36510
37109
  deriveNamespaceKey(sourceMasterKey, item.namespace)
36511
37110
  );
@@ -36514,28 +37113,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36514
37113
  skipped++;
36515
37114
  continue;
36516
37115
  }
36517
- await stateStore.write(
36518
- item.namespace,
36519
- item.key,
36520
- bytesToString(plaintext),
36521
- destinationSigner.identity_id,
36522
- destinationSigner.encrypted_private_key,
36523
- identityEncryptionKey,
36524
- {
36525
- content_type: item.entry.metadata.content_type,
36526
- ttl_seconds: item.entry.metadata.ttl_seconds,
36527
- tags: [
36528
- ...item.entry.metadata.tags ?? [],
36529
- "exit-import",
36530
- `source:${item.entry.kid}`
36531
- ]
36532
- }
36533
- );
36534
- imported++;
36535
37116
  } catch {
36536
37117
  skippedInvalidSig++;
36537
37118
  skipped++;
37119
+ continue;
36538
37120
  }
37121
+ await stateStore.write(
37122
+ item.namespace,
37123
+ item.key,
37124
+ bytesToString(plaintext),
37125
+ destinationSigner.identity_id,
37126
+ destinationSigner.encrypted_private_key,
37127
+ identityEncryptionKey,
37128
+ {
37129
+ content_type: item.entry.metadata.content_type,
37130
+ ttl_seconds: item.entry.metadata.ttl_seconds,
37131
+ tags: [
37132
+ ...item.entry.metadata.tags ?? [],
37133
+ "exit-import",
37134
+ `source:${item.entry.kid}`
37135
+ ]
37136
+ }
37137
+ );
37138
+ imported++;
37139
+ importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
36539
37140
  }
36540
37141
  return {
36541
37142
  status: "rekeyed",
@@ -36546,6 +37147,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
36546
37147
  conflicts
36547
37148
  };
36548
37149
  }
37150
+ async function cleanupStagedPaths(storage, staged) {
37151
+ let removed = 0;
37152
+ const failed = [];
37153
+ for (const loc of staged) {
37154
+ try {
37155
+ const ok2 = await storage.delete(loc.namespace, loc.key);
37156
+ if (ok2) {
37157
+ removed++;
37158
+ } else {
37159
+ failed.push(loc);
37160
+ }
37161
+ } catch {
37162
+ failed.push(loc);
37163
+ }
37164
+ }
37165
+ return { removed, failed };
37166
+ }
36549
37167
  async function stageArtifact(storage, namespace, key, value) {
36550
37168
  await storage.write(namespace, key, jsonBytes(value));
36551
37169
  }
@@ -36670,6 +37288,8 @@ async function importExitBundle(opts) {
36670
37288
  }
36671
37289
  const importId = importIdForManifest(manifest);
36672
37290
  const stagedArtifacts = [];
37291
+ const stagedLocations = [];
37292
+ const importedRekeyEntries = [];
36673
37293
  if (identityArtifact) {
36674
37294
  await stageArtifact(
36675
37295
  opts.storage,
@@ -36678,10 +37298,15 @@ async function importExitBundle(opts) {
36678
37298
  identityArtifact.json
36679
37299
  );
36680
37300
  stagedArtifacts.push("public_identity");
37301
+ stagedLocations.push({
37302
+ namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
37303
+ key: identityArtifact.json.bundle.identity_id
37304
+ });
36681
37305
  }
36682
37306
  if (policySet) {
36683
37307
  await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
36684
37308
  stagedArtifacts.push("policy_set");
37309
+ stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
36685
37310
  }
36686
37311
  if (auditReceipts) {
36687
37312
  await stageArtifact(
@@ -36691,10 +37316,12 @@ async function importExitBundle(opts) {
36691
37316
  auditReceipts.json
36692
37317
  );
36693
37318
  stagedArtifacts.push("audit_receipts");
37319
+ stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
36694
37320
  }
36695
37321
  if (commitments) {
36696
37322
  await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
36697
37323
  stagedArtifacts.push("commitments");
37324
+ stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
36698
37325
  }
36699
37326
  if (placeholderMetadata) {
36700
37327
  await stageArtifact(
@@ -36704,12 +37331,17 @@ async function importExitBundle(opts) {
36704
37331
  placeholderMetadata.json
36705
37332
  );
36706
37333
  stagedArtifacts.push("placeholder_vault_metadata");
37334
+ stagedLocations.push({
37335
+ namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
37336
+ key: importId
37337
+ });
36707
37338
  }
36708
37339
  await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
36709
37340
  manifest: manifest.body,
36710
37341
  verified_at: verification.verified_at,
36711
37342
  activated_at: (/* @__PURE__ */ new Date()).toISOString()
36712
37343
  });
37344
+ stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
36713
37345
  const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
36714
37346
  let reputationResult = {
36715
37347
  imported_attestations: 0,
@@ -36734,26 +37366,57 @@ async function importExitBundle(opts) {
36734
37366
  encryptedState?.json ?? null,
36735
37367
  opts
36736
37368
  );
36737
- const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
36738
- encryptedState.json,
36739
- opts,
36740
- sourceMasterKey,
36741
- publicKeys.byIdentityId
36742
- ) : {
36743
- status: "staged_requires_source_key",
36744
- imported_keys: 0,
36745
- skipped_keys: encryptedState.json.entries.length,
36746
- skipped_invalid_sig: 0,
36747
- skipped_unknown_kid: 0,
36748
- conflicts: conflicts.state_conflicts.length
36749
- } : {
36750
- status: "not_requested",
36751
- imported_keys: 0,
36752
- skipped_keys: 0,
36753
- skipped_invalid_sig: 0,
36754
- skipped_unknown_kid: 0,
36755
- conflicts: 0
36756
- };
37369
+ let stateResult;
37370
+ try {
37371
+ stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
37372
+ encryptedState.json,
37373
+ opts,
37374
+ sourceMasterKey,
37375
+ publicKeys.byIdentityId,
37376
+ importedRekeyEntries
37377
+ ) : {
37378
+ status: "staged_requires_source_key",
37379
+ imported_keys: 0,
37380
+ skipped_keys: encryptedState.json.entries.length,
37381
+ skipped_invalid_sig: 0,
37382
+ skipped_unknown_kid: 0,
37383
+ conflicts: conflicts.state_conflicts.length
37384
+ } : {
37385
+ status: "not_requested",
37386
+ imported_keys: 0,
37387
+ skipped_keys: 0,
37388
+ skipped_invalid_sig: 0,
37389
+ skipped_unknown_kid: 0,
37390
+ conflicts: 0
37391
+ };
37392
+ } catch (err) {
37393
+ const toCleanup = [
37394
+ ...importedRekeyEntries,
37395
+ ...stagedLocations
37396
+ ];
37397
+ const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
37398
+ opts.auditLog.append(
37399
+ "l1",
37400
+ "exit_bundle_rekey_failed_cleanup",
37401
+ manifest.body.identity_binding.identity_id,
37402
+ {
37403
+ import_id: importId,
37404
+ manifest_version: manifest.body.manifest_version,
37405
+ rekey_entries_removed: importedRekeyEntries.length,
37406
+ staged_artifacts_removed: stagedLocations.length,
37407
+ removed_total: cleanup.removed,
37408
+ cleanup_failed_count: cleanup.failed.length,
37409
+ original_error: err instanceof Error ? err.message : String(err)
37410
+ },
37411
+ "failure"
37412
+ );
37413
+ await opts.auditLog.flush();
37414
+ const originalMessage = err instanceof Error ? err.message : String(err);
37415
+ throw new ExitBundleImportError(
37416
+ "REKEY_FAILED_AND_CLEANED",
37417
+ `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).`
37418
+ );
37419
+ }
36757
37420
  opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
36758
37421
  import_id: importId,
36759
37422
  manifest_version: manifest.body.manifest_version,
@@ -37832,7 +38495,6 @@ ${err.message}
37832
38495
  timestamp: alert.timestamp
37833
38496
  });
37834
38497
  } : void 0;
37835
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
37836
38498
  const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
37837
38499
  const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
37838
38500
  const approvalAggregator = new ApprovalAggregator({
@@ -37842,6 +38504,20 @@ ${err.message}
37842
38504
  identityId: aggregatorIdentityId,
37843
38505
  fortressId: fortressIdForAggregator
37844
38506
  });
38507
+ const wrappedApprovalChannel = new AggregatorBackedChannel({
38508
+ underlying: approvalChannel,
38509
+ aggregator: approvalAggregator,
38510
+ resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
38511
+ replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
38512
+ });
38513
+ const gate = new ApprovalGate(
38514
+ policy,
38515
+ baseline,
38516
+ wrappedApprovalChannel,
38517
+ auditLog,
38518
+ injectionDetector,
38519
+ onInjectionAlert
38520
+ );
37845
38521
  gate.setApprovalEventCallback((event) => {
37846
38522
  void approvalAggregator.ingest(event);
37847
38523
  });
@@ -38039,6 +38715,7 @@ var init_src = __esm({
38039
38715
  init_webhook();
38040
38716
  init_gate();
38041
38717
  init_approval_aggregator();
38718
+ init_aggregator_backed_channel();
38042
38719
  init_tools4();
38043
38720
  init_router();
38044
38721
  init_router();
@@ -41003,6 +41680,22 @@ var init_broker = __esm({
41003
41680
  auditLog;
41004
41681
  issuer;
41005
41682
  principalIdentityId;
41683
+ /**
41684
+ * Per-secret-name mutex. Hardening wave 6 finding #64: two concurrent
41685
+ * addSecret() / rotateSecret() / deleteSecret() calls on the same name
41686
+ * MUST serialize cleanly. The keychain backend's `find-then-add` and
41687
+ * `find-then-delete-then-add` shapes (KeychainBackend.addSecret /
41688
+ * .rotateSecret) are not atomic against another caller racing the same
41689
+ * service-name; without serialization the second caller can observe a
41690
+ * stale "exists" check and either drop the new value or leave a
41691
+ * duplicate keychain entry.
41692
+ *
41693
+ * Implementation: an in-memory promise chain per name. Subsequent
41694
+ * callers `await` the chain tail and append their own work; failures
41695
+ * propagate to the failing caller without poisoning the chain for
41696
+ * later callers.
41697
+ */
41698
+ nameLocks = /* @__PURE__ */ new Map();
41006
41699
  constructor(opts) {
41007
41700
  this.backend = opts.backend;
41008
41701
  this.auditLog = opts.auditLog;
@@ -41013,6 +41706,40 @@ var init_broker = __esm({
41013
41706
  grants: opts.grants
41014
41707
  });
41015
41708
  }
41709
+ /**
41710
+ * Serialize `op` against any other in-flight write to the same secret
41711
+ * `name`. Per-name fairness only, distinct names run in parallel.
41712
+ * The current chain tail is used as the acceptance gate; we then
41713
+ * publish a new tail that swallows the operation's outcome so a
41714
+ * thrown error does not poison the next caller's wait.
41715
+ */
41716
+ async withNameLock(name, op) {
41717
+ const previous = this.nameLocks.get(name) ?? Promise.resolve();
41718
+ let release = () => {
41719
+ };
41720
+ const next = new Promise((resolve8) => {
41721
+ release = resolve8;
41722
+ });
41723
+ this.nameLocks.set(name, next);
41724
+ try {
41725
+ await previous.catch(() => {
41726
+ });
41727
+ return await op();
41728
+ } finally {
41729
+ release();
41730
+ if (this.nameLocks.get(name) === next) {
41731
+ this.nameLocks.delete(name);
41732
+ }
41733
+ }
41734
+ }
41735
+ /**
41736
+ * Diagnostic-only: visible for tests so they can assert that distinct
41737
+ * names do not contend on a shared lock. Not part of the public broker
41738
+ * contract; do not consume from production code.
41739
+ */
41740
+ __nameLockCountForTests() {
41741
+ return this.nameLocks.size;
41742
+ }
41016
41743
  /** Ensure backend is initialized and unlocked. Audits the unlock. */
41017
41744
  async ensureUnlocked(passphrase) {
41018
41745
  await this.backend.ensureInitialized(passphrase);
@@ -41025,31 +41752,37 @@ var init_broker = __esm({
41025
41752
  );
41026
41753
  }
41027
41754
  async addSecret(name, value) {
41028
- await this.backend.addSecret(name, value);
41029
- this.auditLog.append(
41030
- "l3",
41031
- BROKER_OPS.SECRET_ADDED,
41032
- this.principalIdentityId,
41033
- { secret: name }
41034
- );
41755
+ await this.withNameLock(name, async () => {
41756
+ await this.backend.addSecret(name, value);
41757
+ this.auditLog.append(
41758
+ "l3",
41759
+ BROKER_OPS.SECRET_ADDED,
41760
+ this.principalIdentityId,
41761
+ { secret: name }
41762
+ );
41763
+ });
41035
41764
  }
41036
41765
  async rotateSecret(name, newValue) {
41037
- await this.backend.rotateSecret(name, newValue);
41038
- this.auditLog.append(
41039
- "l3",
41040
- BROKER_OPS.SECRET_ROTATED,
41041
- this.principalIdentityId,
41042
- { secret: name }
41043
- );
41766
+ await this.withNameLock(name, async () => {
41767
+ await this.backend.rotateSecret(name, newValue);
41768
+ this.auditLog.append(
41769
+ "l3",
41770
+ BROKER_OPS.SECRET_ROTATED,
41771
+ this.principalIdentityId,
41772
+ { secret: name }
41773
+ );
41774
+ });
41044
41775
  }
41045
41776
  async deleteSecret(name) {
41046
- await this.backend.deleteSecret(name);
41047
- this.auditLog.append(
41048
- "l3",
41049
- BROKER_OPS.SECRET_DELETED,
41050
- this.principalIdentityId,
41051
- { secret: name }
41052
- );
41777
+ await this.withNameLock(name, async () => {
41778
+ await this.backend.deleteSecret(name);
41779
+ this.auditLog.append(
41780
+ "l3",
41781
+ BROKER_OPS.SECRET_DELETED,
41782
+ this.principalIdentityId,
41783
+ { secret: name }
41784
+ );
41785
+ });
41053
41786
  }
41054
41787
  async listSecretNames() {
41055
41788
  return this.backend.listSecretNames();
@@ -41089,6 +41822,19 @@ var init_broker = __esm({
41089
41822
  liveTokenCount() {
41090
41823
  return this.issuer.liveTokenCount();
41091
41824
  }
41825
+ /**
41826
+ * Drop expired tokens from the in-memory issuer map. Hardening wave 6
41827
+ * finding #86: previously expiry pruning depended on opportunistic
41828
+ * `pruneExpired()` calls; now the cocoon-unlock initialization path
41829
+ * (openBroker -> after backend.ensureInitialized -> after Broker
41830
+ * construction) fires this once so each cocoon-unlock cycle drops
41831
+ * stale bindings before any operator interaction.
41832
+ *
41833
+ * Returns the number of tokens removed. Safe to call repeatedly; idempotent.
41834
+ */
41835
+ pruneExpiredTokens() {
41836
+ return this.issuer.pruneExpired();
41837
+ }
41092
41838
  /**
41093
41839
  * Audit query restricted to broker-scoped operations. Returns entries
41094
41840
  * with their timestamps, op, and result (never the secret value).
@@ -41247,6 +41993,7 @@ async function openBroker(opts = {}) {
41247
41993
  grants,
41248
41994
  principalIdentityId: opts.principalIdentityId ?? "sanctuary-broker"
41249
41995
  });
41996
+ broker.pruneExpiredTokens();
41250
41997
  return {
41251
41998
  broker,
41252
41999
  close: async () => {
@@ -42115,8 +42862,6 @@ var init_health = __esm({
42115
42862
  DEFAULT_TIMEOUT_MS4 = 500;
42116
42863
  }
42117
42864
  });
42118
-
42119
- // src/cli/agents/cli.ts
42120
42865
  function resolveCtx(args) {
42121
42866
  const env = args.env ?? process.env;
42122
42867
  const discoverOpts = {
@@ -42154,6 +42899,8 @@ async function runAgentsCommand(args) {
42154
42899
  return await cmdShow2(rest, ctx);
42155
42900
  case "status":
42156
42901
  return await cmdStatus(rest, ctx);
42902
+ case "config":
42903
+ return await cmdConfig(rest, ctx);
42157
42904
  default:
42158
42905
  ctx.err.write(`Unknown subcommand: ${sub}
42159
42906
  `);
@@ -42169,10 +42916,18 @@ async function runAgentsCommand(args) {
42169
42916
  }
42170
42917
  function printUsage4(s) {
42171
42918
  s.write(`Usage: sanctuary agents <command> [flags]
42919
+ sanctuary agent <command> [flags] (alias)
42172
42920
 
42173
42921
  list [--json] List every tenant visible on this host.
42174
- show <tenant> [--json] Show details for one tenant.
42922
+ show <tenant> [--json] Show details for one tenant (includes
42923
+ approval-redirect state).
42175
42924
  status [--json] One-line-per-tenant running/stopped summary.
42925
+ config <tenant> [opts] Write tenant principal-policy.yaml fields.
42926
+ --approval-redirect=<bool> Toggle cross-harness inbox redirect.
42927
+ --approval-redirect-mode=<replace|notify>
42928
+ Pick replace (bypass underlying channel)
42929
+ or notify (race both paths). Default
42930
+ replace when toggled on.
42176
42931
 
42177
42932
  Options:
42178
42933
  --fortress <path> Scope discovery to a specific storage path
@@ -42274,6 +43029,7 @@ async function cmdShow2(argv, ctx) {
42274
43029
  return 1;
42275
43030
  }
42276
43031
  const probe = await ctx.probe(tenant);
43032
+ const approvalRedirect = await readApprovalRedirectState(tenant);
42277
43033
  const payload = {
42278
43034
  name: tenant.name,
42279
43035
  storage_path: tenant.storage_path,
@@ -42287,7 +43043,8 @@ async function cmdShow2(argv, ctx) {
42287
43043
  running: probe.running,
42288
43044
  status: probe.status,
42289
43045
  reason: probe.reason
42290
- }
43046
+ },
43047
+ approval_redirect: approvalRedirect
42291
43048
  };
42292
43049
  if (hasJsonFlag(argv)) {
42293
43050
  ctx.out.write(JSON.stringify(payload, null, 2) + "\n");
@@ -42331,10 +43088,173 @@ async function cmdShow2(argv, ctx) {
42331
43088
  }
42332
43089
  ctx.out.write(
42333
43090
  `probe: ${probe.running ? "running" : "not-running"}${probe.reason ? ` (${probe.reason})` : ""}
43091
+ `
43092
+ );
43093
+ ctx.out.write(
43094
+ `approval_redirect: ${approvalRedirect.enabled ? `on (${approvalRedirect.mode})` : "off"}
42334
43095
  `
42335
43096
  );
42336
43097
  return 0;
42337
43098
  }
43099
+ async function readApprovalRedirectState(tenant) {
43100
+ const policyPath = path.join(tenant.storage_path, "principal-policy.yaml");
43101
+ try {
43102
+ const content = await promises.readFile(policyPath, "utf-8");
43103
+ const parsed = parsePolicy(content);
43104
+ const cfg = parsed.approval_redirect;
43105
+ if (!cfg) return { enabled: false, mode: "replace" };
43106
+ return {
43107
+ enabled: !!cfg.enabled,
43108
+ mode: cfg.mode === "notify" ? "notify" : "replace"
43109
+ };
43110
+ } catch {
43111
+ return { enabled: false, mode: "replace" };
43112
+ }
43113
+ }
43114
+ function parseBoolFlag(raw) {
43115
+ if (raw === void 0) return null;
43116
+ const v = raw.toLowerCase();
43117
+ if (v === "true" || v === "yes" || v === "on" || v === "1") return true;
43118
+ if (v === "false" || v === "no" || v === "off" || v === "0") return false;
43119
+ return null;
43120
+ }
43121
+ function findFlagValue(argv, name) {
43122
+ for (let i = 0; i < argv.length; i++) {
43123
+ const a = argv[i];
43124
+ if (a === name) {
43125
+ return argv[i + 1];
43126
+ }
43127
+ const eq = `${name}=`;
43128
+ if (a.startsWith(eq)) {
43129
+ return a.slice(eq.length);
43130
+ }
43131
+ }
43132
+ return void 0;
43133
+ }
43134
+ async function cmdConfig(argv, ctx) {
43135
+ const positional = argv.find((a) => !a.startsWith("--"));
43136
+ if (!positional) {
43137
+ ctx.err.write(
43138
+ "Missing tenant. Usage: sanctuary agents config <tenant> --approval-redirect=<bool>\n"
43139
+ );
43140
+ return 2;
43141
+ }
43142
+ const tenant = await findTenant(positional, ctx.discoverOpts);
43143
+ if (!tenant) {
43144
+ ctx.err.write(`sanctuary agents: unknown tenant "${positional}"
43145
+ `);
43146
+ return 1;
43147
+ }
43148
+ const redirectFlag = parseBoolFlag(
43149
+ findFlagValue(argv, "--approval-redirect")
43150
+ );
43151
+ const modeFlag = findFlagValue(argv, "--approval-redirect-mode");
43152
+ if (redirectFlag === null && modeFlag === void 0) {
43153
+ ctx.err.write(
43154
+ "sanctuary agents config: nothing to do. Pass --approval-redirect=<bool> or --approval-redirect-mode=<replace|notify>.\n"
43155
+ );
43156
+ return 2;
43157
+ }
43158
+ if (modeFlag !== void 0 && modeFlag !== "replace" && modeFlag !== "notify") {
43159
+ ctx.err.write(
43160
+ `sanctuary agents config: --approval-redirect-mode must be "replace" or "notify" (got "${modeFlag}")
43161
+ `
43162
+ );
43163
+ return 2;
43164
+ }
43165
+ const current = await readApprovalRedirectState(tenant);
43166
+ const next = {
43167
+ enabled: redirectFlag !== null ? redirectFlag : current.enabled,
43168
+ mode: modeFlag === "notify" || modeFlag === "replace" ? modeFlag : current.mode
43169
+ };
43170
+ await writeApprovalRedirectToPolicyFile(tenant.storage_path, next);
43171
+ if (hasJsonFlag(argv)) {
43172
+ ctx.out.write(
43173
+ JSON.stringify(
43174
+ {
43175
+ tenant: tenant.name,
43176
+ approval_redirect: next
43177
+ },
43178
+ null,
43179
+ 2
43180
+ ) + "\n"
43181
+ );
43182
+ } else {
43183
+ ctx.out.write(
43184
+ `sanctuary agents config: tenant "${tenant.name}" approval_redirect=${next.enabled ? `on (${next.mode})` : "off"}
43185
+ `
43186
+ );
43187
+ ctx.out.write(
43188
+ ` Takes effect on the next gate request for the running server.
43189
+ `
43190
+ );
43191
+ }
43192
+ return 0;
43193
+ }
43194
+ async function writeApprovalRedirectToPolicyFile(storagePath, state) {
43195
+ const policyPath = path.join(storagePath, "principal-policy.yaml");
43196
+ let content;
43197
+ try {
43198
+ content = await promises.readFile(policyPath, "utf-8");
43199
+ } catch (err) {
43200
+ const code = err?.code;
43201
+ if (code !== "ENOENT") throw err;
43202
+ content = await defaultPolicyTextForBootstrap();
43203
+ }
43204
+ const block = renderApprovalRedirectBlock(state);
43205
+ const updated = upsertApprovalRedirectBlock(content, block);
43206
+ await promises.writeFile(policyPath, updated, "utf-8");
43207
+ await promises.chmod(policyPath, 384);
43208
+ }
43209
+ function renderApprovalRedirectBlock(state) {
43210
+ return [
43211
+ "# Approval Redirect (v1.3 WP-V1.3-10 Upsilon-2)",
43212
+ "approval_redirect:",
43213
+ ` enabled: ${state.enabled ? "true" : "false"}`,
43214
+ ` mode: ${state.mode}`
43215
+ ].join("\n");
43216
+ }
43217
+ function upsertApprovalRedirectBlock(content, block) {
43218
+ const lines = content.split("\n");
43219
+ const startIdx = lines.findIndex((l) => l.startsWith("approval_redirect:"));
43220
+ if (startIdx === -1) {
43221
+ const trimmed = content.endsWith("\n") ? content : content + "\n";
43222
+ return trimmed + "\n" + block + "\n";
43223
+ }
43224
+ let blockStart = startIdx;
43225
+ if (blockStart > 0 && lines[blockStart - 1] !== void 0 && lines[blockStart - 1].startsWith("# Approval Redirect")) {
43226
+ blockStart = blockStart - 1;
43227
+ }
43228
+ let blockEnd = startIdx + 1;
43229
+ while (blockEnd < lines.length) {
43230
+ const l = lines[blockEnd];
43231
+ if (l === "") {
43232
+ blockEnd++;
43233
+ continue;
43234
+ }
43235
+ if (/^[A-Za-z0-9#]/.test(l)) {
43236
+ break;
43237
+ }
43238
+ blockEnd++;
43239
+ }
43240
+ const before = lines.slice(0, blockStart);
43241
+ const after = lines.slice(blockEnd);
43242
+ const replaced = [...before, ...block.split("\n"), ...after].join("\n");
43243
+ return replaced.endsWith("\n") ? replaced : replaced + "\n";
43244
+ }
43245
+ async function defaultPolicyTextForBootstrap() {
43246
+ return [
43247
+ "version: 1",
43248
+ "tier1_always_approve:",
43249
+ " - state_export",
43250
+ " - state_import",
43251
+ " - state_delete",
43252
+ "approval_channel:",
43253
+ " type: stderr",
43254
+ " timeout_seconds: 300",
43255
+ ""
43256
+ ].join("\n");
43257
+ }
42338
43258
  async function cmdStatus(argv, ctx) {
42339
43259
  const tenants = await discoverTenants(ctx.discoverOpts);
42340
43260
  const probes = await Promise.all(tenants.map((t) => ctx.probe(t)));
@@ -42375,6 +43295,7 @@ var init_cli5 = __esm({
42375
43295
  "src/cli/agents/cli.ts"() {
42376
43296
  init_discovery();
42377
43297
  init_health();
43298
+ init_loader();
42378
43299
  }
42379
43300
  });
42380
43301
 
@@ -43803,7 +44724,7 @@ async function main() {
43803
44724
  const code = await runIdentityCommand2({ argv: args.slice(1) });
43804
44725
  process.exit(code);
43805
44726
  }
43806
- if (args[0] === "agents") {
44727
+ if (args[0] === "agents" || args[0] === "agent") {
43807
44728
  const { runAgentsCommand: runAgentsCommand2 } = await Promise.resolve().then(() => (init_agents(), agents_exports));
43808
44729
  const code = await runAgentsCommand2({ argv: args.slice(1) });
43809
44730
  process.exit(code);