@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/index.js CHANGED
@@ -4254,6 +4254,10 @@ var DEFAULT_CHANNEL = {
4254
4254
  // SEC-002: auto_deny is not configurable. Timeout always denies.
4255
4255
  // Field omitted intentionally — all channels hardcode deny on timeout.
4256
4256
  };
4257
+ var DEFAULT_APPROVAL_REDIRECT = {
4258
+ enabled: false,
4259
+ mode: "replace"
4260
+ };
4257
4261
  var DEFAULT_POLICY = {
4258
4262
  version: 1,
4259
4263
  tier1_always_approve: [
@@ -4327,6 +4331,7 @@ var DEFAULT_POLICY = {
4327
4331
  "handshake_status",
4328
4332
  "handshake_exchange",
4329
4333
  "handshake_verify_attestation",
4334
+ "handshake_abort",
4330
4335
  "reputation_query_weighted",
4331
4336
  "federation_peers",
4332
4337
  "federation_trust_evaluate",
@@ -4368,7 +4373,8 @@ var DEFAULT_POLICY = {
4368
4373
  "compliance_eu_ai_act_annex_iii_classify"
4369
4374
  // Read-only; rule-based Annex III classifier
4370
4375
  ],
4371
- approval_channel: DEFAULT_CHANNEL
4376
+ approval_channel: DEFAULT_CHANNEL,
4377
+ approval_redirect: DEFAULT_APPROVAL_REDIRECT
4372
4378
  };
4373
4379
  function extractOperationName(toolName) {
4374
4380
  if (toolName.startsWith("proxy/")) {
@@ -4465,9 +4471,35 @@ function validatePolicy(raw) {
4465
4471
  };
4466
4472
  delete merged.auto_deny;
4467
4473
  return merged;
4468
- })()
4474
+ })(),
4475
+ approval_redirect: parseApprovalRedirect(raw.approval_redirect)
4469
4476
  };
4470
4477
  }
4478
+ function parseApprovalRedirect(raw) {
4479
+ if (raw === void 0 || raw === null) {
4480
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4481
+ }
4482
+ if (typeof raw !== "object") {
4483
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4484
+ }
4485
+ const obj = raw;
4486
+ const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
4487
+ const modeRaw = obj.mode;
4488
+ let mode = DEFAULT_APPROVAL_REDIRECT.mode;
4489
+ if (modeRaw !== void 0) {
4490
+ if (modeRaw !== "replace" && modeRaw !== "notify") {
4491
+ throw new Error(
4492
+ `approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
4493
+ );
4494
+ }
4495
+ mode = modeRaw;
4496
+ }
4497
+ const result = { enabled, mode };
4498
+ if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
4499
+ result.per_agent = obj.per_agent;
4500
+ }
4501
+ return result;
4502
+ }
4471
4503
  function generateDefaultPolicyYaml() {
4472
4504
  return `# Sanctuary Principal Policy v1
4473
4505
  # This file controls what your agent can do without asking.
@@ -4546,6 +4578,7 @@ tier3_always_allow:
4546
4578
  - handshake_status
4547
4579
  - handshake_exchange
4548
4580
  - handshake_verify_attestation
4581
+ - handshake_abort
4549
4582
  - reputation_query_weighted
4550
4583
  - federation_peers
4551
4584
  - federation_trust_evaluate
@@ -4580,6 +4613,21 @@ tier3_always_allow:
4580
4613
  approval_channel:
4581
4614
  type: stderr
4582
4615
  timeout_seconds: 300
4616
+
4617
+ # \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
4618
+ # Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
4619
+ # resolve via the unified approval inbox at /api/approval-inbox/* instead
4620
+ # of (or in addition to) the configured approval_channel above.
4621
+ #
4622
+ # mode:
4623
+ # replace: bypass the approval_channel entirely; the gate awaits a
4624
+ # decision from the inbox (default once enabled).
4625
+ # notify: fire BOTH the approval_channel and the inbox; first decision
4626
+ # wins. Right shape for harnesses that cannot fully suppress
4627
+ # their local approval prompt (e.g. Mastra-class).
4628
+ approval_redirect:
4629
+ enabled: false
4630
+ mode: replace
4583
4631
  `;
4584
4632
  }
4585
4633
  var MalformedPrincipalPolicyError = class extends Error {
@@ -19615,6 +19663,169 @@ var ApprovalAggregator = class {
19615
19663
  }
19616
19664
  };
19617
19665
 
19666
+ // src/principal-policy/channels/aggregator-backed-channel.ts
19667
+ var DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
19668
+ function auditEntryIdFor(request) {
19669
+ return `${request.timestamp}:${request.operation}`;
19670
+ }
19671
+ function statusToDecision(entry) {
19672
+ switch (entry.status) {
19673
+ case "approved":
19674
+ return {
19675
+ decision: "approve",
19676
+ decided_by: "human"
19677
+ };
19678
+ case "denied":
19679
+ return {
19680
+ decision: "deny",
19681
+ decided_by: "human"
19682
+ };
19683
+ case "timeout":
19684
+ case "expired":
19685
+ return {
19686
+ decision: "deny",
19687
+ decided_by: "timeout"
19688
+ };
19689
+ default:
19690
+ return null;
19691
+ }
19692
+ }
19693
+ var AggregatorBackedChannel = class {
19694
+ underlying;
19695
+ aggregator;
19696
+ resolveRedirect;
19697
+ replaceModeTimeoutMs;
19698
+ now;
19699
+ constructor(opts) {
19700
+ this.underlying = opts.underlying;
19701
+ this.aggregator = opts.aggregator;
19702
+ this.resolveRedirect = opts.resolveRedirect;
19703
+ this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
19704
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
19705
+ }
19706
+ /** Expose underlying for tests / wire-up reuse. */
19707
+ getUnderlying() {
19708
+ return this.underlying;
19709
+ }
19710
+ async requestApproval(request) {
19711
+ const cfg = this.resolveRedirect(request);
19712
+ if (!cfg.enabled) {
19713
+ return this.underlying.requestApproval(request);
19714
+ }
19715
+ if (cfg.mode === "replace") {
19716
+ return this.awaitAggregatorDecision(request);
19717
+ }
19718
+ return this.notifyMode(request);
19719
+ }
19720
+ /**
19721
+ * `replace` mode. Subscribe to the aggregator's event stream BEFORE
19722
+ * checking already-stored entries (avoids a race where the entry resolves
19723
+ * between list and subscribe). Match incoming events to this request by
19724
+ * audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
19725
+ */
19726
+ async awaitAggregatorDecision(request) {
19727
+ const auditId = auditEntryIdFor(request);
19728
+ return new Promise((resolveOuter) => {
19729
+ let settled = false;
19730
+ let unsubscribe = null;
19731
+ let timeoutHandle = null;
19732
+ const settle = (response) => {
19733
+ if (settled) return;
19734
+ settled = true;
19735
+ if (timeoutHandle) clearTimeout(timeoutHandle);
19736
+ if (unsubscribe) {
19737
+ try {
19738
+ unsubscribe();
19739
+ } catch {
19740
+ }
19741
+ }
19742
+ resolveOuter(response);
19743
+ };
19744
+ const onEvent = (emit) => {
19745
+ if (emit.type !== "resolved") return;
19746
+ if (emit.entry.audit_log_entry_id !== auditId) return;
19747
+ const mapped = statusToDecision(emit.entry);
19748
+ if (!mapped) return;
19749
+ settle({
19750
+ decision: mapped.decision,
19751
+ decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
19752
+ decided_by: mapped.decided_by
19753
+ });
19754
+ };
19755
+ try {
19756
+ unsubscribe = this.aggregator.onEvent(onEvent);
19757
+ } catch (err) {
19758
+ settle({
19759
+ decision: "deny",
19760
+ decided_at: this.now().toISOString(),
19761
+ decided_by: "channel_failure"
19762
+ });
19763
+ throw err instanceof Error ? err : new Error(String(err));
19764
+ }
19765
+ void this.aggregator.list({ limit: 200 }).then((entries) => {
19766
+ for (const entry of entries) {
19767
+ if (entry.audit_log_entry_id !== auditId) continue;
19768
+ const mapped = statusToDecision(entry);
19769
+ if (!mapped) return;
19770
+ settle({
19771
+ decision: mapped.decision,
19772
+ decided_at: entry.resolved_at ?? this.now().toISOString(),
19773
+ decided_by: mapped.decided_by
19774
+ });
19775
+ return;
19776
+ }
19777
+ }).catch(() => {
19778
+ });
19779
+ timeoutHandle = setTimeout(() => {
19780
+ settle({
19781
+ decision: "deny",
19782
+ decided_at: this.now().toISOString(),
19783
+ decided_by: "timeout"
19784
+ });
19785
+ }, this.replaceModeTimeoutMs);
19786
+ });
19787
+ }
19788
+ /**
19789
+ * `notify` mode. Fire the underlying channel and listen on the
19790
+ * aggregator simultaneously; whichever resolves first wins. Both
19791
+ * paths produce identical `ApprovalResponse` shapes; the gate's
19792
+ * downstream audit logging is unchanged.
19793
+ *
19794
+ * On underlying-channel failure, fall through to the aggregator wait
19795
+ * (still bounded by `replaceModeTimeoutMs`). Operator can still
19796
+ * resolve from the inbox even if the dashboard/webhook is down.
19797
+ */
19798
+ async notifyMode(request) {
19799
+ const aggregatorPromise = this.awaitAggregatorDecision(request);
19800
+ let underlyingPromise;
19801
+ try {
19802
+ underlyingPromise = this.underlying.requestApproval(request);
19803
+ } catch (err) {
19804
+ const response = await aggregatorPromise;
19805
+ return response;
19806
+ }
19807
+ return Promise.race([
19808
+ aggregatorPromise,
19809
+ underlyingPromise.catch(
19810
+ () => new Promise(() => {
19811
+ })
19812
+ )
19813
+ ]);
19814
+ }
19815
+ };
19816
+ function makeRedirectResolverFromPolicySupplier(supplier) {
19817
+ return (_request) => {
19818
+ const cfg = supplier().approval_redirect;
19819
+ if (!cfg || cfg.enabled !== true) {
19820
+ return { enabled: false, mode: "replace" };
19821
+ }
19822
+ return {
19823
+ enabled: true,
19824
+ mode: cfg.mode === "notify" ? "notify" : "replace"
19825
+ };
19826
+ };
19827
+ }
19828
+
19618
19829
  // src/principal-policy/tools.ts
19619
19830
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
19620
19831
  return [
@@ -20485,6 +20696,71 @@ function verifyAttestation(attestation, now) {
20485
20696
  };
20486
20697
  }
20487
20698
 
20699
+ // src/handshake/audit.ts
20700
+ var HANDSHAKE_LIFECYCLE_OPS = {
20701
+ INITIATED: "handshake_initiated",
20702
+ COMPLETED: "handshake_completed",
20703
+ FAILED: "handshake_failed",
20704
+ ABORTED: "handshake_aborted"
20705
+ };
20706
+ function auditHandshakeInitiated(auditLog, ctx) {
20707
+ auditLog.append(
20708
+ "l4",
20709
+ HANDSHAKE_LIFECYCLE_OPS.INITIATED,
20710
+ ctx.identity_id,
20711
+ detailsFromContext(ctx),
20712
+ "success"
20713
+ );
20714
+ }
20715
+ function auditHandshakeCompleted(auditLog, ctx) {
20716
+ const details = detailsFromContext(ctx);
20717
+ if (ctx.trust_tier !== void 0) {
20718
+ details.trust_tier = ctx.trust_tier;
20719
+ }
20720
+ auditLog.append(
20721
+ "l4",
20722
+ HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
20723
+ ctx.identity_id,
20724
+ details,
20725
+ "success"
20726
+ );
20727
+ }
20728
+ function auditHandshakeFailed(auditLog, ctx) {
20729
+ const details = detailsFromContext(ctx);
20730
+ details.reason = ctx.reason;
20731
+ if (ctx.error !== void 0) {
20732
+ details.error = ctx.error;
20733
+ }
20734
+ auditLog.append(
20735
+ "l4",
20736
+ HANDSHAKE_LIFECYCLE_OPS.FAILED,
20737
+ ctx.identity_id,
20738
+ details,
20739
+ "failure"
20740
+ );
20741
+ }
20742
+ function auditHandshakeAborted(auditLog, ctx) {
20743
+ const details = detailsFromContext(ctx);
20744
+ details.reason = ctx.reason;
20745
+ auditLog.append(
20746
+ "l4",
20747
+ HANDSHAKE_LIFECYCLE_OPS.ABORTED,
20748
+ ctx.identity_id,
20749
+ details,
20750
+ "failure"
20751
+ );
20752
+ }
20753
+ function detailsFromContext(ctx) {
20754
+ const details = {
20755
+ session_id: ctx.session_id,
20756
+ role: ctx.role
20757
+ };
20758
+ if (ctx.counterparty_id !== void 0) {
20759
+ details.counterparty_id = ctx.counterparty_id;
20760
+ }
20761
+ return details;
20762
+ }
20763
+
20488
20764
  // src/handshake/tools.ts
20489
20765
  function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
20490
20766
  const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
@@ -20518,6 +20794,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20518
20794
  const { challenge, session } = initiateHandshake(shr);
20519
20795
  sessions.set(session.session_id, session);
20520
20796
  auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
20797
+ auditHandshakeInitiated(auditLog, {
20798
+ session_id: session.session_id,
20799
+ role: "initiator",
20800
+ identity_id: shr.body.instance_id
20801
+ });
20521
20802
  return toolResult({
20522
20803
  session_id: session.session_id,
20523
20804
  challenge,
@@ -20557,10 +20838,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20557
20838
  );
20558
20839
  if ("error" in result) {
20559
20840
  auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
20841
+ auditHandshakeFailed(auditLog, {
20842
+ session_id: "unknown",
20843
+ role: "responder",
20844
+ identity_id: shr.body.instance_id,
20845
+ reason: classifyRespondFailure(result.error),
20846
+ error: result.error
20847
+ });
20560
20848
  return toolResult({ error: result.error });
20561
20849
  }
20562
20850
  sessions.set(result.session.session_id, result.session);
20563
20851
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
20852
+ auditHandshakeInitiated(auditLog, {
20853
+ session_id: result.session.session_id,
20854
+ role: "responder",
20855
+ identity_id: shr.body.instance_id,
20856
+ counterparty_id: challenge.shr.body.instance_id
20857
+ });
20564
20858
  let autoPublishResult;
20565
20859
  if (autoPublishHandshakes) {
20566
20860
  autoPublishResult = { attempted: true };
@@ -20668,9 +20962,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20668
20962
  const response = args.response;
20669
20963
  const session = sessions.get(sessionId);
20670
20964
  if (!session) {
20965
+ auditHandshakeFailed(auditLog, {
20966
+ session_id: sessionId,
20967
+ role: "initiator",
20968
+ identity_id: "unknown",
20969
+ reason: "session_unknown",
20970
+ error: `No handshake session found: ${sessionId}`
20971
+ });
20671
20972
  return toolResult({ error: `No handshake session found: ${sessionId}` });
20672
20973
  }
20673
20974
  if (session.state !== "initiated") {
20975
+ auditHandshakeFailed(auditLog, {
20976
+ session_id: sessionId,
20977
+ role: "initiator",
20978
+ identity_id: session.our_shr.body.instance_id,
20979
+ reason: "session_state_mismatch",
20980
+ error: `Session is in state '${session.state}', expected 'initiated'`
20981
+ });
20674
20982
  return toolResult({
20675
20983
  error: `Session is in state '${session.state}', expected 'initiated'`
20676
20984
  });
@@ -20684,6 +20992,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20684
20992
  if ("error" in result) {
20685
20993
  session.state = "failed";
20686
20994
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
20995
+ auditHandshakeFailed(auditLog, {
20996
+ session_id: sessionId,
20997
+ role: "initiator",
20998
+ identity_id: session.our_shr.body.instance_id,
20999
+ reason: classifyCompleteFailure(result.error),
21000
+ error: result.error
21001
+ });
20687
21002
  return toolResult({ error: result.error });
20688
21003
  }
20689
21004
  session.state = "completed";
@@ -20692,6 +21007,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20692
21007
  session.result = result.result;
20693
21008
  handshakeResults.set(result.result.counterparty_id, result.result);
20694
21009
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
21010
+ auditHandshakeCompleted(auditLog, {
21011
+ session_id: sessionId,
21012
+ role: "initiator",
21013
+ identity_id: session.our_shr.body.instance_id,
21014
+ counterparty_id: result.result.counterparty_id,
21015
+ trust_tier: result.result.trust_tier
21016
+ });
20695
21017
  return toolResult({
20696
21018
  completion: result.completion,
20697
21019
  result: result.result,
@@ -20739,6 +21061,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20739
21061
  void 0,
20740
21062
  result.verified ? "success" : "failure"
20741
21063
  );
21064
+ if (result.verified) {
21065
+ auditHandshakeCompleted(auditLog, {
21066
+ session_id: session.session_id,
21067
+ role: "responder",
21068
+ identity_id: session.our_shr.body.instance_id,
21069
+ counterparty_id: result.counterparty_id,
21070
+ trust_tier: result.trust_tier
21071
+ });
21072
+ } else {
21073
+ auditHandshakeFailed(auditLog, {
21074
+ session_id: session.session_id,
21075
+ role: "responder",
21076
+ identity_id: session.our_shr.body.instance_id,
21077
+ counterparty_id: result.counterparty_id,
21078
+ reason: classifyCompleteFailure(result.errors.join("; ")),
21079
+ error: result.errors.join("; ")
21080
+ });
21081
+ }
20742
21082
  return toolResult({ result });
20743
21083
  }
20744
21084
  return toolResult({
@@ -20846,10 +21186,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20846
21186
  _content_trust: "external"
20847
21187
  });
20848
21188
  }
21189
+ },
21190
+ {
21191
+ name: "handshake_abort",
21192
+ 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.",
21193
+ inputSchema: {
21194
+ type: "object",
21195
+ properties: {
21196
+ session_id: {
21197
+ type: "string",
21198
+ description: "Session ID returned from handshake_initiate / handshake_respond."
21199
+ },
21200
+ reason: {
21201
+ type: "string",
21202
+ enum: [
21203
+ "operator_cancelled",
21204
+ "session_timeout",
21205
+ "transport_dropped",
21206
+ "shutdown",
21207
+ "other"
21208
+ ],
21209
+ description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
21210
+ }
21211
+ },
21212
+ required: ["session_id"]
21213
+ },
21214
+ handler: async (args) => {
21215
+ const sessionId = args.session_id;
21216
+ const reason = args.reason ?? "operator_cancelled";
21217
+ const session = sessions.get(sessionId);
21218
+ if (!session) {
21219
+ return toolResult({ error: `No handshake session found: ${sessionId}` });
21220
+ }
21221
+ if (session.state === "completed") {
21222
+ return toolResult({
21223
+ error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
21224
+ });
21225
+ }
21226
+ sessions.delete(sessionId);
21227
+ auditHandshakeAborted(auditLog, {
21228
+ session_id: sessionId,
21229
+ role: session.role,
21230
+ identity_id: session.our_shr.body.instance_id,
21231
+ ...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
21232
+ reason
21233
+ });
21234
+ return toolResult({
21235
+ aborted: true,
21236
+ session_id: sessionId,
21237
+ reason
21238
+ });
21239
+ }
20849
21240
  }
20850
21241
  ];
20851
21242
  return { tools, handshakeResults };
20852
21243
  }
21244
+ function classifyRespondFailure(error) {
21245
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
21246
+ if (error.includes("SHR verification failed")) return "shr_invalid";
21247
+ if (error.includes("No identity available")) return "no_signing_identity";
21248
+ return "other";
21249
+ }
21250
+ function classifyCompleteFailure(error) {
21251
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
21252
+ if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
21253
+ if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
21254
+ if (error.includes("No identity available")) return "no_signing_identity";
21255
+ return "other";
21256
+ }
20853
21257
 
20854
21258
  // src/federation/registry.ts
20855
21259
  var DEFAULT_CAPABILITIES = {
@@ -31575,7 +31979,14 @@ var OPERATOR_CHAT_OPS = {
31575
31979
  * successful thread removal. Body carries thread_id + turn_count of
31576
31980
  * the deleted bundle.
31577
31981
  */
31578
- CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
31982
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
31983
+ /**
31984
+ * Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
31985
+ * the multi-turn coherence fold cannot load the active thread's prior
31986
+ * turns; the concierge degrades to single-turn after emitting. Body
31987
+ * carries thread_id + a stable failure_reason enum.
31988
+ */
31989
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
31579
31990
  };
31580
31991
 
31581
31992
  // src/chat/operator-chat-types.ts
@@ -31584,6 +31995,13 @@ var CONCIERGE_THREAD_KEY = "_fortress";
31584
31995
 
31585
31996
  // src/chat/operator-chat-service.ts
31586
31997
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
31998
+ var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
31999
+ var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
32000
+ var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
32001
+ var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
32002
+ function approxTokenLen(text) {
32003
+ return Math.ceil(text.length / 4);
32004
+ }
31587
32005
  var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
31588
32006
  1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
31589
32007
  2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
@@ -31620,6 +32038,11 @@ var OperatorChatService = class {
31620
32038
  piiFilter;
31621
32039
  conciergeMaxTokens;
31622
32040
  memory;
32041
+ historyWindowTurns;
32042
+ historyFreshnessMs;
32043
+ historyTokenBudget;
32044
+ sessionTtlMs;
32045
+ clock;
31623
32046
  /**
31624
32047
  * In-memory thread_id assigned to the active concierge session.
31625
32048
  * The first sendConcierge call after construction allocates a fresh
@@ -31627,6 +32050,14 @@ var OperatorChatService = class {
31627
32050
  * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
31628
32051
  */
31629
32052
  activeMemoryThreadId;
32053
+ /**
32054
+ * Wall-clock ms of the most recent sendConcierge that touched the
32055
+ * active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
32056
+ * check: a fresh sendConcierge after `sessionTtlMs` of quiet
32057
+ * allocates a new thread_id even though the prior one is still
32058
+ * readable from the memory store.
32059
+ */
32060
+ lastInteractionAt;
31630
32061
  constructor(deps) {
31631
32062
  this.store = deps.store;
31632
32063
  this.auditLog = deps.auditLog;
@@ -31638,6 +32069,11 @@ var OperatorChatService = class {
31638
32069
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
31639
32070
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
31640
32071
  if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
32072
+ this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
32073
+ this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
32074
+ this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
32075
+ this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
32076
+ this.clock = deps.conciergeClock ?? (() => Date.now());
31641
32077
  }
31642
32078
  // ── Concierge ─────────────────────────────────────────────────────────
31643
32079
  /**
@@ -31656,6 +32092,10 @@ var OperatorChatService = class {
31656
32092
  throw new Error("concierge query must not be empty");
31657
32093
  }
31658
32094
  const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
32095
+ const nowMs = this.clock();
32096
+ if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
32097
+ this.activeMemoryThreadId = void 0;
32098
+ }
31659
32099
  const operatorMessage = {
31660
32100
  message_id: randomUUID(),
31661
32101
  surface: "concierge",
@@ -31668,6 +32108,25 @@ var OperatorChatService = class {
31668
32108
  CONCIERGE_THREAD_KEY,
31669
32109
  operatorMessage
31670
32110
  );
32111
+ let priorTurns = [];
32112
+ let memoryReadFailureReason = null;
32113
+ let activeThreadIdForRound;
32114
+ if (this.memory) {
32115
+ activeThreadIdForRound = this.ensureActiveMemoryThread();
32116
+ const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
32117
+ if (result.ok) {
32118
+ const cutoff = nowMs - this.historyFreshnessMs;
32119
+ const fresh = result.turns.filter((t) => {
32120
+ const ts = Date.parse(t.created_at);
32121
+ return Number.isFinite(ts) && ts >= cutoff;
32122
+ });
32123
+ const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
32124
+ priorTurns = recent;
32125
+ } else {
32126
+ memoryReadFailureReason = result.reason;
32127
+ this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
32128
+ }
32129
+ }
31671
32130
  if (this.memory) {
31672
32131
  const threadId = this.ensureActiveMemoryThread();
31673
32132
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
@@ -31689,7 +32148,7 @@ var OperatorChatService = class {
31689
32148
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
31690
32149
  outcome = "substrate_disabled";
31691
32150
  } else {
31692
- const context = await this.assembleConciergeContext();
32151
+ const context = await this.assembleConciergeContext(priorTurns);
31693
32152
  const response = await this.substrateSelector.invokeSummarize(
31694
32153
  "concierge",
31695
32154
  {
@@ -31728,10 +32187,14 @@ var OperatorChatService = class {
31728
32187
  CONCIERGE_THREAD_KEY,
31729
32188
  responseMessage
31730
32189
  );
32190
+ let assistantTurnId;
31731
32191
  if (this.memory) {
31732
32192
  const threadId = this.ensureActiveMemoryThread();
31733
- await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
31734
- });
32193
+ const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
32194
+ if (persisted) assistantTurnId = persisted.turn_id;
32195
+ }
32196
+ if (this.memory && activeThreadIdForRound) {
32197
+ this.lastInteractionAt = nowMs;
31735
32198
  }
31736
32199
  const payload = {
31737
32200
  version: "1.2",
@@ -31744,7 +32207,12 @@ var OperatorChatService = class {
31744
32207
  response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
31745
32208
  substrate: servedBy,
31746
32209
  latency_ms: latencyMs,
31747
- outcome
32210
+ outcome,
32211
+ ...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
32212
+ ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
32213
+ ...this.memory ? {
32214
+ prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
32215
+ } : {}
31748
32216
  };
31749
32217
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
31750
32218
  return {
@@ -31754,6 +32222,25 @@ var OperatorChatService = class {
31754
32222
  outcome
31755
32223
  };
31756
32224
  }
32225
+ /**
32226
+ * Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
32227
+ * out of `sendConcierge` so the read-fold path stays readable. Emits
32228
+ * with `result: "failure"` since the concierge fell back to
32229
+ * single-turn mode for this round-trip.
32230
+ */
32231
+ emitMemoryReadFailed(threadId, reason) {
32232
+ const payload = {
32233
+ version: "1.2",
32234
+ event_id: makeEventId("conc-memfail"),
32235
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
32236
+ identity_id: this.identityId,
32237
+ kind: "operator_concierge_memory_read_failed",
32238
+ surface: "concierge",
32239
+ thread_id: threadId,
32240
+ failure_reason: reason
32241
+ };
32242
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
32243
+ }
31757
32244
  /**
31758
32245
  * Read the persisted concierge thread, oldest message first. Returns
31759
32246
  * an empty array when no thread exists yet.
@@ -31876,6 +32363,11 @@ var OperatorChatService = class {
31876
32363
  * ## Sanctuary reference
31877
32364
  * <static domain reference block>
31878
32365
  *
32366
+ * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
32367
+ * OPERATOR: ...
32368
+ * CONCIERGE: ...
32369
+ * ---
32370
+ *
31879
32371
  * ## Recent activity
31880
32372
  * <recentActivity output>
31881
32373
  *
@@ -31885,37 +32377,69 @@ var OperatorChatService = class {
31885
32377
  * ## Open inbox
31886
32378
  * <openInbox output>
31887
32379
  * ```
31888
- */
31889
- async assembleConciergeContext() {
32380
+ *
32381
+ * The substrate selector ships a `context: string` shape (not a
32382
+ * messages array), so multi-turn coherence is folded as a structured
32383
+ * prior-conversation section with explicit OPERATOR / CONCIERGE
32384
+ * boundaries. Coordinator-CTO guidance: prefer messages-array shape
32385
+ * if available; the v1.2 selector does not expose one, so structured
32386
+ * serialization is the canonical path for v1.3.
32387
+ */
32388
+ async assembleConciergeContext(priorTurns = []) {
31890
32389
  const ref = `## Sanctuary reference
31891
32390
  ${SANCTUARY_DOMAIN_REFERENCE}`;
32391
+ const priorSection = this.formatPriorTurnsSection(priorTurns);
31892
32392
  if (!this.contextProviders) {
31893
- return `${ref}
31894
-
31895
- ## Recent activity
31896
- (no providers wired)
31897
-
31898
- ## Wrapped agents
31899
- (no providers wired)
31900
-
31901
- ## Open inbox
31902
- (no providers wired)`;
32393
+ return [
32394
+ ref,
32395
+ ...priorSection ? [priorSection] : [],
32396
+ "## Recent activity\n(no providers wired)",
32397
+ "## Wrapped agents\n(no providers wired)",
32398
+ "## Open inbox\n(no providers wired)"
32399
+ ].join("\n\n");
31903
32400
  }
31904
32401
  const [activity, agents, inbox] = await Promise.all([
31905
32402
  this.contextProviders.recentActivity(),
31906
32403
  this.contextProviders.agentInventory(),
31907
32404
  this.contextProviders.openInbox()
31908
32405
  ]);
31909
- return `${ref}
31910
-
31911
- ## Recent activity
31912
- ${activity}
31913
-
31914
- ## Wrapped agents
31915
- ${agents}
31916
-
31917
- ## Open inbox
31918
- ${inbox}`;
32406
+ return [
32407
+ ref,
32408
+ ...priorSection ? [priorSection] : [],
32409
+ `## Recent activity
32410
+ ${activity}`,
32411
+ `## Wrapped agents
32412
+ ${agents}`,
32413
+ `## Open inbox
32414
+ ${inbox}`
32415
+ ].join("\n\n");
32416
+ }
32417
+ /**
32418
+ * Render the prior-conversation section with token-budget enforcement
32419
+ * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
32420
+ * section exceeds `historyTokenBudget`. Returns an empty string when
32421
+ * the input is empty or when the budget excludes every turn.
32422
+ */
32423
+ formatPriorTurnsSection(turns) {
32424
+ if (turns.length === 0) return "";
32425
+ const HEADER = "## Prior conversation";
32426
+ const lines = turns.map(formatPriorTurnLine);
32427
+ const headerTokens = approxTokenLen(`${HEADER}
32428
+ `);
32429
+ const sepTokens = approxTokenLen("\n");
32430
+ let runningTokens = headerTokens;
32431
+ let runningLines = [];
32432
+ for (let i = lines.length - 1; i >= 0; i--) {
32433
+ const line = lines[i];
32434
+ const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
32435
+ if (runningTokens + tokens > this.historyTokenBudget) break;
32436
+ runningTokens += tokens;
32437
+ runningLines.push(line);
32438
+ }
32439
+ if (runningLines.length === 0) return "";
32440
+ runningLines = runningLines.reverse();
32441
+ return `${HEADER}
32442
+ ${runningLines.join("\n")}`;
31919
32443
  }
31920
32444
  // ── audit helpers ────────────────────────────────────────────────────
31921
32445
  emit(operation, payload, result) {
@@ -31931,6 +32455,10 @@ ${inbox}`;
31931
32455
  function makeEventId(prefix) {
31932
32456
  return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
31933
32457
  }
32458
+ function formatPriorTurnLine(turn) {
32459
+ const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
32460
+ return `${label}: ${turn.content}`;
32461
+ }
31934
32462
  function hashOf(input) {
31935
32463
  return hashToString(sha256(stringToBytes(input)));
31936
32464
  }
@@ -32099,6 +32627,65 @@ var ConciergeMemoryStore = class {
32099
32627
  }
32100
32628
  return turns;
32101
32629
  }
32630
+ /**
32631
+ * Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
32632
+ * `readThread` collapses every failure mode to an empty array, this
32633
+ * variant returns a discriminated result so the multi-turn fold path
32634
+ * can degrade cleanly + emit `operator_concierge_memory_read_failed`
32635
+ * with a concrete cause.
32636
+ *
32637
+ * - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
32638
+ * - Bundle present, decode + decrypt + schema check pass → ok with turns.
32639
+ * - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
32640
+ * - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
32641
+ * - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
32642
+ * - Storage IO error → `io_failed`.
32643
+ */
32644
+ async readThreadStrict(threadId, opts) {
32645
+ const key = bundleKey(threadId);
32646
+ let raw;
32647
+ try {
32648
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
32649
+ } catch {
32650
+ return { ok: false, reason: "io_failed" };
32651
+ }
32652
+ if (!raw) return { ok: true, turns: [] };
32653
+ if (raw.length > MAX_BUNDLE_BYTES2) {
32654
+ return { ok: false, reason: "oversize_bundle" };
32655
+ }
32656
+ let envelope;
32657
+ try {
32658
+ envelope = JSON.parse(bytesToString(raw));
32659
+ } catch {
32660
+ return { ok: false, reason: "schema_mismatch" };
32661
+ }
32662
+ let plaintext;
32663
+ try {
32664
+ const aad = stringToBytes(threadId);
32665
+ plaintext = decrypt(envelope, this.encryptionKey, aad);
32666
+ } catch {
32667
+ return { ok: false, reason: "decrypt_failed" };
32668
+ }
32669
+ let parsed;
32670
+ try {
32671
+ parsed = JSON.parse(bytesToString(plaintext));
32672
+ } catch {
32673
+ return { ok: false, reason: "schema_mismatch" };
32674
+ }
32675
+ if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
32676
+ if (parsed.thread_id !== threadId) {
32677
+ return { ok: false, reason: "schema_mismatch" };
32678
+ }
32679
+ let turns = parsed.turns;
32680
+ if (opts?.sinceTurnId !== void 0) {
32681
+ const cutoff = opts.sinceTurnId;
32682
+ turns = turns.filter((t) => t.turn_id > cutoff);
32683
+ }
32684
+ if (opts?.limit !== void 0) {
32685
+ turns = turns.slice(0, opts.limit);
32686
+ }
32687
+ return { ok: true, turns };
32688
+ }
32102
32689
  /**
32103
32690
  * Enumerate concierge threads in this fortress with summary metadata.
32104
32691
  * Sorted newest-first by last_turn_at.
@@ -35083,7 +35670,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
35083
35670
  }
35084
35671
  return null;
35085
35672
  }
35086
- async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
35673
+ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
35087
35674
  const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
35088
35675
  if (!destinationSigner) {
35089
35676
  return {
@@ -35145,8 +35732,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
35145
35732
  }
35146
35733
  }
35147
35734
  }
35735
+ let plaintext;
35148
35736
  try {
35149
- const plaintext = decrypt(
35737
+ plaintext = decrypt(
35150
35738
  item.entry.payload,
35151
35739
  deriveNamespaceKey(sourceMasterKey, item.namespace)
35152
35740
  );
@@ -35155,28 +35743,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
35155
35743
  skipped++;
35156
35744
  continue;
35157
35745
  }
35158
- await stateStore.write(
35159
- item.namespace,
35160
- item.key,
35161
- bytesToString(plaintext),
35162
- destinationSigner.identity_id,
35163
- destinationSigner.encrypted_private_key,
35164
- identityEncryptionKey,
35165
- {
35166
- content_type: item.entry.metadata.content_type,
35167
- ttl_seconds: item.entry.metadata.ttl_seconds,
35168
- tags: [
35169
- ...item.entry.metadata.tags ?? [],
35170
- "exit-import",
35171
- `source:${item.entry.kid}`
35172
- ]
35173
- }
35174
- );
35175
- imported++;
35176
35746
  } catch {
35177
35747
  skippedInvalidSig++;
35178
35748
  skipped++;
35749
+ continue;
35179
35750
  }
35751
+ await stateStore.write(
35752
+ item.namespace,
35753
+ item.key,
35754
+ bytesToString(plaintext),
35755
+ destinationSigner.identity_id,
35756
+ destinationSigner.encrypted_private_key,
35757
+ identityEncryptionKey,
35758
+ {
35759
+ content_type: item.entry.metadata.content_type,
35760
+ ttl_seconds: item.entry.metadata.ttl_seconds,
35761
+ tags: [
35762
+ ...item.entry.metadata.tags ?? [],
35763
+ "exit-import",
35764
+ `source:${item.entry.kid}`
35765
+ ]
35766
+ }
35767
+ );
35768
+ imported++;
35769
+ importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
35180
35770
  }
35181
35771
  return {
35182
35772
  status: "rekeyed",
@@ -35187,6 +35777,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
35187
35777
  conflicts
35188
35778
  };
35189
35779
  }
35780
+ async function cleanupStagedPaths(storage, staged) {
35781
+ let removed = 0;
35782
+ const failed = [];
35783
+ for (const loc of staged) {
35784
+ try {
35785
+ const ok = await storage.delete(loc.namespace, loc.key);
35786
+ if (ok) {
35787
+ removed++;
35788
+ } else {
35789
+ failed.push(loc);
35790
+ }
35791
+ } catch {
35792
+ failed.push(loc);
35793
+ }
35794
+ }
35795
+ return { removed, failed };
35796
+ }
35190
35797
  async function stageArtifact(storage, namespace, key, value) {
35191
35798
  await storage.write(namespace, key, jsonBytes(value));
35192
35799
  }
@@ -35311,6 +35918,8 @@ async function importExitBundle(opts) {
35311
35918
  }
35312
35919
  const importId = importIdForManifest(manifest);
35313
35920
  const stagedArtifacts = [];
35921
+ const stagedLocations = [];
35922
+ const importedRekeyEntries = [];
35314
35923
  if (identityArtifact) {
35315
35924
  await stageArtifact(
35316
35925
  opts.storage,
@@ -35319,10 +35928,15 @@ async function importExitBundle(opts) {
35319
35928
  identityArtifact.json
35320
35929
  );
35321
35930
  stagedArtifacts.push("public_identity");
35931
+ stagedLocations.push({
35932
+ namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
35933
+ key: identityArtifact.json.bundle.identity_id
35934
+ });
35322
35935
  }
35323
35936
  if (policySet) {
35324
35937
  await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
35325
35938
  stagedArtifacts.push("policy_set");
35939
+ stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
35326
35940
  }
35327
35941
  if (auditReceipts) {
35328
35942
  await stageArtifact(
@@ -35332,10 +35946,12 @@ async function importExitBundle(opts) {
35332
35946
  auditReceipts.json
35333
35947
  );
35334
35948
  stagedArtifacts.push("audit_receipts");
35949
+ stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
35335
35950
  }
35336
35951
  if (commitments) {
35337
35952
  await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
35338
35953
  stagedArtifacts.push("commitments");
35954
+ stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
35339
35955
  }
35340
35956
  if (placeholderMetadata) {
35341
35957
  await stageArtifact(
@@ -35345,12 +35961,17 @@ async function importExitBundle(opts) {
35345
35961
  placeholderMetadata.json
35346
35962
  );
35347
35963
  stagedArtifacts.push("placeholder_vault_metadata");
35964
+ stagedLocations.push({
35965
+ namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
35966
+ key: importId
35967
+ });
35348
35968
  }
35349
35969
  await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
35350
35970
  manifest: manifest.body,
35351
35971
  verified_at: verification.verified_at,
35352
35972
  activated_at: (/* @__PURE__ */ new Date()).toISOString()
35353
35973
  });
35974
+ stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
35354
35975
  const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
35355
35976
  let reputationResult = {
35356
35977
  imported_attestations: 0,
@@ -35375,26 +35996,57 @@ async function importExitBundle(opts) {
35375
35996
  encryptedState?.json ?? null,
35376
35997
  opts
35377
35998
  );
35378
- const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
35379
- encryptedState.json,
35380
- opts,
35381
- sourceMasterKey,
35382
- publicKeys.byIdentityId
35383
- ) : {
35384
- status: "staged_requires_source_key",
35385
- imported_keys: 0,
35386
- skipped_keys: encryptedState.json.entries.length,
35387
- skipped_invalid_sig: 0,
35388
- skipped_unknown_kid: 0,
35389
- conflicts: conflicts.state_conflicts.length
35390
- } : {
35391
- status: "not_requested",
35392
- imported_keys: 0,
35393
- skipped_keys: 0,
35394
- skipped_invalid_sig: 0,
35395
- skipped_unknown_kid: 0,
35396
- conflicts: 0
35397
- };
35999
+ let stateResult;
36000
+ try {
36001
+ stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
36002
+ encryptedState.json,
36003
+ opts,
36004
+ sourceMasterKey,
36005
+ publicKeys.byIdentityId,
36006
+ importedRekeyEntries
36007
+ ) : {
36008
+ status: "staged_requires_source_key",
36009
+ imported_keys: 0,
36010
+ skipped_keys: encryptedState.json.entries.length,
36011
+ skipped_invalid_sig: 0,
36012
+ skipped_unknown_kid: 0,
36013
+ conflicts: conflicts.state_conflicts.length
36014
+ } : {
36015
+ status: "not_requested",
36016
+ imported_keys: 0,
36017
+ skipped_keys: 0,
36018
+ skipped_invalid_sig: 0,
36019
+ skipped_unknown_kid: 0,
36020
+ conflicts: 0
36021
+ };
36022
+ } catch (err) {
36023
+ const toCleanup = [
36024
+ ...importedRekeyEntries,
36025
+ ...stagedLocations
36026
+ ];
36027
+ const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
36028
+ opts.auditLog.append(
36029
+ "l1",
36030
+ "exit_bundle_rekey_failed_cleanup",
36031
+ manifest.body.identity_binding.identity_id,
36032
+ {
36033
+ import_id: importId,
36034
+ manifest_version: manifest.body.manifest_version,
36035
+ rekey_entries_removed: importedRekeyEntries.length,
36036
+ staged_artifacts_removed: stagedLocations.length,
36037
+ removed_total: cleanup.removed,
36038
+ cleanup_failed_count: cleanup.failed.length,
36039
+ original_error: err instanceof Error ? err.message : String(err)
36040
+ },
36041
+ "failure"
36042
+ );
36043
+ await opts.auditLog.flush();
36044
+ const originalMessage = err instanceof Error ? err.message : String(err);
36045
+ throw new ExitBundleImportError(
36046
+ "REKEY_FAILED_AND_CLEANED",
36047
+ `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).`
36048
+ );
36049
+ }
35398
36050
  opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
35399
36051
  import_id: importId,
35400
36052
  manifest_version: manifest.body.manifest_version,
@@ -36393,7 +37045,6 @@ ${err.message}
36393
37045
  timestamp: alert.timestamp
36394
37046
  });
36395
37047
  } : void 0;
36396
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
36397
37048
  const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
36398
37049
  const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
36399
37050
  const approvalAggregator = new ApprovalAggregator({
@@ -36403,6 +37054,20 @@ ${err.message}
36403
37054
  identityId: aggregatorIdentityId,
36404
37055
  fortressId: fortressIdForAggregator
36405
37056
  });
37057
+ const wrappedApprovalChannel = new AggregatorBackedChannel({
37058
+ underlying: approvalChannel,
37059
+ aggregator: approvalAggregator,
37060
+ resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
37061
+ replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
37062
+ });
37063
+ const gate = new ApprovalGate(
37064
+ policy,
37065
+ baseline,
37066
+ wrappedApprovalChannel,
37067
+ auditLog,
37068
+ injectionDetector,
37069
+ onInjectionAlert
37070
+ );
36406
37071
  gate.setApprovalEventCallback((event) => {
36407
37072
  void approvalAggregator.ingest(event);
36408
37073
  });