@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.cjs CHANGED
@@ -4261,6 +4261,10 @@ var DEFAULT_CHANNEL = {
4261
4261
  // SEC-002: auto_deny is not configurable. Timeout always denies.
4262
4262
  // Field omitted intentionally — all channels hardcode deny on timeout.
4263
4263
  };
4264
+ var DEFAULT_APPROVAL_REDIRECT = {
4265
+ enabled: false,
4266
+ mode: "replace"
4267
+ };
4264
4268
  var DEFAULT_POLICY = {
4265
4269
  version: 1,
4266
4270
  tier1_always_approve: [
@@ -4334,6 +4338,7 @@ var DEFAULT_POLICY = {
4334
4338
  "handshake_status",
4335
4339
  "handshake_exchange",
4336
4340
  "handshake_verify_attestation",
4341
+ "handshake_abort",
4337
4342
  "reputation_query_weighted",
4338
4343
  "federation_peers",
4339
4344
  "federation_trust_evaluate",
@@ -4375,7 +4380,8 @@ var DEFAULT_POLICY = {
4375
4380
  "compliance_eu_ai_act_annex_iii_classify"
4376
4381
  // Read-only; rule-based Annex III classifier
4377
4382
  ],
4378
- approval_channel: DEFAULT_CHANNEL
4383
+ approval_channel: DEFAULT_CHANNEL,
4384
+ approval_redirect: DEFAULT_APPROVAL_REDIRECT
4379
4385
  };
4380
4386
  function extractOperationName(toolName) {
4381
4387
  if (toolName.startsWith("proxy/")) {
@@ -4472,9 +4478,35 @@ function validatePolicy(raw) {
4472
4478
  };
4473
4479
  delete merged.auto_deny;
4474
4480
  return merged;
4475
- })()
4481
+ })(),
4482
+ approval_redirect: parseApprovalRedirect(raw.approval_redirect)
4476
4483
  };
4477
4484
  }
4485
+ function parseApprovalRedirect(raw) {
4486
+ if (raw === void 0 || raw === null) {
4487
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4488
+ }
4489
+ if (typeof raw !== "object") {
4490
+ return { ...DEFAULT_APPROVAL_REDIRECT };
4491
+ }
4492
+ const obj = raw;
4493
+ const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_APPROVAL_REDIRECT.enabled;
4494
+ const modeRaw = obj.mode;
4495
+ let mode = DEFAULT_APPROVAL_REDIRECT.mode;
4496
+ if (modeRaw !== void 0) {
4497
+ if (modeRaw !== "replace" && modeRaw !== "notify") {
4498
+ throw new Error(
4499
+ `approval_redirect.mode must be "replace" or "notify" (got ${JSON.stringify(modeRaw)})`
4500
+ );
4501
+ }
4502
+ mode = modeRaw;
4503
+ }
4504
+ const result = { enabled, mode };
4505
+ if (obj.per_agent !== void 0 && typeof obj.per_agent === "object" && obj.per_agent !== null) {
4506
+ result.per_agent = obj.per_agent;
4507
+ }
4508
+ return result;
4509
+ }
4478
4510
  function generateDefaultPolicyYaml() {
4479
4511
  return `# Sanctuary Principal Policy v1
4480
4512
  # This file controls what your agent can do without asking.
@@ -4553,6 +4585,7 @@ tier3_always_allow:
4553
4585
  - handshake_status
4554
4586
  - handshake_exchange
4555
4587
  - handshake_verify_attestation
4588
+ - handshake_abort
4556
4589
  - reputation_query_weighted
4557
4590
  - federation_peers
4558
4591
  - federation_trust_evaluate
@@ -4587,6 +4620,21 @@ tier3_always_allow:
4587
4620
  approval_channel:
4588
4621
  type: stderr
4589
4622
  timeout_seconds: 300
4623
+
4624
+ # \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
4625
+ # Cross-harness approval-inbox redirect. When enabled, Tier 1/2 approvals
4626
+ # resolve via the unified approval inbox at /api/approval-inbox/* instead
4627
+ # of (or in addition to) the configured approval_channel above.
4628
+ #
4629
+ # mode:
4630
+ # replace: bypass the approval_channel entirely; the gate awaits a
4631
+ # decision from the inbox (default once enabled).
4632
+ # notify: fire BOTH the approval_channel and the inbox; first decision
4633
+ # wins. Right shape for harnesses that cannot fully suppress
4634
+ # their local approval prompt (e.g. Mastra-class).
4635
+ approval_redirect:
4636
+ enabled: false
4637
+ mode: replace
4590
4638
  `;
4591
4639
  }
4592
4640
  var MalformedPrincipalPolicyError = class extends Error {
@@ -19622,6 +19670,169 @@ var ApprovalAggregator = class {
19622
19670
  }
19623
19671
  };
19624
19672
 
19673
+ // src/principal-policy/channels/aggregator-backed-channel.ts
19674
+ var DEFAULT_REPLACE_MODE_TIMEOUT_MS = 5 * 60 * 1e3;
19675
+ function auditEntryIdFor(request) {
19676
+ return `${request.timestamp}:${request.operation}`;
19677
+ }
19678
+ function statusToDecision(entry) {
19679
+ switch (entry.status) {
19680
+ case "approved":
19681
+ return {
19682
+ decision: "approve",
19683
+ decided_by: "human"
19684
+ };
19685
+ case "denied":
19686
+ return {
19687
+ decision: "deny",
19688
+ decided_by: "human"
19689
+ };
19690
+ case "timeout":
19691
+ case "expired":
19692
+ return {
19693
+ decision: "deny",
19694
+ decided_by: "timeout"
19695
+ };
19696
+ default:
19697
+ return null;
19698
+ }
19699
+ }
19700
+ var AggregatorBackedChannel = class {
19701
+ underlying;
19702
+ aggregator;
19703
+ resolveRedirect;
19704
+ replaceModeTimeoutMs;
19705
+ now;
19706
+ constructor(opts) {
19707
+ this.underlying = opts.underlying;
19708
+ this.aggregator = opts.aggregator;
19709
+ this.resolveRedirect = opts.resolveRedirect;
19710
+ this.replaceModeTimeoutMs = opts.replaceModeTimeoutMs ?? DEFAULT_REPLACE_MODE_TIMEOUT_MS;
19711
+ this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
19712
+ }
19713
+ /** Expose underlying for tests / wire-up reuse. */
19714
+ getUnderlying() {
19715
+ return this.underlying;
19716
+ }
19717
+ async requestApproval(request) {
19718
+ const cfg = this.resolveRedirect(request);
19719
+ if (!cfg.enabled) {
19720
+ return this.underlying.requestApproval(request);
19721
+ }
19722
+ if (cfg.mode === "replace") {
19723
+ return this.awaitAggregatorDecision(request);
19724
+ }
19725
+ return this.notifyMode(request);
19726
+ }
19727
+ /**
19728
+ * `replace` mode. Subscribe to the aggregator's event stream BEFORE
19729
+ * checking already-stored entries (avoids a race where the entry resolves
19730
+ * between list and subscribe). Match incoming events to this request by
19731
+ * audit_entry_id. Time out after `replaceModeTimeoutMs` to honor SEC-002.
19732
+ */
19733
+ async awaitAggregatorDecision(request) {
19734
+ const auditId = auditEntryIdFor(request);
19735
+ return new Promise((resolveOuter) => {
19736
+ let settled = false;
19737
+ let unsubscribe = null;
19738
+ let timeoutHandle = null;
19739
+ const settle = (response) => {
19740
+ if (settled) return;
19741
+ settled = true;
19742
+ if (timeoutHandle) clearTimeout(timeoutHandle);
19743
+ if (unsubscribe) {
19744
+ try {
19745
+ unsubscribe();
19746
+ } catch {
19747
+ }
19748
+ }
19749
+ resolveOuter(response);
19750
+ };
19751
+ const onEvent = (emit) => {
19752
+ if (emit.type !== "resolved") return;
19753
+ if (emit.entry.audit_log_entry_id !== auditId) return;
19754
+ const mapped = statusToDecision(emit.entry);
19755
+ if (!mapped) return;
19756
+ settle({
19757
+ decision: mapped.decision,
19758
+ decided_at: emit.entry.resolved_at ?? this.now().toISOString(),
19759
+ decided_by: mapped.decided_by
19760
+ });
19761
+ };
19762
+ try {
19763
+ unsubscribe = this.aggregator.onEvent(onEvent);
19764
+ } catch (err) {
19765
+ settle({
19766
+ decision: "deny",
19767
+ decided_at: this.now().toISOString(),
19768
+ decided_by: "channel_failure"
19769
+ });
19770
+ throw err instanceof Error ? err : new Error(String(err));
19771
+ }
19772
+ void this.aggregator.list({ limit: 200 }).then((entries) => {
19773
+ for (const entry of entries) {
19774
+ if (entry.audit_log_entry_id !== auditId) continue;
19775
+ const mapped = statusToDecision(entry);
19776
+ if (!mapped) return;
19777
+ settle({
19778
+ decision: mapped.decision,
19779
+ decided_at: entry.resolved_at ?? this.now().toISOString(),
19780
+ decided_by: mapped.decided_by
19781
+ });
19782
+ return;
19783
+ }
19784
+ }).catch(() => {
19785
+ });
19786
+ timeoutHandle = setTimeout(() => {
19787
+ settle({
19788
+ decision: "deny",
19789
+ decided_at: this.now().toISOString(),
19790
+ decided_by: "timeout"
19791
+ });
19792
+ }, this.replaceModeTimeoutMs);
19793
+ });
19794
+ }
19795
+ /**
19796
+ * `notify` mode. Fire the underlying channel and listen on the
19797
+ * aggregator simultaneously; whichever resolves first wins. Both
19798
+ * paths produce identical `ApprovalResponse` shapes; the gate's
19799
+ * downstream audit logging is unchanged.
19800
+ *
19801
+ * On underlying-channel failure, fall through to the aggregator wait
19802
+ * (still bounded by `replaceModeTimeoutMs`). Operator can still
19803
+ * resolve from the inbox even if the dashboard/webhook is down.
19804
+ */
19805
+ async notifyMode(request) {
19806
+ const aggregatorPromise = this.awaitAggregatorDecision(request);
19807
+ let underlyingPromise;
19808
+ try {
19809
+ underlyingPromise = this.underlying.requestApproval(request);
19810
+ } catch (err) {
19811
+ const response = await aggregatorPromise;
19812
+ return response;
19813
+ }
19814
+ return Promise.race([
19815
+ aggregatorPromise,
19816
+ underlyingPromise.catch(
19817
+ () => new Promise(() => {
19818
+ })
19819
+ )
19820
+ ]);
19821
+ }
19822
+ };
19823
+ function makeRedirectResolverFromPolicySupplier(supplier) {
19824
+ return (_request) => {
19825
+ const cfg = supplier().approval_redirect;
19826
+ if (!cfg || cfg.enabled !== true) {
19827
+ return { enabled: false, mode: "replace" };
19828
+ }
19829
+ return {
19830
+ enabled: true,
19831
+ mode: cfg.mode === "notify" ? "notify" : "replace"
19832
+ };
19833
+ };
19834
+ }
19835
+
19625
19836
  // src/principal-policy/tools.ts
19626
19837
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
19627
19838
  return [
@@ -20492,6 +20703,71 @@ function verifyAttestation(attestation, now) {
20492
20703
  };
20493
20704
  }
20494
20705
 
20706
+ // src/handshake/audit.ts
20707
+ var HANDSHAKE_LIFECYCLE_OPS = {
20708
+ INITIATED: "handshake_initiated",
20709
+ COMPLETED: "handshake_completed",
20710
+ FAILED: "handshake_failed",
20711
+ ABORTED: "handshake_aborted"
20712
+ };
20713
+ function auditHandshakeInitiated(auditLog, ctx) {
20714
+ auditLog.append(
20715
+ "l4",
20716
+ HANDSHAKE_LIFECYCLE_OPS.INITIATED,
20717
+ ctx.identity_id,
20718
+ detailsFromContext(ctx),
20719
+ "success"
20720
+ );
20721
+ }
20722
+ function auditHandshakeCompleted(auditLog, ctx) {
20723
+ const details = detailsFromContext(ctx);
20724
+ if (ctx.trust_tier !== void 0) {
20725
+ details.trust_tier = ctx.trust_tier;
20726
+ }
20727
+ auditLog.append(
20728
+ "l4",
20729
+ HANDSHAKE_LIFECYCLE_OPS.COMPLETED,
20730
+ ctx.identity_id,
20731
+ details,
20732
+ "success"
20733
+ );
20734
+ }
20735
+ function auditHandshakeFailed(auditLog, ctx) {
20736
+ const details = detailsFromContext(ctx);
20737
+ details.reason = ctx.reason;
20738
+ if (ctx.error !== void 0) {
20739
+ details.error = ctx.error;
20740
+ }
20741
+ auditLog.append(
20742
+ "l4",
20743
+ HANDSHAKE_LIFECYCLE_OPS.FAILED,
20744
+ ctx.identity_id,
20745
+ details,
20746
+ "failure"
20747
+ );
20748
+ }
20749
+ function auditHandshakeAborted(auditLog, ctx) {
20750
+ const details = detailsFromContext(ctx);
20751
+ details.reason = ctx.reason;
20752
+ auditLog.append(
20753
+ "l4",
20754
+ HANDSHAKE_LIFECYCLE_OPS.ABORTED,
20755
+ ctx.identity_id,
20756
+ details,
20757
+ "failure"
20758
+ );
20759
+ }
20760
+ function detailsFromContext(ctx) {
20761
+ const details = {
20762
+ session_id: ctx.session_id,
20763
+ role: ctx.role
20764
+ };
20765
+ if (ctx.counterparty_id !== void 0) {
20766
+ details.counterparty_id = ctx.counterparty_id;
20767
+ }
20768
+ return details;
20769
+ }
20770
+
20495
20771
  // src/handshake/tools.ts
20496
20772
  function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
20497
20773
  const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
@@ -20525,6 +20801,11 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20525
20801
  const { challenge, session } = initiateHandshake(shr);
20526
20802
  sessions.set(session.session_id, session);
20527
20803
  auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
20804
+ auditHandshakeInitiated(auditLog, {
20805
+ session_id: session.session_id,
20806
+ role: "initiator",
20807
+ identity_id: shr.body.instance_id
20808
+ });
20528
20809
  return toolResult({
20529
20810
  session_id: session.session_id,
20530
20811
  challenge,
@@ -20564,10 +20845,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20564
20845
  );
20565
20846
  if ("error" in result) {
20566
20847
  auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
20848
+ auditHandshakeFailed(auditLog, {
20849
+ session_id: "unknown",
20850
+ role: "responder",
20851
+ identity_id: shr.body.instance_id,
20852
+ reason: classifyRespondFailure(result.error),
20853
+ error: result.error
20854
+ });
20567
20855
  return toolResult({ error: result.error });
20568
20856
  }
20569
20857
  sessions.set(result.session.session_id, result.session);
20570
20858
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
20859
+ auditHandshakeInitiated(auditLog, {
20860
+ session_id: result.session.session_id,
20861
+ role: "responder",
20862
+ identity_id: shr.body.instance_id,
20863
+ counterparty_id: challenge.shr.body.instance_id
20864
+ });
20571
20865
  let autoPublishResult;
20572
20866
  if (autoPublishHandshakes) {
20573
20867
  autoPublishResult = { attempted: true };
@@ -20675,9 +20969,23 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20675
20969
  const response = args.response;
20676
20970
  const session = sessions.get(sessionId);
20677
20971
  if (!session) {
20972
+ auditHandshakeFailed(auditLog, {
20973
+ session_id: sessionId,
20974
+ role: "initiator",
20975
+ identity_id: "unknown",
20976
+ reason: "session_unknown",
20977
+ error: `No handshake session found: ${sessionId}`
20978
+ });
20678
20979
  return toolResult({ error: `No handshake session found: ${sessionId}` });
20679
20980
  }
20680
20981
  if (session.state !== "initiated") {
20982
+ auditHandshakeFailed(auditLog, {
20983
+ session_id: sessionId,
20984
+ role: "initiator",
20985
+ identity_id: session.our_shr.body.instance_id,
20986
+ reason: "session_state_mismatch",
20987
+ error: `Session is in state '${session.state}', expected 'initiated'`
20988
+ });
20681
20989
  return toolResult({
20682
20990
  error: `Session is in state '${session.state}', expected 'initiated'`
20683
20991
  });
@@ -20691,6 +20999,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20691
20999
  if ("error" in result) {
20692
21000
  session.state = "failed";
20693
21001
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
21002
+ auditHandshakeFailed(auditLog, {
21003
+ session_id: sessionId,
21004
+ role: "initiator",
21005
+ identity_id: session.our_shr.body.instance_id,
21006
+ reason: classifyCompleteFailure(result.error),
21007
+ error: result.error
21008
+ });
20694
21009
  return toolResult({ error: result.error });
20695
21010
  }
20696
21011
  session.state = "completed";
@@ -20699,6 +21014,13 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20699
21014
  session.result = result.result;
20700
21015
  handshakeResults.set(result.result.counterparty_id, result.result);
20701
21016
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
21017
+ auditHandshakeCompleted(auditLog, {
21018
+ session_id: sessionId,
21019
+ role: "initiator",
21020
+ identity_id: session.our_shr.body.instance_id,
21021
+ counterparty_id: result.result.counterparty_id,
21022
+ trust_tier: result.result.trust_tier
21023
+ });
20702
21024
  return toolResult({
20703
21025
  completion: result.completion,
20704
21026
  result: result.result,
@@ -20746,6 +21068,24 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20746
21068
  void 0,
20747
21069
  result.verified ? "success" : "failure"
20748
21070
  );
21071
+ if (result.verified) {
21072
+ auditHandshakeCompleted(auditLog, {
21073
+ session_id: session.session_id,
21074
+ role: "responder",
21075
+ identity_id: session.our_shr.body.instance_id,
21076
+ counterparty_id: result.counterparty_id,
21077
+ trust_tier: result.trust_tier
21078
+ });
21079
+ } else {
21080
+ auditHandshakeFailed(auditLog, {
21081
+ session_id: session.session_id,
21082
+ role: "responder",
21083
+ identity_id: session.our_shr.body.instance_id,
21084
+ counterparty_id: result.counterparty_id,
21085
+ reason: classifyCompleteFailure(result.errors.join("; ")),
21086
+ error: result.errors.join("; ")
21087
+ });
21088
+ }
20749
21089
  return toolResult({ result });
20750
21090
  }
20751
21091
  return toolResult({
@@ -20853,10 +21193,74 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog, opti
20853
21193
  _content_trust: "external"
20854
21194
  });
20855
21195
  }
21196
+ },
21197
+ {
21198
+ name: "handshake_abort",
21199
+ 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.",
21200
+ inputSchema: {
21201
+ type: "object",
21202
+ properties: {
21203
+ session_id: {
21204
+ type: "string",
21205
+ description: "Session ID returned from handshake_initiate / handshake_respond."
21206
+ },
21207
+ reason: {
21208
+ type: "string",
21209
+ enum: [
21210
+ "operator_cancelled",
21211
+ "session_timeout",
21212
+ "transport_dropped",
21213
+ "shutdown",
21214
+ "other"
21215
+ ],
21216
+ description: "Why the session is being aborted. Defaults to 'operator_cancelled'."
21217
+ }
21218
+ },
21219
+ required: ["session_id"]
21220
+ },
21221
+ handler: async (args) => {
21222
+ const sessionId = args.session_id;
21223
+ const reason = args.reason ?? "operator_cancelled";
21224
+ const session = sessions.get(sessionId);
21225
+ if (!session) {
21226
+ return toolResult({ error: `No handshake session found: ${sessionId}` });
21227
+ }
21228
+ if (session.state === "completed") {
21229
+ return toolResult({
21230
+ error: `Session ${sessionId} already completed; abort is only valid for in-flight sessions`
21231
+ });
21232
+ }
21233
+ sessions.delete(sessionId);
21234
+ auditHandshakeAborted(auditLog, {
21235
+ session_id: sessionId,
21236
+ role: session.role,
21237
+ identity_id: session.our_shr.body.instance_id,
21238
+ ...session.their_shr ? { counterparty_id: session.their_shr.body.instance_id } : {},
21239
+ reason
21240
+ });
21241
+ return toolResult({
21242
+ aborted: true,
21243
+ session_id: sessionId,
21244
+ reason
21245
+ });
21246
+ }
20856
21247
  }
20857
21248
  ];
20858
21249
  return { tools, handshakeResults };
20859
21250
  }
21251
+ function classifyRespondFailure(error) {
21252
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
21253
+ if (error.includes("SHR verification failed")) return "shr_invalid";
21254
+ if (error.includes("No identity available")) return "no_signing_identity";
21255
+ return "other";
21256
+ }
21257
+ function classifyCompleteFailure(error) {
21258
+ if (error.includes("Unsupported protocol version")) return "protocol_version_unsupported";
21259
+ if (error.includes("SHR verification failed") || error.includes("SHR")) return "shr_invalid";
21260
+ if (error.includes("nonce signature is invalid")) return "nonce_signature_invalid";
21261
+ if (error.includes("No identity available")) return "no_signing_identity";
21262
+ return "other";
21263
+ }
20860
21264
 
20861
21265
  // src/federation/registry.ts
20862
21266
  var DEFAULT_CAPABILITIES = {
@@ -31582,7 +31986,14 @@ var OPERATOR_CHAT_OPS = {
31582
31986
  * successful thread removal. Body carries thread_id + turn_count of
31583
31987
  * the deleted bundle.
31584
31988
  */
31585
- CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted"
31989
+ CONCIERGE_THREAD_DELETED: "operator_concierge_thread_deleted",
31990
+ /**
31991
+ * Concierge memory fold-read failed (WP-V1.3-9 Tau-2). Emitted when
31992
+ * the multi-turn coherence fold cannot load the active thread's prior
31993
+ * turns; the concierge degrades to single-turn after emitting. Body
31994
+ * carries thread_id + a stable failure_reason enum.
31995
+ */
31996
+ CONCIERGE_MEMORY_READ_FAILED: "operator_concierge_memory_read_failed"
31586
31997
  };
31587
31998
 
31588
31999
  // src/chat/operator-chat-types.ts
@@ -31591,6 +32002,13 @@ var CONCIERGE_THREAD_KEY = "_fortress";
31591
32002
 
31592
32003
  // src/chat/operator-chat-service.ts
31593
32004
  var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
32005
+ var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
32006
+ var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
32007
+ var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
32008
+ var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
32009
+ function approxTokenLen(text) {
32010
+ return Math.ceil(text.length / 4);
32011
+ }
31594
32012
  var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
31595
32013
  1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
31596
32014
  2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
@@ -31627,6 +32045,11 @@ var OperatorChatService = class {
31627
32045
  piiFilter;
31628
32046
  conciergeMaxTokens;
31629
32047
  memory;
32048
+ historyWindowTurns;
32049
+ historyFreshnessMs;
32050
+ historyTokenBudget;
32051
+ sessionTtlMs;
32052
+ clock;
31630
32053
  /**
31631
32054
  * In-memory thread_id assigned to the active concierge session.
31632
32055
  * The first sendConcierge call after construction allocates a fresh
@@ -31634,6 +32057,14 @@ var OperatorChatService = class {
31634
32057
  * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
31635
32058
  */
31636
32059
  activeMemoryThreadId;
32060
+ /**
32061
+ * Wall-clock ms of the most recent sendConcierge that touched the
32062
+ * active session thread. Drives the WP-V1.3-9 Tau-2 session-TTL
32063
+ * check: a fresh sendConcierge after `sessionTtlMs` of quiet
32064
+ * allocates a new thread_id even though the prior one is still
32065
+ * readable from the memory store.
32066
+ */
32067
+ lastInteractionAt;
31637
32068
  constructor(deps) {
31638
32069
  this.store = deps.store;
31639
32070
  this.auditLog = deps.auditLog;
@@ -31645,6 +32076,11 @@ var OperatorChatService = class {
31645
32076
  if (deps.conciergePiiFilter) this.piiFilter = deps.conciergePiiFilter;
31646
32077
  this.conciergeMaxTokens = deps.conciergeMaxTokens ?? DEFAULT_CONCIERGE_MAX_TOKENS;
31647
32078
  if (deps.conciergeMemory) this.memory = deps.conciergeMemory;
32079
+ this.historyWindowTurns = deps.conciergeHistoryWindowTurns !== void 0 && deps.conciergeHistoryWindowTurns > 0 ? deps.conciergeHistoryWindowTurns : DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS;
32080
+ this.historyFreshnessMs = deps.conciergeHistoryFreshnessMs !== void 0 && deps.conciergeHistoryFreshnessMs > 0 ? deps.conciergeHistoryFreshnessMs : DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS;
32081
+ this.historyTokenBudget = deps.conciergeHistoryTokenBudget !== void 0 && deps.conciergeHistoryTokenBudget > 0 ? deps.conciergeHistoryTokenBudget : DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET;
32082
+ this.sessionTtlMs = deps.conciergeSessionTtlMs !== void 0 && deps.conciergeSessionTtlMs > 0 ? deps.conciergeSessionTtlMs : DEFAULT_CONCIERGE_SESSION_TTL_MS;
32083
+ this.clock = deps.conciergeClock ?? (() => Date.now());
31648
32084
  }
31649
32085
  // ── Concierge ─────────────────────────────────────────────────────────
31650
32086
  /**
@@ -31663,6 +32099,10 @@ var OperatorChatService = class {
31663
32099
  throw new Error("concierge query must not be empty");
31664
32100
  }
31665
32101
  const filterResult = this.piiFilter ? this.piiFilter.filter(trimmed) : { filtered: trimmed, redactions: 0 };
32102
+ const nowMs = this.clock();
32103
+ if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
32104
+ this.activeMemoryThreadId = void 0;
32105
+ }
31666
32106
  const operatorMessage = {
31667
32107
  message_id: crypto.randomUUID(),
31668
32108
  surface: "concierge",
@@ -31675,6 +32115,25 @@ var OperatorChatService = class {
31675
32115
  CONCIERGE_THREAD_KEY,
31676
32116
  operatorMessage
31677
32117
  );
32118
+ let priorTurns = [];
32119
+ let memoryReadFailureReason = null;
32120
+ let activeThreadIdForRound;
32121
+ if (this.memory) {
32122
+ activeThreadIdForRound = this.ensureActiveMemoryThread();
32123
+ const result = await this.memory.readThreadStrict(activeThreadIdForRound).catch(() => ({ ok: false, reason: "io_failed" }));
32124
+ if (result.ok) {
32125
+ const cutoff = nowMs - this.historyFreshnessMs;
32126
+ const fresh = result.turns.filter((t) => {
32127
+ const ts = Date.parse(t.created_at);
32128
+ return Number.isFinite(ts) && ts >= cutoff;
32129
+ });
32130
+ const recent = fresh.length > this.historyWindowTurns ? fresh.slice(fresh.length - this.historyWindowTurns) : fresh;
32131
+ priorTurns = recent;
32132
+ } else {
32133
+ memoryReadFailureReason = result.reason;
32134
+ this.emitMemoryReadFailed(activeThreadIdForRound, result.reason);
32135
+ }
32136
+ }
31678
32137
  if (this.memory) {
31679
32138
  const threadId = this.ensureActiveMemoryThread();
31680
32139
  await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
@@ -31696,7 +32155,7 @@ var OperatorChatService = class {
31696
32155
  conciergeBody = "Concierge unavailable. The chosen substrate does not support summarization. Pick a different substrate in the Policy center.";
31697
32156
  outcome = "substrate_disabled";
31698
32157
  } else {
31699
- const context = await this.assembleConciergeContext();
32158
+ const context = await this.assembleConciergeContext(priorTurns);
31700
32159
  const response = await this.substrateSelector.invokeSummarize(
31701
32160
  "concierge",
31702
32161
  {
@@ -31735,10 +32194,14 @@ var OperatorChatService = class {
31735
32194
  CONCIERGE_THREAD_KEY,
31736
32195
  responseMessage
31737
32196
  );
32197
+ let assistantTurnId;
31738
32198
  if (this.memory) {
31739
32199
  const threadId = this.ensureActiveMemoryThread();
31740
- await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => {
31741
- });
32200
+ const persisted = await this.memory.appendTurn(threadId, "assistant", conciergeBody).catch(() => void 0);
32201
+ if (persisted) assistantTurnId = persisted.turn_id;
32202
+ }
32203
+ if (this.memory && activeThreadIdForRound) {
32204
+ this.lastInteractionAt = nowMs;
31742
32205
  }
31743
32206
  const payload = {
31744
32207
  version: "1.2",
@@ -31751,7 +32214,12 @@ var OperatorChatService = class {
31751
32214
  response_hash: outcome === "ok" ? hashOf(conciergeBody) : null,
31752
32215
  substrate: servedBy,
31753
32216
  latency_ms: latencyMs,
31754
- outcome
32217
+ outcome,
32218
+ ...activeThreadIdForRound !== void 0 ? { thread_id: activeThreadIdForRound } : {},
32219
+ ...assistantTurnId !== void 0 ? { turn_index: assistantTurnId } : {},
32220
+ ...this.memory ? {
32221
+ prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
32222
+ } : {}
31755
32223
  };
31756
32224
  this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
31757
32225
  return {
@@ -31761,6 +32229,25 @@ var OperatorChatService = class {
31761
32229
  outcome
31762
32230
  };
31763
32231
  }
32232
+ /**
32233
+ * Emit the WP-V1.3-9 Tau-2 graceful-degradation audit event. Pulled
32234
+ * out of `sendConcierge` so the read-fold path stays readable. Emits
32235
+ * with `result: "failure"` since the concierge fell back to
32236
+ * single-turn mode for this round-trip.
32237
+ */
32238
+ emitMemoryReadFailed(threadId, reason) {
32239
+ const payload = {
32240
+ version: "1.2",
32241
+ event_id: makeEventId("conc-memfail"),
32242
+ emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
32243
+ identity_id: this.identityId,
32244
+ kind: "operator_concierge_memory_read_failed",
32245
+ surface: "concierge",
32246
+ thread_id: threadId,
32247
+ failure_reason: reason
32248
+ };
32249
+ this.emit(OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED, payload, "failure");
32250
+ }
31764
32251
  /**
31765
32252
  * Read the persisted concierge thread, oldest message first. Returns
31766
32253
  * an empty array when no thread exists yet.
@@ -31883,6 +32370,11 @@ var OperatorChatService = class {
31883
32370
  * ## Sanctuary reference
31884
32371
  * <static domain reference block>
31885
32372
  *
32373
+ * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
32374
+ * OPERATOR: ...
32375
+ * CONCIERGE: ...
32376
+ * ---
32377
+ *
31886
32378
  * ## Recent activity
31887
32379
  * <recentActivity output>
31888
32380
  *
@@ -31892,37 +32384,69 @@ var OperatorChatService = class {
31892
32384
  * ## Open inbox
31893
32385
  * <openInbox output>
31894
32386
  * ```
31895
- */
31896
- async assembleConciergeContext() {
32387
+ *
32388
+ * The substrate selector ships a `context: string` shape (not a
32389
+ * messages array), so multi-turn coherence is folded as a structured
32390
+ * prior-conversation section with explicit OPERATOR / CONCIERGE
32391
+ * boundaries. Coordinator-CTO guidance: prefer messages-array shape
32392
+ * if available; the v1.2 selector does not expose one, so structured
32393
+ * serialization is the canonical path for v1.3.
32394
+ */
32395
+ async assembleConciergeContext(priorTurns = []) {
31897
32396
  const ref = `## Sanctuary reference
31898
32397
  ${SANCTUARY_DOMAIN_REFERENCE}`;
32398
+ const priorSection = this.formatPriorTurnsSection(priorTurns);
31899
32399
  if (!this.contextProviders) {
31900
- return `${ref}
31901
-
31902
- ## Recent activity
31903
- (no providers wired)
31904
-
31905
- ## Wrapped agents
31906
- (no providers wired)
31907
-
31908
- ## Open inbox
31909
- (no providers wired)`;
32400
+ return [
32401
+ ref,
32402
+ ...priorSection ? [priorSection] : [],
32403
+ "## Recent activity\n(no providers wired)",
32404
+ "## Wrapped agents\n(no providers wired)",
32405
+ "## Open inbox\n(no providers wired)"
32406
+ ].join("\n\n");
31910
32407
  }
31911
32408
  const [activity, agents, inbox] = await Promise.all([
31912
32409
  this.contextProviders.recentActivity(),
31913
32410
  this.contextProviders.agentInventory(),
31914
32411
  this.contextProviders.openInbox()
31915
32412
  ]);
31916
- return `${ref}
31917
-
31918
- ## Recent activity
31919
- ${activity}
31920
-
31921
- ## Wrapped agents
31922
- ${agents}
31923
-
31924
- ## Open inbox
31925
- ${inbox}`;
32413
+ return [
32414
+ ref,
32415
+ ...priorSection ? [priorSection] : [],
32416
+ `## Recent activity
32417
+ ${activity}`,
32418
+ `## Wrapped agents
32419
+ ${agents}`,
32420
+ `## Open inbox
32421
+ ${inbox}`
32422
+ ].join("\n\n");
32423
+ }
32424
+ /**
32425
+ * Render the prior-conversation section with token-budget enforcement
32426
+ * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
32427
+ * section exceeds `historyTokenBudget`. Returns an empty string when
32428
+ * the input is empty or when the budget excludes every turn.
32429
+ */
32430
+ formatPriorTurnsSection(turns) {
32431
+ if (turns.length === 0) return "";
32432
+ const HEADER = "## Prior conversation";
32433
+ const lines = turns.map(formatPriorTurnLine);
32434
+ const headerTokens = approxTokenLen(`${HEADER}
32435
+ `);
32436
+ const sepTokens = approxTokenLen("\n");
32437
+ let runningTokens = headerTokens;
32438
+ let runningLines = [];
32439
+ for (let i = lines.length - 1; i >= 0; i--) {
32440
+ const line = lines[i];
32441
+ const tokens = approxTokenLen(line) + (runningLines.length > 0 ? sepTokens : 0);
32442
+ if (runningTokens + tokens > this.historyTokenBudget) break;
32443
+ runningTokens += tokens;
32444
+ runningLines.push(line);
32445
+ }
32446
+ if (runningLines.length === 0) return "";
32447
+ runningLines = runningLines.reverse();
32448
+ return `${HEADER}
32449
+ ${runningLines.join("\n")}`;
31926
32450
  }
31927
32451
  // ── audit helpers ────────────────────────────────────────────────────
31928
32452
  emit(operation, payload, result) {
@@ -31938,6 +32462,10 @@ ${inbox}`;
31938
32462
  function makeEventId(prefix) {
31939
32463
  return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
31940
32464
  }
32465
+ function formatPriorTurnLine(turn) {
32466
+ const label = turn.role === "user" ? "OPERATOR" : "CONCIERGE";
32467
+ return `${label}: ${turn.content}`;
32468
+ }
31941
32469
  function hashOf(input) {
31942
32470
  return hashToString(sha256.sha256(stringToBytes(input)));
31943
32471
  }
@@ -32106,6 +32634,65 @@ var ConciergeMemoryStore = class {
32106
32634
  }
32107
32635
  return turns;
32108
32636
  }
32637
+ /**
32638
+ * Read turns with explicit failure surfacing (WP-V1.3-9 Tau-2). Where
32639
+ * `readThread` collapses every failure mode to an empty array, this
32640
+ * variant returns a discriminated result so the multi-turn fold path
32641
+ * can degrade cleanly + emit `operator_concierge_memory_read_failed`
32642
+ * with a concrete cause.
32643
+ *
32644
+ * - No bundle on disk → `{ ok: true, turns: [] }` (a fresh thread).
32645
+ * - Bundle present, decode + decrypt + schema check pass → ok with turns.
32646
+ * - Bundle present, oversize → `{ ok: false, reason: "oversize_bundle" }`.
32647
+ * - Bundle present, decryption fails → `{ ok: false, reason: "decrypt_failed" }`.
32648
+ * - Bundle present, schema mismatch (version / thread_id) → `schema_mismatch`.
32649
+ * - Storage IO error → `io_failed`.
32650
+ */
32651
+ async readThreadStrict(threadId, opts) {
32652
+ const key = bundleKey(threadId);
32653
+ let raw;
32654
+ try {
32655
+ raw = await this.storage.read(CONCIERGE_MEMORY_NAMESPACE, key);
32656
+ } catch {
32657
+ return { ok: false, reason: "io_failed" };
32658
+ }
32659
+ if (!raw) return { ok: true, turns: [] };
32660
+ if (raw.length > MAX_BUNDLE_BYTES2) {
32661
+ return { ok: false, reason: "oversize_bundle" };
32662
+ }
32663
+ let envelope;
32664
+ try {
32665
+ envelope = JSON.parse(bytesToString(raw));
32666
+ } catch {
32667
+ return { ok: false, reason: "schema_mismatch" };
32668
+ }
32669
+ let plaintext;
32670
+ try {
32671
+ const aad = stringToBytes(threadId);
32672
+ plaintext = decrypt(envelope, this.encryptionKey, aad);
32673
+ } catch {
32674
+ return { ok: false, reason: "decrypt_failed" };
32675
+ }
32676
+ let parsed;
32677
+ try {
32678
+ parsed = JSON.parse(bytesToString(plaintext));
32679
+ } catch {
32680
+ return { ok: false, reason: "schema_mismatch" };
32681
+ }
32682
+ if (parsed.version !== 1) return { ok: false, reason: "schema_mismatch" };
32683
+ if (parsed.thread_id !== threadId) {
32684
+ return { ok: false, reason: "schema_mismatch" };
32685
+ }
32686
+ let turns = parsed.turns;
32687
+ if (opts?.sinceTurnId !== void 0) {
32688
+ const cutoff = opts.sinceTurnId;
32689
+ turns = turns.filter((t) => t.turn_id > cutoff);
32690
+ }
32691
+ if (opts?.limit !== void 0) {
32692
+ turns = turns.slice(0, opts.limit);
32693
+ }
32694
+ return { ok: true, turns };
32695
+ }
32109
32696
  /**
32110
32697
  * Enumerate concierge threads in this fortress with summary metadata.
32111
32698
  * Sorted newest-first by last_turn_at.
@@ -35090,7 +35677,7 @@ async function resolveSourceMasterKey(encryptedState, opts) {
35090
35677
  }
35091
35678
  return null;
35092
35679
  }
35093
- async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
35680
+ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId, importedRekeyEntries) {
35094
35681
  const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
35095
35682
  if (!destinationSigner) {
35096
35683
  return {
@@ -35152,8 +35739,9 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
35152
35739
  }
35153
35740
  }
35154
35741
  }
35742
+ let plaintext;
35155
35743
  try {
35156
- const plaintext = decrypt(
35744
+ plaintext = decrypt(
35157
35745
  item.entry.payload,
35158
35746
  deriveNamespaceKey(sourceMasterKey, item.namespace)
35159
35747
  );
@@ -35162,28 +35750,30 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
35162
35750
  skipped++;
35163
35751
  continue;
35164
35752
  }
35165
- await stateStore.write(
35166
- item.namespace,
35167
- item.key,
35168
- bytesToString(plaintext),
35169
- destinationSigner.identity_id,
35170
- destinationSigner.encrypted_private_key,
35171
- identityEncryptionKey,
35172
- {
35173
- content_type: item.entry.metadata.content_type,
35174
- ttl_seconds: item.entry.metadata.ttl_seconds,
35175
- tags: [
35176
- ...item.entry.metadata.tags ?? [],
35177
- "exit-import",
35178
- `source:${item.entry.kid}`
35179
- ]
35180
- }
35181
- );
35182
- imported++;
35183
35753
  } catch {
35184
35754
  skippedInvalidSig++;
35185
35755
  skipped++;
35756
+ continue;
35186
35757
  }
35758
+ await stateStore.write(
35759
+ item.namespace,
35760
+ item.key,
35761
+ bytesToString(plaintext),
35762
+ destinationSigner.identity_id,
35763
+ destinationSigner.encrypted_private_key,
35764
+ identityEncryptionKey,
35765
+ {
35766
+ content_type: item.entry.metadata.content_type,
35767
+ ttl_seconds: item.entry.metadata.ttl_seconds,
35768
+ tags: [
35769
+ ...item.entry.metadata.tags ?? [],
35770
+ "exit-import",
35771
+ `source:${item.entry.kid}`
35772
+ ]
35773
+ }
35774
+ );
35775
+ imported++;
35776
+ importedRekeyEntries?.push({ namespace: item.namespace, key: item.key });
35187
35777
  }
35188
35778
  return {
35189
35779
  status: "rekeyed",
@@ -35194,6 +35784,23 @@ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIde
35194
35784
  conflicts
35195
35785
  };
35196
35786
  }
35787
+ async function cleanupStagedPaths(storage, staged) {
35788
+ let removed = 0;
35789
+ const failed = [];
35790
+ for (const loc of staged) {
35791
+ try {
35792
+ const ok = await storage.delete(loc.namespace, loc.key);
35793
+ if (ok) {
35794
+ removed++;
35795
+ } else {
35796
+ failed.push(loc);
35797
+ }
35798
+ } catch {
35799
+ failed.push(loc);
35800
+ }
35801
+ }
35802
+ return { removed, failed };
35803
+ }
35197
35804
  async function stageArtifact(storage, namespace, key, value) {
35198
35805
  await storage.write(namespace, key, jsonBytes(value));
35199
35806
  }
@@ -35318,6 +35925,8 @@ async function importExitBundle(opts) {
35318
35925
  }
35319
35926
  const importId = importIdForManifest(manifest);
35320
35927
  const stagedArtifacts = [];
35928
+ const stagedLocations = [];
35929
+ const importedRekeyEntries = [];
35321
35930
  if (identityArtifact) {
35322
35931
  await stageArtifact(
35323
35932
  opts.storage,
@@ -35326,10 +35935,15 @@ async function importExitBundle(opts) {
35326
35935
  identityArtifact.json
35327
35936
  );
35328
35937
  stagedArtifacts.push("public_identity");
35938
+ stagedLocations.push({
35939
+ namespace: EXIT_PUBLIC_IDENTITIES_NAMESPACE,
35940
+ key: identityArtifact.json.bundle.identity_id
35941
+ });
35329
35942
  }
35330
35943
  if (policySet) {
35331
35944
  await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
35332
35945
  stagedArtifacts.push("policy_set");
35946
+ stagedLocations.push({ namespace: EXIT_POLICY_SETS_NAMESPACE, key: importId });
35333
35947
  }
35334
35948
  if (auditReceipts) {
35335
35949
  await stageArtifact(
@@ -35339,10 +35953,12 @@ async function importExitBundle(opts) {
35339
35953
  auditReceipts.json
35340
35954
  );
35341
35955
  stagedArtifacts.push("audit_receipts");
35956
+ stagedLocations.push({ namespace: EXIT_AUDIT_RECEIPTS_NAMESPACE, key: importId });
35342
35957
  }
35343
35958
  if (commitments) {
35344
35959
  await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
35345
35960
  stagedArtifacts.push("commitments");
35961
+ stagedLocations.push({ namespace: EXIT_COMMITMENTS_NAMESPACE, key: importId });
35346
35962
  }
35347
35963
  if (placeholderMetadata) {
35348
35964
  await stageArtifact(
@@ -35352,12 +35968,17 @@ async function importExitBundle(opts) {
35352
35968
  placeholderMetadata.json
35353
35969
  );
35354
35970
  stagedArtifacts.push("placeholder_vault_metadata");
35971
+ stagedLocations.push({
35972
+ namespace: EXIT_PLACEHOLDER_METADATA_NAMESPACE,
35973
+ key: importId
35974
+ });
35355
35975
  }
35356
35976
  await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
35357
35977
  manifest: manifest.body,
35358
35978
  verified_at: verification.verified_at,
35359
35979
  activated_at: (/* @__PURE__ */ new Date()).toISOString()
35360
35980
  });
35981
+ stagedLocations.push({ namespace: EXIT_IMPORT_NAMESPACE, key: importId });
35361
35982
  const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
35362
35983
  let reputationResult = {
35363
35984
  imported_attestations: 0,
@@ -35382,26 +36003,57 @@ async function importExitBundle(opts) {
35382
36003
  encryptedState?.json ?? null,
35383
36004
  opts
35384
36005
  );
35385
- const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
35386
- encryptedState.json,
35387
- opts,
35388
- sourceMasterKey,
35389
- publicKeys.byIdentityId
35390
- ) : {
35391
- status: "staged_requires_source_key",
35392
- imported_keys: 0,
35393
- skipped_keys: encryptedState.json.entries.length,
35394
- skipped_invalid_sig: 0,
35395
- skipped_unknown_kid: 0,
35396
- conflicts: conflicts.state_conflicts.length
35397
- } : {
35398
- status: "not_requested",
35399
- imported_keys: 0,
35400
- skipped_keys: 0,
35401
- skipped_invalid_sig: 0,
35402
- skipped_unknown_kid: 0,
35403
- conflicts: 0
35404
- };
36006
+ let stateResult;
36007
+ try {
36008
+ stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
36009
+ encryptedState.json,
36010
+ opts,
36011
+ sourceMasterKey,
36012
+ publicKeys.byIdentityId,
36013
+ importedRekeyEntries
36014
+ ) : {
36015
+ status: "staged_requires_source_key",
36016
+ imported_keys: 0,
36017
+ skipped_keys: encryptedState.json.entries.length,
36018
+ skipped_invalid_sig: 0,
36019
+ skipped_unknown_kid: 0,
36020
+ conflicts: conflicts.state_conflicts.length
36021
+ } : {
36022
+ status: "not_requested",
36023
+ imported_keys: 0,
36024
+ skipped_keys: 0,
36025
+ skipped_invalid_sig: 0,
36026
+ skipped_unknown_kid: 0,
36027
+ conflicts: 0
36028
+ };
36029
+ } catch (err) {
36030
+ const toCleanup = [
36031
+ ...importedRekeyEntries,
36032
+ ...stagedLocations
36033
+ ];
36034
+ const cleanup = await cleanupStagedPaths(opts.storage, toCleanup);
36035
+ opts.auditLog.append(
36036
+ "l1",
36037
+ "exit_bundle_rekey_failed_cleanup",
36038
+ manifest.body.identity_binding.identity_id,
36039
+ {
36040
+ import_id: importId,
36041
+ manifest_version: manifest.body.manifest_version,
36042
+ rekey_entries_removed: importedRekeyEntries.length,
36043
+ staged_artifacts_removed: stagedLocations.length,
36044
+ removed_total: cleanup.removed,
36045
+ cleanup_failed_count: cleanup.failed.length,
36046
+ original_error: err instanceof Error ? err.message : String(err)
36047
+ },
36048
+ "failure"
36049
+ );
36050
+ await opts.auditLog.flush();
36051
+ const originalMessage = err instanceof Error ? err.message : String(err);
36052
+ throw new ExitBundleImportError(
36053
+ "REKEY_FAILED_AND_CLEANED",
36054
+ `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).`
36055
+ );
36056
+ }
35405
36057
  opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
35406
36058
  import_id: importId,
35407
36059
  manifest_version: manifest.body.manifest_version,
@@ -36400,7 +37052,6 @@ ${err.message}
36400
37052
  timestamp: alert.timestamp
36401
37053
  });
36402
37054
  } : void 0;
36403
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
36404
37055
  const fortressIdForAggregator = fortressIdFromStoragePath(config.storage_path);
36405
37056
  const aggregatorIdentityId = identityManager.getPrimaryIdentityId() ?? `fortress:${config.storage_path}`;
36406
37057
  const approvalAggregator = new ApprovalAggregator({
@@ -36410,6 +37061,20 @@ ${err.message}
36410
37061
  identityId: aggregatorIdentityId,
36411
37062
  fortressId: fortressIdForAggregator
36412
37063
  });
37064
+ const wrappedApprovalChannel = new AggregatorBackedChannel({
37065
+ underlying: approvalChannel,
37066
+ aggregator: approvalAggregator,
37067
+ resolveRedirect: makeRedirectResolverFromPolicySupplier(() => policy),
37068
+ replaceModeTimeoutMs: policy.approval_channel.timeout_seconds * 1e3
37069
+ });
37070
+ const gate = new ApprovalGate(
37071
+ policy,
37072
+ baseline,
37073
+ wrappedApprovalChannel,
37074
+ auditLog,
37075
+ injectionDetector,
37076
+ onInjectionAlert
37077
+ );
36413
37078
  gate.setApprovalEventCallback((event) => {
36414
37079
  void approvalAggregator.ingest(event);
36415
37080
  });